@lupinum/ginko-content 0.3.0-rc.4 → 0.3.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.
Files changed (157) hide show
  1. package/README.md +4 -5
  2. package/dist/cms-contract/index.d.ts +2 -2
  3. package/dist/cms-contract/index.js +1 -2
  4. package/dist/cms-contract/mdc.js +6 -4
  5. package/dist/cms-contract/path.d.ts +1 -1
  6. package/dist/cms-contract/path.js +0 -1
  7. package/dist/cms-contract/render-policy.js +4 -0
  8. package/dist/cms-contract/schema.d.ts +1 -1
  9. package/dist/cms-contract/schema.js +3 -1
  10. package/dist/core/content/graph.d.ts +7 -5
  11. package/dist/core/content/graph.js +33 -40
  12. package/dist/core/content/locale.d.ts +3 -1
  13. package/dist/core/content/locale.js +1 -0
  14. package/dist/core/content/path.d.ts +26 -1
  15. package/dist/core/content/path.js +43 -13
  16. package/dist/core/content/structural.d.ts +1 -2
  17. package/dist/core/content/structural.js +1 -1
  18. package/dist/core/data-source-error.d.ts +5 -0
  19. package/dist/core/data-source-error.js +10 -0
  20. package/dist/core/errors.d.ts +1 -1
  21. package/dist/core/markdown/normalize-comark.d.ts +6 -0
  22. package/dist/core/markdown/normalize-comark.js +15 -0
  23. package/dist/core/markdown/parse-comark.d.ts +3 -0
  24. package/dist/core/markdown/parse-comark.js +30 -0
  25. package/dist/core/provider-route-record.d.ts +18 -0
  26. package/dist/core/provider-route-record.js +106 -0
  27. package/dist/core/query/execute.d.ts +6 -6
  28. package/dist/core/query/execute.js +46 -67
  29. package/dist/core/query/limits.d.ts +2 -0
  30. package/dist/core/query/limits.js +2 -0
  31. package/dist/core/query/lower.d.ts +9 -3
  32. package/dist/core/query/lower.js +170 -89
  33. package/dist/core/query/plan.d.ts +88 -97
  34. package/dist/core/references/resolve.d.ts +1 -1
  35. package/dist/core/references/resolve.js +31 -13
  36. package/dist/core/references/schema.d.ts +2 -0
  37. package/dist/core/references/schema.js +6 -8
  38. package/dist/core/runtime-diagnostics.d.ts +7 -0
  39. package/dist/core/runtime-diagnostics.js +9 -0
  40. package/dist/features/agent/walker.js +10 -7
  41. package/dist/features/collections/resolve.d.ts +0 -2
  42. package/dist/features/collections/resolve.js +2 -3
  43. package/dist/features/localization/config.d.ts +8 -0
  44. package/dist/features/localization/config.js +3 -1
  45. package/dist/features/localization/locale-policy.d.ts +10 -2
  46. package/dist/features/localization/locale-policy.js +81 -35
  47. package/dist/features/localization/path.d.ts +0 -10
  48. package/dist/features/localization/path.js +1 -23
  49. package/dist/features/localization/reference-path.d.ts +7 -0
  50. package/dist/features/localization/reference-path.js +10 -0
  51. package/dist/features/localization/results.d.ts +3 -26
  52. package/dist/features/localization/results.js +10 -34
  53. package/dist/features/localization/route-projector.d.ts +17 -9
  54. package/dist/features/localization/route-projector.js +35 -29
  55. package/dist/features/navigation/build.js +2 -1
  56. package/dist/features/navigation/canonical.d.ts +2 -8
  57. package/dist/features/navigation/canonical.js +8 -48
  58. package/dist/features/navigation/diagnostics.d.ts +11 -0
  59. package/dist/features/navigation/diagnostics.js +52 -0
  60. package/dist/features/navigation/resolve.d.ts +18 -2
  61. package/dist/features/navigation/resolve.js +28 -5
  62. package/dist/features/query/context.d.ts +4 -2
  63. package/dist/features/query/diagnostics.d.ts +4 -0
  64. package/dist/features/query/diagnostics.js +22 -0
  65. package/dist/features/query/documents.js +6 -10
  66. package/dist/features/query/localized-docs.d.ts +0 -7
  67. package/dist/features/query/localized-docs.js +5 -30
  68. package/dist/features/query/navigation.js +2 -11
  69. package/dist/features/query/pagination.js +3 -6
  70. package/dist/features/query/populate.d.ts +1 -0
  71. package/dist/features/query/populate.js +76 -13
  72. package/dist/features/query/query-plan-boundary.d.ts +9 -0
  73. package/dist/features/query/query-plan-boundary.js +137 -0
  74. package/dist/features/query/responses.d.ts +3 -0
  75. package/dist/features/query/responses.js +4 -4
  76. package/dist/features/query/routes.d.ts +3 -10
  77. package/dist/features/query/routes.js +23 -13
  78. package/dist/integrations/nitro/build.js +14 -14
  79. package/dist/integrations/nitro/ingest.js +14 -5
  80. package/dist/integrations/nitro/preview.js +23 -6
  81. package/dist/module.d.mts +2 -0
  82. package/dist/module.json +1 -1
  83. package/dist/module.mjs +27 -14
  84. package/dist/parsers/markdown.js +5 -6
  85. package/dist/parsers/path-meta.js +13 -6
  86. package/dist/portability/mdc.js +9 -4
  87. package/dist/public/client.d.ts +1 -0
  88. package/dist/public/data-source.d.ts +29 -16
  89. package/dist/public/data-source.js +3 -0
  90. package/dist/public/navigation.d.ts +7 -0
  91. package/dist/public/navigation.js +8 -0
  92. package/dist/public/provider-binder.js +62 -32
  93. package/dist/public/provider-contract.d.ts +15 -4
  94. package/dist/public/provider-document.d.ts +19 -17
  95. package/dist/public/provider-document.js +4 -12
  96. package/dist/public/provider-query.d.ts +63 -30
  97. package/dist/public/provider-query.js +44 -14
  98. package/dist/public/provider.d.ts +1 -1
  99. package/dist/public/server.d.ts +2 -2
  100. package/dist/public/server.js +0 -1
  101. package/dist/runtime/app/components/ContentRendererInline.vue +2 -2
  102. package/dist/runtime/app/composables/utils.js +3 -0
  103. package/dist/runtime/server/agent-markdown.js +9 -54
  104. package/dist/runtime/server/agent-site.js +12 -24
  105. package/dist/runtime/server/api/locales.js +1 -1
  106. package/dist/runtime/server/api/navigation.js +15 -11
  107. package/dist/runtime/server/api/query.d.ts +1 -1
  108. package/dist/runtime/server/api/query.js +2 -1
  109. package/dist/runtime/server/api/revalidate.js +2 -2
  110. package/dist/runtime/server/cache-adapter.js +2 -2
  111. package/dist/runtime/server/cache-adapters.d.ts +2 -12
  112. package/dist/runtime/server/cache-adapters.js +0 -9
  113. package/dist/runtime/server/collection-helpers.js +16 -19
  114. package/dist/runtime/server/index.d.ts +1 -1
  115. package/dist/runtime/server/index.js +0 -1
  116. package/dist/runtime/server/middleware/agent-link-headers.js +5 -2
  117. package/dist/runtime/server/middleware/preview.d.ts +2 -0
  118. package/dist/runtime/server/middleware/preview.js +7 -0
  119. package/dist/runtime/server/navigation-query.d.ts +7 -2
  120. package/dist/runtime/server/navigation-query.js +60 -16
  121. package/dist/runtime/server/pagefind.js +8 -9
  122. package/dist/runtime/server/provider-query.d.ts +14 -17
  123. package/dist/runtime/server/provider-query.js +108 -105
  124. package/dist/runtime/server/provider-result.d.ts +2 -2
  125. package/dist/runtime/server/provider-route-facts.js +68 -90
  126. package/dist/runtime/server/providers/filesystem.js +43 -62
  127. package/dist/runtime/server/providers/index.js +5 -5
  128. package/dist/runtime/server/query-api.js +12 -5
  129. package/dist/runtime/server/query-executor.d.ts +2 -2
  130. package/dist/runtime/server/query-executor.js +6 -2
  131. package/dist/runtime/server/query-http-validation.d.ts +8 -22
  132. package/dist/runtime/server/query-http-validation.js +91 -341
  133. package/dist/storage/graph.js +6 -1
  134. package/dist/storage/references.js +10 -4
  135. package/dist/storage/snapshot-runtime.js +6 -1
  136. package/dist/storage/validation.d.ts +5 -2
  137. package/dist/storage/validation.js +48 -6
  138. package/dist/testing/data-source-contract.d.ts +19 -0
  139. package/dist/testing/data-source-contract.js +32 -3
  140. package/dist/testing/provider-contract.js +4 -11
  141. package/dist/testing/provider-fixture.d.ts +8 -2
  142. package/dist/testing/provider-fixture.js +86 -17
  143. package/dist/types/api.d.ts +10 -0
  144. package/dist/types/config.d.ts +2 -9
  145. package/dist/types/config.js +2 -2
  146. package/dist/types/fields.d.ts +1 -2
  147. package/dist/types/fields.js +28 -19
  148. package/dist/types/module.d.ts +3 -3
  149. package/dist/types/navigation.d.ts +13 -0
  150. package/dist/types/navigation.js +28 -0
  151. package/dist/types/query-parts/public.d.ts +4 -3
  152. package/dist/types/query-parts/results.d.ts +0 -1
  153. package/dist/types/reference.d.ts +14 -1
  154. package/dist/types/reference.js +13 -1
  155. package/dist/types/runtime.d.ts +4 -15
  156. package/dist/web-types.json +1 -1
  157. package/package.json +6 -1
package/README.md CHANGED
@@ -10,8 +10,7 @@ Use it when you want content files to stay simple, but your Nuxt app still
10
10
  needs explicit APIs for route resolution, typed frontmatter, localized content,
11
11
  and server-side reads.
12
12
 
13
- This README describes the `0.3` prerelease. Install it from npm's `next`
14
- channel; `latest` remains `0.2.1` until the stable cutover.
13
+ This README describes the stable `0.3` line.
15
14
 
16
15
  ## Requirements
17
16
 
@@ -23,14 +22,14 @@ channel; `latest` remains `0.2.1` until the stable cutover.
23
22
  ## Install
24
23
 
25
24
  ```bash
26
- npx nuxi module add @lupinum/ginko-content@next
25
+ npx nuxi module add @lupinum/ginko-content
27
26
  ```
28
27
 
29
28
  The Nuxt CLI installs the package and registers the module in `nuxt.config.ts`.
30
29
  If you prefer to install by hand:
31
30
 
32
31
  ```bash
33
- pnpm add @lupinum/ginko-content@next
32
+ pnpm add @lupinum/ginko-content
34
33
  ```
35
34
 
36
35
  ```ts
@@ -79,7 +78,7 @@ Render the current route through the collection:
79
78
  ```vue
80
79
  <!-- pages/[...slug].vue -->
81
80
  <script setup lang="ts">
82
- import { pages } from '~/content.config'
81
+ import { pages } from '~~/content.config'
83
82
 
84
83
  const { page } = await useContentPage(pages)
85
84
  </script>
@@ -22,8 +22,8 @@
22
22
  */
23
23
  export { RESOLVED_CONTENT_CONTRACT_VERSION, buildResolvedContentContract, type BuildResolvedContentContractInput, type BuildResolvedContentContractOptions, } from './build.js';
24
24
  export type { PortableComponentPolicyV1, PortableMediaType, ResolvedContentCollectionV1, ResolvedContentContractV1, ResolvedContentFieldTypeV1, ResolvedContentFieldV1, ResolvedContentValidationV1, ContentCmsCollectionConfig, ContentCmsFieldConfig, ContentCmsFieldType, ContentCmsRelationConfig, ContentCollectionConfig, ContentCollectionI18nConfig, } from './types.js';
25
- export { describeId, generatePath, generateCanonicalKey, generateTitle, isDraftPath, isPartialPath, longestMountForPath, mountContentPath, normalizeContentPath, normalizeRouteMounts, prefixPathWithLocale, refineUrlPart, routeRemainder, routeToContentPathCandidates, slugifyUrlSegment, stripLocalePrefix, } from './path.js';
26
- export { CONTENT_FIELD_METADATA_KEY, CONTENT_REFERENCE_PREFIX, collectTopLevelReferenceFields, getContentFieldMetadata, getObjectShape, getReferenceDescriptor, getSchemaDef, getSchemaTypeName, unwrapSchema, type ContentFieldMetadata, type ContentFieldSchema, } from './schema.js';
25
+ export { describeId, generatePath, generateCanonicalKey, generateTitle, isDraftPath, isPartialPath, mountContentPath, normalizeContentPath, normalizeRouteMounts, prefixPathWithLocale, refineUrlPart, routeRemainder, routeToContentPathCandidates, slugifyUrlSegment, stripLocalePrefix, } from './path.js';
26
+ export { CONTENT_FIELD_METADATA_KEY, CONTENT_REFERENCE_METADATA_KEY, collectTopLevelReferenceFields, getContentFieldMetadata, getObjectShape, getReferenceDescriptor, getSchemaDef, getSchemaTypeName, unwrapSchema, type ContentFieldMetadata, type ContentFieldSchema, } from './schema.js';
27
27
  export { parseMdcBody, type ParseMdcBodyOptions, type ParseMdcBodyResult, } from './mdc.js';
28
28
  export { assertPublicMarkdownAst, isSafePublicMarkdownUrl, PublicMarkdownValidationError, validatePublicMarkdownAst, type PublicMarkdownIssue, type PublicMarkdownIssueCode, type PublicMarkdownValidationResult, } from './render-policy.js';
29
29
  export { verifyPublicImageBytes, type VerifiedPublicImage, } from './asset-bytes.js';
@@ -9,7 +9,6 @@ export {
9
9
  generateTitle,
10
10
  isDraftPath,
11
11
  isPartialPath,
12
- longestMountForPath,
13
12
  mountContentPath,
14
13
  normalizeContentPath,
15
14
  normalizeRouteMounts,
@@ -22,7 +21,7 @@ export {
22
21
  } from "./path.js";
23
22
  export {
24
23
  CONTENT_FIELD_METADATA_KEY,
25
- CONTENT_REFERENCE_PREFIX,
24
+ CONTENT_REFERENCE_METADATA_KEY,
26
25
  collectTopLevelReferenceFields,
27
26
  getContentFieldMetadata,
28
27
  getObjectShape,
@@ -1,9 +1,11 @@
1
- import { parse } from "comark";
1
+ import { normalizeComarkNodes } from "../core/markdown/normalize-comark.js";
2
+ import { parseComark } from "../core/markdown/parse-comark.js";
2
3
  import { mapMarkdownNodes, toMarkdownRoot } from "../core/markdown/tree.js";
3
4
  export async function parseMdcBody(raw, options = {}) {
4
- const tree = await parse(raw ?? "");
5
- const toc = deriveToc(tree.nodes, options);
6
- const body = toMarkdownRoot(tree.nodes, toc);
5
+ const tree = await parseComark(raw ?? "");
6
+ const nodes = normalizeComarkNodes(tree.nodes);
7
+ const toc = deriveToc(nodes, options);
8
+ const body = toMarkdownRoot(nodes, toc);
7
9
  const searchText = renderPlainText(body);
8
10
  return { body, toc, searchText };
9
11
  }
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * All re-exports MUST be runtime-pure (no Node, no Nuxt, no h3). They are.
8
8
  */
9
- export { describeId, generatePath, generateCanonicalKey, generateTitle, isDraftPath, isPartialPath, normalizeContentPath, normalizeRouteMounts, longestMountForPath, routeRemainder, mountContentPath, prefixPathWithLocale, stripLocalePrefix, refineUrlPart, routeToContentPathCandidates, } from '../core/content/path.js';
9
+ export { describeId, generatePath, generateCanonicalKey, generateTitle, isDraftPath, isPartialPath, normalizeContentPath, normalizeRouteMounts, routeRemainder, mountContentPath, prefixPathWithLocale, stripLocalePrefix, refineUrlPart, routeToContentPathCandidates, } from '../core/content/path.js';
10
10
  export { slugifyUrlSegment } from '../core/content/slug.js';
@@ -7,7 +7,6 @@ export {
7
7
  isPartialPath,
8
8
  normalizeContentPath,
9
9
  normalizeRouteMounts,
10
- longestMountForPath,
11
10
  routeRemainder,
12
11
  mountContentPath,
13
12
  prefixPathWithLocale,
@@ -226,6 +226,10 @@ export function validatePublicMarkdownAst(value, policy = { components: {} }) {
226
226
  continue;
227
227
  }
228
228
  if (name === "style" && tag === "span" && isSafeShikiStyle(propValue)) continue;
229
+ if (tag === "blockquote" && name === "data-alert") {
230
+ if (typeof propValue !== "string" || !["note", "tip", "important", "warning", "caution"].includes(propValue)) report("invalid_prop_value", propPath, "Blockquote alert metadata is invalid.");
231
+ continue;
232
+ }
229
233
  if (!name || /^on/i.test(name) || /^v-|^@|^:|^#/.test(name) || FORBIDDEN_PROPS.has(lower)) {
230
234
  report("unsafe_prop", propPath, `Property "${name}" is not render-safe.`);
231
235
  continue;
@@ -15,4 +15,4 @@ export { getSchemaDef, getSchemaTypeName, unwrapSchema, getObjectShape, getRefer
15
15
  */
16
16
  export { CONTENT_FIELD_METADATA_KEY, type ContentFieldMetadata, type ContentFieldSchema, } from '../types/fields.js';
17
17
  export { getContentFieldMetadata } from '../types/fields.js';
18
- export { CONTENT_REFERENCE_PREFIX } from '../types/reference.js';
18
+ export { CONTENT_REFERENCE_METADATA_KEY, } from '../types/reference.js';
@@ -10,4 +10,6 @@ export {
10
10
  CONTENT_FIELD_METADATA_KEY
11
11
  } from "../types/fields.js";
12
12
  export { getContentFieldMetadata } from "../types/fields.js";
13
- export { CONTENT_REFERENCE_PREFIX } from "../types/reference.js";
13
+ export {
14
+ CONTENT_REFERENCE_METADATA_KEY
15
+ } from "../types/reference.js";
@@ -14,7 +14,7 @@
14
14
  * Canonical keys are only unique within a collection.
15
15
  * - **`byCanonical`**: a derived convenience index containing only keys
16
16
  * that occur in one collection. Ambiguous unscoped lookups fail closed.
17
- * - **`byRoute`**: route path (user-visible URL) content id. Used by the
17
+ * - **`byRoute`**: locale plus route path → canonical key. Used by the
18
18
  * page resolver when the request arrives as a localized URL.
19
19
  * - **`byRef`**: normalized ref string → canonical key. This is how
20
20
  * markdown links such as `[Ada]($authors.ada)` find their target without a scan.
@@ -28,8 +28,8 @@
28
28
  */
29
29
  import type { ParsedContent } from '../../types/content';
30
30
  import type { ContentLocaleEntry } from '../../types/query';
31
- import type { ContentManifest, ManifestVariant, ResolvedVariant } from '../../types/runtime';
32
- export interface ContentGraphVariant extends ManifestVariant {
31
+ import type { ContentVariantIdentity, ResolvedVariant } from '../../types/runtime';
32
+ export interface ContentGraphVariant extends ContentVariantIdentity {
33
33
  document: ParsedContent;
34
34
  }
35
35
  export interface ContentGraph {
@@ -44,7 +44,9 @@ export interface ContentGraph {
44
44
  byCollectionCanonical: Record<string, Record<string, Record<string, ContentGraphVariant>>>;
45
45
  /** Unambiguous canonical keys only. Derived from `byCollectionCanonical`. */
46
46
  byCanonical: Record<string, Record<string, ContentGraphVariant>>;
47
- /** route pathcontent id. Used by page resolution. */
47
+ /** Exact collectionref canonical key alias index. */
48
+ byCollectionRef: Record<string, Record<string, string>>;
49
+ /** `"<locale>:<route path>"` → canonical key. Used by page resolution. */
48
50
  byRoute: Record<string, string>;
49
51
  /** normalized ref string → canonical key. */
50
52
  byRef: Record<string, string>;
@@ -54,7 +56,6 @@ export interface ContentGraph {
54
56
  referenceTargets: Map<string, string>;
55
57
  /** Exact collection-scoped user-writable reference targets. */
56
58
  referenceTargetsByCollection: Record<string, Map<string, string>>;
57
- manifest: ContentManifest;
58
59
  }
59
60
  /**
60
61
  * Build a `ContentGraph` from a flat list of parsed documents.
@@ -71,6 +72,7 @@ export interface ContentGraph {
71
72
  export declare const buildContentGraph: (documents: ParsedContent[], options?: {
72
73
  locales?: string[];
73
74
  defaultLocale?: string;
75
+ referencePathAliases?: (document: ParsedContent) => readonly string[];
74
76
  }) => ContentGraph;
75
77
  /** Resolve the exact collection-scoped variant set, or an unambiguous one. */
76
78
  export declare const getGraphCanonicalVariants: (graph: ContentGraph, canonicalKey: string, collection?: string) => Record<string, ContentGraphVariant> | undefined;
@@ -7,20 +7,15 @@ const normalizePath = (path) => {
7
7
  }
8
8
  return path.startsWith("/") ? path.endsWith("/") ? path.slice(0, -1) || "/" : path : `/${path.replace(/\/+$/, "")}`;
9
9
  };
10
- const emptyManifest = () => ({
11
- byCollectionCanonical: {},
12
- byCanonical: {},
13
- byCollectionRef: {},
14
- byRef: {},
15
- byRoute: {},
16
- paths: {},
17
- collections: {}
18
- });
19
10
  export const buildContentGraph = (documents, options = {}) => {
20
- const manifest = emptyManifest();
21
11
  const byId = {};
12
+ const byCollection = {};
13
+ const byPath = {};
22
14
  const byCollectionCanonical = {};
23
15
  const byCanonical = {};
16
+ const byCollectionRef = {};
17
+ const byRef = {};
18
+ const byRoute = {};
24
19
  const byNavigationPath = {};
25
20
  const defaultLocale = options.defaultLocale || "";
26
21
  for (const document of documents) {
@@ -30,15 +25,15 @@ export const buildContentGraph = (documents, options = {}) => {
30
25
  continue;
31
26
  }
32
27
  const path = normalizePath(document.path);
33
- manifest.paths[path] ||= [];
28
+ byPath[path] ||= [];
34
29
  if (document.locale === defaultLocale) {
35
- manifest.paths[path].unshift(documentId);
30
+ byPath[path].unshift(documentId);
36
31
  } else {
37
- manifest.paths[path].push(documentId);
32
+ byPath[path].push(documentId);
38
33
  }
39
34
  if (document.collection) {
40
- manifest.collections[document.collection] ||= [];
41
- manifest.collections[document.collection].push(documentId);
35
+ byCollection[document.collection] ||= [];
36
+ byCollection[document.collection].push(documentId);
42
37
  }
43
38
  if (isNavigationFile(document)) {
44
39
  const locale2 = document.locale || defaultLocale;
@@ -58,45 +53,43 @@ export const buildContentGraph = (documents, options = {}) => {
58
53
  path,
59
54
  document
60
55
  };
61
- manifest.byCollectionCanonical[collection] ||= {};
62
- manifest.byCollectionCanonical[collection][document.canonicalKey] ||= {};
63
- manifest.byCollectionCanonical[collection][document.canonicalKey][locale] = variant;
64
- manifest.byRoute[`${locale}:${path}`] = document.canonicalKey;
56
+ byCollectionCanonical[collection] ||= {};
57
+ byCollectionCanonical[collection][document.canonicalKey] ||= {};
58
+ byCollectionCanonical[collection][document.canonicalKey][locale] = variant;
59
+ byRoute[`${locale}:${path}`] = document.canonicalKey;
65
60
  if (document.type === "markdown" && typeof document.ref === "string" && document.ref.length) {
66
- manifest.byCollectionRef[collection] ||= {};
67
- manifest.byCollectionRef[collection][document.ref] = document.canonicalKey;
61
+ byCollectionRef[collection] ||= {};
62
+ byCollectionRef[collection][document.ref] = document.canonicalKey;
68
63
  }
69
64
  }
70
65
  const collectionsByCanonical = /* @__PURE__ */ new Map();
71
- for (const [collection, canonicalEntries] of Object.entries(manifest.byCollectionCanonical)) {
72
- byCollectionCanonical[collection] = {};
73
- for (const [canonicalKey, variants] of Object.entries(canonicalEntries)) {
74
- const graphVariants = Object.fromEntries(
75
- Object.entries(variants).map(([locale, variant]) => [locale, { ...variant, document: byId[variant.contentId] }])
76
- );
77
- byCollectionCanonical[collection][canonicalKey] = graphVariants;
66
+ for (const [collection, canonicalEntries] of Object.entries(byCollectionCanonical)) {
67
+ for (const canonicalKey of Object.keys(canonicalEntries)) {
78
68
  collectionsByCanonical.set(canonicalKey, [...collectionsByCanonical.get(canonicalKey) || [], collection]);
79
69
  }
80
70
  }
81
71
  for (const [canonicalKey, collections] of collectionsByCanonical) {
82
72
  if (collections.length !== 1) continue;
83
73
  const collection = collections[0];
84
- manifest.byCanonical[canonicalKey] = manifest.byCollectionCanonical[collection][canonicalKey];
85
74
  byCanonical[canonicalKey] = byCollectionCanonical[collection][canonicalKey];
86
75
  }
87
76
  const collectionsByRef = /* @__PURE__ */ new Map();
88
- for (const [collection, refs] of Object.entries(manifest.byCollectionRef)) {
77
+ for (const [collection, refs] of Object.entries(byCollectionRef)) {
89
78
  for (const ref of Object.keys(refs)) {
90
79
  collectionsByRef.set(ref, [...collectionsByRef.get(ref) || [], collection]);
91
80
  }
92
81
  }
93
82
  for (const [ref, collections] of collectionsByRef) {
94
- if (collections.length === 1) manifest.byRef[ref] = manifest.byCollectionRef[collections[0]][ref];
83
+ if (collections.length === 1) byRef[ref] = byCollectionRef[collections[0]][ref];
95
84
  }
96
85
  const referenceTargetsByCollection = Object.fromEntries(
97
- Object.keys(manifest.byCollectionCanonical).map((collection) => [
86
+ Object.keys(byCollectionCanonical).map((collection) => [
98
87
  collection,
99
- buildReferenceTargets(documents.filter((document) => (document.collection || "content") === collection && !document.partial && !isNavigationFile(document)), options.locales || [])
88
+ buildReferenceTargets(
89
+ documents.filter((document) => (document.collection || "content") === collection && !document.partial && !isNavigationFile(document)),
90
+ options.locales || [],
91
+ options.referencePathAliases
92
+ )
100
93
  ])
101
94
  );
102
95
  const referenceTargetCollections = /* @__PURE__ */ new Map();
@@ -112,16 +105,16 @@ export const buildContentGraph = (documents, options = {}) => {
112
105
  return {
113
106
  documents,
114
107
  byId,
115
- byCollection: manifest.collections,
116
- byPath: manifest.paths,
108
+ byCollection,
109
+ byPath,
117
110
  byCollectionCanonical,
118
111
  byCanonical,
119
- byRoute: manifest.byRoute,
120
- byRef: manifest.byRef,
112
+ byCollectionRef,
113
+ byRoute,
114
+ byRef,
121
115
  byNavigationPath,
122
116
  referenceTargets,
123
- referenceTargetsByCollection,
124
- manifest
117
+ referenceTargetsByCollection
125
118
  };
126
119
  };
127
120
  export const getGraphCanonicalVariants = (graph, canonicalKey, collection) => {
@@ -180,7 +173,7 @@ export const resolveGraphCanonicalKey = (graph, identity, collection) => {
180
173
  if (hasCollectionVariant(normalizedIdentity)) {
181
174
  return normalizedIdentity;
182
175
  }
183
- const refCanonicalKey = collection ? graph.manifest.byCollectionRef[collection]?.[normalizedIdentity] : graph.byRef[normalizedIdentity];
176
+ const refCanonicalKey = collection ? graph.byCollectionRef[collection]?.[normalizedIdentity] : graph.byRef[normalizedIdentity];
184
177
  if (refCanonicalKey && hasCollectionVariant(refCanonicalKey)) {
185
178
  return refCanonicalKey;
186
179
  }
@@ -27,7 +27,7 @@ export declare const INLINE_LOCALE_ID_SEPARATOR = "#__locale=";
27
27
  * configured fallbacks, and default locale are ordered by priority and
28
28
  * deduplicated without reordering.
29
29
  */
30
- export declare const resolveLocaleChain: (requestedLocale: string | undefined, defaultLocale?: string, fallback?: Record<string, string[]>) => string[];
30
+ export declare const resolveLocaleChain: (requestedLocale: string | undefined, defaultLocale?: string, fallback?: Readonly<Record<string, readonly string[]>>) => string[];
31
31
  /**
32
32
  * Produce the fallback portion of the canonical locale chain, excluding the
33
33
  * requested locale itself.
@@ -68,3 +68,5 @@ export declare const splitInlineLocaleVariantId: (id: string) => {
68
68
  * override should not fail ingest for the whole document.
69
69
  */
70
70
  export declare const expandDataLocaleVariants: (document: ParsedContent, i18nConfig?: ContentCollectionI18nConfig) => ParsedContent[];
71
+ /** Physical locale identity used when content has no configured locale authority. */
72
+ export declare const DEFAULT_CONTENT_LOCALE = "en";
@@ -94,3 +94,4 @@ export const expandDataLocaleVariants = (document, i18nConfig) => {
94
94
  }
95
95
  return variants;
96
96
  };
97
+ export const DEFAULT_CONTENT_LOCALE = "en";
@@ -29,14 +29,39 @@ export declare const generateCanonicalKey: (parts: string[], { translatedSlugs,
29
29
  export declare const generateTitle: (path: string) => string;
30
30
  export type RouteMounts = Record<string, string>;
31
31
  export declare const normalizeRouteMounts: (route: string | Record<string, string> | undefined, locales?: string[], defaultLocale?: string) => RouteMounts | undefined;
32
- export declare const longestMountForPath: (path: string, mounts: RouteMounts) => [string, string];
33
32
  export declare const routeRemainder: (path: string, mount: string) => string;
33
+ /**
34
+ * Remove a configured route mount from a filesystem-derived path when that
35
+ * exact mount is present. Source layouts are allowed to live elsewhere, so
36
+ * unlike provider unmounting this operation leaves non-matching paths intact.
37
+ */
38
+ export declare const canonicalizeSourcePath: (path: string, mount?: string) => {
39
+ path: string;
40
+ removedSegments: number;
41
+ };
34
42
  export declare const mountContentPath: (remainder: string, locale: string | undefined, mounts?: RouteMounts) => string;
35
43
  export declare const prefixPathWithLocale: (path: string, locale?: string, defaultLocale?: string) => string;
36
44
  export declare const stripLocalePrefix: (path: string, locales?: string[], defaultLocale?: string, explicitLocale?: string) => {
37
45
  locale: string | undefined;
38
46
  path: string;
39
47
  };
48
+ export declare const lowerRouteToCanonicalCandidates: (route: string, requestedLocale: string | undefined, localeChain: readonly string[], defaultLocale: string, locales: readonly string[], mountForLocale: (locale: string) => string) => {
49
+ locale: string;
50
+ contentPath: string;
51
+ }[];
52
+ /**
53
+ * Released CMS-contract adapter returning mounted provider coordinates.
54
+ *
55
+ * This function GUESSES: it unions locales from the mount map, the requested
56
+ * chain, and the default locale, and it substitutes `/` for any locale with no
57
+ * declared mount. The content engine deliberately does not call it — route
58
+ * lowering goes through `lowerRouteToCandidates`
59
+ * (`features/localization/route-projector.ts`), which validates the resolved
60
+ * locale's configured mount and fails instead of substituting one.
61
+ *
62
+ * It exists only so CMS adapters compiled against the released contract keep
63
+ * working. New code should use the resolved collection locale policy.
64
+ */
40
65
  export declare const routeToContentPathCandidates: (route: string, requestedLocale: string | undefined, localeChain: string[], defaultLocale?: string, mounts?: RouteMounts) => {
41
66
  locale: string;
42
67
  path: string;
@@ -103,25 +103,37 @@ export const normalizeRouteMounts = (route, locales = [], defaultLocale) => {
103
103
  const entries = Object.entries(route).filter(([, mount]) => typeof mount === "string" && mount.length > 0).map(([locale, mount]) => [locale, normalizeContentPath(mount)]);
104
104
  return entries.length ? Object.fromEntries(entries) : void 0;
105
105
  };
106
- export const longestMountForPath = (path, mounts) => {
107
- const normalized = normalizeContentPath(path);
108
- return Object.entries(mounts).filter(([, mount]) => normalized === mount || normalized.startsWith(`${mount}/`)).sort((a, b) => b[1].length - a[1].length)[0];
109
- };
110
106
  export const routeRemainder = (path, mount) => {
111
107
  const normalizedPath = normalizeContentPath(path);
112
108
  const normalizedMount = normalizeContentPath(mount);
109
+ if (normalizedMount === "/") {
110
+ return normalizedPath;
111
+ }
113
112
  if (normalizedPath === normalizedMount) {
114
113
  return "/";
115
114
  }
116
115
  return normalizeContentPath(normalizedPath.slice(normalizedMount.length) || "/");
117
116
  };
117
+ export const canonicalizeSourcePath = (path, mount) => {
118
+ const normalizedPath = normalizeContentPath(path);
119
+ const normalizedMount = mount ? normalizeContentPath(mount) : "/";
120
+ const removesMount = normalizedMount !== "/" && (normalizedPath === normalizedMount || normalizedPath.startsWith(`${normalizedMount}/`));
121
+ return {
122
+ path: removesMount ? routeRemainder(normalizedPath, normalizedMount) : normalizedPath,
123
+ removedSegments: removesMount ? normalizedMount.split("/").filter(Boolean).length : 0
124
+ };
125
+ };
118
126
  export const mountContentPath = (remainder, locale, mounts) => {
119
127
  const mount = locale && mounts?.[locale];
120
128
  if (!mount) {
121
129
  return normalizeContentPath(remainder);
122
130
  }
123
131
  const suffix = normalizeContentPath(remainder);
124
- return normalizeContentPath(suffix === "/" ? mount : `${mount}${suffix}`);
132
+ const normalizedMount = normalizeContentPath(mount);
133
+ if (normalizedMount === "/") {
134
+ return suffix;
135
+ }
136
+ return normalizeContentPath(suffix === "/" ? normalizedMount : `${normalizedMount}${suffix}`);
125
137
  };
126
138
  const isLocalePrefixedPath = (path, locales) => {
127
139
  const segments = path.split("/").filter(Boolean);
@@ -142,19 +154,37 @@ export const stripLocalePrefix = (path, locales = [], defaultLocale, explicitLoc
142
154
  const routePath = routeLocale ? normalizeContentPath("/" + segments.slice(1).join("/")) : normalized;
143
155
  return { locale, path: routePath };
144
156
  };
157
+ export const lowerRouteToCanonicalCandidates = (route, requestedLocale, localeChain, defaultLocale, locales, mountForLocale) => {
158
+ const stripped = stripLocalePrefix(route, [...locales], defaultLocale, requestedLocale);
159
+ const activeLocale = stripped.locale || requestedLocale || defaultLocale;
160
+ const activeMount = normalizeContentPath(mountForLocale(activeLocale));
161
+ const path = normalizeContentPath(stripped.path);
162
+ const matchesExpectedMount = activeMount === "/" || path === activeMount || path.startsWith(`${activeMount}/`);
163
+ if (!matchesExpectedMount) {
164
+ return [];
165
+ }
166
+ const contentPath = routeRemainder(path, activeMount);
167
+ return localeChain.map((locale) => ({
168
+ locale,
169
+ contentPath
170
+ }));
171
+ };
145
172
  export const routeToContentPathCandidates = (route, requestedLocale, localeChain, defaultLocale, mounts) => {
146
173
  const locales = Array.from(new Set([...Object.keys(mounts || {}), ...localeChain, defaultLocale].filter(Boolean)));
147
- const stripped = stripLocalePrefix(route, locales, defaultLocale, requestedLocale);
148
174
  if (!mounts) {
175
+ const stripped = stripLocalePrefix(route, locales, defaultLocale, requestedLocale);
149
176
  return localeChain.map((locale) => ({ locale, path: stripped.path }));
150
177
  }
151
- const activeLocale = stripped.locale || requestedLocale || defaultLocale;
152
- const activeMount = activeLocale ? mounts[activeLocale] : void 0;
153
- const matchedMount = activeMount && (stripped.path === activeMount || stripped.path.startsWith(`${activeMount}/`)) ? activeMount : longestMountForPath(stripped.path, mounts)?.[1];
154
- const remainder = matchedMount ? routeRemainder(stripped.path, matchedMount) : stripped.path;
155
- return localeChain.map((locale) => ({
156
- locale,
157
- path: mountContentPath(remainder, locale, mounts)
178
+ return lowerRouteToCanonicalCandidates(
179
+ route,
180
+ requestedLocale,
181
+ localeChain,
182
+ defaultLocale || localeChain[0] || "",
183
+ locales,
184
+ (locale) => mounts[locale] || "/"
185
+ ).map((candidate) => ({
186
+ locale: candidate.locale,
187
+ path: mountContentPath(candidate.contentPath, candidate.locale, mounts)
158
188
  }));
159
189
  };
160
190
  export const pathHasLocalePrefix = isLocalePrefixedPath;
@@ -1,7 +1,6 @@
1
- import type { ParsedContent } from '../../types/content';
2
1
  /**
3
2
  * Module-private source classification. `navigationFile` intentionally does
4
3
  * not belong to the public `ParsedContent` contract: it only exists between
5
4
  * parsing and structural graph/navigation processing.
6
5
  */
7
- export declare const isNavigationFile: (document: ParsedContent) => boolean;
6
+ export declare const isNavigationFile: (document: unknown) => boolean;
@@ -1 +1 @@
1
- export const isNavigationFile = (document) => document.navigationFile === true;
1
+ export const isNavigationFile = (document) => Boolean(document) && typeof document === "object" && document.navigationFile === true;
@@ -0,0 +1,5 @@
1
+ export type ContentDataSourceErrorCode = 'QUERY_CURSOR_INVALID' | 'BACKEND_FAILURE';
2
+ export declare const createContentDataSourceError: (code: ContentDataSourceErrorCode) => Error;
3
+ export declare const isContentDataSourceError: (value: unknown) => value is Error & {
4
+ code: ContentDataSourceErrorCode;
5
+ };
@@ -0,0 +1,10 @@
1
+ class ContentDataSourceError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super(code === "QUERY_CURSOR_INVALID" ? "Content data-source query cursor is invalid." : "Content data-source operation failed.");
5
+ this.name = "ContentDataSourceError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export const createContentDataSourceError = (code) => new ContentDataSourceError(code);
10
+ export const isContentDataSourceError = (value) => value instanceof ContentDataSourceError;
@@ -5,7 +5,7 @@
5
5
  * a `ContentError` with one of the codes below. Boundary code (HTTP handler,
6
6
  * CLI) maps codes to user-facing responses; tests pattern-match on `.code`.
7
7
  */
8
- export type ContentErrorCode = 'PARSE_FAILED' | 'TRANSFORM_FAILED' | 'VALIDATION_FAILED' | 'INVALID_CONTENT' | 'DUPLICATE_CANONICAL_ID' | 'DUPLICATE_LOCALIZED_PATH' | 'CONFLICTING_REFS' | 'INVALID_REF_VALUE' | 'INVALID_NAVIGATION_YAML' | 'SCHEMA_VALIDATION_FAILED' | 'NON_JSON_VALUE' | 'CONFLICTING_COLLECTION_MATCH' | 'TRANSLATED_SLUG_CONFLICT';
8
+ export type ContentErrorCode = 'PARSE_FAILED' | 'TRANSFORM_FAILED' | 'VALIDATION_FAILED' | 'INVALID_CONTENT' | 'DUPLICATE_CANONICAL_ID' | 'DUPLICATE_LOCALIZED_PATH' | 'CONFLICTING_REFS' | 'INVALID_REF_VALUE' | 'INVALID_NAVIGATION_YAML' | 'INVALID_NAVIGATION_METADATA' | 'SCHEMA_VALIDATION_FAILED' | 'NON_JSON_VALUE' | 'CONFLICTING_COLLECTION_MATCH' | 'TRANSLATED_SLUG_CONFLICT';
9
9
  export declare class ContentError extends Error {
10
10
  readonly code: ContentErrorCode;
11
11
  readonly context: Record<string, unknown>;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Normalizes parser-owned Comark metadata into the inert public AST contract.
3
+ * Keep this deliberately narrow: authored structural props such as `as` remain
4
+ * untouched and are rejected by the render and portability policies.
5
+ */
6
+ export declare function normalizeComarkNodes(nodes: unknown[]): unknown[];
@@ -0,0 +1,15 @@
1
+ const GFM_ALERTS = /* @__PURE__ */ new Set(["note", "tip", "important", "warning", "caution"]);
2
+ export function normalizeComarkNodes(nodes) {
3
+ const normalize = (node) => {
4
+ if (!Array.isArray(node)) return node;
5
+ const [tag, rawProps, ...children] = node;
6
+ const props = isRecord(rawProps) ? { ...rawProps } : rawProps;
7
+ if (tag === "blockquote" && isRecord(props) && typeof props.as === "string" && GFM_ALERTS.has(props.as.toLowerCase())) {
8
+ props["data-alert"] = props.as.toLowerCase();
9
+ delete props.as;
10
+ }
11
+ return [tag, props, ...children.map(normalize)];
12
+ };
13
+ return nodes.map(normalize);
14
+ }
15
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -0,0 +1,3 @@
1
+ import type { ComarkPlugin } from 'comark';
2
+ /** The single configured Comark entry point used by every parsing boundary. */
3
+ export declare const parseComark: (markdown: string, plugins?: readonly ComarkPlugin[]) => Promise<import("comark").ComarkTree<Record<string, any>, Record<string, any>>>;
@@ -0,0 +1,30 @@
1
+ import { createParse, defineComarkPlugin, parseFrontmatter } from "comark";
2
+ const typedComponentFrontmatter = defineComarkPlugin(() => ({
3
+ name: "ginko-typed-component-frontmatter",
4
+ markdownItPlugins: [
5
+ (markdown) => {
6
+ markdown.core.ruler.after("block", "ginko_typed_component_frontmatter", (state) => {
7
+ const lines = state.src.split(/\r?\n/);
8
+ for (const token of state.tokens) {
9
+ if (token.type !== "mdc_block_open" || !token.map) continue;
10
+ const [startLine, endLine] = token.map;
11
+ if (lines[startLine + 1]?.trim() !== "---") continue;
12
+ const parsed = parseFrontmatter(lines.slice(startLine + 1, endLine).join("\n"));
13
+ if (!parsed.frontmatterText) continue;
14
+ const yamlEntries = Object.entries(parsed.data);
15
+ const yamlKeys = new Set(yamlEntries.map(([key]) => key));
16
+ token.attrs = [
17
+ ...(token.attrs ?? []).filter(([key]) => !yamlKeys.has(key)),
18
+ ...yamlEntries
19
+ ];
20
+ }
21
+ });
22
+ }
23
+ ]
24
+ }));
25
+ export const parseComark = async (markdown, plugins = []) => {
26
+ const parse = createParse({
27
+ plugins: [typedComponentFrontmatter(), ...plugins]
28
+ });
29
+ return await parse(markdown);
30
+ };
@@ -0,0 +1,18 @@
1
+ import type { ContentProviderRouteFact, ContentRouteRecord } from '../public/provider-contract';
2
+ export declare const CONTENT_ROUTE_LIMITS: Readonly<{
3
+ maxRouteRecordBytes: number;
4
+ maxTotalRouteBytes: number;
5
+ maxSitemapImagesPerRoute: 16;
6
+ maxSitemapImageLocationBytes: number;
7
+ }>;
8
+ export type ContentRouteRecordValidationCode = 'RESPONSE_INVALID' | 'RESULT_LIMIT_EXCEEDED';
9
+ export declare class ContentRouteRecordValidationError extends Error {
10
+ readonly code: ContentRouteRecordValidationCode;
11
+ readonly field: string;
12
+ constructor(code: ContentRouteRecordValidationCode, field: string, message: string);
13
+ }
14
+ export declare const normalizeRawProviderRouteFact: (value: unknown, field?: string) => ContentProviderRouteFact;
15
+ export declare const normalizeRawContentRouteRecord: (value: unknown, field?: string) => {
16
+ record: ContentRouteRecord;
17
+ serializedBytes: number;
18
+ };