@bendyline/squisq-formats 2.8.7 → 2.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/README.md CHANGED
@@ -141,6 +141,27 @@ const page = markdownDocToPlainHtml(markdownDoc, { themeId: 'warm-earth' });
141
141
  const imported = await htmlToMarkdownDoc(htmlString);
142
142
  ```
143
143
 
144
+ To place a document inside your own page — a site header, navigation, footer, and stylesheet — write the page as an HTML template with standard `<slot>` elements and fill it:
145
+
146
+ ```ts
147
+ import { applyHtmlPageTemplate, markdownDocToPlainHtmlParts } from '@bendyline/squisq-formats/html';
148
+
149
+ // template: <title><slot name="title">Post</slot> · My Blog</title> … <h1><slot name="title"></slot></h1> <slot></slot>
150
+ const parts = markdownDocToPlainHtmlParts(markdownDoc);
151
+ const page = applyHtmlPageTemplate(
152
+ template,
153
+ {
154
+ bodyHtml: parts.bodyHtml,
155
+ headHtml: parts.featureHeadHtml,
156
+ title: 'Hello',
157
+ values: { date: '2026-10-28' },
158
+ },
159
+ { templatePath: '_templates/post.html', outputPath: 'posts/hello.html' },
160
+ );
161
+ ```
162
+
163
+ The unnamed `<slot>` receives the body (between `<!-- squisq:content -->` markers, which `htmlToMarkdownDoc` honors on re-import), named slots receive text values or keep their fallback children, relative URLs are rebased from the template's folder to the page's, and everything else in the template is copied byte for byte.
164
+
144
165
  The subpath also exports `markdownDocsToPlainHtmlBundle` / `markdownDocsToHtmlBundle` (recursive multi-doc ZIP bundles with `.md` links rewritten to `.html`), `htmlToMarkdownDocSync` / `htmlToMarkdown`, and helpers (`collectImagePaths`, `collectLinkRefs`, `inferMimeType`).
145
166
 
146
167
  ### EPUB
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-ALDCFHZT.js";
4
+ } from "./chunk-GDUZJ75A.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
8
8
  import {
9
9
  OUTSIDE_IN_FORMAT_IDS,
10
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
10
11
  resolveOutsideInLayout
11
- } from "./chunk-C23JYVPG.js";
12
+ } from "./chunk-WJTC5OD6.js";
12
13
 
13
14
  // src/outside-in/index.ts
14
15
  import {
@@ -63,6 +64,17 @@ function isOutsideInMarkdownEditingEnabled(source) {
63
64
  const frontmatter = yaml === null ? null : parseFrontmatter(yaml);
64
65
  return frontmatter?.[OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY] === true;
65
66
  }
67
+ function readOutsideInOutputTemplate(source) {
68
+ const frontmatter = typeof source === "string" ? (() => {
69
+ const yaml = rawFrontmatter(source);
70
+ return yaml === null ? null : parseFrontmatter(yaml);
71
+ })() : source.frontmatter;
72
+ const value = frontmatter?.[OUTSIDE_IN_OUTPUT_TEMPLATE_KEY];
73
+ if (typeof value !== "string") return null;
74
+ const trimmed = value.trim();
75
+ if (!trimmed) return null;
76
+ return trimmed.toLowerCase() === "none" ? "none" : trimmed;
77
+ }
66
78
  function withOutsideInMarkdownEditing(source, layout, enabled = true) {
67
79
  return setFrontmatterValues(withOutsideInMetadata(source, layout), {
68
80
  [OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY]: enabled
@@ -205,6 +217,7 @@ export {
205
217
  readOutsideInMetadata,
206
218
  withOutsideInMetadata,
207
219
  isOutsideInMarkdownEditingEnabled,
220
+ readOutsideInOutputTemplate,
208
221
  withOutsideInMarkdownEditing,
209
222
  importOutsideInDocument,
210
223
  renderOutsideInDocument
@@ -306,6 +306,39 @@ import {
306
306
  import { resolveFontFamily, buildGoogleFontsUrl } from "@bendyline/squisq/schemas";
307
307
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
308
308
  function markdownDocToPlainHtml(doc, options = {}) {
309
+ const { title, body, fontsLink, iconsLink, themedCss, featureCss } = renderPlainHtml(
310
+ doc,
311
+ options
312
+ );
313
+ const safeCss = escapeStyleText(`${themedCss}
314
+ ${featureCss}`);
315
+ return `<!DOCTYPE html>
316
+ <html lang="en">
317
+ <head>
318
+ <meta charset="UTF-8">
319
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
320
+ <title>${escapeHtml2(title)}</title>
321
+ ${fontsLink}${iconsLink}<style>
322
+ ${safeCss}
323
+ </style>
324
+ </head>
325
+ <body>
326
+ ${body}
327
+ </body>
328
+ </html>`;
329
+ }
330
+ function markdownDocToPlainHtmlParts(doc, options = {}) {
331
+ const { title, body, iconsLink, featureCss } = renderPlainHtml(doc, options);
332
+ return {
333
+ title,
334
+ bodyHtml: body,
335
+ featureHeadHtml: `${iconsLink}<style data-squisq>
336
+ ${escapeStyleText(featureCss)}
337
+ </style>
338
+ `
339
+ };
340
+ }
341
+ function renderPlainHtml(doc, options) {
309
342
  const {
310
343
  title = "Document",
311
344
  images,
@@ -342,22 +375,14 @@ ${escapeStyleText(iconsCss)}
342
375
  ${VIDEO_EMBED_CSS}` : "";
343
376
  const svgCss = docUsesSvgIslands(doc) ? `
344
377
  ${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>`;
378
+ return {
379
+ title,
380
+ body,
381
+ fontsLink,
382
+ iconsLink,
383
+ themedCss,
384
+ featureCss: `${FEATURE_CSS}${videoCss}${svgCss}`
385
+ };
361
386
  }
362
387
  function escapeStyleText(css) {
363
388
  return css.replace(/</g, "\\3c ");
@@ -1177,6 +1202,502 @@ function titleForFilename(path, mdDoc) {
1177
1202
  return base.replace(/\.md$/i, "");
1178
1203
  }
1179
1204
 
1205
+ // src/html/pageTemplate.ts
1206
+ var HTML_PAGE_TEMPLATE_CONTENT_START = "<!-- squisq:content -->";
1207
+ var HTML_PAGE_TEMPLATE_CONTENT_END = "<!-- /squisq:content -->";
1208
+ var HtmlPageTemplateError = class extends Error {
1209
+ constructor(issues) {
1210
+ super(
1211
+ issues.length === 1 ? `Invalid page template: ${describeIssue(issues[0])}` : `Invalid page template:
1212
+ ${issues.map((issue) => `- ${describeIssue(issue)}`).join("\n")}`
1213
+ );
1214
+ this.name = "HtmlPageTemplateError";
1215
+ this.issues = issues;
1216
+ }
1217
+ };
1218
+ function isHtmlPageTemplate(html) {
1219
+ return scanHtml(html).some(isLightSlot);
1220
+ }
1221
+ function inspectHtmlPageTemplate(html) {
1222
+ return analyzeTemplate(html).inspection;
1223
+ }
1224
+ function analyzeTemplate(html) {
1225
+ const tags = scanHtml(html);
1226
+ const { slots, issues } = collectSlots(tags);
1227
+ const lines = lineIndex(html);
1228
+ const errors = issues.map(({ message, offset }) => ({ message, line: lines(offset) }));
1229
+ const defaultSlots = slots.filter((slot) => slot.name === null);
1230
+ if (defaultSlots.length === 0) {
1231
+ errors.push({
1232
+ message: "The template has no unnamed <slot> for the document content.",
1233
+ line: 1
1234
+ });
1235
+ } else if (defaultSlots.length > 1) {
1236
+ for (const extra of defaultSlots.slice(1)) {
1237
+ errors.push({
1238
+ message: "The template has more than one unnamed <slot>; the content can appear only once.",
1239
+ line: lines(extra.start)
1240
+ });
1241
+ }
1242
+ }
1243
+ const namedSlots = [];
1244
+ for (const slot of slots) {
1245
+ if (slot.name !== null && !namedSlots.includes(slot.name)) namedSlots.push(slot.name);
1246
+ }
1247
+ return {
1248
+ tags,
1249
+ slots,
1250
+ inspection: {
1251
+ isTemplate: tags.some(isLightSlot),
1252
+ defaultSlotCount: defaultSlots.length,
1253
+ namedSlots,
1254
+ errors: errors.sort((a, b) => a.line - b.line)
1255
+ }
1256
+ };
1257
+ }
1258
+ function isLightSlot(tag) {
1259
+ return tag.kind === "start" && tag.name === "slot" && !tag.inert;
1260
+ }
1261
+ function applyHtmlPageTemplate(template, content, options = {}) {
1262
+ const { tags, slots, inspection } = analyzeTemplate(template);
1263
+ if (inspection.errors.length > 0) throw new HtmlPageTemplateError(inspection.errors);
1264
+ const values = content.values ?? {};
1265
+ const valueFor = (name) => name === "title" ? content.title : values[name];
1266
+ const edits = [];
1267
+ const replacements = /* @__PURE__ */ new Set();
1268
+ for (const slot of slots) {
1269
+ const value = slot.name === null ? void 0 : valueFor(slot.name);
1270
+ if (slot.name === null || value !== void 0) {
1271
+ const replacement = {
1272
+ start: slot.start,
1273
+ end: slot.end,
1274
+ text: slot.name === null ? `${HTML_PAGE_TEMPLATE_CONTENT_START}
1275
+ ${content.bodyHtml}
1276
+ ${HTML_PAGE_TEMPLATE_CONTENT_END}` : escapeHtml3(value ?? "")
1277
+ };
1278
+ edits.push(replacement);
1279
+ replacements.add(replacement);
1280
+ } else {
1281
+ edits.push({ start: slot.start, end: slot.contentStart, text: "" });
1282
+ if (slot.end > slot.contentEnd)
1283
+ edits.push({ start: slot.contentEnd, end: slot.end, text: "" });
1284
+ }
1285
+ }
1286
+ const title = tags.find(
1287
+ (tag) => tag.kind === "start" && tag.name === "title" && !tag.inert
1288
+ );
1289
+ if (title?.text) {
1290
+ edits.push({
1291
+ start: title.text.start,
1292
+ end: title.text.end,
1293
+ text: fillTitleText(template.slice(title.text.start, title.text.end), valueFor)
1294
+ });
1295
+ }
1296
+ for (const tag of tags) {
1297
+ if (tag.kind !== "start" || tag.name !== "meta" || tag.inert) continue;
1298
+ const key = (attributeValue(tag, "name") ?? attributeValue(tag, "property"))?.toLowerCase();
1299
+ const value = key ? metaValue(key, content.title, values.description) : void 0;
1300
+ if (value === void 0) continue;
1301
+ const contentAttribute = tag.attributes.find((attribute) => attribute.name === "content");
1302
+ const text = `content="${escapeAttr2(value)}"`;
1303
+ if (contentAttribute) {
1304
+ edits.push({ start: contentAttribute.start, end: contentAttribute.end, text });
1305
+ } else {
1306
+ edits.push({ start: tag.closeStart, end: tag.closeStart, text: ` ${text}` });
1307
+ }
1308
+ }
1309
+ const fromDir = options.templatePath === void 0 ? null : posixDirname2(options.templatePath);
1310
+ const toDir = options.outputPath === void 0 ? null : posixDirname2(options.outputPath);
1311
+ if (fromDir !== null && toDir !== null && fromDir !== toDir) {
1312
+ for (const tag of tags) {
1313
+ if (tag.kind !== "start") continue;
1314
+ for (const attribute of tag.attributes) {
1315
+ if (attribute.value === null) continue;
1316
+ const rewrite = URL_ATTRIBUTES.has(attribute.name) ? rebaseUrl : SRCSET_ATTRIBUTES.has(attribute.name) ? rebaseSrcset : null;
1317
+ if (!rewrite) continue;
1318
+ const raw = template.slice(attribute.valueStart, attribute.valueEnd);
1319
+ const rebased = rewrite(raw, fromDir, toDir);
1320
+ if (rebased !== raw) {
1321
+ edits.push({ start: attribute.valueStart, end: attribute.valueEnd, text: rebased });
1322
+ }
1323
+ }
1324
+ }
1325
+ }
1326
+ const headInsert = `${content.headHtml ?? ""}${title ? "" : `<title>${escapeHtml3(content.title)}</title>
1327
+ `}`;
1328
+ if (headInsert) {
1329
+ const at = headInsertionPoint(tags);
1330
+ edits.push({ start: at, end: at, text: headInsert });
1331
+ }
1332
+ const live = edits.filter(
1333
+ (edit) => replacements.has(edit) || ![...replacements].some(
1334
+ (slot) => edit.end > edit.start ? edit.start >= slot.start && edit.end <= slot.end : edit.start > slot.start && edit.start < slot.end
1335
+ )
1336
+ );
1337
+ return applyEdits(template, live);
1338
+ }
1339
+ function extractHtmlPageTemplateContent(html) {
1340
+ const start = html.indexOf(HTML_PAGE_TEMPLATE_CONTENT_START);
1341
+ if (start < 0) return null;
1342
+ const from = start + HTML_PAGE_TEMPLATE_CONTENT_START.length;
1343
+ const end = html.lastIndexOf(HTML_PAGE_TEMPLATE_CONTENT_END);
1344
+ return end < from ? null : html.slice(from, end);
1345
+ }
1346
+ var TEXT_ELEMENTS = /* @__PURE__ */ new Set([
1347
+ "script",
1348
+ "style",
1349
+ "title",
1350
+ "textarea",
1351
+ "xmp",
1352
+ "iframe",
1353
+ "noembed",
1354
+ "noframes",
1355
+ "noscript"
1356
+ ]);
1357
+ function scanHtml(html) {
1358
+ const tags = [];
1359
+ let templateDepth = 0;
1360
+ let foreignDepth = 0;
1361
+ let index = 0;
1362
+ while (index < html.length) {
1363
+ const open = html.indexOf("<", index);
1364
+ if (open < 0) break;
1365
+ const next = html[open + 1];
1366
+ if (html.startsWith("<!--", open)) {
1367
+ const close = html.indexOf("-->", open + 4);
1368
+ index = close < 0 ? html.length : close + 3;
1369
+ } else if (next === "!" || next === "?") {
1370
+ const close = html.indexOf(">", open + 2);
1371
+ index = close < 0 ? html.length : close + 1;
1372
+ } else if (next === "/" && isAsciiLetter(html[open + 2])) {
1373
+ const nameEnd = scanName(html, open + 2);
1374
+ const name = html.slice(open + 2, nameEnd).toLowerCase();
1375
+ const close = html.indexOf(">", nameEnd);
1376
+ const end = close < 0 ? html.length : close + 1;
1377
+ if (name === "template" && templateDepth > 0) templateDepth--;
1378
+ if ((name === "svg" || name === "math") && foreignDepth > 0) foreignDepth--;
1379
+ tags.push({ kind: "end", name, start: open, end, inert: templateDepth + foreignDepth > 0 });
1380
+ index = end;
1381
+ } else if (isAsciiLetter(next)) {
1382
+ const tag = scanStartTag(html, open, templateDepth + foreignDepth > 0);
1383
+ tags.push(tag);
1384
+ index = tag.end;
1385
+ if (tag.name === "template") templateDepth++;
1386
+ else if ((tag.name === "svg" || tag.name === "math") && !tag.selfClosing) foreignDepth++;
1387
+ else if (TEXT_ELEMENTS.has(tag.name) && foreignDepth === 0) {
1388
+ const closing = new RegExp(`</${tag.name}(?=[\\s/>])`, "gi");
1389
+ closing.lastIndex = tag.end;
1390
+ const match = closing.exec(html);
1391
+ const textEnd = match ? match.index : html.length;
1392
+ tag.text = { start: tag.end, end: textEnd };
1393
+ index = textEnd;
1394
+ }
1395
+ } else {
1396
+ index = open + 1;
1397
+ }
1398
+ }
1399
+ return tags;
1400
+ }
1401
+ function scanStartTag(html, start, inert) {
1402
+ const nameEnd = scanName(html, start + 1);
1403
+ const name = html.slice(start + 1, nameEnd).toLowerCase();
1404
+ const attributes = [];
1405
+ let index = nameEnd;
1406
+ for (; ; ) {
1407
+ index = skipSpace(html, index);
1408
+ if (index >= html.length) {
1409
+ return {
1410
+ kind: "start",
1411
+ name,
1412
+ start,
1413
+ end: html.length,
1414
+ closeStart: html.length,
1415
+ selfClosing: false,
1416
+ attributes,
1417
+ inert
1418
+ };
1419
+ }
1420
+ const char = html[index];
1421
+ if (char === ">") {
1422
+ return {
1423
+ kind: "start",
1424
+ name,
1425
+ start,
1426
+ end: index + 1,
1427
+ closeStart: index,
1428
+ selfClosing: false,
1429
+ attributes,
1430
+ inert
1431
+ };
1432
+ }
1433
+ if (char === "/") {
1434
+ if (html[index + 1] === ">") {
1435
+ return {
1436
+ kind: "start",
1437
+ name,
1438
+ start,
1439
+ end: index + 2,
1440
+ closeStart: index,
1441
+ selfClosing: true,
1442
+ attributes,
1443
+ inert
1444
+ };
1445
+ }
1446
+ index++;
1447
+ continue;
1448
+ }
1449
+ const attributeStart = index;
1450
+ index++;
1451
+ while (index < html.length && !isSpace(html[index]) && !"/>=".includes(html[index])) index++;
1452
+ const attributeName = html.slice(attributeStart, index).toLowerCase();
1453
+ const afterName = index;
1454
+ index = skipSpace(html, index);
1455
+ if (html[index] !== "=") {
1456
+ attributes.push({
1457
+ name: attributeName,
1458
+ value: null,
1459
+ start: attributeStart,
1460
+ end: afterName,
1461
+ valueStart: afterName,
1462
+ valueEnd: afterName
1463
+ });
1464
+ continue;
1465
+ }
1466
+ index = skipSpace(html, index + 1);
1467
+ const quote = html[index];
1468
+ let valueStart;
1469
+ let valueEnd;
1470
+ if (quote === '"' || quote === "'") {
1471
+ valueStart = index + 1;
1472
+ const close = html.indexOf(quote, valueStart);
1473
+ valueEnd = close < 0 ? html.length : close;
1474
+ index = close < 0 ? html.length : close + 1;
1475
+ } else {
1476
+ valueStart = index;
1477
+ while (index < html.length && !isSpace(html[index]) && html[index] !== ">") index++;
1478
+ valueEnd = index;
1479
+ }
1480
+ attributes.push({
1481
+ name: attributeName,
1482
+ value: html.slice(valueStart, valueEnd),
1483
+ start: attributeStart,
1484
+ end: index,
1485
+ valueStart,
1486
+ valueEnd
1487
+ });
1488
+ }
1489
+ }
1490
+ function scanName(html, from) {
1491
+ let index = from;
1492
+ while (index < html.length && !isSpace(html[index]) && html[index] !== "/" && html[index] !== ">") {
1493
+ index++;
1494
+ }
1495
+ return index;
1496
+ }
1497
+ function skipSpace(html, from) {
1498
+ let index = from;
1499
+ while (index < html.length && isSpace(html[index])) index++;
1500
+ return index;
1501
+ }
1502
+ function isSpace(char) {
1503
+ return char === " " || char === "\n" || char === " " || char === "\r" || char === "\f";
1504
+ }
1505
+ function isAsciiLetter(char) {
1506
+ return char !== void 0 && /^[A-Za-z]$/.test(char);
1507
+ }
1508
+ function attributeValue(tag, name) {
1509
+ return tag.attributes.find((attribute) => attribute.name === name)?.value ?? void 0;
1510
+ }
1511
+ function collectSlots(tags) {
1512
+ const slots = [];
1513
+ const issues = [];
1514
+ let open = null;
1515
+ for (const tag of tags) {
1516
+ if (tag.name !== "slot" || tag.inert) continue;
1517
+ if (tag.kind === "start") {
1518
+ if (open) {
1519
+ issues.push({
1520
+ message: "A <slot> cannot be placed inside another <slot>.",
1521
+ offset: tag.start
1522
+ });
1523
+ continue;
1524
+ }
1525
+ if (tag.selfClosing) {
1526
+ slots.push({
1527
+ name: slotName(tag),
1528
+ start: tag.start,
1529
+ end: tag.end,
1530
+ contentStart: tag.end,
1531
+ contentEnd: tag.end
1532
+ });
1533
+ } else {
1534
+ open = tag;
1535
+ }
1536
+ } else if (open) {
1537
+ slots.push({
1538
+ name: slotName(open),
1539
+ start: open.start,
1540
+ end: tag.end,
1541
+ contentStart: open.end,
1542
+ contentEnd: tag.start
1543
+ });
1544
+ open = null;
1545
+ } else {
1546
+ issues.push({ message: "A </slot> has no matching <slot>.", offset: tag.start });
1547
+ }
1548
+ }
1549
+ if (open) {
1550
+ issues.push({
1551
+ message: "A <slot> is never closed; write <slot></slot> or <slot/>.",
1552
+ offset: open.start
1553
+ });
1554
+ }
1555
+ return { slots, issues };
1556
+ }
1557
+ function slotName(tag) {
1558
+ const name = attributeValue(tag, "name")?.trim();
1559
+ return name ? name : null;
1560
+ }
1561
+ function fillTitleText(text, valueFor) {
1562
+ const { slots } = collectSlots(scanHtml(text));
1563
+ if (slots.length === 0) return escapeHtml3(valueFor("title") ?? "");
1564
+ const edits = slots.map((slot) => {
1565
+ const value = valueFor(slot.name ?? "title");
1566
+ return {
1567
+ start: slot.start,
1568
+ end: slot.end,
1569
+ text: value === void 0 ? text.slice(slot.contentStart, slot.contentEnd) : escapeHtml3(value)
1570
+ };
1571
+ });
1572
+ return applyEdits(text, edits);
1573
+ }
1574
+ var TITLE_META = /* @__PURE__ */ new Set(["og:title", "twitter:title"]);
1575
+ var DESCRIPTION_META = /* @__PURE__ */ new Set(["description", "og:description", "twitter:description"]);
1576
+ function metaValue(key, title, description) {
1577
+ if (TITLE_META.has(key)) return title;
1578
+ if (DESCRIPTION_META.has(key)) return description;
1579
+ return void 0;
1580
+ }
1581
+ function headInsertionPoint(tags) {
1582
+ for (const tag of tags) {
1583
+ if (tag.inert) continue;
1584
+ if (tag.kind === "end" && tag.name === "head") return tag.start;
1585
+ if (tag.kind !== "start") continue;
1586
+ if (tag.name === "body") return tag.start;
1587
+ if (tag.name === "style") return tag.start;
1588
+ if (tag.name === "link" && (attributeValue(tag, "rel") ?? "").toLowerCase().split(/\s+/).includes("stylesheet")) {
1589
+ return tag.start;
1590
+ }
1591
+ }
1592
+ const container = tags.find(
1593
+ (tag) => tag.kind === "start" && (tag.name === "head" || tag.name === "html")
1594
+ );
1595
+ return container?.end ?? 0;
1596
+ }
1597
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
1598
+ "href",
1599
+ "src",
1600
+ "poster",
1601
+ "action",
1602
+ "formaction",
1603
+ "cite",
1604
+ "data",
1605
+ "background",
1606
+ "xlink:href"
1607
+ ]);
1608
+ var SRCSET_ATTRIBUTES = /* @__PURE__ */ new Set(["srcset", "imagesrcset"]);
1609
+ function rebaseUrl(raw, fromDir, toDir) {
1610
+ const url = raw.trim();
1611
+ if (!url || url.startsWith("#") || url.startsWith("?") || url.startsWith("/") || url.startsWith("\\") || /^[a-z][a-z0-9+.-]*:/i.test(url) || url.includes("{{")) {
1612
+ return raw;
1613
+ }
1614
+ const cut = url.search(/[?#]/);
1615
+ const path = cut < 0 ? url : url.slice(0, cut);
1616
+ const suffix = cut < 0 ? "" : url.slice(cut);
1617
+ if (path.includes("&")) return raw;
1618
+ const target = normalizePath(fromDir ? `${fromDir}/${path}` : path);
1619
+ if (target === null) return raw;
1620
+ const isDirectory = path.endsWith("/") || /(?:^|\/)\.\.?$/.test(path);
1621
+ let relative = relativeTo(toDir, target);
1622
+ if (isDirectory) relative = relative ? `${relative}/` : "./";
1623
+ const leading = raw.slice(0, raw.length - raw.trimStart().length);
1624
+ const trailing = raw.slice(raw.trimEnd().length);
1625
+ return `${leading}${relative}${suffix}${trailing}`;
1626
+ }
1627
+ function rebaseSrcset(raw, fromDir, toDir) {
1628
+ let out = "";
1629
+ let index = 0;
1630
+ while (index < raw.length) {
1631
+ const separator = /^[\s,]*/.exec(raw.slice(index))?.[0] ?? "";
1632
+ out += separator;
1633
+ index += separator.length;
1634
+ if (index >= raw.length) break;
1635
+ let urlEnd = index;
1636
+ while (urlEnd < raw.length && !isSpace(raw[urlEnd])) urlEnd++;
1637
+ let url = raw.slice(index, urlEnd);
1638
+ const commas = /,+$/.exec(url)?.[0] ?? "";
1639
+ url = url.slice(0, url.length - commas.length);
1640
+ out += rebaseUrl(url, fromDir, toDir) + commas;
1641
+ index = urlEnd;
1642
+ if (commas) continue;
1643
+ let descriptorEnd = index;
1644
+ let depth = 0;
1645
+ while (descriptorEnd < raw.length) {
1646
+ const char = raw[descriptorEnd];
1647
+ if (char === "(") depth++;
1648
+ else if (char === ")") depth = Math.max(0, depth - 1);
1649
+ else if (char === "," && depth === 0) break;
1650
+ descriptorEnd++;
1651
+ }
1652
+ out += raw.slice(index, descriptorEnd);
1653
+ index = descriptorEnd;
1654
+ }
1655
+ return out;
1656
+ }
1657
+ function relativeTo(fromDir, target) {
1658
+ const from = fromDir ? fromDir.split("/") : [];
1659
+ const to = target ? target.split("/") : [];
1660
+ let shared = 0;
1661
+ while (shared < from.length && shared < to.length && from[shared] === to[shared]) shared++;
1662
+ return [...from.slice(shared).map(() => ".."), ...to.slice(shared)].join("/");
1663
+ }
1664
+ function posixDirname2(path) {
1665
+ const normalized = normalizePath(path.replace(/\\/g, "/")) ?? "";
1666
+ const slash = normalized.lastIndexOf("/");
1667
+ return slash < 0 ? "" : normalized.slice(0, slash);
1668
+ }
1669
+ function applyEdits(source, edits) {
1670
+ const ordered = [...edits].sort(
1671
+ (a, b) => a.start - b.start || Number(a.end > a.start) - Number(b.end > b.start)
1672
+ );
1673
+ let out = "";
1674
+ let cursor = 0;
1675
+ for (const edit of ordered) {
1676
+ if (edit.start < cursor) continue;
1677
+ out += source.slice(cursor, edit.start) + edit.text;
1678
+ cursor = edit.end;
1679
+ }
1680
+ return out + source.slice(cursor);
1681
+ }
1682
+ function lineIndex(source) {
1683
+ return (offset) => {
1684
+ let line = 1;
1685
+ for (let index = source.indexOf("\n"); index >= 0 && index < offset; index = source.indexOf("\n", index + 1)) {
1686
+ line++;
1687
+ }
1688
+ return line;
1689
+ };
1690
+ }
1691
+ function describeIssue(issue) {
1692
+ return `${issue.message} (line ${issue.line})`;
1693
+ }
1694
+ function escapeHtml3(s) {
1695
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1696
+ }
1697
+ function escapeAttr2(s) {
1698
+ return escapeHtml3(s).replace(/"/g, "&quot;");
1699
+ }
1700
+
1180
1701
  // src/html/docsHtmlBundle.ts
1181
1702
  import JSZip2 from "jszip";
1182
1703
  import { parseMarkdown as parseMarkdown2, inferDocumentTitle as inferDocumentTitle2 } from "@bendyline/squisq/markdown";
@@ -1997,11 +2518,13 @@ function htmlToMarkdownDocSync(html, options = {}) {
1997
2518
  const embedded = findEmbeddedSquisqDoc(nodes);
1998
2519
  if (embedded) return docToMarkdown(parseEmbeddedSquisqDoc(embedded));
1999
2520
  const headMetadata = options.headMetadata === false ? {} : readHeadMetadata(nodes);
2521
+ const templated = extractHtmlPageTemplateContent(html);
2522
+ const contentNodes = templated === null ? nodes : parseHtmlToNodes(templated);
2000
2523
  const {
2001
2524
  nodes: bodyNodes,
2002
2525
  bodies,
2003
2526
  identifiers
2004
- } = extractFootnoteSection(dropContentlessElements(nodes));
2527
+ } = extractFootnoteSection(dropContentlessElements(contentNodes));
2005
2528
  const clean = (input) => options.sanitize === false ? input : sanitizeHtmlNodes2(input);
2006
2529
  const children = blocksFromNodes(clean(bodyNodes));
2007
2530
  if (bodies.size > 0) {
@@ -2121,8 +2644,16 @@ export {
2121
2644
  collectImagePaths,
2122
2645
  generateExternalHtml,
2123
2646
  markdownDocToPlainHtml,
2647
+ markdownDocToPlainHtmlParts,
2124
2648
  markdownDocsToPlainHtmlBundle,
2125
2649
  collectLinkRefs,
2650
+ HTML_PAGE_TEMPLATE_CONTENT_START,
2651
+ HTML_PAGE_TEMPLATE_CONTENT_END,
2652
+ HtmlPageTemplateError,
2653
+ isHtmlPageTemplate,
2654
+ inspectHtmlPageTemplate,
2655
+ applyHtmlPageTemplate,
2656
+ extractHtmlPageTemplateContent,
2126
2657
  markdownDocsToHtmlBundle,
2127
2658
  htmlToMarkdownDocSync,
2128
2659
  htmlToMarkdownDoc,
@@ -1,5 +1,6 @@
1
1
  // src/outside-in/layout.ts
2
2
  var OUTSIDE_IN_FORMAT_IDS = ["html", "docx", "pdf", "pptx", "xlsx", "csv"];
3
+ var OUTSIDE_IN_OUTPUT_TEMPLATE_KEY = "squisq-output-template";
3
4
  var OUTSIDE_IN_FORMAT_SET = new Set(OUTSIDE_IN_FORMAT_IDS);
4
5
  function normalizePath(path) {
5
6
  const slash = path.replace(/\\/g, "/");
@@ -83,6 +84,7 @@ function chooseOutsideInMarkdownPath(layout, paths) {
83
84
 
84
85
  export {
85
86
  OUTSIDE_IN_FORMAT_IDS,
87
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
86
88
  resolveOutsideInLayout,
87
89
  isOutsideInTargetPath,
88
90
  chooseOutsideInMarkdownPath
@@ -118,7 +118,7 @@ interface PlainHtmlExportOptions {
118
118
  htmlPolicy?: HtmlPolicy;
119
119
  /**
120
120
  * How a hosted video (a paragraph that is only a link to a YouTube, Vimeo,
121
- * Loom, Dailymotion or Wistia page, or that provider's `<iframe>` embed
121
+ * Loom or Wistia page, or that provider's `<iframe>` embed
122
122
  * code) renders:
123
123
  *
124
124
  * - `player` (default) — the provider's player iframe;
@@ -138,6 +138,134 @@ interface PlainHtmlExportOptions {
138
138
  * renders consistently when opened standalone.
139
139
  */
140
140
  declare function markdownDocToPlainHtml(doc: MarkdownDocument, options?: PlainHtmlExportOptions): string;
141
+ /**
142
+ * The pieces of a plain HTML page that belong to the document itself, for a
143
+ * host that places the document inside a page of its own (see
144
+ * `applyHtmlPageTemplate`).
145
+ */
146
+ interface PlainHtmlParts {
147
+ /** The `title` option, unescaped. */
148
+ title: string;
149
+ /** The rendered document body, footnotes included. */
150
+ bodyHtml: string;
151
+ /**
152
+ * `<head>` markup for the CSS the body's own constructs need — feature
153
+ * sections, video embeds, SVG islands, icons — and nothing that styles the
154
+ * page around them: no theme typography, colors, fonts, or `body` rules.
155
+ */
156
+ featureHeadHtml: string;
157
+ }
158
+ /**
159
+ * Render a document's body and the functional CSS it depends on, without the
160
+ * page shell or theme stylesheet `markdownDocToPlainHtml` wraps around them.
161
+ * The theme (when one resolves) still informs body rendering, such as the
162
+ * color an SVG island's `currentColor` follows.
163
+ */
164
+ declare function markdownDocToPlainHtmlParts(doc: MarkdownDocument, options?: PlainHtmlExportOptions): PlainHtmlParts;
165
+
166
+ /**
167
+ * HTML Page Templates
168
+ *
169
+ * Places a rendered document inside a page of the author's own — a site
170
+ * header, navigation, footer, and stylesheet — instead of the fixed shell
171
+ * `markdownDocToPlainHtml` emits. A template is an ordinary HTML file that
172
+ * marks where content goes with the HTML standard's `<slot>` element:
173
+ *
174
+ * - the one unnamed `<slot>` receives the document body;
175
+ * - `<slot name="title">` receives the document title, and
176
+ * `<slot name="KEY">` the caller's value for `KEY` (typically a
177
+ * frontmatter field), inserted as text;
178
+ * - a slot with no value is replaced by its children — the standard fallback
179
+ * content — so a template opened directly in a browser previews with its
180
+ * placeholders.
181
+ *
182
+ * `<slot/>` is accepted as an empty slot (the spelling Astro and JSX authors
183
+ * use), although HTML itself has no self-closing `<slot>`.
184
+ *
185
+ * `<title>` cannot contain elements, so slots written inside it are matched
186
+ * as text; a `<title>` without one is replaced by the document title.
187
+ * Existing description and Open Graph / Twitter title and description
188
+ * `<meta>` tags receive the document's values. Relative URLs in the template
189
+ * are rewritten so they still resolve from the generated page's location.
190
+ *
191
+ * The template is scanned, never re-serialized: everything outside the
192
+ * replaced ranges is emitted byte for byte, so its formatting, comments, and
193
+ * scripts survive unchanged.
194
+ */
195
+ /** Marks the start of the injected document body in a templated page. */
196
+ declare const HTML_PAGE_TEMPLATE_CONTENT_START = "<!-- squisq:content -->";
197
+ /** Marks the end of the injected document body in a templated page. */
198
+ declare const HTML_PAGE_TEMPLATE_CONTENT_END = "<!-- /squisq:content -->";
199
+ interface HtmlPageTemplateContent {
200
+ /** Rendered document body. Inserted as-is; the caller sanitizes it. */
201
+ bodyHtml: string;
202
+ /**
203
+ * Markup the body needs in `<head>`, such as
204
+ * `PlainHtmlParts.featureHeadHtml`. Inserted before the template's own
205
+ * stylesheets so the template's CSS cascades last.
206
+ */
207
+ headHtml?: string;
208
+ /** Document title, as text. */
209
+ title: string;
210
+ /**
211
+ * Text for named slots, keyed by slot name. `description` also fills
212
+ * description `<meta>` tags. `title` comes from {@link title}.
213
+ */
214
+ values?: Readonly<Record<string, string | undefined>>;
215
+ }
216
+ interface HtmlPageTemplateOptions {
217
+ /**
218
+ * Container-relative path of the template file: the location its relative
219
+ * URLs were written against.
220
+ */
221
+ templatePath?: string;
222
+ /**
223
+ * Container-relative path of the page being generated. With
224
+ * {@link templatePath}, relative URLs in the template are rewritten to
225
+ * resolve from here.
226
+ */
227
+ outputPath?: string;
228
+ }
229
+ interface HtmlPageTemplateIssue {
230
+ message: string;
231
+ /** 1-based line in the template. */
232
+ line: number;
233
+ }
234
+ interface HtmlPageTemplateInspection {
235
+ /** True when the HTML has a `<slot>` outside any `<template>`. */
236
+ isTemplate: boolean;
237
+ /** Unnamed slots outside `<template>`; a usable template has exactly one. */
238
+ defaultSlotCount: number;
239
+ /** Distinct named slots, in document order. */
240
+ namedSlots: string[];
241
+ /** Problems that make {@link applyHtmlPageTemplate} refuse the template. */
242
+ errors: HtmlPageTemplateIssue[];
243
+ }
244
+ /** Thrown by {@link applyHtmlPageTemplate} for a template it cannot use. */
245
+ declare class HtmlPageTemplateError extends Error {
246
+ readonly issues: readonly HtmlPageTemplateIssue[];
247
+ constructor(issues: readonly HtmlPageTemplateIssue[]);
248
+ }
249
+ /**
250
+ * True when `html` is a page template: it has a `<slot>` outside any
251
+ * `<template>`. Outside a shadow tree a slot does nothing, so an ordinary
252
+ * page has no reason to carry one.
253
+ */
254
+ declare function isHtmlPageTemplate(html: string): boolean;
255
+ /** Describe a template's slots and any problems that would prevent its use. */
256
+ declare function inspectHtmlPageTemplate(html: string): HtmlPageTemplateInspection;
257
+ /**
258
+ * Generate a page by filling `template`'s slots with `content`.
259
+ *
260
+ * @throws {HtmlPageTemplateError} when the template has no single unnamed
261
+ * slot, or its slots are unbalanced or nested.
262
+ */
263
+ declare function applyHtmlPageTemplate(template: string, content: HtmlPageTemplateContent, options?: HtmlPageTemplateOptions): string;
264
+ /**
265
+ * The document body a templated page carries between its content markers, or
266
+ * null when `html` has none. Lets an importer skip the template's chrome.
267
+ */
268
+ declare function extractHtmlPageTemplateContent(html: string): string | null;
141
269
 
142
270
  /**
143
271
  * Recursive plain-HTML bundle export.
@@ -349,4 +477,4 @@ declare function docToHtml(doc: Doc, options: HtmlExportOptions): string;
349
477
  */
350
478
  declare function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Promise<Blob>;
351
479
 
352
- export { type HtmlBundleOptions, HtmlExportOptions, type HtmlZipExportOptions, type PlainHtmlBundleOptions, type PlainHtmlExportOptions, arrayBufferToBase64DataUrl, collectLinkRefs, docToHtml, docToHtmlZip, extractFilename, inferMimeType, markdownDocToPlainHtml, markdownDocsToHtmlBundle, markdownDocsToPlainHtmlBundle };
480
+ export { HTML_PAGE_TEMPLATE_CONTENT_END, HTML_PAGE_TEMPLATE_CONTENT_START, type HtmlBundleOptions, HtmlExportOptions, type HtmlPageTemplateContent, HtmlPageTemplateError, type HtmlPageTemplateInspection, type HtmlPageTemplateIssue, type HtmlPageTemplateOptions, type HtmlZipExportOptions, type PlainHtmlBundleOptions, type PlainHtmlExportOptions, type PlainHtmlParts, applyHtmlPageTemplate, arrayBufferToBase64DataUrl, collectLinkRefs, docToHtml, docToHtmlZip, extractFilename, extractHtmlPageTemplateContent, inferMimeType, inspectHtmlPageTemplate, isHtmlPageTemplate, markdownDocToPlainHtml, markdownDocToPlainHtmlParts, markdownDocsToHtmlBundle, markdownDocsToPlainHtmlBundle };
@@ -1,16 +1,24 @@
1
1
  import {
2
+ HTML_PAGE_TEMPLATE_CONTENT_END,
3
+ HTML_PAGE_TEMPLATE_CONTENT_START,
4
+ HtmlPageTemplateError,
5
+ applyHtmlPageTemplate,
2
6
  collectImagePaths,
3
7
  collectLinkRefs,
4
8
  docToHtml,
5
9
  docToHtmlZip,
10
+ extractHtmlPageTemplateContent,
6
11
  generateExternalHtml,
7
12
  htmlToMarkdown,
8
13
  htmlToMarkdownDoc,
9
14
  htmlToMarkdownDocSync,
15
+ inspectHtmlPageTemplate,
16
+ isHtmlPageTemplate,
10
17
  markdownDocToPlainHtml,
18
+ markdownDocToPlainHtmlParts,
11
19
  markdownDocsToHtmlBundle,
12
20
  markdownDocsToPlainHtmlBundle
13
- } from "../chunk-B6UQPW75.js";
21
+ } from "../chunk-LK4KCVN6.js";
14
22
  import {
15
23
  arrayBufferToBase64DataUrl,
16
24
  extractFilename,
@@ -20,18 +28,26 @@ import "../chunk-VCZUXQUL.js";
20
28
  import "../chunk-BXWNU4T5.js";
21
29
  import "../chunk-JZR32FON.js";
22
30
  export {
31
+ HTML_PAGE_TEMPLATE_CONTENT_END,
32
+ HTML_PAGE_TEMPLATE_CONTENT_START,
33
+ HtmlPageTemplateError,
34
+ applyHtmlPageTemplate,
23
35
  arrayBufferToBase64DataUrl,
24
36
  collectImagePaths,
25
37
  collectLinkRefs,
26
38
  docToHtml,
27
39
  docToHtmlZip,
28
40
  extractFilename,
41
+ extractHtmlPageTemplateContent,
29
42
  generateExternalHtml,
30
43
  htmlToMarkdown,
31
44
  htmlToMarkdownDoc,
32
45
  htmlToMarkdownDocSync,
33
46
  inferMimeType,
47
+ inspectHtmlPageTemplate,
48
+ isHtmlPageTemplate,
34
49
  markdownDocToPlainHtml,
50
+ markdownDocToPlainHtmlParts,
35
51
  markdownDocsToHtmlBundle,
36
52
  markdownDocsToPlainHtmlBundle
37
53
  };
package/dist/index.d.ts CHANGED
@@ -8,10 +8,10 @@ export { EpubExportOptions, docToEpub, markdownDocToEpub } from './epub/index.js
8
8
  export { ExtractedFileTheme, InferSourceFormat, InferThemeOptions, InferredFileTheme, compileExtractedTheme, inferThemeFromFile } from './infer/index.js';
9
9
  export { B as BUILTIN_FORMAT_IDS, a as BuiltinFormatOptions, C as ConversionLimits, b as ConversionResult, c as ConvertOptions, d as ConvertSource, D as DEFAULT_CONVERSION_LIMITS, e as DbkFormatOptions, F as FormatDefinition, f as FormatId, g as FormatRegistry, M as MarkdownFormatOptions, N as NormalizedInput, P as PreparedConversion, h as PreparedExportOptions, r as resolveConversionLimits } from './types-CVv9eldp.js';
10
10
  export { ConversionError, ConversionErrorCode, ConversionErrorOptions, convert, createRegistry, defaultFormats, defaultRegistry, prepareConversion } from './registry/index.js';
11
- export { ImportedOutsideInDocument, OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY, OutsideInMetadata, RenderOutsideInOptions, importOutsideInDocument, isOutsideInMarkdownEditingEnabled, readOutsideInMetadata, renderOutsideInDocument, withOutsideInMarkdownEditing, withOutsideInMetadata } from './outside-in/index.js';
11
+ export { ImportedOutsideInDocument, OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY, OutsideInMetadata, RenderOutsideInOptions, importOutsideInDocument, isOutsideInMarkdownEditingEnabled, readOutsideInMetadata, readOutsideInOutputTemplate, renderOutsideInDocument, withOutsideInMarkdownEditing, withOutsideInMetadata } from './outside-in/index.js';
12
12
  export { Z as ZipSafetyError, a as ZipSafetyErrorCode, b as ZipSafetyErrorOptions, c as ZipSafetyLimits } from './zipLimits-BOKCB7qk.js';
13
13
  export { H as HtmlExportOptions, a as HtmlImportOptions, c as collectImagePaths, h as htmlToMarkdown, b as htmlToMarkdownDoc, d as htmlToMarkdownDocSync } from './import-ipP4Ghp8.js';
14
- export { OUTSIDE_IN_FORMAT_IDS, OutsideInFormatId, OutsideInLayout, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout } from './outside-in/layout.js';
14
+ export { OUTSIDE_IN_FORMAT_IDS, OUTSIDE_IN_OUTPUT_TEMPLATE_KEY, OutsideInFormatId, OutsideInLayout, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout } from './outside-in/layout.js';
15
15
  export { P as PptxExportOptions, a as PptxImportOptions, d as docToPptx, m as markdownDocToPptx, p as pptxToMarkdownDoc } from './import-Cn_V1_7e.js';
16
16
  export { X as XlsxExportOptions, a as XlsxImportOptions, b as XlsxTable, c as XlsxTableColumn, d as XlsxTablesOptions, e as docToXlsx, g as gridToTables, m as markdownDocToXlsx, x as xlsxToMarkdownDoc, f as xlsxToTables } from './export-C4bBD3Mx.js';
17
17
  import '@bendyline/squisq/schemas';
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  htmlToMarkdown,
6
6
  htmlToMarkdownDoc,
7
7
  htmlToMarkdownDocSync
8
- } from "./chunk-B6UQPW75.js";
8
+ } from "./chunk-LK4KCVN6.js";
9
9
  import {
10
10
  docToEpub,
11
11
  markdownDocToEpub
@@ -25,10 +25,11 @@ import {
25
25
  importOutsideInDocument,
26
26
  isOutsideInMarkdownEditingEnabled,
27
27
  readOutsideInMetadata,
28
+ readOutsideInOutputTemplate,
28
29
  renderOutsideInDocument,
29
30
  withOutsideInMarkdownEditing,
30
31
  withOutsideInMetadata
31
- } from "./chunk-IYNDHW2C.js";
32
+ } from "./chunk-6ALSRHD7.js";
32
33
  import {
33
34
  DEFAULT_CONVERSION_LIMITS,
34
35
  convert,
@@ -37,16 +38,17 @@ import {
37
38
  defaultRegistry,
38
39
  prepareConversion,
39
40
  resolveConversionLimits
40
- } from "./chunk-ALDCFHZT.js";
41
+ } from "./chunk-GDUZJ75A.js";
41
42
  import {
42
43
  ConversionError
43
44
  } from "./chunk-KXOZMWBS.js";
44
45
  import {
45
46
  OUTSIDE_IN_FORMAT_IDS,
47
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
46
48
  chooseOutsideInMarkdownPath,
47
49
  isOutsideInTargetPath,
48
50
  resolveOutsideInLayout
49
- } from "./chunk-C23JYVPG.js";
51
+ } from "./chunk-WJTC5OD6.js";
50
52
  import {
51
53
  docToDocx,
52
54
  docxToDoc,
@@ -105,6 +107,7 @@ export {
105
107
  ConversionError,
106
108
  DEFAULT_CONVERSION_LIMITS,
107
109
  OUTSIDE_IN_FORMAT_IDS,
110
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
108
111
  OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY,
109
112
  ZipSafetyError,
110
113
  chooseOutsideInMarkdownPath,
@@ -147,6 +150,7 @@ export {
147
150
  pptxToMarkdownDoc,
148
151
  prepareConversion,
149
152
  readOutsideInMetadata,
153
+ readOutsideInOutputTemplate,
150
154
  renderOutsideInDocument,
151
155
  resolveConversionLimits,
152
156
  resolveOutsideInLayout,
@@ -2,7 +2,7 @@ import { MarkdownDocument } from '@bendyline/squisq/markdown';
2
2
  import { ContentContainer } from '@bendyline/squisq/storage';
3
3
  import { c as ConvertOptions, b as ConversionResult } from '../types-CVv9eldp.js';
4
4
  import { OutsideInLayout, OutsideInFormatId } from './layout.js';
5
- export { OUTSIDE_IN_FORMAT_IDS, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout } from './layout.js';
5
+ export { OUTSIDE_IN_FORMAT_IDS, OUTSIDE_IN_OUTPUT_TEMPLATE_KEY, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout } from './layout.js';
6
6
  import '@bendyline/squisq/schemas';
7
7
  import '@bendyline/squisq/transform';
8
8
  import '../docx/index.js';
@@ -65,6 +65,13 @@ declare function readOutsideInMetadata(source: string): OutsideInMetadata | null
65
65
  declare function withOutsideInMetadata(source: string, layout: OutsideInLayout): string;
66
66
  /** True only when the companion explicitly opts into rendered-file updates. */
67
67
  declare function isOutsideInMarkdownEditingEnabled(source: string | MarkdownDocument): boolean;
68
+ /**
69
+ * The companion's page template choice: the authored path, `'none'` when it
70
+ * opts out of any host default, or null when it names none. Resolving the
71
+ * path against the companion is the host's job, since only the host knows
72
+ * where the container root is.
73
+ */
74
+ declare function readOutsideInOutputTemplate(source: string | MarkdownDocument): string | null;
68
75
  /**
69
76
  * Opt a companion into or out of Markdown-driven regeneration while keeping
70
77
  * the portable target relationship current.
@@ -82,4 +89,4 @@ declare function renderOutsideInDocument(source: {
82
89
  container?: ContentContainer;
83
90
  }, options?: RenderOutsideInOptions): Promise<ConversionResult>;
84
91
 
85
- export { type ImportedOutsideInDocument, OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY, OutsideInFormatId, OutsideInLayout, type OutsideInMetadata, type RenderOutsideInOptions, importOutsideInDocument, isOutsideInMarkdownEditingEnabled, readOutsideInMetadata, renderOutsideInDocument, withOutsideInMarkdownEditing, withOutsideInMetadata };
92
+ export { type ImportedOutsideInDocument, OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY, OutsideInFormatId, OutsideInLayout, type OutsideInMetadata, type RenderOutsideInOptions, importOutsideInDocument, isOutsideInMarkdownEditingEnabled, readOutsideInMetadata, readOutsideInOutputTemplate, renderOutsideInDocument, withOutsideInMarkdownEditing, withOutsideInMetadata };
@@ -3,27 +3,31 @@ import {
3
3
  importOutsideInDocument,
4
4
  isOutsideInMarkdownEditingEnabled,
5
5
  readOutsideInMetadata,
6
+ readOutsideInOutputTemplate,
6
7
  renderOutsideInDocument,
7
8
  withOutsideInMarkdownEditing,
8
9
  withOutsideInMetadata
9
- } from "../chunk-IYNDHW2C.js";
10
- import "../chunk-ALDCFHZT.js";
10
+ } from "../chunk-6ALSRHD7.js";
11
+ import "../chunk-GDUZJ75A.js";
11
12
  import "../chunk-KXOZMWBS.js";
12
13
  import {
13
14
  OUTSIDE_IN_FORMAT_IDS,
15
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
14
16
  chooseOutsideInMarkdownPath,
15
17
  isOutsideInTargetPath,
16
18
  resolveOutsideInLayout
17
- } from "../chunk-C23JYVPG.js";
19
+ } from "../chunk-WJTC5OD6.js";
18
20
  import "../chunk-7AWFHP5U.js";
19
21
  export {
20
22
  OUTSIDE_IN_FORMAT_IDS,
23
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
21
24
  OUTSIDE_IN_UPDATE_FROM_MARKDOWN_KEY,
22
25
  chooseOutsideInMarkdownPath,
23
26
  importOutsideInDocument,
24
27
  isOutsideInMarkdownEditingEnabled,
25
28
  isOutsideInTargetPath,
26
29
  readOutsideInMetadata,
30
+ readOutsideInOutputTemplate,
27
31
  renderOutsideInDocument,
28
32
  resolveOutsideInLayout,
29
33
  withOutsideInMarkdownEditing,
@@ -19,6 +19,12 @@
19
19
  */
20
20
  declare const OUTSIDE_IN_FORMAT_IDS: readonly ["html", "docx", "pdf", "pptx", "xlsx", "csv"];
21
21
  type OutsideInFormatId = (typeof OUTSIDE_IN_FORMAT_IDS)[number];
22
+ /**
23
+ * Frontmatter naming the page template a rendered output is generated
24
+ * through: a path relative to the companion Markdown, a `/`-prefixed path
25
+ * from the container root, or `none` to opt out of a host default.
26
+ */
27
+ declare const OUTSIDE_IN_OUTPUT_TEMPLATE_KEY = "squisq-output-template";
22
28
  interface OutsideInLayout {
23
29
  /** User-facing rendered file, relative to the host's workspace root. */
24
30
  targetPath: string;
@@ -55,4 +61,4 @@ declare function isOutsideInTargetPath(path: string): boolean;
55
61
  */
56
62
  declare function chooseOutsideInMarkdownPath(layout: OutsideInLayout, paths: readonly string[]): string | null;
57
63
 
58
- export { OUTSIDE_IN_FORMAT_IDS, type OutsideInFormatId, type OutsideInLayout, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout };
64
+ export { OUTSIDE_IN_FORMAT_IDS, OUTSIDE_IN_OUTPUT_TEMPLATE_KEY, type OutsideInFormatId, type OutsideInLayout, chooseOutsideInMarkdownPath, isOutsideInTargetPath, resolveOutsideInLayout };
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  OUTSIDE_IN_FORMAT_IDS,
3
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
3
4
  chooseOutsideInMarkdownPath,
4
5
  isOutsideInTargetPath,
5
6
  resolveOutsideInLayout
6
- } from "../chunk-C23JYVPG.js";
7
+ } from "../chunk-WJTC5OD6.js";
7
8
  export {
8
9
  OUTSIDE_IN_FORMAT_IDS,
10
+ OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
9
11
  chooseOutsideInMarkdownPath,
10
12
  isOutsideInTargetPath,
11
13
  resolveOutsideInLayout
@@ -9,7 +9,7 @@ import {
9
9
  defaultRegistry,
10
10
  prepareConversion,
11
11
  resolveConversionLimits
12
- } from "../chunk-ALDCFHZT.js";
12
+ } from "../chunk-GDUZJ75A.js";
13
13
  import {
14
14
  ConversionError
15
15
  } from "../chunk-KXOZMWBS.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "2.8.7",
3
+ "version": "2.8.8",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -125,7 +125,7 @@
125
125
  "typecheck": "tsc --noEmit"
126
126
  },
127
127
  "dependencies": {
128
- "@bendyline/squisq": "2.13.6",
128
+ "@bendyline/squisq": "2.13.7",
129
129
  "@pdf-lib/fontkit": "1.1.1",
130
130
  "@pdf-lib/upng": "1.0.1",
131
131
  "@xmldom/xmldom": "0.9.12",
File without changes