blume 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cli/index.js +2755 -2564
  2. package/dist/cli/index.js.map +33 -31
  3. package/dist/types/core/package-json.d.ts +12 -0
  4. package/dist/types/migrate/shared.d.ts +153 -0
  5. package/docs/advanced/migrate.mdx +1 -0
  6. package/docs/configuration/ai.mdx +1 -1
  7. package/docs/configuration/theming.mdx +1 -1
  8. package/docs/content/i18n.mdx +1 -1
  9. package/docs/content/sources.mdx +1 -1
  10. package/package.json +1 -1
  11. package/src/ai/mcp/discovery.ts +3 -1
  12. package/src/ai/mcp/server.ts +3 -1
  13. package/src/astro/component-slots.ts +10 -2
  14. package/src/astro/static-assets.ts +10 -3
  15. package/src/astro/templates.ts +53 -21
  16. package/src/cli/coalesce.ts +43 -0
  17. package/src/cli/commands/dev.ts +31 -17
  18. package/src/cli/dev-lock.ts +4 -2
  19. package/src/components/content/ColorItem.astro +6 -3
  20. package/src/components/content/Prompt.astro +7 -3
  21. package/src/components/content/Tabs.astro +13 -2
  22. package/src/components/content/mermaid-element.ts +20 -2
  23. package/src/components/islands/ask-ai.tsx +4 -8
  24. package/src/components/islands/base-path.ts +30 -0
  25. package/src/components/islands/hooks.ts +12 -8
  26. package/src/components/layout/PageActions.astro +17 -11
  27. package/src/components/layout/Search.astro +4 -1
  28. package/src/components/layout/search/types.ts +16 -5
  29. package/src/components/openapi/ParametersTable.astro +1 -1
  30. package/src/components/openapi/SchemaProperty.astro +1 -1
  31. package/src/components/openapi/SchemaTable.astro +3 -3
  32. package/src/components/openapi/helpers.ts +17 -8
  33. package/src/components/openapi/snippets.ts +17 -4
  34. package/src/core/config.ts +15 -6
  35. package/src/core/graph.ts +6 -1
  36. package/src/core/navigation.ts +5 -1
  37. package/src/core/sources/filesystem.ts +19 -1
  38. package/src/core/sources/mdx-remote.ts +20 -4
  39. package/src/core/sources/mintlify.ts +14 -28
  40. package/src/core/sources/normalize.ts +28 -6
  41. package/src/core/sources/watch.ts +44 -0
  42. package/src/markdown/code-title.ts +6 -3
  43. package/src/markdown/package-install.ts +3 -1
  44. package/src/migrate/fumadocs/content.ts +3 -5
  45. package/src/migrate/fumadocs/index.ts +24 -9
  46. package/src/migrate/mintlify/config.ts +2 -6
  47. package/src/migrate/mintlify/index.ts +119 -32
  48. package/src/migrate/mintlify/snippets.ts +17 -8
  49. package/src/migrate/nextra/index.ts +16 -1
  50. package/src/migrate/shared.ts +77 -4
  51. package/src/migrate/starlight/content.ts +3 -6
  52. package/src/og/card.ts +16 -4
  53. package/src/openapi/render-mdx.ts +10 -1
  54. package/src/search/sync/orama-cloud.ts +2 -0
  55. package/src/search/sync/typesense.ts +4 -0
  56. package/src/theme/icons.ts +13 -4
  57. package/src/theme/palette.ts +38 -17
@@ -1,4 +1,4 @@
1
- import { renameTag, rewriteCallouts } from "../shared.ts";
1
+ import { renameTag, rewriteCallouts, stripImports } from "../shared.ts";
2
2
 
3
3
  /**
4
4
  * Source-to-source rewrites that turn Starlight-only MDX into idiomatic Blume
@@ -14,11 +14,8 @@ const STARLIGHT_IMPORT =
14
14
  * Blume injects its components globally, so these imports would fail to resolve
15
15
  * once the Starlight packages are gone.
16
16
  */
17
- export const stripStarlightImports = (source: string): string => {
18
- const stripped = source.replace(STARLIGHT_IMPORT, "");
19
- // Collapse the blank gap a removed import block leaves behind.
20
- return stripped === source ? source : stripped.replaceAll(/\n{3,}/gu, "\n\n");
21
- };
17
+ export const stripStarlightImports = (source: string): string =>
18
+ stripImports(source, STARLIGHT_IMPORT);
22
19
 
23
20
  /** Starlight `<Aside type="X">` values mapped to Blume directive names. */
24
21
  const ASIDE_TYPE_DIRECTIVES: Record<string, string> = {
package/src/og/card.ts CHANGED
@@ -19,10 +19,19 @@ const ACCENT_HEX: Record<string, string> = {
19
19
  teal: "#14b8a6",
20
20
  };
21
21
 
22
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/iu;
23
+
22
24
  // OG rendering uses hex (Takumi's color parser does not accept oklch); named
23
- // presets map to hex, raw hex passes through, anything else falls back.
24
- const resolveAccent = (accent: string): string =>
25
- ACCENT_HEX[accent] ?? (accent.startsWith("#") ? accent : "#3b82f6");
25
+ // presets map to hex, well-formed hex passes through, anything else falls
26
+ // back a malformed hex (`#12345` typo) would throw inside Takumi and fail
27
+ // the build at OG prerender with an opaque native error. `hasOwn` keeps a
28
+ // preset name like "constructor" from resolving up the prototype chain.
29
+ const resolveAccent = (accent: string): string => {
30
+ if (Object.hasOwn(ACCENT_HEX, accent)) {
31
+ return ACCENT_HEX[accent] as string;
32
+ }
33
+ return HEX_COLOR.test(accent) ? accent : "#3b82f6";
34
+ };
26
35
 
27
36
  export interface OgCardOptions {
28
37
  /** Large headline — the page title. */
@@ -75,7 +84,10 @@ export const truncate = (value: string, max: number): string => {
75
84
  // stays within the lockup.
76
85
  const MARK_HEIGHT = 32;
77
86
  const MARK_MAX_WIDTH = 100;
78
- const VIEW_BOX = /viewBox="0 0 (?<w>[\d.]+) (?<h>[\d.]+)"/u;
87
+ // Accept either quote style and a non-zero min-x/min-y; only width/height
88
+ // matter for the aspect ratio. A miss falls back to a square mark.
89
+ const VIEW_BOX =
90
+ /viewBox=(?<q>["'])[\d.-]+[\s,]+[\d.-]+[\s,]+(?<w>[\d.]+)[\s,]+(?<h>[\d.]+)\k<q>/u;
79
91
 
80
92
  // Render the configured logo as the brand mark. A `currentColor` logo carries
81
93
  // no intrinsic color, so it is painted in the foreground to read on the light
@@ -21,8 +21,17 @@ const ENTITIES: Record<string, string> = {
21
21
  "{": "&#123;",
22
22
  "}": "&#125;",
23
23
  };
24
+ // MDX also parses lines starting with `import`/`export` as ESM ("import the
25
+ // SDK…" is common spec prose). Entity-escape the keyword's first letter so the
26
+ // construct can't match; it still renders as the literal word.
27
+ const MDX_ESM_KEYWORD = /^(?<keyword>import|export)\b/gmu;
24
28
  const mdxSafe = (text: string): string =>
25
- text.replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char);
29
+ text
30
+ .replace(MDX_UNSAFE, (char) => ENTITIES[char] ?? char)
31
+ .replace(
32
+ MDX_ESM_KEYWORD,
33
+ (keyword) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`
34
+ );
26
35
 
27
36
  /** Frontmatter + body for one operation or overview page. */
28
37
  export interface RenderedPage {
@@ -31,6 +31,8 @@ export const syncOramaCloud = async (
31
31
  content: record.content,
32
32
  description: record.description,
33
33
  id: record._id,
34
+ // Carried so an i18n site can filter hosted results per language.
35
+ locale: record.locale,
34
36
  tag: record.tag,
35
37
  title: record.title,
36
38
  url: record.url,
@@ -54,6 +54,9 @@ export const syncTypesense = async (
54
54
  { name: "content", type: "string" },
55
55
  { name: "url", type: "string" },
56
56
  { facet: true, name: "tag", optional: true, type: "string" },
57
+ // Carried as a facet so an i18n site can filter hosted results per
58
+ // language (the SearchRecord contract).
59
+ { facet: true, name: "locale", optional: true, type: "string" },
57
60
  ],
58
61
  name: config.collection,
59
62
  });
@@ -62,6 +65,7 @@ export const syncTypesense = async (
62
65
  content: record.content,
63
66
  description: record.description,
64
67
  id: record._id,
68
+ locale: record.locale,
65
69
  tag: record.tag,
66
70
  title: record.title,
67
71
  url: record.url,
@@ -75,6 +75,15 @@ const PREFIX_SETS: Record<string, string> = {
75
75
  ti: "tabler",
76
76
  };
77
77
 
78
+ /**
79
+ * Own-property map lookup. Icon names and library/iconType hints come from
80
+ * content and config, so a value like `constructor:x` would otherwise resolve
81
+ * an Object.prototype member (a function) and crash resolution deep in the
82
+ * build with no pointer to the offending page.
83
+ */
84
+ const ownEntry = <T>(map: Record<string, T>, key: string): T | undefined =>
85
+ Object.hasOwn(map, key) ? map[key] : undefined;
86
+
78
87
  export interface ResolvedIcon {
79
88
  /** Inner SVG markup (self-styled: carries its own fill/stroke). */
80
89
  body: string;
@@ -100,13 +109,13 @@ const normalize = (name: string): string =>
100
109
  /** Which set a bare name resolves against, given library/iconType hints. */
101
110
  const setFor = (options: ResolveIconOptions): string => {
102
111
  if (options.iconType) {
103
- const set = ICON_TYPE_SETS[normalize(options.iconType)];
112
+ const set = ownEntry(ICON_TYPE_SETS, normalize(options.iconType));
104
113
  if (set) {
105
114
  return set;
106
115
  }
107
116
  }
108
117
  if (options.library) {
109
- const set = LIBRARY_SETS[normalize(options.library)];
118
+ const set = ownEntry(LIBRARY_SETS, normalize(options.library));
110
119
  if (set) {
111
120
  return set;
112
121
  }
@@ -115,7 +124,7 @@ const setFor = (options: ResolveIconOptions): string => {
115
124
  };
116
125
 
117
126
  const fromSet = (setName: string, iconName: string): ResolvedIcon | null => {
118
- const set = SETS[setName];
127
+ const set = ownEntry(SETS, setName);
119
128
  const data = set && getIconData(set, iconName);
120
129
  if (!data) {
121
130
  return null;
@@ -146,7 +155,7 @@ export const resolveIcon = (
146
155
  const normalized = normalize(name);
147
156
  const colon = normalized.indexOf(":");
148
157
  if (colon > 0) {
149
- const setName = PREFIX_SETS[normalized.slice(0, colon)];
158
+ const setName = ownEntry(PREFIX_SETS, normalized.slice(0, colon));
150
159
  if (setName) {
151
160
  return resolveInSet(setName, normalized.slice(colon + 1));
152
161
  }
@@ -22,6 +22,17 @@ const CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
22
22
  const safeColor = (value: string, fallback: string): string =>
23
23
  CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
24
24
 
25
+ /**
26
+ * Resolve a named preset or fall back to {@link safeColor}. `hasOwn` keeps a
27
+ * value like "constructor" from resolving an Object.prototype member — which
28
+ * would stringify a function into the generated CSS, breaking the rule (the
29
+ * exact breakout safeColor exists to prevent).
30
+ */
31
+ const presetOrColor = (value: string): string =>
32
+ Object.hasOwn(ACCENTS, value)
33
+ ? (ACCENTS[value] as string)
34
+ : safeColor(value, FALLBACK_ACCENT);
35
+
25
36
  /** Like {@link safeColor} but drops an unsafe/absent value to `null`. */
26
37
  const safeColorOrNull = (value: string | undefined): string | null =>
27
38
  value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
@@ -94,10 +105,24 @@ const themeRootCss = (
94
105
 
95
106
  const themeDarkCss = (
96
107
  theme: ResolvedConfig["theme"],
97
- accentDark: string | null
108
+ options: {
109
+ accent: string;
110
+ action: string | null;
111
+ backgroundDecoration: string;
112
+ }
98
113
  ): string => {
114
+ // Mode-shared tokens (accent, action, decoration) must be re-declared here:
115
+ // the base stylesheet's own `:root[data-theme="dark"]` block outranks the
116
+ // `:root` config tokens on specificity, so without this block dark mode
117
+ // would silently keep its neutral defaults and ignore the config.
99
118
  const tokens = [
100
- ...cssToken("--blume-accent", accentDark),
119
+ ` --blume-accent: ${options.accent};`,
120
+ " --blume-accent-foreground: oklch(1 0 0);",
121
+ ...cssToken("--blume-action", options.action),
122
+ ...cssToken(
123
+ "--blume-action-foreground",
124
+ options.action ? "oklch(1 0 0)" : null
125
+ ),
101
126
  ...cssToken("--blume-background", safeColorOrNull(theme.backgroundDark)),
102
127
  ...cssToken(
103
128
  "--blume-background-image",
@@ -105,10 +130,8 @@ const themeDarkCss = (
105
130
  ? backgroundImageCss(theme.backgroundImageDark)
106
131
  : null
107
132
  ),
108
- ];
109
- if (tokens.length === 0) {
110
- return "";
111
- }
133
+ options.backgroundDecoration.trimEnd(),
134
+ ].filter(Boolean);
112
135
  return `:root[data-theme="dark"] {
113
136
  ${tokens.join("\n")}
114
137
  }
@@ -121,7 +144,7 @@ ${tokens.join("\n")}
121
144
  * arbitrary colors without a config change.
122
145
  */
123
146
  export const resolveAccent = (theme: ResolvedConfig["theme"]): string =>
124
- ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
147
+ presetOrColor(theme.accent);
125
148
 
126
149
  /** Resolve the configured radius preset to a CSS length. */
127
150
  export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
@@ -133,15 +156,9 @@ export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
133
156
  * arbitrary colors without a config change.
134
157
  */
135
158
  export const buildThemeCss = (theme: ResolvedConfig["theme"]): string => {
136
- const accent =
137
- ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
138
- const accentDark = theme.accentDark
139
- ? (ACCENTS[theme.accentDark] ??
140
- safeColor(theme.accentDark, FALLBACK_ACCENT))
141
- : null;
142
- const action = theme.action
143
- ? (ACCENTS[theme.action] ?? safeColor(theme.action, FALLBACK_ACCENT))
144
- : null;
159
+ const accent = presetOrColor(theme.accent);
160
+ const accentDark = theme.accentDark ? presetOrColor(theme.accentDark) : null;
161
+ const action = theme.action ? presetOrColor(theme.action) : null;
145
162
  const backgroundDecoration = backgroundDecorationCss(
146
163
  theme.backgroundDecoration
147
164
  );
@@ -152,7 +169,11 @@ export const buildThemeCss = (theme: ResolvedConfig["theme"]): string => {
152
169
  backgroundDecoration,
153
170
  radius,
154
171
  });
155
- const dark = themeDarkCss(theme, accentDark);
172
+ const dark = themeDarkCss(theme, {
173
+ accent: accentDark ?? accent,
174
+ action,
175
+ backgroundDecoration,
176
+ });
156
177
 
157
178
  return `/* Generated by Blume from theme config. */
158
179
  :root {