import { NodeType, NodeProp, NodeSet, Tree, Parser, parseMixed } from '@lezer/common'; import { styleTags, tags, Tag } from '@lezer/highlight'; class CompositeBlock { static create(type, value, from, parentHash, end) { let hash = (parentHash + (parentHash << 8) + type + (value << 4)) | 0; return new CompositeBlock(type, value, from, hash, end, [], []); } constructor(type, // Used for indentation in list items, markup character in lists value, from, hash, end, children, positions) { this.type = type; this.value = value; this.from = from; this.hash = hash; this.end = end; this.children = children; this.positions = positions; this.hashProp = [[NodeProp.contextHash, hash]]; } addChild(child, pos) { 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, 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) }); } } var Type; (function (Type) { Type[Type["Document"] = 1] = "Document"; Type[Type["CodeBlock"] = 2] = "CodeBlock"; Type[Type["FencedCode"] = 3] = "FencedCode"; Type[Type["Blockquote"] = 4] = "Blockquote"; Type[Type["HorizontalRule"] = 5] = "HorizontalRule"; Type[Type["BulletList"] = 6] = "BulletList"; Type[Type["OrderedList"] = 7] = "OrderedList"; Type[Type["ListItem"] = 8] = "ListItem"; Type[Type["ATXHeading1"] = 9] = "ATXHeading1"; Type[Type["ATXHeading2"] = 10] = "ATXHeading2"; Type[Type["ATXHeading3"] = 11] = "ATXHeading3"; Type[Type["ATXHeading4"] = 12] = "ATXHeading4"; Type[Type["ATXHeading5"] = 13] = "ATXHeading5"; Type[Type["ATXHeading6"] = 14] = "ATXHeading6"; Type[Type["SetextHeading1"] = 15] = "SetextHeading1"; Type[Type["SetextHeading2"] = 16] = "SetextHeading2"; Type[Type["HTMLBlock"] = 17] = "HTMLBlock"; Type[Type["LinkReference"] = 18] = "LinkReference"; Type[Type["Paragraph"] = 19] = "Paragraph"; Type[Type["CommentBlock"] = 20] = "CommentBlock"; Type[Type["ProcessingInstructionBlock"] = 21] = "ProcessingInstructionBlock"; // Inline Type[Type["Escape"] = 22] = "Escape"; Type[Type["Entity"] = 23] = "Entity"; Type[Type["HardBreak"] = 24] = "HardBreak"; Type[Type["Emphasis"] = 25] = "Emphasis"; Type[Type["StrongEmphasis"] = 26] = "StrongEmphasis"; Type[Type["Link"] = 27] = "Link"; Type[Type["Image"] = 28] = "Image"; Type[Type["InlineCode"] = 29] = "InlineCode"; Type[Type["HTMLTag"] = 30] = "HTMLTag"; Type[Type["Comment"] = 31] = "Comment"; Type[Type["ProcessingInstruction"] = 32] = "ProcessingInstruction"; Type[Type["URL"] = 33] = "URL"; // Smaller tokens Type[Type["HeaderMark"] = 34] = "HeaderMark"; Type[Type["QuoteMark"] = 35] = "QuoteMark"; Type[Type["ListMark"] = 36] = "ListMark"; Type[Type["LinkMark"] = 37] = "LinkMark"; Type[Type["EmphasisMark"] = 38] = "EmphasisMark"; Type[Type["CodeMark"] = 39] = "CodeMark"; Type[Type["CodeText"] = 40] = "CodeText"; Type[Type["CodeInfo"] = 41] = "CodeInfo"; Type[Type["LinkTitle"] = 42] = "LinkTitle"; Type[Type["LinkLabel"] = 43] = "LinkLabel"; })(Type || (Type = {})); /// Data structure used to accumulate a block's content during [leaf /// block parsing](#BlockParser.leaf). class LeafBlock { /// @internal constructor( /// The start position of the block. start, /// The block's text content. content) { this.start = start; this.content = content; /// @internal this.marks = []; /// The block parsers active for this block. this.parsers = []; } } /// Data structure used during block-level per-line parsing. class Line { constructor() { /// The line's full text. this.text = ""; /// The base indent provided by the composite contexts (that have /// been handled so far). this.baseIndent = 0; /// The string position corresponding to the base indent. this.basePos = 0; /// The number of contexts handled @internal this.depth = 0; /// Any markers (i.e. block quote markers) parsed for the contexts. @internal this.markers = []; /// The position of the next non-whitespace character beyond any /// list, blockquote, or other composite block markers. this.pos = 0; /// The column of the next non-whitespace character. this.indent = 0; /// The character code of the character after `pos`. this.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) { return skipSpace(this.text, from); } /// @internal reset(text) { 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) { 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) { 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) { this.markers.push(elt); } /// Find the column position at `to`, optionally starting at a given /// position and column. countIndent(to, 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) { 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, cx, 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.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; } }; function space(ch) { return ch == 32 || ch == 9 || ch == 10 || ch == 13; } function skipSpace(line, i = 0) { while (i < line.length && space(line.charCodeAt(i))) i++; return i; } function skipSpaceBack(line, i, to) { while (i > to && space(line.charCodeAt(i - 1))) i--; return i; } function isFencedCode(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) { return line.next != 62 /* '>' */ ? -1 : line.text.charCodeAt(line.pos + 1) == 32 ? 2 : 1; } function isHorizontalRule(line, cx, breaking) { 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) return -1; return count < 3 ? -1 : 1; } function inList(cx, type) { for (let i = cx.stack.length - 1; i >= 0; i--) if (cx.stack[i].type == type) return true; return false; } function isBulletList(line, cx, breaking) { 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, cx, breaking) { 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) { 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) { 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 ? 1 /* Mark.Open */ : 0) | (canClose ? 2 /* Mark.Close */ : 0))); }, 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, 1 /* Mark.Open */)) : -1; }, Image(cx, next, start) { return next == 33 /* '!' */ && cx.char(start + 1) == 91 /* '[' */ ? cx.append(new InlineDelimiter(ImageStart, start, start + 2, 1 /* 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 = 0; } return link.to; } } return -1; } }; function finishLink(cx, content, type, start, startPos) { 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); 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, start, offset) { 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, start, offset) { 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, start, offset, requireNonWS) { 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. class InlineContext { /// @internal constructor( /// The parser that is being used. parser, /// The text of this inline section. text, /// The starting offset of the section in the document. offset) { this.parser = parser; this.text = text; this.offset = offset; /// @internal this.parts = []; } /// Get the character code at the given (document-relative) /// position. char(pos) { 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, to) { return this.text.slice(from - this.offset, to - this.offset); } /// @internal append(elt) { 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, from, to, open, close) { return this.append(new InlineDelimiter(type, from, to, (open ? 1 /* Mark.Open */ : 0) | (close ? 2 /* Mark.Close */ : 0))); } /// Add an inline element. Returns the end of the element. addElement(elt) { 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) { // 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 & 2 /* Mark.Close */))) continue; let emp = close.type == EmphasisUnderscore || close.type == EmphasisAsterisk; let closeSize = close.to - close.from; let open, 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 & 1 /* Mark.Open */) && part.type == close.type && // Ignore emphasis delimiters where the character count doesn't match !(emp && ((close.side & 1 /* Mark.Open */) || (part.side & 2 /* 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]); 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) { for (let i = this.parts.length - 1; i >= 0; i--) { let part = this.parts[i]; if (part instanceof InlineDelimiter && part.type == type) 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) { let content = this.resolveMarkers(startIndex); this.parts.length = startIndex; return content; } /// 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) { return skipSpace(this.text, from - this.offset) + this.offset; } elt(type, from, to, children) { if (typeof type == "string") return elt(this.parser.getNodeType(type), from, to, children); return new TreeElement(type, from); } } function injectMarks(elements, marks) { 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 { constructor(fragments, input) { this.fragments = fragments; this.input = input; // Index into fragment array this.i = 0; // Active fragment this.fragment = null; this.fragmentEnd = -1; // Cursor into the current fragment, if any. When `moveTo` returns // true, this points at the first block after `pos`. this.cursor = null; 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, lineStart) { 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) { let tree = this.cursor.tree; return tree && tree.prop(NodeProp.contextHash) == hash; } takeNodes(cx) { 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, ranges) { 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/...": tags.quote, HorizontalRule: tags.contentSeparator, "ATXHeading1/... SetextHeading1/...": tags.heading1, "ATXHeading2/... SetextHeading2/...": tags.heading2, "ATXHeading3/...": tags.heading3, "ATXHeading4/...": tags.heading4, "ATXHeading5/...": tags.heading5, "ATXHeading6/...": tags.heading6, "Comment CommentBlock": tags.comment, Escape: tags.escape, Entity: tags.character, "Emphasis/...": tags.emphasis, "StrongEmphasis/...": tags.strong, "Link/... Image/...": tags.link, "OrderedList/... BulletList/...": tags.list, "BlockQuote/...": tags.quote, "InlineCode CodeText": tags.monospace, URL: tags.url, "HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": tags.processingInstruction, "CodeInfo LinkLabel": tags.labelName, LinkTitle: tags.string, Paragraph: tags.content }); /// The default CommonMark parser. 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), []); function leftOverSpace(node, from, to) { let ranges = []; for (let n = node.firstChild, pos = from;; n = n.nextSibling) { let nextPos = n ? n.from : to; if (nextPos > pos) ranges.push({ from: pos, to: nextPos }); if (!n) break; pos = n.to; } return ranges; } /// Create a Markdown extension to enable nested parsing on code /// blocks and/or embedded HTML. function parseCode(config) { let { codeParser, htmlParser } = config; let wrap = parseMixed((node, input) => { let id = node.type.id; if (codeParser && (id == Type.CodeBlock || id == Type.FencedCode)) { let info = ""; if (id == Type.FencedCode) { let infoNode = node.node.getChild(Type.CodeInfo); if (infoNode) info = input.read(infoNode.from, infoNode.to); } let parser = codeParser(info); if (parser) return { parser, overlay: node => node.type.id == Type.CodeText }; } else if (htmlParser && (id == Type.HTMLBlock || id == Type.HTMLTag)) { return { parser: htmlParser, overlay: leftOverSpace(node.node, node.from, node.to) }; } return null; }); return { wrap }; } const StrikethroughDelim = { resolve: "Strikethrough", mark: "StrikethroughMark" }; /// An extension that implements /// [GFM-style](https://github.github.com/gfm/#strikethrough-extension-) /// Strikethrough syntax using `~~` delimiters. const Strikethrough = { defineNodes: [{ name: "Strikethrough", style: { "Strikethrough/...": tags.strikethrough } }, { name: "StrikethroughMark", style: tags.processingInstruction }], parseInline: [{ name: "Strikethrough", parse(cx, next, pos) { if (next != 126 /* '~' */ || cx.char(pos + 1) != 126 || cx.char(pos + 2) == 126) return -1; let before = cx.slice(pos - 1, pos), after = cx.slice(pos + 2, pos + 3); let sBefore = /\s|^$/.test(before), sAfter = /\s|^$/.test(after); let pBefore = Punctuation.test(before), pAfter = Punctuation.test(after); return cx.addDelimiter(StrikethroughDelim, pos, pos + 2, !sAfter && (!pAfter || sBefore || pBefore), !sBefore && (!pBefore || sAfter || pAfter)); }, after: "Emphasis" }] }; function parseRow(cx, line, startI = 0, elts, offset = 0) { let count = 0, first = true, cellStart = -1, cellEnd = -1, esc = false; let parseCell = () => { elts.push(cx.elt("TableCell", offset + cellStart, offset + cellEnd, cx.parser.parseInline(line.slice(cellStart, cellEnd), offset + cellStart))); }; for (let i = startI; i < line.length; i++) { let next = line.charCodeAt(i); if (next == 124 /* '|' */ && !esc) { if (!first || cellStart > -1) count++; first = false; if (elts) { if (cellStart > -1) parseCell(); elts.push(cx.elt("TableDelimiter", i + offset, i + offset + 1)); } cellStart = cellEnd = -1; } else if (esc || next != 32 && next != 9) { if (cellStart < 0) cellStart = i; cellEnd = i + 1; } esc = !esc && next == 92; } if (cellStart > -1) { count++; if (elts) parseCell(); } return count; } function hasPipe(str, start) { for (let i = start; i < str.length; i++) { let next = str.charCodeAt(i); if (next == 124 /* '|' */) return true; if (next == 92 /* '\\' */) i++; } return false; } const delimiterLine = /^\|?(\s*:?-+:?\s*\|)+(\s*:?-+:?\s*)?$/; class TableParser { constructor() { // Null means we haven't seen the second line yet, false means this // isn't a table, and an array means this is a table and we've // parsed the given rows so far. this.rows = null; } nextLine(cx, line, leaf) { if (this.rows == null) { // Second line this.rows = false; let lineText; if ((line.next == 45 || line.next == 58 || line.next == 124 /* '-:|' */) && delimiterLine.test(lineText = line.text.slice(line.pos))) { let firstRow = [], firstCount = parseRow(cx, leaf.content, 0, firstRow, leaf.start); if (firstCount == parseRow(cx, lineText, line.pos)) this.rows = [cx.elt("TableHeader", leaf.start, leaf.start + leaf.content.length, firstRow), cx.elt("TableDelimiter", cx.lineStart + line.pos, cx.lineStart + line.text.length)]; } } else if (this.rows) { // Line after the second let content = []; parseRow(cx, line.text, line.pos, content, cx.lineStart); this.rows.push(cx.elt("TableRow", cx.lineStart + line.pos, cx.lineStart + line.text.length, content)); } return false; } finish(cx, leaf) { if (!this.rows) return false; cx.addLeafElement(leaf, cx.elt("Table", leaf.start, leaf.start + leaf.content.length, this.rows)); return true; } } /// This extension provides /// [GFM-style](https://github.github.com/gfm/#tables-extension-) /// tables, using syntax like this: /// /// ``` /// | head 1 | head 2 | /// | --- | --- | /// | cell 1 | cell 2 | /// ``` const Table = { defineNodes: [ { name: "Table", block: true }, { name: "TableHeader", style: { "TableHeader/...": tags.heading } }, "TableRow", { name: "TableCell", style: tags.content }, { name: "TableDelimiter", style: tags.processingInstruction }, ], parseBlock: [{ name: "Table", leaf(_, leaf) { return hasPipe(leaf.content, 0) ? new TableParser : null; }, endLeaf(cx, line, leaf) { if (leaf.parsers.some(p => p instanceof TableParser) || !hasPipe(line.text, line.basePos)) return false; let next = cx.scanLine(cx.absoluteLineEnd + 1).text; return delimiterLine.test(next) && parseRow(cx, line.text, line.basePos) == parseRow(cx, next, line.basePos); }, before: "SetextHeading" }] }; class TaskParser { nextLine() { return false; } finish(cx, leaf) { cx.addLeafElement(leaf, cx.elt("Task", leaf.start, leaf.start + leaf.content.length, [ cx.elt("TaskMarker", leaf.start, leaf.start + 3), ...cx.parser.parseInline(leaf.content.slice(3), leaf.start + 3) ])); return true; } } /// Extension providing /// [GFM-style](https://github.github.com/gfm/#task-list-items-extension-) /// task list items, where list items can be prefixed with `[ ]` or /// `[x]` to add a checkbox. const TaskList = { defineNodes: [ { name: "Task", block: true, style: tags.list }, { name: "TaskMarker", style: tags.atom } ], parseBlock: [{ name: "TaskList", leaf(cx, leaf) { return /^\[[ xX]\][ \t]/.test(leaf.content) && cx.parentType().name == "ListItem" ? new TaskParser : null; }, after: "SetextHeading" }] }; const autolinkRE = /(www\.)|(https?:\/\/)|([\w.+-]+@)|(mailto:|xmpp:)/gy; const urlRE = /[\w-]+(\.\w+(\.\w+)?)(\/[^\s<]*)?/gy; const emailRE = /[\w.+-]+@[\w-]+\.[\w.-]+/gy; const xmppResourceRE = /\/[a-zA-Z\d@.]+/gy; function count(str, from, to, ch) { let result = 0; for (let i = from; i < to; i++) if (str[i] == ch) result++; return result; } function autolinkURLEnd(text, from) { urlRE.lastIndex = from; let m = urlRE.exec(text); if (!m) return -1; let end = from + m[0].length; for (;;) { let last = text[end - 1], m; if (/[?!.,:*_~]/.test(last) || last == ")" && count(text, from, end, ")") > count(text, from, end, "(")) end--; else if (last == ";" && (m = /&(?:#\d+|#x[a-f\d]+|\w+);$/.exec(text.slice(from, end)))) end = from + m.index; else break; } return end; } function autolinkEmailEnd(text, from) { emailRE.lastIndex = from; let m = emailRE.exec(text); if (!m) return -1; let last = m[0][m[0].length - 1]; return last == "_" || last == "-" ? -1 : from + m[0].length - (last == "." ? 1 : 0); } /// Extension that implements autolinking for /// `www.`/`http://`/`https://`/`mailto:`/`xmpp:` URLs and email /// addresses. const Autolink = { parseInline: [{ name: "Autolink", parse(cx, next, absPos) { let pos = absPos - cx.offset; autolinkRE.lastIndex = pos; let m = autolinkRE.exec(cx.text), end = -1; if (!m) return -1; if (m[1] || m[2]) { // www., http:// end = autolinkURLEnd(cx.text, pos + m[0].length); } else if (m[3]) { // email address end = autolinkEmailEnd(cx.text, pos); } else { // mailto:/xmpp: end = autolinkEmailEnd(cx.text, pos + m[0].length); if (end > -1 && m[0] == "xmpp:") { xmppResourceRE.lastIndex = end; m = xmppResourceRE.exec(cx.text); if (m) end = m.index + m[0].length; } } if (end < 0) return -1; cx.addElement(cx.elt("URL", absPos, end + cx.offset)); return end + cx.offset; } }] }; /// Extension bundle containing [`Table`](#Table), /// [`TaskList`](#TaskList), [`Strikethrough`](#Strikethrough), and /// [`Autolink`](#Autolink). const GFM = [Table, TaskList, Strikethrough, Autolink]; function parseSubSuper(ch, node, mark) { return (cx, next, pos) => { if (next != ch || cx.char(pos + 1) == ch) return -1; let elts = [cx.elt(mark, pos, pos + 1)]; for (let i = pos + 1; i < cx.end; i++) { let next = cx.char(i); if (next == ch) return cx.addElement(cx.elt(node, pos, i + 1, elts.concat(cx.elt(mark, i, i + 1)))); if (next == 92 /* '\\' */) elts.push(cx.elt("Escape", i, i++ + 2)); if (space(next)) break; } return -1; }; } /// Extension providing /// [Pandoc-style](https://pandoc.org/MANUAL.html#superscripts-and-subscripts) /// superscript using `^` markers. const Superscript = { defineNodes: [ { name: "Superscript", style: tags.special(tags.content) }, { name: "SuperscriptMark", style: tags.processingInstruction } ], parseInline: [{ name: "Superscript", parse: parseSubSuper(94 /* '^' */, "Superscript", "SuperscriptMark") }] }; /// Extension providing /// [Pandoc-style](https://pandoc.org/MANUAL.html#superscripts-and-subscripts) /// subscript using `~` markers. const Subscript = { defineNodes: [ { name: "Subscript", style: tags.special(tags.content) }, { name: "SubscriptMark", style: tags.processingInstruction } ], parseInline: [{ name: "Subscript", parse: parseSubSuper(126 /* '~' */, "Subscript", "SubscriptMark") }] }; /// Extension that parses two colons with only letters, underscores, /// and numbers between them as `Emoji` nodes. const Emoji = { defineNodes: [{ name: "Emoji", style: tags.character }], parseInline: [{ name: "Emoji", parse(cx, next, pos) { let match; if (next != 58 /* ':' */ || !(match = /^[a-zA-Z_0-9]+:/.exec(cx.slice(pos + 1, cx.end)))) return -1; return cx.addElement(cx.elt("Emoji", pos, pos + 1 + match[0].length)); } }] }; export { Autolink, BlockContext, Element, Emoji, GFM, InlineContext, LeafBlock, Line, MarkdownParser, Strikethrough, Subscript, Superscript, Table, TaskList, parseCode, parser };