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.
Files changed (272) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +2 -0
  3. data/.rubocop.yml +343 -0
  4. data/CHANGELOG.md +18 -0
  5. data/CLAUDE.md +154 -0
  6. data/Gemfile +15 -0
  7. data/LICENSE +24 -0
  8. data/README.adoc +95 -0
  9. data/Rakefile +21 -0
  10. data/TODO.complete/01-true-type-font-metrics.md +56 -0
  11. data/TODO.complete/02-font-embedding.md +51 -0
  12. data/TODO.complete/03-knuth-plass-breaker.md +42 -0
  13. data/TODO.complete/04-table-flowable.md +60 -0
  14. data/TODO.complete/05-list-flowable.md +50 -0
  15. data/TODO.complete/06-notes-and-examples.md +41 -0
  16. data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
  17. data/TODO.complete/08-image-flowable.md +45 -0
  18. data/TODO.complete/09-toc-and-leaders.md +66 -0
  19. data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
  20. data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
  21. data/TODO.complete/12-metadata-info-dict.md +42 -0
  22. data/TODO.complete/13-xmp-metadata.md +43 -0
  23. data/TODO.complete/14-footnotes.md +43 -0
  24. data/TODO.complete/15-color-management.md +33 -0
  25. data/TODO.complete/16-pdfua-struct-tree.md +35 -0
  26. data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
  27. data/TODO.complete/18-math-rendering.md +41 -0
  28. data/TODO.complete/19-preformatted-and-code.md +31 -0
  29. data/TODO.complete/20-page-break-controls.md +46 -0
  30. data/TODO.complete/21-svg-renderer.md +40 -0
  31. data/TODO.complete/22-definition-lists.md +36 -0
  32. data/TODO.complete/23-multi-column.md +34 -0
  33. data/TODO.complete/24-cover-page-fidelity.md +40 -0
  34. data/TODO.complete/25-i18n-and-multi-language.md +28 -0
  35. data/TODO.complete/26-pixel-diff-harness.md +36 -0
  36. data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
  37. data/TODO.complete/28-restore-nested-module-style.md +41 -0
  38. data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
  39. data/TODO.complete/30-adapter-decomposition.md +76 -0
  40. data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
  41. data/TODO.complete/32-debug-output-cleanup.md +59 -0
  42. data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
  43. data/TODO.complete/34-table-column-auto-layout.md +50 -0
  44. data/TODO.complete/35-image-path-resolution.md +66 -0
  45. data/TODO.complete/36-xsl-converter-completeness.md +40 -0
  46. data/TODO.complete/37-content-model-completeness.md +54 -0
  47. data/TODO.complete/38-pipeline-config-externalization.md +55 -0
  48. data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
  49. data/TODO.complete/40-vertical-writing-modes.md +63 -0
  50. data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
  51. data/TODO.complete/42-font-manifest-configuration.md +75 -0
  52. data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
  53. data/TODO.complete/44-back-of-book-index-generation.md +51 -0
  54. data/TODO.complete/45-pdf-forms-acroform.md +52 -0
  55. data/TODO.complete/46-pdf-digital-signatures.md +47 -0
  56. data/TODO.complete/47-ocp-flavor-separation.md +50 -0
  57. data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
  58. data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
  59. data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
  60. data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
  61. data/TODO.complete/52-flavor-packaging-separation.md +49 -0
  62. data/TODO.complete/53-second-flavor-spike.md +74 -0
  63. data/TODO.complete/54-flavor-manifest-contract.md +72 -0
  64. data/TODO.complete/55-header-footer-style-configuration.md +57 -0
  65. data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
  66. data/TODO.complete/57-strict-section-typing.md +49 -0
  67. data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
  68. data/TODO.complete/59-xsl-converter-profiles.md +76 -0
  69. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
  70. data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
  71. data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
  72. data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
  73. data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
  74. data/TODO.complete/66-semantic-content-types.md +171 -0
  75. data/TODO.complete/README.md +48 -0
  76. data/TODO.implementation/01-foundation.md +46 -0
  77. data/TODO.implementation/02-content-contract.md +47 -0
  78. data/TODO.implementation/03-layoutspec-contract.md +46 -0
  79. data/TODO.implementation/04-style-system.md +43 -0
  80. data/TODO.implementation/05-visitor-pattern.md +40 -0
  81. data/TODO.implementation/06-afm-data-and-parser.md +37 -0
  82. data/TODO.implementation/07-afm-metrics.md +41 -0
  83. data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
  84. data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
  85. data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
  86. data/TODO.implementation/11-greedy-line-breaker.md +48 -0
  87. data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
  88. data/TODO.implementation/13-line-and-alignment.md +45 -0
  89. data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
  90. data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
  91. data/TODO.implementation/16-flowable-and-frame.md +52 -0
  92. data/TODO.implementation/17-flow-context.md +50 -0
  93. data/TODO.implementation/18-text-flowable.md +52 -0
  94. data/TODO.implementation/19-spacer-and-page-break.md +52 -0
  95. data/TODO.implementation/20-engine-paged.md +63 -0
  96. data/TODO.implementation/21-output-tree.md +56 -0
  97. data/TODO.implementation/22-pdf-renderer.md +77 -0
  98. data/TODO.implementation/23-end-to-end-smoke.md +53 -0
  99. data/TODO.implementation/24-page-template-and-region.md +51 -0
  100. data/TODO.implementation/25-page-sequence-master.md +54 -0
  101. data/TODO.implementation/26-two-pass-layout.md +66 -0
  102. data/TODO.implementation/27-table-model.md +48 -0
  103. data/TODO.implementation/28-table-layouts.md +53 -0
  104. data/TODO.implementation/29-table-renderer.md +55 -0
  105. data/TODO.implementation/30-table-flowable.md +53 -0
  106. data/TODO.implementation/31-list-model.md +45 -0
  107. data/TODO.implementation/32-list-flowable.md +54 -0
  108. data/TODO.implementation/33-image-flowable.md +59 -0
  109. data/TODO.implementation/34-svg-renderer.md +58 -0
  110. data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
  111. data/TODO.implementation/36-inline-builder.md +50 -0
  112. data/TODO.implementation/37-hyperlink.md +53 -0
  113. data/TODO.implementation/38-sub-superscript.md +44 -0
  114. data/TODO.implementation/39-leader-and-field-runs.md +60 -0
  115. data/TODO.implementation/40-bookmark-outline.md +45 -0
  116. data/TODO.implementation/41-destinations.md +52 -0
  117. data/TODO.implementation/42-toc-builder.md +52 -0
  118. data/TODO.implementation/43-footnotes.md +51 -0
  119. data/TODO.implementation/44-multi-column.md +45 -0
  120. data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
  121. data/TODO.implementation/46-color-management.md +47 -0
  122. data/TODO.implementation/47-metadata-and-xmp.md +41 -0
  123. data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
  124. data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
  125. data/TODO.implementation/50-composer-facade.md +59 -0
  126. data/TODO.implementation/51-style-presets.md +44 -0
  127. data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
  128. data/TODO.implementation/53-oiml-layout-spec.md +121 -0
  129. data/TODO.implementation/54-oiml-pipeline.md +97 -0
  130. data/TODO.implementation/55-pdf-comparator.md +98 -0
  131. data/TODO.implementation/56-diff-formatters.md +95 -0
  132. data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
  133. data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
  134. data/TODO.implementation/59-pixel-diff-mode.md +93 -0
  135. data/TODO.implementation/60-yard-documentation.md +43 -0
  136. data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
  137. data/TODO.implementation/62-validation-framework.md +107 -0
  138. data/TODO.implementation/63-layout-rules-profile.md +110 -0
  139. data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
  140. data/TODO.implementation/65-feature-extractor.md +64 -0
  141. data/TODO.implementation/66-auto-fixer.md +75 -0
  142. data/TODO.implementation/README.md +169 -0
  143. data/data/arrolio/afm/Courier-Bold.afm +342 -0
  144. data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
  145. data/data/arrolio/afm/Courier-Oblique.afm +342 -0
  146. data/data/arrolio/afm/Courier.afm +342 -0
  147. data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
  148. data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
  149. data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
  150. data/data/arrolio/afm/Helvetica.afm +3051 -0
  151. data/data/arrolio/afm/Symbol.afm +213 -0
  152. data/data/arrolio/afm/Times-Bold.afm +2588 -0
  153. data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
  154. data/data/arrolio/afm/Times-Italic.afm +2667 -0
  155. data/data/arrolio/afm/Times-Roman.afm +2419 -0
  156. data/data/arrolio/afm/ZapfDingbats.afm +225 -0
  157. data/data/arrolio/glyphlist.txt +4306 -0
  158. data/exe/arrolio2pdf +28 -0
  159. data/lib/arrolio/asset_resolver.rb +36 -0
  160. data/lib/arrolio/color.rb +107 -0
  161. data/lib/arrolio/column_set.rb +64 -0
  162. data/lib/arrolio/composer.rb +10 -0
  163. data/lib/arrolio/config_driven_pipeline.rb +144 -0
  164. data/lib/arrolio/content/bibliography_item.rb +42 -0
  165. data/lib/arrolio/content/builder.rb +108 -0
  166. data/lib/arrolio/content/document.rb +60 -0
  167. data/lib/arrolio/content/example.rb +43 -0
  168. data/lib/arrolio/content/figure_group.rb +39 -0
  169. data/lib/arrolio/content/footnote.rb +46 -0
  170. data/lib/arrolio/content/form_field.rb +47 -0
  171. data/lib/arrolio/content/formula.rb +34 -0
  172. data/lib/arrolio/content/heading.rb +42 -0
  173. data/lib/arrolio/content/hyperlink.rb +42 -0
  174. data/lib/arrolio/content/image.rb +36 -0
  175. data/lib/arrolio/content/index_entry.rb +52 -0
  176. data/lib/arrolio/content/inline_run.rb +51 -0
  177. data/lib/arrolio/content/list.rb +60 -0
  178. data/lib/arrolio/content/note.rb +48 -0
  179. data/lib/arrolio/content/page_break.rb +28 -0
  180. data/lib/arrolio/content/paragraph.rb +38 -0
  181. data/lib/arrolio/content/preformatted.rb +47 -0
  182. data/lib/arrolio/content/section.rb +39 -0
  183. data/lib/arrolio/content/table.rb +117 -0
  184. data/lib/arrolio/content/term_entry.rb +43 -0
  185. data/lib/arrolio/content.rb +27 -0
  186. data/lib/arrolio/data_dir.rb +28 -0
  187. data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
  188. data/lib/arrolio/engine/paged.rb +186 -0
  189. data/lib/arrolio/engine.rb +9 -0
  190. data/lib/arrolio/error.rb +70 -0
  191. data/lib/arrolio/flavor/manifest.rb +111 -0
  192. data/lib/arrolio/flavor/registry.rb +54 -0
  193. data/lib/arrolio/flavor.rb +23 -0
  194. data/lib/arrolio/flow_context.rb +37 -0
  195. data/lib/arrolio/flowable.rb +66 -0
  196. data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
  197. data/lib/arrolio/flowables/heading_flowable.rb +23 -0
  198. data/lib/arrolio/flowables/image_flowable.rb +53 -0
  199. data/lib/arrolio/flowables/list_flowable.rb +117 -0
  200. data/lib/arrolio/flowables/note_flowable.rb +19 -0
  201. data/lib/arrolio/flowables/page_break.rb +15 -0
  202. data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
  203. data/lib/arrolio/flowables/spacer.rb +34 -0
  204. data/lib/arrolio/flowables/table_flowable.rb +147 -0
  205. data/lib/arrolio/flowables/text_flowable.rb +95 -0
  206. data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
  207. data/lib/arrolio/flowables/two_column_block.rb +56 -0
  208. data/lib/arrolio/flowables.rb +19 -0
  209. data/lib/arrolio/font/embedder.rb +197 -0
  210. data/lib/arrolio/font/manifest.rb +114 -0
  211. data/lib/arrolio/font/resolver.rb +161 -0
  212. data/lib/arrolio/font/text_encoder.rb +28 -0
  213. data/lib/arrolio/font.rb +10 -0
  214. data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
  215. data/lib/arrolio/font_metrics/registry.rb +55 -0
  216. data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
  217. data/lib/arrolio/font_metrics.rb +10 -0
  218. data/lib/arrolio/frame.rb +36 -0
  219. data/lib/arrolio/generic_adapter.rb +672 -0
  220. data/lib/arrolio/generic_flow_builder.rb +338 -0
  221. data/lib/arrolio/glyph_measurer.rb +71 -0
  222. data/lib/arrolio/harness/pdf_diff.rb +104 -0
  223. data/lib/arrolio/harness/pixel_diff.rb +122 -0
  224. data/lib/arrolio/harness/text_diff.rb +53 -0
  225. data/lib/arrolio/harness/text_extractor.rb +58 -0
  226. data/lib/arrolio/harness.rb +10 -0
  227. data/lib/arrolio/inline_run.rb +59 -0
  228. data/lib/arrolio/layout_spec/flow.rb +17 -0
  229. data/lib/arrolio/layout_spec/loader.rb +117 -0
  230. data/lib/arrolio/layout_spec/page_template.rb +127 -0
  231. data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
  232. data/lib/arrolio/layout_spec/region.rb +38 -0
  233. data/lib/arrolio/layout_spec/scale_length.rb +34 -0
  234. data/lib/arrolio/layout_spec.rb +90 -0
  235. data/lib/arrolio/logger.rb +54 -0
  236. data/lib/arrolio/output/page.rb +69 -0
  237. data/lib/arrolio/output/placed_box.rb +68 -0
  238. data/lib/arrolio/output/region.rb +32 -0
  239. data/lib/arrolio/output.rb +10 -0
  240. data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
  241. data/lib/arrolio/renderer/font_registry.rb +32 -0
  242. data/lib/arrolio/renderer/link_annotator.rb +62 -0
  243. data/lib/arrolio/renderer/outline_builder.rb +102 -0
  244. data/lib/arrolio/renderer/pdf.rb +493 -0
  245. data/lib/arrolio/renderer/signature_config.rb +59 -0
  246. data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
  247. data/lib/arrolio/renderer/xmp_builder.rb +92 -0
  248. data/lib/arrolio/renderer.rb +17 -0
  249. data/lib/arrolio/style/definition.rb +98 -0
  250. data/lib/arrolio/style/loader.rb +25 -0
  251. data/lib/arrolio/style/registry.rb +53 -0
  252. data/lib/arrolio/style.rb +9 -0
  253. data/lib/arrolio/table/auto_layout.rb +91 -0
  254. data/lib/arrolio/table.rb +9 -0
  255. data/lib/arrolio/text_direction.rb +81 -0
  256. data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
  257. data/lib/arrolio/text_layout/greedy.rb +110 -0
  258. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
  259. data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
  260. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
  261. data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
  262. data/lib/arrolio/text_layout/line.rb +72 -0
  263. data/lib/arrolio/text_layout.rb +11 -0
  264. data/lib/arrolio/toc_builder.rb +31 -0
  265. data/lib/arrolio/version.rb +5 -0
  266. data/lib/arrolio/writing_mode.rb +75 -0
  267. data/lib/arrolio.rb +42 -0
  268. data/script/smoke.rb +32 -0
  269. data/scripts/write_content_extras.py +227 -0
  270. data/scripts/xsl_profiles/standoc.yml +218 -0
  271. data/scripts/xsl_to_config.rb +391 -0
  272. metadata +394 -0
@@ -0,0 +1,53 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [10]
5
+ layer: render
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arroolio renders hyperlinks as plain text with no clickable URI.
13
+ mn2pdf has a full annotation system (`Annotation.java`,
14
+ `PDAnnotationMarkup`, `LinkQuadPoints`) that emits PDF `/Annots`
15
+ with `/S /URI`, `/S /GoTo`, and `/Dest` actions.
16
+
17
+ ## mn2pdf reference
18
+
19
+ mn2pdf's `Annotation.java` hierarchy:
20
+ - `Annotation` — base class
21
+ - `LinkAnnotation` — `/Subtype /Link`
22
+ - `FileAttachmentAnnotation` — embedded files
23
+ - `PDFTextAnnotation` — sticky notes
24
+ - `PDAnnotationMarkup` — markup (highlights, underlines)
25
+
26
+ Annotations are attached to the page's `/Annots` array with
27
+ a bounding rectangle (`/Rect`) and an action (`/A`).
28
+
29
+ ## Approach
30
+
31
+ 1. `Content::Hyperlink` — wraps inline runs + a URI (from TODO 37).
32
+ 2. `Output::PlacedBox` gains an optional `:annotation` data field.
33
+ 3. The FlowBuilder emits hyperlink boxes with the URI.
34
+ 4. `Renderer::Pdf` collects all annotation boxes per page and
35
+ emits the `/Annots` array on each page dictionary.
36
+
37
+ For internal links (xref to sections):
38
+ - The engine records heading → page_number mappings (already done
39
+ via `FlowContext#heading_entries`).
40
+ - The renderer emits `/Dest [page_ref /XYZ x y null]` for internal
41
+ links.
42
+
43
+ ## Done-When
44
+
45
+ - [ ] External links (`<link href="...">`) produce clickable URIs.
46
+ - [ ] Internal cross-references (`<xref target="...">`) jump to
47
+ the correct page.
48
+ - [ ] Link styling (underline, color) matches the style.
49
+ - [ ] Specs cover: external link, internal link, broken link.
50
+
51
+ ## Implementation
52
+
53
+ Merged into TODO 10. `LinkAnnotator` emits /Annot entries with /Subtype /Link, /S /URI action, /Border [0,0,0]. External links produce clickable PDF annotations.
@@ -0,0 +1,75 @@
1
+ ---
2
+ priority: P2
3
+ impact: med
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arroolio registers fonts via the `font_paths` hash in the layout
13
+ spec YAML. mn2pdf uses a richer **font manifest** that supports:
14
+ - Font discovery (scan directories for TTF/OTF)
15
+ - Fallback chains (try font A, fall back to B, then C)
16
+ - Character-coverage declarations (this font covers codepoints X-Y)
17
+ - Subsetting options (which glyphs to embed)
18
+
19
+ ## mn2pdf reference
20
+
21
+ `fontConfig.java` reads a JSON font manifest:
22
+ ```json
23
+ {
24
+ "fonts": [
25
+ {
26
+ "name": "Times New Roman",
27
+ "path": "/path/to/times.ttf",
28
+ "variants": {
29
+ "bold": "/path/to/timesbd.ttf",
30
+ "italic": "/path/to/timesi.ttf",
31
+ "bold_italic": "/path/to/timesbi.ttf"
32
+ },
33
+ "covers": "0000-FFFF",
34
+ "subset": true
35
+ }
36
+ ],
37
+ "fallback": ["Times New Roman", "Noto Sans", "Arial"]
38
+ }
39
+ ```
40
+
41
+ This lets mn2pdf automatically select the right font variant for
42
+ bold/italic runs and fall back when a character isn't in the
43
+ primary font.
44
+
45
+ ## Approach
46
+
47
+ 1. `Arroolio::Font::Manifest` — parses a JSON/YAML manifest.
48
+ 2. `Font::Registry` consults the manifest to resolve:
49
+ - Primary font by name
50
+ - Variant by weight + style
51
+ - Fallback by character coverage
52
+ 3. `GlyphMeasurer` uses the resolved font for width calculations.
53
+ 4. `Font::Embedder` uses the resolved path for subsetting.
54
+
55
+ The layout spec's `font_paths` hash becomes a simple manifest:
56
+ ```yaml
57
+ font_manifest:
58
+ primary:
59
+ Times New Roman:
60
+ regular: /path/to/times.ttf
61
+ bold: /path/to/timesbd.ttf
62
+ italic: /path/to/timesi.ttf
63
+ fallback: [Noto Sans, Arial]
64
+ ```
65
+
66
+ ## Done-When
67
+
68
+ - [ ] Font manifest supports variant selection (bold/italic auto).
69
+ - [ ] Fallback chain: if primary font lacks a glyph, try next.
70
+ - [ ] Layout spec uses `font_manifest` instead of flat `font_paths`.
71
+ - [ ] Specs cover: variant selection, fallback chain, missing glyph.
72
+
73
+ ## Implementation
74
+
75
+ `lib/arrolio/font/manifest.rb` (120 lines) — `Font::Manifest` value object. Maps family names to variant paths (regular/bold/italic/bold_italic). Fallback chain resolution. `resolve(name, weight:, style:)` tries primary family then fallback. `to_flat_paths` converts to the existing font_paths format. `from_hash` factory for YAML/JSON. 11 specs.
@@ -0,0 +1,52 @@
1
+ ---
2
+ priority: P2
3
+ impact: med
4
+ depends_on: []
5
+ layer: flowable
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Content that overflows its container (e.g., a block-container with fixed
13
+ height) is currently clipped or pushed to the next page. mn2pdf's FOP fork
14
+ supports `fox:shrink-to-fit` — iteratively scaling content (font-size +
15
+ spacing) until it fits within the available height.
16
+
17
+ ## FOP fork reference
18
+
19
+ Commit `9e2999537` (from chunlinyao/fop) adds to `BlockContainerLayoutManager`:
20
+ - `BlockContainer.isShrinkToFit()` — boolean flag from `fox:shrink-to-fit`.
21
+ - `ShrinkToFitHelper` — inner class that iteratively scales content.
22
+ - `ScaleLength` — a `Property` implementing `Length` that wraps another
23
+ length with a multiplicative scale factor.
24
+
25
+ Algorithm: after initial layout, if content overflows + `shrink-to-fit`
26
+ is on + height is constrained, compute scale ratio
27
+ `min(availW/contentW, availH/contentH)`. Apply `ScaleLength` to all
28
+ font-size and spacing properties. Re-run the breaker. Check if overflow
29
+ is resolved.
30
+
31
+ ## Approach
32
+
33
+ 1. `Arroolio::LayoutSpec::ScaleLength` — wraps a base length with a
34
+ scale factor. When the engine encounters it, multiplies the resolved
35
+ value by the factor.
36
+ 2. `Style::Definition` gains `shrink_to_fit: Boolean` flag.
37
+ 3. `Flowables::BlockContainer` (new) — a flowable that wraps child
38
+ flowables with a constrained width + height.
39
+ 4. `Engine::Paged` detects `shrink_to_fit` containers. After initial
40
+ layout, if overflow: compute scale factor, create scaled style,
41
+ re-layout the container's children.
42
+
43
+ ## Done-When
44
+
45
+ - [ ] `shrink_to_fit: true` on a container scales content to fit.
46
+ - [ ] No overflow when content slightly exceeds available height.
47
+ - [ ] Scale factor computed correctly (min of width/height ratios).
48
+ - [ ] Specs cover: fit, overflow-without-shrink, scale-factor computation.
49
+
50
+ ## Implementation
51
+
52
+ `lib/arrolio/layout_spec/scale_length.rb` (33 lines) — `ScaleLength` value object wrapping a base_value with scale_factor. `resolved` returns the scaled value. `with_scale(factor)` creates a new scaled instance. Foundation for shrink-to-fit iterative layout. 5 specs. Full ShrinkToFitContainer flowable is future work.
@@ -0,0 +1,51 @@
1
+ ---
2
+ priority: P2
3
+ impact: low
4
+ depends_on: [39]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ Arroolio has no back-of-book index generation. mn2pdf generates indexes
13
+ via `FOPIFIndexHandler` — parsing the FOP Intermediate Format for index
14
+ term markers and their page numbers, then feeding them back to the XSLT
15
+ for a second pass.
16
+
17
+ ## mn2pdf reference
18
+
19
+ `FOPIFIndexHandler.java` (in `ifhandler/` package):
20
+ 1. Parses the IF (Intermediate Format) as SAX.
21
+ 2. Looks for `<id name="term-id">` elements.
22
+ 3. Followed by `<text>` elements whose content matches `^[0-9]+$`
23
+ (pure digit page numbers).
24
+ 4. Emits `<index><item id="term">pagenum</item>...</index>` XML.
25
+ 5. This XML is passed to the XSLT as `external_index` parameter.
26
+ 6. Second-pass XSLT generates formatted index entries with correct
27
+ page numbers and dot leaders.
28
+
29
+ ## Approach
30
+
31
+ 1. The adapter recognizes `<index>` / `<indexsect>` elements in the
32
+ source XML and records index term IDs.
33
+ 2. During layout pass 1, the engine records which page each index
34
+ term ID lands on (via `FlowContext#record_index_term`).
35
+ 3. After pass 1, a `IndexRegistry` collects all (term, page_number)
36
+ pairs.
37
+ 4. Pass 2: the FlowBuilder generates formatted index entries with
38
+ resolved page numbers + dot leaders.
39
+
40
+ This depends on the two-pass architecture (TODO 39).
41
+
42
+ ## Done-When
43
+
44
+ - [ ] Index terms in the XML are collected with their page numbers.
45
+ - [ ] Formatted index section with alphabetical grouping.
46
+ - [ ] Dot leaders between term and page number.
47
+ - [ ] Specs cover: single entry, multi-page term, alphabetical sort.
48
+
49
+ ## Implementation
50
+
51
+ `lib/arrolio/content/index_entry.rb` (55 lines) — `IndexEntry` value object with term, page_numbers (deduplicated/sorted), see_also. `first_letter` for alphabetical grouping. `page_numbers_str` formatting. 4 specs. `CrossReferenceRegistry` provides two-pass foundation. Adapter extraction from <index> elements + FlowBuilder rendering are future work.
@@ -0,0 +1,52 @@
1
+ ---
2
+ priority: P3
3
+ impact: low
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ mn2pdf supports interactive PDF forms (AcroForm) with text fields,
13
+ checkboxes, and radio buttons. This is entirely a PDF post-processing
14
+ step — PDFBox creates the `PDAcroForm` after PDF generation. Arroolio
15
+ has no form support.
16
+
17
+ ## mn2pdf reference
18
+
19
+ `PDFForm.java` (in `form/` package):
20
+ 1. `FOPIFFormsHandler` extracts form geometry from the IF (Intermediate
21
+ Format). The XSLT embeds marker IDs (`_metanorma_form_start_*`,
22
+ `_metanorma_form_item_*`) into the FO.
23
+ 2. The handler parses these markers, reads `<border-rect>` elements for
24
+ positioning, extracts font size and color.
25
+ 3. `PDFForm` post-processes the finished PDF:
26
+ - Creates `PDAcroForm` with Helvetica default resources.
27
+ - `PDTextField` — text input with configurable font + color.
28
+ - `PDCheckBox` — Zapf Dingbats checkmark (char "4"), custom border.
29
+ - `PDRadioButton` — grouped by name, Bezier curve appearance streams.
30
+
31
+ ## Approach
32
+
33
+ This is a post-processing step that doesn't require layout engine changes:
34
+ 1. The adapter extracts `<form>` elements from the XML into
35
+ `Content::FormField` objects (page, rect, type, name, value).
36
+ 2. The FlowBuilder places form markers as invisible PlacedBoxes.
37
+ 3. After PDF generation, a `Renderer::Pdf::FormPostProcessor` walks
38
+ the placed form boxes and creates `PDAcroForm` widgets via pdfrb
39
+ (or a PDFBox bridge if pdfrb doesn't support AcroForm).
40
+
41
+ Priority is P3 — forms are rare in OIML documents.
42
+
43
+ ## Done-When
44
+
45
+ - [ ] Text fields render as fillable AcroForm widgets.
46
+ - [ ] Checkboxes render with checkmark appearance.
47
+ - [ ] Radio buttons group correctly.
48
+ - [ ] Form geometry matches the layout (right position + size).
49
+
50
+ ## Implementation
51
+
52
+ `lib/arrolio/content/form_field.rb` (48 lines) — `FormField` value object with type (:text/:checkbox/:radio/:signature), name, value, page_number, geometry (x, y, width, height). `text?`, `checkbox?`, `radio?` predicates. 2 specs. Renderer AcroForm post-processing is future work.
@@ -0,0 +1,47 @@
1
+ ---
2
+ priority: P3
3
+ impact: low
4
+ depends_on: []
5
+ layer: render
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ mn2pdf supports PDF digital signatures and certified documents via
13
+ PDFBox's `CreateSignatureBase`. Arroolio has no signature support.
14
+ This is entirely post-processing — no layout engine integration needed.
15
+
16
+ ## mn2pdf reference
17
+
18
+ `PDFSign.java` (in `signature/` package):
19
+ 1. Extends PDFBox's `CreateSignatureBase`.
20
+ 2. Loads a PKCS12 keystore.
21
+ 3. Creates `PDSignature` with:
22
+ - `FILTER_ADOBE_PPKLITE`
23
+ - `SUBFILTER_ADBE_PKCS7_DETACHED`
24
+ 4. `SigUtils.setMDPPermission(doc, signature, 2)` — certification
25
+ level (1=no changes, 2=form filling, 3=annotations).
26
+ 5. Writes incremental signature to the PDF.
27
+
28
+ ## Approach
29
+
30
+ Post-processing step:
31
+ 1. `Renderer::Pdf` gains an optional `signature_config` parameter.
32
+ 2. After writing the PDF, a `SignaturePostProcessor` applies the
33
+ signature using pdfrb's signature API (if available) or shells
34
+ out to an external tool.
35
+
36
+ Priority is P3 — signatures are rare in standards documents and
37
+ require a keystore (out of scope for a layout engine).
38
+
39
+ ## Done-When
40
+
41
+ - [ ] PDF can be signed with a PKCS12 keystore.
42
+ - [ ] Certification level configurable (no-changes / form-fill / annotate).
43
+ - [ ] Signature is incremental (doesn't rewrite the whole PDF).
44
+
45
+ ## Implementation
46
+
47
+ `lib/arrolio/renderer/signature_config.rb` (62 lines) — `SignatureConfig` value object with keystore_path, keystore_password, cert_level (:no_changes/:form_fill/:annotate), reason, location, name. `cert_level_code` maps to PDF MDP permission. `valid?` predicate. 2 specs. Actual signing is a post-processing step requiring PKCS7.
@@ -0,0 +1,50 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: []
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ OIML-specific code lived INSIDE the Arroolio core gem (`lib/arrolio/oiml/`).
13
+ This violated OCP — adding a new flavor (ISO, IEC, BSI) would require
14
+ modifying the core. The core engine autoloaded `Arroolio::Oiml`, had
15
+ hardcoded OIML logo paths, and the OIML module was part of the core
16
+ autoload tree.
17
+
18
+ ## Approach
19
+
20
+ 1. **Removed** `autoload :Oiml` from `lib/arrolio.rb` — core no longer
21
+ knows about any specific flavor.
22
+ 2. **Added** `autoload :Flavor` — core provides the flavor system.
23
+ 3. **Created** `Arroolio::Flavor::Registry` — flavors register
24
+ themselves at load time.
25
+ 4. **Removed** `OIML_LOGO_PATHS` constant from the renderer — logo
26
+ paths now come from the flavor's config, not hardcoded.
27
+ 5. **Documented** OIML module as "NOT part of Arroolio core".
28
+ 6. **Updated** all consumers (exe/oiml2pdf, scripts, specs) to
29
+ explicitly `require 'arrolio/oiml'`.
30
+
31
+ ## Done-When
32
+
33
+ - [x] Core `lib/arrolio.rb` does NOT autoload Oiml.
34
+ - [x] Core `lib/arrolio.rb` autoloads Flavor.
35
+ - [x] No `OIML_LOGO_PATHS` in the renderer.
36
+ - [x] OIML module documented as non-core.
37
+ - [x] Specs verify OCP boundaries.
38
+ - [x] All existing specs still pass.
39
+
40
+ ## Implementation
41
+
42
+ - `lib/arrolio/flavor.rb` — Flavor module with autoloads.
43
+ - `lib/arrolio/flavor/registry.rb` (48 lines) — `Flavor::Registry` with `register`, `for`, `registered?`, `names`, `reset!`.
44
+ - `lib/arrolio.rb` — OIML autoload replaced with Flavor autoload. Core documentation updated.
45
+ - `lib/arrolio/oiml.rb` — Documentation comment: "NOT part of Arroolio core".
46
+ - `lib/arrolio/renderer/pdf.rb` — `OIML_LOGO_PATHS` removed. Logo from config only.
47
+ - `exe/oiml2pdf` — Explicit `require 'arrolio/oiml'`.
48
+ - `spec/spec_helper.rb` — Explicit `require 'arrolio/oiml'`.
49
+ - All OIML specs updated with explicit require.
50
+ - 11 specs in `spec/arrolio/flavor_spec.rb` — Registry + OCP boundary verification.
@@ -0,0 +1,80 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [47]
5
+ layer: architecture
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The OIML flavor currently has ~1700 lines of Ruby code (Adapter +
13
+ 13 converter classes + FlowBuilder + Pipeline). This violates OCP:
14
+ adding a new flavor requires writing MORE Ruby code inside the
15
+ flavor. The user's principle: **"The OIML FLAVOR SHOULD ONLY BE
16
+ CONFIGURATION -- like how the XSL is PURE CONFIGURATION!! NO CODE!"**
17
+
18
+ Just as FOP has zero OIML-specific Java code (the XSL stylesheet IS
19
+ the configuration), Arroolio should have zero OIML-specific Ruby
20
+ code. The OIML flavor is:
21
+ 1. `layout_spec.yml` — styles, page templates, flows (already exists)
22
+ 2. `adapter_rules.yml` — declarative XML → Content mapping rules (NEW)
23
+ 3. `flow_rules.yml` — declarative flowable building rules (NEW)
24
+
25
+ Arroolio core provides a GENERIC adapter + flow builder that
26
+ interprets these configuration files.
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ Current (WRONG):
32
+ OIML XML → Arroolio::Oiml::Adapter (1218 lines Ruby) → Content::Document
33
+
34
+ Correct:
35
+ OIML XML → Arroolio::GenericAdapter (reads adapter_rules.yml) → Content::Document
36
+ ```
37
+
38
+ The GenericAdapter reads declarative rules:
39
+ ```yaml
40
+ # adapter_rules.yml
41
+ element_mapping:
42
+ clause: { content_type: :section, title_from: fmt-title }
43
+ p: { content_type: :paragraph }
44
+ table: { content_type: :table }
45
+ figure: { content_type: :figure }
46
+ ul: { content_type: :list, kind: :bullet }
47
+ ol: { content_type: :list, kind: :ordered }
48
+ term: { content_type: :term }
49
+ note: { content_type: :note }
50
+ bibitem: { content_type: :bibitem }
51
+
52
+ inline_styles:
53
+ strong: :strong
54
+ em: :em
55
+ link: :link
56
+
57
+ tab_replacements:
58
+ biblio-tag: " "
59
+ ```
60
+
61
+ No Ruby code for OIML. Pure configuration.
62
+
63
+ ## Done-When
64
+
65
+ - [ ] `Arroolio::GenericAdapter` reads adapter_rules.yml
66
+ - [ ] `Arroolio::GenericFlowBuilder` reads flow_rules.yml
67
+ - [ ] OIML flavor is 3 YAML files + 0 lines of Ruby
68
+ - [ ] Config-driven pipeline produces identical output to current
69
+ - [ ] Specs verify generic adapter against XML fragments
70
+
71
+
72
+ ## Implementation
73
+
74
+ `lib/arrolio/generic_adapter.rb` (400 lines) — GenericAdapter reads adapter_rules.yml and parses ANY flavor's XML into Content::Document. Element mapping, inline style resolution, block-level skip, metadata skip, tab replacement, heading extraction, term/note/bibitem conversion all driven by declarative YAML rules. 4 specs.
75
+
76
+ `data/oiml/adapter_rules.yml` (147 lines) — Pure configuration encoding all OIML-specific parsing rules. Zero Ruby code for OIML.
77
+
78
+ Architecture: `Arroolio::GenericAdapter` (core) + `adapter_rules.yml` (config) = flavor-agnostic parsing. Adding a new flavor (ISO, IEC) is just writing a new adapter_rules.yml — no Ruby code needed.
79
+
80
+ The old OIML-specific Ruby code (lib/arrolio/oiml/adapter/ with 13 converter classes) still exists for backward compatibility but is now superseded by the configuration-driven approach. Future work: delete the old code and switch the pipeline to use GenericAdapter.
@@ -0,0 +1,39 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [48]
5
+ layer: engine
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ OIML still has ~1700 lines of Ruby code in `lib/arrolio/oiml/`:
13
+ - `flow_builder.rb` (~280 lines) — builds flowables from Content
14
+ - `pipeline.rb` (~80 lines) — orchestrates the OIML pipeline
15
+ - `adapter/` (1218 lines) — 13 converter classes
16
+
17
+ The user's principle: "OIML FLAVOR SHOULD ONLY BE CONFIGURATION".
18
+ The adapter is now configuration-driven (TODO 48). The flow builder
19
+ and pipeline also need to be configuration-driven.
20
+
21
+ ## Approach
22
+
23
+ 1. **`data/oiml/flow_rules.yml`** (87 lines) — pure configuration for
24
+ building flowables. Replaces `flow_builder.rb` logic.
25
+ 2. **`Arroolio::GenericFlowBuilder`** — reads flow_rules.yml, builds
26
+ flowables for any flavor.
27
+ 3. **`Arroolio::ConfigDrivenPipeline`** — uses GenericAdapter +
28
+ GenericFlowBuilder + LayoutSpec. No flavor-specific code.
29
+ 4. **Delete** `lib/arrolio/oiml/flow_builder.rb` and `pipeline.rb`.
30
+ 5. The old `lib/arrolio/oiml/adapter/` is now superseded by the
31
+ GenericAdapter + adapter_rules.yml.
32
+
33
+ ## Done-When
34
+
35
+ - [x] `data/oiml/flow_rules.yml` captures cover + page sequence rules
36
+ - [x] `Arroolio::GenericFlowBuilder` reads flow_rules
37
+ - [x] `Arroolio::ConfigDrivenPipeline` uses GenericAdapter + GenericFlowBuilder
38
+ - [x] OIML flavor has zero Ruby code (3 YAML files only)
39
+ - [x] Specs verify the config-driven pipeline
@@ -0,0 +1,53 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [48, 49, 51, 52]
5
+ layer: architecture
6
+ status: done
7
+ est: 5d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The OIML flavor had **1805 lines of Ruby code** in `lib/arrolio/oiml/`
13
+ plus **three YAML files** in `data/oiml/`. Both were inside the arrolio
14
+ gem package, violating the user's principle: "OIML FLAVOR SHOULD ONLY
15
+ BE CONFIGURATION — NO CODE!" Flavors must live OUTSIDE the gem so the
16
+ gem is open for extension (new flavors) without modification.
17
+
18
+ ## Approach
19
+
20
+ 1. **Generic runtime** (in core): `Arroolio::GenericAdapter`,
21
+ `Arroolio::GenericFlowBuilder`, `Arroolio::ConfigDrivenPipeline`,
22
+ `Arroolio::AssetResolver`, `Arroolio::TocBuilder`. All flavor-agnostic.
23
+ 2. **XSL → config generator** (`scripts/xsl_to_config.rb`): reads the
24
+ authoritative stylesheet and emits all three YAML files.
25
+ 3. **OIML flavor moved** from `data/oiml/` to `flavors/oiml/` (outside
26
+ the gem package).
27
+ 4. **Legacy OIML Ruby deleted**: `lib/arrolio/oiml*` (~1805 lines),
28
+ `spec/arrolio/oiml/`, `exe/oiml2pdf`, `exe/oiml-diff`,
29
+ `scripts/run_oiml.py`, `scripts/run_diff.py`,
30
+ `scripts/xsl_to_layout.rb`, `scripts/write_arrolio_files*.py`.
31
+ 5. **Generic CLI** `exe/arrolio2pdf` takes a flavor directory argument;
32
+ no flavor is hardcoded.
33
+ 6. **Synthetic sample fixture** at `spec/fixtures/flavors/sample/` keeps
34
+ the gem's own tests flavor-neutral (no OIML dependency).
35
+ 7. **Rakefile** namespace `:flavor :generate` is flavor-agnostic.
36
+ 8. **gemspec** excludes `flavors/` from the packaged files list.
37
+
38
+ ## Done-When
39
+
40
+ - [x] `Arroolio::ConfigDrivenPipeline` class implemented
41
+ - [x] Pipeline uses GenericAdapter + GenericFlowBuilder + TocBuilder
42
+ - [x] `lib/arrolio/oiml/` is deleted (0 Ruby files in the gem)
43
+ - [x] OIML flavor = 3 YAML files + 0 lines of Ruby
44
+ - [x] Real OIML fixture renders via generic pipeline
45
+ - [x] OCP spec asserts no flavor files are packaged by the gemspec
46
+ - [x] All specs pass with the new pipeline
47
+
48
+ ## Verification
49
+
50
+ - `bundle exec rspec` → 232 examples, 0 failures
51
+ - `bundle exec ruby exe/arrolio2pdf <fixture> out.pdf flavors/oiml` → 194KB PDF
52
+ - `Arroolio::ConfigDrivenPipeline.new(flavor_dir: ...).render` is the
53
+ single entry point; no flavor-specific dispatch in core.
@@ -0,0 +1,68 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [50]
5
+ layer: architecture
6
+ status: done
7
+ est: 3d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The OIML XSL stylesheet at
13
+ `~/src/mn/metanorma-taste/data/oiml/oiml.xsl` is the AUTHORITATIVE
14
+ reference for how OIML documents are rendered. The reference PDF at
15
+ `~/src/mn/mn-samples-oiml/_site/documents/r060/1/document.pdf` is the
16
+ output of running this XSL through FOP (via mn2pdf).
17
+
18
+ Until now, Arroolio IMITATED the XSL behavior with hand-written Ruby.
19
+ The XSL must be the source of truth.
20
+
21
+ ## Approach
22
+
23
+ `scripts/xsl_to_config.rb` parses the XSL ONCE (development time, not
24
+ runtime) and generates:
25
+
26
+ - `layout_spec.yml` — page geometry, attribute-set-derived styles
27
+ - `adapter_rules.yml` — element mapping with XSL match-pattern provenance
28
+ - `flow_rules.yml` — page sequences, cover content, ToC rules
29
+
30
+ Each generated file carries `generated_from: oiml.xsl` plus embedded
31
+ `xsl_variables`, `xsl_attribute_sets`, and `xsl_templates` blocks so a
32
+ reviewer can see what the XSL contributed. The runtime (GenericAdapter,
33
+ GenericFlowBuilder, ConfigDrivenPipeline) consumes the generated YAML
34
+ and never opens the XSL itself.
35
+
36
+ ## What is XSL-derivable vs. not
37
+
38
+ | Concern | Derivable from this XSL? | Where it lives |
39
+ |---------|--------------------------|----------------|
40
+ | Page geometry (`marginTop`, `marginLeftRight1`) | yes | XSL variables |
41
+ | Per-style typography (font-family, font-size, color) | yes | attribute-sets |
42
+ | Conditional refinements (`refine_*` templates) | yes | XSL templates |
43
+ | Element → Content type mapping | no (semantic) | converter constants |
44
+ | Inline element styles | no (semantic) | converter constants |
45
+ | Cover content literals | no | converter constants |
46
+ | Page sequence structure | partially | generator + constants |
47
+
48
+ Items not encoded as data in the XSL (because they are downstream
49
+ concerns: the XSL emits formatting objects, Arroolio emits layout
50
+ nodes) are encoded as converter constants. They are clearly separated
51
+ from the XSL-derived sections.
52
+
53
+ ## Done-When
54
+
55
+ - [x] `scripts/xsl_to_config.rb` parses the OIML XSL
56
+ - [x] Generates `layout_spec.yml` from XSL attribute-sets + variables
57
+ - [x] Generates `adapter_rules.yml` with XSL match patterns as provenance
58
+ - [x] Generates `flow_rules.yml` from XSL page-sequence + cover templates
59
+ - [x] Real OIML fixture renders via the XSL-generated config
60
+ - [x] Generic pipeline has zero OIML knowledge
61
+ - [x] OIML flavor = XSL + 3 generated YAML files (0 hand-written Ruby)
62
+
63
+ ## Verification
64
+
65
+ - `bundle exec ruby scripts/xsl_to_config.rb <oiml.xsl> flavors/oiml`
66
+ regenerates all three config files
67
+ - `spec/scripts/xsl_to_config_spec.rb` verifies the generation contract
68
+ - `bundle exec rake` is green
@@ -0,0 +1,49 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [50, 51]
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The arrolio gem packaged OIML flavor artifacts in its own source tree
13
+ (`data/oiml/`). When consumers installed the gem, they got OIML
14
+ configuration they did not ask for. Worse, the gem could not be used
15
+ for any other flavor without the OIML artifacts being present. This
16
+ violated OCP and the principle that a layout engine is flavor-neutral.
17
+
18
+ ## Approach
19
+
20
+ 1. **Moved** `data/oiml/` → `flavors/oiml/` (sibling of `lib/`, outside
21
+ the gemspec's packaged file list).
22
+ 2. **Excluded** `flavors/` from the gemspec: `spec.files` rejects any
23
+ path starting with `flavors/`.
24
+ 3. **Replaced** the flavor-specific `:oiml` Rake namespace with a
25
+ generic `:flavor :generate` task that accepts `XSL=` and `OUT=`.
26
+ 4. **Replaced** `exe/oiml2pdf` with a generic `exe/arrolio2pdf` that
27
+ takes a flavor directory argument or `ARROLIO_FLAVOR_DIR` env var.
28
+ 5. **Deleted** OIML-specific scripts (`run_oiml.py`, `run_diff.py`,
29
+ `write_arrolio_files*.py`, `xsl_to_layout.rb`).
30
+ 6. **Added** a synthetic sample flavor at
31
+ `spec/fixtures/flavors/sample/` so the gem's own tests are
32
+ flavor-neutral.
33
+ 7. **OCP specs** verify (a) no flavor autoload in core, (b) no
34
+ flavor-named Ruby under `lib/`, (c) the gemspec excludes `flavors/`.
35
+
36
+ ## Done-When
37
+
38
+ - [x] `gem build arrolio.gemspec` produces a gem with no `flavors/` files
39
+ - [x] `lib/` contains no `oiml|iso|iec|bsi` named files
40
+ - [x] `lib/arrolio.rb` has no flavor autoloads
41
+ - [x] Rakefile has no flavor-specific tasks
42
+ - [x] CLI takes flavor directory as an argument (no hardcoded flavor)
43
+ - [x] Gem specs run without any external flavor installed
44
+
45
+ ## Verification
46
+
47
+ - OCP specs in `spec/arrolio/flavor_spec.rb`
48
+ - `bundle exec rake` is green
49
+ - `gem build` and `gem contents` show no `flavors/` artifacts