@ai-matrx/print 0.8.7 → 0.8.8

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.js CHANGED
@@ -23,6 +23,103 @@ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "wid
23
23
  // src/markdown.ts
24
24
  import { escapeHtml as escapeHtml2 } from "@ai-matrx/kit/html-escape";
25
25
 
26
+ // src/safe-html.ts
27
+ import { fromHtml } from "hast-util-from-html";
28
+ import { defaultSchema, sanitize } from "hast-util-sanitize";
29
+ import { toHtml } from "hast-util-to-html";
30
+ var STRIP_WITH_CONTENT = [
31
+ "script",
32
+ "style",
33
+ "noscript",
34
+ "template",
35
+ "iframe",
36
+ "object",
37
+ "embed",
38
+ "textarea",
39
+ "select",
40
+ "head",
41
+ "title"
42
+ ];
43
+ var FORBIDDEN_TAGS = [
44
+ ...STRIP_WITH_CONTENT,
45
+ "form",
46
+ "meta",
47
+ "base",
48
+ "link",
49
+ "frame",
50
+ "frameset",
51
+ "applet",
52
+ "button",
53
+ "svg",
54
+ "math"
55
+ ];
56
+ var baseAttributes = defaultSchema.attributes ?? {};
57
+ var SAFE_HTML_SCHEMA = {
58
+ ...defaultSchema,
59
+ tagNames: (defaultSchema.tagNames ?? []).filter((t) => !FORBIDDEN_TAGS.includes(t)),
60
+ strip: STRIP_WITH_CONTENT,
61
+ // Authored anchors keep their names; the GitHub `user-content-` rewrite
62
+ // would also break `<label for>` → `<input id>` pairs.
63
+ clobberPrefix: "",
64
+ clobber: []
65
+ };
66
+ var PRINT_CLASS = /^(?:(?:matrx|print|code|doc|language)-[\w-]*|inline-code|is-checked)$/;
67
+ function withPrintClasses(defs = []) {
68
+ let found = false;
69
+ const out = defs.map((d) => {
70
+ if (Array.isArray(d) && d[0] === "className") {
71
+ found = true;
72
+ return [...d, PRINT_CLASS];
73
+ }
74
+ return d;
75
+ });
76
+ return found ? out : [...out, ["className", PRINT_CLASS]];
77
+ }
78
+ var printAttributes = Object.fromEntries(
79
+ Object.entries(baseAttributes).map(([tag, defs]) => [tag, withPrintClasses(defs)])
80
+ );
81
+ var cellAlign = ["style", /^text-align:\s*(?:left|center|right);?$/];
82
+ var PRINT_HTML_SCHEMA = {
83
+ ...SAFE_HTML_SCHEMA,
84
+ tagNames: [
85
+ .../* @__PURE__ */ new Set([...SAFE_HTML_SCHEMA.tagNames ?? [], "section", "figure", "figcaption", "label"])
86
+ ],
87
+ attributes: {
88
+ ...printAttributes,
89
+ "*": [
90
+ ...baseAttributes["*"] ?? [],
91
+ ["className", PRINT_CLASS],
92
+ "role",
93
+ "ariaLabel",
94
+ "ariaHidden",
95
+ "dataLabel",
96
+ "dataLanguage",
97
+ "dataArtifactType"
98
+ ],
99
+ // Table-cell alignment is the one inline style the converter writes.
100
+ td: withPrintClasses([...baseAttributes["td"] ?? [], [...cellAlign]]),
101
+ th: withPrintClasses([...baseAttributes["th"] ?? [], [...cellAlign]]),
102
+ // The flashcard flip is a checkbox + label pair (GitHub forces `disabled`).
103
+ input: withPrintClasses([["type", "checkbox"], "checked"]),
104
+ label: withPrintClasses(["htmlFor"])
105
+ },
106
+ required: { input: { type: "checkbox" } },
107
+ protocols: {
108
+ ...defaultSchema.protocols,
109
+ href: [...defaultSchema.protocols?.href ?? [], "tel"],
110
+ // Embedded images (`data:image/png;base64,…`) — an <img> never runs script.
111
+ src: [...defaultSchema.protocols?.src ?? [], "data"]
112
+ }
113
+ };
114
+ function sanitizeHtmlFragment(html, schema = SAFE_HTML_SCHEMA) {
115
+ if (!html) return "";
116
+ const tree = sanitize(fromHtml(html, { fragment: true }), schema);
117
+ return toHtml(tree, { characterReferences: { useNamedReferences: true } });
118
+ }
119
+ function sanitizePrintHtml(html) {
120
+ return sanitizeHtmlFragment(html, PRINT_HTML_SCHEMA);
121
+ }
122
+
26
123
  // src/markdown-css.ts
27
124
  var MARKDOWN_DOCUMENT_TOKENS_CSS = `:root {
28
125
  --matrx-print-accent: #667eea;
@@ -1362,11 +1459,11 @@ function protectRegions(markdown, g, stash, ctx) {
1362
1459
  text = next;
1363
1460
  }
1364
1461
  text = sanitizeRawTags(text);
1365
- if (text !== before && !ctx.strippedAnnounced) {
1462
+ if ((text !== before || hasDisallowedTag(text)) && !ctx.strippedAnnounced) {
1366
1463
  ctx.strippedAnnounced = true;
1367
1464
  ctx.options.onNotice?.({
1368
1465
  code: "html-stripped",
1369
- 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.",
1466
+ 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.",
1370
1467
  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.'
1371
1468
  });
1372
1469
  }
@@ -1378,6 +1475,14 @@ ${stash.block(flashcardsHtml(g, cards, ctx))}
1378
1475
  });
1379
1476
  return text;
1380
1477
  }
1478
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
1479
+ function hasDisallowedTag(text) {
1480
+ for (const m of text.matchAll(/<\/?([a-zA-Z][\w:-]*)/g)) {
1481
+ const tag = m[1].toLowerCase();
1482
+ if (tag !== "flashcards" && !ALLOWED_TAGS.has(tag)) return true;
1483
+ }
1484
+ return false;
1485
+ }
1381
1486
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
1382
1487
  function decodeForSchemeCheck(value) {
1383
1488
  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();
@@ -1432,7 +1537,7 @@ function splitTableRow(row) {
1432
1537
  if (r.endsWith("|") && !r.endsWith("\\|")) r = r.slice(0, -1);
1433
1538
  return r.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
1434
1539
  }
1435
- function cellAlign(separator) {
1540
+ function cellAlign2(separator) {
1436
1541
  const s = separator.trim();
1437
1542
  const left = s.startsWith(":");
1438
1543
  const right = s.endsWith(":");
@@ -1446,7 +1551,7 @@ function convertTables(html, g) {
1446
1551
  tableRegex,
1447
1552
  (_m, headerRow, separatorRow, bodyRows) => {
1448
1553
  const headers = splitTableRow(headerRow);
1449
- const aligns = splitTableRow(separatorRow).map(cellAlign);
1554
+ const aligns = splitTableRow(separatorRow).map(cellAlign2);
1450
1555
  const rows = bodyRows.trim().split("\n").filter((r) => r.trim()).map(splitTableRow);
1451
1556
  let t = `<table${cls(g.table)}>
1452
1557
  <thead${cls(g.thead)}>
@@ -1560,7 +1665,9 @@ function markdownToHtml(markdown, options = {}) {
1560
1665
  return "";
1561
1666
  }
1562
1667
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1563
- return `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`;
1668
+ return sanitizePrintHtml(
1669
+ `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`
1670
+ );
1564
1671
  }
1565
1672
  function documentBody(markdown, ctx) {
1566
1673
  const g = DOCUMENT_GRAMMAR;
@@ -1630,7 +1737,7 @@ function markdownToArticleHtml(markdown, options = {}) {
1630
1737
  return "";
1631
1738
  }
1632
1739
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1633
- return articleBody(source, newContext(options));
1740
+ return sanitizePrintHtml(articleBody(source, newContext(options)));
1634
1741
  }
1635
1742
  function articleBody(markdown, ctx) {
1636
1743
  const g = ARTICLE_GRAMMAR;