@meith/markdown 0.16.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/LICENSE.md +165 -0
- package/package.json +21 -0
- package/src/attribution.ts +77 -0
- package/src/bbcode.ts +243 -0
- package/src/blocks.ts +459 -0
- package/src/body.ts +88 -0
- package/src/budget.ts +23 -0
- package/src/escape-source.ts +11 -0
- package/src/escape.ts +27 -0
- package/src/extensions.ts +96 -0
- package/src/features.ts +47 -0
- package/src/index.ts +80 -0
- package/src/inline.ts +525 -0
- package/src/limits.ts +15 -0
- package/src/mentions.ts +66 -0
- package/src/nodes.ts +71 -0
- package/src/pipeline.ts +47 -0
- package/src/plain.ts +78 -0
- package/src/quote.ts +47 -0
- package/src/render.ts +194 -0
- package/src/url.ts +31 -0
- package/src/vocabulary.ts +68 -0
- package/src/word-filter.ts +64 -0
package/src/features.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface MarkdownFeatures {
|
|
2
|
+
readonly headings: boolean
|
|
3
|
+
readonly lists: boolean
|
|
4
|
+
readonly quotes: boolean
|
|
5
|
+
readonly code: boolean
|
|
6
|
+
readonly codeSpans: boolean
|
|
7
|
+
readonly tables: boolean
|
|
8
|
+
readonly rules: boolean
|
|
9
|
+
readonly images: boolean
|
|
10
|
+
readonly links: boolean
|
|
11
|
+
readonly emphasis: boolean
|
|
12
|
+
readonly directives: boolean
|
|
13
|
+
readonly mentions: boolean
|
|
14
|
+
readonly attachments: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FULL_FEATURES: MarkdownFeatures = {
|
|
18
|
+
headings: true,
|
|
19
|
+
lists: true,
|
|
20
|
+
quotes: true,
|
|
21
|
+
code: true,
|
|
22
|
+
codeSpans: true,
|
|
23
|
+
tables: true,
|
|
24
|
+
rules: true,
|
|
25
|
+
images: true,
|
|
26
|
+
links: true,
|
|
27
|
+
emphasis: true,
|
|
28
|
+
directives: true,
|
|
29
|
+
mentions: true,
|
|
30
|
+
attachments: true,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const SIGNATURE_FEATURES: MarkdownFeatures = {
|
|
34
|
+
headings: false,
|
|
35
|
+
lists: false,
|
|
36
|
+
quotes: false,
|
|
37
|
+
code: false,
|
|
38
|
+
codeSpans: true,
|
|
39
|
+
tables: false,
|
|
40
|
+
rules: false,
|
|
41
|
+
images: false,
|
|
42
|
+
links: true,
|
|
43
|
+
emphasis: true,
|
|
44
|
+
directives: false,
|
|
45
|
+
mentions: false,
|
|
46
|
+
attachments: false,
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AttributedAuthor,
|
|
3
|
+
memberByNameHref,
|
|
4
|
+
type QuoteAttribution,
|
|
5
|
+
quoteAttribution,
|
|
6
|
+
} from './attribution'
|
|
7
|
+
export { bbcodeToMarkdown } from './bbcode'
|
|
8
|
+
export { type ParseOptions, parse } from './blocks'
|
|
9
|
+
export {
|
|
10
|
+
BodyFormat,
|
|
11
|
+
type MarkdownRenderOptions,
|
|
12
|
+
postBodyHtml,
|
|
13
|
+
RENDER_VERSION,
|
|
14
|
+
type RenderablePost,
|
|
15
|
+
type RenderedBody,
|
|
16
|
+
renderMarkdown,
|
|
17
|
+
sourceAsMarkdown,
|
|
18
|
+
vocabularyOptions,
|
|
19
|
+
} from './body'
|
|
20
|
+
export { escapeAttribute, escapeHtml, unescapeHtml } from './escape'
|
|
21
|
+
export { escapeMarkdownText, plainAuthorName } from './escape-source'
|
|
22
|
+
export {
|
|
23
|
+
type CompiledSmilies,
|
|
24
|
+
compileSmilies,
|
|
25
|
+
createDirectiveRegistry,
|
|
26
|
+
type DirectiveDefinition,
|
|
27
|
+
type DirectiveRegistry,
|
|
28
|
+
directiveNames,
|
|
29
|
+
NO_DIRECTIVES,
|
|
30
|
+
renderSmilies,
|
|
31
|
+
type SmileyDefinition,
|
|
32
|
+
} from './extensions'
|
|
33
|
+
export {
|
|
34
|
+
FULL_FEATURES,
|
|
35
|
+
type MarkdownFeatures,
|
|
36
|
+
SIGNATURE_FEATURES,
|
|
37
|
+
} from './features'
|
|
38
|
+
export { type InlineContext, parseInline } from './inline'
|
|
39
|
+
export { DEFAULT_LIMITS, type MarkdownLimits } from './limits'
|
|
40
|
+
export { extractMentions, extractQuotedAuthors } from './mentions'
|
|
41
|
+
export {
|
|
42
|
+
type Alignment,
|
|
43
|
+
type Block,
|
|
44
|
+
type Inline,
|
|
45
|
+
type ListItem,
|
|
46
|
+
type MarkdownDocument,
|
|
47
|
+
type TableCell,
|
|
48
|
+
textOf,
|
|
49
|
+
} from './nodes'
|
|
50
|
+
export {
|
|
51
|
+
authorRef,
|
|
52
|
+
CORE_RENDERING,
|
|
53
|
+
type MarkdownAuthorRef,
|
|
54
|
+
type MarkdownPipeline,
|
|
55
|
+
type MarkdownRenderContext,
|
|
56
|
+
type MarkdownSourceKind,
|
|
57
|
+
NO_VOCABULARY_SOURCE,
|
|
58
|
+
renderThrough,
|
|
59
|
+
} from './pipeline'
|
|
60
|
+
export { plainText, summarise } from './plain'
|
|
61
|
+
export { type QuoteInput, quoteBlock } from './quote'
|
|
62
|
+
export {
|
|
63
|
+
localizeQuoteAttribution,
|
|
64
|
+
type RenderContext,
|
|
65
|
+
renderDocument,
|
|
66
|
+
renderInline,
|
|
67
|
+
} from './render'
|
|
68
|
+
export { safeImageUrl, safeUrl, type UrlPolicy } from './url'
|
|
69
|
+
export {
|
|
70
|
+
type BoardVocabulary,
|
|
71
|
+
compileVocabulary,
|
|
72
|
+
EMPTY_VOCABULARY,
|
|
73
|
+
type VocabularySource,
|
|
74
|
+
} from './vocabulary'
|
|
75
|
+
export {
|
|
76
|
+
applyWordFilter,
|
|
77
|
+
type CompiledWordFilter,
|
|
78
|
+
compileWordFilter,
|
|
79
|
+
type WordFilterRule,
|
|
80
|
+
} from './word-filter'
|
package/src/inline.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import type { NodeBudget } from './budget'
|
|
2
|
+
import type { MarkdownFeatures } from './features'
|
|
3
|
+
import type { MarkdownLimits } from './limits'
|
|
4
|
+
import type { Inline } from './nodes'
|
|
5
|
+
import { textOf } from './nodes'
|
|
6
|
+
|
|
7
|
+
export interface InlineContext {
|
|
8
|
+
readonly features: MarkdownFeatures
|
|
9
|
+
readonly limits: MarkdownLimits
|
|
10
|
+
readonly directives: ReadonlySet<string>
|
|
11
|
+
readonly budget: NodeBudget
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PUNCTUATION = /[!-#%-*,-/:;?@[-\]_{}\p{P}\p{S}]/u
|
|
15
|
+
|
|
16
|
+
const ESCAPABLE = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
|
|
17
|
+
|
|
18
|
+
const DIRECTIVE_NAME = /^[a-z][a-z0-9]{0,15}$/
|
|
19
|
+
|
|
20
|
+
const BARE_URL = /^(https?:\/\/|www\.)[^\s<]*/i
|
|
21
|
+
|
|
22
|
+
const AUTOLINK = /^<([a-z][a-z0-9+.-]{1,31}:[^\s<>]*)>/i
|
|
23
|
+
const AUTOLINK_EMAIL = /^<([^\s<>@]+@[^\s<>@]+\.[^\s<>@]+)>/
|
|
24
|
+
|
|
25
|
+
const MENTION = /^@([\p{L}\p{N}][\p{L}\p{N}._-]*)/u
|
|
26
|
+
|
|
27
|
+
const MENTION_QUOTED = /^@"([^"\n]+)"/
|
|
28
|
+
|
|
29
|
+
const MENTION_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._-]*$/u
|
|
30
|
+
|
|
31
|
+
const MENTION_MAX = 64
|
|
32
|
+
|
|
33
|
+
const ATTACHMENT = /^\[attachment=(\d{1,10})\]/
|
|
34
|
+
|
|
35
|
+
type Delimiter = {
|
|
36
|
+
readonly t: 'delim'
|
|
37
|
+
readonly char: string
|
|
38
|
+
length: number
|
|
39
|
+
readonly canOpen: boolean
|
|
40
|
+
readonly canClose: boolean
|
|
41
|
+
readonly raw: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Item = { readonly t: 'node'; readonly node: Inline } | Delimiter
|
|
45
|
+
|
|
46
|
+
function isWhitespace(character: string | undefined): boolean {
|
|
47
|
+
return character === undefined || /\s/.test(character)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPunctuation(character: string | undefined): boolean {
|
|
51
|
+
return character !== undefined && PUNCTUATION.test(character)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function flanking(
|
|
55
|
+
source: string,
|
|
56
|
+
start: number,
|
|
57
|
+
end: number,
|
|
58
|
+
char: string,
|
|
59
|
+
): { canOpen: boolean; canClose: boolean } {
|
|
60
|
+
const before = start === 0 ? undefined : source[start - 1]
|
|
61
|
+
const after = end >= source.length ? undefined : source[end]
|
|
62
|
+
|
|
63
|
+
const leftFlanking =
|
|
64
|
+
!isWhitespace(after) && (!isPunctuation(after) || isWhitespace(before) || isPunctuation(before))
|
|
65
|
+
const rightFlanking =
|
|
66
|
+
!isWhitespace(before) && (!isPunctuation(before) || isWhitespace(after) || isPunctuation(after))
|
|
67
|
+
|
|
68
|
+
if (char === '_') {
|
|
69
|
+
return {
|
|
70
|
+
canOpen: leftFlanking && (!rightFlanking || isPunctuation(before)),
|
|
71
|
+
canClose: rightFlanking && (!leftFlanking || isPunctuation(after)),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { canOpen: leftFlanking, canClose: rightFlanking }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function matchingBracket(source: string, from: number): number {
|
|
78
|
+
let depth = 0
|
|
79
|
+
let index = from
|
|
80
|
+
while (index < source.length) {
|
|
81
|
+
const character = source[index]!
|
|
82
|
+
if (character === '\\') {
|
|
83
|
+
index += 2
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (character === '`') {
|
|
87
|
+
const run = /^`+/.exec(source.slice(index))![0]
|
|
88
|
+
const close = source.indexOf(run, index + run.length)
|
|
89
|
+
index = close === -1 ? index + run.length : close + run.length
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
if (character === '[') depth += 1
|
|
93
|
+
else if (character === ']') {
|
|
94
|
+
depth -= 1
|
|
95
|
+
if (depth === 0) return index
|
|
96
|
+
}
|
|
97
|
+
index += 1
|
|
98
|
+
}
|
|
99
|
+
return -1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface Destination {
|
|
103
|
+
readonly url: string
|
|
104
|
+
readonly title: string | null
|
|
105
|
+
readonly end: number
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readDestination(source: string, from: number): Destination | null {
|
|
109
|
+
if (source[from] !== '(') return null
|
|
110
|
+
let index = from + 1
|
|
111
|
+
while (index < source.length && /\s/.test(source[index]!)) index += 1
|
|
112
|
+
|
|
113
|
+
let url: string
|
|
114
|
+
if (source[index] === '<') {
|
|
115
|
+
const close = source.indexOf('>', index + 1)
|
|
116
|
+
if (close === -1) return null
|
|
117
|
+
url = source.slice(index + 1, close)
|
|
118
|
+
if (url.includes('\n')) return null
|
|
119
|
+
index = close + 1
|
|
120
|
+
} else {
|
|
121
|
+
let depth = 0
|
|
122
|
+
const start = index
|
|
123
|
+
while (index < source.length) {
|
|
124
|
+
const character = source[index]!
|
|
125
|
+
if (character === '\\' && index + 1 < source.length) {
|
|
126
|
+
index += 2
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (/\s/.test(character)) break
|
|
130
|
+
if (character === '(') depth += 1
|
|
131
|
+
if (character === ')') {
|
|
132
|
+
if (depth === 0) break
|
|
133
|
+
depth -= 1
|
|
134
|
+
}
|
|
135
|
+
index += 1
|
|
136
|
+
}
|
|
137
|
+
url = unescapeBackslashes(source.slice(start, index))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
while (index < source.length && /\s/.test(source[index]!)) index += 1
|
|
141
|
+
|
|
142
|
+
let title: string | null = null
|
|
143
|
+
const opener = source[index]
|
|
144
|
+
if (opener === '"' || opener === "'" || opener === '(') {
|
|
145
|
+
const closer = opener === '(' ? ')' : opener
|
|
146
|
+
const close = source.indexOf(closer, index + 1)
|
|
147
|
+
if (close === -1) return null
|
|
148
|
+
title = unescapeBackslashes(source.slice(index + 1, close))
|
|
149
|
+
index = close + 1
|
|
150
|
+
while (index < source.length && /\s/.test(source[index]!)) index += 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (source[index] !== ')') return null
|
|
154
|
+
return { url: url.trim(), title, end: index + 1 }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function unescapeBackslashes(value: string): string {
|
|
158
|
+
return value.replace(/\\(.)/gs, (whole, character: string) =>
|
|
159
|
+
ESCAPABLE.test(character) ? character : whole,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function trimBareUrl(url: string): string {
|
|
164
|
+
let end = url.length
|
|
165
|
+
while (end > 0) {
|
|
166
|
+
const character = url[end - 1]!
|
|
167
|
+
if ('.,;:!?"\''.includes(character)) {
|
|
168
|
+
end -= 1
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (character === ')' || character === ']') {
|
|
172
|
+
const open = character === ')' ? '(' : '['
|
|
173
|
+
const slice = url.slice(0, end)
|
|
174
|
+
const opens = slice.split(open).length - 1
|
|
175
|
+
const closes = slice.split(character).length - 1
|
|
176
|
+
if (closes > opens) {
|
|
177
|
+
end -= 1
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
return url.slice(0, end)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function parseInline(source: string, context: InlineContext, allowLinks = true): Inline[] {
|
|
187
|
+
const items: Item[] = []
|
|
188
|
+
let buffer = ''
|
|
189
|
+
let index = 0
|
|
190
|
+
let delimiters = 0
|
|
191
|
+
|
|
192
|
+
const push = (node: Inline): boolean => {
|
|
193
|
+
if (!context.budget.take()) return false
|
|
194
|
+
items.push({ t: 'node', node })
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const flush = (): void => {
|
|
199
|
+
if (buffer.length === 0) return
|
|
200
|
+
items.push({ t: 'node', node: { kind: 'text', value: buffer } })
|
|
201
|
+
buffer = ''
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
while (index < source.length) {
|
|
205
|
+
if (context.budget.spent) {
|
|
206
|
+
context.budget.exhaust()
|
|
207
|
+
buffer += source.slice(index)
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
const character = source[index]!
|
|
211
|
+
|
|
212
|
+
if (character === '\\') {
|
|
213
|
+
const next = source[index + 1]
|
|
214
|
+
if (next !== undefined && ESCAPABLE.test(next)) {
|
|
215
|
+
buffer += next
|
|
216
|
+
index += 2
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
buffer += character
|
|
220
|
+
index += 1
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (character === '\n') {
|
|
225
|
+
flush()
|
|
226
|
+
if (!push({ kind: 'break' })) break
|
|
227
|
+
index += 1
|
|
228
|
+
while (index < source.length && (source[index] === ' ' || source[index] === '\t')) index += 1
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (character === '`' && context.features.codeSpans) {
|
|
233
|
+
const run = /^`+/.exec(source.slice(index))![0]
|
|
234
|
+
let search = index + run.length
|
|
235
|
+
let close = -1
|
|
236
|
+
while (search < source.length) {
|
|
237
|
+
const at = source.indexOf(run, search)
|
|
238
|
+
if (at === -1) break
|
|
239
|
+
if (source[at + run.length] === '`') {
|
|
240
|
+
search = at + /^`+/.exec(source.slice(at))![0].length
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
close = at
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
if (close !== -1) {
|
|
247
|
+
let value = source.slice(index + run.length, close).replace(/\n/g, ' ')
|
|
248
|
+
if (
|
|
249
|
+
value.length > 2 &&
|
|
250
|
+
value.startsWith(' ') &&
|
|
251
|
+
value.endsWith(' ') &&
|
|
252
|
+
value.trim() !== ''
|
|
253
|
+
) {
|
|
254
|
+
value = value.slice(1, -1)
|
|
255
|
+
}
|
|
256
|
+
flush()
|
|
257
|
+
if (!push({ kind: 'code', value })) break
|
|
258
|
+
index = close + run.length
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
buffer += run
|
|
262
|
+
index += run.length
|
|
263
|
+
continue
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (character === '<' && allowLinks && context.features.links) {
|
|
267
|
+
const rest = source.slice(index)
|
|
268
|
+
const scheme = AUTOLINK.exec(rest)
|
|
269
|
+
const email = AUTOLINK_EMAIL.exec(rest)
|
|
270
|
+
if (scheme !== null) {
|
|
271
|
+
flush()
|
|
272
|
+
if (
|
|
273
|
+
!push({
|
|
274
|
+
kind: 'link',
|
|
275
|
+
href: scheme[1]!,
|
|
276
|
+
title: null,
|
|
277
|
+
children: [{ kind: 'text', value: scheme[1]! }],
|
|
278
|
+
})
|
|
279
|
+
)
|
|
280
|
+
break
|
|
281
|
+
index += scheme[0].length
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
if (email !== null) {
|
|
285
|
+
flush()
|
|
286
|
+
if (
|
|
287
|
+
!push({
|
|
288
|
+
kind: 'link',
|
|
289
|
+
href: `mailto:${email[1]!}`,
|
|
290
|
+
title: null,
|
|
291
|
+
children: [{ kind: 'text', value: email[1]! }],
|
|
292
|
+
})
|
|
293
|
+
) {
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
index += email[0].length
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
buffer += character
|
|
300
|
+
index += 1
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (character === '!' && source[index + 1] === '[') {
|
|
305
|
+
const close = matchingBracket(source, index + 1)
|
|
306
|
+
const destination = close === -1 ? null : readDestination(source, close + 1)
|
|
307
|
+
if (destination !== null) {
|
|
308
|
+
const alt = textOf(parseInline(source.slice(index + 2, close), context, false))
|
|
309
|
+
flush()
|
|
310
|
+
if (context.features.images) {
|
|
311
|
+
if (!push({ kind: 'image', src: destination.url, alt })) break
|
|
312
|
+
} else {
|
|
313
|
+
buffer += alt
|
|
314
|
+
}
|
|
315
|
+
index = destination.end
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
buffer += character
|
|
319
|
+
index += 1
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (character === '[' && context.features.attachments) {
|
|
324
|
+
const attachment = ATTACHMENT.exec(source.slice(index))
|
|
325
|
+
if (attachment !== null) {
|
|
326
|
+
flush()
|
|
327
|
+
if (!push({ kind: 'attachment', id: Number(attachment[1]) })) break
|
|
328
|
+
index += attachment[0].length
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (character === '[' && allowLinks && context.features.links) {
|
|
334
|
+
const close = matchingBracket(source, index)
|
|
335
|
+
const destination = close === -1 ? null : readDestination(source, close + 1)
|
|
336
|
+
if (destination !== null) {
|
|
337
|
+
const children = parseInline(source.slice(index + 1, close), context, false)
|
|
338
|
+
flush()
|
|
339
|
+
if (!push({ kind: 'link', href: destination.url, title: destination.title, children }))
|
|
340
|
+
break
|
|
341
|
+
index = destination.end
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
buffer += character
|
|
345
|
+
index += 1
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (character === ':' && context.features.directives) {
|
|
350
|
+
const match = /^:([a-z][a-z0-9]{0,15})\[/.exec(source.slice(index))
|
|
351
|
+
if (match !== null && context.directives.has(match[1]!) && DIRECTIVE_NAME.test(match[1]!)) {
|
|
352
|
+
const bracket = index + match[0].length - 1
|
|
353
|
+
const close = matchingBracket(source, bracket)
|
|
354
|
+
if (close !== -1) {
|
|
355
|
+
const children = parseInline(source.slice(bracket + 1, close), context, allowLinks)
|
|
356
|
+
flush()
|
|
357
|
+
if (!push({ kind: 'directive', name: match[1]!, children })) break
|
|
358
|
+
index = close + 1
|
|
359
|
+
continue
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
buffer += character
|
|
363
|
+
index += 1
|
|
364
|
+
continue
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (character === '@' && allowLinks && context.features.mentions) {
|
|
368
|
+
const before = index === 0 ? undefined : source[index - 1]
|
|
369
|
+
if (before === undefined || /[\s(<*_~]/.test(before)) {
|
|
370
|
+
const rest = source.slice(index)
|
|
371
|
+
const quoted = MENTION_QUOTED.exec(rest)
|
|
372
|
+
const bare = quoted === null ? MENTION.exec(rest) : null
|
|
373
|
+
|
|
374
|
+
let name: string | null = null
|
|
375
|
+
let consumed = 0
|
|
376
|
+
if (quoted !== null && MENTION_NAME.test(quoted[1]!)) {
|
|
377
|
+
name = quoted[1]!
|
|
378
|
+
consumed = quoted[0].length
|
|
379
|
+
} else if (bare !== null) {
|
|
380
|
+
name = bare[1]!.replace(/\.+$/, '')
|
|
381
|
+
consumed = 1 + name.length
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (name !== null && name.length > 0 && name.length <= MENTION_MAX) {
|
|
385
|
+
flush()
|
|
386
|
+
if (!push({ kind: 'mention', name })) break
|
|
387
|
+
index += consumed
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
buffer += character
|
|
392
|
+
index += 1
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (
|
|
397
|
+
(character === '*' || character === '_' || character === '~') &&
|
|
398
|
+
context.features.emphasis
|
|
399
|
+
) {
|
|
400
|
+
let end = index
|
|
401
|
+
while (end < source.length && source[end] === character) end += 1
|
|
402
|
+
const run = source.slice(index, end)
|
|
403
|
+
if (delimiters >= context.limits.maxDelimiters) {
|
|
404
|
+
buffer += run
|
|
405
|
+
index += run.length
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
const { canOpen, canClose } = flanking(source, index, index + run.length, character)
|
|
409
|
+
if (!canOpen && !canClose) {
|
|
410
|
+
buffer += run
|
|
411
|
+
index += run.length
|
|
412
|
+
continue
|
|
413
|
+
}
|
|
414
|
+
flush()
|
|
415
|
+
delimiters += 1
|
|
416
|
+
items.push({ t: 'delim', char: character, length: run.length, canOpen, canClose, raw: run })
|
|
417
|
+
index += run.length
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (allowLinks && context.features.links) {
|
|
422
|
+
const before = index === 0 ? undefined : source[index - 1]
|
|
423
|
+
if (before === undefined || /[\s(<*_~]/.test(before)) {
|
|
424
|
+
const bare = BARE_URL.exec(source.slice(index))
|
|
425
|
+
if (bare !== null) {
|
|
426
|
+
const url = trimBareUrl(bare[0])
|
|
427
|
+
if (url.length > 0 && url.length <= context.limits.maxUrlLength) {
|
|
428
|
+
flush()
|
|
429
|
+
const href = url.toLowerCase().startsWith('www.') ? `https://${url}` : url
|
|
430
|
+
if (
|
|
431
|
+
!push({ kind: 'link', href, title: null, children: [{ kind: 'text', value: url }] })
|
|
432
|
+
)
|
|
433
|
+
break
|
|
434
|
+
index += url.length
|
|
435
|
+
continue
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
buffer += character
|
|
442
|
+
index += 1
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
flush()
|
|
446
|
+
return resolveEmphasis(items, context)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function literal(items: readonly Item[]): Inline[] {
|
|
450
|
+
const out: Inline[] = []
|
|
451
|
+
for (const item of items) {
|
|
452
|
+
const node: Inline = item.t === 'delim' ? { kind: 'text', value: item.raw } : item.node
|
|
453
|
+
const last = out[out.length - 1]
|
|
454
|
+
if (node.kind === 'text' && last !== undefined && last.kind === 'text') {
|
|
455
|
+
out[out.length - 1] = { kind: 'text', value: last.value + node.value }
|
|
456
|
+
continue
|
|
457
|
+
}
|
|
458
|
+
out.push(node)
|
|
459
|
+
}
|
|
460
|
+
return out
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function resolveEmphasis(items: Item[], context: InlineContext): Inline[] {
|
|
464
|
+
let index = 0
|
|
465
|
+
while (index < items.length) {
|
|
466
|
+
const closer = items[index]!
|
|
467
|
+
if (closer.t !== 'delim' || !closer.canClose) {
|
|
468
|
+
index += 1
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let found = -1
|
|
473
|
+
for (let back = index - 1; back >= 0; back -= 1) {
|
|
474
|
+
const candidate = items[back]!
|
|
475
|
+
if (candidate.t !== 'delim' || candidate.char !== closer.char || !candidate.canOpen) continue
|
|
476
|
+
const bothWays = candidate.canClose || closer.canOpen
|
|
477
|
+
const ruleOfThree =
|
|
478
|
+
bothWays &&
|
|
479
|
+
(candidate.length + closer.length) % 3 === 0 &&
|
|
480
|
+
!(candidate.length % 3 === 0 && closer.length % 3 === 0)
|
|
481
|
+
if (ruleOfThree) continue
|
|
482
|
+
found = back
|
|
483
|
+
break
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (found === -1) {
|
|
487
|
+
index += 1
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!context.budget.take()) break
|
|
492
|
+
|
|
493
|
+
const opener = items[found] as Delimiter
|
|
494
|
+
const strike = closer.char === '~'
|
|
495
|
+
const use = strike
|
|
496
|
+
? Math.min(opener.length, closer.length, 2)
|
|
497
|
+
: opener.length >= 2 && closer.length >= 2
|
|
498
|
+
? 2
|
|
499
|
+
: 1
|
|
500
|
+
|
|
501
|
+
const children = literal(items.slice(found + 1, index))
|
|
502
|
+
const node: Inline = strike
|
|
503
|
+
? { kind: 'strike', children }
|
|
504
|
+
: use === 2
|
|
505
|
+
? { kind: 'strong', children }
|
|
506
|
+
: { kind: 'emphasis', children }
|
|
507
|
+
|
|
508
|
+
opener.length -= use
|
|
509
|
+
closer.length -= use
|
|
510
|
+
|
|
511
|
+
const replacement: Item[] = []
|
|
512
|
+
if (opener.length > 0) {
|
|
513
|
+
replacement.push({ ...opener, raw: opener.raw.slice(0, opener.length) })
|
|
514
|
+
}
|
|
515
|
+
replacement.push({ t: 'node', node })
|
|
516
|
+
if (closer.length > 0) {
|
|
517
|
+
replacement.push({ ...closer, raw: closer.raw.slice(0, closer.length) })
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
items.splice(found, index - found + 1, ...replacement)
|
|
521
|
+
index = found + replacement.length - (closer.length > 0 ? 1 : 0)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return literal(items)
|
|
525
|
+
}
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface MarkdownLimits {
|
|
2
|
+
readonly maxInput: number
|
|
3
|
+
readonly maxDepth: number
|
|
4
|
+
readonly maxNodes: number
|
|
5
|
+
readonly maxUrlLength: number
|
|
6
|
+
readonly maxDelimiters: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_LIMITS: MarkdownLimits = {
|
|
10
|
+
maxInput: 64 * 1024,
|
|
11
|
+
maxDepth: 12,
|
|
12
|
+
maxNodes: 4000,
|
|
13
|
+
maxUrlLength: 2048,
|
|
14
|
+
maxDelimiters: 2000,
|
|
15
|
+
}
|