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 { 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,

View file

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

View file

@ -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<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 {
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':
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!;
@ -149,6 +307,11 @@ function parseCommand(text: string, i: number, j: number): Node {
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,66 +392,11 @@ function parseDataText(text: string): Node {
};
}
function _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 => _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='') {
function printNode(node: Node, offset='') {
switch (node.type) {
case 'TemplateReference':
console.log(`${offset}${node.type} [name=${JSON.stringify(node.reference)}]`);
break;
case 'Conditional':
console.log(`${offset}${node.conditionType}`);
console.log(`${offset}Condition:\n`)
@ -297,11 +423,6 @@ export namespace Gin {
}
}
export function execute(template: Template, data: Record<string, any>) {
return _execute(template.root, data, '');
}
};
/**
* Stopgap :(
*/
@ -351,3 +472,8 @@ export default abstract class Yuck {
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 { 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',
@ -46,34 +35,25 @@ 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')
})
})