Files
upstream-tiptap/packages/extension-table/tests/integration/tableMarkdown.test.ts
upstream-mirror 083a46e93b
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:11:02 +05:00

276 lines
9.9 KiB
TypeScript

import { Code } from '@tiptap/extension-code'
import Document from '@tiptap/extension-document'
import HardBreak from '@tiptap/extension-hard-break'
import Paragraph from '@tiptap/extension-paragraph'
import { TableKit } from '@tiptap/extension-table'
import Text from '@tiptap/extension-text'
import { MarkdownManager } from '@tiptap/markdown'
import { describe, expect, it } from 'vite-plus/test'
describe('table markdown — inline code with pipe characters', () => {
const manager = new MarkdownManager({
extensions: [Document, Paragraph, Text, Code, TableKit],
})
it('should parse `||` inside a code span as a single cell', () => {
const markdown = '| Header |\n| ------ |\n| `||` |'
const parsed = manager.parse(markdown)
const bodyRow = parsed.content?.[0]?.content?.[1]?.content || []
expect(bodyRow).toHaveLength(1)
const textNode = bodyRow[0]?.content?.[0]?.content?.[0]
expect(textNode?.text).toBe('||')
expect(textNode?.marks?.[0]?.type).toBe('code')
})
it('should parse `a || b` inside a code span as a single cell', () => {
const markdown = '| H | H | H |\n| - | - | - |\n| `||` | or | `a || b` |'
const parsed = manager.parse(markdown)
const table = parsed.content?.[0]
expect(table?.type).toBe('table')
const bodyRow = table?.content?.[1]?.content || []
expect(bodyRow).toHaveLength(3)
const cell1 = bodyRow[0]?.content?.[0]?.content?.[0]
expect(cell1?.text).toBe('||')
expect(cell1?.marks?.[0]?.type).toBe('code')
const cell2 = bodyRow[1]?.content?.[0]?.content?.[0]
expect(cell2?.text).toBe('or')
expect(cell2?.marks).toBeUndefined()
const cell3 = bodyRow[2]?.content?.[0]?.content?.[0]
expect(cell3?.text).toBe('a || b')
expect(cell3?.marks?.[0]?.type).toBe('code')
})
it('should parse `&&` inside a code span correctly (regression guard)', () => {
const markdown = '| H | H | H |\n| - | - | - |\n| `&&` | and | `a && b` |'
const parsed = manager.parse(markdown)
const bodyRow = parsed.content?.[0]?.content?.[1]?.content || []
expect(bodyRow).toHaveLength(3)
const cell1 = bodyRow[0]?.content?.[0]?.content?.[0]
expect(cell1?.text).toBe('&&')
expect(cell1?.marks?.[0]?.type).toBe('code')
const cell3 = bodyRow[2]?.content?.[0]?.content?.[0]
expect(cell3?.text).toBe('a && b')
expect(cell3?.marks?.[0]?.type).toBe('code')
})
it('should roundtrip table with inline code containing pipes', () => {
const markdown = '| H |\n| - |\n| `a || b` |'
const parsed = manager.parse(markdown)
const serialized = manager.serialize(parsed)
const reparsed = manager.parse(serialized)
const cell = reparsed.content?.[0]?.content?.[1]?.content?.[0]?.content?.[0]?.content?.[0]
expect(cell?.text).toBe('a || b')
expect(cell?.marks?.[0]?.type).toBe('code')
})
it('should not affect table cells without backtick spans', () => {
const markdown = '| a | b |\n| - | - |\n| 1 | 2 |'
const parsed = manager.parse(markdown)
const bodyRow = parsed.content?.[0]?.content?.[1]?.content || []
expect(bodyRow).toHaveLength(2)
expect(bodyRow[0]?.content?.[0]?.content?.[0]?.text).toBe('1')
expect(bodyRow[1]?.content?.[0]?.content?.[0]?.text).toBe('2')
})
it('should not corrupt content that follows a table with inline code containing pipes', () => {
const markdown = '| H |\n| - |\n| `a || b` |\n\nParagraph after.'
const parsed = manager.parse(markdown)
expect(parsed.content).toHaveLength(2)
expect(parsed.content?.[0]?.type).toBe('table')
expect(parsed.content?.[1]?.type).toBe('paragraph')
expect(parsed.content?.[1]?.content?.[0]?.text).toBe('Paragraph after.')
})
it('should not affect inline code with pipes outside of tables', () => {
const markdown = 'Use `a || b` for logical or.'
const parsed = manager.parse(markdown)
// Must parse as a paragraph, not accidentally as a table
const block = parsed.content?.[0]
expect(block?.type).toBe('paragraph')
// The code mark must be present on one of the inline nodes
const codeNode = block?.content?.find(n => n.marks?.some(m => m.type === 'code'))
expect(codeNode).toBeDefined()
expect(codeNode?.marks?.[0]?.type).toBe('code')
})
it('should treat a setext heading with pipe in title as a heading, not a table', () => {
// '`a | b`\n---' is a setext h2, not a table. Our tokenizer must not
// activate on it and must not contaminate the inline queue via
// helper.blockTokens (which would turn '|' into '\|' in the output).
const markdown = '`a | b`\n---'
const parsed = manager.parse(markdown)
const block = parsed.content?.[0]
expect(block?.type).toBe('heading')
// The pipe must be preserved verbatim inside the code span.
const codeNode = block?.content?.find((n: any) => n.marks?.some((m: any) => m.type === 'code'))
expect(codeNode?.text).toBe('a | b')
})
it('should parse `||` inside a code span in a pipeless table as a single cell', () => {
const markdown = 'Expression | Meaning\n---------- | -------\n`||` | or'
const parsed = manager.parse(markdown)
const table = parsed.content?.[0]
expect(table?.type).toBe('table')
const bodyRow = table?.content?.[1]?.content || []
expect(bodyRow).toHaveLength(2)
const cell1 = bodyRow[0]?.content?.[0]?.content?.[0]
expect(cell1?.text).toBe('||')
expect(cell1?.marks?.[0]?.type).toBe('code')
const cell2 = bodyRow[1]?.content?.[0]?.content?.[0]
expect(cell2?.text).toBe('or')
})
it('should not corrupt content that follows a pipeless table with inline code containing pipes', () => {
const markdown = 'H1 | H2\n-- | --\n`a || b` | x\n\nParagraph after.'
const parsed = manager.parse(markdown)
expect(parsed.content).toHaveLength(2)
expect(parsed.content?.[0]?.type).toBe('table')
expect(parsed.content?.[1]?.type).toBe('paragraph')
expect(parsed.content?.[1]?.content?.[0]?.text).toBe('Paragraph after.')
})
it('should include a no-pipe body row as a table row, matching marked native behavior', () => {
// marked treats any non-blank line before the blank line as a table body row,
// even if it contains no pipe. Our tokenizer must not cut it off into a paragraph.
const markdown = 'A | B\n--- | ---\n`a || b` | c\njust some text\n\nAfter'
const parsed = manager.parse(markdown)
const table = parsed.content?.[0]
expect(table?.type).toBe('table')
// 2 body rows: "`a || b` | c" and "just some text"
expect(table?.content).toHaveLength(3) // header row + 2 body rows
expect(parsed.content?.[1]?.type).toBe('paragraph')
expect(parsed.content?.[1]?.content?.[0]?.text).toBe('After')
})
})
describe('table markdown alignment', () => {
const markdownManager = new MarkdownManager({
extensions: [Document, Paragraph, Text, TableKit],
})
it('should parse and serialize left/right/center table alignment', () => {
const markdown = `| left | right | center |
| :---- | ----: | :-----: |
| a | b | c |`
const parsed = markdownManager.parse(markdown)
const table = parsed.content?.[0]
expect(table?.type).toBe('table')
const headerCells = table?.content?.[0]?.content || []
expect(headerCells[0]?.attrs?.align).toBe('left')
expect(headerCells[1]?.attrs?.align).toBe('right')
expect(headerCells[2]?.attrs?.align).toBe('center')
const serialized = markdownManager.serialize(parsed)
expect(serialized).toContain('| left | right | center |')
expect(serialized).toMatch(/\|\s*:[-]+\s*\|\s*[-]+:\s*\|\s*:[-]+:\s*\|/)
})
})
describe('table markdown line breaks', () => {
const markdownManager = new MarkdownManager({
extensions: [Document, Paragraph, Text, HardBreak, TableKit],
})
const cell = (content: Record<string, unknown>[]) => ({
type: 'tableCell',
content,
})
const tableDoc = (cells: ReturnType<typeof cell>[]) => ({
type: 'doc',
content: [
{
type: 'table',
content: [{ type: 'tableRow', content: cells }],
},
],
})
it('keeps hard breaks in cells as <br>', () => {
const doc = tableDoc([
cell([
{
type: 'paragraph',
content: [
{ type: 'text', text: 'foo' },
{ type: 'hardBreak' },
{ type: 'text', text: 'bar' },
],
},
]),
])
const serialized = markdownManager.serialize(doc)
expect(serialized).toContain('foo<br>bar')
})
it('keeps consecutive hard breaks as separate <br> tags', () => {
const doc = tableDoc([
cell([
{
type: 'paragraph',
content: [
{ type: 'text', text: 'foo' },
{ type: 'hardBreak' },
{ type: 'hardBreak' },
{ type: 'text', text: 'bar' },
],
},
]),
])
const serialized = markdownManager.serialize(doc)
expect(serialized).toContain('foo<br><br>bar')
})
it('round trips <br> inside cells', () => {
const markdown = `| desc |
| --- |
| foo<br>bar |`
const parsed = markdownManager.parse(markdown)
const parsedText = JSON.stringify(parsed)
expect(parsedText).toContain('"hardBreak"')
const serialized = markdownManager.serialize(parsed)
expect(serialized).toContain('foo<br>bar')
})
it('joins multiple paragraphs in a cell with <br>', () => {
const doc = tableDoc([
cell([
{ type: 'paragraph', content: [{ type: 'text', text: 'one' }] },
{ type: 'paragraph', content: [{ type: 'text', text: 'two' }] },
]),
])
const serialized = markdownManager.serialize(doc)
expect(serialized).toContain('one<br>two')
expect(serialized).not.toContain('\u001F')
})
it('leaves cells without breaks alone', () => {
const doc = tableDoc([
cell([{ type: 'paragraph', content: [{ type: 'text', text: 'plain' }] }]),
])
const serialized = markdownManager.serialize(doc)
expect(serialized).toContain('| plain |')
expect(serialized).not.toContain('<br>')
})
})