@tabnas/markdown 0.4.2 → 0.5.0
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/README.md +78 -12
- package/dist/ast.d.ts +83 -0
- package/dist/ast.js +248 -0
- package/dist/ast.js.map +1 -0
- package/dist/block.d.ts +10 -0
- package/dist/block.js +971 -0
- package/dist/block.js.map +1 -0
- package/dist/common.d.ts +49 -0
- package/dist/common.js +155 -0
- package/dist/common.js.map +1 -0
- package/dist/commonmark.d.ts +14 -0
- package/dist/commonmark.js +31 -0
- package/dist/commonmark.js.map +1 -0
- package/dist/entities.d.ts +1 -0
- package/dist/entities.js +14 -0
- package/dist/entities.js.map +1 -0
- package/dist/html.d.ts +10 -0
- package/dist/html.js +270 -0
- package/dist/html.js.map +1 -0
- package/dist/inline.d.ts +13 -0
- package/dist/inline.js +902 -0
- package/dist/inline.js.map +1 -0
- package/dist/markdown.d.ts +18 -84
- package/dist/markdown.js +80 -687
- package/dist/markdown.js.map +1 -1
- package/dist/node.d.ts +74 -0
- package/dist/node.js +167 -0
- package/dist/node.js.map +1 -0
- package/dist/options.d.ts +21 -0
- package/dist/options.js +16 -0
- package/dist/options.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/ast.ts +352 -0
- package/src/block.ts +1094 -0
- package/src/common.ts +166 -0
- package/src/commonmark.ts +34 -0
- package/src/entities.ts +14 -0
- package/src/html.ts +321 -0
- package/src/inline.ts +1034 -0
- package/src/markdown.ts +126 -770
- package/src/node.ts +240 -0
- package/src/options.ts +39 -0
- package/src/tsconfig.json +2 -0
package/src/inline.ts
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
/* Copyright (c) 2021-2026 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// Phase 2 of the CommonMark parse (spec 0.31.2, Appendix A, "Phase 2: inline
|
|
4
|
+
// structure"). The block phase left the raw text of every paragraph and
|
|
5
|
+
// heading in `stringContent`; this module turns that text into inline
|
|
6
|
+
// children and clears it.
|
|
7
|
+
//
|
|
8
|
+
// The shape is the spec's own algorithm: one left-to-right scan over the
|
|
9
|
+
// subject that appends nodes as it goes, plus two auxiliary stacks that are
|
|
10
|
+
// resolved out of band, because neither construct can be decided at the
|
|
11
|
+
// point where its opener is seen:
|
|
12
|
+
//
|
|
13
|
+
// * the delimiter stack (§6.4) holds `*`/`_` (and, under GFM, `~`) runs,
|
|
14
|
+
// and is resolved by `processEmphasis`;
|
|
15
|
+
// * the bracket stack (§6.3) holds `[` and `![`, and is resolved by
|
|
16
|
+
// `parseCloseBracket` when a `]` turns up.
|
|
17
|
+
//
|
|
18
|
+
// Everything else — code spans, autolinks, raw HTML, entities, escapes,
|
|
19
|
+
// breaks — is decided locally at the scan position. That is what produces
|
|
20
|
+
// the precedence §6.3 requires: a code span, autolink or raw HTML tag is
|
|
21
|
+
// consumed whole during the scan, so it binds more tightly than emphasis and
|
|
22
|
+
// more tightly than the brackets of a link label. Conversely brackets bind
|
|
23
|
+
// more tightly than emphasis, because bracket matching happens during the
|
|
24
|
+
// scan while emphasis matching happens afterwards over the delimiter stack.
|
|
25
|
+
|
|
26
|
+
import { MdNode } from './node.ts'
|
|
27
|
+
import type { NodeType } from './node.ts'
|
|
28
|
+
import type { ParserOptions, RefMap } from './options.ts'
|
|
29
|
+
import {
|
|
30
|
+
ENTITY,
|
|
31
|
+
ESCAPABLE,
|
|
32
|
+
decodeEntity,
|
|
33
|
+
isUnicodePunctuation,
|
|
34
|
+
isUnicodeWhitespace,
|
|
35
|
+
normalizeReference,
|
|
36
|
+
unescapeString,
|
|
37
|
+
} from './common.ts'
|
|
38
|
+
|
|
39
|
+
const C_NEWLINE = 10
|
|
40
|
+
const C_SPACE = 32
|
|
41
|
+
const C_BANG = 33
|
|
42
|
+
const C_AMPERSAND = 38
|
|
43
|
+
const C_OPEN_PAREN = 40
|
|
44
|
+
const C_CLOSE_PAREN = 41
|
|
45
|
+
const C_ASTERISK = 42
|
|
46
|
+
const C_COLON = 58
|
|
47
|
+
const C_LESSTHAN = 60
|
|
48
|
+
const C_OPEN_BRACKET = 91
|
|
49
|
+
const C_BACKSLASH = 92
|
|
50
|
+
const C_CLOSE_BRACKET = 93
|
|
51
|
+
const C_UNDERSCORE = 95
|
|
52
|
+
const C_BACKTICK = 96
|
|
53
|
+
const C_TILDE = 126
|
|
54
|
+
const C_DEL = 127
|
|
55
|
+
|
|
56
|
+
// --- §6.6 Raw HTML: the tag grammar, spelled out because it is stricter than
|
|
57
|
+
// `<[^>]*>` in every part. Attribute names are XML-restricted-to-ASCII;
|
|
58
|
+
// unquoted attribute values exclude quotes, `=`, `<`, `>`, backtick and all
|
|
59
|
+
// control characters; comments/PIs/declarations/CDATA each have their own
|
|
60
|
+
// terminator. `\s*` stands in for "spaces, tabs, and up to one line ending":
|
|
61
|
+
// a paragraph can never contain a blank line, so a single `\s*` can only ever
|
|
62
|
+
// span one line ending anyway.
|
|
63
|
+
|
|
64
|
+
const TAGNAME = '[A-Za-z][A-Za-z0-9-]*'
|
|
65
|
+
const ATTRIBUTENAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*'
|
|
66
|
+
const UNQUOTEDVALUE = "[^\"'=<>`\\x00-\\x20]+"
|
|
67
|
+
const SINGLEQUOTEDVALUE = "'[^']*'"
|
|
68
|
+
const DOUBLEQUOTEDVALUE = '"[^"]*"'
|
|
69
|
+
const ATTRIBUTEVALUE =
|
|
70
|
+
'(?:' + UNQUOTEDVALUE + '|' + SINGLEQUOTEDVALUE + '|' + DOUBLEQUOTEDVALUE + ')'
|
|
71
|
+
const ATTRIBUTEVALUESPEC = '(?:\\s*=\\s*' + ATTRIBUTEVALUE + ')'
|
|
72
|
+
const ATTRIBUTE = '(?:\\s+' + ATTRIBUTENAME + ATTRIBUTEVALUESPEC + '?)'
|
|
73
|
+
const OPENTAG = '<' + TAGNAME + ATTRIBUTE + '*\\s*/?>'
|
|
74
|
+
const CLOSETAG = '</' + TAGNAME + '\\s*>'
|
|
75
|
+
// §6.6: `<!-->` and `<!--->` are comments in their own right; otherwise the
|
|
76
|
+
// body simply may not contain `-->`, which the lazy quantifier enforces.
|
|
77
|
+
const HTMLCOMMENT = '<!-->|<!--->|<!--[\\s\\S]*?-->'
|
|
78
|
+
const PROCESSINGINSTRUCTION = '[<][?][\\s\\S]*?[?][>]'
|
|
79
|
+
const DECLARATION = '<![A-Za-z][^>]*>'
|
|
80
|
+
const CDATA = '<!\\[CDATA\\[[\\s\\S]*?\\]\\]>'
|
|
81
|
+
const HTMLTAG =
|
|
82
|
+
'(?:' +
|
|
83
|
+
OPENTAG +
|
|
84
|
+
'|' +
|
|
85
|
+
CLOSETAG +
|
|
86
|
+
'|' +
|
|
87
|
+
HTMLCOMMENT +
|
|
88
|
+
'|' +
|
|
89
|
+
PROCESSINGINSTRUCTION +
|
|
90
|
+
'|' +
|
|
91
|
+
DECLARATION +
|
|
92
|
+
'|' +
|
|
93
|
+
CDATA +
|
|
94
|
+
')'
|
|
95
|
+
|
|
96
|
+
const ESCAPED_CHAR = '\\\\' + ESCAPABLE
|
|
97
|
+
|
|
98
|
+
// Every regex handed to `match()` is sticky: it must match at `pos`, and
|
|
99
|
+
// `lastIndex` then gives the new position. (The equivalent of anchoring with
|
|
100
|
+
// `^` on a sliced subject, without the slicing.)
|
|
101
|
+
const reHtmlTag = new RegExp(HTMLTAG, 'iy')
|
|
102
|
+
const reEntityHere = new RegExp(ENTITY, 'iy')
|
|
103
|
+
const reEscapable = new RegExp('^' + ESCAPABLE)
|
|
104
|
+
|
|
105
|
+
const reTicksHere = /`+/y
|
|
106
|
+
const reTicks = /`+/g
|
|
107
|
+
|
|
108
|
+
// Chunks of ordinary text: everything up to the next character that could
|
|
109
|
+
// start an inline construct. `~` is only special under GFM, hence two.
|
|
110
|
+
const reMain = /[^\n`[\]\\!<&*_]+/y
|
|
111
|
+
const reMainGfm = /[^\n`[\]\\!<&*_~]+/y
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Maximum `(` nesting inside a bare link destination. The spec describes
|
|
115
|
+
* balanced parentheses without stating a bound; cmark uses 32, and the bound
|
|
116
|
+
* is what keeps an unbalanced run from being quadratic (see
|
|
117
|
+
* parseLinkDestination).
|
|
118
|
+
*/
|
|
119
|
+
const MAX_LINK_PAREN_NESTING = 32
|
|
120
|
+
|
|
121
|
+
const reSpnl = / *(?:\n *)?/y
|
|
122
|
+
const reInitialSpace = / */y
|
|
123
|
+
const reIndent = / {0,3}/y
|
|
124
|
+
const reFinalSpace = / *$/
|
|
125
|
+
const reSpaceAtEndOfLine = / *(?:\n|$)/y
|
|
126
|
+
const reWhitespaceChar = /[ \t\n\v\f\r]/
|
|
127
|
+
|
|
128
|
+
// §4.7: at most 999 characters between the brackets, brackets inside only if
|
|
129
|
+
// backslash-escaped. `s` so that an escaped line ending counts as an escape.
|
|
130
|
+
const reLinkLabel = /\[(?:[^\\[\]]|\\.){0,1000}\]/sy
|
|
131
|
+
|
|
132
|
+
// §6.3 link destination, pointy-bracket form: no line endings, no unescaped
|
|
133
|
+
// `<` or `>`.
|
|
134
|
+
const reLinkDestinationBraces = /<(?:[^<>\n\\\x00]|\\.)*>/y
|
|
135
|
+
|
|
136
|
+
// §6.3 link title in `"`, `'` or `(...)` delimiters. The escape alternative
|
|
137
|
+
// comes first so that `\"` inside a double-quoted title is consumed as an
|
|
138
|
+
// escape rather than closing the title.
|
|
139
|
+
const reLinkTitle = new RegExp(
|
|
140
|
+
'(?:"(?:' +
|
|
141
|
+
ESCAPED_CHAR +
|
|
142
|
+
'|[^"\\x00])*"' +
|
|
143
|
+
"|'(?:" +
|
|
144
|
+
ESCAPED_CHAR +
|
|
145
|
+
"|[^'\\x00])*'" +
|
|
146
|
+
'|\\((?:' +
|
|
147
|
+
ESCAPED_CHAR +
|
|
148
|
+
'|[^()\\x00])*\\))',
|
|
149
|
+
'y'
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// §6.5 autolinks. Scheme: an ASCII letter then 1–31 more of letter/digit/
|
|
153
|
+
// `+`/`.`/`-`, i.e. 2–32 characters in total, case-insensitive.
|
|
154
|
+
const reAutolink = /<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>/y
|
|
155
|
+
const reEmailAutolink =
|
|
156
|
+
/<([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>/y
|
|
157
|
+
|
|
158
|
+
function text(literal: string): MdNode {
|
|
159
|
+
const node = new MdNode('text')
|
|
160
|
+
node.literal = literal
|
|
161
|
+
return node
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// §2.1 punctuation is P* ∪ S*, which `isUnicodePunctuation` already covers.
|
|
165
|
+
// `*$*alpha.` is the case that needs the symbol half: with `$` counted as
|
|
166
|
+
// punctuation the closing `*` is not right-flanking, so the line stays
|
|
167
|
+
// literal text.
|
|
168
|
+
const isPunctuation = isUnicodePunctuation
|
|
169
|
+
|
|
170
|
+
/** One `*`, `_` or `~` run on the delimiter stack (§6.4). */
|
|
171
|
+
type Delimiter = {
|
|
172
|
+
/** Character code of the delimiter character. */
|
|
173
|
+
cc: number
|
|
174
|
+
/** Delimiters still unused; drops as pairs are consumed. */
|
|
175
|
+
numdelims: number
|
|
176
|
+
/** Run length as originally scanned — what the rule of three is computed on. */
|
|
177
|
+
origdelims: number
|
|
178
|
+
node: MdNode
|
|
179
|
+
previous: Delimiter | null
|
|
180
|
+
next: Delimiter | null
|
|
181
|
+
canOpen: boolean
|
|
182
|
+
canClose: boolean
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** One `[` or `![` on the bracket stack (§6.3). */
|
|
186
|
+
type Bracket = {
|
|
187
|
+
node: MdNode
|
|
188
|
+
previous: Bracket | null
|
|
189
|
+
/** Top of the delimiter stack when this bracket opened — the emphasis floor. */
|
|
190
|
+
previousDelimiter: Delimiter | null
|
|
191
|
+
/** Offset of the `[` in the subject. */
|
|
192
|
+
index: number
|
|
193
|
+
image: boolean
|
|
194
|
+
/** Cleared on earlier openers once a link is built: no links in links. */
|
|
195
|
+
active: boolean
|
|
196
|
+
/** Another bracket opened after this one, so its text contains a `[`. */
|
|
197
|
+
bracketAfter: boolean
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type DelimScan = { numdelims: number; canOpen: boolean; canClose: boolean }
|
|
201
|
+
|
|
202
|
+
class InlineParser {
|
|
203
|
+
subject = ''
|
|
204
|
+
pos = 0
|
|
205
|
+
delimiters: Delimiter | null = null
|
|
206
|
+
brackets: Bracket | null = null
|
|
207
|
+
refmap: RefMap
|
|
208
|
+
options: ParserOptions
|
|
209
|
+
|
|
210
|
+
constructor(refmap: RefMap, options: ParserOptions) {
|
|
211
|
+
this.refmap = refmap
|
|
212
|
+
this.options = options
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- scanning primitives ---
|
|
216
|
+
|
|
217
|
+
/** Character code at `pos`, or -1 at the end of the subject. */
|
|
218
|
+
peek(): number {
|
|
219
|
+
return this.pos < this.subject.length ? this.subject.charCodeAt(this.pos) : -1
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Match a sticky regex at `pos`; on success advance past it. */
|
|
223
|
+
match(re: RegExp): string | null {
|
|
224
|
+
re.lastIndex = this.pos
|
|
225
|
+
const m = re.exec(this.subject)
|
|
226
|
+
if (null === m) return null
|
|
227
|
+
this.pos = re.lastIndex
|
|
228
|
+
return m[0]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Spaces, and at most one line ending, between the parts of a link. */
|
|
232
|
+
spnl(): void {
|
|
233
|
+
this.match(reSpnl)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The whole code point ending at `end`, so an astral character is
|
|
238
|
+
* classified as one character by the flanking rules rather than as a lone
|
|
239
|
+
* surrogate. Start of subject counts as a line ending (§6.4).
|
|
240
|
+
*/
|
|
241
|
+
charBefore(end: number): string {
|
|
242
|
+
if (end <= 0) return '\n'
|
|
243
|
+
const lo = this.subject.charCodeAt(end - 1)
|
|
244
|
+
if (lo >= 0xdc00 && lo <= 0xdfff && end >= 2) {
|
|
245
|
+
const hi = this.subject.charCodeAt(end - 2)
|
|
246
|
+
if (hi >= 0xd800 && hi <= 0xdbff) return this.subject.slice(end - 2, end)
|
|
247
|
+
}
|
|
248
|
+
return this.subject.charAt(end - 1)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The whole code point at `i`; end of subject counts as a line ending. */
|
|
252
|
+
charAtPos(i: number): string {
|
|
253
|
+
if (i >= this.subject.length) return '\n'
|
|
254
|
+
const cp = this.subject.codePointAt(i)
|
|
255
|
+
return undefined === cp ? '\n' : String.fromCodePoint(cp)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- §6.3 code spans ---
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A code span runs from a backtick string to the next backtick string of
|
|
262
|
+
* *exactly* the same length. Unmatched opening backticks are literal text.
|
|
263
|
+
*/
|
|
264
|
+
parseBackticks(block: MdNode): boolean {
|
|
265
|
+
const ticks = this.match(reTicksHere)
|
|
266
|
+
if (null === ticks) return false
|
|
267
|
+
|
|
268
|
+
const afterOpenTicks = this.pos
|
|
269
|
+
reTicks.lastIndex = this.pos
|
|
270
|
+
let m: RegExpExecArray | null
|
|
271
|
+
|
|
272
|
+
while (null !== (m = reTicks.exec(this.subject))) {
|
|
273
|
+
this.pos = reTicks.lastIndex
|
|
274
|
+
if (m[0] === ticks) {
|
|
275
|
+
const node = new MdNode('code')
|
|
276
|
+
// Line endings become spaces; then one leading and one trailing
|
|
277
|
+
// space are stripped together, but only if the content is not all
|
|
278
|
+
// spaces (which is what lets a code span hold backticks).
|
|
279
|
+
const contents = this.subject
|
|
280
|
+
.slice(afterOpenTicks, this.pos - ticks.length)
|
|
281
|
+
.replace(/\n/g, ' ')
|
|
282
|
+
node.literal =
|
|
283
|
+
contents.length > 2 &&
|
|
284
|
+
' ' === contents[0] &&
|
|
285
|
+
' ' === contents[contents.length - 1] &&
|
|
286
|
+
null !== /[^ ]/.exec(contents)
|
|
287
|
+
? contents.slice(1, contents.length - 1)
|
|
288
|
+
: contents
|
|
289
|
+
block.appendChild(node)
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// No closing run of the same length: the opener is literal text.
|
|
295
|
+
this.pos = afterOpenTicks
|
|
296
|
+
block.appendChild(text(ticks))
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- §6.1 backslash escapes, §6.7 hard line breaks ---
|
|
301
|
+
|
|
302
|
+
parseBackslash(block: MdNode): boolean {
|
|
303
|
+
this.pos += 1
|
|
304
|
+
if (C_NEWLINE === this.peek()) {
|
|
305
|
+
this.pos += 1
|
|
306
|
+
block.appendChild(new MdNode('linebreak'))
|
|
307
|
+
this.match(reInitialSpace)
|
|
308
|
+
} else if (reEscapable.test(this.subject.charAt(this.pos))) {
|
|
309
|
+
block.appendChild(text(this.subject.charAt(this.pos)))
|
|
310
|
+
this.pos += 1
|
|
311
|
+
} else {
|
|
312
|
+
block.appendChild(text('\\'))
|
|
313
|
+
}
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- §6.5 autolinks ---
|
|
318
|
+
|
|
319
|
+
parseAutolink(block: MdNode): boolean {
|
|
320
|
+
let m = this.match(reEmailAutolink)
|
|
321
|
+
if (null !== m) {
|
|
322
|
+
const dest = m.slice(1, m.length - 1)
|
|
323
|
+
block.appendChild(this.makeAutolink('mailto:' + dest, dest))
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
m = this.match(reAutolink)
|
|
327
|
+
if (null !== m) {
|
|
328
|
+
const dest = m.slice(1, m.length - 1)
|
|
329
|
+
block.appendChild(this.makeAutolink(dest, dest))
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
return false
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
makeAutolink(destination: string, label: string): MdNode {
|
|
336
|
+
// Autolink text is literal: no backslash escapes, no entity references.
|
|
337
|
+
const node = new MdNode('link')
|
|
338
|
+
// Not percent-encoded here — see parseLinkDestination.
|
|
339
|
+
node.destination = destination
|
|
340
|
+
node.title = null
|
|
341
|
+
node.appendChild(text(label))
|
|
342
|
+
return node
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// --- §6.6 raw HTML ---
|
|
346
|
+
|
|
347
|
+
parseHtmlTag(block: MdNode): boolean {
|
|
348
|
+
const m = this.match(reHtmlTag)
|
|
349
|
+
if (null === m) return false
|
|
350
|
+
const node = new MdNode('html_inline')
|
|
351
|
+
node.literal = m
|
|
352
|
+
block.appendChild(node)
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- §6.4 emphasis: delimiter runs and flanking ---
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Classify the run of `cc` starting at `pos` without consuming it.
|
|
360
|
+
*
|
|
361
|
+
* §6.4: a run is left-flanking iff it is not followed by Unicode
|
|
362
|
+
* whitespace and either not followed by Unicode punctuation or else
|
|
363
|
+
* preceded by whitespace or punctuation; right-flanking is the mirror
|
|
364
|
+
* image. `_` additionally may not open inside a word (rules 2, 4, 6, 8),
|
|
365
|
+
* which is what keeps `snake_case_words` intact; `*` has no such
|
|
366
|
+
* restriction.
|
|
367
|
+
*/
|
|
368
|
+
scanDelims(cc: number): DelimScan | null {
|
|
369
|
+
const startpos = this.pos
|
|
370
|
+
let numdelims = 0
|
|
371
|
+
|
|
372
|
+
while (cc === this.peek()) {
|
|
373
|
+
numdelims += 1
|
|
374
|
+
this.pos += 1
|
|
375
|
+
}
|
|
376
|
+
if (0 === numdelims) return null
|
|
377
|
+
|
|
378
|
+
const charBefore = this.charBefore(startpos)
|
|
379
|
+
const charAfter = this.charAtPos(this.pos)
|
|
380
|
+
|
|
381
|
+
const afterIsWhitespace = isUnicodeWhitespace(charAfter)
|
|
382
|
+
const afterIsPunctuation = isPunctuation(charAfter)
|
|
383
|
+
const beforeIsWhitespace = isUnicodeWhitespace(charBefore)
|
|
384
|
+
const beforeIsPunctuation = isPunctuation(charBefore)
|
|
385
|
+
|
|
386
|
+
const leftFlanking =
|
|
387
|
+
!afterIsWhitespace &&
|
|
388
|
+
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation)
|
|
389
|
+
const rightFlanking =
|
|
390
|
+
!beforeIsWhitespace &&
|
|
391
|
+
(!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation)
|
|
392
|
+
|
|
393
|
+
let canOpen: boolean
|
|
394
|
+
let canClose: boolean
|
|
395
|
+
if (C_UNDERSCORE === cc) {
|
|
396
|
+
canOpen = leftFlanking && (!rightFlanking || beforeIsPunctuation)
|
|
397
|
+
canClose = rightFlanking && (!leftFlanking || afterIsPunctuation)
|
|
398
|
+
} else {
|
|
399
|
+
canOpen = leftFlanking
|
|
400
|
+
canClose = rightFlanking
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.pos = startpos
|
|
404
|
+
return { numdelims, canOpen, canClose }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Emit a delimiter run as text and push it on the delimiter stack. */
|
|
408
|
+
handleDelim(cc: number, block: MdNode): boolean {
|
|
409
|
+
const res = this.scanDelims(cc)
|
|
410
|
+
if (null === res) return false
|
|
411
|
+
|
|
412
|
+
const numdelims = res.numdelims
|
|
413
|
+
const startpos = this.pos
|
|
414
|
+
this.pos += numdelims
|
|
415
|
+
|
|
416
|
+
const node = text(this.subject.slice(startpos, this.pos))
|
|
417
|
+
block.appendChild(node)
|
|
418
|
+
|
|
419
|
+
// GFM strikethrough only recognises runs of one or two tildes; longer
|
|
420
|
+
// runs stay literal.
|
|
421
|
+
const stackable = C_TILDE === cc ? numdelims <= 2 : true
|
|
422
|
+
|
|
423
|
+
if ((res.canOpen || res.canClose) && stackable) {
|
|
424
|
+
const delim: Delimiter = {
|
|
425
|
+
cc,
|
|
426
|
+
numdelims,
|
|
427
|
+
origdelims: numdelims,
|
|
428
|
+
node,
|
|
429
|
+
previous: this.delimiters,
|
|
430
|
+
next: null,
|
|
431
|
+
canOpen: res.canOpen,
|
|
432
|
+
canClose: res.canClose,
|
|
433
|
+
}
|
|
434
|
+
if (null !== delim.previous) delim.previous.next = delim
|
|
435
|
+
this.delimiters = delim
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return true
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
removeDelimiter(delim: Delimiter): void {
|
|
442
|
+
if (null !== delim.previous) delim.previous.next = delim.next
|
|
443
|
+
if (null === delim.next) {
|
|
444
|
+
// Top of stack.
|
|
445
|
+
this.delimiters = delim.previous
|
|
446
|
+
} else {
|
|
447
|
+
delim.next.previous = delim.previous
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The spec's "process emphasis" procedure. `stackBottom` is the floor: for
|
|
453
|
+
* the whole-block call it is null, and for the inlines of a link it is the
|
|
454
|
+
* delimiter that was on top when the link's `[` opened.
|
|
455
|
+
*/
|
|
456
|
+
processEmphasis(stackBottom: Delimiter | null): void {
|
|
457
|
+
// openers_bottom, keyed by (delimiter char, whether the closer can also
|
|
458
|
+
// open, closer run length mod 3). Once a closer of some class fails to
|
|
459
|
+
// find an opener, no later closer of the same class needs to look past
|
|
460
|
+
// that point either — this is what keeps the search linear.
|
|
461
|
+
const openersBottom = new Map<string, Delimiter | null>()
|
|
462
|
+
|
|
463
|
+
// First closer above stackBottom.
|
|
464
|
+
let closer = this.delimiters
|
|
465
|
+
while (null !== closer && closer.previous !== stackBottom) {
|
|
466
|
+
closer = closer.previous
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
while (null !== closer) {
|
|
470
|
+
if (!closer.canClose) {
|
|
471
|
+
closer = closer.next
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const closercc = closer.cc
|
|
476
|
+
const key = closercc + ':' + (closer.canOpen ? 1 : 0) + ':' + (closer.origdelims % 3)
|
|
477
|
+
const bottom = openersBottom.has(key)
|
|
478
|
+
? (openersBottom.get(key) as Delimiter | null)
|
|
479
|
+
: stackBottom
|
|
480
|
+
|
|
481
|
+
let opener = closer.previous
|
|
482
|
+
let openerFound = false
|
|
483
|
+
while (null !== opener && opener !== stackBottom && opener !== bottom) {
|
|
484
|
+
if (opener.cc === closercc && opener.canOpen) {
|
|
485
|
+
if (C_TILDE === closercc) {
|
|
486
|
+
// GFM strikethrough: opener and closer runs must be the same
|
|
487
|
+
// length, so `~~x~` is not a match.
|
|
488
|
+
if (opener.numdelims === closer.numdelims) {
|
|
489
|
+
openerFound = true
|
|
490
|
+
break
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
// §6.4 rules 9 and 10, the "rule of three": when either side of
|
|
494
|
+
// the pair could both open and close, the two run lengths may
|
|
495
|
+
// not sum to a multiple of 3 — unless both are themselves
|
|
496
|
+
// multiples of 3.
|
|
497
|
+
const oddMatch =
|
|
498
|
+
(closer.canOpen || opener.canClose) &&
|
|
499
|
+
0 !== closer.origdelims % 3 &&
|
|
500
|
+
0 === (opener.origdelims + closer.origdelims) % 3
|
|
501
|
+
if (!oddMatch) {
|
|
502
|
+
openerFound = true
|
|
503
|
+
break
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
opener = opener.previous
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const oldCloser = closer
|
|
511
|
+
|
|
512
|
+
if (openerFound && null !== opener) {
|
|
513
|
+
// Two delimiters make strong, one makes emph; strikethrough always
|
|
514
|
+
// consumes the whole (equal-length) run.
|
|
515
|
+
const useDelims =
|
|
516
|
+
C_TILDE === closercc
|
|
517
|
+
? closer.numdelims
|
|
518
|
+
: closer.numdelims >= 2 && opener.numdelims >= 2
|
|
519
|
+
? 2
|
|
520
|
+
: 1
|
|
521
|
+
const nodeType: NodeType =
|
|
522
|
+
C_TILDE === closercc ? 'del' : 1 === useDelims ? 'emph' : 'strong'
|
|
523
|
+
|
|
524
|
+
const openerInl = opener.node
|
|
525
|
+
const closerInl = closer.node
|
|
526
|
+
|
|
527
|
+
opener.numdelims -= useDelims
|
|
528
|
+
closer.numdelims -= useDelims
|
|
529
|
+
openerInl.literal = (openerInl.literal ?? '').slice(
|
|
530
|
+
0,
|
|
531
|
+
(openerInl.literal ?? '').length - useDelims
|
|
532
|
+
)
|
|
533
|
+
closerInl.literal = (closerInl.literal ?? '').slice(
|
|
534
|
+
0,
|
|
535
|
+
(closerInl.literal ?? '').length - useDelims
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
// Everything between the two delimiter text nodes becomes the
|
|
539
|
+
// content of the new node.
|
|
540
|
+
const emph = new MdNode(nodeType)
|
|
541
|
+
let tmp = openerInl.next
|
|
542
|
+
while (null !== tmp && tmp !== closerInl) {
|
|
543
|
+
const nxt = tmp.next
|
|
544
|
+
tmp.unlink()
|
|
545
|
+
emph.appendChild(tmp)
|
|
546
|
+
tmp = nxt
|
|
547
|
+
}
|
|
548
|
+
openerInl.insertAfter(emph)
|
|
549
|
+
|
|
550
|
+
// Delimiters between the pair can never match anything now.
|
|
551
|
+
if (opener.next !== closer) {
|
|
552
|
+
opener.next = closer
|
|
553
|
+
closer.previous = opener
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (0 === opener.numdelims) {
|
|
557
|
+
openerInl.unlink()
|
|
558
|
+
this.removeDelimiter(opener)
|
|
559
|
+
}
|
|
560
|
+
if (0 === closer.numdelims) {
|
|
561
|
+
closerInl.unlink()
|
|
562
|
+
const tempstack = closer.next
|
|
563
|
+
this.removeDelimiter(closer)
|
|
564
|
+
closer = tempstack
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
closer = closer.next
|
|
568
|
+
openersBottom.set(key, oldCloser.previous)
|
|
569
|
+
if (!oldCloser.canOpen) {
|
|
570
|
+
// It found no opener and cannot be one: it is inert.
|
|
571
|
+
this.removeDelimiter(oldCloser)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Drop everything above the floor.
|
|
577
|
+
while (null !== this.delimiters && this.delimiters !== stackBottom) {
|
|
578
|
+
this.removeDelimiter(this.delimiters)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// --- §6.3 links and images ---
|
|
583
|
+
|
|
584
|
+
/** Link title without its delimiters, unescaped; null if none here. */
|
|
585
|
+
parseLinkTitle(): string | null {
|
|
586
|
+
const title = this.match(reLinkTitle)
|
|
587
|
+
if (null === title) return null
|
|
588
|
+
return unescapeString(title.slice(1, title.length - 1))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Link destination: either `<...>`, or a bare run with balanced unescaped
|
|
593
|
+
* parentheses and no ASCII control characters or spaces.
|
|
594
|
+
*/
|
|
595
|
+
/**
|
|
596
|
+
* A destination is backslash-unescaped and entity-decoded, but **not**
|
|
597
|
+
* percent-encoded: `[l](/ä)` yields `/ä`, not `/%C3%A4`.
|
|
598
|
+
*
|
|
599
|
+
* Encoding is a rendering concern and `html.ts` applies `normalizeURI` when
|
|
600
|
+
* it writes the attribute. Doing it here too would be invisible in the HTML
|
|
601
|
+
* (`normalizeURI` is idempotent over its own output) but would put the
|
|
602
|
+
* encoded form in the public AST, where mdast — and every previous release
|
|
603
|
+
* of this package — promises the decoded one.
|
|
604
|
+
*/
|
|
605
|
+
parseLinkDestination(): string | null {
|
|
606
|
+
const braced = this.match(reLinkDestinationBraces)
|
|
607
|
+
if (null !== braced) {
|
|
608
|
+
return unescapeString(braced.slice(1, braced.length - 1))
|
|
609
|
+
}
|
|
610
|
+
if (C_LESSTHAN === this.peek()) return null
|
|
611
|
+
|
|
612
|
+
const savepos = this.pos
|
|
613
|
+
let openparens = 0
|
|
614
|
+
let c = this.peek()
|
|
615
|
+
|
|
616
|
+
while (-1 !== c) {
|
|
617
|
+
if (C_BACKSLASH === c && reEscapable.test(this.subject.charAt(this.pos + 1))) {
|
|
618
|
+
this.pos += 1
|
|
619
|
+
if (-1 !== this.peek()) this.pos += 1
|
|
620
|
+
} else if (C_OPEN_PAREN === c) {
|
|
621
|
+
// Bail rather than scan on once nesting passes the limit. Without
|
|
622
|
+
// this, input like `[a](b` repeated makes every `]` scan to end of
|
|
623
|
+
// input looking for parens that never balance, which is quadratic in
|
|
624
|
+
// the document size. cmark caps nesting at the same depth, and no
|
|
625
|
+
// spec example nests more than two deep.
|
|
626
|
+
if (MAX_LINK_PAREN_NESTING <= openparens) return null
|
|
627
|
+
this.pos += 1
|
|
628
|
+
openparens += 1
|
|
629
|
+
} else if (C_CLOSE_PAREN === c) {
|
|
630
|
+
if (openparens < 1) break
|
|
631
|
+
this.pos += 1
|
|
632
|
+
openparens -= 1
|
|
633
|
+
} else if (c <= C_SPACE || C_DEL === c) {
|
|
634
|
+
// Spaces and ASCII control characters end the bare form.
|
|
635
|
+
break
|
|
636
|
+
} else {
|
|
637
|
+
this.pos += 1
|
|
638
|
+
}
|
|
639
|
+
c = this.peek()
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// An empty destination is only legal immediately before the closing
|
|
643
|
+
// paren of an inline link, as in `[link]()`.
|
|
644
|
+
if (this.pos === savepos && C_CLOSE_PAREN !== c) return null
|
|
645
|
+
if (0 !== openparens) return null
|
|
646
|
+
|
|
647
|
+
return unescapeString(this.subject.slice(savepos, this.pos))
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Length of the link label at `pos`, including brackets; 0 if invalid. */
|
|
651
|
+
parseLinkLabel(): number {
|
|
652
|
+
const m = this.match(reLinkLabel)
|
|
653
|
+
// §4.7: at most 999 characters between the brackets.
|
|
654
|
+
if (null === m || m.length > 1001) return 0
|
|
655
|
+
return m.length
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
addBracket(node: MdNode, index: number, image: boolean): void {
|
|
659
|
+
if (null !== this.brackets) this.brackets.bracketAfter = true
|
|
660
|
+
this.brackets = {
|
|
661
|
+
node,
|
|
662
|
+
previous: this.brackets,
|
|
663
|
+
previousDelimiter: this.delimiters,
|
|
664
|
+
index,
|
|
665
|
+
image,
|
|
666
|
+
active: true,
|
|
667
|
+
bracketAfter: false,
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
removeBracket(): void {
|
|
672
|
+
if (null !== this.brackets) this.brackets = this.brackets.previous
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
parseOpenBracket(block: MdNode): boolean {
|
|
676
|
+
const startpos = this.pos
|
|
677
|
+
this.pos += 1
|
|
678
|
+
const node = text('[')
|
|
679
|
+
block.appendChild(node)
|
|
680
|
+
this.addBracket(node, startpos, false)
|
|
681
|
+
return true
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** `!` only matters when it introduces an image. */
|
|
685
|
+
parseBang(block: MdNode): boolean {
|
|
686
|
+
const startpos = this.pos
|
|
687
|
+
this.pos += 1
|
|
688
|
+
if (C_OPEN_BRACKET === this.peek()) {
|
|
689
|
+
this.pos += 1
|
|
690
|
+
const node = text('![')
|
|
691
|
+
block.appendChild(node)
|
|
692
|
+
this.addBracket(node, startpos + 1, true)
|
|
693
|
+
} else {
|
|
694
|
+
block.appendChild(text('!'))
|
|
695
|
+
}
|
|
696
|
+
return true
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The spec's "look for link or image" procedure. On a `]`, walk back to
|
|
701
|
+
* the newest bracket opener and try, in order: an inline `(dest "title")`,
|
|
702
|
+
* a full `[label]`, a collapsed `[]` or a shortcut reference. Anything that
|
|
703
|
+
* fails backtracks to just after the `]` and leaves a literal `]`.
|
|
704
|
+
*/
|
|
705
|
+
parseCloseBracket(block: MdNode): boolean {
|
|
706
|
+
this.pos += 1
|
|
707
|
+
const startpos = this.pos
|
|
708
|
+
|
|
709
|
+
const opener = this.brackets
|
|
710
|
+
if (null === opener) {
|
|
711
|
+
block.appendChild(text(']'))
|
|
712
|
+
return true
|
|
713
|
+
}
|
|
714
|
+
if (!opener.active) {
|
|
715
|
+
// Deactivated by an enclosing link: not an opener any more.
|
|
716
|
+
block.appendChild(text(']'))
|
|
717
|
+
this.removeBracket()
|
|
718
|
+
return true
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const isImage = opener.image
|
|
722
|
+
let dest: string | null = null
|
|
723
|
+
let title: string | null = null
|
|
724
|
+
let matched = false
|
|
725
|
+
|
|
726
|
+
const savepos = this.pos
|
|
727
|
+
|
|
728
|
+
// Inline link: `](` destination title `)`.
|
|
729
|
+
if (C_OPEN_PAREN === this.peek()) {
|
|
730
|
+
this.pos += 1
|
|
731
|
+
this.spnl()
|
|
732
|
+
const d = this.parseLinkDestination()
|
|
733
|
+
if (null !== d) {
|
|
734
|
+
this.spnl()
|
|
735
|
+
// A title has to be separated from the destination by whitespace,
|
|
736
|
+
// so `[a](/url"t")` is not a titled link.
|
|
737
|
+
let t: string | null = null
|
|
738
|
+
if (reWhitespaceChar.test(this.subject.charAt(this.pos - 1))) {
|
|
739
|
+
t = this.parseLinkTitle()
|
|
740
|
+
}
|
|
741
|
+
this.spnl()
|
|
742
|
+
if (C_CLOSE_PAREN === this.peek()) {
|
|
743
|
+
this.pos += 1
|
|
744
|
+
dest = d
|
|
745
|
+
title = t
|
|
746
|
+
matched = true
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!matched) this.pos = savepos
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (!matched) {
|
|
753
|
+
// Reference forms. A second label gives the full form; an empty or
|
|
754
|
+
// absent one means the link text is itself the label (collapsed and
|
|
755
|
+
// shortcut) — which is only possible if that text holds no bracket.
|
|
756
|
+
let reflabel: string | null = null
|
|
757
|
+
const beforelabel = this.pos
|
|
758
|
+
const n = this.parseLinkLabel()
|
|
759
|
+
if (n > 2) {
|
|
760
|
+
reflabel = this.subject.slice(beforelabel + 1, beforelabel + n - 1)
|
|
761
|
+
} else if (!opener.bracketAfter) {
|
|
762
|
+
reflabel = this.subject.slice(opener.index + 1, startpos - 1)
|
|
763
|
+
}
|
|
764
|
+
if (0 === n) this.pos = savepos
|
|
765
|
+
|
|
766
|
+
if (null !== reflabel) {
|
|
767
|
+
const link = this.refmap[normalizeReference(reflabel)]
|
|
768
|
+
if (undefined !== link) {
|
|
769
|
+
dest = link.destination
|
|
770
|
+
title = link.title
|
|
771
|
+
matched = true
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!matched) {
|
|
777
|
+
this.removeBracket()
|
|
778
|
+
this.pos = startpos
|
|
779
|
+
block.appendChild(text(']'))
|
|
780
|
+
return true
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const node = new MdNode(isImage ? 'image' : 'link')
|
|
784
|
+
node.destination = dest
|
|
785
|
+
node.title = null === title || '' === title ? null : title
|
|
786
|
+
|
|
787
|
+
let tmp = opener.node.next
|
|
788
|
+
while (null !== tmp) {
|
|
789
|
+
const nxt = tmp.next
|
|
790
|
+
tmp.unlink()
|
|
791
|
+
node.appendChild(tmp)
|
|
792
|
+
tmp = nxt
|
|
793
|
+
}
|
|
794
|
+
block.appendChild(node)
|
|
795
|
+
|
|
796
|
+
// Emphasis inside the link text resolves now, and only down to the
|
|
797
|
+
// delimiter that was current when the `[` opened.
|
|
798
|
+
this.processEmphasis(opener.previousDelimiter)
|
|
799
|
+
this.removeBracket()
|
|
800
|
+
opener.node.unlink()
|
|
801
|
+
|
|
802
|
+
// Links may not contain links (§6.3): every earlier link opener is
|
|
803
|
+
// spent. Image openers survive, since images may nest in links.
|
|
804
|
+
if (!isImage) {
|
|
805
|
+
let b = this.brackets
|
|
806
|
+
while (null !== b) {
|
|
807
|
+
if (!b.image) b.active = false
|
|
808
|
+
b = b.previous
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return true
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// --- §6.2 entities, §6.8/§6.9 breaks and plain text ---
|
|
816
|
+
|
|
817
|
+
parseEntity(block: MdNode): boolean {
|
|
818
|
+
const m = this.match(reEntityHere)
|
|
819
|
+
if (null === m) return false
|
|
820
|
+
block.appendChild(text(decodeEntity(m)))
|
|
821
|
+
return true
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
parseString(block: MdNode): boolean {
|
|
825
|
+
const m = this.match(this.options.gfm ? reMainGfm : reMain)
|
|
826
|
+
if (null === m) return false
|
|
827
|
+
block.appendChild(text(m))
|
|
828
|
+
return true
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* §6.7/§6.8: two or more spaces before the line ending make a hard break,
|
|
833
|
+
* anything else a soft one. Trailing spaces are dropped either way, as is
|
|
834
|
+
* the indentation of the next line.
|
|
835
|
+
*/
|
|
836
|
+
parseNewline(block: MdNode): boolean {
|
|
837
|
+
this.pos += 1
|
|
838
|
+
const lastc = block.lastChild
|
|
839
|
+
if (
|
|
840
|
+
null !== lastc &&
|
|
841
|
+
'text' === lastc.type &&
|
|
842
|
+
null !== lastc.literal &&
|
|
843
|
+
' ' === lastc.literal.charAt(lastc.literal.length - 1)
|
|
844
|
+
) {
|
|
845
|
+
const hardbreak = ' ' === lastc.literal.charAt(lastc.literal.length - 2)
|
|
846
|
+
lastc.literal = lastc.literal.replace(reFinalSpace, '')
|
|
847
|
+
block.appendChild(new MdNode(hardbreak ? 'linebreak' : 'softbreak'))
|
|
848
|
+
} else {
|
|
849
|
+
block.appendChild(new MdNode('softbreak'))
|
|
850
|
+
}
|
|
851
|
+
this.match(reInitialSpace)
|
|
852
|
+
return true
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** One inline construct at `pos`; false only at the end of the subject. */
|
|
856
|
+
parseInline(block: MdNode): boolean {
|
|
857
|
+
const c = this.peek()
|
|
858
|
+
if (-1 === c) return false
|
|
859
|
+
|
|
860
|
+
let res = false
|
|
861
|
+
switch (c) {
|
|
862
|
+
case C_NEWLINE:
|
|
863
|
+
res = this.parseNewline(block)
|
|
864
|
+
break
|
|
865
|
+
case C_BACKSLASH:
|
|
866
|
+
res = this.parseBackslash(block)
|
|
867
|
+
break
|
|
868
|
+
case C_BACKTICK:
|
|
869
|
+
res = this.parseBackticks(block)
|
|
870
|
+
break
|
|
871
|
+
case C_ASTERISK:
|
|
872
|
+
case C_UNDERSCORE:
|
|
873
|
+
res = this.handleDelim(c, block)
|
|
874
|
+
break
|
|
875
|
+
case C_TILDE:
|
|
876
|
+
// Only GFM gives `~` a meaning; otherwise it is ordinary text (and
|
|
877
|
+
// the non-GFM `reMain` lets parseString swallow whole runs of it).
|
|
878
|
+
res = this.options.gfm ? this.handleDelim(c, block) : this.parseString(block)
|
|
879
|
+
break
|
|
880
|
+
case C_OPEN_BRACKET:
|
|
881
|
+
res = this.parseOpenBracket(block)
|
|
882
|
+
break
|
|
883
|
+
case C_BANG:
|
|
884
|
+
res = this.parseBang(block)
|
|
885
|
+
break
|
|
886
|
+
case C_CLOSE_BRACKET:
|
|
887
|
+
res = this.parseCloseBracket(block)
|
|
888
|
+
break
|
|
889
|
+
case C_LESSTHAN:
|
|
890
|
+
res = this.parseAutolink(block) || this.parseHtmlTag(block)
|
|
891
|
+
break
|
|
892
|
+
case C_AMPERSAND:
|
|
893
|
+
res = this.parseEntity(block)
|
|
894
|
+
break
|
|
895
|
+
default:
|
|
896
|
+
res = this.parseString(block)
|
|
897
|
+
break
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (!res) {
|
|
901
|
+
// Nothing claimed it: the character is literal text.
|
|
902
|
+
this.pos += 1
|
|
903
|
+
block.appendChild(text(String.fromCharCode(c)))
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return true
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Parse one paragraph or heading's raw content into inline children. */
|
|
910
|
+
parse(block: MdNode): void {
|
|
911
|
+
// Trimming is what stops trailing spaces at the end of a block from
|
|
912
|
+
// making a hard break, and drops the leading indentation of the first line.
|
|
913
|
+
this.subject = block.stringContent.trim()
|
|
914
|
+
this.pos = 0
|
|
915
|
+
this.delimiters = null
|
|
916
|
+
this.brackets = null
|
|
917
|
+
|
|
918
|
+
while (this.parseInline(block));
|
|
919
|
+
|
|
920
|
+
block.stringContent = ''
|
|
921
|
+
this.processEmphasis(null)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* §4.7: consume leading link reference definitions. Called by the block
|
|
926
|
+
* phase while finalizing a paragraph, repeatedly, until it returns 0.
|
|
927
|
+
*/
|
|
928
|
+
parseReference(s: string): number {
|
|
929
|
+
this.subject = s
|
|
930
|
+
this.pos = 0
|
|
931
|
+
|
|
932
|
+
const startpos = this.pos
|
|
933
|
+
|
|
934
|
+
// §4.7 allows up to three spaces of indentation before the label. The
|
|
935
|
+
// block phase normally strips a paragraph line's indentation before it
|
|
936
|
+
// gets here, so this usually consumes nothing.
|
|
937
|
+
this.match(reIndent)
|
|
938
|
+
|
|
939
|
+
const labelStart = this.pos
|
|
940
|
+
const matchChars = this.parseLinkLabel()
|
|
941
|
+
if (0 === matchChars) return 0
|
|
942
|
+
const rawlabel = this.subject.slice(labelStart + 1, labelStart + matchChars - 1)
|
|
943
|
+
|
|
944
|
+
if (C_COLON === this.peek()) {
|
|
945
|
+
this.pos += 1
|
|
946
|
+
} else {
|
|
947
|
+
this.pos = startpos
|
|
948
|
+
return 0
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
this.spnl()
|
|
952
|
+
|
|
953
|
+
const dest = this.parseLinkDestination()
|
|
954
|
+
if (null === dest) {
|
|
955
|
+
this.pos = startpos
|
|
956
|
+
return 0
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const beforetitle = this.pos
|
|
960
|
+
this.spnl()
|
|
961
|
+
let title: string | null = null
|
|
962
|
+
if (this.pos !== beforetitle) {
|
|
963
|
+
title = this.parseLinkTitle()
|
|
964
|
+
}
|
|
965
|
+
if (null === title) {
|
|
966
|
+
this.pos = beforetitle
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Nothing but spaces may follow on the definition's last line.
|
|
970
|
+
let atLineEnd = true
|
|
971
|
+
if (null === this.match(reSpaceAtEndOfLine)) {
|
|
972
|
+
if (null === title) {
|
|
973
|
+
atLineEnd = false
|
|
974
|
+
} else {
|
|
975
|
+
// What looked like a title is something else; the definition may
|
|
976
|
+
// still be valid without it.
|
|
977
|
+
title = null
|
|
978
|
+
this.pos = beforetitle
|
|
979
|
+
atLineEnd = null !== this.match(reSpaceAtEndOfLine)
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (!atLineEnd) {
|
|
983
|
+
this.pos = startpos
|
|
984
|
+
return 0
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const normlabel = normalizeReference(rawlabel)
|
|
988
|
+
if ('' === normlabel) {
|
|
989
|
+
// A label must hold at least one non-whitespace character.
|
|
990
|
+
this.pos = startpos
|
|
991
|
+
return 0
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// First definition wins.
|
|
995
|
+
if (undefined === this.refmap[normlabel]) {
|
|
996
|
+
this.refmap[normlabel] = { destination: dest, title }
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return this.pos - startpos
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Phase 2 entry point: walk the block tree and parse the string content of
|
|
1005
|
+
* every paragraph and heading into inline children.
|
|
1006
|
+
*/
|
|
1007
|
+
export function parseInlines(doc: MdNode, refmap: RefMap, options: ParserOptions): void {
|
|
1008
|
+
const parser = new InlineParser(refmap, options)
|
|
1009
|
+
const walker = doc.walker()
|
|
1010
|
+
|
|
1011
|
+
let ev = walker.next()
|
|
1012
|
+
while (null !== ev) {
|
|
1013
|
+
const node = ev.node
|
|
1014
|
+
if (!ev.entering && ('paragraph' === node.type || 'heading' === node.type)) {
|
|
1015
|
+
parser.parse(node)
|
|
1016
|
+
}
|
|
1017
|
+
ev = walker.next()
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Reference definitions are parsed during the block phase, before any
|
|
1022
|
+
// InlineParser for the document exists, so this one is kept aside for it.
|
|
1023
|
+
// Nothing here depends on the parse options.
|
|
1024
|
+
const referenceParser = new InlineParser({}, { gfm: false, breaks: false })
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Consume link reference definitions from the front of a paragraph's raw
|
|
1028
|
+
* content, adding them to `refmap`. Returns the number of characters
|
|
1029
|
+
* consumed, or 0 if `s` does not start with a definition.
|
|
1030
|
+
*/
|
|
1031
|
+
export function parseReference(s: string, refmap: RefMap): number {
|
|
1032
|
+
referenceParser.refmap = refmap
|
|
1033
|
+
return referenceParser.parseReference(s)
|
|
1034
|
+
}
|