@farvardin/lezer-parser-markdown 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +279 -0
- package/LICENSE +21 -0
- package/README.md +719 -0
- package/bin/build-readme.cjs +39 -0
- package/build.js +16 -0
- package/dist/index.cjs +2357 -0
- package/dist/index.d.cts +600 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.js +2340 -0
- package/package.json +37 -0
- package/publish.sh +1 -0
- package/src/README.md +83 -0
- package/src/extension.ts +301 -0
- package/src/index.ts +5 -0
- package/src/markdown.ts +1966 -0
- package/src/nest.ts +46 -0
- package/test/compare-tree.ts +14 -0
- package/test/spec.ts +79 -0
- package/test/test-extension.ts +277 -0
- package/test/test-incremental.ts +265 -0
- package/test/test-markdown.ts +3574 -0
- package/test/test-nesting.ts +86 -0
- package/test/tsconfig.json +12 -0
- package/tsconfig.json +14 -0
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,1966 @@
|
|
|
1
|
+
import {Tree, TreeBuffer, NodeType, NodeProp, NodePropSource, TreeFragment, NodeSet, TreeCursor,
|
|
2
|
+
Input, Parser, PartialParse, SyntaxNode, ParseWrapper} from "@lezer/common"
|
|
3
|
+
import {styleTags, tags as t, Tag} from "@lezer/highlight"
|
|
4
|
+
|
|
5
|
+
class 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
|
+
|
|
41
|
+
export 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).
|
|
95
|
+
export 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.
|
|
111
|
+
export 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
|
+
|
|
205
|
+
function 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
|
+
|
|
215
|
+
const 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
|
+
|
|
233
|
+
export function space(ch: number) { return ch == 32 || ch == 9 || ch == 10 || ch == 13 }
|
|
234
|
+
|
|
235
|
+
function skipSpace(line: string, i = 0) {
|
|
236
|
+
while (i < line.length && space(line.charCodeAt(i))) i++
|
|
237
|
+
return i
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function skipSpaceBack(line: string, i: number, to: number) {
|
|
241
|
+
while (i > to && space(line.charCodeAt(i - 1))) i--
|
|
242
|
+
return i
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function 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
|
+
|
|
254
|
+
function isBlockquote(line: Line) {
|
|
255
|
+
return line.next != 62 /* '>' */ ? -1 : line.text.charCodeAt(line.pos + 1) == 32 ? 2 : 1
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function 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
|
+
|
|
272
|
+
function 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
|
+
|
|
278
|
+
function 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
|
+
|
|
284
|
+
function 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
|
+
|
|
301
|
+
function isAtxHeading(line: Line) {
|
|
302
|
+
if (line.next != 61 /* '=' */) return -1
|
|
303
|
+
let pos = line.pos + 1
|
|
304
|
+
while (pos < line.text.length && line.text.charCodeAt(pos) == 61) 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
|
+
|
|
310
|
+
function 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
|
+
|
|
319
|
+
const EmptyLine = /^[ \t]*$/, CommentEnd = /-->/, ProcessingEnd = /\?>/
|
|
320
|
+
const 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
|
+
|
|
330
|
+
function 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
|
+
|
|
338
|
+
function 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.
|
|
354
|
+
type BlockResult = boolean | null
|
|
355
|
+
|
|
356
|
+
function 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.
|
|
366
|
+
const 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
|
+
|
|
515
|
+
const 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.
|
|
522
|
+
class 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
|
+
|
|
592
|
+
function 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
|
+
|
|
601
|
+
class 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
|
+
|
|
620
|
+
const 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
|
+
|
|
625
|
+
const 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
|
+
|
|
635
|
+
const scanLineResult = {text: "", end: 0}
|
|
636
|
+
|
|
637
|
+
/// Block-level parsing functions get access to this context object.
|
|
638
|
+
export 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
|
+
|
|
939
|
+
function 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).
|
|
975
|
+
export 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.
|
|
997
|
+
export 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.
|
|
1045
|
+
export 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.
|
|
1079
|
+
export 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.
|
|
1098
|
+
export 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.
|
|
1118
|
+
export type MarkdownExtension = MarkdownConfig | readonly MarkdownExtension[]
|
|
1119
|
+
|
|
1120
|
+
/// A Markdown parser configuration.
|
|
1121
|
+
export 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
|
+
|
|
1266
|
+
function nonEmpty<T>(a: undefined | readonly T[]): a is readonly T[] {
|
|
1267
|
+
return a != null && a.length > 0
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function 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
|
+
|
|
1291
|
+
function 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
|
+
|
|
1297
|
+
let nodeTypes = [NodeType.none]
|
|
1298
|
+
for (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
|
+
|
|
1307
|
+
const none: readonly any[] = []
|
|
1308
|
+
|
|
1309
|
+
class 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.
|
|
1336
|
+
export 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
|
+
|
|
1363
|
+
class 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
|
+
|
|
1380
|
+
function elt(type: Type, from: number, to: number, children?: readonly (Element | TreeElement)[]) {
|
|
1381
|
+
return new Element(type, from, to, children)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const 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.
|
|
1390
|
+
export 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
|
+
|
|
1407
|
+
const EmphasisUnderscore: DelimiterType = {resolve: "Emphasis", mark: "EmphasisMark"}
|
|
1408
|
+
const EmphasisAsterisk: DelimiterType = {resolve: "Emphasis", mark: "EmphasisMark"}
|
|
1409
|
+
const LinkStart: DelimiterType = {}, ImageStart: DelimiterType = {}
|
|
1410
|
+
|
|
1411
|
+
class InlineDelimiter {
|
|
1412
|
+
constructor(readonly type: DelimiterType,
|
|
1413
|
+
readonly from: number,
|
|
1414
|
+
readonly to: number,
|
|
1415
|
+
public side: Mark) {}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const Escapable = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
|
1419
|
+
|
|
1420
|
+
export let Punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\u2010-\u2027]/
|
|
1421
|
+
try { Punctuation = new RegExp("[\\p{S}|\\p{P}]", "u") } catch (_) {}
|
|
1422
|
+
|
|
1423
|
+
const 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 != 47 && 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
|
+
// 95 = _ 5F
|
|
1491
|
+
// 47 = / 2F
|
|
1492
|
+
// 42 = * 2A
|
|
1493
|
+
// 35 = # 23
|
|
1494
|
+
// 61 = = 3D
|
|
1495
|
+
return cx.append(new InlineDelimiter(next == 47 ? EmphasisUnderscore : EmphasisAsterisk, start, pos,
|
|
1496
|
+
(canOpen ? Mark.Open : Mark.None) | (canClose ? Mark.Close : Mark.None)))
|
|
1497
|
+
},
|
|
1498
|
+
|
|
1499
|
+
HardBreak(cx, next, start) {
|
|
1500
|
+
if (next == 92 /* '\\' */ && cx.char(start + 1) == 10 /* '\n' */)
|
|
1501
|
+
return cx.append(elt(Type.HardBreak, start, start + 2))
|
|
1502
|
+
if (next == 32) {
|
|
1503
|
+
let pos = start + 1
|
|
1504
|
+
while (cx.char(pos) == 32) pos++
|
|
1505
|
+
if (cx.char(pos) == 10 && pos >= start + 2)
|
|
1506
|
+
return cx.append(elt(Type.HardBreak, start, pos + 1))
|
|
1507
|
+
}
|
|
1508
|
+
return -1
|
|
1509
|
+
},
|
|
1510
|
+
|
|
1511
|
+
Link(cx, next, start) {
|
|
1512
|
+
return next == 91 /* '[' */ ? cx.append(new InlineDelimiter(LinkStart, start, start + 1, Mark.Open)) : -1
|
|
1513
|
+
},
|
|
1514
|
+
|
|
1515
|
+
Image(cx, next, start) {
|
|
1516
|
+
return next == 33 /* '!' */ && cx.char(start + 1) == 91 /* '[' */
|
|
1517
|
+
? cx.append(new InlineDelimiter(ImageStart, start, start + 2, Mark.Open)) : -1
|
|
1518
|
+
},
|
|
1519
|
+
|
|
1520
|
+
LinkEnd(cx, next, start) {
|
|
1521
|
+
if (next != 93 /* ']' */) return -1
|
|
1522
|
+
// Scanning back to the next link/image start marker
|
|
1523
|
+
for (let i = cx.parts.length - 1; i >= 0; i--) {
|
|
1524
|
+
let part = cx.parts[i]
|
|
1525
|
+
if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) {
|
|
1526
|
+
// If this one has been set invalid (because it would produce
|
|
1527
|
+
// a nested link) or there's no valid link here ignore both.
|
|
1528
|
+
if (!part.side || cx.skipSpace(part.to) == start && !/[(\[]/.test(cx.slice(start + 1, start + 2))) {
|
|
1529
|
+
cx.parts[i] = null
|
|
1530
|
+
return -1
|
|
1531
|
+
}
|
|
1532
|
+
// Finish the content and replace the entire range in
|
|
1533
|
+
// this.parts with the link/image node.
|
|
1534
|
+
let content = cx.takeContent(i)
|
|
1535
|
+
let link = cx.parts[i] = finishLink(cx, content, part.type == LinkStart ? Type.Link : Type.Image, part.from, start + 1)
|
|
1536
|
+
// Set any open-link markers before this link to invalid.
|
|
1537
|
+
if (part.type == LinkStart) for (let j = 0; j < i; j++) {
|
|
1538
|
+
let p = cx.parts[j]
|
|
1539
|
+
if (p instanceof InlineDelimiter && p.type == LinkStart) p.side = Mark.None
|
|
1540
|
+
}
|
|
1541
|
+
return link.to
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return -1
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function finishLink(cx: InlineContext, content: Element[], type: Type, start: number, startPos: number) {
|
|
1549
|
+
let {text} = cx, next = cx.char(startPos), endPos = startPos
|
|
1550
|
+
content.unshift(elt(Type.LinkMark, start, start + (type == Type.Image ? 2 : 1)))
|
|
1551
|
+
content.push(elt(Type.LinkMark, startPos - 1, startPos))
|
|
1552
|
+
if (next == 40 /* '(' */) {
|
|
1553
|
+
let pos = cx.skipSpace(startPos + 1)
|
|
1554
|
+
let dest = parseURL(text, pos - cx.offset, cx.offset), title
|
|
1555
|
+
if (dest) {
|
|
1556
|
+
pos = cx.skipSpace(dest.to)
|
|
1557
|
+
// The destination and title must be separated by whitespace
|
|
1558
|
+
if (pos != dest.to) {
|
|
1559
|
+
title = parseLinkTitle(text, pos - cx.offset, cx.offset)
|
|
1560
|
+
if (title) pos = cx.skipSpace(title.to)
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (cx.char(pos) == 41 /* ')' */) {
|
|
1564
|
+
content.push(elt(Type.LinkMark, startPos, startPos + 1))
|
|
1565
|
+
endPos = pos + 1
|
|
1566
|
+
if (dest) content.push(dest)
|
|
1567
|
+
if (title) content.push(title)
|
|
1568
|
+
content.push(elt(Type.LinkMark, pos, endPos))
|
|
1569
|
+
}
|
|
1570
|
+
} else if (next == 91 /* '[' */) {
|
|
1571
|
+
let label = parseLinkLabel(text, startPos - cx.offset, cx.offset, false)
|
|
1572
|
+
if (label) {
|
|
1573
|
+
content.push(label)
|
|
1574
|
+
endPos = label.to
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return elt(type, start, endPos, content)
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// These return `null` when falling off the end of the input, `false`
|
|
1581
|
+
// when parsing fails otherwise (for use in the incremental link
|
|
1582
|
+
// reference parser).
|
|
1583
|
+
|
|
1584
|
+
function parseURL(text: string, start: number, offset: number): null | false | Element {
|
|
1585
|
+
let next = text.charCodeAt(start)
|
|
1586
|
+
if (next == 60 /* '<' */) {
|
|
1587
|
+
for (let pos = start + 1; pos < text.length; pos++) {
|
|
1588
|
+
let ch = text.charCodeAt(pos)
|
|
1589
|
+
if (ch == 62 /* '>' */) return elt(Type.URL, start + offset, pos + 1 + offset)
|
|
1590
|
+
if (ch == 60 || ch == 10 /* '<\n' */) return false
|
|
1591
|
+
}
|
|
1592
|
+
return null
|
|
1593
|
+
} else {
|
|
1594
|
+
let depth = 0, pos = start
|
|
1595
|
+
for (let escaped = false; pos < text.length; pos++) {
|
|
1596
|
+
let ch = text.charCodeAt(pos)
|
|
1597
|
+
if (space(ch)) {
|
|
1598
|
+
break
|
|
1599
|
+
} else if (escaped) {
|
|
1600
|
+
escaped = false
|
|
1601
|
+
} else if (ch == 40 /* '(' */) {
|
|
1602
|
+
depth++
|
|
1603
|
+
} else if (ch == 41 /* ')' */) {
|
|
1604
|
+
if (!depth) break
|
|
1605
|
+
depth--
|
|
1606
|
+
} else if (ch == 92 /* '\\' */) {
|
|
1607
|
+
escaped = true
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return pos > start ? elt(Type.URL, start + offset, pos + offset) : pos == text.length ? null : false
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function parseLinkTitle(text: string, start: number, offset: number): null | false | Element {
|
|
1615
|
+
let next = text.charCodeAt(start)
|
|
1616
|
+
if (next != 39 && next != 34 && next != 40 /* '"\'(' */) return false
|
|
1617
|
+
let end = next == 40 ? 41 : next
|
|
1618
|
+
for (let pos = start + 1, escaped = false; pos < text.length; pos++) {
|
|
1619
|
+
let ch = text.charCodeAt(pos)
|
|
1620
|
+
if (escaped) escaped = false
|
|
1621
|
+
else if (ch == end) return elt(Type.LinkTitle, start + offset, pos + 1 + offset)
|
|
1622
|
+
else if (ch == 92 /* '\\' */) escaped = true
|
|
1623
|
+
}
|
|
1624
|
+
return null
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function parseLinkLabel(text: string, start: number, offset: number, requireNonWS: boolean): null | false | Element {
|
|
1628
|
+
for (let escaped = false, pos = start + 1, end = Math.min(text.length, pos + 999); pos < end; pos++) {
|
|
1629
|
+
let ch = text.charCodeAt(pos)
|
|
1630
|
+
if (escaped) escaped = false
|
|
1631
|
+
else if (ch == 93 /* ']' */) return requireNonWS ? false : elt(Type.LinkLabel, start + offset, pos + 1 + offset)
|
|
1632
|
+
else {
|
|
1633
|
+
if (requireNonWS && !space(ch)) requireNonWS = false
|
|
1634
|
+
if (ch == 91 /* '[' */) return false
|
|
1635
|
+
else if (ch == 92 /* '\\' */) escaped = true
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return null
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/// Inline parsing functions get access to this context, and use it to
|
|
1642
|
+
/// read the content and emit syntax nodes.
|
|
1643
|
+
export class InlineContext {
|
|
1644
|
+
/// @internal
|
|
1645
|
+
parts: (Element | InlineDelimiter | null)[] = []
|
|
1646
|
+
|
|
1647
|
+
/// @internal
|
|
1648
|
+
constructor(
|
|
1649
|
+
/// The parser that is being used.
|
|
1650
|
+
readonly parser: MarkdownParser,
|
|
1651
|
+
/// The text of this inline section.
|
|
1652
|
+
readonly text: string,
|
|
1653
|
+
/// The starting offset of the section in the document.
|
|
1654
|
+
readonly offset: number
|
|
1655
|
+
) {}
|
|
1656
|
+
|
|
1657
|
+
/// Get the character code at the given (document-relative)
|
|
1658
|
+
/// position.
|
|
1659
|
+
char(pos: number) { return pos >= this.end ? -1 : this.text.charCodeAt(pos - this.offset) }
|
|
1660
|
+
|
|
1661
|
+
/// The position of the end of this inline section.
|
|
1662
|
+
get end() { return this.offset + this.text.length }
|
|
1663
|
+
|
|
1664
|
+
/// Get a substring of this inline section. Again uses
|
|
1665
|
+
/// document-relative positions.
|
|
1666
|
+
slice(from: number, to: number) { return this.text.slice(from - this.offset, to - this.offset) }
|
|
1667
|
+
|
|
1668
|
+
/// @internal
|
|
1669
|
+
append(elt: Element | InlineDelimiter) {
|
|
1670
|
+
this.parts.push(elt)
|
|
1671
|
+
return elt.to
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/// Add a [delimiter](#DelimiterType) at this given position. `open`
|
|
1675
|
+
/// and `close` indicate whether this delimiter is opening, closing,
|
|
1676
|
+
/// or both. Returns the end of the delimiter, for convenient
|
|
1677
|
+
/// returning from [parse functions](#InlineParser.parse).
|
|
1678
|
+
addDelimiter(type: DelimiterType, from: number, to: number, open: boolean, close: boolean) {
|
|
1679
|
+
return this.append(new InlineDelimiter(type, from, to, (open ? Mark.Open : Mark.None) | (close ? Mark.Close : Mark.None)))
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/// Returns true when there is an unmatched link or image opening
|
|
1683
|
+
/// token before the current position.
|
|
1684
|
+
get hasOpenLink() {
|
|
1685
|
+
for (let i = this.parts.length - 1; i >= 0; i--) {
|
|
1686
|
+
let part = this.parts[i]
|
|
1687
|
+
if (part instanceof InlineDelimiter && (part.type == LinkStart || part.type == ImageStart)) return true
|
|
1688
|
+
}
|
|
1689
|
+
return false
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/// Add an inline element. Returns the end of the element.
|
|
1693
|
+
addElement(elt: Element) {
|
|
1694
|
+
return this.append(elt)
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/// Resolve markers between this.parts.length and from, wrapping matched markers in the
|
|
1698
|
+
/// appropriate node and updating the content of this.parts. @internal
|
|
1699
|
+
resolveMarkers(from: number) {
|
|
1700
|
+
// Scan forward, looking for closing tokens
|
|
1701
|
+
for (let i = from; i < this.parts.length; i++) {
|
|
1702
|
+
let close = this.parts[i]
|
|
1703
|
+
if (!(close instanceof InlineDelimiter && close.type.resolve && (close.side & Mark.Close))) continue
|
|
1704
|
+
|
|
1705
|
+
let emp = close.type == EmphasisUnderscore || close.type == EmphasisAsterisk
|
|
1706
|
+
let closeSize = close.to - close.from
|
|
1707
|
+
let open: InlineDelimiter | undefined, j = i - 1
|
|
1708
|
+
// Continue scanning for a matching opening token
|
|
1709
|
+
for (; j >= from; j--) {
|
|
1710
|
+
let part = this.parts[j]
|
|
1711
|
+
if (part instanceof InlineDelimiter && (part.side & Mark.Open) && part.type == close.type &&
|
|
1712
|
+
// Ignore emphasis delimiters where the character count doesn't match
|
|
1713
|
+
!(emp && ((close.side & Mark.Open) || (part.side & Mark.Close)) &&
|
|
1714
|
+
(part.to - part.from + closeSize) % 3 == 0 && ((part.to - part.from) % 3 || closeSize % 3))) {
|
|
1715
|
+
open = part
|
|
1716
|
+
break
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (!open) continue
|
|
1720
|
+
|
|
1721
|
+
let type = close.type.resolve, content = []
|
|
1722
|
+
let start = open.from, end = close.to
|
|
1723
|
+
// Emphasis marker effect depends on the character count. Size consumed is minimum of the two
|
|
1724
|
+
// markers.
|
|
1725
|
+
if (emp) {
|
|
1726
|
+
let size = Math.min(2, open.to - open.from, closeSize)
|
|
1727
|
+
start = open.to - size
|
|
1728
|
+
end = close.from + size
|
|
1729
|
+
type = size == 1 ? "Emphasis" : "StrongEmphasis"
|
|
1730
|
+
}
|
|
1731
|
+
// Move the covered region into content, optionally adding marker nodes
|
|
1732
|
+
if (open.type.mark) content.push(this.elt(open.type.mark, start, open.to))
|
|
1733
|
+
for (let k = j + 1; k < i; k++) {
|
|
1734
|
+
if (this.parts[k] instanceof Element) content.push(this.parts[k] as Element)
|
|
1735
|
+
this.parts[k] = null
|
|
1736
|
+
}
|
|
1737
|
+
if (close.type.mark) content.push(this.elt(close.type.mark, close.from, end))
|
|
1738
|
+
let element = this.elt(type, start, end, content)
|
|
1739
|
+
// If there are leftover emphasis marker characters, shrink the close/open markers. Otherwise, clear them.
|
|
1740
|
+
this.parts[j] = emp && open.from != start ? new InlineDelimiter(open.type, open.from, start, open.side) : null
|
|
1741
|
+
let keep = this.parts[i] = emp && close.to != end ? new InlineDelimiter(close.type, end, close.to, close.side) : null
|
|
1742
|
+
// Insert the new element in this.parts
|
|
1743
|
+
if (keep) this.parts.splice(i, 0, element)
|
|
1744
|
+
else this.parts[i] = element
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Collect the elements remaining in this.parts into an array.
|
|
1748
|
+
let result = []
|
|
1749
|
+
for (let i = from; i < this.parts.length; i++) {
|
|
1750
|
+
let part = this.parts[i]
|
|
1751
|
+
if (part instanceof Element) result.push(part)
|
|
1752
|
+
}
|
|
1753
|
+
return result
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/// Find an opening delimiter of the given type. Returns `null` if
|
|
1757
|
+
/// no delimiter is found, or an index that can be passed to
|
|
1758
|
+
/// [`takeContent`](#InlineContext.takeContent) otherwise.
|
|
1759
|
+
findOpeningDelimiter(type: DelimiterType) {
|
|
1760
|
+
for (let i = this.parts.length - 1; i >= 0; i--) {
|
|
1761
|
+
let part = this.parts[i]
|
|
1762
|
+
if (part instanceof InlineDelimiter && part.type == type && (part.side & Mark.Open)) return i
|
|
1763
|
+
}
|
|
1764
|
+
return null
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/// Remove all inline elements and delimiters starting from the
|
|
1768
|
+
/// given index (which you should get from
|
|
1769
|
+
/// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter),
|
|
1770
|
+
/// resolve delimiters inside of them, and return them as an array
|
|
1771
|
+
/// of elements.
|
|
1772
|
+
takeContent(startIndex: number) {
|
|
1773
|
+
let content = this.resolveMarkers(startIndex)
|
|
1774
|
+
this.parts.length = startIndex
|
|
1775
|
+
return content
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/// Return the delimiter at the given index. Mostly useful to get
|
|
1779
|
+
/// additional info out of a delimiter index returned by
|
|
1780
|
+
/// [`findOpeningDelimiter`](#InlineContext.findOpeningDelimiter).
|
|
1781
|
+
/// Returns null if there is no delimiter at this index.
|
|
1782
|
+
getDelimiterAt(index: number): {from: number, to: number, type: DelimiterType} | null {
|
|
1783
|
+
let part = this.parts[index]
|
|
1784
|
+
return part instanceof InlineDelimiter ? part : null
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/// Skip space after the given (document) position, returning either
|
|
1788
|
+
/// the position of the next non-space character or the end of the
|
|
1789
|
+
/// section.
|
|
1790
|
+
skipSpace(from: number) { return skipSpace(this.text, from - this.offset) + this.offset }
|
|
1791
|
+
|
|
1792
|
+
/// Create an [`Element`](#Element) for a syntax node.
|
|
1793
|
+
elt(type: string, from: number, to: number, children?: readonly Element[]): Element
|
|
1794
|
+
elt(tree: Tree, at: number): Element
|
|
1795
|
+
elt(type: string | Tree, from: number, to?: number, children?: readonly Element[]): Element {
|
|
1796
|
+
if (typeof type == "string") return elt(this.parser.getNodeType(type), from, to!, children)
|
|
1797
|
+
return new TreeElement(type, from)
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/// The opening delimiter type used by the standard link parser.
|
|
1801
|
+
static linkStart = LinkStart
|
|
1802
|
+
|
|
1803
|
+
/// Opening delimiter type used for standard images.
|
|
1804
|
+
static imageStart = ImageStart
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function injectMarks(elements: readonly (Element | TreeElement)[], marks: Element[]) {
|
|
1808
|
+
if (!marks.length) return elements
|
|
1809
|
+
if (!elements.length) return marks
|
|
1810
|
+
let elts = elements.slice(), eI = 0
|
|
1811
|
+
for (let mark of marks) {
|
|
1812
|
+
while (eI < elts.length && elts[eI].to < mark.to) eI++
|
|
1813
|
+
if (eI < elts.length && elts[eI].from < mark.from) {
|
|
1814
|
+
let e = elts[eI]
|
|
1815
|
+
if (e instanceof Element)
|
|
1816
|
+
elts[eI] = new Element(e.type, e.from, e.to, injectMarks(e.children, [mark]))
|
|
1817
|
+
} else {
|
|
1818
|
+
elts.splice(eI++, 0, mark)
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return elts
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// These are blocks that can span blank lines, and should thus only be
|
|
1825
|
+
// reused if their next sibling is also being reused.
|
|
1826
|
+
const NotLast = [Type.CodeBlock, Type.ListItem, Type.OrderedList, Type.BulletList]
|
|
1827
|
+
|
|
1828
|
+
class FragmentCursor {
|
|
1829
|
+
// Index into fragment array
|
|
1830
|
+
i = 0
|
|
1831
|
+
// Active fragment
|
|
1832
|
+
fragment: TreeFragment | null = null
|
|
1833
|
+
fragmentEnd = -1
|
|
1834
|
+
// Cursor into the current fragment, if any. When `moveTo` returns
|
|
1835
|
+
// true, this points at the first block after `pos`.
|
|
1836
|
+
cursor: TreeCursor | null = null
|
|
1837
|
+
|
|
1838
|
+
constructor(readonly fragments: readonly TreeFragment[], readonly input: Input) {
|
|
1839
|
+
if (fragments.length) this.fragment = fragments[this.i++]
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
nextFragment() {
|
|
1843
|
+
this.fragment = this.i < this.fragments.length ? this.fragments[this.i++] : null
|
|
1844
|
+
this.cursor = null
|
|
1845
|
+
this.fragmentEnd = -1
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
moveTo(pos: number, lineStart: number) {
|
|
1849
|
+
while (this.fragment && this.fragment.to <= pos) this.nextFragment()
|
|
1850
|
+
if (!this.fragment || this.fragment.from > (pos ? pos - 1 : 0)) return false
|
|
1851
|
+
if (this.fragmentEnd < 0) {
|
|
1852
|
+
let end = this.fragment.to
|
|
1853
|
+
while (end > 0 && this.input.read(end - 1, end) != "\n") end--
|
|
1854
|
+
this.fragmentEnd = end ? end - 1 : 0
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
let c = this.cursor
|
|
1858
|
+
if (!c) {
|
|
1859
|
+
c = this.cursor = this.fragment.tree.cursor()
|
|
1860
|
+
c.firstChild()
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
let rPos = pos + this.fragment.offset
|
|
1864
|
+
while (c.to <= rPos) if (!c.parent()) return false
|
|
1865
|
+
for (;;) {
|
|
1866
|
+
if (c.from >= rPos) return this.fragment.from <= lineStart
|
|
1867
|
+
if (!c.childAfter(rPos)) return false
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
matches(hash: number) {
|
|
1872
|
+
let tree = this.cursor!.tree
|
|
1873
|
+
return tree && tree.prop(NodeProp.contextHash) == hash
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
takeNodes(cx: BlockContext) {
|
|
1877
|
+
let cur = this.cursor!, off = this.fragment!.offset, fragEnd = this.fragmentEnd - (this.fragment!.openEnd ? 1 : 0)
|
|
1878
|
+
let start = cx.absoluteLineStart, end = start, blockI = cx.block.children.length
|
|
1879
|
+
let prevEnd = end, prevI = blockI
|
|
1880
|
+
for (;;) {
|
|
1881
|
+
if (cur.to - off > fragEnd) {
|
|
1882
|
+
if (cur.type.isAnonymous && cur.firstChild()) continue
|
|
1883
|
+
break
|
|
1884
|
+
}
|
|
1885
|
+
let pos = toRelative(cur.from - off, cx.ranges)
|
|
1886
|
+
if (cur.to - off <= cx.ranges[cx.rangeI].to) { // Fits in current range
|
|
1887
|
+
cx.addNode(cur.tree!, pos)
|
|
1888
|
+
} else {
|
|
1889
|
+
let dummy = new Tree(cx.parser.nodeSet.types[Type.Paragraph], [], [], 0, cx.block.hashProp)
|
|
1890
|
+
cx.reusePlaceholders.set(dummy, cur.tree!)
|
|
1891
|
+
cx.addNode(dummy, pos)
|
|
1892
|
+
}
|
|
1893
|
+
// Taken content must always end in a block, because incremental
|
|
1894
|
+
// parsing happens on block boundaries. Never stop directly
|
|
1895
|
+
// after an indented code block, since those can continue after
|
|
1896
|
+
// any number of blank lines.
|
|
1897
|
+
if (cur.type.is("Block")) {
|
|
1898
|
+
if (NotLast.indexOf(cur.type.id) < 0) {
|
|
1899
|
+
end = cur.to - off
|
|
1900
|
+
blockI = cx.block.children.length
|
|
1901
|
+
} else {
|
|
1902
|
+
end = prevEnd
|
|
1903
|
+
blockI = prevI
|
|
1904
|
+
}
|
|
1905
|
+
prevEnd = cur.to - off
|
|
1906
|
+
prevI = cx.block.children.length
|
|
1907
|
+
}
|
|
1908
|
+
if (!cur.nextSibling()) break
|
|
1909
|
+
}
|
|
1910
|
+
while (cx.block.children.length > blockI) {
|
|
1911
|
+
cx.block.children.pop()
|
|
1912
|
+
cx.block.positions.pop()
|
|
1913
|
+
}
|
|
1914
|
+
return end - start
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Convert an input-stream-relative position to a
|
|
1919
|
+
// Markdown-doc-relative position by subtracting the size of all input
|
|
1920
|
+
// gaps before `abs`.
|
|
1921
|
+
function toRelative(abs: number, ranges: readonly {from: number, to: number}[]) {
|
|
1922
|
+
let pos = abs
|
|
1923
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
1924
|
+
let gapFrom = ranges[i - 1].to, gapTo = ranges[i].from
|
|
1925
|
+
if (gapFrom < abs) pos -= gapTo - gapFrom
|
|
1926
|
+
}
|
|
1927
|
+
return pos
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const markdownHighlighting = styleTags({
|
|
1931
|
+
"Blockquote/...": t.quote,
|
|
1932
|
+
HorizontalRule: t.contentSeparator,
|
|
1933
|
+
"ATXHeading1/... SetextHeading1/...": t.heading1,
|
|
1934
|
+
"ATXHeading2/... SetextHeading2/...": t.heading2,
|
|
1935
|
+
"ATXHeading3/...": t.heading3,
|
|
1936
|
+
"ATXHeading4/...": t.heading4,
|
|
1937
|
+
"ATXHeading5/...": t.heading5,
|
|
1938
|
+
"ATXHeading6/...": t.heading6,
|
|
1939
|
+
"Comment CommentBlock": t.comment,
|
|
1940
|
+
Escape: t.escape,
|
|
1941
|
+
Entity: t.character,
|
|
1942
|
+
"Emphasis/...": t.emphasis,
|
|
1943
|
+
"StrongEmphasis/...": t.strong,
|
|
1944
|
+
"Link/... Image/...": t.link,
|
|
1945
|
+
"OrderedList/... BulletList/...": t.list,
|
|
1946
|
+
"BlockQuote/...": t.quote,
|
|
1947
|
+
"InlineCode CodeText": t.monospace,
|
|
1948
|
+
"URL Autolink": t.url,
|
|
1949
|
+
"HeaderMark HardBreak QuoteMark ListMark LinkMark EmphasisMark CodeMark": t.processingInstruction,
|
|
1950
|
+
"CodeInfo LinkLabel": t.labelName,
|
|
1951
|
+
LinkTitle: t.string,
|
|
1952
|
+
Paragraph: t.content
|
|
1953
|
+
})
|
|
1954
|
+
|
|
1955
|
+
/// The default CommonMark parser.
|
|
1956
|
+
export const parser = new MarkdownParser(
|
|
1957
|
+
new NodeSet(nodeTypes).extend(markdownHighlighting),
|
|
1958
|
+
Object.keys(DefaultBlockParsers).map(n => DefaultBlockParsers[n]),
|
|
1959
|
+
Object.keys(DefaultBlockParsers).map(n => DefaultLeafBlocks[n]),
|
|
1960
|
+
Object.keys(DefaultBlockParsers),
|
|
1961
|
+
DefaultEndLeaf,
|
|
1962
|
+
DefaultSkipMarkup,
|
|
1963
|
+
Object.keys(DefaultInline).map(n => DefaultInline[n]),
|
|
1964
|
+
Object.keys(DefaultInline),
|
|
1965
|
+
[]
|
|
1966
|
+
)
|