@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/src/node.ts ADDED
@@ -0,0 +1,240 @@
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
+ // inlines
31
+ | 'text'
32
+ | 'softbreak'
33
+ | 'linebreak'
34
+ | 'code'
35
+ | 'html_inline'
36
+ | 'emph'
37
+ | 'strong'
38
+ | 'link'
39
+ | 'image'
40
+ | 'del'
41
+
42
+ const CONTAINER_TYPES: Record<string, true> = {
43
+ document: true,
44
+ block_quote: true,
45
+ list: true,
46
+ item: true,
47
+ paragraph: true,
48
+ heading: true,
49
+ emph: true,
50
+ strong: true,
51
+ link: true,
52
+ image: true,
53
+ del: true,
54
+ }
55
+
56
+ export type ListType = 'bullet' | 'ordered'
57
+
58
+ export type ListData = {
59
+ type: ListType
60
+ /** Tight lists omit the <p> wrapper around item paragraphs. Decided at finalize. */
61
+ tight: boolean
62
+ /** Ordered-list start number; ignored for bullet lists. */
63
+ start: number
64
+ /** '.' or ')' for ordered lists — a change of delimiter starts a new list. */
65
+ delimiter: string
66
+ /** '-', '+' or '*' for bullet lists — a change of char starts a new list. */
67
+ bulletChar: string
68
+ /** Columns of padding between the marker and the item content. */
69
+ padding: number
70
+ /** Column at which the marker starts. */
71
+ markerOffset: number
72
+ }
73
+
74
+ /** [startLine, startCol], [endLine, endCol] — all 1-based, as in the spec. */
75
+ export type SourcePos = [[number, number], [number, number]]
76
+
77
+ export class MdNode {
78
+ type: NodeType
79
+
80
+ parent: MdNode | null = null
81
+ firstChild: MdNode | null = null
82
+ lastChild: MdNode | null = null
83
+ prev: MdNode | null = null
84
+ next: MdNode | null = null
85
+
86
+ sourcepos: SourcePos
87
+
88
+ /** Text payload for text/code/html_block/html_inline/code_block. */
89
+ literal: string | null = null
90
+
91
+ /** Heading level 1..6. */
92
+ level = 0
93
+
94
+ /** Link/image destination, already entity-decoded and backslash-unescaped. */
95
+ destination: string | null = null
96
+ /** Link/image title, already entity-decoded and backslash-unescaped. */
97
+ title: string | null = null
98
+
99
+ /** Fenced code info string (raw, entity-decoded); null for indented code. */
100
+ info: string | null = null
101
+ /** True for fenced code blocks, false for indented. */
102
+ isFenced = false
103
+ fenceChar = ''
104
+ fenceLength = 0
105
+ fenceOffset = 0
106
+
107
+ listData: ListData | null = null
108
+
109
+ // --- block-phase bookkeeping (not part of the rendered tree) ---
110
+
111
+ /** Open blocks accept further lines; closed ones are finalized. */
112
+ open = true
113
+ /** Accumulated raw content, consumed by the inline phase. */
114
+ stringContent = ''
115
+ /** A paragraph/heading whose last line was blank cannot be lazily continued. */
116
+ lastLineBlank = false
117
+ lastLineChecked = false
118
+
119
+ constructor(type: NodeType, sourcepos?: SourcePos) {
120
+ this.type = type
121
+ this.sourcepos = sourcepos ?? [
122
+ [0, 0],
123
+ [0, 0],
124
+ ]
125
+ }
126
+
127
+ /** Containers may hold children; leaves carry only `literal`. */
128
+ isContainer(): boolean {
129
+ return true === CONTAINER_TYPES[this.type]
130
+ }
131
+
132
+ appendChild(child: MdNode): void {
133
+ child.unlink()
134
+ child.parent = this
135
+ if (this.lastChild) {
136
+ this.lastChild.next = child
137
+ child.prev = this.lastChild
138
+ this.lastChild = child
139
+ } else {
140
+ this.firstChild = child
141
+ this.lastChild = child
142
+ }
143
+ }
144
+
145
+ prependChild(child: MdNode): void {
146
+ child.unlink()
147
+ child.parent = this
148
+ if (this.firstChild) {
149
+ this.firstChild.prev = child
150
+ child.next = this.firstChild
151
+ this.firstChild = child
152
+ } else {
153
+ this.firstChild = child
154
+ this.lastChild = child
155
+ }
156
+ }
157
+
158
+ insertAfter(sibling: MdNode): void {
159
+ sibling.unlink()
160
+ sibling.next = this.next
161
+ if (sibling.next) sibling.next.prev = sibling
162
+ sibling.prev = this
163
+ this.next = sibling
164
+ sibling.parent = this.parent
165
+ if (!sibling.next && sibling.parent) sibling.parent.lastChild = sibling
166
+ }
167
+
168
+ insertBefore(sibling: MdNode): void {
169
+ sibling.unlink()
170
+ sibling.prev = this.prev
171
+ if (sibling.prev) sibling.prev.next = sibling
172
+ sibling.next = this
173
+ this.prev = sibling
174
+ sibling.parent = this.parent
175
+ if (!sibling.prev && sibling.parent) sibling.parent.firstChild = sibling
176
+ }
177
+
178
+ /** Detach from the tree, leaving the node itself intact. */
179
+ unlink(): void {
180
+ if (this.prev) this.prev.next = this.next
181
+ else if (this.parent) this.parent.firstChild = this.next
182
+
183
+ if (this.next) this.next.prev = this.prev
184
+ else if (this.parent) this.parent.lastChild = this.prev
185
+
186
+ this.parent = null
187
+ this.next = null
188
+ this.prev = null
189
+ }
190
+
191
+ /** Depth-first walker. Containers are entered and exited; leaves once. */
192
+ walker(): NodeWalker {
193
+ return new NodeWalker(this)
194
+ }
195
+ }
196
+
197
+ export type WalkEvent = { entering: boolean; node: MdNode }
198
+
199
+ export class NodeWalker {
200
+ private current: MdNode | null
201
+ private root: MdNode
202
+ private entering = true
203
+
204
+ constructor(root: MdNode) {
205
+ this.current = root
206
+ this.root = root
207
+ }
208
+
209
+ next(): WalkEvent | null {
210
+ const cur = this.current
211
+ const entering = this.entering
212
+ if (null === cur) return null
213
+
214
+ const container = cur.isContainer()
215
+
216
+ if (entering && container) {
217
+ if (cur.firstChild) {
218
+ this.current = cur.firstChild
219
+ this.entering = true
220
+ } else {
221
+ this.entering = false
222
+ }
223
+ } else if (cur === this.root) {
224
+ this.current = null
225
+ } else if (null === cur.next) {
226
+ this.current = cur.parent
227
+ this.entering = false
228
+ } else {
229
+ this.current = cur.next
230
+ this.entering = true
231
+ }
232
+
233
+ return { entering, node: cur }
234
+ }
235
+
236
+ resumeAt(node: MdNode, entering: boolean): void {
237
+ this.current = node
238
+ this.entering = entering
239
+ }
240
+ }
package/src/options.ts ADDED
@@ -0,0 +1,39 @@
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 GFM strikethrough (`~~x~~`). Default true.
10
+ *
11
+ * Strikethrough is the only GFM extension implemented; tables, task list
12
+ * items, autolink literals, footnotes and raw-HTML filtering are not, and
13
+ * this flag does not gate them.
14
+ */
15
+ gfm: boolean
16
+ /** Render soft line breaks as hard breaks. */
17
+ breaks: boolean
18
+ }
19
+
20
+ export const DEFAULT_OPTIONS: ParserOptions = {
21
+ gfm: true,
22
+ breaks: false,
23
+ }
24
+
25
+ export function resolveOptions(opts?: Partial<ParserOptions>): ParserOptions {
26
+ return {
27
+ gfm: opts?.gfm ?? DEFAULT_OPTIONS.gfm,
28
+ breaks: opts?.breaks ?? DEFAULT_OPTIONS.breaks,
29
+ }
30
+ }
31
+
32
+ /** A resolved link reference definition (§4.7). */
33
+ export type RefDef = {
34
+ destination: string
35
+ title: string | null
36
+ }
37
+
38
+ /** Keys are labels put through `normalizeReference`. */
39
+ 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,