@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.
@@ -548,14 +548,22 @@ interface HtmlRenderContext {
548
548
  xhtml?: boolean;
549
549
  /** Where an in-document id lives (EPUB chapters split the document). */
550
550
  hrefFor?: (id: string) => string;
551
- /** Image URL override (EPUB packages images inside the book). */
552
- srcFor?: (url: string) => string;
551
+ /** Image URL override (EPUB packages images inside the book; the Markdown→HTML engine's asset port). `null` = unavailable. */
552
+ srcFor?: (url: string) => string | null;
553
553
  }
554
554
  declare function renderBlocksHtml(blocks: Block[], x: HtmlRenderContext): string;
555
555
  /** The print stylesheet for a document's settings. Light mode, always. */
556
556
  declare function documentCss(s: DocumentSettings): string;
557
557
  interface DocumentHtmlOptions {
558
558
  images?: ImageMap | undefined;
559
+ /** Image URL override (see `HtmlRenderContext.srcFor`). */
560
+ srcFor?: ((url: string) => string | null) | undefined;
561
+ /**
562
+ * Rewrites the rendered body (cover + sections) before it is placed in the
563
+ * page — the Markdown→HTML engine renders kind atoms and applies the
564
+ * allow-list here, so the trusted page shell and its CSS stay intact.
565
+ */
566
+ transformBody?: ((html: string) => string) | undefined;
559
567
  }
560
568
  /** A complete, standalone print-grade HTML document. */
561
569
  declare function documentToHtml(tree: DocTree, options?: DocumentHtmlOptions): string;
@@ -548,14 +548,22 @@ interface HtmlRenderContext {
548
548
  xhtml?: boolean;
549
549
  /** Where an in-document id lives (EPUB chapters split the document). */
550
550
  hrefFor?: (id: string) => string;
551
- /** Image URL override (EPUB packages images inside the book). */
552
- srcFor?: (url: string) => string;
551
+ /** Image URL override (EPUB packages images inside the book; the Markdown→HTML engine's asset port). `null` = unavailable. */
552
+ srcFor?: (url: string) => string | null;
553
553
  }
554
554
  declare function renderBlocksHtml(blocks: Block[], x: HtmlRenderContext): string;
555
555
  /** The print stylesheet for a document's settings. Light mode, always. */
556
556
  declare function documentCss(s: DocumentSettings): string;
557
557
  interface DocumentHtmlOptions {
558
558
  images?: ImageMap | undefined;
559
+ /** Image URL override (see `HtmlRenderContext.srcFor`). */
560
+ srcFor?: ((url: string) => string | null) | undefined;
561
+ /**
562
+ * Rewrites the rendered body (cover + sections) before it is placed in the
563
+ * page — the Markdown→HTML engine renders kind atoms and applies the
564
+ * allow-list here, so the trusted page shell and its CSS stay intact.
565
+ */
566
+ transformBody?: ((html: string) => string) | undefined;
559
567
  }
560
568
  /** A complete, standalone print-grade HTML document. */
561
569
  declare function documentToHtml(tree: DocTree, options?: DocumentHtmlOptions): string;
package/dist/document.js CHANGED
@@ -87,6 +87,249 @@ function contentColumnWidths(b, n, avail, measure, pad = 10) {
87
87
 
88
88
  // src/document/math.ts
89
89
  import { listIslands, tokenizeSource } from "@ai-matrx/content-ir/source";
90
+
91
+ // src/safe-html.ts
92
+ import { fromHtml } from "hast-util-from-html";
93
+ import { defaultSchema, sanitize } from "hast-util-sanitize";
94
+ import { toHtml } from "hast-util-to-html";
95
+ var STRIP_WITH_CONTENT = [
96
+ "script",
97
+ "style",
98
+ "noscript",
99
+ "template",
100
+ "iframe",
101
+ "object",
102
+ "embed",
103
+ "textarea",
104
+ "select",
105
+ "head",
106
+ "title"
107
+ ];
108
+ var FORBIDDEN_TAGS = [
109
+ ...STRIP_WITH_CONTENT,
110
+ "form",
111
+ "meta",
112
+ "base",
113
+ "link",
114
+ "frame",
115
+ "frameset",
116
+ "applet",
117
+ "button",
118
+ "svg",
119
+ "math"
120
+ ];
121
+ var baseAttributes = defaultSchema.attributes ?? {};
122
+ var SAFE_HTML_SCHEMA = {
123
+ ...defaultSchema,
124
+ tagNames: (defaultSchema.tagNames ?? []).filter((t) => !FORBIDDEN_TAGS.includes(t)),
125
+ strip: STRIP_WITH_CONTENT,
126
+ // Authored anchors keep their names; the GitHub `user-content-` rewrite
127
+ // would also break `<label for>` → `<input id>` pairs.
128
+ clobberPrefix: "",
129
+ clobber: []
130
+ };
131
+ var PRINT_CLASS = /^(?:(?:matrx|print|code|doc|language)-[\w-]*|inline-code|is-checked)$/;
132
+ function withPrintClasses(defs = []) {
133
+ let found = false;
134
+ const out = defs.map((d) => {
135
+ if (Array.isArray(d) && d[0] === "className") {
136
+ found = true;
137
+ return [...d, PRINT_CLASS];
138
+ }
139
+ return d;
140
+ });
141
+ return found ? out : [...out, ["className", PRINT_CLASS]];
142
+ }
143
+ var printAttributes = Object.fromEntries(
144
+ Object.entries(baseAttributes).map(([tag, defs]) => [tag, withPrintClasses(defs)])
145
+ );
146
+ var cellAlign = ["style", /^text-align:\s*(?:left|center|right);?$/];
147
+ var PRINT_HTML_SCHEMA = {
148
+ ...SAFE_HTML_SCHEMA,
149
+ tagNames: [
150
+ .../* @__PURE__ */ new Set([...SAFE_HTML_SCHEMA.tagNames ?? [], "section", "figure", "figcaption", "label"])
151
+ ],
152
+ attributes: {
153
+ ...printAttributes,
154
+ "*": [
155
+ ...baseAttributes["*"] ?? [],
156
+ ["className", PRINT_CLASS],
157
+ "role",
158
+ "ariaLabel",
159
+ "ariaHidden",
160
+ "dataLabel",
161
+ "dataLanguage",
162
+ "dataArtifactType"
163
+ ],
164
+ // Table-cell alignment is the one inline style the converter writes.
165
+ td: withPrintClasses([...baseAttributes["td"] ?? [], [...cellAlign]]),
166
+ th: withPrintClasses([...baseAttributes["th"] ?? [], [...cellAlign]]),
167
+ // The flashcard flip is a checkbox + label pair (GitHub forces `disabled`).
168
+ input: withPrintClasses([["type", "checkbox"], "checked"]),
169
+ label: withPrintClasses(["htmlFor"])
170
+ },
171
+ required: { input: { type: "checkbox" } },
172
+ protocols: {
173
+ ...defaultSchema.protocols,
174
+ href: [...defaultSchema.protocols?.href ?? [], "tel"],
175
+ // Embedded images (`data:image/png;base64,…`) — an <img> never runs script.
176
+ src: [...defaultSchema.protocols?.src ?? [], "data"]
177
+ }
178
+ };
179
+ function sanitizeHtmlFragment(html, schema = SAFE_HTML_SCHEMA) {
180
+ if (!html) return "";
181
+ const tree = sanitize(fromHtml(html, { fragment: true }), schema);
182
+ return toHtml(tree, { characterReferences: { useNamedReferences: true } });
183
+ }
184
+ var MATHML_TAGS = [
185
+ "math",
186
+ "maction",
187
+ "annotation",
188
+ "menclose",
189
+ "merror",
190
+ "mfrac",
191
+ "mglyph",
192
+ "mi",
193
+ "mlabeledtr",
194
+ "mmultiscripts",
195
+ "mn",
196
+ "mo",
197
+ "mover",
198
+ "mpadded",
199
+ "mphantom",
200
+ "mprescripts",
201
+ "none",
202
+ "mroot",
203
+ "mrow",
204
+ "ms",
205
+ "mspace",
206
+ "msqrt",
207
+ "mstyle",
208
+ "msub",
209
+ "msubsup",
210
+ "msup",
211
+ "mtable",
212
+ "mtd",
213
+ "mtext",
214
+ "mtr",
215
+ "munder",
216
+ "munderover",
217
+ "semantics"
218
+ ];
219
+ var MATHML_ATTRIBUTES = [
220
+ "xmlns",
221
+ "display",
222
+ "displaystyle",
223
+ "mathvariant",
224
+ "mathsize",
225
+ "mathcolor",
226
+ "mathbackground",
227
+ "scriptlevel",
228
+ "dir",
229
+ "fence",
230
+ "form",
231
+ "largeop",
232
+ "lspace",
233
+ "rspace",
234
+ "movablelimits",
235
+ "separator",
236
+ "stretchy",
237
+ "symmetric",
238
+ "maxsize",
239
+ "minsize",
240
+ "accent",
241
+ "accentunder",
242
+ "align",
243
+ "columnalign",
244
+ "rowalign",
245
+ "columnspacing",
246
+ "rowspacing",
247
+ "columnlines",
248
+ "rowlines",
249
+ "frame",
250
+ "framespacing",
251
+ "equalrows",
252
+ "equalcolumns",
253
+ "linethickness",
254
+ "numalign",
255
+ "denomalign",
256
+ "bevelled",
257
+ "notation",
258
+ "width",
259
+ "height",
260
+ "depth",
261
+ "voffset",
262
+ "lquote",
263
+ "rquote",
264
+ "columnspan",
265
+ "rowspan",
266
+ "encoding",
267
+ "open",
268
+ "close",
269
+ "separators",
270
+ "actiontype",
271
+ "selection",
272
+ "mslinebreak",
273
+ "linebreak",
274
+ "indentalign",
275
+ "data*"
276
+ ];
277
+ var MATHML_SCHEMA = {
278
+ tagNames: MATHML_TAGS,
279
+ attributes: { "*": MATHML_ATTRIBUTES },
280
+ strip: ["script", "style"],
281
+ clobberPrefix: "",
282
+ clobber: [],
283
+ protocols: {}
284
+ };
285
+ function sanitizeMathml(mathml) {
286
+ return sanitizeHtmlFragment(mathml, MATHML_SCHEMA);
287
+ }
288
+ var MATH_SVG_TAGS = ["svg", "g", "path", "rect", "line", "polyline", "polygon", "circle", "ellipse", "text", "title", "desc"];
289
+ var MATH_SVG_ATTRIBUTES = [
290
+ "xmlns",
291
+ "width",
292
+ "height",
293
+ "viewBox",
294
+ "role",
295
+ "focusable",
296
+ "ariaHidden",
297
+ "ariaLabel",
298
+ "stroke",
299
+ "fill",
300
+ "strokeWidth",
301
+ "strokeLinecap",
302
+ "strokeLinejoin",
303
+ "transform",
304
+ "d",
305
+ "x",
306
+ "y",
307
+ "x1",
308
+ "y1",
309
+ "x2",
310
+ "y2",
311
+ "points",
312
+ "cx",
313
+ "cy",
314
+ "r",
315
+ "rx",
316
+ "ry",
317
+ "dataMmlNode",
318
+ "dataC"
319
+ ];
320
+ var MATH_SVG_SCHEMA = {
321
+ tagNames: MATH_SVG_TAGS,
322
+ attributes: { "*": MATH_SVG_ATTRIBUTES },
323
+ strip: ["script", "style", "foreignObject"],
324
+ clobberPrefix: "",
325
+ clobber: [],
326
+ protocols: {}
327
+ };
328
+ function sanitizeMathSvg(svg) {
329
+ return sanitizeHtmlFragment(svg, MATH_SVG_SCHEMA);
330
+ }
331
+
332
+ // src/document/math.ts
90
333
  var MATH_OPEN = "\uE100";
91
334
  var MATH_CLOSE = "\uE101";
92
335
  var MATH_TOKEN_RE = /\uE100(\d+)\uE101/g;
@@ -153,12 +396,12 @@ async function typesetMathSpans(spans) {
153
396
  const mathml = mj.mml(s.tex, s.display);
154
397
  const err = /<merror[\s\S]*?<mtext[^>]*>([\s\S]*?)<\/mtext>/.exec(mathml);
155
398
  if (err) {
156
- out.set(key, { mathml, error: decodeXml(err[1]) });
399
+ out.set(key, { mathml: sanitizeMathml(mathml), error: decodeXml(err[1]) });
157
400
  continue;
158
401
  }
159
- const entry = { mathml: mathml.replace(/\n\s*/g, "") };
402
+ const entry = { mathml: sanitizeMathml(mathml.replace(/\n\s*/g, "")) };
160
403
  if (s.display) {
161
- const svg = mj.svg(s.tex, true);
404
+ const svg = sanitizeMathSvg(mj.svg(s.tex, true));
162
405
  entry.svg = svg;
163
406
  entry.widthEx = Number(/width="([\d.]+)ex"/.exec(svg)?.[1] ?? 0);
164
407
  entry.heightEx = Number(/height="([\d.]+)ex"/.exec(svg)?.[1] ?? 0);
@@ -3268,7 +3511,10 @@ function mathHtml(tex, display, x, inText = false) {
3268
3511
  }
3269
3512
  var href = (id, x) => x.hrefFor ? x.hrefFor(id) : `#${id}`;
3270
3513
  function imageSrc(url, x) {
3271
- if (x.srcFor) return x.srcFor(url);
3514
+ if (x.srcFor) {
3515
+ const src = x.srcFor(url);
3516
+ return src === null ? null : safeUrl(src);
3517
+ }
3272
3518
  const resolved = x.images?.get(url);
3273
3519
  if (resolved) return resolved.dataUrl;
3274
3520
  return safeUrl(url);
@@ -3354,7 +3600,7 @@ function coverHtml(s, x) {
3354
3600
  s.title ?? ""
3355
3601
  )}</h1>${s.subtitle ? `<p class="doc-cover-subtitle">${escapeHtml(s.subtitle)}</p>` : ""}${s.author ? `<p class="doc-cover-author">${escapeHtml(s.author)}</p>` : ""}${s.date ? `<p class="doc-cover-date">${escapeHtml(s.date)}</p>` : ""}</section>`;
3356
3602
  }
3357
- var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ")}"`;
3603
+ var cssString = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ").replace(/</g, "\\3C ").replace(/>/g, "\\3E ")}"`;
3358
3604
  function marginContent(template, s) {
3359
3605
  if (!template) return "none";
3360
3606
  return templateParts(template, s).map((p) => "field" in p ? `counter(${p.field})` : cssString(p.text)).join(" ");
@@ -3449,7 +3695,7 @@ ${[2, 3, 4, 5, 6].map((l) => `.doc-toc .toc-l${l}{padding-left:${(l - 1) * 1.2}e
3449
3695
  }
3450
3696
  function documentToHtml(tree, options = {}) {
3451
3697
  const s = tree.settings;
3452
- const x = { tree, images: options.images };
3698
+ const x = { tree, images: options.images, ...options.srcFor ? { srcFor: options.srcFor } : {} };
3453
3699
  const sections = tree.sections.map((sec) => {
3454
3700
  const cls = [
3455
3701
  "doc-section",
@@ -3472,8 +3718,8 @@ ${s.author ? `<meta name="author" content="${escapeHtml(s.author)}">` : ""}
3472
3718
  </head>
3473
3719
  <body>
3474
3720
  <main class="doc">
3475
- ${s.cover ? coverHtml(s, x) : ""}
3476
- ${sections}
3721
+ ${(options.transformBody ?? ((h) => h))(`${s.cover ? coverHtml(s, x) : ""}
3722
+ ${sections}`)}
3477
3723
  </main>
3478
3724
  </body>
3479
3725
  </html>`;
@@ -4812,6 +5058,7 @@ import { gfm } from "micromark-extension-gfm";
4812
5058
 
4813
5059
  // src/markdown.ts
4814
5060
  import { escapeHtml as escapeHtml3 } from "@ai-matrx/kit/html-escape";
5061
+ import { findTableEnd, rowCells, tableStartsAt, unescapeCellPipes } from "@ai-matrx/content-ir/source";
4815
5062
  function removeThinkingContent(content) {
4816
5063
  if (!content) return "";
4817
5064
  const hide = (_match, before, after) => before.length >= 2 || after.length >= 2 ? "\n\n" : "\n";
@@ -4884,6 +5131,7 @@ function looksLikeJson(body) {
4884
5131
  return false;
4885
5132
  }
4886
5133
  }
5134
+ var ALLOWED_TAGS = new Set(PRINT_HTML_SCHEMA.tagNames ?? []);
4887
5135
  var LINK_URL = String.raw`((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?`;
4888
5136
  var IMAGE_RE = new RegExp(String.raw`!\[([^\]]*)\]\(` + LINK_URL + String.raw`\)`, "g");
4889
5137
  var LINK_RE = new RegExp(String.raw`\[([^\]]+)\]\(` + LINK_URL + String.raw`\)`, "g");