import type MarkdownIt from 'markdown-it'; import type * as StateBlock from 'markdown-it/lib/rules_block/state_block'; import hljs from '../../highlight'; // Regex to match the FrontMatter delimiter (--- at start of line, optionally with trailing whitespace) const frontMatterDelimiterRegex = /^---\s*$/; const plugin = (markdownIt: MarkdownIt, _ruleOptions: unknown) => { // Add a block rule to parse frontmatter at the beginning of the document markdownIt.block.ruler.before('fence', 'frontmatter', (state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean => { // FrontMatter must be at the very start of the document if (startLine !== 0) { return false; } const startPos = state.bMarks[startLine] + state.tShift[startLine]; const startMax = state.eMarks[startLine]; const startLineText = state.src.slice(startPos, startMax); // Check if the first line is --- if (!frontMatterDelimiterRegex.test(startLineText)) { return false; } // Find the closing --- let nextLine = startLine + 1; let foundEnd = false; while (nextLine < endLine) { const pos = state.bMarks[nextLine] + state.tShift[nextLine]; const max = state.eMarks[nextLine]; const lineText = state.src.slice(pos, max); if (frontMatterDelimiterRegex.test(lineText)) { foundEnd = true; break; } nextLine++; } if (!foundEnd) { // No closing delimiter - not a valid FrontMatter block return false; } // If we're just checking if the rule matches (silent mode), return true if (silent) { return true; } // Extract the content between the delimiters const contentStartLine = startLine + 1; const contentEndLine = nextLine; let content = ''; for (let line = contentStartLine; line < contentEndLine; line++) { const pos = state.bMarks[line]; const max = state.eMarks[line]; content += `${state.src.slice(pos, max)}\n`; } // Remove trailing newline content = content.slice(0, -1); // Create the token const token = state.push('frontmatter', 'div', 0); token.content = content; token.map = [startLine, nextLine + 1]; token.markup = '---'; // Move past the closing delimiter state.line = nextLine + 1; return true; }); // Add a renderer for the frontmatter token markdownIt.renderer.rules.frontmatter = (tokens, idx) => { const token = tokens[idx]; const content = token.content; const escapeHtml = markdownIt.utils.escapeHtml; // Escape the content for HTML const contentHtml = escapeHtml(content); // Apply YAML syntax highlighting let highlightedContent: string; try { if (hljs.getLanguage('yaml')) { highlightedContent = hljs.highlight(content, { language: 'yaml', ignoreIllegals: true }).value; } else { highlightedContent = contentHtml; } } catch (_error) { highlightedContent = contentHtml; } // Return the joplin-editable block structure // The source block contains just the YAML content (without delimiters) // The data-joplin-source-open and data-joplin-source-close attributes define // what gets prepended/appended when converting back to markdown // is the HTML entity for newline // Note: We use
 without a  child to avoid the
		// isCodeBlock() detection in turndown which would convert it to a fenced code block
		// IMPORTANT: No whitespace between joplin-editable and joplin-source elements!
		// The turndown joplinEditableBlockInfo function iterates childNodes and crashes
		// on text nodes (whitespace) because they don't have classList.
		return `

${highlightedContent}

`; }; }; const assets = () => { return [ { inline: true, mime: 'text/css', text: ` .joplin-frontmatter-rendered { margin-bottom: 1em; } .joplin-frontmatter-rendered pre.hljs { margin: 0; border-radius: 0; white-space: pre-wrap; word-break: break-word; } .joplin-frontmatter-marker { margin: 0; border: none; border-top: 2px solid var(--joplin-divider-color, #ccc); } `, }, ].map(e => { return { source: 'frontmatter', ...e, }; }); }; export default { plugin, assets, };