@dogsbay/format-dogsbay-md 0.2.0-beta.13 → 0.2.0-beta.130
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/attributes.d.ts +16 -2
- package/dist/attributes.js +14 -3
- package/dist/browser.d.ts +24 -0
- package/dist/browser.js +24 -0
- package/dist/capabilities.d.ts +25 -0
- package/dist/capabilities.js +91 -0
- package/dist/cli.d.ts +18 -1
- package/dist/cli.js +271 -30
- package/dist/code-regions.d.ts +88 -0
- package/dist/code-regions.js +214 -0
- package/dist/container-rule.d.ts +83 -0
- package/dist/container-rule.js +280 -0
- package/dist/diagnostics.d.ts +51 -0
- package/dist/diagnostics.js +335 -0
- package/dist/directive-guard.d.ts +10 -0
- package/dist/directive-guard.js +15 -0
- package/dist/directive-preserve.d.ts +71 -0
- package/dist/directive-preserve.js +426 -0
- package/dist/directives.d.ts +0 -1
- package/dist/directives.js +23 -2
- package/dist/escape.d.ts +26 -8
- package/dist/escape.js +101 -31
- package/dist/frontmatter.d.ts +11 -0
- package/dist/frontmatter.js +66 -0
- package/dist/headings.d.ts +43 -0
- package/dist/headings.js +156 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +13 -3
- package/dist/inline.d.ts +0 -4
- package/dist/inline.js +242 -10
- package/dist/minja-mask.d.ts +63 -0
- package/dist/minja-mask.js +127 -0
- package/dist/nav-file.d.ts +26 -4
- package/dist/nav-file.js +91 -15
- package/dist/nav.d.ts +7 -1
- package/dist/nav.js +23 -5
- package/dist/parse-attrs.d.ts +9 -1
- package/dist/parse-attrs.js +28 -1
- package/dist/parse.d.ts +37 -2
- package/dist/parse.js +811 -50
- package/dist/plugin-block-leaf.d.ts +0 -1
- package/dist/plugin-block-leaf.js +3 -16
- package/dist/plugin-containers.d.ts +23 -6
- package/dist/plugin-containers.js +69 -4
- package/dist/plugin-inline-directives.d.ts +2 -1
- package/dist/plugin-inline-directives.js +10 -16
- package/dist/plugin-mark.d.ts +12 -0
- package/dist/plugin-mark.js +74 -0
- package/dist/plugin-math.d.ts +22 -0
- package/dist/plugin-math.js +90 -0
- package/dist/renderers/diagrams.d.ts +28 -0
- package/dist/renderers/diagrams.js +92 -0
- package/dist/renderers/mermaid.d.ts +23 -0
- package/dist/renderers/mermaid.js +151 -0
- package/dist/renderers/pele.d.ts +13 -0
- package/dist/renderers/pele.js +42 -0
- package/dist/serialize.d.ts +1 -2
- package/dist/serialize.js +520 -71
- package/dist/template-pages.d.ts +68 -0
- package/dist/template-pages.js +248 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.js +18 -2
- package/dist/yaml.d.ts +1 -2
- package/dist/yaml.js +78 -3
- package/package.json +27 -12
- package/dist/attributes.d.ts.map +0 -1
- package/dist/attributes.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/directives.d.ts.map +0 -1
- package/dist/directives.js.map +0 -1
- package/dist/escape.d.ts.map +0 -1
- package/dist/escape.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js.map +0 -1
- package/dist/nav-file.d.ts.map +0 -1
- package/dist/nav-file.js.map +0 -1
- package/dist/nav.d.ts.map +0 -1
- package/dist/nav.js.map +0 -1
- package/dist/parse-attrs.d.ts.map +0 -1
- package/dist/parse-attrs.js.map +0 -1
- package/dist/parse.d.ts.map +0 -1
- package/dist/parse.js.map +0 -1
- package/dist/plugin-block-leaf.d.ts.map +0 -1
- package/dist/plugin-block-leaf.js.map +0 -1
- package/dist/plugin-containers.d.ts.map +0 -1
- package/dist/plugin-containers.js.map +0 -1
- package/dist/plugin-inline-directives.d.ts.map +0 -1
- package/dist/plugin-inline-directives.js.map +0 -1
- package/dist/serialize.d.ts.map +0 -1
- package/dist/serialize.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/yaml.d.ts.map +0 -1
- package/dist/yaml.js.map +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-region walker — THE fence grammar shared by every tool that must
|
|
3
|
+
* treat code specially: the build preprocessor masks Minja openers
|
|
4
|
+
* inside code so template examples never evaluate, the workspace
|
|
5
|
+
* indexer blanks code so `{% include %}` examples never count as real
|
|
6
|
+
* includes, and the refactor commands edit only prose so code samples
|
|
7
|
+
* are never rewritten. One implementation, so none of them can drift.
|
|
8
|
+
*
|
|
9
|
+
* Fence rules (CommonMark): a fence opens with ``` or ~~~ (3+ chars,
|
|
10
|
+
* info string allowed) and closes only on a line of the SAME character
|
|
11
|
+
* with AT LEAST the opener's length and nothing but whitespace after —
|
|
12
|
+
* the serializer emits escalated fences (```` around content containing
|
|
13
|
+
* ```), so honoring the length rule is load-bearing. Fence-looking
|
|
14
|
+
* lines inside an open fence that don't close it are code content.
|
|
15
|
+
*/
|
|
16
|
+
type LineKind = "prose" | "fence" | "code";
|
|
17
|
+
/** Context passed to a code transform: which fence the line belongs to. */
|
|
18
|
+
export interface CodeRegionContext {
|
|
19
|
+
/**
|
|
20
|
+
* Fence info string (e.g. `yaml {minja}`). For inline code spans:
|
|
21
|
+
* the markdown-it-attrs group immediately following the closing
|
|
22
|
+
* backticks when present (e.g. `{minja}` for `` `x`{minja} ``),
|
|
23
|
+
* empty otherwise — so `isMinjaProcessedFence` grades fences and
|
|
24
|
+
* marked inline spans with the same predicate.
|
|
25
|
+
*/
|
|
26
|
+
fenceInfo: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Transform CODE content: every line inside a fence, and the inner
|
|
30
|
+
* content of inline code spans on prose lines (backticks kept). Fence
|
|
31
|
+
* marker lines themselves are untouched.
|
|
32
|
+
*
|
|
33
|
+
* `transformInlineAttrs` (optional) rewrites the attr group of an
|
|
34
|
+
* inline span at re-emission — return `""` to drop it. Only inline
|
|
35
|
+
* spans see it; fence info strings are never routed through it.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Line-by-line fence tracker on THE shared grammar — for tools that must
|
|
39
|
+
* walk a document themselves (the remote-include inliner) instead of
|
|
40
|
+
* transforming regions. A locally re-implemented open/close regex pair
|
|
41
|
+
* drifted immediately: it missed the deflist-hosted `: ```` opener the
|
|
42
|
+
* shared grammar just gained, took the indented closer for an opener,
|
|
43
|
+
* and re-created the exact phantom-fence inversion it was fixing
|
|
44
|
+
* (issue #039 review).
|
|
45
|
+
*
|
|
46
|
+
* step(line) returns what the line IS: "fence" (an opener or closer),
|
|
47
|
+
* "code" (content of an open fence), or "prose". open() reports the
|
|
48
|
+
* state AFTER the last step, so an opener steps "fence" with
|
|
49
|
+
* open()===true and its closer steps "fence" with open()===false.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createFenceTracker(): {
|
|
52
|
+
step(line: string): LineKind;
|
|
53
|
+
open(): boolean;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Transform only fence OPENING lines' info strings, on the same shared
|
|
57
|
+
* grammar — a fence-looking line inside an open fence is code content
|
|
58
|
+
* and is never touched. Used to strip the spent `{minja}` marker after
|
|
59
|
+
* the terminal Minja render (a fence marker that survives into the
|
|
60
|
+
* mirror leaks into serializers that round-trip fence info verbatim —
|
|
61
|
+
* the Obsidian vault showed literal ```` ```yaml {minja} ````).
|
|
62
|
+
*/
|
|
63
|
+
export declare function transformFenceInfo(source: string, transform: (info: string) => string): string;
|
|
64
|
+
export declare function transformCodeRegions(source: string, transform: (codeText: string, ctx: CodeRegionContext) => string, transformInlineAttrs?: (attrs: string) => string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The inverse walk: transform PROSE lines (outside fences), leaving
|
|
67
|
+
* fenced content and the fence lines untouched. Used by refactor
|
|
68
|
+
* commands that rewrite markdown structure line-wise (heading shifts,
|
|
69
|
+
* section splits) — a `# heading` inside a code example must never be
|
|
70
|
+
* treated as structure.
|
|
71
|
+
*/
|
|
72
|
+
export declare function transformProseLines(source: string, transform: (line: string, lineIndex: number) => string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Transform PROSE TEXT SEGMENTS: prose lines with their inline code
|
|
75
|
+
* spans protected (the transform sees only the non-code stretches of
|
|
76
|
+
* each prose line). This is the write-side counterpart of
|
|
77
|
+
* transformCodeRegions — refactor commands rewrite references through
|
|
78
|
+
* it so `[x](old.md)` or `{{ key }}` inside examples survive verbatim.
|
|
79
|
+
*/
|
|
80
|
+
export declare function transformProseSegments(source: string, transform: (proseText: string) => string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Fence state BEFORE each line: `openBefore[i]` is true when line i
|
|
83
|
+
* starts inside an unclosed fence. One extra trailing entry gives the
|
|
84
|
+
* state after the last line. Lets line-range refactors check "does
|
|
85
|
+
* this range start or end mid-fence" with the same grammar.
|
|
86
|
+
*/
|
|
87
|
+
export declare function fenceOpenBefore(source: string): boolean[];
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-region walker — THE fence grammar shared by every tool that must
|
|
3
|
+
* treat code specially: the build preprocessor masks Minja openers
|
|
4
|
+
* inside code so template examples never evaluate, the workspace
|
|
5
|
+
* indexer blanks code so `{% include %}` examples never count as real
|
|
6
|
+
* includes, and the refactor commands edit only prose so code samples
|
|
7
|
+
* are never rewritten. One implementation, so none of them can drift.
|
|
8
|
+
*
|
|
9
|
+
* Fence rules (CommonMark): a fence opens with ``` or ~~~ (3+ chars,
|
|
10
|
+
* info string allowed) and closes only on a line of the SAME character
|
|
11
|
+
* with AT LEAST the opener's length and nothing but whitespace after —
|
|
12
|
+
* the serializer emits escalated fences (```` around content containing
|
|
13
|
+
* ```), so honoring the length rule is load-bearing. Fence-looking
|
|
14
|
+
* lines inside an open fence that don't close it are code content.
|
|
15
|
+
*/
|
|
16
|
+
// A fence may open as DEFLIST DEFINITION content with the definition
|
|
17
|
+
// marker glued to the opener (`: ```` — the adoc engine emits this when
|
|
18
|
+
// a dlist definition's first block is a listing; markdown-it-deflist also
|
|
19
|
+
// accepts `~` as the marker). markdown-it opens a fence there, so this
|
|
20
|
+
// grammar must too: without the prefix the walker took the CLOSER for an
|
|
21
|
+
// opener and inverted — prose after the dlist was masked as code and its
|
|
22
|
+
// `{{ attr }}` refs never resolved (virt-cluster-resource-requirements,
|
|
23
|
+
// the #030 residual class). The marker takes 1–3 spaces: at 4+ markdown-it
|
|
24
|
+
// reads an indented code block, not a fence. The info capture excludes a
|
|
25
|
+
// trailing `\r` so CRLF text grades identically (`.` can't match `\r`,
|
|
26
|
+
// which silently disabled the whole grammar on CRLF files).
|
|
27
|
+
//
|
|
28
|
+
// KNOWN bounded divergence: markdown-it only forms a deflist when a term
|
|
29
|
+
// paragraph precedes; at document start or right after a heading/list a
|
|
30
|
+
// `: ```` line is plain text. The walker opens (and, via the dedent
|
|
31
|
+
// rule, promptly closes) a fence there — a line-local grammar cannot see
|
|
32
|
+
// the term context. The divergence is bounded to the pseudo-fence's own
|
|
33
|
+
// region; the tail re-aligns at the first dedent.
|
|
34
|
+
const OPEN_RE = /^\s*(?:[:~][ \t]{1,3})?(`{3,}|~{3,})(.*?)\r?$/;
|
|
35
|
+
const CLOSE_RE = /^\s*(`{3,}|~{3,})\s*$/;
|
|
36
|
+
const stepFence = (state, line) => {
|
|
37
|
+
if (state.open) {
|
|
38
|
+
// A deflist-hosted fence ends when a non-blank line dedents out of
|
|
39
|
+
// the definition (indent < 4) — markdown-it terminates the fence
|
|
40
|
+
// BEFORE reading that line, so reprocess it in closed state: it may
|
|
41
|
+
// itself open a REAL fence (the col-0 ``` after a dedented deflist
|
|
42
|
+
// fence swallowed the document tail when taken as a closer — review
|
|
43
|
+
// finding).
|
|
44
|
+
if (state.hosted && /^ {0,3}\S/.test(line)) {
|
|
45
|
+
state.open = false;
|
|
46
|
+
state.info = "";
|
|
47
|
+
state.hosted = false;
|
|
48
|
+
// fall through to closed-state handling for this same line
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const close = line.match(CLOSE_RE);
|
|
52
|
+
if (close && close[1][0] === state.marker && close[1].length >= state.length) {
|
|
53
|
+
state.open = false;
|
|
54
|
+
state.info = "";
|
|
55
|
+
state.hosted = false;
|
|
56
|
+
return "fence";
|
|
57
|
+
}
|
|
58
|
+
return "code";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const open = line.match(OPEN_RE);
|
|
62
|
+
if (open) {
|
|
63
|
+
state.open = true;
|
|
64
|
+
state.marker = open[1][0];
|
|
65
|
+
state.length = open[1].length;
|
|
66
|
+
state.info = open[2].trim();
|
|
67
|
+
state.hosted = /^\s*[:~][ \t]/.test(line);
|
|
68
|
+
return "fence";
|
|
69
|
+
}
|
|
70
|
+
return "prose";
|
|
71
|
+
};
|
|
72
|
+
const newState = () => ({ open: false, marker: "", length: 0, info: "", hosted: false });
|
|
73
|
+
/**
|
|
74
|
+
* Transform CODE content: every line inside a fence, and the inner
|
|
75
|
+
* content of inline code spans on prose lines (backticks kept). Fence
|
|
76
|
+
* marker lines themselves are untouched.
|
|
77
|
+
*
|
|
78
|
+
* `transformInlineAttrs` (optional) rewrites the attr group of an
|
|
79
|
+
* inline span at re-emission — return `""` to drop it. Only inline
|
|
80
|
+
* spans see it; fence info strings are never routed through it.
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* Line-by-line fence tracker on THE shared grammar — for tools that must
|
|
84
|
+
* walk a document themselves (the remote-include inliner) instead of
|
|
85
|
+
* transforming regions. A locally re-implemented open/close regex pair
|
|
86
|
+
* drifted immediately: it missed the deflist-hosted `: ```` opener the
|
|
87
|
+
* shared grammar just gained, took the indented closer for an opener,
|
|
88
|
+
* and re-created the exact phantom-fence inversion it was fixing
|
|
89
|
+
* (issue #039 review).
|
|
90
|
+
*
|
|
91
|
+
* step(line) returns what the line IS: "fence" (an opener or closer),
|
|
92
|
+
* "code" (content of an open fence), or "prose". open() reports the
|
|
93
|
+
* state AFTER the last step, so an opener steps "fence" with
|
|
94
|
+
* open()===true and its closer steps "fence" with open()===false.
|
|
95
|
+
*/
|
|
96
|
+
export function createFenceTracker() {
|
|
97
|
+
const state = newState();
|
|
98
|
+
return {
|
|
99
|
+
step: (line) => stepFence(state, line),
|
|
100
|
+
open: () => state.open,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Transform only fence OPENING lines' info strings, on the same shared
|
|
105
|
+
* grammar — a fence-looking line inside an open fence is code content
|
|
106
|
+
* and is never touched. Used to strip the spent `{minja}` marker after
|
|
107
|
+
* the terminal Minja render (a fence marker that survives into the
|
|
108
|
+
* mirror leaks into serializers that round-trip fence info verbatim —
|
|
109
|
+
* the Obsidian vault showed literal ```` ```yaml {minja} ````).
|
|
110
|
+
*/
|
|
111
|
+
export function transformFenceInfo(source, transform) {
|
|
112
|
+
const state = newState();
|
|
113
|
+
return source
|
|
114
|
+
.split("\n")
|
|
115
|
+
.map((line) => {
|
|
116
|
+
const wasOpen = state.open;
|
|
117
|
+
if (stepFence(state, line) === "fence" && !wasOpen) {
|
|
118
|
+
// Opening fence: rewrite the info string in place. Untouched info
|
|
119
|
+
// returns the line BYTE-IDENTICAL (incl. any trailing \r) — this
|
|
120
|
+
// is a write path on the shared grammar, and rewriting lines the
|
|
121
|
+
// transform didn't change broke mirror byte-stability (review).
|
|
122
|
+
const m = line.match(OPEN_RE);
|
|
123
|
+
if (m) {
|
|
124
|
+
const runEnd = line.indexOf(m[1]) + m[1].length;
|
|
125
|
+
const hadCR = line.endsWith("\r");
|
|
126
|
+
const info = line.substring(runEnd, hadCR ? line.length - 1 : line.length);
|
|
127
|
+
const newInfo = transform(info);
|
|
128
|
+
return newInfo === info ? line : line.substring(0, runEnd) + newInfo + (hadCR ? "\r" : "");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return line;
|
|
132
|
+
})
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
135
|
+
export function transformCodeRegions(source, transform, transformInlineAttrs) {
|
|
136
|
+
const state = newState();
|
|
137
|
+
return source
|
|
138
|
+
.split("\n")
|
|
139
|
+
.map((line) => {
|
|
140
|
+
switch (stepFence(state, line)) {
|
|
141
|
+
case "fence":
|
|
142
|
+
return line;
|
|
143
|
+
case "code":
|
|
144
|
+
return transform(line, { fenceInfo: state.info });
|
|
145
|
+
default:
|
|
146
|
+
// An attr group directly after the closing ticks travels as
|
|
147
|
+
// the region's info string: `` `x`{minja} `` opts the span
|
|
148
|
+
// into Minja processing exactly like a `{minja}` fence.
|
|
149
|
+
// Backticks are excluded from the group so a brace group can
|
|
150
|
+
// never swallow a span opener and shift pairing vs markdown-it.
|
|
151
|
+
return line.replace(/(`+)([^`]*?)\1(\{[^{}`\n]*\})?/g, (_m, ticks, inner, attrs) => ticks + transform(inner, { fenceInfo: attrs ?? "" }) + ticks +
|
|
152
|
+
(attrs === undefined ? "" : transformInlineAttrs ? transformInlineAttrs(attrs) : attrs));
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.join("\n");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The inverse walk: transform PROSE lines (outside fences), leaving
|
|
159
|
+
* fenced content and the fence lines untouched. Used by refactor
|
|
160
|
+
* commands that rewrite markdown structure line-wise (heading shifts,
|
|
161
|
+
* section splits) — a `# heading` inside a code example must never be
|
|
162
|
+
* treated as structure.
|
|
163
|
+
*/
|
|
164
|
+
export function transformProseLines(source, transform) {
|
|
165
|
+
const state = newState();
|
|
166
|
+
return source
|
|
167
|
+
.split("\n")
|
|
168
|
+
.map((line, index) => (stepFence(state, line) === "prose" ? transform(line, index) : line))
|
|
169
|
+
.join("\n");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Transform PROSE TEXT SEGMENTS: prose lines with their inline code
|
|
173
|
+
* spans protected (the transform sees only the non-code stretches of
|
|
174
|
+
* each prose line). This is the write-side counterpart of
|
|
175
|
+
* transformCodeRegions — refactor commands rewrite references through
|
|
176
|
+
* it so `[x](old.md)` or `{{ key }}` inside examples survive verbatim.
|
|
177
|
+
*/
|
|
178
|
+
export function transformProseSegments(source, transform) {
|
|
179
|
+
const state = newState();
|
|
180
|
+
return source
|
|
181
|
+
.split("\n")
|
|
182
|
+
.map((line) => {
|
|
183
|
+
if (stepFence(state, line) !== "prose")
|
|
184
|
+
return line;
|
|
185
|
+
// Split the line into code spans and prose stretches; transform
|
|
186
|
+
// only the prose stretches
|
|
187
|
+
const parts = [];
|
|
188
|
+
let last = 0;
|
|
189
|
+
for (const match of line.matchAll(/(`+)[^`]*?\1/g)) {
|
|
190
|
+
parts.push(transform(line.slice(last, match.index)));
|
|
191
|
+
parts.push(match[0]);
|
|
192
|
+
last = match.index + match[0].length;
|
|
193
|
+
}
|
|
194
|
+
parts.push(transform(line.slice(last)));
|
|
195
|
+
return parts.join("");
|
|
196
|
+
})
|
|
197
|
+
.join("\n");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Fence state BEFORE each line: `openBefore[i]` is true when line i
|
|
201
|
+
* starts inside an unclosed fence. One extra trailing entry gives the
|
|
202
|
+
* state after the last line. Lets line-range refactors check "does
|
|
203
|
+
* this range start or end mid-fence" with the same grammar.
|
|
204
|
+
*/
|
|
205
|
+
export function fenceOpenBefore(source) {
|
|
206
|
+
const state = newState();
|
|
207
|
+
const states = [];
|
|
208
|
+
for (const line of source.split("\n")) {
|
|
209
|
+
states.push(state.open);
|
|
210
|
+
stepFence(state, line);
|
|
211
|
+
}
|
|
212
|
+
states.push(state.open);
|
|
213
|
+
return states;
|
|
214
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `:::name` container block rule: markdown-it-container 4.0.0 (MIT,
|
|
3
|
+
* Vitaly Puzrin, Alex Kocharin), adapted. Three changes, all in how the
|
|
4
|
+
* closing line is found:
|
|
5
|
+
*
|
|
6
|
+
* 1. A closer may carry an HTML comment, `::: <!-- end note -->`, the way an
|
|
7
|
+
* author labels an XML end tag. The comment changes nothing about which
|
|
8
|
+
* container closes; it rides on the close token (`info`) so the parser can
|
|
9
|
+
* keep it and diagnostics can check the name.
|
|
10
|
+
* 2. A `:::` line inside a code block does not close the container.
|
|
11
|
+
* markdown-it-container scanned raw lines, so a code example showing a
|
|
12
|
+
* directive closed the container around it and the rest of the page became
|
|
13
|
+
* one code block, with no message. Which lines are code comes from one
|
|
14
|
+
* block-level pass over the source with containers off (codeLines).
|
|
15
|
+
*
|
|
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.
|
|
20
|
+
*
|
|
21
|
+
* markdown-it-container's licence:
|
|
22
|
+
*
|
|
23
|
+
* Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
|
|
24
|
+
*
|
|
25
|
+
* Permission is hereby granted, free of charge, to any person
|
|
26
|
+
* obtaining a copy of this software and associated documentation
|
|
27
|
+
* files (the "Software"), to deal in the Software without
|
|
28
|
+
* restriction, including without limitation the rights to use,
|
|
29
|
+
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
30
|
+
* copies of the Software, and to permit persons to whom the
|
|
31
|
+
* Software is furnished to do so, subject to the following
|
|
32
|
+
* conditions:
|
|
33
|
+
*
|
|
34
|
+
* The above copyright notice and this permission notice shall be
|
|
35
|
+
* included in all copies or substantial portions of the Software.
|
|
36
|
+
*
|
|
37
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
38
|
+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
39
|
+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
40
|
+
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
41
|
+
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
42
|
+
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
43
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
44
|
+
* OTHER DEALINGS IN THE SOFTWARE.
|
|
45
|
+
*/
|
|
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. `:::` always opens a block that needs a
|
|
62
|
+
* closer; a directive with no body is a leaf, `::youtube{#id}` (plugin-block-leaf.ts), so no
|
|
63
|
+
* opener depends on the line after it.
|
|
64
|
+
*
|
|
65
|
+
* VERIFY. The scan reads raw lines, and markdown-it may not read an opener there as one: in raw
|
|
66
|
+
* HTML, a `$$` math block, a fence that never closes. Where that is depends on where the blocks
|
|
67
|
+
* around it end, so no pass over the page can say in advance. So after the body is tokenized, every
|
|
68
|
+
* opener the scan pushed must have opened a block in it; any that did not are left out and the
|
|
69
|
+
* body is scanned and tokenized again. Counted wrongly, an opener leaves the outer block open to
|
|
70
|
+
* the end of the page (the rule before the stack lost one closer at most). What a block learns is
|
|
71
|
+
* kept for the parse, by its opening line: when an outer block retries, the blocks inside it are
|
|
72
|
+
* tokenized again and start from what they learned, rather than repeating their own retries (which
|
|
73
|
+
* doubled the work at every level). A learned line can be stale in the new context, so the check
|
|
74
|
+
* runs both ways: a skipped line that markdown-it did open as a block is pushed again. Retried
|
|
75
|
+
* lines come out of one budget per parse (a few times the page's lines); past it, the scan stands.
|
|
76
|
+
*/
|
|
77
|
+
export interface ContainerOptions {
|
|
78
|
+
validate: (params: string, markup: string) => boolean;
|
|
79
|
+
render?: (tokens: unknown[], idx: number) => string;
|
|
80
|
+
}
|
|
81
|
+
/** Make `name` an opener for the nesting scan (the known directives; not the generic container). */
|
|
82
|
+
export declare function registerOpener(name: string): void;
|
|
83
|
+
export declare function containerRule(md: MarkdownIt, name: string, options: ContainerOptions): void;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What may follow a closer's colons: nothing, or spaces and one HTML comment, then spaces. The
|
|
3
|
+
* comment is not optional inside the group: `[ \t]*(…)?[ \t]*` backtracks quadratically on a long
|
|
4
|
+
* run of spaces followed by anything else.
|
|
5
|
+
*/
|
|
6
|
+
const CLOSER_TAIL = /^(?:[ \t]*|[ \t]*(<!--(?:(?!-->)[\s\S])*-->)[ \t]*)$/;
|
|
7
|
+
/** state.env keys: the pre-pass that finds the code, and its result. */
|
|
8
|
+
const NO_CONTAINERS = "__dogsbayNoContainers";
|
|
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";
|
|
13
|
+
/**
|
|
14
|
+
* The source lines inside a fenced code block, from one block-level pass over the whole source
|
|
15
|
+
* with containers switched off, made the first time a container needs it and kept in the parse's
|
|
16
|
+
* env. A `:::` line cannot change where a fence starts or ends (a fence ends only at its own
|
|
17
|
+
* closing fence or the end of its list item or quote), so the pass agrees with the real parse,
|
|
18
|
+
* and markdown-it reads fences in quotes, list items and definitions as a line scan does not.
|
|
19
|
+
* One pass per parse, so the cost is linear however many containers there are or how deep.
|
|
20
|
+
*
|
|
21
|
+
* Only a fence that CLOSES counts. With no container to end it, an unclosed fence in the pass runs
|
|
22
|
+
* to the end of the page, and every closer after it would be code: a ````` ```` ````` fence closed
|
|
23
|
+
* with three backticks would take the rest of the page into its container. Left out, it ends at
|
|
24
|
+
* the container's closer, as with markdown-it-container. (Still misread, and no worse than
|
|
25
|
+
* markdown-it-container: a `:::name` straight after a list item, lazily continuing it in the pass,
|
|
26
|
+
* so a fence indented four in the container is read in the item.)
|
|
27
|
+
*/
|
|
28
|
+
function codeLines(state) {
|
|
29
|
+
const env = state.env;
|
|
30
|
+
const cached = env?.[CODE_LINES];
|
|
31
|
+
if (cached && cached.src === state.src && cached.md === state.md)
|
|
32
|
+
return cached.lines;
|
|
33
|
+
const tokens = [];
|
|
34
|
+
state.md.block.parse(state.src, state.md, { [NO_CONTAINERS]: true }, tokens);
|
|
35
|
+
const source = state.src.split(/\r\n|\r|\n/);
|
|
36
|
+
const lines = new Set();
|
|
37
|
+
for (const t of tokens) {
|
|
38
|
+
if (t.type !== "fence" || !t.map || !closesFence(source[t.map[1] - 1], t.markup, t.map[1] - 1 > t.map[0]))
|
|
39
|
+
continue;
|
|
40
|
+
// A fence's opening line is not inside it; the lines after it, to its closing fence, are
|
|
41
|
+
for (let l = t.map[0] + 1; l < t.map[1]; l++)
|
|
42
|
+
lines.add(l);
|
|
43
|
+
}
|
|
44
|
+
if (env)
|
|
45
|
+
env[CODE_LINES] = { src: state.src, md: state.md, lines };
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
/** Whether `line` (as written, with any `>` and indentation) is the closing fence of a fence opened with `markup`. */
|
|
49
|
+
function closesFence(line, markup, after) {
|
|
50
|
+
if (!after || line === undefined)
|
|
51
|
+
return false;
|
|
52
|
+
const m = /^[ \t>]*(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
53
|
+
return m !== null && m[1][0] === markup[0] && m[1].length >= markup.length;
|
|
54
|
+
}
|
|
55
|
+
/** The directive names the scan treats as openers (plugin-containers registers them). */
|
|
56
|
+
const KNOWN_OPENERS = new Set();
|
|
57
|
+
/** Whether the text after an opener's colons starts with a known directive name. */
|
|
58
|
+
function isKnownOpener(rest) {
|
|
59
|
+
const m = /^\s*([A-Za-z][\w-]*)(?=$|[\s{[])/.exec(rest);
|
|
60
|
+
return m !== null && KNOWN_OPENERS.has(m[1]);
|
|
61
|
+
}
|
|
62
|
+
/** Make `name` an opener for the nesting scan (the known directives; not the generic container). */
|
|
63
|
+
export function registerOpener(name) {
|
|
64
|
+
KNOWN_OPENERS.add(name);
|
|
65
|
+
}
|
|
66
|
+
/** Tokens whose lines are not block openers: an opener the scan pushed inside one is wrong (VERIFY). */
|
|
67
|
+
const READ_AS_OTHER = new Set(["paragraph_open", "heading_open", "html_block", "fence", "code_block", "math_block", "hr", "table_open"]);
|
|
68
|
+
/** The openers each block (by opening line) learned to skip in this parse (VERIFY). */
|
|
69
|
+
function learned(state) {
|
|
70
|
+
const env = state.env;
|
|
71
|
+
if (!env)
|
|
72
|
+
return new Map();
|
|
73
|
+
const kept = env[LEARNED];
|
|
74
|
+
if (kept && kept.src === state.src)
|
|
75
|
+
return kept.map;
|
|
76
|
+
const map = new Map();
|
|
77
|
+
env[LEARNED] = { src: state.src, map };
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
/** The lines left in this parse's retry budget (VERIFY), shared by every block in it. */
|
|
81
|
+
function retryBudget(state) {
|
|
82
|
+
const env = state.env;
|
|
83
|
+
const fresh = { lines: 4 * state.bMarks.length + 1000 };
|
|
84
|
+
if (!env)
|
|
85
|
+
return fresh;
|
|
86
|
+
const kept = env[RETRY_BUDGET];
|
|
87
|
+
if (kept && kept.src === state.src)
|
|
88
|
+
return kept.budget;
|
|
89
|
+
env[RETRY_BUDGET] = { src: state.src, budget: fresh };
|
|
90
|
+
return fresh;
|
|
91
|
+
}
|
|
92
|
+
export function containerRule(md, name, options) {
|
|
93
|
+
const MARKER = ":".charCodeAt(0);
|
|
94
|
+
function container(state, startLine, endLine, silent) {
|
|
95
|
+
const start = state.bMarks[startLine] + state.tShift[startLine];
|
|
96
|
+
const max = state.eMarks[startLine];
|
|
97
|
+
if (state.src.charCodeAt(start) !== MARKER)
|
|
98
|
+
return false;
|
|
99
|
+
if (state.env?.[NO_CONTAINERS])
|
|
100
|
+
return false;
|
|
101
|
+
let pos = start + 1;
|
|
102
|
+
while (pos <= max && state.src.charCodeAt(pos) === MARKER)
|
|
103
|
+
pos++;
|
|
104
|
+
const markerCount = pos - start;
|
|
105
|
+
if (markerCount < 3)
|
|
106
|
+
return false;
|
|
107
|
+
const markup = state.src.slice(start, pos);
|
|
108
|
+
const params = state.src.slice(pos, max);
|
|
109
|
+
if (!options.validate(params, markup))
|
|
110
|
+
return false;
|
|
111
|
+
if (silent)
|
|
112
|
+
return true;
|
|
113
|
+
const oldParent = state.parentType;
|
|
114
|
+
const oldLineMax = state.lineMax;
|
|
115
|
+
state.parentType = "container";
|
|
116
|
+
const tokensBefore = state.tokens.length;
|
|
117
|
+
// Openers markdown-it did not open, found by an earlier try (VERIFY above)
|
|
118
|
+
const known = learned(state);
|
|
119
|
+
const notOpeners = new Set(known.get(startLine));
|
|
120
|
+
let scan;
|
|
121
|
+
for (;;) {
|
|
122
|
+
scan = scanBody(state, startLine, endLine, markerCount, notOpeners);
|
|
123
|
+
// lazy continuations never run past the closer
|
|
124
|
+
state.lineMax = scan.nextLine;
|
|
125
|
+
const open = state.push(`container_${name}_open`, "div", 1);
|
|
126
|
+
open.markup = markup;
|
|
127
|
+
open.block = true;
|
|
128
|
+
open.info = params;
|
|
129
|
+
open.map = [startLine, scan.nextLine];
|
|
130
|
+
if (!scan.autoClosed && (scan.skippedCloser !== undefined || scan.legacyCloser !== undefined)) {
|
|
131
|
+
open.meta = {
|
|
132
|
+
...(open.meta ?? {}),
|
|
133
|
+
...(scan.skippedCloser !== undefined ? { skippedCloser: scan.skippedCloser } : {}),
|
|
134
|
+
...(scan.legacyCloser !== undefined ? { legacyCloser: scan.legacyCloser } : {}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
state.md.block.tokenize(state, startLine + 1, scan.nextLine);
|
|
138
|
+
const close = state.push(`container_${name}_close`, "div", -1);
|
|
139
|
+
close.markup = scan.closeMarkup;
|
|
140
|
+
close.block = true;
|
|
141
|
+
close.info = scan.closeComment;
|
|
142
|
+
// Wrong: a pushed opener's line that markdown-it read as something else. A line it read as
|
|
143
|
+
// nothing (past markdown-it's nesting limit, where it drops the rest) is not evidence.
|
|
144
|
+
// Missed: a skipped line markdown-it did open as a block (a learned line, stale here).
|
|
145
|
+
const pushed = new Set(scan.pushed);
|
|
146
|
+
const wrong = [];
|
|
147
|
+
const missed = [];
|
|
148
|
+
if (pushed.size > 0 || notOpeners.size > 0) {
|
|
149
|
+
for (let i = tokensBefore + 1; i < state.tokens.length; i++) {
|
|
150
|
+
const t = state.tokens[i];
|
|
151
|
+
if (!t.map)
|
|
152
|
+
continue;
|
|
153
|
+
if (READ_AS_OTHER.has(t.type)) {
|
|
154
|
+
for (let l = t.map[0]; l < t.map[1]; l++)
|
|
155
|
+
if (pushed.delete(l))
|
|
156
|
+
wrong.push(l);
|
|
157
|
+
}
|
|
158
|
+
else if (t.nesting === 1 && t.type.startsWith("container_") && notOpeners.has(t.map[0]))
|
|
159
|
+
missed.push(t.map[0]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (wrong.length === 0 && missed.length === 0)
|
|
163
|
+
break;
|
|
164
|
+
const budget = retryBudget(state);
|
|
165
|
+
if (budget.lines < scan.nextLine - startLine) {
|
|
166
|
+
// Out of budget: this reading stands, with an opener known to be wrong; say so (diagnostics)
|
|
167
|
+
open.meta = { ...(open.meta ?? {}), unverified: true };
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
budget.lines -= scan.nextLine - startLine;
|
|
171
|
+
for (const l of wrong)
|
|
172
|
+
notOpeners.add(l);
|
|
173
|
+
for (const l of missed)
|
|
174
|
+
notOpeners.delete(l);
|
|
175
|
+
state.tokens.length = tokensBefore;
|
|
176
|
+
}
|
|
177
|
+
if (notOpeners.size > 0)
|
|
178
|
+
known.set(startLine, new Set(notOpeners));
|
|
179
|
+
else
|
|
180
|
+
known.delete(startLine);
|
|
181
|
+
state.parentType = oldParent;
|
|
182
|
+
state.lineMax = oldLineMax;
|
|
183
|
+
state.line = scan.nextLine + (scan.autoClosed ? 1 : 0);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
/** Find where the block opened at `startLine` ends (NESTING above), skipping the `notOpeners` lines as openers. */
|
|
187
|
+
function scanBody(state, startLine, endLine, markerCount, notOpeners) {
|
|
188
|
+
let code;
|
|
189
|
+
let nextLine = startLine;
|
|
190
|
+
const scan = { nextLine, autoClosed: false, closeMarkup: "", closeComment: "", pushed: [] };
|
|
191
|
+
// Blocks opened inside this one and not yet closed, innermost last (see NESTING above)
|
|
192
|
+
// `least` is the fewest colons from the outermost to this one, and `openCount` counts the open
|
|
193
|
+
// blocks by colons, so a closer that fits none is turned away without walking the stack
|
|
194
|
+
const inner = [];
|
|
195
|
+
const openCount = new Map();
|
|
196
|
+
const truncate = (k) => {
|
|
197
|
+
for (let j = k; j < inner.length; j++)
|
|
198
|
+
openCount.set(inner[j].colons, openCount.get(inner[j].colons) - 1);
|
|
199
|
+
inner.length = k;
|
|
200
|
+
};
|
|
201
|
+
for (;;) {
|
|
202
|
+
nextLine++;
|
|
203
|
+
// unclosed: closed by the end of the document or of the parent block
|
|
204
|
+
if (nextLine >= endLine)
|
|
205
|
+
break;
|
|
206
|
+
const start = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
207
|
+
const max = state.eMarks[nextLine];
|
|
208
|
+
if (start >= max)
|
|
209
|
+
continue;
|
|
210
|
+
// a non-empty line indented less than the block ends it (a list item's container)
|
|
211
|
+
if (state.sCount[nextLine] < state.blkIndent)
|
|
212
|
+
break;
|
|
213
|
+
// a block opened deeper (in a list item) ended at a line indented less than its opener
|
|
214
|
+
while (inner.length > 0 && state.sCount[nextLine] < inner[inner.length - 1].indent)
|
|
215
|
+
truncate(inner.length - 1);
|
|
216
|
+
if (state.src.charCodeAt(start) !== MARKER)
|
|
217
|
+
continue;
|
|
218
|
+
// openers and closers are indented less than 4 spaces past the block
|
|
219
|
+
if (state.sCount[nextLine] - state.blkIndent >= 4)
|
|
220
|
+
continue;
|
|
221
|
+
let pos = start + 1;
|
|
222
|
+
while (pos < max && state.src.charCodeAt(pos) === MARKER)
|
|
223
|
+
pos++;
|
|
224
|
+
const colons = pos - start;
|
|
225
|
+
if (colons < 3)
|
|
226
|
+
continue;
|
|
227
|
+
const rest = state.src.slice(pos, max);
|
|
228
|
+
const tail = CLOSER_TAIL.exec(rest);
|
|
229
|
+
const opener = !tail && isKnownOpener(rest);
|
|
230
|
+
if (!tail && !opener)
|
|
231
|
+
continue;
|
|
232
|
+
// a `:::` in a code example is code
|
|
233
|
+
if ((code ??= codeLines(state)).has(nextLine)) {
|
|
234
|
+
if (tail && colons >= markerCount)
|
|
235
|
+
scan.skippedCloser ??= nextLine;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (opener) {
|
|
239
|
+
if (notOpeners.has(nextLine))
|
|
240
|
+
continue;
|
|
241
|
+
const least = Math.min(colons, inner.length > 0 ? inner[inner.length - 1].least : colons);
|
|
242
|
+
inner.push({ colons, indent: state.sCount[nextLine], least });
|
|
243
|
+
openCount.set(colons, (openCount.get(colons) ?? 0) + 1);
|
|
244
|
+
scan.pushed.push(nextLine);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Where the rule before the stack (the first closer with enough colons) would have ended this block
|
|
248
|
+
if (colons >= markerCount)
|
|
249
|
+
scan.legacyCloser ??= nextLine;
|
|
250
|
+
// The innermost open block opened with exactly this many colons, else the innermost opened
|
|
251
|
+
// with fewer; it closes, and any block still open inside it. Index -1 is this block.
|
|
252
|
+
const closes = (fits) => {
|
|
253
|
+
for (let k = inner.length - 1; k >= 0; k--)
|
|
254
|
+
if (fits(inner[k].colons))
|
|
255
|
+
return k;
|
|
256
|
+
return fits(markerCount) ? -1 : undefined;
|
|
257
|
+
};
|
|
258
|
+
const exact = (openCount.get(colons) ?? 0) > 0 || markerCount === colons;
|
|
259
|
+
const fewer = markerCount < colons || (inner.length > 0 && inner[inner.length - 1].least < colons);
|
|
260
|
+
const k = exact ? closes((n) => n === colons) : fewer ? closes((n) => n < colons) : undefined;
|
|
261
|
+
// shorter than every open block: it closes nothing here
|
|
262
|
+
if (k === undefined)
|
|
263
|
+
continue;
|
|
264
|
+
if (k >= 0) {
|
|
265
|
+
truncate(k);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
scan.autoClosed = true;
|
|
269
|
+
scan.closeMarkup = state.src.slice(start, pos);
|
|
270
|
+
scan.closeComment = tail?.[1] ?? "";
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
scan.nextLine = nextLine;
|
|
274
|
+
return scan;
|
|
275
|
+
}
|
|
276
|
+
md.block.ruler.before("fence", `container_${name}`, container, { alt: ["paragraph", "reference", "blockquote", "list"] });
|
|
277
|
+
const render = options.render ?? (() => "");
|
|
278
|
+
md.renderer.rules[`container_${name}_open`] = render;
|
|
279
|
+
md.renderer.rules[`container_${name}_close`] = render;
|
|
280
|
+
}
|