idml 0.10.12 → 0.10.13

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: 2f2a7b73c56645a87269780fc0cab9f998f5a6bf0d167b0ce9433dba9a8019cd
4
+ data.tar.gz: e4bb1522273937a4c2cf5631de1927f4583f63d16f2b351f893921407d89dc88
5
5
  SHA512:
6
- metadata.gz: 6c06eb1149c2fed295382a4ce65504d12c821d884ffa6cd145f9ac96e0837c82b1dbfdd16abd082e84366dd6c0bfd6edc8670e187d71785f1de0e7a415cc9892
7
- data.tar.gz: 12352d559b36427ec1be4ce71e31cfc149e24ec52a1fe70b65c3ecc720575c1a4f7a9aab8b96e67329747f33bc2005494c6f0c2034508b642edf37ba917a45ca
6
+ metadata.gz: 83756750d549a02ff7080686509ac45acce77405db6c9d3ba0ee6dae3643622b8eb89198d28fead77047dac50bdcf9491bdab05b7e03d02e18560f1021fa5360
7
+ data.tar.gz: 8a9fc59030029b4342019b205f9fe7da46ae6296729d641c49c199391cd5765ffea195dfca6cde46ff0df6518f496dcf9a1b66dc5dad0b808503230be8057107
@@ -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,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"
@@ -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
@@ -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
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.13"
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.13
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,7 @@ 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
218
219
  - TODO.pdf/16-typed-model-pipeline.md
219
220
  - TODO.pdf/17-shape-rendering.md
220
221
  - TODO.pdf/18-text-rendering.md
@@ -352,6 +353,7 @@ files:
352
353
  - lib/idml/elements/ink.rb
353
354
  - lib/idml/elements/inner_glow_setting.rb
354
355
  - lib/idml/elements/inner_shadow_setting.rb
356
+ - lib/idml/elements/inset_spacing.rb
355
357
  - lib/idml/elements/layer.rb
356
358
  - lib/idml/elements/link.rb
357
359
  - lib/idml/elements/master_spread_object.rb
@@ -527,6 +529,8 @@ files:
527
529
  - lib/idml/render/style_resolver.rb
528
530
  - lib/idml/render/text_wrap_resolver.rb
529
531
  - lib/idml/render/wrap_contour.rb
532
+ - lib/idml/schema.rb
533
+ - lib/idml/schema/rnc.rb
530
534
  - lib/idml/text_engine.rb
531
535
  - lib/idml/text_engine/cjk_layout.rb
532
536
  - lib/idml/text_engine/justifier.rb