Skip to content

Instantly share code, notes, and snippets.

@rpaul-stripe
Created September 18, 2023 21:15
Show Gist options
  • Save rpaul-stripe/986ab19947a9e53236dffcc55694663e to your computer and use it in GitHub Desktop.
Save rpaul-stripe/986ab19947a9e53236dffcc55694663e to your computer and use it in GitHub Desktop.
Monaco editor for Markdoc
import { editor, languages, MarkerSeverity, IRange } from "monaco-editor";
import * as Markdoc from "@markdoc/markdoc";
import language from "./language";
// Markdoc config
const config = {};
const model = editor.createModel("", "markdoc");
window.MonacoEnvironment = {
getWorkerUrl: () => "./vs/editor/editor.worker.js"
};
languages.register({
id: "markdoc",
extensions: [".md"],
aliases: ["markdown"],
mimetypes: ["text/markdown"],
});
function getContentRangeInLine(line: number, text: string): IRange {
const lineContent = model.getLineContent(line);
const startColumn = lineContent.indexOf(text) + 1;
const endColumn = startColumn + text.length;
const range = {
startLineNumber: line,
endLineNumber: line,
startColumn,
endColumn,
};
return model.validateRange(range);
}
function severity(level: string): MarkerSeverity {
switch (level) {
case "debug":
case "info":
return MarkerSeverity.Info;
case "warning":
return MarkerSeverity.Warning;
default:
return MarkerSeverity.Error;
}
}
function errorMarker(error: Markdoc.ValidateError): editor.IMarkerData {
const {
lines: [startLine, endLineNumber],
error: {id: code, level, message},
} = error;
return {
code, message,
source: 'markdoc',
severity: severity(level),
startLineNumber: startLine + 1,
startColumn: 0,
endLineNumber,
endColumn: model.getLineMaxColumn(endLineNumber),
}
}
let ast: Markdoc.Node | null = null;
languages.setMonarchTokensProvider("markdoc", language);
languages.registerFoldingRangeProvider("markdoc", {
provideFoldingRanges(model, context, token) {
const ranges: languages.FoldingRange[] = [];
if (!ast) return ranges;
for (const {type, lines} of ast.walk())
if (type.startsWith('tag') && lines.length === 4)
ranges.push({start: lines[0] + 1, end: lines[2] + 1});
return ranges;
},
}),
languages.registerLinkProvider("markdoc", {
provideLinks(model, token) {
const links: languages.ILink[] = [];
if (!ast) return {links};
for (const node of ast.walk()) {
if (node.type === 'tag' && node.tag === 'partial') {
const {file} = node.attributes;
if (typeof file !== 'string' || typeof node.lines[0] !== 'number') {
continue;
}
links.push({
url: ``, // Logic for generating editor links here
tooltip: 'Edit partial in new tab',
range: getContentRangeInLine(node.lines[1], file),
});
}
}
return {links};
},
});
model.onDidChangeContent(ev => {
const text = model.getValue();
ast = Markdoc.parse(text);
const errors = Markdoc.validate(ast, config);
const markers = errors.map(errorMarker);
editor.setModelMarkers(model, "markdoc", markers);
});
editor.setTheme("vs-dark");
editor.create(document.getElementById("editor"), {model});
// Forked from Monaco's official Markdown highlighting rules
import type {languages} from 'monaco-editor';
const language: languages.IMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.md',
// escape codes
control: /[\\`*_\[\]{}()#+\-\.!]/,
noncontrol: /[^\\`*_\[\]{}()#+\-\.!]/,
escapes: /\\(?:@control)/,
// escape codes for javascript/CSS strings
jsescapes: /\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,
tokenizer: {
root: [
// headers (with #)
[
/^(\s*)(#+)((?:[^\\#]|@escapes)+)(.*$)/,
['white', 'keyword', 'keyword', 'keyword'],
],
// headers (with =)
[/^\s*(=+|\-+)\s*$/, 'keyword'],
// headers (with ***)
[/^\s*((\*[ ]?)+)\s*$/, 'meta.separator'],
// quote
[/^\s*>+/, 'comment'],
// list (starting with * or number)
[/^\s*([\*\-+:]|\d+\.)\s/, 'keyword'],
// code block (3 tilde)
[
/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,
{token: 'string', next: '@codeblock'},
],
// github style code blocks (with backticks and language)
[
/^\s*```\s*((?:\w|[\/\-#])+)\s*.*$/,
{token: 'string', next: '@codeblockgh', nextEmbedded: '$1'},
],
// github style code blocks (with backticks but no language)
[/^\s*```\s*$/, {token: 'string', next: '@codeblock'}],
// markup within lines
{include: '@linecontent'},
],
codeblock: [
[/^\s*~~~\s*$/, {token: 'string', next: '@pop'}],
[/^\s*```\s*$/, {token: 'string', next: '@pop'}],
[/.*$/, 'variable.source'],
],
// github style code blocks
codeblockgh: [
[
/```\s*$/,
{token: 'variable.source', next: '@pop', nextEmbedded: '@pop'},
],
[/[^`]+/, 'variable.source'],
],
linecontent: [
// escapes
[/&\w+;/, 'string.escape'],
[/@escapes/, 'escape'],
// various markup
[/\b__([^\\_]|@escapes|_(?!_))+__\b/, 'strong'],
[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/, 'strong'],
[/\b_[^_]+_\b/, 'emphasis'],
[/\*([^\\*]|@escapes)+\*/, 'emphasis'],
[/`([^\\`]|@escapes)+`/, 'variable'],
// links
[
/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,
['string.link', '', 'string.link'],
],
[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/, 'string.link'],
// Markdoc tags
[
/({%\s*\/?)([a-zA-Z0-9_-]+)/,
[{token: 'tag'}, {token: 'type', next: '@tag'}],
],
[/{%\s*/, {token: 'tag', next: '@tag'}],
],
tag: [
[/[ \t\r\n]+/, 'white'],
[/(\$|@)[a-zA-Z0-9_-]+/, 'variable.name'],
[/(\.|#)[a-zA-Z0-9_-]+/, 'attribute.value'],
[/(\w+)(\s*=\s*)/, ['attribute.name', 'delimiter']],
[/"[^"]+"/, 'string'],
[/[0-9]+/, 'number'],
[/true|false/, 'keyword'],
[/([a-zA-Z0-9_-]+)(\()/, ['identifier', 'delimiter']],
[/\/?%}/, 'tag', '@pop'],
],
},
};
export default language;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment