@roottale/cms-core 0.66.0 → 0.67.1
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 +8 -0
- package/dist/index.cjs +328 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -1
- package/dist/index.d.ts +547 -1
- package/dist/index.js +311 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -973,10 +973,10 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
973
973
|
|
|
974
974
|
// src/internal-content-links.ts
|
|
975
975
|
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
976
|
-
function findInternalContentLinkTokens(
|
|
976
|
+
function findInternalContentLinkTokens(text2) {
|
|
977
977
|
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
978
978
|
const tokens = [];
|
|
979
|
-
for (const match of
|
|
979
|
+
for (const match of text2.matchAll(pattern)) {
|
|
980
980
|
const [raw, key, label] = match;
|
|
981
981
|
if (!key || !label || match.index === void 0) continue;
|
|
982
982
|
tokens.push({
|
|
@@ -1035,19 +1035,19 @@ function escapeHtmlText(value) {
|
|
|
1035
1035
|
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1036
1036
|
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1037
1037
|
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1038
|
-
function renderInternalContentLinksInText(
|
|
1039
|
-
const tokens = findInternalContentLinkTokens(
|
|
1040
|
-
if (tokens.length === 0) return { html:
|
|
1038
|
+
function renderInternalContentLinksInText(text2, resolve) {
|
|
1039
|
+
const tokens = findInternalContentLinkTokens(text2);
|
|
1040
|
+
if (tokens.length === 0) return { html: text2, hasTokens: false };
|
|
1041
1041
|
let cursor = 0;
|
|
1042
1042
|
let html = "";
|
|
1043
1043
|
for (const token of tokens) {
|
|
1044
|
-
html +=
|
|
1044
|
+
html += text2.slice(cursor, token.index);
|
|
1045
1045
|
const path = resolve(token.key);
|
|
1046
1046
|
const label = escapeHtmlText(token.label);
|
|
1047
1047
|
html += isSafeInternalPath(path) ? `<a href="${escapeHtmlText(path)}" class="${INTERNAL_CONTENT_LINK_CLASS}">${label}</a>` : `<span class="${INTERNAL_CONTENT_LINK_CLASS} ${INTERNAL_CONTENT_LINK_PENDING_CLASS}" ${INTERNAL_CONTENT_LINK_PENDING_ATTR}="${escapeHtmlText(token.key)}">${label}</span>`;
|
|
1048
1048
|
cursor = token.index + token.raw.length;
|
|
1049
1049
|
}
|
|
1050
|
-
html +=
|
|
1050
|
+
html += text2.slice(cursor);
|
|
1051
1051
|
return { html, hasTokens: true };
|
|
1052
1052
|
}
|
|
1053
1053
|
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
@@ -1081,12 +1081,12 @@ function renderInternalContentLinksInHtml(html, resolve) {
|
|
|
1081
1081
|
let cursor = 0;
|
|
1082
1082
|
let out = "";
|
|
1083
1083
|
let hasTokens = false;
|
|
1084
|
-
const emitText = (
|
|
1085
|
-
if (blockedDepth > 0 || !
|
|
1086
|
-
out +=
|
|
1084
|
+
const emitText = (text2) => {
|
|
1085
|
+
if (blockedDepth > 0 || !text2) {
|
|
1086
|
+
out += text2;
|
|
1087
1087
|
return;
|
|
1088
1088
|
}
|
|
1089
|
-
const rendered = renderInternalContentLinksInText(
|
|
1089
|
+
const rendered = renderInternalContentLinksInText(text2, resolve);
|
|
1090
1090
|
hasTokens ||= rendered.hasTokens;
|
|
1091
1091
|
out += rendered.html;
|
|
1092
1092
|
};
|
|
@@ -1225,8 +1225,8 @@ function fromCodePointSafe2(n) {
|
|
|
1225
1225
|
}
|
|
1226
1226
|
}
|
|
1227
1227
|
function decodeUrlProbe(value) {
|
|
1228
|
-
const
|
|
1229
|
-
return [...
|
|
1228
|
+
const decoded2 = value.replace(/&#x([0-9a-f]+);?/gi, (_, h) => fromCodePointSafe2(parseInt(h, 16))).replace(/&#(\d+);?/g, (_, d) => fromCodePointSafe2(parseInt(d, 10))).replace(/&([a-z]+);/gi, (m, name) => NAMED_ENTITY[name.toLowerCase()] ?? m);
|
|
1229
|
+
return [...decoded2].filter((c) => {
|
|
1230
1230
|
const n = c.codePointAt(0) ?? 0;
|
|
1231
1231
|
return n > 32 && n !== 127;
|
|
1232
1232
|
}).join("").toLowerCase();
|
|
@@ -1850,6 +1850,287 @@ function formatSectionLockViolations(violations) {
|
|
|
1850
1850
|
return `${label} \uAD6C\uC870 \uBCC0\uACBD(${violation.field})`;
|
|
1851
1851
|
}).join(" / ");
|
|
1852
1852
|
}
|
|
1853
|
+
|
|
1854
|
+
// src/metadata-resolver.ts
|
|
1855
|
+
function text(value) {
|
|
1856
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1857
|
+
}
|
|
1858
|
+
function imageUrl(value, base) {
|
|
1859
|
+
const candidate = text(value);
|
|
1860
|
+
if (!candidate || candidate.startsWith("//") || /[\\\u0000-\u0020\u007f]/u.test(candidate)) return void 0;
|
|
1861
|
+
if (candidate.startsWith("/") && !base) return candidate;
|
|
1862
|
+
try {
|
|
1863
|
+
const url = new URL(candidate, base);
|
|
1864
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return void 0;
|
|
1865
|
+
return url.href;
|
|
1866
|
+
} catch {
|
|
1867
|
+
return void 0;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function resolveMetadata(input) {
|
|
1871
|
+
const layers = [input.overrides, input.defaults, input.siteDefaults, input.fallback];
|
|
1872
|
+
const result = {};
|
|
1873
|
+
for (const field of ["title", "description", "ogImageAlt"]) {
|
|
1874
|
+
for (const layer of layers) {
|
|
1875
|
+
const value = text(layer?.[field]);
|
|
1876
|
+
if (value !== void 0) {
|
|
1877
|
+
result[field] = value;
|
|
1878
|
+
break;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
for (const layer of layers) {
|
|
1883
|
+
const value = imageUrl(layer?.ogImage, input.imageBaseUrl);
|
|
1884
|
+
if (value !== void 0) {
|
|
1885
|
+
result.ogImage = value;
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
for (const field of ["noindex", "nofollow"]) {
|
|
1890
|
+
for (const layer of layers) {
|
|
1891
|
+
const value = layer?.[field];
|
|
1892
|
+
if (typeof value === "boolean") {
|
|
1893
|
+
result[field] = value;
|
|
1894
|
+
break;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (input.robots?.forceNoindex) result.noindex = true;
|
|
1899
|
+
if (input.robots?.forceNofollow) result.nofollow = true;
|
|
1900
|
+
return result;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/metadata-contract.ts
|
|
1904
|
+
import { z as z2 } from "zod";
|
|
1905
|
+
var METADATA_FIELDS = ["title", "description", "ogImage", "ogImageAlt", "noindex", "nofollow"];
|
|
1906
|
+
var MetadataFieldSchema = z2.enum(METADATA_FIELDS);
|
|
1907
|
+
var MetadataTargetKindSchema = z2.enum(["route", "post", "term"]);
|
|
1908
|
+
var textValue = (max) => z2.string().trim().min(1).max(max);
|
|
1909
|
+
var imageValue = textValue(2048).refine((value) => {
|
|
1910
|
+
if (/[\\\u0000-\u0020\u007f]/u.test(value) || value.startsWith("//")) return false;
|
|
1911
|
+
if (value.startsWith("/")) return true;
|
|
1912
|
+
try {
|
|
1913
|
+
const url = new URL(value);
|
|
1914
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
1915
|
+
} catch {
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
}, "\uACF5\uC720 \uC774\uBBF8\uC9C0\uC5D0\uB294 \uC0AC\uC774\uD2B8 \uB0B4\uBD80 \uACBD\uB85C\uB098 HTTP/HTTPS \uC8FC\uC18C\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694");
|
|
1919
|
+
var metadataFields = {
|
|
1920
|
+
// Existing content titles (500) plus a site title template (400) remain valid automatic values.
|
|
1921
|
+
title: textValue(1e3),
|
|
1922
|
+
description: textValue(2e3),
|
|
1923
|
+
ogImage: imageValue,
|
|
1924
|
+
// Automatic alt text may use the complete automatic title and its site suffix.
|
|
1925
|
+
ogImageAlt: textValue(1e3),
|
|
1926
|
+
noindex: z2.boolean(),
|
|
1927
|
+
nofollow: z2.boolean()
|
|
1928
|
+
};
|
|
1929
|
+
var MetadataValuesSchema = z2.object(metadataFields).partial().strict();
|
|
1930
|
+
var MetadataPatchSchema = z2.object({
|
|
1931
|
+
title: textValue(300).nullable().optional(),
|
|
1932
|
+
description: metadataFields.description.nullable().optional(),
|
|
1933
|
+
ogImage: metadataFields.ogImage.nullable().optional(),
|
|
1934
|
+
ogImageAlt: textValue(500).nullable().optional(),
|
|
1935
|
+
noindex: metadataFields.noindex.nullable().optional(),
|
|
1936
|
+
nofollow: metadataFields.nofollow.nullable().optional()
|
|
1937
|
+
}).strict();
|
|
1938
|
+
var MetadataRobotsPolicySchema = z2.object({
|
|
1939
|
+
forceNoindex: z2.boolean().optional(),
|
|
1940
|
+
forceNofollow: z2.boolean().optional()
|
|
1941
|
+
}).strict();
|
|
1942
|
+
function readMetadataValues(input) {
|
|
1943
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
1944
|
+
const record = input;
|
|
1945
|
+
const output = {};
|
|
1946
|
+
for (const field of METADATA_FIELDS) {
|
|
1947
|
+
const parsed = metadataFields[field].safeParse(record[field]);
|
|
1948
|
+
if (parsed.success) output[field] = parsed.data;
|
|
1949
|
+
}
|
|
1950
|
+
return MetadataValuesSchema.parse(output);
|
|
1951
|
+
}
|
|
1952
|
+
function mergeMetadataPatch(current, input) {
|
|
1953
|
+
const patch = MetadataPatchSchema.parse(input);
|
|
1954
|
+
const next = { ...current };
|
|
1955
|
+
for (const field of METADATA_FIELDS) {
|
|
1956
|
+
if (patch[field] === null) delete next[field];
|
|
1957
|
+
else if (patch[field] !== void 0) next[field] = patch[field];
|
|
1958
|
+
}
|
|
1959
|
+
return next;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// src/metadata-defaults.ts
|
|
1963
|
+
import { z as z3 } from "zod";
|
|
1964
|
+
var fallbackText = z3.string().max(2e3);
|
|
1965
|
+
var imageOrigin = z3.string().max(2048).refine((value) => {
|
|
1966
|
+
try {
|
|
1967
|
+
const url = new URL(value);
|
|
1968
|
+
return url.protocol === "https:" && url.origin === value;
|
|
1969
|
+
} catch {
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
}, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
|
|
1973
|
+
var MetadataTextRuleSchema = z3.object({
|
|
1974
|
+
source: z3.enum(["content", "fallback"]).optional(),
|
|
1975
|
+
stripLeadingLabel: z3.enum(["question", "answer"]).optional(),
|
|
1976
|
+
replaceLineBreakTokens: z3.boolean().optional(),
|
|
1977
|
+
removeEmDashes: z3.boolean().optional(),
|
|
1978
|
+
normalizeWhitespace: z3.boolean().optional(),
|
|
1979
|
+
maxLength: z3.number().int().min(1).max(2e3).optional(),
|
|
1980
|
+
ellipsis: z3.string().max(10).optional(),
|
|
1981
|
+
fallback: fallbackText.optional(),
|
|
1982
|
+
fallbackWithParent: fallbackText.optional(),
|
|
1983
|
+
fallbackWithoutParent: fallbackText.optional(),
|
|
1984
|
+
fallbackBySlug: z3.record(z3.string().max(200), fallbackText).optional()
|
|
1985
|
+
}).strict().refine((rule) => !rule.ellipsis || rule.maxLength !== void 0 && rule.ellipsis.length < rule.maxLength, "\uB9D0\uC904\uC784\uD45C\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
1986
|
+
var MetadataDefaultRulesSchema = z3.object({
|
|
1987
|
+
title: MetadataTextRuleSchema.optional(),
|
|
1988
|
+
description: MetadataTextRuleSchema.optional(),
|
|
1989
|
+
useFirstBodyImage: z3.boolean().optional(),
|
|
1990
|
+
useTermImage: z3.boolean().optional(),
|
|
1991
|
+
imageAltSource: z3.enum(["content", "title"]).optional(),
|
|
1992
|
+
legacyImageSource: z3.literal("legacy-thumbnail").optional(),
|
|
1993
|
+
imageAllowedOrigins: z3.array(imageOrigin).min(1).max(32).optional(),
|
|
1994
|
+
legacyImageAllowedOrigins: z3.array(imageOrigin).min(1).max(32).optional(),
|
|
1995
|
+
normalizeCloudflareImages: z3.boolean().optional(),
|
|
1996
|
+
fallback: MetadataValuesSchema.optional()
|
|
1997
|
+
}).strict().refine((rule) => !rule.legacyImageSource || Boolean(rule.legacyImageAllowedOrigins?.length), "\uC774\uC804 \uC774\uBBF8\uC9C0 \uC6D0\uBCF8\uC5D0\uB294 \uD5C8\uC6A9 origin \uC120\uC5B8\uC774 \uD544\uC694\uD569\uB2C8\uB2E4");
|
|
1998
|
+
function cleanText(value, rule) {
|
|
1999
|
+
let text2 = value.trim();
|
|
2000
|
+
if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
|
|
2001
|
+
if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
|
|
2002
|
+
if (rule?.replaceLineBreakTokens) text2 = text2.replace(/\[\[br(?::[^\]]*)?\]\]/giu, " ");
|
|
2003
|
+
if (rule?.removeEmDashes) text2 = text2.replaceAll("\u2014", "");
|
|
2004
|
+
if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
|
|
2005
|
+
return text2.trim();
|
|
2006
|
+
}
|
|
2007
|
+
function formatText(value, rule, input, name) {
|
|
2008
|
+
let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
|
|
2009
|
+
if (!text2) {
|
|
2010
|
+
const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : (input.slug ? rule?.fallbackBySlug?.[input.slug] : void 0) ?? rule?.fallbackWithoutParent ?? rule?.fallback;
|
|
2011
|
+
text2 = cleanText((fallback ?? "").replaceAll("%s", () => name), rule);
|
|
2012
|
+
}
|
|
2013
|
+
if (!text2) return void 0;
|
|
2014
|
+
if (rule?.maxLength && text2.length > rule.maxLength) {
|
|
2015
|
+
const suffix = rule.ellipsis ?? "";
|
|
2016
|
+
const head = text2.slice(0, rule.maxLength - suffix.length);
|
|
2017
|
+
return suffix ? head.trimEnd() + suffix : head;
|
|
2018
|
+
}
|
|
2019
|
+
return text2;
|
|
2020
|
+
}
|
|
2021
|
+
function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
|
|
2022
|
+
if (!normalizeCloudflareImages) return value;
|
|
2023
|
+
try {
|
|
2024
|
+
const url = new URL(value);
|
|
2025
|
+
const [account, id, variant] = url.pathname.split("/").filter(Boolean);
|
|
2026
|
+
if (url.protocol === "https:" && url.hostname === "imagedelivery.net" && account && id && variant) {
|
|
2027
|
+
url.pathname = `/${account}/${id}/lg`;
|
|
2028
|
+
return url.href;
|
|
2029
|
+
}
|
|
2030
|
+
} catch {
|
|
2031
|
+
}
|
|
2032
|
+
return value;
|
|
2033
|
+
}
|
|
2034
|
+
function legacyImageUrl(value, rule) {
|
|
2035
|
+
if (!value || rule?.legacyImageSource !== "legacy-thumbnail") return void 0;
|
|
2036
|
+
try {
|
|
2037
|
+
const url = new URL(value);
|
|
2038
|
+
if (!rule.legacyImageAllowedOrigins?.includes(url.origin)) return void 0;
|
|
2039
|
+
return readMetadataValues({ ogImage: url.href }).ogImage;
|
|
2040
|
+
} catch {
|
|
2041
|
+
return void 0;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function contentCandidateAllowed(value, input, rule) {
|
|
2045
|
+
if (!rule?.imageAllowedOrigins) return true;
|
|
2046
|
+
if (value.startsWith("/") && !value.startsWith("//")) return true;
|
|
2047
|
+
try {
|
|
2048
|
+
const url = new URL(value);
|
|
2049
|
+
if (url.protocol !== "https:") return false;
|
|
2050
|
+
const origins = [...rule.imageAllowedOrigins];
|
|
2051
|
+
if (input.imageBaseUrl) origins.push(new URL(input.imageBaseUrl).origin);
|
|
2052
|
+
return origins.includes(url.origin);
|
|
2053
|
+
} catch {
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function formatMetadataDefaults(input, declaration) {
|
|
2058
|
+
const dynamic = declaration[input.kind];
|
|
2059
|
+
const rule = dynamic?.defaultRules?.[input.modelKey];
|
|
2060
|
+
const name = cleanText(input.title, rule?.title);
|
|
2061
|
+
const title = formatText(input.title, rule?.title, input, name);
|
|
2062
|
+
const template = dynamic?.titleTemplates?.[input.modelKey];
|
|
2063
|
+
const description = formatText(input.description, rule?.description, input, name);
|
|
2064
|
+
const bodyImage = input.firstBodyImage && (input.firstBodyImage.startsWith("https://") || /^\/(?!\/)/u.test(input.firstBodyImage)) ? input.firstBodyImage : void 0;
|
|
2065
|
+
const candidateImage = input.kind === "terms" && rule?.useTermImage === false ? void 0 : input.featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
|
|
2066
|
+
const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
|
|
2067
|
+
return {
|
|
2068
|
+
...title ? { title: template ? template.replace("%s", () => title) : title } : {},
|
|
2069
|
+
...description ? { description } : {},
|
|
2070
|
+
...ogImage ? { ogImage } : {}
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/redirect-paths.ts
|
|
2075
|
+
var CONTROL_OR_BACKSLASH = /[\\\u0000-\u001f\u007f]/u;
|
|
2076
|
+
var PROTECTED_PREFIX = /^\/(?:api|_next|_vercel|auth|admin|login|logout|revalidate|preview|assets|static|images|fonts|\.well-known)(?:\/|$)/i;
|
|
2077
|
+
var STATIC_EXTENSION = /\.(?:ico|png|jpe?g|gif|svg|webp|avif|css|js|txt|xml|json|woff2?|map)$/i;
|
|
2078
|
+
function decoded(value) {
|
|
2079
|
+
try {
|
|
2080
|
+
return decodeURIComponent(value);
|
|
2081
|
+
} catch {
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
function trimInternalPath(path) {
|
|
2086
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
2087
|
+
if (path.length > 1) path = path.replace(/\/+$/, "");
|
|
2088
|
+
return path || "/";
|
|
2089
|
+
}
|
|
2090
|
+
function normalizeRedirectPath(raw) {
|
|
2091
|
+
return trimInternalPath((decoded(raw) ?? raw).split(/[?#]/, 1)[0] ?? "");
|
|
2092
|
+
}
|
|
2093
|
+
function isProtectedRedirectPath(path) {
|
|
2094
|
+
const normalized = normalizeRedirectPath(path);
|
|
2095
|
+
return PROTECTED_PREFIX.test(normalized) || STATIC_EXTENSION.test(normalized);
|
|
2096
|
+
}
|
|
2097
|
+
function unsafePath(path) {
|
|
2098
|
+
return CONTROL_OR_BACKSLASH.test(path) || path.startsWith("//") || /(?:^|\/)\.{1,2}(?:\/|$)/.test(path) || /[%](?:2f|5c|0[0-9a-f]|1[0-9a-f]|7f)/i.test(path);
|
|
2099
|
+
}
|
|
2100
|
+
function normalizeRedirectSource(raw) {
|
|
2101
|
+
const invalid = { ok: false, code: "invalid_source_path" };
|
|
2102
|
+
if (CONTROL_OR_BACKSLASH.test(raw)) return invalid;
|
|
2103
|
+
const trimmed = raw.trim();
|
|
2104
|
+
if (!trimmed || /^[a-z][a-z\d+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) return invalid;
|
|
2105
|
+
const rawPath = trimmed.split(/[?#]/, 1)[0] ?? "";
|
|
2106
|
+
const path = decoded(rawPath);
|
|
2107
|
+
if (!path || unsafePath(path) || /[?#*]/.test(path)) return invalid;
|
|
2108
|
+
const normalized = trimInternalPath(path);
|
|
2109
|
+
if (isProtectedRedirectPath(normalized)) return invalid;
|
|
2110
|
+
return { ok: true, value: normalized };
|
|
2111
|
+
}
|
|
2112
|
+
function normalizeRedirectTarget(raw) {
|
|
2113
|
+
const invalid = { ok: false, code: "invalid_target" };
|
|
2114
|
+
const decodedRaw = decoded(raw);
|
|
2115
|
+
if (decodedRaw === null || CONTROL_OR_BACKSLASH.test(raw) || CONTROL_OR_BACKSLASH.test(decodedRaw)) return invalid;
|
|
2116
|
+
const trimmed = raw.trim();
|
|
2117
|
+
if (!trimmed || trimmed.startsWith("//")) return invalid;
|
|
2118
|
+
if (trimmed.startsWith("/")) {
|
|
2119
|
+
const suffixIndex = trimmed.search(/[?#]/);
|
|
2120
|
+
const rawPath = suffixIndex === -1 ? trimmed : trimmed.slice(0, suffixIndex);
|
|
2121
|
+
const path = decoded(rawPath);
|
|
2122
|
+
if (!path || unsafePath(path) || /[?#]/.test(path)) return invalid;
|
|
2123
|
+
const suffix = suffixIndex === -1 ? "" : trimmed.slice(suffixIndex);
|
|
2124
|
+
return { ok: true, value: `${trimInternalPath(path)}${suffix}`, kind: "internal" };
|
|
2125
|
+
}
|
|
2126
|
+
try {
|
|
2127
|
+
const url = new URL(trimmed);
|
|
2128
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password) return invalid;
|
|
2129
|
+
return { ok: true, value: url.toString(), kind: "external" };
|
|
2130
|
+
} catch {
|
|
2131
|
+
return invalid;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1853
2134
|
export {
|
|
1854
2135
|
BLOCK_NAME_PATTERN,
|
|
1855
2136
|
CMS_FONT_SIZES,
|
|
@@ -1866,6 +2147,14 @@ export {
|
|
|
1866
2147
|
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1867
2148
|
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1868
2149
|
LEGACY_BLOG_DETAIL_PATH,
|
|
2150
|
+
METADATA_FIELDS,
|
|
2151
|
+
MetadataDefaultRulesSchema,
|
|
2152
|
+
MetadataFieldSchema,
|
|
2153
|
+
MetadataPatchSchema,
|
|
2154
|
+
MetadataRobotsPolicySchema,
|
|
2155
|
+
MetadataTargetKindSchema,
|
|
2156
|
+
MetadataTextRuleSchema,
|
|
2157
|
+
MetadataValuesSchema,
|
|
1869
2158
|
PHASE_1_CORE_BLOCKS,
|
|
1870
2159
|
PHASE_1_CSP,
|
|
1871
2160
|
POST_FOOTER_SLOT,
|
|
@@ -1894,6 +2183,7 @@ export {
|
|
|
1894
2183
|
findInternalContentLinkTokens,
|
|
1895
2184
|
findSectionLockViolations,
|
|
1896
2185
|
findSitePatternPlacementRule,
|
|
2186
|
+
formatMetadataDefaults,
|
|
1897
2187
|
formatSectionLockViolations,
|
|
1898
2188
|
htmlToPlainText,
|
|
1899
2189
|
injectStableIdsTree,
|
|
@@ -1903,18 +2193,25 @@ export {
|
|
|
1903
2193
|
isAllowedEmbedSrc,
|
|
1904
2194
|
isAllowedMapEmbedSrc,
|
|
1905
2195
|
isInCollectionMode,
|
|
2196
|
+
isProtectedRedirectPath,
|
|
1906
2197
|
isSectionContentPropPath,
|
|
1907
2198
|
isSitePatternSlotKey,
|
|
1908
2199
|
isValidBlock,
|
|
1909
2200
|
isValidSitePatternKey,
|
|
1910
2201
|
issuePreviewToken,
|
|
2202
|
+
mergeMetadataPatch,
|
|
2203
|
+
metadataContentImageUrl,
|
|
1911
2204
|
normalizeEmbed,
|
|
1912
2205
|
normalizeHexColor,
|
|
1913
2206
|
normalizeImageAlt,
|
|
2207
|
+
normalizeRedirectPath,
|
|
2208
|
+
normalizeRedirectSource,
|
|
2209
|
+
normalizeRedirectTarget,
|
|
1914
2210
|
parseBlock,
|
|
1915
2211
|
postsForCollection,
|
|
1916
2212
|
postsInAnyCollection,
|
|
1917
2213
|
postsNotInAnyCollection,
|
|
2214
|
+
readMetadataValues,
|
|
1918
2215
|
readSitePatternPlacements,
|
|
1919
2216
|
readSitePatternPresentation,
|
|
1920
2217
|
referencedSitePatternKeys,
|
|
@@ -1925,6 +2222,7 @@ export {
|
|
|
1925
2222
|
resolveCategoryTreeEntryPath,
|
|
1926
2223
|
resolveCategoryTreeHubPaths,
|
|
1927
2224
|
resolveCategoryTreeSelection,
|
|
2225
|
+
resolveMetadata,
|
|
1928
2226
|
resolvePatternSlots,
|
|
1929
2227
|
resolvePostCategorySlug,
|
|
1930
2228
|
resolvePostCollection,
|