import {Tree, TreeBuffer, NodeType, NodeProp, NodePropSource, TreeFragment, NodeSet, TreeCursor, Input, Parser, PartialParse, SyntaxNode, ParseWrapper} from "@lezer/common" import {styleTags, tags as t, Tag} from "@lezer/highlight" class CompositeBlock { static create(type: number, value: number, from: number, parentHash: number, end: number) { let hash = (parentHash + (parentHash << 8) + type + (value << 4)) | 0 return new CompositeBlock(type, value, from, hash, end, [], []) } /// @internal hashProp: [NodeProp, any][] constructor(readonly type: number, // Used for indentation in list items, markup character in lists readonly value: number, readonly from: number, readonly hash: number, public end: number, readonly children: (Tree | TreeBuffer)[], readonly positions: number[]) { this.hashProp = [[NodeProp.contextHash, hash]] } addChild(child: Tree, pos: number) { if (child.prop(NodeProp.contextHash) != this.hash) child = new Tree(child.type, child.children, child.positions, child.length, this.hashProp) this.children.push(child) this.positions.push(pos) } toTree(nodeSet: NodeSet, end = this.end) { let last = this.children.length - 1 if (last >= 0) end = Math.max(end, this.positions[last] + this.children[last].length + this.from) return new Tree(nodeSet.types[this.type], this.children, this.positions, end - this.from).balance({ makeTree: (children, positions, length) => new Tree(NodeType.none, children, positions, length, this.hashProp) }) } } export enum Type { Document = 1, CodeBlock, FencedCode, Blockquote, HorizontalRule, BulletList, OrderedList, ListItem, ATXHeading1, ATXHeading2, ATXHeading3, ATXHeading4, ATXHeading5, ATXHeading6, SetextHeading1, SetextHeading2, HTMLBlock, LinkReference, Paragraph, CommentBlock, ProcessingInstructionBlock, // Inline Escape, Entity, HardBreak, Emphasis, StrongEmphasis, Link, Image, InlineCode, HTMLTag, Comment, ProcessingInstruction, Autolink, // Smaller tokens HeaderMark, QuoteMark, ListMark, LinkMark, EmphasisMark, CodeMark, CodeText, CodeInfo, LinkTitle, LinkLabel, URL } /// Data structure used to accumulate a block's content during [leaf /// block parsing](#BlockParser.leaf). export class LeafBlock { /// @internal marks: Element[] = [] /// The block parsers active for this block. parsers: LeafBlockParser[] = [] /// @internal constructor( /// The start position of the block. readonly start: number, /// The block's text content. public content: string ) {} } /// Data structure used during block-level per-line parsing. export class Line { /// The line's full text. text = "" /// The base indent provided by the composite contexts (that have /// been handled so far). baseIndent = 0 /// The string position corresponding to the base indent. basePos = 0 /// The number of contexts handled @internal depth = 0 /// Any markers (i.e. block quote markers) parsed for the contexts. @internal markers: Element[] = [] /// The position of the next non-whitespace character beyond any /// list, blockquote, or other composite block markers. pos = 0 /// The column of the next non-whitespace character. indent = 0 /// The character code of the character after `pos`. next = -1 /// @internal forward() { if (this.basePos > this.pos) this.forwardInner() } /// @internal forwardInner() { let newPos = this.skipSpace(this.basePos) this.indent = this.countIndent(newPos, this.pos, this.indent) this.pos = newPos this.next = newPos == this.text.length ? -1 : this.text.charCodeAt(newPos) } /// Skip whitespace after the given position, return the position of /// the next non-space character or the end of the line if there's /// only space after `from`. skipSpace(from: number) { return skipSpace(this.text, from) } /// @internal reset(text: string) { this.text = text this.baseIndent = this.basePos = this.pos = this.indent = 0 this.forwardInner() this.depth = 1 while (this.markers.length) this.markers.pop() } /// Move the line's base position forward to the given position. /// This should only be called by composite [block /// parsers](#BlockParser.parse) or [markup skipping /// functions](#NodeSpec.composite). moveBase(to: number) { this.basePos = to this.baseIndent = this.countIndent(to, this.pos, this.indent) } /// Move the line's base position forward to the given _column_. moveBaseColumn(indent: number) { this.baseIndent = indent this.basePos = this.findColumn(indent) } /// Store a composite-block-level marker. Should be called from /// [markup skipping functions](#NodeSpec.composite) when they /// consume any non-whitespace characters. addMarker(elt: Element) { this.markers.push(elt) } /// Find the column position at `to`, optionally starting at a given /// position and column. countIndent(to: number, from = 0, indent = 0) { for (let i = from; i < to; i++) indent += this.text.charCodeAt(i) == 9 ? 4 - indent % 4 : 1 return indent } /// Find the position corresponding to the given column. findColumn(goal: number) { let i = 0 for (let indent = 0; i < this.text.length && indent < goal; i++) indent += this.text.charCodeAt(i) == 9 ? 4 - indent % 4 : 1 return i } /// @internal scrub() { if (!this.baseIndent) return this.text let result = "" for (let i = 0; i < this.basePos; i++) result += " " return result + this.text.slice(this.basePos) } } function skipForList(bl: CompositeBlock, cx: BlockContext, line: Line) { if (line.pos == line.text.length || (bl != cx.block && line.indent >= cx.stack[line.depth + 1].value + line.baseIndent)) return true if (line.indent >= line.baseIndent + 4) return false let size = (bl.type == Type.OrderedList ? isOrderedList : isBulletList)(line, cx, false) return size > 0 && (bl.type != Type.BulletList || isHorizontalRule(line, cx, false) < 0) && line.text.charCodeAt(line.pos + size - 1) == bl.value } const DefaultSkipMarkup: {[type: number]: (bl: CompositeBlock, cx: BlockContext, line: Line) => boolean} = { [Type.Blockquote](bl, cx, line) { if (line.next != 62 /* '>' */) return false line.markers.push(elt(Type.QuoteMark, cx.lineStart + line.pos, cx.lineStart + line.pos + 1)) line.moveBase(line.pos + (space(line.text.charCodeAt(line.pos + 1)) ? 2 : 1)) bl.end = cx.lineStart + line.text.length return true }, [Type.ListItem](bl, _cx, line) { if (line.indent < line.baseIndent + bl.value && line.next > -1) return false line.moveBaseColumn(line.baseIndent + bl.value) return true }, [Type.OrderedList]: skipForList, [Type.BulletList]: skipForList, [Type.Document]() { return true } } export function space(ch: number) { return ch == 32 || ch == 9 || ch == 10 || ch == 13 } function skipSpace(line: string, i = 0) { while (i < line.length && space(line.charCodeAt(i))) i++ return i } function skipSpaceBack(line: string, i: number, to: number) { while (i > to && space(line.charCodeAt(i - 1))) i-- return i } function isFencedCode(line: Line) { if (line.next != 96 && line.next != 126 /* '`~' */) return -1 let pos = line.pos + 1 while (pos < line.text.length && line.text.charCodeAt(pos) == line.next) pos++ if (pos < line.pos + 3) return -1 if (line.next == 96) for (let i = pos; i < line.text.length; i++) if (line.text.charCodeAt(i) == 96) return -1 return pos } function isBlockquote(line: Line) { return line.next != 62 /* '>' */ ? -1 : line.text.charCodeAt(line.pos + 1) == 32 ? 2 : 1 } function isHorizontalRule(line: Line, cx: BlockContext, breaking: boolean) { if (line.next != 42 && line.next != 45 && line.next != 95 /* '_-*' */) return -1 let count = 1 for (let pos = line.pos + 1; pos < line.text.length; pos++) { let ch = line.text.charCodeAt(pos) if (ch == line.next) count++ else if (!space(ch)) return -1 } // Setext headers take precedence if (breaking && line.next == 45 && isSetextUnderline(line) > -1 && line.depth == cx.stack.length && cx.parser.leafBlockParsers.indexOf(DefaultLeafBlocks.SetextHeading) > -1) return -1 return count < 3 ? -1 : 1 } function inList(cx: BlockContext, type: Type) { for (let i = cx.stack.length - 1; i >= 0; i--) if (cx.stack[i].type == type) return true return false } function isBulletList(line: Line, cx: BlockContext, breaking: boolean) { return (line.next == 45 || line.next == 43 || line.next == 42 /* '-+*' */) && (line.pos == line.text.length - 1 || space(line.text.charCodeAt(line.pos + 1))) && (!breaking || inList(cx, Type.BulletList) || line.skipSpace(line.pos + 2) < line.text.length) ? 1 : -1 } function isOrderedList(line: Line, cx: BlockContext, breaking: boolean) { let pos = line.pos, next = line.next for (;;) { if (next >= 48 && next <= 57 /* '0-9' */) pos++ else break if (pos == line.text.length) return -1 next = line.text.charCodeAt(pos) } if (pos == line.pos || pos > line.pos + 9 || (next != 46 && next != 41 /* '.)' */) || (pos < line.text.length - 1 && !space(line.text.charCodeAt(pos + 1))) || breaking && !inList(cx, Type.OrderedList) && (line.skipSpace(pos + 1) == line.text.length || pos > line.pos + 1 || line.next != 49 /* '1' */)) return -1 return pos + 1 - line.pos } function isAtxHeading(line: Line) { if (line.next != 35 /* '#' */) return -1 let pos = line.pos + 1 while (pos < line.text.length && line.text.charCodeAt(pos) == 35) pos++ if (pos < line.text.length && line.text.charCodeAt(pos) != 32) return -1 let size = pos - line.pos return size > 6 ? -1 : size } function isSetextUnderline(line: Line) { if (line.next != 45 && line.next != 61 /* '-=' */ || line.indent >= line.baseIndent + 4) return -1 let pos = line.pos + 1 while (pos < line.text.length && line.text.charCodeAt(pos) == line.next) pos++ let end = pos while (pos < line.text.length && space(line.text.charCodeAt(pos))) pos++ return pos == line.text.length ? end : -1 } const EmptyLine = /^[ \t]*$/, CommentEnd = /-->/, ProcessingEnd = /\?>/ const HTMLBlockStyle = [ [/^<(?:script|pre|style)(?:\s|>|$)/i, /<\/(?:script|pre|style)>/i], [/^\s*/i.exec(after) if (comment) return cx.append(elt(Type.Comment, start, start + 1 + comment[0].length)) let procInst = /^\?[^]*?\?>/.exec(after) if (procInst) return cx.append(elt(Type.ProcessingInstruction, start, start + 1 + procInst[0].length)) let m = /^(?:![A-Z][^]*?>|!\[CDATA\[[^]*?\]\]>|\/\s*[a-zA-Z][\w-]*\s*>|\s*[a-zA-Z][\w-]*(\s+[a-zA-Z:_][\w-.:]*(?:\s*=\s*(?:[^\s"'=<>`]+|'[^']*'|"[^"]*"))?)*\s*(\/\s*)?>)/.exec(after) if (!m) return -1 return cx.append(elt(Type.HTMLTag, start, start + 1 + m[0].length)) }, Emphasis(cx, next, start) { if (next != 95 && next != 42) return -1 let pos = start + 1 while (cx.char(pos) == next) pos++ let before = cx.slice(start - 1, start), after = cx.slice(pos, pos + 1) let pBefore = Punctuation.test(before), pAfter = Punctuation.test(after) let sBefore = /\s|^$/.test(before), sAfter = /\s|^$/.test(after) let leftFlanking = !sAfter && (!pAfter || sBefore || pBefore) let rightFlanking = !sBefore && (!pBefore || sAfter || pAfter) let canOpen = leftFlanking && (next == 42 || !rightFlanking || pBefore) let canClose = rightFlanking && (next == 42 || !leftFlanking || pAfter) return cx.append(new InlineDelimiter(next == 95 ? EmphasisUnderscore : EmphasisAsterisk, start, pos, (canOpen ? Mark.Open : Mark.None) | (canClose ? Mark.Close : Mark.None))) }, HardBreak(cx, next, start) { if (next == 92 /* '\\' */ && cx.char(start + 1) == 10 /* '\n' */) return cx.append(elt(Type.HardBreak, start, start + 2)) if (next == 32) { let pos = start + 1 while (cx.char(pos) == 32) pos++ if (cx.char(pos) == 10 && pos >= start + 2) return cx.append(elt(Type.HardBreak, start, pos + 1)) } return -1 }, Link(cx, next, start) { return next == 91 /* '[' */ ? cx.append(new InlineDelimiter(LinkStart, start, start + 1, Mark.Open)) : -1 }, Image(cx, next, start) { return next == 33 /* '!' */ && cx.char(start + 1) == 91 /* '[' */ ? cx.append(new InlineDelimiter(ImageStart, start, start + 2, Mark.Open)) : -1 }, LinkEnd(cx, next, start) { if (next != 93 /* ']' */) return -1 // Scanning back to the next link/image start marker for (let i = cx.parts.length - 1; i >= 0; i--) { let part = cx.parts[i] if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) { // If this one has been set invalid (because it would produce // a nested link) or there's no valid link here ignore both. if (!part.side || cx.skipSpace(part.to) == start && !/[(\[]/.test(cx.slice(start + 1, start + 2))) { cx.parts[i] = null return -1 } // Finish the content and replace the entire range in // this.parts with the link/image node. let content = cx.takeContent(i) let link = cx.parts[i] = finishLink(cx, content, part.type == LinkStart ? Type.Link : Type.Image, part.from, start + 1) // Set any open-link markers before this link to invalid. if (part.type == LinkStart) for (let j = 0; j < i; j++) { let p = cx.parts[j] if (p instanceof InlineDelimiter && p.type == LinkStart) p.side = Mark.None } return link.to } } return -1 } } function finishLink(cx: InlineContext, content: Element[], type: Type, start: number, startPos: number) { let {text} = cx, next = cx.char(startPos), endPos = startPos content.unshift(elt(Type.LinkMark, start, start + (type == Type.Image ? 2 : 1))) content.push(elt(Type.LinkMark, startPos - 1, startPos)) if (next == 40 /* '(' */) { let pos = cx.skipSpace(startPos + 1) let dest = parseURL(text, pos - cx.offset, cx.offset), title if (dest) { pos = cx.skipSpace(dest.to) // The destination and title must be separated by whitespace if (pos != dest.to) { title = parseLinkTitle(text, pos - cx.offset, cx.offset) if (title) pos = cx.skipSpace(title.to) } } if (cx.char(pos) == 41 /* ')' */) { content.push(elt(Type.LinkMark, startPos, startPos + 1)) endPos = pos + 1 if (dest) content.push(dest) if (title) content.push(title) content.push(elt(Type.LinkMark, pos, endPos)) } } else if (next == 91 /* '[' */) { let label = parseLinkLabel(text, startPos - cx.offset, cx.offset, false) if (label) { content.push(label) endPos = label.to } } return elt(type, start, endPos, content) } // These return `null` when falling off the end of the input, `false` // when parsing fails otherwise (for use in the incremental link // reference parser). function parseURL(text: string, start: number, offset: number): null | false | Element { let next = text.charCodeAt(start) if (next == 60 /* '<' */) { for (let pos = start + 1; pos < text.length; pos++) { let ch = text.charCodeAt(pos) if (ch == 62 /* '>' */) return elt(Type.URL, start + offset, pos + 1 + offset) if (ch == 60 || ch == 10 /* '<\n' */) return false } return null } else { let depth = 0, pos = start for (let escaped = false; pos < text.length; pos++) { let ch = text.charCodeAt(pos) if (space(ch)) { break } else if (escaped) { escaped = false } else if (ch == 40 /* '(' */) { depth++ } else if (ch == 41 /* ')' */) { if (!depth) break depth-- } else if (ch == 92 /* '\\' */) { escaped = true } } return pos > start ? elt(Type.URL, start + offset, pos + offset) : pos == text.length ? null : false } } function parseLinkTitle(text: string, start: number, offset: number): null | false | Element { let next = text.charCodeAt(start) if (next != 39 && next != 34 && next != 40 /* '"\'(' */) return false let end = next == 40 ? 41 : next for (let pos = start + 1, escaped = false; pos < text.length; pos++) { let ch = text.charCodeAt(pos) if (escaped) escaped = false else if (ch == end) return elt(Type.LinkTitle, start + offset, pos + 1 + offset) else if (ch == 92 /* '\\' */) escaped = true } return null } function parseLinkLabel(text: string, start: number, offset: number, requireNonWS: boolean): null | false | Element { for (let escaped = false, pos = start + 1, end = Math.min(text.length, pos + 999); pos < end; pos++) { let ch = text.charCodeAt(pos) if (escaped) escaped = false else if (ch == 93 /* ']' */) return requireNonWS ? false : elt(Type.LinkLabel, start + offset, pos + 1 + offset) else { if (requireNonWS && !space(ch)) requireNonWS = false if (ch == 91 /* '[' */) return false else if (ch == 92 /* '\\' */) escaped = true } } return null } /// Inline parsing functions get access to this context, and use it to /// read the content and emit syntax nodes. export class InlineContext { /// @internal parts: (Element | InlineDelimiter | null)[] = [] /// @internal constructor( /// The parser that is being used. readonly parser: MarkdownParser, /// The text of this inline section. readonly text: string, /// The starting offset of the section in the document. readonly offset: number ) {} /// Get the character code at the given (document-relative) /// position. char(pos: number) { return pos >= this.end ? -1 : this.text.charCodeAt(pos - this.offset) } /// The position of the end of this inline section. get end() { return this.offset + this.text.length } /// Get a substring of this inline section. Again uses /// document-relative positions. slice(from: number, to: number) { return this.text.slice(from - this.offset, to - this.offset) } /// @internal append(elt: Element | InlineDelimiter) { this.parts.push(elt) return elt.to } /// Add a [delimiter](#DelimiterType) at this given position. `open` /// and `close` indicate whether this delimiter is opening, closing, /// or both. Returns the end of the delimiter, for convenient /// returning from [parse functions](#InlineParser.parse). addDelimiter(type: DelimiterType, from: number, to: number, open: boolean, close: boolean) { return this.append(new InlineDelimiter(type, from, to, (open ? Mark.Open : Mark.None) | (close ? Mark.Close : Mark.None))) } /// Returns true when there is an unmatched link or image opening /// token before the current position. get hasOpenLink() { for (let i = this.parts.length - 1; i >= 0; i--) { let part = this.parts[i] if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) return true } return false } /// Add an inline element. Returns the end of the element. addElement(elt: Element) { return this.append(elt) } /// Resolve markers between this.parts.length and from, wrapping matched markers in the /// appropriate node and updating the content of this.parts. @internal resolveMarkers(from: number) { // Scan forward, looking for closing tokens for (let i = from; i < this.parts.length; i++) { let close = this.parts[i] if (!(close instanceof InlineDelimiter && close.type.resolve && (close.side & Mark.Close))) continue let emp = close.type == EmphasisUnderscore || close.type == EmphasisAsterisk let closeSize = close.to - close.from let open: InlineDelimiter | undefined, j = i - 1 // Continue scanning for a matching opening token for (; j >= from; j--) { let part = this.parts[j] if (part instanceof InlineDelimiter && (part.side & Mark.Open) && part.type == close.type && // Ignore emphasis delimiters where the character count doesn't match !(emp && ((close.side & Mark.Open) || (part.side & Mark.Close)) && (part.to - part.from + closeSize) % 3 == 0 && ((part.to - part.from) % 3 || closeSize % 3))) { open = part break } } if (!open) continue let type = close.type.resolve, content = [] let start = open.from, end = close.to // Emphasis marker effect depends on the character count. Size consumed is minimum of the two // markers. if (emp) { let size = Math.min(2, open.to - open.from, closeSize) start = open.to - size end = close.from + size type = size == 1 ? "Emphasis" : "StrongEmphasis" } // Move the covered region into content, optionally adding marker nodes if (open.type.mark) content.push(this.elt(open.type.mark, start, open.to)) for (let k = j + 1; k < i; k++) { if (this.parts[k] instanceof Element) content.push(this.parts[k] as Element) this.parts[k] = null } if (close.type.mark) content.push(this.elt(close.type.mark, close.from, end)) let element = this.elt(type, start, end, content) // If there are leftover emphasis marker characters, shrink the close/open markers. Otherwise, clear them. this.parts[j] = emp && open.from != start ? new InlineDelimiter(open.type, open.from, start, open.side) : null let keep = this.parts[i] = emp && close.to != end ? new InlineDelimiter(close.type, end, close.to, close.side) : null // Insert the new element in this.parts if (keep) this.parts.splice(i, 0, element) else this.parts[i] = element } // Collect the elements remaining in this.parts into an array. let result = [] for (let i = from; i < this.parts.length; i++) { let part = this.parts[i] if (part instanceof Element) result.push(part) } return result } /// Find an opening delimiter of the given type. Returns `null` if /// no delimiter is found, or an index that can be passed to /// [`takeContent`](#InlineContext.takeContent) otherwise. findOpeningDelimiter(type: DelimiterType) { for (let i = this.parts.length - 1; i >= 0; i--) { let part = this.parts[i] if (part instanceof InlineDelimiter && part.type == type && (part.side & Mark.Open)) return i } return null } /// Remove all inline elements and delimiters starting from the /// given index (which you should get from /// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter), /// resolve delimiters inside of them, and return them as an array /// of elements. takeContent(startIndex: number) { let content = this.resolveMarkers(startIndex) this.parts.length = startIndex return content } /// Return the delimiter at the given index. Mostly useful to get /// additional info out of a delimiter index returned by /// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter). /// Returns null if there is no delimiter at this index. getDelimiterAt(index: number): {from: number, to: number, type: DelimiterType} | null { let part = this.parts[index] return part instanceof InlineDelimiter ? part : null } /// Skip space after the given (document) position, returning either /// the position of the next non-space character or the end of the /// section. skipSpace(from: number) { return skipSpace(this.text, from - this.offset) + this.offset } /// Create an [`Element`](#Element) for a syntax node. elt(type: string, from: number, to: number, children?: readonly Element[]): Element elt(tree: Tree, at: number): Element elt(type: string | Tree, from: number, to?: number, children?: readonly Element[]): Element { if (typeof type == "string") return elt(this.parser.getNodeType(type), from, to!, children) return new TreeElement(type, from) } /// The opening delimiter type used by the standard link parser. static linkStart = LinkStart /// Opening delimiter type used for standard images. static imageStart = ImageStart } function injectMarks(elements: readonly (Element | TreeElement)[], marks: Element[]) { if (!marks.length) return elements if (!elements.length) return marks let elts = elements.slice(), eI = 0 for (let mark of marks) { while (eI < elts.length && elts[eI].to < mark.to) eI++ if (eI < elts.length && elts[eI].from < mark.from) { let e = elts[eI] if (e instanceof Element) elts[eI] = new Element(e.type, e.from, e.to, injectMarks(e.children, [mark])) } else { elts.splice(eI++, 0, mark) } } return elts } // These are blocks that can span blank lines, and should thus only be // reused if their next sibling is also being reused. const NotLast = [Type.CodeBlock, Type.ListItem, Type.OrderedList, Type.BulletList] class FragmentCursor { // Index into fragment array i = 0 // Active fragment fragment: TreeFragment | null = null fragmentEnd = -1 // Cursor into the current fragment, if any. When `moveTo` returns // true, this points at the first block after `pos`. cursor: TreeCursor | null = null constructor(readonly fragments: readonly TreeFragment[], readonly input: Input) { if (fragments.length) this.fragment = fragments[this.i++] } nextFragment() { this.fragment = this.i < this.fragments.length ? this.fragments[this.i++] : null this.cursor = null this.fragmentEnd = -1 } moveTo(pos: number, lineStart: number) { while (this.fragment && this.fragment.to <= pos) this.nextFragment() if (!this.fragment || this.fragment.from > (pos ? pos - 1 : 0)) return false if (this.fragmentEnd < 0) { let end = this.fragment.to while (end > 0 && this.input.read(end - 1, end) != "\n") end-- this.fragmentEnd = end ? end - 1 : 0 } let c = this.cursor if (!c) { c = this.cursor = this.fragment.tree.cursor() c.firstChild() } let rPos = pos + this.fragment.offset while (c.to <= rPos) if (!c.parent()) return false for (;;) { if (c.from >= rPos) return this.fragment.from <= lineStart if (!c.childAfter(rPos)) return false } } matches(hash: number) { let tree = this.cursor!.tree return tree && tree.prop(NodeProp.contextHash) == hash } takeNodes(cx: BlockContext) { let cur = this.cursor!, off = this.fragment!.offset, fragEnd = this.fragmentEnd - (this.fragment!.openEnd ? 1 : 0) let start = cx.absoluteLineStart, end = start, blockI = cx.block.children.length let prevEnd = end, prevI = blockI for (;;) { if (cur.to - off > fragEnd) { if (cur.type.isAnonymous && cur.firstChild()) continue break } let pos = toRelative(cur.from - off, cx.ranges) if (cur.to - off <= cx.ranges[cx.rangeI].to) { // Fits in current range cx.addNode(cur.tree!, pos) } else { let dummy = new Tree(cx.parser.nodeSet.types[Type.Paragraph], [], [], 0, cx.block.hashProp) cx.reusePlaceholders.set(dummy, cur.tree!) cx.addNode(dummy, pos) } // Taken content must always end in a block, because incremental // parsing happens on block boundaries. Never stop directly // after an indented code block, since those can continue after // any number of blank lines. if (cur.type.is("Block")) { if (NotLast.indexOf(cur.type.id) < 0) { end = cur.to - off blockI = cx.block.children.length } else { end = prevEnd blockI = prevI } prevEnd = cur.to - off prevI = cx.block.children.length } if (!cur.nextSibling()) break } while (cx.block.children.length > blockI) { cx.block.children.pop() cx.block.positions.pop() } return end - start } } // Convert an input-stream-relative position to a // Markdown-doc-relative position by subtracting the size of all input // gaps before `abs`. function toRelative(abs: number, ranges: readonly {from: number, to: number}[]) { let pos = abs for (let i = 1; i < ranges.length; i++) { let gapFrom = ranges[i - 1].to, gapTo = ranges[i].from if (gapFrom < abs) pos -= gapTo - gapFrom } return pos } const markdownHighlighting = styleTags({ "Blockquote/...": t.quote, HorizontalRule: t.contentSeparator, "ATXHeading1/... SetextHeading1/...": t.heading1, "ATXHeading2/... SetextHeading2/...": t.heading2, "ATXHeading3/...": t.heading3, "ATXHeading4/...": t.heading4, "ATXHeading5/...": t.heading5, "ATXHeading6/...": t.heading6, "Comment CommentBlock": t.comment, Escape: t.escape, Entity: t.character, "Emphasis/...": t.emphasis, "StrongEmphasis/...": t.strong, "Link/... Image/...": t.link, "OrderedList/... BulletList/...": t.list, "BlockQuote/...": t.quote, "InlineCode CodeText": t.monospace, "URL Autolink": t.url, "HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": t.processingInstruction, "CodeInfo LinkLabel": t.labelName, LinkTitle: t.string, Paragraph: t.content }) /// The default CommonMark parser. export const parser = new MarkdownParser( new NodeSet(nodeTypes).extend(markdownHighlighting), Object.keys(DefaultBlockParsers).map(n => DefaultBlockParsers[n]), Object.keys(DefaultBlockParsers).map(n => DefaultLeafBlocks[n]), Object.keys(DefaultBlockParsers), DefaultEndLeaf, DefaultSkipMarkup, Object.keys(DefaultInline).map(n => DefaultInline[n]), Object.keys(DefaultInline), [] )