一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
145 行
4.4 KiB
TypeScript
145 行
4.4 KiB
TypeScript
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 <pre class="hljs"> without a <code> 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 `<div class="joplin-editable joplin-frontmatter"><pre class="joplin-source" hidden data-joplin-language="frontmatter" data-joplin-source-open="--- " data-joplin-source-close=" --- ">${contentHtml}</pre><div class="joplin-rendered joplin-frontmatter-rendered"><hr class="joplin-frontmatter-marker"/><pre class="hljs">${highlightedContent}</pre><hr class="joplin-frontmatter-marker"/></div></div>`;
|
|
};
|
|
};
|
|
|
|
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,
|
|
};
|