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,391 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'nokogiri'
5
+ require 'yaml'
6
+
7
+ module XslConfigGenerator
8
+ XSL_NS = 'http://www.w3.org/1999/XSL/Transform'
9
+ FO_NS = 'http://www.w3.org/1999/XSL/Format'
10
+ DEFAULT_PROFILE = File.expand_path('xsl_profiles/standoc.yml', __dir__).freeze
11
+
12
+ def self.generate(xsl_path:, output_dir:, profile: DEFAULT_PROFILE)
13
+ Builder.new(xsl_path, output_dir, profile).generate
14
+ end
15
+
16
+ class Builder
17
+ def initialize(xsl_path, output_dir, profile_path)
18
+ @xsl_path = File.expand_path(xsl_path)
19
+ @output_dir = File.expand_path(output_dir)
20
+ @profile_path = profile_path
21
+ @stylesheet = File.basename(@xsl_path)
22
+ @profile = YAML.safe_load_file(profile_path, aliases: true) || {}
23
+ @document = Nokogiri::XML(File.read(@xsl_path)) { |config| config.nonet }
24
+ @attribute_sets = extract_attribute_sets
25
+ @variables = extract_variables
26
+ @templates = extract_templates
27
+ end
28
+
29
+ def generate
30
+ File.write(File.join(@output_dir, 'layout_spec.yml'), yaml_file(layout_spec))
31
+ File.write(File.join(@output_dir, 'adapter_rules.yml'), yaml_file(adapter_rules))
32
+ File.write(File.join(@output_dir, 'flow_rules.yml'), yaml_file(flow_rules))
33
+ profile_name = File.basename(@profile_path)
34
+ warn "generated XSL configuration from #{@stylesheet} (profile: #{profile_name}) in #{@output_dir}"
35
+ end
36
+
37
+ private
38
+
39
+ def stylesheet_name
40
+ @stylesheet
41
+ end
42
+
43
+ def layout_spec
44
+ {
45
+ 'generated_from' => stylesheet_name,
46
+ 'profile' => File.basename(@profile_path),
47
+ 'stylesheet' => stylesheet_name,
48
+ 'xsl_variables' => @variables,
49
+ 'xsl_attribute_sets' => @attribute_sets,
50
+ 'default_page_template' => 'body',
51
+ 'page_templates' => {
52
+ 'body' => {
53
+ 'page_size' => 'A4',
54
+ 'margins' => {
55
+ 'top' => length(@variables['marginTop'], '26.5mm'),
56
+ 'bottom' => length(@variables['marginBottom'], '26.5mm'),
57
+ 'left' => length(@variables['marginLeftRight1'], '25.5mm'),
58
+ 'right' => length(@variables['marginLeftRight1'], '25.5mm')
59
+ },
60
+ 'region_extents' => { 'before' => '18mm', 'after' => '18mm' }
61
+ }
62
+ },
63
+ 'styles' => styles,
64
+ 'flows' => {
65
+ 'main' => { 'region' => 'body', 'default_style_id' => 'body' }
66
+ }
67
+ }
68
+ end
69
+
70
+ def styles
71
+ text_styles.merge(inline_styles_arg)
72
+ .merge(list_styles)
73
+ .merge(table_styles)
74
+ .merge(structure_styles)
75
+ end
76
+
77
+ def text_styles
78
+ o = @profile['style_overrides'] || {}
79
+ {
80
+ 'body' => style_from('root-style', 'clause_style').merge('margin_bottom' => o['body_margin_bottom'] || 8),
81
+ **heading_styles,
82
+ 'note' => style_from('note-style').merge('parent' => 'body'),
83
+ 'termnote' => style_from('termnote-style').merge('parent' => 'body'),
84
+ 'note_label' => style_from('note-name-style').merge(
85
+ 'parent' => 'body', 'font_name' => o['em_font_name']
86
+ ),
87
+ 'example' => style_from('example-style').merge('parent' => 'body'),
88
+ 'caption_label' => { 'parent' => 'body' },
89
+ 'example_label' => { 'parent' => 'note_label' }
90
+ }
91
+ end
92
+
93
+ def inline_styles_arg
94
+ o = @profile['style_overrides'] || {}
95
+ {
96
+ 'inline' => { 'parent' => 'body' },
97
+ 'strong' => { 'parent' => 'body', 'font_name' => o['strong_font_name'] },
98
+ 'em' => { 'parent' => 'body', 'font_name' => o['em_font_name'] },
99
+ 'monospace' => { 'parent' => 'body', 'font_name' => 'Courier' },
100
+ 'preformatted' => {
101
+ 'parent' => 'body', 'font_name' => 'Courier', 'font_size' => 9,
102
+ 'align' => 'left', 'line_spacing' => 1.0, 'margin_bottom' => 6
103
+ },
104
+ 'identifier' => { 'parent' => 'body', 'font_name' => o['identifier_font_name'] },
105
+ 'term' => style_from('term-style').merge('parent' => 'body', 'font_name' => o['term_font_name']),
106
+ 'xref' => style_from('xref-style').merge('parent' => 'body'),
107
+ 'link' => style_from('link-style').merge('parent' => 'body', 'font_name' => o['link_font_name']),
108
+ 'bibitem' => { 'parent' => 'body', 'margin_top' => 4, 'margin_bottom' => 4 },
109
+ 'footnote' => { 'parent' => 'body', 'font_size' => 9, 'margin_top' => 2, 'margin_bottom' => 2 }
110
+ }
111
+ end
112
+
113
+ def table_styles
114
+ o = @profile['style_overrides'] || {}
115
+ {
116
+ 'table' => { 'parent' => 'body', 'font_size' => 10 },
117
+ 'table_cell' => { 'parent' => 'table', 'margin_top' => 2, 'margin_bottom' => 2 },
118
+ 'table_header_cell' => { 'parent' => 'table_cell', 'font_name' => o['table_header_cell_font_name'] },
119
+ 'figure_caption' => style_from('figure-name-style').merge(
120
+ 'parent' => 'body', 'font_size' => 10, 'align' => 'center'
121
+ )
122
+ }
123
+ end
124
+
125
+ def structure_styles
126
+ toc_styles.merge(cover_styles).merge(section_styles)
127
+ end
128
+
129
+ def toc_styles
130
+ {
131
+ 'toc_title' => style_from('toc-title-style').merge('parent' => 'body'),
132
+ 'toc_entry' => { 'parent' => 'body', 'font_size' => 11 },
133
+ 'toc_entry_sub' => { 'parent' => 'toc_entry', 'margin_left' => 12 }
134
+ }
135
+ end
136
+
137
+ def cover_styles
138
+ o = @profile['style_overrides'] || {}
139
+ {
140
+ 'cover_docidentifier' => {
141
+ 'parent' => 'body', 'font_name' => o['cover_docidentifier_font_name'], 'font_size' => 28,
142
+ 'align' => 'center', 'fill_color' => '#221E1F'
143
+ },
144
+ 'cover_title' => {
145
+ 'parent' => 'body', 'font_name' => o['cover_title_font_name'], 'font_size' => 16, 'align' => 'center'
146
+ },
147
+ 'cover_title_part' => { 'parent' => 'cover_title', 'font_size' => 14 },
148
+ 'cover_title_other' => {
149
+ 'parent' => 'body', 'font_name' => o['cover_title_other_font_name'], 'font_size' => 11, 'align' => 'center'
150
+ },
151
+ 'cover_org' => { 'parent' => 'body', 'font_name' => o['cover_org_font_name'], 'font_size' => 13,
152
+ 'align' => 'right' },
153
+ 'cover_label' => {
154
+ 'parent' => 'body', 'font_name' => o['cover_label_font_name'], 'font_size' => 13,
155
+ 'align' => 'right', 'fill_color' => '#221E1F'
156
+ },
157
+ 'doc_title' => {
158
+ 'parent' => 'body', 'font_name' => o['strong_font_name'], 'font_size' => 18,
159
+ 'align' => 'center', 'margin_bottom' => 36
160
+ }
161
+ }
162
+ end
163
+
164
+ def heading_styles
165
+ (1..6).to_h do |level|
166
+ attrs = style_from('title-style', 'title-style', level: level)
167
+ attrs['parent'] = 'body'
168
+ attrs['font_name'] = (@profile['style_overrides'] || {})['strong_font_name']
169
+ attrs['keep_together'] = true
170
+ attrs['align'] = 'left' if level == 1
171
+ ["heading_#{level}", attrs]
172
+ end
173
+ end
174
+
175
+ def list_styles
176
+ o = @profile['style_overrides'] || {}
177
+ attrs = style_from(nil, 'list-style').merge('parent' => 'body')
178
+ {
179
+ 'list_bullet' => attrs,
180
+ 'list_ordered' => attrs,
181
+ 'list_marker' => { 'parent' => 'body', 'font_name' => o['list_marker_font_name'] }
182
+ }
183
+ end
184
+
185
+ def section_styles
186
+ (1..6).to_h do |level|
187
+ ["section_body_#{level}", { 'parent' => 'body', 'margin_left' => (level - 1) * 12 }]
188
+ end
189
+ end
190
+
191
+ def adapter_rules
192
+ {
193
+ 'generated_from' => stylesheet_name,
194
+ 'profile' => File.basename(@profile_path),
195
+ 'stylesheet' => stylesheet_name,
196
+ 'metadata' => {
197
+ 'root' => 'bibdata',
198
+ 'fields' => @profile['metadata_fields'] || {}
199
+ },
200
+ 'cover' => { 'fields' => @profile['cover_fields'] || [] },
201
+ 'sections' => { 'container' => 'sections' },
202
+ 'element_mapping' => @profile['element_mapping'] || {},
203
+ 'paragraph_styles' => @profile['paragraph_styles'] || {},
204
+ 'inline_styles' => @profile['inline_styles'] || {},
205
+ 'span_class_styles' => @profile['span_class_styles'] || {},
206
+ 'block_level_elements' => @profile['block_level_elements'] || [],
207
+ 'skip_metadata_elements' => @profile['skip_metadata_elements'] || [],
208
+ 'skip_elements' => @profile['skip_elements'] || [],
209
+ 'tab_replacements' => @profile['tab_replacements'] || {},
210
+ 'heading' => { 'source' => (@profile['selectors'] || {})['heading'] || 'fmt-title' },
211
+ 'selectors' => @profile['selectors'] || {},
212
+ 'xsl_templates' => @templates.select { |template| template['match'] }
213
+ }
214
+ end
215
+
216
+ def flow_rules
217
+ {
218
+ 'generated_from' => stylesheet_name,
219
+ 'profile' => File.basename(@profile_path),
220
+ 'stylesheet' => stylesheet_name,
221
+ 'xsl_variables' => @variables,
222
+ 'xsl_templates' => @templates.select { |template| template['name'] },
223
+ 'page_sequences' => page_sequences,
224
+ 'cover_content' => @profile['cover_content'] || [],
225
+ 'section' => {
226
+ 'heading_style' => '{{title_style_id}}',
227
+ 'body_style' => '{{style_id}}',
228
+ 'insert_page_break_before' => true,
229
+ 'insert_page_break_after_section' => true
230
+ },
231
+ 'content_to_flowable' => {
232
+ 'Paragraph' => 'text_paragraph', 'Table' => 'table', 'List' => 'list',
233
+ 'Image' => 'image', 'Preformatted' => 'preformatted', 'PageBreak' => 'page_break'
234
+ },
235
+ 'style_resolution' => {
236
+ 'paragraph' => 'resolve_from_style_id',
237
+ 'heading' => 'resolve_from_title_style_id',
238
+ 'table_cell' => 'resolve_from_table_cell_style',
239
+ 'list_item' => 'resolve_from_list_style'
240
+ },
241
+ 'toc' => { 'styles' => { 'entry' => 'toc_entry', 'sub' => 'toc_entry_sub' } }
242
+ }
243
+ end
244
+
245
+ def page_sequences
246
+ names = @templates.filter_map { |template| template['name'] }
247
+ sequences = [{ 'role' => 'cover', 'header' => nil, 'footer' => nil, 'build_content' => 'cover_content' }]
248
+ if names.include?('back-page')
249
+ sequences << { 'role' => 'back_of_cover', 'header' => nil, 'footer' => nil, 'page_break_after' => true }
250
+ end
251
+ if names.any? { |name| name.include?('preface') }
252
+ sequences << { 'role' => 'preface', 'header' => header_template, 'footer' => '%d', 'header_align' => 'right' }
253
+ end
254
+ sequences << { 'role' => 'body', 'header' => header_template, 'footer' => '%d', 'header_align' => 'right' }
255
+ if names.include?('insertMainSectionsPageSequences')
256
+ sequences << { 'role' => 'bibliography', 'header' => header_template, 'footer' => '%d',
257
+ 'header_align' => 'right' }
258
+ end
259
+ sequences
260
+ end
261
+
262
+ def header_template
263
+ return nil unless @templates.any? { |template| template['name'] == 'insertHeaderEven' }
264
+
265
+ @profile['header_template']
266
+ end
267
+
268
+ def extract_attribute_sets
269
+ @document.xpath('//xsl:attribute-set', 'xsl' => XSL_NS).to_h do |node|
270
+ attrs = node.xpath('./xsl:attribute', 'xsl' => XSL_NS).to_h do |attribute|
271
+ [attribute['name'], attribute.text.strip]
272
+ end
273
+ [node['name'], attrs]
274
+ end
275
+ end
276
+
277
+ def extract_variables
278
+ @document.xpath('//xsl:variable', 'xsl' => XSL_NS).to_h do |node|
279
+ [node['name'], node.text.strip]
280
+ end.reject { |_name, value| value.empty? } # rubocop:disable Style/MultilineBlockChain -- idiomatic Hash.to_h.reject
281
+ end
282
+
283
+ def extract_templates
284
+ @document.xpath('//xsl:template', 'xsl' => XSL_NS).map do |node|
285
+ attributes = node.xpath('.//xsl:attribute', 'xsl' => XSL_NS).map do |attribute|
286
+ condition = attribute.at_xpath('ancestor::xsl:if', 'xsl' => XSL_NS)
287
+ {
288
+ 'name' => attribute['name'],
289
+ 'value' => attribute.text.strip,
290
+ 'test' => condition&.[]('test')
291
+ }.compact
292
+ end
293
+ {
294
+ 'name' => node['name'],
295
+ 'match' => node['match'],
296
+ 'mode' => node['mode'],
297
+ 'priority' => node['priority'],
298
+ 'use_attribute_sets' => node['use-attribute-sets'],
299
+ 'attributes' => attributes,
300
+ 'output_elements' => node.css('*').select do |child|
301
+ child.namespace && child.namespace.href == FO_NS
302
+ end.map(&:name).uniq
303
+ }.compact
304
+ end
305
+ end
306
+
307
+ def style_from(attribute_set_name, refine_name = nil, level: nil)
308
+ attrs = @attribute_sets.fetch(attribute_set_name, {}).dup
309
+ attrs.merge!(refine_attributes(refine_name, level)) if refine_name
310
+ attrs.each_with_object({}) do |(name, value), result|
311
+ key = style_key(name)
312
+ next unless key
313
+
314
+ result[key] = style_value(key, value)
315
+ end
316
+ end
317
+
318
+ def refine_attributes(name, level)
319
+ template = @templates.find { |item| item['name'] == "refine_#{name}" }
320
+ return {} unless template
321
+
322
+ attributes = {}
323
+ template.fetch('attributes', []).each do |attribute|
324
+ next unless condition_matches_level?(attribute['test'], level)
325
+
326
+ attributes[attribute['name']] = attribute['value']
327
+ end
328
+ attributes
329
+ end
330
+
331
+ def style_key(name)
332
+ {
333
+ 'font-family' => 'font_name', 'font-size' => 'font_size',
334
+ 'font-weight' => 'font_weight', 'font-style' => 'font_style',
335
+ 'color' => 'fill_color', 'line-height' => 'line_spacing',
336
+ 'text-align' => 'align', 'margin-left' => 'margin_left',
337
+ 'margin-right' => 'margin_right', 'margin-top' => 'margin_top',
338
+ 'margin-bottom' => 'margin_bottom', 'space-before' => 'margin_top',
339
+ 'space-after' => 'margin_bottom', 'text-indent' => 'text_indent'
340
+ }[name]
341
+ end
342
+
343
+ def style_value(key, value)
344
+ return primary_font(value) if key == 'font_name'
345
+ return value.to_f if key == 'font_size' && value.to_s.match?(/[\d.]+pt\z/)
346
+ return value.to_f if key == 'line_spacing' && value.to_s.match?(/\A[\d.]+\z/)
347
+ return value.to_sym.to_s if key == 'align'
348
+
349
+ value
350
+ end
351
+
352
+ def primary_font(value)
353
+ value.to_s.split(',').first.strip.gsub(/["']/, '')
354
+ end
355
+
356
+ def condition_matches_level?(test, level)
357
+ return true unless test
358
+ return false unless level
359
+
360
+ match = test.match(/\$level\s*(=|>=)\s*(\d+)/)
361
+ return false unless match
362
+
363
+ match[1] == '=' ? level == match[2].to_i : level >= match[2].to_i
364
+ end
365
+
366
+ def length(value, fallback)
367
+ value && !value.empty? ? "#{value}mm" : fallback
368
+ end
369
+
370
+ def yaml_file(value)
371
+ "# Generated from #{stylesheet_name} by scripts/xsl_to_config.rb.\n" \
372
+ "# DO NOT EDIT BY HAND — edit the XSL and rerun the generator.\n\n" \
373
+ "#{YAML.dump(value).delete_prefix("---\n")}"
374
+ end
375
+ end
376
+ end
377
+
378
+ xsl_path = ARGV[0]
379
+ output_dir = ARGV[1]
380
+ profile_arg = ARGV[2] && File.expand_path(ARGV[2])
381
+
382
+ unless xsl_path && output_dir
383
+ warn 'usage: xsl_to_config.rb <xsl-path> <output-dir> [profile-path]'
384
+ exit 2
385
+ end
386
+
387
+ XslConfigGenerator.generate(
388
+ xsl_path: xsl_path,
389
+ output_dir: output_dir,
390
+ profile: profile_arg || XslConfigGenerator::DEFAULT_PROFILE
391
+ )