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.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +343 -0
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +154 -0
- data/Gemfile +15 -0
- data/LICENSE +24 -0
- data/README.adoc +95 -0
- data/Rakefile +21 -0
- data/TODO.complete/01-true-type-font-metrics.md +56 -0
- data/TODO.complete/02-font-embedding.md +51 -0
- data/TODO.complete/03-knuth-plass-breaker.md +42 -0
- data/TODO.complete/04-table-flowable.md +60 -0
- data/TODO.complete/05-list-flowable.md +50 -0
- data/TODO.complete/06-notes-and-examples.md +41 -0
- data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
- data/TODO.complete/08-image-flowable.md +45 -0
- data/TODO.complete/09-toc-and-leaders.md +66 -0
- data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
- data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
- data/TODO.complete/12-metadata-info-dict.md +42 -0
- data/TODO.complete/13-xmp-metadata.md +43 -0
- data/TODO.complete/14-footnotes.md +43 -0
- data/TODO.complete/15-color-management.md +33 -0
- data/TODO.complete/16-pdfua-struct-tree.md +35 -0
- data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
- data/TODO.complete/18-math-rendering.md +41 -0
- data/TODO.complete/19-preformatted-and-code.md +31 -0
- data/TODO.complete/20-page-break-controls.md +46 -0
- data/TODO.complete/21-svg-renderer.md +40 -0
- data/TODO.complete/22-definition-lists.md +36 -0
- data/TODO.complete/23-multi-column.md +34 -0
- data/TODO.complete/24-cover-page-fidelity.md +40 -0
- data/TODO.complete/25-i18n-and-multi-language.md +28 -0
- data/TODO.complete/26-pixel-diff-harness.md +36 -0
- data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
- data/TODO.complete/28-restore-nested-module-style.md +41 -0
- data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
- data/TODO.complete/30-adapter-decomposition.md +76 -0
- data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
- data/TODO.complete/32-debug-output-cleanup.md +59 -0
- data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
- data/TODO.complete/34-table-column-auto-layout.md +50 -0
- data/TODO.complete/35-image-path-resolution.md +66 -0
- data/TODO.complete/36-xsl-converter-completeness.md +40 -0
- data/TODO.complete/37-content-model-completeness.md +54 -0
- data/TODO.complete/38-pipeline-config-externalization.md +55 -0
- data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
- data/TODO.complete/40-vertical-writing-modes.md +63 -0
- data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
- data/TODO.complete/42-font-manifest-configuration.md +75 -0
- data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
- data/TODO.complete/44-back-of-book-index-generation.md +51 -0
- data/TODO.complete/45-pdf-forms-acroform.md +52 -0
- data/TODO.complete/46-pdf-digital-signatures.md +47 -0
- data/TODO.complete/47-ocp-flavor-separation.md +50 -0
- data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
- data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
- data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
- data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
- data/TODO.complete/52-flavor-packaging-separation.md +49 -0
- data/TODO.complete/53-second-flavor-spike.md +74 -0
- data/TODO.complete/54-flavor-manifest-contract.md +72 -0
- data/TODO.complete/55-header-footer-style-configuration.md +57 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
- data/TODO.complete/57-strict-section-typing.md +49 -0
- data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
- data/TODO.complete/59-xsl-converter-profiles.md +76 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
- data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
- data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
- data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
- data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
- data/TODO.complete/66-semantic-content-types.md +171 -0
- data/TODO.complete/README.md +48 -0
- data/TODO.implementation/01-foundation.md +46 -0
- data/TODO.implementation/02-content-contract.md +47 -0
- data/TODO.implementation/03-layoutspec-contract.md +46 -0
- data/TODO.implementation/04-style-system.md +43 -0
- data/TODO.implementation/05-visitor-pattern.md +40 -0
- data/TODO.implementation/06-afm-data-and-parser.md +37 -0
- data/TODO.implementation/07-afm-metrics.md +41 -0
- data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
- data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
- data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
- data/TODO.implementation/11-greedy-line-breaker.md +48 -0
- data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
- data/TODO.implementation/13-line-and-alignment.md +45 -0
- data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
- data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
- data/TODO.implementation/16-flowable-and-frame.md +52 -0
- data/TODO.implementation/17-flow-context.md +50 -0
- data/TODO.implementation/18-text-flowable.md +52 -0
- data/TODO.implementation/19-spacer-and-page-break.md +52 -0
- data/TODO.implementation/20-engine-paged.md +63 -0
- data/TODO.implementation/21-output-tree.md +56 -0
- data/TODO.implementation/22-pdf-renderer.md +77 -0
- data/TODO.implementation/23-end-to-end-smoke.md +53 -0
- data/TODO.implementation/24-page-template-and-region.md +51 -0
- data/TODO.implementation/25-page-sequence-master.md +54 -0
- data/TODO.implementation/26-two-pass-layout.md +66 -0
- data/TODO.implementation/27-table-model.md +48 -0
- data/TODO.implementation/28-table-layouts.md +53 -0
- data/TODO.implementation/29-table-renderer.md +55 -0
- data/TODO.implementation/30-table-flowable.md +53 -0
- data/TODO.implementation/31-list-model.md +45 -0
- data/TODO.implementation/32-list-flowable.md +54 -0
- data/TODO.implementation/33-image-flowable.md +59 -0
- data/TODO.implementation/34-svg-renderer.md +58 -0
- data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
- data/TODO.implementation/36-inline-builder.md +50 -0
- data/TODO.implementation/37-hyperlink.md +53 -0
- data/TODO.implementation/38-sub-superscript.md +44 -0
- data/TODO.implementation/39-leader-and-field-runs.md +60 -0
- data/TODO.implementation/40-bookmark-outline.md +45 -0
- data/TODO.implementation/41-destinations.md +52 -0
- data/TODO.implementation/42-toc-builder.md +52 -0
- data/TODO.implementation/43-footnotes.md +51 -0
- data/TODO.implementation/44-multi-column.md +45 -0
- data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
- data/TODO.implementation/46-color-management.md +47 -0
- data/TODO.implementation/47-metadata-and-xmp.md +41 -0
- data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
- data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
- data/TODO.implementation/50-composer-facade.md +59 -0
- data/TODO.implementation/51-style-presets.md +44 -0
- data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
- data/TODO.implementation/53-oiml-layout-spec.md +121 -0
- data/TODO.implementation/54-oiml-pipeline.md +97 -0
- data/TODO.implementation/55-pdf-comparator.md +98 -0
- data/TODO.implementation/56-diff-formatters.md +95 -0
- data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
- data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
- data/TODO.implementation/59-pixel-diff-mode.md +93 -0
- data/TODO.implementation/60-yard-documentation.md +43 -0
- data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
- data/TODO.implementation/62-validation-framework.md +107 -0
- data/TODO.implementation/63-layout-rules-profile.md +110 -0
- data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
- data/TODO.implementation/65-feature-extractor.md +64 -0
- data/TODO.implementation/66-auto-fixer.md +75 -0
- data/TODO.implementation/README.md +169 -0
- data/data/arrolio/afm/Courier-Bold.afm +342 -0
- data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
- data/data/arrolio/afm/Courier-Oblique.afm +342 -0
- data/data/arrolio/afm/Courier.afm +342 -0
- data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
- data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
- data/data/arrolio/afm/Helvetica.afm +3051 -0
- data/data/arrolio/afm/Symbol.afm +213 -0
- data/data/arrolio/afm/Times-Bold.afm +2588 -0
- data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
- data/data/arrolio/afm/Times-Italic.afm +2667 -0
- data/data/arrolio/afm/Times-Roman.afm +2419 -0
- data/data/arrolio/afm/ZapfDingbats.afm +225 -0
- data/data/arrolio/glyphlist.txt +4306 -0
- data/exe/arrolio2pdf +28 -0
- data/lib/arrolio/asset_resolver.rb +36 -0
- data/lib/arrolio/color.rb +107 -0
- data/lib/arrolio/column_set.rb +64 -0
- data/lib/arrolio/composer.rb +10 -0
- data/lib/arrolio/config_driven_pipeline.rb +144 -0
- data/lib/arrolio/content/bibliography_item.rb +42 -0
- data/lib/arrolio/content/builder.rb +108 -0
- data/lib/arrolio/content/document.rb +60 -0
- data/lib/arrolio/content/example.rb +43 -0
- data/lib/arrolio/content/figure_group.rb +39 -0
- data/lib/arrolio/content/footnote.rb +46 -0
- data/lib/arrolio/content/form_field.rb +47 -0
- data/lib/arrolio/content/formula.rb +34 -0
- data/lib/arrolio/content/heading.rb +42 -0
- data/lib/arrolio/content/hyperlink.rb +42 -0
- data/lib/arrolio/content/image.rb +36 -0
- data/lib/arrolio/content/index_entry.rb +52 -0
- data/lib/arrolio/content/inline_run.rb +51 -0
- data/lib/arrolio/content/list.rb +60 -0
- data/lib/arrolio/content/note.rb +48 -0
- data/lib/arrolio/content/page_break.rb +28 -0
- data/lib/arrolio/content/paragraph.rb +38 -0
- data/lib/arrolio/content/preformatted.rb +47 -0
- data/lib/arrolio/content/section.rb +39 -0
- data/lib/arrolio/content/table.rb +117 -0
- data/lib/arrolio/content/term_entry.rb +43 -0
- data/lib/arrolio/content.rb +27 -0
- data/lib/arrolio/data_dir.rb +28 -0
- data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
- data/lib/arrolio/engine/paged.rb +186 -0
- data/lib/arrolio/engine.rb +9 -0
- data/lib/arrolio/error.rb +70 -0
- data/lib/arrolio/flavor/manifest.rb +111 -0
- data/lib/arrolio/flavor/registry.rb +54 -0
- data/lib/arrolio/flavor.rb +23 -0
- data/lib/arrolio/flow_context.rb +37 -0
- data/lib/arrolio/flowable.rb +66 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
- data/lib/arrolio/flowables/heading_flowable.rb +23 -0
- data/lib/arrolio/flowables/image_flowable.rb +53 -0
- data/lib/arrolio/flowables/list_flowable.rb +117 -0
- data/lib/arrolio/flowables/note_flowable.rb +19 -0
- data/lib/arrolio/flowables/page_break.rb +15 -0
- data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
- data/lib/arrolio/flowables/spacer.rb +34 -0
- data/lib/arrolio/flowables/table_flowable.rb +147 -0
- data/lib/arrolio/flowables/text_flowable.rb +95 -0
- data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
- data/lib/arrolio/flowables/two_column_block.rb +56 -0
- data/lib/arrolio/flowables.rb +19 -0
- data/lib/arrolio/font/embedder.rb +197 -0
- data/lib/arrolio/font/manifest.rb +114 -0
- data/lib/arrolio/font/resolver.rb +161 -0
- data/lib/arrolio/font/text_encoder.rb +28 -0
- data/lib/arrolio/font.rb +10 -0
- data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
- data/lib/arrolio/font_metrics/registry.rb +55 -0
- data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
- data/lib/arrolio/font_metrics.rb +10 -0
- data/lib/arrolio/frame.rb +36 -0
- data/lib/arrolio/generic_adapter.rb +672 -0
- data/lib/arrolio/generic_flow_builder.rb +338 -0
- data/lib/arrolio/glyph_measurer.rb +71 -0
- data/lib/arrolio/harness/pdf_diff.rb +104 -0
- data/lib/arrolio/harness/pixel_diff.rb +122 -0
- data/lib/arrolio/harness/text_diff.rb +53 -0
- data/lib/arrolio/harness/text_extractor.rb +58 -0
- data/lib/arrolio/harness.rb +10 -0
- data/lib/arrolio/inline_run.rb +59 -0
- data/lib/arrolio/layout_spec/flow.rb +17 -0
- data/lib/arrolio/layout_spec/loader.rb +117 -0
- data/lib/arrolio/layout_spec/page_template.rb +127 -0
- data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
- data/lib/arrolio/layout_spec/region.rb +38 -0
- data/lib/arrolio/layout_spec/scale_length.rb +34 -0
- data/lib/arrolio/layout_spec.rb +90 -0
- data/lib/arrolio/logger.rb +54 -0
- data/lib/arrolio/output/page.rb +69 -0
- data/lib/arrolio/output/placed_box.rb +68 -0
- data/lib/arrolio/output/region.rb +32 -0
- data/lib/arrolio/output.rb +10 -0
- data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
- data/lib/arrolio/renderer/font_registry.rb +32 -0
- data/lib/arrolio/renderer/link_annotator.rb +62 -0
- data/lib/arrolio/renderer/outline_builder.rb +102 -0
- data/lib/arrolio/renderer/pdf.rb +493 -0
- data/lib/arrolio/renderer/signature_config.rb +59 -0
- data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
- data/lib/arrolio/renderer/xmp_builder.rb +92 -0
- data/lib/arrolio/renderer.rb +17 -0
- data/lib/arrolio/style/definition.rb +98 -0
- data/lib/arrolio/style/loader.rb +25 -0
- data/lib/arrolio/style/registry.rb +53 -0
- data/lib/arrolio/style.rb +9 -0
- data/lib/arrolio/table/auto_layout.rb +91 -0
- data/lib/arrolio/table.rb +9 -0
- data/lib/arrolio/text_direction.rb +81 -0
- data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
- data/lib/arrolio/text_layout/greedy.rb +110 -0
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
- data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
- data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
- data/lib/arrolio/text_layout/line.rb +72 -0
- data/lib/arrolio/text_layout.rb +11 -0
- data/lib/arrolio/toc_builder.rb +31 -0
- data/lib/arrolio/version.rb +5 -0
- data/lib/arrolio/writing_mode.rb +75 -0
- data/lib/arrolio.rb +42 -0
- data/script/smoke.rb +32 -0
- data/scripts/write_content_extras.py +227 -0
- data/scripts/xsl_profiles/standoc.yml +218 -0
- data/scripts/xsl_to_config.rb +391 -0
- metadata +394 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 6
|
|
4
|
+
depends_on: [20]
|
|
5
|
+
layer: output
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Engine produces laid-out pages but currently they're tied to Pdfrb's
|
|
13
|
+
page object. The Output tree should be a medium-neutral representation
|
|
14
|
+
of "what goes where" — separate from both the layout algorithm and
|
|
15
|
+
the renderer. The PDF renderer walks the Output tree; future
|
|
16
|
+
renderers (PostScript, PPML, an in-memory canvas for tests) walk the
|
|
17
|
+
same tree.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
Files under `lib/arrolio/output/`:
|
|
22
|
+
|
|
23
|
+
- `page.rb` — `Page` value object:
|
|
24
|
+
- `number` (Integer)
|
|
25
|
+
- `template_ref` (Symbol or PageTemplate reference)
|
|
26
|
+
- `regions` (Hash of region_name → Region)
|
|
27
|
+
- `static_content` (Array of StaticContent)
|
|
28
|
+
- `size` (Array `[w, h]`)
|
|
29
|
+
|
|
30
|
+
- `region.rb` — `Region` value object:
|
|
31
|
+
- `name` (`:body`, `:before`, `:after`, ...)
|
|
32
|
+
- `frame` (the consumed Frame snapshot)
|
|
33
|
+
- `placed_boxes` (Array of PlacedBox)
|
|
34
|
+
|
|
35
|
+
- `placed_box.rb` — `PlacedBox` value object:
|
|
36
|
+
- `x`, `y`, `width`, `height` (Float)
|
|
37
|
+
- `content_ref` (opaque reference to the source flowable or run)
|
|
38
|
+
- `style` (resolved Style::Definition snapshot)
|
|
39
|
+
- `kind` (`:text`, `:image`, `:shape`, ...)
|
|
40
|
+
|
|
41
|
+
- `static_content.rb` — region name + a list of flowables (rendered
|
|
42
|
+
in pass 2 with resolved citations).
|
|
43
|
+
|
|
44
|
+
The Output tree is fully reified before rendering — no lazy
|
|
45
|
+
generation. This makes rendering straightforward and lets us diff
|
|
46
|
+
Output trees between runs.
|
|
47
|
+
|
|
48
|
+
## Done-When
|
|
49
|
+
|
|
50
|
+
- [ ] Engine returns `[Output::Page]`.
|
|
51
|
+
- [ ] Each Page has at least a body Region with placed_boxes.
|
|
52
|
+
- [ ] StaticContent records region + flowables but doesn't render
|
|
53
|
+
until pass 2.
|
|
54
|
+
- [ ] Output tree is fully frozen; no mutation after construction.
|
|
55
|
+
- [ ] `Output::Dumper.to_yaml(page)` produces a stable, diffable
|
|
56
|
+
representation.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 6
|
|
4
|
+
depends_on: [21]
|
|
5
|
+
layer: render
|
|
6
|
+
est: 3d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The PDF renderer walks `Output::Page[]` and emits PDF bytes via
|
|
13
|
+
Pdfrb. This is where Arrolio stops being medium-neutral and becomes
|
|
14
|
+
PDF-specific. The renderer must NOT make layout decisions — every
|
|
15
|
+
position and style comes from the Output tree.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/renderer/pdf.rb`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Arrolio::Renderer::Pdf
|
|
23
|
+
def render(pages, io:)
|
|
24
|
+
pdfrb_doc = Pdfrb::Document.new
|
|
25
|
+
pages.each { |page| render_page(pdfrb_doc, page) }
|
|
26
|
+
pdfrb_doc.write(io: io)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def render_page(doc, output_page)
|
|
32
|
+
pdfrb_page = doc.pages.add(media_box: [0, 0, *output_page.size])
|
|
33
|
+
canvas = pdfrb_page.canvas
|
|
34
|
+
output_page.regions.each_value do |region|
|
|
35
|
+
render_region(canvas, region)
|
|
36
|
+
end
|
|
37
|
+
output_page.static_content.each { |sc| render_static(canvas, sc) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render_region(canvas, region)
|
|
41
|
+
region.placed_boxes.each { |box| render_box(canvas, box) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_box(canvas, box)
|
|
45
|
+
case box.kind
|
|
46
|
+
when :text then render_text(canvas, box)
|
|
47
|
+
when :image then render_image(canvas, box)
|
|
48
|
+
when :shape then render_shape(canvas, box)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Mapping table from Arrolio Output concepts to Pdfrb primitives:
|
|
55
|
+
|
|
56
|
+
| Arrolio | Pdfrb |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `Output::Page` of size `[w,h]` | `doc.pages.add(media_box: [0,0,w,h])` |
|
|
59
|
+
| `PlacedBox` kind `:text` | `canvas.text(content, at:, font:, size:, char_spacing:, word_spacing:)` |
|
|
60
|
+
| `PlacedBox` kind `:image` | `name = doc.images.add(path); canvas.image(name, at:, width:, height:)` |
|
|
61
|
+
| `PlacedBox` kind `:shape` (rectangle, line) | `canvas.rectangle/line + canvas.fill/stroke` |
|
|
62
|
+
| `StaticContent` | render via the same path, but at region-specific coordinates |
|
|
63
|
+
|
|
64
|
+
The renderer holds no state between pages — each page renders from
|
|
65
|
+
the Output tree afresh.
|
|
66
|
+
|
|
67
|
+
## Done-When
|
|
68
|
+
|
|
69
|
+
- [ ] `Renderer::Pdf.new.render([Output::Page.new(...)], io: StringIO.new)`
|
|
70
|
+
produces bytes starting with `%PDF-`.
|
|
71
|
+
- [ ] A single text page renders the text correctly.
|
|
72
|
+
- [ ] Multi-page documents produce N pages with correct page numbers
|
|
73
|
+
in PDF metadata.
|
|
74
|
+
- [ ] Round-trip: rendered PDF re-read by Pdfrb has the expected
|
|
75
|
+
number of pages and extractable text.
|
|
76
|
+
- [ ] No layout decisions leak into the renderer (all coordinates
|
|
77
|
+
come from Output::PlacedBox).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 6
|
|
4
|
+
depends_on: [22]
|
|
5
|
+
layer: output
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Need a single test that proves the full pipeline works end-to-end:
|
|
13
|
+
Content → LayoutSpec → Engine → Output → Renderer → PDF bytes that
|
|
14
|
+
read back correctly. This is the MVP milestone.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
File: `spec/arrolio/end_to_end_smoke_spec.rb`.
|
|
19
|
+
|
|
20
|
+
Build the simplest non-trivial document:
|
|
21
|
+
- A4 page, 25mm margins.
|
|
22
|
+
- One section with a heading and two paragraphs of body text.
|
|
23
|
+
- Use the standard 14 Type1 fonts (Helvetica + Times).
|
|
24
|
+
|
|
25
|
+
Run it through the pipeline:
|
|
26
|
+
```ruby
|
|
27
|
+
content = Arrolio::Content::Document.build { ... }
|
|
28
|
+
layout = Arrolio::LayoutSpec.build { ... }
|
|
29
|
+
pages = Arrolio::Engine::Paged.new(layout_spec: layout,
|
|
30
|
+
flowables: FlowBuilder.(content, layout)).layout
|
|
31
|
+
out = StringIO.new
|
|
32
|
+
Arrolio::Renderer::Pdf.new.render(pages, io: out)
|
|
33
|
+
|
|
34
|
+
# Assertions
|
|
35
|
+
reopened = Pdfrb::Document.new(io: StringIO.new(out.string))
|
|
36
|
+
expect(reopened.pages.count).to eq(1)
|
|
37
|
+
extracted = Pdfrb::Task::ExtractText.(reopened).first
|
|
38
|
+
expect(extracted).to include("Hello, World!")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Also a multi-page variant: 50 paragraphs that overflow to 2+ pages.
|
|
42
|
+
|
|
43
|
+
This spec becomes the regression canary — any breaking change in
|
|
44
|
+
any layer fails it.
|
|
45
|
+
|
|
46
|
+
## Done-When
|
|
47
|
+
|
|
48
|
+
- [ ] Single-page smoke spec passes.
|
|
49
|
+
- [ ] Multi-page overflow spec passes (≥ 2 pages, text extractable).
|
|
50
|
+
- [ ] Round-trip: write → read → re-write → re-read produces same
|
|
51
|
+
text extraction.
|
|
52
|
+
- [ ] The spec runs in < 2 seconds.
|
|
53
|
+
- [ ] Spec is marked `:e2e` so it can be excluded from fast unit runs.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 7
|
|
4
|
+
depends_on: [03, 20]
|
|
5
|
+
layer: template
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Documents need more than one page geometry: cover pages differ from
|
|
13
|
+
body pages, running headers/footers vary, odd/even pages mirror each
|
|
14
|
+
other. The PageTemplate + Region types define static geometry; the
|
|
15
|
+
engine instantiates them per output page.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files under `lib/arrolio/layout_spec/`:
|
|
20
|
+
|
|
21
|
+
- `page_template.rb` (extended from TODO 03):
|
|
22
|
+
- `name` (Symbol).
|
|
23
|
+
- `page_size` (`[w, h]` or named like `:A4`, `:Letter`).
|
|
24
|
+
- `margins` (Hash with `:top`, `:right`, `:bottom`, `:left`).
|
|
25
|
+
- `region_extents` (Hash `:before => 15mm`, etc.).
|
|
26
|
+
- Computed `regions` (Hash of name → Region with absolute geometry).
|
|
27
|
+
|
|
28
|
+
- `region.rb`:
|
|
29
|
+
- `name`, `x`, `y`, `width`, `height`.
|
|
30
|
+
- `flow_ref` (optional — names the Flow that fills this region).
|
|
31
|
+
- `to_frame` → returns a fresh `Frame` for runtime use.
|
|
32
|
+
|
|
33
|
+
Helper: `PageSizes` constant mapping `:A4` → `[595, 842]`, `:Letter`
|
|
34
|
+
→ `[612, 792]`, `:Legal` → `[612, 1008]`, etc.
|
|
35
|
+
|
|
36
|
+
Region geometry computed from page size + margins + region extents:
|
|
37
|
+
```
|
|
38
|
+
body = Region(x: left_margin, y: bottom_margin,
|
|
39
|
+
width: page_w - L - R,
|
|
40
|
+
height: page_h - T - B - before_extent - after_extent)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Done-When
|
|
44
|
+
|
|
45
|
+
- [ ] `PageTemplate.new(page_size: :A4, margins: 25)` produces a
|
|
46
|
+
template with body region of 545×743pt.
|
|
47
|
+
- [ ] Asymmetric margins honoured.
|
|
48
|
+
- [ ] `Region#to_frame` returns a Frame with `remaining_height ==
|
|
49
|
+
height`.
|
|
50
|
+
- [ ] All 14 standard page sizes supported.
|
|
51
|
+
- [ ] Specs cover A4, Letter, asymmetric margins, with-header-footer.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 7
|
|
4
|
+
depends_on: [24]
|
|
5
|
+
layer: template
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The engine needs to know which PageTemplate to use for each output
|
|
13
|
+
page. FOP's `fo:page-sequence-master` solves this with rules: first,
|
|
14
|
+
odd, even, blank, last. Same model in Ruby.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
File: `lib/arrolio/layout_spec/page_sequence_master.rb`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Arrolio::LayoutSpec::PageSequenceMaster
|
|
22
|
+
attr_reader :rules, :fallback
|
|
23
|
+
|
|
24
|
+
def initialize(rules: {}, fallback: nil)
|
|
25
|
+
# rules: { first: template_name, odd: ..., even: ..., blank: ..., last: ... }
|
|
26
|
+
# All values are Symbol template names.
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def template_name_for(page_number:, total: nil, blank: false)
|
|
30
|
+
# Returns a Symbol template name. Selection order:
|
|
31
|
+
# 1. first page (page_number == 1) && rules[:first]
|
|
32
|
+
# 2. blank page && rules[:blank]
|
|
33
|
+
# 3. last page (page_number == total) && rules[:last]
|
|
34
|
+
# 4. odd/even by parity
|
|
35
|
+
# 5. fallback or rules[:default]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Engine integration:
|
|
41
|
+
- Track page_number, total (set in pass 1), and "is this a blank page"
|
|
42
|
+
(page with no body content, only static).
|
|
43
|
+
- On each new page, query the master for the template name.
|
|
44
|
+
- Look up the actual PageTemplate in the LayoutSpec.
|
|
45
|
+
|
|
46
|
+
## Done-When
|
|
47
|
+
|
|
48
|
+
- [ ] `template_name_for(page_number: 1)` returns `rules[:first]` if set.
|
|
49
|
+
- [ ] `template_name_for(page_number: 4, total: 4)` returns
|
|
50
|
+
`rules[:last]` if set.
|
|
51
|
+
- [ ] `template_name_for(page_number: 2)` returns `rules[:even]`.
|
|
52
|
+
- [ ] `template_name_for(page_number: 3)` returns `rules[:odd]`.
|
|
53
|
+
- [ ] Falls through to fallback when no rule matches.
|
|
54
|
+
- [ ] Engine uses the selected template to size each new page.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 7
|
|
4
|
+
depends_on: [25]
|
|
5
|
+
layer: template
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Cross-references (`fo:page-number-citation`, "Page X of Y") need to
|
|
13
|
+
know page numbers that aren't decided until layout completes. The
|
|
14
|
+
engine runs two passes:
|
|
15
|
+
|
|
16
|
+
1. **Pass 1**: lay out everything with placeholder field values.
|
|
17
|
+
Record which page each citation target lands on.
|
|
18
|
+
2. **Pass 2**: re-render the static-content regions (headers/footers)
|
|
19
|
+
with resolved citation values.
|
|
20
|
+
|
|
21
|
+
Body content is NOT re-rendered in pass 2 — its layout is stable.
|
|
22
|
+
Only the static regions need field resolution.
|
|
23
|
+
|
|
24
|
+
## Approach
|
|
25
|
+
|
|
26
|
+
Modify `Arrolio::Engine::Paged` (TODO 20):
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
def layout
|
|
30
|
+
pass1_result = layout_pass1
|
|
31
|
+
context.page_count = pass1_result.pages.length
|
|
32
|
+
pass2(pass1_result.pages)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def pass2(pages)
|
|
36
|
+
pages.each_with_index do |page, i|
|
|
37
|
+
context.page_number = i + 1
|
|
38
|
+
page.render_static_content(context)
|
|
39
|
+
end
|
|
40
|
+
pages
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The `Output::Page#render_static_content(context)` method:
|
|
45
|
+
- For each `StaticContent` entry, resolve its flowables' field runs.
|
|
46
|
+
- Rebuild the placed boxes for the static region only.
|
|
47
|
+
- Replace the region's `placed_boxes` with the new ones.
|
|
48
|
+
|
|
49
|
+
Field runs (TODO 39) implement `resolve(context)` returning a String.
|
|
50
|
+
The static-content flow is built once during pass 1, but its field
|
|
51
|
+
runs are evaluated against `context` each time the region is
|
|
52
|
+
re-rendered.
|
|
53
|
+
|
|
54
|
+
Citation recording: when the engine places a flowable that has a
|
|
55
|
+
`citation_target_id`, it records
|
|
56
|
+
`context.record_citation(target_id, current_page_number)`.
|
|
57
|
+
|
|
58
|
+
## Done-When
|
|
59
|
+
|
|
60
|
+
- [ ] A document with "Page X of Y" in the footer renders correctly
|
|
61
|
+
(X = current page, Y = total).
|
|
62
|
+
- [ ] A `page-number-citation` to a section heading on page 5
|
|
63
|
+
resolves to "5" in static content.
|
|
64
|
+
- [ ] Body layout is identical between pass 1 and pass 2 (no relayout).
|
|
65
|
+
- [ ] Pass 2 only re-evaluates field runs; no other work.
|
|
66
|
+
- [ ] 100-page document: pass 2 completes in < 1 second.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 8
|
|
4
|
+
depends_on: [02, 16]
|
|
5
|
+
layer: table
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Tables are the hardest layout primitive. Need typed Row/Cell/Column
|
|
13
|
+
model that the layout algorithms (TODO 28) and renderer (TODO 29)
|
|
14
|
+
operate on. Cells can span multiple columns or rows. Header rows
|
|
15
|
+
repeat across page breaks.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files under `lib/arrolio/table/`:
|
|
20
|
+
|
|
21
|
+
- `table.rb` — value object holding `rows` (Array of Row),
|
|
22
|
+
`column_specs` (Array of ColumnSpec), `style` (TableStyle).
|
|
23
|
+
Methods: `num_columns`, `num_rows`, `header_rows`, `body_rows`.
|
|
24
|
+
|
|
25
|
+
- `row.rb` — `Row = Struct.new(:cells, :header, :style,
|
|
26
|
+
keyword_init: true)`. `header?` alias. Cells is an Array of Cell.
|
|
27
|
+
|
|
28
|
+
- `cell.rb` — `Cell = Struct.new(:content, :colspan, :rowspan,
|
|
29
|
+
:style, keyword_init: true)`. `content` is a Flowable or String
|
|
30
|
+
(Strings auto-wrapped in TextFlowable). `colspan?`, `rowspan?`
|
|
31
|
+
predicates.
|
|
32
|
+
|
|
33
|
+
- `column_spec.rb` — `ColumnSpec = Struct.new(:width, :min, :max,
|
|
34
|
+
:auto, keyword_init: true)`. Class methods: `.fixed(width)`,
|
|
35
|
+
`.auto(min:, max:)`. `fixed?` predicate.
|
|
36
|
+
|
|
37
|
+
Cell flowables: `Cell#flowables` normalises content into a list of
|
|
38
|
+
`Flowable` instances. Strings become `TextFlowable` with the cell's
|
|
39
|
+
style.
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] Construct a 3-row × 3-column table; verify `num_rows == 3`,
|
|
44
|
+
`num_columns == 3`.
|
|
45
|
+
- [ ] A row marked `header: true` appears in `header_rows`.
|
|
46
|
+
- [ ] A cell with `colspan: 2` reports `colspan? == true`.
|
|
47
|
+
- [ ] String cell content auto-wraps in TextFlowable.
|
|
48
|
+
- [ ] Specs cover construction, header detection, span predicates.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 8
|
|
4
|
+
depends_on: [27, 16]
|
|
5
|
+
layer: table
|
|
6
|
+
est: 3d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Given a Table + target width, compute the column widths and row
|
|
13
|
+
heights. Two strategies: Fixed (caller-specified widths) and Auto
|
|
14
|
+
(CSS 2.1 §17.5.2.2 — distribute surplus by content).
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
Files under `lib/arrolio/table_layout/`:
|
|
19
|
+
|
|
20
|
+
- `resolved_grid.rb` — value object: `column_widths`, `row_heights`,
|
|
21
|
+
`cell_rects` (per-cell { x, y, width, height }). `total_width`,
|
|
22
|
+
`total_height`.
|
|
23
|
+
|
|
24
|
+
- `fixed.rb` — Fixed strategy:
|
|
25
|
+
- Take column widths from specs.
|
|
26
|
+
- Scale proportionally if sum != target_width.
|
|
27
|
+
- Compute each row's height as the max cell content height at the
|
|
28
|
+
cell's resolved width.
|
|
29
|
+
|
|
30
|
+
- `auto.rb` — Auto strategy (CSS 2.1):
|
|
31
|
+
1. For each cell, measure min-content width (longest unbreakable
|
|
32
|
+
word) and max-content width (single line).
|
|
33
|
+
2. Aggregate to per-column min/max (respecting colspan: distribute
|
|
34
|
+
spanned cell's min/max evenly across spanned columns).
|
|
35
|
+
3. Distribute target_width:
|
|
36
|
+
- If surplus over max: distribute by max-content ratio.
|
|
37
|
+
- If between min and max: distribute by max ratio of the slack.
|
|
38
|
+
- If less than min: each column gets its min (may overflow).
|
|
39
|
+
4. Compute row heights same as Fixed.
|
|
40
|
+
|
|
41
|
+
Both strategies return a `ResolvedGrid`.
|
|
42
|
+
|
|
43
|
+
## Done-When
|
|
44
|
+
|
|
45
|
+
- [ ] Fixed layout: `Fixed.new(table, target_width: 300).layout`
|
|
46
|
+
honours the spec widths.
|
|
47
|
+
- [ ] Fixed layout scales when target_width != sum(specs).
|
|
48
|
+
- [ ] Auto layout: a column containing "antidisestablishmentarianism"
|
|
49
|
+
gets at least the word's width.
|
|
50
|
+
- [ ] Auto layout: narrow column for short content, wide for long.
|
|
51
|
+
- [ ] ResolvedGrid.cell_rects respects colspan (cell spans 2 columns'
|
|
52
|
+
combined width) and rowspan.
|
|
53
|
+
- [ ] Specs cover both strategies + colspan/rowspan.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 8
|
|
4
|
+
depends_on: [28, 22]
|
|
5
|
+
layer: table
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Given a resolved grid (column widths + cell rects), render the table:
|
|
13
|
+
draw borders as a single-line grid (border-collapse model), fill
|
|
14
|
+
backgrounds, place each cell's content via its flowables. The
|
|
15
|
+
renderer must produce `PlacedBox[]` entries that the PDF renderer
|
|
16
|
+
(TODO 22) consumes.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
File: `lib/arrolio/table_renderer.rb`.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class Arrolio::TableRenderer
|
|
24
|
+
def initialize(table, grid)
|
|
25
|
+
def render(canvas, x_origin, y_origin, context) -> Float (total height)
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Steps:
|
|
30
|
+
1. **Backgrounds**: for each row, fill the row's rect with the row's
|
|
31
|
+
`background_color` (or table's `header_background` for header rows).
|
|
32
|
+
2. **Borders**: border-collapse grid.
|
|
33
|
+
- Vertical lines: at each column boundary (including outer left/right).
|
|
34
|
+
- Horizontal lines: at each row boundary (including outer top/bottom).
|
|
35
|
+
- Stroke colour from `table.style.border_color`; width from
|
|
36
|
+
`table.style.border_width`.
|
|
37
|
+
3. **Content**: for each cell, stack its flowables vertically inside
|
|
38
|
+
the cell rect (accounting for `cell_padding`).
|
|
39
|
+
|
|
40
|
+
Output: emits `canvas` operations directly OR returns PlacedBox[]
|
|
41
|
+
(we'll do canvas operations directly for simplicity; the PlacedBox
|
|
42
|
+
indirection is for non-PDF renderers, deferred).
|
|
43
|
+
|
|
44
|
+
PDF coordinate flip: PDF y grows up; table y grows down. Convert via
|
|
45
|
+
`y_origin - rect.y - rect.height`.
|
|
46
|
+
|
|
47
|
+
## Done-When
|
|
48
|
+
|
|
49
|
+
- [ ] A 2×2 table renders as a visible grid with content in each cell.
|
|
50
|
+
- [ ] Header row background differs from body rows when
|
|
51
|
+
`header_background` is set.
|
|
52
|
+
- [ ] Border-collapse: adjacent cells share a single 1pt line, not
|
|
53
|
+
doubled.
|
|
54
|
+
- [ ] Cell content respects `cell_padding`.
|
|
55
|
+
- [ ] Colspan cell spans the combined width visually.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 8
|
|
4
|
+
depends_on: [29, 16]
|
|
5
|
+
layer: table
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Tables longer than a single page must split between rows (never
|
|
13
|
+
mid-cell). Header rows repeat on every fragment. The TableFlowable
|
|
14
|
+
wraps a Table so it integrates with the page-flow engine like any
|
|
15
|
+
other Flowable.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/flowables/table_flowable.rb`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Arrolio::Flowables::TableFlowable < Arrolio::Flowable
|
|
23
|
+
def initialize(table, algorithm: :auto)
|
|
24
|
+
def height(width, context)
|
|
25
|
+
def render(canvas, x, y, width, context)
|
|
26
|
+
def splittable?; true; end
|
|
27
|
+
def do_split(width, remaining_height, context) -> [head, tail]
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`do_split` algorithm:
|
|
32
|
+
1. Layout the table at `width` to get the resolved grid.
|
|
33
|
+
2. Walk rows; cumulate heights.
|
|
34
|
+
3. Header rows always go in the head.
|
|
35
|
+
4. Body rows: include in head if `cum_height + row_h <= remaining_height`;
|
|
36
|
+
otherwise include in tail.
|
|
37
|
+
5. If no body rows fit (`head_rows == header_rows`), return
|
|
38
|
+
`[nil, self]` so the engine advances to a new page.
|
|
39
|
+
6. Build two tables: `head_table` (header + first N body rows),
|
|
40
|
+
`tail_table` (header + remaining body rows — header repeats!).
|
|
41
|
+
7. Return `[TableFlowable.new(head_table), TableFlowable.new(tail_table)]`.
|
|
42
|
+
|
|
43
|
+
`render` uses TableRenderer.
|
|
44
|
+
|
|
45
|
+
## Done-When
|
|
46
|
+
|
|
47
|
+
- [ ] A short table (fits on one page) renders without splitting.
|
|
48
|
+
- [ ] A 30-row table on a single-page frame splits into multiple
|
|
49
|
+
pages, header row repeated on each.
|
|
50
|
+
- [ ] Split is between rows, never mid-cell.
|
|
51
|
+
- [ ] `do_split` with insufficient remaining returns `[nil, self]`.
|
|
52
|
+
- [ ] Engine then advances and renders the table on the next page.
|
|
53
|
+
- [ ] Colspan/rowspan preserved across splits.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 9
|
|
4
|
+
depends_on: [02, 16]
|
|
5
|
+
layer: list
|
|
6
|
+
est: 1d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Lists (bullet, numbered, definition) need a typed model: items with
|
|
13
|
+
markers, nested lists, marker schemes (decimal, alpha, roman, bullet
|
|
14
|
+
variants).
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
Files under `lib/arrolio/list/`:
|
|
19
|
+
|
|
20
|
+
- `list.rb` — value object: `items` (Array of Item), `marker_scheme`
|
|
21
|
+
(Symbol), `style`. Methods: `nested?`, `level`.
|
|
22
|
+
|
|
23
|
+
- `item.rb` — `Item = Struct.new(:marker, :content, :sublists,
|
|
24
|
+
keyword_init: true)`. `marker` may be a String (explicit) or nil
|
|
25
|
+
(use scheme). `content` is a Flowable or Array. `sublists` is
|
|
26
|
+
Array of List (for nesting).
|
|
27
|
+
|
|
28
|
+
- `marker_scheme.rb` — module of constants:
|
|
29
|
+
- `BULLET` ("•")
|
|
30
|
+
- `DASH` ("—")
|
|
31
|
+
- `DECIMAL` (Proc: `->(i) { "#{i}." }`)
|
|
32
|
+
- `LOWER_ALPHA`, `UPPER_ALPHA`
|
|
33
|
+
- `LOWER_ROMAN`, `UPPER_ROMAN`
|
|
34
|
+
- Custom schemes via Proc.
|
|
35
|
+
|
|
36
|
+
Nested lists indent and may use different schemes at different
|
|
37
|
+
levels (decimal at level 0, lower-alpha at level 1, etc.).
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] A 3-item bullet list has `marker_scheme == :bullet`.
|
|
42
|
+
- [ ] `MarkerScheme::DECIMAL.call(3) == "3."`.
|
|
43
|
+
- [ ] `MarkerScheme::LOWER_ROMAN.call(4) == "iv."`.
|
|
44
|
+
- [ ] A nested list (`item.sublists`) reports `level == 1`.
|
|
45
|
+
- [ ] Specs cover all built-in schemes + nesting.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 9
|
|
4
|
+
depends_on: [31, 18]
|
|
5
|
+
layer: list
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
A list flowable must:
|
|
13
|
+
- Render items as a two-column layout (label gutter + body).
|
|
14
|
+
- Auto-size the gutter to the widest marker.
|
|
15
|
+
- Apply hanging indent so wrapped lines align with the first line.
|
|
16
|
+
- Support nested lists (deeper indent).
|
|
17
|
+
- Be splittable between items (one item per page minimum).
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
File: `lib/arrolio/flowables/list_flowable.rb`.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class Arrolio::Flowables::ListFlowable < Arrolio::Flowable
|
|
25
|
+
def initialize(list, level: 0)
|
|
26
|
+
def height(width, context)
|
|
27
|
+
def render(canvas, x, y, width, context)
|
|
28
|
+
def splittable?; true; end
|
|
29
|
+
def do_split(width, remaining_height, context)
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Layout algorithm:
|
|
34
|
+
1. For each item, compute the marker text (from scheme or explicit).
|
|
35
|
+
2. Label column width = max marker width + small gap (e.g. 6pt).
|
|
36
|
+
3. Body column width = `width - label_column - indent`.
|
|
37
|
+
4. Render item:
|
|
38
|
+
- Place marker in label column (right-aligned to body column).
|
|
39
|
+
- Place content flowable in body column.
|
|
40
|
+
- Hanging indent: if content wraps, subsequent paragraphs in the
|
|
41
|
+
item align with the body column's left edge.
|
|
42
|
+
5. Nested lists: render as a sub-ListFlowable indented further.
|
|
43
|
+
|
|
44
|
+
Splitting: between items only. If first item doesn't fit on the
|
|
45
|
+
current frame, advance page.
|
|
46
|
+
|
|
47
|
+
## Done-When
|
|
48
|
+
|
|
49
|
+
- [ ] A 3-item bullet list renders with "•" markers in the left gutter.
|
|
50
|
+
- [ ] Decimal-numbered list auto-sizes gutter to "12." or wider.
|
|
51
|
+
- [ ] Wrapped content lines align with the first line (hanging indent).
|
|
52
|
+
- [ ] Nested list indents further and uses different marker scheme.
|
|
53
|
+
- [ ] A list longer than the page splits between items.
|
|
54
|
+
- [ ] Specs cover all of the above.
|