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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
module Harness
|
|
7
|
+
# Extracts text from PDF files for diff/matching. Uses poppler's
|
|
8
|
+
# +pdftotext+ as the primary backend (reliable with CIDFont
|
|
9
|
+
# subsets); falls back to returning an empty string if the
|
|
10
|
+
# tool is unavailable.
|
|
11
|
+
class TextExtractor
|
|
12
|
+
attr_reader :path
|
|
13
|
+
|
|
14
|
+
def initialize(path)
|
|
15
|
+
@path = path.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns an Array of Strings — one per page. Uses +pdftotext+
|
|
19
|
+
# in raw mode (no layout reconstruction) for maximum text
|
|
20
|
+
# recovery. For layout-aware extraction, pass +layout: true+.
|
|
21
|
+
def pages(layout: false)
|
|
22
|
+
return [] unless pdftotext_available?
|
|
23
|
+
|
|
24
|
+
args = ['pdftotext']
|
|
25
|
+
args << '-layout' if layout
|
|
26
|
+
args << '-q' << @path << '-'
|
|
27
|
+
stdout, _stderr, status = Open3.capture3(*args)
|
|
28
|
+
return [] unless status.success?
|
|
29
|
+
|
|
30
|
+
text = stdout.force_encoding('UTF-8').scrub
|
|
31
|
+
split_by_page(text)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns all text as a single String (pages joined by "\f").
|
|
35
|
+
def full_text(layout: false)
|
|
36
|
+
pages(layout: layout).join("\f")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Word count across all pages.
|
|
40
|
+
def word_count
|
|
41
|
+
pages.sum { |text| text.split.length }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def pdftotext_available?
|
|
47
|
+
_, _, status = Open3.capture3('which', 'pdftotext')
|
|
48
|
+
status.success?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def split_by_page(text)
|
|
52
|
+
return [text] unless text.include?("\f")
|
|
53
|
+
|
|
54
|
+
text.split("\f", -1)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
module Harness
|
|
5
|
+
autoload :PdfDiff, 'arrolio/harness/pdf_diff'
|
|
6
|
+
autoload :TextDiff, 'arrolio/harness/text_diff'
|
|
7
|
+
autoload :TextExtractor, 'arrolio/harness/text_extractor'
|
|
8
|
+
autoload :PixelDiff, 'arrolio/harness/pixel_diff'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# One inline run within a paragraph: a contiguous span of text
|
|
5
|
+
# sharing a single Style. A paragraph is laid out as a list of
|
|
6
|
+
# InlineRun instances. Lives at the Layout level so that other
|
|
7
|
+
# Layout modules (InlineBuilder, TextLayout, FO::Compiler) all
|
|
8
|
+
# share the same type.
|
|
9
|
+
#
|
|
10
|
+
# Carries optional baseline_shift (:sub / :sup / nil) and
|
|
11
|
+
# font_size_scale for subscript/superscript rendering, plus
|
|
12
|
+
# optional href for hyperlink annotations.
|
|
13
|
+
class InlineRun
|
|
14
|
+
attr_reader :text, :style, :baseline_shift, :font_size_scale, :href
|
|
15
|
+
|
|
16
|
+
def initialize(text, style: Style.new, baseline_shift: nil,
|
|
17
|
+
font_size_scale: 1.0, href: nil)
|
|
18
|
+
@text = text.to_s
|
|
19
|
+
@style = style
|
|
20
|
+
@baseline_shift = baseline_shift
|
|
21
|
+
@font_size_scale = font_size_scale.to_f
|
|
22
|
+
@href = href
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def empty?
|
|
26
|
+
@text.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def length
|
|
30
|
+
@text.length
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def width(measurer, font_size: style.font_size)
|
|
34
|
+
measurer.width_of_run(
|
|
35
|
+
@text,
|
|
36
|
+
font_size: font_size * @font_size_scale,
|
|
37
|
+
character_spacing: style.character_spacing,
|
|
38
|
+
word_spacing: style.word_spacing
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hyperlink?
|
|
43
|
+
!@href.nil? && !@href.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def ==(other)
|
|
47
|
+
other.is_a?(self.class) &&
|
|
48
|
+
text == other.text &&
|
|
49
|
+
style == other.style &&
|
|
50
|
+
baseline_shift == other.baseline_shift &&
|
|
51
|
+
font_size_scale == other.font_size_scale &&
|
|
52
|
+
href == other.href
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def hash
|
|
56
|
+
[text, style, baseline_shift, font_size_scale, href].hash
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
class LayoutSpec
|
|
5
|
+
# Declares a named content flow into a region. Declarative
|
|
6
|
+
# only — Engine::Paged takes a flat Flowable list and a body
|
|
7
|
+
# template today.
|
|
8
|
+
Flow = Struct.new(:name, :region, :source, :default_style_id,
|
|
9
|
+
keyword_init: true) do
|
|
10
|
+
def initialize(*)
|
|
11
|
+
super
|
|
12
|
+
self.name = self.name.to_sym if self.name
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Arrolio
|
|
6
|
+
class LayoutSpec
|
|
7
|
+
# Loads a LayoutSpec from YAML. Lengths accept mm/cm/in/pt/px.
|
|
8
|
+
module Loader
|
|
9
|
+
MM_TO_PT = 2.83464567
|
|
10
|
+
CM_TO_PT = 28.3464567
|
|
11
|
+
IN_TO_PT = 72.0
|
|
12
|
+
PT_TO_PT = 1.0
|
|
13
|
+
PX_TO_PT = 0.75
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def load(source)
|
|
18
|
+
build_from_hash(read_yaml(source))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read_yaml(source)
|
|
22
|
+
case source
|
|
23
|
+
when Hash then source
|
|
24
|
+
when String
|
|
25
|
+
if File.exist?(source)
|
|
26
|
+
result = YAML.safe_load_file(source, aliases: true)
|
|
27
|
+
result.is_a?(Hash) ? result : {}
|
|
28
|
+
else
|
|
29
|
+
result = YAML.safe_load(source, aliases: true)
|
|
30
|
+
result.is_a?(Hash) ? result : {}
|
|
31
|
+
end
|
|
32
|
+
when IO, StringIO
|
|
33
|
+
result = YAML.safe_load(source.read, aliases: true)
|
|
34
|
+
result.is_a?(Hash) ? result : {}
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "Loader needs Hash/String/IO; got #{source.class}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_from_hash(hash)
|
|
41
|
+
templates = build_templates(hash.fetch('page_templates', {}))
|
|
42
|
+
default = hash.fetch('default_page_template') do
|
|
43
|
+
templates.keys.first or
|
|
44
|
+
raise LayoutSpecError, 'no page templates in layout spec', spec_field: :page_templates
|
|
45
|
+
end
|
|
46
|
+
styles = Style::Registry.new(hash.fetch('styles', {}))
|
|
47
|
+
flows = hash.fetch('flows', {}).transform_values do |f|
|
|
48
|
+
f = { 'region' => f } unless f.is_a?(Hash)
|
|
49
|
+
Flow.new(
|
|
50
|
+
name: f['name'],
|
|
51
|
+
region: f['region']&.to_sym,
|
|
52
|
+
source: f['source'],
|
|
53
|
+
default_style_id: f['default_style_id']&.to_sym
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
LayoutSpec.new(
|
|
57
|
+
page_templates: templates,
|
|
58
|
+
default_page_template: default,
|
|
59
|
+
styles: styles,
|
|
60
|
+
flows: flows,
|
|
61
|
+
font_paths: hash.fetch('font_paths', {}),
|
|
62
|
+
header_footer: hash.fetch('header_footer', {}),
|
|
63
|
+
cover_logo: hash.fetch('cover_logo', {}),
|
|
64
|
+
font_manifest: hash.fetch('font_manifest', {})
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_templates(hash)
|
|
69
|
+
hash.to_h do |name, attrs|
|
|
70
|
+
attrs = {} unless attrs.is_a?(Hash)
|
|
71
|
+
[name, build_template(name, attrs)]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_template(name, attrs)
|
|
76
|
+
PageTemplate.new(
|
|
77
|
+
name: name,
|
|
78
|
+
page_size: attrs['page_size'] || PageTemplate::LETTER,
|
|
79
|
+
margins: build_margins(attrs),
|
|
80
|
+
region_extents: build_region_extents(attrs.fetch('region_extents', {}))
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_margins(attrs)
|
|
85
|
+
margins = attrs['margins']
|
|
86
|
+
return PageTemplate::LETTER.first if margins.nil?
|
|
87
|
+
|
|
88
|
+
case margins
|
|
89
|
+
when Numeric, String then { all: parse_length(margins) }
|
|
90
|
+
when Hash
|
|
91
|
+
margins.to_h { |k, v| [k.to_sym, parse_length(v)] }
|
|
92
|
+
else
|
|
93
|
+
raise LayoutSpecError, 'margins must be number/string/hash', spec_field: :margins
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_region_extents(hash)
|
|
98
|
+
hash.to_h { |k, v| [k.to_sym, parse_length(v)] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_length(value)
|
|
102
|
+
return value.to_f if value.is_a?(Numeric)
|
|
103
|
+
|
|
104
|
+
case value.to_s
|
|
105
|
+
when /\A([\d.]+)mm\z/ then Regexp.last_match(1).to_f * MM_TO_PT
|
|
106
|
+
when /\A([\d.]+)cm\z/ then Regexp.last_match(1).to_f * CM_TO_PT
|
|
107
|
+
when /\A([\d.]+)in\z/ then Regexp.last_match(1).to_f * IN_TO_PT
|
|
108
|
+
when /\A([\d.]+)px\z/ then Regexp.last_match(1).to_f * PX_TO_PT
|
|
109
|
+
when /\A([\d.]+)pt\z/ then Regexp.last_match(1).to_f * PT_TO_PT
|
|
110
|
+
when /\A[\d.]+\z/ then value.to_f
|
|
111
|
+
else
|
|
112
|
+
raise LayoutSpecError, "cannot parse length #{value.inspect}", spec_field: :length
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
class LayoutSpec
|
|
5
|
+
# Page geometry template ("fo:simple-page-master" in XSL-FO).
|
|
6
|
+
# Coordinates are PDF-default user space: origin at the
|
|
7
|
+
# bottom-left of the page, +y+ grows upward. Region +y+ is the
|
|
8
|
+
# bottom edge of the region.
|
|
9
|
+
class PageTemplate
|
|
10
|
+
LETTER = [612.0, 792.0].freeze
|
|
11
|
+
A4 = [595.276, 841.89].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :name, :page_size, :margins, :region_extents, :regions
|
|
14
|
+
|
|
15
|
+
def initialize(name:, page_size: LETTER, margins: 72, region_extents: {})
|
|
16
|
+
@name = name.to_sym
|
|
17
|
+
@page_size = resolve_page_size(page_size)
|
|
18
|
+
@margins = normalise_margins(margins)
|
|
19
|
+
@region_extents = default_region_extents.merge(region_extents.to_h)
|
|
20
|
+
@regions = compute_regions.freeze
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def body_region
|
|
25
|
+
@regions[:body]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def region(name)
|
|
29
|
+
@regions[name.to_sym]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def width
|
|
33
|
+
@page_size[0]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def height
|
|
37
|
+
@page_size[1]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
other.is_a?(self.class) &&
|
|
42
|
+
name == other.name &&
|
|
43
|
+
page_size == other.page_size &&
|
|
44
|
+
margins == other.margins &&
|
|
45
|
+
region_extents == other.region_extents
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias eql? ==
|
|
49
|
+
|
|
50
|
+
def hash
|
|
51
|
+
[self.class, name, page_size, margins, region_extents].hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def resolve_page_size(spec)
|
|
57
|
+
case spec
|
|
58
|
+
when Symbol, String
|
|
59
|
+
constant = spec.to_s.upcase.to_sym
|
|
60
|
+
self.class.const_defined?(constant, false) ? self.class.const_get(constant) : LETTER
|
|
61
|
+
when Array
|
|
62
|
+
[spec[0].to_f, spec[1].to_f]
|
|
63
|
+
else
|
|
64
|
+
LETTER
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def normalise_margins(m)
|
|
69
|
+
case m
|
|
70
|
+
when Numeric
|
|
71
|
+
{ top: m.to_f, right: m.to_f, bottom: m.to_f, left: m.to_f }
|
|
72
|
+
when Hash
|
|
73
|
+
base = m.transform_keys(&:to_sym)
|
|
74
|
+
all = base[:all]
|
|
75
|
+
{
|
|
76
|
+
top: base[:top] || all || 72.0,
|
|
77
|
+
right: base[:right] || all || 72.0,
|
|
78
|
+
bottom: base[:bottom] || all || 72.0,
|
|
79
|
+
left: base[:left] || all || 72.0
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
{ top: 72.0, right: 72.0, bottom: 72.0, left: 72.0 }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def default_region_extents
|
|
87
|
+
{ before: 0.0, after: 0.0, start: 0.0, end: 0.0 }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def compute_regions
|
|
91
|
+
pw = width
|
|
92
|
+
ph = height
|
|
93
|
+
m = @margins
|
|
94
|
+
e = @region_extents
|
|
95
|
+
inner_w = pw - m[:left] - m[:right] - e[:start] - e[:end]
|
|
96
|
+
inner_h = ph - m[:top] - m[:bottom] - e[:before] - e[:after]
|
|
97
|
+
{
|
|
98
|
+
body: Region.new(name: :body,
|
|
99
|
+
x: m[:left] + e[:start],
|
|
100
|
+
y: m[:bottom] + e[:after],
|
|
101
|
+
width: inner_w,
|
|
102
|
+
height: inner_h),
|
|
103
|
+
before: Region.new(name: :before,
|
|
104
|
+
x: m[:left] + e[:start],
|
|
105
|
+
y: ph - m[:top] - e[:before],
|
|
106
|
+
width: inner_w,
|
|
107
|
+
height: e[:before]),
|
|
108
|
+
after: Region.new(name: :after,
|
|
109
|
+
x: m[:left] + e[:start],
|
|
110
|
+
y: m[:bottom],
|
|
111
|
+
width: inner_w,
|
|
112
|
+
height: e[:after]),
|
|
113
|
+
start: Region.new(name: :start,
|
|
114
|
+
x: m[:left],
|
|
115
|
+
y: m[:bottom] + e[:after],
|
|
116
|
+
width: e[:start],
|
|
117
|
+
height: inner_h),
|
|
118
|
+
end: Region.new(name: :end,
|
|
119
|
+
x: pw - m[:right] - e[:end],
|
|
120
|
+
y: m[:bottom] + e[:after],
|
|
121
|
+
width: e[:end],
|
|
122
|
+
height: inner_h)
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
class LayoutSpec
|
|
5
|
+
# Selects the appropriate page template based on page parity
|
|
6
|
+
# (odd/even) and the configured template set. Supports three
|
|
7
|
+
# modes:
|
|
8
|
+
# - Single template (current behavior): always returns the
|
|
9
|
+
# same template for every page.
|
|
10
|
+
# - Odd/even templates: returns different templates for odd
|
|
11
|
+
# vs even pages, typically mirroring the inner/outer margins
|
|
12
|
+
# for double-sided printing.
|
|
13
|
+
# - Custom mapping: caller provides a proc or hash mapping
|
|
14
|
+
# page numbers to template names.
|
|
15
|
+
class PageTemplateSelector
|
|
16
|
+
attr_reader :default_template, :odd_template, :even_template
|
|
17
|
+
|
|
18
|
+
# +default_template:+ template name used when no odd/even
|
|
19
|
+
# templates are configured.
|
|
20
|
+
# +odd_template:+ template name for odd pages (1, 3, 5, ...).
|
|
21
|
+
# +even_template:+ template name for even pages (2, 4, 6, ...).
|
|
22
|
+
def initialize(default_template:, odd_template: nil, even_template: nil)
|
|
23
|
+
@default_template = default_template.to_sym
|
|
24
|
+
@odd_template = odd_template&.to_sym
|
|
25
|
+
@even_template = even_template&.to_sym
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the template name for the given page number.
|
|
30
|
+
def name_for(page_number)
|
|
31
|
+
return @default_template unless @odd_template || @even_template
|
|
32
|
+
|
|
33
|
+
if page_number.odd?
|
|
34
|
+
(@odd_template || @default_template)
|
|
35
|
+
else
|
|
36
|
+
(@even_template || @default_template)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Does this selector use different templates for odd/even?
|
|
41
|
+
def alternating?
|
|
42
|
+
!(@odd_template.nil? && @even_template.nil?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ==(other)
|
|
46
|
+
other.is_a?(self.class) &&
|
|
47
|
+
default_template == other.default_template &&
|
|
48
|
+
odd_template == other.odd_template &&
|
|
49
|
+
even_template == other.even_template
|
|
50
|
+
end
|
|
51
|
+
alias eql? ==
|
|
52
|
+
|
|
53
|
+
def hash
|
|
54
|
+
[self.class, default_template, odd_template, even_template].hash
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
class LayoutSpec
|
|
5
|
+
# Named rectangle within a PageTemplate. The five standard
|
|
6
|
+
# regions are +:body+, +:before+ (header), +:after+ (footer),
|
|
7
|
+
# +:start+ (left), +:end+ (right). Pure geometry; runtime
|
|
8
|
+
# consumed state lives in Frame.
|
|
9
|
+
class Region
|
|
10
|
+
attr_reader :name, :x, :y, :width, :height
|
|
11
|
+
|
|
12
|
+
def initialize(name:, x:, y:, width:, height:)
|
|
13
|
+
@name = name.to_sym
|
|
14
|
+
@x = x.to_f
|
|
15
|
+
@y = y.to_f
|
|
16
|
+
@width = width.to_f
|
|
17
|
+
@height = height.to_f
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def area
|
|
22
|
+
@width * @height
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def ==(other)
|
|
26
|
+
other.is_a?(self.class) &&
|
|
27
|
+
name == other.name && x == other.x && y == other.y &&
|
|
28
|
+
width == other.width && height == other.height
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[self.class, name, x, y, width, height].hash
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
class LayoutSpec
|
|
5
|
+
class ScaleLength
|
|
6
|
+
attr_reader :base_value, :scale_factor
|
|
7
|
+
|
|
8
|
+
def initialize(base_value:, scale_factor: 1.0)
|
|
9
|
+
@base_value = base_value.to_f
|
|
10
|
+
@scale_factor = scale_factor.to_f
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolved
|
|
15
|
+
@base_value * @scale_factor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with_scale(factor)
|
|
19
|
+
self.class.new(base_value: @base_value, scale_factor: factor)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(self.class) &&
|
|
24
|
+
base_value == other.base_value &&
|
|
25
|
+
scale_factor == other.scale_factor
|
|
26
|
+
end
|
|
27
|
+
alias eql? ==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[self.class, base_value, scale_factor].hash
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arrolio::LayoutSpec — typed layout description, YAML-loadable.
|
|
4
|
+
module Arrolio
|
|
5
|
+
class LayoutSpec
|
|
6
|
+
autoload :PageTemplate, 'arrolio/layout_spec/page_template'
|
|
7
|
+
autoload :PageTemplateSelector, 'arrolio/layout_spec/page_template_selector'
|
|
8
|
+
autoload :ScaleLength, 'arrolio/layout_spec/scale_length'
|
|
9
|
+
autoload :Region, 'arrolio/layout_spec/region'
|
|
10
|
+
autoload :Flow, 'arrolio/layout_spec/flow'
|
|
11
|
+
autoload :Loader, 'arrolio/layout_spec/loader'
|
|
12
|
+
|
|
13
|
+
attr_reader :page_templates, :default_page_template, :styles, :flows,
|
|
14
|
+
:font_paths, :header_footer_config, :cover_logo_config,
|
|
15
|
+
:font_manifest_config
|
|
16
|
+
|
|
17
|
+
def initialize(page_templates:, default_page_template:,
|
|
18
|
+
styles:, flows: {}, font_paths: {},
|
|
19
|
+
header_footer: {}, cover_logo: {}, font_manifest: {})
|
|
20
|
+
@font_paths = font_paths.transform_keys(&:to_s).freeze
|
|
21
|
+
@page_templates = page_templates.transform_keys(&:to_sym).freeze
|
|
22
|
+
@default_page_template = default_page_template.to_sym
|
|
23
|
+
@styles = styles
|
|
24
|
+
@flows = flows.transform_keys(&:to_sym).freeze
|
|
25
|
+
@header_footer_config = (header_footer || {}).freeze
|
|
26
|
+
@cover_logo_config = (cover_logo || {}).freeze
|
|
27
|
+
@font_manifest_config = (font_manifest || {}).freeze
|
|
28
|
+
validate!
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def page_template(name = nil)
|
|
33
|
+
key = (name || @default_page_template).to_sym
|
|
34
|
+
@page_templates[key] ||
|
|
35
|
+
(raise LayoutSpecError,
|
|
36
|
+
"Unknown page template #{key.inspect}; have: #{@page_templates.keys.inspect}",
|
|
37
|
+
spec_field: :page_template)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_style(name, fallback: nil)
|
|
41
|
+
@styles.resolve(name, fallback: fallback || Style::Definition.new)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def flow(name)
|
|
45
|
+
@flows[name.to_sym]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ==(other)
|
|
49
|
+
other.is_a?(self.class) &&
|
|
50
|
+
page_templates == other.page_templates &&
|
|
51
|
+
default_page_template == other.default_page_template &&
|
|
52
|
+
styles == other.styles &&
|
|
53
|
+
flows == other.flows &&
|
|
54
|
+
header_footer_config == other.header_footer_config &&
|
|
55
|
+
cover_logo_config == other.cover_logo_config &&
|
|
56
|
+
font_manifest_config == other.font_manifest_config
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
alias eql? ==
|
|
60
|
+
|
|
61
|
+
def hash
|
|
62
|
+
[self.class, page_templates, default_page_template, styles, flows,
|
|
63
|
+
header_footer_config, cover_logo_config, font_manifest_config].hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def validate!
|
|
69
|
+
unless @page_templates.key?(@default_page_template)
|
|
70
|
+
raise LayoutSpecError,
|
|
71
|
+
"Default page template #{@default_page_template.inspect} is not in page_templates",
|
|
72
|
+
spec_field: :default_page_template
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless @styles.is_a?(Style::Registry)
|
|
76
|
+
raise LayoutSpecError,
|
|
77
|
+
"styles must be a Style::Registry (got #{@styles.class})",
|
|
78
|
+
spec_field: :styles
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
@page_templates.each do |name, tpl|
|
|
82
|
+
next if tpl.is_a?(PageTemplate)
|
|
83
|
+
|
|
84
|
+
raise LayoutSpecError,
|
|
85
|
+
"page_templates[#{name.inspect}] must be a PageTemplate",
|
|
86
|
+
spec_field: :page_templates
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arrolio
|
|
4
|
+
# Structured logging facade. Replaces scattered +warn+ calls with
|
|
5
|
+
# level-filtered, silencable output. Default level +:warn+ keeps
|
|
6
|
+
# production rendering quiet; set +Arrolio::Logger.level = :debug+
|
|
7
|
+
# or +ARROOLIO_LOG_LEVEL=debug+ to restore tracing during development.
|
|
8
|
+
class Logger
|
|
9
|
+
LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }.freeze
|
|
10
|
+
DEFAULT_LEVEL = :warn
|
|
11
|
+
|
|
12
|
+
@level = LEVELS.fetch(DEFAULT_LEVEL)
|
|
13
|
+
@target = $stderr
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
attr_accessor :target
|
|
17
|
+
attr_reader :level
|
|
18
|
+
|
|
19
|
+
def level=(name)
|
|
20
|
+
@level = LEVELS.fetch(name.to_sym) { LEVELS.fetch(DEFAULT_LEVEL) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def level_name
|
|
24
|
+
LEVELS.key(@level) || DEFAULT_LEVEL
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
LEVELS.each_key do |name|
|
|
28
|
+
define_method(name) do |msg|
|
|
29
|
+
emit(name, msg)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Reset to defaults — used by specs and between pipeline runs.
|
|
34
|
+
def reset!
|
|
35
|
+
@level = LEVELS.fetch(DEFAULT_LEVEL)
|
|
36
|
+
@target = $stderr
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def emit(level_name, msg)
|
|
42
|
+
threshold = LEVELS.fetch(level_name)
|
|
43
|
+
return if threshold < @level
|
|
44
|
+
|
|
45
|
+
@target.puts "[#{level_name}] #{msg}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Pick up the env var on first load. Allows
|
|
50
|
+
# +ARROOLIO_LOG_LEVEL=debug bundle exec rake+ without code changes.
|
|
51
|
+
env_level = ENV['ARROOLIO_LOG_LEVEL'].to_s.strip
|
|
52
|
+
self.level = env_level unless env_level.empty?
|
|
53
|
+
end
|
|
54
|
+
end
|