@kadal/prettier-plugin 0.0.0-stage → 0.4.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/README.md CHANGED
@@ -1,3 +1,64 @@
1
- # Temporary Holding Version
1
+ # @kadal/prettier-plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```sh
4
+ npm install --save-dev prettier @kadal/prettier-plugin
5
+ ```
6
+
7
+ Part of [Kadal Deck](https://deck.kadal.cc/docs/). Prettier for decks: `deck.mdx` (and `slides.mdx`) formatted the way the engine reads them.
8
+
9
+ ```json
10
+ // .prettierrc
11
+ { "plugins": ["@kadal/prettier-plugin"] }
12
+ ```
13
+
14
+ That is all: with the plugin loaded, a file named `deck.mdx` or `slides.mdx` is formatted as a deck. Other deck files ask for it by parser:
15
+
16
+ ```json
17
+ {
18
+ "plugins": ["@kadal/prettier-plugin"],
19
+ "overrides": [{ "files": ["slides/*.mdx", "talks/**/*.mdx"], "options": { "parser": "kadal-deck" } }]
20
+ }
21
+ ```
22
+
23
+ `kadal-deck format` (in `@kadal/deck`) runs the same formatter without any setup.
24
+
25
+ ## What it does
26
+
27
+ - **Separators**: exactly `---` between slides and `----` for a slide beneath (a vertical stack), with one blank line around each. Pieces the engine skips (nothing in them) go.
28
+ - **Headmatter and frontmatter**: the YAML through Prettier's YAML printer. Comments stay, keys stay in the order you wrote them, and the values are checked to be unchanged. A ```` ```yaml ```` block frontmatter stays a block.
29
+ - **Slide bodies**: Prettier's Markdown printer (`-` bullets, `_emphasis_`, aligned tables, one blank line between blocks). Left exactly as written:
30
+ - code fences (with their `{2|3}` steps, `{monaco}`, titles), magic-move blocks
31
+ - `<Notes>` and the HTML-comment notes at the end of a slide
32
+ - `<style>` blocks
33
+ - `$$ … $$` maths
34
+ - `::right::` slot markers, `::code-group` … `::`, `<<< @/snippet` imports
35
+ - JSX and expressions
36
+ - A thematic break is written `***`, since `---` would start a new slide.
37
+
38
+ ## Safety
39
+
40
+ Prettier's Markdown printer reads MDX the way MDX 1 did; the engine compiles MDX 3. So every formatted slide is parsed again (CommonMark, and MDX 3 when the slide is valid MDX 3) and compared with the original as a syntax tree. A slide whose meaning would change keeps its text as written. The whole deck is then cut again and checked to have the same slides, stacked the same way, with the same frontmatter. If it does not, the formatter throws instead of writing.
41
+
42
+ A slide containing `prettier-ignore` is left alone.
43
+
44
+ ## Options
45
+
46
+ | Option | Default | |
47
+ |---|---|---|
48
+ | `kadalDeckBodies` | `true` | `false` formats the separators and the frontmatter only |
49
+
50
+ `printWidth`, `tabWidth`, `useTabs`, `proseWrap`, `singleQuote`, `bracketSpacing` and `endOfLine` reach the YAML and Markdown printers as usual.
51
+
52
+ ## API
53
+
54
+ ```ts
55
+ import * as prettier from "prettier";
56
+ import { formatDeck } from "@kadal/prettier-plugin";
57
+
58
+ const { text, changed, kept } = await formatDeck(source, prettier, { proseWrap: "preserve" });
59
+ // kept: [{ slide, line, reason }] for each part left as written, and why
60
+ ```
61
+
62
+ ## Licence
63
+
64
+ AGPL-3.0-or-later, like the engine (see [LICENSING.md](https://github.com/kadalcc/deck/blob/main/LICENSING.md)).
@@ -0,0 +1,68 @@
1
+ //#region src/format.d.ts
2
+ /**
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * FORMATTING A DECK
5
+ * ─────────────────────────────────────────────────────────────────────────────
6
+ *
7
+ * A deck is cut the engine's way (split.ts) and put back together with one spelling:
8
+ *
9
+ * --- the headmatter: fenced YAML, nothing above it
10
+ * title: … YAML through Prettier's YAML printer — comments kept, keys in the
11
+ * --- order they were written, the values unchanged (checked)
12
+ *
13
+ * # The first slide each body through Prettier's Markdown (MDX) printer, with code,
14
+ * notes, magic-move, `<style>`, `::slot::` and the other sugar left
15
+ * --- byte for byte (protect.ts), and the result checked to mean the
16
+ * layout: center same (guard.ts)
17
+ * --- a separator: exactly `---`, or `----` for a slide beneath
18
+ * one blank line around every separator and after every frontmatter
19
+ * # The next slide one newline at the end of the file
20
+ *
21
+ * A ```yaml block frontmatter stays a block (its YAML is formatted the same way). A piece the
22
+ * engine skips — nothing in it, no frontmatter — goes. A thematic break Prettier would print as
23
+ * `---` (which would start a slide) is written `***`.
24
+ *
25
+ * Then the whole result is cut again and compared with the original: the same slides, stacked
26
+ * the same way, with the same frontmatter values. If not, nothing is returned but an error — a
27
+ * formatter that changes the deck is worse than none.
28
+ * ─────────────────────────────────────────────────────────────────────────────
29
+ */
30
+ /** The part of Prettier this needs: `format`, so a caller can bring its own copy. */
31
+ export interface PrettierLike {
32
+ format(source: string, options: Record<string, unknown>): Promise<string>;
33
+ }
34
+ /** The Prettier options that reach the YAML and Markdown printers. */
35
+ export interface DeckFormatOptions {
36
+ printWidth?: number;
37
+ tabWidth?: number;
38
+ useTabs?: boolean;
39
+ proseWrap?: "always" | "never" | "preserve";
40
+ singleQuote?: boolean;
41
+ bracketSpacing?: boolean;
42
+ /** false: separators and frontmatter only; every body stays as written. Default true. */
43
+ bodies?: boolean;
44
+ }
45
+ export interface SlideReport {
46
+ /** 0-based slide index (as the engine counts: skipped pieces do not count). */
47
+ slide: number;
48
+ /** 1-based line of the slide in the original text. */
49
+ line: number;
50
+ /** Why the body (or the frontmatter) was left as written. */
51
+ reason: string;
52
+ }
53
+ export interface FormatResult {
54
+ text: string;
55
+ changed: boolean;
56
+ /** Slides some part of which was deliberately left as written, and why. */
57
+ kept: SlideReport[];
58
+ }
59
+ export declare class DeckFormatError extends Error {
60
+ name: string;
61
+ }
62
+ /**
63
+ * Format a deck's text. `prettier` is Prettier itself (or anything with its `format`). Throws a
64
+ * `DeckFormatError` when the result would not be the same deck — which the checks make a bug
65
+ * report, never a silent change.
66
+ */
67
+ export declare function formatDeck(source: string, prettier: PrettierLike, options?: DeckFormatOptions): Promise<FormatResult>;
68
+ //#endregion
package/dist/format.js ADDED
@@ -0,0 +1,197 @@
1
+ import { mask, unmask } from "./protect.js";
2
+ import { sameMeaning } from "./guard.js";
3
+ import { cutDeck, isSlide, looksLikeYaml } from "./split.js";
4
+ import YAML from "yaml";
5
+ //#region src/format.ts
6
+ var DeckFormatError = class extends Error {
7
+ name = "DeckFormatError";
8
+ };
9
+ const PASS_THROUGH = [
10
+ "printWidth",
11
+ "tabWidth",
12
+ "useTabs",
13
+ "proseWrap",
14
+ "singleQuote",
15
+ "bracketSpacing"
16
+ ];
17
+ function innerOptions(options) {
18
+ const out = { endOfLine: "lf" };
19
+ for (const k of PASS_THROUGH) if (options[k] !== void 0) out[k] = options[k];
20
+ return out;
21
+ }
22
+ /** Leading and trailing blank lines off; trailing spaces off every line outside protected text. */
23
+ const trimLines = (lines) => {
24
+ let a = 0;
25
+ let b = lines.length;
26
+ while (a < b && !lines[a].trim()) a++;
27
+ while (b > a && !lines[b - 1].trim()) b--;
28
+ return lines.slice(a, b);
29
+ };
30
+ async function formatYaml(yaml, kind, prettier, options) {
31
+ const asWritten = trimLines(yaml.split("\n")).join("\n");
32
+ let before;
33
+ try {
34
+ before = YAML.parse(yaml);
35
+ } catch {
36
+ return {
37
+ yaml: asWritten,
38
+ reason: "the frontmatter is not valid YAML"
39
+ };
40
+ }
41
+ let out;
42
+ try {
43
+ out = (await prettier.format(yaml, {
44
+ ...innerOptions(options),
45
+ parser: "yaml"
46
+ })).replace(/\n+$/, "");
47
+ } catch {
48
+ return {
49
+ yaml: asWritten,
50
+ reason: "Prettier could not format the frontmatter"
51
+ };
52
+ }
53
+ let after;
54
+ try {
55
+ after = YAML.parse(out);
56
+ } catch {
57
+ return {
58
+ yaml: asWritten,
59
+ reason: "the formatted frontmatter did not parse"
60
+ };
61
+ }
62
+ if (JSON.stringify(before) !== JSON.stringify(after)) return {
63
+ yaml: asWritten,
64
+ reason: "formatting would change the frontmatter's values"
65
+ };
66
+ const lines = out.split("\n");
67
+ if (kind === "fence" && (!looksLikeYaml(lines) || lines.some((l) => /^-{3,}\s*$/.test(l)))) return {
68
+ yaml: asWritten,
69
+ reason: "the formatted frontmatter would not read as frontmatter"
70
+ };
71
+ return {
72
+ yaml: out,
73
+ reason: null
74
+ };
75
+ }
76
+ /** A byte-order mark, kept if the file had one. */
77
+ const BOM = String.fromCharCode(65279);
78
+ let nonceCounter = 0;
79
+ const newNonce = () => `${Date.now().toString(36)}${(nonceCounter++).toString(36)}`;
80
+ async function formatBody(body, prettier, options) {
81
+ const asWritten = trimLines(body.split("\n")).join("\n");
82
+ if (!asWritten) return {
83
+ body: "",
84
+ reason: null
85
+ };
86
+ if (options.bodies === false) return {
87
+ body: asWritten,
88
+ reason: null
89
+ };
90
+ if (asWritten.includes("prettier-ignore")) return {
91
+ body: asWritten,
92
+ reason: "it asks Prettier to leave it (prettier-ignore)"
93
+ };
94
+ const masked = mask(asWritten, newNonce());
95
+ let out;
96
+ try {
97
+ out = await prettier.format(masked.text, {
98
+ ...innerOptions(options),
99
+ parser: "mdx",
100
+ embeddedLanguageFormatting: "off"
101
+ });
102
+ } catch (e) {
103
+ return {
104
+ body: asWritten,
105
+ reason: `Prettier could not parse it (${(e instanceof Error ? e.message : String(e)).split("\n")[0]})`
106
+ };
107
+ }
108
+ out = out.split("\n").map((l) => /^-{3,}\s*$/.test(l) ? "***" : l).join("\n").replace(/\n+$/, "");
109
+ const why = sameMeaning(masked.text, out, masked.nonce);
110
+ if (why) return {
111
+ body: asWritten,
112
+ reason: why
113
+ };
114
+ const restored = unmask(out, masked);
115
+ if (restored === null) return {
116
+ body: asWritten,
117
+ reason: "a protected block could not be put back"
118
+ };
119
+ return {
120
+ body: trimLines(restored.split("\n")).join("\n"),
121
+ reason: null
122
+ };
123
+ }
124
+ /** The engine's view of a deck, for the before/after check: slides, stacking, frontmatter. */
125
+ function shape(pieces) {
126
+ const out = [];
127
+ pieces.forEach((p, i) => {
128
+ let fm = {};
129
+ try {
130
+ fm = p.frontmatter ? YAML.parse(p.frontmatter.yaml) ?? {} : {};
131
+ } catch {
132
+ fm = "invalid";
133
+ }
134
+ const keys = fm && typeof fm === "object" ? Object.keys(fm).length : 1;
135
+ if (!isSlide(p, i, keys)) return;
136
+ out.push([
137
+ i === 0 ? "head" : p.vertical ? "v" : "h",
138
+ p.frontmatter?.kind ?? null,
139
+ fm
140
+ ]);
141
+ });
142
+ return JSON.stringify(out);
143
+ }
144
+ /**
145
+ * Format a deck's text. `prettier` is Prettier itself (or anything with its `format`). Throws a
146
+ * `DeckFormatError` when the result would not be the same deck — which the checks make a bug
147
+ * report, never a silent change.
148
+ */
149
+ async function formatDeck(source, prettier, options = {}) {
150
+ const bom = source.startsWith(BOM) ? BOM : "";
151
+ const text = (bom ? source.slice(1) : source).replace(/\r\n?/g, "\n");
152
+ const pieces = cutDeck(text);
153
+ const kept = [];
154
+ const parts = [];
155
+ let slide = 0;
156
+ for (const [i, piece] of pieces.entries()) {
157
+ let keys = 0;
158
+ try {
159
+ const v = piece.frontmatter ? YAML.parse(piece.frontmatter.yaml) : null;
160
+ keys = v && typeof v === "object" ? Object.keys(v).length : 0;
161
+ } catch {
162
+ keys = 1;
163
+ }
164
+ const counts = isSlide(piece, i, keys);
165
+ if (!counts && !(i === 0 && piece.frontmatter)) continue;
166
+ const report = (reason) => kept.push({
167
+ slide,
168
+ line: piece.line + 1,
169
+ reason
170
+ });
171
+ const lines = [];
172
+ if (piece.frontmatter) {
173
+ const fm = await formatYaml(piece.frontmatter.yaml, piece.frontmatter.kind, prettier, options);
174
+ if (fm.reason) report(fm.reason);
175
+ const yaml = fm.yaml ? fm.yaml.split("\n") : [];
176
+ if (piece.frontmatter.kind === "fence") lines.push(i === 0 ? "---" : piece.vertical ? "----" : "---", ...yaml, "---");
177
+ else lines.push(piece.vertical ? "----" : "---", "", piece.frontmatter.open.trim(), ...yaml, piece.frontmatter.close.trim());
178
+ } else if (i > 0) lines.push(piece.vertical ? "----" : "---");
179
+ const body = await formatBody(piece.body.join("\n"), prettier, options);
180
+ if (body.reason) report(`the body is left as written: ${body.reason}`);
181
+ if (body.body) {
182
+ if (lines.length) lines.push("");
183
+ lines.push(...body.body.split("\n"));
184
+ }
185
+ parts.push(lines.join("\n"));
186
+ if (counts) slide++;
187
+ }
188
+ const out = `${bom}${parts.join("\n\n")}\n`;
189
+ if (shape(pieces) !== shape(cutDeck(out.slice(bom.length)))) throw new DeckFormatError("Formatting would change the deck's slides or their frontmatter, so nothing was changed. Please report this deck at https://github.com/kadalcc/deck/issues.");
190
+ return {
191
+ text: out,
192
+ changed: out !== source,
193
+ kept
194
+ };
195
+ }
196
+ //#endregion
197
+ export { DeckFormatError, formatDeck };
package/dist/guard.js ADDED
@@ -0,0 +1,68 @@
1
+ import { placeholderPattern } from "./protect.js";
2
+ import remarkGfm from "remark-gfm";
3
+ import remarkMath from "remark-math";
4
+ import remarkMdx from "remark-mdx";
5
+ import remarkParse from "remark-parse";
6
+ import { unified } from "unified";
7
+ //#region src/guard.ts
8
+ const commonmark = unified().use(remarkParse).use(remarkGfm).use(remarkMath);
9
+ const mdx = unified().use(remarkParse).use(remarkMdx).use(remarkGfm).use(remarkMath);
10
+ /** Any value without the positions (and parser data) nested in it: JSX attributes carry both. */
11
+ function bare(v) {
12
+ if (Array.isArray(v)) return v.map(bare);
13
+ if (!v || typeof v !== "object") return v;
14
+ return Object.fromEntries(Object.entries(v).filter(([k]) => k !== "position" && k !== "data").map(([k, x]) => [k, bare(x)]));
15
+ }
16
+ /** A tree as plain data: no positions, whitespace folded in text and HTML. */
17
+ function plain(node) {
18
+ const out = {};
19
+ for (const [k, v] of Object.entries(node)) {
20
+ if (k === "position" || k === "data") continue;
21
+ if (k === "children") continue;
22
+ if (k === "value" && typeof v === "string" && (node.type === "text" || node.type === "html")) out[k] = v.replace(/\s+/g, " ").trim();
23
+ else out[k] = bare(v);
24
+ }
25
+ if (node.children) {
26
+ const kids = [];
27
+ for (const c of node.children) {
28
+ const p = plain(c);
29
+ const last = kids[kids.length - 1];
30
+ if (p.type === "text" && last?.type === "text") last.value = `${last.value ?? ""} ${p.value ?? ""}`.replace(/\s+/g, " ").trim();
31
+ else kids.push(p);
32
+ }
33
+ out.children = kids;
34
+ }
35
+ return out;
36
+ }
37
+ function parse(processor, text) {
38
+ try {
39
+ return JSON.stringify(plain(processor.parse(text)));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ /** Placeholders as MDX comment expressions on lines of their own (blank lines around). */
45
+ function forMdx(text, nonce) {
46
+ const re = placeholderPattern(nonce);
47
+ return text.split("\n").map((line) => {
48
+ const m = re.exec(line);
49
+ return m ? `\n${m[1]}{/* kadal ${m[2]} */}\n` : line;
50
+ }).join("\n");
51
+ }
52
+ /**
53
+ * Whether `after` says what `before` says (both masked with the same nonce). Returns null when it
54
+ * does, or the reason it does not.
55
+ */
56
+ function sameMeaning(before, after, nonce) {
57
+ const a = parse(commonmark, before);
58
+ const b = parse(commonmark, after);
59
+ if (a === null || b === null || a !== b) return "the Markdown would read differently";
60
+ const am = parse(mdx, forMdx(before, nonce));
61
+ if (am === null) return null;
62
+ const bm = parse(mdx, forMdx(after, nonce));
63
+ if (bm === null) return "the formatted MDX would not compile";
64
+ if (am !== bm) return "the MDX would read differently";
65
+ return null;
66
+ }
67
+ //#endregion
68
+ export { sameMeaning };
@@ -0,0 +1,11 @@
1
+ import { DeckFormatError, DeckFormatOptions, FormatResult, PrettierLike, SlideReport, formatDeck } from "./format.js";
2
+ import { Piece, cutDeck } from "./split.js";
3
+ import { Plugin, SupportOptions } from "prettier";
4
+ //#region src/index.d.ts
5
+ export declare const languages: Plugin["languages"];
6
+ export declare const options: SupportOptions;
7
+ export declare const parsers: Plugin["parsers"];
8
+ export declare const printers: Plugin["printers"];
9
+ declare const plugin: Plugin;
10
+ //#endregion
11
+ export { DeckFormatError, type DeckFormatOptions, type FormatResult, type Piece, type PrettierLike, type SlideReport, cutDeck, plugin as default, formatDeck };
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ import { cutDeck } from "./split.js";
2
+ import { DeckFormatError, formatDeck } from "./format.js";
3
+ import * as prettier from "prettier";
4
+ import { utils } from "prettier/doc";
5
+ //#region src/index.ts
6
+ const PARSER = "kadal-deck";
7
+ const languages = [{
8
+ name: "Kadal Deck",
9
+ parsers: [PARSER],
10
+ filenames: ["deck.mdx", "slides.mdx"],
11
+ vscodeLanguageIds: ["kadal-deck"]
12
+ }];
13
+ const options = { kadalDeckBodies: {
14
+ type: "boolean",
15
+ category: "Kadal Deck",
16
+ default: true,
17
+ description: "Format each slide's Markdown too. false: only the separators and the frontmatter."
18
+ } };
19
+ const parser = {
20
+ astFormat: PARSER,
21
+ async parse(text, opts) {
22
+ const o = opts;
23
+ return {
24
+ type: "kadal-deck",
25
+ text: (await formatDeck(text, prettier, {
26
+ printWidth: o.printWidth,
27
+ tabWidth: o.tabWidth,
28
+ useTabs: o.useTabs,
29
+ proseWrap: o.proseWrap,
30
+ singleQuote: o.singleQuote,
31
+ bracketSpacing: o.bracketSpacing,
32
+ bodies: o.kadalDeckBodies !== false
33
+ })).text,
34
+ source: text
35
+ };
36
+ },
37
+ locStart: () => 0,
38
+ locEnd: (node) => node.source.length
39
+ };
40
+ const parsers = { [PARSER]: parser };
41
+ const printer = { print: (path) => utils.replaceEndOfLine(path.node.text) };
42
+ const printers = { [PARSER]: printer };
43
+ const plugin = {
44
+ languages,
45
+ options,
46
+ parsers,
47
+ printers
48
+ };
49
+ //#endregion
50
+ export { DeckFormatError, cutDeck, plugin as default, formatDeck, languages, options, parsers, printers };
@@ -0,0 +1,92 @@
1
+ //#region src/protect.ts
2
+ const FENCE = /^(\s*)(`{3,}|~{3,})/;
3
+ const SINGLE_LINE = [
4
+ /^::[a-z][\w-]*::[ \t]*$/,
5
+ /^[ \t]*:{2,}code-group\b/,
6
+ /^[ \t]*:{2,}[ \t]*$/,
7
+ /^[ \t]*<<<[ \t]+\S/
8
+ ];
9
+ /** The line index that ends a multi-line protected block starting at `i`, or null if `i` starts none. */
10
+ function blockEnd(lines, i) {
11
+ const line = lines[i];
12
+ const fence = FENCE.exec(line);
13
+ if (fence) {
14
+ const mark = fence[2];
15
+ for (let k = i + 1; k < lines.length; k++) {
16
+ const close = /^\s*(`{3,}|~{3,})/.exec(lines[k]);
17
+ if (close && close[1][0] === mark[0] && close[1].length >= mark.length) return k;
18
+ }
19
+ return lines.length - 1;
20
+ }
21
+ const until = (closing, from = i) => {
22
+ for (let k = from; k < lines.length; k++) if (closing.test(lines[k])) return k;
23
+ return lines.length - 1;
24
+ };
25
+ const t = line.trimStart();
26
+ if (t.startsWith("$$")) {
27
+ if (/\$\$\s*$/.test(t.slice(2))) return i;
28
+ return until(/^\s*\$\$\s*$/, i + 1);
29
+ }
30
+ if (/^<Notes(\s|>|$)/.test(t)) return until(/<\/Notes>/);
31
+ if (/^<style(\s|>|$)/i.test(t)) return until(/<\/style\s*>/i);
32
+ if (t.startsWith("<!--")) return until(/-->/);
33
+ if (SINGLE_LINE.some((re) => re.test(line))) return i;
34
+ return null;
35
+ }
36
+ /** Swap every protected block for a placeholder line. */
37
+ function mask(body, nonce) {
38
+ const lines = body.split("\n");
39
+ const out = [];
40
+ const segments = [];
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const end = blockEnd(lines, i);
43
+ if (end === null) {
44
+ out.push(lines[i]);
45
+ continue;
46
+ }
47
+ const indent = /^[ \t]*/.exec(lines[i])[0];
48
+ segments.push({
49
+ lines: lines.slice(i, end + 1),
50
+ indent
51
+ });
52
+ out.push(`${indent}<!--kadal-fmt-${nonce}-${segments.length - 1}-->`);
53
+ i = end;
54
+ }
55
+ return {
56
+ text: out.join("\n"),
57
+ segments,
58
+ nonce
59
+ };
60
+ }
61
+ /** The placeholder lines' pattern for one nonce. */
62
+ function placeholderPattern(nonce) {
63
+ return new RegExp(`^([ \\t]*)<!--kadal-fmt-${nonce}-(\\d+)-->[ \\t]*$`);
64
+ }
65
+ /**
66
+ * Put the protected blocks back, re-indented with their placeholder. Null when a placeholder went
67
+ * missing, appeared twice, or ended up inside a line — then the caller keeps the original.
68
+ */
69
+ function unmask(text, masked) {
70
+ const re = placeholderPattern(masked.nonce);
71
+ const seen = /* @__PURE__ */ new Set();
72
+ const out = [];
73
+ for (const line of text.split("\n")) {
74
+ const m = re.exec(line);
75
+ if (!m) {
76
+ if (line.includes(`<!--kadal-fmt-${masked.nonce}-`)) return null;
77
+ out.push(line);
78
+ continue;
79
+ }
80
+ const n = Number(m[2]);
81
+ const seg = masked.segments[n];
82
+ if (!seg || seen.has(n)) return null;
83
+ seen.add(n);
84
+ const indent = m[1];
85
+ for (const l of seg.lines) if (indent === seg.indent) out.push(l);
86
+ else if (l.startsWith(seg.indent)) out.push(indent + l.slice(seg.indent.length));
87
+ else out.push(l.trim() ? l : "");
88
+ }
89
+ return seen.size === masked.segments.length ? out.join("\n") : null;
90
+ }
91
+ //#endregion
92
+ export { mask, placeholderPattern, unmask };
@@ -0,0 +1,26 @@
1
+ //#region src/split.d.ts
2
+ export interface PieceFrontmatter {
3
+ /** `---` fences (the separator opens it), or a ```yaml block as the body's first thing. */
4
+ kind: "fence" | "block";
5
+ /** The YAML between the fences, as written. */
6
+ yaml: string;
7
+ /** For a block: its opening and closing fence lines as written (```yaml … ```). */
8
+ open: string;
9
+ close: string;
10
+ }
11
+ export interface Piece {
12
+ /** The separator line as written; null for the piece at the top of the file. */
13
+ separator: string | null;
14
+ vertical: boolean;
15
+ frontmatter: PieceFrontmatter | null;
16
+ /** The body's lines (after the frontmatter, or the separator), as written. */
17
+ body: string[];
18
+ /** 0-based line where the piece starts in the text, for messages. */
19
+ line: number;
20
+ }
21
+ /**
22
+ * Cut the text at separators (the engine's `cut`, line for line). The head piece is always first,
23
+ * possibly empty.
24
+ */
25
+ export declare function cutDeck(text: string): Piece[];
26
+ //#endregion