@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/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.9
4
+ ### Unreleased
5
+
6
+ - **New subpath `/markdown-html` — THE ONE Markdown→HTML engine** (Matrx
7
+ Alchemy CONTRACT §1.9, ALC-16). `createMarkdownHtmlEngine({ projectors })`
8
+ → `parse` (content-ir marks every kind island, each becomes an atom
9
+ placeholder, the `/document` parse runs once), `fragment` (skins
10
+ `document` / `article` / `inline`; profiles `generic` / `google-docs` /
11
+ `word`), `document`, `plainText` (tables as TSV, TeX kept as `$…$`). Every
12
+ string ends in the allow-list. Optional asset resolver for private images
13
+ (an unresolved image becomes its description with a notice).
14
+ - **Security (LIST T26):** the display-math SVG the PDF writer draws is
15
+ allow-list sanitized (`MATH_SVG_SCHEMA`, `sanitizeMathSvg`) — TeX `\href`,
16
+ `\style`, `\class`, `\cssId`, `\bbox[…]`, `\mmlToken[…]` no longer reach it.
17
+ - `/safe-html`: `engineHtmlSchema`, `sanitizeEngineHtml`.
18
+ - `/document`: `documentToHtml` accepts `srcFor` and `transformBody`; an
19
+ image `srcFor` result now also passes the URL check (EPUB included).
20
+
21
+ - **Tables print by THE GFM rule** (`@ai-matrx/content-ir/source`, verify-RC-B4):
22
+ tables written without edge pipes print as tables; an escaped `\|` stays in
23
+ its cell by backslash parity and prints as `|`; a line that starts another
24
+ block (`- x`, `> y`) ends a pipe-less table; rows print at the header's width.
25
+ The private `splitTableRow` and the pipe-led-only table regex are gone.
26
+ Requires `@ai-matrx/content-ir` with `/source` table exports.
27
+
28
+ ## 0.8.8
29
+
30
+ - **Security: every markdown→HTML path is allow-list sanitized.** The regex
31
+ converter (`markdownToHtml`, `markdownToArticleHtml`, and through them
32
+ `renderMarkdownDocument`, `markdownToHtmlForSkin`, `markdownToPdfBlob`) used a
33
+ block-list and let `<iframe>`, `<form>`, `<meta http-equiv=refresh>`,
34
+ `<object>`/`<embed>`, `<base>`, `<link>`, `<svg>`, `<textarea>`/`<select>` and
35
+ inline `style` through. Output now goes through `PRINT_HTML_SCHEMA`
36
+ (hast-util-sanitize's GitHub schema plus the converter's own `.matrx-*` /
37
+ `print-*` / `code-*` classes, table-cell `text-align`, the flashcard
38
+ checkbox + label, figures, page-break data/aria attributes). Sync, DOM-free.
39
+ - **Security (`/document`):** TeX `\href`, `\style`, `\class`, `\cssId`,
40
+ `\bbox[css]` and `\mmlToken[attrs]` no longer write `href`/`style` into the
41
+ exported MathML (`MATHML_SCHEMA`); a front-matter `header`/`footer` can no
42
+ longer close the document's `<style>` element.
43
+ - **New subpath `/safe-html`:** `sanitizeHtmlFragment`, `SAFE_HTML_SCHEMA`
44
+ (plain GitHub allow-list for email/CMS), `PRINT_HTML_SCHEMA`,
45
+ `sanitizePrintHtml`, `MATHML_SCHEMA`, `sanitizeMathml`.
46
+ - **Visible differences:** author raw-HTML classes outside the print
47
+ vocabulary are dropped (`<div class="note">` → `<div class="matrx-div">`);
48
+ author inline styles are dropped; HTML serialization is normalized (`<img …>`
49
+ not `<img … />`, text `>` and `"` literal instead of `&gt;`/`&quot;`) —
50
+ same rendered page.
51
+ - Consumer action: none — refresh to latest.
52
+
3
53
  ## 0.8.7
4
54
 
5
55
  - **Fixed: hidden reasoning merged two paragraphs.** `removeThinkingContent`
@@ -583,3 +633,4 @@ originals die at the adoption swap):
583
633
  defaults; the template registry is now immutable; the dev-time geometry
584
634
  check no longer reads `process.env` bare (which would throw in browser
585
635
  consumers).
636
+
package/dist/document.cjs CHANGED
@@ -170,6 +170,249 @@ 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
+ var MATH_SVG_TAGS = ["svg", "g", "path", "rect", "line", "polyline", "polygon", "circle", "ellipse", "text", "title", "desc"];
372
+ var MATH_SVG_ATTRIBUTES = [
373
+ "xmlns",
374
+ "width",
375
+ "height",
376
+ "viewBox",
377
+ "role",
378
+ "focusable",
379
+ "ariaHidden",
380
+ "ariaLabel",
381
+ "stroke",
382
+ "fill",
383
+ "strokeWidth",
384
+ "strokeLinecap",
385
+ "strokeLinejoin",
386
+ "transform",
387
+ "d",
388
+ "x",
389
+ "y",
390
+ "x1",
391
+ "y1",
392
+ "x2",
393
+ "y2",
394
+ "points",
395
+ "cx",
396
+ "cy",
397
+ "r",
398
+ "rx",
399
+ "ry",
400
+ "dataMmlNode",
401
+ "dataC"
402
+ ];
403
+ var MATH_SVG_SCHEMA = {
404
+ tagNames: MATH_SVG_TAGS,
405
+ attributes: { "*": MATH_SVG_ATTRIBUTES },
406
+ strip: ["script", "style", "foreignObject"],
407
+ clobberPrefix: "",
408
+ clobber: [],
409
+ protocols: {}
410
+ };
411
+ function sanitizeMathSvg(svg) {
412
+ return sanitizeHtmlFragment(svg, MATH_SVG_SCHEMA);
413
+ }
414
+
415
+ // src/document/math.ts
173
416
  var MATH_OPEN = "\uE100";
174
417
  var MATH_CLOSE = "\uE101";
175
418
  var MATH_TOKEN_RE = /\uE100(\d+)\uE101/g;
@@ -236,12 +479,12 @@ async function typesetMathSpans(spans) {
236
479
  const mathml = mj.mml(s.tex, s.display);
237
480
  const err = /<merror[\s\S]*?<mtext[^>]*>([\s\S]*?)<\/mtext>/.exec(mathml);
238
481
  if (err) {
239
- out.set(key, { mathml, error: decodeXml(err[1]) });
482
+ out.set(key, { mathml: sanitizeMathml(mathml), error: decodeXml(err[1]) });
240
483
  continue;
241
484
  }
242
- const entry = { mathml: mathml.replace(/\n\s*/g, "") };
485
+ const entry = { mathml: sanitizeMathml(mathml.replace(/\n\s*/g, "")) };
243
486
  if (s.display) {
244
- const svg = mj.svg(s.tex, true);
487
+ const svg = sanitizeMathSvg(mj.svg(s.tex, true));
245
488
  entry.svg = svg;
246
489
  entry.widthEx = Number(/width="([\d.]+)ex"/.exec(svg)?.[1] ?? 0);
247
490
  entry.heightEx = Number(/height="([\d.]+)ex"/.exec(svg)?.[1] ?? 0);
@@ -3351,7 +3594,10 @@ function mathHtml(tex, display, x, inText = false) {
3351
3594
  }
3352
3595
  var href = (id, x) => x.hrefFor ? x.hrefFor(id) : `#${id}`;
3353
3596
  function imageSrc(url, x) {
3354
- if (x.srcFor) return x.srcFor(url);
3597
+ if (x.srcFor) {
3598
+ const src = x.srcFor(url);
3599
+ return src === null ? null : safeUrl(src);
3600
+ }
3355
3601
  const resolved = x.images?.get(url);
3356
3602
  if (resolved) return resolved.dataUrl;
3357
3603
  return safeUrl(url);
@@ -3437,7 +3683,7 @@ function coverHtml(s, x) {
3437
3683
  s.title ?? ""
3438
3684
  )}</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
3685
  }
3440
- var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ")}"`;
3686
+ var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ").replace(/</g, "\\3C ").replace(/>/g, "\\3E ")}"`;
3441
3687
  function marginContent(template, s) {
3442
3688
  if (!template) return "none";
3443
3689
  return templateParts(template, s).map((p) => "field" in p ? `counter(${p.field})` : cssString(p.text)).join(" ");
@@ -3532,7 +3778,7 @@ ${[2, 3, 4, 5, 6].map((l) => `.doc-toc .toc-l${l}{padding-left:${(l - 1) * 1.2}e
3532
3778
  }
3533
3779
  function documentToHtml(tree, options = {}) {
3534
3780
  const s = tree.settings;
3535
- const x = { tree, images: options.images };
3781
+ const x = { tree, images: options.images, ...options.srcFor ? { srcFor: options.srcFor } : {} };
3536
3782
  const sections = tree.sections.map((sec) => {
3537
3783
  const cls = [
3538
3784
  "doc-section",
@@ -3555,8 +3801,8 @@ ${s.author ? `<meta name="author" content="${(0, import_html_escape.escapeHtml)(
3555
3801
  </head>
3556
3802
  <body>
3557
3803
  <main class="doc">
3558
- ${s.cover ? coverHtml(s, x) : ""}
3559
- ${sections}
3804
+ ${(options.transformBody ?? ((h) => h))(`${s.cover ? coverHtml(s, x) : ""}
3805
+ ${sections}`)}
3560
3806
  </main>
3561
3807
  </body>
3562
3808
  </html>`;
@@ -4895,6 +5141,7 @@ var import_micromark_extension_gfm = require("micromark-extension-gfm");
4895
5141
 
4896
5142
  // src/markdown.ts
4897
5143
  var import_html_escape3 = require("@ai-matrx/kit/html-escape");
5144
+ var import_source2 = require("@ai-matrx/content-ir/source");
4898
5145
  function removeThinkingContent(content) {
4899
5146
  if (!content) return "";
4900
5147
  const hide = (_match, before, after) => before.length >= 2 || after.length >= 2 ? "\n\n" : "\n";
@@ -4967,6 +5214,7 @@ function looksLikeJson(body) {
4967
5214
  return false;
4968
5215
  }
4969
5216
  }
5217
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
4970
5218
  var LINK_URL = String.raw`((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?`;
4971
5219
  var IMAGE_RE = new RegExp(String.raw`!\[([^\]]*)\]\(` + LINK_URL + String.raw`\)`, "g");
4972
5220
  var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`, "g");