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