feat(editor): use @codemirror/lang-yaml for Quarto YAML frontmatter
Build and Deploy Verso / deploy (push) Successful in 12m20s

Replace the custom regex-based ViewPlugin with the official
@codemirror/lang-yaml package. yamlFrontmatter({ content: mdLS })
wraps the Markdown language with a mixed parser: the leading ---/---
block is handed to the full Lezer YAML parser (proper key/value/scalar/
anchor/alias highlighting), while the document body continues to use
the Markdown parser. The manual Frontmatter extension import is also
removed since yamlFrontmatter handles frontmatter recognition itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-07 18:30:35 +00:00
parent f9788a1c69
commit 2c0f387cef
3 changed files with 14 additions and 130 deletions
@@ -1,7 +1,8 @@
import { markdown as markdownLanguage } from '@codemirror/lang-markdown'
import { yamlFrontmatter } from '@codemirror/lang-yaml'
import { shortcuts } from './shortcuts'
import { languages } from '../index'
import { Strikethrough, Frontmatter } from '@lezer/markdown'
import { Strikethrough } from '@lezer/markdown'
import {
HighlightStyle,
LanguageSupport,
@@ -10,23 +11,25 @@ import {
import { tags } from '@lezer/highlight'
import { markdownDocumentOutline } from './document-outline'
import { quartoCompletions } from './complete'
import { yamlFrontmatterHighlighting } from './yaml-frontmatter'
export const markdown = () => {
const { language, support } = markdownLanguage({
const mdLS = markdownLanguage({
codeLanguages: languages,
// Frontmatter teaches the Markdown grammar to recognise the leading ---/---
// block as a YAML header rather than mis-parsing it as Setext headings.
extensions: [Strikethrough, Frontmatter],
extensions: [Strikethrough],
})
return new LanguageSupport(language, [
support,
// yamlFrontmatter wraps the Markdown language so that the leading ---/---
// block is parsed as real YAML (with full Lezer YAML highlighting) and the
// rest of the document is still parsed as Markdown. It replaces our
// previous regex-based ViewPlugin and the manual Frontmatter extension.
const quartoLS = yamlFrontmatter({ content: mdLS })
return new LanguageSupport(quartoLS.language, [
quartoLS.support,
shortcuts(),
syntaxHighlighting(markdownHighlightStyle),
markdownDocumentOutline,
language.data.of({ autocomplete: quartoCompletions }),
yamlFrontmatterHighlighting,
mdLS.language.data.of({ autocomplete: quartoCompletions }),
])
}
@@ -1,120 +0,0 @@
import { RangeSetBuilder } from '@codemirror/state'
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
} from '@codemirror/view'
// Matches YAML boolean/null literals
const YAML_BOOL_RE = /^(true|false|yes|no|on|off|null|~)$/i
// Matches plain integers and floats
const YAML_NUMBER_RE = /^-?(?:0x[\da-fA-F]+|0o[0-7]+|\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/
function valueClass(value: string): string {
if (value.startsWith('"') || value.startsWith("'")) return 'tok-string'
if (YAML_BOOL_RE.test(value)) return 'tok-keyword'
if (YAML_NUMBER_RE.test(value)) return 'tok-number'
return 'tok-attributeValue'
}
// Find the first ':' that acts as a YAML key-value separator — it must be
// followed by a space, tab, or end-of-string (not '/' as in URLs).
function findKeyColon(text: string): number {
let inSingle = false
let inDouble = false
for (let i = 0; i < text.length; i++) {
const c = text[i]
if (c === "'" && !inDouble) { inSingle = !inSingle; continue }
if (c === '"' && !inSingle) { inDouble = !inDouble; continue }
if (inSingle || inDouble) continue
if (c === ':') {
const next = text[i + 1]
if (next === undefined || next === ' ' || next === '\t') return i
}
}
return -1
}
function buildDecorations(view: EditorView): DecorationSet {
const doc = view.state.doc
const builder = new RangeSetBuilder<Decoration>()
if (doc.lines < 2) return builder.finish()
// Only activate when the document starts with a YAML frontmatter block
if (doc.line(1).text.trim() !== '---') return builder.finish()
let fmEndLine = -1
for (let n = 2; n <= doc.lines; n++) {
const lineText = doc.line(n).text.trim()
if (lineText === '---' || lineText === '...') {
fmEndLine = n
break
}
}
if (fmEndLine < 0) return builder.finish()
// Color the opening '---' delimiter
const openLine = doc.line(1)
builder.add(openLine.from, openLine.to, Decoration.mark({ class: 'tok-comment' }))
// Decorate content lines 2..(fmEndLine - 1)
for (let n = 2; n < fmEndLine; n++) {
const line = doc.line(n)
const text = line.text
const trimmed = text.trimStart()
if (!trimmed) continue
const indent = text.length - trimmed.length
const base = line.from + indent
// YAML comment
if (trimmed[0] === '#') {
builder.add(base, line.to, Decoration.mark({ class: 'tok-comment' }))
continue
}
// Key: value — use tok-typeName for keys (35/41 theme coverage, distinct
// colour in most themes) and type-appropriate classes for values.
const colonPos = findKeyColon(trimmed)
if (colonPos <= 0) continue
const key = trimmed.slice(0, colonPos).trimEnd()
if (key) {
builder.add(base, base + key.length, Decoration.mark({ class: 'tok-typeName' }))
}
const afterColon = trimmed.slice(colonPos + 1)
const leadingSpace = afterColon.length - afterColon.trimStart().length
const value = afterColon.trimStart().trimEnd()
if (value) {
const valFrom = base + colonPos + 1 + leadingSpace
builder.add(valFrom, valFrom + value.length, Decoration.mark({ class: valueClass(value) }))
}
}
// Color the closing '---' / '...' delimiter
const closeLine = doc.line(fmEndLine)
builder.add(closeLine.from, closeLine.to, Decoration.mark({ class: 'tok-comment' }))
return builder.finish()
}
// Highlights YAML key/value/comment lines inside a Quarto/Pandoc frontmatter
// block (the region delimited by leading and trailing '---' lines).
export const yamlFrontmatterHighlighting = ViewPlugin.fromClass(
class {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = buildDecorations(view)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = buildDecorations(update.view)
}
}
},
{ decorations: v => v.decorations }
)
+1
View File
@@ -206,6 +206,7 @@
"@codemirror/commands": "6.10.1",
"@codemirror/lang-markdown": "6.5.0",
"@codemirror/lang-python": "6.2.1",
"@codemirror/lang-yaml": "6.1.3",
"@codemirror/language": "6.12.1",
"@codemirror/lint": "6.9.2",
"@codemirror/search": "6.5.8",