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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flavor
|
|
5
|
+
# Flavor registry. Flavors register their pipeline class at
|
|
6
|
+
# load time; applications look up flavors by name. This enables
|
|
7
|
+
# multiple flavors to coexist without
|
|
8
|
+
# the core engine knowing about any of them.
|
|
9
|
+
#
|
|
10
|
+
# A flavor's pipeline class must respond to:
|
|
11
|
+
# .render(xml, io:, **opts) — render XML to PDF bytes
|
|
12
|
+
#
|
|
13
|
+
# Example:
|
|
14
|
+
# Arrolio::Flavor::Registry.register(:my_flavor, Arrolio::Oiml::Pipeline)
|
|
15
|
+
# Arrolio::Flavor::Registry.for(:my_flavor).render(xml, io: io)
|
|
16
|
+
class Registry
|
|
17
|
+
@flavors = {}
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_reader :flavors
|
|
21
|
+
|
|
22
|
+
# Register a flavor by name. +pipeline_class+ must respond
|
|
23
|
+
# to +.render(xml, io:, **opts)+.
|
|
24
|
+
def register(name, pipeline_class)
|
|
25
|
+
@flavors[name.to_sym] = pipeline_class
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Look up a registered flavor. Returns the pipeline class
|
|
29
|
+
# or raises if not found.
|
|
30
|
+
def for(name)
|
|
31
|
+
key = name.to_sym
|
|
32
|
+
@flavors[key] ||
|
|
33
|
+
raise(ArgumentError,
|
|
34
|
+
"Unknown flavor #{name.inspect}; registered: #{@flavors.keys.inspect}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# All registered flavor names.
|
|
38
|
+
def names
|
|
39
|
+
@flavors.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Is a flavor registered?
|
|
43
|
+
def registered?(name)
|
|
44
|
+
@flavors.key?(name.to_sym)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Clear all registrations — used in specs.
|
|
48
|
+
def reset!
|
|
49
|
+
@flavors.clear
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# Flavor system — allows external document flavors to plug into
|
|
5
|
+
# Arrolio without modifying core. Each flavor lives in its own
|
|
6
|
+
# directory (XSL + 3 generated YAML files) outside the gem and is
|
|
7
|
+
# rendered via Arrolio::ConfigDrivenPipeline.
|
|
8
|
+
#
|
|
9
|
+
# Arrolio core is flavor-agnostic: it knows about Content::Document,
|
|
10
|
+
# Flowable, Engine, and Renderer — but NOT about any specific XML
|
|
11
|
+
# format, XSL stylesheet, or document structure.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# # In the flavor's gem:
|
|
15
|
+
# Arrolio::Flavor::Registry.register(:my_flavor, MyFlavorPipeline)
|
|
16
|
+
#
|
|
17
|
+
# # In the application:
|
|
18
|
+
# Arrolio::Flavor::Registry.for(:my_flavor).render(xml, io: f)
|
|
19
|
+
module Flavor
|
|
20
|
+
autoload :Registry, 'arrolio/flavor/registry'
|
|
21
|
+
autoload :Manifest, 'arrolio/flavor/manifest'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# State threaded through every flowable fit/emit call. Holds
|
|
5
|
+
# page-level state flowables need: current page number, total
|
|
6
|
+
# page count (filled in between pass 1 and pass 2), the running
|
|
7
|
+
# citation table, and the LayoutSpec for style resolution.
|
|
8
|
+
class FlowContext
|
|
9
|
+
attr_accessor :page_number, :page_count, :renderer
|
|
10
|
+
attr_reader :layout_spec, :citations, :heading_entries
|
|
11
|
+
|
|
12
|
+
def initialize(layout_spec:, page_number: 1, page_count: nil)
|
|
13
|
+
@layout_spec = layout_spec
|
|
14
|
+
@page_number = page_number
|
|
15
|
+
@page_count = page_count
|
|
16
|
+
@citations = {}
|
|
17
|
+
@heading_entries = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def citation_for(ref_id)
|
|
21
|
+
@citations[ref_id.to_s]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def record_citation(ref_id, page_number)
|
|
25
|
+
@citations[ref_id.to_s] ||= page_number
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_heading(number:, title:, level:, page_number:, id: nil)
|
|
29
|
+
@heading_entries << { number: number, title: title, level: level,
|
|
30
|
+
page_number: page_number, id: id }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def total_pages
|
|
34
|
+
@page_count
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arrolio::Flowable — abstract base for everything the page-flow
|
|
4
|
+
# engine places into Frames.
|
|
5
|
+
module Arrolio
|
|
6
|
+
class Flowable
|
|
7
|
+
attr_reader :style
|
|
8
|
+
|
|
9
|
+
def initialize(style: Style::Definition.new)
|
|
10
|
+
@style = style
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def height(_width, _context = nil)
|
|
14
|
+
raise NotImplementedError, "#{self.class}#height not implemented"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def emit(_x, _y, _width, _context = nil)
|
|
18
|
+
raise NotImplementedError, "#{self.class}#emit not implemented"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def splittable?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def keep_together?
|
|
26
|
+
@style.keep_together
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def keep_with_next?
|
|
30
|
+
@style.keep_with_next
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def page_break_before?
|
|
34
|
+
@style.page_break_before
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def page_break_after?
|
|
38
|
+
@style.page_break_after
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def space_before
|
|
42
|
+
@style.margin_top
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def space_after
|
|
46
|
+
@style.margin_bottom
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def split(width, remaining_height, context = nil)
|
|
50
|
+
natural = height(width, context)
|
|
51
|
+
if natural <= remaining_height
|
|
52
|
+
[self, nil]
|
|
53
|
+
elsif splittable?
|
|
54
|
+
do_split(width, remaining_height, context)
|
|
55
|
+
elsif keep_together?
|
|
56
|
+
[nil, self]
|
|
57
|
+
else
|
|
58
|
+
[self, nil]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def do_split(_width, _remaining_height, _context = nil)
|
|
63
|
+
[self, nil]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Zero-height placeholder that signals "the footnote with this id
|
|
6
|
+
# appears on the current page". The engine detects these during
|
|
7
|
+
# layout and records the footnote on the current Output::Page so
|
|
8
|
+
# the renderer can draw it at the bottom of the page.
|
|
9
|
+
#
|
|
10
|
+
# Carries a reference to the full Content::Footnote (marker +
|
|
11
|
+
# body). The marker is also rendered inline (as a superscript
|
|
12
|
+
# indicator) by the flow builder when it constructs the run
|
|
13
|
+
# sequence; this flowable's sole job is to anchor the footnote
|
|
14
|
+
# to a page.
|
|
15
|
+
class FootnoteMarkerFlowable < Flowable
|
|
16
|
+
attr_reader :footnote
|
|
17
|
+
|
|
18
|
+
def initialize(footnote)
|
|
19
|
+
super(style: Style::Definition.new)
|
|
20
|
+
@footnote = footnote
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def height(_width, _context = nil) = 0.0
|
|
24
|
+
|
|
25
|
+
def emit(_x, _y, _width, _context = nil) = [[], 0.0]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# A section heading: optional number + title text. Lives
|
|
6
|
+
# separate from TextFlowable so the adapter and Engine can
|
|
7
|
+
# recognise headings by type (for outline / TOC building).
|
|
8
|
+
class HeadingFlowable < TextFlowable
|
|
9
|
+
attr_reader :level, :number, :title, :id
|
|
10
|
+
|
|
11
|
+
def initialize(title, level: 1, number: nil, id: nil,
|
|
12
|
+
style: Style::Definition.new, measurer: nil)
|
|
13
|
+
full_text = number ? "#{number} #{title}" : title.to_s
|
|
14
|
+
super([InlineRun.new(full_text, style: style)],
|
|
15
|
+
style: style, measurer: measurer)
|
|
16
|
+
@title = title
|
|
17
|
+
@level = level.to_i
|
|
18
|
+
@number = number
|
|
19
|
+
@id = id
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Renders a PNG / JPEG / SVG via Pdfrb's image registry. Scales
|
|
6
|
+
# to +display_width+ while preserving aspect ratio; if
|
|
7
|
+
# +display_width+ is nil, scales to fit the available width.
|
|
8
|
+
#
|
|
9
|
+
# Image registration is deferred to the render pass: the PlacedBox
|
|
10
|
+
# carries the source path, and the renderer resolves it (including
|
|
11
|
+
# SVG-to-PNG rasterization if needed) when it walks the page tree.
|
|
12
|
+
class ImageFlowable < Flowable
|
|
13
|
+
attr_reader :src, :natural_width, :natural_height, :display_width, :alt
|
|
14
|
+
|
|
15
|
+
def initialize(src, natural_width:, natural_height:,
|
|
16
|
+
display_width: nil, alt: nil,
|
|
17
|
+
style: Style::Definition.new)
|
|
18
|
+
super(style: style)
|
|
19
|
+
@src = src.to_s
|
|
20
|
+
@natural_width = natural_width.to_f
|
|
21
|
+
@natural_height = natural_height.to_f
|
|
22
|
+
@display_width = display_width
|
|
23
|
+
@alt = alt
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def height(width, _context = nil)
|
|
27
|
+
return 0.0 if @natural_width.zero?
|
|
28
|
+
|
|
29
|
+
actual_w = actual_width(width)
|
|
30
|
+
actual_w * (@natural_height / @natural_width)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def emit(x, y, width, _context = nil)
|
|
34
|
+
return [[], 0.0] if @natural_width.zero?
|
|
35
|
+
|
|
36
|
+
actual_w = actual_width(width)
|
|
37
|
+
actual_h = actual_w * (@natural_height / @natural_width)
|
|
38
|
+
box = Output::PlacedBox.image(
|
|
39
|
+
x: x, y: y - actual_h,
|
|
40
|
+
width: actual_w, height: actual_h,
|
|
41
|
+
image_ref: @src
|
|
42
|
+
)
|
|
43
|
+
[[box], actual_h]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def actual_width(width)
|
|
49
|
+
@display_width && @display_width < width ? @display_width : width
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Hanging-indent list. Each item has a marker column (fixed
|
|
6
|
+
# width) and a body column (wraps under body, not marker).
|
|
7
|
+
class ListFlowable < Flowable
|
|
8
|
+
attr_reader :items, :kind, :marker_width, :body_indent
|
|
9
|
+
|
|
10
|
+
def initialize(items, kind: :bullet, style: Style::Definition.new,
|
|
11
|
+
marker_width: 18.0, body_indent: 6.0)
|
|
12
|
+
super(style: style)
|
|
13
|
+
@kind = kind.to_sym
|
|
14
|
+
@marker_width = marker_width.to_f
|
|
15
|
+
@body_indent = body_indent.to_f
|
|
16
|
+
@items = items
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def height(width, context = nil)
|
|
21
|
+
@items.sum do |item|
|
|
22
|
+
_, body_flowables = item
|
|
23
|
+
body_width = width - @marker_width - @body_indent
|
|
24
|
+
body_flowables.sum { |f| f.height(body_width, context) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit(x, y, width, context = nil)
|
|
29
|
+
boxes = []
|
|
30
|
+
consumed = 0.0
|
|
31
|
+
cursor = y
|
|
32
|
+
body_x = x + @marker_width + @body_indent
|
|
33
|
+
body_width = width - @marker_width - @body_indent
|
|
34
|
+
|
|
35
|
+
@items.each_with_index do |item, idx|
|
|
36
|
+
marker_text, body_flowables = item
|
|
37
|
+
marker = marker_text || default_marker(idx)
|
|
38
|
+
|
|
39
|
+
if marker.is_a?(String)
|
|
40
|
+
if marker.empty?
|
|
41
|
+
# no marker to draw
|
|
42
|
+
else
|
|
43
|
+
mboxes, = marker_flowable(marker, x, cursor, width, context).emit(x, cursor, width, context)
|
|
44
|
+
boxes.concat(mboxes)
|
|
45
|
+
end
|
|
46
|
+
elsif marker
|
|
47
|
+
# marker is a Flowable (e.g. NoteFlowable passes a TextFlowable)
|
|
48
|
+
mboxes, = marker.emit(x, cursor, width, context)
|
|
49
|
+
boxes.concat(mboxes)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
body_flowables.each do |f|
|
|
53
|
+
fh = f.height(body_width, context)
|
|
54
|
+
fboxes, = f.emit(body_x, cursor, body_width, context)
|
|
55
|
+
boxes.concat(fboxes)
|
|
56
|
+
cursor -= fh
|
|
57
|
+
consumed += fh
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[boxes, consumed]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def splittable?
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def do_split(width, remaining_height, context = nil)
|
|
69
|
+
head_items = []
|
|
70
|
+
tail_items = []
|
|
71
|
+
used = 0.0
|
|
72
|
+
|
|
73
|
+
@items.each do |item|
|
|
74
|
+
_, body_flowables = item
|
|
75
|
+
item_h = body_flowables.sum { |f| f.height(width - @marker_width - @body_indent, context) }
|
|
76
|
+
if used + item_h <= remaining_height || head_items.empty?
|
|
77
|
+
head_items << item
|
|
78
|
+
used += item_h
|
|
79
|
+
else
|
|
80
|
+
tail_items << item
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
head = if head_items.empty?
|
|
85
|
+
nil
|
|
86
|
+
else
|
|
87
|
+
self.class.new(head_items, kind: @kind, style: @style,
|
|
88
|
+
marker_width: @marker_width,
|
|
89
|
+
body_indent: @body_indent)
|
|
90
|
+
end
|
|
91
|
+
tail = if tail_items.empty?
|
|
92
|
+
nil
|
|
93
|
+
else
|
|
94
|
+
self.class.new(tail_items, kind: @kind, style: @style,
|
|
95
|
+
marker_width: @marker_width,
|
|
96
|
+
body_indent: @body_indent)
|
|
97
|
+
end
|
|
98
|
+
[head, tail]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def default_marker(idx)
|
|
104
|
+
case @kind
|
|
105
|
+
when :ordered then "#{idx + 1}."
|
|
106
|
+
when :bullet then "\u25A0"
|
|
107
|
+
else "\u2022"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def marker_flowable(text, _x, _y, _w, _ctx)
|
|
112
|
+
marker_style = @style.with(font_size: @style.font_size * 0.6)
|
|
113
|
+
TextFlowable.new([InlineRun.new(text, style: marker_style)], style: marker_style)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Note / example callout: italic label in marker column, body
|
|
6
|
+
# indented. Reuses ListFlowable's hanging-indent infrastructure.
|
|
7
|
+
class NoteFlowable < ListFlowable
|
|
8
|
+
def initialize(label_text, body_flowables, style: Style::Definition.new,
|
|
9
|
+
marker_width: 22.0, body_indent: 6.0)
|
|
10
|
+
label_style = style.with(font_size: style.font_size)
|
|
11
|
+
marker_run = InlineRun.new("#{label_text} ", style: label_style)
|
|
12
|
+
marker_flow = TextFlowable.new([marker_run], style: label_style)
|
|
13
|
+
super([[marker_flow, body_flowables]],
|
|
14
|
+
kind: :note, style: style,
|
|
15
|
+
marker_width: marker_width, body_indent: body_indent)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
class PageBreak < Flowable
|
|
6
|
+
def height(_width, _context = nil) = 0.0
|
|
7
|
+
|
|
8
|
+
def emit(_x, _y, _width, _context = nil) = [[], 0.0]
|
|
9
|
+
|
|
10
|
+
def page_break_after?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Forces a page break AND switches the active page sequence:
|
|
6
|
+
# role + header/footer templates apply to subsequent pages
|
|
7
|
+
# until the next PageSequenceStart.
|
|
8
|
+
class PageSequenceStart < Flowable
|
|
9
|
+
attr_reader :role, :header_template, :footer_template,
|
|
10
|
+
:header_align, :footer_align, :initial_page_number
|
|
11
|
+
|
|
12
|
+
def initialize(role:, header_template: nil, footer_template: nil,
|
|
13
|
+
header_align: :right, footer_align: :center,
|
|
14
|
+
initial_page_number: nil)
|
|
15
|
+
@role = role.to_sym
|
|
16
|
+
@header_template = header_template
|
|
17
|
+
@footer_template = footer_template
|
|
18
|
+
@header_align = header_align.to_sym
|
|
19
|
+
@footer_align = footer_align.to_sym
|
|
20
|
+
@initial_page_number = initial_page_number
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def height(_width, _context = nil) = 0.0
|
|
24
|
+
|
|
25
|
+
def emit(_x, _y, _width, _context = nil) = [[], 0.0]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
class Spacer < Flowable
|
|
6
|
+
attr_reader :amount
|
|
7
|
+
|
|
8
|
+
def initialize(amount, style: Style::Definition.new)
|
|
9
|
+
super(style: style)
|
|
10
|
+
@amount = amount.to_f
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def height(_width, _context = nil)
|
|
14
|
+
@amount
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def emit(_x, _y, _width, _context = nil)
|
|
18
|
+
[[], @amount]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def splittable?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def do_split(_width, remaining_height, _context = nil)
|
|
26
|
+
if @amount <= remaining_height
|
|
27
|
+
[self, nil]
|
|
28
|
+
else
|
|
29
|
+
[self.class.new(remaining_height), self.class.new(@amount - remaining_height)]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Flowables
|
|
5
|
+
# Real table layout: equal-width columns by default (TODO 04
|
|
6
|
+
# adds auto/fixed), borders per cell, header row repeat.
|
|
7
|
+
class TableFlowable < Flowable
|
|
8
|
+
attr_reader :table, :column_widths
|
|
9
|
+
|
|
10
|
+
def initialize(table, style: Style::Definition.new)
|
|
11
|
+
super(style: style)
|
|
12
|
+
@table = table
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def height(width, _context = nil)
|
|
16
|
+
cols = column_widths_for(width)
|
|
17
|
+
total = 0.0
|
|
18
|
+
rows.each do |row|
|
|
19
|
+
total += row_height(row, cols)
|
|
20
|
+
end
|
|
21
|
+
total
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def emit(x, y, width, context = nil)
|
|
25
|
+
cols = column_widths_for(width)
|
|
26
|
+
boxes = []
|
|
27
|
+
consumed = 0.0
|
|
28
|
+
cursor = y
|
|
29
|
+
header_rows, body_rows = partition_rows
|
|
30
|
+
|
|
31
|
+
(header_rows + body_rows).each do |row|
|
|
32
|
+
rh = row_height(row, cols)
|
|
33
|
+
cursor -= rh
|
|
34
|
+
consumed += rh
|
|
35
|
+
cell_x = x.to_f
|
|
36
|
+
row.cells.each_with_index do |cell, ci|
|
|
37
|
+
cw = cols[ci] || (width / [row.cells.length, 1].max)
|
|
38
|
+
render_cell(cell, cell_x, cursor, cw, rh, boxes, context)
|
|
39
|
+
cell_x += cw
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
[boxes, consumed]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def splittable?
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def do_split(width, remaining_height, _context = nil)
|
|
51
|
+
cols = column_widths_for(width)
|
|
52
|
+
head_rows = []
|
|
53
|
+
tail_rows = []
|
|
54
|
+
used = 0.0
|
|
55
|
+
header, body = partition_rows
|
|
56
|
+
|
|
57
|
+
body.each do |row|
|
|
58
|
+
rh = row_height(row, cols)
|
|
59
|
+
if used + rh <= remaining_height || head_rows.empty?
|
|
60
|
+
head_rows << row
|
|
61
|
+
used += rh
|
|
62
|
+
else
|
|
63
|
+
tail_rows << row
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if head_rows.any?
|
|
68
|
+
head_table = rebuild_table(header, head_rows)
|
|
69
|
+
head = self.class.new(head_table, style: @style)
|
|
70
|
+
else
|
|
71
|
+
head = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
tail_table = rebuild_table(header, tail_rows)
|
|
75
|
+
tail = self.class.new(tail_table, style: @style)
|
|
76
|
+
[head, tail]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def rows
|
|
82
|
+
@table.rows
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def partition_rows
|
|
86
|
+
[@table.header, @table.body]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def column_widths_for(width)
|
|
90
|
+
return Array.new(@table.column_count, width.to_f / [@table.column_count, 1].max) if @table.column_count.zero?
|
|
91
|
+
|
|
92
|
+
@cached_widths ||= Table::AutoLayout.new(@table, available_width: width).compute
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def row_height(row, cols)
|
|
96
|
+
max_h = 0.0
|
|
97
|
+
row.cells.each_with_index do |cell, ci|
|
|
98
|
+
cw = cols[ci] || 50.0
|
|
99
|
+
content_h = cell.content.sum do |node|
|
|
100
|
+
paragraph_height(node, cw)
|
|
101
|
+
end
|
|
102
|
+
max_h = [max_h, content_h].max
|
|
103
|
+
end
|
|
104
|
+
max_h + 4.0
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def paragraph_height(paragraph, width)
|
|
108
|
+
return 0.0 unless paragraph.is_a?(Content::Paragraph)
|
|
109
|
+
|
|
110
|
+
style = cell_style(paragraph)
|
|
111
|
+
runs = paragraph.inline_runs.map { |r| InlineRun.new(r.text, style: style) }
|
|
112
|
+
TextFlowable.new(runs, style: style).height(width)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def render_cell(cell, x, y, w, h, boxes, _context)
|
|
116
|
+
style = cell_style(cell)
|
|
117
|
+
runs = []
|
|
118
|
+
cell.content.each do |node|
|
|
119
|
+
if node.is_a?(Content::Paragraph)
|
|
120
|
+
runs.concat(node.inline_runs.map { |r| InlineRun.new(r.text, style: style) })
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
if runs.any?
|
|
124
|
+
tf = TextFlowable.new(runs, style: style)
|
|
125
|
+
tboxes, = tf.emit(x + 2, y + h - 2, w - 4, nil)
|
|
126
|
+
boxes.concat(tboxes)
|
|
127
|
+
end
|
|
128
|
+
# Cell border: rectangle around the full cell.
|
|
129
|
+
boxes << Output::PlacedBox.rect(x: x, y: y, width: w, height: h,
|
|
130
|
+
stroke_width: 0.5, stroke_color: 'black')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def rebuild_table(header, body)
|
|
134
|
+
Content::Table.new(header: header.map { |r| r.cells.map(&:text) },
|
|
135
|
+
body: body.map { |r| r.cells.map(&:text) })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Use the parent table's style (which inherits from body →
|
|
139
|
+
# Times New Roman) rather than hard-coding Times-Roman.
|
|
140
|
+
# Falls back to Times-Roman only if the table style has no
|
|
141
|
+
# font_name (shouldn't happen in practice).
|
|
142
|
+
def cell_style(_cell_or_para)
|
|
143
|
+
@style.font_name ? @style : Style::Definition.new(font_name: 'Times-Roman', font_size: 10)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|