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 +4 -4
- data/CLAUDE.md +17 -20
- data/TODO.complete/README.md +6 -4
- data/lib/arrolio/flowables/table_flowable.rb +7 -12
- data/lib/arrolio/harness/text_extractor.rb +5 -1
- data/lib/arrolio/table/split_policy.rb +52 -0
- data/lib/arrolio/table.rb +1 -0
- data/lib/arrolio/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d2fb34e355fe255920ca92e339c7c2529db69b9668130d2f5c9f36319f2aa13
|
|
4
|
+
data.tar.gz: a591ae1001ae525c96d19a7321eee6840a105e3d9e6404a5b2a2454e68bd7b46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
56
|
+
## Project state (as of 2026-09-01)
|
|
57
57
|
|
|
58
|
-
|
|
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/
|
|
63
|
-
- `lib/
|
|
64
|
-
- `lib/
|
|
65
|
-
- `lib/
|
|
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
|
|
75
|
-
- TODO
|
|
76
|
-
- TODO
|
|
77
|
-
- TODO
|
|
78
|
-
- TODO
|
|
79
|
-
- TODO
|
|
80
|
-
-
|
|
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
|
-
- `
|
|
141
|
-
-
|
|
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
|
|
data/TODO.complete/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# TODO.complete — Path to 100% mn2pdf parity
|
|
2
2
|
|
|
3
|
-
## Current baseline (2026-
|
|
3
|
+
## Current baseline (2026-09-01)
|
|
4
4
|
|
|
5
|
-
`bundle exec rake parity:check` reports **
|
|
6
|
-
on the OIML r060/1 fixture, **28 pages vs 28 reference
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
data/lib/arrolio/version.rb
CHANGED
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.
|
|
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-
|
|
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
|