@markup-carve/pandoc-carve 0.1.0 → 0.1.2
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 +13 -11
- package/dist/convert.js +144 -7
- package/dist/reverse.js +87 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ node -e "import('@markup-carve/pandoc-carve').then(m => process.stdout.write(m.c
|
|
|
144
144
|
| `::: \|` line blocks (verse) | LineBlock, one entry per line, an empty entry per stanza break |
|
|
145
145
|
| Ordered markers `1.` / `1)` / `a.` / `iv.` | OrderedList with the matching style and delimiter. Pandoc's example list `(@)` and its `(1)` marker have no Carve form and are reported |
|
|
146
146
|
| A no-break space the parser resolved (`\ `) | U+00A0 (the engines publish a private-use sentinel for it) |
|
|
147
|
-
| A reference link or
|
|
147
|
+
| A reference link, image or footnote nothing defines (`[r][]`, `[^f]`) | the literal source as text, which is what Carve renders, plus a warning naming the label |
|
|
148
148
|
|
|
149
149
|
The complete node-by-node contract lives in the test goldens. Worked
|
|
150
150
|
input/output pairs in both directions - including how interactive constructs
|
|
@@ -204,13 +204,15 @@ a processor with neither extension enabled would render.
|
|
|
204
204
|
per-column alignment, a foot, and a body group's intermediate header rows.
|
|
205
205
|
It converts back to the pandoc table by default; `listTable: false` returns
|
|
206
206
|
the degraded div instead.
|
|
207
|
-
- A pandoc table with ROW-HEAD COLUMNS
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
- A pandoc table with ROW-HEAD COLUMNS stays a pipe table. `RowHeadColumns` says
|
|
208
|
+
the leading N cells of every body row are row headers, and a pipe table says
|
|
209
|
+
that per cell: `|= Mercury | 4,879.4 |` is a `<th scope="row">`. Marking the
|
|
210
|
+
cells needs no extension and round-trips, because the export direction derives
|
|
211
|
+
the count back from the leading run.
|
|
212
|
+
- On the way out, one TableBody per RUN of body rows that agree on that leading
|
|
213
|
+
run. Pandoc's `RowHeadColumns` is a count on a body and a `Table` holds a list
|
|
214
|
+
of bodies, so rows that disagree are simply different bodies. A table whose
|
|
215
|
+
body rows all agree emits exactly one body, as before.
|
|
214
216
|
- Pandoc `SmallCaps` has no Carve node and is not getting one: it imports as a
|
|
215
217
|
`[text]{.smallcaps}` span, with a warning saying so. The span is not a dead
|
|
216
218
|
end, though - the export direction reads that class back as `SmallCaps`, the
|
|
@@ -266,9 +268,9 @@ a processor with neither extension enabled would render.
|
|
|
266
268
|
rows or row-head columns) survives into the exchange AST, so
|
|
267
269
|
`pandocToCarveAst` hands it on whole - but PART 9 section 16's pipe table
|
|
268
270
|
spells only a leading run of header rows, so `pandocToCarve` flattens the
|
|
269
|
-
rest into body rows and says so. Row-head columns are the exception -
|
|
270
|
-
|
|
271
|
-
a
|
|
271
|
+
rest into body rows and says so. Row-head columns are the exception - the pipe
|
|
272
|
+
table marks them on the cells (above). Use the AST entry point when a foot or
|
|
273
|
+
a second body group has to survive.
|
|
272
274
|
- A marker on a HEAD cell (`|=> Name |`) is the column's alignment and becomes
|
|
273
275
|
pandoc's `ColSpec`; a marker on a BODY cell (`|> 12 |`) aligns that cell alone
|
|
274
276
|
and becomes the cell's own `Alignment`. The two are not interchangeable: a
|
package/dist/convert.js
CHANGED
|
@@ -389,6 +389,24 @@ function kids(ctx, n) {
|
|
|
389
389
|
* Returns null when the node is not an unresolved reference, so the caller
|
|
390
390
|
* converts it normally.
|
|
391
391
|
*/
|
|
392
|
+
/**
|
|
393
|
+
* The heading a COLLAPSED reference reaches, or null.
|
|
394
|
+
*
|
|
395
|
+
* `[Some Heading][]` with no definition anywhere links to the heading whose
|
|
396
|
+
* RENDERED text matches, case-insensitively - so `[plain one][]` reaches
|
|
397
|
+
* `# Plain One`. The engine resolves this after the parse, which is why the
|
|
398
|
+
* node still carries an empty `href` here.
|
|
399
|
+
*
|
|
400
|
+
* Only the collapsed form. `[text][Some Heading]` does not reach a heading,
|
|
401
|
+
* measured on the engine: it renders as its literal source. Detected on
|
|
402
|
+
* `rawRef` ending in `][]`, which is what makes the form collapsed.
|
|
403
|
+
*/
|
|
404
|
+
function collapsedHeadingRef(ctx, n) {
|
|
405
|
+
if (!String(n.rawRef ?? '').endsWith('][]'))
|
|
406
|
+
return undefined;
|
|
407
|
+
const text = plainText(n.children ?? []).trim().toLowerCase();
|
|
408
|
+
return text ? ctx.headingIdByText.get(text) : undefined;
|
|
409
|
+
}
|
|
392
410
|
function unresolvedReference(ctx, n, kind) {
|
|
393
411
|
if (n.ref === undefined)
|
|
394
412
|
return null;
|
|
@@ -453,6 +471,12 @@ function inline(ctx, n) {
|
|
|
453
471
|
: text;
|
|
454
472
|
}
|
|
455
473
|
case 'link': {
|
|
474
|
+
// A collapsed reference to a heading is RESOLVED, not missing, so
|
|
475
|
+
// it is tried before the missing-definition path reports one.
|
|
476
|
+
const heading = String(n.href ?? '') === '' ? collapsedHeadingRef(ctx, n) : undefined;
|
|
477
|
+
if (heading !== undefined) {
|
|
478
|
+
return [P.Link(toAttr(n.attrs), kids(ctx, n), ['#' + heading, ''])];
|
|
479
|
+
}
|
|
456
480
|
const unresolved = unresolvedReference(ctx, n, 'link');
|
|
457
481
|
if (unresolved)
|
|
458
482
|
return unresolved;
|
|
@@ -466,7 +490,18 @@ function inline(ctx, n) {
|
|
|
466
490
|
case 'autolink': {
|
|
467
491
|
const href = String(n.href ?? '');
|
|
468
492
|
const cls = href.startsWith('mailto:') ? 'email' : 'uri';
|
|
469
|
-
|
|
493
|
+
// An autolink takes a trailing attribute like any other inline
|
|
494
|
+
// (`<https://example.com>{.ext}`), and pandoc's Link has the slot
|
|
495
|
+
// for it, but only the synthesized `uri`/`email` class was written -
|
|
496
|
+
// so the author's id, classes and key-values left the document with
|
|
497
|
+
// nothing reported. They join the synthesized class rather than
|
|
498
|
+
// replace it: the class says what KIND of link this is, and a
|
|
499
|
+
// consumer keying on it should not lose that because the author
|
|
500
|
+
// added one of their own.
|
|
501
|
+
const [id, classes, kvs] = toAttr(n.attrs);
|
|
502
|
+
return [
|
|
503
|
+
P.Link([id, [cls, ...classes], kvs], [P.Str(String(n.text ?? href))], [href, '']),
|
|
504
|
+
];
|
|
470
505
|
}
|
|
471
506
|
case 'image': {
|
|
472
507
|
const unresolved = unresolvedReference(ctx, n, 'image');
|
|
@@ -506,10 +541,15 @@ function inline(ctx, n) {
|
|
|
506
541
|
ctx.crossrefTargets.get(target.toLowerCase()) ??
|
|
507
542
|
findCaseInsensitive(ctx.crossrefTargets, target);
|
|
508
543
|
if (found) {
|
|
544
|
+
// A caption target cannot come back as a crossref (see
|
|
545
|
+
// `captionTargets`), so it goes out as a plain link.
|
|
546
|
+
const classes = ctx.captionTargets.has(target.toLowerCase())
|
|
547
|
+
? []
|
|
548
|
+
: ['crossref'];
|
|
509
549
|
ctx.inCrossref = true;
|
|
510
550
|
try {
|
|
511
551
|
return [
|
|
512
|
-
P.Link(P.attr(undefined,
|
|
552
|
+
P.Link(P.attr(undefined, classes), inlines(ctx, found), [
|
|
513
553
|
`#${target}`,
|
|
514
554
|
'',
|
|
515
555
|
]),
|
|
@@ -540,8 +580,27 @@ function inline(ctx, n) {
|
|
|
540
580
|
const id = String(n.id ?? '');
|
|
541
581
|
const def = ctx.footnoteDefs[id];
|
|
542
582
|
if (!def) {
|
|
543
|
-
|
|
544
|
-
|
|
583
|
+
// THE LITERAL SOURCE, not a superscript.
|
|
584
|
+
//
|
|
585
|
+
// Carve renders an unresolved `[^f]` as the four characters the
|
|
586
|
+
// author typed. This used to emit `Superscript [Str "f"]`, which
|
|
587
|
+
// is a construct the document does not contain: the reader saw a
|
|
588
|
+
// raised `f` where the source says `[^f]`, and the round trip
|
|
589
|
+
// came back spelling it `{^f^}` - a superscript in Carve too, so
|
|
590
|
+
// the loss was permanent rather than merely cosmetic.
|
|
591
|
+
//
|
|
592
|
+
// The sibling path has been right all along. An unresolved link
|
|
593
|
+
// or image reference returns its `rawRef` as text for exactly
|
|
594
|
+
// this reason (see `unresolvedReference`), and there was no
|
|
595
|
+
// reason for a footnote to be the one construct that invented a
|
|
596
|
+
// node instead.
|
|
597
|
+
//
|
|
598
|
+
// A footnote reference carries no `rawRef`, so the source is
|
|
599
|
+
// rebuilt from the id. That is faithful even where the author
|
|
600
|
+
// wrote a trailing attribute: the engine drops the attribute
|
|
601
|
+
// along with the reference, so `[^a]{.ref}` renders `[^a]` too.
|
|
602
|
+
warn(ctx, `footnote: missing definition for [^${id}] - emitting the literal source`);
|
|
603
|
+
return textInlines(`[^${id}]`);
|
|
545
604
|
}
|
|
546
605
|
const note = P.Note(blocks(ctx, def));
|
|
547
606
|
ctx.noteCount++;
|
|
@@ -752,6 +811,15 @@ function block(ctx, n) {
|
|
|
752
811
|
// A block-attribute line can attach attrs to ANY block. Pandoc's Para,
|
|
753
812
|
// BlockQuote, lists etc. have no Attr slot - preserve via a Div wrapper.
|
|
754
813
|
const a = n.attrs;
|
|
814
|
+
// NOTHING IS NOT WRAPPED. A node that renders nothing where it sits - a
|
|
815
|
+
// link reference definition, an abbreviation definition - converts to no
|
|
816
|
+
// blocks, and wrapping that in a Div to carry its attrs put a visible empty
|
|
817
|
+
// element into every writer's output where Carve renders none:
|
|
818
|
+
// `[a]: /u {.c}` came back as `<div class="c"></div>`. The attrs are not
|
|
819
|
+
// lost by skipping it; they are already on whatever the definition feeds,
|
|
820
|
+
// which is where they render.
|
|
821
|
+
if (!result.length)
|
|
822
|
+
return result;
|
|
755
823
|
if (!ATTR_CARRYING.has(n.type) && hasAttrs(a)) {
|
|
756
824
|
// In roundtrip mode the carve-block marker lets the reverse direction
|
|
757
825
|
// restore the attrs onto the inner block instead of keeping a wrapper.
|
|
@@ -1066,7 +1134,26 @@ function table(ctx, n, caption, shortCaption = null) {
|
|
|
1066
1134
|
// that was never in the source.
|
|
1067
1135
|
const firstRowIsHead = headCount > 0;
|
|
1068
1136
|
const firstRow = rows[0] ?? [];
|
|
1069
|
-
|
|
1137
|
+
// THE COLUMN COUNT IS THE WIDEST ROW, NOT THE FIRST. A Carve table is
|
|
1138
|
+
// RAGGED - PART 9 §16 keeps each row's own cell count - while a pandoc
|
|
1139
|
+
// table is rectangular and its ColSpec list DEFINES the width. Sizing the
|
|
1140
|
+
// list from the first row therefore emitted a table whose later rows have
|
|
1141
|
+
// cells past the last column, which is not a shape pandoc's own readers
|
|
1142
|
+
// produce: `| ~x~ |` above `| a | b |` declared one column, and `b` was
|
|
1143
|
+
// dropped on the way back with nothing reported. The alignment of a column
|
|
1144
|
+
// no first-row cell covers is simply unset.
|
|
1145
|
+
const width = Math.max(0, ...rows.map((row) => row.length));
|
|
1146
|
+
if (rows.some((row) => row.length < width)) {
|
|
1147
|
+
// Padding is the conversion, not a defect - but it is a change the
|
|
1148
|
+
// author can see, so it is reported like every other one.
|
|
1149
|
+
warn(ctx, `table: ${rows.filter((row) => row.length < width).length} row(s) shorter than the `
|
|
1150
|
+
+ `widest (${width} cells) are padded with empty cells - a pandoc table is `
|
|
1151
|
+
+ 'rectangular, and PART 9 §16 keeps each row\'s own cell count');
|
|
1152
|
+
}
|
|
1153
|
+
const colAligns = Array.from({ length: width }, (_, c) => {
|
|
1154
|
+
const cell = firstRow[c];
|
|
1155
|
+
return firstRowIsHead && cell ? ALIGN[cell.align ?? ''] ?? 'AlignDefault' : 'AlignDefault';
|
|
1156
|
+
});
|
|
1070
1157
|
const origin = rows.map(() => []);
|
|
1071
1158
|
const emitted = rows.map((row) => row.map(() => null));
|
|
1072
1159
|
for (let r = 0; r < rows.length; r++) {
|
|
@@ -1173,8 +1260,50 @@ function table(ctx, n, caption, shortCaption = null) {
|
|
|
1173
1260
|
footRows = toRows(at, at + groups.footRows);
|
|
1174
1261
|
}
|
|
1175
1262
|
else {
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1263
|
+
// ROW HEADERS ARE DERIVED WHEN NOTHING DECLARED THEM. A body row may
|
|
1264
|
+
// open with header cells (`|= Mercury | 4,879.4 |`), which the engine
|
|
1265
|
+
// renders as `<th scope="row">`. Pandoc says the same thing with
|
|
1266
|
+
// `RowHeadColumns`, a COUNT on the body - and the count was only ever
|
|
1267
|
+
// read from an explicit `rowGroups`, so a table that simply marked its
|
|
1268
|
+
// first cells came out as ordinary `<td>` and lost the row headers.
|
|
1269
|
+
//
|
|
1270
|
+
// ONE BODY PER RUN OF ROWS THAT AGREE, not one body for the table.
|
|
1271
|
+
//
|
|
1272
|
+
// This used to take the MINIMUM leading run across every body row,
|
|
1273
|
+
// because one `RowHeadColumns` is one number. A body whose rows disagree
|
|
1274
|
+
// therefore lost every row header above that minimum, and lost it
|
|
1275
|
+
// SILENTLY - the cells simply came out `<td>`. Corpus 354-2 is the
|
|
1276
|
+
// smallest case: a plain row followed by a row-header row takes the
|
|
1277
|
+
// minimum 0, so `<th scope="row">b c</th>` came back `<td>b c</td>`.
|
|
1278
|
+
//
|
|
1279
|
+
// Pandoc's own model has the slot for this. A `Table` holds a LIST of
|
|
1280
|
+
// bodies and each carries its own `RowHeadColumns`, so consecutive rows
|
|
1281
|
+
// that agree become one body and a change starts the next one. Nothing
|
|
1282
|
+
// is invented: a document with a single run still emits exactly one
|
|
1283
|
+
// body, which is what every table that agreed already produced.
|
|
1284
|
+
bodies = [];
|
|
1285
|
+
const leadOf = (row) => {
|
|
1286
|
+
let n = 0;
|
|
1287
|
+
while (n < row.length && row[n].header === true)
|
|
1288
|
+
n++;
|
|
1289
|
+
return Math.min(n, width);
|
|
1290
|
+
};
|
|
1291
|
+
let at = headCount;
|
|
1292
|
+
while (at < rows.length) {
|
|
1293
|
+
const lead = leadOf(rows[at]);
|
|
1294
|
+
let to = at + 1;
|
|
1295
|
+
while (to < rows.length && leadOf(rows[to]) === lead)
|
|
1296
|
+
to++;
|
|
1297
|
+
const group = { bodyRows: toRows(at, to) };
|
|
1298
|
+
if (lead > 0)
|
|
1299
|
+
group.rowHeadColumns = lead;
|
|
1300
|
+
bodies.push(group);
|
|
1301
|
+
at = to;
|
|
1302
|
+
}
|
|
1303
|
+
// A table with a head and no body rows at all still needs a body,
|
|
1304
|
+
// because that is the shape pandoc's own readers produce.
|
|
1305
|
+
if (!bodies.length)
|
|
1306
|
+
bodies = [{ bodyRows: [] }];
|
|
1178
1307
|
}
|
|
1179
1308
|
return P.Table(toAttr(n.attrs), caption, colAligns, toRows(0, headCount), bodies, footRows, shortCaption);
|
|
1180
1309
|
}
|
|
@@ -1652,6 +1781,8 @@ export function convert(ast, options = {}) {
|
|
|
1652
1781
|
warnings: [],
|
|
1653
1782
|
footnoteDefs,
|
|
1654
1783
|
crossrefTargets: new Map(),
|
|
1784
|
+
headingIdByText: new Map(),
|
|
1785
|
+
captionTargets: new Set(),
|
|
1655
1786
|
tight: false,
|
|
1656
1787
|
inCrossref: false,
|
|
1657
1788
|
captionCounts: new Map(),
|
|
@@ -1726,6 +1857,10 @@ function collectCrossrefTargets(ctx, nodes, captionCounts, inPanel = false) {
|
|
|
1726
1857
|
const id = a.id ?? slugify(plainText(children));
|
|
1727
1858
|
if (id && !ctx.crossrefTargets.has(id))
|
|
1728
1859
|
ctx.crossrefTargets.set(id, children);
|
|
1860
|
+
const text = plainText(children).trim().toLowerCase();
|
|
1861
|
+
// First heading wins, the same way the id map resolves a duplicate.
|
|
1862
|
+
if (id && text && !ctx.headingIdByText.has(text))
|
|
1863
|
+
ctx.headingIdByText.set(text, id);
|
|
1729
1864
|
}
|
|
1730
1865
|
else if (!inPanel && (n.type === 'figure' || n.type === 'table')) {
|
|
1731
1866
|
// Every figure counts, whatever it wraps. A quote used to be
|
|
@@ -1740,6 +1875,7 @@ function collectCrossrefTargets(ctx, nodes, captionCounts, inPanel = false) {
|
|
|
1740
1875
|
const a = (n.attrs ?? {});
|
|
1741
1876
|
if (a.id && !ctx.crossrefTargets.has(a.id)) {
|
|
1742
1877
|
ctx.crossrefTargets.set(a.id, [{ type: 'text', value: `${label} ${next}` }]);
|
|
1878
|
+
ctx.captionTargets.add(a.id.toLowerCase());
|
|
1743
1879
|
}
|
|
1744
1880
|
}
|
|
1745
1881
|
}
|
|
@@ -1784,6 +1920,7 @@ function figureGroupTargets(ctx, n, captionCounts, inPanel) {
|
|
|
1784
1920
|
const a = (n.attrs ?? {});
|
|
1785
1921
|
if (a.id && !ctx.crossrefTargets.has(a.id)) {
|
|
1786
1922
|
ctx.crossrefTargets.set(a.id, [{ type: 'text', value: resolved }]);
|
|
1923
|
+
ctx.captionTargets.add(a.id.toLowerCase());
|
|
1787
1924
|
}
|
|
1788
1925
|
}
|
|
1789
1926
|
let panelIndex = 0;
|
package/dist/reverse.js
CHANGED
|
@@ -270,7 +270,16 @@ function link(ctx, c) {
|
|
|
270
270
|
const [a, xs, [href, title]] = c;
|
|
271
271
|
const [, classes] = a;
|
|
272
272
|
if (classes.includes('uri') || classes.includes('email')) {
|
|
273
|
-
|
|
273
|
+
const node = { type: 'autolink', href, text: stringify(xs) };
|
|
274
|
+
// `uri`/`email` is the class the forward direction SYNTHESIZES to say
|
|
275
|
+
// what kind of link this is, so it is not the author's and does not
|
|
276
|
+
// come back. Anything else on the Attr is the author's - an autolink
|
|
277
|
+
// takes a trailing attribute like any other inline - and used to be
|
|
278
|
+
// dropped here without a word.
|
|
279
|
+
const rest = fromAttr([a[0], classes.filter((c) => c !== 'uri' && c !== 'email'), a[2]]);
|
|
280
|
+
if (rest)
|
|
281
|
+
node.attrs = rest;
|
|
282
|
+
return [node];
|
|
274
283
|
}
|
|
275
284
|
if (classes.includes('crossref')) {
|
|
276
285
|
return [{ type: 'heading_ref', target: href.replace(/^#/, '') }];
|
|
@@ -746,12 +755,25 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
|
|
|
746
755
|
// leading run.
|
|
747
756
|
const allRaw = [...headRaw];
|
|
748
757
|
const isHeaderRow = headRaw.map(() => true);
|
|
758
|
+
// How many LEADING cells of each row are row headers.
|
|
759
|
+
//
|
|
760
|
+
// This is pandoc's `RowHeadColumns`, and the pipe table spells it directly:
|
|
761
|
+
// a body row opening `|= Mercury |` is a row header, `<th scope="row">`,
|
|
762
|
+
// measured on the engine. It used to be treated as unspellable, so such a
|
|
763
|
+
// table left the pipe form for a `::: list-table {header-cols=N}` - more
|
|
764
|
+
// markup, an extension the reader has to enable, and one number for the
|
|
765
|
+
// whole table where the cells can each say it. Marking the cells keeps the
|
|
766
|
+
// readable form and round-trips, because the forward direction derives the
|
|
767
|
+
// count back from exactly this run.
|
|
768
|
+
const rowHeadCols = headRaw.map(() => 0);
|
|
749
769
|
const groupBodies = [];
|
|
750
770
|
for (const body of tbodies) {
|
|
751
771
|
const bodyHead = body[2];
|
|
752
772
|
const bodyRows = body[3];
|
|
753
773
|
allRaw.push(...bodyHead, ...bodyRows);
|
|
754
774
|
isHeaderRow.push(...bodyHead.map(() => true), ...bodyRows.map(() => false));
|
|
775
|
+
const heads = typeof body[1] === 'number' && body[1] > 0 ? body[1] : 0;
|
|
776
|
+
rowHeadCols.push(...bodyHead.map(() => 0), ...bodyRows.map(() => heads));
|
|
755
777
|
const group = { headRows: bodyHead.length, bodyRows: bodyRows.length };
|
|
756
778
|
const rowHeadColumns = body[1];
|
|
757
779
|
if (typeof rowHeadColumns === 'number' && rowHeadColumns > 0) {
|
|
@@ -764,6 +786,7 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
|
|
|
764
786
|
}
|
|
765
787
|
allRaw.push(...footRaw);
|
|
766
788
|
isHeaderRow.push(...footRaw.map(() => false));
|
|
789
|
+
rowHeadCols.push(...footRaw.map(() => 0));
|
|
767
790
|
// Occupancy grid: pending[r][c] = continuation marker owed at that position.
|
|
768
791
|
// Decided BEFORE the grid walk: the pipe path flattens as it goes and warns
|
|
769
792
|
// while doing it, and that warning is wrong for a table that is about to be
|
|
@@ -772,28 +795,12 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
|
|
|
772
795
|
const cellBlocks = raw[4];
|
|
773
796
|
return Array.isArray(cellBlocks) && !isInlineShaped(cellBlocks);
|
|
774
797
|
}));
|
|
775
|
-
//
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
// `reportUnspellableGroups` names the loss instead.
|
|
782
|
-
//
|
|
783
|
-
// ONLY WHEN THE BODIES AGREE. `header-cols` is one number for the whole
|
|
784
|
-
// table, so a table whose bodies differ - and `1` beside a plain `0` is the
|
|
785
|
-
// ordinary way they differ - would come back with row headers ADDED to the
|
|
786
|
-
// bodies that had none. Widening the markup is worse than the flattening it
|
|
787
|
-
// was meant to avoid: dropping a row header renders a `td` where a `th`
|
|
788
|
-
// belonged, while inventing one asserts a heading the source never made.
|
|
789
|
-
// Such a table keeps the pipe form, where the loss is only a loss and is
|
|
790
|
-
// reported. A table forced onto the list-table path by its block cells has
|
|
791
|
-
// no such choice, and `listTable` reports the widening there instead.
|
|
792
|
-
const rowHeadCounts = new Set(groupBodies.map((b) => b.rowHeadColumns ?? 0));
|
|
793
|
-
const hasRowHeadColumns = ctx.target === 'source'
|
|
794
|
-
&& rowHeadCounts.size === 1
|
|
795
|
-
&& (groupBodies[0]?.rowHeadColumns ?? 0) > 0;
|
|
796
|
-
const useListTable = hasBlockCells || hasRowHeadColumns;
|
|
798
|
+
// Row-head columns no longer send a table to the list-table form: the pipe
|
|
799
|
+
// table marks those cells directly (see `rowHeadCols` above), which is
|
|
800
|
+
// readable, needs no extension, and round-trips. Block cells remain the one
|
|
801
|
+
// reason to leave, because a Carve `table_cell` holds INLINES and there is
|
|
802
|
+
// no pipe form for a cell holding blocks at all.
|
|
803
|
+
const useListTable = hasBlockCells;
|
|
797
804
|
const pending = allRaw.map(() => Array(nCols));
|
|
798
805
|
const rows = [];
|
|
799
806
|
// The pandoc blocks that sat at each grid position, kept alongside the
|
|
@@ -801,29 +808,35 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
|
|
|
801
808
|
// as blocks without walking the occupancy grid a second time.
|
|
802
809
|
const cellBlocksAt = allRaw.map(() => []);
|
|
803
810
|
for (let r = 0; r < allRaw.length; r++) {
|
|
804
|
-
|
|
811
|
+
// A row is header cells throughout when it is a header ROW, and for its
|
|
812
|
+
// leading run when the body declared row-head columns.
|
|
813
|
+
const headTo = isHeaderRow[r] === true ? nCols : (rowHeadCols[r] ?? 0);
|
|
805
814
|
const cells = [];
|
|
806
815
|
const rawCells = allRaw[r][1];
|
|
807
816
|
let rawIdx = 0;
|
|
808
817
|
for (let col = 0; col < nCols; col++) {
|
|
809
818
|
if (pending[r][col]) {
|
|
810
|
-
cells.push({ type: 'table_cell', header:
|
|
819
|
+
cells.push({ type: 'table_cell', header: col < headTo, children: [], span: 'rowspan' });
|
|
811
820
|
continue;
|
|
812
821
|
}
|
|
813
822
|
const raw = rawCells[rawIdx++];
|
|
814
823
|
if (!raw) {
|
|
815
|
-
cells.push({ type: 'table_cell', header:
|
|
824
|
+
cells.push({ type: 'table_cell', header: col < headTo, children: [] });
|
|
816
825
|
continue;
|
|
817
826
|
}
|
|
818
827
|
const [cellAttr, cellAlign, rowSpan, colSpan, cellBlocks] = raw;
|
|
819
828
|
cellBlocksAt[r][col] = cellBlocks;
|
|
820
829
|
const cell = {
|
|
821
830
|
type: 'table_cell',
|
|
822
|
-
header:
|
|
831
|
+
header: col < headTo,
|
|
823
832
|
children: useListTable ? [] : cellInlines(ctx, cellBlocks),
|
|
824
833
|
};
|
|
834
|
+
// The column's alignment is carried by a HEADER ROW's cells, or by
|
|
835
|
+
// every cell when there is no header row. A row-head cell is not a
|
|
836
|
+
// header row, so it does not carry the column - its own marker
|
|
837
|
+
// aligns itself alone, which is what the engine does.
|
|
825
838
|
const align = ALIGN_BACK[cellAlign.t]
|
|
826
|
-
?? (
|
|
839
|
+
?? (isHeaderRow[r] === true || !hasHeaderRow ? colAligns[col] : '');
|
|
827
840
|
if (align)
|
|
828
841
|
cell.align = align;
|
|
829
842
|
const cellAttrs = fromAttr(cellAttr);
|
|
@@ -831,7 +844,7 @@ function table(ctx, c, captionOverride, shortCaptionOverride = null) {
|
|
|
831
844
|
cell.attrs = cellAttrs;
|
|
832
845
|
cells.push(cell);
|
|
833
846
|
for (let j = 1; j < colSpan && col + j < nCols; j++) {
|
|
834
|
-
cells.push({ type: 'table_cell', header:
|
|
847
|
+
cells.push({ type: 'table_cell', header: col + j < headTo, children: [], span: 'colspan' });
|
|
835
848
|
}
|
|
836
849
|
// A 2D block gets a rowspan continuation at EVERY covered column
|
|
837
850
|
// of the lower rows - that is how Carve's grid expresses it.
|
|
@@ -1222,6 +1235,48 @@ function figure(ctx, c) {
|
|
|
1222
1235
|
out.push({ type: 'paragraph', children: caption });
|
|
1223
1236
|
return out;
|
|
1224
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* The inverse of convert.ts's `citationDefinition`: citeproc's own bibliography
|
|
1240
|
+
* entry, `Div ("ref-<key>", ["csl-entry"], ...)`, back to a Carve `[@key]:`
|
|
1241
|
+
* definition line.
|
|
1242
|
+
*
|
|
1243
|
+
* The forward direction has written this shape since the arm was added, but
|
|
1244
|
+
* nothing read it, so a bibliography line went out as a definition and came
|
|
1245
|
+
* back as a fenced div carrying an id and a class - the entry rendered in the
|
|
1246
|
+
* body of the document, and the key that binds it to its citations survived
|
|
1247
|
+
* only as the text of an id. That asymmetry was invisible while the engine
|
|
1248
|
+
* still handed a definition line to the bridge as a paragraph holding a
|
|
1249
|
+
* citation group; it surfaced the moment carve-js started emitting a real
|
|
1250
|
+
* `citation_definition` node for one.
|
|
1251
|
+
*
|
|
1252
|
+
* Only the exact shape the forward direction produces converts back: a
|
|
1253
|
+
* `ref-`-prefixed id, `csl-entry` first among the classes, and a body that is
|
|
1254
|
+
* empty or one `Para`. A `--citeproc` bibliography whose entry runs to several
|
|
1255
|
+
* blocks has no Carve definition to be, so it stays the div it is rather than
|
|
1256
|
+
* being flattened into one.
|
|
1257
|
+
*/
|
|
1258
|
+
function citationDefinition(ctx, id, classes, kvs, body) {
|
|
1259
|
+
if (classes[0] !== 'csl-entry')
|
|
1260
|
+
return undefined;
|
|
1261
|
+
if (!id.startsWith('ref-') || id === 'ref-')
|
|
1262
|
+
return undefined;
|
|
1263
|
+
if (body.length > 1)
|
|
1264
|
+
return undefined;
|
|
1265
|
+
const only = body[0];
|
|
1266
|
+
if (only && only.t !== 'Para')
|
|
1267
|
+
return undefined;
|
|
1268
|
+
const node = {
|
|
1269
|
+
type: 'citation_definition',
|
|
1270
|
+
key: id.slice('ref-'.length),
|
|
1271
|
+
children: only ? inlines(ctx, only.c) : [],
|
|
1272
|
+
};
|
|
1273
|
+
// The id is the key's carrier here, so it is spent; anything else the Attr
|
|
1274
|
+
// holds is the author's and rides back as the definition's attributes.
|
|
1275
|
+
const attrs = fromAttr(['', classes.slice(1), kvs]);
|
|
1276
|
+
if (attrs)
|
|
1277
|
+
node.attrs = attrs;
|
|
1278
|
+
return node;
|
|
1279
|
+
}
|
|
1225
1280
|
function div(ctx, c) {
|
|
1226
1281
|
const [a, body] = c;
|
|
1227
1282
|
const [id, classes, rawKvs] = a;
|
|
@@ -1243,6 +1298,9 @@ function div(ctx, c) {
|
|
|
1243
1298
|
return [child];
|
|
1244
1299
|
}
|
|
1245
1300
|
}
|
|
1301
|
+
const definition = citationDefinition(ctx, id, classes, kvs, body);
|
|
1302
|
+
if (definition)
|
|
1303
|
+
return [definition];
|
|
1246
1304
|
let kind;
|
|
1247
1305
|
let rest = [];
|
|
1248
1306
|
if (classes.includes('admonition')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markup-carve/pandoc-carve",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"source": "https://github.com/markup-carve/pandoc-carve"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@markup-carve/carve": "
|
|
52
|
+
"@markup-carve/carve": "^0.1.4"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^24.0.0",
|