@ai-matrx/print 0.8.7 → 0.8.9

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/markdown.cjs CHANGED
@@ -67,6 +67,104 @@ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "wid
67
67
 
68
68
  // src/markdown.ts
69
69
  var import_html_escape2 = require("@ai-matrx/kit/html-escape");
70
+ var import_source = require("@ai-matrx/content-ir/source");
71
+
72
+ // src/safe-html.ts
73
+ var import_hast_util_from_html = require("hast-util-from-html");
74
+ var import_hast_util_sanitize = require("hast-util-sanitize");
75
+ var import_hast_util_to_html = require("hast-util-to-html");
76
+ var STRIP_WITH_CONTENT = [
77
+ "script",
78
+ "style",
79
+ "noscript",
80
+ "template",
81
+ "iframe",
82
+ "object",
83
+ "embed",
84
+ "textarea",
85
+ "select",
86
+ "head",
87
+ "title"
88
+ ];
89
+ var FORBIDDEN_TAGS = [
90
+ ...STRIP_WITH_CONTENT,
91
+ "form",
92
+ "meta",
93
+ "base",
94
+ "link",
95
+ "frame",
96
+ "frameset",
97
+ "applet",
98
+ "button",
99
+ "svg",
100
+ "math"
101
+ ];
102
+ var baseAttributes = import_hast_util_sanitize.defaultSchema.attributes ?? {};
103
+ var SAFE_HTML_SCHEMA = {
104
+ ...import_hast_util_sanitize.defaultSchema,
105
+ tagNames: (import_hast_util_sanitize.defaultSchema.tagNames ?? []).filter((t) => !FORBIDDEN_TAGS.includes(t)),
106
+ strip: STRIP_WITH_CONTENT,
107
+ // Authored anchors keep their names; the GitHub `user-content-` rewrite
108
+ // would also break `<label for>` → `<input id>` pairs.
109
+ clobberPrefix: "",
110
+ clobber: []
111
+ };
112
+ var PRINT_CLASS = /^(?:(?:matrx|print|code|doc|language)-[\w-]*|inline-code|is-checked)$/;
113
+ function withPrintClasses(defs = []) {
114
+ let found = false;
115
+ const out = defs.map((d) => {
116
+ if (Array.isArray(d) && d[0] === "className") {
117
+ found = true;
118
+ return [...d, PRINT_CLASS];
119
+ }
120
+ return d;
121
+ });
122
+ return found ? out : [...out, ["className", PRINT_CLASS]];
123
+ }
124
+ var printAttributes = Object.fromEntries(
125
+ Object.entries(baseAttributes).map(([tag, defs]) => [tag, withPrintClasses(defs)])
126
+ );
127
+ var cellAlign = ["style", /^text-align:\s*(?:left|center|right);?$/];
128
+ var PRINT_HTML_SCHEMA = {
129
+ ...SAFE_HTML_SCHEMA,
130
+ tagNames: [
131
+ .../* @__PURE__ */ new Set([...SAFE_HTML_SCHEMA.tagNames ?? [], "section", "figure", "figcaption", "label"])
132
+ ],
133
+ attributes: {
134
+ ...printAttributes,
135
+ "*": [
136
+ ...baseAttributes["*"] ?? [],
137
+ ["className", PRINT_CLASS],
138
+ "role",
139
+ "ariaLabel",
140
+ "ariaHidden",
141
+ "dataLabel",
142
+ "dataLanguage",
143
+ "dataArtifactType"
144
+ ],
145
+ // Table-cell alignment is the one inline style the converter writes.
146
+ td: withPrintClasses([...baseAttributes["td"] ?? [], [...cellAlign]]),
147
+ th: withPrintClasses([...baseAttributes["th"] ?? [], [...cellAlign]]),
148
+ // The flashcard flip is a checkbox + label pair (GitHub forces `disabled`).
149
+ input: withPrintClasses([["type", "checkbox"], "checked"]),
150
+ label: withPrintClasses(["htmlFor"])
151
+ },
152
+ required: { input: { type: "checkbox" } },
153
+ protocols: {
154
+ ...import_hast_util_sanitize.defaultSchema.protocols,
155
+ href: [...import_hast_util_sanitize.defaultSchema.protocols?.href ?? [], "tel"],
156
+ // Embedded images (`data:image/png;base64,…`) — an <img> never runs script.
157
+ src: [...import_hast_util_sanitize.defaultSchema.protocols?.src ?? [], "data"]
158
+ }
159
+ };
160
+ function sanitizeHtmlFragment(html, schema = SAFE_HTML_SCHEMA) {
161
+ if (!html) return "";
162
+ const tree = (0, import_hast_util_sanitize.sanitize)((0, import_hast_util_from_html.fromHtml)(html, { fragment: true }), schema);
163
+ return (0, import_hast_util_to_html.toHtml)(tree, { characterReferences: { useNamedReferences: true } });
164
+ }
165
+ function sanitizePrintHtml(html) {
166
+ return sanitizeHtmlFragment(html, PRINT_HTML_SCHEMA);
167
+ }
70
168
 
71
169
  // src/markdown-css.ts
72
170
  var MARKDOWN_DOCUMENT_TOKENS_CSS = `:root {
@@ -1407,11 +1505,11 @@ function protectRegions(markdown, g, stash, ctx) {
1407
1505
  text = next;
1408
1506
  }
1409
1507
  text = sanitizeRawTags(text);
1410
- if (text !== before && !ctx.strippedAnnounced) {
1508
+ if ((text !== before || hasDisallowedTag(text)) && !ctx.strippedAnnounced) {
1411
1509
  ctx.strippedAnnounced = true;
1412
1510
  ctx.options.onNotice?.({
1413
1511
  code: "html-stripped",
1414
- message: "Unsafe or page-level markup (a <script>, <style>, <head>, an <html>/<body> shell, an on\u2026 event handler, or a javascript: URL) was removed from the rendered copy.",
1512
+ message: "Markup that is not on the safe list (a <script>, <style>, <iframe>, <form>, <object>/<embed>, <meta>, <link>, an on\u2026 event handler, inline style, or a javascript: URL) was removed from the rendered copy.",
1415
1513
  remedy: 'To show a full HTML page, put it in an ```html fence or an <artifact type="html"> block \u2014 it is printed as its source.'
1416
1514
  });
1417
1515
  }
@@ -1423,6 +1521,14 @@ ${stash.block(flashcardsHtml(g, cards, ctx))}
1423
1521
  });
1424
1522
  return text;
1425
1523
  }
1524
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
1525
+ function hasDisallowedTag(text) {
1526
+ for (const m of text.matchAll(/<\/?([a-zA-Z][\w:-]*)/g)) {
1527
+ const tag = m[1].toLowerCase();
1528
+ if (tag !== "flashcards" && !ALLOWED_TAGS.has(tag)) return true;
1529
+ }
1530
+ return false;
1531
+ }
1426
1532
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
1427
1533
  function decodeForSchemeCheck(value) {
1428
1534
  return value.replace(/&#x([0-9a-f]+);?/gi, (_m, h) => String.fromCharCode(parseInt(h, 16))).replace(/&#(\d+);?/g, (_m, d) => String.fromCharCode(Number(d))).replace(/&colon;/gi, ":").replace(/[\u0000-\u0020]+/g, "").toLowerCase();
@@ -1471,13 +1577,7 @@ var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`,
1471
1577
  function convertRules(html, g) {
1472
1578
  return html.replace(/^\s{0,3}#\s*={3,}\s*$/gm, `<hr${cls(g.hr, "matrx-hr-heavy")}>`).replace(/^\s{0,3}\*(?:\s*\*){2,}\s*$/gm, `<hr${cls(g.hr, "matrx-hr-accent")}>`).replace(/^\s{0,3}(?:-\s*){3,}$/gm, `<hr${cls(g.hr)}>`).replace(/^\s{0,3}(?:_\s*){3,}$/gm, `<hr${cls(g.hr)}>`);
1473
1579
  }
1474
- function splitTableRow(row) {
1475
- let r = row.trim();
1476
- if (r.startsWith("|")) r = r.slice(1);
1477
- if (r.endsWith("|") && !r.endsWith("\\|")) r = r.slice(0, -1);
1478
- return r.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
1479
- }
1480
- function cellAlign(separator) {
1580
+ function cellAlign2(separator) {
1481
1581
  const s = separator.trim();
1482
1582
  const left = s.startsWith(":");
1483
1583
  const right = s.endsWith(":");
@@ -1486,40 +1586,48 @@ function cellAlign(separator) {
1486
1586
  return "";
1487
1587
  }
1488
1588
  function convertTables(html, g) {
1489
- const tableRegex = /^(\|.*\|)\s*\n(\|[-\s:|]*\|)\s*\n((?:\|.*\|\s*\n?)*)/gm;
1490
- return html.replace(
1491
- tableRegex,
1492
- (_m, headerRow, separatorRow, bodyRows) => {
1493
- const headers = splitTableRow(headerRow);
1494
- const aligns = splitTableRow(separatorRow).map(cellAlign);
1495
- const rows = bodyRows.trim().split("\n").filter((r) => r.trim()).map(splitTableRow);
1496
- let t = `<table${cls(g.table)}>
1589
+ const lines = html.split("\n");
1590
+ const out = [];
1591
+ for (let i = 0; i < lines.length; i += 1) {
1592
+ if (!(0, import_source.tableStartsAt)(lines, i)) {
1593
+ out.push(lines[i] ?? "");
1594
+ continue;
1595
+ }
1596
+ const end = (0, import_source.findTableEnd)(lines, i);
1597
+ const cellsOf = (line) => (0, import_source.rowCells)(line).map(import_source.unescapeCellPipes);
1598
+ const headers = cellsOf(lines[i] ?? "");
1599
+ const aligns = (0, import_source.rowCells)(lines[i + 1] ?? "").map(cellAlign2);
1600
+ const rows = lines.slice(i + 2, end).map((line) => {
1601
+ const cells = cellsOf(line);
1602
+ return headers.map((_h, k) => cells[k] ?? "");
1603
+ });
1604
+ let t = `<table${cls(g.table)}>
1497
1605
  <thead${cls(g.thead)}>
1498
1606
  <tr${cls(g.tr)}>
1499
1607
  `;
1500
- headers.forEach((h, i) => {
1501
- t += `<th${cls(g.th)}${aligns[i] ?? ""}>${h}</th>
1608
+ headers.forEach((h, k) => {
1609
+ t += `<th${cls(g.th)}${aligns[k] ?? ""}>${h}</th>
1502
1610
  `;
1503
- });
1504
- t += "</tr>\n</thead>\n";
1505
- if (rows.length > 0) {
1506
- t += `<tbody${cls(g.tbody)}>
1611
+ });
1612
+ t += "</tr>\n</thead>\n";
1613
+ if (rows.length > 0) {
1614
+ t += `<tbody${cls(g.tbody)}>
1507
1615
  `;
1508
- for (const row of rows) {
1509
- t += `<tr${cls(g.tr)}>
1616
+ for (const row of rows) {
1617
+ t += `<tr${cls(g.tr)}>
1510
1618
  `;
1511
- row.forEach((c, i) => {
1512
- t += `<td${cls(g.td)}${aligns[i] ?? ""}>${c}</td>
1619
+ row.forEach((c, k) => {
1620
+ t += `<td${cls(g.td)}${aligns[k] ?? ""}>${c}</td>
1513
1621
  `;
1514
- });
1515
- t += "</tr>\n";
1516
- }
1517
- t += "</tbody>\n";
1622
+ });
1623
+ t += "</tr>\n";
1518
1624
  }
1519
- return `${t}</table>
1520
- `;
1625
+ t += "</tbody>\n";
1521
1626
  }
1522
- );
1627
+ out.push(`${t}</table>`);
1628
+ i = end - 1;
1629
+ }
1630
+ return out.join("\n");
1523
1631
  }
1524
1632
  function convertLists(html, g) {
1525
1633
  const lines = html.split("\n");
@@ -1605,7 +1713,9 @@ function markdownToHtml(markdown, options = {}) {
1605
1713
  return "";
1606
1714
  }
1607
1715
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1608
- return `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`;
1716
+ return sanitizePrintHtml(
1717
+ `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`
1718
+ );
1609
1719
  }
1610
1720
  function documentBody(markdown, ctx) {
1611
1721
  const g = DOCUMENT_GRAMMAR;
@@ -1675,7 +1785,7 @@ function markdownToArticleHtml(markdown, options = {}) {
1675
1785
  return "";
1676
1786
  }
1677
1787
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1678
- return articleBody(source, newContext(options));
1788
+ return sanitizePrintHtml(articleBody(source, newContext(options)));
1679
1789
  }
1680
1790
  function articleBody(markdown, ctx) {
1681
1791
  const g = ARTICLE_GRAMMAR;