@@ -15,3 +15,4 @@ node_modules
|
|||||||
test_space
|
test_space
|
||||||
silverbullet
|
silverbullet
|
||||||
deploy.json
|
deploy.json
|
||||||
|
*.generated
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+8
-1
@@ -25,7 +25,10 @@ import {
|
|||||||
xmlLanguage,
|
xmlLanguage,
|
||||||
yamlLanguage,
|
yamlLanguage,
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import { highlightingQueryParser } from "./markdown_parser/parser.ts";
|
import {
|
||||||
|
expressionParser,
|
||||||
|
highlightingQueryParser,
|
||||||
|
} from "./markdown_parser/parser.ts";
|
||||||
|
|
||||||
export const builtinLanguages: Record<string, Language> = {
|
export const builtinLanguages: Record<string, Language> = {
|
||||||
"meta": StreamLanguage.define(yamlLanguage),
|
"meta": StreamLanguage.define(yamlLanguage),
|
||||||
@@ -81,6 +84,10 @@ export const builtinLanguages: Record<string, Language> = {
|
|||||||
name: "query",
|
name: "query",
|
||||||
parser: highlightingQueryParser,
|
parser: highlightingQueryParser,
|
||||||
}),
|
}),
|
||||||
|
"expression": LRLanguage.define({
|
||||||
|
name: "expression",
|
||||||
|
parser: expressionParser,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function languageFor(name: string): Language | null {
|
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",
|
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],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Program":[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
|
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",
|
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],
|
tokenizers: [0, 1, 2],
|
||||||
topRules: {"Program":[0,1]},
|
topRules: {"Program":[0,1]},
|
||||||
specialized: [{term: 5, get: value => spec_Identifier[value] || -1}],
|
specialized: [{term: 5, get: (value) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 798
|
tokenPrec: 798
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -136,3 +136,9 @@ Deno.test("Test command link arguments", () => {
|
|||||||
const args2 = findNodeOfType(commands[1], "CommandLinkArgs");
|
const args2 = findNodeOfType(commands[1], "CommandLinkArgs");
|
||||||
assertEquals(args2!.children![0].text, '"other", "args", 123');
|
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" };
|
const HighlightDelim = { resolve: "Highlight", mark: "HighlightMark" };
|
||||||
|
|
||||||
export const Highlight: MarkdownConfig = {
|
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 attributeStartRegex = /^\[([\w\$]+)(::?\s*)/;
|
||||||
|
|
||||||
export const Attribute: MarkdownConfig = {
|
export const Attribute: MarkdownConfig = {
|
||||||
@@ -355,6 +389,7 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language {
|
|||||||
TaskList,
|
TaskList,
|
||||||
Comment,
|
Comment,
|
||||||
Highlight,
|
Highlight,
|
||||||
|
TemplateDirective,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
Table,
|
Table,
|
||||||
...mdExtensions.map(mdExtensionSyntaxConfig),
|
...mdExtensions.map(mdExtensionSyntaxConfig),
|
||||||
|
|||||||
@@ -40,5 +40,17 @@ export function handlebarHelpers() {
|
|||||||
nextWeek.setDate(nextWeek.getDate() + 7);
|
nextWeek.setDate(nextWeek.getDate() + 7);
|
||||||
return niceDate(nextWeek);
|
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 { expandPropertyNames } from "$sb/lib/json.ts";
|
||||||
import type { BuiltinSettings } from "../web/types.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>) {
|
export function safeRun(fn: () => Promise<void>) {
|
||||||
fn().catch((e) => {
|
fn().catch((e) => {
|
||||||
console.error(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() {
|
export function isMacLike() {
|
||||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
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
|
// TODO: This is naive, may be better to use a proper parser
|
||||||
const yamlSettingsRegex = /```yaml([^`]+)```/;
|
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): {
|
export function parseYamlSettings(settingsMarkdown: string): {
|
||||||
[key: string]: any;
|
[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(
|
export async function ensureSettingsAndIndex(
|
||||||
space: SpacePrimitives,
|
space: SpacePrimitives,
|
||||||
): Promise<BuiltinSettings> {
|
): Promise<BuiltinSettings> {
|
||||||
|
|||||||
+16
-36
@@ -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 type { Query, QueryExpression } from "$sb/types.ts";
|
||||||
|
import { language } from "$sb/syscalls.ts";
|
||||||
|
|
||||||
export function astToKvQuery(
|
export function astToKvQuery(
|
||||||
node: AST,
|
node: AST,
|
||||||
@@ -20,10 +21,10 @@ export function astToKvQuery(
|
|||||||
query.filter = [
|
query.filter = [
|
||||||
"and",
|
"and",
|
||||||
query.filter,
|
query.filter,
|
||||||
expressionToKvQueryFilter(clause[2]),
|
expressionToKvQueryExpression(clause[2]),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
query.filter = expressionToKvQueryFilter(clause[2]);
|
query.filter = expressionToKvQueryExpression(clause[2]);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -123,18 +124,18 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression {
|
|||||||
}
|
}
|
||||||
case "BinExpression": {
|
case "BinExpression": {
|
||||||
const lval = expressionToKvQueryExpression(node[1]);
|
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]);
|
const val = expressionToKvQueryExpression(node[3]);
|
||||||
return [binOp as any, lval, val];
|
return [binOp as any, lval, val];
|
||||||
}
|
}
|
||||||
case "LogicalExpression": {
|
case "LogicalExpression": {
|
||||||
const op1 = expressionToKvQueryFilter(node[1]);
|
const op1 = expressionToKvQueryExpression(node[1]);
|
||||||
const op = node[2];
|
const op = node[2];
|
||||||
const op2 = expressionToKvQueryFilter(node[3]);
|
const op2 = expressionToKvQueryExpression(node[3]);
|
||||||
return [op[1] as any, op1, op2];
|
return [op[1] as any, op1, op2];
|
||||||
}
|
}
|
||||||
case "ParenthesizedExpression": {
|
case "ParenthesizedExpression": {
|
||||||
return expressionToKvQueryFilter(node[2]);
|
return expressionToKvQueryExpression(node[2]);
|
||||||
}
|
}
|
||||||
case "Call": {
|
case "Call": {
|
||||||
// console.log("Call", node);
|
// console.log("Call", node);
|
||||||
@@ -151,33 +152,12 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression {
|
|||||||
throw new Error(`Not supported: ${node[0]}`);
|
throw new Error(`Not supported: ${node[0]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export async function parseQuery(query: string): Promise<Query> {
|
||||||
function expressionToKvQueryFilter(
|
const queryAST = parseTreeToAST(
|
||||||
node: AST,
|
await language.parseLanguage(
|
||||||
): QueryExpression {
|
"query",
|
||||||
const [expressionType] = node;
|
query,
|
||||||
if (expressionType === "Expression") {
|
),
|
||||||
return expressionToKvQueryFilter(node[1]);
|
);
|
||||||
}
|
return astToKvQuery(queryAST[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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ Deno.test("Test query parser", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
astToKvQuery(wrapQueryParse(`page where true`)!),
|
||||||
|
{
|
||||||
|
querySource: "page",
|
||||||
|
filter: ["boolean", true],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
astToKvQuery(wrapQueryParse(`page where name =~ /test/`)!),
|
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";
|
import { FunctionMap, KV, Query, QueryExpression } from "$sb/types.ts";
|
||||||
|
|
||||||
export function evalQueryExpression(
|
export function evalQueryExpression(
|
||||||
|
|||||||
@@ -134,9 +134,11 @@ export type CodeWidgetContent = {
|
|||||||
markdown?: string;
|
markdown?: string;
|
||||||
script?: string;
|
script?: string;
|
||||||
buttons?: CodeWidgetButton[];
|
buttons?: CodeWidgetButton[];
|
||||||
|
banner?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CodeWidgetButton = {
|
export type CodeWidgetButton = {
|
||||||
|
widgetTarget?: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
svg: string;
|
svg: string;
|
||||||
invokeFunction: string;
|
invokeFunction: string;
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export const builtins: Record<string, Record<string, string>> = {
|
|||||||
ref: "!string",
|
ref: "!string",
|
||||||
name: "!string",
|
name: "!string",
|
||||||
displayName: "string",
|
displayName: "string",
|
||||||
aliases: "array",
|
aliases: "string[]",
|
||||||
created: "!date",
|
created: "!date",
|
||||||
lastModified: "!date",
|
lastModified: "!date",
|
||||||
perm: "!rw|ro",
|
perm: "!rw|ro",
|
||||||
contentType: "!string",
|
contentType: "!string",
|
||||||
size: "!number",
|
size: "!number",
|
||||||
tags: "array",
|
tags: "string[]",
|
||||||
},
|
},
|
||||||
task: {
|
task: {
|
||||||
ref: "!string",
|
ref: "!string",
|
||||||
@@ -27,11 +27,11 @@ export const builtins: Record<string, Record<string, string>> = {
|
|||||||
state: "!string",
|
state: "!string",
|
||||||
deadline: "string",
|
deadline: "string",
|
||||||
pos: "!number",
|
pos: "!number",
|
||||||
tags: "array",
|
tags: "string[]",
|
||||||
},
|
},
|
||||||
taskstate: {
|
taskstate: {
|
||||||
ref: "!string",
|
ref: "!string",
|
||||||
tags: "!array",
|
tags: "!string[]",
|
||||||
state: "!string",
|
state: "!string",
|
||||||
count: "!number",
|
count: "!number",
|
||||||
page: "!string",
|
page: "!string",
|
||||||
@@ -76,6 +76,7 @@ export const builtins: Record<string, Record<string, string>> = {
|
|||||||
pos: "!number",
|
pos: "!number",
|
||||||
type: "string",
|
type: "string",
|
||||||
trigger: "string",
|
trigger: "string",
|
||||||
|
forTags: "string[]",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -187,3 +187,12 @@ functions:
|
|||||||
path: lint.ts:lintYAML
|
path: lint.ts:lintYAML
|
||||||
events:
|
events:
|
||||||
- editor:lint
|
- editor:lint
|
||||||
|
|
||||||
|
renderFrontmatterWidget:
|
||||||
|
path: frontmatter.ts:renderFrontmatterWidget
|
||||||
|
env: client
|
||||||
|
panelWidget: frontmatter
|
||||||
|
|
||||||
|
editFrontmatter:
|
||||||
|
path: frontmatter.ts:editFrontmatter
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export async function renderTOC(): Promise<CodeWidgetContent | null> {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (headers.length < headerThreshold) {
|
if (headers.length < headerThreshold) {
|
||||||
console.log("Not enough headers, not showing TOC", headers.length);
|
// Not enough headers, not showing TOC
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// console.log("Headers", headers);
|
// console.log("Headers", headers);
|
||||||
|
|||||||
@@ -267,7 +267,10 @@ function render(
|
|||||||
}
|
}
|
||||||
case "Hashtag":
|
case "Hashtag":
|
||||||
return {
|
return {
|
||||||
name: "strong",
|
name: "span",
|
||||||
|
attrs: {
|
||||||
|
class: "hashtag",
|
||||||
|
},
|
||||||
body: t.children![0].text!,
|
body: t.children![0].text!,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -414,6 +417,16 @@ function render(
|
|||||||
case "Entity":
|
case "Entity":
|
||||||
return t.children![0].text!;
|
return t.children![0].text!;
|
||||||
|
|
||||||
|
case "TemplateDirective": {
|
||||||
|
return {
|
||||||
|
name: "span",
|
||||||
|
attrs: {
|
||||||
|
class: "template-directive",
|
||||||
|
},
|
||||||
|
body: renderToText(t),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
case undefined:
|
case undefined:
|
||||||
return t.text!;
|
return t.text!;
|
||||||
|
|||||||
+9
-20
@@ -1,11 +1,7 @@
|
|||||||
import type { LintEvent } from "$sb/app_event.ts";
|
import type { LintEvent } from "$sb/app_event.ts";
|
||||||
import { events, language, space } from "$sb/syscalls.ts";
|
import { events, space } from "$sb/syscalls.ts";
|
||||||
import {
|
import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts";
|
||||||
findNodeOfType,
|
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||||
parseTreeToAST,
|
|
||||||
traverseTreeAsync,
|
|
||||||
} from "$sb/lib/tree.ts";
|
|
||||||
import { astToKvQuery } from "$sb/lib/parse-query.ts";
|
|
||||||
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
|
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
|
||||||
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
|
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
|
||||||
import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts";
|
import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts";
|
||||||
@@ -18,13 +14,9 @@ export async function widget(
|
|||||||
const pageObject = await loadPageObject(pageName);
|
const pageObject = await loadPageObject(pageName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const queryAST = parseTreeToAST(
|
const parsedQuery = await parseQuery(
|
||||||
await language.parseLanguage(
|
await replaceTemplateVars(bodyText, pageObject),
|
||||||
"query",
|
|
||||||
await replaceTemplateVars(bodyText, pageObject),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const parsedQuery = astToKvQuery(queryAST[1]);
|
|
||||||
|
|
||||||
if (!parsedQuery.limit) {
|
if (!parsedQuery.limit) {
|
||||||
parsedQuery.limit = ["number", 1000];
|
parsedQuery.limit = ["number", 1000];
|
||||||
@@ -115,13 +107,10 @@ export async function lintQuery(
|
|||||||
}
|
}
|
||||||
const bodyText = codeText.children![0].text!;
|
const bodyText = codeText.children![0].text!;
|
||||||
try {
|
try {
|
||||||
const queryAST = parseTreeToAST(
|
const parsedQuery = await parseQuery(
|
||||||
await language.parseLanguage(
|
await replaceTemplateVars(bodyText, pageObject),
|
||||||
"query",
|
|
||||||
await replaceTemplateVars(bodyText, pageObject),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const parsedQuery = astToKvQuery(queryAST[1]);
|
|
||||||
const allSources = await allQuerySources();
|
const allSources = await allQuerySources();
|
||||||
if (
|
if (
|
||||||
parsedQuery.querySource &&
|
parsedQuery.querySource &&
|
||||||
@@ -141,7 +130,7 @@ export async function lintQuery(
|
|||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await space.getPageMeta(templatePage);
|
await space.getPageMeta(templatePage);
|
||||||
} catch (e: any) {
|
} catch {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
from: codeText.from!,
|
from: codeText.from!,
|
||||||
to: codeText.to!,
|
to: codeText.to!,
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { ObjectValue } from "$sb/types.ts";
|
import { ObjectValue } from "$sb/types.ts";
|
||||||
|
|
||||||
export type TemplateFrontmatter = {
|
export type TemplateFrontmatter = {
|
||||||
trigger?: string; // slash command name
|
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
type?: "page";
|
type?: "page";
|
||||||
// Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string
|
// Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string
|
||||||
frontmatter?: Record<string, any> | 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>;
|
export type TemplateObject = ObjectValue<TemplateFrontmatter>;
|
||||||
|
|||||||
+10
-3
@@ -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) {
|
if (v === undefined) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (typeof v === "string") {
|
if (typeof v === "string") {
|
||||||
return v.replaceAll("\n", " ").replaceAll("|", "\\|");
|
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;
|
return "" + v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nicely format an array of JSON objects as a Markdown table
|
// Nicely format an array of JSON objects as a Markdown table
|
||||||
export function jsonToMDTable(
|
export function jsonToMDTable(
|
||||||
jsonArray: any[],
|
jsonArray: any[],
|
||||||
valueTransformer: (k: string, v: any) => string = defaultJsonTransformer,
|
valueTransformer: (v: any) => string = defaultJsonTransformer,
|
||||||
): string {
|
): string {
|
||||||
const headers = new Set<string>();
|
const headers = new Set<string>();
|
||||||
for (const entry of jsonArray) {
|
for (const entry of jsonArray) {
|
||||||
@@ -79,7 +86,7 @@ export function jsonToMDTable(
|
|||||||
for (const val of jsonArray) {
|
for (const val of jsonArray) {
|
||||||
const el = [];
|
const el = [];
|
||||||
for (const prop of headerList) {
|
for (const prop of headerList) {
|
||||||
const s = valueTransformer(prop, val[prop]);
|
const s = valueTransformer(val[prop]);
|
||||||
el.push(s);
|
el.push(s);
|
||||||
}
|
}
|
||||||
lines.push("|" + el.join("|") + "|");
|
lines.push("|" + el.join("|") + "|");
|
||||||
|
|||||||
+6
-3
@@ -1,9 +1,12 @@
|
|||||||
#!/bin/sh -e
|
#!/bin/sh -e
|
||||||
|
|
||||||
QUERY_GRAMMAR=common/markdown_parser/query.grammar
|
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
|
echo "@top Program { Expression }" > $EXPRESSION_GRAMMAR
|
||||||
tail -n +2 $QUERY_GRAMMAR >> $EXPRESSION_GRAMMAR
|
tail -n +2 $QUERY_GRAMMAR >> $EXPRESSION_GRAMMAR
|
||||||
|
|
||||||
npx lezer-generator $QUERY_GRAMMAR -o common/markdown_parser/parse-query.js
|
deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $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 $EXPRESSION_GRAMMAR -o common/markdown_parser/parse-expression.js
|
||||||
+2
-2
@@ -289,7 +289,7 @@ export class Client {
|
|||||||
|
|
||||||
this.pageNavigator.subscribe(
|
this.pageNavigator.subscribe(
|
||||||
async (pageName, pos: number | string | undefined) => {
|
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);
|
const stateRestored = await this.loadPage(pageName, pos === undefined);
|
||||||
if (pos) {
|
if (pos) {
|
||||||
@@ -1012,7 +1012,6 @@ export class Client {
|
|||||||
|
|
||||||
// Widget and image height caching
|
// Widget and image height caching
|
||||||
private widgetCache = new LimitedMap<WidgetCacheItem>(100); // bodyText -> WidgetCacheItem
|
private widgetCache = new LimitedMap<WidgetCacheItem>(100); // bodyText -> WidgetCacheItem
|
||||||
private imageHeightCache = new LimitedMap<number>(100); // url -> height
|
|
||||||
private widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
|
private widgetHeightCache = new LimitedMap<number>(100); // bodytext -> height
|
||||||
|
|
||||||
async loadCaches() {
|
async loadCaches() {
|
||||||
@@ -1066,4 +1065,5 @@ type WidgetCacheItem = {
|
|||||||
height: number;
|
height: number;
|
||||||
html: string;
|
html: string;
|
||||||
buttons?: CodeWidgetButton[];
|
buttons?: CodeWidgetButton[];
|
||||||
|
banner?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+30
-68
@@ -1,76 +1,38 @@
|
|||||||
import { Decoration, EditorState, foldedRanges, syntaxTree } from "../deps.ts";
|
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
|
||||||
import {
|
import {
|
||||||
decoratorStateField,
|
decoratorStateField,
|
||||||
HtmlWidget,
|
|
||||||
invisibleDecoration,
|
invisibleDecoration,
|
||||||
isCursorInRange,
|
isCursorInRange,
|
||||||
} from "./util.ts";
|
} 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() {
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-9
@@ -12,33 +12,35 @@ import { taskListPlugin } from "./task.ts";
|
|||||||
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
|
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
|
||||||
import { cleanCommandLinkPlugin } from "./command_link.ts";
|
import { cleanCommandLinkPlugin } from "./command_link.ts";
|
||||||
import { fencedCodePlugin } from "./fenced_code.ts";
|
import { fencedCodePlugin } from "./fenced_code.ts";
|
||||||
|
import { frontmatterPlugin } from "./frontmatter.ts";
|
||||||
|
|
||||||
export function cleanModePlugins(editor: Client) {
|
export function cleanModePlugins(client: Client) {
|
||||||
return [
|
return [
|
||||||
linkPlugin(editor),
|
linkPlugin(client),
|
||||||
blockquotePlugin(),
|
blockquotePlugin(),
|
||||||
admonitionPlugin(editor),
|
admonitionPlugin(client),
|
||||||
hideMarksPlugin(),
|
hideMarksPlugin(),
|
||||||
hideHeaderMarkPlugin(),
|
hideHeaderMarkPlugin(),
|
||||||
cleanBlockPlugin(),
|
cleanBlockPlugin(),
|
||||||
fencedCodePlugin(editor),
|
frontmatterPlugin(client),
|
||||||
|
fencedCodePlugin(client),
|
||||||
taskListPlugin({
|
taskListPlugin({
|
||||||
// TODO: Move this logic elsewhere?
|
// TODO: Move this logic elsewhere?
|
||||||
onCheckboxClick: (pos) => {
|
onCheckboxClick: (pos) => {
|
||||||
const clickEvent: ClickEvent = {
|
const clickEvent: ClickEvent = {
|
||||||
page: editor.currentPage!,
|
page: client.currentPage!,
|
||||||
altKey: false,
|
altKey: false,
|
||||||
ctrlKey: false,
|
ctrlKey: false,
|
||||||
metaKey: false,
|
metaKey: false,
|
||||||
pos: pos,
|
pos: pos,
|
||||||
};
|
};
|
||||||
// Propagate click event from checkbox
|
// Propagate click event from checkbox
|
||||||
editor.dispatchAppEvent("page:click", clickEvent);
|
client.dispatchAppEvent("page:click", clickEvent);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
listBulletPlugin(),
|
listBulletPlugin(),
|
||||||
tablePlugin(editor),
|
tablePlugin(client),
|
||||||
cleanWikiLinkPlugin(editor),
|
cleanWikiLinkPlugin(client),
|
||||||
cleanCommandLinkPlugin(editor),
|
cleanCommandLinkPlugin(client),
|
||||||
] as Extension[];
|
] 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"
|
const widget = renderMode === "markdown"
|
||||||
? new MarkdownWidget(
|
? new MarkdownWidget(
|
||||||
from + lineStrings[0].length + 1,
|
from + lineStrings[0].length + 1,
|
||||||
editor,
|
editor,
|
||||||
lineStrings.slice(1, lineStrings.length - 1).join("\n"),
|
`widget:${editor.currentPage}:${bodyText}`,
|
||||||
|
bodyText,
|
||||||
codeWidgetCallback,
|
codeWidgetCallback,
|
||||||
"sb-markdown-widget",
|
"sb-markdown-widget",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 { resolveAttachmentPath } from "$sb/lib/resolve.ts";
|
||||||
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||||
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||||
import { renderToText } from "$sb/lib/tree.ts";
|
|
||||||
|
|
||||||
const activeWidgets = new Set<MarkdownWidget>();
|
const activeWidgets = new Set<MarkdownWidget>();
|
||||||
|
|
||||||
export class MarkdownWidget extends WidgetType {
|
export class MarkdownWidget extends WidgetType {
|
||||||
renderedMarkdown?: string;
|
|
||||||
public dom?: HTMLElement;
|
public dom?: HTMLElement;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly from: number | undefined,
|
readonly from: number | undefined,
|
||||||
readonly client: Client,
|
readonly client: Client,
|
||||||
|
readonly cacheKey: string,
|
||||||
readonly bodyText: string,
|
readonly bodyText: string,
|
||||||
readonly codeWidgetCallback: CodeWidgetCallback,
|
readonly codeWidgetCallback: CodeWidgetCallback,
|
||||||
readonly className: string,
|
readonly className: string,
|
||||||
@@ -26,11 +25,12 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
toDOM(): HTMLElement {
|
toDOM(): HTMLElement {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = this.className;
|
div.className = this.className;
|
||||||
const cacheItem = this.client.getWidgetCache(this.bodyText);
|
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||||
if (cacheItem) {
|
if (cacheItem) {
|
||||||
div.innerHTML = this.wrapHtml(
|
div.innerHTML = this.wrapHtml(
|
||||||
cacheItem.html,
|
cacheItem.html,
|
||||||
cacheItem.buttons,
|
cacheItem.buttons || [],
|
||||||
|
cacheItem.banner,
|
||||||
);
|
);
|
||||||
this.attachListeners(div, cacheItem.buttons);
|
this.attachListeners(div, cacheItem.buttons);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
if (!widgetContent) {
|
if (!widgetContent) {
|
||||||
div.innerHTML = "";
|
div.innerHTML = "";
|
||||||
this.client.setWidgetCache(
|
this.client.setWidgetCache(
|
||||||
this.bodyText,
|
this.cacheKey,
|
||||||
{ height: div.clientHeight, html: "" },
|
{ height: div.clientHeight, html: "" },
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -73,8 +73,6 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
this.client.currentPage,
|
this.client.currentPage,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
// Used for the source button
|
|
||||||
this.renderedMarkdown = renderToText(mdTree);
|
|
||||||
|
|
||||||
const html = renderMarkdownToHtml(mdTree, {
|
const html = renderMarkdownToHtml(mdTree, {
|
||||||
// Annotate every element with its position so we can use it to put
|
// Annotate every element with its position so we can use it to put
|
||||||
@@ -97,27 +95,48 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
// HTML still same as in cache, no need to re-render
|
// HTML still same as in cache, no need to re-render
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
div.innerHTML = this.wrapHtml(html, widgetContent.buttons);
|
div.innerHTML = this.wrapHtml(
|
||||||
|
html,
|
||||||
|
widgetContent.buttons || [],
|
||||||
|
widgetContent.banner,
|
||||||
|
);
|
||||||
this.attachListeners(div, widgetContent.buttons);
|
this.attachListeners(div, widgetContent.buttons);
|
||||||
|
|
||||||
// Let's give it a tick, then measure and cache
|
// Let's give it a tick, then measure and cache
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.client.setWidgetCache(
|
this.client.setWidgetCache(
|
||||||
this.bodyText,
|
this.cacheKey,
|
||||||
{ height: div.offsetHeight, html, buttons: widgetContent.buttons },
|
{
|
||||||
|
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[]) {
|
private wrapHtml(
|
||||||
if (!buttons) {
|
html: string,
|
||||||
return html;
|
buttons: CodeWidgetButton[],
|
||||||
|
banner?: string,
|
||||||
|
) {
|
||||||
|
if (!html) {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
return `<div class="button-bar">${
|
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> `
|
`<button data-button="${idx}" title="${button.description}">${button.svg}</button> `
|
||||||
).join("")
|
).join("")
|
||||||
}</div>${html}`;
|
}</div>${
|
||||||
|
banner ? `<div class="sb-banner">${escapeHtml(banner)}</div>` : ""
|
||||||
|
}${html}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) {
|
private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) {
|
||||||
@@ -126,6 +145,7 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
// Override default click behavior with a local navigate (faster)
|
// Override default click behavior with a local navigate (faster)
|
||||||
el.addEventListener("click", (e) => {
|
el.addEventListener("click", (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
const [pageName, pos] = el.dataset.ref!.split(/[$@]/);
|
||||||
if (pos && pos.match(/^\d+$/)) {
|
if (pos && pos.match(/^\d+$/)) {
|
||||||
this.client.navigate(pageName, +pos);
|
this.client.navigate(pageName, +pos);
|
||||||
@@ -138,9 +158,18 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
// Implement task toggling
|
// Implement task toggling
|
||||||
div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => {
|
div.querySelectorAll("span[data-external-task-ref]").forEach((el: any) => {
|
||||||
const taskRef = el.dataset.externalTaskRef;
|
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",
|
"change",
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
|
e.stopPropagation();
|
||||||
const oldState = e.target.dataset.state;
|
const oldState = e.target.dataset.state;
|
||||||
const newState = oldState === " " ? "x" : " ";
|
const newState = oldState === " " ? "x" : " ";
|
||||||
// Update state in DOM as well for future toggles
|
// 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++) {
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
const button = buttons[i];
|
const button = buttons[i];
|
||||||
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
if (button.widgetTarget) {
|
||||||
"click",
|
div.addEventListener("click", () => {
|
||||||
() => {
|
console.log("Widget clicked");
|
||||||
console.log("Button clicked:", button.description);
|
|
||||||
this.client.system.localSyscall("system.invokeFunction", [
|
this.client.system.localSyscall("system.invokeFunction", [
|
||||||
button.invokeFunction,
|
button.invokeFunction,
|
||||||
this.from,
|
this.from,
|
||||||
]).then((newContent: string | undefined) => {
|
]).catch(console.error);
|
||||||
if (newContent) {
|
});
|
||||||
div.innerText = newContent;
|
} else {
|
||||||
}
|
div.querySelector(`button[data-button="${i}"]`)!.addEventListener(
|
||||||
this.client.focus();
|
"click",
|
||||||
}).catch(console.error);
|
(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 {
|
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);
|
// console.log("Calling estimated height", this.bodyText, cacheItem);
|
||||||
return cacheItem ? cacheItem.height : -1;
|
return cacheItem ? cacheItem.height : -1;
|
||||||
}
|
}
|
||||||
@@ -192,7 +229,7 @@ export class MarkdownWidget extends WidgetType {
|
|||||||
eq(other: WidgetType): boolean {
|
eq(other: WidgetType): boolean {
|
||||||
return (
|
return (
|
||||||
other instanceof MarkdownWidget &&
|
other instanceof MarkdownWidget &&
|
||||||
other.bodyText === this.bodyText
|
other.bodyText === this.bodyText && other.cacheKey === this.cacheKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,3 +255,10 @@ function garbageCollectWidgets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setInterval(garbageCollectWidgets, 5000);
|
setInterval(garbageCollectWidgets, 5000);
|
||||||
|
|
||||||
|
function escapeHtml(text: string) {
|
||||||
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(
|
||||||
|
/>/g,
|
||||||
|
">",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const straightQuoteContexts = [
|
|||||||
"FrontMatterCode",
|
"FrontMatterCode",
|
||||||
"Attribute",
|
"Attribute",
|
||||||
"CommandLink",
|
"CommandLink",
|
||||||
|
"TemplateDirective",
|
||||||
];
|
];
|
||||||
|
|
||||||
// TODO: Add support for selection (put quotes around or create blockquote block?)
|
// TODO: Add support for selection (put quotes around or create blockquote block?)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function postScriptPrefacePlugin(
|
|||||||
undefined,
|
undefined,
|
||||||
editor,
|
editor,
|
||||||
`top:${editor.currentPage}`,
|
`top:${editor.currentPage}`,
|
||||||
|
"",
|
||||||
topCallback,
|
topCallback,
|
||||||
"sb-markdown-top-widget",
|
"sb-markdown-top-widget",
|
||||||
),
|
),
|
||||||
@@ -33,6 +34,7 @@ export function postScriptPrefacePlugin(
|
|||||||
undefined,
|
undefined,
|
||||||
editor,
|
editor,
|
||||||
`bottom:${editor.currentPage}`,
|
`bottom:${editor.currentPage}`,
|
||||||
|
"",
|
||||||
bottomCallback,
|
bottomCallback,
|
||||||
"sb-markdown-bottom-widget",
|
"sb-markdown-bottom-widget",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -166,8 +166,9 @@ export function shouldRenderAsCode(
|
|||||||
if (mainSelection.empty) {
|
if (mainSelection.empty) {
|
||||||
return checkRangeOverlap(range, [mainSelection.from, mainSelection.to]);
|
return checkRangeOverlap(range, [mainSelection.from, mainSelection.to]);
|
||||||
} else {
|
} else {
|
||||||
// If the selection is encompassing the fenced code we render as code
|
// If the selection is encompassing the fenced code we render as code, or vice versa
|
||||||
return checkRangeSubset([mainSelection.from, mainSelection.to], range);
|
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) {
|
if (!functionDef.panelWidget) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!["top", "bottom"].includes(functionDef.panelWidget)) {
|
if (!["top", "bottom", "frontmatter"].includes(functionDef.panelWidget)) {
|
||||||
errors.push(
|
errors.push(
|
||||||
`Panel widgets must be attached to either 'top' or 'bottom'.`,
|
`Panel widgets must be attached to either 'top' or 'bottom'.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -172,7 +172,8 @@
|
|||||||
color: var(--editor-heading-meta-color);
|
color: var(--editor-heading-meta-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-hashtag {
|
.sb-hashtag,
|
||||||
|
.sb-markdown-frontmatter-widget span.hashtag {
|
||||||
color: var(--editor-hashtag-color);
|
color: var(--editor-hashtag-color);
|
||||||
background-color: var(--editor-hashtag-background-color);
|
background-color: var(--editor-hashtag-background-color);
|
||||||
border: 1px solid var(--editor-hashtag-border-color);
|
border: 1px solid var(--editor-hashtag-border-color);
|
||||||
@@ -313,6 +314,10 @@
|
|||||||
color: var(--editor-frontmatter-color);
|
color: var(--editor-frontmatter-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sb-markdown-frontmatter-widget {
|
||||||
|
background-color: var(--editor-frontmatter-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
.sb-frontmatter-marker {
|
.sb-frontmatter-marker {
|
||||||
color: var(--editor-frontmatter-marker-color);
|
color: var(--editor-frontmatter-marker-color);
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-8
@@ -429,7 +429,33 @@
|
|||||||
margin-top: 10px;
|
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-widget,
|
||||||
|
.sb-markdown-frontmatter-widget,
|
||||||
.sb-markdown-top-widget:has(*),
|
.sb-markdown-top-widget:has(*),
|
||||||
.sb-markdown-bottom-widget:has(*) {
|
.sb-markdown-bottom-widget:has(*) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -437,6 +463,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 48px;
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
@@ -504,8 +531,7 @@
|
|||||||
background: var(--editor-widget-background-color);
|
background: var(--editor-widget-background-color);
|
||||||
padding-inline: 3px;
|
padding-inline: 3px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
// border-radius: 0 5px;
|
border-radius: 0 0 0 5px;
|
||||||
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -553,12 +579,6 @@
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-frontmatter-marker {
|
|
||||||
float: right;
|
|
||||||
font-size: 80%;
|
|
||||||
padding-right: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-scroller {
|
.cm-scroller {
|
||||||
// Give some breathing space at the bottom of the screen
|
// Give some breathing space at the bottom of the screen
|
||||||
padding-bottom: 20em;
|
padding-bottom: 20em;
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ html {
|
|||||||
--editor-admonition-note-background-color: rgba(0, 184, 212, 0.1);
|
--editor-admonition-note-background-color: rgba(0, 184, 212, 0.1);
|
||||||
--editor-admonition-warning-border-color: rgb(255, 145, 0);
|
--editor-admonition-warning-border-color: rgb(255, 145, 0);
|
||||||
--editor-admonition-warning-background-color: rgba(255, 145, 0, 0.1);
|
--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-color: var(--subtle-color);
|
||||||
--editor-frontmatter-marker-color: #89000080;
|
--editor-frontmatter-marker-color: #89000080;
|
||||||
--editor-widget-background-color: rgb(238, 238, 238);
|
--editor-widget-background-color: rgb(238, 238, 238);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ release.
|
|||||||
_Not yet released, this will likely become 0.6.0._
|
_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.
|
* **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.
|
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.
|
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
|
status: Draft
|
||||||
|
|||||||
@@ -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.
|
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). 🎉
|
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
|
uri: github:silverbulletmd/silverbullet-github/github.plug.js
|
||||||
repo: https://github.com/silverbulletmd/silverbullet-github
|
repo: https://github.com/silverbulletmd/silverbullet-github
|
||||||
author: Zef Hemel
|
author: Zef Hemel
|
||||||
|
shareSupport: true
|
||||||
|
tags: plug
|
||||||
---
|
---
|
||||||
#plug #share-support
|
|
||||||
```template
|
```template
|
||||||
page: "[[!raw.githubusercontent.com/silverbulletmd/silverbullet-github/main/README]]"
|
page: "[[!raw.githubusercontent.com/silverbulletmd/silverbullet-github/main/README]]"
|
||||||
raw: true
|
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.
|
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]].
|
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:
|
The Markdown plug provides support for various advanced Markdown features, specifically:
|
||||||
|
|
||||||
* {[Markdown Preview: Toggle]} preview
|
* {[Markdown Preview: Toggle]} preview
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
uri: github:silverbulletmd/silverbullet-mattermost/mattermost.plug.json
|
uri: github:silverbulletmd/silverbullet-mattermost/mattermost.plug.json
|
||||||
repo: https://github.com/silverbulletmd/silverbullet-mattermost
|
repo: https://github.com/silverbulletmd/silverbullet-mattermost
|
||||||
author: Zef Hemel
|
author: Zef Hemel
|
||||||
|
shareSupport: true
|
||||||
|
tags: plug
|
||||||
---
|
---
|
||||||
#plug #share-support
|
|
||||||
|
|
||||||
> **warning** Unmaintained
|
> **warning** Unmaintained
|
||||||
> This plug is currently not being maintained, it may break at any time
|
> 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
|
repo: https://github.com/silverbulletmd/silverbullet-mermaid
|
||||||
author: Zef Hemel
|
author: Zef Hemel
|
||||||
---
|
---
|
||||||
|
|
||||||
Example use:
|
Example use:
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
uri: github:m1lt0n/silverbullet-serendipity/serendipity.plug.json
|
uri: github:m1lt0n/silverbullet-serendipity/serendipity.plug.json
|
||||||
repo: https://github.com/m1lt0n/silverbullet-serendipity
|
repo: https://github.com/m1lt0n/silverbullet-serendipity
|
||||||
author: Pantelis Vratsalis
|
author: Pantelis Vratsalis
|
||||||
|
tags: plug
|
||||||
---
|
---
|
||||||
#plug
|
|
||||||
```template
|
```template
|
||||||
page: "[[!raw.githubusercontent.com/m1lt0n/silverbullet-serendipity/main/README]]"
|
page: "[[!raw.githubusercontent.com/m1lt0n/silverbullet-serendipity/main/README]]"
|
||||||
raw: true
|
raw: true
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
repo: https://github.com/silverbulletmd/silverbullet
|
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]].
|
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.
|
See the [original RFC](https://github.com/silverbulletmd/silverbullet/discussions/117) for implementation details.
|
||||||
|
|
||||||
Specific implementations for sharing are implemented in other plugs, specifically:
|
Specific implementations for sharing are implemented in other plugs, specifically:
|
||||||
```query
|
```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.
|
The Tasks plug implements task support in SilverBullet.
|
||||||
|
|
||||||
## Task states
|
## Task states
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#plug
|
---
|
||||||
|
tags: plug
|
||||||
|
---
|
||||||
The [[Plugs/Template]] plug implements a few templating mechanisms.
|
The [[Plugs/Template]] plug implements a few templating mechanisms.
|
||||||
|
|
||||||
# Daily Note
|
# 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:
|
1. _Live_ uses, where page content is dynamically updated based on templates:
|
||||||
* [[Live Queries]]
|
* [[Live Queries]]
|
||||||
* [[Live Templates]]
|
* [[Live Templates]]
|
||||||
|
* [[Live Frontmatter Templates]]
|
||||||
2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page:
|
2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page:
|
||||||
* [[Slash Templates]]
|
* [[Slash Templates]]
|
||||||
* [[Page 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:
|
[[Frontmatter]] has special meaning in templates. The following attributes are used:
|
||||||
|
|
||||||
* `tags`: should always be set to `template`
|
* `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]]
|
* `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]].
|
* `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.
|
* `pageName` (optional, [[Page Templates]] only): specify a (template for a) page name.
|
||||||
|
|||||||
@@ -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}}
|
||||||
Reference in New Issue
Block a user