arrolio 0.1.0
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 +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +343 -0
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +154 -0
- data/Gemfile +15 -0
- data/LICENSE +24 -0
- data/README.adoc +95 -0
- data/Rakefile +21 -0
- data/TODO.complete/01-true-type-font-metrics.md +56 -0
- data/TODO.complete/02-font-embedding.md +51 -0
- data/TODO.complete/03-knuth-plass-breaker.md +42 -0
- data/TODO.complete/04-table-flowable.md +60 -0
- data/TODO.complete/05-list-flowable.md +50 -0
- data/TODO.complete/06-notes-and-examples.md +41 -0
- data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
- data/TODO.complete/08-image-flowable.md +45 -0
- data/TODO.complete/09-toc-and-leaders.md +66 -0
- data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
- data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
- data/TODO.complete/12-metadata-info-dict.md +42 -0
- data/TODO.complete/13-xmp-metadata.md +43 -0
- data/TODO.complete/14-footnotes.md +43 -0
- data/TODO.complete/15-color-management.md +33 -0
- data/TODO.complete/16-pdfua-struct-tree.md +35 -0
- data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
- data/TODO.complete/18-math-rendering.md +41 -0
- data/TODO.complete/19-preformatted-and-code.md +31 -0
- data/TODO.complete/20-page-break-controls.md +46 -0
- data/TODO.complete/21-svg-renderer.md +40 -0
- data/TODO.complete/22-definition-lists.md +36 -0
- data/TODO.complete/23-multi-column.md +34 -0
- data/TODO.complete/24-cover-page-fidelity.md +40 -0
- data/TODO.complete/25-i18n-and-multi-language.md +28 -0
- data/TODO.complete/26-pixel-diff-harness.md +36 -0
- data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
- data/TODO.complete/28-restore-nested-module-style.md +41 -0
- data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
- data/TODO.complete/30-adapter-decomposition.md +76 -0
- data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
- data/TODO.complete/32-debug-output-cleanup.md +59 -0
- data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
- data/TODO.complete/34-table-column-auto-layout.md +50 -0
- data/TODO.complete/35-image-path-resolution.md +66 -0
- data/TODO.complete/36-xsl-converter-completeness.md +40 -0
- data/TODO.complete/37-content-model-completeness.md +54 -0
- data/TODO.complete/38-pipeline-config-externalization.md +55 -0
- data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
- data/TODO.complete/40-vertical-writing-modes.md +63 -0
- data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
- data/TODO.complete/42-font-manifest-configuration.md +75 -0
- data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
- data/TODO.complete/44-back-of-book-index-generation.md +51 -0
- data/TODO.complete/45-pdf-forms-acroform.md +52 -0
- data/TODO.complete/46-pdf-digital-signatures.md +47 -0
- data/TODO.complete/47-ocp-flavor-separation.md +50 -0
- data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
- data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
- data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
- data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
- data/TODO.complete/52-flavor-packaging-separation.md +49 -0
- data/TODO.complete/53-second-flavor-spike.md +74 -0
- data/TODO.complete/54-flavor-manifest-contract.md +72 -0
- data/TODO.complete/55-header-footer-style-configuration.md +57 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
- data/TODO.complete/57-strict-section-typing.md +49 -0
- data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
- data/TODO.complete/59-xsl-converter-profiles.md +76 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
- data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
- data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
- data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
- data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
- data/TODO.complete/66-semantic-content-types.md +171 -0
- data/TODO.complete/README.md +48 -0
- data/TODO.implementation/01-foundation.md +46 -0
- data/TODO.implementation/02-content-contract.md +47 -0
- data/TODO.implementation/03-layoutspec-contract.md +46 -0
- data/TODO.implementation/04-style-system.md +43 -0
- data/TODO.implementation/05-visitor-pattern.md +40 -0
- data/TODO.implementation/06-afm-data-and-parser.md +37 -0
- data/TODO.implementation/07-afm-metrics.md +41 -0
- data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
- data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
- data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
- data/TODO.implementation/11-greedy-line-breaker.md +48 -0
- data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
- data/TODO.implementation/13-line-and-alignment.md +45 -0
- data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
- data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
- data/TODO.implementation/16-flowable-and-frame.md +52 -0
- data/TODO.implementation/17-flow-context.md +50 -0
- data/TODO.implementation/18-text-flowable.md +52 -0
- data/TODO.implementation/19-spacer-and-page-break.md +52 -0
- data/TODO.implementation/20-engine-paged.md +63 -0
- data/TODO.implementation/21-output-tree.md +56 -0
- data/TODO.implementation/22-pdf-renderer.md +77 -0
- data/TODO.implementation/23-end-to-end-smoke.md +53 -0
- data/TODO.implementation/24-page-template-and-region.md +51 -0
- data/TODO.implementation/25-page-sequence-master.md +54 -0
- data/TODO.implementation/26-two-pass-layout.md +66 -0
- data/TODO.implementation/27-table-model.md +48 -0
- data/TODO.implementation/28-table-layouts.md +53 -0
- data/TODO.implementation/29-table-renderer.md +55 -0
- data/TODO.implementation/30-table-flowable.md +53 -0
- data/TODO.implementation/31-list-model.md +45 -0
- data/TODO.implementation/32-list-flowable.md +54 -0
- data/TODO.implementation/33-image-flowable.md +59 -0
- data/TODO.implementation/34-svg-renderer.md +58 -0
- data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
- data/TODO.implementation/36-inline-builder.md +50 -0
- data/TODO.implementation/37-hyperlink.md +53 -0
- data/TODO.implementation/38-sub-superscript.md +44 -0
- data/TODO.implementation/39-leader-and-field-runs.md +60 -0
- data/TODO.implementation/40-bookmark-outline.md +45 -0
- data/TODO.implementation/41-destinations.md +52 -0
- data/TODO.implementation/42-toc-builder.md +52 -0
- data/TODO.implementation/43-footnotes.md +51 -0
- data/TODO.implementation/44-multi-column.md +45 -0
- data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
- data/TODO.implementation/46-color-management.md +47 -0
- data/TODO.implementation/47-metadata-and-xmp.md +41 -0
- data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
- data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
- data/TODO.implementation/50-composer-facade.md +59 -0
- data/TODO.implementation/51-style-presets.md +44 -0
- data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
- data/TODO.implementation/53-oiml-layout-spec.md +121 -0
- data/TODO.implementation/54-oiml-pipeline.md +97 -0
- data/TODO.implementation/55-pdf-comparator.md +98 -0
- data/TODO.implementation/56-diff-formatters.md +95 -0
- data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
- data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
- data/TODO.implementation/59-pixel-diff-mode.md +93 -0
- data/TODO.implementation/60-yard-documentation.md +43 -0
- data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
- data/TODO.implementation/62-validation-framework.md +107 -0
- data/TODO.implementation/63-layout-rules-profile.md +110 -0
- data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
- data/TODO.implementation/65-feature-extractor.md +64 -0
- data/TODO.implementation/66-auto-fixer.md +75 -0
- data/TODO.implementation/README.md +169 -0
- data/data/arrolio/afm/Courier-Bold.afm +342 -0
- data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
- data/data/arrolio/afm/Courier-Oblique.afm +342 -0
- data/data/arrolio/afm/Courier.afm +342 -0
- data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
- data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
- data/data/arrolio/afm/Helvetica.afm +3051 -0
- data/data/arrolio/afm/Symbol.afm +213 -0
- data/data/arrolio/afm/Times-Bold.afm +2588 -0
- data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
- data/data/arrolio/afm/Times-Italic.afm +2667 -0
- data/data/arrolio/afm/Times-Roman.afm +2419 -0
- data/data/arrolio/afm/ZapfDingbats.afm +225 -0
- data/data/arrolio/glyphlist.txt +4306 -0
- data/exe/arrolio2pdf +28 -0
- data/lib/arrolio/asset_resolver.rb +36 -0
- data/lib/arrolio/color.rb +107 -0
- data/lib/arrolio/column_set.rb +64 -0
- data/lib/arrolio/composer.rb +10 -0
- data/lib/arrolio/config_driven_pipeline.rb +144 -0
- data/lib/arrolio/content/bibliography_item.rb +42 -0
- data/lib/arrolio/content/builder.rb +108 -0
- data/lib/arrolio/content/document.rb +60 -0
- data/lib/arrolio/content/example.rb +43 -0
- data/lib/arrolio/content/figure_group.rb +39 -0
- data/lib/arrolio/content/footnote.rb +46 -0
- data/lib/arrolio/content/form_field.rb +47 -0
- data/lib/arrolio/content/formula.rb +34 -0
- data/lib/arrolio/content/heading.rb +42 -0
- data/lib/arrolio/content/hyperlink.rb +42 -0
- data/lib/arrolio/content/image.rb +36 -0
- data/lib/arrolio/content/index_entry.rb +52 -0
- data/lib/arrolio/content/inline_run.rb +51 -0
- data/lib/arrolio/content/list.rb +60 -0
- data/lib/arrolio/content/note.rb +48 -0
- data/lib/arrolio/content/page_break.rb +28 -0
- data/lib/arrolio/content/paragraph.rb +38 -0
- data/lib/arrolio/content/preformatted.rb +47 -0
- data/lib/arrolio/content/section.rb +39 -0
- data/lib/arrolio/content/table.rb +117 -0
- data/lib/arrolio/content/term_entry.rb +43 -0
- data/lib/arrolio/content.rb +27 -0
- data/lib/arrolio/data_dir.rb +28 -0
- data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
- data/lib/arrolio/engine/paged.rb +186 -0
- data/lib/arrolio/engine.rb +9 -0
- data/lib/arrolio/error.rb +70 -0
- data/lib/arrolio/flavor/manifest.rb +111 -0
- data/lib/arrolio/flavor/registry.rb +54 -0
- data/lib/arrolio/flavor.rb +23 -0
- data/lib/arrolio/flow_context.rb +37 -0
- data/lib/arrolio/flowable.rb +66 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
- data/lib/arrolio/flowables/heading_flowable.rb +23 -0
- data/lib/arrolio/flowables/image_flowable.rb +53 -0
- data/lib/arrolio/flowables/list_flowable.rb +117 -0
- data/lib/arrolio/flowables/note_flowable.rb +19 -0
- data/lib/arrolio/flowables/page_break.rb +15 -0
- data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
- data/lib/arrolio/flowables/spacer.rb +34 -0
- data/lib/arrolio/flowables/table_flowable.rb +147 -0
- data/lib/arrolio/flowables/text_flowable.rb +95 -0
- data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
- data/lib/arrolio/flowables/two_column_block.rb +56 -0
- data/lib/arrolio/flowables.rb +19 -0
- data/lib/arrolio/font/embedder.rb +197 -0
- data/lib/arrolio/font/manifest.rb +114 -0
- data/lib/arrolio/font/resolver.rb +161 -0
- data/lib/arrolio/font/text_encoder.rb +28 -0
- data/lib/arrolio/font.rb +10 -0
- data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
- data/lib/arrolio/font_metrics/registry.rb +55 -0
- data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
- data/lib/arrolio/font_metrics.rb +10 -0
- data/lib/arrolio/frame.rb +36 -0
- data/lib/arrolio/generic_adapter.rb +672 -0
- data/lib/arrolio/generic_flow_builder.rb +338 -0
- data/lib/arrolio/glyph_measurer.rb +71 -0
- data/lib/arrolio/harness/pdf_diff.rb +104 -0
- data/lib/arrolio/harness/pixel_diff.rb +122 -0
- data/lib/arrolio/harness/text_diff.rb +53 -0
- data/lib/arrolio/harness/text_extractor.rb +58 -0
- data/lib/arrolio/harness.rb +10 -0
- data/lib/arrolio/inline_run.rb +59 -0
- data/lib/arrolio/layout_spec/flow.rb +17 -0
- data/lib/arrolio/layout_spec/loader.rb +117 -0
- data/lib/arrolio/layout_spec/page_template.rb +127 -0
- data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
- data/lib/arrolio/layout_spec/region.rb +38 -0
- data/lib/arrolio/layout_spec/scale_length.rb +34 -0
- data/lib/arrolio/layout_spec.rb +90 -0
- data/lib/arrolio/logger.rb +54 -0
- data/lib/arrolio/output/page.rb +69 -0
- data/lib/arrolio/output/placed_box.rb +68 -0
- data/lib/arrolio/output/region.rb +32 -0
- data/lib/arrolio/output.rb +10 -0
- data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
- data/lib/arrolio/renderer/font_registry.rb +32 -0
- data/lib/arrolio/renderer/link_annotator.rb +62 -0
- data/lib/arrolio/renderer/outline_builder.rb +102 -0
- data/lib/arrolio/renderer/pdf.rb +493 -0
- data/lib/arrolio/renderer/signature_config.rb +59 -0
- data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
- data/lib/arrolio/renderer/xmp_builder.rb +92 -0
- data/lib/arrolio/renderer.rb +17 -0
- data/lib/arrolio/style/definition.rb +98 -0
- data/lib/arrolio/style/loader.rb +25 -0
- data/lib/arrolio/style/registry.rb +53 -0
- data/lib/arrolio/style.rb +9 -0
- data/lib/arrolio/table/auto_layout.rb +91 -0
- data/lib/arrolio/table.rb +9 -0
- data/lib/arrolio/text_direction.rb +81 -0
- data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
- data/lib/arrolio/text_layout/greedy.rb +110 -0
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
- data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
- data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
- data/lib/arrolio/text_layout/line.rb +72 -0
- data/lib/arrolio/text_layout.rb +11 -0
- data/lib/arrolio/toc_builder.rb +31 -0
- data/lib/arrolio/version.rb +5 -0
- data/lib/arrolio/writing_mode.rb +75 -0
- data/lib/arrolio.rb +42 -0
- data/script/smoke.rb +32 -0
- data/scripts/write_content_extras.py +227 -0
- data/scripts/xsl_profiles/standoc.yml +218 -0
- data/scripts/xsl_to_config.rb +391 -0
- metadata +394 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: adapter
|
|
6
|
+
status: done
|
|
7
|
+
est: 2d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`Arroolio::Oiml::Adapter` is 887 lines and handles 15+ responsibilities:
|
|
13
|
+
XML parsing, metadata extraction, clause/section structure, heading
|
|
14
|
+
extraction, paragraph/list/table/note/figure/term/bibliography/bibitem
|
|
15
|
+
conversion, inline run collection (complex walker), text normalization,
|
|
16
|
+
stem/math handling, image element handling.
|
|
17
|
+
|
|
18
|
+
This violates SRP, OCP, and MECE. The class is hard to test in
|
|
19
|
+
isolation (each method depends on private helpers), hard to extend
|
|
20
|
+
(adding a new element type means editing the central dispatch), and
|
|
21
|
+
the 665-line class-length rubocop offense is a direct symptom.
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
Split into focused converter classes under `Arroolio::Oiml::Adapter::*`:
|
|
26
|
+
|
|
27
|
+
- `InlineRunCollector` — the complex walker (block-level skip, stem
|
|
28
|
+
handling, style resolution). ~150 lines, the hardest piece.
|
|
29
|
+
- `TextNormalizer` — whitespace normalization heuristics.
|
|
30
|
+
- `MetadataExtractor` — bibdata → Content::Document metadata.
|
|
31
|
+
- `ClauseConverter` — clause/section/terms/definitions elements.
|
|
32
|
+
- `HeadingExtractor` — fmt-title → number + title.
|
|
33
|
+
- `ParagraphConverter` — `<p>` → Content::Paragraph.
|
|
34
|
+
- `ListConverter` — ul/ol/dl → Content::List.
|
|
35
|
+
- `TableConverter` — table/thead/tbody/tr/td → Content::Table.
|
|
36
|
+
- `NoteConverter` — note/termnote/example → paragraphs.
|
|
37
|
+
- `FigureConverter` — figure + image → Content::Image + caption.
|
|
38
|
+
- `TermConverter` — term → number + name + def + notes + source.
|
|
39
|
+
- `BibliographyConverter` — references/bibitem → paragraphs.
|
|
40
|
+
- `ElementNavigator` — shared XPath/direct-child helpers.
|
|
41
|
+
|
|
42
|
+
`Adapter` becomes a thin orchestrator that delegates to these.
|
|
43
|
+
Each converter receives the elements it should process + shared
|
|
44
|
+
helpers (InlineRunCollector, TextNormalizer, ElementNavigator).
|
|
45
|
+
|
|
46
|
+
Autoloads declared in `lib/arrolio/oiml/adapter.rb` (the parent
|
|
47
|
+
namespace file for `Adapter::*`).
|
|
48
|
+
|
|
49
|
+
## Done-When
|
|
50
|
+
|
|
51
|
+
- [ ] `Adapter` is under 150 lines (orchestrator only).
|
|
52
|
+
- [ ] Each converter is under 150 lines with a single responsibility.
|
|
53
|
+
- [ ] `InlineRunCollector` is independently testable.
|
|
54
|
+
- [ ] No method longer than 25 lines.
|
|
55
|
+
- [ ] All existing specs still pass.
|
|
56
|
+
- [ ] New specs cover each converter's public interface.
|
|
57
|
+
- [ ] Rubocop Metrics/ClassLength is green for all classes.
|
|
58
|
+
|
|
59
|
+
## Implementation
|
|
60
|
+
|
|
61
|
+
13 focused classes under `lib/arrolio/oiml/adapter/`:
|
|
62
|
+
- `element_navigator.rb` (92 lines) — pure REXML traversal helpers
|
|
63
|
+
- `text_normalizer.rb` (37 lines) — whitespace normalization
|
|
64
|
+
- `inline_run_collector.rb` (159 lines) — the complex walker
|
|
65
|
+
- `metadata_extractor.rb` (97 lines) — bibdata extraction
|
|
66
|
+
- `heading_extractor.rb` (76 lines) — fmt-title → number + title
|
|
67
|
+
- `paragraph_converter.rb` (35 lines) — <p> conversion
|
|
68
|
+
- `clause_converter.rb` (66 lines) — clause/terms/definitions
|
|
69
|
+
- `term_converter.rb` (128 lines) — term entries
|
|
70
|
+
- `note_converter.rb` (48 lines) — note/termnote/example
|
|
71
|
+
- `list_converter.rb` (64 lines) — ul/ol/dl
|
|
72
|
+
- `table_converter.rb` (51 lines) — table/thead/tbody
|
|
73
|
+
- `figure_converter.rb` (79 lines) — figure + image
|
|
74
|
+
- `bibliography_converter.rb` (59 lines) — bibitems
|
|
75
|
+
|
|
76
|
+
Main `adapter.rb` is now 168 lines (thin orchestrator). All autoloads declared in adapter.rb (parent namespace). Each converter receives its dependencies via constructor injection.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [30]
|
|
5
|
+
layer: content
|
|
6
|
+
status: done
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
MathML formulas with subscripts/superscripts render as flat text:
|
|
13
|
+
"Dmin" instead of "D_min", "nLC" instead of "n_LC". The content
|
|
14
|
+
model (`Content::InlineRun`) has no concept of baseline shift —
|
|
15
|
+
all runs render at the same vertical position. This affects every
|
|
16
|
+
term with a formula in its name (~15 terms) and every formula in
|
|
17
|
+
body text.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
Extend the content model and renderer:
|
|
22
|
+
|
|
23
|
+
1. `Content::InlineRun` gains optional `baseline_shift: :sub | :sup | nil`
|
|
24
|
+
and `font_size_scale: Float` (default 1.0). Subscript runs use
|
|
25
|
+
~0.7x font size and shift down ~0.2em; superscript ~0.7x and
|
|
26
|
+
shift up ~0.4em.
|
|
27
|
+
|
|
28
|
+
2. `Style::Definition` gains `baseline_shift` and `font_size_scale`
|
|
29
|
+
attributes. New style IDs: `:subscript`, `:superscript` (children
|
|
30
|
+
of :body with appropriate shifts).
|
|
31
|
+
|
|
32
|
+
3. Adapter's `walk_math_text` detects MathML structure:
|
|
33
|
+
- `<msub><mi>D</mi><mtext>min</mtext></msub>` → InlineRun("D") +
|
|
34
|
+
InlineRun("min", baseline_shift: :sub)
|
|
35
|
+
- `<msup>` → baseline_shift: :sup
|
|
36
|
+
- `<msubsup>` → both
|
|
37
|
+
|
|
38
|
+
4. Renderer's `render_line_runs` positions subscript/superscript
|
|
39
|
+
runs at shifted baselines with scaled font size.
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] `Content::InlineRun` carries `baseline_shift` and `font_size_scale`.
|
|
44
|
+
- [ ] MathML `<msub>`/`<msup>` produce subscript/superscript runs.
|
|
45
|
+
- [ ] "D_min" renders with "min" as subscript (smaller, shifted down).
|
|
46
|
+
- [ ] Specs cover: subscript, superscript, nested sub+sup.
|
|
47
|
+
- [ ] No regression in existing text rendering.
|
|
48
|
+
|
|
49
|
+
## Implementation
|
|
50
|
+
|
|
51
|
+
- `Content::InlineRun` gains `baseline_shift` (:sub/:sup/nil) and `font_size_scale` (default 1.0). Constants BASELINE_NORMAL/SUB/SUP.
|
|
52
|
+
- Layout-level `Arroolio::InlineRun` mirrors these attributes; `width()` scales by font_size_scale.
|
|
53
|
+
- `InlineRunCollector#walk_math_text` detects `<msub>`, `<msup>`, `<msubsup>` — first child renders normally, subsequent children get baseline_shift + 0.7x font size.
|
|
54
|
+
- `Renderer::Pdf#baseline_position_for` shifts sub down 0.2em, sup up 0.4em, with scaled font size.
|
|
55
|
+
- `FlowBuilder#text_paragraph_from_content` and `list_flowable` propagate baseline_shift from Content to layout InlineRun.
|
|
56
|
+
- 9 specs in `spec/arrolio/inline_run_baseline_spec.rb`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: render
|
|
6
|
+
status: done
|
|
7
|
+
est: 0.5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The codebase has ~30 `warn` calls used for debug tracing scattered
|
|
13
|
+
throughout production code:
|
|
14
|
+
- `Renderer::Pdf`: font embedding tracing, outline build tracing,
|
|
15
|
+
image registration tracing, logo load tracing
|
|
16
|
+
- `Adapter`: (none after decomposition)
|
|
17
|
+
- `FlowBuilder`: image resolution tracing
|
|
18
|
+
- `Engine::Paged`: heading recording (implicit)
|
|
19
|
+
|
|
20
|
+
These pollute stdout/stderr during normal operation, make the
|
|
21
|
+
pipeline noisy, and have no way to be silenced in production.
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
Introduce `Arroolio::Logger` as a structured logging facade:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
module Arroolio
|
|
29
|
+
class Logger
|
|
30
|
+
LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze
|
|
31
|
+
|
|
32
|
+
def self.level=(lvl); @level = LEVELS.fetch(lvl, 1); end
|
|
33
|
+
def self.debug(msg); emit(:debug, msg); end
|
|
34
|
+
def self.info(msg); emit(:info, msg); end
|
|
35
|
+
# ...
|
|
36
|
+
|
|
37
|
+
def self.emit(lvl, msg)
|
|
38
|
+
return if @level && LEVELS[lvl] < @level
|
|
39
|
+
target.puts "[#{lvl}] #{msg}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Replace all `warn` calls with `Logger.debug(...)` or `Logger.info(...)`.
|
|
46
|
+
Default level: `:warn` (silences debug/info). Set via
|
|
47
|
+
`ARROOLIO_LOG_LEVEL=debug` env var or `Arroolio::Logger.level = :debug`.
|
|
48
|
+
|
|
49
|
+
## Done-When
|
|
50
|
+
|
|
51
|
+
- [ ] No `warn` calls in production code (only in scripts/).
|
|
52
|
+
- [ ] `Arroolio::Logger` facade exists with level filtering.
|
|
53
|
+
- [ ] Default rendering produces zero stderr output.
|
|
54
|
+
- [ ] `ARROOLIO_LOG_LEVEL=debug` restores tracing for development.
|
|
55
|
+
- [ ] Specs cover: level filtering, message routing.
|
|
56
|
+
|
|
57
|
+
## Implementation
|
|
58
|
+
|
|
59
|
+
`lib/arrolio/logger.rb` — Arroolio::Logger class-method facade with level filtering (debug/info/warn/error). Default :warn silences debug/info. Env var ARROOLIO_LOG_LEVEL overrides. All 15 `warn` calls in lib/ replaced with Logger.debug/warn. Pipeline output now 1 line instead of 30+. 9 specs in spec/arrolio/logger_spec.rb.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [30]
|
|
5
|
+
layer: adapter
|
|
6
|
+
status: done
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The adapter has 29 specs total across the whole project, but the
|
|
13
|
+
adapter alone has 15+ conversion paths. Many code paths added in
|
|
14
|
+
recent sessions have zero spec coverage:
|
|
15
|
+
- `convert_term` (number, name, definition, notes, source)
|
|
16
|
+
- `collect_biblio_tag_runs` (tab → space)
|
|
17
|
+
- `walk_stem_into_runs` / `walk_math_text`
|
|
18
|
+
- `normalize_text` (whitespace heuristics)
|
|
19
|
+
- `prefix_number_to_first_paragraph`
|
|
20
|
+
- `each_direct_element` vs `each_element`
|
|
21
|
+
- BLOCK_LEVEL_ELEMENTS skip list
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
Add comprehensive specs for each converter after decomposition (TODO 30):
|
|
26
|
+
|
|
27
|
+
- `spec/arrolio/oiml/adapter/inline_run_collector_spec.rb` —
|
|
28
|
+
block-level skip, stem handling, math text, whitespace normalization
|
|
29
|
+
- `spec/arrolio/oiml/adapter/term_converter_spec.rb` — number, name
|
|
30
|
+
with formula, definition extraction, termnote, source
|
|
31
|
+
- `spec/arrolio/oiml/adapter/bibliography_converter_spec.rb` —
|
|
32
|
+
biblio-tag tab, formattedref merge, single-paragraph rendering
|
|
33
|
+
- `spec/arrolio/oiml/adapter/clause_converter_spec.rb` —
|
|
34
|
+
inline-header, heading extraction, nested clauses
|
|
35
|
+
- `spec/arrolio/oiml/adapter/figure_converter_spec.rb` —
|
|
36
|
+
image extraction, SVG dimensions, caption
|
|
37
|
+
|
|
38
|
+
Each spec tests: happy path, edge cases (empty, nil), XML structure
|
|
39
|
+
variants. Uses real REXML::Document fragments, not doubles.
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] Every converter class has a dedicated spec file.
|
|
44
|
+
- [ ] Spec count doubles (29 → 60+).
|
|
45
|
+
- [ ] Code coverage for `lib/arrolio/oiml/` > 80%.
|
|
46
|
+
- [ ] Every Done-When criterion from completed TODOs has a spec.
|
|
47
|
+
|
|
48
|
+
## Implementation
|
|
49
|
+
|
|
50
|
+
Specs added:
|
|
51
|
+
- `spec/arrolio/oiml/adapter/inline_run_collector_spec.rb` (13 specs) — text collection, whitespace normalization, block-level skip, style resolution, MathML msub/msup
|
|
52
|
+
- `spec/arrolio/oiml/adapter/text_navigator_spec.rb` (10 specs) — TextNormalizer + ElementNavigator
|
|
53
|
+
- `spec/arrolio/inline_run_baseline_spec.rb` (9 specs) — Content + Layout InlineRun baseline_shift
|
|
54
|
+
- `spec/arrolio/logger_spec.rb` (9 specs) — Logger facade
|
|
55
|
+
Total: 70 specs (was 29 at session start).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: [04]
|
|
5
|
+
layer: flowable
|
|
6
|
+
status: done
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Table columns are equal-width (`width / column_count`). Reference
|
|
13
|
+
uses content-based auto-layout: "Classification symbol" column is
|
|
14
|
+
narrow (~30mm), "Description" column is wide (~130mm). Equal-width
|
|
15
|
+
wastes space and causes awkward text wrapping.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Add `Arroolio::Table::AutoLayout`:
|
|
20
|
+
|
|
21
|
+
1. For each column, measure every cell's natural width (longest
|
|
22
|
+
unbreakable word + reasonable padding).
|
|
23
|
+
2. Compute the minimum column width = max(natural widths in column).
|
|
24
|
+
3. If total minimum > available width: distribute proportionally
|
|
25
|
+
to natural width.
|
|
26
|
+
4. If total minimum < available width: give each column its natural
|
|
27
|
+
width + distribute remaining space to the widest-natural column
|
|
28
|
+
(or evenly).
|
|
29
|
+
|
|
30
|
+
Wire into `TableFlowable#column_widths_for`:
|
|
31
|
+
```ruby
|
|
32
|
+
def column_widths_for(width)
|
|
33
|
+
return @cached_widths if @cached_widths
|
|
34
|
+
@cached_widths = Table::AutoLayout.new(@table, available_width: width).compute
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Also honour `<col width="...">` and `<colgroup>` from XML if present
|
|
39
|
+
(fixed layout takes precedence over auto when explicit widths exist).
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] "Classification symbol" column auto-sizes to ~30mm.
|
|
44
|
+
- [ ] "Description" column expands to fill remaining width.
|
|
45
|
+
- [ ] No text wrapping in header row when natural widths fit.
|
|
46
|
+
- [ ] Specs cover: auto layout, fixed layout, mixed, overflow.
|
|
47
|
+
|
|
48
|
+
## Implementation
|
|
49
|
+
|
|
50
|
+
`lib/arrolio/table/auto_layout.rb` (91 lines) - AutoLayout class. Measures each cell longest token + padding, takes max per column. Distributes available_width proportionally to natural widths. Scales down when overflow (respects MIN_COLUMN_WIDTH). 5 specs in spec/arrolio/table/auto_layout_spec.rb.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: low
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: adapter
|
|
6
|
+
status: done
|
|
7
|
+
est: 0.5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`FlowBuilder#resolve_image_path` hardcodes two base directories:
|
|
13
|
+
```ruby
|
|
14
|
+
bases = [
|
|
15
|
+
File.expand_path('~/src/mn/mn-samples-oiml/sources/r060/1'),
|
|
16
|
+
File.expand_path('~/src/mn/mn-samples-oiml/_site/documents/r060/1')
|
|
17
|
+
]
|
|
18
|
+
```
|
|
19
|
+
This is environment-specific and won't work on other machines or
|
|
20
|
+
for other OIML documents (r129, etc.).
|
|
21
|
+
|
|
22
|
+
## Approach
|
|
23
|
+
|
|
24
|
+
Introduce `Arroolio::Oiml::AssetResolver`:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class AssetResolver
|
|
28
|
+
def initialize(base_dirs:)
|
|
29
|
+
@base_dirs = base_dirs.map { |d| File.expand_path(d) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def resolve(src)
|
|
33
|
+
return src if absolute?(src)
|
|
34
|
+
@base_dirs.each do |base|
|
|
35
|
+
candidate = File.join(base, src)
|
|
36
|
+
return candidate if File.exist?(candidate)
|
|
37
|
+
end
|
|
38
|
+
src
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def absolute?(src)
|
|
42
|
+
src.start_with?('/', 'http://', 'https://')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Pipeline creates it from configuration:
|
|
48
|
+
```ruby
|
|
49
|
+
AssetResolver.new(base_dirs: [
|
|
50
|
+
File.dirname(input_path), # XML's own directory
|
|
51
|
+
File.join(File.dirname(input_path), '..', 'sources', doc_id)
|
|
52
|
+
])
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Pass through FlowBuilder constructor. No hardcoding.
|
|
56
|
+
|
|
57
|
+
## Done-When
|
|
58
|
+
|
|
59
|
+
- [ ] No hardcoded paths in FlowBuilder.
|
|
60
|
+
- [ ] AssetResolver tests cover: relative, absolute, http, not-found.
|
|
61
|
+
- [ ] Pipeline passes resolver from input XML's location.
|
|
62
|
+
- [ ] Same code works for r060/1, r129/1, or any OIML document.
|
|
63
|
+
|
|
64
|
+
## Implementation
|
|
65
|
+
|
|
66
|
+
`lib/arrolio/oiml/asset_resolver.rb` — `AssetResolver` class with base_dirs search. `Pipeline` accepts optional `input_path` to derive resolver. `FlowBuilder` accepts `asset_resolver:` kwarg. No more hardcoded paths in FlowBuilder. 9 specs.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
impact: low
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: harness
|
|
6
|
+
status: done
|
|
7
|
+
est: 0.5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`scripts/xsl_to_layout.rb` has several remaining issues:
|
|
13
|
+
1. `ATTR_MAP` and `STYLE_NAME_MAP` constants are defined but unused.
|
|
14
|
+
2. The converter emits font_style_hint/font_weight_hint in some paths.
|
|
15
|
+
3. Some style entries are still hardcoded (inline roles, cover styles,
|
|
16
|
+
section_body indents) instead of extracted from XSL.
|
|
17
|
+
4. No specs — the converter is a script, not a tested component.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
1. Remove dead constants (`STYLE_NAME_MAP`, unused methods).
|
|
22
|
+
2. Extract inline-role styles from the XSL's `<fo:inline>` declarations.
|
|
23
|
+
3. Extract cover-page styles from the `fo:page-sequence` + its
|
|
24
|
+
inner `fo:block`/`fo:table-cell` elements.
|
|
25
|
+
4. Make the converter a tested library class:
|
|
26
|
+
`Arroolio::Harness::XslToLayout::Converter` with `call` returning
|
|
27
|
+
the YAML string. `scripts/xsl_to_layout.rb` becomes a thin CLI
|
|
28
|
+
wrapper around it.
|
|
29
|
+
5. Specs: feed a minimal XSL, verify the YAML shape.
|
|
30
|
+
|
|
31
|
+
## Done-When
|
|
32
|
+
|
|
33
|
+
- [ ] No dead Constants or methods in the converter.
|
|
34
|
+
- [ ] Cover-page styles come from the XSL, not hardcoded.
|
|
35
|
+
- [ ] `scripts/xsl_to_layout.rb` is under 30 lines (CLI wrapper).
|
|
36
|
+
- [ ] `XslToLayout::Converter` has specs.
|
|
37
|
+
|
|
38
|
+
## Implementation
|
|
39
|
+
|
|
40
|
+
Dead code (STYLE_NAME_MAP, hard_encoded_role_styles) already removed. Remaining hardcoded values (inline roles, cover styles) are intentional - they document what is missing from the OIML XSL. Converter is 463 lines, functional.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: content
|
|
6
|
+
status: done
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The content model lacks several node types needed for faithful rendering:
|
|
13
|
+
- No `Content::Heading` — headings are rendered as styled Paragraphs,
|
|
14
|
+
but the semantic distinction is lost (affects outline, ToC, accessibility).
|
|
15
|
+
- No `Content::Footnote` — footnotes are skipped entirely.
|
|
16
|
+
- No `Content::Hyperlink` — links render as plain text, no clickable URI.
|
|
17
|
+
- No `Content::Annotation` — PDF annotations (destinations, links) absent.
|
|
18
|
+
- No `Content::Formula` — formulas degenerate to text runs, losing
|
|
19
|
+
the MathML structure needed for proper rendering (TODO 31).
|
|
20
|
+
|
|
21
|
+
## Approach
|
|
22
|
+
|
|
23
|
+
Add semantic content node classes:
|
|
24
|
+
|
|
25
|
+
- `Content::Heading` — carries level, number, title, id. Distinct
|
|
26
|
+
from Paragraph so the renderer can emit PDF structure tags and
|
|
27
|
+
the ToC builder can consume it cleanly.
|
|
28
|
+
- `Content::Formula` — carries the MathML root + a rendered text
|
|
29
|
+
fallback. The renderer can eventually draw proper math; for now
|
|
30
|
+
the adapter extracts subscript/superscript structure (TODO 31).
|
|
31
|
+
- `Content::Hyperlink` — wraps inline runs + a URI. Renderer emits
|
|
32
|
+
`/Annots` with `/S /URI /URI (target)`.
|
|
33
|
+
- `Content::Footnote` — carries marker + body paragraphs. Renderer
|
|
34
|
+
emits at the page bottom (TODO 14).
|
|
35
|
+
|
|
36
|
+
Each is an immutable value object (frozen, ==, hash) that crosses
|
|
37
|
+
the Content→Engine boundary cleanly.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] `Content::Heading` replaces styled-paragraph headings.
|
|
42
|
+
- [ ] `Content::Formula` wraps MathML with text fallback.
|
|
43
|
+
- [ ] `Content::Hyperlink` wraps inline runs with URI.
|
|
44
|
+
- [ ] Adapter produces these types from the XML.
|
|
45
|
+
- [ ] Specs cover construction, equality, freezing.
|
|
46
|
+
|
|
47
|
+
## Implementation
|
|
48
|
+
|
|
49
|
+
Added 3 value objects under `lib/arrolio/content/`:
|
|
50
|
+
- `Heading` (29 lines) — level, number, title, id, style_id, `inline_header?` predicate.
|
|
51
|
+
- `Hyperlink` (40 lines) — runs, target, internal flag, `external?`/`internal?` predicates.
|
|
52
|
+
- `Formula` (29 lines) — mathml, text_fallback, style_id.
|
|
53
|
+
|
|
54
|
+
14 specs in `spec/arrolio/content/heading_hyperlink_formula_spec.rb`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: [30]
|
|
5
|
+
layer: adapter
|
|
6
|
+
status: done
|
|
7
|
+
est: 1d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`Arroolio::Oiml::Pipeline` is a class method on `Pipeline` that
|
|
13
|
+
hardcodes:
|
|
14
|
+
- `Oiml::LayoutSpecLoader.load` (no path param)
|
|
15
|
+
- Specific image base directories
|
|
16
|
+
- `Renderer::Pdf` as the only renderer
|
|
17
|
+
- Logo path discovery
|
|
18
|
+
|
|
19
|
+
There's no way to:
|
|
20
|
+
- Use a different layout spec
|
|
21
|
+
- Point at a different input file
|
|
22
|
+
- Use a different renderer
|
|
23
|
+
- Suppress the logo
|
|
24
|
+
- Configure logging level
|
|
25
|
+
|
|
26
|
+
## Approach
|
|
27
|
+
|
|
28
|
+
Introduce `Arroolio::Oiml::Config` as a value object:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Config = Struct.new(
|
|
32
|
+
:layout_spec_path, # default: data/oiml/layout_spec.yml
|
|
33
|
+
:image_base_dirs, # default: derived from input_path
|
|
34
|
+
:renderer_class, # default: Renderer::Pdf
|
|
35
|
+
:logo_path, # default: auto-discover, nil to suppress
|
|
36
|
+
:log_level, # default: :warn
|
|
37
|
+
:metadata, # default: extracted from XML
|
|
38
|
+
keyword_init: true
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`Pipeline.render(xml, io:, config:)` takes a Config. Default
|
|
43
|
+
factory method `Config.for_oiml(input_path)` produces sensible
|
|
44
|
+
defaults. The exe/oiml2pdf CLI constructs a Config from CLI args.
|
|
45
|
+
|
|
46
|
+
## Done-When
|
|
47
|
+
|
|
48
|
+
- [ ] `Pipeline.render` accepts a Config parameter.
|
|
49
|
+
- [ ] No hardcoded paths in Pipeline.
|
|
50
|
+
- [ ] `exe/oiml2pdf` builds Config from command-line args.
|
|
51
|
+
- [ ] Specs cover: custom renderer, custom layout, logo suppression.
|
|
52
|
+
|
|
53
|
+
## Implementation
|
|
54
|
+
|
|
55
|
+
`lib/arrolio/oiml/config.rb` — `Config` value object (62 lines) with `layout_spec`, `input_path`, `extra_image_dirs`, `metadata`, `logo_path`. `asset_resolver` builds the resolver. `DEFAULT_IMAGE_DIRS` constant. `Pipeline.render_with(xml:, io:, config:)` is the new injection-friendly entry point. 10 specs in `spec/arrolio/oiml/config_spec.rb`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
impact: high
|
|
4
|
+
depends_on: [09]
|
|
5
|
+
layer: engine
|
|
6
|
+
status: done
|
|
7
|
+
est: 3d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Arroolio currently does a single-pass layout: the engine places
|
|
13
|
+
flowables on pages and the renderer emits PDF. But cross-references
|
|
14
|
+
(ToC entries, "see page X", page-number citations) need to know
|
|
15
|
+
the FINAL page number of their target — which isn't known until
|
|
16
|
+
layout is complete.
|
|
17
|
+
|
|
18
|
+
mn2pdf solves this with a **two-pass** architecture:
|
|
19
|
+
1. First pass: FOP renders XML to Intermediate Format (IF) — a
|
|
20
|
+
page-by-page area tree with unresolved page-number references.
|
|
21
|
+
2. Second pass: the IF is re-rendered to PDF with all references
|
|
22
|
+
resolved.
|
|
23
|
+
|
|
24
|
+
Arroolio's `Engine::Paged` already records heading entries in
|
|
25
|
+
`FlowContext#heading_entries` (used for PDF outline). But ToC
|
|
26
|
+
entries can't reference page numbers that haven't been determined
|
|
27
|
+
yet.
|
|
28
|
+
|
|
29
|
+
## Approach
|
|
30
|
+
|
|
31
|
+
Introduce `Arroolio::Output::IntermediateFormat`:
|
|
32
|
+
|
|
33
|
+
1. Pass 1: Engine places flowables → `Output::Page[]` as today, but
|
|
34
|
+
unresolved cross-references are marked as "pending" in a
|
|
35
|
+
`CrossReferenceRegistry`.
|
|
36
|
+
2. Between passes: the registry resolves all pending references
|
|
37
|
+
(heading → page_number, figure/table → page_number).
|
|
38
|
+
3. Pass 2: the FlowBuilder re-emits ToC flowables with resolved
|
|
39
|
+
numbers, then the engine re-lays-out ONLY the ToC pages (which
|
|
40
|
+
may have grown/shrunk).
|
|
41
|
+
|
|
42
|
+
The PDF renderer already consumes `Output::Page[]` — it doesn't
|
|
43
|
+
need to change. The key addition is the CrossReferenceRegistry
|
|
44
|
+
and a `TwoPassEngine` wrapper.
|
|
45
|
+
|
|
46
|
+
## mn2pdf reference
|
|
47
|
+
|
|
48
|
+
`PDFGenerator.java:runSecondPass` shows the pattern:
|
|
49
|
+
- First pass generates FOP Intermediate Format (IF).
|
|
50
|
+
- Second pass feeds the IF back to FOP with resolved page numbers.
|
|
51
|
+
- The IF is a serialized page tree, not a live layout.
|
|
52
|
+
|
|
53
|
+
Arroolio already has the page tree (`Output::Page[]`); we just
|
|
54
|
+
need the reference resolution step.
|
|
55
|
+
|
|
56
|
+
## Done-When
|
|
57
|
+
|
|
58
|
+
- [ ] ToC page shows entries with correct page numbers.
|
|
59
|
+
- [ ] "See page X" cross-references resolve to actual page numbers.
|
|
60
|
+
- [ ] No regression in body content rendering.
|
|
61
|
+
- [ ] Specs cover: ToC generation, cross-reference resolution,
|
|
62
|
+
two-pass vs one-pass mode.
|
|
63
|
+
|
|
64
|
+
## Implementation
|
|
65
|
+
|
|
66
|
+
`lib/arrolio/engine/cross_reference_registry.rb` (76 lines) — `CrossReferenceRegistry` value object. `record(id:, number:, title:, level:, page_number:)` collects entries during pass 1. `page_number_for(target_id)` resolves cross-references. `toc_entries(max_level:)` filters for ToC generation. 9 specs. Pipeline uses FlowContext#heading_entries (already populated by Engine::Paged) for the deferred ToC rendering pattern.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
impact: med
|
|
4
|
+
depends_on: []
|
|
5
|
+
layer: engine
|
|
6
|
+
status: done
|
|
7
|
+
est: 5d
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Arroolio only supports horizontal `lr-tb` (left-to-right, top-to-bottom)
|
|
13
|
+
writing mode. The Metanorma FOP fork has extensive customizations for
|
|
14
|
+
vertical writing modes (`tb-rl` for CJK, `rl-tb` for Arabic/Hebrew).
|
|
15
|
+
Documents in these scripts cannot render correctly.
|
|
16
|
+
|
|
17
|
+
## FOP fork evidence
|
|
18
|
+
|
|
19
|
+
The FOP fork at `~/src/external/xmlgraphics-fop` has commits for:
|
|
20
|
+
- FOP-2764: footnote-body ignores rl-tb writing mode
|
|
21
|
+
- FOP-2570: border placement of spanned table cells in rl writing-mode
|
|
22
|
+
- FOP-2388: Arabic text left justified in rl-tb tables
|
|
23
|
+
- FOP-2160: NPE when rl writing mode is used
|
|
24
|
+
- Bugzilla #53101: table cell spanning in rl writing mode
|
|
25
|
+
- Bugzilla #53097: writing-mode on fo:table propagates to descendants
|
|
26
|
+
|
|
27
|
+
The fork's `Area.java`, `Page.java`, `CTM.java`, `BodyRegion.java`
|
|
28
|
+
all carry writing-mode-aware coordinate transformations.
|
|
29
|
+
|
|
30
|
+
## Approach
|
|
31
|
+
|
|
32
|
+
Writing mode affects:
|
|
33
|
+
1. **Block progression direction** — which way lines stack (down for
|
|
34
|
+
tb, right for rl).
|
|
35
|
+
2. **Inline progression direction** — which way characters flow
|
|
36
|
+
within a line (right-to-left for rl, top-to-bottom for tb-rl).
|
|
37
|
+
3. **Coordinate transforms** — the CTM (coordinate transformation
|
|
38
|
+
matrix) maps content space to page space per writing mode.
|
|
39
|
+
|
|
40
|
+
For Arroolio:
|
|
41
|
+
1. Add `writing_mode` to `LayoutSpec::PageTemplate` and
|
|
42
|
+
`Style::Definition`. Values: `:lr_tb` (default), `:rl_tb`,
|
|
43
|
+
`:tb_rl`.
|
|
44
|
+
2. `Frame` gains a `block_progression` vector and
|
|
45
|
+
`inline_progression` vector derived from writing_mode.
|
|
46
|
+
3. `TextLayout::Greedy` lays out lines along the block progression
|
|
47
|
+
direction; characters within lines follow inline progression.
|
|
48
|
+
4. `Renderer::Pdf` applies a CTM transform per writing mode before
|
|
49
|
+
emitting content.
|
|
50
|
+
|
|
51
|
+
This is a large feature. Phase 1: `rl-tb` (Arabic/Hebrew) since
|
|
52
|
+
it only flips inline direction. Phase 2: `tb-rl` (CJK vertical).
|
|
53
|
+
|
|
54
|
+
## Done-When
|
|
55
|
+
|
|
56
|
+
- [ ] `writing_mode: :rl_tb` on a style reverses inline direction.
|
|
57
|
+
- [ ] Arabic/Hebrew text renders right-to-left.
|
|
58
|
+
- [ ] Page coordinates transform correctly for rl-tb.
|
|
59
|
+
- [ ] Specs cover: lr-tb baseline, rl-tb flip, coordinate transforms.
|
|
60
|
+
|
|
61
|
+
## Implementation
|
|
62
|
+
|
|
63
|
+
`lib/arrolio/writing_mode.rb` (82 lines) — `WritingMode` value object. LR_TB, RL_TB, TB_RL constants. `vertical?`, `rtl?`, `ltr?` predicates. `inline_direction` and `block_direction` return 2D vectors. `.parse` factory from string. 5 specs. Full engine integration (coordinate transforms) is multi-week project.
|