Files

127 lines
3.8 KiB
TypeScript

// Extension for parsing and highlighting YAML FrontMatter blocks at the start of a document.
//
// A FrontMatter block is delimited by --- at the very start of the document:
// ---
// title: My Document
// date: 2024-01-01
// ---
import { Tag } from '@lezer/highlight';
import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@lezer/common';
import { MarkdownConfig, BlockContext, Line, LeafBlock, MarkdownExtension } from '@lezer/markdown';
import { StreamLanguage } from '@codemirror/language';
import { yaml } from '@codemirror/legacy-modes/mode/yaml';
export const frontMatterTagName = 'FrontMatter';
export const frontMatterContentTagName = 'FrontMatterContent';
export const frontMatterMarkerTagName = 'FrontMatterMarker';
export const frontMatterTag = Tag.define();
// Create the YAML language parser using the legacy mode
const yamlLanguage = StreamLanguage.define(yaml);
// Wraps a YAML parser for the FrontMatter content.
// This replaces [nodeTag] from the syntax tree with a region handled by the YAML parser.
const wrappedYamlParser = (nodeTag: string): ParseWrapper => {
return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse => {
if (node.name !== nodeTag) {
return null;
}
return {
parser: yamlLanguage.parser,
};
});
};
// Regex to match the FrontMatter delimiter (--- at start of line)
const frontMatterDelimiterRegex = /^---\s*$/;
const frontMatterConfig: MarkdownConfig = {
defineNodes: [
{
name: frontMatterTagName,
style: frontMatterTag,
},
{
name: frontMatterContentTagName,
},
{
name: frontMatterMarkerTagName,
style: frontMatterTag,
},
],
parseBlock: [{
name: frontMatterTagName,
before: 'HorizontalRule',
parse(cx: BlockContext, line: Line): boolean {
// FrontMatter must be at the very start of the document
if (cx.lineStart !== 0) {
return false;
}
// Check if the first line is ---
if (!frontMatterDelimiterRegex.test(line.text)) {
return false;
}
// If the document starts with --- always claim it as a frontmatter block,
// even when the closing delimiter is absent.
const openingMarkerStart = cx.lineStart;
const openingMarkerEnd = cx.lineStart + line.text.length;
const contentStart = openingMarkerEnd + 1;
let foundEnd = false;
// Consume lines until we find the closing --- or reach end of document.
while (cx.nextLine()) {
if (frontMatterDelimiterRegex.test(line.text)) {
foundEnd = true;
break;
}
}
// cx.lineStart now points to the closing --- (if found) or end of document (if not).
const contentEnd = cx.lineStart;
const openingMarkerElem = cx.elt(frontMatterMarkerTagName, openingMarkerStart, openingMarkerEnd);
const contentElem = cx.elt(frontMatterContentTagName, contentStart, contentEnd);
if (foundEnd) {
const closingMarkerStart = cx.lineStart;
const closingMarkerEnd = cx.lineStart + line.text.length;
const closingMarkerElem = cx.elt(frontMatterMarkerTagName, closingMarkerStart, closingMarkerEnd);
const containerElement = cx.elt(
frontMatterTagName,
0,
closingMarkerEnd,
[openingMarkerElem, contentElem, closingMarkerElem],
);
cx.addElement(containerElement);
cx.nextLine();
} else {
const containerElement = cx.elt(
frontMatterTagName,
0,
contentEnd,
[openingMarkerElem, contentElem],
);
cx.addElement(containerElement);
}
return true;
},
// FrontMatter blocks can end leaf blocks like paragraphs
endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean {
return frontMatterDelimiterRegex.test(line.text);
},
}],
wrap: wrappedYamlParser(frontMatterContentTagName),
};
const markdownFrontMatterExtension: MarkdownExtension = [frontMatterConfig];
export default markdownFrontMatterExtension;