From cd07c310cbfc4b935bb12ddd41230d5ebd5cea66 Mon Sep 17 00:00:00 2001 From: kree Date: Fri, 14 Aug 2026 03:17:52 -0400 Subject: [PATCH] got parser to work (mostly i think). it aint so pretty tho. ALSO BRO FUCKING CRLF --- examples/data.html | 11 ++ examples/define.html | 4 + src/api/index.ts | 6 +- src/index.ts | 6 +- src/util/gin.ts | 300 ++++++++++++++++++++++++++++++------------ src/util/goop.ts | 8 ++ test/util/gin.test.ts | 50 +++---- 7 files changed, 258 insertions(+), 127 deletions(-) create mode 100644 examples/data.html create mode 100644 examples/define.html create mode 100644 src/util/goop.ts diff --git a/examples/data.html b/examples/data.html new file mode 100644 index 0000000..c21537b --- /dev/null +++ b/examples/data.html @@ -0,0 +1,11 @@ +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 \ No newline at end of file diff --git a/examples/define.html b/examples/define.html new file mode 100644 index 0000000..45add26 --- /dev/null +++ b/examples/define.html @@ -0,0 +1,4 @@ +{{define "T1"}}ONE{{end}} +{{define "T2"}}TWO{{end}} +{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}} +{{template "T3"}} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index a0ff034..b942680 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,6 +1,6 @@ import { Elysia, t } from 'elysia'; import { API } from './service'; -import Yuck from '../util/gin'; +import Yuck, { instance as r } from '../util/gin'; export const api = new Elysia({ prefix: '/api' }) .get("discover", async ({ query }) => { @@ -8,7 +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 r.HTML("card", movie); return Yuck.renderMoviePage( movie.id, movie.title, @@ -23,7 +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 r.HTML("movie", movie); return Yuck.renderMoviePage( movie.id, movie.title, diff --git a/src/index.ts b/src/index.ts index 30a25a3..0ca3c3b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,15 +2,17 @@ import { Elysia, file } from "elysia"; import { api } from './api' import staticPlugin from "@elysia/static"; import html from "@elysia/html"; -import Yuck from "./util/gin"; +import Yuck, { instance as r } from "./util/gin"; const HOST = process.env.HOST ?? 'localhost'; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000; +r.LoadHTMLGlob('public/**/*html') + const app = new Elysia() .use(api) .use(html()) - .get("/", () => (Yuck.renderDiscoverPage())) + .get("/", () => (r.HTML('discover', {}))) .get('/favicon.ico', file('public/assets/favicon.ico')) .use(staticPlugin({ prefix: '/assets', assets: 'public/assets' })) .listen({ diff --git a/src/util/gin.ts b/src/util/gin.ts index 039a9a8..b9c46aa 100644 --- a/src/util/gin.ts +++ b/src/util/gin.ts @@ -1,5 +1,8 @@ type Node = { - type: 'Text' | 'Data' | 'Conditional' | 'Marker' | 'Container' | 'Empty' + type: + 'Text' | 'Data' | 'Conditional' | + 'Container' |'Marker' | 'Define' | + 'TemplateReference' | 'TemplateContainer' | 'Empty' // Text startIndex?: number; @@ -15,7 +18,13 @@ type Node = { consequent?: Node; alternative?: Node; - // Marker + // Define + definition?: string; + + // Template + reference?: string; + + // Markers markerType?: 'else' | 'end'; // Container @@ -30,6 +39,140 @@ type Template = { root: Node; }; +export class Engine { + private bank: Map; + + constructor() { + this.bank = new Map(); + } + + HTML(name: string, data: object) { + return this._execute(this.bank.get(name)!.root, data, ''); + } + + async LoadHTMLGlob(pattern: string) { + const glob = new Bun.Glob(pattern); + const files = await Array.fromAsync(glob.scan()); + for (const f of files) { + const cleanPath = f.replaceAll('\\', '/'); + console.log('loading...', cleanPath) + // FUCKING CRLF. DAMMIT WINDOWS! + const text = (await Bun.file(cleanPath).text()).replaceAll('\r', '') + const template = { + name: cleanPath, + root: parse(text) + }; + this.bundle(template); + } + } + + printTemplates() { + this.bank.forEach((t) => { + console.log(t.name); + printNode(t.root) + }); + } + + private bundle(template: Template) { + const nodes = [...template.root.placeholders!] + + const containers: Record = {'.': {type: 'Container', placeholders: []}}; + let currentDefinition = '.'; + + while (nodes.length) { + const node = nodes.shift()!; + + switch (node.type!) { + case 'Define': + currentDefinition = node.definition!; + containers[currentDefinition] = {type: 'Container', placeholders: []} + break; + case 'Marker': + if (node.markerType !== 'end') { + throw new Error('bruh what.'); + } + currentDefinition = '.' + break; + default: + containers[currentDefinition].placeholders!.push(node); + break; + } + } + + Object.keys(containers).forEach(name => { + if (name == '.') { + this.bank.set(template.name, { name: template.name, root: containers[name]}); + } else { + this.bank.set(name, { name, root: containers[name]}); + } + }); + + console.log({bank: this.bank}); + } + + _execute(node: Node, data: Record | string, build: string): string { + if (!node) { + return build; + } + + switch (node.type) { + case 'Container': + return build + node.placeholders!.map(p => this._execute(p, data, '')).join(''); + case 'Conditional': + const condEval = this._execute(node.condition!, data, ''); + let value = condEval; + try { + value = JSON.parse(value); + } catch (e) {} + if (node.conditionType == 'if') { + // BREH JS BS (without the parenthesis it includes build in its ternary condition -.-) + return build + (!!value ? + this._execute(node.consequent!, data, '') : + this._execute(node.alternative!, data, '')) + } else { + // BREH JS BS + return build + (!!value ? + this._execute(node.consequent!, value, '') : + this._execute(node.alternative!, value, '')) + } + case 'Data': + console.log('ref', node.key, data) + + if (node.key == ".") { + if (typeof data === 'string') { + return build + data + } else { + return build + JSON.stringify(data); + } + } else { + if (typeof data === 'object') { + const inner = data[node.key!.substring(1)] + if (typeof inner === 'string') { + return build + inner + } + return build + JSON.stringify(inner); + } + throw new Error('huh?') + } + case 'Text': + return node.text!; + case 'TemplateReference': + return this._execute(this.bank.get(node.reference!)!.root, data, build) + case 'Empty': + return build; + default: + return `${build}[NOT_IMPLEMENTED ${node.type}]` + } + } +} + +function parse(text: string): Node { + let root: Node = { + type: 'Container', + placeholders: [] + }; + return _parse(text, root); +} function _parse(text: string, root: Node): Node { if (!text || text.length < 1) { @@ -40,6 +183,7 @@ function _parse(text: string, root: Node): Node { const open = []; let prevScope = root; let scope = root; + let inConditional = false; let textNode: Node = { type: 'Text', @@ -89,8 +233,21 @@ function _parse(text: string, root: Node): Node { if (command.type == 'Marker') { switch (command.markerType) { case 'end': - scope = prevScope; - altPath = false; + if (inConditional) { + inConditional = false; + scope = prevScope; + altPath = false; + } 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; + } + } break; case 'else': altPath = true; @@ -98,6 +255,7 @@ function _parse(text: string, root: Node): Node { } } else if (command.type == 'Conditional') { + inConditional = true; scope.placeholders!.push(command) prevScope = scope; scope = command!; @@ -148,6 +306,11 @@ function parseCommand(text: string, i: number, j: number): Node { j -= 2; view = text.substring(i, j); } + + while (text.charAt(i) === ' ') { + i++; + view = text.substring(i, j); + } if (view.startsWith('.')) { node = parseDataText(view); @@ -161,6 +324,24 @@ function parseCommand(text: string, i: number, j: number): Node { node.type = 'Marker'; node.markerType = view; } + + if (view.startsWith("define")) { + node.type = 'Define'; + let definition = view.split(' ')[1]; + if (definition.startsWith('"') && definition.endsWith('"')) { + definition = definition.slice(1, -1); + } + node.definition = definition; + } + + if (view.startsWith("template")) { + node.type = 'TemplateReference'; + let name = view.split(' ')[1]; + if (name.startsWith('"') && name.endsWith('"')) { + name = name.slice(1, -1); + } + node.reference = name; + } return node; }; @@ -211,97 +392,37 @@ function parseDataText(text: string): Node { }; } -function _execute(node: Node, data: Record | string, build: string): string { - if (!node) { - return build; - } - +function printNode(node: Node, offset='') { switch (node.type) { - case 'Container': - return build + node.placeholders!.map(p => _execute(p, data, '')).join(''); + case 'TemplateReference': + console.log(`${offset}${node.type} [name=${JSON.stringify(node.reference)}]`); + break; case 'Conditional': - const condEval = _execute(node.condition!, data, ''); - let value = condEval; - try { - value = JSON.parse(value); - } catch (e) {} - if (node.conditionType == 'if') { - // BREH JS BS (without the parenthesis it includes build in its ternary condition -.-) - return build + (!!value ? - _execute(node.consequent!, data, '') : - _execute(node.alternative!, data, '')) - } else { - // BREH JS BS - return build + (!!value ? - _execute(node.consequent!, value, '') : - _execute(node.alternative!, value, '')) + 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': - if (node.key == ".") { - if (typeof data === 'string') { - return build + data - } else { - return build + JSON.stringify(data); - } - } else { - if (typeof data === 'object') { - const inner = data[node.key!.substring(1)] - if (typeof inner === 'string') { - return build + inner - } - return build + JSON.stringify(inner); - } - throw new Error('huh?') - } + console.log(`${offset}${node.type} [key=${node.key}]`); + break; case 'Text': - return node.text!; + console.log(`${offset}${node.type} [text=${JSON.stringify(node.text)}]`); + break; default: - return build + '[NOT_IMPLEMENTED]' + console.log(`${offset}${node.type}`); } } -export namespace Gin { - export function parse(text: string): Node { - let root: Node = { - type: 'Container', - placeholders: [] - }; - return _parse(text, root); - }; - - export 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)}]`); - break; - default: - console.log(`${offset}${node.type}`); - } - } - - export function execute(template: Template, data: Record) { - return _execute(template.root, data, ''); - } -}; - /** * Stopgap :( */ @@ -350,4 +471,9 @@ export default abstract class Yuck { // NAH ima implement that in the engine return `

[look at me im a bunch of provider logos. yippeeee]

` } -} \ No newline at end of file +} + +const instance = new Engine(); +export { + instance +}; \ No newline at end of file diff --git a/src/util/goop.ts b/src/util/goop.ts new file mode 100644 index 0000000..ea5ae2a --- /dev/null +++ b/src/util/goop.ts @@ -0,0 +1,8 @@ +/** + * Go json translator pretty much + * + * This is just cause I gotta stick to not touching Public templates + */ +export default function ungoop(obj: object) { + // TODO. will prob just use the type system +} diff --git a/test/util/gin.test.ts b/test/util/gin.test.ts index 9941bf1..c82abcb 100644 --- a/test/util/gin.test.ts +++ b/test/util/gin.test.ts @@ -1,26 +1,15 @@ -import { describe, it, expect } from 'bun:test' -import { Gin } from '../../src/util/gin' +import { describe, it, expect, beforeAll } from 'bun:test' +import { Engine } from '../../src/util/gin' describe('Gin template porting test', () => { - it('parse', async () => { - 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`; + const e = new Engine(); - const template = { - name: 'example', - root: Gin.parse(example) - }; + beforeAll(async () => { + await e.LoadHTMLGlob('examples/*html'); + e.printTemplates(); + }) + it('data', async () => { const data = [ { Name: 'Aunt Mildred', @@ -40,40 +29,31 @@ Josie`; ] const expected = - `Dear Aunt Mildred, +`Dear Aunt Mildred, It was a pleasure to see you at the wedding. Thank you for the lovely bone china tea set. Best wishes, Josie - Dear Uncle John, It is a shame you couldn't make it to the wedding. Thank you for the lovely moleskin pants. Best wishes, Josie - Dear Cousin Rodney, It is a shame you couldn't make it to the wedding. Best wishes, Josie` - const actual = data.map(r => Gin.execute(template, r)).join('\n\n'); - console.debug(Gin.execute(template, data[0])) + const actual = data.map(d => e.HTML("examples/data.html", d)).join('\n'); expect(actual).toEqual(expected); - }) - // it('parseGlob', async () => { - // const gin = new Gin(); - // const paths = await gin.parseGlob('public/**/*html'); - // }) - - // it('parseHTML', async () => { - // const gin = new Gin(); - // const text = await gin.parseHTML('public/pages/movie.html'); - // console.log(text); - // }) + it('define', async () => { + expect(e.HTML("T1", {})).toEqual('ONE') + expect(e.HTML("T2", {})).toEqual('TWO') + expect(e.HTML("T3", {})).toEqual('ONE TWO') + }) }) \ No newline at end of file