Class: MaquinaRemend::Scanner

Inherits:
Object
  • Object
show all
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 code is 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

Instance Method Summary collapse

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(options = {})
  @context ||= {}
  @context[options] ||=
    Context.new(
      in_code_fence: fence_open?,
      in_inline_code: inline_code_open?,
      in_math: math_open?,
      open_fence_info: open_fence_info,
      options: options
    )
end

#fence_open? ⇒ Boolean

True when the buffer ends inside a fenced code block. Surfaces to handlers as MaquinaRemend::Context#in_code_fence?.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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