idml 0.10.12 → 0.10.14

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: 49122b9c9f5974361567afb8fc2e063602e370141f0d5990f8de156275c312c4
4
- data.tar.gz: c4f21424d6cd5d2f9345801aeb53be0b78d26f3d0c423b8ad20b179dff4ef38f
3
+ metadata.gz: 00f5eb099cc78c69be52672e179bc0d2c10b102554bb74b28272b04d0ed904c6
4
+ data.tar.gz: c7936ca3c76fef90639d532cc70c0deb7423b2dcd9cd3d73ce6b88bff5a1257c
5
5
  SHA512:
6
- metadata.gz: 6c06eb1149c2fed295382a4ce65504d12c821d884ffa6cd145f9ac96e0837c82b1dbfdd16abd082e84366dd6c0bfd6edc8670e187d71785f1de0e7a415cc9892
7
- data.tar.gz: 12352d559b36427ec1be4ce71e31cfc149e24ec52a1fe70b65c3ecc720575c1a4f7a9aab8b96e67329747f33bc2005494c6f0c2034508b642edf37ba917a45ca
6
+ metadata.gz: 354bb00de55fe629d84853a8e1dfdc2c8b821f708fc15584cf9ac509ba3489a56f92f48f71698cfcf9b28c9f3814b4c0366f3a88d40aa3e2e05b33b242e20a60
7
+ data.tar.gz: 52cc665c20cdf7c8c766593e38fd18b847f15da0ac3a09b1accfd5dbbe2b48614a7eadb5452b8f4b295bff1870c7a2acb1a48a0c03174f15916f681d62c4a16c
@@ -0,0 +1,48 @@
1
+ # TODO PDF 151: Schema conformance for the Elements layer
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-31
4
+
5
+ ## Problem
6
+
7
+ Architecture review round four: 147 element classes held hundreds
8
+ of wire attributes transcribed from the RNC, but only 11 classes
9
+ had attribute-set specs — the project's own drift defense
10
+ (CLAUDE.md convention 5) covered a tenth of the surface. Any
11
+ hand-edit that added, dropped, or misspelled a `map_attribute`
12
+ passed every test.
13
+
14
+ ## Solution
15
+
16
+ - **`Idml::Schema::Rnc`** (`schema/rnc.rb`): the RNC parsing
17
+ extracted from the generator script into a lib adapter
18
+ (`element_attribute_map`, `element_definition`). Two consumers
19
+ over one seam — `scripts/rnc_to_lutaml.rb` (now presentation
20
+ only) and the conformance spec.
21
+ - **Schema-conformance spec** (`elements_schema_conformance_spec.rb`):
22
+ for every Elements class — no wire attribute outside the RNC
23
+ universe (catches typos and inventions), and every mapped
24
+ destination is a declared attribute (mapping/declaration
25
+ consistency). Legacy non-schema classes (TableCell) carry an
26
+ explicit allow-list. Full equality is documented as
27
+ unenforceable (deliberate subsets; typedef-composed attributes).
28
+
29
+ **Bug found by the probe and fixed**: `TextFramePreference`
30
+ carried four invented attributes (`InsetTop/Left/Bottom/Right`)
31
+ that appear in no schema, no spec, and no real file — while the
32
+ schema-faithful inset carrier (`Properties > InsetSpacing`, spec
33
+ examples 31/32) was unmodeled, so real documents' text frame
34
+ insets were silently ignored. New `Elements::InsetSpacing`
35
+ (single unit → all sides; list → [top, right, bottom, left]),
36
+ wired into `layout_frame`; the invented attributes removed.
37
+
38
+ ## Files
39
+
40
+ - `lib/idml/schema.rb`, `lib/idml/schema/rnc.rb` (new)
41
+ - `scripts/rnc_to_lutaml.rb`
42
+ - `lib/idml/elements/inset_spacing.rb` (new)
43
+ - `lib/idml/elements/text_frame_preference.rb`,
44
+ `lib/idml/elements/properties.rb`, `lib/idml/elements.rb`
45
+ - `lib/idml/render/renderers/text_frame_renderer.rb`,
46
+ `lib/idml.rb`
47
+ - `spec/idml/elements_schema_conformance_spec.rb` (new)
48
+ - `spec/idml/elements/inset_spacing_spec.rb` (new)
@@ -0,0 +1,27 @@
1
+ # TODO PDF 152: RTL paragraph alignment
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-31
4
+
5
+ ## Problem
6
+
7
+ ParagraphDirection (LeftToRightDirection /
8
+ RightToLeftDirection) was parsed but unused: RTL paragraphs
9
+ aligned left/right by LTR convention, so Arabic/Hebrew text sat
10
+ on the wrong edge.
11
+
12
+ ## Solution
13
+
14
+ `StyleResolver::Paragraph` carries `paragraph_direction` (PSR or
15
+ paragraph style); `TextEngine::Justifier` gains a `direction:`
16
+ argument that mirrors the visual alignment under
17
+ RightToLeftDirection — left↔right, start↔end; center and
18
+ justified unchanged. Both justify call sites (frame renderer and
19
+ footnote layout) pass the paragraph's direction.
20
+
21
+ ## Files
22
+
23
+ - `lib/idml/text_engine/justifier.rb`
24
+ - `lib/idml/render/style_resolver.rb`
25
+ - `lib/idml/render/renderers/text_frame_renderer.rb`
26
+ - `lib/idml/render/footnote.rb`
27
+ - `spec/idml/text_engine/text_engine_spec.rb`
@@ -0,0 +1,31 @@
1
+ # TODO PDF 153: Keep options in vertical writing
2
+
3
+ ## Status: COMPLETE (approximation) — implemented 2026-08-31
4
+
5
+ ## Problem
6
+
7
+ Keep options (TODOs 123/127/133) applied only to the horizontal
8
+ engine; vertical paragraphs split across frames regardless of
9
+ KeepAllLinesTogether / KeepWithNext.
10
+
11
+ ## Solution
12
+
13
+ `VerticalEmit.vertical_render` consults a keep check BEFORE
14
+ placing each paragraph (deferral after placement cannot un-emit):
15
+
16
+ - **KeepAllLinesTogether** defers the paragraph wholly to the
17
+ next frame when its column need exceeds the remaining columns.
18
+ - **KeepWithNext** also defers when the follower's forced break
19
+ or column need would strand it.
20
+ - Never defers the frame's first paragraph — the same progress
21
+ guarantee as KeepPolicy (shared: `KeepPolicy.paragraph_break?`).
22
+
23
+ Approximation: column need = runs count (one column per run),
24
+ which under-estimates runs spanning several columns — deferral
25
+ errs toward placing. KeepFirstLines/KeepLastLines windows remain
26
+ horizontal-only (they are line-window concepts).
27
+
28
+ ## Files
29
+
30
+ - `lib/idml/render/renderers/vertical_emit.rb`
31
+ - `spec/idml/render/vertical_render_spec.rb`
@@ -1,6 +1,6 @@
1
1
  # TODO PDF 61: Known limitations and future work
2
2
 
3
- ## Status: DOCUMENTED — refreshed 2026-08-28
3
+ ## Status: DOCUMENTED — refreshed 2026-08-31
4
4
 
5
5
  Previously listed limitations (font embedding, tagged PDF, per-run
6
6
  styling, dead code) were resolved by TODOs 52/53/55/67/76 long ago;
@@ -14,8 +14,9 @@ this list reflects the current state.
14
14
  wrap after explicit hyphens (TODO 139).
15
15
  - **Justification**: word/letter spacing caps apply (TODO 119),
16
16
  MaximumGlyphScaling stretches (TODO 129) and MinimumGlyphScaling
17
- compresses overlong lines (TODO 135); ToBinding / RTL binding
18
- alignment is not applied.
17
+ compresses overlong lines (TODO 135); RTL paragraphs mirror
18
+ left/right alignment (TODO 152); ToBinding / kashida-based
19
+ Arabic justification is not applied.
19
20
  - **Keep options**: KeepAllLinesTogether (TODO 123), KeepWithNext
20
21
  (TODO 127), and the KeepFirstLines / KeepLastLines windows
21
22
  (TODO 133, line-count approximation) are honored.
@@ -39,8 +40,9 @@ this list reflects the current state.
39
40
  Desired applies per class pair) are applied; Minimum/Maximum
40
41
  aki bounds and CompressionPriority are parsed but unused.
41
42
  - **Vertical mode**: Latin runs rotate as one group per segment
42
- (TODO 142); ruby placement is beside-column; keep options do
43
- not apply.
43
+ (TODO 142); ruby placement is beside-column; KeepAllLines /
44
+ KeepWithNext apply with a column-count approximation (TODO 153);
45
+ keep windows are horizontal-only.
44
46
 
45
47
  ### Structural features
46
48
  - **Endnotes** (TODO 117): reference markers render and endnote TEXT
@@ -62,5 +64,5 @@ this list reflects the current state.
62
64
  - Hyphenation dictionary integration
63
65
  - NextColumn wrap via column-jump chaining (multi-column frames)
64
66
  - Minimum/Maximum mojikumi aki bounds + CompressionPriority
65
- - ToBinding / RTL binding alignment
66
- - Vertical-mode keep options
67
+ - ToBinding / kashida Arabic justification
68
+ - Vertical keep windows (KeepFirstLines / KeepLastLines)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<InsetSpacing>` inside TextFramePreference's Properties —
6
+ # the schema-faithful carrier of text frame insets (the
7
+ # spec's examples 31/32): a single `type="unit"` value applies
8
+ # to all sides; a `type="list"` of ListItems carries
9
+ # [top, right, bottom, left].
10
+ class InsetSpacing < Lutaml::Model::Serializable
11
+ attribute :type, :string
12
+ attribute :value, :string
13
+ attribute :list_item, TypedValue, collection: true
14
+
15
+ xml do
16
+ root "InsetSpacing"
17
+ map_attribute "type", to: :type
18
+ map_content to: :value
19
+ map_element "ListItem", to: :list_item
20
+ end
21
+
22
+ # [top, right, bottom, left] insets; nil components when the
23
+ # document doesn't declare them.
24
+ def sides
25
+ return Array.new(4, value&.to_f) if list_item.empty?
26
+
27
+ (0...4).map { |i| list_item[i]&.value&.to_f }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -18,6 +18,7 @@ module Idml
18
18
  attribute :paragraph_border_color, Idml::Elements::TypedValue
19
19
  attribute :text_wrap_offset, Idml::Elements::TypedValue
20
20
  attribute :paragraph_border_gap_color, Idml::Elements::TypedValue
21
+ attribute :inset_spacing, Idml::Elements::InsetSpacing
21
22
 
22
23
  xml do
23
24
  root "Properties"
@@ -29,6 +30,7 @@ module Idml
29
30
  map_element "ParagraphBorderColor", to: :paragraph_border_color
30
31
  map_element "TextWrapOffset", to: :text_wrap_offset
31
32
  map_element "ParagraphBorderGapColor", to: :paragraph_border_gap_color
33
+ map_element "InsetSpacing", to: :inset_spacing
32
34
  end
33
35
 
34
36
  def first_geometry
@@ -5,12 +5,10 @@ module Idml
5
5
  # Typed model of `<TextFramePreference>`. Every
6
6
  # attribute from `TextFramePreference_Object` in
7
7
  # `reference-docs/schemas/package/Resources/Styles.rnc` is declared (33 attributes),
8
- # generated by `scripts/rnc_to_lutaml.rb`.
8
+ # generated by `scripts/rnc_to_lutaml.rb`. Insets live in the
9
+ # Properties > InsetSpacing child (spec examples 31/32).
9
10
  class TextFramePreference < Lutaml::Model::Serializable
10
- attribute :inset_top, :float
11
- attribute :inset_left, :float
12
- attribute :inset_bottom, :float
13
- attribute :inset_right, :float
11
+ attribute :properties, Idml::Elements::Properties, collection: true
14
12
  attribute :footnotes_enable_overrides, :boolean
15
13
  attribute :footnotes_span_across_columns, :boolean
16
14
  attribute :footnotes_minimum_spacing, :float
@@ -47,10 +45,7 @@ module Idml
47
45
 
48
46
  xml do
49
47
  root "TextFramePreference"
50
- map_attribute "InsetTop", to: :inset_top
51
- map_attribute "InsetLeft", to: :inset_left
52
- map_attribute "InsetBottom", to: :inset_bottom
53
- map_attribute "InsetRight", to: :inset_right
48
+ map_element "Properties", to: :properties
54
49
  map_attribute "FootnotesEnableOverrides", to: :footnotes_enable_overrides
55
50
  map_attribute "FootnotesSpanAcrossColumns", to: :footnotes_span_across_columns
56
51
  map_attribute "FootnotesMinimumSpacing", to: :footnotes_minimum_spacing
data/lib/idml/elements.rb CHANGED
@@ -90,6 +90,7 @@ module Idml
90
90
  autoload :MasterSpreadObject, "idml/elements/master_spread_object"
91
91
  autoload :MixedInk, "idml/elements/mixed_ink"
92
92
  autoload :MixedInkGroup, "idml/elements/mixed_ink_group"
93
+ autoload :InsetSpacing, "idml/elements/inset_spacing"
93
94
  autoload :MojikumiTable, "idml/elements/mojikumi_table"
94
95
  autoload :MojikumiTableProperties, "idml/elements/mojikumi_table"
95
96
  autoload :OverrideMojikumiAki, "idml/elements/mojikumi_table"
@@ -138,6 +138,7 @@ module Idml
138
138
  TextEngine::Justifier.justify(
139
139
  line: line, frame_width: wrap_width,
140
140
  alignment: paragraph.alignment || :left,
141
+ direction: paragraph.paragraph_direction,
141
142
  last_line: last_line?(paragraph, run, index, lines),
142
143
  limits: limits
143
144
  )
@@ -244,20 +244,40 @@ module Idml
244
244
  # back to 0 when the preference is absent or doesn't declare
245
245
  # them.
246
246
  def self.layout_frame(frame_item, box)
247
- pref = frame_item.text_frame_preference&.first
247
+ top, right, bottom, left = frame_insets(
248
+ frame_item.text_frame_preference&.first,
249
+ )
248
250
  TextEngine::Frame.new(
249
251
  x: box[:x],
250
252
  y: box[:y],
251
253
  width: box[:width],
252
254
  height: box[:height],
253
- inset_top: pref&.inset_top,
254
- inset_bottom: pref&.inset_bottom,
255
- inset_left: pref&.inset_left,
256
- inset_right: pref&.inset_right,
255
+ inset_top: top,
256
+ inset_bottom: bottom,
257
+ inset_left: left,
258
+ inset_right: right,
257
259
  )
258
260
  end
259
261
  private_class_method :layout_frame
260
262
 
263
+ # Insets from Properties > InsetSpacing (spec examples
264
+ # 31/32): one unit value applies to all sides; a list
265
+ # carries [top, right, bottom, left]. Missing sides are 0.
266
+ def self.frame_insets(pref)
267
+ props = pref&.properties
268
+ spacing = props.first&.inset_spacing if props
269
+ return Array.new(4, 0.0) unless spacing
270
+
271
+ side_or_zero(spacing)
272
+ end
273
+
274
+ def self.side_or_zero(spacing)
275
+ [0, 1, 2, 3].map do |index|
276
+ spacing.sides[index] || 0.0
277
+ end
278
+ end
279
+ private_class_method :frame_insets
280
+
261
281
  # Walks the chain state's paragraphs/runs and emits one
262
282
  # `canvas.text` per line via the Shaper → LineBreaker →
263
283
  # Justifier → VerticalLayout pipeline. Cursor y descends
@@ -833,7 +853,8 @@ module Idml
833
853
  line: line, frame_width: wrap_width,
834
854
  alignment: paragraph.alignment || :left,
835
855
  last_line: paragraph_last && index == lines.length - 1,
836
- limits: limits
856
+ limits: limits,
857
+ direction: paragraph.paragraph_direction
837
858
  )
838
859
  end
839
860
  end
@@ -24,10 +24,20 @@ module Idml
24
24
  layout_frame = layout_frame(context.item, box)
25
25
  left_limit = layout_frame.x + (layout_frame.inset_left || 0)
26
26
  resume_vertical_paragraph(state)
27
- next_column = state.column_offset || 0
27
+ entry_offset = state.column_offset || 0
28
+ next_column = entry_offset
28
29
  pending = 0
29
30
 
30
- state.paragraphs.each do |paragraph|
31
+ state.paragraphs.each_with_index do |paragraph, index|
32
+ if vertical_keep_deferred?(paragraph, state.paragraphs[index + 1],
33
+ layout_frame, font, left_limit,
34
+ next_column, pending, entry_offset)
35
+ state.current_paragraph = nil
36
+ state.runs_remaining = []
37
+ state.column_offset = 0
38
+ break
39
+ end
40
+
31
41
  remaining_runs, next_column = vertical_paragraph(
32
42
  canvas, paragraph, context, layout_frame, font,
33
43
  left_limit, next_column
@@ -45,6 +55,64 @@ module Idml
45
55
  state.paragraphs = state.paragraphs[pending..]
46
56
  end
47
57
 
58
+ # Keep options in vertical mode (TODO 153, column-count
59
+ # approximation — one column per run under-estimates runs
60
+ # that span several columns, so deferral errs toward
61
+ # placing): KeepAllLinesTogether defers the paragraph
62
+ # wholly to the next frame; KeepWithNext also defers when
63
+ # the follower (or its forced break) would strand. Never
64
+ # defers the frame's first paragraph — the progress
65
+ # guarantee, shared with KeepPolicy.
66
+ def vertical_keep_deferred?(paragraph, next_paragraph,
67
+ layout_frame, _font, left_limit,
68
+ next_column, pending, entry_offset)
69
+ placed = pending.positive? || entry_offset.positive?
70
+ return false unless placed
71
+ return false unless keep_flagged?(paragraph)
72
+
73
+ needed = [paragraph.runs.length, 1].max
74
+ return true if paragraph.keep_all_lines_together &&
75
+ needed > vertical_room(layout_frame, paragraph, left_limit,
76
+ next_column)
77
+
78
+ with_next_deferred?(paragraph, next_paragraph, layout_frame,
79
+ left_limit, next_column, needed)
80
+ end
81
+
82
+ def keep_flagged?(paragraph)
83
+ paragraph.keep_all_lines_together ||
84
+ !paragraph.keep_with_next.nil?
85
+ end
86
+
87
+ def with_next_deferred?(paragraph, next_paragraph, layout_frame,
88
+ left_limit, next_column, needed)
89
+ return false if paragraph.keep_with_next.nil?
90
+
91
+ follower_needed = [next_paragraph&.runs&.length || 1, 1].max
92
+ follower_break = KeepPolicy.paragraph_break?(next_paragraph)
93
+ room = vertical_room(layout_frame, paragraph, left_limit,
94
+ next_column)
95
+ follower_break || needed + follower_needed > room
96
+ end
97
+
98
+ def vertical_room(layout_frame, paragraph, left_limit,
99
+ next_column)
100
+ leading = TextEngine::VerticalLayout.leading_for(
101
+ paragraph.auto_leading,
102
+ paragraph.runs.first&.point_size ||
103
+ TextFrameRenderer::DEFAULT_SIZE,
104
+ )
105
+ vertical_available_columns(layout_frame, leading, left_limit,
106
+ next_column)
107
+ end
108
+
109
+ def vertical_available_columns(layout_frame, leading, left_limit,
110
+ next_column)
111
+ right = layout_frame.x + layout_frame.width -
112
+ (layout_frame.inset_right || 0)
113
+ (((right - left_limit) / leading).floor - next_column).to_i
114
+ end
115
+
48
116
  # Folds a chain-resumed paragraph (runs_remaining) back to
49
117
  # the head of the paragraph list so the vertical path sees a
50
118
  # uniform sequence. The consumed column offset persists in
@@ -70,6 +70,9 @@ module Idml
70
70
  :maximum_letter_spacing,
71
71
  :maximum_glyph_scaling,
72
72
  :minimum_glyph_scaling,
73
+ # ParagraphDirection (LeftToRightDirection /
74
+ # RightToLeftDirection) — mirrors alignment under RTL.
75
+ :paragraph_direction,
73
76
  # Forced paragraph break (StartParagraph: NextPage /
74
77
  # NextColumn / NextFrame / NextOddPage / NextEvenPage).
75
78
  :start_paragraph,
@@ -197,6 +200,9 @@ module Idml
197
200
  paragraph = Paragraph.new(
198
201
  runs: runs,
199
202
  alignment: runs.first.alignment,
203
+ paragraph_direction: resolve_attr(
204
+ style_lookup, psr, :paragraph_direction
205
+ ),
200
206
  space_before: resolve_attr(style_lookup, psr, :space_before),
201
207
  space_after: resolve_attr(style_lookup, psr, :space_after),
202
208
  first_line_indent: resolve_attr(style_lookup, psr,
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Schema
5
+ # Parses RelaxNG Compact element definitions into data:
6
+ # element name → wire attribute names. Handles the Adobe
7
+ # package schemas' shape (`NAME_Object = element Name { ... }`
8
+ # with balanced braces; attributes as `attribute X { type }`).
9
+ # Two consumers over this seam: the class generator script and
10
+ # the schema-conformance spec.
11
+ module Rnc
12
+ ELEMENT_DEF = /(\w+)\s*+=\s*+element\s*+([\w:]+)\s*+\{/
13
+ ATTRIBUTE_DEF = /attribute\s++(\w+)\s*+\{([^}]*)\}/
14
+
15
+ module_function
16
+
17
+ # { "Story" => [["Self", ...]], ... } for every element
18
+ # definition across the given RNC files. An element defined
19
+ # in several files keeps each definition as an alternative
20
+ # (array of attribute-name arrays) — a class conforms when
21
+ # its wire attributes equal ANY one alternative.
22
+ def element_attribute_map(paths)
23
+ definitions = Hash.new { |h, k| h[k] = [] }
24
+ each_element(paths) do |element_name, body|
25
+ definitions[element_name] << body_attributes(body)
26
+ end
27
+ definitions.to_h
28
+ end
29
+
30
+ # [xml_element_name, attrs_with_types] for one named
31
+ # definition (`Story_Object`) in one file; nil when absent.
32
+ # attrs_with_types: [["Self", "xsd:string"], ...].
33
+ def element_definition(path, definition_name)
34
+ src = File.read(path)
35
+ prefix = /#{Regexp.escape(definition_name)}\s*=\s*element\s+/
36
+ pattern = /#{prefix}([\w:]+)\s*\{/
37
+ match = src.match(pattern)
38
+ return nil unless match
39
+
40
+ body = balanced_body(src, match.end(0))
41
+ return nil unless body
42
+
43
+ attrs_with_types = body.scan(ATTRIBUTE_DEF)
44
+ .map { |name, type| [name, type.strip] }
45
+ [match[1], attrs_with_types]
46
+ end
47
+
48
+ def body_attributes(body)
49
+ body.scan(ATTRIBUTE_DEF).map(&:first).uniq
50
+ end
51
+
52
+ def each_element(paths, &block)
53
+ paths.each do |path|
54
+ scan_file(path, &block)
55
+ end
56
+ end
57
+
58
+ def scan_file(path)
59
+ src = File.read(path)
60
+ src.scan(ELEMENT_DEF) do
61
+ match = Regexp.last_match
62
+ body = balanced_body(src, match.end(0))
63
+ next unless body
64
+
65
+ element_name = match[2].sub(/^idPkg:/, "")
66
+ yield(element_name, body)
67
+ end
68
+ end
69
+
70
+ # The balanced-brace body starting just after `from` (which
71
+ # points past the opening brace); nil when unterminated.
72
+ def balanced_body(src, from)
73
+ depth = 1
74
+ i = from
75
+ while i < src.length && depth.positive?
76
+ case src[i]
77
+ when "{" then depth += 1
78
+ when "}" then depth -= 1
79
+ end
80
+ i += 1
81
+ end
82
+ return nil if depth.positive?
83
+
84
+ src[from...(i - 1)]
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ # Namespace for adapters over the committed RelaxNG Compact
5
+ # schemas (`reference-docs/schemas/`). The RNC files are the
6
+ # authoritative interface of the Elements layer — this namespace
7
+ # turns them into data for the generator script and the
8
+ # conformance spec (two consumers, one seam).
9
+ module Schema
10
+ autoload :Rnc, "#{__dir__}/schema/rnc"
11
+ end
12
+ end
@@ -21,13 +21,13 @@ module Idml
21
21
  DEFAULT_MIN_GLYPH_SCALING = 100.0
22
22
 
23
23
  def self.justify(line:, frame_width:, alignment: :left,
24
- last_line: false, limits: nil)
25
- new(frame_width, alignment, last_line, limits).justify(line)
24
+ last_line: false, limits: nil, direction: nil)
25
+ new(frame_width, alignment, last_line, limits, direction).justify(line)
26
26
  end
27
27
 
28
- def initialize(frame_width, alignment, last_line, limits)
28
+ def initialize(frame_width, alignment, last_line, limits, direction)
29
29
  @frame_width = frame_width
30
- @alignment = alignment
30
+ @alignment = mirrored_alignment(alignment, direction)
31
31
  @last_line = last_line
32
32
  @limits = limits
33
33
  end
@@ -44,7 +44,18 @@ module Idml
44
44
  line
45
45
  end
46
46
 
47
- private
47
+ RTL_ALIGNMENT_MIRROR = {
48
+ left: :right, right: :left,
49
+ start: :end, end: :start
50
+ }.freeze
51
+
52
+ # RightToLeftDirection mirrors the visual alignment: what is
53
+ # "left" in an LTR paragraph sits right in an RTL one.
54
+ def mirrored_alignment(alignment, direction)
55
+ return alignment unless direction == "RightToLeftDirection"
56
+
57
+ RTL_ALIGNMENT_MIRROR[alignment] || alignment
58
+ end
48
59
 
49
60
  # The paragraph's last line stays ragged under full
50
61
  # justification.
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.10.12"
4
+ VERSION = "0.10.14"
5
5
  end
data/lib/idml.rb CHANGED
@@ -18,6 +18,7 @@ module Idml
18
18
  autoload :Validation, "#{__dir__}/idml/validation"
19
19
  autoload :Composition, "#{__dir__}/idml/composition"
20
20
  autoload :Geometry, "#{__dir__}/idml/geometry"
21
+ autoload :Schema, "#{__dir__}/idml/schema"
21
22
  autoload :TextEngine, "#{__dir__}/idml/text_engine"
22
23
  autoload :Render, "#{__dir__}/idml/render"
23
24
  autoload :CLI, "#{__dir__}/idml/cli"
@@ -10,21 +10,11 @@
10
10
  # Example:
11
11
  # scripts/rnc_to_lutaml.rb reference-docs/schemas/package/Stories/Story.rnc Story_Object
12
12
  #
13
- # The script reads the named element definition (e.g.,
14
- # `Story_Object = element Story { ... }`), extracts every `attribute
15
- # Name { Type }?` line, and prints Ruby code suitable for pasting
16
- # into a Lutaml::Model::Serializable subclass.
17
- #
18
- # Generated output:
19
- # attribute :self_attr, :string
20
- # attribute :first_line_indent, :float
21
- # # ... etc.
22
- # xml do
23
- # map_attribute "Self", to: :self_attr
24
- # map_attribute "FirstLineIndent", to: :first_line_indent
25
- # end
13
+ # RNC parsing lives in Idml::Schema::Rnc (shared with the
14
+ # schema-conformance spec); this script owns presentation only.
26
15
 
27
- require "strscan"
16
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
17
+ require "idml"
28
18
 
29
19
  RESERVED_RUBY_NAMES = %w[self class integer float string end do begin ensure
30
20
  rescue retry next return break yield nil true false].freeze
@@ -56,59 +46,25 @@ def ruby_attr_name(xml_name)
56
46
  base
57
47
  end
58
48
 
59
- def extract_element(rnc_path, element_name)
60
- src = File.read(rnc_path)
61
- pattern = /#{Regexp.escape(element_name)}\s*=\s*element\s+([\w:]+)\s*\{/
62
- match = src.match(pattern)
63
- abort "#{element_name} not found in #{rnc_path}" unless match
64
-
65
- xml_element_name = match[1]
66
- start_pos = match.end(0)
67
-
68
- # Walk the brace structure to find the matching close.
69
- depth = 1
70
- i = start_pos
71
- while i < src.length && depth.positive?
72
- case src[i]
73
- when "{" then depth += 1
74
- when "}" then depth -= 1
75
- end
76
- i += 1
77
- end
78
- body = src[start_pos...(i - 1)]
79
-
80
- [xml_element_name, body]
81
- end
82
-
83
- def extract_attributes(body)
84
- attrs = []
85
- body.scan(/^\s*attribute\s+(\w+)\s*\{\s*([^}]+)\}/) do |name, type|
86
- attrs << [name, type.strip]
87
- end
88
- attrs
89
- end
90
-
91
49
  rnc_path, element_name = ARGV
92
50
  abort "usage: #{$PROGRAM_NAME} <rnc-file> <ElementName>" unless rnc_path && element_name
93
51
 
94
- xml_root, body = extract_element(rnc_path, element_name)
95
- attrs = extract_attributes(body)
52
+ xml_root, attrs = Idml::Schema::Rnc.element_definition(rnc_path, element_name)
53
+ abort "#{element_name} not found in #{rnc_path}" unless xml_root
96
54
 
97
55
  puts "# xml root: <#{xml_root}>"
98
56
  puts "# source: #{rnc_path} (#{element_name})"
99
57
  puts "# #{attrs.length} attributes"
100
58
  puts
101
- puts "attribute :_placeholder_, :string # remove me"
102
- puts
103
- puts "xml do"
104
- puts " root \"#{xml_root.sub(/^idPkg:/, '')}\""
105
- attrs.each do |name, _type| # rubocop:disable Style/HashEachMethods
106
- puts %( map_attribute "#{name}", to: :#{ruby_attr_name(name)}")
107
- end
108
- puts "end"
109
- puts
110
59
  puts "# attribute declarations:"
111
60
  attrs.each do |name, type|
112
61
  lutaml = lutaml_type(type.split(/\s/).first)
113
62
  puts "attribute :#{ruby_attr_name(name)}, :#{lutaml}"
114
63
  end
64
+ puts
65
+ puts "xml do"
66
+ puts %( root "#{xml_root.sub(/^idPkg:/, '')}")
67
+ attrs.each do |name, _type| # rubocop:disable Style/HashEachMethods
68
+ puts %( map_attribute "#{name}", to: :#{ruby_attr_name(name)})
69
+ end
70
+ puts "end"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.12
4
+ version: 0.10.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-30 00:00:00.000000000 Z
11
+ date: 2026-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -215,6 +215,9 @@ files:
215
215
  - TODO.pdf/149-architecture-round-two.md
216
216
  - TODO.pdf/15-wire-spread-children.md
217
217
  - TODO.pdf/150-architecture-round-three.md
218
+ - TODO.pdf/151-schema-conformance.md
219
+ - TODO.pdf/152-rtl-alignment.md
220
+ - TODO.pdf/153-vertical-keeps.md
218
221
  - TODO.pdf/16-typed-model-pipeline.md
219
222
  - TODO.pdf/17-shape-rendering.md
220
223
  - TODO.pdf/18-text-rendering.md
@@ -352,6 +355,7 @@ files:
352
355
  - lib/idml/elements/ink.rb
353
356
  - lib/idml/elements/inner_glow_setting.rb
354
357
  - lib/idml/elements/inner_shadow_setting.rb
358
+ - lib/idml/elements/inset_spacing.rb
355
359
  - lib/idml/elements/layer.rb
356
360
  - lib/idml/elements/link.rb
357
361
  - lib/idml/elements/master_spread_object.rb
@@ -527,6 +531,8 @@ files:
527
531
  - lib/idml/render/style_resolver.rb
528
532
  - lib/idml/render/text_wrap_resolver.rb
529
533
  - lib/idml/render/wrap_contour.rb
534
+ - lib/idml/schema.rb
535
+ - lib/idml/schema/rnc.rb
530
536
  - lib/idml/text_engine.rb
531
537
  - lib/idml/text_engine/cjk_layout.rb
532
538
  - lib/idml/text_engine/justifier.rb