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 = /