Fixes #678
parent
9bb72d01b2
commit
d0ebda0d7a
|
@ -135,11 +135,12 @@ const TemplateDirective: MarkdownConfig = {
|
||||||
{ name: "TemplateExpressionDirective" },
|
{ name: "TemplateExpressionDirective" },
|
||||||
{ name: "TemplateIfStartDirective", style: ct.DirectiveTag },
|
{ name: "TemplateIfStartDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateEachStartDirective", style: ct.DirectiveTag },
|
{ name: "TemplateEachStartDirective", style: ct.DirectiveTag },
|
||||||
|
{ name: "TemplateEachVarStartDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateLetStartDirective", style: ct.DirectiveTag },
|
{ name: "TemplateLetStartDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateIfEndDirective", style: ct.DirectiveTag },
|
{ name: "TemplateIfEndDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateEachEndDirective", style: ct.DirectiveTag },
|
{ name: "TemplateEachEndDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateLetEndDirective", style: ct.DirectiveTag },
|
{ name: "TemplateLetEndDirective", style: ct.DirectiveTag },
|
||||||
{ name: "TemplateLetVar", style: t.variableName },
|
{ name: "TemplateVar", style: t.variableName },
|
||||||
{ name: "TemplateDirectiveMark", style: ct.DirectiveMarkTag },
|
{ name: "TemplateDirectiveMark", style: ct.DirectiveMarkTag },
|
||||||
],
|
],
|
||||||
parseInline: [
|
parseInline: [
|
||||||
|
@ -182,41 +183,52 @@ const TemplateDirective: MarkdownConfig = {
|
||||||
const endPos = pos + valueLength + 1;
|
const endPos = pos + valueLength + 1;
|
||||||
let bodyEl: any;
|
let bodyEl: any;
|
||||||
|
|
||||||
// Is this an open block directive?
|
// Is this an let block directive?
|
||||||
const openBlockMatch = /^(\s*#(if|each)\s*)(.+)$/s.exec(bodyText);
|
const openLetBlockMatch = /^(\s*#let\s*)(@\w+)(\s*=\s*)(.+)$/s.exec(
|
||||||
if (openBlockMatch) {
|
bodyText,
|
||||||
const [_, directiveStart, directiveType, directiveBody] =
|
);
|
||||||
openBlockMatch;
|
if (openLetBlockMatch) {
|
||||||
|
const [_, directiveStart, varName, eq, expr] = openLetBlockMatch;
|
||||||
const parsedExpression = highlightingExpressionParser.parse(
|
const parsedExpression = highlightingExpressionParser.parse(
|
||||||
directiveBody,
|
expr,
|
||||||
);
|
);
|
||||||
bodyEl = cx.elt(
|
bodyEl = cx.elt(
|
||||||
directiveType === "if"
|
"TemplateLetStartDirective",
|
||||||
? "TemplateIfStartDirective"
|
|
||||||
: "TemplateEachStartDirective",
|
|
||||||
pos + 2,
|
pos + 2,
|
||||||
endPos - 2,
|
endPos - 2,
|
||||||
[cx.elt(parsedExpression, pos + 2 + directiveStart.length)],
|
[
|
||||||
|
cx.elt(
|
||||||
|
"TemplateVar",
|
||||||
|
pos + 2 + directiveStart.length,
|
||||||
|
pos + 2 + directiveStart.length + varName.length,
|
||||||
|
),
|
||||||
|
cx.elt(
|
||||||
|
parsedExpression,
|
||||||
|
pos + 2 + directiveStart.length + varName.length + eq.length,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bodyEl) {
|
if (!bodyEl) {
|
||||||
// Is this an open block directive?
|
// Is this an #each @p = block directive?
|
||||||
const openLetBlockMatch = /^(\s*#let\s*)(@\w+)(\s*=\s*)(.+)$/s.exec(
|
const openEachVariableBlockMatch =
|
||||||
bodyText,
|
/^(\s*#each\s*)(@\w+)(\s*in\s*)(.+)$/s.exec(
|
||||||
);
|
bodyText,
|
||||||
if (openLetBlockMatch) {
|
);
|
||||||
const [_, directiveStart, varName, eq, expr] = openLetBlockMatch;
|
if (openEachVariableBlockMatch) {
|
||||||
|
const [_, directiveStart, varName, eq, expr] =
|
||||||
|
openEachVariableBlockMatch;
|
||||||
const parsedExpression = highlightingExpressionParser.parse(
|
const parsedExpression = highlightingExpressionParser.parse(
|
||||||
expr,
|
expr,
|
||||||
);
|
);
|
||||||
bodyEl = cx.elt(
|
bodyEl = cx.elt(
|
||||||
"TemplateLetStartDirective",
|
"TemplateEachVarStartDirective",
|
||||||
pos + 2,
|
pos + 2,
|
||||||
endPos - 2,
|
endPos - 2,
|
||||||
[
|
[
|
||||||
cx.elt(
|
cx.elt(
|
||||||
"TemplateLetVar",
|
"TemplateVar",
|
||||||
pos + 2 + directiveStart.length,
|
pos + 2 + directiveStart.length,
|
||||||
pos + 2 + directiveStart.length + varName.length,
|
pos + 2 + directiveStart.length + varName.length,
|
||||||
),
|
),
|
||||||
|
@ -228,6 +240,25 @@ const TemplateDirective: MarkdownConfig = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!bodyEl) {
|
||||||
|
// Is this an open block directive?
|
||||||
|
const openBlockMatch = /^(\s*#(if|each)\s*)(.+)$/s.exec(bodyText);
|
||||||
|
if (openBlockMatch) {
|
||||||
|
const [_, directiveStart, directiveType, directiveBody] =
|
||||||
|
openBlockMatch;
|
||||||
|
const parsedExpression = highlightingExpressionParser.parse(
|
||||||
|
directiveBody,
|
||||||
|
);
|
||||||
|
bodyEl = cx.elt(
|
||||||
|
directiveType === "if"
|
||||||
|
? "TemplateIfStartDirective"
|
||||||
|
: "TemplateEachStartDirective",
|
||||||
|
pos + 2,
|
||||||
|
endPos - 2,
|
||||||
|
[cx.elt(parsedExpression, pos + 2 + directiveStart.length)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!bodyEl) {
|
if (!bodyEl) {
|
||||||
// Is this a directive close?
|
// Is this a directive close?
|
||||||
|
|
|
@ -135,6 +135,14 @@ Deno.test("Test template", async () => {
|
||||||
"1\n2\n3\n",
|
"1\n2\n3\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
await parseAndRender(
|
||||||
|
"{{#each @v in [1, 2, 3]}}\n{{@v}}\n{{/each}}",
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
"1\n2\n3\n",
|
||||||
|
);
|
||||||
|
|
||||||
function parseAndRender(template: string, value: any): Promise<string> {
|
function parseAndRender(template: string, value: any): Promise<string> {
|
||||||
const parsedTemplate = parseTemplate(template);
|
const parsedTemplate = parseTemplate(template);
|
||||||
return renderTemplate(parsedTemplate, value, variables, functionMap);
|
return renderTemplate(parsedTemplate, value, variables, functionMap);
|
||||||
|
|
|
@ -2,10 +2,7 @@ import { AST } from "$sb/lib/tree.ts";
|
||||||
import { evalQueryExpression } from "$sb/lib/query_expression.ts";
|
import { evalQueryExpression } from "$sb/lib/query_expression.ts";
|
||||||
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
||||||
import { FunctionMap } from "$sb/types.ts";
|
import { FunctionMap } from "$sb/types.ts";
|
||||||
import {
|
import { jsonToMDTable } from "../../plugs/template/util.ts";
|
||||||
jsonObjectToMDTable,
|
|
||||||
jsonToMDTable,
|
|
||||||
} from "../../plugs/template/util.ts";
|
|
||||||
|
|
||||||
export async function renderTemplate(
|
export async function renderTemplate(
|
||||||
ast: AST,
|
ast: AST,
|
||||||
|
@ -50,6 +47,13 @@ async function renderTemplateElement(
|
||||||
variables,
|
variables,
|
||||||
functionMap,
|
functionMap,
|
||||||
);
|
);
|
||||||
|
case "EachVarDirective":
|
||||||
|
return await renderEachVarDirective(
|
||||||
|
ast,
|
||||||
|
value,
|
||||||
|
variables,
|
||||||
|
functionMap,
|
||||||
|
);
|
||||||
case "IfDirective":
|
case "IfDirective":
|
||||||
return await renderIfDirective(ast, value, variables, functionMap);
|
return await renderIfDirective(ast, value, variables, functionMap);
|
||||||
case "LetDirective":
|
case "LetDirective":
|
||||||
|
@ -101,6 +105,48 @@ async function renderExpressionDirective(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function renderEachVarDirective(
|
||||||
|
ast: AST,
|
||||||
|
value: any[],
|
||||||
|
variables: Record<string, any>,
|
||||||
|
functionMap: FunctionMap,
|
||||||
|
): Promise<string> {
|
||||||
|
const [_eachVarDirective, name, expression, template] = ast;
|
||||||
|
const expr = expressionToKvQueryExpression(expression);
|
||||||
|
const values = await evalQueryExpression(
|
||||||
|
expr,
|
||||||
|
value,
|
||||||
|
variables,
|
||||||
|
functionMap,
|
||||||
|
);
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error(
|
||||||
|
`Expecting a list expression for #each var directive, instead got ${values}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resultPieces: string[] = [];
|
||||||
|
for (const itemValue of values) {
|
||||||
|
const localVariables = { ...variables, [name as any]: itemValue };
|
||||||
|
try {
|
||||||
|
resultPieces.push(
|
||||||
|
await renderTemplate(
|
||||||
|
template,
|
||||||
|
value,
|
||||||
|
localVariables,
|
||||||
|
functionMap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(
|
||||||
|
`Error rendering #each directive: ${e.message} for item ${
|
||||||
|
JSON.stringify(itemValue)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultPieces.join("");
|
||||||
|
}
|
||||||
|
|
||||||
async function renderEachDirective(
|
async function renderEachDirective(
|
||||||
ast: AST,
|
ast: AST,
|
||||||
value: any[],
|
value: any[],
|
||||||
|
@ -182,11 +228,11 @@ async function renderLetDirective(
|
||||||
variables,
|
variables,
|
||||||
functionMap,
|
functionMap,
|
||||||
);
|
);
|
||||||
const newGlobalVariables = { ...variables, [name as any]: val };
|
const newVariables = { ...variables, [name as any]: val };
|
||||||
return await renderTemplate(
|
return await renderTemplate(
|
||||||
template,
|
template,
|
||||||
value,
|
value,
|
||||||
newGlobalVariables,
|
newVariables,
|
||||||
functionMap,
|
functionMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,15 +75,31 @@ function processTree(tree: AST): AST {
|
||||||
const body = bodyElements.map(processTree);
|
const body = bodyElements.map(processTree);
|
||||||
switch (blockType) {
|
switch (blockType) {
|
||||||
case "each": {
|
case "each": {
|
||||||
|
const eachExpr = blockTextContent.trim();
|
||||||
|
const eachVarMatch = eachExpr.match(/@(\w+)\s*in\s*(.+)$/s);
|
||||||
|
if (!eachVarMatch) {
|
||||||
|
// Not a each var declaration, just an expression
|
||||||
|
const expressionTree = parseTreeToAST(parse(
|
||||||
|
expressionLanguage,
|
||||||
|
blockTextContent.trim(),
|
||||||
|
));
|
||||||
|
// console.log("Each body", bodyElements);
|
||||||
|
return ["EachDirective", expressionTree[1], [
|
||||||
|
"Template",
|
||||||
|
...stripInitialNewline(body),
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
// This is a #each @p = version
|
||||||
const expressionTree = parseTreeToAST(parse(
|
const expressionTree = parseTreeToAST(parse(
|
||||||
expressionLanguage,
|
expressionLanguage,
|
||||||
blockTextContent.trim(),
|
eachVarMatch[2],
|
||||||
));
|
));
|
||||||
// console.log("Each body", bodyElements);
|
return [
|
||||||
return ["EachDirective", expressionTree[1], [
|
"EachVarDirective",
|
||||||
"Template",
|
eachVarMatch[1],
|
||||||
...stripInitialNewline(body),
|
expressionTree[1],
|
||||||
]];
|
["Template", ...stripInitialNewline(body)],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
case "let": {
|
case "let": {
|
||||||
const letExpr = blockTextContent.trim();
|
const letExpr = blockTextContent.trim();
|
||||||
|
@ -156,7 +172,8 @@ function stripInitialNewline(body: any[]) {
|
||||||
|
|
||||||
// After each block directive, strip the next newline
|
// After each block directive, strip the next newline
|
||||||
if (
|
if (
|
||||||
["IfDirective", "EachDirective", "LetDirective"].includes(el[1][0])
|
["IfDirective", "EachDirective", "EachVarDirective", "LetDirective"]
|
||||||
|
.includes(el[1][0])
|
||||||
) {
|
) {
|
||||||
// console.log("Got a block directive, consider stripping the next one", el);
|
// console.log("Got a block directive, consider stripping the next one", el);
|
||||||
stripNext = true;
|
stripNext = true;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { events } from "$sb/syscalls.ts";
|
||||||
import { QueryProviderEvent } from "$sb/app_event.ts";
|
import { QueryProviderEvent } from "$sb/app_event.ts";
|
||||||
import { resolvePath } from "$sb/lib/resolve.ts";
|
import { resolvePath } from "$sb/lib/resolve.ts";
|
||||||
import { renderQueryTemplate } from "../template/util.ts";
|
import { renderQueryTemplate } from "../template/util.ts";
|
||||||
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
|
||||||
|
|
||||||
export async function query(
|
export async function query(
|
||||||
query: string,
|
query: string,
|
||||||
|
|
|
@ -60,14 +60,22 @@ You can also add an optional `else` clause:
|
||||||
```
|
```
|
||||||
|
|
||||||
# each directive
|
# each directive
|
||||||
To iterate over a collection use an `#each` directive. On each iteration, the current item that is iterated over will be set as the active object (accessible via `.` and its attributes via the `attribute` syntax):
|
To iterate over a collection use an `#each` directive. There are two variants of `#each`, one with and one without variable assignment:
|
||||||
|
|
||||||
|
* `#each @varname in expression` repeats the body of this directive assigning every value to `@varname` one by one
|
||||||
|
* `#each expression` repeats the body of this directive assigning every value to `.` one by one.
|
||||||
|
|
||||||
```template
|
```template
|
||||||
Counting to 3:
|
Counting to 3 with a variable name:
|
||||||
{{#each [1, 2, 3]}}
|
{{#each [1, 2, 3]}}
|
||||||
* {{.}}
|
* {{.}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
|
And using a variable name iterator:
|
||||||
|
{{#each @v in [1, 2, 3]}}
|
||||||
|
* {{@v}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
Iterating over the three last modified pages:
|
Iterating over the three last modified pages:
|
||||||
{{#each {page order by lastModified desc limit 3}}}
|
{{#each {page order by lastModified desc limit 3}}}
|
||||||
* {{name}}
|
* {{name}}
|
||||||
|
|
Loading…
Reference in New Issue