ts-rich-editor (TipTap) shows the formatting while writing: bold, italic, strike, code, headings, lists, task lists, quotes, code blocks and links, with @ suggesting the project's members and pasted screenshots uploaded as attachments. It reads and writes the same Markdown the API keeps (mentions as @[Name](id), images as attachment:<id>, tables too), so nothing already written changes. Used for comments, replies, editing a comment or a description, a new ticket from the board and the public proposal form; the Markdown hint and the paste directive are gone. List markers sat on a line of their own, and task lists showed a stray bullet, in the editor and when read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
2.3 KiB
TypeScript
89 lines
2.3 KiB
TypeScript
import { Editor } from '@tiptap/core';
|
|
import { markdownOf, richTextExtensions } from './rich-text';
|
|
|
|
/** Markdown in, the editor's Markdown out: what an edit that changes nothing saves. */
|
|
function roundTrip(markdown: string): string {
|
|
const editor = new Editor({
|
|
element: document.createElement('div'),
|
|
extensions: richTextExtensions(),
|
|
content: markdown,
|
|
contentType: 'markdown',
|
|
});
|
|
const out = markdownOf(editor);
|
|
editor.destroy();
|
|
return out;
|
|
}
|
|
|
|
describe('rich text', () => {
|
|
it('keeps mentions as the API reads them', () => {
|
|
expect(roundTrip('Hola @[Ana López](101), ¿lo miras?')).toBe(
|
|
'Hola @[Ana López](101), ¿lo miras?',
|
|
);
|
|
});
|
|
|
|
it('keeps pasted images pointing at their attachment', () => {
|
|
expect(roundTrip('Mira:\n\n')).toContain(
|
|
'',
|
|
);
|
|
});
|
|
|
|
it('keeps formatting, lists, task lists, quotes, code and links', () => {
|
|
const text = [
|
|
'## Pasos',
|
|
'',
|
|
'Texto con **negrita**, *cursiva*, ~~tachado~~ y `código`.',
|
|
'',
|
|
'- uno',
|
|
'- dos',
|
|
'',
|
|
'1. primero',
|
|
'2. segundo',
|
|
'',
|
|
'- [ ] pendiente',
|
|
'- [x] hecho',
|
|
'',
|
|
'> una cita',
|
|
'',
|
|
'```',
|
|
'const a = 1;',
|
|
'```',
|
|
'',
|
|
'[enlace](https://example.com)',
|
|
].join('\n');
|
|
const out = roundTrip(text);
|
|
for (const piece of [
|
|
'## Pasos',
|
|
'**negrita**',
|
|
'*cursiva*',
|
|
'~~tachado~~',
|
|
'`código`',
|
|
'- uno',
|
|
'1. primero',
|
|
'- [ ] pendiente',
|
|
'- [x] hecho',
|
|
'> una cita',
|
|
'const a = 1;',
|
|
'[enlace](https://example.com)',
|
|
])
|
|
expect(out).toContain(piece);
|
|
});
|
|
|
|
it('keeps tables', () => {
|
|
const out = roundTrip('| a | b |\n| --- | --- |\n| 1 | 2 |');
|
|
expect(out).toMatch(/\|\s*a\s*\|\s*b\s*\|/);
|
|
expect(out).toMatch(/\|\s*1\s*\|\s*2\s*\|/);
|
|
});
|
|
|
|
it('answers an empty string for an empty editor', () => {
|
|
expect(roundTrip('')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('rich text, line breaks', () => {
|
|
it('keeps a single line break as a line break', () => {
|
|
const out = roundTrip('primera línea\nsegunda línea');
|
|
expect(out).not.toBe('primera línea segunda línea');
|
|
expect(out).toMatch(/primera línea(\s{2}|\\)?\nsegunda línea/);
|
|
});
|
|
});
|