arrolio 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edfcfe9ff191c034c3702c111c0ac2a023d4290966f594be72b018940998584a
4
- data.tar.gz: a724d7703f6893aea53756e0141bd43ab416809381c75ba61b434e57f07f47cf
3
+ metadata.gz: 6d2fb34e355fe255920ca92e339c7c2529db69b9668130d2f5c9f36319f2aa13
4
+ data.tar.gz: a591ae1001ae525c96d19a7321eee6840a105e3d9e6404a5b2a2454e68bd7b46
5
5
  SHA512:
6
- metadata.gz: f2214e2fc97366287645096f3b2969caa1c879204039bc5cce2018171739e8a95a3c8753127c167faa377b8e0c8a5894687ad2ff99f5f289c0c97fd0d2af44bd
7
- data.tar.gz: ddedc8d9ddc9449a582a781d8741b829f3d90268d6a1b55eea430c54e58781ce586996f3b1a041b26aab0ce25c2654092a542e8ead2aa6a080f933d7d8c5db12
6
+ metadata.gz: 3587a3bf6d2bebc2d66b4f990bccc365f8508a08ee75d191636c737a5d4c6eb31ff1cd8069175e010a40a38f17d2b98a8c788da3bb164dbb182aa632eb85fd99
7
+ data.tar.gz: deed9c49dc8e31bc26854c8dcb92b4f46214318da53a067786bdc1ac86329e6403d53bce132826a314b7e6876d28c51bd45ce6089ad8eb1d07eb61d0fb92e0b2
data/CLAUDE.md CHANGED
@@ -53,16 +53,17 @@ PDF bytes (via Pdfrb)
53
53
 
54
54
  Each layer is independently testable. Keep layer boundaries airtight: Content knows nothing about pages or PDF; Engine knows nothing about PDF bytes; Renderer knows nothing about layout algorithms.
55
55
 
56
- ## Project state (as of 2026-08-03)
56
+ ## Project state (as of 2026-09-01)
57
57
 
58
- Generic config-driven pipeline works end-to-end: the OIML r060/1 fixture renders to a 25-page A4 PDF (~194 KB) via `Arroolio::ConfigDrivenPipeline` pointed at `flavors/oiml/`. 232 specs pass.
58
+ The generic config-driven pipeline renders the OIML r060/1 fixture to a 28-page A4 PDF matching the mn2pdf v2.55 reference at **68.92% text parity, 28/28 pages** (`bundle exec rake parity:check`). 452 specs, 0 rubocop offenses, 214 files.
59
59
 
60
60
  What's in the gem (core, flavor-agnostic):
61
61
  - `lib/arrolio/{content,style,layout_spec,font,font_metrics,glyph_measurer,inline_run,text_layout,flowable,flowables,frame,flow_context,engine,output,renderer,harness,flavor}/`
62
- - `lib/arrolio/generic_adapter.rb` — reads `adapter_rules.yml`, parses any flavor's XML into `Content::Document`.
63
- - `lib/arrolio/generic_flow_builder.rb` — reads `flow_rules.yml`, builds flowables for any flavor.
64
- - `lib/arrolio/config_driven_pipeline.rb` — orchestrates adapter + flow builder + engine + renderer + ToC.
65
- - `lib/arrolio/asset_resolver.rb`, `lib/arrolio/toc_builder.rb`generic helpers.
62
+ - `lib/arroolio/generic_adapter.rb` — dispatch + document glue; per-concern conversion lives in included seams under `lib/arroolio/generic_adapter/` (FootnoteExtraction, TableConversion, InlineRunCollection, HeadingExtraction, ListConversion, MetadataExtraction, DocumentExtraction). Adding an element family = a module + one include.
63
+ - `lib/arroolio/generic_flow_builder.rb` — `build()` + dispatch; per-family emission seams under `lib/arroolio/generic_flow_builder/` (Sequences, Terms, Notes, Figures, Lists, Bibliography, Tables).
64
+ - `lib/arroolio/config_driven_pipeline.rb` — orchestrates adapter + flow builder + engine + renderer + ToC; resolves fonts via fontist (`FontScanner`).
65
+ - `lib/arroolio/renderer/pdf.rb` — page emission; Pdfrb coupling points in `lib/arroolio/renderer/pdf/` (FontEmbedding, Assets, Metadata). The renderer never measures glyph widths `Line::PlacedRun` carries `chunk_widths` from the breakers.
66
+ - `lib/arroolio/asset_resolver.rb`, `lib/arroolio/toc_builder.rb` — generic helpers (both with direct specs).
66
67
  - `exe/arrolio2pdf` — generic CLI taking a flavor directory argument.
67
68
  - `scripts/xsl_to_config.rb` — generates all three flavor YAML files from an XSL stylesheet.
68
69
  - `data/arrolio/` — AFM metrics + glyphlist for the 14 PDF standard fonts (core data, not flavor data).
@@ -71,18 +72,13 @@ What's outside the gem (flavor artifacts):
71
72
  - `flavors/oiml/{layout_spec,adapter_rules,flow_rules}.yml` — generated by `xsl_to_config.rb` from the authoritative XSL at `~/src/mn/metanorma-taste/data/oiml/oiml.xsl`.
72
73
  - `spec/fixtures/flavors/sample/` — synthetic flavor-neutral fixture for the gem's tests.
73
74
 
74
- What's deferred (intentionally — see `TODO.complete/`):
75
- - TODO 53: second flavor spike (proves the generic design works beyond OIML).
76
- - TODO 54: `flavors/<name>/manifest.yml` contract.
77
- - TODO 55: header/footer style in `layout_spec.yml` (currently hardcoded in renderer).
78
- - TODO 56: split `GenericAdapter` into MECE per-element converters.
79
- - TODO 57: strict `Content::Document#sections` typing (only `Section[]`).
80
- - TODO 58: flavor packaging convention (`arrolio-<flavor>` gems).
81
- - TODO 59: XSL converter profiles (metanorma, dita, docbook).
82
- - TODO 60: footnote / note-hanging-indent / example-body-margin fidelity.
83
- - TODO 61: typed error model + strict mode.
84
- - TODO 62: font resolution via fontist (currently absolute paths in YAML).
85
- - Plus the older TODO.implementation items (Knuth-Plass, PageSequenceMaster, table layout, etc.).
75
+ What's open (intentionally — see `TODO.complete/`, frontmatter `status:` is authoritative):
76
+ - TODO 87: odd/even page templates beyond header parity (headers mirror; templates don't).
77
+ - TODO 88: hyphenation support.
78
+ - TODO 90: MathML mfrac true stacking (the open piece).
79
+ - TODO 93: figure text searchability (figures render as images; the reference's figure text extracts).
80
+ - TODO 96: pagination drift parity — the master ledger: remaining region deficits, keep-rules negative results (the reference does NOT enforce widows; term entries are NOT atomic), the heading-gap coupled-landing recipe, and the C2 margin-unification decode (a dedicated re-tuning session, not a release-day change).
81
+ - TODOs 53-62 are DONE (second flavor spike, manifest contract, header/footer config, adapter MECE, strict typing, packaging docs, XSL profiles, footnote fidelity, typed errors, fontist).
86
82
 
87
83
  The TODO files in `TODO.complete/` and `TODO.implementation/` are the roadmap. `TODO.complete/README.md` covers the parity roadmap; `TODO.implementation/README.md` covers the foundation phase plan.
88
84
 
@@ -137,8 +133,9 @@ When extending any layer, apply OCP/DRY/MECE strictly:
137
133
  ## Renderer/Pdfrb coupling notes
138
134
 
139
135
  - `Renderer::Pdf#render_page` deliberately points each pdfrb page's `Resources` Hash at the catalog's. Pdfrb's `Fonts#add` attaches fonts to the *catalog* Resources, but each Page shadows with its own empty Hash — without this workaround, fonts are invisible to readers.
140
- - `Renderer::FontRegistry` memoises font family name Pdfrb resource Symbol per render pass. Falls back to Helvetica for unknown families so the render never crashes mid-document. TODO 62 makes this strict.
141
- - TODO 55 moves the header/footer visual style (Arial 9pt + 0.5pt border, currently hardcoded in the renderer) into `layout_spec.yml` so each flavor controls its own header/footer look.
136
+ - Margin contract (see `Flowable`'s doc): TextFlowable (and HeadingFlowable) count `space_before/after` inside height/emit; List/Image/Table-based flowables do not — spacing around them travels as explicit Spacers. The engine collapses via `Engine::MarginCollapse`; do NOT unify without a dedicated re-tuning session (decoded in TODO 96).
137
+ - Header/footer styling comes from `layout_spec.yml` (`header_footer:` block); font resolution goes through fontist (`FontScanner`) with AFM fallback for the 14 standard fonts.
138
+ - Parity workflow: `bundle exec rake parity:check` / `parity:diff PAGE=N`; drift-map tooling and diagnostic cautions live in TODO 96.
142
139
 
143
140
  ## Key files to read first
144
141
 
@@ -1,11 +1,13 @@
1
1
  # TODO.complete — Path to 100% mn2pdf parity
2
2
 
3
- ## Current baseline (2026-08-09)
3
+ ## Current baseline (2026-09-01)
4
4
 
5
- `bundle exec rake parity:check` reports **62.7%** overall similarity
6
- on the OIML r060/1 fixture, **28 pages vs 28 reference** (all pages PARTIAL+).
5
+ `bundle exec rake parity:check` reports **68.92%** overall similarity
6
+ on the OIML r060/1 fixture, **28 pages vs 28 reference**.
7
7
 
8
- 376 specs pass, 0 failures, 0 rubocop offenses across 174 files.
8
+ 452 specs pass, 0 failures, 0 rubocop offenses across 214 files.
9
+ TODO 96 is the live ledger (region deficits, keep-rule negative
10
+ results, the C2 margin decode).
9
11
 
10
12
  ## Status snapshot
11
13
 
@@ -99,20 +99,15 @@ module Arrolio
99
99
  header, body = partition_rows
100
100
  header_count = header.length
101
101
 
102
- head_groups = []
103
- tail_groups = []
104
102
  # The head part also carries the caption and the repeated
105
103
  # header rows — they consume the same budget as body rows.
106
- used = caption_height(width) + heights[0, header_count].sum
107
- body_row_groups.each do |group|
108
- group_h = heights[group.first, group.length].sum
109
- if used + group_h <= remaining_height
110
- head_groups << group
111
- used += group_h
112
- else
113
- tail_groups << group
114
- end
115
- end
104
+ budget = remaining_height - caption_height(width) -
105
+ heights[0, header_count].sum
106
+ result = Table::SplitPolicy.new(groups: body_row_groups,
107
+ group_heights: heights,
108
+ budget: budget).call
109
+ head_groups = result.head_groups
110
+ tail_groups = result.tail_groups
116
111
 
117
112
  head_rows = head_groups.flat_map { |g| body[g.first - header_count, g.length] }
118
113
  tail_rows = tail_groups.flat_map { |g| body[g.first - header_count, g.length] }
@@ -51,7 +51,11 @@ module Arrolio
51
51
  def split_by_page(text)
52
52
  return [text] unless text.include?("\f")
53
53
 
54
- text.split("\f", -1)
54
+ pages = text.split("\f", -1)
55
+ # pdftotext ends output with a trailing form-feed for some
56
+ # writers; the empty string after it is not a page.
57
+ pages.pop if pages.last.to_s.strip.empty? && pages.length > 1
58
+ pages
55
59
  end
56
60
  end
57
61
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arrolio
4
+ module Table
5
+ # Pure table-split decision: which welded row groups fit the
6
+ # page budget. The budget is the remaining page height minus
7
+ # what travels with every part (the caption and the repeated
8
+ # header rows). Groups are greedily packed while they fit; an
9
+ # empty head means whole-move (the engine moves the entire
10
+ # table to the next page).
11
+ class SplitPolicy
12
+ Result = Struct.new(:head_groups, :tail_groups, keyword_init: true) do
13
+ def whole_move?
14
+ head_groups.empty?
15
+ end
16
+ end
17
+
18
+ # +groups+ Array of Arrays of row indexes (welded by rowspan).
19
+ # +group_heights+ parallel Array of Float heights per group.
20
+ # +budget+ Float height available for body groups.
21
+ def initialize(groups:, group_heights:, budget:)
22
+ @groups = groups
23
+ @group_heights = group_heights
24
+ @budget = budget.to_f
25
+ end
26
+
27
+ # Rows split IN ORDER: once a group does not fit, every
28
+ # later group goes to the tail (a fitting later group must
29
+ # never jump ahead of an overflowing earlier one - the
30
+ # original greedy loop allowed exactly that reordering).
31
+ def call
32
+ head = []
33
+ tail = []
34
+ used = 0.0
35
+ overflowing = false
36
+ @groups.each do |group|
37
+ height = group_height_for(group)
38
+ overflowing ||= used + height > @budget
39
+ (overflowing ? tail : head) << group
40
+ used += height unless overflowing
41
+ end
42
+ Result.new(head_groups: head, tail_groups: tail)
43
+ end
44
+
45
+ private
46
+
47
+ def group_height_for(group)
48
+ @group_heights[group.first, group.length].sum
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/arrolio/table.rb CHANGED
@@ -6,5 +6,6 @@ module Arrolio
6
6
  module Table
7
7
  autoload :AutoLayout, 'arrolio/table/auto_layout'
8
8
  autoload :Grid, 'arrolio/table/grid'
9
+ autoload :SplitPolicy, 'arrolio/table/split_policy'
9
10
  end
10
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arrolio
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.7'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arrolio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-31 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fontisan
@@ -432,6 +432,7 @@ files:
432
432
  - lib/arrolio/table.rb
433
433
  - lib/arrolio/table/auto_layout.rb
434
434
  - lib/arrolio/table/grid.rb
435
+ - lib/arrolio/table/split_policy.rb
435
436
  - lib/arrolio/text_direction.rb
436
437
  - lib/arrolio/text_layout.rb
437
438
  - lib/arrolio/text_layout/break_opportunity.rb