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:
parent
cd07c310cb
commit
090d299a84
5 changed files with 132 additions and 123 deletions
19
examples/range.html
Normal file
19
examples/range.html
Normal 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 }}
|
||||||
|
|
@ -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()
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
160
src/util/gin.ts
160
src/util/gin.ts
|
|
@ -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 {
|
} else if (command.type == 'Range') {
|
||||||
switch (scope.type) {
|
inRange = true;
|
||||||
case 'Container':
|
|
||||||
scope.placeholders!.push(command)
|
scope.placeholders!.push(command)
|
||||||
break;
|
prevScope = scope;
|
||||||
case 'Conditional':
|
scope = command!;
|
||||||
const placeholders = (altPath) ? scope.alternative!.placeholders! : scope.consequent!.placeholders!;
|
} else {
|
||||||
placeholders.push(command);
|
pushScope(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
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue