@roottale/cms-core 0.39.0 → 0.42.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @roottale/cms-core
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d7dfe0a: `postsNotInAnyCollection`(cms-core) — `postsInAnyCollection` 의 대칭으로, 어느 스트림에도 안 속한("미소속") 글만 남긴다.
8
+
9
+ `RootTaleBlogList` 에 `excludeCollections?: boolean` prop 추가 — `collections` 와 함께 주면 스트림 소속 글을 전부 제외해 `/blog` 목록의 스트림 글 누출을 막는다(`collection` 과 배타, `collections` 없이 단독 지정은 throw). 아카이브 스트림(증상/질환/치료)만 선언하고 나머지 글을 `/blog` 로 보여주는 사이트용 — `blog` 자체가 선언 섹션인 사이트는 반대로 `collection="blog"` 가 정경로.
10
+
11
+ - 8e8bc1c: `sanitizeHtml` 를 정규식 deny-list 에서 `sanitize-html` 기반 parser allow-list 로 교체했다. Tiptap/renderer 출력 태그와 RootTale `rt-*` class, 안전 URL scheme, raster `data:image/*` 정책을 보존하면서 미지 태그·이벤트 속성·비허용 style·SVG data URL 을 제거한다.
12
+
13
+ ## 0.41.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 3768f72: FAQ 블록 — `<details>` 아코디언 + FAQPage 구조화데이터(JSON-LD).
18
+
19
+ 어드민 에디터의 FAQ 블록(`roottale/faq` → faqItem/faqQuestion/faqAnswer)이
20
+ 발행되면 `RootTaleBlogPost`가 자동으로:
21
+ - `<details class="rt-cms-faq-item"><summary class="rt-cms-faq-q">…</summary>
22
+ <div class="rt-cms-faq-a">…</div></details>` 아코디언(0 JS)으로 렌더하고,
23
+ - 글 단위 `FAQPage` JSON-LD(`<script type="application/ld+json">`)를 1개 삽입한다.
24
+
25
+ 추가 연동/키 불필요 — 기존 단일 API 키 위에서 동작하며, 업그레이드 시 기존 글에도
26
+ FAQ 블록이 있으면 즉시 반영된다. `@roottale/cms-core` 에 추출 유틸
27
+ `extractFaqEntries(doc)` / `faqPageJsonLd(entries)` 추가(렌더러 공유). 스타일은
28
+ `cms-public.css` 의 `.rt-cms-faq*` 클래스로 테마 토큰을 따른다.
29
+
3
30
  ## 0.39.0
4
31
 
5
32
  ### Patch Changes
package/dist/index.cjs ADDED
@@ -0,0 +1,647 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ BLOCK_NAME_PATTERN: () => BLOCK_NAME_PATTERN,
34
+ FIELD_BINDING_SOURCE: () => FIELD_BINDING_SOURCE,
35
+ FIELD_BINDING_SOURCE_ALIAS: () => FIELD_BINDING_SOURCE_ALIAS,
36
+ PHASE_1_CORE_BLOCKS: () => PHASE_1_CORE_BLOCKS,
37
+ PHASE_1_CSP: () => PHASE_1_CSP,
38
+ applyBlockBindings: () => applyBlockBindings,
39
+ bindingSrcValue: () => bindingSrcValue,
40
+ bindingTextValue: () => bindingTextValue,
41
+ blockSchema: () => blockSchema,
42
+ boundFieldValue: () => boundFieldValue,
43
+ chainResolvers: () => chainResolvers,
44
+ createFieldBindingResolver: () => createFieldBindingResolver,
45
+ ensureStableIds: () => ensureStableIds,
46
+ extractFaqEntries: () => extractFaqEntries,
47
+ faqPageJsonLd: () => faqPageJsonLd,
48
+ injectStableIdsTree: () => injectStableIdsTree,
49
+ isAllowedEmbedSrc: () => isAllowedEmbedSrc,
50
+ isInCollectionMode: () => isInCollectionMode,
51
+ isValidBlock: () => isValidBlock,
52
+ issuePreviewToken: () => issuePreviewToken,
53
+ normalizeEmbed: () => normalizeEmbed,
54
+ parseBlock: () => parseBlock,
55
+ postsForCollection: () => postsForCollection,
56
+ postsInAnyCollection: () => postsInAnyCollection,
57
+ postsNotInAnyCollection: () => postsNotInAnyCollection,
58
+ resolvePostCollection: () => resolvePostCollection,
59
+ resolvePostPath: () => resolvePostPath,
60
+ resolvePostUrl: () => resolvePostUrl,
61
+ sanitizeHtml: () => sanitizeHtml,
62
+ serializeBlock: () => serializeBlock,
63
+ validateCollections: () => validateCollections,
64
+ verifyPreviewToken: () => verifyPreviewToken
65
+ });
66
+ module.exports = __toCommonJS(src_exports);
67
+
68
+ // src/block.ts
69
+ var import_zod = require("zod");
70
+ var BLOCK_NAME_PATTERN = /^(core|roottale)\/[a-z0-9-]+$/;
71
+ var blockMetadataSchema = import_zod.z.object({
72
+ bindings: import_zod.z.record(
73
+ import_zod.z.object({
74
+ source: import_zod.z.string(),
75
+ args: import_zod.z.record(import_zod.z.unknown()).optional()
76
+ })
77
+ ).optional(),
78
+ name: import_zod.z.string().optional(),
79
+ reusableRef: import_zod.z.string().optional()
80
+ }).strict().optional();
81
+ var blockSchema = import_zod.z.lazy(
82
+ () => import_zod.z.object({
83
+ _id: import_zod.z.string().uuid(),
84
+ name: import_zod.z.string().regex(BLOCK_NAME_PATTERN),
85
+ attributes: import_zod.z.record(import_zod.z.unknown()),
86
+ innerBlocks: import_zod.z.array(blockSchema),
87
+ rawHtml: import_zod.z.string().optional(),
88
+ metadata: blockMetadataSchema
89
+ }).strict()
90
+ );
91
+ function isValidBlock(value) {
92
+ return blockSchema.safeParse(value).success;
93
+ }
94
+ function parseBlock(value) {
95
+ return blockSchema.parse(value);
96
+ }
97
+ var PHASE_1_CORE_BLOCKS = [
98
+ "core/paragraph",
99
+ "core/heading",
100
+ "core/image",
101
+ "core/list",
102
+ "core/quote",
103
+ "core/code",
104
+ "core/columns",
105
+ "core/group",
106
+ "core/separator",
107
+ "core/spacer"
108
+ ];
109
+
110
+ // src/bindings.ts
111
+ var FIELD_BINDING_SOURCE = "roottale/field";
112
+ var FIELD_BINDING_SOURCE_ALIAS = "acf/field";
113
+ function createFieldBindingResolver(fields) {
114
+ const values = fields ?? {};
115
+ return (binding) => {
116
+ if (binding.source !== FIELD_BINDING_SOURCE && binding.source !== FIELD_BINDING_SOURCE_ALIAS) {
117
+ return void 0;
118
+ }
119
+ const name = binding.args?.name;
120
+ if (typeof name !== "string") return void 0;
121
+ const v = values[name];
122
+ return v === void 0 ? void 0 : v;
123
+ };
124
+ }
125
+ function chainResolvers(...resolvers) {
126
+ return (binding, attribute, block) => {
127
+ for (const r of resolvers) {
128
+ const v = r(binding, attribute, block);
129
+ if (v !== void 0) return v;
130
+ }
131
+ return void 0;
132
+ };
133
+ }
134
+ function applyToBlock(block, resolver) {
135
+ const bindings = block.metadata?.bindings;
136
+ let attributes = block.attributes;
137
+ if (bindings) {
138
+ let mutated = false;
139
+ const next = { ...block.attributes };
140
+ for (const [attr, binding] of Object.entries(bindings)) {
141
+ const value = resolver(binding, attr, block);
142
+ if (value !== void 0) {
143
+ next[attr] = value;
144
+ mutated = true;
145
+ }
146
+ }
147
+ if (mutated) attributes = next;
148
+ }
149
+ const innerBlocks = block.innerBlocks.length ? block.innerBlocks.map((b) => applyToBlock(b, resolver)) : block.innerBlocks;
150
+ if (attributes === block.attributes && innerBlocks === block.innerBlocks) {
151
+ return block;
152
+ }
153
+ return { ...block, attributes, innerBlocks };
154
+ }
155
+ function applyBlockBindings(blocks, resolver) {
156
+ return blocks.map((b) => applyToBlock(b, resolver));
157
+ }
158
+ function boundFieldValue(bindings, attribute, fields) {
159
+ if (!bindings || typeof bindings !== "object" || !fields) return void 0;
160
+ const binding = bindings[attribute];
161
+ if (!binding || binding.source !== FIELD_BINDING_SOURCE && binding.source !== FIELD_BINDING_SOURCE_ALIAS) {
162
+ return void 0;
163
+ }
164
+ const name = binding.args?.name;
165
+ if (typeof name !== "string") return void 0;
166
+ const value = fields[name];
167
+ if (value === void 0 || value === null || value === "") return void 0;
168
+ return value;
169
+ }
170
+ function bindingTextValue(value) {
171
+ if (typeof value === "string") return value;
172
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
173
+ return void 0;
174
+ }
175
+ function bindingSrcValue(value) {
176
+ if (value && typeof value === "object") {
177
+ const url = value.url;
178
+ if (typeof url === "string" && /^https?:\/\//i.test(url)) return url;
179
+ return void 0;
180
+ }
181
+ if (typeof value === "string" && /^https?:\/\//i.test(value)) return value;
182
+ return void 0;
183
+ }
184
+
185
+ // src/collections.ts
186
+ function trimTrailingSlash(value) {
187
+ return value.replace(/\/+$/, "");
188
+ }
189
+ function isInCollectionMode(collections) {
190
+ return !!collections && collections.length > 0;
191
+ }
192
+ function resolvePostCollection(post, collections) {
193
+ if (!post.collectionKey) return null;
194
+ return collections.find((c) => c.key === post.collectionKey) ?? null;
195
+ }
196
+ function resolvePostUrl(siteUrl, post, collections) {
197
+ const collection = resolvePostCollection(post, collections);
198
+ if (!collection) return null;
199
+ return `${trimTrailingSlash(siteUrl)}${trimTrailingSlash(collection.basePath)}/${encodeURIComponent(post.slug)}`;
200
+ }
201
+ function resolvePostPath(post, collections) {
202
+ const collection = resolvePostCollection(post, collections);
203
+ if (!collection) return null;
204
+ return `${trimTrailingSlash(collection.basePath)}/${encodeURIComponent(post.slug)}`;
205
+ }
206
+ function postsForCollection(posts, collections, key) {
207
+ return posts.filter((p) => resolvePostCollection(p, collections)?.key === key);
208
+ }
209
+ function postsInAnyCollection(posts, collections) {
210
+ return posts.filter((p) => resolvePostCollection(p, collections) !== null);
211
+ }
212
+ function postsNotInAnyCollection(posts, collections) {
213
+ return posts.filter((p) => resolvePostCollection(p, collections) === null);
214
+ }
215
+ function validateCollections(collections) {
216
+ const problems = [];
217
+ const seenKey = /* @__PURE__ */ new Set();
218
+ const seenBase = /* @__PURE__ */ new Set();
219
+ const categoryOwner = /* @__PURE__ */ new Map();
220
+ for (const c of collections) {
221
+ if (seenKey.has(c.key)) problems.push(`\uC911\uBCF5 collection key: "${c.key}"`);
222
+ seenKey.add(c.key);
223
+ const base = trimTrailingSlash(c.basePath);
224
+ if (seenBase.has(base)) problems.push(`\uC911\uBCF5 basePath: "${c.basePath}"`);
225
+ seenBase.add(base);
226
+ if (!c.basePath.startsWith("/")) {
227
+ problems.push(`basePath \uB294 "/" \uB85C \uC2DC\uC791\uD574\uC57C \uD568: "${c.basePath}" (${c.key})`);
228
+ }
229
+ for (const slug of c.categories) {
230
+ const owner = categoryOwner.get(slug);
231
+ if (owner && owner !== c.key) {
232
+ problems.push(
233
+ `category "${slug}" \uAC00 collection "${owner}" \uC640 "${c.key}" \uC5D0 \uC911\uBCF5 \u2014 \uD55C \uCE74\uD14C\uACE0\uB9AC\uB294 \uD55C \uC2A4\uD2B8\uB9BC\uC5D0\uB9CC.`
234
+ );
235
+ }
236
+ categoryOwner.set(slug, c.key);
237
+ }
238
+ }
239
+ return problems;
240
+ }
241
+
242
+ // src/delimiter.ts
243
+ function serializeBlock(block) {
244
+ const exportable = sanitizeAttributesForExport(block);
245
+ const attrsJson = serializeAttributes(exportable.attributes);
246
+ const innerHtml = block.rawHtml ?? "";
247
+ const innerSerialized = (block.innerBlocks ?? []).map((b) => serializeBlock(b)).join("\n");
248
+ if (!innerHtml && innerSerialized.length === 0) {
249
+ return attrsJson ? `<!-- wp:${normalizeName(block.name)} ${attrsJson} /-->` : `<!-- wp:${normalizeName(block.name)} /-->`;
250
+ }
251
+ const open = attrsJson ? `<!-- wp:${normalizeName(block.name)} ${attrsJson} -->` : `<!-- wp:${normalizeName(block.name)} -->`;
252
+ const close = `<!-- /wp:${normalizeName(block.name)} -->`;
253
+ const body = [innerHtml, innerSerialized].filter(Boolean).join("\n");
254
+ return `${open}
255
+ ${body}
256
+ ${close}`;
257
+ }
258
+ function normalizeName(name) {
259
+ return name.startsWith("core/") ? name.slice("core/".length) : name;
260
+ }
261
+ function serializeAttributes(attrs) {
262
+ const keys = Object.keys(attrs);
263
+ if (keys.length === 0) return "";
264
+ return JSON.stringify(attrs);
265
+ }
266
+ function sanitizeAttributesForExport(block) {
267
+ return { attributes: { ...block.attributes } };
268
+ }
269
+
270
+ // src/embed.ts
271
+ var YOUTUBE_HOSTS = /* @__PURE__ */ new Set([
272
+ "youtube.com",
273
+ "www.youtube.com",
274
+ "m.youtube.com",
275
+ "youtu.be",
276
+ "www.youtu.be"
277
+ ]);
278
+ var VIMEO_HOSTS = /* @__PURE__ */ new Set(["vimeo.com", "www.vimeo.com", "player.vimeo.com"]);
279
+ var ALLOWED_EMBED_HOSTS = /* @__PURE__ */ new Set([
280
+ "www.youtube-nocookie.com",
281
+ "youtube-nocookie.com",
282
+ "www.youtube.com",
283
+ "youtube.com",
284
+ "player.vimeo.com"
285
+ ]);
286
+ var YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
287
+ var VIMEO_ID = /^\d+$/;
288
+ function normalizeEmbed(input) {
289
+ if (typeof input !== "string") return null;
290
+ const raw = input.trim();
291
+ if (raw.length === 0) return null;
292
+ let url;
293
+ try {
294
+ url = new URL(raw);
295
+ } catch {
296
+ return null;
297
+ }
298
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
299
+ const host = url.hostname.toLowerCase();
300
+ if (YOUTUBE_HOSTS.has(host)) {
301
+ const id = extractYouTubeId(url, host);
302
+ if (id) {
303
+ return {
304
+ provider: "youtube",
305
+ embedSrc: `https://www.youtube-nocookie.com/embed/${id}`,
306
+ originalUrl: raw
307
+ };
308
+ }
309
+ }
310
+ if (VIMEO_HOSTS.has(host)) {
311
+ const id = extractVimeoId(url, host);
312
+ if (id) {
313
+ return {
314
+ provider: "vimeo",
315
+ embedSrc: `https://player.vimeo.com/video/${id}`,
316
+ originalUrl: raw
317
+ };
318
+ }
319
+ }
320
+ return null;
321
+ }
322
+ function isAllowedEmbedSrc(value) {
323
+ if (typeof value !== "string") return false;
324
+ let url;
325
+ try {
326
+ url = new URL(value);
327
+ } catch {
328
+ return false;
329
+ }
330
+ if (url.protocol !== "https:") return false;
331
+ const host = url.hostname.toLowerCase();
332
+ if (!ALLOWED_EMBED_HOSTS.has(host)) return false;
333
+ if (host === "player.vimeo.com") {
334
+ return /^\/video\/\d+$/.test(url.pathname);
335
+ }
336
+ return new RegExp("^/embed/[A-Za-z0-9_-]{11}$").test(url.pathname);
337
+ }
338
+ function extractYouTubeId(url, host) {
339
+ if (host === "youtu.be" || host === "www.youtu.be") {
340
+ const id = url.pathname.split("/").filter(Boolean)[0] ?? "";
341
+ return YOUTUBE_ID.test(id) ? id : null;
342
+ }
343
+ const v = url.searchParams.get("v");
344
+ if (v && YOUTUBE_ID.test(v)) return v;
345
+ const parts = url.pathname.split("/").filter(Boolean);
346
+ if (parts.length >= 2 && (parts[0] === "embed" || parts[0] === "shorts" || parts[0] === "v")) {
347
+ return YOUTUBE_ID.test(parts[1]) ? parts[1] : null;
348
+ }
349
+ return null;
350
+ }
351
+ function extractVimeoId(url, host) {
352
+ const parts = url.pathname.split("/").filter(Boolean);
353
+ let candidate;
354
+ if (host === "player.vimeo.com") {
355
+ const vi = parts.indexOf("video");
356
+ candidate = vi >= 0 ? parts[vi + 1] : parts[parts.length - 1];
357
+ } else {
358
+ candidate = parts[0];
359
+ }
360
+ return candidate && VIMEO_ID.test(candidate) ? candidate : null;
361
+ }
362
+
363
+ // src/faq.ts
364
+ function plainText(node) {
365
+ if (!node) return "";
366
+ if (typeof node.text === "string") return node.text;
367
+ if (!Array.isArray(node.content)) return "";
368
+ return node.content.map(plainText).join(" ");
369
+ }
370
+ function collapse(s) {
371
+ return s.replace(/\s+/g, " ").trim();
372
+ }
373
+ function extractFaqEntries(doc) {
374
+ const out = [];
375
+ const walk = (node) => {
376
+ if (!node || typeof node !== "object") return;
377
+ if (node.type === "faqItem" && Array.isArray(node.content)) {
378
+ const q = node.content.find((c) => c.type === "faqQuestion");
379
+ const a = node.content.find((c) => c.type === "faqAnswer");
380
+ const question = collapse(plainText(q));
381
+ const answer = collapse(plainText(a));
382
+ if (question && answer) out.push({ question, answer });
383
+ return;
384
+ }
385
+ if (Array.isArray(node.content)) node.content.forEach(walk);
386
+ };
387
+ if (doc && typeof doc === "object") {
388
+ walk(doc);
389
+ }
390
+ return out;
391
+ }
392
+ function faqPageJsonLd(entries) {
393
+ if (entries.length === 0) return null;
394
+ return {
395
+ "@context": "https://schema.org",
396
+ "@type": "FAQPage",
397
+ mainEntity: entries.map((e) => ({
398
+ "@type": "Question",
399
+ name: e.question,
400
+ acceptedAnswer: { "@type": "Answer", text: e.answer }
401
+ }))
402
+ };
403
+ }
404
+
405
+ // src/preview-token.ts
406
+ var import_node_crypto = require("crypto");
407
+ var DELIMITER = ".";
408
+ function base64url(buf) {
409
+ return buf.toString("base64url");
410
+ }
411
+ function fromBase64url(s) {
412
+ return Buffer.from(s, "base64url");
413
+ }
414
+ function sign(secret, message) {
415
+ return base64url((0, import_node_crypto.createHmac)("sha256", secret).update(message).digest());
416
+ }
417
+ function issuePreviewToken(secret, payload) {
418
+ const exp = payload.expiresAt.getTime();
419
+ const nonce = base64url((0, import_node_crypto.randomBytes)(12));
420
+ const message = `${payload.postId}${DELIMITER}${exp}${DELIMITER}${nonce}`;
421
+ const sig = sign(secret, message);
422
+ return `${message}${DELIMITER}${sig}`;
423
+ }
424
+ function verifyPreviewToken(secret, token) {
425
+ const parts = token.split(DELIMITER);
426
+ if (parts.length !== 4) return null;
427
+ const [postId, expRaw, nonce, sig] = parts;
428
+ const exp = Number(expRaw);
429
+ if (!Number.isFinite(exp)) return null;
430
+ if (Date.now() > exp) return null;
431
+ const expectedSig = sign(secret, `${postId}${DELIMITER}${exp}${DELIMITER}${nonce}`);
432
+ const a = fromBase64url(sig);
433
+ const b = fromBase64url(expectedSig);
434
+ if (a.length !== b.length) return null;
435
+ if (!(0, import_node_crypto.timingSafeEqual)(a, b)) return null;
436
+ return { postId, expiresAt: new Date(exp) };
437
+ }
438
+
439
+ // src/sanitize.ts
440
+ var import_sanitize_html = __toESM(require("sanitize-html"), 1);
441
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "xlink:href"]);
442
+ var ALLOWED_URL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
443
+ var MALFORMED_DANGEROUS_TOKEN_RE = /<\/?[^>\s/]*(?:script|style|iframe|object|embed|svg|math|template)[^>\s/]*(?:\s[^>]*)?>[\s\S]*?<\/?[^>\s/]*(?:script|style|iframe|object|embed|svg|math|template)[^>\s/]*(?:\s[^>]*)?>/gi;
444
+ var SLASH_EVENT_ATTR_IN_URL_RE = /\/on[a-z]+=/i;
445
+ var ROOTTALE_CLASS_RE = /^rt-[a-z0-9_-]+$/;
446
+ var HEX_COLOR_RE = /^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i;
447
+ var TEXT_ALIGN_RE = /^(left|right|center|justify)$/;
448
+ var ASPECT_RATIO_RE = /^\d+(?:\.\d+)?\s*\/\s*\d+(?:\.\d+)?$/;
449
+ var NAMED_ENTITY = {
450
+ colon: ":",
451
+ tab: " ",
452
+ newline: "\n",
453
+ sol: "/",
454
+ semi: ";",
455
+ num: "#",
456
+ period: ".",
457
+ comma: ",",
458
+ lpar: "(",
459
+ rpar: ")"
460
+ };
461
+ function fromCodePointSafe(n) {
462
+ if (!Number.isFinite(n) || n < 0 || n > 1114111) return "";
463
+ try {
464
+ return String.fromCodePoint(n);
465
+ } catch {
466
+ return "";
467
+ }
468
+ }
469
+ function decodeUrlProbe(value) {
470
+ const decoded = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) => fromCodePointSafe(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
471
+ return [...decoded].filter((c) => {
472
+ const n = c.codePointAt(0) ?? 0;
473
+ return n > 32 && n !== 127;
474
+ }).join("").toLowerCase();
475
+ }
476
+ function stripMalformedDangerousTokens(html) {
477
+ let out = html;
478
+ for (let i = 0; i < 3; i++) {
479
+ const next = out.replace(MALFORMED_DANGEROUS_TOKEN_RE, "");
480
+ if (next === out) return out;
481
+ out = next;
482
+ }
483
+ return out;
484
+ }
485
+ function isSafeUrlValue(tag, attr, value) {
486
+ const probe = decodeUrlProbe(value);
487
+ if (SLASH_EVENT_ATTR_IN_URL_RE.test(probe)) return false;
488
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe)?.[1];
489
+ if (!scheme) return true;
490
+ if (ALLOWED_URL_SCHEMES.has(scheme)) return true;
491
+ if (scheme === "data" && tag === "img" && attr === "src" && probe.startsWith("data:image/")) {
492
+ return !probe.startsWith("data:image/svg");
493
+ }
494
+ return false;
495
+ }
496
+ function sanitizeUrlAttributes(tagName, attribs) {
497
+ const next = {};
498
+ for (const [name, value] of Object.entries(attribs)) {
499
+ const attr = name.toLowerCase();
500
+ next[name] = URL_ATTRS.has(attr) && !isSafeUrlValue(tagName, attr, value) ? "#" : value;
501
+ }
502
+ return { tagName, attribs: next };
503
+ }
504
+ var SANITIZE_OPTIONS = {
505
+ allowedTags: [
506
+ "a",
507
+ "blockquote",
508
+ "br",
509
+ "cite",
510
+ "code",
511
+ "details",
512
+ "div",
513
+ "em",
514
+ "figcaption",
515
+ "figure",
516
+ "h2",
517
+ "h3",
518
+ "h4",
519
+ "hr",
520
+ "img",
521
+ "li",
522
+ "mark",
523
+ "ol",
524
+ "p",
525
+ "pre",
526
+ "s",
527
+ "span",
528
+ "strong",
529
+ "summary",
530
+ "table",
531
+ "tbody",
532
+ "td",
533
+ "th",
534
+ "tr",
535
+ "u",
536
+ "ul"
537
+ ],
538
+ allowedAttributes: {
539
+ "*": ["class", "data-*"],
540
+ a: [
541
+ "href",
542
+ "title",
543
+ { name: "rel", multiple: true, values: ["noopener", "noreferrer"] },
544
+ { name: "target", values: ["_blank"] }
545
+ ],
546
+ details: ["open", "class", "data-*"],
547
+ div: ["class", "data-*", "style"],
548
+ h2: ["id", "class", "data-*", "style"],
549
+ h3: ["id", "class", "data-*", "style"],
550
+ h4: ["id", "class", "data-*", "style"],
551
+ img: ["src", "alt", "title", "width", "height", "loading", "class", "data-*"],
552
+ mark: ["class", "data-*", "style"],
553
+ p: ["class", "data-*", "style"],
554
+ span: ["class", "data-*", "style"],
555
+ td: ["colspan", "rowspan", "class", "data-*"],
556
+ th: ["colspan", "rowspan", "class", "data-*"]
557
+ },
558
+ allowedClasses: {
559
+ "*": [ROOTTALE_CLASS_RE]
560
+ },
561
+ allowedStyles: {
562
+ "*": {
563
+ "aspect-ratio": [ASPECT_RATIO_RE],
564
+ background: [HEX_COLOR_RE],
565
+ "background-color": [HEX_COLOR_RE],
566
+ color: [HEX_COLOR_RE],
567
+ "text-align": [TEXT_ALIGN_RE]
568
+ }
569
+ },
570
+ allowedSchemes: ["http", "https", "mailto", "tel"],
571
+ allowedSchemesByTag: {
572
+ img: ["data"]
573
+ },
574
+ allowedSchemesAppliedToAttributes: ["href", "src", "xlink:href"],
575
+ allowProtocolRelative: false,
576
+ nonTextTags: [
577
+ "script",
578
+ "style",
579
+ "textarea",
580
+ "option",
581
+ "iframe",
582
+ "object",
583
+ "embed",
584
+ "svg",
585
+ "math",
586
+ "template"
587
+ ],
588
+ transformTags: {
589
+ "*": sanitizeUrlAttributes
590
+ }
591
+ };
592
+ function sanitizeHtml(html) {
593
+ if (!html) return html;
594
+ return (0, import_sanitize_html.default)(stripMalformedDangerousTokens(html), SANITIZE_OPTIONS);
595
+ }
596
+ var PHASE_1_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://imagedelivery.net https://*.r2.cloudflarestorage.com; font-src 'self' data:; connect-src 'self'; frame-src https://www.youtube-nocookie.com https://www.youtube.com https://player.vimeo.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
597
+
598
+ // src/stable-id.ts
599
+ var import_uuidv7 = require("uuidv7");
600
+ function ensureStableIds(block) {
601
+ return {
602
+ ...block,
603
+ _id: block._id ?? (0, import_uuidv7.uuidv7)(),
604
+ innerBlocks: (block.innerBlocks ?? []).map(
605
+ (inner) => ensureStableIds(inner)
606
+ )
607
+ };
608
+ }
609
+ function injectStableIdsTree(blocks) {
610
+ return blocks.map((b) => ensureStableIds(b));
611
+ }
612
+ // Annotate the CommonJS export names for ESM import in node:
613
+ 0 && (module.exports = {
614
+ BLOCK_NAME_PATTERN,
615
+ FIELD_BINDING_SOURCE,
616
+ FIELD_BINDING_SOURCE_ALIAS,
617
+ PHASE_1_CORE_BLOCKS,
618
+ PHASE_1_CSP,
619
+ applyBlockBindings,
620
+ bindingSrcValue,
621
+ bindingTextValue,
622
+ blockSchema,
623
+ boundFieldValue,
624
+ chainResolvers,
625
+ createFieldBindingResolver,
626
+ ensureStableIds,
627
+ extractFaqEntries,
628
+ faqPageJsonLd,
629
+ injectStableIdsTree,
630
+ isAllowedEmbedSrc,
631
+ isInCollectionMode,
632
+ isValidBlock,
633
+ issuePreviewToken,
634
+ normalizeEmbed,
635
+ parseBlock,
636
+ postsForCollection,
637
+ postsInAnyCollection,
638
+ postsNotInAnyCollection,
639
+ resolvePostCollection,
640
+ resolvePostPath,
641
+ resolvePostUrl,
642
+ sanitizeHtml,
643
+ serializeBlock,
644
+ validateCollections,
645
+ verifyPreviewToken
646
+ });
647
+ //# sourceMappingURL=index.cjs.map