@ai-matrx/print 0.8.6 → 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;
@@ -1098,7 +1195,8 @@ function splitAtPageBreaks(markdown) {
1098
1195
  // src/markdown.ts
1099
1196
  function removeThinkingContent(content) {
1100
1197
  if (!content) return "";
1101
- let result = content.replace(/\n*<thinking>[\s\S]*?<\/thinking>\n*/gi, "\n").replace(/\n*<think>[\s\S]*?<\/think>\n*/gi, "\n").replace(/\n*<reasoning>[\s\S]*?<\/reasoning>\n*/gi, "\n");
1198
+ const hide = (_match, before, after) => before.length >= 2 || after.length >= 2 ? "\n\n" : "\n";
1199
+ let result = content.replace(/(\n*)<thinking>[\s\S]*?<\/thinking>(\n*)/gi, hide).replace(/(\n*)<think>[\s\S]*?<\/think>(\n*)/gi, hide).replace(/(\n*)<reasoning>[\s\S]*?<\/reasoning>(\n*)/gi, hide);
1102
1200
  result = result.replace(/\n{3,}/g, "\n\n");
1103
1201
  return result.trim();
1104
1202
  }
@@ -1406,11 +1504,11 @@ function protectRegions(markdown, g, stash, ctx) {
1406
1504
  text = next;
1407
1505
  }
1408
1506
  text = sanitizeRawTags(text);
1409
- if (text !== before && !ctx.strippedAnnounced) {
1507
+ if ((text !== before || hasDisallowedTag(text)) && !ctx.strippedAnnounced) {
1410
1508
  ctx.strippedAnnounced = true;
1411
1509
  ctx.options.onNotice?.({
1412
1510
  code: "html-stripped",
1413
- 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.",
1414
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.'
1415
1513
  });
1416
1514
  }
@@ -1422,6 +1520,14 @@ ${stash.block(flashcardsHtml(g, cards, ctx))}
1422
1520
  });
1423
1521
  return text;
1424
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
+ }
1425
1531
  var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href", "poster", "background"]);
1426
1532
  function decodeForSchemeCheck(value) {
1427
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();
@@ -1476,7 +1582,7 @@ function splitTableRow(row) {
1476
1582
  if (r.endsWith("|") && !r.endsWith("\\|")) r = r.slice(0, -1);
1477
1583
  return r.split(/(?<!\\)\|/).map((c) => c.trim().replace(/\\\|/g, "|"));
1478
1584
  }
1479
- function cellAlign(separator) {
1585
+ function cellAlign2(separator) {
1480
1586
  const s = separator.trim();
1481
1587
  const left = s.startsWith(":");
1482
1588
  const right = s.endsWith(":");
@@ -1490,7 +1596,7 @@ function convertTables(html, g) {
1490
1596
  tableRegex,
1491
1597
  (_m, headerRow, separatorRow, bodyRows) => {
1492
1598
  const headers = splitTableRow(headerRow);
1493
- const aligns = splitTableRow(separatorRow).map(cellAlign);
1599
+ const aligns = splitTableRow(separatorRow).map(cellAlign2);
1494
1600
  const rows = bodyRows.trim().split("\n").filter((r) => r.trim()).map(splitTableRow);
1495
1601
  let t = `<table${cls(g.table)}>
1496
1602
  <thead${cls(g.thead)}>
@@ -1604,7 +1710,9 @@ function markdownToHtml(markdown, options = {}) {
1604
1710
  return "";
1605
1711
  }
1606
1712
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1607
- 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
+ );
1608
1716
  }
1609
1717
  function documentBody(markdown, ctx) {
1610
1718
  const g = DOCUMENT_GRAMMAR;
@@ -1674,7 +1782,7 @@ function markdownToArticleHtml(markdown, options = {}) {
1674
1782
  return "";
1675
1783
  }
1676
1784
  const source = scrubTokens(options.includeThinking ? markdown : removeThinkingContent(markdown));
1677
- return articleBody(source, newContext(options));
1785
+ return sanitizePrintHtml(articleBody(source, newContext(options)));
1678
1786
  }
1679
1787
  function articleBody(markdown, ctx) {
1680
1788
  const g = ARTICLE_GRAMMAR;