@dogsbay/format-dogsbay-md 0.2.0-beta.126 → 0.2.0-beta.127
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/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/container-rule.d.ts +41 -3
- package/dist/container-rule.js +190 -37
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +8 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/parse.d.ts +9 -0
- package/dist/parse.js +27 -0
- package/dist/plugin-containers.d.ts +2 -3
- package/dist/plugin-containers.js +3 -1
- package/package.json +4 -4
package/dist/browser.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (js-yaml only) rather than gray-matter, whose entry `require('fs')`s at
|
|
12
12
|
* module load and would drag `node:fs` back into this graph.
|
|
13
13
|
*/
|
|
14
|
-
export { dogsbayMdToTree, parseInline, type ParseResult, type ParseOptions } from "./parse.js";
|
|
14
|
+
export { dogsbayMdToTree, parseInline, containerOpenBefore, type ParseResult, type ParseOptions } from "./parse.js";
|
|
15
15
|
export { KNOWN_INLINE_DIRECTIVES, type ParseDiagnostic } from "./diagnostics.js";
|
|
16
16
|
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
|
17
17
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
package/dist/browser.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* (js-yaml only) rather than gray-matter, whose entry `require('fs')`s at
|
|
12
12
|
* module load and would drag `node:fs` back into this graph.
|
|
13
13
|
*/
|
|
14
|
-
export { dogsbayMdToTree, parseInline } from "./parse.js";
|
|
14
|
+
export { dogsbayMdToTree, parseInline, containerOpenBefore } from "./parse.js";
|
|
15
15
|
export { KNOWN_INLINE_DIRECTIVES } from "./diagnostics.js";
|
|
16
16
|
// The `:::name` blocks the parser knows (an editor completes names from it)
|
|
17
17
|
export { KNOWN_BLOCK_DIRECTIVES } from "./plugin-containers.js";
|
package/dist/container-rule.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `:::name` container block rule: markdown-it-container 4.0.0 (MIT,
|
|
3
|
-
* Vitaly Puzrin, Alex Kocharin), adapted.
|
|
3
|
+
* Vitaly Puzrin, Alex Kocharin), adapted. Three changes, all in how the
|
|
4
4
|
* closing line is found:
|
|
5
5
|
*
|
|
6
6
|
* 1. A closer may carry an HTML comment, `::: <!-- end note -->`, the way an
|
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
* one code block, with no message. Which lines are code comes from one
|
|
14
14
|
* block-level pass over the source with containers off (codeLines).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* 3. Blocks nest by a stack, not by colon count (NESTING below).
|
|
17
|
+
*
|
|
18
|
+
* Everything else (indentation, auto-close at the end of the parent) is
|
|
19
|
+
* markdown-it-container's.
|
|
18
20
|
*
|
|
19
21
|
* markdown-it-container's licence:
|
|
20
22
|
*
|
|
@@ -42,8 +44,44 @@
|
|
|
42
44
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
43
45
|
*/
|
|
44
46
|
import type MarkdownIt from "markdown-it";
|
|
47
|
+
/**
|
|
48
|
+
* NESTING. A closer with N colons closes the innermost open block opened with exactly N colons,
|
|
49
|
+
* else the innermost opened with fewer, and any block still open inside it. So every block can close with plain `:::`
|
|
50
|
+
* (`:::steps` … `:::tabs` … `:::` … `:::`), and today's scaled nesting (`::::steps` … `:::tabs`
|
|
51
|
+
* … `:::` … `::::`) and the inverse (inner blocks with more colons) read the same. Pandoc's fenced
|
|
52
|
+
* divs nest the same way. Before the stack, a closer ended the OUTERMOST block it was long
|
|
53
|
+
* enough for, so a nested block needed more colons on the outer one, and one `:::` could close
|
|
54
|
+
* several blocks (that case now leaves the outer one open, and the diagnostic says why). "Exactly
|
|
55
|
+
* N first" keeps the old reading where the author scaled: `::::note` … `:::tip` … `::::` closes the
|
|
56
|
+
* note (and the tip with it) as before, rather than the tip.
|
|
57
|
+
*
|
|
58
|
+
* Only a known directive name opens a block in the scan. An unknown `:::word` is a block only if
|
|
59
|
+
* it has a closer of its own and is otherwise text (markUnclosedGenericContainers), and the scan
|
|
60
|
+
* cannot know which, so it does not push one: an unknown block nested at the same colon count
|
|
61
|
+
* as its parent still needs more colons on the parent. A body-less block (`:::youtube{#id}`) is
|
|
62
|
+
* pushed only when a closer that fits it follows, or it would take its parent's.
|
|
63
|
+
*
|
|
64
|
+
* VERIFY. The scan reads raw lines, and markdown-it may not read an opener there as one: in raw
|
|
65
|
+
* HTML, a `$$` math block, a fence that never closes. Where that is depends on where the blocks
|
|
66
|
+
* around it end, so no pass over the page can say in advance. So after the body is tokenized, every
|
|
67
|
+
* opener the scan pushed must have opened a block in it; any that did not are left out and the
|
|
68
|
+
* body is scanned and tokenized again. Counted wrongly, an opener leaves the outer block open to
|
|
69
|
+
* the end of the page (the rule before the stack lost one closer at most). What a block learns is
|
|
70
|
+
* kept for the parse, by its opening line: when an outer block retries, the blocks inside it are
|
|
71
|
+
* tokenized again and start from what they learned, rather than repeating their own retries (which
|
|
72
|
+
* doubled the work at every level). A learned line can be stale in the new context, so the check
|
|
73
|
+
* runs both ways: a skipped line that markdown-it did open as a block is pushed again. Retried
|
|
74
|
+
* lines come out of one budget per parse (a few times the page's lines); past it, the scan stands.
|
|
75
|
+
*/
|
|
45
76
|
export interface ContainerOptions {
|
|
46
77
|
validate: (params: string, markup: string) => boolean;
|
|
47
78
|
render?: (tokens: unknown[], idx: number) => string;
|
|
48
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Make `name` an opener for the nesting scan (the known directives; not the generic container).
|
|
82
|
+
* A `leaf` is written with no body (`:::youtube{#id}`) and often with no closer.
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerOpener(name: string, options?: {
|
|
85
|
+
leaf?: boolean;
|
|
86
|
+
}): void;
|
|
49
87
|
export declare function containerRule(md: MarkdownIt, name: string, options: ContainerOptions): void;
|
package/dist/container-rule.js
CHANGED
|
@@ -7,6 +7,9 @@ const CLOSER_TAIL = /^(?:[ \t]*|[ \t]*(<!--(?:(?!-->)[\s\S])*-->)[ \t]*)$/;
|
|
|
7
7
|
/** state.env keys: the pre-pass that finds the code, and its result. */
|
|
8
8
|
const NO_CONTAINERS = "__dogsbayNoContainers";
|
|
9
9
|
const CODE_LINES = "__dogsbayCodeLines";
|
|
10
|
+
/** state.env keys for VERIFY: the parse's retry budget, and each block's learned openers to skip. */
|
|
11
|
+
const RETRY_BUDGET = "__dogsbayContainerRetries";
|
|
12
|
+
const LEARNED = "__dogsbayContainerNotOpeners";
|
|
10
13
|
/**
|
|
11
14
|
* The source lines inside a fenced code block, from one block-level pass over the whole source
|
|
12
15
|
* with containers switched off, made the first time a container needs it and kept in the parse's
|
|
@@ -49,11 +52,63 @@ function closesFence(line, markup, after) {
|
|
|
49
52
|
const m = /^[ \t>]*(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
50
53
|
return m !== null && m[1][0] === markup[0] && m[1].length >= markup.length;
|
|
51
54
|
}
|
|
55
|
+
/** The directive names the scan treats as openers (plugin-containers registers them), and whether each is body-less. */
|
|
56
|
+
const KNOWN_OPENERS = new Map();
|
|
57
|
+
/** The known directive the text after an opener's colons starts with. */
|
|
58
|
+
function knownOpener(rest) {
|
|
59
|
+
const m = /^\s*([A-Za-z][\w-]*)(?=$|[\s{[])/.exec(rest);
|
|
60
|
+
return m === null ? undefined : KNOWN_OPENERS.get(m[1]);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Make `name` an opener for the nesting scan (the known directives; not the generic container).
|
|
64
|
+
* A `leaf` is written with no body (`:::youtube{#id}`) and often with no closer.
|
|
65
|
+
*/
|
|
66
|
+
export function registerOpener(name, options = {}) {
|
|
67
|
+
KNOWN_OPENERS.set(name, { leaf: options.leaf === true });
|
|
68
|
+
}
|
|
69
|
+
/** Whether the next non-blank line after `line`, before `endLine`, is a closer for a block opened with `colons`. */
|
|
70
|
+
function closerFollows(state, line, endLine, colons) {
|
|
71
|
+
for (let l = line + 1; l < endLine; l++) {
|
|
72
|
+
const start = state.bMarks[l] + state.tShift[l];
|
|
73
|
+
const max = state.eMarks[l];
|
|
74
|
+
if (start >= max)
|
|
75
|
+
continue;
|
|
76
|
+
const m = /^(:{3,})(.*)$/.exec(state.src.slice(start, max));
|
|
77
|
+
return m !== null && m[1].length >= colons && CLOSER_TAIL.test(m[2]);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/** Tokens whose lines are not block openers: an opener the scan pushed inside one is wrong (VERIFY). */
|
|
82
|
+
const READ_AS_OTHER = new Set(["paragraph_open", "heading_open", "html_block", "fence", "code_block", "math_block", "hr", "table_open"]);
|
|
83
|
+
/** The openers each block (by opening line) learned to skip in this parse (VERIFY). */
|
|
84
|
+
function learned(state) {
|
|
85
|
+
const env = state.env;
|
|
86
|
+
if (!env)
|
|
87
|
+
return new Map();
|
|
88
|
+
const kept = env[LEARNED];
|
|
89
|
+
if (kept && kept.src === state.src)
|
|
90
|
+
return kept.map;
|
|
91
|
+
const map = new Map();
|
|
92
|
+
env[LEARNED] = { src: state.src, map };
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
/** The lines left in this parse's retry budget (VERIFY), shared by every block in it. */
|
|
96
|
+
function retryBudget(state) {
|
|
97
|
+
const env = state.env;
|
|
98
|
+
const fresh = { lines: 4 * state.bMarks.length + 1000 };
|
|
99
|
+
if (!env)
|
|
100
|
+
return fresh;
|
|
101
|
+
const kept = env[RETRY_BUDGET];
|
|
102
|
+
if (kept && kept.src === state.src)
|
|
103
|
+
return kept.budget;
|
|
104
|
+
env[RETRY_BUDGET] = { src: state.src, budget: fresh };
|
|
105
|
+
return fresh;
|
|
106
|
+
}
|
|
52
107
|
export function containerRule(md, name, options) {
|
|
53
108
|
const MARKER = ":".charCodeAt(0);
|
|
54
109
|
function container(state, startLine, endLine, silent) {
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
111
|
+
const max = state.eMarks[startLine];
|
|
57
112
|
if (state.src.charCodeAt(start) !== MARKER)
|
|
58
113
|
return false;
|
|
59
114
|
if (state.env?.[NO_CONTAINERS])
|
|
@@ -73,64 +128,162 @@ export function containerRule(md, name, options) {
|
|
|
73
128
|
const oldParent = state.parentType;
|
|
74
129
|
const oldLineMax = state.lineMax;
|
|
75
130
|
state.parentType = "container";
|
|
131
|
+
const tokensBefore = state.tokens.length;
|
|
132
|
+
// Openers markdown-it did not open, found by an earlier try (VERIFY above)
|
|
133
|
+
const known = learned(state);
|
|
134
|
+
const notOpeners = new Set(known.get(startLine));
|
|
135
|
+
let scan;
|
|
136
|
+
for (;;) {
|
|
137
|
+
scan = scanBody(state, startLine, endLine, markerCount, notOpeners);
|
|
138
|
+
// lazy continuations never run past the closer
|
|
139
|
+
state.lineMax = scan.nextLine;
|
|
140
|
+
const open = state.push(`container_${name}_open`, "div", 1);
|
|
141
|
+
open.markup = markup;
|
|
142
|
+
open.block = true;
|
|
143
|
+
open.info = params;
|
|
144
|
+
open.map = [startLine, scan.nextLine];
|
|
145
|
+
if (!scan.autoClosed && (scan.skippedCloser !== undefined || scan.legacyCloser !== undefined)) {
|
|
146
|
+
open.meta = {
|
|
147
|
+
...(open.meta ?? {}),
|
|
148
|
+
...(scan.skippedCloser !== undefined ? { skippedCloser: scan.skippedCloser } : {}),
|
|
149
|
+
...(scan.legacyCloser !== undefined ? { legacyCloser: scan.legacyCloser } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
state.md.block.tokenize(state, startLine + 1, scan.nextLine);
|
|
153
|
+
const close = state.push(`container_${name}_close`, "div", -1);
|
|
154
|
+
close.markup = scan.closeMarkup;
|
|
155
|
+
close.block = true;
|
|
156
|
+
close.info = scan.closeComment;
|
|
157
|
+
// Wrong: a pushed opener's line that markdown-it read as something else. A line it read as
|
|
158
|
+
// nothing (past markdown-it's nesting limit, where it drops the rest) is not evidence.
|
|
159
|
+
// Missed: a skipped line markdown-it did open as a block (a learned line, stale here).
|
|
160
|
+
const pushed = new Set(scan.pushed);
|
|
161
|
+
const wrong = [];
|
|
162
|
+
const missed = [];
|
|
163
|
+
if (pushed.size > 0 || notOpeners.size > 0) {
|
|
164
|
+
for (let i = tokensBefore + 1; i < state.tokens.length; i++) {
|
|
165
|
+
const t = state.tokens[i];
|
|
166
|
+
if (!t.map)
|
|
167
|
+
continue;
|
|
168
|
+
if (READ_AS_OTHER.has(t.type)) {
|
|
169
|
+
for (let l = t.map[0]; l < t.map[1]; l++)
|
|
170
|
+
if (pushed.delete(l))
|
|
171
|
+
wrong.push(l);
|
|
172
|
+
}
|
|
173
|
+
else if (t.nesting === 1 && t.type.startsWith("container_") && notOpeners.has(t.map[0]))
|
|
174
|
+
missed.push(t.map[0]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (wrong.length === 0 && missed.length === 0)
|
|
178
|
+
break;
|
|
179
|
+
const budget = retryBudget(state);
|
|
180
|
+
if (budget.lines < scan.nextLine - startLine)
|
|
181
|
+
break;
|
|
182
|
+
budget.lines -= scan.nextLine - startLine;
|
|
183
|
+
for (const l of wrong)
|
|
184
|
+
notOpeners.add(l);
|
|
185
|
+
for (const l of missed)
|
|
186
|
+
notOpeners.delete(l);
|
|
187
|
+
state.tokens.length = tokensBefore;
|
|
188
|
+
}
|
|
189
|
+
if (notOpeners.size > 0)
|
|
190
|
+
known.set(startLine, new Set(notOpeners));
|
|
191
|
+
else
|
|
192
|
+
known.delete(startLine);
|
|
193
|
+
state.parentType = oldParent;
|
|
194
|
+
state.lineMax = oldLineMax;
|
|
195
|
+
state.line = scan.nextLine + (scan.autoClosed ? 1 : 0);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
/** Find where the block opened at `startLine` ends (NESTING above), skipping the `notOpeners` lines as openers. */
|
|
199
|
+
function scanBody(state, startLine, endLine, markerCount, notOpeners) {
|
|
76
200
|
let code;
|
|
77
201
|
let nextLine = startLine;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
202
|
+
const scan = { nextLine, autoClosed: false, closeMarkup: "", closeComment: "", pushed: [] };
|
|
203
|
+
// Blocks opened inside this one and not yet closed, innermost last (see NESTING above)
|
|
204
|
+
// `least` is the fewest colons from the outermost to this one, and `openCount` counts the open
|
|
205
|
+
// blocks by colons, so a closer that fits none is turned away without walking the stack
|
|
206
|
+
const inner = [];
|
|
207
|
+
const openCount = new Map();
|
|
208
|
+
const truncate = (k) => {
|
|
209
|
+
for (let j = k; j < inner.length; j++)
|
|
210
|
+
openCount.set(inner[j].colons, openCount.get(inner[j].colons) - 1);
|
|
211
|
+
inner.length = k;
|
|
212
|
+
};
|
|
82
213
|
for (;;) {
|
|
83
214
|
nextLine++;
|
|
84
215
|
// unclosed: closed by the end of the document or of the parent block
|
|
85
216
|
if (nextLine >= endLine)
|
|
86
217
|
break;
|
|
87
|
-
start = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
88
|
-
max = state.eMarks[nextLine];
|
|
218
|
+
const start = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
219
|
+
const max = state.eMarks[nextLine];
|
|
220
|
+
if (start >= max)
|
|
221
|
+
continue;
|
|
89
222
|
// a non-empty line indented less than the block ends it (a list item's container)
|
|
90
|
-
if (
|
|
223
|
+
if (state.sCount[nextLine] < state.blkIndent)
|
|
91
224
|
break;
|
|
225
|
+
// a block opened deeper (in a list item) ended at a line indented less than its opener
|
|
226
|
+
while (inner.length > 0 && state.sCount[nextLine] < inner[inner.length - 1].indent)
|
|
227
|
+
truncate(inner.length - 1);
|
|
92
228
|
if (state.src.charCodeAt(start) !== MARKER)
|
|
93
229
|
continue;
|
|
94
|
-
//
|
|
230
|
+
// openers and closers are indented less than 4 spaces past the block
|
|
95
231
|
if (state.sCount[nextLine] - state.blkIndent >= 4)
|
|
96
232
|
continue;
|
|
97
|
-
pos = start + 1;
|
|
233
|
+
let pos = start + 1;
|
|
98
234
|
while (pos < max && state.src.charCodeAt(pos) === MARKER)
|
|
99
235
|
pos++;
|
|
100
|
-
|
|
101
|
-
if (
|
|
236
|
+
const colons = pos - start;
|
|
237
|
+
if (colons < 3)
|
|
102
238
|
continue;
|
|
103
|
-
const
|
|
104
|
-
|
|
239
|
+
const rest = state.src.slice(pos, max);
|
|
240
|
+
const tail = CLOSER_TAIL.exec(rest);
|
|
241
|
+
const opener = tail ? undefined : knownOpener(rest);
|
|
242
|
+
if (!tail && !opener)
|
|
105
243
|
continue;
|
|
106
244
|
// a `:::` in a code example is code
|
|
107
245
|
if ((code ??= codeLines(state)).has(nextLine)) {
|
|
108
|
-
|
|
246
|
+
if (tail && colons >= markerCount)
|
|
247
|
+
scan.skippedCloser ??= nextLine;
|
|
109
248
|
continue;
|
|
110
249
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
250
|
+
if (opener) {
|
|
251
|
+
if (notOpeners.has(nextLine) || (opener.leaf && !closerFollows(state, nextLine, endLine, colons)))
|
|
252
|
+
continue;
|
|
253
|
+
const least = Math.min(colons, inner.length > 0 ? inner[inner.length - 1].least : colons);
|
|
254
|
+
inner.push({ colons, indent: state.sCount[nextLine], least });
|
|
255
|
+
openCount.set(colons, (openCount.get(colons) ?? 0) + 1);
|
|
256
|
+
scan.pushed.push(nextLine);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
// Where the rule before the stack (the first closer with enough colons) would have ended this block
|
|
260
|
+
if (colons >= markerCount)
|
|
261
|
+
scan.legacyCloser ??= nextLine;
|
|
262
|
+
// The innermost open block opened with exactly this many colons, else the innermost opened
|
|
263
|
+
// with fewer; it closes, and any block still open inside it. Index -1 is this block.
|
|
264
|
+
const closes = (fits) => {
|
|
265
|
+
for (let k = inner.length - 1; k >= 0; k--)
|
|
266
|
+
if (fits(inner[k].colons))
|
|
267
|
+
return k;
|
|
268
|
+
return fits(markerCount) ? -1 : undefined;
|
|
269
|
+
};
|
|
270
|
+
const exact = (openCount.get(colons) ?? 0) > 0 || markerCount === colons;
|
|
271
|
+
const fewer = markerCount < colons || (inner.length > 0 && inner[inner.length - 1].least < colons);
|
|
272
|
+
const k = exact ? closes((n) => n === colons) : fewer ? closes((n) => n < colons) : undefined;
|
|
273
|
+
// shorter than every open block: it closes nothing here
|
|
274
|
+
if (k === undefined)
|
|
275
|
+
continue;
|
|
276
|
+
if (k >= 0) {
|
|
277
|
+
truncate(k);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
scan.autoClosed = true;
|
|
281
|
+
scan.closeMarkup = state.src.slice(start, pos);
|
|
282
|
+
scan.closeComment = tail?.[1] ?? "";
|
|
114
283
|
break;
|
|
115
284
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const open = state.push(`container_${name}_open`, "div", 1);
|
|
119
|
-
open.markup = markup;
|
|
120
|
-
open.block = true;
|
|
121
|
-
open.info = params;
|
|
122
|
-
open.map = [startLine, nextLine];
|
|
123
|
-
if (skippedCloser !== undefined && !autoClosed)
|
|
124
|
-
open.meta = { ...(open.meta ?? {}), skippedCloser };
|
|
125
|
-
state.md.block.tokenize(state, startLine + 1, nextLine);
|
|
126
|
-
const close = state.push(`container_${name}_close`, "div", -1);
|
|
127
|
-
close.markup = closeMarkup;
|
|
128
|
-
close.block = true;
|
|
129
|
-
close.info = closeComment;
|
|
130
|
-
state.parentType = oldParent;
|
|
131
|
-
state.lineMax = oldLineMax;
|
|
132
|
-
state.line = nextLine + (autoClosed ? 1 : 0);
|
|
133
|
-
return true;
|
|
285
|
+
scan.nextLine = nextLine;
|
|
286
|
+
return scan;
|
|
134
287
|
}
|
|
135
288
|
md.block.ruler.before("fence", `container_${name}`, container, { alt: ["paragraph", "reference", "blockquote", "list"] });
|
|
136
289
|
const render = options.render ?? (() => "");
|
package/dist/diagnostics.d.ts
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -73,7 +73,12 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
73
73
|
const colons = (tok.markup ?? ":::").length;
|
|
74
74
|
const closed = closesContainer(lines[tok.map[1]], colons);
|
|
75
75
|
const skipped = tok.meta?.skippedCloser;
|
|
76
|
-
|
|
76
|
+
const legacy = tok.meta?.legacyCloser;
|
|
77
|
+
if (!closed && legacy !== undefined && !generic) {
|
|
78
|
+
// Before the nesting stack, the closer on that line ended this block and the ones inside it
|
|
79
|
+
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${lines[legacy]?.trim()} on line ${lineOf(legacy) ?? "?"} closes only the innermost open block; each block needs its own closer (one \`:::\` used to close several)`);
|
|
80
|
+
}
|
|
81
|
+
else if (!closed && skipped !== undefined) {
|
|
77
82
|
add(tok.map[0], "unclosed-container", `:::${name} is never closed — the ${lines[skipped]?.trim()} on line ${lineOf(skipped) ?? "?"} is inside a code block, so it does not close it (add a closer after the code)`);
|
|
78
83
|
}
|
|
79
84
|
else if (!closed) {
|
|
@@ -99,12 +104,11 @@ export function collectDiagnostics(tokens, lines, lineOf) {
|
|
|
99
104
|
add(tok.map[0], "bad-attributes", `the attributes of :::${name} ${problem} — they are ignored`);
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
|
-
// A `:::` line
|
|
103
|
-
// meant to close ended earlier (an inner block of the same fence closed it), or there is none
|
|
107
|
+
// A `:::` line no open block took: every block it could close is already closed
|
|
104
108
|
if (tok.type === "inline" && tok.map) {
|
|
105
109
|
(tok.content ?? "").split("\n").forEach((l, k) => {
|
|
106
110
|
if (/^\s*:{3,}[ \t]*(?:<!--(?:(?!-->)[\s\S])*-->[ \t]*)?$/.test(l)) {
|
|
107
|
-
add(tok.map[0] + k, "stray-closer", `stray \`${l.trim()}\` — it closes nothing and is shown as text
|
|
111
|
+
add(tok.map[0] + k, "stray-closer", `stray \`${l.trim()}\` — it closes nothing and is shown as text: every block before it is already closed (each block takes one closer)`);
|
|
108
112
|
}
|
|
109
113
|
});
|
|
110
114
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
|
2
2
|
export type { SerializeOptions } from "./types.js";
|
|
3
3
|
export { DOGSBAY_MD_CAPABILITIES } from "./capabilities.js";
|
|
4
4
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
5
|
-
export { dogsbayMdToTree, parseInline } from "./parse.js";
|
|
5
|
+
export { dogsbayMdToTree, parseInline, containerOpenBefore } from "./parse.js";
|
|
6
6
|
export { extractHeadings, slugifyHeadingText, type HeadingInfo } from "./headings.js";
|
|
7
7
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker } from "./code-regions.js";
|
|
8
8
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export { treeToDogsbayMd, nodeToDogsbayMd } from "./serialize.js";
|
|
6
6
|
export { DOGSBAY_MD_CAPABILITIES } from "./capabilities.js";
|
|
7
7
|
export { inlineToDogsbayMd } from "./inline.js";
|
|
8
|
-
export { dogsbayMdToTree, parseInline } from "./parse.js";
|
|
8
|
+
export { dogsbayMdToTree, parseInline, containerOpenBefore } from "./parse.js";
|
|
9
9
|
export { extractHeadings, slugifyHeadingText } from "./headings.js";
|
|
10
10
|
export { transformCodeRegions, transformProseLines, transformProseSegments, fenceOpenBefore, createFenceTracker } from "./code-regions.js";
|
|
11
11
|
export { maskMinjaInCode, unmaskMinjaInCode, maskAllMinjaOpeners, isMinjaProcessedFence, stripInlineMinjaMarkers, stripFenceMinjaMarkers } from "./minja-mask.js";
|
package/dist/parse.d.ts
CHANGED
|
@@ -42,3 +42,12 @@ export declare function dogsbayMdToTree(source: string, options?: ParseOptions):
|
|
|
42
42
|
* Parse a single inline string to InlineNode[] (useful for testing).
|
|
43
43
|
*/
|
|
44
44
|
export declare function parseInline(source: string): InlineNode[];
|
|
45
|
+
/**
|
|
46
|
+
* Per line of `source` (the body, no front matter, split at `\n` as the commands engine splits
|
|
47
|
+
* it): whether the line is inside a `:::` block as the parser nests them. The opener's line is
|
|
48
|
+
* not, nor the closer's; the body between is. Read from the
|
|
49
|
+
* parse rather than re-derived, so the commands engine's outline (a heading in a block is never
|
|
50
|
+
* structure) cannot drift from the parser: a line scan cannot see where a list item ends a block,
|
|
51
|
+
* which names are blocks, or raw HTML. An unknown name never closed is text, so not a block.
|
|
52
|
+
*/
|
|
53
|
+
export declare function containerOpenBefore(source: string): boolean[];
|
package/dist/parse.js
CHANGED
|
@@ -388,6 +388,33 @@ function keepListItemBraces(tokens, src) {
|
|
|
388
388
|
tok.attrs = null;
|
|
389
389
|
});
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Per line of `source` (the body, no front matter, split at `\n` as the commands engine splits
|
|
393
|
+
* it): whether the line is inside a `:::` block as the parser nests them. The opener's line is
|
|
394
|
+
* not, nor the closer's; the body between is. Read from the
|
|
395
|
+
* parse rather than re-derived, so the commands engine's outline (a heading in a block is never
|
|
396
|
+
* structure) cannot drift from the parser: a line scan cannot see where a list item ends a block,
|
|
397
|
+
* which names are blocks, or raw HTML. An unknown name never closed is text, so not a block.
|
|
398
|
+
*/
|
|
399
|
+
export function containerOpenBefore(source) {
|
|
400
|
+
// As markdown-it's normalize step (which block.parse skips): \r\n is a line break. A lone \r is
|
|
401
|
+
// one to markdown-it but not to the engine's split at \n, so it becomes a space
|
|
402
|
+
const src = source.replace(/\r\n/g, "\n").replace(/\r/g, " ");
|
|
403
|
+
const inside = new Array(src.split("\n").length).fill(false);
|
|
404
|
+
// block rules only: the inline pass changes no line a block spans
|
|
405
|
+
const md = sharedParser();
|
|
406
|
+
const tokens = [];
|
|
407
|
+
md.block.parse(src, md, {}, tokens);
|
|
408
|
+
markUnclosedGenericContainers(tokens, src);
|
|
409
|
+
for (const tok of tokens) {
|
|
410
|
+
const map = tok.map;
|
|
411
|
+
if (tok.nesting !== 1 || !tok.type.startsWith("container_") || !map || tok.meta?.unclosed)
|
|
412
|
+
continue;
|
|
413
|
+
for (let l = map[0] + 1; l < map[1] && l < inside.length; l++)
|
|
414
|
+
inside[l] = true;
|
|
415
|
+
}
|
|
416
|
+
return inside;
|
|
417
|
+
}
|
|
391
418
|
/**
|
|
392
419
|
* markdown-it-container runs a block with no closing marker to the end of the document. For a
|
|
393
420
|
* generic (unknown-name) container that would turn any prose line starting `:::word` into a block
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Register the container rule (container-rule.ts, adapted from markdown-it-container) for each
|
|
3
3
|
* known Dogsbay block directive.
|
|
4
4
|
*
|
|
5
|
-
* Each directive name is
|
|
6
|
-
*
|
|
7
|
-
* nest. Same-name nesting requires more colons (::::).
|
|
5
|
+
* Each directive name is registered separately. Blocks nest at any colon count, the same name
|
|
6
|
+
* too: the rule keeps a stack of the blocks open inside it (NESTING in container-rule.ts).
|
|
8
7
|
*/
|
|
9
8
|
import type MarkdownIt from "markdown-it";
|
|
10
9
|
export declare const KNOWN_BLOCK_DIRECTIVES: readonly ["tabs", "tab", "steps", "step", "cards", "card", "grid", "grid-item", "details", "callout", "note", "tip", "info", "important", "warning", "caution", "danger", "success", "hint", "attention", "error", "failure", "bug", "question", "abstract", "quote", "button", "figure", "math", "api-endpoint", "api-params", "api-response", "config", "files", "example", "accordion", "accordion-item", "link-card", "autodoc", "api-class", "api-member", "api-doc", "api-source", "youtube", "openapi", "theme"];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { containerRule } from "./container-rule.js";
|
|
1
|
+
import { containerRule, registerOpener } from "./container-rule.js";
|
|
2
2
|
import { parseAttrs } from "./parse-attrs.js";
|
|
3
3
|
export const KNOWN_BLOCK_DIRECTIVES = [
|
|
4
4
|
"tabs",
|
|
@@ -72,6 +72,8 @@ export const KNOWN_BLOCK_DIRECTIVES = [
|
|
|
72
72
|
export const GENERIC_CONTAINER = "dogsbay_generic";
|
|
73
73
|
export function containersPlugin(md) {
|
|
74
74
|
for (const name of KNOWN_BLOCK_DIRECTIVES) {
|
|
75
|
+
// `:::youtube{#id}` has no body, and is often written with no closer
|
|
76
|
+
registerOpener(name, { leaf: name === "youtube" });
|
|
75
77
|
containerRule(md, name, {
|
|
76
78
|
validate: (params) => {
|
|
77
79
|
const trimmed = params.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-dogsbay-md",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.127",
|
|
4
4
|
"description": "Dogsbay Markdown dialect — canonical format for TreeNode[] with directives, YAML bodies, and intelligent fallbacks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"markdown-it-deflist": "^3.0.1",
|
|
40
40
|
"markdown-it-footnote": "^4.0.0",
|
|
41
41
|
"markdown-it-front-matter": "^0.2.4",
|
|
42
|
-
"@dogsbay/minja": "0.2.0-beta.
|
|
43
|
-
"@dogsbay/serialize-core": "0.2.0-beta.
|
|
44
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
42
|
+
"@dogsbay/minja": "0.2.0-beta.127",
|
|
43
|
+
"@dogsbay/serialize-core": "0.2.0-beta.127",
|
|
44
|
+
"@dogsbay/types": "0.2.0-beta.127"
|
|
45
45
|
},
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"repository": {
|