htmx-router
Advanced tools
Comparing version 1.0.0-alpha.4 to 1.0.0-alpha.5
import { readFile } from "fs/promises"; | ||
export async function ReadConfig() { | ||
return JSON.parse(await readFile(process.argv[2] || "./htmx-router.json", "utf-8")); | ||
return JSON.parse(await readFile(process.argv[2] || "./htmx-config.json", "utf-8")); | ||
} |
@@ -17,4 +17,4 @@ #!/usr/bin/env node | ||
import { GenericContext, RouteTree } from "htmx-router/bin/router"; | ||
import { RegisterDynamic } from "htmx-router/bin/util/dynamic"; | ||
import { GetClientEntryURL } from 'htmx-router/bin/client/entry'; | ||
import { DynamicReference } from "htmx-router/bin/util/dynamic"; | ||
import { GetMountUrl } from 'htmx-router/bin/client/mount'; | ||
@@ -36,14 +36,8 @@ import { GetSheetUrl } from 'htmx-router/bin/util/css'; | ||
export function Dynamic<T extends Record<string, string>>(props: { | ||
params: T, | ||
loader: (params: T, ctx: GenericContext) => Promise<JSX.Element> | ||
params?: T, | ||
loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element> | ||
children?: JSX.Element | ||
}): JSX.Element { | ||
const path = RegisterDynamic(props.loader); | ||
const query = new URLSearchParams(); | ||
for (const key in props.params) query.set(key, props.params[key]); | ||
const url = path + query.toString(); | ||
return <div | ||
hx-get={url} | ||
hx-get={DynamicReference(props.loader, props.params)} | ||
hx-trigger="load" | ||
@@ -50,0 +44,0 @@ hx-swap="outerHTML transition:true" |
@@ -21,3 +21,3 @@ /** | ||
+ `// hash: ${hash}\n` | ||
+ BuildClientServer(imported)), | ||
+ BuildClientServer(config.adapter, imported)), | ||
writeFile(CutString(config.source, ".", -1)[0] + ".manifest.tsx", BuildClientManifest(config.adapter, imported)) | ||
@@ -48,3 +48,9 @@ ]); | ||
} | ||
function BuildClientServer(imported) { | ||
function SafeScript(type, script) { | ||
switch (type) { | ||
case "react": return `<script dangerouslySetInnerHTML={{__html: ${script}}}></script>`; | ||
default: return `<script>${script}</script>`; | ||
} | ||
} | ||
function BuildClientServer(type, imported) { | ||
const names = new Array(); | ||
@@ -63,3 +69,3 @@ for (const imp of imported) { | ||
+ `\t\t<div className={island}>{ssr}</div>\n` | ||
+ "\t\t<script>{`Router.mountAboveWith('${name}', ${data})`}</script>\n" | ||
+ `\t\t${SafeScript(type, "`Router.mountAboveWith('${name}', ${data})`")}\n` | ||
+ "\t</>);\n" | ||
@@ -66,0 +72,0 @@ + "}\n" |
@@ -6,2 +6,5 @@ import { QuickHash } from "../util/hash.js"; | ||
const theme = { | ||
get: () => { | ||
return (localStorage.getItem("theme") || theme.infer()); | ||
}, | ||
infer: () => { | ||
@@ -14,8 +17,6 @@ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||
apply: () => { | ||
const current = localStorage.getItem("theme") || theme.infer(); | ||
document.documentElement.setAttribute('data-theme', current); | ||
document.documentElement.setAttribute('data-theme', theme.get()); | ||
}, | ||
toggle: () => { | ||
const current = localStorage.getItem("theme") || theme.infer(); | ||
if (current === "dark") | ||
if (theme.get() === "dark") | ||
localStorage.setItem("theme", "light"); | ||
@@ -25,2 +26,3 @@ else | ||
theme.apply(); | ||
return localStorage.getItem("theme"); | ||
} | ||
@@ -55,2 +57,41 @@ }; | ||
global.htmx.onLoad(Mount); | ||
// Track the number of active requests | ||
let activeRequests = 0; | ||
const updateLoadingAttribute = () => { | ||
if (activeRequests > 0) | ||
document.body.setAttribute('data-loading', 'true'); | ||
else | ||
document.body.removeAttribute('data-loading'); | ||
}; | ||
const originalXHROpen = XMLHttpRequest.prototype.open; | ||
const originalXHRSend = XMLHttpRequest.prototype.send; | ||
// @ts-ignore | ||
XMLHttpRequest.prototype.open = function (...args) { | ||
this.addEventListener('loadstart', () => { | ||
activeRequests++; | ||
updateLoadingAttribute(); | ||
}); | ||
this.addEventListener('loadend', () => { | ||
activeRequests--; | ||
updateLoadingAttribute(); | ||
}); | ||
originalXHROpen.apply(this, args); | ||
}; | ||
XMLHttpRequest.prototype.send = function (...args) { | ||
originalXHRSend.apply(this, args); | ||
}; | ||
// Override fetch | ||
const originalFetch = window.fetch; | ||
window.fetch = async (...args) => { | ||
activeRequests++; | ||
updateLoadingAttribute(); | ||
try { | ||
const response = await originalFetch(...args); | ||
return response; | ||
} | ||
finally { | ||
activeRequests--; | ||
updateLoadingAttribute(); | ||
} | ||
}; | ||
return { | ||
@@ -57,0 +98,0 @@ mountAboveWith: RequestMount, |
import { RouteModule, CatchFunction, RenderFunction } from './types.js'; | ||
import { RouteContext, GenericContext } from "./router.js"; | ||
import { createRequestHandler } from './request/index.js'; | ||
import { MetaDescriptor, RenderMetaDescriptor, ShellOptions, ApplyMetaDescriptorDefaults, LdJsonObject, OpenGraph, OpenGraphImage, OpenGraphVideo, OpenGraphAudio, InferShellOptions } from './util/shell.js'; | ||
import { redirect, refresh, revalidate, text, json } from './util/response.js'; | ||
import { Cookies, CookieOptions } from "./util/cookies.js"; | ||
import { EventSourceConnection } from "./util/event-source.js"; | ||
import { DynamicReference } from './util/dynamic.js'; | ||
import { StyleClass } from './util/css.js'; | ||
import { Endpoint } from './util/endpoint.js'; | ||
import { redirect, text, json, refresh } from './response.js'; | ||
export { CatchFunction, CookieOptions, Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RenderFunction, RouteContext, RouteModule, StyleClass, redirect, text, json, refresh }; | ||
export { createRequestHandler, CatchFunction, RenderFunction, RouteContext, RouteModule, GenericContext, Cookies, CookieOptions, Endpoint, DynamicReference, StyleClass, EventSourceConnection, redirect, refresh, revalidate, text, json, MetaDescriptor, RenderMetaDescriptor, ShellOptions, ApplyMetaDescriptorDefaults, LdJsonObject, OpenGraph, OpenGraphImage, OpenGraphVideo, OpenGraphAudio, InferShellOptions }; |
import { RouteContext, GenericContext } from "./router.js"; | ||
import { createRequestHandler } from './request/index.js'; | ||
import { RenderMetaDescriptor, ApplyMetaDescriptorDefaults } from './util/shell.js'; | ||
import { redirect, refresh, revalidate, text, json } from './util/response.js'; | ||
import { Cookies } from "./util/cookies.js"; | ||
import { EventSourceConnection } from "./util/event-source.js"; | ||
import { DynamicReference } from './util/dynamic.js'; | ||
import { StyleClass } from './util/css.js'; | ||
import { Endpoint } from './util/endpoint.js'; | ||
import { redirect, text, json, refresh } from './response.js'; | ||
export { Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RouteContext, StyleClass, redirect, text, json, refresh }; | ||
export { createRequestHandler, RouteContext, GenericContext, | ||
// Request helpers | ||
Cookies, Endpoint, DynamicReference, | ||
// CSS Helper | ||
StyleClass, | ||
// SSE helper | ||
EventSourceConnection, | ||
// Response helpers | ||
redirect, refresh, revalidate, text, json, RenderMetaDescriptor, ApplyMetaDescriptorDefaults }; |
@@ -30,4 +30,12 @@ import { GenericContext } from '../router.js'; | ||
response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers }); | ||
// Override with context headers | ||
if (response.headers !== ctx.headers) { | ||
for (const [key, value] of ctx.headers) { | ||
if (ctx.headers.has(key)) | ||
continue; | ||
response.headers.set(key, value); | ||
} | ||
} | ||
// Merge cookie changes | ||
const headers = Object.fromEntries(ctx.headers); | ||
const headers = Object.fromEntries(response.headers); | ||
const cookies = ctx.cookie.export(); | ||
@@ -38,15 +46,3 @@ if (cookies.length > 0) { | ||
} | ||
// Merge context headers | ||
if (response.headers !== ctx.headers) { | ||
for (const [key, value] of response.headers) { | ||
if (!headers[key]) { | ||
headers[key] = value; | ||
continue; | ||
} | ||
if (!Array.isArray(headers[key])) | ||
headers[key] = [headers[key]]; | ||
headers[key].push(value); | ||
} | ||
} | ||
return { response, headers }; | ||
} |
@@ -1,4 +0,9 @@ | ||
export declare function redirect(url: string, init?: ResponseInit): Response; | ||
export declare function text(text: string, init?: ResponseInit): Response; | ||
export declare function json(data: unknown, init?: ResponseInit): Response; | ||
export declare function refresh(init?: ResponseInit): Response; | ||
export declare function redirect(url: string, init?: ResponseInit & { | ||
client?: boolean; | ||
}): Response; | ||
export declare function revalidate(init?: ResponseInit): Response; | ||
export declare function refresh(init?: ResponseInit & { | ||
client?: boolean; | ||
}): Response; |
@@ -1,10 +0,1 @@ | ||
export function redirect(url, init) { | ||
init ||= {}; | ||
init.statusText ||= "Temporary Redirect"; | ||
init.status = 307; | ||
const res = new Response("", init); | ||
res.headers.set("X-Caught", "true"); | ||
res.headers.set("Location", url); | ||
return res; | ||
} | ||
export function text(text, init) { | ||
@@ -16,2 +7,3 @@ init ||= {}; | ||
res.headers.set("Content-Type", "text/plain"); | ||
res.headers.set("X-Caught", "true"); | ||
return res; | ||
@@ -25,4 +17,23 @@ } | ||
res.headers.set("Content-Type", "application/json"); | ||
res.headers.set("X-Caught", "true"); | ||
return res; | ||
} | ||
export function redirect(url, init) { | ||
init ||= {}; | ||
init.statusText ||= "Temporary Redirect"; | ||
init.status = 307; | ||
const res = new Response("", init); | ||
if (!init?.client) | ||
res.headers.set("Location", url); | ||
res.headers.set("HX-Location", url); // use hx-boost if applicable | ||
return res; | ||
} | ||
export function revalidate(init) { | ||
init ||= {}; | ||
init.statusText ||= "ok"; | ||
init.status = 200; | ||
const res = new Response("", init); | ||
res.headers.set("HX-Location", ""); | ||
return res; | ||
} | ||
export function refresh(init) { | ||
@@ -33,4 +44,6 @@ init ||= {}; | ||
const res = new Response("", init); | ||
if (!init?.client) | ||
res.headers.set("Refresh", "0"); // fallback | ||
res.headers.set("HX-Refresh", "true"); | ||
return res; | ||
} |
@@ -156,3 +156,3 @@ import * as endpoint from './util/endpoint.js'; | ||
return res; | ||
this.unwrap(ctx, res); | ||
return this.unwrap(ctx, res); | ||
} | ||
@@ -159,0 +159,0 @@ return res; |
@@ -5,5 +5,5 @@ /** | ||
import { GenericContext } from "../router.js"; | ||
export declare function RegisterDynamic<T>(load: Loader<T>): string; | ||
type Loader<T> = (params: T, ctx: GenericContext) => Promise<JSX.Element>; | ||
export declare function DynamicReference<T extends Record<string, string>>(loader: Loader<T>, params?: T): string; | ||
type Loader<T> = (ctx: GenericContext, params: T) => Promise<JSX.Element>; | ||
export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>; | ||
export {}; |
@@ -7,3 +7,3 @@ /** | ||
const index = new Map(); | ||
export function RegisterDynamic(load) { | ||
function Register(load) { | ||
const existing = index.get(load); | ||
@@ -15,6 +15,17 @@ if (existing) | ||
registry.set(name, load); | ||
const url = `/_/dynamic/${name}?`; | ||
const url = `/_/dynamic/${name}`; | ||
index.set(load, url); | ||
return url; | ||
} | ||
export function DynamicReference(loader, params) { | ||
let url = Register(loader); | ||
if (params) { | ||
const query = new URLSearchParams(); | ||
if (params) | ||
for (const key in params) | ||
query.set(key, params[key]); | ||
url += "?" + query.toString(); | ||
} | ||
return url; | ||
} | ||
export async function _resolve(fragments, ctx) { | ||
@@ -30,3 +41,3 @@ if (!fragments[2]) | ||
ctx.headers.set("X-Partial", "true"); | ||
return ctx.render(await endpoint(props, ctx)); | ||
return ctx.render(await endpoint(ctx, props)); | ||
} |
@@ -1,32 +0,120 @@ | ||
/** | ||
* These types are just helpers which could be useful | ||
* But the goal is to add a feature in the future to help will shells merging meta data | ||
* Currently I want more experience using the slug-shell pattern before I build it out | ||
*/ | ||
export type ShellOptions<D = {}> = D & MetaDescriptor; | ||
export declare function ApplyMetaDescriptorDefaults(options: ShellOptions, defaults: Readonly<Partial<ShellOptions>>): void; | ||
export type InferShellOptions<F> = F extends (jsx: any, options: infer U) => any ? U : never; | ||
export type MetaDescriptor = { | ||
charSet: "utf-8"; | ||
} | { | ||
title: string; | ||
} | { | ||
name: string; | ||
content: string; | ||
} | { | ||
property: string; | ||
content: string; | ||
} | { | ||
httpEquiv: string; | ||
content: string; | ||
} | { | ||
"script:ld+json": LdJsonObject; | ||
} | { | ||
tagName: "meta" | "link"; | ||
[name: string]: string; | ||
} | { | ||
[name: string]: unknown; | ||
title?: string; | ||
description?: string; | ||
meta?: Record<string, string>; | ||
og?: OpenGraph<string>; | ||
jsonLD?: LdJsonObject[]; | ||
}; | ||
export declare function RenderMetaDescriptor<T>(options: ShellOptions<T>): string; | ||
export type LdJsonObject = { | ||
[Key in string]?: LdJsonValue | undefined; | ||
}; | ||
export type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[]; | ||
export type LdJsonPrimitive = string | number | boolean | null; | ||
export type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray; | ||
type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[]; | ||
type LdJsonPrimitive = string | number | boolean | null; | ||
type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray; | ||
export type OpenGraphType = "website" | "article" | "book" | "profile" | "music.song" | "music.album" | "music.playlist" | "music.radio_station" | "video.movie" | "video.episode" | "video.tv_show" | "video.other" | string; | ||
export type OpenGraph<T extends OpenGraphType = string> = { | ||
type?: T; | ||
title?: string; | ||
description?: string; | ||
determiner?: string; | ||
url?: string; | ||
secure_url?: string; | ||
locale?: string | { | ||
base: string; | ||
alternative: string[]; | ||
}; | ||
image?: OpenGraphImage[]; | ||
video?: OpenGraphVideo[]; | ||
audio?: OpenGraphAudio[]; | ||
} & (T extends "music.song" ? OpenGraphSong : T extends "music.album" ? OpenGraphAlbum : T extends "music.playlist" ? OpenGraphPlaylist : T extends "music.radio_station" ? OpenGraphRadioStation : T extends "video.movie" ? OpenGraphMovie : T extends "video.episode" ? OpenGraphEpisode : T extends "video.tv_show" ? OpenGraphTvShow : T extends "video.other" ? OpenGraphVideoOther : T extends "article" ? OpenGraphArticle : T extends "book" ? OpenGraphBook : T extends "profile" ? OpenGraphProfile : {}); | ||
export type OpenGraphImage = { | ||
url: string; | ||
secure_url?: string; | ||
type?: string; | ||
width?: number; | ||
height?: number; | ||
alt?: string; | ||
}; | ||
export type OpenGraphVideo = { | ||
url: string; | ||
type?: string; | ||
secure_url?: string; | ||
width?: number; | ||
height?: number; | ||
alt?: string; | ||
}; | ||
export type OpenGraphAudio = { | ||
url: string; | ||
type?: string; | ||
secure_url?: string; | ||
}; | ||
type OpenGraphSong = { | ||
duration?: number; | ||
album?: Array<string | { | ||
url: string; | ||
disc?: number; | ||
track?: number; | ||
}>; | ||
musician?: string[]; | ||
}; | ||
type OpenGraphAlbum = { | ||
songs?: Array<string | { | ||
url: string; | ||
disc?: number; | ||
track?: number; | ||
}>; | ||
musician?: string[]; | ||
release_date?: Date; | ||
}; | ||
type OpenGraphPlaylist = { | ||
songs?: Array<string | { | ||
url: string; | ||
disc?: number; | ||
track?: number; | ||
}>; | ||
creator?: string[]; | ||
}; | ||
type OpenGraphRadioStation = { | ||
creator?: string[]; | ||
}; | ||
type OpenGraphMovie = { | ||
actors?: Array<string | { | ||
url: string; | ||
role: string; | ||
}>; | ||
directors?: string[]; | ||
writers?: string[]; | ||
duration?: number; | ||
release_date?: Date; | ||
tag: string[]; | ||
}; | ||
type OpenGraphEpisode = OpenGraphMovie & { | ||
series?: string; | ||
}; | ||
type OpenGraphTvShow = OpenGraphMovie; | ||
type OpenGraphVideoOther = OpenGraphMovie; | ||
type OpenGraphArticle = { | ||
published_time?: Date; | ||
modified_time?: Date; | ||
expiration_time?: Date; | ||
authors?: string[]; | ||
section?: string; | ||
tag?: string; | ||
}; | ||
type OpenGraphBook = { | ||
authors?: string[]; | ||
isbn?: string; | ||
release_date?: Date; | ||
tag?: string; | ||
}; | ||
type OpenGraphProfile = { | ||
first_name?: string; | ||
last_name?: string; | ||
username?: string; | ||
gender?: "male" | "female"; | ||
}; | ||
export {}; |
@@ -1,8 +0,251 @@ | ||
/** | ||
* These types are just helpers which could be useful | ||
* But the goal is to add a feature in the future to help will shells merging meta data | ||
* Currently I want more experience using the slug-shell pattern before I build it out | ||
*/ | ||
export {}; | ||
// export type ShellOptions = { meta?: Array<MetaDescriptor> } | undefined; | ||
// export type ShellProps<T> = T & ShellOptions; | ||
export function ApplyMetaDescriptorDefaults(options, defaults) { | ||
if (defaults.title && !options.title) | ||
options.title = defaults.title; | ||
if (defaults.description && !options.description) | ||
options.description = defaults.description; | ||
if (defaults.meta && !options.meta) | ||
options.meta = defaults.meta; | ||
if (defaults.og && !options.og) | ||
options.og = defaults.og; | ||
if (defaults.jsonLD && !options.jsonLD) | ||
options.jsonLD = defaults.jsonLD; | ||
} | ||
export function RenderMetaDescriptor(options) { | ||
let out = ""; | ||
if (options.title) | ||
out += `<title>${EscapeHTML(options.title)}</title>`; | ||
if (options.description) | ||
out += `<meta name="description" content="${EscapeHTML(options.description)}">\n`; | ||
if (options.meta) | ||
for (const key in options.meta) { | ||
out += `<meta name="${EscapeHTML(key)}" content="${EscapeHTML(options.meta[key])}">\n`; | ||
} | ||
if (options.jsonLD) | ||
for (const json of options.jsonLD) { | ||
out += `<script>${EscapeHTML(JSON.stringify(json))}</script>\n`; | ||
} | ||
// Auto apply og:title + og:description if not present | ||
if (options.title && !options.og?.title) | ||
out += `<meta property="og:title" content="${EscapeHTML(options.title)}">\n`; | ||
if (options.description && !options.og?.description) | ||
out += `<meta property="og:description" content="${EscapeHTML(options.description)}">\n`; | ||
// Apply open graphs | ||
if (options.og) | ||
out += RenderOpenGraph(options.og); | ||
return out; | ||
} | ||
function RenderOpenGraph(og) { | ||
// Manually encoding everything rather than using a loop to ensure they are in the correct order | ||
// And to ensure extra values can't leak in creating unsafe og tags | ||
const type = og.type || "website"; | ||
let out = RenderProperty("og:type", type); | ||
if (og.title) | ||
out += RenderProperty("og:title", og.title); | ||
if (og.description) | ||
out += RenderProperty("og:description", og.description); | ||
if (og.determiner) | ||
out += RenderProperty("og:determiner", og.determiner); | ||
if (og.url) | ||
out += RenderProperty("og:url", og.url); | ||
if (og.secure_url) | ||
out += RenderProperty("og:secure_url", og.secure_url); | ||
if (og.locale) { | ||
if (typeof og.locale === "string") | ||
out += RenderProperty("og:locale", og.locale); | ||
else { | ||
out += RenderProperty("og:locale", og.locale.base); | ||
for (const l of og.locale.alternative) | ||
out += RenderProperty("og:locale:alternative", l); | ||
} | ||
} | ||
if (og.image) | ||
for (const img of og.image) { | ||
out += RenderProperty("og:image", img.url); | ||
if (img.secure_url) | ||
out += RenderProperty("og:image:secure_url", img.secure_url); | ||
if (img.type) | ||
out += RenderProperty("og:image:type", img.type); | ||
if (img.width) | ||
out += RenderProperty("og:image:width", img.width.toString()); | ||
if (img.height) | ||
out += RenderProperty("og:image:height", img.height.toString()); | ||
if (img.alt) | ||
out += RenderProperty("og:image:alt", img.alt); | ||
} | ||
if (og.video) | ||
for (const vid of og.video) { | ||
out += RenderProperty("og:video", vid.url); | ||
if (vid.secure_url) | ||
out += RenderProperty("og:video:secure_url", vid.secure_url); | ||
if (vid.type) | ||
out += RenderProperty("og:video:type", vid.type); | ||
if (vid.width) | ||
out += RenderProperty("og:video:width", vid.width.toString()); | ||
if (vid.height) | ||
out += RenderProperty("og:video:height", vid.height.toString()); | ||
if (vid.alt) | ||
out += RenderProperty("og:video:alt", vid.alt); | ||
} | ||
if (og.audio) | ||
for (const audio of og.audio) { | ||
out += RenderProperty("og:audio", audio.url); | ||
if (audio.secure_url) | ||
out += RenderProperty("og:audio:secure_url", audio.secure_url); | ||
if (audio.type) | ||
out += RenderProperty("og:audio:type", audio.type); | ||
} | ||
return out + RenderOpenGraphExtras(og); | ||
} | ||
function RenderProperty(name, value) { | ||
return `<meta property="${name}" content="${EscapeHTML(value)}">\n`; | ||
} | ||
function RenderOpenGraphExtras(og) { | ||
let out = ""; | ||
if (og.type === "music.song") { | ||
const g = og; | ||
if (g.duration) | ||
out += RenderProperty("og:music:duration", g.duration.toString()); | ||
if (g.album) | ||
for (const album of g.album) { | ||
if (typeof album === "string") | ||
out += RenderProperty("og:music:album", album); | ||
else { | ||
out += RenderProperty("og:music:album", album.url); | ||
if (album.disc) | ||
out += RenderProperty("og:music:album:disc", album.disc.toString()); | ||
if (album.track) | ||
out += RenderProperty("og:music:album:track", album.track.toString()); | ||
} | ||
} | ||
if (g.musician) | ||
for (const profile of g.musician) | ||
out += RenderProperty("og:music:musician", profile); | ||
return out; | ||
} | ||
if (og.type === "music.album") { | ||
const g = og; | ||
if (g.songs) | ||
for (const song of g.songs) { | ||
if (typeof song === "string") | ||
out += RenderProperty("og:music:song", song); | ||
else { | ||
out += RenderProperty("og:music:song", song.url); | ||
if (song.disc) | ||
out += RenderProperty("og:music:song:disc", song.disc.toString()); | ||
if (song.track) | ||
out += RenderProperty("og:music:song:track", song.track.toString()); | ||
} | ||
} | ||
if (g.musician) | ||
for (const profile of g.musician) | ||
out += RenderProperty("og:music:musician", profile); | ||
if (g.release_date) | ||
out += RenderProperty("og:music:release_date", g.release_date.toISOString()); | ||
return out; | ||
} | ||
if (og.type === "music.playlist") { | ||
const g = og; | ||
if (g.songs) | ||
for (const song of g.songs) { | ||
if (typeof song === "string") | ||
out += RenderProperty("og:music:song", song); | ||
else { | ||
out += RenderProperty("og:music:song", song.url); | ||
if (song.disc) | ||
out += RenderProperty("og:music:song:disc", song.disc.toString()); | ||
if (song.track) | ||
out += RenderProperty("og:music:song:track", song.track.toString()); | ||
} | ||
} | ||
if (g.creator) | ||
for (const profile of g.creator) | ||
out += RenderProperty("og:music:creator", profile); | ||
return out; | ||
} | ||
if (og.type === "music.radio_station") { | ||
const g = og; | ||
if (g.creator) | ||
for (const profile of g.creator) | ||
out += RenderProperty("og:music:creator", profile); | ||
return out; | ||
} | ||
if (og.type === "video.movie" || og.type === "video.episode" || og.type === "video.tv_show" || og.type === "video.other") { | ||
const g = og; | ||
if (g.actors) | ||
for (const actor of g.actors) { | ||
if (typeof actor === "string") | ||
out += RenderProperty("og:video:actor", actor); | ||
else { | ||
out += RenderProperty("og:video:actor", actor.url); | ||
out += RenderProperty("og:video:actor:role", actor.role); | ||
} | ||
} | ||
if (g.directors) | ||
for (const profile of g.directors) | ||
out += RenderProperty("og:video:director", profile); | ||
if (g.writers) | ||
for (const profile of g.writers) | ||
out += RenderProperty("og:video:writer", profile); | ||
if (g.duration) | ||
out += RenderProperty("og:video:duration", g.duration.toString()); | ||
if (g.release_date) | ||
out += RenderProperty("og:video:release_date", g.release_date.toISOString()); | ||
if (g.tag) | ||
for (const tag of g.tag) | ||
out += RenderProperty("og:video:tag", tag); | ||
if (g.series) | ||
out += RenderProperty("og:video:series", g.series); | ||
} | ||
if (og.type === "article") { | ||
const g = og; | ||
if (g.published_time) | ||
out += RenderProperty("og:article:published_time", g.published_time.toISOString()); | ||
if (g.modified_time) | ||
out += RenderProperty("og:article:modified_time", g.modified_time.toISOString()); | ||
if (g.expiration_time) | ||
out += RenderProperty("og:article:expiration_time", g.expiration_time.toISOString()); | ||
if (g.authors) | ||
for (const profile of g.authors) | ||
out += RenderProperty("og:article:author", profile); | ||
if (g.section) | ||
out += RenderProperty("og:article:section", g.section); | ||
if (g.tag) | ||
for (const tag of g.tag) | ||
out += RenderProperty("og:video:tag", tag); | ||
} | ||
if (og.type === "book") { | ||
const g = og; | ||
if (g.authors) | ||
for (const profile of g.authors) | ||
out += RenderProperty("og:article:author", profile); | ||
if (g.isbn) | ||
out += RenderProperty("og:book:isbn", g.isbn); | ||
if (g.release_date) | ||
out += RenderProperty("og:book:release_date", g.release_date.toISOString()); | ||
if (g.tag) | ||
for (const tag of g.tag) | ||
out += RenderProperty("og:video:tag", tag); | ||
} | ||
if (og.type === "profile") { | ||
const g = og; | ||
if (g.first_name) | ||
out += RenderProperty("og:profile:first_name", g.first_name); | ||
if (g.last_name) | ||
out += RenderProperty("og:profile:last_name", g.last_name); | ||
if (g.username) | ||
out += RenderProperty("og:profile:username", g.username); | ||
if (g.gender) | ||
out += RenderProperty("og:profile:gender", g.gender); | ||
} | ||
return ""; | ||
} | ||
const escapeTo = { | ||
"&": "&", | ||
"<": "<", | ||
">": ">", | ||
"\"": """, | ||
"'": "'", | ||
}; | ||
function EscapeHTML(str) { | ||
return str.replace(/[&<>"']/g, (match) => escapeTo[match] || match); | ||
} |
{ | ||
"name": "htmx-router", | ||
"version": "1.0.0-alpha.4", | ||
"version": "1.0.0-alpha.5", | ||
"description": "A simple SSR framework with dynamic+client islands", | ||
@@ -5,0 +5,0 @@ "keywords": ["htmx", "router", ""], |
71235
52
1749