parent
9040993232
commit
91027af5fe
3
.gitignore
vendored
3
.gitignore
vendored
@ -14,4 +14,5 @@ node_modules
|
||||
*.db*
|
||||
test_space
|
||||
silverbullet
|
||||
deploy.json
|
||||
deploy.json
|
||||
*.generated
|
37
common/json.test.ts
Normal file
37
common/json.test.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { assertEquals } from "../test_deps.ts";
|
||||
import { traverseAndRewriteJSON } from "./json.ts";
|
||||
|
||||
Deno.test("traverseAndRewrite should recursively traverse and rewrite object properties", () => {
|
||||
const bufArray = new Uint8Array([1, 2, 3]);
|
||||
const obj = {
|
||||
foo: "bar",
|
||||
list: ["hello", { sup: "world" }],
|
||||
nested: {
|
||||
baz: "qux",
|
||||
},
|
||||
special: {
|
||||
value: () => {
|
||||
return bufArray;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rewritten = traverseAndRewriteJSON(obj, (val) => {
|
||||
if (typeof val?.value === "function") {
|
||||
return val.value();
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
return val.toUpperCase();
|
||||
}
|
||||
return val;
|
||||
});
|
||||
|
||||
assertEquals(rewritten, {
|
||||
foo: "BAR",
|
||||
list: ["HELLO", { sup: "WORLD" }],
|
||||
nested: {
|
||||
baz: "QUX",
|
||||
},
|
||||
special: bufArray,
|
||||
});
|
||||
});
|
26
common/json.ts
Normal file
26
common/json.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Traverses and rewrites an object recursively.
|
||||
*
|
||||
* @param obj - The object to traverse and rewrite.
|
||||
* @param rewrite - The function to apply for rewriting each value.
|
||||
* @returns The rewritten object.
|
||||
*/
|
||||
export function traverseAndRewriteJSON(
|
||||
obj: any,
|
||||
rewrite: (val: any) => any,
|
||||
): any {
|
||||
// Apply rewrite to object as a whole
|
||||
obj = rewrite(obj);
|
||||
// Recurse down if this is an array or a "plain object"
|
||||
if (
|
||||
obj && Array.isArray(obj) ||
|
||||
(typeof obj === "object" && obj.constructor === Object)
|
||||
) {
|
||||
const keys = Object.keys(obj);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
obj[key] = traverseAndRewriteJSON(obj[key], rewrite);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
@ -25,7 +25,10 @@ import {
|
||||
xmlLanguage,
|
||||
yamlLanguage,
|
||||
} from "./deps.ts";
|
||||
import { highlightingQueryParser } from "./markdown_parser/parser.ts";
|
||||
import {
|
||||
expressionParser,
|
||||
highlightingQueryParser,
|
||||
} from "./markdown_parser/parser.ts";
|
||||
|
||||
export const builtinLanguages: Record<string, Language> = {
|
||||
"meta": StreamLanguage.define(yamlLanguage),
|
||||
@ -81,6 +84,10 @@ export const builtinLanguages: Record<string, Language> = {
|
||||
name: "query",
|
||||
parser: highlightingQueryParser,
|
||||
}),
|
||||
"expression": LRLanguage.define({
|
||||
name: "expression",
|
||||
parser: expressionParser,
|
||||
}),
|
||||
};
|
||||
|
||||
export function languageFor(name: string): Language | null {
|
||||
|
@ -1,119 +0,0 @@
|
||||
@top Program { Expression }
|
||||
|
||||
@precedence {
|
||||
mulop @left
|
||||
addop @left
|
||||
binop @left
|
||||
and @left
|
||||
or @left
|
||||
}
|
||||
|
||||
@skip {
|
||||
space
|
||||
}
|
||||
|
||||
commaSep<content> { content ("," content)* }
|
||||
|
||||
kw<term> { @specialize[@name={term}]<Identifier, term> }
|
||||
|
||||
|
||||
Query {
|
||||
TagIdentifier ( WhereClause | LimitClause | OrderClause | SelectClause | RenderClause )*
|
||||
}
|
||||
|
||||
WhereClause { kw<"where"> Expression }
|
||||
LimitClause { kw<"limit"> Expression }
|
||||
OrderClause { Order commaSep<OrderBy> }
|
||||
OrderBy { Expression OrderDirection? }
|
||||
SelectClause { kw<"select"> commaSep<Select> }
|
||||
RenderClause { kw<"render"> ( kw<"each"> | kw<"all"> )? PageRef }
|
||||
|
||||
Select { Identifier | Expression kw<"as"> Identifier }
|
||||
|
||||
OrderDirection {
|
||||
OrderKW
|
||||
}
|
||||
|
||||
Value { Number | String | Bool | Regex | kw<"null"> | List }
|
||||
|
||||
Attribute {
|
||||
LVal "." Identifier
|
||||
}
|
||||
|
||||
Call {
|
||||
Identifier "(" commaSep<Expression> ")" | Identifier "(" ")"
|
||||
}
|
||||
|
||||
LVal {
|
||||
Identifier
|
||||
| Attribute
|
||||
}
|
||||
|
||||
ParenthesizedExpression { "(" Expression ")" }
|
||||
|
||||
LogicalExpression {
|
||||
Expression !and kw<"and"> Expression
|
||||
| Expression !or kw<"or"> Expression
|
||||
}
|
||||
|
||||
Expression {
|
||||
Value
|
||||
| LVal
|
||||
| ParenthesizedExpression
|
||||
| LogicalExpression
|
||||
| BinExpression
|
||||
| Call
|
||||
}
|
||||
|
||||
BinExpression {
|
||||
Expression !binop "<" Expression
|
||||
| Expression !binop "<=" Expression
|
||||
| Expression !binop "=" Expression
|
||||
| Expression !binop "!=" Expression
|
||||
| Expression !binop ">=" Expression
|
||||
| Expression !binop ">" Expression
|
||||
| Expression !binop "=~" Expression
|
||||
| Expression !binop "!=~" Expression
|
||||
| Expression !binop InKW Expression
|
||||
|
||||
| Expression !mulop "*" Expression
|
||||
| Expression !mulop "/" Expression
|
||||
| Expression !mulop "%" Expression
|
||||
| Expression !addop "+" Expression
|
||||
| Expression !addop "-" Expression
|
||||
}
|
||||
|
||||
List { "[" commaSep<Expression> "]" }
|
||||
|
||||
|
||||
Bool {
|
||||
BooleanKW
|
||||
}
|
||||
|
||||
|
||||
@tokens {
|
||||
space { std.whitespace+ }
|
||||
|
||||
TagIdentifier { @asciiLetter (@asciiLetter | @digit | "-" | "_" | "/" )* }
|
||||
|
||||
Identifier { @asciiLetter (@asciiLetter | @digit | "-" | "_")* }
|
||||
|
||||
String {
|
||||
("\"" | "“" | "”") ![\"”“]* ("\"" | "“" | "”")
|
||||
}
|
||||
PageRef {
|
||||
"[" "[" ![\]]* "]" "]"
|
||||
}
|
||||
Order { "order by" }
|
||||
Regex { "/" ( ![/\\\n\r] | "\\" _ )* "/"? }
|
||||
|
||||
Number { std.digit+ }
|
||||
|
||||
BooleanKW { "true" | "false" }
|
||||
|
||||
InKW { "in" }
|
||||
|
||||
OrderKW { "asc" | "desc" }
|
||||
|
||||
@precedence { Order, BooleanKW, InKW, OrderKW, Identifier, Number }
|
||||
}
|
@ -13,6 +13,6 @@ export const parser = LRParser.deserialize({
|
||||
tokenData: "/j~RuX^#fpq#fqr$Zrs$nuv%fxy%kyz%pz{%u{|%z|}&P}!O&U!O!P&Z!P!Q&`!Q![)Q!^!_)Y!_!`)g!`!a)t!c!}*R!}#O*g#P#Q*l#T#Y*R#Y#Z*q#Z#]*R#]#^-b#^#h*R#h#i.d#i#o*R#y#z#f$f$g#f#BY#BZ#f$IS$I_#f$Ip$Iq$n$Iq$Ir$n$I|$JO#f$JT$JU#f$KV$KW#f&FU&FV#f~#kYh~X^#fpq#f#y#z#f$f$g#f#BY#BZ#f$IS$I_#f$I|$JO#f$JT$JU#f$KV$KW#f&FU&FV#f~$^P!_!`$a~$fPs~#r#s$i~$nOw~~$qWOr$nrs%Zs$Ip$n$Ip$Iq%Z$Iq$Ir%Z$Ir;'S$n;'S;=`%`<%lO$n~%`OT~~%cP;=`<%l$n~%kOz~~%pOn~~%uOo~~%zOx~~&PO{~~&UOk~~&ZO|~~&`Om~R&gXyQWPOY'SZ]'S^!P'S!P!Q't!Q#O'S#O#P'y#P;'S'S;'S;=`(z<%lO'SP'XXWPOY'SZ]'S^!P'S!P!Q't!Q#O'S#O#P'y#P;'S'S;'S;=`(z<%lO'SP'yOWPP'|RO;'S'S;'S;=`(V;=`O'SP([YWPOY'SZ]'S^!P'S!P!Q't!Q#O'S#O#P'y#P;'S'S;'S;=`(z;=`<%l'S<%lO'SP(}P;=`<%l'S~)VPS~!Q![)Q~)_Pp~!_!`)b~)gOq~~)lPr~#r#s)o~)tOv~~)yPu~!_!`)|~*ROt~~*WTX~}!O*R!Q![*R!c!}*R#R#S*R#T#o*R~*lOi~~*qOl~~*vUX~}!O*R!Q![*R!c!}*R#R#S*R#T#U+Y#U#o*R~+_VX~}!O*R!Q![*R!c!}*R#R#S*R#T#`*R#`#a+t#a#o*R~+yVX~}!O*R!Q![*R!c!}*R#R#S*R#T#g*R#g#h,`#h#o*R~,eVX~}!O*R!Q![*R!c!}*R#R#S*R#T#X*R#X#Y,z#Y#o*R~-RTV~X~}!O*R!Q![*R!c!}*R#R#S*R#T#o*R~-gVX~}!O*R!Q![*R!c!}*R#R#S*R#T#b*R#b#c-|#c#o*R~.TTc~X~}!O*R!Q![*R!c!}*R#R#S*R#T#o*R~.iVX~}!O*R!Q![*R!c!}*R#R#S*R#T#f*R#f#g/O#g#o*R~/TVX~}!O*R!Q![*R!c!}*R#R#S*R#T#i*R#i#j,`#j#o*R",
|
||||
tokenizers: [0, 1],
|
||||
topRules: {"Program":[0,1]},
|
||||
specialized: [{term: 9, get: value => spec_Identifier[value] || -1}],
|
||||
specialized: [{term: 9, get: (value) => spec_Identifier[value] || -1}],
|
||||
tokenPrec: 431
|
||||
})
|
||||
|
@ -13,6 +13,6 @@ export const parser = LRParser.deserialize({
|
||||
tokenData: "8k~RzX^#upq#uqr$jrs$}uv%uxy%zyz&Pz{&U{|&Z|}&`}!O&e!O!P&j!P!Q&o!Q![)a!^!_)i!_!`)v!`!a*T!c!}*b!}#O+d#P#Q,a#T#U,f#U#W*b#W#X.c#X#Y*b#Y#Z/S#Z#]*b#]#^2^#^#c*b#c#d3j#d#h*b#h#i7Z#i#o*b#y#z#u$f$g#u#BY#BZ#u$IS$I_#u$Ip$Iq$}$Iq$Ir$}$I|$JO#u$JT$JU#u$KV$KW#u&FU&FV#u~#zY!P~X^#upq#u#y#z#u$f$g#u#BY#BZ#u$IS$I_#u$I|$JO#u$JT$JU#u$KV$KW#u&FU&FV#u~$mP!_!`$p~$uP![~#r#s$x~$}O!`~~%QWOr$}rs%js$Ip$}$Ip$Iq%j$Iq$Ir%j$Ir;'S$};'S;=`%o<%lO$}~%oOY~~%rP;=`<%l$}~%zO!c~~&PO!V~~&UO!W~~&ZO!a~~&`O!d~~&eO!S~~&jO!e~~&oO!U~U&vX!bS]QOY'cZ]'c^!P'c!P!Q(T!Q#O'c#O#P(Y#P;'S'c;'S;=`)Z<%lO'cQ'hX]QOY'cZ]'c^!P'c!P!Q(T!Q#O'c#O#P(Y#P;'S'c;'S;=`)Z<%lO'cQ(YO]QQ(]RO;'S'c;'S;=`(f;=`O'cQ(kY]QOY'cZ]'c^!P'c!P!Q(T!Q#O'c#O#P(Y#P;'S'c;'S;=`)Z;=`<%l'c<%lO'cQ)^P;=`<%l'c~)fPX~!Q![)a~)nP!X~!_!`)q~)vO!Y~~){P!Z~#r#s*O~*TO!_~~*YP!^~!_!`*]~*bO!]~V*iURPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#o*bP+QURP}!O*{!P!Q*{!Q![*{!c!}*{#R#S*{#T#o*{V+iP!QQ!}#O+lT+oTO#P+l#P#Q,O#Q;'S+l;'S;=`,Z<%lO+lT,RP#P#Q,UT,ZOxTT,^P;=`<%l+l~,fO!T~V,mWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#g*b#g#h-V#h#o*bV-^WRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#V*b#V#W-v#W#o*bV.PURPoSTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#o*bV.jWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#X*b#X#Y,f#Y#o*bV/ZVRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#U/p#U#o*bV/wWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#`*b#`#a0a#a#o*bV0hWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#g*b#g#h1Q#h#o*bV1XWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#X*b#X#Y1q#Y#o*bV1zURP[QTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#o*bV2eWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#b*b#b#c2}#c#o*bV3WURPgSTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#o*bV3qWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#f*b#f#g4Z#g#o*bV4bWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#W*b#W#X4z#X#o*bV5RWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#X*b#X#Y5k#Y#o*bV5rWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#f*b#f#g6[#g#o*bV6cVRPTUpq6x}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#o*bU6{P#U#V7OU7RP#m#n7UU7ZOlUV7bWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#f*b#f#g7z#g#o*bV8RWRPTU}!O*b!P!Q*{!Q![*b!c!}*b#R#S*b#T#i*b#i#j1Q#j#o*b",
|
||||
tokenizers: [0, 1, 2],
|
||||
topRules: {"Program":[0,1]},
|
||||
specialized: [{term: 5, get: value => spec_Identifier[value] || -1}],
|
||||
specialized: [{term: 5, get: (value) => spec_Identifier[value] || -1}],
|
||||
tokenPrec: 798
|
||||
})
|
||||
|
@ -136,3 +136,9 @@ Deno.test("Test command link arguments", () => {
|
||||
const args2 = findNodeOfType(commands[1], "CommandLinkArgs");
|
||||
assertEquals(args2!.children![0].text, '"other", "args", 123');
|
||||
});
|
||||
|
||||
Deno.test("Test template directives", () => {
|
||||
const lang = buildMarkdown([]);
|
||||
const tree = parse(lang, `Hello there {{name}}!`);
|
||||
console.log("Template directive", JSON.stringify(tree, null, 2));
|
||||
});
|
||||
|
@ -135,6 +135,38 @@ const CommandLink: MarkdownConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
export const templateDirectiveRegex = /^\{\{([^\}]+)\}\}/;
|
||||
|
||||
const TemplateDirective: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
{ name: "TemplateDirective", style: t.monospace },
|
||||
{ name: "TemplateDirectiveMark", style: t.monospace },
|
||||
],
|
||||
parseInline: [
|
||||
{
|
||||
name: "TemplateDirective",
|
||||
parse(cx, next, pos) {
|
||||
let match: RegExpMatchArray | null;
|
||||
if (
|
||||
next != 123 /* '{' */ ||
|
||||
!(match = templateDirectiveRegex.exec(cx.slice(pos, cx.end)))
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
const fullMatch = match[0];
|
||||
const endPos = pos + fullMatch.length;
|
||||
return cx.addElement(
|
||||
cx.elt("TemplateDirective", pos, endPos, [
|
||||
cx.elt("TemplateDirectiveMark", pos, pos + 2),
|
||||
cx.elt("TemplateDirectiveMark", endPos - 2, endPos),
|
||||
]),
|
||||
);
|
||||
},
|
||||
after: "Emphasis",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const HighlightDelim = { resolve: "Highlight", mark: "HighlightMark" };
|
||||
|
||||
export const Highlight: MarkdownConfig = {
|
||||
@ -175,6 +207,8 @@ export const highlightingQueryParser = queryParser.configure({
|
||||
],
|
||||
});
|
||||
|
||||
export { parser as expressionParser } from "./parse-expression.js";
|
||||
|
||||
export const attributeStartRegex = /^\[([\w\$]+)(::?\s*)/;
|
||||
|
||||
export const Attribute: MarkdownConfig = {
|
||||
@ -355,6 +389,7 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language {
|
||||
TaskList,
|
||||
Comment,
|
||||
Highlight,
|
||||
TemplateDirective,
|
||||
Strikethrough,
|
||||
Table,
|
||||
...mdExtensions.map(mdExtensionSyntaxConfig),
|
||||
|
@ -40,5 +40,17 @@ export function handlebarHelpers() {
|
||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||
return niceDate(nextWeek);
|
||||
},
|
||||
ifEq: function (v1: any, v2: any, options: any) {
|
||||
if (v1 === v2) {
|
||||
return options.fn(this);
|
||||
}
|
||||
return options.inverse(this);
|
||||
},
|
||||
ifNeq: function (v1: any, v2: any, options: any) {
|
||||
if (v1 !== v2) {
|
||||
return options.fn(this);
|
||||
}
|
||||
return options.inverse(this);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -4,12 +4,20 @@ import { SpacePrimitives } from "./spaces/space_primitives.ts";
|
||||
import { expandPropertyNames } from "$sb/lib/json.ts";
|
||||
import type { BuiltinSettings } from "../web/types.ts";
|
||||
|
||||
/**
|
||||
* Runs a function safely by catching any errors and logging them to the console.
|
||||
* @param fn - The function to run.
|
||||
*/
|
||||
export function safeRun(fn: () => Promise<void>) {
|
||||
fn().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current platform is Mac-like (Mac, iPhone, iPod, iPad).
|
||||
* @returns A boolean indicating if the platform is Mac-like.
|
||||
*/
|
||||
export function isMacLike() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||
}
|
||||
@ -17,6 +25,11 @@ export function isMacLike() {
|
||||
// TODO: This is naive, may be better to use a proper parser
|
||||
const yamlSettingsRegex = /```yaml([^`]+)```/;
|
||||
|
||||
/**
|
||||
* Parses YAML settings from a Markdown string.
|
||||
* @param settingsMarkdown - The Markdown string containing the YAML settings.
|
||||
* @returns An object representing the parsed YAML settings.
|
||||
*/
|
||||
export function parseYamlSettings(settingsMarkdown: string): {
|
||||
[key: string]: any;
|
||||
} {
|
||||
@ -35,6 +48,12 @@ export function parseYamlSettings(settingsMarkdown: string): {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the settings and index page exist in the given space.
|
||||
* If they don't exist, default settings and index page will be created.
|
||||
* @param space - The SpacePrimitives object representing the space.
|
||||
* @returns A promise that resolves to the built-in settings.
|
||||
*/
|
||||
export async function ensureSettingsAndIndex(
|
||||
space: SpacePrimitives,
|
||||
): Promise<BuiltinSettings> {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { AST } from "$sb/lib/tree.ts";
|
||||
import { type AST, parseTreeToAST } from "$sb/lib/tree.ts";
|
||||
import type { Query, QueryExpression } from "$sb/types.ts";
|
||||
import { language } from "$sb/syscalls.ts";
|
||||
|
||||
export function astToKvQuery(
|
||||
node: AST,
|
||||
@ -20,10 +21,10 @@ export function astToKvQuery(
|
||||
query.filter = [
|
||||
"and",
|
||||
query.filter,
|
||||
expressionToKvQueryFilter(clause[2]),
|
||||
expressionToKvQueryExpression(clause[2]),
|
||||
];
|
||||
} else {
|
||||
query.filter = expressionToKvQueryFilter(clause[2]);
|
||||
query.filter = expressionToKvQueryExpression(clause[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -123,18 +124,18 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression {
|
||||
}
|
||||
case "BinExpression": {
|
||||
const lval = expressionToKvQueryExpression(node[1]);
|
||||
const binOp = (node[2] as string).trim();
|
||||
const binOp = node[2][0] === "InKW" ? "in" : (node[2] as string).trim();
|
||||
const val = expressionToKvQueryExpression(node[3]);
|
||||
return [binOp as any, lval, val];
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
const op1 = expressionToKvQueryFilter(node[1]);
|
||||
const op1 = expressionToKvQueryExpression(node[1]);
|
||||
const op = node[2];
|
||||
const op2 = expressionToKvQueryFilter(node[3]);
|
||||
const op2 = expressionToKvQueryExpression(node[3]);
|
||||
return [op[1] as any, op1, op2];
|
||||
}
|
||||
case "ParenthesizedExpression": {
|
||||
return expressionToKvQueryFilter(node[2]);
|
||||
return expressionToKvQueryExpression(node[2]);
|
||||
}
|
||||
case "Call": {
|
||||
// console.log("Call", node);
|
||||
@ -151,33 +152,12 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression {
|
||||
throw new Error(`Not supported: ${node[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
function expressionToKvQueryFilter(
|
||||
node: AST,
|
||||
): QueryExpression {
|
||||
const [expressionType] = node;
|
||||
if (expressionType === "Expression") {
|
||||
return expressionToKvQueryFilter(node[1]);
|
||||
}
|
||||
switch (expressionType) {
|
||||
case "BinExpression": {
|
||||
const lval = expressionToKvQueryExpression(node[1]);
|
||||
const binOp = node[2][0] === "InKW" ? "in" : (node[2] as string).trim();
|
||||
const val = expressionToKvQueryExpression(node[3]);
|
||||
return [binOp as any, lval, val];
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
// console.log("Logical expression", node);
|
||||
// 0 = first operand, 1 = whitespace, 2 = operator, 3 = whitespace, 4 = second operand
|
||||
const op1 = expressionToKvQueryFilter(node[1]);
|
||||
const op = node[2]; // 1 is whitespace
|
||||
const op2 = expressionToKvQueryFilter(node[3]);
|
||||
return [op[1] as any, op1, op2];
|
||||
}
|
||||
case "ParenthesizedExpression": {
|
||||
return expressionToKvQueryFilter(node[2]);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown expression type: ${expressionType}`);
|
||||
}
|
||||
export async function parseQuery(query: string): Promise<Query> {
|
||||
const queryAST = parseTreeToAST(
|
||||
await language.parseLanguage(
|
||||
"query",
|
||||
query,
|
||||
),
|
||||
);
|
||||
return astToKvQuery(queryAST[1]);
|
||||
}
|
||||
|
@ -21,6 +21,14 @@ Deno.test("Test query parser", () => {
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(wrapQueryParse(`page where true`)!),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["boolean", true],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(wrapQueryParse(`page where name =~ /test/`)!),
|
||||
{
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ParseTree, renderToText, replaceNodesMatching } from "$sb/lib/tree.ts";
|
||||
import { FunctionMap, KV, Query, QueryExpression } from "$sb/types.ts";
|
||||
|
||||
export function evalQueryExpression(
|
||||
|
@ -134,9 +134,11 @@ export type CodeWidgetContent = {
|
||||
markdown?: string;
|
||||
script?: string;
|
||||
buttons?: CodeWidgetButton[];
|
||||
banner?: string;
|
||||
};
|
||||
|
||||
export type CodeWidgetButton = {
|
||||
widgetTarget?: boolean;
|
||||
description: string;
|
||||
svg: string;
|
||||
invokeFunction: string;
|
||||
|
@ -11,13 +11,13 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
displayName: "string",
|
||||
aliases: "array",
|
||||
aliases: "string[]",
|
||||
created: "!date",
|
||||
lastModified: "!date",
|
||||
perm: "!rw|ro",
|
||||
contentType: "!string",
|
||||
size: "!number",
|
||||
tags: "array",
|
||||
tags: "string[]",
|
||||
},
|
||||
task: {
|
||||
ref: "!string",
|
||||
@ -27,11 +27,11 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
state: "!string",
|
||||
deadline: "string",
|
||||
pos: "!number",
|
||||
tags: "array",
|
||||
tags: "string[]",
|
||||
},
|
||||
taskstate: {
|
||||
ref: "!string",
|
||||
tags: "!array",
|
||||
tags: "!string[]",
|
||||
state: "!string",
|
||||
count: "!number",
|
||||
page: "!string",
|
||||
@ -76,6 +76,7 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
pos: "!number",
|
||||
type: "string",
|
||||
trigger: "string",
|
||||
forTags: "string[]",
|
||||
},
|
||||
};
|
||||
|
||||
|
82
plugs/index/frontmatter.ts
Normal file
82
plugs/index/frontmatter.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { CodeWidgetContent } from "$sb/types.ts";
|
||||
import { editor, language, markdown, space } from "$sb/syscalls.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { queryObjects } from "./api.ts";
|
||||
import { TemplateObject } from "../template/types.ts";
|
||||
import { renderTemplate } from "../template/plug_api.ts";
|
||||
import { loadPageObject } from "../template/template.ts";
|
||||
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
||||
import { evalQueryExpression } from "$sb/lib/query.ts";
|
||||
import { parseTreeToAST } from "$sb/lib/tree.ts";
|
||||
|
||||
// Somewhat decent looking default template
|
||||
const fallbackTemplate = `{{#each .}}
|
||||
{{#ifEq @key "tags"}}{{else}}**{{@key}}**: {{.}}
|
||||
{{/ifEq}}
|
||||
{{/each}}
|
||||
{{#if tags}}_Tagged with_ {{#each tags}}#{{.}} {{/each}}{{/if}}`;
|
||||
|
||||
export async function renderFrontmatterWidget(): Promise<
|
||||
CodeWidgetContent | null
|
||||
> {
|
||||
const text = await editor.getText();
|
||||
const pageMeta = await loadPageObject(await editor.getCurrentPage());
|
||||
const parsedMd = await markdown.parseMarkdown(text);
|
||||
const frontmatter = await extractFrontmatter(parsedMd);
|
||||
|
||||
const allFrontMatterTemplates = await queryObjects<TemplateObject>(
|
||||
"template",
|
||||
{
|
||||
filter: ["=", ["attr", "type"], ["string", "frontmatter"]],
|
||||
orderBy: [{ expr: ["attr", "priority"], desc: false }],
|
||||
},
|
||||
);
|
||||
let templateText = fallbackTemplate;
|
||||
// Strategy: walk through all matching templates, evaluate the 'where' expression, and pick the first one that matches
|
||||
for (const template of allFrontMatterTemplates) {
|
||||
const exprAST = parseTreeToAST(
|
||||
await language.parseLanguage("expression", template.where!),
|
||||
);
|
||||
const parsedExpression = expressionToKvQueryExpression(exprAST[1]);
|
||||
if (evalQueryExpression(parsedExpression, pageMeta)) {
|
||||
// Match! We're happy
|
||||
templateText = await space.readPage(template.ref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const summaryText = await renderTemplate(
|
||||
templateText,
|
||||
pageMeta,
|
||||
frontmatter,
|
||||
);
|
||||
// console.log("Rendered", summaryText);
|
||||
return {
|
||||
markdown: summaryText.text,
|
||||
banner: "frontmatter",
|
||||
buttons: [
|
||||
{
|
||||
description: "Reload",
|
||||
svg:
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
|
||||
invokeFunction: "index.refreshWidgets",
|
||||
},
|
||||
{
|
||||
description: "Edit",
|
||||
svg:
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
|
||||
invokeFunction: "index.editFrontmatter",
|
||||
},
|
||||
{
|
||||
description: "",
|
||||
svg: "",
|
||||
widgetTarget: true,
|
||||
invokeFunction: "index.editFrontmatter",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export async function editFrontmatter() {
|
||||
// 4 = after the frontmatter (--- + newline)
|
||||
await editor.moveCursor(4, true);
|
||||
}
|
@ -186,4 +186,13 @@ functions:
|
||||
lintYAML:
|
||||
path: lint.ts:lintYAML
|
||||
events:
|
||||
- editor:lint
|
||||
- editor:lint
|
||||
|
||||
renderFrontmatterWidget:
|
||||
path: frontmatter.ts:renderFrontmatterWidget
|
||||
env: client
|
||||
panelWidget: frontmatter
|
||||
|
||||
editFrontmatter:
|
||||
path: frontmatter.ts:editFrontmatter
|
||||
|
||||
|
@ -48,7 +48,7 @@ export async function renderTOC(): Promise<CodeWidgetContent | null> {
|
||||
return false;
|
||||
});
|
||||
if (headers.length < headerThreshold) {
|
||||
console.log("Not enough headers, not showing TOC", headers.length);
|
||||
// Not enough headers, not showing TOC
|
||||
return null;
|
||||
}
|
||||
// console.log("Headers", headers);
|
||||
|
@ -267,7 +267,10 @@ function render(
|
||||
}
|
||||
case "Hashtag":
|
||||
return {
|
||||
name: "strong",
|
||||
name: "span",
|
||||
attrs: {
|
||||
class: "hashtag",
|
||||
},
|
||||
body: t.children![0].text!,
|
||||
};
|
||||
|
||||
@ -414,6 +417,16 @@ function render(
|
||||
case "Entity":
|
||||
return t.children![0].text!;
|
||||
|
||||
case "TemplateDirective": {
|
||||
return {
|
||||
name: "span",
|
||||
attrs: {
|
||||
class: "template-directive",
|
||||
},
|
||||
body: renderToText(t),
|
||||
};
|
||||
}
|
||||
|
||||
// Text
|
||||
case undefined:
|
||||
return t.text!;
|
||||
|
@ -1,11 +1,7 @@
|
||||
import type { LintEvent } from "$sb/app_event.ts";
|
||||
import { events, language, space } from "$sb/syscalls.ts";
|
||||
import {
|
||||
findNodeOfType,
|
||||
parseTreeToAST,
|
||||
traverseTreeAsync,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { astToKvQuery } from "$sb/lib/parse-query.ts";
|
||||
import { events, space } from "$sb/syscalls.ts";
|
||||
import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts";
|
||||
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
|
||||
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
|
||||
import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts";
|
||||
@ -18,13 +14,9 @@ export async function widget(
|
||||
const pageObject = await loadPageObject(pageName);
|
||||
|
||||
try {
|
||||
const queryAST = parseTreeToAST(
|
||||
await language.parseLanguage(
|
||||
"query",
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
),
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
const parsedQuery = astToKvQuery(queryAST[1]);
|
||||
|
||||
if (!parsedQuery.limit) {
|
||||
parsedQuery.limit = ["number", 1000];
|
||||
@ -115,13 +107,10 @@ export async function lintQuery(
|
||||
}
|
||||
const bodyText = codeText.children![0].text!;
|
||||
try {
|
||||
const queryAST = parseTreeToAST(
|
||||
await language.parseLanguage(
|
||||
"query",
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
),
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
const parsedQuery = astToKvQuery(queryAST[1]);
|
||||
|
||||
const allSources = await allQuerySources();
|
||||
if (
|
||||
parsedQuery.querySource &&
|
||||
@ -141,7 +130,7 @@ export async function lintQuery(
|
||||
);
|
||||
try {
|
||||
await space.getPageMeta(templatePage);
|
||||
} catch (e: any) {
|
||||
} catch {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { ObjectValue } from "$sb/types.ts";
|
||||
|
||||
export type TemplateFrontmatter = {
|
||||
trigger?: string; // slash command name
|
||||
displayName?: string;
|
||||
type?: "page";
|
||||
// Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string
|
||||
frontmatter?: Record<string, any> | string;
|
||||
|
||||
// Specific for slash templates
|
||||
trigger?: string;
|
||||
|
||||
// Specific for frontmatter templates
|
||||
where?: string; // expression (SB query style)
|
||||
priority?: number; // When multiple templates match, the one with the highest priority is used
|
||||
};
|
||||
|
||||
export type TemplateObject = ObjectValue<TemplateFrontmatter>;
|
||||
|
@ -36,20 +36,27 @@ export function buildHandebarOptions(pageMeta: PageMeta) {
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultJsonTransformer(_k: string, v: any) {
|
||||
export function defaultJsonTransformer(v: any): string {
|
||||
if (v === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof v === "string") {
|
||||
return v.replaceAll("\n", " ").replaceAll("|", "\\|");
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
return v.map(defaultJsonTransformer).join(", ");
|
||||
} else if (typeof v === "object") {
|
||||
return Object.entries(v).map(([k, v]: [string, any]) =>
|
||||
`${k}: ${defaultJsonTransformer(v)}`
|
||||
).join(", ");
|
||||
}
|
||||
return "" + v;
|
||||
}
|
||||
|
||||
// Nicely format an array of JSON objects as a Markdown table
|
||||
export function jsonToMDTable(
|
||||
jsonArray: any[],
|
||||
valueTransformer: (k: string, v: any) => string = defaultJsonTransformer,
|
||||
valueTransformer: (v: any) => string = defaultJsonTransformer,
|
||||
): string {
|
||||
const headers = new Set<string>();
|
||||
for (const entry of jsonArray) {
|
||||
@ -79,7 +86,7 @@ export function jsonToMDTable(
|
||||
for (const val of jsonArray) {
|
||||
const el = [];
|
||||
for (const prop of headerList) {
|
||||
const s = valueTransformer(prop, val[prop]);
|
||||
const s = valueTransformer(val[prop]);
|
||||
el.push(s);
|
||||
}
|
||||
lines.push("|" + el.join("|") + "|");
|
||||
|
@ -1,9 +1,12 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
QUERY_GRAMMAR=common/markdown_parser/query.grammar
|
||||
EXPRESSION_GRAMMAR=common/markdown_parser/expression.grammar
|
||||
EXPRESSION_GRAMMAR=common/markdown_parser/expression.grammar.generated
|
||||
LEZER_GENERATOR_VERSION=1.5.1
|
||||
|
||||
# Generate a patched grammer for just expressions
|
||||
echo "@top Program { Expression }" > $EXPRESSION_GRAMMAR
|
||||
tail -n +2 $QUERY_GRAMMAR >> $EXPRESSION_GRAMMAR
|
||||
|
||||
npx lezer-generator $QUERY_GRAMMAR -o common/markdown_parser/parse-query.js
|
||||
npx lezer-generator $EXPRESSION_GRAMMAR -o common/markdown_parser/parse-expression.js
|
||||
deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $QUERY_GRAMMAR -o common/markdown_parser/parse-query.js
|
||||
deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $EXPRESSION_GRAMMAR -o common/markdown_parser/parse-expression.js
|
@ -289,7 +289,7 @@ export class Client {
|
||||
|
||||
this.pageNavigator.subscribe(
|
||||
async (pageName, pos: number | string | undefined) => {
|
||||
console.log("Now navigating to", pageName, pos);
|
||||
console.log("Now navigating to", pageName);
|
||||
|
||||
const stateRestored = await this.loadPage(pageName, pos === undefined);
|
||||
if (pos) {
|
||||
@ -1012,7 +1012,6 @@ export class Client {
|
||||
|
||||
// Widget and image height caching
|
||||
private widgetCache = new LimitedMap<WidgetCacheItem>(100); // bodyText -> WidgetCacheItem
|
||||
private imageHeightCache = new LimitedMap<number>(100); // url -> height
|
||||
private widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
|
||||
|
||||
async loadCaches() {
|
||||
@ -1066,4 +1065,5 @@ type WidgetCacheItem = {
|
||||
height: number;
|
||||
html: string;
|
||||
buttons?: CodeWidgetButton[];
|
||||
banner?: string;
|
||||
};
|
||||
|
@ -1,76 +1,38 @@
|
||||
import { Decoration, EditorState, foldedRanges, syntaxTree } from "../deps.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import {
|
||||
decoratorStateField,
|
||||
HtmlWidget,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
} from "./util.ts";
|
||||
|
||||
function hideNodes(state: EditorState) {
|
||||
const widgets: any[] = [];
|
||||
const foldRanges = foldedRanges(state);
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "HorizontalRule" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-hr",
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "Image" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "FrontMatterMarker"
|
||||
) {
|
||||
const parent = node.node.parent!;
|
||||
|
||||
const folded = foldRanges.iter();
|
||||
let shouldShowFrontmatterBanner = false;
|
||||
while (folded.value) {
|
||||
// Check if cursor is in the folded range
|
||||
if (isCursorInRange(state, [folded.from, folded.to])) {
|
||||
// console.log("Cursor is in folded area, ");
|
||||
shouldShowFrontmatterBanner = true;
|
||||
break;
|
||||
}
|
||||
folded.next();
|
||||
}
|
||||
if (!isCursorInRange(state, [parent.from, parent.to])) {
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(node.from),
|
||||
);
|
||||
shouldShowFrontmatterBanner = true;
|
||||
}
|
||||
if (shouldShowFrontmatterBanner && parent.from === node.from) {
|
||||
// Only put this on the first line of the frontmatter
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new HtmlWidget(
|
||||
`frontmatter`,
|
||||
"sb-frontmatter-marker",
|
||||
),
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
}
|
||||
|
||||
export function cleanBlockPlugin() {
|
||||
return decoratorStateField(hideNodes);
|
||||
return decoratorStateField(
|
||||
(state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "HorizontalRule" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-hr",
|
||||
}).range(node.from),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
node.name === "Image" &&
|
||||
!isCursorInRange(state, [node.from, node.to])
|
||||
) {
|
||||
widgets.push(invisibleDecoration.range(node.from, node.to));
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -12,33 +12,35 @@ import { taskListPlugin } from "./task.ts";
|
||||
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
|
||||
import { cleanCommandLinkPlugin } from "./command_link.ts";
|
||||
import { fencedCodePlugin } from "./fenced_code.ts";
|
||||
import { frontmatterPlugin } from "./frontmatter.ts";
|
||||
|
||||
export function cleanModePlugins(editor: Client) {
|
||||
export function cleanModePlugins(client: Client) {
|
||||
return [
|
||||
linkPlugin(editor),
|
||||
linkPlugin(client),
|
||||
blockquotePlugin(),
|
||||
admonitionPlugin(editor),
|
||||
admonitionPlugin(client),
|
||||
hideMarksPlugin(),
|
||||
hideHeaderMarkPlugin(),
|
||||
cleanBlockPlugin(),
|
||||
fencedCodePlugin(editor),
|
||||
frontmatterPlugin(client),
|
||||
fencedCodePlugin(client),
|
||||
taskListPlugin({
|
||||
// TODO: Move this logic elsewhere?
|
||||
onCheckboxClick: (pos) => {
|
||||
const clickEvent: ClickEvent = {
|
||||
page: editor.currentPage!,
|
||||
page: client.currentPage!,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
pos: pos,
|
||||
};
|
||||
// Propagate click event from checkbox
|
||||
editor.dispatchAppEvent("page:click", clickEvent);
|
||||
client.dispatchAppEvent("page:click", clickEvent);
|
||||
},
|
||||
}),
|
||||
listBulletPlugin(),
|
||||
tablePlugin(editor),
|
||||
cleanWikiLinkPlugin(editor),
|
||||
cleanCommandLinkPlugin(editor),
|
||||
tablePlugin(client),
|
||||
cleanWikiLinkPlugin(client),
|
||||
cleanCommandLinkPlugin(client),
|
||||
] as Extension[];
|
||||
}
|
||||
|
@ -71,11 +71,15 @@ export function fencedCodePlugin(editor: Client) {
|
||||
);
|
||||
});
|
||||
|
||||
const bodyText = lineStrings.slice(1, lineStrings.length - 1).join(
|
||||
"\n",
|
||||
);
|
||||
const widget = renderMode === "markdown"
|
||||
? new MarkdownWidget(
|
||||
from + lineStrings[0].length + 1,
|
||||
editor,
|
||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
||||
`widget:${editor.currentPage}:${bodyText}`,
|
||||
bodyText,
|
||||
codeWidgetCallback,
|
||||
"sb-markdown-widget",
|
||||
)
|
||||
|
84
web/cm_plugins/frontmatter.ts
Normal file
84
web/cm_plugins/frontmatter.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { Client } from "../client.ts";
|
||||
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||
import { MarkdownWidget } from "./markdown_widget.ts";
|
||||
import { decoratorStateField, HtmlWidget, isCursorInRange } from "./util.ts";
|
||||
|
||||
export function frontmatterPlugin(client: Client) {
|
||||
const panelWidgetHook = client.system.panelWidgetHook;
|
||||
const frontmatterCallback = panelWidgetHook.callbacks.get("frontmatter");
|
||||
return decoratorStateField(
|
||||
(state: EditorState) => {
|
||||
const widgets: any[] = [];
|
||||
|
||||
syntaxTree(state).iterate({
|
||||
enter(node) {
|
||||
if (
|
||||
node.name === "FrontMatter"
|
||||
) {
|
||||
if (!isCursorInRange(state, [node.from, node.to])) {
|
||||
if (frontmatterCallback) {
|
||||
// Render as a widget
|
||||
const text = state.sliceDoc(node.from, node.to);
|
||||
const lineStrings = text.split("\n");
|
||||
|
||||
const lines: { from: number; to: number }[] = [];
|
||||
let fromIt = node.from;
|
||||
for (const line of lineStrings) {
|
||||
lines.push({
|
||||
from: fromIt,
|
||||
to: fromIt + line.length,
|
||||
});
|
||||
fromIt += line.length + 1;
|
||||
}
|
||||
|
||||
lines.slice(0, lines.length - 1).forEach((line) => {
|
||||
widgets.push(
|
||||
// Reusing line-table-outside here for laziness reasons
|
||||
Decoration.line({ class: "sb-line-table-outside" }).range(
|
||||
line.from,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new MarkdownWidget(
|
||||
undefined,
|
||||
client,
|
||||
`frontmatter:${client.currentPage}`,
|
||||
"",
|
||||
frontmatterCallback,
|
||||
"sb-markdown-frontmatter-widget",
|
||||
),
|
||||
block: true,
|
||||
}).range(lines[lines.length - 1].from),
|
||||
);
|
||||
} else if (!frontmatterCallback) {
|
||||
// Not rendering as a widget
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new HtmlWidget(
|
||||
`frontmatter`,
|
||||
"sb-frontmatter-marker",
|
||||
),
|
||||
}).range(node.from),
|
||||
);
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(node.from),
|
||||
);
|
||||
widgets.push(
|
||||
Decoration.line({
|
||||
class: "sb-line-frontmatter-outside",
|
||||
}).range(state.doc.lineAt(node.to).from),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return Decoration.set(widgets, true);
|
||||
},
|
||||
);
|
||||
}
|
@ -5,17 +5,16 @@ import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
|
||||
import { resolveAttachmentPath } from "$sb/lib/resolve.ts";
|
||||
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
|
||||
const activeWidgets = new Set<MarkdownWidget>();
|
||||
|
||||
export class MarkdownWidget extends WidgetType {
|
||||
renderedMarkdown?: string;
|
||||
public dom?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
readonly from: number | undefined,
|
||||
readonly client: Client,
|
||||
readonly cacheKey: string,
|
||||
readonly bodyText: string,
|
||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||
readonly className: string,
|
||||
@ -26,11 +25,12 @@ export class MarkdownWidget extends WidgetType {
|
||||
toDOM(): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = this.className;
|
||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||
if (cacheItem) {
|
||||
div.innerHTML = this.wrapHtml(
|
||||
cacheItem.html,
|
||||
cacheItem.buttons,
|
||||
cacheItem.buttons || [],
|
||||
cacheItem.banner,
|
||||
);
|
||||
this.attachListeners(div, cacheItem.buttons);
|
||||
}
|
||||
@ -55,7 +55,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
if (!widgetContent) {
|
||||
div.innerHTML = "";
|
||||
this.client.setWidgetCache(
|
||||
this.bodyText,
|
||||
this.cacheKey,
|
||||
{ height: div.clientHeight, html: "" },
|
||||
);
|
||||
return;
|
||||
@ -73,8 +73,6 @@ export class MarkdownWidget extends WidgetType {
|
||||
this.client.currentPage,
|
||||
],
|
||||
);
|
||||
// Used for the source button
|
||||
this.renderedMarkdown = renderToText(mdTree);
|
||||
|
||||
const html = renderMarkdownToHtml(mdTree, {
|
||||
// Annotate every element with its position so we can use it to put
|
||||
@ -97,27 +95,48 @@ export class MarkdownWidget extends WidgetType {
|
||||
// HTML still same as in cache, no need to re-render
|
||||
return;
|
||||
}
|
||||
div.innerHTML = this.wrapHtml(html, widgetContent.buttons);
|
||||
div.innerHTML = this.wrapHtml(
|
||||
html,
|
||||
widgetContent.buttons || [],
|
||||
widgetContent.banner,
|
||||
);
|
||||
this.attachListeners(div, widgetContent.buttons);
|
||||
|
||||
// Let's give it a tick, then measure and cache
|
||||
setTimeout(() => {
|
||||
this.client.setWidgetCache(
|
||||
this.bodyText,
|
||||
{ height: div.offsetHeight, html, buttons: widgetContent.buttons },
|
||||
this.cacheKey,
|
||||
{
|
||||
height: div.offsetHeight,
|
||||
html,
|
||||
buttons: widgetContent.buttons,
|
||||
banner: widgetContent.banner,
|
||||
},
|
||||
);
|
||||
// Because of the rejiggering of the DOM, we need to do a no-op cursor move to make sure it's positioned correctly
|
||||
this.client.editorView.dispatch({
|
||||
selection: {
|
||||
anchor: this.client.editorView.state.selection.main.anchor,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private wrapHtml(html: string, buttons?: CodeWidgetButton[]) {
|
||||
if (!buttons) {
|
||||
return html;
|
||||
private wrapHtml(
|
||||
html: string,
|
||||
buttons: CodeWidgetButton[],
|
||||
banner?: string,
|
||||
) {
|
||||
if (!html) {
|
||||
return "";
|
||||
}
|
||||
return `<div class="button-bar">${
|
||||
buttons.map((button, idx) =>
|
||||
buttons.filter((button) => !button.widgetTarget).map((button, idx) =>
|
||||
`<button data-button="${idx}" title="${button.description}">${button.svg}</button> `
|
||||
).join("")
|
||||
}</div>${html}`;
|
||||
}</div>${
|
||||
banner ? `<div class="sb-banner">${escapeHtml(banner)}</div>` : ""
|
||||
}${html}`;
|
||||
}
|
||||
|
||||
private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) {
|
||||
@ -126,6 +145,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
// Override default click behavior with a local navigate (faster)
|
||||
el.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
||||
if (pos && pos.match(/^\d+$/)) {
|
||||
this.client.navigate(pageName, +pos);
|
||||
@ -138,9 +158,18 @@ export class MarkdownWidget extends WidgetType {
|
||||
// Implement task toggling
|
||||
div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => {
|
||||
const taskRef = el.dataset.externalTaskRef;
|
||||
el.querySelector("input[type=checkbox]").addEventListener(
|
||||
const input = el.querySelector("input[type=checkbox]")!;
|
||||
input.addEventListener(
|
||||
"click",
|
||||
(e: any) => {
|
||||
// Avoid triggering the click on the parent
|
||||
e.stopPropagation();
|
||||
},
|
||||
);
|
||||
input.addEventListener(
|
||||
"change",
|
||||
(e: any) => {
|
||||
e.stopPropagation();
|
||||
const oldState = e.target.dataset.state;
|
||||
const newState = oldState === " " ? "x" : " ";
|
||||
// Update state in DOM as well for future toggles
|
||||
@ -162,29 +191,37 @@ export class MarkdownWidget extends WidgetType {
|
||||
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
const button = buttons[i];
|
||||
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
console.log("Button clicked:", button.description);
|
||||
if (button.widgetTarget) {
|
||||
div.addEventListener("click", () => {
|
||||
console.log("Widget clicked");
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).then((newContent: string | undefined) => {
|
||||
if (newContent) {
|
||||
div.innerText = newContent;
|
||||
}
|
||||
this.client.focus();
|
||||
}).catch(console.error);
|
||||
},
|
||||
);
|
||||
]).catch(console.error);
|
||||
});
|
||||
} else {
|
||||
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
console.log("Button clicked:", button.description);
|
||||
this.client.system.localSyscall("system.invokeFunction", [
|
||||
button.invokeFunction,
|
||||
this.from,
|
||||
]).then((newContent: string | undefined) => {
|
||||
if (newContent) {
|
||||
div.innerText = newContent;
|
||||
}
|
||||
this.client.focus();
|
||||
}).catch(console.error);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// div.querySelectorAll("ul > li").forEach((el) => {
|
||||
// el.classList.add("sb-line-li-1", "sb-line-ul");
|
||||
// });
|
||||
}
|
||||
|
||||
get estimatedHeight(): number {
|
||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
||||
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||
// console.log("Calling estimated height", this.bodyText, cacheItem);
|
||||
return cacheItem ? cacheItem.height : -1;
|
||||
}
|
||||
@ -192,7 +229,7 @@ export class MarkdownWidget extends WidgetType {
|
||||
eq(other: WidgetType): boolean {
|
||||
return (
|
||||
other instanceof MarkdownWidget &&
|
||||
other.bodyText === this.bodyText
|
||||
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -218,3 +255,10 @@ function garbageCollectWidgets() {
|
||||
}
|
||||
|
||||
setInterval(garbageCollectWidgets, 5000);
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(
|
||||
/>/g,
|
||||
">",
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ const straightQuoteContexts = [
|
||||
"FrontMatterCode",
|
||||
"Attribute",
|
||||
"CommandLink",
|
||||
"TemplateDirective",
|
||||
];
|
||||
|
||||
// TODO: Add support for selection (put quotes around or create blockquote block?)
|
||||
|
@ -17,6 +17,7 @@ export function postScriptPrefacePlugin(
|
||||
undefined,
|
||||
editor,
|
||||
`top:${editor.currentPage}`,
|
||||
"",
|
||||
topCallback,
|
||||
"sb-markdown-top-widget",
|
||||
),
|
||||
@ -33,6 +34,7 @@ export function postScriptPrefacePlugin(
|
||||
undefined,
|
||||
editor,
|
||||
`bottom:${editor.currentPage}`,
|
||||
"",
|
||||
bottomCallback,
|
||||
"sb-markdown-bottom-widget",
|
||||
),
|
||||
|
@ -166,8 +166,9 @@ export function shouldRenderAsCode(
|
||||
if (mainSelection.empty) {
|
||||
return checkRangeOverlap(range, [mainSelection.from, mainSelection.to]);
|
||||
} else {
|
||||
// If the selection is encompassing the fenced code we render as code
|
||||
return checkRangeSubset([mainSelection.from, mainSelection.to], range);
|
||||
// If the selection is encompassing the fenced code we render as code, or vice versa
|
||||
return checkRangeSubset([mainSelection.from, mainSelection.to], range) ||
|
||||
checkRangeSubset(range, [mainSelection.from, mainSelection.to]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ export class PanelWidgetHook implements Hook<PanelWidgetT> {
|
||||
if (!functionDef.panelWidget) {
|
||||
continue;
|
||||
}
|
||||
if (!["top", "bottom"].includes(functionDef.panelWidget)) {
|
||||
if (!["top", "bottom", "frontmatter"].includes(functionDef.panelWidget)) {
|
||||
errors.push(
|
||||
`Panel widgets must be attached to either 'top' or 'bottom'.`,
|
||||
);
|
||||
|
@ -172,7 +172,8 @@
|
||||
color: var(--editor-heading-meta-color);
|
||||
}
|
||||
|
||||
.sb-hashtag {
|
||||
.sb-hashtag,
|
||||
.sb-markdown-frontmatter-widget span.hashtag {
|
||||
color: var(--editor-hashtag-color);
|
||||
background-color: var(--editor-hashtag-background-color);
|
||||
border: 1px solid var(--editor-hashtag-border-color);
|
||||
@ -313,6 +314,10 @@
|
||||
color: var(--editor-frontmatter-color);
|
||||
}
|
||||
|
||||
.sb-markdown-frontmatter-widget {
|
||||
background-color: var(--editor-frontmatter-background-color);
|
||||
}
|
||||
|
||||
.sb-frontmatter-marker {
|
||||
color: var(--editor-frontmatter-marker-color);
|
||||
}
|
||||
|
@ -429,7 +429,33 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sb-markdown-frontmatter-widget {
|
||||
margin-bottom: -1.3ch;
|
||||
padding: 8px;
|
||||
|
||||
.sb-banner {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
font-size: 80%;
|
||||
color: var(--editor-frontmatter-marker-color);
|
||||
}
|
||||
|
||||
span.hashtag {
|
||||
border-radius: 6px;
|
||||
padding: 0 3px;
|
||||
margin: 0 1px 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-markdown-frontmatter-widget+.sb-frontmatter {
|
||||
background-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.sb-markdown-widget,
|
||||
.sb-markdown-frontmatter-widget,
|
||||
.sb-markdown-top-widget:has(*),
|
||||
.sb-markdown-bottom-widget:has(*) {
|
||||
overflow-y: auto;
|
||||
@ -437,6 +463,7 @@
|
||||
border-radius: 5px;
|
||||
white-space: normal;
|
||||
position: relative;
|
||||
min-height: 48px;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@ -504,8 +531,7 @@
|
||||
background: var(--editor-widget-background-color);
|
||||
padding-inline: 3px;
|
||||
padding: 4px 0;
|
||||
// border-radius: 0 5px;
|
||||
|
||||
border-radius: 0 0 0 5px;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
@ -553,12 +579,6 @@
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.sb-frontmatter-marker {
|
||||
float: right;
|
||||
font-size: 80%;
|
||||
padding-right: 7px;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
// Give some breathing space at the bottom of the screen
|
||||
padding-bottom: 20em;
|
||||
|
@ -98,7 +98,7 @@ html {
|
||||
--editor-admonition-note-background-color: rgba(0, 184, 212, 0.1);
|
||||
--editor-admonition-warning-border-color: rgb(255, 145, 0);
|
||||
--editor-admonition-warning-background-color: rgba(255, 145, 0, 0.1);
|
||||
--editor-frontmatter-background-color: rgba(255, 246, 189, 0.5);
|
||||
--editor-frontmatter-background-color: rgba(255, 246, 189, 0.3);
|
||||
--editor-frontmatter-color: var(--subtle-color);
|
||||
--editor-frontmatter-marker-color: #89000080;
|
||||
--editor-widget-background-color: rgb(238, 238, 238);
|
||||
|
@ -6,6 +6,8 @@ release.
|
||||
_Not yet released, this will likely become 0.6.0._
|
||||
|
||||
* **Directives have now been removed** from the code base. Please use [[Live Queries]] and [[Live Templates]] instead. If you hadn’t migrated yet and want to auto migrate, downgrade your SilverBullet version to 0.5.11 (e.g. using the `zefhemel/silverbullet:0.5.11` docker image) and run the {[Directive: Convert Entire Space to Live/Templates]} command with that version.
|
||||
* Custom renderer for [[Frontmatter]], enabling... [[Live Frontmatter Templates]] to specify custom rendering (using [[Templates]] of course) — see some of the plugs pages (e.g. [[Plugs/Editor]], [[Plugs/Git]]) to see what you can do with this (template here: [[internal-template/plug-frontmatter]]).
|
||||
* Somewhat nicer rendering of {{templateVars}}.
|
||||
|
||||
---
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
---
|
||||
status: Complete
|
||||
tags: meta
|
||||
---
|
||||
Frontmatter is a common format to attach additional metadata (data about data) to markdown documents.
|
||||
|
||||
In SilverBullet, there are multiple ways to attach [[Metadata]] to a page; frontmatter is one of them.
|
||||
|
||||
You create it by starting your markdown document with `---` followed by [[YAML]] encoded attributes and then ending with `---` again. Followed by the regular body of your document.
|
||||
You create it by starting your markdown document with `---` followed by [[YAML]] encoded attributes and then ending with `---` again. Followed by the regular body of your document. This very page contains some frontmatter, click on it to see the underlying code.
|
||||
|
||||
Here is an example:
|
||||
Here is another example:
|
||||
|
||||
---
|
||||
status: Draft
|
||||
|
33
website/Live Frontmatter Templates.md
Normal file
33
website/Live Frontmatter Templates.md
Normal file
@ -0,0 +1,33 @@
|
||||
Live Frontmatter Templates allow you to override the default rendering of [[Frontmatter]] at the top of your pages with a custom template.
|
||||
|
||||
If you have no idea what that means or what you would use this for; you probably don’t need this feature. Don’t worry about it.
|
||||
|
||||
# Defining
|
||||
Live Frontmatter Templates follow the same pattern as other [[Templates]] with a few additional attributes:
|
||||
|
||||
* `tags`: should be set to `template` as for any other template
|
||||
* `type`: should be set to `frontmatter`
|
||||
* `where`: should contain an [[Live Queries$expression]] that evaluates to true for the _pages_ you would like to apply this Live Frontmatter Template to, usually this checks for a specific tag, but it can be any expression. Think of this as a `where` clause that should match for the pages this template is applied to.
|
||||
* `priority` (optional): in case you have multiple Live Frontmatter Templates that have matching `where` expression, the one with the priority set to the lowest number wins.
|
||||
|
||||
# Example
|
||||
The following Frontmatter Template applies to all pages tagged with `person` (see the `where`). It first lists all [[Frontmatter]] attributes, followed by a use of the [[!silverbullet.md/template/live/incoming]] template, showing all incomplete tasks that reference this particular page.
|
||||
|
||||
Indeed, you can use [[Live Queries]] and [[Live Templates]] here as well.
|
||||
|
||||
---
|
||||
tags: template
|
||||
type: frontmatter
|
||||
where: 'tags = "person"'
|
||||
---
|
||||
{{#each .}}**{{@key}}**: {{.}}
|
||||
{{/each}}
|
||||
## Incoming tasks
|
||||
```template
|
||||
page: "[[!silverbullet.md/template/live/incoming]]"
|
||||
```
|
||||
|
||||
## Plug frontmatter template
|
||||
This site uses the [[internal-template/plug-frontmatter]] template for pages tagged with `plug`, such as [[Plugs/Editor]], [[Plugs/Github]] and [[Plugs/Mermaid]].
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
#plug
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
|
||||
The `editor` plug implements foundational editor functionality for SilverBullet.
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
#plug
|
||||
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
The Emoji plug provides support for auto-completion of the `:emoji:` style syntax. It currently has support for the [15.1 emoji unicode standard](https://emojipedia.org/emoji-15.1). 🎉
|
@ -2,8 +2,10 @@
|
||||
uri: github:silverbulletmd/silverbullet-github/github.plug.js
|
||||
repo: https://github.com/silverbulletmd/silverbullet-github
|
||||
author: Zef Hemel
|
||||
shareSupport: true
|
||||
tags: plug
|
||||
---
|
||||
#plug #share-support
|
||||
|
||||
```template
|
||||
page: "[[!raw.githubusercontent.com/silverbulletmd/silverbullet-github/main/README]]"
|
||||
raw: true
|
||||
|
@ -1,5 +1,6 @@
|
||||
#plug
|
||||
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
SilverBullet has a generic indexing infrastructure for [[Objects]]. Pages are automatically index upon save, so about every second.
|
||||
|
||||
The [[Plugs/Index]] plug also defines syntax for [[Tags]].
|
||||
|
@ -1,5 +1,7 @@
|
||||
#share-support #plug
|
||||
|
||||
---
|
||||
tags: plug
|
||||
shareSupport: true
|
||||
---
|
||||
The Markdown plug provides support for various advanced Markdown features, specifically:
|
||||
|
||||
* {[Markdown Preview: Toggle]} preview
|
||||
|
@ -2,9 +2,9 @@
|
||||
uri: github:silverbulletmd/silverbullet-mattermost/mattermost.plug.json
|
||||
repo: https://github.com/silverbulletmd/silverbullet-mattermost
|
||||
author: Zef Hemel
|
||||
shareSupport: true
|
||||
tags: plug
|
||||
---
|
||||
#plug #share-support
|
||||
|
||||
> **warning** Unmaintained
|
||||
> This plug is currently not being maintained, it may break at any time
|
||||
|
||||
|
@ -5,7 +5,6 @@ uri: github:silverbulletmd/silverbullet-mermaid/mermaid.plug.js
|
||||
repo: https://github.com/silverbulletmd/silverbullet-mermaid
|
||||
author: Zef Hemel
|
||||
---
|
||||
|
||||
Example use:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
@ -2,8 +2,9 @@
|
||||
uri: github:m1lt0n/silverbullet-serendipity/serendipity.plug.json
|
||||
repo: https://github.com/m1lt0n/silverbullet-serendipity
|
||||
author: Pantelis Vratsalis
|
||||
tags: plug
|
||||
---
|
||||
#plug
|
||||
|
||||
```template
|
||||
page: "[[!raw.githubusercontent.com/m1lt0n/silverbullet-serendipity/main/README]]"
|
||||
raw: true
|
||||
|
@ -1,13 +1,12 @@
|
||||
---
|
||||
repo: https://github.com/silverbulletmd/silverbullet
|
||||
tags: plug
|
||||
---
|
||||
#plug
|
||||
|
||||
The Share plug provides infrastructure for sharing pages outside of your space. It standardizes the {[Share: Publish]} (bound to `Cmd-s` or `Ctrl-s`) to publish the current page to all share providers specified under the `$share` key in [[Frontmatter]].
|
||||
|
||||
See the [original RFC](https://github.com/silverbulletmd/silverbullet/discussions/117) for implementation details.
|
||||
|
||||
Specific implementations for sharing are implemented in other plugs, specifically:
|
||||
```query
|
||||
share-support render [[template/page]]
|
||||
plug where shareSupport = true render [[template/page]]
|
||||
```
|
||||
|
@ -1,5 +1,6 @@
|
||||
#plug
|
||||
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
The Tasks plug implements task support in SilverBullet.
|
||||
|
||||
## Task states
|
||||
|
@ -1,5 +1,6 @@
|
||||
#plug
|
||||
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
The [[Plugs/Template]] plug implements a few templating mechanisms.
|
||||
|
||||
# Daily Note
|
||||
|
@ -5,6 +5,7 @@ There are two general uses for templates:
|
||||
1. _Live_ uses, where page content is dynamically updated based on templates:
|
||||
* [[Live Queries]]
|
||||
* [[Live Templates]]
|
||||
* [[Live Frontmatter Templates]]
|
||||
2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page:
|
||||
* [[Slash Templates]]
|
||||
* [[Page Templates]]
|
||||
@ -24,7 +25,7 @@ Tagging a page with a `#template` tag (either in the [[Frontmatter]] or using a
|
||||
[[Frontmatter]] has special meaning in templates. The following attributes are used:
|
||||
|
||||
* `tags`: should always be set to `template`
|
||||
* `type` (optional): should be set to `page` for [[Page Templates]]
|
||||
* `type` (optional): should be set to `page` for [[Page Templates]] and to `frontmatter` for [[Live Frontmatter Templates]]
|
||||
* `trigger` (optional): defines the slash command name for [[Slash Templates]]
|
||||
* `displayName` (optional): defines an alternative name to use when e.g. showing the template picker for [[Page Templates]], or when template completing a `render` clause in a [[Live Templates]].
|
||||
* `pageName` (optional, [[Page Templates]] only): specify a (template for a) page name.
|
||||
|
7
website/internal-template/plug-frontmatter.md
Normal file
7
website/internal-template/plug-frontmatter.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
tags: template
|
||||
type: frontmatter
|
||||
where: 'tags = "plug"'
|
||||
---
|
||||
{{#if author}}This page documents a [[Plugs|plug]] created by **{{author}}**. [Repository]({{repo}}).{{else}}This page documents a [[Plugs|plug]] built into SilverBullet.{{/if}}
|
||||
{{#if shareSupport}}_This plug supports [[Plugs/Share]]_{{/if}}
|
Loading…
Reference in New Issue
Block a user