@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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.8
4
+
5
+ - **Security: every markdown→HTML path is allow-list sanitized.** The regex
6
+ converter (`markdownToHtml`, `markdownToArticleHtml`, and through them
7
+ `renderMarkdownDocument`, `markdownToHtmlForSkin`, `markdownToPdfBlob`) used a
8
+ block-list and let `<iframe>`, `<form>`, `<meta http-equiv=refresh>`,
9
+ `<object>`/`<embed>`, `<base>`, `<link>`, `<svg>`, `<textarea>`/`<select>` and
10
+ inline `style` through. Output now goes through `PRINT_HTML_SCHEMA`
11
+ (hast-util-sanitize's GitHub schema plus the converter's own `.matrx-*` /
12
+ `print-*` / `code-*` classes, table-cell `text-align`, the flashcard
13
+ checkbox + label, figures, page-break data/aria attributes). Sync, DOM-free.
14
+ - **Security (`/document`):** TeX `\href`, `\style`, `\class`, `\cssId`,
15
+ `\bbox[css]` and `\mmlToken[attrs]` no longer write `href`/`style` into the
16
+ exported MathML (`MATHML_SCHEMA`); a front-matter `header`/`footer` can no
17
+ longer close the document's `<style>` element.
18
+ - **New subpath `/safe-html`:** `sanitizeHtmlFragment`, `SAFE_HTML_SCHEMA`
19
+ (plain GitHub allow-list for email/CMS), `PRINT_HTML_SCHEMA`,
20
+ `sanitizePrintHtml`, `MATHML_SCHEMA`, `sanitizeMathml`.
21
+ - **Visible differences:** author raw-HTML classes outside the print
22
+ vocabulary are dropped (`<div class="note">` → `<div class="matrx-div">`);
23
+ author inline styles are dropped; HTML serialization is normalized (`<img …>`
24
+ not `<img … />`, text `>` and `"` literal instead of `&gt;`/`&quot;`) —
25
+ same rendered page.
26
+ - Consumer action: none — refresh to latest.
27
+
3
28
  ## 0.8.7
4
29
 
5
30
  - **Fixed: hidden reasoning merged two paragraphs.** `removeThinkingContent`
package/dist/document.cjs CHANGED
@@ -170,6 +170,206 @@ function contentColumnWidths(b, n, avail, measure, pad = 10) {
170
170
 
171
171
  // src/document/math.ts
172
172
  var import_source = require("@ai-matrx/content-ir/source");
173
+
174
+ // src/safe-html.ts
175
+ var import_hast_util_from_html = require("hast-util-from-html");
176
+ var import_hast_util_sanitize = require("hast-util-sanitize");
177
+ var import_hast_util_to_html = require("hast-util-to-html");
178
+ var STRIP_WITH_CONTENT = [
179
+ "script",
180
+ "style",
181
+ "noscript",
182
+ "template",
183
+ "iframe",
184
+ "object",
185
+ "embed",
186
+ "textarea",
187
+ "select",
188
+ "head",
189
+ "title"
190
+ ];
191
+ var FORBIDDEN_TAGS = [
192
+ ...STRIP_WITH_CONTENT,
193
+ "form",
194
+ "meta",
195
+ "base",
196
+ "link",
197
+ "frame",
198
+ "frameset",
199
+ "applet",
200
+ "button",
201
+ "svg",
202
+ "math"
203
+ ];
204
+ var baseAttributes = import_hast_util_sanitize.defaultSchema.attributes ?? {};
205
+ var SAFE_HTML_SCHEMA = {
206
+ ...import_hast_util_sanitize.defaultSchema,
207
+ tagNames: (import_hast_util_sanitize.defaultSchema.tagNames ?? []).filter((t) => !FORBIDDEN_TAGS.includes(t)),
208
+ strip: STRIP_WITH_CONTENT,
209
+ // Authored anchors keep their names; the GitHub `user-content-` rewrite
210
+ // would also break `<label for>` → `<input id>` pairs.
211
+ clobberPrefix: "",
212
+ clobber: []
213
+ };
214
+ var PRINT_CLASS = /^(?:(?:matrx|print|code|doc|language)-[\w-]*|inline-code|is-checked)$/;
215
+ function withPrintClasses(defs = []) {
216
+ let found = false;
217
+ const out = defs.map((d) => {
218
+ if (Array.isArray(d) && d[0] === "className") {
219
+ found = true;
220
+ return [...d, PRINT_CLASS];
221
+ }
222
+ return d;
223
+ });
224
+ return found ? out : [...out, ["className", PRINT_CLASS]];
225
+ }
226
+ var printAttributes = Object.fromEntries(
227
+ Object.entries(baseAttributes).map(([tag, defs]) => [tag, withPrintClasses(defs)])
228
+ );
229
+ var cellAlign = ["style", /^text-align:\s*(?:left|center|right);?$/];
230
+ var PRINT_HTML_SCHEMA = {
231
+ ...SAFE_HTML_SCHEMA,
232
+ tagNames: [
233
+ .../* @__PURE__ */ new Set([...SAFE_HTML_SCHEMA.tagNames ?? [], "section", "figure", "figcaption", "label"])
234
+ ],
235
+ attributes: {
236
+ ...printAttributes,
237
+ "*": [
238
+ ...baseAttributes["*"] ?? [],
239
+ ["className", PRINT_CLASS],
240
+ "role",
241
+ "ariaLabel",
242
+ "ariaHidden",
243
+ "dataLabel",
244
+ "dataLanguage",
245
+ "dataArtifactType"
246
+ ],
247
+ // Table-cell alignment is the one inline style the converter writes.
248
+ td: withPrintClasses([...baseAttributes["td"] ?? [], [...cellAlign]]),
249
+ th: withPrintClasses([...baseAttributes["th"] ?? [], [...cellAlign]]),
250
+ // The flashcard flip is a checkbox + label pair (GitHub forces `disabled`).
251
+ input: withPrintClasses([["type", "checkbox"], "checked"]),
252
+ label: withPrintClasses(["htmlFor"])
253
+ },
254
+ required: { input: { type: "checkbox" } },
255
+ protocols: {
256
+ ...import_hast_util_sanitize.defaultSchema.protocols,
257
+ href: [...import_hast_util_sanitize.defaultSchema.protocols?.href ?? [], "tel"],
258
+ // Embedded images (`data:image/png;base64,…`) — an <img> never runs script.
259
+ src: [...import_hast_util_sanitize.defaultSchema.protocols?.src ?? [], "data"]
260
+ }
261
+ };
262
+ function sanitizeHtmlFragment(html, schema = SAFE_HTML_SCHEMA) {
263
+ if (!html) return "";
264
+ const tree = (0, import_hast_util_sanitize.sanitize)((0, import_hast_util_from_html.fromHtml)(html, { fragment: true }), schema);
265
+ return (0, import_hast_util_to_html.toHtml)(tree, { characterReferences: { useNamedReferences: true } });
266
+ }
267
+ var MATHML_TAGS = [
268
+ "math",
269
+ "maction",
270
+ "annotation",
271
+ "menclose",
272
+ "merror",
273
+ "mfrac",
274
+ "mglyph",
275
+ "mi",
276
+ "mlabeledtr",
277
+ "mmultiscripts",
278
+ "mn",
279
+ "mo",
280
+ "mover",
281
+ "mpadded",
282
+ "mphantom",
283
+ "mprescripts",
284
+ "none",
285
+ "mroot",
286
+ "mrow",
287
+ "ms",
288
+ "mspace",
289
+ "msqrt",
290
+ "mstyle",
291
+ "msub",
292
+ "msubsup",
293
+ "msup",
294
+ "mtable",
295
+ "mtd",
296
+ "mtext",
297
+ "mtr",
298
+ "munder",
299
+ "munderover",
300
+ "semantics"
301
+ ];
302
+ var MATHML_ATTRIBUTES = [
303
+ "xmlns",
304
+ "display",
305
+ "displaystyle",
306
+ "mathvariant",
307
+ "mathsize",
308
+ "mathcolor",
309
+ "mathbackground",
310
+ "scriptlevel",
311
+ "dir",
312
+ "fence",
313
+ "form",
314
+ "largeop",
315
+ "lspace",
316
+ "rspace",
317
+ "movablelimits",
318
+ "separator",
319
+ "stretchy",
320
+ "symmetric",
321
+ "maxsize",
322
+ "minsize",
323
+ "accent",
324
+ "accentunder",
325
+ "align",
326
+ "columnalign",
327
+ "rowalign",
328
+ "columnspacing",
329
+ "rowspacing",
330
+ "columnlines",
331
+ "rowlines",
332
+ "frame",
333
+ "framespacing",
334
+ "equalrows",
335
+ "equalcolumns",
336
+ "linethickness",
337
+ "numalign",
338
+ "denomalign",
339
+ "bevelled",
340
+ "notation",
341
+ "width",
342
+ "height",
343
+ "depth",
344
+ "voffset",
345
+ "lquote",
346
+ "rquote",
347
+ "columnspan",
348
+ "rowspan",
349
+ "encoding",
350
+ "open",
351
+ "close",
352
+ "separators",
353
+ "actiontype",
354
+ "selection",
355
+ "mslinebreak",
356
+ "linebreak",
357
+ "indentalign",
358
+ "data*"
359
+ ];
360
+ var MATHML_SCHEMA = {
361
+ tagNames: MATHML_TAGS,
362
+ attributes: { "*": MATHML_ATTRIBUTES },
363
+ strip: ["script", "style"],
364
+ clobberPrefix: "",
365
+ clobber: [],
366
+ protocols: {}
367
+ };
368
+ function sanitizeMathml(mathml) {
369
+ return sanitizeHtmlFragment(mathml, MATHML_SCHEMA);
370
+ }
371
+
372
+ // src/document/math.ts
173
373
  var MATH_OPEN = "\uE100";
174
374
  var MATH_CLOSE = "\uE101";
175
375
  var MATH_TOKEN_RE = /\uE100(\d+)\uE101/g;
@@ -236,10 +436,10 @@ async function typesetMathSpans(spans) {
236
436
  const mathml = mj.mml(s.tex, s.display);
237
437
  const err = /<merror[\s\S]*?<mtext[^>]*>([\s\S]*?)<\/mtext>/.exec(mathml);
238
438
  if (err) {
239
- out.set(key, { mathml, error: decodeXml(err[1]) });
439
+ out.set(key, { mathml: sanitizeMathml(mathml), error: decodeXml(err[1]) });
240
440
  continue;
241
441
  }
242
- const entry = { mathml: mathml.replace(/\n\s*/g, "") };
442
+ const entry = { mathml: sanitizeMathml(mathml.replace(/\n\s*/g, "")) };
243
443
  if (s.display) {
244
444
  const svg = mj.svg(s.tex, true);
245
445
  entry.svg = svg;
@@ -3437,7 +3637,7 @@ function coverHtml(s, x) {
3437
3637
  s.title ?? ""
3438
3638
  )}</h1>${s.subtitle ? `<p class="doc-cover-subtitle">${(0, import_html_escape.escapeHtml)(s.subtitle)}</p>` : ""}${s.author ? `<p class="doc-cover-author">${(0, import_html_escape.escapeHtml)(s.author)}</p>` : ""}${s.date ? `<p class="doc-cover-date">${(0, import_html_escape.escapeHtml)(s.date)}</p>` : ""}</section>`;
3439
3639
  }
3440
- var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ")}"`;
3640
+ var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ").replace(/</g, "\\3C ").replace(/>/g, "\\3E ")}"`;
3441
3641
  function marginContent(template, s) {
3442
3642
  if (!template) return "none";
3443
3643
  return templateParts(template, s).map((p) => "field" in p ? `counter(${p.field})` : cssString(p.text)).join(" ");
@@ -4967,6 +5167,7 @@ function looksLikeJson(body) {
4967
5167
  return false;
4968
5168
  }
4969
5169
  }
5170
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
4970
5171
  var LINK_URL = String.raw`((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?`;
4971
5172
  var IMAGE_RE = new RegExp(String.raw`!\[([^\]]*)\]\(` + LINK_URL + String.raw`\)`, "g");
4972
5173
  var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`, "g");