diff --git a/src/renderer/src/components/RichEditor/extensions/yaml-front-matter.ts b/src/renderer/src/components/RichEditor/extensions/yaml-front-matter.ts index afbff76898..f0162517a4 100644 --- a/src/renderer/src/components/RichEditor/extensions/yaml-front-matter.ts +++ b/src/renderer/src/components/RichEditor/extensions/yaml-front-matter.ts @@ -1,11 +1,8 @@ -import { loggerService } from '@logger' import { mergeAttributes, Node } from '@tiptap/core' import { ReactNodeViewRenderer } from '@tiptap/react' import YamlFrontMatterNodeView from '../components/YamlFrontMatterNodeView' -const logger = loggerService.withContext('YamlFrontMatterExtension') - declare module '@tiptap/core' { interface Commands { yamlFrontMatter: { @@ -24,30 +21,17 @@ export const YamlFrontMatter = Node.create({ markdownTokenizer: { name: 'yamlFrontMatter', level: 'block', - // Optimization: check if content starts with --- - start(src: string) { - logger.info('🔍 Tokenizer start() called', { - srcLength: src.length, - srcPrefix: src.substring(0, 60).replace(/\n/g, '\\n'), - startsWithDashes: src.startsWith('---\n') - }) - const result = src.match(/^---\n/) ? 0 : -1 - logger.info('✅ Tokenizer start() result:', { result }) + start(src: string) { + const result = src.match(/^\s*---\n/) ? 0 : -1 return result }, // Parse YAML front matter tokenize(src: string) { - logger.info('🔍 Tokenizer tokenize() called', { - srcLength: src.length, - srcPrefix: src.substring(0, 120).replace(/\n/g, '\\n') - }) - // Match: ---\n...yaml content...\n--- const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(src) if (!match) { - logger.warn('❌ Tokenizer tokenize() - NO MATCH FOUND') return undefined } @@ -56,74 +40,37 @@ export const YamlFrontMatter = Node.create({ raw: match[0], text: match[1] // YAML content without delimiters } - - logger.info('✅ Tokenizer tokenize() - MATCH FOUND', { - rawLength: token.raw.length, - textLength: token.text.length, - textPreview: token.text.substring(0, 100).replace(/\n/g, '\\n') - }) - return token } }, // Parse markdown token to Tiptap JSON parseMarkdown(token, helpers) { - logger.info('🔍 parseMarkdown() called', { - tokenType: token.type, - hasText: !!token.text, - textLength: token.text?.length || 0, - textPreview: token.text?.substring(0, 100).replace(/\n/g, '\\n'), - hasTokens: !!token.tokens, - tokensLength: token.tokens?.length || 0 - }) - - // Since this is an atom node, we don't need child content - const result = { - type: 'yamlFrontMatter', // Use explicit node name instead of this.name - attrs: { - content: token.text || '' - } + const attrs = { + content: token.text || '' } - logger.info('✅ parseMarkdown() result', { - type: result.type, - contentLength: result.attrs.content.length - }) - - return result + return helpers.createNode('yamlFrontMatter', attrs) }, // Serialize Tiptap node to markdown renderMarkdown(node) { - logger.info('🔍 renderMarkdown() called', { - nodeType: node.type, - hasContent: !!node.attrs?.content, - contentLength: node.attrs?.content?.length || 0, - contentPreview: node.attrs?.content?.substring(0, 100).replace(/\n/g, '\\n') - }) - const content = node.attrs?.content || '' if (!content.trim()) { - logger.info('⚠️ renderMarkdown() - empty content, returning empty string') return '' } - const trimmedContent = content.trim() let result = '' - // Ensure proper format with closing --- - if (trimmedContent.endsWith('---')) { - result = trimmedContent + '\n\n' + // Ensure proper format with opening and closing --- + // The content is stored without the --- delimiters, so we need to add them back + if (content.endsWith('---')) { + // Content already has closing ---, just add opening + result = '---\n' + content + '\n\n' } else { - result = trimmedContent + '\n---\n\n' + // Add both opening and closing --- + result = '---\n' + content + '\n---\n\n' } - - logger.info('✅ renderMarkdown() result', { - resultLength: result.length, - resultPreview: result.substring(0, 120).replace(/\n/g, '\\n') - }) - return result }, diff --git a/src/renderer/src/utils/markdownConverter.ts b/src/renderer/src/utils/markdownConverter.ts index 3ccfb1fb74..ab26d82f28 100644 --- a/src/renderer/src/utils/markdownConverter.ts +++ b/src/renderer/src/utils/markdownConverter.ts @@ -1,6 +1,5 @@ import { loggerService } from '@logger' import { MARKDOWN_SOURCE_LINE_ATTR } from '@renderer/components/RichEditor/constants' -import he from 'he' import MarkdownIt from 'markdown-it' import TurndownService from 'turndown' @@ -58,141 +57,6 @@ defaultBlockRules.forEach((ruleName) => { } }) -// Override the code_block and code_inline renderers to properly escape HTML entities -md.renderer.rules.code_block = function (tokens, idx) { - const token = tokens[idx] - const langName = token.info ? ` class="language-${token.info.trim()}"` : '' - const escapedContent = he.encode(token.content, { useNamedReferences: false }) - let html = `
${escapedContent}
` - html = injectLineNumber(token, html) - return html -} - -md.renderer.rules.code_inline = function (tokens, idx) { - const token = tokens[idx] - const escapedContent = he.encode(token.content, { useNamedReferences: false }) - return `${escapedContent}` -} - -md.renderer.rules.fence = function (tokens, idx) { - const token = tokens[idx] - const langName = token.info ? ` class="language-${token.info.trim()}"` : '' - const escapedContent = he.encode(token.content, { useNamedReferences: false }) - let html = `
${escapedContent}
` - html = injectLineNumber(token, html) - return html -} - -interface TokenLike { - content: string - block?: boolean - map?: [number, number] -} - -interface BlockStateLike { - src: string - bMarks: number[] - eMarks: number[] - tShift: number[] - line: number - parentType: string - blkIndent: number - push: (type: string, tag: string, nesting: number) => TokenLike -} - -interface InlineStateLike { - src: string - pos: number - posMax: number - push: (type: string, tag: string, nesting: number) => TokenLike & { content?: string } -} - -function yamlFrontMatterPlugin(md: MarkdownIt) { - // Parser: recognize YAML front matter - md.block.ruler.before( - 'table', - 'yaml_front_matter', - (stateLike: unknown, startLine: number, endLine: number, silent: boolean): boolean => { - const state = stateLike as BlockStateLike - - // Only check at the very beginning of the document - if (startLine !== 0) { - return false - } - - const startPos = state.bMarks[startLine] + state.tShift[startLine] - const maxPos = state.eMarks[startLine] - - // Must begin with --- at document start - if (startPos + 3 > maxPos) return false - if ( - state.src.charCodeAt(startPos) !== 0x2d /* - */ || - state.src.charCodeAt(startPos + 1) !== 0x2d /* - */ || - state.src.charCodeAt(startPos + 2) !== 0x2d /* - */ - ) { - return false - } - - // If requested only to validate existence - if (silent) return true - - // Search for closing --- - let nextLine = startLine + 1 - let found = false - - for (nextLine = startLine + 1; nextLine < endLine; nextLine++) { - const lineStart = state.bMarks[nextLine] + state.tShift[nextLine] - const lineEnd = state.eMarks[nextLine] - const line = state.src.slice(lineStart, lineEnd).trim() - - if (line === '---') { - found = true - break - } - } - - if (!found) { - return false - } - - // Extract YAML content between the --- delimiters, preserving original indentation - const yamlLines: string[] = [] - for (let lineIdx = startLine + 1; lineIdx < nextLine; lineIdx++) { - // Use the original line markers without shift to preserve indentation - const lineStart = state.bMarks[lineIdx] - const lineEnd = state.eMarks[lineIdx] - yamlLines.push(state.src.slice(lineStart, lineEnd)) - } - - // Also capture the closing --- line with its indentation - const closingLineStart = state.bMarks[nextLine] - const closingLineEnd = state.eMarks[nextLine] - const closingLine = state.src.slice(closingLineStart, closingLineEnd) - - const yamlContent = yamlLines.join('\n') + '\n' + closingLine - - const token = state.push('yaml_front_matter', 'div', 0) - token.block = true - token.map = [startLine, nextLine + 1] - token.content = yamlContent - - state.line = nextLine + 1 - return true - } - ) - - // Renderer: output YAML front matter as special HTML element - md.renderer.rules.yaml_front_matter = (tokens: Array<{ content?: string }>, idx: number): string => { - const token = tokens[idx] - const content = token?.content ?? '' - let html = `
${content}
` - html = injectLineNumber(token, html) - return html - } -} - -md.use(yamlFrontMatterPlugin) - // Initialize turndown service const turndownService = new TurndownService({ headingStyle: 'atx', // Use # for headings @@ -204,28 +68,6 @@ const turndownService = new TurndownService({ strongDelimiter: '**' // Use ** for strong }) -// Custom rule to preserve YAML front matter -turndownService.addRule('yamlFrontMatter', { - filter: (node: Element) => { - return node.nodeName === 'DIV' && node.getAttribute?.('data-type') === 'yaml-front-matter' - }, - replacement: (_content: string, node: Node) => { - const element = node as Element - const yamlContent = element.getAttribute?.('data-content') || '' - const decodedContent = he.decode(yamlContent, { - isAttributeValue: false, - strict: false - }) - // The decodedContent already includes the complete YAML with closing --- - // We just need to add the opening --- if it's not there - if (decodedContent.startsWith('---')) { - return decodedContent - } else { - return `---\n${decodedContent}` - } - } -}) - /** * Gets plain text preview from Markdown content * @param markdown - Markdown string