Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol
diffdown.com
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)