diff --git a/examples/range.html b/examples/range.html new file mode 100644 index 0000000..1a28455 --- /dev/null +++ b/examples/range.html @@ -0,0 +1,19 @@ +{{ range .FlatRate }} + + {{ .ProviderName }} + +{{ end }} {{ range .Rent }} + + {{ .ProviderName }} + +{{ end }} \ No newline at end of file diff --git a/src/api/index.ts b/src/api/index.ts index b942680..5cd8ea6 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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); }); diff --git a/src/api/service.ts b/src/api/service.ts index 514325f..64a35e9 100644 --- a/src/api/service.ts +++ b/src/api/service.ts @@ -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); } } \ No newline at end of file diff --git a/src/util/gin.ts b/src/util/gin.ts index b9c46aa..091c056 100644 --- a/src/util/gin.ts +++ b/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, build: string): string { + _execute(node: Node, data: Record | string, build: string, parentData: Record = {}): 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 if (command.type == 'Range') { + inRange = true; + scope.placeholders!.push(command) + prevScope = scope; + scope = command!; } 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) } } i++; @@ -312,17 +327,17 @@ 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); } - + if (view.startsWith('if') || view.trim().startsWith('with')) { 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 `

[look at me im a bunch of provider logos. yippeeee]

` - } -} - const instance = new Engine(); export { instance diff --git a/src/util/goop.ts b/src/util/goop.ts index ea5ae2a..9a219fd 100644 --- a/src/util/goop.ts +++ b/src/util/goop.ts @@ -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 "id" ? "ID" : + S extends `${infer H}.${infer T}` ? `${H}${Capitalize>}` : + S extends `${infer H}_${infer T}` ? `${H}${Capitalize>}` : S; + +type Pascal = S extends "id" ? "ID" : Capitalize>; + +export type Camelize = + T extends readonly (infer U)[] ? Camelize[] : + T extends object ? { [K in keyof T as Pascal]: Camelize } : + T; + +const pascalKey = (k: string) => + k === "id" + ? "ID" + : k + .replace(/[._](\w)/g, (_, c) => c.toUpperCase()) + .replace(/^\w/, (c) => c.toUpperCase()); + +export default function ungoop(obj: T): Camelize { + 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; +} \ No newline at end of file