Remove "syntax" support from plugs

This commit is contained in:
Zef Hemel
2024-01-24 13:34:12 +01:00
parent aaacec6d61
commit ad4a795e7f
24 changed files with 185 additions and 315 deletions
+1 -33
View File
@@ -27,40 +27,8 @@ export type SilverBulletHooks =
& EndpointHookT
& PlugNamespaceHookT;
/** Syntax extension allow plugs to declaratively add new *inline* parse tree nodes to the markdown parser. */
export type SyntaxExtensions = {
/** A map of node **name** (also called "type"), to parsing and highlighting instructions. Each entry defines a new node. By convention node names (types) are UpperCamelCase (PascalCase).
*
* see: plug-api/lib/tree.ts#ParseTree
*/
syntax?: { [key: string]: NodeDef };
};
/** Parsing and highlighting instructions for SyntaxExtension */
export type NodeDef = {
/** An array of possible first characters to begin matching on.
*
* **Example**: If this node has the regex '[abc][123]', NodeDef.firstCharacters should be ["a", "b", "c"].
*/
firstCharacters: string[];
/** A regular expression that matches the *entire* syntax, including the first character. */
regex: string;
/** CSS styles to apply to the matched text.
*
* Key-value pair of CSS key to value:
*
* **Example**: `backgroundColor: "rgba(22,22,22,0.07)"`
*/
styles?: { [key: string]: string };
/** CSS class name to apply to the matched text */
className?: string;
};
/** A plug manifest configures hooks, declares syntax extensions, and describes plug metadata.
*
* Typically the manifest file is in a plug's root directory, named `${plugName}.plug.yaml`.
*/
export type Manifest = plugos.Manifest<SilverBulletHooks> & SyntaxExtensions;
export type Manifest = plugos.Manifest<SilverBulletHooks>;
+6
View File
@@ -17,6 +17,12 @@ export const AttributeTag = Tag.define();
export const AttributeNameTag = Tag.define();
export const AttributeValueTag = Tag.define();
export const NamedAnchorTag = Tag.define();
export const TaskTag = Tag.define();
export const TaskMarkTag = Tag.define();
export const TaskStateTag = Tag.define();
export const TaskDeadlineTag = Tag.define();
export const HashtagTag = Tag.define();
export const NakedURLTag = Tag.define();
-72
View File
@@ -1,72 +0,0 @@
import { Tag } from "../deps.ts";
import type { MarkdownConfig } from "../deps.ts";
import { System } from "../../plugos/system.ts";
import { Manifest, NodeDef } from "../manifest.ts";
export type MDExt = {
// unicode char code for efficiency .charCodeAt(0)
firstCharCodes: number[];
regex: RegExp;
nodeType: string;
tag: Tag;
styles?: { [key: string]: string };
className?: string;
};
export function mdExtensionSyntaxConfig({
regex,
firstCharCodes,
nodeType,
}: MDExt): MarkdownConfig {
return {
defineNodes: [nodeType],
parseInline: [
{
name: nodeType,
parse(cx, next, pos) {
if (!firstCharCodes.includes(next)) {
return -1;
}
const match = regex.exec(cx.slice(pos, cx.end));
if (!match) {
return -1;
}
return cx.addElement(cx.elt(nodeType, pos, pos + match[0].length));
},
// after: "Emphasis",
},
],
};
}
export function mdExtensionStyleTags({ nodeType, tag }: MDExt): {
[selector: string]: Tag | readonly Tag[];
} {
return {
[nodeType]: tag,
};
}
export function loadMarkdownExtensions(system: System<any>): MDExt[] {
const mdExtensions: MDExt[] = [];
for (const plug of system.loadedPlugs.values()) {
const manifest = plug.manifest as Manifest;
if (manifest.syntax) {
for (const [nodeType, def] of Object.entries(manifest.syntax)) {
mdExtensions.push(nodeDefToMDExt(nodeType, def));
}
}
}
return mdExtensions;
}
export function nodeDefToMDExt(nodeType: string, def: NodeDef): MDExt {
return {
nodeType,
tag: Tag.define(),
firstCharCodes: def.firstCharacters.map((ch) => ch.charCodeAt(0)),
regex: new RegExp("^" + def.regex),
styles: def.styles,
className: def.className,
};
}
+8 -14
View File
@@ -1,11 +1,11 @@
import { parse } from "./parse_tree.ts";
import buildMarkdown from "./parser.ts";
import {
collectNodesOfType,
findNodeOfType,
renderToText,
} from "../../plug-api/lib/tree.ts";
import { assertEquals, assertNotEquals } from "../../test_deps.ts";
import { extendedMarkdownLanguage } from "./parser.ts";
const sample1 = `---
type: page
@@ -26,8 +26,7 @@ name: Zef
Supper`;
Deno.test("Test parser", () => {
const lang = buildMarkdown([]);
let tree = parse(lang, sample1);
let tree = parse(extendedMarkdownLanguage, sample1);
// console.log("tree", JSON.stringify(tree, null, 2));
// Check if rendering back to text works
assertEquals(renderToText(tree), sample1);
@@ -45,7 +44,7 @@ Deno.test("Test parser", () => {
// Find frontmatter
let node = findNodeOfType(tree, "FrontMatter");
assertNotEquals(node, undefined);
tree = parse(lang, sampleInvalid1);
tree = parse(extendedMarkdownLanguage, sampleInvalid1);
node = findNodeOfType(tree, "FrontMatter");
// console.log("Invalid node", node);
assertEquals(node, undefined);
@@ -62,8 +61,7 @@ And one with nested brackets: [array: [1, 2, 3]]
`;
Deno.test("Test inline attribute syntax", () => {
const lang = buildMarkdown([]);
const tree = parse(lang, inlineAttributeSample);
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
// console.log("Attribute parsed", JSON.stringify(tree, null, 2));
const attributes = collectNodesOfType(tree, "Attribute");
let nameNode = findNodeOfType(attributes[0], "AttributeName");
@@ -89,8 +87,7 @@ const multiStatusTaskExample = `
`;
Deno.test("Test multi-status tasks", () => {
const lang = buildMarkdown([]);
const tree = parse(lang, multiStatusTaskExample);
const tree = parse(extendedMarkdownLanguage, multiStatusTaskExample);
// console.log("Tasks parsed", JSON.stringify(tree, null, 2));
const tasks = collectNodesOfType(tree, "Task");
assertEquals(tasks.length, 3);
@@ -107,8 +104,7 @@ const commandLinkSample = `
`;
Deno.test("Test command links", () => {
const lang = buildMarkdown([]);
const tree = parse(lang, commandLinkSample);
const tree = parse(extendedMarkdownLanguage, commandLinkSample);
const commands = collectNodesOfType(tree, "CommandLink");
console.log("Command links parsed", JSON.stringify(commands, null, 2));
assertEquals(commands.length, 3);
@@ -125,8 +121,7 @@ const commandLinkArgsSample = `
`;
Deno.test("Test command link arguments", () => {
const lang = buildMarkdown([]);
const tree = parse(lang, commandLinkArgsSample);
const tree = parse(extendedMarkdownLanguage, commandLinkArgsSample);
const commands = collectNodesOfType(tree, "CommandLink");
assertEquals(commands.length, 2);
@@ -138,7 +133,6 @@ Deno.test("Test command link arguments", () => {
});
Deno.test("Test template directives", () => {
const lang = buildMarkdown([]);
const tree = parse(lang, `Hello there {{name}}!`);
const tree = parse(extendedMarkdownLanguage, `Hello there {{name}}!`);
console.log("Template directive", JSON.stringify(tree, null, 2));
});
+125 -55
View File
@@ -1,6 +1,5 @@
import {
BlockContext,
Language,
LeafBlock,
LeafBlockParser,
Line,
@@ -9,16 +8,14 @@ import {
StreamLanguage,
Strikethrough,
styleTags,
Tag,
tags as t,
yamlLanguage,
} from "../deps.ts";
import * as ct from "./customtags.ts";
import { HashtagTag, TaskDeadlineTag } from "./customtags.ts";
import { NakedURLTag } from "./customtags.ts";
import { TaskList } from "./extended_task.ts";
import {
MDExt,
mdExtensionStyleTags,
mdExtensionSyntaxConfig,
} from "./markdown_ext.ts";
export const pageLinkRegex = /^\[\[([^\]\|]+)(\|([^\]]+))?\]\]/;
@@ -313,6 +310,77 @@ export const Comment: MarkdownConfig = {
],
};
type RegexParserExtension = {
// unicode char code for efficiency .charCodeAt(0)
firstCharCode: number;
regex: RegExp;
nodeType: string;
tag: Tag;
className?: string;
};
function regexParser({
regex,
firstCharCode,
nodeType,
}: RegexParserExtension): MarkdownConfig {
return {
defineNodes: [nodeType],
parseInline: [
{
name: nodeType,
parse(cx, next, pos) {
if (firstCharCode !== next) {
return -1;
}
const match = regex.exec(cx.slice(pos, cx.end));
if (!match) {
return -1;
}
return cx.addElement(cx.elt(nodeType, pos, pos + match[0].length));
},
},
],
};
}
const NakedURL = regexParser(
{
firstCharCode: 104, // h
regex:
/^https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}([-a-zA-Z0-9()@:%_\+.~#?&=\/]*)/,
nodeType: "NakedURL",
className: "sb-naked-url",
tag: NakedURLTag,
},
);
const Hashtag = regexParser(
{
firstCharCode: 35, // #
regex: /^#[^#\d\s\[\]]+\w+/,
nodeType: "Hashtag",
className: "sb-hashtag",
tag: ct.HashtagTag,
},
);
const TaskDeadline = regexParser({
firstCharCode: 55357, // 📅
regex: /^📅\s*\d{4}\-\d{2}\-\d{2}/,
className: "sb-task-deadline",
nodeType: "DeadlineDate",
tag: ct.TaskDeadlineTag,
});
const NamedAnchor = regexParser({
firstCharCode: 36, // $
regex: /^\$[a-zA-Z\.\-\/]+[\w\.\-\/]*/,
className: "sb-named-anchor",
nodeType: "NamedAnchor",
tag: ct.NamedAnchorTag,
});
import { Table } from "./table_parser.ts";
import { foldNodeProp } from "@codemirror/language";
@@ -379,54 +447,56 @@ export const FrontMatter: MarkdownConfig = {
}],
};
export default function buildMarkdown(mdExtensions: MDExt[]): Language {
return markdown({
extensions: [
WikiLink,
CommandLink,
Attribute,
FrontMatter,
TaskList,
Comment,
Highlight,
TemplateDirective,
Strikethrough,
Table,
...mdExtensions.map(mdExtensionSyntaxConfig),
{
props: [
foldNodeProp.add({
// Don't fold at the list level
BulletList: () => null,
OrderedList: () => null,
// Fold list items
ListItem: (tree, state) => ({
from: state.doc.lineAt(tree.from).to,
to: tree.to,
}),
// Fold frontmatter
FrontMatter: (tree) => ({
from: tree.from,
to: tree.to,
}),
export const extendedMarkdownLanguage = markdown({
extensions: [
WikiLink,
CommandLink,
Attribute,
FrontMatter,
TaskList,
Comment,
Highlight,
TemplateDirective,
Strikethrough,
Table,
NakedURL,
Hashtag,
TaskDeadline,
NamedAnchor,
{
props: [
foldNodeProp.add({
// Don't fold at the list level
BulletList: () => null,
OrderedList: () => null,
// Fold list items
ListItem: (tree, state) => ({
from: state.doc.lineAt(tree.from).to,
to: tree.to,
}),
// Fold frontmatter
FrontMatter: (tree) => ({
from: tree.from,
to: tree.to,
}),
}),
styleTags({
Task: ct.TaskTag,
TaskMark: ct.TaskMarkTag,
Comment: ct.CommentTag,
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark":
t.processingInstruction,
"TableHeader/...": t.heading,
TableCell: t.content,
CodeInfo: ct.CodeInfoTag,
HorizontalRule: ct.HorizontalRuleTag,
}),
...mdExtensions.map((mdExt) =>
styleTags(mdExtensionStyleTags(mdExt))
),
],
},
],
}).language;
}
styleTags({
Task: ct.TaskTag,
TaskMark: ct.TaskMarkTag,
Comment: ct.CommentTag,
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark":
t.processingInstruction,
"TableHeader/...": t.heading,
TableCell: t.content,
CodeInfo: ct.CodeInfoTag,
HorizontalRule: ct.HorizontalRuleTag,
Hashtag: ct.HashtagTag,
NakedURL: ct.NakedURLTag,
DeadlineDate: ct.TaskDeadlineTag,
NamedAnchor: ct.NamedAnchorTag,
}),
],
},
],
}).language;
+3 -3
View File
@@ -1,12 +1,12 @@
import { SysCallMapping } from "../../plugos/system.ts";
import { parse } from "../markdown_parser/parse_tree.ts";
import { Language } from "../../web/deps.ts";
import type { ParseTree } from "$sb/lib/tree.ts";
import { extendedMarkdownLanguage } from "../markdown_parser/parser.ts";
export function markdownSyscalls(lang: Language): SysCallMapping {
export function markdownSyscalls(): SysCallMapping {
return {
"markdown.parseMarkdown": (_ctx, text: string): ParseTree => {
return parse(lang, text);
return parse(extendedMarkdownLanguage, text);
},
};
}