arrolio 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -2
  3. data/Rakefile +15 -0
  4. data/TODO.complete/56-generic-adapter-mece-decomposition.md +20 -0
  5. data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +92 -55
  6. data/TODO.complete/70-ttf-font-metrics-parity.md +68 -0
  7. data/TODO.complete/71-cover-layout-parity.md +45 -0
  8. data/TODO.complete/72-table-layout-parity.md +49 -0
  9. data/TODO.complete/73-toc-leaders-parity.md +55 -0
  10. data/TODO.complete/74-svg-image-rendering-parity.md +76 -0
  11. data/TODO.complete/75-mathml-formula-rendering-parity.md +52 -0
  12. data/TODO.complete/76-odd-even-page-templates-parity.md +50 -0
  13. data/TODO.complete/77-text-formatting-parity.md +55 -0
  14. data/TODO.complete/78-bibliography-formatting-parity.md +62 -0
  15. data/TODO.complete/79-line-breaking-parity.md +80 -0
  16. data/TODO.complete/80-parity-diff-harness-in-ci.md +68 -0
  17. data/TODO.complete/81-conditional-style-refinements.md +70 -0
  18. data/TODO.complete/82-semantic-presentation-dedup.md +65 -0
  19. data/TODO.complete/83-nested-list-support.md +74 -0
  20. data/TODO.complete/84-figure-svg-rendering.md +70 -0
  21. data/TODO.complete/85-locality-reference-formatting.md +48 -0
  22. data/TODO.complete/86-list-item-text-overflow.md +80 -0
  23. data/TODO.complete/87-odd-even-page-templates.md +55 -0
  24. data/TODO.complete/88-hyphenation-support.md +52 -0
  25. data/TODO.complete/89-table-rowspan.md +49 -0
  26. data/TODO.complete/90-mathml-stacked-layout.md +65 -0
  27. data/TODO.complete/91-parity-diff-diagnostics.md +50 -0
  28. data/TODO.complete/92-table-cell-height-parity.md +66 -0
  29. data/TODO.complete/93-figure-text-searchability.md +49 -0
  30. data/TODO.complete/94-fo-space-resolution.md +63 -0
  31. data/TODO.complete/95-title-header-placement.md +55 -0
  32. data/TODO.complete/96-pagination-drift-parity.md +517 -0
  33. data/TODO.complete/README.md +58 -38
  34. data/lib/arrolio/config_driven_pipeline.rb +45 -1
  35. data/lib/arrolio/content/document.rb +4 -3
  36. data/lib/arrolio/content/section.rb +3 -0
  37. data/lib/arrolio/content/table.rb +25 -14
  38. data/lib/arrolio/embedded_block_extractor.rb +54 -0
  39. data/lib/arrolio/engine/paged.rb +109 -28
  40. data/lib/arrolio/flavor/manifest.rb +0 -1
  41. data/lib/arrolio/flavor/registry.rb +1 -1
  42. data/lib/arrolio/flowable.rb +21 -0
  43. data/lib/arrolio/flowables/figure_flowable.rb +17 -0
  44. data/lib/arrolio/flowables/footnote_marker_flowable.rb +32 -13
  45. data/lib/arrolio/flowables/group_flowable.rb +57 -0
  46. data/lib/arrolio/flowables/list_flowable.rb +46 -18
  47. data/lib/arrolio/flowables/note_flowable.rb +84 -9
  48. data/lib/arrolio/flowables/page_sequence_start.rb +8 -4
  49. data/lib/arrolio/flowables/positioned_block.rb +55 -0
  50. data/lib/arrolio/flowables/rotated_text.rb +51 -0
  51. data/lib/arrolio/flowables/table_flowable.rb +303 -71
  52. data/lib/arrolio/flowables/text_flowable.rb +67 -11
  53. data/lib/arrolio/flowables/toc_line_flowable.rb +70 -7
  54. data/lib/arrolio/flowables.rb +4 -0
  55. data/lib/arrolio/font/embedder.rb +22 -3
  56. data/lib/arrolio/font_scanner.rb +133 -0
  57. data/lib/arrolio/generic_adapter/document_extraction.rb +101 -0
  58. data/lib/arrolio/generic_adapter/footnote_extraction.rb +81 -0
  59. data/lib/arrolio/generic_adapter/heading_extraction.rb +92 -0
  60. data/lib/arrolio/generic_adapter/inline_run_collection.rb +136 -0
  61. data/lib/arrolio/generic_adapter/list_conversion.rb +70 -0
  62. data/lib/arrolio/generic_adapter/metadata_extraction.rb +46 -0
  63. data/lib/arrolio/generic_adapter/table_conversion.rb +105 -0
  64. data/lib/arrolio/generic_adapter.rb +263 -317
  65. data/lib/arrolio/generic_flow_builder/bibliography.rb +47 -0
  66. data/lib/arrolio/generic_flow_builder/figures.rb +90 -0
  67. data/lib/arrolio/generic_flow_builder/lists.rb +66 -0
  68. data/lib/arrolio/generic_flow_builder/notes.rb +61 -0
  69. data/lib/arrolio/generic_flow_builder/sequences.rb +102 -0
  70. data/lib/arrolio/generic_flow_builder/tables.rb +28 -0
  71. data/lib/arrolio/generic_flow_builder/terms.rb +89 -0
  72. data/lib/arrolio/generic_flow_builder.rb +92 -187
  73. data/lib/arrolio/glyph_measurer.rb +16 -5
  74. data/lib/arrolio/inline_run.rb +2 -1
  75. data/lib/arrolio/layout_spec/page_template.rb +14 -8
  76. data/lib/arrolio/math_ml/inline_run_extractor.rb +272 -0
  77. data/lib/arrolio/math_ml.rb +7 -0
  78. data/lib/arrolio/output/page.rb +5 -4
  79. data/lib/arrolio/output/placed_box.rb +5 -2
  80. data/lib/arrolio/renderer/pdf/assets.rb +94 -0
  81. data/lib/arrolio/renderer/pdf/font_embedding.rb +77 -0
  82. data/lib/arrolio/renderer/pdf/metadata.rb +52 -0
  83. data/lib/arrolio/renderer/pdf.rb +140 -215
  84. data/lib/arrolio/style/definition.rb +7 -3
  85. data/lib/arrolio/style/registry.rb +6 -1
  86. data/lib/arrolio/table/auto_layout.rb +19 -10
  87. data/lib/arrolio/table/grid.rb +105 -0
  88. data/lib/arrolio/table.rb +1 -0
  89. data/lib/arrolio/text_layout/greedy.rb +15 -1
  90. data/lib/arrolio/text_layout/knuth_plass/breaker.rb +216 -92
  91. data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +31 -17
  92. data/lib/arrolio/text_layout/knuth_plass.rb +3 -0
  93. data/lib/arrolio/text_layout/line.rb +10 -2
  94. data/lib/arrolio/version.rb +1 -1
  95. data/lib/arrolio.rb +8 -4
  96. data/scripts/parity_check.rb +52 -0
  97. data/scripts/parity_diff.rb +89 -0
  98. data/scripts/xsl_profiles/standoc.yml +8 -2
  99. data/scripts/xsl_to_config.rb +28 -1
  100. metadata +91 -5
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ class GenericAdapter
5
+ # Extracted conversion seam (TODO 56): one module per concern,
6
+ # included into the adapter. The class keeps dispatch and glue.
7
+ module MetadataExtraction
8
+ def extract_metadata(root)
9
+ config = @rules['metadata'] || {}
10
+ bibdata_path = config['root'] || 'bibdata'
11
+ bibdata = find_first(root, bibdata_path)
12
+ return {} unless bibdata
13
+
14
+ result = {}
15
+ (config['fields'] || {}).each do |key, xpath|
16
+ value = text_of(find_first(bibdata, xpath))
17
+ result[key.to_sym] = value if value && !value.empty?
18
+ end
19
+ derive_metadata_fields(result)
20
+ result
21
+ end
22
+
23
+ def derive_metadata_fields(result)
24
+ lang = result[:language].to_s
25
+ version_date = result[:revision_date]
26
+ year = version_date&.[](0, 4)
27
+ result[:revision_year] = year if year&.match?(/\A\d{4}\z/)
28
+ return if lang.empty? && version_date.nil?
29
+
30
+ lang_initial = lang[0, 1].upcase
31
+ year_part = year&.match?(/\A\d{4}\z/) ? "#{year} " : ''
32
+ result[:edition_label] = "#{year_part}(#{lang_initial})"
33
+ end
34
+
35
+ def extract_cover(root)
36
+ metadata = extract_metadata(root)
37
+ config = @rules['cover'] || {}
38
+ result = {}
39
+ (config['fields'] || ['docidentifier', 'edition', 'title_main', 'title_part']).each do |field|
40
+ result[field.to_sym] = metadata[field.to_sym] if metadata[field.to_sym]
41
+ end
42
+ result.empty? ? nil : result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ class GenericAdapter
5
+ # Table conversion, split from the adapter body (TODO 56 MECE
6
+ # split). Converts <table> to Content::Table: rows and cells
7
+ # with colspan/rowspan/alignment, captions, and cell footnotes
8
+ # lifted to the table.
9
+ module TableConversion
10
+ def convert_table(elem)
11
+ header, header_footnotes = convert_table_rows(find_first(elem, selector('table_header')), true)
12
+ body, body_footnotes = convert_table_rows(find_first(elem, selector('table_body')), false)
13
+ caption = extract_table_caption(elem)
14
+ Content::Table.new(header: header, body: body, style_id: :table,
15
+ id: elem.attribute(selector('id_attribute'))&.value,
16
+ caption: caption,
17
+ footnotes: header_footnotes + body_footnotes)
18
+ end
19
+
20
+ # The caption carries inline content (math subscripts, xrefs) —
21
+ # flattening to text serializes stems to their raw asciimath
22
+ # form ("n_(\"LC\")"). Collect runs like figure captions do.
23
+ def extract_table_caption(elem)
24
+ name = find_first(elem, selector('figure_caption')) ||
25
+ find_first(elem, selector('figure_caption_fallback'))
26
+ return nil unless name
27
+
28
+ runs = collect_inline_runs(name)
29
+ runs.empty? ? nil : Content::Paragraph.new(runs, style_id: :figure_caption)
30
+ end
31
+
32
+ def convert_table_rows(parent, is_header)
33
+ return [[], []] unless parent
34
+
35
+ rows = []
36
+ footnotes = []
37
+ row_name = selector('table_row')
38
+ cell_names = Array(selector('table_cell'))
39
+ each_child(parent, row_name) do |tr|
40
+ cells = []
41
+ each_element(tr) do |cell|
42
+ next unless cell_names.include?(cell.name)
43
+
44
+ runs, cell_footnotes = cell_runs_with_footnotes(cell)
45
+ footnotes.concat(cell_footnotes)
46
+ cells << Content::Table::Cell.new(
47
+ [Content::Paragraph.new(runs, style_id: is_header ? :table_header_cell : :table_cell)],
48
+ colspan: (cell.attribute('colspan')&.value || 1).to_i,
49
+ rowspan: (cell.attribute('rowspan')&.value || 1).to_i,
50
+ style_id: is_header ? :table_header_cell : :table_cell,
51
+ align: cell.attribute('align')&.value,
52
+ valign: cell.attribute('valign')&.value
53
+ )
54
+ end
55
+ rows << Content::Table::Row.new(cells)
56
+ end
57
+ [rows, footnotes]
58
+ end
59
+
60
+ # Footnotes referenced from table cells keep only their marker
61
+ # (a superscript run) in the cell; their body is collected onto
62
+ # the table and rendered below it — the FOP table-footnote
63
+ # convention. Inline collection skips the marker element's
64
+ # subtree so the body text never leaks into the cell.
65
+ def cell_runs_with_footnotes(cell)
66
+ fn_name = selector('footnote_marker')
67
+ runs = collect_inline_runs(cell, exclude: fn_name)
68
+ return [runs, []] unless fn_name
69
+
70
+ footnotes = []
71
+ cell.each_recursive do |node|
72
+ next unless node.is_a?(REXML::Element) && node.name == fn_name
73
+
74
+ marker = node.attribute('reference')&.value ||
75
+ node.attribute('id')&.value || ''
76
+ unless marker.empty?
77
+ runs << Content::InlineRun.new(
78
+ marker, style_id: :inline,
79
+ baseline_shift: Content::InlineRun::BASELINE_SUP,
80
+ font_size_scale: 0.7
81
+ )
82
+ end
83
+ footnotes << table_footnote(node, marker)
84
+ end
85
+ [runs, footnotes]
86
+ end
87
+
88
+ def table_footnote(fn_elem, marker)
89
+ para_name = selector('paragraph')
90
+ body = []
91
+ each_element(fn_elem) do |child|
92
+ next unless child.name == para_name
93
+
94
+ body << convert_paragraph(child)
95
+ end
96
+ Content::Footnote.new(
97
+ marker: marker,
98
+ body: body,
99
+ id: fn_elem.attribute(selector('id_attribute'))&.value
100
+ )
101
+ end
102
+
103
+ end
104
+ end
105
+ end