@codepress/blog 0.0.0-stage → 0.1.0
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/README.md +118 -2
- package/dist/blog.css +227 -0
- package/dist/chunks/BlogEditor-VF3UAJB6.js +2527 -0
- package/dist/chunks/chunk-3GCZFCUK.js +51 -0
- package/dist/chunks/chunk-3WRTL63O.js +106 -0
- package/dist/chunks/chunk-AEQDRBA3.js +128 -0
- package/dist/chunks/chunk-AJVVTSKZ.js +90 -0
- package/dist/chunks/chunk-EICE5ONB.js +197 -0
- package/dist/chunks/chunk-HWR3R5MU.js +48 -0
- package/dist/chunks/chunk-QX6276YA.js +0 -0
- package/dist/chunks/chunk-TYX2HJZC.js +68 -0
- package/dist/chunks/chunk-UA243KAQ.js +104 -0
- package/dist/chunks/chunk-WJVZD4UI.js +114 -0
- package/dist/chunks/chunk-XLJBCIXI.js +93 -0
- package/dist/cli.js +440 -0
- package/dist/content-source.d.ts +31 -0
- package/dist/editor/BlogEditor.d.ts +37 -0
- package/dist/editor/BlogEditorMount.d.ts +64 -0
- package/dist/editor/DocumentActions.d.ts +39 -0
- package/dist/editor/EditDesignLink.d.ts +30 -0
- package/dist/editor/NewPostForm.d.ts +10 -0
- package/dist/editor/SlashMenu.d.ts +5 -0
- package/dist/editor/Toolbar.d.ts +39 -0
- package/dist/editor/blocks.d.ts +5 -0
- package/dist/editor/client.d.ts +88 -0
- package/dist/editor/client.js +6 -0
- package/dist/editor/component-node.d.ts +14 -0
- package/dist/editor/config.d.ts +24 -0
- package/dist/editor/document.d.ts +47 -0
- package/dist/editor/extensions.d.ts +19 -0
- package/dist/editor/header.d.ts +20 -0
- package/dist/editor/index.d.ts +11 -0
- package/dist/editor/index.js +209 -0
- package/dist/editor/paste-guard.d.ts +14 -0
- package/dist/editor/primitives.d.ts +17 -0
- package/dist/editor/props-form.d.ts +17 -0
- package/dist/editor/serialize.d.ts +34 -0
- package/dist/editor/slash-command.d.ts +21 -0
- package/dist/feed.d.ts +4 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +72 -0
- package/dist/next/app.d.ts +34 -0
- package/dist/next/config.d.ts +44 -0
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.js +94 -0
- package/dist/next/pages.d.ts +33 -0
- package/dist/next/pages.js +167 -0
- package/dist/next/routes.d.ts +14 -0
- package/dist/next/routes.js +37 -0
- package/dist/next/views.d.ts +18 -0
- package/dist/react/components.d.ts +18 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +15 -0
- package/dist/react/render.d.ts +7 -0
- package/dist/registry.d.ts +95 -0
- package/dist/sanitize.d.ts +33 -0
- package/dist/schema.d.ts +24 -0
- package/dist/seo.d.ts +63 -0
- package/dist/sitemap.d.ts +4 -0
- package/dist/xml.d.ts +1 -0
- package/package.json +92 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHttpContentSource
|
|
3
|
+
} from "./chunk-AEQDRBA3.js";
|
|
4
|
+
|
|
5
|
+
// src/next/config.ts
|
|
6
|
+
var DEFAULT_API_BASE = "https://api.codepress.dev/v1";
|
|
7
|
+
var DEFAULT_BASE_PATH = "/blog";
|
|
8
|
+
var DEFAULT_REVALIDATE_SECONDS = 60;
|
|
9
|
+
function resolveBlog(config) {
|
|
10
|
+
const origin = new URL(config.baseUrl).origin;
|
|
11
|
+
const basePath = `/${(config.basePath ?? DEFAULT_BASE_PATH).replace(/^\/+|\/+$/g, "")}`;
|
|
12
|
+
const name = config.name ?? new URL(origin).host;
|
|
13
|
+
return {
|
|
14
|
+
site: {
|
|
15
|
+
baseUrl: `${origin}${basePath}`,
|
|
16
|
+
homeUrl: origin,
|
|
17
|
+
name,
|
|
18
|
+
title: config.title ?? `${name} Blog`,
|
|
19
|
+
description: config.description ?? `Posts from ${name}.`,
|
|
20
|
+
defaultImage: config.defaultImage ?? ""
|
|
21
|
+
},
|
|
22
|
+
basePath,
|
|
23
|
+
revalidate: config.revalidate ?? DEFAULT_REVALIDATE_SECONDS,
|
|
24
|
+
source: createHttpContentSource({
|
|
25
|
+
apiBase: config.apiBase ?? DEFAULT_API_BASE,
|
|
26
|
+
site: config.site,
|
|
27
|
+
timeoutMs: config.timeoutMs
|
|
28
|
+
})
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function isBuildPhase() {
|
|
32
|
+
return process.env.NEXT_PHASE === "phase-production-build";
|
|
33
|
+
}
|
|
34
|
+
async function listPostsForRender(blog) {
|
|
35
|
+
try {
|
|
36
|
+
return await blog.source.listPosts();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (!isBuildPhase()) throw error;
|
|
39
|
+
console.error("[blog] prerendering an empty blog, API unavailable:", error);
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
DEFAULT_API_BASE,
|
|
46
|
+
DEFAULT_BASE_PATH,
|
|
47
|
+
DEFAULT_REVALIDATE_SECONDS,
|
|
48
|
+
resolveBlog,
|
|
49
|
+
isBuildPhase,
|
|
50
|
+
listPostsForRender
|
|
51
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/editor/client.ts
|
|
2
|
+
async function failure(response) {
|
|
3
|
+
try {
|
|
4
|
+
const body = await response.json();
|
|
5
|
+
if (typeof body.detail === "string") return body.detail;
|
|
6
|
+
} catch {
|
|
7
|
+
}
|
|
8
|
+
return `Request failed (${response.status})`;
|
|
9
|
+
}
|
|
10
|
+
var networkMessage = "Could not reach the server.";
|
|
11
|
+
function createDraftClient({
|
|
12
|
+
apiBase,
|
|
13
|
+
auth
|
|
14
|
+
}) {
|
|
15
|
+
const root = apiBase.replace(/\/+$/, "");
|
|
16
|
+
const request = async (method, path, body) => {
|
|
17
|
+
const headers = { Accept: "application/json" };
|
|
18
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
19
|
+
const token = await auth.getToken();
|
|
20
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
21
|
+
const response = await fetch(`${root}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
headers,
|
|
24
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
25
|
+
});
|
|
26
|
+
auth.onResponse?.(response);
|
|
27
|
+
return response;
|
|
28
|
+
};
|
|
29
|
+
const draftPath = (site, slug) => `/content-sites/${site}/drafts/${slug}`;
|
|
30
|
+
async function action(send) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await send();
|
|
33
|
+
if (response.ok) return { kind: "ok" };
|
|
34
|
+
if (response.status === 422) {
|
|
35
|
+
const body = await response.json().catch(() => ({}));
|
|
36
|
+
if (body.violations) {
|
|
37
|
+
return {
|
|
38
|
+
kind: "rejected",
|
|
39
|
+
detail: body.detail ?? "The content was rejected.",
|
|
40
|
+
violations: body.violations
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
kind: "error",
|
|
46
|
+
message: await failure(response),
|
|
47
|
+
status: response.status
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return { kind: "error", message: networkMessage };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
async getCapabilities(site) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await request("GET", `/content-sites/${site}/me`);
|
|
57
|
+
return response.ok ? await response.json() : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async getDraft(site, slug) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await request("GET", draftPath(site, slug));
|
|
65
|
+
if (!response.ok) return { error: await failure(response) };
|
|
66
|
+
return { draft: await response.json() };
|
|
67
|
+
} catch {
|
|
68
|
+
return { error: networkMessage };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async saveDraft(site, slug, html, baseSha) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await request("PUT", draftPath(site, slug), {
|
|
74
|
+
html,
|
|
75
|
+
base_sha: baseSha
|
|
76
|
+
});
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
return { kind: "saved", draft: await response.json() };
|
|
79
|
+
}
|
|
80
|
+
const body = await response.json().catch(() => ({}));
|
|
81
|
+
if (response.status === 409 && body.current) {
|
|
82
|
+
return { kind: "conflict", current: body.current };
|
|
83
|
+
}
|
|
84
|
+
if (response.status === 422 && body.violations) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "rejected",
|
|
87
|
+
detail: body.detail ?? "The content was rejected.",
|
|
88
|
+
violations: body.violations
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: "error",
|
|
93
|
+
message: body.detail ?? `Request failed (${response.status})`
|
|
94
|
+
};
|
|
95
|
+
} catch {
|
|
96
|
+
return { kind: "error", message: networkMessage };
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
publishDraft: (site, slug) => action(() => request("POST", `${draftPath(site, slug)}/publish`)),
|
|
100
|
+
discardDraft: (site, slug) => action(() => request("DELETE", draftPath(site, slug)))
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
createDraftClient
|
|
106
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SLUG_PATTERN,
|
|
3
|
+
postMetaSchema
|
|
4
|
+
} from "./chunk-XLJBCIXI.js";
|
|
5
|
+
|
|
6
|
+
// src/seo.ts
|
|
7
|
+
function blogUrl(site) {
|
|
8
|
+
return site.baseUrl.replace(/\/+$/, "");
|
|
9
|
+
}
|
|
10
|
+
function postUrl(site, slug) {
|
|
11
|
+
return `${blogUrl(site)}/${slug}`;
|
|
12
|
+
}
|
|
13
|
+
function feedUrl(site) {
|
|
14
|
+
return `${blogUrl(site)}/feed.xml`;
|
|
15
|
+
}
|
|
16
|
+
function siteOrigin(site) {
|
|
17
|
+
return new URL(site.baseUrl).origin;
|
|
18
|
+
}
|
|
19
|
+
function homeUrl(site) {
|
|
20
|
+
return site.homeUrl ?? siteOrigin(site);
|
|
21
|
+
}
|
|
22
|
+
function jsonLd(value) {
|
|
23
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
24
|
+
}
|
|
25
|
+
function absoluteImage(site, ogImage) {
|
|
26
|
+
if (!ogImage) return site.defaultImage;
|
|
27
|
+
return ogImage.startsWith("/") ? `${siteOrigin(site)}${ogImage}` : ogImage;
|
|
28
|
+
}
|
|
29
|
+
function blogPostingJsonLd(site, slug, meta) {
|
|
30
|
+
return {
|
|
31
|
+
"@context": "https://schema.org",
|
|
32
|
+
"@type": "BlogPosting",
|
|
33
|
+
headline: meta.title,
|
|
34
|
+
description: meta.description,
|
|
35
|
+
url: postUrl(site, slug),
|
|
36
|
+
mainEntityOfPage: postUrl(site, slug),
|
|
37
|
+
datePublished: meta.date,
|
|
38
|
+
dateModified: meta.updated ?? meta.date,
|
|
39
|
+
image: absoluteImage(site, meta.ogImage),
|
|
40
|
+
author: (meta.authors?.length ? meta.authors : [site.name]).map((name) => ({
|
|
41
|
+
"@type": "Person",
|
|
42
|
+
name
|
|
43
|
+
})),
|
|
44
|
+
publisher: {
|
|
45
|
+
"@type": "Organization",
|
|
46
|
+
name: site.name,
|
|
47
|
+
url: homeUrl(site)
|
|
48
|
+
},
|
|
49
|
+
...meta.tags?.length ? { keywords: meta.tags.join(", ") } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function breadcrumbJsonLd(site, slug, title) {
|
|
53
|
+
return {
|
|
54
|
+
"@context": "https://schema.org",
|
|
55
|
+
"@type": "BreadcrumbList",
|
|
56
|
+
itemListElement: [
|
|
57
|
+
{
|
|
58
|
+
"@type": "ListItem",
|
|
59
|
+
position: 1,
|
|
60
|
+
name: site.name,
|
|
61
|
+
item: homeUrl(site)
|
|
62
|
+
},
|
|
63
|
+
{ "@type": "ListItem", position: 2, name: "Blog", item: blogUrl(site) },
|
|
64
|
+
{
|
|
65
|
+
"@type": "ListItem",
|
|
66
|
+
position: 3,
|
|
67
|
+
name: title,
|
|
68
|
+
item: postUrl(site, slug)
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/content-source.ts
|
|
75
|
+
import { z } from "zod";
|
|
76
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
77
|
+
var postSchema = z.object({
|
|
78
|
+
slug: z.string(),
|
|
79
|
+
meta: postMetaSchema,
|
|
80
|
+
html: z.string()
|
|
81
|
+
});
|
|
82
|
+
var listSchema = z.object({
|
|
83
|
+
posts: z.array(z.object({ slug: z.string(), meta: postMetaSchema }))
|
|
84
|
+
});
|
|
85
|
+
function createHttpContentSource({
|
|
86
|
+
apiBase,
|
|
87
|
+
site,
|
|
88
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
89
|
+
}) {
|
|
90
|
+
const siteUrl = `${apiBase.replace(/\/+$/, "")}/content-sites/${encodeURIComponent(site)}`;
|
|
91
|
+
async function fetchJson(path) {
|
|
92
|
+
const response = await fetch(`${siteUrl}${path}`, {
|
|
93
|
+
headers: { Accept: "application/json" },
|
|
94
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
95
|
+
});
|
|
96
|
+
if (response.status === 404) return null;
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`[blog] backend ${path} answered ${response.status}`);
|
|
99
|
+
}
|
|
100
|
+
return response.json();
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
async listPosts() {
|
|
104
|
+
const body = await fetchJson("/posts");
|
|
105
|
+
if (body === null) return [];
|
|
106
|
+
return listSchema.parse(body).posts;
|
|
107
|
+
},
|
|
108
|
+
async getPost(slug) {
|
|
109
|
+
if (!SLUG_PATTERN.test(slug)) return null;
|
|
110
|
+
const body = await fetchJson(`/posts/${slug}`);
|
|
111
|
+
if (body === null) return null;
|
|
112
|
+
const { meta, html } = postSchema.parse(body);
|
|
113
|
+
return { slug, meta, html };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
blogUrl,
|
|
120
|
+
postUrl,
|
|
121
|
+
feedUrl,
|
|
122
|
+
homeUrl,
|
|
123
|
+
jsonLd,
|
|
124
|
+
absoluteImage,
|
|
125
|
+
blogPostingJsonLd,
|
|
126
|
+
breadcrumbJsonLd,
|
|
127
|
+
createHttpContentSource
|
|
128
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sanitizeNodes
|
|
3
|
+
} from "./chunk-EICE5ONB.js";
|
|
4
|
+
import {
|
|
5
|
+
COMPONENTS,
|
|
6
|
+
PROPS_ATTRIBUTE,
|
|
7
|
+
isComponentTag
|
|
8
|
+
} from "./chunk-XLJBCIXI.js";
|
|
9
|
+
|
|
10
|
+
// src/react/components.tsx
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
var CALLOUT_LABELS = {
|
|
13
|
+
info: "Note",
|
|
14
|
+
warn: "Warning",
|
|
15
|
+
tip: "Tip"
|
|
16
|
+
};
|
|
17
|
+
function CpCallout({ type, children, ...rest }) {
|
|
18
|
+
return /* @__PURE__ */ jsxs(
|
|
19
|
+
"aside",
|
|
20
|
+
{
|
|
21
|
+
...rest,
|
|
22
|
+
role: "note",
|
|
23
|
+
className: `not-prose cp-callout cp-callout--${type}`,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsx("p", { className: "cp-callout__label", children: CALLOUT_LABELS[type] }),
|
|
26
|
+
children
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function CpFigure({
|
|
32
|
+
src,
|
|
33
|
+
alt,
|
|
34
|
+
caption,
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
...rest
|
|
38
|
+
}) {
|
|
39
|
+
return /* @__PURE__ */ jsxs("figure", { ...rest, className: "cp-figure", children: [
|
|
40
|
+
/* @__PURE__ */ jsx(
|
|
41
|
+
"img",
|
|
42
|
+
{
|
|
43
|
+
src,
|
|
44
|
+
alt,
|
|
45
|
+
width,
|
|
46
|
+
height,
|
|
47
|
+
loading: "lazy",
|
|
48
|
+
className: "cp-figure__img"
|
|
49
|
+
}
|
|
50
|
+
),
|
|
51
|
+
caption ? /* @__PURE__ */ jsx("figcaption", { className: "cp-figure__caption", children: caption }) : null
|
|
52
|
+
] });
|
|
53
|
+
}
|
|
54
|
+
var COMPONENT_VIEWS = {
|
|
55
|
+
"cp-callout": CpCallout,
|
|
56
|
+
"cp-figure": CpFigure
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/react/render.tsx
|
|
60
|
+
import {
|
|
61
|
+
domToReact
|
|
62
|
+
} from "html-react-parser";
|
|
63
|
+
import { createElement } from "react";
|
|
64
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
65
|
+
var options = {
|
|
66
|
+
replace(node) {
|
|
67
|
+
if (node.type !== "tag" || !isComponentTag(node.name)) return void 0;
|
|
68
|
+
const definition = COMPONENTS[node.name];
|
|
69
|
+
const parsed = definition.props.safeParse(
|
|
70
|
+
JSON.parse(node.attribs[PROPS_ATTRIBUTE] ?? "{}")
|
|
71
|
+
);
|
|
72
|
+
if (!parsed.success) return /* @__PURE__ */ jsx2(Fragment, {});
|
|
73
|
+
const blockId = node.attribs["data-cp-id"];
|
|
74
|
+
return createElement(
|
|
75
|
+
COMPONENT_VIEWS[node.name],
|
|
76
|
+
{ ...parsed.data, ...blockId ? { "data-cp-id": blockId } : {} },
|
|
77
|
+
definition.content === "none" ? void 0 : domToReact(node.children, options)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function renderPostBody(html) {
|
|
82
|
+
return domToReact(sanitizeNodes(html).nodes, options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
CpCallout,
|
|
87
|
+
CpFigure,
|
|
88
|
+
COMPONENT_VIEWS,
|
|
89
|
+
renderPostBody
|
|
90
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOCK_ID_ATTRIBUTE,
|
|
3
|
+
COMPONENTS,
|
|
4
|
+
ELEMENTS,
|
|
5
|
+
PROPS_ATTRIBUTE,
|
|
6
|
+
URL_SCHEMES,
|
|
7
|
+
isComponentTag,
|
|
8
|
+
isElementTag
|
|
9
|
+
} from "./chunk-XLJBCIXI.js";
|
|
10
|
+
|
|
11
|
+
// src/sanitize.ts
|
|
12
|
+
import { Element, Text } from "domhandler";
|
|
13
|
+
import render from "dom-serializer";
|
|
14
|
+
import { parseDocument } from "htmlparser2";
|
|
15
|
+
var DROP_WITH_CONTENT = /* @__PURE__ */ new Set([
|
|
16
|
+
"script",
|
|
17
|
+
"style",
|
|
18
|
+
"iframe",
|
|
19
|
+
"frame",
|
|
20
|
+
"frameset",
|
|
21
|
+
"object",
|
|
22
|
+
"embed",
|
|
23
|
+
"applet",
|
|
24
|
+
"noscript",
|
|
25
|
+
"template",
|
|
26
|
+
"svg",
|
|
27
|
+
"math",
|
|
28
|
+
"textarea",
|
|
29
|
+
"select",
|
|
30
|
+
"title",
|
|
31
|
+
"head",
|
|
32
|
+
"audio",
|
|
33
|
+
"video"
|
|
34
|
+
]);
|
|
35
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set(["br", "hr", "img"]);
|
|
36
|
+
var MAX_DEPTH = 64;
|
|
37
|
+
var HAS_CONTROL_CHARACTERS = /[\u0000- \u007f-\u009f]/;
|
|
38
|
+
var SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
|
|
39
|
+
function sanitizeUrl(value) {
|
|
40
|
+
const url = value.trim();
|
|
41
|
+
if (!url || HAS_CONTROL_CHARACTERS.test(url)) return null;
|
|
42
|
+
const scheme = SCHEME.exec(url)?.[1]?.toLowerCase();
|
|
43
|
+
if (scheme) {
|
|
44
|
+
return URL_SCHEMES.includes(scheme) ? url : null;
|
|
45
|
+
}
|
|
46
|
+
if (url.startsWith("#")) return url;
|
|
47
|
+
if (url.startsWith("/") && !/^\/[/\\]/.test(url)) return url;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
var ATTRIBUTE_VALUE_PATTERNS = {
|
|
51
|
+
[BLOCK_ID_ATTRIBUTE]: /^[A-Za-z0-9_-]{1,64}$/,
|
|
52
|
+
id: /^[A-Za-z][\w-]{0,99}$/,
|
|
53
|
+
target: /^(_blank|_self)$/,
|
|
54
|
+
loading: /^(lazy|eager)$/,
|
|
55
|
+
width: /^\d{1,5}$/,
|
|
56
|
+
height: /^\d{1,5}$/,
|
|
57
|
+
rel: /^[a-z]+(\s+[a-z]+)*$/i
|
|
58
|
+
};
|
|
59
|
+
function cleanAttributes(tag, attribs, issues) {
|
|
60
|
+
const definition = ELEMENTS[tag];
|
|
61
|
+
const allowed = new Set(definition?.attributes ?? []);
|
|
62
|
+
const urlAttributes = new Set(definition?.urlAttributes ?? []);
|
|
63
|
+
const cleaned = {};
|
|
64
|
+
for (const [name, value] of Object.entries(attribs)) {
|
|
65
|
+
if (!allowed.has(name)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
kind: "attribute",
|
|
68
|
+
tag,
|
|
69
|
+
name,
|
|
70
|
+
message: `<${tag}> does not allow the attribute "${name}"`
|
|
71
|
+
});
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (urlAttributes.has(name)) {
|
|
75
|
+
const url = sanitizeUrl(value);
|
|
76
|
+
if (url === null) {
|
|
77
|
+
issues.push({
|
|
78
|
+
kind: "url",
|
|
79
|
+
tag,
|
|
80
|
+
name,
|
|
81
|
+
message: `<${tag} ${name}> has a URL that is not allowed`
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
cleaned[name] = url;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const pattern = ATTRIBUTE_VALUE_PATTERNS[name];
|
|
89
|
+
if (pattern && !pattern.test(value)) {
|
|
90
|
+
issues.push({
|
|
91
|
+
kind: "attribute",
|
|
92
|
+
tag,
|
|
93
|
+
name,
|
|
94
|
+
message: `<${tag} ${name}> has an invalid value`
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
cleaned[name] = value;
|
|
99
|
+
}
|
|
100
|
+
if (tag === "a" && cleaned.target === "_blank") {
|
|
101
|
+
cleaned.rel = "noopener noreferrer";
|
|
102
|
+
}
|
|
103
|
+
return cleaned;
|
|
104
|
+
}
|
|
105
|
+
function sanitizeChildren(children, issues, depth) {
|
|
106
|
+
return children.flatMap((child) => sanitizeNode(child, issues, depth));
|
|
107
|
+
}
|
|
108
|
+
function sanitizeComponent(element, issues, depth) {
|
|
109
|
+
const tag = element.name;
|
|
110
|
+
const definition = COMPONENTS[tag];
|
|
111
|
+
const attribs = {};
|
|
112
|
+
for (const [name, value] of Object.entries(element.attribs)) {
|
|
113
|
+
if (!definition.attributes.includes(name)) {
|
|
114
|
+
issues.push({
|
|
115
|
+
kind: "attribute",
|
|
116
|
+
tag,
|
|
117
|
+
name,
|
|
118
|
+
message: `<${tag}> does not allow the attribute "${name}"`
|
|
119
|
+
});
|
|
120
|
+
} else if (name !== PROPS_ATTRIBUTE) {
|
|
121
|
+
const valid = ATTRIBUTE_VALUE_PATTERNS[name]?.test(value) ?? true;
|
|
122
|
+
if (valid) attribs[name] = value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = JSON.parse(element.attribs[PROPS_ATTRIBUTE] ?? "");
|
|
128
|
+
} catch {
|
|
129
|
+
raw = void 0;
|
|
130
|
+
}
|
|
131
|
+
const parsed = definition.props.safeParse(raw);
|
|
132
|
+
if (!parsed.success) {
|
|
133
|
+
issues.push({
|
|
134
|
+
kind: "props",
|
|
135
|
+
tag,
|
|
136
|
+
name: PROPS_ATTRIBUTE,
|
|
137
|
+
message: `<${tag}> has invalid ${PROPS_ATTRIBUTE}: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"} ${issue.message}`).join("; ")}`
|
|
138
|
+
});
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
attribs[PROPS_ATTRIBUTE] = JSON.stringify(parsed.data);
|
|
142
|
+
const children = definition.content === "none" ? [] : sanitizeChildren(element.children, issues, depth + 1);
|
|
143
|
+
return [new Element(tag, attribs, children)];
|
|
144
|
+
}
|
|
145
|
+
function sanitizeNode(node, issues, depth) {
|
|
146
|
+
if (node.type === "text") return [new Text(node.data)];
|
|
147
|
+
if (!(node instanceof Element)) return [];
|
|
148
|
+
const tag = node.name;
|
|
149
|
+
if (depth > MAX_DEPTH) {
|
|
150
|
+
issues.push({ kind: "element", tag, message: "nested too deeply" });
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
if (DROP_WITH_CONTENT.has(tag)) {
|
|
154
|
+
issues.push({
|
|
155
|
+
kind: "element",
|
|
156
|
+
tag,
|
|
157
|
+
message: `<${tag}> is not allowed and was removed with its content`
|
|
158
|
+
});
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
if (isComponentTag(tag)) return sanitizeComponent(node, issues, depth);
|
|
162
|
+
if (tag.startsWith("cp-")) {
|
|
163
|
+
issues.push({
|
|
164
|
+
kind: "element",
|
|
165
|
+
tag,
|
|
166
|
+
message: `<${tag}> is not a registered component`
|
|
167
|
+
});
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
if (!isElementTag(tag)) {
|
|
171
|
+
issues.push({
|
|
172
|
+
kind: "element",
|
|
173
|
+
tag,
|
|
174
|
+
message: `<${tag}> is not allowed; its children were kept`
|
|
175
|
+
});
|
|
176
|
+
return sanitizeChildren(node.children, issues, depth + 1);
|
|
177
|
+
}
|
|
178
|
+
const attribs = cleanAttributes(tag, node.attribs, issues);
|
|
179
|
+
if (tag === "img" && !attribs.src) return [];
|
|
180
|
+
const children = VOID_ELEMENTS.has(tag) ? [] : sanitizeChildren(node.children, issues, depth + 1);
|
|
181
|
+
return [new Element(tag, attribs, children)];
|
|
182
|
+
}
|
|
183
|
+
function sanitizeNodes(html) {
|
|
184
|
+
const issues = [];
|
|
185
|
+
const nodes = sanitizeChildren(parseDocument(html).children, issues, 0);
|
|
186
|
+
return { nodes, issues };
|
|
187
|
+
}
|
|
188
|
+
function sanitizeHtml(html) {
|
|
189
|
+
return render(sanitizeNodes(html).nodes);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export {
|
|
193
|
+
DROP_WITH_CONTENT,
|
|
194
|
+
sanitizeUrl,
|
|
195
|
+
sanitizeNodes,
|
|
196
|
+
sanitizeHtml
|
|
197
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPONENTS,
|
|
3
|
+
ELEMENTS,
|
|
4
|
+
URL_SCHEMES,
|
|
5
|
+
postMetaSchema
|
|
6
|
+
} from "./chunk-XLJBCIXI.js";
|
|
7
|
+
|
|
8
|
+
// src/schema.ts
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
var schemaVersion = 1;
|
|
11
|
+
function toJsonSchema(schema) {
|
|
12
|
+
const { $schema: _draft, ...jsonSchema } = z.toJSONSchema(schema, {
|
|
13
|
+
target: "draft-7"
|
|
14
|
+
});
|
|
15
|
+
return jsonSchema;
|
|
16
|
+
}
|
|
17
|
+
function buildContentSchema() {
|
|
18
|
+
return {
|
|
19
|
+
version: schemaVersion,
|
|
20
|
+
elements: Object.fromEntries(
|
|
21
|
+
Object.entries(ELEMENTS).map(([tag, definition]) => [
|
|
22
|
+
tag,
|
|
23
|
+
{
|
|
24
|
+
attributes: [...definition.attributes],
|
|
25
|
+
...definition.urlAttributes ? { urlAttributes: [...definition.urlAttributes] } : {}
|
|
26
|
+
}
|
|
27
|
+
])
|
|
28
|
+
),
|
|
29
|
+
components: Object.fromEntries(
|
|
30
|
+
Object.entries(COMPONENTS).map(([tag, definition]) => [
|
|
31
|
+
tag,
|
|
32
|
+
{
|
|
33
|
+
attributes: [...definition.attributes],
|
|
34
|
+
content: definition.content,
|
|
35
|
+
props: toJsonSchema(definition.props)
|
|
36
|
+
}
|
|
37
|
+
])
|
|
38
|
+
),
|
|
39
|
+
urlSchemes: [...URL_SCHEMES],
|
|
40
|
+
relativeUrls: true,
|
|
41
|
+
meta: toJsonSchema(postMetaSchema)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
schemaVersion,
|
|
47
|
+
buildContentSchema
|
|
48
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blogUrl,
|
|
3
|
+
feedUrl,
|
|
4
|
+
postUrl
|
|
5
|
+
} from "./chunk-AEQDRBA3.js";
|
|
6
|
+
|
|
7
|
+
// src/xml.ts
|
|
8
|
+
function escapeXml(value) {
|
|
9
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/feed.ts
|
|
13
|
+
function feedXml(site, posts) {
|
|
14
|
+
const items = posts.map(({ slug, meta }) => {
|
|
15
|
+
const categories = (meta.tags ?? []).map((tag) => ` <category>${escapeXml(tag)}</category>`).join("\n");
|
|
16
|
+
return ` <item>
|
|
17
|
+
<title>${escapeXml(meta.title)}</title>
|
|
18
|
+
<link>${escapeXml(postUrl(site, slug))}</link>
|
|
19
|
+
<guid isPermaLink="true">${escapeXml(postUrl(site, slug))}</guid>
|
|
20
|
+
<pubDate>${(/* @__PURE__ */ new Date(`${meta.date}T00:00:00Z`)).toUTCString()}</pubDate>
|
|
21
|
+
<description>${escapeXml(meta.description)}</description>${categories ? `
|
|
22
|
+
${categories}` : ""}
|
|
23
|
+
</item>`;
|
|
24
|
+
}).join("\n");
|
|
25
|
+
const newest = posts[0]?.meta;
|
|
26
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
27
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
28
|
+
<channel>
|
|
29
|
+
<title>${escapeXml(site.title)}</title>
|
|
30
|
+
<link>${escapeXml(blogUrl(site))}</link>
|
|
31
|
+
<description>${escapeXml(site.description)}</description>
|
|
32
|
+
<language>${escapeXml(site.language ?? "en")}</language>
|
|
33
|
+
<atom:link href="${escapeXml(feedUrl(site))}" rel="self" type="application/rss+xml" />${newest ? `
|
|
34
|
+
<lastBuildDate>${(/* @__PURE__ */ new Date(`${newest.updated ?? newest.date}T00:00:00Z`)).toUTCString()}</lastBuildDate>` : ""}
|
|
35
|
+
${items}
|
|
36
|
+
</channel>
|
|
37
|
+
</rss>
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/sitemap.ts
|
|
42
|
+
function sitemapXml(site, posts) {
|
|
43
|
+
const newest = posts[0]?.meta;
|
|
44
|
+
const entries = [
|
|
45
|
+
{
|
|
46
|
+
loc: blogUrl(site),
|
|
47
|
+
lastmod: newest ? newest.updated ?? newest.date : void 0
|
|
48
|
+
},
|
|
49
|
+
...posts.map(({ slug, meta }) => ({
|
|
50
|
+
loc: postUrl(site, slug),
|
|
51
|
+
lastmod: meta.updated ?? meta.date
|
|
52
|
+
}))
|
|
53
|
+
];
|
|
54
|
+
const urls = entries.map(
|
|
55
|
+
({ loc, lastmod }) => ` <url><loc>${escapeXml(loc)}</loc>${lastmod ? `<lastmod>${lastmod}</lastmod>` : ""}</url>`
|
|
56
|
+
).join("\n");
|
|
57
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
58
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
59
|
+
${urls}
|
|
60
|
+
</urlset>
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
escapeXml,
|
|
66
|
+
feedXml,
|
|
67
|
+
sitemapXml
|
|
68
|
+
};
|