@glw907/cairn-cms 0.9.0 → 0.11.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/components/ComponentForm.svelte +178 -0
- package/dist/components/ComponentForm.svelte.d.ts +20 -0
- package/dist/components/ComponentForm.svelte.d.ts.map +1 -0
- package/dist/components/ComponentInsertDialog.svelte +92 -0
- package/dist/components/ComponentInsertDialog.svelte.d.ts +20 -0
- package/dist/components/ComponentInsertDialog.svelte.d.ts.map +1 -0
- package/dist/components/EditPage.svelte +6 -3
- package/dist/components/EditPage.svelte.d.ts +3 -0
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/IconPicker.svelte +51 -0
- package/dist/components/IconPicker.svelte.d.ts +20 -0
- package/dist/components/IconPicker.svelte.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -1
- package/dist/content/compose.d.ts.map +1 -1
- package/dist/content/compose.js +1 -0
- package/dist/content/types.d.ts +5 -0
- package/dist/content/types.d.ts.map +1 -1
- package/dist/delivery/CairnHead.svelte +36 -0
- package/dist/delivery/CairnHead.svelte.d.ts +15 -0
- package/dist/delivery/CairnHead.svelte.d.ts.map +1 -0
- package/dist/delivery/content-index.d.ts +8 -6
- package/dist/delivery/content-index.d.ts.map +1 -1
- package/dist/delivery/content-index.js +1 -1
- package/dist/delivery/index.d.ts +22 -0
- package/dist/delivery/index.d.ts.map +1 -0
- package/dist/delivery/index.js +19 -0
- package/dist/delivery/json-ld.d.ts +2 -0
- package/dist/delivery/json-ld.d.ts.map +1 -0
- package/dist/delivery/json-ld.js +16 -0
- package/dist/delivery/responses.d.ts +14 -0
- package/dist/delivery/responses.d.ts.map +1 -0
- package/dist/delivery/responses.js +30 -0
- package/dist/delivery/seo.d.ts +4 -0
- package/dist/delivery/seo.d.ts.map +1 -1
- package/dist/delivery/seo.js +11 -0
- package/dist/delivery/site-descriptors.d.ts +5 -0
- package/dist/delivery/site-descriptors.d.ts.map +1 -0
- package/dist/delivery/site-descriptors.js +9 -0
- package/dist/delivery/site-index.d.ts +8 -2
- package/dist/delivery/site-index.d.ts.map +1 -1
- package/dist/delivery/site-index.js +28 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/render/component-grammar.d.ts +10 -0
- package/dist/render/component-grammar.d.ts.map +1 -0
- package/dist/render/component-grammar.js +140 -0
- package/dist/render/component-insert.d.ts +14 -0
- package/dist/render/component-insert.d.ts.map +1 -0
- package/dist/render/component-insert.js +9 -0
- package/dist/render/component-reference.d.ts +11 -0
- package/dist/render/component-reference.d.ts.map +1 -0
- package/dist/render/component-reference.js +34 -0
- package/dist/render/component-validate.d.ts +10 -0
- package/dist/render/component-validate.d.ts.map +1 -0
- package/dist/render/component-validate.js +30 -0
- package/dist/render/registry.d.ts +45 -1
- package/dist/render/registry.d.ts.map +1 -1
- package/dist/render/registry.js +13 -0
- package/dist/sveltekit/public-routes.d.ts +11 -0
- package/dist/sveltekit/public-routes.d.ts.map +1 -1
- package/dist/sveltekit/public-routes.js +15 -2
- package/package.json +7 -1
- package/src/lib/components/ComponentForm.svelte +178 -0
- package/src/lib/components/ComponentInsertDialog.svelte +92 -0
- package/src/lib/components/EditPage.svelte +6 -3
- package/src/lib/components/IconPicker.svelte +51 -0
- package/src/lib/components/index.ts +3 -1
- package/src/lib/content/compose.ts +1 -0
- package/src/lib/content/types.ts +5 -0
- package/src/lib/delivery/CairnHead.svelte +36 -0
- package/src/lib/delivery/content-index.ts +15 -10
- package/src/lib/delivery/index.ts +32 -0
- package/src/lib/delivery/json-ld.ts +16 -0
- package/src/lib/delivery/responses.ts +34 -0
- package/src/lib/delivery/seo.ts +13 -0
- package/src/lib/delivery/site-descriptors.ts +12 -0
- package/src/lib/delivery/site-index.ts +27 -2
- package/src/lib/index.ts +16 -2
- package/src/lib/render/component-grammar.ts +167 -0
- package/src/lib/render/component-insert.ts +15 -0
- package/src/lib/render/component-reference.ts +38 -0
- package/src/lib/render/component-validate.ts +36 -0
- package/src/lib/render/registry.ts +61 -1
- package/src/lib/sveltekit/public-routes.ts +23 -2
- package/dist/components/ComponentPalette.svelte +0 -50
- package/dist/components/ComponentPalette.svelte.d.ts +0 -16
- package/dist/components/ComponentPalette.svelte.d.ts.map +0 -1
- package/src/lib/components/ComponentPalette.svelte +0 -50
|
@@ -45,7 +45,7 @@ export function createContentIndex(files, descriptor) {
|
|
|
45
45
|
excerpt: deriveExcerpt(body, { description: asString(frontmatter.description) }),
|
|
46
46
|
wordCount: wordCount(body),
|
|
47
47
|
draft: frontmatter.draft === true,
|
|
48
|
-
frontmatter,
|
|
48
|
+
frontmatter: frontmatter,
|
|
49
49
|
body,
|
|
50
50
|
};
|
|
51
51
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { createContentIndex, fromGlob } from './content-index.js';
|
|
2
|
+
export type { RawFile, ContentSummary, ContentEntry, ContentIndex } from './content-index.js';
|
|
3
|
+
export { createSiteIndex } from './site-index.js';
|
|
4
|
+
export type { SiteIndex, ConceptIndex } from './site-index.js';
|
|
5
|
+
export { siteDescriptors } from './site-descriptors.js';
|
|
6
|
+
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
7
|
+
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
8
|
+
export type { FeedChannel, FeedItem } from './feeds.js';
|
|
9
|
+
export { buildSitemap } from './sitemap.js';
|
|
10
|
+
export type { SitemapUrl } from './sitemap.js';
|
|
11
|
+
export { buildRobots } from './robots.js';
|
|
12
|
+
export { buildSeoMeta } from './seo.js';
|
|
13
|
+
export type { SeoInput, SeoMeta } from './seo.js';
|
|
14
|
+
export { paginate } from './paginate.js';
|
|
15
|
+
export type { Page } from './paginate.js';
|
|
16
|
+
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
17
|
+
export { jsonLdScript } from './json-ld.js';
|
|
18
|
+
export { permalink } from '../content/permalink.js';
|
|
19
|
+
export { createPublicRoutes } from '../sveltekit/public-routes.js';
|
|
20
|
+
export type { PublicRoutesDeps, ListData, TagData, TagIndexData, EntryData, } from '../sveltekit/public-routes.js';
|
|
21
|
+
export { default as CairnHead } from './CairnHead.svelte';
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAC9F,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzD,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,SAAS,GACV,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// cairn-cms: the public delivery entry (@glw907/cairn-cms/delivery). The complete, canonical,
|
|
2
|
+
// backend-free toolkit a SvelteKit site wires its public pages with: the content index and the
|
|
3
|
+
// site resolver, the descriptor helper, the syndication and SEO builders, the endpoint response
|
|
4
|
+
// helpers, the catch-all route loaders, and the head component. It imports nothing from auth,
|
|
5
|
+
// github, or email, so importing it does not pull the server backend into a public bundle.
|
|
6
|
+
export { createContentIndex, fromGlob } from './content-index.js';
|
|
7
|
+
export { createSiteIndex } from './site-index.js';
|
|
8
|
+
export { siteDescriptors } from './site-descriptors.js';
|
|
9
|
+
export { deriveExcerpt, wordCount } from './excerpt.js';
|
|
10
|
+
export { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
11
|
+
export { buildSitemap } from './sitemap.js';
|
|
12
|
+
export { buildRobots } from './robots.js';
|
|
13
|
+
export { buildSeoMeta } from './seo.js';
|
|
14
|
+
export { paginate } from './paginate.js';
|
|
15
|
+
export { rssResponse, jsonFeedResponse, sitemapResponse, robotsResponse } from './responses.js';
|
|
16
|
+
export { jsonLdScript } from './json-ld.js';
|
|
17
|
+
export { permalink } from '../content/permalink.js';
|
|
18
|
+
export { createPublicRoutes } from '../sveltekit/public-routes.js';
|
|
19
|
+
export { default as CairnHead } from './CairnHead.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-ld.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/json-ld.ts"],"names":[],"mappings":"AAOA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQlE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// cairn-cms: serialize a JSON-LD object into a safe inline script string. JSON.stringify does
|
|
2
|
+
// not escape <, >, or &, so a value containing "</script>" would close the element and inject
|
|
3
|
+
// markup. Escaping the three characters to their JSON unicode forms keeps the structured data
|
|
4
|
+
// identical for a parser while making the bytes unable to break out of the script element.
|
|
5
|
+
// The line separator U+2028 and paragraph separator U+2029 get the same treatment: they are
|
|
6
|
+
// legal inside a JSON string but unsafe in inline script text, where some parsers read them as
|
|
7
|
+
// line terminators, so an author pasting one into frontmatter would corrupt the JSON-LD block.
|
|
8
|
+
export function jsonLdScript(data) {
|
|
9
|
+
const json = JSON.stringify(data)
|
|
10
|
+
.replace(/</g, '\\u003c')
|
|
11
|
+
.replace(/>/g, '\\u003e')
|
|
12
|
+
.replace(/&/g, '\\u0026')
|
|
13
|
+
.replace(/\u2028/g, '\\u2028')
|
|
14
|
+
.replace(/\u2029/g, '\\u2029');
|
|
15
|
+
return `<script type="application/ld+json">${json}</script>`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type FeedChannel, type FeedItem } from './feeds.js';
|
|
2
|
+
import { type SitemapUrl } from './sitemap.js';
|
|
3
|
+
/** An RSS 2.0 feed response. */
|
|
4
|
+
export declare function rssResponse(channel: FeedChannel, items: FeedItem[]): Response;
|
|
5
|
+
/** A JSON Feed 1.1 response. */
|
|
6
|
+
export declare function jsonFeedResponse(channel: FeedChannel, items: FeedItem[]): Response;
|
|
7
|
+
/** A sitemap response. */
|
|
8
|
+
export declare function sitemapResponse(urls: SitemapUrl[]): Response;
|
|
9
|
+
/** A robots.txt response. */
|
|
10
|
+
export declare function robotsResponse(opts: {
|
|
11
|
+
sitemapUrl: string;
|
|
12
|
+
disallow?: string[];
|
|
13
|
+
}): Response;
|
|
14
|
+
//# sourceMappingURL=responses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/responses.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAG7D,gCAAgC;AAChC,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAI7E;AAED,gCAAgC;AAChC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,CAIlF;AAED,0BAA0B;AAC1B,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,QAAQ,CAI5D;AAED,6BAA6B;AAC7B,wBAAgB,cAAc,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,QAAQ,CAI1F"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// cairn-cms: response helpers for the public delivery endpoints. Each wraps a builder in a
|
|
2
|
+
// Response with the correct Content-Type, so a site's +server.ts GET is a single call. The
|
|
3
|
+
// content type is the one detail every site otherwise copies and occasionally gets wrong.
|
|
4
|
+
import { buildRssFeed, buildJsonFeed } from './feeds.js';
|
|
5
|
+
import { buildSitemap } from './sitemap.js';
|
|
6
|
+
import { buildRobots } from './robots.js';
|
|
7
|
+
/** An RSS 2.0 feed response. */
|
|
8
|
+
export function rssResponse(channel, items) {
|
|
9
|
+
return new Response(buildRssFeed(channel, items), {
|
|
10
|
+
headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/** A JSON Feed 1.1 response. */
|
|
14
|
+
export function jsonFeedResponse(channel, items) {
|
|
15
|
+
return new Response(buildJsonFeed(channel, items), {
|
|
16
|
+
headers: { 'Content-Type': 'application/feed+json; charset=utf-8' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** A sitemap response. */
|
|
20
|
+
export function sitemapResponse(urls) {
|
|
21
|
+
return new Response(buildSitemap(urls), {
|
|
22
|
+
headers: { 'Content-Type': 'application/xml; charset=utf-8' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** A robots.txt response. */
|
|
26
|
+
export function robotsResponse(opts) {
|
|
27
|
+
return new Response(buildRobots(opts), {
|
|
28
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
29
|
+
});
|
|
30
|
+
}
|
package/dist/delivery/seo.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface SeoInput {
|
|
|
12
12
|
json?: string;
|
|
13
13
|
};
|
|
14
14
|
image?: string;
|
|
15
|
+
/** A robots meta directive, e.g. "noindex, nofollow". Omitted from the head when absent. */
|
|
16
|
+
robots?: string;
|
|
17
|
+
/** Author name, emitted as article:author for the article type. */
|
|
18
|
+
author?: string;
|
|
15
19
|
}
|
|
16
20
|
/** Plain-data head: a title, meta tags, link tags, and one JSON-LD object. */
|
|
17
21
|
export interface SeoMeta {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/seo.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"seo.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/seo.ts"],"names":[],"mappings":"AAIA,6EAA6E;AAC7E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,sCAAsC;AACtC,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAsDrD"}
|
package/dist/delivery/seo.js
CHANGED
|
@@ -17,6 +17,17 @@ export function buildSeoMeta(input) {
|
|
|
17
17
|
meta.push({ property: 'og:image', content: input.image });
|
|
18
18
|
meta.push({ name: 'twitter:image', content: input.image });
|
|
19
19
|
}
|
|
20
|
+
if (input.robots) {
|
|
21
|
+
meta.push({ name: 'robots', content: input.robots });
|
|
22
|
+
}
|
|
23
|
+
if (type === 'article') {
|
|
24
|
+
if (input.published)
|
|
25
|
+
meta.push({ property: 'article:published_time', content: input.published });
|
|
26
|
+
if (input.modified)
|
|
27
|
+
meta.push({ property: 'article:modified_time', content: input.modified });
|
|
28
|
+
if (input.author)
|
|
29
|
+
meta.push({ property: 'article:author', content: input.author });
|
|
30
|
+
}
|
|
20
31
|
const links = [{ rel: 'canonical', href: input.canonicalUrl }];
|
|
21
32
|
if (input.feeds?.rss) {
|
|
22
33
|
links.push({ rel: 'alternate', type: 'application/rss+xml', href: input.feeds.rss, title: input.siteName });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CairnAdapter, ConceptDescriptor } from '../content/types.js';
|
|
2
|
+
import type { SiteConfig } from '../nav/site-config.js';
|
|
3
|
+
/** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
|
|
4
|
+
export declare function siteDescriptors(adapter: CairnAdapter, siteConfig: SiteConfig): ConceptDescriptor[];
|
|
5
|
+
//# sourceMappingURL=site-descriptors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-descriptors.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-descriptors.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,GAAG,iBAAiB,EAAE,CAElG"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// cairn-cms: the one-call descriptor helper. A delivery site needs the same per-concept
|
|
2
|
+
// descriptors the admin runtime uses; this wraps the two calls that derive them so the
|
|
3
|
+
// pairing is not tribal knowledge. The YAML URL policy stays the single source of truth.
|
|
4
|
+
import { normalizeConcepts } from '../content/concepts.js';
|
|
5
|
+
import { urlPolicyFrom } from '../nav/site-config.js';
|
|
6
|
+
/** Per-concept descriptors for a site, from its adapter content and its parsed site config. */
|
|
7
|
+
export function siteDescriptors(adapter, siteConfig) {
|
|
8
|
+
return normalizeConcepts(adapter.content, urlPolicyFrom(siteConfig));
|
|
9
|
+
}
|
|
@@ -23,6 +23,12 @@ export interface SiteIndex {
|
|
|
23
23
|
/** Every non-draft summary across concepts, for the site-wide sitemap. */
|
|
24
24
|
all(): ContentSummary[];
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Union per-concept indexes into a site-level resolver. Throws on a duplicate permalink and,
|
|
28
|
+
* unless `validate` is `false`, on any entry whose frontmatter fails its concept's validator,
|
|
29
|
+
* so malformed content fails the build instead of shipping.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createSiteIndex(concepts: ConceptIndex[], opts?: {
|
|
32
|
+
validate?: boolean;
|
|
33
|
+
}): SiteIndex;
|
|
28
34
|
//# sourceMappingURL=site-index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"site-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErF,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACpF,uGAAuG;IACvG,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC9C,0EAA0E;IAC1E,GAAG,IAAI,cAAc,EAAE,CAAC;CACzB;
|
|
1
|
+
{"version":3,"file":"site-index.d.ts","sourceRoot":"","sources":["../../src/lib/delivery/site-index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAErF,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,iBAAiB,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;CACrB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IACpD,iFAAiF;IACjF,QAAQ,CAAC,KAAK,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,CAAC,EAAE,cAAc,CAAA;KAAE,CAAC;IACpF,uGAAuG;IACvG,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC9C,0EAA0E;IAC1E,GAAG,IAAI,cAAc,EAAE,CAAC;CACzB;AA2BD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,SAAS,CAmCtG"}
|
|
@@ -2,8 +2,34 @@
|
|
|
2
2
|
function normalizePath(path) {
|
|
3
3
|
return path.length > 1 ? path.replace(/\/+$/, '') : path;
|
|
4
4
|
}
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** Validate every entry (drafts included) against its concept, aggregating failures. */
|
|
6
|
+
function validateAll(concepts) {
|
|
7
|
+
const problems = [];
|
|
8
|
+
for (const { descriptor, index } of concepts) {
|
|
9
|
+
for (const summary of index.all({ includeDrafts: true })) {
|
|
10
|
+
const entry = index.byId(summary.id);
|
|
11
|
+
if (!entry)
|
|
12
|
+
continue;
|
|
13
|
+
const result = descriptor.validate(entry.frontmatter, entry.body);
|
|
14
|
+
if (!result.ok) {
|
|
15
|
+
for (const [field, message] of Object.entries(result.errors)) {
|
|
16
|
+
problems.push(`${descriptor.dir}/${summary.id}: ${field}: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (problems.length > 0) {
|
|
22
|
+
throw new Error(`site index: ${problems.length} invalid frontmatter field(s):\n ${problems.join('\n ')}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Union per-concept indexes into a site-level resolver. Throws on a duplicate permalink and,
|
|
27
|
+
* unless `validate` is `false`, on any entry whose frontmatter fails its concept's validator,
|
|
28
|
+
* so malformed content fails the build instead of shipping.
|
|
29
|
+
*/
|
|
30
|
+
export function createSiteIndex(concepts, opts = {}) {
|
|
31
|
+
if (opts.validate !== false)
|
|
32
|
+
validateAll(concepts);
|
|
7
33
|
const byPath = new Map();
|
|
8
34
|
const byId = new Map();
|
|
9
35
|
for (const { descriptor, index } of concepts) {
|
package/dist/index.d.ts
CHANGED
|
@@ -9,8 +9,14 @@ export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown,
|
|
|
9
9
|
export { validateFields } from './content/validate.js';
|
|
10
10
|
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
11
11
|
export type { DatePrefix } from './content/ids.js';
|
|
12
|
-
export { defineRegistry } from './render/registry.js';
|
|
13
|
-
export type { ComponentDef, ComponentRegistry } from './render/registry.js';
|
|
12
|
+
export { defineRegistry, emptyValues } from './render/registry.js';
|
|
13
|
+
export type { ComponentDef, ComponentRegistry, FieldType, AttributeField, SlotKind, SlotDef, ComponentValues, } from './render/registry.js';
|
|
14
|
+
export { serializeComponent, parseComponent } from './render/component-grammar.js';
|
|
15
|
+
export { validateComponent } from './render/component-validate.js';
|
|
16
|
+
export type { ComponentValidation } from './render/component-validate.js';
|
|
17
|
+
export { buildComponentInsert, type ComponentInsert } from './render/component-insert.js';
|
|
18
|
+
export { generateComponentReference } from './render/component-reference.js';
|
|
19
|
+
export type { ReferenceOptions } from './render/component-reference.js';
|
|
14
20
|
export { glyph } from './render/glyph.js';
|
|
15
21
|
export type { IconSet } from './render/glyph.js';
|
|
16
22
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGnE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,SAAS,EACT,cAAc,EACd,cAAc,EACd,OAAO,EACP,UAAU,EACV,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGnE,YAAY,EACV,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,SAAS,EACT,YAAY,EACZ,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,EACZ,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EACL,SAAS,EACT,cAAc,EACd,cAAc,EACd,OAAO,EACP,UAAU,EACV,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnE,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,QAAQ,EACR,OAAO,EACP,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,oBAAoB,EAAE,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC1F,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EACL,cAAc,EACd,SAAS,EACT,OAAO,EACP,QAAQ,EACR,SAAS,EACT,SAAS,EACT,aAAa,GACd,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACzF,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EACL,OAAO,EACP,eAAe,EACf,YAAY,EACZ,WAAW,EACX,OAAO,EACP,OAAO,EACP,UAAU,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAG5D,OAAO,EACL,eAAe,EACf,aAAa,EACb,WAAW,EACX,OAAO,EACP,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAKhE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAC3E,YAAY,EACV,OAAO,EACP,cAAc,EACd,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,YAAY,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,11 @@ export { frontmatterFromForm, dateInputValue, serializeMarkdown, parseMarkdown,
|
|
|
8
8
|
export { validateFields } from './content/validate.js';
|
|
9
9
|
export { isValidId, idFromFilename, filenameFromId, slugify, slugFromId, composeDatedId, } from './content/ids.js';
|
|
10
10
|
// Render engine (Plan 04): generic directive pipeline; sites own the component registry.
|
|
11
|
-
export { defineRegistry } from './render/registry.js';
|
|
11
|
+
export { defineRegistry, emptyValues } from './render/registry.js';
|
|
12
|
+
export { serializeComponent, parseComponent } from './render/component-grammar.js';
|
|
13
|
+
export { validateComponent } from './render/component-validate.js';
|
|
14
|
+
export { buildComponentInsert } from './render/component-insert.js';
|
|
15
|
+
export { generateComponentReference } from './render/component-reference.js';
|
|
12
16
|
export { glyph } from './render/glyph.js';
|
|
13
17
|
export { remarkDirectiveStamp } from './render/remark-directives.js';
|
|
14
18
|
export { rehypeDispatch, isElement, strProp, iconSpan, splitHead, cardShell, markFirstList, } from './render/rehype-dispatch.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentDef, ComponentValues } from './registry.js';
|
|
2
|
+
export declare function serializeComponent(def: ComponentDef, values: ComponentValues): string;
|
|
3
|
+
/** Parse a serialized component directive back into guided-form values, the inverse of
|
|
4
|
+
* {@link serializeComponent}. The grammar is reversible, so the editor can round-trip a
|
|
5
|
+
* saved directive through the form. */
|
|
6
|
+
export declare function parseComponent(markdown: string, def: ComponentDef): Promise<ComponentValues>;
|
|
7
|
+
/** The raw attribute keys present on the component's opening directive, read from the parsed tree
|
|
8
|
+
* (quote-aware, unlike a regex over the source). Used by validation to flag unknown keys. */
|
|
9
|
+
export declare function parseRawAttributeKeys(markdown: string, def: ComponentDef): string[];
|
|
10
|
+
//# sourceMappingURL=component-grammar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-grammar.d.ts","sourceRoot":"","sources":["../../src/lib/render/component-grammar.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAW,MAAM,eAAe,CAAC;AA8B5E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,GAAG,MAAM,CA2BrF;AAwBD;;wCAEwC;AACxC,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,CAAC,CAqClG;AAED;8FAC8F;AAC9F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,EAAE,CAMnF"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkDirective from 'remark-directive';
|
|
4
|
+
import remarkStringify from 'remark-stringify';
|
|
5
|
+
const COLON = ':';
|
|
6
|
+
function attrBlock(def, values) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
for (const field of def.attributes ?? []) {
|
|
9
|
+
const v = values.attributes[field.key];
|
|
10
|
+
if (field.type === 'boolean') {
|
|
11
|
+
if (v === true)
|
|
12
|
+
parts.push(`${field.key}="true"`);
|
|
13
|
+
}
|
|
14
|
+
else if (typeof v === 'string' && v !== '') {
|
|
15
|
+
// The directive attribute grammar (mdast-util-directive) treats a literal `"` as the value
|
|
16
|
+
// terminator and decodes HTML entities, so a backslash escape does not survive a round-trip.
|
|
17
|
+
// Encode `&` first (so existing entities are not double-decoded) then `"`; the parser decodes
|
|
18
|
+
// both back. A backslash is literal in this grammar and needs no escaping.
|
|
19
|
+
const escaped = v.replace(/&/g, '&').replace(/"/g, '"');
|
|
20
|
+
parts.push(`${field.key}="${escaped}"`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return parts.length ? `{${parts.join(' ')}}` : '';
|
|
24
|
+
}
|
|
25
|
+
function slotByName(def, name) {
|
|
26
|
+
return (def.slots ?? []).find((s) => s.name === name);
|
|
27
|
+
}
|
|
28
|
+
function nestedSlots(def) {
|
|
29
|
+
return (def.slots ?? []).filter((s) => s.name !== 'title' && s.name !== 'body');
|
|
30
|
+
}
|
|
31
|
+
export function serializeComponent(def, values) {
|
|
32
|
+
const fence = COLON.repeat(nestedSlots(def).length > 0 ? 4 : 3);
|
|
33
|
+
const title = slotByName(def, 'title') ? values.slots.title ?? '' : '';
|
|
34
|
+
// Escape brackets in the label so a `[` or `]` in the title does not break the directive label
|
|
35
|
+
// grammar; remark un-escapes them back to literal text on parse, so readLabel recovers them.
|
|
36
|
+
const label = title ? `[${title.replace(/\[/g, '\\[').replace(/\]/g, '\\]')}]` : '';
|
|
37
|
+
const open = `${fence}${def.name}${label}${attrBlock(def, values)}`;
|
|
38
|
+
const lines = [open];
|
|
39
|
+
const body = slotByName(def, 'body') ? values.slots.body ?? '' : '';
|
|
40
|
+
if (body)
|
|
41
|
+
lines.push(body);
|
|
42
|
+
for (const slot of nestedSlots(def)) {
|
|
43
|
+
const raw = values.slots[slot.name];
|
|
44
|
+
const content = slot.kind === 'repeatable'
|
|
45
|
+
? (Array.isArray(raw) ? raw : []).filter((i) => i !== '').map((i) => `- ${i}`).join('\n')
|
|
46
|
+
: (raw ?? '');
|
|
47
|
+
if (!content)
|
|
48
|
+
continue;
|
|
49
|
+
if (lines.length > 1)
|
|
50
|
+
lines.push(''); // blank line before this block
|
|
51
|
+
lines.push(`${COLON.repeat(3)}${slot.name}`, content, COLON.repeat(3));
|
|
52
|
+
}
|
|
53
|
+
lines.push(fence);
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
function isContainer(node) {
|
|
57
|
+
return node.type === 'containerDirective';
|
|
58
|
+
}
|
|
59
|
+
// Pin the bullet to `-` so a markdown body or slot that uses dash bullets round-trips unchanged
|
|
60
|
+
// rather than drifting to remark-stringify's default `*`, which would silently mutate author content.
|
|
61
|
+
const toMd = unified().use(remarkStringify, { bullet: '-' });
|
|
62
|
+
/** Render mdast children back to trimmed markdown text. */
|
|
63
|
+
function childrenToText(children) {
|
|
64
|
+
const root = { type: 'root', children };
|
|
65
|
+
return String(toMd.stringify(root)).trim();
|
|
66
|
+
}
|
|
67
|
+
/** Parse a serialized component directive back into guided-form values, the inverse of
|
|
68
|
+
* {@link serializeComponent}. The grammar is reversible, so the editor can round-trip a
|
|
69
|
+
* saved directive through the form. */
|
|
70
|
+
export async function parseComponent(markdown, def) {
|
|
71
|
+
const tree = unified().use(remarkParse).use(remarkDirective).parse(markdown);
|
|
72
|
+
const root = tree.children.find((c) => isContainer(c) && c.name === def.name);
|
|
73
|
+
const values = emptyComponentValues(def);
|
|
74
|
+
if (!root)
|
|
75
|
+
return values;
|
|
76
|
+
for (const field of def.attributes ?? []) {
|
|
77
|
+
const raw = root.attributes?.[field.key];
|
|
78
|
+
if (field.type === 'boolean')
|
|
79
|
+
values.attributes[field.key] = raw === 'true';
|
|
80
|
+
else if (typeof raw === 'string')
|
|
81
|
+
values.attributes[field.key] = raw;
|
|
82
|
+
}
|
|
83
|
+
const titleSlot = slotByName(def, 'title');
|
|
84
|
+
const bodySlot = slotByName(def, 'body');
|
|
85
|
+
const nested = nestedSlots(def);
|
|
86
|
+
const nestedNames = new Set(nested.map((s) => s.name));
|
|
87
|
+
const directChildren = root.children.filter((c) => !(isContainer(c) && nestedNames.has(c.name)) && !isDirectiveLabel(c));
|
|
88
|
+
const nestedChildren = root.children.filter((c) => isContainer(c) && nestedNames.has(c.name));
|
|
89
|
+
if (titleSlot)
|
|
90
|
+
values.slots.title = readLabel(root) ?? '';
|
|
91
|
+
if (bodySlot)
|
|
92
|
+
values.slots.body = childrenToText(directChildren);
|
|
93
|
+
for (const slot of nested) {
|
|
94
|
+
const node = nestedChildren.find((c) => c.name === slot.name);
|
|
95
|
+
if (!node)
|
|
96
|
+
continue;
|
|
97
|
+
if (slot.kind === 'repeatable')
|
|
98
|
+
values.slots[slot.name] = readListItems(node.children);
|
|
99
|
+
else
|
|
100
|
+
values.slots[slot.name] = childrenToText(node.children);
|
|
101
|
+
}
|
|
102
|
+
return values;
|
|
103
|
+
}
|
|
104
|
+
/** The raw attribute keys present on the component's opening directive, read from the parsed tree
|
|
105
|
+
* (quote-aware, unlike a regex over the source). Used by validation to flag unknown keys. */
|
|
106
|
+
export function parseRawAttributeKeys(markdown, def) {
|
|
107
|
+
const tree = unified().use(remarkParse).use(remarkDirective).parse(markdown);
|
|
108
|
+
const root = tree.children.find((c) => isContainer(c) && c.name === def.name);
|
|
109
|
+
return Object.keys(root?.attributes ?? {});
|
|
110
|
+
}
|
|
111
|
+
// A bare parse base: empty strings, false, and empty lists, with no attribute defaults applied. The
|
|
112
|
+
// `emptyValues` helper in registry.ts seeds form defaults instead, so it is deliberately not reused
|
|
113
|
+
// here; the parse must overwrite only the fields actually present in the markdown.
|
|
114
|
+
function emptyComponentValues(def) {
|
|
115
|
+
const attributes = {};
|
|
116
|
+
for (const f of def.attributes ?? [])
|
|
117
|
+
attributes[f.key] = f.type === 'boolean' ? false : '';
|
|
118
|
+
const slots = {};
|
|
119
|
+
for (const s of def.slots ?? [])
|
|
120
|
+
slots[s.name] = s.kind === 'repeatable' ? [] : '';
|
|
121
|
+
return { attributes, slots };
|
|
122
|
+
}
|
|
123
|
+
// mdast-util-directive carries the `[label]` as a paragraph whose `data.directiveLabel` is set.
|
|
124
|
+
function isDirectiveLabel(node) {
|
|
125
|
+
return Boolean(node.data?.directiveLabel);
|
|
126
|
+
}
|
|
127
|
+
function readLabel(root) {
|
|
128
|
+
for (const child of root.children) {
|
|
129
|
+
const p = child;
|
|
130
|
+
if (p.type === 'paragraph' && p.data?.directiveLabel)
|
|
131
|
+
return (p.children ?? []).map((c) => c.value ?? '').join('');
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
function readListItems(children) {
|
|
136
|
+
const list = children.find((c) => c.type === 'list');
|
|
137
|
+
if (!list?.children)
|
|
138
|
+
return [];
|
|
139
|
+
return list.children.map((li) => childrenToText(li.children ?? []));
|
|
140
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentDef, ComponentValues } from './registry.js';
|
|
2
|
+
/** The outcome of preparing a guided-form component for insertion: the markdown to insert, or the
|
|
3
|
+
* field-keyed errors to show on the form. */
|
|
4
|
+
export type ComponentInsert = {
|
|
5
|
+
ok: true;
|
|
6
|
+
markdown: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
errors: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
/** Serialize a component's form values, then validate the result against its schema. Returns the
|
|
12
|
+
* markdown to insert at the cursor, or the field errors keyed by attribute key or slot name. */
|
|
13
|
+
export declare function buildComponentInsert(def: ComponentDef, values: ComponentValues): Promise<ComponentInsert>;
|
|
14
|
+
//# sourceMappingURL=component-insert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-insert.d.ts","sourceRoot":"","sources":["../../src/lib/render/component-insert.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnE;8CAC8C;AAC9C,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAE7G;iGACiG;AACjG,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAI/G"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { serializeComponent } from './component-grammar.js';
|
|
2
|
+
import { validateComponent } from './component-validate.js';
|
|
3
|
+
/** Serialize a component's form values, then validate the result against its schema. Returns the
|
|
4
|
+
* markdown to insert at the cursor, or the field errors keyed by attribute key or slot name. */
|
|
5
|
+
export async function buildComponentInsert(def, values) {
|
|
6
|
+
const markdown = serializeComponent(def, values);
|
|
7
|
+
const verdict = await validateComponent(markdown, def);
|
|
8
|
+
return verdict.ok ? { ok: true, markdown } : { ok: false, errors: verdict.errors };
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ComponentRegistry } from './registry.js';
|
|
2
|
+
export interface ReferenceOptions {
|
|
3
|
+
/** The H1 title of the reference document. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** The one-line blockquote summary under the title. */
|
|
6
|
+
summary: string;
|
|
7
|
+
}
|
|
8
|
+
/** Build a self-contained markdown reference (the llms-full.txt shape) for a component registry, for
|
|
9
|
+
* authors and for pointing an LLM at one curated file. */
|
|
10
|
+
export declare function generateComponentReference(registry: ComponentRegistry, opts: ReferenceOptions): string;
|
|
11
|
+
//# sourceMappingURL=component-reference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-reference.d.ts","sourceRoot":"","sources":["../../src/lib/render/component-reference.ts"],"names":[],"mappings":"AACA,OAAO,EAAkC,KAAK,iBAAiB,EAAwB,MAAM,eAAe,CAAC;AAE7G,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;2DAC2D;AAC3D,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAGtG"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { serializeComponent } from './component-grammar.js';
|
|
2
|
+
import { emptyValues } from './registry.js';
|
|
3
|
+
/** Build a self-contained markdown reference (the llms-full.txt shape) for a component registry, for
|
|
4
|
+
* authors and for pointing an LLM at one curated file. */
|
|
5
|
+
export function generateComponentReference(registry, opts) {
|
|
6
|
+
const sections = registry.defs.map((def) => componentSection(def));
|
|
7
|
+
return `# ${opts.title}\n\n> ${opts.summary}\n\n${sections.join('\n\n')}\n`;
|
|
8
|
+
}
|
|
9
|
+
function componentSection(def) {
|
|
10
|
+
const lines = [`## ${def.label} (\`:::${def.name}\`)`, '', def.description ?? ''];
|
|
11
|
+
if (def.use)
|
|
12
|
+
lines.push('', `**When to use:** ${def.use}`);
|
|
13
|
+
lines.push('', '```', serializeComponent(def, exampleValues(def)), '```');
|
|
14
|
+
return lines.join('\n');
|
|
15
|
+
}
|
|
16
|
+
/** Seed example values that show every declared field: an ellipsis for strings, one sample list item. */
|
|
17
|
+
function exampleValues(def) {
|
|
18
|
+
const values = emptyValues(def);
|
|
19
|
+
for (const field of def.attributes ?? []) {
|
|
20
|
+
if (field.type === 'boolean')
|
|
21
|
+
values.attributes[field.key] = false;
|
|
22
|
+
else
|
|
23
|
+
values.attributes[field.key] = field.options?.[0] ?? '…';
|
|
24
|
+
}
|
|
25
|
+
for (const slot of def.slots ?? []) {
|
|
26
|
+
if (slot.kind === 'repeatable')
|
|
27
|
+
values.slots[slot.name] = ['…'];
|
|
28
|
+
else if (slot.name === 'title')
|
|
29
|
+
values.slots[slot.name] = 'Title';
|
|
30
|
+
else
|
|
31
|
+
values.slots[slot.name] = '…';
|
|
32
|
+
}
|
|
33
|
+
return values;
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentDef } from './registry.js';
|
|
2
|
+
/** A validation verdict: ok, or field-keyed error messages. */
|
|
3
|
+
export type ComponentValidation = {
|
|
4
|
+
ok: true;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
errors: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
export declare function validateComponent(markdown: string, def: ComponentDef): Promise<ComponentValidation>;
|
|
10
|
+
//# sourceMappingURL=component-validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-validate.d.ts","sourceRoot":"","sources":["../../src/lib/render/component-validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,+DAA+D;AAC/D,MAAM,MAAM,mBAAmB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAE/F,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6BzG"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseComponent, parseRawAttributeKeys } from './component-grammar.js';
|
|
2
|
+
export async function validateComponent(markdown, def) {
|
|
3
|
+
const values = await parseComponent(markdown, def);
|
|
4
|
+
const errors = {};
|
|
5
|
+
const declared = new Set((def.attributes ?? []).map((f) => f.key));
|
|
6
|
+
for (const field of def.attributes ?? []) {
|
|
7
|
+
const v = values.attributes[field.key];
|
|
8
|
+
const filled = field.type === 'boolean' ? true : typeof v === 'string' && v !== '';
|
|
9
|
+
if (field.required && !filled) {
|
|
10
|
+
errors[field.key] = `${field.label} is required.`;
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (field.type === 'select' && typeof v === 'string' && v !== '' && !(field.options ?? []).includes(v)) {
|
|
14
|
+
errors[field.key] = `${field.label} must be one of: ${(field.options ?? []).join(', ')}.`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const key of parseRawAttributeKeys(markdown, def)) {
|
|
18
|
+
if (!declared.has(key))
|
|
19
|
+
errors[key] = `Unknown attribute "${key}".`;
|
|
20
|
+
}
|
|
21
|
+
for (const slot of def.slots ?? []) {
|
|
22
|
+
if (!slot.required)
|
|
23
|
+
continue;
|
|
24
|
+
const v = values.slots[slot.name];
|
|
25
|
+
const filled = Array.isArray(v) ? v.length > 0 : typeof v === 'string' && v !== '';
|
|
26
|
+
if (!filled)
|
|
27
|
+
errors[slot.name] = `${slot.label} is required.`;
|
|
28
|
+
}
|
|
29
|
+
return Object.keys(errors).length ? { ok: false, errors } : { ok: true };
|
|
30
|
+
}
|