feat(editor): use @codemirror/lang-yaml for Quarto YAML frontmatter
Build and Deploy Verso / deploy (push) Successful in 12m20s
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:
@@ -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 }),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
-120
@@ -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 }
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user