1
0

Large "query" plug refactor into "directive"

This commit is contained in:
Zef Hemel 2022-10-28 16:17:40 +02:00
parent 366564f2ec
commit 540af411a0
47 changed files with 1000 additions and 870 deletions

View File

@ -3,5 +3,6 @@
"typescriptHero.imports.stringQuoteStyle": "\"",
"deno.enable": true,
"deno.importMap": "import_map.json",
"deno.config": "deno.jsonc"
"deno.config": "deno.jsonc",
"deno.unstable": true
}

View File

@ -159,6 +159,7 @@ export class DiskSpacePrimitives implements SpacePrimitives {
})
) {
const fullPath = file.path;
try {
const s = await Deno.stat(fullPath);
allFiles.push({
name: fullPath.substring(this.rootPath.length + 1),
@ -167,6 +168,13 @@ export class DiskSpacePrimitives implements SpacePrimitives {
size: s.size,
perm: "rw",
});
} catch (e: any) {
if (e instanceof Deno.errors.NotFound) {
// Ignore, temporariy file already deleted by the time we got here
} else {
console.error("Failed to stat", fullPath, e);
}
}
}
return allFiles;

View File

@ -10,7 +10,8 @@
"watch-plugs": "./build_plugs.sh -w",
"bundle": "deno bundle --importmap import_map.json silverbullet.ts dist/silverbullet.js",
// Regenerates some bundle files (checked into the repo)
"generate": "deno run -A plugos/gen.ts"
// Install lezer-generator with "npm install -g @lezer/generator"
"generate": "deno run -A plugos/gen.ts && lezer-generator plugs/directive/query.grammar -o plugs/directive/parse-query.js"
},
"compilerOptions": {

View File

@ -3,7 +3,7 @@ import {
collectNodesMatching,
ParseTree,
renderToText,
} from "./tree.ts";
} from "$sb/lib/tree.ts";
export const queryRegex =
/(<!--\s*#query\s+(.+?)-->)(.+?)(<!--\s*\/query\s*-->)/gs;

View File

@ -72,8 +72,11 @@ export async function bundle(
// Functions
for (const def of Object.values(manifest.functions || {})) {
if (!def.path) {
continue;
}
let jsFunctionName = "default",
filePath = path.join(rootPath, def.path!);
filePath = path.join(rootPath, def.path);
if (filePath.indexOf(":") !== -1) {
[filePath, jsFunctionName] = filePath.split(":");
}

View File

@ -50,7 +50,7 @@ export function createSandbox(plug: Plug<any>) {
permissions: {
// Allow network access and servers (main use case: fetch)
net: true,
// This is required for console loggin to work, apparently?
// This is required for console logging to work, apparently?
env: true,
// No talking to native code
ffi: false,

View File

@ -52,12 +52,28 @@ export class Plug<HookT> {
return !funDef.env || funDef.env === this.runtimeEnv;
}
async invoke(name: string, args: Array<any>): Promise<any> {
if (!this.sandbox.isLoaded(name)) {
async invoke(name: string, args: any[]): Promise<any> {
const funDef = this.manifest!.functions[name];
if (!funDef) {
throw new Error(`Function ${name} not found in manifest`);
}
if (funDef.redirect) {
// Function redirect, look up
// deno-lint-ignore no-this-alias
let plug: Plug<HookT> | undefined = this;
if (funDef.redirect.indexOf(".") !== -1) {
const [plugName, functionName] = funDef.redirect.split(".");
plug = this.system.loadedPlugs.get(plugName);
if (!plug) {
throw Error(`Plug ${plugName} redirected to not found`);
}
name = functionName;
} else {
name = funDef.redirect;
}
return plug.invoke(name, args);
}
if (!this.sandbox.isLoaded(name)) {
if (!this.canInvoke(name)) {
throw new Error(
`Function ${name} is not available in ${this.runtimeEnv}`,

View File

@ -40,6 +40,12 @@ Deno.test("Run a deno sandbox", async () => {
};
})()`,
},
redirectTest: {
redirect: "addTen",
},
redirectTest2: {
redirect: "test.addTen",
},
addNumbersSyscall: {
code: `(() => {
return {
@ -90,6 +96,8 @@ Deno.test("Run a deno sandbox", async () => {
createSandbox,
);
assertEquals(await plug.invoke("addTen", [10]), 20);
assertEquals(await plug.invoke("redirectTest", [10]), 20);
assertEquals(await plug.invoke("redirectTest2", [10]), 20);
for (let i = 0; i < 100; i++) {
assertEquals(await plug.invoke("addNumbersSyscall", [10, i]), 10 + i);
}

View File

@ -16,7 +16,12 @@ export interface Manifest<HookT> {
}
export type FunctionDef<HookT> = {
// Read the function from this path and inline it
// Format: filename:functionName
path?: string;
// Reuse an
// Format: plugName.functionName
redirect?: string;
code?: string;
env?: RuntimeEnvironment;
} & HookT;

View File

@ -169,8 +169,10 @@ functions:
operation: getFileMeta
# Template commands
insertPageMeta:
insertTemplateText:
path: "./template.ts:insertTemplateText"
insertFrontMatter:
redirect: insertTemplateText
slashCommand:
name: front-matter
description: Insert page front matter
@ -179,49 +181,13 @@ functions:
|^|
---
insertTask:
path: "./template.ts:insertTemplateText"
redirect: insertTemplateText
slashCommand:
name: task
description: Insert a task
value: "* [ ] |^|"
insertQuery:
path: "./template.ts:insertTemplateText"
slashCommand:
name: query
description: Insert a query
value: |
<!-- #query |^| -->
<!-- /query -->
insertInclude:
path: "./template.ts:insertTemplateText"
slashCommand:
name: include
description: Include another page
value: |
<!-- #include [[|^|]] -->
<!-- /include -->
insertInjectTemplate:
path: "./template.ts:insertTemplateText"
slashCommand:
name: use
description: Use a template
value: |
<!-- #use [[|^|]] {} -->
<!-- /use -->
insertInjectCleanTemplate:
path: "./template.ts:insertTemplateText"
slashCommand:
name: use-verbose
description: Use a template (verbose mode)
value: |
<!-- #use-verbose [[|^|]] {} -->
<!-- /use-verbose -->
insertHRTemplate:
path: "./template.ts:insertTemplateText"
redirect: insertTemplateText
slashCommand:
name: hr
description: Insert a horizontal rule

View File

@ -21,7 +21,7 @@ import {
replaceNodesMatching,
} from "$sb/lib/tree.ts";
import { applyQuery } from "$sb/lib/query.ts";
import { extractMeta } from "../query/data.ts";
import { extractMeta } from "../directive/data.ts";
// Key space:
// pl:toPage:pos => pageName

View File

@ -1,5 +1,5 @@
import { editor, markdown, space } from "$sb/silverbullet-syscall/mod.ts";
import { extractMeta } from "../query/data.ts";
import { extractMeta } from "../directive/data.ts";
import { renderToText } from "$sb/lib/tree.ts";
import { niceDate } from "$sb/lib/dates.ts";
import { readSettings } from "$sb/lib/settings_page.ts";

View File

@ -0,0 +1,41 @@
import { editor, space } from "$sb/silverbullet-syscall/mod.ts";
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts";
import { renderDirectives } from "./directives.ts";
export async function updateDirectivesOnPageCommand() {
const currentPage = await editor.getCurrentPage();
await editor.save();
if (
await invokeFunction(
"server",
"updateDirectivesOnPage",
currentPage,
)
) {
await editor.reloadPage();
}
}
// Called from client, running on server
export async function updateDirectivesOnPage(
pageName: string,
): Promise<boolean> {
let text = "";
try {
text = await space.readPage(pageName);
} catch {
console.warn(
"Could not read page",
pageName,
"perhaps it doesn't yet exist",
);
return false;
}
const newText = await renderDirectives(pageName, text);
if (text !== newText) {
await space.writePage(pageName, newText);
return true;
}
return false;
}

View File

@ -13,7 +13,6 @@ import {
} from "$sb/lib/tree.ts";
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
import * as YAML from "yaml";
import { text } from "https://esm.sh/v96/@fortawesome/fontawesome-svg-core@1.3.0/X-ZS9AZm9ydGF3ZXNvbWUvZm9udGF3ZXNvbWUtY29tbW9uLXR5cGVz/index.d.ts";
export async function indexData({ name, tree }: IndexTreeEvent) {
const dataObjects: { key: string; value: any }[] = [];

View File

@ -0,0 +1,72 @@
name: directive
imports:
- https://get.silverbullet.md/global.plug.json
functions:
updateDirectivesOnPage:
path: ./command.ts:updateDirectivesOnPage
updateDirectivesOnPageCommand:
path: ./command.ts:updateDirectivesOnPageCommand
command:
name: "Directives: Update"
key: "Alt-q"
events:
- editor:pageLoaded
indexData:
path: ./data.ts:indexData
events:
- page:index
dataQueryProvider:
path: ./data.ts:queryProvider
events:
- query:data
queryComplete:
path: ./complete.ts:queryComplete
events:
- page:complete
# Templates
insertQuery:
redirect: core.insertTemplateText
slashCommand:
name: query
description: Insert a query
value: |
<!-- #query |^| -->
<!-- /query -->
insertInclude:
redirect: core.insertTemplateText
slashCommand:
name: include
description: Include another page
value: |
<!-- #include [[|^|]] -->
<!-- /include -->
insertUseTemplate:
redirect: core.insertTemplateText
slashCommand:
name: use
description: Use a template
value: |
<!-- #use [[|^|]] {} -->
<!-- /use -->
insertUseVerboseTemplate:
redirect: core.insertTemplateText
slashCommand:
name: use-verbose
description: Use a template (verbose mode)
value: |
<!-- #use-verbose [[|^|]] {} -->
<!-- /use-verbose -->
insertEvalTemplate:
redirect: core.insertTemplateText
slashCommand:
name: eval
description: Evaluate a JavaScript expression
value: |
<!-- #eval |^| -->
<!-- /eval -->

View File

@ -0,0 +1,70 @@
import { nodeAtPos, ParseTree, renderToText } from "$sb/lib/tree.ts";
import { replaceAsync } from "$sb/lib/util.ts";
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
import { extractMeta } from "./data.ts";
import { evalDirectiveRenderer } from "./eval_directive.ts";
import { queryDirectiveRenderer } from "./query_directive.ts";
import {
cleanTemplateInstantiations,
templateDirectiveRenderer,
} from "./template_directive.ts";
export const directiveRegex =
/(<!--\s*#(use|use-verbose|include|eval|query)\s+(.*?)-->)(.+?)(<!--\s*\/\2\s*-->)/gs;
/**
* Looks for directives in the text dispatches them based on name
*/
export async function directiveDispatcher(
pageName: string,
text: string,
tree: ParseTree,
directiveRenderers: Record<
string,
(directive: string, pageName: string, arg: string) => Promise<string>
>,
): Promise<string> {
return replaceAsync(
text,
directiveRegex,
async (fullMatch, startInst, type, arg, _body, endInst, index) => {
const currentNode = nodeAtPos(tree, index + 1);
// console.log("Node type", currentNode?.type);
if (currentNode?.type !== "CommentBlock") {
// If not a comment block, it's likely a code block, ignore
// console.log("Not comment block, ingoring", fullMatch);
return fullMatch;
}
arg = arg.trim();
try {
const newBody = await directiveRenderers[type](type, pageName, arg);
return `${startInst}\n${newBody.trim()}\n${endInst}`;
} catch (e: any) {
return `${startInst}\n**ERROR:** ${e.message}\n${endInst}`;
}
},
);
}
export async function renderDirectives(
pageName: string,
text: string,
): Promise<string> {
const tree = await markdown.parseMarkdown(text);
const metaData = extractMeta(tree, ["$disableDirectives"]);
// console.log("Meta data", pageName, metaData);
if (metaData.$disableDirectives) {
return text;
}
text = renderToText(tree);
text = await directiveDispatcher(pageName, text, tree, {
use: templateDirectiveRenderer,
"use-verbose": templateDirectiveRenderer,
"include": templateDirectiveRenderer,
query: queryDirectiveRenderer,
eval: evalDirectiveRenderer,
});
return await cleanTemplateInstantiations(text);
}

View File

@ -0,0 +1,60 @@
// This is some shocking stuff. My profession would kill me for this.
import { YAML } from "../../common/deps.ts";
import { jsonToMDTable, renderTemplate } from "./util.ts";
// Enables plugName.functionName(arg1, arg2) syntax in JS expressions
function translateJs(js: string): string {
return js.replaceAll(
/(\w+\.\w+)\s*\(/g,
'await invokeFunction("$1", ',
);
}
// Syntaxes to support:
// - random JS expression
// - random JS expression render [[some/template]]
const expressionRegex = /(.+?)(\s+render\s+\[\[([^\]]+)\]\])?$/;
// This is rather scary and fragile stuff, but it works.
export async function evalDirectiveRenderer(
_directive: string,
_pageName: string,
expression: string,
): Promise<string> {
console.log("Got JS expression", expression);
const match = expressionRegex.exec(expression);
if (!match) {
throw new Error(`Invalid eval directive: ${expression}`);
}
let template = "";
if (match[3]) {
// This is the template reference
expression = match[1];
template = match[3];
}
try {
// Why the weird "eval" call? https://esbuild.github.io/content-types/#direct-eval
const result = await (0, eval)(
`(async () => {
function invokeFunction(name, ...args) {
return syscall("system.invokeFunction", "server", name, ...args);
}
return ${translateJs(expression)};
})()`,
);
if (template) {
return await renderTemplate(template, result);
}
if (typeof result === "string") {
return result;
} else if (typeof result === "number") {
return "" + result;
} else if (Array.isArray(result)) {
return jsonToMDTable(result);
}
return YAML.stringify(result);
} catch (e: any) {
return `**ERROR:** ${e.message}`;
}
}

View File

@ -0,0 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
export const parser = LRParser.deserialize({
version: 14,
states: "&fOVQPOOOmQQO'#C^QOQPOOOtQPO'#C`OyQQO'#CkO!OQPO'#CmO!TQPO'#CnO!YQPO'#CoOOQO'#Cq'#CqO!bQQO,58xO!iQQO'#CcO#WQQO'#CaOOQO'#Ca'#CaOOQO,58z,58zO#oQPO,59VOOQO,59X,59XO#tQQO'#DaOOQO,59Y,59YOOQO,59Z,59ZOOQO-E6o-E6oO$]QQO,58}OtQPO,58|O$tQQO1G.qO%`QPO'#CsO%eQQO,59{OOQO'#Cg'#CgOOQO'#Ci'#CiO$]QQO'#CjOOQO'#Cd'#CdOOQO1G.i1G.iOOQO1G.h1G.hOOQO'#Cl'#ClOOQO7+$]7+$]OOQO,59_,59_OOQO-E6q-E6qO%|QPO'#C}O&UQPO,59UO$]QQO'#CrO&ZQPO,59iOOQO1G.p1G.pOOQO,59^,59^OOQO-E6p-E6p",
stateData: "&c~OjOS~ORPO~OkRO}SO!RTO!SUO!UVO~OhQX~P[ORYO~O!O^O~OX_O~OR`O~OYbOdbO~OhQa~P[OldOtdOudOvdOwdOxdOydOzdO{dO~O|eOhTXkTX}TX!RTX!STX!UTX~ORfO~OrgOh!TXk!TX}!TX!R!TX!S!TX!U!TX~OXlOYlO[lOmiOniOojOpkO~O!PoO!QoOh_ik_i}_i!R_i!S_i!U_i~ORqO~OrgOh!Tak!Ta}!Ta!R!Ta!S!Ta!U!Ta~OruOsqX~OswO~OruOsqa~O",
goto: "#e!UPP!VP!Y!^!a!d!jPP!sP!s!s!Y!x!Y!Y!YP!{#R#XPPPPPPPPP#_PPPPPPPPPPPPPPPPP#bRQOTWPXR]RR[RQZRRneQmdQskRxuVldkuRpfQXPRcXQvsRyvQh`RrhRtkRaU",
nodeNames: "⚠ Program Query Name WhereClause LogicalExpr AndExpr FilterExpr Value Number String Bool Regex Null List OrderClause Order LimitClause SelectClause RenderClause PageRef",
maxTerm: 52,
skippedNodes: [0],
repeatNodeCount: 3,
tokenData: "B[~R}X^$Opq$Oqr$srs%W|}%r}!O%w!P!Q&Y!Q!['P!^!_'X!_!`'f!`!a's!c!}%w!}#O(Q#P#Q(q#R#S%w#T#U(v#U#V+]#V#W%w#W#X,X#X#Y%w#Y#Z.T#Z#]%w#]#^0e#^#`%w#`#a1a#a#b%w#b#c3t#c#d5p#d#f%w#f#g8T#g#h;P#h#i={#i#k%w#k#l?w#l#o%w#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$Ip$Iq%W$Iq$Ir%W$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$TYj~X^$Opq$O#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$vP!_!`$y~%OPv~#r#s%R~%WOz~~%ZUOr%Wrs%ms$Ip%W$Ip$Iq%m$Iq$Ir%m$Ir~%W~%rOY~~%wOr~P%|SRP}!O%w!c!}%w#R#S%w#T#o%w~&_V[~OY&YZ]&Y^!P&Y!P!Q&t!Q#O&Y#O#P&y#P~&Y~&yO[~~&|PO~&Y~'UPX~!Q!['P~'^Pl~!_!`'a~'fOt~~'kPu~#r#s'n~'sOy~~'xPx~!_!`'{~(QOw~R(VPpQ!}#O(YP(]RO#P(Y#P#Q(f#Q~(YP(iP#P#Q(lP(qOdP~(vOs~R({WRP}!O%w!c!}%w#R#S%w#T#b%w#b#c)e#c#g%w#g#h*a#h#o%wR)jURP}!O%w!c!}%w#R#S%w#T#W%w#W#X)|#X#o%wR*TS|QRP}!O%w!c!}%w#R#S%w#T#o%wR*fURP}!O%w!c!}%w#R#S%w#T#V%w#V#W*x#W#o%wR+PS!QQRP}!O%w!c!}%w#R#S%w#T#o%wR+bURP}!O%w!c!}%w#R#S%w#T#m%w#m#n+t#n#o%wR+{S!OQRP}!O%w!c!}%w#R#S%w#T#o%wR,^URP}!O%w!c!}%w#R#S%w#T#X%w#X#Y,p#Y#o%wR,uURP}!O%w!c!}%w#R#S%w#T#g%w#g#h-X#h#o%wR-^URP}!O%w!c!}%w#R#S%w#T#V%w#V#W-p#W#o%wR-wS!PQRP}!O%w!c!}%w#R#S%w#T#o%wR.YTRP}!O%w!c!}%w#R#S%w#T#U.i#U#o%wR.nURP}!O%w!c!}%w#R#S%w#T#`%w#`#a/Q#a#o%wR/VURP}!O%w!c!}%w#R#S%w#T#g%w#g#h/i#h#o%wR/nURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y0Q#Y#o%wR0XSnQRP}!O%w!c!}%w#R#S%w#T#o%wR0jURP}!O%w!c!}%w#R#S%w#T#b%w#b#c0|#c#o%wR1TS{QRP}!O%w!c!}%w#R#S%w#T#o%wR1fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^1x#^#o%wR1}URP}!O%w!c!}%w#R#S%w#T#a%w#a#b2a#b#o%wR2fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^2x#^#o%wR2}URP}!O%w!c!}%w#R#S%w#T#h%w#h#i3a#i#o%wR3hS!RQRP}!O%w!c!}%w#R#S%w#T#o%wR3yURP}!O%w!c!}%w#R#S%w#T#i%w#i#j4]#j#o%wR4bURP}!O%w!c!}%w#R#S%w#T#`%w#`#a4t#a#o%wR4yURP}!O%w!c!}%w#R#S%w#T#`%w#`#a5]#a#o%wR5dSoQRP}!O%w!c!}%w#R#S%w#T#o%wR5uURP}!O%w!c!}%w#R#S%w#T#f%w#f#g6X#g#o%wR6^URP}!O%w!c!}%w#R#S%w#T#W%w#W#X6p#X#o%wR6uURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y7X#Y#o%wR7^URP}!O%w!c!}%w#R#S%w#T#f%w#f#g7p#g#o%wR7wS}QRP}!O%w!c!}%w#R#S%w#T#o%wR8YURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y8l#Y#o%wR8qURP}!O%w!c!}%w#R#S%w#T#b%w#b#c9T#c#o%wR9YURP}!O%w!c!}%w#R#S%w#T#W%w#W#X9l#X#o%wR9qURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y:T#Y#o%wR:YURP}!O%w!c!}%w#R#S%w#T#f%w#f#g:l#g#o%wR:sS!UQRP}!O%w!c!}%w#R#S%w#T#o%wR;UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y;h#Y#o%wR;mURP}!O%w!c!}%w#R#S%w#T#`%w#`#a<P#a#o%wR<UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y<h#Y#o%wR<mURP}!O%w!c!}%w#R#S%w#T#V%w#V#W=P#W#o%wR=UURP}!O%w!c!}%w#R#S%w#T#h%w#h#i=h#i#o%wR=oS!SQRP}!O%w!c!}%w#R#S%w#T#o%wR>QURP}!O%w!c!}%w#R#S%w#T#f%w#f#g>d#g#o%wR>iURP}!O%w!c!}%w#R#S%w#T#i%w#i#j>{#j#o%wR?QURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y?d#Y#o%wR?kSmQRP}!O%w!c!}%w#R#S%w#T#o%wR?|URP}!O%w!c!}%w#R#S%w#T#[%w#[#]@`#]#o%wR@eURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y@w#Y#o%wR@|URP}!O%w!c!}%w#R#S%w#T#f%w#f#gA`#g#o%wRAeURP}!O%w!c!}%w#R#S%w#T#X%w#X#YAw#Y#o%wRBOSkQRP}!O%w!c!}%w#R#S%w#T#o%w",
tokenizers: [0, 1],
topRules: {"Program":[0,1]},
tokenPrec: 0
})

View File

@ -1,5 +1,6 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const Program = 1,
export const
Program = 1,
Query = 2,
Name = 3,
WhereClause = 4,
@ -18,4 +19,4 @@ export const Program = 1,
LimitClause = 17,
SelectClause = 18,
RenderClause = 19,
PageRef = 20;
PageRef = 20

View File

@ -1,10 +1,10 @@
import {
collectNodesOfType,
findNodeOfType,
ParseTree,
replaceNodesMatching,
} from "$sb/lib/tree.ts";
import { lezerToParseTree } from "../../common/parse_tree.ts";
import { valueNodeToVal } from "./engine.ts";
// @ts-ignore auto generated
import { parser } from "./parse-query.js";
@ -75,3 +75,33 @@ export function parseQuery(query: string): ParsedQuery {
return parsedQuery;
}
export function valueNodeToVal(valNode: ParseTree): any {
switch (valNode.type) {
case "Number":
return +valNode.children![0].text!;
case "Bool":
return valNode.children![0].text! === "true";
case "Null":
return null;
case "Name":
return valNode.children![0].text!;
case "Regex": {
const val = valNode.children![0].text!;
return val.substring(1, val.length - 1);
}
case "String": {
const stringVal = valNode.children![0].text!;
return stringVal.substring(1, stringVal.length - 1);
}
case "PageRef": {
const pageRefVal = valNode.children![0].text!;
return pageRefVal.substring(2, pageRefVal.length - 2);
}
case "List": {
return collectNodesOfType(valNode, "Value").map((t) =>
valueNodeToVal(t.children![0])
);
}
}
}

View File

@ -0,0 +1,34 @@
import { events } from "$sb/plugos-syscall/mod.ts";
import { replaceTemplateVars } from "../core/template.ts";
import { renderTemplate } from "./util.ts";
import { parseQuery } from "./parser.ts";
import { jsonToMDTable } from "./util.ts";
export async function queryDirectiveRenderer(
_directive: string,
pageName: string,
query: string,
): Promise<string> {
const parsedQuery = parseQuery(replaceTemplateVars(query, pageName));
console.log("Parsed query", parsedQuery);
// Let's dispatch an event and see what happens
const results = await events.dispatchEvent(
`query:${parsedQuery.table}`,
{ query: parsedQuery, pageName: pageName },
10 * 1000,
);
if (results.length === 0) {
return "";
} else if (results.length === 1) {
if (parsedQuery.render) {
const rendered = await renderTemplate(parsedQuery.render, results[0]);
return rendered.trim();
} else {
return jsonToMDTable(results[0]);
}
} else {
throw new Error(`Too many query results: ${results.length}`);
}
}

View File

@ -0,0 +1,89 @@
import { queryRegex } from "$sb/lib/query.ts";
import { renderToText } from "$sb/lib/tree.ts";
import { replaceAsync } from "$sb/lib/util.ts";
import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
import Handlebars from "handlebars";
import { replaceTemplateVars } from "../core/template.ts";
import { extractMeta } from "./data.ts";
import { directiveRegex, renderDirectives } from "./directives.ts";
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
export async function templateDirectiveRenderer(
directive: string,
pageName: string,
arg: string,
): Promise<string> {
const match = arg.match(templateRegex);
if (!match) {
throw new Error(`Invalid template directive: ${arg}`);
}
const template = match[1];
const args = match[2];
let parsedArgs = {};
if (args) {
try {
parsedArgs = JSON.parse(args);
} catch {
throw new Error(`Failed to parse template instantiation args: ${arg}`);
}
}
let templateText = "";
if (template.startsWith("http://") || template.startsWith("https://")) {
try {
const req = await fetch(template);
templateText = await req.text();
} catch (e: any) {
templateText = `ERROR: ${e.message}`;
}
} else {
templateText = await space.readPage(template);
}
let newBody = templateText;
// if it's a template injection (not a literal "include")
if (directive === "use" || directive === "use-verbose") {
const tree = await markdown.parseMarkdown(templateText);
extractMeta(tree, ["$disableDirectives"]);
templateText = renderToText(tree);
const templateFn = Handlebars.compile(
replaceTemplateVars(templateText, pageName),
{ noEscape: true },
);
newBody = templateFn(parsedArgs);
// Recursively render directives
newBody = await renderDirectives(pageName, newBody);
}
return newBody.trim();
}
export function cleanTemplateInstantiations(text: string): Promise<string> {
return replaceAsync(
text,
directiveRegex,
(
_fullMatch,
startInst,
type,
_args,
body,
endInst,
): Promise<string> => {
if (type === "use") {
body = body.replaceAll(
queryRegex,
(
_fullMatch: string,
_startQuery: string,
_query: string,
body: string,
) => {
return body.trim();
},
);
}
return Promise.resolve(`${startInst}${body}${endInst}`);
},
);
}

View File

@ -1,3 +1,9 @@
import Handlebars from "handlebars";
import * as YAML from "yaml";
import { space } from "$sb/silverbullet-syscall/mod.ts";
import { niceDate } from "$sb/lib/dates.ts";
const maxWidth = 70;
// Nicely format an array of JSON objects as a Markdown table
export function jsonToMDTable(
@ -64,3 +70,42 @@ export function jsonToMDTable(
return new Array(length + 1).join(ch);
}
}
export async function renderTemplate(
renderTemplate: string,
data: any[],
): Promise<string> {
Handlebars.registerHelper("json", (v: any) => JSON.stringify(v));
Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts)));
Handlebars.registerHelper("prefixLines", (v: string, prefix: string) =>
v
.split("\n")
.map((l) => prefix + l)
.join("\n"));
Handlebars.registerHelper(
"substring",
(s: string, from: number, to: number, elipsis = "") =>
s.length > to - from ? s.substring(from, to) + elipsis : s,
);
Handlebars.registerHelper("yaml", (v: any, prefix: string) => {
if (typeof prefix === "string") {
let yaml = YAML.stringify(v)
.split("\n")
.join("\n" + prefix)
.trim();
if (Array.isArray(v)) {
return "\n" + prefix + yaml;
} else {
return yaml;
}
} else {
return YAML.stringify(v).trim();
}
});
let templateText = await space.readPage(renderTemplate);
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
const template = Handlebars.compile(templateText, { noEscape: true });
return template(data);
}

View File

@ -1,80 +0,0 @@
import { collectNodesOfType, ParseTree } from "$sb/lib/tree.ts";
import Handlebars from "handlebars";
import * as YAML from "yaml";
import { space } from "$sb/silverbullet-syscall/mod.ts";
import { niceDate } from "$sb/lib/dates.ts";
import { ParsedQuery } from "$sb/lib/query.ts";
export function valueNodeToVal(valNode: ParseTree): any {
switch (valNode.type) {
case "Number":
return +valNode.children![0].text!;
case "Bool":
return valNode.children![0].text! === "true";
case "Null":
return null;
case "Name":
return valNode.children![0].text!;
case "Regex": {
const val = valNode.children![0].text!;
return val.substring(1, val.length - 1);
}
case "String": {
const stringVal = valNode.children![0].text!;
return stringVal.substring(1, stringVal.length - 1);
}
case "PageRef": {
const pageRefVal = valNode.children![0].text!;
return pageRefVal.substring(2, pageRefVal.length - 2);
}
case "List": {
return collectNodesOfType(valNode, "Value").map((t) =>
valueNodeToVal(t.children![0])
);
}
}
}
export async function renderQuery(
parsedQuery: ParsedQuery,
data: any[],
): Promise<string> {
if (parsedQuery.render) {
Handlebars.registerHelper("json", (v: any) => JSON.stringify(v));
Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts)));
Handlebars.registerHelper("prefixLines", (v: string, prefix: string) =>
v
.split("\n")
.map((l) => prefix + l)
.join("\n"));
Handlebars.registerHelper(
"substring",
(s: string, from: number, to: number, elipsis = "") =>
s.length > to - from ? s.substring(from, to) + elipsis : s,
);
Handlebars.registerHelper("yaml", (v: any, prefix: string) => {
if (typeof prefix === "string") {
let yaml = 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(parsedQuery.render);
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
const template = Handlebars.compile(templateText, { noEscape: true });
return template(data);
}
return "ERROR";
}

View File

@ -1,177 +0,0 @@
import { editor } from "$sb/silverbullet-syscall/mod.ts";
import Handlebars from "handlebars";
import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts";
import { renderQuery } from "./engine.ts";
import { parseQuery } from "./parser.ts";
import { replaceTemplateVars } from "../core/template.ts";
import { jsonToMDTable } from "./util.ts";
import { queryRegex } from "$sb/lib/query.ts";
import { events } from "$sb/plugos-syscall/mod.ts";
import { replaceAsync } from "$sb/lib/util.ts";
import { nodeAtPos, renderToText } from "$sb/lib/tree.ts";
import { extractMeta } from "./data.ts";
export async function updateMaterializedQueriesCommand() {
const currentPage = await editor.getCurrentPage();
await editor.save();
if (
await invokeFunction(
"server",
"updateMaterializedQueriesOnPage",
currentPage,
)
) {
await editor.reloadPage();
}
}
export const templateInstRegex =
/(<!--\s*#(use|use-verbose|include)\s+\[\[([^\]]+)\]\](.*?)-->)(.+?)(<!--\s*\/\2\s*-->)/gs;
function updateTemplateInstantiations(
text: string,
pageName: string,
): Promise<string> {
return replaceAsync(
text,
templateInstRegex,
async (fullMatch, startInst, type, template, args, _body, endInst) => {
args = args.trim();
let parsedArgs = {};
if (args) {
try {
parsedArgs = JSON.parse(args);
} catch {
console.error("Failed to parse template instantiation args", args);
return fullMatch;
}
}
let templateText = "";
if (template.startsWith("http://") || template.startsWith("https://")) {
try {
const req = await fetch(template);
templateText = await req.text();
} catch (e: any) {
templateText = `ERROR: ${e.message}`;
}
} else {
templateText = await space.readPage(template);
}
let newBody = templateText;
// if it's a template injection (not a literal "include")
if (type === "use" || type === "use-verbose") {
const tree = await markdown.parseMarkdown(templateText);
extractMeta(tree, ["$disableDirectives"]);
templateText = renderToText(tree);
const templateFn = Handlebars.compile(
replaceTemplateVars(templateText, pageName),
{ noEscape: true },
);
newBody = templateFn(parsedArgs);
}
return `${startInst}\n${newBody.trim()}\n${endInst}`;
},
);
}
function cleanTemplateInstantiations(text: string): Promise<string> {
return replaceAsync(
text,
templateInstRegex,
(
_fullMatch,
startInst,
type,
_template,
_args,
body,
endInst,
): Promise<string> => {
if (type === "use") {
body = body.replaceAll(
queryRegex,
(
_fullMatch: string,
_startQuery: string,
_query: string,
body: string,
) => {
return body.trim();
},
);
}
return Promise.resolve(`${startInst}${body}${endInst}`);
},
);
}
// Called from client, running on server
export async function updateMaterializedQueriesOnPage(
pageName: string,
): Promise<boolean> {
// console.log("Updating queries");
let text = "";
try {
text = await space.readPage(pageName);
} catch {
console.warn(
"Could not read page",
pageName,
"perhaps it doesn't yet exist",
);
return false;
}
let newText = await updateTemplateInstantiations(text, pageName);
const tree = await markdown.parseMarkdown(newText);
const metaData = extractMeta(tree, ["$disableDirectives"]);
// console.log("Meta data", pageName, metaData);
if (metaData.$disableDirectives) {
console.log("Directives disabled, skipping");
return false;
}
newText = renderToText(tree);
newText = await replaceAsync(
newText,
queryRegex,
async (fullMatch, startQuery, query, _body, endQuery, index) => {
const currentNode = nodeAtPos(tree, index + 1);
if (currentNode?.type !== "CommentBlock") {
// If not a comment block, it's likely a code block, ignore
return fullMatch;
}
const parsedQuery = parseQuery(replaceTemplateVars(query, pageName));
// console.log("Parsed query", parsedQuery);
// Let's dispatch an event and see what happens
const results = await events.dispatchEvent(
`query:${parsedQuery.table}`,
{ query: parsedQuery, pageName: pageName },
10 * 1000,
);
if (results.length === 0) {
return `${startQuery}\n${endQuery}`;
} else if (results.length === 1) {
if (parsedQuery.render) {
const rendered = await renderQuery(parsedQuery, results[0]);
return `${startQuery}\n${rendered.trim()}\n${endQuery}`;
} else {
return `${startQuery}\n${jsonToMDTable(results[0])}\n${endQuery}`;
}
} else {
console.error("Too many query results", results);
return fullMatch;
}
},
);
newText = await cleanTemplateInstantiations(newText);
if (text !== newText) {
await space.writePage(pageName, newText);
return true;
}
return false;
}

View File

@ -1,21 +0,0 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import { LRParser } from "@lezer/lr";
export const parser = LRParser.deserialize({
version: 14,
states:
"&fOVQPOOOmQQO'#C^QOQPOOOtQPO'#C`OyQQO'#CkO!OQPO'#CmO!TQPO'#CnO!YQPO'#CoOOQO'#Cq'#CqO!bQQO,58xO!iQQO'#CcO#WQQO'#CaOOQO'#Ca'#CaOOQO,58z,58zO#oQPO,59VOOQO,59X,59XO#tQQO'#DaOOQO,59Y,59YOOQO,59Z,59ZOOQO-E6o-E6oO$]QQO,58}OtQPO,58|O$tQQO1G.qO%`QPO'#CsO%eQQO,59{OOQO'#Cg'#CgOOQO'#Ci'#CiO$]QQO'#CjOOQO'#Cd'#CdOOQO1G.i1G.iOOQO1G.h1G.hOOQO'#Cl'#ClOOQO7+$]7+$]OOQO,59_,59_OOQO-E6q-E6qO%|QPO'#C}O&UQPO,59UO$]QQO'#CrO&ZQPO,59iOOQO1G.p1G.pOOQO,59^,59^OOQO-E6p-E6p",
stateData:
"&c~OjOS~ORPO~OkRO}SO!RTO!SUO!UVO~OhQX~P[ORYO~O!O^O~OX_O~OR`O~OYbOdbO~OhQa~P[OldOtdOudOvdOwdOxdOydOzdO{dO~O|eOhTXkTX}TX!RTX!STX!UTX~ORfO~OrgOh!TXk!TX}!TX!R!TX!S!TX!U!TX~OXlOYlO[lOmiOniOojOpkO~O!PoO!QoOh_ik_i}_i!R_i!S_i!U_i~ORqO~OrgOh!Tak!Ta}!Ta!R!Ta!S!Ta!U!Ta~OruOsqX~OswO~OruOsqa~O",
goto:
"#e!UPP!VP!Y!^!a!d!jPP!sP!s!s!Y!x!Y!Y!YP!{#R#XPPPPPPPPP#_PPPPPPPPPPPPPPPPP#bRQOTWPXR]RR[RQZRRneQmdQskRxuVldkuRpfQXPRcXQvsRyvQh`RrhRtkRaU",
nodeNames:
"⚠ Program Query Name WhereClause LogicalExpr AndExpr FilterExpr Value Number String Bool Regex Null List OrderClause Order LimitClause SelectClause RenderClause PageRef",
maxTerm: 52,
skippedNodes: [0],
repeatNodeCount: 3,
tokenData:
"B[~R}X^$Opq$Oqr$srs%W|}%r}!O%w!P!Q&Y!Q!['P!^!_'X!_!`'f!`!a's!c!}%w!}#O(Q#P#Q(q#R#S%w#T#U(v#U#V+]#V#W%w#W#X,X#X#Y%w#Y#Z.T#Z#]%w#]#^0e#^#`%w#`#a1a#a#b%w#b#c3t#c#d5p#d#f%w#f#g8T#g#h;P#h#i={#i#k%w#k#l?w#l#o%w#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$Ip$Iq%W$Iq$Ir%W$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$TYj~X^$Opq$O#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$vP!_!`$y~%OPv~#r#s%R~%WOz~~%ZUOr%Wrs%ms$Ip%W$Ip$Iq%m$Iq$Ir%m$Ir~%W~%rOY~~%wOr~P%|SRP}!O%w!c!}%w#R#S%w#T#o%w~&_V[~OY&YZ]&Y^!P&Y!P!Q&t!Q#O&Y#O#P&y#P~&Y~&yO[~~&|PO~&Y~'UPX~!Q!['P~'^Pl~!_!`'a~'fOt~~'kPu~#r#s'n~'sOy~~'xPx~!_!`'{~(QOw~R(VPpQ!}#O(YP(]RO#P(Y#P#Q(f#Q~(YP(iP#P#Q(lP(qOdP~(vOs~R({WRP}!O%w!c!}%w#R#S%w#T#b%w#b#c)e#c#g%w#g#h*a#h#o%wR)jURP}!O%w!c!}%w#R#S%w#T#W%w#W#X)|#X#o%wR*TS|QRP}!O%w!c!}%w#R#S%w#T#o%wR*fURP}!O%w!c!}%w#R#S%w#T#V%w#V#W*x#W#o%wR+PS!QQRP}!O%w!c!}%w#R#S%w#T#o%wR+bURP}!O%w!c!}%w#R#S%w#T#m%w#m#n+t#n#o%wR+{S!OQRP}!O%w!c!}%w#R#S%w#T#o%wR,^URP}!O%w!c!}%w#R#S%w#T#X%w#X#Y,p#Y#o%wR,uURP}!O%w!c!}%w#R#S%w#T#g%w#g#h-X#h#o%wR-^URP}!O%w!c!}%w#R#S%w#T#V%w#V#W-p#W#o%wR-wS!PQRP}!O%w!c!}%w#R#S%w#T#o%wR.YTRP}!O%w!c!}%w#R#S%w#T#U.i#U#o%wR.nURP}!O%w!c!}%w#R#S%w#T#`%w#`#a/Q#a#o%wR/VURP}!O%w!c!}%w#R#S%w#T#g%w#g#h/i#h#o%wR/nURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y0Q#Y#o%wR0XSnQRP}!O%w!c!}%w#R#S%w#T#o%wR0jURP}!O%w!c!}%w#R#S%w#T#b%w#b#c0|#c#o%wR1TS{QRP}!O%w!c!}%w#R#S%w#T#o%wR1fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^1x#^#o%wR1}URP}!O%w!c!}%w#R#S%w#T#a%w#a#b2a#b#o%wR2fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^2x#^#o%wR2}URP}!O%w!c!}%w#R#S%w#T#h%w#h#i3a#i#o%wR3hS!RQRP}!O%w!c!}%w#R#S%w#T#o%wR3yURP}!O%w!c!}%w#R#S%w#T#i%w#i#j4]#j#o%wR4bURP}!O%w!c!}%w#R#S%w#T#`%w#`#a4t#a#o%wR4yURP}!O%w!c!}%w#R#S%w#T#`%w#`#a5]#a#o%wR5dSoQRP}!O%w!c!}%w#R#S%w#T#o%wR5uURP}!O%w!c!}%w#R#S%w#T#f%w#f#g6X#g#o%wR6^URP}!O%w!c!}%w#R#S%w#T#W%w#W#X6p#X#o%wR6uURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y7X#Y#o%wR7^URP}!O%w!c!}%w#R#S%w#T#f%w#f#g7p#g#o%wR7wS}QRP}!O%w!c!}%w#R#S%w#T#o%wR8YURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y8l#Y#o%wR8qURP}!O%w!c!}%w#R#S%w#T#b%w#b#c9T#c#o%wR9YURP}!O%w!c!}%w#R#S%w#T#W%w#W#X9l#X#o%wR9qURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y:T#Y#o%wR:YURP}!O%w!c!}%w#R#S%w#T#f%w#f#g:l#g#o%wR:sS!UQRP}!O%w!c!}%w#R#S%w#T#o%wR;UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y;h#Y#o%wR;mURP}!O%w!c!}%w#R#S%w#T#`%w#`#a<P#a#o%wR<UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y<h#Y#o%wR<mURP}!O%w!c!}%w#R#S%w#T#V%w#V#W=P#W#o%wR=UURP}!O%w!c!}%w#R#S%w#T#h%w#h#i=h#i#o%wR=oS!SQRP}!O%w!c!}%w#R#S%w#T#o%wR>QURP}!O%w!c!}%w#R#S%w#T#f%w#f#g>d#g#o%wR>iURP}!O%w!c!}%w#R#S%w#T#i%w#i#j>{#j#o%wR?QURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y?d#Y#o%wR?kSmQRP}!O%w!c!}%w#R#S%w#T#o%wR?|URP}!O%w!c!}%w#R#S%w#T#[%w#[#]@`#]#o%wR@eURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y@w#Y#o%wR@|URP}!O%w!c!}%w#R#S%w#T#f%w#f#gA`#g#o%wRAeURP}!O%w!c!}%w#R#S%w#T#X%w#X#YAw#Y#o%wRBOSkQRP}!O%w!c!}%w#R#S%w#T#o%w",
tokenizers: [0, 1],
topRules: { "Program": [0, 1] },
tokenPrec: 0,
});

View File

@ -1,25 +0,0 @@
name: query
imports:
- https://get.silverbullet.md/global.plug.json
functions:
updateMaterializedQueriesOnPage:
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage
updateMaterializedQueriesCommand:
path: ./materialized_queries.ts:updateMaterializedQueriesCommand
command:
name: "Materialized Queries: Update"
key: "Alt-q"
events:
- editor:pageLoaded
indexData:
path: ./data.ts:indexData
events:
- page:index
dataQueryProvider:
path: ./data.ts:queryProvider
events:
- query:data
queryComplete:
path: ./complete.ts:queryComplete
events:
- page:complete

View File

@ -125,7 +125,7 @@ export class HttpServer {
eventSyscalls(this.eventHook),
markdownSyscalls(buildMarkdown([])),
esbuildSyscalls([this.globalModules]),
systemSyscalls(this),
systemSyscalls(this, this.system),
sandboxSyscalls(this.system),
assetSyscalls(this.system),
// jwtSyscalls(),

View File

@ -1,18 +1,33 @@
import { SysCallMapping } from "../../plugos/system.ts";
import { Plug } from "../../plugos/plug.ts";
import { SysCallMapping, System } from "../../plugos/system.ts";
import type { HttpServer } from "../http_server.ts";
export function systemSyscalls(httpServer: HttpServer): SysCallMapping {
export function systemSyscalls(
httpServer: HttpServer,
system: System<any>,
): SysCallMapping {
return {
"system.invokeFunction": (
ctx,
env: string,
// Ignored in this context, always assuming server (this place)
_env: string,
name: string,
...args: any[]
) => {
if (!ctx.plug) {
throw Error("No plug associated with context");
}
return ctx.plug.invoke(name, args);
let plug: Plug<any> | undefined = ctx.plug;
if (name.indexOf(".") !== -1) {
// plug name in the name
const [plugName, functionName] = name.split(".");
plug = system.loadedPlugs.get(plugName);
if (!plug) {
throw Error(`Plug ${plugName} not found`);
}
name = functionName;
}
return plug.invoke(name, args);
},
"system.reloadPlugs": () => {
return httpServer.reloadPlugs();

View File

@ -2,8 +2,6 @@ import {
preactRender,
useEffect,
useReducer,
Y,
yCollab,
yUndoManagerKeymap,
} from "./deps.ts";
@ -160,7 +158,7 @@ export class Editor {
spaceSyscalls(this),
indexerSyscalls(this.space),
fulltextSyscalls(this.space),
systemSyscalls(this),
systemSyscalls(this, this.system),
markdownSyscalls(buildMarkdown(this.mdExtensions)),
clientStoreSyscalls(),
storeSyscalls(this.space),

View File

@ -1,8 +1,12 @@
import { SysCallMapping } from "../../plugos/system.ts";
import type { Plug } from "../../plugos/plug.ts";
import { SysCallMapping, System } from "../../plugos/system.ts";
import type { Editor } from "../editor.tsx";
import { CommandDef } from "../hooks/command.ts";
export function systemSyscalls(editor: Editor): SysCallMapping {
export function systemSyscalls(
editor: Editor,
system: System<any>,
): SysCallMapping {
return {
"system.invokeFunction": (
ctx,
@ -14,11 +18,21 @@ export function systemSyscalls(editor: Editor): SysCallMapping {
throw Error("No plug associated with context");
}
let plug: Plug<any> | undefined = ctx.plug;
if (name.indexOf(".") !== -1) {
// plug name in the name
const [plugName, functionName] = name.split(".");
plug = system.loadedPlugs.get(plugName);
if (!plug) {
throw Error(`Plug ${plugName} not found`);
}
name = functionName;
}
if (env === "client") {
return ctx.plug.invoke(name, args);
return plug.invoke(name, args);
}
return editor.space.invokeFunction(ctx.plug, env, name, args);
return editor.space.invokeFunction(plug, env, name, args);
},
"system.invokeCommand": (ctx, name: string) => {
return editor.runCommandByName(name);

View File

@ -5,122 +5,121 @@ release.
## 0.1.4
- Breaking change (for those who used it): the named anchor syntax has changed
from `@anchorname` to `$anchorname`. This is to avoid conflicts with
potentialy future use of `@` for other purposes (like mentioning people).
Linking to an anchor still uses the `[[page@anchorname]]` syntax. So, you
create an anchor $likethis you can then reference it [[@likethis]].
* Breaking change (for those who used it): the named anchor syntax has changed from `@anchorname` to `$anchorname`. This is to avoid conflicts with potentialy future use of `@` for other purposes (like mentioning people). Linking to an anchor still uses the `[[page@anchorname]]` syntax. So, you create an anchor $likethis you can then reference it [[@likethis]].
* The `query` plug has been renamed to `directive` (because it supports many other features now) and significantly refactored. New docs: [[🔌 Directive]]
* 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.
---
## 0.1.3
- Silver Bullet now runs on Windows!
- Frontmatter support! You can now use front matter in your markdown, to do this
* Silver Bullet now runs on Windows!
* Frontmatter support! You can now use front matter in your markdown, to do this
start your page with `---` and end it with `---`. This will now be the
preferred way to define page meta data (although the old way will still work).
The old `/meta` slash command has now been replaced with `/front-matter`.
- Tags are now indexed as page meta without the prefixing `#` character, the
* Tags are now indexed as page meta without the prefixing `#` character, the
reason is to make this compatible with Obsidian. You can now attach tags to
your page either by just using a `#tag` at the top level of your page, or by
adding a `tags` attribute to your front matter.
- {[Search Space]} works again. You may have to {[Space: Reindex]} to get
* {[Search Space]} works again. You may have to {[Space: Reindex]} to get
results. Search results now also snow a snippet of the page, with the phrase
highlighted.
- Faster page indexing.
- `silverbullet` now has sub-commands. It defaults to just running the server
* Faster page indexing.
* `silverbullet` now has sub-commands. It defaults to just running the server
(when passed a path to a directory), but you can also run
`silverbullet --help` to see the available commands. Commands currently
available:
- `silverbullet upgrade` to perform a self upgrade
- `silverbullet fix` to attempt to solve any issues with your space (deletes
* `silverbullet upgrade` to perform a self upgrade
* `silverbullet fix` to attempt to solve any issues with your space (deletes
your `_plug` directory and `data.db` file)
- `silverbullet plug:compile` replaces the old `plugos-bundle` command.
- `silverbullet version` prints the current version
* `silverbullet plug:compile` replaces the old `plugos-bundle` command.
* `silverbullet version` prints the current version
---
## 0.1.2
- Breaking plugs API change: `readPage`, `readAttachment`, `readFile` now return
* Breaking plugs API change: `readPage`, `readAttachment`, `readFile` now return
the read data object directly, without it being wrapped with a text object.
- A whole bunch of deprecated syscalls have been removed
* A whole bunch of deprecated syscalls have been removed
---
## 0.1.0 First Deno release
- The entire repo has been migrated to [Deno](https://deno.land)
- This may temporarily break some things.
- If somehow youre experiencing trouble, try the following:
- Delete all files under `_plug` in your pages folder, e.g. with
* The entire repo has been migrated to [Deno](https://deno.land)
* This may temporarily break some things.
* If somehow youre experiencing trouble, try the following:
* Delete all files under `_plug` in your pages folder, e.g. with
`rm -rf pages/_plug`.
- Delete your `data.db`
- Changes:
- `PLUGS` is now longer required
- `PLUGS` no longer supports `builtin:` plug URLs, all builtins are
* Delete your `data.db`
* Changes:
* `PLUGS` is now longer required
* `PLUGS` no longer supports `builtin:` plug URLs, all builtins are
automatically loaded and no longer should be listed.
- Plugs no longer should be built with node and npm, PRs will be issued to all
* Plugs no longer should be built with node and npm, PRs will be issued to all
existing plugs later to help with this transition.
- Know breakages:
- Full text search is not yet implemented (the SQLite used does not support it
* Know breakages:
* Full text search is not yet implemented (the SQLite used does not support it
right now)
- Github auth has not been ported (yet)
- Technical changes:
- Server runs on Deno (and Oak instead of Express)
- Client is now built with ESBuild
- React has been replaced with Preact
- Package management in Deno works based on http imports, so npm is no longer
* Github auth has not been ported (yet)
* Technical changes:
* Server runs on Deno (and Oak instead of Express)
* Client is now built with ESBuild
* React has been replaced with Preact
* Package management in Deno works based on http imports, so npm is no longer
used.
---
## 0.0.35
- Big refactor of the internal Space API unifying attachment and page handling.
* Big refactor of the internal Space API unifying attachment and page handling.
This shouldn't affect (most) existing code and plugs (except some more exotic
areas), but if stuff breaks, please report it.
- Technical change: Upgrades are now detected on the server-side, and plugs
* Technical change: Upgrades are now detected on the server-side, and plugs
re-loaded and pages indexed upon every upgrade.
- Various bug fixes (e.g. using HTML tags in a page before completely broke
* Various bug fixes (e.g. using HTML tags in a page before completely broke
syntax highlighting)
- Exposed `fulltext.*` syscalls on the client
* Exposed `fulltext.*` syscalls on the client
---
## 0.0.34
- Change to attachment handling: the `attachment/` prefix for links and images
* Change to attachment handling: the `attachment/` prefix for links and images
is no longer used, if you already had links to attachments in your notes, you
will need to remove the `attachment/` prefix manually. Sorry about that.
- Improved styling for completion (especially slash commands)
- Completion for commands using the (undocumented) `{[Command Syntax]}` yep,
* Improved styling for completion (especially slash commands)
* Completion for commands using the (undocumented) `{[Command Syntax]}` yep,
that exists.
---
## 0.0.33
- **Attachments**: you can now copy & paste, or drag & drop files (images, PDF,
* **Attachments**: you can now copy & paste, or drag & drop files (images, PDF,
whatever you like) into a page and it will be uploaded and appropriately
linked from your page. Attachment size is currently limited to 100mb.
- Changed full-text search page prefix from `@search/` to `🔍` for the {[Search
* Changed full-text search page prefix from `@search/` to `🔍` for the {[Search
Space]} command.
- `page`, `plug` and `attachment` are now _reserved page names_, you cannot name
* `page`, `plug` and `attachment` are now _reserved page names_, you cannot name
your pages these (you will get an error when explicitly navigating to them).
---
## 0.0.32
- **Inline image previews**: use the standard
* **Inline image previews**: use the standard
`![alt text](https://url.com/image.jpg)` notation and a preview of the image
will appear automatically. Example:
![Inline image preview](https://user-images.githubusercontent.com/812886/186218876-6d8a4a71-af8b-4e9e-83eb-4ac89607a6b4.png)
- **Dark mode**. Toggle between the dark and light mode using a new button,
* **Dark mode**. Toggle between the dark and light mode using a new button,
top-right.
![Dark mode screenshot](https://user-images.githubusercontent.com/6335792/187000151-ba06ce55-ad27-494b-bfe9-6b19ef62145b.png)
- **Named anchors** and references, create an anchor with the new @anchor
* **Named anchors** and references, create an anchor with the new @anchor
notation (anywhere on a page), then reference it locally via [[@anchor]] or
cross page via [[CHANGELOG@anchor]].
@ -128,73 +127,73 @@ release.
## 0.0.31
- Update to the query language: the `render` clause now uses page reference
* Update to the query language: the `render` clause now uses page reference
syntax `[[page]]`. For example `render [[template/task]]` rather than
`render "template/task"`. The old syntax still works, but is deprecated,
completion for the old syntax has been removed.
- Updates to templates:
- For the `Template: Instantiate Page` command, the page meta value `$name` is
* Updates to templates:
* For the `Template: Instantiate Page` command, the page meta value `$name` is
now used to configure the page name (was `name` before). Also if `$name` is
the only page meta defined, it will remove the page meta entirely when
instantiating.
- You can now configure a daily note prefix with `dailyNotePrefix` in
* You can now configure a daily note prefix with `dailyNotePrefix` in
`SETTINGS` and create a template for your daily note under
`template/page/Daily Note` (configurable via the `dailyNoteTemplate`
setting).
- You can now set a quick note prefix with `quickNotePrefix` in `SETTINGS`.
- Directives (e.g. `#query`, `#import`, `#use`) changes:
- Renamed `#template` directive to `#use-verbose`
- New `#use` directive will clean all the embedded queries and templates in
* You can now set a quick note prefix with `quickNotePrefix` in `SETTINGS`.
* Directives (e.g. `#query`, `#import`, `#use`) changes:
* Renamed `#template` directive to `#use-verbose`
* New `#use` directive will clean all the embedded queries and templates in
its scope
- All directives now use the page reference syntax `[[page name]]` instead of
* All directives now use the page reference syntax `[[page name]]` instead of
`"page name"`, this includes `#use` and `#use-verbose` as well as `#import`.
- The `link` query provider now also returns the `pos` of a link (in addition
* The `link` query provider now also returns the `pos` of a link (in addition
to the `page`)
- New `$disableDirectives` page metadata attribute can be used to disable
* New `$disableDirectives` page metadata attribute can be used to disable
directives processing in a page (useful for templates)
- Added a new `/hr` slash command to insert a horizontal rule (`---`) useful for
* Added a new `/hr` slash command to insert a horizontal rule (`---`) useful for
mobile devices (where these are harder to type)
---
## 0.0.30
- Slash commands now only trigger after a non-word character to avoid "false
* Slash commands now only trigger after a non-word character to avoid "false
positives" like "hello/world".
- Page auto complete now works with slashes in the name.
- Having a `SETTINGS` page is now mandatory. One is auto generated if none is
* Page auto complete now works with slashes in the name.
* Having a `SETTINGS` page is now mandatory. One is auto generated if none is
present.
- Added an `indexPage` setting to set the index page for the space (which by
* Added an `indexPage` setting to set the index page for the space (which by
default is `index`). When navigating to this page, the page name will
"disappear" from the URL. That is, the index URL will simply be
`http://localhost:3000/`.
- This feature is now used in `website` and set to `Silver Bullet` there. To
* This feature is now used in `website` and set to `Silver Bullet` there. To
also make the title look nicer when visiting https://silverbullet.md
---
## 0.0.29
- Added the `Link: Unfurl` command, which is scoped to only work (and be
* Added the `Link: Unfurl` command, which is scoped to only work (and be
visible) when used on “naked URLs”, that is: URLs not embedded in a link or
other place, such as this one: https://silverbullet.md
- Plugs can implement their own unfurlers by responding to the
* Plugs can implement their own unfurlers by responding to the
`unfurl:options` event (see the
[Twitter plug](https://github.com/silverbulletmd/silverbullet-twitter) for
an example).
- Core implements only one unfurl option: “Extract title” which pulls the
* Core implements only one unfurl option: “Extract title” which pulls the
`<title>` tag from the linked URL and replaces it with a `[bla](URL)` style
link.
- Removed status bar: to further simplify the SB UI. You can still pull up the
* Removed status bar: to further simplify the SB UI. You can still pull up the
same stat on demand with the `Stats: Show` command.
- The page switcher is now maintaining its ordering based on, in order:
* The page switcher is now maintaining its ordering based on, in order:
1. Last opened pages (in current session)
2. Last modified date (on disk)
3. Everything else
4. The currently open page (at the bottom)
- Filter boxes (used for the page switching and command palette among other
* Filter boxes (used for the page switching and command palette among other
things) now also support PgUp, PgDown, Home and End and have some visual
glitches fixed as well.
- Reverted exposing an empty `window` object to sandboxes running in workers and
* Reverted exposing an empty `window` object to sandboxes running in workers and
node.js (introduced in 0.0.28)
- Renamed Markdown-preview related commands to something more consistentnt
* Renamed Markdown-preview related commands to something more consistentnt

View File

@ -1,63 +1,37 @@
## Markdown as a platform
Silver Bullet (SB) is highly-extensible,
[open source](https://github.com/silverbulletmd/silverbullet) **personal
knowledge management** software. Indeed, thats fancy language for “a note
taking app with links.”
knowledge management** software. Indeed, thats fancy language for “a note taking app with links.”
Here is a screenshot:
![Silver Bullet PWA screenshot](silverbullet-pwa.png)
At its core, SB is a Markdown editor that stores _pages_ (notes) as plain
markdown files in a folder referred to as a _space_. Pages can be cross-linked
using the `[[link to other page]]` syntax. However, once you leverage its
various extensions (called _plugs_) it can feel more like a _knowledge
platform_, allowing you to annotate, combine and query your accumulated
knowledge in creative ways, specific to you. To get a good feel for it,
[watch this video](https://youtu.be/RYdc3UF9gok).
Or [try it in a sandbox demo environment](https://demo.silverbullet.md/Sandbox).
markdown files in a folder referred to as a _space_. Pages can be cross-linked using the `[[link to other page]]` syntax. However, once you leverage its various extensions (called _plugs_) it can feel more like a _knowledge platform_, allowing you to annotate, combine and query your accumulated knowledge in creative ways, specific to you. To get a good feel for it, [watch this video](https://youtu.be/RYdc3UF9gok).
## Extensions
What type of extensions, you ask? Let us demonstrate this in a very meta way: by
querying a list of plugs and injecting it into this page!
What type of extensions, you ask? Let us demonstrate this in a very meta way: by querying a list of plugs and injecting it into this page!
Heres a list of (non-built-in) plugs documented in this space (note the
`#query` ... `/query` notation used):
<!-- #query page where type = "plug" order by name render [[template/plug]] -->
- [[🔌 Backlinks]] by **Guillermo Vayá**
([repo](https://github.com/Willyfrog/silverbullet-backlinks))
- [[🔌 Core]] by **Silver Bullet Authors**
([repo](https://github.com/silverbulletmd/silverbullet))
- [[🔌 Ghost]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-ghost))
- [[🔌 Git]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-github))
- [[🔌 Github]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-github))
- [[🔌 Mattermost]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
- [[🔌 Mount]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-mount))
- [[🔌 Query]] by **Silver Bullet Authors**
([repo](https://github.com/silverbulletmd/silverbullet))
* [[🔌 Backlinks]] by **Guillermo Vayá** ([repo](https://github.com/Willyfrog/silverbullet-backlinks))
* [[🔌 Core]] by **Silver Bullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet))
* [[🔌 Directive]] by **Silver Bullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet))
* [[🔌 Ghost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-ghost))
* [[🔌 Git]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github))
* [[🔌 Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github))
* [[🔌 Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
* [[🔌 Mount]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mount))
<!-- /query -->
In a regular SB installation, the body of this query 👆 (in between the
placeholders) would automatically be kept up to date as new pages are added to
the space that match the query. 🤯 Have a look at the [[template/plug]]
_template_ (referenced in the `render` clause) to see how the results are
rendered using handlebars syntax and have a look at one of the linked pages to
see how the _metadata_ is specified, which is subsequently used to query and
render in this page. And to learn about the specific plug, of course.
placeholders) would automatically be kept up to date as new pages are added to the space that match the query. 🤯 Have a look at the [[template/plug]] _template_ (referenced in the `render` clause) to see how the results are rendered using handlebars syntax and have a look at one of the linked pages to see how the _metadata_ is specified, which is subsequently used to query and render in this page. And to learn about the specific plug, of course.
## Explore more
Click on the links below to explore various aspects of Silver Bullet more
in-depth:
@ -69,28 +43,17 @@ in-depth:
More of a video person? Here are two to get you started:
- [A Tour of Silver Bullets features](https://youtu.be/RYdc3UF9gok) — spoiler
alert: its cool.
- [A look the SilverBullet architecture](https://youtu.be/mXCGau05p5o) — spoiler
alert: its plugs all the way down.
- [A Tour of Silver Bullets features](https://youtu.be/RYdc3UF9gok) — spoiler alert: its cool.
- [A look the SilverBullet architecture](https://youtu.be/mXCGau05p5o) — spoiler alert: its plugs all the way down.
## Principles
Some core principles that underly Silver Bullets philosophy:
- **Free and open source**. Silver Bullet is MIT licensed.
- **The truth is in the markdown.** Markdown is simply text files, stored on
disk. Nothing fancy. No proprietary formats or lock in. While SB uses a
database for indexing and caching some data, all of that can be rebuilt from
its markdown source at any time. If SB would ever go away, you can still read
your pages with any text editor.
- **Single, distraction-free mode.** SB doesnt have a separate view and edit
mode. It doesnt have a “focus mode.” Youre always in focused edit mode, why
wouldnt you?
- **Keyboard oriented**. You can use SB fully using the keyboard, typin the
keys.
- **Extend it your way**. SB is highly extensible with [[🔌 Plugs]], and you can
customize it to your liking and your workflows.
- **The truth is in the markdown.** Markdown is simply text files, stored on disk. Nothing fancy. No proprietary formats or lock in. While SB uses a database for indexing and caching some data, all of that can be rebuilt from its markdown source at any time. If SB would ever go away, you can still read your pages with any text editor.
- **Single, distraction-free mode.** SB doesnt have a separate view and edit mode. It doesnt have a “focus mode.” Youre always in focused edit mode, why wouldnt you?
- **Keyboard oriented**. You can use SB fully using the keyboard, typin the keys.
- **Extend it your way**. SB is highly extensible with [[🔌 Plugs]], and you can customize it to your liking and your workflows.
## Installing Silver Bullet
@ -120,32 +83,22 @@ With Deno installed (see instruction above), run:
deno install -f --name silverbullet -A --unstable https://get.silverbullet.md
```
This will install `silverbullet` into your `~/.deno/bin` folder (which should
already be in your path if you installed Deno following the previous
instructions).
This will install `silverbullet` into your `~/.deno/bin` folder (which should already be in your path if you installed Deno following the previous instructions).
To run Silver Bullet create a folder for your pages (it can be empty, or be an
existing folder with `.md` files) and run the following command in your
terminal:
To run Silver Bullet create a folder for your pages (it can be empty, or be an existing folder with `.md` files) and run the following command in your terminal:
```shell
silverbullet <pages-path>
```
By default, SB will bind to port `3000`, to use a different port use the
`--port` flag. By default SB doesnt offer any sort of authentication, to add
basic password authentication, pass the `--password` flag.
`--port` flag. By default SB doesnt offer any sort of authentication, to add basic password authentication, pass the `--password` flag.
Once downloaded and booted, SB will print out a URL to open SB in your browser
(spoiler alert: by default this will be http://localhost:3000 ).
Once downloaded and booted, SB will print out a URL to open SB in your browser (spoiler alert: by default this will be http://localhost:3000 ).
#protip: If you have a PWA enabled browser (like any browser based on Chromium)
hit that little button right of the location bar to install SB, and give it its
own window frame (sans location bar) and desktop/dock icon. At last the PWA has
found its killer app.
#protip: If you have a PWA enabled browser (like any browser based on Chromium) hit that little button right of the location bar to install SB, and give it its own window frame (sans location bar) and desktop/dock icon. At last the PWA has found its killer app.
## Upgrading Silver Bullet
Simply run this:
silverbullet upgrade
@ -154,11 +107,8 @@ And restart Silver Bullet. You should be good to go.
## Troubleshooting
If you upgraded to the new Deno-based Silver Bullet from an old version, you may
have to use the `silverbullet fix <pages-path>` command to flush out your old
database and plugs. Plugs will likely need to be updated.
If you upgraded to the new Deno-based Silver Bullet from an old version, you may have to use the `silverbullet fix <pages-path>` command to flush out your old database and plugs. Plugs will likely need to be updated.
If you (hypothetically) find bugs or have feature requests, post them in
[our issue tracker](https://github.com/silverbulletmd/silverbullet/issues). Want
to contribute?
[Check out the code](https://github.com/silverbulletmd/silverbullet).
to contribute? [Check out the code](https://github.com/silverbulletmd/silverbullet).

View File

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

View File

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

View File

@ -0,0 +1,7 @@
---
$disableDirectives: true
---
<!-- #query task where tags = "{{.}}" and done = false render [[template/task]] -->
<!-- /query -->

1
website/template/task.md Normal file
View File

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

View File

@ -1,3 +0,0 @@
{{#each .}}
- [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}} {{/each}}

View File

@ -1,10 +1,9 @@
```meta
---
type: plug
uri: builtin:core
repo: https://github.com/silverbulletmd/silverbullet
author: Silver Bullet Authors
$disableDirectives: true
```
---
This documentation is still a WIP.
@ -14,26 +13,22 @@ The core plug implements a few templating mechanisms.
### Page Templates
The {[Template: Instantiate Page]} command enables you to create a new page
based on a page template.
The {[Template: Instantiate Page]} command enables you to create a new page based on a page template.
Page templates, by default, are looked for in the `template/page/` prefix. So
creating e.g. a `template/page/Meeting Notes` page will create a “Meeting Notes”
template. You can override this prefix by setting the `pageTemplatePrefix` in
`SETTINGS`.
Page templates, by default, are looked for in the `template/page/` prefix. So creating e.g. a `template/page/Meeting Notes` page will create a “Meeting Notes” template. You can override this prefix by setting the `pageTemplatePrefix` in `SETTINGS`.
Page templates have one “magic” type of page metadata that is used during
instantiation:
- `$name` is used as the default value for a new page based on this template
* `$name` is used as the default value for a new page based on this template
In addition, any standard template placeholders are available (see below)
For instance:
```meta
---
$name: "📕 "
```
---
# {{page}}
As recorded on {{today}}.
@ -42,8 +37,7 @@ For instance:
## Notes
## Conclusions
Will prompt you to pick a page name (defaulting to “📕 “), and then create the
following page (on 2022-08-08) when you pick “📕 Harry Potter” as a page name:
Will prompt you to pick a page name (defaulting to “📕 “), and then create the following page (on 2022-08-08) when you pick “📕 Harry Potter” as a page name:
# 📕 Harry Potter
As recorded on 2022-08-08.
@ -54,17 +48,11 @@ following page (on 2022-08-08) when you pick “📕 Harry Potter” as a page n
### Snippets
Snippets are similar to page templates, except you insert them into an existing
page with the `/snippet` slash command. The default prefix is `snippet/` which
is configurable via the `snippetPrefix` setting in `SETTINGS`.
Snippets are similar to page templates, except you insert them into an existing page with the `/snippet` slash command. The default prefix is `snippet/` which is configurable via the `snippetPrefix` setting in `SETTINGS`.
Snippet templates do not support the `$name` page meta, because it doesnt
apply.
Snippet templates do not support the `$name` page meta, because it doesnt apply.
However, snippets do support the special `|^|` placeholder for placing the
cursor caret after injecting the snippet. If you leave it out, the cursor will
simply be placed at the end, but if you like to insert the cursor elsewhere,
that position can be set with the `|^|` placeholder.
However, snippets do support the special `|^|` placeholder for placing the cursor caret after injecting the snippet. If you leave it out, the cursor will simply be placed at the end, but if you like to insert the cursor elsewhere, that position can be set with the `|^|` placeholder.
For instance to replicate the `/query` slash command as a snippet:
@ -74,53 +62,14 @@ For instance to replicate the `/query` slash command as a snippet:
Which would insert the cursor right after `#query`.
### Dynamic template injection
In addition to using the `/snippet` slash command to insert a template as a
one-off, its also possible to reuse templates that update dynamically (similar
to [[🔌 Query]]). For this, you use the `#use` and `#use-verbose` directives.
In its most basic form:
<!-- #use [[template/plug]] -->
<!-- /use -->
Upon load (or when updating materialized queries) the body of this dynamic
section will be replaced with the content of the referenced template.
The referenced template will be treated as a Handlebars template (just like when
using a `render` clause with `#query`).
Optionally, you can pass any JSON-formatted value as an argument, which will be
exposed in the template as the top-level value.
For example, given the following template:
Hello there {{name}} you are {{age}} years old!
You can reference and instantiate as follows:
<!-- #use [[template/plug]] {"name": "Pete", "age": 50} -->
<!-- /use -->
If a template contains any dynamic sections with directives, these will all be
removed before injecting the content into the page. This makes things look
cleaner. If you want to preserve them, use `#use-verbose` instead of `#use`.
### Daily Note
The {[Open Daily Note]} command navigates (or creates) a daily note prefixed
with a 📅 emoji by default, but this is configurable via the `dailyNotePrefix`
setting in `SETTINGS`. If you have a page template (see above) named
`Daily Note` it will use this as a template, otherwise, the page will just be
empty.
with a 📅 emoji by default, but this is configurable via the `dailyNotePrefix` setting in `SETTINGS`. If you have a page template (see above) named `Daily Note` it will use this as a template, otherwise, the page will just be empty.
### Quick Note
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

75
website/🔌 Directive.md Normal file
View File

@ -0,0 +1,75 @@
---
type: plug
repo: https://github.com/silverbulletmd/silverbullet
author: Silver Bullet Authors
---
The directive plug is a built-in plug implementing various so-called “directive” that all take the form of `<!-- #directiveName ... -->` and close with `<!-- /directiveName -->`. Currently the following directives are supported:
* `#query` to perform queries: [[🔌 Directive/Query]]
* `#include` to inline the content of another page verbatim: [[@include]]
* `#use` to use the content of another as a [handlebars](https://handlebarsjs.com/) template: [[@use]]
* `#eval` to evaluate an arbitrary JavaScript expression and inline the result: [[@eval]]
## Include
$include
The `#include` directive can be used to embed another page into your existing one. The syntax is as follows:
<!-- #include [[page reference]] -->
<!-- /include -->
Whenever the directives are updated, the body of the directive will be replaced with the latest version of the reference page.
## Use
$use
The `#use` directive can be used to use a referenced page as a handbars template. Optionally, a JSON object can be passed as argument to the template:
<!-- #use [[template/plug]] {"name": "Example plug", "repo": "https://google.com"} -->
<!-- /use -->
which renders as follows:
<!-- #use [[template/plug]] {"name": "Example plug", "repo": "https://google.com"} -->
* [[Example plug]] by **** ([repo](https://google.com))
<!-- /use -->
Note that a string is also a valid JSON value:
* [ ] #test This is a test task
<!-- #use [[template/tagged-tasks]] "#test" -->
* [ ] [[🔌 Directive@1340]] This is a test task #test
<!-- /use -->
which renders as:
<!-- #use [[template/tagged-tasks]] "#test" -->
* [ ] [[🔌 Directive@1492]] This is a test task #test
<!-- /use -->
## Eval
$eval
The `#eval` directive can be used to evaluate arbitrary JavaScript expressions. Its also possible to invoke arbitrary plug functions this way.
A simple example is multiplying numbers:
<!-- #eval 10 * 10 -->
100
<!-- /eval -->
However, you can also invoke arbitrary plug functions, e.g. the `titleUnfurlOptions` function in the `core` plug:
<!-- #eval core.titleUnfurlOptions() -->
|id |name |
|------------|-------------|
|title-unfurl|Extract title|
<!-- /eval -->
Optionally, you can use a `render` clause to render the result as a template, similar to [[🔌 Directive/Query]]:
<!-- #eval core.titleUnfurlOptions() render [[template/debug]] -->
id: title-unfurl
name: Extract title
---
<!-- /eval -->

View File

@ -0,0 +1,229 @@
## Query
The `#query` is the most widely used directive. It can be used to query various data sources and render results in various ways.
### Syntax
1. _start with_: `<!-- #query [QUERY GOES HERE] -->`
2. _end with_: `<!-- /query -->`
3. _write your query_: replace `[QUERY GOES HERE]` with any query you want using the options below.
4. _available query options_: Usage of options is similar to SQL except for thespecial `render` option. The `render` option is used to display the data in a format that you created in a separate template.
* `where`
* `order by`
* `limit`
* `select`
* `render`
P.S.: If you are a developer or have a technical knowledge to read a code and would like to know more about syntax, please check out
[query grammar](https://github.com/silverbulletmd/silverbullet/blob/main/packages/plugs/query/query.grammar).
#### 2.1. Available query operators:
- `=` equals
- `!=` not equals
- `<` less than
- `<=` less than or equals
- `>` greater than
- `>=` greater than or equals
- `=~` to match against a regular expression
- `!=~` does not match this regular expression
- `in` member of a list (e.g. `prop in ["foo", "bar"]`)
Further, you can combine multiple of these with `and`. Example
`prop =~ /something/ and prop != “something”`.
### 3. How to run a query?
After writing the query, there are three options:
1. Open the **command palette** and run {[Directives: Update]}
2. Use shortcut: hit **Alt-q** (Windows, Linux) or **Option-q** (Mac)
3. Go to another page and come back to the page where the query is located, it always updates when a page is loaded
After using one of the options, the “body” of the query is replaced with the new results of the query data will be displayed.
### 4. Data sources
Available data sources can be categorized as:
1. Builtin data sources
2. Data that can be inserted by users
3. Plugs data sources
The best part about data sources: there is auto-completion. 🎉
Start writing `<!— #query` or simply use `/query` slash command, it will show you all available data sources. 🤯
#### 4.1. Available data sources
- `page`: list of all pages
- `task`: list of all tasks created with `[ ]`
- `full-text`: use it with `where phrase = "SOME_TEXT"`. List of all pages where `SOME_TEXT` is mentioned
- `item`: list of ordered and unordered items such as bulleted lists
- `tag`: list of all hashtags used in all pages
- `link`: list of all pages giving a link to the page where query is written
- `data`: You can insert data using the syntax below. You can query the data using the `data` source.
```data
name: John
age: 50
city: Milan
country: Italy
---
name: Jane
age: 53
city: Rome
country: Italy
---
name: Francesco
age: 28
city: Berlin
country: Germany
```
Example:
<!-- #query data where age > 20 and country = "Italy" -->
|name|age|city |country|page |pos|
|----|--|-----|-----|------------|-|
|John|50|Milan|Italy|🔌 Directive|0|
|Jane|53|Rome |Italy|🔌 Directive|1|
<!-- /query -->
#### 4.2 Plugs data sources
Certain plugs can also provide special data sources to query specific data. Some examples are:
- [[🔌 Github]] provides `gh-pull` to query PRs for selected repo
- [[🔌 Mattermost]] provides `mm-saved` to fetch (by default 15) saved posts in
Mattermost
For a complete list of data sources, please check plugs own pages.
### 5. Templates
Templates are predefined formats to render the body of the query.
#### 5.1 How to create a template?
It is pretty easy. You just need to create a new page. However, it is
recommended to create your templates using `template/[TEMPLATE_NAME]`
convention. For this guide, we will create `template/plug` to display list of Plugs available in Silver Bullet. We will use this template in the Examples section below.
#### 5.2 What is the syntax?
We are using Handlebars which is a simple templating language. It is using double curly braces and the name of the parameter to be injected. For our `template/plug`, we are using simple template like below.
* [[{{name}}]] by **{{author}}** ([repo]({{repo}}))
Let me break it down for you
- `*` is creating a bullet point for each item in Silver Bullet
- `[[{{name}}]]` is injecting the name of Plug and creating an internal link to
the page of the Plug
- `**{{author}}**` is injecting the author of the Plug and making it bold
- `([repo]({{repo}}))` is injecting the name of the Plug and creating an
external link to the GitHub page of the Plug
For more information on the Handlebars syntax, you can read the
[official documentation](https://handlebarsjs.com/).
#### 5.3 How to use the template?
You just need to add the `render` keyword followed by the link of the template to the query like below:
`#query page where type = "plug" render [[template/plug]]`
You can see the usage of our template in example 6.4 below.
### 6. Examples
We will walk you through a set of examples starting from a very basic one
through one formatting the data using templates.
Our goal in this exercise is to (i) get all plug pages (ii) ordered by last
modified time and (iii) display in a nice format.
For the sake of simplicity, we will use the `page` data source and limit the
results not to spoil the page.
#### 6.1 Simple query without any condition
**Goal:** We would like to get the list of all pages.
**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 10 -->
|name |lastModified |perm|tags |type|uri |repo |author |
|--|--|--|--|--|--|--|--|
|SETTINGS |1665558946821|rw|undefined|undefined|undefined |undefined |undefined |
|Silver Bullet |1666964349821|rw|protip|undefined|undefined |undefined |undefined |
|Sandbox |1665558946826|rw|undefined|undefined|undefined |undefined |undefined |
|🔌 Core |1666963501687|rw|undefined|plug|builtin:core |https://github.com/silverbulletmd/silverbullet |Silver Bullet Authors|
|CHANGELOG |1666959942128|rw|undefined|undefined|undefined |undefined |undefined |
|💡 Inspiration|1665558946820|rw|undefined|undefined|undefined |undefined |undefined |
|🔌 Mount |1665567345520|rw|undefined|plug|github:silverbulletmd/silverbullet-mount/mount.plug.json|https://github.com/silverbulletmd/silverbullet-mount|Zef Hemel |
|🤯 Features |1665567345521|rw|undefined|undefined|undefined |undefined |undefined |
|🔌 Ghost |1665558946819|rw|undefined|plug|github:silverbulletmd/silverbullet-ghost/ghost.plug.json|https://github.com/silverbulletmd/silverbullet-ghost|Zef Hemel |
|PUBLISH |1665558946821|rw|undefined|undefined|undefined |undefined |undefined |
<!-- /query -->
#### 6.2 Simple query with a condition
**Goal:** We would like to get all plug pages sorted by last modified time.
**Result:** Okay, this is what we wanted but there is also information such as
perm, type and lastModified that we don't need.
<!-- #query page where type = "plug" order by lastModified desc limit 5 -->
|name |lastModified |perm|size |contentType |type|repo |author |uri |
|--|--|--|--|--|--|--|--|--|
|🔌 Directive |1666965349992|rw|13004|text/markdown|plug|https://github.com/silverbulletmd/silverbullet |Silver Bullet Authors|undefined |
|🔌 Core |1666963501687|rw|undefined|undefined |plug|https://github.com/silverbulletmd/silverbullet |Silver Bullet Authors|builtin:core |
|🔌 Mattermost|1665567345533|rw|undefined|undefined |plug|https://github.com/silverbulletmd/silverbullet-mattermost|Zef Hemel |github:silverbulletmd/silverbullet-mattermost/mattermost.plug.json|
|🔌 Github |1665567345532|rw|undefined|undefined |plug|https://github.com/silverbulletmd/silverbullet-github |Zef Hemel |github:silverbulletmd/silverbullet-github/github.plug.json |
|🔌 Backlinks |1665567345530|rw|undefined|undefined |plug|https://github.com/Willyfrog/silverbullet-backlinks |Guillermo Vayá |ghr:Willyfrog/silverbullet-backlinks |
<!-- /query -->
#### 6.3 Query to select only certain fields
**Goal:** We would like to get all plug pages, selecting only `name`, `author`
and `repo` columns and then sort by last modified time.
**Result:** Okay, this is much better. However, I believe this needs a touch
from a visual perspective.
<!-- #query page select name author repo uri where type = "plug" order by lastModified desc limit 5 -->
|name |author |repo |ri |
|--|--|--|--|
|🔌 Directive |Silver Bullet Authors|https://github.com/silverbulletmd/silverbullet |undefined|
|🔌 Core |Silver Bullet Authors|https://github.com/silverbulletmd/silverbullet |undefined|
|🔌 Mattermost|Zef Hemel |https://github.com/silverbulletmd/silverbullet-mattermost|undefined|
|🔌 Github |Zef Hemel |https://github.com/silverbulletmd/silverbullet-github |undefined|
|🔌 Backlinks |Guillermo Vayá |https://github.com/Willyfrog/silverbullet-backlinks |undefined|
<!-- /query -->
#### 6.4 Display the data in a format defined by a template
**Goal:** We would like to display the data from step 5.3 in a nice format using bullet points with links to Plug pages, with the author name and a link to their GitHub repo.
**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 uri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
* [[🔌 Directive]] by **Silver Bullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet))
* [[🔌 Core]] by **Silver Bullet Authors** ([repo](https://github.com/silverbulletmd/silverbullet))
* [[🔌 Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
* [[🔌 Github]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-github))
* [[🔌 Backlinks]] by **Guillermo Vayá** ([repo](https://github.com/Willyfrog/silverbullet-backlinks))
<!-- /query -->
PS: You don't need to select only certain fields to use templates. Templates are
smart enough to get only the information needed to render the data. Therefore,
the following queries are the same in terms of end result when using the
templates.
```yaml
<!-- #query page select name author repo uri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
```
```yaml
<!-- #query page where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
```

View File

@ -28,7 +28,7 @@ restart SB itself.
([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
- [[🔌 Mount]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-mount))
- [[🔌 Query]] by **Silver Bullet Authors**
- [[🔌 Directive]] by **Silver Bullet Authors**
([repo](https://github.com/silverbulletmd/silverbullet))
<!-- /query -->

View File

@ -1,270 +0,0 @@
```meta
type: plug
uri: core:query
repo: https://github.com/silverbulletmd/silverbullet
author: Silver Bullet Authors
```
### 1. What?
The query plug is a built-in plug implementing the `<!-- #query -->` mechanism.
You can use the query plug to automatically receive information from your pages.
### 2. Syntax
1. _start with_: `<!-- #query [QUERY GOES HERE] -->`
2. _end with_: `<!-- /query -->`
3. _write your query_: replace `[QUERY GOES HERE]` with any query you want using
the options below
4. _available query options_: Usage of options is similar to SQL except for the
special `render` option. The `render` option is used to display the data in a
format that you created in a separate template.
- `where`
- `order by`
- `limit`
- `select`
- `render`
P.S.: If you are a developer or have a technical knowledge to read a code and
would like to know more about syntax, please check out
[query grammar](https://github.com/silverbulletmd/silverbullet/blob/main/packages/plugs/query/query.grammar).
#### 2.1. Available query operators:
- `=` equals
- `!=` not equals
- `<` less than
- `<=` less than or equals
- `>` greater than
- `>=` greater than or equals
- `=~` to match against a regular expression
- `!=~` does not match this regular expression
- `in` member of a list (e.g. `prop in ["foo", "bar"]`)
Further, you can combine multiple of these with `and`. Example
`prop =~ /something/ and prop != “something”`.
### 3. How to run a query?
After writing the query, there are three options:
- Open the **command palette** and run **Materialized Queries: Update**
- Use shortcut: hit **Alt-q** (Windows, Linux) or **Option-q** (Mac)
- Go to another page and come back to the page where the query is located
After using one of the options, the “body” of the query is replaced with the new
results of the query data will be displayed.
### 4. Data sources
Available data sources can be categorized as:
1. Builtin data sources
2. Data that can be inserted by users
3. Plugs data sources
The best part about data sources: there is auto-completion. 🎉
Start writing `<!— #query` or simply use `/query` slash command, it will show
you all available data sources. 🤯
#### 4.1. Available data sources
- `page`: list of all pages 📄
- `task`: list of all tasks created with `[]` syntax ✅
- `full-text`: use it with `where phrase = "SOME_TEXT"`. List of all pages where
`SOME_TEXT` is mentioned ✍️
- `item`: list of ordered and unordered items such as bulleted lists ⏺️
- `tags`: list of all hashtags used in all pages ⚡
- `link`: list of all pages giving a link to the page where query is written 🔗
- `data`: You can insert data using the syntax below 🖥️. You can query the data
using `data` option.
```data
name: John
age: 50
city: Milan
country: Italy
---
name: Jane
age: 53
city: Rome
country: Italy
---
name: Francesco
age: 28
city: Berlin
country: Germany
```
<!-- #query data where age > 20 and country = "Italy" -->
| name | age | city | country | page | pos |
| ---- | --- | ----- | ------- | ------- | ---- |
| John | 50 | Milan | Italy | 🔌 Query | 2696 |
| Jane | 53 | Rome | Italy | 🔌 Query | 2742 |
<!-- /query -->
#### 4.2 Plugs data sources
Certain plugs can also provide special data sources to query specific data. Some
examples are:
- [[🔌 Github]] provides `gh-pull` to query PRs for selected repo
- [[🔌 Mattermost]] provides `mm-saved` to fetch (by default 15) saved posts in
Mattermost
For a complete list of data sources, please check plugs own pages.
### 5. Templates
Templates are predefined formats to render the body of the query.
#### 5.1 How to create a template?
It is pretty easy. You just need to create a new page. However, it is
recommended to create your templates using `template/[TEMPLATE_NAME]`
convention. For this guide, we will create `template/plug` to display list of
Plugs available in Silver Bullet. We will use this template in the Examples
section below.
#### 5.2 What is the syntax?
We are using Handlebars which is a simple templating language. It is using
double curly braces and the name of the parameter to be injected. For our
`template/plug`, we are using simple template like below.
`* [[{{name}}]] by **{{author}}** ([repo]({{repo}}))`
Let me break it down for you
- `*` is creating a bullet point for each item in Silver Bullet
- `[[{{name}}]]` is injecting the name of Plug and creating an internal link to
the page of the Plug
- `**{{author}}**` is injecting the author of the Plug and making it bold
- `([repo]({{repo}}))` is injecting the name of the Plug and creating an
external link to the GitHub page of the Plug
For more information on the Handlebars syntax, you can read the
[official documentation](https://handlebarsjs.com/).
#### 5.3 How to use the template?
You just need to add the `render` keyword followed by the link of the template
to the query like below:
`#query page where type = "plug" render [[template/plug]]`
You can see the usage of our template in example 6.4 below.
### 6. Examples
We will walk you through a set of examples starting from a very basic one
through one formatting the data using templates.
Our goal in this exercise is to (i) get all plug pages (ii) ordered by last
modified time and (iii) display in a nice format.
For the sake of simplicity, we will use the `page` data source and limit the
results not to spoil the page.
#### 6.1 Simple query without any condition
**Goal:** We would like to get the list of all pages.
**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 10 -->
| name | lastModified | perm | tags | type | uri | repo | author |
| ----------------- | ------------- | ---- | --------- | --------- | ------------------------------------ | --------------------------------------------------- | -------------- |
| SETTINGS | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| Silver Bullet | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| CHANGELOG | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| Mattermost Plugin | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| PLUGS | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| index | 1661112513714 | rw | undefined | undefined | undefined | undefined | undefined |
| template/plug | 1661112513718 | rw | undefined | undefined | undefined | undefined | undefined |
| template/tasks | 1661112513718 | rw | #each | undefined | undefined | undefined | undefined |
| 💡 Inspiration | 1661112513718 | rw | undefined | undefined | undefined | undefined | undefined |
| 🔌 Backlinks | 1661112513718 | rw | undefined | plug | ghr:Willyfrog/silverbullet-backlinks | https://github.com/Willyfrog/silverbullet-backlinks | Guillermo Vayá |
<!-- /query -->
#### 6.2 Simple query with a condition
**Goal:** We would like to get all plug pages sorted by last modified time.
**Result:** Okay, this is what we wanted but there is also information such as
perm, type and lastModified that we don't need.
<!-- #query page where type = "plug" order by lastModified desc limit 5 -->
| name | lastModified | perm | type | uri | repo | author |
| ----------- | ------------- | ---- | ---- | ---------------------------------------------------------- | ----------------------------------------------------- | --------------------- |
| 🔌 Query | 1661114193972 | rw | plug | core:query | https://github.com/silverbulletmd/silverbullet | Silver Bullet Authors |
| 🔌 Backlinks | 1661112513718 | rw | plug | ghr:Willyfrog/silverbullet-backlinks | https://github.com/Willyfrog/silverbullet-backlinks | Guillermo Vayá |
| 🔌 Core | 1661112513718 | rw | plug | builtin:core | https://github.com/silverbulletmd/silverbullet | Silver Bullet Authors |
| 🔌 Ghost | 1661112513718 | rw | plug | github:silverbulletmd/silverbullet-ghost/ghost.plug.json | https://github.com/silverbulletmd/silverbullet-ghost | Zef Hemel |
| 🔌 Git | 1661112513718 | rw | plug | github:silverbulletmd/silverbullet-github/github.plug.json | https://github.com/silverbulletmd/silverbullet-github | Zef Hemel |
<!-- /query -->
#### 6.3 Query to select only certain fields
**Goal:** We would like to get all plug pages, selecting only `name`, `author`
and `repo` columns and then sort by last modified time.
**Result:** Okay, this is much better. However, I believe this needs a touch
from a visual perspective.
<!-- #query page select name author repo uri where type = "plug" order by lastModified desc limit 5 -->
| name | author | repo |
| ----------- | --------------------- | ----------------------------------------------------- |
| 🔌 Query | Silver Bullet Authors | https://github.com/silverbulletmd/silverbullet |
| 🔌 Backlinks | Guillermo Vayá | https://github.com/Willyfrog/silverbullet-backlinks |
| 🔌 Core | Silver Bullet Authors | https://github.com/silverbulletmd/silverbullet |
| 🔌 Ghost | Zef Hemel | https://github.com/silverbulletmd/silverbullet-ghost |
| 🔌 Git | Zef Hemel | https://github.com/silverbulletmd/silverbullet-github |
<!-- /query -->
#### 6.4 Display the data in a format defined by a template
**Goal:** We would like to display the data from step 5.3 in a nice format using
bullet points with links to Plug pages, with the author name and a link to their
GitHub repo.
**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 uri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
- [[🔌 Query]] by **Silver Bullet Authors**
([repo](https://github.com/silverbulletmd/silverbullet))
- [[🔌 Backlinks]] by **Guillermo Vayá**
([repo](https://github.com/Willyfrog/silverbullet-backlinks))
- [[🔌 Core]] by **Silver Bullet Authors**
([repo](https://github.com/silverbulletmd/silverbullet))
- [[🔌 Ghost]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-ghost))
- [[🔌 Git]] by **Zef Hemel**
([repo](https://github.com/silverbulletmd/silverbullet-github))
<!-- /query -->
PS: You don't need to select only certain fields to use templates. Templates are
smart enough to get only the information needed to render the data. Therefore,
the following queries are the same in terms of end result when using the
templates.
```yaml
<!-- #query page select name author repo uri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
```
```yaml
<!-- #query page where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
```

View File

@ -1,15 +1,15 @@
- **Powerful Markdown editor** at its core (powered by
* **Powerful Markdown editor** at its core (powered by
[CodeMirror](https://codemirror.net))
- **Distraction-free** UI with
* **Distraction-free** UI with
[What You See is What You Mean](https://en.wikipedia.org/wiki/WYSIWYM)
Markdown editing.
- **Future proof**: stores all notes in a regular folder with markdown files, no
* **Future proof**: stores all notes in a regular folder with markdown files, no
proprietary file formats. While SB uses an SQLite database for indexes, this
database can be wiped and rebuilt based on your pages at any time. Your
Markdown files are the single source of truth.
- **Run anywhere**: run it on your local machine, or install it on a server. You
* **Run anywhere**: run it on your local machine, or install it on a server. You
access it via your web browser (desktop or mobile), or install it as a PWA
(giving it its own window frame and dock/launcher/dock icon).
- **Keyboard oriented:** you can fully operate SB via the keyboard (on
* **Keyboard oriented:** you can fully operate SB via the keyboard (on
laptop/desktop machines as well as iPads with a keyboard).
- **Extensible** through [[🔌 Plugs]]
* **Extensible** through [[🔌 Plugs]]