@markup-carve/pandoc-carve 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ npm install @markup-carve/pandoc-carve
20
20
  ```
21
21
 
22
22
  The CLI requires Pandoc 3.x on `PATH` for formats other than JSON. CI currently
23
- tests Pandoc 3.11. Pandoc is not bundled.
23
+ tests Pandoc 3.11 and 3.10.2. Pandoc is not bundled.
24
24
 
25
25
  ## CLI
26
26
 
@@ -105,6 +105,13 @@ them. Highlights include:
105
105
  - raw target content only survives in matching writers;
106
106
  - comments and attribute placement require round-trip metadata.
107
107
 
108
+ These limits are measured rather than asserted.
109
+ [pandoc-format-fidelity](https://github.com/markup-carve/pandoc-format-fidelity) runs Carve
110
+ source through this bridge and back for every writer it lists, and publishes the verdicts per
111
+ format: `results/carve-rt.json` holds the Carve round-trip lane, and
112
+ [the report](https://github.com/markup-carve/pandoc-format-fidelity/blob/main/docs/index.html)
113
+ covers every pandoc writer and reader.
114
+
108
115
  Dangerous URL schemes such as `javascript:` are blanked and reported. The
109
116
  [URL policy](docs/reference.md#dangerous-url-schemes-are-blanked) documents the
110
117
  exact behavior. The [mapping and limitations reference](docs/reference.md#what-maps-to-what)
@@ -10,7 +10,7 @@ const RULES = [
10
10
  { test: /^block: pandoc node/, code: 'unsupported-pandoc-block', class: 'unsupported' },
11
11
  { test: /^citation: .*typed locator/, code: 'citation-locator-flattened', class: 'normalized' },
12
12
  { test: /^citation: .*suppresses its author/, code: 'citation-mode-normalized', class: 'normalized' },
13
- { test: /^Cite mixes /, code: 'citation-mode-normalized', class: 'normalized' },
13
+ { test: /^Cite mixes /, code: 'citation-mode-normalized', class: 'degraded' },
14
14
  { test: /^Cite mapped /, code: 'citation-bibliography-not-emitted', class: 'degraded' },
15
15
  { test: /^SmallCaps /, code: 'smallcaps-degraded', class: 'degraded' },
16
16
  { test: /^short caption:/, code: 'short-caption-source-unavailable', class: 'unsupported' },
@@ -40,16 +40,19 @@ const RULES = [
40
40
  { test: /^list-table: structure/, code: 'list-table-structure-degraded', class: 'degraded' },
41
41
  { test: /^list-table: the short caption/, code: 'list-table-short-caption-dropped', class: 'lossy' },
42
42
  { test: /^list-table: a body group's attributes/, code: 'list-table-body-attributes-dropped', class: 'lossy' },
43
- { test: /^list-table: the table's .* body groups/, code: 'list-table-body-groups-merged', class: 'normalized' },
44
- { test: /^list-table: the body groups disagree/, code: 'list-table-row-heads-normalized', class: 'normalized' },
43
+ { test: /^list-table: a row's attributes/, code: 'list-table-row-attributes-dropped', class: 'lossy' },
44
+ { test: /^list-table: the table's .* body groups/, code: 'list-table-body-groups-merged', class: 'degraded' },
45
+ { test: /^list-table: the body groups disagree/, code: 'list-table-row-heads-normalized', class: 'degraded' },
45
46
  { test: /^list-table: rowspan/, code: 'list-table-rowspan-clipped', class: 'lossy' },
46
47
  { test: /^table: a row header outside/, code: 'table-row-head-outside-leading-run', class: 'lossy' },
47
48
  { test: /^table: attributes on/, code: 'table-continuation-attributes-dropped', class: 'lossy' },
48
49
  { test: /^table: colspan continuation/, code: 'table-colspan-origin-missing', class: 'degraded' },
49
50
  { test: /^table: rowspan crossing/, code: 'table-rowspan-clipped', class: 'lossy' },
50
51
  { test: /^table: rowspan continuation/, code: 'table-rowspan-origin-missing', class: 'degraded' },
51
- { test: /^table: .*padded with empty cells/, code: 'table-groups-normalized', class: 'normalized' },
52
+ { test: /^table: .*padded with empty cells/, code: 'table-groups-normalized', class: 'degraded' },
52
53
  { test: /^table: a cell holds block content/, code: 'table-groups-normalized', class: 'normalized' },
54
+ { test: /^table: the merged id/, code: 'table-wrapper-id-dropped', class: 'lossy' },
55
+ { test: /^table: a blank row has no pipe-table source spelling/, code: 'table-blank-row-spelled', class: 'normalized' },
53
56
  { test: /^table: .*converted with the implicit head\/body split/, code: 'table-row-groups-invalid', class: 'degraded' },
54
57
  { test: /^table: .*preserved in the Carve AST as `rowGroups`/, code: 'table-groups-flattened', class: 'degraded' },
55
58
  { test: /^table: a foot row's row header is dropped/, code: 'table-foot-row-header-dropped', class: 'lossy' },
package/dist/reverse.js CHANGED
@@ -983,9 +983,8 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
983
983
  }));
984
984
  // Row-head columns no longer send a table to the list-table form: the pipe
985
985
  // table marks those cells directly (see `rowHeadCols` above), which is
986
- // readable, needs no extension, and round-trips. Block cells remain the one
987
- // reason to leave, because a Carve `table_cell` holds INLINES and there is
988
- // no pipe form for a cell holding blocks at all.
986
+ // readable, needs no extension, and round-trips. Block cells need the
987
+ // list-table form, as do blank rows without attributes or alignment.
989
988
  //
990
989
  // A FOOT USED TO BE A SECOND REASON, AND IT WAS A REAL ONE AT THE TIME.
991
990
  // The clause read `|| (ctx.target === 'source' && footRaw.length > 0)`.
@@ -1005,7 +1004,25 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
1005
1004
  // (corpus 376, spec f074372), so at the time the pipe form genuinely could
1006
1005
  // not carry a foot and the list-table detour was the only way to keep it.
1007
1006
  // The limitation is gone, so the detour is too.
1008
- const useListTable = ctx.target === 'source' && hasBlockCells;
1007
+ const occupied = allRaw.map(() => []);
1008
+ const hasBlankRows = allRaw.some((row, r) => {
1009
+ let col = 0;
1010
+ let blank = nCols > 0;
1011
+ for (const raw of row[1]) {
1012
+ while (occupied[r][col])
1013
+ col++;
1014
+ const [attr, align, height, width, content] = raw;
1015
+ if (width > 1 || height > 1 || content.length || fromAttr(attr) || align.t !== 'AlignDefault')
1016
+ blank = false;
1017
+ for (let down = 1; down < height && r + down < allRaw.length; down++) {
1018
+ for (let across = 0; across < width; across++)
1019
+ occupied[r + down][col + across] = true;
1020
+ }
1021
+ col += width;
1022
+ }
1023
+ return blank && !occupied[r].some(Boolean);
1024
+ });
1025
+ const useListTable = ctx.target === 'source' && (hasBlockCells || hasBlankRows);
1009
1026
  const pending = allRaw.map(() => Array(nCols));
1010
1027
  const rows = [];
1011
1028
  // The pandoc blocks that sat at each grid position, kept alongside the
@@ -1093,6 +1110,7 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
1093
1110
  attrs: fromAttr(a),
1094
1111
  colAligns,
1095
1112
  colWidths: columns.map((column) => column.width),
1113
+ blankRows: !hasBlockCells && hasBlankRows,
1096
1114
  });
1097
1115
  }
1098
1116
  const node = { type: 'table', rows };
@@ -1299,14 +1317,15 @@ function isInlineShaped(cellBlocks) {
1299
1317
  */
1300
1318
  function listTable(ctx, input) {
1301
1319
  const { rows, cellBlocksAt, headRows, footRows, bodies, caption, attrs, colAligns, colWidths } = input;
1302
- // One reason is left to be here, so the diagnostic states it rather than
1303
- // selecting between three strings two of which nothing could reach: row-head
1304
- // columns are marked on the cells and a foot is stated on the attribute
1305
- // line, both in the pipe form.
1306
- warn(ctx, 'table: a cell holds block content, which a pipe table cannot spell - emitted as a `::: list-table` (structure preserved)');
1320
+ warn(ctx, input.blankRows
1321
+ ? 'table: a blank row has no pipe-table source spelling - emitted as a `::: list-table` (structure preserved)'
1322
+ : 'table: a cell holds block content, which a pipe table cannot spell - emitted as a `::: list-table` (structure preserved)');
1307
1323
  if (bodies.some((b) => b.attrs)) {
1308
1324
  warn(ctx, 'list-table: a body group\'s attributes are dropped - the extension has no body to hang them on');
1309
1325
  }
1326
+ if (rows.some((row) => row.attrs)) {
1327
+ warn(ctx, 'list-table: a row\'s attributes are dropped - the extension has no row-attribute spelling');
1328
+ }
1310
1329
  if (bodies.slice(1).some((b) => b.headRows === 0)) {
1311
1330
  warn(ctx, `list-table: the table's ${bodies.length} body groups merge where a later body has no header row to mark its boundary`);
1312
1331
  }
@@ -1471,27 +1490,6 @@ function figure(ctx, c) {
1471
1490
  return [node];
1472
1491
  }
1473
1492
  }
1474
- if (single?.t === 'Table') {
1475
- // The wrapper and the Table collapse into ONE Carve node, so their
1476
- // attrs merge rather than the inner one silently winning: pandoc's
1477
- // readers put the label on the Figure, not on the Table it wraps, and
1478
- // dropping it took the id a `</#id>` resolves against with it. The
1479
- // outer id wins, classes union, key/values merge with the outer taking
1480
- // precedence.
1481
- //
1482
- // A table is the only host that collapses this way, because it is the
1483
- // only one Carve gives a caption of its own. Every other host keeps
1484
- // both nodes - the wrapper stays a `figure` and the host's own attrs
1485
- // ride on the target - which is what the Div arm below sorts out.
1486
- //
1487
- // It matters most for a §4c table PANEL, whose id is what resolves as
1488
- // the group's number plus a letter.
1489
- const node = table(ctx, single.c, caption, shortCaption);
1490
- const outer = fromAttr(a);
1491
- if (outer)
1492
- node.attrs = mergeCAttrs(node.attrs, outer);
1493
- return [node];
1494
- }
1495
1493
  // A HOST THAT CARRIES ITS OWN ATTRIBUTES ARRIVES INSIDE A DIV. Pandoc's
1496
1494
  // BlockQuote, Para and CodeBlock have no Attr slot, so `block()` in the
1497
1495
  // forward direction wraps an attributed one in a Div. A `div` is not a
@@ -1515,6 +1513,21 @@ function figure(ctx, c) {
1515
1513
  ]);
1516
1514
  }
1517
1515
  }
1516
+ if (host?.t === 'Table') {
1517
+ // A table caption lives on the table, including when a reader puts
1518
+ // an alignment Div between it and the Figure. Outer attributes win.
1519
+ const node = table(ctx, host.c, caption, shortCaption);
1520
+ const outer = fromAttr(a);
1521
+ for (const attrs of [hostAttrs, outer]) {
1522
+ const current = node.attrs;
1523
+ if (current?.id && attrs?.id && current.id !== attrs.id) {
1524
+ warn(ctx, `table: the merged id "${current.id}" is dropped in favor of "${attrs.id}" - one Carve node has one id`);
1525
+ }
1526
+ if (attrs)
1527
+ node.attrs = mergeCAttrs(current, attrs);
1528
+ }
1529
+ return [node];
1530
+ }
1518
1531
  if (host && FIGURE_HOSTS.has(host.t)) {
1519
1532
  // A single-host `Figure` is an ordinary `figure` around that host -
1520
1533
  // the generic captioned wrapper of PART 9 §4b, which is what the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markup-carve/pandoc-carve",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Carve to Pandoc bridge: convert Carve markup to Pandoc's JSON AST and reach every pandoc output format (LaTeX, Typst, DOCX, PDF, ...)",
5
5
  "keywords": [
6
6
  "carve",