Awesome frontmatter (#617)

Live Frontmatter Templates
This commit is contained in:
Zef Hemel
2024-01-04 20:08:12 +01:00
committed by GitHub
parent 9040993232
commit 91027af5fe
53 changed files with 646 additions and 342 deletions
+37
View 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
View 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;
}
+8 -1
View File
@@ -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 {
-119
View File
@@ -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 }
}
+1 -1
View File
@@ -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
})
+1 -1
View File
@@ -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
})
+6
View File
@@ -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));
});
+35
View File
@@ -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),
+12
View File
@@ -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);
},
};
}
+19
View File
@@ -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> {