@bendyline/squisq-formats 2.8.7 → 2.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.
Files changed (39) hide show
  1. package/README.md +21 -0
  2. package/dist/chunk-4LIFEQKX.js +358 -0
  3. package/dist/{chunk-UXEANVBI.js → chunk-5HUHSLGA.js} +1 -84
  4. package/dist/{chunk-IYNDHW2C.js → chunk-6HUQFEP3.js} +75 -5
  5. package/dist/{chunk-BXWNU4T5.js → chunk-DTJQK7U7.js} +43 -0
  6. package/dist/{chunk-QU4EVC62.js → chunk-GFHUJ7L4.js} +1 -1
  7. package/dist/{chunk-TMOXUUTG.js → chunk-NQNIFIDP.js} +6 -4
  8. package/dist/{chunk-B6UQPW75.js → chunk-SZ3OIY6G.js} +697 -44
  9. package/dist/{chunk-KROH7FNT.js → chunk-VKLJUHC2.js} +6 -4
  10. package/dist/{chunk-C23JYVPG.js → chunk-WJTC5OD6.js} +2 -0
  11. package/dist/{chunk-ALDCFHZT.js → chunk-WVDAFJFS.js} +59 -21
  12. package/dist/chunk-YRFBWYAD.js +128 -0
  13. package/dist/{chunk-SVYTUYZM.js → chunk-ZJERBWTB.js} +6 -4
  14. package/dist/data/index.d.ts +27 -5
  15. package/dist/data/index.js +6 -2
  16. package/dist/diagrams/index.d.ts +19 -8
  17. package/dist/diagrams/index.js +7 -242
  18. package/dist/docx/index.d.ts +1 -1
  19. package/dist/docx/index.js +3 -3
  20. package/dist/epub/index.d.ts +1 -1
  21. package/dist/epub/index.js +3 -3
  22. package/dist/fontAwesome-C6FRDYTY.js +14 -0
  23. package/dist/html/index.d.ts +214 -2
  24. package/dist/html/index.js +25 -4
  25. package/dist/index.d.ts +4 -2
  26. package/dist/index.js +15 -10
  27. package/dist/{materialize-G4WC5UG3.js → materialize-ICXNO4S4.js} +4 -1
  28. package/dist/outside-in/index.d.ts +21 -2
  29. package/dist/outside-in/index.js +7 -3
  30. package/dist/outside-in/layout.d.ts +7 -1
  31. package/dist/outside-in/layout.js +3 -1
  32. package/dist/pdf/index.d.ts +1 -1
  33. package/dist/pdf/index.js +3 -3
  34. package/dist/plainHtmlIcons-KBSIJ5EN.js +62 -0
  35. package/dist/pptx/index.js +2 -2
  36. package/dist/readers-3P5A6A2A.js +17 -0
  37. package/dist/registry/index.js +1 -1
  38. package/package.json +2 -2
  39. /package/dist/{chunk-LBXBRFBZ.js → chunk-DDMPZZJF.js} +0 -0
@@ -1,3 +1,7 @@
1
+ import {
2
+ findDiagrams,
3
+ rasterizeDiagrams
4
+ } from "./chunk-4LIFEQKX.js";
1
5
  import {
2
6
  arrayBufferToBase64DataUrl,
3
7
  extractFilename,
@@ -8,8 +12,9 @@ import {
8
12
  } from "./chunk-VCZUXQUL.js";
9
13
  import {
10
14
  FootnoteIndex,
11
- footnoteIds
12
- } from "./chunk-BXWNU4T5.js";
15
+ footnoteIds,
16
+ withoutBlockTags
17
+ } from "./chunk-DTJQK7U7.js";
13
18
  import {
14
19
  imageSizeAttributes
15
20
  } from "./chunk-JZR32FON.js";
@@ -294,7 +299,9 @@ function escapeHtml(str) {
294
299
  import {
295
300
  findBlockVideoEmbed,
296
301
  isSvgIslandLang,
302
+ mediaKindForUrl,
297
303
  parseSvgIsland,
304
+ parseTimeSeconds,
298
305
  sanitizeHtmlNodes,
299
306
  svgIslandDataUrl,
300
307
  svgSourceFromDataUrl,
@@ -304,8 +311,45 @@ import {
304
311
  readFrontmatterThemeId as readFrontmatterThemeId2
305
312
  } from "@bendyline/squisq/markdown";
306
313
  import { resolveFontFamily, buildGoogleFontsUrl } from "@bendyline/squisq/schemas";
307
- import { resolveThemeForDoc } from "@bendyline/squisq/doc";
314
+ import {
315
+ isMediaAnnotationName,
316
+ parseStandaloneAnnotation,
317
+ resolveThemeForDoc
318
+ } from "@bendyline/squisq/doc";
308
319
  function markdownDocToPlainHtml(doc, options = {}) {
320
+ const { title, body, fontsLink, iconsLink, themedCss, featureCss } = renderPlainHtml(
321
+ doc,
322
+ options
323
+ );
324
+ const safeCss = escapeStyleText(`${themedCss}
325
+ ${featureCss}`);
326
+ return `<!DOCTYPE html>
327
+ <html lang="en">
328
+ <head>
329
+ <meta charset="UTF-8">
330
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
331
+ <title>${escapeHtml2(title)}</title>
332
+ ${fontsLink}${iconsLink}<style>
333
+ ${safeCss}
334
+ </style>
335
+ </head>
336
+ <body>
337
+ ${body}
338
+ </body>
339
+ </html>`;
340
+ }
341
+ function markdownDocToPlainHtmlParts(doc, options = {}) {
342
+ const { title, body, iconsLink, featureCss } = renderPlainHtml(doc, options);
343
+ return {
344
+ title,
345
+ bodyHtml: body,
346
+ featureHeadHtml: `${iconsLink}<style data-squisq>
347
+ ${escapeStyleText(featureCss)}
348
+ </style>
349
+ `
350
+ };
351
+ }
352
+ function renderPlainHtml(doc, options) {
309
353
  const {
310
354
  title = "Document",
311
355
  images,
@@ -314,7 +358,8 @@ function markdownDocToPlainHtml(doc, options = {}) {
314
358
  iconsCss,
315
359
  htmlPolicy = "sanitize",
316
360
  externalResources = "deny",
317
- videoEmbeds
361
+ videoEmbeds,
362
+ icons
318
363
  } = options;
319
364
  const resolveId = themeId ?? readFrontmatterThemeId2(doc.frontmatter);
320
365
  const theme = options.theme ?? (resolveId ? resolveThemeForDoc(doc, resolveId, options.themeRegistry) : void 0);
@@ -325,13 +370,14 @@ function markdownDocToPlainHtml(doc, options = {}) {
325
370
  htmlPolicy,
326
371
  footnotes,
327
372
  videoEmbeds,
373
+ icons,
328
374
  textColor: theme?.colors.text
329
375
  };
330
- const body = renderTopLevel(doc.children, ctx) + renderFootnotesSection(footnotes, ctx);
376
+ const blocks = withoutBlockTags(doc, { keepMedia: true }).children;
377
+ const body = renderTopLevel(blocks, ctx) + renderFootnotesSection(footnotes, ctx);
331
378
  const fontsLink = theme && externalResources === "allow" ? renderFontsLink(theme) : "";
332
- const usesIcons = docUsesIcons(doc);
333
379
  let iconsLink = "";
334
- if (usesIcons) {
380
+ if (ctx.usesFontIcons) {
335
381
  iconsLink = iconsCss ? `<style data-fa-inline>
336
382
  ${escapeStyleText(iconsCss)}
337
383
  </style>
@@ -342,40 +388,25 @@ ${escapeStyleText(iconsCss)}
342
388
  ${VIDEO_EMBED_CSS}` : "";
343
389
  const svgCss = docUsesSvgIslands(doc) ? `
344
390
  ${SVG_ISLAND_CSS}` : "";
345
- const safeCss = escapeStyleText(`${themedCss}
346
- ${FEATURE_CSS}${videoCss}${svgCss}`);
347
- return `<!DOCTYPE html>
348
- <html lang="en">
349
- <head>
350
- <meta charset="UTF-8">
351
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
352
- <title>${escapeHtml2(title)}</title>
353
- ${fontsLink}${iconsLink}<style>
354
- ${safeCss}
355
- </style>
356
- </head>
357
- <body>
358
- ${body}
359
- </body>
360
- </html>`;
391
+ const mediaCss = ctx.usesMediaPlayers ? `
392
+ ${MEDIA_PLAYER_CSS}` : "";
393
+ const iconCss = ctx.usesSvgIcons ? `
394
+ ${SVG_ICON_CSS}` : "";
395
+ return {
396
+ title,
397
+ body,
398
+ fontsLink,
399
+ iconsLink,
400
+ themedCss,
401
+ featureCss: `${FEATURE_CSS}${videoCss}${svgCss}${mediaCss}${iconCss}`
402
+ };
361
403
  }
362
404
  function escapeStyleText(css) {
363
405
  return css.replace(/</g, "\\3c ");
364
406
  }
365
407
  var FONT_AWESOME_LINK = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.3.1/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
366
408
  `;
367
- function docUsesIcons(doc) {
368
- function visit(node) {
369
- if (!node || typeof node !== "object") return false;
370
- const n = node;
371
- if (n.type === "inlineIcon") return true;
372
- if (Array.isArray(n.children)) {
373
- for (const child of n.children) if (visit(child)) return true;
374
- }
375
- return false;
376
- }
377
- return visit(doc);
378
- }
409
+ var SVG_ICON_CSS = ` .squisq-icon { display: inline-block; height: 1em; vertical-align: -0.125em; overflow: visible; }`;
379
410
  function docUsesVideoEmbeds(doc, htmlPolicy) {
380
411
  const iframeIn = (nodes) => nodes.some(
381
412
  (node) => node.type === "htmlElement" && (videoEmbedFromIframe(node) !== null || iframeIn(node.children))
@@ -416,15 +447,21 @@ function renderTopLevel(children, ctx) {
416
447
  continue;
417
448
  }
418
449
  }
419
- const video = node && node.type === "paragraph" ? findBlockVideoEmbed(node) : null;
420
- if (video) {
421
- out.push(videoEmbedToHtml(video.embed, video.title, ctx));
422
- continue;
423
- }
424
- out.push(nodeToHtml(node, ctx));
450
+ const html = topLevelBlockToHtml(node, ctx);
451
+ if (html !== null) out.push(html);
425
452
  }
426
453
  return out.join("\n");
427
454
  }
455
+ function topLevelBlockToHtml(node, ctx) {
456
+ if (node?.type !== "paragraph") return nodeToHtml(node, ctx);
457
+ const video = findBlockVideoEmbed(node);
458
+ if (video) return videoEmbedToHtml(video.embed, video.title, ctx);
459
+ const tag = parseStandaloneAnnotation(node);
460
+ if (tag?.template && isMediaAnnotationName(tag.template)) {
461
+ return mediaAnnotationToHtml(tag.template, tag.params, ctx);
462
+ }
463
+ return nodeToHtml(node, ctx);
464
+ }
428
465
  function renderFootnotesSection(footnotes, ctx) {
429
466
  if (footnotes.isEmpty) return "";
430
467
  const items = footnotes.ordered().map((fn) => {
@@ -459,7 +496,7 @@ function renderFeatureSection(heading, bodyNodes, side, ctx) {
459
496
  const headingHtml = `<${headingTag}>${childrenToHtml({ children: heading.children }, ctx)}</${headingTag}>`;
460
497
  const featured = takeFirstImage(bodyNodes);
461
498
  const media = featured.image ? renderFeatureImage(featured.image, ctx) : '<div class="squisq-feature__media squisq-feature__media--empty"></div>';
462
- const textHtml = [headingHtml, ...featured.remaining.map((n) => nodeToHtml(n, ctx))].filter((s) => s.length > 0).join("\n");
499
+ const textHtml = [headingHtml, ...featured.remaining.map((n) => topLevelBlockToHtml(n, ctx))].filter((s) => !!s).join("\n");
463
500
  const sideClass = side === "leftFeature" ? "squisq-feature--left" : "squisq-feature--right";
464
501
  return `<section class="squisq-feature ${sideClass}">
465
502
  ${media}
@@ -782,6 +819,11 @@ function nodeToHtml(node, ctx) {
782
819
  case "image": {
783
820
  const original = node.url ?? "";
784
821
  const resolved = ctx?.images?.get(original) ?? original;
822
+ const mediaKind = mediaKindForUrl(original);
823
+ if (mediaKind) {
824
+ const label = node.alt || node.title || void 0;
825
+ return mediaPlayerHtml(mediaKind, resolved, label, ctx) ?? escapeHtml2(node.alt ?? "");
826
+ }
785
827
  const safeSrc = sanitizeUrl(resolved, "media") ?? svgDataImageToHtmlSrc(resolved);
786
828
  if (!safeSrc) return escapeHtml2(node.alt ?? "");
787
829
  return `<img src="${escapeAttr(safeSrc)}" alt="${escapeAttr(node.alt ?? "")}"${imageSizeAttributes(node)} />`;
@@ -791,9 +833,16 @@ function nodeToHtml(node, ctx) {
791
833
  case "table":
792
834
  return tableToHtml(node, ctx);
793
835
  case "inlineIcon": {
836
+ const token = escapeAttr(node.token ?? `${node.family}:${node.name}`);
837
+ const svg = ctx?.icons?.get(`${node.family}:${node.name}`);
838
+ if (svg) {
839
+ ctx.usesSvgIcons = true;
840
+ const width = Number((svg.width / svg.height).toFixed(4));
841
+ return `<svg class="squisq-icon" viewBox="0 0 ${svg.width} ${svg.height}" width="${width}em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" data-icon="${token}"><path d="${escapeAttr(svg.path)}"/></svg>`;
842
+ }
843
+ if (ctx) ctx.usesFontIcons = true;
794
844
  const family = escapeAttr(node.family ?? "solid");
795
845
  const name = escapeAttr(node.name ?? "");
796
- const token = escapeAttr(node.token ?? `${node.family}:${node.name}`);
797
846
  return `<i class="fa-${family} fa-${name}" data-icon="${token}" aria-hidden="true"></i>`;
798
847
  }
799
848
  case "htmlBlock":
@@ -930,6 +979,28 @@ var VIDEO_EMBED_CSS = ` .squisq-video-embed { display: block; max-width: 720px;
930
979
  .squisq-video-embed-play::after { content: ''; position: absolute; left: 26px; top: 12px; border-style: solid; border-width: 10px 0 10px 16px; border-color: transparent transparent transparent #fff; }
931
980
  .squisq-video-embed-provider { position: absolute; left: 12px; bottom: 10px; color: #fff; font-size: 0.8em; font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); }
932
981
  .squisq-video-embed-caption { margin-top: 0.4em; font-size: 0.875em; opacity: 0.8; }`;
982
+ function mediaPlayerHtml(kind, src, label, ctx) {
983
+ const safeSrc = sanitizeUrl(src, "media");
984
+ if (!safeSrc) return null;
985
+ if (ctx) ctx.usesMediaPlayers = true;
986
+ const aria = label ? ` aria-label="${escapeAttr(label)}"` : "";
987
+ return `<${kind} class="squisq-media" src="${escapeAttr(safeSrc)}" controls preload="metadata"${aria}></${kind}>`;
988
+ }
989
+ function mediaAnnotationToHtml(template, params, ctx) {
990
+ if (!params.src) return null;
991
+ const kind = template === "video" ? "video" : "audio";
992
+ let src = ctx?.images?.get(params.src) ?? params.src;
993
+ const start = parseTimeSeconds(params.clipStart ?? "");
994
+ const end = parseTimeSeconds(params.clipEnd ?? "");
995
+ if ((start !== null || end !== null) && !src.includes("#")) {
996
+ src += `#t=${start ?? ""}${end !== null ? `,${end}` : ""}`;
997
+ }
998
+ const player = mediaPlayerHtml(kind, src, void 0, ctx);
999
+ return player ? `<figure class="squisq-media-clip">${player}</figure>` : null;
1000
+ }
1001
+ var MEDIA_PLAYER_CSS = ` .squisq-media-clip { margin: 1.25em 0; }
1002
+ video.squisq-media { display: block; width: 100%; max-width: 720px; height: auto; border-radius: 6px; }
1003
+ audio.squisq-media { display: block; width: 100%; max-width: 480px; }`;
933
1004
  function svgDataImageToHtmlSrc(url) {
934
1005
  const source = svgSourceFromDataUrl(url);
935
1006
  const result = source === null ? null : parseSvgIsland(source);
@@ -951,6 +1022,80 @@ function escapeAttr(s) {
951
1022
  return escapeHtml2(s).replace(/"/g, "&quot;");
952
1023
  }
953
1024
 
1025
+ // src/html/plainHtmlPrepare.ts
1026
+ import { isDataFilePath } from "@bendyline/squisq/doc";
1027
+ var PAGE_DIAGRAM_KINDS = [
1028
+ "mermaid",
1029
+ "diagram",
1030
+ "timeline",
1031
+ "container",
1032
+ "chart"
1033
+ ];
1034
+ function needsPlainHtmlPreparation(doc, options = {}) {
1035
+ let icons = false;
1036
+ let data = false;
1037
+ const visit = (node) => {
1038
+ if (!node || typeof node !== "object" || icons && data) return;
1039
+ const n = node;
1040
+ if (n.type === "inlineIcon") icons = true;
1041
+ if (n.type === "heading") {
1042
+ const src = n.templateAnnotation?.params?.src;
1043
+ if (src && isDataFilePath(src)) data = true;
1044
+ }
1045
+ if (Array.isArray(n.children)) n.children.forEach(visit);
1046
+ };
1047
+ doc.children.forEach(visit);
1048
+ if (icons || data && options.container) return true;
1049
+ if (!options.renderDiagram) return false;
1050
+ return findDiagrams(doc).some((diagram) => PAGE_DIAGRAM_KINDS.includes(diagram.kind));
1051
+ }
1052
+ async function prepareMarkdownDocForPlainHtml(doc, options = {}) {
1053
+ const { renderDiagram, container, signal, onWarning } = options;
1054
+ let markdownDoc = doc;
1055
+ if (container) {
1056
+ const { materializeDataReferences, DATA_PREVIEW_ROWS } = await import("./materialize-ICXNO4S4.js");
1057
+ markdownDoc = await materializeDataReferences(markdownDoc, container, onWarning, {
1058
+ maxRows: options.maxDataRows ?? DATA_PREVIEW_ROWS,
1059
+ tableCache: options.dataTableCache
1060
+ });
1061
+ }
1062
+ signal?.throwIfAborted();
1063
+ const { plainHtmlIcons } = await import("./plainHtmlIcons-KBSIJ5EN.js");
1064
+ const icons = await plainHtmlIcons(markdownDoc);
1065
+ const images = /* @__PURE__ */ new Map();
1066
+ if (!renderDiagram) return { markdownDoc, images, icons };
1067
+ const pictured = await rasterizeDiagrams(markdownDoc, renderDiagram, {
1068
+ kinds: PAGE_DIAGRAM_KINDS,
1069
+ urlPrefix: "squisq-picture",
1070
+ signal,
1071
+ onWarning
1072
+ });
1073
+ const widths = /* @__PURE__ */ new Map();
1074
+ for (const [url, picture] of pictured.images) {
1075
+ const bytes = picture.data;
1076
+ const buffer = bytes.buffer.slice(
1077
+ bytes.byteOffset,
1078
+ bytes.byteOffset + bytes.byteLength
1079
+ );
1080
+ images.set(url, arrayBufferToBase64DataUrl(buffer, picture.contentType));
1081
+ widths.set(url, Math.round(picture.width));
1082
+ }
1083
+ return { markdownDoc: withPictureWidths(pictured.markdownDoc, widths), images, icons };
1084
+ }
1085
+ function withPictureWidths(doc, widths) {
1086
+ if (widths.size === 0) return doc;
1087
+ const visit = (node) => {
1088
+ if (node.type === "image") {
1089
+ const width = widths.get(node.url);
1090
+ return width === void 0 ? node : { ...node, width };
1091
+ }
1092
+ const children = node.children;
1093
+ if (!Array.isArray(children)) return node;
1094
+ return { ...node, children: children.map(visit) };
1095
+ };
1096
+ return { ...doc, children: doc.children.map(visit) };
1097
+ }
1098
+
954
1099
  // src/html/plainHtmlBundle.ts
955
1100
  import JSZip from "jszip";
956
1101
  import { parseMarkdown, inferDocumentTitle } from "@bendyline/squisq/markdown";
@@ -1177,6 +1322,502 @@ function titleForFilename(path, mdDoc) {
1177
1322
  return base.replace(/\.md$/i, "");
1178
1323
  }
1179
1324
 
1325
+ // src/html/pageTemplate.ts
1326
+ var HTML_PAGE_TEMPLATE_CONTENT_START = "<!-- squisq:content -->";
1327
+ var HTML_PAGE_TEMPLATE_CONTENT_END = "<!-- /squisq:content -->";
1328
+ var HtmlPageTemplateError = class extends Error {
1329
+ constructor(issues) {
1330
+ super(
1331
+ issues.length === 1 ? `Invalid page template: ${describeIssue(issues[0])}` : `Invalid page template:
1332
+ ${issues.map((issue) => `- ${describeIssue(issue)}`).join("\n")}`
1333
+ );
1334
+ this.name = "HtmlPageTemplateError";
1335
+ this.issues = issues;
1336
+ }
1337
+ };
1338
+ function isHtmlPageTemplate(html) {
1339
+ return scanHtml(html).some(isLightSlot);
1340
+ }
1341
+ function inspectHtmlPageTemplate(html) {
1342
+ return analyzeTemplate(html).inspection;
1343
+ }
1344
+ function analyzeTemplate(html) {
1345
+ const tags = scanHtml(html);
1346
+ const { slots, issues } = collectSlots(tags);
1347
+ const lines = lineIndex(html);
1348
+ const errors = issues.map(({ message, offset }) => ({ message, line: lines(offset) }));
1349
+ const defaultSlots = slots.filter((slot) => slot.name === null);
1350
+ if (defaultSlots.length === 0) {
1351
+ errors.push({
1352
+ message: "The template has no unnamed <slot> for the document content.",
1353
+ line: 1
1354
+ });
1355
+ } else if (defaultSlots.length > 1) {
1356
+ for (const extra of defaultSlots.slice(1)) {
1357
+ errors.push({
1358
+ message: "The template has more than one unnamed <slot>; the content can appear only once.",
1359
+ line: lines(extra.start)
1360
+ });
1361
+ }
1362
+ }
1363
+ const namedSlots = [];
1364
+ for (const slot of slots) {
1365
+ if (slot.name !== null && !namedSlots.includes(slot.name)) namedSlots.push(slot.name);
1366
+ }
1367
+ return {
1368
+ tags,
1369
+ slots,
1370
+ inspection: {
1371
+ isTemplate: tags.some(isLightSlot),
1372
+ defaultSlotCount: defaultSlots.length,
1373
+ namedSlots,
1374
+ errors: errors.sort((a, b) => a.line - b.line)
1375
+ }
1376
+ };
1377
+ }
1378
+ function isLightSlot(tag) {
1379
+ return tag.kind === "start" && tag.name === "slot" && !tag.inert;
1380
+ }
1381
+ function applyHtmlPageTemplate(template, content, options = {}) {
1382
+ const { tags, slots, inspection } = analyzeTemplate(template);
1383
+ if (inspection.errors.length > 0) throw new HtmlPageTemplateError(inspection.errors);
1384
+ const values = content.values ?? {};
1385
+ const valueFor = (name) => name === "title" ? content.title : values[name];
1386
+ const edits = [];
1387
+ const replacements = /* @__PURE__ */ new Set();
1388
+ for (const slot of slots) {
1389
+ const value = slot.name === null ? void 0 : valueFor(slot.name);
1390
+ if (slot.name === null || value !== void 0) {
1391
+ const replacement = {
1392
+ start: slot.start,
1393
+ end: slot.end,
1394
+ text: slot.name === null ? `${HTML_PAGE_TEMPLATE_CONTENT_START}
1395
+ ${content.bodyHtml}
1396
+ ${HTML_PAGE_TEMPLATE_CONTENT_END}` : escapeHtml3(value ?? "")
1397
+ };
1398
+ edits.push(replacement);
1399
+ replacements.add(replacement);
1400
+ } else {
1401
+ edits.push({ start: slot.start, end: slot.contentStart, text: "" });
1402
+ if (slot.end > slot.contentEnd)
1403
+ edits.push({ start: slot.contentEnd, end: slot.end, text: "" });
1404
+ }
1405
+ }
1406
+ const title = tags.find(
1407
+ (tag) => tag.kind === "start" && tag.name === "title" && !tag.inert
1408
+ );
1409
+ if (title?.text) {
1410
+ edits.push({
1411
+ start: title.text.start,
1412
+ end: title.text.end,
1413
+ text: fillTitleText(template.slice(title.text.start, title.text.end), valueFor)
1414
+ });
1415
+ }
1416
+ for (const tag of tags) {
1417
+ if (tag.kind !== "start" || tag.name !== "meta" || tag.inert) continue;
1418
+ const key = (attributeValue(tag, "name") ?? attributeValue(tag, "property"))?.toLowerCase();
1419
+ const value = key ? metaValue(key, content.title, values.description) : void 0;
1420
+ if (value === void 0) continue;
1421
+ const contentAttribute = tag.attributes.find((attribute) => attribute.name === "content");
1422
+ const text = `content="${escapeAttr2(value)}"`;
1423
+ if (contentAttribute) {
1424
+ edits.push({ start: contentAttribute.start, end: contentAttribute.end, text });
1425
+ } else {
1426
+ edits.push({ start: tag.closeStart, end: tag.closeStart, text: ` ${text}` });
1427
+ }
1428
+ }
1429
+ const fromDir = options.templatePath === void 0 ? null : posixDirname2(options.templatePath);
1430
+ const toDir = options.outputPath === void 0 ? null : posixDirname2(options.outputPath);
1431
+ if (fromDir !== null && toDir !== null && fromDir !== toDir) {
1432
+ for (const tag of tags) {
1433
+ if (tag.kind !== "start") continue;
1434
+ for (const attribute of tag.attributes) {
1435
+ if (attribute.value === null) continue;
1436
+ const rewrite = URL_ATTRIBUTES.has(attribute.name) ? rebaseUrl : SRCSET_ATTRIBUTES.has(attribute.name) ? rebaseSrcset : null;
1437
+ if (!rewrite) continue;
1438
+ const raw = template.slice(attribute.valueStart, attribute.valueEnd);
1439
+ const rebased = rewrite(raw, fromDir, toDir);
1440
+ if (rebased !== raw) {
1441
+ edits.push({ start: attribute.valueStart, end: attribute.valueEnd, text: rebased });
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ const headInsert = `${content.headHtml ?? ""}${title ? "" : `<title>${escapeHtml3(content.title)}</title>
1447
+ `}`;
1448
+ if (headInsert) {
1449
+ const at = headInsertionPoint(tags);
1450
+ edits.push({ start: at, end: at, text: headInsert });
1451
+ }
1452
+ const live = edits.filter(
1453
+ (edit) => replacements.has(edit) || ![...replacements].some(
1454
+ (slot) => edit.end > edit.start ? edit.start >= slot.start && edit.end <= slot.end : edit.start > slot.start && edit.start < slot.end
1455
+ )
1456
+ );
1457
+ return applyEdits(template, live);
1458
+ }
1459
+ function extractHtmlPageTemplateContent(html) {
1460
+ const start = html.indexOf(HTML_PAGE_TEMPLATE_CONTENT_START);
1461
+ if (start < 0) return null;
1462
+ const from = start + HTML_PAGE_TEMPLATE_CONTENT_START.length;
1463
+ const end = html.lastIndexOf(HTML_PAGE_TEMPLATE_CONTENT_END);
1464
+ return end < from ? null : html.slice(from, end);
1465
+ }
1466
+ var TEXT_ELEMENTS = /* @__PURE__ */ new Set([
1467
+ "script",
1468
+ "style",
1469
+ "title",
1470
+ "textarea",
1471
+ "xmp",
1472
+ "iframe",
1473
+ "noembed",
1474
+ "noframes",
1475
+ "noscript"
1476
+ ]);
1477
+ function scanHtml(html) {
1478
+ const tags = [];
1479
+ let templateDepth = 0;
1480
+ let foreignDepth = 0;
1481
+ let index = 0;
1482
+ while (index < html.length) {
1483
+ const open = html.indexOf("<", index);
1484
+ if (open < 0) break;
1485
+ const next = html[open + 1];
1486
+ if (html.startsWith("<!--", open)) {
1487
+ const close = html.indexOf("-->", open + 4);
1488
+ index = close < 0 ? html.length : close + 3;
1489
+ } else if (next === "!" || next === "?") {
1490
+ const close = html.indexOf(">", open + 2);
1491
+ index = close < 0 ? html.length : close + 1;
1492
+ } else if (next === "/" && isAsciiLetter(html[open + 2])) {
1493
+ const nameEnd = scanName(html, open + 2);
1494
+ const name = html.slice(open + 2, nameEnd).toLowerCase();
1495
+ const close = html.indexOf(">", nameEnd);
1496
+ const end = close < 0 ? html.length : close + 1;
1497
+ if (name === "template" && templateDepth > 0) templateDepth--;
1498
+ if ((name === "svg" || name === "math") && foreignDepth > 0) foreignDepth--;
1499
+ tags.push({ kind: "end", name, start: open, end, inert: templateDepth + foreignDepth > 0 });
1500
+ index = end;
1501
+ } else if (isAsciiLetter(next)) {
1502
+ const tag = scanStartTag(html, open, templateDepth + foreignDepth > 0);
1503
+ tags.push(tag);
1504
+ index = tag.end;
1505
+ if (tag.name === "template") templateDepth++;
1506
+ else if ((tag.name === "svg" || tag.name === "math") && !tag.selfClosing) foreignDepth++;
1507
+ else if (TEXT_ELEMENTS.has(tag.name) && foreignDepth === 0) {
1508
+ const closing = new RegExp(`</${tag.name}(?=[\\s/>])`, "gi");
1509
+ closing.lastIndex = tag.end;
1510
+ const match = closing.exec(html);
1511
+ const textEnd = match ? match.index : html.length;
1512
+ tag.text = { start: tag.end, end: textEnd };
1513
+ index = textEnd;
1514
+ }
1515
+ } else {
1516
+ index = open + 1;
1517
+ }
1518
+ }
1519
+ return tags;
1520
+ }
1521
+ function scanStartTag(html, start, inert) {
1522
+ const nameEnd = scanName(html, start + 1);
1523
+ const name = html.slice(start + 1, nameEnd).toLowerCase();
1524
+ const attributes = [];
1525
+ let index = nameEnd;
1526
+ for (; ; ) {
1527
+ index = skipSpace(html, index);
1528
+ if (index >= html.length) {
1529
+ return {
1530
+ kind: "start",
1531
+ name,
1532
+ start,
1533
+ end: html.length,
1534
+ closeStart: html.length,
1535
+ selfClosing: false,
1536
+ attributes,
1537
+ inert
1538
+ };
1539
+ }
1540
+ const char = html[index];
1541
+ if (char === ">") {
1542
+ return {
1543
+ kind: "start",
1544
+ name,
1545
+ start,
1546
+ end: index + 1,
1547
+ closeStart: index,
1548
+ selfClosing: false,
1549
+ attributes,
1550
+ inert
1551
+ };
1552
+ }
1553
+ if (char === "/") {
1554
+ if (html[index + 1] === ">") {
1555
+ return {
1556
+ kind: "start",
1557
+ name,
1558
+ start,
1559
+ end: index + 2,
1560
+ closeStart: index,
1561
+ selfClosing: true,
1562
+ attributes,
1563
+ inert
1564
+ };
1565
+ }
1566
+ index++;
1567
+ continue;
1568
+ }
1569
+ const attributeStart = index;
1570
+ index++;
1571
+ while (index < html.length && !isSpace(html[index]) && !"/>=".includes(html[index])) index++;
1572
+ const attributeName = html.slice(attributeStart, index).toLowerCase();
1573
+ const afterName = index;
1574
+ index = skipSpace(html, index);
1575
+ if (html[index] !== "=") {
1576
+ attributes.push({
1577
+ name: attributeName,
1578
+ value: null,
1579
+ start: attributeStart,
1580
+ end: afterName,
1581
+ valueStart: afterName,
1582
+ valueEnd: afterName
1583
+ });
1584
+ continue;
1585
+ }
1586
+ index = skipSpace(html, index + 1);
1587
+ const quote = html[index];
1588
+ let valueStart;
1589
+ let valueEnd;
1590
+ if (quote === '"' || quote === "'") {
1591
+ valueStart = index + 1;
1592
+ const close = html.indexOf(quote, valueStart);
1593
+ valueEnd = close < 0 ? html.length : close;
1594
+ index = close < 0 ? html.length : close + 1;
1595
+ } else {
1596
+ valueStart = index;
1597
+ while (index < html.length && !isSpace(html[index]) && html[index] !== ">") index++;
1598
+ valueEnd = index;
1599
+ }
1600
+ attributes.push({
1601
+ name: attributeName,
1602
+ value: html.slice(valueStart, valueEnd),
1603
+ start: attributeStart,
1604
+ end: index,
1605
+ valueStart,
1606
+ valueEnd
1607
+ });
1608
+ }
1609
+ }
1610
+ function scanName(html, from) {
1611
+ let index = from;
1612
+ while (index < html.length && !isSpace(html[index]) && html[index] !== "/" && html[index] !== ">") {
1613
+ index++;
1614
+ }
1615
+ return index;
1616
+ }
1617
+ function skipSpace(html, from) {
1618
+ let index = from;
1619
+ while (index < html.length && isSpace(html[index])) index++;
1620
+ return index;
1621
+ }
1622
+ function isSpace(char) {
1623
+ return char === " " || char === "\n" || char === " " || char === "\r" || char === "\f";
1624
+ }
1625
+ function isAsciiLetter(char) {
1626
+ return char !== void 0 && /^[A-Za-z]$/.test(char);
1627
+ }
1628
+ function attributeValue(tag, name) {
1629
+ return tag.attributes.find((attribute) => attribute.name === name)?.value ?? void 0;
1630
+ }
1631
+ function collectSlots(tags) {
1632
+ const slots = [];
1633
+ const issues = [];
1634
+ let open = null;
1635
+ for (const tag of tags) {
1636
+ if (tag.name !== "slot" || tag.inert) continue;
1637
+ if (tag.kind === "start") {
1638
+ if (open) {
1639
+ issues.push({
1640
+ message: "A <slot> cannot be placed inside another <slot>.",
1641
+ offset: tag.start
1642
+ });
1643
+ continue;
1644
+ }
1645
+ if (tag.selfClosing) {
1646
+ slots.push({
1647
+ name: slotName(tag),
1648
+ start: tag.start,
1649
+ end: tag.end,
1650
+ contentStart: tag.end,
1651
+ contentEnd: tag.end
1652
+ });
1653
+ } else {
1654
+ open = tag;
1655
+ }
1656
+ } else if (open) {
1657
+ slots.push({
1658
+ name: slotName(open),
1659
+ start: open.start,
1660
+ end: tag.end,
1661
+ contentStart: open.end,
1662
+ contentEnd: tag.start
1663
+ });
1664
+ open = null;
1665
+ } else {
1666
+ issues.push({ message: "A </slot> has no matching <slot>.", offset: tag.start });
1667
+ }
1668
+ }
1669
+ if (open) {
1670
+ issues.push({
1671
+ message: "A <slot> is never closed; write <slot></slot> or <slot/>.",
1672
+ offset: open.start
1673
+ });
1674
+ }
1675
+ return { slots, issues };
1676
+ }
1677
+ function slotName(tag) {
1678
+ const name = attributeValue(tag, "name")?.trim();
1679
+ return name ? name : null;
1680
+ }
1681
+ function fillTitleText(text, valueFor) {
1682
+ const { slots } = collectSlots(scanHtml(text));
1683
+ if (slots.length === 0) return escapeHtml3(valueFor("title") ?? "");
1684
+ const edits = slots.map((slot) => {
1685
+ const value = valueFor(slot.name ?? "title");
1686
+ return {
1687
+ start: slot.start,
1688
+ end: slot.end,
1689
+ text: value === void 0 ? text.slice(slot.contentStart, slot.contentEnd) : escapeHtml3(value)
1690
+ };
1691
+ });
1692
+ return applyEdits(text, edits);
1693
+ }
1694
+ var TITLE_META = /* @__PURE__ */ new Set(["og:title", "twitter:title"]);
1695
+ var DESCRIPTION_META = /* @__PURE__ */ new Set(["description", "og:description", "twitter:description"]);
1696
+ function metaValue(key, title, description) {
1697
+ if (TITLE_META.has(key)) return title;
1698
+ if (DESCRIPTION_META.has(key)) return description;
1699
+ return void 0;
1700
+ }
1701
+ function headInsertionPoint(tags) {
1702
+ for (const tag of tags) {
1703
+ if (tag.inert) continue;
1704
+ if (tag.kind === "end" && tag.name === "head") return tag.start;
1705
+ if (tag.kind !== "start") continue;
1706
+ if (tag.name === "body") return tag.start;
1707
+ if (tag.name === "style") return tag.start;
1708
+ if (tag.name === "link" && (attributeValue(tag, "rel") ?? "").toLowerCase().split(/\s+/).includes("stylesheet")) {
1709
+ return tag.start;
1710
+ }
1711
+ }
1712
+ const container = tags.find(
1713
+ (tag) => tag.kind === "start" && (tag.name === "head" || tag.name === "html")
1714
+ );
1715
+ return container?.end ?? 0;
1716
+ }
1717
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
1718
+ "href",
1719
+ "src",
1720
+ "poster",
1721
+ "action",
1722
+ "formaction",
1723
+ "cite",
1724
+ "data",
1725
+ "background",
1726
+ "xlink:href"
1727
+ ]);
1728
+ var SRCSET_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset"]);
1729
+ function rebaseUrl(raw, fromDir, toDir) {
1730
+ const url = raw.trim();
1731
+ if (!url || url.startsWith("#") || url.startsWith("?") || url.startsWith("/") || url.startsWith("\\") || /^[a-z][a-z0-9+.-]*:/i.test(url) || url.includes("{{")) {
1732
+ return raw;
1733
+ }
1734
+ const cut = url.search(/[?#]/);
1735
+ const path = cut < 0 ? url : url.slice(0, cut);
1736
+ const suffix = cut < 0 ? "" : url.slice(cut);
1737
+ if (path.includes("&")) return raw;
1738
+ const target = normalizePath(fromDir ? `${fromDir}/${path}` : path);
1739
+ if (target === null) return raw;
1740
+ const isDirectory = path.endsWith("/") || /(?:^|\/)\.\.?$/.test(path);
1741
+ let relative = relativeTo(toDir, target);
1742
+ if (isDirectory) relative = relative ? `${relative}/` : "./";
1743
+ const leading = raw.slice(0, raw.length - raw.trimStart().length);
1744
+ const trailing = raw.slice(raw.trimEnd().length);
1745
+ return `${leading}${relative}${suffix}${trailing}`;
1746
+ }
1747
+ function rebaseSrcset(raw, fromDir, toDir) {
1748
+ let out = "";
1749
+ let index = 0;
1750
+ while (index < raw.length) {
1751
+ const separator = /^[\s,]*/.exec(raw.slice(index))?.[0] ?? "";
1752
+ out += separator;
1753
+ index += separator.length;
1754
+ if (index >= raw.length) break;
1755
+ let urlEnd = index;
1756
+ while (urlEnd < raw.length && !isSpace(raw[urlEnd])) urlEnd++;
1757
+ let url = raw.slice(index, urlEnd);
1758
+ const commas = /,+$/.exec(url)?.[0] ?? "";
1759
+ url = url.slice(0, url.length - commas.length);
1760
+ out += rebaseUrl(url, fromDir, toDir) + commas;
1761
+ index = urlEnd;
1762
+ if (commas) continue;
1763
+ let descriptorEnd = index;
1764
+ let depth = 0;
1765
+ while (descriptorEnd < raw.length) {
1766
+ const char = raw[descriptorEnd];
1767
+ if (char === "(") depth++;
1768
+ else if (char === ")") depth = Math.max(0, depth - 1);
1769
+ else if (char === "," && depth === 0) break;
1770
+ descriptorEnd++;
1771
+ }
1772
+ out += raw.slice(index, descriptorEnd);
1773
+ index = descriptorEnd;
1774
+ }
1775
+ return out;
1776
+ }
1777
+ function relativeTo(fromDir, target) {
1778
+ const from = fromDir ? fromDir.split("/") : [];
1779
+ const to = target ? target.split("/") : [];
1780
+ let shared = 0;
1781
+ while (shared < from.length && shared < to.length && from[shared] === to[shared]) shared++;
1782
+ return [...from.slice(shared).map(() => ".."), ...to.slice(shared)].join("/");
1783
+ }
1784
+ function posixDirname2(path) {
1785
+ const normalized = normalizePath(path.replace(/\\/g, "/")) ?? "";
1786
+ const slash = normalized.lastIndexOf("/");
1787
+ return slash < 0 ? "" : normalized.slice(0, slash);
1788
+ }
1789
+ function applyEdits(source, edits) {
1790
+ const ordered = [...edits].sort(
1791
+ (a, b) => a.start - b.start || Number(a.end > a.start) - Number(b.end > b.start)
1792
+ );
1793
+ let out = "";
1794
+ let cursor = 0;
1795
+ for (const edit of ordered) {
1796
+ if (edit.start < cursor) continue;
1797
+ out += source.slice(cursor, edit.start) + edit.text;
1798
+ cursor = edit.end;
1799
+ }
1800
+ return out + source.slice(cursor);
1801
+ }
1802
+ function lineIndex(source) {
1803
+ return (offset) => {
1804
+ let line = 1;
1805
+ for (let index = source.indexOf("\n"); index >= 0 && index < offset; index = source.indexOf("\n", index + 1)) {
1806
+ line++;
1807
+ }
1808
+ return line;
1809
+ };
1810
+ }
1811
+ function describeIssue(issue) {
1812
+ return `${issue.message} (line ${issue.line})`;
1813
+ }
1814
+ function escapeHtml3(s) {
1815
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1816
+ }
1817
+ function escapeAttr2(s) {
1818
+ return escapeHtml3(s).replace(/"/g, "&quot;");
1819
+ }
1820
+
1180
1821
  // src/html/docsHtmlBundle.ts
1181
1822
  import JSZip2 from "jszip";
1182
1823
  import { parseMarkdown as parseMarkdown2, inferDocumentTitle as inferDocumentTitle2 } from "@bendyline/squisq/markdown";
@@ -1997,11 +2638,13 @@ function htmlToMarkdownDocSync(html, options = {}) {
1997
2638
  const embedded = findEmbeddedSquisqDoc(nodes);
1998
2639
  if (embedded) return docToMarkdown(parseEmbeddedSquisqDoc(embedded));
1999
2640
  const headMetadata = options.headMetadata === false ? {} : readHeadMetadata(nodes);
2641
+ const templated = extractHtmlPageTemplateContent(html);
2642
+ const contentNodes = templated === null ? nodes : parseHtmlToNodes(templated);
2000
2643
  const {
2001
2644
  nodes: bodyNodes,
2002
2645
  bodies,
2003
2646
  identifiers
2004
- } = extractFootnoteSection(dropContentlessElements(nodes));
2647
+ } = extractFootnoteSection(dropContentlessElements(contentNodes));
2005
2648
  const clean = (input) => options.sanitize === false ? input : sanitizeHtmlNodes2(input);
2006
2649
  const children = blocksFromNodes(clean(bodyNodes));
2007
2650
  if (bodies.size > 0) {
@@ -2121,8 +2764,18 @@ export {
2121
2764
  collectImagePaths,
2122
2765
  generateExternalHtml,
2123
2766
  markdownDocToPlainHtml,
2767
+ markdownDocToPlainHtmlParts,
2768
+ needsPlainHtmlPreparation,
2769
+ prepareMarkdownDocForPlainHtml,
2124
2770
  markdownDocsToPlainHtmlBundle,
2125
2771
  collectLinkRefs,
2772
+ HTML_PAGE_TEMPLATE_CONTENT_START,
2773
+ HTML_PAGE_TEMPLATE_CONTENT_END,
2774
+ HtmlPageTemplateError,
2775
+ isHtmlPageTemplate,
2776
+ inspectHtmlPageTemplate,
2777
+ applyHtmlPageTemplate,
2778
+ extractHtmlPageTemplateContent,
2126
2779
  markdownDocsToHtmlBundle,
2127
2780
  htmlToMarkdownDocSync,
2128
2781
  htmlToMarkdownDoc,