Bunch of stuff
parent
89f93963f5
commit
ad37b9ed10
|
@ -21,13 +21,41 @@
|
|||
"invoke": "toggle_h2",
|
||||
"mac": "Cmd-2",
|
||||
"key": "Ctrl-2"
|
||||
},
|
||||
"Page: Delete": {
|
||||
"invoke": "deletePage"
|
||||
},
|
||||
"Page: Rename": {
|
||||
"invoke": "renamePage"
|
||||
},
|
||||
"Pages: Reindex": {
|
||||
"invoke": "reindexPages"
|
||||
},
|
||||
"Pages: Back Links": {
|
||||
"invoke": "showBackLinks"
|
||||
}
|
||||
},
|
||||
"events": {
|
||||
"page:click": ["taskToggle", "clickNavigate"],
|
||||
"editor:complete": ["pageComplete"]
|
||||
"editor:complete": ["pageComplete"],
|
||||
"page:index": ["indexLinks"]
|
||||
},
|
||||
"functions": {
|
||||
"indexLinks": {
|
||||
"path": "./page.ts:indexLinks"
|
||||
},
|
||||
"deletePage": {
|
||||
"path": "./page.ts:deletePage"
|
||||
},
|
||||
"showBackLinks": {
|
||||
"path": "./page.ts:showBackLinks"
|
||||
},
|
||||
"renamePage": {
|
||||
"path": "./page.ts:renamePage"
|
||||
},
|
||||
"reindexPages": {
|
||||
"path": "./page.ts:reindex"
|
||||
},
|
||||
"pageComplete": {
|
||||
"path": "./navigate.ts:pageComplete"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import { IndexEvent } from "../../webapp/src/app_event.ts";
|
||||
import { pageLinkRegex } from "../../webapp/src/constant.ts";
|
||||
import { syscall } from "./lib/syscall.ts";
|
||||
|
||||
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
|
||||
|
||||
export async function indexLinks({ name, text }: IndexEvent) {
|
||||
console.log("Now indexing", name);
|
||||
let backLinks: { key: string; value: string }[] = [];
|
||||
for (let match of text.matchAll(wikilinkRegex)) {
|
||||
let toPage = match[1];
|
||||
let pos = match.index!;
|
||||
backLinks.push({
|
||||
key: `pl:${toPage}:${pos}`,
|
||||
value: name,
|
||||
});
|
||||
}
|
||||
console.log("Found", backLinks.length, "wiki link(s)");
|
||||
await syscall("indexer.batchSet", name, backLinks);
|
||||
}
|
||||
|
||||
export async function deletePage() {
|
||||
let pageMeta = await syscall("editor.getCurrentPage");
|
||||
console.log("Navigating to start page");
|
||||
await syscall("editor.navigate", "start");
|
||||
console.log("Deleting page from space");
|
||||
await syscall("space.deletePage", pageMeta.name);
|
||||
console.log("Reloading page list");
|
||||
await syscall("space.reloadPageList");
|
||||
}
|
||||
|
||||
export async function renamePage() {
|
||||
const pageMeta = await syscall("editor.getCurrentPage");
|
||||
const oldName = pageMeta.name;
|
||||
const newName = await syscall("editor.prompt", `Rename ${oldName} to:`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
console.log("New name", newName);
|
||||
|
||||
let pagesToUpdate = await getBackLinks(oldName);
|
||||
console.log("All pages containing backlinks", pagesToUpdate);
|
||||
|
||||
let text = await syscall("editor.getText");
|
||||
console.log("Writing new page to space");
|
||||
await syscall("space.writePage", newName, text);
|
||||
console.log("Deleting page from space");
|
||||
await syscall("space.deletePage", oldName);
|
||||
console.log("Reloading page list");
|
||||
await syscall("space.reloadPageList");
|
||||
console.log("Navigating to new page");
|
||||
await syscall("editor.navigate", newName);
|
||||
|
||||
let pageToUpdateSet = new Set<string>();
|
||||
for (let pageToUpdate of pagesToUpdate) {
|
||||
pageToUpdateSet.add(pageToUpdate.page);
|
||||
}
|
||||
|
||||
for (let pageToUpdate of pageToUpdateSet) {
|
||||
console.log("Now going to update links in", pageToUpdate);
|
||||
let { text } = await syscall("space.readPage", pageToUpdate);
|
||||
if (!text) {
|
||||
// Page likely does not exist, but at least we can skip it
|
||||
continue;
|
||||
}
|
||||
let newText = text.replaceAll(`[[${oldName}]]`, `[[${newName}]]`);
|
||||
if (text !== newText) {
|
||||
console.log("Changes made, saving...");
|
||||
await syscall("space.writePage", pageToUpdate, newText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BackLink = {
|
||||
page: string;
|
||||
pos: number;
|
||||
};
|
||||
|
||||
async function getBackLinks(pageName: string): Promise<BackLink[]> {
|
||||
let allBackLinks = await syscall(
|
||||
"indexer.scanPrefixGlobal",
|
||||
`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 showBackLinks() {
|
||||
const pageMeta = await syscall("editor.getCurrentPage");
|
||||
let backLinks = await getBackLinks(pageMeta.name);
|
||||
|
||||
console.log("Backlinks", backLinks);
|
||||
}
|
||||
|
||||
export async function reindex() {
|
||||
await syscall("space.reindex");
|
||||
}
|
|
@ -18,7 +18,7 @@ const pagesPath = "../pages";
|
|||
|
||||
const fsRouter = new Router();
|
||||
|
||||
fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] }));
|
||||
fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST", "DELETE"] }));
|
||||
|
||||
fsRouter.get("/", async (context) => {
|
||||
const localPath = pagesPath;
|
||||
|
@ -96,6 +96,22 @@ fsRouter.put("/:page(.*)", async (context) => {
|
|||
context.response.body = "OK";
|
||||
});
|
||||
|
||||
fsRouter.delete("/:page(.*)", async (context) => {
|
||||
const pageName = context.params.page;
|
||||
const localPath = `${pagesPath}/${pageName}.md`;
|
||||
try {
|
||||
await Deno.remove(localPath);
|
||||
} catch (e) {
|
||||
console.error("Error deleting file", localPath, e);
|
||||
context.response.status = 500;
|
||||
context.response.body = e.message;
|
||||
return;
|
||||
}
|
||||
console.log("Deleted", localPath);
|
||||
|
||||
context.response.body = "OK";
|
||||
});
|
||||
|
||||
const app = new Application();
|
||||
app.use(
|
||||
new Router()
|
||||
|
@ -109,7 +125,8 @@ app.use(async (context, next) => {
|
|||
index: "index.html",
|
||||
});
|
||||
} catch {
|
||||
next();
|
||||
await context.send({ root: "../webapp/dist", path: "index.html" });
|
||||
// next();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"@codemirror/state": "^0.19.7",
|
||||
"@codemirror/view": "^0.19.42",
|
||||
"@parcel/service-worker": "^2.3.2",
|
||||
"dexie": "^3.2.1",
|
||||
"idb": "^7.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
|
|
|
@ -2,6 +2,7 @@ export type AppEvent =
|
|||
| "app:ready"
|
||||
| "page:save"
|
||||
| "page:click"
|
||||
| "page:index"
|
||||
| "editor:complete";
|
||||
|
||||
export type ClickEvent = {
|
||||
|
@ -10,3 +11,12 @@ export type ClickEvent = {
|
|||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
};
|
||||
|
||||
export type IndexEvent = {
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface AppEventDispatcher {
|
||||
dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]>;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,35 @@
|
|||
import { PageMeta } from "../types";
|
||||
import { FilterList } from "./filter";
|
||||
import { FilterList, Option } from "./filter";
|
||||
|
||||
export function PageNavigator({
|
||||
allPages: allPages,
|
||||
allPages,
|
||||
onNavigate,
|
||||
currentPage,
|
||||
}: {
|
||||
allPages: PageMeta[];
|
||||
onNavigate: (page: string | undefined) => void;
|
||||
currentPage?: PageMeta;
|
||||
}) {
|
||||
let options: Option[] = [];
|
||||
for (let pageMeta of allPages) {
|
||||
if (currentPage && currentPage.name == pageMeta.name) {
|
||||
continue;
|
||||
}
|
||||
// Order by last modified date in descending order
|
||||
let orderId = -pageMeta.lastModified.getTime();
|
||||
// Unless it was opened and is still in memory
|
||||
if (pageMeta.lastOpened) {
|
||||
orderId = -pageMeta.lastOpened.getTime();
|
||||
}
|
||||
options.push({
|
||||
...pageMeta,
|
||||
orderId: orderId,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<FilterList
|
||||
placeholder=""
|
||||
options={allPages.map((meta) => ({
|
||||
...meta,
|
||||
// Order by last modified date in descending order
|
||||
orderId: -meta.lastModified.getTime(),
|
||||
}))}
|
||||
options={options}
|
||||
allowNew={true}
|
||||
newHint="Create page"
|
||||
onSelect={(opt) => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const pageLinkRegex = /\[\[([\w\s\/\:,\.\-]+)\]\]/;
|
|
@ -39,6 +39,7 @@ import customMarkdownStyle from "./style";
|
|||
import dbSyscalls from "./syscalls/db.localstorage";
|
||||
import { Plugin } from "./plugins/runtime";
|
||||
import editorSyscalls from "./syscalls/editor.browser";
|
||||
import indexerSyscalls from "./syscalls/indexer.native";
|
||||
import spaceSyscalls from "./syscalls/space.native";
|
||||
import {
|
||||
Action,
|
||||
|
@ -47,8 +48,15 @@ import {
|
|||
initialViewState,
|
||||
PageMeta,
|
||||
} from "./types";
|
||||
import { AppEvent, ClickEvent } from "./app_event";
|
||||
import {
|
||||
AppEvent,
|
||||
AppEventDispatcher,
|
||||
ClickEvent,
|
||||
IndexEvent,
|
||||
} from "./app_event";
|
||||
import { safeRun } from "./util";
|
||||
import { Indexer } from "./indexer";
|
||||
import { IPageNavigator, PathPageNavigator } from "./navigator";
|
||||
|
||||
class PageState {
|
||||
editorState: EditorState;
|
||||
|
@ -64,21 +72,23 @@ class PageState {
|
|||
|
||||
const watchInterval = 5000;
|
||||
|
||||
export class Editor {
|
||||
export class Editor implements AppEventDispatcher {
|
||||
editorView?: EditorView;
|
||||
viewState: AppViewState;
|
||||
viewDispatch: React.Dispatch<Action>;
|
||||
$hashChange?: () => void;
|
||||
openPages: Map<string, PageState>;
|
||||
fs: Space;
|
||||
space: Space;
|
||||
editorCommands: Map<string, AppCommand>;
|
||||
plugins: Plugin[];
|
||||
indexer: Indexer;
|
||||
navigationResolve?: (val: undefined) => void;
|
||||
pageNavigator: IPageNavigator;
|
||||
|
||||
constructor(fs: Space, parent: Element) {
|
||||
constructor(space: Space, parent: Element) {
|
||||
this.editorCommands = new Map();
|
||||
this.openPages = new Map();
|
||||
this.plugins = [];
|
||||
this.fs = fs;
|
||||
this.space = space;
|
||||
this.viewState = initialViewState;
|
||||
this.viewDispatch = () => {};
|
||||
this.render(parent);
|
||||
|
@ -86,16 +96,30 @@ export class Editor {
|
|||
state: this.createEditorState(""),
|
||||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
this.addListeners();
|
||||
// this.watch();
|
||||
this.pageNavigator = new PathPageNavigator();
|
||||
this.indexer = new Indexer("page-index", space);
|
||||
this.watch();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadPageList();
|
||||
await this.loadPlugins();
|
||||
this.$hashChange!();
|
||||
this.focus();
|
||||
await this.dispatchAppEvent("app:ready");
|
||||
|
||||
this.pageNavigator.subscribe(async (pageName) => {
|
||||
await this.save();
|
||||
console.log("Now navigating to", pageName);
|
||||
|
||||
if (!this.editorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadPage(pageName);
|
||||
});
|
||||
|
||||
if (this.pageNavigator.getCurrentPage() === "") {
|
||||
this.pageNavigator.navigate("start");
|
||||
}
|
||||
}
|
||||
|
||||
async loadPlugins() {
|
||||
|
@ -103,7 +127,8 @@ export class Editor {
|
|||
system.registerSyscalls(
|
||||
dbSyscalls,
|
||||
editorSyscalls(this),
|
||||
spaceSyscalls(this)
|
||||
spaceSyscalls(this),
|
||||
indexerSyscalls(this.indexer)
|
||||
);
|
||||
|
||||
await system.bootServiceWorker();
|
||||
|
@ -332,41 +357,20 @@ export class Editor {
|
|||
return null;
|
||||
}
|
||||
|
||||
click(event: MouseEvent, view: EditorView) {
|
||||
// if (event.metaKey || event.ctrlKey) {
|
||||
// let coords = view.posAtCoords(event)!;
|
||||
// let node = syntaxTree(view.state).resolveInner(coords);
|
||||
// if (node && node.name === "WikiLinkPage") {
|
||||
// let pageName = view.state.sliceDoc(node.from, node.to);
|
||||
// this.navigate(pageName);
|
||||
// }
|
||||
// if (node && node.name === "TaskMarker") {
|
||||
// let checkBoxText = view.state.sliceDoc(node.from, node.to);
|
||||
// if (checkBoxText === "[x]" || checkBoxText === "[X]") {
|
||||
// view.dispatch({
|
||||
// changes: { from: node.from, to: node.to, insert: "[ ]" },
|
||||
// });
|
||||
// } else {
|
||||
// view.dispatch({
|
||||
// changes: { from: node.from, to: node.to, insert: "[x]" },
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
async save() {
|
||||
const editorState = this.editorView!.state;
|
||||
|
||||
if (!this.currentPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.viewState.isSaved) {
|
||||
console.log("Page not modified, skipping saving");
|
||||
return;
|
||||
}
|
||||
// Write to file system
|
||||
let pageMeta = await this.fs.writePage(
|
||||
this.currentPage.name,
|
||||
editorState.sliceDoc()
|
||||
);
|
||||
let text = editorState.sliceDoc();
|
||||
let pageMeta = await this.space.writePage(this.currentPage.name, text);
|
||||
|
||||
// Update in open page cache
|
||||
this.openPages.set(
|
||||
|
@ -381,10 +385,18 @@ export class Editor {
|
|||
if (pageMeta.created) {
|
||||
await this.loadPageList();
|
||||
}
|
||||
|
||||
// Reindex page
|
||||
await this.indexPage(text, pageMeta);
|
||||
}
|
||||
|
||||
private async indexPage(text: string, pageMeta: PageMeta) {
|
||||
console.log("Indexing page", pageMeta.name);
|
||||
this.indexer.indexPage(this, pageMeta, text, true);
|
||||
}
|
||||
|
||||
async loadPageList() {
|
||||
let pagesMeta = await this.fs.listPages();
|
||||
let pagesMeta = await this.space.listPages();
|
||||
this.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: pagesMeta,
|
||||
|
@ -394,25 +406,27 @@ export class Editor {
|
|||
watch() {
|
||||
setInterval(() => {
|
||||
safeRun(async () => {
|
||||
if (!this.currentPage) {
|
||||
return;
|
||||
if (this.currentPage && this.viewState.isSaved) {
|
||||
await this.checkForNewVersion(this.currentPage);
|
||||
}
|
||||
const currentPageName = this.currentPage.name;
|
||||
let newPageMeta = await this.fs.getPageMeta(currentPageName);
|
||||
});
|
||||
}, watchInterval);
|
||||
}
|
||||
|
||||
async checkForNewVersion(cachedMeta: PageMeta) {
|
||||
const currentPageName = cachedMeta.name;
|
||||
let newPageMeta = await this.space.getPageMeta(currentPageName);
|
||||
if (
|
||||
this.currentPage.lastModified.getTime() <
|
||||
newPageMeta.lastModified.getTime()
|
||||
cachedMeta.lastModified.getTime() !== newPageMeta.lastModified.getTime()
|
||||
) {
|
||||
console.log("File changed on disk, reloading");
|
||||
let pageData = await this.fs.readPage(currentPageName);
|
||||
let pageData = await this.space.readPage(currentPageName);
|
||||
this.openPages.set(
|
||||
newPageMeta.name,
|
||||
new PageState(this.createEditorState(pageData.text), 0, newPageMeta)
|
||||
);
|
||||
await this.loadPage(currentPageName);
|
||||
}
|
||||
});
|
||||
}, watchInterval);
|
||||
}
|
||||
|
||||
focus() {
|
||||
|
@ -420,37 +434,24 @@ export class Editor {
|
|||
}
|
||||
|
||||
async navigate(name: string) {
|
||||
location.hash = encodeURIComponent(name);
|
||||
}
|
||||
|
||||
hashChange() {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
await this.save();
|
||||
const pageName = decodeURIComponent(location.hash.substring(1));
|
||||
console.log("Now navigating to", pageName);
|
||||
|
||||
if (!this.editorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadPage(pageName);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
await this.pageNavigator.navigate(name);
|
||||
}
|
||||
|
||||
async loadPage(pageName: string) {
|
||||
let pageState = this.openPages.get(pageName);
|
||||
if (!pageState) {
|
||||
let pageData = await this.fs.readPage(pageName);
|
||||
let pageData = await this.space.readPage(pageName);
|
||||
pageState = new PageState(
|
||||
this.createEditorState(pageData.text),
|
||||
0,
|
||||
pageData.meta
|
||||
);
|
||||
this.openPages.set(pageName, pageState!);
|
||||
} else {
|
||||
// Loaded page from in-mory cache, let's async see if this page hasn't been updated
|
||||
this.checkForNewVersion(pageState.meta).catch((e) => {
|
||||
console.error("Failed to check for new version");
|
||||
});
|
||||
}
|
||||
this.editorView!.setState(pageState!.editorState);
|
||||
this.editorView!.scrollDOM.scrollTop = pageState!.scrollTop;
|
||||
|
@ -459,16 +460,15 @@ export class Editor {
|
|||
type: "page-loaded",
|
||||
meta: pageState.meta,
|
||||
});
|
||||
}
|
||||
|
||||
addListeners() {
|
||||
this.$hashChange = this.hashChange.bind(this);
|
||||
window.addEventListener("hashchange", this.$hashChange);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.$hashChange) {
|
||||
window.removeEventListener("hashchange", this.$hashChange);
|
||||
let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
|
||||
if (
|
||||
(indexerPageMeta &&
|
||||
pageState.meta.lastModified.getTime() !==
|
||||
indexerPageMeta.lastModified.getTime()) ||
|
||||
!indexerPageMeta
|
||||
) {
|
||||
await this.indexPage(pageState.editorState.sliceDoc(), pageState.meta);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,12 +477,6 @@ export class Editor {
|
|||
this.viewState = viewState;
|
||||
this.viewDispatch = dispatch;
|
||||
|
||||
useEffect(() => {
|
||||
if (!location.hash) {
|
||||
this.navigate("start");
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Auto save
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => {
|
||||
|
@ -508,17 +502,13 @@ export class Editor {
|
|||
{viewState.showPageNavigator && (
|
||||
<PageNavigator
|
||||
allPages={viewState.allPages}
|
||||
currentPage={this.currentPage}
|
||||
onNavigate={(page) => {
|
||||
dispatch({ type: "stop-navigate" });
|
||||
editor!.focus();
|
||||
editor.focus();
|
||||
if (page) {
|
||||
editor
|
||||
?.save()
|
||||
.then(() => {
|
||||
editor!.navigate(page);
|
||||
})
|
||||
.catch((e) => {
|
||||
alert("Could not save page, not switching");
|
||||
safeRun(async () => {
|
||||
editor.navigate(page);
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import { Dexie, Table } from "dexie";
|
||||
import { AppEventDispatcher, IndexEvent } from "./app_event";
|
||||
import { Space } from "./space";
|
||||
import { PageMeta } from "./types";
|
||||
|
||||
function constructKey(pageName: string, key: string): string {
|
||||
return `${pageName}:${key}`;
|
||||
}
|
||||
|
||||
function cleanKey(pageName: string, fromKey: string): string {
|
||||
return fromKey.substring(pageName.length + 1);
|
||||
}
|
||||
|
||||
export type KV = {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export class Indexer {
|
||||
db: Dexie;
|
||||
pageIndex: Table;
|
||||
space: Space;
|
||||
|
||||
constructor(name: string, space: Space) {
|
||||
this.db = new Dexie(name);
|
||||
this.space = space;
|
||||
this.db.version(1).stores({
|
||||
pageIndex: "ck, page, key",
|
||||
});
|
||||
this.pageIndex = this.db.table("pageIndex");
|
||||
}
|
||||
|
||||
async clearPageIndexForPage(pageName: string) {
|
||||
await this.pageIndex.where({ page: pageName }).delete();
|
||||
}
|
||||
|
||||
async clearPageIndex() {
|
||||
await this.pageIndex.clear();
|
||||
}
|
||||
|
||||
async setPageIndexPageMeta(pageName: string, meta: PageMeta) {
|
||||
await this.set(pageName, "$meta", {
|
||||
lastModified: meta.lastModified.getTime(),
|
||||
});
|
||||
}
|
||||
|
||||
async getPageIndexPageMeta(pageName: string): Promise<PageMeta | null> {
|
||||
let meta = await this.get(pageName, "$meta");
|
||||
if (meta) {
|
||||
return {
|
||||
name: pageName,
|
||||
lastModified: new Date(meta.lastModified),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async indexPage(
|
||||
appEventDispatcher: AppEventDispatcher,
|
||||
pageMeta: PageMeta,
|
||||
text: string,
|
||||
withFlush: boolean
|
||||
) {
|
||||
if (withFlush) {
|
||||
await this.clearPageIndexForPage(pageMeta.name);
|
||||
}
|
||||
let indexEvent: IndexEvent = {
|
||||
name: pageMeta.name,
|
||||
text,
|
||||
};
|
||||
await appEventDispatcher.dispatchAppEvent("page:index", indexEvent);
|
||||
await this.setPageIndexPageMeta(pageMeta.name, pageMeta);
|
||||
}
|
||||
|
||||
async reindexSpace(space: Space, appEventDispatcher: AppEventDispatcher) {
|
||||
await this.clearPageIndex();
|
||||
let allPages = await space.listPages();
|
||||
// TODO: Parallelize?
|
||||
for (let page of allPages) {
|
||||
let pageData = await space.readPage(page.name);
|
||||
await this.indexPage(
|
||||
appEventDispatcher,
|
||||
pageData.meta,
|
||||
pageData.text,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async set(pageName: string, key: string, value: any) {
|
||||
await this.pageIndex.put({
|
||||
ck: constructKey(pageName, key),
|
||||
page: pageName,
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
async batchSet(pageName: string, kvs: KV[]) {
|
||||
await this.pageIndex.bulkPut(
|
||||
kvs.map(({ key, value }) => ({
|
||||
ck: constructKey(pageName, key),
|
||||
key: key,
|
||||
page: pageName,
|
||||
value: value,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async get(pageName: string, key: string): Promise<any | null> {
|
||||
let result = await this.pageIndex.get({
|
||||
ck: constructKey(pageName, key),
|
||||
});
|
||||
return result ? result.value : null;
|
||||
}
|
||||
|
||||
async scanPrefixForPage(
|
||||
pageName: string,
|
||||
keyPrefix: string
|
||||
): Promise<{ key: string; value: any }[]> {
|
||||
let results = await this.pageIndex
|
||||
.where("ck")
|
||||
.startsWith(constructKey(pageName, keyPrefix))
|
||||
.toArray();
|
||||
return results.map((result) => ({
|
||||
key: cleanKey(pageName, result.key),
|
||||
value: result.value,
|
||||
}));
|
||||
}
|
||||
|
||||
async scanPrefixGlobal(
|
||||
keyPrefix: string
|
||||
): Promise<{ key: string; value: any }[]> {
|
||||
let results = await this.pageIndex
|
||||
.where("key")
|
||||
.startsWith(keyPrefix)
|
||||
.toArray();
|
||||
return results.map((result) => ({
|
||||
key: result.key,
|
||||
value: result.value,
|
||||
}));
|
||||
}
|
||||
|
||||
async deletePrefixForPage(pageName: string, keyPrefix: string) {
|
||||
await this.pageIndex
|
||||
.where("ck")
|
||||
.startsWith(constructKey(pageName, keyPrefix))
|
||||
.delete();
|
||||
}
|
||||
|
||||
async delete(pageName: string, key: string) {
|
||||
await this.pageIndex.delete(constructKey(pageName, key));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { safeRun } from "./util";
|
||||
|
||||
export interface IPageNavigator {
|
||||
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void;
|
||||
navigate(page: string): void;
|
||||
getCurrentPage(): string;
|
||||
}
|
||||
|
||||
function encodePageUrl(name: string): string {
|
||||
return name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
function decodePageUrl(url: string): string {
|
||||
return url.replaceAll("_", " ");
|
||||
}
|
||||
|
||||
export class PathPageNavigator implements IPageNavigator {
|
||||
navigationResolve?: (value: undefined) => void;
|
||||
async navigate(page: string) {
|
||||
console.log("Pushing state", page);
|
||||
window.history.pushState({ page: page }, page, `/${encodePageUrl(page)}`);
|
||||
window.dispatchEvent(new PopStateEvent("popstate"));
|
||||
await new Promise<undefined>((resolve) => {
|
||||
this.navigationResolve = resolve;
|
||||
});
|
||||
this.navigationResolve = undefined;
|
||||
}
|
||||
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
|
||||
const cb = () => {
|
||||
console.log("State popped", this.getCurrentPage());
|
||||
safeRun(async () => {
|
||||
await pageLoadCallback(this.getCurrentPage());
|
||||
if (this.navigationResolve) {
|
||||
this.navigationResolve(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener("popstate", cb);
|
||||
cb();
|
||||
}
|
||||
|
||||
getCurrentPage(): string {
|
||||
return decodePageUrl(location.pathname.substring(1));
|
||||
}
|
||||
}
|
||||
|
||||
export class HashPageNavigator implements IPageNavigator {
|
||||
navigationResolve?: (value: undefined) => void;
|
||||
async navigate(page: string) {
|
||||
location.hash = encodePageUrl(page);
|
||||
await new Promise<undefined>((resolve) => {
|
||||
this.navigationResolve = resolve;
|
||||
});
|
||||
this.navigationResolve = undefined;
|
||||
}
|
||||
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
|
||||
const cb = () => {
|
||||
safeRun(async () => {
|
||||
await pageLoadCallback(this.getCurrentPage());
|
||||
if (this.navigationResolve) {
|
||||
this.navigationResolve(undefined);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.addEventListener("hashchange", cb);
|
||||
cb();
|
||||
}
|
||||
getCurrentPage(): string {
|
||||
return decodePageUrl(location.hash.substring(1));
|
||||
}
|
||||
}
|
|
@ -2,6 +2,11 @@ import { styleTags } from "@codemirror/highlight";
|
|||
import { MarkdownConfig, TaskList } from "@lezer/markdown";
|
||||
import { commonmark, mkLang } from "./markdown/markdown";
|
||||
import * as ct from "./customtags";
|
||||
import { pageLinkRegex } from "./constant";
|
||||
|
||||
const pageLinkRegexPrefix = new RegExp(
|
||||
"^" + pageLinkRegex.toString().slice(1, -1)
|
||||
);
|
||||
|
||||
const WikiLink: MarkdownConfig = {
|
||||
defineNodes: ["WikiLink", "WikiLinkPage"],
|
||||
|
@ -12,13 +17,13 @@ const WikiLink: MarkdownConfig = {
|
|||
let match: RegExpMatchArray | null;
|
||||
if (
|
||||
next != 91 /* '[' */ ||
|
||||
!(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))
|
||||
!(match = pageLinkRegexPrefix.exec(cx.slice(pos, cx.end)))
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
return cx.addElement(
|
||||
cx.elt("WikiLink", pos, pos + match[0].length + 1, [
|
||||
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 1),
|
||||
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 2),
|
||||
])
|
||||
);
|
||||
},
|
||||
|
|
|
@ -9,6 +9,11 @@ export default function reducer(
|
|||
case "page-loaded":
|
||||
return {
|
||||
...state,
|
||||
allPages: state.allPages.map((pageMeta) =>
|
||||
pageMeta.name === action.meta.name
|
||||
? { ...pageMeta, lastOpened: new Date() }
|
||||
: pageMeta
|
||||
),
|
||||
currentPage: action.meta,
|
||||
isSaved: true,
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ export interface Space {
|
|||
listPages(): Promise<PageMeta[]>;
|
||||
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
|
||||
writePage(name: string, text: string): Promise<PageMeta>;
|
||||
deletePage(name: string): Promise<void>;
|
||||
getPageMeta(name: string): Promise<PageMeta>;
|
||||
}
|
||||
|
||||
|
@ -12,6 +13,7 @@ export class HttpRemoteSpace implements Space {
|
|||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
async listPages(): Promise<PageMeta[]> {
|
||||
let req = await fetch(this.url, {
|
||||
method: "GET",
|
||||
|
@ -22,6 +24,7 @@ export class HttpRemoteSpace implements Space {
|
|||
lastModified: new Date(meta.lastModified),
|
||||
}));
|
||||
}
|
||||
|
||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
let req = await fetch(`${this.url}/${name}`, {
|
||||
method: "GET",
|
||||
|
@ -34,6 +37,7 @@ export class HttpRemoteSpace implements Space {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
async writePage(name: string, text: string): Promise<PageMeta> {
|
||||
let req = await fetch(`${this.url}/${name}`, {
|
||||
method: "PUT",
|
||||
|
@ -47,6 +51,15 @@ export class HttpRemoteSpace implements Space {
|
|||
};
|
||||
}
|
||||
|
||||
async deletePage(name: string): Promise<void> {
|
||||
let req = await fetch(`${this.url}/${name}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (req.status !== 200) {
|
||||
throw Error(`Failed to delete page: ${req.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getPageMeta(name: string): Promise<PageMeta> {
|
||||
let req = await fetch(`${this.url}/${name}`, {
|
||||
method: "OPTIONS",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Editor } from "../editor";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import { Transaction } from "@codemirror/state";
|
||||
import { PageMeta } from "../types";
|
||||
|
||||
type SyntaxNode = {
|
||||
name: string;
|
||||
|
@ -26,6 +27,9 @@ function ensureAnchor(expr: any, start: boolean) {
|
|||
}
|
||||
|
||||
export default (editor: Editor) => ({
|
||||
"editor.getCurrentPage": (): PageMeta => {
|
||||
return editor.currentPage!;
|
||||
},
|
||||
"editor.getText": () => {
|
||||
return editor.editorView?.state.sliceDoc();
|
||||
},
|
||||
|
@ -120,4 +124,7 @@ export default (editor: Editor) => ({
|
|||
"editor.dispatch": (change: Transaction) => {
|
||||
editor.editorView!.dispatch(change);
|
||||
},
|
||||
"editor.prompt": (message: string): string | null => {
|
||||
return prompt(message);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { Indexer, KV } from "../indexer";
|
||||
|
||||
export default (indexer: Indexer) => ({
|
||||
"indexer.scanPrefixForPage": async (pageName: string, keyPrefix: string) => {
|
||||
return await indexer.scanPrefixForPage(pageName, keyPrefix);
|
||||
},
|
||||
"indexer.scanPrefixGlobal": async (keyPrefix: string) => {
|
||||
return await indexer.scanPrefixGlobal(keyPrefix);
|
||||
},
|
||||
"indexer.get": async (pageName: string, key: string): Promise<any> => {
|
||||
return await indexer.get(pageName, key);
|
||||
},
|
||||
"indexer.set": async (pageName: string, key: string, value: any) => {
|
||||
await indexer.set(pageName, key, value);
|
||||
},
|
||||
"indexer.batchSet": async (pageName: string, kvs: KV[]) => {
|
||||
await indexer.batchSet(pageName, kvs);
|
||||
},
|
||||
"indexer.delete": async (pageName: string, key: string) => {
|
||||
await indexer.delete(pageName, key);
|
||||
},
|
||||
});
|
|
@ -5,12 +5,30 @@ export default (editor: Editor) => ({
|
|||
"space.listPages": (): PageMeta[] => {
|
||||
return editor.viewState.allPages;
|
||||
},
|
||||
"space.reloadPageList": async () => {
|
||||
await editor.loadPageList();
|
||||
},
|
||||
"space.reindex": async () => {
|
||||
await editor.indexer.reindexSpace(editor.space, editor);
|
||||
},
|
||||
"space.readPage": async (
|
||||
name: string
|
||||
): Promise<{ text: string; meta: PageMeta }> => {
|
||||
return await editor.fs.readPage(name);
|
||||
return await editor.space.readPage(name);
|
||||
},
|
||||
"space.writePage": async (name: string, text: string): Promise<PageMeta> => {
|
||||
return await editor.fs.writePage(name, text);
|
||||
return await editor.space.writePage(name, text);
|
||||
},
|
||||
"space.deletePage": async (name: string) => {
|
||||
console.log("Clearing page index", name);
|
||||
await editor.indexer.clearPageIndexForPage(name);
|
||||
// If we're deleting the current page, navigate to the start page
|
||||
if (editor.currentPage?.name === name) {
|
||||
await editor.navigate("start");
|
||||
}
|
||||
// Remove page from open pages in editor
|
||||
editor.openPages.delete(name);
|
||||
console.log("Deleting page");
|
||||
await editor.space.deletePage(name);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ export type PageMeta = {
|
|||
name: string;
|
||||
lastModified: Date;
|
||||
created?: boolean;
|
||||
lastOpened?: Date;
|
||||
};
|
||||
|
||||
export type AppCommand = {
|
||||
|
|
|
@ -1186,6 +1186,11 @@ detect-libc@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||
|
||||
dexie@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
|
||||
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
|
||||
|
||||
dom-serializer@^1.0.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
|
||||
|
|
Loading…
Reference in New Issue