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

View file

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

View file

@ -1,6 +1,6 @@
type Node = { type Node = {
type: type:
'Text' | 'Data' | 'Conditional' | 'Text' | 'Data' | 'Conditional' | 'Range' |
'Container' |'Marker' | 'Define' | 'Container' |'Marker' | 'Define' |
'TemplateReference' | 'TemplateContainer' | 'Empty' 'TemplateReference' | 'TemplateContainer' | 'Empty'
@ -18,6 +18,9 @@ type Node = {
consequent?: Node; consequent?: Node;
alternative?: Node; alternative?: Node;
// Range
listKey?: string;
// Define // Define
definition?: string; definition?: string;
@ -110,7 +113,7 @@ export class Engine {
console.log({bank: this.bank}); 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) { if (!node) {
return build; return build;
} }
@ -135,18 +138,32 @@ export class Engine {
this._execute(node.consequent!, value, '') : this._execute(node.consequent!, value, '') :
this._execute(node.alternative!, 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') { if (typeof data === 'string') {
return build + data return build + source
} else { } else {
return build + JSON.stringify(data); return build + JSON.stringify(source);
} }
} else { } else {
if (typeof data === 'object') { if (typeof source === 'object') {
const inner = data[node.key!.substring(1)] const strippedKey = key.substring(1)
const inner = source[strippedKey]
if (typeof inner === 'string') { if (typeof inner === 'string') {
return build + inner return build + inner
} }
@ -184,6 +201,7 @@ function _parse(text: string, root: Node): Node {
let prevScope = root; let prevScope = root;
let scope = root; let scope = root;
let inConditional = false; let inConditional = false;
let inRange = false;
let textNode: Node = { let textNode: Node = {
type: 'Text', type: 'Text',
@ -193,6 +211,19 @@ function _parse(text: string, root: Node): Node {
let altPath = false; 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) { while (i < text.length) {
textNode.endIndex = i; textNode.endIndex = i;
@ -211,14 +242,7 @@ function _parse(text: string, root: Node): Node {
} }
textNode.text = text.substring(textNode.startIndex!, textNode.endIndex!) textNode.text = text.substring(textNode.startIndex!, textNode.endIndex!)
switch (scope.type) { pushScope(textNode);
case 'Container':
scope.placeholders!.push(textNode)
break;
case 'Conditional':
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
placeholders.push(textNode);
}
textNode = { textNode = {
type: 'Text', type: 'Text',
@ -237,38 +261,29 @@ function _parse(text: string, root: Node): Node {
inConditional = false; inConditional = false;
scope = prevScope; scope = prevScope;
altPath = false; altPath = false;
} else if (inRange) {
inRange = false;
scope = prevScope;
} else { } else {
switch (scope.type) { pushScope(command)
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;
break; break;
} }
} } else if (command.type == 'Conditional') {
else if (command.type == 'Conditional') {
inConditional = true; inConditional = true;
scope.placeholders!.push(command) scope.placeholders!.push(command)
prevScope = scope; prevScope = scope;
scope = command!; scope = command!;
} else if (command.type == 'Range') {
inRange = true;
scope.placeholders!.push(command)
prevScope = scope;
scope = command!;
} else { } else {
switch (scope.type) { pushScope(command)
case 'Container':
scope.placeholders!.push(command)
break;
case 'Conditional':
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
placeholders.push(command);
break;
}
} }
} }
i++; i++;
@ -312,7 +327,7 @@ function parseCommand(text: string, i: number, j: number): Node {
view = text.substring(i, j); view = text.substring(i, j);
} }
if (view.startsWith('.')) { if (view.startsWith('.') || view.startsWith('$')) {
node = parseDataText(view); node = parseDataText(view);
} }
@ -320,9 +335,9 @@ function parseCommand(text: string, i: number, j: number): Node {
node = parseConditionalText(text, i, j); node = parseConditionalText(text, i, j);
} }
if (view == 'else' || view == 'end') { if (view.startsWith('else') || view.startsWith('end')) {
node.type = 'Marker'; node.type = 'Marker';
node.markerType = view; node.markerType = view.trim() as 'else' | 'end';
} }
if (view.startsWith("define")) { if (view.startsWith("define")) {
@ -342,6 +357,15 @@ function parseCommand(text: string, i: number, j: number): Node {
} }
node.reference = name; node.reference = name;
} }
if (view.startsWith("range")) {
node.type = 'Range';
let anchor = view.split(' ')[1];
node.listKey = anchor.trim();
node.placeholders = [];
}
return node; return node;
}; };
@ -406,6 +430,12 @@ function printNode(node: Node, offset='') {
console.log(offset + 'Else:\n') console.log(offset + 'Else:\n')
printNode(node.alternative!, offset + '\t') printNode(node.alternative!, offset + '\t')
break; break;
case 'Range':
console.log(`${offset}${node.type} [with=${node.listKey}]`);
for (let p of node.placeholders ?? []) {
printNode(p, offset + '|__');
}
break;
case 'Container': case 'Container':
console.log(offset + node.type) console.log(offset + node.type)
for (let p of node.placeholders ?? []) { 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(); const instance = new Engine();
export { export {
instance instance

View file

@ -3,6 +3,33 @@
* *
* This is just cause I gotta stick to not touching Public templates * 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>;
} }