epub-codec 1.1.0 → 1.2.0

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 (50) hide show
  1. package/README.md +3 -1
  2. package/dist/codec.d.cts +2 -2
  3. package/dist/codec.d.ts +2 -2
  4. package/dist/{diagnostics-tvGwPH46.d.cts → diagnostics-C0CRdoda.d.cts} +8 -0
  5. package/dist/{diagnostics-tvGwPH46.d.ts → diagnostics-C0CRdoda.d.ts} +8 -0
  6. package/dist/diagnostics.cjs +8 -0
  7. package/dist/diagnostics.d.cts +1 -1
  8. package/dist/diagnostics.d.ts +1 -1
  9. package/dist/diagnostics.js +8 -0
  10. package/dist/index.cjs +8 -3
  11. package/dist/index.d.cts +6 -6
  12. package/dist/index.d.ts +6 -6
  13. package/dist/index.js +5 -5
  14. package/dist/opf/metadata.cjs +4 -5
  15. package/dist/opf/metadata.d.cts +1 -1
  16. package/dist/opf/metadata.d.ts +1 -1
  17. package/dist/opf/metadata.js +5 -6
  18. package/dist/opf/parse.d.cts +1 -1
  19. package/dist/opf/parse.d.ts +1 -1
  20. package/dist/read.d.cts +1 -1
  21. package/dist/read.d.ts +1 -1
  22. package/dist/write.cjs +1 -1
  23. package/dist/write.d.cts +1 -1
  24. package/dist/write.d.ts +1 -1
  25. package/dist/write.js +1 -1
  26. package/dist/xhtml/context.cjs +18 -0
  27. package/dist/xhtml/context.d.cts +4 -2
  28. package/dist/xhtml/context.d.ts +4 -2
  29. package/dist/xhtml/context.js +16 -0
  30. package/dist/xhtml/inline.cjs +9 -3
  31. package/dist/xhtml/inline.js +9 -3
  32. package/dist/xhtml/read.cjs +330 -47
  33. package/dist/xhtml/read.js +331 -48
  34. package/dist/xhtml/write.cjs +72 -23
  35. package/dist/xhtml/write.d.cts +1 -1
  36. package/dist/xhtml/write.d.ts +1 -1
  37. package/dist/xhtml/write.js +72 -23
  38. package/dist/xml/entities.cjs +4 -0
  39. package/dist/xml/entities.d.cts +3 -1
  40. package/dist/xml/entities.d.ts +3 -1
  41. package/dist/xml/entities.js +4 -1
  42. package/dist/xml/node.cjs +4 -0
  43. package/dist/xml/node.d.cts +2 -1
  44. package/dist/xml/node.d.ts +2 -1
  45. package/dist/xml/node.js +4 -1
  46. package/dist/xml/query.cjs +9 -0
  47. package/dist/xml/query.d.cts +2 -1
  48. package/dist/xml/query.d.ts +2 -1
  49. package/dist/xml/query.js +9 -1
  50. package/package.json +2 -2
@@ -1,10 +1,12 @@
1
1
  import { EpubDiagnosticCodes } from "../diagnostics.js";
2
- import { attrValue, elementsWithTag, findChildElement, rootElement, textContent } from "../xml/query.js";
2
+ import { decodeEntities, decodeTextLikeNode } from "../xml/entities.js";
3
+ import { isTextLikeNode } from "../xml/node.js";
4
+ import { attrValue, findChildElement, rootElement } from "../xml/query.js";
3
5
  import { parseXml } from "../xml/parse.js";
4
- import { decodeEntities } from "../xml/entities.js";
5
6
  import { POINTS_PER_PIXEL, detectImageFormat, readImageDimensions } from "../image/dimensions.js";
6
7
  import { bytesToBase64 } from "../util/base64.js";
7
8
  import { buildXml } from "../xml/build.js";
9
+ import { isInertElement, reportInertElementSkip } from "./context.js";
8
10
  import { isFootnoteAside, isFootnoteReferenceAnchor } from "./footnote.js";
9
11
  import { HORIZONTAL_RULE_STYLE_ID, MONOSPACE_FONT_FAMILY } from "./style-constants.js";
10
12
  import { buildInlineRuns } from "./inline.js";
@@ -61,18 +63,32 @@ function decorateParagraph(paragraph, state) {
61
63
  }
62
64
  return decorated;
63
65
  }
66
+ function constructsField(inline) {
67
+ return inline.constructs.length > 0 ? { constructs: inline.constructs } : {};
68
+ }
64
69
  function buildIdElementMap(nodes) {
65
70
  const map = /* @__PURE__ */ new Map();
66
71
  const stack = [...nodes];
67
72
  while (stack.length > 0) {
68
73
  const node = stack.pop();
69
- if (node?.type !== "element") continue;
74
+ if (node?.type !== "element" || isInertElement(node.tag)) continue;
70
75
  const id = attrValue(node, "id");
71
76
  if (id !== void 0 && !map.has(id)) map.set(id, node);
72
77
  stack.push(...node.children);
73
78
  }
74
79
  return map;
75
80
  }
81
+ function elementsWithTagSkippingInert(nodes, tag) {
82
+ const out = [];
83
+ const stack = [...nodes];
84
+ while (stack.length > 0) {
85
+ const node = stack.pop();
86
+ if (node?.type !== "element" || isInertElement(node.tag)) continue;
87
+ if (node.tag === tag) out.push(node);
88
+ stack.push(...node.children);
89
+ }
90
+ return out;
91
+ }
76
92
  function readStyleResidue(html, context) {
77
93
  const head = findChildElement(html.children, "head");
78
94
  if (head === void 0) return;
@@ -99,7 +115,7 @@ function readXhtmlBody(xml, options) {
99
115
  };
100
116
  const idElements = buildIdElementMap(body.children);
101
117
  const footnoteTargetIds = /* @__PURE__ */ new Set();
102
- for (const anchor of elementsWithTag(body.children, "a")) {
118
+ for (const anchor of elementsWithTagSkippingInert(body.children, "a")) {
103
119
  const name = isFootnoteReferenceAnchor(anchor, idElements);
104
120
  if (name !== void 0) footnoteTargetIds.add(name);
105
121
  }
@@ -155,11 +171,11 @@ function readContainerChildren(nodes, state) {
155
171
  if (segment.length === 0) return;
156
172
  const inline = buildInlineRuns(segment, {}, state.context);
157
173
  segment = [];
158
- if (inline.runs.every((run) => run.text.trim().length === 0)) return;
174
+ if (inline.runs.every((run) => run.text.trim().length === 0) && inline.constructs.length === 0) return;
159
175
  const paragraph = {
160
176
  kind: "paragraph",
161
177
  runs: inline.runs,
162
- ...inline.constructs.length > 0 ? { constructs: inline.constructs } : {}
178
+ ...constructsField(inline)
163
179
  };
164
180
  blocks.push(decorateParagraph(paragraph, state));
165
181
  };
@@ -169,7 +185,7 @@ function readContainerChildren(nodes, state) {
169
185
  blocks.push(...readBlockElement(node, state));
170
186
  continue;
171
187
  }
172
- if (node.type === "element" || node.type === "text") segment.push(node);
188
+ if (node.type === "element" || isTextLikeNode(node)) segment.push(node);
173
189
  }
174
190
  flush();
175
191
  return blocks;
@@ -204,7 +220,7 @@ function readBlockElementInner(element, state) {
204
220
  kind: "paragraph",
205
221
  headingLevel,
206
222
  runs: inline.runs,
207
- ...inline.constructs.length > 0 ? { constructs: inline.constructs } : {}
223
+ ...constructsField(inline)
208
224
  }, state)];
209
225
  }
210
226
  switch (element.tag) {
@@ -219,12 +235,16 @@ function readBlockElementInner(element, state) {
219
235
  case "ul":
220
236
  case "ol": return readList(element, state);
221
237
  case "dl": return readDefinitionList(element, state);
222
- case "table": return [readTable(element, state)];
238
+ case "table": return readTable(element, state);
223
239
  case "figure": return readContainerChildren(element.children, state);
224
- case "figcaption": return [decorateParagraph({
225
- kind: "paragraph",
226
- runs: buildInlineRuns(element.children, {}, state.context).runs
227
- }, state)];
240
+ case "figcaption": {
241
+ const inline = buildInlineRuns(element.children, {}, state.context);
242
+ return [decorateParagraph({
243
+ kind: "paragraph",
244
+ runs: inline.runs,
245
+ ...constructsField(inline)
246
+ }, state)];
247
+ }
228
248
  case "img": {
229
249
  const block = readImage(element, state);
230
250
  return block === void 0 ? [] : [block];
@@ -236,23 +256,123 @@ function readBlockElementInner(element, state) {
236
256
  function readPre(element, state) {
237
257
  const languageSource = findChildElement(element.children, "code") ?? element;
238
258
  const codeLanguage = languageFromClass(attrValue(languageSource, "class"));
239
- const text = decodeEntities(textContent(element.children));
259
+ const inline = containsFootnoteReference(element.children, state.context) ? readPreRuns(element.children, state.context) : {
260
+ runs: readPreFlatRuns(element.children, state.context),
261
+ constructs: []
262
+ };
240
263
  return decorateParagraph({
241
264
  kind: "paragraph",
242
- runs: text.length > 0 ? [{
243
- text,
244
- fontFamily: MONOSPACE_FONT_FAMILY
245
- }] : [],
246
- ...codeLanguage !== void 0 ? { codeLanguage } : {}
265
+ runs: inline.runs,
266
+ preformatted: true,
267
+ ...codeLanguage !== void 0 ? { codeLanguage } : {},
268
+ ...constructsField(inline)
247
269
  }, state);
248
270
  }
271
+ function readPreFlatRuns(nodes, context) {
272
+ const text = readPreText(nodes, context);
273
+ return text.length > 0 ? [{
274
+ text,
275
+ fontFamily: MONOSPACE_FONT_FAMILY
276
+ }] : [];
277
+ }
278
+ function containsFootnoteReference(nodes, context) {
279
+ for (const node of nodes) {
280
+ if (node.type !== "element" || isInertElement(node.tag)) continue;
281
+ if (node.tag === "a" && isFootnoteReferenceAnchor(node, context.idElements) !== void 0) return true;
282
+ if (containsFootnoteReference(node.children, context)) return true;
283
+ }
284
+ return false;
285
+ }
286
+ function readPreRuns(nodes, context) {
287
+ const runs = [];
288
+ const constructs = [];
289
+ let buffer = "";
290
+ const flush = () => {
291
+ if (buffer.length === 0) return;
292
+ runs.push({
293
+ text: buffer,
294
+ fontFamily: MONOSPACE_FONT_FAMILY
295
+ });
296
+ buffer = "";
297
+ };
298
+ for (const node of nodes) {
299
+ if (isTextLikeNode(node)) {
300
+ buffer += decodeTextLikeNode(node);
301
+ continue;
302
+ }
303
+ if (node.type !== "element") continue;
304
+ if (isInertElement(node.tag)) {
305
+ reportInertElementSkip(node.tag, context);
306
+ continue;
307
+ }
308
+ if (node.tag === "img") {
309
+ buffer += decodeEntities(readPreImageFallbackText(node, context));
310
+ continue;
311
+ }
312
+ if (node.tag === "br") {
313
+ buffer += "\n";
314
+ continue;
315
+ }
316
+ const footnoteName = node.tag === "a" ? isFootnoteReferenceAnchor(node, context.idElements) : void 0;
317
+ if (footnoteName === void 0 && !containsFootnoteReference([node], context)) {
318
+ buffer += readPreText(node.children, context);
319
+ continue;
320
+ }
321
+ flush();
322
+ const startRun = runs.length;
323
+ const nested = readPreRuns(node.children, context);
324
+ runs.push(...nested.runs);
325
+ for (const construct of nested.constructs) constructs.push({
326
+ ...construct,
327
+ startRun: construct.startRun + startRun,
328
+ endRun: construct.endRun + startRun
329
+ });
330
+ if (footnoteName !== void 0) constructs.push({
331
+ descriptor: {
332
+ kind: "anchor",
333
+ anchorType: "footnote",
334
+ name: footnoteName
335
+ },
336
+ startRun,
337
+ endRun: runs.length
338
+ });
339
+ }
340
+ flush();
341
+ return {
342
+ runs,
343
+ constructs
344
+ };
345
+ }
346
+ function readPreText(nodes, context) {
347
+ let out = "";
348
+ for (const node of nodes) if (isTextLikeNode(node)) out += decodeTextLikeNode(node);
349
+ else if (node.type === "element" && node.tag === "br") out += "\n";
350
+ else if (node.type === "element" && node.tag === "img") out += decodeEntities(readPreImageFallbackText(node, context));
351
+ else if (node.type === "element" && isInertElement(node.tag)) {
352
+ reportInertElementSkip(node.tag, context);
353
+ continue;
354
+ } else if (node.type === "element") out += readPreText(node.children, context);
355
+ return out;
356
+ }
357
+ function readPreImageFallbackText(element, context) {
358
+ const src = attrValue(element, "src");
359
+ const alt = attrValue(element, "alt");
360
+ const label = src === void 0 ? "<img>" : `<img src="${src}">`;
361
+ context.sink({
362
+ code: EpubDiagnosticCodes.IMAGE_PRE_UNSUPPORTED,
363
+ severity: "warning",
364
+ message: `${label} inside a <pre>/<code> block cannot become a real image block (this package reads a <pre> as a single text-content paragraph, so there is no block list to insert an image block into); degraded to its alt text`,
365
+ href: context.sourceHref
366
+ });
367
+ return alt ?? "";
368
+ }
249
369
  function languageFromClass(className) {
250
370
  if (className === void 0) return;
251
371
  return /(?:^|\s)language-(\S+)/u.exec(className)?.[1];
252
372
  }
253
373
  function containsHeading(nodes) {
254
374
  for (const node of nodes) {
255
- if (node.type !== "element") continue;
375
+ if (node.type !== "element" || isInertElement(node.tag)) continue;
256
376
  if (headingLevelOf(node.tag) !== void 0) return true;
257
377
  if (containsHeading(node.children)) return true;
258
378
  }
@@ -278,74 +398,237 @@ function readList(element, state) {
278
398
  });
279
399
  const level = state.list === void 0 ? 0 : state.list.level + 1;
280
400
  const blocks = [];
401
+ let previousItem;
402
+ let strayNodes = [];
281
403
  for (const child of element.children) {
282
- if (child.type !== "element" || child.tag !== "li") continue;
283
- const itemState = withListItem(state, {
284
- numId,
285
- level,
286
- itemId: state.minter.mintItemId()
287
- });
288
- blocks.push(...readContainerChildren(child.children, itemState));
404
+ if (child.type === "element" && child.tag === "li") {
405
+ blocks.push(...flushListStrayContent(strayNodes, previousItem, element.tag, state));
406
+ strayNodes = [];
407
+ previousItem = {
408
+ numId,
409
+ level,
410
+ itemId: state.minter.mintItemId()
411
+ };
412
+ blocks.push(...readContainerChildren(child.children, withListItem(state, previousItem)));
413
+ continue;
414
+ }
415
+ if (child.type === "element" && isInertElement(child.tag)) {
416
+ reportInertElementSkip(child.tag, state.context);
417
+ continue;
418
+ }
419
+ if (child.type === "element" || isTextLikeNode(child)) strayNodes.push(child);
289
420
  }
421
+ blocks.push(...flushListStrayContent(strayNodes, previousItem, element.tag, state));
422
+ return blocks;
423
+ }
424
+ function flushListStrayContent(nodes, previousItem, tag, state) {
425
+ if (nodes.length === 0) return [];
426
+ const blocks = readContainerChildren(nodes, previousItem === void 0 ? state : withListItem(state, previousItem));
427
+ if (blocks.length === 0) return [];
428
+ state.context.sink({
429
+ code: EpubDiagnosticCodes.LIST_CONTENT_OUTSIDE_ITEM,
430
+ severity: "info",
431
+ message: previousItem === void 0 ? `content sits directly inside a <${tag}> before its first <li> (not valid HTML5); recovered as ordinary content immediately before the list, inheriting whatever list membership its own enclosing context already carries (none, unless this <${tag}> is itself nested inside another list's <li>)` : `content sits directly inside a <${tag}> rather than inside an <li> (not valid HTML5); recovered as a continuation of the preceding <li>'s own content`,
432
+ href: state.context.sourceHref
433
+ });
290
434
  return blocks;
291
435
  }
292
436
  function startAttr(element) {
293
437
  return positiveIntAttr(element, "start");
294
438
  }
295
439
  function readDefinitionList(element, state) {
440
+ return readDefinitionListEntries(element.children, state);
441
+ }
442
+ function readDefinitionListEntries(nodes, state) {
296
443
  const blocks = [];
297
- for (const child of element.children) {
298
- if (child.type !== "element") continue;
299
- if (child.tag === "dt") {
444
+ let strayNodes = [];
445
+ const flushStray = () => {
446
+ blocks.push(...flushDefinitionListStrayContent(strayNodes, state));
447
+ strayNodes = [];
448
+ };
449
+ for (const child of nodes) {
450
+ if (child.type === "element" && child.tag === "dt") {
451
+ flushStray();
300
452
  const inline = buildInlineRuns(child.children, {}, state.context);
301
453
  blocks.push(decorateParagraph({
302
454
  kind: "paragraph",
303
- runs: inline.runs
455
+ runs: inline.runs,
456
+ ...constructsField(inline)
304
457
  }, state));
305
- } else if (child.tag === "dd") {
458
+ continue;
459
+ }
460
+ if (child.type === "element" && child.tag === "dd") {
461
+ flushStray();
306
462
  const inline = buildInlineRuns(child.children, {}, state.context);
307
463
  blocks.push(decorateParagraph({
308
464
  kind: "paragraph",
309
465
  runs: inline.runs,
466
+ ...constructsField(inline),
310
467
  indentLeftPt: 36 + state.quoteDepth * 36
311
468
  }, state));
469
+ continue;
470
+ }
471
+ if (child.type === "element" && child.tag === "div") {
472
+ flushStray();
473
+ blocks.push(...readDefinitionListEntries(child.children, state));
474
+ continue;
312
475
  }
476
+ if (child.type === "element" && isInertElement(child.tag)) {
477
+ reportInertElementSkip(child.tag, state.context);
478
+ continue;
479
+ }
480
+ if (child.type === "element" || isTextLikeNode(child)) strayNodes.push(child);
313
481
  }
482
+ flushStray();
483
+ return blocks;
484
+ }
485
+ function flushDefinitionListStrayContent(nodes, state) {
486
+ if (nodes.length === 0) return [];
487
+ const blocks = readContainerChildren(nodes, state);
488
+ if (blocks.length === 0) return [];
489
+ state.context.sink({
490
+ code: EpubDiagnosticCodes.DEFINITION_LIST_CONTENT_OUTSIDE_ENTRY,
491
+ severity: "info",
492
+ message: "content sits directly inside a <dl> (or one of its <div> wrappers) outside any dt/dd (not valid HTML5); recovered as ordinary content -- a non-conformant wrapper's own dt/dd children, if any, lose their distinct term/definition treatment and degrade to plain concatenated text",
493
+ href: state.context.sourceHref
494
+ });
314
495
  return blocks;
315
496
  }
316
497
  function readTable(element, state) {
498
+ const captionElements = element.children.filter((c) => c.type === "element" && c.tag === "caption");
317
499
  const rows = [];
318
500
  let columnCount = 0;
501
+ const strayNodes = [];
319
502
  for (const section of element.children) {
320
- if (section.type !== "element") continue;
321
- const rowContainers = section.tag === "tr" ? [section] : section.tag === "thead" || section.tag === "tbody" || section.tag === "tfoot" ? section.children.filter((c) => c.type === "element" && c.tag === "tr") : [];
503
+ if (section.type === "element" && section.tag === "caption") continue;
504
+ if (section.type !== "element") {
505
+ if (isTextLikeNode(section)) strayNodes.push(section);
506
+ continue;
507
+ }
508
+ if (section.tag === "colgroup") {
509
+ collectColgroupStrayContent(section, strayNodes, state.context);
510
+ continue;
511
+ }
512
+ if (isInertElement(section.tag)) {
513
+ reportInertElementSkip(section.tag, state.context);
514
+ continue;
515
+ }
516
+ const rowContainers = section.tag === "tr" ? [section] : section.tag === "thead" || section.tag === "tbody" || section.tag === "tfoot" ? collectRowGroupRows(section, strayNodes, state.context) : void 0;
517
+ if (rowContainers === void 0) {
518
+ strayNodes.push(section);
519
+ continue;
520
+ }
322
521
  for (const tr of rowContainers) {
323
522
  const cells = [];
324
- for (const cellNode of tr.children) {
325
- if (cellNode.type !== "element" || cellNode.tag !== "td" && cellNode.tag !== "th") continue;
326
- const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
327
- const paragraph = {
328
- kind: "paragraph",
329
- runs: buildInlineRuns(cellNode.children, cellStyle, state.context).runs
330
- };
331
- const colSpan = positiveIntAttr(cellNode, "colspan");
332
- const rowSpan = positiveIntAttr(cellNode, "rowspan");
333
- cells.push({
334
- blocks: [paragraph],
335
- ...colSpan !== void 0 ? { colSpan } : {},
336
- ...rowSpan !== void 0 ? { rowSpan } : {}
523
+ let strayCellNodes = [];
524
+ const flushStrayCell = () => {
525
+ if (strayCellNodes.length === 0) return;
526
+ const recovered = readContainerChildren(strayCellNodes, state);
527
+ strayCellNodes = [];
528
+ if (recovered.length === 0) return;
529
+ state.context.sink({
530
+ code: EpubDiagnosticCodes.TABLE_ROW_CONTENT_OUTSIDE_CELL,
531
+ severity: "info",
532
+ message: "content sits directly inside a <tr> rather than inside a <td>/<th> (not valid HTML5); recovered as its own cell in the row's own column sequence",
533
+ href: state.context.sourceHref
337
534
  });
535
+ cells.push({ blocks: recovered });
536
+ };
537
+ for (const cellNode of tr.children) {
538
+ if (cellNode.type === "element" && (cellNode.tag === "td" || cellNode.tag === "th")) {
539
+ flushStrayCell();
540
+ const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
541
+ const inline = buildInlineRuns(cellNode.children, cellStyle, state.context);
542
+ const paragraph = {
543
+ kind: "paragraph",
544
+ runs: inline.runs,
545
+ ...constructsField(inline)
546
+ };
547
+ const colSpan = positiveIntAttr(cellNode, "colspan");
548
+ const rowSpan = positiveIntAttr(cellNode, "rowspan");
549
+ cells.push({
550
+ blocks: [paragraph],
551
+ ...colSpan !== void 0 ? { colSpan } : {},
552
+ ...rowSpan !== void 0 ? { rowSpan } : {}
553
+ });
554
+ continue;
555
+ }
556
+ if (cellNode.type === "element" && isInertElement(cellNode.tag)) {
557
+ reportInertElementSkip(cellNode.tag, state.context);
558
+ continue;
559
+ }
560
+ if (cellNode.type === "element" || isTextLikeNode(cellNode)) strayCellNodes.push(cellNode);
338
561
  }
562
+ flushStrayCell();
339
563
  columnCount = Math.max(columnCount, cells.length);
340
564
  rows.push({ cells });
341
565
  }
342
566
  }
567
+ const strayBlocks = strayNodes.length === 0 ? [] : readContainerChildren(strayNodes, state);
568
+ if (strayBlocks.length > 0) state.context.sink({
569
+ code: EpubDiagnosticCodes.TABLE_CONTENT_UNRECOGNIZED,
570
+ severity: "info",
571
+ message: "content sits directly inside a <table> (or one of its <thead>/<tbody>/<tfoot> row groups) outside any row, caption, or colgroup, or inside a <colgroup> itself (not valid HTML5); recovered as ordinary content immediately before the table",
572
+ href: state.context.sourceHref
573
+ });
343
574
  const width = columnCount > 0 ? state.contentWidthPt / columnCount : state.contentWidthPt;
344
- return {
575
+ const table = {
345
576
  kind: "table",
346
577
  rows,
347
578
  columnWidthsPt: new Array(Math.max(columnCount, 1)).fill(width)
348
579
  };
580
+ const captionBlocks = captionElements.flatMap((captionElement, index) => readTableCaption(captionElement, index > 0, state));
581
+ return [
582
+ ...strayBlocks,
583
+ ...captionBlocks,
584
+ table
585
+ ];
586
+ }
587
+ function collectRowGroupRows(section, strayNodes, context) {
588
+ const trs = [];
589
+ for (const child of section.children) {
590
+ if (child.type === "element" && child.tag === "tr") {
591
+ trs.push(child);
592
+ continue;
593
+ }
594
+ if (child.type === "element" && isInertElement(child.tag)) {
595
+ reportInertElementSkip(child.tag, context);
596
+ continue;
597
+ }
598
+ if (child.type === "element" || isTextLikeNode(child)) strayNodes.push(child);
599
+ }
600
+ return trs;
601
+ }
602
+ function collectColgroupStrayContent(section, strayNodes, context) {
603
+ for (const child of section.children) {
604
+ if (child.type === "element" && child.tag === "col") continue;
605
+ if (child.type === "element" && isInertElement(child.tag)) {
606
+ reportInertElementSkip(child.tag, context);
607
+ continue;
608
+ }
609
+ if (child.type === "element" || isTextLikeNode(child)) strayNodes.push(child);
610
+ }
611
+ }
612
+ function readTableCaption(captionElement, isDuplicate, state) {
613
+ const captionInline = buildInlineRuns(captionElement.children, {}, state.context);
614
+ if (captionInline.runs.every((run) => run.text.trim().length === 0) && captionInline.constructs.length === 0) return [];
615
+ if (isDuplicate) state.context.sink({
616
+ code: EpubDiagnosticCodes.TABLE_DUPLICATE_CAPTION,
617
+ severity: "info",
618
+ message: "<table> carries more than one <caption> (HTML5 permits at most one); every caption beyond the first is still read as its own ordinary paragraph immediately before the table, rather than being silently discarded",
619
+ href: state.context.sourceHref
620
+ });
621
+ state.context.sink({
622
+ code: EpubDiagnosticCodes.TABLE_CAPTION_UNSUPPORTED,
623
+ severity: "info",
624
+ message: "<caption> has no document-schema.js table-caption field to carry its own distinct tag; read as an ordinary paragraph immediately before the table",
625
+ href: state.context.sourceHref
626
+ });
627
+ return [decorateParagraph({
628
+ kind: "paragraph",
629
+ runs: captionInline.runs,
630
+ ...constructsField(captionInline)
631
+ }, state)];
349
632
  }
350
633
  function positiveIntAttr(element, name) {
351
634
  const raw = attrValue(element, name);
@@ -60,24 +60,31 @@ function writeSectionChildren(children, context) {
60
60
  return out;
61
61
  }
62
62
  function writeSectionChild(child, context) {
63
- if ((0, document_schema_js.isHeadingGroupNode)(child)) return [writeHeading(child), ...writeSectionChildren(child.children, context)];
63
+ if ((0, document_schema_js.isHeadingGroupNode)(child)) return [writeHeading(child, context), ...writeSectionChildren(child.children, context)];
64
64
  if ((0, document_schema_js.isListGroupNode)(child)) return [writeList([child], context)];
65
65
  if ((0, document_schema_js.isSectionConstructGroupNode)(child)) return writeSectionConstructGroup(child, context);
66
66
  return writeLeafBlock(child, context);
67
67
  }
68
- function writeHeading(group) {
68
+ function writeHeading(group, context) {
69
69
  const level = Math.min(6, Math.max(1, group.node.headingLevel));
70
- return element(`h${String(level)}`, {}, writeRunsToNodes(group.node.runs, group.node.constructs));
70
+ return element(`h${String(level)}`, {}, writeRunsToNodes(group.node.runs, group.node.constructs, context));
71
71
  }
72
72
  function writeList(items, context) {
73
73
  const numId = items[0]?.node.list.numId;
74
74
  const info = numId === void 0 ? void 0 : require_xhtml_list_id.parseListNumId(numId);
75
- return element(info?.type === "ordered" ? "ol" : "ul", info?.type === "ordered" && info.start !== void 0 ? { start: String(info.start) } : {}, items.map((item) => {
76
- const paragraph = item.node;
77
- const anchorNodes = writeRunsToNodes(paragraph.runs, paragraph.constructs);
78
- const nested = writeSectionChildren(item.children, context);
79
- return element("li", {}, [...anchorNodes, ...nested]);
80
- }));
75
+ const tag = info?.type === "ordered" ? "ol" : "ul";
76
+ const attrs = info?.type === "ordered" && info.start !== void 0 ? { start: String(info.start) } : {};
77
+ const liNodes = [];
78
+ let index = 0;
79
+ while (index < items.length) {
80
+ const itemId = items[index]?.node.list.itemId;
81
+ let end = index + 1;
82
+ if (itemId !== void 0) while (end < items.length && items[end]?.node.list.itemId === itemId) end += 1;
83
+ const liChildren = items.slice(index, end).flatMap((item, position) => [...writeParagraphAsEmbeddedNodes(item.node, context, position > 0), ...writeSectionChildren(item.children, context)]);
84
+ liNodes.push(element("li", {}, liChildren));
85
+ index = end;
86
+ }
87
+ return element(tag, attrs, liNodes);
81
88
  }
82
89
  function writeSectionConstructGroup(group, context) {
83
90
  const descriptor = group.node;
@@ -96,7 +103,7 @@ function writeSectionConstructGroup(group, context) {
96
103
  }
97
104
  function writeLeafBlock(block, context) {
98
105
  switch (block.kind) {
99
- case "paragraph": return [writeParagraph(block)];
106
+ case "paragraph": return [writeParagraph(block, context)];
100
107
  case "table": return [writeTable(block, context)];
101
108
  case "image": return [writeImage(block, context)];
102
109
  case "pageBreak": return [];
@@ -111,38 +118,80 @@ function writeLeafBlock(block, context) {
111
118
  }
112
119
  }
113
120
  function isPreBlockParagraph(paragraph) {
121
+ if (paragraph.preformatted === true) return true;
114
122
  if (paragraph.codeLanguage !== void 0) return true;
115
123
  return paragraph.runs.length === 1 && paragraph.runs[0]?.fontFamily === "Courier New" && paragraph.runs[0].text.includes("\n");
116
124
  }
117
- function writeParagraph(paragraph) {
118
- if (paragraph.styleId === "HorizontalRule" && paragraph.runs.length === 0) return element("hr");
119
- if (isPreBlockParagraph(paragraph)) return element("pre", {}, [element("code", paragraph.codeLanguage === void 0 ? {} : { class: `language-${paragraph.codeLanguage}` }, [text(paragraph.runs.map((run) => run.text).join(""))])]);
120
- return element("p", {}, writeRunsToNodes(paragraph.runs, paragraph.constructs));
125
+ function isHorizontalRuleParagraph(paragraph) {
126
+ return paragraph.styleId === "HorizontalRule" && paragraph.runs.length === 0;
127
+ }
128
+ function writePreElement(paragraph, context) {
129
+ return element("pre", {}, [element("code", paragraph.codeLanguage === void 0 ? {} : { class: `language-${paragraph.codeLanguage}` }, writePreRunsToNodes(paragraph.runs, paragraph.constructs, context))]);
130
+ }
131
+ function writeParagraph(paragraph, context) {
132
+ if (isHorizontalRuleParagraph(paragraph)) return element("hr");
133
+ if (isPreBlockParagraph(paragraph)) return writePreElement(paragraph, context);
134
+ return element("p", {}, writeRunsToNodes(paragraph.runs, paragraph.constructs, context));
135
+ }
136
+ function writeParagraphAsEmbeddedNodes(paragraph, context, wrapOrdinaryInParagraph) {
137
+ if (isHorizontalRuleParagraph(paragraph)) return [element("hr")];
138
+ if (isPreBlockParagraph(paragraph)) return [writePreElement(paragraph, context)];
139
+ const runNodes = writeRunsToNodes(paragraph.runs, paragraph.constructs, context);
140
+ return wrapOrdinaryInParagraph ? [element("p", {}, runNodes)] : runNodes;
121
141
  }
122
142
  function isFootnoteExtent(construct) {
123
143
  return construct.descriptor.kind === "anchor" && construct.descriptor.anchorType === "footnote";
124
144
  }
125
- function writeRunsToNodes(runs, constructs) {
145
+ function describeUnrepresentedFootnoteExtent(extent) {
146
+ if (extent.endRun === extent.startRun) return `a footnote reference ('${extent.descriptor.name}') marking the boundary before run ${extent.startRun} is never reached by the write walk -- either because it sits inside another footnote reference's own winning run range, or because its own run range falls outside this paragraph's actual runs; a point anchor wraps no run text of its own, so nothing besides this reference's own <a> link is missing from the output`;
147
+ return `a footnote reference ('${extent.descriptor.name}') is never reached by the write walk and cannot be represented as its own <a> element -- either because it overlaps another footnote reference's own winning run range, or because its own run range falls outside this paragraph's actual runs; wherever an overlap is the cause, the run text underneath it is not lost, since it is still written, wrapped by whichever extent's range actually claims it, or written unwrapped past it, but this reference's own anchor link is not emitted regardless of cause`;
148
+ }
149
+ function writeRunRangeNodes(runs, constructs, renderRun, renderExtentRange, context) {
126
150
  const footnoteExtents = (constructs ?? []).filter(isFootnoteExtent);
151
+ const emittedExtents = /* @__PURE__ */ new Set();
127
152
  const out = [];
128
153
  let index = 0;
129
- while (index < runs.length) {
130
- const extent = footnoteExtents.find((e) => e.startRun === index);
131
- if (extent !== void 0) {
132
- const inner = runs.slice(extent.startRun, extent.endRun).flatMap((run) => writeRunNodes(run));
154
+ while (index <= runs.length) {
155
+ const extentsHere = footnoteExtents.filter((e) => e.startRun === index);
156
+ for (const point of extentsHere.filter((e) => e.endRun === e.startRun)) {
157
+ emittedExtents.add(point);
133
158
  out.push(element("a", {
134
159
  "epub:type": "noteref",
135
- href: `#${extent.descriptor.name}`
136
- }, inner));
137
- index = Math.max(extent.endRun, index + 1);
160
+ href: `#${point.descriptor.name}`
161
+ }, renderExtentRange([])));
162
+ }
163
+ const [primaryRange] = index < runs.length ? extentsHere.filter((e) => e.endRun > e.startRun) : [];
164
+ if (primaryRange !== void 0) {
165
+ emittedExtents.add(primaryRange);
166
+ const rangeRuns = runs.slice(primaryRange.startRun, primaryRange.endRun);
167
+ out.push(element("a", {
168
+ "epub:type": "noteref",
169
+ href: `#${primaryRange.descriptor.name}`
170
+ }, renderExtentRange(rangeRuns)));
171
+ index = primaryRange.endRun;
138
172
  continue;
139
173
  }
140
174
  const run = runs[index];
141
- if (run !== void 0) out.push(...writeRunNodes(run));
175
+ if (run !== void 0) out.push(...renderRun(run));
142
176
  index += 1;
143
177
  }
178
+ for (const extent of footnoteExtents) {
179
+ if (emittedExtents.has(extent)) continue;
180
+ context.sink({
181
+ code: require_diagnostics.EpubDiagnosticCodes.CONSTRUCT_UNREPRESENTED,
182
+ severity: "info",
183
+ message: describeUnrepresentedFootnoteExtent(extent),
184
+ href: context.sourceHref
185
+ });
186
+ }
144
187
  return out;
145
188
  }
189
+ function writeRunsToNodes(runs, constructs, context) {
190
+ return writeRunRangeNodes(runs, constructs, writeRunNodes, (rangeRuns) => rangeRuns.flatMap((run) => writeRunNodes(run)), context);
191
+ }
192
+ function writePreRunsToNodes(runs, constructs, context) {
193
+ return writeRunRangeNodes(runs, constructs, (run) => run.text.length > 0 ? [text(run.text)] : [], (rangeRuns) => [text(rangeRuns.map((run) => run.text).join(""))], context);
194
+ }
146
195
  function writeRunNodes(run) {
147
196
  const segments = run.text.split("\n");
148
197
  let nodes = [];
@@ -1,4 +1,4 @@
1
- import { i as EpubDiagnosticSink } from "../diagnostics-tvGwPH46.cjs";
1
+ import { i as EpubDiagnosticSink } from "../diagnostics-C0CRdoda.cjs";
2
2
  import { XmlElement } from "../xml/node.cjs";
3
3
  import { ContentBlock, ContentImageBlock } from "document-schema.js";
4
4
  //#region src/xhtml/write.d.ts
@@ -1,4 +1,4 @@
1
- import { i as EpubDiagnosticSink } from "../diagnostics-tvGwPH46.js";
1
+ import { i as EpubDiagnosticSink } from "../diagnostics-C0CRdoda.js";
2
2
  import { XmlElement } from "../xml/node.js";
3
3
  import { ContentBlock, ContentImageBlock } from "document-schema.js";
4
4
  //#region src/xhtml/write.d.ts