Module: Rubino::UI::MarkdownRepair
- Defined in:
- lib/rubino/ui/markdown_repair.rb
Overview
Repairs markdown that is syntactically INCOMPLETE because it is still streaming, so a forgiving live renderer can style the in-flight block instead of leaking raw markers (‘**plan`, a lone “ ` “, an open “` fence). The repair is temporary — applied only to the live preview of the current block; the block re-renders strict + complete the moment it commits, so a wrong guess here is at worst one transient frame.
Two cases (the universal caveat in every streaming-markdown writeup —Streamdown’s ‘remend`, marked.js #3657 — is “track code first”):
* Inside an open CODE FENCE: just append the matching closing fence so
the body renders as code. NEVER touch the body — a `**` in code is
literal. (A ```markdown/md WRAPPER is left as-is: the renderer unwraps
and re-renders its body as markdown.)
* Outside code: close a dangling inline span — unterminated `` ` `` code,
or `*`/`_`/`**`/`__` emphasis — but only when it actually OPENS a span
(left-flanking: a non-space follows the marker), so literal asterisks
like "2 * 3" are never mis-repaired into italics.
Class Method Summary collapse
-
.backtick_run(text, idx) ⇒ Object
Number of consecutive backticks starting at idx.
-
.close_inline(text) ⇒ Object
Scan the text tracking an open inline-code run and a stack of open emphasis markers, then append closers (innermost first) for whatever is still open at the end.
-
.close_open_spans(text, fence: nil) ⇒ Object
text : the raw in-flight block (StreamingMarkdown#tail) fence : StreamingMarkdown#open_fence — nil outside a fence, else { len:, plain: } (plain:false ⇒ a “‘markdown/md wrapper) Returns text with just enough trailing syntax appended to parse cleanly.
-
.left_flanking?(text, idx) ⇒ Boolean
A marker OPENS emphasis only if left-flanking: the char that follows it (at idx) exists and is not whitespace.
-
.right_flanking?(text, idx) ⇒ Boolean
A marker CLOSES emphasis only if right-flanking: the char BEFORE it is not whitespace.
-
.scan_code(text, idx, code_run) ⇒ Object
Toggle the open inline-code run at a backtick: open with this run length, or close when a run of ≥ the opener arrives.
-
.scan_emphasis(text, idx, emphasis) ⇒ Object
Match a ‘*`/`_`/`**`/`__` marker against the emphasis stack (push an opener, pop a matching closer), mutating it.
Class Method Details
.backtick_run(text, idx) ⇒ Object
Number of consecutive backticks starting at idx.
93 94 95 96 97 |
# File 'lib/rubino/ui/markdown_repair.rb', line 93 def backtick_run(text, idx) run = 0 run += 1 while text[idx + run] == "`" run end |
.close_inline(text) ⇒ Object
Scan the text tracking an open inline-code run and a stack of open emphasis markers, then append closers (innermost first) for whatever is still open at the end. Inline code suppresses emphasis scanning.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/rubino/ui/markdown_repair.rb', line 45 def close_inline(text) code_run = nil # backtick run length of the open inline-code span, or nil emphasis = [] # open emphasis markers ("*","_","**","__"), innermost last idx = 0 len = text.length while idx < len ch = text[idx] if ch == "`" code_run = scan_code(text, idx, code_run) idx += backtick_run(text, idx) elsif code_run.nil? && ["*", "_"].include?(ch) idx += scan_emphasis(text, idx, emphasis) else idx += 1 end end closers = +"" closers << ("`" * code_run) if code_run emphasis.reverse_each { |marker| closers << marker } closers.empty? ? text : (text + closers) end |
.close_open_spans(text, fence: nil) ⇒ Object
text : the raw in-flight block (StreamingMarkdown#tail) fence : StreamingMarkdown#open_fence — nil outside a fence, else
{ len:, plain: } (plain:false ⇒ a ```markdown/md wrapper)
Returns text with just enough trailing syntax appended to parse cleanly.
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/rubino/ui/markdown_repair.rb', line 30 def close_open_spans(text, fence: nil) return text if text.nil? || text.empty? if fence return text unless fence[:plain] # wrapper: renderer unwraps + re-renders return "#{text}\n#{"`" * fence[:len].to_i}" end close_inline(text) end |
.left_flanking?(text, idx) ⇒ Boolean
A marker OPENS emphasis only if left-flanking: the char that follows it (at idx) exists and is not whitespace. “**plan” opens; “2 * 3” does not.
101 102 103 104 |
# File 'lib/rubino/ui/markdown_repair.rb', line 101 def left_flanking?(text, idx) nxt = text[idx] !nxt.nil? && nxt !~ /\s/ end |
.right_flanking?(text, idx) ⇒ Boolean
A marker CLOSES emphasis only if right-flanking: the char BEFORE it is not whitespace. Guards against treating “text * ” as a closer.
108 109 110 111 |
# File 'lib/rubino/ui/markdown_repair.rb', line 108 def right_flanking?(text, idx) prev = idx.positive? ? text[idx - 1] : nil !prev.nil? && prev !~ /\s/ end |
.scan_code(text, idx, code_run) ⇒ Object
Toggle the open inline-code run at a backtick: open with this run length, or close when a run of ≥ the opener arrives. Returns the new run state.
71 72 73 74 75 76 |
# File 'lib/rubino/ui/markdown_repair.rb', line 71 def scan_code(text, idx, code_run) run = backtick_run(text, idx) return run if code_run.nil? run >= code_run ? nil : code_run end |
.scan_emphasis(text, idx, emphasis) ⇒ Object
Match a ‘*`/`_`/`**`/`__` marker against the emphasis stack (push an opener, pop a matching closer), mutating it. Returns the marker length consumed so the caller can advance.
81 82 83 84 85 86 87 88 89 90 |
# File 'lib/rubino/ui/markdown_repair.rb', line 81 def scan_emphasis(text, idx, emphasis) ch = text[idx] marker = text[idx, 2] == (ch * 2) ? ch * 2 : ch if emphasis.last == marker && right_flanking?(text, idx) emphasis.pop elsif left_flanking?(text, idx + marker.length) emphasis.push(marker) end marker.length end |