@roottale/cms-core 0.56.0 → 0.57.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 +27 -0
- package/dist/index.cjs +283 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -1
- package/dist/index.d.ts +126 -1
- package/dist/index.js +260 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -382,6 +382,58 @@ declare function ensureImportedHtmlDocumentRoots(html: string, allowedAssetUrls?
|
|
|
382
382
|
/** 저장된 안전 CSS가 같은 importedHtml 블록 밖으로 새지 않도록 범위를 고정한다. */
|
|
383
383
|
declare function scopeImportedCss(css: string, scope: string, allowedAssetUrls?: readonly string[]): string;
|
|
384
384
|
|
|
385
|
+
declare const INTERNAL_CONTENT_LINK_TOKEN_SOURCE: string;
|
|
386
|
+
interface InternalContentLinkToken {
|
|
387
|
+
/** 토큰이 시작하는 문자 인덱스(텍스트 기준). */
|
|
388
|
+
readonly index: number;
|
|
389
|
+
/** 원문 토큰 전체(`[[internal:…|…]]`). */
|
|
390
|
+
readonly raw: string;
|
|
391
|
+
/** 소문자로 정규화한 키. */
|
|
392
|
+
readonly key: string;
|
|
393
|
+
/** 표시 문구(앞뒤 공백 제거). */
|
|
394
|
+
readonly label: string;
|
|
395
|
+
}
|
|
396
|
+
/** 텍스트 안의 토큰을 앞에서부터 모두 찾는다. 겹치지 않는다. */
|
|
397
|
+
declare function findInternalContentLinkTokens(text: string): InternalContentLinkToken[];
|
|
398
|
+
/** 공개 경로(`/faq/a/b/slug`) → 키(`faq.a.b.slug`). 조각이 없으면 null. */
|
|
399
|
+
declare function internalContentKeyFromPath(path: string | null | undefined): string | null;
|
|
400
|
+
/** 키 → 공개 경로. 없으면 null(문구만 표시). */
|
|
401
|
+
type InternalContentLinkResolver = (key: string) => string | null;
|
|
402
|
+
interface InternalContentPathEntry {
|
|
403
|
+
/** 저장된 정규 공개 경로(`/faq/a/b/slug`). 없으면 대상이 아니다. */
|
|
404
|
+
readonly path?: string | null;
|
|
405
|
+
/** 옛 slug 목록 — 옛 키로 남은 본문도 현재 주소로 이어 준다. */
|
|
406
|
+
readonly previousSlugs?: readonly string[] | null;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* 발행 글 목록에서 "키 → 현재 경로" 색인을 만든다. 옛 slug 도 같은 글로 등록한다
|
|
410
|
+
* (경로 마지막 조각을 옛 slug 로 바꾼 키). 현재 경로가 옛 키와 겹치면 현재가 이긴다.
|
|
411
|
+
*/
|
|
412
|
+
declare function internalContentPathIndex(entries: Iterable<InternalContentPathEntry>): Map<string, string>;
|
|
413
|
+
/** Map 이나 함수 어느 쪽이든 resolver 로. */
|
|
414
|
+
declare function internalContentLinkResolverFrom(source: InternalContentLinkResolver | ReadonlyMap<string, string> | null | undefined): InternalContentLinkResolver;
|
|
415
|
+
/** 링크로 렌더된 토큰의 class — 사이트 CSS 가 잡을 고리. */
|
|
416
|
+
declare const INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
417
|
+
/** 대상이 아직 없어 문구만 남긴 토큰의 class·data 속성. */
|
|
418
|
+
declare const INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
419
|
+
declare const INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
420
|
+
/**
|
|
421
|
+
* 이미 이스케이프된 텍스트 한 구간의 토큰을 HTML 로 바꾼다. 텍스트 부분은 다시
|
|
422
|
+
* 이스케이프하지 않는다(정화기가 이미 처리한 값). 토큰이 없으면 원문 그대로.
|
|
423
|
+
*/
|
|
424
|
+
declare function renderInternalContentLinksInText(text: string, resolve: InternalContentLinkResolver): {
|
|
425
|
+
readonly html: string;
|
|
426
|
+
readonly hasTokens: boolean;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* 정화된 HTML(원문 보존 블록 등)의 텍스트 구간에만 토큰을 적용한다. `<a>`·`<code>`·
|
|
430
|
+
* `<pre>` 안과 태그 속성값은 건드리지 않는다.
|
|
431
|
+
*/
|
|
432
|
+
declare function renderInternalContentLinksInHtml(html: string, resolve: InternalContentLinkResolver): {
|
|
433
|
+
readonly html: string;
|
|
434
|
+
readonly hasTokens: boolean;
|
|
435
|
+
};
|
|
436
|
+
|
|
385
437
|
interface PreviewTokenPayload {
|
|
386
438
|
postId: string;
|
|
387
439
|
expiresAt: Date;
|
|
@@ -419,4 +471,77 @@ declare function injectStableIdsTree(blocks: readonly (Omit<Block, "_id"> & {
|
|
|
419
471
|
_id?: string;
|
|
420
472
|
})[]): Block[];
|
|
421
473
|
|
|
422
|
-
|
|
474
|
+
/**
|
|
475
|
+
* 공통 블록(Site Patterns) — 여러 글에 같은 자리로 붙는 재사용 블록의 순수 규칙.
|
|
476
|
+
*
|
|
477
|
+
* 블록 본문은 ROOT-ADMIN 이 `site_patterns` 에 저장하고, "어느 자리(slot)에 어느
|
|
478
|
+
* 유형(collection)의 글이면 어떤 블록"인지는 배치 규칙(`settings.site_pattern_placements`)
|
|
479
|
+
* 으로 정한다. 공개 API 는 글마다 `pattern_slots`(자리 → 블록 key | null) 를 계산해
|
|
480
|
+
* 내려주므로 FRONT 는 규칙을 몰라도 된다. 이 파일은 api-core(계산)·tenant-admin(편집)·
|
|
481
|
+
* cms-renderer-next(렌더)가 같은 판정을 공유하기 위한 프레임워크 비의존 모듈이다.
|
|
482
|
+
*
|
|
483
|
+
* 규칙 판정:
|
|
484
|
+
* - 자리는 `SITE_PATTERN_SLOTS` 에 선언된 것만 계산한다(미선언 자리는 무시).
|
|
485
|
+
* - 글의 `collectionKey` 와 같은 규칙이 있으면 그 규칙(블록 없음 = null 도 명시적 결정).
|
|
486
|
+
* - 없으면 `collectionKey: null`(모든 글 기본) 규칙.
|
|
487
|
+
* - 가리키는 블록이 발행 상태가 아니면 null(FRONT 가 미발행 블록을 그리지 않게).
|
|
488
|
+
*/
|
|
489
|
+
/** 블록을 놓을 수 있는 자리. key 는 공개 API `pattern_slots` 의 키와 같다. */
|
|
490
|
+
declare const SITE_PATTERN_SLOTS: readonly [{
|
|
491
|
+
readonly key: "post_footer";
|
|
492
|
+
readonly label: "글 하단";
|
|
493
|
+
readonly description: "글 본문 바로 아래";
|
|
494
|
+
}];
|
|
495
|
+
type SitePatternSlotKey = (typeof SITE_PATTERN_SLOTS)[number]["key"];
|
|
496
|
+
declare const POST_FOOTER_SLOT: SitePatternSlotKey;
|
|
497
|
+
/** 배치 규칙을 담는 `settings` 키. */
|
|
498
|
+
declare const SITE_PATTERN_PLACEMENTS_KEY = "site_pattern_placements";
|
|
499
|
+
/** 블록 key 형식 — 소문자·숫자·`-`·`_`, 64자 이하, 영숫자로 시작. */
|
|
500
|
+
declare const SITE_PATTERN_KEY_PATTERN: RegExp;
|
|
501
|
+
declare function isValidSitePatternKey(key: string): boolean;
|
|
502
|
+
declare function isSitePatternSlotKey(value: string): value is SitePatternSlotKey;
|
|
503
|
+
/**
|
|
504
|
+
* 배치 규칙 1건. `collectionKey: null` = 모든 글의 기본값, 문자열 = 그 유형의 글만.
|
|
505
|
+
* `patternKey: null` = 이 자리에 블록을 놓지 않는다(유형 규칙이면 기본값 덮어쓰기).
|
|
506
|
+
*/
|
|
507
|
+
interface SitePatternPlacementRule {
|
|
508
|
+
readonly slot: SitePatternSlotKey;
|
|
509
|
+
readonly collectionKey: string | null;
|
|
510
|
+
readonly patternKey: string | null;
|
|
511
|
+
}
|
|
512
|
+
interface SitePatternPlacements {
|
|
513
|
+
readonly rules: readonly SitePatternPlacementRule[];
|
|
514
|
+
}
|
|
515
|
+
declare const EMPTY_SITE_PATTERN_PLACEMENTS: SitePatternPlacements;
|
|
516
|
+
/** 글 1건의 자리별 블록 key. 선언된 모든 자리를 키로 가진다(없으면 null). */
|
|
517
|
+
type PatternSlots = Record<string, string | null>;
|
|
518
|
+
/**
|
|
519
|
+
* 저장된 값(unknown) → 정규화된 배치 규칙. 형식이 틀린 항목·미선언 자리는 버리고,
|
|
520
|
+
* 같은 (slot, collectionKey) 는 마지막 항목이 이긴다. 이중 인코딩된 JSON 문자열도 받는다.
|
|
521
|
+
*/
|
|
522
|
+
declare function readSitePatternPlacements(raw: unknown): SitePatternPlacements;
|
|
523
|
+
/** (slot, collectionKey) 에 해당하는 규칙. 없으면 undefined(= 기본값으로 위임). */
|
|
524
|
+
declare function findSitePatternPlacementRule(placements: SitePatternPlacements, slot: SitePatternSlotKey, collectionKey: string | null): SitePatternPlacementRule | undefined;
|
|
525
|
+
/**
|
|
526
|
+
* 규칙 1건을 갈아 끼운 새 배치 규칙을 돌려준다(불변). `patternKey: undefined` 는
|
|
527
|
+
* "규칙 삭제 = 기본값으로 위임"이다 — 유형 규칙에서 "표시 안 함"(null)과 구분된다.
|
|
528
|
+
*/
|
|
529
|
+
declare function setSitePatternPlacementRule(placements: SitePatternPlacements, target: {
|
|
530
|
+
slot: SitePatternSlotKey;
|
|
531
|
+
collectionKey: string | null;
|
|
532
|
+
}, patternKey: string | null | undefined): SitePatternPlacements;
|
|
533
|
+
/** 선언된 자리마다 null 인 빈 결과 — 규칙이나 발행 블록이 없을 때의 기본 응답. */
|
|
534
|
+
declare function emptyPatternSlots(): PatternSlots;
|
|
535
|
+
/**
|
|
536
|
+
* 글 1건의 자리별 블록 key 를 계산한다. `publishedKeys` 에 없는 블록(미발행·삭제)은 null.
|
|
537
|
+
*/
|
|
538
|
+
declare function resolvePatternSlots(post: {
|
|
539
|
+
readonly collectionKey: string | null;
|
|
540
|
+
}, placements: SitePatternPlacements, publishedKeys: ReadonlySet<string>): PatternSlots;
|
|
541
|
+
/**
|
|
542
|
+
* 규칙에서 참조하는 블록 key 집합 — 어드민이 "이 블록은 어디에 쓰이는가"를 표시하거나
|
|
543
|
+
* 삭제 전 경고할 때 쓴다.
|
|
544
|
+
*/
|
|
545
|
+
declare function referencedSitePatternKeys(placements: SitePatternPlacements): ReadonlySet<string>;
|
|
546
|
+
|
|
547
|
+
export { BLOCK_NAME_PATTERN, type BindingResolver, type Block, type BlockBinding, type BlockDefinition, type BlockRegistry, type CategoryTreeEntry, type CategoryTreePresentation, type CategoryTreeTerm, type ClientHydrationSpec, 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_KEY_PATTERN, SITE_PATTERN_PLACEMENTS_KEY, SITE_PATTERN_SLOTS, type SitePatternPlacementRule, type SitePatternPlacements, type SitePatternSlotKey, applyBlockBindings, bindingSrcValue, bindingTextValue, blockSchema, boundFieldValue, chainResolvers, createFieldBindingResolver, emptyPatternSlots, ensureImportedHtmlDocumentRoots, ensureStableIds, extractFaqEntries, faqPageJsonLd, findInternalContentLinkTokens, findSitePatternPlacementRule, htmlToPlainText, injectStableIdsTree, internalContentKeyFromPath, internalContentLinkResolverFrom, internalContentPathIndex, isAllowedEmbedSrc, isAllowedMapEmbedSrc, isInCollectionMode, isSitePatternSlotKey, isValidBlock, isValidSitePatternKey, issuePreviewToken, normalizeEmbed, normalizeImageAlt, parseBlock, postsForCollection, postsInAnyCollection, postsNotInAnyCollection, readSitePatternPlacements, referencedSitePatternKeys, renderInternalContentLinksInHtml, renderInternalContentLinksInText, resolveCategoryPath, resolveCategoryTreeChain, resolveCategoryTreeEntryPath, resolveCategoryTreeHubPaths, resolvePatternSlots, resolvePostCategorySlug, resolvePostCollection, resolvePostPath, resolvePostPublicPath, resolvePostUrl, sanitizeHtml, sanitizeImportedCss, sanitizeImportedHtml, scopeImportedCss, serializeBlock, setSitePatternPlacementRule, validateCollections, verifyPreviewToken };
|
package/dist/index.d.ts
CHANGED
|
@@ -382,6 +382,58 @@ declare function ensureImportedHtmlDocumentRoots(html: string, allowedAssetUrls?
|
|
|
382
382
|
/** 저장된 안전 CSS가 같은 importedHtml 블록 밖으로 새지 않도록 범위를 고정한다. */
|
|
383
383
|
declare function scopeImportedCss(css: string, scope: string, allowedAssetUrls?: readonly string[]): string;
|
|
384
384
|
|
|
385
|
+
declare const INTERNAL_CONTENT_LINK_TOKEN_SOURCE: string;
|
|
386
|
+
interface InternalContentLinkToken {
|
|
387
|
+
/** 토큰이 시작하는 문자 인덱스(텍스트 기준). */
|
|
388
|
+
readonly index: number;
|
|
389
|
+
/** 원문 토큰 전체(`[[internal:…|…]]`). */
|
|
390
|
+
readonly raw: string;
|
|
391
|
+
/** 소문자로 정규화한 키. */
|
|
392
|
+
readonly key: string;
|
|
393
|
+
/** 표시 문구(앞뒤 공백 제거). */
|
|
394
|
+
readonly label: string;
|
|
395
|
+
}
|
|
396
|
+
/** 텍스트 안의 토큰을 앞에서부터 모두 찾는다. 겹치지 않는다. */
|
|
397
|
+
declare function findInternalContentLinkTokens(text: string): InternalContentLinkToken[];
|
|
398
|
+
/** 공개 경로(`/faq/a/b/slug`) → 키(`faq.a.b.slug`). 조각이 없으면 null. */
|
|
399
|
+
declare function internalContentKeyFromPath(path: string | null | undefined): string | null;
|
|
400
|
+
/** 키 → 공개 경로. 없으면 null(문구만 표시). */
|
|
401
|
+
type InternalContentLinkResolver = (key: string) => string | null;
|
|
402
|
+
interface InternalContentPathEntry {
|
|
403
|
+
/** 저장된 정규 공개 경로(`/faq/a/b/slug`). 없으면 대상이 아니다. */
|
|
404
|
+
readonly path?: string | null;
|
|
405
|
+
/** 옛 slug 목록 — 옛 키로 남은 본문도 현재 주소로 이어 준다. */
|
|
406
|
+
readonly previousSlugs?: readonly string[] | null;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* 발행 글 목록에서 "키 → 현재 경로" 색인을 만든다. 옛 slug 도 같은 글로 등록한다
|
|
410
|
+
* (경로 마지막 조각을 옛 slug 로 바꾼 키). 현재 경로가 옛 키와 겹치면 현재가 이긴다.
|
|
411
|
+
*/
|
|
412
|
+
declare function internalContentPathIndex(entries: Iterable<InternalContentPathEntry>): Map<string, string>;
|
|
413
|
+
/** Map 이나 함수 어느 쪽이든 resolver 로. */
|
|
414
|
+
declare function internalContentLinkResolverFrom(source: InternalContentLinkResolver | ReadonlyMap<string, string> | null | undefined): InternalContentLinkResolver;
|
|
415
|
+
/** 링크로 렌더된 토큰의 class — 사이트 CSS 가 잡을 고리. */
|
|
416
|
+
declare const INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
417
|
+
/** 대상이 아직 없어 문구만 남긴 토큰의 class·data 속성. */
|
|
418
|
+
declare const INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
419
|
+
declare const INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
420
|
+
/**
|
|
421
|
+
* 이미 이스케이프된 텍스트 한 구간의 토큰을 HTML 로 바꾼다. 텍스트 부분은 다시
|
|
422
|
+
* 이스케이프하지 않는다(정화기가 이미 처리한 값). 토큰이 없으면 원문 그대로.
|
|
423
|
+
*/
|
|
424
|
+
declare function renderInternalContentLinksInText(text: string, resolve: InternalContentLinkResolver): {
|
|
425
|
+
readonly html: string;
|
|
426
|
+
readonly hasTokens: boolean;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* 정화된 HTML(원문 보존 블록 등)의 텍스트 구간에만 토큰을 적용한다. `<a>`·`<code>`·
|
|
430
|
+
* `<pre>` 안과 태그 속성값은 건드리지 않는다.
|
|
431
|
+
*/
|
|
432
|
+
declare function renderInternalContentLinksInHtml(html: string, resolve: InternalContentLinkResolver): {
|
|
433
|
+
readonly html: string;
|
|
434
|
+
readonly hasTokens: boolean;
|
|
435
|
+
};
|
|
436
|
+
|
|
385
437
|
interface PreviewTokenPayload {
|
|
386
438
|
postId: string;
|
|
387
439
|
expiresAt: Date;
|
|
@@ -419,4 +471,77 @@ declare function injectStableIdsTree(blocks: readonly (Omit<Block, "_id"> & {
|
|
|
419
471
|
_id?: string;
|
|
420
472
|
})[]): Block[];
|
|
421
473
|
|
|
422
|
-
|
|
474
|
+
/**
|
|
475
|
+
* 공통 블록(Site Patterns) — 여러 글에 같은 자리로 붙는 재사용 블록의 순수 규칙.
|
|
476
|
+
*
|
|
477
|
+
* 블록 본문은 ROOT-ADMIN 이 `site_patterns` 에 저장하고, "어느 자리(slot)에 어느
|
|
478
|
+
* 유형(collection)의 글이면 어떤 블록"인지는 배치 규칙(`settings.site_pattern_placements`)
|
|
479
|
+
* 으로 정한다. 공개 API 는 글마다 `pattern_slots`(자리 → 블록 key | null) 를 계산해
|
|
480
|
+
* 내려주므로 FRONT 는 규칙을 몰라도 된다. 이 파일은 api-core(계산)·tenant-admin(편집)·
|
|
481
|
+
* cms-renderer-next(렌더)가 같은 판정을 공유하기 위한 프레임워크 비의존 모듈이다.
|
|
482
|
+
*
|
|
483
|
+
* 규칙 판정:
|
|
484
|
+
* - 자리는 `SITE_PATTERN_SLOTS` 에 선언된 것만 계산한다(미선언 자리는 무시).
|
|
485
|
+
* - 글의 `collectionKey` 와 같은 규칙이 있으면 그 규칙(블록 없음 = null 도 명시적 결정).
|
|
486
|
+
* - 없으면 `collectionKey: null`(모든 글 기본) 규칙.
|
|
487
|
+
* - 가리키는 블록이 발행 상태가 아니면 null(FRONT 가 미발행 블록을 그리지 않게).
|
|
488
|
+
*/
|
|
489
|
+
/** 블록을 놓을 수 있는 자리. key 는 공개 API `pattern_slots` 의 키와 같다. */
|
|
490
|
+
declare const SITE_PATTERN_SLOTS: readonly [{
|
|
491
|
+
readonly key: "post_footer";
|
|
492
|
+
readonly label: "글 하단";
|
|
493
|
+
readonly description: "글 본문 바로 아래";
|
|
494
|
+
}];
|
|
495
|
+
type SitePatternSlotKey = (typeof SITE_PATTERN_SLOTS)[number]["key"];
|
|
496
|
+
declare const POST_FOOTER_SLOT: SitePatternSlotKey;
|
|
497
|
+
/** 배치 규칙을 담는 `settings` 키. */
|
|
498
|
+
declare const SITE_PATTERN_PLACEMENTS_KEY = "site_pattern_placements";
|
|
499
|
+
/** 블록 key 형식 — 소문자·숫자·`-`·`_`, 64자 이하, 영숫자로 시작. */
|
|
500
|
+
declare const SITE_PATTERN_KEY_PATTERN: RegExp;
|
|
501
|
+
declare function isValidSitePatternKey(key: string): boolean;
|
|
502
|
+
declare function isSitePatternSlotKey(value: string): value is SitePatternSlotKey;
|
|
503
|
+
/**
|
|
504
|
+
* 배치 규칙 1건. `collectionKey: null` = 모든 글의 기본값, 문자열 = 그 유형의 글만.
|
|
505
|
+
* `patternKey: null` = 이 자리에 블록을 놓지 않는다(유형 규칙이면 기본값 덮어쓰기).
|
|
506
|
+
*/
|
|
507
|
+
interface SitePatternPlacementRule {
|
|
508
|
+
readonly slot: SitePatternSlotKey;
|
|
509
|
+
readonly collectionKey: string | null;
|
|
510
|
+
readonly patternKey: string | null;
|
|
511
|
+
}
|
|
512
|
+
interface SitePatternPlacements {
|
|
513
|
+
readonly rules: readonly SitePatternPlacementRule[];
|
|
514
|
+
}
|
|
515
|
+
declare const EMPTY_SITE_PATTERN_PLACEMENTS: SitePatternPlacements;
|
|
516
|
+
/** 글 1건의 자리별 블록 key. 선언된 모든 자리를 키로 가진다(없으면 null). */
|
|
517
|
+
type PatternSlots = Record<string, string | null>;
|
|
518
|
+
/**
|
|
519
|
+
* 저장된 값(unknown) → 정규화된 배치 규칙. 형식이 틀린 항목·미선언 자리는 버리고,
|
|
520
|
+
* 같은 (slot, collectionKey) 는 마지막 항목이 이긴다. 이중 인코딩된 JSON 문자열도 받는다.
|
|
521
|
+
*/
|
|
522
|
+
declare function readSitePatternPlacements(raw: unknown): SitePatternPlacements;
|
|
523
|
+
/** (slot, collectionKey) 에 해당하는 규칙. 없으면 undefined(= 기본값으로 위임). */
|
|
524
|
+
declare function findSitePatternPlacementRule(placements: SitePatternPlacements, slot: SitePatternSlotKey, collectionKey: string | null): SitePatternPlacementRule | undefined;
|
|
525
|
+
/**
|
|
526
|
+
* 규칙 1건을 갈아 끼운 새 배치 규칙을 돌려준다(불변). `patternKey: undefined` 는
|
|
527
|
+
* "규칙 삭제 = 기본값으로 위임"이다 — 유형 규칙에서 "표시 안 함"(null)과 구분된다.
|
|
528
|
+
*/
|
|
529
|
+
declare function setSitePatternPlacementRule(placements: SitePatternPlacements, target: {
|
|
530
|
+
slot: SitePatternSlotKey;
|
|
531
|
+
collectionKey: string | null;
|
|
532
|
+
}, patternKey: string | null | undefined): SitePatternPlacements;
|
|
533
|
+
/** 선언된 자리마다 null 인 빈 결과 — 규칙이나 발행 블록이 없을 때의 기본 응답. */
|
|
534
|
+
declare function emptyPatternSlots(): PatternSlots;
|
|
535
|
+
/**
|
|
536
|
+
* 글 1건의 자리별 블록 key 를 계산한다. `publishedKeys` 에 없는 블록(미발행·삭제)은 null.
|
|
537
|
+
*/
|
|
538
|
+
declare function resolvePatternSlots(post: {
|
|
539
|
+
readonly collectionKey: string | null;
|
|
540
|
+
}, placements: SitePatternPlacements, publishedKeys: ReadonlySet<string>): PatternSlots;
|
|
541
|
+
/**
|
|
542
|
+
* 규칙에서 참조하는 블록 key 집합 — 어드민이 "이 블록은 어디에 쓰이는가"를 표시하거나
|
|
543
|
+
* 삭제 전 경고할 때 쓴다.
|
|
544
|
+
*/
|
|
545
|
+
declare function referencedSitePatternKeys(placements: SitePatternPlacements): ReadonlySet<string>;
|
|
546
|
+
|
|
547
|
+
export { BLOCK_NAME_PATTERN, type BindingResolver, type Block, type BlockBinding, type BlockDefinition, type BlockRegistry, type CategoryTreeEntry, type CategoryTreePresentation, type CategoryTreeTerm, type ClientHydrationSpec, 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_KEY_PATTERN, SITE_PATTERN_PLACEMENTS_KEY, SITE_PATTERN_SLOTS, type SitePatternPlacementRule, type SitePatternPlacements, type SitePatternSlotKey, applyBlockBindings, bindingSrcValue, bindingTextValue, blockSchema, boundFieldValue, chainResolvers, createFieldBindingResolver, emptyPatternSlots, ensureImportedHtmlDocumentRoots, ensureStableIds, extractFaqEntries, faqPageJsonLd, findInternalContentLinkTokens, findSitePatternPlacementRule, htmlToPlainText, injectStableIdsTree, internalContentKeyFromPath, internalContentLinkResolverFrom, internalContentPathIndex, isAllowedEmbedSrc, isAllowedMapEmbedSrc, isInCollectionMode, isSitePatternSlotKey, isValidBlock, isValidSitePatternKey, issuePreviewToken, normalizeEmbed, normalizeImageAlt, parseBlock, postsForCollection, postsInAnyCollection, postsNotInAnyCollection, readSitePatternPlacements, referencedSitePatternKeys, renderInternalContentLinksInHtml, renderInternalContentLinksInText, resolveCategoryPath, resolveCategoryTreeChain, resolveCategoryTreeEntryPath, resolveCategoryTreeHubPaths, resolvePatternSlots, resolvePostCategorySlug, resolvePostCollection, resolvePostPath, resolvePostPublicPath, resolvePostUrl, sanitizeHtml, sanitizeImportedCss, sanitizeImportedHtml, scopeImportedCss, serializeBlock, setSitePatternPlacementRule, validateCollections, verifyPreviewToken };
|
package/dist/index.js
CHANGED
|
@@ -959,6 +959,152 @@ function scopeImportedCss(css, scope, allowedAssetUrls = []) {
|
|
|
959
959
|
}
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
// src/internal-content-links.ts
|
|
963
|
+
var INTERNAL_CONTENT_LINK_TOKEN_SOURCE = String.raw`\[\[internal:([a-z0-9가-힣]+(?:[.-][a-z0-9가-힣]+)*)\|([^\]\n|]+)\]\]`;
|
|
964
|
+
function findInternalContentLinkTokens(text) {
|
|
965
|
+
const pattern = new RegExp(INTERNAL_CONTENT_LINK_TOKEN_SOURCE, "giu");
|
|
966
|
+
const tokens = [];
|
|
967
|
+
for (const match of text.matchAll(pattern)) {
|
|
968
|
+
const [raw, key, label] = match;
|
|
969
|
+
if (!key || !label || match.index === void 0) continue;
|
|
970
|
+
tokens.push({
|
|
971
|
+
index: match.index,
|
|
972
|
+
raw,
|
|
973
|
+
key: key.toLowerCase(),
|
|
974
|
+
label: label.trim()
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
return tokens;
|
|
978
|
+
}
|
|
979
|
+
function internalContentKeyFromPath(path) {
|
|
980
|
+
if (!path) return null;
|
|
981
|
+
const segments = path.split("/").filter(Boolean).map((segment) => {
|
|
982
|
+
try {
|
|
983
|
+
return decodeURIComponent(segment).trim().toLowerCase();
|
|
984
|
+
} catch {
|
|
985
|
+
return "";
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
if (segments.length === 0 || segments.some((segment) => segment === "" || segment.includes("."))) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
return segments.join(".");
|
|
992
|
+
}
|
|
993
|
+
function internalContentPathIndex(entries) {
|
|
994
|
+
const index = /* @__PURE__ */ new Map();
|
|
995
|
+
const previous = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const entry of entries) {
|
|
997
|
+
const path = entry.path;
|
|
998
|
+
const key = internalContentKeyFromPath(path);
|
|
999
|
+
if (!path || !key) continue;
|
|
1000
|
+
index.set(key, path);
|
|
1001
|
+
const segments = key.split(".");
|
|
1002
|
+
for (const oldSlug of entry.previousSlugs ?? []) {
|
|
1003
|
+
const oldKey = [...segments.slice(0, -1), oldSlug.trim().toLowerCase()].join(".");
|
|
1004
|
+
if (oldKey && !oldKey.includes("..")) previous.set(oldKey, path);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
for (const [key, path] of previous) {
|
|
1008
|
+
if (!index.has(key)) index.set(key, path);
|
|
1009
|
+
}
|
|
1010
|
+
return index;
|
|
1011
|
+
}
|
|
1012
|
+
function internalContentLinkResolverFrom(source) {
|
|
1013
|
+
if (!source) return () => null;
|
|
1014
|
+
if (typeof source === "function") return source;
|
|
1015
|
+
return (key) => source.get(key.toLowerCase()) ?? null;
|
|
1016
|
+
}
|
|
1017
|
+
function isSafeInternalPath(path) {
|
|
1018
|
+
return typeof path === "string" && path.startsWith("/") && !path.startsWith("//");
|
|
1019
|
+
}
|
|
1020
|
+
function escapeHtmlText(value) {
|
|
1021
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1022
|
+
}
|
|
1023
|
+
var INTERNAL_CONTENT_LINK_CLASS = "rt-internal-link";
|
|
1024
|
+
var INTERNAL_CONTENT_LINK_PENDING_CLASS = "rt-internal-link--pending";
|
|
1025
|
+
var INTERNAL_CONTENT_LINK_PENDING_ATTR = "data-rt-internal-link-pending";
|
|
1026
|
+
function renderInternalContentLinksInText(text, resolve) {
|
|
1027
|
+
const tokens = findInternalContentLinkTokens(text);
|
|
1028
|
+
if (tokens.length === 0) return { html: text, hasTokens: false };
|
|
1029
|
+
let cursor = 0;
|
|
1030
|
+
let html = "";
|
|
1031
|
+
for (const token of tokens) {
|
|
1032
|
+
html += text.slice(cursor, token.index);
|
|
1033
|
+
const path = resolve(token.key);
|
|
1034
|
+
const label = escapeHtmlText(token.label);
|
|
1035
|
+
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>`;
|
|
1036
|
+
cursor = token.index + token.raw.length;
|
|
1037
|
+
}
|
|
1038
|
+
html += text.slice(cursor);
|
|
1039
|
+
return { html, hasTokens: true };
|
|
1040
|
+
}
|
|
1041
|
+
var INTERNAL_LINK_BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
|
|
1042
|
+
"a",
|
|
1043
|
+
"code",
|
|
1044
|
+
"pre",
|
|
1045
|
+
"script",
|
|
1046
|
+
"style",
|
|
1047
|
+
"textarea"
|
|
1048
|
+
]);
|
|
1049
|
+
var VOID_HTML_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "input", "source", "wbr"]);
|
|
1050
|
+
function htmlTagEnd(html, start) {
|
|
1051
|
+
let quote = null;
|
|
1052
|
+
for (let index = start + 1; index < html.length; index += 1) {
|
|
1053
|
+
const character = html[index];
|
|
1054
|
+
if (quote) {
|
|
1055
|
+
if (character === quote) quote = null;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (character === '"' || character === "'") {
|
|
1059
|
+
quote = character;
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
if (character === ">") return index;
|
|
1063
|
+
}
|
|
1064
|
+
return -1;
|
|
1065
|
+
}
|
|
1066
|
+
function renderInternalContentLinksInHtml(html, resolve) {
|
|
1067
|
+
if (!html.includes("[[internal:")) return { html, hasTokens: false };
|
|
1068
|
+
let blockedDepth = 0;
|
|
1069
|
+
let cursor = 0;
|
|
1070
|
+
let out = "";
|
|
1071
|
+
let hasTokens = false;
|
|
1072
|
+
const emitText = (text) => {
|
|
1073
|
+
if (blockedDepth > 0 || !text) {
|
|
1074
|
+
out += text;
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const rendered = renderInternalContentLinksInText(text, resolve);
|
|
1078
|
+
hasTokens ||= rendered.hasTokens;
|
|
1079
|
+
out += rendered.html;
|
|
1080
|
+
};
|
|
1081
|
+
while (cursor < html.length) {
|
|
1082
|
+
const tagStart = html.indexOf("<", cursor);
|
|
1083
|
+
if (tagStart < 0) {
|
|
1084
|
+
emitText(html.slice(cursor));
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
emitText(html.slice(cursor, tagStart));
|
|
1088
|
+
const tagEnd = htmlTagEnd(html, tagStart);
|
|
1089
|
+
if (tagEnd < 0) {
|
|
1090
|
+
out += html.slice(tagStart);
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1093
|
+
const tag = html.slice(tagStart, tagEnd + 1);
|
|
1094
|
+
out += tag;
|
|
1095
|
+
const parsed = /^<\s*(\/?)\s*([a-z0-9-]+)/i.exec(tag);
|
|
1096
|
+
const tagName = parsed?.[2]?.toLowerCase();
|
|
1097
|
+
if (tagName && INTERNAL_LINK_BLOCKED_HTML_TAGS.has(tagName)) {
|
|
1098
|
+
const isClosing = parsed?.[1] === "/";
|
|
1099
|
+
const isVoid = VOID_HTML_TAGS.has(tagName) || /\/\s*>$/.test(tag);
|
|
1100
|
+
if (isClosing) blockedDepth = Math.max(0, blockedDepth - 1);
|
|
1101
|
+
else if (!isVoid) blockedDepth += 1;
|
|
1102
|
+
}
|
|
1103
|
+
cursor = tagEnd + 1;
|
|
1104
|
+
}
|
|
1105
|
+
return { html: out, hasTokens };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
962
1108
|
// src/preview-token.ts
|
|
963
1109
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
964
1110
|
var DELIMITER = ".";
|
|
@@ -1266,14 +1412,114 @@ function ensureStableIds(block) {
|
|
|
1266
1412
|
function injectStableIdsTree(blocks) {
|
|
1267
1413
|
return blocks.map((b) => ensureStableIds(b));
|
|
1268
1414
|
}
|
|
1415
|
+
|
|
1416
|
+
// src/site-patterns.ts
|
|
1417
|
+
var SITE_PATTERN_SLOTS = [
|
|
1418
|
+
{ key: "post_footer", label: "\uAE00 \uD558\uB2E8", description: "\uAE00 \uBCF8\uBB38 \uBC14\uB85C \uC544\uB798" }
|
|
1419
|
+
];
|
|
1420
|
+
var POST_FOOTER_SLOT = "post_footer";
|
|
1421
|
+
var SITE_PATTERN_PLACEMENTS_KEY = "site_pattern_placements";
|
|
1422
|
+
var SITE_PATTERN_KEY_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
1423
|
+
function isValidSitePatternKey(key) {
|
|
1424
|
+
return SITE_PATTERN_KEY_PATTERN.test(key);
|
|
1425
|
+
}
|
|
1426
|
+
function isSitePatternSlotKey(value) {
|
|
1427
|
+
return SITE_PATTERN_SLOTS.some((slot) => slot.key === value);
|
|
1428
|
+
}
|
|
1429
|
+
var EMPTY_SITE_PATTERN_PLACEMENTS = { rules: [] };
|
|
1430
|
+
function isRecord(value) {
|
|
1431
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1432
|
+
}
|
|
1433
|
+
function readNullableKey(value) {
|
|
1434
|
+
if (value === null) return null;
|
|
1435
|
+
if (typeof value === "string") {
|
|
1436
|
+
const trimmed = value.trim();
|
|
1437
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1438
|
+
}
|
|
1439
|
+
return void 0;
|
|
1440
|
+
}
|
|
1441
|
+
function readSitePatternPlacements(raw) {
|
|
1442
|
+
let value = raw;
|
|
1443
|
+
if (typeof value === "string") {
|
|
1444
|
+
try {
|
|
1445
|
+
value = JSON.parse(value);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return EMPTY_SITE_PATTERN_PLACEMENTS;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (!isRecord(value) || !Array.isArray(value.rules)) {
|
|
1451
|
+
return EMPTY_SITE_PATTERN_PLACEMENTS;
|
|
1452
|
+
}
|
|
1453
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const item of value.rules) {
|
|
1455
|
+
if (!isRecord(item)) continue;
|
|
1456
|
+
const slot = typeof item.slot === "string" ? item.slot : "";
|
|
1457
|
+
if (!isSitePatternSlotKey(slot)) continue;
|
|
1458
|
+
const collectionKey = readNullableKey(item.collectionKey);
|
|
1459
|
+
if (collectionKey === void 0) continue;
|
|
1460
|
+
const patternKey = readNullableKey(item.patternKey);
|
|
1461
|
+
if (patternKey === void 0) continue;
|
|
1462
|
+
if (patternKey !== null && !isValidSitePatternKey(patternKey)) continue;
|
|
1463
|
+
byTarget.set(`${slot}\0${collectionKey ?? ""}`, {
|
|
1464
|
+
slot,
|
|
1465
|
+
collectionKey,
|
|
1466
|
+
patternKey
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
return { rules: [...byTarget.values()] };
|
|
1470
|
+
}
|
|
1471
|
+
function findSitePatternPlacementRule(placements, slot, collectionKey) {
|
|
1472
|
+
return placements.rules.find(
|
|
1473
|
+
(rule) => rule.slot === slot && rule.collectionKey === collectionKey
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
function setSitePatternPlacementRule(placements, target, patternKey) {
|
|
1477
|
+
const rest = placements.rules.filter(
|
|
1478
|
+
(rule) => !(rule.slot === target.slot && rule.collectionKey === target.collectionKey)
|
|
1479
|
+
);
|
|
1480
|
+
if (patternKey === void 0) return { rules: rest };
|
|
1481
|
+
return {
|
|
1482
|
+
rules: [
|
|
1483
|
+
...rest,
|
|
1484
|
+
{ slot: target.slot, collectionKey: target.collectionKey, patternKey }
|
|
1485
|
+
]
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function emptyPatternSlots() {
|
|
1489
|
+
return Object.fromEntries(SITE_PATTERN_SLOTS.map((slot) => [slot.key, null]));
|
|
1490
|
+
}
|
|
1491
|
+
function resolvePatternSlots(post, placements, publishedKeys) {
|
|
1492
|
+
const slots = emptyPatternSlots();
|
|
1493
|
+
for (const slot of SITE_PATTERN_SLOTS) {
|
|
1494
|
+
const specific = post.collectionKey !== null ? findSitePatternPlacementRule(placements, slot.key, post.collectionKey) : void 0;
|
|
1495
|
+
const rule = specific ?? findSitePatternPlacementRule(placements, slot.key, null);
|
|
1496
|
+
const key = rule?.patternKey ?? null;
|
|
1497
|
+
slots[slot.key] = key !== null && publishedKeys.has(key) ? key : null;
|
|
1498
|
+
}
|
|
1499
|
+
return slots;
|
|
1500
|
+
}
|
|
1501
|
+
function referencedSitePatternKeys(placements) {
|
|
1502
|
+
return new Set(
|
|
1503
|
+
placements.rules.map((rule) => rule.patternKey).filter((key) => key !== null)
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1269
1506
|
export {
|
|
1270
1507
|
BLOCK_NAME_PATTERN,
|
|
1508
|
+
EMPTY_SITE_PATTERN_PLACEMENTS,
|
|
1271
1509
|
FIELD_BINDING_SOURCE,
|
|
1272
1510
|
FIELD_BINDING_SOURCE_ALIAS,
|
|
1273
1511
|
IMAGE_ALT_MAX_LENGTH,
|
|
1512
|
+
INTERNAL_CONTENT_LINK_CLASS,
|
|
1513
|
+
INTERNAL_CONTENT_LINK_PENDING_ATTR,
|
|
1514
|
+
INTERNAL_CONTENT_LINK_PENDING_CLASS,
|
|
1515
|
+
INTERNAL_CONTENT_LINK_TOKEN_SOURCE,
|
|
1274
1516
|
LEGACY_BLOG_DETAIL_PATH,
|
|
1275
1517
|
PHASE_1_CORE_BLOCKS,
|
|
1276
1518
|
PHASE_1_CSP,
|
|
1519
|
+
POST_FOOTER_SLOT,
|
|
1520
|
+
SITE_PATTERN_KEY_PATTERN,
|
|
1521
|
+
SITE_PATTERN_PLACEMENTS_KEY,
|
|
1522
|
+
SITE_PATTERN_SLOTS,
|
|
1277
1523
|
applyBlockBindings,
|
|
1278
1524
|
bindingSrcValue,
|
|
1279
1525
|
bindingTextValue,
|
|
@@ -1281,16 +1527,24 @@ export {
|
|
|
1281
1527
|
boundFieldValue,
|
|
1282
1528
|
chainResolvers,
|
|
1283
1529
|
createFieldBindingResolver,
|
|
1530
|
+
emptyPatternSlots,
|
|
1284
1531
|
ensureImportedHtmlDocumentRoots,
|
|
1285
1532
|
ensureStableIds,
|
|
1286
1533
|
extractFaqEntries,
|
|
1287
1534
|
faqPageJsonLd,
|
|
1535
|
+
findInternalContentLinkTokens,
|
|
1536
|
+
findSitePatternPlacementRule,
|
|
1288
1537
|
htmlToPlainText,
|
|
1289
1538
|
injectStableIdsTree,
|
|
1539
|
+
internalContentKeyFromPath,
|
|
1540
|
+
internalContentLinkResolverFrom,
|
|
1541
|
+
internalContentPathIndex,
|
|
1290
1542
|
isAllowedEmbedSrc,
|
|
1291
1543
|
isAllowedMapEmbedSrc,
|
|
1292
1544
|
isInCollectionMode,
|
|
1545
|
+
isSitePatternSlotKey,
|
|
1293
1546
|
isValidBlock,
|
|
1547
|
+
isValidSitePatternKey,
|
|
1294
1548
|
issuePreviewToken,
|
|
1295
1549
|
normalizeEmbed,
|
|
1296
1550
|
normalizeImageAlt,
|
|
@@ -1298,10 +1552,15 @@ export {
|
|
|
1298
1552
|
postsForCollection,
|
|
1299
1553
|
postsInAnyCollection,
|
|
1300
1554
|
postsNotInAnyCollection,
|
|
1555
|
+
readSitePatternPlacements,
|
|
1556
|
+
referencedSitePatternKeys,
|
|
1557
|
+
renderInternalContentLinksInHtml,
|
|
1558
|
+
renderInternalContentLinksInText,
|
|
1301
1559
|
resolveCategoryPath,
|
|
1302
1560
|
resolveCategoryTreeChain,
|
|
1303
1561
|
resolveCategoryTreeEntryPath,
|
|
1304
1562
|
resolveCategoryTreeHubPaths,
|
|
1563
|
+
resolvePatternSlots,
|
|
1305
1564
|
resolvePostCategorySlug,
|
|
1306
1565
|
resolvePostCollection,
|
|
1307
1566
|
resolvePostPath,
|
|
@@ -1312,6 +1571,7 @@ export {
|
|
|
1312
1571
|
sanitizeImportedHtml,
|
|
1313
1572
|
scopeImportedCss,
|
|
1314
1573
|
serializeBlock,
|
|
1574
|
+
setSitePatternPlacementRule,
|
|
1315
1575
|
validateCollections,
|
|
1316
1576
|
verifyPreviewToken
|
|
1317
1577
|
};
|