idml 0.7.3 → 0.7.5

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: 66e6127aeed8e54926e01cacb67c81445565c1e43a558015fd9740e022c12372
4
- data.tar.gz: 20b6dcdef5ee316ca394a2b062ed0dac2db9137e08e28f7649944623420e3938
3
+ metadata.gz: a713a0a63c6af7b6090c9a6260467db40ee9a7c497f37561dccf2526ceadaa71
4
+ data.tar.gz: a6695a7318b96ed6664e15b9e2a99e40d2da6ef05139154ba7ccfab73e3eeecf
5
5
  SHA512:
6
- metadata.gz: 94180fd8ac2231198178f40f2051d4e1bd9c56d917cd271808fc87f7de66746739c4ecad65e23f6bbb57be539f061ff5ef9abfda56e71a2bb23ddc403ac3d9a5
7
- data.tar.gz: 353fcc512f92ed689fc1e41cabf4fe9310bcb5ce0464be24b0be6a452c1559f860201c7132318bb3fd4be9c510eed63ebb98f056b927b9e781bffcfc2123f632
6
+ metadata.gz: 92e8aff4adbb8935c2e3ec7b7ead236458ec2f7171a2923eee9de63b4b4f0ed96d95f7f39c9c184d97643b0aa9096e3aa1b40e890610e82d514bb36b17c6f14b
7
+ data.tar.gz: 88c470bb2d55962f57a950fe6c1ef526d34ce36d7fafe38ed045d4f7019821586a9045d1f027e28eb304a1d0a57da9f157256603a83e48e873decd3f2620554c
@@ -1,6 +1,9 @@
1
1
  # TODO PDF 115: Bullet and numbered list rendering
2
2
 
3
- ## Status: OPEN gap identified in 2026-08-10 audit
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.
4
7
 
5
8
  ## Problem
6
9
 
@@ -0,0 +1,62 @@
1
+ # TODO PDF 116: AppliedParagraphStyle / AppliedCharacterStyle resolution from Resources/Styles.xml
2
+
3
+ ## Status: OPEN — critical gap identified in 2026-08-10 audit
4
+
5
+ ## Problem
6
+
7
+ In real IDML documents, the `<ParagraphStyleRange>` element often
8
+ carries ONLY an `AppliedParagraphStyle` reference and NO inline
9
+ formatting attributes. The actual formatting (Justification,
10
+ SpaceBefore, PointSize, etc.) lives in the referenced
11
+ `<ParagraphStyle>` definition in `Resources/Styles.xml`.
12
+
13
+ Example from the sample-with-table-more fixture:
14
+ ```xml
15
+ <ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle">
16
+ <CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]">
17
+ ...
18
+ </CharacterStyleRange>
19
+ </ParagraphStyleRange>
20
+ ```
21
+
22
+ The PSR has NO inline Justification, SpaceBefore, etc. Those values
23
+ are on `ParagraphStyle Self="ParagraphStyle/$ID/NormalParagraphStyle"`
24
+ in Styles.xml.
25
+
26
+ Today StyleResolver reads attributes directly from the PSR element.
27
+ When the PSR has only a style reference (common in real documents),
28
+ the renderer uses default values for everything.
29
+
30
+ ## Impact
31
+
32
+ For the `sample-with-table-more` fixture, every PSR uses
33
+ `NormalParagraphStyle` — which in InDesign maps to 12pt Regular with
34
+ left alignment. The renderer happens to produce approximately right
35
+ output because the defaults match. But for documents that use custom
36
+ paragraph styles (Heading 1 = 24pt Bold Centered, Body Text = 11pt
37
+ Justified with 6pt SpaceAfter), the output would be wrong.
38
+
39
+ ## What needs to happen
40
+
41
+ 1. `Render::StyleResolver` resolves `AppliedParagraphStyle` to a
42
+ `ParagraphStyle` from `Parts::Style` (Resources/Styles.xml).
43
+ 2. PSR inline attributes override the style's attributes (when both
44
+ are present — this is the IDML "local override" mechanism).
45
+ 3. Same for `AppliedCharacterStyle` → `CharacterStyle` resolution.
46
+ 4. The merged attributes become the Paragraph/StyledRun's values.
47
+
48
+ Architecture: `Parts::Style` already parses `<ParagraphStyle>` and
49
+ `<CharacterStyle>` entries. StyleResolver needs access to the style
50
+ part (via `context.package.styles` or similar).
51
+
52
+ ## Acceptance criteria
53
+
54
+ - [ ] PSR with only AppliedParagraphStyle resolves formatting from
55
+ the referenced ParagraphStyle.
56
+ - [ ] PSR inline attributes override style attributes.
57
+ - [ ] CharacterStyle resolution for CSR's AppliedCharacterStyle.
58
+ - [ ] Spec: fixture with custom paragraph style renders correctly.
59
+
60
+ ## Dependencies
61
+
62
+ - None — pure feature work on existing models.
@@ -1,10 +1,10 @@
1
- # TODO PDF 99: Table column widths from ColumnAttributes
1
+ # TODO PDF 99: Table column widths from Column elements
2
2
 
3
- ## Status: DONE — Table now models `column_count` and
4
- `single_column_width`; SchemaLayout uses SingleColumnWidth when
5
- declared, else falls back to even division. (IDML has no separate
6
- ColumnAttributes element column widths live on Table directly
7
- when uniform.)
3
+ ## Status: DONE — Table now models both `single_column_width`
4
+ (uniform) and per-column `<Column>` elements with individual
5
+ `SingleColumnWidth` values. SchemaLayout uses per-column widths
6
+ when `<Column>` children are present; falls back to uniform when
7
+ not.
8
8
 
9
9
  ## Problem
10
10
 
@@ -0,0 +1,45 @@
1
+ # frozen_string: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Column>` — a column definition within an IDML Table. Carries
6
+ # the column's width (SingleColumnWidth), type (HeaderColumn /
7
+ # BodyColumn), insets, and visual properties (fill, diagonal lines).
8
+ #
9
+ # Schema: `Column_Object` in
10
+ # `reference-docs/schemas/package/Stories/Story.rnc`. Real IDML
11
+ # tables have `<Column>` children alongside `<Row>` and `<Cell>`
12
+ # siblings. The `Name` attribute is the column index as a string
13
+ # (e.g., "0", "1", "2").
14
+ #
15
+ # The renderer's SchemaLayout reads `SingleColumnWidth` per column
16
+ # to compute per-column x positions and widths (replaces the
17
+ # even-division / uniform-width fallback).
18
+ class Column < Lutaml::Model::Serializable
19
+ attribute :self_attr, :string
20
+ attribute :name, :string
21
+ attribute :single_column_width, :float
22
+ attribute :column_type, :string
23
+ attribute :fill_color, :string
24
+ attribute :fill_tint, :float
25
+
26
+ xml do
27
+ root "Column"
28
+ map_attribute "Self", to: :self_attr
29
+ map_attribute "Name", to: :name
30
+ map_attribute "SingleColumnWidth", to: :single_column_width
31
+ map_attribute "ColumnType", to: :column_type
32
+ map_attribute "FillColor", to: :fill_color
33
+ map_attribute "FillTint", to: :fill_tint
34
+ end
35
+
36
+ # Column Name is a string integer (e.g., "0", "1").
37
+ # Returns the integer index or nil if Name is missing/invalid.
38
+ def index
39
+ return nil unless name
40
+
41
+ Integer(name, 10, exception: false) # rubocop:disable Style/RescueModifier
42
+ end
43
+ end
44
+ end
45
+ end
@@ -46,6 +46,7 @@ module Idml
46
46
  attribute :table_row, Idml::Elements::TableRow, collection: true
47
47
  attribute :cell, Idml::Elements::Cell, collection: true
48
48
  attribute :row, Idml::Elements::Row, collection: true
49
+ attribute :column, Idml::Elements::Column, collection: true
49
50
  attribute :properties, Idml::Elements::Properties, collection: true
50
51
 
51
52
  xml do
@@ -77,6 +78,7 @@ module Idml
77
78
  map_element "TableRow", to: :table_row
78
79
  map_element "Cell", to: :cell
79
80
  map_element "Row", to: :row
81
+ map_element "Column", to: :column
80
82
  map_element "Properties", to: :properties
81
83
  end
82
84
 
data/lib/idml/elements.rb CHANGED
@@ -17,6 +17,7 @@ module Idml
17
17
  autoload :ButtonPreference, "idml/elements/pref_button_preference"
18
18
  autoload :Bookmark, "idml/elements/bookmark"
19
19
  autoload :Cell, "idml/elements/cell"
20
+ autoload :Column, "idml/elements/column"
20
21
  autoload :Condition, "idml/elements/condition"
21
22
  autoload :CellStyle, "idml/elements/cell_style"
22
23
  autoload :CellStyleGroup, "idml/elements/cell_style_group"
@@ -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
@@ -243,7 +243,8 @@ module Idml
243
243
  mark_covered(covered, col_idx, row_idx, col_span, row_span)
244
244
 
245
245
  yield cell_x(col_idx), cell_y(row_idx, col_span, row_span),
246
- cell_w(col_span), cell_h(row_idx, row_span), cell
246
+ cell_w(col_idx, col_span), cell_h(row_idx, row_span),
247
+ cell
247
248
  end
248
249
  end
249
250
  end
@@ -286,7 +287,7 @@ module Idml
286
287
  private :mark_covered
287
288
 
288
289
  def cell_x(col)
289
- box[:x] + (col * per_col_width)
290
+ box[:x] + cumulative_col_width(col)
290
291
  end
291
292
 
292
293
  # PDF-coordinate bottom y of the cell's rectangle. For a
@@ -297,9 +298,11 @@ module Idml
297
298
  box[:y] + total_height - cumulative_height(last_row + 1)
298
299
  end
299
300
 
300
- # Width covered by `span` columns starting at the cell's col.
301
- def cell_w(span)
302
- per_col_width * (span || 1)
301
+ # Width covered by `span` columns starting at `col`. Uses
302
+ # per-column widths from Column elements when available;
303
+ # falls back to uniform width when not.
304
+ def cell_w(col, span)
305
+ span.to_i.times.sum { |i| column_width_at(col + i) }
303
306
  end
304
307
 
305
308
  # Total height covered by `span` rows starting at `row_idx`.
@@ -318,6 +321,41 @@ module Idml
318
321
  end
319
322
  end
320
323
 
324
+ # Returns per-column widths from Column elements when
325
+ # present; empty array otherwise. When non-empty, the
326
+ # renderer uses per-column widths for x positions and
327
+ # span widths instead of uniform per_col_width.
328
+ def per_column_widths
329
+ return @per_column_widths if defined?(@per_column_widths)
330
+
331
+ cols = table.column
332
+ @per_column_widths = if cols.empty?
333
+ []
334
+ else
335
+ cols.filter_map do |col|
336
+ w = col.single_column_width
337
+ w&.positive? ? w : nil
338
+ end
339
+ end
340
+ end
341
+
342
+ # Width of a single column at index `idx`. Uses per-column
343
+ # widths when available; falls back to uniform.
344
+ def column_width_at(idx)
345
+ return per_col_width if per_column_widths.empty?
346
+ return per_col_width unless idx.between?(0, per_column_widths.length - 1)
347
+
348
+ per_column_widths[idx]
349
+ end
350
+
351
+ # Cumulative width from column 0 up to (but not including)
352
+ # column `col`. Used for x positioning.
353
+ def cumulative_col_width(col)
354
+ return col * per_col_width if per_column_widths.empty?
355
+
356
+ col.times.sum { |i| column_width_at(i) }
357
+ end
358
+
321
359
  def row_height(idx)
322
360
  return 0.0 unless idx.between?(0, row_count - 1)
323
361
 
@@ -55,6 +55,10 @@ module Idml
55
55
  :right_indent,
56
56
  :auto_leading,
57
57
  :drop_cap_lines,
58
+ :bullets_and_numbering_list_type,
59
+ :bullet_character_value,
60
+ :bullets_text_after,
61
+ :numbering_expression,
58
62
  :drop_cap_characters,
59
63
  # RuleAbove / RuleBelow — paragraph rules (horizontal lines
60
64
  # above/below the paragraph block). Driven by PSR attributes.
@@ -115,7 +119,7 @@ module Idml
115
119
  runs = csr_runs(psr, condition_filter: condition_filter)
116
120
  next if runs.empty?
117
121
 
118
- Paragraph.new(
122
+ paragraph = Paragraph.new(
119
123
  runs: runs,
120
124
  alignment: runs.first.alignment,
121
125
  space_before: psr.space_before,
@@ -126,6 +130,10 @@ module Idml
126
130
  auto_leading: psr.auto_leading,
127
131
  drop_cap_lines: psr.drop_cap_lines,
128
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,
129
137
  rule_above: psr.rule_above,
130
138
  rule_above_line_weight: psr.rule_above_line_weight,
131
139
  rule_above_color: psr.stroke_color,
@@ -143,6 +151,8 @@ module Idml
143
151
  rule_below_right_indent: psr.rule_below_right_indent,
144
152
  rule_below_width: psr.rule_below_width,
145
153
  )
154
+ prepend_list_marker(paragraph)
155
+ paragraph
146
156
  end
147
157
  end
148
158
 
@@ -193,6 +203,19 @@ module Idml
193
203
  end
194
204
  private_class_method :alignment_for
195
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
+
196
219
  # Concatenate runs into a single block. Used when the renderer
197
220
  # can't handle per-run styling (e.g., no font metrics available).
198
221
  def self.concatenate(runs)
data/lib/idml/render.rb CHANGED
@@ -16,6 +16,7 @@ module Idml
16
16
  autoload :CharacterStyle, "#{__dir__}/render/character_style"
17
17
  autoload :ParagraphRules, "#{__dir__}/render/paragraph_rules"
18
18
  autoload :DropCap, "#{__dir__}/render/drop_cap"
19
+ autoload :ListMarker, "#{__dir__}/render/list_marker"
19
20
  autoload :StoryChainController, "#{__dir__}/render/story_chain_controller"
20
21
  autoload :LayerFilter, "#{__dir__}/render/layer_filter"
21
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.3"
4
+ VERSION = "0.7.5"
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.3
4
+ version: 0.7.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -172,6 +172,7 @@ files:
172
172
  - TODO.pdf/113-multi-column-text-frames.md
173
173
  - TODO.pdf/114-text-wrap-around-shapes.md
174
174
  - TODO.pdf/115-bullet-numbered-lists.md
175
+ - TODO.pdf/116-applied-style-resolution.md
175
176
  - TODO.pdf/12-idml-to-pdf-pipeline.md
176
177
  - TODO.pdf/13-cjk-text-layout.md
177
178
  - TODO.pdf/14-page-item-element-models.md
@@ -280,6 +281,7 @@ files:
280
281
  - lib/idml/elements/character_style_group.rb
281
282
  - lib/idml/elements/character_style_range.rb
282
283
  - lib/idml/elements/color.rb
284
+ - lib/idml/elements/column.rb
283
285
  - lib/idml/elements/condition.rb
284
286
  - lib/idml/elements/content.rb
285
287
  - lib/idml/elements/content_transparency_setting.rb
@@ -444,6 +446,7 @@ files:
444
446
  - lib/idml/render/image.rb
445
447
  - lib/idml/render/image_collector.rb
446
448
  - lib/idml/render/layer_filter.rb
449
+ - lib/idml/render/list_marker.rb
447
450
  - lib/idml/render/metadata_builder.rb
448
451
  - lib/idml/render/page_item_renderer.rb
449
452
  - lib/idml/render/paragraph_rules.rb