Migrate to Deno (#86)

Big bang migration to Deno 🤯
This commit is contained in:
Zef Hemel
2022-10-10 14:50:21 +02:00
committed by GitHub
parent 78f83c70d8
commit 561aa6891f
287 changed files with 4577 additions and 25087 deletions
+49
View File
@@ -0,0 +1,49 @@
import { collectNodesOfType } from "../../common/tree.ts";
import {
batchSet,
queryPrefix,
} from "../../syscall/silverbullet-syscall/index.ts";
import {
getCurrentPage,
matchBefore,
} from "../../syscall/silverbullet-syscall/editor.ts";
import type { IndexTreeEvent } from "../../web/app_event.ts";
import { removeQueries } from "../query/util.ts";
// Key space
// a:pageName:anchorName => pos
export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) {
removeQueries(tree);
let anchors: { key: string; value: string }[] = [];
collectNodesOfType(tree, "NamedAnchor").forEach((n) => {
let aName = n.children![0].text!;
anchors.push({
key: `a:${pageName}:${aName}`,
value: "" + n.from,
});
});
console.log("Found", anchors.length, "anchors(s)");
await batchSet(pageName, anchors);
}
export async function anchorComplete() {
let prefix = await matchBefore("\\[\\[[^\\]@:]*@[\\w\\.\\-\\/]*");
if (!prefix) {
return null;
}
const [pageRefPrefix, anchorRef] = prefix.text.split("@");
let pageRef = pageRefPrefix.substring(2);
if (!pageRef) {
pageRef = await getCurrentPage();
}
let allAnchors = await queryPrefix(`a:${pageRef}:@${anchorRef}`);
return {
from: prefix.from + pageRefPrefix.length + 1,
options: allAnchors.map((a) => ({
label: a.key.split("@")[1],
type: "anchor",
})),
};
}
+78
View File
@@ -0,0 +1,78 @@
import type {
FileData,
FileEncoding,
} from "../../common/spaces/space_primitives.ts";
import { renderToText, replaceNodesMatching } from "../../common/tree.ts";
import type { FileMeta } from "../../common/types.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
const pagePrefix = "💭 ";
export async function readFileCloud(
name: string,
encoding: FileEncoding,
): Promise<{ data: FileData; meta: FileMeta } | undefined> {
let originalUrl = name.substring(
pagePrefix.length,
name.length - ".md".length,
);
let url = originalUrl;
if (!url.includes("/")) {
url += "/index";
}
if (!url.startsWith("127.0.0.1")) {
url = `https://${url}`;
} else {
url = `http://${url}`;
}
let text = "";
try {
let r = await fetch(`${url}.md`);
text = await r.text();
if (r.status !== 200) {
text = `ERROR: ${text}`;
}
} catch (e: any) {
console.error("ERROR", e.message);
text = e.message;
}
return {
data: await translateLinksWithPrefix(
text,
`${pagePrefix}${originalUrl.split("/")[0]}/`,
),
meta: {
name,
contentType: "text/markdown",
lastModified: 0,
size: text.length,
perm: "ro",
},
};
}
async function translateLinksWithPrefix(
text: string,
prefix: string,
): Promise<string> {
let tree = await parseMarkdown(text);
replaceNodesMatching(tree, (tree) => {
if (tree.type === "WikiLinkPage") {
// Add the prefix in the link text
tree.children![0].text = prefix + tree.children![0].text;
}
return undefined;
});
text = renderToText(tree);
return text;
}
export async function getFileMetaCloud(name: string): Promise<FileMeta> {
return {
name,
size: 0,
contentType: "text/markdown",
lastModified: 0,
perm: "ro",
};
}
+18
View File
@@ -0,0 +1,18 @@
import { matchBefore } from "../../syscall/silverbullet-syscall/editor.ts";
import { listCommands } from "../../syscall/silverbullet-syscall/system.ts";
export async function commandComplete() {
let prefix = await matchBefore("\\{\\[[^\\]]*");
if (!prefix) {
return null;
}
let allCommands = await listCommands();
return {
from: prefix.from + 2,
options: Object.keys(allCommands).map((commandName) => ({
label: commandName,
type: "command",
})),
};
}
+388
View File
@@ -0,0 +1,388 @@
name: core
syntax:
Hashtag:
firstCharacters:
- "#"
regex: "#[^#\\d\\s]+\\w+"
className: sb-hashtag
NakedURL:
firstCharacters:
- "h"
regex: "https?:\\/\\/[-a-zA-Z0-9@:%._\\+~#=]{1,256}([-a-zA-Z0-9()@:%_\\+.~#?&=\\/]*)"
className: sb-naked-url
CommandLink:
firstCharacters:
- "{"
regex: "\\{\\[[^\\]]+\\]\\}"
className: sb-command-link
NamedAnchor:
firstCharacters:
- "@"
regex: "@[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*"
className: sb-named-anchor
functions:
clearPageIndex:
path: "./page.ts:clearPageIndex"
env: server
events:
- page:saved
- page:deleted
pageQueryProvider:
path: ./page.ts:pageQueryProvider
events:
- query:page
parseIndexTextRepublish:
path: "./page.ts:parseIndexTextRepublish"
events:
- page:index_text
reindexSpaceCommand:
path: "./page.ts:reindexCommand"
command:
name: "Space: Reindex"
reindexSpace:
path: "./page.ts:reindexSpace"
env: server
deletePage:
path: "./page.ts:deletePage"
command:
name: "Page: Delete"
editorLoad:
path: "./editor.ts:editorLoad"
events:
- plugs:loaded
toggleReadOnlyode:
path: "./editor.ts:toggleReadOnlyMode"
command:
name: "Editor: Toggle Read Only Mode"
# Backlinks
indexLinks:
path: "./page.ts:indexLinks"
events:
- page:index
linkQueryProvider:
path: ./page.ts:linkQueryProvider
events:
- query:link
renamePage:
path: "./page.ts:renamePage"
command:
name: "Page: Rename"
mac: Cmd-Alt-r
key: Ctrl-Alt-r
pageComplete:
path: "./page.ts:pageComplete"
events:
- page:complete
# Commands
commandComplete:
path: "./command.ts:commandComplete"
events:
- page:complete
# Item indexing
indexItem:
path: "./item.ts:indexItems"
events:
- page:index
itemQueryProvider:
path: "./item.ts:queryProvider"
events:
- query:item
# Navigation
linkNavigate:
path: "./navigate.ts:linkNavigate"
command:
name: Navigate To page
key: Ctrl-Enter
mac: Cmd-Enter
clickNavigate:
path: "./navigate.ts:clickNavigate"
events:
- page:click
navigateHome:
path: "./navigate.ts:navigateCommand"
command:
name: "Navigate: Home"
key: "Alt-h"
page: ""
# Hashtags
indexTags:
path: "./tags.ts:indexTags"
events:
- page:index
tagComplete:
path: "./tags.ts:tagComplete"
events:
- page:complete
tagProvider:
path: "./tags.ts:tagProvider"
events:
- query:tag
# Anchors
indexAnchors:
path: "./anchor.ts:indexAnchors"
events:
- page:index
anchorComplete:
path: "./anchor.ts:anchorComplete"
events:
- page:complete
# Full text search
# searchIndex:
# path: ./search.ts:index
# events:
# - page:index
# searchUnindex:
# path: "./search.ts:unindex"
# env: server
# events:
# - page:deleted
searchQueryProvider:
path: ./search.ts:queryProvider
events:
- query:full-text
searchCommand:
path: ./search.ts:searchCommand
command:
name: "Search Space"
key: Ctrl-Shift-f
mac: Cmd-Shift-f
readPageSearch:
path: ./search.ts:readPageSearch
pageNamespace:
pattern: "🔍 .+"
operation: readFile
getPageMetaSearch:
path: ./search.ts:getPageMetaSearch
pageNamespace:
pattern: "🔍 .+"
operation: getFileMeta
# Template commands
insertPageMeta:
path: "./template.ts:insertTemplateText"
slashCommand:
name: meta
description: Insert a page metadata block
value: |
```meta
|^|
```
insertTask:
path: "./template.ts:insertTemplateText"
slashCommand:
name: task
description: Insert a task
value: "* [ ] |^|"
insertQuery:
path: "./template.ts:insertTemplateText"
slashCommand:
name: query
description: Insert a query
value: |
<!-- #query |^| -->
<!-- /query -->
insertInclude:
path: "./template.ts:insertTemplateText"
slashCommand:
name: include
description: Include another page
value: |
<!-- #include [[|^|]] -->
<!-- /include -->
insertInjectTemplate:
path: "./template.ts:insertTemplateText"
slashCommand:
name: use
description: Use a template
value: |
<!-- #use [[|^|]] {} -->
<!-- /use -->
insertInjectCleanTemplate:
path: "./template.ts:insertTemplateText"
slashCommand:
name: use-verbose
description: Use a template (verbose mode)
value: |
<!-- #use-verbose [[|^|]] {} -->
<!-- /use-verbose -->
insertHRTemplate:
path: "./template.ts:insertTemplateText"
slashCommand:
name: hr
description: Insert a horizontal rule
value: "---"
quickNoteCommand:
path: ./template.ts:quickNoteCommand
command:
name: "Quick Note"
key: "Alt-Shift-n"
priority: 1
dailyNoteCommand:
path: ./template.ts:dailyNoteCommand
command:
name: "Open Daily Note"
key: "Alt-Shift-d"
instantiateTemplateCommand:
path: ./template.ts:instantiateTemplateCommand
command:
name: "Template: Instantiate Page"
insertSnippet:
path: ./template.ts:insertSnippet
command:
name: "Template: Insert Snippet"
slashCommand:
name: snippet
description: Insert a snippet
insertTodayCommand:
path: "./template.ts:insertTemplateText"
slashCommand:
name: today
description: Insert today's date
value: "{{today}}"
insertTomorrowCommand:
path: "./template.ts:insertTemplateText"
slashCommand:
name: tomorrow
description: Insert tomorrow's date
value: "{{tomorrow}}"
# Text editing commands
quoteSelectionCommand:
path: ./text.ts:quoteSelection
command:
name: "Text: Quote Selection"
key: "Ctrl-Shift-."
mac: "Cmd-Shift-."
listifySelection:
path: ./text.ts:listifySelection
command:
name: "Text: Listify Selection"
numberListifySelection:
path: ./text.ts:numberListifySelection
command:
name: "Text: Number Listify Selection"
linkSelection:
path: ./text.ts:linkSelection
command:
name: "Text: Link Selection"
key: "Ctrl-Shift-k"
mac: "Cmd-Shift-k"
bold:
path: ./text.ts:wrapSelection
command:
name: "Text: Bold"
key: "Ctrl-b"
mac: "Cmd-b"
wrapper: "**"
italic:
path: ./text.ts:wrapSelection
command:
name: "Text: Italic"
key: "Ctrl-i"
mac: "Cmd-i"
wrapper: "_"
marker:
path: ./text.ts:wrapSelection
command:
name: "Text: Marker"
key: "Alt-m"
wrapper: "=="
# Plug manager
updatePlugsCommand:
path: ./plugmanager.ts:updatePlugsCommand
command:
name: "Plugs: Update"
key: "Ctrl-Shift-p"
mac: "Cmd-Shift-p"
updatePlugs:
path: ./plugmanager.ts:updatePlugs
env: server
getPlugHTTPS:
path: "./plugmanager.ts:getPlugHTTPS"
events:
- get-plug:https
getPlugGithub:
path: "./plugmanager.ts:getPlugGithub"
events:
- get-plug:github
getPlugGithubRelease:
path: "./plugmanager.ts:getPlugGithubRelease"
events:
- get-plug:ghr
# Debug commands
parseCommand:
path: ./debug.ts:parsePageCommand
command:
name: "Debug: Parse Document"
showLogsCommand:
path: ./debug.ts:showLogsCommand
command:
name: "Debug: Show Logs"
key: "Ctrl-Alt-l"
mac: "Cmd-Alt-l"
events:
- log:reload
hideBhsCommand:
path: ./debug.ts:hideBhsCommand
command:
name: "UI: Hide BHS"
key: "Ctrl-Alt-b"
mac: "Cmd-Alt-b"
# Link unfurl infrastructure
unfurlLink:
path: ./link.ts:unfurlCommand
command:
name: "Link: Unfurl"
key: "Ctrl-Shift-u"
mac: "Cmd-Shift-u"
contexts:
- NakedURL
unfurlExec:
env: server
path: ./link.ts:unfurlExec
# Title-based link unfurl
titleUnfurlOptions:
path: ./link.ts:titleUnfurlOptions
events:
- unfurl:options
titleUnfurl:
path: ./link.ts:titleUnfurl
events:
- unfurl:title-unfurl
# Random stuff
statsCommand:
path: ./stats.ts:statsCommand
command:
name: "Stats: Show"
key: "Ctrl-s"
mac: "Cmd-s"
# Cloud pages
readPageCloud:
path: ./cloud.ts:readFileCloud
pageNamespace:
pattern: "💭 .+"
operation: readFile
getPageMetaCloud:
path: ./cloud.ts:getFileMetaCloud
pageNamespace:
pattern: "💭 .+"
operation: getFileMeta
+11
View File
@@ -0,0 +1,11 @@
export function niceDate(d: Date): string {
function pad(n: number) {
let s = String(n);
if (s.length === 1) {
s = '0' + s;
}
return s;
}
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
}
+83
View File
@@ -0,0 +1,83 @@
import { getLogs } from "../../syscall/plugos-syscall/sandbox.ts";
import {
getText,
hidePanel,
showPanel,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
import { getServerLogs } from "../../syscall/silverbullet-syscall/sandbox.ts";
export async function parsePageCommand() {
console.log(
"AST",
JSON.stringify(await parseMarkdown(await getText()), null, 2)
);
}
export async function showLogsCommand() {
let clientLogs = await getLogs();
let serverLogs = await getServerLogs();
await showPanel(
"bhs",
1,
`
<style>
#client-log-header {
position: absolute;
left: 0;
top: 5px;
}
#server-log-header {
position: absolute;
right: 0;
top: 5px;
width: 50%;
}
#client-log {
position: absolute;
left: 0;
top: 30px;
bottom: 0;
width: 50%;
overflow: scroll;
}
#server-log {
position: absolute;
right: 0;
top: 30px;
bottom: 0;
width: 50%;
overflow: scroll;
}
</style>
<div id="client-log-header">Client logs (max 100)</div>
<div id="client-log">
<pre>${clientLogs
.map((le) => `[${le.level}] ${le.message}`)
.join("\n")}</pre>
</div>
<div id="server-log-header">Server logs (max 100)</div>
<div id="server-log">
<pre>${serverLogs
.map((le) => `[${le.level}] ${le.message}`)
.join("\n")}</pre>
</div>`,
`
var clientDiv = document.getElementById("client-log");
clientDiv.scrollTop = clientDiv.scrollHeight;
var serverDiv = document.getElementById("server-log");
serverDiv.scrollTop = serverDiv.scrollHeight;
if(window.reloadInterval) {
clearInterval(window.reloadInterval);
}
window.reloadInterval = setInterval(() => {
sendEvent("log:reload");
}, 1000);
`
);
}
export async function hideBhsCommand() {
await hidePanel("bhs");
}
+16
View File
@@ -0,0 +1,16 @@
import * as clientStore from "../../syscall/silverbullet-syscall/clientStore.ts";
import { enableReadOnlyMode } from "../../syscall/silverbullet-syscall/editor.ts";
export async function editorLoad() {
let readOnlyMode = await clientStore.get("readOnlyMode");
if (readOnlyMode) {
await enableReadOnlyMode(true);
}
}
export async function toggleReadOnlyMode() {
let readOnlyMode = await clientStore.get("readOnlyMode");
readOnlyMode = !readOnlyMode;
await enableReadOnlyMode(readOnlyMode);
await clientStore.set("readOnlyMode", readOnlyMode);
}
+87
View File
@@ -0,0 +1,87 @@
import type { IndexTreeEvent } from "../../web/app_event.ts";
import {
batchSet,
queryPrefix,
} from "../../syscall/silverbullet-syscall/index.ts";
import {
collectNodesOfType,
ParseTree,
renderToText,
} from "../../common/tree.ts";
import { removeQueries } from "../query/util.ts";
import { applyQuery, QueryProviderEvent } from "../query/engine.ts";
export type Item = {
name: string;
nested?: string;
tags?: string[];
// Not stored in DB
page?: string;
pos?: number;
};
export async function indexItems({ name, tree }: IndexTreeEvent) {
let items: { key: string; value: Item }[] = [];
removeQueries(tree);
console.log("Indexing items", name);
let coll = collectNodesOfType(tree, "ListItem");
coll.forEach((n) => {
if (!n.children) {
return;
}
if (collectNodesOfType(n, "Task").length > 0) {
// This is a task item, skip it
return;
}
let textNodes: ParseTree[] = [];
let nested: string | undefined;
for (let child of n.children!.slice(1)) {
if (child.type === "OrderedList" || child.type === "BulletList") {
nested = renderToText(child);
break;
}
textNodes.push(child);
}
let itemText = textNodes.map(renderToText).join("").trim();
let item: Item = {
name: itemText,
};
if (nested) {
item.nested = nested;
}
collectNodesOfType(n, "Hashtag").forEach((h) => {
if (!item.tags) {
item.tags = [];
}
item.tags.push(h.children![0].text!);
});
items.push({
key: `it:${n.from}`,
value: item,
});
});
console.log("Found", items.length, "item(s)");
await batchSet(name, items);
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<any[]> {
let allItems: Item[] = [];
for (let { key, page, value } of await queryPrefix("it:")) {
let [, pos] = key.split(":");
allItems.push({
...value,
page: page,
pos: +pos,
});
}
return applyQuery(query, allItems);
}
+83
View File
@@ -0,0 +1,83 @@
import { nodeAtPos } from "../../common/tree.ts";
import {
filterBox,
flashNotification,
getCursor,
getText,
replaceRange,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
import { dispatch as dispatchEvent } from "../../syscall/plugos-syscall/event.ts";
import { invokeFunction } from "../../syscall/silverbullet-syscall/system.ts";
type UnfurlOption = {
id: string;
name: string;
};
export async function unfurlCommand() {
let mdTree = await parseMarkdown(await getText());
let nakedUrlNode = nodeAtPos(mdTree, await getCursor());
let url = nakedUrlNode!.children![0].text!;
console.log("Got URL to unfurl", url);
let optionResponses = await dispatchEvent("unfurl:options", url);
let options: UnfurlOption[] = [];
for (let resp of optionResponses) {
options.push(...resp);
}
let selectedUnfurl: any = await filterBox(
"Unfurl",
options,
"Select the unfurl strategy of your choice"
);
if (!selectedUnfurl) {
return;
}
try {
let replacement = await invokeFunction(
"server",
"unfurlExec",
selectedUnfurl.id,
url
);
await replaceRange(nakedUrlNode?.from!, nakedUrlNode?.to!, replacement);
} catch (e: any) {
await flashNotification(e.message, "error");
}
}
export async function titleUnfurlOptions(url: string): Promise<UnfurlOption[]> {
return [
{
id: "title-unfurl",
name: "Extract title",
},
];
}
// Run on the server because plugs will likely rely on fetch for this
export async function unfurlExec(id: string, url: string): Promise<string> {
let replacement = await dispatchEvent(`unfurl:${id}`, url);
if (replacement.length === 0) {
throw new Error("Unfurl failed");
} else {
return replacement[0];
}
}
const titleRegex = /<title[^>]*>\s*([^<]+)\s*<\/title\s*>/i;
export async function titleUnfurl(url: string): Promise<string> {
let response = await fetch(url);
if (response.status < 200 || response.status >= 300) {
console.error("Unfurl failed", await response.text());
throw new Error(`Failed to fetch: ${await response.statusText}`);
}
let body = await response.text();
let match = titleRegex.exec(body);
if (match) {
return `[${match[1]}](${url})`;
} else {
throw new Error("No title found");
}
}
+84
View File
@@ -0,0 +1,84 @@
import type { ClickEvent } from "../../web/app_event.ts";
import {
flashNotification,
getCurrentPage,
getCursor,
getText,
navigate as navigateTo,
openUrl,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
import { nodeAtPos, ParseTree } from "../../common/tree.ts";
import { invokeCommand } from "../../syscall/silverbullet-syscall/system.ts";
// Checks if the URL contains a protocol, if so keeps it, otherwise assumes an attachment
function patchUrl(url: string): string {
if (url.indexOf("://") === -1) {
return `fs/${url}`;
}
return url;
}
async function actionClickOrActionEnter(mdTree: ParseTree | null) {
if (!mdTree) {
return;
}
// console.log("Attempting to navigate based on syntax node", mdTree);
switch (mdTree.type) {
case "WikiLinkPage": {
let pageLink = mdTree.children![0].text!;
let pos;
if (pageLink.includes("@")) {
[pageLink, pos] = pageLink.split("@");
if (pos.match(/^\d+$/)) {
pos = +pos;
}
}
if (!pageLink) {
pageLink = await getCurrentPage();
}
await navigateTo(pageLink, pos);
break;
}
case "URL":
case "NakedURL":
await openUrl(patchUrl(mdTree.children![0].text!));
break;
case "Link": {
const url = patchUrl(mdTree.children![4].children![0].text!);
if (url.length <= 1) {
return flashNotification("Empty link, ignoring", "error");
}
await openUrl(url);
break;
}
case "CommandLink": {
const command = mdTree
.children![0].text!.substring(2, mdTree.children![0].text!.length - 2)
.trim();
console.log("Got command link", command);
await invokeCommand(command);
break;
}
}
}
export async function linkNavigate() {
const mdTree = await parseMarkdown(await getText());
const newNode = nodeAtPos(mdTree, await getCursor());
await actionClickOrActionEnter(newNode);
}
export async function clickNavigate(event: ClickEvent) {
// Navigate by default, don't navigate when Ctrl or Cmd is held
if (event.ctrlKey || event.metaKey) {
return;
}
const mdTree = await parseMarkdown(await getText());
const newNode = nodeAtPos(mdTree, event.pos);
await actionClickOrActionEnter(newNode);
}
export async function navigateCommand(cmdDef: any) {
await navigateTo(cmdDef.page);
}
+248
View File
@@ -0,0 +1,248 @@
import type { IndexEvent, IndexTreeEvent } from "../../web/app_event.ts";
import {
batchSet,
clearPageIndex as clearPageIndexSyscall,
clearPageIndexForPage,
queryPrefix,
set,
} from "../../syscall/silverbullet-syscall/index.ts";
import {
flashNotification,
getCurrentPage,
getCursor,
getText,
matchBefore,
navigate,
prompt,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { dispatch } from "../../syscall/plugos-syscall/event.ts";
import {
deletePage as deletePageSyscall,
listPages,
readPage,
writePage,
} from "../../syscall/silverbullet-syscall/space.ts";
import { invokeFunction } from "../../syscall/silverbullet-syscall/system.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
import {
addParentPointers,
collectNodesMatching,
ParseTree,
renderToText,
replaceNodesMatching,
} from "../../common/tree.ts";
import { applyQuery, QueryProviderEvent } from "../query/engine.ts";
import { extractMeta } from "../query/data.ts";
// Key space:
// pl:toPage:pos => pageName
// meta => metaJson
export async function indexLinks({ name, tree }: IndexTreeEvent) {
let backLinks: { key: string; value: string }[] = [];
// [[Style Links]]
console.log("Now indexing", name);
let pageMeta = extractMeta(tree);
if (Object.keys(pageMeta).length > 0) {
console.log("Extracted page meta data", pageMeta);
// Don't index meta data starting with $
for (let key in pageMeta) {
if (key.startsWith("$")) {
delete pageMeta[key];
}
}
await set(name, "meta:", pageMeta);
}
collectNodesMatching(tree, (n) => n.type === "WikiLinkPage").forEach((n) => {
let toPage = n.children![0].text!;
if (toPage.includes("@")) {
toPage = toPage.split("@")[0];
}
backLinks.push({
key: `pl:${toPage}:${n.from}`,
value: name,
});
});
console.log("Found", backLinks.length, "wiki link(s)");
await batchSet(name, backLinks);
}
export async function pageQueryProvider({
query,
}: QueryProviderEvent): Promise<any[]> {
let allPages = await listPages();
let allPageMap: Map<string, any> = new Map(
allPages.map((pm) => [pm.name, pm])
);
for (let { page, value } of await queryPrefix("meta:")) {
let p = allPageMap.get(page);
if (p) {
for (let [k, v] of Object.entries(value)) {
p[k] = v;
}
}
}
allPages = [...allPageMap.values()];
return applyQuery(query, allPages);
}
export async function linkQueryProvider({
query,
pageName,
}: QueryProviderEvent): Promise<any[]> {
let links: any[] = [];
for (let { value: name, key } of await queryPrefix(`pl:${pageName}:`)) {
const [, , pos] = key.split(":"); // Key: pl:page:pos
links.push({ name, pos });
}
return applyQuery(query, links);
}
export async function deletePage() {
let pageName = await getCurrentPage();
console.log("Navigating to index page");
await navigate("");
console.log("Deleting page from space");
await deletePageSyscall(pageName);
}
export async function renamePage() {
const oldName = await getCurrentPage();
const cursor = await getCursor();
console.log("Old name is", oldName);
const newName = await prompt(`Rename ${oldName} to:`, oldName);
if (!newName) {
return;
}
if (newName.trim() === oldName.trim()) {
return;
}
console.log("New name", newName);
let pagesToUpdate = await getBackLinks(oldName);
console.log("All pages containing backlinks", pagesToUpdate);
let text = await getText();
console.log("Writing new page to space");
await writePage(newName, text);
console.log("Navigating to new page");
await navigate(newName, cursor, true);
console.log("Deleting page from space");
await deletePageSyscall(oldName);
let pageToUpdateSet = new Set<string>();
for (let pageToUpdate of pagesToUpdate) {
pageToUpdateSet.add(pageToUpdate.page);
}
for (let pageToUpdate of pageToUpdateSet) {
if (pageToUpdate === oldName) {
continue;
}
console.log("Now going to update links in", pageToUpdate);
let { text } = await readPage(pageToUpdate);
// console.log("Received text", text);
if (!text) {
// Page likely does not exist, but at least we can skip it
continue;
}
let mdTree = await parseMarkdown(text);
addParentPointers(mdTree);
replaceNodesMatching(mdTree, (n): ParseTree | undefined | null => {
if (n.type === "WikiLinkPage") {
let pageName = n.children![0].text!;
if (pageName === oldName) {
n.children![0].text = newName;
return n;
}
// page name with @pos position
if (pageName.startsWith(`${oldName}@`)) {
let [, pos] = pageName.split("@");
n.children![0].text = `${newName}@${pos}`;
return n;
}
}
return;
});
// let newText = text.replaceAll(`[[${oldName}]]`, `[[${newName}]]`);
let newText = renderToText(mdTree);
if (text !== newText) {
console.log("Changes made, saving...");
await writePage(pageToUpdate, newText);
}
}
}
type BackLink = {
page: string;
pos: number;
};
async function getBackLinks(pageName: string): Promise<BackLink[]> {
let allBackLinks = await queryPrefix(`pl:${pageName}:`);
let pagesToUpdate: BackLink[] = [];
for (let { key, value } of allBackLinks) {
let keyParts = key.split(":");
pagesToUpdate.push({
page: value,
pos: +keyParts[keyParts.length - 1],
});
}
return pagesToUpdate;
}
export async function reindexCommand() {
await flashNotification("Reindexing...");
await invokeFunction("server", "reindexSpace");
await flashNotification("Reindexing done");
}
// Completion
export async function pageComplete() {
let prefix = await matchBefore("\\[\\[[^\\]@:]*");
if (!prefix) {
return null;
}
let allPages = await listPages();
return {
from: prefix.from + 2,
options: allPages.map((pageMeta) => ({
label: pageMeta.name,
type: "page",
})),
};
}
// Server functions
export async function reindexSpace() {
console.log("Clearing page index...");
await clearPageIndexSyscall();
console.log("Listing all pages");
let pages = await listPages();
for (let { name } of pages) {
console.log("Indexing", name);
const { text } = await readPage(name);
let parsed = await parseMarkdown(text);
await dispatch("page:index", {
name,
tree: parsed,
});
}
console.log("Indexing completed!");
}
export async function clearPageIndex(page: string) {
console.log("Clearing page index for page", page);
await clearPageIndexForPage(page);
}
export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
await dispatch("page:index", {
name,
tree: await parseMarkdown(text),
});
}
+120
View File
@@ -0,0 +1,120 @@
import { dispatch } from "../../syscall/plugos-syscall/event.ts";
import { Manifest } from "../../common/manifest.ts";
import {
flashNotification,
save,
} from "../../syscall/silverbullet-syscall/editor.ts";
import {
deleteAttachment,
listPlugs,
writeAttachment,
} from "../../syscall/silverbullet-syscall/space.ts";
import {
invokeFunction,
reloadPlugs,
} from "../../syscall/silverbullet-syscall/system.ts";
import { readYamlPage } from "../lib/yaml_page.ts";
export async function updatePlugsCommand() {
await save();
flashNotification("Updating plugs...");
try {
await invokeFunction("server", "updatePlugs");
flashNotification("And... done!");
await reloadPlugs();
} catch (e: any) {
flashNotification("Error updating plugs: " + e.message, "error");
}
}
export async function updatePlugs() {
let plugList: string[] = [];
try {
const plugListRead: any[] = await readYamlPage("PLUGS");
plugList = plugListRead.filter((plug) => typeof plug === "string");
if (plugList.length !== plugListRead.length) {
throw new Error(
`Some of the plugs were not in a yaml list format, they were ignored`
);
}
} catch (e: any) {
throw new Error(`Error processing PLUGS: ${e.message}`);
}
console.log("Plug YAML", plugList);
let allPlugNames: string[] = [];
for (let plugUri of plugList) {
let [protocol, ...rest] = plugUri.split(":");
let manifests = await dispatch(`get-plug:${protocol}`, rest.join(":"));
if (manifests.length === 0) {
console.error("Could not resolve plug", plugUri);
}
// console.log("Got manifests", plugUri, protocol, manifests);
let manifest = manifests[0];
allPlugNames.push(manifest.name);
// console.log("Writing", `_plug/${manifest.name}`);
await writeAttachment(
`_plug/${manifest.name}.plug.json`,
"string",
JSON.stringify(manifest)
);
}
// And delete extra ones
for (let existingPlug of await listPlugs()) {
let plugName = existingPlug.substring(
"_plug/".length,
existingPlug.length - ".plug.json".length
);
console.log("Considering", plugName);
if (!allPlugNames.includes(plugName)) {
console.log("Removing plug", plugName);
await deleteAttachment(existingPlug);
}
}
await reloadPlugs();
}
export async function getPlugHTTPS(url: string): Promise<Manifest> {
let fullUrl = `https:${url}`;
console.log("Now fetching plug manifest from", fullUrl);
let req = await fetch(fullUrl);
if (req.status !== 200) {
throw new Error(`Could not fetch plug manifest from ${fullUrl}`);
}
let json = await req.json();
return json;
}
export async function getPlugGithub(identifier: string): Promise<Manifest> {
let [owner, repo, path] = identifier.split("/");
let [repoClean, branch] = repo.split("@");
if (!branch) {
branch = "main"; // or "master"?
}
return getPlugHTTPS(
`//raw.githubusercontent.com/${owner}/${repoClean}/${branch}/${path}`
);
}
export async function getPlugGithubRelease(
identifier: string
): Promise<Manifest> {
let [owner, repo, version] = identifier.split("/");
if (!version || version === "latest") {
console.log("fetching the latest version");
const req = await fetch(
`https://api.github.com/repos/${owner}/${repo}/releases/latest`
);
if (req.status !== 200) {
throw new Error(
`Could not fetch latest relase manifest from ${identifier}}`
);
}
const result = await req.json();
version = result.name;
}
const finalUrl = `//github.com/${owner}/${repo}/releases/download/${version}/${repo}.plug.json`;
return getPlugHTTPS(finalUrl);
}
+86
View File
@@ -0,0 +1,86 @@
import {
fullTextDelete,
fullTextIndex,
fullTextSearch,
} from "../../syscall/plugos-syscall/fulltext.ts";
import { renderToText } from "../../common/tree.ts";
import { PageMeta } from "../../common/types.ts";
import { queryPrefix } from "../../syscall/silverbullet-syscall/index.ts";
import { navigate, prompt } from "../../syscall/silverbullet-syscall/editor.ts";
import { IndexTreeEvent } from "../../web/app_event.ts";
import { applyQuery, QueryProviderEvent } from "../query/engine.ts";
import { removeQueries } from "../query/util.ts";
const searchPrefix = "🔍 ";
export async function index(data: IndexTreeEvent) {
removeQueries(data.tree);
let cleanText = renderToText(data.tree);
await fullTextIndex(data.name, cleanText);
}
export async function unindex(pageName: string) {
await fullTextDelete(pageName);
}
export async function queryProvider({
query,
}: QueryProviderEvent): Promise<any[]> {
let phraseFilter = query.filter.find((f) => f.prop === "phrase");
if (!phraseFilter) {
throw Error("No 'phrase' filter specified, this is mandatory");
}
let results = await fullTextSearch(phraseFilter.value, 100);
let allPageMap: Map<string, any> = new Map(
results.map((r: any) => [r.name, r])
);
for (let { page, value } of await queryPrefix("meta:")) {
let p = allPageMap.get(page);
if (p) {
for (let [k, v] of Object.entries(value)) {
p[k] = v;
}
}
}
// Remove the "phrase" filter
query.filter.splice(query.filter.indexOf(phraseFilter), 1);
results = applyQuery(query, results);
return results;
}
export async function searchCommand() {
let phrase = await prompt("Search for: ");
if (phrase) {
await navigate(`${searchPrefix}${phrase}`);
}
}
export async function readPageSearch(
name: string
): Promise<{ text: string; meta: PageMeta }> {
let phrase = name.substring(searchPrefix.length);
let results = await fullTextSearch(phrase, 100);
const text = `# Search results for "${phrase}"\n${results
.map((r: any) => `* [[${r.name}]] (score: ${r.rank})`)
.join("\n")}
`;
return {
text: text,
meta: {
name,
lastModified: 0,
perm: "ro",
},
};
}
export async function getPageMetaSearch(name: string): Promise<PageMeta> {
return {
name,
lastModified: 0,
perm: "ro",
};
}
+25
View File
@@ -0,0 +1,25 @@
import {
flashNotification,
getText,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { listPages } from "../../syscall/silverbullet-syscall/space.ts";
function countWords(str: string): number {
const matches = str.match(/[\w\d\'-]+/gi);
return matches ? matches.length : 0;
}
function readingTime(wordCount: number): number {
// 225 is average word reading speed for adults
return Math.ceil(wordCount / 225);
}
export async function statsCommand() {
const text = await getText();
const allPages = await listPages();
const wordCount = countWords(text);
const time = readingTime(wordCount);
await flashNotification(
`${wordCount} words; ${time} minutes read; ${allPages.length} total pages in space.`
);
}
+63
View File
@@ -0,0 +1,63 @@
import { collectNodesOfType } from "../../common/tree.ts";
import {
batchSet,
queryPrefix,
} from "../../syscall/silverbullet-syscall/index.ts";
import { matchBefore } from "../../syscall/silverbullet-syscall/editor.ts";
import type { IndexTreeEvent } from "../../web/app_event.ts";
import { applyQuery, QueryProviderEvent } from "../query/engine.ts";
import { removeQueries } from "../query/util.ts";
// Key space
// tag:TAG => true (for completion)
export async function indexTags({ name, tree }: IndexTreeEvent) {
removeQueries(tree);
let allTags = new Set<string>();
collectNodesOfType(tree, "Hashtag").forEach((n) => {
allTags.add(n.children![0].text!);
});
batchSet(
name,
[...allTags].map((t) => ({ key: `tag:${t}`, value: t }))
);
}
export async function tagComplete() {
let prefix = await matchBefore("#[^#\\s]+");
// console.log("Running tag complete", prefix);
if (!prefix) {
return null;
}
let allTags = await queryPrefix(`tag:${prefix.text}`);
return {
from: prefix.from,
options: allTags.map((tag) => ({
label: tag.value,
type: "tag",
})),
};
}
type Tag = {
name: string;
freq: number;
};
export async function tagProvider({ query }: QueryProviderEvent) {
let allTags = new Map<string, number>();
for (let { value } of await queryPrefix("tag:")) {
let currentFreq = allTags.get(value);
if (!currentFreq) {
currentFreq = 0;
}
allTags.set(value, currentFreq + 1);
}
return applyQuery(
query,
[...allTags.entries()].map(([name, freq]) => ({
name,
freq,
}))
);
}
+175
View File
@@ -0,0 +1,175 @@
import {
getPageMeta,
listPages,
readPage,
writePage,
} from "$sb/silverbullet-syscall/space.ts";
import {
filterBox,
getCurrentPage,
getCursor,
insertAtCursor,
moveCursor,
navigate,
prompt,
} from "../../syscall/silverbullet-syscall/editor.ts";
import { parseMarkdown } from "../../syscall/silverbullet-syscall/markdown.ts";
import { extractMeta } from "../query/data.ts";
import { renderToText } from "../../common/tree.ts";
import { niceDate } from "./dates.ts";
import { readSettings } from "../lib/settings_page.ts";
export async function instantiateTemplateCommand() {
const allPages = await listPages();
const { pageTemplatePrefix } = await readSettings({
pageTemplatePrefix: "template/page/",
});
const selectedTemplate = await filterBox(
"Template",
allPages
.filter((pageMeta) => pageMeta.name.startsWith(pageTemplatePrefix))
.map((pageMeta) => ({
...pageMeta,
name: pageMeta.name.slice(pageTemplatePrefix.length),
})),
`Select the template to create a new page from (listing any page starting with <tt>${pageTemplatePrefix}</tt>)`,
);
if (!selectedTemplate) {
return;
}
console.log("Selected template", selectedTemplate);
const { text } = await readPage(
`${pageTemplatePrefix}${selectedTemplate.name}`,
);
const parseTree = await parseMarkdown(text);
const additionalPageMeta = extractMeta(parseTree, [
"$name",
"$disableDirectives",
]);
const pageName = await prompt("Name of new page", additionalPageMeta.$name);
if (!pageName) {
return;
}
const pageText = replaceTemplateVars(renderToText(parseTree), pageName);
await writePage(pageName, pageText);
await navigate(pageName);
}
export async function insertSnippet() {
let allPages = await listPages();
let { snippetPrefix } = await readSettings({
snippetPrefix: "snippet/",
});
let cursorPos = await getCursor();
let page = await getCurrentPage();
let allSnippets = allPages
.filter((pageMeta) => pageMeta.name.startsWith(snippetPrefix))
.map((pageMeta) => ({
...pageMeta,
name: pageMeta.name.slice(snippetPrefix.length),
}));
let selectedSnippet = await filterBox(
"Snippet",
allSnippets,
`Select the snippet to insert (listing any page starting with <tt>${snippetPrefix}</tt>)`,
);
if (!selectedSnippet) {
return;
}
let { text } = await readPage(`${snippetPrefix}${selectedSnippet.name}`);
let templateText = replaceTemplateVars(text, page);
let carretPos = templateText.indexOf("|^|");
templateText = templateText.replace("|^|", "");
templateText = replaceTemplateVars(templateText, page);
await insertAtCursor(templateText);
if (carretPos !== -1) {
await moveCursor(cursorPos + carretPos);
}
}
// TODO: This should probably be replaced with handlebards somehow?
export function replaceTemplateVars(s: string, pageName: string): string {
return s.replaceAll(/\{\{([^\}]+)\}\}/g, (match, v) => {
switch (v) {
case "today":
return niceDate(new Date());
case "tomorrow":
let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return niceDate(tomorrow);
case "yesterday":
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return niceDate(yesterday);
case "lastWeek":
let lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
return niceDate(lastWeek);
case "page":
return pageName;
}
return match;
});
}
export async function quickNoteCommand() {
let { quickNotePrefix } = await readSettings({
quickNotePrefix: "📥 ",
});
let isoDate = new Date().toISOString();
let [date, time] = isoDate.split("T");
time = time.split(".")[0];
let pageName = `${quickNotePrefix}${date} ${time}`;
await navigate(pageName);
}
export async function dailyNoteCommand() {
let { dailyNoteTemplate, dailyNotePrefix } = await readSettings({
dailyNoteTemplate: "template/page/Daily Note",
dailyNotePrefix: "📅 ",
});
let dailyNoteTemplateText = "";
try {
let { text } = await readPage(dailyNoteTemplate);
dailyNoteTemplateText = text;
} catch {
console.warn(`No daily note template found at ${dailyNoteTemplate}`);
}
let date = niceDate(new Date());
let pageName = `${dailyNotePrefix}${date}`;
if (dailyNoteTemplateText) {
try {
await getPageMeta(pageName);
} catch {
// Doesn't exist, let's create
await writePage(
pageName,
replaceTemplateVars(dailyNoteTemplateText, pageName),
);
}
await navigate(pageName);
} else {
await navigate(pageName);
}
}
export async function insertTemplateText(cmdDef: any) {
let cursorPos = await getCursor();
let page = await getCurrentPage();
let templateText: string = cmdDef.value;
let carretPos = templateText.indexOf("|^|");
templateText = templateText.replace("|^|", "");
templateText = replaceTemplateVars(templateText, page);
await insertAtCursor(templateText);
if (carretPos !== -1) {
await moveCursor(cursorPos + carretPos);
}
}
+138
View File
@@ -0,0 +1,138 @@
import {
getSelection,
getText,
insertAtCursor,
moveCursor,
replaceRange,
setSelection,
} from "$sb/silverbullet-syscall/editor.ts";
export async function quoteSelection() {
let text = await getText();
const selection = await getSelection();
let from = selection.from;
while (from >= 0 && text[from] !== "\n") {
from--;
}
from++;
if (text[from] === ">" && text[from + 1] === " ") {
// Already quoted, we have to unquote
text = text.slice(from + 2, selection.to);
text = text.replaceAll("\n> ", "\n");
} else {
text = text.slice(from, selection.to);
text = `> ${text.replaceAll("\n", "\n> ")}`;
}
await replaceRange(from, selection.to, text);
}
export async function listifySelection() {
let text = await getText();
const selection = await getSelection();
let from = selection.from;
while (from >= 0 && text[from] !== "\n") {
from--;
}
from++;
text = text.slice(from, selection.to);
text = `* ${text.replaceAll(/\n(?!\n)/g, "\n* ")}`;
await replaceRange(from, selection.to, text);
}
export async function numberListifySelection() {
let text = await getText();
const selection = await getSelection();
let from = selection.from;
while (from >= 0 && text[from] !== "\n") {
from--;
}
from++;
text = text.slice(from, selection.to);
let counter = 1;
text = `1. ${
text.replaceAll(/\n(?!\n)/g, () => {
counter++;
return `\n${counter}. `;
})
}`;
await replaceRange(from, selection.to, text);
}
export async function linkSelection() {
const text = await getText();
const selection = await getSelection();
const textSelection = text.slice(selection.from, selection.to);
let linkedText = `[]()`;
let pos = 1;
if (textSelection.length > 0) {
try {
new URL(textSelection);
linkedText = `[](${textSelection})`;
} catch {
linkedText = `[${textSelection}]()`;
pos = linkedText.length - 1;
}
}
await replaceRange(selection.from, selection.to, linkedText);
await moveCursor(selection.from + pos);
}
export function wrapSelection(cmdDef: any) {
return insertMarker(cmdDef.wrapper);
}
async function insertMarker(marker: string) {
let text = await getText();
const selection = await getSelection();
if (selection.from === selection.to) {
// empty selection
if (markerAt(selection.from)) {
// Already there, skipping ahead
await moveCursor(selection.from + marker.length);
} else {
// Not there, inserting
await insertAtCursor(marker + marker);
await moveCursor(selection.from + marker.length);
}
} else {
let from = selection.from;
let to = selection.to;
let hasMarker = markerAt(from);
if (!markerAt(from)) {
// Maybe just before the cursor? We'll accept that
from = selection.from - marker.length;
to = selection.to + marker.length;
hasMarker = markerAt(from);
}
if (!hasMarker) {
// Adding
await replaceRange(
selection.from,
selection.to,
marker + text.slice(selection.from, selection.to) + marker,
);
await setSelection(
selection.from + marker.length,
selection.to + marker.length,
);
} else {
// Removing
await replaceRange(
from,
to,
text.substring(from + marker.length, to - marker.length),
);
await setSelection(from, to - marker.length * 2);
}
}
function markerAt(pos: number) {
for (var i = 0; i < marker.length; i++) {
if (text[pos + i] !== marker[i]) {
return false;
}
}
return true;
}
}