markdown-it-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-ja.md ADDED
@@ -0,0 +1,133 @@
1
+ # markdown-it-dl-list
2
+
3
+ `<dl>`, `<dt>`, `<dd>` を使った
4
+ **コロン記法の定義リスト**をサポートする **markdown-it プラグイン**です。
5
+
6
+ Pandoc などに見られる定義リスト構文に着想を得た、
7
+ シンプルで読みやすい記法を markdown-it で利用できます。
8
+
9
+ ## 特徴
10
+
11
+ - コロン(`:`)による定義リスト構文
12
+ - `<dl>`, `<dt>`, `<dd>` を生成
13
+ - 1つの用語に複数の定義を記述可能
14
+ - 定義を持たない用語(dt-only)に対応
15
+ - 入れ子の定義リストをサポート
16
+ - 標準的な markdown-it の処理フローに統合可能
17
+
18
+ ## インストール
19
+
20
+ ```bash
21
+ npm install markdown-it-dl-list
22
+ ```
23
+
24
+ ## 使い方
25
+
26
+ ```js
27
+ import MarkdownIt from "markdown-it";
28
+ import dlList from "markdown-it-dl-list";
29
+
30
+ const md = new MarkdownIt();
31
+ md.use(dlList);
32
+
33
+ const src = `
34
+ : 用語
35
+ : 説明文その1
36
+ : 説明文その2
37
+ `;
38
+
39
+ console.log(md.render(src));
40
+ ```
41
+
42
+ 出力結果:
43
+
44
+ ```html
45
+ <dl>
46
+ <dt>用語</dt>
47
+ <dd>説明文その1</dd>
48
+ <dd>説明文その2</dd>
49
+ </dl>
50
+ ```
51
+
52
+ ## 構文
53
+
54
+ ### 基本形
55
+
56
+ ```markdown
57
+ : 用語
58
+ : 説明文
59
+ ```
60
+
61
+ ### 複数の定義
62
+
63
+ ```markdown
64
+ : 用語
65
+ : 説明文その1
66
+ : 説明文その2
67
+ ```
68
+
69
+ ### 定義を持たない用語(dt-only)
70
+
71
+ 定義を持たない用語は、
72
+ **直後が空行またはファイル末尾の場合のみ**有効です。
73
+
74
+ ```markdown
75
+ : 用語のみ
76
+
77
+ 次の行
78
+ ```
79
+
80
+ ### 複数行の用語(dt 継続行)
81
+
82
+ 用語行の直後にインデントされた行が続く場合、
83
+ それらは同じ用語として扱われます。
84
+
85
+ ```markdown
86
+ : これは複数行の
87
+ 用語です。
88
+ : これは複数行の
89
+ 説明文です。
90
+ ```
91
+
92
+ ### 入れ子の定義リスト
93
+
94
+ ```markdown
95
+ : 外側の用語
96
+ : : 内側の用語
97
+ : 内側の説明文
98
+ : 次の説明文
99
+ ```
100
+
101
+ ## オプション
102
+
103
+ ```ts
104
+ type DlListOptions = {
105
+ /** dd 行に必要なインデント(スペース数)。デフォルト: 4 */
106
+ ddIndent?: number;
107
+
108
+ /** 定義(dd)を必須とするかどうか。デフォルト: true */
109
+ requireDd?: boolean;
110
+
111
+ /** 空行で現在の定義リストを終了するか。デフォルト: true */
112
+ breakOnBlankLine?: boolean;
113
+ };
114
+ ```
115
+
116
+ 使用例:
117
+
118
+ ```js
119
+ md.use(dlList, {
120
+ ddIndent: 2,
121
+ requireDd: true,
122
+ });
123
+ ```
124
+
125
+ ## このプラグインが「しないこと」
126
+
127
+ * 定義リスト以外の Markdown の挙動は変更しません
128
+ * markdown-it の標準的な段落処理は維持されます
129
+ * すべての既存定義リスト構文を網羅することは目的としていません
130
+
131
+ ## ライセンス
132
+
133
+ MIT
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # markdown-it-dl-list
2
+
3
+ A **markdown-it plugin** that adds support for **colon-based definition lists**
4
+ using `<dl>`, `<dt>`, and `<dd>`.
5
+
6
+ This plugin enables a simple and readable definition list syntax inspired by
7
+ Pandoc and other Markdown variants.
8
+
9
+ ## Features
10
+
11
+ - Colon-based definition list syntax
12
+ - Supports `<dl>`, `<dt>`, and `<dd>`
13
+ - Multiple definitions per term
14
+ - Term-only entries (dt-only)
15
+ - Nested definition lists
16
+ - Designed to work with standard markdown-it pipelines
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install markdown-it-dl-list
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```js
27
+ import MarkdownIt from "markdown-it";
28
+ import dlList from "markdown-it-dl-list";
29
+
30
+ const md = new MarkdownIt();
31
+ md.use(dlList);
32
+
33
+ const src = `
34
+ : Term
35
+ : Definition line 1
36
+ : Definition line 2
37
+ `;
38
+
39
+ console.log(md.render(src));
40
+ ```
41
+
42
+ Output:
43
+
44
+ ```html
45
+ <dl>
46
+ <dt>Term</dt>
47
+ <dd>Definition line 1</dd>
48
+ <dd>Definition line 2</dd>
49
+ </dl>
50
+ ```
51
+
52
+ ## Syntax
53
+
54
+ ### Basic form
55
+
56
+ ```markdown
57
+ : Term
58
+ : Definition
59
+ ```
60
+
61
+ ### Multiple definitions
62
+
63
+ ```markdown
64
+ : Term
65
+ : First definition
66
+ : Second definition
67
+ ```
68
+
69
+ ### Term-only (dt-only)
70
+
71
+ A term without definitions is allowed **only when followed by a blank line or EOF**:
72
+
73
+ ```markdown
74
+ : Term only
75
+
76
+ Next paragraph.
77
+ ```
78
+
79
+ ### Multiline terms
80
+
81
+ Indented lines following a term are treated as part of the term:
82
+
83
+ ```markdown
84
+ : This is a
85
+ multiline term
86
+ : This is a
87
+ multiline definition
88
+ ```
89
+
90
+ ### Nested definition lists
91
+
92
+ ```markdown
93
+ : Outer term
94
+ : : Inner term
95
+ : Inner definition
96
+ : Next definition
97
+ ```
98
+
99
+ ## Options
100
+
101
+ ```ts
102
+ type DlListOptions = {
103
+ /** Indent (spaces) required for dd lines. Default: 4 */
104
+ ddIndent?: number;
105
+
106
+ /** Require at least one dd unless dt-only is followed by blank line or EOF. Default: true */
107
+ requireDd?: boolean;
108
+
109
+ /** Stop parsing the current dl at the first blank line after items. Default: true */
110
+ breakOnBlankLine?: boolean;
111
+ };
112
+ ```
113
+
114
+ Example:
115
+
116
+ ```js
117
+ md.use(dlList, {
118
+ ddIndent: 2,
119
+ requireDd: true,
120
+ });
121
+ ```
122
+
123
+ ## What this plugin does NOT do
124
+
125
+ * Does not modify Markdown rendering outside definition lists
126
+ * Does not change markdown-it default paragraph behavior
127
+ * Does not attempt to support every existing definition list syntax variant
128
+
129
+ ## License
130
+
131
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,273 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ default: () => dlListPlugin
23
+ });
24
+ module.exports = __toCommonJS(index_exports);
25
+ var RULE_NAME = "dl_list_colon";
26
+ var T_DL_OPEN = "dl_list_open";
27
+ var T_DL_CLOSE = "dl_list_close";
28
+ var T_DT_OPEN = "dl_dt_open";
29
+ var T_DT_CLOSE = "dl_dt_close";
30
+ var T_DD_OPEN = "dl_dd_open";
31
+ var T_DD_CLOSE = "dl_dd_close";
32
+ var DEFAULT_DD_INDENT = 4;
33
+ var DEFAULT_REQUIRE_DD = true;
34
+ var DEFAULT_BREAK_ON_BLANK = true;
35
+ function dlListPlugin(md, opts = {}) {
36
+ const ddIndent = clampInt(opts.ddIndent ?? DEFAULT_DD_INDENT, 1, 12);
37
+ const requireDd = opts.requireDd ?? DEFAULT_REQUIRE_DD;
38
+ const breakOnBlankLine = opts.breakOnBlankLine ?? DEFAULT_BREAK_ON_BLANK;
39
+ md.block.ruler.before("paragraph", RULE_NAME, (state, startLine, endLine, silent) => {
40
+ const begin = startLine;
41
+ const firstDt = readDtBlock(state, begin, endLine, ddIndent);
42
+ if (!firstDt) return false;
43
+ const firstHasDd = hasDdHeaderAtSameLevel(state, firstDt, ddIndent, endLine);
44
+ const firstDtOnlyOk = !firstHasDd && isDtOnlyBoundary(state, firstDt.nextLine, endLine);
45
+ if (requireDd && !firstHasDd && !firstDtOnlyOk) return false;
46
+ if (silent) return true;
47
+ const parsed = parseDlItems(state, begin, endLine, { ddIndent, requireDd, breakOnBlankLine });
48
+ if (!parsed) return false;
49
+ renderDlTokens(state, begin, parsed.endLine, parsed.items);
50
+ state.line = parsed.endLine;
51
+ return true;
52
+ });
53
+ md.renderer.rules[T_DL_OPEN] = () => "<dl>\n";
54
+ md.renderer.rules[T_DL_CLOSE] = () => "</dl>\n";
55
+ md.renderer.rules[T_DT_OPEN] = () => "<dt>";
56
+ md.renderer.rules[T_DT_CLOSE] = () => "</dt>\n";
57
+ md.renderer.rules[T_DD_OPEN] = () => "<dd>";
58
+ md.renderer.rules[T_DD_CLOSE] = () => "</dd>\n";
59
+ }
60
+ function parseDlItems(state, begin, endLine, ctx) {
61
+ const items = [];
62
+ let line = begin;
63
+ while (line < endLine) {
64
+ const dtBlock = readDtBlock(state, line, endLine, ctx.ddIndent);
65
+ if (!dtBlock) break;
66
+ const { dds, nextLineAfterDds } = collectDds(state, dtBlock, endLine, ctx.ddIndent);
67
+ if (dds.length === 0) {
68
+ const afterDt = dtBlock.nextLine;
69
+ const dtOnlyHere = isDtOnlyBoundary(state, afterDt, endLine);
70
+ if (ctx.requireDd && !dtOnlyHere) break;
71
+ items.push({ dtLine: line, dtText: dtBlock.text, dds: [] });
72
+ line = afterDt;
73
+ break;
74
+ }
75
+ items.push({ dtLine: line, dtText: dtBlock.text, dds });
76
+ line = nextLineAfterDds;
77
+ if (ctx.breakOnBlankLine && line < endLine && isBlankLine(state, line)) break;
78
+ }
79
+ if (items.length === 0) return null;
80
+ return { items, endLine: line };
81
+ }
82
+ function renderDlTokens(state, begin, endLine, items) {
83
+ const dlOpen = state.push(T_DL_OPEN, "dl", 1);
84
+ dlOpen.map = [begin, endLine];
85
+ for (const it of items) {
86
+ const dtOpen = state.push(T_DT_OPEN, "dt", 1);
87
+ dtOpen.map = [it.dtLine, it.dtLine + 1];
88
+ pushInline(state, it.dtText, it.dtLine);
89
+ state.push(T_DT_CLOSE, "dt", -1);
90
+ for (const d of it.dds) {
91
+ state.push(T_DD_OPEN, "dd", 1);
92
+ if (shouldBlockParseDd(d.text)) {
93
+ const ddText = looksLikeNestedDl(d.text) ? normalizeNestedDlText(d.text) : d.text;
94
+ const normalized = normalizeIndentedBlock(ddText);
95
+ parseDdContentIntoTokens(state, normalized);
96
+ } else {
97
+ pushInline(state, d.text, d.line);
98
+ }
99
+ state.push(T_DD_CLOSE, "dd", -1);
100
+ }
101
+ }
102
+ state.push(T_DL_CLOSE, "dl", -1);
103
+ }
104
+ function collectDds(state, dtBlock, endLine, ddIndent) {
105
+ const dds = [];
106
+ let next = dtBlock.nextLine;
107
+ while (next < endLine) {
108
+ if (isBlankLine(state, next)) break;
109
+ const ddBlock = readDdBlock(state, next, endLine, dtBlock.baseIndent, ddIndent);
110
+ if (!ddBlock) break;
111
+ dds.push({ line: next, text: ddBlock.text });
112
+ next = ddBlock.nextLine;
113
+ }
114
+ return { dds, nextLineAfterDds: next };
115
+ }
116
+ function hasDdHeaderAtSameLevel(state, dtBlock, ddIndent, endLine) {
117
+ const minIndent = dtBlock.baseIndent + ddIndent;
118
+ if (dtBlock.nextLine >= endLine) return false;
119
+ return !!parseDdHeaderAtLevel(state, dtBlock.nextLine, minIndent) || isEmptyDdHeaderAtLevel(state, dtBlock.nextLine, minIndent);
120
+ }
121
+ function isDtOnlyBoundary(state, line, endLine) {
122
+ return line >= endLine || isBlankLine(state, line);
123
+ }
124
+ function shouldBlockParseDd(text) {
125
+ return looksLikeNestedDl(text) || text.indexOf("\n") >= 0;
126
+ }
127
+ function looksLikeNestedDl(text) {
128
+ return text.replace(/^\s+/, "").indexOf(":") === 0;
129
+ }
130
+ function pushInline(state, text, line) {
131
+ const token = state.push("inline", "", 0);
132
+ token.map = [line, line + 1];
133
+ token.content = text;
134
+ token.children = [];
135
+ }
136
+ function isBlankLine(state, line) {
137
+ const start = state.bMarks[line] + state.tShift[line];
138
+ const end = state.eMarks[line];
139
+ return start >= end;
140
+ }
141
+ function parseDtLine(state, line) {
142
+ if (line >= state.lineMax) return null;
143
+ const raw = getLineText(state, line);
144
+ const m = raw.match(/^( {0,3}):[ \t]+(.+?)\s*$/);
145
+ return m ? { text: m[2] } : null;
146
+ }
147
+ function parseDdHeaderAtLevel(state, line, minIndent) {
148
+ if (line >= state.lineMax) return null;
149
+ const raw = getLineText(state, line);
150
+ const indent = countLeadingSpaces(raw);
151
+ if (indent < minIndent || indent > minIndent + 3) return null;
152
+ const re = new RegExp(`^( {${indent}})(::?)[ \\t]+(.+?)\\s*$`);
153
+ const m = raw.match(re);
154
+ if (!m) return null;
155
+ const marker = m[2];
156
+ return { text: m[3], isNestedDlStart: marker === "::" };
157
+ }
158
+ function isEmptyDdHeaderAtLevel(state, line, minIndent) {
159
+ if (line >= state.lineMax) return false;
160
+ const raw = getLineText(state, line);
161
+ const indent = countLeadingSpaces(raw);
162
+ if (indent < minIndent || indent > minIndent + 3) return false;
163
+ const re = new RegExp(`^( {${indent}})(::?)\\s*$`);
164
+ return re.test(raw);
165
+ }
166
+ function readDtBlock(state, startLine, endLine, ddMinIndent) {
167
+ const dt = parseDtLine(state, startLine);
168
+ if (!dt) return null;
169
+ const raw0 = getLineText(state, startLine);
170
+ const baseIndent = countLeadingSpaces(raw0);
171
+ const lines = [dt.text];
172
+ let line = startLine + 1;
173
+ while (line < endLine) {
174
+ if (isWhitespaceOnlyLine(state, line)) break;
175
+ if (parseDtLine(state, line)) break;
176
+ const minIndent = baseIndent + ddMinIndent;
177
+ if (parseDdHeaderAtLevel(state, line, minIndent) || isEmptyDdHeaderAtLevel(state, line, minIndent)) break;
178
+ const raw = getLineText(state, line);
179
+ const first = raw.charCodeAt(0);
180
+ const isSpaceOrTab = first === 32 || first === 9;
181
+ if (!isSpaceOrTab) break;
182
+ lines.push(stripUpTo(raw, baseIndent + 2).replace(/\s+$/, ""));
183
+ line++;
184
+ }
185
+ return { baseIndent, text: lines.join("\n"), nextLine: line };
186
+ }
187
+ function readDdBlock(state, startLine, endLine, baseIndent, ddIndent) {
188
+ const minIndent = baseIndent + ddIndent;
189
+ const dd0 = parseDdHeaderAtLevel(state, startLine, minIndent);
190
+ const emptyHeader = !dd0 && isEmptyDdHeaderAtLevel(state, startLine, minIndent);
191
+ if (!dd0 && !emptyHeader) return null;
192
+ const lines = [];
193
+ if (dd0) {
194
+ lines.push(dd0.isNestedDlStart ? `: ${dd0.text}` : dd0.text);
195
+ }
196
+ let line = startLine + 1;
197
+ while (line < endLine) {
198
+ if (isWhitespaceOnlyLine(state, line)) {
199
+ const next = line + 1;
200
+ if (next >= endLine) break;
201
+ if (isWhitespaceOnlyLine(state, next)) break;
202
+ if (parseDtLine(state, next)) break;
203
+ if (parseDdHeaderAtLevel(state, next, minIndent) || isEmptyDdHeaderAtLevel(state, next, minIndent)) break;
204
+ const rawNext = getLineText(state, next);
205
+ const indentNext = countLeadingSpaces(rawNext);
206
+ if (!emptyHeader && indentNext < minIndent) break;
207
+ lines.push("");
208
+ line++;
209
+ continue;
210
+ }
211
+ if (parseDtLine(state, line)) break;
212
+ if (parseDdHeaderAtLevel(state, line, minIndent) || isEmptyDdHeaderAtLevel(state, line, minIndent)) break;
213
+ const raw = getLineText(state, line);
214
+ const indent = countLeadingSpaces(raw);
215
+ if (!emptyHeader && indent < minIndent) break;
216
+ const cut = emptyHeader ? Math.min(indent, minIndent) : minIndent;
217
+ lines.push(stripUpTo(raw, cut).replace(/\s+$/, ""));
218
+ line++;
219
+ }
220
+ return { text: lines.length === 0 ? "" : lines.join("\n"), nextLine: line };
221
+ }
222
+ function getLineText(state, line) {
223
+ const start = state.bMarks[line];
224
+ const end = state.eMarks[line];
225
+ return state.src.slice(start, end);
226
+ }
227
+ function isWhitespaceOnlyLine(state, line) {
228
+ return getLineText(state, line).trim().length === 0;
229
+ }
230
+ function countLeadingSpaces(s) {
231
+ let i = 0;
232
+ while (i < s.length && s.charCodeAt(i) === 32) i++;
233
+ return i;
234
+ }
235
+ function stripUpTo(s, n) {
236
+ let i = 0;
237
+ while (i < s.length && i < n && s.charCodeAt(i) === 32) i++;
238
+ return s.slice(i);
239
+ }
240
+ function clampInt(n, min, max) {
241
+ if (typeof n !== "number" || !isFinite(n)) return min;
242
+ return Math.max(min, Math.min(max, Math.floor(n)));
243
+ }
244
+ function normalizeNestedDlText(text) {
245
+ const lines = text.split("\n");
246
+ if (lines.length <= 1) return text;
247
+ for (let i = 1; i < lines.length; i++) {
248
+ const m = lines[i].match(/^(\s*):(.*)$/);
249
+ if (m) lines[i] = " :" + m[2];
250
+ }
251
+ return lines.join("\n");
252
+ }
253
+ function normalizeIndentedBlock(text) {
254
+ const lines = text.split("\n");
255
+ let min = Infinity;
256
+ for (const l of lines) {
257
+ if (l.trim().length === 0) continue;
258
+ const n = countLeadingSpaces(l);
259
+ if (n < min) min = n;
260
+ }
261
+ if (!isFinite(min) || min === 0) return text;
262
+ return lines.map((l) => l.trim().length === 0 ? "" : stripUpTo(l, min)).join("\n");
263
+ }
264
+ function parseDdContentIntoTokens(state, text) {
265
+ var _a, _b, _c;
266
+ const start = state.tokens.length;
267
+ state.md.block.parse(text, state.md, state.env, state.tokens);
268
+ const added = state.tokens.slice(start);
269
+ if (added.length === 3 && ((_a = added[0]) == null ? void 0 : _a.type) === "paragraph_open" && ((_b = added[1]) == null ? void 0 : _b.type) === "inline" && ((_c = added[2]) == null ? void 0 : _c.type) === "paragraph_close") {
270
+ state.tokens.length = start;
271
+ state.tokens.push(added[1]);
272
+ }
273
+ }
@@ -0,0 +1,16 @@
1
+ import MarkdownIt from 'markdown-it';
2
+
3
+ /**
4
+ * Options for the colon-based definition list plugin.
5
+ */
6
+ type DlListOptions = {
7
+ /** Indent (spaces) required for dd lines. Default: 4 */
8
+ ddIndent?: number;
9
+ /** If true, dt-only items are allowed only when followed by blank line or EOF. Default: true */
10
+ requireDd?: boolean;
11
+ /** If true, stop parsing the current dl at the first blank line after items. Default: true */
12
+ breakOnBlankLine?: boolean;
13
+ };
14
+ declare function dlListPlugin(md: MarkdownIt, opts?: DlListOptions): void;
15
+
16
+ export { type DlListOptions, dlListPlugin as default };
@@ -0,0 +1,16 @@
1
+ import MarkdownIt from 'markdown-it';
2
+
3
+ /**
4
+ * Options for the colon-based definition list plugin.
5
+ */
6
+ type DlListOptions = {
7
+ /** Indent (spaces) required for dd lines. Default: 4 */
8
+ ddIndent?: number;
9
+ /** If true, dt-only items are allowed only when followed by blank line or EOF. Default: true */
10
+ requireDd?: boolean;
11
+ /** If true, stop parsing the current dl at the first blank line after items. Default: true */
12
+ breakOnBlankLine?: boolean;
13
+ };
14
+ declare function dlListPlugin(md: MarkdownIt, opts?: DlListOptions): void;
15
+
16
+ export { type DlListOptions, dlListPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,253 @@
1
+ // src/index.ts
2
+ var RULE_NAME = "dl_list_colon";
3
+ var T_DL_OPEN = "dl_list_open";
4
+ var T_DL_CLOSE = "dl_list_close";
5
+ var T_DT_OPEN = "dl_dt_open";
6
+ var T_DT_CLOSE = "dl_dt_close";
7
+ var T_DD_OPEN = "dl_dd_open";
8
+ var T_DD_CLOSE = "dl_dd_close";
9
+ var DEFAULT_DD_INDENT = 4;
10
+ var DEFAULT_REQUIRE_DD = true;
11
+ var DEFAULT_BREAK_ON_BLANK = true;
12
+ function dlListPlugin(md, opts = {}) {
13
+ const ddIndent = clampInt(opts.ddIndent ?? DEFAULT_DD_INDENT, 1, 12);
14
+ const requireDd = opts.requireDd ?? DEFAULT_REQUIRE_DD;
15
+ const breakOnBlankLine = opts.breakOnBlankLine ?? DEFAULT_BREAK_ON_BLANK;
16
+ md.block.ruler.before("paragraph", RULE_NAME, (state, startLine, endLine, silent) => {
17
+ const begin = startLine;
18
+ const firstDt = readDtBlock(state, begin, endLine, ddIndent);
19
+ if (!firstDt) return false;
20
+ const firstHasDd = hasDdHeaderAtSameLevel(state, firstDt, ddIndent, endLine);
21
+ const firstDtOnlyOk = !firstHasDd && isDtOnlyBoundary(state, firstDt.nextLine, endLine);
22
+ if (requireDd && !firstHasDd && !firstDtOnlyOk) return false;
23
+ if (silent) return true;
24
+ const parsed = parseDlItems(state, begin, endLine, { ddIndent, requireDd, breakOnBlankLine });
25
+ if (!parsed) return false;
26
+ renderDlTokens(state, begin, parsed.endLine, parsed.items);
27
+ state.line = parsed.endLine;
28
+ return true;
29
+ });
30
+ md.renderer.rules[T_DL_OPEN] = () => "<dl>\n";
31
+ md.renderer.rules[T_DL_CLOSE] = () => "</dl>\n";
32
+ md.renderer.rules[T_DT_OPEN] = () => "<dt>";
33
+ md.renderer.rules[T_DT_CLOSE] = () => "</dt>\n";
34
+ md.renderer.rules[T_DD_OPEN] = () => "<dd>";
35
+ md.renderer.rules[T_DD_CLOSE] = () => "</dd>\n";
36
+ }
37
+ function parseDlItems(state, begin, endLine, ctx) {
38
+ const items = [];
39
+ let line = begin;
40
+ while (line < endLine) {
41
+ const dtBlock = readDtBlock(state, line, endLine, ctx.ddIndent);
42
+ if (!dtBlock) break;
43
+ const { dds, nextLineAfterDds } = collectDds(state, dtBlock, endLine, ctx.ddIndent);
44
+ if (dds.length === 0) {
45
+ const afterDt = dtBlock.nextLine;
46
+ const dtOnlyHere = isDtOnlyBoundary(state, afterDt, endLine);
47
+ if (ctx.requireDd && !dtOnlyHere) break;
48
+ items.push({ dtLine: line, dtText: dtBlock.text, dds: [] });
49
+ line = afterDt;
50
+ break;
51
+ }
52
+ items.push({ dtLine: line, dtText: dtBlock.text, dds });
53
+ line = nextLineAfterDds;
54
+ if (ctx.breakOnBlankLine && line < endLine && isBlankLine(state, line)) break;
55
+ }
56
+ if (items.length === 0) return null;
57
+ return { items, endLine: line };
58
+ }
59
+ function renderDlTokens(state, begin, endLine, items) {
60
+ const dlOpen = state.push(T_DL_OPEN, "dl", 1);
61
+ dlOpen.map = [begin, endLine];
62
+ for (const it of items) {
63
+ const dtOpen = state.push(T_DT_OPEN, "dt", 1);
64
+ dtOpen.map = [it.dtLine, it.dtLine + 1];
65
+ pushInline(state, it.dtText, it.dtLine);
66
+ state.push(T_DT_CLOSE, "dt", -1);
67
+ for (const d of it.dds) {
68
+ state.push(T_DD_OPEN, "dd", 1);
69
+ if (shouldBlockParseDd(d.text)) {
70
+ const ddText = looksLikeNestedDl(d.text) ? normalizeNestedDlText(d.text) : d.text;
71
+ const normalized = normalizeIndentedBlock(ddText);
72
+ parseDdContentIntoTokens(state, normalized);
73
+ } else {
74
+ pushInline(state, d.text, d.line);
75
+ }
76
+ state.push(T_DD_CLOSE, "dd", -1);
77
+ }
78
+ }
79
+ state.push(T_DL_CLOSE, "dl", -1);
80
+ }
81
+ function collectDds(state, dtBlock, endLine, ddIndent) {
82
+ const dds = [];
83
+ let next = dtBlock.nextLine;
84
+ while (next < endLine) {
85
+ if (isBlankLine(state, next)) break;
86
+ const ddBlock = readDdBlock(state, next, endLine, dtBlock.baseIndent, ddIndent);
87
+ if (!ddBlock) break;
88
+ dds.push({ line: next, text: ddBlock.text });
89
+ next = ddBlock.nextLine;
90
+ }
91
+ return { dds, nextLineAfterDds: next };
92
+ }
93
+ function hasDdHeaderAtSameLevel(state, dtBlock, ddIndent, endLine) {
94
+ const minIndent = dtBlock.baseIndent + ddIndent;
95
+ if (dtBlock.nextLine >= endLine) return false;
96
+ return !!parseDdHeaderAtLevel(state, dtBlock.nextLine, minIndent) || isEmptyDdHeaderAtLevel(state, dtBlock.nextLine, minIndent);
97
+ }
98
+ function isDtOnlyBoundary(state, line, endLine) {
99
+ return line >= endLine || isBlankLine(state, line);
100
+ }
101
+ function shouldBlockParseDd(text) {
102
+ return looksLikeNestedDl(text) || text.indexOf("\n") >= 0;
103
+ }
104
+ function looksLikeNestedDl(text) {
105
+ return text.replace(/^\s+/, "").indexOf(":") === 0;
106
+ }
107
+ function pushInline(state, text, line) {
108
+ const token = state.push("inline", "", 0);
109
+ token.map = [line, line + 1];
110
+ token.content = text;
111
+ token.children = [];
112
+ }
113
+ function isBlankLine(state, line) {
114
+ const start = state.bMarks[line] + state.tShift[line];
115
+ const end = state.eMarks[line];
116
+ return start >= end;
117
+ }
118
+ function parseDtLine(state, line) {
119
+ if (line >= state.lineMax) return null;
120
+ const raw = getLineText(state, line);
121
+ const m = raw.match(/^( {0,3}):[ \t]+(.+?)\s*$/);
122
+ return m ? { text: m[2] } : null;
123
+ }
124
+ function parseDdHeaderAtLevel(state, line, minIndent) {
125
+ if (line >= state.lineMax) return null;
126
+ const raw = getLineText(state, line);
127
+ const indent = countLeadingSpaces(raw);
128
+ if (indent < minIndent || indent > minIndent + 3) return null;
129
+ const re = new RegExp(`^( {${indent}})(::?)[ \\t]+(.+?)\\s*$`);
130
+ const m = raw.match(re);
131
+ if (!m) return null;
132
+ const marker = m[2];
133
+ return { text: m[3], isNestedDlStart: marker === "::" };
134
+ }
135
+ function isEmptyDdHeaderAtLevel(state, line, minIndent) {
136
+ if (line >= state.lineMax) return false;
137
+ const raw = getLineText(state, line);
138
+ const indent = countLeadingSpaces(raw);
139
+ if (indent < minIndent || indent > minIndent + 3) return false;
140
+ const re = new RegExp(`^( {${indent}})(::?)\\s*$`);
141
+ return re.test(raw);
142
+ }
143
+ function readDtBlock(state, startLine, endLine, ddMinIndent) {
144
+ const dt = parseDtLine(state, startLine);
145
+ if (!dt) return null;
146
+ const raw0 = getLineText(state, startLine);
147
+ const baseIndent = countLeadingSpaces(raw0);
148
+ const lines = [dt.text];
149
+ let line = startLine + 1;
150
+ while (line < endLine) {
151
+ if (isWhitespaceOnlyLine(state, line)) break;
152
+ if (parseDtLine(state, line)) break;
153
+ const minIndent = baseIndent + ddMinIndent;
154
+ if (parseDdHeaderAtLevel(state, line, minIndent) || isEmptyDdHeaderAtLevel(state, line, minIndent)) break;
155
+ const raw = getLineText(state, line);
156
+ const first = raw.charCodeAt(0);
157
+ const isSpaceOrTab = first === 32 || first === 9;
158
+ if (!isSpaceOrTab) break;
159
+ lines.push(stripUpTo(raw, baseIndent + 2).replace(/\s+$/, ""));
160
+ line++;
161
+ }
162
+ return { baseIndent, text: lines.join("\n"), nextLine: line };
163
+ }
164
+ function readDdBlock(state, startLine, endLine, baseIndent, ddIndent) {
165
+ const minIndent = baseIndent + ddIndent;
166
+ const dd0 = parseDdHeaderAtLevel(state, startLine, minIndent);
167
+ const emptyHeader = !dd0 && isEmptyDdHeaderAtLevel(state, startLine, minIndent);
168
+ if (!dd0 && !emptyHeader) return null;
169
+ const lines = [];
170
+ if (dd0) {
171
+ lines.push(dd0.isNestedDlStart ? `: ${dd0.text}` : dd0.text);
172
+ }
173
+ let line = startLine + 1;
174
+ while (line < endLine) {
175
+ if (isWhitespaceOnlyLine(state, line)) {
176
+ const next = line + 1;
177
+ if (next >= endLine) break;
178
+ if (isWhitespaceOnlyLine(state, next)) break;
179
+ if (parseDtLine(state, next)) break;
180
+ if (parseDdHeaderAtLevel(state, next, minIndent) || isEmptyDdHeaderAtLevel(state, next, minIndent)) break;
181
+ const rawNext = getLineText(state, next);
182
+ const indentNext = countLeadingSpaces(rawNext);
183
+ if (!emptyHeader && indentNext < minIndent) break;
184
+ lines.push("");
185
+ line++;
186
+ continue;
187
+ }
188
+ if (parseDtLine(state, line)) break;
189
+ if (parseDdHeaderAtLevel(state, line, minIndent) || isEmptyDdHeaderAtLevel(state, line, minIndent)) break;
190
+ const raw = getLineText(state, line);
191
+ const indent = countLeadingSpaces(raw);
192
+ if (!emptyHeader && indent < minIndent) break;
193
+ const cut = emptyHeader ? Math.min(indent, minIndent) : minIndent;
194
+ lines.push(stripUpTo(raw, cut).replace(/\s+$/, ""));
195
+ line++;
196
+ }
197
+ return { text: lines.length === 0 ? "" : lines.join("\n"), nextLine: line };
198
+ }
199
+ function getLineText(state, line) {
200
+ const start = state.bMarks[line];
201
+ const end = state.eMarks[line];
202
+ return state.src.slice(start, end);
203
+ }
204
+ function isWhitespaceOnlyLine(state, line) {
205
+ return getLineText(state, line).trim().length === 0;
206
+ }
207
+ function countLeadingSpaces(s) {
208
+ let i = 0;
209
+ while (i < s.length && s.charCodeAt(i) === 32) i++;
210
+ return i;
211
+ }
212
+ function stripUpTo(s, n) {
213
+ let i = 0;
214
+ while (i < s.length && i < n && s.charCodeAt(i) === 32) i++;
215
+ return s.slice(i);
216
+ }
217
+ function clampInt(n, min, max) {
218
+ if (typeof n !== "number" || !isFinite(n)) return min;
219
+ return Math.max(min, Math.min(max, Math.floor(n)));
220
+ }
221
+ function normalizeNestedDlText(text) {
222
+ const lines = text.split("\n");
223
+ if (lines.length <= 1) return text;
224
+ for (let i = 1; i < lines.length; i++) {
225
+ const m = lines[i].match(/^(\s*):(.*)$/);
226
+ if (m) lines[i] = " :" + m[2];
227
+ }
228
+ return lines.join("\n");
229
+ }
230
+ function normalizeIndentedBlock(text) {
231
+ const lines = text.split("\n");
232
+ let min = Infinity;
233
+ for (const l of lines) {
234
+ if (l.trim().length === 0) continue;
235
+ const n = countLeadingSpaces(l);
236
+ if (n < min) min = n;
237
+ }
238
+ if (!isFinite(min) || min === 0) return text;
239
+ return lines.map((l) => l.trim().length === 0 ? "" : stripUpTo(l, min)).join("\n");
240
+ }
241
+ function parseDdContentIntoTokens(state, text) {
242
+ var _a, _b, _c;
243
+ const start = state.tokens.length;
244
+ state.md.block.parse(text, state.md, state.env, state.tokens);
245
+ const added = state.tokens.slice(start);
246
+ if (added.length === 3 && ((_a = added[0]) == null ? void 0 : _a.type) === "paragraph_open" && ((_b = added[1]) == null ? void 0 : _b.type) === "inline" && ((_c = added[2]) == null ? void 0 : _c.type) === "paragraph_close") {
247
+ state.tokens.length = start;
248
+ state.tokens.push(added[1]);
249
+ }
250
+ }
251
+ export {
252
+ dlListPlugin as default
253
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "markdown-it-dl-list",
3
+ "version": "0.1.0",
4
+ "description": "markdown-it plugin for colon-based definition lists (<dl>, <dt>, <dd>)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/kanemu/dl-list-markdown"
10
+ },
11
+ "homepage": "https://github.com/kanemu/dl-list-markdown/tree/main/packages/markdown-it-dl-list",
12
+ "bugs": {
13
+ "url": "https://github.com/kanemu/dl-list-markdown/issues"
14
+ },
15
+ "keywords": [
16
+ "markdown-it",
17
+ "markdown",
18
+ "definition-list",
19
+ "dl",
20
+ "dt",
21
+ "dd",
22
+ "plugin"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "require": "./dist/index.cjs"
29
+ }
30
+ },
31
+ "main": "./dist/index.cjs",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "files": [
35
+ "dist/**",
36
+ "README.md",
37
+ "README-ja.md",
38
+ "LICENSE"
39
+ ],
40
+ "sideEffects": false,
41
+ "peerDependencies": {
42
+ "markdown-it": "^14.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "markdown-it": {
46
+ "optional": false
47
+ }
48
+ },
49
+ "devDependencies": {
50
+ "@types/markdown-it": "^14.1.2",
51
+ "markdown-it": "^14.1.0",
52
+ "rimraf": "^6.1.2",
53
+ "tsup": "^8.5.1",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.0.17"
56
+ },
57
+ "scripts": {
58
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
59
+ "test": "vitest run",
60
+ "dev": "vitest",
61
+ "clean": "rimraf dist"
62
+ }
63
+ }