@asciidoctor/core 3.0.4 → 4.0.0-alpha.1

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 (91) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24154 -0
  3. package/build/node/index.cjs +24735 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +53 -100
  6. package/src/abstract_block.js +849 -0
  7. package/src/abstract_node.js +954 -0
  8. package/src/attribute_entry.js +12 -0
  9. package/src/attribute_list.js +380 -0
  10. package/src/block.js +168 -0
  11. package/src/browser/asset.js +22 -0
  12. package/src/browser/reader.js +138 -0
  13. package/src/browser.js +121 -0
  14. package/src/callouts.js +85 -0
  15. package/src/compliance.js +54 -0
  16. package/src/constants.js +665 -0
  17. package/src/convert.js +370 -0
  18. package/src/converter/composite.js +83 -0
  19. package/src/converter/docbook5.js +1031 -0
  20. package/src/converter/html5.js +1899 -0
  21. package/src/converter/manpage.js +935 -0
  22. package/src/converter/template.js +459 -0
  23. package/src/converter.js +478 -0
  24. package/src/data/stylesheet-data.js +2 -0
  25. package/src/document.js +2134 -0
  26. package/src/extensions.js +1952 -0
  27. package/src/footnote.js +28 -0
  28. package/src/helpers.js +355 -0
  29. package/src/index.js +138 -0
  30. package/src/inline.js +158 -0
  31. package/src/list.js +240 -0
  32. package/src/load.js +276 -0
  33. package/src/logging.js +526 -0
  34. package/src/parser.js +3661 -0
  35. package/src/path_resolver.js +472 -0
  36. package/src/reader.js +1755 -0
  37. package/src/rx.js +829 -0
  38. package/src/section.js +354 -0
  39. package/src/stylesheets.js +30 -0
  40. package/src/substitutors.js +2241 -0
  41. package/src/syntaxHighlighter/highlightjs.js +90 -0
  42. package/src/syntaxHighlighter/html_pipeline.js +33 -0
  43. package/src/syntax_highlighter.js +304 -0
  44. package/src/table.js +952 -0
  45. package/src/timings.js +78 -0
  46. package/types/abstract_block.d.ts +343 -0
  47. package/types/abstract_node.d.ts +471 -0
  48. package/types/attribute_entry.d.ts +7 -0
  49. package/types/attribute_list.d.ts +52 -0
  50. package/types/block.d.ts +55 -0
  51. package/types/browser/asset.d.ts +7 -0
  52. package/types/browser/reader.d.ts +29 -0
  53. package/types/callouts.d.ts +36 -0
  54. package/types/compliance.d.ts +23 -0
  55. package/types/constants.d.ts +268 -0
  56. package/types/convert.d.ts +34 -0
  57. package/types/converter/composite.d.ts +20 -0
  58. package/types/converter/docbook5.d.ts +41 -0
  59. package/types/converter/html5.d.ts +51 -0
  60. package/types/converter/manpage.d.ts +59 -0
  61. package/types/converter/template.d.ts +83 -0
  62. package/types/converter.d.ts +150 -0
  63. package/types/data/stylesheet-data.d.ts +2 -0
  64. package/types/document.d.ts +495 -0
  65. package/types/extensions.d.ts +876 -0
  66. package/types/footnote.d.ts +18 -0
  67. package/types/helpers.d.ts +146 -0
  68. package/types/index.d.ts +73 -3731
  69. package/types/inline.d.ts +69 -0
  70. package/types/list.d.ts +114 -0
  71. package/types/load.d.ts +39 -0
  72. package/types/logging.d.ts +187 -0
  73. package/types/parser.d.ts +114 -0
  74. package/types/path_resolver.d.ts +103 -0
  75. package/types/reader.d.ts +184 -0
  76. package/types/rx.d.ts +513 -0
  77. package/types/section.d.ts +122 -0
  78. package/types/stylesheets.d.ts +10 -0
  79. package/types/substitutors.d.ts +208 -0
  80. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  81. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  82. package/types/syntax_highlighter.d.ts +167 -0
  83. package/types/table.d.ts +231 -0
  84. package/types/timings.d.ts +25 -0
  85. package/types/tsconfig.json +9 -0
  86. package/LICENSE +0 -21
  87. package/dist/browser/asciidoctor.js +0 -47654
  88. package/dist/browser/asciidoctor.min.js +0 -1452
  89. package/dist/graalvm/asciidoctor.js +0 -47402
  90. package/dist/node/asciidoctor.cjs +0 -21567
  91. package/dist/node/asciidoctor.js +0 -23037
package/src/table.js ADDED
@@ -0,0 +1,952 @@
1
+ // ESM conversion of table.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - Table::Rows#[] (alias for send) → explicit bySection/head/foot/body access.
5
+ // - Table::Cell references Document, PreprocessorReader, Parser — these are
6
+ // imported lazily (dynamic import) to avoid circular dependency issues.
7
+ // - String#squeeze(ch) → replaceAll(ch+ch, ch) loop (only used for '"').
8
+ // - Number#truncate(precision) → Math.trunc(n * 10^p) / 10^p.
9
+ // - :asciidoc / :literal / :header symbols → strings 'asciidoc', 'literal', 'header'.
10
+
11
+ import { AbstractBlock } from './abstract_block.js'
12
+ import { AbstractNode } from './abstract_node.js'
13
+ import { Inline } from './inline.js'
14
+ import { applyLogging } from './logging.js'
15
+ import { LF, ATTR_REF_HEAD, BASIC_SUBS, NORMAL_SUBS } from './constants.js'
16
+ import { BlankLineRx, LeadingInlineAnchorRx } from './rx.js'
17
+
18
+ /**
19
+ * Truncate a float to `precision` decimal places (like Ruby's Float#truncate).
20
+ * @param {number} value
21
+ * @param {number} precision
22
+ * @returns {number}
23
+ * @internal
24
+ */
25
+ function truncate(value, precision) {
26
+ const factor = 10 ** precision
27
+ return Math.trunc(value * factor) / factor
28
+ }
29
+
30
+ /**
31
+ * Collapse consecutive identical characters (like Ruby's String#squeeze(q)).
32
+ * @param {string} str
33
+ * @param {string} ch
34
+ * @returns {string}
35
+ * @internal
36
+ */
37
+ function squeezeChar(str, ch) {
38
+ const double = ch + ch
39
+ while (str.includes(double)) str = str.replaceAll(double, ch)
40
+ return str
41
+ }
42
+
43
+ const DEFAULT_PRECISION = 4
44
+
45
+ // ── Table ─────────────────────────────────────────────────────────────────────
46
+
47
+ export class Table extends AbstractBlock {
48
+ constructor(parent, attributes) {
49
+ super(parent, 'table')
50
+ this.rows = new Table.Rows()
51
+ this.columns = []
52
+ this.hasHeaderOption = false
53
+
54
+ // Resolve tablepcwidth from 'width' attribute
55
+ let pcwidthIntval = 100
56
+ const pcwidth = attributes.width
57
+ if (pcwidth != null) {
58
+ let v = parseInt(pcwidth, 10)
59
+ if (Number.isNaN(v)) v = 0
60
+ if (v > 100 || v < 1) {
61
+ if (!(v === 0 && (pcwidth === '0' || pcwidth === '0%'))) v = 100
62
+ }
63
+ pcwidthIntval = v
64
+ }
65
+ this.attributes.tablepcwidth = pcwidthIntval
66
+
67
+ const pagewidthAttr = this.document.attributes.pagewidth
68
+ if (pagewidthAttr != null) {
69
+ const abswidthVal = truncate(
70
+ (pcwidthIntval / 100.0) * parseFloat(pagewidthAttr),
71
+ DEFAULT_PRECISION
72
+ )
73
+ this.attributes.tableabswidth =
74
+ abswidthVal === Math.trunc(abswidthVal)
75
+ ? Math.trunc(abswidthVal)
76
+ : abswidthVal
77
+ }
78
+
79
+ if ('rotate-option' in attributes) this.attributes.orientation = 'landscape'
80
+ }
81
+
82
+ /**
83
+ * Returns the header option state if the row being processed is the header row, otherwise false.
84
+ * @returns {boolean|string}
85
+ * @internal
86
+ */
87
+ headerRow() {
88
+ const val = this.hasHeaderOption
89
+ return val && this.rows.body.length === 0 ? val : false
90
+ }
91
+
92
+ /**
93
+ * Create Column objects from the column test array.
94
+ * @param {Object[]} colspecs
95
+ * @internal
96
+ */
97
+ createColumns(colspecs) {
98
+ const cols = []
99
+ let autowidthCols = null
100
+ let widthBase = 0
101
+ for (const colspec of colspecs) {
102
+ const colwidth = colspec.width
103
+ cols.push(new Table.Column(this, cols.length, colspec))
104
+ if (colwidth < 0) {
105
+ ;(autowidthCols ??= []).push(cols[cols.length - 1])
106
+ } else {
107
+ widthBase += colwidth
108
+ }
109
+ }
110
+ this.columns = cols
111
+ const numCols = cols.length
112
+ if (numCols > 0) {
113
+ this.attributes.colcount = numCols
114
+ const effectiveWidthBase =
115
+ widthBase > 0 || autowidthCols ? widthBase : null
116
+ this.assignColumnWidths(effectiveWidthBase, autowidthCols)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Assign percentage (and absolute) widths to all columns.
122
+ * @param {number|null} [widthBase=null]
123
+ * @param {Table.Column[]|null} [autowidthCols=null]
124
+ * @internal
125
+ */
126
+ assignColumnWidths(widthBase = null, autowidthCols = null) {
127
+ const precision = DEFAULT_PRECISION
128
+ let totalWidth = 0
129
+ let colPcwidth = 0
130
+
131
+ if (widthBase != null) {
132
+ if (autowidthCols) {
133
+ let autowidth
134
+ if (widthBase > 100) {
135
+ autowidth = 0
136
+ this.logger.warn(
137
+ `total column width must not exceed 100% when using autowidth columns; got ${widthBase}%`
138
+ )
139
+ } else {
140
+ autowidth = truncate(
141
+ (100.0 - widthBase) / autowidthCols.length,
142
+ precision
143
+ )
144
+ if (Math.trunc(autowidth) === autowidth)
145
+ autowidth = Math.trunc(autowidth)
146
+ widthBase = 100
147
+ }
148
+ const autowAttrs = { width: autowidth, 'autowidth-option': '' }
149
+ for (const col of autowidthCols) col.updateAttributes(autowAttrs)
150
+ }
151
+ for (const col of this.columns) {
152
+ totalWidth += colPcwidth = col.assignWidth(null, widthBase, precision)
153
+ }
154
+ } else {
155
+ colPcwidth = truncate(100.0 / this.columns.length, precision)
156
+ if (Math.trunc(colPcwidth) === colPcwidth)
157
+ colPcwidth = Math.trunc(colPcwidth)
158
+ for (const col of this.columns) {
159
+ totalWidth += col.assignWidth(colPcwidth, null, precision)
160
+ }
161
+ }
162
+
163
+ // Donate balance to the last column (half-up rounding)
164
+ if (totalWidth !== 100) {
165
+ const balance = +(100 - totalWidth + colPcwidth).toFixed(precision)
166
+ this.columns[this.columns.length - 1].assignWidth(
167
+ balance,
168
+ null,
169
+ precision
170
+ )
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Partition rows into header, footer, and body.
176
+ * @param {Object} attrs
177
+ * @internal
178
+ */
179
+ async partitionHeaderFooter(attrs) {
180
+ const body = this.rows.body
181
+ let numBodyRows = (this.attributes.rowcount = body.length)
182
+
183
+ if (numBodyRows > 0) {
184
+ if (this.hasHeaderOption === true) {
185
+ this.rows.head = [
186
+ await Promise.all(
187
+ body.shift().map((cell) => cell.reinitialize(true))
188
+ ),
189
+ ]
190
+ numBodyRows--
191
+ } else if (this.hasHeaderOption === null) {
192
+ this.hasHeaderOption = false
193
+ body.unshift(
194
+ await Promise.all(
195
+ body.shift().map((cell) => cell.reinitialize(false))
196
+ )
197
+ )
198
+ }
199
+ }
200
+
201
+ if (numBodyRows > 0 && 'footer-option' in attrs) {
202
+ this.rows.foot = [body.pop()]
203
+ }
204
+ }
205
+ }
206
+
207
+ // ── Table.Rows ────────────────────────────────────────────────────────────────
208
+
209
+ Table.Rows = class Rows {
210
+ constructor(head = [], foot = [], body = []) {
211
+ this.head = head
212
+ this.foot = foot
213
+ this.body = body
214
+ }
215
+
216
+ /**
217
+ * Retrieve the rows grouped by section as a nested Array.
218
+ * @returns {Array<[string, Array]>}
219
+ */
220
+ bySection() {
221
+ return [
222
+ ['head', this.head],
223
+ ['body', this.body],
224
+ ['foot', this.foot],
225
+ ]
226
+ }
227
+
228
+ toObject() {
229
+ return { head: this.head, body: this.body, foot: this.foot }
230
+ }
231
+ }
232
+
233
+ // ── Table.Column ──────────────────────────────────────────────────────────────
234
+
235
+ Table.Column = class Column extends AbstractNode {
236
+ constructor(table, index, attributes = {}) {
237
+ super(table, 'table_column')
238
+ this.style = attributes.style ?? null
239
+ attributes.colnumber = index + 1
240
+ if (!('width' in attributes)) attributes.width = 1
241
+ if (!('halign' in attributes)) attributes.halign = 'left'
242
+ if (!('valign' in attributes)) attributes.valign = 'top'
243
+ this.updateAttributes(attributes)
244
+ }
245
+
246
+ /** Alias for parent (always a Table). */
247
+ get table() {
248
+ return this.getParent()
249
+ }
250
+
251
+ /**
252
+ * Calculate and assign the widths for this column.
253
+ * @param {number|null} colPcwidth
254
+ * @param {number|null} widthBase
255
+ * @param {number} precision
256
+ * @returns {number} The resolved colpcwidth value.
257
+ * @internal
258
+ */
259
+ assignWidth(colPcwidth, widthBase, precision) {
260
+ if (widthBase != null) {
261
+ colPcwidth = truncate(
262
+ (parseFloat(this.attributes.width) * 100.0) / widthBase,
263
+ precision
264
+ )
265
+ if (Math.trunc(colPcwidth) === colPcwidth)
266
+ colPcwidth = Math.trunc(colPcwidth)
267
+ }
268
+ const tableAbswidth = this.getParent().attributes.tableabswidth
269
+ if (tableAbswidth != null) {
270
+ const colAbswidth = truncate(
271
+ (colPcwidth / 100.0) * tableAbswidth,
272
+ precision
273
+ )
274
+ this.attributes.colabswidth =
275
+ colAbswidth === Math.trunc(colAbswidth)
276
+ ? Math.trunc(colAbswidth)
277
+ : colAbswidth
278
+ }
279
+ this.attributes.colpcwidth = colPcwidth
280
+ return colPcwidth
281
+ }
282
+
283
+ isBlock() {
284
+ return false
285
+ }
286
+ isInline() {
287
+ return false
288
+ }
289
+
290
+ // ── JavaScript-style accessors ────────────────────────────────────────────────
291
+
292
+ /**
293
+ * Get the parent table of this column.
294
+ * @returns {Table}
295
+ */
296
+ getTable() {
297
+ return this.table
298
+ }
299
+ }
300
+
301
+ // ── Table.Cell ────────────────────────────────────────────────────────────────
302
+
303
+ /** @extends {AbstractBlock<string | string[]>} */
304
+ class Cell extends AbstractBlock {
305
+ static get DOUBLE_LF() {
306
+ return LF + LF
307
+ }
308
+
309
+ /** @internal */
310
+ _reinitializeArgs
311
+
312
+ /** @internal */
313
+ _innerDocSetup
314
+
315
+ /** @internal */
316
+ _subs
317
+
318
+ /** @internal */
319
+ _text
320
+
321
+ /** @internal */
322
+ _cellbgcolor
323
+
324
+ /** @internal */
325
+ _convertedText
326
+
327
+ constructor(column, cellText, attributes = {}, opts = {}) {
328
+ super(column, 'table_cell')
329
+ this._cursor = null
330
+ this._reinitializeArgs = null
331
+ if (this.document.sourcemap && opts.cursor) {
332
+ this.sourceLocation = Object.assign({}, opts.cursor)
333
+ }
334
+
335
+ let cellStyle = null
336
+ let inHeaderRow = false
337
+ let asciidoc = false
338
+ let literal = false
339
+ let normalPsv = false
340
+ let innerDocumentCursor = null
341
+
342
+ if (column) {
343
+ inHeaderRow = column.table.headerRow()
344
+ if (inHeaderRow) {
345
+ if (inHeaderRow === 'implicit') {
346
+ const cs = column.style ?? attributes?.style
347
+ if (cs === 'asciidoc' || cs === 'literal') {
348
+ this._reinitializeArgs = [
349
+ column,
350
+ cellText,
351
+ attributes && { ...attributes },
352
+ opts,
353
+ ]
354
+ }
355
+ cellStyle = null
356
+ }
357
+ // else: don't set cellStyle from column for header row
358
+ } else {
359
+ cellStyle = column.style ?? null
360
+ }
361
+ // Inherit column attributes
362
+ this.updateAttributes(column.attributes)
363
+ }
364
+
365
+ if (attributes != null) {
366
+ if (Object.keys(attributes).length === 0) {
367
+ this.colspan = null
368
+ this.rowspan = null
369
+ } else {
370
+ this.colspan = attributes.colspan
371
+ ? parseInt(attributes.colspan, 10)
372
+ : null
373
+ this.rowspan = attributes.rowspan
374
+ ? parseInt(attributes.rowspan, 10)
375
+ : null
376
+ delete attributes.colspan
377
+ delete attributes.rowspan
378
+ if (!inHeaderRow) cellStyle = attributes.style ?? cellStyle
379
+ this.updateAttributes(attributes)
380
+ }
381
+
382
+ switch (cellStyle) {
383
+ case 'asciidoc': {
384
+ asciidoc = true
385
+ innerDocumentCursor = opts.cursor
386
+ cellText = cellText.trimEnd()
387
+ if (cellText.startsWith(LF)) {
388
+ let linesAdvanced = 0
389
+ while (cellText.startsWith(LF)) {
390
+ cellText = cellText.slice(1)
391
+ linesAdvanced++
392
+ }
393
+ if (
394
+ innerDocumentCursor &&
395
+ typeof innerDocumentCursor.advance === 'function'
396
+ ) {
397
+ innerDocumentCursor.advance(linesAdvanced)
398
+ }
399
+ } else {
400
+ cellText = cellText.trimStart()
401
+ }
402
+ break
403
+ }
404
+ case 'literal':
405
+ literal = true
406
+ cellText = cellText.trimEnd()
407
+ while (cellText.startsWith(LF)) cellText = cellText.slice(1)
408
+ break
409
+ default:
410
+ normalPsv = true
411
+ cellText = cellText != null ? cellText.trim() : ''
412
+ }
413
+ } else {
414
+ this.colspan = null
415
+ this.rowspan = null
416
+ if (cellStyle === 'asciidoc') asciidoc = true
417
+ }
418
+
419
+ if (asciidoc) {
420
+ const parentDoc = this.document
421
+ // Store the setup data for create() to handle asynchronously.
422
+ this._innerDocSetup = {
423
+ lines: cellText.split(LF, -1),
424
+ parentDoc,
425
+ parentDoctitle: parentDoc.attributes.doctitle,
426
+ options: {
427
+ safe: parentDoc.safe,
428
+ backend: parentDoc.backend,
429
+ header_footer: false,
430
+ parent: parentDoc,
431
+ cursor: innerDocumentCursor,
432
+ },
433
+ }
434
+ delete parentDoc.attributes.doctitle
435
+ this._subs = null
436
+ } else if (literal) {
437
+ this.contentModel = 'verbatim'
438
+ this._subs = [...BASIC_SUBS]
439
+ } else {
440
+ if (normalPsv) {
441
+ if (inHeaderRow) {
442
+ this._cursor = opts.cursor ?? null
443
+ } else {
444
+ this._catalogInlineAnchor(cellText, opts.cursor)
445
+ }
446
+ }
447
+ this.contentModel = 'simple'
448
+ this._subs = [...NORMAL_SUBS]
449
+ }
450
+ this._text = cellText
451
+ this.style = cellStyle
452
+ }
453
+
454
+ /** Alias for parent (always a Column). */
455
+ get column() {
456
+ return this.getParent()
457
+ }
458
+
459
+ /**
460
+ * Factory — create and fully initialize a Cell asynchronously.
461
+ * For AsciiDoc cells, parses the nested document.
462
+ *
463
+ * NOTE: _innerContent is NOT pre-computed here. Document.convert() will call
464
+ * _convertAsciiDocCells() after parse completes (so callouts are rewound and
465
+ * all cross-references from the parent document are already registered).
466
+ * @param {Table.Column} column
467
+ * @param {string} cellText
468
+ * @param {Object} [attributes={}]
469
+ * @param {Object} [opts={}]
470
+ * @returns {Promise<Table.Cell>}
471
+ */
472
+ static async create(column, cellText, attributes = {}, opts = {}) {
473
+ const cell = new Table.Cell(column, cellText, attributes, opts)
474
+ if (cell._innerDocSetup) {
475
+ const { lines, parentDoc, parentDoctitle, options } = cell._innerDocSetup
476
+ cell._innerDocSetup = null
477
+ const innerDoc = await parentDoc.constructor.create(lines, options)
478
+ if (parentDoctitle) parentDoc.attributes.doctitle = parentDoctitle
479
+ cell._innerDocument = innerDoc
480
+ }
481
+ return /** @type {Table.Cell} */ cell
482
+ }
483
+
484
+ /** @returns {Promise<Table.Cell>} */
485
+ async reinitialize(hasHeader) {
486
+ if (hasHeader) {
487
+ this._reinitializeArgs = null
488
+ } else if (this._reinitializeArgs) {
489
+ return Table.Cell.create(...this._reinitializeArgs)
490
+ } else {
491
+ this.style = this.attributes.style ?? null
492
+ }
493
+ if (this._cursor) this._catalogInlineAnchor()
494
+ return /** @type {Table.Cell} */ (this)
495
+ }
496
+
497
+ _catalogInlineAnchor(cellText = this._text, cursor = null) {
498
+ if (!cursor) {
499
+ cursor = this._cursor
500
+ this._cursor = null
501
+ }
502
+ if (!cellText.startsWith('[[')) return
503
+ const m = cellText.match(LeadingInlineAnchorRx)
504
+ if (!m) return
505
+ const doc = this.document
506
+ let reftext = m[2] ?? null
507
+ if (reftext?.includes(ATTR_REF_HEAD)) reftext = doc.subAttributes(reftext)
508
+ doc.register('refs', [
509
+ m[1],
510
+ new Inline(this, 'anchor', reftext, { type: 'ref', id: m[1] }),
511
+ ])
512
+ }
513
+
514
+ /**
515
+ * Get the text with substitutions applied.
516
+ * The result is pre-computed during Document.parse() via precomputeText().
517
+ * Falls back to the raw text if precomputeText() has not been called yet.
518
+ * @returns {string|null}
519
+ */
520
+ get text() {
521
+ return this._convertedText ?? this._text ?? null
522
+ }
523
+
524
+ /**
525
+ * Pre-compute the converted text asynchronously.
526
+ * Called during Document.parse() so the synchronous getter works during conversion.
527
+ * @returns {Promise<void>}
528
+ */
529
+ async precomputeText() {
530
+ if (this._subs && this._convertedText == null) {
531
+ this._convertedText = await this.applySubs(this._text, this._subs)
532
+ // Capture the cellbgcolor attribute value as set by {set:cellbgcolor:...} in cell text.
533
+ // Since {set:...} attribute assignments happen during applySubs, and the document attribute
534
+ // is shared state, we must capture it per-cell immediately after text processing.
535
+ this._cellbgcolor = this.document.attributes.cellbgcolor
536
+ }
537
+ }
538
+
539
+ set text(val) {
540
+ this._text = val
541
+ this._convertedText = null
542
+ }
543
+
544
+ /**
545
+ * Get the content — converted body data.
546
+ * For AsciiDoc cells, returns the pre-computed content (set by Document.convert()).
547
+ * @returns {Promise<string|string[]>}
548
+ */
549
+ async content() {
550
+ if (this.style === 'asciidoc') {
551
+ return this._innerContent ?? ''
552
+ }
553
+ if (this._text.includes(Table.Cell.DOUBLE_LF)) {
554
+ const parts = []
555
+ for (const rawPara of this.text.split(BlankLineRx)) {
556
+ const para = rawPara.trim()
557
+ if (!para) continue
558
+ const cs = this.style
559
+ parts.push(
560
+ cs && cs !== 'header'
561
+ ? await new Inline(this.getParent(), 'quoted', para, {
562
+ type: cs,
563
+ }).convert()
564
+ : para
565
+ )
566
+ }
567
+ return parts
568
+ }
569
+ const subbedText = this.text
570
+ if (!subbedText) return []
571
+ const cs = this.style
572
+ if (cs && cs !== 'header') {
573
+ return [
574
+ await new Inline(this.getParent(), 'quoted', subbedText, {
575
+ type: cs,
576
+ }).convert(),
577
+ ]
578
+ }
579
+ return [subbedText]
580
+ }
581
+
582
+ lines() {
583
+ return this._text.split(LF)
584
+ }
585
+ source() {
586
+ return this._text
587
+ }
588
+
589
+ get innerDocument() {
590
+ return this._innerDocument ?? null
591
+ }
592
+
593
+ get file() {
594
+ return this.sourceLocation?.file ?? null
595
+ }
596
+ get lineno() {
597
+ return this.sourceLocation?.lineno ?? null
598
+ }
599
+
600
+ toString() {
601
+ return `${super.toString()} - [text: ${this._text}, colspan: ${this.colspan ?? 1}, rowspan: ${this.rowspan ?? 1}, attributes: ${JSON.stringify(this.attributes)}]`
602
+ }
603
+
604
+ // ── JavaScript-style accessors ────────────────────────────────────────────────
605
+
606
+ /**
607
+ * Get the text with substitutions applied.
608
+ * @returns {string|null}
609
+ */
610
+ getText() {
611
+ return this.text
612
+ }
613
+
614
+ /**
615
+ * Set the raw text of this cell.
616
+ * @param {string|null} val
617
+ */
618
+ setText(val) {
619
+ this.text = val
620
+ }
621
+
622
+ /**
623
+ * Get the inner document for AsciiDoc-style cells.
624
+ * @returns {Document|null}
625
+ */
626
+ getInnerDocument() {
627
+ return this.innerDocument
628
+ }
629
+
630
+ /**
631
+ * Get the source file where this cell is defined.
632
+ * @returns {string|null}
633
+ */
634
+ getFile() {
635
+ return this.file
636
+ }
637
+
638
+ /**
639
+ * Get the source line number where this cell is defined.
640
+ * @returns {number|null}
641
+ */
642
+ getLineNumber() {
643
+ return this.lineno
644
+ }
645
+ }
646
+ Table.Cell = Cell
647
+
648
+ // ── Table.ParserContext ───────────────────────────────────────────────────────
649
+
650
+ Table.ParserContext = class ParserContext {
651
+ static get FORMATS() {
652
+ return new Set(['psv', 'csv', 'dsv', 'tsv'])
653
+ }
654
+
655
+ static get DELIMITERS() {
656
+ return {
657
+ psv: ['|', /\|/],
658
+ csv: [',', /,/],
659
+ dsv: [':', /:/],
660
+ tsv: ['\t', /\t/],
661
+ '!sv': ['!', /!/],
662
+ }
663
+ }
664
+
665
+ constructor(reader, table, attributes = {}) {
666
+ this._reader = reader
667
+ this._startCursor = reader.cursor
668
+ reader.mark()
669
+ this.table = table
670
+ this.buffer = ''
671
+
672
+ // Determine format
673
+ let xsv
674
+ if ('format' in attributes) {
675
+ xsv = attributes.format
676
+ if (ParserContext.FORMATS.has(xsv)) {
677
+ if (xsv === 'tsv') {
678
+ this.format = 'csv'
679
+ } else {
680
+ this.format = xsv
681
+ if (xsv === 'psv' && table.document.nested()) xsv = '!sv'
682
+ }
683
+ } else {
684
+ this.logger.error(
685
+ this.messageWithContext(`illegal table format: ${xsv}`, {
686
+ source_location: reader.cursorAtPrevLine(),
687
+ })
688
+ )
689
+ this.format = 'psv'
690
+ xsv = table.document.nested() ? '!sv' : 'psv'
691
+ }
692
+ } else {
693
+ this.format = 'psv'
694
+ xsv = table.document.nested() ? '!sv' : 'psv'
695
+ }
696
+
697
+ // Determine delimiter
698
+ const delimiters = ParserContext.DELIMITERS
699
+ if ('separator' in attributes) {
700
+ const sep = attributes.separator
701
+ if (!sep) {
702
+ ;[this.delimiter, this.delimiterRe] = delimiters[xsv]
703
+ } else if (sep === '\\t') {
704
+ ;[this.delimiter, this.delimiterRe] = delimiters.tsv
705
+ } else {
706
+ this.delimiter = sep
707
+ this.delimiterRe = new RegExp(
708
+ sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
709
+ )
710
+ }
711
+ } else {
712
+ ;[this.delimiter, this.delimiterRe] = delimiters[xsv]
713
+ }
714
+
715
+ this.colcount = table.columns.length === 0 ? -1 : table.columns.length
716
+ this._cellspecs = []
717
+ this._cellOpen = false
718
+ this._activeRowspans = [0]
719
+ this._columnVisits = 0
720
+ this._currentRow = []
721
+ this._linenum = -1
722
+ }
723
+
724
+ startsWith(line) {
725
+ return line.startsWith(this.delimiter)
726
+ }
727
+
728
+ matchDelimiter(line) {
729
+ return line.match(this.delimiterRe)
730
+ }
731
+
732
+ skipPastDelimiter(pre) {
733
+ this.buffer = `${this.buffer}${pre}${this.delimiter}`
734
+ }
735
+
736
+ skipPastEscapedDelimiter(pre) {
737
+ this.buffer = `${this.buffer}${pre.slice(0, -1)}${this.delimiter}`
738
+ }
739
+
740
+ bufferHasUnclosedQuotesInText(text, q = '"') {
741
+ let record = text.trim()
742
+ if (record === q) return true
743
+ if (!record.startsWith(q)) return false
744
+ const qq = q + q
745
+ const trailingQuote = record.endsWith(q)
746
+ if ((trailingQuote && record.endsWith(qq)) || record.startsWith(qq)) {
747
+ record = squeezeChar(record, q)
748
+ return record.startsWith(q) && !record.endsWith(q)
749
+ }
750
+ return !trailingQuote
751
+ }
752
+
753
+ bufferHasUnclosedQuotes(append = null, q = '"') {
754
+ const record = (append ? this.buffer + append : this.buffer).trim()
755
+ if (!record.startsWith(q)) return false
756
+ // Walk the quoted field character by character (RFC 4180)
757
+ let i = 1 // skip the opening quote
758
+ while (i < record.length) {
759
+ if (record[i] === q) {
760
+ if (i + 1 < record.length && record[i + 1] === q) {
761
+ i += 2 // escaped quote ""
762
+ } else {
763
+ return false // closing quote found → field is closed
764
+ }
765
+ } else {
766
+ i++
767
+ }
768
+ }
769
+ return true // closing quote never found
770
+ }
771
+
772
+ takeCellspec() {
773
+ return this._cellspecs.shift() ?? null
774
+ }
775
+
776
+ pushCellspec(cellspec = {}) {
777
+ this._cellspecs.push(cellspec ?? {})
778
+ }
779
+
780
+ keepCellOpen() {
781
+ this._cellOpen = true
782
+ }
783
+ markCellClosed() {
784
+ this._cellOpen = false
785
+ }
786
+ isCellOpen() {
787
+ return this._cellOpen
788
+ }
789
+ isCellClosed() {
790
+ return !this._cellOpen
791
+ }
792
+
793
+ async closeOpenCell(nextCellspec = {}) {
794
+ this.pushCellspec(nextCellspec)
795
+ if (this.isCellOpen()) await this.closeCell(true)
796
+ this._advance()
797
+ }
798
+
799
+ async closeCell(eol = false) {
800
+ let cellText, cellspec, repeat
801
+
802
+ if (this.format === 'psv') {
803
+ cellText = this.buffer
804
+ this.buffer = ''
805
+ cellspec = this.takeCellspec()
806
+ if (cellspec) {
807
+ repeat = cellspec.repeatcol ?? 1
808
+ delete cellspec.repeatcol
809
+ } else {
810
+ this.logger.error(
811
+ this.messageWithContext(
812
+ 'table missing leading separator; recovering automatically',
813
+ {
814
+ source_location: this._startCursor,
815
+ }
816
+ )
817
+ )
818
+ cellspec = {}
819
+ repeat = 1
820
+ }
821
+ } else {
822
+ cellText = this.buffer.trim()
823
+ this.buffer = ''
824
+ cellspec = null
825
+ repeat = 1
826
+ if (this.format === 'csv' && cellText && cellText.includes('"')) {
827
+ const q = '"'
828
+ if (cellText.startsWith(q)) {
829
+ if (
830
+ cellText.length > 1 &&
831
+ cellText.endsWith(q) &&
832
+ !this.bufferHasUnclosedQuotesInText(cellText, q)
833
+ ) {
834
+ const inner = cellText.slice(1, cellText.length - 1)
835
+ cellText = squeezeChar(inner.trim(), q)
836
+ } else {
837
+ this.logger.error(
838
+ this.messageWithContext(
839
+ 'unclosed quote in CSV data; setting cell to empty',
840
+ {
841
+ source_location: this._reader.cursorAtPrevLine(),
842
+ }
843
+ )
844
+ )
845
+ cellText = ''
846
+ }
847
+ } else {
848
+ cellText = squeezeChar(cellText, '"')
849
+ }
850
+ }
851
+ }
852
+
853
+ for (let i = 1; i <= repeat; i++) {
854
+ let column
855
+ if (this.colcount === -1) {
856
+ this.table.columns.push(
857
+ (column = new Table.Column(
858
+ this.table,
859
+ this.table.columns.length + i - 1
860
+ ))
861
+ )
862
+ if (cellspec && 'colspan' in cellspec) {
863
+ const extraCols = parseInt(cellspec.colspan, 10) - 1
864
+ if (extraCols > 0) {
865
+ const offset = this.table.columns.length
866
+ for (let j = 0; j < extraCols; j++) {
867
+ this.table.columns.push(new Table.Column(this.table, offset + j))
868
+ }
869
+ }
870
+ }
871
+ } else {
872
+ column = this.table.columns[this._currentRow.length] ?? null
873
+ }
874
+
875
+ const cursorBeforeMark = this._reader.cursorBeforeMark()
876
+ this._reader.mark()
877
+ const cell = await Table.Cell.create(column, cellText, cellspec, {
878
+ cursor: cursorBeforeMark,
879
+ })
880
+
881
+ if (cell.rowspan && cell.rowspan !== 1) {
882
+ this._activateRowspan(cell.rowspan, cell.colspan ?? 1)
883
+ }
884
+ this._columnVisits += cell.colspan ?? 1
885
+ this._currentRow.push(cell)
886
+
887
+ const rowStatus = this._endOfRow()
888
+ if (
889
+ rowStatus > -1 &&
890
+ (this.colcount !== -1 || this._linenum > 0 || (eol && i === repeat))
891
+ ) {
892
+ if (rowStatus > 0) {
893
+ this.logger.error(
894
+ this.messageWithContext(
895
+ 'dropping cell because it exceeds specified number of columns',
896
+ { source_location: cursorBeforeMark }
897
+ )
898
+ )
899
+ this._closeRow(true)
900
+ } else {
901
+ this._closeRow()
902
+ }
903
+ }
904
+ }
905
+ this._cellOpen = false
906
+ }
907
+
908
+ closeTable() {
909
+ if (this._columnVisits === 0) return
910
+ this.logger.error(
911
+ this.messageWithContext(
912
+ 'dropping cells from incomplete row detected end of table',
913
+ {
914
+ source_location: this._reader.cursorBeforeMark(),
915
+ }
916
+ )
917
+ )
918
+ }
919
+
920
+ /**
921
+ * @param {boolean} [drop=false]
922
+ * @internal
923
+ */
924
+ _closeRow(drop = false) {
925
+ if (!drop) this.table.rows.body.push(this._currentRow)
926
+ if (this.colcount === -1) this.colcount = this._columnVisits
927
+ this._columnVisits = 0
928
+ this._currentRow = []
929
+ this._activeRowspans.shift()
930
+ this._activeRowspans[0] ??= 0
931
+ }
932
+
933
+ _activateRowspan(rowspan, colspan) {
934
+ for (let i = 1; i < rowspan; i++) {
935
+ this._activeRowspans[i] = (this._activeRowspans[i] ?? 0) + colspan
936
+ }
937
+ }
938
+
939
+ _endOfRow() {
940
+ if (this.colcount === -1) return 0
941
+ const eff = this._columnVisits + (this._activeRowspans[0] ?? 0)
942
+ if (eff < this.colcount) return -1
943
+ if (eff === this.colcount) return 0
944
+ return 1
945
+ }
946
+
947
+ _advance() {
948
+ this._linenum++
949
+ }
950
+ }
951
+
952
+ applyLogging(Table.ParserContext.prototype)