micromark-extension-dl-list 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yohei Kanamura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # micromark-extension-dl-list
2
+
3
+ A **micromark extension** that adds colon-based definition list syntax.
4
+
5
+ This package provides **syntax only** and is intended to be used with
6
+ remark or other unified pipelines.
7
+
8
+ For the detailed definition list syntax,
9
+ → **[docs/syntax.md](https://github.com/kanemu/unified-dl-list/blob/main/docs/syntax.md)**.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install micromark-extension-dl-list
15
+ ````
16
+
17
+ or with pnpm:
18
+
19
+ ```bash
20
+ pnpm add micromark-extension-dl-list
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### With micromark (HTML output)
26
+
27
+ This package can be used directly with `micromark`
28
+ to parse colon-based definition lists and generate
29
+ `<dl>`, `<dt>`, and `<dd>` elements.
30
+
31
+ ```js
32
+ import { micromark } from 'micromark'
33
+ import { dlList, dlListHtml } from 'micromark-extension-dl-list'
34
+
35
+ const md = `
36
+ : term
37
+ : description
38
+ : another description
39
+ `
40
+
41
+ const html = micromark(md, {
42
+ extensions: [dlList()],
43
+ htmlExtensions: [dlListHtml()]
44
+ })
45
+
46
+ console.log(html)
47
+ ```
48
+
49
+ Output:
50
+
51
+ ```html
52
+ <dl>
53
+ <dt>term</dt>
54
+ <dd>description</dd>
55
+ <dd>another description</dd>
56
+ </dl>
57
+ ```
58
+
59
+ ## What this package does
60
+
61
+ * Adds colon-based definition list syntax to micromark
62
+ * Emits tokens for `<dl>`, `<dt>`, and `<dd>`
63
+
64
+ ## What this package does NOT do
65
+
66
+ - Does not generate mdast nodes
67
+ - Does not provide a remark plugin
68
+
69
+ ## Related packages
70
+
71
+ This package is part of the **[unified-dl-list](https://github.com/kanemu/unified-dl-list)** monorepo:
72
+
73
+ - [`remark-dl-list`](https://github.com/kanemu/unified-dl-list/tree/main/packages/remark-dl-list)
74
+ - [`mdast-util-dl-list`](https://github.com/kanemu/unified-dl-list/tree/main/packages/mdast-util-dl-list)
75
+ - [`hast-util-dl-list`](https://github.com/kanemu/unified-dl-list/tree/main/packages/hast-util-dl-list)
76
+
77
+ ## License
78
+
79
+ MIT
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "micromark-extension-dl-list",
3
+ "version": "0.1.0",
4
+ "description": "A micromark extension that adds colon-based definition list (<dl>, <dt>, <dd>) support with CommonMark-compatible parsing rules.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/kanemu/unified-dl-list",
10
+ "directory": "packages/micromark-extension-dl-list"
11
+ },
12
+ "homepage": "https://github.com/kanemu/unified-dl-list/tree/main/packages/micromark-extension-dl-list",
13
+ "bugs": {
14
+ "url": "https://github.com/kanemu/unified-dl-list/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.d.ts",
19
+ "import": "./src/index.js"
20
+ },
21
+ "./syntax.js": {
22
+ "import": "./src/syntax.js"
23
+ },
24
+ "./html.js": {
25
+ "import": "./src/html.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "src/**/*.js",
30
+ "src/**/*.d.ts",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "dependencies": {
35
+ "micromark": "^4.0.2",
36
+ "micromark-util-character": "^2.1.1",
37
+ "micromark-util-symbol": "^2.0.1"
38
+ },
39
+ "keywords": [
40
+ "micromark",
41
+ "micromark-extension",
42
+ "markdown",
43
+ "definition-list",
44
+ "dl",
45
+ "dt",
46
+ "dd",
47
+ "commonmark"
48
+ ],
49
+ "author": "Yohei Kanamura",
50
+ "license": "MIT",
51
+ "scripts": {
52
+ "test": "node --test ./test/*.test.js"
53
+ }
54
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Tab width used for column calculation.
3
+ * CommonMark / micromark treat a tab stop as 4 columns in indentation contexts.
4
+ *
5
+ * @type {number}
6
+ */
7
+ export const TAB_SIZE = 4
8
+
9
+ /**
10
+ * Upper bound (exclusive) of indentation columns allowed before `:` to start a dl-list construct.
11
+ *
12
+ * - CommonMark list rule allows up to 3 columns of indentation (0–3).
13
+ * - We keep it as an exclusive upper bound so callers can write `col < MAX_PREFIX_COLS`.
14
+ *
15
+ * @type {number}
16
+ */
17
+ export const MAX_PREFIX_COLS = 4
package/src/html.js ADDED
@@ -0,0 +1,207 @@
1
+ import { micromark } from 'micromark'
2
+ import { dlList } from './syntax.js'
3
+
4
+ /**
5
+ * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
6
+ */
7
+
8
+ /**
9
+ * @typedef {object} DlListHtmlOptions
10
+ * @property {number} [maxDepth] Maximum recursion depth for nested dl parsing inside dd containers.
11
+ */
12
+
13
+ function deindentByColumns(raw, cols) {
14
+ if (!cols) return raw
15
+
16
+ const text = raw.replace(/\r\n?/g, '\n')
17
+ const lines = text.split('\n')
18
+
19
+ return lines
20
+ .map((line) => {
21
+ let col = 0
22
+ let i = 0
23
+
24
+ while (i < line.length && col < cols) {
25
+ const ch = line.charCodeAt(i)
26
+
27
+ if (ch === 0x20) {
28
+ col += 1
29
+ i += 1
30
+ continue
31
+ }
32
+
33
+ if (ch === 0x09) {
34
+ const r = col % 4
35
+ const step = r === 0 ? 4 : 4 - r
36
+ if (col + step > cols) break
37
+ col += step
38
+ i += 1
39
+ continue
40
+ }
41
+
42
+ break
43
+ }
44
+
45
+ return line.slice(i)
46
+ })
47
+ .join('\n')
48
+ }
49
+
50
+ function isListMarkerLine(line) {
51
+ return /^([-*]|\d+\.)\s/.test(line)
52
+ }
53
+
54
+ function normalizeFlatListIndentInDd(raw) {
55
+ const text = raw.replace(/\r\n?/g, '\n')
56
+ const lines = text.split('\n')
57
+
58
+ let firstIdx = -1
59
+ for (let i = 0; i < lines.length; i++) {
60
+ if (lines[i].trim() !== '') {
61
+ firstIdx = i
62
+ break
63
+ }
64
+ }
65
+ if (firstIdx === -1) return raw
66
+
67
+ const first = lines[firstIdx]
68
+ if (!isListMarkerLine(first)) return raw
69
+
70
+ for (let i = firstIdx + 1; i < lines.length; i++) {
71
+ const line = lines[i]
72
+ if (line.startsWith(' ') && isListMarkerLine(line.slice(2))) {
73
+ lines[i] = line.slice(2)
74
+ }
75
+ }
76
+
77
+ return lines.join('\n')
78
+ }
79
+
80
+ function normalizeNestedDlIndentInDd(raw) {
81
+ const text = raw.replace(/\r\n?/g, '\n')
82
+ const lines = text.split('\n')
83
+
84
+ let firstIdx = -1
85
+ for (let i = 0; i < lines.length; i++) {
86
+ if (lines[i].trim() !== '') {
87
+ firstIdx = i
88
+ break
89
+ }
90
+ }
91
+ if (firstIdx === -1) return raw
92
+ if (!lines[firstIdx].startsWith(':')) return raw
93
+
94
+ for (let i = firstIdx + 1; i < lines.length; i++) {
95
+ const line = lines[i]
96
+ if (!line.startsWith(' ')) continue
97
+
98
+ let n = 0
99
+ while (n < line.length && line.charCodeAt(n) === 0x20) n++
100
+
101
+ if (n >= 2 && line.charCodeAt(n) === 0x3a && (n - 2) % 4 === 0) {
102
+ lines[i] = line.slice(2)
103
+ }
104
+ }
105
+
106
+ return lines.join('\n')
107
+ }
108
+
109
+ function renderInlineMarkdown(raw) {
110
+ let html = micromark(raw)
111
+ const m = html.match(/^<p>([\s\S]*)<\/p>\n?$/)
112
+ if (m) return m[1]
113
+ return html
114
+ }
115
+
116
+ /**
117
+ * HTML extension for dl-list tokens.
118
+ *
119
+ * - `dlTermText` is rendered as inline markdown.
120
+ * - `dlDescContainer` is deindented and re-parsed as block markdown (with nested dl support).
121
+ *
122
+ * @param {DlListHtmlOptions=} options
123
+ * @returns {HtmlExtension}
124
+ */
125
+ export function dlListHtml(options = {}) {
126
+ const maxDepth = options.maxDepth ?? 8
127
+
128
+ /** @type {HtmlExtension} */
129
+ return {
130
+ enter: {
131
+ dlList() {
132
+ this.tag('<dl>')
133
+ },
134
+
135
+ dlItem() { },
136
+
137
+ dlTerm() {
138
+ this.tag('<dt>')
139
+ },
140
+ dlDesc() {
141
+ this.tag('<dd>')
142
+ },
143
+
144
+ dlIndent() { },
145
+ dlMarkerSpace() { },
146
+ dlLineEnding() { },
147
+
148
+ dlHardBreak() {
149
+ this.raw('\n')
150
+ },
151
+
152
+ dlTermText() { },
153
+
154
+ dlDescContainer() { }
155
+ },
156
+
157
+ exit: {
158
+ dlList() {
159
+ this.tag('</dl>')
160
+ },
161
+
162
+ dlItem() { },
163
+
164
+ dlTerm() {
165
+ this.tag('</dt>')
166
+ },
167
+ dlDesc() {
168
+ this.tag('</dd>')
169
+ },
170
+
171
+ dlTermText(token) {
172
+ const raw = this.sliceSerialize(token)
173
+ this.raw(renderInlineMarkdown(raw))
174
+ },
175
+
176
+ dlIndent() { },
177
+ dlMarkerSpace() { },
178
+ dlLineEnding() { },
179
+
180
+ dlDescContainer(token) {
181
+ let raw = this.sliceSerialize(token)
182
+ raw = deindentByColumns(raw, token._dlIndent || 0)
183
+ raw = normalizeFlatListIndentInDd(raw)
184
+ raw = normalizeNestedDlIndentInDd(raw)
185
+
186
+ if (maxDepth <= 0) {
187
+ this.raw(micromark(raw))
188
+ return
189
+ }
190
+
191
+ const html = micromark(raw, {
192
+ extensions: [dlList()],
193
+ htmlExtensions: [dlListHtml({ maxDepth: maxDepth - 1 })]
194
+ })
195
+
196
+ const trimmed = html.replace(/^\s+|\s+$/g, '')
197
+ const m = trimmed.match(/^<p>([\s\S]*)<\/p>$/)
198
+ if (m) {
199
+ this.raw(m[1])
200
+ return
201
+ }
202
+
203
+ this.raw(html)
204
+ }
205
+ }
206
+ }
207
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Extension, HtmlExtension } from "micromark-util-types";
2
+
3
+ export function dlList(): Extension;
4
+
5
+ export type DlListHtmlOptions = {
6
+ maxDepth?: number;
7
+ };
8
+
9
+ export function dlListHtml(options?: DlListHtmlOptions): HtmlExtension;
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { dlList } from './syntax.js'
2
+ export { dlListHtml } from './html.js'
@@ -0,0 +1,169 @@
1
+ import { markdownLineEnding } from 'micromark-util-character'
2
+ import { codes } from 'micromark-util-symbol'
3
+ import { MAX_PREFIX_COLS } from './constants.js'
4
+ import { isEof, isIndent, consumeSafe, advanceColumn } from './util.js'
5
+
6
+ /**
7
+ * Lookahead tokenizers used by dl-list to confirm start / continuation without consuming input.
8
+ *
9
+ * These are separated from tokenize.js to keep the main tokenizer readable.
10
+ */
11
+
12
+ /**
13
+ * Check whether the current line can start dl-list after optional indentation (<= 3 cols).
14
+ * Succeeds when it can reach ':' before exceeding MAX_PREFIX_COLS.
15
+ */
16
+ export function checkPrefixFactory() {
17
+ return {
18
+ tokenize(effects2, ok2, nok2) {
19
+ let col = 0
20
+ return start2
21
+
22
+ /** @type {import('micromark-util-types').State} */
23
+ function start2(code) {
24
+ if (isEof(code)) return nok2(code)
25
+ if (code === codes.colon) return ok2(code)
26
+
27
+ if (isIndent(code) && col < MAX_PREFIX_COLS) {
28
+ col = advanceColumn(col, code)
29
+ consumeSafe(effects2, code)
30
+ if (col > MAX_PREFIX_COLS) return nok2(code)
31
+ return start2
32
+ }
33
+
34
+ return nok2(code)
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Check whether the dl-list should continue after a line ending.
42
+ *
43
+ * - ok when next line begins with ':' or indentation
44
+ * - stop on blank line (do not consume blank-line EOL in the main tokenizer)
45
+ */
46
+ export function checkAfterEolContinueFactory() {
47
+ return {
48
+ tokenize(effects2, ok2, nok2) {
49
+ let opened = false
50
+ return start2
51
+
52
+ function start2(code) {
53
+ if (isEof(code)) return ok2(code)
54
+ if (!markdownLineEnding(code)) return nok2(code)
55
+
56
+ effects2.enter('dlCheck')
57
+ opened = true
58
+
59
+ // consume the line ending so we can inspect the next line head
60
+ effects2.consume(code)
61
+ return head
62
+ }
63
+
64
+ function head(code) {
65
+ if (isEof(code)) return endOk(code)
66
+ if (markdownLineEnding(code)) return endNok(code) // blank line -> stop
67
+ if (code === codes.colon) return endOk(code)
68
+ if (isIndent(code)) return endOk(code)
69
+ return endNok(code)
70
+ }
71
+
72
+ function endOk(code) {
73
+ if (opened) {
74
+ effects2.exit('dlCheck')
75
+ opened = false
76
+ }
77
+ return ok2(code)
78
+ }
79
+
80
+ function endNok(code) {
81
+ if (opened) {
82
+ effects2.exit('dlCheck')
83
+ opened = false
84
+ }
85
+ return nok2(code)
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Check whether a ':' at baseIndent can start a dl-list.
93
+ *
94
+ * Requires:
95
+ * - the ':' line itself exists
96
+ * - next line is EOF or blank, OR
97
+ * - next line begins with ':' at baseIndent or ddIndent, OR
98
+ * - next line is indented beyond baseIndent (continuation for dt)
99
+ */
100
+ export function checkDlStartFactory(baseIndentArg, ddIndentArg) {
101
+ return {
102
+ tokenize(effects2, ok2, nok2) {
103
+ let col = 0
104
+ let opened = false
105
+ return start2
106
+
107
+ function start2(code) {
108
+ if (isEof(code)) return nok2(code)
109
+ if (code !== codes.colon) return nok2(code)
110
+
111
+ effects2.enter('dlCheck')
112
+ opened = true
113
+
114
+ effects2.consume(code) // ':'
115
+ return restOfLine
116
+ }
117
+
118
+ function restOfLine(code) {
119
+ if (isEof(code)) return endOk(code)
120
+
121
+ if (markdownLineEnding(code)) {
122
+ effects2.consume(code) // consume EOL to inspect next line head
123
+ col = 0
124
+ return nextLineHead
125
+ }
126
+
127
+ effects2.consume(code)
128
+ return restOfLine
129
+ }
130
+
131
+ function nextLineHead(code) {
132
+ if (isEof(code)) return endOk(code) // allow EOF
133
+ if (markdownLineEnding(code)) return endOk(code) // allow blank line
134
+
135
+ if (isIndent(code) && col < 512) {
136
+ col = advanceColumn(col, code)
137
+ effects2.consume(code)
138
+ return nextLineHead
139
+ }
140
+
141
+ // next line must start a field: ":" at baseIndent or ddIndent
142
+ if (code === codes.colon && (col === baseIndentArg || col === ddIndentArg)) {
143
+ return endOk(code)
144
+ }
145
+
146
+ // allow an indented, non-blank continuation line for the term
147
+ if (col > baseIndentArg) return endOk(code)
148
+
149
+ return endNok(code)
150
+ }
151
+
152
+ function endOk(code) {
153
+ if (opened) {
154
+ effects2.exit('dlCheck')
155
+ opened = false
156
+ }
157
+ return ok2(code)
158
+ }
159
+
160
+ function endNok(code) {
161
+ if (opened) {
162
+ effects2.exit('dlCheck')
163
+ opened = false
164
+ }
165
+ return nok2(code)
166
+ }
167
+ }
168
+ }
169
+ }
package/src/syntax.js ADDED
@@ -0,0 +1,31 @@
1
+ import { codes } from 'micromark-util-symbol'
2
+ import { tokenizeDlList } from './tokenize.js'
3
+
4
+ /**
5
+ * Micromark extension for colon-based definition lists.
6
+ *
7
+ * Syntax (flow):
8
+ * - A line whose first non-indentation character within 0–3 columns is `:` starts a dl-list.
9
+ * - The first `:` line is a term (`dt`).
10
+ * - Subsequent lines indented by 4+ columns and starting with `:` are descriptions (`dd`).
11
+ * - Continuation lines (indented, without `:`) are appended to the last opened dt/dd.
12
+ *
13
+ * Design constraints:
14
+ * - Do not consume indentation unless dl-list is confirmed by lookahead.
15
+ * - Do not consume blank-line EOL that terminates the list.
16
+ *
17
+ * @returns {import('micromark-util-types').Extension}
18
+ */
19
+ export function dlList() {
20
+ /** @type {import('micromark-util-types').Construct} */
21
+ const construct = { name: 'dlList', tokenize: tokenizeDlList, concrete: true }
22
+
23
+ /** @type {import('micromark-util-types').Extension} */
24
+ return {
25
+ flow: {
26
+ [codes.colon]: construct,
27
+ [codes.space]: construct,
28
+ [codes.ht]: construct
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,462 @@
1
+ import { markdownLineEnding } from 'micromark-util-character'
2
+ import { codes } from 'micromark-util-symbol'
3
+ import { MAX_PREFIX_COLS } from './constants.js'
4
+ import {
5
+ isEof,
6
+ isIndent,
7
+ consumeSafe,
8
+ advanceColumn,
9
+ consumeLineEndingSafe
10
+ } from './util.js'
11
+ import {
12
+ checkPrefixFactory,
13
+ checkAfterEolContinueFactory,
14
+ checkDlStartFactory
15
+ } from './lookahead.js'
16
+
17
+ /**
18
+ * Tokenize a dl-list at flow level.
19
+ *
20
+ * @internal
21
+ * @this {import('micromark-util-types').TokenizeContext}
22
+ * @param {import('micromark-util-types').Effects} effects
23
+ * @param {import('micromark-util-types').State} ok
24
+ * @param {import('micromark-util-types').State} nok
25
+ * @returns {import('micromark-util-types').State}
26
+ */
27
+ export function tokenizeDlList(effects, ok, nok) {
28
+ /** @type {number} */
29
+ let baseIndent = 0
30
+
31
+ /** @type {number} */
32
+ let ddIndent = 4
33
+
34
+ /** @type {'term'|'desc'|null} */
35
+ let lastField = null
36
+
37
+ /** @type {boolean} */
38
+ let listOpen = false
39
+ /** @type {boolean} */
40
+ let itemOpen = false
41
+ /** @type {boolean} */
42
+ let termOpen = false
43
+ /** @type {boolean} */
44
+ let descOpen = false
45
+ /** @type {boolean} */
46
+ let termTextOpen = false
47
+
48
+ const closeTermTextIfOpen = () => {
49
+ if (termTextOpen) {
50
+ effects.exit('dlTermText')
51
+ termTextOpen = false
52
+ }
53
+ }
54
+
55
+ const closeTermIfOpen = () => {
56
+ closeTermTextIfOpen()
57
+ if (termOpen) {
58
+ effects.exit('dlTerm')
59
+ termOpen = false
60
+ }
61
+ }
62
+
63
+ const closeDescIfOpen = () => {
64
+ if (descOpen) {
65
+ effects.exit('dlDesc')
66
+ descOpen = false
67
+ }
68
+ }
69
+
70
+ const closeFieldIfOpen = () => {
71
+ closeDescIfOpen()
72
+ closeTermIfOpen()
73
+ lastField = null
74
+ }
75
+
76
+ const closeItemIfOpen = () => {
77
+ closeFieldIfOpen()
78
+ if (itemOpen) {
79
+ effects.exit('dlItem')
80
+ itemOpen = false
81
+ }
82
+ }
83
+
84
+ const closeAll = () => {
85
+ closeItemIfOpen()
86
+ if (listOpen) {
87
+ effects.exit('dlList')
88
+ listOpen = false
89
+ }
90
+ }
91
+
92
+ const start = (code) => {
93
+ baseIndent = 0
94
+ ddIndent = 4
95
+ lastField = null
96
+ listOpen = false
97
+ itemOpen = false
98
+ termOpen = false
99
+ descOpen = false
100
+ termTextOpen = false
101
+ return prefix(code, 0)
102
+ }
103
+
104
+ const prefix = (code, col) => {
105
+ if (isEof(code)) return nok(code)
106
+
107
+ if (code === codes.colon) {
108
+ if (col > 3) return nok(code)
109
+
110
+ return effects.check(
111
+ checkDlStartFactory(col, col + 4),
112
+ onOk,
113
+ nok
114
+ )(code)
115
+
116
+ function onOk() {
117
+ baseIndent = col
118
+ ddIndent = baseIndent + 4
119
+ effects.enter('dlList')
120
+ listOpen = true
121
+ return termMarker(code)
122
+ }
123
+ }
124
+
125
+ if (isIndent(code) && col < MAX_PREFIX_COLS) {
126
+ return effects.check(checkPrefixFactory(), onOk, nok)(code)
127
+ function onOk() {
128
+ return prefixConsume(code, 0)
129
+ }
130
+ }
131
+
132
+ return nok(code)
133
+ }
134
+
135
+ const prefixConsume = (code, col) => {
136
+ if (isEof(code)) return nok(code)
137
+
138
+ if (code === codes.colon) {
139
+ if (col > 3) return nok(code)
140
+
141
+ return effects.check(
142
+ checkDlStartFactory(col, col + 4),
143
+ onOk,
144
+ nok
145
+ )(code)
146
+
147
+ function onOk() {
148
+ baseIndent = col
149
+ ddIndent = baseIndent + 4
150
+ effects.enter('dlList')
151
+ listOpen = true
152
+ return termMarker(code)
153
+ }
154
+ }
155
+
156
+ if (isIndent(code) && col < MAX_PREFIX_COLS) {
157
+ effects.enter('dlIndent')
158
+ consumeSafe(effects, code)
159
+ effects.exit('dlIndent')
160
+
161
+ const nextCol = advanceColumn(col, code)
162
+ if (nextCol > MAX_PREFIX_COLS) return nok(code)
163
+
164
+ return (c) => prefixConsume(c, nextCol)
165
+ }
166
+
167
+ return nok(code)
168
+ }
169
+
170
+ const termMarker = (code) => {
171
+ if (isEof(code) || code !== codes.colon) return nok(code)
172
+
173
+ closeItemIfOpen()
174
+
175
+ effects.enter('dlItem')
176
+ itemOpen = true
177
+
178
+ effects.enter('dlTerm')
179
+ termOpen = true
180
+ lastField = 'term'
181
+
182
+ consumeSafe(effects, code) // ':'
183
+ return afterMarkerToTerm
184
+ }
185
+
186
+ const afterMarkerToTerm = (code) => {
187
+ if (isEof(code) || markdownLineEnding(code)) return afterEol(code)
188
+
189
+ if (isIndent(code)) {
190
+ effects.enter('dlMarkerSpace')
191
+ consumeSafe(effects, code)
192
+ effects.exit('dlMarkerSpace')
193
+ return termTextStart
194
+ }
195
+
196
+ return termTextStart(code)
197
+ }
198
+
199
+ const termTextStart = (code) => {
200
+ effects.enter('dlTermText')
201
+ termTextOpen = true
202
+ return termText(code)
203
+ }
204
+
205
+ const termText = (code) => {
206
+ if (isEof(code) || markdownLineEnding(code)) {
207
+ effects.exit('dlTermText')
208
+ termTextOpen = false
209
+ return afterEol(code)
210
+ }
211
+ consumeSafe(effects, code)
212
+ return termText
213
+ }
214
+
215
+ const afterEol = (code) => {
216
+ if (isEof(code)) {
217
+ closeAll()
218
+ return ok(code)
219
+ }
220
+ if (!markdownLineEnding(code)) return nok(code)
221
+
222
+ // Decide whether to continue *before* claiming the line ending.
223
+ // If we stop here, leave the line ending to the parent tokenizer.
224
+ return effects.check(
225
+ checkAfterEolContinueFactory(),
226
+ onContinue,
227
+ onStop
228
+ )(code)
229
+
230
+ function onContinue() {
231
+ consumeLineEndingSafe(effects, code)
232
+ return lineStart
233
+ }
234
+
235
+ function onStop() {
236
+ closeAll()
237
+ return ok(code)
238
+ }
239
+ }
240
+
241
+ const lineStart = (code) => {
242
+ if (isEof(code) || markdownLineEnding(code)) {
243
+ closeAll()
244
+ return ok(code)
245
+ }
246
+
247
+ if (code === codes.colon) {
248
+ closeFieldIfOpen()
249
+ return termMarker(code)
250
+ }
251
+
252
+ if (isIndent(code)) return scanIndent(code, 0)
253
+
254
+ closeAll()
255
+ return ok(code)
256
+ }
257
+
258
+ const scanIndent = (code, col) => {
259
+ if (isEof(code)) {
260
+ closeAll()
261
+ return ok(code)
262
+ }
263
+
264
+ if (isIndent(code) && col < 512) {
265
+ effects.enter('dlIndent')
266
+ consumeSafe(effects, code)
267
+ effects.exit('dlIndent')
268
+
269
+ const nextCol = advanceColumn(col, code)
270
+ return (c) => scanIndent(c, nextCol)
271
+ }
272
+
273
+ if (col >= ddIndent && code === codes.colon) {
274
+ closeTermIfOpen()
275
+ return descMarker(code)
276
+ }
277
+
278
+ if (col === baseIndent && code === codes.colon) {
279
+ closeFieldIfOpen()
280
+ return termMarker(code)
281
+ }
282
+
283
+ if (col > baseIndent) {
284
+ if (!termOpen && !descOpen) {
285
+ closeAll()
286
+ return ok(code)
287
+ }
288
+ return continuationLine(code)
289
+ }
290
+
291
+ closeAll()
292
+ return ok(code)
293
+ }
294
+
295
+ const descMarker = (code) => {
296
+ if (isEof(code) || code !== codes.colon) return nok(code)
297
+
298
+ closeDescIfOpen()
299
+
300
+ effects.enter('dlDesc')
301
+ descOpen = true
302
+ lastField = 'desc'
303
+
304
+ consumeSafe(effects, code) // ':'
305
+ return afterMarkerToDesc
306
+ }
307
+
308
+ const afterMarkerToDesc = (code) => {
309
+ if (isEof(code)) return afterEol(code)
310
+
311
+ // IMPORTANT:
312
+ // Even if the dd marker line ends immediately, open a container so
313
+ // subsequent indented lines become part of this dd.
314
+ // (This removes the need for dlDescText.)
315
+ if (markdownLineEnding(code)) {
316
+ const t = effects.enter('dlDescContainer')
317
+ // @ts-ignore
318
+ t._dlIndent = ddIndent
319
+ return descContainerContent(code)
320
+ }
321
+
322
+ // dd マーカー直後のスペースはコンテナに入れない(見た目調整用)
323
+ if (isIndent(code)) {
324
+ effects.enter('dlMarkerSpace')
325
+ consumeSafe(effects, code)
326
+ effects.exit('dlMarkerSpace')
327
+ return afterMarkerToDesc
328
+ }
329
+
330
+ const t = effects.enter('dlDescContainer')
331
+ // html.js が参照する deindent 量(columns)
332
+ // @ts-ignore
333
+ t._dlIndent = ddIndent
334
+
335
+ return descContainerContent(code)
336
+ }
337
+
338
+ const descContainerContent = (code) => {
339
+ if (isEof(code)) {
340
+ effects.exit('dlDescContainer')
341
+ closeAll()
342
+ return ok(code)
343
+ }
344
+
345
+ if (markdownLineEnding(code)) {
346
+ return effects.check(
347
+ checkAfterEolContinueFactory(),
348
+ onContinue,
349
+ onStop
350
+ )(code)
351
+
352
+ function onContinue() {
353
+ consumeLineEndingSafe(effects, code)
354
+ return descContainerLineStart
355
+ }
356
+
357
+ function onStop() {
358
+ effects.exit('dlDescContainer')
359
+ closeAll()
360
+ return ok(code)
361
+ }
362
+ }
363
+
364
+ consumeSafe(effects, code)
365
+ return descContainerContent
366
+ }
367
+
368
+ const descContainerLineStart = (code) => {
369
+ if (isEof(code)) {
370
+ effects.exit('dlDescContainer')
371
+ closeAll()
372
+ return ok(code)
373
+ }
374
+
375
+ if (markdownLineEnding(code)) {
376
+ // 空行はコンテナに含める(段落分離に必要)
377
+ consumeLineEndingSafe(effects, code)
378
+ return descContainerLineStart
379
+ }
380
+
381
+ // 次行が ":" で始まる (= 次の term / 同階層) 場合、dd コンテナを閉じて tokenizer 側で処理
382
+ if (code === codes.colon) {
383
+ effects.exit('dlDescContainer')
384
+ closeDescIfOpen()
385
+ return lineStart(code)
386
+ }
387
+
388
+ if (isIndent(code)) return descContainerScanIndent(code, 0)
389
+
390
+ // インデント無しは dl-list 終了
391
+ effects.exit('dlDescContainer')
392
+ closeAll()
393
+ return ok(code)
394
+ }
395
+
396
+ const descContainerScanIndent = (code, col) => {
397
+ if (isEof(code)) {
398
+ effects.exit('dlDescContainer')
399
+ closeAll()
400
+ return ok(code)
401
+ }
402
+
403
+ if (isIndent(code) && col < 512) {
404
+ // コンテナなので indent もそのまま入れる
405
+ consumeSafe(effects, code)
406
+ const nextCol = advanceColumn(col, code)
407
+ return (c) => descContainerScanIndent(c, nextCol)
408
+ }
409
+
410
+ if (col === baseIndent && code === codes.colon) {
411
+ effects.exit('dlDescContainer')
412
+ closeDescIfOpen()
413
+ return termMarker(code)
414
+ }
415
+
416
+ // 深いインデントの ":" は dd 本文(入れ子 dl 等)の可能性があるので閉じない
417
+ if (col === ddIndent && code === codes.colon) {
418
+ effects.exit('dlDescContainer')
419
+ closeDescIfOpen()
420
+ return descMarker(code)
421
+ }
422
+
423
+ // それ以外は dd 本文継続(この行の残りを食う)
424
+ return descContainerContent(code)
425
+ }
426
+
427
+ const continuationLine = (code) => {
428
+ effects.enter('dlHardBreak')
429
+ effects.exit('dlHardBreak')
430
+
431
+ if (lastField === 'term' && termOpen) {
432
+ effects.enter('dlTermText')
433
+ termTextOpen = true
434
+ return contTextAsTerm(code)
435
+ }
436
+
437
+ if (lastField === 'desc' && descOpen) {
438
+ // Fallback safety:
439
+ // If we ever reach here with an open dd but no container,
440
+ // treat continuation as dd container content (no dlDescText).
441
+ const t = effects.enter('dlDescContainer')
442
+ // @ts-ignore
443
+ t._dlIndent = ddIndent
444
+ return descContainerContent(code)
445
+ }
446
+
447
+ closeAll()
448
+ return ok(code)
449
+ }
450
+
451
+ const contTextAsTerm = (code) => {
452
+ if (isEof(code) || markdownLineEnding(code)) {
453
+ effects.exit('dlTermText')
454
+ termTextOpen = false
455
+ return afterEol(code)
456
+ }
457
+ consumeSafe(effects, code)
458
+ return contTextAsTerm
459
+ }
460
+
461
+ return start
462
+ }
package/src/util.js ADDED
@@ -0,0 +1,72 @@
1
+ import { markdownLineEnding } from 'micromark-util-character'
2
+ import { codes } from 'micromark-util-symbol'
3
+ import { TAB_SIZE } from './constants.js'
4
+
5
+ /**
6
+ * @param {number|null} code
7
+ * @returns {boolean}
8
+ */
9
+ export function isEof(code) {
10
+ return code === codes.eof
11
+ }
12
+
13
+ /**
14
+ * True for indentation codes that micromark uses at line starts.
15
+ *
16
+ * @param {number|null} code
17
+ * @returns {boolean}
18
+ */
19
+ export function isIndent(code) {
20
+ return (
21
+ code === codes.space ||
22
+ code === codes.ht ||
23
+ code === codes.virtualSpace ||
24
+ code === codes.horizontalTab
25
+ )
26
+ }
27
+
28
+ /**
29
+ * Consume a code only when it represents a number.
30
+ * (micromark uses negative “virtual” codes too; those are still numbers and are valid to consume.)
31
+ *
32
+ * @param {import('micromark-util-types').Effects} effects
33
+ * @param {number|null} code
34
+ * @returns {void}
35
+ */
36
+ export function consumeSafe(effects, code) {
37
+ if (typeof code !== 'number') return
38
+ effects.consume(code)
39
+ }
40
+
41
+ /**
42
+ * Advance column count by one character, respecting TAB_SIZE.
43
+ *
44
+ * @param {number} col
45
+ * @param {number|null} code
46
+ * @returns {number}
47
+ */
48
+ export function advanceColumn(col, code) {
49
+ if (code === codes.ht || code === codes.horizontalTab) {
50
+ const r = col % TAB_SIZE
51
+ return col + (r === 0 ? TAB_SIZE : TAB_SIZE - r)
52
+ }
53
+ return col + 1
54
+ }
55
+
56
+ /**
57
+ * Consume a line ending as a token (`dlLineEnding`) so downstream can reason about it.
58
+ *
59
+ * NOTE:
60
+ * - Do not call this in the "blank line ends dl-list" path.
61
+ * In that case, the EOL must remain for CommonMark to see the blank-line boundary.
62
+ *
63
+ * @param {import('micromark-util-types').Effects} effects
64
+ * @param {number|null} code
65
+ * @returns {void}
66
+ */
67
+ export function consumeLineEndingSafe(effects, code) {
68
+ if (!markdownLineEnding(code)) return
69
+ effects.enter('dlLineEnding')
70
+ effects.consume(code)
71
+ effects.exit('dlLineEnding')
72
+ }