blume 2.2.2 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/THIRD-PARTY-NOTICES.md +51 -0
- package/dist/cli/{chunk-kmswjxj9.js → chunk-0b1a8xqz.js} +3 -3
- package/dist/cli/{chunk-rtvgmg2j.js → chunk-0qx6vwwf.js} +2 -2
- package/dist/cli/{chunk-qcyxkj44.js → chunk-3ps63r91.js} +43 -17
- package/dist/cli/chunk-3ps63r91.js.map +15 -0
- package/dist/cli/{chunk-b680sv30.js → chunk-402xaaxn.js} +10 -8
- package/dist/cli/chunk-402xaaxn.js.map +13 -0
- package/dist/cli/{chunk-k45sq16q.js → chunk-49xa4yf2.js} +39 -23
- package/dist/cli/{chunk-k45sq16q.js.map → chunk-49xa4yf2.js.map} +3 -3
- package/dist/cli/{chunk-j4nfgn2j.js → chunk-51wr1wre.js} +1 -1
- package/dist/cli/{chunk-9jqmq5hv.js → chunk-531qbwn1.js} +6 -6
- package/dist/cli/{chunk-r5nm9w0c.js → chunk-5bp65pv4.js} +107 -37
- package/dist/cli/{chunk-r5nm9w0c.js.map → chunk-5bp65pv4.js.map} +11 -10
- package/dist/cli/{chunk-yg8ckc3g.js → chunk-6nvcjzts.js} +2 -2
- package/dist/cli/{chunk-x7bh6d9j.js → chunk-6v5w8vzd.js} +4 -4
- package/dist/cli/{chunk-f260jdrn.js → chunk-8cshtem6.js} +5 -5
- package/dist/cli/{chunk-ydqzkhjn.js → chunk-8h5ceyk0.js} +6 -6
- package/dist/cli/{chunk-e1n41w9m.js → chunk-8v90n67z.js} +3 -3
- package/dist/cli/{chunk-etckvbrh.js → chunk-98me5yas.js} +14 -14
- package/dist/cli/{chunk-hktq4j5r.js → chunk-9k7gg4ga.js} +16 -10
- package/dist/cli/{chunk-hktq4j5r.js.map → chunk-9k7gg4ga.js.map} +5 -5
- package/dist/cli/{chunk-34wap3ed.js → chunk-9qmc57zw.js} +3 -3
- package/dist/cli/{chunk-beb0z983.js → chunk-a1xmzxk2.js} +16 -16
- package/dist/cli/{chunk-gwy55cx3.js → chunk-ajw85dgf.js} +2 -2
- package/dist/cli/{chunk-69rjf786.js → chunk-ansqj08e.js} +5 -5
- package/dist/cli/{chunk-q1wwrxzw.js → chunk-b0vwhmdf.js} +3 -3
- package/dist/cli/chunk-b0vwhmdf.js.map +10 -0
- package/dist/cli/{chunk-nqna8dzp.js → chunk-b5gztt4j.js} +17 -17
- package/dist/cli/{chunk-fnxgtjeq.js → chunk-bc4jz2m4.js} +1 -1
- package/dist/cli/{chunk-aafhwx74.js → chunk-d4h1y312.js} +1 -1
- package/dist/cli/{chunk-080mfch6.js → chunk-et7zwqkg.js} +15 -15
- package/dist/cli/{chunk-487b95qy.js → chunk-f29q3j13.js} +41 -25
- package/dist/cli/{chunk-487b95qy.js.map → chunk-f29q3j13.js.map} +3 -3
- package/dist/cli/{chunk-c5bjxq8v.js → chunk-f72388jn.js} +2 -2
- package/dist/cli/{chunk-t2h8pwy1.js → chunk-g0hpwbyx.js} +4 -4
- package/dist/cli/{chunk-40vk8kgf.js → chunk-j102xmnk.js} +7 -7
- package/dist/cli/{chunk-ppa6mg4d.js → chunk-m54bz9ng.js} +6 -6
- package/dist/cli/{chunk-h0qwdbem.js → chunk-mt6njy68.js} +2 -2
- package/dist/cli/{chunk-2ssh60qh.js → chunk-n091sgam.js} +6 -6
- package/dist/cli/{chunk-1ch3ct3r.js → chunk-p33bf2nb.js} +14 -14
- package/dist/cli/{chunk-4zxh25hc.js → chunk-saxq5jjr.js} +2 -2
- package/dist/cli/{chunk-x60e15st.js → chunk-sbt1s5j2.js} +17 -17
- package/dist/cli/{chunk-gxhcf2hs.js → chunk-tckd6qwv.js} +2 -2
- package/dist/cli/{chunk-p834rkcn.js → chunk-tf4134kk.js} +5 -5
- package/dist/cli/{chunk-zxd5jrgx.js → chunk-vxdjss7f.js} +76 -34
- package/dist/cli/{chunk-zxd5jrgx.js.map → chunk-vxdjss7f.js.map} +4 -4
- package/dist/cli/{chunk-9h8mh2tk.js → chunk-x2pgg22t.js} +1 -1
- package/dist/cli/{chunk-hn3g2b89.js → chunk-xhrwrzrh.js} +9 -9
- package/dist/cli/index.js +18 -18
- package/dist/types/ai/ask-context.d.ts +8 -0
- package/dist/types/ai/link-text.d.ts +5 -0
- package/dist/types/ai/relative-links.d.ts +26 -6
- package/dist/types/core/config-input.d.ts +41 -1
- package/dist/types/core/data.d.ts +14 -0
- package/dist/types/core/last-modified.d.ts +33 -18
- package/dist/types/core/links.d.ts +16 -0
- package/dist/types/core/schema.d.ts +17 -1
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/deploy/sitemap.d.ts +9 -0
- package/dist/types/search/documents.d.ts +3 -1
- package/dist/types/search/fold.d.ts +33 -0
- package/dist/types/search/orama-index.d.ts +4 -4
- package/dist/types/seo/jsonld.d.ts +33 -0
- package/docs/configuration/search.mdx +2 -2
- package/docs/configuration/theming.mdx +13 -1
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +1 -1
- package/docs/content/sources/remote-mdx.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +16 -1
- package/docs/discoverability/sitemap-and-robots.mdx +12 -0
- package/docs/discoverability/structured-data.mdx +44 -0
- package/package.json +4 -3
- package/skills/blume-migrate/references/docus.md +1 -2
- package/skills/blume-migrate/references/docusaurus.md +1 -1
- package/skills/blume-migrate/references/vitepress.md +2 -2
- package/skills/blume-migrate/references/vuepress.md +1 -1
- package/src/ai/ask-context.ts +52 -14
- package/src/ai/ask-data.ts +4 -1
- package/src/ai/ask-tools.ts +17 -5
- package/src/ai/link-text.ts +6 -0
- package/src/ai/llms.ts +1 -7
- package/src/ai/markdown.ts +8 -4
- package/src/ai/relative-links.ts +155 -48
- package/src/ai/site-skill.ts +1 -4
- package/src/astro/generate.ts +7 -0
- package/src/astro/templates.ts +36 -20
- package/src/components/layout/Header.astro +18 -10
- package/src/components/layout/PageActions.astro +1 -0
- package/src/components/layout/PageLayout.astro +31 -13
- package/src/components/layout/ReferenceLayout.astro +6 -1
- package/src/components/layout/RootLayout.astro +13 -1
- package/src/components/layout/head-scripts.ts +5 -1
- package/src/components/layout/locale-redirect.ts +38 -19
- package/src/components/layout/search/types.ts +12 -4
- package/src/core/config-input.ts +40 -1
- package/src/core/config.ts +5 -2
- package/src/core/data.ts +14 -0
- package/src/core/last-modified.ts +80 -34
- package/src/core/links.ts +43 -2
- package/src/core/manifest.ts +2 -0
- package/src/core/project-graph.ts +83 -43
- package/src/core/schema.ts +48 -9
- package/src/core/types.ts +10 -0
- package/src/deploy/sitemap.ts +127 -49
- package/src/search/documents.ts +34 -11
- package/src/search/fold.ts +175 -0
- package/src/search/orama-index.ts +40 -49
- package/src/seo/jsonld.ts +100 -23
- package/src/theme/chrome-icons.ts +2 -1
- package/src/theme/entry.ts +4 -1
- package/dist/cli/chunk-b680sv30.js.map +0 -13
- package/dist/cli/chunk-q1wwrxzw.js.map +0 -10
- package/dist/cli/chunk-qcyxkj44.js.map +0 -14
- /package/dist/cli/{chunk-kmswjxj9.js.map → chunk-0b1a8xqz.js.map} +0 -0
- /package/dist/cli/{chunk-rtvgmg2j.js.map → chunk-0qx6vwwf.js.map} +0 -0
- /package/dist/cli/{chunk-j4nfgn2j.js.map → chunk-51wr1wre.js.map} +0 -0
- /package/dist/cli/{chunk-9jqmq5hv.js.map → chunk-531qbwn1.js.map} +0 -0
- /package/dist/cli/{chunk-yg8ckc3g.js.map → chunk-6nvcjzts.js.map} +0 -0
- /package/dist/cli/{chunk-x7bh6d9j.js.map → chunk-6v5w8vzd.js.map} +0 -0
- /package/dist/cli/{chunk-f260jdrn.js.map → chunk-8cshtem6.js.map} +0 -0
- /package/dist/cli/{chunk-ydqzkhjn.js.map → chunk-8h5ceyk0.js.map} +0 -0
- /package/dist/cli/{chunk-e1n41w9m.js.map → chunk-8v90n67z.js.map} +0 -0
- /package/dist/cli/{chunk-etckvbrh.js.map → chunk-98me5yas.js.map} +0 -0
- /package/dist/cli/{chunk-34wap3ed.js.map → chunk-9qmc57zw.js.map} +0 -0
- /package/dist/cli/{chunk-beb0z983.js.map → chunk-a1xmzxk2.js.map} +0 -0
- /package/dist/cli/{chunk-gwy55cx3.js.map → chunk-ajw85dgf.js.map} +0 -0
- /package/dist/cli/{chunk-69rjf786.js.map → chunk-ansqj08e.js.map} +0 -0
- /package/dist/cli/{chunk-nqna8dzp.js.map → chunk-b5gztt4j.js.map} +0 -0
- /package/dist/cli/{chunk-fnxgtjeq.js.map → chunk-bc4jz2m4.js.map} +0 -0
- /package/dist/cli/{chunk-aafhwx74.js.map → chunk-d4h1y312.js.map} +0 -0
- /package/dist/cli/{chunk-080mfch6.js.map → chunk-et7zwqkg.js.map} +0 -0
- /package/dist/cli/{chunk-c5bjxq8v.js.map → chunk-f72388jn.js.map} +0 -0
- /package/dist/cli/{chunk-t2h8pwy1.js.map → chunk-g0hpwbyx.js.map} +0 -0
- /package/dist/cli/{chunk-40vk8kgf.js.map → chunk-j102xmnk.js.map} +0 -0
- /package/dist/cli/{chunk-ppa6mg4d.js.map → chunk-m54bz9ng.js.map} +0 -0
- /package/dist/cli/{chunk-h0qwdbem.js.map → chunk-mt6njy68.js.map} +0 -0
- /package/dist/cli/{chunk-2ssh60qh.js.map → chunk-n091sgam.js.map} +0 -0
- /package/dist/cli/{chunk-1ch3ct3r.js.map → chunk-p33bf2nb.js.map} +0 -0
- /package/dist/cli/{chunk-4zxh25hc.js.map → chunk-saxq5jjr.js.map} +0 -0
- /package/dist/cli/{chunk-x60e15st.js.map → chunk-sbt1s5j2.js.map} +0 -0
- /package/dist/cli/{chunk-gxhcf2hs.js.map → chunk-tckd6qwv.js.map} +0 -0
- /package/dist/cli/{chunk-p834rkcn.js.map → chunk-tf4134kk.js.map} +0 -0
- /package/dist/cli/{chunk-9h8mh2tk.js.map → chunk-x2pgg22t.js.map} +0 -0
- /package/dist/cli/{chunk-hn3g2b89.js.map → chunk-xhrwrzrh.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# blume
|
|
2
2
|
|
|
3
|
+
## 2.2.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b89bbc6: update Astro Vercel adapter to v11.0.14
|
|
8
|
+
- 22cf754: Ship the license notices for the Lucide and Feather icons Blume includes, in a new `THIRD-PARTY-NOTICES.md`.
|
|
9
|
+
- 90b974f: A site can now ship a single color scheme. `theme: { mode: "dark", toggle: false }` drops the light/dark toggle from the header and renders every page dark, including for readers who picked light before the toggle was removed. Before, `theme.mode` only set the starting scheme: the toggle always showed, and a reader's saved choice outranked it. With `mode: "system"`, `toggle: false` follows each reader's OS setting.
|
|
10
|
+
|
|
11
|
+
## 2.2.3
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 528f95e: The assistant now reads a page's links to other pages (`[Install](./install)`, `[Setup](../setup.md)`, `[Setup](/guides/setup.md)`) as the routes they mean, as the `.md` mirrors, `llms-full.txt`, and MCP `get_page` already did, so the links it cites in an answer land on the page they mean. A link on a translated page to a page that isn't translated yet points at the page the assistant can read. Pages from a remote or CMS source, such as `mdxRemote()`, now get that rewrite on every agent surface too. Before, the assistant read such links as written and could cite `../setup.md`, a link that led nowhere from the answer, and a remote page's links stayed as written on the agent surfaces, though the rendered page and `blume validate` resolved them. A relative link in a raw HTML `<a href>` now points on the agent surfaces where it goes on the rendered page, resolved against the page's URL as the browser resolves it, rather than the way a Markdown link resolves.
|
|
16
|
+
- 87cb8c4: The assistant cites pages as links more reliably on smaller models. Each documentation excerpt it reads, and each `search_docs` and `read_page` result, is now headed by the Markdown link to cite (`## [Page Title](/route)`), and the model is asked to copy that link. A page whose route has a space, a parenthesis, or a percent sign is cited with those characters percent-encoded (`%20`, `%28`, `%29`, `%25`), so its link works too. Before, the heading was `## Page Title (/route)`, and a model that copied it wrote `[Page Title (/route)]`, `[[Page Title (/route)]]`, or `[/route]`, which showed in the answer as raw text instead of a link.
|
|
17
|
+
- e41c71f: Search now keeps words whole whatever Latin letters they hold. Orama's default tokenizer split words at any letter beyond ASCII and a few accented vowels, so Swedish "när" was indexed as "n" and "r" and matched unrelated pages, and German "Prüfung" as "pr" and "fung". Blume now keeps every Latin letter, folding accents the way Orama already folded "café" and spelling ø, ł and ß as o, l and ss, in the default Orama search dialog, the MCP server's `search_docs`, and the assistant. A soft hyphen (`­`) no longer splits the word it sits in, and a result found by folding is highlighted and excerpted at the accented word. ASCII text is tokenized exactly as before.
|
|
18
|
+
- 2a0628c: The language switcher keeps the reader's place: the chosen link carries the current URL fragment.
|
|
19
|
+
- 34b3e0f: Add SEO options for docs that live inside another site: shared JSON-LD entity ids and attribution (`seo.jsonLd`), a default social image in place of generated cards (`seo.og.image`), publication dates from git for pages without a front matter `date` (`seo.datePublished: "git"`), and hreflang alternates in the sitemap (`seo.sitemap: { alternates: true }`). Page JSON-LD now also carries the page's social image and credits its front matter `authors`, and hreflang links leave out `noindex`, hidden, and canonical-elsewhere translations.
|
|
20
|
+
|
|
3
21
|
## 2.2.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
Blume includes icons from [Lucide](https://lucide.dev). A few are copied into its source as SVG markup (`src/theme/chrome-icons.ts`, `src/theme/entry.ts`, and `src/components/layout/PageActions.astro`), and the rest are inlined into built sites from the `@iconify-json/lucide` package. Lucide's license follows, including the MIT license for the icons Lucide derives from Feather.
|
|
4
|
+
|
|
5
|
+
## Lucide
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
ISC License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
11
|
+
|
|
12
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
13
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
14
|
+
copyright notice and this permission notice appear in all copies.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
17
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
19
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
20
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
21
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
22
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
The following Lucide icons are derived from the Feather project:
|
|
27
|
+
|
|
28
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
29
|
+
|
|
30
|
+
The MIT License (MIT) (for the icons listed above)
|
|
31
|
+
|
|
32
|
+
Copyright (c) 2013-present Cole Bemis
|
|
33
|
+
|
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
35
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
36
|
+
in the Software without restriction, including without limitation the rights
|
|
37
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
38
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
39
|
+
furnished to do so, subject to the following conditions:
|
|
40
|
+
|
|
41
|
+
The above copyright notice and this permission notice shall be included in all
|
|
42
|
+
copies or substantial portions of the Software.
|
|
43
|
+
|
|
44
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
45
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
46
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
47
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
48
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
49
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
50
|
+
SOFTWARE.
|
|
51
|
+
```
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
VERSION_ID,
|
|
9
9
|
VERSION_LIKE,
|
|
10
10
|
versionizeRoute
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5bp65pv4.js";
|
|
12
12
|
import {
|
|
13
13
|
isWithin,
|
|
14
14
|
scanProject
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-vxdjss7f.js";
|
|
16
16
|
import {
|
|
17
17
|
mountBasePath,
|
|
18
18
|
stripBasePath
|
|
@@ -280,4 +280,4 @@ var cutVersion = async (root, id, options = {}) => {
|
|
|
280
280
|
export { CutError, unregisteredSnapshotDiagnostics, cutVersion };
|
|
281
281
|
|
|
282
282
|
//# debugId=1864DE871652686F64756E2164756E21
|
|
283
|
-
//# sourceMappingURL=chunk-
|
|
283
|
+
//# sourceMappingURL=chunk-0b1a8xqz.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
BUILTIN_CHILD_PROPS,
|
|
5
5
|
commandsFor,
|
|
6
6
|
detectProjectPackageManager
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-5bp65pv4.js";
|
|
8
8
|
import {
|
|
9
9
|
packageRoot
|
|
10
10
|
} from "./index.js";
|
|
@@ -307,4 +307,4 @@ var missingDependencyDiagnostic = async (config, root, severity, frameworks = []
|
|
|
307
307
|
export { validateUsedComponents, unknownPropDiagnostics, missingExampleDiagnostics, discoverIslands, EXAMPLE_SCAN_GLOB, exampleScanRoots, discoverExamples, exampleMarkdownLookup, missingDependencyDiagnostic };
|
|
308
308
|
|
|
309
309
|
//# debugId=01581CA3D72AB06264756E2164756E21
|
|
310
|
-
//# sourceMappingURL=chunk-
|
|
310
|
+
//# sourceMappingURL=chunk-0qx6vwwf.js.map
|
|
@@ -35,13 +35,31 @@ import { z as z2 } from "zod";
|
|
|
35
35
|
import { z } from "zod";
|
|
36
36
|
|
|
37
37
|
// src/search/orama-index.ts
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
import { create, insert, insertMultiple, search } from "@orama/orama";
|
|
39
|
+
|
|
40
|
+
// src/search/fold.ts
|
|
41
|
+
var LATIN_ACCENTS = /(?<letter>\p{Script=Latin})\p{M}+/gu;
|
|
42
|
+
var NON_ASCII_LATIN = /(?=\P{ASCII})\p{Script=Latin}/gu;
|
|
43
|
+
var LATIN_FOLDS = new Map([
|
|
44
|
+
["ß", "ss"],
|
|
45
|
+
["æ", "ae"],
|
|
46
|
+
["œ", "oe"],
|
|
47
|
+
["ø", "o"],
|
|
48
|
+
["ł", "l"],
|
|
49
|
+
["đ", "d"],
|
|
50
|
+
["ð", "d"],
|
|
51
|
+
["ħ", "h"],
|
|
52
|
+
["ı", "i"],
|
|
53
|
+
["þ", "th"]
|
|
54
|
+
]);
|
|
55
|
+
var SOFT_HYPHEN = /\u00AD/gu;
|
|
56
|
+
var stripSoftHyphens = (text) => text.replace(SOFT_HYPHEN, "");
|
|
57
|
+
var foldLatin = (lowered) => stripSoftHyphens(lowered).normalize("NFD").replace(NON_ASCII_LATIN, (letter) => LATIN_FOLDS.get(letter) ?? letter.normalize("NFKD").toLowerCase()).replace(LATIN_ACCENTS, "$<letter>");
|
|
58
|
+
var LATIN_RUN = /\p{Script=Latin}[\p{Script=Latin}\p{M}]*/gu;
|
|
59
|
+
var foldLatinTerm = (term) => term.replace(LATIN_RUN, (run) => foldLatin(run));
|
|
60
|
+
var LETTER_TAIL = String.raw`[\p{M}\u00AD]*`;
|
|
61
|
+
|
|
62
|
+
// src/search/orama-index.ts
|
|
45
63
|
var SCHEMA = {
|
|
46
64
|
content: "string",
|
|
47
65
|
contentType: "enum",
|
|
@@ -78,9 +96,14 @@ var isBigramLanguage = (language) => {
|
|
|
78
96
|
};
|
|
79
97
|
var BIGRAM_SCRIPTS = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆〇ー゙゚]+$/u;
|
|
80
98
|
var TERM = /[\p{L}\p{M}\p{N}]+(?:(?:['’](?=\p{L})|(?<=\p{N})[.,](?=\p{N}))[\p{L}\p{M}\p{N}]+)*/gu;
|
|
81
|
-
var
|
|
82
|
-
var
|
|
83
|
-
|
|
99
|
+
var LATIN_SEPARATOR = /[^\p{Script=Latin}0-9_'-]+/u;
|
|
100
|
+
var latinTokenizer = () => ({
|
|
101
|
+
language: "english",
|
|
102
|
+
normalizationCache: new Map,
|
|
103
|
+
tokenize: (raw) => [
|
|
104
|
+
...new Set(foldLatin(raw.toLowerCase()).split(LATIN_SEPARATOR).filter(Boolean))
|
|
105
|
+
]
|
|
106
|
+
});
|
|
84
107
|
var addBigrams = (run, tokens) => {
|
|
85
108
|
const characters = [...run];
|
|
86
109
|
if (characters.length === 1) {
|
|
@@ -125,9 +148,9 @@ var segmentingTokenizer = (locale) => {
|
|
|
125
148
|
return;
|
|
126
149
|
}
|
|
127
150
|
flush();
|
|
128
|
-
tokens.add(
|
|
151
|
+
tokens.add(foldLatinTerm(term));
|
|
129
152
|
};
|
|
130
|
-
for (const segment of segmenter.segment(raw.normalize("NFC").toLowerCase())) {
|
|
153
|
+
for (const segment of segmenter.segment(stripSoftHyphens(raw).normalize("NFC").toLowerCase())) {
|
|
131
154
|
if (!segment.isWordLike) {
|
|
132
155
|
flush();
|
|
133
156
|
continue;
|
|
@@ -161,11 +184,11 @@ var localeTokenizers = (documents) => {
|
|
|
161
184
|
return tokenizers;
|
|
162
185
|
};
|
|
163
186
|
var dispatchingTokenizer = (tokenizers) => {
|
|
164
|
-
const standard =
|
|
187
|
+
const standard = latinTokenizer();
|
|
165
188
|
return {
|
|
166
189
|
language: standard.language,
|
|
167
190
|
normalizationCache: standard.normalizationCache,
|
|
168
|
-
tokenize: (raw, language
|
|
191
|
+
tokenize: (raw, language) => (tokenizers.get(language ?? "") ?? standard).tokenize(raw)
|
|
169
192
|
};
|
|
170
193
|
};
|
|
171
194
|
var buildOramaIndex = async (documents, locale) => {
|
|
@@ -183,7 +206,10 @@ var buildOramaIndex = async (documents, locale) => {
|
|
|
183
206
|
}
|
|
184
207
|
translationTokenizers.set(db, translations);
|
|
185
208
|
} else {
|
|
186
|
-
db =
|
|
209
|
+
db = create({
|
|
210
|
+
components: { tokenizer: tokenizer ?? latinTokenizer() },
|
|
211
|
+
schema: SCHEMA
|
|
212
|
+
});
|
|
187
213
|
await insertMultiple(db, rows);
|
|
188
214
|
}
|
|
189
215
|
if (documents.some((doc) => (doc.boost ?? 1) !== 1)) {
|
|
@@ -545,5 +571,5 @@ export {
|
|
|
545
571
|
mcpStdioCommand
|
|
546
572
|
};
|
|
547
573
|
|
|
548
|
-
//# debugId=
|
|
549
|
-
//# sourceMappingURL=chunk-
|
|
574
|
+
//# debugId=9C79557EDA515E7164756E2164756E21
|
|
575
|
+
//# sourceMappingURL=chunk-3ps63r91.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/cli/commands/mcp-stdio.ts", "../src/ai/mcp/stdio.ts", "../src/ai/mcp/server.ts", "../src/ai/mcp/query.ts", "../src/search/orama-index.ts", "../src/search/fold.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { readFile } from \"node:fs/promises\";\n\nimport { defineCommand } from \"citty\";\n\nimport type { McpData } from \"../../ai/mcp/data.ts\";\nimport { serveMcpStdio } from \"../../ai/mcp/stdio.ts\";\nimport { commandMeta } from \"../command-meta.ts\";\n\nexport const mcpStdioCommand = defineCommand({\n args: {\n data: {\n description: \"Path to a serialized MCP data snapshot (JSON).\",\n required: true,\n type: \"string\",\n },\n },\n meta: commandMeta[\"mcp-stdio\"],\n async run({ args }) {\n // stdout belongs to the JSON-RPC transport from here on; every diagnostic\n // must go to stderr or the MCP client chokes on the stray line.\n let data: McpData;\n try {\n // SAFETY: the snapshot is written by `blume eval` itself as\n // JSON.stringify of an McpData; a hand-mangled file fails the parse and\n // lands in the catch below.\n data = JSON.parse(await readFile(args.data, \"utf-8\")) as McpData;\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `blume mcp-stdio: cannot load the snapshot at ${args.data}: ${detail}\\n`\n );\n process.exit(1);\n }\n await serveMcpStdio(data);\n },\n});\n",
|
|
6
|
+
"import { once } from \"node:events\";\nimport type { Readable, Writable } from \"node:stream\";\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport type { McpData } from \"./data.ts\";\nimport { buildServer, createIndexProvider } from \"./server.ts\";\n\n/** Streams for `serveMcpStdio`, injectable so tests can use in-memory pipes. */\nexport interface McpStdioStreams {\n stdin?: Readable;\n stdout?: Writable;\n}\n\n/**\n * Serve a precomputed MCP data snapshot over a stdio transport until the\n * client hangs up.\n *\n * The SDK transport reads its input stream but never watches for its end, so\n * a client that simply closes the pipe — which is how every MCP host shuts a\n * stdio server down — would leave the process running forever. EOF on the\n * input stream therefore ends the serve loop explicitly.\n */\nexport const serveMcpStdio = async (\n data: McpData,\n streams: McpStdioStreams = {}\n): Promise<void> => {\n const stdin = streams.stdin ?? process.stdin;\n const stdout = streams.stdout ?? process.stdout;\n const transport = new StdioServerTransport(stdin, stdout);\n const server = buildServer(\n data,\n createIndexProvider(data.documents, data.defaultLocale)\n );\n await server.connect(transport);\n await once(stdin, \"end\");\n await transport.close();\n};\n",
|
|
7
|
+
"import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { ServerOptions } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { WebStandardStreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js\";\nimport {\n ErrorCode,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { mountBasePath } from \"../../core/base-path.ts\";\nimport { readCappedText } from \"../../core/request-body.ts\";\nimport type { McpData } from \"./data.ts\";\nimport {\n createIndexProvider,\n getNavigation,\n getPageMarkdown,\n listPages,\n normalizeRoute,\n searchDocs,\n TOOL_INPUTS,\n urlFor,\n} from \"./query.ts\";\nimport type { OramaIndexProvider } from \"./query.ts\";\nimport { MCP_TOOLS } from \"./tools.ts\";\n\nexport { createIndexProvider } from \"./query.ts\";\nexport type { OramaIndexProvider } from \"./query.ts\";\n\n/**\n * The low-level SDK `Server` is used (rather than the high-level `McpServer`)\n * because the latter's `registerTool` is generic over the caller's Zod instance;\n * Blume's zod and the SDK's may resolve to different copies, whose types don't\n * unify. The operations themselves live in `query.ts`, shared with the JSON\n * docs API; this module is the MCP transport over them.\n */\n\n/** Every page resource is the page's agent Markdown. */\nconst RESOURCE_MIME_TYPE = \"text/markdown\";\n/** The MCP spec's JSON-RPC code for an unknown resource URI. */\nconst RESOURCE_NOT_FOUND = -32_002;\n/** URI scheme for page resources when no `deployment.site` is configured. */\nconst LOCAL_RESOURCE_SCHEME = \"blume:\";\n/**\n * The largest request body the HTTP endpoint reads. A JSON-RPC call to these\n * tools is a few hundred bytes; the cap keeps one oversized POST from being\n * buffered whole, as the assistant route's does.\n */\nconst MCP_BODY_LIMIT_BYTES = 65_536;\n\nconst CORS_HEADERS = {\n \"Access-Control-Allow-Headers\":\n \"Content-Type, Mcp-Session-Id, Mcp-Protocol-Version\",\n \"Access-Control-Allow-Methods\": \"GET, POST, OPTIONS\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Expose-Headers\": \"Mcp-Session-Id\",\n};\n\n/**\n * A tool's advertised JSON Schema. The dialect key is dropped (noise in a\n * tools/list payload), as is the root `additionalProperties: false` — the\n * runtime strips unknown keys rather than rejecting them, and the advertised\n * schema shouldn't promise stricter validation than the server performs.\n */\nconst inputSchemaFor = (schema: z.ZodType) => {\n const {\n $schema: _dialect,\n additionalProperties: _closed,\n ...rest\n } = z.toJSONSchema(schema);\n return rest;\n};\n\n/** The `tools/list` payload, derived from shared metadata + input schemas. */\nconst TOOL_DEFINITIONS = MCP_TOOLS.map((tool) => ({\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: inputSchemaFor(\n // SAFETY: TOOL_INPUTS declares a schema for every MCP_TOOLS name; the two\n // lists are maintained together so names and descriptions never drift.\n TOOL_INPUTS[tool.name as keyof typeof TOOL_INPUTS]\n ),\n name: tool.name,\n title: tool.title,\n}));\n\n/** One `resources/list` entry: a page served as `text/markdown`. */\ninterface PageResource {\n description?: string;\n mimeType: string;\n name: string;\n title: string;\n uri: string;\n}\n\n/**\n * A page's resource URI. Resource URIs must be absolute, so this is the page's\n * served URL when a site is configured (the same URL `search_docs` and\n * `list_pages` emit, so an agent can hand either back to `resources/read`),\n * and a `blume:` URI carrying the based route otherwise.\n */\nconst resourceUri = (route: string, data: McpData): string =>\n data.site\n ? urlFor(route, data)\n : `${LOCAL_RESOURCE_SCHEME}${mountBasePath(data.base, route)}`;\n\n/** The `pages` key a resource URI (either form, or a bare route) names. */\nconst resourceRoute = (uri: string, data: McpData): string =>\n normalizeRoute(\n uri.startsWith(LOCAL_RESOURCE_SCHEME)\n ? uri.slice(LOCAL_RESOURCE_SCHEME.length)\n : uri,\n data\n );\n\n/**\n * The `tools/call` params, as far as the server reads them before dispatch:\n * each tool validates its own `arguments` against `TOOL_INPUTS`.\n */\nconst TOOL_CALL_PARAMS = z.object({\n arguments: z.record(z.string(), z.unknown()).optional(),\n name: z.string(),\n});\n\n/** A tool call's `arguments` object, before the tool's own validation. */\ntype ToolArguments = NonNullable<\n z.output<typeof TOOL_CALL_PARAMS>[\"arguments\"]\n>;\n\n/** A tool call's text result, marked as an error when `isError` is set. */\nconst text = (value: string, isError = false) => {\n const content = [{ text: value, type: \"text\" as const }];\n return isError ? { content, isError: true } : { content };\n};\n\n/** Construct a fresh MCP server with Blume's read-only docs tools registered. */\nexport const buildServer = (\n data: McpData,\n index: OramaIndexProvider\n): Server => {\n const capabilities = { resources: {}, tools: {} };\n const serverOptions: ServerOptions = data.instructions\n ? { capabilities, instructions: data.instructions }\n : { capabilities };\n const server = new Server(\n { name: data.name, version: data.version },\n serverOptions\n );\n\n server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: TOOL_DEFINITIONS,\n }));\n\n // Every page doubles as a resource, so a client that attaches context by\n // URI (rather than calling tools) can browse and read the docs too. The\n // list is the same route set `list_pages` returns; reading one serves the\n // same agent Markdown `get_page` does.\n server.setRequestHandler(ListResourcesRequestSchema, () => ({\n resources: data.routes.map((route) => {\n const resource: PageResource = {\n mimeType: RESOURCE_MIME_TYPE,\n name: route.title,\n title: route.title,\n uri: resourceUri(route.route, data),\n };\n if (route.description) {\n resource.description = route.description;\n }\n return resource;\n }),\n }));\n\n server.setRequestHandler(ReadResourceRequestSchema, (request) => {\n const { uri } = request.params;\n const markdown = getPageMarkdown(data, resourceRoute(uri, data));\n if (markdown === undefined) {\n throw new McpError(\n RESOURCE_NOT_FOUND,\n `No page found at \"${uri}\". Use resources/list or list_pages to find valid URIs.`\n );\n }\n return {\n contents: [{ mimeType: RESOURCE_MIME_TYPE, text: markdown, uri }],\n };\n });\n\n const callTool = async (name: string, args: ToolArguments) => {\n if (name === \"search_docs\") {\n const results = await searchDocs(\n data,\n index,\n TOOL_INPUTS.search_docs.parse(args)\n );\n return text(JSON.stringify(results, null, 2));\n }\n\n if (name === \"get_page\") {\n const input = TOOL_INPUTS.get_page.parse(args);\n const key = normalizeRoute(input.route, data);\n const markdown = getPageMarkdown(data, key);\n if (markdown === undefined) {\n return text(\n `No page found at \"${key}\". Use list_pages or search_docs to find valid routes.`,\n true\n );\n }\n return text(markdown);\n }\n\n if (name === \"list_pages\") {\n const listing = listPages(data, TOOL_INPUTS.list_pages.parse(args));\n return text(JSON.stringify(listing, null, 2));\n }\n\n if (name === \"get_navigation\") {\n const result = getNavigation(\n data,\n TOOL_INPUTS.get_navigation.parse(args)\n );\n if (\"error\" in result) {\n return text(result.error, true);\n }\n return text(JSON.stringify(result.navigation, null, 2));\n }\n\n // The MCP spec answers an unknown tool with a protocol error, not a\n // failed tool result.\n throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${name}`);\n };\n\n // `tools/call` goes through the fallback handler rather than a registered\n // one: the SDK parses a registered handler's request against its own\n // schema first and reports a malformed call (`arguments` that aren't an\n // object) as -32603 Internal error carrying a raw Zod dump. Reading the\n // params here answers it as -32602 Invalid params with a short message.\n server.fallbackRequestHandler = async (request) => {\n if (request.method !== \"tools/call\") {\n throw new McpError(ErrorCode.MethodNotFound, \"Method not found\");\n }\n const params = TOOL_CALL_PARAMS.safeParse(request.params);\n if (!params.success) {\n throw new McpError(\n ErrorCode.InvalidParams,\n \"Invalid tools/call params: `name` must be a string and `arguments`, when given, an object.\"\n );\n }\n return await callTool(params.data.name, params.data.arguments ?? {});\n };\n\n return server;\n};\n\n/** A JSON-RPC error response the transport would send, for errors raised before it runs. */\nconst jsonRpcError = (\n status: number,\n code: number,\n message: string\n): Response =>\n Response.json(\n { error: { code, message }, id: null, jsonrpc: \"2.0\" },\n { headers: CORS_HEADERS, status }\n );\n\n/**\n * Build a stateless Streamable-HTTP MCP request handler from a data snapshot.\n *\n * The Orama index is built once and reused; a fresh `Server` and transport are\n * created per request (required by the SDK's stateless mode, which skips session\n * tracking). `enableJsonResponse` makes each call a plain request/response — no\n * SSE — which suits read-only docs tools and runs on any adapter (Node, Vercel,\n * Netlify, Cloudflare). CORS is added so browser-based connectors (e.g.\n * claude.ai) can reach the endpoint.\n */\nexport const createMcpFetchHandler = (\n data: McpData\n): ((request: Request) => Promise<Response>) => {\n const index = createIndexProvider(data.documents, data.defaultLocale);\n\n return async (request: Request): Promise<Response> => {\n if (request.method === \"OPTIONS\") {\n return new Response(null, { headers: CORS_HEADERS, status: 204 });\n }\n if (request.method === \"GET\") {\n // No server-initiated streams are needed for read-only tools.\n return new Response(\"Method Not Allowed\", {\n headers: { ...CORS_HEADERS, Allow: \"POST, OPTIONS\" },\n status: 405,\n });\n }\n\n // The transport would read the body with `request.json()`, buffering\n // whatever a client sends; read it under the cap first and hand the\n // transport a copy.\n let forwarded = request;\n if (request.method === \"POST\") {\n const body = await readCappedText(request, MCP_BODY_LIMIT_BYTES);\n if (body === undefined) {\n return jsonRpcError(\n 413,\n ErrorCode.InvalidRequest,\n \"Request too large: the body must be at most 64 KB.\"\n );\n }\n forwarded = new Request(request.url, {\n body,\n headers: request.headers,\n method: \"POST\",\n });\n }\n\n const server = buildServer(data, index);\n const transport = new WebStandardStreamableHTTPServerTransport({\n enableJsonResponse: true,\n // The SDK enables stateless mode only when this is `undefined`; `null` is\n // not an accepted value for the `(() => string) | undefined` option.\n // oxlint-disable-next-line sonarjs/no-undefined-assignment\n sessionIdGenerator: undefined,\n });\n await server.connect(transport);\n const response = await transport.handleRequest(forwarded);\n\n const headers = new Headers(response.headers);\n for (const [key, value] of Object.entries(CORS_HEADERS)) {\n headers.set(key, value);\n }\n return new Response(response.body, {\n headers,\n status: response.status,\n statusText: response.statusText,\n });\n };\n};\n",
|
|
8
|
+
"import { z } from \"zod\";\n\nimport {\n mountBasePath,\n normalizeRoute as normalizePageRoute,\n stripBasePath,\n} from \"../../core/base-path.ts\";\nimport { absoluteUrl } from \"../../core/site-url.ts\";\nimport { trimEnd } from \"../../core/trim.ts\";\nimport type { Navigation } from \"../../core/types.ts\";\nimport { buildOramaIndex, queryOramaIndex } from \"../../search/orama-index.ts\";\nimport type { OramaDoc } from \"../../search/orama-index.ts\";\nimport type { McpData } from \"./data.ts\";\n\n/**\n * The read-only docs operations behind the agent-facing surfaces — search,\n * page lookup, the page listing, and the navigation tree — over an\n * {@link McpData} snapshot. The MCP server (`server.ts`) and the JSON docs API\n * (`ai/api/handlers.ts`) are both thin transports over these, so a tool call\n * and its REST twin can never answer differently. SDK-free on purpose: the\n * prerendered API endpoints import this module without pulling the MCP SDK\n * into their bundle.\n *\n * Each input is defined once in Zod: the runtime parse and the JSON Schema\n * advertised by `tools/list` (via `z.toJSONSchema`) derive from the same\n * definition, so they cannot drift.\n */\n\n/** Default and maximum number of hits returned by a search. */\nexport const DEFAULT_SEARCH_LIMIT = 8;\nexport const MAX_SEARCH_LIMIT = 20;\n/** Excerpt length when a page has no description. */\nconst EXCERPT_LENGTH = 200;\n\n// Each field is a preprocess pipe: the input side accepts the sloppy shapes\n// LLM callers actually send (a bare string for an array field, `[]`/`{}`\n// meaning \"no filter\", out-of-range limits clamped rather than rejected), and\n// the pipe's *output* side is the clean shape — which is exactly what\n// `z.toJSONSchema` emits for `tools/list`. No coercion can ever fail, so a\n// tool call is never rejected on argument shape, matching the previous\n// hand-rolled coercions.\n\n/**\n * The optional content-type filter `search_docs` and `list_pages` share.\n * `[]` or no usable strings mean \"no filter\", not \"match nothing\"; a bare\n * string is accepted as a one-element list.\n */\nconst contentTypesField = z.preprocess((value) => {\n const list = (Array.isArray(value) ? value : [value]).filter(\n (entry): entry is string => typeof entry === \"string\"\n );\n return list.length > 0 ? list : undefined;\n}, z.array(z.string()).optional().describe('Only include pages of these content types (frontmatter `type`, e.g. `[\"doc\", \"rfc\"]`). `list_pages` shows each page\\'s type. Omit to include every type.'));\n\n/**\n * The optional facet filter `search_docs` and `list_pages` share. A value\n * is read the way `pageFacets` stores one: a string as-is, a number or\n * boolean stringified (`{\"priority\": 1}` matches the facet `\"1\"`). Entries of\n * any other shape are dropped; an empty `{}` means \"no filter\".\n */\n/** Accepts any plain object, so the usable entries can be sifted out. */\nconst looseFacetObject = z.record(z.string(), z.unknown());\n\n/** A filter value a facet can hold, stringified like `pageFacets` does. */\nconst facetFilterValue = z\n .union([z.string(), z.number(), z.boolean()])\n .transform(String);\n\nconst filtersField = z.preprocess((value) => {\n const candidate = looseFacetObject.safeParse(value);\n if (!candidate.success) {\n return;\n }\n const entries = Object.entries(candidate.data).flatMap(([key, entry]) => {\n const parsed = facetFilterValue.safeParse(entry);\n return parsed.success ? [[key, parsed.data] as const] : [];\n });\n return entries.length > 0 ? Object.fromEntries(entries) : undefined;\n}, z.record(z.string(), z.string()).optional().describe('Only include pages matching every facet, key → required value (e.g. `{\"status\": \"enforced\"}`). Facets are metadata the site declares per content type; `list_pages` shows each page\\'s facet values. Omit for no facet filtering.'));\n\n/** Clamped into range rather than rejected; non-numeric means the default. */\nconst limitField = z.preprocess(\n (value) => {\n // `Number` is the identity on numbers, so one conversion covers both the\n // well-typed call and a numeric string.\n const num = Number(value);\n return Number.isFinite(num)\n ? Math.min(Math.max(Math.trunc(num), 1), MAX_SEARCH_LIMIT)\n : undefined;\n },\n z\n .int()\n .min(1)\n .max(MAX_SEARCH_LIMIT)\n .optional()\n .describe(`Maximum hits to return (default ${DEFAULT_SEARCH_LIMIT}).`)\n);\n\n/** A required text field; a missing or non-string value coerces to \"\". */\nconst textField = (description: string) =>\n z.preprocess((value) => {\n const parsed = z.string().safeParse(value);\n return parsed.success ? parsed.data : \"\";\n }, z.string().describe(description));\n\n/** An optional trimmed text field; blank or non-string means \"absent\". */\nconst optionalTextField = (description: string) =>\n z.preprocess((value) => {\n const parsed = z.string().safeParse(value);\n const trimmed = parsed.success ? parsed.data.trim() : \"\";\n return trimmed || undefined;\n }, z.string().optional().describe(description));\n\n/** The optional locale filter `search_docs` and `list_pages` share. */\nconst localeField = optionalTextField(\n \"Only include pages in this locale (e.g. `fr`). Omit for every language.\"\n);\n\n/** The optional docs-version scope `search_docs` and `list_pages` share. */\nconst versionField = optionalTextField(\n 'Docs version to scope to on a versioned site: `\"latest\"` (the default — current docs only), `\"all\"` (every version), or an archived version id (e.g. `\"v1.0\"`). Ignored when the site is unversioned.'\n);\n\n/** Every tool's input schema — the runtime parse and tools/list source. */\nexport const TOOL_INPUTS = {\n get_navigation: z.object({\n locale: optionalTextField(\n \"Locale whose navigation tree to return (defaults to the default locale).\"\n ),\n version: optionalTextField(\n \"Archived version id whose tree to return (defaults to the current docs).\"\n ),\n }),\n get_page: z.object({\n route: textField(\"The page route, e.g. `/guides/install`.\"),\n }),\n list_pages: z.object({\n contentTypes: contentTypesField,\n filters: filtersField,\n locale: localeField,\n version: versionField,\n }),\n search_docs: z.object({\n contentTypes: contentTypesField,\n filters: filtersField,\n limit: limitField,\n locale: localeField,\n query: textField(\"The search query.\"),\n version: versionField,\n }),\n};\n\n/** One `search_docs` result entry; `version` only appears on versioned sites. */\nexport interface SearchHitPayload {\n contentType: string | undefined;\n excerpt: string;\n facets: Record<string, string> | undefined;\n route: string;\n title: string;\n url: string;\n version?: string;\n}\n\n/** One `list_pages` entry; `version` only appears on versioned sites. */\nexport interface PageListingPayload {\n contentType: string;\n description: string | undefined;\n facets: Record<string, string> | undefined;\n lastModified: string | null;\n route: string;\n title: string;\n url: string;\n version?: string;\n}\n\n/** Whether a page's facet values satisfy every requested filter entry. */\nconst matchesFacets = (\n facets: Record<string, string> | undefined,\n filters: Record<string, string>\n): boolean =>\n Object.entries(filters).every(([key, value]) => facets?.[key] === value);\n\n/**\n * Resolve the `version` scope on a versioned site: `undefined` disables the\n * filter (`\"all\"`), `\"\"` is the current docs (the default — agents almost\n * always want the live documentation), and anything else is an archived id\n * (an unknown id simply matches nothing). On an unversioned site the input is\n * ignored entirely. The input arrives pre-trimmed (blank coerced to absent)\n * from the tool's input schema.\n */\nconst asVersionScope = (\n value: string | undefined,\n data: McpData\n): string | undefined => {\n if (!data.archivedVersions) {\n return;\n }\n if (value === \"all\") {\n return;\n }\n if (value === undefined || value === \"latest\" || value === \"current\") {\n return \"\";\n }\n return value;\n};\n\n/**\n * Error message for a `get_navigation` version id that isn't a configured\n * archived version, or `null` when the id is valid (or the site is\n * unversioned, where the id is ignored like the other tools' scopes). Unlike\n * `asVersionScope`'s match-nothing filters, a bad id here would otherwise\n * silently return the *current* tree posing as the requested snapshot.\n */\nconst unknownVersionError = (\n versionId: string | undefined,\n data: McpData\n): string | null =>\n versionId &&\n data.archivedVersions &&\n !data.archivedVersions.includes(versionId)\n ? `Unknown version \"${versionId}\". Archived versions: ${data.archivedVersions.join(\", \")}.`\n : null;\n\n/**\n * Normalize a user-supplied route to a `pages` key (`/`, `/a/b`, no suffix).\n * Accepts a full URL too — `search_docs` hits and llms.txt entries carry\n * `site` + `deployment.base`, and an agent following \"pass a route from\n * `search_docs`\" will hand one straight back — reducing it to its base-less,\n * percent-decoded path.\n */\nexport const normalizeRoute = (input: string, data: McpData): string => {\n let value = input.trim();\n if (/^https?:\\/\\//iu.test(value)) {\n try {\n value = new URL(value).pathname;\n } catch {\n // Not parseable as a URL after all; treat it as a path.\n }\n }\n try {\n value = decodeURI(value);\n } catch {\n // Malformed percent sequence — compare it as written.\n }\n // Trailing slashes come off before the suffix so `/a/b.md/` still loses its\n // `.md`; normalizePageRoute then settles the leading slash.\n const noSuffix = trimEnd(value, \"/\").replace(/\\.mdx?$/u, \"\");\n return stripBasePath(data.base, normalizePageRoute(noSuffix));\n};\n\n/** Build the absolute (or root-relative) URL for a route. */\nexport const urlFor = (route: string, data: McpData): string => {\n // Routes are base-less manifest paths; layer `deployment.base` on top so the\n // URL matches where the page is served (the sitemap/llms.txt convention).\n const path = mountBasePath(data.base, route);\n // Concatenate rather than `new URL(path, site)` — a root-absolute path\n // would drop the base path of a subpath deployment (`acme.com/docs`).\n return data.site ? absoluteUrl(data.site, path) : path;\n};\n\n/** A hit's excerpt: its description, else the head of its content with an\n * ellipsis only when something was actually cut off. */\nconst excerptFor = (doc: OramaDoc): string => {\n if (doc.description) {\n return doc.description;\n }\n const head = doc.content.slice(0, EXCERPT_LENGTH).trim();\n return doc.content.length > EXCERPT_LENGTH ? `${head}…` : head;\n};\n\n/** Lazily builds the Orama index over a snapshot's documents, once. */\nexport type OramaIndexProvider = () => Promise<\n Awaited<ReturnType<typeof buildOramaIndex>>\n>;\n\n/**\n * Memoize the search index so every server built from a snapshot shares it.\n * `locale` is the snapshot's `defaultLocale`, forwarded so non-Latin scripts\n * (Japanese and Chinese, but equally Cyrillic, Greek, Hebrew, Devanagari…)\n * get a word-segmenting tokenizer.\n */\nexport const createIndexProvider = (\n documents: OramaDoc[],\n locale?: string\n): OramaIndexProvider => {\n let dbPromise: ReturnType<OramaIndexProvider> | null = null;\n return function provideIndex() {\n dbPromise ??= buildOramaIndex(documents, locale);\n return dbPromise;\n };\n};\n\n/** Full-text search over the snapshot; the `search_docs` operation. */\nexport const searchDocs = async (\n data: McpData,\n index: OramaIndexProvider,\n input: z.output<typeof TOOL_INPUTS.search_docs>\n): Promise<SearchHitPayload[]> => {\n const db = await index();\n const hits = await queryOramaIndex(\n db,\n input.query,\n input.limit ?? DEFAULT_SEARCH_LIMIT,\n {\n contentTypes: input.contentTypes,\n facets: input.filters,\n locale: input.locale,\n version: asVersionScope(input.version, data),\n }\n );\n // `route` is the key `get_page` takes (the tool descriptions promise\n // it); `url` is where the page is served.\n return hits.map((doc: OramaDoc) => {\n const hit: SearchHitPayload = {\n contentType: doc.contentType,\n excerpt: excerptFor(doc),\n facets: doc.facets,\n route: doc.route,\n title: doc.title,\n url: urlFor(doc.route, data),\n };\n if (data.archivedVersions) {\n hit.version = doc.version ?? \"\";\n }\n return hit;\n });\n};\n\n/** A page's agent Markdown, or `undefined` when no page has the route. */\nexport const getPageMarkdown = (\n data: McpData,\n route: string\n): string | undefined => data.pages[route];\n\n/** Every non-hidden route matching the filters; the `list_pages` operation. */\nexport const listPages = (\n data: McpData,\n input: z.output<typeof TOOL_INPUTS.list_pages>\n): PageListingPayload[] => {\n const { contentTypes, filters, locale } = input;\n const versionScope = asVersionScope(input.version, data);\n return data.routes\n .filter(\n (route) =>\n (!contentTypes || contentTypes.includes(route.contentType)) &&\n (!filters || matchesFacets(route.facets, filters)) &&\n (!locale || route.locale === locale) &&\n (versionScope === undefined || route.version === versionScope)\n )\n .map((route) => {\n const listing: PageListingPayload = {\n contentType: route.contentType,\n description: route.description,\n facets: route.facets,\n lastModified: route.lastModified,\n route: route.route,\n title: route.title,\n url: urlFor(route.route, data),\n };\n if (data.archivedVersions) {\n listing.version = route.version;\n }\n return listing;\n });\n};\n\n/**\n * The navigation tree for a locale and version; the `get_navigation`\n * operation. A version id selects the snapshot's tree; a locale selects its\n * language (falling back through the default locale to any tree the snapshot\n * has). Without a version, a locale selects the current docs' localized\n * tree. An unknown id on a versioned site is an error string — the current\n * tree would silently masquerade as the requested snapshot.\n */\nexport const getNavigation = (\n data: McpData,\n input: z.output<typeof TOOL_INPUTS.get_navigation>\n): { error: string } | { navigation: Navigation } => {\n const { locale, version: versionId } = input;\n const unknownVersion = unknownVersionError(versionId, data);\n if (unknownVersion) {\n return { error: unknownVersion };\n }\n let { navigation } = data;\n const byLocale = versionId\n ? data.navigationByVersion?.[versionId]\n : undefined;\n if (byLocale) {\n navigation =\n (locale ? byLocale[locale] : undefined) ??\n byLocale[data.defaultLocale ?? \"\"] ??\n Object.values(byLocale)[0] ??\n navigation;\n } else if (locale && data.navigationByLocale?.[locale]) {\n navigation = data.navigationByLocale[locale];\n }\n return { navigation };\n};\n",
|
|
9
|
+
"import { create, insert, insertMultiple, search } from \"@orama/orama\";\nimport type {\n AnyOrama,\n EnumArrComparisonOperator,\n EnumComparisonOperator,\n Result,\n SearchParamsFullText,\n Tokenizer,\n} from \"@orama/orama\";\n\nimport { foldLatin, foldLatinTerm, stripSoftHyphens } from \"./fold.ts\";\n\n/**\n * The minimal document shape both the client-side search dialog and the\n * server-side MCP `search_docs` tool index. Mirrors the `blume-search.json`\n * entries built by `buildSearchDocuments`.\n */\nexport interface OramaDoc {\n content: string;\n description: string;\n route: string;\n title: string;\n /** Locale code; indexed as an enum so queries can filter to one language. */\n locale?: string;\n /**\n * Docs version; indexed as an enum so queries can filter to one version.\n * The current docs carry `\"\"`, which the enum stores and matches exactly.\n */\n version?: string;\n /** Resolved page `type`; indexed as an enum so queries can filter by type. */\n contentType?: string;\n /** Declared facet values (`content.types.<type>.facets`), key → value. */\n facets?: Record<string, string>;\n /** Carried through for the search dialog's breadcrumb + filter pills. Stored\n * but not indexed, so they ride along on the returned document untouched. */\n breadcrumb?: string[];\n section?: string;\n /** The page's relevance multiplier (`search.boost`); stored, not indexed. */\n boost?: number;\n /** Extra terms the page is found by (`search.keywords`). */\n keywords?: string[];\n}\n\nconst SCHEMA = {\n content: \"string\",\n // Enums (not full-text \"string\") so `where` does an exact-match filter.\n contentType: \"enum\",\n description: \"string\",\n // Facets, flattened to `key:value` terms — enum[] so one static schema\n // serves every project's facet keys, with `containsAll` matching a filter\n // set. Derived from `facets` at insert time.\n facetTerms: \"enum[]\",\n keywords: \"string[]\",\n locale: \"enum\",\n route: \"string\",\n title: \"string\",\n version: \"enum\",\n} as const;\n\n/** Flatten a facet map to the `key:value` terms the `facetTerms` enum holds. */\nconst toFacetTerms = (facets: Record<string, string>): string[] =>\n Object.entries(facets).map(([key, value]) => `${key}:${value}`);\n\n/**\n * Title and description outrank body text, matching the search dialog; a\n * page's keywords count as much as its title, since its author chose them.\n */\nconst BOOST = { description: 2, keywords: 3, title: 3 };\n\n/** Databases holding a boosted page, which rank by boosted score. */\nconst boostedDatabases = new WeakSet<AnyOrama>();\n\n/** A search match as Orama's sort function sees it: id, score, document. */\ntype RankedMatch = [unknown, number, OramaDoc];\n\n/** Rank matches by relevance times the page's `search.boost`. */\nconst byBoostedScore = (a: RankedMatch, b: RankedMatch): number =>\n b[1] * (b[2].boost ?? 1) - a[1] * (a[2].boost ?? 1);\n\n/**\n * The script {@link latinTokenizer} indexes. {@link LATIN_SEPARATOR} splits on\n * every character of any other script, so such text collapses to zero tokens\n * and every query silently returns no hits. Unspaced scripts are the\n * best-known casualty, but the failure is not about spacing — Russian, Greek,\n * Hebrew and Hindi lose their tokens the same way Japanese does.\n */\nconst LATIN_SCRIPT = \"Latn\";\n\n/**\n * Parse a locale tag, tolerating the legacy forms that reach here from\n * hand-written config: underscores (`ru_RU`), POSIX suffixes (`ja_JP.UTF-8`,\n * `zh_TW@Big5`), and extlang tags ICU rejects (`zh-cmn-Hans`). The POSIX\n * codeset/modifier suffix is dropped first — it carries no BCP 47 meaning,\n * and dropping it keeps an explicit script subtag in play: `az_Cyrl.UTF-8`\n * must resolve to `Cyrl`, not fall back to `az` and maximize to Latin.\n * `Intl.Locale` still throws on extlang forms, so a failed parse retries with\n * the primary language subtag alone — enough to resolve the script, which is\n * all that gates the tokenizer. Returns `undefined` when even that subtag is\n * unparseable.\n */\nconst parseLocale = (tag: string): Intl.Locale | undefined => {\n const hyphenated = tag.replace(/[.@].*$/u, \"\").replaceAll(\"_\", \"-\");\n try {\n return new Intl.Locale(hyphenated);\n } catch {\n // Retry below with the primary subtag.\n }\n const [primary = \"\"] = hyphenated.split(\"-\");\n try {\n return new Intl.Locale(primary);\n } catch {\n return undefined;\n }\n};\n\n/**\n * The maximized locale — script and language filled in from CLDR likely-subtags\n * and alias data, so `ru` resolves to `Cyrl`, `cmn` to the canonical `zh`, and\n * an explicit script subtag is honored: `sr-Latn` reports `Latn` while\n * `az-Cyrl` reports `Cyrl`. `undefined` for tags {@link parseLocale} cannot\n * salvage; a well-formed tag ICU knows nothing about keeps an `undefined`\n * script instead.\n */\nconst resolveLocale = (tag: string): Intl.Locale | undefined =>\n parseLocale(tag)?.maximize();\n\n/**\n * Indexes whose language maximizes to one of these scripts are built as\n * character bigrams rather than whole segments, and queried to match\n * accordingly. Japanese and Chinese write compounds in {@link BIGRAM_SCRIPTS}\n * without delimiters, and the writing system — not the language subtag — is\n * what carries that convention: `yue` (Cantonese) maximizes to `Hant` and\n * needs bigrams the same way `zh` does. Korean maximizes to `Kore` and Thai\n * to `Thai`; Korean separates words with spaces and Thai has no comparable\n * bigram convention, so both keep the plain segmented tokens even where a\n * page mixes in Han or kana.\n *\n * Dictionary segmentation alone drops the adjacency that makes a compound term\n * distinctive: 資金決済法 becomes 資金 / 決済 / 法, and because Orama scores a\n * bag of words, a page that merely mentions each fragment somewhere outranks\n * the page about the law itself. Bigrams put that adjacency back as index\n * terms, and dropping the whole-segment tokens keeps a fragment as common as\n * 法 from matching on its own.\n */\nconst BIGRAM_INDEX_SCRIPTS = new Set([\"Hans\", \"Hant\", \"Jpan\"]);\n\n/**\n * Whether the index keyed to this tokenizer language is built from bigrams.\n * {@link segmentingTokenizer} stores the canonical maximized language, so the\n * builder and the strict pass in {@link queryOramaIndex} share this predicate\n * — an index is never built from bigrams that the query side then matches\n * loosely, or vice versa.\n */\nconst isBigramLanguage = (language: string): boolean => {\n const script = resolveLocale(language)?.script;\n return script !== undefined && BIGRAM_INDEX_SCRIPTS.has(script);\n};\n\n/**\n * Segments written entirely in these scripts are the ones re-cut into bigrams,\n * matching the scripts Lucene's CJK analyzer bigrams. Property escapes rather\n * than ranges, so ideographs outside the basic plane are covered as well —\n * 𠮟, the 常用漢字表 form of しかる, is one. The literals that follow belong to\n * no script of their own but appear only inside such words: the iteration\n * marks, the prolonged sound mark, and the halfwidth voiced sound marks.\n */\nconst BIGRAM_SCRIPTS =\n /^[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}々〆〇ー゙゚]+$/u;\n\n/**\n * One index term inside a word-like segment. Being word-like does not make a\n * segment all letters: UAX #29 keeps connector punctuation, format characters\n * and mid-number punctuation *within* a word, so `Intl.Segmenter` reports\n * スネーク_ケース and robots.txt as one segment each. A term is a run of\n * letters, combining marks and digits. Marks are spelling, not punctuation —\n * Thai writes vowels and tones as combining marks, so dropping them leaves\n * consonant skeletons that collapse distinct words (เสื้อ, shirt, and เสือ,\n * tiger, differ by one mark). Two separators stay where the surrounding text\n * makes them part of the word: an apostrophe followed by a letter (don't),\n * and a decimal point or thousands separator flanked by digits (1.0.3,\n * 1,000) — split, either would leave one-letter and one-digit fragments that\n * co-occur on unrelated pages.\n */\nconst TERM =\n /[\\p{L}\\p{M}\\p{N}]+(?:(?:['’](?=\\p{L})|(?<=\\p{N})[.,](?=\\p{N}))[\\p{L}\\p{M}\\p{N}]+)*/gu;\n\nconst LATIN_SEPARATOR = /[^\\p{Script=Latin}0-9_'-]+/u;\n\n/**\n * Modeled on Orama's default tokenizer, with the splitter widened from the\n * accented vowels it keeps (`àèéìòóù`) to every Latin letter: Prüfung, når and\n * İletişim index as whole words rather than splitting at ü, å and İ. Words\n * fold with {@link foldLatin}, and ASCII text tokenizes exactly as in Orama's.\n */\nconst latinTokenizer = (): Tokenizer => ({\n language: \"english\",\n normalizationCache: new Map(),\n tokenize: (raw: string): string[] => [\n ...new Set(\n foldLatin(raw.toLowerCase()).split(LATIN_SEPARATOR).filter(Boolean)\n ),\n ],\n});\n\n/**\n * Emit every overlapping 2-character window of `run`, or the lone character.\n * Windows are cut by code point: an ideograph outside the basic plane is a\n * surrogate pair, and slicing by code unit would split it into halves that\n * match nothing.\n *\n * Dropping the whole-segment tokens means a single-character query reaches\n * only pages where the character opens a bigram: a run-final 法 sits in 示法,\n * which the query 法 does not prefix-match. Lucene's CJK analyzer shares this\n * property; indexing lone characters alongside the bigrams would reinvite the\n * fragment noise this file exists to remove.\n */\nconst addBigrams = (run: string, tokens: Set<string>): void => {\n const characters = [...run];\n if (characters.length === 1) {\n tokens.add(run);\n return;\n }\n let previous = \"\";\n for (const character of characters) {\n if (previous) {\n tokens.add(previous + character);\n }\n previous = character;\n }\n};\n\n/**\n * `Intl.Segmenter` is missing on some runtimes even though the lib type\n * declares it, so the constructor's presence is probed before use.\n */\nconst hasSegmenter = (\n segmenter: typeof Intl.Segmenter | undefined\n): segmenter is typeof Intl.Segmenter => typeof segmenter === \"function\";\n\n/**\n * A word-segmenting tokenizer for languages the default splitter can't handle,\n * built on `Intl.Segmenter` (the same engine `@orama/tokenizers` wraps).\n * Input is NFC-normalized and lowercased before segmenting — unlike the\n * upstream tokenizers — so decomposed text (macOS filenames, some CMS\n * pipelines) indexes the same terms a composed query produces, and Latin\n * terms (\"GDPR\", English pages on a mixed-locale site) still match\n * case-insensitively, with their diacritics folded by\n * {@link foldLatinTerm}. Soft hyphens go first, so a word they break still\n * segments whole. Returns `undefined` for scripts the default\n * tokenizer already serves ({@link resolveLocale} decides, so `sr-Latn` keeps\n * the default while `az-Cyrl` is segmented), and on runtimes without\n * `Intl.Segmenter`, where the caller falls back to {@link latinTokenizer}.\n *\n * On a {@link BIGRAM_INDEX_SCRIPTS} index, runs of adjacent\n * {@link BIGRAM_SCRIPTS} segments are joined and re-cut into character\n * bigrams; everything else (Latin, digits, and every segment on a Korean or\n * Thai index) is emitted one {@link TERM} at a time. Separators end a run\n * either way — whether they stand between segments, as 「クーリング・オフ」\n * does, or inside one.\n */\nconst segmentingTokenizer = (locale?: string): Tokenizer | undefined => {\n const resolved = resolveLocale(locale ?? \"\");\n if (!resolved?.script || resolved.script === LATIN_SCRIPT) {\n return;\n }\n if (!hasSegmenter(Intl.Segmenter)) {\n return;\n }\n // The canonical maximized language (`cmn` → `zh`), so the bigram predicate\n // here and the strict query pass read the same name.\n const { language } = resolved;\n const segmenter = new Intl.Segmenter(language, { granularity: \"word\" });\n const bigram = isBigramLanguage(language);\n return {\n language,\n normalizationCache: new Map(),\n tokenize: (raw: string): string[] => {\n const tokens = new Set<string>();\n let run = \"\";\n const flush = (): void => {\n if (run) {\n addBigrams(run, tokens);\n run = \"\";\n }\n };\n const take = (term: string): void => {\n if (bigram && BIGRAM_SCRIPTS.test(term)) {\n run += term;\n return;\n }\n flush();\n tokens.add(foldLatinTerm(term));\n };\n for (const segment of segmenter.segment(\n stripSoftHyphens(raw).normalize(\"NFC\").toLowerCase()\n )) {\n if (!segment.isWordLike) {\n flush();\n continue;\n }\n let end = 0;\n for (const match of segment.segment.matchAll(TERM)) {\n // A gap means punctuation stood there, which ends the run as surely\n // as a non-word-like segment would: スネーク_ケース pairs either side\n // of the connector, never across it.\n if (match.index > end) {\n flush();\n }\n take(match[0]);\n end = match.index + match[0].length;\n }\n if (end < segment.segment.length) {\n flush();\n }\n }\n flush();\n return [...tokens];\n },\n };\n};\n\n/**\n * On a Latin-default index holding pages in non-Latin locales, the tokenizer\n * each of those locales' pages was indexed with, keyed by locale code. A query\n * reads it to tokenize its term the way the pages it searches were.\n */\nconst translationTokenizers = new WeakMap<AnyOrama, Map<string, Tokenizer>>();\n\n/** A segmenting tokenizer for each document locale in a non-Latin script. */\nconst localeTokenizers = (documents: OramaDoc[]): Map<string, Tokenizer> => {\n const tokenizers = new Map<string, Tokenizer>();\n for (const code of new Set(documents.map((doc) => doc.locale ?? \"\"))) {\n const tokenizer = segmentingTokenizer(code);\n if (tokenizer) {\n tokenizers.set(code, tokenizer);\n }\n }\n return tokenizers;\n};\n\n/**\n * One tokenizer that hands each text to its locale's: Orama passes the\n * language given to `insert` and `search` through to `tokenize`, so a page\n * inserted under `ja` and a query searched under `ja` both reach the Japanese\n * tokenizer, and everything else reaches {@link latinTokenizer}, exactly as on\n * an index with no translations.\n */\nconst dispatchingTokenizer = (\n tokenizers: Map<string, Tokenizer>\n): Tokenizer => {\n const standard = latinTokenizer();\n return {\n language: standard.language,\n normalizationCache: standard.normalizationCache,\n tokenize: (raw, language) =>\n (tokenizers.get(language ?? \"\") ?? standard).tokenize(raw),\n };\n};\n\n/**\n * Build an in-memory Orama full-text index from search documents. Shared by the\n * Orama client loader (browser), the MCP server, and assistant grounding (Node),\n * so ranking is identical wherever docs are queried. `locale` — the site's\n * `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for every\n * non-Latin script, all of which {@link latinTokenizer} reduces to zero\n * tokens. A non-Latin default's tokenizer serves every page: Latin words\n * survive segmentation intact, so English pages on a segmented index stay\n * searchable. A Latin default keeps {@link latinTokenizer} for its Latin-script\n * pages, and each page in a non-Latin locale (the Japanese and Hindi\n * translations of an English site) is indexed with its own locale's tokenizer.\n */\nexport const buildOramaIndex = async (\n documents: OramaDoc[],\n locale?: string\n): Promise<AnyOrama> => {\n const rows = documents.map((doc) =>\n doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc\n );\n const tokenizer = segmentingTokenizer(locale);\n const translations = tokenizer ? undefined : localeTokenizers(documents);\n let db: AnyOrama;\n if (translations && translations.size > 0) {\n db = create({\n components: { tokenizer: dispatchingTokenizer(translations) },\n schema: SCHEMA,\n });\n for (const row of rows) {\n // oxlint-disable-next-line no-await-in-loop -- one at a time, in order, so each page is tokenized under its own locale and ties still rank in document order.\n await insert(db, row, row.locale);\n }\n translationTokenizers.set(db, translations);\n } else {\n db = create({\n components: { tokenizer: tokenizer ?? latinTokenizer() },\n schema: SCHEMA,\n });\n await insertMultiple(db, rows);\n }\n if (documents.some((doc) => (doc.boost ?? 1) !== 1)) {\n boostedDatabases.add(db);\n }\n return db;\n};\n\n/** Orama keeps only documents matching every token at a threshold of 0. */\nconst ALL_TOKENS = 0;\n\n/** Optional exact-match filters applied to a query via Orama's `where`. */\nexport interface OramaQueryFilters {\n /** Keep only documents whose `contentType` is in this list. */\n contentTypes?: string[];\n /**\n * Keep only documents matching every facet, key → required value. Facet\n * keys and values come from the `facets` field on the indexed documents.\n */\n facets?: Record<string, string>;\n /** Keep only documents in this locale. */\n locale?: string;\n /**\n * Keep only documents of this docs version (`\"\"` is the current docs — a\n * meaningful filter value, so absence alone disables version filtering).\n */\n version?: string;\n}\n\n/**\n * The exact-match `where` clause the filters compile to. Orama types `where`\n * openly (any schema property to an operator), mirrored here by the index\n * signature; this module only ever emits the two enum operators.\n */\ninterface OramaWhereClause {\n [property: string]:\n | EnumArrComparisonOperator\n | EnumComparisonOperator\n | undefined;\n}\n\n/** A ranked match, scored and carrying its document. */\ntype Hit = Result<OramaDoc>;\n\n/**\n * One search, with the term tokenized under `language`. On a bigrammed\n * tokenizer the strict pass runs first: a term is only meant to match where\n * its bigrams sit together, and scoring them independently lets a page\n * sharing a couple of windows outrank the page the term is about. Terms\n * spanning several words rarely appear in full on one page, so an empty\n * strict result falls back to the default pass rather than reporting no\n * matches.\n */\nconst rankedHits = async (\n db: AnyOrama,\n params: SearchParamsFullText<AnyOrama, OramaDoc>,\n language: string | undefined,\n bigrammed: boolean\n): Promise<Hit[]> => {\n // The result-document generic is OramaDoc because `buildOramaIndex` is the\n // only writer to this database and inserts OramaDoc records (plus the\n // derived `facetTerms`).\n const strict = bigrammed\n ? await search<AnyOrama, OramaDoc>(\n db,\n { ...params, threshold: ALL_TOKENS },\n language\n )\n : undefined;\n const found =\n strict && strict.hits.length > 0\n ? strict\n : await search<AnyOrama, OramaDoc>(db, params, language);\n return found.hits;\n};\n\n/** A hit's rank across searches: its relevance times the page's boost. */\nconst boostedScore = (hit: Hit): number =>\n hit.score * (hit.document.boost ?? 1);\n\n/**\n * Query the index, returning the matching documents (highest-ranked first).\n * `filters` narrows results by exact `where` matches on the enum fields:\n * `locale` to one language, `contentTypes` to a set of page types, `facets`\n * to documents carrying every requested `key:value` term.\n *\n * On an index whose translations have tokenizers of their own, a query scoped\n * to a locale is tokenized with that locale's tokenizer. An unscoped query\n * searches each locale's pages with its own tokenizer, and the Latin-script\n * pages with {@link latinTokenizer}, then merges the matches by score: every\n * search runs over the one index, so their scores share its term statistics.\n */\nexport const queryOramaIndex = async (\n db: AnyOrama,\n term: string,\n limit: number,\n filters?: OramaQueryFilters\n): Promise<OramaDoc[]> => {\n const facetTerms = filters?.facets ? toFacetTerms(filters.facets) : [];\n const where: OramaWhereClause = {};\n if (filters?.locale) {\n where.locale = { eq: filters.locale };\n }\n // `\"\"` (the current docs) is a real filter value, so test for presence.\n if (filters?.version !== undefined) {\n where.version = { eq: filters.version };\n }\n if (filters?.contentTypes && filters.contentTypes.length > 0) {\n where.contentType = { in: filters.contentTypes };\n }\n if (facetTerms.length > 0) {\n where.facetTerms = { containsAll: facetTerms };\n }\n // Boosts multiply every match's score before the limit applies (Orama\n // sorts the whole match set with a sort function), so a boosted page can\n // rise past matches outside the first page.\n const unfiltered = {\n boost: BOOST,\n limit,\n properties: [\"title\", \"keywords\", \"description\", \"content\"],\n ...(boostedDatabases.has(db) && { sortBy: byBoostedScore }),\n term,\n };\n const params =\n Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;\n const translations = translationTokenizers.get(db);\n if (!translations) {\n const bigrammed = isBigramLanguage(db.tokenizer?.language ?? \"\");\n const hits = await rankedHits(db, params, undefined, bigrammed);\n return hits.map((hit) => hit.document);\n }\n if (filters?.locale) {\n const { language = \"\" } = translations.get(filters.locale) ?? {};\n const bigrammed = isBigramLanguage(language);\n const hits = await rankedHits(db, params, filters.locale, bigrammed);\n return hits.map((hit) => hit.document);\n }\n // `nin` matches only pages with a locale, which every Blume-built index\n // gives each page. (Orama's `not` would admit locale-less pages, but it\n // drops the sibling filters beside it.)\n const searches = [\n rankedHits(\n db,\n {\n ...params,\n where: { ...where, locale: { nin: [...translations.keys()] } },\n },\n undefined,\n false\n ),\n ...[...translations].map(([code, tokenizer]) =>\n rankedHits(\n db,\n { ...params, where: { ...where, locale: { eq: code } } },\n code,\n isBigramLanguage(tokenizer.language)\n )\n ),\n ];\n const found = await Promise.all(searches);\n return found\n .flat()\n .toSorted((a, b) => boostedScore(b) - boostedScore(a))\n .slice(0, limit)\n .map((hit) => hit.document);\n};\n",
|
|
10
|
+
"/**\n * Latin folding, shared by the search index and everything that finds a query\n * in a page's text afterwards: the search dialog's highlights and excerpts,\n * and the assistant's excerpts. The index matches Prüfung for `prufung`, so\n * the rest must too, or a page found by folding shows its opening with\n * nothing marked.\n */\n\n/** A Latin letter and the combining marks on it, which NFD splits off. */\nconst LATIN_ACCENTS = /(?<letter>\\p{Script=Latin})\\p{M}+/gu;\n\n/** A Latin character outside ASCII, once NFD has split off its accents. */\nconst NON_ASCII_LATIN = /(?=\\P{ASCII})\\p{Script=Latin}/gu;\n\n/**\n * Latin letters NFD leaves whole, spelled the way an unaccented query types\n * them. Orama's own diacritics table folds the same letters, though its\n * English splitter cuts words at them first.\n */\nconst LATIN_FOLDS = new Map([\n [\"ß\", \"ss\"],\n [\"æ\", \"ae\"],\n [\"œ\", \"oe\"],\n [\"ø\", \"o\"],\n [\"ł\", \"l\"],\n [\"đ\", \"d\"],\n [\"ð\", \"d\"],\n [\"ħ\", \"h\"],\n [\"ı\", \"i\"],\n [\"þ\", \"th\"],\n]);\n\n/**\n * U+00AD, the invisible break hint `­` writes inside a long word. It\n * belongs to no word: split there, Prüfungs­ordnung would index as two.\n */\nconst SOFT_HYPHEN = /\\u00AD/gu;\n\n/** `text` without its soft hyphens. */\nexport const stripSoftHyphens = (text: string): string =>\n text.replace(SOFT_HYPHEN, \"\");\n\n/**\n * Fold lowercased text's Latin letters to the plain letters an unaccented\n * query types: accents drop (café → cafe), letters NFD leaves whole are\n * spelled out (Łódź → lodz, ışık and IŞIK → isik, Straße → strasse), and\n * compatibility forms unpack (the fi ligature → fi, fullwidth API → api).\n * Soft hyphens go too. Marks on any other script, and marks on no letter\n * (the keycap in 1️⃣), stay.\n */\nexport const foldLatin = (lowered: string): string =>\n stripSoftHyphens(lowered)\n .normalize(\"NFD\")\n .replace(\n NON_ASCII_LATIN,\n (letter) =>\n LATIN_FOLDS.get(letter) ?? letter.normalize(\"NFKD\").toLowerCase()\n )\n .replace(LATIN_ACCENTS, \"$<letter>\");\n\n/**\n * A run of Latin letters and the marks on them (lowercase İ is i plus a\n * combining dot), inside a term of any script.\n */\nconst LATIN_RUN = /\\p{Script=Latin}[\\p{Script=Latin}\\p{M}]*/gu;\n\n/**\n * {@link foldLatin} for the Latin letters of a lowercased term, whatever\n * script the rest of it is in: a segmenter keeps caféनमस्ते together as one\n * term. Only the Latin runs fold. Marks are spelling elsewhere (Thai vowels\n * and tones; the breve that separates Cyrillic й from и), and NFD would split\n * a Hangul syllable into its jamo.\n */\nexport const foldLatinTerm = (term: string): string =>\n term.replace(LATIN_RUN, (run) => foldLatin(run));\n\n/**\n * The Unicode blocks holding Latin letters outside ASCII: Latin-1 through\n * Latin Extended-B, the phonetic extensions and Latin Extended Additional,\n * superscripts through number forms, Latin Extended-C, -D and -E, the\n * ligatures, and the fullwidth forms.\n */\nconst LATIN_BLOCKS = [\n [0x00_80, 0x02_4f],\n [0x1d_00, 0x1e_ff],\n [0x20_70, 0x21_8f],\n [0x2c_60, 0x2c_7f],\n [0xa7_20, 0xa7_ff],\n [0xab_30, 0xab_6f],\n [0xfb_00, 0xfb_06],\n [0xff_21, 0xff_5a],\n] as const;\n\nconst LATIN = /^\\p{Script=Latin}$/u;\nconst PLAIN = /^[a-z]+$/u;\n\nlet spellings: Map<string, string> | undefined;\n\n/**\n * Every Latin character outside ASCII that folds to plain letters, keyed by\n * those letters: `a` → àáâ…, `ss` → ßẞ. Built on first use, not at import.\n */\nconst latinSpellings = (): Map<string, string> => {\n if (spellings) {\n return spellings;\n }\n spellings = new Map();\n for (const [first, last] of LATIN_BLOCKS) {\n for (let code = first; code <= last; code += 1) {\n const char = String.fromCodePoint(code);\n const plain = foldLatin(char.toLowerCase());\n if (LATIN.test(char) && PLAIN.test(plain)) {\n spellings.set(plain, `${spellings.get(plain) ?? \"\"}${char}`);\n }\n }\n }\n return spellings;\n};\n\n/** A Latin letter in a query, with the marks a decomposed query puts on it. */\nconst QUERY_LETTER = /\\p{Script=Latin}\\p{M}*/gu;\n\nconst REGEXP_SPECIAL = /[$()*+.?[\\\\\\]^{|}]/gu;\n\n/** What may follow a letter inside a word: its accents, or a soft hyphen. */\nconst LETTER_TAIL = String.raw`[\\p{M}\\u00AD]*`;\n\n/**\n * The longest plain spelling a single character folds to (ⅷ → viii), so the\n * pattern builder knows how far ahead to look.\n */\nconst longestSpelling = (folds: Map<string, string>): number =>\n Math.max(...[...folds.keys()].map((plain) => plain.length));\n\n/**\n * A regex source that finds `term` in unfolded text whatever accents, soft\n * hyphens or spelled-out letters its Latin words carry there: `prufung`\n * matches Prüfung, `strasse` matches Straße. Compile it with the `iu` flags.\n * Characters outside Latin match as themselves.\n */\nexport const accentInsensitive = (term: string): string => {\n const folds = latinSpellings();\n const longest = longestSpelling(folds);\n const letter = (plain: string): string =>\n `[${plain}${folds.get(plain) ?? \"\"}]${LETTER_TAIL}`;\n const chars = [\n ...stripSoftHyphens(term.toLowerCase()).replace(QUERY_LETTER, (latin) =>\n foldLatin(latin)\n ),\n ];\n let source = \"\";\n let index = 0;\n while (index < chars.length) {\n let length = Math.min(longest, chars.length - index);\n // The longest run here that a single character spells out — `ss` for ß.\n while (\n length > 1 &&\n !folds.has(chars.slice(index, index + length).join(\"\"))\n ) {\n length -= 1;\n }\n const run = chars.slice(index, index + length);\n const plain = run.join(\"\");\n if (length > 1) {\n const letters = run.map((char) => letter(char)).join(\"\");\n source += `(?:${letters}|[${folds.get(plain) ?? \"\"}]${LETTER_TAIL})`;\n } else if (PLAIN.test(plain)) {\n source += letter(plain);\n } else {\n source += plain.replaceAll(REGEXP_SPECIAL, String.raw`\\$&`);\n }\n index += length;\n }\n return source;\n};\n"
|
|
11
|
+
],
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "9C79557EDA515E7164756E2164756E21",
|
|
14
|
+
"names": []
|
|
15
|
+
}
|
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
downlevelComponentNode,
|
|
13
13
|
downlevelComponents,
|
|
14
14
|
contentIndexable,
|
|
15
|
+
relativeLinkRewriter,
|
|
15
16
|
projectComponentSerializers,
|
|
16
17
|
applyAudienceVisibility,
|
|
17
18
|
agentMarkdown,
|
|
18
19
|
buildRawMarkdown
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-5bp65pv4.js";
|
|
20
21
|
import {
|
|
21
22
|
normalizeBasePath
|
|
22
23
|
} from "./chunk-zccbjcj0.js";
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
RESERVED_META_KEYWORDS,
|
|
29
30
|
isForeignKeyword,
|
|
30
31
|
isLineRange
|
|
31
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-vxdjss7f.js";
|
|
32
33
|
|
|
33
34
|
// src/search/documents.ts
|
|
34
35
|
import { markdownToMdast, mdxToMdast } from "satteri";
|
|
@@ -226,14 +227,14 @@ var buildCrumbIndex = (sidebar) => {
|
|
|
226
227
|
}
|
|
227
228
|
return index;
|
|
228
229
|
};
|
|
229
|
-
var pageBody = async (project, page, options, components) => {
|
|
230
|
+
var pageBody = async (project, page, options, components, rewriteLinks) => {
|
|
230
231
|
if (!page) {
|
|
231
232
|
return "";
|
|
232
233
|
}
|
|
233
234
|
const parsed = frontmatter_default(await readExpandedEntryText(project, page));
|
|
234
235
|
const visible = applyAudienceVisibility(parsed.content, options?.audience ?? "web");
|
|
235
|
-
if (
|
|
236
|
-
return downlevelComponents(visible.trim(), components, parsed.data);
|
|
236
|
+
if (rewriteLinks) {
|
|
237
|
+
return downlevelComponents(rewriteLinks(visible, page).trim(), components, parsed.data);
|
|
237
238
|
}
|
|
238
239
|
return toPlainText(visible, page.format, {
|
|
239
240
|
downlevel: {
|
|
@@ -277,6 +278,7 @@ var buildSearchDocuments = async (project, options) => {
|
|
|
277
278
|
return page ? contentIndexable(page, config, graph) : false;
|
|
278
279
|
});
|
|
279
280
|
const components = projectComponentSerializers(project);
|
|
281
|
+
const rewriteLinks = options?.content === "markdown" ? relativeLinkRewriter(project, { assistant: true }) : undefined;
|
|
280
282
|
const { i18n } = project.config;
|
|
281
283
|
const docsSections = new Map;
|
|
282
284
|
const docsSection = (locale) => {
|
|
@@ -292,7 +294,7 @@ var buildSearchDocuments = async (project, options) => {
|
|
|
292
294
|
};
|
|
293
295
|
return await Promise.all(indexable.map(async (route) => {
|
|
294
296
|
const page = pageById.get(route.id);
|
|
295
|
-
const body = await pageBody(project, page, options, components);
|
|
297
|
+
const body = await pageBody(project, page, options, components, rewriteLinks);
|
|
296
298
|
const tags = page?.meta?.search?.tags;
|
|
297
299
|
const crumb = crumbs.get(route.path);
|
|
298
300
|
const facets = page ? pageFacets(page, project.config) : undefined;
|
|
@@ -396,5 +398,5 @@ var buildMcpData = async (project) => {
|
|
|
396
398
|
|
|
397
399
|
export { EXPANDABLE_VISIBLE_LINES, buildSearchDocuments, buildMcpData };
|
|
398
400
|
|
|
399
|
-
//# debugId=
|
|
400
|
-
//# sourceMappingURL=chunk-
|
|
401
|
+
//# debugId=0C8488DE9FAE851264756E2164756E21
|
|
402
|
+
//# sourceMappingURL=chunk-402xaaxn.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/search/documents.ts", "../src/markdown/code-title.ts", "../src/search/facets.ts", "../src/ai/mcp/data.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import type { Nodes } from \"mdast\";\nimport { markdownToMdast, mdxToMdast } from \"satteri\";\n\nimport {\n componentRegistry,\n downlevelComponentNode,\n downlevelComponents,\n} from \"../ai/component-markdown.ts\";\nimport type {\n ComponentMarkdown,\n DownlevelWalk,\n MdastNode as DownlevelNode,\n} from \"../ai/component-markdown.ts\";\nimport { relativeLinkRewriter } from \"../ai/relative-links.ts\";\nimport type { RelativeLinkRewriter } from \"../ai/relative-links.ts\";\nimport { projectComponentSerializers } from \"../ai/serializers.ts\";\nimport { applyAudienceVisibility } from \"../ai/visibility.ts\";\nimport type { VisibilityAudience } from \"../ai/visibility.ts\";\nimport matter from \"../core/frontmatter.ts\";\nimport { parseHeadingMarkers } from \"../core/heading-markers.ts\";\nimport { EN_UI, resolveUIStrings } from \"../core/i18n-ui.ts\";\nimport { contentIndexable } from \"../core/manifest.ts\";\nimport type { BlumeProject } from \"../core/project-graph.ts\";\nimport { HTML_COMMENT } from \"../core/sources/normalize.ts\";\nimport { readExpandedEntryText } from \"../core/sources/read.ts\";\nimport type { NavNode, PageRecord } from \"../core/types.ts\";\nimport { parseCodeTitle } from \"../markdown/code-title.ts\";\nimport { directiveSource } from \"../markdown/directives.ts\";\nimport type { DirectiveNode } from \"../markdown/directives.ts\";\nimport {\n MARKDOWN_BODY_FEATURES,\n MDX_BODY_FEATURES,\n} from \"../markdown/features.ts\";\nimport { normalizeFence } from \"../markdown/fence-language.ts\";\nimport { pageFacets } from \"./facets.ts\";\n\n/** A document indexed by the client-side search providers (Orama, FlexSearch). */\nexport interface SearchDocument {\n route: string;\n title: string;\n description: string;\n content: string;\n /** Ancestor section labels for the result breadcrumb, e.g. `[\"Guides\"]`. */\n breadcrumb: string[];\n /** Top-level section label, used by the search filter pills. */\n section: string;\n /** Locale code, so the dialog can filter results to the active language. */\n locale: string;\n /** Resolved page `type` (`doc`, `blog`, a custom `rfc`…), for type filters. */\n contentType: string;\n /** Docs version (`\"\"` for the current docs), so results scope to the viewed version. */\n version: string;\n /** Frontmatter `search.tags`, surfaced for hosted-provider faceting. */\n tags?: string[];\n /**\n * Declared facet values (`content.types.<type>.facets`), key → value.\n * Filterable through the MCP tools' `filters` input.\n */\n facets?: Record<string, string>;\n /** Frontmatter `search.boost`: the page's relevance multiplier, when set. */\n boost?: number;\n /** Frontmatter `search.keywords`: extra terms the page is found by. */\n keywords?: string[];\n}\n\n/**\n * A record uploaded to a hosted search backend (Algolia, Orama Cloud,\n * Typesense, Mixedbread). `_id` is the stable per-page key; each sync adapts it\n * to the backend's own id field (`objectID`, `id`, …).\n */\nexport interface SearchRecord {\n _id: string;\n url: string;\n title: string;\n description: string;\n content: string;\n /** Locale code, carried as a facet for per-language filtering. */\n locale: string;\n /**\n * Docs version, carried as a facet for per-version filtering. The current\n * docs upload as `\"current\"` — hosted backends treat an empty facet value\n * unreliably, so the sentinel stands in for the empty version id.\n */\n version: string;\n /** Single faceting tag (the first frontmatter tag, when present). */\n tag?: string;\n /**\n * The page's relevance multiplier, 1 unless `search.boost` sets it: a\n * ranking attribute every record carries, so the backend can sort by it.\n */\n boost: number;\n /** Extra terms the page is found by (`search.keywords`). */\n keywords?: string[];\n}\n\n// Tag-shaped only: a name (or closing slash/fragment) right after `<`, and no\n// newline inside. Applied *within* html/JSX nodes so their inner prose is\n// kept; the surrounding Markdown is walked as a tree, so a bare `<` in prose\n// (\"costs < 5 credits\") is ordinary text and never at risk.\nconst HTML_OR_JSX = /<\\/?[a-zA-Z][^\\n<>]*>|<\\/?>/gu;\nconst WHITESPACE = /\\s+/gu;\n\n// Parents whose children are inline: no separator is inserted after them, or\n// `re*ally*` would index as `re ally`. Every other parent is block-shaped and\n// ends with a space so adjacent paragraphs/headings/cells don't fuse.\nconst INLINE_PARENTS = new Set([\n \"delete\",\n \"emphasis\",\n \"footnoteReference\",\n \"link\",\n \"linkReference\",\n \"mdxJsxTextElement\",\n \"strong\",\n \"subscript\",\n \"superscript\",\n]);\n\n// Nodes that never render as prose: image alt text was never indexed, MDX\n// expressions (`{props.x}`) and ESM (`export const meta`) are code, and math\n// is LaTeX source rendered by KaTeX, not searchable words.\nconst NON_PROSE = new Set<Nodes[\"type\"]>([\n \"image\",\n \"imageReference\",\n \"inlineMath\",\n \"math\",\n \"mdxFlowExpression\",\n \"mdxTextExpression\",\n \"mdxjsEsm\",\n]);\n\ntype PageFormat = PageRecord[\"format\"];\n\n/** What the plain-text walk carries: the code-block policy and the downlevel context. */\ninterface Walk {\n includeCodeBlocks: boolean;\n downlevel: DownlevelWalk;\n}\n\n// Front matter is already off (core/frontmatter.ts) by the time a body gets\n// here, so a body that opens with a `---` divider must read as a thematic\n// break rather than a second front matter block — as the renderer reads it.\nconst MD_PARSE = { features: MARKDOWN_BODY_FEATURES };\nconst MDX_PARSE = { features: MDX_BODY_FEATURES };\n\nconst parseMdx = (markdown: string): Nodes | undefined => {\n try {\n return mdxToMdast(markdown, MDX_PARSE);\n } catch {\n return undefined;\n }\n};\n\n/**\n * Parse a page body the way the renderer reads it. `.mdx` pages go through\n * the MDX grammar with the renderer's feature set, so components become\n * `mdxJsx*` nodes whose children — prose and fences, however they're indented\n * — walk like top-level content, and `:::` directives and `$$` math parse as\n * such; Markdown would instead fold a tight component into one html node and\n * read its indented children as an indented code block. A page the MDX\n * grammar rejects (an HTML comment, an unclosed tag) fails to render too, so\n * it has no body to index: reading it as Markdown instead would put its ESM,\n * expressions, and raw tags in the index as prose.\n */\nconst parseMarkdown = (\n markdown: string,\n format: PageFormat\n): Nodes | undefined =>\n format === \"mdx\" ? parseMdx(markdown) : markdownToMdast(markdown, MD_PARSE);\n\n// Fenced code is excluded from the plain index by default (ranking noise) —\n// the \"markdown\" extraction keeps it for assistant grounding. Code-heavy docs opt\n// in via `search.indexing.includeCodeBlocks`, which indexes the fence body and\n// its rendered title (```ts blume.config.ts) but never the fence markers,\n// language, or other meta keywords.\nconst collectCode = (\n node: Extract<Nodes, { type: \"code\" }>,\n out: string[],\n walk: Walk\n): void => {\n if (!walk.includeCodeBlocks) {\n return;\n }\n // The fence as the page renders it (`[file.ts]{2}` splits, `JSON`\n // lowercases), so the indexed title is the one the page shows.\n const fence = normalizeFence(node.lang, node.meta) ?? node;\n const title = parseCodeTitle(\n fence.meta ?? undefined,\n fence.lang ?? undefined\n );\n if (title) {\n out.push(title, \" \");\n }\n // The trailing space keeps the fence apart from what follows it.\n out.push(node.value, \" \");\n};\n\n/**\n * A text or leaf directive as the page shows it. Blume renders neither (see\n * `markdown/directives.ts`), and prose is full of text that parses as one —\n * `16:9`, `og:image` — so the page shows the source the author wrote, and the\n * index reads the same text rather than dropping the `:name` part.\n */\nconst directiveText = (\n node: Extract<Nodes, { type: \"leafDirective\" | \"textDirective\" }>,\n walk: Walk\n): string =>\n directiveSource(\n // SAFETY: DirectiveNode is a structural subset of Satteri's directive\n // nodes — a `name`, optional attributes and children, and a position.\n node as DirectiveNode,\n node.type === \"textDirective\" ? \":\" : \"::\",\n // The walked tree was parsed from this text, so its offsets index into it.\n walk.downlevel.source\n );\n\n// Block boundaries separate words; so does an empty inline element (`<br />`,\n// a self-closing icon), which otherwise fuses its neighbors.\nconst separatesWords = (node: Extract<Nodes, { children: unknown }>): boolean =>\n !INLINE_PARENTS.has(node.type) || node.children.length === 0;\n\n/** Fold one mdast node into the plain-text accumulator. */\nconst collectText = (node: Nodes, out: string[], walk: Walk): void => {\n if (NON_PROSE.has(node.type)) {\n return;\n }\n switch (node.type) {\n case \"code\": {\n collectCode(node, out, walk);\n return;\n }\n // A component with a serializer indexes as the text the page shows —\n // a Card's title and call to action, a TypeTable's descriptions — read\n // from the Markdown the serializer emits (its contract is Markdown, not\n // MDX). One without a serializer walks its children like any block.\n case \"mdxJsxFlowElement\": {\n // SAFETY: DownlevelNode is a structural subset of Satteri's mdast\n // output — the walk checks for offsets before reading them.\n const text = downlevelComponentNode(\n node as DownlevelNode,\n walk.downlevel\n );\n if (text !== null) {\n collectText(markdownToMdast(text, MD_PARSE), out, walk);\n return;\n }\n break;\n }\n // Inline code is kept verbatim — `Array<T>` is a type parameter, not a\n // tag, and its tokens must stay searchable.\n case \"inlineCode\": {\n out.push(node.value);\n return;\n }\n // A raw-HTML run. In a `.md` page (or an `.mdx` page MDX couldn't parse)\n // CommonMark folds a block-level `<Callout>` with no blank lines into ONE\n // html node holding all its inner prose, so the node can't just be\n // dropped — strip the tag-shaped runs and keep the text.\n case \"html\": {\n out.push(\n node.value.replaceAll(HTML_COMMENT, \" \").replaceAll(HTML_OR_JSX, \" \"),\n \" \"\n );\n return;\n }\n case \"break\": {\n out.push(\" \");\n return;\n }\n case \"textDirective\": {\n out.push(directiveText(node, walk));\n return;\n }\n case \"leafDirective\": {\n out.push(directiveText(node, walk), \" \");\n return;\n }\n case \"heading\": {\n // oxlint-disable-next-line no-use-before-define -- mutual recursion: a heading's children are walked like any others\n collectHeading(node, out, walk);\n return;\n }\n default: {\n break;\n }\n }\n if (\"value\" in node) {\n out.push(node.value);\n return;\n }\n if (\"children\" in node) {\n for (const child of node.children) {\n collectText(child, out, walk);\n }\n if (separatesWords(node)) {\n out.push(\" \");\n }\n }\n};\n\n/**\n * Fold a heading into the accumulator. Trailing heading markers\n * (`[#custom-id]`, `{#custom-id}`, `[!toc]`, `[toc]`) are anchor metadata,\n * not prose — strip them so they never pollute the index. Only a marker that\n * ends the heading's final plain-text child counts, mirroring the renderer: a\n * heading ending in inline code or an image keeps its bracketed text on the\n * page (so it stays searchable), and a heading that is nothing but markers\n * renders them literally.\n */\nconst collectHeading = (\n node: Extract<Nodes, { type: \"heading\" }>,\n out: string[],\n walk: Walk\n): void => {\n const last = node.children.at(-1);\n const trailing = last?.type === \"text\" ? last : undefined;\n const inner: string[] = [];\n const kept = trailing ? node.children.slice(0, -1) : node.children;\n for (const child of kept) {\n collectText(child, inner, walk);\n }\n if (trailing) {\n const stripped = parseHeadingMarkers(trailing.value).text;\n const literal = stripped === \"\" && node.children.length === 1;\n inner.push(literal ? trailing.value : stripped);\n }\n out.push(inner.join(\"\").trimEnd(), \" \");\n};\n\n/**\n * Reduce Markdown/MDX to plain, searchable text: parse (GFM included) and walk\n * the tree instead of regex-stripping the source, so reference-style links,\n * autolinks, setext headings, tables, and literal `*`/`~`/`>` in prose all\n * reduce correctly. This feeds the client index *and* every hosted-provider\n * record, so anything lost here is a permanent search-quality loss.\n */\nconst toPlainText = (\n markdown: string,\n format: PageFormat,\n walk: Walk\n): string => {\n const tree = parseMarkdown(markdown, format);\n if (!tree) {\n return \"\";\n }\n const out: string[] = [];\n collectText(tree, out, walk);\n return out.join(\"\").replaceAll(WHITESPACE, \" \").trim();\n};\n\ninterface Crumbs {\n breadcrumb: string[];\n section: string;\n}\n\n/**\n * Map each page route to its ancestor section labels by walking the nav sidebar\n * once. The nearest ancestor group — the sidebar section a page appears under —\n * becomes its `section`, the dimension the search filter pills group by, so the\n * pills mirror the visible sidebar (and honor folder-meta renames).\n */\nconst buildCrumbIndex = (sidebar: NavNode[]): Map<string, Crumbs> => {\n const index = new Map<string, Crumbs>();\n // A config-sidebar section's landing page (the group's `root`) lives on the\n // *group* node, not on any page leaf — record it under the group's own label\n // so the section's landing page carries the same facet as its children. A\n // real page leaf for the route (filesystem sidebars emit index pages as\n // leaves) wins, so group routes are merged in only where no leaf claimed one.\n const groupRoutes = new Map<string, Crumbs>();\n const walk = (nodes: NavNode[], trail: string[]): void => {\n for (const node of nodes) {\n if (node.kind === \"group\") {\n if (node.route && !groupRoutes.has(node.route)) {\n groupRoutes.set(node.route, {\n breadcrumb: [...trail, node.label],\n section: node.label,\n });\n }\n walk(node.children, [...trail, node.label]);\n } else if (node.route) {\n index.set(node.route, {\n breadcrumb: trail,\n section: trail.at(-1) ?? \"\",\n });\n }\n }\n };\n walk(sidebar, []);\n for (const [route, crumbs] of groupRoutes) {\n if (!index.has(route)) {\n index.set(route, crumbs);\n }\n }\n return index;\n};\n\ninterface BuildOptions {\n includeWhenDisabled?: boolean;\n content?: \"markdown\" | \"plain\";\n audience?: VisibilityAudience;\n}\n\n/**\n * Read a page's body and reduce it per the extraction options. The\n * `\"markdown\"` body, the one `rewriteLinks` comes with, is agent-facing: its\n * page links are rewritten, then its components downleveled in the source\n * with the serializers every agent surface uses (front matter in scope for\n * prop expressions), in the order llms-full.txt takes. The plain body is\n * parsed as written and walked, with the same serializers applied per\n * component on the way, so the index reads a `<Card>` or `<TypeTable>` as\n * the text it shows.\n */\nconst pageBody = async (\n project: BlumeProject,\n page: PageRecord | undefined,\n options: Pick<BuildOptions, \"audience\"> | undefined,\n components: Record<string, ComponentMarkdown>,\n rewriteLinks: RelativeLinkRewriter | undefined\n): Promise<string> => {\n if (!page) {\n return \"\";\n }\n const parsed = matter(await readExpandedEntryText(project, page));\n const visible = applyAudienceVisibility(\n parsed.content,\n options?.audience ?? \"web\"\n );\n if (rewriteLinks) {\n return downlevelComponents(\n rewriteLinks(visible, page).trim(),\n components,\n parsed.data\n );\n }\n return toPlainText(visible, page.format, {\n downlevel: {\n frontmatter: parsed.data,\n registry: componentRegistry(components),\n source: visible,\n },\n includeCodeBlocks: project.config.search.indexing.includeCodeBlocks,\n });\n};\n\n/** The ranking a page asks for: its `search.boost` and `search.keywords`. */\nconst rankingFields = (\n search: PageRecord[\"meta\"][\"search\"] | undefined\n): Pick<SearchDocument, \"boost\" | \"keywords\"> => {\n const { boost, keywords } = search ?? {};\n return {\n ...(boost !== undefined && boost !== 1 && { boost }),\n ...(keywords && keywords.length > 0 && { keywords }),\n };\n};\n\n/**\n * Build search documents from the content graph. Only indexable pages are\n * included (per the route manifest), and content comes from the source files,\n * so the index is identical in dev and build.\n *\n * Pass `includeWhenDisabled` to index pages on their content merits even when\n * the search provider is `none` — used by the MCP server, which is a separate\n * feature from on-page search.\n *\n * `content` selects the extraction: `\"plain\"` (default) strips Markdown to bare\n * searchable text; `\"markdown\"` keeps the body's Markdown — code blocks, lists,\n * headings — for assistant grounding, where fenced examples are often the answer\n * and stripping them makes the model unable to cite content the docs do contain.\n * The `\"markdown\"` body is agent-facing, so components are downleveled with the\n * same serializers the `.md` mirror, llms-full.txt and MCP `get_page` use: a\n * page whose body is a `<CardGroup>` of `<Card>`s is prose to a reader and bare\n * JSX to anything reading the source, and section landing pages are exactly\n * that shape. Its page links point at the pages they mean, as they do there\n * too, as the routes the assistant's snapshot keys those pages by (see\n * `relativeLinkRewriter`'s `assistant`): the model cites the links it reads.\n *\n * `audience` resolves `<Visibility>` blocks before extraction: `\"web\"`\n * (default) keeps web-only content and drops agents-only blocks — the site\n * search and hosted syncs must not surface content the page hides — while\n * `\"agents\"` mirrors llms-full.txt/MCP `get_page` (web removed, agents kept).\n *\n * Whether fenced code joins the plain extraction is site-wide policy, read\n * from `search.indexing.includeCodeBlocks` here rather than threaded by each\n * caller, so the client index, hosted syncs, eject, and the MCP `search_docs`\n * index can't drift apart.\n */\nexport const buildSearchDocuments = async (\n project: BlumeProject,\n options?: BuildOptions\n): Promise<SearchDocument[]> => {\n const pageById = new Map(project.graph.pages.map((page) => [page.id, page]));\n\n // Build the crumb index from every locale's sidebar (their nodes carry\n // locale-prefixed routes), so localized pages get the right section/breadcrumb.\n // Falls back to the single default-locale nav when i18n is off.\n const byLocale = Object.values(project.graph.navigationByLocale ?? {});\n // Archived versions' trees contribute too, so snapshot pages get their own\n // section/breadcrumb instead of falling through to the \"Docs\" default.\n const byVersion = Object.values(project.graph.navigationByVersion ?? {})\n .flatMap((locales) => Object.values(locales))\n .map((nav) => nav.sidebar);\n const sidebars = [\n ...(byLocale.length > 0\n ? byLocale.map((nav) => nav.sidebar)\n : [project.graph.navigation?.sidebar ?? []]),\n ...byVersion,\n ];\n const crumbs = new Map<string, Crumbs>();\n for (const sidebar of sidebars) {\n for (const [route, crumb] of buildCrumbIndex(sidebar)) {\n crumbs.set(route, crumb);\n }\n }\n\n const indexable = project.manifest.routes.filter((route) => {\n if (!options?.includeWhenDisabled) {\n return route.indexable;\n }\n // A fallback route renders the fallback locale's page at another locale's\n // URL, so indexing it would only repeat that page as a second hit — the\n // reason site search leaves it out too.\n if (route.fallback) {\n return false;\n }\n const page = pageById.get(route.id);\n const { config, graph } = project;\n return page ? contentIndexable(page, config, graph) : false;\n });\n\n // Built once — `downlevelComponents` rebuilds its registry per call\n // otherwise.\n const components = projectComponentSerializers(project);\n // The `\"markdown\"` body is the assistant's: the model cites the links it\n // reads, so a page link points at the page the snapshot holds.\n const rewriteLinks =\n options?.content === \"markdown\"\n ? relativeLinkRewriter(project, { assistant: true })\n : undefined;\n\n // A page in no sidebar group falls under the \"Docs\" section, in its own\n // language: the label is a UI string, resolved once per locale.\n const { i18n } = project.config;\n const docsSections = new Map<string, string>();\n const docsSection = (locale: string): string => {\n let label = docsSections.get(locale);\n if (label === undefined) {\n label = (\n i18n\n ? resolveUIStrings(locale, {\n defaultLocale: i18n.defaultLocale,\n overrides: i18n.ui,\n })\n : EN_UI\n ).search.docs;\n docsSections.set(locale, label);\n }\n return label;\n };\n\n return await Promise.all(\n indexable.map(async (route) => {\n const page = pageById.get(route.id);\n const body = await pageBody(\n project,\n page,\n options,\n components,\n rewriteLinks\n );\n const tags = page?.meta?.search?.tags;\n const crumb = crumbs.get(route.path);\n const facets = page ? pageFacets(page, project.config) : undefined;\n const document: SearchDocument = {\n breadcrumb: crumb?.breadcrumb ?? [],\n content: body,\n contentType: route.contentType,\n description: page?.description ?? \"\",\n locale: route.locale,\n route: route.path,\n section: crumb?.section || docsSection(route.locale),\n tags: tags && tags.length > 0 ? tags : undefined,\n title: route.title,\n version: route.version,\n };\n if (facets) {\n document.facets = facets;\n }\n return { ...document, ...rankingFields(page?.meta?.search) };\n })\n );\n};\n\n/**\n * Map per-page search documents to the flat record shape hosted backends\n * ingest. One record per page, keyed by route; the first tag becomes the\n * faceting `tag`, and every record carries a `boost` (1 by default).\n */\nexport const toSearchRecords = (documents: SearchDocument[]): SearchRecord[] =>\n documents.map((doc) => ({\n _id: doc.route,\n boost: doc.boost ?? 1,\n content: doc.content,\n description: doc.description,\n ...(doc.keywords && { keywords: doc.keywords }),\n locale: doc.locale,\n tag: doc.tags?.[0],\n title: doc.title,\n url: doc.route,\n version: doc.version || \"current\",\n }));\n",
|
|
6
|
+
"/**\n * Code-fence meta. A Shiki transformer reads the tokens after the language and\n * promotes them to attributes on the rendered `<pre>`:\n *\n * - a title — the bare words (```ts blume.config.ts, ```js Install the\n * client) or `title=\"...\"` — becomes `data-title`; the theme's code header\n * shows it, falling back to the language label.\n * - the `lineNumbers` keyword (```ts file.ts lineNumbers) becomes\n * `data-line-numbers`; the theme renders a counter-driven line-number gutter.\n * - the `wrap` keyword becomes `data-wrap`: this block's long lines wrap\n * instead of scrolling, as `markdown.code.wrap` does for every block.\n * - the `expandable` keyword becomes `data-expandable` on a block of\n * {@link EXPANDABLE_MIN_LINES} lines or more: the theme shows its first\n * {@link EXPANDABLE_VISIBLE_LINES} lines, and the page script adds a toggle\n * that shows the rest in full. A shorter block renders as it is.\n */\n\nimport {\n isForeignKeyword,\n isLineRange,\n metaTokens,\n QUOTED_ATTR,\n RESERVED_META_KEYWORDS,\n} from \"./fence-meta.ts\";\n\n/** The lines a collapsed `expandable` block shows. */\nexport const EXPANDABLE_VISIBLE_LINES = 10;\n\n/**\n * The lines a block needs before `expandable` collapses it: a few more than it\n * shows, so the toggle never hides a line or two behind a click.\n */\nexport const EXPANDABLE_MIN_LINES = 16;\n\n/** The slice of Shiki's transformer `this` context Blume reads. */\ninterface CodeMetaContext {\n options: { lang?: string; meta?: { __raw?: string } };\n /** The block's code, as highlighted. */\n source?: string;\n}\n\n/** The `<pre>` hast node a Shiki `pre` hook receives. */\ninterface PreNode {\n properties: Record<string, boolean | number | string | undefined>;\n}\n\n/** A Shiki-compatible transformer, typed structurally to avoid a Shiki dep. */\nexport interface CodeTitleTransformer {\n name: string;\n pre: (this: CodeMetaContext, node: PreNode) => void;\n}\n\nconst TITLE_ATTR = /(?:^|\\s)title=(?:\"(?<dq>[^\"]*)\"|'(?<sq>[^']*)')/u;\n\n// The bare words are the title (```ts blume.config.ts, ```js Install the\n// client): every token that isn't a Shiki line range (`{1,3-5}`), a\n// `key=value` attr, a reserved keyword, or another tool's keyword for a\n// fence in `lang` (```rust ignore).\nconst isTitleToken = (token: string, lang: string | undefined): boolean =>\n token.length > 0 &&\n !isLineRange(token) &&\n !token.includes(\"=\") &&\n !RESERVED_META_KEYWORDS.has(token) &&\n !isForeignKeyword(token, lang);\n\n// A title wrapped in one pair of brackets (```ts [file.ts], the Docus and\n// VitePress code-group spelling) shows without them.\nconst BRACKETED_TITLE = /^\\[(?<inner>[^\\]]+)\\]$/u;\n\n/**\n * The title a fence's meta string promotes to `data-title`, if any. `lang` is\n * the fence's language, which decides the other tools' keywords it drops.\n */\nexport const parseCodeTitle = (\n raw: string | undefined,\n lang?: string\n): string | undefined => {\n if (!raw) {\n return undefined;\n }\n // Blank every *other* quoted attr first, so a `title=\"…\"` embedded in\n // another attribute's value (`caption='set title=\"X\" here'`) can't be\n // promoted to the block title.\n const scrubbed = raw.replace(QUOTED_ATTR, (attr) =>\n attr.startsWith(\"title=\") ? attr : \" \"\n );\n const explicit = scrubbed.match(TITLE_ATTR);\n const attrTitle = explicit?.groups?.dq ?? explicit?.groups?.sq;\n if (attrTitle) {\n return attrTitle;\n }\n // The shared tokenizer keeps a quoted attr (rejected below by its `=`) and\n // a spaced line range (`{1, 3-5}`) whole, so neither can shed a fragment\n // that reads as a bare title.\n const title = metaTokens(raw)\n .filter((token) => isTitleToken(token, lang))\n .join(\" \");\n return BRACKETED_TITLE.exec(title)?.groups?.inner ?? (title || undefined);\n};\n\n// The shared tokenizer keeps a quoted attr whole, so a keyword inside a\n// quoted value (`title=\"enable lineNumbers later\"`) is never a token of its own.\nconst hasKeyword = (raw: string | undefined, keyword: string): boolean =>\n metaTokens(raw).includes(keyword);\n\n/** How many lines a block's code runs to, a trailing newline aside. */\nexport const lineCount = (source = \"\"): number =>\n source === \"\" ? 0 : source.replace(/\\r?\\n$/u, \"\").split(/\\r?\\n/u).length;\n\n/** Build the transformer. Runs after Shiki's built-in `data-language` hook. */\nexport const codeTitleTransformer = (): CodeTitleTransformer => ({\n name: \"blume:code-meta\",\n pre(node) {\n const raw = this.options.meta?.__raw;\n const title = parseCodeTitle(raw, this.options.lang);\n if (title) {\n node.properties.dataTitle = title;\n }\n if (hasKeyword(raw, \"lineNumbers\")) {\n node.properties.dataLineNumbers = true;\n }\n if (hasKeyword(raw, \"wrap\")) {\n node.properties.dataWrap = true;\n }\n if (\n hasKeyword(raw, \"expandable\") &&\n lineCount(this.source) >= EXPANDABLE_MIN_LINES\n ) {\n node.properties.dataExpandable = true;\n }\n },\n});\n",
|
|
7
|
+
"import type { ResolvedConfig } from \"../core/schema.ts\";\nimport type { PageRecord } from \"../core/types.ts\";\n\n/**\n * Resolve a page's facet values: the subset of its validated custom\n * frontmatter named by its content type's `facets` declaration. Strings facet\n * as-is; numbers and booleans are stringified so an enum-like `priority: 1`\n * or `enforced: true` still filters; any other shape (objects, arrays,\n * transformed dates) is not a facet value and is skipped. Returns `undefined`\n * when nothing facets, so the field stays absent from serialized documents\n * rather than shipping as `{}` on every page.\n */\n/** Whether a custom frontmatter value stringifies into a usable facet value. */\nconst isFacetValue = <T>(value: T): value is T & (string | number | boolean) =>\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\";\n\nexport const pageFacets = (\n page: Pick<PageRecord, \"contentType\" | \"custom\">,\n config: ResolvedConfig\n): Record<string, string> | undefined => {\n const declared = config.content.types[page.contentType]?.facets;\n if (!declared || declared.length === 0 || !page.custom) {\n return;\n }\n const facets: Record<string, string> = {};\n for (const key of declared) {\n const value = page.custom[key];\n if (isFacetValue(value)) {\n facets[key] = String(value);\n }\n }\n return Object.keys(facets).length > 0 ? facets : undefined;\n};\n",
|
|
8
|
+
"import { normalizeBasePath } from \"../../core/base-path.ts\";\nimport type { BlumeProject } from \"../../core/project-graph.ts\";\nimport type { Navigation } from \"../../core/types.ts\";\nimport { buildSearchDocuments } from \"../../search/documents.ts\";\nimport { pageFacets } from \"../../search/facets.ts\";\nimport type { OramaDoc } from \"../../search/orama-index.ts\";\nimport { agentMarkdown, buildRawMarkdown } from \"../markdown.ts\";\n\n/** A page entry surfaced by the `list_pages` MCP tool. */\nexport interface McpRoute {\n contentType: string;\n description?: string;\n /** Declared facet values (`content.types.<type>.facets`), key → value. */\n facets?: Record<string, string>;\n indexable: boolean;\n lastModified: string | null;\n /** Resolved locale code (the default locale when not under i18n). */\n locale: string;\n route: string;\n title: string;\n /** Docs version (`\"\"` for the current docs). */\n version: string;\n}\n\n/**\n * The self-contained snapshot the generated MCP endpoint serves. Bundles the\n * search documents, raw page Markdown, route list, and navigation so the server\n * works regardless of the configured search provider and needs no filesystem\n * access at request time. Serialized to `generated/mcp-data.json`.\n */\nexport interface McpData {\n /**\n * Normalized `deployment.base` (`\"\"` or `/seg`), layered onto routes when\n * emitting URLs — the site is base-less and routes are base-less manifest\n * paths, matching the sitemap/llms.txt convention.\n */\n base: string;\n /**\n * The site's `i18n.defaultLocale`, when i18n is configured. Selects a\n * word-segmenting Orama tokenizer for every non-Latin script, so\n * `search_docs` can match CJK, Cyrillic, Greek, Hebrew, or Devanagari\n * content.\n */\n defaultLocale?: string;\n /**\n * Archived docs version ids, in configured order. Present only on a\n * versioned site; its presence is what makes `search_docs`/`list_pages`\n * default to the current docs.\n */\n archivedVersions?: string[];\n documents: OramaDoc[];\n instructions?: string;\n name: string;\n navigation: Navigation;\n /** Per-locale trees for a locale-aware `get_navigation` (i18n sites only). */\n navigationByLocale?: Record<string, Navigation>;\n /** Per-archived-version trees, keyed by version id then locale code. */\n navigationByVersion?: Record<string, Record<string, Navigation>>;\n pages: Record<string, string>;\n routes: McpRoute[];\n site: string | null;\n version: string;\n}\n\n/** Build the MCP data snapshot from a resolved project. */\nexport const buildMcpData = async (project: BlumeProject): Promise<McpData> => {\n const { config, graph, manifest } = project;\n const [documents, rawMarkdown] = await Promise.all([\n // The MCP server is independent of on-page search, so index docs even when\n // the search provider is `none`. Documents are agent-facing, so\n // `<Visibility>` resolves like `get_page`/llms-full.txt (web-only content\n // removed, agents-only kept).\n buildSearchDocuments(project, {\n audience: \"agents\",\n includeWhenDisabled: true,\n }),\n buildRawMarkdown(project),\n ]);\n\n // `get_page` serves the agent variant: components downleveled to Markdown.\n const pages = Object.fromEntries(\n Object.entries(rawMarkdown).map(([route, entry]) => [\n route,\n agentMarkdown(entry),\n ])\n );\n\n const pageById = new Map(graph.pages.map((page) => [page.id, page]));\n\n const routes: McpRoute[] = [];\n // Fallback routes render the fallback locale's page at another locale's\n // URL: listing them would pass that page off as a translation, once per\n // locale. `get_page` still resolves them through `pages`, as llms.txt and\n // search leave them out.\n for (const route of manifest.routes) {\n if (route.hidden || route.fallback) {\n continue;\n }\n const page = pageById.get(route.id);\n const facets = page ? pageFacets(page, config) : undefined;\n const entry: McpRoute = {\n contentType: route.contentType,\n description: page?.description,\n indexable: route.indexable,\n lastModified: route.lastModified ?? null,\n locale: route.locale,\n route: route.path,\n title: route.title,\n version: route.version,\n };\n if (facets) {\n entry.facets = facets;\n }\n routes.push(entry);\n }\n\n const data: McpData = {\n base: normalizeBasePath(config.deployment.options.base),\n defaultLocale: config.i18n?.defaultLocale,\n documents: documents.map((doc) => {\n const document: OramaDoc = {\n content: doc.content,\n contentType: doc.contentType,\n description: doc.description,\n locale: doc.locale,\n route: doc.route,\n title: doc.title,\n version: doc.version,\n };\n if (doc.facets) {\n document.facets = doc.facets;\n }\n if (doc.boost !== undefined) {\n document.boost = doc.boost;\n }\n if (doc.keywords) {\n document.keywords = doc.keywords;\n }\n return document;\n }),\n instructions: config.agents.mcp.instructions,\n name: config.agents.mcp.name ?? config.title,\n navigation: graph.navigation,\n pages,\n routes,\n site: config.deployment.options.site ?? null,\n version: manifest.blumeVersion,\n };\n if (config.versions) {\n data.archivedVersions = config.versions.archived.map(\n (version) => version.id\n );\n data.navigationByVersion = graph.navigationByVersion;\n }\n if (config.i18n) {\n data.navigationByLocale = graph.navigationByLocale;\n }\n return data;\n};\n"
|
|
9
|
+
],
|
|
10
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA;;;ACyBO,IAAM,2BAA2B;AA0BxC,IAAM,aAAa;AAMnB,IAAM,eAAe,CAAC,OAAe,SACnC,MAAM,SAAS,KACf,CAAC,YAAY,KAAK,KAClB,CAAC,MAAM,SAAS,GAAG,KACnB,CAAC,uBAAuB,IAAI,KAAK,KACjC,CAAC,iBAAiB,OAAO,IAAI;AAI/B,IAAM,kBAAkB;AAMjB,IAAM,iBAAiB,CAC5B,KACA,SACuB;AAAA,EACvB,IAAI,CAAC,KAAK;AAAA,IACR;AAAA,EACF;AAAA,EAIA,MAAM,WAAW,IAAI,QAAQ,aAAa,CAAC,SACzC,KAAK,WAAW,QAAQ,IAAI,OAAO,GACrC;AAAA,EACA,MAAM,WAAW,SAAS,MAAM,UAAU;AAAA,EAC1C,MAAM,YAAY,UAAU,QAAQ,MAAM,UAAU,QAAQ;AAAA,EAC5D,IAAI,WAAW;AAAA,IACb,OAAO;AAAA,EACT;AAAA,EAIA,MAAM,QAAQ,WAAW,GAAG,EACzB,OAAO,CAAC,UAAU,aAAa,OAAO,IAAI,CAAC,EAC3C,KAAK,GAAG;AAAA,EACX,OAAO,gBAAgB,KAAK,KAAK,GAAG,QAAQ,UAAU,SAAS;AAAA;;;ACpFjE,IAAM,eAAe,CAAI,UACvB,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU;AAEZ,IAAM,aAAa,CACxB,MACA,WACuC;AAAA,EACvC,MAAM,WAAW,OAAO,QAAQ,MAAM,KAAK,cAAc;AAAA,EACzD,IAAI,CAAC,YAAY,SAAS,WAAW,KAAK,CAAC,KAAK,QAAQ;AAAA,IACtD;AAAA,EACF;AAAA,EACA,MAAM,SAAiC,CAAC;AAAA,EACxC,WAAW,OAAO,UAAU;AAAA,IAC1B,MAAM,QAAQ,KAAK,OAAO;AAAA,IAC1B,IAAI,aAAa,KAAK,GAAG;AAAA,MACvB,OAAO,OAAO,OAAO,KAAK;AAAA,IAC5B;AAAA,EACF;AAAA,EACA,OAAO,OAAO,KAAK,MAAM,EAAE,SAAS,IAAI,SAAS;AAAA;;;AFkEnD,IAAM,cAAc;AACpB,IAAM,aAAa;AAKnB,IAAM,iBAAiB,IAAI,IAAI;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKD,IAAM,YAAY,IAAI,IAAmB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAaD,IAAM,WAAW,EAAE,UAAU,uBAAuB;AACpD,IAAM,YAAY,EAAE,UAAU,kBAAkB;AAEhD,IAAM,WAAW,CAAC,aAAwC;AAAA,EACxD,IAAI;AAAA,IACF,OAAO,WAAW,UAAU,SAAS;AAAA,IACrC,MAAM;AAAA,IACN;AAAA;AAAA;AAeJ,IAAM,gBAAgB,CACpB,UACA,WAEA,WAAW,QAAQ,SAAS,QAAQ,IAAI,gBAAgB,UAAU,QAAQ;AAO5E,IAAM,cAAc,CAClB,MACA,KACA,SACS;AAAA,EACT,IAAI,CAAC,KAAK,mBAAmB;AAAA,IAC3B;AAAA,EACF;AAAA,EAGA,MAAM,QAAQ,eAAe,KAAK,MAAM,KAAK,IAAI,KAAK;AAAA,EACtD,MAAM,QAAQ,eACZ,MAAM,QAAQ,WACd,MAAM,QAAQ,SAChB;AAAA,EACA,IAAI,OAAO;AAAA,IACT,IAAI,KAAK,OAAO,GAAG;AAAA,EACrB;AAAA,EAEA,IAAI,KAAK,KAAK,OAAO,GAAG;AAAA;AAS1B,IAAM,gBAAgB,CACpB,MACA,SAEA,gBAGE,MACA,KAAK,SAAS,kBAAkB,MAAM,MAEtC,KAAK,UAAU,MACjB;AAIF,IAAM,iBAAiB,CAAC,SACtB,CAAC,eAAe,IAAI,KAAK,IAAI,KAAK,KAAK,SAAS,WAAW;AAG7D,IAAM,cAAc,CAAC,MAAa,KAAe,SAAqB;AAAA,EACpE,IAAI,UAAU,IAAI,KAAK,IAAI,GAAG;AAAA,IAC5B;AAAA,EACF;AAAA,EACA,QAAQ,KAAK;AAAA,SACN,QAAQ;AAAA,MACX,YAAY,MAAM,KAAK,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,SAKK,qBAAqB;AAAA,MAGxB,MAAM,OAAO,uBACX,MACA,KAAK,SACP;AAAA,MACA,IAAI,SAAS,MAAM;AAAA,QACjB,YAAY,gBAAgB,MAAM,QAAQ,GAAG,KAAK,IAAI;AAAA,QACtD;AAAA,MACF;AAAA,MACA;AAAA,IACF;AAAA,SAGK,cAAc;AAAA,MACjB,IAAI,KAAK,KAAK,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,SAKK,QAAQ;AAAA,MACX,IAAI,KACF,KAAK,MAAM,WAAW,cAAc,GAAG,EAAE,WAAW,aAAa,GAAG,GACpE,GACF;AAAA,MACA;AAAA,IACF;AAAA,SACK,SAAS;AAAA,MACZ,IAAI,KAAK,GAAG;AAAA,MACZ;AAAA,IACF;AAAA,SACK,iBAAiB;AAAA,MACpB,IAAI,KAAK,cAAc,MAAM,IAAI,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,SACK,iBAAiB;AAAA,MACpB,IAAI,KAAK,cAAc,MAAM,IAAI,GAAG,GAAG;AAAA,MACvC;AAAA,IACF;AAAA,SACK,WAAW;AAAA,MAEd,eAAe,MAAM,KAAK,IAAI;AAAA,MAC9B;AAAA,IACF;AAAA,aACS;AAAA,MACP;AAAA,IACF;AAAA;AAAA,EAEF,IAAI,WAAW,MAAM;AAAA,IACnB,IAAI,KAAK,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AAAA,EACA,IAAI,cAAc,MAAM;AAAA,IACtB,WAAW,SAAS,KAAK,UAAU;AAAA,MACjC,YAAY,OAAO,KAAK,IAAI;AAAA,IAC9B;AAAA,IACA,IAAI,eAAe,IAAI,GAAG;AAAA,MACxB,IAAI,KAAK,GAAG;AAAA,IACd;AAAA,EACF;AAAA;AAYF,IAAM,iBAAiB,CACrB,MACA,KACA,SACS;AAAA,EACT,MAAM,OAAO,KAAK,SAAS,GAAG,EAAE;AAAA,EAChC,MAAM,WAAW,MAAM,SAAS,SAAS,OAAO;AAAA,EAChD,MAAM,QAAkB,CAAC;AAAA,EACzB,MAAM,OAAO,WAAW,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;AAAA,EAC1D,WAAW,SAAS,MAAM;AAAA,IACxB,YAAY,OAAO,OAAO,IAAI;AAAA,EAChC;AAAA,EACA,IAAI,UAAU;AAAA,IACZ,MAAM,WAAW,oBAAoB,SAAS,KAAK,EAAE;AAAA,IACrD,MAAM,UAAU,aAAa,MAAM,KAAK,SAAS,WAAW;AAAA,IAC5D,MAAM,KAAK,UAAU,SAAS,QAAQ,QAAQ;AAAA,EAChD;AAAA,EACA,IAAI,KAAK,MAAM,KAAK,EAAE,EAAE,QAAQ,GAAG,GAAG;AAAA;AAUxC,IAAM,cAAc,CAClB,UACA,QACA,SACW;AAAA,EACX,MAAM,OAAO,cAAc,UAAU,MAAM;AAAA,EAC3C,IAAI,CAAC,MAAM;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAgB,CAAC;AAAA,EACvB,YAAY,MAAM,KAAK,IAAI;AAAA,EAC3B,OAAO,IAAI,KAAK,EAAE,EAAE,WAAW,YAAY,GAAG,EAAE,KAAK;AAAA;AAcvD,IAAM,kBAAkB,CAAC,YAA4C;AAAA,EACnE,MAAM,QAAQ,IAAI;AAAA,EAMlB,MAAM,cAAc,IAAI;AAAA,EACxB,MAAM,OAAO,CAAC,OAAkB,UAA0B;AAAA,IACxD,WAAW,QAAQ,OAAO;AAAA,MACxB,IAAI,KAAK,SAAS,SAAS;AAAA,QACzB,IAAI,KAAK,SAAS,CAAC,YAAY,IAAI,KAAK,KAAK,GAAG;AAAA,UAC9C,YAAY,IAAI,KAAK,OAAO;AAAA,YAC1B,YAAY,CAAC,GAAG,OAAO,KAAK,KAAK;AAAA,YACjC,SAAS,KAAK;AAAA,UAChB,CAAC;AAAA,QACH;AAAA,QACA,KAAK,KAAK,UAAU,CAAC,GAAG,OAAO,KAAK,KAAK,CAAC;AAAA,MAC5C,EAAO,SAAI,KAAK,OAAO;AAAA,QACrB,MAAM,IAAI,KAAK,OAAO;AAAA,UACpB,YAAY;AAAA,UACZ,SAAS,MAAM,GAAG,EAAE,KAAK;AAAA,QAC3B,CAAC;AAAA,MACH;AAAA,IACF;AAAA;AAAA,EAEF,KAAK,SAAS,CAAC,CAAC;AAAA,EAChB,YAAY,OAAO,WAAW,aAAa;AAAA,IACzC,IAAI,CAAC,MAAM,IAAI,KAAK,GAAG;AAAA,MACrB,MAAM,IAAI,OAAO,MAAM;AAAA,IACzB;AAAA,EACF;AAAA,EACA,OAAO;AAAA;AAmBT,IAAM,WAAW,OACf,SACA,MACA,SACA,YACA,iBACoB;AAAA,EACpB,IAAI,CAAC,MAAM;AAAA,IACT,OAAO;AAAA,EACT;AAAA,EACA,MAAM,SAAS,oBAAO,MAAM,sBAAsB,SAAS,IAAI,CAAC;AAAA,EAChE,MAAM,UAAU,wBACd,OAAO,SACP,SAAS,YAAY,KACvB;AAAA,EACA,IAAI,cAAc;AAAA,IAChB,OAAO,oBACL,aAAa,SAAS,IAAI,EAAE,KAAK,GACjC,YACA,OAAO,IACT;AAAA,EACF;AAAA,EACA,OAAO,YAAY,SAAS,KAAK,QAAQ;AAAA,IACvC,WAAW;AAAA,MACT,aAAa,OAAO;AAAA,MACpB,UAAU,kBAAkB,UAAU;AAAA,MACtC,QAAQ;AAAA,IACV;AAAA,IACA,mBAAmB,QAAQ,OAAO,OAAO,SAAS;AAAA,EACpD,CAAC;AAAA;AAIH,IAAM,gBAAgB,CACpB,WAC+C;AAAA,EAC/C,QAAQ,OAAO,aAAa,UAAU,CAAC;AAAA,EACvC,OAAO;AAAA,OACD,UAAU,aAAa,UAAU,KAAK,EAAE,MAAM;AAAA,OAC9C,YAAY,SAAS,SAAS,KAAK,EAAE,SAAS;AAAA,EACpD;AAAA;AAkCK,IAAM,uBAAuB,OAClC,SACA,YAC8B;AAAA,EAC9B,MAAM,WAAW,IAAI,IAAI,QAAQ,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAAA,EAK3E,MAAM,WAAW,OAAO,OAAO,QAAQ,MAAM,sBAAsB,CAAC,CAAC;AAAA,EAGrE,MAAM,YAAY,OAAO,OAAO,QAAQ,MAAM,uBAAuB,CAAC,CAAC,EACpE,QAAQ,CAAC,YAAY,OAAO,OAAO,OAAO,CAAC,EAC3C,IAAI,CAAC,QAAQ,IAAI,OAAO;AAAA,EAC3B,MAAM,WAAW;AAAA,IACf,GAAI,SAAS,SAAS,IAClB,SAAS,IAAI,CAAC,QAAQ,IAAI,OAAO,IACjC,CAAC,QAAQ,MAAM,YAAY,WAAW,CAAC,CAAC;AAAA,IAC5C,GAAG;AAAA,EACL;AAAA,EACA,MAAM,SAAS,IAAI;AAAA,EACnB,WAAW,WAAW,UAAU;AAAA,IAC9B,YAAY,OAAO,UAAU,gBAAgB,OAAO,GAAG;AAAA,MACrD,OAAO,IAAI,OAAO,KAAK;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,QAAQ,SAAS,OAAO,OAAO,CAAC,UAAU;AAAA,IAC1D,IAAI,CAAC,SAAS,qBAAqB;AAAA,MACjC,OAAO,MAAM;AAAA,IACf;AAAA,IAIA,IAAI,MAAM,UAAU;AAAA,MAClB,OAAO;AAAA,IACT;AAAA,IACA,MAAM,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,IAClC,QAAQ,QAAQ,UAAU;AAAA,IAC1B,OAAO,OAAO,iBAAiB,MAAM,QAAQ,KAAK,IAAI;AAAA,GACvD;AAAA,EAID,MAAM,aAAa,4BAA4B,OAAO;AAAA,EAGtD,MAAM,eACJ,SAAS,YAAY,aACjB,qBAAqB,SAAS,EAAE,WAAW,KAAK,CAAC,IACjD;AAAA,EAIN,QAAQ,SAAS,QAAQ;AAAA,EACzB,MAAM,eAAe,IAAI;AAAA,EACzB,MAAM,cAAc,CAAC,WAA2B;AAAA,IAC9C,IAAI,QAAQ,aAAa,IAAI,MAAM;AAAA,IACnC,IAAI,UAAU,WAAW;AAAA,MACvB,SACE,OACI,iBAAiB,QAAQ;AAAA,QACvB,eAAe,KAAK;AAAA,QACpB,WAAW,KAAK;AAAA,MAClB,CAAC,IACD,OACJ,OAAO;AAAA,MACT,aAAa,IAAI,QAAQ,KAAK;AAAA,IAChC;AAAA,IACA,OAAO;AAAA;AAAA,EAGT,OAAO,MAAM,QAAQ,IACnB,UAAU,IAAI,OAAO,UAAU;AAAA,IAC7B,MAAM,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,IAClC,MAAM,OAAO,MAAM,SACjB,SACA,MACA,SACA,YACA,YACF;AAAA,IACA,MAAM,OAAO,MAAM,MAAM,QAAQ;AAAA,IACjC,MAAM,QAAQ,OAAO,IAAI,MAAM,IAAI;AAAA,IACnC,MAAM,SAAS,OAAO,WAAW,MAAM,QAAQ,MAAM,IAAI;AAAA,IACzD,MAAM,WAA2B;AAAA,MAC/B,YAAY,OAAO,cAAc,CAAC;AAAA,MAClC,SAAS;AAAA,MACT,aAAa,MAAM;AAAA,MACnB,aAAa,MAAM,eAAe;AAAA,MAClC,QAAQ,MAAM;AAAA,MACd,OAAO,MAAM;AAAA,MACb,SAAS,OAAO,WAAW,YAAY,MAAM,MAAM;AAAA,MACnD,MAAM,QAAQ,KAAK,SAAS,IAAI,OAAO;AAAA,MACvC,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,IACjB;AAAA,IACA,IAAI,QAAQ;AAAA,MACV,SAAS,SAAS;AAAA,IACpB;AAAA,IACA,OAAO,KAAK,aAAa,cAAc,MAAM,MAAM,MAAM,EAAE;AAAA,GAC5D,CACH;AAAA;;;AG3gBK,IAAM,eAAe,OAAO,YAA4C;AAAA,EAC7E,QAAQ,QAAQ,OAAO,aAAa;AAAA,EACpC,OAAO,WAAW,eAAe,MAAM,QAAQ,IAAI;AAAA,IAKjD,qBAAqB,SAAS;AAAA,MAC5B,UAAU;AAAA,MACV,qBAAqB;AAAA,IACvB,CAAC;AAAA,IACD,iBAAiB,OAAO;AAAA,EAC1B,CAAC;AAAA,EAGD,MAAM,QAAQ,OAAO,YACnB,OAAO,QAAQ,WAAW,EAAE,IAAI,EAAE,OAAO,WAAW;AAAA,IAClD;AAAA,IACA,cAAc,KAAK;AAAA,EACrB,CAAC,CACH;AAAA,EAEA,MAAM,WAAW,IAAI,IAAI,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAAA,EAEnE,MAAM,SAAqB,CAAC;AAAA,EAK5B,WAAW,SAAS,SAAS,QAAQ;AAAA,IACnC,IAAI,MAAM,UAAU,MAAM,UAAU;AAAA,MAClC;AAAA,IACF;AAAA,IACA,MAAM,OAAO,SAAS,IAAI,MAAM,EAAE;AAAA,IAClC,MAAM,SAAS,OAAO,WAAW,MAAM,MAAM,IAAI;AAAA,IACjD,MAAM,QAAkB;AAAA,MACtB,aAAa,MAAM;AAAA,MACnB,aAAa,MAAM;AAAA,MACnB,WAAW,MAAM;AAAA,MACjB,cAAc,MAAM,gBAAgB;AAAA,MACpC,QAAQ,MAAM;AAAA,MACd,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,IACjB;AAAA,IACA,IAAI,QAAQ;AAAA,MACV,MAAM,SAAS;AAAA,IACjB;AAAA,IACA,OAAO,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,MAAM,OAAgB;AAAA,IACpB,MAAM,kBAAkB,OAAO,WAAW,QAAQ,IAAI;AAAA,IACtD,eAAe,OAAO,MAAM;AAAA,IAC5B,WAAW,UAAU,IAAI,CAAC,QAAQ;AAAA,MAChC,MAAM,WAAqB;AAAA,QACzB,SAAS,IAAI;AAAA,QACb,aAAa,IAAI;AAAA,QACjB,aAAa,IAAI;AAAA,QACjB,QAAQ,IAAI;AAAA,QACZ,OAAO,IAAI;AAAA,QACX,OAAO,IAAI;AAAA,QACX,SAAS,IAAI;AAAA,MACf;AAAA,MACA,IAAI,IAAI,QAAQ;AAAA,QACd,SAAS,SAAS,IAAI;AAAA,MACxB;AAAA,MACA,IAAI,IAAI,UAAU,WAAW;AAAA,QAC3B,SAAS,QAAQ,IAAI;AAAA,MACvB;AAAA,MACA,IAAI,IAAI,UAAU;AAAA,QAChB,SAAS,WAAW,IAAI;AAAA,MAC1B;AAAA,MACA,OAAO;AAAA,KACR;AAAA,IACD,cAAc,OAAO,OAAO,IAAI;AAAA,IAChC,MAAM,OAAO,OAAO,IAAI,QAAQ,OAAO;AAAA,IACvC,YAAY,MAAM;AAAA,IAClB;AAAA,IACA;AAAA,IACA,MAAM,OAAO,WAAW,QAAQ,QAAQ;AAAA,IACxC,SAAS,SAAS;AAAA,EACpB;AAAA,EACA,IAAI,OAAO,UAAU;AAAA,IACnB,KAAK,mBAAmB,OAAO,SAAS,SAAS,IAC/C,CAAC,YAAY,QAAQ,EACvB;AAAA,IACA,KAAK,sBAAsB,MAAM;AAAA,EACnC;AAAA,EACA,IAAI,OAAO,MAAM;AAAA,IACf,KAAK,qBAAqB,MAAM;AAAA,EAClC;AAAA,EACA,OAAO;AAAA;",
|
|
11
|
+
"debugId": "0C8488DE9FAE851264756E2164756E21",
|
|
12
|
+
"names": []
|
|
13
|
+
}
|