// The Markdown subset the operations accept text in — core/src/markdown.rs // line for line, because a batch applied here and the same batch applied // through the core must produce the same runs. // // - Paragraphs split on blank lines; the lines of one paragraph join with // a space. // - A line starting `- ` or `1. ` (any digits) is its own paragraph with // the marker stripped. // - Inline: **bold**, *italic*, `code`, ==highlight==, [text](url). A // delimiter with no closing partner is literal text. Inside a code span // nothing else is parsed. A link's text may carry marks; a link inside a // link is literal. // - The only escape is \*, which yields a literal *. // // A run is {t, b?, i?, c?, h?, href?}: the view's run shape, false and // absent marks omitted. parseLines is the plain variant for code blocks. export function sameMarks (a, b) { return !!a.b === !!b.b && !!a.i === !!b.i && !!a.c === !!b.c && !!a.h === !!b.h && (a.href ?? null) === (b.href ?? null) } /** A run with only the marks that are on, so deepEqual sees the view shape. */ export function run (t, marks = {}) { const r = { t } if (marks.b) r.b = true if (marks.i) r.i = true if (marks.c) r.c = true if (marks.h) r.h = true if (marks.href != null) r.href = marks.href return r } /** Appends a run to a paragraph, merging into the last one when the marks match. */ export function pushRun (paragraph, r) { const last = paragraph[paragraph.length - 1] if (last && sameMarks(last, r)) last.t += r.t else paragraph.push(r) } /** Rust's str::lines(): split on \n or \r\n, the final line ending optional, no phantom last line. */ export function lines (input) { const out = [] let start = 0 while (start < input.length) { const nl = input.indexOf('\n', start) if (nl === -1) { out.push(input.slice(start)); break } const line = input.slice(start, nl) out.push(line.endsWith('\r') ? line.slice(0, -1) : line) start = nl + 1 } return out } export function parse (input) { const sources = [] let current = null for (let line of lines(input)) { line = line.trim() if (line === '') { if (current !== null) { sources.push(current); current = null } continue } const item = listItem(line) if (item !== null) { if (current !== null) sources.push(current) current = item continue } current = current === null ? line : current + ' ' + line } if (current !== null) sources.push(current) return sources.map(parseInline) } export function parseLines (input) { return lines(input).map(line => (line === '' ? [] : [run(line)])) } function listItem (line) { if (line.startsWith('- ')) return line.slice(2).trimStart() let digits = 0 while (digits < line.length && line.charCodeAt(digits) >= 48 && line.charCodeAt(digits) <= 57) digits++ if (digits > 0 && line.startsWith('. ', digits)) return line.slice(digits + 2).trimStart() return null } class Parser { constructor () { this.runs = [] this.buf = '' this.marks = { b: false, i: false, c: false, h: false, href: null } } flush () { if (this.buf === '') return pushRun(this.runs, run(this.buf, this.marks)) this.buf = '' } toggle (delim, rest, key) { if (this.marks[key]) { this.flush() this.marks[key] = false return true } if (hasPartner(rest, delim)) { this.flush() this.marks[key] = true return true } return false } parse (src) { let i = 0 while (i < src.length) { const rest = src.slice(i) if (rest.startsWith('\\*')) { this.buf += '*'; i += 2; continue } if (rest.startsWith('**')) { if (this.toggle('**', rest.slice(2), 'b')) { i += 2; continue } } else if (rest.startsWith('==')) { if (this.toggle('==', rest.slice(2), 'h')) { i += 2; continue } } else if (rest.startsWith('*')) { if (this.toggle('*', rest.slice(1), 'i')) { i += 1; continue } } else if (rest.startsWith('`')) { const end = rest.indexOf('`', 1) if (end !== -1) { this.flush() const was = this.marks.c this.marks.c = true this.buf += rest.slice(1, end) this.flush() this.marks.c = was i += end + 1 continue } } else if (rest.startsWith('[') && this.marks.href === null) { const l = link(rest) if (l !== null) { this.flush() const outer = { ...this.marks } this.marks.href = l.url this.parse(l.text) this.flush() this.marks = outer i += l.len continue } } const ch = String.fromCodePoint(rest.codePointAt(0)) this.buf += ch i += ch.length } } } /** Whether delim occurs in rest as itself: for a lone *, one that is not half of a **. */ function hasPartner (rest, delim) { if (delim !== '*') return rest.includes(delim) let i = 0 while (i < rest.length) { if (rest[i] === '*') { if (rest[i + 1] === '*') { i += 2; continue } return true } i++ } return false } /** [text](url) at the start of s: the text, the url, and the length the whole link spans. */ function link (s) { const close = s.indexOf('](') if (close === -1) return null const text = s.slice(1, close) const urlStart = close + 2 const end = s.indexOf(')', urlStart) if (end === -1) return null const url = s.slice(urlStart, end) if (text === '' || url === '') return null return { text, url, len: end + 1 } } function parseInline (src) { const p = new Parser() p.parse(src) p.flush() return p.runs }