htmx-router 1.0.0-alpha.4 → 1.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli/config.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { readFile } from "fs/promises";
2
2
  export async function ReadConfig() {
3
- return JSON.parse(await readFile(process.argv[2] || "./htmx-router.json", "utf-8"));
3
+ return JSON.parse(await readFile(process.argv[2] || "./htmx-config.json", "utf-8"));
4
4
  }
package/bin/cli/index.js CHANGED
@@ -14,8 +14,8 @@ await writeFile(config.router.output, `/*---------------------------------------
14
14
  /* eslint-disable @typescript-eslint/no-explicit-any */
15
15
 
16
16
  import { GenericContext, RouteTree } from "htmx-router/bin/router";
17
- import { RegisterDynamic } from "htmx-router/bin/util/dynamic";
18
17
  import { GetClientEntryURL } from 'htmx-router/bin/client/entry';
18
+ import { DynamicReference } from "htmx-router/bin/util/dynamic";
19
19
  import { GetMountUrl } from 'htmx-router/bin/client/mount';
20
20
  import { GetSheetUrl } from 'htmx-router/bin/util/css';
21
21
  import { RouteModule } from "htmx-router";
@@ -32,18 +32,12 @@ for (const path in modules) {
32
32
  }
33
33
 
34
34
  export function Dynamic<T extends Record<string, string>>(props: {
35
- params: T,
36
- loader: (params: T, ctx: GenericContext) => Promise<JSX.Element>
35
+ params?: T,
36
+ loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
37
37
  children?: JSX.Element
38
38
  }): JSX.Element {
39
- const path = RegisterDynamic(props.loader);
40
-
41
- const query = new URLSearchParams();
42
- for (const key in props.params) query.set(key, props.params[key]);
43
- const url = path + query.toString();
44
-
45
39
  return <div
46
- hx-get={url}
40
+ hx-get={DynamicReference(props.loader, props.params)}
47
41
  hx-trigger="load"
48
42
  hx-swap="outerHTML transition:true"
49
43
  style={{ display: "contents" }}
@@ -18,7 +18,7 @@ export async function GenerateClient(config, force = false) {
18
18
  writeFile(config.source, source
19
19
  + pivot
20
20
  + `// hash: ${hash}\n`
21
- + BuildClientServer(imported)),
21
+ + BuildClientServer(config.adapter, imported)),
22
22
  writeFile(CutString(config.source, ".", -1)[0] + ".manifest.tsx", BuildClientManifest(config.adapter, imported))
23
23
  ]);
24
24
  }
@@ -44,7 +44,13 @@ function ParseImports(source) {
44
44
  }
45
45
  return out;
46
46
  }
47
- function BuildClientServer(imported) {
47
+ function SafeScript(type, script) {
48
+ switch (type) {
49
+ case "react": return `<script dangerouslySetInnerHTML={{__html: ${script}}}></script>`;
50
+ default: return `<script>${script}</script>`;
51
+ }
52
+ }
53
+ function BuildClientServer(type, imported) {
48
54
  const names = new Array();
49
55
  for (const imp of imported) {
50
56
  if (Array.isArray(imp.mapping))
@@ -58,7 +64,7 @@ function BuildClientServer(imported) {
58
64
  + "function mount(name: string, data: string, ssr?: JSX.Element) {\n"
59
65
  + "\treturn (<>\n"
60
66
  + `\t\t<div className={island}>{ssr}</div>\n`
61
- + "\t\t<script>{`Router.mountAboveWith('${name}', ${data})`}</script>\n"
67
+ + `\t\t${SafeScript(type, "`Router.mountAboveWith('${name}', ${data})`")}\n`
62
68
  + "\t</>);\n"
63
69
  + "}\n"
64
70
  + "\n"
@@ -3,6 +3,9 @@ import { CutString } from "../helper.js";
3
3
  // this function simply exists so it can be stringified and written into the client js bundle
4
4
  function ClientMounter() {
5
5
  const theme = {
6
+ get: () => {
7
+ return (localStorage.getItem("theme") || theme.infer());
8
+ },
6
9
  infer: () => {
7
10
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
8
11
  const current = prefersDark ? 'dark' : 'light';
@@ -10,16 +13,15 @@ function ClientMounter() {
10
13
  return current;
11
14
  },
12
15
  apply: () => {
13
- const current = localStorage.getItem("theme") || theme.infer();
14
- document.documentElement.setAttribute('data-theme', current);
16
+ document.documentElement.setAttribute('data-theme', theme.get());
15
17
  },
16
18
  toggle: () => {
17
- const current = localStorage.getItem("theme") || theme.infer();
18
- if (current === "dark")
19
+ if (theme.get() === "dark")
19
20
  localStorage.setItem("theme", "light");
20
21
  else
21
22
  localStorage.setItem("theme", "dark");
22
23
  theme.apply();
24
+ return localStorage.getItem("theme");
23
25
  }
24
26
  };
25
27
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
@@ -49,6 +51,45 @@ function ClientMounter() {
49
51
  document.addEventListener("DOMContentLoaded", Mount);
50
52
  if (global.htmx)
51
53
  global.htmx.onLoad(Mount);
54
+ // Track the number of active requests
55
+ let activeRequests = 0;
56
+ const updateLoadingAttribute = () => {
57
+ if (activeRequests > 0)
58
+ document.body.setAttribute('data-loading', 'true');
59
+ else
60
+ document.body.removeAttribute('data-loading');
61
+ };
62
+ const originalXHROpen = XMLHttpRequest.prototype.open;
63
+ const originalXHRSend = XMLHttpRequest.prototype.send;
64
+ // @ts-ignore
65
+ XMLHttpRequest.prototype.open = function (...args) {
66
+ this.addEventListener('loadstart', () => {
67
+ activeRequests++;
68
+ updateLoadingAttribute();
69
+ });
70
+ this.addEventListener('loadend', () => {
71
+ activeRequests--;
72
+ updateLoadingAttribute();
73
+ });
74
+ originalXHROpen.apply(this, args);
75
+ };
76
+ XMLHttpRequest.prototype.send = function (...args) {
77
+ originalXHRSend.apply(this, args);
78
+ };
79
+ // Override fetch
80
+ const originalFetch = window.fetch;
81
+ window.fetch = async (...args) => {
82
+ activeRequests++;
83
+ updateLoadingAttribute();
84
+ try {
85
+ const response = await originalFetch(...args);
86
+ return response;
87
+ }
88
+ finally {
89
+ activeRequests--;
90
+ updateLoadingAttribute();
91
+ }
92
+ };
52
93
  return {
53
94
  mountAboveWith: RequestMount,
54
95
  theme
package/bin/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { RouteModule, CatchFunction, RenderFunction } from './types.js';
2
2
  import { RouteContext, GenericContext } from "./router.js";
3
3
  import { createRequestHandler } from './request/index.js';
4
+ import { MetaDescriptor, RenderMetaDescriptor, ShellOptions, ApplyMetaDescriptorDefaults, LdJsonObject, OpenGraph, OpenGraphImage, OpenGraphVideo, OpenGraphAudio, InferShellOptions } from './util/shell.js';
5
+ import { redirect, refresh, revalidate, text, json } from './util/response.js';
4
6
  import { Cookies, CookieOptions } from "./util/cookies.js";
5
7
  import { EventSourceConnection } from "./util/event-source.js";
8
+ import { DynamicReference } from './util/dynamic.js';
6
9
  import { StyleClass } from './util/css.js';
7
10
  import { Endpoint } from './util/endpoint.js';
8
- import { redirect, text, json, refresh } from './response.js';
9
- export { CatchFunction, CookieOptions, Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RenderFunction, RouteContext, RouteModule, StyleClass, redirect, text, json, refresh };
11
+ 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 };
package/bin/index.js CHANGED
@@ -1,8 +1,18 @@
1
1
  import { RouteContext, GenericContext } from "./router.js";
2
2
  import { createRequestHandler } from './request/index.js';
3
+ import { RenderMetaDescriptor, ApplyMetaDescriptorDefaults } from './util/shell.js';
4
+ import { redirect, refresh, revalidate, text, json } from './util/response.js';
3
5
  import { Cookies } from "./util/cookies.js";
4
6
  import { EventSourceConnection } from "./util/event-source.js";
7
+ import { DynamicReference } from './util/dynamic.js';
5
8
  import { StyleClass } from './util/css.js';
6
9
  import { Endpoint } from './util/endpoint.js';
7
- import { redirect, text, json, refresh } from './response.js';
8
- export { Cookies, createRequestHandler, Endpoint, EventSourceConnection, GenericContext, RouteContext, StyleClass, redirect, text, json, refresh };
10
+ export { createRequestHandler, RouteContext, GenericContext,
11
+ // Request helpers
12
+ Cookies, Endpoint, DynamicReference,
13
+ // CSS Helper
14
+ StyleClass,
15
+ // SSE helper
16
+ EventSourceConnection,
17
+ // Response helpers
18
+ redirect, refresh, revalidate, text, json, RenderMetaDescriptor, ApplyMetaDescriptorDefaults };
@@ -27,24 +27,20 @@ export async function Resolve(request, tree, config) {
27
27
  let response = await tree.resolve(fragments, ctx);
28
28
  if (response === null)
29
29
  response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
30
+ // Override with context headers
31
+ if (response.headers !== ctx.headers) {
32
+ for (const [key, value] of ctx.headers) {
33
+ if (ctx.headers.has(key))
34
+ continue;
35
+ response.headers.set(key, value);
36
+ }
37
+ }
30
38
  // Merge cookie changes
31
- const headers = Object.fromEntries(ctx.headers);
39
+ const headers = Object.fromEntries(response.headers);
32
40
  const cookies = ctx.cookie.export();
33
41
  if (cookies.length > 0) {
34
42
  headers['set-cookie'] = cookies;
35
43
  response.headers.set("Set-Cookie", cookies[0]); // Response object doesn't support multi-header..[]
36
44
  }
37
- // Merge context headers
38
- if (response.headers !== ctx.headers) {
39
- for (const [key, value] of response.headers) {
40
- if (!headers[key]) {
41
- headers[key] = value;
42
- continue;
43
- }
44
- if (!Array.isArray(headers[key]))
45
- headers[key] = [headers[key]];
46
- headers[key].push(value);
47
- }
48
- }
49
45
  return { response, headers };
50
46
  }
package/bin/response.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- export declare function redirect(url: string, init?: ResponseInit): Response;
2
1
  export declare function text(text: string, init?: ResponseInit): Response;
3
2
  export declare function json(data: unknown, init?: ResponseInit): Response;
4
- export declare function refresh(init?: ResponseInit): Response;
3
+ export declare function redirect(url: string, init?: ResponseInit & {
4
+ client?: boolean;
5
+ }): Response;
6
+ export declare function revalidate(init?: ResponseInit): Response;
7
+ export declare function refresh(init?: ResponseInit & {
8
+ client?: boolean;
9
+ }): Response;
package/bin/response.js CHANGED
@@ -1,18 +1,10 @@
1
- export function redirect(url, init) {
2
- init ||= {};
3
- init.statusText ||= "Temporary Redirect";
4
- init.status = 307;
5
- const res = new Response("", init);
6
- res.headers.set("X-Caught", "true");
7
- res.headers.set("Location", url);
8
- return res;
9
- }
10
1
  export function text(text, init) {
11
2
  init ||= {};
12
3
  init.statusText ||= "ok";
13
4
  init.status = 200;
14
5
  const res = new Response(text, init);
15
6
  res.headers.set("Content-Type", "text/plain");
7
+ res.headers.set("X-Caught", "true");
16
8
  return res;
17
9
  }
18
10
  export function json(data, init) {
@@ -21,6 +13,25 @@ export function json(data, init) {
21
13
  init.status = 200;
22
14
  const res = new Response(JSON.stringify(data), init);
23
15
  res.headers.set("Content-Type", "application/json");
16
+ res.headers.set("X-Caught", "true");
17
+ return res;
18
+ }
19
+ export function redirect(url, init) {
20
+ init ||= {};
21
+ init.statusText ||= "Temporary Redirect";
22
+ init.status = 307;
23
+ const res = new Response("", init);
24
+ if (!init?.client)
25
+ res.headers.set("Location", url);
26
+ res.headers.set("HX-Location", url); // use hx-boost if applicable
27
+ return res;
28
+ }
29
+ export function revalidate(init) {
30
+ init ||= {};
31
+ init.statusText ||= "ok";
32
+ init.status = 200;
33
+ const res = new Response("", init);
34
+ res.headers.set("HX-Location", "");
24
35
  return res;
25
36
  }
26
37
  export function refresh(init) {
@@ -28,6 +39,8 @@ export function refresh(init) {
28
39
  init.statusText ||= "ok";
29
40
  init.status = 200;
30
41
  const res = new Response("", init);
42
+ if (!init?.client)
43
+ res.headers.set("Refresh", "0"); // fallback
31
44
  res.headers.set("HX-Refresh", "true");
32
45
  return res;
33
46
  }
package/bin/router.js CHANGED
@@ -153,7 +153,7 @@ export class RouteTree {
153
153
  return res;
154
154
  if (res.headers.has("X-Caught"))
155
155
  return res;
156
- this.unwrap(ctx, res);
156
+ return this.unwrap(ctx, res);
157
157
  }
158
158
  return res;
159
159
  }
@@ -2,7 +2,7 @@
2
2
  * This whole file is only for internal use but the generated router for the <Dynamic> component
3
3
  */
4
4
  import { GenericContext } from "../router.js";
5
- export declare function RegisterDynamic<T>(load: Loader<T>): string;
6
- type Loader<T> = (params: T, ctx: GenericContext) => Promise<JSX.Element>;
5
+ export declare function DynamicReference<T extends Record<string, string>>(loader: Loader<T>, params?: T): string;
6
+ type Loader<T> = (ctx: GenericContext, params: T) => Promise<JSX.Element>;
7
7
  export declare function _resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
8
8
  export {};
@@ -4,17 +4,28 @@
4
4
  import { QuickHash } from "../util/hash.js";
5
5
  const registry = new Map();
6
6
  const index = new Map();
7
- export function RegisterDynamic(load) {
7
+ function Register(load) {
8
8
  const existing = index.get(load);
9
9
  if (existing)
10
10
  return existing;
11
11
  const hash = QuickHash(String(load));
12
12
  const name = `${encodeURIComponent(load.name)}-${hash}`;
13
13
  registry.set(name, load);
14
- const url = `/_/dynamic/${name}?`;
14
+ const url = `/_/dynamic/${name}`;
15
15
  index.set(load, url);
16
16
  return url;
17
17
  }
18
+ export function DynamicReference(loader, params) {
19
+ let url = Register(loader);
20
+ if (params) {
21
+ const query = new URLSearchParams();
22
+ if (params)
23
+ for (const key in params)
24
+ query.set(key, params[key]);
25
+ url += "?" + query.toString();
26
+ }
27
+ return url;
28
+ }
18
29
  export async function _resolve(fragments, ctx) {
19
30
  if (!fragments[2])
20
31
  return null;
@@ -25,5 +36,5 @@ export async function _resolve(fragments, ctx) {
25
36
  for (const [key, value] of ctx.url.searchParams)
26
37
  props[key] = value;
27
38
  ctx.headers.set("X-Partial", "true");
28
- return ctx.render(await endpoint(props, ctx));
39
+ return ctx.render(await endpoint(ctx, props));
29
40
  }
@@ -0,0 +1,11 @@
1
+ export declare function text(text: string, init?: ResponseInit): Response;
2
+ export declare function json<T>(data: T, init?: ResponseInit): Omit<Response, "json"> & {
3
+ json(): Promise<T>;
4
+ };
5
+ export declare function redirect(url: string, init?: ResponseInit & {
6
+ clientOnly?: boolean;
7
+ }): Response;
8
+ export declare function revalidate(init?: ResponseInit): Response;
9
+ export declare function refresh(init?: ResponseInit & {
10
+ clientOnly?: boolean;
11
+ }): Response;
@@ -0,0 +1,46 @@
1
+ export function text(text, init) {
2
+ init ||= {};
3
+ init.statusText ||= "ok";
4
+ init.status = 200;
5
+ const res = new Response(text, init);
6
+ res.headers.set("Content-Type", "text/plain");
7
+ res.headers.set("X-Caught", "true");
8
+ return res;
9
+ }
10
+ export function json(data, init) {
11
+ init ||= {};
12
+ init.statusText ||= "ok";
13
+ init.status = 200;
14
+ const res = new Response(JSON.stringify(data), init);
15
+ res.headers.set("Content-Type", "application/json");
16
+ res.headers.set("X-Caught", "true");
17
+ return res;
18
+ }
19
+ export function redirect(url, init) {
20
+ init ||= {};
21
+ init.statusText ||= "Temporary Redirect";
22
+ init.status = 307;
23
+ const res = new Response("", init);
24
+ if (!init?.clientOnly)
25
+ res.headers.set("Location", url);
26
+ res.headers.set("HX-Location", url); // use hx-boost if applicable
27
+ return res;
28
+ }
29
+ export function revalidate(init) {
30
+ init ||= {};
31
+ init.statusText ||= "ok";
32
+ init.status = 200;
33
+ const res = new Response("", init);
34
+ res.headers.set("HX-Location", "");
35
+ return res;
36
+ }
37
+ export function refresh(init) {
38
+ init ||= {};
39
+ init.statusText ||= "ok";
40
+ init.status = 200;
41
+ const res = new Response("", init);
42
+ if (!init?.clientOnly)
43
+ res.headers.set("Refresh", "0"); // fallback
44
+ res.headers.set("HX-Refresh", "true");
45
+ return res;
46
+ }
@@ -1,32 +1,120 @@
1
- /**
2
- * These types are just helpers which could be useful
3
- * But the goal is to add a feature in the future to help will shells merging meta data
4
- * Currently I want more experience using the slug-shell pattern before I build it out
5
- */
1
+ export type ShellOptions<D = {}> = D & MetaDescriptor;
2
+ export declare function ApplyMetaDescriptorDefaults(options: ShellOptions, defaults: Readonly<Partial<ShellOptions>>): void;
3
+ export type InferShellOptions<F> = F extends (jsx: any, options: infer U) => any ? U : never;
6
4
  export type MetaDescriptor = {
7
- charSet: "utf-8";
8
- } | {
9
- title: string;
10
- } | {
11
- name: string;
12
- content: string;
13
- } | {
14
- property: string;
15
- content: string;
16
- } | {
17
- httpEquiv: string;
18
- content: string;
19
- } | {
20
- "script:ld+json": LdJsonObject;
21
- } | {
22
- tagName: "meta" | "link";
23
- [name: string]: string;
24
- } | {
25
- [name: string]: unknown;
5
+ title?: string;
6
+ description?: string;
7
+ meta?: Record<string, string>;
8
+ og?: OpenGraph<string>;
9
+ jsonLD?: LdJsonObject[];
26
10
  };
11
+ export declare function RenderMetaDescriptor<T>(options: ShellOptions<T>): string;
27
12
  export type LdJsonObject = {
28
13
  [Key in string]?: LdJsonValue | undefined;
29
14
  };
30
- export type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
31
- export type LdJsonPrimitive = string | number | boolean | null;
32
- export type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;
15
+ type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
16
+ type LdJsonPrimitive = string | number | boolean | null;
17
+ type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;
18
+ 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;
19
+ export type OpenGraph<T extends OpenGraphType = string> = {
20
+ type?: T;
21
+ title?: string;
22
+ description?: string;
23
+ determiner?: string;
24
+ url?: string;
25
+ secure_url?: string;
26
+ locale?: string | {
27
+ base: string;
28
+ alternative: string[];
29
+ };
30
+ image?: OpenGraphImage[];
31
+ video?: OpenGraphVideo[];
32
+ audio?: OpenGraphAudio[];
33
+ } & (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 : {});
34
+ export type OpenGraphImage = {
35
+ url: string;
36
+ secure_url?: string;
37
+ type?: string;
38
+ width?: number;
39
+ height?: number;
40
+ alt?: string;
41
+ };
42
+ export type OpenGraphVideo = {
43
+ url: string;
44
+ type?: string;
45
+ secure_url?: string;
46
+ width?: number;
47
+ height?: number;
48
+ alt?: string;
49
+ };
50
+ export type OpenGraphAudio = {
51
+ url: string;
52
+ type?: string;
53
+ secure_url?: string;
54
+ };
55
+ type OpenGraphSong = {
56
+ duration?: number;
57
+ album?: Array<string | {
58
+ url: string;
59
+ disc?: number;
60
+ track?: number;
61
+ }>;
62
+ musician?: string[];
63
+ };
64
+ type OpenGraphAlbum = {
65
+ songs?: Array<string | {
66
+ url: string;
67
+ disc?: number;
68
+ track?: number;
69
+ }>;
70
+ musician?: string[];
71
+ release_date?: Date;
72
+ };
73
+ type OpenGraphPlaylist = {
74
+ songs?: Array<string | {
75
+ url: string;
76
+ disc?: number;
77
+ track?: number;
78
+ }>;
79
+ creator?: string[];
80
+ };
81
+ type OpenGraphRadioStation = {
82
+ creator?: string[];
83
+ };
84
+ type OpenGraphMovie = {
85
+ actors?: Array<string | {
86
+ url: string;
87
+ role: string;
88
+ }>;
89
+ directors?: string[];
90
+ writers?: string[];
91
+ duration?: number;
92
+ release_date?: Date;
93
+ tag: string[];
94
+ };
95
+ type OpenGraphEpisode = OpenGraphMovie & {
96
+ series?: string;
97
+ };
98
+ type OpenGraphTvShow = OpenGraphMovie;
99
+ type OpenGraphVideoOther = OpenGraphMovie;
100
+ type OpenGraphArticle = {
101
+ published_time?: Date;
102
+ modified_time?: Date;
103
+ expiration_time?: Date;
104
+ authors?: string[];
105
+ section?: string;
106
+ tag?: string;
107
+ };
108
+ type OpenGraphBook = {
109
+ authors?: string[];
110
+ isbn?: string;
111
+ release_date?: Date;
112
+ tag?: string;
113
+ };
114
+ type OpenGraphProfile = {
115
+ first_name?: string;
116
+ last_name?: string;
117
+ username?: string;
118
+ gender?: "male" | "female";
119
+ };
120
+ export {};
package/bin/util/shell.js CHANGED
@@ -1,8 +1,251 @@
1
- /**
2
- * These types are just helpers which could be useful
3
- * But the goal is to add a feature in the future to help will shells merging meta data
4
- * Currently I want more experience using the slug-shell pattern before I build it out
5
- */
6
- export {};
7
- // export type ShellOptions = { meta?: Array<MetaDescriptor> } | undefined;
8
- // export type ShellProps<T> = T & ShellOptions;
1
+ export function ApplyMetaDescriptorDefaults(options, defaults) {
2
+ if (defaults.title && !options.title)
3
+ options.title = defaults.title;
4
+ if (defaults.description && !options.description)
5
+ options.description = defaults.description;
6
+ if (defaults.meta && !options.meta)
7
+ options.meta = defaults.meta;
8
+ if (defaults.og && !options.og)
9
+ options.og = defaults.og;
10
+ if (defaults.jsonLD && !options.jsonLD)
11
+ options.jsonLD = defaults.jsonLD;
12
+ }
13
+ export function RenderMetaDescriptor(options) {
14
+ let out = "";
15
+ if (options.title)
16
+ out += `<title>${EscapeHTML(options.title)}</title>`;
17
+ if (options.description)
18
+ out += `<meta name="description" content="${EscapeHTML(options.description)}">\n`;
19
+ if (options.meta)
20
+ for (const key in options.meta) {
21
+ out += `<meta name="${EscapeHTML(key)}" content="${EscapeHTML(options.meta[key])}">\n`;
22
+ }
23
+ if (options.jsonLD)
24
+ for (const json of options.jsonLD) {
25
+ out += `<script>${EscapeHTML(JSON.stringify(json))}</script>\n`;
26
+ }
27
+ // Auto apply og:title + og:description if not present
28
+ if (options.title && !options.og?.title)
29
+ out += `<meta property="og:title" content="${EscapeHTML(options.title)}">\n`;
30
+ if (options.description && !options.og?.description)
31
+ out += `<meta property="og:description" content="${EscapeHTML(options.description)}">\n`;
32
+ // Apply open graphs
33
+ if (options.og)
34
+ out += RenderOpenGraph(options.og);
35
+ return out;
36
+ }
37
+ function RenderOpenGraph(og) {
38
+ // Manually encoding everything rather than using a loop to ensure they are in the correct order
39
+ // And to ensure extra values can't leak in creating unsafe og tags
40
+ const type = og.type || "website";
41
+ let out = RenderProperty("og:type", type);
42
+ if (og.title)
43
+ out += RenderProperty("og:title", og.title);
44
+ if (og.description)
45
+ out += RenderProperty("og:description", og.description);
46
+ if (og.determiner)
47
+ out += RenderProperty("og:determiner", og.determiner);
48
+ if (og.url)
49
+ out += RenderProperty("og:url", og.url);
50
+ if (og.secure_url)
51
+ out += RenderProperty("og:secure_url", og.secure_url);
52
+ if (og.locale) {
53
+ if (typeof og.locale === "string")
54
+ out += RenderProperty("og:locale", og.locale);
55
+ else {
56
+ out += RenderProperty("og:locale", og.locale.base);
57
+ for (const l of og.locale.alternative)
58
+ out += RenderProperty("og:locale:alternative", l);
59
+ }
60
+ }
61
+ if (og.image)
62
+ for (const img of og.image) {
63
+ out += RenderProperty("og:image", img.url);
64
+ if (img.secure_url)
65
+ out += RenderProperty("og:image:secure_url", img.secure_url);
66
+ if (img.type)
67
+ out += RenderProperty("og:image:type", img.type);
68
+ if (img.width)
69
+ out += RenderProperty("og:image:width", img.width.toString());
70
+ if (img.height)
71
+ out += RenderProperty("og:image:height", img.height.toString());
72
+ if (img.alt)
73
+ out += RenderProperty("og:image:alt", img.alt);
74
+ }
75
+ if (og.video)
76
+ for (const vid of og.video) {
77
+ out += RenderProperty("og:video", vid.url);
78
+ if (vid.secure_url)
79
+ out += RenderProperty("og:video:secure_url", vid.secure_url);
80
+ if (vid.type)
81
+ out += RenderProperty("og:video:type", vid.type);
82
+ if (vid.width)
83
+ out += RenderProperty("og:video:width", vid.width.toString());
84
+ if (vid.height)
85
+ out += RenderProperty("og:video:height", vid.height.toString());
86
+ if (vid.alt)
87
+ out += RenderProperty("og:video:alt", vid.alt);
88
+ }
89
+ if (og.audio)
90
+ for (const audio of og.audio) {
91
+ out += RenderProperty("og:audio", audio.url);
92
+ if (audio.secure_url)
93
+ out += RenderProperty("og:audio:secure_url", audio.secure_url);
94
+ if (audio.type)
95
+ out += RenderProperty("og:audio:type", audio.type);
96
+ }
97
+ return out + RenderOpenGraphExtras(og);
98
+ }
99
+ function RenderProperty(name, value) {
100
+ return `<meta property="${name}" content="${EscapeHTML(value)}">\n`;
101
+ }
102
+ function RenderOpenGraphExtras(og) {
103
+ let out = "";
104
+ if (og.type === "music.song") {
105
+ const g = og;
106
+ if (g.duration)
107
+ out += RenderProperty("og:music:duration", g.duration.toString());
108
+ if (g.album)
109
+ for (const album of g.album) {
110
+ if (typeof album === "string")
111
+ out += RenderProperty("og:music:album", album);
112
+ else {
113
+ out += RenderProperty("og:music:album", album.url);
114
+ if (album.disc)
115
+ out += RenderProperty("og:music:album:disc", album.disc.toString());
116
+ if (album.track)
117
+ out += RenderProperty("og:music:album:track", album.track.toString());
118
+ }
119
+ }
120
+ if (g.musician)
121
+ for (const profile of g.musician)
122
+ out += RenderProperty("og:music:musician", profile);
123
+ return out;
124
+ }
125
+ if (og.type === "music.album") {
126
+ const g = og;
127
+ if (g.songs)
128
+ for (const song of g.songs) {
129
+ if (typeof song === "string")
130
+ out += RenderProperty("og:music:song", song);
131
+ else {
132
+ out += RenderProperty("og:music:song", song.url);
133
+ if (song.disc)
134
+ out += RenderProperty("og:music:song:disc", song.disc.toString());
135
+ if (song.track)
136
+ out += RenderProperty("og:music:song:track", song.track.toString());
137
+ }
138
+ }
139
+ if (g.musician)
140
+ for (const profile of g.musician)
141
+ out += RenderProperty("og:music:musician", profile);
142
+ if (g.release_date)
143
+ out += RenderProperty("og:music:release_date", g.release_date.toISOString());
144
+ return out;
145
+ }
146
+ if (og.type === "music.playlist") {
147
+ const g = og;
148
+ if (g.songs)
149
+ for (const song of g.songs) {
150
+ if (typeof song === "string")
151
+ out += RenderProperty("og:music:song", song);
152
+ else {
153
+ out += RenderProperty("og:music:song", song.url);
154
+ if (song.disc)
155
+ out += RenderProperty("og:music:song:disc", song.disc.toString());
156
+ if (song.track)
157
+ out += RenderProperty("og:music:song:track", song.track.toString());
158
+ }
159
+ }
160
+ if (g.creator)
161
+ for (const profile of g.creator)
162
+ out += RenderProperty("og:music:creator", profile);
163
+ return out;
164
+ }
165
+ if (og.type === "music.radio_station") {
166
+ const g = og;
167
+ if (g.creator)
168
+ for (const profile of g.creator)
169
+ out += RenderProperty("og:music:creator", profile);
170
+ return out;
171
+ }
172
+ if (og.type === "video.movie" || og.type === "video.episode" || og.type === "video.tv_show" || og.type === "video.other") {
173
+ const g = og;
174
+ if (g.actors)
175
+ for (const actor of g.actors) {
176
+ if (typeof actor === "string")
177
+ out += RenderProperty("og:video:actor", actor);
178
+ else {
179
+ out += RenderProperty("og:video:actor", actor.url);
180
+ out += RenderProperty("og:video:actor:role", actor.role);
181
+ }
182
+ }
183
+ if (g.directors)
184
+ for (const profile of g.directors)
185
+ out += RenderProperty("og:video:director", profile);
186
+ if (g.writers)
187
+ for (const profile of g.writers)
188
+ out += RenderProperty("og:video:writer", profile);
189
+ if (g.duration)
190
+ out += RenderProperty("og:video:duration", g.duration.toString());
191
+ if (g.release_date)
192
+ out += RenderProperty("og:video:release_date", g.release_date.toISOString());
193
+ if (g.tag)
194
+ for (const tag of g.tag)
195
+ out += RenderProperty("og:video:tag", tag);
196
+ if (g.series)
197
+ out += RenderProperty("og:video:series", g.series);
198
+ }
199
+ if (og.type === "article") {
200
+ const g = og;
201
+ if (g.published_time)
202
+ out += RenderProperty("og:article:published_time", g.published_time.toISOString());
203
+ if (g.modified_time)
204
+ out += RenderProperty("og:article:modified_time", g.modified_time.toISOString());
205
+ if (g.expiration_time)
206
+ out += RenderProperty("og:article:expiration_time", g.expiration_time.toISOString());
207
+ if (g.authors)
208
+ for (const profile of g.authors)
209
+ out += RenderProperty("og:article:author", profile);
210
+ if (g.section)
211
+ out += RenderProperty("og:article:section", g.section);
212
+ if (g.tag)
213
+ for (const tag of g.tag)
214
+ out += RenderProperty("og:video:tag", tag);
215
+ }
216
+ if (og.type === "book") {
217
+ const g = og;
218
+ if (g.authors)
219
+ for (const profile of g.authors)
220
+ out += RenderProperty("og:article:author", profile);
221
+ if (g.isbn)
222
+ out += RenderProperty("og:book:isbn", g.isbn);
223
+ if (g.release_date)
224
+ out += RenderProperty("og:book:release_date", g.release_date.toISOString());
225
+ if (g.tag)
226
+ for (const tag of g.tag)
227
+ out += RenderProperty("og:video:tag", tag);
228
+ }
229
+ if (og.type === "profile") {
230
+ const g = og;
231
+ if (g.first_name)
232
+ out += RenderProperty("og:profile:first_name", g.first_name);
233
+ if (g.last_name)
234
+ out += RenderProperty("og:profile:last_name", g.last_name);
235
+ if (g.username)
236
+ out += RenderProperty("og:profile:username", g.username);
237
+ if (g.gender)
238
+ out += RenderProperty("og:profile:gender", g.gender);
239
+ }
240
+ return "";
241
+ }
242
+ const escapeTo = {
243
+ "&": "&amp;",
244
+ "<": "&lt;",
245
+ ">": "&gt;",
246
+ "\"": "&quot;",
247
+ "'": "&#39;",
248
+ };
249
+ function EscapeHTML(str) {
250
+ return str.replace(/[&<>"']/g, (match) => escapeTo[match] || match);
251
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmx-router",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "A simple SSR framework with dynamic+client islands",
5
5
  "keywords": ["htmx", "router", ""],
6
6
  "main": "./bin/index.js",