@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/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>
package/src/tsconfig.json CHANGED
@@ -3,6 +3,8 @@
3
3
  "esModuleInterop": true,
4
4
  "module": "nodenext",
5
5
  "noEmitOnError": true,
6
+ "allowImportingTsExtensions": true,
7
+ "rewriteRelativeImportExtensions": true,
6
8
  "outDir": "../dist",
7
9
  "rootDir": ".",
8
10
  "resolveJsonModule": true,