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,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
module KnuthPlass
|
|
6
|
+
# Optimal line breaking via dynamic programming (Knuth & Plass,
|
|
7
|
+
# 1981). Finds the set of break points that minimizes the total
|
|
8
|
+
# "badness" — the sum of squared adjustment ratios across all
|
|
9
|
+
# lines, plus penalties for flagged breaks.
|
|
10
|
+
#
|
|
11
|
+
# The algorithm runs in O(n²) worst case (n = item count) but
|
|
12
|
+
# converges in O(n) for typical text because most candidate
|
|
13
|
+
# breaks are pruned early.
|
|
14
|
+
#
|
|
15
|
+
# Produces TextLayout::Line[] output — compatible with the
|
|
16
|
+
# existing Greedy breaker so the engine can swap freely.
|
|
17
|
+
class Breaker
|
|
18
|
+
Infinity = Float::INFINITY
|
|
19
|
+
|
|
20
|
+
# Tolerance for adjustment ratio — lines with ratio above
|
|
21
|
+
# this are considered "underfull" and penalized.
|
|
22
|
+
TOLERANCE = 100.0
|
|
23
|
+
|
|
24
|
+
# Extra penalty for consecutive flagged breaks (hyphens).
|
|
25
|
+
FLAGGED_PENALTY = 3000.0
|
|
26
|
+
|
|
27
|
+
# Extra penalty for the last line being underfull.
|
|
28
|
+
DEMERITS_LAST_LINE = 50.0
|
|
29
|
+
|
|
30
|
+
attr_reader :items, :line_widths, :runs, :measurer, :align
|
|
31
|
+
|
|
32
|
+
# +items:+ Array of KnuthPlass::Item (Box, Glue, Penalty).
|
|
33
|
+
# +line_widths:+ Array of Float — width available per line.
|
|
34
|
+
# If shorter than the number of lines, the last width repeats.
|
|
35
|
+
# +runs:+ Array of InlineRun — for building placed runs.
|
|
36
|
+
# +measurer:+ GlyphMeasurer for width lookups.
|
|
37
|
+
# +align:+ :left, :right, :center, :justify.
|
|
38
|
+
def initialize(items:, line_widths:, runs:, measurer:, align: :left)
|
|
39
|
+
@items = items
|
|
40
|
+
@line_widths = line_widths
|
|
41
|
+
@runs = runs
|
|
42
|
+
@measurer = measurer
|
|
43
|
+
@align = align
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def layout
|
|
47
|
+
nodes = active_nodes
|
|
48
|
+
return [] if nodes.empty?
|
|
49
|
+
|
|
50
|
+
# Only consider nodes that reached the end of the items
|
|
51
|
+
# (the FINISHED penalty at the last position).
|
|
52
|
+
final_nodes = nodes.select { |n| n.position == @items.length - 1 }
|
|
53
|
+
return [] if final_nodes.empty?
|
|
54
|
+
|
|
55
|
+
# Backtrack from the best final node.
|
|
56
|
+
breaks = reconstruct_breaks(best_final_node(final_nodes))
|
|
57
|
+
|
|
58
|
+
# Build Line objects from the break points.
|
|
59
|
+
lines = []
|
|
60
|
+
prev_item = 0
|
|
61
|
+
breaks.each_with_index do |br, line_idx|
|
|
62
|
+
width = line_width_for(line_idx)
|
|
63
|
+
placed = build_placed_runs(prev_item, br, width)
|
|
64
|
+
used = compute_line_width(prev_item, br)
|
|
65
|
+
lines << Line.new(placed, width: used, max_width: width, align: @align)
|
|
66
|
+
prev_item = br
|
|
67
|
+
end
|
|
68
|
+
lines
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Internal: a node in the dynamic programming graph.
|
|
72
|
+
Node = Struct.new(:position, :line, :fitness, :total_demerits,
|
|
73
|
+
:previous, keyword_init: true)
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Active-node dynamic programming. Each active node represents
|
|
78
|
+
# a feasible break position. We iterate through items and
|
|
79
|
+
# try breaking at each feasible position.
|
|
80
|
+
def active_nodes
|
|
81
|
+
@active = [Node.new(position: -1, line: 0, fitness: 1,
|
|
82
|
+
total_demerits: 0, previous: nil)]
|
|
83
|
+
@items.each_with_index do |_item, i|
|
|
84
|
+
next unless feasible_break?(i)
|
|
85
|
+
|
|
86
|
+
@active = @active.reject { |a| prune?(a, i) }
|
|
87
|
+
candidates = @active.filter_map { |a| try_break(a, i) }
|
|
88
|
+
best = best_candidate(candidates)
|
|
89
|
+
@active << best if best
|
|
90
|
+
end
|
|
91
|
+
@active
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def feasible_break?(i)
|
|
95
|
+
item = @items[i]
|
|
96
|
+
(item.penalty? && !item.no_break?) || (item.glue? && i.positive? && @items[i - 1].box?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def prune?(node, _current)
|
|
100
|
+
node.line >= @line_widths.length + 10
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def try_break(node, break_pos)
|
|
104
|
+
item = @items[break_pos]
|
|
105
|
+
width = line_width_for(node.line)
|
|
106
|
+
line_w = compute_line_width(node.position + 1, break_pos)
|
|
107
|
+
|
|
108
|
+
is_forced = item.penalty? && item.forced_break?
|
|
109
|
+
ratio = adjustment_ratio(line_w, width, break_pos)
|
|
110
|
+
|
|
111
|
+
# Forced breaks are always accepted but still penalized
|
|
112
|
+
# for bad ratios so the algorithm prefers shorter lines.
|
|
113
|
+
unless is_forced
|
|
114
|
+
return nil if ratio.abs > TOLERANCE
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ratio = 0.0 if ratio == Infinity || ratio == -Infinity
|
|
118
|
+
demerits = demerits_for(ratio, break_pos)
|
|
119
|
+
total = node.total_demerits + demerits
|
|
120
|
+
fitness = fitness_class(ratio)
|
|
121
|
+
|
|
122
|
+
Node.new(
|
|
123
|
+
position: break_pos,
|
|
124
|
+
line: node.line + 1,
|
|
125
|
+
fitness: fitness,
|
|
126
|
+
total_demerits: total,
|
|
127
|
+
previous: node
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def best_candidate(candidates)
|
|
132
|
+
candidates.min_by(&:total_demerits)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def best_final_node(nodes)
|
|
136
|
+
nodes.min_by(&:total_demerits)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Adjustment ratio: how stretched (positive) or shrunk
|
|
140
|
+
# (negative) a line is. 0 = perfect fit.
|
|
141
|
+
def adjustment_ratio(natural_width, target_width, break_pos)
|
|
142
|
+
glue_stretch = total_stretch(break_pos)
|
|
143
|
+
glue_shrink = total_shrink(break_pos)
|
|
144
|
+
|
|
145
|
+
if natural_width < target_width
|
|
146
|
+
glue_stretch.positive? ? (target_width - natural_width) / glue_stretch : Infinity
|
|
147
|
+
elsif natural_width > target_width
|
|
148
|
+
glue_shrink.positive? ? (target_width - natural_width) / glue_shrink : -Infinity
|
|
149
|
+
else
|
|
150
|
+
0.0
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def demerits_for(ratio, break_pos)
|
|
155
|
+
penalty = @items[break_pos].penalty? ? @items[break_pos].penalty : 0
|
|
156
|
+
badness = if ratio.negative?
|
|
157
|
+
(100 * ((-ratio) ** 3)).to_i
|
|
158
|
+
else
|
|
159
|
+
0
|
|
160
|
+
end
|
|
161
|
+
# Standard Knuth-Plass demerits. Forced breaks (penalty =
|
|
162
|
+
# -Infinity) use d = (1 + badness)^2 — the penalty term
|
|
163
|
+
# drops out because there's no choice about breaking.
|
|
164
|
+
d = if penalty.infinite?
|
|
165
|
+
(1 + badness) ** 2
|
|
166
|
+
elsif penalty >= 0
|
|
167
|
+
(1 + badness + penalty) ** 2
|
|
168
|
+
else
|
|
169
|
+
((1 + badness) ** 2) - (penalty ** 2)
|
|
170
|
+
end
|
|
171
|
+
flagged = @items[break_pos].penalty? && @items[break_pos].flagged ? FLAGGED_PENALTY : 0
|
|
172
|
+
d + flagged
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def fitness_class(ratio)
|
|
176
|
+
case ratio
|
|
177
|
+
when -Infinity..-0.5 then 0 # tight
|
|
178
|
+
when -0.5..0.5 then 1 # normal
|
|
179
|
+
when 0.5..1.0 then 2 # loose
|
|
180
|
+
else 3 # very loose
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Sum of widths from item +start+ to item +stop+ (exclusive).
|
|
185
|
+
# Discards trailing glue at the break point.
|
|
186
|
+
def compute_line_width(start, stop)
|
|
187
|
+
return 0.0 if start >= stop
|
|
188
|
+
|
|
189
|
+
sum = 0.0
|
|
190
|
+
(start...stop).each do |i|
|
|
191
|
+
item = @items[i]
|
|
192
|
+
next if item.nil?
|
|
193
|
+
|
|
194
|
+
# Trailing glue after last box is discarded at break.
|
|
195
|
+
last_box = last_box_index_in(start, stop)
|
|
196
|
+
sum += item.width if i <= last_box
|
|
197
|
+
end
|
|
198
|
+
sum
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def last_box_index_in(start, stop)
|
|
202
|
+
idx = start - 1
|
|
203
|
+
(start...stop).each do |i|
|
|
204
|
+
idx = i if @items[i]&.box?
|
|
205
|
+
end
|
|
206
|
+
idx
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def total_stretch(break_pos)
|
|
210
|
+
sum = 0.0
|
|
211
|
+
@items[0..break_pos].each { |item| sum += item.stretch if item.glue? }
|
|
212
|
+
sum
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def total_shrink(break_pos)
|
|
216
|
+
sum = 0.0
|
|
217
|
+
@items[0..break_pos].each { |item| sum += item.shrink if item.glue? }
|
|
218
|
+
sum
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def line_width_for(line_idx)
|
|
222
|
+
@line_widths[[line_idx, @line_widths.length - 1].min]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def reconstruct_breaks(final_node)
|
|
226
|
+
breaks = []
|
|
227
|
+
node = final_node
|
|
228
|
+
while node&.previous
|
|
229
|
+
breaks << node.position
|
|
230
|
+
node = node.previous
|
|
231
|
+
end
|
|
232
|
+
breaks.reverse
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def build_placed_runs(start_item, stop_item, _width)
|
|
236
|
+
placed = []
|
|
237
|
+
x_offset = 0.0
|
|
238
|
+
(start_item...stop_item).each do |i|
|
|
239
|
+
item = @items[i]
|
|
240
|
+
next unless item&.box?
|
|
241
|
+
|
|
242
|
+
run = @runs[item.run_index]
|
|
243
|
+
next unless run
|
|
244
|
+
|
|
245
|
+
slice = run.text[item.char_offset, item.char_length]
|
|
246
|
+
next if slice.nil? || slice.empty?
|
|
247
|
+
|
|
248
|
+
sub = InlineRun.new(slice, style: run.style,
|
|
249
|
+
baseline_shift: run.baseline_shift,
|
|
250
|
+
font_size_scale: run.font_size_scale,
|
|
251
|
+
href: run.href)
|
|
252
|
+
placed << Line::PlacedRun.new(run: sub, x_offset: x_offset)
|
|
253
|
+
x_offset += item.width
|
|
254
|
+
end
|
|
255
|
+
placed
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def finalize_node(position, line, fitness, total_demerits, previous)
|
|
259
|
+
Node.new(position: position, line: line, fitness: fitness,
|
|
260
|
+
total_demerits: total_demerits, previous: previous)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
module KnuthPlass
|
|
6
|
+
# Base class for items in the Knuth-Plass line-breaking model.
|
|
7
|
+
# Every item has a width. Box and Glue are "content"; Penalty
|
|
8
|
+
# is a potential break point.
|
|
9
|
+
class Item
|
|
10
|
+
attr_reader :width
|
|
11
|
+
|
|
12
|
+
def initialize(width:)
|
|
13
|
+
@width = width.to_f
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def box? = false
|
|
17
|
+
def glue? = false
|
|
18
|
+
def penalty? = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A box is a unit of content that cannot be broken across
|
|
22
|
+
# lines (a word, a glyph cluster). Has a fixed width.
|
|
23
|
+
class Box < Item
|
|
24
|
+
attr_reader :run_index, :char_offset, :char_length
|
|
25
|
+
|
|
26
|
+
def initialize(width:, run_index:, char_offset:, char_length:)
|
|
27
|
+
super(width: width)
|
|
28
|
+
@run_index = run_index
|
|
29
|
+
@char_offset = char_offset
|
|
30
|
+
@char_length = char_length
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def box? = true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Glue represents inter-word space. Has a natural width plus
|
|
37
|
+
# stretchability (how much it can grow) and shrinkability
|
|
38
|
+
# (how much it can shrink). At a break point, trailing glue
|
|
39
|
+
# is discarded (doesn't contribute to line width).
|
|
40
|
+
class Glue < Item
|
|
41
|
+
attr_reader :stretch, :shrink, :run_index, :char_offset
|
|
42
|
+
|
|
43
|
+
def initialize(width:, stretch:, shrink:, run_index: nil, char_offset: nil)
|
|
44
|
+
super(width: width)
|
|
45
|
+
@stretch = stretch.to_f
|
|
46
|
+
@shrink = shrink.to_f
|
|
47
|
+
@run_index = run_index
|
|
48
|
+
@char_offset = char_offset
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def glue? = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A penalty represents a potential break point. Width is the
|
|
55
|
+
# width of the hyphen (or 0 if no hyphen). Flagged penalties
|
|
56
|
+
# (e.g., hyphenation) incur extra cost. A penalty of +Infinity+
|
|
57
|
+
# means "never break here"; -Infinity means "always break".
|
|
58
|
+
class Penalty < Item
|
|
59
|
+
attr_reader :penalty, :flagged, :run_index, :char_offset
|
|
60
|
+
|
|
61
|
+
def initialize(penalty:, width: 0, flagged: false, run_index: nil, char_offset: nil)
|
|
62
|
+
super(width: width)
|
|
63
|
+
@penalty = penalty.to_f
|
|
64
|
+
@flagged = flagged
|
|
65
|
+
@run_index = run_index
|
|
66
|
+
@char_offset = char_offset
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def penalty? = true
|
|
70
|
+
|
|
71
|
+
def forced_break?
|
|
72
|
+
@penalty == -Float::INFINITY
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def no_break?
|
|
76
|
+
@penalty == Float::INFINITY
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A forced break at the end of a paragraph.
|
|
81
|
+
FINISHED = Penalty.new(penalty: -Float::INFINITY).freeze
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
module KnuthPlass
|
|
6
|
+
# Converts a list of InlineRun values into KnuthPlass::Item
|
|
7
|
+
# sequences (Box, Glue, Penalty). Each word becomes a Box;
|
|
8
|
+
# spaces become Glue; potential break points become Penalty
|
|
9
|
+
# items.
|
|
10
|
+
class ItemBuilder
|
|
11
|
+
HYPHEN_PENALTY = 50.0
|
|
12
|
+
|
|
13
|
+
attr_reader :runs, :measurer
|
|
14
|
+
|
|
15
|
+
def initialize(runs, measurer:)
|
|
16
|
+
@runs = runs
|
|
17
|
+
@measurer = measurer
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build
|
|
21
|
+
items = []
|
|
22
|
+
@runs.each_with_index do |run, run_idx|
|
|
23
|
+
build_items_for_run(run, run_idx, items)
|
|
24
|
+
end
|
|
25
|
+
items << FINISHED
|
|
26
|
+
items
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build_items_for_run(run, run_idx, items)
|
|
32
|
+
text = run.text
|
|
33
|
+
return if text.nil? || text.empty?
|
|
34
|
+
|
|
35
|
+
# Handle forced breaks first.
|
|
36
|
+
if text.include?("\n")
|
|
37
|
+
build_newline_items(run, run_idx, text, items)
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
words = text.split(/(\s+)/)
|
|
42
|
+
offset = 0
|
|
43
|
+
words.each do |word|
|
|
44
|
+
if word.match?(/\s+/)
|
|
45
|
+
width = measure(word, run)
|
|
46
|
+
stretch = measure(' ', run) * 0.5
|
|
47
|
+
shrink = measure(' ', run) * 0.33
|
|
48
|
+
items << Glue.new(width: width, stretch: stretch, shrink: shrink,
|
|
49
|
+
run_index: run_idx, char_offset: offset)
|
|
50
|
+
elsif word.include?('-')
|
|
51
|
+
build_hyphenated_word(word, run, run_idx, offset, items)
|
|
52
|
+
elsif !word.empty?
|
|
53
|
+
width = measure(word, run)
|
|
54
|
+
items << Box.new(width: width, run_index: run_idx,
|
|
55
|
+
char_offset: offset, char_length: word.length)
|
|
56
|
+
end
|
|
57
|
+
offset += word.length
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_newline_items(run, run_idx, text, items)
|
|
62
|
+
segments = text.split("\n")
|
|
63
|
+
segments.each_with_index do |seg, i|
|
|
64
|
+
unless seg.empty?
|
|
65
|
+
width = measure(seg, run)
|
|
66
|
+
items << Box.new(width: width, run_index: run_idx,
|
|
67
|
+
char_offset: text.index(seg) || 0,
|
|
68
|
+
char_length: seg.length)
|
|
69
|
+
end
|
|
70
|
+
items << Penalty.new(penalty: -Float::INFINITY, run_index: run_idx) if i < segments.length - 1
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_hyphenated_word(word, run, run_idx, offset, items)
|
|
75
|
+
parts = word.split(/(-)/)
|
|
76
|
+
part_offset = offset
|
|
77
|
+
parts.each do |part|
|
|
78
|
+
next if part.empty?
|
|
79
|
+
|
|
80
|
+
width = measure(part, run)
|
|
81
|
+
items << Box.new(width: width, run_index: run_idx,
|
|
82
|
+
char_offset: part_offset, char_length: part.length)
|
|
83
|
+
if part == '-'
|
|
84
|
+
items << Penalty.new(width: 0, penalty: HYPHEN_PENALTY,
|
|
85
|
+
flagged: true, run_index: run_idx,
|
|
86
|
+
char_offset: part_offset)
|
|
87
|
+
end
|
|
88
|
+
part_offset += part.length
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def measure(text, run)
|
|
93
|
+
@measurer.width_of_run(
|
|
94
|
+
text,
|
|
95
|
+
font_size: run.style.font_size * run.font_size_scale,
|
|
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,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arrolio::TextLayout::KnuthPlass — optimal line breaking.
|
|
4
|
+
module Arrolio
|
|
5
|
+
module TextLayout
|
|
6
|
+
module KnuthPlass
|
|
7
|
+
autoload :Item, 'arrolio/text_layout/knuth_plass/item'
|
|
8
|
+
autoload :Breaker, 'arrolio/text_layout/knuth_plass/breaker'
|
|
9
|
+
autoload :ItemBuilder, 'arrolio/text_layout/knuth_plass/item_builder'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TextLayout
|
|
5
|
+
# One laid-out line: a list of PlacedRun tuples plus the
|
|
6
|
+
# line's content width, target width, and alignment.
|
|
7
|
+
class Line
|
|
8
|
+
PlacedRun = Struct.new(:run, :x_offset, keyword_init: true) do
|
|
9
|
+
def freeze
|
|
10
|
+
run.freeze
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :placed_runs, :width, :max_width, :align
|
|
16
|
+
|
|
17
|
+
def initialize(placed_runs, width:, max_width:, align: :left)
|
|
18
|
+
@placed_runs = placed_runs.freeze
|
|
19
|
+
@width = width.to_f
|
|
20
|
+
@max_width = max_width.to_f
|
|
21
|
+
@align = align
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def empty?
|
|
26
|
+
@placed_runs.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def x_offset
|
|
30
|
+
case @align
|
|
31
|
+
when :center then ((@max_width - @width) / 2.0)
|
|
32
|
+
when :right then (@max_width - @width)
|
|
33
|
+
else 0.0
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def justified?
|
|
38
|
+
@align == :justify
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def justify_stretch
|
|
42
|
+
return 0.0 unless justified?
|
|
43
|
+
return 0.0 if @width >= @max_width
|
|
44
|
+
|
|
45
|
+
gaps = word_gap_count
|
|
46
|
+
return 0.0 if gaps.zero?
|
|
47
|
+
|
|
48
|
+
(@max_width - @width) / gaps.to_f
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ==(other)
|
|
52
|
+
other.is_a?(self.class) &&
|
|
53
|
+
placed_runs == other.placed_runs &&
|
|
54
|
+
width == other.width &&
|
|
55
|
+
max_width == other.max_width &&
|
|
56
|
+
align == other.align
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias eql? ==
|
|
60
|
+
|
|
61
|
+
def hash
|
|
62
|
+
[self.class, placed_runs, width, max_width, align].hash
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def word_gap_count
|
|
68
|
+
@placed_runs.sum { |pr| pr.run.text.count(' ') }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arrolio::TextLayout — paragraph line-breaking. Pure + stateless.
|
|
4
|
+
module Arrolio
|
|
5
|
+
module TextLayout
|
|
6
|
+
autoload :Greedy, 'arrolio/text_layout/greedy'
|
|
7
|
+
autoload :Line, 'arrolio/text_layout/line'
|
|
8
|
+
autoload :BreakOpportunity, 'arrolio/text_layout/break_opportunity'
|
|
9
|
+
autoload :KnuthPlass, 'arrolio/text_layout/knuth_plass'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module TocBuilder
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def build_flowables(context, layout_spec, rules: {})
|
|
8
|
+
styles = rules.fetch('styles', {})
|
|
9
|
+
entries = context.heading_entries
|
|
10
|
+
entries.map do |entry|
|
|
11
|
+
number = entry[:number]
|
|
12
|
+
title = entry[:title].to_s
|
|
13
|
+
label = number ? "#{number} #{title}" : title
|
|
14
|
+
style_id = style_id_for(entry[:level], styles)
|
|
15
|
+
style = layout_spec.resolve_style(style_id)
|
|
16
|
+
Flowables::TocLineFlowable.new(
|
|
17
|
+
label,
|
|
18
|
+
entry[:page_number],
|
|
19
|
+
level: entry[:level] || 1,
|
|
20
|
+
style: style
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def style_id_for(level, styles)
|
|
26
|
+
key = level.to_i >= 2 ? 'sub' : 'entry'
|
|
27
|
+
default_style = key == 'sub' ? 'toc_entry_sub' : 'toc_entry'
|
|
28
|
+
(styles[key] || default_style).to_sym
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# Writing mode enumeration for vertical/RTL layout. Controls
|
|
5
|
+
# the inline and block progression directions used by the
|
|
6
|
+
# layout engine. The values map to XSL-FO writing-mode
|
|
7
|
+
# property values.
|
|
8
|
+
#
|
|
9
|
+
# +LR_TB+ — left-to-right inline, top-to-bottom block (Latin, CJK).
|
|
10
|
+
# +RL_TB+ — right-to-left inline, top-to-bottom block (Arabic, Hebrew).
|
|
11
|
+
# +TB_RL+ — top-to-bottom inline, right-to-left block (vertical CJK).
|
|
12
|
+
class WritingMode
|
|
13
|
+
LR_TB = :lr_tb
|
|
14
|
+
RL_TB = :rl_tb
|
|
15
|
+
TB_RL = :tb_rl
|
|
16
|
+
DEFAULT = LR_TB
|
|
17
|
+
|
|
18
|
+
ALL = [LR_TB, RL_TB, TB_RL].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :mode
|
|
21
|
+
|
|
22
|
+
def initialize(mode = DEFAULT)
|
|
23
|
+
@mode = mode.to_sym
|
|
24
|
+
freeze
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def vertical?
|
|
28
|
+
@mode == TB_RL
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rtl?
|
|
32
|
+
@mode == RL_TB
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ltr?
|
|
36
|
+
@mode == LR_TB
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns the inline progression direction as a 2D vector.
|
|
40
|
+
# +[1, 0]+ = rightward, +[-1, 0]+ = leftward, +[0, 1]+ = downward.
|
|
41
|
+
def inline_direction
|
|
42
|
+
case @mode
|
|
43
|
+
when LR_TB then [1, 0]
|
|
44
|
+
when RL_TB then [-1, 0]
|
|
45
|
+
when TB_RL then [0, 1]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns the block progression direction as a 2D vector.
|
|
50
|
+
def block_direction
|
|
51
|
+
case @mode
|
|
52
|
+
when LR_TB, RL_TB then [0, -1]
|
|
53
|
+
when TB_RL then [-1, 0]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ==(other)
|
|
58
|
+
other.is_a?(self.class) && mode == other.mode
|
|
59
|
+
end
|
|
60
|
+
alias eql? ==
|
|
61
|
+
|
|
62
|
+
def hash
|
|
63
|
+
[self.class, mode].hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.parse(value)
|
|
67
|
+
case value.to_s.downcase.tr('-', '_')
|
|
68
|
+
when 'lr_tb', 'lrtb' then new(LR_TB)
|
|
69
|
+
when 'rl_tb', 'rltb' then new(RL_TB)
|
|
70
|
+
when 'tb_rl', 'tbrl' then new(TB_RL)
|
|
71
|
+
else new(DEFAULT)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|