@roottale/cms-core 0.67.1 → 0.68.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/dist/index.js CHANGED
@@ -1960,42 +1960,37 @@ function mergeMetadataPatch(current, input) {
1960
1960
  }
1961
1961
 
1962
1962
  // src/metadata-defaults.ts
1963
+ import { z as z4 } from "zod";
1964
+
1965
+ // src/metadata-text.ts
1963
1966
  import { z as z3 } from "zod";
1964
1967
  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");
1968
+ var templateToken = /\{\{(?:title|field:[a-zA-Z][a-zA-Z0-9_-]{0,159})\}\}/gu;
1969
+ var template = fallbackText.refine(
1970
+ (value) => !/[{}]/u.test(value.replace(templateToken, "")),
1971
+ "\uC790\uB3D9 \uD14D\uC2A4\uD2B8\uC5D0\uB294 {{title}} \uB610\uB294 {{field:\uD544\uB4DC\uC774\uB984}} \uD1A0\uD070\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4"
1972
+ );
1973
1973
  var MetadataTextRuleSchema = z3.object({
1974
- source: z3.enum(["content", "fallback"]).optional(),
1974
+ source: z3.enum(["content", "fallback", "template"]).optional(),
1975
+ template: template.optional(),
1975
1976
  stripLeadingLabel: z3.enum(["question", "answer"]).optional(),
1976
1977
  replaceLineBreakTokens: z3.boolean().optional(),
1977
1978
  removeEmDashes: z3.boolean().optional(),
1978
1979
  normalizeWhitespace: z3.boolean().optional(),
1979
1980
  maxLength: z3.number().int().min(1).max(2e3).optional(),
1981
+ lengthUnit: z3.enum(["code-units", "code-points"]).optional(),
1982
+ wordBoundaryMinLength: z3.number().int().min(1).max(2e3).optional(),
1980
1983
  ellipsis: z3.string().max(10).optional(),
1984
+ bodyFallback: z3.object({
1985
+ preferContaining: z3.string().trim().min(1).max(100).optional(),
1986
+ stripLeadingLabel: z3.literal("conclusion").optional()
1987
+ }).strict().optional(),
1981
1988
  fallback: fallbackText.optional(),
1982
1989
  fallbackWithParent: fallbackText.optional(),
1983
1990
  fallbackWithoutParent: fallbackText.optional(),
1984
1991
  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) {
1992
+ }).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").refine((rule) => rule.source === "template" ? rule.template !== void 0 : rule.template === void 0, "\uD14D\uC2A4\uD2B8 template\uC740 source: template\uACFC \uD568\uAED8 \uC120\uC5B8\uD569\uB2C8\uB2E4").refine((rule) => !rule.wordBoundaryMinLength || Boolean(rule.maxLength && rule.wordBoundaryMinLength < rule.maxLength), "\uB2E8\uC5B4 \uACBD\uACC4\uC5D0\uB294 \uB354 \uAE34 \uCD5C\uB300 \uAE38\uC774\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4");
1993
+ function cleanMetadataText(value, rule) {
1999
1994
  let text2 = value.trim();
2000
1995
  if (rule?.stripLeadingLabel === "question") text2 = text2.replace(/^\s*Q[..:]?\s*/iu, "");
2001
1996
  if (rule?.stripLeadingLabel === "answer") text2 = text2.replace(/^\s*A[..:]?\s*/iu, "");
@@ -2004,20 +1999,90 @@ function cleanText(value, rule) {
2004
1999
  if (rule?.normalizeWhitespace) text2 = text2.replace(/\s+/gu, " ");
2005
2000
  return text2.trim();
2006
2001
  }
2007
- function formatText(value, rule, input, name) {
2008
- let text2 = rule?.source === "fallback" ? "" : cleanText(value ?? "", rule);
2002
+ function metadataBodyParagraphs(value) {
2003
+ const paragraphs = [];
2004
+ const textParts = (node) => {
2005
+ if (Array.isArray(node)) return node.flatMap(textParts);
2006
+ if (!node || typeof node !== "object") return [];
2007
+ const item = node;
2008
+ return [...typeof item.text === "string" ? [item.text] : [], ...textParts(item.content)];
2009
+ };
2010
+ const walk = (node) => {
2011
+ if (Array.isArray(node)) {
2012
+ node.forEach(walk);
2013
+ return;
2014
+ }
2015
+ if (!node || typeof node !== "object") return;
2016
+ const item = node;
2017
+ if (item.type === "paragraph") {
2018
+ const text2 = textParts(item.content).join(" ").replace(/\s+/gu, " ").trim();
2019
+ if (text2) paragraphs.push(text2);
2020
+ } else walk(item.content);
2021
+ };
2022
+ walk(value);
2023
+ return paragraphs;
2024
+ }
2025
+ function formatMetadataText(value, rule, input, name) {
2026
+ let text2 = rule?.source === "fallback" ? "" : cleanMetadataText(value ?? "", rule);
2027
+ if (rule?.source === "template") {
2028
+ text2 = cleanMetadataText((rule.template ?? "").replace(templateToken, (token) => {
2029
+ if (token === "{{title}}") return name;
2030
+ const field = token.slice(8, -2);
2031
+ const value2 = Object.hasOwn(input.fields ?? {}, field) ? input.fields?.[field] : void 0;
2032
+ return typeof value2 === "string" ? value2.trim() : "";
2033
+ }), rule);
2034
+ }
2035
+ if (!text2 && rule?.bodyFallback) {
2036
+ const paragraphs = input.bodyParagraphs ?? [];
2037
+ const preferred = rule.bodyFallback.preferContaining;
2038
+ text2 = (preferred ? paragraphs.find((paragraph) => paragraph.includes(preferred)) : void 0) ?? paragraphs[0] ?? "";
2039
+ if (rule.bodyFallback.stripLeadingLabel === "conclusion") text2 = text2.replace(/^(먼저\s*)?결론부터\s*(말씀드리면|말씀드릴게요)[.!?]?\s*/u, "");
2040
+ text2 = cleanMetadataText(text2, rule);
2041
+ }
2009
2042
  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);
2043
+ const slugFallback = input.slug && Object.hasOwn(rule?.fallbackBySlug ?? {}, input.slug) ? rule?.fallbackBySlug?.[input.slug] : void 0;
2044
+ const fallback = input.parentId ? rule?.fallbackWithParent ?? rule?.fallback : slugFallback ?? rule?.fallbackWithoutParent ?? rule?.fallback;
2045
+ text2 = cleanMetadataText((fallback ?? "").replaceAll("%s", () => name), rule);
2012
2046
  }
2013
2047
  if (!text2) return void 0;
2014
- if (rule?.maxLength && text2.length > rule.maxLength) {
2048
+ const units = rule?.lengthUnit === "code-points" ? [...text2] : text2.split("");
2049
+ if (rule?.maxLength && units.length > rule.maxLength) {
2015
2050
  const suffix = rule.ellipsis ?? "";
2016
- const head = text2.slice(0, rule.maxLength - suffix.length);
2051
+ const suffixLength = rule.lengthUnit === "code-points" ? [...suffix].length : suffix.length;
2052
+ let head = units.slice(0, rule.maxLength - suffixLength).join("");
2053
+ const boundary = head.lastIndexOf(" ");
2054
+ if (rule.wordBoundaryMinLength && boundary >= rule.wordBoundaryMinLength) head = head.slice(0, boundary);
2017
2055
  return suffix ? head.trimEnd() + suffix : head;
2018
2056
  }
2019
2057
  return text2;
2020
2058
  }
2059
+
2060
+ // src/metadata-defaults.ts
2061
+ var imageOrigin = z4.string().max(2048).refine((value) => {
2062
+ try {
2063
+ const url = new URL(value);
2064
+ return url.protocol === "https:" && url.origin === value;
2065
+ } catch {
2066
+ return false;
2067
+ }
2068
+ }, "\uC774\uBBF8\uC9C0 \uD5C8\uC6A9 \uBAA9\uB85D\uC5D0\uB294 \uACBD\uB85C \uC5C6\uB294 HTTPS origin\uB9CC \uC120\uC5B8\uD569\uB2C8\uB2E4");
2069
+ var MetadataDefaultRulesSchema = z4.object({
2070
+ title: MetadataTextRuleSchema.optional(),
2071
+ titleTemplateMaxLength: z4.number().int().min(1).max(1e3).optional(),
2072
+ description: MetadataTextRuleSchema.optional(),
2073
+ /** Code-owned defaults precede site defaults but always follow manual overrides. */
2074
+ defaults: MetadataValuesSchema.optional(),
2075
+ robots: MetadataRobotsPolicySchema.optional(),
2076
+ useFeaturedImage: z4.boolean().optional(),
2077
+ useFirstBodyImage: z4.boolean().optional(),
2078
+ useTermImage: z4.boolean().optional(),
2079
+ imageAltSource: z4.enum(["content", "title"]).optional(),
2080
+ legacyImageSource: z4.literal("legacy-thumbnail").optional(),
2081
+ imageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2082
+ legacyImageAllowedOrigins: z4.array(imageOrigin).min(1).max(32).optional(),
2083
+ normalizeCloudflareImages: z4.boolean().optional(),
2084
+ fallback: MetadataValuesSchema.optional()
2085
+ }).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");
2021
2086
  function metadataContentImageUrl(value, normalizeCloudflareImages = false) {
2022
2087
  if (!normalizeCloudflareImages) return value;
2023
2088
  try {
@@ -2057,15 +2122,19 @@ function contentCandidateAllowed(value, input, rule) {
2057
2122
  function formatMetadataDefaults(input, declaration) {
2058
2123
  const dynamic = declaration[input.kind];
2059
2124
  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);
2125
+ const name = cleanMetadataText(input.title, rule?.title);
2126
+ const title = formatMetadataText(input.title, rule?.title, input, name);
2127
+ const template2 = dynamic?.titleTemplates?.[input.modelKey];
2128
+ const templatedTitle = title && template2 ? template2.replace("%s", () => title) : title;
2129
+ const finalTitle = templatedTitle && rule?.titleTemplateMaxLength && templatedTitle.length > rule.titleTemplateMaxLength ? title : templatedTitle;
2130
+ const description = formatMetadataText(input.description, rule?.description, input, name);
2064
2131
  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);
2132
+ const featuredImage = (input.kind === "terms" ? rule?.useTermImage : rule?.useFeaturedImage) === false ? void 0 : input.featuredImage;
2133
+ const candidateImage = featuredImage || (rule?.useFirstBodyImage ? bodyImage : void 0) || legacyImageUrl(input.legacyImage, rule);
2066
2134
  const ogImage = candidateImage && contentCandidateAllowed(candidateImage, input, rule) ? readMetadataValues({ ogImage: metadataContentImageUrl(candidateImage, rule?.normalizeCloudflareImages) }).ogImage : void 0;
2067
2135
  return {
2068
- ...title ? { title: template ? template.replace("%s", () => title) : title } : {},
2136
+ ...readMetadataValues(rule?.defaults),
2137
+ ...finalTitle ? { title: finalTitle } : {},
2069
2138
  ...description ? { description } : {},
2070
2139
  ...ogImage ? { ogImage } : {}
2071
2140
  };
@@ -2200,6 +2269,7 @@ export {
2200
2269
  isValidSitePatternKey,
2201
2270
  issuePreviewToken,
2202
2271
  mergeMetadataPatch,
2272
+ metadataBodyParagraphs,
2203
2273
  metadataContentImageUrl,
2204
2274
  normalizeEmbed,
2205
2275
  normalizeHexColor,