diff --git a/common/markdown_parser/parser.ts b/common/markdown_parser/parser.ts index 3c141d87..a3c0e76a 100644 --- a/common/markdown_parser/parser.ts +++ b/common/markdown_parser/parser.ts @@ -135,11 +135,12 @@ const TemplateDirective: MarkdownConfig = { { name: "TemplateExpressionDirective" }, { name: "TemplateIfStartDirective", style: ct.DirectiveTag }, { name: "TemplateEachStartDirective", style: ct.DirectiveTag }, + { name: "TemplateEachVarStartDirective", style: ct.DirectiveTag }, { name: "TemplateLetStartDirective", style: ct.DirectiveTag }, { name: "TemplateIfEndDirective", style: ct.DirectiveTag }, { name: "TemplateEachEndDirective", style: ct.DirectiveTag }, { name: "TemplateLetEndDirective", style: ct.DirectiveTag }, - { name: "TemplateLetVar", style: t.variableName }, + { name: "TemplateVar", style: t.variableName }, { name: "TemplateDirectiveMark", style: ct.DirectiveMarkTag }, ], parseInline: [ @@ -182,41 +183,52 @@ const TemplateDirective: MarkdownConfig = { const endPos = pos + valueLength + 1; let bodyEl: any; - // Is this an open block directive? - const openBlockMatch = /^(\s*#(if|each)\s*)(.+)$/s.exec(bodyText); - if (openBlockMatch) { - const [_, directiveStart, directiveType, directiveBody] = - openBlockMatch; + // Is this an let block directive? + const openLetBlockMatch = /^(\s*#let\s*)(@\w+)(\s*=\s*)(.+)$/s.exec( + bodyText, + ); + if (openLetBlockMatch) { + const [_, directiveStart, varName, eq, expr] = openLetBlockMatch; const parsedExpression = highlightingExpressionParser.parse( - directiveBody, + expr, ); bodyEl = cx.elt( - directiveType === "if" - ? "TemplateIfStartDirective" - : "TemplateEachStartDirective", + "TemplateLetStartDirective", pos + 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) { - // Is this an open block directive? - const openLetBlockMatch = /^(\s*#let\s*)(@\w+)(\s*=\s*)(.+)$/s.exec( - bodyText, - ); - if (openLetBlockMatch) { - const [_, directiveStart, varName, eq, expr] = openLetBlockMatch; + // Is this an #each @p = block directive? + const openEachVariableBlockMatch = + /^(\s*#each\s*)(@\w+)(\s*in\s*)(.+)$/s.exec( + bodyText, + ); + if (openEachVariableBlockMatch) { + const [_, directiveStart, varName, eq, expr] = + openEachVariableBlockMatch; const parsedExpression = highlightingExpressionParser.parse( expr, ); bodyEl = cx.elt( - "TemplateLetStartDirective", + "TemplateEachVarStartDirective", pos + 2, endPos - 2, [ cx.elt( - "TemplateLetVar", + "TemplateVar", pos + 2 + directiveStart.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) { // Is this a directive close? diff --git a/common/template/render.test.ts b/common/template/render.test.ts index 0b350fed..e2bdc2ea 100644 --- a/common/template/render.test.ts +++ b/common/template/render.test.ts @@ -135,6 +135,14 @@ Deno.test("Test template", async () => { "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 { const parsedTemplate = parseTemplate(template); return renderTemplate(parsedTemplate, value, variables, functionMap); diff --git a/common/template/render.ts b/common/template/render.ts index 81a9aa2f..b3401b73 100644 --- a/common/template/render.ts +++ b/common/template/render.ts @@ -2,10 +2,7 @@ import { AST } from "$sb/lib/tree.ts"; import { evalQueryExpression } from "$sb/lib/query_expression.ts"; import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts"; import { FunctionMap } from "$sb/types.ts"; -import { - jsonObjectToMDTable, - jsonToMDTable, -} from "../../plugs/template/util.ts"; +import { jsonToMDTable } from "../../plugs/template/util.ts"; export async function renderTemplate( ast: AST, @@ -50,6 +47,13 @@ async function renderTemplateElement( variables, functionMap, ); + case "EachVarDirective": + return await renderEachVarDirective( + ast, + value, + variables, + functionMap, + ); case "IfDirective": return await renderIfDirective(ast, value, variables, functionMap); case "LetDirective": @@ -101,6 +105,48 @@ async function renderExpressionDirective( } } +async function renderEachVarDirective( + ast: AST, + value: any[], + variables: Record, + functionMap: FunctionMap, +): Promise { + 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( ast: AST, value: any[], @@ -182,11 +228,11 @@ async function renderLetDirective( variables, functionMap, ); - const newGlobalVariables = { ...variables, [name as any]: val }; + const newVariables = { ...variables, [name as any]: val }; return await renderTemplate( template, value, - newGlobalVariables, + newVariables, functionMap, ); } diff --git a/common/template/template_parser.ts b/common/template/template_parser.ts index 70f9f0bd..89d6e90e 100644 --- a/common/template/template_parser.ts +++ b/common/template/template_parser.ts @@ -75,15 +75,31 @@ function processTree(tree: AST): AST { const body = bodyElements.map(processTree); switch (blockType) { 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( expressionLanguage, - blockTextContent.trim(), + eachVarMatch[2], )); - // console.log("Each body", bodyElements); - return ["EachDirective", expressionTree[1], [ - "Template", - ...stripInitialNewline(body), - ]]; + return [ + "EachVarDirective", + eachVarMatch[1], + expressionTree[1], + ["Template", ...stripInitialNewline(body)], + ]; } case "let": { const letExpr = blockTextContent.trim(); @@ -156,7 +172,8 @@ function stripInitialNewline(body: any[]) { // After each block directive, strip the next newline 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); stripNext = true; diff --git a/plugs/query/api.ts b/plugs/query/api.ts index a05f1569..ed098207 100644 --- a/plugs/query/api.ts +++ b/plugs/query/api.ts @@ -4,7 +4,6 @@ import { events } from "$sb/syscalls.ts"; import { QueryProviderEvent } from "$sb/app_event.ts"; import { resolvePath } from "$sb/lib/resolve.ts"; import { renderQueryTemplate } from "../template/util.ts"; -import { parse } from "../../common/markdown_parser/parse_tree.ts"; export async function query( query: string, diff --git a/website/Template Language.md b/website/Template Language.md index f231df76..b967d92f 100644 --- a/website/Template Language.md +++ b/website/Template Language.md @@ -60,14 +60,22 @@ You can also add an optional `else` clause: ``` # 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 -Counting to 3: +Counting to 3 with a variable name: {{#each [1, 2, 3]}} * {{.}} {{/each}} +And using a variable name iterator: +{{#each @v in [1, 2, 3]}} +* {{@v}} +{{/each}} + Iterating over the three last modified pages: {{#each {page order by lastModified desc limit 3}}} * {{name}}