got parser to work (mostly i think). it aint so pretty tho. ALSO BRO FUCKING CRLF

This commit is contained in:
kree 2026-08-14 03:17:52 -04:00
parent 43779e110a
commit cd07c310cb
7 changed files with 258 additions and 127 deletions

11
examples/data.html Normal file
View file

@ -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

4
examples/define.html Normal file
View file

@ -0,0 +1,4 @@
{{define "T1"}}ONE{{end}}
{{define "T2"}}TWO{{end}}
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
{{template "T3"}}

View file

@ -1,6 +1,6 @@
import { Elysia, t } from 'elysia'; import { Elysia, t } from 'elysia';
import { API } from './service'; import { API } from './service';
import Yuck from '../util/gin'; import Yuck, { instance as r } from '../util/gin';
export const api = new Elysia({ prefix: '/api' }) export const api = new Elysia({ prefix: '/api' })
.get("discover", async ({ query }) => { .get("discover", async ({ query }) => {
@ -8,7 +8,7 @@ export const api = new Elysia({ prefix: '/api' })
const releaseYear = movie.release_date.substring(0, 4); const releaseYear = movie.release_date.substring(0, 4);
const voteAverage = movie.vote_average.toFixed(2); const voteAverage = movie.vote_average.toFixed(2);
// return Gin.render("card", movie); // return r.HTML("card", movie);
return Yuck.renderMoviePage( return Yuck.renderMoviePage(
movie.id, movie.id,
movie.title, movie.title,
@ -23,7 +23,7 @@ export const api = new Elysia({ prefix: '/api' })
const releaseYear = movie.release_date.substring(0, 4); const releaseYear = movie.release_date.substring(0, 4);
const voteAverage = movie.vote_average.toFixed(2); const voteAverage = movie.vote_average.toFixed(2);
// return Gin.render("movie", movie); // return r.HTML("movie", movie);
return Yuck.renderMoviePage( return Yuck.renderMoviePage(
movie.id, movie.id,
movie.title, movie.title,

View file

@ -2,15 +2,17 @@ import { Elysia, file } from "elysia";
import { api } from './api' import { api } from './api'
import staticPlugin from "@elysia/static"; import staticPlugin from "@elysia/static";
import html from "@elysia/html"; 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 HOST = process.env.HOST ?? 'localhost';
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
r.LoadHTMLGlob('public/**/*html')
const app = new Elysia() const app = new Elysia()
.use(api) .use(api)
.use(html()) .use(html())
.get("/", () => (Yuck.renderDiscoverPage())) .get("/", () => (r.HTML('discover', {})))
.get('/favicon.ico', file('public/assets/favicon.ico')) .get('/favicon.ico', file('public/assets/favicon.ico'))
.use(staticPlugin({ prefix: '/assets', assets: 'public/assets' })) .use(staticPlugin({ prefix: '/assets', assets: 'public/assets' }))
.listen({ .listen({

View file

@ -1,5 +1,8 @@
type Node = { type Node = {
type: 'Text' | 'Data' | 'Conditional' | 'Marker' | 'Container' | 'Empty' type:
'Text' | 'Data' | 'Conditional' |
'Container' |'Marker' | 'Define' |
'TemplateReference' | 'TemplateContainer' | 'Empty'
// Text // Text
startIndex?: number; startIndex?: number;
@ -15,7 +18,13 @@ type Node = {
consequent?: Node; consequent?: Node;
alternative?: Node; alternative?: Node;
// Marker // Define
definition?: string;
// Template
reference?: string;
// Markers
markerType?: 'else' | 'end'; markerType?: 'else' | 'end';
// Container // Container
@ -30,6 +39,140 @@ type Template = {
root: Node; root: Node;
}; };
export class Engine {
private bank: Map<string, Template>;
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<string, Node> = {'.': {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, any> | 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 { function _parse(text: string, root: Node): Node {
if (!text || text.length < 1) { if (!text || text.length < 1) {
@ -40,6 +183,7 @@ function _parse(text: string, root: Node): Node {
const open = []; const open = [];
let prevScope = root; let prevScope = root;
let scope = root; let scope = root;
let inConditional = false;
let textNode: Node = { let textNode: Node = {
type: 'Text', type: 'Text',
@ -89,8 +233,21 @@ function _parse(text: string, root: Node): Node {
if (command.type == 'Marker') { if (command.type == 'Marker') {
switch (command.markerType) { switch (command.markerType) {
case 'end': case 'end':
if (inConditional) {
inConditional = false;
scope = prevScope; scope = prevScope;
altPath = false; 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; break;
case 'else': case 'else':
altPath = true; altPath = true;
@ -98,6 +255,7 @@ function _parse(text: string, root: Node): Node {
} }
} }
else if (command.type == 'Conditional') { else if (command.type == 'Conditional') {
inConditional = true;
scope.placeholders!.push(command) scope.placeholders!.push(command)
prevScope = scope; prevScope = scope;
scope = command!; scope = command!;
@ -149,6 +307,11 @@ function parseCommand(text: string, i: number, j: number): Node {
view = text.substring(i, j); view = text.substring(i, j);
} }
while (text.charAt(i) === ' ') {
i++;
view = text.substring(i, j);
}
if (view.startsWith('.')) { if (view.startsWith('.')) {
node = parseDataText(view); node = parseDataText(view);
} }
@ -161,6 +324,24 @@ function parseCommand(text: string, i: number, j: number): Node {
node.type = 'Marker'; node.type = 'Marker';
node.markerType = view; 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; return node;
}; };
@ -211,66 +392,11 @@ function parseDataText(text: string): Node {
}; };
} }
function _execute(node: Node, data: Record<string, any> | string, build: string): string { function printNode(node: Node, offset='') {
if (!node) {
return build;
}
switch (node.type) {
case 'Container':
return build + node.placeholders!.map(p => _execute(p, data, '')).join('');
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, ''))
}
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?')
}
case 'Text':
return node.text!;
default:
return build + '[NOT_IMPLEMENTED]'
}
}
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) { switch (node.type) {
case 'TemplateReference':
console.log(`${offset}${node.type} [name=${JSON.stringify(node.reference)}]`);
break;
case 'Conditional': case 'Conditional':
console.log(`${offset}${node.conditionType}`); console.log(`${offset}${node.conditionType}`);
console.log(`${offset}Condition:\n`) console.log(`${offset}Condition:\n`)
@ -295,12 +421,7 @@ export namespace Gin {
default: default:
console.log(`${offset}${node.type}`); console.log(`${offset}${node.type}`);
} }
} }
export function execute(template: Template, data: Record<string, any>) {
return _execute(template.root, data, '');
}
};
/** /**
* Stopgap :( * Stopgap :(
@ -351,3 +472,8 @@ export default abstract class Yuck {
return `<p>[look at me im a bunch of provider logos. yippeeee]</p>` return `<p>[look at me im a bunch of provider logos. yippeeee]</p>`
} }
} }
const instance = new Engine();
export {
instance
};

8
src/util/goop.ts Normal file
View file

@ -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
}

View file

@ -1,26 +1,15 @@
import { describe, it, expect } from 'bun:test' import { describe, it, expect, beforeAll } from 'bun:test'
import { Gin } from '../../src/util/gin' import { Engine } from '../../src/util/gin'
describe('Gin template porting test', () => { describe('Gin template porting test', () => {
it('parse', async () => { const e = new Engine();
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 template = { beforeAll(async () => {
name: 'example', await e.LoadHTMLGlob('examples/*html');
root: Gin.parse(example) e.printTemplates();
}; })
it('data', async () => {
const data = [ const data = [
{ {
Name: 'Aunt Mildred', Name: 'Aunt Mildred',
@ -40,40 +29,31 @@ Josie`;
] ]
const expected = const expected =
`Dear Aunt Mildred, `Dear Aunt Mildred,
It was a pleasure to see you at the wedding. It was a pleasure to see you at the wedding.
Thank you for the lovely bone china tea set. Thank you for the lovely bone china tea set.
Best wishes, Best wishes,
Josie Josie
Dear Uncle John, Dear Uncle John,
It is a shame you couldn't make it to the wedding. It is a shame you couldn't make it to the wedding.
Thank you for the lovely moleskin pants. Thank you for the lovely moleskin pants.
Best wishes, Best wishes,
Josie Josie
Dear Cousin Rodney, Dear Cousin Rodney,
It is a shame you couldn't make it to the wedding. It is a shame you couldn't make it to the wedding.
Best wishes, Best wishes,
Josie` Josie`
const actual = data.map(r => Gin.execute(template, r)).join('\n\n'); const actual = data.map(d => e.HTML("examples/data.html", d)).join('\n');
console.debug(Gin.execute(template, data[0]))
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}) })
// it('parseGlob', async () => { it('define', async () => {
// const gin = new Gin(); expect(e.HTML("T1", {})).toEqual('ONE')
// const paths = await gin.parseGlob('public/**/*html'); expect(e.HTML("T2", {})).toEqual('TWO')
// }) expect(e.HTML("T3", {})).toEqual('ONE TWO')
})
// it('parseHTML', async () => {
// const gin = new Gin();
// const text = await gin.parseHTML('public/pages/movie.html');
// console.log(text);
// })
}) })