got parser to work (mostly i think). it aint so pretty tho. ALSO BRO FUCKING CRLF
This commit is contained in:
parent
43779e110a
commit
cd07c310cb
7 changed files with 258 additions and 127 deletions
11
examples/data.html
Normal file
11
examples/data.html
Normal 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
4
examples/define.html
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{{define "T1"}}ONE{{end}}
|
||||
{{define "T2"}}TWO{{end}}
|
||||
{{define "T3"}}{{template "T1"}} {{template "T2"}}{{end}}
|
||||
{{template "T3"}}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
298
src/util/gin.ts
298
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<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':
|
||||
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!;
|
||||
|
|
@ -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,97 +392,37 @@ function parseDataText(text: string): Node {
|
|||
};
|
||||
}
|
||||
|
||||
function _execute(node: Node, data: Record<string, any> | 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<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
8
src/util/goop.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue