FINALLY DONE! THE PARSER IS HANDWRITTEN! MY COLLAR BLUE BUT MY NECK IS REEEEEED. (goop is llm-written, but that's go->node key remapping stuff; unrelated)

This commit is contained in:
kree 2026-08-14 17:06:28 -04:00
parent cd07c310cb
commit 090d299a84
5 changed files with 132 additions and 123 deletions

19
examples/range.html Normal file
View file

@ -0,0 +1,19 @@
{{ range .FlatRate }}
<a href="{{ $.Link }}">
<img
src="https://image.tmdb.org/t/p/original{{ .LogoPath }}"
title="{{ .ProviderName }}"
alt="{{ .ProviderName }}"
class="[&[alt*=Apple]]:mix-blend-screen rounded-lg"
/>
</a>
{{ end }} {{ range .Rent }}
<a href="{{ $.Link }}">
<img
src="https://image.tmdb.org/t/p/original{{ .LogoPath }}"
title="{{ .ProviderName }}"
alt="{{ .ProviderName }}"
class="[&[alt*=Apple]]:mix-blend-screen rounded-lg"
/>
</a>
{{ end }}

View file

@ -1,40 +1,23 @@
import { Elysia, t } from 'elysia';
import { API } from './service';
import Yuck, { instance as r } from '../util/gin';
import { instance as r } from '../util/gin';
export const api = new Elysia({ prefix: '/api' })
.get("discover", async ({ query }) => {
const movie = await API.discover(query);
const releaseYear = movie.release_date.substring(0, 4);
const voteAverage = movie.vote_average.toFixed(2);
const releaseYear = movie.ReleaseDate.substring(0, 4);
const voteAverage = movie.VoteAverage.toFixed(2);
// return r.HTML("card", movie);
return Yuck.renderMoviePage(
movie.id,
movie.title,
releaseYear,
voteAverage,
movie.overview,
movie.poster_path
);
return r.HTML("card", movie);
})
.get("movie/:id", async ({ params: { id } }) => {
const movie = await API.movie(parseInt(id));
const releaseYear = movie.release_date.substring(0, 4);
const voteAverage = movie.vote_average.toFixed(2);
const releaseYear = movie.ReleaseDate.substring(0, 4);
const voteAverage = movie.VoteAverage.toFixed(2);
// return r.HTML("movie", movie);
return Yuck.renderMoviePage(
movie.id,
movie.title,
releaseYear,
voteAverage,
movie.overview,
movie.poster_path!
);
return r.HTML("movie", movie);
})
.get("providers/watch/:id", async ({ params: { id } }) => {
const providers = await API.providers(parseInt(id));
// return Gin.render("providers", movie);
return Yuck.renderProviderPage()
return r.HTML("providers", providers);
});

View file

@ -1,4 +1,5 @@
import { MovieQueryOptions, TMDB } from "tmdb-ts";
import ungoop from "../util/goop";
const tmbd = new TMDB(process.env.TMBD_API_KEY!);
@ -7,18 +8,17 @@ export class API {
const response = await tmbd.discover.movie(query);
const randomIndex = Math.floor(Math.random() * response.results.length);
const selectedMovie = response.results[randomIndex];
return selectedMovie;
return ungoop(selectedMovie);
}
static async movie(id: number) {
return await tmbd.movies.details(id);
return ungoop(await tmbd.movies.details(id));
}
static async providers(movieId: number, countryCode = 'US') {
// TODO: Fix surfacing the name of the provider somehow
const providers = await tmbd.movies.watchProviders(movieId);
const localProviders: { link: string } = (providers.results as any)[countryCode];
console.log(localProviders)
return localProviders;
return ungoop(localProviders);
}
}

View file

@ -1,6 +1,6 @@
type Node = {
type:
'Text' | 'Data' | 'Conditional' |
'Text' | 'Data' | 'Conditional' | 'Range' |
'Container' |'Marker' | 'Define' |
'TemplateReference' | 'TemplateContainer' | 'Empty'
@ -18,6 +18,9 @@ type Node = {
consequent?: Node;
alternative?: Node;
// Range
listKey?: string;
// Define
definition?: string;
@ -110,7 +113,7 @@ export class Engine {
console.log({bank: this.bank});
}
_execute(node: Node, data: Record<string, any> | string, build: string): string {
_execute(node: Node, data: Record<string, any> | string, build: string, parentData: Record<string, any> = {}): string {
if (!node) {
return build;
}
@ -135,18 +138,32 @@ export class Engine {
this._execute(node.consequent!, value, '') :
this._execute(node.alternative!, value, ''))
}
case 'Data':
console.log('ref', node.key, data)
if (node.key == ".") {
case 'Range':
const strippedListKey = node.listKey!.substring(1);
const list: any[] = data[strippedListKey] ?? []
let unwrapped = list.reduce((s, d) => {
const subBuild = node.placeholders!.map(p => this._execute(p, d, '', data)).join('')
return s + subBuild;
}, '');
return build + unwrapped;
case 'Data':
let source = data;
let key = node.key!;
if (key.startsWith('$')) {
source = parentData;
key = key.substring(1);
}
if (key == ".") {
if (typeof data === 'string') {
return build + data
return build + source
} else {
return build + JSON.stringify(data);
return build + JSON.stringify(source);
}
} else {
if (typeof data === 'object') {
const inner = data[node.key!.substring(1)]
if (typeof source === 'object') {
const strippedKey = key.substring(1)
const inner = source[strippedKey]
if (typeof inner === 'string') {
return build + inner
}
@ -184,6 +201,7 @@ function _parse(text: string, root: Node): Node {
let prevScope = root;
let scope = root;
let inConditional = false;
let inRange = false;
let textNode: Node = {
type: 'Text',
@ -193,6 +211,19 @@ function _parse(text: string, root: Node): Node {
let altPath = false;
const pushScope = (node: Node) => {
switch (scope.type) {
case 'Container':
case 'Range':
scope.placeholders!.push(node)
break;
case 'Conditional':
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
placeholders.push(node);
}
}
while (i < text.length) {
textNode.endIndex = i;
@ -211,14 +242,7 @@ function _parse(text: string, root: Node): Node {
}
textNode.text = text.substring(textNode.startIndex!, textNode.endIndex!)
switch (scope.type) {
case 'Container':
scope.placeholders!.push(textNode)
break;
case 'Conditional':
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
placeholders.push(textNode);
}
pushScope(textNode);
textNode = {
type: 'Text',
@ -237,38 +261,29 @@ function _parse(text: string, root: Node): Node {
inConditional = false;
scope = prevScope;
altPath = false;
} else if (inRange) {
inRange = false;
scope = prevScope;
} 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;
}
pushScope(command)
}
break;
case 'else':
altPath = true;
break;
}
}
else if (command.type == 'Conditional') {
} else if (command.type == 'Conditional') {
inConditional = true;
scope.placeholders!.push(command)
prevScope = scope;
scope = command!;
} else {
switch (scope.type) {
case 'Container':
} else if (command.type == 'Range') {
inRange = true;
scope.placeholders!.push(command)
break;
case 'Conditional':
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
placeholders.push(command);
break;
}
prevScope = scope;
scope = command!;
} else {
pushScope(command)
}
}
i++;
@ -312,7 +327,7 @@ function parseCommand(text: string, i: number, j: number): Node {
view = text.substring(i, j);
}
if (view.startsWith('.')) {
if (view.startsWith('.') || view.startsWith('$')) {
node = parseDataText(view);
}
@ -320,9 +335,9 @@ function parseCommand(text: string, i: number, j: number): Node {
node = parseConditionalText(text, i, j);
}
if (view == 'else' || view == 'end') {
if (view.startsWith('else') || view.startsWith('end')) {
node.type = 'Marker';
node.markerType = view;
node.markerType = view.trim() as 'else' | 'end';
}
if (view.startsWith("define")) {
@ -342,6 +357,15 @@ function parseCommand(text: string, i: number, j: number): Node {
}
node.reference = name;
}
if (view.startsWith("range")) {
node.type = 'Range';
let anchor = view.split(' ')[1];
node.listKey = anchor.trim();
node.placeholders = [];
}
return node;
};
@ -406,6 +430,12 @@ function printNode(node: Node, offset='') {
console.log(offset + 'Else:\n')
printNode(node.alternative!, offset + '\t')
break;
case 'Range':
console.log(`${offset}${node.type} [with=${node.listKey}]`);
for (let p of node.placeholders ?? []) {
printNode(p, offset + '|__');
}
break;
case 'Container':
console.log(offset + node.type)
for (let p of node.placeholders ?? []) {
@ -423,56 +453,6 @@ function printNode(node: Node, offset='') {
}
}
/**
* Stopgap :(
*/
export default abstract class Yuck {
static async renderDiscoverPage() {
const discover = await Bun.file('public/pages/discover.html').text();
const header = await Bun.file('public/templates/header.html').text();
const headerHtml = header
.replaceAll(/{{\s*[^}]*?}}/g, '');
const discoverHtml = discover
.replace('{{ template "header" . }}', headerHtml)
.replaceAll(/{{\s*[^}]*?}}/g, '')
return discoverHtml;
}
static async renderMoviePage(
id: number,
title: string,
releaseYear: string,
voteAverage: string,
overview: string,
posterPath: string
) {
const header = await Bun.file('public/templates/header.html').text();
const card = await Bun.file('public/templates/card.html').text();
const headerHtml = header
.replaceAll(/{{\s*[^}]*?}}/g, '');
const cardHtml = card
.replaceAll('{{ .ID }}', id.toString())
.replaceAll('{{ .Title }}', title)
.replaceAll('{{ .ReleaseDate }}', releaseYear)
.replaceAll('{{ .VoteAverage }}', voteAverage)
.replaceAll('{{ .Overview }}', overview)
.replaceAll('{{ .PosterPath }}', posterPath)
.replaceAll(/{{\s*[^}]*?}}/g, '')
return [headerHtml, cardHtml].join("");
}
static async renderProviderPage() {
// NAH ima implement that in the engine
return `<p>[look at me im a bunch of provider logos. yippeeee]</p>`
}
}
const instance = new Engine();
export {
instance

View file

@ -3,6 +3,33 @@
*
* 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
type Camel<S extends string> =
S extends "id" ? "ID" :
S extends `${infer H}.${infer T}` ? `${H}${Capitalize<Camel<T>>}` :
S extends `${infer H}_${infer T}` ? `${H}${Capitalize<Camel<T>>}` : S;
type Pascal<S extends string> = S extends "id" ? "ID" : Capitalize<Camel<S>>;
export type Camelize<T> =
T extends readonly (infer U)[] ? Camelize<U>[] :
T extends object ? { [K in keyof T as Pascal<K & string>]: Camelize<T[K]> } :
T;
const pascalKey = (k: string) =>
k === "id"
? "ID"
: k
.replace(/[._](\w)/g, (_, c) => c.toUpperCase())
.replace(/^\w/, (c) => c.toUpperCase());
export default function ungoop<T>(obj: T): Camelize<T> {
return (
Array.isArray(obj) ? obj.map(ungoop)
: obj && typeof obj === "object"
? Object.fromEntries(
Object.entries(obj).map(([k, v]) => [pascalKey(k), ungoop(v)]),
)
: obj
) as Camelize<T>;
}