Module: Hecks::Bluebook::Expression::CanonicalForm
- Defined in:
- lib/hecks/bluebook/expression/canonical_form.rb
Defined Under Namespace
Classes: Rule
Constant Summary collapse
- STRATEGIES =
Hecks::Vocabulary.fetch("NormalisationStrategy")
- RULES =
READ, NOT RESTATED — the admitted normalisation rules, projected from the grammar chapter by bin/expression_projection exactly as the evaluator's operator table is. See Evaluator::PROJECTION for why a projection rather than a boot.
JSON.parse( File.read(File.join(__dir__, "projection.json")), symbolize_names: true ).fetch(:normalisations).map { |row| Rule.new(**row) }.freeze
Class Method Summary collapse
- .apply(source) ⇒ Object
-
.map_outside_strings(text) ⇒ Object
Applies a normalisation rule to the text OUTSIDE quoted string literals only, copying every quoted run through byte-for-byte.
- .replace(text, rule) ⇒ Object
- .step(text, rule) ⇒ Object
- .table ⇒ Object
Class Method Details
.apply(source) ⇒ Object
34 35 36 |
# File 'lib/hecks/bluebook/expression/canonical_form.rb', line 34 def apply(source) RULES.sort_by(&:position).reduce(source.to_s) { |text, rule| step(text, rule) }.strip end |
.map_outside_strings(text) ⇒ Object
Applies a normalisation rule to the text OUTSIDE quoted string
literals only, copying every quoted run through byte-for-byte.
Every rule here (collapse_whitespace, the .length→.size fold)
used to run quote-blind — "a b" collapsed to "a b" and
"a.length" folded to "a.size" just as readily as the real
source outside the quotes, silently rewriting what a predicate
compares a string attribute against, not merely how the
predicate itself is spelled. A canonical string literal's
CONTENTS are data, never syntax to normalise.
Handles both " and ' delimiters (this grammar's own
Resolver.quoted? admits either), quote-aware exactly the way
Evaluator.top_level_index/Resolver.array_elements already are
elsewhere in this sublanguage. An unterminated quote (malformed
input) is passed through raw rather than risk mangling it further.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/hecks/bluebook/expression/canonical_form.rb', line 72 def map_outside_strings(text) result = +"" buffer = +"" quote = nil text.each_char do |char| if quote buffer << char if char == quote result << buffer buffer = +"" quote = nil end elsif ['"', "'"].include?(char) result << yield(buffer) # `char.dup`, not `char.to_s` (a no-op on a String — always # returns self, never a copy) and not `+char` either # (`String#+@` only dups a FROZEN receiver; `each_char`'s # yielded strings aren't frozen, so `+char` is just as # much a no-op here). Without a REAL copy, `buffer` and # `quote` alias the same mutable object: the very next # `buffer << char` grows `quote` right along with it, so # `char == quote` can only ever compare a single character # against an ever-lengthening string and never closes the # literal — everything after a predicate's first quoted # string silently skipped normalisation for the rest of # the text, undetected because passing text through # unnormalised is silent. MASKED by every existing spec # here, which only checks that quoted CONTENTS survive # untouched (the M7 fix this method exists for) — that # still holds by accident once the bug makes the "outside" # branch unreachable. Found live: a multi-line `given`/ # `ensures` block whose ONLY quoted literal closes before a # later line — the newline and that later line's own # indentation went uncollapsed, diverging from # `hecks-parse`'s own (correct) single-space join. # `spec/parser_parity_spec.rb`, roster's "a front-row seat # takes a member of age". buffer = char.dup quote = char.dup else buffer << char end end result << (quote ? buffer : yield(buffer)) result end |
.replace(text, rule) ⇒ Object
47 48 49 50 51 52 53 54 55 |
# File 'lib/hecks/bluebook/expression/canonical_form.rb', line 47 def replace(text, rule) map_outside_strings(text) do |segment| if rule.boundary == "none" segment.gsub(rule.source_token, rule.replacement) else segment.gsub(/#{Regexp.escape(rule.source_token)}(?![[:alnum:]_])/, rule.replacement) end end end |
.step(text, rule) ⇒ Object
38 39 40 41 42 43 44 45 |
# File 'lib/hecks/bluebook/expression/canonical_form.rb', line 38 def step(text, rule) case rule.strategy when "collapse_whitespace" then map_outside_strings(text) { |segment| segment.gsub(/\s+/, " ") } when "replace" then replace(text, rule) else raise ArgumentError, "#{rule.strategy.inspect} is not a linked normalisation strategy" end end |
.table ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/hecks/bluebook/expression/canonical_form.rb', line 22 def table RULES.sort_by(&:position).map do |rule| { strategy: rule.strategy, source_token: rule.source_token, replacement: rule.replacement, boundary: rule.boundary, position: rule.position.to_s } end end |