idml 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6cc7058e67830e2dc6ea523ea6ca054f702ce29b51989a19deb023e5a840bee
4
- data.tar.gz: 434b5af5ea49b9f2b48728b100b612ff0cd784079abfd6d419dbeb16554b3ebb
3
+ metadata.gz: 2f60e795f4325dcc4006a28915a18baf7069e21767694ca670c41f84db62d98d
4
+ data.tar.gz: 4f1f4846d41050b518c8add338ce431951b45ea58891b2c56154b47e59478bc7
5
5
  SHA512:
6
- metadata.gz: a59cabfafa44138cb40ed8b6e493396c110ffe43728c0d0ea94d8ef4d8039efad635ba23660852b1040b58ce7141b6e301bf27ecb96599efd6d72069e6d81a95
7
- data.tar.gz: d142ebcfc9eb6591827c72d3cd33303ee3a4c70637031b03622ea1355d453cac4aac1654f60ee7a3ee54a050755e88fb84c32c01df11df802c434442c0f9a4e2
6
+ metadata.gz: bbf7caac497ce2ab01a8c35af7bad78a24b7a706db6baa582b7d416cae1ce4995f18a3903379c4e994ab49ca1f28efd33e162821dead95250ae8248c041d16cd
7
+ data.tar.gz: ed5d2ae0abf40c0c6bf101d3c05ca35f59bc48aefed14366de7f5ac0f15979ff458ca7cf8493d012c4f6a78b0c1889815f987ea3dc9f24971f31ae34583f6fc4
@@ -1,6 +1,17 @@
1
1
  # TODO PDF 103: Drop caps (DropCapLines, DropCapCharacters)
2
2
 
3
- ## Status: OPENgap identified in 2026-08-08 audit
3
+ ## Status: PARTIAL`Render::DropCap` helper computes drop-cap
4
+ geometry (font_size = base × lines, width via font_metrics.measure_text,
5
+ height = leading × lines). TextFrameRenderer.emit_drop_cap emits the
6
+ drop cap as an enlarged glyph at the paragraph's top-left when the
7
+ paragraph declares DropCapLines/DropCapCharacters > 0 and is the
8
+ chain's first paragraph.
9
+
10
+ Wrap-around text is NOT yet implemented — the drop cap renders as an
11
+ enlarged glyph; subsequent paragraph text flows normally and may
12
+ overlap the drop cap. Real wrap-around (indent lines 1..M to the
13
+ right of the drop cap) requires per-line wrap-width control in
14
+ VerticalLayout, which is a bigger refactor. Tracked as follow-up.
4
15
 
5
16
  ## Problem
6
17
 
@@ -0,0 +1,53 @@
1
+ # TODO PDF 112: TextFrame vertical justification (TopAlign / CenterAlign / BottomAlign / JustifyAlign)
2
+
3
+ ## Status: DONE for TopAlign / CenterAlign / BottomAlign. JustifyAlign
4
+ defers to TopAlign (inter-paragraph space distribution not yet
5
+ implemented). Content height is approximated by treating each run
6
+ as one line — wrap may produce more lines, so slack is over-estimated;
7
+ we'd rather under-offset than overflow.
8
+
9
+ ## Problem
10
+
11
+ `Elements::TextFramePreference` carries `VerticalJustification`
12
+ (attribute VerticalJustification on TextFramePreference_Object in
13
+ Styles.rnc). Possible values: TopAlign, CenterAlign, BottomAlign,
14
+ JustifyAlign.
15
+
16
+ Today TextFrameRenderer always starts text at the top of the frame
17
+ (`cursor_y = box[:y] + box[:height] - inset_top`). For
18
+ `VerticalJustification="CenterAlign"` documents, the entire text
19
+ block should be vertically centered within the frame's text area;
20
+ for `BottomAlign`, text sits at the bottom.
21
+
22
+ This is a common layout requirement — callouts, captions, footers,
23
+ and sidebars often use bottom or center vertical justification.
24
+
25
+ ## What needs to happen
26
+
27
+ 1. `TextFrameRenderer#engine_render` reads
28
+ `frame.text_frame_preference.vertical_justification`.
29
+ 2. Compute the total content height (sum of all paragraph heights +
30
+ space_before/after).
31
+ 3. If VerticalJustification is CenterAlign: top-offset =
32
+ (frame_text_area_height - content_height) / 2.
33
+ 4. If BottomAlign: top-offset = (frame_text_area_height - content_height).
34
+ 5. Adjust the initial `cursor_y` by top-offset before laying out
35
+ the first paragraph.
36
+ 6. JustifyAlign: distribute extra vertical space between paragraphs
37
+ (more complex — defer).
38
+
39
+ ## Acceptance criteria
40
+
41
+ - [ ] TextFrame with VerticalJustification="CenterAlign" centers
42
+ text vertically in the frame.
43
+ - [ ] TextFrame with VerticalJustification="BottomAlign" aligns
44
+ text to the bottom.
45
+ - [ ] TextFrame with VerticalJustification="TopAlign" (default)
46
+ renders as today.
47
+ - [ ] Spec coverage per value.
48
+
49
+ ## Dependencies
50
+
51
+ - TODO 108 (multi-frame story flow) — content_height calculation
52
+ needs to consider all paragraphs that will render, which the
53
+ chain state already tracks.
@@ -0,0 +1,48 @@
1
+ # TODO PDF 113: Multi-column text frames (TextColumnCount, TextColumnGutter)
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-10 audit
4
+
5
+ ## Problem
6
+
7
+ `Elements::TextFramePreference` declares:
8
+ - `TextColumnCount` — number of text columns in the frame (1-40)
9
+ - `TextColumnGutter` — gap between columns (in points)
10
+ - `TextColumnFixedWidth` — width of each column when uniform
11
+ - `UseFixedColumnWidth` — boolean
12
+ - `UseFlexibleColumnWidth` — boolean
13
+ - `TextColumnMaxWidth` — max column width when flexible
14
+
15
+ Real-world documents frequently use multi-column layouts
16
+ (magazines, newspapers, technical reports, brochures). Today the
17
+ renderer treats every text frame as a single column, ignoring
18
+ TextColumnCount > 1.
19
+
20
+ Effect: multi-column stories render as one wide column that over-
21
+ laps adjacent content. The PDF is unreadable for multi-column docs.
22
+
23
+ ## What needs to happen
24
+
25
+ 1. `TextFrameRenderer` reads `text_frame_preference.text_column_count`.
26
+ 2. When > 1: split the frame's text area into N columns separated
27
+ by `text_column_gutter`.
28
+ 3. Render text column-by-column: column 1 fills top-to-bottom,
29
+ overflow continues at top of column 2, etc.
30
+ 4. StoryThreader chain state threads across columns WITHIN a frame
31
+ before crossing to the next frame.
32
+ 5. Optional: support TextColumnFixedWidth vs flexible-width
33
+ distribution.
34
+
35
+ Architecture: this combines TODO 108 (chain flow) with column
36
+ awareness. Each column acts like a mini-frame in the chain.
37
+
38
+ ## Acceptance criteria
39
+
40
+ - [ ] TextFrame with TextColumnCount=2 renders two side-by-side columns.
41
+ - [ ] Text overflows from column 1 → column 2 → next frame.
42
+ - [ ] TextColumnGutter honored as the gap between columns.
43
+ - [ ] Single-column frames render as today.
44
+
45
+ ## Dependencies
46
+
47
+ - TODO 108 (multi-frame story flow) — column-to-column flow uses
48
+ the same chain state mechanism.
@@ -0,0 +1,50 @@
1
+ # TODO PDF 114: Text wrap around shapes (TextWrapPreference)
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-10 audit
4
+
5
+ ## Problem
6
+
7
+ IDML page items (Rectangle, Polygon, GraphicLine, Group, Image, EPS)
8
+ can declare a `<TextWrapPreference>` child that controls how text
9
+ in neighboring TextFrames wraps around the item. Settings include:
10
+
11
+ - WrapMode: None / Jump / Next / Bounding Box / Shape / Drop Cap
12
+ - Contour: the wrap boundary (could differ from the visual shape)
13
+ - Inverse: text wraps INSIDE the contour (rare)
14
+ - OffsetTop/Left/Bottom/Right: additional margin around the contour
15
+
16
+ Real documents wrap text around images and callout boxes constantly.
17
+ Without text wrap support, overlapping items produce unreadable
18
+ output — text from one frame runs over a graphic in another.
19
+
20
+ Today TextFrameRenderer knows nothing about TextWrapPreference on
21
+ other page items. It renders into the frame's full geometric
22
+ rectangle regardless of overlapping wrap contours.
23
+
24
+ ## What needs to happen
25
+
26
+ 1. `Render::TextWrapResolver` reads TextWrapPreference from all
27
+ page items in the spread and indexes their contours by Self.
28
+ 2. Per TextFrame: compute the intersection of the frame's text area
29
+ with all wrap contours in the spread.
30
+ 3. TextFrameRenderer queries the resolver per text line; each line's
31
+ wrap width is reduced by the overlap on the left and right.
32
+ 4. Lines that fall entirely inside a wrap region are skipped.
33
+
34
+ This is a substantial feature — wrap computation is non-trivial
35
+ (geometry intersection, contour following). Most documents use a
36
+ simple Bounding Box mode (rectangle subtract), which is tractable.
37
+
38
+ ## Acceptance criteria
39
+
40
+ - [ ] TextWrapMode="BoundingBox" with an image inside the frame's
41
+ bounds reduces wrap width on affected lines.
42
+ - [ ] TextWrapMode="None" (default) renders as today.
43
+ - [ ] OffsetTop/Left/Bottom/Right honored (expands the contour by
44
+ the offsets).
45
+
46
+ ## Dependencies
47
+
48
+ - TODO 108 (multi-frame story flow) — wrap-aware wrap width needs
49
+ to combine with chain-threaded line layout.
50
+ - TODO 113 (multi-column) — wrap interacts with column boundaries.
@@ -0,0 +1,49 @@
1
+ # TODO PDF 115: Bullet and numbered list rendering
2
+
3
+ ## Status: DONE for inline marker emission. Hanging-indent layout
4
+ (bullet in a left gutter with wrapped lines indented to align with
5
+ text after marker) is a follow-up — currently the marker sits inline
6
+ before the first run's text.
7
+
8
+ ## Problem
9
+
10
+ `Elements::ParagraphStyleRange` carries a large set of bullets and
11
+ numbering attributes (BulletsAndNumberingListType, NumberingExpression,
12
+ BulletsTextAfter, NumberingLevel, NumberingContinue, NumberingStartAt,
13
+ BulletsAlignment, NumberingAlignment, BulletCharacterType,
14
+ BulletCharacterValue, RestartPolicy, LowerLevel, UpperLevel).
15
+
16
+ The renderer ignores all of these. Lists appear in the PDF as plain
17
+ paragraphs without bullet markers or numbering.
18
+
19
+ Real documents (technical specs, tutorials, procedures, contracts)
20
+ use lists extensively.
21
+
22
+ ## What needs to happen
23
+
24
+ 1. Read BulletsAndNumberingListType from PSR:
25
+ - "BulletList" → emit a bullet glyph at the paragraph's start
26
+ - "NumberedList" → emit a number/letter at the paragraph's start
27
+ 2. Apply BulletsAlignment (left/center/right) within a hanging-indent
28
+ gutter.
29
+ 3. Auto-increment numbering per NumberingLevel +
30
+ NumberingContinue + NumberingStartAt rules.
31
+ 4. Honor RestartPolicy for sub-list restart behavior.
32
+ 5. BulletCharacterValue picks the glyph (Unicode codepoint).
33
+
34
+ Architecture: list markers prepend the paragraph's first run; the
35
+ renderer needs an in-memory counter per numbering chain (similar
36
+ to StoryChainController).
37
+
38
+ ## Acceptance criteria
39
+
40
+ - [ ] PSR with BulletsAndNumberingListType="BulletList" renders a
41
+ bullet glyph before the paragraph's first character.
42
+ - [ ] PSR with BulletsAndNumberingListType="NumberedList" renders
43
+ a sequential number, auto-incremented per chain.
44
+ - [ ] BulletsTextAfter (e.g., ". ", ") ") honored as the marker's
45
+ suffix.
46
+
47
+ ## Dependencies
48
+
49
+ - None — independent feature work on PSR.
@@ -0,0 +1,83 @@
1
+ # frozen_string: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Computes drop-cap geometry for a paragraph. IDML's DropCapLines
6
+ # (M) and DropCapCharacters (N) declare that the first N
7
+ # characters of the paragraph should render at M-lines-tall and
8
+ # the next M text lines should wrap to the right of the drop cap.
9
+ #
10
+ # The helper returns a `DropCapLayout` struct with the drop cap
11
+ # text, font size, width (for wrap offset), and total height.
12
+ # Returns nil when the paragraph doesn't declare drop caps.
13
+ module DropCap
14
+ DropCapLayout = Struct.new(
15
+ :text, :width, :height, :font_size, :wrap_offset, :lines,
16
+ keyword_init: true
17
+ )
18
+
19
+ # Returns a DropCapLayout for the paragraph, or nil when drop
20
+ # caps aren't declared. `font_metrics` measures the drop-cap
21
+ # text's natural width. `base_size` is the paragraph's normal
22
+ # font size (drop cap is enlarged relative to it).
23
+ def self.layout(paragraph, font_metrics:, base_size:, leading:)
24
+ return nil unless active?(paragraph)
25
+ return nil unless font_metrics
26
+
27
+ chars = paragraph.drop_cap_characters.to_i
28
+ lines = paragraph.drop_cap_lines.to_i
29
+ return nil if chars <= 0 || lines <= 0
30
+
31
+ build_layout(paragraph, font_metrics, base_size, leading, chars, lines)
32
+ end
33
+
34
+ # True when the paragraph declares an active drop cap
35
+ # (both DropCapLines and DropCapCharacters are positive integers).
36
+ def self.active?(paragraph)
37
+ return false unless paragraph
38
+ return false unless paragraph.drop_cap_lines.to_i.positive?
39
+
40
+ paragraph.drop_cap_characters.to_i.positive?
41
+ end
42
+
43
+ # Pulls the first N characters from the paragraph's first run.
44
+ # If the first run is shorter than N, takes from subsequent runs
45
+ # until N chars are accumulated (rare — drop caps typically use
46
+ # 1-2 chars).
47
+ def self.extract_drop_cap_text(paragraph, n)
48
+ runs = paragraph.runs
49
+ return nil if runs.empty?
50
+
51
+ collected = +""
52
+ runs.each do |run|
53
+ needed = n - collected.length
54
+ break if needed <= 0
55
+
56
+ collected << run.text[0, needed]
57
+ break if collected.length >= n
58
+ end
59
+ collected
60
+ end
61
+
62
+ def self.build_layout(paragraph, font_metrics, base_size, leading,
63
+ chars, lines)
64
+ text = extract_drop_cap_text(paragraph, chars)
65
+ return nil if text.nil? || text.empty?
66
+
67
+ font_size = base_size * lines
68
+ width = font_metrics.measure_text(text, font_size).to_f
69
+ return nil unless width&.positive?
70
+
71
+ DropCapLayout.new(
72
+ text: text,
73
+ width: width,
74
+ height: leading * lines,
75
+ font_size: font_size,
76
+ wrap_offset: width,
77
+ lines: lines,
78
+ )
79
+ end
80
+ private_class_method :build_layout
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Produces a list-marker string for a paragraph (bullet glyph or
6
+ # numbered-list expression). IDML stores the resolved marker
7
+ # directly on the PSR (BulletCharacterValue as a Unicode codepoint,
8
+ # NumberingExpression as the rendered text), so the renderer
9
+ # doesn't need to maintain its own counter.
10
+ #
11
+ # Used by `StyleResolver` to prepend the marker to the paragraph's
12
+ # first run's text. The marker becomes part of the run's natural
13
+ # text flow — no special positioning needed for MVP.
14
+ #
15
+ # Hanging-indent layout (bullet aligned in a left gutter with
16
+ # wrapped lines indented to align with text after the marker) is
17
+ # a follow-up; for now the marker sits inline before the text.
18
+ module ListMarker
19
+ DEFAULT_BULLET = "•".freeze # BULLET
20
+ DEFAULT_TEXT_AFTER = "\t".freeze
21
+
22
+ # Returns the marker string for the paragraph, or nil when the
23
+ # paragraph isn't part of a list.
24
+ def self.marker_for(paragraph)
25
+ return nil unless paragraph
26
+
27
+ case paragraph.bullets_and_numbering_list_type
28
+ when "BulletList" then bullet_marker(paragraph)
29
+ when "NumberedList" then numbered_marker(paragraph)
30
+ end
31
+ end
32
+
33
+ def self.bullet_marker(paragraph)
34
+ bullet = bullet_char(paragraph)
35
+ suffix = text_after(paragraph)
36
+ "#{bullet}#{suffix}"
37
+ end
38
+ private_class_method :bullet_marker
39
+
40
+ def self.numbered_marker(paragraph)
41
+ expr = paragraph.numbering_expression
42
+ return nil if expr.nil? || expr.empty?
43
+
44
+ suffix = text_after(paragraph)
45
+ "#{expr}#{suffix}"
46
+ end
47
+ private_class_method :numbered_marker
48
+
49
+ def self.bullet_char(paragraph)
50
+ codepoint = paragraph.bullet_character_value
51
+ return DEFAULT_BULLET unless codepoint&.positive?
52
+
53
+ [codepoint].pack("U")
54
+ end
55
+ private_class_method :bullet_char
56
+
57
+ def self.text_after(paragraph)
58
+ paragraph.bullets_text_after || DEFAULT_TEXT_AFTER
59
+ end
60
+ private_class_method :text_after
61
+ end
62
+ end
63
+ end
@@ -157,8 +157,9 @@ module Idml
157
157
  # next frame in the chain.
158
158
  def self.engine_render(canvas, state, context, box, font)
159
159
  layout_frame = layout_frame(context.item, box)
160
- cursor_y = box[:y] + box[:height] - (layout_frame.inset_top || 0)
160
+ top_y = box[:y] + box[:height] - (layout_frame.inset_top || 0)
161
161
  bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
162
+ cursor_y = vertical_justify_top(top_y, bottom_limit, state, context)
162
163
  char_cursor = state.char_cursor
163
164
 
164
165
  if state.current_paragraph
@@ -174,6 +175,77 @@ module Idml
174
175
  end
175
176
  private_class_method :engine_render
176
177
 
178
+ # Computes the starting cursor_y for vertical justification.
179
+ # IDML's TextFramePreference.VerticalJustification can be:
180
+ # - TopAlign (default): start at frame top, no offset
181
+ # - CenterAlign: center the content block vertically
182
+ # - BottomAlign: align content to the frame bottom
183
+ # - JustifyAlign: distribute extra space between paragraphs
184
+ # (deferred — falls back to TopAlign behavior)
185
+ #
186
+ # Content height is estimated by walking paragraphs and
187
+ # summing leading × lines + space_before/after. Without
188
+ # shaping/wrapping we can't know exact line count, so we
189
+ # approximate by treating each run as one line.
190
+ def self.vertical_justify_top(top_y, bottom_limit, state, context)
191
+ justification = text_frame_vertical_justification(context)
192
+ return top_y unless %w[CenterAlign BottomAlign].include?(justification)
193
+
194
+ content_height = estimate_content_height(state)
195
+ available = top_y - bottom_limit
196
+ slack = [available - content_height, 0].max
197
+ return top_y if slack.zero?
198
+
199
+ case justification
200
+ when "CenterAlign" then top_y - (slack / 2)
201
+ when "BottomAlign" then top_y - slack
202
+ else top_y
203
+ end
204
+ end
205
+ private_class_method :vertical_justify_top
206
+
207
+ def self.text_frame_vertical_justification(context)
208
+ pref = context.item&.text_frame_preference&.first
209
+ pref&.vertical_justification
210
+ end
211
+ private_class_method :text_frame_vertical_justification
212
+
213
+ # Estimates total renderable content height for vertical
214
+ # justification. Walks paragraphs and runs, summing leading
215
+ # per run plus paragraph space_before/after. Approximation:
216
+ # treats each run as one line (wrap may produce more lines,
217
+ # which would over-estimate slack; acceptable for
218
+ # justification since we'd rather under-offset than overflow).
219
+ def self.estimate_content_height(state)
220
+ paragraphs = paragraphs_for_estimate(state)
221
+ return 0 if paragraphs.empty?
222
+
223
+ paragraphs.sum do |paragraph|
224
+ paragraph_height(paragraph)
225
+ end
226
+ end
227
+ private_class_method :estimate_content_height
228
+
229
+ def self.paragraphs_for_estimate(state)
230
+ result = []
231
+ result << state.current_paragraph if state.current_paragraph
232
+ result + state.paragraphs
233
+ end
234
+ private_class_method :paragraphs_for_estimate
235
+
236
+ def self.paragraph_height(paragraph)
237
+ size = paragraph.runs.first&.point_size || DEFAULT_SIZE
238
+ leading = leading_for(paragraph, size)
239
+ line_count = [paragraph.runs.length, 1].max
240
+ (leading * line_count) + space_before_after(paragraph)
241
+ end
242
+ private_class_method :paragraph_height
243
+
244
+ def self.space_before_after(paragraph)
245
+ (paragraph.space_before || 0) + (paragraph.space_after || 0)
246
+ end
247
+ private_class_method :space_before_after
248
+
177
249
  def self.resume_paragraph(canvas, state, context, layout_frame, font,
178
250
  cursor_y, bottom_limit, char_cursor)
179
251
  paragraph = state.current_paragraph
@@ -233,6 +305,8 @@ module Idml
233
305
 
234
306
  emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
235
307
  frame_left, frame_right, first_paragraph)
308
+ emit_drop_cap(canvas, paragraph, context, frame_left, cursor_y,
309
+ first_paragraph)
236
310
 
237
311
  rendered_count = 0
238
312
  runs.each do |run|
@@ -260,6 +334,33 @@ module Idml
260
334
  end
261
335
  private_class_method :render_runs_for_paragraph
262
336
 
337
+ # Renders the drop cap for a paragraph (when declared and the
338
+ # first paragraph of the chain). Wrap-around text is not yet
339
+ # implemented — the drop cap renders as an enlarged glyph at
340
+ # the paragraph's top-left; subsequent text may overlap it.
341
+ # Marked as a follow-up in TODO 103.
342
+ def self.emit_drop_cap(canvas, paragraph, context, frame_left,
343
+ cursor_y, first_paragraph)
344
+ return nil unless first_paragraph
345
+ return nil unless DropCap.active?(paragraph)
346
+
347
+ base_size = paragraph.runs.first&.point_size || DEFAULT_SIZE
348
+ leading = leading_for(paragraph, base_size)
349
+ drop_cap = DropCap.layout(paragraph,
350
+ font_metrics: context.font_metrics,
351
+ base_size: base_size, leading: leading)
352
+ return nil unless drop_cap
353
+
354
+ drop_at_y = cursor_y - drop_cap.font_size + (base_size * 0.25)
355
+ canvas.text(
356
+ drop_cap.text,
357
+ at: [frame_left, drop_at_y],
358
+ font: font_for_run(paragraph.runs.first, context),
359
+ size: drop_cap.font_size,
360
+ )
361
+ end
362
+ private_class_method :emit_drop_cap
363
+
263
364
  def self.emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
264
365
  frame_left, frame_right,
265
366
  first_paragraph)
@@ -54,6 +54,12 @@ module Idml
54
54
  :left_indent,
55
55
  :right_indent,
56
56
  :auto_leading,
57
+ :drop_cap_lines,
58
+ :bullets_and_numbering_list_type,
59
+ :bullet_character_value,
60
+ :bullets_text_after,
61
+ :numbering_expression,
62
+ :drop_cap_characters,
57
63
  # RuleAbove / RuleBelow — paragraph rules (horizontal lines
58
64
  # above/below the paragraph block). Driven by PSR attributes.
59
65
  :rule_above,
@@ -113,7 +119,7 @@ module Idml
113
119
  runs = csr_runs(psr, condition_filter: condition_filter)
114
120
  next if runs.empty?
115
121
 
116
- Paragraph.new(
122
+ paragraph = Paragraph.new(
117
123
  runs: runs,
118
124
  alignment: runs.first.alignment,
119
125
  space_before: psr.space_before,
@@ -122,6 +128,12 @@ module Idml
122
128
  left_indent: psr.left_indent,
123
129
  right_indent: psr.right_indent,
124
130
  auto_leading: psr.auto_leading,
131
+ drop_cap_lines: psr.drop_cap_lines,
132
+ drop_cap_characters: psr.drop_cap_characters,
133
+ bullets_and_numbering_list_type: psr.bullets_and_numbering_list_type,
134
+ bullet_character_value: psr.bullet_character_value,
135
+ bullets_text_after: psr.bullets_text_after,
136
+ numbering_expression: psr.numbering_expression,
125
137
  rule_above: psr.rule_above,
126
138
  rule_above_line_weight: psr.rule_above_line_weight,
127
139
  rule_above_color: psr.stroke_color,
@@ -139,6 +151,8 @@ module Idml
139
151
  rule_below_right_indent: psr.rule_below_right_indent,
140
152
  rule_below_width: psr.rule_below_width,
141
153
  )
154
+ prepend_list_marker(paragraph)
155
+ paragraph
142
156
  end
143
157
  end
144
158
 
@@ -189,6 +203,19 @@ module Idml
189
203
  end
190
204
  private_class_method :alignment_for
191
205
 
206
+ # Prepends the list marker (bullet glyph or numbered expression)
207
+ # to the paragraph's first run when the paragraph is part of
208
+ # a list. Mutates the first run's text in place.
209
+ def self.prepend_list_marker(paragraph)
210
+ marker = ListMarker.marker_for(paragraph)
211
+ return unless marker
212
+ return if paragraph.runs.empty?
213
+
214
+ first_run = paragraph.runs.first
215
+ first_run.text = "#{marker}#{first_run.text}"
216
+ end
217
+ private_class_method :prepend_list_marker
218
+
192
219
  # Concatenate runs into a single block. Used when the renderer
193
220
  # can't handle per-run styling (e.g., no font metrics available).
194
221
  def self.concatenate(runs)
data/lib/idml/render.rb CHANGED
@@ -15,6 +15,8 @@ module Idml
15
15
  autoload :StrokeStyle, "#{__dir__}/render/stroke_style"
16
16
  autoload :CharacterStyle, "#{__dir__}/render/character_style"
17
17
  autoload :ParagraphRules, "#{__dir__}/render/paragraph_rules"
18
+ autoload :DropCap, "#{__dir__}/render/drop_cap"
19
+ autoload :ListMarker, "#{__dir__}/render/list_marker"
18
20
  autoload :StoryChainController, "#{__dir__}/render/story_chain_controller"
19
21
  autoload :LayerFilter, "#{__dir__}/render/layer_filter"
20
22
  autoload :ConditionFilter, "#{__dir__}/render/condition_filter"
data/lib/idml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.7.2"
4
+ VERSION = "0.7.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -168,6 +168,10 @@ files:
168
168
  - TODO.pdf/11-font-embedding.md
169
169
  - TODO.pdf/110-conditional-text.md
170
170
  - TODO.pdf/111-footnote-support.md
171
+ - TODO.pdf/112-text-frame-vertical-justification.md
172
+ - TODO.pdf/113-multi-column-text-frames.md
173
+ - TODO.pdf/114-text-wrap-around-shapes.md
174
+ - TODO.pdf/115-bullet-numbered-lists.md
171
175
  - TODO.pdf/12-idml-to-pdf-pipeline.md
172
176
  - TODO.pdf/13-cjk-text-layout.md
173
177
  - TODO.pdf/14-page-item-element-models.md
@@ -430,6 +434,7 @@ files:
430
434
  - lib/idml/render/color_helper.rb
431
435
  - lib/idml/render/color_resolver.rb
432
436
  - lib/idml/render/condition_filter.rb
437
+ - lib/idml/render/drop_cap.rb
433
438
  - lib/idml/render/font_reference_resolver.rb
434
439
  - lib/idml/render/font_setup.rb
435
440
  - lib/idml/render/gradient_resolver.rb
@@ -439,6 +444,7 @@ files:
439
444
  - lib/idml/render/image.rb
440
445
  - lib/idml/render/image_collector.rb
441
446
  - lib/idml/render/layer_filter.rb
447
+ - lib/idml/render/list_marker.rb
442
448
  - lib/idml/render/metadata_builder.rb
443
449
  - lib/idml/render/page_item_renderer.rb
444
450
  - lib/idml/render/paragraph_rules.rb