import TurndownService from 'turndown'; import { gfm } from 'turndown-plugin-gfm'; import { markdownToHtml } from './MarkdownView'; // Matched to the dialect the generator writes, so a save is not a rewrite. const turndown = new TurndownService({ headingStyle: 'atx', hr: '---', bulletListMarker: '-', codeBlockStyle: 'fenced', fence: '```', emDelimiter: '*', strongDelimiter: '**', linkStyle: 'inlined', }); // Tables, strikethrough and task lists. turndown.use(gfm); // Turndown pads list markers to four columns ("- item"); the generator // writes "- item", so the default would rewrite every bullet on first save. turndown.addRule('tightListItem', { filter: 'li', replacement: (content, node, options) => { const body = content .replace(/^\n+/, '') .replace(/\n+$/, '\n') // Continuation lines line up under the text, not under the marker. .replace(/\n/g, '\n '); const parent = node.parentNode as HTMLElement | null; let prefix = `${options.bulletListMarker} `; if (parent && parent.nodeName === 'OL') { const start = Number(parent.getAttribute('start') ?? 1); const index = Array.prototype.indexOf.call(parent.children, node); prefix = `${(Number.isFinite(start) ? start : 1) + index}. `; } const trailing = node.nextSibling && !/\n$/.test(body) ? '\n' : ''; return prefix + body + trailing; }, }); // Tiptap emits `
  • `; turndown's GFM rule only knows the // `` shape, so without this a checklist loses its ticks. turndown.addRule('tiptapTaskItem', { filter: node => node.nodeName === 'LI' && node.getAttribute('data-type') === 'taskItem', replacement: (_content, node) => { const element = node as HTMLElement; const checked = element.getAttribute('data-checked') === 'true'; const text = (element.textContent ?? '').trim(); return `- [${checked ? 'x' : ' '}] ${text}\n`; }, }); export const markdownToEditorHtml = (markdown: string): string => { if (!markdown || typeof markdown !== 'string') return ''; return markdownToHtml(markdown); }; export const htmlToMarkdown = (html: string): string => { if (!html || typeof html !== 'string') return ''; return ( turndown .turndown(html) // An empty paragraph can leave three or more newlines behind. .replace(/\n{3,}/g, '\n\n') .trim() ); };