diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..13566b81
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..8a9f0c28
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/silverbullet.iml b/.idea/silverbullet.iml
new file mode 100644
index 00000000..5e764c4f
--- /dev/null
+++ b/.idea/silverbullet.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/common/syscalls/markdown.ts b/common/syscalls/markdown.ts
new file mode 100644
index 00000000..57e472be
--- /dev/null
+++ b/common/syscalls/markdown.ts
@@ -0,0 +1,16 @@
+import {SysCallMapping} from "../../plugos/system";
+import {MarkdownTree, nodeAtPos, parse, render} from "../tree";
+
+export function markdownSyscalls(): SysCallMapping {
+ return {
+ parse(ctx, text: string): MarkdownTree {
+ return parse(text);
+ },
+ nodeAtPos(ctx, mdTree: MarkdownTree, pos: number): MarkdownTree | null {
+ return nodeAtPos(mdTree, pos);
+ },
+ render(ctx, mdTree: MarkdownTree): string {
+ return render(mdTree);
+ },
+ };
+}
diff --git a/common/tree.test.ts b/common/tree.test.ts
new file mode 100644
index 00000000..77d1648b
--- /dev/null
+++ b/common/tree.test.ts
@@ -0,0 +1,26 @@
+import {expect, test} from "@jest/globals";
+import {nodeAtPos, parse, render} from "./tree";
+
+const mdTest1 = `
+# Heading
+## Sub _heading_ cool
+
+Hello, this is some **bold** text and *italic*. And [a link](http://zef.me).
+
+- This is a list
+- With another item
+- TODOs:
+ - [ ] A task that's not yet done
+ - [x] Hello
+- And a _third_ one [[Wiki Page]] yo
+`;
+
+test("Run a Node sandbox", async () => {
+ let mdTree = parse(mdTest1);
+ console.log(JSON.stringify(mdTree, null, 2));
+ expect(nodeAtPos(mdTree, 4)!.type).toBe("ATXHeading1");
+ expect(nodeAtPos(mdTree, mdTest1.indexOf("Wiki Page"))!.type).toBe(
+ "WikiLink"
+ );
+ expect(render(mdTree)).toBe(mdTest1);
+});
diff --git a/common/tree.ts b/common/tree.ts
new file mode 100644
index 00000000..8ba2073c
--- /dev/null
+++ b/common/tree.ts
@@ -0,0 +1,115 @@
+import {SyntaxNode} from "@lezer/common";
+import wikiMarkdownLang from "../webapp/parser";
+
+export type MarkdownTree = {
+ type?: string; // undefined === text node
+ from: number;
+ to: number;
+ text?: string;
+ children?: MarkdownTree[];
+ parent?: MarkdownTree;
+};
+
+function treeToAST(text: string, n: SyntaxNode): MarkdownTree {
+ let children: MarkdownTree[] = [];
+ let nodeText: string | undefined;
+ let child = n.firstChild;
+ while (child) {
+ children.push(treeToAST(text, child));
+ child = child.nextSibling;
+ }
+
+ if (children.length === 0) {
+ children = [
+ {
+ from: n.from,
+ to: n.to,
+ text: text.substring(n.from, n.to),
+ },
+ ];
+ } else {
+ let newChildren: MarkdownTree[] | string = [];
+ let index = n.from;
+ for (let child of children) {
+ let s = text.substring(index, child.from);
+ if (s) {
+ newChildren.push({
+ from: index,
+ to: child.from,
+ text: s,
+ });
+ }
+ newChildren.push(child);
+ index = child.to;
+ }
+ let s = text.substring(index, n.to);
+ if (s) {
+ newChildren.push({ from: index, to: n.to, text: s });
+ }
+ children = newChildren;
+ }
+
+ let result: MarkdownTree = {
+ type: n.name,
+ from: n.from,
+ to: n.to,
+ };
+ if (children.length > 0) {
+ result.children = children;
+ }
+ if (nodeText) {
+ result.text = nodeText;
+ }
+ return result;
+}
+
+// Currently unused
+function addParentPointers(mdTree: MarkdownTree) {
+ if (!mdTree.children) {
+ return;
+ }
+ for (let child of mdTree.children) {
+ child.parent = mdTree;
+ addParentPointers(child);
+ }
+}
+
+// Finds non-text node at position
+export function nodeAtPos(
+ mdTree: MarkdownTree,
+ pos: number
+): MarkdownTree | null {
+ if (pos < mdTree.from || pos > mdTree.to) {
+ return null;
+ }
+ if (!mdTree.children) {
+ return mdTree;
+ }
+ for (let child of mdTree.children) {
+ let n = nodeAtPos(child, pos);
+ if (n && n.text) {
+ // Got a text node, let's return its parent
+ return mdTree;
+ } else if (n) {
+ // Got it
+ return n;
+ }
+ }
+ return null;
+}
+
+// Turn MarkdownTree back into regular markdown text
+export function render(mdTree: MarkdownTree): string {
+ let pieces: string[] = [];
+ if (mdTree.text) {
+ return mdTree.text;
+ }
+ for (let child of mdTree.children!) {
+ pieces.push(render(child));
+ }
+ return pieces.join("");
+}
+
+export function parse(text: string): MarkdownTree {
+ return treeToAST(text, wikiMarkdownLang.parser.parse(text).topNode);
+}
diff --git a/package.json b/package.json
index e6725137..1c19d7ab 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
"context": "node"
},
"test": {
- "source": [],
+ "source": ["common/tree.test.ts"],
"outputFormat": "commonjs",
"isLibrary": true,
"context": "node"
@@ -54,6 +54,9 @@
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-solid-svg-icons": "6.0.0",
"@fortawesome/react-fontawesome": "0.1.17",
+ "@codemirror/highlight": "^0.19.0",
+ "@codemirror/language": "^0.19.0",
+ "@lezer/markdown": "^0.15.0",
"@jest/globals": "^27.5.1",
"better-sqlite3": "^7.5.0",
"body-parser": "^1.19.2",
diff --git a/plugos-silverbullet-syscall/markdown.ts b/plugos-silverbullet-syscall/markdown.ts
new file mode 100644
index 00000000..06d223f3
--- /dev/null
+++ b/plugos-silverbullet-syscall/markdown.ts
@@ -0,0 +1,17 @@
+import {syscall} from "./syscall";
+import type {MarkdownTree} from "../common/tree";
+
+export async function parse(text: string): Promise {
+ return syscall("markdown.parse", text);
+}
+
+export async function nodeAtPos(
+ mdTree: MarkdownTree,
+ pos: number
+): Promise {
+ return syscall("markdown.nodeAtPos", mdTree, pos);
+}
+
+export async function render(mdTree: MarkdownTree): Promise {
+ return syscall("markdown.render", mdTree);
+}
diff --git a/plugs/core/materialized_queries.ts b/plugs/core/materialized_queries.ts
index 493c67e0..383d6a2f 100644
--- a/plugs/core/materialized_queries.ts
+++ b/plugs/core/materialized_queries.ts
@@ -1,16 +1,11 @@
-import {
- flashNotification,
- getCurrentPage,
- reloadPage,
- save,
-} from "plugos-silverbullet-syscall/editor";
+import {flashNotification, getCurrentPage, reloadPage, save,} from "plugos-silverbullet-syscall/editor";
-import { readPage, writePage } from "plugos-silverbullet-syscall/space";
-import { invokeFunctionOnServer } from "plugos-silverbullet-syscall/system";
-import { scanPrefixGlobal } from "plugos-silverbullet-syscall";
+import {readPage, writePage} from "plugos-silverbullet-syscall/space";
+import {invokeFunctionOnServer} from "plugos-silverbullet-syscall/system";
+import {scanPrefixGlobal} from "plugos-silverbullet-syscall";
export const queryRegex =
- /()(.+?)()/gs;
+ /()(.+?)()/gs;
export function whiteOutQueries(text: string): string {
return text.replaceAll(queryRegex, (match) =>
diff --git a/plugs/core/navigate.ts b/plugs/core/navigate.ts
index 791ad10e..6192c73d 100644
--- a/plugs/core/navigate.ts
+++ b/plugs/core/navigate.ts
@@ -1,12 +1,14 @@
-import { ClickEvent } from "../../webapp/app_event";
-import { updateMaterializedQueriesCommand } from "./materialized_queries";
+import {ClickEvent} from "../../webapp/app_event";
+import {updateMaterializedQueriesCommand} from "./materialized_queries";
import {
- getSyntaxNodeAtPos,
- getSyntaxNodeUnderCursor,
- navigate as navigateTo,
- openUrl,
+ getSyntaxNodeAtPos,
+ getSyntaxNodeUnderCursor,
+ getText,
+ navigate as navigateTo,
+ openUrl,
} from "plugos-silverbullet-syscall/editor";
-import { taskToggleAtPos } from "../tasks/task";
+import {taskToggleAtPos} from "../tasks/task";
+import {nodeAtPos, parse} from "plugos-silverbullet-syscall/markdown";
const materializedQueryPrefix = /