arrolio 0.1.0 → 0.1.2

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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Rakefile +15 -0
  4. data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
  5. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
  6. data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
  7. data/TODO.complete/71-cover-layout-parity.md +45 -0
  8. data/TODO.complete/72-table-layout-parity.md +49 -0
  9. data/TODO.complete/73-toc-leaders-parity.md +55 -0
  10. data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
  11. data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
  12. data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
  13. data/TODO.complete/77-text-formatting-parity.md +55 -0
  14. data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
  15. data/TODO.complete/79-line-breaking-parity.md +80 -0
  16. data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
  17. data/TODO.complete/81-conditional-style-refinements.md +70 -0
  18. data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
  19. data/TODO.complete/83-nested-list-support.md +74 -0
  20. data/TODO.complete/84-figure-svg-rendering.md +70 -0
  21. data/TODO.complete/85-locality-reference-formatting.md +48 -0
  22. data/TODO.complete/86-list-item-text-overflow.md +80 -0
  23. data/TODO.complete/87-odd-even-page-templates.md +55 -0
  24. data/TODO.complete/88-hyphenation-support.md +52 -0
  25. data/TODO.complete/89-table-rowspan.md +49 -0
  26. data/TODO.complete/90-mathml-stacked-layout.md +65 -0
  27. data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
  28. data/TODO.complete/92-table-cell-height-parity.md +66 -0
  29. data/TODO.complete/93-figure-text-searchability.md +49 -0
  30. data/TODO.complete/94-fo-space-resolution.md +63 -0
  31. data/TODO.complete/95-title-header-placement.md +55 -0
  32. data/TODO.complete/96-pagination-drift-parity.md +517 -0
  33. data/TODO.complete/README.md +58 -38
  34. data/lib/arrolio/config_driven_pipeline.rb +45 -1
  35. data/lib/arrolio/content/document.rb +4 -3
  36. data/lib/arrolio/content/section.rb +3 -0
  37. data/lib/arrolio/content/table.rb +25 -14
  38. data/lib/arrolio/embedded_block_extractor.rb +54 -0
  39. data/lib/arrolio/engine/paged.rb +109 -28
  40. data/lib/arrolio/flavor/manifest.rb +0 -1
  41. data/lib/arrolio/flavor/registry.rb +1 -1
  42. data/lib/arrolio/flowable.rb +21 -0
  43. data/lib/arrolio/flowables/figure_flowable.rb +17 -0
  44. data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
  45. data/lib/arrolio/flowables/group_flowable.rb +57 -0
  46. data/lib/arrolio/flowables/list_flowable.rb +46 -18
  47. data/lib/arrolio/flowables/note_flowable.rb +84 -9
  48. data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
  49. data/lib/arrolio/flowables/positioned_block.rb +55 -0
  50. data/lib/arrolio/flowables/rotated_text.rb +51 -0
  51. data/lib/arrolio/flowables/table_flowable.rb +303 -71
  52. data/lib/arrolio/flowables/text_flowable.rb +67 -11
  53. data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
  54. data/lib/arrolio/flowables.rb +4 -0
  55. data/lib/arrolio/font/embedder.rb +22 -3
  56. data/lib/arrolio/font_scanner.rb +133 -0
  57. data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
  58. data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
  59. data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
  60. data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
  61. data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
  62. data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
  63. data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
  64. data/lib/arrolio/generic_adapter.rb +263 -317
  65. data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
  66. data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
  67. data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
  68. data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
  69. data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
  70. data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
  71. data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
  72. data/lib/arrolio/generic_flow_builder.rb +92 -187
  73. data/lib/arrolio/glyph_measurer.rb +16 -5
  74. data/lib/arrolio/inline_run.rb +2 -1
  75. data/lib/arrolio/layout_spec/page_template.rb +14 -8
  76. data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
  77. data/lib/arrolio/math_ml.rb +7 -0
  78. data/lib/arrolio/output/page.rb +5 -4
  79. data/lib/arrolio/output/placed_box.rb +5 -2
  80. data/lib/arrolio/renderer/pdf/assets.rb +94 -0
  81. data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
  82. data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
  83. data/lib/arrolio/renderer/pdf.rb +140 -215
  84. data/lib/arrolio/style/definition.rb +7 -3
  85. data/lib/arrolio/style/registry.rb +6 -1
  86. data/lib/arrolio/table/auto_layout.rb +19 -10
  87. data/lib/arrolio/table/grid.rb +105 -0
  88. data/lib/arrolio/table.rb +1 -0
  89. data/lib/arrolio/text_layout/greedy.rb +15 -1
  90. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
  91. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
  92. data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
  93. data/lib/arrolio/text_layout/line.rb +10 -2
  94. data/lib/arrolio/version.rb +1 -1
  95. data/lib/arrolio.rb +8 -4
  96. data/scripts/parity_check.rb +52 -0
  97. data/scripts/parity_diff.rb +89 -0
  98. data/scripts/xsl_profiles/standoc.yml +8 -2
  99. data/scripts/xsl_to_config.rb +28 -1
  100. metadata +91 -5
@@ -0,0 +1,55 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: [70, 79]
5
+ layer: adapter
6
+ status: in_progress
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Section number formatting and several inline-formatting details
13
+ differ from the reference.
14
+
15
+ ## Status (2026-08-05)
16
+
17
+ - [x] **Section number "2.1" extracts as "2.1" not "21"** —
18
+ delimiter included via `autonum-delim` + `caption-label`
19
+ class match in `extract_from_heading`.
20
+ - [x] **`<sub>` produces baseline_shift=:sub, scale=0.7** —
21
+ `baseline_for_style` in `collect_inline_runs` checks the
22
+ inline_styles mapping; elements mapping to `:subscript` or
23
+ `:superscript` set the baseline state carried through the
24
+ walker.
25
+ - [x] **`<sup>` produces baseline_shift=:sup, scale=0.7** — same
26
+ path as `<sub>`.
27
+ - [x] **Note labels have em-dash suffix** —
28
+ `formatted_note_label(label)` appends the configured suffix
29
+ (default "—") from `flow_rules.note.label_suffix`.
30
+
31
+ ## Still pending
32
+
33
+ - [ ] **List bullet indent**: match the XSL's
34
+ `provisional-distance-between-starts` for list labels.
35
+ - [ ] **Hyperlink underline**: extend `Style::Definition` with an
36
+ `underline` boolean; renderer emits `/UL true` before
37
+ drawing underlined runs.
38
+ - [ ] **`<strong>` weight via Bold variant**: the `:strong` style
39
+ currently inherits the body font. Renderer should pick the
40
+ Bold variant automatically based on style name (similar to
41
+ how table headers do it in TODO 72).
42
+
43
+ ## Done-When
44
+
45
+ - [x] Section number "2.1" extracts as "2.1" not "21"
46
+ - [x] Note labels have the em-dash suffix
47
+ - [x] `<sub>` / `<sup>` get baseline_shift + scale
48
+ - [ ] List bullet indent matches reference
49
+ - [ ] Hyperlinks render underlined
50
+ - [ ] `<strong>` uses the Bold font variant
51
+
52
+ ## Expected improvement
53
+
54
+ Estimated 2-3% overall similarity once the remaining items land
55
+ (primarily from list indent matching and strong/bold rendering).
@@ -0,0 +1,62 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: [70]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Bibliography page renders content but with wrong formatting. The
13
+ reference uses numbered references `[1]`, `[2]` with hanging indent
14
+ so wrap lines align under the citation text, not under the tag.
15
+
16
+ A separate bug in `extract_bibliography` compounded this: the
17
+ `each_child` loop pushed Arrays into items without flattening,
18
+ hiding all bibliography items from the flow builder.
19
+
20
+ ## Status (2026-08-06)
21
+
22
+ - [x] **`extract_bibliography` now flattens** — uses `items.concat`
23
+ instead of `items <<`.
24
+ - [x] **`bibliography_item_flowable` emits a TextFlowable with
25
+ hanging_indent** equal to the marker width. First line: tag
26
+ flush left + citation starts after marker. Wrap lines:
27
+ indented by marker_w so they align under the citation text.
28
+ - [x] **TextFlowable supports hanging_indent** — new attribute
29
+ reduces effective width for layout and signals the renderer
30
+ to offset wrap lines.
31
+
32
+ ## Verification
33
+
34
+ Page 28 bibliography page now renders entries with proper hanging
35
+ indent:
36
+ ```
37
+ [1] OIML V 1:2013, International Vocabulary of Terms in Legal
38
+ Metrology (VIML)
39
+ [2] OIML V 2-200:2012, International Vocabulary of Metrology —
40
+ Basic and General Concepts and Associated Terms (VIM)
41
+ ```
42
+
43
+ ## Still pending
44
+
45
+ - **4pt gap between entries** — current `:bibitem` style has
46
+ `margin_top: 4`, `margin_bottom: 4` but it's not visible in the
47
+ extracted output.
48
+ - **Bibliography section heading style** — heading renders but
49
+ may not use the exact level-1 style.
50
+
51
+ ## Done-When
52
+
53
+ - [x] Bibliography entries render as single paragraphs
54
+ - [x] All entries appear in output (was 0 due to concat bug)
55
+ - [x] Hanging indent so wrap lines align with text start
56
+ - [ ] 4pt gap between entries (visual only — text diff unaffected)
57
+ - [ ] Page 28 similarity > 85% (currently ~50% due to body drift)
58
+
59
+ ## Measurement
60
+
61
+ `bundle exec rake parity:check` — page 28 bibliography page now
62
+ renders all 12 entries with proper hanging indent.
@@ -0,0 +1,80 @@
1
+ ---
2
+ priority: P2
3
+ impact: high
4
+ depends_on: [70]
5
+ layer: text
6
+ status: in_progress
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ FOP uses Knuth-Plass optimal fitting. The original Arroolio KP
13
+ implementation had multiple bugs producing per-word lines and
14
+ overfills. As of 2026-08-06, the major issues are fixed:
15
+
16
+ ## Status (2026-08-06)
17
+
18
+ Fixed bugs:
19
+ - [x] **Badness formula only counted negative ratios.** Now uses
20
+ `100 * |ratio|^3` clamped to 10_000 for both directions.
21
+ - [x] **TOLERANCE was 100.0.** Now 2.5, rejecting pathological
22
+ overfills.
23
+ - [x] **`build_placed_runs` skipped Glue items entirely** — every
24
+ space between words was dropped. Now emits `InlineRun(' ')`
25
+ per Glue.
26
+ - [x] **`total_stretch`/`total_shrink` summed the entire document.**
27
+ Renamed to `line_stretch(start, stop)` / `line_shrink(start, stop)`,
28
+ scoped to the current line.
29
+ - [x] **`Arroolio::Style::Definition` typo** (double-o). Fixed.
30
+ - [x] **Emergency stretch glue at the end was rendering as trailing
31
+ space.** build_placed_runs now skips Glue items with infinite
32
+ stretch.
33
+ - [x] **Each Glue became its own Tj block**, confusing pdftotext
34
+ into dropping spaces between words ("durabilityerror"). Now
35
+ Box+Glue items merge into a single placed_run per group.
36
+
37
+ Remaining gaps:
38
+ - **Hyphenation** — long words at line boundaries get pushed to
39
+ the next line, leaving the previous line underfull. Needs TeX
40
+ hyphenation patterns (external dependency).
41
+ - **Slight overfill on dense paragraphs** — KP picks overfull lines
42
+ when the alternative has higher demerits. Acceptable but not
43
+ pixel-perfect.
44
+ - **Consecutive-flag penalty** — flagged breaks (hyphens) next to
45
+ each other aren't penalised correctly.
46
+
47
+ ## Approach
48
+
49
+ 1. **Tune demerits to match FOP.** FOP uses Knuth-Plass with
50
+ specific constants. Compare our line break choices against the
51
+ reference PDF on >20 body paragraphs.
52
+
53
+ 2. **Add hyphenation.** Use `text-hyphen` or `ruby-hyphen` gem
54
+ (or a TeX pattern data file) to compute break opportunities
55
+ within words. ItemBuilder should emit `Box + Penalty(flagged)`
56
+ at each valid hyphenation point. This matches FOP/TeX behavior.
57
+
58
+ 3. **Handle underfull last line.** Last line of a justified
59
+ paragraph is left-aligned (already done in renderer).
60
+
61
+ ## Done-When
62
+
63
+ - [x] Knuth-Plass is the default line breaker for body text
64
+ - [x] Badness formula is correct for positive and negative ratios
65
+ - [x] KP produces multi-word lines (no per-word regression)
66
+ - [x] Spaces between words are preserved in extracted text
67
+ - [x] Last line of a justified paragraph is left-aligned
68
+ - [x] Emergency stretch glue doesn't render as trailing space
69
+ - [ ] Line break positions match FOP on >80% of body paragraphs
70
+ - [ ] Hyphenation is supported for long words at line boundaries
71
+ - [ ] Overall similarity > 60% on OIML r060/1 fixture
72
+
73
+ ## Measurement
74
+
75
+ `bundle exec rake parity:check` — current baseline 47.88%
76
+ overall, 30 pages vs 28 reference. Body pages 5-15 mostly OK
77
+ (55-87%); table pages 18-25 LOW (5-35%) due to TODO 72 work
78
+ still in progress.
79
+
80
+ Last measured: 2026-08-06.
@@ -0,0 +1,68 @@
1
+ ---
2
+ priority: P0
3
+ impact: critical
4
+ depends_on: [70]
5
+ layer: harness
6
+ status: done
7
+ est: 1d
8
+ completion_date: 2026-08-05
9
+ ---
10
+
11
+ ## Problem
12
+
13
+ Throughout development, "28 pages, matches the reference" was claimed
14
+ without ever running the PDF diff. The diff harness existed
15
+ (`Harness::PdfDiff`) but was not integrated into the development loop
16
+ or CI, and the script that called it had a unit bug that disguised
17
+ the actual parity number.
18
+
19
+ ## Approach (delivered)
20
+
21
+ 1. **`rake parity:check`** renders the OIML r060/1 fixture through
22
+ the generic pipeline and diffs it against the reference PDF,
23
+ printing per-page similarity and an overall score.
24
+
25
+ 2. **Unit-correct reporting.** `PdfDiff#similarity` is a Float in
26
+ [0, 1]; the script now multiplies by 100 for display and
27
+ compares against fractional thresholds (0.5 / 0.1) rather than
28
+ the previous buggy > 50 / > 10 integer checks.
29
+
30
+ 3. **`scripts/parity_check.rb` invokes `ParityCheck.run`** at the
31
+ bottom (previously the module was defined but never invoked —
32
+ the script exited silently with no output).
33
+
34
+ 4. **`ConfigDrivenPipeline` resolves fonts via FontScanner** so
35
+ the rendered PDF embeds the actual Jost/Times fonts the
36
+ reference uses, making the diff meaningful instead of comparing
37
+ against system fallbacks.
38
+
39
+ ## Done-When
40
+
41
+ - [x] `rake parity:check` renders + diffs + prints per-page report
42
+ - [x] Reporting shows percentages in [0, 100] not [0, 1]
43
+ - [x] Script invokes the diff (no silent exit)
44
+ - [x] Fonts are embedded in the rendered PDF, so the diff is
45
+ meaningful
46
+
47
+ ## Verification
48
+
49
+ ```
50
+ $ bundle exec rake parity:check
51
+ Overall similarity: 24.58%
52
+ Our pages: 30, Reference pages: 28
53
+
54
+ Page 1: 48.0% [PARTIAL]
55
+ Page 2: 100.0% [OK]
56
+ ...
57
+ ```
58
+
59
+ ## Not Yet Done
60
+
61
+ - `spec/arrolio/parity_spec.rb` regression spec — the next parity
62
+ PR should add this with a floor threshold (start at 20%, raise
63
+ as TODOs land). The spec belongs in the gem'sRSpec suite so CI
64
+ fails on regressions.
65
+
66
+ - GitHub Actions wiring — the spec runs locally via `bundle exec
67
+ rspec`; CI invocation is added in the workflow file (`.github/
68
+ workflows/*.yml`) by the release engineer.
@@ -0,0 +1,70 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [70]
5
+ layer: adapter
6
+ status: in_progress
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The XSL has many conditional style refinements (`refine_*` templates)
13
+ that apply margin/space properties only in specific contexts (annex
14
+ headings, list-item paragraphs, term definitions). The converter
15
+ captures these as unconditional properties in `layout_spec.yml`,
16
+ causing spacing to be wrong for most content.
17
+
18
+ ## Bugs found (2026-08-08)
19
+
20
+ - [x] **heading_1 margin_bottom: 30pt applied unconditionally.** The
21
+ XSL `refine_title-style` only sets `margin-bottom: 30pt` when
22
+ `ancestor::mn:annex`. Our YAML applied it to ALL level-1
23
+ headings, adding 30pt × ~8 headings = 240pt of dead space.
24
+ Fixed: removed from heading_1; needs separate `heading_1_annex`
25
+ style.
26
+ - [x] **edition_label always included year.** XSL `get_edition` uses
27
+ `version/revision-date` (child element), which is empty when
28
+ `<version>` has text content. Our profile mapped
29
+ `revision_date: version` (text), always extracting the year.
30
+ Fixed: profile now uses `version/revision-date`, and
31
+ `derive_metadata_fields` falls back to label without year.
32
+ - [x] **Doc title block missing.** XSL renders zzSTDTitle1 paragraphs
33
+ as "Part N - title-part" from bibdata. Our adapter extracted the
34
+ paragraph text instead of constructing from metadata. Fixed:
35
+ `extract_title_block` now constructs from `part_number` and
36
+ `title_part` metadata.
37
+ - [ ] **Term entry spacing is 3× too tall.** Each term entry component
38
+ (number, preferred, definition) inherits `term` style with
39
+ margin_bottom: 12pt. The XSL applies it once per entry (on the
40
+ outer block). Fix needs either a container flowable or
41
+ per-component style override.
42
+ - [ ] **heading_2 space-after: 12pt may be unconditional.** XSL
43
+ `refine_title-style` level 2 adds `space-after: 12pt`
44
+ unconditionally. Verify against reference.
45
+ - [ ] **clause-style space-before/space-after.** XSL
46
+ `refine_clause_style` adds space properties conditionally.
47
+
48
+ ## Approach
49
+
50
+ 1. **Audit all `refine_*` templates** in the XSL for conditional
51
+ attributes. Map each condition to a style selector.
52
+
53
+ 2. **Extend the converter** to emit context-specific styles (e.g.,
54
+ `heading_1_annex`, `p_in_list`, `term_definition`) when the
55
+ XSL condition applies.
56
+
57
+ 3. **Or:** model FO space-resolution rules (space-after of previous
58
+ vs space-before of next → take the max, not the sum).
59
+
60
+ ## Done-When
61
+
62
+ - [ ] All conditional margins match the reference PDF on pages 5-15
63
+ - [ ] Term entries use 12pt spacing per entry, not per component
64
+ - [ ] Annex headings have 30pt margin; body headings don't
65
+ - [ ] Overall parity > 55% on OIML r060/1 fixture
66
+
67
+ ## Measurement
68
+
69
+ `bundle exec rake parity:check` — current baseline 53.53%.
70
+ Last measured: 2026-08-08.
@@ -0,0 +1,65 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [66]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The standoc presentation XML contains paired semantic + format
13
+ elements for cross-references, terms, and identifiers:
14
+
15
+ ```xml
16
+ <xref target="X" id="id123">clause=5.1.1</xref>
17
+ <semx element="xref" source="id123">
18
+ <fmt-xref target="X">clause=5.1.1</fmt-xref>
19
+ </semx>
20
+ ```
21
+
22
+ Both the semantic `<xref>` and presentation `<fmt-xref>` carry the
23
+ same text. Our inline walker was collecting text from BOTH, producing
24
+ doubled output: `clause=5.1.1clause=5.1.1`.
25
+
26
+ ## Fix (2026-08-08)
27
+
28
+ Added semantic elements to `skip_metadata_elements` in the standoc
29
+ profile:
30
+
31
+ ```yaml
32
+ skip_metadata_elements:
33
+ ...
34
+ - xref
35
+ - eref
36
+ - identifier
37
+ ```
38
+
39
+ The presentation equivalents (`fmt-xref`, `fmt-identifier`,
40
+ `fmt-preferred`) remain in `inline_styles` and provide the display
41
+ text.
42
+
43
+ ## Remaining gaps
44
+
45
+ - [ ] **`clause=5.1.1` should render as "clause 5.1.1" or "5.1.1".**
46
+ The `=` in the raw text is a locality separator. The XSL has
47
+ i18n templates for locality rendering. Needs locality parsing
48
+ in the inline walker.
49
+ - [ ] **Some fmt-* elements may also be doubled** when their semantic
50
+ counterpart has no `id` attribute (can't match via `source`).
51
+ Audit all element pairs.
52
+ - [ ] **`fmt-name` in skip_elements vs inline.** The block-level
53
+ `convert_children` skips `fmt-name`, but the inline
54
+ `collect_inline_runs` doesn't. Verify consistency.
55
+
56
+ ## Done-When
57
+
58
+ - [x] No doubled xref/eref/identifier text in output
59
+ - [ ] Locality references render as "clause X.Y.Z" not "clause=X.Y.Z"
60
+ - [ ] All semantic/presentation pairs deduplicated
61
+
62
+ ## Measurement
63
+
64
+ Parity improved from 50.52% → 53.53% after deduplication.
65
+ Last measured: 2026-08-08.
@@ -0,0 +1,74 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [66]
5
+ layer: adapter
6
+ status: done
7
+ est: 0.5d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The adapter's `convert_list` only collected `<p>` paragraph children
13
+ of `<li>` elements, silently dropping nested `<ol>`/`<ul>` lists.
14
+ Similarly, `list_flowable` in the flow builder only converted
15
+ `Content::Paragraph` items, skipping `Content::List`.
16
+
17
+ This caused entire sub-lists to disappear from the output. For
18
+ example, item b) in section 6.1:
19
+
20
+ ```
21
+ b) For legally relevant software... shall be applied.
22
+ 1) The exception described in clause 5.1.1...
23
+ 2) The level of conformity...
24
+ 3) Updating the legally relevant software...
25
+ 4) The software documentation shall include...
26
+ ```
27
+
28
+ Items 1-4 were completely absent from our output.
29
+
30
+ ## Fix (2026-08-08)
31
+
32
+ ### Adapter (`convert_list`)
33
+
34
+ ```ruby
35
+ content = []
36
+ each_element(li) do |child|
37
+ next if child.parent && child.parent != li # direct children only
38
+
39
+ if child.name == para_name
40
+ content << convert_paragraph(child)
41
+ elsif list_mapping.key?(child.name)
42
+ nested_kind = list_mapping[child.name]['kind']&.to_sym || :bullet
43
+ content << convert_list(child, kind: nested_kind)
44
+ end
45
+ end
46
+ ```
47
+
48
+ ### Flow builder (`list_flowable`)
49
+
50
+ ```ruby
51
+ def flowable_for_list_content(content)
52
+ case content
53
+ when Content::Paragraph then [paragraph_flowable(content)]
54
+ when Content::List then [list_flowable(content)]
55
+ else []
56
+ end
57
+ end
58
+ ```
59
+
60
+ The `ListFlowable` already handles arbitrary body flowables via
61
+ `f.emit`, so nested `ListFlowable` renders correctly at increased
62
+ indent.
63
+
64
+ ## Done-When
65
+
66
+ - [x] Nested `<ol>`/`<ul>` inside `<li>` are collected by adapter
67
+ - [x] `Content::List` inside list item content is converted by flow builder
68
+ - [x] Nested list markers render at increased indent
69
+ - [x] Item b) sub-items 1-4 visible in output
70
+
71
+ ## Measurement
72
+
73
+ Verified on OIML r060/1 page 26: items 1-4 now visible.
74
+ Last measured: 2026-08-08.
@@ -0,0 +1,70 @@
1
+ ---
2
+ priority: P1
3
+ impact: critical
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Figures in the standoc presentation XML contain inline `<svg>` elements
13
+ inside `<image>` tags. The adapter was only looking for external image
14
+ file references (`images/figure-1.svg`) which don't exist, causing all
15
+ 4 figures to be missing from the output — the biggest source of
16
+ pagination drift.
17
+
18
+ ## Fix (2026-08-09)
19
+
20
+ ### Adapter (`extract_figure_image`)
21
+
22
+ When a `<figure>` has an `<image>` element:
23
+ 1. Search recursively for `<svg>` inside the image element
24
+ 2. If found, serialize the SVG XML and extract viewBox dimensions
25
+ 3. Store as `Content::Image` with `inline-svg:` prefix
26
+
27
+ When no `<image>` element exists, search recursively in the figure for
28
+ standalone `<svg>`.
29
+
30
+ ### Flow builder (`image_flowable`)
31
+
32
+ Recognize the `inline-svg:` prefix:
33
+ 1. Extract SVG XML from the prefixed source string
34
+ 2. Write to a temp `.svg` file
35
+ 3. Pass to `ImageFlowable` which delegates to the renderer
36
+
37
+ ### Renderer (existing)
38
+
39
+ The renderer already rasterizes `.svg` files via `rsvg-convert` to PNG
40
+ and caches the result. No renderer changes needed.
41
+
42
+ ### Config (`flow_rules.yml`)
43
+
44
+ Set `max_display_width: 450` (was default 106) to allow full-width
45
+ figure rendering.
46
+
47
+ ## Impact
48
+
49
+ Parity: 53.53% → **56.98%** (+3.45%)
50
+ Page count: 26 → **27** (reference 28)
51
+
52
+ | Page | Before | After |
53
+ |------|--------|-------|
54
+ | 5 | 94.1% | 99.6% |
55
+ | 9 | 35.7% | 87.4% |
56
+ | 12 | 37.4% | 64.0% |
57
+ | 14 | 45.8% | 79.5% |
58
+
59
+ ## Done-When
60
+
61
+ - [x] Inline SVG extracted from `<image>` elements
62
+ - [x] viewBox dimensions used for aspect ratio
63
+ - [x] rsvg-convert rasterizes SVG to PNG
64
+ - [x] Figures appear in output at correct size
65
+ - [x] Parity improved by 3.45%
66
+
67
+ ## Measurement
68
+
69
+ `bundle exec rake parity:check` — 56.98%.
70
+ Last measured: 2026-08-09.
@@ -0,0 +1,48 @@
1
+ ---
2
+ priority: P2
3
+ impact: medium
4
+ depends_on: [84]
5
+ layer: adapter
6
+ status: done
7
+ est: 0.5d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Cross-references to clauses in the document rendered as raw locality
13
+ strings: `clause=5.1.1` instead of `clause 5.1.1`. The `=` is a
14
+ locality separator used in standoc presentation XML that the XSL i18n
15
+ templates replace with a space.
16
+
17
+ ## Fix (2026-08-09)
18
+
19
+ Added `format_locality_text` method in the inline walker. When text is
20
+ collected from inside a `<fmt-xref>` element, the `=` between a word
21
+ and a value is replaced with a space:
22
+
23
+ ```ruby
24
+ def format_locality_text(text)
25
+ text.gsub(/([a-zA-Z]+)=/, '\\1 ')
26
+ end
27
+ ```
28
+
29
+ Examples:
30
+ - `clause=5.1.1` → `clause 5.1.1`
31
+ - `table=2` → `table 2`
32
+ - `figure=4` → `figure 4`
33
+
34
+ The walker tracks `in_xref` context to only apply this transformation
35
+ inside cross-references, not globally.
36
+
37
+ ## Done-When
38
+
39
+ - [x] `clause=X` renders as `clause X`
40
+ - [x] Only applied inside fmt-xref elements
41
+ - [x] Non-locality `=` preserved (e.g., in URLs or formulas)
42
+ - [x] Specs cover locality formatting
43
+
44
+ ## Measurement
45
+
46
+ Improves readability of ~15 cross-references. Parity impact is small
47
+ (text content change, not spacing).
48
+ Last measured: 2026-08-09.
@@ -0,0 +1,80 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [79]
5
+ layer: text
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ List item body paragraphs sometimes lose text after cross-references.
13
+ The placed box correctly contains 2 lines of text (verified via debug),
14
+ but the renderer only outputs line 1 — line 2 is silently dropped.
15
+
16
+ ## Example
17
+
18
+ Input paragraph:
19
+ ```
20
+ For legally relevant software of digital load cells the following
21
+ statements according to [OIML_D_31_2008] shall be applied.
22
+ ```
23
+
24
+ Rendered PDF (before fix): the line ran to x=597 on a 595pt page —
25
+ "be applied." was drawn off-page and clipped by readers.
26
+
27
+ ## Root cause (found 2026-08-17)
28
+
29
+ NOT a font-metrics mismatch — TTF advance widths match the embedded
30
+ render exactly. The Knuth-Plass breaker rejected every intermediate
31
+ break of the paragraph (best candidate ratio 2.68 > TOLERANCE 2.5),
32
+ so the only surviving path was the FORCED final break, which bypasses
33
+ the ratio check: the whole paragraph rendered as one 552.6pt line on
34
+ a 426.7pt measure.
35
+
36
+ ## Fix
37
+
38
+ - `KnuthPlass::Breaker`: TeX-style emergency-stretch pass. When the
39
+ first pass yields no feasible solution, re-solve with
40
+ EMERGENCY_STRETCH added to every line's stretchability — the
41
+ paragraph breaks properly instead of overflowing.
42
+ - `KnuthPlass::Breaker`: removed the `prune?` cap that disabled
43
+ active nodes beyond `line_widths.length + 10` lines — any
44
+ paragraph or table cell longer than 11 lines got its tail crammed
45
+ into one overfull line (visible as overlapping table-cell text).
46
+ - `KnuthPlass::Breaker#build_placed_runs`: style-preserving run
47
+ grouping. A line is merged into one text operation per rendering
48
+ signature; bold/italic/sub/superscript runs keep their formatting
49
+ (previously the whole line took the first run's style). Trailing
50
+ glue at the break is dropped so justify counts real word gaps.
51
+ - `TextFlowable#collect_runs`: re-inserts the word separator that a
52
+ line break consumed when a split paragraph is rebuilt — otherwise
53
+ re-layout glues boundary words ("…increasing anddecreasing").
54
+ Hyphen breaks gain no space ("analogue-active" stays joined).
55
+ - `Renderer::Pdf#render_line_runs`: justified lines are drawn as one
56
+ positioned text operation per word (FOP's model) with the stretch
57
+ accumulated in each word's x. PDF word spacing (Tw) does not apply
58
+ to two-byte CID-encoded embedded fonts, so justification cannot
59
+ rely on it; the old boundary-shift approach concentrated the whole
60
+ line's stretch at run boundaries (34pt holes around math runs).
61
+ - `GenericFlowBuilder`: `require 'tmpdir'` (was a transitive-require
62
+ accident).
63
+ - Prefix sums for item widths/stretch/shrink make width queries O(1).
64
+
65
+ ## Done-When
66
+
67
+ - [x] "be applied." renders after "shall" in list item b)
68
+ - [x] Text never overflows body width in list items
69
+ - [x] GlyphMeasurer widths match rendered widths (verified: measured
70
+ 501.6pt vs rendered 501.2pt for the failing line)
71
+ - [x] Specs cover list item with long inline tokens
72
+
73
+ ## Measurement
74
+
75
+ Overall similarity 62.7% → 59.9% (2026-08-17). Text pages unchanged;
76
+ the drop is pages 23–27, where previously-overlapping table cells now
77
+ occupy their true height. Our table rows are taller than FOP's
78
+ (column-width debt — TODO 72/92); the old render accidentally matched
79
+ page boundaries while its cell text overlapped. Recovering the metric
80
+ requires table layout parity, not reverting these fixes.