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,74 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [52]
5
+ layer: architecture
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The generic pipeline handled only one real flavor (OIML). Without a
13
+ second flavor implemented using only XSL + 3 generated YAML files, the
14
+ "any flavor is pure configuration" claim was unverified.
15
+
16
+ ## Approach
17
+
18
+ Created a synthetic second flavor at
19
+ `spec/fixtures/flavors/altvocab/` with a deliberately non-Metanorma
20
+ vocabulary:
21
+
22
+ | Role | sample (standoc) | altvocab |
23
+ |------|------------------|----------|
24
+ | Container | `metanorma` | `article` |
25
+ | Metadata | `bibdata` | `meta` |
26
+ | Sections container | `sections` | `body` |
27
+ | Section | `clause` | `sec` |
28
+ | Heading | `fmt-title` (depth) | `h2` (level) |
29
+ | Paragraph | `p` | `para` |
30
+ | List item | `li` | `li` (same) |
31
+ | Marker | `fmt-name` | `marker` |
32
+ | Note label | `fmt-name` | `label` |
33
+ | Figure image | `image` | `img` |
34
+ | Figure caption | `fmt-name` | `caption` |
35
+ | Table row/cell | `tr`/`td,th` | `row`/`cell,header` |
36
+ | Bibliography | `bibliography` | `refs` |
37
+ | References | `references` | `list` |
38
+ | Item | `bibitem` | `entry` |
39
+ | Inline strong | `strong` | `b` |
40
+ | Inline italic | `em` | `i` |
41
+ | Stem | `stem`/`fmt-stem` | `math`/`mathfmt` |
42
+
43
+ This is a genuinely different vocabulary — not just renamed standoc.
44
+ The fact that the generic adapter parses it correctly proves there is
45
+ no hidden Metanorma dependency.
46
+
47
+ The flavor is rendered through the same `ConfigDrivenPipeline` with
48
+ zero core code changes. All element selectors come from the flavor's
49
+ `adapter_rules.yml`. All flow rules come from its `flow_rules.yml`.
50
+ All styles come from its `layout_spec.yml`. It declares its own
51
+ `manifest.yml`.
52
+
53
+ ## Done-When
54
+
55
+ - [x] Second flavor directory exists at `spec/fixtures/flavors/altvocab/`
56
+ - [x] Uses a deliberately non-Metanorma vocabulary (article/sec/para/h2)
57
+ - [x] Renders a fixture via the generic pipeline with zero core changes
58
+ - [x] All selectors come from `adapter_rules.yml`
59
+ - [x] Manifest is loadable via `Flavor::Manifest.load`
60
+ - [x] Specs verify parse correctness + PDF render + manifest exposure
61
+ - [x] All 267+ specs still pass
62
+
63
+ ## Verification
64
+
65
+ - `spec/arrolio/second_flavor_spike_spec.rb` (3 specs)
66
+ - `bundle exec rake` is green
67
+ - `bundle exec ruby exe/arrolio2pdf <fixture.xml> out.pdf spec/fixtures/flavors/altvocab`
68
+
69
+ ## Outcome
70
+
71
+ The generic design is now proven across two distinct vocabularies
72
+ (Metanorma standoc + synthetic altvocab). Adding a third real-world
73
+ flavor (ISO, IEC, BSI, DITA, DocBook) is a configuration exercise, not
74
+ a code change.
@@ -0,0 +1,72 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [50]
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ A flavor directory was just "three YAML files". There was no
13
+ machine-readable manifest declaring the flavor's name, version,
14
+ upstream XSL, required fonts, or config-file locations. This meant:
15
+
16
+ - No way to detect a broken/truncated flavor directory at load time.
17
+ - No way to surface flavor metadata in error messages.
18
+ - No discovery mechanism for installed flavor gems.
19
+
20
+ ## Approach
21
+
22
+ 1. **`flavors/<name>/manifest.yml`** is the flavor's contract:
23
+
24
+ ```yaml
25
+ name: oiml
26
+ version: 1.0.0
27
+ description: OIML International Recommendation
28
+ upstream:
29
+ xsl: oiml.xsl
30
+ doctypes:
31
+ - recommendation
32
+ - document
33
+ fonts:
34
+ required:
35
+ - Times New Roman
36
+ - Jost
37
+ optional:
38
+ - Cambria Math
39
+ config_files:
40
+ layout_spec: layout_spec.yml
41
+ adapter_rules: adapter_rules.yml
42
+ flow_rules: flow_rules.yml
43
+ ```
44
+
45
+ 2. **`Arroolio::Flavor::Manifest`** class (autoloaded from
46
+ `lib/arrolio/flavor/manifest.rb`): loads + validates the manifest,
47
+ provides typed accessors (`name`, `version`, `description`,
48
+ `required_fonts`, `optional_fonts`, `config_path_for(:layout_spec)`).
49
+ 3. **`ConfigDrivenPipeline.new(flavor_dir:)`** auto-detects the
50
+ manifest if present and exposes it via `pipeline.manifest`.
51
+ 4. **`Flavor::Manifest.load(flavor_dir)`** raises
52
+ `Arroolio::FlavorError` (new typed error) with structured metadata
53
+ when the manifest is missing or malformed.
54
+ 5. **Manifests added** to `flavors/oiml/manifest.yml` and
55
+ `spec/fixtures/flavors/sample/manifest.yml`.
56
+
57
+ ## Done-When
58
+
59
+ - [x] `Arroolio::Flavor::Manifest` class with typed accessors
60
+ - [x] `Arroolio::FlavorError` typed error for missing/malformed manifests
61
+ - [x] `flavors/oiml/manifest.yml` exists
62
+ - [x] `spec/fixtures/flavors/sample/manifest.yml` exists
63
+ - [x] `ConfigDrivenPipeline` exposes `pipeline.manifest`
64
+ - [x] Specs cover: valid manifest load, missing manifest, malformed
65
+ manifest, missing required field
66
+ - [x] All 249+ specs still pass
67
+
68
+ ## Verification
69
+
70
+ - `bundle exec rspec spec/arrolio/flavor/manifest_spec.rb`
71
+ - `bundle exec rake` is green
72
+ - OIML r060/1 fixture still renders via the generic pipeline
@@ -0,0 +1,57 @@
1
+ ---
2
+ priority: P1
3
+ impact: med
4
+ depends_on: [50, 63]
5
+ layer: render
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The renderer (`lib/arrolio/renderer/pdf.rb`) hardcoded OIML-specific
13
+ header/footer and cover-logo values:
14
+
15
+ - Margins: `26.5 * MM_TO_PT` / `25.5 * MM_TO_PT` (OIML values)
16
+ - Header/footer font: `'Arial'` hardcoded
17
+ - Header/footer size: `9.0` hardcoded
18
+ - Rule width: `0.5` hardcoded
19
+ - Cover logo dimensions: `35.0mm` / `1459.0/1667.0` aspect (OIML logo)
20
+ - Cover logo margin: `25.5mm`
21
+
22
+ These are all OIML conventions. A flavor with different margins,
23
+ fonts, or logo dimensions would not render correctly without
24
+ modifying core — violating OCP.
25
+
26
+ ## Approach
27
+
28
+ 1. **LayoutSpec carries config**: `LayoutSpec` now exposes
29
+ `header_footer_config` and `cover_logo_config` Hashes, loaded from
30
+ `layout_spec.yml`'s `header_footer:` and `cover_logo:` blocks.
31
+ 2. **Renderer reads from layout_spec**: `Renderer::Pdf#render`
32
+ accepts a `layout_spec:` keyword; `header_footer_style` and
33
+ `cover_logo_style` read values from it (falling back to
34
+ engine-safe defaults when nil).
35
+ 3. **Pipeline passes layout_spec through**: `ConfigDrivenPipeline`
36
+ forwards `layout_spec:` to `Renderer::Pdf#render`.
37
+ 4. **Flavor YAML declares its values**: `flavors/oiml/layout_spec.yml`
38
+ declares `header_footer:` (Arial 9pt + 0.5pt rule + 26.5/25.5mm
39
+ margins) and `cover_logo:` (35mm / 0.8758 aspect / 25.5mm margin).
40
+ 5. **Sample fixture also declares** its own (Helvetica-based) values.
41
+
42
+ ## Done-When
43
+
44
+ - [x] `LayoutSpec` exposes `header_footer_config` and `cover_logo_config`
45
+ - [x] `Loader` reads them from `layout_spec.yml`
46
+ - [x] `Renderer::Pdf#render` accepts `layout_spec:` and reads values
47
+ - [x] No hardcoded `'Arial'` or `26.5` literals in the renderer logic
48
+ - [x] Engine-safe defaults apply when `layout_spec:` is nil
49
+ - [x] Specs verify: configured values used, defaults used, both paths
50
+ tested
51
+ - [x] OIML r060/1 still renders 28 pages via the generic pipeline
52
+
53
+ ## Verification
54
+
55
+ - `spec/arrolio/layout_spec_config_spec.rb` (7 specs)
56
+ - `bundle exec rake` is green
57
+ - OIML render via `exe/arrolio2pdf` produces a 28-page PDF
@@ -0,0 +1,56 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [50, 52, 63]
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The generic adapter still hardcoded Metanorma standoc vocabulary
13
+ throughout (`preface`, `bibliography`, `references`, `bibitem`,
14
+ `clause`, `foreword`, `title`, `span`, `tab`, `br`). A flavor with a
15
+ different vocabulary (DITA, DocBook, JATS) would not work without
16
+ modifying core — violating OCP.
17
+
18
+ ## Approach
19
+
20
+ 1. **Extended `selectors` block** in `adapter_rules.yml`: added keys
21
+ for `preface_container`, `preface_children`, `bibliography_container`,
22
+ `bibliography_reference`, `bibliography_item`, `fallback_title`,
23
+ `span`, `tab_inline`, `break_inline`.
24
+ 2. **`GenericAdapter` reads selectors** for every element it touches —
25
+ no hardcoded element names outside the clearly-labelled
26
+ `DEFAULT_SELECTORS` constant.
27
+ 3. **`DEFAULT_SELECTORS` documents the standoc convention** as a
28
+ fallback so flavors that omit selectors still work, but every
29
+ real flavor (OIML + sample fixture) declares its full selector set.
30
+ 4. **`scripts/xsl_to_config.rb`** emits the full `selectors` block
31
+ (27 keys) in generated `adapter_rules.yml`.
32
+ 5. **Bug fix**: `extract_bibliography` had referenced `preface_elem`
33
+ in the bibliography scope — fixed to `biblio_elem`.
34
+ 6. **Strict typing in constructor**: `Content::Document.new` now
35
+ validates that `sections`, `preface`, and `bibliography` arrays
36
+ contain only `Content::Section` instances, raising `ContentError`
37
+ at the construction site rather than deep in the pipeline.
38
+
39
+ ## Done-When
40
+
41
+ - [x] `GenericAdapter` has no Metanorma element names outside
42
+ `DEFAULT_SELECTORS`
43
+ - [x] `DEFAULT_SELECTORS` includes the 9 new document-structure keys
44
+ - [x] OCP grep spec enforces no `fmt-*`, no flavor literals, no OIML
45
+ references in the generic core
46
+ - [x] Non-Metanorma vocabulary spec proves the adapter parses
47
+ custom XML using only selectors
48
+ - [x] `Content::Document` constructor validates Section typing
49
+ - [x] Real OIML pipeline still renders 28 pages via the generic
50
+ pipeline
51
+
52
+ ## Verification
53
+
54
+ - `spec/arrolio/ocp_grep_spec.rb` — 5 OCP guard specs
55
+ - `bundle exec rake` is green (244 examples)
56
+ - OIML render: 28 pages, ~258 KB
@@ -0,0 +1,49 @@
1
+ ---
2
+ priority: P0
3
+ impact: high
4
+ depends_on: [50, 63]
5
+ layer: adapter
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `Content::Document#sections` returned a mixed Array that, despite
13
+ earlier fixes, had no constructor-level guarantee of being
14
+ `Section[]`. A flavor whose adapter mistakenly pushed a Paragraph
15
+ into the array would fail deep in the pipeline with `NoMethodError:
16
+ undefined method 'heading?'` rather than at the construction site.
17
+
18
+ This is a typing failure: `sections` should be `Section[]` by
19
+ contract. Validation belongs in the constructor.
20
+
21
+ ## Approach
22
+
23
+ 1. **Constructor validation**: `Content::Document.new` raises
24
+ `ContentError` if any element of `sections` (or `preface`,
25
+ `bibliography`) is not a `Content::Section`.
26
+ 2. **Adapter guarantee**: `GenericAdapter#extract_sections` returns
27
+ only Section instances — non-section children of `<sections>` are
28
+ skipped (the previous behavior pushed zzSTDTitle paragraphs,
29
+ which is now handled by the cover extraction).
30
+ 3. **Specs** cover the validation: a real Section passes; a
31
+ Paragraph raises with structured metadata.
32
+
33
+ ## Done-When
34
+
35
+ - [x] `Content::Document#sections` is guaranteed `Section[]` by
36
+ constructor validation
37
+ - [x] `Content::Document#preface` and `#bibliography` are likewise
38
+ validated as `Section[]`
39
+ - [x] `GenericAdapter#extract_sections` returns `Section[]` only
40
+ - [x] Specs cover: valid sections pass; non-section raises
41
+ ContentError
42
+ - [x] Real OIML pipeline renders (25+ pages) without typing failures
43
+
44
+ ## Verification
45
+
46
+ - `spec/arrolio/content/document_spec.rb` (or similar) covers the
47
+ constructor validation
48
+ - `bundle exec rake` is green
49
+ - OIML r060/1 fixture renders via the generic pipeline
@@ -0,0 +1,47 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [50, 52]
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ There was no documented convention for how third parties should package,
13
+ install, and distribute Arroolio flavors. Without one, "flavors are
14
+ external" is an aspiration, not a deliverable.
15
+
16
+ ## Approach
17
+
18
+ 1. **Wrote `flavors/PACKAGING.md`**: the canonical guide describing
19
+ the anatomy of a flavor gem, the selector/layout_spec/flow_rules
20
+ contracts, the `Flavor::Registry` opt-in registration, the
21
+ `xsl_to_config.rb` workflow, and a verification checklist.
22
+ 2. **Documented the selector contract**: every XML element/attribute
23
+ name the generic adapter touches is named in
24
+ `adapter_rules.yml`'s `selectors:` block (36 keys). A flavor with a
25
+ non-Metanorma vocabulary overrides these.
26
+ 3. **Documented the layout_spec contract**: `header_footer:` and
27
+ `cover_logo:` blocks drive the renderer; `page_templates:`,
28
+ `styles:`, `flows:` drive the engine.
29
+ 4. **Documented the flow_rules contract**: `page_sequences:[]`,
30
+ `cover_content:[]`, `section:`, `content_to_flowable:`, `toc:`.
31
+
32
+ ## Done-When
33
+
34
+ - [x] `flavors/PACKAGING.md` exists with the full packaging contract
35
+ - [x] Selector contract documented (every key explained)
36
+ - [x] Layout_spec contract documented (every block explained)
37
+ - [x] Flow_rules contract documented (every block explained)
38
+ - [x] Registration-via-Registry pattern documented
39
+ - [x] Verification checklist for flavor encapsulation
40
+
41
+ ## Verification
42
+
43
+ - `flavors/PACKAGING.md` is the single source of truth for flavor
44
+ authors
45
+ - The gem itself ships zero flavor artifacts (gemspec excludes
46
+ `flavors/`)
47
+ - The gem's own specs run without any real flavor installed
@@ -0,0 +1,76 @@
1
+ ---
2
+ priority: P2
3
+ impact: med
4
+ depends_on: [50, 51]
5
+ layer: architecture
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `scripts/xsl_to_config.rb` hardcoded constants that were not derivable
13
+ from the XSL but were OIML/Metanorma-specific:
14
+
15
+ - 6× `'oiml.xsl'` literal strings (claimed to be derived but were hardcoded)
16
+ - "Times New Roman Italic", "Times New Roman Bold" inline-style defaults
17
+ - "Jost SemiBold", "Jost", "Jost Light" cover-style defaults
18
+ - "Organisation Internationale de Métrologie Légale" + "International Organization of Legal Metrology" literals
19
+ - Standoc element vocabulary (`clause`, `p`, `fmt-title`, `biblio-tag`, ...)
20
+ - Hardcoded default XSL path pointing to OIML
21
+
22
+ Running the converter against an ISO/IEC/DITA XSL would have produced
23
+ YAML claiming OIML provenance with OIML font defaults — silently wrong.
24
+
25
+ ## Approach
26
+
27
+ 1. **Created `scripts/xsl_profiles/standoc.yml`** — the single file
28
+ where Metanorma standoc vocabulary lives. Carries:
29
+ - `element_mapping`, `inline_styles`, `span_class_styles`
30
+ - `block_level_elements`, `skip_metadata_elements`, `skip_elements`
31
+ - `selectors` (37 keys)
32
+ - `metadata_fields` (XPath mapping)
33
+ - `cover_fields`, `style_overrides`, `cover_content`
34
+ - `header_template`
35
+
36
+ 2. **Refactored `xsl_to_config.rb`** to read everything from the
37
+ profile. The converter has ZERO hardcoded element names, font
38
+ names, or OIML literals.
39
+
40
+ 3. **`generated_from` and `stylesheet`** are now derived from the XSL
41
+ filename via `File.basename(@xsl_path)`. A new `profile:` field
42
+ records which profile was used.
43
+
44
+ 4. **Profile is a constructor argument** with a sensible default
45
+ (`scripts/xsl_profiles/standoc.yml`). A future DITA profile can be
46
+ added without touching the converter.
47
+
48
+ 5. **Removed the OIML-default XSL path** from the CLI. The script now
49
+ requires explicit `<xsl-path> <output-dir> [profile-path]` args.
50
+
51
+ ## Done-When
52
+
53
+ - [x] `scripts/xsl_profiles/standoc.yml` exists with the constants
54
+ - [x] Generator accepts `profile:` argument, defaults to standoc
55
+ - [x] No OIML-specific literals (`oiml`, `Times New Roman`, `Jost`,
56
+ `Organisation Internationale`) in the generator
57
+ - [x] Generated YAML carries `generated_from: <actual-xsl-filename>`
58
+ and `profile: standoc.yml`
59
+ - [x] Regenerating OIML config produces correct output
60
+ - [x] Real OIML fixture still renders 28 pages via the regenerated config
61
+ - [x] All specs pass
62
+
63
+ ## Verification
64
+
65
+ - `grep 'oiml\|Times New Roman\|Jost' scripts/xsl_to_config.rb` → empty
66
+ - `head flavors/oiml/layout_spec.yml` shows
67
+ `generated_from: oiml.xsl`, `profile: standoc.yml`
68
+ - `bundle exec rake` is green
69
+ - `bundle exec ruby exe/arrolio2pdf <fixture.xml> out.pdf flavors/oiml`
70
+ produces a 28-page PDF
71
+
72
+ ## Outcome
73
+
74
+ The converter is now truly generic. Adding a new vocabulary family
75
+ (DITA, DocBook, JATS) means adding `scripts/xsl_profiles/<family>.yml`
76
+ — no converter changes needed.
@@ -0,0 +1,62 @@
1
+ ---
2
+ priority: P2
3
+ impact: med
4
+ depends_on: [50]
5
+ layer: adapter
6
+ status: blocked
7
+ est: 2d
8
+ ---
9
+
10
+ ## Status: blocked
11
+
12
+ Requires:
13
+ 1. A fixture document that uses `<fmt-footnote-container>` /
14
+ `<fmt-fn-body>` with realistic content (the OIML r060/1 fixture
15
+ has none).
16
+ 2. A reference PDF that renders these elements so we can measure
17
+ fidelity.
18
+ 3. Page-bottom footnote-line rendering support in the engine (Engine
19
+ currently has no mechanism for per-page footnotes that collect
20
+ from the body and render at the bottom).
21
+
22
+ The foundation is in place — `Content::Footnote` is already defined
23
+ (autoloaded from `lib/arrolio/content/footnote.rb`), and the skip list
24
+ in `adapter_rules.yml` can drop `fmt-footnote-container`/`fmt-fn-body`
25
+ once the flow builder knows how to render them. But without fixtures
26
+ and an engine-side page-bottom collection mechanism, the implementation
27
+ cannot be validated.
28
+
29
+ ## Problem (when unblocked)
30
+
31
+ The generic pipeline currently loses some information that the legacy
32
+ OIML adapter preserved:
33
+
34
+ - `fmt-footnote-container` / `fmt-fn-body` are in the `skip_elements`
35
+ list, so footnotes are dropped from the body and never rendered.
36
+ - `fmt-xref-label` is skipped, so cross-references to notes/examples
37
+ lose their visual prefix.
38
+ - The `example-body-style` margin-left and `note` list-block indent
39
+ from the XSL are not translated into Arroolio layout (notes/examples
40
+ render as plain paragraphs without the XSL's hanging indent).
41
+
42
+ ## Approach (when unblocked)
43
+
44
+ 1. **Footnote flowable** (`Arroolio::Flowables::FootnoteFlowable`):
45
+ carries a marker + body, renders as superscript marker in the body
46
+ text and a footnote line at the page bottom.
47
+ 2. **Note hanging indent**: the XSL renders notes as `fo:list-block`
48
+ with `provisional-distance-between-starts: 14.5mm`. The
49
+ `GenericFlowBuilder` should emit a `NoteFlowable` (already exists)
50
+ with the configured indent.
51
+ 3. **Example body margin**: `example-body-style` margin-left 12.5mm
52
+ should come from the layout_spec's `example_body` style.
53
+ 4. **Cross-reference label**: `fmt-xref-label` should become a styled
54
+ inline run (caption_label).
55
+
56
+ ## Done-When (when unblocked)
57
+
58
+ - [ ] Footnotes render with superscript markers + page-bottom lines
59
+ - [ ] Notes render with a hanging indent matching the XSL
60
+ - [ ] Examples render with the inner-paragraph left margin
61
+ - [ ] Cross-reference labels appear with the correct style
62
+ - [ ] Diff against the reference PDF improves for these features
@@ -0,0 +1,62 @@
1
+ ---
2
+ priority: P1
3
+ impact: high
4
+ depends_on: [50]
5
+ layer: engine
6
+ status: done
7
+ est: 2d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ The pipeline swallowed many errors silently:
13
+
14
+ - Missing font file → warned and fell back to Helvetica, producing
15
+ visually wrong output for CJK or specialist fonts without telling
16
+ the caller anything was wrong.
17
+ - No `strict:` mode for callers who want to fail loud rather than
18
+ render broken output.
19
+ - `RenderError` was a bare class with no metadata.
20
+
21
+ ## Approach
22
+
23
+ 1. **`RenderError` typed metadata**: now carries `missing_fonts:` —
24
+ an Array of `[name, path]` pairs.
25
+ 2. **`FontMetrics::Registry.register_ttf_all(paths, strict:)`**: when
26
+ `strict: true`, raises `Arroolio::RenderError` listing every
27
+ required font whose path does not exist on disk.
28
+ 3. **`ConfigDrivenPipeline.new(strict: true)`**: forwards to the
29
+ registry, so the render fails at the font-registration step before
30
+ producing any output rather than midway through.
31
+ 4. **`ConfigDrivenPipeline.render(..., strict: true)`**: same at the
32
+ class level.
33
+ 5. **`pipeline.strict` reader** for inspection.
34
+
35
+ ## Done-When
36
+
37
+ - [x] `Arroolio::RenderError` carries structured `missing_fonts:` metadata
38
+ - [x] `FontMetrics::Registry.register_ttf_all` accepts `strict:` kwarg
39
+ - [x] `ConfigDrivenPipeline` accepts `strict:` kwarg and forwards it
40
+ - [x] Default behavior (strict: false) is unchanged — backwards compatible
41
+ - [x] Specs cover: strict mode raises with missing_fonts; non-strict
42
+ mode does not raise; strict reader works
43
+
44
+ ## Verification
45
+
46
+ - `spec/arrolio/strict_mode_spec.rb` (4 specs)
47
+ - `bundle exec rake` is green
48
+ - Real OIML fixture still renders 28 pages with `strict: false`
49
+
50
+ ## Future strict-mode expansions
51
+
52
+ The pattern is established. Future strict checks can be added with
53
+ minimal ceremony:
54
+
55
+ - Missing image: `RenderError(missing_images:)` when an `<image src=>`
56
+ cannot be resolved and `strict:` is true.
57
+ - Unknown style: `LayoutSpecError(unknown_style:)` when a style_id
58
+ referenced by the flow builder isn't in the layout_spec's registry.
59
+ - Adapter selector gap: `AdapterError(missing_selector:)` when the
60
+ adapter needs a selector that the flavor didn't declare.
61
+
62
+ These are individually TODO-worthy but follow the same template.
@@ -0,0 +1,66 @@
1
+ ---
2
+ priority: P2
3
+ impact: med
4
+ depends_on: [50, 61]
5
+ layer: render
6
+ status: done
7
+ est: 1d
8
+ ---
9
+
10
+ ## Problem
11
+
12
+ `layout_spec.yml` used absolute user-local paths for fonts
13
+ (`/Users/mulgogi/.fontist/fonts/Times.ttf`). This works on the
14
+ developer's machine but breaks for any other consumer. The generated
15
+ OIML `layout_spec.yml` baked in my home directory.
16
+
17
+ ## Approach
18
+
19
+ Implemented `Arroolio::Font::Resolver` — a portable font-resolution
20
+ abstraction with a fallback chain:
21
+
22
+ 1. **`FontManifest`** (from `layout_spec.yml`'s `font_manifest:` block):
23
+ explicit family → variant path mapping declared by the flavor.
24
+ 2. **`font_paths`** (legacy explicit paths in `layout_spec.yml`):
25
+ preserved for backward compatibility.
26
+ 3. **fontist** (optional runtime dependency): if the `fontist` gem is
27
+ available at runtime, families are looked up via fontist's
28
+ manifest. Skipped silently if fontist is not installed.
29
+ 4. **PDF standard 14 fallback**: Helvetica, Times-Roman, Courier, etc.
30
+ resolve to themselves (built-in PDF fonts).
31
+
32
+ Strict mode (from TODO 61) raises `RenderError` listing every
33
+ unresolved required font. The resolver never hard-fails when fontist
34
+ is absent — it just falls through to the next strategy.
35
+
36
+ `LayoutSpec` now carries a `font_manifest_config` Hash loaded from
37
+ `layout_spec.yml`'s `font_manifest:` block, alongside the existing
38
+ `header_footer_config` and `cover_logo_config`.
39
+
40
+ ## Done-When
41
+
42
+ - [x] `Arroolio::Font::Resolver` class exists, autoloaded
43
+ - [x] `Arroolio::Font::FontManifest` class exists, autoloaded
44
+ - [x] `LayoutSpec#font_manifest_config` accessor exists
45
+ - [x] `Loader` reads `font_manifest:` block from YAML
46
+ - [x] Resolution chain: manifest → font_paths → fontist → standard 14
47
+ - [x] Strict mode raises `RenderError` with `missing_fonts:` metadata
48
+ - [x] Specs cover: each resolution strategy, strict mode, standard 14,
49
+ manifest fallback paths, missing files
50
+
51
+ ## Verification
52
+
53
+ - `spec/arrolio/font/resolver_spec.rb` (16 specs)
54
+ - `bundle exec rake` is green (283 specs)
55
+ - The resolver does NOT require fontist at install time — it's a
56
+ runtime-optional integration via `Object.const_defined?(:Fontist)`
57
+
58
+ ## Outcome
59
+
60
+ Flavors can now declare fonts by family name with optional explicit
61
+ paths. When a flavor gem is packaged, the `font_manifest:` block can
62
+ list family names; at render time the resolver picks whichever path
63
+ the local environment provides (manifest, font_paths, or fontist).
64
+
65
+ Adding a real fontist integration later is a one-line change in
66
+ `fontist_lookup` — the abstraction is in place.