@tabnas/markdown 0.4.2 → 0.5.1
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 +131 -10
- package/dist/ast.d.ts +113 -0
- package/dist/ast.js +273 -0
- package/dist/ast.js.map +1 -0
- package/dist/block.d.ts +10 -0
- package/dist/block.js +1411 -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 +16 -0
- package/dist/html.js +393 -0
- package/dist/html.js.map +1 -0
- package/dist/inline.d.ts +19 -0
- package/dist/inline.js +1331 -0
- package/dist/inline.js.map +1 -0
- package/dist/markdown.d.ts +21 -84
- package/dist/markdown.js +80 -687
- package/dist/markdown.js.map +1 -1
- package/dist/node.d.ts +112 -0
- package/dist/node.js +203 -0
- package/dist/node.js.map +1 -0
- package/dist/options.d.ts +26 -0
- package/dist/options.js +16 -0
- package/dist/options.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/src/ast.ts +406 -0
- package/src/block.ts +1579 -0
- package/src/common.ts +166 -0
- package/src/commonmark.ts +34 -0
- package/src/entities.ts +14 -0
- package/src/html.ts +446 -0
- package/src/inline.ts +1495 -0
- package/src/markdown.ts +132 -770
- package/src/node.ts +293 -0
- package/src/options.ts +44 -0
- package/src/tsconfig.json +2 -0
package/src/node.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/* Copyright (c) 2021-2026 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// Native CommonMark node tree — the internal parse result.
|
|
4
|
+
//
|
|
5
|
+
// This is deliberately a linked tree (parent/firstChild/lastChild/prev/next)
|
|
6
|
+
// rather than a children array, because both phases of the CommonMark
|
|
7
|
+
// algorithm (see spec 0.31.2 Appendix A, "A parsing strategy") splice nodes
|
|
8
|
+
// in and out mid-walk: the block phase keeps a spine of open blocks and
|
|
9
|
+
// closes them from the tail, and the inline phase's delimiter stack rewrites
|
|
10
|
+
// sibling runs in place. An array-of-children shape forces an index rewrite
|
|
11
|
+
// on every one of those operations.
|
|
12
|
+
//
|
|
13
|
+
// The public, mdast-adjacent AST that `@tabnas/markdown` has always returned
|
|
14
|
+
// is produced from this tree by `ast.ts`. That projection is lossy on
|
|
15
|
+
// purpose (it drops raw inline HTML boundaries and source positions); the
|
|
16
|
+
// tree is what `html.ts` renders for CommonMark conformance.
|
|
17
|
+
|
|
18
|
+
export type NodeType =
|
|
19
|
+
// containers
|
|
20
|
+
| 'document'
|
|
21
|
+
| 'block_quote'
|
|
22
|
+
| 'list'
|
|
23
|
+
| 'item'
|
|
24
|
+
// leaf blocks
|
|
25
|
+
| 'paragraph'
|
|
26
|
+
| 'heading'
|
|
27
|
+
| 'thematic_break'
|
|
28
|
+
| 'code_block'
|
|
29
|
+
| 'html_block'
|
|
30
|
+
// GFM tables (extension): a leaf block as far as the spec's algorithm is
|
|
31
|
+
// concerned — no block-level element can be inserted in one — but a
|
|
32
|
+
// container as far as the tree is concerned, since its rows and cells are
|
|
33
|
+
// real nodes and its cells hold inlines.
|
|
34
|
+
| 'table'
|
|
35
|
+
| 'table_row'
|
|
36
|
+
| 'table_cell'
|
|
37
|
+
// inlines
|
|
38
|
+
| 'text'
|
|
39
|
+
| 'softbreak'
|
|
40
|
+
| 'linebreak'
|
|
41
|
+
| 'code'
|
|
42
|
+
| 'html_inline'
|
|
43
|
+
| 'emph'
|
|
44
|
+
| 'strong'
|
|
45
|
+
| 'link'
|
|
46
|
+
| 'image'
|
|
47
|
+
| 'del'
|
|
48
|
+
|
|
49
|
+
const CONTAINER_TYPES: Record<string, true> = {
|
|
50
|
+
document: true,
|
|
51
|
+
block_quote: true,
|
|
52
|
+
list: true,
|
|
53
|
+
item: true,
|
|
54
|
+
paragraph: true,
|
|
55
|
+
heading: true,
|
|
56
|
+
table: true,
|
|
57
|
+
table_row: true,
|
|
58
|
+
table_cell: true,
|
|
59
|
+
emph: true,
|
|
60
|
+
strong: true,
|
|
61
|
+
link: true,
|
|
62
|
+
image: true,
|
|
63
|
+
del: true,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Per-column alignment of a GFM table, from the colons in its delimiter row:
|
|
68
|
+
* `:--` left, `--:` right, `:-:` center, `---` none.
|
|
69
|
+
*/
|
|
70
|
+
export type TableAlign = 'left' | 'right' | 'center' | null
|
|
71
|
+
|
|
72
|
+
export type ListType = 'bullet' | 'ordered'
|
|
73
|
+
|
|
74
|
+
export type ListData = {
|
|
75
|
+
type: ListType
|
|
76
|
+
/** Tight lists omit the <p> wrapper around item paragraphs. Decided at finalize. */
|
|
77
|
+
tight: boolean
|
|
78
|
+
/** Ordered-list start number; ignored for bullet lists. */
|
|
79
|
+
start: number
|
|
80
|
+
/** '.' or ')' for ordered lists — a change of delimiter starts a new list. */
|
|
81
|
+
delimiter: string
|
|
82
|
+
/** '-', '+' or '*' for bullet lists — a change of char starts a new list. */
|
|
83
|
+
bulletChar: string
|
|
84
|
+
/** Columns of padding between the marker and the item content. */
|
|
85
|
+
padding: number
|
|
86
|
+
/** Column at which the marker starts. */
|
|
87
|
+
markerOffset: number
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** [startLine, startCol], [endLine, endCol] — all 1-based, as in the spec. */
|
|
91
|
+
export type SourcePos = [[number, number], [number, number]]
|
|
92
|
+
|
|
93
|
+
export class MdNode {
|
|
94
|
+
type: NodeType
|
|
95
|
+
|
|
96
|
+
parent: MdNode | null = null
|
|
97
|
+
firstChild: MdNode | null = null
|
|
98
|
+
lastChild: MdNode | null = null
|
|
99
|
+
prev: MdNode | null = null
|
|
100
|
+
next: MdNode | null = null
|
|
101
|
+
|
|
102
|
+
sourcepos: SourcePos
|
|
103
|
+
|
|
104
|
+
/** Text payload for text/code/html_block/html_inline/code_block. */
|
|
105
|
+
literal: string | null = null
|
|
106
|
+
|
|
107
|
+
/** Heading level 1..6. */
|
|
108
|
+
level = 0
|
|
109
|
+
|
|
110
|
+
/** Link/image destination, already entity-decoded and backslash-unescaped. */
|
|
111
|
+
destination: string | null = null
|
|
112
|
+
/** Link/image title, already entity-decoded and backslash-unescaped. */
|
|
113
|
+
title: string | null = null
|
|
114
|
+
|
|
115
|
+
/** Fenced code info string (raw, entity-decoded); null for indented code. */
|
|
116
|
+
info: string | null = null
|
|
117
|
+
/** True for fenced code blocks, false for indented. */
|
|
118
|
+
isFenced = false
|
|
119
|
+
fenceChar = ''
|
|
120
|
+
fenceLength = 0
|
|
121
|
+
fenceOffset = 0
|
|
122
|
+
|
|
123
|
+
listData: ListData | null = null
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* GFM tables: one entry per column, in order, set on the **table** node by
|
|
127
|
+
* the block phase from the delimiter row. Every row of the table — header
|
|
128
|
+
* and body alike — has exactly this many cells, because short rows are
|
|
129
|
+
* padded and long ones are truncated. Null on every other node type.
|
|
130
|
+
*/
|
|
131
|
+
tableAlign: TableAlign[] | null = null
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* GFM tables: true on the one `table_row` that is the header row, which is
|
|
135
|
+
* always the table's first child. mdast has no header flag and relies on
|
|
136
|
+
* that convention, but the renderer needs to choose `<th>` over `<td>`
|
|
137
|
+
* without walking back up to the table.
|
|
138
|
+
*/
|
|
139
|
+
isHeaderRow = false
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* GFM task list items (`- [x] foo`): `true`/`false` for a checked/unchecked
|
|
143
|
+
* item, `null` for an ordinary one. Set on `item` nodes by the block phase
|
|
144
|
+
* when `gfm` is on, once the marker has been consumed from the item's first
|
|
145
|
+
* paragraph. mdast's field and mdast's semantics — `ast.ts` projects it
|
|
146
|
+
* straight through to `listItem.checked`.
|
|
147
|
+
*/
|
|
148
|
+
checked: boolean | null = null
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The `gfm` parse option, recorded on the **document** node by the block
|
|
152
|
+
* phase and meaningless anywhere else.
|
|
153
|
+
*
|
|
154
|
+
* The renderer needs it because GFM's disallowed-raw-HTML filter is a
|
|
155
|
+
* render-time concern, and `renderHTML(tree)` may be called with no options
|
|
156
|
+
* at all. Without this the default (`gfm: true`) would apply the filter to a
|
|
157
|
+
* tree that was parsed as plain CommonMark, which is exactly the case the
|
|
158
|
+
* conformance runner exercises.
|
|
159
|
+
*/
|
|
160
|
+
gfm = true
|
|
161
|
+
|
|
162
|
+
// --- block-phase bookkeeping (not part of the rendered tree) ---
|
|
163
|
+
|
|
164
|
+
/** Open blocks accept further lines; closed ones are finalized. */
|
|
165
|
+
open = true
|
|
166
|
+
/** Accumulated raw content, consumed by the inline phase. */
|
|
167
|
+
stringContent = ''
|
|
168
|
+
/** A paragraph/heading whose last line was blank cannot be lazily continued. */
|
|
169
|
+
lastLineBlank = false
|
|
170
|
+
lastLineChecked = false
|
|
171
|
+
|
|
172
|
+
constructor(type: NodeType, sourcepos?: SourcePos) {
|
|
173
|
+
this.type = type
|
|
174
|
+
this.sourcepos = sourcepos ?? [
|
|
175
|
+
[0, 0],
|
|
176
|
+
[0, 0],
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Containers may hold children; leaves carry only `literal`. */
|
|
181
|
+
isContainer(): boolean {
|
|
182
|
+
return true === CONTAINER_TYPES[this.type]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
appendChild(child: MdNode): void {
|
|
186
|
+
child.unlink()
|
|
187
|
+
child.parent = this
|
|
188
|
+
if (this.lastChild) {
|
|
189
|
+
this.lastChild.next = child
|
|
190
|
+
child.prev = this.lastChild
|
|
191
|
+
this.lastChild = child
|
|
192
|
+
} else {
|
|
193
|
+
this.firstChild = child
|
|
194
|
+
this.lastChild = child
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
prependChild(child: MdNode): void {
|
|
199
|
+
child.unlink()
|
|
200
|
+
child.parent = this
|
|
201
|
+
if (this.firstChild) {
|
|
202
|
+
this.firstChild.prev = child
|
|
203
|
+
child.next = this.firstChild
|
|
204
|
+
this.firstChild = child
|
|
205
|
+
} else {
|
|
206
|
+
this.firstChild = child
|
|
207
|
+
this.lastChild = child
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
insertAfter(sibling: MdNode): void {
|
|
212
|
+
sibling.unlink()
|
|
213
|
+
sibling.next = this.next
|
|
214
|
+
if (sibling.next) sibling.next.prev = sibling
|
|
215
|
+
sibling.prev = this
|
|
216
|
+
this.next = sibling
|
|
217
|
+
sibling.parent = this.parent
|
|
218
|
+
if (!sibling.next && sibling.parent) sibling.parent.lastChild = sibling
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
insertBefore(sibling: MdNode): void {
|
|
222
|
+
sibling.unlink()
|
|
223
|
+
sibling.prev = this.prev
|
|
224
|
+
if (sibling.prev) sibling.prev.next = sibling
|
|
225
|
+
sibling.next = this
|
|
226
|
+
this.prev = sibling
|
|
227
|
+
sibling.parent = this.parent
|
|
228
|
+
if (!sibling.prev && sibling.parent) sibling.parent.firstChild = sibling
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Detach from the tree, leaving the node itself intact. */
|
|
232
|
+
unlink(): void {
|
|
233
|
+
if (this.prev) this.prev.next = this.next
|
|
234
|
+
else if (this.parent) this.parent.firstChild = this.next
|
|
235
|
+
|
|
236
|
+
if (this.next) this.next.prev = this.prev
|
|
237
|
+
else if (this.parent) this.parent.lastChild = this.prev
|
|
238
|
+
|
|
239
|
+
this.parent = null
|
|
240
|
+
this.next = null
|
|
241
|
+
this.prev = null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Depth-first walker. Containers are entered and exited; leaves once. */
|
|
245
|
+
walker(): NodeWalker {
|
|
246
|
+
return new NodeWalker(this)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type WalkEvent = { entering: boolean; node: MdNode }
|
|
251
|
+
|
|
252
|
+
export class NodeWalker {
|
|
253
|
+
private current: MdNode | null
|
|
254
|
+
private root: MdNode
|
|
255
|
+
private entering = true
|
|
256
|
+
|
|
257
|
+
constructor(root: MdNode) {
|
|
258
|
+
this.current = root
|
|
259
|
+
this.root = root
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
next(): WalkEvent | null {
|
|
263
|
+
const cur = this.current
|
|
264
|
+
const entering = this.entering
|
|
265
|
+
if (null === cur) return null
|
|
266
|
+
|
|
267
|
+
const container = cur.isContainer()
|
|
268
|
+
|
|
269
|
+
if (entering && container) {
|
|
270
|
+
if (cur.firstChild) {
|
|
271
|
+
this.current = cur.firstChild
|
|
272
|
+
this.entering = true
|
|
273
|
+
} else {
|
|
274
|
+
this.entering = false
|
|
275
|
+
}
|
|
276
|
+
} else if (cur === this.root) {
|
|
277
|
+
this.current = null
|
|
278
|
+
} else if (null === cur.next) {
|
|
279
|
+
this.current = cur.parent
|
|
280
|
+
this.entering = false
|
|
281
|
+
} else {
|
|
282
|
+
this.current = cur.next
|
|
283
|
+
this.entering = true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { entering, node: cur }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
resumeAt(node: MdNode, entering: boolean): void {
|
|
290
|
+
this.current = node
|
|
291
|
+
this.entering = entering
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* Copyright (c) 2021-2026 Richard Rodger, MIT License */
|
|
2
|
+
|
|
3
|
+
// Types shared by both parse phases. Kept in their own module so `block.ts`
|
|
4
|
+
// and `inline.ts` can depend on them without importing `commonmark.ts`, which
|
|
5
|
+
// imports them in turn.
|
|
6
|
+
|
|
7
|
+
export type ParserOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Enable the GFM extensions. Default true.
|
|
10
|
+
*
|
|
11
|
+
* Five are implemented, and this one flag gates all of them together:
|
|
12
|
+
* tables, strikethrough (`~~x~~`), task list items (`- [x] foo`), autolink
|
|
13
|
+
* literals (bare `www.` / `http://` / `https://` / `ftp://` / `a@b.co`) and
|
|
14
|
+
* the disallowed-raw-HTML filter. Footnotes are not implemented.
|
|
15
|
+
*
|
|
16
|
+
* The first four are parse-time; the raw-HTML filter is applied by the
|
|
17
|
+
* renderer, which is why `html.ts` reads this option too. With `gfm:false`
|
|
18
|
+
* the output is plain CommonMark, byte for byte.
|
|
19
|
+
*/
|
|
20
|
+
gfm: boolean
|
|
21
|
+
/** Render soft line breaks as hard breaks. */
|
|
22
|
+
breaks: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_OPTIONS: ParserOptions = {
|
|
26
|
+
gfm: true,
|
|
27
|
+
breaks: false,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveOptions(opts?: Partial<ParserOptions>): ParserOptions {
|
|
31
|
+
return {
|
|
32
|
+
gfm: opts?.gfm ?? DEFAULT_OPTIONS.gfm,
|
|
33
|
+
breaks: opts?.breaks ?? DEFAULT_OPTIONS.breaks,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A resolved link reference definition (§4.7). */
|
|
38
|
+
export type RefDef = {
|
|
39
|
+
destination: string
|
|
40
|
+
title: string | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Keys are labels put through `normalizeReference`. */
|
|
44
|
+
export type RefMap = Record<string, RefDef>
|