Class: MaquinaRemend::Scanner
- Inherits:
-
Object
- Object
- MaquinaRemend::Scanner
- Defined in:
- lib/maquina_remend/scanner.rb
Overview
One pass over the buffer, tracking the only three states a handler is allowed to ask about. The guards in the pattern spec are the reason this exists: nothing may be "repaired" inside a fence, inside inline code, or inside math, and the only way to know is to have walked the buffer.
A scanner is built over one immutable buffer and caches everything it derives, so the expensive walk happens once. When a handler changes the text, MaquinaRemend::Pipeline throws the scanner away and builds a new one rather than trying to update this one.
Hosts should reach for MaquinaRemend.context instead: it answers the question — is the tail inside a fence, and what language is it — without exposing the scanner's internals. The scanner itself is public because the built-in handlers need it, not because it is a stable surface.
Constant Summary collapse
- FENCE_LINE =
A fenced code block's opening or closing line: up to three spaces of indent, three or more backticks or tildes, then an optional info string captured as group 2.
/\A {0,3}(`{3,}|~{3,})[ \t]*(\S*)/- INLINE_CODE =
A balanced inline code span. Matching only balanced spans is the point: what is left over after these are masked out is the unterminated one.
/(?<!`)(`+)(?!`)(.*?)(?<!`)\1(?!`)/m- MATH_SPANS =
The math delimiters whose contents must never be treated as markdown. Underscores inside
$$a_1 + b_2$$are subscripts, not emphasis, and a LaTeX span is math even when it uses no dollar sign at all. [ /\$\$.*?\$\$/m, # block math /\\\[.*?\\\]/m, # LaTeX display /\\\(.*?\\\)/m # LaTeX inline ].freeze
- MASK =
Masked spans keep their length but must not read as whitespace: a "**" that follows
codeis a valid closer, and blanking the code span with spaces makes it look like an opener instead. Learned from real streamed output. "\u0001"
Instance Attribute Summary collapse
-
#open_fence_info ⇒ Object
readonly
The info string of the fence that is currently open —
"ruby"for a fence opened with three backticks andruby— ornilwhen no fence is open or the fence carried no info string. -
#paragraph_offset ⇒ Object
readonly
The byte offset #paragraph starts at: the end of the last blank line that was not inside a fence.
-
#text ⇒ Object
readonly
The buffer this scanner was built over, unmodified.
Instance Method Summary collapse
-
#context(options = {}) ⇒ Object
The MaquinaRemend::Context for this buffer, carrying
optionsthrough to any handler that wants to read them. -
#fence_open? ⇒ Boolean
True when the buffer ends inside a fenced code block.
-
#initialize(text) ⇒ Scanner
constructor
Walks
textonce and caches what the walk found. -
#inline_code_open? ⇒ Boolean
An unterminated inline code span in the paragraph: an odd number of backticks once the balanced spans are gone.
-
#masked_paragraph ⇒ Object
The paragraph with inline code and math replaced by placeholders of the same length, so delimiter counting cannot see inside them.
-
#masked_text ⇒ Object
The whole buffer with fenced blocks and inline code blanked out, lengths preserved.
-
#math_open? ⇒ Boolean
Block math spans paragraphs, so this question is asked of the whole document - but only of the parts of it that are prose.
-
#paragraph ⇒ Object
Everything after the last blank line outside a fence.
-
#prefix ⇒ Object
Everything before #paragraph.
Constructor Details
#initialize(text) ⇒ Scanner
Walks text once and caches what the walk found. The buffer is not
copied and never mutated.
52 53 54 55 56 57 58 |
# File 'lib/maquina_remend/scanner.rb', line 52 def initialize(text) @text = text @fence_open = false @open_fence_info = nil @paragraph_offset = 0 scan end |
Instance Attribute Details
#open_fence_info ⇒ Object (readonly)
The info string of the fence that is currently open — "ruby" for a
fence opened with three backticks and ruby — or nil when no fence is
open or the fence carried no info string.
48 49 50 |
# File 'lib/maquina_remend/scanner.rb', line 48 def open_fence_info @open_fence_info end |
#paragraph_offset ⇒ Object (readonly)
The byte offset #paragraph starts at: the end of the last blank line that was not inside a fence.
72 73 74 |
# File 'lib/maquina_remend/scanner.rb', line 72 def paragraph_offset @paragraph_offset end |
#text ⇒ Object (readonly)
The buffer this scanner was built over, unmodified.
43 44 45 |
# File 'lib/maquina_remend/scanner.rb', line 43 def text @text end |
Instance Method Details
#context(options = {}) ⇒ Object
The MaquinaRemend::Context for this buffer, carrying options through to
any handler that wants to read them. Memoised per option hash, since the
pipeline asks for the same one repeatedly.
115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/maquina_remend/scanner.rb', line 115 def context( = {}) @context ||= {} @context[] ||= Context.new( in_code_fence: fence_open?, in_inline_code: inline_code_open?, in_math: math_open?, open_fence_info: open_fence_info, options: ) end |
#fence_open? ⇒ Boolean
True when the buffer ends inside a fenced code block. Surfaces to handlers as MaquinaRemend::Context#in_code_fence?.
62 |
# File 'lib/maquina_remend/scanner.rb', line 62 def fence_open? = @fence_open |
#inline_code_open? ⇒ Boolean
An unterminated inline code span in the paragraph: an odd number of backticks once the balanced spans are gone.
83 84 85 86 87 |
# File 'lib/maquina_remend/scanner.rb', line 83 def inline_code_open? return @inline_code_open if defined?(@inline_code_open) @inline_code_open = masked_code.count("`").odd? end |
#masked_paragraph ⇒ Object
The paragraph with inline code and math replaced by placeholders of the same length, so delimiter counting cannot see inside them.
106 107 108 109 110 |
# File 'lib/maquina_remend/scanner.rb', line 106 def masked_paragraph @masked_paragraph ||= MATH_SPANS.reduce(masked_code) do |masked, pattern| masked.gsub(pattern) { MASK * ::Regexp.last_match(0).length } end end |
#masked_text ⇒ Object
The whole buffer with fenced blocks and inline code blanked out, lengths preserved.
100 101 102 |
# File 'lib/maquina_remend/scanner.rb', line 100 def masked_text @masked_text ||= mask_fences(text).gsub(INLINE_CODE) { MASK * ::Regexp.last_match(0).length } end |
#math_open? ⇒ Boolean
Block math spans paragraphs, so this question is asked of the whole document - but only of the parts of it that are prose. A document that merely writes about "$$x = 1" inside a code span has no open math.
92 93 94 95 96 |
# File 'lib/maquina_remend/scanner.rb', line 92 def math_open? return @math_open if defined?(@math_open) @math_open = masked_text.scan(/(?<!\\)\$\$/).length.odd? end |
#paragraph ⇒ Object
Everything after the last blank line outside a fence. Emphasis cannot span a blank line, so this is the only region a completion may touch.
66 67 68 |
# File 'lib/maquina_remend/scanner.rb', line 66 def paragraph text[@paragraph_offset..] || "" end |
#prefix ⇒ Object
Everything before #paragraph. A handler that rewrites the paragraph
rebuilds the buffer as scanner.prefix + repaired_paragraph, which is how
a repair stays confined to the region it is allowed to touch.
77 78 79 |
# File 'lib/maquina_remend/scanner.rb', line 77 def prefix text[0...@paragraph_offset] || "" end |