blume 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/cli/index.js +1715 -539
- package/dist/cli/index.js.map +40 -31
- package/dist/types/core/config-input.d.ts +131 -11
- package/dist/types/core/config.d.ts +9 -1
- package/dist/types/core/data.d.ts +24 -5
- package/dist/types/core/i18n-ui.d.ts +58 -799
- package/dist/types/core/schema.d.ts +534 -3305
- package/dist/types/theme/fonts.d.ts +55 -11
- package/docs/02-deployment.mdx +2 -0
- package/docs/07-faq.mdx +14 -14
- package/docs/advanced/skills.mdx +2 -2
- package/docs/configuration/ai.mdx +126 -2
- package/docs/configuration/index.mdx +19 -1
- package/docs/configuration/search.mdx +17 -0
- package/docs/configuration/seo.mdx +26 -3
- package/docs/configuration/theming.mdx +44 -2
- package/docs/content/syntax.mdx +18 -2
- package/docs/reference/cli.mdx +3 -3
- package/package.json +9 -8
- package/skills/blume/SKILL.md +6 -4
- package/skills/blume-migrate/SKILL.md +5 -3
- package/skills/blume-migrate/references/mintlify.md +5 -5
- package/skills/blume-migrate/references/monorepo.md +2 -1
- package/src/ai/agent-readability.ts +31 -1
- package/src/ai/api-catalog.ts +81 -0
- package/src/ai/ask-context.ts +7 -1
- package/src/ai/ask-data.ts +1 -0
- package/src/ai/link-headers.ts +52 -0
- package/src/ai/llms.ts +12 -1
- package/src/ai/markdown.ts +15 -2
- package/src/ai/mcp/data.ts +7 -0
- package/src/ai/mcp/discovery.ts +70 -15
- package/src/ai/mcp/server.ts +14 -8
- package/src/ai/mcp/stdio.ts +4 -1
- package/src/ai/skills.ts +193 -0
- package/src/ai/tar.ts +104 -0
- package/src/ai/web-bot-auth.ts +30 -0
- package/src/astro/generate.ts +116 -6
- package/src/astro/integration.ts +52 -14
- package/src/astro/templates.ts +191 -37
- package/src/audit/catalog.ts +20 -0
- package/src/audit/checks/dns-aid.ts +190 -0
- package/src/audit/report.ts +5 -0
- package/src/audit/run.ts +2 -0
- package/src/cli/commands/build.ts +178 -9
- package/src/cli/init/scaffold.ts +1 -1
- package/src/components/islands/ask-ai.tsx +4 -1
- package/src/components/islands/webmcp.ts +203 -0
- package/src/components/layout/NavTree.astro +4 -4
- package/src/components/layout/PageLayout.astro +2 -0
- package/src/components/layout/ReferenceLayout.astro +2 -0
- package/src/components/layout/RootLayout.astro +63 -11
- package/src/components/layout/Search.astro +2 -2
- package/src/components/layout/WebMcp.astro +49 -0
- package/src/components/layout/search/orama.ts +5 -2
- package/src/core/config-input.ts +143 -11
- package/src/core/config.ts +17 -1
- package/src/core/content-assets.ts +199 -0
- package/src/core/data.ts +21 -5
- package/src/core/diagnostics.ts +6 -5
- package/src/core/i18n-ui.ts +19 -28
- package/src/core/project-graph.ts +6 -0
- package/src/core/schema.ts +224 -71
- package/src/core/sources/normalize.ts +5 -5
- package/src/deploy/headers.ts +45 -3
- package/src/deploy/vercel-negotiation.ts +233 -0
- package/src/markdown/mermaid.ts +7 -1
- package/src/markdown/table-wrap.ts +33 -1
- package/src/og/card.ts +91 -22
- package/src/og/derive.ts +200 -0
- package/src/og/index.ts +6 -1
- package/src/search/orama-index.ts +151 -7
- package/src/theme/entry.ts +34 -13
- package/src/theme/fonts.ts +183 -30
- package/dist/types/og/card.d.ts +0 -63
- package/dist/types/og/dimensions.d.ts +0 -12
package/src/og/derive.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges `theme.fonts` into the OG card renderer. A site that explicitly
|
|
3
|
+
* picks its typefaces gets matching cards (and non-Latin coverage) without
|
|
4
|
+
* configuring `seo.og.fonts`; untouched defaults derive nothing, so plain
|
|
5
|
+
* sites keep Takumi's built-in font and gain no build-time font fetch.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
import { isAbsolute, join } from "pathe";
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
FontsConfig,
|
|
14
|
+
FontValue,
|
|
15
|
+
LocalFontConfig,
|
|
16
|
+
RemoteFontConfig,
|
|
17
|
+
} from "../theme/fonts.ts";
|
|
18
|
+
import { GOOGLE_FONTS, isFontSlug } from "../theme/fonts.ts";
|
|
19
|
+
import type { OgFont, OgFontFamilies, OgLocalFont } from "./card.ts";
|
|
20
|
+
|
|
21
|
+
/** Fonts plus per-role families for the generated OG endpoint. */
|
|
22
|
+
export interface DerivedOgFonts {
|
|
23
|
+
families?: OgFontFamilies;
|
|
24
|
+
fonts: OgFont[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The weights the card actually renders at (title 600, everything else 400). */
|
|
28
|
+
const CARD_WEIGHTS = [400, 600];
|
|
29
|
+
|
|
30
|
+
/** Resolve a config path against the project root. */
|
|
31
|
+
const absoluteSrc = (root: string, src: string): string =>
|
|
32
|
+
isAbsolute(src) ? src : join(root, src);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The weight spec to fetch for a derived Google family: the declared weights
|
|
36
|
+
* the card uses, the declared numeric weights otherwise, a lone variable
|
|
37
|
+
* range as-is, or nothing (family default) as the last resort.
|
|
38
|
+
*/
|
|
39
|
+
const googleWeights = (
|
|
40
|
+
weights: (number | string)[]
|
|
41
|
+
): number[] | string | undefined => {
|
|
42
|
+
const numbers = weights.filter(
|
|
43
|
+
(weight): weight is number => typeof weight === "number"
|
|
44
|
+
);
|
|
45
|
+
const used = numbers.filter((weight) => CARD_WEIGHTS.includes(weight));
|
|
46
|
+
if (used.length > 0) {
|
|
47
|
+
return used;
|
|
48
|
+
}
|
|
49
|
+
if (numbers.length > 0) {
|
|
50
|
+
return numbers;
|
|
51
|
+
}
|
|
52
|
+
const [first] = weights;
|
|
53
|
+
return weights.length === 1 && typeof first === "string" ? first : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const googleOgFont = (name: string, weights: (number | string)[]): OgFont => {
|
|
57
|
+
const weight = googleWeights(weights);
|
|
58
|
+
return weight === undefined ? { name } : { name, weight };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Per-variant local entries for the renderer (paths made absolute). */
|
|
62
|
+
const localOgFonts = (font: LocalFontConfig, root: string): OgLocalFont[] =>
|
|
63
|
+
font.variants.map((variant) => ({
|
|
64
|
+
name: font.name,
|
|
65
|
+
src: absoluteSrc(root, variant.src),
|
|
66
|
+
...(typeof variant.weight === "number" ? { weight: variant.weight } : {}),
|
|
67
|
+
// Takumi's per-face style is normal/italic; oblique falls back to the file.
|
|
68
|
+
...(variant.style === "normal" || variant.style === "italic"
|
|
69
|
+
? { style: variant.style }
|
|
70
|
+
: {}),
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The card fonts for one theme role, or null when the role can't flow into
|
|
75
|
+
* the renderer (an unknown slug string, or a provider Takumi can't fetch —
|
|
76
|
+
* `googleFonts` only speaks Google's css2 endpoint).
|
|
77
|
+
*/
|
|
78
|
+
const roleFonts = (value: FontValue, root: string): OgFont[] | null => {
|
|
79
|
+
if (typeof value === "string") {
|
|
80
|
+
if (!isFontSlug(value)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const def = GOOGLE_FONTS[value];
|
|
84
|
+
return [googleOgFont(def.family, def.weights)];
|
|
85
|
+
}
|
|
86
|
+
if ("variants" in value) {
|
|
87
|
+
return localOgFonts(value, root);
|
|
88
|
+
}
|
|
89
|
+
const remote = value as RemoteFontConfig;
|
|
90
|
+
if ((remote.provider ?? "google") !== "google") {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return [googleOgFont(remote.name, remote.weights ?? CARD_WEIGHTS)];
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The family name a theme role registers under. */
|
|
97
|
+
const roleFamily = (value: FontValue): string | null => {
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
return isFontSlug(value) ? GOOGLE_FONTS[value].family : null;
|
|
100
|
+
}
|
|
101
|
+
return value.name;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Derive the OG card fonts from the theme's display and body roles (the two
|
|
106
|
+
* the card renders), deduped, plus the per-role family names so the title
|
|
107
|
+
* keeps the display face and the body text the body face.
|
|
108
|
+
*/
|
|
109
|
+
export const deriveOgFonts = (
|
|
110
|
+
fonts: FontsConfig,
|
|
111
|
+
root: string
|
|
112
|
+
): DerivedOgFonts => {
|
|
113
|
+
const derived: OgFont[] = [];
|
|
114
|
+
const seen = new Set<string>();
|
|
115
|
+
const families: OgFontFamilies = {};
|
|
116
|
+
|
|
117
|
+
const roles = [
|
|
118
|
+
["title", fonts?.display],
|
|
119
|
+
["body", fonts?.body],
|
|
120
|
+
] as const;
|
|
121
|
+
for (const [role, value] of roles) {
|
|
122
|
+
if (value === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const roleEntries = roleFonts(value, root);
|
|
126
|
+
if (!roleEntries) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
families[role] = roleFamily(value) ?? undefined;
|
|
130
|
+
for (const entry of roleEntries) {
|
|
131
|
+
const key = JSON.stringify(entry);
|
|
132
|
+
if (!seen.has(key)) {
|
|
133
|
+
seen.add(key);
|
|
134
|
+
derived.push(entry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
fonts: derived,
|
|
141
|
+
...(families.title || families.body ? { families } : {}),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Explicit `seo.og.fonts` with local `src` paths resolved to absolute. */
|
|
146
|
+
export const resolveOgFontSources = (fonts: OgFont[], root: string): OgFont[] =>
|
|
147
|
+
fonts.map((font) =>
|
|
148
|
+
typeof font !== "string" && "src" in font
|
|
149
|
+
? { ...font, src: absoluteSrc(root, font.src) }
|
|
150
|
+
: font
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The fonts baked into the generated OG endpoint. An explicit `seo.og.fonts`
|
|
155
|
+
* always wins (including `[]` to opt out, keeping the card's role styling
|
|
156
|
+
* untouched); otherwise a site that explicitly set `theme.fonts` gets its
|
|
157
|
+
* display/body fonts derived so cards match the site without extra config.
|
|
158
|
+
*/
|
|
159
|
+
export const resolveOgFonts = (
|
|
160
|
+
options: {
|
|
161
|
+
/** Explicit `seo.og.fonts`, or undefined when unset. */
|
|
162
|
+
ogFonts: OgFont[] | undefined;
|
|
163
|
+
themeFonts: FontsConfig;
|
|
164
|
+
/** Whether the config file set `theme.fonts` itself (gates derivation). */
|
|
165
|
+
themeFontsConfigured: boolean;
|
|
166
|
+
},
|
|
167
|
+
root: string
|
|
168
|
+
): DerivedOgFonts => {
|
|
169
|
+
if (options.ogFonts) {
|
|
170
|
+
return { fonts: resolveOgFontSources(options.ogFonts, root) };
|
|
171
|
+
}
|
|
172
|
+
return options.themeFontsConfigured
|
|
173
|
+
? deriveOgFonts(options.themeFonts, root)
|
|
174
|
+
: { fonts: [] };
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Every configured local font file (theme roles and `seo.og.fonts`) that is
|
|
179
|
+
* missing on disk, as absolute paths. Generation fails on these up front — the
|
|
180
|
+
* alternative is Astro or the OG renderer crashing later with a bare ENOENT.
|
|
181
|
+
*/
|
|
182
|
+
export const missingFontFiles = (
|
|
183
|
+
options: { ogFonts: OgFont[]; themeFonts: FontsConfig },
|
|
184
|
+
root: string
|
|
185
|
+
): string[] => {
|
|
186
|
+
const sources: string[] = [];
|
|
187
|
+
for (const value of Object.values(options.themeFonts ?? {})) {
|
|
188
|
+
if (typeof value !== "string" && "variants" in value) {
|
|
189
|
+
sources.push(
|
|
190
|
+
...value.variants.map((variant) => absoluteSrc(root, variant.src))
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const font of options.ogFonts) {
|
|
195
|
+
if (typeof font !== "string" && "src" in font) {
|
|
196
|
+
sources.push(absoluteSrc(root, font.src));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...new Set(sources)].filter((path) => !existsSync(path));
|
|
200
|
+
};
|
package/src/og/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create, insertMultiple, search } from "@orama/orama";
|
|
2
|
-
import type { AnyOrama } from "@orama/orama";
|
|
2
|
+
import type { AnyOrama, Tokenizer } from "@orama/orama";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The minimal document shape both the client-side search dialog and the
|
|
@@ -31,23 +31,161 @@ const SCHEMA = {
|
|
|
31
31
|
/** Title and description outrank body text, matching the search dialog. */
|
|
32
32
|
const BOOST = { description: 2, title: 3 };
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Scripts written without spaces between words. Orama's default tokenizer
|
|
36
|
+
* splits on a Latin-centric delimiter class, so text in these languages
|
|
37
|
+
* collapses to zero tokens and every query silently returns no hits. Keyed by
|
|
38
|
+
* the primary language subtag of `i18n.defaultLocale`.
|
|
39
|
+
*/
|
|
40
|
+
const SEGMENTED_LANGUAGES = new Set(["ja", "ko", "th", "zh"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Languages indexed as character bigrams rather than whole segments, and
|
|
44
|
+
* queried to match accordingly. Japanese and Chinese write compounds in
|
|
45
|
+
* {@link BIGRAM_SCRIPTS} without delimiters; Korean separates words with
|
|
46
|
+
* spaces and Thai has no comparable bigram convention, so both keep the plain
|
|
47
|
+
* segmented tokens even where a page mixes in Han or kana.
|
|
48
|
+
*
|
|
49
|
+
* Dictionary segmentation alone drops the adjacency that makes a compound term
|
|
50
|
+
* distinctive: 資金決済法 becomes 資金 / 決済 / 法, and because Orama scores a
|
|
51
|
+
* bag of words, a page that merely mentions each fragment somewhere outranks
|
|
52
|
+
* the page about the law itself. Bigrams put that adjacency back as index
|
|
53
|
+
* terms, and dropping the whole-segment tokens keeps a fragment as common as
|
|
54
|
+
* 法 from matching on its own.
|
|
55
|
+
*/
|
|
56
|
+
const BIGRAM_LANGUAGES = new Set(["ja", "zh"]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Segments written entirely in these scripts are the ones re-cut into bigrams,
|
|
60
|
+
* matching the scripts Lucene's CJK analyzer bigrams. Property escapes rather
|
|
61
|
+
* than ranges, so ideographs outside the basic plane are covered as well —
|
|
62
|
+
* 𠮟, the 常用漢字表 form of しかる, is one. The literals that follow belong to
|
|
63
|
+
* no script of their own but appear only inside such words: the iteration
|
|
64
|
+
* marks, the prolonged sound mark, and the halfwidth voiced sound marks.
|
|
65
|
+
*/
|
|
66
|
+
const BIGRAM_SCRIPTS =
|
|
67
|
+
/^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}々〆〇ー゙゚]+$/u;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Emit every overlapping 2-character window of `run`, or the lone character.
|
|
71
|
+
* Windows are cut by code point: an ideograph outside the basic plane is a
|
|
72
|
+
* surrogate pair, and slicing by code unit would split it into halves that
|
|
73
|
+
* match nothing.
|
|
74
|
+
*
|
|
75
|
+
* Dropping the whole-segment tokens means a single-character query reaches
|
|
76
|
+
* only pages where the character opens a bigram: a run-final 法 sits in 示法,
|
|
77
|
+
* which the query 法 does not prefix-match. Lucene's CJK analyzer shares this
|
|
78
|
+
* property; indexing lone characters alongside the bigrams would reinvite the
|
|
79
|
+
* fragment noise this file exists to remove.
|
|
80
|
+
*/
|
|
81
|
+
const addBigrams = (run: string, tokens: Set<string>): void => {
|
|
82
|
+
const characters = [...run];
|
|
83
|
+
if (characters.length === 1) {
|
|
84
|
+
tokens.add(run);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let previous = "";
|
|
88
|
+
for (const character of characters) {
|
|
89
|
+
if (previous) {
|
|
90
|
+
tokens.add(previous + character);
|
|
91
|
+
}
|
|
92
|
+
previous = character;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A word-segmenting tokenizer for languages the default splitter can't handle,
|
|
98
|
+
* built on `Intl.Segmenter` (the same engine `@orama/tokenizers` wraps).
|
|
99
|
+
* Input is lowercased before segmenting — unlike the upstream tokenizers —
|
|
100
|
+
* so Latin terms ("GDPR", English pages on a mixed-locale site) still match
|
|
101
|
+
* case-insensitively. Returns `undefined` for languages the default tokenizer
|
|
102
|
+
* already serves, and on runtimes without `Intl.Segmenter`, where the caller
|
|
103
|
+
* falls back to Orama's default.
|
|
104
|
+
*
|
|
105
|
+
* On a {@link BIGRAM_LANGUAGES} index, runs of adjacent
|
|
106
|
+
* {@link BIGRAM_SCRIPTS} segments are joined and re-cut into character
|
|
107
|
+
* bigrams; everything else (Latin, digits, and every segment on a Korean or
|
|
108
|
+
* Thai index) is emitted as the segmenter produced it. Punctuation and spaces
|
|
109
|
+
* are not word-like, so they end a run — 「クーリング・オフ」 bigrams either
|
|
110
|
+
* side of the interpunct rather than across it.
|
|
111
|
+
*/
|
|
112
|
+
const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
113
|
+
const language = locale?.toLowerCase().split(/[-_]/u)[0] ?? "";
|
|
114
|
+
if (!SEGMENTED_LANGUAGES.has(language)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (typeof Intl.Segmenter !== "function") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const segmenter = new Intl.Segmenter(language, { granularity: "word" });
|
|
121
|
+
// Keyed off the same set the strict query pass reads, so an index is never
|
|
122
|
+
// built from bigrams that the query side then matches loosely.
|
|
123
|
+
const bigram = BIGRAM_LANGUAGES.has(language);
|
|
124
|
+
return {
|
|
125
|
+
language,
|
|
126
|
+
normalizationCache: new Map(),
|
|
127
|
+
tokenize: (raw: string): string[] => {
|
|
128
|
+
const tokens = new Set<string>();
|
|
129
|
+
let run = "";
|
|
130
|
+
const flush = (): void => {
|
|
131
|
+
if (run) {
|
|
132
|
+
addBigrams(run, tokens);
|
|
133
|
+
run = "";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (const segment of segmenter.segment(raw.toLowerCase())) {
|
|
137
|
+
if (!segment.isWordLike) {
|
|
138
|
+
flush();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (bigram && BIGRAM_SCRIPTS.test(segment.segment)) {
|
|
142
|
+
run += segment.segment;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
flush();
|
|
146
|
+
tokens.add(segment.segment);
|
|
147
|
+
}
|
|
148
|
+
flush();
|
|
149
|
+
return [...tokens];
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
34
154
|
/**
|
|
35
155
|
* Build an in-memory Orama full-text index from search documents. Shared by the
|
|
36
|
-
* Orama client loader (browser)
|
|
37
|
-
* identical wherever docs are queried.
|
|
156
|
+
* Orama client loader (browser), the MCP server, and Ask AI grounding (Node),
|
|
157
|
+
* so ranking is identical wherever docs are queried. `locale` — the site's
|
|
158
|
+
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for languages
|
|
159
|
+
* written without spaces (Japanese, Chinese, Korean, Thai); the tokenizer
|
|
160
|
+
* belongs to the database, so on a mixed-locale site it applies to every
|
|
161
|
+
* document, which is safe because Latin words survive segmentation intact.
|
|
38
162
|
*/
|
|
39
163
|
export const buildOramaIndex = async (
|
|
40
|
-
documents: OramaDoc[]
|
|
164
|
+
documents: OramaDoc[],
|
|
165
|
+
locale?: string
|
|
41
166
|
): Promise<AnyOrama> => {
|
|
42
|
-
const
|
|
167
|
+
const tokenizer = segmentingTokenizer(locale);
|
|
168
|
+
const db = create({
|
|
169
|
+
schema: SCHEMA,
|
|
170
|
+
...(tokenizer ? { components: { tokenizer } } : {}),
|
|
171
|
+
});
|
|
43
172
|
await insertMultiple(db, documents);
|
|
44
173
|
return db;
|
|
45
174
|
};
|
|
46
175
|
|
|
176
|
+
/** Orama keeps only documents matching every token at a threshold of 0. */
|
|
177
|
+
const ALL_TOKENS = 0;
|
|
178
|
+
|
|
47
179
|
/**
|
|
48
180
|
* Query the index, returning the matching documents (highest-ranked first).
|
|
49
181
|
* When `locale` is given, results are filtered to that language via an exact
|
|
50
182
|
* `where` match on the `locale` enum.
|
|
183
|
+
*
|
|
184
|
+
* On a bigrammed index the strict pass runs first: a term is only meant to
|
|
185
|
+
* match where its bigrams sit together, and scoring them independently lets a
|
|
186
|
+
* page sharing a couple of windows outrank the page the term is about. Terms
|
|
187
|
+
* spanning several words rarely appear in full on one page, so an empty strict
|
|
188
|
+
* result falls back to the default pass rather than reporting no matches.
|
|
51
189
|
*/
|
|
52
190
|
export const queryOramaIndex = async (
|
|
53
191
|
db: AnyOrama,
|
|
@@ -55,12 +193,18 @@ export const queryOramaIndex = async (
|
|
|
55
193
|
limit: number,
|
|
56
194
|
locale?: string
|
|
57
195
|
): Promise<OramaDoc[]> => {
|
|
58
|
-
const
|
|
196
|
+
const params = {
|
|
59
197
|
boost: BOOST,
|
|
60
198
|
limit,
|
|
61
199
|
properties: ["title", "description", "content"],
|
|
62
200
|
term,
|
|
63
201
|
...(locale ? { where: { locale: { eq: locale } } } : {}),
|
|
64
|
-
}
|
|
202
|
+
};
|
|
203
|
+
const bigrammed = BIGRAM_LANGUAGES.has(db.tokenizer?.language ?? "");
|
|
204
|
+
const strict = bigrammed
|
|
205
|
+
? await search(db, { ...params, threshold: ALL_TOKENS })
|
|
206
|
+
: undefined;
|
|
207
|
+
const found =
|
|
208
|
+
strict && strict.hits.length > 0 ? strict : await search(db, params);
|
|
65
209
|
return found.hits.map((hit) => hit.document as unknown as OramaDoc);
|
|
66
210
|
};
|
package/src/theme/entry.ts
CHANGED
|
@@ -452,19 +452,29 @@ blume-diff {
|
|
|
452
452
|
even though its chrome wrapper is not-prose. */
|
|
453
453
|
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
454
454
|
display: block;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
455
|
+
/* Tall blocks scroll vertically in place instead of taking the page. Both
|
|
456
|
+
axes live on the code element for the same reason: the pre stays static
|
|
457
|
+
so the header bar and copy button never drift with the scroll. */
|
|
458
|
+
max-height: 24rem;
|
|
459
|
+
overflow: auto;
|
|
460
|
+
/* The small bottom inset keeps the horizontal thumb off the last line's
|
|
461
|
+
descenders now that scrollbars are visible. */
|
|
462
|
+
padding: 0 1.25rem 0.375rem;
|
|
463
|
+
/* Thin theme-colored scrollbars, matching the sidebar treatment, so a
|
|
464
|
+
height-capped block reads as scrollable instead of simply ending.
|
|
465
|
+
Safari before 18.2 supports neither property and falls back to the
|
|
466
|
+
platform-default scrollbar — acceptable, since macOS overlays it. */
|
|
467
|
+
scrollbar-color: var(--blume-border) transparent;
|
|
468
|
+
scrollbar-width: thin;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* The dark border token is too close to the page background to read as a
|
|
472
|
+
scrollbar thumb; derive a brighter one from the muted foreground instead. */
|
|
473
|
+
:root[data-theme="dark"]
|
|
474
|
+
.prose
|
|
475
|
+
:where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
476
|
+
scrollbar-color: color-mix(in oklab, var(--blume-muted-foreground) 55%, transparent)
|
|
477
|
+
transparent;
|
|
468
478
|
}
|
|
469
479
|
|
|
470
480
|
/* Word wrap (markdown.code.wrap): long lines wrap instead of scrolling. The
|
|
@@ -532,6 +542,10 @@ pre.blume-source {
|
|
|
532
542
|
|
|
533
543
|
pre.blume-source > code {
|
|
534
544
|
flex: 1;
|
|
545
|
+
/* The pane's measured height is authoritative (it can exceed the prose
|
|
546
|
+
24rem cap), so undo the generic prose max-height: the code must fill
|
|
547
|
+
whatever height the tab was given. */
|
|
548
|
+
max-height: none;
|
|
535
549
|
min-height: 0;
|
|
536
550
|
overflow: auto;
|
|
537
551
|
}
|
|
@@ -713,6 +727,13 @@ pre:has(.line.focused):hover .line:not(.focused) {
|
|
|
713
727
|
display: none !important;
|
|
714
728
|
}
|
|
715
729
|
|
|
730
|
+
/* Paper can't scroll: uncap the code scroller so long blocks print in
|
|
731
|
+
full, same as tab panels are force-expanded below. */
|
|
732
|
+
.prose :where(pre:not(.twoslash, .twoslash pre, blume-panel-tabs *) > code) {
|
|
733
|
+
max-height: none;
|
|
734
|
+
overflow: visible;
|
|
735
|
+
}
|
|
736
|
+
|
|
716
737
|
#blume-content {
|
|
717
738
|
padding: 0 !important;
|
|
718
739
|
}
|