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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Renderer
|
|
5
|
+
# Builds an XMP metadata packet (RDF/XML wrapped in xpacket
|
|
6
|
+
# processing instructions). XMP carries Dublin Core properties
|
|
7
|
+
# (dc:title, dc:creator) alongside PDF-specific properties
|
|
8
|
+
# (pdf:Producer, xmp:CreatorTool). The packet is stored as a
|
|
9
|
+
# stream on the PDF catalog's /Metadata entry.
|
|
10
|
+
class XmpBuilder
|
|
11
|
+
XPACKET_BEGIN = '<?xpacket begin="\xEF\xBB\xBF" id="W5M0MpCehiHzreSzNTczkc9d"?>'
|
|
12
|
+
XPACKET_END = '<?xpacket end="w"?>'
|
|
13
|
+
|
|
14
|
+
attr_reader :metadata
|
|
15
|
+
|
|
16
|
+
def initialize(metadata = {})
|
|
17
|
+
@metadata = metadata
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build
|
|
21
|
+
[XPACKET_BEGIN, xmp_body, XPACKET_END].join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def xmp_body
|
|
27
|
+
%(<x:xmpmeta xmlns:x="adobe:ns:meta/">) +
|
|
28
|
+
%(<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">) +
|
|
29
|
+
rdf_descriptions +
|
|
30
|
+
%(</rdf:RDF>) +
|
|
31
|
+
%(</x:xmpmeta>)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rdf_descriptions
|
|
35
|
+
[dc_description, pdf_description, xmp_description].compact.join
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dc_description
|
|
39
|
+
return '' unless @metadata[:title] || @metadata[:author]
|
|
40
|
+
|
|
41
|
+
inner = []
|
|
42
|
+
if @metadata[:title]
|
|
43
|
+
inner << (%(<dc:title><rdf:Alt><rdf:li xml:lang="x-default">) +
|
|
44
|
+
escape(@metadata[:title].to_s) +
|
|
45
|
+
%(</rdf:li></rdf:Alt></dc:title>))
|
|
46
|
+
end
|
|
47
|
+
if @metadata[:author]
|
|
48
|
+
inner << (%(<dc:creator><rdf:Seq><rdf:li>) +
|
|
49
|
+
escape(@metadata[:author].to_s) +
|
|
50
|
+
%(</rdf:li></rdf:Seq></dc:creator>))
|
|
51
|
+
end
|
|
52
|
+
%(<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">) +
|
|
53
|
+
inner.join +
|
|
54
|
+
%(</rdf:Description>)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pdf_description
|
|
58
|
+
producer = escape(@metadata[:producer] || 'Arrolio + Pdfrb')
|
|
59
|
+
%(<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">) +
|
|
60
|
+
%(<pdf:Producer>) + producer + %(</pdf:Producer>) +
|
|
61
|
+
%(</rdf:Description>)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def xmp_description
|
|
65
|
+
creator_tool = escape(@metadata[:creator_tool] || 'Arrolio (Ruby)')
|
|
66
|
+
create_date = xmp_date(@metadata[:created_at])
|
|
67
|
+
modify_date = xmp_date(@metadata[:modified_at])
|
|
68
|
+
%(<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">) +
|
|
69
|
+
%(<xmp:CreatorTool>) + creator_tool + %(</xmp:CreatorTool>) +
|
|
70
|
+
(create_date ? %(<xmp:CreateDate>) + create_date + %(</xmp:CreateDate>) : '') +
|
|
71
|
+
(modify_date ? %(<xmp:ModifyDate>) + modify_date + %(</xmp:ModifyDate>) : '') +
|
|
72
|
+
%(</rdf:Description>)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def xmp_date(time)
|
|
76
|
+
t = time || Time.now
|
|
77
|
+
t = Time.parse(t.to_s) unless t.is_a?(Time)
|
|
78
|
+
t.strftime('%Y-%m-%dT%H:%M:%S%:z')
|
|
79
|
+
rescue StandardError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def escape(str)
|
|
84
|
+
str.to_s
|
|
85
|
+
.gsub('&', '&')
|
|
86
|
+
.gsub('<', '<')
|
|
87
|
+
.gsub('>', '>')
|
|
88
|
+
.gsub('"', '"')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
# Arrolio::Renderer — walks Output::Page[] and emits bytes.
|
|
6
|
+
module Arrolio
|
|
7
|
+
module Renderer
|
|
8
|
+
autoload :Pdf, 'arrolio/renderer/pdf'
|
|
9
|
+
autoload :FontRegistry, 'arrolio/renderer/font_registry'
|
|
10
|
+
autoload :OutlineBuilder, 'arrolio/renderer/outline_builder'
|
|
11
|
+
autoload :XmpBuilder, 'arrolio/renderer/xmp_builder'
|
|
12
|
+
autoload :AccessibilityTagger, 'arrolio/renderer/accessibility_tagger'
|
|
13
|
+
autoload :LinkAnnotator, 'arrolio/renderer/link_annotator'
|
|
14
|
+
autoload :StructureTreeBuilder, 'arrolio/renderer/structure_tree_builder'
|
|
15
|
+
autoload :SignatureConfig, 'arrolio/renderer/signature_config'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Style
|
|
5
|
+
class Definition
|
|
6
|
+
attr_reader :font_name, :font_size, :fill_color, :stroke_color,
|
|
7
|
+
:line_spacing, :align, :valign,
|
|
8
|
+
:margin_top, :margin_right, :margin_bottom, :margin_left,
|
|
9
|
+
:padding_top, :padding_right, :padding_bottom, :padding_left,
|
|
10
|
+
:character_spacing, :word_spacing, :line_break,
|
|
11
|
+
:keep_together, :page_break_before, :page_break_after,
|
|
12
|
+
:keep_with_next, :widows, :orphans, :parent
|
|
13
|
+
|
|
14
|
+
DEFAULTS = {
|
|
15
|
+
font_name: 'Helvetica', font_size: 12.0,
|
|
16
|
+
fill_color: nil, stroke_color: nil,
|
|
17
|
+
line_spacing: 1.2, align: :left, valign: :top,
|
|
18
|
+
margin_top: 0.0, margin_right: 0.0, margin_bottom: 0.0, margin_left: 0.0,
|
|
19
|
+
padding_top: 0.0, padding_right: 0.0, padding_bottom: 0.0, padding_left: 0.0,
|
|
20
|
+
character_spacing: 0.0, word_spacing: 0.0,
|
|
21
|
+
line_break: :greedy,
|
|
22
|
+
keep_together: false, page_break_before: false, page_break_after: false,
|
|
23
|
+
keep_with_next: false, widows: 2, orphans: 2, parent: nil
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(**opts)
|
|
27
|
+
merged = DEFAULTS.merge(opts.transform_keys(&:to_sym))
|
|
28
|
+
@font_name = merged.fetch(:font_name).to_s
|
|
29
|
+
@font_size = (merged.fetch(:font_size) || 0).to_f
|
|
30
|
+
@fill_color = merged.fetch(:fill_color)
|
|
31
|
+
@stroke_color = merged.fetch(:stroke_color)
|
|
32
|
+
@line_spacing = (merged.fetch(:line_spacing) || 0).to_f
|
|
33
|
+
@align = merged.fetch(:align).to_sym
|
|
34
|
+
@valign = merged.fetch(:valign).to_sym
|
|
35
|
+
@margin_top = (merged.fetch(:margin_top) || 0).to_f
|
|
36
|
+
@margin_right = (merged.fetch(:margin_right) || 0).to_f
|
|
37
|
+
@margin_bottom = (merged.fetch(:margin_bottom) || 0).to_f
|
|
38
|
+
@margin_left = (merged.fetch(:margin_left) || 0).to_f
|
|
39
|
+
@padding_top = (merged.fetch(:padding_top) || 0).to_f
|
|
40
|
+
@padding_right = (merged.fetch(:padding_right) || 0).to_f
|
|
41
|
+
@padding_bottom = (merged.fetch(:padding_bottom) || 0).to_f
|
|
42
|
+
@padding_left = (merged.fetch(:padding_left) || 0).to_f
|
|
43
|
+
@character_spacing = (merged.fetch(:character_spacing) || 0).to_f
|
|
44
|
+
@word_spacing = (merged.fetch(:word_spacing) || 0).to_f
|
|
45
|
+
@line_break = merged.fetch(:line_break).to_sym
|
|
46
|
+
@keep_together = truthy?(merged.fetch(:keep_together))
|
|
47
|
+
@page_break_before = truthy?(merged.fetch(:page_break_before))
|
|
48
|
+
@page_break_after = truthy?(merged.fetch(:page_break_after))
|
|
49
|
+
@keep_with_next = truthy?(merged.fetch(:keep_with_next))
|
|
50
|
+
@widows = merged.fetch(:widows).to_i
|
|
51
|
+
@orphans = merged.fetch(:orphans).to_i
|
|
52
|
+
@parent = merged.fetch(:parent)
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def with(**opts)
|
|
57
|
+
self.class.new(**to_h, **opts.transform_keys(&:to_sym))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def space_before = @margin_top
|
|
61
|
+
def space_after = @margin_bottom
|
|
62
|
+
|
|
63
|
+
def ==(other)
|
|
64
|
+
other.is_a?(self.class) && to_h == other.to_h
|
|
65
|
+
end
|
|
66
|
+
alias eql? ==
|
|
67
|
+
|
|
68
|
+
def hash = to_h.hash
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
font_name: @font_name, font_size: @font_size,
|
|
73
|
+
fill_color: @fill_color, stroke_color: @stroke_color,
|
|
74
|
+
line_spacing: @line_spacing, align: @align, valign: @valign,
|
|
75
|
+
margin_top: @margin_top, margin_right: @margin_right,
|
|
76
|
+
margin_bottom: @margin_bottom, margin_left: @margin_left,
|
|
77
|
+
padding_top: @padding_top, padding_right: @padding_right,
|
|
78
|
+
padding_bottom: @padding_bottom, padding_left: @padding_left,
|
|
79
|
+
character_spacing: @character_spacing, word_spacing: @word_spacing,
|
|
80
|
+
line_break: @line_break, keep_together: @keep_together,
|
|
81
|
+
page_break_before: @page_break_before, page_break_after: @page_break_after,
|
|
82
|
+
keep_with_next: @keep_with_next, widows: @widows, orphans: @orphans,
|
|
83
|
+
parent: @parent
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def truthy?(v)
|
|
90
|
+
case v
|
|
91
|
+
when true, 'true', 'always' then true
|
|
92
|
+
when Integer then v.positive?
|
|
93
|
+
else false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
module Style
|
|
7
|
+
module Loader
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def load(source)
|
|
11
|
+
Registry.new(load_hash(source) || {})
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def load_hash(source)
|
|
15
|
+
case source
|
|
16
|
+
when Hash then source
|
|
17
|
+
when String then ::YAML.safe_load(source, aliases: true) || {}
|
|
18
|
+
when IO, StringIO then ::YAML.safe_load(source.read, aliases: true) || {}
|
|
19
|
+
else
|
|
20
|
+
raise ArgumentError, 'Loader.load needs a Hash, String, or IO'
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
module Style
|
|
7
|
+
class Registry
|
|
8
|
+
attr_reader :overrides
|
|
9
|
+
|
|
10
|
+
def initialize(overrides = {})
|
|
11
|
+
@overrides = {}
|
|
12
|
+
overrides.each { |name, attrs| register(name, **attrs) }
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(name, **opts)
|
|
17
|
+
@overrides[name.to_sym] = opts.transform_keys(&:to_sym)
|
|
18
|
+
name.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolve(name, fallback: Definition.new)
|
|
22
|
+
key = name.to_sym
|
|
23
|
+
return fallback unless @overrides.key?(key)
|
|
24
|
+
|
|
25
|
+
chain = []
|
|
26
|
+
cursor = key
|
|
27
|
+
seen = Set.new
|
|
28
|
+
while cursor && @overrides.key?(cursor) && seen.add?(cursor)
|
|
29
|
+
chain << @overrides[cursor]
|
|
30
|
+
parent = @overrides[cursor][:parent]
|
|
31
|
+
cursor = parent&.to_sym
|
|
32
|
+
end
|
|
33
|
+
merged = chain.reverse_each.reduce({}) { |acc, h| acc.merge(h) }
|
|
34
|
+
merged.delete(:parent)
|
|
35
|
+
Definition.new(**merged)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def names = @overrides.keys
|
|
39
|
+
def key?(name) = @overrides.key?(name.to_sym)
|
|
40
|
+
def [](name) = @overrides.[](name.to_sym)
|
|
41
|
+
|
|
42
|
+
def ==(other)
|
|
43
|
+
other.is_a?(self.class) && overrides == other.overrides
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
alias eql? ==
|
|
47
|
+
|
|
48
|
+
def hash
|
|
49
|
+
[self.class, overrides].hash
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Table
|
|
5
|
+
# Computes column widths for a Content::Table based on natural
|
|
6
|
+
# content widths. Each column gets at least its natural width
|
|
7
|
+
# (longest unbreakable token + padding). Remaining space is
|
|
8
|
+
# distributed proportionally to the column's natural width, so
|
|
9
|
+
# wider columns get more extra space.
|
|
10
|
+
#
|
|
11
|
+
# If the total natural width exceeds the available width, columns
|
|
12
|
+
# are scaled down proportionally (each gets a share proportional
|
|
13
|
+
# to its natural width, but never below a minimum).
|
|
14
|
+
class AutoLayout
|
|
15
|
+
MIN_COLUMN_WIDTH = 20.0 # points — prevents columns from collapsing
|
|
16
|
+
CELL_PADDING = 8.0 # points — left + right padding per cell
|
|
17
|
+
|
|
18
|
+
attr_reader :table, :available_width, :measurer
|
|
19
|
+
|
|
20
|
+
def initialize(table, available_width:, measurer: nil)
|
|
21
|
+
@table = table
|
|
22
|
+
@available_width = available_width.to_f
|
|
23
|
+
@measurer = measurer || default_measurer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns an Array of Float widths (one per column), summing
|
|
27
|
+
# to +available_width+.
|
|
28
|
+
def compute
|
|
29
|
+
return Array.new(column_count, @available_width / column_count) if column_count.zero?
|
|
30
|
+
|
|
31
|
+
natural = natural_widths
|
|
32
|
+
distribute(natural)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def column_count
|
|
38
|
+
@table.column_count
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Natural width of each column = max cell natural width across rows.
|
|
42
|
+
def natural_widths
|
|
43
|
+
widths = Array.new(column_count, MIN_COLUMN_WIDTH)
|
|
44
|
+
@table.rows.each do |row|
|
|
45
|
+
row.cells.each_with_index do |cell, ci|
|
|
46
|
+
next if ci >= widths.length
|
|
47
|
+
|
|
48
|
+
w = cell_natural_width(cell)
|
|
49
|
+
widths[ci] = w if w > widths[ci]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
widths
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def cell_natural_width(cell)
|
|
56
|
+
longest = cell.text.split(/\s+/).max_by(&:length) || ''
|
|
57
|
+
@measurer.width_of_string(longest, font_size: 10.0) + CELL_PADDING
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Distribute +available_width+ across columns based on their
|
|
61
|
+
# natural widths. If naturals fit, distribute extra space
|
|
62
|
+
# proportionally. If they overflow, scale down proportionally
|
|
63
|
+
# (but never below MIN_COLUMN_WIDTH).
|
|
64
|
+
def distribute(natural)
|
|
65
|
+
total_natural = natural.sum
|
|
66
|
+
if total_natural >= @available_width
|
|
67
|
+
scale_down(natural, total_natural)
|
|
68
|
+
else
|
|
69
|
+
distribute_extra(natural, total_natural)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def scale_down(natural, total_natural)
|
|
74
|
+
ratio = @available_width / total_natural
|
|
75
|
+
natural.map { |w| [w * ratio, MIN_COLUMN_WIDTH].max }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def distribute_extra(natural, total_natural)
|
|
79
|
+
extra = @available_width - total_natural
|
|
80
|
+
natural.map.with_index do |w, _i|
|
|
81
|
+
share = total_natural.positive? ? (w / total_natural) : 0.0
|
|
82
|
+
w + (extra * share)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_measurer
|
|
87
|
+
GlyphMeasurer.new(font_name: 'Times New Roman')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# Unicode text direction detection for BIDI-aware layout. Detects
|
|
5
|
+
# whether a string is predominantly left-to-right (Latin, CJK),
|
|
6
|
+
# right-to-left (Arabic, Hebrew), or mixed. Used by the text
|
|
7
|
+
# layout engine to apply correct character ordering within lines.
|
|
8
|
+
#
|
|
9
|
+
# This module implements the "paragraph level" detection from
|
|
10
|
+
# Unicode UAX #9 (BIDI Algorithm) — a simplified version that
|
|
11
|
+
# suffices for most standards documents. Full BIDI reordering
|
|
12
|
+
# (handling embedded runs, override controls) is future work.
|
|
13
|
+
module TextDirection
|
|
14
|
+
LTR = :ltr
|
|
15
|
+
RTL = :rtl
|
|
16
|
+
MIXED = :mixed
|
|
17
|
+
|
|
18
|
+
# Unicode blocks containing RTL characters.
|
|
19
|
+
RTL_RANGES = [
|
|
20
|
+
0x0590..0x05FF, # Hebrew
|
|
21
|
+
0x0600..0x06FF, # Arabic
|
|
22
|
+
0x0700..0x074F, # Syriac
|
|
23
|
+
0x0750..0x077F, # Arabic Supplement
|
|
24
|
+
0x08A0..0x08FF, # Arabic Extended-A
|
|
25
|
+
0xFB1D..0xFB4F, # Hebrew Presentation Forms
|
|
26
|
+
0xFB50..0xFDFF, # Arabic Presentation Forms-A
|
|
27
|
+
0xFE70..0xFEFF # Arabic Presentation Forms-B
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Returns the dominant direction of a string.
|
|
33
|
+
# +:ltr+ — all or predominantly LTR characters.
|
|
34
|
+
# +:rtl+ — predominantly RTL characters.
|
|
35
|
+
# +:mixed+ — significant presence of both LTR and RTL.
|
|
36
|
+
def detect(string)
|
|
37
|
+
return LTR if string.nil? || string.empty?
|
|
38
|
+
|
|
39
|
+
chars = string.each_char.to_a
|
|
40
|
+
rtl_count = chars.count { |c| rtl?(c) }
|
|
41
|
+
ltr_count = chars.count { |c| ltr?(c) }
|
|
42
|
+
|
|
43
|
+
return LTR if rtl_count.zero?
|
|
44
|
+
return RTL if ltr_count.zero?
|
|
45
|
+
|
|
46
|
+
ratio = rtl_count.to_f / (rtl_count + ltr_count)
|
|
47
|
+
ratio > 0.6 ? RTL : (ratio < 0.3 ? LTR : MIXED)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Is this character an RTL character?
|
|
51
|
+
def rtl?(char)
|
|
52
|
+
cp = char.ord
|
|
53
|
+
RTL_RANGES.any? { |range| range.cover?(cp) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Is this character an LTR character (Latin, Cyrillic, Greek,
|
|
57
|
+
# CJK, etc.)? Excludes whitespace, punctuation, and digits
|
|
58
|
+
# (which are neutral in BIDI).
|
|
59
|
+
def ltr?(char)
|
|
60
|
+
cp = char.ord
|
|
61
|
+
return false if cp < 0x0041 # below 'A'
|
|
62
|
+
return false if rtl?(char)
|
|
63
|
+
return false if neutral?(char)
|
|
64
|
+
|
|
65
|
+
# Latin, Latin Extended, Cyrillic, Greek, CJK, etc.
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# BIDI-neutral characters (whitespace, punctuation, digits).
|
|
70
|
+
def neutral?(char)
|
|
71
|
+
char.match?(/\s/) || char.match?(/[0-9]/) || char.match?(/[^\p{L}]/)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Reverses a string character-by-character for RTL rendering.
|
|
75
|
+
# Does NOT do full BIDI reordering — only flips the base
|
|
76
|
+
# direction. Adequate for simple RTL text without embedded LTR.
|
|
77
|
+
def reverse_for_rtl(string)
|
|
78
|
+
string.reverse
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
# A candidate line-break position within a list of InlineRuns.
|
|
6
|
+
class BreakOpportunity
|
|
7
|
+
attr_reader :run_index, :char_offset, :width_before, :type
|
|
8
|
+
|
|
9
|
+
def initialize(run_index:, char_offset:, width_before:, type:)
|
|
10
|
+
@run_index = run_index
|
|
11
|
+
@char_offset = char_offset
|
|
12
|
+
@width_before = width_before.to_f
|
|
13
|
+
@type = type
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def forced?
|
|
18
|
+
@type == :forced
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def soft?
|
|
22
|
+
@type == :soft
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
other.is_a?(self.class) &&
|
|
27
|
+
run_index == other.run_index &&
|
|
28
|
+
char_offset == other.char_offset &&
|
|
29
|
+
width_before == other.width_before &&
|
|
30
|
+
type == other.type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
[self.class, run_index, char_offset, width_before, type].hash
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def each_in(runs, measurer:, font_size:)
|
|
41
|
+
return enum_for(:each_in, runs, measurer: measurer, font_size: font_size) unless block_given?
|
|
42
|
+
|
|
43
|
+
yield new(run_index: 0, char_offset: 0, width_before: 0.0, type: :start)
|
|
44
|
+
|
|
45
|
+
cumulative = 0.0
|
|
46
|
+
runs.each_with_index do |run, run_index|
|
|
47
|
+
text = run.text
|
|
48
|
+
i = 0
|
|
49
|
+
while i < text.length
|
|
50
|
+
ch = text[i]
|
|
51
|
+
if ch == "\n"
|
|
52
|
+
yield new(run_index: run_index, char_offset: i,
|
|
53
|
+
width_before: cumulative, type: :forced)
|
|
54
|
+
i += 1
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if ch == ' '
|
|
59
|
+
cumulative += char_width(run, ' ', measurer, font_size)
|
|
60
|
+
yield new(run_index: run_index, char_offset: i + 1,
|
|
61
|
+
width_before: cumulative, type: :soft)
|
|
62
|
+
i += 1
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if ch == '-' || ch == '—' || ch == ''
|
|
67
|
+
cumulative += char_width(run, ch, measurer, font_size)
|
|
68
|
+
yield new(run_index: run_index, char_offset: i + 1,
|
|
69
|
+
width_before: cumulative, type: :soft)
|
|
70
|
+
i += 1
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if ch == ''
|
|
75
|
+
yield new(run_index: run_index, char_offset: i,
|
|
76
|
+
width_before: cumulative, type: :soft)
|
|
77
|
+
i += 1
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
cumulative += char_width(run, ch, measurer, font_size)
|
|
82
|
+
i += 1
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
yield new(run_index: runs.length, char_offset: 0,
|
|
87
|
+
width_before: cumulative, type: :end)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def char_width(run, char, measurer, font_size)
|
|
93
|
+
measurer.width_of_run(
|
|
94
|
+
char,
|
|
95
|
+
font_size: font_size,
|
|
96
|
+
character_spacing: run.style.character_spacing,
|
|
97
|
+
word_spacing: 0
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
# First-fit greedy line breaker. FOP's default. Linear time.
|
|
6
|
+
class Greedy
|
|
7
|
+
attr_reader :runs, :measurer, :width, :align
|
|
8
|
+
|
|
9
|
+
def initialize(runs, measurer:, width:, align: :left)
|
|
10
|
+
@runs = runs
|
|
11
|
+
@measurer = measurer
|
|
12
|
+
@width = width.to_f
|
|
13
|
+
@align = align
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def layout
|
|
17
|
+
opps = opportunities
|
|
18
|
+
return [] if opps.empty? || opps.length == 1
|
|
19
|
+
|
|
20
|
+
lines = []
|
|
21
|
+
line_start = 0
|
|
22
|
+
i = 1
|
|
23
|
+
while i < opps.length
|
|
24
|
+
opp = opps[i]
|
|
25
|
+
prev = opps[i - 1]
|
|
26
|
+
|
|
27
|
+
if opp.forced?
|
|
28
|
+
emit_line(lines, opps, line_start, i, prev.width_before)
|
|
29
|
+
line_start = i + 1
|
|
30
|
+
i += 2
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if overflows?(opps, line_start, opp) && (i - line_start > 1)
|
|
35
|
+
emit_line(lines, opps, line_start, i - 1, prev.width_before)
|
|
36
|
+
line_start = i - 1
|
|
37
|
+
end
|
|
38
|
+
i += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
emit_final_line(lines, opps, line_start) unless lines_built_complete?(opps, line_start)
|
|
42
|
+
lines
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def opportunities
|
|
48
|
+
BreakOpportunity.each_in(@runs, measurer: @measurer,
|
|
49
|
+
font_size: font_size).to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def font_size
|
|
53
|
+
@runs.first&.style&.font_size || 12
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def overflows?(opps, line_start, opp)
|
|
57
|
+
opp.width_before - opps[line_start].width_before > @width
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def emit_line(lines, opps, start_idx, end_idx, _line_width)
|
|
61
|
+
placed = build_placed_runs(opps, start_idx, end_idx)
|
|
62
|
+
width_used = opps[end_idx].width_before - opps[start_idx].width_before
|
|
63
|
+
lines << Line.new(placed, width: width_used,
|
|
64
|
+
max_width: @width, align: @align)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emit_final_line(lines, opps, line_start)
|
|
68
|
+
last = opps.last
|
|
69
|
+
prev_w = line_start.positive? ? opps[line_start - 1].width_before : 0.0
|
|
70
|
+
return if last.width_before <= prev_w && !lines.empty?
|
|
71
|
+
|
|
72
|
+
emit_line(lines, opps, line_start, opps.length - 1, last.width_before)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def lines_built_complete?(opps, line_start)
|
|
76
|
+
line_start >= opps.length - 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_placed_runs(opps, start_idx, end_idx)
|
|
80
|
+
return [] if start_idx >= end_idx
|
|
81
|
+
|
|
82
|
+
start_opp = opps[start_idx]
|
|
83
|
+
end_opp = opps[end_idx] || opps.last
|
|
84
|
+
start_run = start_opp.run_index
|
|
85
|
+
start_off = start_opp.char_offset
|
|
86
|
+
end_run = end_opp.run_index
|
|
87
|
+
end_off = end_opp.char_offset
|
|
88
|
+
|
|
89
|
+
placed = []
|
|
90
|
+
x_offset = 0.0
|
|
91
|
+
(start_run..end_run).each do |run_idx|
|
|
92
|
+
next unless run_idx < @runs.length
|
|
93
|
+
|
|
94
|
+
run = @runs[run_idx]
|
|
95
|
+
s_off = run_idx == start_run ? start_off : 0
|
|
96
|
+
e_off = run_idx == end_run ? end_off : run.text.length
|
|
97
|
+
next if s_off >= e_off
|
|
98
|
+
|
|
99
|
+
slice = run.text[s_off, e_off - s_off]
|
|
100
|
+
next if slice.nil? || slice.empty?
|
|
101
|
+
|
|
102
|
+
sub_run = InlineRun.new(slice, style: run.style)
|
|
103
|
+
placed << Line::PlacedRun.new(run: sub_run, x_offset: x_offset)
|
|
104
|
+
x_offset += sub_run.width(@measurer)
|
|
105
|
+
end
|
|
106
|
+
placed
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|