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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { JsonObject } from "./json.ts";
|
|
11
11
|
import { asString, getPath, objectsIn } from "./json.ts";
|
|
12
|
-
import type { InlineMarks } from "./lower.ts";
|
|
12
|
+
import type { InlineMarks, InlinePart } from "./lower.ts";
|
|
13
13
|
import {
|
|
14
14
|
absoluteUrl,
|
|
15
15
|
blockquote,
|
|
@@ -17,10 +17,10 @@ import {
|
|
|
17
17
|
headingPrefix,
|
|
18
18
|
image,
|
|
19
19
|
joinBlocks,
|
|
20
|
+
linkParts,
|
|
20
21
|
listItem,
|
|
21
22
|
markdownDocument,
|
|
22
23
|
renderInline,
|
|
23
|
-
renderLink,
|
|
24
24
|
unsupported,
|
|
25
25
|
writesMdx,
|
|
26
26
|
} from "./lower.ts";
|
|
@@ -139,45 +139,50 @@ const embeddedEntry = (
|
|
|
139
139
|
);
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
-
const
|
|
142
|
+
const inlineParts = (
|
|
143
143
|
nodes: JsonObject[],
|
|
144
144
|
options: ContentfulRichTextOptions
|
|
145
|
-
):
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.map((node) => renderInlineNode(node, options))
|
|
149
|
-
.join("");
|
|
145
|
+
): InlinePart[] =>
|
|
146
|
+
// oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
|
|
147
|
+
nodes.flatMap((node) => inlineNodeParts(node, options));
|
|
150
148
|
|
|
151
|
-
const
|
|
149
|
+
const inlineNodeParts = (
|
|
152
150
|
node: JsonObject,
|
|
153
151
|
options: ContentfulRichTextOptions
|
|
154
|
-
):
|
|
152
|
+
): InlinePart[] => {
|
|
155
153
|
switch (asString(node.nodeType)) {
|
|
156
154
|
case "text": {
|
|
157
|
-
return
|
|
155
|
+
return [{ marks: marksOf(node), text: asString(node.value) ?? "" }];
|
|
158
156
|
}
|
|
159
157
|
case "hyperlink": {
|
|
160
|
-
return
|
|
161
|
-
|
|
158
|
+
return linkParts(
|
|
159
|
+
inlineParts(children(node), options),
|
|
162
160
|
asString(getPath(node, "data.uri"))
|
|
163
161
|
);
|
|
164
162
|
}
|
|
165
163
|
case "asset-hyperlink": {
|
|
166
|
-
return
|
|
167
|
-
|
|
164
|
+
return linkParts(
|
|
165
|
+
inlineParts(children(node), options),
|
|
168
166
|
linkedAsset(node, options)?.url
|
|
169
167
|
);
|
|
170
168
|
}
|
|
171
169
|
case "embedded-entry-inline": {
|
|
172
|
-
return embeddedEntry(node, options);
|
|
170
|
+
return [{ markdown: embeddedEntry(node, options) }];
|
|
173
171
|
}
|
|
174
172
|
default: {
|
|
175
173
|
// entry-hyperlink (no route to point at) and anything unknown: the label.
|
|
176
|
-
return
|
|
174
|
+
return inlineParts(children(node), options);
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
177
|
};
|
|
180
178
|
|
|
179
|
+
/** A block's inline children as Markdown, guarded as one block. */
|
|
180
|
+
const blockText = (
|
|
181
|
+
node: JsonObject,
|
|
182
|
+
options: ContentfulRichTextOptions
|
|
183
|
+
): string =>
|
|
184
|
+
guardBlockStart(renderInline(inlineParts(children(node), options)));
|
|
185
|
+
|
|
181
186
|
const renderList = (
|
|
182
187
|
items: JsonObject[],
|
|
183
188
|
marker: (index: number) => string,
|
|
@@ -198,12 +203,14 @@ const renderList = (
|
|
|
198
203
|
)
|
|
199
204
|
.join("\n");
|
|
200
205
|
|
|
206
|
+
// Each of a cell's paragraphs is a block as much as a page's is, so it goes
|
|
207
|
+
// through the same guard: two struck runs in one still meet as `~~~~`.
|
|
201
208
|
const cellText = (
|
|
202
209
|
cell: JsonObject,
|
|
203
210
|
options: ContentfulRichTextOptions
|
|
204
211
|
): string =>
|
|
205
212
|
children(cell)
|
|
206
|
-
.map((paragraph) =>
|
|
213
|
+
.map((paragraph) => blockText(paragraph, options))
|
|
207
214
|
.join(" ")
|
|
208
215
|
.replaceAll(CELL_BREAK, " ")
|
|
209
216
|
.replaceAll(CELL_PIPE, String.raw`\|`);
|
|
@@ -235,11 +242,11 @@ const renderBlock = (
|
|
|
235
242
|
const type = asString(node.nodeType) ?? "";
|
|
236
243
|
const heading = HEADING.exec(type)?.groups?.level;
|
|
237
244
|
if (heading) {
|
|
238
|
-
return `${headingPrefix(Number(heading))}${
|
|
245
|
+
return `${headingPrefix(Number(heading))}${blockText(node, options)}`;
|
|
239
246
|
}
|
|
240
247
|
switch (type) {
|
|
241
248
|
case "paragraph": {
|
|
242
|
-
return
|
|
249
|
+
return blockText(node, options);
|
|
243
250
|
}
|
|
244
251
|
case "blockquote": {
|
|
245
252
|
return blockquote(
|
|
@@ -6,6 +6,8 @@ import { glob } from "tinyglobby";
|
|
|
6
6
|
|
|
7
7
|
import { BlumeError } from "../diagnostics.ts";
|
|
8
8
|
import matter from "../frontmatter.ts";
|
|
9
|
+
import type { Diagnostic } from "../types.ts";
|
|
10
|
+
import { unloadablePathDiagnostic } from "./normalize.ts";
|
|
9
11
|
import type { ContentSource, SourceEntry, SourceLoadResult } from "./types.ts";
|
|
10
12
|
import {
|
|
11
13
|
baselineScanIgnore,
|
|
@@ -54,8 +56,24 @@ export const filesystemSource = (
|
|
|
54
56
|
});
|
|
55
57
|
files.sort();
|
|
56
58
|
|
|
59
|
+
// A file Astro's content loader can't read would publish a route that
|
|
60
|
+
// only ever renders "Page not found"; it is reported and left out.
|
|
61
|
+
const diagnostics: Diagnostic[] = [];
|
|
62
|
+
const loadable: string[] = [];
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const unloadable = unloadablePathDiagnostic(
|
|
65
|
+
relative(contentRoot, file),
|
|
66
|
+
file
|
|
67
|
+
);
|
|
68
|
+
if (unloadable) {
|
|
69
|
+
diagnostics.push(unloadable);
|
|
70
|
+
} else {
|
|
71
|
+
loadable.push(file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
const entries = await Promise.all(
|
|
58
|
-
|
|
76
|
+
loadable.map(async (file): Promise<SourceEntry> => {
|
|
59
77
|
const source = await readFile(file, "utf-8");
|
|
60
78
|
const ext = extname(file).toLowerCase();
|
|
61
79
|
const format = ext === ".mdx" ? "mdx" : "md";
|
|
@@ -73,7 +91,7 @@ export const filesystemSource = (
|
|
|
73
91
|
})
|
|
74
92
|
);
|
|
75
93
|
|
|
76
|
-
return { diagnostics
|
|
94
|
+
return { diagnostics, entries };
|
|
77
95
|
};
|
|
78
96
|
|
|
79
97
|
const validate = (): void => {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
snapshotCache,
|
|
17
17
|
} from "./cache.ts";
|
|
18
18
|
import { destination, escapeMarkdownText } from "./lower.ts";
|
|
19
|
+
import { REMOTE_TIMEOUT_MS } from "./remote.ts";
|
|
19
20
|
import type {
|
|
20
21
|
ContentSource,
|
|
21
22
|
SourceContext,
|
|
@@ -358,15 +359,33 @@ export const githubReleasesSource = (
|
|
|
358
359
|
(options.drafts || !release.draft) &&
|
|
359
360
|
(options.prereleases || !release.prerelease);
|
|
360
361
|
|
|
362
|
+
// Each page gets {@link REMOTE_TIMEOUT_MS}, body included: an API that
|
|
363
|
+
// accepts the connection and never answers would otherwise hold the scan,
|
|
364
|
+
// and so the build or every dev rescan, indefinitely. The timeout's error
|
|
365
|
+
// names the URL and the limit, so the empty-changelog warning says what
|
|
366
|
+
// stalled instead of only that an operation was aborted.
|
|
361
367
|
const fetchPage = async (page: number): Promise<GithubRelease[]> => {
|
|
362
368
|
const url = `${base}/repos/${options.owner}/${options.repo}/releases?per_page=${PER_PAGE}&page=${page}`;
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
369
|
+
try {
|
|
370
|
+
const res = await doFetch(url, {
|
|
371
|
+
headers: githubHeaders(),
|
|
372
|
+
signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS),
|
|
373
|
+
});
|
|
374
|
+
if (!res.ok) {
|
|
375
|
+
throw new Error(`${url} -> ${res.status}`);
|
|
376
|
+
}
|
|
377
|
+
// SAFETY: GitHub's releases endpoint returns a JSON array of release
|
|
378
|
+
// objects; `GithubRelease` models only the fields the adapter reads.
|
|
379
|
+
return (await res.json()) as GithubRelease[];
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`${url} did not respond within ${REMOTE_TIMEOUT_MS / 1000}s`,
|
|
384
|
+
{ cause: error }
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
throw error;
|
|
366
388
|
}
|
|
367
|
-
// SAFETY: GitHub's releases endpoint returns a JSON array of release
|
|
368
|
-
// objects; `GithubRelease` models only the fields the adapter reads.
|
|
369
|
-
return (await res.json()) as GithubRelease[];
|
|
370
389
|
};
|
|
371
390
|
|
|
372
391
|
const fetchReleases = async (): Promise<GithubRelease[]> => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { JsonObject } from "./json.ts";
|
|
9
9
|
import { asNumber, asObject, asString, getPath, objectsIn } from "./json.ts";
|
|
10
|
-
import type { InlineMarks } from "./lower.ts";
|
|
10
|
+
import type { InlineMarks, InlinePart } from "./lower.ts";
|
|
11
11
|
import {
|
|
12
12
|
absoluteUrl,
|
|
13
13
|
blockquote,
|
|
@@ -15,10 +15,10 @@ import {
|
|
|
15
15
|
headingPrefix,
|
|
16
16
|
image,
|
|
17
17
|
indent,
|
|
18
|
+
linkParts,
|
|
18
19
|
listItem,
|
|
19
20
|
markdownDocument,
|
|
20
21
|
renderInline,
|
|
21
|
-
renderLink,
|
|
22
22
|
unsupported,
|
|
23
23
|
writesMdx,
|
|
24
24
|
} from "./lower.ts";
|
|
@@ -67,48 +67,53 @@ const customBlock = (node: JsonObject, options: LexicalOptions): string => {
|
|
|
67
67
|
);
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
nodes
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
const inlineParts = (
|
|
71
|
+
nodes: JsonObject[],
|
|
72
|
+
options: LexicalOptions
|
|
73
|
+
): InlinePart[] =>
|
|
74
|
+
// oxlint-disable-next-line no-use-before-define -- mutual recursion: a link's label is itself inline content
|
|
75
|
+
nodes.flatMap((node) => inlineNodeParts(node, options));
|
|
75
76
|
|
|
76
|
-
const
|
|
77
|
+
const inlineNodeParts = (
|
|
77
78
|
node: JsonObject,
|
|
78
79
|
options: LexicalOptions
|
|
79
|
-
):
|
|
80
|
+
): InlinePart[] => {
|
|
80
81
|
switch (typeOf(node)) {
|
|
81
82
|
case "text": {
|
|
82
|
-
return
|
|
83
|
+
return [{ marks: marksOf(node), text: asString(node.text) ?? "" }];
|
|
83
84
|
}
|
|
84
85
|
case "linebreak": {
|
|
85
|
-
return "\n";
|
|
86
|
+
return [{ markdown: "\n" }];
|
|
86
87
|
}
|
|
87
88
|
case "tab": {
|
|
88
|
-
return "\t";
|
|
89
|
+
return [{ markdown: "\t" }];
|
|
89
90
|
}
|
|
90
91
|
case "link":
|
|
91
92
|
case "autolink": {
|
|
92
93
|
// `fields.url` on Payload 3; `url` on older Lexical payloads. An
|
|
93
94
|
// internal link (`linkType: "internal"`) carries a document, not a
|
|
94
95
|
// URL, so its label stays.
|
|
95
|
-
return
|
|
96
|
-
|
|
96
|
+
return linkParts(
|
|
97
|
+
inlineParts(children(node), options),
|
|
97
98
|
asString(getPath(node, "fields.url")) ?? asString(node.url)
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
case "inlineBlock": {
|
|
101
|
-
return customBlock(node, options);
|
|
102
|
+
return [{ markdown: customBlock(node, options) }];
|
|
102
103
|
}
|
|
103
104
|
default: {
|
|
104
|
-
return
|
|
105
|
+
return inlineParts(children(node), options);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
};
|
|
108
109
|
|
|
110
|
+
/** Inline nodes as Markdown, guarded as one block. */
|
|
111
|
+
const inlineText = (nodes: JsonObject[], options: LexicalOptions): string =>
|
|
112
|
+
guardBlockStart(renderInline(inlineParts(nodes, options)));
|
|
113
|
+
|
|
109
114
|
/** A text block's inline children, joined and guarded as one block. */
|
|
110
115
|
const blockText = (node: JsonObject, options: LexicalOptions): string =>
|
|
111
|
-
|
|
116
|
+
inlineText(children(node), options);
|
|
112
117
|
|
|
113
118
|
const renderUpload = (node: JsonObject, options: LexicalOptions): string => {
|
|
114
119
|
const value = asObject(node.value);
|
|
@@ -145,7 +150,7 @@ const renderList = (node: JsonObject, options: LexicalOptions): string => {
|
|
|
145
150
|
if (listType === "check") {
|
|
146
151
|
check = item.checked === true ? "[x] " : "[ ] ";
|
|
147
152
|
}
|
|
148
|
-
const line = `${check}${
|
|
153
|
+
const line = `${check}${inlineText(inline, options)}`;
|
|
149
154
|
items.push(listItem(lastMarker, sub ? `${line}\n${sub}` : line));
|
|
150
155
|
index += 1;
|
|
151
156
|
}
|
|
@@ -9,13 +9,29 @@ import { isSafeHref } from "../safe-href.ts";
|
|
|
9
9
|
|
|
10
10
|
// Markdown/raw-HTML structure characters. Rich-text leaves are *plain text* —
|
|
11
11
|
// formatting arrives as marks, never as syntax in the text — so a literal
|
|
12
|
-
// `*`, `_`, `[`, backtick, `~`, or `<` typed in the CMS must render as
|
|
13
|
-
// itself. Unescaped, it opened emphasis
|
|
14
|
-
// let CMS prose inject raw HTML into
|
|
15
|
-
// expression once an entry is written
|
|
12
|
+
// `*`, `_`, `[`, backtick, `~`, `^`, or `<` typed in the CMS must render as
|
|
13
|
+
// itself. Unescaped, it opened emphasis, a code span, or a superscript
|
|
14
|
+
// (`2^10 and 2^20`) mid-paragraph, and `<` let CMS prose inject raw HTML into
|
|
15
|
+
// the rendered page. `{` and `}` open an expression once an entry is written
|
|
16
|
+
// as MDX (see {@link writesMdx}), and `$$` opens math there. CommonMark
|
|
16
17
|
// backslash-escapes every ASCII punctuation character, so `\*` is always the
|
|
17
18
|
// literal asterisk, in `.md` and `.mdx` alike.
|
|
18
|
-
const MARKDOWN_SPECIALS = /[\\`*_{}[\]
|
|
19
|
+
const MARKDOWN_SPECIALS = /[\\`*_{}[\]~<$^]/gu;
|
|
20
|
+
|
|
21
|
+
// MDX reads a colon ahead of a letter or digit as a text directive (`10:30`,
|
|
22
|
+
// `og:image`, `pets:read`), so that colon is escaped too. One inside a bare
|
|
23
|
+
// URL (`http://localhost:3000`) stays as written: an escape there would end
|
|
24
|
+
// the autolink early.
|
|
25
|
+
const DIRECTIVE_COLON = /(?<!(?:[a-z][\w+.-]*:\/\/|www\.)\S*):(?=[a-z\d])/giu;
|
|
26
|
+
|
|
27
|
+
// GFM autolinks a bare `http://`, `https://`, or `www.` URL in prose, taking
|
|
28
|
+
// its characters verbatim up to the next space or `<`: an escape inside one
|
|
29
|
+
// reaches the href as a backslash (`a\_b`), so prose leaves a bare URL's
|
|
30
|
+
// characters as written. That includes a trailing `*`, `_`, or `~`, which
|
|
31
|
+
// GFM leaves out of the link: escaped, its backslash would still land inside.
|
|
32
|
+
// A link label is the exception — GFM links nothing inside one, so there
|
|
33
|
+
// every special is escaped (see `linkParts`).
|
|
34
|
+
const BARE_URL = /\b(?:https?:\/\/|www\.)[\w-][^\s<]*/giu;
|
|
19
35
|
|
|
20
36
|
// An entity or numeric character reference (`©`, `&`) is decoded
|
|
21
37
|
// even in plain text, so one typed in the CMS would render as the character
|
|
@@ -61,15 +77,37 @@ const escapeEsmStart = (text: string): string =>
|
|
|
61
77
|
(keyword: string) => `&#${keyword.codePointAt(0)};${keyword.slice(1)}`
|
|
62
78
|
);
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
const escapeSpecials = (text: string): string =>
|
|
81
|
+
text.replaceAll(MARKDOWN_SPECIALS, String.raw`\$&`);
|
|
82
|
+
|
|
83
|
+
/** The escapes that apply everywhere, once the specials are escaped. */
|
|
84
|
+
const escapeRest = (text: string): string =>
|
|
65
85
|
escapeEsmStart(
|
|
66
86
|
escapeBlockStart(
|
|
67
87
|
text
|
|
68
|
-
.replaceAll(
|
|
88
|
+
.replaceAll(DIRECTIVE_COLON, String.raw`\:`)
|
|
69
89
|
.replaceAll(CHARACTER_REFERENCE, String.raw`\&`)
|
|
70
90
|
)
|
|
71
91
|
);
|
|
72
92
|
|
|
93
|
+
/**
|
|
94
|
+
* CMS text as Markdown that renders as written, wherever it lands — a link
|
|
95
|
+
* label included, where GFM autolinks nothing, so every special is escaped.
|
|
96
|
+
*/
|
|
97
|
+
export const escapeMarkdownText = (text: string): string =>
|
|
98
|
+
escapeRest(escapeSpecials(text));
|
|
99
|
+
|
|
100
|
+
/** CMS prose as {@link escapeMarkdownText}, except inside a bare URL. */
|
|
101
|
+
const escapeProse = (text: string): string => {
|
|
102
|
+
let out = "";
|
|
103
|
+
let last = 0;
|
|
104
|
+
for (const match of text.matchAll(BARE_URL)) {
|
|
105
|
+
out += escapeSpecials(text.slice(last, match.index)) + match[0];
|
|
106
|
+
last = match.index + match[0].length;
|
|
107
|
+
}
|
|
108
|
+
return escapeRest(out + escapeSpecials(text.slice(last)));
|
|
109
|
+
};
|
|
110
|
+
|
|
73
111
|
/** The marks a lowerer can put on an inline run. */
|
|
74
112
|
export interface InlineMarks {
|
|
75
113
|
bold?: boolean;
|
|
@@ -78,6 +116,20 @@ export interface InlineMarks {
|
|
|
78
116
|
strike?: boolean;
|
|
79
117
|
}
|
|
80
118
|
|
|
119
|
+
/** A run of CMS text and the marks on it. */
|
|
120
|
+
export interface InlineRun {
|
|
121
|
+
marks: InlineMarks;
|
|
122
|
+
text: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Inline Markdown a lowerer rendered itself: a link, an embed, a break. */
|
|
126
|
+
export interface InlineMarkdown {
|
|
127
|
+
markdown: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One piece of a block's inline content, in order. */
|
|
131
|
+
export type InlinePart = InlineMarkdown | InlineRun;
|
|
132
|
+
|
|
81
133
|
const BACKTICK_RUN = /`+/gu;
|
|
82
134
|
|
|
83
135
|
/** The longest run of backticks in `text`, 0 when there is none. */
|
|
@@ -100,47 +152,153 @@ export const codeSpan = (code: string): string => {
|
|
|
100
152
|
return `${fence}${pad}${code}${pad}${fence}`;
|
|
101
153
|
};
|
|
102
154
|
|
|
155
|
+
// Two struck runs side by side meet as `~~~~`: a closer and an opener that
|
|
156
|
+
// GFM reads as one four-tilde run, so both print as literal tildes. A literal
|
|
157
|
+
// `~` is escaped outside a bare URL, so an unescaped `~~~~` outside a code
|
|
158
|
+
// span, a link destination, or a bare URL is exactly that seam, and dropping
|
|
159
|
+
// it merges the runs into one strikethrough. The other alternatives only step
|
|
160
|
+
// over escapes, code spans, destinations, and bare URLs so the seam is never
|
|
161
|
+
// matched inside them.
|
|
162
|
+
const SEAM_STEPS =
|
|
163
|
+
/\\[\s\S]|(?<ticks>`+)[\s\S]*?(?<!`)\k<ticks>(?!`)|\]\((?:<(?:\\.|[^\\<>\n])*>|(?:\\.|[^\\\s)])+)\)/u;
|
|
164
|
+
const STRIKE_SEAM = new RegExp(
|
|
165
|
+
`${SEAM_STEPS.source}|${BARE_URL.source}|~~~~`,
|
|
166
|
+
"giu"
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// A heading that ends in a space and a run of `#` reads that run as the
|
|
170
|
+
// optional closing sequence and drops it (`## Issue #` renders "Issue").
|
|
171
|
+
const CLOSING_HASHES = /(?<=[ \t])#+(?=[ \t]*$)/u;
|
|
172
|
+
|
|
103
173
|
/**
|
|
104
174
|
* A block's inline text, guarded where only the whole block can tell. Runs
|
|
105
175
|
* are escaped one at a time, but "import" ending one run and a space or code
|
|
106
176
|
* span opening the next form an MDX `import` statement only once they are
|
|
107
|
-
* joined,
|
|
108
|
-
* an indented code block
|
|
109
|
-
*
|
|
177
|
+
* joined, whitespace the first runs put in front of a block turns it into
|
|
178
|
+
* an indented code block, two struck runs fuse into literal tildes, and a
|
|
179
|
+
* trailing ` #` is a heading's closing sequence. Apply it to the joined text
|
|
180
|
+
* of a paragraph, heading, quote, or list item — never to code.
|
|
110
181
|
*/
|
|
111
182
|
export const guardBlockStart = (text: string): string =>
|
|
112
|
-
escapeEsmStart(
|
|
183
|
+
escapeEsmStart(
|
|
184
|
+
text
|
|
185
|
+
.replaceAll(BLOCK_INDENT, "")
|
|
186
|
+
.replaceAll(STRIKE_SEAM, (match: string) =>
|
|
187
|
+
match === "~~~~" ? "" : match
|
|
188
|
+
)
|
|
189
|
+
.replace(CLOSING_HASHES, String.raw`\$&`)
|
|
190
|
+
);
|
|
113
191
|
|
|
114
192
|
const EDGE_SPACE = /^(?<lead>\s*)(?<body>[\s\S]*?)(?<trail>\s*)$/u;
|
|
115
193
|
|
|
116
194
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* escaped first. Whitespace at either edge moves outside the delimiters,
|
|
120
|
-
* since `** bold **` is not strong emphasis but `**bold**` is — editors
|
|
121
|
-
* routinely bold a word together with the space after it.
|
|
195
|
+
* `text` with `render` applied between its edge whitespace, or as it is when
|
|
196
|
+
* it is only whitespace.
|
|
122
197
|
*/
|
|
123
|
-
|
|
198
|
+
const betweenEdges = (
|
|
199
|
+
text: string,
|
|
200
|
+
render: (body: string) => string
|
|
201
|
+
): string => {
|
|
124
202
|
const match = EDGE_SPACE.exec(text);
|
|
125
|
-
const lead = match?.groups?.lead ?? "";
|
|
126
203
|
const body = match?.groups?.body ?? "";
|
|
127
|
-
const trail = match?.groups?.trail ?? "";
|
|
128
204
|
if (body === "") {
|
|
129
205
|
return text;
|
|
130
206
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
207
|
+
return `${match?.groups?.lead ?? ""}${render(body)}${match?.groups?.trail ?? ""}`;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const MARK_NAMES = ["bold", "code", "italic", "strike"] as const;
|
|
211
|
+
|
|
212
|
+
const sameMarks = (
|
|
213
|
+
a: InlineMarks,
|
|
214
|
+
b: InlineMarks,
|
|
215
|
+
names: readonly (keyof InlineMarks)[] = MARK_NAMES
|
|
216
|
+
): boolean => names.every((mark) => Boolean(a[mark]) === Boolean(b[mark]));
|
|
217
|
+
|
|
218
|
+
/** A stretch of neighboring runs that share marks, and those marks. */
|
|
219
|
+
interface RunGroup {
|
|
220
|
+
marks: InlineMarks;
|
|
221
|
+
runs: InlineRun[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Runs split into stretches of neighbors that share the `names` marks. */
|
|
225
|
+
const groupRuns = (
|
|
226
|
+
runs: InlineRun[],
|
|
227
|
+
names: readonly (keyof InlineMarks)[] = MARK_NAMES
|
|
228
|
+
): RunGroup[] => {
|
|
229
|
+
const groups: RunGroup[] = [];
|
|
230
|
+
for (const run of runs) {
|
|
231
|
+
const group = groups.at(-1);
|
|
232
|
+
if (group && sameMarks(group.marks, run.marks, names)) {
|
|
233
|
+
group.runs.push(run);
|
|
234
|
+
} else {
|
|
235
|
+
groups.push({ marks: run.marks, runs: [run] });
|
|
236
|
+
}
|
|
137
237
|
}
|
|
138
|
-
|
|
139
|
-
|
|
238
|
+
return groups;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Consecutive text runs as Markdown. Neighbors with the same bold, italic,
|
|
243
|
+
* and strikethrough share one set of delimiters — rendered one run at a
|
|
244
|
+
* time, two bold runs met as `****` and two italic ones as `**`, which print
|
|
245
|
+
* as literal asterisks. Code stays verbatim inside the backticks (an escape
|
|
246
|
+
* would render as a backslash), and neighboring code joins into one span;
|
|
247
|
+
* everything else is escaped. Whitespace at either edge moves outside the
|
|
248
|
+
* delimiters, since `** bold **` is not strong emphasis but `**bold**` is —
|
|
249
|
+
* editors routinely bold a word together with the space after it.
|
|
250
|
+
*/
|
|
251
|
+
const renderRuns = (
|
|
252
|
+
runs: InlineRun[],
|
|
253
|
+
escape: (text: string) => string
|
|
254
|
+
): string =>
|
|
255
|
+
groupRuns(runs, ["bold", "italic", "strike"])
|
|
256
|
+
.map(({ marks, runs: shared }) => {
|
|
257
|
+
const inner = groupRuns(shared)
|
|
258
|
+
.map((same) =>
|
|
259
|
+
betweenEdges(same.runs.map((run) => run.text).join(""), (body) =>
|
|
260
|
+
same.marks.code ? codeSpan(body) : escape(body)
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
.join("");
|
|
264
|
+
return betweenEdges(inner, (body) => {
|
|
265
|
+
let out = body;
|
|
266
|
+
if (marks.bold) {
|
|
267
|
+
out = `**${out}**`;
|
|
268
|
+
}
|
|
269
|
+
if (marks.italic) {
|
|
270
|
+
out = `*${out}*`;
|
|
271
|
+
}
|
|
272
|
+
if (marks.strike) {
|
|
273
|
+
out = `~~${out}~~`;
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
});
|
|
277
|
+
})
|
|
278
|
+
.join("");
|
|
279
|
+
|
|
280
|
+
/** Inline parts as Markdown, each stretch of runs through {@link renderRuns}. */
|
|
281
|
+
const renderParts = (
|
|
282
|
+
parts: InlinePart[],
|
|
283
|
+
escape: (text: string) => string
|
|
284
|
+
): string => {
|
|
285
|
+
let out = "";
|
|
286
|
+
let runs: InlineRun[] = [];
|
|
287
|
+
for (const part of parts) {
|
|
288
|
+
if ("markdown" in part) {
|
|
289
|
+
out += renderRuns(runs, escape) + part.markdown;
|
|
290
|
+
runs = [];
|
|
291
|
+
} else {
|
|
292
|
+
runs.push(part);
|
|
293
|
+
}
|
|
140
294
|
}
|
|
141
|
-
return
|
|
295
|
+
return out + renderRuns(runs, escape);
|
|
142
296
|
};
|
|
143
297
|
|
|
298
|
+
/** A block's inline content as Markdown. */
|
|
299
|
+
export const renderInline = (parts: InlinePart[]): string =>
|
|
300
|
+
renderParts(parts, escapeProse);
|
|
301
|
+
|
|
144
302
|
// A destination with whitespace, a control character, or parentheses ends
|
|
145
303
|
// early in `[label](…)`; CommonMark's pointy-bracket form carries it intact.
|
|
146
304
|
// oxlint-disable-next-line no-control-regex -- control characters are exactly what the bare form refuses.
|
|
@@ -167,13 +325,22 @@ export const destination = (url: string): string => {
|
|
|
167
325
|
};
|
|
168
326
|
|
|
169
327
|
/**
|
|
170
|
-
* A Markdown link, or the
|
|
171
|
-
* CMS content is the editor's, not the site author's, so
|
|
172
|
-
* isn't a web, mail, or relative address (`javascript:`,
|
|
173
|
-
* becomes a clickable link on the docs site (see
|
|
328
|
+
* A Markdown link over inline parts, or the parts alone when the target is
|
|
329
|
+
* missing — or unsafe: CMS content is the editor's, not the site author's, so
|
|
330
|
+
* a destination that isn't a web, mail, or relative address (`javascript:`,
|
|
331
|
+
* `data:`) never becomes a clickable link on the docs site (see
|
|
332
|
+
* `safe-links.ts`). The parts stay parts then, so their runs merge with their
|
|
333
|
+
* neighbors'. A label escapes a bare URL too: GFM autolinks nothing inside a
|
|
334
|
+
* link, so its specials would read as syntax.
|
|
174
335
|
*/
|
|
175
|
-
export const
|
|
176
|
-
href && isSafeHref(href)
|
|
336
|
+
export const linkParts = (parts: InlinePart[], href?: string): InlinePart[] =>
|
|
337
|
+
href && isSafeHref(href)
|
|
338
|
+
? [
|
|
339
|
+
{
|
|
340
|
+
markdown: `[${renderParts(parts, escapeMarkdownText)}](${destination(href)})`,
|
|
341
|
+
},
|
|
342
|
+
]
|
|
343
|
+
: parts;
|
|
177
344
|
|
|
178
345
|
/** The ATX prefix for a heading level, clamped to Markdown's six. */
|
|
179
346
|
export const headingPrefix = (level: number): string =>
|