htmx-router 1.0.0-alpha.3 → 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 +1 -1
- package/bin/cli/index.js +5 -10
- package/bin/client/index.js +10 -3
- package/bin/client/mount.js +45 -4
- package/bin/index.d.ts +4 -1
- package/bin/index.js +12 -1
- package/bin/request/native.js +4 -4
- package/bin/response.d.ts +9 -0
- package/bin/response.js +46 -0
- package/bin/router.d.ts +2 -1
- package/bin/router.js +21 -25
- package/bin/types.d.ts +1 -1
- package/bin/util/dynamic.d.ts +2 -2
- package/bin/util/dynamic.js +14 -3
- package/bin/util/response.d.ts +11 -0
- package/bin/util/response.js +46 -0
- package/bin/util/shell.d.ts +115 -27
- package/bin/util/shell.js +251 -8
- package/package.json +1 -1
package/bin/cli/config.js
CHANGED
package/bin/cli/index.js
CHANGED
|
@@ -11,10 +11,11 @@ await writeFile(config.router.output, `/*---------------------------------------
|
|
|
11
11
|
* Generated by htmx-router *
|
|
12
12
|
* Warn: Any changes will be overwritten *
|
|
13
13
|
-------------------------------------------*/
|
|
14
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
14
15
|
|
|
15
16
|
import { GenericContext, RouteTree } from "htmx-router/bin/router";
|
|
16
|
-
import { RegisterDynamic } from "htmx-router/bin/util/dynamic";
|
|
17
17
|
import { GetClientEntryURL } from 'htmx-router/bin/client/entry';
|
|
18
|
+
import { DynamicReference } from "htmx-router/bin/util/dynamic";
|
|
18
19
|
import { GetMountUrl } from 'htmx-router/bin/client/mount';
|
|
19
20
|
import { GetSheetUrl } from 'htmx-router/bin/util/css';
|
|
20
21
|
import { RouteModule } from "htmx-router";
|
|
@@ -31,18 +32,12 @@ for (const path in modules) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function Dynamic<T extends Record<string, string>>(props: {
|
|
34
|
-
params
|
|
35
|
-
loader: (
|
|
35
|
+
params?: T,
|
|
36
|
+
loader: (ctx: GenericContext, params?: T) => Promise<JSX.Element>
|
|
36
37
|
children?: JSX.Element
|
|
37
38
|
}): JSX.Element {
|
|
38
|
-
const path = RegisterDynamic(props.loader);
|
|
39
|
-
|
|
40
|
-
const query = new URLSearchParams();
|
|
41
|
-
for (const key in props.params) query.set(key, props.params[key]);
|
|
42
|
-
const url = path + query.toString();
|
|
43
|
-
|
|
44
39
|
return <div
|
|
45
|
-
hx-get={
|
|
40
|
+
hx-get={DynamicReference(props.loader, props.params)}
|
|
46
41
|
hx-trigger="load"
|
|
47
42
|
hx-swap="outerHTML transition:true"
|
|
48
43
|
style={{ display: "contents" }}
|
package/bin/client/index.js
CHANGED
|
@@ -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
|
|
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
|
-
+
|
|
67
|
+
+ `\t\t${SafeScript(type, "`Router.mountAboveWith('${name}', ${data})`")}\n`
|
|
62
68
|
+ "\t</>);\n"
|
|
63
69
|
+ "}\n"
|
|
64
70
|
+ "\n"
|
|
@@ -83,6 +89,7 @@ function BuildClientManifest(type, imports) {
|
|
|
83
89
|
+ " * Generated by htmx-router *\n"
|
|
84
90
|
+ " * Warn: Any changes will be overwritten *\n"
|
|
85
91
|
+ "-------------------------------------------*/\n\n"
|
|
92
|
+
+ "/* eslint-disable @typescript-eslint/no-explicit-any */\n"
|
|
86
93
|
+ "const client = {\n";
|
|
87
94
|
const render = renderer[type];
|
|
88
95
|
if (!render) {
|
package/bin/client/mount.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
document.documentElement.setAttribute('data-theme', current);
|
|
16
|
+
document.documentElement.setAttribute('data-theme', theme.get());
|
|
15
17
|
},
|
|
16
18
|
toggle: () => {
|
|
17
|
-
|
|
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,8 +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
|
-
export { CatchFunction,
|
|
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,7 +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
|
-
export {
|
|
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 };
|
package/bin/request/native.js
CHANGED
|
@@ -26,17 +26,17 @@ export async function Resolve(request, tree, config) {
|
|
|
26
26
|
const fragments = x.split("/").slice(1);
|
|
27
27
|
let response = await tree.resolve(fragments, ctx);
|
|
28
28
|
if (response === null)
|
|
29
|
-
response = new Response("
|
|
30
|
-
//
|
|
29
|
+
response = new Response("No Route Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
|
|
30
|
+
// Override with context headers
|
|
31
31
|
if (response.headers !== ctx.headers) {
|
|
32
32
|
for (const [key, value] of ctx.headers) {
|
|
33
|
-
if (
|
|
33
|
+
if (ctx.headers.has(key))
|
|
34
34
|
continue;
|
|
35
35
|
response.headers.set(key, value);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
// Merge cookie changes
|
|
39
|
-
const headers = Object.fromEntries(
|
|
39
|
+
const headers = Object.fromEntries(response.headers);
|
|
40
40
|
const cookies = ctx.cookie.export();
|
|
41
41
|
if (cookies.length > 0) {
|
|
42
42
|
headers['set-cookie'] = cookies;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function text(text: string, init?: ResponseInit): Response;
|
|
2
|
+
export declare function json(data: unknown, 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
ADDED
|
@@ -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?.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", "");
|
|
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?.client)
|
|
43
|
+
res.headers.set("Refresh", "0"); // fallback
|
|
44
|
+
res.headers.set("HX-Refresh", "true");
|
|
45
|
+
return res;
|
|
46
|
+
}
|
package/bin/router.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare class RouteLeaf {
|
|
|
26
26
|
module: RouteModule<any>;
|
|
27
27
|
constructor(module: RouteModule<any>);
|
|
28
28
|
resolve(ctx: GenericContext): Promise<Response | null>;
|
|
29
|
-
error(ctx: GenericContext, e: unknown): Promise<Response
|
|
29
|
+
error(ctx: GenericContext, e: unknown): Promise<Response>;
|
|
30
30
|
private renderWrapper;
|
|
31
31
|
}
|
|
32
32
|
export declare class RouteTree {
|
|
@@ -39,6 +39,7 @@ export declare class RouteTree {
|
|
|
39
39
|
constructor(root?: boolean);
|
|
40
40
|
ingest(path: string | string[], module: RouteModule<any>): void;
|
|
41
41
|
resolve(fragments: string[], ctx: GenericContext): Promise<Response | null>;
|
|
42
|
+
private _resolve;
|
|
42
43
|
private resolveIndex;
|
|
43
44
|
private resolveNext;
|
|
44
45
|
private resolveWild;
|
package/bin/router.js
CHANGED
|
@@ -55,10 +55,8 @@ export class RouteLeaf {
|
|
|
55
55
|
}
|
|
56
56
|
async error(ctx, e) {
|
|
57
57
|
if (!this.module.error)
|
|
58
|
-
|
|
58
|
+
throw e;
|
|
59
59
|
const res = await this.module.error(ctx, e);
|
|
60
|
-
if (res === null)
|
|
61
|
-
return null;
|
|
62
60
|
if (res instanceof Response)
|
|
63
61
|
return res;
|
|
64
62
|
return ctx.render(res);
|
|
@@ -79,10 +77,7 @@ export class RouteLeaf {
|
|
|
79
77
|
throw new Response("Method not Allowed", { status: 405, statusText: "Method not Allowed", headers: ctx.headers });
|
|
80
78
|
}
|
|
81
79
|
catch (e) {
|
|
82
|
-
|
|
83
|
-
return await this.module.error(ctx, e);
|
|
84
|
-
else
|
|
85
|
-
throw e;
|
|
80
|
+
return await this.error(ctx, e);
|
|
86
81
|
}
|
|
87
82
|
return null;
|
|
88
83
|
}
|
|
@@ -138,12 +133,29 @@ export class RouteTree {
|
|
|
138
133
|
next.ingest(path, module);
|
|
139
134
|
}
|
|
140
135
|
async resolve(fragments, ctx) {
|
|
136
|
+
if (!this.slug)
|
|
137
|
+
return await this._resolve(fragments, ctx);
|
|
138
|
+
try {
|
|
139
|
+
return await this._resolve(fragments, ctx);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return this.unwrap(ctx, e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async _resolve(fragments, ctx) {
|
|
141
146
|
let res = await this.resolveNative(fragments, ctx)
|
|
142
147
|
|| await this.resolveIndex(fragments, ctx)
|
|
143
148
|
|| await this.resolveNext(fragments, ctx)
|
|
144
149
|
|| await this.resolveWild(fragments, ctx)
|
|
145
150
|
|| await this.resolveSlug(fragments, ctx);
|
|
146
|
-
|
|
151
|
+
if (res instanceof Response) {
|
|
152
|
+
if (100 <= res.status && res.status <= 399)
|
|
153
|
+
return res;
|
|
154
|
+
if (res.headers.has("X-Caught"))
|
|
155
|
+
return res;
|
|
156
|
+
return this.unwrap(ctx, res);
|
|
157
|
+
}
|
|
158
|
+
return res;
|
|
147
159
|
}
|
|
148
160
|
async resolveIndex(fragments, ctx) {
|
|
149
161
|
if (fragments.length > 0)
|
|
@@ -187,29 +199,13 @@ export class RouteTree {
|
|
|
187
199
|
return await ResolveNatively(fragments, ctx);
|
|
188
200
|
}
|
|
189
201
|
async unwrap(ctx, res) {
|
|
190
|
-
if (!BadResponse(res))
|
|
191
|
-
return res;
|
|
192
202
|
if (!this.slug)
|
|
193
|
-
|
|
194
|
-
if (res === null)
|
|
195
|
-
res = new Response("Not Found", { status: 404, statusText: "Not Found", headers: ctx.headers });
|
|
196
|
-
if (res.headers.has("X-Caught"))
|
|
197
|
-
return res;
|
|
203
|
+
throw res;
|
|
198
204
|
const caught = await this.slug.error(ctx, res);
|
|
199
|
-
if (!caught)
|
|
200
|
-
return res;
|
|
201
205
|
caught.headers.set("X-Caught", "true");
|
|
202
206
|
return caught;
|
|
203
207
|
}
|
|
204
208
|
}
|
|
205
|
-
function BadResponse(res) {
|
|
206
|
-
if (res === null)
|
|
207
|
-
return true;
|
|
208
|
-
if (res.status < 200)
|
|
209
|
-
return true;
|
|
210
|
-
if (res.status > 299)
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
209
|
async function ResolveNatively(fragments, ctx) {
|
|
214
210
|
switch (fragments[1]) {
|
|
215
211
|
case "dynamic": return dynamic._resolve(fragments, ctx);
|
package/bin/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ParameterShaper } from "./util/parameters.js";
|
|
2
2
|
import { RouteContext } from "./router.js";
|
|
3
|
-
export type CatchFunction<T> = (args: T, err: unknown) => Promise<Response | JSX.Element
|
|
3
|
+
export type CatchFunction<T> = (args: T, err: unknown) => Promise<Response | JSX.Element>;
|
|
4
4
|
export type RenderFunction<T> = (args: T) => Promise<Response | JSX.Element | null>;
|
|
5
5
|
export type RouteModule<T extends ParameterShaper> = {
|
|
6
6
|
parameters?: T;
|
package/bin/util/dynamic.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
type Loader<T> = (
|
|
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 {};
|
package/bin/util/dynamic.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
+
}
|
package/bin/util/shell.d.ts
CHANGED
|
@@ -1,32 +1,120 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
"&": "&",
|
|
244
|
+
"<": "<",
|
|
245
|
+
">": ">",
|
|
246
|
+
"\"": """,
|
|
247
|
+
"'": "'",
|
|
248
|
+
};
|
|
249
|
+
function EscapeHTML(str) {
|
|
250
|
+
return str.replace(/[&<>"']/g, (match) => escapeTo[match] || match);
|
|
251
|
+
}
|