diff --git a/packages/editor-ext/src/lib/markdown/utils/tabs.marked.ts b/packages/editor-ext/src/lib/markdown/utils/tabs.marked.ts index 2691a27a4..ed54b78ea 100644 --- a/packages/editor-ext/src/lib/markdown/utils/tabs.marked.ts +++ b/packages/editor-ext/src/lib/markdown/utils/tabs.marked.ts @@ -13,7 +13,8 @@ interface TabbedToken { activeTabIndex: number; } -const HEADER_RE = /^===([!+])?\s*["'“”‘’]([^"'“”‘’\n]+)["'“”‘’]\s*$/gm; +const HEADER_RE = + /^===([!+])?\s*["'“”‘’]((?:\\.|[^"'“”‘’\n])+?)["'“”‘’]\s*$/gm; export const tabsExtension = { name: 'tabbed', @@ -38,7 +39,7 @@ export const tabsExtension = { index: m.index, raw: m[0], marker: m[1] ?? '', - label: m[2].trim(), + label: unescapeTabLabel(m[2].trim()), }); } @@ -87,7 +88,7 @@ export const tabsExtension = { if (body.length > 0) { body = body .split('\n') - .map((line) => line.replace(/^\s{2,4}/, '')) + .map((line) => line.replace(/^(?:\t| {2,4})/, '')) .join('\n'); } @@ -137,6 +138,10 @@ export const tabsExtension = { }, }; +function unescapeTabLabel(value: string): string { + return value.replace(/\\(["\\])/g, '$1'); +} + function escapeHtml(value: string): string { return value .replace(/&/g, '&') diff --git a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts index d48a560da..d6e5fbce4 100644 --- a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts +++ b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts @@ -69,7 +69,7 @@ function tabs(turndownService: _TurndownService) { const isNestedTabsNode = node.parentElement?.closest('div[data-type="tabs"]') !== null; - + const tabBlocks = tabNodes.map((tabNode, index) => { const labelNode = tabNode.querySelector( ':scope > div[data-type="tabLabel"]', @@ -279,15 +279,16 @@ function video(turndownService: _TurndownService) { function sanitizeTabLabel(value: string): string { return value .replace(/[\r\n]+/g, ' ') + .replace(/\\/g, '\\\\') .replace(/"/g, '\\"') .trim(); } function indentMarkdownBlock(content: string): string { - if (!content) return ' '; + if (!content) return '\t'; return content .split('\n') - .map((line) => (line.trim() ? ` ${line}` : '')) + .map((line) => (line.trim() ? `\t${line}` : '')) .join('\n'); } diff --git a/packages/editor-ext/src/lib/tabs/tabs.ts b/packages/editor-ext/src/lib/tabs/tabs.ts index 6c8c3c9fa..bff636ac6 100644 --- a/packages/editor-ext/src/lib/tabs/tabs.ts +++ b/packages/editor-ext/src/lib/tabs/tabs.ts @@ -15,7 +15,7 @@ export interface TabsOptions { view: ComponentType> | null; } -const TAB_INPUT_REGEX = /^\s*===\s*["'“”‘’]([^"'“”‘’\n]+)["'“”‘’]\s+$/; +const TAB_INPUT_REGEX = /^\s*===\s*["'“”‘’]((?:\\.|[^"'“”‘’\n])+?)["'“”‘’]\s+$/; declare module '@tiptap/core' { interface Commands { @@ -85,9 +85,8 @@ export const Tabs = Node.create({ new InputRule({ find: TAB_INPUT_REGEX, handler: ({ range, match }) => { - const label = ( - typeof match[1] === 'string' ? match[1] : 'Tab 1' - ).trim(); + const rawLabel = typeof match[1] === 'string' ? match[1] : 'Tab 1'; + const label = rawLabel.replace(/\\(["\\])/g, '$1').trim(); this.editor.commands.insertTabs(label, range); }, @@ -222,7 +221,10 @@ export const Tabs = Node.create({ (pos: 'left' | 'right') => ({ state, tr, dispatch }) => { const { $from } = state.selection; - const tabs = findParentNode((node) => node.type.name === this.name, $from); + const tabs = findParentNode( + (node) => node.type.name === this.name, + $from, + ); if (!tabs || tabs.node.childCount <= 0) return false; const currentTabIndex = clampIndex( @@ -265,7 +267,10 @@ export const Tabs = Node.create({ (pos: 'left' | 'right') => ({ state, tr, dispatch }) => { const { $from } = state.selection; - const tabs = findParentNode((node) => node.type.name === this.name, $from); + const tabs = findParentNode( + (node) => node.type.name === this.name, + $from, + ); if (!tabs || tabs.node.childCount <= 1) return false; const currentTabIndex = clampIndex( @@ -356,7 +361,10 @@ export const Tabs = Node.create({ () => ({ state, tr, dispatch }) => { const { $from } = state.selection; - const tabs = findParentNode((node) => node.type.name === this.name, $from); + const tabs = findParentNode( + (node) => node.type.name === this.name, + $from, + ); if (!tabs) return false; if (tabs.node.childCount < 2) { @@ -398,8 +406,11 @@ export const Tabs = Node.create({ () => ({ state, tr, dispatch }) => { const { $from } = state.selection; - const tabs = findParentNode((node) => node.type.name === this.name, $from); - if (!tabs || tabs.node.childCount < 0) return false; + const tabs = findParentNode( + (node) => node.type.name === this.name, + $from, + ); + if (tabs?.node.childCount <= 0) return false; tr.delete(tabs.pos, tabs.pos + tabs.node.nodeSize); @@ -417,12 +428,12 @@ export const Tabs = Node.create({ if (!empty) return false; if ($from.parent.content.size > 0) return false; - + const tabsNode = findParentNode( (node) => node.type.name === this.name, $from, ); - + if (!tabsNode) return false; return editor .chain()