From b81ac470c5a06a308d3c12bc4f5bfd6f3fdf2bb2 Mon Sep 17 00:00:00 2001 From: kree Date: Thu, 6 Aug 2026 09:29:49 -0400 Subject: [PATCH] initial parser logic. still needs fixing for Text --- src/api/index.ts | 3 + src/util/gin.ts | 266 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 243 insertions(+), 26 deletions(-) diff --git a/src/api/index.ts b/src/api/index.ts index 7a6360a..a0ff034 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -8,6 +8,7 @@ export const api = new Elysia({ prefix: '/api' }) const releaseYear = movie.release_date.substring(0, 4); const voteAverage = movie.vote_average.toFixed(2); + // return Gin.render("card", movie); return Yuck.renderMoviePage( movie.id, movie.title, @@ -22,6 +23,7 @@ export const api = new Elysia({ prefix: '/api' }) const releaseYear = movie.release_date.substring(0, 4); const voteAverage = movie.vote_average.toFixed(2); + // return Gin.render("movie", movie); return Yuck.renderMoviePage( movie.id, movie.title, @@ -33,5 +35,6 @@ export const api = new Elysia({ prefix: '/api' }) }) .get("providers/watch/:id", async ({ params: { id } }) => { const providers = await API.providers(parseInt(id)); + // return Gin.render("providers", movie); return Yuck.renderProviderPage() }); diff --git a/src/util/gin.ts b/src/util/gin.ts index 889621d..688eb2e 100644 --- a/src/util/gin.ts +++ b/src/util/gin.ts @@ -1,39 +1,253 @@ -// TODO: Finish template engine and parse the data like god intended -type GinNode = { + +const example = +`Dear {{.Name}}, +{{if .Attended}} +It was a pleasure to see you at the wedding. +{{- else}} +It is a shame you couldn't make it to the wedding. +{{- end}} +{{with .Gift -}} +Thank you for the lovely {{.}}. +{{end}} +Best wishes, +Josie`; + +type Node = { + type: 'Text' | 'Data' | 'Conditional' | 'Marker' | 'Container' | 'Empty' - path: string; - children: GinNode[]; + // Text + startIndex?: number; + endIndex?: number; + text?: string; + + // Data + key?: string; + + // Conditional + conditionType?: 'if' | 'with'; + condition?: Node; + consequent?: Node; + alternative?: Node; + + // Marker + markerType?: 'else' | 'end'; + + // Container + placeholders?: Node[], + + // Scoping + parent?: Node; +}; + +function parse(text: string): Node { + let root: Node = { + type: 'Container', + placeholders: [] + }; + return _parse(text, root); } -class Gin { +function _parse(text: string, root: Node): Node { + if (!text || text.length < 1) { + return root; + }; - /** - * Preprocess template files from a given path into some data structure that tags them and notes substitutable areas - * - * @param path - */ - setup(path: string) {} + let i = 0; + const open = []; + let prevScope = root; + let scope = root; - /** - * Creates HTML string from named template and supplied data - * - * @param name - * @param mappings - */ - render(name: string, data: Record): string { - return ''; + let textNode: Node = { + type: 'Text', + startIndex: 0, + endIndex: 0 } - /** - * Constructs actual dependency tree. Parsing is prob gonna be some recursive business - * - * @param path - */ - private crawl(path: string): GinNode | undefined { - return; + + let altPath = false; + while (i < text.length) { + textNode.endIndex = i; + + if (text.substring(i, i+2) == '{{') { + open.push(i); + } + + if (text.substring(i, i+2) == '}}') { + textNode.parent = scope; + textNode.text = text.substring(textNode.startIndex!, textNode.endIndex!) + switch (scope.type) { + case 'Container': + scope.placeholders!.push(textNode) + break; + case 'Conditional': + const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!; + placeholders.push(textNode); + } + + const j = open.pop()!; + + textNode = { + type: 'Text', + startIndex: j+2, + endIndex: i + }; + + const command = parseCommand(text, j+2, i); + command.parent = scope; + + if (command.type == 'Marker') { + switch (command.markerType) { + case 'end': + scope = prevScope; + altPath = false; + break; + case 'else': + altPath = true; + break; + } + } + else if (command.type == 'Conditional') { + scope.placeholders!.push(command) + prevScope = scope; + scope = command!; + } else { + switch (scope.type) { + case 'Container': + scope.placeholders!.push(command) + break; + case 'Conditional': + const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!; + placeholders.push(command); + break; + } + } + } + i++; + } + + if (textNode.startIndex! < textNode.endIndex!) { + textNode.parent = scope; + textNode.text = text.substring(textNode.startIndex!, textNode.endIndex!) + scope.placeholders!.push(textNode); + } + + return root; +} + +function parseCommand(text: string, i: number, j: number): Node { + let node: Node = { + type: 'Empty' + }; + + if (i >= j) { + return node; + }; + + let view = text.substring(i, j); + + + // not supported + if (view.startsWith("- ")) { + i += 2; + view = text.substring(i, j); + } + + // not supported + if (view.endsWith(" -")) { + j -= 2; + view = text.substring(i, j); + } + + if (view.startsWith('.')) { + node = parseDataText(view); + } + + if (view.startsWith('if') || view.trim().startsWith('with')) { + node = parseConditionalText(text, i, j); + } + + if (view == 'else' || view == 'end') { + node.type = 'Marker'; + node.markerType = view; + } + return node; +}; + +function parseConditionalText(text: string, i: number, j: number): Node { + const view = text.substring(i, j); + + let node: Node = { + type: 'Conditional', + conditionType: view.startsWith('if') ? 'if' : 'with', + consequent: { + type: 'Container', + placeholders: [] + }, + alternative: { + type: 'Container', + placeholders: [] + }, + }; + node.consequent!.parent = node; + node.alternative!.parent = node; + + const pipelineStart = i + view.indexOf(' ') + 1; + const pipelineView = text.substring(pipelineStart, j) + + if (pipelineView.startsWith('.')) { + node.condition = { + type: 'Data', + key: pipelineView, + parent: node, + } + } else { + node.condition = { + type: 'Text', + startIndex: pipelineStart, + endIndex: j, + text: pipelineView, + parent: node, + } + } + + return node; +} + +function parseDataText(text: string): Node { + return { + type: 'Data', + key: text.trim() + }; +} + +function printNode(node: Node, offset='') { + switch (node.type) { + case 'Conditional': + console.log(`${offset}${node.conditionType}`); + console.log(`${offset}Condition:\n`) + printNode(node.condition!, offset + '\t') + console.log(`${offset}Then:\n`) + printNode(node.consequent!, offset + '\t') + console.log(offset + 'Else:\n') + printNode(node.alternative!, offset + '\t') + break; + case 'Container': + console.log(offset + node.type) + for (let p of node.placeholders ?? []) { + printNode(p, offset + '|__'); + } + break; + case 'Data': + console.log(`${offset}${node.type} [key=${node.key}]`); + break; + case 'Text': + console.log(`${offset}${node.type} [text=${JSON.stringify(node.text)}]`); + default: + console.log(`${offset}${node.type}`); } } +printNode(parse(example)); /** * Stopgap :( */