Added pageNamespace hook support
parent
cd89634fd6
commit
9a6a86f8b5
|
@ -4,12 +4,14 @@ import { CronHookT } from "@plugos/plugos/hooks/node_cron";
|
|||
import { EventHookT } from "@plugos/plugos/hooks/event";
|
||||
import { CommandHookT } from "@silverbulletmd/web/hooks/command";
|
||||
import { SlashCommandHookT } from "@silverbulletmd/web/hooks/slash_command";
|
||||
import { PageNamespaceHookT } from "../server/hooks/page_namespace";
|
||||
|
||||
export type SilverBulletHooks = CommandHookT &
|
||||
SlashCommandHookT &
|
||||
EndpointHookT &
|
||||
CronHookT &
|
||||
EventHookT;
|
||||
EventHookT &
|
||||
PageNamespaceHookT;
|
||||
|
||||
export type SyntaxExtensions = {
|
||||
syntax?: { [key: string]: NodeDef };
|
||||
|
|
|
@ -56,6 +56,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
|
|||
meta: {
|
||||
name: pageName,
|
||||
lastModified: s.mtime.getTime(),
|
||||
perm: "rw",
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
|
@ -88,6 +89,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
|
|||
return {
|
||||
name: pageName,
|
||||
lastModified: s.mtime.getTime(),
|
||||
perm: "rw",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error while writing page", pageName, e);
|
||||
|
@ -102,6 +104,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
|
|||
return {
|
||||
name: pageName,
|
||||
lastModified: s.mtime.getTime(),
|
||||
perm: "rw",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error while getting page meta", pageName, e);
|
||||
|
@ -132,6 +135,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
|
|||
pages.add({
|
||||
name: this.pathToPageName(fullPath),
|
||||
lastModified: s.mtime.getTime(),
|
||||
perm: "rw",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
|||
result.add({
|
||||
name: pageName,
|
||||
lastModified: meta.lastModified,
|
||||
perm: "rw",
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -160,10 +161,10 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
|||
}
|
||||
|
||||
private responseToMeta(name: string, res: Response): PageMeta {
|
||||
const meta = {
|
||||
return {
|
||||
name,
|
||||
lastModified: +(res.headers.get("Last-Modified") || "0"),
|
||||
perm: (res.headers.get("X-Permission") as "rw" | "ro") || "rw",
|
||||
};
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,9 +72,10 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives {
|
|||
selfUpdate?: boolean,
|
||||
lastModified?: number
|
||||
): Promise<PageMeta> {
|
||||
let meta = {
|
||||
const meta: PageMeta = {
|
||||
name,
|
||||
lastModified: lastModified ? lastModified : Date.now() + this.timeSkew,
|
||||
perm: "rw",
|
||||
};
|
||||
await this.pageTable.put({
|
||||
name,
|
||||
|
|
|
@ -31,9 +31,10 @@ export class Space extends EventEmitter<SpaceEvents> {
|
|||
newPageList.pages.forEach((meta) => {
|
||||
const pageName = meta.name;
|
||||
const oldPageMeta = this.pageMetaCache.get(pageName);
|
||||
const newPageMeta = {
|
||||
const newPageMeta: PageMeta = {
|
||||
name: pageName,
|
||||
lastModified: meta.lastModified,
|
||||
perm: meta.perm,
|
||||
};
|
||||
if (
|
||||
!oldPageMeta &&
|
||||
|
|
|
@ -8,3 +8,19 @@ functions:
|
|||
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: "@search/.+"
|
||||
operation: readPage
|
||||
getPageMetaSearch:
|
||||
path: ./search.ts:getPageMetaSearch
|
||||
pageNamespace:
|
||||
pattern: "@search/.+"
|
||||
operation: getPageMeta
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { fullTextIndex, fullTextSearch } from "@plugos/plugos-syscall/fulltext";
|
||||
import { renderToText } from "@silverbulletmd/common/tree";
|
||||
import { PageMeta } from "@silverbulletmd/common/types";
|
||||
import { scanPrefixGlobal } from "@silverbulletmd/plugos-silverbullet-syscall";
|
||||
import {
|
||||
navigate,
|
||||
prompt,
|
||||
} from "@silverbulletmd/plugos-silverbullet-syscall/editor";
|
||||
import { IndexTreeEvent } from "@silverbulletmd/web/app_event";
|
||||
import { applyQuery, QueryProviderEvent } from "../query/engine";
|
||||
import { removeQueries } from "../query/util";
|
||||
|
@ -38,3 +43,37 @@ export async function queryProvider({
|
|||
results = applyQuery(query, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function searchCommand() {
|
||||
let phrase = await prompt("Search for: ");
|
||||
if (phrase) {
|
||||
await navigate(`@search/${phrase}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readPageSearch(
|
||||
name: string
|
||||
): Promise<{ text: string; meta: PageMeta }> {
|
||||
let phrase = name.substring("@search/".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",
|
||||
};
|
||||
}
|
||||
|
|
|
@ -36,6 +36,8 @@ import {
|
|||
ensureFTSTable,
|
||||
fullTextSearchSyscalls,
|
||||
} from "@plugos/plugos/syscalls/fulltext.knex_sqlite";
|
||||
import { PlugSpacePrimitives } from "./hooks/plug_space_primitives";
|
||||
import { PageNamespaceHook } from "./hooks/page_namespace";
|
||||
|
||||
const safeFilename = /^[a-zA-Z0-9_\-\.]+$/;
|
||||
|
||||
|
@ -69,9 +71,14 @@ export class ExpressServer {
|
|||
// Setup system
|
||||
this.eventHook = new EventHook();
|
||||
this.system.addHook(this.eventHook);
|
||||
let namespaceHook = new PageNamespaceHook();
|
||||
this.system.addHook(namespaceHook);
|
||||
this.space = new Space(
|
||||
new EventedSpacePrimitives(
|
||||
new DiskSpacePrimitives(options.pagesPath),
|
||||
new PlugSpacePrimitives(
|
||||
new DiskSpacePrimitives(options.pagesPath),
|
||||
namespaceHook
|
||||
),
|
||||
this.eventHook
|
||||
),
|
||||
true
|
||||
|
@ -227,6 +234,7 @@ export class ExpressServer {
|
|||
let pageData = await this.space.readPage(pageName);
|
||||
res.status(200);
|
||||
res.header("Last-Modified", "" + pageData.meta.lastModified);
|
||||
res.header("X-Permission", pageData.meta.perm);
|
||||
res.header("Content-Type", "text/markdown");
|
||||
res.send(pageData.text);
|
||||
} catch (e) {
|
||||
|
@ -251,6 +259,7 @@ export class ExpressServer {
|
|||
);
|
||||
res.status(200);
|
||||
res.header("Last-Modified", "" + meta.lastModified);
|
||||
res.header("X-Permission", meta.perm);
|
||||
res.send("OK");
|
||||
} catch (err) {
|
||||
res.status(500);
|
||||
|
@ -264,6 +273,7 @@ export class ExpressServer {
|
|||
const meta = await this.space.getPageMeta(pageName);
|
||||
res.status(200);
|
||||
res.header("Last-Modified", "" + meta.lastModified);
|
||||
res.header("X-Permission", meta.perm);
|
||||
res.header("Content-Type", "text/markdown");
|
||||
res.send("");
|
||||
} catch (e) {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { Plug } from "@plugos/plugos/plug";
|
||||
import { System } from "@plugos/plugos/system";
|
||||
import { Hook, Manifest } from "@plugos/plugos/types";
|
||||
import { Express, NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
export type PageNamespaceOperation =
|
||||
| "readPage"
|
||||
| "writePage"
|
||||
| "listPages"
|
||||
| "getPageMeta"
|
||||
| "deletePage";
|
||||
|
||||
export type PageNamespaceDef = {
|
||||
pattern: string;
|
||||
operation: PageNamespaceOperation;
|
||||
};
|
||||
|
||||
export type PageNamespaceHookT = {
|
||||
pageNamespace?: PageNamespaceDef;
|
||||
};
|
||||
|
||||
type SpaceFunction = {
|
||||
operation: PageNamespaceOperation;
|
||||
pattern: RegExp;
|
||||
plug: Plug<PageNamespaceHookT>;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||
spaceFunctions: SpaceFunction[] = [];
|
||||
constructor() {}
|
||||
|
||||
apply(system: System<PageNamespaceHookT>): void {
|
||||
system.on({
|
||||
plugLoaded: () => {
|
||||
this.updateCache(system);
|
||||
},
|
||||
plugUnloaded: () => {
|
||||
this.updateCache(system);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateCache(system: System<PageNamespaceHookT>) {
|
||||
this.spaceFunctions = [];
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
if (plug.manifest?.functions) {
|
||||
for (let [funcName, funcDef] of Object.entries(
|
||||
plug.manifest.functions
|
||||
)) {
|
||||
if (funcDef.pageNamespace) {
|
||||
this.spaceFunctions.push({
|
||||
operation: funcDef.pageNamespace.operation,
|
||||
pattern: new RegExp(funcDef.pageNamespace.pattern),
|
||||
plug,
|
||||
name: funcName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<PageNamespaceHookT>): string[] {
|
||||
let errors: string[] = [];
|
||||
if (!manifest.functions) {
|
||||
return [];
|
||||
}
|
||||
for (let [funcName, funcDef] of Object.entries(manifest.functions)) {
|
||||
if (funcDef.pageNamespace) {
|
||||
if (!funcDef.pageNamespace.pattern) {
|
||||
errors.push(`Function ${funcName} has a namespace but no pattern`);
|
||||
}
|
||||
if (!funcDef.pageNamespace.operation) {
|
||||
errors.push(`Function ${funcName} has a namespace but no operation`);
|
||||
}
|
||||
if (
|
||||
!["readPage", "writePage", "getPageMeta", "listPages"].includes(
|
||||
funcDef.pageNamespace.operation
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`Function ${funcName} has an invalid operation ${funcDef.pageNamespace.operation}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
import { Plug } from "@plugos/plugos/plug";
|
||||
import { SpacePrimitives } from "@silverbulletmd/common/spaces/space_primitives";
|
||||
import { PageMeta } from "@silverbulletmd/common/types";
|
||||
import { PageNamespaceHook, PageNamespaceOperation } from "./page_namespace";
|
||||
|
||||
export class PlugSpacePrimitives implements SpacePrimitives {
|
||||
constructor(
|
||||
private wrapped: SpacePrimitives,
|
||||
private hook: PageNamespaceHook
|
||||
) {}
|
||||
|
||||
performOperation(
|
||||
type: PageNamespaceOperation,
|
||||
pageName: string,
|
||||
...args: any[]
|
||||
): Promise<any> | false {
|
||||
for (let { operation, pattern, plug, name } of this.hook.spaceFunctions) {
|
||||
if (operation === type && pageName.match(pattern)) {
|
||||
return plug.invoke(name, [pageName, ...args]);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async fetchPageList(): Promise<{
|
||||
pages: Set<PageMeta>;
|
||||
nowTimestamp: number;
|
||||
}> {
|
||||
let allPages = new Set<PageMeta>();
|
||||
for (let { plug, name, operation } of this.hook.spaceFunctions) {
|
||||
if (operation === "listPages") {
|
||||
for (let pm of await plug.invoke(name, [])) {
|
||||
allPages.add(pm);
|
||||
}
|
||||
}
|
||||
}
|
||||
let result = await this.wrapped.fetchPageList();
|
||||
for (let pm of result.pages) {
|
||||
allPages.add(pm);
|
||||
}
|
||||
return {
|
||||
nowTimestamp: result.nowTimestamp,
|
||||
pages: allPages,
|
||||
};
|
||||
}
|
||||
|
||||
readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
let result = this.performOperation("readPage", name);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return this.wrapped.readPage(name);
|
||||
}
|
||||
|
||||
getPageMeta(name: string): Promise<PageMeta> {
|
||||
let result = this.performOperation("getPageMeta", name);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return this.wrapped.getPageMeta(name);
|
||||
}
|
||||
|
||||
writePage(
|
||||
name: string,
|
||||
text: string,
|
||||
selfUpdate?: boolean,
|
||||
lastModified?: number
|
||||
): Promise<PageMeta> {
|
||||
let result = this.performOperation(
|
||||
"writePage",
|
||||
name,
|
||||
text,
|
||||
selfUpdate,
|
||||
lastModified
|
||||
);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return this.wrapped.writePage(name, text, selfUpdate, lastModified);
|
||||
}
|
||||
|
||||
deletePage(name: string): Promise<void> {
|
||||
let result = this.performOperation("deletePage", name);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return this.wrapped.deletePage(name);
|
||||
}
|
||||
|
||||
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
||||
return this.wrapped.proxySyscall(plug, name, args);
|
||||
}
|
||||
|
||||
invokeFunction(
|
||||
plug: Plug<any>,
|
||||
env: string,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any> {
|
||||
return this.wrapped.invokeFunction(plug, env, name, args);
|
||||
}
|
||||
}
|
|
@ -159,6 +159,7 @@ export function FilterList({
|
|||
}
|
||||
break;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
highlightSpecialChars,
|
||||
KeyBinding,
|
||||
keymap,
|
||||
runScopeHandlers,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from "@codemirror/view";
|
||||
|
@ -56,7 +57,7 @@ import {
|
|||
MDExt,
|
||||
} from "@silverbulletmd/common/markdown_ext";
|
||||
import { FilterList } from "./components/filter";
|
||||
import { FilterOption } from "@silverbulletmd/common/types";
|
||||
import { FilterOption, PageMeta } from "@silverbulletmd/common/types";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import sandboxSyscalls from "@plugos/plugos/syscalls/sandbox";
|
||||
import globalModules from "../common/dist/global.plug.json";
|
||||
|
@ -144,6 +145,16 @@ export class Editor {
|
|||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Make keyboard shortcuts work even when the editor is in read only mode or not focused
|
||||
window.addEventListener("keydown", (ev) => {
|
||||
if (!this.editorView?.hasFocus) {
|
||||
console.log("Window-level keyboard event", ev);
|
||||
if (runScopeHandlers(this.editorView!, ev, "editor")) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get currentPage(): string | undefined {
|
||||
|
@ -454,7 +465,10 @@ export class Editor {
|
|||
this.createEditorState(this.currentPage, editorView.state.sliceDoc())
|
||||
);
|
||||
if (editorView.contentDOM) {
|
||||
this.tweakEditorDOM(editorView.contentDOM);
|
||||
this.tweakEditorDOM(
|
||||
editorView.contentDOM,
|
||||
this.viewState.perm === "ro"
|
||||
);
|
||||
}
|
||||
|
||||
this.restoreState(this.currentPage);
|
||||
|
@ -516,30 +530,31 @@ export class Editor {
|
|||
console.log("Creating new page", pageName);
|
||||
doc = {
|
||||
text: "",
|
||||
meta: { name: pageName, lastModified: 0 },
|
||||
meta: { name: pageName, lastModified: 0, perm: "rw" } as PageMeta,
|
||||
};
|
||||
}
|
||||
|
||||
let editorState = this.createEditorState(pageName, doc.text);
|
||||
editorView.setState(editorState);
|
||||
if (editorView.contentDOM) {
|
||||
this.tweakEditorDOM(editorView.contentDOM);
|
||||
this.tweakEditorDOM(editorView.contentDOM, doc.meta.perm === "ro");
|
||||
}
|
||||
this.restoreState(pageName);
|
||||
this.space.watchPage(pageName);
|
||||
|
||||
this.viewDispatch({
|
||||
type: "page-loaded",
|
||||
name: pageName,
|
||||
meta: doc.meta,
|
||||
});
|
||||
|
||||
await this.eventHook.dispatchEvent("editor:pageSwitched");
|
||||
}
|
||||
|
||||
tweakEditorDOM(contentDOM: HTMLElement) {
|
||||
tweakEditorDOM(contentDOM: HTMLElement, readOnly: boolean) {
|
||||
contentDOM.spellcheck = true;
|
||||
contentDOM.setAttribute("autocorrect", "on");
|
||||
contentDOM.setAttribute("autocapitalize", "on");
|
||||
contentDOM.setAttribute("contenteditable", readOnly ? "false" : "true");
|
||||
}
|
||||
|
||||
private restoreState(pageName: string) {
|
||||
|
|
|
@ -14,12 +14,13 @@ export default function reducer(
|
|||
...state,
|
||||
allPages: new Set(
|
||||
[...state.allPages].map((pageMeta) =>
|
||||
pageMeta.name === action.name
|
||||
pageMeta.name === action.meta.name
|
||||
? { ...pageMeta, lastOpened: Date.now() }
|
||||
: pageMeta
|
||||
)
|
||||
),
|
||||
currentPage: action.name,
|
||||
perm: action.meta.perm,
|
||||
currentPage: action.meta.name,
|
||||
};
|
||||
case "page-changed":
|
||||
return {
|
||||
|
|
|
@ -18,6 +18,8 @@ export type ActionButton = {
|
|||
|
||||
export type AppViewState = {
|
||||
currentPage?: string;
|
||||
perm: "ro" | "rw";
|
||||
|
||||
showPageNavigator: boolean;
|
||||
showCommandPalette: boolean;
|
||||
unsavedChanges: boolean;
|
||||
|
@ -45,6 +47,7 @@ export type AppViewState = {
|
|||
};
|
||||
|
||||
export const initialViewState: AppViewState = {
|
||||
perm: "rw",
|
||||
showPageNavigator: false,
|
||||
showCommandPalette: false,
|
||||
unsavedChanges: false,
|
||||
|
@ -68,7 +71,7 @@ export const initialViewState: AppViewState = {
|
|||
};
|
||||
|
||||
export type Action =
|
||||
| { type: "page-loaded"; name: string }
|
||||
| { type: "page-loaded"; meta: PageMeta }
|
||||
| { type: "pages-listed"; pages: Set<PageMeta> }
|
||||
| { type: "page-changed" }
|
||||
| { type: "page-saved" }
|
||||
|
|
Loading…
Reference in New Issue