idml 0.10.11 → 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 +4 -4
- data/TODO.pdf/150-architecture-round-three.md +41 -0
- data/TODO.pdf/151-schema-conformance.md +48 -0
- data/lib/idml/elements/inset_spacing.rb +31 -0
- data/lib/idml/elements/properties.rb +2 -0
- data/lib/idml/elements/text_frame_preference.rb +4 -9
- data/lib/idml/elements.rb +1 -0
- data/lib/idml/render/pipeline.rb +10 -8
- data/lib/idml/render/renderers/text_frame_renderer.rb +25 -5
- data/lib/idml/render/text_wrap_resolver.rb +0 -29
- data/lib/idml/schema/rnc.rb +88 -0
- data/lib/idml/schema.rb +12 -0
- data/lib/idml/version.rb +1 -1
- data/lib/idml.rb +1 -0
- data/scripts/rnc_to_lutaml.rb +13 -57
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2f2a7b73c56645a87269780fc0cab9f998f5a6bf0d167b0ce9433dba9a8019cd
|
|
4
|
+
data.tar.gz: e4bb1522273937a4c2cf5631de1927f4583f63d16f2b351f893921407d89dc88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83756750d549a02ff7080686509ac45acce77405db6c9d3ba0ee6dae3643622b8eb89198d28fead77047dac50bdcf9491bdab05b7e03d02e18560f1021fa5360
|
|
7
|
+
data.tar.gz: 8a9fc59030029b4342019b205f9fe7da46ae6296729d641c49c199391cd5765ffea195dfca6cde46ff0df6518f496dcf9a1b66dc5dad0b808503230be8057107
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# TODO PDF 150: Architecture review round three — seam repairs
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-30
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Round-three review findings (after TODOs 148-149):
|
|
8
|
+
|
|
9
|
+
1. **Page-dimensions seam leak (bug)**: `ImageCollector` and
|
|
10
|
+
`HyperlinkEmitter` received a hardcoded `DEFAULT_HEIGHT = 792`
|
|
11
|
+
while pages rendered at per-page dimensions — both use the
|
|
12
|
+
height for the IDML→PDF y-flip, so images and link rectangles
|
|
13
|
+
misplaced on any non-letter page.
|
|
14
|
+
2. **Dead interface kept alive by its tests**:
|
|
15
|
+
`TextWrapResolver#overlap_width` (+ `line_overlap`) had zero
|
|
16
|
+
lib callers since TODO 138 — only its own spec — and encoded
|
|
17
|
+
sum-semantics for multiple contours where the real interface
|
|
18
|
+
(`wrap_adjustment`) uses max. The spec suite asserted behavior
|
|
19
|
+
the renderer no longer had.
|
|
20
|
+
3. **Thirteen copies of the synthetic-package triplet**: every
|
|
21
|
+
engine-path spec repeated `Dir.mktmpdir → Package.write →
|
|
22
|
+
Package.new`, with drifting names and inconsistent part sets.
|
|
23
|
+
|
|
24
|
+
## Solution
|
|
25
|
+
|
|
26
|
+
- Pipeline threads per-page `dims[:height]` into both consumers;
|
|
27
|
+
the constants stay only as `page_dimensions_for` fallbacks.
|
|
28
|
+
- `overlap_width` / `line_overlap` deleted; all 15 spec call
|
|
29
|
+
sites migrated to `wrap_adjustment` (the "sums contours" spec
|
|
30
|
+
now documents max-combination honestly). The polygon variant
|
|
31
|
+
`WrapContour.overlap_width` stays — it is load-bearing.
|
|
32
|
+
- `build_package(parts, name)` helper in spec_helper; thirteen
|
|
33
|
+
triplet copies replaced; three local helpers that shadowed the
|
|
34
|
+
global renamed (`anchored_fixture`, `decorated_fixture`,
|
|
35
|
+
`footnote_fixture`, `endnote_fixture`, `footnote_package`).
|
|
36
|
+
|
|
37
|
+
## Files
|
|
38
|
+
|
|
39
|
+
- `lib/idml/render/pipeline.rb`
|
|
40
|
+
- `lib/idml/render/text_wrap_resolver.rb`
|
|
41
|
+
- `spec/spec_helper.rb` + 13 spec files
|
|
@@ -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 :
|
|
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
|
-
|
|
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"
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -96,9 +96,6 @@ module Idml
|
|
|
96
96
|
position_tracker, condition_filter, style_lookup,
|
|
97
97
|
page_offset)
|
|
98
98
|
pages = spread.spread.flat_map(&:page)
|
|
99
|
-
image_refs = ImageCollector.new(writer: writer,
|
|
100
|
-
base_dir: File.dirname(@package.path),
|
|
101
|
-
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
102
99
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
103
100
|
font_resource, font_metrics, font_map,
|
|
104
101
|
structure, position_tracker, condition_filter,
|
|
@@ -108,21 +105,26 @@ module Idml
|
|
|
108
105
|
pages.each do |page|
|
|
109
106
|
current += 1
|
|
110
107
|
dims = page_dimensions_for(page)
|
|
108
|
+
image_refs = ImageCollector.new(
|
|
109
|
+
writer: writer,
|
|
110
|
+
base_dir: File.dirname(@package.path),
|
|
111
|
+
page_height: dims[:height],
|
|
112
|
+
).collect(spread)
|
|
111
113
|
canvas = writer.add_page(width: dims[:width], height: dims[:height])
|
|
112
114
|
renderer.render(canvas, spread, page_width: dims[:width],
|
|
113
115
|
page_height: dims[:height],
|
|
114
116
|
image_refs: image_refs,
|
|
115
117
|
page_index: current)
|
|
116
|
-
emit_hyperlinks(writer, spread, current,
|
|
117
|
-
position_tracker)
|
|
118
|
+
emit_hyperlinks(writer, spread, current, dims[:height],
|
|
119
|
+
layer_filter, position_tracker)
|
|
118
120
|
end
|
|
119
121
|
current
|
|
120
122
|
end
|
|
121
123
|
|
|
122
|
-
def emit_hyperlinks(writer, spread, page_index,
|
|
123
|
-
position_tracker)
|
|
124
|
+
def emit_hyperlinks(writer, spread, page_index, page_height,
|
|
125
|
+
layer_filter, position_tracker)
|
|
124
126
|
HyperlinkEmitter.new(writer: writer, package: @package,
|
|
125
|
-
page_height:
|
|
127
|
+
page_height: page_height,
|
|
126
128
|
layer_filter: layer_filter,
|
|
127
129
|
position_tracker: position_tracker)
|
|
128
130
|
.emit_for(spread, page_index)
|
|
@@ -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
|
-
|
|
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:
|
|
254
|
-
inset_bottom:
|
|
255
|
-
inset_left:
|
|
256
|
-
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
|
|
@@ -159,28 +159,6 @@ module Idml
|
|
|
159
159
|
end
|
|
160
160
|
private :interval_side_adjustment
|
|
161
161
|
|
|
162
|
-
# Returns the total horizontal overlap of all contours with a
|
|
163
|
-
# text line at the given y range within the given x range.
|
|
164
|
-
# Sum, not max — multiple overlapping contours compound.
|
|
165
|
-
# Returns 0.0 when there's no overlap. Inverse contours
|
|
166
|
-
# reduce by the frame width minus their own width (text may
|
|
167
|
-
# only flow inside).
|
|
168
|
-
def overlap_width(line_y, line_height, frame_x, frame_right)
|
|
169
|
-
@contours.sum do |contour|
|
|
170
|
-
width = if contour.is_a?(WrapContour::Shape)
|
|
171
|
-
WrapContour.overlap_width(contour, line_y,
|
|
172
|
-
line_height, frame_x,
|
|
173
|
-
frame_right)
|
|
174
|
-
else
|
|
175
|
-
line_overlap(contour, line_y, line_height,
|
|
176
|
-
frame_x, frame_right)[:width]
|
|
177
|
-
end
|
|
178
|
-
next width unless contour.inverse
|
|
179
|
-
|
|
180
|
-
(frame_right - frame_x) - width
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
162
|
# The wrap shape for an item: a bounding-box rectangle
|
|
185
163
|
# (BoundingBoxTextWrap — the legacy "BoundingBox" spelling is
|
|
186
164
|
# also accepted from early synthetic fixtures) or a flattened
|
|
@@ -250,13 +228,6 @@ module Idml
|
|
|
250
228
|
end
|
|
251
229
|
private_class_method :contour_mode?
|
|
252
230
|
|
|
253
|
-
def line_overlap(contour, line_y, line_height, frame_x, frame_right)
|
|
254
|
-
return { width: 0.0 } unless y_overlap?(contour, line_y, line_height)
|
|
255
|
-
return { width: 0.0 } unless x_overlap?(contour, frame_x, frame_right)
|
|
256
|
-
|
|
257
|
-
{ width: x_overlap_width(contour, frame_x, frame_right) }
|
|
258
|
-
end
|
|
259
|
-
|
|
260
231
|
def y_overlap?(contour, line_y, line_height)
|
|
261
232
|
line_top = line_y + line_height
|
|
262
233
|
line_bottom = line_y
|
|
@@ -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
|
data/lib/idml/schema.rb
ADDED
|
@@ -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
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"
|
data/scripts/rnc_to_lutaml.rb
CHANGED
|
@@ -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
|
-
#
|
|
14
|
-
#
|
|
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
|
-
|
|
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,
|
|
95
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-08-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -214,6 +214,8 @@ files:
|
|
|
214
214
|
- TODO.pdf/148-architecture-improvements.md
|
|
215
215
|
- TODO.pdf/149-architecture-round-two.md
|
|
216
216
|
- TODO.pdf/15-wire-spread-children.md
|
|
217
|
+
- TODO.pdf/150-architecture-round-three.md
|
|
218
|
+
- TODO.pdf/151-schema-conformance.md
|
|
217
219
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
218
220
|
- TODO.pdf/17-shape-rendering.md
|
|
219
221
|
- TODO.pdf/18-text-rendering.md
|
|
@@ -351,6 +353,7 @@ files:
|
|
|
351
353
|
- lib/idml/elements/ink.rb
|
|
352
354
|
- lib/idml/elements/inner_glow_setting.rb
|
|
353
355
|
- lib/idml/elements/inner_shadow_setting.rb
|
|
356
|
+
- lib/idml/elements/inset_spacing.rb
|
|
354
357
|
- lib/idml/elements/layer.rb
|
|
355
358
|
- lib/idml/elements/link.rb
|
|
356
359
|
- lib/idml/elements/master_spread_object.rb
|
|
@@ -526,6 +529,8 @@ files:
|
|
|
526
529
|
- lib/idml/render/style_resolver.rb
|
|
527
530
|
- lib/idml/render/text_wrap_resolver.rb
|
|
528
531
|
- lib/idml/render/wrap_contour.rb
|
|
532
|
+
- lib/idml/schema.rb
|
|
533
|
+
- lib/idml/schema/rnc.rb
|
|
529
534
|
- lib/idml/text_engine.rb
|
|
530
535
|
- lib/idml/text_engine/cjk_layout.rb
|
|
531
536
|
- lib/idml/text_engine/justifier.rb
|