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