Files
upstream-tiptap/packages/markdown/tests/integration/whitespace-only-marked-text.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

179 lines
5.5 KiB
TypeScript

import { Bold } from '@tiptap/extension-bold'
import { Code } from '@tiptap/extension-code'
import { Document } from '@tiptap/extension-document'
import { Italic } from '@tiptap/extension-italic'
import { Paragraph } from '@tiptap/extension-paragraph'
import Strike from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import { describe, expect, it } from 'vite-plus/test'
import { MarkdownManager } from '../../src/MarkdownManager.js'
describe('Whitespace-only marked text nodes', () => {
const extensions = [Document, Paragraph, Text, Bold, Italic, Code, Strike]
const markdownManager = new MarkdownManager({ extensions })
const paragraph = (content: Array<Record<string, any>>) => ({
type: 'doc',
content: [{ type: 'paragraph', content }],
})
it.each([
['bold', 'a b'],
['italic', 'a b'],
['code', 'a b'],
['strike', 'a b'],
])('emits no delimiters for a whitespace-only %s node', (markType, expected) => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: markType }] },
{ type: 'text', text: 'b' },
]),
)
expect(markdown).toBe(expected)
})
it.each([
['tab', '\t'],
['non-breaking space', '\u00a0'],
['newline', '\n'],
])('emits no delimiters for a whitespace-only node containing a %s', (_name, whitespace) => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: whitespace, marks: [{ type: 'bold' }] },
{ type: 'text', text: 'b' },
]),
)
expect(markdown).toBe(`a${whitespace}b`)
})
it('emits no delimiters for a multi-space whitespace-only node', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }] },
{ type: 'text', text: 'b' },
]),
)
expect(markdown).toBe('a b')
})
it('emits no delimiters when a whitespace-only node carries nested marks', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }, { type: 'italic' }] },
{ type: 'text', text: 'b' },
]),
)
expect(markdown).toBe('a b')
})
it('opens a continuing mark after the whitespace (Issue #8212)', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }] },
{ type: 'text', text: 'b', marks: [{ type: 'bold' }] },
]),
)
expect(markdown).toBe('a **b**')
})
it('keeps a continuing mark and drops the transient one', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }, { type: 'italic' }] },
{ type: 'text', text: 'b', marks: [{ type: 'bold' }] },
]),
)
expect(markdown).toBe('a **b**')
})
it('keeps a continuing italic when bold is the transient mark', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }, { type: 'italic' }] },
{ type: 'text', text: 'b', marks: [{ type: 'italic' }] },
]),
)
expect(markdown).toBe('a *b*')
})
it('spans a mark across the whitespace when it opened on an earlier node', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a', marks: [{ type: 'bold' }] },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }, { type: 'italic' }] },
{ type: 'text', text: 'b', marks: [{ type: 'bold' }] },
]),
)
expect(markdown).toBe('**a b**')
})
it('closes an already-open mark before a whitespace-only node that ends it', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a', marks: [{ type: 'bold' }] },
{ type: 'text', text: ' ', marks: [{ type: 'bold' }] },
{ type: 'text', text: 'b' },
]),
)
expect(markdown).toBe('**a** b')
})
it('keeps expelling trailing whitespace out of a mark that has text', () => {
const markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'Label: ', marks: [{ type: 'bold' }] },
{ type: 'text', text: 'value' },
]),
)
expect(markdown).toBe('**Label:** value')
})
it.each([
['a single mark', [{ type: 'bold' }]],
['nested marks', [{ type: 'bold' }, { type: 'italic' }]],
])('survives repeated serialize/parse cycles with %s', (_name, marks) => {
// Parsing `****` reads it as literal asterisks, which the next serialize escapes.
// Assert the value on every cycle, not that it settles: the broken output settles too.
let markdown = markdownManager.serialize(
paragraph([
{ type: 'text', text: 'a' },
{ type: 'text', text: ' ', marks },
{ type: 'text', text: 'b' },
]),
)
for (let cycle = 0; cycle < 3; cycle += 1) {
expect(markdown).toBe('a b')
markdown = markdownManager.serialize(markdownManager.parse(markdown))
}
expect(markdown).toBe('a b')
})
it('still emphasises a node that is only punctuation', () => {
const markdown = markdownManager.serialize(
paragraph([{ type: 'text', text: ')', marks: [{ type: 'bold' }] }]),
)
expect(markdown).toBe('**)**')
})
})