From 7c825348b21aceccd4da35669c79016fe41c44ff Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sun, 2 Jul 2023 11:25:32 +0200 Subject: [PATCH] Big refactors and fixes * Query regen * Fix anchor completion * Dependency fixes * Changelog update --- Dockerfile | 9 +- build_web.ts | 60 +++---- common/deps.ts | 28 +-- common/markdown_parser/parse-query.js | 23 +-- common/markdown_parser/parse-query.terms.js | 5 +- .../spaces/asset_bundle_space_primitives.ts | 4 +- common/spaces/disk_space_primitives.ts | 8 +- common/spaces/evented_space_primitives.ts | 6 +- common/spaces/fallback_space_primitives.ts | 4 +- common/spaces/file_meta_space_primitives.ts | 27 ++- common/spaces/filtered_space_primitives.ts | 4 +- common/spaces/http_space_primitives.ts | 24 ++- common/spaces/indexeddb_space_primitives.ts | 21 +-- common/spaces/plug_space_primitives.ts | 23 +-- common/spaces/space_primitives.ts | 2 +- common/spaces/sync.ts | 8 +- import_map.json | 32 ++-- plug-api/app_event.ts | 1 + plug-api/lib/fetch.ts | 6 + plug-api/plugos-syscall/fetch.ts | 2 + plug-api/silverbullet-syscall/editor.ts | 4 + plugos/compile.ts | 6 +- plugos/lib/kv_store.dexie.ts | 6 +- plugs/builtin_plugs.ts | 1 + plugs/collab/collab.ts | 8 +- plugs/core/anchor.ts | 4 +- plugs/core/broken_links.ts | 66 +++++++ plugs/core/cloud.ts | 8 +- plugs/core/core.plug.yaml | 21 +++ plugs/core/navigate.ts | 20 +-- plugs/core/page.ts | 25 +-- plugs/core/sync.ts | 7 + plugs/core/template.ts | 78 ++++----- plugs/directive/command.ts | 11 +- plugs/directive/complete.ts | 22 +++ plugs/directive/directive.plug.yaml | 4 + plugs/directive/directives.ts | 29 ++-- plugs/directive/eval_directive.ts | 10 +- plugs/directive/query_directive.ts | 8 +- plugs/directive/template_directive.ts | 23 +-- plugs/directive/util.ts | 75 ++++---- plugs/federation/federation.plug.yaml | 29 ++++ plugs/federation/federation.ts | 162 ++++++++++++++++++ plugs/markdown/markdown_render.ts | 10 +- plugs/markdown/preview.ts | 5 +- plugs/search/search.ts | 8 +- plugs/tasks/task.ts | 21 ++- server/collab.test.ts | 16 +- server/collab.ts | 92 +++++----- server/http_server.ts | 138 ++++++++------- web/cm_plugins/editor_paste.ts | 11 +- web/cm_plugins/inline_image.ts | 18 +- web/cm_plugins/table.ts | 2 +- web/cm_plugins/wiki_link.ts | 7 +- web/collab_manager.ts | 21 +-- web/components/top_bar.tsx | 1 - web/deps.ts | 2 +- web/editor.tsx | 32 ++-- web/service_worker.ts | 13 +- web/sync_service.ts | 6 +- web/syscalls/editor.ts | 4 + website/CHANGELOG.md | 21 ++- website/Mattermost Plugin.md | 35 ---- website/SETTINGS.md | 4 + website/_headers | 2 +- website/template/debug.md | 1 - website/template/page/Daily Note.md | 1 + website/template/plug.md | 2 +- website/template/task.md | 2 +- website/πŸ”Œ Core/Plug Management.md | 1 - website/πŸ”Œ Core/Tags.md | 4 +- website/πŸ”Œ Core/Templates.md | 15 +- website/πŸ”Œ Directive/Query.md | 52 +++--- website/πŸ”Œ Plugs.md | 31 ++-- 74 files changed, 935 insertions(+), 567 deletions(-) create mode 100644 plug-api/lib/fetch.ts create mode 100644 plugs/core/broken_links.ts create mode 100644 plugs/core/sync.ts create mode 100644 plugs/federation/federation.plug.yaml create mode 100644 plugs/federation/federation.ts delete mode 100644 website/Mattermost Plugin.md create mode 100644 website/template/page/Daily Note.md diff --git a/Dockerfile b/Dockerfile index 2bb019c..028c7a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM lukechannings/deno:v1.33.2 +FROM lukechannings/deno:v1.34.3 # The volume that will keep the space data # Create a volume first: # docker volume create myspace @@ -12,8 +12,6 @@ ARG TARGETARCH # Adding tini manually, as it's not included anymore in the new baseimage ENV TINI_VERSION v0.19.0 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} /tini -# Copy the bundled version of silverbullet into the container -ADD ./dist/silverbullet.js /silverbullet.js ENV SILVERBULLET_UID_GID 1000 ENV SILVERBULLET_USERNAME silverbullet @@ -25,6 +23,8 @@ RUN mkdir -p /space \ && chown -R ${SILVERBULLET_USERNAME}:${SILVERBULLET_USERNAME} /space \ && chown -R ${SILVERBULLET_USERNAME}:${SILVERBULLET_USERNAME} /deno-dir \ && chmod +x /tini \ + && apt update \ + && apt install -y git \ && echo "**** cleanup ****" \ && apt-get -y autoremove \ && apt-get clean \ @@ -42,6 +42,9 @@ USER ${SILVERBULLET_USERNAME} # Port map this when running, e.g. with -p 3002:3000 (where 3002 is the host port) EXPOSE 3000 +# Copy the bundled version of silverbullet into the container +ADD ./dist/silverbullet.js /silverbullet.js + # Run the server, allowing to pass in additional argument at run time, e.g. # docker run -p 3002:3000 -v myspace:/space -it zefhemel/silverbullet --user me:letmein ENTRYPOINT /tini -- deno run -A /silverbullet.js -L0.0.0.0 /space diff --git a/build_web.ts b/build_web.ts index 0858d67..516958f 100644 --- a/build_web.ts +++ b/build_web.ts @@ -82,36 +82,36 @@ async function buildCopyBundleAssets() { "dist/plug_asset_bundle.json", ); - await Promise.all([ - esbuild.build({ - entryPoints: [ - { - in: "web/boot.ts", - out: ".client/client", - }, - { - in: "web/service_worker.ts", - out: "service_worker", - }, - ], - outdir: "dist_client_bundle", - absWorkingDir: Deno.cwd(), - bundle: true, - treeShaking: true, - sourcemap: "linked", - minify: true, - jsxFactory: "h", - jsx: "automatic", - jsxFragment: "Fragment", - jsxImportSource: "https://esm.sh/preact@10.11.1", - plugins: [ - ...denoPlugins({ - importMapURL: new URL("./import_map.json", import.meta.url) - .toString(), - }), - ], - }), - ]); + console.log("Now ESBuilding the client and service workers..."); + + await esbuild.build({ + entryPoints: [ + { + in: "web/boot.ts", + out: ".client/client", + }, + { + in: "web/service_worker.ts", + out: "service_worker", + }, + ], + outdir: "dist_client_bundle", + absWorkingDir: Deno.cwd(), + bundle: true, + treeShaking: true, + sourcemap: "linked", + minify: true, + jsxFactory: "h", + jsx: "automatic", + jsxFragment: "Fragment", + jsxImportSource: "https://esm.sh/preact@10.11.1", + plugins: [ + ...denoPlugins({ + importMapURL: new URL("./import_map.json", import.meta.url) + .toString(), + }), + ], + }); // Patch the service_worker {{CACHE_NAME}} let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js"); diff --git a/common/deps.ts b/common/deps.ts index fd7f8e7..d5473af 100644 --- a/common/deps.ts +++ b/common/deps.ts @@ -62,7 +62,7 @@ export { } from "@codemirror/view"; export type { DecorationSet, KeyBinding } from "@codemirror/view"; -export { markdown } from "https://esm.sh/@codemirror/lang-markdown@6.1.1?external=@codemirror/state,@lezer/common,@codemirror/language,@lezer/markdown,@codemirror/view,@lezer/highlight,@codemirror/lang-html"; +export { markdown } from "https://esm.sh/@codemirror/lang-markdown@6.1.1?external=@codemirror/state,@lezer/common,@codemirror/language,@lezer/markdown,@codemirror/view,@lezer/highlight,@codemirror/lang-html&target=es2022"; export { EditorSelection, @@ -99,20 +99,20 @@ export { unfoldCode, } from "@codemirror/language"; -export { yaml as yamlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/yaml?external=@codemirror/language"; +export { yaml as yamlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/yaml?external=@codemirror/language&target=es2022"; export { pgSQL as postgresqlLanguage, standardSQL as sqlLanguage, -} from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/sql?external=@codemirror/language"; -export { rust as rustLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/rust?external=@codemirror/language"; -export { css as cssLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/css?external=@codemirror/language"; -export { python as pythonLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/python?external=@codemirror/language"; -export { protobuf as protobufLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/protobuf?external=@codemirror/language"; -export { shell as shellLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/shell?external=@codemirror/language"; -export { swift as swiftLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/swift?external=@codemirror/language"; -export { toml as tomlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/toml?external=@codemirror/language"; -export { xml as xmlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/xml?external=@codemirror/language"; -export { json as jsonLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/javascript?external=@codemirror/language"; +} from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/sql?external=@codemirror/language&target=es2022"; +export { rust as rustLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/rust?external=@codemirror/language&target=es2022"; +export { css as cssLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/css?external=@codemirror/language&target=es2022"; +export { python as pythonLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/python?external=@codemirror/language&target=es2022"; +export { protobuf as protobufLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/protobuf?external=@codemirror/language&target=es2022"; +export { shell as shellLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/shell?external=@codemirror/language&target=es2022"; +export { swift as swiftLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/swift?external=@codemirror/language&target=es2022"; +export { toml as tomlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/toml?external=@codemirror/language&target=es2022"; +export { xml as xmlLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/xml?external=@codemirror/language&target=es2022"; +export { json as jsonLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/javascript?external=@codemirror/language&target=es2022"; export { c as cLanguage, cpp as cppLanguage, @@ -123,12 +123,12 @@ export { objectiveC as objectiveCLanguage, objectiveCpp as objectiveCppLanguage, scala as scalaLanguage, -} from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/clike?external=@codemirror/language"; +} from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/clike?external=@codemirror/language&target=es2022"; export { javascriptLanguage, typescriptLanguage, -} from "https://esm.sh/@codemirror/lang-javascript@6.1.8?external=@codemirror/language,@codemirror/autocomplete,@codemirror/view,@codemirror/state,@codemirror/lint,@lezer/common,@lezer/lr,@lezer/javascript,@codemirror/commands"; +} from "https://esm.sh/@codemirror/lang-javascript@6.1.8?external=@codemirror/language,@codemirror/autocomplete,@codemirror/view,@codemirror/state,@codemirror/lint,@lezer/common,@lezer/lr,@lezer/javascript,@codemirror/commands&target=es2022"; export { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts"; diff --git a/common/markdown_parser/parse-query.js b/common/markdown_parser/parse-query.js index 2bea08e..539d415 100644 --- a/common/markdown_parser/parse-query.js +++ b/common/markdown_parser/parse-query.js @@ -1,21 +1,16 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -import { LRParser } from "@lezer/lr"; +import {LRParser} from "@lezer/lr" export const parser = LRParser.deserialize({ version: 14, - states: - "&`OVQPOOOmQQO'#C^QOQPOOOtQPO'#C`OyQPO'#ClO!OQPO'#CnO!TQPO'#CqO!YQPO'#CsOOQO'#Cv'#CvO!bQQO,58xO!iQQO'#CcO#WQQO'#CbOOQO,58z,58zOOQO,59W,59WO#oQQO,59YO$ZQQO'#D`OOQO,59],59]OOQO,59_,59_OOQO-E6t-E6tO$rQQO,58}OtQPO'#CxO%ZQQO,58|OOQO'#Cp'#CpOOQO1G.t1G.tO%rQPO'#CyO%wQQO,59zOOQO'#Cg'#CgO$rQQO'#CjOOQO'#Cd'#CdOOQO1G.i1G.iOOQO,59d,59dOOQO-E6v-E6vOOQO,59e,59eOOQO-E6w-E6wO&`QPO'#DRO&hQPO,59UO$rQQO'#CwO&mQPO,59mOOQO1G.p1G.pOOQO,59c,59cOOQO-E6u-E6u", - stateData: - "&u~OpOS~ORPO~OTROaSOcTOfUOhVO~OnQX~P[ORYO~OX]O~OR^O~OR_O~OYaOiaO~OnQa~P[OqcOxcOycOzcO{cO|cO}cO!OcO!PcO~O_dOTUXaUXcUXfUXhUXnUX~O!QfO!RfOTbaabacbafbahbanba~OvhOT!SXa!SXc!SXf!SXh!SXn!SX~OXlOYlO[lO]lOrjOsjOtkO~O_dOTUaaUacUafUahUanUa~ORpO~OvhOT!Saa!Sac!Saf!Sah!San!Sa~OvtOwuX~OwvO~OvtOwua~O", - goto: - "#g!TPP!UP!XP!]!`!fPP!oPP!oP!XP!XP!t!XP!XPP!w!}#T#ZPPPPPPP#aPPPPPPPPPPPP#dRQOTWPXR[RQZRRndQmcQrkRwtVlcktRg^QXPRbXQurRxuQeZRoeQi_RqiRskR`U", - nodeNames: - "⚠ Program Query Name WhereClause Where LogicalExpr FilterExpr Value Number String Bool Regex Null List And LimitClause Limit OrderClause Order OrderDirection SelectClause Select RenderClause Render PageRef", + states: "&`OVQPOOOmQQO'#C^QOQPOOOtQPO'#C`OyQPO'#ClO!OQPO'#CnO!TQPO'#CqO!YQPO'#CsOOQO'#Cv'#CvO!bQQO,58xO!iQQO'#CcO#WQQO'#CbOOQO,58z,58zOOQO,59W,59WO#oQQO,59YO$ZQQO'#D`OOQO,59],59]OOQO,59_,59_OOQO-E6t-E6tO$rQQO,58}OtQPO'#CxO%ZQQO,58|OOQO'#Cp'#CpOOQO1G.t1G.tO%rQPO'#CyO%wQQO,59zOOQO'#Cg'#CgO$rQQO'#CjOOQO'#Cd'#CdOOQO1G.i1G.iOOQO,59d,59dOOQO-E6v-E6vOOQO,59e,59eOOQO-E6w-E6wO&`QPO'#DRO&hQPO,59UO$rQQO'#CwO&mQPO,59mOOQO1G.p1G.pOOQO,59c,59cOOQO-E6u-E6u", + stateData: "&u~OpOS~ORPO~OTROaSOcTOfUOhVO~OnQX~P[ORYO~OX]O~OR^O~OR_O~OYaOiaO~OnQa~P[OqcOxcOycOzcO{cO|cO}cO!OcO!PcO~O_dOTUXaUXcUXfUXhUXnUX~O!QfO!RfOTbaabacbafbahbanba~OvhOT!SXa!SXc!SXf!SXh!SXn!SX~OXlOYlO[lO]lOrjOsjOtkO~O_dOTUaaUacUafUahUanUa~ORpO~OvhOT!Saa!Sac!Saf!Sah!San!Sa~OvtOwuX~OwvO~OvtOwua~O", + goto: "#g!TPP!UP!XP!]!`!fPP!oPP!oP!XP!XP!t!XP!XPP!w!}#T#ZPPPPPPP#aPPPPPPPPPPPP#dRQOTWPXR[RQZRRndQmcQrkRwtVlcktRg^QXPRbXQurRxuQeZRoeQi_RqiRskR`U", + nodeNames: "⚠ Program Query Name WhereClause Where LogicalExpr FilterExpr Value Number String Bool Regex Null List And LimitClause Limit OrderClause Order OrderDirection SelectClause Select RenderClause Render PageRef", maxTerm: 50, skippedNodes: [0], repeatNodeCount: 4, - tokenData: - "Ao~R|X^#{pq#{qr$prs%T|}%o}!O%t!P!Q&V!Q![&|!^!_'U!_!`'c!`!a'p!c!}%t!}#O'}#P#Q(n#R#S%t#T#U(s#U#W%t#W#X+Y#X#Y%t#Y#Z-U#Z#]%t#]#^/f#^#`%t#`#a0b#a#b%t#b#c2u#c#d4q#d#f%t#f#g7h#g#h:d#h#i=`#i#k%t#k#l?[#l#o%t#y#z#{$f$g#{#BY#BZ#{$IS$I_#{$Ip$Iq%T$Iq$Ir%T$I|$JO#{$JT$JU#{$KV$KW#{&FU&FV#{~$QYp~X^#{pq#{#y#z#{$f$g#{#BY#BZ#{$IS$I_#{$I|$JO#{$JT$JU#{$KV$KW#{&FU&FV#{~$sP!_!`$v~${Pz~#r#s%O~%TO!O~~%WUOr%Trs%js$Ip%T$Ip$Iq%j$Iq$Ir%j$Ir~%T~%oOY~~%tOv~P%ySRP}!O%t!c!}%t#R#S%t#T#o%t~&[V[~OY&VZ]&V^!P&V!P!Q&q!Q#O&V#O#P&v#P~&V~&vO[~~&yPO~&V~'RPX~!Q![&|~'ZPq~!_!`'^~'cOx~~'hPy~#r#s'k~'pO}~~'uP|~!_!`'x~'}O{~R(SPtQ!}#O(VP(YRO#P(V#P#Q(c#Q~(VP(fP#P#Q(iP(nOiP~(sOw~R(xWRP}!O%t!c!}%t#R#S%t#T#b%t#b#c)b#c#g%t#g#h*^#h#o%tR)gURP}!O%t!c!}%t#R#S%t#T#W%t#W#X)y#X#o%tR*QS_QRP}!O%t!c!}%t#R#S%t#T#o%tR*cURP}!O%t!c!}%t#R#S%t#T#V%t#V#W*u#W#o%tR*|S!RQRP}!O%t!c!}%t#R#S%t#T#o%tR+_URP}!O%t!c!}%t#R#S%t#T#X%t#X#Y+q#Y#o%tR+vURP}!O%t!c!}%t#R#S%t#T#g%t#g#h,Y#h#o%tR,_URP}!O%t!c!}%t#R#S%t#T#V%t#V#W,q#W#o%tR,xS!QQRP}!O%t!c!}%t#R#S%t#T#o%tR-ZTRP}!O%t!c!}%t#R#S%t#T#U-j#U#o%tR-oURP}!O%t!c!}%t#R#S%t#T#`%t#`#a.R#a#o%tR.WURP}!O%t!c!}%t#R#S%t#T#g%t#g#h.j#h#o%tR.oURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y/R#Y#o%tR/YSsQRP}!O%t!c!}%t#R#S%t#T#o%tR/kURP}!O%t!c!}%t#R#S%t#T#b%t#b#c/}#c#o%tR0US!PQRP}!O%t!c!}%t#R#S%t#T#o%tR0gURP}!O%t!c!}%t#R#S%t#T#]%t#]#^0y#^#o%tR1OURP}!O%t!c!}%t#R#S%t#T#a%t#a#b1b#b#o%tR1gURP}!O%t!c!}%t#R#S%t#T#]%t#]#^1y#^#o%tR2OURP}!O%t!c!}%t#R#S%t#T#h%t#h#i2b#i#o%tR2iSaQRP}!O%t!c!}%t#R#S%t#T#o%tR2zURP}!O%t!c!}%t#R#S%t#T#i%t#i#j3^#j#o%tR3cURP}!O%t!c!}%t#R#S%t#T#`%t#`#a3u#a#o%tR3zURP}!O%t!c!}%t#R#S%t#T#`%t#`#a4^#a#o%tR4eSRP]Q}!O%t!c!}%t#R#S%t#T#o%tR4vURP}!O%t!c!}%t#R#S%t#T#f%t#f#g5Y#g#o%tR5_URP}!O%t!c!}%t#R#S%t#T#W%t#W#X5q#X#o%tR5vURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y6Y#Y#o%tR6_URP}!O%t!c!}%t#R#S%t#T#f%t#f#g6q#g#o%tR6vTRPpq7V}!O%t!c!}%t#R#S%t#T#o%tQ7YP#U#V7]Q7`P#m#n7cQ7hOcQR7mURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y8P#Y#o%tR8UURP}!O%t!c!}%t#R#S%t#T#b%t#b#c8h#c#o%tR8mURP}!O%t!c!}%t#R#S%t#T#W%t#W#X9P#X#o%tR9UURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y9h#Y#o%tR9mURP}!O%t!c!}%t#R#S%t#T#f%t#f#g:P#g#o%tR:WSRPhQ}!O%t!c!}%t#R#S%t#T#o%tR:iURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y:{#Y#o%tR;QURP}!O%t!c!}%t#R#S%t#T#`%t#`#a;d#a#o%tR;iURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y;{#Y#o%tR`#j#o%tR>eURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y>w#Y#o%tR?OSrQRP}!O%t!c!}%t#R#S%t#T#o%tR?aURP}!O%t!c!}%t#R#S%t#T#[%t#[#]?s#]#o%tR?xURP}!O%t!c!}%t#R#S%t#T#X%t#X#Y@[#Y#o%tR@aURP}!O%t!c!}%t#R#S%t#T#f%t#f#g@s#g#o%tR@xURP}!O%t!c!}%t#R#S%t#T#X%t#X#YA[#Y#o%tRAcSRPTQ}!O%t!c!}%t#R#S%t#T#o%t", + tokenData: "C`~R|X^#{pq#{qr$prs%T|}%{}!O&Q!P!Q&c!Q![(a!^!_(i!_!`(v!`!a)T!c!}&Q!}#O)b#P#Q*_#R#S&Q#T#U*d#U#W&Q#W#X,y#X#Y&Q#Y#Z.u#Z#]&Q#]#^1V#^#`&Q#`#a2R#a#b&Q#b#c4f#c#d6b#d#f&Q#f#g9X#g#hT#W#o&QR>YURP}!O&Q!c!}&Q#R#S&Q#T#h&Q#h#i>l#i#o&QR>sSRPfQ}!O&Q!c!}&Q#R#S&Q#T#o&QR?UURP}!O&Q!c!}&Q#R#S&Q#T#f&Q#f#g?h#g#o&QR?mURP}!O&Q!c!}&Q#R#S&Q#T#i&Q#i#j@P#j#o&QR@UURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y@h#Y#o&QR@oSrQRP}!O&Q!c!}&Q#R#S&Q#T#o&QRAQURP}!O&Q!c!}&Q#R#S&Q#T#[&Q#[#]Ad#]#o&QRAiURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#YA{#Y#o&QRBQURP}!O&Q!c!}&Q#R#S&Q#T#f&Q#f#gBd#g#o&QRBiURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#YB{#Y#o&QRCSSRPTQ}!O&Q!c!}&Q#R#S&Q#T#o&Q", tokenizers: [0, 1], - topRules: { "Program": [0, 1] }, - tokenPrec: 0, -}); + topRules: {"Program":[0,1]}, + tokenPrec: 0 +}) diff --git a/common/markdown_parser/parse-query.terms.js b/common/markdown_parser/parse-query.terms.js index d1299c8..4b169e8 100644 --- a/common/markdown_parser/parse-query.terms.js +++ b/common/markdown_parser/parse-query.terms.js @@ -1,5 +1,6 @@ // This file was generated by lezer-generator. You probably shouldn't edit it. -export const Program = 1, +export const + Program = 1, Query = 2, Name = 3, WhereClause = 4, @@ -23,4 +24,4 @@ export const Program = 1, Select = 22, RenderClause = 23, Render = 24, - PageRef = 25; + PageRef = 25 diff --git a/common/spaces/asset_bundle_space_primitives.ts b/common/spaces/asset_bundle_space_primitives.ts index 695529f..c2e42b2 100644 --- a/common/spaces/asset_bundle_space_primitives.ts +++ b/common/spaces/asset_bundle_space_primitives.ts @@ -57,7 +57,7 @@ export class AssetBundlePlugSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { if (this.assetBundle.has(name)) { console.warn("Attempted to write to read-only asset file", name); @@ -67,7 +67,7 @@ export class AssetBundlePlugSpacePrimitives implements SpacePrimitives { name, data, selfUpdate, - lastModified, + meta, ); } diff --git a/common/spaces/disk_space_primitives.ts b/common/spaces/disk_space_primitives.ts index 1634bc6..7e8faff 100644 --- a/common/spaces/disk_space_primitives.ts +++ b/common/spaces/disk_space_primitives.ts @@ -71,7 +71,7 @@ export class DiskSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, _selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { const localPath = this.filenameToPath(name); try { @@ -87,9 +87,9 @@ export class DiskSpacePrimitives implements SpacePrimitives { // Actually write the file await Deno.write(file.rid, data); - if (lastModified) { - console.log("Seting mtime to", new Date(lastModified)); - await Deno.futime(file.rid, new Date(), new Date(lastModified)); + if (meta?.lastModified) { + // console.log("Seting mtime to", new Date(meta.lastModified)); + await Deno.futime(file.rid, new Date(), new Date(meta.lastModified)); } file.close(); diff --git a/common/spaces/evented_space_primitives.ts b/common/spaces/evented_space_primitives.ts index f49b612..6e03a89 100644 --- a/common/spaces/evented_space_primitives.ts +++ b/common/spaces/evented_space_primitives.ts @@ -20,13 +20,13 @@ export class EventedSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { const newMeta = await this.wrapped.writeFile( name, data, selfUpdate, - lastModified, + meta, ); // This can happen async if (name.endsWith(".md")) { @@ -47,7 +47,7 @@ export class EventedSpacePrimitives implements SpacePrimitives { console.error("Error dispatching page:saved event", e); }); } - if (name.endsWith(".plug.js")) { + if (name.startsWith("_plug/") && name.endsWith(".plug.js")) { await this.eventHook.dispatchEvent("plug:changed", name); } return newMeta; diff --git a/common/spaces/fallback_space_primitives.ts b/common/spaces/fallback_space_primitives.ts index d67d402..2d26ac5 100644 --- a/common/spaces/fallback_space_primitives.ts +++ b/common/spaces/fallback_space_primitives.ts @@ -35,9 +35,9 @@ export class FallbackSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, selfUpdate?: boolean | undefined, - lastModified?: number | undefined, + meta?: FileMeta, ): Promise { - return this.primary.writeFile(name, data, selfUpdate, lastModified); + return this.primary.writeFile(name, data, selfUpdate, meta); } deleteFile(name: string): Promise { return this.primary.deleteFile(name); diff --git a/common/spaces/file_meta_space_primitives.ts b/common/spaces/file_meta_space_primitives.ts index 0f1f0b2..a0a9a43 100644 --- a/common/spaces/file_meta_space_primitives.ts +++ b/common/spaces/file_meta_space_primitives.ts @@ -43,21 +43,40 @@ export class FileMetaSpacePrimitives implements SpacePrimitives { return this.wrapped.readFile(name); } - getFileMeta(name: string): Promise { - return this.wrapped.getFileMeta(name); + async getFileMeta(name: string): Promise { + const meta = await this.wrapped.getFileMeta(name); + if (name.endsWith(".md")) { + const pageName = name.slice(0, -3); + const additionalMeta = await this.indexSyscalls["index.get"]( + {} as any, + pageName, + "meta:", + ); + if (additionalMeta) { + for (const [k, v] of Object.entries(additionalMeta)) { + if ( + ["name", "lastModified", "size", "perm", "contentType"].includes(k) + ) { + continue; + } + meta[k] = v; + } + } + } + return meta; } writeFile( name: string, data: Uint8Array, selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { return this.wrapped.writeFile( name, data, selfUpdate, - lastModified, + meta, ); } diff --git a/common/spaces/filtered_space_primitives.ts b/common/spaces/filtered_space_primitives.ts index 20b33c1..947bad5 100644 --- a/common/spaces/filtered_space_primitives.ts +++ b/common/spaces/filtered_space_primitives.ts @@ -25,9 +25,9 @@ export class FilteredSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, selfUpdate?: boolean | undefined, - lastModified?: number | undefined, + meta?: FileMeta, ): Promise { - return this.wrapped.writeFile(name, data, selfUpdate, lastModified); + return this.wrapped.writeFile(name, data, selfUpdate, meta); } deleteFile(name: string): Promise { return this.wrapped.deleteFile(name); diff --git a/common/spaces/http_space_primitives.ts b/common/spaces/http_space_primitives.ts index 22bb211..8673ca9 100644 --- a/common/spaces/http_space_primitives.ts +++ b/common/spaces/http_space_primitives.ts @@ -23,7 +23,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { const result = await fetch(url, { ...options }); if ( - result.status === 401 + this.getRealStatus(result) === 401 ) { // Invalid credentials, reloading the browser should trigger authentication console.log("Going to redirect after", url); @@ -33,13 +33,20 @@ export class HttpSpacePrimitives implements SpacePrimitives { return result; } + getRealStatus(r: Response) { + if (r.headers.get("X-Status")) { + return +r.headers.get("X-Status")!; + } + return r.status; + } + async fetchFileList(): Promise { const resp = await this.authenticatedFetch(this.url, { method: "GET", }); if ( - resp.status === 200 && + this.getRealStatus(resp) === 200 && this.expectedSpacePath && resp.headers.get("X-Space-Path") !== this.expectedSpacePath ) { @@ -60,7 +67,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { method: "GET", }, ); - if (res.status === 404) { + if (this.getRealStatus(res) === 404) { throw new Error(`Not found`); } return { @@ -73,13 +80,14 @@ export class HttpSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, _selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { const headers: Record = { "Content-Type": "application/octet-stream", }; - if (lastModified) { - headers["X-Last-Modified"] = "" + lastModified; + if (meta) { + headers["X-Last-Modified"] = "" + meta.lastModified; + headers["X-Perm"] = "" + meta.perm; } const res = await this.authenticatedFetch( @@ -101,7 +109,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { method: "DELETE", }, ); - if (req.status !== 200) { + if (this.getRealStatus(req) !== 200) { throw Error(`Failed to delete file: ${req.statusText}`); } } @@ -113,7 +121,7 @@ export class HttpSpacePrimitives implements SpacePrimitives { method: "OPTIONS", }, ); - if (res.status === 404) { + if (this.getRealStatus(res) === 404) { throw new Error(`Not found`); } return this.responseToMeta(name, res); diff --git a/common/spaces/indexeddb_space_primitives.ts b/common/spaces/indexeddb_space_primitives.ts index 4e4046b..ca86288 100644 --- a/common/spaces/indexeddb_space_primitives.ts +++ b/common/spaces/indexeddb_space_primitives.ts @@ -5,6 +5,7 @@ import { mime } from "../deps.ts"; export type FileContent = { name: string; + meta: FileMeta; data: Uint8Array; }; @@ -35,10 +36,6 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives { async readFile( name: string, ): Promise<{ data: Uint8Array; meta: FileMeta }> { - const fileMeta = await this.filesMetaTable.get(name); - if (!fileMeta) { - throw new Error("Not found"); - } const fileContent = await this.filesContentTable.get(name); if (!fileContent) { throw new Error("Not found"); @@ -46,7 +43,7 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives { return { data: fileContent.data, - meta: fileMeta, + meta: fileContent.meta, }; } @@ -54,18 +51,18 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, _selfUpdate?: boolean, - lastModified?: number, + suggestedMeta?: FileMeta, ): Promise { - const fileMeta: FileMeta = { + const meta: FileMeta = { name, - lastModified: lastModified || Date.now(), + lastModified: suggestedMeta?.lastModified || Date.now(), contentType: mime.getType(name) || "application/octet-stream", size: data.byteLength, - perm: "rw", + perm: suggestedMeta?.perm || "rw", }; - await this.filesContentTable.put({ name, data }); - await this.filesMetaTable.put(fileMeta); - return fileMeta; + await this.filesContentTable.put({ name, data, meta }); + await this.filesMetaTable.put(meta); + return meta; } async deleteFile(name: string): Promise { diff --git a/common/spaces/plug_space_primitives.ts b/common/spaces/plug_space_primitives.ts index fee09e1..349fa1a 100644 --- a/common/spaces/plug_space_primitives.ts +++ b/common/spaces/plug_space_primitives.ts @@ -4,11 +4,6 @@ import { NamespaceOperation, PageNamespaceHook, } from "../hooks/page_namespace.ts"; -import { - base64DecodeDataUrl, - base64EncodedDataUrl, -} from "../../plugos/asset_bundle/base64.ts"; -import { mime } from "../deps.ts"; export class PlugSpacePrimitives implements SpacePrimitives { constructor( @@ -40,6 +35,7 @@ export class PlugSpacePrimitives implements SpacePrimitives { for ( const { operation, pattern, plug, name, env } of this.hook.spaceFunctions ) { + // console.log("Going to match agains pattern", pattern, path); if ( operation === type && path.match(pattern) && (!this.env || (env && env === this.env)) @@ -73,16 +69,13 @@ export class PlugSpacePrimitives implements SpacePrimitives { async readFile( name: string, ): Promise<{ data: Uint8Array; meta: FileMeta }> { - const result: { data: string; meta: FileMeta } | false = await this + const result: { data: Uint8Array; meta: FileMeta } | false = await this .performOperation( "readFile", name, ); if (result) { - return { - data: base64DecodeDataUrl(result.data), - meta: result.meta, - }; + return result; } return this.wrapped.readFile(name); } @@ -99,16 +92,14 @@ export class PlugSpacePrimitives implements SpacePrimitives { name: string, data: Uint8Array, selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise { const result = this.performOperation( "writeFile", name, - base64EncodedDataUrl( - mime.getType(name) || "application/octet-stream", - data, - ), + data, selfUpdate, + meta, ); if (result) { return result; @@ -118,7 +109,7 @@ export class PlugSpacePrimitives implements SpacePrimitives { name, data, selfUpdate, - lastModified, + meta, ); } diff --git a/common/spaces/space_primitives.ts b/common/spaces/space_primitives.ts index 80b1922..891cd4d 100644 --- a/common/spaces/space_primitives.ts +++ b/common/spaces/space_primitives.ts @@ -15,7 +15,7 @@ export interface SpacePrimitives { data: Uint8Array, // Used to decide whether or not to emit change events selfUpdate?: boolean, - lastModified?: number, + meta?: FileMeta, ): Promise; deleteFile(name: string): Promise; } diff --git a/common/spaces/sync.ts b/common/spaces/sync.ts index 8857670..e84eb85 100644 --- a/common/spaces/sync.ts +++ b/common/spaces/sync.ts @@ -135,7 +135,7 @@ export class SpaceSync { name, data, false, - meta.lastModified, + meta, ); snapshot.set(name, [ primaryHash, @@ -157,7 +157,7 @@ export class SpaceSync { name, data, false, - meta.lastModified, + meta, ); snapshot.set(name, [ writtenMeta.lastModified, @@ -219,7 +219,7 @@ export class SpaceSync { name, data, false, - meta.lastModified, + meta, ); snapshot.set(name, [ primaryHash, @@ -243,7 +243,7 @@ export class SpaceSync { name, data, false, - meta.lastModified, + meta, ); snapshot.set(name, [ writtenMeta.lastModified, diff --git a/import_map.json b/import_map.json index b3601a2..0b83e09 100644 --- a/import_map.json +++ b/import_map.json @@ -1,24 +1,24 @@ { "imports": { - "@lezer/common": "https://esm.sh/@lezer/common@1.0.2", - "@lezer/lr": "https://esm.sh/@lezer/lr@1.3.5?external=@lezer/common&target=deno", - "@lezer/markdown": "https://esm.sh/@lezer/markdown@1.0.2?external=@lezer/common,@codemirror/language,@lezer/highlight,@lezer/lr", - "@lezer/javascript": "https://esm.sh/@lezer/javascript@1.4.3?external=@lezer/common,@codemirror/language,@lezer/highlight,@lezer/lr", - "@lezer/highlight": "https://esm.sh/@lezer/highlight@1.1.6?external=@lezer/common,@lezer/lr", - "@lezer/html": "https://esm.sh/@lezer/html@1.3.4?external=@lezer/common,@lezer/lr", + "@lezer/common": "https://esm.sh/@lezer/common@1.0.2&target=es2022", + "@lezer/lr": "https://esm.sh/@lezer/lr@1.3.5?external=@lezer/common&target=es2022", + "@lezer/markdown": "https://esm.sh/@lezer/markdown@1.0.2?external=@lezer/common,@codemirror/language,@lezer/highlight,@lezer/lr&target=es2022", + "@lezer/javascript": "https://esm.sh/@lezer/javascript@1.4.3?external=@lezer/common,@codemirror/language,@lezer/highlight,@lezer/lr&target=es2022", + "@lezer/highlight": "https://esm.sh/@lezer/highlight@1.1.6?external=@lezer/common,@lezer/lr&target=es2022", + "@lezer/html": "https://esm.sh/@lezer/html@1.3.4?external=@lezer/common,@lezer/lr&target=es2022", - "@codemirror/state": "https://esm.sh/@codemirror/state@6.2.1", - "@codemirror/language": "https://esm.sh/@codemirror/language@6.7.0?external=@codemirror/state,@lezer/common,@lezer/lr,@codemirror/view,@lezer/highlight", - "@codemirror/commands": "https://esm.sh/@codemirror/commands@6.2.4?external=@codemirror/state,@codemirror/view", - "@codemirror/view": "https://esm.sh/@codemirror/view@6.9.0?external=@codemirror/state,@lezer/common", - "@codemirror/autocomplete": "https://esm.sh/@codemirror/autocomplete@6.7.1?external=@codemirror/state,@codemirror/commands,@lezer/common,@codemirror/view", - "@codemirror/lint": "https://esm.sh/@codemirror/lint@6.2.1?external=@codemirror/state,@lezer/common", - "@codemirror/lang-css": "https://esm.sh/@codemirror/lang-css@6.2.0?external=@codemirror/language,@codemirror/autocomplete,@codemirror/state,@lezer/lr,@lezer/html", - "@codemirror/lang-html": "https://esm.sh/@codemirror/lang-html@6.4.3?external=@codemirror/language,@codemirror/autocomplete,@codemirror/lang-css,@codemirror/state,@lezer/lr,@lezer/html", - "@codemirror/search": "https://esm.sh/@codemirror/search@6.4.0?external=@codemirror/state,@codemirror/view", + "@codemirror/state": "https://esm.sh/@codemirror/state@6.2.1&target=es2022", + "@codemirror/language": "https://esm.sh/@codemirror/language@6.7.0?external=@codemirror/state,@lezer/common,@lezer/lr,@codemirror/view,@lezer/highlight&target=es2022", + "@codemirror/commands": "https://esm.sh/@codemirror/commands@6.2.4?external=@codemirror/state,@codemirror/view&target=es2022", + "@codemirror/view": "https://esm.sh/@codemirror/view@6.9.0?external=@codemirror/state,@lezer/common&target=es2022", + "@codemirror/autocomplete": "https://esm.sh/@codemirror/autocomplete@6.7.1?external=@codemirror/state,@codemirror/commands,@lezer/common,@codemirror/view&target=es2022", + "@codemirror/lint": "https://esm.sh/@codemirror/lint@6.2.1?external=@codemirror/state,@lezer/common&target=es2022", + "@codemirror/lang-css": "https://esm.sh/@codemirror/lang-css@6.2.0?external=@codemirror/language,@codemirror/autocomplete,@codemirror/state,@lezer/lr,@lezer/html&target=es2022", + "@codemirror/lang-html": "https://esm.sh/@codemirror/lang-html@6.4.3?external=@codemirror/language,@codemirror/autocomplete,@codemirror/lang-css,@codemirror/state,@lezer/lr,@lezer/html&target=es2022", + "@codemirror/search": "https://esm.sh/@codemirror/search@6.4.0?external=@codemirror/state,@codemirror/view&target=es2022", "preact": "https://esm.sh/preact@10.11.1", - "yjs": "https://esm.sh/yjs@13.5.42?target=es2022", + "yjs": "https://esm.sh/yjs@13.5.42?deps=lib0@0.2.70&target=es2022", "$sb/": "./plug-api/", "handlebars": "https://esm.sh/handlebars@4.7.7?target=es2022", "dexie": "https://esm.sh/dexie@3.2.2" diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index 1008f71..fa512fc 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -43,6 +43,7 @@ export type PublishEvent = { }; export type CompleteEvent = { + pageName: string; linePrefix: string; pos: number; }; diff --git a/plug-api/lib/fetch.ts b/plug-api/lib/fetch.ts new file mode 100644 index 0000000..4eb7265 --- /dev/null +++ b/plug-api/lib/fetch.ts @@ -0,0 +1,6 @@ +declare global { + function nativeFetch( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise; +} diff --git a/plug-api/plugos-syscall/fetch.ts b/plug-api/plugos-syscall/fetch.ts index 9b526e2..9195444 100644 --- a/plug-api/plugos-syscall/fetch.ts +++ b/plug-api/plugos-syscall/fetch.ts @@ -13,6 +13,8 @@ export function sandboxFetch( } export function monkeyPatchFetch() { + // @ts-ignore: monkey patching fetch + globalThis.nativeFetch = globalThis.fetch; // @ts-ignore: monkey patching fetch globalThis.fetch = async function ( url: string, diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index f760195..693b1b0 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -129,6 +129,10 @@ export function vimEx(exCommand: string): Promise { return syscall("editor.vimEx", exCommand); } +export function syncSpace(): Promise { + return syscall("editor.syncSpace"); +} + // Folding export function fold() { diff --git a/plugos/compile.ts b/plugos/compile.ts index 74774df..3f810ab 100644 --- a/plugos/compile.ts +++ b/plugos/compile.ts @@ -4,9 +4,9 @@ import { bundleAssets } from "./asset_bundle/builder.ts"; import { Manifest } from "./types.ts"; import { version } from "../version.ts"; -// const workerRuntimeUrl = new URL("./worker_runtime.ts", import.meta.url); -const workerRuntimeUrl = - `https://deno.land/x/silverbullet@${version}/plugos/worker_runtime.ts`; +const workerRuntimeUrl = new URL("./worker_runtime.ts", import.meta.url); +// const workerRuntimeUrl = +// `https://deno.land/x/silverbullet@${version}/plugos/worker_runtime.ts`; export type CompileOptions = { debug?: boolean; diff --git a/plugos/lib/kv_store.dexie.ts b/plugos/lib/kv_store.dexie.ts index f875484..024d173 100644 --- a/plugos/lib/kv_store.dexie.ts +++ b/plugos/lib/kv_store.dexie.ts @@ -5,9 +5,9 @@ export class DexieKVStore implements KVStore { db: Dexie; items: Table; constructor( - private dbName: string, - private tableName: string, - private indexedDB?: any, + dbName: string, + tableName: string, + indexedDB?: any, ) { this.db = new Dexie(dbName, { indexedDB, diff --git a/plugs/builtin_plugs.ts b/plugs/builtin_plugs.ts index 93f099d..7f3b10a 100644 --- a/plugs/builtin_plugs.ts +++ b/plugs/builtin_plugs.ts @@ -8,4 +8,5 @@ export const builtinPlugNames = [ "share", "tasks", "search", + "federation", ]; diff --git a/plugs/collab/collab.ts b/plugs/collab/collab.ts index abc6d3a..5669873 100644 --- a/plugs/collab/collab.ts +++ b/plugs/collab/collab.ts @@ -14,7 +14,6 @@ import { collab, editor, markdown } from "$sb/silverbullet-syscall/mod.ts"; import { nanoid } from "https://esm.sh/nanoid@4.0.0"; import { FileMeta } from "../../common/types.ts"; -import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts"; const defaultServer = "wss://collab.silverbullet.md"; @@ -118,7 +117,7 @@ export function shareNoop() { export function readFileCollab( name: string, -): { data: string; meta: FileMeta } { +): { data: Uint8Array; meta: FileMeta } { if (!name.endsWith(".md")) { throw new Error("Not found"); } @@ -127,10 +126,7 @@ export function readFileCollab( return { // encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl" - data: base64EncodedDataUrl( - "text/markdown", - new TextEncoder().encode(text), - ), + data: new TextEncoder().encode(text), meta: { name, contentType: "text/markdown", diff --git a/plugs/core/anchor.ts b/plugs/core/anchor.ts index 6eb6b1d..05879c1 100644 --- a/plugs/core/anchor.ts +++ b/plugs/core/anchor.ts @@ -1,5 +1,5 @@ import { collectNodesOfType } from "$sb/lib/tree.ts"; -import { editor, index } from "$sb/silverbullet-syscall/mod.ts"; +import { index } from "$sb/silverbullet-syscall/mod.ts"; import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts"; import { removeQueries } from "$sb/lib/query.ts"; @@ -29,7 +29,7 @@ export async function anchorComplete(completeEvent: CompleteEvent) { let [pageRef, anchorRef] = match[1].split("@"); if (!pageRef) { - pageRef = await editor.getCurrentPage(); + pageRef = completeEvent.pageName; } const allAnchors = await index.queryPrefix( `a:${pageRef}:${anchorRef}`, diff --git a/plugs/core/broken_links.ts b/plugs/core/broken_links.ts new file mode 100644 index 0000000..f926b6e --- /dev/null +++ b/plugs/core/broken_links.ts @@ -0,0 +1,66 @@ +import { traverseTree } from "../../plug-api/lib/tree.ts"; +import { + editor, + markdown, + space, +} from "../../plug-api/silverbullet-syscall/mod.ts"; + +export async function brokenLinksCommand() { + const pageName = "BROKEN LINKS"; + await editor.flashNotification("Scanning your space..."); + const allPages = await space.listPages(); + const allPagesMap = new Map(allPages.map((p) => [p.name, true])); + const brokenLinks: { page: string; link: string; pos: number }[] = []; + for (const pageMeta of allPages) { + const text = await space.readPage(pageMeta.name); + const tree = await markdown.parseMarkdown(text); + traverseTree(tree, (tree) => { + if (tree.type === "WikiLinkPage") { + // Add the prefix in the link text + const [pageName] = tree.children![0].text!.split("@"); + if (pageName.startsWith("πŸ’­ ")) { + return true; + } + if ( + pageName && !pageName.startsWith("{{") + ) { + if (!allPagesMap.has(pageName)) { + brokenLinks.push({ + page: pageMeta.name, + link: pageName, + pos: tree.from!, + }); + } + } + } + if (tree.type === "PageRef") { + const pageName = tree.children![0].text!.slice(2, -2); + if (pageName.startsWith("πŸ’­ ")) { + return true; + } + if (!allPagesMap.has(pageName)) { + brokenLinks.push({ + page: pageMeta.name, + link: pageName, + pos: tree.from!, + }); + } + } + + if (tree.type === "DirectiveBody") { + // Don't look inside directive bodies + return true; + } + + return false; + }); + } + const lines: string[] = []; + for (const brokenLink of brokenLinks) { + lines.push( + `* [[${brokenLink.page}@${brokenLink.pos}]]: ${brokenLink.link}`, + ); + } + await space.writePage(pageName, lines.join("\n")); + await editor.navigate(pageName); +} diff --git a/plugs/core/cloud.ts b/plugs/core/cloud.ts index 748b0e6..96be1d5 100644 --- a/plugs/core/cloud.ts +++ b/plugs/core/cloud.ts @@ -1,13 +1,12 @@ import { renderToText, replaceNodesMatching } from "$sb/lib/tree.ts"; import type { FileMeta } from "../../common/types.ts"; import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; -import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts"; export const cloudPrefix = "πŸ’­ "; export async function readFileCloud( name: string, -): Promise<{ data: string; meta: FileMeta } | undefined> { +): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> { const originalUrl = name.substring( cloudPrefix.length, name.length - ".md".length, @@ -38,10 +37,7 @@ export async function readFileCloud( `${cloudPrefix}${originalUrl.split("/")[0]}/`, ); return { - data: base64EncodedDataUrl( - "text/markdown", - new TextEncoder().encode(text), - ), + data: new TextEncoder().encode(text), meta: { name, contentType: "text/markdown", diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 1d4d444..1e90886 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -52,6 +52,13 @@ functions: command: name: "Page: Copy" + syncSpaceCommand: + path: "./sync.ts:syncSpaceCommand" + command: + name: "Sync: Now" + key: "Alt-Shift-s" + mac: "Cmd-Shift-s" + # Attachments attachmentQueryProvider: path: ./attachment.ts:attachmentQueryProvider @@ -180,6 +187,15 @@ functions: description: Turn line into h4 header match: "^#*\\s*" replace: "#### " + insertCodeBlock: + redirect: insertTemplateText + slashCommand: + name: code + description: Insert code block + value: | + ``` + |^| + ``` newPage: path: ./page.ts:newPageCommand @@ -433,3 +449,8 @@ functions: name: "Editor: Vim: Load VIMRC" events: - editor:modeswitch + + brokenLinksCommand: + path: ./broken_links.ts:brokenLinksCommand + command: + name: "Broken Links: Show" diff --git a/plugs/core/navigate.ts b/plugs/core/navigate.ts index 6461867..ec56d86 100644 --- a/plugs/core/navigate.ts +++ b/plugs/core/navigate.ts @@ -35,6 +35,7 @@ async function actionClickOrActionEnter( return; } } + const currentPage = await editor.getCurrentPage(); switch (mdTree.type) { case "WikiLink": { let pageLink = mdTree.children![1]!.children![0].text!; @@ -46,20 +47,20 @@ async function actionClickOrActionEnter( } } if (!pageLink) { - pageLink = await editor.getCurrentPage(); + pageLink = currentPage; } await editor.navigate(pageLink, pos, false, inNewWindow); break; } case "PageRef": { const bracketedPageRef = mdTree.children![0].text!; - await editor.navigate( - // Slicing off the initial [[ and final ]] - bracketedPageRef.substring(2, bracketedPageRef.length - 2), - 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; } case "NakedURL": @@ -71,13 +72,12 @@ async function actionClickOrActionEnter( if (!urlNode) { return; } - let url = urlNode.children![0].text!; + const url = urlNode.children![0].text!; if (url.length <= 1) { return editor.flashNotification("Empty link, ignoring", "error"); } if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) { - url = decodeURIComponent(url); - return editor.openUrl(`/.fs/${url}`); + return editor.openUrl(`/.fs/${decodeURI(url)}`); } else { await editor.openUrl(url); } diff --git a/plugs/core/page.ts b/plugs/core/page.ts index 59c94e2..7cf410a 100644 --- a/plugs/core/page.ts +++ b/plugs/core/page.ts @@ -20,9 +20,9 @@ import { renderToText, replaceNodesMatching, } from "$sb/lib/tree.ts"; -import { applyQuery } from "$sb/lib/query.ts"; +import { applyQuery, removeQueries } from "$sb/lib/query.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; -import { invokeFunction } from "../../plug-api/silverbullet-syscall/system.ts"; +import { invokeFunction } from "$sb/silverbullet-syscall/system.ts"; // Key space: // pl:toPage:pos => pageName @@ -32,6 +32,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) { const backLinks: { key: string; value: string }[] = []; // [[Style Links]] // console.log("Now indexing links for", name); + removeQueries(tree); const pageMeta = await extractFrontmatter(tree); if (Object.keys(pageMeta).length > 0) { // console.log("Extracted page meta data", pageMeta); @@ -44,8 +45,6 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) { await index.set(name, "meta:", pageMeta); } - // throw new Error("Boom"); - collectNodesMatching(tree, (n) => n.type === "WikiLinkPage").forEach((n) => { let toPage = n.children![0].text!; if (toPage.includes("@")) { @@ -106,7 +105,7 @@ export async function copyPage() { await space.getPageMeta(newName); // So when we get to this point, we error out throw new Error( - `Page ${newName} already exists, cannot rename to existing page.`, + `Page ${newName} already exists, cannot rename to existing page.`, ); } catch (e: any) { if (e.message === "Not found") { @@ -165,6 +164,7 @@ export async function renamePage(cmdDef: any) { console.log("All pages containing backlinks", pagesToUpdate); const text = await editor.getText(); + console.log("Writing new page to space"); const newPageMeta = await space.writePage(newName, text); console.log("Navigating to new page"); @@ -198,6 +198,7 @@ export async function renamePage(cmdDef: any) { } const mdTree = await markdown.parseMarkdown(text); addParentPointers(mdTree); + // The links in the page are going to be relative pointers to the old name replaceNodesMatching(mdTree, (n): ParseTree | undefined | null => { if (n.type === "WikiLinkPage") { const pageName = n.children![0].text!; @@ -265,18 +266,20 @@ export async function reindexCommand() { // Completion export async function pageComplete(completeEvent: CompleteEvent) { - const match = /\[\[([^\]@:]*)$/.exec(completeEvent.linePrefix); + const match = /\[\[([^\]@:\{}]*)$/.exec(completeEvent.linePrefix); if (!match) { return null; } const allPages = await space.listPages(); return { from: completeEvent.pos - match[1].length, - options: allPages.map((pageMeta) => ({ - label: pageMeta.name, - boost: pageMeta.lastModified, - type: "page", - })), + options: allPages.map((pageMeta) => { + return { + label: pageMeta.name, + boost: pageMeta.lastModified, + type: "page", + }; + }), }; } diff --git a/plugs/core/sync.ts b/plugs/core/sync.ts new file mode 100644 index 0000000..f53576a --- /dev/null +++ b/plugs/core/sync.ts @@ -0,0 +1,7 @@ +import { editor } from "$sb/silverbullet-syscall/mod.ts"; + +export async function syncSpaceCommand() { + await editor.flashNotification("Syncing space..."); + await editor.syncSpace(); + await editor.flashNotification("Done."); +} diff --git a/plugs/core/template.ts b/plugs/core/template.ts index 3137e09..bf712c7 100644 --- a/plugs/core/template.ts +++ b/plugs/core/template.ts @@ -3,6 +3,10 @@ import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; import { renderToText } from "$sb/lib/tree.ts"; import { niceDate } from "$sb/lib/dates.ts"; import { readSettings } from "$sb/lib/settings_page.ts"; +import { PageMeta } from "../../web/types.ts"; +import { buildHandebarOptions } from "../directive/util.ts"; + +import Handlebars from "handlebars"; export async function instantiateTemplateCommand() { const allPages = await space.listPages(); @@ -36,10 +40,16 @@ export async function instantiateTemplateCommand() { "$disableDirectives", ]); + const tempPageMeta: PageMeta = { + name: "", + lastModified: 0, + perm: "rw", + }; + if (additionalPageMeta.$name) { additionalPageMeta.$name = replaceTemplateVars( additionalPageMeta.$name, - "", + tempPageMeta, ); } @@ -50,6 +60,7 @@ export async function instantiateTemplateCommand() { if (!pageName) { return; } + tempPageMeta.name = pageName; try { // Fails if doesn't exist @@ -67,7 +78,7 @@ export async function instantiateTemplateCommand() { // The preferred scenario, let's keep going } - const pageText = replaceTemplateVars(renderToText(parseTree), pageName); + const pageText = replaceTemplateVars(renderToText(parseTree), tempPageMeta); await space.writePage(pageName, pageText); await editor.navigate(pageName); } @@ -79,6 +90,7 @@ export async function insertSnippet() { }); const cursorPos = await editor.getCursor(); const page = await editor.getCurrentPage(); + const pageMeta = await space.getPageMeta(page); const allSnippets = allPages .filter((pageMeta) => pageMeta.name.startsWith(snippetPrefix)) .map((pageMeta) => ({ @@ -97,10 +109,10 @@ export async function insertSnippet() { } const text = await space.readPage(`${snippetPrefix}${selectedSnippet.name}`); - let templateText = replaceTemplateVars(text, page); + let templateText = replaceTemplateVars(text, pageMeta); const carretPos = templateText.indexOf("|^|"); templateText = templateText.replace("|^|", ""); - templateText = replaceTemplateVars(templateText, page); + templateText = replaceTemplateVars(templateText, pageMeta); await editor.insertAtCursor(templateText); if (carretPos !== -1) { await editor.moveCursor(cursorPos + carretPos); @@ -108,37 +120,9 @@ export async function insertSnippet() { } // TODO: This should probably be replaced with handlebards somehow? -export function replaceTemplateVars(s: string, pageName: string): string { - return s.replaceAll(/\{\{([^\}]+)\}\}/g, (match, v) => { - switch (v) { - case "today": - return niceDate(new Date()); - case "tomorrow": { - const tomorrow = new Date(); - tomorrow.setDate(tomorrow.getDate() + 1); - return niceDate(tomorrow); - } - - case "yesterday": { - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - return niceDate(yesterday); - } - case "lastWeek": { - const lastWeek = new Date(); - lastWeek.setDate(lastWeek.getDate() - 7); - return niceDate(lastWeek); - } - case "nextWeek": { - const nextWeek = new Date(); - nextWeek.setDate(nextWeek.getDate() + 7); - return niceDate(nextWeek); - } - case "page": - return pageName; - } - return match; - }); +export function replaceTemplateVars(s: string, pageMeta: PageMeta): string { + const template = Handlebars.compile(s, { noEscape: true }); + return template({}, buildHandebarOptions(pageMeta)); } export async function quickNoteCommand() { @@ -159,6 +143,7 @@ export async function dailyNoteCommand() { }); const date = niceDate(new Date()); const pageName = `${dailyNotePrefix}${date}`; + let carretPos = 0; try { await space.getPageMeta(pageName); @@ -167,15 +152,25 @@ export async function dailyNoteCommand() { let dailyNoteTemplateText = ""; try { dailyNoteTemplateText = await space.readPage(dailyNoteTemplate); + carretPos = dailyNoteTemplateText.indexOf("|^|"); + if (carretPos === -1) { + carretPos = 0; + } + dailyNoteTemplateText = dailyNoteTemplateText.replace("|^|", ""); } catch { console.warn(`No daily note template found at ${dailyNoteTemplate}`); } + await space.writePage( pageName, - replaceTemplateVars(dailyNoteTemplateText, pageName), + replaceTemplateVars(dailyNoteTemplateText, { + name: pageName, + lastModified: 0, + perm: "rw", + }), ); } - await editor.navigate(pageName); + await editor.navigate(pageName, carretPos); } function getWeekStartDate(monday = false) { @@ -210,7 +205,11 @@ export async function weeklyNoteCommand() { // Doesn't exist, let's create await space.writePage( pageName, - replaceTemplateVars(weeklyNoteTemplateText, pageName), + replaceTemplateVars(weeklyNoteTemplateText, { + name: pageName, + lastModified: 0, + perm: "rw", + }), ); } await editor.navigate(pageName); @@ -222,10 +221,11 @@ export async function weeklyNoteCommand() { export async function insertTemplateText(cmdDef: any) { const cursorPos = await editor.getCursor(); const page = await editor.getCurrentPage(); + const pageMeta = await space.getPageMeta(page); let templateText: string = cmdDef.value; const carretPos = templateText.indexOf("|^|"); templateText = templateText.replace("|^|", ""); - templateText = replaceTemplateVars(templateText, page); + templateText = replaceTemplateVars(templateText, pageMeta); await editor.insertAtCursor(templateText); if (carretPos !== -1) { await editor.moveCursor(cursorPos + carretPos); diff --git a/plugs/directive/command.ts b/plugs/directive/command.ts index 0bee520..e82e417 100644 --- a/plugs/directive/command.ts +++ b/plugs/directive/command.ts @@ -1,4 +1,4 @@ -import { editor, markdown, sync } from "$sb/silverbullet-syscall/mod.ts"; +import { editor, markdown, space, sync } from "$sb/silverbullet-syscall/mod.ts"; import { removeParentPointers, renderToText, @@ -6,11 +6,12 @@ import { } from "$sb/lib/tree.ts"; import { renderDirectives } from "./directives.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; +import { PageMeta } from "../../web/types.ts"; export async function updateDirectivesOnPageCommand(arg: any) { // If `arg` is a string, it's triggered automatically via an event, not explicitly via a command const explicitCall = typeof arg !== "string"; - const pageName = await editor.getCurrentPage(); + const pageMeta = await space.getPageMeta(await editor.getCurrentPage()); const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); const metaData = await extractFrontmatter(tree, ["$disableDirectives"]); @@ -55,7 +56,7 @@ export async function updateDirectivesOnPageCommand(arg: any) { } const fullMatch = text.substring(tree.from!, tree.to!); try { - const promise = renderDirectives(pageName, tree); + const promise = renderDirectives(pageMeta, tree); replacements.push({ textPromise: promise, fullMatch, @@ -117,7 +118,7 @@ export async function updateDirectivesOnPageCommand(arg: any) { // Pure server driven implementation of directive updating export async function updateDirectives( - pageName: string, + pageMeta: PageMeta, text: string, ) { const tree = await markdown.parseMarkdown(text); @@ -134,7 +135,7 @@ export async function updateDirectives( const fullMatch = text.substring(tree.from!, tree.to!); try { const promise = renderDirectives( - pageName, + pageMeta, tree, ); replacements.push({ diff --git a/plugs/directive/complete.ts b/plugs/directive/complete.ts index b8e0c6f..f5a6878 100644 --- a/plugs/directive/complete.ts +++ b/plugs/directive/complete.ts @@ -1,5 +1,7 @@ import { events } from "$sb/plugos-syscall/mod.ts"; import { CompleteEvent } from "$sb/app_event.ts"; +import { buildHandebarOptions, handlebarHelpers } from "./util.ts"; +import { PageMeta } from "../../web/types.ts"; export async function queryComplete(completeEvent: CompleteEvent) { const match = /#query ([\w\-_]+)*$/.exec(completeEvent.linePrefix); @@ -18,3 +20,23 @@ export async function queryComplete(completeEvent: CompleteEvent) { })), }; } + +export function handlebarHelperComplete(completeEvent: CompleteEvent) { + const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix); + if (!match) { + return null; + } + + const handlebarOptions = buildHandebarOptions({ name: "" } as PageMeta); + const allCompletions = Object.keys(handlebarOptions.helpers).concat( + Object.keys(handlebarOptions.data).map((key) => `@${key}`), + ); + + return { + from: completeEvent.pos - match[1].length, + options: allCompletions + .map((name) => ({ + label: name, + })), + }; +} diff --git a/plugs/directive/directive.plug.yaml b/plugs/directive/directive.plug.yaml index 71e4dd3..5207078 100644 --- a/plugs/directive/directive.plug.yaml +++ b/plugs/directive/directive.plug.yaml @@ -21,6 +21,10 @@ functions: path: ./complete.ts:queryComplete events: - editor:complete + handlebarHelperComplete: + path: ./complete.ts:handlebarHelperComplete + events: + - editor:complete # Templates insertQuery: diff --git a/plugs/directive/directives.ts b/plugs/directive/directives.ts index 5e78ce2..8633bd8 100644 --- a/plugs/directive/directives.ts +++ b/plugs/directive/directives.ts @@ -1,5 +1,6 @@ import { ParseTree, renderToText } from "$sb/lib/tree.ts"; import { sync } from "../../plug-api/silverbullet-syscall/mod.ts"; +import { PageMeta } from "../../web/types.ts"; import { evalDirectiveRenderer } from "./eval_directive.ts"; import { queryDirectiveRenderer } from "./query_directive.ts"; @@ -17,13 +18,13 @@ export const directiveRegex = * Looks for directives in the text dispatches them based on name */ export async function directiveDispatcher( - pageName: string, + pageMeta: PageMeta, directiveTree: ParseTree, directiveRenderers: Record< string, ( directive: string, - pageName: string, + pageMeta: PageMeta, arg: string | ParseTree, ) => Promise >, @@ -34,6 +35,14 @@ export async function directiveDispatcher( const directiveStartText = renderToText(directiveStart).trim(); const directiveEndText = renderToText(directiveEnd).trim(); + if (!(await sync.hasInitialSyncCompleted())) { + console.info( + "Initial sync hasn't completed yet, not updating directives.", + ); + // Render the query directive as-is + return renderToText(directiveTree); + } + if (directiveStart.children!.length === 1) { // Everything not #query const match = directiveStartRegex.exec(directiveStart.children![0].text!); @@ -44,7 +53,7 @@ export async function directiveDispatcher( let [_fullMatch, type, arg] = match; try { arg = arg.trim(); - const newBody = await directiveRenderers[type](type, pageName, arg); + const newBody = await directiveRenderers[type](type, pageMeta, arg); const result = `${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`; return result; @@ -53,17 +62,9 @@ export async function directiveDispatcher( } } else { // #query - if (!(await sync.hasInitialSyncCompleted())) { - console.info( - "Initial sync hasn't completed yet, not updating query directives.", - ); - // Render the query directive as-is - return renderToText(directiveTree); - } - const newBody = await directiveRenderers["query"]( "query", - pageName, + pageMeta, directiveStart.children![1], // The query ParseTree ); const result = @@ -73,10 +74,10 @@ export async function directiveDispatcher( } export async function renderDirectives( - pageName: string, + pageMeta: PageMeta, directiveTree: ParseTree, ): Promise { - const replacementText = await directiveDispatcher(pageName, directiveTree, { + const replacementText = await directiveDispatcher(pageMeta, directiveTree, { use: templateDirectiveRenderer, include: templateDirectiveRenderer, query: queryDirectiveRenderer, diff --git a/plugs/directive/eval_directive.ts b/plugs/directive/eval_directive.ts index 81a7a5c..a9d6eea 100644 --- a/plugs/directive/eval_directive.ts +++ b/plugs/directive/eval_directive.ts @@ -3,6 +3,8 @@ import { YAML } from "$sb/plugos-syscall/mod.ts"; import { ParseTree } from "$sb/lib/tree.ts"; import { jsonToMDTable, renderTemplate } from "./util.ts"; +import { PageMeta } from "../../web/types.ts"; +import { replaceTemplateVars } from "../core/template.ts"; // Enables plugName.functionName(arg1, arg2) syntax in JS expressions function translateJs(js: string): string { @@ -20,13 +22,13 @@ const expressionRegex = /(.+?)(\s+render\s+\[\[([^\]]+)\]\])?$/; // This is rather scary and fragile stuff, but it works. export async function evalDirectiveRenderer( _directive: string, - _pageName: string, + pageMeta: PageMeta, expression: string | ParseTree, ): Promise { if (typeof expression !== "string") { throw new Error("Expected a string"); } - console.log("Got JS expression", expression); + // console.log("Got JS expression", expression); const match = expressionRegex.exec(expression); if (!match) { throw new Error(`Invalid eval directive: ${expression}`); @@ -44,11 +46,11 @@ export async function evalDirectiveRenderer( function invokeFunction(name, ...args) { return syscall("system.invokeFunction", "server", name, ...args); } - return ${translateJs(expression)}; + return ${replaceTemplateVars(translateJs(expression), pageMeta)}; })()`, ); if (template) { - return await renderTemplate(template, result); + return await renderTemplate(pageMeta, template, result); } if (typeof result === "string") { return result; diff --git a/plugs/directive/query_directive.ts b/plugs/directive/query_directive.ts index e9645a2..a41b727 100644 --- a/plugs/directive/query_directive.ts +++ b/plugs/directive/query_directive.ts @@ -5,17 +5,18 @@ import { renderTemplate } from "./util.ts"; import { parseQuery } from "./parser.ts"; import { jsonToMDTable } from "./util.ts"; import { ParseTree } from "$sb/lib/tree.ts"; +import { PageMeta } from "../../web/types.ts"; export async function queryDirectiveRenderer( _directive: string, - pageName: string, + pageMeta: PageMeta, query: string | ParseTree, ): Promise { if (typeof query === "string") { throw new Error("Argument must be a ParseTree"); } const parsedQuery = parseQuery( - JSON.parse(replaceTemplateVars(JSON.stringify(query), pageName)), + JSON.parse(replaceTemplateVars(JSON.stringify(query), pageMeta)), ); const eventName = `query:${parsedQuery.table}`; @@ -24,7 +25,7 @@ export async function queryDirectiveRenderer( // Let's dispatch an event and see what happens const results = await events.dispatchEvent( eventName, - { query: parsedQuery, pageName: pageName }, + { query: parsedQuery, pageName: pageMeta.name }, 30 * 1000, ); if (results.length === 0) { @@ -34,6 +35,7 @@ export async function queryDirectiveRenderer( // console.log("Parsed query", parsedQuery); if (parsedQuery.render) { const rendered = await renderTemplate( + pageMeta, parsedQuery.render, results[0], ); diff --git a/plugs/directive/template_directive.ts b/plugs/directive/template_directive.ts index 3c20d8f..7190fb3 100644 --- a/plugs/directive/template_directive.ts +++ b/plugs/directive/template_directive.ts @@ -8,13 +8,14 @@ import { replaceTemplateVars } from "../core/template.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; import { directiveRegex } from "./directives.ts"; import { updateDirectives } from "./command.ts"; -import { registerHandlebarsHelpers } from "./util.ts"; +import { buildHandebarOptions } from "./util.ts"; +import { PageMeta } from "../../web/types.ts"; const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/; export async function templateDirectiveRenderer( directive: string, - pageName: string, + pageMeta: PageMeta, arg: string | ParseTree, ): Promise { if (typeof arg !== "string") { @@ -29,9 +30,13 @@ export async function templateDirectiveRenderer( let parsedArgs = {}; if (args) { try { - parsedArgs = JSON.parse(args); + parsedArgs = JSON.parse(replaceTemplateVars(args, pageMeta)); } catch { - throw new Error(`Failed to parse template instantiation args: ${arg}`); + throw new Error( + `Failed to parse template instantiation arg: ${ + replaceTemplateVars(args, pageMeta) + }`, + ); } } let templateText = ""; @@ -51,18 +56,14 @@ export async function templateDirectiveRenderer( // if it's a template injection (not a literal "include") if (directive === "use") { - registerHandlebarsHelpers(); const templateFn = Handlebars.compile( - replaceTemplateVars(newBody, pageName), + newBody, { noEscape: true }, ); - if (typeof parsedArgs !== "string") { - (parsedArgs as any).page = pageName; - } - newBody = templateFn(parsedArgs); + newBody = templateFn(parsedArgs, buildHandebarOptions(pageMeta)); // Recursively render directives - newBody = await updateDirectives(pageName, newBody); + newBody = await updateDirectives(pageMeta, newBody); } return newBody.trim(); } diff --git a/plugs/directive/util.ts b/plugs/directive/util.ts index d19f473..ec59f8f 100644 --- a/plugs/directive/util.ts +++ b/plugs/directive/util.ts @@ -2,6 +2,7 @@ import Handlebars from "handlebars"; import { space } from "$sb/silverbullet-syscall/mod.ts"; import { niceDate } from "$sb/lib/dates.ts"; +import { PageMeta } from "../../web/types.ts"; const maxWidth = 70; @@ -79,47 +80,55 @@ export function jsonToMDTable( } export async function renderTemplate( + pageMeta: PageMeta, renderTemplate: string, data: any[], ): Promise { - registerHandlebarsHelpers(); - - // Handlebars.registerHelper("yaml", (v: any, prefix: string) => { - // if (typeof prefix === "string") { - // let yaml = (await YAML.stringify(v)) - // .split("\n") - // .join("\n" + prefix) - // .trim(); - // if (Array.isArray(v)) { - // return "\n" + prefix + yaml; - // } else { - // return yaml; - // } - // } else { - // return YAML.stringify(v).trim(); - // } - // }); let templateText = await space.readPage(renderTemplate); templateText = `{{#each .}}\n${templateText}\n{{/each}}`; const template = Handlebars.compile(templateText, { noEscape: true }); - return template(data); + return template(data, buildHandebarOptions(pageMeta)); } -export function registerHandlebarsHelpers() { - Handlebars.registerHelper("json", (v: any) => JSON.stringify(v)); - Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts))); - Handlebars.registerHelper("escapeRegexp", (ts: any) => { - return ts.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); - }); - Handlebars.registerHelper("prefixLines", (v: string, prefix: string) => - v - .split("\n") - .map((l) => prefix + l) - .join("\n")); +export function buildHandebarOptions(pageMeta: PageMeta) { + return { + helpers: handlebarHelpers(pageMeta.name), + data: { page: pageMeta }, + }; +} - Handlebars.registerHelper( - "substring", - (s: string, from: number, to: number, elipsis = "") => +export function handlebarHelpers(pageName: string) { + return { + json: (v: any) => JSON.stringify(v), + niceDate: (ts: any) => niceDate(new Date(ts)), + escapeRegexp: (ts: any) => { + return ts.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + }, + prefixLines: (v: string, prefix: string) => + v.split("\n").map((l) => prefix + l).join("\n"), + substring: (s: string, from: number, to: number, elipsis = "") => s.length > to - from ? s.substring(from, to) + elipsis : s, - ); + + today: () => niceDate(new Date()), + tomorrow: () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + return niceDate(tomorrow); + }, + yesterday: () => { + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + return niceDate(yesterday); + }, + lastWeek: () => { + const lastWeek = new Date(); + lastWeek.setDate(lastWeek.getDate() - 7); + return niceDate(lastWeek); + }, + nextWeek: () => { + const nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + return niceDate(nextWeek); + }, + }; } diff --git a/plugs/federation/federation.plug.yaml b/plugs/federation/federation.plug.yaml new file mode 100644 index 0000000..321d4e7 --- /dev/null +++ b/plugs/federation/federation.plug.yaml @@ -0,0 +1,29 @@ +name: federation +requiredPermissions: + - fetch +functions: + listFiles: + path: ./federation.ts:listFiles + pageNamespace: + pattern: "!.+" + operation: listFiles + readFile: + path: ./federation.ts:readFile + pageNamespace: + pattern: "!.+" + operation: readFile + writeFile: + path: ./federation.ts:writeFile + pageNamespace: + pattern: "!.+" + operation: writeFile + deleteFile: + path: ./federation.ts:deleteFile + pageNamespace: + pattern: "!.+" + operation: deleteFile + getFileMeta: + path: ./federation.ts:getFileMeta + pageNamespace: + pattern: "!.+" + operation: getFileMeta \ No newline at end of file diff --git a/plugs/federation/federation.ts b/plugs/federation/federation.ts new file mode 100644 index 0000000..7cfad9c --- /dev/null +++ b/plugs/federation/federation.ts @@ -0,0 +1,162 @@ +import "$sb/lib/fetch.ts"; +import type { FileMeta } from "../../common/types.ts"; +import { readSetting } from "$sb/lib/settings_page.ts"; + +function resolveFederated(pageName: string): string { + // URL without the prefix "!"" + let url = pageName.substring(1); + const pieces = url.split("/"); + pieces.splice(1, 0, ".fs"); + url = pieces.join("/"); + if (!url.startsWith("127.0.0.1") && !url.startsWith("localhost")) { + url = `https://${url}`; + } else { + url = `http://${url}`; + } + return url; +} + +async function responseToFileMeta( + r: Response, + name: string, +): Promise { + let perm = r.headers.get("X-Permission") as any || "ro"; + const federationConfigs = await readFederationConfigs(); + const federationConfig = federationConfigs.find((config) => + name.startsWith(config.uri) + ); + if (federationConfig?.perm) { + perm = federationConfig.perm; + } + return { + name: name, + size: r.headers.get("Content-length") + ? +r.headers.get("Content-length")! + : 0, + contentType: r.headers.get("Content-type")!, + perm: perm, + lastModified: +(r.headers.get("X-Last-Modified") || "0"), + }; +} + +type FederationConfig = { + uri: string; + perm?: "ro" | "rw"; +}; +let federationConfigs: FederationConfig[] = []; +let lastFederationUrlFetch = 0; + +async function readFederationConfigs() { + // Update at most every 5 seconds + if (Date.now() > lastFederationUrlFetch + 5000) { + federationConfigs = await readSetting("federate", []); + // Normalize URIs + for (const config of federationConfigs) { + if (!config.uri.startsWith("!")) { + config.uri = `!${config.uri}`; + } + } + lastFederationUrlFetch = Date.now(); + } + return federationConfigs; +} + +export async function listFiles(): Promise { + let fileMetas: FileMeta[] = []; + // Fetch them all in parallel + await Promise.all((await readFederationConfigs()).map(async (config) => { + // console.log("Fetching from federated", config); + const uriParts = config.uri.split("/"); + const rootUri = uriParts[0]; + const prefix = uriParts.slice(1).join("/"); + const r = await nativeFetch(resolveFederated(rootUri)); + fileMetas = fileMetas.concat( + (await r.json()).filter((meta: FileMeta) => meta.name.startsWith(prefix)) + .map((meta: FileMeta) => ({ + ...meta, + perm: config.perm || meta.perm, + name: `${rootUri}/${meta.name}`, + })), + ); + })); + // console.log("All of em: ", fileMetas); + return fileMetas; +} + +export async function readFile( + name: string, +): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> { + const url = resolveFederated(name); + const r = await nativeFetch(url); + const fileMeta = await responseToFileMeta(r, name); + console.log("Fetching", url); + if (r.status === 404) { + throw Error("Not found"); + } + const data = await r.arrayBuffer(); + if (!r.ok) { + return errorResult(name, `**Error**: Could not load`); + } + + return { + data: new Uint8Array(data), + meta: fileMeta, + }; +} + +function errorResult( + name: string, + error: string, +): { data: Uint8Array; meta: FileMeta } { + return { + data: new TextEncoder().encode(error), + meta: { + name, + contentType: "text/markdown", + lastModified: 0, + size: 0, + perm: "ro", + }, + }; +} + +export async function writeFile( + name: string, + data: Uint8Array, +): Promise { + const url = resolveFederated(name); + console.log("Writing federation file", url); + + const r = await nativeFetch(url, { + method: "PUT", + body: data, + }); + const fileMeta = await responseToFileMeta(r, name); + if (!r.ok) { + throw new Error("Could not write file"); + } + + return fileMeta; +} + +export async function deleteFile( + name: string, +): Promise { + console.log("Deleting federation file", name); + const url = resolveFederated(name); + const r = await nativeFetch(url, { method: "DELETE" }); + if (!r.ok) { + throw Error("Failed to delete file"); + } +} + +export async function getFileMeta(name: string): Promise { + const url = resolveFederated(name); + console.log("Fetching federation file meta", url); + const r = await nativeFetch(url, { method: "OPTIONS" }); + const fileMeta = await responseToFileMeta(r, name); + if (!r.ok) { + throw new Error("Not found"); + } + return fileMeta; +} diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index 7955864..e1efef8 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -12,7 +12,7 @@ type MarkdownRenderOptions = { annotationPositions?: true; attachmentUrlPrefix?: string; // When defined, use to inline images as data: urls - inlineAttachments?: (url: string) => string; + translateUrls?: (url: string) => string; }; function cleanTags(values: (Tag | null)[]): Tag[] { @@ -385,13 +385,17 @@ export function renderMarkdownToHtml( ) { preprocess(t, options); const htmlTree = posPreservingRender(t, options); - if (htmlTree && options.inlineAttachments) { + if (htmlTree && options.translateUrls) { traverseTag(htmlTree, (t) => { if (typeof t === "string") { return; } if (t.name === "img") { - t.attrs!.src = options.inlineAttachments!(t.attrs!.src!); + t.attrs!.src = options.translateUrls!(t.attrs!.src!); + } + + if (t.name === "a") { + t.attrs!.href = options.translateUrls!(t.attrs!.href!); } }); } diff --git a/plugs/markdown/preview.ts b/plugs/markdown/preview.ts index e652574..621ccfd 100644 --- a/plugs/markdown/preview.ts +++ b/plugs/markdown/preview.ts @@ -1,4 +1,4 @@ -import { editor, space, system } from "$sb/silverbullet-syscall/mod.ts"; +import { editor, system } from "$sb/silverbullet-syscall/mod.ts"; import { asset, store } from "$sb/plugos-syscall/mod.ts"; import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts"; @@ -7,6 +7,7 @@ export async function updateMarkdownPreview() { if (!(await store.get("enableMarkdownPreview"))) { return; } + const pageName = await editor.getCurrentPage(); const text = await editor.getText(); const mdTree = await parseMarkdown(text); // const cleanMd = await cleanMarkdown(text); @@ -15,7 +16,7 @@ export async function updateMarkdownPreview() { const html = renderMarkdownToHtml(mdTree, { smartHardBreak: true, annotationPositions: true, - inlineAttachments: (url) => { + translateUrls: (url) => { if (!url.includes("://")) { return `/.fs/${url}`; } diff --git a/plugs/search/search.ts b/plugs/search/search.ts index b60a558..7fd65d3 100644 --- a/plugs/search/search.ts +++ b/plugs/search/search.ts @@ -91,7 +91,7 @@ export async function searchCommand() { export async function readFileSearch( name: string, -): Promise<{ data: string; meta: FileMeta }> { +): Promise<{ data: Uint8Array; meta: FileMeta }> { const phrase = name.substring( searchPrefix.length, name.length - ".md".length, @@ -105,11 +105,7 @@ export async function readFileSearch( `; return { - // encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl" - data: base64EncodedDataUrl( - "text/markdown", - new TextEncoder().encode(text), - ), + data: new TextEncoder().encode(text), meta: { name, contentType: "text/markdown", diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts index 84a5ea5..45d0fcd 100644 --- a/plugs/tasks/task.ts +++ b/plugs/tasks/task.ts @@ -85,17 +85,21 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) { } export function taskToggle(event: ClickEvent) { - return taskToggleAtPos(event.pos); + return taskToggleAtPos(event.page, event.pos); } -export function previewTaskToggle(eventString: string) { +export async function previewTaskToggle(eventString: string) { const [eventName, pos] = JSON.parse(eventString); if (eventName === "task") { - return taskToggleAtPos(+pos); + return taskToggleAtPos(await editor.getCurrentPage(), +pos); } } -async function toggleTaskMarker(node: ParseTree, moveToPos: number) { +async function toggleTaskMarker( + _pageName: string, + node: ParseTree, + moveToPos: number, +) { let changeTo = "[x]"; if (node.children![0].text === "[x]" || node.children![0].text === "[X]") { changeTo = "[ ]"; @@ -136,14 +140,14 @@ async function toggleTaskMarker(node: ParseTree, moveToPos: number) { } } -export async function taskToggleAtPos(pos: number) { +export async function taskToggleAtPos(pageName: string, pos: number) { const text = await editor.getText(); const mdTree = await markdown.parseMarkdown(text); addParentPointers(mdTree); const node = nodeAtPos(mdTree, pos); if (node && node.type === "TaskMarker") { - await toggleTaskMarker(node, pos); + await toggleTaskMarker(pageName, node, pos); } } @@ -156,7 +160,7 @@ export async function taskToggleCommand() { const node = nodeAtPos(tree, pos); // We kwow node.type === Task (due to the task context) const taskMarker = findNodeOfType(node!, "TaskMarker"); - await toggleTaskMarker(taskMarker!, pos); + await toggleTaskMarker(await editor.getCurrentPage(), taskMarker!, pos); } export async function postponeCommand() { @@ -181,7 +185,7 @@ export async function postponeCommand() { return; } // Parse "naive" due date - let [yyyy, mm, dd] = date.split("-").map(Number) + let [yyyy, mm, dd] = date.split("-").map(Number); // Create new naive Date object. // `monthIndex` parameter is zero-based, so subtract 1 from parsed month. const d = new Date(yyyy, mm - 1, dd); @@ -214,6 +218,7 @@ export async function queryProvider({ query, }: QueryProviderEvent): Promise { const allTasks: Task[] = []; + for (const { key, page, value } of await index.queryPrefix("task:")) { const pos = key.split(":")[1]; allTasks.push({ diff --git a/server/collab.test.ts b/server/collab.test.ts index 6f5008d..952cbab 100644 --- a/server/collab.test.ts +++ b/server/collab.test.ts @@ -6,12 +6,7 @@ Deno.test("Collab server", async () => { console.log("Client 1 joins page 1"); assertEquals(collabServer.updatePresence("client1", "page1"), {}); assertEquals(collabServer.pages.size, 1); - console.log("CLient 1 leaves page 1"); - assertEquals(collabServer.updatePresence("client1", undefined, "page1"), {}); - assertEquals(collabServer.pages.size, 0); - assertEquals(collabServer.updatePresence("client1", "page1"), {}); - console.log("Client 1 joins page 2"); - assertEquals(collabServer.updatePresence("client1", "page2", "page1"), {}); + assertEquals(collabServer.updatePresence("client1", "page2"), {}); assertEquals(collabServer.pages.size, 1); console.log("Client 2 joins to page 2, collab id created, but not exposed"); assertEquals( @@ -22,22 +17,23 @@ Deno.test("Collab server", async () => { collabServer.updatePresence("client1", "page2").collabId !== undefined, ); console.log("Client 2 moves to page 1, collab id destroyed"); - assertEquals(collabServer.updatePresence("client2", "page1", "page2"), {}); - assertEquals(collabServer.updatePresence("client1", "page2", "page2"), {}); + assertEquals(collabServer.updatePresence("client2", "page1"), {}); + assertEquals(collabServer.updatePresence("client1", "page2"), {}); assertEquals(collabServer.pages.get("page2")!.collabId, undefined); assertEquals(collabServer.pages.get("page1")!.collabId, undefined); console.log("Going to cleanup, which should have no effect"); collabServer.cleanup(50); assertEquals(collabServer.pages.size, 2); - collabServer.updatePresence("client2", "page2", "page1"); + collabServer.updatePresence("client2", "page2"); console.log("Going to sleep 20ms"); await sleep(20); console.log("Then client 1 pings, but client 2 does not"); - collabServer.updatePresence("client1", "page2", "page2"); + collabServer.updatePresence("client1", "page2"); await sleep(20); console.log("Going to cleanup, which should clean client 2"); collabServer.cleanup(35); assertEquals(collabServer.pages.get("page2")!.collabId, undefined); + assertEquals(collabServer.clients.size, 1); console.log(collabServer); }); diff --git a/server/collab.ts b/server/collab.ts index a0cd6d6..8f43918 100644 --- a/server/collab.ts +++ b/server/collab.ts @@ -14,7 +14,7 @@ type CollabPage = { }; export class CollabServer { - // clients: Map = new Map(); + clients: Map = new Map(); // clientId -> openPage pages: Map = new Map(); yCollabServer?: Hocuspocus; @@ -29,13 +29,18 @@ export class CollabServer { updatePresence( clientId: string, - currentPage?: string, - previousPage?: string, + currentPage: string, ): { collabId?: string } { - if (previousPage && currentPage !== previousPage) { + let client = this.clients.get(clientId); + if (!client) { + client = { openPage: "", lastUpdate: 0 }; + this.clients.set(clientId, client); + } + client.lastUpdate = Date.now(); + if (currentPage !== client.openPage) { // Client switched pages // Update last page record - const lastCollabPage = this.pages.get(previousPage); + const lastCollabPage = this.pages.get(client.openPage); if (lastCollabPage) { lastCollabPage.clients.delete(clientId); if (lastCollabPage.clients.size === 1) { @@ -43,7 +48,7 @@ export class CollabServer { } if (lastCollabPage.clients.size === 0) { - this.pages.delete(previousPage); + this.pages.delete(client.openPage); } else { // Elect a new master client if (lastCollabPage.masterClientId === clientId) { @@ -53,45 +58,43 @@ export class CollabServer { } } } + // Ok, let's update our records now + client.openPage = currentPage; } - if (currentPage) { - // Update new page - let nextCollabPage = this.pages.get(currentPage); - if (!nextCollabPage) { - // Newly opened page (no other clients on this page right now) - nextCollabPage = { - clients: new Map(), - masterClientId: clientId, - }; - this.pages.set(currentPage, nextCollabPage); - } - // Register last ping from us - nextCollabPage.clients.set(clientId, Date.now()); + // Update new page + let nextCollabPage = this.pages.get(currentPage); + if (!nextCollabPage) { + // Newly opened page (no other clients on this page right now) + nextCollabPage = { + clients: new Map(), + masterClientId: clientId, + }; + this.pages.set(currentPage, nextCollabPage); + } + // Register last ping from us + nextCollabPage.clients.set(clientId, Date.now()); - if (nextCollabPage.clients.size > 1 && !nextCollabPage.collabId) { - // Create a new collabId - nextCollabPage.collabId = nanoid(); - } - // console.log("State", this.pages); - if (nextCollabPage.collabId) { - // We will now expose this collabId, except when we're just starting this session - // in which case we'll wait for the original client to publish the document - const existingyCollabSession = this.yCollabServer?.documents.get( - buildCollabId(nextCollabPage.collabId, `${currentPage}.md`), - ); - if (existingyCollabSession) { - // console.log("Found an existing collab session already, let's join!"); - return { collabId: nextCollabPage.collabId }; - } else if (clientId === nextCollabPage.masterClientId) { - // console.log("We're the master, so we should connect"); - return { collabId: nextCollabPage.collabId }; - } else { - // We're not the first client, so we need to wait for the first client to connect - // console.log("We're not the master, so we should wait"); - return {}; - } + if (nextCollabPage.clients.size > 1 && !nextCollabPage.collabId) { + // Create a new collabId + nextCollabPage.collabId = nanoid(); + } + // console.log("State", this.pages); + if (nextCollabPage.collabId) { + // We will now expose this collabId, except when we're just starting this session + // in which case we'll wait for the original client to publish the document + const existingyCollabSession = this.yCollabServer?.documents.get( + buildCollabId(nextCollabPage.collabId, `${currentPage}.md`), + ); + if (existingyCollabSession) { + // console.log("Found an existing collab session already, let's join!"); + return { collabId: nextCollabPage.collabId }; + } else if (clientId === nextCollabPage.masterClientId) { + // console.log("We're the master, so we should connect"); + return { collabId: nextCollabPage.collabId }; } else { + // We're not the first client, so we need to wait for the first client to connect + // console.log("We're not the master, so we should wait"); return {}; } } else { @@ -121,6 +124,13 @@ export class CollabServer { this.pages.delete(pageName); } } + + for (const [clientId, { lastUpdate }] of this.clients) { + if (Date.now() - lastUpdate > timeout) { + // Eject client + this.clients.delete(clientId); + } + } } route(app: Application) { diff --git a/server/http_server.ts b/server/http_server.ts index 6362d45..af71600 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -9,6 +9,7 @@ import { gitIgnoreCompiler } from "./deps.ts"; import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts"; import { CollabServer } from "./collab.ts"; import { Authenticator } from "./auth.ts"; +import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts"; export type ServerOptions = { hostname: string; @@ -183,6 +184,49 @@ export class HttpServer { "/logo.png", "/.auth", ]; + + app.use(async ({ request, response, cookies }, next) => { + if (request.url.pathname === "/.auth") { + if (request.url.search === "?logout") { + await cookies.delete("auth"); + // Implicit fallthrough to login page + } + if (request.method === "GET") { + response.headers.set("Content-type", "text/html"); + response.body = this.clientAssetBundle.readTextFileSync( + ".client/auth.html", + ); + return; + } else if (request.method === "POST") { + const values = await request.body({ type: "form" }).value; + const username = values.get("username")!, + password = values.get("password")!, + refer = values.get("refer"); + const hashedPassword = await this.authenticator.authenticate( + username, + password, + ); + if (hashedPassword) { + await cookies.set("auth", `${username}:${hashedPassword}`, { + expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // in a week + sameSite: "strict", + }); + response.redirect(refer || "/"); + // console.log("All headers", request.headers); + } else { + response.redirect("/.auth?error=1"); + } + return; + } else { + response.status = 401; + response.body = "Unauthorized"; + return; + } + } else { + await next(); + } + }); + if ((await this.authenticator.getAllUsers()).length > 0) { app.use(async ({ request, response, cookies }, next) => { if (!excludedPaths.includes(request.url.pathname)) { @@ -204,55 +248,20 @@ export class HttpServer { return; } } - - if (request.url.pathname === "/.auth") { - if (request.url.search === "?logout") { - await cookies.delete("auth"); - // Implicit fallthrough to login page - } - if (request.method === "GET") { - response.headers.set("Content-type", "text/html"); - response.body = this.clientAssetBundle.readTextFileSync( - ".client/auth.html", - ); - return; - } else if (request.method === "POST") { - const values = await request.body({ type: "form" }).value; - const username = values.get("username")!, - password = values.get("password")!, - refer = values.get("refer"); - const hashedPassword = await this.authenticator.authenticate( - username, - password, - ); - if (hashedPassword) { - await cookies.set("auth", `${username}:${hashedPassword}`, { - expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // in a week - sameSite: "strict", - }); - response.redirect(refer || "/"); - // console.log("All headers", request.headers); - } else { - response.redirect("/.auth?error=1"); - } - return; - } else { - response.status = 401; - response.body = "Unauthorized"; - return; - } - } else { - // Unauthenticated access to excluded paths - await next(); - } + await next(); }); } } private buildFsRouter(spacePrimitives: SpacePrimitives): Router { const fsRouter = new Router(); + const corsMiddleware = oakCors({ + allowedHeaders: "*", + exposedHeaders: "*", + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + }); // File list - fsRouter.get("/", async ({ response }) => { + fsRouter.get("/", corsMiddleware, async ({ response }) => { response.headers.set("Content-type", "application/json"); response.headers.set("X-Space-Path", this.options.pagesPath); const files = await spacePrimitives.fetchFileList(); @@ -260,7 +269,7 @@ export class HttpServer { }); // RPC - fsRouter.post("/", async ({ request, response }) => { + fsRouter.post("/", corsMiddleware, async ({ request, response }) => { const body = await request.body({ type: "json" }).value; try { switch (body.operation) { @@ -300,19 +309,6 @@ export class HttpServer { }); return; } - case "presence": { - // RPC to check (for collab purposes) which client has what page open - response.headers.set("Content-Type", "application/json"); - console.log("Got presence update", body); - response.body = JSON.stringify( - this.collab.updatePresence( - body.clientId, - body.currentPage, - body.previousPage, - ), - ); - return; - } default: response.headers.set("Content-Type", "text/plain"); response.status = 400; @@ -327,7 +323,7 @@ export class HttpServer { }); fsRouter - .get("\/(.+)", async ({ params, response, request }) => { + .get("\/(.+)", corsMiddleware, async ({ params, response, request }) => { const name = params[0]; console.log("Loading file", name); if (name.startsWith(".")) { @@ -364,7 +360,7 @@ export class HttpServer { response.body = ""; } }) - .put("\/(.+)", async ({ request, response, params }) => { + .put("\/(.+)", corsMiddleware, async ({ request, response, params }) => { const name = params[0]; console.log("Saving file", name); if (name.startsWith(".")) { @@ -400,8 +396,16 @@ export class HttpServer { console.error("Pipeline failed", err); } }) - .options("\/(.+)", async ({ response, params }) => { + .options("\/(.+)", async ({ request, response, params }) => { const name = params[0]; + // Manually set CORS headers + response.headers.set("access-control-allow-headers", "*"); + response.headers.set( + "access-control-allow-methods", + "GET,POST,PUT,DELETE,OPTIONS", + ); + response.headers.set("access-control-allow-origin", "*"); + response.headers.set("access-control-expose-headers", "*"); try { const meta = await spacePrimitives.getFileMeta(name); response.status = 200; @@ -409,13 +413,25 @@ export class HttpServer { response.headers.set("X-Last-Modified", "" + meta.lastModified); response.headers.set("X-Content-Length", "" + meta.size); response.headers.set("X-Permission", meta.perm); + + const clientId = request.headers.get("X-Client-Id"); + if (name.endsWith(".md") && clientId) { + const pageName = name.substring(0, name.length - ".md".length); + console.log(`Got presence update from ${clientId}: ${pageName}`); + const { collabId } = this.collab.updatePresence(clientId, pageName); + if (collabId) { + response.headers.set("X-Collab-Id", collabId); + } + } } catch { - response.status = 404; + // Have to do this because of CORS + response.status = 200; + response.headers.set("X-Status", "404"); response.body = "Not found"; // console.error("Options failed", err); } }) - .delete("\/(.+)", async ({ response, params }) => { + .delete("\/(.+)", corsMiddleware, async ({ response, params }) => { const name = params[0]; console.log("Deleting file", name); try { diff --git a/web/cm_plugins/editor_paste.ts b/web/cm_plugins/editor_paste.ts index 9e3c41e..aea5d9f 100644 --- a/web/cm_plugins/editor_paste.ts +++ b/web/cm_plugins/editor_paste.ts @@ -180,12 +180,13 @@ export function attachmentExtension(editor: Editor) { if (!finalFileName) { return; } - await editor.space.writeAttachment(finalFileName, new Uint8Array(data)); - let attachmentMarkdown = `[${finalFileName}](${ - encodeURIComponent(finalFileName) - })`; + await editor.space.writeAttachment( + finalFileName, + new Uint8Array(data), + ); + let attachmentMarkdown = `[${finalFileName}](${encodeURI(finalFileName)})`; if (mimeType.startsWith("image/")) { - attachmentMarkdown = `![](${encodeURIComponent(finalFileName)})`; + attachmentMarkdown = `![](${encodeURI(finalFileName)})`; } editor.editorView!.dispatch({ changes: [ diff --git a/web/cm_plugins/inline_image.ts b/web/cm_plugins/inline_image.ts index 71ba211..8a26a0c 100644 --- a/web/cm_plugins/inline_image.ts +++ b/web/cm_plugins/inline_image.ts @@ -8,6 +8,7 @@ import { import { decoratorStateField } from "./util.ts"; import type { Space } from "../space.ts"; +import type { Editor } from "../editor.tsx"; class InlineImageWidget extends WidgetType { constructor( @@ -39,13 +40,7 @@ class InlineImageWidget extends WidgetType { this.space.setCachedImageHeight(this.url, img.height); } }; - if (this.url.startsWith("http")) { - img.src = this.url; - } else { - // This is an attachment image, rewrite the URL a little - img.src = `/.fs/${decodeURIComponent(this.url)}`; - } - + img.src = this.url; img.alt = this.title; img.title = this.title; img.style.display = "block"; @@ -58,7 +53,7 @@ class InlineImageWidget extends WidgetType { } } -export function inlineImagesPlugin(space: Space) { +export function inlineImagesPlugin(editor: Editor) { return decoratorStateField((state: EditorState) => { const widgets: Range[] = []; const imageRegex = /!\[(?[^\]]*)\]\((?<url>.+)\)/; @@ -76,11 +71,14 @@ export function inlineImagesPlugin(space: Space) { return; } - const url = imageRexexResult.groups.url; + let url = imageRexexResult.groups.url; const title = imageRexexResult.groups.title; + if (url.indexOf("://") === -1) { + url = `/.fs/${url}`; + } widgets.push( Decoration.widget({ - widget: new InlineImageWidget(url, title, space), + widget: new InlineImageWidget(url, title, editor.space), block: true, }).range(node.to), ); diff --git a/web/cm_plugins/table.ts b/web/cm_plugins/table.ts index 358e357..004990b 100644 --- a/web/cm_plugins/table.ts +++ b/web/cm_plugins/table.ts @@ -37,7 +37,7 @@ class TableViewWidget extends WidgetType { // Annotate every element with its position so we can use it to put // the cursor there when the user clicks on the table. annotationPositions: true, - inlineAttachments: (url) => { + translateUrls: (url) => { if (!url.includes("://")) { return `/.fs/${url}`; } diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index b8bdd6e..83af63e 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -33,6 +33,7 @@ export function cleanWikiLinkPlugin(editor: Editor) { if (page.includes("@")) { cleanPage = page.split("@")[0]; } + // console.log("Resolved page", resolvedPage); for (const pageMeta of allPages) { if (pageMeta.name === cleanPage) { pageExists = true; @@ -76,8 +77,10 @@ export function cleanWikiLinkPlugin(editor: Editor) { widget: new LinkWidget( { text: linkText, - title: pageExists ? `Navigate to ${page}` : `Create ${page}`, - href: `/${page}`, + title: pageExists + ? `Navigate to ${cleanPage}` + : `Create ${cleanPage}`, + href: `/${cleanPage}`, cssClass: pageExists ? "sb-wiki-link-page" : "sb-wiki-link-page-missing", diff --git a/web/collab_manager.ts b/web/collab_manager.ts index ec7f049..cbc6b26 100644 --- a/web/collab_manager.ts +++ b/web/collab_manager.ts @@ -15,7 +15,7 @@ export class CollabManager { "editor:pageLoaded", (pageName, previousPage) => { console.log("Page loaded", pageName, previousPage); - this.updatePresence(pageName, previousPage).catch(console.error); + this.updatePresence(pageName).catch(console.error); }, ); } @@ -26,25 +26,20 @@ export class CollabManager { }, collabPingInterval); } - async updatePresence(currentPage?: string, previousPage?: string) { + async updatePresence(currentPage: string) { try { + // This is signaled through an OPTIONS call on the file we have open const resp = await this.editor.remoteSpacePrimitives.authenticatedFetch( - this.editor.remoteSpacePrimitives.url, + `${this.editor.remoteSpacePrimitives.url}/${currentPage}.md`, { - method: "POST", + method: "OPTIONS", headers: { - "Content-Type": "application/json", + "X-Client-Id": this.clientId, }, - body: JSON.stringify({ - operation: "presence", - clientId: this.clientId, - previousPage, - currentPage, - }), - keepalive: true, // important for beforeunload event }, ); - const { collabId } = await resp.json(); + const collabId = resp.headers.get("X-Collab-Id"); + // Not reading body at all, is that a problem? if (this.editor.collabState && !this.editor.collabState.isLocalCollab) { // We're in a remote collab mode, don't do anything diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index a7c8c34..09b6529 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -8,7 +8,6 @@ import type { ComponentChildren, FunctionalComponent } from "../deps.ts"; import { Notification } from "../types.ts"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; import { MiniEditor } from "./mini_editor.tsx"; -import process from "https://deno.land/std@0.177.1/node/process.ts"; export type ActionButton = { icon: FunctionalComponent<FeatherProps>; diff --git a/web/deps.ts b/web/deps.ts index 9e1e121..5eb1c33 100644 --- a/web/deps.ts +++ b/web/deps.ts @@ -21,7 +21,7 @@ export { yCollab, yUndoManagerKeymap, } from "https://esm.sh/y-codemirror.next@0.3.2?external=yjs,@codemirror/state,@codemirror/commands,@codemirror/history,@codemirror/view"; -export { HocuspocusProvider } from "https://esm.sh/@hocuspocus/provider@2.1.0?external=yjs,ws&target=es2022"; +export { HocuspocusProvider } from "https://esm.sh/@hocuspocus/provider@2.2.0?deps=lib0@0.2.70&external=yjs,ws&target=es2022"; // Vim mode export { diff --git a/web/editor.tsx b/web/editor.tsx index 25f76bb..dcd7612 100644 --- a/web/editor.tsx +++ b/web/editor.tsx @@ -242,15 +242,8 @@ export class Editor { true, ); - const plugSpacePrimitives = new PlugSpacePrimitives( - // Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet - new FallbackSpacePrimitives( - new IndexedDBSpacePrimitives( - `${dbPrefix}_space`, - globalThis.indexedDB, - ), - this.remoteSpacePrimitives, - ), + const plugSpaceRemotePrimitives = new PlugSpacePrimitives( + this.remoteSpacePrimitives, namespaceHook, ); @@ -258,7 +251,14 @@ export class Editor { const localSpacePrimitives = new FilteredSpacePrimitives( new FileMetaSpacePrimitives( new EventedSpacePrimitives( - plugSpacePrimitives, + // Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet + new FallbackSpacePrimitives( + new IndexedDBSpacePrimitives( + `${dbPrefix}_space`, + globalThis.indexedDB, + ), + plugSpaceRemotePrimitives, + ), this.eventHook, ), indexSyscalls, @@ -279,12 +279,16 @@ export class Editor { this.syncService = new SyncService( localSpacePrimitives, - this.remoteSpacePrimitives, + plugSpaceRemotePrimitives, this.kvStore, this.eventHook, (path) => { // TODO: At some point we should remove the data.db exception here - return path !== "data.db" && !plugSpacePrimitives.isLikelyHandled(path); + return path !== "data.db" && + // Exclude all plug space primitives paths + !plugSpaceRemotePrimitives.isLikelyHandled(path) || + // Except federated ones + path.startsWith("!"); }, ); @@ -892,7 +896,7 @@ export class Editor { ), ], }), - inlineImagesPlugin(this.space), + inlineImagesPlugin(this), highlightSpecialChars(), history(), drawSelection(), @@ -1117,6 +1121,7 @@ export class Editor { const linePrefix = line.text.slice(0, selection.from - line.from); const results = await this.dispatchAppEvent(eventName, { + pageName: this.currentPage!, linePrefix, pos: selection.from, } as CompleteEvent); @@ -1127,6 +1132,7 @@ export class Editor { console.error( "Got completion results from multiple sources, cannot deal with that", ); + console.error("Previously had", actualResult, "now also got", result); return null; } actualResult = result; diff --git a/web/service_worker.ts b/web/service_worker.ts index 86144c0..393f7fb 100644 --- a/web/service_worker.ts +++ b/web/service_worker.ts @@ -82,10 +82,13 @@ self.addEventListener("fetch", (event: any) => { } const requestUrl = new URL(event.request.url); + const pathname = requestUrl.pathname; // console.log("In service worker, pathname is", pathname); + // Are we fetching a URL from the same origin as the app? If not, we don't handle it here + const fetchingLocal = location.host === requestUrl.host; // If this is a /.fs request, this can either be a plug worker load or an attachment load - if (pathname.startsWith("/.fs")) { + if (fetchingLocal && pathname.startsWith("/.fs")) { if (fileContentTable && !event.request.headers.has("x-sync-mode")) { // console.log( // "Attempting to serve file from locally synced space:", @@ -101,8 +104,10 @@ self.addEventListener("fetch", (event: any) => { // console.log("Serving from space", path); return new Response(data.data, { headers: { - "Content-type": mime.getType(path) || - "application/octet-stream", + "Content-type": data.meta.contentType, + "Content-Length": "" + data.meta.size, + "X-Permission": data.meta.perm, + "X-Last-Modified": "" + data.meta.lastModified, }, }); } else { @@ -120,7 +125,7 @@ self.addEventListener("fetch", (event: any) => { // Just fetch the file directly return fetch(event.request); } - } else if (pathname !== "/.auth") { + } else if (fetchingLocal && pathname !== "/.auth") { // Must be a page URL, let's serve index.html which will handle it return caches.match(precacheFiles["/"]).then((response) => { // This shouldnt't happen, index.html not in the cache for some reason diff --git a/web/sync_service.ts b/web/sync_service.ts index d555bf3..2b82fcb 100644 --- a/web/sync_service.ts +++ b/web/sync_service.ts @@ -88,8 +88,8 @@ export class SyncService { async hasInitialSyncCompleted(): Promise<boolean> { // Initial sync has happened when sync progress has been reported at least once, but the syncStartTime has been reset (which happens after sync finishes) - return !!(!(await this.kvStore.get(syncStartTimeKey)) && - (await this.kvStore.get(syncLastActivityKey))); + return !(await this.kvStore.has(syncStartTimeKey)) && + (await this.kvStore.has(syncLastActivityKey)); } async registerSyncStart(): Promise<void> { @@ -371,7 +371,7 @@ export class SyncService { name, data, false, - meta.lastModified, + meta, ); // Update snapshot snapshot.set(name, [ diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index a8ce0b4..8bba05c 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -182,6 +182,10 @@ export function editorSyscalls(editor: Editor): SysCallMapping { const cm = vimGetCm(editor.editorView!)!; return Vim.handleEx(cm, exCommand); }, + // Sync + "editor.syncSpace": () => { + return editor.syncService.syncSpace(); + }, // Folding "editor.fold": () => { foldCode(editor.editorView!); diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 6fdb607..1c62021 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -4,7 +4,12 @@ release. --- ## Next -* Folding is here (at least with commands, not much UI): {[Fold: Fold]}, {[Fold: Unfold]}, {[Fold: Fold All]} and {[Fold: Unfold All]}. + +* **Breaking change (for some templates):** Template in various places allowed you to use `{{variables}}` and various handlebars functions. There also used to be a magic `{{page}}` variable that you could use in various places, but not everywhere. This has now been unified. And the magical `{{page}}` now has been replaced with the global `@page` which does not just expose the page’s name, but any page meta data. More information here: [[πŸ”Œ Core/Templates@vars]]. You will now get completion for built-in handlebars helpers after typing `{{`. +* Folding is here (at least with commands, not much UI): {[Fold: Fold]}, {[Fold: Unfold]}, {[Fold: Toggle Fold]}, {[Fold: Fold All]} and {[Fold: Unfold All]}. +* {[Broken Links: Show]} command (not complete yet, but already useful) +* The `Daily Note` template now supports setting a caret position with `|^|`. +* Explicit {[Sync: Now]} command for those who are impatient --- @@ -63,7 +68,7 @@ Besides these architectural changes, a few other breaking changes were made to s ## 0.2.12 * Added support to override CSS styles on a per-space basis. This replaces the previous `fontFamily` setting. See [[STYLES]] for hints on how to use this new experimental feature. -* [[Sync]]: Support to exclude prefixes (via [[SETTINGS]]) +* Sync: Support to exclude prefixes (via [[SETTINGS]]) * Reverted behavior of using up/down arrow keys to move between the page title and page content (and rename based on it). This resulted in undesirable behavior too often. You can now rename a page by clicking/tapping on the title, changing the name and hitting Enter or clicking anywhere outside the page title to apply the rename. * Documentation updates (on https://silverbullet.md): * [[Special Pages]] @@ -95,16 +100,16 @@ Besides these architectural changes, a few other breaking changes were made to s --- ## 0.2.9 -* Fixed copy & paste, drag & drop of attachments in the [[Desktop]] app -* Continuous [[Sync]] +* Fixed copy & paste, drag & drop of attachments in the Desktop app +* Continuous Sync * Support for embedding [[Markdown/Code Widgets]]. * ~~Ability to set the editor font via the `fontFamily` setting~~ in [[SETTINGS]] (restart the app/reload the page to make it go into effect). **Update**: now done via [[STYLES]] --- ## 0.2.8 -* [[Sync]] should now be usable and is documented -* Windows and Mac [[Desktop]] apps now have proper icons (only Linux left) -* [[Mobile]] app for iOS in TestFlight +* Sync should now be usable and is documented +* Windows and Mac Desktop apps now have proper icons (only Linux left) +* Mobile app for iOS in TestFlight * New onboarding index page when you create a new space, pulling content from [[Getting Started]]. * Various bug fixes @@ -229,7 +234,7 @@ Besides these architectural changes, a few other breaking changes were made to s * New directive `#eval` see [[πŸ”Œ Directive@eval]] * New PlugOS feature: redirecting function calls. Instead of specifying a `path` for a function, you can now specify `redirect` pointing to another function name, either in the same plug using the `plugName.functionName` syntax. * `Cmd-click` or `Ctrl-click` now opens page references in a new window. You can `Alt-click` to put your cursor at the target without navigation. -* New {[Open Weekly Note]} command (weeks start on Sunday by default, to allow for planning, but you can change this to Monday by setting the `weeklyNoteMonday` to `true` in [[Settings]]). Like for {[Open Daily Note]} you can create a template in `template/page/Weekly Note`. +* New {[Open Weekly Note]} command (weeks start on Sunday by default, to allow for planning, but you can change this to Monday by setting the `weeklyNoteMonday` to `true` in [[SETTINGS]]). Like for {[Open Daily Note]} you can create a template in `template/page/Weekly Note`. * The `Create page` option when navigating pages now always appears as the _second_ option. Let me know how you like it. * New `Preview` using a custom markdown renderer offering a lot of extra flexibility (and a much smaller file size). New thing it does: * Render front matter in a table diff --git a/website/Mattermost Plugin.md b/website/Mattermost Plugin.md deleted file mode 100644 index 1385f7f..0000000 --- a/website/Mattermost Plugin.md +++ /dev/null @@ -1,35 +0,0 @@ -Work on the `mattermost-plugin` β€”Β integration of SB into Mattermost as a -product/plugin as a proof of concept. - -To do: - -- [ ] Bundle backend with node.js as part of plugin - - Various options investigated, including [nexe](https://github.com/nexe/nexe) - and [pkg](https://github.com/vercel/pkg) but neither of these will work - great due to dynamically loading and resolving of modules in the node.js - sandbox implementation. - - Most straight-forward option is to simply bundle the `node` binary per - platform + a trimmed down `node_modules` modeled how `npx` does this. Once - per platform. -- [ ] Fix CSS styling issues -- [ ] Store pages in DB instead of text files (general SB feature, not MM - specific) -- [ ] Switch over SB plugin to use MM database (MySQL, Postgres) as backing - store rather than SQLite -- [ ] Freeze plug configuration (don’t allow anybody or at most admins) to - update plugs for security reasons. We may simply remove the `PLUGS` page. - - What about `SETTINGS`? - - Easy option: disable, don’t use - - Fancier option: make them user specific with a layer on top of the FS - - What about `SECRETS`? - -To deliberate on: - -- Consider page locking mechanisms, or re-implement real-time collaboration - (would require introducing web sockets again and OT) β€”Β big project. -- Consider page revision options -- Scope of spaces, tied to: - - Personal (default SB PKMS use case, no permission, collaboration issues) - - Channel (old Boards model) - - Team - - Server diff --git a/website/SETTINGS.md b/website/SETTINGS.md index 25accf5..b89aa06 100644 --- a/website/SETTINGS.md +++ b/website/SETTINGS.md @@ -25,4 +25,8 @@ spaceIgnore: | dist largefolder *.mp4 + +# Federation +#federate: +#- someserver ``` diff --git a/website/_headers b/website/_headers index ea8e6f3..2a052f6 100644 --- a/website/_headers +++ b/website/_headers @@ -11,4 +11,4 @@ access-control-allow-headers: * access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS access-control-allow-origin: * - access-control-expose-headers: * + access-control-expose-headers: * \ No newline at end of file diff --git a/website/template/debug.md b/website/template/debug.md index 939ac19..45b06d6 100644 --- a/website/template/debug.md +++ b/website/template/debug.md @@ -1,5 +1,4 @@ {{#each .}} {{@key}}: {{.}} {{/each}} - --- \ No newline at end of file diff --git a/website/template/page/Daily Note.md b/website/template/page/Daily Note.md new file mode 100644 index 0000000..1501f4b --- /dev/null +++ b/website/template/page/Daily Note.md @@ -0,0 +1 @@ +* |^| \ No newline at end of file diff --git a/website/template/plug.md b/website/template/plug.md index 8fa0a9a..fb7813a 100644 --- a/website/template/plug.md +++ b/website/template/plug.md @@ -1 +1 @@ -* [[{{name}}]] {{#if author}}by **{{author}}** ([repo]({{repo}})){{/if}} \ No newline at end of file +* [[{{name}}]] {{#if author}}by **{{author}}** ([repo]({{repo}})){{/if}} \ No newline at end of file diff --git a/website/template/task.md b/website/template/task.md index 6efc241..d5318fe 100644 --- a/website/template/task.md +++ b/website/template/task.md @@ -1 +1 @@ -* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}} {{#if deadline}}πŸ“… {{deadline}}{{/if}} \ No newline at end of file +* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}} \ No newline at end of file diff --git a/website/πŸ”Œ Core/Plug Management.md b/website/πŸ”Œ Core/Plug Management.md index 3883f5e..ac200f1 100644 --- a/website/πŸ”Œ Core/Plug Management.md +++ b/website/πŸ”Œ Core/Plug Management.md @@ -9,4 +9,3 @@ The [[πŸ”Œ Core]] plug has support for the following URI prefixes for plugs: * `https:` loading plugs via HTTPS, e.g. `[https://](https://raw.githubusercontent.com/silverbulletmd/silverbullet-github/main/github.plug.json)` * `github:org/repo/file.plug.json` internally rewritten to a `https` url as above. * `ghr:org/repo/version` to fetch a plug from a Github release -* \ No newline at end of file diff --git a/website/πŸ”Œ Core/Tags.md b/website/πŸ”Œ Core/Tags.md index 43ebe76..2358954 100644 --- a/website/πŸ”Œ Core/Tags.md +++ b/website/πŸ”Œ Core/Tags.md @@ -18,7 +18,7 @@ and be queried: <!-- #query item where tags = "core-tag" --> |name |tags |page |pos| |-------------------------------|--------|------------|---| -|This is a tagged item #core-tag|core-tag|πŸ”Œ Core/Tags|485| +|This is a tagged item #core-tag|core-tag|πŸ”Œ Core/Tags|486| <!-- /query --> and **tags**: @@ -28,5 +28,5 @@ and **tags**: And they can be queried this way: <!-- #query task where tags = "core-tag" render [[template/task]] --> -* [ ] [[πŸ”Œ Core/Tags@533]] This is a tagged task #core-tag +* [ ] [[πŸ”Œ Core/Tags@783]] This is a tagged task #core-tag <!-- /query --> diff --git a/website/πŸ”Œ Core/Templates.md b/website/πŸ”Œ Core/Templates.md index 04a129c..739ed77 100644 --- a/website/πŸ”Œ Core/Templates.md +++ b/website/πŸ”Œ Core/Templates.md @@ -19,7 +19,7 @@ For instance: $name: "πŸ“• " --- - # {{page}} + # {{@page.name}} As recorded on {{today}}. ## Introduction @@ -64,8 +64,8 @@ with a πŸ—“οΈ emoji by default, but this is configurable via the `weeklyNotePre The {[Quick Note]} command will navigate to an empty page named with the current date and time prefixed with a πŸ“₯ emoji, but this is configurable via the `quickNotePrefix` in `SETTINGS`. The use case is to take a quick note outside of your current context. -### Template placeholders - +### Template helpers +$vars Currently supported (hardcoded in the code): - `{{today}}`: Today’s date in the usual YYYY-MM-DD format @@ -73,4 +73,11 @@ Currently supported (hardcoded in the code): - `{{yesterday}}`: Yesterday’s date in the usual YYY-MM-DD format - `{{lastWeek}}`: Current date - 7 days - `{{nextWeek}}`: Current date + 7 days -- `{{page}}`: The name of the current page +- `{{escapeRegexp "hello/there"}}` to escape a regexp, useful when injecting e.g. a page name into a query β€” think `name =~ /{{escapeRegexp @page.name}}/` +- `{{json @page}}` translate any (object) value to JSON, mostly useful for debugging +- `{{relativePath @page.name}}` translate a path to a relative one (to the current page), useful when injecting page names, e.g. `{{relativePath name}}`. +- `{{translateAbsoluteLinks "text"}}` translates all absolute page links in the argument string to relative ones. +- `{{substring "my string" 0 3}}` performs a substring operation on the first argument, which in this example would result in `my ` +- `{{prefixLines "my string\nanother" " "}}` prefixes each line (except the first) with the given prefix. +- `{{niceDate @page.lastModified}}` translates any timestamp into a β€œnice” format (e.g. `2023-06-20`). +- The `@page` variable contains all page meta data (`name`, `lastModified`, `contentType`, as well as any custom [[Frontmatter]] attributes). You can address it like so: `{{@page.name}}` diff --git a/website/πŸ”Œ Directive/Query.md b/website/πŸ”Œ Directive/Query.md index ac975ea..4f0fa79 100644 --- a/website/πŸ”Œ Directive/Query.md +++ b/website/πŸ”Œ Directive/Query.md @@ -163,11 +163,11 @@ For the sake of simplicity, we will use the `page` data source and limit the res **Result:** Look at the data. This is more than we need. The query even gives us template pages. Let's try to limit it in the next step. <!-- #query page limit 3 --> -|name |lastModified |contentType |size |perm|tags| -|-----------|-------------|-------------|-----|--|----| -|CHANGELOG |1684497544505|text/markdown|23605|rw|tags| -|Cloud Links|1676121406519|text/markdown|1177 |rw| | -|Frontmatter|1676121406519|text/markdown|1090 |rw| | +|name |lastModified |contentType |size |perm|tags| +|--------------|-------------|-------------|-----|--|----| +|Authentication|1686682290943|text/markdown|1730 |rw| | +|BROKEN LINKS |1688066558009|text/markdown|196 |rw| | +|CHANGELOG |1687348511871|text/markdown|27899|rw|tags| <!-- /query --> @@ -178,13 +178,13 @@ For the sake of simplicity, we will use the `page` data source and limit the res **Result:** Okay, this is what we wanted but there is also information such as `perm`, `type` and `lastModified` that we don't need. <!-- #query page where type = "plug" order by lastModified desc limit 5 --> -|name |lastModified |contentType |size|perm|type|uri |repo |author | -|--|--|--|--|--|--|--|--|--| -|πŸ”Œ Git |1676639116714|text/markdown|943 |rw|plug|github:silverbulletmd/silverbullet-git/git.plug.json |https://github.com/silverbulletmd/silverbullet-git |Zef Hemel | -|πŸ”Œ Share |1676121406530|text/markdown|711 |rw|plug| |https://github.com/silverbulletmd/silverbullet | | -|πŸ”Œ Tasks |1676121406530|text/markdown|1229|rw|plug| |https://github.com/silverbulletmd/silverbullet | | -|πŸ”Œ Twitter |1676121406530|text/markdown|1269|rw|plug|github:silverbulletmd/silverbullet-twitter/twitter.plug.json|https://github.com/silverbulletmd/silverbullet-twitter|SilverBullet Authors| -|πŸ”Œ Graph View|1676121406529|text/markdown|1041|rw|plug|github:bbroeksema/silverbullet-graphview/graphview.plug.json|https://github.com/bbroeksema/silverbullet-graphview |Bertjan Broeksema | +|name |lastModified |contentType |size|perm|type|uri |repo |author |share-support| +|--|--|--|--|--|--|--|--|--|--| +|πŸ”Œ KaTeX |1687099068396|text/markdown|1342|rw|plug|github:silverbulletmd/silverbullet-katex/katex.plug.js |https://github.com/silverbulletmd/silverbullet-katex |Zef Hemel | | +|πŸ”Œ Core |1687094809367|text/markdown|402 |rw|plug| |https://github.com/silverbulletmd/silverbullet | | | +|πŸ”Œ Collab |1686682290959|text/markdown|2969|rw|plug| |https://github.com/silverbulletmd/silverbullet | |true| +|πŸ”Œ Twitter|1685105433212|text/markdown|1266|rw|plug|github:silverbulletmd/silverbullet-twitter/twitter.plug.js|https://github.com/silverbulletmd/silverbullet-twitter|SilverBullet Authors| | +|πŸ”Œ Mermaid|1685105423879|text/markdown|1096|rw|plug|github:silverbulletmd/silverbullet-mermaid/mermaid.plug.js|https://github.com/silverbulletmd/silverbullet-mermaid|Zef Hemel | | <!-- /query --> #### 6.3 Query to select only certain fields @@ -195,14 +195,14 @@ and `repo` columns and then sort by last modified time. **Result:** Okay, this is much better. However, I believe this needs a touch from a visual perspective. -<!-- #query page select name author repo uririrririrririrririrririrririrririri where type = "plug" order by lastModified desc limit 5 --> -|name |author |repo |ririrririrririrririrririri| -|--|--|--|--| -|πŸ”Œ Git |Zef Hemel |https://github.com/silverbulletmd/silverbullet-git || -|πŸ”Œ Share | |https://github.com/silverbulletmd/silverbullet || -|πŸ”Œ Tasks | |https://github.com/silverbulletmd/silverbullet || -|πŸ”Œ Twitter |SilverBullet Authors|https://github.com/silverbulletmd/silverbullet-twitter|| -|πŸ”Œ Graph View|Bertjan Broeksema |https://github.com/bbroeksema/silverbullet-graphview || +<!-- #query page select name, author, repo where type = "plug" order by lastModified desc limit 5 --> +|name |author |repo | +|--|--|--| +|πŸ”Œ KaTeX |Zef Hemel |https://github.com/silverbulletmd/silverbullet-katex | +|πŸ”Œ Core | |https://github.com/silverbulletmd/silverbullet | +|πŸ”Œ Collab | |https://github.com/silverbulletmd/silverbullet | +|πŸ”Œ Twitter|SilverBullet Authors|https://github.com/silverbulletmd/silverbullet-twitter| +|πŸ”Œ Mermaid|Zef Hemel |https://github.com/silverbulletmd/silverbullet-mermaid| <!-- /query --> #### 6.4 Display the data in a format defined by a template @@ -211,12 +211,12 @@ from a visual perspective. **Result:** Here you go. This is the result we would like to achieve πŸŽ‰. Did you see how I used `render` and `template/plug` in a query? πŸš€ -<!-- #query page select name author repo uririrririrririrririrririrririrririri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] --> -* [[πŸ”Œ Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git)) -* [[πŸ”Œ Share]] -* [[πŸ”Œ Tasks]] -* [[πŸ”Œ Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter)) -* [[πŸ”Œ Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview)) +<!-- #query page where type = "plug" order by lastModified desc limit 5 render [[template/plug]] --> +* [[πŸ”Œ KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex)) +* [[πŸ”Œ Core]] +* [[πŸ”Œ Collab]] +* [[πŸ”Œ Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter)) +* [[πŸ”Œ Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid)) <!-- /query --> PS: You don't need to select only certain fields to use templates. Templates are diff --git a/website/πŸ”Œ Plugs.md b/website/πŸ”Œ Plugs.md index a39d870..2c0c4ba 100644 --- a/website/πŸ”Œ Plugs.md +++ b/website/πŸ”Œ Plugs.md @@ -17,27 +17,27 @@ Plugs are distributed as self-contained JavaScript bundles (ending with `.plug.j ## Core plugs These plugs are distributed with SilverBullet and are automatically enabled: <!-- #query page where type = "plug" and uri = null order by name render [[template/plug]] --> -* [[πŸ”Œ Collab]] -* [[πŸ”Œ Core]] -* [[πŸ”Œ Directive]] -* [[πŸ”Œ Emoji]] -* [[πŸ”Œ Markdown]] -* [[πŸ”Œ Share]] +* [[πŸ”Œ Collab]] +* [[πŸ”Œ Core]] +* [[πŸ”Œ Directive]] +* [[πŸ”Œ Emoji]] +* [[πŸ”Œ Markdown]] +* [[πŸ”Œ Share]] * [[πŸ”Œ Tasks]] <!-- /query --> ## Third-party plugs These plugs are written either by third parties or distributed separately from the main SB distribution: <!-- #query page where type = "plug" and uri != null order by name render [[template/plug]] --> -* [[πŸ”Œ Backlinks]] by **Guillermo VayΓ‘** ([repo](https://github.com/silverbulletmd/silverbullet-backlinks)) -* [[πŸ”Œ Ghost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-ghost)) -* [[πŸ”Œ Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git)) -* [[πŸ”Œ Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github)) -* [[πŸ”Œ Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview)) -* [[πŸ”Œ KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex)) -* [[πŸ”Œ Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost)) -* [[πŸ”Œ Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid)) -* [[πŸ”Œ Serendipity]] by **Pantelis Vratsalis** ([repo](https://github.com/m1lt0n/silverbullet-serendipity)) +* [[πŸ”Œ Backlinks]] by **Guillermo VayΓ‘** ([repo](https://github.com/silverbulletmd/silverbullet-backlinks)) +* [[πŸ”Œ Ghost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-ghost)) +* [[πŸ”Œ Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git)) +* [[πŸ”Œ Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github)) +* [[πŸ”Œ Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview)) +* [[πŸ”Œ KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex)) +* [[πŸ”Œ Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost)) +* [[πŸ”Œ Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid)) +* [[πŸ”Œ Serendipity]] by **Pantelis Vratsalis** ([repo](https://github.com/m1lt0n/silverbullet-serendipity)) * [[πŸ”Œ Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter)) <!-- /query --> @@ -93,4 +93,3 @@ Once you’re happy with your plug, you can distribute it in various ways: `- github:yourgithubuser/yourrepo/yourplugname.plug.js` to their `PLUGS` file - Add a release in your github repo and instruct users to add the release as `- ghr:yourgithubuser/yourrepo` or if they need a specific release `- ghr:yourgithubuser/yourrepo/release-name` - You can put it on any other web server, and tell people to load it via https, e.g., `- https://mydomain.com/mypugname.plug.js`. -