1
0

Big refactors and fixes

* Query regen
* Fix anchor completion
* Dependency fixes
* Changelog update
This commit is contained in:
Zef Hemel 2023-07-02 11:25:32 +02:00 committed by GitHub
parent fee2c5928e
commit 7c825348b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 935 additions and 567 deletions

View File

@ -1,4 +1,4 @@
FROM lukechannings/deno:v1.33.2 FROM lukechannings/deno:v1.34.3
# The volume that will keep the space data # The volume that will keep the space data
# Create a volume first: # Create a volume first:
# docker volume create myspace # docker volume create myspace
@ -12,8 +12,6 @@ ARG TARGETARCH
# Adding tini manually, as it's not included anymore in the new baseimage # Adding tini manually, as it's not included anymore in the new baseimage
ENV TINI_VERSION v0.19.0 ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${TARGETARCH} /tini 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_UID_GID 1000
ENV SILVERBULLET_USERNAME silverbullet 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} /space \
&& chown -R ${SILVERBULLET_USERNAME}:${SILVERBULLET_USERNAME} /deno-dir \ && chown -R ${SILVERBULLET_USERNAME}:${SILVERBULLET_USERNAME} /deno-dir \
&& chmod +x /tini \ && chmod +x /tini \
&& apt update \
&& apt install -y git \
&& echo "**** cleanup ****" \ && echo "**** cleanup ****" \
&& apt-get -y autoremove \ && apt-get -y autoremove \
&& apt-get clean \ && 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) # Port map this when running, e.g. with -p 3002:3000 (where 3002 is the host port)
EXPOSE 3000 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. # 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 # 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 ENTRYPOINT /tini -- deno run -A /silverbullet.js -L0.0.0.0 /space

View File

@ -82,36 +82,36 @@ async function buildCopyBundleAssets() {
"dist/plug_asset_bundle.json", "dist/plug_asset_bundle.json",
); );
await Promise.all([ console.log("Now ESBuilding the client and service workers...");
esbuild.build({
entryPoints: [ await esbuild.build({
{ entryPoints: [
in: "web/boot.ts", {
out: ".client/client", in: "web/boot.ts",
}, out: ".client/client",
{ },
in: "web/service_worker.ts", {
out: "service_worker", in: "web/service_worker.ts",
}, out: "service_worker",
], },
outdir: "dist_client_bundle", ],
absWorkingDir: Deno.cwd(), outdir: "dist_client_bundle",
bundle: true, absWorkingDir: Deno.cwd(),
treeShaking: true, bundle: true,
sourcemap: "linked", treeShaking: true,
minify: true, sourcemap: "linked",
jsxFactory: "h", minify: true,
jsx: "automatic", jsxFactory: "h",
jsxFragment: "Fragment", jsx: "automatic",
jsxImportSource: "https://esm.sh/preact@10.11.1", jsxFragment: "Fragment",
plugins: [ jsxImportSource: "https://esm.sh/preact@10.11.1",
...denoPlugins({ plugins: [
importMapURL: new URL("./import_map.json", import.meta.url) ...denoPlugins({
.toString(), importMapURL: new URL("./import_map.json", import.meta.url)
}), .toString(),
], }),
}), ],
]); });
// Patch the service_worker {{CACHE_NAME}} // Patch the service_worker {{CACHE_NAME}}
let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js"); let swCode = await Deno.readTextFile("dist_client_bundle/service_worker.js");

View File

@ -62,7 +62,7 @@ export {
} from "@codemirror/view"; } from "@codemirror/view";
export type { DecorationSet, KeyBinding } 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 { export {
EditorSelection, EditorSelection,
@ -99,20 +99,20 @@ export {
unfoldCode, unfoldCode,
} from "@codemirror/language"; } 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 { export {
pgSQL as postgresqlLanguage, pgSQL as postgresqlLanguage,
standardSQL as sqlLanguage, standardSQL as sqlLanguage,
} from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/sql?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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; 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"; export { json as jsonLanguage } from "https://esm.sh/@codemirror/legacy-modes@6.3.2/mode/javascript?external=@codemirror/language&target=es2022";
export { export {
c as cLanguage, c as cLanguage,
cpp as cppLanguage, cpp as cppLanguage,
@ -123,12 +123,12 @@ export {
objectiveC as objectiveCLanguage, objectiveC as objectiveCLanguage,
objectiveCpp as objectiveCppLanguage, objectiveCpp as objectiveCppLanguage,
scala as scalaLanguage, 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 { export {
javascriptLanguage, javascriptLanguage,
typescriptLanguage, 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"; export { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts";

View File

@ -1,21 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // 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({ export const parser = LRParser.deserialize({
version: 14, version: 14,
states: 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",
"&`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",
stateData: goto: "#g!TPP!UP!XP!]!`!fPP!oPP!oP!XP!XP!t!XP!XPP!w!}#T#ZPPPPPPP#aPPPPPPPPPPPP#dRQOTWPXR[RQZRRndQmcQrkRwtVlcktRg^QXPRbXQurRxuQeZRoeQi_RqiRskR`U",
"&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", 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",
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, maxTerm: 50,
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 4, repeatNodeCount: 4,
tokenData: 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#h<T#h#i?P#i#k&Q#k#l@{#l#o&Q#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~~%WWOr%Trs%ps$Ip%T$Ip$Iq%p$Iq$Ir%p$Ir;'S%T;'S;=`%u<%lO%T~%uOY~~%xP;=`<%l%T~&QOv~P&VSRP}!O&Q!c!}&Q#R#S&Q#T#o&Q~&hX[~OY&cZ]&c^!P&c!P!Q'T!Q#O&c#O#P'Y#P;'S&c;'S;=`(Z<%lO&c~'YO[~~']RO;'S&c;'S;=`'f;=`O&c~'kY[~OY&cZ]&c^!P&c!P!Q'T!Q#O&c#O#P'Y#P;'S&c;'S;=`(Z;=`<%l&c<%lO&c~(^P;=`<%l&c~(fPX~!Q![(a~(nPq~!_!`(q~(vOx~~({Py~#r#s)O~)TO}~~)YP|~!_!`)]~)bO{~R)gPtQ!}#O)jP)mTO#P)j#P#Q)|#Q;'S)j;'S;=`*X<%lO)jP*PP#P#Q*SP*XOiPP*[P;=`<%l)j~*dOw~R*iWRP}!O&Q!c!}&Q#R#S&Q#T#b&Q#b#c+R#c#g&Q#g#h+}#h#o&QR+WURP}!O&Q!c!}&Q#R#S&Q#T#W&Q#W#X+j#X#o&QR+qS_QRP}!O&Q!c!}&Q#R#S&Q#T#o&QR,SURP}!O&Q!c!}&Q#R#S&Q#T#V&Q#V#W,f#W#o&QR,mS!RQRP}!O&Q!c!}&Q#R#S&Q#T#o&QR-OURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y-b#Y#o&QR-gURP}!O&Q!c!}&Q#R#S&Q#T#g&Q#g#h-y#h#o&QR.OURP}!O&Q!c!}&Q#R#S&Q#T#V&Q#V#W.b#W#o&QR.iS!QQRP}!O&Q!c!}&Q#R#S&Q#T#o&QR.zTRP}!O&Q!c!}&Q#R#S&Q#T#U/Z#U#o&QR/`URP}!O&Q!c!}&Q#R#S&Q#T#`&Q#`#a/r#a#o&QR/wURP}!O&Q!c!}&Q#R#S&Q#T#g&Q#g#h0Z#h#o&QR0`URP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y0r#Y#o&QR0ySsQRP}!O&Q!c!}&Q#R#S&Q#T#o&QR1[URP}!O&Q!c!}&Q#R#S&Q#T#b&Q#b#c1n#c#o&QR1uS!PQRP}!O&Q!c!}&Q#R#S&Q#T#o&QR2WURP}!O&Q!c!}&Q#R#S&Q#T#]&Q#]#^2j#^#o&QR2oURP}!O&Q!c!}&Q#R#S&Q#T#a&Q#a#b3R#b#o&QR3WURP}!O&Q!c!}&Q#R#S&Q#T#]&Q#]#^3j#^#o&QR3oURP}!O&Q!c!}&Q#R#S&Q#T#h&Q#h#i4R#i#o&QR4YSaQRP}!O&Q!c!}&Q#R#S&Q#T#o&QR4kURP}!O&Q!c!}&Q#R#S&Q#T#i&Q#i#j4}#j#o&QR5SURP}!O&Q!c!}&Q#R#S&Q#T#`&Q#`#a5f#a#o&QR5kURP}!O&Q!c!}&Q#R#S&Q#T#`&Q#`#a5}#a#o&QR6USRP]Q}!O&Q!c!}&Q#R#S&Q#T#o&QR6gURP}!O&Q!c!}&Q#R#S&Q#T#f&Q#f#g6y#g#o&QR7OURP}!O&Q!c!}&Q#R#S&Q#T#W&Q#W#X7b#X#o&QR7gURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y7y#Y#o&QR8OURP}!O&Q!c!}&Q#R#S&Q#T#f&Q#f#g8b#g#o&QR8gTRPpq8v}!O&Q!c!}&Q#R#S&Q#T#o&QQ8yP#U#V8|Q9PP#m#n9SQ9XOcQR9^URP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y9p#Y#o&QR9uURP}!O&Q!c!}&Q#R#S&Q#T#b&Q#b#c:X#c#o&QR:^URP}!O&Q!c!}&Q#R#S&Q#T#W&Q#W#X:p#X#o&QR:uURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y;X#Y#o&QR;^URP}!O&Q!c!}&Q#R#S&Q#T#f&Q#f#g;p#g#o&QR;wSRPhQ}!O&Q!c!}&Q#R#S&Q#T#o&QR<YURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y<l#Y#o&QR<qURP}!O&Q!c!}&Q#R#S&Q#T#`&Q#`#a=T#a#o&QR=YURP}!O&Q!c!}&Q#R#S&Q#T#X&Q#X#Y=l#Y#o&QR=qURP}!O&Q!c!}&Q#R#S&Q#T#V&Q#V#W>T#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",
"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<QURP}!O%t!c!}%t#R#S%t#T#V%t#V#W<d#W#o%tR<iURP}!O%t!c!}%t#R#S%t#T#h%t#h#i<{#i#o%tR=SSRPfQ}!O%t!c!}%t#R#S%t#T#o%tR=eURP}!O%t!c!}%t#R#S%t#T#f%t#f#g=w#g#o%tR=|URP}!O%t!c!}%t#R#S%t#T#i%t#i#j>`#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",
tokenizers: [0, 1], tokenizers: [0, 1],
topRules: { "Program": [0, 1] }, topRules: {"Program":[0,1]},
tokenPrec: 0, tokenPrec: 0
}); })

View File

@ -1,5 +1,6 @@
// This file was generated by lezer-generator. You probably shouldn't edit it. // This file was generated by lezer-generator. You probably shouldn't edit it.
export const Program = 1, export const
Program = 1,
Query = 2, Query = 2,
Name = 3, Name = 3,
WhereClause = 4, WhereClause = 4,
@ -23,4 +24,4 @@ export const Program = 1,
Select = 22, Select = 22,
RenderClause = 23, RenderClause = 23,
Render = 24, Render = 24,
PageRef = 25; PageRef = 25

View File

@ -57,7 +57,7 @@ export class AssetBundlePlugSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean, selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
if (this.assetBundle.has(name)) { if (this.assetBundle.has(name)) {
console.warn("Attempted to write to read-only asset file", name); console.warn("Attempted to write to read-only asset file", name);
@ -67,7 +67,7 @@ export class AssetBundlePlugSpacePrimitives implements SpacePrimitives {
name, name,
data, data,
selfUpdate, selfUpdate,
lastModified, meta,
); );
} }

View File

@ -71,7 +71,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
_selfUpdate?: boolean, _selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
const localPath = this.filenameToPath(name); const localPath = this.filenameToPath(name);
try { try {
@ -87,9 +87,9 @@ export class DiskSpacePrimitives implements SpacePrimitives {
// Actually write the file // Actually write the file
await Deno.write(file.rid, data); await Deno.write(file.rid, data);
if (lastModified) { if (meta?.lastModified) {
console.log("Seting mtime to", new Date(lastModified)); // console.log("Seting mtime to", new Date(meta.lastModified));
await Deno.futime(file.rid, new Date(), new Date(lastModified)); await Deno.futime(file.rid, new Date(), new Date(meta.lastModified));
} }
file.close(); file.close();

View File

@ -20,13 +20,13 @@ export class EventedSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean, selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
const newMeta = await this.wrapped.writeFile( const newMeta = await this.wrapped.writeFile(
name, name,
data, data,
selfUpdate, selfUpdate,
lastModified, meta,
); );
// This can happen async // This can happen async
if (name.endsWith(".md")) { if (name.endsWith(".md")) {
@ -47,7 +47,7 @@ export class EventedSpacePrimitives implements SpacePrimitives {
console.error("Error dispatching page:saved event", e); 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); await this.eventHook.dispatchEvent("plug:changed", name);
} }
return newMeta; return newMeta;

View File

@ -35,9 +35,9 @@ export class FallbackSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean | undefined, selfUpdate?: boolean | undefined,
lastModified?: number | undefined, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
return this.primary.writeFile(name, data, selfUpdate, lastModified); return this.primary.writeFile(name, data, selfUpdate, meta);
} }
deleteFile(name: string): Promise<void> { deleteFile(name: string): Promise<void> {
return this.primary.deleteFile(name); return this.primary.deleteFile(name);

View File

@ -43,21 +43,40 @@ export class FileMetaSpacePrimitives implements SpacePrimitives {
return this.wrapped.readFile(name); return this.wrapped.readFile(name);
} }
getFileMeta(name: string): Promise<FileMeta> { async getFileMeta(name: string): Promise<FileMeta> {
return this.wrapped.getFileMeta(name); 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( writeFile(
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean, selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
return this.wrapped.writeFile( return this.wrapped.writeFile(
name, name,
data, data,
selfUpdate, selfUpdate,
lastModified, meta,
); );
} }

View File

@ -25,9 +25,9 @@ export class FilteredSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean | undefined, selfUpdate?: boolean | undefined,
lastModified?: number | undefined, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
return this.wrapped.writeFile(name, data, selfUpdate, lastModified); return this.wrapped.writeFile(name, data, selfUpdate, meta);
} }
deleteFile(name: string): Promise<void> { deleteFile(name: string): Promise<void> {
return this.wrapped.deleteFile(name); return this.wrapped.deleteFile(name);

View File

@ -23,7 +23,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
const result = await fetch(url, { ...options }); const result = await fetch(url, { ...options });
if ( if (
result.status === 401 this.getRealStatus(result) === 401
) { ) {
// Invalid credentials, reloading the browser should trigger authentication // Invalid credentials, reloading the browser should trigger authentication
console.log("Going to redirect after", url); console.log("Going to redirect after", url);
@ -33,13 +33,20 @@ export class HttpSpacePrimitives implements SpacePrimitives {
return result; return result;
} }
getRealStatus(r: Response) {
if (r.headers.get("X-Status")) {
return +r.headers.get("X-Status")!;
}
return r.status;
}
async fetchFileList(): Promise<FileMeta[]> { async fetchFileList(): Promise<FileMeta[]> {
const resp = await this.authenticatedFetch(this.url, { const resp = await this.authenticatedFetch(this.url, {
method: "GET", method: "GET",
}); });
if ( if (
resp.status === 200 && this.getRealStatus(resp) === 200 &&
this.expectedSpacePath && this.expectedSpacePath &&
resp.headers.get("X-Space-Path") !== this.expectedSpacePath resp.headers.get("X-Space-Path") !== this.expectedSpacePath
) { ) {
@ -60,7 +67,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
method: "GET", method: "GET",
}, },
); );
if (res.status === 404) { if (this.getRealStatus(res) === 404) {
throw new Error(`Not found`); throw new Error(`Not found`);
} }
return { return {
@ -73,13 +80,14 @@ export class HttpSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
_selfUpdate?: boolean, _selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
}; };
if (lastModified) { if (meta) {
headers["X-Last-Modified"] = "" + lastModified; headers["X-Last-Modified"] = "" + meta.lastModified;
headers["X-Perm"] = "" + meta.perm;
} }
const res = await this.authenticatedFetch( const res = await this.authenticatedFetch(
@ -101,7 +109,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
method: "DELETE", method: "DELETE",
}, },
); );
if (req.status !== 200) { if (this.getRealStatus(req) !== 200) {
throw Error(`Failed to delete file: ${req.statusText}`); throw Error(`Failed to delete file: ${req.statusText}`);
} }
} }
@ -113,7 +121,7 @@ export class HttpSpacePrimitives implements SpacePrimitives {
method: "OPTIONS", method: "OPTIONS",
}, },
); );
if (res.status === 404) { if (this.getRealStatus(res) === 404) {
throw new Error(`Not found`); throw new Error(`Not found`);
} }
return this.responseToMeta(name, res); return this.responseToMeta(name, res);

View File

@ -5,6 +5,7 @@ import { mime } from "../deps.ts";
export type FileContent = { export type FileContent = {
name: string; name: string;
meta: FileMeta;
data: Uint8Array; data: Uint8Array;
}; };
@ -35,10 +36,6 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives {
async readFile( async readFile(
name: string, name: string,
): Promise<{ data: Uint8Array; meta: FileMeta }> { ): 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); const fileContent = await this.filesContentTable.get(name);
if (!fileContent) { if (!fileContent) {
throw new Error("Not found"); throw new Error("Not found");
@ -46,7 +43,7 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives {
return { return {
data: fileContent.data, data: fileContent.data,
meta: fileMeta, meta: fileContent.meta,
}; };
} }
@ -54,18 +51,18 @@ export class IndexedDBSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
_selfUpdate?: boolean, _selfUpdate?: boolean,
lastModified?: number, suggestedMeta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
const fileMeta: FileMeta = { const meta: FileMeta = {
name, name,
lastModified: lastModified || Date.now(), lastModified: suggestedMeta?.lastModified || Date.now(),
contentType: mime.getType(name) || "application/octet-stream", contentType: mime.getType(name) || "application/octet-stream",
size: data.byteLength, size: data.byteLength,
perm: "rw", perm: suggestedMeta?.perm || "rw",
}; };
await this.filesContentTable.put({ name, data }); await this.filesContentTable.put({ name, data, meta });
await this.filesMetaTable.put(fileMeta); await this.filesMetaTable.put(meta);
return fileMeta; return meta;
} }
async deleteFile(name: string): Promise<void> { async deleteFile(name: string): Promise<void> {

View File

@ -4,11 +4,6 @@ import {
NamespaceOperation, NamespaceOperation,
PageNamespaceHook, PageNamespaceHook,
} from "../hooks/page_namespace.ts"; } from "../hooks/page_namespace.ts";
import {
base64DecodeDataUrl,
base64EncodedDataUrl,
} from "../../plugos/asset_bundle/base64.ts";
import { mime } from "../deps.ts";
export class PlugSpacePrimitives implements SpacePrimitives { export class PlugSpacePrimitives implements SpacePrimitives {
constructor( constructor(
@ -40,6 +35,7 @@ export class PlugSpacePrimitives implements SpacePrimitives {
for ( for (
const { operation, pattern, plug, name, env } of this.hook.spaceFunctions const { operation, pattern, plug, name, env } of this.hook.spaceFunctions
) { ) {
// console.log("Going to match agains pattern", pattern, path);
if ( if (
operation === type && path.match(pattern) && operation === type && path.match(pattern) &&
(!this.env || (env && env === this.env)) (!this.env || (env && env === this.env))
@ -73,16 +69,13 @@ export class PlugSpacePrimitives implements SpacePrimitives {
async readFile( async readFile(
name: string, name: string,
): Promise<{ data: Uint8Array; meta: FileMeta }> { ): Promise<{ data: Uint8Array; meta: FileMeta }> {
const result: { data: string; meta: FileMeta } | false = await this const result: { data: Uint8Array; meta: FileMeta } | false = await this
.performOperation( .performOperation(
"readFile", "readFile",
name, name,
); );
if (result) { if (result) {
return { return result;
data: base64DecodeDataUrl(result.data),
meta: result.meta,
};
} }
return this.wrapped.readFile(name); return this.wrapped.readFile(name);
} }
@ -99,16 +92,14 @@ export class PlugSpacePrimitives implements SpacePrimitives {
name: string, name: string,
data: Uint8Array, data: Uint8Array,
selfUpdate?: boolean, selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta> { ): Promise<FileMeta> {
const result = this.performOperation( const result = this.performOperation(
"writeFile", "writeFile",
name, name,
base64EncodedDataUrl( data,
mime.getType(name) || "application/octet-stream",
data,
),
selfUpdate, selfUpdate,
meta,
); );
if (result) { if (result) {
return result; return result;
@ -118,7 +109,7 @@ export class PlugSpacePrimitives implements SpacePrimitives {
name, name,
data, data,
selfUpdate, selfUpdate,
lastModified, meta,
); );
} }

View File

@ -15,7 +15,7 @@ export interface SpacePrimitives {
data: Uint8Array, data: Uint8Array,
// Used to decide whether or not to emit change events // Used to decide whether or not to emit change events
selfUpdate?: boolean, selfUpdate?: boolean,
lastModified?: number, meta?: FileMeta,
): Promise<FileMeta>; ): Promise<FileMeta>;
deleteFile(name: string): Promise<void>; deleteFile(name: string): Promise<void>;
} }

View File

@ -135,7 +135,7 @@ export class SpaceSync {
name, name,
data, data,
false, false,
meta.lastModified, meta,
); );
snapshot.set(name, [ snapshot.set(name, [
primaryHash, primaryHash,
@ -157,7 +157,7 @@ export class SpaceSync {
name, name,
data, data,
false, false,
meta.lastModified, meta,
); );
snapshot.set(name, [ snapshot.set(name, [
writtenMeta.lastModified, writtenMeta.lastModified,
@ -219,7 +219,7 @@ export class SpaceSync {
name, name,
data, data,
false, false,
meta.lastModified, meta,
); );
snapshot.set(name, [ snapshot.set(name, [
primaryHash, primaryHash,
@ -243,7 +243,7 @@ export class SpaceSync {
name, name,
data, data,
false, false,
meta.lastModified, meta,
); );
snapshot.set(name, [ snapshot.set(name, [
writtenMeta.lastModified, writtenMeta.lastModified,

View File

@ -1,24 +1,24 @@
{ {
"imports": { "imports": {
"@lezer/common": "https://esm.sh/@lezer/common@1.0.2", "@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=deno", "@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", "@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", "@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", "@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", "@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/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", "@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", "@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", "@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", "@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", "@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", "@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", "@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", "@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", "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/", "$sb/": "./plug-api/",
"handlebars": "https://esm.sh/handlebars@4.7.7?target=es2022", "handlebars": "https://esm.sh/handlebars@4.7.7?target=es2022",
"dexie": "https://esm.sh/dexie@3.2.2" "dexie": "https://esm.sh/dexie@3.2.2"

View File

@ -43,6 +43,7 @@ export type PublishEvent = {
}; };
export type CompleteEvent = { export type CompleteEvent = {
pageName: string;
linePrefix: string; linePrefix: string;
pos: number; pos: number;
}; };

6
plug-api/lib/fetch.ts Normal file
View File

@ -0,0 +1,6 @@
declare global {
function nativeFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response>;
}

View File

@ -13,6 +13,8 @@ export function sandboxFetch(
} }
export function monkeyPatchFetch() { export function monkeyPatchFetch() {
// @ts-ignore: monkey patching fetch
globalThis.nativeFetch = globalThis.fetch;
// @ts-ignore: monkey patching fetch // @ts-ignore: monkey patching fetch
globalThis.fetch = async function ( globalThis.fetch = async function (
url: string, url: string,

View File

@ -129,6 +129,10 @@ export function vimEx(exCommand: string): Promise<any> {
return syscall("editor.vimEx", exCommand); return syscall("editor.vimEx", exCommand);
} }
export function syncSpace(): Promise<number> {
return syscall("editor.syncSpace");
}
// Folding // Folding
export function fold() { export function fold() {

View File

@ -4,9 +4,9 @@ import { bundleAssets } from "./asset_bundle/builder.ts";
import { Manifest } from "./types.ts"; import { Manifest } from "./types.ts";
import { version } from "../version.ts"; import { version } from "../version.ts";
// const workerRuntimeUrl = new URL("./worker_runtime.ts", import.meta.url); const workerRuntimeUrl = new URL("./worker_runtime.ts", import.meta.url);
const workerRuntimeUrl = // const workerRuntimeUrl =
`https://deno.land/x/silverbullet@${version}/plugos/worker_runtime.ts`; // `https://deno.land/x/silverbullet@${version}/plugos/worker_runtime.ts`;
export type CompileOptions = { export type CompileOptions = {
debug?: boolean; debug?: boolean;

View File

@ -5,9 +5,9 @@ export class DexieKVStore implements KVStore {
db: Dexie; db: Dexie;
items: Table<KV, string>; items: Table<KV, string>;
constructor( constructor(
private dbName: string, dbName: string,
private tableName: string, tableName: string,
private indexedDB?: any, indexedDB?: any,
) { ) {
this.db = new Dexie(dbName, { this.db = new Dexie(dbName, {
indexedDB, indexedDB,

View File

@ -8,4 +8,5 @@ export const builtinPlugNames = [
"share", "share",
"tasks", "tasks",
"search", "search",
"federation",
]; ];

View File

@ -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 { nanoid } from "https://esm.sh/nanoid@4.0.0";
import { FileMeta } from "../../common/types.ts"; import { FileMeta } from "../../common/types.ts";
import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts";
const defaultServer = "wss://collab.silverbullet.md"; const defaultServer = "wss://collab.silverbullet.md";
@ -118,7 +117,7 @@ export function shareNoop() {
export function readFileCollab( export function readFileCollab(
name: string, name: string,
): { data: string; meta: FileMeta } { ): { data: Uint8Array; meta: FileMeta } {
if (!name.endsWith(".md")) { if (!name.endsWith(".md")) {
throw new Error("Not found"); throw new Error("Not found");
} }
@ -127,10 +126,7 @@ export function readFileCollab(
return { return {
// encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl" // encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl"
data: base64EncodedDataUrl( data: new TextEncoder().encode(text),
"text/markdown",
new TextEncoder().encode(text),
),
meta: { meta: {
name, name,
contentType: "text/markdown", contentType: "text/markdown",

View File

@ -1,5 +1,5 @@
import { collectNodesOfType } from "$sb/lib/tree.ts"; 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 type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts";
import { removeQueries } from "$sb/lib/query.ts"; import { removeQueries } from "$sb/lib/query.ts";
@ -29,7 +29,7 @@ export async function anchorComplete(completeEvent: CompleteEvent) {
let [pageRef, anchorRef] = match[1].split("@"); let [pageRef, anchorRef] = match[1].split("@");
if (!pageRef) { if (!pageRef) {
pageRef = await editor.getCurrentPage(); pageRef = completeEvent.pageName;
} }
const allAnchors = await index.queryPrefix( const allAnchors = await index.queryPrefix(
`a:${pageRef}:${anchorRef}`, `a:${pageRef}:${anchorRef}`,

View File

@ -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);
}

View File

@ -1,13 +1,12 @@
import { renderToText, replaceNodesMatching } from "$sb/lib/tree.ts"; import { renderToText, replaceNodesMatching } from "$sb/lib/tree.ts";
import type { FileMeta } from "../../common/types.ts"; import type { FileMeta } from "../../common/types.ts";
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts";
export const cloudPrefix = "💭 "; export const cloudPrefix = "💭 ";
export async function readFileCloud( export async function readFileCloud(
name: string, name: string,
): Promise<{ data: string; meta: FileMeta } | undefined> { ): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> {
const originalUrl = name.substring( const originalUrl = name.substring(
cloudPrefix.length, cloudPrefix.length,
name.length - ".md".length, name.length - ".md".length,
@ -38,10 +37,7 @@ export async function readFileCloud(
`${cloudPrefix}${originalUrl.split("/")[0]}/`, `${cloudPrefix}${originalUrl.split("/")[0]}/`,
); );
return { return {
data: base64EncodedDataUrl( data: new TextEncoder().encode(text),
"text/markdown",
new TextEncoder().encode(text),
),
meta: { meta: {
name, name,
contentType: "text/markdown", contentType: "text/markdown",

View File

@ -52,6 +52,13 @@ functions:
command: command:
name: "Page: Copy" name: "Page: Copy"
syncSpaceCommand:
path: "./sync.ts:syncSpaceCommand"
command:
name: "Sync: Now"
key: "Alt-Shift-s"
mac: "Cmd-Shift-s"
# Attachments # Attachments
attachmentQueryProvider: attachmentQueryProvider:
path: ./attachment.ts:attachmentQueryProvider path: ./attachment.ts:attachmentQueryProvider
@ -180,6 +187,15 @@ functions:
description: Turn line into h4 header description: Turn line into h4 header
match: "^#*\\s*" match: "^#*\\s*"
replace: "#### " replace: "#### "
insertCodeBlock:
redirect: insertTemplateText
slashCommand:
name: code
description: Insert code block
value: |
```
|^|
```
newPage: newPage:
path: ./page.ts:newPageCommand path: ./page.ts:newPageCommand
@ -433,3 +449,8 @@ functions:
name: "Editor: Vim: Load VIMRC" name: "Editor: Vim: Load VIMRC"
events: events:
- editor:modeswitch - editor:modeswitch
brokenLinksCommand:
path: ./broken_links.ts:brokenLinksCommand
command:
name: "Broken Links: Show"

View File

@ -35,6 +35,7 @@ async function actionClickOrActionEnter(
return; return;
} }
} }
const currentPage = await editor.getCurrentPage();
switch (mdTree.type) { switch (mdTree.type) {
case "WikiLink": { case "WikiLink": {
let pageLink = mdTree.children![1]!.children![0].text!; let pageLink = mdTree.children![1]!.children![0].text!;
@ -46,20 +47,20 @@ async function actionClickOrActionEnter(
} }
} }
if (!pageLink) { if (!pageLink) {
pageLink = await editor.getCurrentPage(); pageLink = currentPage;
} }
await editor.navigate(pageLink, pos, false, inNewWindow); await editor.navigate(pageLink, pos, false, inNewWindow);
break; break;
} }
case "PageRef": { case "PageRef": {
const bracketedPageRef = mdTree.children![0].text!; const bracketedPageRef = mdTree.children![0].text!;
await editor.navigate(
// Slicing off the initial [[ and final ]] // Slicing off the initial [[ and final ]]
bracketedPageRef.substring(2, bracketedPageRef.length - 2), const pageName = bracketedPageRef.substring(
0, 2,
false, bracketedPageRef.length - 2,
inNewWindow,
); );
await editor.navigate(pageName, 0, false, inNewWindow);
break; break;
} }
case "NakedURL": case "NakedURL":
@ -71,13 +72,12 @@ async function actionClickOrActionEnter(
if (!urlNode) { if (!urlNode) {
return; return;
} }
let url = urlNode.children![0].text!; const url = urlNode.children![0].text!;
if (url.length <= 1) { if (url.length <= 1) {
return editor.flashNotification("Empty link, ignoring", "error"); return editor.flashNotification("Empty link, ignoring", "error");
} }
if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) { if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) {
url = decodeURIComponent(url); return editor.openUrl(`/.fs/${decodeURI(url)}`);
return editor.openUrl(`/.fs/${url}`);
} else { } else {
await editor.openUrl(url); await editor.openUrl(url);
} }

View File

@ -20,9 +20,9 @@ import {
renderToText, renderToText,
replaceNodesMatching, replaceNodesMatching,
} from "$sb/lib/tree.ts"; } 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 { 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: // Key space:
// pl:toPage:pos => pageName // pl:toPage:pos => pageName
@ -32,6 +32,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
const backLinks: { key: string; value: string }[] = []; const backLinks: { key: string; value: string }[] = [];
// [[Style Links]] // [[Style Links]]
// console.log("Now indexing links for", name); // console.log("Now indexing links for", name);
removeQueries(tree);
const pageMeta = await extractFrontmatter(tree); const pageMeta = await extractFrontmatter(tree);
if (Object.keys(pageMeta).length > 0) { if (Object.keys(pageMeta).length > 0) {
// console.log("Extracted page meta data", pageMeta); // console.log("Extracted page meta data", pageMeta);
@ -44,8 +45,6 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
await index.set(name, "meta:", pageMeta); await index.set(name, "meta:", pageMeta);
} }
// throw new Error("Boom");
collectNodesMatching(tree, (n) => n.type === "WikiLinkPage").forEach((n) => { collectNodesMatching(tree, (n) => n.type === "WikiLinkPage").forEach((n) => {
let toPage = n.children![0].text!; let toPage = n.children![0].text!;
if (toPage.includes("@")) { if (toPage.includes("@")) {
@ -106,7 +105,7 @@ export async function copyPage() {
await space.getPageMeta(newName); await space.getPageMeta(newName);
// So when we get to this point, we error out // So when we get to this point, we error out
throw new Error( throw new Error(
`Page ${newName} already exists, cannot rename to existing page.`, `Page ${newName} already exists, cannot rename to existing page.`,
); );
} catch (e: any) { } catch (e: any) {
if (e.message === "Not found") { if (e.message === "Not found") {
@ -165,6 +164,7 @@ export async function renamePage(cmdDef: any) {
console.log("All pages containing backlinks", pagesToUpdate); console.log("All pages containing backlinks", pagesToUpdate);
const text = await editor.getText(); const text = await editor.getText();
console.log("Writing new page to space"); console.log("Writing new page to space");
const newPageMeta = await space.writePage(newName, text); const newPageMeta = await space.writePage(newName, text);
console.log("Navigating to new page"); console.log("Navigating to new page");
@ -198,6 +198,7 @@ export async function renamePage(cmdDef: any) {
} }
const mdTree = await markdown.parseMarkdown(text); const mdTree = await markdown.parseMarkdown(text);
addParentPointers(mdTree); addParentPointers(mdTree);
// The links in the page are going to be relative pointers to the old name
replaceNodesMatching(mdTree, (n): ParseTree | undefined | null => { replaceNodesMatching(mdTree, (n): ParseTree | undefined | null => {
if (n.type === "WikiLinkPage") { if (n.type === "WikiLinkPage") {
const pageName = n.children![0].text!; const pageName = n.children![0].text!;
@ -265,18 +266,20 @@ export async function reindexCommand() {
// Completion // Completion
export async function pageComplete(completeEvent: CompleteEvent) { export async function pageComplete(completeEvent: CompleteEvent) {
const match = /\[\[([^\]@:]*)$/.exec(completeEvent.linePrefix); const match = /\[\[([^\]@:\{}]*)$/.exec(completeEvent.linePrefix);
if (!match) { if (!match) {
return null; return null;
} }
const allPages = await space.listPages(); const allPages = await space.listPages();
return { return {
from: completeEvent.pos - match[1].length, from: completeEvent.pos - match[1].length,
options: allPages.map((pageMeta) => ({ options: allPages.map((pageMeta) => {
label: pageMeta.name, return {
boost: pageMeta.lastModified, label: pageMeta.name,
type: "page", boost: pageMeta.lastModified,
})), type: "page",
};
}),
}; };
} }

7
plugs/core/sync.ts Normal file
View File

@ -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.");
}

View File

@ -3,6 +3,10 @@ import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { renderToText } from "$sb/lib/tree.ts"; import { renderToText } from "$sb/lib/tree.ts";
import { niceDate } from "$sb/lib/dates.ts"; import { niceDate } from "$sb/lib/dates.ts";
import { readSettings } from "$sb/lib/settings_page.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() { export async function instantiateTemplateCommand() {
const allPages = await space.listPages(); const allPages = await space.listPages();
@ -36,10 +40,16 @@ export async function instantiateTemplateCommand() {
"$disableDirectives", "$disableDirectives",
]); ]);
const tempPageMeta: PageMeta = {
name: "",
lastModified: 0,
perm: "rw",
};
if (additionalPageMeta.$name) { if (additionalPageMeta.$name) {
additionalPageMeta.$name = replaceTemplateVars( additionalPageMeta.$name = replaceTemplateVars(
additionalPageMeta.$name, additionalPageMeta.$name,
"", tempPageMeta,
); );
} }
@ -50,6 +60,7 @@ export async function instantiateTemplateCommand() {
if (!pageName) { if (!pageName) {
return; return;
} }
tempPageMeta.name = pageName;
try { try {
// Fails if doesn't exist // Fails if doesn't exist
@ -67,7 +78,7 @@ export async function instantiateTemplateCommand() {
// The preferred scenario, let's keep going // 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 space.writePage(pageName, pageText);
await editor.navigate(pageName); await editor.navigate(pageName);
} }
@ -79,6 +90,7 @@ export async function insertSnippet() {
}); });
const cursorPos = await editor.getCursor(); const cursorPos = await editor.getCursor();
const page = await editor.getCurrentPage(); const page = await editor.getCurrentPage();
const pageMeta = await space.getPageMeta(page);
const allSnippets = allPages const allSnippets = allPages
.filter((pageMeta) => pageMeta.name.startsWith(snippetPrefix)) .filter((pageMeta) => pageMeta.name.startsWith(snippetPrefix))
.map((pageMeta) => ({ .map((pageMeta) => ({
@ -97,10 +109,10 @@ export async function insertSnippet() {
} }
const text = await space.readPage(`${snippetPrefix}${selectedSnippet.name}`); const text = await space.readPage(`${snippetPrefix}${selectedSnippet.name}`);
let templateText = replaceTemplateVars(text, page); let templateText = replaceTemplateVars(text, pageMeta);
const carretPos = templateText.indexOf("|^|"); const carretPos = templateText.indexOf("|^|");
templateText = templateText.replace("|^|", ""); templateText = templateText.replace("|^|", "");
templateText = replaceTemplateVars(templateText, page); templateText = replaceTemplateVars(templateText, pageMeta);
await editor.insertAtCursor(templateText); await editor.insertAtCursor(templateText);
if (carretPos !== -1) { if (carretPos !== -1) {
await editor.moveCursor(cursorPos + carretPos); await editor.moveCursor(cursorPos + carretPos);
@ -108,37 +120,9 @@ export async function insertSnippet() {
} }
// TODO: This should probably be replaced with handlebards somehow? // TODO: This should probably be replaced with handlebards somehow?
export function replaceTemplateVars(s: string, pageName: string): string { export function replaceTemplateVars(s: string, pageMeta: PageMeta): string {
return s.replaceAll(/\{\{([^\}]+)\}\}/g, (match, v) => { const template = Handlebars.compile(s, { noEscape: true });
switch (v) { return template({}, buildHandebarOptions(pageMeta));
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 async function quickNoteCommand() { export async function quickNoteCommand() {
@ -159,6 +143,7 @@ export async function dailyNoteCommand() {
}); });
const date = niceDate(new Date()); const date = niceDate(new Date());
const pageName = `${dailyNotePrefix}${date}`; const pageName = `${dailyNotePrefix}${date}`;
let carretPos = 0;
try { try {
await space.getPageMeta(pageName); await space.getPageMeta(pageName);
@ -167,15 +152,25 @@ export async function dailyNoteCommand() {
let dailyNoteTemplateText = ""; let dailyNoteTemplateText = "";
try { try {
dailyNoteTemplateText = await space.readPage(dailyNoteTemplate); dailyNoteTemplateText = await space.readPage(dailyNoteTemplate);
carretPos = dailyNoteTemplateText.indexOf("|^|");
if (carretPos === -1) {
carretPos = 0;
}
dailyNoteTemplateText = dailyNoteTemplateText.replace("|^|", "");
} catch { } catch {
console.warn(`No daily note template found at ${dailyNoteTemplate}`); console.warn(`No daily note template found at ${dailyNoteTemplate}`);
} }
await space.writePage( await space.writePage(
pageName, 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) { function getWeekStartDate(monday = false) {
@ -210,7 +205,11 @@ export async function weeklyNoteCommand() {
// Doesn't exist, let's create // Doesn't exist, let's create
await space.writePage( await space.writePage(
pageName, pageName,
replaceTemplateVars(weeklyNoteTemplateText, pageName), replaceTemplateVars(weeklyNoteTemplateText, {
name: pageName,
lastModified: 0,
perm: "rw",
}),
); );
} }
await editor.navigate(pageName); await editor.navigate(pageName);
@ -222,10 +221,11 @@ export async function weeklyNoteCommand() {
export async function insertTemplateText(cmdDef: any) { export async function insertTemplateText(cmdDef: any) {
const cursorPos = await editor.getCursor(); const cursorPos = await editor.getCursor();
const page = await editor.getCurrentPage(); const page = await editor.getCurrentPage();
const pageMeta = await space.getPageMeta(page);
let templateText: string = cmdDef.value; let templateText: string = cmdDef.value;
const carretPos = templateText.indexOf("|^|"); const carretPos = templateText.indexOf("|^|");
templateText = templateText.replace("|^|", ""); templateText = templateText.replace("|^|", "");
templateText = replaceTemplateVars(templateText, page); templateText = replaceTemplateVars(templateText, pageMeta);
await editor.insertAtCursor(templateText); await editor.insertAtCursor(templateText);
if (carretPos !== -1) { if (carretPos !== -1) {
await editor.moveCursor(cursorPos + carretPos); await editor.moveCursor(cursorPos + carretPos);

View File

@ -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 { import {
removeParentPointers, removeParentPointers,
renderToText, renderToText,
@ -6,11 +6,12 @@ import {
} from "$sb/lib/tree.ts"; } from "$sb/lib/tree.ts";
import { renderDirectives } from "./directives.ts"; import { renderDirectives } from "./directives.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { PageMeta } from "../../web/types.ts";
export async function updateDirectivesOnPageCommand(arg: any) { export async function updateDirectivesOnPageCommand(arg: any) {
// If `arg` is a string, it's triggered automatically via an event, not explicitly via a command // If `arg` is a string, it's triggered automatically via an event, not explicitly via a command
const explicitCall = typeof arg !== "string"; const explicitCall = typeof arg !== "string";
const pageName = await editor.getCurrentPage(); const pageMeta = await space.getPageMeta(await editor.getCurrentPage());
const text = await editor.getText(); const text = await editor.getText();
const tree = await markdown.parseMarkdown(text); const tree = await markdown.parseMarkdown(text);
const metaData = await extractFrontmatter(tree, ["$disableDirectives"]); const metaData = await extractFrontmatter(tree, ["$disableDirectives"]);
@ -55,7 +56,7 @@ export async function updateDirectivesOnPageCommand(arg: any) {
} }
const fullMatch = text.substring(tree.from!, tree.to!); const fullMatch = text.substring(tree.from!, tree.to!);
try { try {
const promise = renderDirectives(pageName, tree); const promise = renderDirectives(pageMeta, tree);
replacements.push({ replacements.push({
textPromise: promise, textPromise: promise,
fullMatch, fullMatch,
@ -117,7 +118,7 @@ export async function updateDirectivesOnPageCommand(arg: any) {
// Pure server driven implementation of directive updating // Pure server driven implementation of directive updating
export async function updateDirectives( export async function updateDirectives(
pageName: string, pageMeta: PageMeta,
text: string, text: string,
) { ) {
const tree = await markdown.parseMarkdown(text); const tree = await markdown.parseMarkdown(text);
@ -134,7 +135,7 @@ export async function updateDirectives(
const fullMatch = text.substring(tree.from!, tree.to!); const fullMatch = text.substring(tree.from!, tree.to!);
try { try {
const promise = renderDirectives( const promise = renderDirectives(
pageName, pageMeta,
tree, tree,
); );
replacements.push({ replacements.push({

View File

@ -1,5 +1,7 @@
import { events } from "$sb/plugos-syscall/mod.ts"; import { events } from "$sb/plugos-syscall/mod.ts";
import { CompleteEvent } from "$sb/app_event.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) { export async function queryComplete(completeEvent: CompleteEvent) {
const match = /#query ([\w\-_]+)*$/.exec(completeEvent.linePrefix); 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,
})),
};
}

View File

@ -21,6 +21,10 @@ functions:
path: ./complete.ts:queryComplete path: ./complete.ts:queryComplete
events: events:
- editor:complete - editor:complete
handlebarHelperComplete:
path: ./complete.ts:handlebarHelperComplete
events:
- editor:complete
# Templates # Templates
insertQuery: insertQuery:

View File

@ -1,5 +1,6 @@
import { ParseTree, renderToText } from "$sb/lib/tree.ts"; import { ParseTree, renderToText } from "$sb/lib/tree.ts";
import { sync } from "../../plug-api/silverbullet-syscall/mod.ts"; import { sync } from "../../plug-api/silverbullet-syscall/mod.ts";
import { PageMeta } from "../../web/types.ts";
import { evalDirectiveRenderer } from "./eval_directive.ts"; import { evalDirectiveRenderer } from "./eval_directive.ts";
import { queryDirectiveRenderer } from "./query_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 * Looks for directives in the text dispatches them based on name
*/ */
export async function directiveDispatcher( export async function directiveDispatcher(
pageName: string, pageMeta: PageMeta,
directiveTree: ParseTree, directiveTree: ParseTree,
directiveRenderers: Record< directiveRenderers: Record<
string, string,
( (
directive: string, directive: string,
pageName: string, pageMeta: PageMeta,
arg: string | ParseTree, arg: string | ParseTree,
) => Promise<string> ) => Promise<string>
>, >,
@ -34,6 +35,14 @@ export async function directiveDispatcher(
const directiveStartText = renderToText(directiveStart).trim(); const directiveStartText = renderToText(directiveStart).trim();
const directiveEndText = renderToText(directiveEnd).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) { if (directiveStart.children!.length === 1) {
// Everything not #query // Everything not #query
const match = directiveStartRegex.exec(directiveStart.children![0].text!); const match = directiveStartRegex.exec(directiveStart.children![0].text!);
@ -44,7 +53,7 @@ export async function directiveDispatcher(
let [_fullMatch, type, arg] = match; let [_fullMatch, type, arg] = match;
try { try {
arg = arg.trim(); arg = arg.trim();
const newBody = await directiveRenderers[type](type, pageName, arg); const newBody = await directiveRenderers[type](type, pageMeta, arg);
const result = const result =
`${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`; `${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`;
return result; return result;
@ -53,17 +62,9 @@ export async function directiveDispatcher(
} }
} else { } else {
// #query // #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"]( const newBody = await directiveRenderers["query"](
"query", "query",
pageName, pageMeta,
directiveStart.children![1], // The query ParseTree directiveStart.children![1], // The query ParseTree
); );
const result = const result =
@ -73,10 +74,10 @@ export async function directiveDispatcher(
} }
export async function renderDirectives( export async function renderDirectives(
pageName: string, pageMeta: PageMeta,
directiveTree: ParseTree, directiveTree: ParseTree,
): Promise<string> { ): Promise<string> {
const replacementText = await directiveDispatcher(pageName, directiveTree, { const replacementText = await directiveDispatcher(pageMeta, directiveTree, {
use: templateDirectiveRenderer, use: templateDirectiveRenderer,
include: templateDirectiveRenderer, include: templateDirectiveRenderer,
query: queryDirectiveRenderer, query: queryDirectiveRenderer,

View File

@ -3,6 +3,8 @@
import { YAML } from "$sb/plugos-syscall/mod.ts"; import { YAML } from "$sb/plugos-syscall/mod.ts";
import { ParseTree } from "$sb/lib/tree.ts"; import { ParseTree } from "$sb/lib/tree.ts";
import { jsonToMDTable, renderTemplate } from "./util.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 // Enables plugName.functionName(arg1, arg2) syntax in JS expressions
function translateJs(js: string): string { function translateJs(js: string): string {
@ -20,13 +22,13 @@ const expressionRegex = /(.+?)(\s+render\s+\[\[([^\]]+)\]\])?$/;
// This is rather scary and fragile stuff, but it works. // This is rather scary and fragile stuff, but it works.
export async function evalDirectiveRenderer( export async function evalDirectiveRenderer(
_directive: string, _directive: string,
_pageName: string, pageMeta: PageMeta,
expression: string | ParseTree, expression: string | ParseTree,
): Promise<string> { ): Promise<string> {
if (typeof expression !== "string") { if (typeof expression !== "string") {
throw new Error("Expected a string"); throw new Error("Expected a string");
} }
console.log("Got JS expression", expression); // console.log("Got JS expression", expression);
const match = expressionRegex.exec(expression); const match = expressionRegex.exec(expression);
if (!match) { if (!match) {
throw new Error(`Invalid eval directive: ${expression}`); throw new Error(`Invalid eval directive: ${expression}`);
@ -44,11 +46,11 @@ export async function evalDirectiveRenderer(
function invokeFunction(name, ...args) { function invokeFunction(name, ...args) {
return syscall("system.invokeFunction", "server", name, ...args); return syscall("system.invokeFunction", "server", name, ...args);
} }
return ${translateJs(expression)}; return ${replaceTemplateVars(translateJs(expression), pageMeta)};
})()`, })()`,
); );
if (template) { if (template) {
return await renderTemplate(template, result); return await renderTemplate(pageMeta, template, result);
} }
if (typeof result === "string") { if (typeof result === "string") {
return result; return result;

View File

@ -5,17 +5,18 @@ import { renderTemplate } from "./util.ts";
import { parseQuery } from "./parser.ts"; import { parseQuery } from "./parser.ts";
import { jsonToMDTable } from "./util.ts"; import { jsonToMDTable } from "./util.ts";
import { ParseTree } from "$sb/lib/tree.ts"; import { ParseTree } from "$sb/lib/tree.ts";
import { PageMeta } from "../../web/types.ts";
export async function queryDirectiveRenderer( export async function queryDirectiveRenderer(
_directive: string, _directive: string,
pageName: string, pageMeta: PageMeta,
query: string | ParseTree, query: string | ParseTree,
): Promise<string> { ): Promise<string> {
if (typeof query === "string") { if (typeof query === "string") {
throw new Error("Argument must be a ParseTree"); throw new Error("Argument must be a ParseTree");
} }
const parsedQuery = parseQuery( const parsedQuery = parseQuery(
JSON.parse(replaceTemplateVars(JSON.stringify(query), pageName)), JSON.parse(replaceTemplateVars(JSON.stringify(query), pageMeta)),
); );
const eventName = `query:${parsedQuery.table}`; const eventName = `query:${parsedQuery.table}`;
@ -24,7 +25,7 @@ export async function queryDirectiveRenderer(
// Let's dispatch an event and see what happens // Let's dispatch an event and see what happens
const results = await events.dispatchEvent( const results = await events.dispatchEvent(
eventName, eventName,
{ query: parsedQuery, pageName: pageName }, { query: parsedQuery, pageName: pageMeta.name },
30 * 1000, 30 * 1000,
); );
if (results.length === 0) { if (results.length === 0) {
@ -34,6 +35,7 @@ export async function queryDirectiveRenderer(
// console.log("Parsed query", parsedQuery); // console.log("Parsed query", parsedQuery);
if (parsedQuery.render) { if (parsedQuery.render) {
const rendered = await renderTemplate( const rendered = await renderTemplate(
pageMeta,
parsedQuery.render, parsedQuery.render,
results[0], results[0],
); );

View File

@ -8,13 +8,14 @@ import { replaceTemplateVars } from "../core/template.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { directiveRegex } from "./directives.ts"; import { directiveRegex } from "./directives.ts";
import { updateDirectives } from "./command.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*/; const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
export async function templateDirectiveRenderer( export async function templateDirectiveRenderer(
directive: string, directive: string,
pageName: string, pageMeta: PageMeta,
arg: string | ParseTree, arg: string | ParseTree,
): Promise<string> { ): Promise<string> {
if (typeof arg !== "string") { if (typeof arg !== "string") {
@ -29,9 +30,13 @@ export async function templateDirectiveRenderer(
let parsedArgs = {}; let parsedArgs = {};
if (args) { if (args) {
try { try {
parsedArgs = JSON.parse(args); parsedArgs = JSON.parse(replaceTemplateVars(args, pageMeta));
} catch { } 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 = ""; let templateText = "";
@ -51,18 +56,14 @@ export async function templateDirectiveRenderer(
// if it's a template injection (not a literal "include") // if it's a template injection (not a literal "include")
if (directive === "use") { if (directive === "use") {
registerHandlebarsHelpers();
const templateFn = Handlebars.compile( const templateFn = Handlebars.compile(
replaceTemplateVars(newBody, pageName), newBody,
{ noEscape: true }, { noEscape: true },
); );
if (typeof parsedArgs !== "string") { newBody = templateFn(parsedArgs, buildHandebarOptions(pageMeta));
(parsedArgs as any).page = pageName;
}
newBody = templateFn(parsedArgs);
// Recursively render directives // Recursively render directives
newBody = await updateDirectives(pageName, newBody); newBody = await updateDirectives(pageMeta, newBody);
} }
return newBody.trim(); return newBody.trim();
} }

View File

@ -2,6 +2,7 @@ import Handlebars from "handlebars";
import { space } from "$sb/silverbullet-syscall/mod.ts"; import { space } from "$sb/silverbullet-syscall/mod.ts";
import { niceDate } from "$sb/lib/dates.ts"; import { niceDate } from "$sb/lib/dates.ts";
import { PageMeta } from "../../web/types.ts";
const maxWidth = 70; const maxWidth = 70;
@ -79,47 +80,55 @@ export function jsonToMDTable(
} }
export async function renderTemplate( export async function renderTemplate(
pageMeta: PageMeta,
renderTemplate: string, renderTemplate: string,
data: any[], data: any[],
): Promise<string> { ): Promise<string> {
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); let templateText = await space.readPage(renderTemplate);
templateText = `{{#each .}}\n${templateText}\n{{/each}}`; templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
const template = Handlebars.compile(templateText, { noEscape: true }); const template = Handlebars.compile(templateText, { noEscape: true });
return template(data); return template(data, buildHandebarOptions(pageMeta));
} }
export function registerHandlebarsHelpers() { export function buildHandebarOptions(pageMeta: PageMeta) {
Handlebars.registerHelper("json", (v: any) => JSON.stringify(v)); return {
Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts))); helpers: handlebarHelpers(pageMeta.name),
Handlebars.registerHelper("escapeRegexp", (ts: any) => { data: { page: pageMeta },
return ts.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); };
}); }
Handlebars.registerHelper("prefixLines", (v: string, prefix: string) =>
v
.split("\n")
.map((l) => prefix + l)
.join("\n"));
Handlebars.registerHelper( export function handlebarHelpers(pageName: string) {
"substring", return {
(s: string, from: number, to: number, elipsis = "") => 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, 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);
},
};
} }

View File

@ -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

View File

@ -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<FileMeta> {
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<FileMeta[]> {
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<FileMeta> {
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<void> {
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<FileMeta> {
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;
}

View File

@ -12,7 +12,7 @@ type MarkdownRenderOptions = {
annotationPositions?: true; annotationPositions?: true;
attachmentUrlPrefix?: string; attachmentUrlPrefix?: string;
// When defined, use to inline images as data: urls // When defined, use to inline images as data: urls
inlineAttachments?: (url: string) => string; translateUrls?: (url: string) => string;
}; };
function cleanTags(values: (Tag | null)[]): Tag[] { function cleanTags(values: (Tag | null)[]): Tag[] {
@ -385,13 +385,17 @@ export function renderMarkdownToHtml(
) { ) {
preprocess(t, options); preprocess(t, options);
const htmlTree = posPreservingRender(t, options); const htmlTree = posPreservingRender(t, options);
if (htmlTree && options.inlineAttachments) { if (htmlTree && options.translateUrls) {
traverseTag(htmlTree, (t) => { traverseTag(htmlTree, (t) => {
if (typeof t === "string") { if (typeof t === "string") {
return; return;
} }
if (t.name === "img") { 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!);
} }
}); });
} }

View File

@ -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 { asset, store } from "$sb/plugos-syscall/mod.ts";
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
import { renderMarkdownToHtml } from "./markdown_render.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts";
@ -7,6 +7,7 @@ export async function updateMarkdownPreview() {
if (!(await store.get("enableMarkdownPreview"))) { if (!(await store.get("enableMarkdownPreview"))) {
return; return;
} }
const pageName = await editor.getCurrentPage();
const text = await editor.getText(); const text = await editor.getText();
const mdTree = await parseMarkdown(text); const mdTree = await parseMarkdown(text);
// const cleanMd = await cleanMarkdown(text); // const cleanMd = await cleanMarkdown(text);
@ -15,7 +16,7 @@ export async function updateMarkdownPreview() {
const html = renderMarkdownToHtml(mdTree, { const html = renderMarkdownToHtml(mdTree, {
smartHardBreak: true, smartHardBreak: true,
annotationPositions: true, annotationPositions: true,
inlineAttachments: (url) => { translateUrls: (url) => {
if (!url.includes("://")) { if (!url.includes("://")) {
return `/.fs/${url}`; return `/.fs/${url}`;
} }

View File

@ -91,7 +91,7 @@ export async function searchCommand() {
export async function readFileSearch( export async function readFileSearch(
name: string, name: string,
): Promise<{ data: string; meta: FileMeta }> { ): Promise<{ data: Uint8Array; meta: FileMeta }> {
const phrase = name.substring( const phrase = name.substring(
searchPrefix.length, searchPrefix.length,
name.length - ".md".length, name.length - ".md".length,
@ -105,11 +105,7 @@ export async function readFileSearch(
`; `;
return { return {
// encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl" data: new TextEncoder().encode(text),
data: base64EncodedDataUrl(
"text/markdown",
new TextEncoder().encode(text),
),
meta: { meta: {
name, name,
contentType: "text/markdown", contentType: "text/markdown",

View File

@ -85,17 +85,21 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
} }
export function taskToggle(event: ClickEvent) { 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); const [eventName, pos] = JSON.parse(eventString);
if (eventName === "task") { 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]"; let changeTo = "[x]";
if (node.children![0].text === "[x]" || node.children![0].text === "[X]") { if (node.children![0].text === "[x]" || node.children![0].text === "[X]") {
changeTo = "[ ]"; 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 text = await editor.getText();
const mdTree = await markdown.parseMarkdown(text); const mdTree = await markdown.parseMarkdown(text);
addParentPointers(mdTree); addParentPointers(mdTree);
const node = nodeAtPos(mdTree, pos); const node = nodeAtPos(mdTree, pos);
if (node && node.type === "TaskMarker") { 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); const node = nodeAtPos(tree, pos);
// We kwow node.type === Task (due to the task context) // We kwow node.type === Task (due to the task context)
const taskMarker = findNodeOfType(node!, "TaskMarker"); const taskMarker = findNodeOfType(node!, "TaskMarker");
await toggleTaskMarker(taskMarker!, pos); await toggleTaskMarker(await editor.getCurrentPage(), taskMarker!, pos);
} }
export async function postponeCommand() { export async function postponeCommand() {
@ -181,7 +185,7 @@ export async function postponeCommand() {
return; return;
} }
// Parse "naive" due date // 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. // Create new naive Date object.
// `monthIndex` parameter is zero-based, so subtract 1 from parsed month. // `monthIndex` parameter is zero-based, so subtract 1 from parsed month.
const d = new Date(yyyy, mm - 1, dd); const d = new Date(yyyy, mm - 1, dd);
@ -214,6 +218,7 @@ export async function queryProvider({
query, query,
}: QueryProviderEvent): Promise<Task[]> { }: QueryProviderEvent): Promise<Task[]> {
const allTasks: Task[] = []; const allTasks: Task[] = [];
for (const { key, page, value } of await index.queryPrefix("task:")) { for (const { key, page, value } of await index.queryPrefix("task:")) {
const pos = key.split(":")[1]; const pos = key.split(":")[1];
allTasks.push({ allTasks.push({

View File

@ -6,12 +6,7 @@ Deno.test("Collab server", async () => {
console.log("Client 1 joins page 1"); console.log("Client 1 joins page 1");
assertEquals(collabServer.updatePresence("client1", "page1"), {}); assertEquals(collabServer.updatePresence("client1", "page1"), {});
assertEquals(collabServer.pages.size, 1); assertEquals(collabServer.pages.size, 1);
console.log("CLient 1 leaves page 1"); assertEquals(collabServer.updatePresence("client1", "page2"), {});
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.pages.size, 1); assertEquals(collabServer.pages.size, 1);
console.log("Client 2 joins to page 2, collab id created, but not exposed"); console.log("Client 2 joins to page 2, collab id created, but not exposed");
assertEquals( assertEquals(
@ -22,22 +17,23 @@ Deno.test("Collab server", async () => {
collabServer.updatePresence("client1", "page2").collabId !== undefined, collabServer.updatePresence("client1", "page2").collabId !== undefined,
); );
console.log("Client 2 moves to page 1, collab id destroyed"); console.log("Client 2 moves to page 1, collab id destroyed");
assertEquals(collabServer.updatePresence("client2", "page1", "page2"), {}); assertEquals(collabServer.updatePresence("client2", "page1"), {});
assertEquals(collabServer.updatePresence("client1", "page2", "page2"), {}); assertEquals(collabServer.updatePresence("client1", "page2"), {});
assertEquals(collabServer.pages.get("page2")!.collabId, undefined); assertEquals(collabServer.pages.get("page2")!.collabId, undefined);
assertEquals(collabServer.pages.get("page1")!.collabId, undefined); assertEquals(collabServer.pages.get("page1")!.collabId, undefined);
console.log("Going to cleanup, which should have no effect"); console.log("Going to cleanup, which should have no effect");
collabServer.cleanup(50); collabServer.cleanup(50);
assertEquals(collabServer.pages.size, 2); assertEquals(collabServer.pages.size, 2);
collabServer.updatePresence("client2", "page2", "page1"); collabServer.updatePresence("client2", "page2");
console.log("Going to sleep 20ms"); console.log("Going to sleep 20ms");
await sleep(20); await sleep(20);
console.log("Then client 1 pings, but client 2 does not"); console.log("Then client 1 pings, but client 2 does not");
collabServer.updatePresence("client1", "page2", "page2"); collabServer.updatePresence("client1", "page2");
await sleep(20); await sleep(20);
console.log("Going to cleanup, which should clean client 2"); console.log("Going to cleanup, which should clean client 2");
collabServer.cleanup(35); collabServer.cleanup(35);
assertEquals(collabServer.pages.get("page2")!.collabId, undefined); assertEquals(collabServer.pages.get("page2")!.collabId, undefined);
assertEquals(collabServer.clients.size, 1);
console.log(collabServer); console.log(collabServer);
}); });

View File

@ -14,7 +14,7 @@ type CollabPage = {
}; };
export class CollabServer { export class CollabServer {
// clients: Map<string, { openPage: string; lastPing: number }> = new Map(); clients: Map<string, { openPage: string; lastUpdate: number }> = new Map(); // clientId -> openPage
pages: Map<string, CollabPage> = new Map(); pages: Map<string, CollabPage> = new Map();
yCollabServer?: Hocuspocus; yCollabServer?: Hocuspocus;
@ -29,13 +29,18 @@ export class CollabServer {
updatePresence( updatePresence(
clientId: string, clientId: string,
currentPage?: string, currentPage: string,
previousPage?: string,
): { collabId?: 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 // Client switched pages
// Update last page record // Update last page record
const lastCollabPage = this.pages.get(previousPage); const lastCollabPage = this.pages.get(client.openPage);
if (lastCollabPage) { if (lastCollabPage) {
lastCollabPage.clients.delete(clientId); lastCollabPage.clients.delete(clientId);
if (lastCollabPage.clients.size === 1) { if (lastCollabPage.clients.size === 1) {
@ -43,7 +48,7 @@ export class CollabServer {
} }
if (lastCollabPage.clients.size === 0) { if (lastCollabPage.clients.size === 0) {
this.pages.delete(previousPage); this.pages.delete(client.openPage);
} else { } else {
// Elect a new master client // Elect a new master client
if (lastCollabPage.masterClientId === clientId) { 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
// Update new page let nextCollabPage = this.pages.get(currentPage);
let nextCollabPage = this.pages.get(currentPage); if (!nextCollabPage) {
if (!nextCollabPage) { // Newly opened page (no other clients on this page right now)
// Newly opened page (no other clients on this page right now) nextCollabPage = {
nextCollabPage = { clients: new Map(),
clients: new Map(), masterClientId: clientId,
masterClientId: clientId, };
}; this.pages.set(currentPage, nextCollabPage);
this.pages.set(currentPage, nextCollabPage); }
} // Register last ping from us
// Register last ping from us nextCollabPage.clients.set(clientId, Date.now());
nextCollabPage.clients.set(clientId, Date.now());
if (nextCollabPage.clients.size > 1 && !nextCollabPage.collabId) { if (nextCollabPage.clients.size > 1 && !nextCollabPage.collabId) {
// Create a new collabId // Create a new collabId
nextCollabPage.collabId = nanoid(); nextCollabPage.collabId = nanoid();
} }
// console.log("State", this.pages); // console.log("State", this.pages);
if (nextCollabPage.collabId) { if (nextCollabPage.collabId) {
// We will now expose this collabId, except when we're just starting this session // 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 // in which case we'll wait for the original client to publish the document
const existingyCollabSession = this.yCollabServer?.documents.get( const existingyCollabSession = this.yCollabServer?.documents.get(
buildCollabId(nextCollabPage.collabId, `${currentPage}.md`), buildCollabId(nextCollabPage.collabId, `${currentPage}.md`),
); );
if (existingyCollabSession) { if (existingyCollabSession) {
// console.log("Found an existing collab session already, let's join!"); // console.log("Found an existing collab session already, let's join!");
return { collabId: nextCollabPage.collabId }; return { collabId: nextCollabPage.collabId };
} else if (clientId === nextCollabPage.masterClientId) { } else if (clientId === nextCollabPage.masterClientId) {
// console.log("We're the master, so we should connect"); // console.log("We're the master, so we should connect");
return { collabId: nextCollabPage.collabId }; 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 { } 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 {}; return {};
} }
} else { } else {
@ -121,6 +124,13 @@ export class CollabServer {
this.pages.delete(pageName); 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) { route(app: Application) {

View File

@ -9,6 +9,7 @@ import { gitIgnoreCompiler } from "./deps.ts";
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts"; import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
import { CollabServer } from "./collab.ts"; import { CollabServer } from "./collab.ts";
import { Authenticator } from "./auth.ts"; import { Authenticator } from "./auth.ts";
import { oakCors } from "https://deno.land/x/cors@v1.2.2/mod.ts";
export type ServerOptions = { export type ServerOptions = {
hostname: string; hostname: string;
@ -183,6 +184,49 @@ export class HttpServer {
"/logo.png", "/logo.png",
"/.auth", "/.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) { if ((await this.authenticator.getAllUsers()).length > 0) {
app.use(async ({ request, response, cookies }, next) => { app.use(async ({ request, response, cookies }, next) => {
if (!excludedPaths.includes(request.url.pathname)) { if (!excludedPaths.includes(request.url.pathname)) {
@ -204,55 +248,20 @@ export class HttpServer {
return; return;
} }
} }
await 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 {
// Unauthenticated access to excluded paths
await next();
}
}); });
} }
} }
private buildFsRouter(spacePrimitives: SpacePrimitives): Router { private buildFsRouter(spacePrimitives: SpacePrimitives): Router {
const fsRouter = new Router(); const fsRouter = new Router();
const corsMiddleware = oakCors({
allowedHeaders: "*",
exposedHeaders: "*",
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
});
// File list // File list
fsRouter.get("/", async ({ response }) => { fsRouter.get("/", corsMiddleware, async ({ response }) => {
response.headers.set("Content-type", "application/json"); response.headers.set("Content-type", "application/json");
response.headers.set("X-Space-Path", this.options.pagesPath); response.headers.set("X-Space-Path", this.options.pagesPath);
const files = await spacePrimitives.fetchFileList(); const files = await spacePrimitives.fetchFileList();
@ -260,7 +269,7 @@ export class HttpServer {
}); });
// RPC // RPC
fsRouter.post("/", async ({ request, response }) => { fsRouter.post("/", corsMiddleware, async ({ request, response }) => {
const body = await request.body({ type: "json" }).value; const body = await request.body({ type: "json" }).value;
try { try {
switch (body.operation) { switch (body.operation) {
@ -300,19 +309,6 @@ export class HttpServer {
}); });
return; 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: default:
response.headers.set("Content-Type", "text/plain"); response.headers.set("Content-Type", "text/plain");
response.status = 400; response.status = 400;
@ -327,7 +323,7 @@ export class HttpServer {
}); });
fsRouter fsRouter
.get("\/(.+)", async ({ params, response, request }) => { .get("\/(.+)", corsMiddleware, async ({ params, response, request }) => {
const name = params[0]; const name = params[0];
console.log("Loading file", name); console.log("Loading file", name);
if (name.startsWith(".")) { if (name.startsWith(".")) {
@ -364,7 +360,7 @@ export class HttpServer {
response.body = ""; response.body = "";
} }
}) })
.put("\/(.+)", async ({ request, response, params }) => { .put("\/(.+)", corsMiddleware, async ({ request, response, params }) => {
const name = params[0]; const name = params[0];
console.log("Saving file", name); console.log("Saving file", name);
if (name.startsWith(".")) { if (name.startsWith(".")) {
@ -400,8 +396,16 @@ export class HttpServer {
console.error("Pipeline failed", err); console.error("Pipeline failed", err);
} }
}) })
.options("\/(.+)", async ({ response, params }) => { .options("\/(.+)", async ({ request, response, params }) => {
const name = params[0]; 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 { try {
const meta = await spacePrimitives.getFileMeta(name); const meta = await spacePrimitives.getFileMeta(name);
response.status = 200; response.status = 200;
@ -409,13 +413,25 @@ export class HttpServer {
response.headers.set("X-Last-Modified", "" + meta.lastModified); response.headers.set("X-Last-Modified", "" + meta.lastModified);
response.headers.set("X-Content-Length", "" + meta.size); response.headers.set("X-Content-Length", "" + meta.size);
response.headers.set("X-Permission", meta.perm); 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 { } catch {
response.status = 404; // Have to do this because of CORS
response.status = 200;
response.headers.set("X-Status", "404");
response.body = "Not found"; response.body = "Not found";
// console.error("Options failed", err); // console.error("Options failed", err);
} }
}) })
.delete("\/(.+)", async ({ response, params }) => { .delete("\/(.+)", corsMiddleware, async ({ response, params }) => {
const name = params[0]; const name = params[0];
console.log("Deleting file", name); console.log("Deleting file", name);
try { try {

View File

@ -180,12 +180,13 @@ export function attachmentExtension(editor: Editor) {
if (!finalFileName) { if (!finalFileName) {
return; return;
} }
await editor.space.writeAttachment(finalFileName, new Uint8Array(data)); await editor.space.writeAttachment(
let attachmentMarkdown = `[${finalFileName}](${ finalFileName,
encodeURIComponent(finalFileName) new Uint8Array(data),
})`; );
let attachmentMarkdown = `[${finalFileName}](${encodeURI(finalFileName)})`;
if (mimeType.startsWith("image/")) { if (mimeType.startsWith("image/")) {
attachmentMarkdown = `![](${encodeURIComponent(finalFileName)})`; attachmentMarkdown = `![](${encodeURI(finalFileName)})`;
} }
editor.editorView!.dispatch({ editor.editorView!.dispatch({
changes: [ changes: [

View File

@ -8,6 +8,7 @@ import {
import { decoratorStateField } from "./util.ts"; import { decoratorStateField } from "./util.ts";
import type { Space } from "../space.ts"; import type { Space } from "../space.ts";
import type { Editor } from "../editor.tsx";
class InlineImageWidget extends WidgetType { class InlineImageWidget extends WidgetType {
constructor( constructor(
@ -39,13 +40,7 @@ class InlineImageWidget extends WidgetType {
this.space.setCachedImageHeight(this.url, img.height); this.space.setCachedImageHeight(this.url, img.height);
} }
}; };
if (this.url.startsWith("http")) { img.src = this.url;
img.src = this.url;
} else {
// This is an attachment image, rewrite the URL a little
img.src = `/.fs/${decodeURIComponent(this.url)}`;
}
img.alt = this.title; img.alt = this.title;
img.title = this.title; img.title = this.title;
img.style.display = "block"; 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) => { return decoratorStateField((state: EditorState) => {
const widgets: Range<Decoration>[] = []; const widgets: Range<Decoration>[] = [];
const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/; const imageRegex = /!\[(?<title>[^\]]*)\]\((?<url>.+)\)/;
@ -76,11 +71,14 @@ export function inlineImagesPlugin(space: Space) {
return; return;
} }
const url = imageRexexResult.groups.url; let url = imageRexexResult.groups.url;
const title = imageRexexResult.groups.title; const title = imageRexexResult.groups.title;
if (url.indexOf("://") === -1) {
url = `/.fs/${url}`;
}
widgets.push( widgets.push(
Decoration.widget({ Decoration.widget({
widget: new InlineImageWidget(url, title, space), widget: new InlineImageWidget(url, title, editor.space),
block: true, block: true,
}).range(node.to), }).range(node.to),
); );

View File

@ -37,7 +37,7 @@ class TableViewWidget extends WidgetType {
// Annotate every element with its position so we can use it to put // Annotate every element with its position so we can use it to put
// the cursor there when the user clicks on the table. // the cursor there when the user clicks on the table.
annotationPositions: true, annotationPositions: true,
inlineAttachments: (url) => { translateUrls: (url) => {
if (!url.includes("://")) { if (!url.includes("://")) {
return `/.fs/${url}`; return `/.fs/${url}`;
} }

View File

@ -33,6 +33,7 @@ export function cleanWikiLinkPlugin(editor: Editor) {
if (page.includes("@")) { if (page.includes("@")) {
cleanPage = page.split("@")[0]; cleanPage = page.split("@")[0];
} }
// console.log("Resolved page", resolvedPage);
for (const pageMeta of allPages) { for (const pageMeta of allPages) {
if (pageMeta.name === cleanPage) { if (pageMeta.name === cleanPage) {
pageExists = true; pageExists = true;
@ -76,8 +77,10 @@ export function cleanWikiLinkPlugin(editor: Editor) {
widget: new LinkWidget( widget: new LinkWidget(
{ {
text: linkText, text: linkText,
title: pageExists ? `Navigate to ${page}` : `Create ${page}`, title: pageExists
href: `/${page}`, ? `Navigate to ${cleanPage}`
: `Create ${cleanPage}`,
href: `/${cleanPage}`,
cssClass: pageExists cssClass: pageExists
? "sb-wiki-link-page" ? "sb-wiki-link-page"
: "sb-wiki-link-page-missing", : "sb-wiki-link-page-missing",

View File

@ -15,7 +15,7 @@ export class CollabManager {
"editor:pageLoaded", "editor:pageLoaded",
(pageName, previousPage) => { (pageName, previousPage) => {
console.log("Page loaded", 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); }, collabPingInterval);
} }
async updatePresence(currentPage?: string, previousPage?: string) { async updatePresence(currentPage: string) {
try { try {
// This is signaled through an OPTIONS call on the file we have open
const resp = await this.editor.remoteSpacePrimitives.authenticatedFetch( const resp = await this.editor.remoteSpacePrimitives.authenticatedFetch(
this.editor.remoteSpacePrimitives.url, `${this.editor.remoteSpacePrimitives.url}/${currentPage}.md`,
{ {
method: "POST", method: "OPTIONS",
headers: { 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) { if (this.editor.collabState && !this.editor.collabState.isLocalCollab) {
// We're in a remote collab mode, don't do anything // We're in a remote collab mode, don't do anything

View File

@ -8,7 +8,6 @@ import type { ComponentChildren, FunctionalComponent } from "../deps.ts";
import { Notification } from "../types.ts"; import { Notification } from "../types.ts";
import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types"; import { FeatherProps } from "https://esm.sh/v99/preact-feather@4.2.1/dist/types";
import { MiniEditor } from "./mini_editor.tsx"; import { MiniEditor } from "./mini_editor.tsx";
import process from "https://deno.land/std@0.177.1/node/process.ts";
export type ActionButton = { export type ActionButton = {
icon: FunctionalComponent<FeatherProps>; icon: FunctionalComponent<FeatherProps>;

View File

@ -21,7 +21,7 @@ export {
yCollab, yCollab,
yUndoManagerKeymap, yUndoManagerKeymap,
} from "https://esm.sh/y-codemirror.next@0.3.2?external=yjs,@codemirror/state,@codemirror/commands,@codemirror/history,@codemirror/view"; } 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 // Vim mode
export { export {

View File

@ -242,15 +242,8 @@ export class Editor {
true, true,
); );
const plugSpacePrimitives = new PlugSpacePrimitives( const plugSpaceRemotePrimitives = new PlugSpacePrimitives(
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet this.remoteSpacePrimitives,
new FallbackSpacePrimitives(
new IndexedDBSpacePrimitives(
`${dbPrefix}_space`,
globalThis.indexedDB,
),
this.remoteSpacePrimitives,
),
namespaceHook, namespaceHook,
); );
@ -258,7 +251,14 @@ export class Editor {
const localSpacePrimitives = new FilteredSpacePrimitives( const localSpacePrimitives = new FilteredSpacePrimitives(
new FileMetaSpacePrimitives( new FileMetaSpacePrimitives(
new EventedSpacePrimitives( 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, this.eventHook,
), ),
indexSyscalls, indexSyscalls,
@ -279,12 +279,16 @@ export class Editor {
this.syncService = new SyncService( this.syncService = new SyncService(
localSpacePrimitives, localSpacePrimitives,
this.remoteSpacePrimitives, plugSpaceRemotePrimitives,
this.kvStore, this.kvStore,
this.eventHook, this.eventHook,
(path) => { (path) => {
// TODO: At some point we should remove the data.db exception here // 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(), highlightSpecialChars(),
history(), history(),
drawSelection(), drawSelection(),
@ -1117,6 +1121,7 @@ export class Editor {
const linePrefix = line.text.slice(0, selection.from - line.from); const linePrefix = line.text.slice(0, selection.from - line.from);
const results = await this.dispatchAppEvent(eventName, { const results = await this.dispatchAppEvent(eventName, {
pageName: this.currentPage!,
linePrefix, linePrefix,
pos: selection.from, pos: selection.from,
} as CompleteEvent); } as CompleteEvent);
@ -1127,6 +1132,7 @@ export class Editor {
console.error( console.error(
"Got completion results from multiple sources, cannot deal with that", "Got completion results from multiple sources, cannot deal with that",
); );
console.error("Previously had", actualResult, "now also got", result);
return null; return null;
} }
actualResult = result; actualResult = result;

View File

@ -82,10 +82,13 @@ self.addEventListener("fetch", (event: any) => {
} }
const requestUrl = new URL(event.request.url); const requestUrl = new URL(event.request.url);
const pathname = requestUrl.pathname; const pathname = requestUrl.pathname;
// console.log("In service worker, pathname is", 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 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")) { if (fileContentTable && !event.request.headers.has("x-sync-mode")) {
// console.log( // console.log(
// "Attempting to serve file from locally synced space:", // "Attempting to serve file from locally synced space:",
@ -101,8 +104,10 @@ self.addEventListener("fetch", (event: any) => {
// console.log("Serving from space", path); // console.log("Serving from space", path);
return new Response(data.data, { return new Response(data.data, {
headers: { headers: {
"Content-type": mime.getType(path) || "Content-type": data.meta.contentType,
"application/octet-stream", "Content-Length": "" + data.meta.size,
"X-Permission": data.meta.perm,
"X-Last-Modified": "" + data.meta.lastModified,
}, },
}); });
} else { } else {
@ -120,7 +125,7 @@ self.addEventListener("fetch", (event: any) => {
// Just fetch the file directly // Just fetch the file directly
return fetch(event.request); 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 // Must be a page URL, let's serve index.html which will handle it
return caches.match(precacheFiles["/"]).then((response) => { return caches.match(precacheFiles["/"]).then((response) => {
// This shouldnt't happen, index.html not in the cache for some reason // This shouldnt't happen, index.html not in the cache for some reason

View File

@ -88,8 +88,8 @@ export class SyncService {
async hasInitialSyncCompleted(): Promise<boolean> { 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) // 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)) && return !(await this.kvStore.has(syncStartTimeKey)) &&
(await this.kvStore.get(syncLastActivityKey))); (await this.kvStore.has(syncLastActivityKey));
} }
async registerSyncStart(): Promise<void> { async registerSyncStart(): Promise<void> {
@ -371,7 +371,7 @@ export class SyncService {
name, name,
data, data,
false, false,
meta.lastModified, meta,
); );
// Update snapshot // Update snapshot
snapshot.set(name, [ snapshot.set(name, [

View File

@ -182,6 +182,10 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
const cm = vimGetCm(editor.editorView!)!; const cm = vimGetCm(editor.editorView!)!;
return Vim.handleEx(cm, exCommand); return Vim.handleEx(cm, exCommand);
}, },
// Sync
"editor.syncSpace": () => {
return editor.syncService.syncSpace();
},
// Folding // Folding
"editor.fold": () => { "editor.fold": () => {
foldCode(editor.editorView!); foldCode(editor.editorView!);

View File

@ -4,7 +4,12 @@ release.
--- ---
## Next ## 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 pages 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 ## 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. * 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. * 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): * Documentation updates (on https://silverbullet.md):
* [[Special Pages]] * [[Special Pages]]
@ -95,16 +100,16 @@ Besides these architectural changes, a few other breaking changes were made to s
--- ---
## 0.2.9 ## 0.2.9
* Fixed copy & paste, drag & drop of attachments in the [[Desktop]] app * Fixed copy & paste, drag & drop of attachments in the Desktop app
* Continuous [[Sync]] * Continuous Sync
* Support for embedding [[Markdown/Code Widgets]]. * 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]] * ~~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 ## 0.2.8
* [[Sync]] should now be usable and is documented * Sync should now be usable and is documented
* Windows and Mac [[Desktop]] apps now have proper icons (only Linux left) * Windows and Mac Desktop apps now have proper icons (only Linux left)
* [[Mobile]] app for iOS in TestFlight * Mobile app for iOS in TestFlight
* New onboarding index page when you create a new space, pulling content from [[Getting Started]]. * New onboarding index page when you create a new space, pulling content from [[Getting Started]].
* Various bug fixes * 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 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. * 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. * `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. * 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: * 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 * Render front matter in a table

View File

@ -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 (dont 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, dont 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

View File

@ -25,4 +25,8 @@ spaceIgnore: |
dist dist
largefolder largefolder
*.mp4 *.mp4
# Federation
#federate:
#- someserver
``` ```

View File

@ -11,4 +11,4 @@
access-control-allow-headers: * access-control-allow-headers: *
access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS
access-control-allow-origin: * access-control-allow-origin: *
access-control-expose-headers: * access-control-expose-headers: *

View File

@ -1,5 +1,4 @@
{{#each .}} {{#each .}}
{{@key}}: {{.}} {{@key}}: {{.}}
{{/each}} {{/each}}
--- ---

View File

@ -0,0 +1 @@
* |^|

View File

@ -1 +1 @@
* [[{{name}}]] {{#if author}}by **{{author}}** ([repo]({{repo}})){{/if}} * [[{{name}}]] {{#if author}}by **{{author}}** ([repo]({{repo}})){{/if}}

View File

@ -1 +1 @@
* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}} {{#if deadline}}📅 {{deadline}}{{/if}} * [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}}

View File

@ -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)` * `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. * `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 * `ghr:org/repo/version` to fetch a plug from a Github release
*

View File

@ -18,7 +18,7 @@ and be queried:
<!-- #query item where tags = "core-tag" --> <!-- #query item where tags = "core-tag" -->
|name |tags |page |pos| |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 --> <!-- /query -->
and **tags**: and **tags**:
@ -28,5 +28,5 @@ and **tags**:
And they can be queried this way: And they can be queried this way:
<!-- #query task where tags = "core-tag" render [[template/task]] --> <!-- #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 --> <!-- /query -->

View File

@ -19,7 +19,7 @@ For instance:
$name: "📕 " $name: "📕 "
--- ---
# {{page}} # {{@page.name}}
As recorded on {{today}}. As recorded on {{today}}.
## Introduction ## 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. 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): Currently supported (hardcoded in the code):
- `{{today}}`: Todays date in the usual YYYY-MM-DD format - `{{today}}`: Todays date in the usual YYYY-MM-DD format
@ -73,4 +73,11 @@ Currently supported (hardcoded in the code):
- `{{yesterday}}`: Yesterdays date in the usual YYY-MM-DD format - `{{yesterday}}`: Yesterdays date in the usual YYY-MM-DD format
- `{{lastWeek}}`: Current date - 7 days - `{{lastWeek}}`: Current date - 7 days
- `{{nextWeek}}`: 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}}`

View File

@ -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. **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 --> <!-- #query page limit 3 -->
|name |lastModified |contentType |size |perm|tags| |name |lastModified |contentType |size |perm|tags|
|-----------|-------------|-------------|-----|--|----| |--------------|-------------|-------------|-----|--|----|
|CHANGELOG |1684497544505|text/markdown|23605|rw|tags| |Authentication|1686682290943|text/markdown|1730 |rw| |
|Cloud Links|1676121406519|text/markdown|1177 |rw| | |BROKEN LINKS |1688066558009|text/markdown|196 |rw| |
|Frontmatter|1676121406519|text/markdown|1090 |rw| | |CHANGELOG |1687348511871|text/markdown|27899|rw|tags|
<!-- /query --> <!-- /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. **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 --> <!-- #query page where type = "plug" order by lastModified desc limit 5 -->
|name |lastModified |contentType |size|perm|type|uri |repo |author | |name |lastModified |contentType |size|perm|type|uri |repo |author |share-support|
|--|--|--|--|--|--|--|--|--| |--|--|--|--|--|--|--|--|--|--|
|🔌 Git |1676639116714|text/markdown|943 |rw|plug|github:silverbulletmd/silverbullet-git/git.plug.json |https://github.com/silverbulletmd/silverbullet-git |Zef Hemel | |🔌 KaTeX |1687099068396|text/markdown|1342|rw|plug|github:silverbulletmd/silverbullet-katex/katex.plug.js |https://github.com/silverbulletmd/silverbullet-katex |Zef Hemel | |
|🔌 Share |1676121406530|text/markdown|711 |rw|plug| |https://github.com/silverbulletmd/silverbullet | | |🔌 Core |1687094809367|text/markdown|402 |rw|plug| |https://github.com/silverbulletmd/silverbullet | | |
|🔌 Tasks |1676121406530|text/markdown|1229|rw|plug| |https://github.com/silverbulletmd/silverbullet | | |🔌 Collab |1686682290959|text/markdown|2969|rw|plug| |https://github.com/silverbulletmd/silverbullet | |true|
|🔌 Twitter |1676121406530|text/markdown|1269|rw|plug|github:silverbulletmd/silverbullet-twitter/twitter.plug.json|https://github.com/silverbulletmd/silverbullet-twitter|SilverBullet Authors| |🔌 Twitter|1685105433212|text/markdown|1266|rw|plug|github:silverbulletmd/silverbullet-twitter/twitter.plug.js|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 | |🔌 Mermaid|1685105423879|text/markdown|1096|rw|plug|github:silverbulletmd/silverbullet-mermaid/mermaid.plug.js|https://github.com/silverbulletmd/silverbullet-mermaid|Zef Hemel | |
<!-- /query --> <!-- /query -->
#### 6.3 Query to select only certain fields #### 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 **Result:** Okay, this is much better. However, I believe this needs a touch
from a visual perspective. from a visual perspective.
<!-- #query page select name author repo uririrririrririrririrririrririrririri where type = "plug" order by lastModified desc limit 5 --> <!-- #query page select name, author, repo where type = "plug" order by lastModified desc limit 5 -->
|name |author |repo |ririrririrririrririrririri| |name |author |repo |
|--|--|--|--| |--|--|--|
|🔌 Git |Zef Hemel |https://github.com/silverbulletmd/silverbullet-git || |🔌 KaTeX |Zef Hemel |https://github.com/silverbulletmd/silverbullet-katex |
|🔌 Share | |https://github.com/silverbulletmd/silverbullet || |🔌 Core | |https://github.com/silverbulletmd/silverbullet |
|🔌 Tasks | |https://github.com/silverbulletmd/silverbullet || |🔌 Collab | |https://github.com/silverbulletmd/silverbullet |
|🔌 Twitter |SilverBullet Authors|https://github.com/silverbulletmd/silverbullet-twitter|| |🔌 Twitter|SilverBullet Authors|https://github.com/silverbulletmd/silverbullet-twitter|
|🔌 Graph View|Bertjan Broeksema |https://github.com/bbroeksema/silverbullet-graphview || |🔌 Mermaid|Zef Hemel |https://github.com/silverbulletmd/silverbullet-mermaid|
<!-- /query --> <!-- /query -->
#### 6.4 Display the data in a format defined by a template #### 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? 🚀 **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]] --> <!-- #query page where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
* [[🔌 Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git)) * [[🔌 KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex))
* [[🔌 Share]] * [[🔌 Core]]
* [[🔌 Tasks]] * [[🔌 Collab]]
* [[🔌 Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter)) * [[🔌 Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter))
* [[🔌 Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview)) * [[🔌 Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid))
<!-- /query --> <!-- /query -->
PS: You don't need to select only certain fields to use templates. Templates are PS: You don't need to select only certain fields to use templates. Templates are

View File

@ -17,27 +17,27 @@ Plugs are distributed as self-contained JavaScript bundles (ending with `.plug.j
## Core plugs ## Core plugs
These plugs are distributed with SilverBullet and are automatically enabled: These plugs are distributed with SilverBullet and are automatically enabled:
<!-- #query page where type = "plug" and uri = null order by name render [[template/plug]] --> <!-- #query page where type = "plug" and uri = null order by name render [[template/plug]] -->
* [[🔌 Collab]] * [[🔌 Collab]]
* [[🔌 Core]] * [[🔌 Core]]
* [[🔌 Directive]] * [[🔌 Directive]]
* [[🔌 Emoji]] * [[🔌 Emoji]]
* [[🔌 Markdown]] * [[🔌 Markdown]]
* [[🔌 Share]] * [[🔌 Share]]
* [[🔌 Tasks]] * [[🔌 Tasks]]
<!-- /query --> <!-- /query -->
## Third-party plugs ## Third-party plugs
These plugs are written either by third parties or distributed separately from the main SB distribution: 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]] --> <!-- #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)) * [[🔌 Backlinks]] by **Guillermo Vayá** ([repo](https://github.com/silverbulletmd/silverbullet-backlinks))
* [[🔌 Ghost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-ghost)) * [[🔌 Ghost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-ghost))
* [[🔌 Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git)) * [[🔌 Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-git))
* [[🔌 Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github)) * [[🔌 Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github))
* [[🔌 Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview)) * [[🔌 Graph View]] by **Bertjan Broeksema** ([repo](https://github.com/bbroeksema/silverbullet-graphview))
* [[🔌 KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex)) * [[🔌 KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex))
* [[🔌 Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost)) * [[🔌 Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
* [[🔌 Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid)) * [[🔌 Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid))
* [[🔌 Serendipity]] by **Pantelis Vratsalis** ([repo](https://github.com/m1lt0n/silverbullet-serendipity)) * [[🔌 Serendipity]] by **Pantelis Vratsalis** ([repo](https://github.com/m1lt0n/silverbullet-serendipity))
* [[🔌 Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter)) * [[🔌 Twitter]] by **SilverBullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet-twitter))
<!-- /query --> <!-- /query -->
@ -93,4 +93,3 @@ Once youre happy with your plug, you can distribute it in various ways:
`- github:yourgithubuser/yourrepo/yourplugname.plug.js` to their `PLUGS` file `- 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` - 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`. - 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`.