@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.cjs CHANGED
@@ -68,6 +68,103 @@ function openPrintWindow(htmlDocument, filename = "print", windowFeatures = "wid
68
68
  // src/markdown.ts
69
69
  var import_html_escape2 = require("@ai-matrx/kit/html-escape");
70
70
 
71
+ // src/safe-html.ts
72
+ var import_hast_util_from_html = require("hast-util-from-html");
73
+ var import_hast_util_sanitize = require("hast-util-sanitize");
74
+ var import_hast_util_to_html = require("hast-util-to-html");
75
+ var STRIP_WITH_CONTENT = [
76
+ "script",
77
+ "style",
78
+ "noscript",
79
+ "template",
80
+ "iframe",
81
+ "object",
82
+ "embed",
83
+ "textarea",
84
+ "select",
85
+ "head",
86
+ "title"
87
+ ];
88
+ var FORBIDDEN_TAGS = [
89
+ ...STRIP_WITH_CONTENT,
90
+ "form",
91
+ "meta",
92
+ "base",
93
+ "link",
94
+ "frame",
95
+ "frameset",
96
+ "applet",
97
+ "button",
98
+ "svg",
99
+ "math"
100
+ ];
101
+ var baseAttributes = import_hast_util_sanitize.defaultSchema.attributes ?? {};
102
+ var SAFE_HTML_SCHEMA = {
103
+ ...import_hast_util_sanitize.defaultSchema,
104
+ tagNames: (import_hast_util_sanitize.defaultSchema.tagNames ?? []).filter((t) => !FORBIDDEN_TAGS.includes(t)),
105
+ strip: STRIP_WITH_CONTENT,
106
+ // Authored anchors keep their names; the GitHub `user-content-` rewrite
107
+ // would also break `<label for>` → `<input id>` pairs.
108
+ clobberPrefix: "",
109
+ clobber: []
110
+ };
111
+ var PRINT_CLASS = /^(?:(?:matrx|print|code|doc|language)-[\w-]*|inline-code|is-checked)$/;
112
+ function withPrintClasses(defs = []) {
113
+ let found = false;
114
+ const out = defs.map((d) => {
115
+ if (Array.isArray(d) && d[0] === "className") {
116
+ found = true;
117
+ return [...d, PRINT_CLASS];
118
+ }
119
+ return d;
120
+ });
121
+ return found ? out : [...out, ["className", PRINT_CLASS]];
122
+ }
123
+ var printAttributes = Object.fromEntries(
124
+ Object.entries(baseAttributes).map(([tag, defs]) => [tag, withPrintClasses(defs)])
125
+ );
126
+ var cellAlign = ["style", /^text-align:\s*(?:left|center|right);?$/];
127
+ var PRINT_HTML_SCHEMA = {
128
+ ...SAFE_HTML_SCHEMA,
129
+ tagNames: [
130
+ .../* @__PURE__ */ new Set([...SAFE_HTML_SCHEMA.tagNames ?? [], "section", "figure", "figcaption", "label"])
131
+ ],
132
+ attributes: {
133
+ ...printAttributes,
134
+ "*": [
135
+ ...baseAttributes["*"] ?? [],
136
+ ["className", PRINT_CLASS],
137
+ "role",
138
+ "ariaLabel",
139
+ "ariaHidden",
140
+ "dataLabel",
141
+ "dataLanguage",
142
+ "dataArtifactType"
143
+ ],
144
+ // Table-cell alignment is the one inline style the converter writes.
145
+ td: withPrintClasses([...baseAttributes["td"] ?? [], [...cellAlign]]),
146
+ th: withPrintClasses([...baseAttributes["th"] ?? [], [...cellAlign]]),
147
+ // The flashcard flip is a checkbox + label pair (GitHub forces `disabled`).
148
+ input: withPrintClasses([["type", "checkbox"], "checked"]),
149
+ label: withPrintClasses(["htmlFor"])
150
+ },
151
+ required: { input: { type: "checkbox" } },
152
+ protocols: {
153
+ ...import_hast_util_sanitize.defaultSchema.protocols,
154
+ href: [...import_hast_util_sanitize.defaultSchema.protocols?.href ?? [], "tel"],
155
+ // Embedded images (`data:image/png;base64,…`) — an <img> never runs script.
156
+ src: [...import_hast_util_sanitize.defaultSchema.protocols?.src ?? [], "data"]
157
+ }
158
+ };
159
+ function sanitizeHtmlFragment(html, schema = SAFE_HTML_SCHEMA) {
160
+ if (!html) return "";
161
+ const tree = (0, import_hast_util_sanitize.sanitize)((0, import_hast_util_from_html.fromHtml)(html, { fragment: true }), schema);
162
+ return (0, import_hast_util_to_html.toHtml)(tree, { characterReferences: { useNamedReferences: true } });
163
+ }
164
+ function sanitizePrintHtml(html) {
165
+ return sanitizeHtmlFragment(html, PRINT_HTML_SCHEMA);
166
+ }
167
+
71
168
  // src/markdown-css.ts
72
169
  var MARKDOWN_DOCUMENT_TOKENS_CSS = `:root {
73
170
  --matrx-print-accent: #667eea;
@@ -1407,11 +1504,11 @@ function protectRegions(markdown, g, stash, ctx) {
1407
1504
  text = next;
1408
1505
  }
1409
1506
  text = sanitizeRawTags(text);
1410
- if (text !== before && !ctx.strippedAnnounced) {
1507
+ if ((text !== before || hasDisallowedTag(text)) && !ctx.strippedAnnounced) {
1411
1508
  ctx.strippedAnnounced = true;
1412
1509
  ctx.options.onNotice?.({
1413
1510
  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.",
1511
+ 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
1512
  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
1513
  });
1417
1514
  }
@@ -1423,6 +1520,14 @@ ${stash.block(flashcardsHtml(g, cards, ctx))}
1423
1520
  });
1424
1521
  return text;
1425
1522
  }
1523
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
1524
+ function hasDisallowedTag(text) {
1525
+ for (const m of text.matchAll(/<\/?([a-zA-Z][\w:-]*)/g)) {
1526
+ const tag = m[1].toLowerCase();
1527
+ if (tag !== "flashcards" && !ALLOWED_TAGS.has(tag)) return true;
1528
+ }
1529
+ return false;
1530
+ }
1426
1531
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
1427
1532
  function decodeForSchemeCheck(value) {
1428
1533
  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();
@@ -1477,7 +1582,7 @@ function splitTableRow(row) {
1477
1582
  if (r.endsWith("|") && !r.endsWith("\\|")) r = r.slice(0, -1);
1478
1583
  return r.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
1479
1584
  }
1480
- function cellAlign(separator) {
1585
+ function cellAlign2(separator) {
1481
1586
  const s = separator.trim();
1482
1587
  const left = s.startsWith(":");
1483
1588
  const right = s.endsWith(":");
@@ -1491,7 +1596,7 @@ function convertTables(html, g) {
1491
1596
  tableRegex,
1492
1597
  (_m, headerRow, separatorRow, bodyRows) => {
1493
1598
  const headers = splitTableRow(headerRow);
1494
- const aligns = splitTableRow(separatorRow).map(cellAlign);
1599
+ const aligns = splitTableRow(separatorRow).map(cellAlign2);
1495
1600
  const rows = bodyRows.trim().split("\n").filter((r) => r.trim()).map(splitTableRow);
1496
1601
  let t = `<table${cls(g.table)}>
1497
1602
  <thead${cls(g.thead)}>
@@ -1605,7 +1710,9 @@ function markdownToHtml(markdown, options = {}) {
1605
1710
  return "";
1606
1711
  }
1607
1712
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1608
- return `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`;
1713
+ return sanitizePrintHtml(
1714
+ `<div class="matrx-content-container">${documentBody(source, newContext(options))}</div>`
1715
+ );
1609
1716
  }
1610
1717
  function documentBody(markdown, ctx) {
1611
1718
  const g = DOCUMENT_GRAMMAR;
@@ -1675,7 +1782,7 @@ function markdownToArticleHtml(markdown, options = {}) {
1675
1782
  return "";
1676
1783
  }
1677
1784
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1678
- return articleBody(source, newContext(options));
1785
+ return sanitizePrintHtml(articleBody(source, newContext(options)));
1679
1786
  }
1680
1787
  function articleBody(markdown, ctx) {
1681
1788
  const g = ARTICLE_GRAMMAR;