@ascribed/astro 0.4.4 → 0.4.5-next.34

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/README.md CHANGED
@@ -71,7 +71,7 @@ A versioned build (`versions` in `ascribe.toml`) is one collection with every li
71
71
 
72
72
  A page's Markdown version has no head, so its rules are response headers, which each build lists in `_ascribe/headers.json`: `X-Robots-Tag` when the page isn't listed, and a canonical `Link` to the page. `astro dev` sends them. With `[consumer] host = "netlify"`, each build writes them in Netlify's format too, at `_ascribe/host/_headers`, and `astro build` merges every project's into the site's `_headers`; a site with its own `public/_headers` keeps it, and the build fails with the lines it lacks. The same setting has `astro build` write the site's `_redirects`, from the moves each project's `ascribe.addresses` records, ahead of a site's own `public/_redirects`.
73
73
 
74
- `npx @ascribed/astro init` writes those files into an Astro project once, as yours from then on: the collection (`src/content.config.ts`), the route (`src/pages/[...slug].astro`), and a layout that loads the elements and draws the sidebar (`src/layouts/Docs.astro`). It never writes over a file. `--project <dir>` names the folder holding `ascribe.toml`, and `--build <name>` the build, when there are several. The layout's sidebar comes from `sidebar(entries, { entry })` in `@ascribed/astro/content`: the project's navigation, in `ascribe.nav`'s order or the default one, from the build's `_ascribe/nav.json`, with no list to keep. `neighbors(entry, entries)` gives a page's previous and next in the same order, for links at its foot. A site's own list, `sidebar(entries, { nav })`, overrides the navigation: its groups, in its order, and the build fails when the list leaves out a page or names one that isn't there; `neighbors` takes it too.
74
+ `npx @ascribed/astro init` writes those files into an Astro project once, as yours from then on: the collection (`src/content.config.ts`), the route (`src/pages/[...slug].astro`), a layout that loads the elements and draws the sidebar (`src/layouts/Docs.astro`), and the layout's stylesheet (`src/styles/docs.css`), whose colors and spaces are the element library's `--ascribe-*` properties. It never writes over a file. `--project <dir>` names the folder holding `ascribe.toml`, and `--build <name>` the build, when there are several. The layout's sidebar comes from `sidebar(entries, { entry })` in `@ascribed/astro/content`: the project's navigation, in `ascribe.nav`'s order or the default one, from the build's `_ascribe/nav.json`, with no list to keep. `neighbors(entry, entries)` gives a page's previous and next in the same order, for links at its foot. A site's own list, `sidebar(entries, { nav })`, overrides the navigation: its groups, in its order, and the build fails when the list leaves out a page or names one that isn't there; `neighbors` takes it too.
75
75
 
76
76
  The [Astro guide](https://ascribed-dev.com/go/astro-guide/) walks through a site: `ascribe.toml`, the collection, the route, and the layout. Its section's how-tos put [a home page in front of the docs](https://ascribed-dev.com/go/a-home-page-in-front-of-the-docs/), or [several projects in one site](https://ascribed-dev.com/go/several-projects-in-one-site/). The [`@ascribed/astro` reference](https://ascribed-dev.com/go/astro-reference/) has the options, the components, what the integration does in `astro build` and `astro dev`, search engines and crawlers, and [review in the site preview](https://ascribed-dev.com/go/review-in-the-site-preview/). [Review](https://ascribed-dev.com/go/review-guide/) walks through reviewing a pull request on your site.
77
77
 
@@ -100,6 +100,7 @@ The [Astro guide](https://ascribed-dev.com/go/astro-guide/) walks through a site
100
100
  | `src/Availability.astro` | A page's availability as the element library's badge |
101
101
  | `src/Versions.astro`, `src/VersionBanner.astro`, `src/versions.ts` | A versioned build's lines, from `<output-dir>/_ascribe/versions.json`, the list of the lines that have a page, and an older line's banner |
102
102
  | `src/code-titles.ts` | The Shiki transformer that shows code block titles and tested lines |
103
+ | `src/code-languages.ts` | The Shiki transformer that gives a code block's `<code>` its language as a class |
103
104
  | `src/Permalink.astro`, `src/permalinks.ts` | The route at each permalink's address, and its pages, from the site output's `_ascribe/permalinks.json` |
104
105
  | `src/MissingPermalink.astro` | The explanation on a site's 404 page for an address under the permalinks' path that matches nothing |
105
106
  | `src/schema.ts` | Copies the generated schema inside the Astro root |
@@ -0,0 +1,10 @@
1
+ import type { AstroUserConfig } from "astro";
2
+ type ShikiTransformer = NonNullable<NonNullable<NonNullable<AstroUserConfig["markdown"]>["shikiConfig"]>["transformers"]>[number];
3
+ /**
4
+ * The language Shiki highlighted the block as, the `data-language` Astro
5
+ * gives its <pre>. Astro highlights a block with no language, and one in a
6
+ * language Shiki doesn't know, as `plaintext`, which has no class, as a block
7
+ * with no language has none before highlighting.
8
+ */
9
+ export declare const codeLanguages: ShikiTransformer;
10
+ export {};
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The language Shiki highlighted the block as, the `data-language` Astro
3
+ * gives its <pre>. Astro highlights a block with no language, and one in a
4
+ * language Shiki doesn't know, as `plaintext`, which has no class, as a block
5
+ * with no language has none before highlighting.
6
+ */
7
+ export const codeLanguages = {
8
+ name: "@ascribed/astro:code-languages",
9
+ code(node) {
10
+ const lang = this.options.lang;
11
+ if (typeof lang !== "string" || lang === "" || lang === "plaintext")
12
+ return;
13
+ this.addClassToHast(node, `language-${lang}`);
14
+ },
15
+ };
16
+ //# sourceMappingURL=code-languages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-languages.js","sourceRoot":"","sources":["../src/code-languages.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAqB;IAC7C,IAAI,EAAE,gCAAgC;IACtC,IAAI,CAAC,IAAI;QACP,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;QAC/B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,WAAW;YAAE,OAAO;QAC5E,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;CACF,CAAC","sourcesContent":["// A code block's language as a class on its <code>, `language-sh`, as\n// CommonMark renders it before Shiki highlights it. Shiki's output keeps the\n// language only in the <pre>'s `data-language`, which HTML-to-text and\n// HTML-to-Markdown converters don't read, so their fences came out with no\n// language (decision #1038, site-render contract §5.2). The integration adds\n// this Shiki transformer, so it's the same in `astro dev` and the build.\nimport type { AstroUserConfig } from \"astro\";\n\ntype ShikiTransformer = NonNullable<\n NonNullable<NonNullable<AstroUserConfig[\"markdown\"]>[\"shikiConfig\"]>[\"transformers\"]\n>[number];\n\n/**\n * The language Shiki highlighted the block as, the `data-language` Astro\n * gives its <pre>. Astro highlights a block with no language, and one in a\n * language Shiki doesn't know, as `plaintext`, which has no class, as a block\n * with no language has none before highlighting.\n */\nexport const codeLanguages: ShikiTransformer = {\n name: \"@ascribed/astro:code-languages\",\n code(node) {\n const lang = this.options.lang;\n if (typeof lang !== \"string\" || lang === \"\" || lang === \"plaintext\") return;\n this.addClassToHast(node, `language-${lang}`);\n },\n};\n"]}
@@ -1,12 +1,56 @@
1
1
  import { CLASS_CODE_TESTED } from "./names.js";
2
2
  /**
3
- * The value of `key="…"` in a code block's meta, if it has one. The meta has
4
- * its Markdown escapes taken off (`title="say \"hi\""` arrives as
5
- * `title="say "hi""`), so the value ends at a quote followed by a space or
6
- * the end, and a quote inside it survives.
3
+ * The value of the first `key=value` pair in a code block's meta, the info
4
+ * string past its language, when it isn't empty. The meta has its Markdown
5
+ * escapes taken off, and is read as SPEC §3.3.1 reads an info string, as
6
+ * `ascribe_core::info_string` does: words parted by spaces and tabs, a pair's
7
+ * value a token or a quoted string in which `\"` and `\\` escape, and a pair
8
+ * followed by a space, a tab, or the end. Any other word isn't a pair.
7
9
  */
8
10
  function metaValue(meta, key) {
9
- return new RegExp(`(?:^|\\s)${key}="(.*?)"(?=\\s|$)`).exec(meta)?.[1] || undefined;
11
+ let i = 0;
12
+ while (i < meta.length) {
13
+ if (meta[i] === " " || meta[i] === "\t") {
14
+ i += 1;
15
+ continue;
16
+ }
17
+ const pair = /^([a-z][a-z0-9-]*)=/.exec(meta.slice(i));
18
+ let end = wordEnd(meta, i);
19
+ let value;
20
+ if (pair !== null) {
21
+ let j = i + pair[0].length;
22
+ if (meta[j] === '"') {
23
+ let text = "";
24
+ j += 1;
25
+ while (j < meta.length && meta[j] !== '"') {
26
+ if (meta[j] === "\\" && (meta[j + 1] === '"' || meta[j + 1] === "\\"))
27
+ j += 1;
28
+ text += meta[j];
29
+ j += 1;
30
+ }
31
+ if (j < meta.length && (j + 1 === meta.length || isSpace(meta[j + 1]))) {
32
+ value = text;
33
+ end = j + 1;
34
+ }
35
+ }
36
+ else if (j < end && !meta.slice(j, end).includes('"')) {
37
+ value = meta.slice(j, end);
38
+ }
39
+ }
40
+ if (value !== undefined && pair?.[1] === key)
41
+ return value || undefined;
42
+ i = end;
43
+ }
44
+ return undefined;
45
+ }
46
+ function isSpace(c) {
47
+ return c === " " || c === "\t";
48
+ }
49
+ function wordEnd(text, from) {
50
+ let i = from;
51
+ while (i < text.length && !isSpace(text[i]))
52
+ i += 1;
53
+ return i;
10
54
  }
11
55
  /** The `title="…"` in a code block's meta, if it has one. */
12
56
  export function codeTitle(meta) {
@@ -1 +1 @@
1
- {"version":3,"file":"code-titles.js","sourceRoot":"","sources":["../src/code-titles.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAM/C;;;;;GAKG;AACH,SAAS,SAAS,CAAC,IAAY,EAAE,GAAW;IAC1C,OAAO,IAAI,MAAM,CAAC,YAAY,GAAG,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;AACrF,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAqB;IAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACtE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE,SAAS,EAAE,CAAC,iBAAiB,CAAC,EAAE;YAC9C,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,EAAE,SAAS,EAAE,CAAC,YAAY,CAAC,EAAE;QACzC,QAAQ,EAAE;YACR,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE;YAC7E,KAAK;SACN;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,UAAU,GAAqB;IAC1C,IAAI,EAAE,6BAA6B;IACnC,IAAI,CAAC,IAAI;QACP,MAAM,IAAI,GAAI,IAAI,CAAC,OAAO,CAAC,IAAuC,EAAE,KAAK,IAAI,EAAE,CAAC;QAChF,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;QAClE,gEAAgE;QAChE,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO;QAC1D,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QACpC,IAAI,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,MAAM,CAAC,CAAC;IACrD,CAAC;CACF,CAAC","sourcesContent":["// A code block's caption, shown above it: its title, and what a tested\n// example was tested with. A `@snippet`'s title is in the code block's info\n// string, as `title=\"…\"`, and its tested line as `tested=\"…\"` (SPEC §4.8), and\n// Astro's own code highlighting ignores both (issue #106), so the integration\n// adds this Shiki transformer, which puts such a block in a\n// <figure class=\"code-title\"> with a <figcaption>: the title, then the tested\n// line in a <div class=\"ascribe-code-tested\"> of its own, whose \"Tested with\"\n// lead-in is the element library's stylesheet's (site-render contract §5.1).\nimport type { AstroUserConfig } from \"astro\";\nimport type { Element, ElementContent } from \"hast\";\nimport { CLASS_CODE_TESTED } from \"./names.js\";\n\ntype ShikiTransformer = NonNullable<\n NonNullable<NonNullable<AstroUserConfig[\"markdown\"]>[\"shikiConfig\"]>[\"transformers\"]\n>[number];\n\n/**\n * The value of `key=\"…\"` in a code block's meta, if it has one. The meta has\n * its Markdown escapes taken off (`title=\"say \\\"hi\\\"\"` arrives as\n * `title=\"say \"hi\"\"`), so the value ends at a quote followed by a space or\n * the end, and a quote inside it survives.\n */\nfunction metaValue(meta: string, key: string): string | undefined {\n return new RegExp(`(?:^|\\\\s)${key}=\"(.*?)\"(?=\\\\s|$)`).exec(meta)?.[1] || undefined;\n}\n\n/** The `title=\"…\"` in a code block's meta, if it has one. */\nexport function codeTitle(meta: string): string | undefined {\n return metaValue(meta, \"title\");\n}\n\n/** The `tested=\"…\"` in a code block's meta, if it has one. */\nexport function codeTested(meta: string): string | undefined {\n return metaValue(meta, \"tested\");\n}\n\n/**\n * The block, in a figure whose caption holds its title and its tested line;\n * `undefined` when it has neither.\n */\nexport function captioned(meta: string, block: ElementContent): Element | undefined {\n const title = codeTitle(meta);\n const tested = codeTested(meta);\n if (title === undefined && tested === undefined) return undefined;\n const caption: ElementContent[] = [];\n if (title !== undefined) caption.push({ type: \"text\", value: title });\n if (tested !== undefined) {\n caption.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [CLASS_CODE_TESTED] },\n children: [{ type: \"text\", value: tested }],\n });\n }\n return {\n type: \"element\",\n tagName: \"figure\",\n properties: { className: [\"code-title\"] },\n children: [\n { type: \"element\", tagName: \"figcaption\", properties: {}, children: caption },\n block,\n ],\n };\n}\n\nexport const codeTitles: ShikiTransformer = {\n name: \"@ascribed/astro:code-titles\",\n root(root) {\n const meta = (this.options.meta as { __raw?: string } | undefined)?.__raw ?? \"\";\n const pre = root.children.find((node) => node.type === \"element\");\n // A site's own transformer may have put it in a figure already.\n if (pre === undefined || pre.tagName === \"figure\") return;\n const figure = captioned(meta, pre);\n if (figure !== undefined) root.children = [figure];\n },\n};\n"]}
1
+ {"version":3,"file":"code-titles.js","sourceRoot":"","sources":["../src/code-titles.ts"],"names":[],"mappings":"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","sourcesContent":["// A code block's caption, shown above it: its title, and what a tested\n// example was tested with. A `@snippet`'s title is in the code block's info\n// string, as `title=\"…\"`, and its tested line as `tested=\"…\"` (SPEC §4.8), and\n// Astro's own code highlighting ignores both (issue #106), so the integration\n// adds this Shiki transformer, which puts such a block in a\n// <figure class=\"code-title\"> with a <figcaption>: the title, then the tested\n// line in a <div class=\"ascribe-code-tested\"> of its own, whose \"Tested with\"\n// lead-in is the element library's stylesheet's (site-render contract §5.1).\nimport type { AstroUserConfig } from \"astro\";\nimport type { Element, ElementContent } from \"hast\";\nimport { CLASS_CODE_TESTED } from \"./names.js\";\n\ntype ShikiTransformer = NonNullable<\n NonNullable<NonNullable<AstroUserConfig[\"markdown\"]>[\"shikiConfig\"]>[\"transformers\"]\n>[number];\n\n/**\n * The value of the first `key=value` pair in a code block's meta, the info\n * string past its language, when it isn't empty. The meta has its Markdown\n * escapes taken off, and is read as SPEC §3.3.1 reads an info string, as\n * `ascribe_core::info_string` does: words parted by spaces and tabs, a pair's\n * value a token or a quoted string in which `\\\"` and `\\\\` escape, and a pair\n * followed by a space, a tab, or the end. Any other word isn't a pair.\n */\nfunction metaValue(meta: string, key: string): string | undefined {\n let i = 0;\n while (i < meta.length) {\n if (meta[i] === \" \" || meta[i] === \"\\t\") {\n i += 1;\n continue;\n }\n const pair = /^([a-z][a-z0-9-]*)=/.exec(meta.slice(i));\n let end = wordEnd(meta, i);\n let value: string | undefined;\n if (pair !== null) {\n let j = i + pair[0].length;\n if (meta[j] === '\"') {\n let text = \"\";\n j += 1;\n while (j < meta.length && meta[j] !== '\"') {\n if (meta[j] === \"\\\\\" && (meta[j + 1] === '\"' || meta[j + 1] === \"\\\\\")) j += 1;\n text += meta[j];\n j += 1;\n }\n if (j < meta.length && (j + 1 === meta.length || isSpace(meta[j + 1]))) {\n value = text;\n end = j + 1;\n }\n } else if (j < end && !meta.slice(j, end).includes('\"')) {\n value = meta.slice(j, end);\n }\n }\n if (value !== undefined && pair?.[1] === key) return value || undefined;\n i = end;\n }\n return undefined;\n}\n\nfunction isSpace(c: string | undefined): boolean {\n return c === \" \" || c === \"\\t\";\n}\n\nfunction wordEnd(text: string, from: number): number {\n let i = from;\n while (i < text.length && !isSpace(text[i])) i += 1;\n return i;\n}\n\n/** The `title=\"…\"` in a code block's meta, if it has one. */\nexport function codeTitle(meta: string): string | undefined {\n return metaValue(meta, \"title\");\n}\n\n/** The `tested=\"…\"` in a code block's meta, if it has one. */\nexport function codeTested(meta: string): string | undefined {\n return metaValue(meta, \"tested\");\n}\n\n/**\n * The block, in a figure whose caption holds its title and its tested line;\n * `undefined` when it has neither.\n */\nexport function captioned(meta: string, block: ElementContent): Element | undefined {\n const title = codeTitle(meta);\n const tested = codeTested(meta);\n if (title === undefined && tested === undefined) return undefined;\n const caption: ElementContent[] = [];\n if (title !== undefined) caption.push({ type: \"text\", value: title });\n if (tested !== undefined) {\n caption.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [CLASS_CODE_TESTED] },\n children: [{ type: \"text\", value: tested }],\n });\n }\n return {\n type: \"element\",\n tagName: \"figure\",\n properties: { className: [\"code-title\"] },\n children: [\n { type: \"element\", tagName: \"figcaption\", properties: {}, children: caption },\n block,\n ],\n };\n}\n\nexport const codeTitles: ShikiTransformer = {\n name: \"@ascribed/astro:code-titles\",\n root(root) {\n const meta = (this.options.meta as { __raw?: string } | undefined)?.__raw ?? \"\";\n const pre = root.children.find((node) => node.type === \"element\");\n // A site's own transformer may have put it in a figure already.\n if (pre === undefined || pre.tagName === \"figure\") return;\n const figure = captioned(meta, pre);\n if (figure !== undefined) root.children = [figure];\n },\n};\n"]}
@@ -0,0 +1,20 @@
1
+ /** A permalink's section, as `_ascribe/help.json` has it. */
2
+ interface HelpEntry {
3
+ title: string;
4
+ summary?: string;
5
+ text: string;
6
+ address?: string;
7
+ source: string;
8
+ }
9
+ /** `_ascribe/help.json`. */
10
+ export interface HelpFile {
11
+ version: number;
12
+ /** The build's name, as `ascribe.toml` declares it. */
13
+ build: string;
14
+ /** For a line of a versioned build, the line. */
15
+ line?: string;
16
+ help: Record<string, HelpEntry>;
17
+ }
18
+ /** The file in a build's site output, or `undefined` when there isn't one. */
19
+ export declare function readHelp(siteRoot: string): HelpFile | undefined;
20
+ export {};
@@ -0,0 +1,24 @@
1
+ // A build's `_ascribe/help.json` (output-layout contract §1.7): each
2
+ // permalink's section. `help.ts` serves its entries at their own addresses,
3
+ // and `permalinks.ts` links each forwarding page to them.
4
+ import { readFileSync } from "node:fs";
5
+ import path from "node:path";
6
+ /** The file's format version this reads. */
7
+ const VERSION = 1;
8
+ /** The file in a build's site output, or `undefined` when there isn't one. */
9
+ export function readHelp(siteRoot) {
10
+ const file = path.join(siteRoot, "_ascribe", "help.json");
11
+ let text;
12
+ try {
13
+ text = readFileSync(file, "utf8");
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ const help = JSON.parse(text);
19
+ if (help.version !== VERSION) {
20
+ throw new Error(`@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`);
21
+ }
22
+ return help;
23
+ }
24
+ //# sourceMappingURL=help-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help-file.js","sourceRoot":"","sources":["../src/help-file.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,4EAA4E;AAC5E,0DAA0D;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,4CAA4C;AAC5C,MAAM,OAAO,GAAG,CAAC,CAAC;AAqBlB,8EAA8E;AAC9E,MAAM,UAAU,QAAQ,CAAC,QAAgB;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAC1D,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAa,CAAC;IAC1C,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,oBAAoB,IAAI,eAAe,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,wEAAwE,OAAO,sDAAsD,CACjM,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["// A build's `_ascribe/help.json` (output-layout contract §1.7): each\n// permalink's section. `help.ts` serves its entries at their own addresses,\n// and `permalinks.ts` links each forwarding page to them.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/** The file's format version this reads. */\nconst VERSION = 1;\n\n/** A permalink's section, as `_ascribe/help.json` has it. */\ninterface HelpEntry {\n title: string;\n summary?: string;\n text: string;\n address?: string;\n source: string;\n}\n\n/** `_ascribe/help.json`. */\nexport interface HelpFile {\n version: number;\n /** The build's name, as `ascribe.toml` declares it. */\n build: string;\n /** For a line of a versioned build, the line. */\n line?: string;\n help: Record<string, HelpEntry>;\n}\n\n/** The file in a build's site output, or `undefined` when there isn't one. */\nexport function readHelp(siteRoot: string): HelpFile | undefined {\n const file = path.join(siteRoot, \"_ascribe\", \"help.json\");\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const help = JSON.parse(text) as HelpFile;\n if (help.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return help;\n}\n"]}
@@ -1,10 +1,9 @@
1
1
  import type { APIRoute } from "astro";
2
+ import { type HelpAnswer } from "./help.js";
2
3
  export declare const getStaticPaths: ({ routePattern }: import("astro").GetStaticPathsOptions) => {
3
4
  params: {
4
5
  id: string;
5
6
  };
6
- props: {
7
- body: string;
8
- };
7
+ props: HelpAnswer;
9
8
  }[];
10
9
  export declare const GET: APIRoute;
@@ -1,10 +1,16 @@
1
1
  import { base, permalinks } from "virtual:ascribe/site";
2
2
  import { helpPaths } from "./help.js";
3
3
  export const getStaticPaths = (({ routePattern }) => helpPaths(permalinks.map((source) => source.siteRoot), base, routePattern));
4
- export const GET = ({ props }) => new Response(props.body, {
5
- headers: {
6
- "Content-Type": "application/json; charset=utf-8",
7
- "Access-Control-Allow-Origin": "*",
8
- },
9
- });
4
+ export const GET = ({ props }) => {
5
+ const { body, type, alternate } = props;
6
+ return new Response(body, {
7
+ headers: {
8
+ "Content-Type": `${type}; charset=utf-8`,
9
+ "Access-Control-Allow-Origin": "*",
10
+ ...(alternate === undefined
11
+ ? {}
12
+ : { Link: `<${alternate}>; rel="alternate"; type="text/markdown"` }),
13
+ },
14
+ });
15
+ };
10
16
  //# sourceMappingURL=help-route.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"help-route.js","sourceRoot":"","sources":["../src/help-route.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAClD,SAAS,CACP,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC3C,IAAI,EACJ,YAAY,CACb,CAA0B,CAAC;AAE9B,MAAM,CAAC,MAAM,GAAG,GAAa,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CACzC,IAAI,QAAQ,CAAE,KAA0B,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE;QACP,cAAc,EAAE,iCAAiC;QACjD,6BAA6B,EAAE,GAAG;KACnC;CACF,CAAC,CAAC","sourcesContent":["// A help entry's own address: one entry of a line's `_ascribe/help.json`, as\n// JSON (`help.ts`). The integration adds this route once for each folder of\n// entries, `<path>[id].json` for the current line and `<line>/go/[id].json`\n// under the base path for each older one; which folder this is comes from\n// the route's pattern. Any origin may read it, as the build's headers say\n// for a host.\nimport type { APIRoute, GetStaticPaths } from \"astro\";\nimport { base, permalinks } from \"virtual:ascribe/site\";\nimport { helpPaths } from \"./help.js\";\n\nexport const getStaticPaths = (({ routePattern }) =>\n helpPaths(\n permalinks.map((source) => source.siteRoot),\n base,\n routePattern,\n )) satisfies GetStaticPaths;\n\nexport const GET: APIRoute = ({ props }) =>\n new Response((props as { body: string }).body, {\n headers: {\n \"Content-Type\": \"application/json; charset=utf-8\",\n \"Access-Control-Allow-Origin\": \"*\",\n },\n });\n"]}
1
+ {"version":3,"file":"help-route.js","sourceRoot":"","sources":["../src/help-route.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAmB,MAAM,WAAW,CAAC;AAEvD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,CAClD,SAAS,CACP,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC3C,IAAI,EACJ,YAAY,CACb,CAA0B,CAAC;AAE9B,MAAM,CAAC,MAAM,GAAG,GAAa,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;IACzC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,KAAmB,CAAC;IACtD,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;QACxB,OAAO,EAAE;YACP,cAAc,EAAE,GAAG,IAAI,iBAAiB;YACxC,6BAA6B,EAAE,GAAG;YAClC,GAAG,CAAC,SAAS,KAAK,SAAS;gBACzB,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,0CAA0C,EAAE,CAAC;SACvE;KACF,CAAC,CAAC;AACL,CAAC,CAAC","sourcesContent":["// A help entry's own addresses: one entry of a line's `_ascribe/help.json`, as\n// JSON, and its section as Markdown (`help.ts`). The integration adds this\n// route once for each folder of entries and each format, `<path>[id].json`\n// and `<path>[id].md` for the current line and `<line>/go/[id].json` and\n// `<line>/go/[id].md` under the base path for each older one; which folder\n// and format this is comes from the route's pattern. Any origin may read\n// it, and the JSON names the Markdown as its alternate, as the build's\n// headers say for a host.\nimport type { APIRoute, GetStaticPaths } from \"astro\";\nimport { base, permalinks } from \"virtual:ascribe/site\";\nimport { helpPaths, type HelpAnswer } from \"./help.js\";\n\nexport const getStaticPaths = (({ routePattern }) =>\n helpPaths(\n permalinks.map((source) => source.siteRoot),\n base,\n routePattern,\n )) satisfies GetStaticPaths;\n\nexport const GET: APIRoute = ({ props }) => {\n const { body, type, alternate } = props as HelpAnswer;\n return new Response(body, {\n headers: {\n \"Content-Type\": `${type}; charset=utf-8`,\n \"Access-Control-Allow-Origin\": \"*\",\n ...(alternate === undefined\n ? {}\n : { Link: `<${alternate}>; rel=\"alternate\"; type=\"text/markdown\"` }),\n },\n });\n};\n"]}
package/dist/help.d.ts CHANGED
@@ -1,22 +1,4 @@
1
- /** A permalink's section, as `_ascribe/help.json` has it. */
2
- interface HelpEntry {
3
- title: string;
4
- summary?: string;
5
- text: string;
6
- address?: string;
7
- source: string;
8
- }
9
- /** `_ascribe/help.json`. */
10
- export interface HelpFile {
11
- version: number;
12
- /** The build's name, as `ascribe.toml` declares it. */
13
- build: string;
14
- /** For a line of a versioned build, the line. */
15
- line?: string;
16
- help: Record<string, HelpEntry>;
17
- }
18
- /** The file in a build's site output, or `undefined` when there isn't one. */
19
- export declare function readHelp(siteRoot: string): HelpFile | undefined;
1
+ import { type HelpFile } from "./help-file.js";
20
2
  /**
21
3
  * What each entry's address answers, by the address from the host's root:
22
4
  * the file it's in with that entry alone, its `version`, `build`, and
@@ -25,21 +7,32 @@ export declare function readHelp(siteRoot: string): HelpFile | undefined;
25
7
  */
26
8
  export declare function helpAddresses(siteRoots: readonly string[]): Map<string, HelpFile>;
27
9
  /**
28
- * The route patterns the entries need, one for each folder that has any:
29
- * the current line's, then each older line's. Throws when a folder isn't
30
- * under `base`, since an Astro site serves nothing outside it.
10
+ * The route patterns the entries need, for each folder that has any, its
11
+ * JSON's and its Markdown's: the current line's, then each older line's.
12
+ * Throws when a folder isn't under `base`, since an Astro site serves
13
+ * nothing outside it.
31
14
  */
32
15
  export declare function helpPatterns(siteRoots: readonly string[], base: string): string[];
16
+ /**
17
+ * An entry's section as Markdown: its title as a heading, then its `text`,
18
+ * the section as the plain output writes it.
19
+ */
20
+ export declare function helpMarkdown(help: HelpFile): string;
21
+ /** What an entry's address answers: its body, and the body's media type. A record, so Astro takes it as props. */
22
+ export interface HelpAnswer extends Record<string, unknown> {
23
+ body: string;
24
+ type: string;
25
+ /** For the JSON, where the same entry is as Markdown, its alternate. */
26
+ alternate?: string;
27
+ }
33
28
  /**
34
29
  * `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:
35
- * one answer for each entry whose address it matches, the entry as JSON.
30
+ * one answer for each entry whose address it matches, the entry as JSON, or
31
+ * its section as Markdown.
36
32
  */
37
33
  export declare function helpPaths(siteRoots: readonly string[], base: string, routePattern: string): {
38
34
  params: {
39
35
  id: string;
40
36
  };
41
- props: {
42
- body: string;
43
- };
37
+ props: HelpAnswer;
44
38
  }[];
45
- export {};
package/dist/help.js CHANGED
@@ -1,33 +1,18 @@
1
- // Each permalink's help at an address of its own, for a product to fetch its
1
+ // Each permalink's help at addresses of its own, for a product to fetch its
2
2
  // line's: one entry of a build's `_ascribe/help.json` (output-layout contract
3
- // §1.7), as JSON. The current line's entries are beside its permalinks'
4
- // addresses, at `<path><name>.json`; an older line's are at `go/<name>.json`
5
- // under the line's own base path, `/docs/3.4/go/connect.json`, since the
6
- // permalinks' addresses are the current line's. The integration adds a route
7
- // (`help-route.ts`) for each of those folders, in `astro build` and `astro dev`.
8
- import { readFileSync } from "node:fs";
3
+ // §1.7), as JSON, and its section as Markdown, for an agent to read the
4
+ // section a permalink names in one fetch. The current line's entries are
5
+ // beside its permalinks' addresses, at `<path><name>.json` and
6
+ // `<path><name>.md`; an older line's are at `go/<name>.json` and
7
+ // `go/<name>.md` under the line's own base path, `/docs/3.4/go/connect.json`,
8
+ // since the permalinks' addresses are the current line's. The integration
9
+ // adds a route (`help-route.ts`) for each of those folders and each format,
10
+ // in `astro build` and `astro dev`.
9
11
  import path from "node:path";
12
+ import { readHelp } from "./help-file.js";
10
13
  import { permalinkPattern, readPermalinks } from "./permalinks.js";
11
14
  import { normalizeBase } from "./project.js";
12
15
  import { olderLines } from "./versions.js";
13
- /** The file's format version this reads. */
14
- const VERSION = 1;
15
- /** The file in a build's site output, or `undefined` when there isn't one. */
16
- export function readHelp(siteRoot) {
17
- const file = path.join(siteRoot, "_ascribe", "help.json");
18
- let text;
19
- try {
20
- text = readFileSync(file, "utf8");
21
- }
22
- catch {
23
- return undefined;
24
- }
25
- const help = JSON.parse(text);
26
- if (help.version !== VERSION) {
27
- throw new Error(`@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`);
28
- }
29
- return help;
30
- }
31
16
  /**
32
17
  * Each line's file, with the folder its entries are served under: the
33
18
  * current line's at the permalinks' path, each older line's at `go/` under
@@ -70,32 +55,60 @@ export function helpAddresses(siteRoots) {
70
55
  }
71
56
  return addresses;
72
57
  }
73
- /** The route pattern for a folder's entries, as Astro takes it: under `base`, which Astro adds. */
74
- function helpPattern(folder, base) {
75
- return `${permalinkPattern(folder, base)}.json`;
58
+ /** The formats an entry is served in, by the extension its address ends with. */
59
+ const FORMATS = [".json", ".md"];
60
+ /** The route pattern for a folder's entries in a format, as Astro takes it: under `base`, which Astro adds. */
61
+ function helpPattern(folder, base, extension) {
62
+ return `${permalinkPattern(folder, base)}${extension}`;
76
63
  }
77
64
  /**
78
- * The route patterns the entries need, one for each folder that has any:
79
- * the current line's, then each older line's. Throws when a folder isn't
80
- * under `base`, since an Astro site serves nothing outside it.
65
+ * The route patterns the entries need, for each folder that has any, its
66
+ * JSON's and its Markdown's: the current line's, then each older line's.
67
+ * Throws when a folder isn't under `base`, since an Astro site serves
68
+ * nothing outside it.
81
69
  */
82
70
  export function helpPatterns(siteRoots, base) {
83
71
  const patterns = helpFolders(siteRoots)
84
72
  .filter(({ help }) => Object.keys(help.help).length > 0)
85
- .map(({ folder }) => helpPattern(folder, base));
73
+ .flatMap(({ folder }) => FORMATS.map((extension) => helpPattern(folder, base, extension)));
86
74
  return [...new Set(patterns)];
87
75
  }
76
+ /**
77
+ * An entry's section as Markdown: its title as a heading, then its `text`,
78
+ * the section as the plain output writes it.
79
+ */
80
+ export function helpMarkdown(help) {
81
+ const [entry] = Object.values(help.help);
82
+ if (entry === undefined)
83
+ return "";
84
+ const text = entry.text.trimEnd();
85
+ return text === "" ? `# ${entry.title}\n` : `# ${entry.title}\n\n${text}\n`;
86
+ }
88
87
  /**
89
88
  * `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:
90
- * one answer for each entry whose address it matches, the entry as JSON.
89
+ * one answer for each entry whose address it matches, the entry as JSON, or
90
+ * its section as Markdown.
91
91
  */
92
92
  export function helpPaths(siteRoots, base, routePattern) {
93
93
  return [...helpAddresses(siteRoots)].flatMap(([address, help]) => {
94
94
  const name = path.posix.basename(address, ".json");
95
95
  const folder = address.slice(0, address.length - `${name}.json`.length);
96
- return helpPattern(folder, base) === routePattern
97
- ? [{ params: { id: name }, props: { body: `${JSON.stringify(help, null, 2)}\n` } }]
98
- : [];
96
+ if (helpPattern(folder, base, ".json") === routePattern) {
97
+ return [
98
+ {
99
+ params: { id: name },
100
+ props: {
101
+ body: `${JSON.stringify(help, null, 2)}\n`,
102
+ type: "application/json",
103
+ alternate: `${folder}${name}.md`,
104
+ },
105
+ },
106
+ ];
107
+ }
108
+ if (helpPattern(folder, base, ".md") === routePattern) {
109
+ return [{ params: { id: name }, props: { body: helpMarkdown(help), type: "text/markdown" } }];
110
+ }
111
+ return [];
99
112
  });
100
113
  }
101
114
  //# sourceMappingURL=help.js.map
package/dist/help.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"help.js","sourceRoot":"","sources":["../src/help.ts"],"names":[],"mappings":"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","sourcesContent":["// Each permalink's help at an address of its own, for a product to fetch its\n// line's: one entry of a build's `_ascribe/help.json` (output-layout contract\n// §1.7), as JSON. The current line's entries are beside its permalinks'\n// addresses, at `<path><name>.json`; an older line's are at `go/<name>.json`\n// under the line's own base path, `/docs/3.4/go/connect.json`, since the\n// permalinks' addresses are the current line's. The integration adds a route\n// (`help-route.ts`) for each of those folders, in `astro build` and `astro dev`.\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { permalinkPattern, readPermalinks } from \"./permalinks.js\";\nimport { normalizeBase } from \"./project.js\";\nimport { olderLines } from \"./versions.js\";\n\n/** The file's format version this reads. */\nconst VERSION = 1;\n\n/** A permalink's section, as `_ascribe/help.json` has it. */\ninterface HelpEntry {\n title: string;\n summary?: string;\n text: string;\n address?: string;\n source: string;\n}\n\n/** `_ascribe/help.json`. */\nexport interface HelpFile {\n version: number;\n /** The build's name, as `ascribe.toml` declares it. */\n build: string;\n /** For a line of a versioned build, the line. */\n line?: string;\n help: Record<string, HelpEntry>;\n}\n\n/** The file in a build's site output, or `undefined` when there isn't one. */\nexport function readHelp(siteRoot: string): HelpFile | undefined {\n const file = path.join(siteRoot, \"_ascribe\", \"help.json\");\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const help = JSON.parse(text) as HelpFile;\n if (help.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(help.version)} of the help file, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return help;\n}\n\n/** A folder of entries' addresses, from the host's root, and the file they're served from. */\ninterface HelpFolder {\n folder: string;\n help: HelpFile;\n}\n\n/**\n * Each line's file, with the folder its entries are served under: the\n * current line's at the permalinks' path, each older line's at `go/` under\n * its base path. A site output with no file, or no permalinks list, has none.\n */\nfunction helpFolders(siteRoots: readonly string[]): HelpFolder[] {\n return siteRoots.flatMap((siteRoot) => {\n const list = readPermalinks(siteRoot);\n const help = readHelp(siteRoot);\n const current =\n list === undefined || help === undefined ? [] : [{ folder: normalizeBase(list.path), help }];\n const older = olderLines(siteRoot).flatMap((line) => {\n const lineHelp = readHelp(line.siteRoot);\n return lineHelp === undefined\n ? []\n : [{ folder: `${normalizeBase(line.base)}go/`, help: lineHelp }];\n });\n return [...current, ...older];\n });\n}\n\n/**\n * What each entry's address answers, by the address from the host's root:\n * the file it's in with that entry alone, its `version`, `build`, and\n * `line` as the file has them, so a product reads a fetched entry and its\n * bundled copy alike. An address two files give keeps the first's.\n */\nexport function helpAddresses(siteRoots: readonly string[]): Map<string, HelpFile> {\n const addresses = new Map<string, HelpFile>();\n for (const { folder, help } of helpFolders(siteRoots)) {\n for (const [name, entry] of Object.entries(help.help)) {\n const address = `${folder}${name}.json`;\n if (addresses.has(address)) continue;\n addresses.set(address, {\n version: help.version,\n build: help.build,\n ...(help.line === undefined ? {} : { line: help.line }),\n help: { [name]: entry },\n });\n }\n }\n return addresses;\n}\n\n/** The route pattern for a folder's entries, as Astro takes it: under `base`, which Astro adds. */\nfunction helpPattern(folder: string, base: string): string {\n return `${permalinkPattern(folder, base)}.json`;\n}\n\n/**\n * The route patterns the entries need, one for each folder that has any:\n * the current line's, then each older line's. Throws when a folder isn't\n * under `base`, since an Astro site serves nothing outside it.\n */\nexport function helpPatterns(siteRoots: readonly string[], base: string): string[] {\n const patterns = helpFolders(siteRoots)\n .filter(({ help }) => Object.keys(help.help).length > 0)\n .map(({ folder }) => helpPattern(folder, base));\n return [...new Set(patterns)];\n}\n\n/**\n * `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:\n * one answer for each entry whose address it matches, the entry as JSON.\n */\nexport function helpPaths(\n siteRoots: readonly string[],\n base: string,\n routePattern: string,\n): { params: { id: string }; props: { body: string } }[] {\n return [...helpAddresses(siteRoots)].flatMap(([address, help]) => {\n const name = path.posix.basename(address, \".json\");\n const folder = address.slice(0, address.length - `${name}.json`.length);\n return helpPattern(folder, base) === routePattern\n ? [{ params: { id: name }, props: { body: `${JSON.stringify(help, null, 2)}\\n` } }]\n : [];\n });\n}\n"]}
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../src/help.ts"],"names":[],"mappings":"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","sourcesContent":["// Each permalink's help at addresses of its own, for a product to fetch its\n// line's: one entry of a build's `_ascribe/help.json` (output-layout contract\n// §1.7), as JSON, and its section as Markdown, for an agent to read the\n// section a permalink names in one fetch. The current line's entries are\n// beside its permalinks' addresses, at `<path><name>.json` and\n// `<path><name>.md`; an older line's are at `go/<name>.json` and\n// `go/<name>.md` under the line's own base path, `/docs/3.4/go/connect.json`,\n// since the permalinks' addresses are the current line's. The integration\n// adds a route (`help-route.ts`) for each of those folders and each format,\n// in `astro build` and `astro dev`.\nimport path from \"node:path\";\nimport { readHelp, type HelpFile } from \"./help-file.js\";\nimport { permalinkPattern, readPermalinks } from \"./permalinks.js\";\nimport { normalizeBase } from \"./project.js\";\nimport { olderLines } from \"./versions.js\";\n\n/** A folder of entries' addresses, from the host's root, and the file they're served from. */\ninterface HelpFolder {\n folder: string;\n help: HelpFile;\n}\n\n/**\n * Each line's file, with the folder its entries are served under: the\n * current line's at the permalinks' path, each older line's at `go/` under\n * its base path. A site output with no file, or no permalinks list, has none.\n */\nfunction helpFolders(siteRoots: readonly string[]): HelpFolder[] {\n return siteRoots.flatMap((siteRoot) => {\n const list = readPermalinks(siteRoot);\n const help = readHelp(siteRoot);\n const current =\n list === undefined || help === undefined ? [] : [{ folder: normalizeBase(list.path), help }];\n const older = olderLines(siteRoot).flatMap((line) => {\n const lineHelp = readHelp(line.siteRoot);\n return lineHelp === undefined\n ? []\n : [{ folder: `${normalizeBase(line.base)}go/`, help: lineHelp }];\n });\n return [...current, ...older];\n });\n}\n\n/**\n * What each entry's address answers, by the address from the host's root:\n * the file it's in with that entry alone, its `version`, `build`, and\n * `line` as the file has them, so a product reads a fetched entry and its\n * bundled copy alike. An address two files give keeps the first's.\n */\nexport function helpAddresses(siteRoots: readonly string[]): Map<string, HelpFile> {\n const addresses = new Map<string, HelpFile>();\n for (const { folder, help } of helpFolders(siteRoots)) {\n for (const [name, entry] of Object.entries(help.help)) {\n const address = `${folder}${name}.json`;\n if (addresses.has(address)) continue;\n addresses.set(address, {\n version: help.version,\n build: help.build,\n ...(help.line === undefined ? {} : { line: help.line }),\n help: { [name]: entry },\n });\n }\n }\n return addresses;\n}\n\n/** The formats an entry is served in, by the extension its address ends with. */\nconst FORMATS = [\".json\", \".md\"] as const;\n\n/** The route pattern for a folder's entries in a format, as Astro takes it: under `base`, which Astro adds. */\nfunction helpPattern(folder: string, base: string, extension: string): string {\n return `${permalinkPattern(folder, base)}${extension}`;\n}\n\n/**\n * The route patterns the entries need, for each folder that has any, its\n * JSON's and its Markdown's: the current line's, then each older line's.\n * Throws when a folder isn't under `base`, since an Astro site serves\n * nothing outside it.\n */\nexport function helpPatterns(siteRoots: readonly string[], base: string): string[] {\n const patterns = helpFolders(siteRoots)\n .filter(({ help }) => Object.keys(help.help).length > 0)\n .flatMap(({ folder }) => FORMATS.map((extension) => helpPattern(folder, base, extension)));\n return [...new Set(patterns)];\n}\n\n/**\n * An entry's section as Markdown: its title as a heading, then its `text`,\n * the section as the plain output writes it.\n */\nexport function helpMarkdown(help: HelpFile): string {\n const [entry] = Object.values(help.help);\n if (entry === undefined) return \"\";\n const text = entry.text.trimEnd();\n return text === \"\" ? `# ${entry.title}\\n` : `# ${entry.title}\\n\\n${text}\\n`;\n}\n\n/** What an entry's address answers: its body, and the body's media type. A record, so Astro takes it as props. */\nexport interface HelpAnswer extends Record<string, unknown> {\n body: string;\n type: string;\n /** For the JSON, where the same entry is as Markdown, its alternate. */\n alternate?: string;\n}\n\n/**\n * `getStaticPaths` for the route at `routePattern`, one of `helpPatterns`:\n * one answer for each entry whose address it matches, the entry as JSON, or\n * its section as Markdown.\n */\nexport function helpPaths(\n siteRoots: readonly string[],\n base: string,\n routePattern: string,\n): { params: { id: string }; props: HelpAnswer }[] {\n return [...helpAddresses(siteRoots)].flatMap(([address, help]) => {\n const name = path.posix.basename(address, \".json\");\n const folder = address.slice(0, address.length - `${name}.json`.length);\n if (helpPattern(folder, base, \".json\") === routePattern) {\n return [\n {\n params: { id: name },\n props: {\n body: `${JSON.stringify(help, null, 2)}\\n`,\n type: \"application/json\",\n alternate: `${folder}${name}.md`,\n },\n },\n ];\n }\n if (helpPattern(folder, base, \".md\") === routePattern) {\n return [{ params: { id: name }, props: { body: helpMarkdown(help), type: \"text/markdown\" } }];\n }\n return [];\n });\n}\n"]}
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ import { writeFile } from "node:fs/promises";
45
45
  import path from "node:path";
46
46
  import { fileURLToPath } from "node:url";
47
47
  import { findBinary } from "./binary.js";
48
+ import { codeLanguages } from "./code-languages.js";
48
49
  import { codeTitles } from "./code-titles.js";
49
50
  import { SeenFiles, watchDev } from "./dev.js";
50
51
  import { agentsMiddleware, copyAgentFiles, copyPublishedFiles, filesMiddleware, folderUnder, inOrder, } from "./files.js";
@@ -326,9 +327,9 @@ export default function ascribe(options) {
326
327
  prerender: true,
327
328
  });
328
329
  }
329
- // Each permalink's help, at `<name>.json` beside its address, and each older line's
330
- // under the line's base path. One route for each folder: the current line's, then each
331
- // older line's.
330
+ // Each permalink's help, at `<name>.json` and `<name>.md` beside its address, and each
331
+ // older line's under the line's base path. One route for each folder and format: the
332
+ // current line's, then each older line's.
332
333
  for (const pattern of helpPatterns(entries.map((entry) => entry.siteRoot), astro.base)) {
333
334
  injectRoute({
334
335
  pattern,
@@ -342,9 +343,11 @@ export default function ascribe(options) {
342
343
  updateConfig({
343
344
  ...supplied,
344
345
  ...(format.supplied === undefined ? {} : { build: { format: format.supplied } }),
345
- ...(options.codeTitles === false
346
- ? {}
347
- : { markdown: { shikiConfig: { transformers: [codeTitles] } } }),
346
+ markdown: {
347
+ shikiConfig: {
348
+ transformers: options.codeTitles === false ? [codeLanguages] : [codeTitles, codeLanguages],
349
+ },
350
+ },
348
351
  vite: {
349
352
  // `Elements.astro` is Astro source, so Vite must compile it, not load it as a Node module.
350
353
  ssr: { noExternal: ["@ascribed/astro"] },