@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,35 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs"
2
+ import { extname, join, relative } from "node:path"
3
+
4
+ const IGNORED_DIRS = new Set(["node_modules", "dist", ".hgdc-docs", ".git", ".github", ".vercel"])
5
+
6
+ /** Dossiers d'images publiés tels quels ; `public/` est servi à la racine du site. */
7
+ export const ASSET_DIRS = ["images", "img", "logo", "assets", "static"]
8
+ const MEDIA = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".mp4", ".webm", ".woff2"])
9
+
10
+ /**
11
+ * Fichiers statiques publiés, et seulement ceux-là : le logo et le favicon déclarés dans
12
+ * docs.json, les médias des dossiers d'images (`images/`, `logo/`…), et tout `public/`.
13
+ * Un `.txt` ou un `.pdf` oublié ailleurs dans le dépôt n'est jamais mis en ligne.
14
+ */
15
+ export function publicFiles(contentDir: string, declared: ReadonlyArray<string>): Array<{ from: string; to: string }> {
16
+ const files: Array<{ from: string; to: string }> = []
17
+ const walk = (dir: string, onlyMedia: boolean, base: string) => {
18
+ if (!existsSync(dir)) return
19
+ for (const name of readdirSync(dir)) {
20
+ if (name.startsWith(".") || IGNORED_DIRS.has(name)) continue
21
+ const path = join(dir, name)
22
+ if (statSync(path).isDirectory()) walk(path, onlyMedia, base)
23
+ else if (!onlyMedia || MEDIA.has(extname(name).toLowerCase()))
24
+ files.push({ from: path, to: relative(base, path) })
25
+ }
26
+ }
27
+ for (const folder of ASSET_DIRS) walk(join(contentDir, folder), true, contentDir)
28
+ walk(join(contentDir, "public"), false, join(contentDir, "public"))
29
+ for (const asset of declared) {
30
+ const from = join(contentDir, asset.replace(/^\/+/, ""))
31
+ if (relative(contentDir, from).startsWith("..") || !existsSync(from)) continue
32
+ files.push({ from, to: relative(contentDir, from) })
33
+ }
34
+ return files
35
+ }
@@ -0,0 +1,12 @@
1
+ ---
2
+ interface Props {
3
+ title: string
4
+ defaultOpen?: boolean
5
+ }
6
+ const { title, defaultOpen = false } = Astro.props
7
+ ---
8
+
9
+ <details class="hgdc-accordion" open={defaultOpen}>
10
+ <summary>{title}</summary>
11
+ <div class="hgdc-accordion__body"><slot /></div>
12
+ </details>
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+
4
+ <div class="hgdc-accordion-group"><slot /></div>
@@ -0,0 +1,15 @@
1
+ ---
2
+ interface Props {
3
+ title: string
4
+ href?: string
5
+ /** Accepté pour la compatibilité Mintlify, ignoré : les icônes de Mintlify sont propriétaires. */
6
+ icon?: string
7
+ }
8
+ const { title, href } = Astro.props
9
+ const Tag = href ? "a" : "div"
10
+ ---
11
+
12
+ <Tag class:list={["hgdc-card", { "hgdc-card--link": href }]} href={href}>
13
+ <span class="hgdc-card__title">{title}</span>
14
+ <div class="hgdc-card__body"><slot /></div>
15
+ </Tag>
@@ -0,0 +1,8 @@
1
+ ---
2
+ interface Props {
3
+ cols?: number
4
+ }
5
+ const { cols = 2 } = Astro.props
6
+ ---
7
+
8
+ <div class="hgdc-card-group not-content" style={`--hgdc-cols:${Math.min(Math.max(cols, 1), 4)}`}><slot /></div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title } = Astro.props
8
+ ---
9
+
10
+ <Aside type="tip" title={title}><slot /></Aside>
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+
4
+ <div class="hgdc-code-group"><slot /></div>
@@ -0,0 +1,8 @@
1
+ ---
2
+ interface Props {
3
+ cols?: number
4
+ }
5
+ const { cols = 2 } = Astro.props
6
+ ---
7
+
8
+ <div class="hgdc-card-group not-content" style={`--hgdc-cols:${Math.min(Math.max(cols, 1), 4)}`}><slot /></div>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title } = Astro.props
8
+ ---
9
+
10
+ <Aside type="danger" title={title}><slot /></Aside>
@@ -0,0 +1,11 @@
1
+ ---
2
+ interface Props {
3
+ caption?: string
4
+ }
5
+ const { caption } = Astro.props
6
+ ---
7
+
8
+ <figure class="hgdc-frame">
9
+ <slot />
10
+ {caption && <figcaption>{caption}</figcaption>}
11
+ </figure>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title = "Info" } = Astro.props
8
+ ---
9
+
10
+ <Aside type="note" title={title}><slot /></Aside>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title } = Astro.props
8
+ ---
9
+
10
+ <Aside type="note" title={title}><slot /></Aside>
@@ -0,0 +1,11 @@
1
+ ---
2
+ interface Props {
3
+ title: string
4
+ }
5
+ const { title } = Astro.props
6
+ ---
7
+
8
+ <li class="hgdc-step">
9
+ <p class="hgdc-step__title">{title}</p>
10
+ <div class="hgdc-step__body"><slot /></div>
11
+ </li>
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+
4
+ <ol class="hgdc-steps"><slot /></ol>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { TabItem } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title: string
6
+ }
7
+ ---
8
+
9
+ <TabItem label={Astro.props.title}><slot /></TabItem>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { Tabs } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ syncKey?: string
6
+ }
7
+ ---
8
+
9
+ <Tabs syncKey={Astro.props.syncKey}><slot /></Tabs>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title } = Astro.props
8
+ ---
9
+
10
+ <Aside type="tip" title={title}><slot /></Aside>
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { Aside } from "@astrojs/starlight/components"
3
+
4
+ interface Props {
5
+ title?: string
6
+ }
7
+ const { title } = Astro.props
8
+ ---
9
+
10
+ <Aside type="caution" title={title}><slot /></Aside>
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Markdown restreint des réponses de l'assistant → arbre de nœuds, rendu ensuite en DOM
3
+ * avec `textContent` seulement : aucun HTML de la réponse n'est jamais interprété. Une
4
+ * réponse du modèle reste une donnée non fiable, même quand elle cite la doc.
5
+ */
6
+ export type Inline =
7
+ | { type: "text"; value: string }
8
+ | { type: "strong" | "em"; children: Array<Inline> }
9
+ | { type: "code"; value: string }
10
+ | { type: "link"; href: string; children: Array<Inline> }
11
+ | { type: "ref"; n: number }
12
+
13
+ export type Block =
14
+ | { type: "paragraph"; children: Array<Inline> }
15
+ | { type: "heading"; children: Array<Inline> }
16
+ | { type: "list"; ordered: boolean; items: Array<Array<Inline>> }
17
+ | { type: "code"; language: string | null; value: string }
18
+
19
+ /**
20
+ * Liens permis : chemins du site, ancres, et adresses des origines données (le site de la
21
+ * doc). Un lien vers ailleurs, même en https, devient du texte : une page injectée ne peut
22
+ * pas faire afficher un lien de hameçonnage par l'assistant.
23
+ */
24
+ export function safeHref(href: string, origins: ReadonlyArray<string> = []): string | null {
25
+ const value = href.trim()
26
+ if (value.startsWith("#")) return value
27
+ if (value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/\\")) return value
28
+ try {
29
+ const url = new URL(value)
30
+ const web = url.protocol === "https:" || url.protocol === "http:"
31
+ return web && origins.includes(url.origin) ? url.toString() : null
32
+ } catch {
33
+ return null
34
+ }
35
+ }
36
+
37
+ const INLINE = /(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\s][^*]*\*|_[^_\s][^_]*_)|(\[\^(\d+)\])|(\[([^\]]+)\]\(([^)\s]+)\))/
38
+
39
+ export function parseInline(source: string, origins: ReadonlyArray<string> = []): Array<Inline> {
40
+ const out: Array<Inline> = []
41
+ let rest = source
42
+ while (rest) {
43
+ const match = INLINE.exec(rest)
44
+ if (!match) {
45
+ out.push({ type: "text", value: rest })
46
+ break
47
+ }
48
+ if (match.index > 0) out.push({ type: "text", value: rest.slice(0, match.index) })
49
+ const [whole, code, strong, em, ref, refN, link, linkText, linkHref] = match
50
+ if (code) out.push({ type: "code", value: code.slice(1, -1) })
51
+ else if (strong) out.push({ type: "strong", children: parseInline(strong.slice(2, -2), origins) })
52
+ else if (em) out.push({ type: "em", children: parseInline(em.slice(1, -1), origins) })
53
+ else if (ref && refN) out.push({ type: "ref", n: Number(refN) })
54
+ else if (link && linkText && linkHref) {
55
+ const href = safeHref(linkHref, origins)
56
+ out.push(
57
+ href ? { type: "link", href, children: parseInline(linkText, origins) } : { type: "text", value: linkText },
58
+ )
59
+ }
60
+ rest = rest.slice(match.index + whole.length)
61
+ }
62
+ return out
63
+ }
64
+
65
+ const LIST = /^\s*(?:([-*])|(\d+)[.)])\s+(.*)$/
66
+
67
+ export function parseMarkdown(source: string, origins: ReadonlyArray<string> = []): Array<Block> {
68
+ const blocks: Array<Block> = []
69
+ const lines = source.replace(/\r\n?/g, "\n").split("\n")
70
+ let paragraph: Array<string> = []
71
+ const flush = () => {
72
+ if (paragraph.length) blocks.push({ type: "paragraph", children: parseInline(paragraph.join(" "), origins) })
73
+ paragraph = []
74
+ }
75
+
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i]!
78
+ const fence = /^\s*```\s*([\w+-]*)\s*$/.exec(line)
79
+ if (fence) {
80
+ flush()
81
+ const body: Array<string> = []
82
+ while (++i < lines.length && !/^\s*```\s*$/.test(lines[i]!)) body.push(lines[i]!)
83
+ blocks.push({ type: "code", language: fence[1] || null, value: body.join("\n") })
84
+ continue
85
+ }
86
+ const heading = /^#{1,6}\s+(.*)$/.exec(line)
87
+ if (heading) {
88
+ flush()
89
+ blocks.push({ type: "heading", children: parseInline(heading[1]!, origins) })
90
+ continue
91
+ }
92
+ const item = LIST.exec(line)
93
+ if (item) {
94
+ flush()
95
+ const ordered = Boolean(item[2])
96
+ const previous = blocks.at(-1)
97
+ const list =
98
+ previous?.type === "list" && previous.ordered === ordered && lines[i - 1]?.trim() !== ""
99
+ ? previous
100
+ : (blocks[blocks.push({ type: "list", ordered, items: [] }) - 1] as Extract<Block, { type: "list" }>)
101
+ list.items.push(parseInline(item[3]!, origins))
102
+ continue
103
+ }
104
+ if (line.trim() === "") flush()
105
+ else paragraph.push(line.trim())
106
+ }
107
+ flush()
108
+ return blocks
109
+ }
@@ -0,0 +1,65 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Contrat de `POST /api/v1/ask` entre le widget du site et la console. Une seule source,
5
+ * importée des deux côtés : un champ ajouté ici est vu par le build et par l'API.
6
+ */
7
+ export const MAX_QUESTION = 1000
8
+ export const MAX_PREVIOUS = 3
9
+
10
+ export const askRequestSchema = z.object({
11
+ project: z.string().regex(/^[a-z0-9]{8,64}$/),
12
+ question: z.string().trim().min(2).max(MAX_QUESTION),
13
+ /** Chemin de la page ouverte : l'assistant la privilégie dans sa recherche. */
14
+ page: z.string().startsWith("/").max(512).optional(),
15
+ /**
16
+ * Échanges précédents, par leur identifiant (`id` des réponses) : le serveur relit ce qu'il a
17
+ * lui-même répondu. Un visiteur ne peut pas glisser une fausse réponse de l'assistant.
18
+ */
19
+ previous: z
20
+ .array(z.string().regex(/^[a-z0-9]{8,64}$/))
21
+ .max(MAX_PREVIOUS)
22
+ .optional(),
23
+ })
24
+
25
+ export type AskRequest = z.infer<typeof askRequestSchema>
26
+
27
+ export const sourceSchema = z.object({
28
+ n: z.number().int().positive(),
29
+ title: z.string(),
30
+ section: z.string().nullable(),
31
+ url: z.string(),
32
+ })
33
+
34
+ export type AnswerSource = z.infer<typeof sourceSchema>
35
+
36
+ export const askResponseSchema = z.object({
37
+ id: z.string(),
38
+ /** `no_answer` : rien dans la doc ne répond, l'assistant le dit au lieu d'inventer. */
39
+ status: z.enum(["answered", "no_answer"]),
40
+ /** Markdown restreint ; `[^n]` renvoie à la source `n`. */
41
+ answer: z.string(),
42
+ sources: z.array(sourceSchema),
43
+ })
44
+
45
+ export type AskResponse = z.infer<typeof askResponseSchema>
46
+
47
+ export const ASK_ERRORS = {
48
+ invalid_request: "La question n'a pas pu être lue.",
49
+ project_not_found: "Cette documentation n'a pas d'assistant.",
50
+ assistant_disabled: "L'assistant est désactivé pour cette documentation.",
51
+ origin_not_allowed: "Cet assistant ne répond que depuis son site de documentation.",
52
+ rate_limited: "Trop de questions d'un coup. Réessaie dans une minute.",
53
+ quota_exceeded: "L'assistant a atteint sa limite de questions pour ce mois.",
54
+ not_indexed: "La documentation n'est pas encore indexée. Réessaie après la prochaine publication.",
55
+ unavailable: "L'assistant ne répond pas pour l'instant. Réessaie dans un moment.",
56
+ } as const
57
+
58
+ export type AskErrorCode = keyof typeof ASK_ERRORS
59
+
60
+ export const askErrorSchema = z.object({
61
+ error: z.object({
62
+ code: z.enum(Object.keys(ASK_ERRORS) as [AskErrorCode, ...Array<AskErrorCode>]),
63
+ message: z.string(),
64
+ }),
65
+ })
@@ -0,0 +1,134 @@
1
+ import GithubSlugger from "github-slugger"
2
+
3
+ /**
4
+ * Découpe une page en passages pour la recherche de l'assistant : une section par titre
5
+ * (`##` à `######`), coupée en paragraphes si elle dépasse `MAX_CHARS`. Chaque passage
6
+ * garde l'ancre de son titre, calculée comme les identifiants de Starlight (github-slugger),
7
+ * pour que la source citée ouvre la bonne section.
8
+ */
9
+ export const MAX_CHARS = 1600
10
+ const MIN_CHARS = 120
11
+
12
+ export type Chunk = {
13
+ /** Fil des titres, page comprise : « Publier › Avec Host ». */
14
+ heading: string
15
+ anchor: string | null
16
+ text: string
17
+ }
18
+
19
+ type Section = { trail: Array<string>; anchor: string | null; lines: Array<string> }
20
+
21
+ /** Au-delà, une ligne n'est pas un titre : la borne garde l'analyse linéaire. */
22
+ const MAX_HEADING_LINE = 500
23
+
24
+ /**
25
+ * Titre ATX (`## Titre`, fermeture `##` facultative), lu caractère par caractère : pas
26
+ * d'expression régulière à quantificateurs chevauchants, donc pas de retour arrière
27
+ * explosif sur une ligne piégée (« ## a » suivi de milliers d'espaces).
28
+ */
29
+ export function parseHeading(line: string): { level: number; text: string } | null {
30
+ if (line.length > MAX_HEADING_LINE || !line.startsWith("#")) return null
31
+ let level = 0
32
+ while (line[level] === "#") level++
33
+ if (level > 6 || (line[level] !== " " && line[level] !== "\t")) return null
34
+ let text = line.slice(level).trim()
35
+ let end = text.length
36
+ while (end > 0 && text[end - 1] === "#") end--
37
+ if (end < text.length && (end === 0 || text[end - 1] === " " || text[end - 1] === "\t"))
38
+ text = text.slice(0, end).trim()
39
+ return text ? { level, text } : null
40
+ }
41
+ const FENCE = /^\s*(```|~~~)/
42
+
43
+ export function chunkPage(title: string, markdown: string): Array<Chunk> {
44
+ const slugger = new GithubSlugger()
45
+ const sections: Array<Section> = [{ trail: [], anchor: null, lines: [] }]
46
+ const stack: Array<{ level: number; text: string }> = []
47
+ let fence: string | null = null
48
+
49
+ for (const line of markdown.replace(/\r\n?/g, "\n").split("\n")) {
50
+ const fenceMatch = FENCE.exec(line)
51
+ if (fenceMatch) fence = fence === null ? fenceMatch[1]! : fence === fenceMatch[1] ? null : fence
52
+ const heading = fence === null && !fenceMatch ? parseHeading(line) : null
53
+ if (!heading) {
54
+ sections.at(-1)!.lines.push(line)
55
+ continue
56
+ }
57
+ const level = heading.level
58
+ const text = stripInline(heading.text)
59
+ if (level === 1) continue // le titre de la page, déjà porté par `title`
60
+ while (stack.length && stack.at(-1)!.level >= level) stack.pop()
61
+ stack.push({ level, text })
62
+ sections.push({ trail: stack.map((s) => s.text), anchor: slugger.slug(text), lines: [] })
63
+ }
64
+
65
+ const chunks: Array<Chunk> = []
66
+ for (const section of sections) {
67
+ const body = section.lines.join("\n").trim()
68
+ if (!body) continue
69
+ const heading = [title, ...section.trail].join(" › ")
70
+ for (const part of splitBody(body)) chunks.push({ heading, anchor: section.anchor, text: part })
71
+ }
72
+ return mergeSmall(chunks)
73
+ }
74
+
75
+ /** Paragraphes regroupés jusqu'à `MAX_CHARS`, sans couper un bloc de code. */
76
+ function splitBody(body: string): Array<string> {
77
+ if (body.length <= MAX_CHARS) return [body]
78
+ const blocks: Array<string> = []
79
+ let current: Array<string> = []
80
+ let fence = false
81
+ for (const line of body.split("\n")) {
82
+ if (FENCE.test(line)) fence = !fence
83
+ if (!fence && line.trim() === "" && current.length) {
84
+ blocks.push(current.join("\n"))
85
+ current = []
86
+ continue
87
+ }
88
+ current.push(line)
89
+ }
90
+ if (current.length) blocks.push(current.join("\n"))
91
+
92
+ const parts: Array<string> = []
93
+ let buffer = ""
94
+ for (const block of blocks) {
95
+ if (buffer && buffer.length + block.length + 2 > MAX_CHARS) {
96
+ parts.push(buffer)
97
+ buffer = ""
98
+ }
99
+ buffer = buffer ? `${buffer}\n\n${block}` : block
100
+ while (buffer.length > MAX_CHARS * 2) {
101
+ parts.push(buffer.slice(0, MAX_CHARS))
102
+ buffer = buffer.slice(MAX_CHARS)
103
+ }
104
+ }
105
+ if (buffer) parts.push(buffer)
106
+ return parts
107
+ }
108
+
109
+ /** Un passage très court rejoint le suivant de la même section : seul, il ne répond à rien. */
110
+ function mergeSmall(chunks: Array<Chunk>): Array<Chunk> {
111
+ const merged: Array<Chunk> = []
112
+ for (const chunk of chunks) {
113
+ const previous = merged.at(-1)
114
+ if (previous && previous.text.length < MIN_CHARS && previous.anchor === chunk.anchor) {
115
+ previous.text = `${previous.text}\n\n${chunk.text}`
116
+ continue
117
+ }
118
+ merged.push({ ...chunk })
119
+ }
120
+ return merged
121
+ }
122
+
123
+ function stripInline(text: string): string {
124
+ return text
125
+ .replace(/`([^`]*)`/g, "$1")
126
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
127
+ .replace(/[*_]/g, "")
128
+ .trim()
129
+ }
130
+
131
+ /** Texte envoyé au modèle d'embeddings : le contexte du titre aide à retrouver un passage court. */
132
+ export function embeddingText(chunk: Chunk): string {
133
+ return `${chunk.heading}\n\n${chunk.text}`
134
+ }