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 { 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,
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
258
src/util/gin.ts
258
src/util/gin.ts
|
|
@ -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`)
|
||||||
|
|
@ -297,11 +423,6 @@ export namespace Gin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
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 { 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',
|
||||||
|
|
@ -46,34 +35,25 @@ 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);
|
|
||||||
// })
|
|
||||||
})
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue