Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com
at 9199d4f348146b93a8d99c8a2fb6f3bf911d45ca 1961 lines 75 kB view raw
1import {Tree, TreeBuffer, NodeType, NodeProp, NodePropSource, TreeFragment, NodeSet, TreeCursor, 2 Input, Parser, PartialParse, SyntaxNode, ParseWrapper} from "@lezer/common" 3import {styleTags, tags as t, Tag} from "@lezer/highlight" 4 5class CompositeBlock { 6 static create(type: number, value: number, from: number, parentHash: number, end: number) { 7 let hash = (parentHash + (parentHash << 8) + type + (value << 4)) | 0 8 return new CompositeBlock(type, value, from, hash, end, [], []) 9 } 10 11 /// @internal 12 hashProp: [NodeProp<any>, any][] 13 14 constructor(readonly type: number, 15 // Used for indentation in list items, markup character in lists 16 readonly value: number, 17 readonly from: number, 18 readonly hash: number, 19 public end: number, 20 readonly children: (Tree | TreeBuffer)[], 21 readonly positions: number[]) { 22 this.hashProp = [[NodeProp.contextHash, hash]] 23 } 24 25 addChild(child: Tree, pos: number) { 26 if (child.prop(NodeProp.contextHash) != this.hash) 27 child = new Tree(child.type, child.children, child.positions, child.length, this.hashProp) 28 this.children.push(child) 29 this.positions.push(pos) 30 } 31 32 toTree(nodeSet: NodeSet, end = this.end) { 33 let last = this.children.length - 1 34 if (last >= 0) end = Math.max(end, this.positions[last] + this.children[last].length + this.from) 35 return new Tree(nodeSet.types[this.type], this.children, this.positions, end - this.from).balance({ 36 makeTree: (children, positions, length) => new Tree(NodeType.none, children, positions, length, this.hashProp) 37 }) 38 } 39} 40 41export enum Type { 42 Document = 1, 43 44 CodeBlock, 45 FencedCode, 46 Blockquote, 47 HorizontalRule, 48 BulletList, 49 OrderedList, 50 ListItem, 51 ATXHeading1, 52 ATXHeading2, 53 ATXHeading3, 54 ATXHeading4, 55 ATXHeading5, 56 ATXHeading6, 57 SetextHeading1, 58 SetextHeading2, 59 HTMLBlock, 60 LinkReference, 61 Paragraph, 62 CommentBlock, 63 ProcessingInstructionBlock, 64 65 // Inline 66 Escape, 67 Entity, 68 HardBreak, 69 Emphasis, 70 StrongEmphasis, 71 Link, 72 Image, 73 InlineCode, 74 HTMLTag, 75 Comment, 76 ProcessingInstruction, 77 Autolink, 78 79 // Smaller tokens 80 HeaderMark, 81 QuoteMark, 82 ListMark, 83 LinkMark, 84 EmphasisMark, 85 CodeMark, 86 CodeText, 87 CodeInfo, 88 LinkTitle, 89 LinkLabel, 90 URL 91} 92 93/// Data structure used to accumulate a block's content during [leaf 94/// block parsing](#BlockParser.leaf). 95export class LeafBlock { 96 /// @internal 97 marks: Element[] = [] 98 /// The block parsers active for this block. 99 parsers: LeafBlockParser[] = [] 100 101 /// @internal 102 constructor( 103 /// The start position of the block. 104 readonly start: number, 105 /// The block's text content. 106 public content: string 107 ) {} 108} 109 110/// Data structure used during block-level per-line parsing. 111export class Line { 112 /// The line's full text. 113 text = "" 114 /// The base indent provided by the composite contexts (that have 115 /// been handled so far). 116 baseIndent = 0 117 /// The string position corresponding to the base indent. 118 basePos = 0 119 /// The number of contexts handled @internal 120 depth = 0 121 /// Any markers (i.e. block quote markers) parsed for the contexts. @internal 122 markers: Element[] = [] 123 /// The position of the next non-whitespace character beyond any 124 /// list, blockquote, or other composite block markers. 125 pos = 0 126 /// The column of the next non-whitespace character. 127 indent = 0 128 /// The character code of the character after `pos`. 129 next = -1 130 131 /// @internal 132 forward() { 133 if (this.basePos > this.pos) this.forwardInner() 134 } 135 136 /// @internal 137 forwardInner() { 138 let newPos = this.skipSpace(this.basePos) 139 this.indent = this.countIndent(newPos, this.pos, this.indent) 140 this.pos = newPos 141 this.next = newPos == this.text.length ? -1 : this.text.charCodeAt(newPos) 142 } 143 144 /// Skip whitespace after the given position, return the position of 145 /// the next non-space character or the end of the line if there's 146 /// only space after `from`. 147 skipSpace(from: number) { return skipSpace(this.text, from) } 148 149 /// @internal 150 reset(text: string) { 151 this.text = text 152 this.baseIndent = this.basePos = this.pos = this.indent = 0 153 this.forwardInner() 154 this.depth = 1 155 while (this.markers.length) this.markers.pop() 156 } 157 158 /// Move the line's base position forward to the given position. 159 /// This should only be called by composite [block 160 /// parsers](#BlockParser.parse) or [markup skipping 161 /// functions](#NodeSpec.composite). 162 moveBase(to: number) { 163 this.basePos = to 164 this.baseIndent = this.countIndent(to, this.pos, this.indent) 165 } 166 167 /// Move the line's base position forward to the given _column_. 168 moveBaseColumn(indent: number) { 169 this.baseIndent = indent 170 this.basePos = this.findColumn(indent) 171 } 172 173 /// Store a composite-block-level marker. Should be called from 174 /// [markup skipping functions](#NodeSpec.composite) when they 175 /// consume any non-whitespace characters. 176 addMarker(elt: Element) { 177 this.markers.push(elt) 178 } 179 180 /// Find the column position at `to`, optionally starting at a given 181 /// position and column. 182 countIndent(to: number, from = 0, indent = 0) { 183 for (let i = from; i < to; i++) 184 indent += this.text.charCodeAt(i) == 9 ? 4 - indent % 4 : 1 185 return indent 186 } 187 188 /// Find the position corresponding to the given column. 189 findColumn(goal: number) { 190 let i = 0 191 for (let indent = 0; i < this.text.length && indent < goal; i++) 192 indent += this.text.charCodeAt(i) == 9 ? 4 - indent % 4 : 1 193 return i 194 } 195 196 /// @internal 197 scrub() { 198 if (!this.baseIndent) return this.text 199 let result = "" 200 for (let i = 0; i < this.basePos; i++) result += " " 201 return result + this.text.slice(this.basePos) 202 } 203} 204 205function skipForList(bl: CompositeBlock, cx: BlockContext, line: Line) { 206 if (line.pos == line.text.length || 207 (bl != cx.block && line.indent >= cx.stack[line.depth + 1].value + line.baseIndent)) return true 208 if (line.indent >= line.baseIndent + 4) return false 209 let size = (bl.type == Type.OrderedList ? isOrderedList : isBulletList)(line, cx, false) 210 return size > 0 && 211 (bl.type != Type.BulletList || isHorizontalRule(line, cx, false) < 0) && 212 line.text.charCodeAt(line.pos + size - 1) == bl.value 213} 214 215const DefaultSkipMarkup: {[type: number]: (bl: CompositeBlock, cx: BlockContext, line: Line) => boolean} = { 216 [Type.Blockquote](bl, cx, line) { 217 if (line.next != 62 /* '>' */) return false 218 line.markers.push(elt(Type.QuoteMark, cx.lineStart + line.pos, cx.lineStart + line.pos + 1)) 219 line.moveBase(line.pos + (space(line.text.charCodeAt(line.pos + 1)) ? 2 : 1)) 220 bl.end = cx.lineStart + line.text.length 221 return true 222 }, 223 [Type.ListItem](bl, _cx, line) { 224 if (line.indent < line.baseIndent + bl.value && line.next > -1) return false 225 line.moveBaseColumn(line.baseIndent + bl.value) 226 return true 227 }, 228 [Type.OrderedList]: skipForList, 229 [Type.BulletList]: skipForList, 230 [Type.Document]() { return true } 231} 232 233export function space(ch: number) { return ch == 32 || ch == 9 || ch == 10 || ch == 13 } 234 235function skipSpace(line: string, i = 0) { 236 while (i < line.length && space(line.charCodeAt(i))) i++ 237 return i 238} 239 240function skipSpaceBack(line: string, i: number, to: number) { 241 while (i > to && space(line.charCodeAt(i - 1))) i-- 242 return i 243} 244 245function isFencedCode(line: Line) { 246 if (line.next != 96 && line.next != 126 /* '`~' */) return -1 247 let pos = line.pos + 1 248 while (pos < line.text.length && line.text.charCodeAt(pos) == line.next) pos++ 249 if (pos < line.pos + 3) return -1 250 if (line.next == 96) for (let i = pos; i < line.text.length; i++) if (line.text.charCodeAt(i) == 96) return -1 251 return pos 252} 253 254function isBlockquote(line: Line) { 255 return line.next != 62 /* '>' */ ? -1 : line.text.charCodeAt(line.pos + 1) == 32 ? 2 : 1 256} 257 258function isHorizontalRule(line: Line, cx: BlockContext, breaking: boolean) { 259 if (line.next != 42 && line.next != 45 && line.next != 95 /* '_-*' */) return -1 260 let count = 1 261 for (let pos = line.pos + 1; pos < line.text.length; pos++) { 262 let ch = line.text.charCodeAt(pos) 263 if (ch == line.next) count++ 264 else if (!space(ch)) return -1 265 } 266 // Setext headers take precedence 267 if (breaking && line.next == 45 && isSetextUnderline(line) > -1 && line.depth == cx.stack.length && 268 cx.parser.leafBlockParsers.indexOf(DefaultLeafBlocks.SetextHeading) > -1) return -1 269 return count < 3 ? -1 : 1 270} 271 272function inList(cx: BlockContext, type: Type) { 273 for (let i = cx.stack.length - 1; i >= 0; i--) 274 if (cx.stack[i].type == type) return true 275 return false 276} 277 278function isBulletList(line: Line, cx: BlockContext, breaking: boolean) { 279 return (line.next == 45 || line.next == 43 || line.next == 42 /* '-+*' */) && 280 (line.pos == line.text.length - 1 || space(line.text.charCodeAt(line.pos + 1))) && 281 (!breaking || inList(cx, Type.BulletList) || line.skipSpace(line.pos + 2) < line.text.length) ? 1 : -1 282} 283 284function isOrderedList(line: Line, cx: BlockContext, breaking: boolean) { 285 let pos = line.pos, next = line.next 286 for (;;) { 287 if (next >= 48 && next <= 57 /* '0-9' */) pos++ 288 else break 289 if (pos == line.text.length) return -1 290 next = line.text.charCodeAt(pos) 291 } 292 if (pos == line.pos || pos > line.pos + 9 || 293 (next != 46 && next != 41 /* '.)' */) || 294 (pos < line.text.length - 1 && !space(line.text.charCodeAt(pos + 1))) || 295 breaking && !inList(cx, Type.OrderedList) && 296 (line.skipSpace(pos + 1) == line.text.length || pos > line.pos + 1 || line.next != 49 /* '1' */)) 297 return -1 298 return pos + 1 - line.pos 299} 300 301function isAtxHeading(line: Line) { 302 if (line.next != 35 /* '#' */) return -1 303 let pos = line.pos + 1 304 while (pos < line.text.length && line.text.charCodeAt(pos) == 35) pos++ 305 if (pos < line.text.length && line.text.charCodeAt(pos) != 32) return -1 306 let size = pos - line.pos 307 return size > 6 ? -1 : size 308} 309 310function isSetextUnderline(line: Line) { 311 if (line.next != 45 && line.next != 61 /* '-=' */ || line.indent >= line.baseIndent + 4) return -1 312 let pos = line.pos + 1 313 while (pos < line.text.length && line.text.charCodeAt(pos) == line.next) pos++ 314 let end = pos 315 while (pos < line.text.length && space(line.text.charCodeAt(pos))) pos++ 316 return pos == line.text.length ? end : -1 317} 318 319const EmptyLine = /^[ \t]*$/, CommentEnd = /-->/, ProcessingEnd = /\?>/ 320const HTMLBlockStyle = [ 321 [/^<(?:script|pre|style)(?:\s|>|$)/i, /<\/(?:script|pre|style)>/i], 322 [/^\s*<!--/, CommentEnd], 323 [/^\s*<\?/, ProcessingEnd], 324 [/^\s*<![A-Z]/, />/], 325 [/^\s*<!\[CDATA\[/, /\]\]>/], 326 [/^\s*<\/?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|\/?>|$)/i, EmptyLine], 327 [/^\s*(?:<\/[a-z][\w-]*\s*>|<[a-z][\w-]*(\s+[a-z:_][\w-.]*(?:\s*=\s*(?:[^\s"'=<>`]+|'[^']*'|"[^"]*"))?)*\s*>)\s*$/i, EmptyLine] 328] 329 330function isHTMLBlock(line: Line, _cx: BlockContext, breaking: boolean) { 331 if (line.next != 60 /* '<' */) return -1 332 let rest = line.text.slice(line.pos) 333 for (let i = 0, e = HTMLBlockStyle.length - (breaking ? 1 : 0); i < e; i++) 334 if (HTMLBlockStyle[i][0].test(rest)) return i 335 return -1 336} 337 338function getListIndent(line: Line, pos: number) { 339 let indentAfter = line.countIndent(pos, line.pos, line.indent) 340 let indented = line.countIndent(line.skipSpace(pos), pos, indentAfter) 341 return indented >= indentAfter + 5 ? indentAfter + 1 : indented 342} 343 344// Return type for block parsing functions. Can be either: 345// 346// - false to indicate that nothing was matched and lower-precedence 347// parsers should run. 348// 349// - true to indicate that a leaf block was parsed and the stream 350// was advanced past its content. 351// 352// - null to indicate that a context was opened and block parsing 353// should continue on this line. 354type BlockResult = boolean | null 355 356function addCodeText(marks: Element[], from: number, to: number) { 357 let last = marks.length - 1 358 if (last >= 0 && marks[last].to == from && marks[last].type == Type.CodeText) (marks[last] as any).to = to 359 else marks.push(elt(Type.CodeText, from, to)) 360} 361 362// Rules for parsing blocks. A return value of false means the rule 363// doesn't apply here, true means it does. When true is returned and 364// `p.line` has been updated, the rule is assumed to have consumed a 365// leaf block. Otherwise, it is assumed to have opened a context. 366const DefaultBlockParsers: {[name: string]: ((cx: BlockContext, line: Line) => BlockResult) | undefined} = { 367 LinkReference: undefined, 368 369 IndentedCode(cx, line) { 370 let base = line.baseIndent + 4 371 if (line.indent < base) return false 372 let start = line.findColumn(base) 373 let from = cx.lineStart + start, to = cx.lineStart + line.text.length 374 let marks: Element[] = [], pendingMarks: Element[] = [] 375 addCodeText(marks, from, to) 376 while (cx.nextLine() && line.depth >= cx.stack.length) { 377 if (line.pos == line.text.length) { // Empty 378 addCodeText(pendingMarks, cx.lineStart - 1, cx.lineStart) 379 for (let m of line.markers) pendingMarks.push(m) 380 } else if (line.indent < base) { 381 break 382 } else { 383 if (pendingMarks.length) { 384 for (let m of pendingMarks) { 385 if (m.type == Type.CodeText) addCodeText(marks, m.from, m.to) 386 else marks.push(m) 387 } 388 pendingMarks = [] 389 } 390 addCodeText(marks, cx.lineStart - 1, cx.lineStart) 391 for (let m of line.markers) marks.push(m) 392 to = cx.lineStart + line.text.length 393 let codeStart = cx.lineStart + line.findColumn(line.baseIndent + 4) 394 if (codeStart < to) addCodeText(marks, codeStart, to) 395 } 396 } 397 if (pendingMarks.length) { 398 pendingMarks = pendingMarks.filter(m => m.type != Type.CodeText) 399 if (pendingMarks.length) line.markers = pendingMarks.concat(line.markers) 400 } 401 402 cx.addNode(cx.buffer.writeElements(marks, -from).finish(Type.CodeBlock, to - from), from) 403 return true 404 }, 405 406 FencedCode(cx, line) { 407 let fenceEnd = isFencedCode(line) 408 if (fenceEnd < 0) return false 409 let from = cx.lineStart + line.pos, ch = line.next, len = fenceEnd - line.pos 410 let infoFrom = line.skipSpace(fenceEnd), infoTo = skipSpaceBack(line.text, line.text.length, infoFrom) 411 let marks: (Element | TreeElement)[] = [elt(Type.CodeMark, from, from + len)] 412 if (infoFrom < infoTo) 413 marks.push(elt(Type.CodeInfo, cx.lineStart + infoFrom, cx.lineStart + infoTo)) 414 415 for (let first = true, empty = true, hasLine = false; cx.nextLine() && line.depth >= cx.stack.length; first = false) { 416 let i = line.pos 417 if (line.indent - line.baseIndent < 4) 418 while (i < line.text.length && line.text.charCodeAt(i) == ch) i++ 419 if (i - line.pos >= len && line.skipSpace(i) == line.text.length) { 420 for (let m of line.markers) marks.push(m) 421 if (empty && hasLine) addCodeText(marks, cx.lineStart - 1, cx.lineStart) 422 marks.push(elt(Type.CodeMark, cx.lineStart + line.pos, cx.lineStart + i)) 423 cx.nextLine() 424 break 425 } else { 426 hasLine = true 427 if (!first) { addCodeText(marks, cx.lineStart - 1, cx.lineStart); empty = false } 428 for (let m of line.markers) marks.push(m) 429 let textStart = cx.lineStart + line.basePos, textEnd = cx.lineStart + line.text.length 430 if (textStart < textEnd) { addCodeText(marks, textStart, textEnd); empty = false } 431 } 432 } 433 cx.addNode(cx.buffer.writeElements(marks, -from) 434 .finish(Type.FencedCode, cx.prevLineEnd() - from), from) 435 return true 436 }, 437 438 Blockquote(cx, line) { 439 let size = isBlockquote(line) 440 if (size < 0) return false 441 cx.startContext(Type.Blockquote, line.pos) 442 cx.addNode(Type.QuoteMark, cx.lineStart + line.pos, cx.lineStart + line.pos + 1) 443 line.moveBase(line.pos + size) 444 return null 445 }, 446 447 HorizontalRule(cx, line) { 448 if (isHorizontalRule(line, cx, false) < 0) return false 449 let from = cx.lineStart + line.pos 450 cx.nextLine() 451 cx.addNode(Type.HorizontalRule, from) 452 return true 453 }, 454 455 BulletList(cx, line) { 456 let size = isBulletList(line, cx, false) 457 if (size < 0) return false 458 if (cx.block.type != Type.BulletList) 459 cx.startContext(Type.BulletList, line.basePos, line.next) 460 let newBase = getListIndent(line, line.pos + 1) 461 cx.startContext(Type.ListItem, line.basePos, newBase - line.baseIndent) 462 cx.addNode(Type.ListMark, cx.lineStart + line.pos, cx.lineStart + line.pos + size) 463 line.moveBaseColumn(newBase) 464 return null 465 }, 466 467 OrderedList(cx, line) { 468 let size = isOrderedList(line, cx, false) 469 if (size < 0) return false 470 if (cx.block.type != Type.OrderedList) 471 cx.startContext(Type.OrderedList, line.basePos, line.text.charCodeAt(line.pos + size - 1)) 472 let newBase = getListIndent(line, line.pos + size) 473 cx.startContext(Type.ListItem, line.basePos, newBase - line.baseIndent) 474 cx.addNode(Type.ListMark, cx.lineStart + line.pos, cx.lineStart + line.pos + size) 475 line.moveBaseColumn(newBase) 476 return null 477 }, 478 479 ATXHeading(cx, line) { 480 let size = isAtxHeading(line) 481 if (size < 0) return false 482 let off = line.pos, from = cx.lineStart + off 483 let endOfSpace = skipSpaceBack(line.text, line.text.length, off), after = endOfSpace 484 while (after > off && line.text.charCodeAt(after - 1) == line.next) after-- 485 if (after == endOfSpace || after == off || !space(line.text.charCodeAt(after - 1))) after = line.text.length 486 let buf = cx.buffer 487 .write(Type.HeaderMark, 0, size) 488 .writeElements(cx.parser.parseInline(line.text.slice(off + size + 1, after), from + size + 1), -from) 489 if (after < line.text.length) buf.write(Type.HeaderMark, after - off, endOfSpace - off) 490 let node = buf.finish(Type.ATXHeading1 - 1 + size, line.text.length - off) 491 cx.nextLine() 492 cx.addNode(node, from) 493 return true 494 }, 495 496 HTMLBlock(cx, line) { 497 let type = isHTMLBlock(line, cx, false) 498 if (type < 0) return false 499 let from = cx.lineStart + line.pos, end = HTMLBlockStyle[type][1] 500 let marks: Element[] = [], trailing = end != EmptyLine 501 while (!end.test(line.text) && cx.nextLine()) { 502 if (line.depth < cx.stack.length) { trailing = false; break } 503 for (let m of line.markers) marks.push(m) 504 } 505 if (trailing) cx.nextLine() 506 let nodeType = end == CommentEnd ? Type.CommentBlock : end == ProcessingEnd ? Type.ProcessingInstructionBlock : Type.HTMLBlock 507 let to = cx.prevLineEnd() 508 cx.addNode(cx.buffer.writeElements(marks, -from).finish(nodeType, to - from), from) 509 return true 510 }, 511 512 SetextHeading: undefined // Specifies relative precedence for block-continue function 513} 514 515const enum RefStage { Failed = -1, Start, Label, Link, Title } 516 517// This implements a state machine that incrementally parses link references. At each 518// next line, it looks ahead to see if the line continues the reference or not. If it 519// doesn't and a valid link is available ending before that line, it finishes that. 520// Similarly, on `finish` (when the leaf is terminated by external circumstances), it 521// creates a link reference if there's a valid reference up to the current point. 522class LinkReferenceParser implements LeafBlockParser { 523 stage = RefStage.Start 524 elts: Element[] = [] 525 pos = 0 526 start: number 527 528 constructor(leaf: LeafBlock) { 529 this.start = leaf.start 530 this.advance(leaf.content) 531 } 532 533 nextLine(cx: BlockContext, line: Line, leaf: LeafBlock) { 534 if (this.stage == RefStage.Failed) return false 535 let content = leaf.content + "\n" + line.scrub() 536 let finish = this.advance(content) 537 if (finish > -1 && finish < content.length) return this.complete(cx, leaf, finish) 538 return false 539 } 540 541 finish(cx: BlockContext, leaf: LeafBlock) { 542 if ((this.stage == RefStage.Link || this.stage == RefStage.Title) && skipSpace(leaf.content, this.pos) == leaf.content.length) 543 return this.complete(cx, leaf, leaf.content.length) 544 return false 545 } 546 547 complete(cx: BlockContext, leaf: LeafBlock, len: number) { 548 cx.addLeafElement(leaf, elt(Type.LinkReference, this.start, this.start + len, this.elts)) 549 return true 550 } 551 552 nextStage(elt: Element | null | false) { 553 if (elt) { 554 this.pos = elt.to - this.start 555 this.elts.push(elt) 556 this.stage++ 557 return true 558 } 559 if (elt === false) this.stage = RefStage.Failed 560 return false 561 } 562 563 advance(content: string) { 564 for (;;) { 565 if (this.stage == RefStage.Failed) { 566 return -1 567 } else if (this.stage == RefStage.Start) { 568 if (!this.nextStage(parseLinkLabel(content, this.pos, this.start, true))) return -1 569 if (content.charCodeAt(this.pos) != 58 /* ':' */) return this.stage = RefStage.Failed 570 this.elts.push(elt(Type.LinkMark, this.pos + this.start, this.pos + this.start + 1)) 571 this.pos++ 572 } else if (this.stage == RefStage.Label) { 573 if (!this.nextStage(parseURL(content, skipSpace(content, this.pos), this.start))) return -1 574 } else if (this.stage == RefStage.Link) { 575 let skip = skipSpace(content, this.pos), end = 0 576 if (skip > this.pos) { 577 let title = parseLinkTitle(content, skip, this.start) 578 if (title) { 579 let titleEnd = lineEnd(content, title.to - this.start) 580 if (titleEnd > 0) { this.nextStage(title); end = titleEnd } 581 } 582 } 583 if (!end) end = lineEnd(content, this.pos) 584 return end > 0 && end < content.length ? end : -1 585 } else { // RefStage.Title 586 return lineEnd(content, this.pos) 587 } 588 } 589 } 590} 591 592function lineEnd(text: string, pos: number) { 593 for (; pos < text.length; pos++) { 594 let next = text.charCodeAt(pos) 595 if (next == 10) break 596 if (!space(next)) return -1 597 } 598 return pos 599} 600 601class SetextHeadingParser implements LeafBlockParser { 602 nextLine(cx: BlockContext, line: Line, leaf: LeafBlock) { 603 let underline = line.depth < cx.stack.length ? -1 : isSetextUnderline(line) 604 let next = line.next 605 if (underline < 0) return false 606 let underlineMark = elt(Type.HeaderMark, cx.lineStart + line.pos, cx.lineStart + underline) 607 cx.nextLine() 608 cx.addLeafElement(leaf, elt(next == 61 ? Type.SetextHeading1 : Type.SetextHeading2, leaf.start, cx.prevLineEnd(), [ 609 ...cx.parser.parseInline(leaf.content, leaf.start), 610 underlineMark 611 ])) 612 return true 613 } 614 615 finish() { 616 return false 617 } 618} 619 620const DefaultLeafBlocks: {[name: string]: (cx: BlockContext, leaf: LeafBlock) => LeafBlockParser | null} = { 621 LinkReference(_, leaf) { return leaf.content.charCodeAt(0) == 91 /* '[' */ ? new LinkReferenceParser(leaf) : null }, 622 SetextHeading() { return new SetextHeadingParser } 623} 624 625const DefaultEndLeaf: readonly ((cx: BlockContext, line: Line) => boolean)[] = [ 626 (_, line) => isAtxHeading(line) >= 0, 627 (_, line) => isFencedCode(line) >= 0, 628 (_, line) => isBlockquote(line) >= 0, 629 (p, line) => isBulletList(line, p, true) >= 0, 630 (p, line) => isOrderedList(line, p, true) >= 0, 631 (p, line) => isHorizontalRule(line, p, true) >= 0, 632 (p, line) => isHTMLBlock(line, p, true) >= 0 633] 634 635const scanLineResult = {text: "", end: 0} 636 637/// Block-level parsing functions get access to this context object. 638export class BlockContext implements PartialParse { 639 /// @internal 640 block: CompositeBlock 641 /// @internal 642 stack: CompositeBlock[] 643 private line = new Line() 644 private atEnd = false 645 private fragments: FragmentCursor | null 646 private to: number 647 /// For reused nodes on gaps, we can't directly put the original 648 /// node into the tree, since that may be bigger than its parent. 649 /// When this happens, we create a dummy tree that is replaced by 650 /// the proper node in `injectGaps` @internal 651 reusePlaceholders: Map<Tree, Tree> = new Map 652 stoppedAt: number | null = null 653 654 /// The start of the current line. 655 lineStart: number 656 /// The absolute (non-gap-adjusted) position of the line @internal 657 absoluteLineStart: number 658 /// The range index that absoluteLineStart points into @internal 659 rangeI = 0 660 /// @internal 661 absoluteLineEnd: number 662 663 /// @internal 664 constructor( 665 /// The parser configuration used. 666 readonly parser: MarkdownParser, 667 /// @internal 668 readonly input: Input, 669 fragments: readonly TreeFragment[], 670 /// @internal 671 readonly ranges: readonly {from: number, to: number}[], 672 ) { 673 this.to = ranges[ranges.length - 1].to 674 this.lineStart = this.absoluteLineStart = this.absoluteLineEnd = ranges[0].from 675 this.block = CompositeBlock.create(Type.Document, 0, this.lineStart, 0, 0) 676 this.stack = [this.block] 677 this.fragments = fragments.length ? new FragmentCursor(fragments, input) : null 678 this.readLine() 679 } 680 681 get parsedPos() { 682 return this.absoluteLineStart 683 } 684 685 advance() { 686 if (this.stoppedAt != null && this.absoluteLineStart > this.stoppedAt) 687 return this.finish() 688 689 let {line} = this 690 for (;;) { 691 for (let markI = 0;;) { 692 let next = line.depth < this.stack.length ? this.stack[this.stack.length - 1] : null 693 while (markI < line.markers.length && (!next || line.markers[markI].from < next.end)) { 694 let mark = line.markers[markI++] 695 this.addNode(mark.type, mark.from, mark.to) 696 } 697 if (!next) break 698 this.finishContext() 699 } 700 if (line.pos < line.text.length) break 701 // Empty line 702 if (!this.nextLine()) return this.finish() 703 } 704 705 if (this.fragments && this.reuseFragment(line.basePos)) return null 706 707 start: for (;;) { 708 for (let type of this.parser.blockParsers) if (type) { 709 let result = type(this, line) 710 if (result != false) { 711 if (result == true) return null 712 line.forward() 713 continue start 714 } 715 } 716 break 717 } 718 719 let leaf = new LeafBlock(this.lineStart + line.pos, line.text.slice(line.pos)) 720 for (let parse of this.parser.leafBlockParsers) if (parse) { 721 let parser = parse!(this, leaf) 722 if (parser) leaf.parsers.push(parser!) 723 } 724 lines: while (this.nextLine()) { 725 if (line.pos == line.text.length) break 726 if (line.indent < line.baseIndent + 4) { 727 for (let stop of this.parser.endLeafBlock) if (stop(this, line, leaf)) break lines 728 } 729 for (let parser of leaf.parsers) if (parser.nextLine(this, line, leaf)) return null 730 leaf.content += "\n" + line.scrub() 731 for (let m of line.markers) leaf.marks.push(m) 732 } 733 this.finishLeaf(leaf) 734 return null 735 } 736 737 stopAt(pos: number) { 738 if (this.stoppedAt != null && this.stoppedAt < pos) throw new RangeError("Can't move stoppedAt forward") 739 this.stoppedAt = pos 740 } 741 742 private reuseFragment(start: number) { 743 if (!this.fragments!.moveTo(this.absoluteLineStart + start, this.absoluteLineStart) || 744 !this.fragments!.matches(this.block.hash)) return false 745 let taken = this.fragments!.takeNodes(this) 746 if (!taken) return false 747 this.absoluteLineStart += taken 748 this.lineStart = toRelative(this.absoluteLineStart, this.ranges) 749 this.moveRangeI() 750 if (this.absoluteLineStart < this.to) { 751 this.lineStart++ 752 this.absoluteLineStart++ 753 this.readLine() 754 } else { 755 this.atEnd = true 756 this.readLine() 757 } 758 return true 759 } 760 761 /// The number of parent blocks surrounding the current block. 762 get depth() { 763 return this.stack.length 764 } 765 766 /// Get the type of the parent block at the given depth. When no 767 /// depth is passed, return the type of the innermost parent. 768 parentType(depth = this.depth - 1) { 769 return this.parser.nodeSet.types[this.stack[depth].type] 770 } 771 772 /// Move to the next input line. This should only be called by 773 /// (non-composite) [block parsers](#BlockParser.parse) that consume 774 /// the line directly, or leaf block parser 775 /// [`nextLine`](#LeafBlockParser.nextLine) methods when they 776 /// consume the current line (and return true). 777 nextLine() { 778 this.lineStart += this.line.text.length 779 if (this.absoluteLineEnd >= this.to) { 780 this.absoluteLineStart = this.absoluteLineEnd 781 this.atEnd = true 782 this.readLine() 783 return false 784 } else { 785 this.lineStart++ 786 this.absoluteLineStart = this.absoluteLineEnd + 1 787 this.moveRangeI() 788 this.readLine() 789 return true 790 } 791 } 792 793 /// Retrieve the text of the line after the current one, without 794 /// actually moving the context's current line forward. 795 peekLine() { 796 return this.scanLine(this.absoluteLineEnd + 1).text 797 } 798 799 private moveRangeI() { 800 while (this.rangeI < this.ranges.length - 1 && this.absoluteLineStart >= this.ranges[this.rangeI].to) { 801 this.rangeI++ 802 this.absoluteLineStart = Math.max(this.absoluteLineStart, this.ranges[this.rangeI].from) 803 } 804 } 805 806 /// @internal 807 /// Collect the text for the next line. 808 scanLine(start: number) { 809 let r = scanLineResult 810 r.end = start 811 if (start >= this.to) { 812 r.text = "" 813 } else { 814 r.text = this.lineChunkAt(start) 815 r.end += r.text.length 816 if (this.ranges.length > 1) { 817 let textOffset = this.absoluteLineStart, rangeI = this.rangeI 818 while (this.ranges[rangeI].to < r.end) { 819 rangeI++ 820 let nextFrom = this.ranges[rangeI].from 821 let after = this.lineChunkAt(nextFrom) 822 r.end = nextFrom + after.length 823 r.text = r.text.slice(0, this.ranges[rangeI - 1].to - textOffset) + after 824 textOffset = r.end - r.text.length 825 } 826 } 827 } 828 return r 829 } 830 831 /// @internal 832 /// Populate this.line with the content of the next line. Skip 833 /// leading characters covered by composite blocks. 834 readLine() { 835 let {line} = this, {text, end} = this.scanLine(this.absoluteLineStart) 836 this.absoluteLineEnd = end 837 line.reset(text) 838 for (; line.depth < this.stack.length; line.depth++) { 839 let cx = this.stack[line.depth], handler = this.parser.skipContextMarkup[cx.type] 840 if (!handler) throw new Error("Unhandled block context " + Type[cx.type]) 841 let marks = this.line.markers.length 842 if (!handler(cx, this, line)) { 843 if (this.line.markers.length > marks) 844 cx.end = this.line.markers[this.line.markers.length - 1].to 845 line.forward() 846 break 847 } 848 line.forward() 849 } 850 } 851 852 private lineChunkAt(pos: number) { 853 let next = this.input.chunk(pos), text 854 if (!this.input.lineChunks) { 855 let eol = next.indexOf("\n") 856 text = eol < 0 ? next : next.slice(0, eol) 857 } else { 858 text = next == "\n" ? "" : next 859 } 860 return pos + text.length > this.to ? text.slice(0, this.to - pos) : text 861 } 862 863 /// The end position of the previous line. 864 prevLineEnd() { return this.atEnd ? this.lineStart : this.lineStart - 1 } 865 866 /// @internal 867 startContext(type: Type, start: number, value = 0) { 868 this.block = CompositeBlock.create(type, value, this.lineStart + start, this.block.hash, this.lineStart + this.line.text.length) 869 this.stack.push(this.block) 870 } 871 872 /// Start a composite block. Should only be called from [block 873 /// parser functions](#BlockParser.parse) that return null. 874 startComposite(type: string, start: number, value = 0) { 875 this.startContext(this.parser.getNodeType(type), start, value) 876 } 877 878 /// @internal 879 addNode(block: Type | Tree, from: number, to?: number) { 880 if (typeof block == "number") block = new Tree(this.parser.nodeSet.types[block], none, none, (to ?? this.prevLineEnd()) - from) 881 this.block.addChild(block, from - this.block.from) 882 } 883 884 /// Add a block element. Can be called by [block 885 /// parsers](#BlockParser.parse). 886 addElement(elt: Element) { 887 this.block.addChild(elt.toTree(this.parser.nodeSet), elt.from - this.block.from) 888 } 889 890 /// Add a block element from a [leaf parser](#LeafBlockParser). This 891 /// makes sure any extra composite block markup (such as blockquote 892 /// markers) inside the block are also added to the syntax tree. 893 addLeafElement(leaf: LeafBlock, elt: Element) { 894 this.addNode(this.buffer 895 .writeElements(injectMarks(elt.children, leaf.marks), -elt.from) 896 .finish(elt.type, elt.to - elt.from), elt.from) 897 } 898 899 /// @internal 900 finishContext() { 901 let cx = this.stack.pop()! 902 let top = this.stack[this.stack.length - 1] 903 top.addChild(cx.toTree(this.parser.nodeSet), cx.from - top.from) 904 this.block = top 905 } 906 907 private finish() { 908 while (this.stack.length > 1) this.finishContext() 909 return this.addGaps(this.block.toTree(this.parser.nodeSet, this.lineStart)) 910 } 911 912 private addGaps(tree: Tree) { 913 return this.ranges.length > 1 ? 914 injectGaps(this.ranges, 0, tree.topNode, this.ranges[0].from, this.reusePlaceholders) : tree 915 } 916 917 /// @internal 918 finishLeaf(leaf: LeafBlock) { 919 for (let parser of leaf.parsers) if (parser.finish(this, leaf)) return 920 let inline = injectMarks(this.parser.parseInline(leaf.content, leaf.start), leaf.marks) 921 this.addNode(this.buffer 922 .writeElements(inline, -leaf.start) 923 .finish(Type.Paragraph, leaf.content.length), leaf.start) 924 } 925 926 /// Create an [`Element`](#Element) object to represent some syntax 927 /// node. 928 elt(type: string, from: number, to: number, children?: readonly Element[]): Element 929 elt(tree: Tree, at: number): Element 930 elt(type: string | Tree, from: number, to?: number, children?: readonly Element[]): Element { 931 if (typeof type == "string") return elt(this.parser.getNodeType(type), from, to!, children) 932 return new TreeElement(type, from) 933 } 934 935 /// @internal 936 get buffer() { return new Buffer(this.parser.nodeSet) } 937} 938 939function injectGaps( 940 ranges: readonly {from: number, to: number}[], rangeI: number, 941 tree: SyntaxNode, offset: number, dummies: Map<Tree, Tree> 942): Tree { 943 let rangeEnd = ranges[rangeI].to 944 let children = [], positions = [], start = tree.from + offset 945 function movePastNext(upto: number, inclusive: boolean) { 946 while (inclusive ? upto >= rangeEnd : upto > rangeEnd) { 947 let size = ranges[rangeI + 1].from - rangeEnd 948 offset += size 949 upto += size 950 rangeI++ 951 rangeEnd = ranges[rangeI].to 952 } 953 } 954 for (let ch = tree.firstChild; ch; ch = ch.nextSibling) { 955 movePastNext(ch.from + offset, true) 956 let from = ch.from + offset, node, reuse = dummies.get(ch.tree!) 957 if (reuse) { 958 node = reuse 959 } else if (ch.to + offset > rangeEnd) { 960 node = injectGaps(ranges, rangeI, ch, offset, dummies) 961 movePastNext(ch.to + offset, false) 962 } else { 963 node = ch.toTree() 964 } 965 children.push(node) 966 positions.push(from - start) 967 } 968 movePastNext(tree.to + offset, false) 969 return new Tree(tree.type, children, positions, tree.to + offset - start, tree.tree ? tree.tree.propValues : undefined) 970} 971 972/// Used in the [configuration](#MarkdownConfig.defineNodes) to define 973/// new [syntax node 974/// types](https://lezer.codemirror.net/docs/ref/#common.NodeType). 975export interface NodeSpec { 976 /// The node's name. 977 name: string 978 /// Should be set to true if this type represents a block node. 979 block?: boolean 980 /// If this is a composite block, this should hold a function that, 981 /// at the start of a new line where that block is active, checks 982 /// whether the composite block should continue (return value) and 983 /// optionally [adjusts](#Line.moveBase) the line's base position 984 /// and [registers](#Line.addMarker) nodes for any markers involved 985 /// in the block's syntax. 986 composite?(cx: BlockContext, line: Line, value: number): boolean 987 /// Add highlighting tag information for this node. The value of 988 /// this property may either by a tag or array of tags to assign 989 /// directly to this node, or an object in the style of 990 /// [`styleTags`](https://lezer.codemirror.net/docs/ref/#highlight.styleTags)'s 991 /// argument to assign more complicated rules. 992 style?: Tag | readonly Tag[] | {[selector: string]: Tag | readonly Tag[]} 993} 994 995/// Inline parsers are called for every character of parts of the 996/// document that are parsed as inline content. 997export interface InlineParser { 998 /// This parser's name, which can be used by other parsers to 999 /// [indicate](#InlineParser.before) a relative precedence. 1000 name: string 1001 /// The parse function. Gets the next character and its position as 1002 /// arguments. Should return -1 if it doesn't handle the character, 1003 /// or add some [element](#InlineContext.addElement) or 1004 /// [delimiter](#InlineContext.addDelimiter) and return the end 1005 /// position of the content it parsed if it can. 1006 parse(cx: InlineContext, next: number, pos: number): number 1007 /// When given, this parser will be installed directly before the 1008 /// parser with the given name. The default configuration defines 1009 /// inline parsers with names Escape, Entity, InlineCode, HTMLTag, 1010 /// Emphasis, HardBreak, Link, and Image. When no `before` or 1011 /// `after` property is given, the parser is added to the end of the 1012 /// list. 1013 before?: string 1014 /// When given, the parser will be installed directly _after_ the 1015 /// parser with the given name. 1016 after?: string 1017} 1018 1019/// Block parsers handle block-level structure. There are three 1020/// general types of block parsers: 1021/// 1022/// - Composite block parsers, which handle things like lists and 1023/// blockquotes. These define a [`parse`](#BlockParser.parse) method 1024/// that [starts](#BlockContext.startComposite) a composite block 1025/// and returns null when it recognizes its syntax. The node type 1026/// used by such a block must define a 1027/// [`composite`](#NodeSpec.composite) function as well. 1028/// 1029/// - Eager leaf block parsers, used for things like code or HTML 1030/// blocks. These can unambiguously recognize their content from its 1031/// first line. They define a [`parse`](#BlockParser.parse) method 1032/// that, if it recognizes the construct, 1033/// [moves](#BlockContext.nextLine) the current line forward to the 1034/// line beyond the end of the block, 1035/// [add](#BlockContext.addElement) a syntax node for the block, and 1036/// return true. 1037/// 1038/// - Leaf block parsers that observe a paragraph-like construct as it 1039/// comes in, and optionally decide to handle it at some point. This 1040/// is used for "setext" (underlined) headings and link references. 1041/// These define a [`leaf`](#BlockParser.leaf) method that checks 1042/// the first line of the block and returns a 1043/// [`LeafBlockParser`](#LeafBlockParser) object if it wants to 1044/// observe that block. 1045export interface BlockParser { 1046 /// The name of the parser. Can be used by other block parsers to 1047 /// [specify](#BlockParser.before) precedence. 1048 name: string 1049 /// The eager parse function, which can look at the block's first 1050 /// line and return `false` to do nothing, `true` if it has parsed 1051 /// (and [moved past](#BlockContext.nextLine) a block), or `null` if 1052 /// it has [started](#BlockContext.startComposite) a composite block. 1053 parse?(cx: BlockContext, line: Line): BlockResult 1054 /// A leaf parse function. If no [regular](#BlockParser.parse) parse 1055 /// functions match for a given line, its content will be 1056 /// accumulated for a paragraph-style block. This method can return 1057 /// an [object](#LeafBlockParser) that overrides that style of 1058 /// parsing in some situations. 1059 leaf?(cx: BlockContext, leaf: LeafBlock): LeafBlockParser | null 1060 /// Some constructs, such as code blocks or newly started 1061 /// blockquotes, can interrupt paragraphs even without a blank line. 1062 /// If your construct can do this, provide a predicate here that 1063 /// recognizes lines that should end a paragraph (or other non-eager 1064 /// [leaf block](#BlockParser.leaf)). 1065 endLeaf?(cx: BlockContext, line: Line, leaf: LeafBlock): boolean 1066 /// When given, this parser will be installed directly before the 1067 /// block parser with the given name. The default configuration 1068 /// defines block parsers with names LinkReference, IndentedCode, 1069 /// FencedCode, Blockquote, HorizontalRule, BulletList, OrderedList, 1070 /// ATXHeading, HTMLBlock, and SetextHeading. 1071 before?: string 1072 /// When given, the parser will be installed directly _after_ the 1073 /// parser with the given name. 1074 after?: string 1075} 1076 1077/// Objects that are used to [override](#BlockParser.leaf) 1078/// paragraph-style blocks should conform to this interface. 1079export interface LeafBlockParser { 1080 /// Update the parser's state for the next line, and optionally 1081 /// finish the block. This is not called for the first line (the 1082 /// object is constructed at that line), but for any further lines. 1083 /// When it returns `true`, the block is finished. It is okay for 1084 /// the function to [consume](#BlockContext.nextLine) the current 1085 /// line or any subsequent lines when returning true. 1086 nextLine(cx: BlockContext, line: Line, leaf: LeafBlock): boolean 1087 /// Called when the block is finished by external circumstances 1088 /// (such as a blank line or the [start](#BlockParser.endLeaf) of 1089 /// another construct). If this parser can handle the block up to 1090 /// its current position, it should 1091 /// [finish](#BlockContext.addLeafElement) the block and return 1092 /// true. 1093 finish(cx: BlockContext, leaf: LeafBlock): boolean 1094} 1095 1096/// Objects of this type are used to 1097/// [configure](#MarkdownParser.configure) the Markdown parser. 1098export interface MarkdownConfig { 1099 /// Node props to add to the parser's node set. 1100 props?: readonly NodePropSource[] 1101 /// Define new [node types](#NodeSpec) for use in parser extensions. 1102 defineNodes?: readonly (string | NodeSpec)[] 1103 /// Define additional [block parsing](#BlockParser) logic. 1104 parseBlock?: readonly BlockParser[] 1105 /// Define new [inline parsing](#InlineParser) logic. 1106 parseInline?: readonly InlineParser[] 1107 /// Remove the named parsers from the configuration. 1108 remove?: readonly string[] 1109 /// Add a parse wrapper (such as a [mixed-language 1110 /// parser](#common.parseMixed)) to this parser. 1111 wrap?: ParseWrapper 1112} 1113 1114/// To make it possible to group extensions together into bigger 1115/// extensions (such as the [Github-flavored Markdown](#GFM) 1116/// extension), [reconfiguration](#MarkdownParser.configure) accepts 1117/// nested arrays of [config](#MarkdownConfig) objects. 1118export type MarkdownExtension = MarkdownConfig | readonly MarkdownExtension[] 1119 1120/// A Markdown parser configuration. 1121export class MarkdownParser extends Parser { 1122 /// @internal 1123 nodeTypes: {[name: string]: number} = Object.create(null) 1124 1125 /// @internal 1126 constructor( 1127 /// The parser's syntax [node 1128 /// types](https://lezer.codemirror.net/docs/ref/#common.NodeSet). 1129 readonly nodeSet: NodeSet, 1130 /// @internal 1131 readonly blockParsers: readonly (((cx: BlockContext, line: Line) => BlockResult) | undefined)[], 1132 /// @internal 1133 readonly leafBlockParsers: readonly (((cx: BlockContext, leaf: LeafBlock) => LeafBlockParser | null) | undefined)[], 1134 /// @internal 1135 readonly blockNames: readonly string[], 1136 /// @internal 1137 readonly endLeafBlock: readonly ((cx: BlockContext, line: Line, leaf: LeafBlock) => boolean)[], 1138 /// @internal 1139 readonly skipContextMarkup: {readonly [type: number]: (bl: CompositeBlock, cx: BlockContext, line: Line) => boolean}, 1140 /// @internal 1141 readonly inlineParsers: readonly (((cx: InlineContext, next: number, pos: number) => number) | undefined)[], 1142 /// @internal 1143 readonly inlineNames: readonly string[], 1144 /// @internal 1145 readonly wrappers: readonly ParseWrapper[] 1146 ) { 1147 super() 1148 for (let t of nodeSet.types) this.nodeTypes[t.name] = t.id 1149 } 1150 1151 createParse(input: Input, fragments: readonly TreeFragment[], ranges: readonly {from: number, to: number}[]): PartialParse { 1152 let parse: PartialParse = new BlockContext(this, input, fragments, ranges) 1153 for (let w of this.wrappers) parse = w(parse, input, fragments, ranges) 1154 return parse 1155 } 1156 1157 /// Reconfigure the parser. 1158 configure(spec: MarkdownExtension) { 1159 let config = resolveConfig(spec) 1160 if (!config) return this 1161 let {nodeSet, skipContextMarkup} = this 1162 let blockParsers = this.blockParsers.slice(), leafBlockParsers = this.leafBlockParsers.slice(), 1163 blockNames = this.blockNames.slice(), inlineParsers = this.inlineParsers.slice(), 1164 inlineNames = this.inlineNames.slice(), endLeafBlock = this.endLeafBlock.slice(), 1165 wrappers = this.wrappers 1166 1167 if (nonEmpty(config.defineNodes)) { 1168 skipContextMarkup = Object.assign({}, skipContextMarkup) 1169 let nodeTypes = nodeSet.types.slice(), styles: {[selector: string]: Tag | readonly Tag[]} | undefined 1170 for (let s of config.defineNodes) { 1171 let {name, block, composite, style} = typeof s == "string" ? {name: s} as NodeSpec : s 1172 if (nodeTypes.some(t => t.name == name)) continue 1173 if (composite) (skipContextMarkup as any)[nodeTypes.length] = 1174 (bl: CompositeBlock, cx: BlockContext, line: Line) => composite!(cx, line, bl.value) 1175 let id = nodeTypes.length 1176 let group = composite ? ["Block", "BlockContext"] : !block ? undefined 1177 : id >= Type.ATXHeading1 && id <= Type.SetextHeading2 ? ["Block", "LeafBlock", "Heading"] : ["Block", "LeafBlock"] 1178 nodeTypes.push(NodeType.define({ 1179 id, 1180 name, 1181 props: group && [[NodeProp.group, group]] 1182 })) 1183 if (style) { 1184 if (!styles) styles = {} 1185 if (Array.isArray(style) || style instanceof Tag) styles[name] = style 1186 else Object.assign(styles, style) 1187 } 1188 } 1189 nodeSet = new NodeSet(nodeTypes) 1190 if (styles) nodeSet = nodeSet.extend(styleTags(styles)) 1191 } 1192 1193 if (nonEmpty(config.props)) nodeSet = nodeSet.extend(...config.props) 1194 1195 if (nonEmpty(config.remove)) { 1196 for (let rm of config.remove) { 1197 let block = this.blockNames.indexOf(rm), inline = this.inlineNames.indexOf(rm) 1198 if (block > -1) blockParsers[block] = leafBlockParsers[block] = undefined 1199 if (inline > -1) inlineParsers[inline] = undefined 1200 } 1201 } 1202 1203 if (nonEmpty(config.parseBlock)) { 1204 for (let spec of config.parseBlock) { 1205 let found = blockNames.indexOf(spec.name) 1206 if (found > -1) { 1207 blockParsers[found] = spec.parse 1208 leafBlockParsers[found] = spec.leaf 1209 } else { 1210 let pos = spec.before ? findName(blockNames, spec.before) 1211 : spec.after ? findName(blockNames, spec.after) + 1 : blockNames.length - 1 1212 blockParsers.splice(pos, 0, spec.parse) 1213 leafBlockParsers.splice(pos, 0, spec.leaf) 1214 blockNames.splice(pos, 0, spec.name) 1215 } 1216 if (spec.endLeaf) endLeafBlock.push(spec.endLeaf) 1217 } 1218 } 1219 1220 if (nonEmpty(config.parseInline)) { 1221 for (let spec of config.parseInline) { 1222 let found = inlineNames.indexOf(spec.name) 1223 if (found > -1) { 1224 inlineParsers[found] = spec.parse 1225 } else { 1226 let pos = spec.before ? findName(inlineNames, spec.before) 1227 : spec.after ? findName(inlineNames, spec.after) + 1 : inlineNames.length - 1 1228 inlineParsers.splice(pos, 0, spec.parse) 1229 inlineNames.splice(pos, 0, spec.name) 1230 } 1231 } 1232 } 1233 1234 if (config.wrap) wrappers = wrappers.concat(config.wrap) 1235 1236 return new MarkdownParser(nodeSet, 1237 blockParsers, leafBlockParsers, blockNames, 1238 endLeafBlock, skipContextMarkup, 1239 inlineParsers, inlineNames, wrappers) 1240 } 1241 1242 /// @internal 1243 getNodeType(name: string) { 1244 let found = this.nodeTypes[name] 1245 if (found == null) throw new RangeError(`Unknown node type '${name}'`) 1246 return found 1247 } 1248 1249 /// Parse the given piece of inline text at the given offset, 1250 /// returning an array of [`Element`](#Element) objects representing 1251 /// the inline content. 1252 parseInline(text: string, offset: number) { 1253 let cx = new InlineContext(this, text, offset) 1254 outer: for (let pos = offset; pos < cx.end;) { 1255 let next = cx.char(pos) 1256 for (let token of this.inlineParsers) if (token) { 1257 let result = token(cx, next, pos) 1258 if (result >= 0) { pos = result; continue outer } 1259 } 1260 pos++ 1261 } 1262 return cx.resolveMarkers(0) 1263 } 1264} 1265 1266function nonEmpty<T>(a: undefined | readonly T[]): a is readonly T[] { 1267 return a != null && a.length > 0 1268} 1269 1270function resolveConfig(spec: MarkdownExtension): MarkdownConfig | null { 1271 if (!Array.isArray(spec)) return spec as MarkdownConfig 1272 if (spec.length == 0) return null 1273 let conf = resolveConfig(spec[0]) 1274 if (spec.length == 1) return conf 1275 let rest = resolveConfig(spec.slice(1)) 1276 if (!rest || !conf) return conf || rest 1277 let conc: <T>(a: readonly T[] | undefined, b: readonly T[] | undefined) => readonly T[] = 1278 (a, b) => (a || none).concat(b || none) 1279 let wrapA = conf.wrap, wrapB = rest.wrap 1280 return { 1281 props: conc(conf.props, rest.props), 1282 defineNodes: conc(conf.defineNodes, rest.defineNodes), 1283 parseBlock: conc(conf.parseBlock, rest.parseBlock), 1284 parseInline: conc(conf.parseInline, rest.parseInline), 1285 remove: conc(conf.remove, rest.remove), 1286 wrap: !wrapA ? wrapB : !wrapB ? wrapA : 1287 (inner, input, fragments, ranges) => wrapA!(wrapB!(inner, input, fragments, ranges), input, fragments, ranges) 1288 } 1289} 1290 1291function findName(names: readonly string[], name: string) { 1292 let found = names.indexOf(name) 1293 if (found < 0) throw new RangeError(`Position specified relative to unknown parser ${name}`) 1294 return found 1295} 1296 1297let nodeTypes = [NodeType.none] 1298for (let i = 1, name; name = Type[i]; i++) { 1299 nodeTypes[i] = NodeType.define({ 1300 id: i, 1301 name, 1302 props: i >= Type.Escape ? [] : [[NodeProp.group, i in DefaultSkipMarkup ? ["Block", "BlockContext"] : ["Block", "LeafBlock"]]], 1303 top: name == "Document" 1304 }) 1305} 1306 1307const none: readonly any[] = [] 1308 1309class Buffer { 1310 content: number[] = [] 1311 nodes: Tree[] = [] 1312 constructor(readonly nodeSet: NodeSet) {} 1313 1314 write(type: Type, from: number, to: number, children = 0) { 1315 this.content.push(type, from, to, 4 + children * 4) 1316 return this 1317 } 1318 1319 writeElements(elts: readonly (Element | TreeElement)[], offset = 0) { 1320 for (let e of elts) e.writeTo(this, offset) 1321 return this 1322 } 1323 1324 finish(type: Type, length: number) { 1325 return Tree.build({ 1326 buffer: this.content, 1327 nodeSet: this.nodeSet, 1328 reused: this.nodes, 1329 topID: type, 1330 length 1331 }) 1332 } 1333} 1334 1335/// Elements are used to compose syntax nodes during parsing. 1336export class Element { 1337 /// @internal 1338 constructor( 1339 /// The node's 1340 /// [id](https://lezer.codemirror.net/docs/ref/#common.NodeType.id). 1341 readonly type: number, 1342 /// The start of the node, as an offset from the start of the document. 1343 readonly from: number, 1344 /// The end of the node. 1345 readonly to: number, 1346 /// The node's child nodes @internal 1347 readonly children: readonly (Element | TreeElement)[] = none 1348 ) {} 1349 1350 /// @internal 1351 writeTo(buf: Buffer, offset: number) { 1352 let startOff = buf.content.length 1353 buf.writeElements(this.children, offset) 1354 buf.content.push(this.type, this.from + offset, this.to + offset, buf.content.length + 4 - startOff) 1355 } 1356 1357 /// @internal 1358 toTree(nodeSet: NodeSet): Tree { 1359 return new Buffer(nodeSet).writeElements(this.children, -this.from).finish(this.type, this.to - this.from) 1360 } 1361} 1362 1363class TreeElement { 1364 constructor(readonly tree: Tree, readonly from: number) {} 1365 1366 get to() { return this.from + this.tree.length } 1367 1368 get type() { return this.tree.type.id } 1369 1370 get children() { return none } 1371 1372 writeTo(buf: Buffer, offset: number) { 1373 buf.nodes.push(this.tree) 1374 buf.content.push(buf.nodes.length - 1, this.from + offset, this.to + offset, -1) 1375 } 1376 1377 toTree(): Tree { return this.tree } 1378} 1379 1380function elt(type: Type, from: number, to: number, children?: readonly (Element | TreeElement)[]) { 1381 return new Element(type, from, to, children) 1382} 1383 1384const enum Mark { None = 0, Open = 1, Close = 2 } 1385 1386/// Delimiters are used during inline parsing to store the positions 1387/// of things that _might_ be delimiters, if another matching 1388/// delimiter is found. They are identified by objects with these 1389/// properties. 1390export interface DelimiterType { 1391 /// If this is given, the delimiter should be matched automatically 1392 /// when a piece of inline content is finished. Such delimiters will 1393 /// be matched with delimiters of the same type according to their 1394 /// [open and close](#InlineContext.addDelimiter) properties. When a 1395 /// match is found, the content between the delimiters is wrapped in 1396 /// a node whose name is given by the value of this property. 1397 /// 1398 /// When this isn't given, you need to match the delimiter eagerly 1399 /// using the [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter) 1400 /// and [`takeContent`](#InlineContext.takeContent) methods. 1401 resolve?: string 1402 /// If the delimiter itself should, when matched, create a syntax 1403 /// node, set this to the name of the syntax node. 1404 mark?: string 1405} 1406 1407const EmphasisUnderscore: DelimiterType = {resolve: "Emphasis", mark: "EmphasisMark"} 1408const EmphasisAsterisk: DelimiterType = {resolve: "Emphasis", mark: "EmphasisMark"} 1409const LinkStart: DelimiterType = {}, ImageStart: DelimiterType = {} 1410 1411class InlineDelimiter { 1412 constructor(readonly type: DelimiterType, 1413 readonly from: number, 1414 readonly to: number, 1415 public side: Mark) {} 1416} 1417 1418const Escapable = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" 1419 1420export let Punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\u2010-\u2027]/ 1421try { Punctuation = new RegExp("[\\p{S}|\\p{P}]", "u") } catch (_) {} 1422 1423const DefaultInline: {[name: string]: (cx: InlineContext, next: number, pos: number) => number} = { 1424 Escape(cx, next, start) { 1425 if (next != 92 /* '\\' */ || start == cx.end - 1) return -1 1426 let escaped = cx.char(start + 1) 1427 for (let i = 0; i < Escapable.length; i++) if (Escapable.charCodeAt(i) == escaped) 1428 return cx.append(elt(Type.Escape, start, start + 2)) 1429 return -1 1430 }, 1431 1432 Entity(cx, next, start) { 1433 if (next != 38 /* '&' */) return -1 1434 let m = /^(?:#\d+|#x[a-f\d]+|\w+);/i.exec(cx.slice(start + 1, start + 31)) 1435 return m ? cx.append(elt(Type.Entity, start, start + 1 + m[0].length)) : -1 1436 }, 1437 1438 InlineCode(cx, next, start) { 1439 if (next != 96 /* '`' */ || start && cx.char(start - 1) == 96) return -1 1440 let pos = start + 1 1441 while (pos < cx.end && cx.char(pos) == 96) pos++ 1442 let size = pos - start, curSize = 0 1443 for (; pos < cx.end; pos++) { 1444 if (cx.char(pos) == 96) { 1445 curSize++ 1446 if (curSize == size && cx.char(pos + 1) != 96) 1447 return cx.append(elt(Type.InlineCode, start, pos + 1, [ 1448 elt(Type.CodeMark, start, start + size), 1449 elt(Type.CodeMark, pos + 1 - size, pos + 1) 1450 ])) 1451 } else { 1452 curSize = 0 1453 } 1454 } 1455 return -1 1456 }, 1457 1458 HTMLTag(cx, next, start) { // or URL 1459 if (next != 60 /* '<' */ || start == cx.end - 1) return -1 1460 let after = cx.slice(start + 1, cx.end) 1461 let url = /^(?:[a-z][-\w+.]+:[^\s>]+|[a-z\d.!#$%&'*+/=?^_`{|}~-]+@[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*)>/i.exec(after) 1462 if (url) { 1463 return cx.append(elt(Type.Autolink, start, start + 1 + url[0].length, [ 1464 elt(Type.LinkMark, start, start + 1), 1465 // url[0] includes the closing bracket, so exclude it from this slice 1466 elt(Type.URL, start + 1, start + url[0].length), 1467 elt(Type.LinkMark, start + url[0].length, start + 1 + url[0].length) 1468 ])) 1469 } 1470 let comment = /^!--[^>](?:-[^-]|[^-])*?-->/i.exec(after) 1471 if (comment) return cx.append(elt(Type.Comment, start, start + 1 + comment[0].length)) 1472 let procInst = /^\?[^]*?\?>/.exec(after) 1473 if (procInst) return cx.append(elt(Type.ProcessingInstruction, start, start + 1 + procInst[0].length)) 1474 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) 1475 if (!m) return -1 1476 return cx.append(elt(Type.HTMLTag, start, start + 1 + m[0].length)) 1477 }, 1478 1479 Emphasis(cx, next, start) { 1480 if (next != 95 && next != 42) return -1 1481 let pos = start + 1 1482 while (cx.char(pos) == next) pos++ 1483 let before = cx.slice(start - 1, start), after = cx.slice(pos, pos + 1) 1484 let pBefore = Punctuation.test(before), pAfter = Punctuation.test(after) 1485 let sBefore = /\s|^$/.test(before), sAfter = /\s|^$/.test(after) 1486 let leftFlanking = !sAfter && (!pAfter || sBefore || pBefore) 1487 let rightFlanking = !sBefore && (!pBefore || sAfter || pAfter) 1488 let canOpen = leftFlanking && (next == 42 || !rightFlanking || pBefore) 1489 let canClose = rightFlanking && (next == 42 || !leftFlanking || pAfter) 1490 return cx.append(new InlineDelimiter(next == 95 ? EmphasisUnderscore : EmphasisAsterisk, start, pos, 1491 (canOpen ? Mark.Open : Mark.None) | (canClose ? Mark.Close : Mark.None))) 1492 }, 1493 1494 HardBreak(cx, next, start) { 1495 if (next == 92 /* '\\' */ && cx.char(start + 1) == 10 /* '\n' */) 1496 return cx.append(elt(Type.HardBreak, start, start + 2)) 1497 if (next == 32) { 1498 let pos = start + 1 1499 while (cx.char(pos) == 32) pos++ 1500 if (cx.char(pos) == 10 && pos >= start + 2) 1501 return cx.append(elt(Type.HardBreak, start, pos + 1)) 1502 } 1503 return -1 1504 }, 1505 1506 Link(cx, next, start) { 1507 return next == 91 /* '[' */ ? cx.append(new InlineDelimiter(LinkStart, start, start + 1, Mark.Open)) : -1 1508 }, 1509 1510 Image(cx, next, start) { 1511 return next == 33 /* '!' */ && cx.char(start + 1) == 91 /* '[' */ 1512 ? cx.append(new InlineDelimiter(ImageStart, start, start + 2, Mark.Open)) : -1 1513 }, 1514 1515 LinkEnd(cx, next, start) { 1516 if (next != 93 /* ']' */) return -1 1517 // Scanning back to the next link/image start marker 1518 for (let i = cx.parts.length - 1; i >= 0; i--) { 1519 let part = cx.parts[i] 1520 if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) { 1521 // If this one has been set invalid (because it would produce 1522 // a nested link) or there's no valid link here ignore both. 1523 if (!part.side || cx.skipSpace(part.to) == start && !/[(\[]/.test(cx.slice(start + 1, start + 2))) { 1524 cx.parts[i] = null 1525 return -1 1526 } 1527 // Finish the content and replace the entire range in 1528 // this.parts with the link/image node. 1529 let content = cx.takeContent(i) 1530 let link = cx.parts[i] = finishLink(cx, content, part.type == LinkStart ? Type.Link : Type.Image, part.from, start + 1) 1531 // Set any open-link markers before this link to invalid. 1532 if (part.type == LinkStart) for (let j = 0; j < i; j++) { 1533 let p = cx.parts[j] 1534 if (p instanceof InlineDelimiter && p.type == LinkStart) p.side = Mark.None 1535 } 1536 return link.to 1537 } 1538 } 1539 return -1 1540 } 1541} 1542 1543function finishLink(cx: InlineContext, content: Element[], type: Type, start: number, startPos: number) { 1544 let {text} = cx, next = cx.char(startPos), endPos = startPos 1545 content.unshift(elt(Type.LinkMark, start, start + (type == Type.Image ? 2 : 1))) 1546 content.push(elt(Type.LinkMark, startPos - 1, startPos)) 1547 if (next == 40 /* '(' */) { 1548 let pos = cx.skipSpace(startPos + 1) 1549 let dest = parseURL(text, pos - cx.offset, cx.offset), title 1550 if (dest) { 1551 pos = cx.skipSpace(dest.to) 1552 // The destination and title must be separated by whitespace 1553 if (pos != dest.to) { 1554 title = parseLinkTitle(text, pos - cx.offset, cx.offset) 1555 if (title) pos = cx.skipSpace(title.to) 1556 } 1557 } 1558 if (cx.char(pos) == 41 /* ')' */) { 1559 content.push(elt(Type.LinkMark, startPos, startPos + 1)) 1560 endPos = pos + 1 1561 if (dest) content.push(dest) 1562 if (title) content.push(title) 1563 content.push(elt(Type.LinkMark, pos, endPos)) 1564 } 1565 } else if (next == 91 /* '[' */) { 1566 let label = parseLinkLabel(text, startPos - cx.offset, cx.offset, false) 1567 if (label) { 1568 content.push(label) 1569 endPos = label.to 1570 } 1571 } 1572 return elt(type, start, endPos, content) 1573} 1574 1575// These return `null` when falling off the end of the input, `false` 1576// when parsing fails otherwise (for use in the incremental link 1577// reference parser). 1578 1579function parseURL(text: string, start: number, offset: number): null | false | Element { 1580 let next = text.charCodeAt(start) 1581 if (next == 60 /* '<' */) { 1582 for (let pos = start + 1; pos < text.length; pos++) { 1583 let ch = text.charCodeAt(pos) 1584 if (ch == 62 /* '>' */) return elt(Type.URL, start + offset, pos + 1 + offset) 1585 if (ch == 60 || ch == 10 /* '<\n' */) return false 1586 } 1587 return null 1588 } else { 1589 let depth = 0, pos = start 1590 for (let escaped = false; pos < text.length; pos++) { 1591 let ch = text.charCodeAt(pos) 1592 if (space(ch)) { 1593 break 1594 } else if (escaped) { 1595 escaped = false 1596 } else if (ch == 40 /* '(' */) { 1597 depth++ 1598 } else if (ch == 41 /* ')' */) { 1599 if (!depth) break 1600 depth-- 1601 } else if (ch == 92 /* '\\' */) { 1602 escaped = true 1603 } 1604 } 1605 return pos > start ? elt(Type.URL, start + offset, pos + offset) : pos == text.length ? null : false 1606 } 1607} 1608 1609function parseLinkTitle(text: string, start: number, offset: number): null | false | Element { 1610 let next = text.charCodeAt(start) 1611 if (next != 39 && next != 34 && next != 40 /* '"\'(' */) return false 1612 let end = next == 40 ? 41 : next 1613 for (let pos = start + 1, escaped = false; pos < text.length; pos++) { 1614 let ch = text.charCodeAt(pos) 1615 if (escaped) escaped = false 1616 else if (ch == end) return elt(Type.LinkTitle, start + offset, pos + 1 + offset) 1617 else if (ch == 92 /* '\\' */) escaped = true 1618 } 1619 return null 1620} 1621 1622function parseLinkLabel(text: string, start: number, offset: number, requireNonWS: boolean): null | false | Element { 1623 for (let escaped = false, pos = start + 1, end = Math.min(text.length, pos + 999); pos < end; pos++) { 1624 let ch = text.charCodeAt(pos) 1625 if (escaped) escaped = false 1626 else if (ch == 93 /* ']' */) return requireNonWS ? false : elt(Type.LinkLabel, start + offset, pos + 1 + offset) 1627 else { 1628 if (requireNonWS && !space(ch)) requireNonWS = false 1629 if (ch == 91 /* '[' */) return false 1630 else if (ch == 92 /* '\\' */) escaped = true 1631 } 1632 } 1633 return null 1634} 1635 1636/// Inline parsing functions get access to this context, and use it to 1637/// read the content and emit syntax nodes. 1638export class InlineContext { 1639 /// @internal 1640 parts: (Element | InlineDelimiter | null)[] = [] 1641 1642 /// @internal 1643 constructor( 1644 /// The parser that is being used. 1645 readonly parser: MarkdownParser, 1646 /// The text of this inline section. 1647 readonly text: string, 1648 /// The starting offset of the section in the document. 1649 readonly offset: number 1650 ) {} 1651 1652 /// Get the character code at the given (document-relative) 1653 /// position. 1654 char(pos: number) { return pos >= this.end ? -1 : this.text.charCodeAt(pos - this.offset) } 1655 1656 /// The position of the end of this inline section. 1657 get end() { return this.offset + this.text.length } 1658 1659 /// Get a substring of this inline section. Again uses 1660 /// document-relative positions. 1661 slice(from: number, to: number) { return this.text.slice(from - this.offset, to - this.offset) } 1662 1663 /// @internal 1664 append(elt: Element | InlineDelimiter) { 1665 this.parts.push(elt) 1666 return elt.to 1667 } 1668 1669 /// Add a [delimiter](#DelimiterType) at this given position. `open` 1670 /// and `close` indicate whether this delimiter is opening, closing, 1671 /// or both. Returns the end of the delimiter, for convenient 1672 /// returning from [parse functions](#InlineParser.parse). 1673 addDelimiter(type: DelimiterType, from: number, to: number, open: boolean, close: boolean) { 1674 return this.append(new InlineDelimiter(type, from, to, (open ? Mark.Open : Mark.None) | (close ? Mark.Close : Mark.None))) 1675 } 1676 1677 /// Returns true when there is an unmatched link or image opening 1678 /// token before the current position. 1679 get hasOpenLink() { 1680 for (let i = this.parts.length - 1; i >= 0; i--) { 1681 let part = this.parts[i] 1682 if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) return true 1683 } 1684 return false 1685 } 1686 1687 /// Add an inline element. Returns the end of the element. 1688 addElement(elt: Element) { 1689 return this.append(elt) 1690 } 1691 1692 /// Resolve markers between this.parts.length and from, wrapping matched markers in the 1693 /// appropriate node and updating the content of this.parts. @internal 1694 resolveMarkers(from: number) { 1695 // Scan forward, looking for closing tokens 1696 for (let i = from; i < this.parts.length; i++) { 1697 let close = this.parts[i] 1698 if (!(close instanceof InlineDelimiter && close.type.resolve && (close.side & Mark.Close))) continue 1699 1700 let emp = close.type == EmphasisUnderscore || close.type == EmphasisAsterisk 1701 let closeSize = close.to - close.from 1702 let open: InlineDelimiter | undefined, j = i - 1 1703 // Continue scanning for a matching opening token 1704 for (; j >= from; j--) { 1705 let part = this.parts[j] 1706 if (part instanceof InlineDelimiter && (part.side & Mark.Open) && part.type == close.type && 1707 // Ignore emphasis delimiters where the character count doesn't match 1708 !(emp && ((close.side & Mark.Open) || (part.side & Mark.Close)) && 1709 (part.to - part.from + closeSize) % 3 == 0 && ((part.to - part.from) % 3 || closeSize % 3))) { 1710 open = part 1711 break 1712 } 1713 } 1714 if (!open) continue 1715 1716 let type = close.type.resolve, content = [] 1717 let start = open.from, end = close.to 1718 // Emphasis marker effect depends on the character count. Size consumed is minimum of the two 1719 // markers. 1720 if (emp) { 1721 let size = Math.min(2, open.to - open.from, closeSize) 1722 start = open.to - size 1723 end = close.from + size 1724 type = size == 1 ? "Emphasis" : "StrongEmphasis" 1725 } 1726 // Move the covered region into content, optionally adding marker nodes 1727 if (open.type.mark) content.push(this.elt(open.type.mark, start, open.to)) 1728 for (let k = j + 1; k < i; k++) { 1729 if (this.parts[k] instanceof Element) content.push(this.parts[k] as Element) 1730 this.parts[k] = null 1731 } 1732 if (close.type.mark) content.push(this.elt(close.type.mark, close.from, end)) 1733 let element = this.elt(type, start, end, content) 1734 // If there are leftover emphasis marker characters, shrink the close/open markers. Otherwise, clear them. 1735 this.parts[j] = emp && open.from != start ? new InlineDelimiter(open.type, open.from, start, open.side) : null 1736 let keep = this.parts[i] = emp && close.to != end ? new InlineDelimiter(close.type, end, close.to, close.side) : null 1737 // Insert the new element in this.parts 1738 if (keep) this.parts.splice(i, 0, element) 1739 else this.parts[i] = element 1740 } 1741 1742 // Collect the elements remaining in this.parts into an array. 1743 let result = [] 1744 for (let i = from; i < this.parts.length; i++) { 1745 let part = this.parts[i] 1746 if (part instanceof Element) result.push(part) 1747 } 1748 return result 1749 } 1750 1751 /// Find an opening delimiter of the given type. Returns `null` if 1752 /// no delimiter is found, or an index that can be passed to 1753 /// [`takeContent`](#InlineContext.takeContent) otherwise. 1754 findOpeningDelimiter(type: DelimiterType) { 1755 for (let i = this.parts.length - 1; i >= 0; i--) { 1756 let part = this.parts[i] 1757 if (part instanceof InlineDelimiter && part.type == type && (part.side & Mark.Open)) return i 1758 } 1759 return null 1760 } 1761 1762 /// Remove all inline elements and delimiters starting from the 1763 /// given index (which you should get from 1764 /// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter), 1765 /// resolve delimiters inside of them, and return them as an array 1766 /// of elements. 1767 takeContent(startIndex: number) { 1768 let content = this.resolveMarkers(startIndex) 1769 this.parts.length = startIndex 1770 return content 1771 } 1772 1773 /// Return the delimiter at the given index. Mostly useful to get 1774 /// additional info out of a delimiter index returned by 1775 /// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter). 1776 /// Returns null if there is no delimiter at this index. 1777 getDelimiterAt(index: number): {from: number, to: number, type: DelimiterType} | null { 1778 let part = this.parts[index] 1779 return part instanceof InlineDelimiter ? part : null 1780 } 1781 1782 /// Skip space after the given (document) position, returning either 1783 /// the position of the next non-space character or the end of the 1784 /// section. 1785 skipSpace(from: number) { return skipSpace(this.text, from - this.offset) + this.offset } 1786 1787 /// Create an [`Element`](#Element) for a syntax node. 1788 elt(type: string, from: number, to: number, children?: readonly Element[]): Element 1789 elt(tree: Tree, at: number): Element 1790 elt(type: string | Tree, from: number, to?: number, children?: readonly Element[]): Element { 1791 if (typeof type == "string") return elt(this.parser.getNodeType(type), from, to!, children) 1792 return new TreeElement(type, from) 1793 } 1794 1795 /// The opening delimiter type used by the standard link parser. 1796 static linkStart = LinkStart 1797 1798 /// Opening delimiter type used for standard images. 1799 static imageStart = ImageStart 1800} 1801 1802function injectMarks(elements: readonly (Element | TreeElement)[], marks: Element[]) { 1803 if (!marks.length) return elements 1804 if (!elements.length) return marks 1805 let elts = elements.slice(), eI = 0 1806 for (let mark of marks) { 1807 while (eI < elts.length && elts[eI].to < mark.to) eI++ 1808 if (eI < elts.length && elts[eI].from < mark.from) { 1809 let e = elts[eI] 1810 if (e instanceof Element) 1811 elts[eI] = new Element(e.type, e.from, e.to, injectMarks(e.children, [mark])) 1812 } else { 1813 elts.splice(eI++, 0, mark) 1814 } 1815 } 1816 return elts 1817} 1818 1819// These are blocks that can span blank lines, and should thus only be 1820// reused if their next sibling is also being reused. 1821const NotLast = [Type.CodeBlock, Type.ListItem, Type.OrderedList, Type.BulletList] 1822 1823class FragmentCursor { 1824 // Index into fragment array 1825 i = 0 1826 // Active fragment 1827 fragment: TreeFragment | null = null 1828 fragmentEnd = -1 1829 // Cursor into the current fragment, if any. When `moveTo` returns 1830 // true, this points at the first block after `pos`. 1831 cursor: TreeCursor | null = null 1832 1833 constructor(readonly fragments: readonly TreeFragment[], readonly input: Input) { 1834 if (fragments.length) this.fragment = fragments[this.i++] 1835 } 1836 1837 nextFragment() { 1838 this.fragment = this.i < this.fragments.length ? this.fragments[this.i++] : null 1839 this.cursor = null 1840 this.fragmentEnd = -1 1841 } 1842 1843 moveTo(pos: number, lineStart: number) { 1844 while (this.fragment && this.fragment.to <= pos) this.nextFragment() 1845 if (!this.fragment || this.fragment.from > (pos ? pos - 1 : 0)) return false 1846 if (this.fragmentEnd < 0) { 1847 let end = this.fragment.to 1848 while (end > 0 && this.input.read(end - 1, end) != "\n") end-- 1849 this.fragmentEnd = end ? end - 1 : 0 1850 } 1851 1852 let c = this.cursor 1853 if (!c) { 1854 c = this.cursor = this.fragment.tree.cursor() 1855 c.firstChild() 1856 } 1857 1858 let rPos = pos + this.fragment.offset 1859 while (c.to <= rPos) if (!c.parent()) return false 1860 for (;;) { 1861 if (c.from >= rPos) return this.fragment.from <= lineStart 1862 if (!c.childAfter(rPos)) return false 1863 } 1864 } 1865 1866 matches(hash: number) { 1867 let tree = this.cursor!.tree 1868 return tree && tree.prop(NodeProp.contextHash) == hash 1869 } 1870 1871 takeNodes(cx: BlockContext) { 1872 let cur = this.cursor!, off = this.fragment!.offset, fragEnd = this.fragmentEnd - (this.fragment!.openEnd ? 1 : 0) 1873 let start = cx.absoluteLineStart, end = start, blockI = cx.block.children.length 1874 let prevEnd = end, prevI = blockI 1875 for (;;) { 1876 if (cur.to - off > fragEnd) { 1877 if (cur.type.isAnonymous && cur.firstChild()) continue 1878 break 1879 } 1880 let pos = toRelative(cur.from - off, cx.ranges) 1881 if (cur.to - off <= cx.ranges[cx.rangeI].to) { // Fits in current range 1882 cx.addNode(cur.tree!, pos) 1883 } else { 1884 let dummy = new Tree(cx.parser.nodeSet.types[Type.Paragraph], [], [], 0, cx.block.hashProp) 1885 cx.reusePlaceholders.set(dummy, cur.tree!) 1886 cx.addNode(dummy, pos) 1887 } 1888 // Taken content must always end in a block, because incremental 1889 // parsing happens on block boundaries. Never stop directly 1890 // after an indented code block, since those can continue after 1891 // any number of blank lines. 1892 if (cur.type.is("Block")) { 1893 if (NotLast.indexOf(cur.type.id) < 0) { 1894 end = cur.to - off 1895 blockI = cx.block.children.length 1896 } else { 1897 end = prevEnd 1898 blockI = prevI 1899 } 1900 prevEnd = cur.to - off 1901 prevI = cx.block.children.length 1902 } 1903 if (!cur.nextSibling()) break 1904 } 1905 while (cx.block.children.length > blockI) { 1906 cx.block.children.pop() 1907 cx.block.positions.pop() 1908 } 1909 return end - start 1910 } 1911} 1912 1913// Convert an input-stream-relative position to a 1914// Markdown-doc-relative position by subtracting the size of all input 1915// gaps before `abs`. 1916function toRelative(abs: number, ranges: readonly {from: number, to: number}[]) { 1917 let pos = abs 1918 for (let i = 1; i < ranges.length; i++) { 1919 let gapFrom = ranges[i - 1].to, gapTo = ranges[i].from 1920 if (gapFrom < abs) pos -= gapTo - gapFrom 1921 } 1922 return pos 1923} 1924 1925const markdownHighlighting = styleTags({ 1926 "Blockquote/...": t.quote, 1927 HorizontalRule: t.contentSeparator, 1928 "ATXHeading1/... SetextHeading1/...": t.heading1, 1929 "ATXHeading2/... SetextHeading2/...": t.heading2, 1930 "ATXHeading3/...": t.heading3, 1931 "ATXHeading4/...": t.heading4, 1932 "ATXHeading5/...": t.heading5, 1933 "ATXHeading6/...": t.heading6, 1934 "Comment CommentBlock": t.comment, 1935 Escape: t.escape, 1936 Entity: t.character, 1937 "Emphasis/...": t.emphasis, 1938 "StrongEmphasis/...": t.strong, 1939 "Link/... Image/...": t.link, 1940 "OrderedList/... BulletList/...": t.list, 1941 "BlockQuote/...": t.quote, 1942 "InlineCode CodeText": t.monospace, 1943 "URL Autolink": t.url, 1944 "HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": t.processingInstruction, 1945 "CodeInfo LinkLabel": t.labelName, 1946 LinkTitle: t.string, 1947 Paragraph: t.content 1948}) 1949 1950/// The default CommonMark parser. 1951export const parser = new MarkdownParser( 1952 new NodeSet(nodeTypes).extend(markdownHighlighting), 1953 Object.keys(DefaultBlockParsers).map(n => DefaultBlockParsers[n]), 1954 Object.keys(DefaultBlockParsers).map(n => DefaultLeafBlocks[n]), 1955 Object.keys(DefaultBlockParsers), 1956 DefaultEndLeaf, 1957 DefaultSkipMarkup, 1958 Object.keys(DefaultInline).map(n => DefaultInline[n]), 1959 Object.keys(DefaultInline), 1960 [] 1961)