parent
9fa52e43e0
commit
aaacec6d61
@ -60,7 +60,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
|||||||
console.error(
|
console.error(
|
||||||
"Got error fetching, throwing offline",
|
"Got error fetching, throwing offline",
|
||||||
url,
|
url,
|
||||||
e.errorMessage,
|
e,
|
||||||
);
|
);
|
||||||
throw new Error("Offline");
|
throw new Error("Offline");
|
||||||
}
|
}
|
||||||
|
24
plug-api/lib/page.test.ts
Normal file
24
plug-api/lib/page.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { encodePageRef, parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
import { assertEquals } from "../../test_deps.ts";
|
||||||
|
|
||||||
|
Deno.test("Page utility functions", () => {
|
||||||
|
// Base cases
|
||||||
|
assertEquals(parsePageRef("foo"), { page: "foo" });
|
||||||
|
assertEquals(parsePageRef("[[foo]]"), { page: "foo" });
|
||||||
|
assertEquals(parsePageRef("foo@1"), { page: "foo", pos: 1 });
|
||||||
|
assertEquals(parsePageRef("foo$bar"), { page: "foo", anchor: "bar" });
|
||||||
|
assertEquals(parsePageRef("foo$bar@1"), {
|
||||||
|
page: "foo",
|
||||||
|
anchor: "bar",
|
||||||
|
pos: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
assertEquals(parsePageRef(""), { page: "" });
|
||||||
|
assertEquals(parsePageRef("user@domain.com"), { page: "user@domain.com" });
|
||||||
|
|
||||||
|
// Encoding
|
||||||
|
assertEquals(encodePageRef({ page: "foo" }), "foo");
|
||||||
|
assertEquals(encodePageRef({ page: "foo", pos: 10 }), "foo@10");
|
||||||
|
assertEquals(encodePageRef({ page: "foo", anchor: "bar" }), "foo$bar");
|
||||||
|
});
|
@ -6,13 +6,47 @@ export function validatePageName(name: string) {
|
|||||||
if (name.startsWith(".")) {
|
if (name.startsWith(".")) {
|
||||||
throw new Error("Page name cannot start with a '.'");
|
throw new Error("Page name cannot start with a '.'");
|
||||||
}
|
}
|
||||||
if (name.includes("@")) {
|
|
||||||
throw new Error("Page name cannot contain '@'");
|
|
||||||
}
|
|
||||||
if (name.includes("$")) {
|
|
||||||
throw new Error("Page name cannot contain '$'");
|
|
||||||
}
|
|
||||||
if (/\.[a-zA-Z]+$/.test(name)) {
|
if (/\.[a-zA-Z]+$/.test(name)) {
|
||||||
throw new Error("Page name can not end with a file extension");
|
throw new Error("Page name can not end with a file extension");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PageRef = {
|
||||||
|
page: string;
|
||||||
|
pos?: number;
|
||||||
|
anchor?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const posRegex = /@(\d+)$/;
|
||||||
|
// Should be kept in sync with the regex in index.plug.yaml
|
||||||
|
const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/;
|
||||||
|
|
||||||
|
export function parsePageRef(name: string): PageRef {
|
||||||
|
// Normalize the page name
|
||||||
|
if (name.startsWith("[[") && name.endsWith("]]")) {
|
||||||
|
name = name.slice(2, -2);
|
||||||
|
}
|
||||||
|
const pageRef: PageRef = { page: name };
|
||||||
|
const posMatch = pageRef.page.match(posRegex);
|
||||||
|
if (posMatch) {
|
||||||
|
pageRef.pos = parseInt(posMatch[1]);
|
||||||
|
pageRef.page = pageRef.page.replace(posRegex, "");
|
||||||
|
}
|
||||||
|
const anchorMatch = pageRef.page.match(anchorRegex);
|
||||||
|
if (anchorMatch) {
|
||||||
|
pageRef.anchor = anchorMatch[1];
|
||||||
|
pageRef.page = pageRef.page.replace(anchorRegex, "");
|
||||||
|
}
|
||||||
|
return pageRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodePageRef(pageRef: PageRef): string {
|
||||||
|
let name = pageRef.page;
|
||||||
|
if (pageRef.pos) {
|
||||||
|
name += `@${pageRef.pos}`;
|
||||||
|
}
|
||||||
|
if (pageRef.anchor) {
|
||||||
|
name += `$${pageRef.anchor}`;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { FilterOption } from "../../web/types.ts";
|
import type { FilterOption } from "../../web/types.ts";
|
||||||
|
import { PageRef } from "$sb/lib/page.ts";
|
||||||
import { UploadFile } from "../types.ts";
|
import { UploadFile } from "../types.ts";
|
||||||
import { syscall } from "./syscall.ts";
|
import { syscall } from "./syscall.ts";
|
||||||
|
|
||||||
@ -31,12 +32,11 @@ export function save(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function navigate(
|
export function navigate(
|
||||||
name: string,
|
pageRef: PageRef,
|
||||||
pos?: string | number,
|
|
||||||
replaceState = false,
|
replaceState = false,
|
||||||
newWindow = false,
|
newWindow = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return syscall("editor.navigate", name, pos, replaceState, newWindow);
|
return syscall("editor.navigate", pageRef, replaceState, newWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openPageNavigator(
|
export function openPageNavigator(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { traverseTree } from "../../plug-api/lib/tree.ts";
|
import { traverseTree } from "../../plug-api/lib/tree.ts";
|
||||||
import { editor, markdown, space } from "$sb/syscalls.ts";
|
import { editor, markdown, space } from "$sb/syscalls.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
export async function brokenLinksCommand() {
|
export async function brokenLinksCommand() {
|
||||||
const pageName = "BROKEN LINKS";
|
const pageName = "BROKEN LINKS";
|
||||||
@ -13,7 +14,7 @@ export async function brokenLinksCommand() {
|
|||||||
traverseTree(tree, (tree) => {
|
traverseTree(tree, (tree) => {
|
||||||
if (tree.type === "WikiLinkPage") {
|
if (tree.type === "WikiLinkPage") {
|
||||||
// Add the prefix in the link text
|
// Add the prefix in the link text
|
||||||
const [pageName] = tree.children![0].text!.split(/[@$]/);
|
const { page: pageName } = parsePageRef(tree.children![0].text!);
|
||||||
if (pageName.startsWith("!")) {
|
if (pageName.startsWith("!")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -53,5 +54,5 @@ export async function brokenLinksCommand() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
await space.writePage(pageName, lines.join("\n"));
|
await space.writePage(pageName, lines.join("\n"));
|
||||||
await editor.navigate(pageName);
|
await editor.navigate({ page: pageName });
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
ParseTree,
|
ParseTree,
|
||||||
} from "$sb/lib/tree.ts";
|
} from "$sb/lib/tree.ts";
|
||||||
import { resolveAttachmentPath, resolvePath } from "$sb/lib/resolve.ts";
|
import { resolveAttachmentPath, resolvePath } from "$sb/lib/resolve.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
async function actionClickOrActionEnter(
|
async function actionClickOrActionEnter(
|
||||||
mdTree: ParseTree | null,
|
mdTree: ParseTree | null,
|
||||||
@ -39,30 +40,22 @@ async function actionClickOrActionEnter(
|
|||||||
const currentPage = await editor.getCurrentPage();
|
const currentPage = await editor.getCurrentPage();
|
||||||
switch (mdTree.type) {
|
switch (mdTree.type) {
|
||||||
case "WikiLink": {
|
case "WikiLink": {
|
||||||
let pageLink = mdTree.children![1]!.children![0].text!;
|
const pageLink = mdTree.children![1]!.children![0].text!;
|
||||||
let pos: string | number = 0;
|
const pageRef = parsePageRef(pageLink);
|
||||||
if (pageLink.includes("@") || pageLink.includes("$")) {
|
pageRef.page = resolvePath(currentPage, pageRef.page);
|
||||||
[pageLink, pos] = pageLink.split(/[@$]/);
|
if (!pageRef.page) {
|
||||||
if (pos.match(/^\d+$/)) {
|
pageRef.page = currentPage;
|
||||||
pos = +pos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pageLink = resolvePath(currentPage, pageLink);
|
// This is an explicit navigate, move to the top
|
||||||
if (!pageLink) {
|
if (pageRef.pos === undefined) {
|
||||||
pageLink = currentPage;
|
pageRef.pos = 0;
|
||||||
}
|
}
|
||||||
await editor.navigate(pageLink, pos, false, inNewWindow);
|
await editor.navigate(pageRef, false, inNewWindow);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "PageRef": {
|
case "PageRef": {
|
||||||
const bracketedPageRef = mdTree.children![0].text!;
|
const pageName = parsePageRef(mdTree.children![0].text!).page;
|
||||||
|
await editor.navigate({ page: pageName, pos: 0 }, false, inNewWindow);
|
||||||
// Slicing off the initial [[ and final ]]
|
|
||||||
const pageName = bracketedPageRef.substring(
|
|
||||||
2,
|
|
||||||
bracketedPageRef.length - 2,
|
|
||||||
);
|
|
||||||
await editor.navigate(pageName, 0, false, inNewWindow);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "NakedURL":
|
case "NakedURL":
|
||||||
@ -125,5 +118,5 @@ export async function clickNavigate(event: ClickEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function navigateCommand(cmdDef: any) {
|
export async function navigateCommand(cmdDef: any) {
|
||||||
await editor.navigate(cmdDef.page);
|
await editor.navigate({ page: cmdDef.page, pos: 0 });
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export async function deletePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("Navigating to index page");
|
console.log("Navigating to index page");
|
||||||
await editor.navigate("");
|
await editor.navigate({ page: "" });
|
||||||
console.log("Deleting page from space");
|
console.log("Deleting page from space");
|
||||||
await space.deletePage(pageName);
|
await space.deletePage(pageName);
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ export async function copyPage(
|
|||||||
if (currentPage === fromName) {
|
if (currentPage === fromName) {
|
||||||
// If we're copying the current page, navigate there
|
// If we're copying the current page, navigate there
|
||||||
console.log("Navigating to new page");
|
console.log("Navigating to new page");
|
||||||
await editor.navigate(newName);
|
await editor.navigate({ page: newName });
|
||||||
} else {
|
} else {
|
||||||
// Otherwise just notify of success
|
// Otherwise just notify of success
|
||||||
await editor.flashNotification("Page copied successfully");
|
await editor.flashNotification("Page copied successfully");
|
||||||
|
@ -2,6 +2,7 @@ import { collectNodesOfType } from "$sb/lib/tree.ts";
|
|||||||
import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts";
|
import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts";
|
||||||
import { ObjectValue, QueryExpression } from "$sb/types.ts";
|
import { ObjectValue, QueryExpression } from "$sb/types.ts";
|
||||||
import { indexObjects, queryObjects } from "./api.ts";
|
import { indexObjects, queryObjects } from "./api.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
type AnchorObject = ObjectValue<{
|
type AnchorObject = ObjectValue<{
|
||||||
name: string;
|
name: string;
|
||||||
@ -27,14 +28,14 @@ export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function anchorComplete(completeEvent: CompleteEvent) {
|
export async function anchorComplete(completeEvent: CompleteEvent) {
|
||||||
const match = /\[\[([^\]@$:]*[@$][\w\.\-\/]*)$/.exec(
|
const match = /\[\[([^\]$:]*\$[\w\.\-\/]*)$/.exec(
|
||||||
completeEvent.linePrefix,
|
completeEvent.linePrefix,
|
||||||
);
|
);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageRef = match[1].split(/[@$]/)[0];
|
const pageRef = parsePageRef(match[1]).page;
|
||||||
let filter: QueryExpression | undefined = ["=", ["attr", "page"], [
|
let filter: QueryExpression | undefined = ["=", ["attr", "page"], [
|
||||||
"string",
|
"string",
|
||||||
pageRef,
|
pageRef,
|
||||||
|
@ -5,6 +5,7 @@ import { indexObjects, queryObjects } from "./api.ts";
|
|||||||
import { ObjectValue } from "$sb/types.ts";
|
import { ObjectValue } from "$sb/types.ts";
|
||||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
import { updateITags } from "$sb/lib/tags.ts";
|
import { updateITags } from "$sb/lib/tags.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
const pageRefRegex = /\[\[([^\]]+)\]\]/g;
|
const pageRefRegex = /\[\[([^\]]+)\]\]/g;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
|||||||
const wikiLinkAlias = findNodeOfType(n, "WikiLinkAlias");
|
const wikiLinkAlias = findNodeOfType(n, "WikiLinkAlias");
|
||||||
let toPage = resolvePath(name, wikiLinkPage.children![0].text!);
|
let toPage = resolvePath(name, wikiLinkPage.children![0].text!);
|
||||||
const pos = wikiLinkPage.from!;
|
const pos = wikiLinkPage.from!;
|
||||||
toPage = toPage.split(/[@$]/)[0];
|
toPage = parsePageRef(toPage).page;
|
||||||
const link: LinkObject = {
|
const link: LinkObject = {
|
||||||
ref: `${name}@${pos}`,
|
ref: `${name}@${pos}`,
|
||||||
tag: "link",
|
tag: "link",
|
||||||
@ -89,7 +90,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
|||||||
const code = codeText.children![0].text!;
|
const code = codeText.children![0].text!;
|
||||||
const matches = code.matchAll(pageRefRegex);
|
const matches = code.matchAll(pageRefRegex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const pageRefName = resolvePath(name, match[1]);
|
const pageRefName = resolvePath(name, parsePageRef(match[1]).page);
|
||||||
const pos = codeText.from! + match.index! + 2;
|
const pos = codeText.from! + match.index! + 2;
|
||||||
const link = {
|
const link = {
|
||||||
ref: `${name}@${pos}`,
|
ref: `${name}@${pos}`,
|
||||||
|
@ -67,7 +67,7 @@ async function renamePage(
|
|||||||
|
|
||||||
if (navigateThere) {
|
if (navigateThere) {
|
||||||
console.log("Navigating to new page");
|
console.log("Navigating to new page");
|
||||||
await editor.navigate(newName, 0, true);
|
await editor.navigate({ page: newName, pos: 0 }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagesToUpdate = await getBackLinks(oldName);
|
const pagesToUpdate = await getBackLinks(oldName);
|
||||||
@ -101,12 +101,16 @@ async function renamePage(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace all links found in place following the patterns [[Page]] and [[Page@pos]] as well as [[Page$anchor]]
|
||||||
const newText = text.replaceAll(`[[${oldName}]]`, () => {
|
const newText = text.replaceAll(`[[${oldName}]]`, () => {
|
||||||
updatedReferences++;
|
updatedReferences++;
|
||||||
return `[[${newName}]]`;
|
return `[[${newName}]]`;
|
||||||
}).replaceAll(`[[${oldName}@`, () => {
|
}).replaceAll(`[[${oldName}@`, () => {
|
||||||
updatedReferences++;
|
updatedReferences++;
|
||||||
return `[[${newName}@`;
|
return `[[${newName}@`;
|
||||||
|
}).replaceAll(`[[${oldName}$`, () => {
|
||||||
|
updatedReferences++;
|
||||||
|
return `[[${newName}$`;
|
||||||
});
|
});
|
||||||
if (text !== newText) {
|
if (text !== newText) {
|
||||||
console.log("Changes made, saving...");
|
console.log("Changes made, saving...");
|
||||||
@ -207,5 +211,5 @@ export async function extractToPageCommand() {
|
|||||||
console.log("Writing new page to space");
|
console.log("Writing new page to space");
|
||||||
await space.writePage(newName, text);
|
await space.writePage(newName, text);
|
||||||
console.log("Navigating to new page");
|
console.log("Navigating to new page");
|
||||||
await editor.navigate(newName);
|
await editor.navigate({ page: newName });
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
renderToText,
|
renderToText,
|
||||||
traverseTree,
|
traverseTree,
|
||||||
} from "$sb/lib/tree.ts";
|
} from "$sb/lib/tree.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
import { Fragment, renderHtml, Tag } from "./html_render.ts";
|
import { Fragment, renderHtml, Tag } from "./html_render.ts";
|
||||||
|
|
||||||
export type MarkdownRenderOptions = {
|
export type MarkdownRenderOptions = {
|
||||||
@ -245,11 +246,11 @@ function render(
|
|||||||
if (aliasNode) {
|
if (aliasNode) {
|
||||||
linkText = aliasNode.children![0].text!;
|
linkText = aliasNode.children![0].text!;
|
||||||
}
|
}
|
||||||
const [pageName] = ref.split(/[@$]/);
|
const { page: pageName, anchor } = parsePageRef(ref);
|
||||||
return {
|
return {
|
||||||
name: "a",
|
name: "a",
|
||||||
attrs: {
|
attrs: {
|
||||||
href: `/${pageName}`,
|
href: `/${pageName}${anchor ? "#" + anchor : ""}`,
|
||||||
class: "wiki-link",
|
class: "wiki-link",
|
||||||
"data-ref": ref,
|
"data-ref": ref,
|
||||||
},
|
},
|
||||||
@ -278,9 +279,9 @@ function render(
|
|||||||
case "Task": {
|
case "Task": {
|
||||||
let externalTaskRef = "";
|
let externalTaskRef = "";
|
||||||
collectNodesOfType(t, "WikiLinkPage").forEach((wikilink) => {
|
collectNodesOfType(t, "WikiLinkPage").forEach((wikilink) => {
|
||||||
const ref = wikilink.children![0].text!;
|
const pageRef = parsePageRef(wikilink.children![0].text!);
|
||||||
if (!externalTaskRef && (ref.includes("@") || ref.includes("$"))) {
|
if (!externalTaskRef && (pageRef.pos !== undefined || pageRef.anchor)) {
|
||||||
externalTaskRef = ref;
|
externalTaskRef = wikilink.children![0].text!;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ export async function addPlugCommand() {
|
|||||||
plugsPrelude + "```yaml\n" + plugList.map((p) => `- ${p}`).join("\n") +
|
plugsPrelude + "```yaml\n" + plugList.map((p) => `- ${p}`).join("\n") +
|
||||||
"\n```",
|
"\n```",
|
||||||
);
|
);
|
||||||
await editor.navigate("PLUGS");
|
await editor.navigate({ page: "PLUGS" });
|
||||||
await updatePlugsCommand();
|
await updatePlugsCommand();
|
||||||
await editor.flashNotification("Plug added!");
|
await editor.flashNotification("Plug added!");
|
||||||
system.reloadPlugs();
|
system.reloadPlugs();
|
||||||
|
@ -48,7 +48,7 @@ export async function queryProvider({
|
|||||||
export async function searchCommand() {
|
export async function searchCommand() {
|
||||||
const phrase = await editor.prompt("Search for: ");
|
const phrase = await editor.prompt("Search for: ");
|
||||||
if (phrase) {
|
if (phrase) {
|
||||||
await editor.navigate(`${searchPrefix}${phrase}`);
|
await editor.navigate({ page: `${searchPrefix}${phrase}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import { ObjectValue } from "$sb/types.ts";
|
|||||||
import { indexObjects, queryObjects } from "../index/plug_api.ts";
|
import { indexObjects, queryObjects } from "../index/plug_api.ts";
|
||||||
import { updateITags } from "$sb/lib/tags.ts";
|
import { updateITags } from "$sb/lib/tags.ts";
|
||||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
export type TaskObject = ObjectValue<
|
export type TaskObject = ObjectValue<
|
||||||
{
|
{
|
||||||
@ -191,8 +192,11 @@ export async function updateTaskState(
|
|||||||
newState: string,
|
newState: string,
|
||||||
) {
|
) {
|
||||||
const currentPage = await editor.getCurrentPage();
|
const currentPage = await editor.getCurrentPage();
|
||||||
const [page, posS] = ref.split("@");
|
const { page, pos } = parsePageRef(ref);
|
||||||
const pos = +posS;
|
if (pos === undefined) {
|
||||||
|
console.error("No position found in page ref, skipping", ref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (page === currentPage) {
|
if (page === currentPage) {
|
||||||
// In current page, just update the task marker with dispatch
|
// In current page, just update the task marker with dispatch
|
||||||
const editorText = await editor.getText();
|
const editorText = await editor.getText();
|
||||||
|
@ -154,7 +154,7 @@ async function instantiatePageTemplate(
|
|||||||
// So, page exists
|
// So, page exists
|
||||||
if (newPageConfig.openIfExists) {
|
if (newPageConfig.openIfExists) {
|
||||||
console.log("Page already exists, navigating there");
|
console.log("Page already exists, navigating there");
|
||||||
await editor.navigate(pageName);
|
await editor.navigate({ page: pageName, pos: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ async function instantiatePageTemplate(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Just navigate there without instantiating
|
// Just navigate there without instantiating
|
||||||
return editor.navigate(pageName);
|
return editor.navigate({ page: pageName, pos: 0 });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// The preferred scenario, let's keep going
|
// The preferred scenario, let's keep going
|
||||||
@ -191,7 +191,10 @@ async function instantiatePageTemplate(
|
|||||||
pageName,
|
pageName,
|
||||||
fullPageText,
|
fullPageText,
|
||||||
);
|
);
|
||||||
await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined);
|
await editor.navigate({
|
||||||
|
page: pageName,
|
||||||
|
pos: carretPos !== -1 ? carretPos : undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
191
web/client.ts
191
web/client.ts
@ -13,7 +13,7 @@ import { FilterOption } from "./types.ts";
|
|||||||
import { ensureSettingsAndIndex } from "../common/util.ts";
|
import { ensureSettingsAndIndex } from "../common/util.ts";
|
||||||
import { EventHook } from "../plugos/hooks/event.ts";
|
import { EventHook } from "../plugos/hooks/event.ts";
|
||||||
import { AppCommand } from "./hooks/command.ts";
|
import { AppCommand } from "./hooks/command.ts";
|
||||||
import { PathPageNavigator } from "./navigator.ts";
|
import { PageState, PathPageNavigator } from "./navigator.ts";
|
||||||
|
|
||||||
import { AppViewState, BuiltinSettings } from "./types.ts";
|
import { AppViewState, BuiltinSettings } from "./types.ts";
|
||||||
|
|
||||||
@ -32,10 +32,9 @@ import { SyncStatus } from "../common/spaces/sync.ts";
|
|||||||
import { HttpSpacePrimitives } from "../common/spaces/http_space_primitives.ts";
|
import { HttpSpacePrimitives } from "../common/spaces/http_space_primitives.ts";
|
||||||
import { FallbackSpacePrimitives } from "../common/spaces/fallback_space_primitives.ts";
|
import { FallbackSpacePrimitives } from "../common/spaces/fallback_space_primitives.ts";
|
||||||
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
||||||
import { validatePageName } from "$sb/lib/page.ts";
|
import { encodePageRef, validatePageName } from "$sb/lib/page.ts";
|
||||||
import { ClientSystem } from "./client_system.ts";
|
import { ClientSystem } from "./client_system.ts";
|
||||||
import { createEditorState } from "./editor_state.ts";
|
import { createEditorState } from "./editor_state.ts";
|
||||||
import { OpenPages } from "./open_pages.ts";
|
|
||||||
import { MainUI } from "./editor_ui.tsx";
|
import { MainUI } from "./editor_ui.tsx";
|
||||||
import { cleanPageRef } from "$sb/lib/resolve.ts";
|
import { cleanPageRef } from "$sb/lib/resolve.ts";
|
||||||
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
@ -55,6 +54,7 @@ import {
|
|||||||
import { LimitedMap } from "$sb/lib/limited_map.ts";
|
import { LimitedMap } from "$sb/lib/limited_map.ts";
|
||||||
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
|
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
|
||||||
import { buildQueryFunctions } from "../common/query_functions.ts";
|
import { buildQueryFunctions } from "../common/query_functions.ts";
|
||||||
|
import { PageRef } from "$sb/lib/page.ts";
|
||||||
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
||||||
|
|
||||||
const autoSaveInterval = 1000;
|
const autoSaveInterval = 1000;
|
||||||
@ -71,6 +71,8 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// history.scrollRestoration = "manual";
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
system!: ClientSystem;
|
system!: ClientSystem;
|
||||||
editorView!: EditorView;
|
editorView!: EditorView;
|
||||||
@ -113,7 +115,6 @@ export class Client {
|
|||||||
eventHook!: EventHook;
|
eventHook!: EventHook;
|
||||||
|
|
||||||
ui!: MainUI;
|
ui!: MainUI;
|
||||||
openPages!: OpenPages;
|
|
||||||
stateDataStore!: DataStore;
|
stateDataStore!: DataStore;
|
||||||
spaceDataStore!: DataStore;
|
spaceDataStore!: DataStore;
|
||||||
mq!: DataStoreMQ;
|
mq!: DataStoreMQ;
|
||||||
@ -192,8 +193,6 @@ export class Client {
|
|||||||
parent: document.getElementById("sb-editor")!,
|
parent: document.getElementById("sb-editor")!,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openPages = new OpenPages(this);
|
|
||||||
|
|
||||||
this.focus();
|
this.focus();
|
||||||
|
|
||||||
await this.system.init();
|
await this.system.init();
|
||||||
@ -312,71 +311,98 @@ export class Client {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private navigateWithinPage(pageState: PageState) {
|
||||||
|
// Did we end up doing anything in terms of internal navigation?
|
||||||
|
let adjustedPosition = false;
|
||||||
|
|
||||||
|
// Was a particular scroll position persisted?
|
||||||
|
if (pageState.scrollTop !== undefined) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("Kicking off scroll to", pageState.scrollTop);
|
||||||
|
this.editorView.scrollDOM.scrollTop = pageState.scrollTop!;
|
||||||
|
});
|
||||||
|
adjustedPosition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was a particular cursor/selection set?
|
||||||
|
if (pageState.selection?.anchor && !pageState.pos && !pageState.anchor) { // Only do this if we got a specific cursor position
|
||||||
|
console.log("Changing cursor position to", pageState.selection);
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: pageState.selection,
|
||||||
|
});
|
||||||
|
adjustedPosition = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Was there a pos or anchor set?
|
||||||
|
let pos: number | undefined = pageState.pos;
|
||||||
|
if (pageState.anchor) {
|
||||||
|
console.log("Navigating to anchor", pageState.anchor);
|
||||||
|
const pageText = this.editorView.state.sliceDoc();
|
||||||
|
|
||||||
|
pos = pageText.indexOf(`$${pageState.anchor}`);
|
||||||
|
|
||||||
|
if (pos === -1) {
|
||||||
|
return this.flashNotification(
|
||||||
|
`Could not find anchor $${pageState.anchor}`,
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustedPosition = true;
|
||||||
|
}
|
||||||
|
if (pos !== undefined) {
|
||||||
|
// setTimeout(() => {
|
||||||
|
console.log("Doing this pos set to", pos);
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: { anchor: pos! },
|
||||||
|
effects: EditorView.scrollIntoView(pos!, {
|
||||||
|
y: "start",
|
||||||
|
yMargin: 5,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
adjustedPosition = true;
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not: just put the cursor at the top of the page, right after the frontmatter
|
||||||
|
if (!adjustedPosition) {
|
||||||
|
// Somewhat ad-hoc way to determine if the document contains frontmatter and if so, putting the cursor _after it_.
|
||||||
|
const pageText = this.editorView.state.sliceDoc();
|
||||||
|
|
||||||
|
// Default the cursor to be at position 0
|
||||||
|
let initialCursorPos = 0;
|
||||||
|
const match = frontMatterRegex.exec(pageText);
|
||||||
|
if (match) {
|
||||||
|
// Frontmatter found, put cursor after it
|
||||||
|
initialCursorPos = match[0].length;
|
||||||
|
}
|
||||||
|
// By default scroll to the top
|
||||||
|
console.log("Scrolling to place after frontmatter", initialCursorPos);
|
||||||
|
this.editorView.scrollDOM.scrollTop = 0;
|
||||||
|
this.editorView.dispatch({
|
||||||
|
selection: { anchor: initialCursorPos },
|
||||||
|
// And then scroll down if required
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private initNavigator() {
|
private initNavigator() {
|
||||||
this.pageNavigator = new PathPageNavigator(
|
this.pageNavigator = new PathPageNavigator(this);
|
||||||
cleanPageRef(renderHandlebarsTemplate(this.settings.indexPage, {}, {})),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.pageNavigator.subscribe(
|
this.pageNavigator.subscribe(async (pageState) => {
|
||||||
async (pageName, pos: number | string | undefined) => {
|
console.log("Now navigating to", pageState);
|
||||||
console.log("Now navigating to", pageName);
|
|
||||||
|
|
||||||
const stateRestored = await this.loadPage(pageName, pos === undefined);
|
await this.loadPage(pageState.page);
|
||||||
if (pos) {
|
|
||||||
if (typeof pos === "string") {
|
|
||||||
console.log("Navigating to anchor", pos);
|
|
||||||
|
|
||||||
// We're going to look up the anchor through a API invocation
|
// Setup scroll position, cursor position, etc
|
||||||
const matchingAnchor = await this.system.system.localSyscall(
|
this.navigateWithinPage(pageState);
|
||||||
"system.invokeFunction",
|
|
||||||
[
|
|
||||||
"index.getObjectByRef",
|
|
||||||
pageName,
|
|
||||||
"anchor",
|
|
||||||
`${pageName}$${pos}`,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingAnchor) {
|
await this.stateDataStore.set(
|
||||||
return this.flashNotification(
|
["client", "lastOpenedPage"],
|
||||||
`Could not find anchor $${pos}`,
|
pageState.page,
|
||||||
"error",
|
);
|
||||||
);
|
});
|
||||||
} else {
|
|
||||||
pos = matchingAnchor.pos as number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.editorView.dispatch({
|
|
||||||
selection: { anchor: pos as number },
|
|
||||||
effects: EditorView.scrollIntoView(pos as number, {
|
|
||||||
y: "start",
|
|
||||||
yMargin: 5,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else if (!stateRestored) {
|
|
||||||
// Somewhat ad-hoc way to determine if the document contains frontmatter and if so, putting the cursor _after it_.
|
|
||||||
const pageText = this.editorView.state.sliceDoc();
|
|
||||||
|
|
||||||
// Default the cursor to be at position 0
|
|
||||||
let initialCursorPos = 0;
|
|
||||||
const match = frontMatterRegex.exec(pageText);
|
|
||||||
if (match) {
|
|
||||||
// Frontmatter found, put cursor after it
|
|
||||||
initialCursorPos = match[0].length;
|
|
||||||
}
|
|
||||||
// By default scroll to the top
|
|
||||||
this.editorView.scrollDOM.scrollTop = 0;
|
|
||||||
this.editorView.dispatch({
|
|
||||||
selection: { anchor: initialCursorPos },
|
|
||||||
// And then scroll down if required
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await this.stateDataStore.set(["client", "lastOpenedPage"], pageName);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (location.hash === "#boot") {
|
if (location.hash === "#boot") {
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -760,7 +786,7 @@ export class Client {
|
|||||||
|
|
||||||
if (this.currentPage) {
|
if (this.currentPage) {
|
||||||
// And update the editor if a page is loaded
|
// And update the editor if a page is loaded
|
||||||
this.openPages.saveState(this.currentPage);
|
// this.openPages.saveState(this.currentPage);
|
||||||
|
|
||||||
editorView.setState(
|
editorView.setState(
|
||||||
createEditorState(
|
createEditorState(
|
||||||
@ -776,7 +802,7 @@ export class Client {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openPages.restoreState(this.currentPage);
|
// this.openPages.restoreState(this.currentPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -865,35 +891,41 @@ export class Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async navigate(
|
async navigate(
|
||||||
name: string,
|
pageRef: PageRef,
|
||||||
pos?: number | string,
|
|
||||||
replaceState = false,
|
replaceState = false,
|
||||||
newWindow = false,
|
newWindow = false,
|
||||||
) {
|
) {
|
||||||
if (!name) {
|
if (!pageRef.page) {
|
||||||
name = cleanPageRef(
|
pageRef.page = cleanPageRef(
|
||||||
renderHandlebarsTemplate(this.settings.indexPage, {}, {}),
|
renderHandlebarsTemplate(this.settings.indexPage, {}, {}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pagePart = name.split(/[@$]/)[0];
|
validatePageName(pageRef.page);
|
||||||
validatePageName(pagePart);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
return this.flashNotification(e.message, "error");
|
return this.flashNotification(e.message, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newWindow) {
|
if (newWindow) {
|
||||||
const win = window.open(`${location.origin}/${name}`, "_blank");
|
const win = window.open(
|
||||||
|
`${location.origin}/${encodePageRef(pageRef)}`,
|
||||||
|
"_blank",
|
||||||
|
);
|
||||||
if (win) {
|
if (win) {
|
||||||
win.focus();
|
win.focus();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.pageNavigator!.navigate(name, pos, replaceState);
|
|
||||||
|
await this.pageNavigator!.navigate(
|
||||||
|
pageRef,
|
||||||
|
replaceState,
|
||||||
|
);
|
||||||
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPage(pageName: string, restoreState = true): Promise<boolean> {
|
async loadPage(pageName: string) {
|
||||||
const loadingDifferentPage = pageName !== this.currentPage;
|
const loadingDifferentPage = pageName !== this.currentPage;
|
||||||
const editorView = this.editorView;
|
const editorView = this.editorView;
|
||||||
const previousPage = this.currentPage;
|
const previousPage = this.currentPage;
|
||||||
@ -902,7 +934,7 @@ export class Client {
|
|||||||
|
|
||||||
// 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);
|
||||||
this.space.unwatchPage(previousPage);
|
this.space.unwatchPage(previousPage);
|
||||||
if (previousPage !== pageName) {
|
if (previousPage !== pageName) {
|
||||||
await this.save(true);
|
await this.save(true);
|
||||||
@ -972,7 +1004,6 @@ export class Client {
|
|||||||
if (editorView.contentDOM) {
|
if (editorView.contentDOM) {
|
||||||
this.tweakEditorDOM(editorView.contentDOM);
|
this.tweakEditorDOM(editorView.contentDOM);
|
||||||
}
|
}
|
||||||
const stateRestored = restoreState && this.openPages.restoreState(pageName);
|
|
||||||
this.space.watchPage(pageName);
|
this.space.watchPage(pageName);
|
||||||
|
|
||||||
// Note: these events are dispatched asynchronously deliberately (not waiting for results)
|
// Note: these events are dispatched asynchronously deliberately (not waiting for results)
|
||||||
@ -986,8 +1017,6 @@ export class Client {
|
|||||||
console.error,
|
console.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stateRestored;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tweakEditorDOM(contentDOM: HTMLElement) {
|
tweakEditorDOM(contentDOM: HTMLElement) {
|
||||||
|
@ -5,6 +5,7 @@ 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 buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
const activeWidgets = new Set<MarkdownWidget>();
|
const activeWidgets = new Set<MarkdownWidget>();
|
||||||
|
|
||||||
@ -152,12 +153,8 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
const pageRef = parsePageRef(el.dataset.ref!);
|
||||||
if (pos && pos.match(/^\d+$/)) {
|
this.client.navigate(pageRef);
|
||||||
this.client.navigate(pageName, +pos);
|
|
||||||
} else {
|
|
||||||
this.client.navigate(pageName, pos);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
LinkWidget,
|
LinkWidget,
|
||||||
} from "./util.ts";
|
} from "./util.ts";
|
||||||
import { resolvePath } from "$sb/lib/resolve.ts";
|
import { resolvePath } from "$sb/lib/resolve.ts";
|
||||||
|
import { parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plugin to hide path prefix when the cursor is not inside.
|
* Plugin to hide path prefix when the cursor is not inside.
|
||||||
@ -30,7 +31,7 @@ export function cleanWikiLinkPlugin(client: Client) {
|
|||||||
|
|
||||||
let pageExists = !client.fullSyncCompleted;
|
let pageExists = !client.fullSyncCompleted;
|
||||||
let cleanPage = page;
|
let cleanPage = page;
|
||||||
cleanPage = page.split(/[@$]/)[0];
|
cleanPage = parsePageRef(page).page;
|
||||||
cleanPage = resolvePath(client.currentPage!, cleanPage);
|
cleanPage = resolvePath(client.currentPage!, cleanPage);
|
||||||
const lowerCasePageName = cleanPage.toLowerCase();
|
const lowerCasePageName = cleanPage.toLowerCase();
|
||||||
for (const pageName of client.allKnownPages) {
|
for (const pageName of client.allKnownPages) {
|
||||||
|
@ -21,7 +21,6 @@ import type { Client } from "./client.ts";
|
|||||||
import { Panel } from "./components/panel.tsx";
|
import { Panel } from "./components/panel.tsx";
|
||||||
import { h } from "./deps.ts";
|
import { h } from "./deps.ts";
|
||||||
import { sleep } from "$sb/lib/async.ts";
|
import { sleep } from "$sb/lib/async.ts";
|
||||||
import { template } from "https://esm.sh/v132/handlebars@4.7.7/runtime.d.ts";
|
|
||||||
|
|
||||||
export class MainUI {
|
export class MainUI {
|
||||||
viewState: AppViewState = initialViewState;
|
viewState: AppViewState = initialViewState;
|
||||||
@ -112,7 +111,7 @@ export class MainUI {
|
|||||||
});
|
});
|
||||||
if (page) {
|
if (page) {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
await client.navigate(page);
|
await client.navigate({ page });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -246,7 +245,7 @@ export class MainUI {
|
|||||||
icon: HomeIcon,
|
icon: HomeIcon,
|
||||||
description: `Go to the index page (Alt-h)`,
|
description: `Go to the index page (Alt-h)`,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
client.navigate("", 0);
|
client.navigate({ page: "", pos: 0 });
|
||||||
// And let's make sure all panels are closed
|
// And let's make sure all panels are closed
|
||||||
dispatch({ type: "hide-filterbox" });
|
dispatch({ type: "hide-filterbox" });
|
||||||
},
|
},
|
||||||
|
165
web/navigator.ts
165
web/navigator.ts
@ -1,43 +1,77 @@
|
|||||||
import { safeRun } from "../common/util.ts";
|
import { safeRun } from "../common/util.ts";
|
||||||
|
import { PageRef, parsePageRef } from "$sb/lib/page.ts";
|
||||||
|
import { Client } from "./client.ts";
|
||||||
|
import { cleanPageRef } from "$sb/lib/resolve.ts";
|
||||||
|
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
|
||||||
|
|
||||||
function encodePageUrl(name: string): string {
|
export type PageState = PageRef & {
|
||||||
return name;
|
scrollTop?: number;
|
||||||
}
|
selection?: {
|
||||||
|
anchor: number;
|
||||||
function decodePageUrl(url: string): string {
|
head?: number;
|
||||||
return url;
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export class PathPageNavigator {
|
export class PathPageNavigator {
|
||||||
navigationResolve?: () => void;
|
navigationResolve?: () => void;
|
||||||
|
root: string;
|
||||||
|
indexPage: string;
|
||||||
|
|
||||||
constructor(readonly indexPage: string, readonly root: string = "") {}
|
openPages = new Map<string, PageState>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private client: Client,
|
||||||
|
) {
|
||||||
|
this.root = "";
|
||||||
|
this.indexPage = cleanPageRef(
|
||||||
|
renderHandlebarsTemplate(client.settings.indexPage, {}, {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates the client to the given page, this involves:
|
||||||
|
* - Patching the current popstate with current state
|
||||||
|
* - Pushing the new state
|
||||||
|
* - Dispatching a popstate event
|
||||||
|
* @param pageRef to navigate to
|
||||||
|
* @param replaceState whether to update the state in place (rather than to push a new state)
|
||||||
|
*/
|
||||||
async navigate(
|
async navigate(
|
||||||
page: string,
|
pageRef: PageRef,
|
||||||
pos?: number | string | undefined,
|
|
||||||
replaceState = false,
|
replaceState = false,
|
||||||
) {
|
) {
|
||||||
let encodedPage = encodePageUrl(page);
|
if (pageRef.page === this.indexPage) {
|
||||||
if (page === this.indexPage) {
|
pageRef.page = "";
|
||||||
encodedPage = "";
|
|
||||||
}
|
}
|
||||||
if (replaceState) {
|
const currentState = this.buildCurrentPageState();
|
||||||
|
// No need to keep pos and anchor if we already have scrollTop and selection
|
||||||
|
const cleanState = { ...currentState, pos: undefined, anchor: undefined };
|
||||||
|
this.openPages.set(currentState.page || this.indexPage, cleanState);
|
||||||
|
if (!replaceState) {
|
||||||
|
console.log("Updating current state", currentState);
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
{ page },
|
cleanState,
|
||||||
page,
|
"",
|
||||||
`${this.root}/${encodedPage}`,
|
`${this.root}/${currentState.page}`,
|
||||||
|
);
|
||||||
|
console.log("Pushing new state", pageRef);
|
||||||
|
window.history.pushState(
|
||||||
|
pageRef,
|
||||||
|
"",
|
||||||
|
`${this.root}/${pageRef.page}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
window.history.pushState(
|
// console.log("Replacing state", pageRef);
|
||||||
{ page },
|
window.history.replaceState(
|
||||||
page,
|
pageRef,
|
||||||
`${this.root}/${encodedPage}`,
|
"",
|
||||||
|
`${this.root}/${pageRef.page}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// console.log("Explicitly dispatching the popstate", pageRef);
|
||||||
globalThis.dispatchEvent(
|
globalThis.dispatchEvent(
|
||||||
new PopStateEvent("popstate", {
|
new PopStateEvent("popstate", {
|
||||||
state: { page, pos },
|
state: pageRef,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
@ -46,52 +80,77 @@ export class PathPageNavigator {
|
|||||||
this.navigationResolve = undefined;
|
this.navigationResolve = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildCurrentPageState(): PageState {
|
||||||
|
const pageState: PageState = this.parseURI();
|
||||||
|
const mainSelection = this.client.editorView.state.selection.main;
|
||||||
|
pageState.scrollTop = this.client.editorView.scrollDOM.scrollTop;
|
||||||
|
pageState.selection = {
|
||||||
|
head: mainSelection.head,
|
||||||
|
anchor: mainSelection.anchor,
|
||||||
|
};
|
||||||
|
return pageState;
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(
|
subscribe(
|
||||||
pageLoadCallback: (
|
pageLoadCallback: (
|
||||||
pageName: string,
|
pageState: PageState,
|
||||||
pos: number | string | undefined,
|
|
||||||
) => Promise<void>,
|
) => Promise<void>,
|
||||||
): void {
|
): void {
|
||||||
const cb = (event?: PopStateEvent) => {
|
const cb = (event: PopStateEvent) => {
|
||||||
const gotoPage = this.getCurrentPage();
|
|
||||||
if (!gotoPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
await pageLoadCallback(
|
const popState = event.state;
|
||||||
this.getCurrentPage(),
|
if (popState) {
|
||||||
event?.state?.pos,
|
// This is the usual case
|
||||||
);
|
if (!popState.page) {
|
||||||
|
popState.page = this.indexPage;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
popState.anchor === undefined && popState.pos === undefined &&
|
||||||
|
popState.selection === undefined &&
|
||||||
|
popState.scrollTop === undefined
|
||||||
|
) {
|
||||||
|
// Pretty low-context popstate, so let's leverage openPages
|
||||||
|
const openPage = this.openPages.get(popState.page);
|
||||||
|
if (openPage) {
|
||||||
|
console.log("Pulling open page state", openPage);
|
||||||
|
popState.selection = openPage.selection;
|
||||||
|
popState.scrollTop = openPage.scrollTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Got popstate state, using", popState);
|
||||||
|
await pageLoadCallback(popState);
|
||||||
|
} else {
|
||||||
|
// This occurs when the page is loaded completely fresh with no browser history around it
|
||||||
|
// console.log("Got null state so using", this.parseURI());
|
||||||
|
const pageRef = this.parseURI();
|
||||||
|
if (!pageRef.page) {
|
||||||
|
pageRef.page = this.indexPage;
|
||||||
|
}
|
||||||
|
await pageLoadCallback(pageRef);
|
||||||
|
}
|
||||||
if (this.navigationResolve) {
|
if (this.navigationResolve) {
|
||||||
this.navigationResolve();
|
this.navigationResolve();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
globalThis.addEventListener("popstate", cb);
|
globalThis.addEventListener("popstate", cb);
|
||||||
cb();
|
|
||||||
|
cb(
|
||||||
|
new PopStateEvent("popstate", {
|
||||||
|
state: this.buildCurrentPageState(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeURI(): [string, number | string] {
|
parseURI(): PageRef {
|
||||||
const [page, pos] = decodeURI(
|
const pageRef = parsePageRef(decodeURI(
|
||||||
location.pathname.substring(this.root.length + 1),
|
location.pathname.substring(this.root.length + 1),
|
||||||
).split(/[@$]/);
|
));
|
||||||
if (pos) {
|
|
||||||
if (pos.match(/^\d+$/)) {
|
|
||||||
return [page, +pos];
|
|
||||||
} else {
|
|
||||||
return [page, pos];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return [page, 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPage(): string {
|
// if (!pageRef.page) {
|
||||||
return decodePageUrl(this.decodeURI()[0]) || this.indexPage;
|
// pageRef.page = this.indexPage;
|
||||||
}
|
// }
|
||||||
|
|
||||||
getCurrentPos(): number | string {
|
return pageRef;
|
||||||
// console.log("Pos", this.decodeURI()[1]);
|
|
||||||
return this.decodeURI()[1];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { Client } from "./client.ts";
|
|
||||||
import { EditorSelection } from "./deps.ts";
|
|
||||||
|
|
||||||
class PageState {
|
|
||||||
constructor(
|
|
||||||
readonly scrollTop: number,
|
|
||||||
readonly selection: EditorSelection,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OpenPages {
|
|
||||||
openPages = new Map<string, PageState>();
|
|
||||||
|
|
||||||
constructor(private client: Client) {}
|
|
||||||
|
|
||||||
restoreState(pageName: string): boolean {
|
|
||||||
const pageState = this.openPages.get(pageName);
|
|
||||||
const editorView = this.client.editorView;
|
|
||||||
if (pageState) {
|
|
||||||
// Restore state
|
|
||||||
try {
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: pageState.selection,
|
|
||||||
// scrollIntoView: true,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// This is fine, just go to the top
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: { anchor: 0 },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
// Next tick, to allow the editor to process the render
|
|
||||||
editorView.scrollDOM.scrollTop = pageState.scrollTop;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
editorView.scrollDOM.scrollTop = 0;
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: { anchor: 0 },
|
|
||||||
scrollIntoView: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.client.focus();
|
|
||||||
return !!pageState;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveState(currentPage: string) {
|
|
||||||
this.openPages.set(
|
|
||||||
currentPage,
|
|
||||||
new PageState(
|
|
||||||
this.client.editorView.scrollDOM.scrollTop,
|
|
||||||
this.client.editorView.state.selection,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||||||
import { SysCallMapping } from "../../plugos/system.ts";
|
import { SysCallMapping } from "../../plugos/system.ts";
|
||||||
import type { FilterOption } from "../types.ts";
|
import type { FilterOption } from "../types.ts";
|
||||||
import { UploadFile } from "../../plug-api/types.ts";
|
import { UploadFile } from "../../plug-api/types.ts";
|
||||||
|
import { PageRef } from "$sb/lib/page.ts";
|
||||||
|
|
||||||
export function editorSyscalls(client: Client): SysCallMapping {
|
export function editorSyscalls(client: Client): SysCallMapping {
|
||||||
const syscalls: SysCallMapping = {
|
const syscalls: SysCallMapping = {
|
||||||
@ -33,12 +34,14 @@ export function editorSyscalls(client: Client): SysCallMapping {
|
|||||||
},
|
},
|
||||||
"editor.navigate": async (
|
"editor.navigate": async (
|
||||||
_ctx,
|
_ctx,
|
||||||
name: string,
|
pageRef: PageRef | string,
|
||||||
pos: number | string,
|
|
||||||
replaceState = false,
|
replaceState = false,
|
||||||
newWindow = false,
|
newWindow = false,
|
||||||
) => {
|
) => {
|
||||||
await client.navigate(name, pos, replaceState, newWindow);
|
if (typeof pageRef === "string") {
|
||||||
|
pageRef = { page: pageRef };
|
||||||
|
}
|
||||||
|
await client.navigate(pageRef, replaceState, newWindow);
|
||||||
},
|
},
|
||||||
"editor.reloadPage": async () => {
|
"editor.reloadPage": async () => {
|
||||||
await client.reloadPage();
|
await client.reloadPage();
|
||||||
|
@ -23,10 +23,10 @@ export function spaceSyscalls(editor: Client): SysCallMapping {
|
|||||||
"space.deletePage": async (_ctx, name: string) => {
|
"space.deletePage": async (_ctx, name: string) => {
|
||||||
// If we're deleting the current page, navigate to the index page
|
// If we're deleting the current page, navigate to the index page
|
||||||
if (editor.currentPage === name) {
|
if (editor.currentPage === name) {
|
||||||
await editor.navigate("");
|
await editor.navigate({ page: "" });
|
||||||
}
|
}
|
||||||
// Remove page from open pages in editor
|
// Remove page from open pages in editor
|
||||||
editor.openPages.openPages.delete(name);
|
// editor.openPages.openPages.delete(name);
|
||||||
console.log("Deleting page");
|
console.log("Deleting page");
|
||||||
await editor.space.deletePage(name);
|
await editor.space.deletePage(name);
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ This enables a few things:
|
|||||||
|
|
||||||
* **Linking and browsing** to other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]). For instance the [[!silverbullet.md/CHANGELOG|SilverBullet CHANGELOG]] without leaving the comfort of your own SilverBullet client.
|
* **Linking and browsing** to other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]). For instance the [[!silverbullet.md/CHANGELOG|SilverBullet CHANGELOG]] without leaving the comfort of your own SilverBullet client.
|
||||||
* **Reusing** content from externally hosted sources, such as:
|
* **Reusing** content from externally hosted sources, such as:
|
||||||
* _Templates_, e.g. by federating with `silverbullet.md/template` will give you access to the example templates hosted there without manually copying and pasting them and automatically pull in the latest version. So you can, for instance, use `render [[!silverbullet.md/template/page]]` to use the [[Library/Core/Query/Page]] template. See [[Libraries]] for more on this use case.
|
* [[Libraries]] synchronization. By federating with `silverbullet.md/Library/Core`, you will get you access to the templates hosted there without copying ({[Library: Import]}‘ing) them and automatically pull in the latest versions.
|
||||||
* _Data_: such as tasks, item, data hosted elsewhere that you want to query from your own space.
|
* _Data_: such as tasks, item, data hosted elsewhere that you want to query from your own space.
|
||||||
|
|
||||||
**Note:** Federation does not support authentication yet, so all federated spaces need to be unauthenticated and will be _read-only_.
|
**Note:** Federation does not support authentication yet, so all federated spaces need to be unauthenticated and will be _read-only_.
|
||||||
@ -17,10 +17,10 @@ To synchronize federated content into your client, you need to list these URIs i
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
federate:
|
federate:
|
||||||
- uri: silverbullet.md/template
|
- uri: silverbullet.md/Library/Core/
|
||||||
```
|
```
|
||||||
|
|
||||||
This will synchronize all content under `!silverbullet.md` with a `template` prefix (so all templates hosted there) locally.
|
This will synchronize all content under `!silverbullet.md` with a `Library/Core/` prefix (so all templates hosted there) locally.
|
||||||
|
|
||||||
Currently, content can only be synchronized in read-only mode, so you can not edit the synchronized files. This will likely change in the future.
|
Currently, content can only be synchronized in read-only mode, so you can not edit the synchronized files. This will likely change in the future.
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ For your first run, you can run the following:
|
|||||||
# Create a local folder "space" to keep files in
|
# Create a local folder "space" to keep files in
|
||||||
$ mkdir -p space
|
$ mkdir -p space
|
||||||
# Run the SilverBullet docker container in the foreground
|
# Run the SilverBullet docker container in the foreground
|
||||||
$ docker run -it -p 3000:3000 -v ./space:/space zefhemel/silverbullet
|
$ sudo docker run -it -p 3000:3000 -v ./space:/space zefhemel/silverbullet
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run SilverBullet in the foreground, interactively, so you can see the logs and instructions.
|
This will run SilverBullet in the foreground, interactively, so you can see the logs and instructions.
|
||||||
@ -49,7 +49,7 @@ $ docker kill silverbullet
|
|||||||
# Remove the old container
|
# Remove the old container
|
||||||
$ docker rm silverbullet
|
$ docker rm silverbullet
|
||||||
# Start a fresh one (same command as before)
|
# Start a fresh one (same command as before)
|
||||||
$ docker run -d --restart unless-stopped --name silverbullet -p 3000:3000 -v ./space:/space zefhemel/silverbullet
|
$ docker run -d --restart unless-stopped --name silverbullet -p 3000:3000 -v $PW/space:/space zefhemel/silverbullet
|
||||||
```
|
```
|
||||||
|
|
||||||
Since this is somewhat burdensome, it is recommended you use a tool like [watchtower](https://github.com/containrrr/watchtower) to automatically update your docker images and restart them. However, if we go there — we may as well use a tool like _docker compose_ to manage your containers, no?
|
Since this is somewhat burdensome, it is recommended you use a tool like [watchtower](https://github.com/containrrr/watchtower) to automatically update your docker images and restart them. However, if we go there — we may as well use a tool like _docker compose_ to manage your containers, no?
|
||||||
|
Loading…
Reference in New Issue
Block a user