blume 2.0.2 → 2.0.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.
- package/CHANGELOG.md +65 -0
- package/dist/cli/{chunk-6k4ftwze.js → chunk-1d7ve1dm.js} +1 -1
- package/dist/cli/{chunk-zp79m0ts.js → chunk-2eytanqx.js} +2 -2
- package/dist/cli/{chunk-8g8ytmgx.js → chunk-2hsdwb9n.js} +19 -19
- package/dist/cli/{chunk-8g8ytmgx.js.map → chunk-2hsdwb9n.js.map} +1 -1
- package/dist/cli/{chunk-g698a744.js → chunk-2z928egk.js} +5 -5
- package/dist/cli/{chunk-ppzjqwx2.js → chunk-35d4wj9f.js} +17 -18
- package/dist/cli/{chunk-ppzjqwx2.js.map → chunk-35d4wj9f.js.map} +3 -3
- package/dist/cli/{chunk-mqc662a6.js → chunk-364znk6q.js} +2 -2
- package/dist/cli/{chunk-gs7r695n.js → chunk-3em5wd2y.js} +21 -8
- package/dist/cli/{chunk-gs7r695n.js.map → chunk-3em5wd2y.js.map} +3 -3
- package/dist/cli/{chunk-k7pj68a8.js → chunk-5f86nr5m.js} +15 -15
- package/dist/cli/{chunk-hr8ne106.js → chunk-5m5nmvyq.js} +68 -43
- package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
- package/dist/cli/{chunk-91ws1n6j.js → chunk-5xvm6tfj.js} +14 -14
- package/dist/cli/{chunk-w4bxdvsa.js → chunk-6dsbexzp.js} +14 -14
- package/dist/cli/{chunk-00gs3wqs.js → chunk-8ktnccpt.js} +1 -1
- package/dist/cli/{chunk-j85scx15.js → chunk-9t7a85s3.js} +130 -44
- package/dist/cli/chunk-9t7a85s3.js.map +10 -0
- package/dist/cli/{chunk-bbnwccaz.js → chunk-a58773jm.js} +2 -2
- package/dist/cli/{chunk-nfcyttvj.js → chunk-acanzt5p.js} +9 -9
- package/dist/cli/{chunk-nfcyttvj.js.map → chunk-acanzt5p.js.map} +1 -1
- package/dist/cli/{chunk-wdrt2k2v.js → chunk-akbpwfxc.js} +90 -26
- package/dist/cli/chunk-akbpwfxc.js.map +10 -0
- package/dist/cli/{chunk-sqw4ekg1.js → chunk-b07cmahc.js} +2 -2
- package/dist/cli/{chunk-xh43dwgw.js → chunk-c8chx29p.js} +42 -29
- package/dist/cli/{chunk-xh43dwgw.js.map → chunk-c8chx29p.js.map} +9 -9
- package/dist/cli/{chunk-7mbqtmgb.js → chunk-crgn1q09.js} +21 -12
- package/dist/cli/chunk-crgn1q09.js.map +10 -0
- package/dist/cli/chunk-e04dxsz1.js +39 -0
- package/dist/cli/chunk-e04dxsz1.js.map +10 -0
- package/dist/cli/{chunk-4e9b9ra6.js → chunk-ey84smr6.js} +3 -3
- package/dist/cli/{chunk-d1v5rhy0.js → chunk-g4hq16wv.js} +13 -13
- package/dist/cli/{chunk-273ygyr4.js → chunk-ga0pf4aj.js} +7 -7
- package/dist/cli/{chunk-273ygyr4.js.map → chunk-ga0pf4aj.js.map} +3 -3
- package/dist/cli/{chunk-pbg5a4s3.js → chunk-hm3vjy5s.js} +64 -45
- package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
- package/dist/cli/{chunk-5r8g91qn.js → chunk-j85vccga.js} +306 -107
- package/dist/cli/chunk-j85vccga.js.map +36 -0
- package/dist/cli/{chunk-n1yg3tj3.js → chunk-p3v96n38.js} +6 -6
- package/dist/cli/{chunk-n1yg3tj3.js.map → chunk-p3v96n38.js.map} +3 -3
- package/dist/cli/{chunk-7vtckvaw.js → chunk-p73c0m7w.js} +14 -14
- package/dist/cli/{chunk-7vtckvaw.js.map → chunk-p73c0m7w.js.map} +1 -1
- package/dist/cli/{chunk-qkb5a8sa.js → chunk-pehfxfta.js} +3 -3
- package/dist/cli/{chunk-v6ya5kcb.js → chunk-pv29h0wf.js} +1223 -978
- package/dist/cli/{chunk-v6ya5kcb.js.map → chunk-pv29h0wf.js.map} +47 -46
- package/dist/cli/{chunk-6dtt0zfn.js → chunk-q58y5e6a.js} +10 -10
- package/dist/cli/{chunk-6dtt0zfn.js.map → chunk-q58y5e6a.js.map} +3 -3
- package/dist/cli/{chunk-fsmrqk8a.js → chunk-r20tn01b.js} +1 -1
- package/dist/cli/{chunk-bfwp9vp6.js → chunk-r9rcc4w7.js} +7 -7
- package/dist/cli/{chunk-h7k3nq3v.js → chunk-tkacnehg.js} +2 -2
- package/dist/cli/{chunk-h2ez8dzb.js → chunk-tzmab476.js} +4 -4
- package/dist/cli/{chunk-3yce002v.js → chunk-vg9r4eb9.js} +6 -6
- package/dist/cli/{chunk-3yce002v.js.map → chunk-vg9r4eb9.js.map} +4 -4
- package/dist/cli/{chunk-hqp2ajnh.js → chunk-wrr3j9w9.js} +16 -17
- package/dist/cli/{chunk-hqp2ajnh.js.map → chunk-wrr3j9w9.js.map} +7 -7
- package/dist/cli/{chunk-ddndchfr.js → chunk-yfyb25rh.js} +38 -26
- package/dist/cli/chunk-yfyb25rh.js.map +14 -0
- package/dist/cli/index.js +20 -18
- package/dist/cli/index.js.map +3 -3
- package/dist/types/ai/link-headers.d.ts +8 -1
- package/dist/types/ai/openapi-components.d.ts +5 -2
- package/dist/types/ai/relative-links.d.ts +9 -7
- package/dist/types/ai/skills.d.ts +4 -1
- package/dist/types/ai/tar.d.ts +1 -3
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
- package/dist/types/analytics/schema.d.ts +14 -0
- package/dist/types/astro/integration.d.ts +3 -2
- package/dist/types/core/base-path.d.ts +21 -7
- package/dist/types/core/config-input.d.ts +9 -7
- package/dist/types/core/directive-diagnostics.d.ts +12 -0
- package/dist/types/core/heading-markers.d.ts +5 -7
- package/dist/types/core/i18n.d.ts +2 -0
- package/dist/types/core/last-modified.d.ts +10 -0
- package/dist/types/core/meta.d.ts +8 -0
- package/dist/types/core/schema.d.ts +7 -0
- package/dist/types/core/sources/lower.d.ts +29 -16
- package/dist/types/core/sources/normalize.d.ts +13 -1
- package/dist/types/core/sources/watch.d.ts +12 -5
- package/dist/types/core/standard-schema.d.ts +5 -0
- package/dist/types/deploy/artifacts.d.ts +6 -4
- package/dist/types/deploy/headers.d.ts +5 -0
- package/dist/types/deploy/platforms/types.d.ts +8 -0
- package/dist/types/deploy/redirects.d.ts +24 -13
- package/dist/types/markdown/directives.d.ts +62 -0
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/markdown/mdast.d.ts +63 -0
- package/dist/types/openapi/asyncapi.d.ts +4 -2
- package/docs/02-deployment.mdx +4 -2
- package/docs/08-faq.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +3 -3
- package/docs/cli/audit.mdx +17 -1
- package/docs/cli/evals.mdx +3 -3
- package/docs/cli/translate.mdx +3 -3
- package/docs/cli/version.mdx +1 -1
- package/docs/configuration/analytics.mdx +20 -1
- package/docs/configuration/customization.mdx +2 -0
- package/docs/configuration/search.mdx +1 -1
- package/docs/content/components.mdx +1 -1
- package/docs/content/frontmatter.mdx +1 -1
- package/docs/content/i18n.mdx +2 -0
- package/docs/content/index.mdx +4 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +3 -1
- package/docs/content/sources.mdx +1 -5
- package/docs/content/syntax.mdx +14 -0
- package/docs/content/versioning.mdx +1 -1
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +2 -1
- package/docs/discoverability/markdown.mdx +2 -2
- package/docs/discoverability/open-graph.mdx +5 -3
- package/docs/discoverability/rss.mdx +1 -1
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +1 -1
- package/docs/references/openapi.mdx +5 -3
- package/package.json +1 -1
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- package/skills/blume-migrate/references/nextra.md +1 -1
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +7 -2
- package/src/ai/agent-readability.ts +2 -2
- package/src/ai/ai-catalog.ts +6 -2
- package/src/ai/api/handlers.ts +2 -2
- package/src/ai/api/spec.ts +2 -2
- package/src/ai/api-catalog.ts +6 -2
- package/src/ai/changelog-markdown.ts +2 -2
- package/src/ai/link-headers.ts +14 -5
- package/src/ai/llms.ts +18 -7
- package/src/ai/mcp/discovery.ts +2 -2
- package/src/ai/mcp/query.ts +2 -2
- package/src/ai/mcp/server.ts +2 -2
- package/src/ai/openapi-components.ts +22 -6
- package/src/ai/relative-links.ts +42 -26
- package/src/ai/serializers.ts +2 -1
- package/src/ai/skills.ts +14 -1
- package/src/ai/tar.ts +139 -12
- package/src/analytics/head.ts +4 -0
- package/src/analytics/index.ts +5 -0
- package/src/analytics/one-dollar-stats.ts +86 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +1 -1
- package/src/astro/include-hmr.ts +31 -12
- package/src/astro/include-refresh.ts +19 -8
- package/src/astro/integration.ts +108 -14
- package/src/astro/runtime-modules.ts +28 -13
- package/src/astro/templates.ts +81 -37
- package/src/audit/checks/links.ts +8 -1
- package/src/audit/graph.ts +3 -1
- package/src/audit/run.ts +12 -9
- package/src/audit/snapshot.ts +5 -0
- package/src/cli/args.ts +32 -0
- package/src/cli/commands/audit.ts +4 -1
- package/src/cli/commands/doctor.ts +3 -1
- package/src/cli/commands/eval.ts +19 -11
- package/src/cli/commands/translate.ts +9 -12
- package/src/cli/commands/validate.ts +3 -1
- package/src/cli/dev-lock.ts +157 -37
- package/src/cli/report-format.ts +11 -6
- package/src/components/colors.ts +19 -0
- package/src/components/content/Badge.astro +5 -3
- package/src/components/content/Component.astro +2 -2
- package/src/components/content/Tab.astro +0 -1
- package/src/components/content/Tabs.astro +4 -0
- package/src/components/content/badge-color.ts +5 -2
- package/src/components/content/base-href.ts +13 -36
- package/src/components/islands/assistant.tsx +21 -4
- package/src/components/islands/base-path.ts +47 -10
- package/src/components/islands/hooks.ts +35 -19
- package/src/components/islands/webmcp.ts +4 -2
- package/src/components/layout/Breadcrumbs.astro +5 -2
- package/src/components/layout/DiscoveryLinks.astro +9 -5
- package/src/components/layout/Header.astro +2 -2
- package/src/components/layout/LanguageSwitcher.astro +2 -2
- package/src/components/layout/NavSelector.astro +2 -2
- package/src/components/layout/NavTabMenu.astro +3 -3
- package/src/components/layout/NavTree.astro +16 -6
- package/src/components/layout/PageActions.astro +4 -3
- package/src/components/layout/PageLayout.astro +7 -6
- package/src/components/layout/Pagination.astro +3 -3
- package/src/components/layout/RootLayout.astro +5 -5
- package/src/components/layout/Search.astro +33 -13
- package/src/components/layout/VersionBanner.astro +2 -2
- package/src/components/layout/analytics-client.ts +12 -0
- package/src/components/layout/toc-active.ts +41 -0
- package/src/components/layout/toc-element.ts +8 -14
- package/src/components/openapi/ApiTagOperations.astro +2 -2
- package/src/components/openapi/AsyncApiOperation.astro +7 -4
- package/src/components/openapi/GraphqlChip.astro +2 -2
- package/src/components/openapi/Operation.astro +2 -0
- package/src/components/openapi/Playground.astro +4 -4
- package/src/components/openapi/RequestPanel.astro +5 -2
- package/src/components/openapi/SchemaTable.astro +7 -0
- package/src/components/openapi/async.ts +38 -6
- package/src/components/openapi/helpers.ts +19 -9
- package/src/components/openapi/message-composer.ts +6 -1
- package/src/components/openapi/message.ts +17 -2
- package/src/components/openapi/operation-model.ts +81 -11
- package/src/components/openapi/panel.ts +4 -2
- package/src/components/openapi/param-style.ts +181 -0
- package/src/components/openapi/playground-client.ts +73 -16
- package/src/components/openapi/request.ts +190 -26
- package/src/components/openapi/schema-tree.ts +30 -24
- package/src/components/openapi/snippets.ts +85 -6
- package/src/components/openapi/ws-client.ts +18 -2
- package/src/core/base-path.ts +62 -16
- package/src/core/config-input.ts +9 -7
- package/src/core/diagnostics.ts +2 -0
- package/src/core/directive-diagnostics.ts +99 -0
- package/src/core/frontmatter.ts +21 -18
- package/src/core/heading-markers.ts +5 -18
- package/src/core/i18n.ts +31 -3
- package/src/core/last-modified.ts +25 -3
- package/src/core/locale-links.ts +5 -1
- package/src/core/meta.ts +33 -19
- package/src/core/navigation.ts +28 -8
- package/src/core/project-graph.ts +27 -5
- package/src/core/schema.ts +24 -4
- package/src/core/sources/contentful-rich-text.ts +27 -20
- package/src/core/sources/filesystem.ts +20 -2
- package/src/core/sources/github-releases.ts +25 -6
- package/src/core/sources/lexical.ts +23 -18
- package/src/core/sources/lower.ts +201 -34
- package/src/core/sources/mdx-remote.ts +51 -16
- package/src/core/sources/normalize.ts +325 -90
- package/src/core/sources/notion.ts +52 -25
- package/src/core/sources/obsidian.ts +23 -6
- package/src/core/sources/portable-text.ts +38 -21
- package/src/core/sources/strapi-blocks.ts +20 -16
- package/src/core/sources/watch.ts +20 -7
- package/src/core/standard-schema.ts +10 -6
- package/src/core/version-cut.ts +52 -15
- package/src/deploy/artifacts.ts +27 -6
- package/src/deploy/cloudflare-negotiation.ts +4 -12
- package/src/deploy/headers.ts +8 -4
- package/src/deploy/node-headers.ts +1 -1
- package/src/deploy/platforms/cloudflare.ts +12 -3
- package/src/deploy/platforms/netlify.ts +1 -0
- package/src/deploy/platforms/node.ts +1 -0
- package/src/deploy/platforms/static.ts +1 -0
- package/src/deploy/platforms/types.ts +8 -0
- package/src/deploy/platforms/vercel.ts +1 -0
- package/src/deploy/redirects.ts +54 -18
- package/src/deploy/robots.ts +2 -2
- package/src/deploy/rss.ts +4 -3
- package/src/deploy/sitemap.ts +7 -5
- package/src/markdown/base-links.ts +34 -36
- package/src/markdown/directives.ts +242 -36
- package/src/markdown/features.ts +17 -0
- package/src/markdown/index.ts +7 -6
- package/src/markdown/mdast.ts +5 -2
- package/src/markdown/relative-links.ts +12 -4
- package/src/og/card.ts +129 -5
- package/src/og/derive.ts +41 -31
- package/src/og/index.ts +1 -0
- package/src/openapi/asyncapi.ts +4 -2
- package/src/openapi/model.ts +22 -14
- package/src/openapi/proxy.ts +63 -10
- package/src/openapi/render-mdx.ts +41 -2
- package/src/registry/eject.ts +181 -24
- package/src/search/adapters/version-scope.ts +30 -0
- package/src/search/documents.ts +67 -26
- package/src/search/popular.ts +2 -1
- package/src/seo/jsonld.ts +7 -3
- package/src/translate/meta.ts +68 -24
- package/src/translate/run.ts +3 -3
- package/src/translate/validate.ts +4 -1
- package/src/translate/work-list.ts +56 -17
- package/dist/cli/chunk-5r8g91qn.js.map +0 -34
- package/dist/cli/chunk-7mbqtmgb.js.map +0 -10
- package/dist/cli/chunk-ddndchfr.js.map +0 -14
- package/dist/cli/chunk-esh98wmb.js +0 -23
- package/dist/cli/chunk-esh98wmb.js.map +0 -10
- package/dist/cli/chunk-hr8ne106.js.map +0 -13
- package/dist/cli/chunk-j85scx15.js.map +0 -10
- package/dist/cli/chunk-pbg5a4s3.js.map +0 -19
- package/dist/cli/chunk-wdrt2k2v.js.map +0 -10
- /package/dist/cli/{chunk-6k4ftwze.js.map → chunk-1d7ve1dm.js.map} +0 -0
- /package/dist/cli/{chunk-zp79m0ts.js.map → chunk-2eytanqx.js.map} +0 -0
- /package/dist/cli/{chunk-g698a744.js.map → chunk-2z928egk.js.map} +0 -0
- /package/dist/cli/{chunk-mqc662a6.js.map → chunk-364znk6q.js.map} +0 -0
- /package/dist/cli/{chunk-k7pj68a8.js.map → chunk-5f86nr5m.js.map} +0 -0
- /package/dist/cli/{chunk-91ws1n6j.js.map → chunk-5xvm6tfj.js.map} +0 -0
- /package/dist/cli/{chunk-w4bxdvsa.js.map → chunk-6dsbexzp.js.map} +0 -0
- /package/dist/cli/{chunk-00gs3wqs.js.map → chunk-8ktnccpt.js.map} +0 -0
- /package/dist/cli/{chunk-bbnwccaz.js.map → chunk-a58773jm.js.map} +0 -0
- /package/dist/cli/{chunk-sqw4ekg1.js.map → chunk-b07cmahc.js.map} +0 -0
- /package/dist/cli/{chunk-4e9b9ra6.js.map → chunk-ey84smr6.js.map} +0 -0
- /package/dist/cli/{chunk-d1v5rhy0.js.map → chunk-g4hq16wv.js.map} +0 -0
- /package/dist/cli/{chunk-qkb5a8sa.js.map → chunk-pehfxfta.js.map} +0 -0
- /package/dist/cli/{chunk-fsmrqk8a.js.map → chunk-r20tn01b.js.map} +0 -0
- /package/dist/cli/{chunk-bfwp9vp6.js.map → chunk-r9rcc4w7.js.map} +0 -0
- /package/dist/cli/{chunk-h7k3nq3v.js.map → chunk-tkacnehg.js.map} +0 -0
- /package/dist/cli/{chunk-h2ez8dzb.js.map → chunk-tzmab476.js.map} +0 -0
|
@@ -3,15 +3,30 @@ import { toString as mdastToString } from "mdast-util-to-string";
|
|
|
3
3
|
import { jsxAttribute, jsxFlowElement } from "./mdast.ts";
|
|
4
4
|
import type { MdastNode, MdastVisitorContext } from "./mdast.ts";
|
|
5
5
|
|
|
6
|
-
interface DirectiveNode extends MdastNode {
|
|
6
|
+
export interface DirectiveNode extends MdastNode {
|
|
7
7
|
attributes?: Record<string, string | null | undefined> | null;
|
|
8
8
|
// Satteri gives an empty container directive (`:::note\n:::`) `children: null`.
|
|
9
9
|
children?: MdastNode[] | null;
|
|
10
10
|
name: string;
|
|
11
|
+
position?: {
|
|
12
|
+
start?: { offset?: number };
|
|
13
|
+
end?: { offset?: number };
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The visitor-context slice the directive visitors use. `source` is the page
|
|
19
|
+
* the directive offsets index into; without it the literal fallback rebuilds
|
|
20
|
+
* a directive from its node instead. `parent` walks up to an enclosing
|
|
21
|
+
* container directive, which renders the directives inside it itself.
|
|
22
|
+
*/
|
|
23
|
+
interface DirectiveVisitorContext extends MdastVisitorContext {
|
|
24
|
+
parent?: (node: MdastNode) => MdastNode | undefined;
|
|
25
|
+
source?: string;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
/** Directive names that map directly onto a Callout type. */
|
|
14
|
-
const CALLOUT_TYPES = new Set([
|
|
29
|
+
export const CALLOUT_TYPES: ReadonlySet<string> = new Set([
|
|
15
30
|
"danger",
|
|
16
31
|
"info",
|
|
17
32
|
"note",
|
|
@@ -25,7 +40,7 @@ interface CalloutAliases {
|
|
|
25
40
|
[alias: string]: string;
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
const
|
|
43
|
+
export const CALLOUT_ALIASES: Readonly<CalloutAliases> = {
|
|
29
44
|
caution: "warning",
|
|
30
45
|
error: "danger",
|
|
31
46
|
important: "note",
|
|
@@ -38,48 +53,239 @@ export const calloutTypeFor = (name: string): string | null => {
|
|
|
38
53
|
if (CALLOUT_TYPES.has(lower)) {
|
|
39
54
|
return lower;
|
|
40
55
|
}
|
|
41
|
-
return
|
|
56
|
+
return CALLOUT_ALIASES[lower] ?? null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** The markers that open a text (`:name`) and a leaf (`::name`) directive. */
|
|
60
|
+
const LITERAL_MARKERS = new Map([
|
|
61
|
+
["leafDirective", "::"],
|
|
62
|
+
["textDirective", ":"],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
/** A directive's `[label]` rebuilt from its label's text, or nothing. */
|
|
66
|
+
const labelSource = (label: MdastNode[]): string =>
|
|
67
|
+
label.length > 0
|
|
68
|
+
? `[${mdastToString(label, { includeImageAlt: false })}]`
|
|
69
|
+
: "";
|
|
70
|
+
|
|
71
|
+
/** A directive's `{attributes}` rebuilt from its node, or nothing. */
|
|
72
|
+
const attributeSource = (node: DirectiveNode): string => {
|
|
73
|
+
const attributes = Object.entries(node.attributes ?? {}).map(
|
|
74
|
+
([key, value]) => (value ? `${key}="${value}"` : key)
|
|
75
|
+
);
|
|
76
|
+
return attributes.length > 0 ? `{${attributes.join(" ")}}` : "";
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** The source a directive node spans, when its offsets point into `source`. */
|
|
80
|
+
const spannedSource = (node: DirectiveNode, source: string): string | null => {
|
|
81
|
+
const start = node.position?.start?.offset;
|
|
82
|
+
const end = node.position?.end?.offset;
|
|
83
|
+
return start !== undefined && end !== undefined
|
|
84
|
+
? source.slice(start, end)
|
|
85
|
+
: null;
|
|
42
86
|
};
|
|
43
87
|
|
|
44
88
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
89
|
+
* A text or leaf directive exactly as the author wrote it. The slice by
|
|
90
|
+
* offsets is the exact text — `[label]` and `{attrs}` included — and is
|
|
91
|
+
* trusted only when it opens with the directive's own marker and name;
|
|
92
|
+
* content an `<include>` spliced in carries no offsets into this page, so it
|
|
93
|
+
* is rebuilt from the node instead.
|
|
49
94
|
*/
|
|
50
|
-
export const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
95
|
+
export const directiveSource = (
|
|
96
|
+
node: DirectiveNode,
|
|
97
|
+
marker: string,
|
|
98
|
+
source: string
|
|
99
|
+
): string => {
|
|
100
|
+
const opening = `${marker}${node.name}`;
|
|
101
|
+
const slice = spannedSource(node, source);
|
|
102
|
+
if (slice?.startsWith(opening)) {
|
|
103
|
+
return slice;
|
|
104
|
+
}
|
|
105
|
+
return `${opening}${labelSource(node.children ?? [])}${attributeSource(node)}`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The node a text or leaf directive renders as: its literal source, as text
|
|
110
|
+
* in place of a text directive and as a paragraph in place of a leaf one.
|
|
111
|
+
*/
|
|
112
|
+
const literalDirective = (
|
|
113
|
+
node: DirectiveNode,
|
|
114
|
+
marker: string,
|
|
115
|
+
source: string
|
|
116
|
+
): MdastNode => {
|
|
117
|
+
const text = { type: "text", value: directiveSource(node, marker, source) };
|
|
118
|
+
return marker === ":" ? text : { children: [text], type: "paragraph" };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const FENCE = /^:{3,}/u;
|
|
122
|
+
|
|
123
|
+
// The line that closes a container: a colon fence, after whatever quote or
|
|
124
|
+
// list indentation the container sits in.
|
|
125
|
+
const CLOSING_FENCE = /\n[\t >]*(?<fence>:{3,})\s*$/u;
|
|
126
|
+
|
|
127
|
+
/** A paragraph holding one line of literal text. */
|
|
128
|
+
const literalLine = (value: string): MdastNode => ({
|
|
129
|
+
children: [{ type: "text", value }],
|
|
130
|
+
type: "paragraph",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The fence lines of a container directive as the author wrote them — the
|
|
135
|
+
* opening one with its `[label]` and `{attrs}` — or, without offsets into
|
|
136
|
+
* this page (an `<include>`), rebuilt from the node. A container left
|
|
137
|
+
* unclosed runs to the end of its parent and has no closing line.
|
|
138
|
+
*/
|
|
139
|
+
const containerFences = (
|
|
140
|
+
node: DirectiveNode,
|
|
141
|
+
label: MdastNode | undefined,
|
|
142
|
+
source: string
|
|
143
|
+
): string[] => {
|
|
144
|
+
const slice = spannedSource(node, source) ?? "";
|
|
145
|
+
const fence = FENCE.exec(slice)?.[0];
|
|
146
|
+
if (fence === undefined || !slice.startsWith(node.name, fence.length)) {
|
|
147
|
+
const labelText = labelSource(label ? [label] : []);
|
|
148
|
+
return [`:::${node.name}${labelText}${attributeSource(node)}`, ":::"];
|
|
149
|
+
}
|
|
150
|
+
const [opening = ""] = slice.split("\n", 1);
|
|
151
|
+
const closing = CLOSING_FENCE.exec(slice.slice(opening.length))?.groups
|
|
152
|
+
?.fence;
|
|
153
|
+
// A shorter fence can't close the container; it is the body's last line.
|
|
154
|
+
return closing !== undefined && closing.length >= fence.length
|
|
155
|
+
? [opening.trimEnd(), closing]
|
|
156
|
+
: [opening.trimEnd()];
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* What a container directive renders as. A callout name becomes a
|
|
161
|
+
* `<Callout>`: the title comes from a `[label]` or a `{title="…"}` attribute,
|
|
162
|
+
* and the body becomes the callout content. Blume has no other container, and
|
|
163
|
+
* dropping one would drop its body with it, so any other name — `:::details`,
|
|
164
|
+
* a `:::warnig` typo — renders its body between its fence lines, as written.
|
|
165
|
+
* The directives inside render here too, since Satteri reaches them only
|
|
166
|
+
* after this replacement has taken their original.
|
|
167
|
+
*/
|
|
168
|
+
const renderContainer = (
|
|
169
|
+
node: DirectiveNode,
|
|
170
|
+
source: string
|
|
171
|
+
): MdastNode | MdastNode[] => {
|
|
172
|
+
const children = (node.children ?? []).flatMap((child) =>
|
|
173
|
+
// oxlint-disable-next-line no-use-before-define -- mutual recursion: a container's body holds directives, containers included
|
|
174
|
+
literalize(child, source)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// A leading `:::name[Label]` parses to a paragraph flagged `directiveLabel`.
|
|
178
|
+
// SAFETY: Satteri stamps `directiveLabel` on that paragraph's `data`; any
|
|
179
|
+
// other node reads undefined and fails the check.
|
|
180
|
+
const labelIndex = children.findIndex(
|
|
181
|
+
(child) =>
|
|
182
|
+
child.type === "paragraph" &&
|
|
183
|
+
(child.data as { directiveLabel?: boolean } | undefined)?.directiveLabel
|
|
184
|
+
);
|
|
185
|
+
const [label] = labelIndex === -1 ? [] : children.splice(labelIndex, 1);
|
|
186
|
+
|
|
187
|
+
const type = calloutTypeFor(node.name);
|
|
188
|
+
if (type === null) {
|
|
189
|
+
const [opening = "", closing] = containerFences(node, label, source);
|
|
190
|
+
return [
|
|
191
|
+
literalLine(opening),
|
|
192
|
+
...children,
|
|
193
|
+
...(closing === undefined ? [] : [literalLine(closing)]),
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Flatten the label's phrasing children so `:::note[Read **this**]` yields
|
|
198
|
+
// `Read this`; image alt is excluded (an image is not label text), matching
|
|
199
|
+
// the historical child-values-only behavior.
|
|
200
|
+
const title =
|
|
201
|
+
node.attributes?.title ??
|
|
202
|
+
(label ? mdastToString(label, { includeImageAlt: false }) : undefined);
|
|
203
|
+
const attributes = [jsxAttribute("type", type)];
|
|
204
|
+
if (title) {
|
|
205
|
+
attributes.push(jsxAttribute("title", title));
|
|
206
|
+
}
|
|
207
|
+
return jsxFlowElement("Callout", attributes, children);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* `node` with every directive in it rendered: text and leaf directives as
|
|
212
|
+
* their literal source, containers as {@link renderContainer}.
|
|
213
|
+
*/
|
|
214
|
+
const literalize = (node: MdastNode, source: string): MdastNode[] => {
|
|
215
|
+
if (node.type === "containerDirective") {
|
|
216
|
+
// SAFETY: a `containerDirective` carries a `name` plus optional
|
|
217
|
+
// attributes, children, and position — the `DirectiveNode` shape.
|
|
218
|
+
return [renderContainer(node as DirectiveNode, source)].flat();
|
|
219
|
+
}
|
|
220
|
+
const marker = LITERAL_MARKERS.get(node.type);
|
|
221
|
+
if (marker) {
|
|
222
|
+
// SAFETY: only Satteri's `textDirective`/`leafDirective` nodes have a
|
|
223
|
+
// marker, and they carry a `name` plus optional attributes, children,
|
|
224
|
+
// and position — the `DirectiveNode` shape.
|
|
225
|
+
return [literalDirective(node as DirectiveNode, marker, source)];
|
|
226
|
+
}
|
|
227
|
+
// SAFETY: a parent's `children` is always a node list; leaves carry none.
|
|
228
|
+
const children = node.children as MdastNode[] | null | undefined;
|
|
229
|
+
if (!children) {
|
|
230
|
+
return [node];
|
|
231
|
+
}
|
|
232
|
+
return [
|
|
233
|
+
{
|
|
234
|
+
...node,
|
|
235
|
+
children: children.flatMap((child) => literalize(child, source)),
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** Whether a container directive (already rendered, body and all) holds `node`. */
|
|
241
|
+
const insideContainer = (node: MdastNode, ctx: DirectiveVisitorContext) => {
|
|
242
|
+
for (
|
|
243
|
+
let parent = ctx.parent?.(node);
|
|
244
|
+
parent !== undefined;
|
|
245
|
+
parent = ctx.parent?.(parent)
|
|
246
|
+
) {
|
|
247
|
+
if (parent.type === "containerDirective") {
|
|
248
|
+
return true;
|
|
55
249
|
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
};
|
|
56
253
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// A
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
child.type === "paragraph" &&
|
|
66
|
-
(child.data as { directiveLabel?: boolean } | undefined)?.directiveLabel
|
|
67
|
-
);
|
|
68
|
-
if (labelIndex !== -1) {
|
|
69
|
-
const [label] = children.splice(labelIndex, 1);
|
|
70
|
-
if (label) {
|
|
71
|
-
// Flatten the label's phrasing children so `:::note[Read **this**]`
|
|
72
|
-
// yields `Read this`; image alt is excluded (an image is not label
|
|
73
|
-
// text), matching the historical child-values-only behavior.
|
|
74
|
-
title ??= mdastToString(label, { includeImageAlt: false }) || undefined;
|
|
75
|
-
}
|
|
254
|
+
/** The text and leaf visitor: render the directive as its literal source. */
|
|
255
|
+
const renderLiteral =
|
|
256
|
+
(marker: string) => (node: DirectiveNode, ctx: DirectiveVisitorContext) => {
|
|
257
|
+
// A container's body was already rendered into its replacement; a
|
|
258
|
+
// transform queued on the replaced original would be dropped with a
|
|
259
|
+
// warning.
|
|
260
|
+
if (insideContainer(node, ctx)) {
|
|
261
|
+
return;
|
|
76
262
|
}
|
|
263
|
+
ctx.replaceNode(node, literalDirective(node, marker, ctx.source ?? ""));
|
|
264
|
+
};
|
|
77
265
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Satteri MDAST plugin for directives. Container directives (`:::note`,
|
|
268
|
+
* `:::warning`, `:::tip`, …) map onto Blume's `<Callout>` component, and any
|
|
269
|
+
* other container renders its body between its literal fence lines (see
|
|
270
|
+
* {@link renderContainer}).
|
|
271
|
+
*
|
|
272
|
+
* Blume handles no text (`:name`) or leaf (`::name`) directives, and prose is
|
|
273
|
+
* full of text that parses as one — `16:9`, `10:30am`, `og:image`,
|
|
274
|
+
* `pets:read` — so both render as the literal source the author wrote instead
|
|
275
|
+
* of vanishing.
|
|
276
|
+
*/
|
|
277
|
+
export const directiveToCalloutPlugin = () => ({
|
|
278
|
+
containerDirective(node: DirectiveNode, ctx: DirectiveVisitorContext) {
|
|
279
|
+
// A container inside another was rendered with its enclosing one.
|
|
280
|
+
if (insideContainer(node, ctx)) {
|
|
281
|
+
return;
|
|
81
282
|
}
|
|
82
|
-
ctx.replaceNode(node,
|
|
283
|
+
ctx.replaceNode(node, renderContainer(node, ctx.source ?? ""));
|
|
83
284
|
},
|
|
285
|
+
leafDirective: renderLiteral("::"),
|
|
84
286
|
name: "blume-directive-callout",
|
|
287
|
+
// Positions are opt-in since satteri 0.10; the literal fallback slices
|
|
288
|
+
// directives out of the source by offset.
|
|
289
|
+
options: { position: true },
|
|
290
|
+
textDirective: renderLiteral(":"),
|
|
85
291
|
});
|
package/src/markdown/features.ts
CHANGED
|
@@ -19,3 +19,20 @@ export const MDX_FEATURES = {
|
|
|
19
19
|
directive: true,
|
|
20
20
|
math: { singleDollarTextMath: false },
|
|
21
21
|
} satisfies Features;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The feature sets for a page body, once its front matter is off. Astro reads
|
|
25
|
+
* a page's front matter itself and hands the renderer the body alone, and the
|
|
26
|
+
* search extractor strips it first too — so front matter parsing is off here:
|
|
27
|
+
* left on, a body that opens with a `---` rule read everything up to the next
|
|
28
|
+
* `---` line as front matter and dropped it from the page.
|
|
29
|
+
*/
|
|
30
|
+
export const MARKDOWN_BODY_FEATURES = {
|
|
31
|
+
...MARKDOWN_FEATURES,
|
|
32
|
+
frontmatter: false,
|
|
33
|
+
} satisfies Features;
|
|
34
|
+
|
|
35
|
+
export const MDX_BODY_FEATURES = {
|
|
36
|
+
...MDX_FEATURES,
|
|
37
|
+
frontmatter: false,
|
|
38
|
+
} satisfies Features;
|
package/src/markdown/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { baseLinksPlugin } from "./base-links.ts";
|
|
|
13
13
|
import { codeTitleTransformer } from "./code-title.ts";
|
|
14
14
|
import { directiveToCalloutPlugin } from "./directives.ts";
|
|
15
15
|
import { externalLinksPlugin } from "./external-links.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { MARKDOWN_BODY_FEATURES, MDX_BODY_FEATURES } from "./features.ts";
|
|
17
17
|
import { headingAnchorPlugin } from "./heading-anchors.ts";
|
|
18
18
|
import { includePlugin } from "./include.ts";
|
|
19
19
|
import { inlineCodeHighlightPlugin } from "./inline-code.ts";
|
|
@@ -295,9 +295,9 @@ export interface BlumeMarkdownOptions {
|
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
297
|
* MDAST plugins that apply to both `.md` and `.mdx`: relative page links
|
|
298
|
-
* rewritten to the
|
|
299
|
-
* rewrite (added only when a `basePath` or
|
|
300
|
-
*
|
|
298
|
+
* rewritten to the served URL of the route they mean, then the base-path link
|
|
299
|
+
* rewrite for root-relative links (added only when a `basePath` or
|
|
300
|
+
* `deployBase` is configured).
|
|
301
301
|
*/
|
|
302
302
|
const blumeSharedMdastPlugins = (
|
|
303
303
|
options: BlumeMarkdownOptions
|
|
@@ -306,6 +306,7 @@ const blumeSharedMdastPlugins = (
|
|
|
306
306
|
relativeLinksPlugin({
|
|
307
307
|
contentRoot: options.contentRoot,
|
|
308
308
|
dataFile: options.dataFile,
|
|
309
|
+
deployBase: options.deployBase,
|
|
309
310
|
})
|
|
310
311
|
),
|
|
311
312
|
...(options.basePath || options.deployBase
|
|
@@ -330,7 +331,7 @@ const blumeIncludePlugin = (options: BlumeMarkdownOptions): MdastPlugin =>
|
|
|
330
331
|
/** Sätteri processor for plain `.md`, with Blume's curated feature set. */
|
|
331
332
|
export const blumeMarkdownProcessor = (options: BlumeMarkdownOptions = {}) =>
|
|
332
333
|
satteri({
|
|
333
|
-
features: { ...
|
|
334
|
+
features: { ...MARKDOWN_BODY_FEATURES },
|
|
334
335
|
hastPlugins: blumeHastPlugins(options),
|
|
335
336
|
mdastPlugins: [
|
|
336
337
|
blumeIncludePlugin(options),
|
|
@@ -359,7 +360,7 @@ export type BlumeMdxOptions = BlumeMarkdownOptions;
|
|
|
359
360
|
*/
|
|
360
361
|
export const blumeMdxProcessor = (options: BlumeMdxOptions = {}) =>
|
|
361
362
|
satteri({
|
|
362
|
-
features: { ...
|
|
363
|
+
features: { ...MDX_BODY_FEATURES },
|
|
363
364
|
hastPlugins: blumeHastPlugins(options),
|
|
364
365
|
mdastPlugins: [
|
|
365
366
|
blumeIncludePlugin(options),
|
package/src/markdown/mdast.ts
CHANGED
|
@@ -19,9 +19,12 @@ export type MdastValue =
|
|
|
19
19
|
| MdastValue[]
|
|
20
20
|
| { [key: string]: MdastValue };
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The visitor context Blume's plugins use to mutate the tree. A list of
|
|
24
|
+
* nodes takes the replaced node's place in order.
|
|
25
|
+
*/
|
|
23
26
|
export interface MdastVisitorContext {
|
|
24
|
-
replaceNode: (node: MdastNode, replacement: MdastNode) => void;
|
|
27
|
+
replaceNode: (node: MdastNode, replacement: MdastNode | MdastNode[]) => void;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** Any MDAST node, keyed loosely since we build a small subset by hand. */
|
|
@@ -2,6 +2,7 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
|
|
3
3
|
import { dirname, normalize, relative, resolve } from "pathe";
|
|
4
4
|
|
|
5
|
+
import { mountBasePath } from "../core/base-path.ts";
|
|
5
6
|
import { isIndexFileName, resolveRelativeHref } from "../core/links.ts";
|
|
6
7
|
import type { RelativeLinkBase } from "../core/links.ts";
|
|
7
8
|
import type { MdastNode, MdastValue } from "./mdast.ts";
|
|
@@ -130,6 +131,8 @@ export interface RelativeLinksPluginOptions {
|
|
|
130
131
|
* to publish the snapshot, the plugin reads the file eject writes instead.
|
|
131
132
|
*/
|
|
132
133
|
dataFile?: string;
|
|
134
|
+
/** Astro's `deployment.base` subdirectory (`""` or `/seg`). */
|
|
135
|
+
deployBase?: string;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
/**
|
|
@@ -145,9 +148,11 @@ export interface RelativeLinksPluginOptions {
|
|
|
145
148
|
* entries (remote sources) by the longest trailing path that names one. A file
|
|
146
149
|
* the snapshot doesn't know keeps its links as written.
|
|
147
150
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* `
|
|
151
|
+
* A rewritten route already carries `basePath`, and the `deployment.base`
|
|
152
|
+
* prefix goes on here, unconditionally: the route is one Blume serves, so
|
|
153
|
+
* `guides/setup.md` under base `/guides` is linked at `/guides/guides/setup`.
|
|
154
|
+
* The base-links plugin that runs next is idempotent per layer, so it leaves
|
|
155
|
+
* the based route alone.
|
|
151
156
|
*/
|
|
152
157
|
export const relativeLinksPlugin = (
|
|
153
158
|
options: RelativeLinksPluginOptions = {}
|
|
@@ -155,6 +160,7 @@ export const relativeLinksPlugin = (
|
|
|
155
160
|
const contentRoot = options.contentRoot
|
|
156
161
|
? resolve(options.contentRoot)
|
|
157
162
|
: undefined;
|
|
163
|
+
const deployBase = options.deployBase ?? "";
|
|
158
164
|
const readSnapshot = routeSnapshotReader(options.dataFile);
|
|
159
165
|
|
|
160
166
|
// Parsed once per published snapshot: the CLI republishes on regeneration,
|
|
@@ -245,7 +251,9 @@ export const relativeLinksPlugin = (
|
|
|
245
251
|
const next = resolveRelativeHref(url, page, page.routeOf, (route) =>
|
|
246
252
|
page.routes.has(route)
|
|
247
253
|
);
|
|
248
|
-
return next ===
|
|
254
|
+
return next === undefined || next === url
|
|
255
|
+
? undefined
|
|
256
|
+
: mountBasePath(deployBase, next);
|
|
249
257
|
};
|
|
250
258
|
|
|
251
259
|
const rewrite = (node: UrlNode, ctx: RelativeLinksContext): void => {
|
package/src/og/card.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
|
|
3
3
|
import { render } from "takumi-js";
|
|
4
4
|
import type { RenderOptions } from "takumi-js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
collectCodepoints,
|
|
7
|
+
container,
|
|
8
|
+
googleFonts,
|
|
9
|
+
image,
|
|
10
|
+
text,
|
|
11
|
+
} from "takumi-js/helpers";
|
|
6
12
|
import type { FontSubset, GoogleFontFamily, Node } from "takumi-js/helpers";
|
|
7
13
|
|
|
8
14
|
import { svgDimensions } from "../core/svg-dimensions.ts";
|
|
@@ -42,6 +48,9 @@ export type OgFont =
|
|
|
42
48
|
}
|
|
43
49
|
| OgLocalFont;
|
|
44
50
|
|
|
51
|
+
/** An OG font fetched from Google Fonts (a bare family name or the object form). */
|
|
52
|
+
export type OgGoogleFont = Exclude<OgFont, OgLocalFont>;
|
|
53
|
+
|
|
45
54
|
/**
|
|
46
55
|
* Which loaded family each card role renders in. Takumi still falls back
|
|
47
56
|
* across every loaded font per glyph, so a family that misses a script
|
|
@@ -106,6 +115,12 @@ export interface OgCardOptions {
|
|
|
106
115
|
* from disk instead of Google Fonts.
|
|
107
116
|
*/
|
|
108
117
|
fonts?: OgFont[];
|
|
118
|
+
/**
|
|
119
|
+
* Script fallbacks, tried after `fonts` for glyphs nothing else draws. A
|
|
120
|
+
* card loads them only when its text needs one, so a Latin-only card never
|
|
121
|
+
* fetches them — see {@link fallbackSubsets}.
|
|
122
|
+
*/
|
|
123
|
+
fallbacks?: OgGoogleFont[];
|
|
109
124
|
/** Per-role families from the loaded fonts (title vs body text). */
|
|
110
125
|
families?: OgFontFamilies;
|
|
111
126
|
}
|
|
@@ -145,9 +160,7 @@ const fontSubsetCache = new Map<string, Promise<FontSubset[]>>();
|
|
|
145
160
|
* fetch failure rejects, failing the build with the cause rather than silently
|
|
146
161
|
* shipping tofu — the same fail-fast the OG accent relies on.
|
|
147
162
|
*/
|
|
148
|
-
const loadFonts = (
|
|
149
|
-
fonts: Exclude<OgFont, OgLocalFont>[]
|
|
150
|
-
): Promise<FontSubset[]> => {
|
|
163
|
+
const loadFonts = (fonts: OgGoogleFont[]): Promise<FontSubset[]> => {
|
|
151
164
|
const key = JSON.stringify(fonts);
|
|
152
165
|
let pending = fontSubsetCache.get(key);
|
|
153
166
|
if (!pending) {
|
|
@@ -160,6 +173,113 @@ const loadFonts = (
|
|
|
160
173
|
return pending;
|
|
161
174
|
};
|
|
162
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The code points Takumi's built-in Geist draws: whitespace, printable ASCII,
|
|
178
|
+
* Latin-1 except `µ`, a few Latin extras French needs, and the typographic
|
|
179
|
+
* punctuation English copy leans on. Probed against the renderer, and
|
|
180
|
+
* `og-card.test.ts` renders every one, so a Takumi upgrade that drops a glyph
|
|
181
|
+
* fails there instead of shipping tofu.
|
|
182
|
+
*/
|
|
183
|
+
export const BUILT_IN_GLYPHS: [number, number][] = [
|
|
184
|
+
[0x09, 0x0d],
|
|
185
|
+
[0x20, 0x7e],
|
|
186
|
+
[0xa0, 0xb4],
|
|
187
|
+
[0xb6, 0xff],
|
|
188
|
+
[0x1_31, 0x1_31],
|
|
189
|
+
[0x1_52, 0x1_53],
|
|
190
|
+
[0x1_78, 0x1_78],
|
|
191
|
+
[0x20_00, 0x20_0f],
|
|
192
|
+
[0x20_13, 0x20_14],
|
|
193
|
+
[0x20_18, 0x20_1a],
|
|
194
|
+
[0x20_1c, 0x20_1e],
|
|
195
|
+
[0x20_20, 0x20_22],
|
|
196
|
+
[0x20_26, 0x20_26],
|
|
197
|
+
[0x20_28, 0x20_30],
|
|
198
|
+
[0x20_32, 0x20_33],
|
|
199
|
+
[0x20_39, 0x20_3a],
|
|
200
|
+
[0x20_44, 0x20_44],
|
|
201
|
+
[0x20_5f, 0x20_6f],
|
|
202
|
+
[0x20_ac, 0x20_ac],
|
|
203
|
+
[0x21_22, 0x21_22],
|
|
204
|
+
[0x21_91, 0x21_91],
|
|
205
|
+
[0x21_93, 0x21_93],
|
|
206
|
+
[0x22_12, 0x22_12],
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/** Emoji render as Twemoji images, never from a font. */
|
|
210
|
+
const EMOJI = /\p{Emoji_Presentation}|\p{Emoji_Component}/u;
|
|
211
|
+
|
|
212
|
+
/** Whether a card draws `codePoint` without any loaded font. */
|
|
213
|
+
const drawnWithoutFonts = (codePoint: number): boolean =>
|
|
214
|
+
BUILT_IN_GLYPHS.some(([from, to]) => codePoint >= from && codePoint <= to) ||
|
|
215
|
+
EMOJI.test(String.fromCodePoint(codePoint));
|
|
216
|
+
|
|
217
|
+
/** Whether `subset` claims `codePoint` (a subset without ranges claims all). */
|
|
218
|
+
const subsetClaims = (subset: FontSubset, codePoint: number): boolean =>
|
|
219
|
+
subset.ranges.length === 0 ||
|
|
220
|
+
subset.ranges.some(([from, to]) => codePoint >= from && codePoint <= to);
|
|
221
|
+
|
|
222
|
+
/** `family`'s subsets that claim any of `codePoints`. */
|
|
223
|
+
const familySubsets = (
|
|
224
|
+
subsets: FontSubset[],
|
|
225
|
+
family: string,
|
|
226
|
+
codePoints: number[]
|
|
227
|
+
): FontSubset[] =>
|
|
228
|
+
subsets.filter(
|
|
229
|
+
(subset) =>
|
|
230
|
+
subset.subsetOf === family &&
|
|
231
|
+
codePoints.some((codePoint) => subsetClaims(subset, codePoint))
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The scripts only the CJK families draw. Google slices those fonts by the
|
|
236
|
+
* glyphs each one really has, so the first family claiming such a character
|
|
237
|
+
* draws it. Other claims follow Google's shared subset definitions whatever
|
|
238
|
+
* the font holds — every family's `latin` claims `※`, Arabic's `symbols`
|
|
239
|
+
* claims `→` — so a claim there proves nothing.
|
|
240
|
+
*/
|
|
241
|
+
const CJK_SCRIPTS =
|
|
242
|
+
/[\p{Script=Bopomofo}\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
|
243
|
+
|
|
244
|
+
/** Whether a claim on `codePoint` means the claiming family draws it. */
|
|
245
|
+
const claimIsExact = (codePoint: number): boolean =>
|
|
246
|
+
CJK_SCRIPTS.test(String.fromCodePoint(codePoint));
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The fallback subsets a card's text needs. Nothing loads, and nothing is
|
|
250
|
+
* fetched, when the built-in font draws every glyph, so a Latin-only card
|
|
251
|
+
* builds offline. Otherwise only subsets claiming a glyph beyond the built-in
|
|
252
|
+
* font load (not every family's `latin`), and a CJK character stops at the
|
|
253
|
+
* first family that claims it: all four CJK families claim most Han, so left
|
|
254
|
+
* to `render` each would download its matching slices although only the
|
|
255
|
+
* first ever paints them.
|
|
256
|
+
*/
|
|
257
|
+
const fallbackSubsets = async (
|
|
258
|
+
fallbacks: OgGoogleFont[] | undefined,
|
|
259
|
+
node: Node
|
|
260
|
+
): Promise<FontSubset[]> => {
|
|
261
|
+
let missing = [...collectCodepoints(node)].filter(
|
|
262
|
+
(codePoint) => !drawnWithoutFonts(codePoint)
|
|
263
|
+
);
|
|
264
|
+
if (!fallbacks?.length || missing.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const subsets = await loadFonts(fallbacks);
|
|
268
|
+
const needed: FontSubset[] = [];
|
|
269
|
+
for (const family of new Set(subsets.map((subset) => subset.subsetOf))) {
|
|
270
|
+
const matching = familySubsets(subsets, family, missing);
|
|
271
|
+
needed.push(...matching);
|
|
272
|
+
missing = missing.filter(
|
|
273
|
+
(codePoint) =>
|
|
274
|
+
!(
|
|
275
|
+
claimIsExact(codePoint) &&
|
|
276
|
+
matching.some((subset) => subsetClaims(subset, codePoint))
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return needed;
|
|
281
|
+
};
|
|
282
|
+
|
|
163
283
|
/**
|
|
164
284
|
* A lazy loader for a local font file, matching the shape `render` accepts
|
|
165
285
|
* alongside Google subsets. Keyed by path so the shared renderer reads and
|
|
@@ -422,7 +542,11 @@ export const renderOgImage = async (
|
|
|
422
542
|
const fontSubsets = googleFamilies.length
|
|
423
543
|
? await loadFonts(googleFamilies)
|
|
424
544
|
: [];
|
|
425
|
-
const cardFonts = [
|
|
545
|
+
const cardFonts = [
|
|
546
|
+
...fontSubsets,
|
|
547
|
+
...localFonts.map(localFontLoader),
|
|
548
|
+
...(await fallbackSubsets(options.fallbacks, node)),
|
|
549
|
+
];
|
|
426
550
|
|
|
427
551
|
return render(node, {
|
|
428
552
|
fonts: cardFonts.length ? cardFonts : undefined,
|