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 { 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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
160
src/util/gin.ts
160
src/util/gin.ts
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue