import { z, ZodSchema } from 'zod'; /** WP-compatible name pattern: `core/` | `roottale/` */ declare const BLOCK_NAME_PATTERN: RegExp; /** * Block schema (recursive — innerBlocks). * `_id` 는 internal-only (export 시 strip, ADR §5). */ declare const blockSchema: z.ZodType; interface Block { /** uuidv7. Internal-only — never exported as WP delimiter attribute. */ _id: string; /** `core/` or `roottale/` */ name: string; attributes: Record; innerBlocks: Block[]; /** WP delimiter innerHTML (preserved for round-trip — codex v2 verdict #7). */ rawHtml?: string; metadata?: { bindings?: Record; }>; name?: string; reusableRef?: string; }; } declare function isValidBlock(value: unknown): value is Block; declare function parseBlock(value: unknown): Block; /** * Phase 1 core block names (10) — ADR-0034 §5. * Phase 2+ 에 `core/embed`, `core/gallery`, `core/cover` 등 추가. */ declare const PHASE_1_CORE_BLOCKS: readonly ["core/paragraph", "core/heading", "core/image", "core/list", "core/quote", "core/code", "core/columns", "core/group", "core/separator", "core/spacer"]; type Phase1CoreBlockName = (typeof PHASE_1_CORE_BLOCKS)[number]; interface BlockBinding { source: string; args?: Record; } /** * 바인딩 해석기. 주어진 binding(+attribute/block)에 대한 값을 반환. * `undefined` 반환 = 미해석(원래 attribute 유지). */ type BindingResolver = (binding: BlockBinding, attribute: string, block: Block) => unknown; /** ACF 필드 바인딩 source id. */ declare const FIELD_BINDING_SOURCE = "roottale/field"; /** WP ACF 호환 alias (import round-trip). */ declare const FIELD_BINDING_SOURCE_ALIAS = "acf/field"; /** * acf 값 record 로부터 필드 바인딩 resolver 생성. * `roottale/field` / `acf/field` source 의 `args.name` 을 record 에서 조회. */ declare function createFieldBindingResolver(fields: Record | undefined | null): BindingResolver; /** 여러 resolver 를 순서대로 시도(첫 non-undefined 채택). */ declare function chainResolvers(...resolvers: BindingResolver[]): BindingResolver; /** * 블록 트리에 바인딩을 적용해 새 트리 반환(immutable). 바인딩 없으면 원본 그대로. */ declare function applyBlockBindings(blocks: readonly Block[], resolver: BindingResolver): Block[]; /** * bindings 에서 `attribute` 의 필드 값을 조회. `roottale/field`/`acf/field` * source 만 해석. 빈 값(undefined/null/"") = undefined → caller 가 fallback 렌더. */ declare function boundFieldValue(bindings: unknown, attribute: string, fields: Record | null | undefined): unknown; /** * content 바인딩 값의 텍스트화. **항상 plain text** — caller 가 escape 해 * 삽입한다 (바인딩 경로로 raw HTML 진입 금지, D4). string/number 외 = undefined. */ declare function bindingTextValue(value: unknown): string | undefined; /** * image `src` 바인딩 값 → URL. 포맷된 MediaRef(`{url}`) 또는 http(s) 문자열만 * 인정 — raw media id(uuid) 는 URL 이 아니므로 fallback. */ declare function bindingSrcValue(value: unknown): string | undefined; /** * 다중 스트림 콘텐츠 라우팅 프리미티브 (ADR-0060). * * 같은 글 풀(post)을 둘 이상의 스트림으로 분리(공지=게시판 /notice, 블로그=그리드 * /blog 등). `cms-renderer-next` / `cms-renderer-astro` 가 동일 로직을 공유하도록 * 여기(cms-core)에 둔다 — cms-client·프레임워크 비의존(구조적 타입만). * * 설계(codex consult `019ef577` 반영): * - **활성화 = `collections.length > 0`.** 빈 배열·미전달 = 레거시 단일 /blog. * - `RouteCollection` 은 **라우팅 최소셋**만(key/basePath/categories/feed/archives와 * 경로 정책) 가진다. * label/layout/icon 등 표시·작성 메타는 templates `CollectionSchema` 에 둔다. */ /** 콘텐츠 스트림 1개 — sitemap/feed/아카이브 파생의 라우팅 단위. */ interface RouteCollection { /** 안정 식별자 (예: "notice" | "blog"). */ readonly key: string; /** * 라우트 base path (예: "/notice" | "/blog"). 앞 슬래시 포함, 뒤 슬래시 없이. * **`""`(빈 문자열) = 주소 없는 유형** — 페이지 안에서 불러와 쓰는 콘텐츠(강사·리뷰 * 등)로, 라우트/사이트맵/피드/아카이브를 일절 파생하지 않는다. 외부 사이트는 * posts API 의 `collectionKey` 필터로 내용만 가져다 렌더한다. `resolvePostUrl`/ * `resolvePostPath` 는 이 값이 빈 문자열이면 항상 null. 소속 판정 * (`resolvePostCollection`)에는 영향 없음 — key 만으로 소속이 성립한다. */ readonly basePath: string; /** * 이 섹션이 제공하는 **주제(category) allowlist** — `${basePath}/categories/{slug}` * 아카이브 라우트 범위. ADR-0060 Amendment 1 (M4): 섹션 소속은 글의 `collectionKey` * 로 정해지므로 이 배열은 더 이상 라우팅 소유권이 아니다(catch-all 개념 폐기). 빈 * 배열 = 이 섹션은 주제 없음. */ readonly categories: readonly string[]; /** RSS 피드 포함 여부 (기본 false). */ readonly feed?: boolean; /** `${basePath}/categories/{slug}` 아카이브 emit 여부 (기본 false). */ readonly archives?: boolean; /** * 글별 상세 라우트 존재 여부 (기본 true — 하위 호환). false 면 이 스트림은 * "목록 전용" 유형(예: 후기 — 목록에서 전문 노출, 글마다 별도 상세 URL 없음). * `resolvePostUrl`/`resolvePostPath` 가 null 을 반환해 sitemap/feed 에서 글 * URL 을 발행하지 않는다. 소속 판정(`resolvePostCollection`)에는 영향 없음 * — 소속과 상세 URL 존재 여부는 별개(M4 원칙 연장). */ readonly detail?: boolean; /** * 글 상세 주소 형식. `flat`(기본) = `${basePath}/{postSlug}`, * `category` = `${basePath}/{categorySlug}/{postSlug}`. * category 모드는 URL을 하나로 결정해야 하므로 `categoryCardinality: "exactly-one"` * 과 함께 쓴다. */ readonly detailPath?: "flat" | "category"; /** * 카테고리 허브 주소 형식. `namespaced`(기본) = * `${basePath}/categories/{slug}`, `direct` = `${basePath}/{slug}`. */ readonly categoryPath?: "namespaced" | "direct"; /** 글에 허용하는 category term 개수. 기본 `multiple`(기존 동작). */ readonly categoryCardinality?: "multiple" | "exactly-one"; } /** resolve 입력 — 글의 구조적 최소 형태(cms-client `CmsPostContent` 와 구조 호환). */ interface RoutablePost { readonly slug: string; /** 글의 주제(category) terms — 아카이브 라우트 필터용. 섹션 판정엔 쓰지 않는다(M4). */ readonly terms: ReadonlyArray<{ readonly taxonomy: string; readonly slug: string; }>; /** * ADR-0060 Amendment 1 (Option A, M4) — 글에 명시된 섹션 key. 섹션 판정의 유일한 * 근거. 미설정/미존재면 어느 스트림에도 안 속함(null). 카테고리 파생 fallback 은 제거됨. */ readonly collectionKey?: string | null; } /** collection 모드인지 — 길이 0(또는 미전달)이면 레거시 단일 /blog 동작. */ declare function isInCollectionMode(collections: readonly RouteCollection[] | undefined): boolean; /** * 글이 속한 collection 1개를 해석 — ADR-0060 Amendment 1 (Option A, M4): **글의 * 명시 `collectionKey` 만** 본다. 미설정/미존재 key 면 null(상세 라우트 없는 글 → * sitemap 제외 신호). 카테고리 slug 파생·catch-all·first-wins 는 M4 에서 제거됨 — * 섹션은 카테고리가 아니라 글에 명시한다. (`categories` 는 이제 주제 allowlist 로만, * 아카이브 라우트에서 쓰임.) */ declare function resolvePostCollection(post: RoutablePost, collections: readonly RouteCollection[]): RouteCollection | null; /** * 글의 정규 절대 URL — 소속 스트림 basePath 기준. 어느 스트림에도 안 속하거나, * 소속 스트림이 `detail: false`(목록 전용 유형) 이거나, `basePath` 가 빈 문자열 * (주소 없는 유형)이면 null. slug 는 percent-encoding (한글 slug 안전). */ declare function resolvePostUrl(siteUrl: string, post: RoutablePost, collections: readonly RouteCollection[]): string | null; /** * category 기반 상세 주소에 사용할 단일 category slug. collection 에 연결된 * category만 세고 정확히 1개일 때만 반환한다. 0개·2개 이상이면 정규 URL을 * 임의 선택하지 않고 null 로 닫는다. */ declare function resolvePostCategorySlug(post: RoutablePost, collection: RouteCollection): string | null; /** 카테고리 허브 경로. archives 가 꺼졌거나 주소가 없으면 null. */ declare function resolveCategoryPath(collection: RouteCollection, categorySlug: string): string | null; /** * 글의 *경로*(siteUrl 없는 basePath-상대) — 예: "/blog/hello". 소속 스트림이 * 없거나, `detail: false`(목록 전용 유형) 이거나, `basePath` 가 빈 문자열 * (주소 없는 유형)이면 null. astro 처럼 상대 href 가 필요한 소비자용 * (`resolvePostUrl` 은 절대 URL). */ declare function resolvePostPath(post: RoutablePost, collections: readonly RouteCollection[]): string | null; /** * 특정 collection에 명시적으로 귀속된 글만 남긴다. */ declare function postsForCollection(posts: readonly T[], collections: readonly RouteCollection[], key: string): T[]; /** feed/sitemap 등 파생에서 제외할, 어느 스트림에도 안 속한 글 제거. */ declare function postsInAnyCollection(posts: readonly T[], collections: readonly RouteCollection[]): T[]; /** * `postsInAnyCollection` 의 대칭 — **어느 스트림에도 안 속한("미소속") 글만** 남긴다. * 판정 시맨틱 = `resolvePostCollection(post, collections) === null`(collectionKey 가 * NULL 이거나 사이트 선언 컬렉션 밖). `/blog` 처럼 "선언 스트림에 안 잡힌 글 = 블로그"로 * 취급하는 표면에서 스트림 글 누출을 막는 데 쓴다(`blog-list-collection-scope.md` §2①). */ declare function postsNotInAnyCollection(posts: readonly T[], collections: readonly RouteCollection[]): T[]; /** * config 검증 — 중복 key/basePath, 그리고 **두 스트림에 동시 등장하는 category slug** * (라우팅 모호성)를 탐지. `basePath === ""`(주소 없는 유형)는 basePath 형식·중복 * 검사를 건너뛴다 — 라우트가 없으므로 여러 개 공존해도 모호성이 없다. key 중복· * category 중복 검사는 주소 유무와 무관하게 전 항목에 적용된다. 문제 메시지 배열 * 반환(빈 = 정상). 빌드 타임 fail-fast 용. */ declare function validateCollections(collections: readonly RouteCollection[]): string[]; /** * 콘텐츠 모델의 계층형 분류 주소 계산기. * * FAQ·도움말·문서처럼 하나의 모델이 여러 단계 분류 허브를 가지는 경우에 쓴다. * 저장소나 프레임워크를 모르며, 공개 API와 ROOT-ADMIN이 같은 계산을 재사용한다. */ interface CategoryTreePresentation { readonly basePath: string; readonly categoryDepth: number; } interface CategoryTreeTerm { readonly id: string; readonly slug: string; readonly parentId?: string | null; } interface CategoryTreeEntry { readonly slug: string; readonly terms: ReadonlyArray<{ readonly id: string; readonly taxonomy: string; }>; } /** * 선택한 말단 분류부터 루트까지의 사슬을 루트→말단 순서로 돌려준다. * 끊긴 부모·순환·중복 id·깊이 불일치·말단이 아닌 선택은 null로 닫는다. */ declare function resolveCategoryTreeChain(presentation: CategoryTreePresentation, leafId: string, terms: readonly CategoryTreeTerm[]): CategoryTreeTerm[] | null; /** 루트부터 선택 분류까지 모든 허브 주소. */ declare function resolveCategoryTreeHubPaths(presentation: CategoryTreePresentation, termId: string, terms: readonly CategoryTreeTerm[]): string[] | null; /** 계층형 분류 모델 항목의 정규 상세 주소. */ declare function resolveCategoryTreeEntryPath(presentation: CategoryTreePresentation, entry: CategoryTreeEntry, terms: readonly CategoryTreeTerm[]): string | null; /** * 글의 정규 공개 경로 — 플랫폼 전체가 쓰는 **하나의** 계산기 (ADR-0105 개정). * * 우선순위: 콘텐츠 모델 presentation(fixed_page → detail → category_tree) → * 호환 컬렉션(`resolvePostPath`) → 레거시 `/blog/{slug}`. `data_only` 모델은 * 상세 주소가 없으므로 호환 컬렉션이 있으면 그 규칙을, 없으면 null 을 돌려준다. * * 이 값은 저장 시 `posts.public_path` 로 물질화되고(경로 이력 포함), 웹훅·사이트맵· * 단축링크·내부 링크 키·리다이렉트는 저장된 값을 읽는다. 여기 말고 다른 곳에서 * 주소를 다시 계산하지 않는다. */ /** 콘텐츠 모델 presentation 의 구조적 최소형(cms-models 스키마와 호환). */ type PublicPathPresentation = { readonly kind: "data_only"; } | { readonly kind: "fixed_page"; readonly path: string; } | { readonly kind: "detail"; readonly detailPath: string; } | { readonly kind: "category_tree"; readonly basePath: string; readonly categoryDepth: number; }; interface PublicPathPost { readonly slug: string; readonly collectionKey?: string | null; readonly modelKey?: string | null; readonly terms: ReadonlyArray<{ readonly id: string; readonly taxonomy: string; readonly slug: string; }>; } interface PublicPathContext { /** 글의 모델 presentation. 모델 글이 아니거나 모델을 못 찾으면 null. */ readonly presentation: PublicPathPresentation | null; /** 모델의 호환 컬렉션 key(`compatibilityCollectionKey`, 미지정이면 모델 key). */ readonly compatibilityCollectionKey?: string | null; readonly collections: readonly RouteCollection[]; readonly categories: readonly CategoryTreeTerm[]; } declare const LEGACY_BLOG_DETAIL_PATH = "/blog"; /** 글의 정규 공개 경로(origin 제외). 상세 주소가 없으면 null. */ declare function resolvePostPublicPath(post: PublicPathPost, context: PublicPathContext): string | null; interface ClientHydrationSpec { hydration?: "visible" | "idle" | "load" | "media" | "only"; module?: string; } interface BlockDefinition> { name: string; meta: { title: string; description?: string; category?: "text" | "media" | "design" | "widgets" | "theme" | "embed"; icon?: string; }; schema: ZodSchema; supports?: { align?: boolean; color?: boolean; spacing?: boolean; typography?: boolean; customClassName?: boolean; }; parent?: readonly string[]; ancestor?: readonly string[]; render: (block: Block & { attributes: A; }) => unknown; clientHydration?: ClientHydrationSpec; } type BlockRegistry = ReadonlyMap; /** * Block → WP delimiter 형식 직렬화 (export). * `_id` 와 `metadata.reusableRef` 는 strip — WP 호환. */ declare function serializeBlock(block: Block): string; type EmbedProvider = "youtube" | "vimeo"; interface NormalizedEmbed { provider: EmbedProvider; /** allowlist host 의 canonical iframe src (가능 시 privacy-enhanced). */ embedSrc: string; /** 사용자가 입력한 원본 URL — fallback 링크 / round-trip 보존용. */ originalUrl: string; } /** * 사용자 입력 URL → allowlist provider 의 canonical embed. 지원하지 않는 * provider / 파싱 실패 시 `null`. */ declare function normalizeEmbed(input: unknown): NormalizedEmbed | null; /** * 이미 저장된 embed src 가 allowlist host(https) 인지 검증 — 렌더러가 iframe * 출력 전 호출. 임의 host / 비 https 는 거부. */ declare function isAllowedEmbedSrc(value: unknown): value is string; /** * 지도 embed src allowlist — 렌더러가 contact 지도 iframe 출력 전 호출 * (W8-7, `isAllowedEmbedSrc` 의 영상 allowlist 와 별도). * * 별도 함수인 이유: 지도는 "외부 API 키가 필요 없는 keyless embed 만"이라는 * 추가 제약이 있다. 허용 형태: * - Google Maps 공유 embed: https://www.google.com/maps/embed?pb=… * - Google Maps keyless 쿼리 embed: https://maps.google.com/maps?q=…&output=embed * - OpenStreetMap export embed: https://www.openstreetmap.org/export/embed.html?… * `/maps/embed/v1/*`(Embed API — 키 필요) 와 그 밖의 host/경로는 거부 — * 거부 시 렌더러는 기존 facade 이미지/길찾기 링크 폴백을 유지한다(fail-soft). */ declare function isAllowedMapEmbedSrc(value: unknown): value is string; interface FaqEntry { question: string; answer: string; } /** doc 을 순회하며 faqItem(질문/답변)을 순서대로 수집. */ declare function extractFaqEntries(doc: unknown): FaqEntry[]; /** FAQPage JSON-LD 객체 (script 삽입은 렌더러 책임). null = 항목 없음. */ declare function faqPageJsonLd(entries: FaqEntry[]): Record | null; /** * 본문 이미지 대체텍스트의 제품 안전 상한. * * HTML 표준의 길이 제한이 아니라, 본문·캡션 전체가 alt 에 잘못 저장돼 * 스크린리더와 검색 결과를 오염시키는 사고를 막기 위한 RootTale 계약이다. */ declare const IMAGE_ALT_MAX_LENGTH = 160; /** * 이미지 alt 를 한 줄 설명으로 정규화한다. * * - 문자열이 아니면 빈 alt * - 개행·연속 공백을 한 칸으로 축약 * - 160자를 넘으면 말줄임표를 포함해 상한 안에서 자름 */ declare function normalizeImageAlt(value: unknown): string; /** ChatGPT HTML의 style 블록에서 실행·외부 로드 위험만 제거한다. */ declare function sanitizeImportedCss(css: string, allowedAssetUrls?: readonly string[]): string; /** class·id·안전한 inline style을 보존하는 원문 디자인 전용 HTML 경계. */ declare function sanitizeImportedHtml(html: string, allowedAssetUrls?: readonly string[]): string; /** 구 버전 저장값도 현재의 html/body 가상 루트 계약으로 올린다. */ declare function ensureImportedHtmlDocumentRoots(html: string, allowedAssetUrls?: readonly string[]): string; /** 저장된 안전 CSS가 같은 importedHtml 블록 밖으로 새지 않도록 범위를 고정한다. */ declare function scopeImportedCss(css: string, scope: string, allowedAssetUrls?: readonly string[]): string; declare const INTERNAL_CONTENT_LINK_TOKEN_SOURCE: string; interface InternalContentLinkToken { /** 토큰이 시작하는 문자 인덱스(텍스트 기준). */ readonly index: number; /** 원문 토큰 전체(`[[internal:…|…]]`). */ readonly raw: string; /** 소문자로 정규화한 키. */ readonly key: string; /** 표시 문구(앞뒤 공백 제거). */ readonly label: string; } /** 텍스트 안의 토큰을 앞에서부터 모두 찾는다. 겹치지 않는다. */ declare function findInternalContentLinkTokens(text: string): InternalContentLinkToken[]; /** 공개 경로(`/faq/a/b/slug`) → 키(`faq.a.b.slug`). 조각이 없으면 null. */ declare function internalContentKeyFromPath(path: string | null | undefined): string | null; /** 키 → 공개 경로. 없으면 null(문구만 표시). */ type InternalContentLinkResolver = (key: string) => string | null; interface InternalContentPathEntry { /** 저장된 정규 공개 경로(`/faq/a/b/slug`). 없으면 대상이 아니다. */ readonly path?: string | null; /** 옛 slug 목록 — 옛 키로 남은 본문도 현재 주소로 이어 준다. */ readonly previousSlugs?: readonly string[] | null; } /** * 발행 글 목록에서 "키 → 현재 경로" 색인을 만든다. 옛 slug 도 같은 글로 등록한다 * (경로 마지막 조각을 옛 slug 로 바꾼 키). 현재 경로가 옛 키와 겹치면 현재가 이긴다. */ declare function internalContentPathIndex(entries: Iterable): Map; /** Map 이나 함수 어느 쪽이든 resolver 로. */ declare function internalContentLinkResolverFrom(source: InternalContentLinkResolver | ReadonlyMap | null | undefined): InternalContentLinkResolver; /** 링크로 렌더된 토큰의 class — 사이트 CSS 가 잡을 고리. */ declare const INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link"; /** 대상이 아직 없어 문구만 남긴 토큰의 class·data 속성. */ declare const INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending"; declare const INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending"; /** * 이미 이스케이프된 텍스트 한 구간의 토큰을 HTML 로 바꾼다. 텍스트 부분은 다시 * 이스케이프하지 않는다(정화기가 이미 처리한 값). 토큰이 없으면 원문 그대로. */ declare function renderInternalContentLinksInText(text: string, resolve: InternalContentLinkResolver): { readonly html: string; readonly hasTokens: boolean; }; /** * 정화된 HTML(원문 보존 블록 등)의 텍스트 구간에만 토큰을 적용한다. ``·``· * `
` 안과 태그 속성값은 건드리지 않는다.
 */
declare function renderInternalContentLinksInHtml(html: string, resolve: InternalContentLinkResolver): {
    readonly html: string;
    readonly hasTokens: boolean;
};

interface PreviewTokenPayload {
    postId: string;
    expiresAt: Date;
}
/** 새 preview token 생성. DB insert 책임은 caller. */
declare function issuePreviewToken(secret: string, payload: PreviewTokenPayload): string;
/** Token 검증 (서명 + 만료). DB row consumed_at update 는 caller (atomic). */
declare function verifyPreviewToken(secret: string, token: string): PreviewTokenPayload | null;

/**
 * XSS sanitize — Tiptap HTML output 또는 rawHtml 의 안전 정리 (렌더 직전 적용).
 * Phase 1 acceptance: `` 등 인라인 벡터가 inert.
 */
declare function sanitizeHtml(html: string): string;
/** 공개 렌더링 원장인 HTML에서 검사·검색용 텍스트를 서버에서도 동일하게 파생한다. */
declare function htmlToPlainText(html: string): string;
/**
 * 표준 CSP 헤더 값 (Phase 1 acceptance test #7 정합).
 * Astro middleware / Next.js header config 에서 사용.
 */
declare const PHASE_1_CSP: string;

/**
 * Block 에 stable id 주입.
 * 이미 `_id` 있는 block 은 그대로, 없으면 uuidv7 생성. recursive (innerBlocks).
 */
declare function ensureStableIds(block: Omit & {
    _id?: string;
}): Block;
/**
 * 한 번에 block tree 전체에 stable id 주입.
 * import 또는 새 post 생성 시 entrypoint.
 */
declare function injectStableIdsTree(blocks: readonly (Omit & {
    _id?: string;
})[]): Block[];

/**
 * 공통 블록(Site Patterns) — 여러 글에 같은 자리로 붙는 재사용 블록의 순수 규칙.
 *
 * 블록 본문은 ROOT-ADMIN 이 `site_patterns` 에 저장하고, "어느 자리(slot)에 어느
 * 콘텐츠 모델(`model_key`)의 글이면 어떤 블록"인지는 배치 규칙
 * (`settings.site_pattern_placements`)으로 정한다. 유형의 단위는 콘텐츠 모델이다 —
 * 컬렉션 설정(ADR-0060)은 ADR-0105 로 폐기 중이라 규칙 키로 쓰지 않는다. 공개 API 는 글마다 `pattern_slots`(자리 → 블록 key | null) 를 계산해
 * 내려주므로 FRONT 는 규칙을 몰라도 된다. 이 파일은 api-core(계산)·tenant-admin(편집)·
 * cms-renderer-next(렌더)가 같은 판정을 공유하기 위한 프레임워크 비의존 모듈이다.
 *
 * 규칙 판정:
 * - 자리는 `SITE_PATTERN_SLOTS` 에 선언된 것만 계산한다(미선언 자리는 무시).
 * - 글의 `modelKey` 와 같은 규칙이 있으면 그 규칙(블록 없음 = null 도 명시적 결정).
 * - 없으면 `modelKey: null`(모든 글 기본) 규칙.
 * - 가리키는 블록이 발행 상태가 아니면 null(FRONT 가 미발행 블록을 그리지 않게).
 */
/** 블록을 놓을 수 있는 자리. key 는 공개 API `pattern_slots` 의 키와 같다. */
declare const SITE_PATTERN_SLOTS: readonly [{
    readonly key: "post_footer";
    readonly label: "글 하단";
    readonly description: "글 본문 바로 아래";
}];
type SitePatternSlotKey = (typeof SITE_PATTERN_SLOTS)[number]["key"];
declare const POST_FOOTER_SLOT: SitePatternSlotKey;
/** 배치 규칙을 담는 `settings` 키. */
declare const SITE_PATTERN_PLACEMENTS_KEY = "site_pattern_placements";
/** 블록 key 형식 — 소문자·숫자·`-`·`_`, 64자 이하, 영숫자로 시작. */
declare const SITE_PATTERN_KEY_PATTERN: RegExp;
declare function isValidSitePatternKey(key: string): boolean;
declare function isSitePatternSlotKey(value: string): value is SitePatternSlotKey;
/**
 * 배치 규칙 1건. `modelKey: null` = 모든 글의 기본값, 문자열 = 그 콘텐츠 모델의 글만.
 * `patternKey: null` = 이 자리에 블록을 놓지 않는다(유형 규칙이면 기본값 덮어쓰기).
 */
interface SitePatternPlacementRule {
    readonly slot: SitePatternSlotKey;
    readonly modelKey: string | null;
    readonly patternKey: string | null;
}
interface SitePatternPlacements {
    readonly rules: readonly SitePatternPlacementRule[];
}
declare const EMPTY_SITE_PATTERN_PLACEMENTS: SitePatternPlacements;
/** 글 1건의 자리별 블록 key. 선언된 모든 자리를 키로 가진다(없으면 null). */
type PatternSlots = Record;
/**
 * 저장된 값(unknown) → 정규화된 배치 규칙. 형식이 틀린 항목·미선언 자리는 버리고,
 * 같은 (slot, modelKey) 는 마지막 항목이 이긴다. 이중 인코딩된 JSON 문자열도 받는다.
 * 2026-08-19 이전에 저장된 규칙은 `collectionKey` 필드를 썼다 — `modelKey` 가 없으면 그
 * 값을 읽는다(실측상 남은 규칙은 기본값 `null` 뿐이라 의미가 같다). 다시 저장하면
 * `modelKey` 로 쓰인다.
 */
declare function readSitePatternPlacements(raw: unknown): SitePatternPlacements;
/** (slot, modelKey) 에 해당하는 규칙. 없으면 undefined(= 기본값으로 위임). */
declare function findSitePatternPlacementRule(placements: SitePatternPlacements, slot: SitePatternSlotKey, modelKey: string | null): SitePatternPlacementRule | undefined;
/**
 * 규칙 1건을 갈아 끼운 새 배치 규칙을 돌려준다(불변). `patternKey: undefined` 는
 * "규칙 삭제 = 기본값으로 위임"이다 — 유형 규칙에서 "표시 안 함"(null)과 구분된다.
 */
declare function setSitePatternPlacementRule(placements: SitePatternPlacements, target: {
    slot: SitePatternSlotKey;
    modelKey: string | null;
}, patternKey: string | null | undefined): SitePatternPlacements;
/** 선언된 자리마다 null 인 빈 결과 — 규칙이나 발행 블록이 없을 때의 기본 응답. */
declare function emptyPatternSlots(): PatternSlots;
/**
 * 글 1건의 자리별 블록 key 를 계산한다. 글의 콘텐츠 모델(`modelKey`) 규칙 > 모든 글 기본
 * 규칙. `publishedKeys` 에 없는 블록(미발행·삭제)은 null.
 */
declare function resolvePatternSlots(post: {
    readonly modelKey: string | null;
}, placements: SitePatternPlacements, publishedKeys: ReadonlySet): PatternSlots;
/**
 * 규칙에서 참조하는 블록 key 집합 — 어드민이 "이 블록은 어디에 쓰이는가"를 표시하거나
 * 삭제 전 경고할 때 쓴다.
 */
declare function referencedSitePatternKeys(placements: SitePatternPlacements): ReadonlySet;
type SitePatternLayout = "plain" | "card";
type SitePatternLinkStyle = "text" | "buttons";
interface SitePatternPresentation {
    /** `plain` = 본문처럼 흐름에 놓임, `card` = 배경색이 있는 카드. */
    readonly layout: SitePatternLayout;
    /** 카드 배경색(hex). layout 이 card 일 때만 의미. */
    readonly background: string;
    /** 마지막 문단의 링크를 버튼으로 그릴지. */
    readonly linkStyle: SitePatternLinkStyle;
    /** 버튼 배경색(hex, 순서대로 1·2·3…; 링크가 더 많으면 마지막 색을 반복). 글자는 흰색. */
    readonly buttonColors: readonly string[];
}
declare const DEFAULT_SITE_PATTERN_PRESENTATION: SitePatternPresentation;
declare const SITE_PATTERN_BUTTON_COLORS_MAX = 4;
declare function normalizeHexColor(value: unknown): string | null;
/** 저장된 값(unknown) → 표시 형태. 틀린 값은 기본값으로 채운다(절대 throw 하지 않음). */
declare function readSitePatternPresentation(raw: unknown): SitePatternPresentation;
/** 버튼 n번째(0부터)의 색 — 링크가 색 개수보다 많으면 마지막 색을 반복. */
declare function sitePatternButtonColor(presentation: SitePatternPresentation, index: number): string;
/**
 * FRONT 가 래퍼 요소에 얹을 data 속성과 CSS 변수. 사이트 CSS 는 이 변수만 읽는다 —
 * `data-pattern-layout`·`data-pattern-link-style`, `--rt-pattern-bg`, `--rt-pattern-btn-1..n`.
 */
declare function sitePatternPresentationAttributes(presentation: SitePatternPresentation): {
    readonly dataset: Readonly>;
    readonly cssVariables: Readonly>;
};

/** 편집기와 공개 렌더러가 공유하는 허용 서식. 기본값은 사이트 CSS를 따른다. */
declare const CMS_FONT_SIZES: readonly [14, 16, 18, 20, 24, 28, 32];
declare const CMS_LINE_HEIGHTS: readonly [1.2, 1.5, 1.8, 2, 2.4];
declare const CMS_PARAGRAPH_SPACINGS: readonly [0, 8, 16, 24, 32];
declare const CMS_IMAGE_WIDTHS: readonly [25, 50, 75, 100];
declare function cmsFontSize(value: unknown): string | null;
declare function cmsLineHeight(value: unknown): number | null;
declare function cmsParagraphSpacing(value: unknown): string | null;
declare function cmsImageWidth(value: unknown): string | null;
declare function cmsImageAlign(value: unknown): "left" | "center" | "right" | null;
declare function cmsImageStyle(attrs: Record): {
    display?: string | undefined;
    marginLeft?: string | undefined;
    marginRight?: string | undefined;
    width?: string | undefined;
    maxWidth?: string | undefined;
    height?: string | undefined;
};

interface SectionLock {
    move?: boolean;
    remove?: boolean;
    structure?: boolean;
}
type SectionLockViolationCode = "locked_section_inserted" | "locked_section_removed" | "locked_section_moved" | "locked_section_lock_changed" | "locked_section_structure_changed";
interface SectionLockViolation {
    code: SectionLockViolationCode;
    sectionId: string;
    sectionType: string;
    field: string;
}
declare function isSectionContentPropPath(sectionType: string, path: readonly (string | number)[]): boolean;
/** 고객 콘텐츠 저장 경계. 기존 lock 플래그와 무관하게 제작 구조를 보호한다. */
declare function findSectionLockViolations(beforeDoc: unknown, afterDoc: unknown): SectionLockViolation[];
declare function formatSectionLockViolations(violations: readonly SectionLockViolation[]): string;

export { BLOCK_NAME_PATTERN, type BindingResolver, type Block, type BlockBinding, type BlockDefinition, type BlockRegistry, CMS_FONT_SIZES, CMS_IMAGE_WIDTHS, CMS_LINE_HEIGHTS, CMS_PARAGRAPH_SPACINGS, type CategoryTreeEntry, type CategoryTreePresentation, type CategoryTreeTerm, type ClientHydrationSpec, DEFAULT_SITE_PATTERN_PRESENTATION, EMPTY_SITE_PATTERN_PLACEMENTS, type EmbedProvider, FIELD_BINDING_SOURCE, FIELD_BINDING_SOURCE_ALIAS, type FaqEntry, IMAGE_ALT_MAX_LENGTH, INTERNAL_CONTENT_LINK_CLASS, INTERNAL_CONTENT_LINK_PENDING_ATTR, INTERNAL_CONTENT_LINK_PENDING_CLASS, INTERNAL_CONTENT_LINK_TOKEN_SOURCE, type InternalContentLinkResolver, type InternalContentLinkToken, LEGACY_BLOG_DETAIL_PATH, type NormalizedEmbed, PHASE_1_CORE_BLOCKS, PHASE_1_CSP, POST_FOOTER_SLOT, type PatternSlots, type Phase1CoreBlockName, type PreviewTokenPayload, type PublicPathContext, type PublicPathPost, type PublicPathPresentation, type RoutablePost, type RouteCollection, SITE_PATTERN_BUTTON_COLORS_MAX, SITE_PATTERN_KEY_PATTERN, SITE_PATTERN_PLACEMENTS_KEY, SITE_PATTERN_SLOTS, type SectionLock, type SectionLockViolation, type SectionLockViolationCode, type SitePatternLayout, type SitePatternLinkStyle, type SitePatternPlacementRule, type SitePatternPlacements, type SitePatternPresentation, type SitePatternSlotKey, applyBlockBindings, bindingSrcValue, bindingTextValue, blockSchema, boundFieldValue, chainResolvers, cmsFontSize, cmsImageAlign, cmsImageStyle, cmsImageWidth, cmsLineHeight, cmsParagraphSpacing, createFieldBindingResolver, emptyPatternSlots, ensureImportedHtmlDocumentRoots, ensureStableIds, extractFaqEntries, faqPageJsonLd, findInternalContentLinkTokens, findSectionLockViolations, findSitePatternPlacementRule, formatSectionLockViolations, htmlToPlainText, injectStableIdsTree, internalContentKeyFromPath, internalContentLinkResolverFrom, internalContentPathIndex, isAllowedEmbedSrc, isAllowedMapEmbedSrc, isInCollectionMode, isSectionContentPropPath, isSitePatternSlotKey, isValidBlock, isValidSitePatternKey, issuePreviewToken, normalizeEmbed, normalizeHexColor, normalizeImageAlt, parseBlock, postsForCollection, postsInAnyCollection, postsNotInAnyCollection, readSitePatternPlacements, readSitePatternPresentation, referencedSitePatternKeys, renderInternalContentLinksInHtml, renderInternalContentLinksInText, resolveCategoryPath, resolveCategoryTreeChain, resolveCategoryTreeEntryPath, resolveCategoryTreeHubPaths, resolvePatternSlots, resolvePostCategorySlug, resolvePostCollection, resolvePostPath, resolvePostPublicPath, resolvePostUrl, sanitizeHtml, sanitizeImportedCss, sanitizeImportedHtml, scopeImportedCss, serializeBlock, setSitePatternPlacementRule, sitePatternButtonColor, sitePatternPresentationAttributes, validateCollections, verifyPreviewToken };