Remove "syntax" support from plugs

pull/662/head
Zef Hemel 2024-01-24 13:34:12 +01:00
parent aaacec6d61
commit ad4a795e7f
24 changed files with 185 additions and 315 deletions

View File

@ -27,40 +27,8 @@ export type SilverBulletHooks =
& EndpointHookT & EndpointHookT
& PlugNamespaceHookT; & 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. /** 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`. * 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>;

View File

@ -17,6 +17,12 @@ export const AttributeTag = Tag.define();
export const AttributeNameTag = Tag.define(); export const AttributeNameTag = Tag.define();
export const AttributeValueTag = Tag.define(); export const AttributeValueTag = Tag.define();
export const NamedAnchorTag = Tag.define();
export const TaskTag = Tag.define(); export const TaskTag = Tag.define();
export const TaskMarkTag = Tag.define(); export const TaskMarkTag = Tag.define();
export const TaskStateTag = Tag.define(); export const TaskStateTag = Tag.define();
export const TaskDeadlineTag = Tag.define();
export const HashtagTag = Tag.define();
export const NakedURLTag = Tag.define();

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,
};
}

View File

@ -1,11 +1,11 @@
import { parse } from "./parse_tree.ts"; import { parse } from "./parse_tree.ts";
import buildMarkdown from "./parser.ts";
import { import {
collectNodesOfType, collectNodesOfType,
findNodeOfType, findNodeOfType,
renderToText, renderToText,
} from "../../plug-api/lib/tree.ts"; } from "../../plug-api/lib/tree.ts";
import { assertEquals, assertNotEquals } from "../../test_deps.ts"; import { assertEquals, assertNotEquals } from "../../test_deps.ts";
import { extendedMarkdownLanguage } from "./parser.ts";
const sample1 = `--- const sample1 = `---
type: page type: page
@ -26,8 +26,7 @@ name: Zef
Supper`; Supper`;
Deno.test("Test parser", () => { Deno.test("Test parser", () => {
const lang = buildMarkdown([]); let tree = parse(extendedMarkdownLanguage, sample1);
let tree = parse(lang, sample1);
// console.log("tree", JSON.stringify(tree, null, 2)); // console.log("tree", JSON.stringify(tree, null, 2));
// Check if rendering back to text works // Check if rendering back to text works
assertEquals(renderToText(tree), sample1); assertEquals(renderToText(tree), sample1);
@ -45,7 +44,7 @@ Deno.test("Test parser", () => {
// Find frontmatter // Find frontmatter
let node = findNodeOfType(tree, "FrontMatter"); let node = findNodeOfType(tree, "FrontMatter");
assertNotEquals(node, undefined); assertNotEquals(node, undefined);
tree = parse(lang, sampleInvalid1); tree = parse(extendedMarkdownLanguage, sampleInvalid1);
node = findNodeOfType(tree, "FrontMatter"); node = findNodeOfType(tree, "FrontMatter");
// console.log("Invalid node", node); // console.log("Invalid node", node);
assertEquals(node, undefined); assertEquals(node, undefined);
@ -62,8 +61,7 @@ And one with nested brackets: [array: [1, 2, 3]]
`; `;
Deno.test("Test inline attribute syntax", () => { Deno.test("Test inline attribute syntax", () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
const tree = parse(lang, inlineAttributeSample);
// console.log("Attribute parsed", JSON.stringify(tree, null, 2)); // console.log("Attribute parsed", JSON.stringify(tree, null, 2));
const attributes = collectNodesOfType(tree, "Attribute"); const attributes = collectNodesOfType(tree, "Attribute");
let nameNode = findNodeOfType(attributes[0], "AttributeName"); let nameNode = findNodeOfType(attributes[0], "AttributeName");
@ -89,8 +87,7 @@ const multiStatusTaskExample = `
`; `;
Deno.test("Test multi-status tasks", () => { Deno.test("Test multi-status tasks", () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, multiStatusTaskExample);
const tree = parse(lang, multiStatusTaskExample);
// console.log("Tasks parsed", JSON.stringify(tree, null, 2)); // console.log("Tasks parsed", JSON.stringify(tree, null, 2));
const tasks = collectNodesOfType(tree, "Task"); const tasks = collectNodesOfType(tree, "Task");
assertEquals(tasks.length, 3); assertEquals(tasks.length, 3);
@ -107,8 +104,7 @@ const commandLinkSample = `
`; `;
Deno.test("Test command links", () => { Deno.test("Test command links", () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, commandLinkSample);
const tree = parse(lang, commandLinkSample);
const commands = collectNodesOfType(tree, "CommandLink"); const commands = collectNodesOfType(tree, "CommandLink");
console.log("Command links parsed", JSON.stringify(commands, null, 2)); console.log("Command links parsed", JSON.stringify(commands, null, 2));
assertEquals(commands.length, 3); assertEquals(commands.length, 3);
@ -125,8 +121,7 @@ const commandLinkArgsSample = `
`; `;
Deno.test("Test command link arguments", () => { Deno.test("Test command link arguments", () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, commandLinkArgsSample);
const tree = parse(lang, commandLinkArgsSample);
const commands = collectNodesOfType(tree, "CommandLink"); const commands = collectNodesOfType(tree, "CommandLink");
assertEquals(commands.length, 2); assertEquals(commands.length, 2);
@ -138,7 +133,6 @@ Deno.test("Test command link arguments", () => {
}); });
Deno.test("Test template directives", () => { Deno.test("Test template directives", () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, `Hello there {{name}}!`);
const tree = parse(lang, `Hello there {{name}}!`);
console.log("Template directive", JSON.stringify(tree, null, 2)); console.log("Template directive", JSON.stringify(tree, null, 2));
}); });

View File

@ -1,6 +1,5 @@
import { import {
BlockContext, BlockContext,
Language,
LeafBlock, LeafBlock,
LeafBlockParser, LeafBlockParser,
Line, Line,
@ -9,16 +8,14 @@ import {
StreamLanguage, StreamLanguage,
Strikethrough, Strikethrough,
styleTags, styleTags,
Tag,
tags as t, tags as t,
yamlLanguage, yamlLanguage,
} from "../deps.ts"; } from "../deps.ts";
import * as ct from "./customtags.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 { TaskList } from "./extended_task.ts";
import {
MDExt,
mdExtensionStyleTags,
mdExtensionSyntaxConfig,
} from "./markdown_ext.ts";
export const pageLinkRegex = /^\[\[([^\]\|]+)(\|([^\]]+))?\]\]/; 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 { Table } from "./table_parser.ts";
import { foldNodeProp } from "@codemirror/language"; import { foldNodeProp } from "@codemirror/language";
@ -379,54 +447,56 @@ export const FrontMatter: MarkdownConfig = {
}], }],
}; };
export default function buildMarkdown(mdExtensions: MDExt[]): Language { export const extendedMarkdownLanguage = markdown({
return markdown({ extensions: [
extensions: [ WikiLink,
WikiLink, CommandLink,
CommandLink, Attribute,
Attribute, FrontMatter,
FrontMatter, TaskList,
TaskList, Comment,
Comment, Highlight,
Highlight, TemplateDirective,
TemplateDirective, Strikethrough,
Strikethrough, Table,
Table, NakedURL,
...mdExtensions.map(mdExtensionSyntaxConfig), Hashtag,
{ TaskDeadline,
props: [ NamedAnchor,
foldNodeProp.add({ {
// Don't fold at the list level props: [
BulletList: () => null, foldNodeProp.add({
OrderedList: () => null, // Don't fold at the list level
// Fold list items BulletList: () => null,
ListItem: (tree, state) => ({ OrderedList: () => null,
from: state.doc.lineAt(tree.from).to, // Fold list items
to: tree.to, ListItem: (tree, state) => ({
}), from: state.doc.lineAt(tree.from).to,
// Fold frontmatter to: tree.to,
FrontMatter: (tree) => ({
from: tree.from,
to: tree.to,
}),
}), }),
// Fold frontmatter
FrontMatter: (tree) => ({
from: tree.from,
to: tree.to,
}),
}),
styleTags({ styleTags({
Task: ct.TaskTag, Task: ct.TaskTag,
TaskMark: ct.TaskMarkTag, TaskMark: ct.TaskMarkTag,
Comment: ct.CommentTag, Comment: ct.CommentTag,
"TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark": "TableDelimiter SubscriptMark SuperscriptMark StrikethroughMark":
t.processingInstruction, t.processingInstruction,
"TableHeader/...": t.heading, "TableHeader/...": t.heading,
TableCell: t.content, TableCell: t.content,
CodeInfo: ct.CodeInfoTag, CodeInfo: ct.CodeInfoTag,
HorizontalRule: ct.HorizontalRuleTag, HorizontalRule: ct.HorizontalRuleTag,
}), Hashtag: ct.HashtagTag,
...mdExtensions.map((mdExt) => NakedURL: ct.NakedURLTag,
styleTags(mdExtensionStyleTags(mdExt)) DeadlineDate: ct.TaskDeadlineTag,
), NamedAnchor: ct.NamedAnchorTag,
], }),
}, ],
], },
}).language; ],
} }).language;

View File

@ -1,12 +1,12 @@
import { SysCallMapping } from "../../plugos/system.ts"; import { SysCallMapping } from "../../plugos/system.ts";
import { parse } from "../markdown_parser/parse_tree.ts"; import { parse } from "../markdown_parser/parse_tree.ts";
import { Language } from "../../web/deps.ts";
import type { ParseTree } from "$sb/lib/tree.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 { return {
"markdown.parseMarkdown": (_ctx, text: string): ParseTree => { "markdown.parseMarkdown": (_ctx, text: string): ParseTree => {
return parse(lang, text); return parse(extendedMarkdownLanguage, text);
}, },
}; };
} }

View File

@ -1,9 +1,9 @@
import "$sb/lib/syscall_mock.ts"; import "$sb/lib/syscall_mock.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { extractAttributes } from "$sb/lib/attribute.ts"; import { extractAttributes } from "$sb/lib/attribute.ts";
import { assertEquals } from "../../test_deps.ts"; import { assertEquals } from "../../test_deps.ts";
import { renderToText } from "$sb/lib/tree.ts"; import { renderToText } from "$sb/lib/tree.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
const inlineAttributeSample = ` const inlineAttributeSample = `
# My document # My document
@ -26,8 +26,7 @@ Top level attributes:
`; `;
Deno.test("Test attribute extraction", async () => { Deno.test("Test attribute extraction", async () => {
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
const tree = parse(lang, inlineAttributeSample);
const toplevelAttributes = await extractAttributes(tree, false); const toplevelAttributes = await extractAttributes(tree, false);
// console.log("All attributes", toplevelAttributes); // console.log("All attributes", toplevelAttributes);
assertEquals(toplevelAttributes.name, "sup"); assertEquals(toplevelAttributes.name, "sup");

View File

@ -1,9 +1,8 @@
import "$sb/lib/syscall_mock.ts"; import "$sb/lib/syscall_mock.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { assertEquals } from "../../test_deps.ts"; import { assertEquals } from "../../test_deps.ts";
import { extractFeedItems } from "$sb/lib/feed.ts"; import { extractFeedItems } from "$sb/lib/feed.ts";
import { nodeDefToMDExt } from "../../common/markdown_parser/markdown_ext.ts"; import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
const feedSample1 = `--- const feedSample1 = `---
test: ignore me test: ignore me
@ -25,11 +24,7 @@ Completely free form
Deno.test("Test feed parsing", async () => { Deno.test("Test feed parsing", async () => {
// Ad hoc added the NamedAnchor extension from the core plug-in inline here // Ad hoc added the NamedAnchor extension from the core plug-in inline here
const lang = buildMarkdown([nodeDefToMDExt("NamedAnchor", { const tree = parse(extendedMarkdownLanguage, feedSample1);
firstCharacters: ["$"],
regex: "\\$[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*",
})]);
const tree = parse(lang, feedSample1);
const items = await extractFeedItems(tree); const items = await extractFeedItems(tree);
assertEquals(items.length, 3); assertEquals(items.length, 3);
assertEquals(items[0], { assertEquals(items[0], {

View File

@ -1,8 +1,7 @@
import wikiMarkdownLang from "../../common/markdown_parser/parser.ts";
import type { ParseTree } from "$sb/lib/tree.ts"; import type { ParseTree } from "$sb/lib/tree.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
export function parseMarkdown(text: string): ParseTree { export function parseMarkdown(text: string): ParseTree {
const lang = wikiMarkdownLang([]); return parse(extendedMarkdownLanguage, text);
return parse(lang, text);
} }

View File

@ -9,9 +9,9 @@ import {
renderToText, renderToText,
replaceNodesMatching, replaceNodesMatching,
} from "./tree.ts"; } from "./tree.ts";
import wikiMarkdownLang from "../../common/markdown_parser/parser.ts";
import { assertEquals, assertNotEquals } from "../../test_deps.ts"; import { assertEquals, assertNotEquals } from "../../test_deps.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
const mdTest1 = ` const mdTest1 = `
# Heading # Heading
@ -49,8 +49,7 @@ name: something
`; `;
Deno.test("Test parsing", () => { Deno.test("Test parsing", () => {
const lang = wikiMarkdownLang([]); const mdTree = parse(extendedMarkdownLanguage, mdTest1);
const mdTree = parse(lang, mdTest1);
addParentPointers(mdTree); addParentPointers(mdTree);
// console.log(JSON.stringify(mdTree, null, 2)); // console.log(JSON.stringify(mdTree, null, 2));
const wikiLink = nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!; const wikiLink = nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!;
@ -75,12 +74,11 @@ Deno.test("Test parsing", () => {
} }
}); });
// console.log(JSON.stringify(mdTree, null, 2)); // console.log(JSON.stringify(mdTree, null, 2));
let mdTree3 = parse(lang, mdTest3); let mdTree3 = parse(extendedMarkdownLanguage, mdTest3);
// console.log(JSON.stringify(mdTree3, null, 2)); // console.log(JSON.stringify(mdTree3, null, 2));
}); });
Deno.test("AST functions", () => { Deno.test("AST functions", () => {
const lang = wikiMarkdownLang([]); const mdTree = parse(extendedMarkdownLanguage, mdTest1);
const mdTree = parse(lang, mdTest1);
console.log(JSON.stringify(parseTreeToAST(mdTree), null, 2)); console.log(JSON.stringify(parseTreeToAST(mdTree), null, 2));
}); });

View File

@ -1,10 +1,4 @@
name: editor name: editor
syntax:
NakedURL:
firstCharacters:
- "h"
regex: "https?:\\/\\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}([-a-zA-Z0-9()@:%_\\+.~#?&=\\/]*)"
className: sb-naked-url
functions: functions:
setEditorMode: setEditorMode:
path: "./editor.ts:setEditorMode" path: "./editor.ts:setEditorMode"

View File

@ -1,15 +1,4 @@
name: index name: index
syntax:
Hashtag:
firstCharacters:
- "#"
regex: "#[^#\\d\\s\\[\\]]+\\w+"
className: sb-hashtag
NamedAnchor:
firstCharacters:
- "$"
regex: "\\$[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*"
className: sb-named-anchor
functions: functions:
loadBuiltinsIntoIndex: loadBuiltinsIntoIndex:
path: builtins.ts:loadBuiltinsIntoIndex path: builtins.ts:loadBuiltinsIntoIndex

View File

@ -1,10 +1,9 @@
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import { System } from "../../plugos/system.ts"; import { System } from "../../plugos/system.ts";
import { createSandbox } from "../../plugos/sandboxes/deno_worker_sandbox.ts"; import { createSandbox } from "../../plugos/sandboxes/deno_worker_sandbox.ts";
import { loadMarkdownExtensions } from "../../common/markdown_parser/markdown_ext.ts";
import { renderMarkdownToHtml } from "./markdown_render.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
Deno.test("Markdown render", async () => { Deno.test("Markdown render", async () => {
const system = new System<any>("server"); const system = new System<any>("server");
@ -20,11 +19,10 @@ Deno.test("Markdown render", async () => {
new URL("../../dist_plug_bundle/_plug/tasks.plug.js", import.meta.url), new URL("../../dist_plug_bundle/_plug/tasks.plug.js", import.meta.url),
), ),
); );
const lang = buildMarkdown(loadMarkdownExtensions(system));
const testFile = Deno.readTextFileSync( const testFile = Deno.readTextFileSync(
new URL("test/example.md", import.meta.url).pathname, new URL("test/example.md", import.meta.url).pathname,
); );
const tree = parse(lang, testFile); const tree = parse(extendedMarkdownLanguage, testFile);
renderMarkdownToHtml(tree, { renderMarkdownToHtml(tree, {
failOnUnknown: true, failOnUnknown: true,
}); });
@ -35,8 +33,7 @@ Deno.test("Markdown render", async () => {
Deno.test("Smart hard break test", () => { Deno.test("Smart hard break test", () => {
const example = `**Hello** const example = `**Hello**
*world!*`; *world!*`;
const lang = buildMarkdown([]); const tree = parse(extendedMarkdownLanguage, example);
const tree = parse(lang, example);
const html = renderMarkdownToHtml(tree, { const html = renderMarkdownToHtml(tree, {
failOnUnknown: true, failOnUnknown: true,
smartHardBreak: true, smartHardBreak: true,
@ -58,7 +55,7 @@ And another
Server: something else Server: something else
📅 last_updated - [Release notes](release_notes_url)`; 📅 last_updated - [Release notes](release_notes_url)`;
const tree2 = parse(lang, example2); const tree2 = parse(extendedMarkdownLanguage, example2);
const html2 = renderMarkdownToHtml(tree2, { const html2 = renderMarkdownToHtml(tree2, {
failOnUnknown: true, failOnUnknown: true,
smartHardBreak: true, smartHardBreak: true,

View File

@ -1,23 +1,4 @@
name: tasks name: tasks
syntax:
DeadlineDate:
firstCharacters:
- "📅"
regex: "📅\\s*\\d{4}\\-\\d{2}\\-\\d{2}"
styles:
backgroundColor: "rgba(22,22,22,0.07)"
CompletedDate:
firstCharacters:
- "✅"
regex: "✅\\s*\\d{4}\\-\\d{2}\\-\\d{2}"
styles:
backgroundColor: "rgba(22,22,22,0.07)"
RepeatInterval:
firstCharacters:
- "🔁"
regex: "🔁\\s*every\\s+\\w+"
styles:
backgroundColor: "rgba(22,22,22,0.07)"
functions: functions:
# API # API
updateTaskState: updateTaskState:

View File

@ -1,7 +1,5 @@
import { PlugNamespaceHook } from "../common/hooks/plug_namespace.ts"; import { PlugNamespaceHook } from "../common/hooks/plug_namespace.ts";
import { SilverBulletHooks } from "../common/manifest.ts"; import { SilverBulletHooks } from "../common/manifest.ts";
import { loadMarkdownExtensions } from "../common/markdown_parser/markdown_ext.ts";
import buildMarkdown from "../common/markdown_parser/parser.ts";
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts"; import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
import { PlugSpacePrimitives } from "../common/spaces/plug_space_primitives.ts"; import { PlugSpacePrimitives } from "../common/spaces/plug_space_primitives.ts";
import { createSandbox } from "../plugos/sandboxes/web_worker_sandbox.ts"; import { createSandbox } from "../plugos/sandboxes/web_worker_sandbox.ts";
@ -135,7 +133,7 @@ export class ServerSystem {
dataStoreSyscalls(this.ds), dataStoreSyscalls(this.ds),
debugSyscalls(), debugSyscalls(),
codeWidgetSyscalls(codeWidgetHook), codeWidgetSyscalls(codeWidgetHook),
markdownSyscalls(buildMarkdown([])), // Will later be replaced with markdown extensions markdownSyscalls(),
); );
// Syscalls that require some additional permissions // Syscalls that require some additional permissions
@ -151,12 +149,6 @@ export class ServerSystem {
await this.loadPlugs(); await this.loadPlugs();
// Load markdown syscalls based on all new syntax (if any)
this.system.registerSyscalls(
[],
markdownSyscalls(buildMarkdown(loadMarkdownExtensions(this.system))),
);
this.listInterval = setInterval(() => { this.listInterval = setInterval(() => {
// runWithSystemLock(this.system, async () => { // runWithSystemLock(this.system, async () => {
// await space.updatePageList(); // await space.updatePageList();

View File

@ -94,17 +94,6 @@ export class Client {
.catch((e) => console.error("Error dispatching editor:updated event", e)); .catch((e) => console.error("Error dispatching editor:updated event", e));
}, 1000); }, 1000);
debouncedPlugsUpdatedEvent = throttle(async () => {
// To register new commands, update editor state based on new plugs
this.rebuildEditorState();
await this.dispatchAppEvent(
"editor:pageLoaded",
this.currentPage,
undefined,
true,
);
}, 1000);
// Track if plugs have been updated since sync cycle // Track if plugs have been updated since sync cycle
fullSyncCompleted = false; fullSyncCompleted = false;
@ -195,7 +184,7 @@ export class Client {
this.focus(); this.focus();
await this.system.init(); this.system.init();
await this.loadSettings(); await this.loadSettings();
@ -773,7 +762,6 @@ export class Client {
async loadPlugs() { async loadPlugs() {
await this.system.reloadPlugsFromSpace(this.space); await this.system.reloadPlugsFromSpace(this.space);
this.rebuildEditorState();
await this.eventHook.dispatchEvent("system:ready"); await this.eventHook.dispatchEvent("system:ready");
await this.dispatchAppEvent("plugs:loaded"); await this.dispatchAppEvent("plugs:loaded");
} }
@ -782,12 +770,7 @@ export class Client {
const editorView = this.editorView; const editorView = this.editorView;
console.log("Rebuilding editor state"); console.log("Rebuilding editor state");
this.system.updateMarkdownParser();
if (this.currentPage) { if (this.currentPage) {
// And update the editor if a page is loaded
// this.openPages.saveState(this.currentPage);
editorView.setState( editorView.setState(
createEditorState( createEditorState(
this, this,
@ -801,8 +784,6 @@ export class Client {
editorView.contentDOM, editorView.contentDOM,
); );
} }
// this.openPages.restoreState(this.currentPage);
} }
} }
@ -930,8 +911,6 @@ export class Client {
const editorView = this.editorView; const editorView = this.editorView;
const previousPage = this.currentPage; const previousPage = this.currentPage;
// console.log("Navigating to", pageName, restoreState);
// Persist current page state and nicely close page // Persist current page state and nicely close page
if (previousPage) { if (previousPage) {
// this.openPages.saveState(previousPage); // this.openPages.saveState(previousPage);

View File

@ -1,6 +1,5 @@
import { PlugNamespaceHook } from "../common/hooks/plug_namespace.ts"; import { PlugNamespaceHook } from "../common/hooks/plug_namespace.ts";
import { Manifest, SilverBulletHooks } from "../common/manifest.ts"; import { SilverBulletHooks } from "../common/manifest.ts";
import buildMarkdown from "../common/markdown_parser/parser.ts";
import { CronHook } from "../plugos/hooks/cron.ts"; import { CronHook } from "../plugos/hooks/cron.ts";
import { EventHook } from "../plugos/hooks/event.ts"; import { EventHook } from "../plugos/hooks/event.ts";
import { createSandbox } from "../plugos/sandboxes/web_worker_sandbox.ts"; import { createSandbox } from "../plugos/sandboxes/web_worker_sandbox.ts";
@ -23,10 +22,6 @@ import { syncSyscalls } from "./syscalls/sync.ts";
import { systemSyscalls } from "./syscalls/system.ts"; import { systemSyscalls } from "./syscalls/system.ts";
import { yamlSyscalls } from "../common/syscalls/yaml.ts"; import { yamlSyscalls } from "../common/syscalls/yaml.ts";
import { Space } from "./space.ts"; import { Space } from "./space.ts";
import {
loadMarkdownExtensions,
MDExt,
} from "../common/markdown_parser/markdown_ext.ts";
import { MQHook } from "../plugos/hooks/mq.ts"; import { MQHook } from "../plugos/hooks/mq.ts";
import { mqSyscalls } from "../plugos/syscalls/mq.ts"; import { mqSyscalls } from "../plugos/syscalls/mq.ts";
import { mqProxySyscalls } from "./syscalls/mq.proxy.ts"; import { mqProxySyscalls } from "./syscalls/mq.proxy.ts";
@ -51,7 +46,6 @@ export class ClientSystem {
slashCommandHook: SlashCommandHook; slashCommandHook: SlashCommandHook;
namespaceHook: PlugNamespaceHook; namespaceHook: PlugNamespaceHook;
codeWidgetHook: CodeWidgetHook; codeWidgetHook: CodeWidgetHook;
mdExtensions: MDExt[] = [];
system: System<SilverBulletHooks>; system: System<SilverBulletHooks>;
panelWidgetHook: PanelWidgetHook; panelWidgetHook: PanelWidgetHook;
@ -139,22 +133,17 @@ export class ClientSystem {
const plugName = plugNameExtractRegex.exec(path)![1]; const plugName = plugNameExtractRegex.exec(path)![1];
console.log("Plug updated, reloading", plugName, "from", path); console.log("Plug updated, reloading", plugName, "from", path);
this.system.unload(path); this.system.unload(path);
const plug = await this.system.load( await this.system.load(
plugName, plugName,
createSandbox(new URL(`/${path}`, location.href)), createSandbox(new URL(`/${path}`, location.href)),
newHash, newHash,
); );
if ((plug.manifest! as Manifest).syntax) {
// If there are syntax extensions, rebuild the markdown parser immediately
this.updateMarkdownParser();
}
this.client.debouncedPlugsUpdatedEvent();
} }
}, },
); );
} }
async init() { init() {
// Slash command hook // Slash command hook
this.slashCommandHook = new SlashCommandHook(this.client); this.slashCommandHook = new SlashCommandHook(this.client);
this.system.addHook(this.slashCommandHook); this.system.addHook(this.slashCommandHook);
@ -166,7 +155,7 @@ export class ClientSystem {
editorSyscalls(this.client), editorSyscalls(this.client),
spaceSyscalls(this.client), spaceSyscalls(this.client),
systemSyscalls(this.system, this.client), systemSyscalls(this.system, this.client),
markdownSyscalls(buildMarkdown(this.mdExtensions)), markdownSyscalls(),
assetSyscalls(this.system), assetSyscalls(this.system),
yamlSyscalls(), yamlSyscalls(),
handlebarsSyscalls(), handlebarsSyscalls(),
@ -222,16 +211,6 @@ export class ClientSystem {
})); }));
} }
updateMarkdownParser() {
// Load all syntax extensions
this.mdExtensions = loadMarkdownExtensions(this.system);
// And reload the syscalls to use the new syntax extensions
this.system.registerSyscalls(
[],
markdownSyscalls(buildMarkdown(this.mdExtensions)),
);
}
localSyscall(name: string, args: any[]) { localSyscall(name: string, args: any[]) {
return this.system.localSyscall(name, args); return this.system.localSyscall(name, args);
} }

View File

@ -1,13 +1,13 @@
import { Diagnostic, linter } from "@codemirror/lint"; import { Diagnostic, linter } from "@codemirror/lint";
import type { Client } from "../client.ts"; import type { Client } from "../client.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { LintEvent } from "$sb/app_event.ts"; import { LintEvent } from "$sb/app_event.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
export function plugLinter(client: Client) { export function plugLinter(client: Client) {
return linter(async (view): Promise<Diagnostic[]> => { return linter(async (view): Promise<Diagnostic[]> => {
const tree = parse( const tree = parse(
buildMarkdown(client.system.mdExtensions), extendedMarkdownLanguage,
view.state.sliceDoc(), view.state.sliceDoc(),
); );
const results = (await client.dispatchAppEvent("editor:lint", { const results = (await client.dispatchAppEvent("editor:lint", {

View File

@ -4,8 +4,8 @@ import type { CodeWidgetButton, CodeWidgetCallback } from "$sb/types.ts";
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts"; import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
import { resolveAttachmentPath } from "$sb/lib/resolve.ts"; import { resolveAttachmentPath } from "$sb/lib/resolve.ts";
import { parse } from "../../common/markdown_parser/parse_tree.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { parsePageRef } from "$sb/lib/page.ts"; import { parsePageRef } from "$sb/lib/page.ts";
import { extendedMarkdownLanguage } from "../../common/markdown_parser/parser.ts";
const activeWidgets = new Set<MarkdownWidget>(); const activeWidgets = new Set<MarkdownWidget>();
@ -59,9 +59,8 @@ export class MarkdownWidget extends WidgetType {
); );
return; return;
} }
const lang = buildMarkdown(this.client.system.mdExtensions);
let mdTree = parse( let mdTree = parse(
lang, extendedMarkdownLanguage,
widgetContent.markdown!, widgetContent.markdown!,
); );
mdTree = await this.client.system.localSyscall( mdTree = await this.client.system.localSyscall(

View File

@ -4,7 +4,7 @@ import { CompletionContext, CompletionResult } from "../deps.ts";
import { PageMeta } from "$sb/types.ts"; import { PageMeta } from "$sb/types.ts";
import { isFederationPath } from "$sb/lib/resolve.ts"; import { isFederationPath } from "$sb/lib/resolve.ts";
const tagRegex = /#[^#\d\s\[\]]+\w+/g; export const tagRegex = /#[^#\d\s\[\]]+\w+/g;
export function PageNavigator({ export function PageNavigator({
allPages, allPages,

View File

@ -1,6 +1,4 @@
import buildMarkdown, { import { commandLinkRegex } from "../common/markdown_parser/parser.ts";
commandLinkRegex,
} from "../common/markdown_parser/parser.ts";
import { readonlyMode } from "./cm_plugins/readonly.ts"; import { readonlyMode } from "./cm_plugins/readonly.ts";
import customMarkdownStyle from "./style.ts"; import customMarkdownStyle from "./style.ts";
import { import {
@ -46,6 +44,7 @@ import { postScriptPrefacePlugin } from "./cm_plugins/top_bottom_panels.ts";
import { languageFor } from "../common/languages.ts"; import { languageFor } from "../common/languages.ts";
import { plugLinter } from "./cm_plugins/lint.ts"; import { plugLinter } from "./cm_plugins/lint.ts";
import { Compartment, Extension } from "@codemirror/state"; import { Compartment, Extension } from "@codemirror/state";
import { extendedMarkdownLanguage } from "../common/markdown_parser/parser.ts";
export function createEditorState( export function createEditorState(
client: Client, client: Client,
@ -55,8 +54,6 @@ export function createEditorState(
): EditorState { ): EditorState {
let touchCount = 0; let touchCount = 0;
const markdownLanguage = buildMarkdown(client.system.mdExtensions);
// Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded // Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded
client.keyHandlerCompartment = new Compartment(); client.keyHandlerCompartment = new Compartment();
const keyBindings = client.keyHandlerCompartment.of( const keyBindings = client.keyHandlerCompartment.of(
@ -82,7 +79,7 @@ export function createEditorState(
// The uber markdown mode // The uber markdown mode
markdown({ markdown({
base: markdownLanguage, base: extendedMarkdownLanguage,
codeLanguages: (info) => { codeLanguages: (info) => {
const lang = languageFor(info); const lang = languageFor(info);
if (lang) { if (lang) {
@ -96,10 +93,10 @@ export function createEditorState(
}, },
addKeymap: true, addKeymap: true,
}), }),
markdownLanguage.data.of({ extendedMarkdownLanguage.data.of({
closeBrackets: { brackets: ["(", "{", "[", "`"] }, closeBrackets: { brackets: ["(", "{", "[", "`"] },
}), }),
syntaxHighlighting(customMarkdownStyle(client.system.mdExtensions)), syntaxHighlighting(customMarkdownStyle()),
autocompletion({ autocompletion({
override: [ override: [
client.editorComplete.bind(client), client.editorComplete.bind(client),

View File

@ -1,9 +1,8 @@
import { HighlightStyle } from "../common/deps.ts"; import { HighlightStyle } from "../common/deps.ts";
import { tagHighlighter, tags as t } from "./deps.ts"; import { tagHighlighter, tags as t } from "./deps.ts";
import * as ct from "../common/markdown_parser/customtags.ts"; import * as ct from "../common/markdown_parser/customtags.ts";
import { MDExt } from "../common/markdown_parser/markdown_ext.ts";
export default function highlightStyles(mdExtension: MDExt[]) { export default function highlightStyles() {
tagHighlighter; tagHighlighter;
return HighlightStyle.define([ return HighlightStyle.define([
{ tag: t.heading1, class: "sb-h1" }, { tag: t.heading1, class: "sb-h1" },
@ -49,8 +48,9 @@ export default function highlightStyles(mdExtension: MDExt[]) {
{ tag: t.processingInstruction, class: "sb-meta" }, { tag: t.processingInstruction, class: "sb-meta" },
{ tag: t.punctuation, class: "sb-punctuation" }, { tag: t.punctuation, class: "sb-punctuation" },
{ tag: ct.HorizontalRuleTag, class: "sb-hr" }, { tag: ct.HorizontalRuleTag, class: "sb-hr" },
...mdExtension.map((mdExt) => { { tag: ct.HashtagTag, class: "sb-hashtag" },
return { tag: mdExt.tag, ...mdExt.styles, class: mdExt.className }; { tag: ct.NakedURLTag, class: "sb-naked-url" },
}), { tag: ct.TaskDeadlineTag, class: "sb-task-deadline" },
{ tag: ct.NamedAnchorTag, class: "sb-named-anchor" },
]); ]);
} }

View File

@ -317,6 +317,10 @@
font-size: 91%; font-size: 91%;
} }
.sb-task-deadline {
background-color: rgba(22, 22, 22, 0.07)
}
.sb-line-frontmatter-outside, .sb-line-frontmatter-outside,
.sb-line-code-outside { .sb-line-code-outside {
.sb-meta { .sb-meta {

View File

@ -6,7 +6,9 @@ release.
## Edge ## Edge
_The changes below are not yet released “properly”. To them out early, check out [the docs on edge](https://community.silverbullet.md/t/living-on-the-edge-builds/27)._ _The changes below are not yet released “properly”. To them out early, check out [the docs on edge](https://community.silverbullet.md/t/living-on-the-edge-builds/27)._
* Nothing new yet, be patient, but check out 0.6.0 below. * Internal changes:
* Big refactor: of navigation and browser history, fixed some {[Page: Rename]} bugs along the way
* Plugs now can no longer define their own markdown syntax, migrated all plug-specific syntax into the main parser. This should remove a bunch of editor “flashing” especially during sync.
--- ---