@hgdc/docs 0.0.0-stage → 0.1.1

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 (173) hide show
  1. package/bin/hgdc-docs.mjs +48 -0
  2. package/docs.schema.json +294 -0
  3. package/package.json +51 -4
  4. package/src/build/auto-import.ts +74 -0
  5. package/src/build/markdown.ts +114 -0
  6. package/src/build/outputs.ts +91 -0
  7. package/src/build/public-files.ts +35 -0
  8. package/src/components/Accordion.astro +12 -0
  9. package/src/components/AccordionGroup.astro +4 -0
  10. package/src/components/Card.astro +15 -0
  11. package/src/components/CardGroup.astro +8 -0
  12. package/src/components/Check.astro +10 -0
  13. package/src/components/CodeGroup.astro +4 -0
  14. package/src/components/Columns.astro +8 -0
  15. package/src/components/Danger.astro +10 -0
  16. package/src/components/Frame.astro +11 -0
  17. package/src/components/Info.astro +10 -0
  18. package/src/components/Note.astro +10 -0
  19. package/src/components/Step.astro +11 -0
  20. package/src/components/Steps.astro +4 -0
  21. package/src/components/Tab.astro +9 -0
  22. package/src/components/Tabs.astro +9 -0
  23. package/src/components/Tip.astro +10 -0
  24. package/src/components/Warning.astro +10 -0
  25. package/src/core/answer-markdown.ts +109 -0
  26. package/src/core/assistant-api.ts +65 -0
  27. package/src/core/chunk.ts +134 -0
  28. package/src/core/config.ts +153 -0
  29. package/src/core/corpus.ts +42 -0
  30. package/src/core/json-schema.ts +12 -0
  31. package/src/core/llms.ts +95 -0
  32. package/src/core/navigation.ts +88 -0
  33. package/src/core/theme.ts +111 -0
  34. package/src/integration.ts +229 -0
  35. package/src/locals.d.ts +6 -0
  36. package/src/overrides/Assistant.astro +72 -0
  37. package/src/overrides/Head.astro +12 -0
  38. package/src/overrides/PageFrame.astro +15 -0
  39. package/src/overrides/PageTitle.astro +94 -0
  40. package/src/overrides/Sidebar.astro +60 -0
  41. package/src/overrides/route-data.ts +33 -0
  42. package/src/styles/theme.css +773 -0
  43. package/src/virtual.d.ts +6 -0
  44. package/src/widget/client.ts +252 -0
  45. package/template/.astro/collections/docs.schema.json +651 -0
  46. package/template/.astro/content-assets.mjs +1 -0
  47. package/template/.astro/content-modules.mjs +18 -0
  48. package/template/.astro/content.d.ts +179 -0
  49. package/template/.astro/types.d.ts +2 -0
  50. package/template/.build/.well-known/llms.txt +38 -0
  51. package/template/.build/404.html +42 -0
  52. package/template/.build/_astro/Assistant.astro_astro_type_script_index_0_lang.DWgmlr85.js +4 -0
  53. package/template/.build/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.C3fM4Kra.js +1 -0
  54. package/template/.build/_astro/Search.astro_astro_type_script_index_0_lang.g9hoxVJg.js +1 -0
  55. package/template/.build/_astro/TableOfContents.astro_astro_type_script_index_0_lang.BpJaGz7G.js +1 -0
  56. package/template/.build/_astro/common.8GHCkW7i.css +3 -0
  57. package/template/.build/_astro/ec.0vx5m.js +3 -0
  58. package/template/.build/_astro/ec.8nqtp.css +1 -0
  59. package/template/.build/_astro/geist-cyrillic-ext-wght-normal.DjL33-gN.woff2 +0 -0
  60. package/template/.build/_astro/geist-cyrillic-wght-normal.BEAKL7Jp.woff2 +0 -0
  61. package/template/.build/_astro/geist-latin-ext-wght-normal.DC-KSUi6.woff2 +0 -0
  62. package/template/.build/_astro/geist-latin-wght-normal.BgDaEnEv.woff2 +0 -0
  63. package/template/.build/_astro/geist-vietnamese-wght-normal.6IgcOCM7.woff2 +0 -0
  64. package/template/.build/_astro/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
  65. package/template/.build/_astro/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
  66. package/template/.build/_astro/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
  67. package/template/.build/_astro/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
  68. package/template/.build/_astro/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
  69. package/template/.build/_astro/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
  70. package/template/.build/_astro/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
  71. package/template/.build/_astro/logo-clair.BqNh6JNi.svg +1 -0
  72. package/template/.build/_astro/logo-sombre.BkgsFXxh.svg +1 -0
  73. package/template/.build/_astro/page.cNngjUgL.js +1 -0
  74. package/template/.build/_astro/print.ehPL0gv-.css +1 -0
  75. package/template/.build/_astro/starlight-toc.D4UDIxQL.js +1 -0
  76. package/template/.build/_astro/ui-core.Clh5jUUO.js +2 -0
  77. package/template/.build/_hgdc/corpus.json +1 -0
  78. package/template/.build/demarrer/premiere-doc/index.html +84 -0
  79. package/template/.build/demarrer/premiere-doc.md +65 -0
  80. package/template/.build/explications/donnees/index.html +114 -0
  81. package/template/.build/explications/donnees.md +28 -0
  82. package/template/.build/explications/fonctionnement/index.html +92 -0
  83. package/template/.build/explications/fonctionnement.md +32 -0
  84. package/template/.build/guides/agents/index.html +113 -0
  85. package/template/.build/guides/agents.md +40 -0
  86. package/template/.build/guides/assistant/index.html +117 -0
  87. package/template/.build/guides/assistant.md +42 -0
  88. package/template/.build/guides/composants/index.html +97 -0
  89. package/template/.build/guides/composants.md +103 -0
  90. package/template/.build/guides/ecrire/index.html +115 -0
  91. package/template/.build/guides/ecrire.md +63 -0
  92. package/template/.build/guides/navigation/index.html +89 -0
  93. package/template/.build/guides/navigation.md +63 -0
  94. package/template/.build/guides/publier/index.html +86 -0
  95. package/template/.build/guides/publier.md +65 -0
  96. package/template/.build/guides/theme/index.html +86 -0
  97. package/template/.build/guides/theme.md +33 -0
  98. package/template/.build/images/favicon.svg +1 -0
  99. package/template/.build/images/logo-clair.svg +1 -0
  100. package/template/.build/images/logo-sombre.svg +1 -0
  101. package/template/.build/index.html +87 -0
  102. package/template/.build/index.md +35 -0
  103. package/template/.build/llms-full.txt +830 -0
  104. package/template/.build/llms.txt +38 -0
  105. package/template/.build/pagefind/fragment/fr-ch_148313b.pf_fragment +0 -0
  106. package/template/.build/pagefind/fragment/fr-ch_424fd16.pf_fragment +0 -0
  107. package/template/.build/pagefind/fragment/fr-ch_4f8575c.pf_fragment +0 -0
  108. package/template/.build/pagefind/fragment/fr-ch_5a4d77d.pf_fragment +0 -0
  109. package/template/.build/pagefind/fragment/fr-ch_6c36edc.pf_fragment +0 -0
  110. package/template/.build/pagefind/fragment/fr-ch_73d7887.pf_fragment +0 -0
  111. package/template/.build/pagefind/fragment/fr-ch_8234ed3.pf_fragment +0 -0
  112. package/template/.build/pagefind/fragment/fr-ch_8f8a19e.pf_fragment +0 -0
  113. package/template/.build/pagefind/fragment/fr-ch_9f1b981.pf_fragment +0 -0
  114. package/template/.build/pagefind/fragment/fr-ch_a19039d.pf_fragment +0 -0
  115. package/template/.build/pagefind/fragment/fr-ch_d72cc1e.pf_fragment +0 -0
  116. package/template/.build/pagefind/fragment/fr-ch_d732215.pf_fragment +0 -0
  117. package/template/.build/pagefind/fragment/fr-ch_e2c46e3.pf_fragment +0 -0
  118. package/template/.build/pagefind/fragment/fr-ch_e78d58b.pf_fragment +0 -0
  119. package/template/.build/pagefind/fragment/fr-ch_e92f972.pf_fragment +0 -0
  120. package/template/.build/pagefind/index/fr-ch_f94c95d.pf_index +0 -0
  121. package/template/.build/pagefind/pagefind-component-ui.css +1509 -0
  122. package/template/.build/pagefind/pagefind-component-ui.js +56 -0
  123. package/template/.build/pagefind/pagefind-entry.json +1 -0
  124. package/template/.build/pagefind/pagefind-highlight.js +1070 -0
  125. package/template/.build/pagefind/pagefind-modular-ui.css +214 -0
  126. package/template/.build/pagefind/pagefind-modular-ui.js +8 -0
  127. package/template/.build/pagefind/pagefind-ui.css +1 -0
  128. package/template/.build/pagefind/pagefind-ui.js +2 -0
  129. package/template/.build/pagefind/pagefind-worker.js +6 -0
  130. package/template/.build/pagefind/pagefind.fr-ch_fb78ab42961f4.pf_meta +0 -0
  131. package/template/.build/pagefind/pagefind.js +6 -0
  132. package/template/.build/pagefind/wasm.fr-ch.pagefind +0 -0
  133. package/template/.build/pagefind/wasm.unknown.pagefind +0 -0
  134. package/template/.build/reference/api-assistant/index.html +161 -0
  135. package/template/.build/reference/api-assistant.md +60 -0
  136. package/template/.build/reference/cli/index.html +153 -0
  137. package/template/.build/reference/cli.md +31 -0
  138. package/template/.build/reference/docs-json/index.html +192 -0
  139. package/template/.build/reference/docs-json.md +43 -0
  140. package/template/.build/reference/mcp/index.html +151 -0
  141. package/template/.build/reference/mcp.md +49 -0
  142. package/template/astro.config.ts +6 -0
  143. package/template/node_modules/.vite/deps/_metadata.json +74 -0
  144. package/template/node_modules/.vite/deps/astro-B3JHJyiv.js +434 -0
  145. package/template/node_modules/.vite/deps/astro-B3JHJyiv.js.map +1 -0
  146. package/template/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  147. package/template/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  148. package/template/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
  149. package/template/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  150. package/template/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  151. package/template/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  152. package/template/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  153. package/template/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  154. package/template/node_modules/.vite/deps/audit-Do8QH17W.js +1543 -0
  155. package/template/node_modules/.vite/deps/audit-Do8QH17W.js.map +1 -0
  156. package/template/node_modules/.vite/deps/highlight-BLtvqtzO.js +48 -0
  157. package/template/node_modules/.vite/deps/highlight-BLtvqtzO.js.map +1 -0
  158. package/template/node_modules/.vite/deps/icons-C92i61-2.js +42 -0
  159. package/template/node_modules/.vite/deps/icons-C92i61-2.js.map +1 -0
  160. package/template/node_modules/.vite/deps/package.json +3 -0
  161. package/template/node_modules/.vite/deps/rolldown-runtime-C3I60J0A.js +4 -0
  162. package/template/node_modules/.vite/deps/settings-BYmTKrLi.js +189 -0
  163. package/template/node_modules/.vite/deps/settings-BYmTKrLi.js.map +1 -0
  164. package/template/node_modules/.vite/deps/toolbar-CEKhukuK.js +461 -0
  165. package/template/node_modules/.vite/deps/toolbar-CEKhukuK.js.map +1 -0
  166. package/template/node_modules/.vite/deps/ui-library-BO4--ANp.js +1015 -0
  167. package/template/node_modules/.vite/deps/ui-library-BO4--ANp.js.map +1 -0
  168. package/template/node_modules/.vite/deps/window-vo3A6TMn.js +32 -0
  169. package/template/node_modules/.vite/deps/window-vo3A6TMn.js.map +1 -0
  170. package/template/node_modules/.vite/deps/xray-Lf9x6g8P.js +112 -0
  171. package/template/node_modules/.vite/deps/xray-Lf9x6g8P.js.map +1 -0
  172. package/template/src/content.config.ts +21 -0
  173. package/README.md +0 -3
@@ -0,0 +1,153 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * `docs.json` : configuration d'un site HGDC Docs, à la racine du dépôt du client.
5
+ *
6
+ * Sous-ensemble du `docs.json` de Mintlify (name, colors, logo, favicon, navigation en
7
+ * onglets, groupes et versions) pour qu'une doc Mintlify se reprenne sans réécriture,
8
+ * plus trois champs HGDC : `project`, `assistant` et `language`. Les champs Mintlify que
9
+ * nous ne gérons pas sont acceptés et ignorés, jamais refusés.
10
+ */
11
+
12
+ const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i
13
+ /** Chemin de page relatif au dossier de la doc, sans extension : `guides/publier`. */
14
+ const PAGE = /^(?!\/)(?!.*\.\.)[a-z0-9][a-z0-9/_-]*$/i
15
+ /** Identifiant d'un projet de la console (id Convex). */
16
+ const PROJECT_ID_PATTERN = /^[a-z0-9]{8,64}$/
17
+
18
+ const hex = z.string().regex(HEX, "couleur attendue au format #rrggbb")
19
+
20
+ export type NavPageRef = string
21
+ export type NavGroupInput = { group: string; pages: Array<NavPageRef | NavGroupInput>; expanded?: boolean }
22
+
23
+ const pageRef = z.string().regex(PAGE, "chemin de page attendu, sans extension ni « / » initial (ex. guides/publier)")
24
+
25
+ const navGroup: z.ZodType<NavGroupInput> = z.lazy(() =>
26
+ z.object({
27
+ group: z.string().min(1),
28
+ pages: z.array(z.union([pageRef, navGroup])).min(1, "un groupe contient au moins une page"),
29
+ expanded: z.boolean().optional(),
30
+ }),
31
+ )
32
+
33
+ const groupsOrPages = {
34
+ groups: z.array(navGroup).optional(),
35
+ pages: z.array(z.union([pageRef, navGroup])).optional(),
36
+ }
37
+
38
+ const navTab = z.object({ tab: z.string().min(1), ...groupsOrPages })
39
+ const navVersion = z.object({
40
+ version: z.string().min(1).max(32),
41
+ default: z.boolean().optional(),
42
+ tabs: z.array(navTab).optional(),
43
+ ...groupsOrPages,
44
+ })
45
+
46
+ const navigation = z.object({
47
+ versions: z.array(navVersion).optional(),
48
+ tabs: z.array(navTab).optional(),
49
+ ...groupsOrPages,
50
+ })
51
+
52
+ const logo = z.union([
53
+ z.string(),
54
+ z.object({ light: z.string(), dark: z.string().optional(), href: z.string().optional() }),
55
+ ])
56
+
57
+ export const docsConfigSchema = z.object({
58
+ $schema: z.string().optional(),
59
+ name: z.string().min(1).max(80),
60
+ description: z.string().max(300).optional(),
61
+ /** Identifiant du projet dans la console : relie la publication à l'assistant et à l'index. */
62
+ project: z
63
+ .string()
64
+ .regex(PROJECT_ID_PATTERN, "identifiant de projet attendu, copié depuis la console docs.hgdc.ch")
65
+ .optional(),
66
+ language: z.enum(["fr", "en", "de", "it"]).default("fr"),
67
+ colors: z.object({ primary: hex, light: hex.optional(), dark: hex.optional() }).optional(),
68
+ logo: logo.optional(),
69
+ favicon: z.string().optional(),
70
+ navigation,
71
+ navbar: z
72
+ .object({
73
+ links: z
74
+ .array(z.object({ label: z.string(), href: z.url() }))
75
+ .max(5)
76
+ .optional(),
77
+ primary: z.object({ label: z.string(), href: z.url() }).optional(),
78
+ })
79
+ .optional(),
80
+ footer: z.object({ socials: z.record(z.string(), z.url()).optional() }).optional(),
81
+ assistant: z
82
+ .object({
83
+ enabled: z.boolean().default(true),
84
+ /** Adresse de l'API de l'assistant ; par défaut celle de HGDC Docs. */
85
+ endpoint: z.url().optional(),
86
+ })
87
+ .optional(),
88
+ })
89
+
90
+ export type DocsConfigInput = z.input<typeof docsConfigSchema>
91
+ export type DocsConfig = z.output<typeof docsConfigSchema>
92
+
93
+ export type ConfigResult = { ok: true; config: DocsConfig } | { ok: false; errors: Array<string> }
94
+
95
+ /** Lit et valide un `docs.json`. Les erreurs disent où corriger, en français. */
96
+ export function parseDocsConfig(raw: string): ConfigResult {
97
+ let json: unknown
98
+ try {
99
+ json = JSON.parse(raw)
100
+ } catch (error) {
101
+ return { ok: false, errors: [`docs.json n'est pas un JSON valide : ${(error as Error).message}`] }
102
+ }
103
+ const parsed = docsConfigSchema.safeParse(json)
104
+ if (!parsed.success) {
105
+ return {
106
+ ok: false,
107
+ errors: parsed.error.issues.map((issue) => `${issue.path.join(".") || "(racine)"} : ${issue.message}`),
108
+ }
109
+ }
110
+ const structural = checkNavigation(parsed.data)
111
+ return structural.length ? { ok: false, errors: structural } : { ok: true, config: parsed.data }
112
+ }
113
+
114
+ /**
115
+ * Projet annoncé par HGDC Host au build (`HGDC_DOCS_PROJECT`, doc activée en un clic) : il
116
+ * remplit `project` quand docs.json n'en a pas, et doit concorder quand les deux existent.
117
+ */
118
+ export function withHostProject(config: DocsConfig, fromHost: string | undefined): ConfigResult {
119
+ const project = fromHost?.trim()
120
+ if (!project) return { ok: true, config }
121
+ if (!PROJECT_ID_PATTERN.test(project))
122
+ return { ok: false, errors: [`HGDC_DOCS_PROJECT : identifiant de projet invalide (${project.slice(0, 64)})`] }
123
+ if (config.project && config.project !== project)
124
+ return {
125
+ ok: false,
126
+ errors: [
127
+ `project : docs.json porte ${config.project}, HGDC Host annonce ${project}. Retire « project » de docs.json ou recopie celui de la console.`,
128
+ ],
129
+ }
130
+ return { ok: true, config: { ...config, project } }
131
+ }
132
+
133
+ function checkNavigation(config: DocsConfig): Array<string> {
134
+ const errors: Array<string> = []
135
+ const nav = config.navigation
136
+ const shapes = [nav.versions, nav.tabs, nav.groups, nav.pages].filter(Boolean).length
137
+ if (shapes !== 1) errors.push("navigation : utiliser une seule forme parmi versions, tabs, groups ou pages")
138
+ const versions = nav.versions ?? []
139
+ if (versions.filter((v) => v.default).length > 1) errors.push("navigation.versions : une seule version par défaut")
140
+ const ids = versions.map((v) => slugifySegment(v.version))
141
+ if (new Set(ids).size !== ids.length) errors.push("navigation.versions : deux versions ont le même nom")
142
+ return errors
143
+ }
144
+
145
+ /** Segment d'URL stable à partir d'un libellé (« Version 2.x » → « version-2-x »). */
146
+ export function slugifySegment(label: string): string {
147
+ return label
148
+ .normalize("NFD")
149
+ .replace(/[̀-ͯ]/g, "")
150
+ .toLowerCase()
151
+ .replace(/[^a-z0-9]+/g, "-")
152
+ .replace(/^-+|-+$/g, "")
153
+ }
@@ -0,0 +1,42 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Corpus publié avec chaque build, à `/_hgdc/corpus.json` : le contenu exact du site en
5
+ * Markdown, page par page. La console le lit après une publication pour indexer
6
+ * l'assistant ; rien d'autre que ce qui est public n'entre dans l'index.
7
+ */
8
+ export const CORPUS_PATH = "/_hgdc/corpus.json"
9
+ export const CORPUS_FORMAT = 1
10
+ export const MAX_PAGES = 2000
11
+ export const MAX_PAGE_CHARS = 200_000
12
+
13
+ /**
14
+ * Chemin d'une page du site : commence par un seul « / », sans « // », sans « \ » ni
15
+ * caractère de contrôle. Sinon `new URL(chemin, site)` sortirait du site (« //evil.example »).
16
+ */
17
+ export function isSitePath(path: string): boolean {
18
+ // oxlint-disable-next-line no-control-regex -- on refuse justement ces caractères
19
+ return path.startsWith("/") && !path.includes("//") && !/[\\\u0000-\u001f]/.test(path)
20
+ }
21
+
22
+ export const corpusPageSchema = z.object({
23
+ path: z.string().max(512).refine(isSitePath, "chemin du site attendu (/guides/publier/)"),
24
+ title: z.string().max(300),
25
+ description: z.string().max(1000).nullable(),
26
+ version: z.string().max(32).nullable(),
27
+ markdown: z.string().max(MAX_PAGE_CHARS),
28
+ })
29
+
30
+ export const corpusSchema = z.object({
31
+ format: z.literal(CORPUS_FORMAT),
32
+ project: z.string().nullable(),
33
+ name: z.string().max(80),
34
+ language: z.string().max(8),
35
+ builtAt: z.iso.datetime(),
36
+ theme: z.object({ primary: z.string().nullable(), logo: z.string().nullable() }),
37
+ versions: z.array(z.object({ id: z.string().nullable(), label: z.string().nullable(), isDefault: z.boolean() })),
38
+ pages: z.array(corpusPageSchema).max(MAX_PAGES),
39
+ })
40
+
41
+ export type CorpusPage = z.infer<typeof corpusPageSchema>
42
+ export type Corpus = z.infer<typeof corpusSchema>
@@ -0,0 +1,12 @@
1
+ import { z } from "zod"
2
+
3
+ import { docsConfigSchema } from "./config.ts"
4
+
5
+ /** JSON Schema de docs.json, pour l'autocomplétion des éditeurs (`"$schema"`). */
6
+ export function docsJsonSchema(): Record<string, unknown> {
7
+ return {
8
+ ...z.toJSONSchema(docsConfigSchema, { io: "input", unrepresentable: "any" }),
9
+ $id: "https://docs.hgdc.ch/docs.schema.json",
10
+ title: "docs.json de HGDC Docs",
11
+ }
12
+ }
@@ -0,0 +1,95 @@
1
+ import type { NavItem, NavTree } from "./navigation.ts"
2
+ import { pagesOf as pagesIn, pageToPath } from "./navigation.ts"
3
+
4
+ /**
5
+ * `llms.txt` et `llms-full.txt` au format de llmstxt.org : un H1, un résumé en citation,
6
+ * puis une section H2 par onglet (ou groupe), chaque page liée à sa version Markdown.
7
+ * Les versions autres que celle par défaut vont sous « Optional », que l'agent peut sauter.
8
+ */
9
+ export type LlmsPage = { page: string; title: string; description: string | null; markdown: string }
10
+
11
+ export type LlmsInput = {
12
+ name: string
13
+ description: string | null
14
+ site: string | null
15
+ tree: NavTree
16
+ pages: ReadonlyMap<string, LlmsPage>
17
+ }
18
+
19
+ /** `/guides/publier/` → `/guides/publier.md`, `/` → `/index.md`. */
20
+ export function markdownPath(path: string): string {
21
+ const trimmed = path.replace(/\/+$/, "")
22
+ return trimmed ? `${trimmed}.md` : "/index.md"
23
+ }
24
+
25
+ function url(site: string | null, path: string): string {
26
+ return site ? new URL(path, site).toString() : path
27
+ }
28
+
29
+ function link(input: LlmsInput, page: string): string | null {
30
+ const data = input.pages.get(page)
31
+ if (!data) return null
32
+ const target = url(input.site, markdownPath(pageToPath(page)))
33
+ return `- [${data.title}](${target})${data.description ? `: ${data.description}` : ""}`
34
+ }
35
+
36
+ function sectionLines(input: LlmsInput, heading: string, items: Array<NavItem>): Array<string> {
37
+ const lines: Array<string> = []
38
+ const loose: Array<string> = []
39
+ const groups: Array<[string, Array<string>]> = []
40
+ const walk = (list: Array<NavItem>, prefix: string | null, into: Array<string>) => {
41
+ for (const item of list) {
42
+ if (item.type === "page") {
43
+ const line = link(input, item.page)
44
+ if (line) into.push(line)
45
+ } else {
46
+ const label = prefix ? `${prefix} › ${item.label}` : item.label
47
+ const bucket: Array<string> = []
48
+ groups.push([label, bucket])
49
+ walk(item.items, label, bucket)
50
+ }
51
+ }
52
+ }
53
+ walk(items, null, loose)
54
+ if (loose.length) lines.push(`## ${heading}`, "", ...loose, "")
55
+ for (const [label, bucket] of groups) {
56
+ if (!bucket.length) continue
57
+ lines.push(`## ${heading === "Documentation" ? label : `${heading} · ${label}`}`, "", ...bucket, "")
58
+ }
59
+ return lines
60
+ }
61
+
62
+ export function llmsTxt(input: LlmsInput): string {
63
+ const lines = [`# ${input.name}`, ""]
64
+ if (input.description) lines.push(`> ${input.description}`, "")
65
+ lines.push(
66
+ "Chaque page existe en Markdown à la même adresse suivie de `.md`. `llms-full.txt` contient toute la documentation en un seul fichier.",
67
+ "",
68
+ )
69
+ const optional: Array<string> = []
70
+ for (const version of input.tree.versions) {
71
+ for (const tab of version.tabs) {
72
+ if (version.isDefault) {
73
+ lines.push(...sectionLines(input, tab.label ?? "Documentation", tab.items))
74
+ continue
75
+ }
76
+ // Anciennes versions : une liste plate sous « Optional », libellée par version.
77
+ for (const page of pagesIn(tab.items)) {
78
+ const line = link(input, page)
79
+ if (line) optional.push(line.replace("- [", `- [${version.label} · `))
80
+ }
81
+ }
82
+ }
83
+ if (optional.length) lines.push("## Optional", "", ...optional, "")
84
+ return `${lines.join("\n").trim()}\n`
85
+ }
86
+
87
+ export function llmsFullTxt(input: LlmsInput, order: Array<string>): string {
88
+ const parts = [`# ${input.name}`, input.description ? `\n> ${input.description}` : ""]
89
+ for (const page of order) {
90
+ const data = input.pages.get(page)
91
+ if (!data) continue
92
+ parts.push(`\n---\n\nSource : ${url(input.site, pageToPath(page))}\n\n# ${data.title}\n\n${data.markdown.trim()}`)
93
+ }
94
+ return `${parts.join("\n").trim()}\n`
95
+ }
@@ -0,0 +1,88 @@
1
+ import { slugifySegment, type DocsConfig, type NavGroupInput } from "./config.ts"
2
+
3
+ /**
4
+ * Navigation normalisée : toujours versions → onglets → éléments, quelle que soit la forme
5
+ * écrite dans `docs.json` (pages seules, groupes, onglets ou versions, comme Mintlify).
6
+ */
7
+ export type NavItem =
8
+ | { type: "page"; page: string }
9
+ | { type: "group"; label: string; expanded: boolean; items: Array<NavItem> }
10
+
11
+ export type NavTab = { id: string; label: string | null; items: Array<NavItem> }
12
+ export type NavVersion = { id: string | null; label: string | null; isDefault: boolean; tabs: Array<NavTab> }
13
+ export type NavTree = { versions: Array<NavVersion> }
14
+
15
+ type Container = { groups?: Array<NavGroupInput>; pages?: Array<string | NavGroupInput> }
16
+
17
+ function items(container: Container): Array<NavItem> {
18
+ if (container.groups) return container.groups.map(group)
19
+ return (container.pages ?? []).map(entry)
20
+ }
21
+
22
+ function entry(value: string | NavGroupInput): NavItem {
23
+ return typeof value === "string" ? { type: "page", page: value } : group(value)
24
+ }
25
+
26
+ function group(value: NavGroupInput): NavItem {
27
+ return { type: "group", label: value.group, expanded: value.expanded ?? true, items: value.pages.map(entry) }
28
+ }
29
+
30
+ function tabs(container: Container & { tabs?: Array<Container & { tab: string }> }): Array<NavTab> {
31
+ if (container.tabs)
32
+ return container.tabs.map((tab) => ({ id: slugifySegment(tab.tab), label: tab.tab, items: items(tab) }))
33
+ return [{ id: "main", label: null, items: items(container) }]
34
+ }
35
+
36
+ export function normalizeNavigation(config: DocsConfig): NavTree {
37
+ const nav = config.navigation
38
+ if (nav.versions) {
39
+ const hasDefault = nav.versions.some((v) => v.default)
40
+ return {
41
+ versions: nav.versions.map((version, index) => ({
42
+ id: slugifySegment(version.version),
43
+ label: version.version,
44
+ isDefault: version.default ?? (!hasDefault && index === 0),
45
+ tabs: tabs(version),
46
+ })),
47
+ }
48
+ }
49
+ return { versions: [{ id: null, label: null, isDefault: true, tabs: tabs(nav) }] }
50
+ }
51
+
52
+ export function pagesOf(list: Array<NavItem>): Array<string> {
53
+ return list.flatMap((item) => (item.type === "page" ? [item.page] : pagesOf(item.items)))
54
+ }
55
+
56
+ export function allPages(tree: NavTree): Array<string> {
57
+ return tree.versions.flatMap((version) => version.tabs.flatMap((tab) => pagesOf(tab.items)))
58
+ }
59
+
60
+ export type PageLocation = { version: NavVersion; tab: NavTab }
61
+
62
+ /** Version et onglet qui contiennent une page ; la première occurrence l'emporte. */
63
+ export function locatePage(tree: NavTree, page: string): PageLocation | null {
64
+ for (const version of tree.versions)
65
+ for (const tab of version.tabs) if (pagesOf(tab.items).includes(page)) return { version, tab }
66
+ return null
67
+ }
68
+
69
+ export function defaultVersion(tree: NavTree): NavVersion {
70
+ return tree.versions.find((v) => v.isDefault) ?? tree.versions[0]!
71
+ }
72
+
73
+ /** Page « index » à la racine du site, « guides/index » sur `/guides/`. */
74
+ export function pageToSlug(page: string): string {
75
+ const lower = page.toLowerCase()
76
+ if (lower === "index") return ""
77
+ return lower.endsWith("/index") ? lower.slice(0, -"/index".length) : lower
78
+ }
79
+
80
+ export function pageToPath(page: string): string {
81
+ const slug = pageToSlug(page)
82
+ return slug ? `/${slug}/` : "/"
83
+ }
84
+
85
+ /** Pages citées dans la navigation mais absentes du dossier : erreur de build lisible. */
86
+ export function missingPages(tree: NavTree, existing: ReadonlySet<string>): Array<string> {
87
+ return [...new Set(allPages(tree))].filter((page) => !existing.has(pageToSlug(page)))
88
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Couleur du client → variables d'accent de Starlight, en clair et en sombre.
3
+ *
4
+ * On garde la teinte et la saturation de la couleur donnée, et on ajuste seulement sa
5
+ * luminosité (OKLCH) jusqu'au contraste exigé : un lien reste lisible à 4,5:1 sur le fond,
6
+ * quelle que soit la couleur choisie par le client.
7
+ */
8
+ export type Oklch = { l: number; c: number; h: number }
9
+ export type AccentPalette = { low: string; accent: string; high: string; onAccent: string }
10
+ export type Theme = { light: AccentPalette; dark: AccentPalette }
11
+
12
+ /** Accent violet de la DA HGDC (`--brand-accent` du kit), si le client n'en donne pas. */
13
+ export const HGDC_ACCENT = "#7c3aed"
14
+
15
+ const LIGHT_BG = "#ffffff"
16
+ const DARK_BG = "#0f0f10"
17
+
18
+ export function themeFor(primary: string = HGDC_ACCENT, dark?: string): Theme {
19
+ const base = hexToOklch(primary)
20
+ const darkBase = dark ? hexToOklch(dark) : base
21
+ return {
22
+ light: palette(base, LIGHT_BG, { low: 0.955, high: 0.36 }),
23
+ dark: palette(darkBase, DARK_BG, { low: 0.27, high: 0.9 }),
24
+ }
25
+ }
26
+
27
+ function palette(base: Oklch, background: string, tones: { low: number; high: number }): AccentPalette {
28
+ const accent = withContrast(base, background, 4.5)
29
+ const low = oklchToHex({ l: tones.low, c: Math.min(base.c, 0.06), h: base.h })
30
+ const high = withContrast({ ...base, l: tones.high }, low, 7)
31
+ const onAccent = contrast("#ffffff", accent) >= contrast("#000000", accent) ? "#ffffff" : "#000000"
32
+ return { low, accent, high, onAccent }
33
+ }
34
+
35
+ /** Couleur la plus proche de `color`, en luminosité seulement, qui atteint `ratio` sur `background`. */
36
+ export function withContrast(color: Oklch, background: string, ratio: number): string {
37
+ const towardsDark = luminance(background) > 0.5
38
+ let l = color.l
39
+ for (let i = 0; i < 100; i++) {
40
+ const hex = oklchToHex({ ...color, l })
41
+ if (contrast(hex, background) >= ratio) return hex
42
+ l = towardsDark ? l - 0.01 : l + 0.01
43
+ if (l <= 0 || l >= 1) break
44
+ }
45
+ return towardsDark ? "#000000" : "#ffffff"
46
+ }
47
+
48
+ export function themeCss(theme: Theme): string {
49
+ const vars = (p: AccentPalette) =>
50
+ `--sl-color-accent-low:${p.low};--sl-color-accent:${p.accent};--sl-color-accent-high:${p.high};--hgdc-on-accent:${p.onAccent};`
51
+ return `:root{${vars(theme.dark)}}:root[data-theme="light"]{${vars(theme.light)}}`
52
+ }
53
+
54
+ // --- Conversions sRGB ↔ OKLCH (Björn Ottosson) et contraste WCAG 2 ---
55
+
56
+ export function hexToRgb(hex: string): [number, number, number] {
57
+ const value = hex.replace("#", "")
58
+ const full =
59
+ value.length === 3
60
+ ? value
61
+ .split("")
62
+ .map((c) => c + c)
63
+ .join("")
64
+ : value
65
+ const n = Number.parseInt(full, 16)
66
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255]
67
+ }
68
+
69
+ const toLinear = (c: number) => {
70
+ const v = c / 255
71
+ return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
72
+ }
73
+ const fromLinear = (v: number) => {
74
+ const c = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055
75
+ return Math.round(Math.min(1, Math.max(0, c)) * 255)
76
+ }
77
+
78
+ export function hexToOklch(hex: string): Oklch {
79
+ const [r, g, b] = hexToRgb(hex).map(toLinear) as [number, number, number]
80
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
81
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
82
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
83
+ const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s
84
+ const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s
85
+ const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
86
+ return { l: L, c: Math.hypot(A, B), h: ((Math.atan2(B, A) * 180) / Math.PI + 360) % 360 }
87
+ }
88
+
89
+ export function oklchToHex({ l: L, c, h }: Oklch): string {
90
+ const A = c * Math.cos((h * Math.PI) / 180)
91
+ const B = c * Math.sin((h * Math.PI) / 180)
92
+ const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3
93
+ const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3
94
+ const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3
95
+ const rgb = [
96
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
97
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
98
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
99
+ ].map(fromLinear)
100
+ return `#${rgb.map((v) => v.toString(16).padStart(2, "0")).join("")}`
101
+ }
102
+
103
+ export function luminance(hex: string): number {
104
+ const [r, g, b] = hexToRgb(hex).map(toLinear) as [number, number, number]
105
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
106
+ }
107
+
108
+ export function contrast(a: string, b: string): number {
109
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number]
110
+ return (hi + 0.05) / (lo + 0.05)
111
+ }