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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A footnote reference in the content tree. Carries the marker
|
|
6
|
+
# (e.g. "1", "a", "*") and the footnote body (paragraphs).
|
|
7
|
+
# The adapter emits these from <fn> elements; the FlowBuilder
|
|
8
|
+
# places the marker inline and defers the body to the page's
|
|
9
|
+
# footnote zone (rendered at the bottom of the page).
|
|
10
|
+
class Footnote
|
|
11
|
+
attr_reader :marker, :body, :id, :style_id
|
|
12
|
+
|
|
13
|
+
def initialize(marker:, body:, id: nil, style_id: :footnote)
|
|
14
|
+
@marker = marker.to_s
|
|
15
|
+
@body = Array(body).freeze
|
|
16
|
+
@id = id&.to_s
|
|
17
|
+
@style_id = style_id.to_sym
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def body_text
|
|
22
|
+
@body.map do |node|
|
|
23
|
+
node.is_a?(Paragraph) ? node.text : node.to_s
|
|
24
|
+
end.join(' ')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def empty?
|
|
28
|
+
@marker.empty? && @body.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ==(other)
|
|
32
|
+
other.is_a?(self.class) &&
|
|
33
|
+
marker == other.marker &&
|
|
34
|
+
body == other.body &&
|
|
35
|
+
id == other.id &&
|
|
36
|
+
style_id == other.style_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias eql? ==
|
|
40
|
+
|
|
41
|
+
def hash
|
|
42
|
+
[self.class, marker, body, id, style_id].hash
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A form field for interactive PDF AcroForm. Carries the field
|
|
6
|
+
# type, name, value, and geometry. The renderer emits it as a
|
|
7
|
+
# PDAcroForm widget (text field, checkbox, radio button) during
|
|
8
|
+
# post-processing. This model captures the semantic intent;
|
|
9
|
+
# the actual PDF widget creation is a renderer post-pass.
|
|
10
|
+
class FormField
|
|
11
|
+
TYPES = %i[text checkbox radio signature].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :type, :name, :value, :page_number,
|
|
14
|
+
:x, :y, :width, :height, :style_id
|
|
15
|
+
|
|
16
|
+
def initialize(type:, name:, page_number:, x:, y:, width:, height:, value: nil, style_id: :form_field)
|
|
17
|
+
@type = type.to_sym
|
|
18
|
+
@name = name.to_s
|
|
19
|
+
@value = value
|
|
20
|
+
@page_number = page_number.to_i
|
|
21
|
+
@x = x.to_f
|
|
22
|
+
@y = y.to_f
|
|
23
|
+
@width = width.to_f
|
|
24
|
+
@height = height.to_f
|
|
25
|
+
@style_id = style_id.to_sym
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def text? = @type == :text
|
|
30
|
+
def checkbox? = @type == :checkbox
|
|
31
|
+
def radio? = @type == :radio
|
|
32
|
+
|
|
33
|
+
def ==(other)
|
|
34
|
+
other.is_a?(self.class) &&
|
|
35
|
+
type == other.type && name == other.name &&
|
|
36
|
+
value == other.value && page_number == other.page_number &&
|
|
37
|
+
x == other.x && y == other.y &&
|
|
38
|
+
width == other.width && height == other.height
|
|
39
|
+
end
|
|
40
|
+
alias eql? ==
|
|
41
|
+
|
|
42
|
+
def hash
|
|
43
|
+
[self.class, type, name, value, page_number, x, y, width, height].hash
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A formula — MathML root + rendered text fallback. Carries the
|
|
6
|
+
# math structure separately from how it's rendered (text fallback
|
|
7
|
+
# today, proper math typesetting in the future). The renderer can
|
|
8
|
+
# later use the MathML for accessible representation while the
|
|
9
|
+
# fallback provides visual rendering.
|
|
10
|
+
class Formula
|
|
11
|
+
attr_reader :mathml, :text_fallback, :style_id
|
|
12
|
+
|
|
13
|
+
def initialize(mathml:, text_fallback:, style_id: :body)
|
|
14
|
+
@mathml = mathml.to_s
|
|
15
|
+
@text_fallback = text_fallback.to_s
|
|
16
|
+
@style_id = style_id.to_sym
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.is_a?(self.class) &&
|
|
22
|
+
mathml == other.mathml &&
|
|
23
|
+
text_fallback == other.text_fallback &&
|
|
24
|
+
style_id == other.style_id
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias eql? ==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[self.class, mathml, text_fallback, style_id].hash
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A semantic heading. Distinct from +Paragraph+ so the renderer
|
|
6
|
+
# can emit PDF structure tags (for accessibility/ToC), the engine
|
|
7
|
+
# can record heading → page mappings for cross-references, and the
|
|
8
|
+
# FlowBuilder can apply distinct rendering rules (keep_together,
|
|
9
|
+
# page-break-before, outline level).
|
|
10
|
+
class Heading
|
|
11
|
+
attr_reader :level, :number, :title, :id, :style_id
|
|
12
|
+
|
|
13
|
+
def initialize(level:, title:, number: nil, id: nil, style_id: :heading_1)
|
|
14
|
+
@level = level.to_i
|
|
15
|
+
@number = number&.to_s
|
|
16
|
+
@title = title.to_s
|
|
17
|
+
@id = id&.to_s
|
|
18
|
+
@style_id = style_id.to_sym
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(self.class) &&
|
|
24
|
+
level == other.level &&
|
|
25
|
+
number == other.number &&
|
|
26
|
+
title == other.title &&
|
|
27
|
+
id == other.id &&
|
|
28
|
+
style_id == other.style_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[self.class, level, number, title, id, style_id].hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inline_header?
|
|
38
|
+
!number.nil? && title.empty?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# Inline runs wrapped with a hyperlink target. The renderer
|
|
6
|
+
# emits a PDF /Annot of subtype /Link with the URI (external)
|
|
7
|
+
# or /Dest reference (internal cross-reference).
|
|
8
|
+
class Hyperlink
|
|
9
|
+
attr_reader :runs, :target, :id
|
|
10
|
+
|
|
11
|
+
def initialize(runs, target:, internal: false, id: nil)
|
|
12
|
+
@runs = Array(runs).freeze
|
|
13
|
+
@target = target.to_s
|
|
14
|
+
@internal_link = internal ? true : false
|
|
15
|
+
@id = id&.to_s
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(self.class) &&
|
|
21
|
+
runs == other.runs &&
|
|
22
|
+
target == other.target &&
|
|
23
|
+
internal? == other.internal? &&
|
|
24
|
+
id == other.id
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias eql? ==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[self.class, runs, target, internal?, id].hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def internal?
|
|
34
|
+
@internal_link
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def external?
|
|
38
|
+
!internal?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
class Image
|
|
6
|
+
attr_reader :src, :width, :height, :alt, :style_id, :id
|
|
7
|
+
|
|
8
|
+
def initialize(src, width: nil, height: nil, alt: nil,
|
|
9
|
+
style_id: :figure_image, id: nil)
|
|
10
|
+
@src = src.to_s
|
|
11
|
+
@width = width&.to_f
|
|
12
|
+
@height = height&.to_f
|
|
13
|
+
@alt = alt
|
|
14
|
+
@style_id = style_id.to_sym
|
|
15
|
+
@id = id
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(self.class) &&
|
|
21
|
+
src == other.src &&
|
|
22
|
+
width == other.width &&
|
|
23
|
+
height == other.height &&
|
|
24
|
+
alt == other.alt &&
|
|
25
|
+
style_id == other.style_id &&
|
|
26
|
+
id == other.id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
alias eql? ==
|
|
30
|
+
|
|
31
|
+
def hash
|
|
32
|
+
[self.class, src, width, height, alt, style_id, id].hash
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A single index entry — a term and the page(s) where it appears.
|
|
6
|
+
# Part of a back-of-book index section. The adapter extracts
|
|
7
|
+
# these from <index>/<primary> elements; the FlowBuilder renders
|
|
8
|
+
# them alphabetically with page numbers.
|
|
9
|
+
class IndexEntry
|
|
10
|
+
attr_reader :term, :page_numbers, :see_also, :style_id
|
|
11
|
+
|
|
12
|
+
def initialize(term:, page_numbers:, see_also: [], style_id: :index_entry)
|
|
13
|
+
@term = term.to_s
|
|
14
|
+
@page_numbers = Array(page_numbers).map(&:to_i).sort.uniq.freeze
|
|
15
|
+
@see_also = Array(see_also).map(&:to_s).freeze
|
|
16
|
+
@style_id = style_id.to_sym
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_page(number)
|
|
21
|
+
return self if @page_numbers.include?(number.to_i)
|
|
22
|
+
|
|
23
|
+
self.class.new(
|
|
24
|
+
term: @term,
|
|
25
|
+
page_numbers: @page_numbers + [number.to_i],
|
|
26
|
+
see_also: @see_also,
|
|
27
|
+
style_id: @style_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def first_letter
|
|
32
|
+
@term[0]&.upcase || ''
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def page_numbers_str
|
|
36
|
+
@page_numbers.join(', ')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def ==(other)
|
|
40
|
+
other.is_a?(self.class) &&
|
|
41
|
+
term == other.term &&
|
|
42
|
+
page_numbers == other.page_numbers &&
|
|
43
|
+
see_also == other.see_also
|
|
44
|
+
end
|
|
45
|
+
alias eql? ==
|
|
46
|
+
|
|
47
|
+
def hash
|
|
48
|
+
[self.class, term, page_numbers, see_also].hash
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A single inline run of text within a paragraph. Carries the
|
|
6
|
+
# +text+ to render, the +style_id+ for visual styling (bold,
|
|
7
|
+
# italic, etc.), optional +href+ for hyperlinks, and optional
|
|
8
|
+
# +baseline_shift+ / +font_size_scale+ for subscript/superscript
|
|
9
|
+
# positioning (used by MathML msub/msup rendering).
|
|
10
|
+
class InlineRun
|
|
11
|
+
BASELINE_NORMAL = nil
|
|
12
|
+
BASELINE_SUB = :sub
|
|
13
|
+
BASELINE_SUP = :sup
|
|
14
|
+
|
|
15
|
+
attr_reader :text, :style_id, :href, :baseline_shift, :font_size_scale
|
|
16
|
+
|
|
17
|
+
def initialize(text, style_id: :inline, href: nil,
|
|
18
|
+
baseline_shift: BASELINE_NORMAL, font_size_scale: 1.0)
|
|
19
|
+
@text = text.to_s
|
|
20
|
+
@style_id = style_id.to_sym
|
|
21
|
+
@href = href
|
|
22
|
+
@baseline_shift = baseline_shift
|
|
23
|
+
@font_size_scale = font_size_scale.to_f
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ==(other)
|
|
28
|
+
other.is_a?(self.class) &&
|
|
29
|
+
text == other.text &&
|
|
30
|
+
style_id == other.style_id &&
|
|
31
|
+
href == other.href &&
|
|
32
|
+
baseline_shift == other.baseline_shift &&
|
|
33
|
+
font_size_scale == other.font_size_scale
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias eql? ==
|
|
37
|
+
|
|
38
|
+
def hash
|
|
39
|
+
[self.class, text, style_id, href, baseline_shift, font_size_scale].hash
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def subscript?
|
|
43
|
+
@baseline_shift == BASELINE_SUB
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def superscript?
|
|
47
|
+
@baseline_shift == BASELINE_SUP
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
class List
|
|
6
|
+
attr_reader :items, :kind, :style_id, :id
|
|
7
|
+
|
|
8
|
+
def initialize(items = [], kind: :bullet, style_id: :list, id: nil)
|
|
9
|
+
@items = items.map { |i| i.is_a?(Item) ? i : Item.new(i) }.freeze
|
|
10
|
+
@kind = kind.to_sym
|
|
11
|
+
@style_id = style_id.to_sym
|
|
12
|
+
@id = id
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ordered? = @kind == :ordered
|
|
17
|
+
def bullet? = @kind == :bullet
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(self.class) &&
|
|
21
|
+
items == other.items &&
|
|
22
|
+
kind == other.kind &&
|
|
23
|
+
style_id == other.style_id &&
|
|
24
|
+
id == other.id
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias eql? ==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[self.class, items, kind, style_id, id].hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Item
|
|
34
|
+
attr_reader :content, :marker
|
|
35
|
+
|
|
36
|
+
def initialize(content, marker: nil)
|
|
37
|
+
@content = Array(content).freeze
|
|
38
|
+
@marker = marker
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def text
|
|
43
|
+
@content.map do |node|
|
|
44
|
+
node.is_a?(Paragraph) ? node.text : node.to_s
|
|
45
|
+
end.join(' ')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ==(other)
|
|
49
|
+
other.is_a?(self.class) && content == other.content && marker == other.marker
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias eql? ==
|
|
53
|
+
|
|
54
|
+
def hash
|
|
55
|
+
[self.class, content, marker].hash
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A semantic note group: optional label ("NOTE 1", "WARNING") plus
|
|
6
|
+
# one or more body Paragraphs. Distinct from a bare Paragraph
|
|
7
|
+
# because the flow builder renders it with a hanging indent and
|
|
8
|
+
# the label in a marker column (matching the XSL's fo:list-block
|
|
9
|
+
# rendering for notes/examples).
|
|
10
|
+
#
|
|
11
|
+
# Wraps the legacy Paragraph-with-`:note`-style emission so flavors
|
|
12
|
+
# see richer structure without breaking older flow builders.
|
|
13
|
+
class Note
|
|
14
|
+
attr_reader :label, :body, :id, :style_id
|
|
15
|
+
|
|
16
|
+
def initialize(body:, label: '', id: nil, style_id: :note)
|
|
17
|
+
@label = label.to_s
|
|
18
|
+
@body = Array(body).freeze
|
|
19
|
+
@id = id&.to_s
|
|
20
|
+
@style_id = style_id.to_sym
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def body_text
|
|
25
|
+
@body.map do |node|
|
|
26
|
+
node.is_a?(Paragraph) ? node.text : node.to_s
|
|
27
|
+
end.join(' ')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def empty?
|
|
31
|
+
@body.empty? || @body.all? { |node| node.is_a?(Paragraph) && node.empty? }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
other.is_a?(self.class) &&
|
|
36
|
+
label == other.label &&
|
|
37
|
+
body == other.body &&
|
|
38
|
+
id == other.id &&
|
|
39
|
+
style_id == other.style_id
|
|
40
|
+
end
|
|
41
|
+
alias eql? ==
|
|
42
|
+
|
|
43
|
+
def hash
|
|
44
|
+
[self.class, label, body, id, style_id].hash
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# An explicit page break in the content tree. Emitted by the
|
|
6
|
+
# adapter when it encounters a <pagebreak/> element or a clause
|
|
7
|
+
# with a page-break-before hint. The FlowBuilder converts this
|
|
8
|
+
# to a Flowables::PageBreak.
|
|
9
|
+
class PageBreak
|
|
10
|
+
attr_reader :id
|
|
11
|
+
|
|
12
|
+
def initialize(id: nil)
|
|
13
|
+
@id = id&.to_s
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ==(other)
|
|
18
|
+
other.is_a?(self.class) && id == other.id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias eql? ==
|
|
22
|
+
|
|
23
|
+
def hash
|
|
24
|
+
[self.class, id].hash
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
class Paragraph
|
|
6
|
+
attr_reader :inline_runs, :style_id, :id, :footnote_refs
|
|
7
|
+
|
|
8
|
+
def initialize(inline_runs = [], style_id: :body, id: nil, footnote_refs: [])
|
|
9
|
+
@inline_runs = Array(inline_runs).freeze
|
|
10
|
+
@style_id = style_id.to_sym
|
|
11
|
+
@id = id
|
|
12
|
+
@footnote_refs = Array(footnote_refs).freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def text
|
|
17
|
+
@inline_runs.map(&:text).join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def empty?
|
|
21
|
+
@inline_runs.empty? || text.strip.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ==(other)
|
|
25
|
+
other.is_a?(self.class) &&
|
|
26
|
+
inline_runs == other.inline_runs &&
|
|
27
|
+
style_id == other.style_id &&
|
|
28
|
+
id == other.id &&
|
|
29
|
+
footnote_refs == other.footnote_refs
|
|
30
|
+
end
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[self.class, inline_runs, style_id, id, footnote_refs].hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A preformatted text block. Preserves whitespace (spaces, tabs,
|
|
6
|
+
# newlines) and renders in monospace font without justification.
|
|
7
|
+
# Used for code blocks, ASCII art, and any content where the
|
|
8
|
+
# visual layout of whitespace matters.
|
|
9
|
+
#
|
|
10
|
+
# Each line becomes a separate inline run in the paragraph so
|
|
11
|
+
# the renderer can apply line-by-line font metrics without the
|
|
12
|
+
# greedy line breaker collapsing internal whitespace.
|
|
13
|
+
class Preformatted
|
|
14
|
+
attr_reader :lines, :language, :style_id, :id
|
|
15
|
+
|
|
16
|
+
def initialize(lines, language: nil, style_id: :preformatted, id: nil)
|
|
17
|
+
@lines = Array(lines).map(&:to_s).freeze
|
|
18
|
+
@language = language&.to_s
|
|
19
|
+
@style_id = style_id.to_sym
|
|
20
|
+
@id = id&.to_s
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def text
|
|
25
|
+
@lines.join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def empty?
|
|
29
|
+
@lines.empty? || @lines.all?(&:empty?)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ==(other)
|
|
33
|
+
other.is_a?(self.class) &&
|
|
34
|
+
lines == other.lines &&
|
|
35
|
+
language == other.language &&
|
|
36
|
+
style_id == other.style_id &&
|
|
37
|
+
id == other.id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
alias eql? ==
|
|
41
|
+
|
|
42
|
+
def hash
|
|
43
|
+
[self.class, lines, language, style_id, id].hash
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
class Section
|
|
6
|
+
attr_reader :title, :level, :number, :id, :children, :style_id, :title_style_id
|
|
7
|
+
|
|
8
|
+
def initialize(title: nil, level: 1, number: nil, id: nil,
|
|
9
|
+
children: [], style_id: :section_body, title_style_id: nil)
|
|
10
|
+
@title = title
|
|
11
|
+
@level = level.to_i
|
|
12
|
+
@number = number
|
|
13
|
+
@id = id
|
|
14
|
+
@children = Array(children).freeze
|
|
15
|
+
@style_id = style_id.to_sym
|
|
16
|
+
@title_style_id = (title_style_id || default_title_style).to_sym
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def heading? = !@title.nil?
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(self.class) &&
|
|
24
|
+
title == other.title && level == other.level && number == other.number &&
|
|
25
|
+
id == other.id && children == other.children &&
|
|
26
|
+
style_id == other.style_id && title_style_id == other.title_style_id
|
|
27
|
+
end
|
|
28
|
+
alias eql? ==
|
|
29
|
+
|
|
30
|
+
def hash
|
|
31
|
+
[self.class, title, level, number, id, children, style_id, title_style_id].hash
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def default_title_style = "heading_#{@level}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|