blume 1.6.5 → 1.7.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 +39 -0
- package/bin/blume.mjs +3 -2
- package/dist/cli/chunk-0qhq7b8q.js +111 -0
- package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
- package/dist/cli/chunk-18tjv4f7.js +96 -0
- package/dist/cli/chunk-18tjv4f7.js.map +10 -0
- package/dist/cli/chunk-27gtm2ym.js +69 -0
- package/dist/cli/chunk-27gtm2ym.js.map +11 -0
- package/dist/cli/chunk-2aj8ddew.js +72 -0
- package/dist/cli/chunk-2aj8ddew.js.map +10 -0
- package/dist/cli/chunk-3r94j3tc.js +221 -0
- package/dist/cli/chunk-3r94j3tc.js.map +10 -0
- package/dist/cli/chunk-4trphnvy.js +102 -0
- package/dist/cli/chunk-4trphnvy.js.map +11 -0
- package/dist/cli/chunk-4xyggvgf.js +21 -0
- package/dist/cli/chunk-4xyggvgf.js.map +10 -0
- package/dist/cli/chunk-5d4q7121.js +4064 -0
- package/dist/cli/chunk-5d4q7121.js.map +40 -0
- package/dist/cli/chunk-5hs6gb7n.js +32 -0
- package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
- package/dist/cli/chunk-6kzzpsx8.js +26 -0
- package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
- package/dist/cli/chunk-8gnpdsn1.js +952 -0
- package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
- package/dist/cli/chunk-9qs6acpw.js +176 -0
- package/dist/cli/chunk-9qs6acpw.js.map +10 -0
- package/dist/cli/chunk-agy5rzxy.js +2453 -0
- package/dist/cli/chunk-agy5rzxy.js.map +15 -0
- package/dist/cli/chunk-bcy492zc.js +16 -0
- package/dist/cli/chunk-bcy492zc.js.map +10 -0
- package/dist/cli/chunk-btfr9yvw.js +41 -0
- package/dist/cli/chunk-btfr9yvw.js.map +10 -0
- package/dist/cli/chunk-cbjnx4s8.js +73 -0
- package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
- package/dist/cli/chunk-cfw6x4rm.js +1967 -0
- package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
- package/dist/cli/chunk-ckh3a410.js +277 -0
- package/dist/cli/chunk-ckh3a410.js.map +11 -0
- package/dist/cli/chunk-drke6t0h.js +259 -0
- package/dist/cli/chunk-drke6t0h.js.map +11 -0
- package/dist/cli/chunk-ev67ycx0.js +15 -0
- package/dist/cli/chunk-ev67ycx0.js.map +10 -0
- package/dist/cli/chunk-ey89bjj1.js +209 -0
- package/dist/cli/chunk-ey89bjj1.js.map +11 -0
- package/dist/cli/chunk-j6pxe0dt.js +69 -0
- package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
- package/dist/cli/chunk-jk1zwka1.js +387 -0
- package/dist/cli/chunk-jk1zwka1.js.map +12 -0
- package/dist/cli/chunk-jtb45atp.js +467 -0
- package/dist/cli/chunk-jtb45atp.js.map +14 -0
- package/dist/cli/chunk-jxkxjsc1.js +76 -0
- package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
- package/dist/cli/chunk-kwx90v78.js +81 -0
- package/dist/cli/chunk-kwx90v78.js.map +10 -0
- package/dist/cli/chunk-n0nyat6g.js +30 -0
- package/dist/cli/chunk-n0nyat6g.js.map +10 -0
- package/dist/cli/chunk-pxj10x8y.js +35 -0
- package/dist/cli/chunk-pxj10x8y.js.map +10 -0
- package/dist/cli/chunk-qq9nm3qd.js +1141 -0
- package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
- package/dist/cli/chunk-s102bysw.js +5170 -0
- package/dist/cli/chunk-s102bysw.js.map +47 -0
- package/dist/cli/chunk-s5dsk8bj.js +769 -0
- package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
- package/dist/cli/chunk-s5e5jt53.js +227 -0
- package/dist/cli/chunk-s5e5jt53.js.map +11 -0
- package/dist/cli/chunk-sbdqrjbb.js +81 -0
- package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
- package/dist/cli/chunk-tnskyrej.js +117 -0
- package/dist/cli/chunk-tnskyrej.js.map +10 -0
- package/dist/cli/chunk-v2ymm99c.js +1016 -0
- package/dist/cli/chunk-v2ymm99c.js.map +13 -0
- package/dist/cli/chunk-v5mm027v.js +185 -0
- package/dist/cli/chunk-v5mm027v.js.map +11 -0
- package/dist/cli/chunk-vt8fgygt.js +23 -0
- package/dist/cli/chunk-vt8fgygt.js.map +10 -0
- package/dist/cli/chunk-vxv4x1n8.js +17 -0
- package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
- package/dist/cli/chunk-wd27zjcz.js +60 -0
- package/dist/cli/chunk-wd27zjcz.js.map +10 -0
- package/dist/cli/chunk-x66c5yjn.js +23 -0
- package/dist/cli/chunk-x66c5yjn.js.map +10 -0
- package/dist/cli/chunk-xv91q4nm.js +5314 -0
- package/dist/cli/chunk-xv91q4nm.js.map +58 -0
- package/dist/cli/chunk-y3g15rvv.js +679 -0
- package/dist/cli/chunk-y3g15rvv.js.map +15 -0
- package/dist/cli/chunk-ye9zdkgv.js +136 -0
- package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
- package/dist/cli/chunk-ynacq3ev.js +1062 -0
- package/dist/cli/chunk-ynacq3ev.js.map +25 -0
- package/dist/cli/chunk-zr3ygrq3.js +54 -0
- package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
- package/dist/cli/index.js +55 -27597
- package/dist/cli/index.js.map +5 -243
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/components/layout/nav-utils.d.ts +33 -1
- package/dist/types/core/code-fences.d.ts +11 -0
- package/dist/types/core/package-root.d.ts +1 -1
- package/dist/types/core/schema.d.ts +70 -0
- package/dist/types/theme/fonts.d.ts +22 -22
- package/docs/02-deployment.mdx +22 -1
- package/docs/configuration/ask-ai.mdx +1 -1
- package/docs/configuration/customization.mdx +2 -9
- package/docs/content/navigation.mdx +2 -0
- package/docs/content/syntax.mdx +1 -1
- package/docs/discoverability/open-graph.mdx +4 -0
- package/docs/reference/cli.mdx +1 -1
- package/package.json +4 -2
- package/src/ai/api/handlers.ts +4 -7
- package/src/ai/api/paths.ts +8 -0
- package/src/ai/api/spec.ts +2 -1
- package/src/ai/ask-context.ts +378 -22
- package/src/astro/generate.ts +161 -28
- package/src/astro/include-hmr.ts +10 -13
- package/src/astro/include-refresh.ts +0 -0
- package/src/astro/index.ts +6 -1
- package/src/astro/integration.ts +280 -53
- package/src/astro/module-types.ts +83 -0
- package/src/astro/templates.ts +256 -108
- package/src/audit/image-size.ts +10 -8
- package/src/cli/command-meta.ts +77 -0
- package/src/cli/commands/add.ts +2 -4
- package/src/cli/commands/audit.ts +2 -4
- package/src/cli/commands/build.ts +70 -346
- package/src/cli/commands/check.ts +2 -4
- package/src/cli/commands/dev.ts +31 -42
- package/src/cli/commands/doctor.ts +2 -4
- package/src/cli/commands/eject.ts +3 -41
- package/src/cli/commands/eval.ts +2 -5
- package/src/cli/commands/init.ts +2 -4
- package/src/cli/commands/mcp-stdio.ts +2 -5
- package/src/cli/commands/preview.ts +3 -5
- package/src/cli/commands/sync.ts +2 -4
- package/src/cli/commands/translate.ts +2 -5
- package/src/cli/commands/validate.ts +2 -4
- package/src/cli/commands/version.ts +2 -4
- package/src/cli/eject-scripts.ts +0 -45
- package/src/cli/host-args.ts +16 -0
- package/src/cli/index.ts +84 -35
- package/src/cli/lazy-command.ts +47 -0
- package/src/components/Icon.astro +24 -0
- package/src/components/content/GithubInfo.astro +4 -1
- package/src/components/icon-sprite-middleware.ts +41 -0
- package/src/components/icon-sprite.ts +93 -0
- package/src/components/layout/IconSprite.astro +11 -0
- package/src/components/layout/NavTree.astro +156 -188
- package/src/components/layout/NavTreeCache.astro +45 -0
- package/src/components/layout/NavTreeScript.astro +256 -0
- package/src/components/layout/PageActions.astro +11 -5
- package/src/components/layout/PageLayout.astro +21 -3
- package/src/components/layout/ReferenceLayout.astro +21 -4
- package/src/components/layout/RootLayout.astro +44 -6
- package/src/components/layout/nav-cache.ts +49 -0
- package/src/components/layout/nav-utils.ts +69 -1
- package/src/components/layout/page-locale.ts +29 -0
- package/src/core/api-name.ts +18 -0
- package/src/core/code-fences.ts +48 -0
- package/src/core/content-assets.ts +3 -7
- package/src/core/includes.ts +3 -7
- package/src/core/package-root.ts +1 -1
- package/src/core/schema.ts +19 -0
- package/src/core/sources/normalize.ts +2 -37
- package/src/core/sources/obsidian.ts +3 -2
- package/src/core/svg-dimensions.ts +97 -0
- package/src/core/version-cut.ts +2 -2
- package/src/deploy/artifacts.ts +370 -0
- package/src/deploy/cloudflare-negotiation.ts +97 -32
- package/src/deploy/function-bundle.ts +66 -20
- package/src/deploy/sitemap.ts +6 -0
- package/src/deploy/vercel-negotiation.ts +8 -30
- package/src/markdown/language-icon.ts +64 -20
- package/src/markdown/mermaid.ts +11 -0
- package/src/og/cache.ts +236 -0
- package/src/og/card.ts +18 -16
- package/src/og/index.ts +8 -1
- package/src/openapi/render-mdx.ts +9 -5
- package/src/registry/eject.ts +23 -10
- package/src/theme/entry.ts +41 -7
- package/src/theme/fonts.ts +30 -23
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRootTab, isUnderPath } from "../../core/navigation.ts";
|
|
2
|
-
import type { NavNode, NavTab } from "../../core/types.ts";
|
|
2
|
+
import type { NavNode, NavTab, Navigation } from "../../core/types.ts";
|
|
3
3
|
|
|
4
4
|
/** A flat, ordered page reference used for previous/next pagination. */
|
|
5
5
|
export interface FlatPage {
|
|
@@ -238,3 +238,71 @@ export const getPagination = (flat: FlatPage[], route: string) => {
|
|
|
238
238
|
prev: index > 0 ? (flat[index - 1] ?? null) : null,
|
|
239
239
|
};
|
|
240
240
|
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A stable id for every group in a sidebar — `g<n>` by pre-order position in
|
|
244
|
+
* the full tree. The layout hands `NavTree` a scoped view of that tree (a
|
|
245
|
+
* tab's section, or the sidebar minus the tab sections), so positions within
|
|
246
|
+
* the rendered slice differ from page to page; these ids name the same group
|
|
247
|
+
* everywhere, which the drill-in panels and the deferred-section fragments
|
|
248
|
+
* (`/blume-nav/…`) rely on. Keyed by node identity: the scoped views reuse
|
|
249
|
+
* the full tree's node objects.
|
|
250
|
+
*/
|
|
251
|
+
export const navGroupIds = (sidebar: NavNode[]): Map<NavNode, string> => {
|
|
252
|
+
const ids = new Map<NavNode, string>();
|
|
253
|
+
const walk = (nodes: NavNode[]): void => {
|
|
254
|
+
for (const node of nodes) {
|
|
255
|
+
if (node.kind === "group") {
|
|
256
|
+
ids.set(node, `g${ids.size}`);
|
|
257
|
+
walk(node.children);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
walk(sidebar);
|
|
262
|
+
return ids;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/** Whether any group in a sidebar renders as a disclosure or a drill-in panel. */
|
|
266
|
+
export const hasDeferrableGroups = (sidebar: NavNode[]): boolean =>
|
|
267
|
+
sidebar.some(
|
|
268
|
+
(node) =>
|
|
269
|
+
node.kind === "group" &&
|
|
270
|
+
((node.display ?? "flat") !== "flat" ||
|
|
271
|
+
hasDeferrableGroups(node.children))
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
/** One of the navigation trees a site renders, by URL segment. */
|
|
275
|
+
export interface NavVariant {
|
|
276
|
+
/** `current`, or an archived version id. */
|
|
277
|
+
version: string;
|
|
278
|
+
/** `default`, or a locale code. */
|
|
279
|
+
locale: string;
|
|
280
|
+
navigation: Navigation;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Every navigation tree the runtime data holds — the default, each locale's,
|
|
285
|
+
* and each archived version's per locale — keyed the way the deferred
|
|
286
|
+
* sidebar fragments' URLs are (`/blume-nav/<version>/<locale>/…`). An
|
|
287
|
+
* unlocalized version tree is keyed by `""` in the data; it maps to
|
|
288
|
+
* `default` here.
|
|
289
|
+
*/
|
|
290
|
+
export const navVariants = (data: {
|
|
291
|
+
navigation: Navigation;
|
|
292
|
+
navigationByLocale: Record<string, Navigation>;
|
|
293
|
+
navigationByVersion: Record<string, Record<string, Navigation>>;
|
|
294
|
+
}): NavVariant[] => [
|
|
295
|
+
{ locale: "default", navigation: data.navigation, version: "current" },
|
|
296
|
+
...Object.entries(data.navigationByLocale).map(([locale, navigation]) => ({
|
|
297
|
+
locale,
|
|
298
|
+
navigation,
|
|
299
|
+
version: "current",
|
|
300
|
+
})),
|
|
301
|
+
...Object.entries(data.navigationByVersion).flatMap(([version, byLocale]) =>
|
|
302
|
+
Object.entries(byLocale).map(([locale, navigation]) => ({
|
|
303
|
+
locale: locale || "default",
|
|
304
|
+
navigation,
|
|
305
|
+
version,
|
|
306
|
+
}))
|
|
307
|
+
),
|
|
308
|
+
];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale defaults for the document shells (`RootLayout`, `PageLayout`,
|
|
3
|
+
* `ReferenceLayout`) when the page doesn't pass `locale`/`dir` itself.
|
|
4
|
+
*
|
|
5
|
+
* The content catch-all always passes both, resolved from the route manifest
|
|
6
|
+
* (which also knows about fallback pages). Custom `.astro` pages, the 404
|
|
7
|
+
* page, and the reference shell don't have that data, so they fall back to
|
|
8
|
+
* what Astro's own i18n routing resolved for the request — `Astro.currentLocale`
|
|
9
|
+
* — which the generated config enables with Blume's locale list. Before this,
|
|
10
|
+
* a custom page under `/fr/` rendered `<html lang="en">` unless it threaded
|
|
11
|
+
* the locale through by hand.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The page's locale: the caller's explicit value, else the one Astro resolved
|
|
16
|
+
* from the URL, else the site's default locale, else `en` (no i18n).
|
|
17
|
+
*/
|
|
18
|
+
export const pageLocale = (
|
|
19
|
+
i18n: { defaultLocale: string } | null,
|
|
20
|
+
explicit?: string,
|
|
21
|
+
current?: string
|
|
22
|
+
): string => explicit ?? current ?? i18n?.defaultLocale ?? "en";
|
|
23
|
+
|
|
24
|
+
/** The configured text direction of a locale; `ltr` when unknown. */
|
|
25
|
+
export const pageDirection = (
|
|
26
|
+
i18n: { locales: { code: string; dir: "ltr" | "rtl" }[] } | null,
|
|
27
|
+
locale: string
|
|
28
|
+
): "ltr" | "rtl" =>
|
|
29
|
+
i18n?.locales.find((entry) => entry.code === locale)?.dir ?? "ltr";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a name already reads as an API's name: it contains `API` or `APIs`
|
|
3
|
+
* as a whole word, anywhere. Word-bounded on both sides, so "OpenAPI" and
|
|
4
|
+
* "AsyncAPI" are product names rather than API names and still take the
|
|
5
|
+
* suffix; unanchored at the end, so "Petstore API v2" is left alone too.
|
|
6
|
+
*/
|
|
7
|
+
const NAMES_AN_API = /\bAPIs?\b/iu;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `name` as the phrase "the ___" in generated prose — the spec title or
|
|
11
|
+
* reference label, followed by ` API` unless it already says so. Keeps
|
|
12
|
+
* "Petstore" reading as "Petstore API" without turning "Petstore API" into
|
|
13
|
+
* "Petstore API API".
|
|
14
|
+
*/
|
|
15
|
+
export const apiNamePhrase = (name: string): string => {
|
|
16
|
+
const trimmed = name.trim();
|
|
17
|
+
return NAMES_AN_API.test(trimmed) ? trimmed : `${trimmed} API`;
|
|
18
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenced-code state for line scanners. Kept free of imports so request-time
|
|
3
|
+
* code (the Ask AI grounding excerpts) can share it with the build-time source
|
|
4
|
+
* scanners without pulling the filesystem and schema modules along.
|
|
5
|
+
*
|
|
6
|
+
* CommonMark allows backtick *and* tilde fences, three or more characters
|
|
7
|
+
* long. The scanners track which delimiter opened the current fence and how
|
|
8
|
+
* long its run was (`null` when outside one), so a ``` line inside a ~~~
|
|
9
|
+
* block — or inside a ````-delimited block (the wrapper `codeBlockLines`
|
|
10
|
+
* emits around code that contains its own ``` fence) — is content, not a
|
|
11
|
+
* toggle.
|
|
12
|
+
*/
|
|
13
|
+
const CODE_FENCE = /^(?<run>`{3,}|~{3,})/u;
|
|
14
|
+
|
|
15
|
+
/** The open fence's delimiter char and run length, or null outside one. */
|
|
16
|
+
export type FenceState = { delimiter: "`" | "~"; length: number } | null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Advance the fenced-code state for one line: an opening fence records its
|
|
20
|
+
* delimiter and run length, only a bare run of the same character at least as
|
|
21
|
+
* long closes it (CommonMark), and any other line leaves the state untouched.
|
|
22
|
+
*/
|
|
23
|
+
export const nextFenceState = (line: string, fence: FenceState): FenceState => {
|
|
24
|
+
const trimmed = line.trimStart();
|
|
25
|
+
const run = trimmed.match(CODE_FENCE)?.groups?.run;
|
|
26
|
+
if (run === undefined) {
|
|
27
|
+
return fence;
|
|
28
|
+
}
|
|
29
|
+
const rest = trimmed.slice(run.length);
|
|
30
|
+
const delimiter = run.startsWith("`") ? ("`" as const) : ("~" as const);
|
|
31
|
+
if (fence === null) {
|
|
32
|
+
// A backtick fence's info string cannot itself contain a backtick
|
|
33
|
+
// (CommonMark) — a line-leading ```inline``` span is a paragraph, and
|
|
34
|
+
// opening a phantom fence on it would swallow every heading and link
|
|
35
|
+
// after it. Tilde fences carry no such rule.
|
|
36
|
+
if (delimiter === "`" && rest.includes("`")) {
|
|
37
|
+
return fence;
|
|
38
|
+
}
|
|
39
|
+
return { delimiter, length: run.length };
|
|
40
|
+
}
|
|
41
|
+
// A closing fence may be followed only by whitespace: a same-length
|
|
42
|
+
// ```js line inside an open ``` block is content, not a closer.
|
|
43
|
+
return fence.delimiter === delimiter &&
|
|
44
|
+
run.length >= fence.length &&
|
|
45
|
+
rest.trim() === ""
|
|
46
|
+
? null
|
|
47
|
+
: fence;
|
|
48
|
+
};
|
|
@@ -11,14 +11,10 @@ import {
|
|
|
11
11
|
} from "pathe";
|
|
12
12
|
|
|
13
13
|
import { normalizeBasePath } from "./base-path.ts";
|
|
14
|
+
import { nextFenceState } from "./code-fences.ts";
|
|
15
|
+
import type { FenceState } from "./code-fences.ts";
|
|
14
16
|
import { hashText } from "./sources/cache.ts";
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
INLINE_CODE,
|
|
18
|
-
MD_IMAGE,
|
|
19
|
-
nextFenceState,
|
|
20
|
-
targetOffsetIn,
|
|
21
|
-
} from "./sources/normalize.ts";
|
|
17
|
+
import { INLINE_CODE, MD_IMAGE, targetOffsetIn } from "./sources/normalize.ts";
|
|
22
18
|
import { readExpandedEntryText } from "./sources/read.ts";
|
|
23
19
|
import type { ContentSource } from "./sources/types.ts";
|
|
24
20
|
import type { PageRecord } from "./types.ts";
|
package/src/core/includes.ts
CHANGED
|
@@ -2,14 +2,10 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { dirname, extname, join, relative, resolve } from "pathe";
|
|
4
4
|
|
|
5
|
+
import { nextFenceState } from "./code-fences.ts";
|
|
6
|
+
import type { FenceState } from "./code-fences.ts";
|
|
5
7
|
import matter from "./frontmatter.ts";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
INLINE_CODE,
|
|
9
|
-
MD_IMAGE,
|
|
10
|
-
nextFenceState,
|
|
11
|
-
targetOffsetIn,
|
|
12
|
-
} from "./sources/normalize.ts";
|
|
8
|
+
import { INLINE_CODE, MD_IMAGE, targetOffsetIn } from "./sources/normalize.ts";
|
|
13
9
|
import type { Diagnostic } from "./types.ts";
|
|
14
10
|
|
|
15
11
|
/**
|
package/src/core/package-root.ts
CHANGED
|
@@ -28,7 +28,7 @@ let cached: string | undefined;
|
|
|
28
28
|
* Anchoring here — rather than at a fixed offset from `import.meta` — keeps the
|
|
29
29
|
* package's own `src/`, assets, and `node_modules` locatable whether the code
|
|
30
30
|
* runs from source under Bun (`src/...`) or from the published, bundled CLI
|
|
31
|
-
* (`dist/cli
|
|
31
|
+
* (`dist/cli/*.js`). The two layouts sit at different depths, so a relative
|
|
32
32
|
* `../..` resolves to different places; locating `package.json` does not.
|
|
33
33
|
*/
|
|
34
34
|
export const packageRoot = (): string => {
|
package/src/core/schema.ts
CHANGED
|
@@ -176,6 +176,25 @@ export const pageMetaSchema = pageMetaBaseSchema;
|
|
|
176
176
|
export type PageMeta = z.infer<typeof pageMetaBaseSchema>;
|
|
177
177
|
export type PageMetaInput = z.input<typeof pageMetaBaseSchema>;
|
|
178
178
|
|
|
179
|
+
/** The fully-defaulted front matter of a page that declares nothing. */
|
|
180
|
+
const EMPTY_PAGE_META: PageMeta = pageMetaBaseSchema.parse({});
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The page schema as the generated content collections declare it, so
|
|
184
|
+
* `entry.data` is typed and normalized (dates as ISO strings, X handles with
|
|
185
|
+
* their `@`) the same way the scan's `PageMeta` is. Two deliberate loosenings
|
|
186
|
+
* over {@link pageMetaSchema}: custom keys (`frontmatter.extend`, per-type
|
|
187
|
+
* maps) pass through instead of failing the strict parse, and a page the
|
|
188
|
+
* strict parse rejects resolves to the empty defaults instead of throwing.
|
|
189
|
+
* The scan has already dropped such a page with a `BLUME_FRONTMATTER_INVALID`
|
|
190
|
+
* diagnostic — Blume continues without it unless `--strict` — so the
|
|
191
|
+
* collection must not turn that dropped page into a failed content sync.
|
|
192
|
+
*/
|
|
193
|
+
export const pageCollectionSchema = pageMetaBaseSchema
|
|
194
|
+
.loose()
|
|
195
|
+
// oxlint-disable-next-line promise/prefer-await-to-then -- zod's catch, not a promise
|
|
196
|
+
.catch(() => EMPTY_PAGE_META);
|
|
197
|
+
|
|
179
198
|
/** Built-in page frontmatter keys; custom keys must never redeclare one. */
|
|
180
199
|
const BUILT_IN_PAGE_META_KEYS = new Set<string>(
|
|
181
200
|
pageMetaBaseSchema.keyof().options
|
|
@@ -4,6 +4,8 @@ import GithubSlugger from "github-slugger";
|
|
|
4
4
|
import { extname } from "pathe";
|
|
5
5
|
|
|
6
6
|
import { withBasePath } from "../base-path.ts";
|
|
7
|
+
import { nextFenceState } from "../code-fences.ts";
|
|
8
|
+
import type { FenceState } from "../code-fences.ts";
|
|
7
9
|
import { diagnosticsFromIssues, diagnosticsFromZod } from "../diagnostics.ts";
|
|
8
10
|
import { occupySlug, parseHeadingMarkers } from "../heading-markers.ts";
|
|
9
11
|
import { localePlacement, localizeRoute } from "../i18n.ts";
|
|
@@ -139,43 +141,6 @@ const mapRoute = (relativePath: string): MappedRoute => {
|
|
|
139
141
|
return { groups, route, segments };
|
|
140
142
|
};
|
|
141
143
|
|
|
142
|
-
// CommonMark allows backtick *and* tilde fences, three or more characters
|
|
143
|
-
// long. The scanners track which delimiter opened the current fence and how
|
|
144
|
-
// long its run was (`null` when outside one), so a ``` line inside a ~~~
|
|
145
|
-
// block — or inside a ````-delimited block (the wrapper `codeBlockLines`
|
|
146
|
-
// emits around code that contains its own ``` fence) — is content, not a
|
|
147
|
-
// toggle. See `nextFenceState`.
|
|
148
|
-
const CODE_FENCE = /^(?<run>`{3,}|~{3,})/u;
|
|
149
|
-
|
|
150
|
-
/** The open fence's delimiter char and run length, or null outside one. */
|
|
151
|
-
export type FenceState = { delimiter: "`" | "~"; length: number } | null;
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Advance the fenced-code state for one line: an opening fence records its
|
|
155
|
-
* delimiter and run length, only a run of the same character at least as long
|
|
156
|
-
* closes it (CommonMark), and any other line leaves the state untouched.
|
|
157
|
-
*/
|
|
158
|
-
export const nextFenceState = (line: string, fence: FenceState): FenceState => {
|
|
159
|
-
const trimmed = line.trimStart();
|
|
160
|
-
const run = trimmed.match(CODE_FENCE)?.groups?.run;
|
|
161
|
-
if (run === undefined) {
|
|
162
|
-
return fence;
|
|
163
|
-
}
|
|
164
|
-
const delimiter = run.startsWith("`") ? ("`" as const) : ("~" as const);
|
|
165
|
-
if (fence === null) {
|
|
166
|
-
// A backtick fence's info string cannot itself contain a backtick
|
|
167
|
-
// (CommonMark) — a line-leading ```inline``` span is a paragraph, and
|
|
168
|
-
// opening a phantom fence on it would swallow every heading and link
|
|
169
|
-
// after it. Tilde fences carry no such rule.
|
|
170
|
-
if (delimiter === "`" && trimmed.slice(run.length).includes("`")) {
|
|
171
|
-
return fence;
|
|
172
|
-
}
|
|
173
|
-
return { delimiter, length: run.length };
|
|
174
|
-
}
|
|
175
|
-
return fence.delimiter === delimiter && run.length >= fence.length
|
|
176
|
-
? null
|
|
177
|
-
: fence;
|
|
178
|
-
};
|
|
179
144
|
// A closing hash sequence must be preceded by whitespace (CommonMark), so a
|
|
180
145
|
// heading like `## What is C#` keeps its trailing `#`. Up to 3 leading spaces
|
|
181
146
|
// are allowed; 4+ is an indented code block.
|
|
@@ -4,18 +4,19 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
4
4
|
|
|
5
5
|
import { basename, isAbsolute, join, relative, resolve } from "pathe";
|
|
6
6
|
|
|
7
|
+
import { nextFenceState } from "../code-fences.ts";
|
|
8
|
+
import type { FenceState } from "../code-fences.ts";
|
|
7
9
|
import { BlumeError } from "../diagnostics.ts";
|
|
8
10
|
import matter from "../frontmatter.ts";
|
|
9
11
|
import { pageMetaSchema } from "../schema.ts";
|
|
10
12
|
import type { ResolvedI18nConfig, ResolvedVersionsConfig } from "../schema.ts";
|
|
11
13
|
import type { Diagnostic } from "../types.ts";
|
|
12
14
|
import { hashText } from "./cache.ts";
|
|
13
|
-
import type { EntryPlacement
|
|
15
|
+
import type { EntryPlacement } from "./normalize.ts";
|
|
14
16
|
import {
|
|
15
17
|
extractHeadings,
|
|
16
18
|
isStringValue,
|
|
17
19
|
localizedRoute,
|
|
18
|
-
nextFenceState,
|
|
19
20
|
placeEntryRef,
|
|
20
21
|
resolveEntryRoute,
|
|
21
22
|
slugifyPath,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixel dimensions declared by an SVG document's root element.
|
|
3
|
+
*
|
|
4
|
+
* Blume needs an SVG's size in two places — the header logo's reserved
|
|
5
|
+
* `<img>` box and the OG card's brand mark — and both read it through this
|
|
6
|
+
* one parser so the header and the card can't disagree about one logo. It
|
|
7
|
+
* inspects the root `<svg …>` tag only: explicit `width`/`height` win, a
|
|
8
|
+
* `viewBox` fills in whatever is missing (scaled to a lone explicit length
|
|
9
|
+
* when there is one), and lengths carry CSS units at 96dpi. Percentages have
|
|
10
|
+
* no pixel meaning and are ignored, so a percent-sized SVG measures by its
|
|
11
|
+
* viewBox alone.
|
|
12
|
+
*/
|
|
13
|
+
export interface SvgDimensions {
|
|
14
|
+
height: number;
|
|
15
|
+
width: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The root tag, tolerating `>` inside a quoted attribute value. */
|
|
19
|
+
const ROOT = /<svg\s(?:[^>"']|"[^"]*"|'[^']*')*>/u;
|
|
20
|
+
const WIDTH = /\swidth=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
|
|
21
|
+
const HEIGHT = /\sheight=(?<quote>['"])(?<value>[^%]+?)\k<quote>/u;
|
|
22
|
+
const VIEWBOX = /\sviewBox=(?<quote>['"])(?<value>.+?)\k<quote>/iu;
|
|
23
|
+
|
|
24
|
+
/** Pixels per CSS unit at 96dpi; an omitted unit means pixels. */
|
|
25
|
+
const INCH_CM = 2.54;
|
|
26
|
+
const UNITS = new Map([
|
|
27
|
+
["cm", 96 / INCH_CM],
|
|
28
|
+
["em", 16],
|
|
29
|
+
["ex", 8],
|
|
30
|
+
["in", 96],
|
|
31
|
+
["m", (96 / INCH_CM) * 100],
|
|
32
|
+
["mm", 96 / INCH_CM / 10],
|
|
33
|
+
["pc", (96 / 72) * 12],
|
|
34
|
+
["pt", 96 / 72],
|
|
35
|
+
["px", 1],
|
|
36
|
+
]);
|
|
37
|
+
const LENGTH =
|
|
38
|
+
/^(?<number>[0-9.]+(?:e\d+)?)(?<unit>in|cm|em|ex|m|mm|pc|pt|px)?$/u;
|
|
39
|
+
|
|
40
|
+
/** A CSS length as whole pixels, or undefined when it isn't one. */
|
|
41
|
+
const parseLength = (value: string | undefined): number | undefined => {
|
|
42
|
+
const groups = value === undefined ? undefined : LENGTH.exec(value)?.groups;
|
|
43
|
+
if (!groups) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const pixels = Math.round(
|
|
47
|
+
Number(groups.number) * (UNITS.get(groups.unit ?? "px") ?? 1)
|
|
48
|
+
);
|
|
49
|
+
return pixels > 0 ? pixels : undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** An attribute's raw value on the root tag, or undefined when absent. */
|
|
53
|
+
const attribute = (root: string, pattern: RegExp): string | undefined =>
|
|
54
|
+
pattern.exec(root)?.groups?.value;
|
|
55
|
+
|
|
56
|
+
/** The viewBox's own width and height (its third and fourth numbers). */
|
|
57
|
+
const viewBoxSize = (root: string): SvgDimensions | null => {
|
|
58
|
+
const raw = attribute(root, VIEWBOX);
|
|
59
|
+
if (raw === undefined) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const bounds = raw.split(" ");
|
|
63
|
+
const width = parseLength(bounds[2]);
|
|
64
|
+
const height = parseLength(bounds[3]);
|
|
65
|
+
return width && height ? { height, width } : null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The SVG's pixel size, or null when the root carries no usable size: no
|
|
70
|
+
* root tag, neither explicit dimensions nor a viewBox, a viewBox that isn't
|
|
71
|
+
* space-separated, or a zero or unparsable length.
|
|
72
|
+
*/
|
|
73
|
+
export const svgDimensions = (svg: string): SvgDimensions | null => {
|
|
74
|
+
const root = ROOT.exec(svg)?.[0];
|
|
75
|
+
if (root === undefined) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const width = parseLength(attribute(root, WIDTH));
|
|
79
|
+
const height = parseLength(attribute(root, HEIGHT));
|
|
80
|
+
if (width && height) {
|
|
81
|
+
return { height, width };
|
|
82
|
+
}
|
|
83
|
+
const box = viewBoxSize(root);
|
|
84
|
+
if (!box) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const ratio = box.width / box.height;
|
|
88
|
+
if (width) {
|
|
89
|
+
const scaled = Math.floor(width / ratio);
|
|
90
|
+
return scaled > 0 ? { height: scaled, width } : null;
|
|
91
|
+
}
|
|
92
|
+
if (height) {
|
|
93
|
+
const scaled = Math.floor(height * ratio);
|
|
94
|
+
return scaled > 0 ? { height, width: scaled } : null;
|
|
95
|
+
}
|
|
96
|
+
return box;
|
|
97
|
+
};
|
package/src/core/version-cut.ts
CHANGED
|
@@ -4,13 +4,13 @@ import { cp, readdir, readFile, rm } from "node:fs/promises";
|
|
|
4
4
|
import { join, relative } from "pathe";
|
|
5
5
|
|
|
6
6
|
import { stripBasePath } from "./base-path.ts";
|
|
7
|
+
import { nextFenceState } from "./code-fences.ts";
|
|
8
|
+
import type { FenceState } from "./code-fences.ts";
|
|
7
9
|
import { writeTextAtomic } from "./fs-atomic.ts";
|
|
8
10
|
import { localizeRoute } from "./i18n.ts";
|
|
9
11
|
import type { BlumeProject } from "./project-graph.ts";
|
|
10
12
|
import { isWithin, scanProject } from "./project-graph.ts";
|
|
11
13
|
import { VERSION_ID } from "./schema.ts";
|
|
12
|
-
import { nextFenceState } from "./sources/normalize.ts";
|
|
13
|
-
import type { FenceState } from "./sources/normalize.ts";
|
|
14
14
|
import { VERSION_LIKE, versionizeRoute } from "./versions.ts";
|
|
15
15
|
|
|
16
16
|
/** What `cutVersion` did, for the CLI to report. */
|