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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
class Table
|
|
6
|
+
attr_reader :header, :body, :column_widths, :style_id, :id
|
|
7
|
+
|
|
8
|
+
def initialize(header: [], body: [], column_widths: nil,
|
|
9
|
+
style_id: :table, id: nil)
|
|
10
|
+
@header = header.map { |row| coerce_row(row) }.freeze
|
|
11
|
+
@body = body.map { |row| coerce_row(row) }.freeze
|
|
12
|
+
@column_widths = column_widths&.map(&:to_f)&.freeze
|
|
13
|
+
@style_id = style_id.to_sym
|
|
14
|
+
@id = id
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def rows
|
|
19
|
+
@header + @body
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def column_count
|
|
23
|
+
rows.map { |r| r.cells.length }.max || 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(self.class) &&
|
|
28
|
+
header == other.header &&
|
|
29
|
+
body == other.body &&
|
|
30
|
+
column_widths == other.column_widths &&
|
|
31
|
+
style_id == other.style_id &&
|
|
32
|
+
id == other.id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
alias eql? ==
|
|
36
|
+
|
|
37
|
+
def hash
|
|
38
|
+
[self.class, header, body, column_widths, style_id, id].hash
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def coerce_row(row)
|
|
44
|
+
case row
|
|
45
|
+
when Row then row
|
|
46
|
+
when Array then Row.new(row.map { |c| c.is_a?(Cell) ? c : Cell.new(c) })
|
|
47
|
+
else
|
|
48
|
+
raise ContentError, "Table row must be an Array or Table::Row (got #{row.class})"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Table::Row
|
|
54
|
+
include Enumerable
|
|
55
|
+
|
|
56
|
+
attr_reader :cells
|
|
57
|
+
|
|
58
|
+
def initialize(cells)
|
|
59
|
+
@cells = Array(cells).freeze
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def each(&block)
|
|
64
|
+
@cells.each(&block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def length
|
|
68
|
+
@cells.length
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.is_a?(self.class) && cells == other.cells
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
alias eql? ==
|
|
76
|
+
|
|
77
|
+
def hash
|
|
78
|
+
[self.class, cells].hash
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Table::Cell
|
|
83
|
+
attr_reader :content, :colspan, :rowspan, :style_id, :align
|
|
84
|
+
|
|
85
|
+
def initialize(content = [], colspan: 1, rowspan: 1,
|
|
86
|
+
style_id: :table_cell, align: nil)
|
|
87
|
+
@content = Array(content).freeze
|
|
88
|
+
@colspan = colspan.to_i
|
|
89
|
+
@rowspan = rowspan.to_i
|
|
90
|
+
@style_id = style_id.to_sym
|
|
91
|
+
@align = align
|
|
92
|
+
freeze
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def text
|
|
96
|
+
@content.map do |node|
|
|
97
|
+
node.is_a?(Paragraph) ? node.text : node.to_s
|
|
98
|
+
end.join(' ')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ==(other)
|
|
102
|
+
other.is_a?(self.class) &&
|
|
103
|
+
content == other.content &&
|
|
104
|
+
colspan == other.colspan &&
|
|
105
|
+
rowspan == other.rowspan &&
|
|
106
|
+
style_id == other.style_id &&
|
|
107
|
+
align == other.align
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
alias eql? ==
|
|
111
|
+
|
|
112
|
+
def hash
|
|
113
|
+
[self.class, content, colspan, rowspan, style_id, align].hash
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
# A dictionary term entry: optional number, preferred term text,
|
|
6
|
+
# definition Paragraphs, and optional source citation. Maps to
|
|
7
|
+
# the `<term>` element in standoc vocabulary. Renders as a
|
|
8
|
+
# hanging-indent group in the flow builder.
|
|
9
|
+
class TermEntry
|
|
10
|
+
attr_reader :number, :preferred, :definition, :source, :id, :style_id
|
|
11
|
+
|
|
12
|
+
def initialize(number: nil, preferred: nil, definition: [],
|
|
13
|
+
source: nil, id: nil, style_id: :term)
|
|
14
|
+
@number = number&.to_s
|
|
15
|
+
@preferred = preferred
|
|
16
|
+
@definition = Array(definition).freeze
|
|
17
|
+
@source = source
|
|
18
|
+
@id = id&.to_s
|
|
19
|
+
@style_id = style_id.to_sym
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
@preferred.nil? && @definition.empty? && @number.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ==(other)
|
|
28
|
+
other.is_a?(self.class) &&
|
|
29
|
+
number == other.number &&
|
|
30
|
+
preferred == other.preferred &&
|
|
31
|
+
definition == other.definition &&
|
|
32
|
+
source == other.source &&
|
|
33
|
+
id == other.id &&
|
|
34
|
+
style_id == other.style_id
|
|
35
|
+
end
|
|
36
|
+
alias eql? ==
|
|
37
|
+
|
|
38
|
+
def hash
|
|
39
|
+
[self.class, number, preferred, definition, source, id, style_id].hash
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Content
|
|
5
|
+
autoload :Document, 'arrolio/content/document'
|
|
6
|
+
autoload :Section, 'arrolio/content/section'
|
|
7
|
+
autoload :Paragraph, 'arrolio/content/paragraph'
|
|
8
|
+
autoload :InlineRun, 'arrolio/content/inline_run'
|
|
9
|
+
autoload :Heading, 'arrolio/content/heading'
|
|
10
|
+
autoload :Hyperlink, 'arrolio/content/hyperlink'
|
|
11
|
+
autoload :Formula, 'arrolio/content/formula'
|
|
12
|
+
autoload :Preformatted, 'arrolio/content/preformatted'
|
|
13
|
+
autoload :PageBreak, 'arrolio/content/page_break'
|
|
14
|
+
autoload :Footnote, 'arrolio/content/footnote'
|
|
15
|
+
autoload :IndexEntry, 'arrolio/content/index_entry'
|
|
16
|
+
autoload :FormField, 'arrolio/content/form_field'
|
|
17
|
+
autoload :Table, 'arrolio/content/table'
|
|
18
|
+
autoload :List, 'arrolio/content/list'
|
|
19
|
+
autoload :Image, 'arrolio/content/image'
|
|
20
|
+
autoload :Builder, 'arrolio/content/builder'
|
|
21
|
+
autoload :Note, 'arrolio/content/note'
|
|
22
|
+
autoload :Example, 'arrolio/content/example'
|
|
23
|
+
autoload :TermEntry, 'arrolio/content/term_entry'
|
|
24
|
+
autoload :FigureGroup, 'arrolio/content/figure_group'
|
|
25
|
+
autoload :BibliographyItem, 'arrolio/content/bibliography_item'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module DataDir
|
|
5
|
+
DELIMITER = '/'
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def root
|
|
9
|
+
File.expand_path('../../data/arrolio', __dir__)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(*segments)
|
|
13
|
+
path = File.join(root, *segments)
|
|
14
|
+
raise Arrolio::Error, "missing data file: #{segments.join(DELIMITER)}" unless File.exist?(path)
|
|
15
|
+
|
|
16
|
+
path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def afm(name)
|
|
20
|
+
File.join(root, 'afm', "#{name}.afm")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def glyphlist
|
|
24
|
+
File.join(root, 'glyphlist.txt')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Engine
|
|
5
|
+
# Collects heading → page-number mappings during layout and
|
|
6
|
+
# resolves cross-references for ToC, page citations, and
|
|
7
|
+
# internal links. The engine records entries during pass 1;
|
|
8
|
+
# after layout is complete, the registry provides resolved
|
|
9
|
+
# references for deferred rendering (e.g., ToC page that
|
|
10
|
+
# appears at the front of the document but needs page numbers
|
|
11
|
+
# from the body).
|
|
12
|
+
#
|
|
13
|
+
# This implements the "deferred rendering" pattern: the ToC
|
|
14
|
+
# section is rendered last (from the registry's resolved data)
|
|
15
|
+
# but appears first in the document. The engine's existing
|
|
16
|
+
# FlowContext#heading_entries feeds into this registry.
|
|
17
|
+
class CrossReferenceRegistry
|
|
18
|
+
Entry = Struct.new(:id, :number, :title, :level, :page_number,
|
|
19
|
+
keyword_init: true)
|
|
20
|
+
|
|
21
|
+
attr_reader :entries
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@entries = []
|
|
25
|
+
@by_id = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Records a heading during layout pass 1.
|
|
29
|
+
def record(id:, number:, title:, level:, page_number:)
|
|
30
|
+
entry = Entry.new(
|
|
31
|
+
id: id, number: number, title: title,
|
|
32
|
+
level: level, page_number: page_number
|
|
33
|
+
)
|
|
34
|
+
@entries << entry
|
|
35
|
+
@by_id[id.to_s] = entry if id
|
|
36
|
+
entry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Resolves a cross-reference target to a page number.
|
|
40
|
+
# Returns nil if the target hasn't been recorded.
|
|
41
|
+
def page_number_for(target_id)
|
|
42
|
+
@by_id[target_id.to_s]&.page_number
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Resolves a cross-reference target to its full entry.
|
|
46
|
+
def entry_for(target_id)
|
|
47
|
+
@by_id[target_id.to_s]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# All entries, suitable for ToC generation. Entries are
|
|
51
|
+
# in document order (the order they were recorded).
|
|
52
|
+
# Pass +max_level:+ to filter to a maximum heading level
|
|
53
|
+
# (e.g., only levels 1-2 for a compact ToC).
|
|
54
|
+
def toc_entries(max_level: nil)
|
|
55
|
+
return @entries.dup if max_level.nil?
|
|
56
|
+
|
|
57
|
+
@entries.select { |e| e.level <= max_level }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def empty?
|
|
61
|
+
@entries.empty?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def count
|
|
65
|
+
@entries.length
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Engine
|
|
5
|
+
# Two-pass page-flow driver.
|
|
6
|
+
class Paged
|
|
7
|
+
attr_reader :layout_spec, :flowables, :pages, :context
|
|
8
|
+
|
|
9
|
+
def initialize(layout_spec:, flowables:)
|
|
10
|
+
@layout_spec = layout_spec
|
|
11
|
+
@flowables = flowables
|
|
12
|
+
@pages = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def layout
|
|
16
|
+
@pages = []
|
|
17
|
+
@page_count_opened = 0
|
|
18
|
+
@footnote_page_map = {} # footnote.id / marker -> first page_number
|
|
19
|
+
context = FlowContext.new(layout_spec: @layout_spec, page_number: 1)
|
|
20
|
+
pending = @flowables.dup
|
|
21
|
+
page_state = open_page(context)
|
|
22
|
+
|
|
23
|
+
until pending.empty?
|
|
24
|
+
flowable = pending.shift
|
|
25
|
+
|
|
26
|
+
if flowable.is_a?(Flowables::PageSequenceStart)
|
|
27
|
+
@current_role = flowable.role
|
|
28
|
+
@current_header = flowable.header_template
|
|
29
|
+
@current_footer = flowable.footer_template
|
|
30
|
+
@current_header_align = flowable.header_align
|
|
31
|
+
@current_footer_align = flowable.footer_align
|
|
32
|
+
unless page_state.fresh && page_state.body_boxes.empty?
|
|
33
|
+
page_state = open_page(context)
|
|
34
|
+
end
|
|
35
|
+
page_state.role = @current_role
|
|
36
|
+
page_state.header = @current_header
|
|
37
|
+
page_state.footer = @current_footer
|
|
38
|
+
page_state.header_align = @current_header_align
|
|
39
|
+
page_state.footer_align = @current_footer_align
|
|
40
|
+
next
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if flowable.is_a?(Flowables::PageBreak)
|
|
44
|
+
page_state = open_page(context)
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Footnote anchors: record on the current page without
|
|
49
|
+
# consuming frame space (the flowable has zero height).
|
|
50
|
+
if flowable.is_a?(Flowables::FootnoteMarkerFlowable)
|
|
51
|
+
key = flowable.footnote.id || flowable.footnote.marker
|
|
52
|
+
@footnote_page_map[key] ||= page_state.page_number
|
|
53
|
+
page_state.footnotes << flowable.footnote unless page_state.footnotes.include?(flowable.footnote)
|
|
54
|
+
next
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
page_state = open_page(context) if flowable.page_break_before? && @pages.any? && !page_state.fresh
|
|
58
|
+
page_state = open_page(context) if page_state.frame.full?
|
|
59
|
+
|
|
60
|
+
width = page_state.frame.width
|
|
61
|
+
if flowable.keep_together? &&
|
|
62
|
+
flowable.height(width, context) > page_state.frame.remaining_height &&
|
|
63
|
+
flowable.height(width, context) <= page_state.frame.height
|
|
64
|
+
page_state = open_page(context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Record heading entries for ToC + outline building.
|
|
68
|
+
if flowable.is_a?(Flowables::HeadingFlowable)
|
|
69
|
+
context.record_heading(number: flowable.number,
|
|
70
|
+
title: flowable.title,
|
|
71
|
+
level: flowable.level,
|
|
72
|
+
page_number: page_state.page_number,
|
|
73
|
+
id: flowable.id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
page_state.fresh = false
|
|
77
|
+
page_state = place(page_state, flowable, context, pending)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@context = context
|
|
81
|
+
context.page_count = @pages.length
|
|
82
|
+
finalize_pages
|
|
83
|
+
@pages
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def place(page_state, flowable, context, pending)
|
|
89
|
+
frame = page_state.frame
|
|
90
|
+
width = frame.width
|
|
91
|
+
natural = flowable.height(width, context)
|
|
92
|
+
|
|
93
|
+
if natural <= frame.remaining_height
|
|
94
|
+
emit(page_state, flowable, context)
|
|
95
|
+
elsif flowable.splittable?
|
|
96
|
+
head, tail = flowable.split(width, frame.remaining_height, context)
|
|
97
|
+
if head
|
|
98
|
+
emit(page_state, head, context)
|
|
99
|
+
pending.unshift(tail) if tail
|
|
100
|
+
else
|
|
101
|
+
return page_state unless frame.full?
|
|
102
|
+
|
|
103
|
+
new_state = open_page(context)
|
|
104
|
+
place(new_state, flowable, context, pending)
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
emit(page_state, flowable, context)
|
|
108
|
+
end
|
|
109
|
+
page_state
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def emit(page_state, flowable, context)
|
|
113
|
+
frame = page_state.frame
|
|
114
|
+
x = frame.x
|
|
115
|
+
y_top = frame.cursor_y
|
|
116
|
+
boxes, consumed = flowable.emit(x, y_top, frame.width, context)
|
|
117
|
+
page_state.body_boxes.concat(boxes)
|
|
118
|
+
frame.consume!(consumed)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def open_page(context)
|
|
122
|
+
template = @layout_spec.page_template
|
|
123
|
+
body = template.body_region
|
|
124
|
+
@page_count_opened = (@page_count_opened || 0) + 1
|
|
125
|
+
page_number = @page_count_opened
|
|
126
|
+
|
|
127
|
+
frame = Frame.new(x: body.x, y: body.y,
|
|
128
|
+
width: body.width, height: body.height)
|
|
129
|
+
context.page_number = page_number
|
|
130
|
+
|
|
131
|
+
state = OpenPage.new(
|
|
132
|
+
template: template,
|
|
133
|
+
page_number: page_number,
|
|
134
|
+
frame: frame,
|
|
135
|
+
body_boxes: [],
|
|
136
|
+
fresh: true,
|
|
137
|
+
role: @current_role || :body,
|
|
138
|
+
header: @current_header,
|
|
139
|
+
footer: @current_footer,
|
|
140
|
+
header_align: @current_header_align || :right,
|
|
141
|
+
footer_align: @current_footer_align || :center,
|
|
142
|
+
footnotes: []
|
|
143
|
+
)
|
|
144
|
+
@pages << state
|
|
145
|
+
state
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def finalize_pages
|
|
149
|
+
total = @pages.length
|
|
150
|
+
built = @pages.map do |state|
|
|
151
|
+
template = state.template
|
|
152
|
+
body = template.body_region
|
|
153
|
+
region = Output::Region.new(
|
|
154
|
+
name: :body,
|
|
155
|
+
x: body.x, y: body.y, width: body.width, height: body.height,
|
|
156
|
+
placed_boxes: state.body_boxes
|
|
157
|
+
)
|
|
158
|
+
Output::Page.new(
|
|
159
|
+
number: state.page_number,
|
|
160
|
+
template_name: template.name,
|
|
161
|
+
template_role: state.role,
|
|
162
|
+
page_size: template.page_size,
|
|
163
|
+
regions: { body: region },
|
|
164
|
+
header_text: format_text(state.header, state.page_number, total),
|
|
165
|
+
footer_text: format_text(state.footer, state.page_number, total),
|
|
166
|
+
header_align: state.header_align,
|
|
167
|
+
footer_align: state.footer_align,
|
|
168
|
+
footnotes: state.footnotes
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
@pages.replace(built)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def format_text(template_text, current, _total)
|
|
175
|
+
return nil if template_text.nil?
|
|
176
|
+
|
|
177
|
+
template_text.to_s.gsub('%d', current.to_s)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
OpenPage = Struct.new(:template, :page_number, :frame, :body_boxes,
|
|
182
|
+
:fresh, :role, :header, :footer,
|
|
183
|
+
:header_align, :footer_align, :footnotes,
|
|
184
|
+
keyword_init: true)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# Base class for every Arrolio failure mode. Subclasses carry
|
|
5
|
+
# typed metadata that callers can inspect (e.g. +page_number+ on
|
|
6
|
+
# a +LayoutError+, +oid+ on a +RenderError+).
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised by Engine::* when a layout pass fails (overflow that
|
|
10
|
+
# can't be resolved, broken cross-reference, missing template).
|
|
11
|
+
class LayoutError < Error
|
|
12
|
+
attr_reader :page_number, :template_name
|
|
13
|
+
|
|
14
|
+
def initialize(message, page_number: nil, template_name: nil)
|
|
15
|
+
@page_number = page_number
|
|
16
|
+
@template_name = template_name
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Raised by Renderer::* when the render pass fails.
|
|
22
|
+
class RenderError < Error
|
|
23
|
+
attr_reader :missing_fonts
|
|
24
|
+
|
|
25
|
+
def initialize(message, missing_fonts: nil)
|
|
26
|
+
@missing_fonts = missing_fonts
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Raised when the content tree violates Arrolio's content
|
|
32
|
+
# contract (e.g. a Section with a non-String title, or a
|
|
33
|
+
# Paragraph with malformed InlineRuns).
|
|
34
|
+
class ContentError < Error
|
|
35
|
+
attr_reader :node
|
|
36
|
+
|
|
37
|
+
def initialize(message, node: nil)
|
|
38
|
+
@node = node
|
|
39
|
+
super(message)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Raised when a LayoutSpec is missing required entries or has
|
|
44
|
+
# invalid values (unknown page-template name, unresolvable style
|
|
45
|
+
# reference, etc.).
|
|
46
|
+
class LayoutSpecError < Error
|
|
47
|
+
attr_reader :spec_field
|
|
48
|
+
|
|
49
|
+
def initialize(message, spec_field: nil)
|
|
50
|
+
@spec_field = spec_field
|
|
51
|
+
super(message)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Raised when a flavor is missing required files (manifest.yml,
|
|
56
|
+
# layout_spec.yml, etc.) or has malformed configuration. Carries
|
|
57
|
+
# the flavor directory and/or manifest path for diagnosis.
|
|
58
|
+
class FlavorError < Error
|
|
59
|
+
attr_reader :flavor_dir, :manifest_path, :missing_fields, :missing_role
|
|
60
|
+
|
|
61
|
+
def initialize(message, flavor_dir: nil, manifest_path: nil,
|
|
62
|
+
missing_fields: nil, missing_role: nil)
|
|
63
|
+
@flavor_dir = flavor_dir
|
|
64
|
+
@manifest_path = manifest_path
|
|
65
|
+
@missing_fields = missing_fields
|
|
66
|
+
@missing_role = missing_role
|
|
67
|
+
super(message)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'arrolio/error'
|
|
5
|
+
|
|
6
|
+
module Arrolio
|
|
7
|
+
module Flavor
|
|
8
|
+
# Typed view over a flavor's manifest.yml. Declares the flavor's
|
|
9
|
+
# name, version, upstream XSL, required/optional fonts, and the
|
|
10
|
+
# paths to its three config files. Validates the manifest at load
|
|
11
|
+
# time so broken flavors fail loudly with `Arrolio::FlavorError`.
|
|
12
|
+
class Manifest
|
|
13
|
+
REQUIRED_FIELDS = ['name', 'version', 'config_files'].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :path, :name, :version, :description, :upstream,
|
|
16
|
+
:doctypes, :required_fonts, :optional_fonts,
|
|
17
|
+
:config_files
|
|
18
|
+
|
|
19
|
+
def self.load(flavor_dir)
|
|
20
|
+
manifest_path = File.join(flavor_dir, 'manifest.yml')
|
|
21
|
+
unless File.exist?(manifest_path)
|
|
22
|
+
raise ::Arrolio::FlavorError.new(
|
|
23
|
+
"manifest.yml not found in #{flavor_dir}",
|
|
24
|
+
flavor_dir: flavor_dir
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
data = YAML.safe_load_file(manifest_path, aliases: true)
|
|
29
|
+
unless data.is_a?(Hash)
|
|
30
|
+
raise ::Arrolio::FlavorError.new(
|
|
31
|
+
"manifest.yml at #{manifest_path} is not a YAML mapping",
|
|
32
|
+
flavor_dir: flavor_dir
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
validate_required_fields!(data, manifest_path)
|
|
37
|
+
validate_config_files!(data, manifest_path, flavor_dir)
|
|
38
|
+
|
|
39
|
+
new(manifest_path, data)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(path, data)
|
|
43
|
+
@path = path
|
|
44
|
+
@name = data.fetch('name')
|
|
45
|
+
@version = data.fetch('version')
|
|
46
|
+
@description = data['description']
|
|
47
|
+
@upstream = data['upstream'] || {}
|
|
48
|
+
@doctypes = data['doctypes'] || []
|
|
49
|
+
fonts = data['fonts'] || {}
|
|
50
|
+
@required_fonts = fonts['required'] || []
|
|
51
|
+
@optional_fonts = fonts['optional'] || []
|
|
52
|
+
@config_files = data.fetch('config_files')
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def config_path_for(role, flavor_dir = File.dirname(@path))
|
|
57
|
+
file = config_files[role.to_s] || config_files[role.to_sym]
|
|
58
|
+
return nil unless file
|
|
59
|
+
|
|
60
|
+
File.join(flavor_dir, file)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ==(other)
|
|
64
|
+
other.is_a?(self.class) &&
|
|
65
|
+
path == other.path &&
|
|
66
|
+
name == other.name &&
|
|
67
|
+
version == other.version &&
|
|
68
|
+
description == other.description &&
|
|
69
|
+
doctypes == other.doctypes &&
|
|
70
|
+
required_fonts == other.required_fonts &&
|
|
71
|
+
optional_fonts == other.optional_fonts &&
|
|
72
|
+
config_files == other.config_files
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
alias eql? ==
|
|
76
|
+
|
|
77
|
+
def hash
|
|
78
|
+
[self.class, path, name, version, config_files].hash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.validate_required_fields!(data, manifest_path)
|
|
82
|
+
missing = REQUIRED_FIELDS.reject { |field| data.key?(field) }
|
|
83
|
+
return if missing.empty?
|
|
84
|
+
|
|
85
|
+
raise ::Arrolio::FlavorError.new(
|
|
86
|
+
"manifest at #{manifest_path} is missing required fields: #{missing.inspect}",
|
|
87
|
+
manifest_path: manifest_path, missing_fields: missing
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
private_class_method :validate_required_fields!
|
|
91
|
+
|
|
92
|
+
def self.validate_config_files!(data, manifest_path, flavor_dir)
|
|
93
|
+
config_files = data['config_files']
|
|
94
|
+
return unless config_files.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
['layout_spec', 'adapter_rules', 'flow_rules'].each do |role|
|
|
97
|
+
filename = config_files[role]
|
|
98
|
+
next unless filename
|
|
99
|
+
next if File.exist?(File.join(flavor_dir, filename))
|
|
100
|
+
|
|
101
|
+
raise ::Arrolio::FlavorError.new(
|
|
102
|
+
"manifest at #{manifest_path} references #{role}=#{filename}, " \
|
|
103
|
+
'but the file does not exist',
|
|
104
|
+
manifest_path: manifest_path, missing_role: role
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
private_class_method :validate_config_files!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|