Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/html-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ class Importer {
}

private table(node: P5Node, path: string, depth: number, attrs?: Attrs): BlockNode {
/*
* `<caption>` is a DIRECT child of the table and holds the table's own
* caption, which `table.caption` has a slot for and Carve spells `^ text`
* after the rows. The row walk below looks only for `tr`, so before this
* the element was skipped and the caption left the document silently -
* pandoc emits exactly this shape for every captioned table.
*/
const captionNode = (node.childNodes ?? []).find((n) => n.tagName === 'caption')
const tr: P5Node[] = []
const walk = (n: P5Node): void => {
if (n.tagName === 'tr') tr.push(n)
Expand Down Expand Up @@ -344,7 +352,10 @@ class Importer {
return { type: 'table_cell', header: cell.tagName === 'th', children: this.inlines(cell.childNodes ?? [], cellPath, depth + 1), ...(kept ? { attrs: kept } : {}) }
}),
}))
return { type: 'table', rows, ...(attrs ? { attrs } : {}) }
const caption = captionNode
? this.inlines(captionNode.childNodes ?? [], `${path}/caption[1]`, depth + 1)
: undefined
return { type: 'table', rows, ...(caption ? { caption } : {}), ...(attrs ? { attrs } : {}) }
}

private figure(node: P5Node, path: string, depth: number, attrs?: Attrs): BlockNode[] {
Expand All @@ -353,6 +364,23 @@ class Importer {
const targets = this.blocks(body, path, depth + 1)
const target = targets[0]
if (target && ['image', 'block_quote', 'table', 'code_block', 'paragraph'].includes(target.type)) {
/*
* A table brings its own caption slot, so a figure-wrapped table can
* arrive carrying TWO captions - its own `<caption>` and the figure's
* `<figcaption>`. Carve spells one `^ ` line per host, and the wrapper
* itself has no spelling at all, so the figure's caption is the one that
* cannot survive. Keeping both wrote two `^ ` lines, and the second
* re-read as a literal paragraph.
*/
if (target.type === 'table' && (target as { caption?: unknown }).caption && captionNode) {
this.add(
'table-degraded',
'Dropped a <figcaption> from a figure wrapping a table that carries its own <caption>: Carve spells one caption per table',
'warning',
`${path}/figcaption[1]`,
)
return [target, ...targets.slice(1)]
}
return [{ type: 'figure', target: target as never, caption: this.inlines(captionNode?.childNodes ?? [], `${path}/figcaption[1]`, depth + 1), ...(attrs ? { attrs } : {}) }, ...targets.slice(1)]
}
this.add('element-unwrapped', 'Unwrapped figure without a representable target', 'warning', path)
Expand Down
32 changes: 32 additions & 0 deletions test/html-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,35 @@ describe('table cell scope on import', () => {
expect(codes(html)).toContain('attribute-dropped')
})
})

describe('table caption on import', () => {
const carve = (html: string) => htmlToCarve(html).value.trim()
const codes = (html: string) => htmlToCarve(html).report.diagnostics.map((d) => d.code)

it('reads a table\'s own <caption>, which pandoc emits for every captioned table', () => {
// The row walk looks only for `tr`, so before this the <caption> element
// was skipped and its text left the document with no diagnostic at all.
const source = carve(
'<table><caption>Fruit prices</caption><thead><tr><th>A</th></tr></thead><tbody><tr><td>1</td></tr></tbody></table>',
)
expect(source).toBe('|=A|\n| 1 |\n^ Fruit prices')
expect(carveToHtml(source)).toContain('<caption>Fruit prices</caption>')
expect(codes('<table><caption>C</caption><tbody><tr><td>1</td></tr></tbody></table>')).toEqual([])
})

it('puts a figure-wrapped table\'s figcaption in the caption slot the table left empty', () => {
expect(carve('<figure><table><tbody><tr><td>1</td></tr></tbody></table><figcaption>Outer</figcaption></figure>'))
.toBe('| 1 |\n^ Outer')
})

it('keeps the table\'s own caption when a figure also captions it, and says so', () => {
// Two captions, one slot. Carve cannot spell the figure WRAPPER around a
// table at all, so the figure's caption is the one that cannot survive.
// Writing both produced a second `^ ` line that re-read as a paragraph.
const html =
'<figure><table><caption>Inner</caption><tbody><tr><td>1</td></tr></tbody></table><figcaption>Outer</figcaption></figure>'
expect(carve(html)).toBe('| 1 |\n^ Inner')
expect(codes(html)).toEqual(['table-degraded'])
expect(carveToHtml(carve(html))).not.toContain('<p>^')
})
})
Loading