@ai-matrx/print 0.6.2 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ - **Added: `@ai-matrx/print/document` — print-grade documents from markdown.**
6
+ One parsed tree (CommonMark + GFM via micromark/mdast) renders to **PDF**
7
+ (real text PDF via pdfmake), **DOCX** (docx), **EPUB 3** and a standalone
8
+ **HTML** file, so every format carries the same layout, numbering and
9
+ references. `exportDocument(markdown, "pdf" | "docx" | "epub" | "html" |
10
+ "markdown")` is the one call.
11
+ - **Document settings in YAML frontmatter** (pandoc names, any spelling):
12
+ page size (letter, legal, tabloid, executive, A3–A5, B5), margins (`1in`,
13
+ `2.5cm`, pandoc `geometry`), orientation, cover page (title, subtitle,
14
+ author, date, logo), running header/footer templates with `{page}` /
15
+ `{pages}` / `{title}` / `{date}` ("Page X of Y"), a different first page,
16
+ 1–3 columns, section numbering, table of contents (depth, title), citation
17
+ style, references, font size, hyphenation.
18
+ - **Section breaks** — `<!-- section landscape columns=2 -->` starts a new
19
+ page and switches orientation/columns from there on (Word's "section break,
20
+ next page"). Added to `/directives` (`parseSectionBreakLine`,
21
+ `isBreakLine`, `SECTION_BREAK_MARKDOWN`); `splitAtPageBreaks` and both
22
+ markdown skins treat a section break as a page break.
23
+ - **Table of contents** — automatic, with real page numbers in PDF, a
24
+ pre-filled Word TOC field (page numbers from the PDF layout; Word refreshes
25
+ on open), the EPUB navigation document, and links in HTML. Placement:
26
+ `toc: true` or a `[[toc]]` line.
27
+ - **Captions, numbering, cross-references** (pandoc-crossref grammar):
28
+ `![Caption](url){#fig:id}`, `Table: Caption {#tbl:id}`, `## Title
29
+ {#sec:id}`, and `@fig:id` / `@tbl:id` / `@sec:id` → "Figure 2", "Table 1",
30
+ "Section 3" (linked; Word gets SEQ fields + bookmarks).
31
+ - **Citations and bibliography** — pandoc citation syntax (`[@key, p. 4]`,
32
+ `[@a; @b]`, `[-@key]`, bare `@key`) over CSL-JSON references in the
33
+ frontmatter, formatted APA 7, MLA 9 or Chicago author-date, with the
34
+ reference list appended (or placed at `<!-- bibliography -->`).
35
+ Footnotes print as endnotes.
36
+ - **Print quality** — keep-with-next headings (a heading is never the last
37
+ line on a page), short paragraphs never split, table rows never split and
38
+ header rows repeat, code/figures/tables kept whole when they fit, widow and
39
+ orphan control, hyphenation (HTML/EPUB CSS, Word auto-hyphenation), always
40
+ light.
41
+ - **Added: `DocumentPrintPreview`** in `/react` — the real PDF pages, a web
42
+ view with a clickable TOC, Print, and one-click PDF / Word / EPUB / HTML /
43
+ Markdown downloads; unresolved references and missing images are listed
44
+ with their fix.
45
+ - Nothing fails silently: unresolved cross-references/citations, unusable
46
+ settings, dropped raw HTML and unavailable images are reported as notices
47
+ with a remedy.
48
+ - Consumer action: none required. New dependencies (`pdfmake`, `docx`,
49
+ `jszip`, `yaml`, mdast/micromark) load only when a document is exported.
50
+
3
51
  ## 0.6.2
4
52
 
5
53
  - **Fixed:** a page-break directive indented four or more columns is code (as
package/README.md CHANGED
@@ -195,3 +195,41 @@ drawer are bundled dependencies.
195
195
  ## License
196
196
 
197
197
  MIT
198
+
199
+ ### `@ai-matrx/print/document` — print-grade documents (PDF, Word, EPUB, HTML)
200
+
201
+ ```ts
202
+ import { exportDocument, downloadDocumentExport } from "@ai-matrx/print/document";
203
+
204
+ const exp = await exportDocument(markdown, "docx"); // "pdf" | "docx" | "epub" | "html" | "markdown"
205
+ downloadDocumentExport(exp); // browser: saves "<title>.docx"
206
+ ```
207
+
208
+ Settings ride in the markdown's frontmatter:
209
+
210
+ ```yaml
211
+ ---
212
+ title: Quarterly Review
213
+ subtitle: Operations
214
+ author: Jane Park
215
+ date: today
216
+ cover: true
217
+ toc: true # or { depth: 2, title: Contents }
218
+ page: { size: a4, orientation: portrait, margins: 1in }
219
+ header: "{title} | | {date}" # left | center | right
220
+ footer: "Page {page} of {pages}"
221
+ columns: 1
222
+ numberSections: true
223
+ csl: apa # apa | mla | chicago
224
+ references:
225
+ - { id: smith2020, title: Paper Cuts, author: "Smith, Jane", issued: 2020, publisher: Acme }
226
+ ---
227
+ ```
228
+
229
+ In the body: `<!-- pagebreak -->`, `<!-- section landscape columns=2 -->`,
230
+ `[[toc]]`, `![Caption](url){#fig:chart}`, `Table: Caption {#tbl:sales}`,
231
+ `## Methods {#sec:methods}`, `see @fig:chart`, `[@smith2020, p. 4]`.
232
+ All four formats come from one parsed tree (`parseDocument`), so they agree.
233
+ `DocumentPrintPreview` (in `/react`) shows the real PDF pages with Print and
234
+ every download.
235
+
@@ -20,10 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/directives.ts
21
21
  var directives_exports = {};
22
22
  __export(directives_exports, {
23
+ BIBLIOGRAPHY_MARKDOWN: () => BIBLIOGRAPHY_MARKDOWN,
23
24
  PAGE_BREAK_CLASS: () => PAGE_BREAK_CLASS,
24
25
  PAGE_BREAK_LABEL: () => PAGE_BREAK_LABEL,
25
26
  PAGE_BREAK_MARKDOWN: () => PAGE_BREAK_MARKDOWN,
27
+ SECTION_BREAK_MARKDOWN: () => SECTION_BREAK_MARKDOWN,
28
+ TOC_MARKDOWN: () => TOC_MARKDOWN,
29
+ isBibliographyLine: () => isBibliographyLine,
30
+ isBreakLine: () => isBreakLine,
26
31
  isPageBreakLine: () => isPageBreakLine,
32
+ isTocLine: () => isTocLine,
33
+ parseSectionBreakLine: () => parseSectionBreakLine,
27
34
  splitAtPageBreaks: () => splitAtPageBreaks
28
35
  });
29
36
  module.exports = __toCommonJS(directives_exports);
@@ -52,6 +59,36 @@ function isPageBreakLine(line) {
52
59
  if (!div) return false;
53
60
  return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? "");
54
61
  }
62
+ var SECTION_BREAK_MARKDOWN = "<!-- section -->";
63
+ var SECTION_FORM = /^<!--\s*section(?:[\s_-]?break)?\b\s*:?\s*([^>]*?)\s*-->$/i;
64
+ function parseSectionBreakLine(line) {
65
+ if (indentColumns(line) >= 4) return null;
66
+ const m = SECTION_FORM.exec(line.trim());
67
+ if (!m) return null;
68
+ const words = (m[1] ?? "").toLowerCase();
69
+ const out = {};
70
+ if (/\blandscape\b/.test(words)) out.orientation = "landscape";
71
+ else if (/\bportrait\b/.test(words)) out.orientation = "portrait";
72
+ const cols = /\b(?:columns?|cols?)\s*[=:]\s*(\d)\b/.exec(words) ?? /\b(\d)\s*[-\s]?columns?\b/.exec(words);
73
+ if (cols) {
74
+ const n = Math.min(3, Math.max(1, Number(cols[1])));
75
+ out.columns = n;
76
+ }
77
+ return out;
78
+ }
79
+ var TOC_MARKDOWN = "[[toc]]";
80
+ var BIBLIOGRAPHY_MARKDOWN = "<!-- bibliography -->";
81
+ var TOC_FORM = /^(?:\[\[\s*toc\s*\]\]|\[toc\]|\$\{toc\}|<!--\s*toc\s*-->|\\tableofcontents)$/i;
82
+ var BIBLIOGRAPHY_FORM = /^(?:\[\[\s*(?:bibliography|references)\s*\]\]|<!--\s*(?:bibliography|references)\s*-->|\\printbibliography|\\bibliography\{[^}]*\})$/i;
83
+ function isTocLine(line) {
84
+ return indentColumns(line) < 4 && TOC_FORM.test(line.trim());
85
+ }
86
+ function isBibliographyLine(line) {
87
+ return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());
88
+ }
89
+ function isBreakLine(line) {
90
+ return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;
91
+ }
55
92
  function splitAtPageBreaks(markdown) {
56
93
  const lines = markdown.split(/\r?\n/);
57
94
  const pieces = [];
@@ -63,7 +100,7 @@ function splitAtPageBreaks(markdown) {
63
100
  const marker = fenceMatch[1];
64
101
  if (fence === null) fence = marker;
65
102
  else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;
66
- } else if (fence === null && isPageBreakLine(line)) {
103
+ } else if (fence === null && isBreakLine(line)) {
67
104
  if (current.join("\n").trim()) pieces.push(current.join("\n"));
68
105
  current = [];
69
106
  continue;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/directives.ts"],"sourcesContent":["/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isPageBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BO,IAAM,sBAAsB;AAG5B,IAAM,mBAAmB;AAGzB,IAAM,mBAAmB;AAEhC,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,WAAW;AACjB,IAAM,iBACJ;AAGF,SAAS,cAAc,MAAsB;AAC3C,MAAI,OAAO;AACX,aAAW,MAAM,MAAM;AACrB,QAAI,OAAO,OAAO,OAAO,OAAU,SAAQ;AAAA,aAClC,OAAO,IAAM,SAAQ;AAAA,QACzB;AAAA,EACP;AACA,SAAO;AACT;AAMO,SAAS,gBAAgB,MAAuB;AACrD,MAAI,cAAc,IAAI,KAAK,EAAG,QAAO;AACrC,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,IAAK,QAAO;AAC3C,MAAI,aAAa,KAAK,CAAC,KAAK,WAAW,KAAK,CAAC,EAAG,QAAO;AACvD,QAAM,MAAM,SAAS,KAAK,CAAC;AAC3B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,eAAe,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE;AACnD;AAQO,SAAS,kBAAkB,UAA4B;AAC5D,QAAM,QAAQ,SAAS,MAAM,OAAO;AACpC,QAAM,SAAmB,CAAC;AAC1B,MAAI,UAAoB,CAAC;AACzB,MAAI,QAAuB;AAE3B,aAAW,QAAQ,OAAO;AAExB,UAAM,aAAa,oBAAoB,KAAK,IAAI;AAChD,QAAI,YAAY;AACd,YAAM,SAAS,WAAW,CAAC;AAC3B,UAAI,UAAU,KAAM,SAAQ;AAAA,eACnB,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,OAAO,UAAU,MAAM,OAAQ,SAAQ;AAAA,IAC5E,WAAW,UAAU,QAAQ,gBAAgB,IAAI,GAAG;AAClD,UAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AAC7D,gBAAU,CAAC;AACX;AAAA,IACF;AACA,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,MAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,KAAK,OAAO,WAAW,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AACpF,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/directives.ts"],"sourcesContent":["/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n"],"mappings":"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","names":[]}
@@ -35,6 +35,22 @@ declare const PAGE_BREAK_LABEL = "Page break";
35
35
  * it is not — in markdown that is a code block, and every surface must agree.
36
36
  */
37
37
  declare function isPageBreakLine(line: string): boolean;
38
+ /** The form editors insert for a plain section break. */
39
+ declare const SECTION_BREAK_MARKDOWN = "<!-- section -->";
40
+ interface SectionBreakDirective {
41
+ /** Absent = inherit the document's orientation. */
42
+ orientation?: "portrait" | "landscape";
43
+ /** Absent = inherit the document's column count. 1–3. */
44
+ columns?: 1 | 2 | 3;
45
+ }
46
+ /** Parse a section-break line; `null` when the line is not one. */
47
+ declare function parseSectionBreakLine(line: string): SectionBreakDirective | null;
48
+ declare const TOC_MARKDOWN = "[[toc]]";
49
+ declare const BIBLIOGRAPHY_MARKDOWN = "<!-- bibliography -->";
50
+ declare function isTocLine(line: string): boolean;
51
+ declare function isBibliographyLine(line: string): boolean;
52
+ /** Any directive that ends the current printed page (page or section break). */
53
+ declare function isBreakLine(line: string): boolean;
38
54
  /**
39
55
  * Split markdown into the pieces that belong on separate pages. Fence-aware:
40
56
  * a directive inside a ``` / ~~~ code block is code, not a break. Always
@@ -43,4 +59,4 @@ declare function isPageBreakLine(line: string): boolean;
43
59
  */
44
60
  declare function splitAtPageBreaks(markdown: string): string[];
45
61
 
46
- export { PAGE_BREAK_CLASS, PAGE_BREAK_LABEL, PAGE_BREAK_MARKDOWN, isPageBreakLine, splitAtPageBreaks };
62
+ export { BIBLIOGRAPHY_MARKDOWN, PAGE_BREAK_CLASS, PAGE_BREAK_LABEL, PAGE_BREAK_MARKDOWN, SECTION_BREAK_MARKDOWN, type SectionBreakDirective, TOC_MARKDOWN, isBibliographyLine, isBreakLine, isPageBreakLine, isTocLine, parseSectionBreakLine, splitAtPageBreaks };
@@ -35,6 +35,22 @@ declare const PAGE_BREAK_LABEL = "Page break";
35
35
  * it is not — in markdown that is a code block, and every surface must agree.
36
36
  */
37
37
  declare function isPageBreakLine(line: string): boolean;
38
+ /** The form editors insert for a plain section break. */
39
+ declare const SECTION_BREAK_MARKDOWN = "<!-- section -->";
40
+ interface SectionBreakDirective {
41
+ /** Absent = inherit the document's orientation. */
42
+ orientation?: "portrait" | "landscape";
43
+ /** Absent = inherit the document's column count. 1–3. */
44
+ columns?: 1 | 2 | 3;
45
+ }
46
+ /** Parse a section-break line; `null` when the line is not one. */
47
+ declare function parseSectionBreakLine(line: string): SectionBreakDirective | null;
48
+ declare const TOC_MARKDOWN = "[[toc]]";
49
+ declare const BIBLIOGRAPHY_MARKDOWN = "<!-- bibliography -->";
50
+ declare function isTocLine(line: string): boolean;
51
+ declare function isBibliographyLine(line: string): boolean;
52
+ /** Any directive that ends the current printed page (page or section break). */
53
+ declare function isBreakLine(line: string): boolean;
38
54
  /**
39
55
  * Split markdown into the pieces that belong on separate pages. Fence-aware:
40
56
  * a directive inside a ``` / ~~~ code block is code, not a break. Always
@@ -43,4 +59,4 @@ declare function isPageBreakLine(line: string): boolean;
43
59
  */
44
60
  declare function splitAtPageBreaks(markdown: string): string[];
45
61
 
46
- export { PAGE_BREAK_CLASS, PAGE_BREAK_LABEL, PAGE_BREAK_MARKDOWN, isPageBreakLine, splitAtPageBreaks };
62
+ export { BIBLIOGRAPHY_MARKDOWN, PAGE_BREAK_CLASS, PAGE_BREAK_LABEL, PAGE_BREAK_MARKDOWN, SECTION_BREAK_MARKDOWN, type SectionBreakDirective, TOC_MARKDOWN, isBibliographyLine, isBreakLine, isPageBreakLine, isTocLine, parseSectionBreakLine, splitAtPageBreaks };
@@ -24,6 +24,36 @@ function isPageBreakLine(line) {
24
24
  if (!div) return false;
25
25
  return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? "");
26
26
  }
27
+ var SECTION_BREAK_MARKDOWN = "<!-- section -->";
28
+ var SECTION_FORM = /^<!--\s*section(?:[\s_-]?break)?\b\s*:?\s*([^>]*?)\s*-->$/i;
29
+ function parseSectionBreakLine(line) {
30
+ if (indentColumns(line) >= 4) return null;
31
+ const m = SECTION_FORM.exec(line.trim());
32
+ if (!m) return null;
33
+ const words = (m[1] ?? "").toLowerCase();
34
+ const out = {};
35
+ if (/\blandscape\b/.test(words)) out.orientation = "landscape";
36
+ else if (/\bportrait\b/.test(words)) out.orientation = "portrait";
37
+ const cols = /\b(?:columns?|cols?)\s*[=:]\s*(\d)\b/.exec(words) ?? /\b(\d)\s*[-\s]?columns?\b/.exec(words);
38
+ if (cols) {
39
+ const n = Math.min(3, Math.max(1, Number(cols[1])));
40
+ out.columns = n;
41
+ }
42
+ return out;
43
+ }
44
+ var TOC_MARKDOWN = "[[toc]]";
45
+ var BIBLIOGRAPHY_MARKDOWN = "<!-- bibliography -->";
46
+ var TOC_FORM = /^(?:\[\[\s*toc\s*\]\]|\[toc\]|\$\{toc\}|<!--\s*toc\s*-->|\\tableofcontents)$/i;
47
+ var BIBLIOGRAPHY_FORM = /^(?:\[\[\s*(?:bibliography|references)\s*\]\]|<!--\s*(?:bibliography|references)\s*-->|\\printbibliography|\\bibliography\{[^}]*\})$/i;
48
+ function isTocLine(line) {
49
+ return indentColumns(line) < 4 && TOC_FORM.test(line.trim());
50
+ }
51
+ function isBibliographyLine(line) {
52
+ return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());
53
+ }
54
+ function isBreakLine(line) {
55
+ return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;
56
+ }
27
57
  function splitAtPageBreaks(markdown) {
28
58
  const lines = markdown.split(/\r?\n/);
29
59
  const pieces = [];
@@ -35,7 +65,7 @@ function splitAtPageBreaks(markdown) {
35
65
  const marker = fenceMatch[1];
36
66
  if (fence === null) fence = marker;
37
67
  else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;
38
- } else if (fence === null && isPageBreakLine(line)) {
68
+ } else if (fence === null && isBreakLine(line)) {
39
69
  if (current.join("\n").trim()) pieces.push(current.join("\n"));
40
70
  current = [];
41
71
  continue;
@@ -46,10 +76,17 @@ function splitAtPageBreaks(markdown) {
46
76
  return pieces;
47
77
  }
48
78
  export {
79
+ BIBLIOGRAPHY_MARKDOWN,
49
80
  PAGE_BREAK_CLASS,
50
81
  PAGE_BREAK_LABEL,
51
82
  PAGE_BREAK_MARKDOWN,
83
+ SECTION_BREAK_MARKDOWN,
84
+ TOC_MARKDOWN,
85
+ isBibliographyLine,
86
+ isBreakLine,
52
87
  isPageBreakLine,
88
+ isTocLine,
89
+ parseSectionBreakLine,
53
90
  splitAtPageBreaks
54
91
  };
55
92
  //# sourceMappingURL=directives.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/directives.ts"],"sourcesContent":["/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isPageBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n"],"mappings":";AA4BO,IAAM,sBAAsB;AAG5B,IAAM,mBAAmB;AAGzB,IAAM,mBAAmB;AAEhC,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,WAAW;AACjB,IAAM,iBACJ;AAGF,SAAS,cAAc,MAAsB;AAC3C,MAAI,OAAO;AACX,aAAW,MAAM,MAAM;AACrB,QAAI,OAAO,OAAO,OAAO,OAAU,SAAQ;AAAA,aAClC,OAAO,IAAM,SAAQ;AAAA,QACzB;AAAA,EACP;AACA,SAAO;AACT;AAMO,SAAS,gBAAgB,MAAuB;AACrD,MAAI,cAAc,IAAI,KAAK,EAAG,QAAO;AACrC,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,IAAK,QAAO;AAC3C,MAAI,aAAa,KAAK,CAAC,KAAK,WAAW,KAAK,CAAC,EAAG,QAAO;AACvD,QAAM,MAAM,SAAS,KAAK,CAAC;AAC3B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,eAAe,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE;AACnD;AAQO,SAAS,kBAAkB,UAA4B;AAC5D,QAAM,QAAQ,SAAS,MAAM,OAAO;AACpC,QAAM,SAAmB,CAAC;AAC1B,MAAI,UAAoB,CAAC;AACzB,MAAI,QAAuB;AAE3B,aAAW,QAAQ,OAAO;AAExB,UAAM,aAAa,oBAAoB,KAAK,IAAI;AAChD,QAAI,YAAY;AACd,YAAM,SAAS,WAAW,CAAC;AAC3B,UAAI,UAAU,KAAM,SAAQ;AAAA,eACnB,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,OAAO,UAAU,MAAM,OAAQ,SAAQ;AAAA,IAC5E,WAAW,UAAU,QAAQ,gBAAgB,IAAI,GAAG;AAClD,UAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AAC7D,gBAAU,CAAC;AACX;AAAA,IACF;AACA,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,MAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,KAAK,OAAO,WAAW,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AACpF,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/directives.ts"],"sourcesContent":["/**\n * @ai-matrx/print/directives — the ONE definition of the print directives\n * markdown can carry, shared by every surface that reads markdown.\n *\n * Why a separate, zero-dependency entry: the directive syntax has three\n * readers — this package's two converters (print window / PDF / HTML page),\n * the chat renderer's remark plugin (the on-screen preview), and any editor\n * that inserts one. If each re-derived the regex they would drift the first\n * time someone added an alias. `/markdown` carries ~20KB of stylesheet, so the\n * chat bundle imports the syntax from here instead and pays nothing.\n *\n * ── Page break ────────────────────────────────────────────────────────────\n * A line that is exactly one of these (case-insensitive, surrounding\n * whitespace ignored) forces the next content onto a new printed page:\n *\n * <!-- pagebreak --> ← canonical; invisible in any other renderer\n * <!-- page-break --> <!-- page break --> <!-- newpage -->\n * \\pagebreak \\newpage ← LaTeX / pandoc habit\n * <div style=\"page-break-after: always\"></div> ← Typora / VS Code habit\n * (also `page-break-before`, `break-after: page`, `break-before: page`,\n * and `<div class=\"page-break\"></div>` / `class=\"pagebreak\"`)\n *\n * On screen every surface draws the same idea: a dashed rule labelled\n * \"Page break\" with generous padding. In print it is a zero-height element\n * with `break-after: page`.\n */\n\n/** The form editors insert and docs recommend. */\nexport const PAGE_BREAK_MARKDOWN = \"<!-- pagebreak -->\";\n\n/** The class every surface puts on a rendered page break. */\nexport const PAGE_BREAK_CLASS = \"matrx-page-break\";\n\n/** Screen label for a rendered page break. */\nexport const PAGE_BREAK_LABEL = \"Page break\";\n\nconst COMMENT_FORM = /^<!--\\s*(?:page[\\s_-]?break|new[\\s_-]?page)\\s*-->$/i;\nconst LATEX_FORM = /^\\\\(?:pagebreak|newpage|clearpage)\\s*(?:\\{\\s*\\})?$/i;\nconst DIV_FORM = /^<div\\b([^>]*)>\\s*<\\/div>$|^<div\\b([^>]*)\\/>$/i;\nconst DIV_BREAK_ATTR =\n /(?:page-break-(?:after|before)\\s*:\\s*always|break-(?:after|before)\\s*:\\s*page|class\\s*=\\s*[\"'](?:[^\"']*\\s)?page-?break(?:\\s[^\"']*)?[\"'])/i;\n\n/** Columns of leading indentation (tab = 4, NBSP counts as a space). */\nfunction indentColumns(line: string): number {\n let cols = 0;\n for (const ch of line) {\n if (ch === \" \" || ch === \"\\u00A0\") cols += 1;\n else if (ch === \"\\t\") cols += 4;\n else break;\n }\n return cols;\n}\n\n/**\n * Is this single line a page-break directive? Indented four or more columns\n * it is not — in markdown that is a code block, and every surface must agree.\n */\nexport function isPageBreakLine(line: string): boolean {\n if (indentColumns(line) >= 4) return false;\n const t = line.trim();\n if (t.length < 8 || t.length > 200) return false;\n if (COMMENT_FORM.test(t) || LATEX_FORM.test(t)) return true;\n const div = DIV_FORM.exec(t);\n if (!div) return false;\n return DIV_BREAK_ATTR.test(div[1] ?? div[2] ?? \"\");\n}\n\n// ── Section break ─────────────────────────────────────────────────────────\n//\n// A section break is a page break that can also change the page layout for\n// everything after it (Word's \"Section break (next page)\"):\n//\n// <!-- section --> ← new page, same layout\n// <!-- section: landscape --> ← new page, landscape from here on\n// <!-- section landscape columns=2 --> ← landscape AND two columns\n// <!-- section portrait columns=1 --> ← back to the default layout\n//\n// Recognised words: `landscape` / `portrait`, `columns=N` (or `cols=N`,\n// `N columns`, 1–3). Unknown words are ignored, never an error — the break\n// still happens. Every page-break reader treats a section break as a break.\n\n/** The form editors insert for a plain section break. */\nexport const SECTION_BREAK_MARKDOWN = \"<!-- section -->\";\n\nexport interface SectionBreakDirective {\n /** Absent = inherit the document's orientation. */\n orientation?: \"portrait\" | \"landscape\";\n /** Absent = inherit the document's column count. 1–3. */\n columns?: 1 | 2 | 3;\n}\n\nconst SECTION_FORM = /^<!--\\s*section(?:[\\s_-]?break)?\\b\\s*:?\\s*([^>]*?)\\s*-->$/i;\n\n/** Parse a section-break line; `null` when the line is not one. */\nexport function parseSectionBreakLine(line: string): SectionBreakDirective | null {\n if (indentColumns(line) >= 4) return null;\n const m = SECTION_FORM.exec(line.trim());\n if (!m) return null;\n const words = (m[1] ?? \"\").toLowerCase();\n const out: SectionBreakDirective = {};\n if (/\\blandscape\\b/.test(words)) out.orientation = \"landscape\";\n else if (/\\bportrait\\b/.test(words)) out.orientation = \"portrait\";\n const cols = /\\b(?:columns?|cols?)\\s*[=:]\\s*(\\d)\\b/.exec(words) ?? /\\b(\\d)\\s*[-\\s]?columns?\\b/.exec(words);\n if (cols) {\n const n = Math.min(3, Math.max(1, Number(cols[1])));\n out.columns = n as 1 | 2 | 3;\n }\n return out;\n}\n\n// ── Table of contents / bibliography placement ───────────────────────────\n//\n// `[[toc]]`, `[toc]`, `[TOC]`, `${toc}`, `<!-- toc -->`, `\\tableofcontents`\n// put the table of contents at that spot (otherwise frontmatter `toc: true`\n// puts it at the start). `<!-- bibliography -->`, `[[bibliography]]`,\n// `\\printbibliography` put the reference list there (otherwise it closes the\n// document whenever something is cited).\n\nexport const TOC_MARKDOWN = \"[[toc]]\";\nexport const BIBLIOGRAPHY_MARKDOWN = \"<!-- bibliography -->\";\n\nconst TOC_FORM = /^(?:\\[\\[\\s*toc\\s*\\]\\]|\\[toc\\]|\\$\\{toc\\}|<!--\\s*toc\\s*-->|\\\\tableofcontents)$/i;\nconst BIBLIOGRAPHY_FORM =\n /^(?:\\[\\[\\s*(?:bibliography|references)\\s*\\]\\]|<!--\\s*(?:bibliography|references)\\s*-->|\\\\printbibliography|\\\\bibliography\\{[^}]*\\})$/i;\n\nexport function isTocLine(line: string): boolean {\n return indentColumns(line) < 4 && TOC_FORM.test(line.trim());\n}\n\nexport function isBibliographyLine(line: string): boolean {\n return indentColumns(line) < 4 && BIBLIOGRAPHY_FORM.test(line.trim());\n}\n\n/** Any directive that ends the current printed page (page or section break). */\nexport function isBreakLine(line: string): boolean {\n return isPageBreakLine(line) || parseSectionBreakLine(line) !== null;\n}\n\n/**\n * Split markdown into the pieces that belong on separate pages. Fence-aware:\n * a directive inside a ``` / ~~~ code block is code, not a break. Always\n * returns at least one piece; empty pieces (two breaks in a row, a break at\n * the very start or end) are kept out so no blank page is produced.\n */\nexport function splitAtPageBreaks(markdown: string): string[] {\n const lines = markdown.split(/\\r?\\n/);\n const pieces: string[] = [];\n let current: string[] = [];\n let fence: string | null = null;\n\n for (const line of lines) {\n // Any indent — the converters accept list-nested fences, so must this.\n const fenceMatch = /^\\s*(`{3,}|~{3,})/.exec(line);\n if (fenceMatch) {\n const marker = fenceMatch[1] as string;\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length) fence = null;\n } else if (fence === null && isBreakLine(line)) {\n if (current.join(\"\\n\").trim()) pieces.push(current.join(\"\\n\"));\n current = [];\n continue;\n }\n current.push(line);\n }\n if (current.join(\"\\n\").trim() || pieces.length === 0) pieces.push(current.join(\"\\n\"));\n return pieces;\n}\n"],"mappings":";AA4BO,IAAM,sBAAsB;AAG5B,IAAM,mBAAmB;AAGzB,IAAM,mBAAmB;AAEhC,IAAM,eAAe;AACrB,IAAM,aAAa;AACnB,IAAM,WAAW;AACjB,IAAM,iBACJ;AAGF,SAAS,cAAc,MAAsB;AAC3C,MAAI,OAAO;AACX,aAAW,MAAM,MAAM;AACrB,QAAI,OAAO,OAAO,OAAO,OAAU,SAAQ;AAAA,aAClC,OAAO,IAAM,SAAQ;AAAA,QACzB;AAAA,EACP;AACA,SAAO;AACT;AAMO,SAAS,gBAAgB,MAAuB;AACrD,MAAI,cAAc,IAAI,KAAK,EAAG,QAAO;AACrC,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,EAAE,SAAS,KAAK,EAAE,SAAS,IAAK,QAAO;AAC3C,MAAI,aAAa,KAAK,CAAC,KAAK,WAAW,KAAK,CAAC,EAAG,QAAO;AACvD,QAAM,MAAM,SAAS,KAAK,CAAC;AAC3B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,eAAe,KAAK,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,EAAE;AACnD;AAiBO,IAAM,yBAAyB;AAStC,IAAM,eAAe;AAGd,SAAS,sBAAsB,MAA4C;AAChF,MAAI,cAAc,IAAI,KAAK,EAAG,QAAO;AACrC,QAAM,IAAI,aAAa,KAAK,KAAK,KAAK,CAAC;AACvC,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,SAAS,EAAE,CAAC,KAAK,IAAI,YAAY;AACvC,QAAM,MAA6B,CAAC;AACpC,MAAI,gBAAgB,KAAK,KAAK,EAAG,KAAI,cAAc;AAAA,WAC1C,eAAe,KAAK,KAAK,EAAG,KAAI,cAAc;AACvD,QAAM,OAAO,uCAAuC,KAAK,KAAK,KAAK,4BAA4B,KAAK,KAAK;AACzG,MAAI,MAAM;AACR,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;AAClD,QAAI,UAAU;AAAA,EAChB;AACA,SAAO;AACT;AAUO,IAAM,eAAe;AACrB,IAAM,wBAAwB;AAErC,IAAM,WAAW;AACjB,IAAM,oBACJ;AAEK,SAAS,UAAU,MAAuB;AAC/C,SAAO,cAAc,IAAI,IAAI,KAAK,SAAS,KAAK,KAAK,KAAK,CAAC;AAC7D;AAEO,SAAS,mBAAmB,MAAuB;AACxD,SAAO,cAAc,IAAI,IAAI,KAAK,kBAAkB,KAAK,KAAK,KAAK,CAAC;AACtE;AAGO,SAAS,YAAY,MAAuB;AACjD,SAAO,gBAAgB,IAAI,KAAK,sBAAsB,IAAI,MAAM;AAClE;AAQO,SAAS,kBAAkB,UAA4B;AAC5D,QAAM,QAAQ,SAAS,MAAM,OAAO;AACpC,QAAM,SAAmB,CAAC;AAC1B,MAAI,UAAoB,CAAC;AACzB,MAAI,QAAuB;AAE3B,aAAW,QAAQ,OAAO;AAExB,UAAM,aAAa,oBAAoB,KAAK,IAAI;AAChD,QAAI,YAAY;AACd,YAAM,SAAS,WAAW,CAAC;AAC3B,UAAI,UAAU,KAAM,SAAQ;AAAA,eACnB,OAAO,CAAC,MAAM,MAAM,CAAC,KAAK,OAAO,UAAU,MAAM,OAAQ,SAAQ;AAAA,IAC5E,WAAW,UAAU,QAAQ,YAAY,IAAI,GAAG;AAC9C,UAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AAC7D,gBAAU,CAAC;AACX;AAAA,IACF;AACA,YAAQ,KAAK,IAAI;AAAA,EACnB;AACA,MAAI,QAAQ,KAAK,IAAI,EAAE,KAAK,KAAK,OAAO,WAAW,EAAG,QAAO,KAAK,QAAQ,KAAK,IAAI,CAAC;AACpF,SAAO;AACT;","names":[]}