claricle 0.1.0 → 0.2.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 +4 -4
- data/.rubocop.yml +10 -1
- data/LICENSE +25 -0
- data/README.adoc +439 -157
- data/TODO.plan/00-overview.md +502 -0
- data/TODO.plan/01-core.md +296 -0
- data/TODO.plan/02-inspect.md +163 -0
- data/TODO.plan/03-conform.md +208 -0
- data/TODO.plan/04-convert.md +216 -0
- data/exe/claricle +1 -1
- data/lib/claricle/batch.rb +240 -0
- data/lib/claricle/cli.rb +514 -40
- data/lib/claricle/conformance_options.rb +49 -0
- data/lib/claricle/conversion_engine.rb +21 -0
- data/lib/claricle/convert.rb +21 -0
- data/lib/claricle/detector.rb +696 -0
- data/lib/claricle/emf_header.rb +33 -0
- data/lib/claricle/errors.rb +73 -0
- data/lib/claricle/fault.rb +69 -0
- data/lib/claricle/handlers/base.rb +169 -0
- data/lib/claricle/handlers/extension_points.rb +124 -0
- data/lib/claricle/handlers/metafile.rb +1011 -0
- data/lib/claricle/handlers/pdf.rb +961 -0
- data/lib/claricle/handlers/pdf_arlington/definitions.rb +54 -0
- data/lib/claricle/handlers/pdf_arlington/kind.rb +62 -0
- data/lib/claricle/handlers/pdf_arlington/predicate_gate.rb +39 -0
- data/lib/claricle/handlers/pdf_arlington/rules.rb +86 -0
- data/lib/claricle/handlers/pdf_arlington/walker.rb +131 -0
- data/lib/claricle/handlers/pdf_arlington.rb +38 -0
- data/lib/claricle/handlers/pdf_profiles.rb +66 -0
- data/lib/claricle/handlers/png.rb +1107 -0
- data/lib/claricle/handlers/postscript.rb +1357 -0
- data/lib/claricle/handlers/svg.rb +787 -0
- data/lib/claricle/image.rb +272 -0
- data/lib/claricle/lossiness/levels.rb +13 -0
- data/lib/claricle/lossiness.rb +864 -0
- data/lib/claricle/models/base.rb +460 -0
- data/lib/claricle/models/batch_item.rb +127 -0
- data/lib/claricle/models/conversion.rb +167 -0
- data/lib/claricle/models/format_capability.rb +40 -0
- data/lib/claricle/models/free_form_hash.rb +77 -0
- data/lib/claricle/models/inspection.rb +188 -0
- data/lib/claricle/models/issue.rb +23 -0
- data/lib/claricle/models/location.rb +51 -0
- data/lib/claricle/models/report.rb +54 -0
- data/lib/claricle/models/uncoerced_integer.rb +38 -0
- data/lib/claricle/postscript_fidelity/verdict.rb +37 -0
- data/lib/claricle/postscript_fidelity/view_box.rb +82 -0
- data/lib/claricle/postscript_fidelity.rb +160 -0
- data/lib/claricle/registry.rb +125 -0
- data/lib/claricle/version.rb +1 -1
- data/lib/claricle/writer.rb +468 -0
- data/lib/claricle.rb +244 -7
- metadata +201 -19
- data/claricle.gemspec +0 -42
- data/sig/claricle.rbs +0 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../detector"
|
|
4
|
+
|
|
5
|
+
module Claricle
|
|
6
|
+
module PostscriptFidelity
|
|
7
|
+
# The root viewBox, which postsvg reads as [llx lly urx ury] instead of
|
|
8
|
+
# [x y width height]: only an origin of 0 0 is read correctly.
|
|
9
|
+
module ViewBox
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# [x, y, width, height], or nil unless four numbers.
|
|
13
|
+
def parse(text)
|
|
14
|
+
parts = text.to_s.strip.split(/[\s,]+/).map { |part| Float(part, exception: false) }
|
|
15
|
+
parts if parts.length == 4 && parts.all?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def root(svg) = parse(Detector.read_root(svg)&.last&.fetch("viewBox", nil))
|
|
19
|
+
|
|
20
|
+
# The height of the viewport `orient` flips about, or nil when unknown.
|
|
21
|
+
def viewport_sum(svg)
|
|
22
|
+
attributes = Detector.read_root(svg)&.last or return
|
|
23
|
+
return height_at_origin(attributes["viewBox"]) if attributes["viewBox"]
|
|
24
|
+
|
|
25
|
+
height = PostscriptFidelity::LENGTH.match(attributes["height"].to_s)
|
|
26
|
+
Float(height[1]) if height
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# [width, height, scale, dx, dy] mapping the viewBox into the width/height
|
|
30
|
+
# viewport (preserveAspectRatio's default, xMidYMid meet), or nil when
|
|
31
|
+
# the viewBox already is the viewport.
|
|
32
|
+
def fit(svg)
|
|
33
|
+
attributes = Detector.read_root(svg)&.last or return
|
|
34
|
+
box = parse(attributes["viewBox"])
|
|
35
|
+
return unless box && box[2].positive? && box[3].positive?
|
|
36
|
+
|
|
37
|
+
width, height = viewport(attributes, box)
|
|
38
|
+
place(width, height, box) unless [width, height] == box[2, 2]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def place(width, height, box)
|
|
42
|
+
scale = [width / box[2], height / box[3]].min
|
|
43
|
+
[width, height, scale, *[width - (scale * box[2]), height - (scale * box[3])].map { |slack| slack / 2 }]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A missing width or height follows the viewBox's aspect ratio.
|
|
47
|
+
def viewport(attributes, box)
|
|
48
|
+
width, height = %w[width height].map { |name| length(attributes[name]) }
|
|
49
|
+
width ||= height ? height * box[2] / box[3] : box[2]
|
|
50
|
+
[width, height || (width * box[3] / box[2])]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def length(text)
|
|
54
|
+
match = PostscriptFidelity::LENGTH.match(text.to_s)
|
|
55
|
+
Float(match[1]) if match
|
|
56
|
+
end
|
|
57
|
+
private_class_method :place, :viewport, :length
|
|
58
|
+
|
|
59
|
+
# The origin the delegate was NOT shown: `rebase_origin?` zeroed it, so `orient` must shift by it.
|
|
60
|
+
def rebased_origin(original, fixed)
|
|
61
|
+
before = root(original)
|
|
62
|
+
after = root(fixed)
|
|
63
|
+
after && after[0, 2] == [0.0, 0.0] && before ? before[0, 2] : [0.0, 0.0]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Rewrites a non-zero origin to 0 0; true when it changed.
|
|
67
|
+
def rebase_origin?(element)
|
|
68
|
+
box = parse(element.attributes["viewBox"])
|
|
69
|
+
return false if box.nil? || box[0, 2] == [0.0, 0.0]
|
|
70
|
+
|
|
71
|
+
element.attributes["viewBox"] = "0 0 #{box[2]} #{box[3]}"
|
|
72
|
+
true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def height_at_origin(text)
|
|
76
|
+
x, y, _, height = parse(text)
|
|
77
|
+
height if x&.zero? && y&.zero?
|
|
78
|
+
end
|
|
79
|
+
private_class_method :height_at_origin
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rexml/document"
|
|
4
|
+
require_relative "detector"
|
|
5
|
+
require_relative "postscript_fidelity/view_box"
|
|
6
|
+
require_relative "postscript_fidelity/verdict"
|
|
7
|
+
|
|
8
|
+
module Claricle
|
|
9
|
+
# Closes seven silent losses in postsvg-0.3.0's SVG -> PS/EPS writer, so the
|
|
10
|
+
# `:lossless` verdict for the shapes Lossiness proves kept is true.
|
|
11
|
+
# `repair` rewrites the SVG before the writer sees it; `orient` fixes the
|
|
12
|
+
# PostScript it returns. Both pass their input through untouched when they
|
|
13
|
+
# cannot read it, leaving the writer's own error to surface.
|
|
14
|
+
#
|
|
15
|
+
# color.rb:60 /\argb\(/ is BEL, not \A: rgb() never parses, paint is dropped
|
|
16
|
+
# attribute_parser.rb:25 number() takes bare "-?\d+(\.\d+)?" only: "10px", ".5", "+5" read as 0
|
|
17
|
+
# line_handler.rb:12 a <line> is always stroked; SVG's default stroke is none
|
|
18
|
+
# (no code) user space is y-down; PostScript is y-up and nothing flips it.
|
|
19
|
+
# `translate` + `scale`, not `concat`: Postsvg.to_svg ignores concat.
|
|
20
|
+
# svg/parser.rb:27-33 viewBox is read as [llx lly urx ury], so a non-zero origin is
|
|
21
|
+
# misplaced: `repair` rewrites it to origin 0 0, `orient` shifts by it.
|
|
22
|
+
# (no code) width/height are ignored when a viewBox is present: `orient` fits the
|
|
23
|
+
# viewBox into them (xMidYMid meet) and sizes the page to them.
|
|
24
|
+
# (no code) the flip mirrors `show` text: each is counter-flipped about its origin.
|
|
25
|
+
# (`<image>` is emitted as a bare `image` operator with no data: nothing to flip.)
|
|
26
|
+
module PostscriptFidelity
|
|
27
|
+
SVG_NAMESPACE = "http://www.w3.org/2000/svg"
|
|
28
|
+
LENGTH_ATTRIBUTES = %w[width height x y x1 y1 x2 y2].freeze
|
|
29
|
+
PAINT_ATTRIBUTES = %w[fill stroke].freeze
|
|
30
|
+
LENGTH = /\A[ \t\r\n]*([+-]?(?:\d+\.\d+|\.\d+|\d+))(?:px)?[ \t\r\n]*\z/i
|
|
31
|
+
RGB = /\A[ \t\r\n]*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)[ \t\r\n]*\z/
|
|
32
|
+
STROKE_INHERITING = %w[stroke style class].freeze
|
|
33
|
+
BBOX = /^%%BoundingBox: (\S+) (\S+) (\S+) (\S+)$/
|
|
34
|
+
HINT = /px|rgb\(|<line|[+.]\d|viewBox/i
|
|
35
|
+
SHOW = /^(\S+) (\S+) moveto\n(\((?:\\.|[^\\)])*\) show)$/
|
|
36
|
+
|
|
37
|
+
POSTSCRIPT_TARGETS = %i[eps ps].freeze
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Yields the SVG to hand the delegate and returns what it produced,
|
|
42
|
+
# corrected. Other targets pass straight through.
|
|
43
|
+
def convert(svg, target)
|
|
44
|
+
return yield(svg) unless POSTSCRIPT_TARGETS.include?(target)
|
|
45
|
+
|
|
46
|
+
fixed = repair(svg)
|
|
47
|
+
orient(yield(fixed), fixed, ViewBox.rebased_origin(svg, fixed))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def repair(svg)
|
|
51
|
+
return svg unless svg.match?(HINT)
|
|
52
|
+
|
|
53
|
+
doc = REXML::Document.new(svg)
|
|
54
|
+
return svg unless doc.root && doc.encoding == "UTF-8"
|
|
55
|
+
|
|
56
|
+
changed = svg_elements(doc.root).map { |element| repaired?(element, doc) }
|
|
57
|
+
changed << ViewBox.rebase_origin?(doc.root)
|
|
58
|
+
changed.any? ? doc.to_s : svg
|
|
59
|
+
rescue REXML::ParseException, RuntimeError, EncodingError, ArgumentError
|
|
60
|
+
svg
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def orient(postscript, svg, origin = [0.0, 0.0])
|
|
64
|
+
match = BBOX.match(postscript)
|
|
65
|
+
return postscript unless match && postscript.include?("%%EndComments\n")
|
|
66
|
+
|
|
67
|
+
box = match.captures.map { |text| Float(text, exception: false) }
|
|
68
|
+
return postscript unless box.all?
|
|
69
|
+
|
|
70
|
+
fit = ViewBox.fit(svg)
|
|
71
|
+
return upright(scaled(postscript, fit, origin)) if fit
|
|
72
|
+
|
|
73
|
+
upright(reflect(postscript, box, ViewBox.viewport_sum(svg) || (box[1] + box[3]), origin))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# width/height differ from the viewBox: postsvg sized the page to the viewBox and applied no scale.
|
|
77
|
+
def scaled(postscript, fit, origin)
|
|
78
|
+
width, height, scale, dx, dy = fit
|
|
79
|
+
shift = origin_shift(origin)
|
|
80
|
+
steps = "#{number(dx)} #{number(dy)} translate\n#{number(scale)} #{number(scale)} scale\n"
|
|
81
|
+
postscript.sub(BBOX) { "%%BoundingBox: 0 0 #{number(width)} #{number(height)}" }
|
|
82
|
+
.sub("%%EndComments\n") { "%%EndComments\n0 #{number(height)} translate\n1 -1 scale\n#{steps}#{shift}" }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def origin_shift(origin)
|
|
86
|
+
origin.all?(&:zero?) ? "" : "#{origin.map { |value| number(-value) }.join(" ")} translate\n"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reflect(postscript, box, sum, origin)
|
|
90
|
+
llx, lly, urx, ury = box
|
|
91
|
+
bbox = [llx, sum - ury, urx, sum - lly].map { |value| number(value) }.join(" ")
|
|
92
|
+
shift = origin_shift(origin)
|
|
93
|
+
postscript.sub(BBOX) { "%%BoundingBox: #{bbox}" }
|
|
94
|
+
.sub("%%EndComments\n") { "%%EndComments\n0 #{number(sum)} translate\n1 -1 scale\n#{shift}" }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The page flip mirrors glyphs; flip each `show` back about its own origin.
|
|
98
|
+
def upright(postscript)
|
|
99
|
+
postscript.gsub(SHOW) do
|
|
100
|
+
x, y, text = Regexp.last_match.captures
|
|
101
|
+
"gsave\n#{x} #{y} translate\n1 -1 scale\n0 0 moveto\n#{text}\ngrestore"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def svg_elements(root)
|
|
106
|
+
found = [root]
|
|
107
|
+
root.each_recursive { |el| found << el }
|
|
108
|
+
found.select { |el| el.namespace == SVG_NAMESPACE }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def repaired?(element, doc)
|
|
112
|
+
return removed_unstroked_line?(element, doc) if element.name == "line"
|
|
113
|
+
|
|
114
|
+
edits = element.attributes.map { |name, value| [name, rewrite(name, value)] }
|
|
115
|
+
edits.reject! { |name, value| value.nil? || element.attributes[name] == value }
|
|
116
|
+
edits.each { |name, value| element.attributes[name] = value }
|
|
117
|
+
edits.any?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def rewrite(name, value)
|
|
121
|
+
if LENGTH_ATTRIBUTES.include?(name) && (match = LENGTH.match(value))
|
|
122
|
+
number(Float(match[1]))
|
|
123
|
+
elsif PAINT_ATTRIBUTES.include?(name) && (match = RGB.match(value))
|
|
124
|
+
match.captures.map { |channel| format("%02x", [channel.to_i, 255].min) }.join.prepend("#")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Only a line whose stroke is certainly none: its own `stroke` says so, or
|
|
129
|
+
# nothing from it, its ancestors or a stylesheet can supply one.
|
|
130
|
+
def removed_unstroked_line?(line, doc)
|
|
131
|
+
return false if line.attributes["style"] || line.attributes["class"]
|
|
132
|
+
return false unless line.attributes["stroke"] == "none" || stroke_free?(line, doc)
|
|
133
|
+
|
|
134
|
+
line.remove
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def stroke_free?(line, doc)
|
|
139
|
+
return false if line.attributes["stroke"] || svg_elements(doc.root).any? { |el| el.name == "style" }
|
|
140
|
+
|
|
141
|
+
ancestors = []
|
|
142
|
+
node = line.parent
|
|
143
|
+
while node.is_a?(REXML::Element)
|
|
144
|
+
ancestors << node
|
|
145
|
+
node = node.parent
|
|
146
|
+
end
|
|
147
|
+
ancestors.none? { |el| STROKE_INHERITING.any? { |name| el.attributes[name] } }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def number(value)
|
|
151
|
+
text = format("%.12f", value).sub(/\.?0+\z/, "")
|
|
152
|
+
text == "-0" ? "0" : text
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private_class_method :svg_elements, :repaired?, :rewrite, :removed_unstroked_line?, :stroke_free?, :number,
|
|
156
|
+
:reflect, :upright, :scaled, :origin_shift
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private_constant :PostscriptFidelity
|
|
160
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Requires live here, next to the derivation that needs them, so there is
|
|
4
|
+
# no load order for the entry point to get wrong. Every file in handlers/
|
|
5
|
+
# is loaded, so a new format is one new file there and nothing else.
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
require_relative "handlers/base"
|
|
8
|
+
Dir[File.join(__dir__, "handlers", "*.rb")].each { |file| require file }
|
|
9
|
+
|
|
10
|
+
module Claricle
|
|
11
|
+
module Registry
|
|
12
|
+
# Every handler class loaded above, in name order so nothing depends on
|
|
13
|
+
# the order the filesystem listed the files in.
|
|
14
|
+
HANDLER_CLASSES = Handlers.const_get(:Base).subclasses.sort_by(&:to_s).freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def handler_for(format)
|
|
18
|
+
HANDLERS.fetch(format) { raise UnsupportedFormat, format }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def formats
|
|
22
|
+
HANDLERS.keys.sort
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# What one format's handler can actually do -- the `formats`
|
|
26
|
+
# command builds its row from this. Derived from the handler, so a
|
|
27
|
+
# row cannot advertise an operation it has not implemented.
|
|
28
|
+
def capabilities_for(format)
|
|
29
|
+
handler_for(format).capabilities
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The profile names one format accepts. Sorted, so the order a
|
|
33
|
+
# handler happens to declare them in never leaks into a message.
|
|
34
|
+
def profiles_for(format)
|
|
35
|
+
handler_for(format).supported_profiles.sort
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Every profile name ANY registered format accepts. This is what
|
|
39
|
+
# lets a batch reject a typo before it opens a single file --
|
|
40
|
+
# whether the name fits the format in hand is a per-file question
|
|
41
|
+
# and is asked again there.
|
|
42
|
+
def profiles
|
|
43
|
+
HANDLERS.values.flat_map(&:supported_profiles).uniq.sort
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The user-facing level names for a profile, or nil when that
|
|
47
|
+
# profile has no separate level selector.
|
|
48
|
+
def levels_for_profile(profile)
|
|
49
|
+
owner = HANDLERS.values.uniq.find { |handler| handler.supported_profiles.include?(profile) }
|
|
50
|
+
owner&.levels_for(profile)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# What one format's handler can convert to -- the `formats` command's
|
|
54
|
+
# `convert_to` column builds from this, the same way `capabilities_for`
|
|
55
|
+
# builds the operations column.
|
|
56
|
+
#
|
|
57
|
+
# `- [format]` matters only for a handler owning more than one format
|
|
58
|
+
# (Handlers::Postscript, :eps and :ps): its `convert_to` declares the
|
|
59
|
+
# union both can reach, since a class-level declaration cannot vary
|
|
60
|
+
# per image, so the declared list itself still names the format asked
|
|
61
|
+
# about as one of its own targets. Every single-format handler's own
|
|
62
|
+
# format was never in its declared list to begin with, so this is a
|
|
63
|
+
# no-op for them.
|
|
64
|
+
#
|
|
65
|
+
# Targets another handler declared it can produce from this format
|
|
66
|
+
# (`convert_from`) are included, so adding a format makes it a target
|
|
67
|
+
# of every source it accepts without editing any of them.
|
|
68
|
+
def convert_targets_for(format)
|
|
69
|
+
(handler_for(format).convert_targets + inbound_targets(format)).uniq - [format]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The handler to convert a `from` image to `to`, when `to` is a format
|
|
73
|
+
# whose own handler declared it accepts `from` and `from`'s handler
|
|
74
|
+
# does not already list `to`. nil means "ask the source's handler".
|
|
75
|
+
def inbound_handler(from:, to:)
|
|
76
|
+
return if HANDLERS[from]&.convert_targets&.include?(to)
|
|
77
|
+
|
|
78
|
+
owner = HANDLERS[to]
|
|
79
|
+
owner if owner&.convert_sources&.include?(from)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The format a handler's declared detector recognises in these leading
|
|
83
|
+
# bytes, or nil. Reached by the detector only after its built-in
|
|
84
|
+
# probes have all declined.
|
|
85
|
+
def detect(header)
|
|
86
|
+
HANDLER_CLASSES.each do |handler|
|
|
87
|
+
format = handler.detect_format(header)
|
|
88
|
+
return format if format
|
|
89
|
+
end
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def inbound_targets(format)
|
|
96
|
+
HANDLERS.values.uniq.select { |handler| handler.convert_sources.include?(format) }
|
|
97
|
+
.flat_map(&:supported_formats)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Two classes claiming a format is a configuration defect, not a
|
|
101
|
+
# last-one-wins: the design has one immutable owner per format.
|
|
102
|
+
def build(handler_classes)
|
|
103
|
+
handler_classes.each_with_object({}) do |handler, map|
|
|
104
|
+
handler.supported_formats.each do |format|
|
|
105
|
+
if map.key?(format)
|
|
106
|
+
raise Error, "duplicate handler for #{format.inspect}: " \
|
|
107
|
+
"#{map[format]} and #{handler}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
map[format] = handler
|
|
111
|
+
end
|
|
112
|
+
end.freeze
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
HANDLERS = send(:build, HANDLER_CLASSES)
|
|
117
|
+
|
|
118
|
+
private_constant :HANDLER_CLASSES, :HANDLERS
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Here rather than in the entry point, for the same reason the requires
|
|
122
|
+
# above are: requiring this file on its own is a supported path, and it
|
|
123
|
+
# used to leave Registry public until claricle.rb happened to run.
|
|
124
|
+
private_constant :Registry
|
|
125
|
+
end
|
data/lib/claricle/version.rb
CHANGED