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,6 +1,8 @@
1
1
  import { EpubDiagnosticCodes } from "../diagnostics.js";
2
+ import { decodeEntities, decodeTextLikeNode } from "../xml/entities.js";
3
+ import { isTextLikeNode } from "../xml/node.js";
2
4
  import { attrValue } from "../xml/query.js";
3
- import { decodeEntities } from "../xml/entities.js";
5
+ import { isInertElement, reportInertElementSkip } from "./context.js";
4
6
  import { isFootnoteReferenceAnchor, sameDocumentFragment } from "./footnote.js";
5
7
  import { MONOSPACE_FONT_FAMILY } from "./style-constants.js";
6
8
  //#region src/xhtml/inline.ts
@@ -27,8 +29,8 @@ function buildInlineRuns(nodes, style, context) {
27
29
  const runs = [];
28
30
  const constructs = [];
29
31
  for (const node of nodes) {
30
- if (node.type === "text") {
31
- const text = normalizeWhitespace(decodeEntities(node.value));
32
+ if (isTextLikeNode(node)) {
33
+ const text = normalizeWhitespace(decodeTextLikeNode(node));
32
34
  if (text.length > 0) runs.push(styledRun(text, style));
33
35
  continue;
34
36
  }
@@ -41,6 +43,10 @@ function buildInlineRuns(nodes, style, context) {
41
43
  };
42
44
  }
43
45
  function appendElement(element, style, context, runs, constructs) {
46
+ if (isInertElement(element.tag)) {
47
+ reportInertElementSkip(element.tag, context);
48
+ return;
49
+ }
44
50
  switch (element.tag) {
45
51
  case "strong":
46
52
  case "b":
@@ -1,11 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_diagnostics = require("../diagnostics.cjs");
3
+ const require_xml_entities = require("../xml/entities.cjs");
4
+ const require_xml_node = require("../xml/node.cjs");
3
5
  const require_xml_query = require("../xml/query.cjs");
4
6
  const require_xml_parse = require("../xml/parse.cjs");
5
- const require_xml_entities = require("../xml/entities.cjs");
6
7
  const require_image_dimensions = require("../image/dimensions.cjs");
7
8
  const require_util_base64 = require("../util/base64.cjs");
8
9
  const require_xml_build = require("../xml/build.cjs");
10
+ const require_xhtml_context = require("./context.cjs");
9
11
  const require_xhtml_footnote = require("./footnote.cjs");
10
12
  const require_xhtml_style_constants = require("./style-constants.cjs");
11
13
  const require_xhtml_inline = require("./inline.cjs");
@@ -62,18 +64,32 @@ function decorateParagraph(paragraph, state) {
62
64
  }
63
65
  return decorated;
64
66
  }
67
+ function constructsField(inline) {
68
+ return inline.constructs.length > 0 ? { constructs: inline.constructs } : {};
69
+ }
65
70
  function buildIdElementMap(nodes) {
66
71
  const map = /* @__PURE__ */ new Map();
67
72
  const stack = [...nodes];
68
73
  while (stack.length > 0) {
69
74
  const node = stack.pop();
70
- if (node?.type !== "element") continue;
75
+ if (node?.type !== "element" || require_xhtml_context.isInertElement(node.tag)) continue;
71
76
  const id = require_xml_query.attrValue(node, "id");
72
77
  if (id !== void 0 && !map.has(id)) map.set(id, node);
73
78
  stack.push(...node.children);
74
79
  }
75
80
  return map;
76
81
  }
82
+ function elementsWithTagSkippingInert(nodes, tag) {
83
+ const out = [];
84
+ const stack = [...nodes];
85
+ while (stack.length > 0) {
86
+ const node = stack.pop();
87
+ if (node?.type !== "element" || require_xhtml_context.isInertElement(node.tag)) continue;
88
+ if (node.tag === tag) out.push(node);
89
+ stack.push(...node.children);
90
+ }
91
+ return out;
92
+ }
77
93
  function readStyleResidue(html, context) {
78
94
  const head = require_xml_query.findChildElement(html.children, "head");
79
95
  if (head === void 0) return;
@@ -100,7 +116,7 @@ function readXhtmlBody(xml, options) {
100
116
  };
101
117
  const idElements = buildIdElementMap(body.children);
102
118
  const footnoteTargetIds = /* @__PURE__ */ new Set();
103
- for (const anchor of require_xml_query.elementsWithTag(body.children, "a")) {
119
+ for (const anchor of elementsWithTagSkippingInert(body.children, "a")) {
104
120
  const name = require_xhtml_footnote.isFootnoteReferenceAnchor(anchor, idElements);
105
121
  if (name !== void 0) footnoteTargetIds.add(name);
106
122
  }
@@ -156,11 +172,11 @@ function readContainerChildren(nodes, state) {
156
172
  if (segment.length === 0) return;
157
173
  const inline = require_xhtml_inline.buildInlineRuns(segment, {}, state.context);
158
174
  segment = [];
159
- if (inline.runs.every((run) => run.text.trim().length === 0)) return;
175
+ if (inline.runs.every((run) => run.text.trim().length === 0) && inline.constructs.length === 0) return;
160
176
  const paragraph = {
161
177
  kind: "paragraph",
162
178
  runs: inline.runs,
163
- ...inline.constructs.length > 0 ? { constructs: inline.constructs } : {}
179
+ ...constructsField(inline)
164
180
  };
165
181
  blocks.push(decorateParagraph(paragraph, state));
166
182
  };
@@ -170,7 +186,7 @@ function readContainerChildren(nodes, state) {
170
186
  blocks.push(...readBlockElement(node, state));
171
187
  continue;
172
188
  }
173
- if (node.type === "element" || node.type === "text") segment.push(node);
189
+ if (node.type === "element" || require_xml_node.isTextLikeNode(node)) segment.push(node);
174
190
  }
175
191
  flush();
176
192
  return blocks;
@@ -205,7 +221,7 @@ function readBlockElementInner(element, state) {
205
221
  kind: "paragraph",
206
222
  headingLevel,
207
223
  runs: inline.runs,
208
- ...inline.constructs.length > 0 ? { constructs: inline.constructs } : {}
224
+ ...constructsField(inline)
209
225
  }, state)];
210
226
  }
211
227
  switch (element.tag) {
@@ -220,12 +236,16 @@ function readBlockElementInner(element, state) {
220
236
  case "ul":
221
237
  case "ol": return readList(element, state);
222
238
  case "dl": return readDefinitionList(element, state);
223
- case "table": return [readTable(element, state)];
239
+ case "table": return readTable(element, state);
224
240
  case "figure": return readContainerChildren(element.children, state);
225
- case "figcaption": return [decorateParagraph({
226
- kind: "paragraph",
227
- runs: require_xhtml_inline.buildInlineRuns(element.children, {}, state.context).runs
228
- }, state)];
241
+ case "figcaption": {
242
+ const inline = require_xhtml_inline.buildInlineRuns(element.children, {}, state.context);
243
+ return [decorateParagraph({
244
+ kind: "paragraph",
245
+ runs: inline.runs,
246
+ ...constructsField(inline)
247
+ }, state)];
248
+ }
229
249
  case "img": {
230
250
  const block = readImage(element, state);
231
251
  return block === void 0 ? [] : [block];
@@ -237,23 +257,123 @@ function readBlockElementInner(element, state) {
237
257
  function readPre(element, state) {
238
258
  const languageSource = require_xml_query.findChildElement(element.children, "code") ?? element;
239
259
  const codeLanguage = languageFromClass(require_xml_query.attrValue(languageSource, "class"));
240
- const text = require_xml_entities.decodeEntities(require_xml_query.textContent(element.children));
260
+ const inline = containsFootnoteReference(element.children, state.context) ? readPreRuns(element.children, state.context) : {
261
+ runs: readPreFlatRuns(element.children, state.context),
262
+ constructs: []
263
+ };
241
264
  return decorateParagraph({
242
265
  kind: "paragraph",
243
- runs: text.length > 0 ? [{
244
- text,
245
- fontFamily: require_xhtml_style_constants.MONOSPACE_FONT_FAMILY
246
- }] : [],
247
- ...codeLanguage !== void 0 ? { codeLanguage } : {}
266
+ runs: inline.runs,
267
+ preformatted: true,
268
+ ...codeLanguage !== void 0 ? { codeLanguage } : {},
269
+ ...constructsField(inline)
248
270
  }, state);
249
271
  }
272
+ function readPreFlatRuns(nodes, context) {
273
+ const text = readPreText(nodes, context);
274
+ return text.length > 0 ? [{
275
+ text,
276
+ fontFamily: require_xhtml_style_constants.MONOSPACE_FONT_FAMILY
277
+ }] : [];
278
+ }
279
+ function containsFootnoteReference(nodes, context) {
280
+ for (const node of nodes) {
281
+ if (node.type !== "element" || require_xhtml_context.isInertElement(node.tag)) continue;
282
+ if (node.tag === "a" && require_xhtml_footnote.isFootnoteReferenceAnchor(node, context.idElements) !== void 0) return true;
283
+ if (containsFootnoteReference(node.children, context)) return true;
284
+ }
285
+ return false;
286
+ }
287
+ function readPreRuns(nodes, context) {
288
+ const runs = [];
289
+ const constructs = [];
290
+ let buffer = "";
291
+ const flush = () => {
292
+ if (buffer.length === 0) return;
293
+ runs.push({
294
+ text: buffer,
295
+ fontFamily: require_xhtml_style_constants.MONOSPACE_FONT_FAMILY
296
+ });
297
+ buffer = "";
298
+ };
299
+ for (const node of nodes) {
300
+ if (require_xml_node.isTextLikeNode(node)) {
301
+ buffer += require_xml_entities.decodeTextLikeNode(node);
302
+ continue;
303
+ }
304
+ if (node.type !== "element") continue;
305
+ if (require_xhtml_context.isInertElement(node.tag)) {
306
+ require_xhtml_context.reportInertElementSkip(node.tag, context);
307
+ continue;
308
+ }
309
+ if (node.tag === "img") {
310
+ buffer += require_xml_entities.decodeEntities(readPreImageFallbackText(node, context));
311
+ continue;
312
+ }
313
+ if (node.tag === "br") {
314
+ buffer += "\n";
315
+ continue;
316
+ }
317
+ const footnoteName = node.tag === "a" ? require_xhtml_footnote.isFootnoteReferenceAnchor(node, context.idElements) : void 0;
318
+ if (footnoteName === void 0 && !containsFootnoteReference([node], context)) {
319
+ buffer += readPreText(node.children, context);
320
+ continue;
321
+ }
322
+ flush();
323
+ const startRun = runs.length;
324
+ const nested = readPreRuns(node.children, context);
325
+ runs.push(...nested.runs);
326
+ for (const construct of nested.constructs) constructs.push({
327
+ ...construct,
328
+ startRun: construct.startRun + startRun,
329
+ endRun: construct.endRun + startRun
330
+ });
331
+ if (footnoteName !== void 0) constructs.push({
332
+ descriptor: {
333
+ kind: "anchor",
334
+ anchorType: "footnote",
335
+ name: footnoteName
336
+ },
337
+ startRun,
338
+ endRun: runs.length
339
+ });
340
+ }
341
+ flush();
342
+ return {
343
+ runs,
344
+ constructs
345
+ };
346
+ }
347
+ function readPreText(nodes, context) {
348
+ let out = "";
349
+ for (const node of nodes) if (require_xml_node.isTextLikeNode(node)) out += require_xml_entities.decodeTextLikeNode(node);
350
+ else if (node.type === "element" && node.tag === "br") out += "\n";
351
+ else if (node.type === "element" && node.tag === "img") out += require_xml_entities.decodeEntities(readPreImageFallbackText(node, context));
352
+ else if (node.type === "element" && require_xhtml_context.isInertElement(node.tag)) {
353
+ require_xhtml_context.reportInertElementSkip(node.tag, context);
354
+ continue;
355
+ } else if (node.type === "element") out += readPreText(node.children, context);
356
+ return out;
357
+ }
358
+ function readPreImageFallbackText(element, context) {
359
+ const src = require_xml_query.attrValue(element, "src");
360
+ const alt = require_xml_query.attrValue(element, "alt");
361
+ const label = src === void 0 ? "<img>" : `<img src="${src}">`;
362
+ context.sink({
363
+ code: require_diagnostics.EpubDiagnosticCodes.IMAGE_PRE_UNSUPPORTED,
364
+ severity: "warning",
365
+ 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`,
366
+ href: context.sourceHref
367
+ });
368
+ return alt ?? "";
369
+ }
250
370
  function languageFromClass(className) {
251
371
  if (className === void 0) return;
252
372
  return /(?:^|\s)language-(\S+)/u.exec(className)?.[1];
253
373
  }
254
374
  function containsHeading(nodes) {
255
375
  for (const node of nodes) {
256
- if (node.type !== "element") continue;
376
+ if (node.type !== "element" || require_xhtml_context.isInertElement(node.tag)) continue;
257
377
  if (headingLevelOf(node.tag) !== void 0) return true;
258
378
  if (containsHeading(node.children)) return true;
259
379
  }
@@ -279,74 +399,237 @@ function readList(element, state) {
279
399
  });
280
400
  const level = state.list === void 0 ? 0 : state.list.level + 1;
281
401
  const blocks = [];
402
+ let previousItem;
403
+ let strayNodes = [];
282
404
  for (const child of element.children) {
283
- if (child.type !== "element" || child.tag !== "li") continue;
284
- const itemState = withListItem(state, {
285
- numId,
286
- level,
287
- itemId: state.minter.mintItemId()
288
- });
289
- blocks.push(...readContainerChildren(child.children, itemState));
405
+ if (child.type === "element" && child.tag === "li") {
406
+ blocks.push(...flushListStrayContent(strayNodes, previousItem, element.tag, state));
407
+ strayNodes = [];
408
+ previousItem = {
409
+ numId,
410
+ level,
411
+ itemId: state.minter.mintItemId()
412
+ };
413
+ blocks.push(...readContainerChildren(child.children, withListItem(state, previousItem)));
414
+ continue;
415
+ }
416
+ if (child.type === "element" && require_xhtml_context.isInertElement(child.tag)) {
417
+ require_xhtml_context.reportInertElementSkip(child.tag, state.context);
418
+ continue;
419
+ }
420
+ if (child.type === "element" || require_xml_node.isTextLikeNode(child)) strayNodes.push(child);
290
421
  }
422
+ blocks.push(...flushListStrayContent(strayNodes, previousItem, element.tag, state));
423
+ return blocks;
424
+ }
425
+ function flushListStrayContent(nodes, previousItem, tag, state) {
426
+ if (nodes.length === 0) return [];
427
+ const blocks = readContainerChildren(nodes, previousItem === void 0 ? state : withListItem(state, previousItem));
428
+ if (blocks.length === 0) return [];
429
+ state.context.sink({
430
+ code: require_diagnostics.EpubDiagnosticCodes.LIST_CONTENT_OUTSIDE_ITEM,
431
+ severity: "info",
432
+ 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`,
433
+ href: state.context.sourceHref
434
+ });
291
435
  return blocks;
292
436
  }
293
437
  function startAttr(element) {
294
438
  return positiveIntAttr(element, "start");
295
439
  }
296
440
  function readDefinitionList(element, state) {
441
+ return readDefinitionListEntries(element.children, state);
442
+ }
443
+ function readDefinitionListEntries(nodes, state) {
297
444
  const blocks = [];
298
- for (const child of element.children) {
299
- if (child.type !== "element") continue;
300
- if (child.tag === "dt") {
445
+ let strayNodes = [];
446
+ const flushStray = () => {
447
+ blocks.push(...flushDefinitionListStrayContent(strayNodes, state));
448
+ strayNodes = [];
449
+ };
450
+ for (const child of nodes) {
451
+ if (child.type === "element" && child.tag === "dt") {
452
+ flushStray();
301
453
  const inline = require_xhtml_inline.buildInlineRuns(child.children, {}, state.context);
302
454
  blocks.push(decorateParagraph({
303
455
  kind: "paragraph",
304
- runs: inline.runs
456
+ runs: inline.runs,
457
+ ...constructsField(inline)
305
458
  }, state));
306
- } else if (child.tag === "dd") {
459
+ continue;
460
+ }
461
+ if (child.type === "element" && child.tag === "dd") {
462
+ flushStray();
307
463
  const inline = require_xhtml_inline.buildInlineRuns(child.children, {}, state.context);
308
464
  blocks.push(decorateParagraph({
309
465
  kind: "paragraph",
310
466
  runs: inline.runs,
467
+ ...constructsField(inline),
311
468
  indentLeftPt: 36 + state.quoteDepth * 36
312
469
  }, state));
470
+ continue;
471
+ }
472
+ if (child.type === "element" && child.tag === "div") {
473
+ flushStray();
474
+ blocks.push(...readDefinitionListEntries(child.children, state));
475
+ continue;
313
476
  }
477
+ if (child.type === "element" && require_xhtml_context.isInertElement(child.tag)) {
478
+ require_xhtml_context.reportInertElementSkip(child.tag, state.context);
479
+ continue;
480
+ }
481
+ if (child.type === "element" || require_xml_node.isTextLikeNode(child)) strayNodes.push(child);
314
482
  }
483
+ flushStray();
484
+ return blocks;
485
+ }
486
+ function flushDefinitionListStrayContent(nodes, state) {
487
+ if (nodes.length === 0) return [];
488
+ const blocks = readContainerChildren(nodes, state);
489
+ if (blocks.length === 0) return [];
490
+ state.context.sink({
491
+ code: require_diagnostics.EpubDiagnosticCodes.DEFINITION_LIST_CONTENT_OUTSIDE_ENTRY,
492
+ severity: "info",
493
+ 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",
494
+ href: state.context.sourceHref
495
+ });
315
496
  return blocks;
316
497
  }
317
498
  function readTable(element, state) {
499
+ const captionElements = element.children.filter((c) => c.type === "element" && c.tag === "caption");
318
500
  const rows = [];
319
501
  let columnCount = 0;
502
+ const strayNodes = [];
320
503
  for (const section of element.children) {
321
- if (section.type !== "element") continue;
322
- 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") : [];
504
+ if (section.type === "element" && section.tag === "caption") continue;
505
+ if (section.type !== "element") {
506
+ if (require_xml_node.isTextLikeNode(section)) strayNodes.push(section);
507
+ continue;
508
+ }
509
+ if (section.tag === "colgroup") {
510
+ collectColgroupStrayContent(section, strayNodes, state.context);
511
+ continue;
512
+ }
513
+ if (require_xhtml_context.isInertElement(section.tag)) {
514
+ require_xhtml_context.reportInertElementSkip(section.tag, state.context);
515
+ continue;
516
+ }
517
+ const rowContainers = section.tag === "tr" ? [section] : section.tag === "thead" || section.tag === "tbody" || section.tag === "tfoot" ? collectRowGroupRows(section, strayNodes, state.context) : void 0;
518
+ if (rowContainers === void 0) {
519
+ strayNodes.push(section);
520
+ continue;
521
+ }
323
522
  for (const tr of rowContainers) {
324
523
  const cells = [];
325
- for (const cellNode of tr.children) {
326
- if (cellNode.type !== "element" || cellNode.tag !== "td" && cellNode.tag !== "th") continue;
327
- const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
328
- const paragraph = {
329
- kind: "paragraph",
330
- runs: require_xhtml_inline.buildInlineRuns(cellNode.children, cellStyle, state.context).runs
331
- };
332
- const colSpan = positiveIntAttr(cellNode, "colspan");
333
- const rowSpan = positiveIntAttr(cellNode, "rowspan");
334
- cells.push({
335
- blocks: [paragraph],
336
- ...colSpan !== void 0 ? { colSpan } : {},
337
- ...rowSpan !== void 0 ? { rowSpan } : {}
524
+ let strayCellNodes = [];
525
+ const flushStrayCell = () => {
526
+ if (strayCellNodes.length === 0) return;
527
+ const recovered = readContainerChildren(strayCellNodes, state);
528
+ strayCellNodes = [];
529
+ if (recovered.length === 0) return;
530
+ state.context.sink({
531
+ code: require_diagnostics.EpubDiagnosticCodes.TABLE_ROW_CONTENT_OUTSIDE_CELL,
532
+ severity: "info",
533
+ 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",
534
+ href: state.context.sourceHref
338
535
  });
536
+ cells.push({ blocks: recovered });
537
+ };
538
+ for (const cellNode of tr.children) {
539
+ if (cellNode.type === "element" && (cellNode.tag === "td" || cellNode.tag === "th")) {
540
+ flushStrayCell();
541
+ const cellStyle = cellNode.tag === "th" ? { bold: true } : {};
542
+ const inline = require_xhtml_inline.buildInlineRuns(cellNode.children, cellStyle, state.context);
543
+ const paragraph = {
544
+ kind: "paragraph",
545
+ runs: inline.runs,
546
+ ...constructsField(inline)
547
+ };
548
+ const colSpan = positiveIntAttr(cellNode, "colspan");
549
+ const rowSpan = positiveIntAttr(cellNode, "rowspan");
550
+ cells.push({
551
+ blocks: [paragraph],
552
+ ...colSpan !== void 0 ? { colSpan } : {},
553
+ ...rowSpan !== void 0 ? { rowSpan } : {}
554
+ });
555
+ continue;
556
+ }
557
+ if (cellNode.type === "element" && require_xhtml_context.isInertElement(cellNode.tag)) {
558
+ require_xhtml_context.reportInertElementSkip(cellNode.tag, state.context);
559
+ continue;
560
+ }
561
+ if (cellNode.type === "element" || require_xml_node.isTextLikeNode(cellNode)) strayCellNodes.push(cellNode);
339
562
  }
563
+ flushStrayCell();
340
564
  columnCount = Math.max(columnCount, cells.length);
341
565
  rows.push({ cells });
342
566
  }
343
567
  }
568
+ const strayBlocks = strayNodes.length === 0 ? [] : readContainerChildren(strayNodes, state);
569
+ if (strayBlocks.length > 0) state.context.sink({
570
+ code: require_diagnostics.EpubDiagnosticCodes.TABLE_CONTENT_UNRECOGNIZED,
571
+ severity: "info",
572
+ 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",
573
+ href: state.context.sourceHref
574
+ });
344
575
  const width = columnCount > 0 ? state.contentWidthPt / columnCount : state.contentWidthPt;
345
- return {
576
+ const table = {
346
577
  kind: "table",
347
578
  rows,
348
579
  columnWidthsPt: new Array(Math.max(columnCount, 1)).fill(width)
349
580
  };
581
+ const captionBlocks = captionElements.flatMap((captionElement, index) => readTableCaption(captionElement, index > 0, state));
582
+ return [
583
+ ...strayBlocks,
584
+ ...captionBlocks,
585
+ table
586
+ ];
587
+ }
588
+ function collectRowGroupRows(section, strayNodes, context) {
589
+ const trs = [];
590
+ for (const child of section.children) {
591
+ if (child.type === "element" && child.tag === "tr") {
592
+ trs.push(child);
593
+ continue;
594
+ }
595
+ if (child.type === "element" && require_xhtml_context.isInertElement(child.tag)) {
596
+ require_xhtml_context.reportInertElementSkip(child.tag, context);
597
+ continue;
598
+ }
599
+ if (child.type === "element" || require_xml_node.isTextLikeNode(child)) strayNodes.push(child);
600
+ }
601
+ return trs;
602
+ }
603
+ function collectColgroupStrayContent(section, strayNodes, context) {
604
+ for (const child of section.children) {
605
+ if (child.type === "element" && child.tag === "col") continue;
606
+ if (child.type === "element" && require_xhtml_context.isInertElement(child.tag)) {
607
+ require_xhtml_context.reportInertElementSkip(child.tag, context);
608
+ continue;
609
+ }
610
+ if (child.type === "element" || require_xml_node.isTextLikeNode(child)) strayNodes.push(child);
611
+ }
612
+ }
613
+ function readTableCaption(captionElement, isDuplicate, state) {
614
+ const captionInline = require_xhtml_inline.buildInlineRuns(captionElement.children, {}, state.context);
615
+ if (captionInline.runs.every((run) => run.text.trim().length === 0) && captionInline.constructs.length === 0) return [];
616
+ if (isDuplicate) state.context.sink({
617
+ code: require_diagnostics.EpubDiagnosticCodes.TABLE_DUPLICATE_CAPTION,
618
+ severity: "info",
619
+ 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",
620
+ href: state.context.sourceHref
621
+ });
622
+ state.context.sink({
623
+ code: require_diagnostics.EpubDiagnosticCodes.TABLE_CAPTION_UNSUPPORTED,
624
+ severity: "info",
625
+ 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",
626
+ href: state.context.sourceHref
627
+ });
628
+ return [decorateParagraph({
629
+ kind: "paragraph",
630
+ runs: captionInline.runs,
631
+ ...constructsField(captionInline)
632
+ }, state)];
350
633
  }
351
634
  function positiveIntAttr(element, name) {
352
635
  const raw = require_xml_query.attrValue(element, name);