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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/Rakefile +15 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
- data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
- data/TODO.complete/71-cover-layout-parity.md +45 -0
- data/TODO.complete/72-table-layout-parity.md +49 -0
- data/TODO.complete/73-toc-leaders-parity.md +55 -0
- data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
- data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
- data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
- data/TODO.complete/77-text-formatting-parity.md +55 -0
- data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
- data/TODO.complete/79-line-breaking-parity.md +80 -0
- data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
- data/TODO.complete/81-conditional-style-refinements.md +70 -0
- data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
- data/TODO.complete/83-nested-list-support.md +74 -0
- data/TODO.complete/84-figure-svg-rendering.md +70 -0
- data/TODO.complete/85-locality-reference-formatting.md +48 -0
- data/TODO.complete/86-list-item-text-overflow.md +80 -0
- data/TODO.complete/87-odd-even-page-templates.md +55 -0
- data/TODO.complete/88-hyphenation-support.md +52 -0
- data/TODO.complete/89-table-rowspan.md +49 -0
- data/TODO.complete/90-mathml-stacked-layout.md +65 -0
- data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
- data/TODO.complete/92-table-cell-height-parity.md +66 -0
- data/TODO.complete/93-figure-text-searchability.md +49 -0
- data/TODO.complete/94-fo-space-resolution.md +63 -0
- data/TODO.complete/95-title-header-placement.md +55 -0
- data/TODO.complete/96-pagination-drift-parity.md +517 -0
- data/TODO.complete/README.md +58 -38
- data/lib/arrolio/config_driven_pipeline.rb +45 -1
- data/lib/arrolio/content/document.rb +4 -3
- data/lib/arrolio/content/section.rb +3 -0
- data/lib/arrolio/content/table.rb +25 -14
- data/lib/arrolio/embedded_block_extractor.rb +54 -0
- data/lib/arrolio/engine/paged.rb +109 -28
- data/lib/arrolio/flavor/manifest.rb +0 -1
- data/lib/arrolio/flavor/registry.rb +1 -1
- data/lib/arrolio/flowable.rb +21 -0
- data/lib/arrolio/flowables/figure_flowable.rb +17 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
- data/lib/arrolio/flowables/group_flowable.rb +57 -0
- data/lib/arrolio/flowables/list_flowable.rb +46 -18
- data/lib/arrolio/flowables/note_flowable.rb +84 -9
- data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
- data/lib/arrolio/flowables/positioned_block.rb +55 -0
- data/lib/arrolio/flowables/rotated_text.rb +51 -0
- data/lib/arrolio/flowables/table_flowable.rb +303 -71
- data/lib/arrolio/flowables/text_flowable.rb +67 -11
- data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
- data/lib/arrolio/flowables.rb +4 -0
- data/lib/arrolio/font/embedder.rb +22 -3
- data/lib/arrolio/font_scanner.rb +133 -0
- data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
- data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
- data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
- data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
- data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
- data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
- data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
- data/lib/arrolio/generic_adapter.rb +263 -317
- data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
- data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
- data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
- data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
- data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
- data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
- data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
- data/lib/arrolio/generic_flow_builder.rb +92 -187
- data/lib/arrolio/glyph_measurer.rb +16 -5
- data/lib/arrolio/inline_run.rb +2 -1
- data/lib/arrolio/layout_spec/page_template.rb +14 -8
- data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
- data/lib/arrolio/math_ml.rb +7 -0
- data/lib/arrolio/output/page.rb +5 -4
- data/lib/arrolio/output/placed_box.rb +5 -2
- data/lib/arrolio/renderer/pdf/assets.rb +94 -0
- data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
- data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
- data/lib/arrolio/renderer/pdf.rb +140 -215
- data/lib/arrolio/style/definition.rb +7 -3
- data/lib/arrolio/style/registry.rb +6 -1
- data/lib/arrolio/table/auto_layout.rb +19 -10
- data/lib/arrolio/table/grid.rb +105 -0
- data/lib/arrolio/table.rb +1 -0
- data/lib/arrolio/text_layout/greedy.rb +15 -1
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
- data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
- data/lib/arrolio/text_layout/line.rb +10 -2
- data/lib/arrolio/version.rb +1 -1
- data/lib/arrolio.rb +8 -4
- data/scripts/parity_check.rb +52 -0
- data/scripts/parity_diff.rb +89 -0
- data/scripts/xsl_profiles/standoc.yml +8 -2
- data/scripts/xsl_to_config.rb +28 -1
- 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.
|