@ascribed/astro 0.2.1-next.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +41 -9
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/content.d.ts +30 -0
  6. package/dist/content.js +101 -2
  7. package/dist/content.js.map +1 -1
  8. package/dist/dev.d.ts +2 -1
  9. package/dist/dev.js +13 -3
  10. package/dist/dev.js.map +1 -1
  11. package/dist/files.d.ts +21 -4
  12. package/dist/files.js +46 -8
  13. package/dist/files.js.map +1 -1
  14. package/dist/head-check.d.ts +6 -0
  15. package/dist/head-check.js +28 -0
  16. package/dist/head-check.js.map +1 -0
  17. package/dist/head.d.ts +54 -0
  18. package/dist/head.js +114 -0
  19. package/dist/head.js.map +1 -0
  20. package/dist/headers.d.ts +50 -0
  21. package/dist/headers.js +155 -0
  22. package/dist/headers.js.map +1 -0
  23. package/dist/index.d.ts +40 -9
  24. package/dist/index.js +330 -95
  25. package/dist/index.js.map +1 -1
  26. package/dist/init.d.ts +41 -0
  27. package/dist/init.js +130 -0
  28. package/dist/init.js.map +1 -0
  29. package/dist/names.d.ts +5 -0
  30. package/dist/names.js +5 -0
  31. package/dist/names.js.map +1 -1
  32. package/dist/nav.d.ts +51 -0
  33. package/dist/nav.js +97 -0
  34. package/dist/nav.js.map +1 -0
  35. package/dist/permalinks.d.ts +78 -0
  36. package/dist/permalinks.js +114 -0
  37. package/dist/permalinks.js.map +1 -0
  38. package/dist/project.d.ts +37 -0
  39. package/dist/project.js +47 -2
  40. package/dist/project.js.map +1 -1
  41. package/dist/projects.d.ts +38 -0
  42. package/dist/projects.js +127 -0
  43. package/dist/projects.js.map +1 -0
  44. package/dist/records.d.ts +136 -0
  45. package/dist/records.js +182 -0
  46. package/dist/records.js.map +1 -0
  47. package/dist/review/devfile.d.ts +6 -1
  48. package/dist/review/devfile.js +12 -2
  49. package/dist/review/devfile.js.map +1 -1
  50. package/dist/review/diff.d.ts +13 -0
  51. package/dist/review/diff.js +34 -1
  52. package/dist/review/diff.js.map +1 -1
  53. package/dist/review/protocol.d.ts +6 -1
  54. package/dist/review/protocol.js.map +1 -1
  55. package/dist/review/routes.d.ts +3 -0
  56. package/dist/review/routes.js +49 -35
  57. package/dist/review/routes.js.map +1 -1
  58. package/dist/review/server.d.ts +15 -0
  59. package/dist/review/server.js +58 -7
  60. package/dist/review/server.js.map +1 -1
  61. package/dist/root-files.d.ts +68 -0
  62. package/dist/root-files.js +103 -0
  63. package/dist/root-files.js.map +1 -0
  64. package/dist/run.d.ts +7 -6
  65. package/dist/run.js +14 -6
  66. package/dist/run.js.map +1 -1
  67. package/dist/shapes.d.ts +8 -1
  68. package/dist/shapes.js.map +1 -1
  69. package/dist/toolbar/app.js +98 -0
  70. package/dist/toolbar/app.js.map +1 -1
  71. package/dist/toolbar/style.d.ts +1 -1
  72. package/dist/toolbar/style.js +31 -23
  73. package/dist/toolbar/style.js.map +1 -1
  74. package/package.json +32 -9
  75. package/src/Head.astro +58 -0
  76. package/src/MissingPermalink.astro +59 -0
  77. package/src/Permalink.astro +78 -0
  78. package/starter/src/content.config.ts +8 -0
  79. package/starter/src/layouts/Docs.astro +87 -0
  80. package/starter/src/nav.ts +13 -0
  81. package/starter/src/pages/[...slug].astro +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascribed/astro",
3
- "version": "0.2.1-next.9",
3
+ "version": "0.3.0",
4
4
  "description": "The Astro integration for Ascribe.",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -12,10 +12,17 @@
12
12
  "homepage": "https://github.com/ascribed-dev/ascribe/tree/main/packages/astro#readme",
13
13
  "bugs": "https://github.com/ascribed-dev/ascribe/issues",
14
14
  "sideEffects": false,
15
+ "bin": {
16
+ "ascribe-astro": "./dist/cli.js"
17
+ },
15
18
  "files": [
16
19
  "dist",
20
+ "starter",
17
21
  "src/Elements.astro",
18
- "src/Availability.astro"
22
+ "src/Availability.astro",
23
+ "src/Permalink.astro",
24
+ "src/MissingPermalink.astro",
25
+ "src/Head.astro"
19
26
  ],
20
27
  "exports": {
21
28
  ".": {
@@ -32,30 +39,47 @@
32
39
  },
33
40
  "./Elements.astro": "./src/Elements.astro",
34
41
  "./Availability.astro": "./src/Availability.astro",
42
+ "./MissingPermalink.astro": "./src/MissingPermalink.astro",
43
+ "./Head.astro": "./src/Head.astro",
35
44
  "./content": {
36
45
  "types": "./dist/content.d.ts",
37
46
  "default": "./dist/content.js"
47
+ },
48
+ "./permalinks": {
49
+ "types": "./dist/permalinks.d.ts",
50
+ "default": "./dist/permalinks.js"
38
51
  }
39
52
  },
40
53
  "engines": {
41
54
  "node": ">=24"
42
55
  },
43
56
  "dependencies": {
44
- "@ascribed/cli": "0.2.1-next.9",
45
- "@ascribed/elements": "0.2.1-next.9",
46
- "@ascribed/review": "0.2.1-next.9",
57
+ "@ascribed/cli": "0.3.0",
58
+ "@ascribed/elements": "0.3.0",
59
+ "@ascribed/review": "0.3.0",
60
+ "github-slugger": "^2.0.0",
47
61
  "property-information": "^7.2.0",
48
62
  "smol-toml": "^1.9.0"
49
63
  },
50
64
  "peerDependencies": {
51
- "astro": "^7.3.5"
65
+ "@types/hast": "^3.0.0",
66
+ "astro": "^7.3.5",
67
+ "satteri": "^0.10.3"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@types/hast": {
71
+ "optional": true
72
+ },
73
+ "satteri": {
74
+ "optional": true
75
+ }
52
76
  },
53
77
  "devDependencies": {
54
78
  "@types/hast": "^3.0.4",
55
79
  "@types/node": "^24.0.0",
56
- "astro": "7.3.5",
80
+ "astro": "7.3.6",
57
81
  "hast-util-from-html": "^2.0.3",
58
- "jsdom": "^30.1.1",
82
+ "jsdom": "^30.1.2",
59
83
  "rehype-raw": "^7.0.0",
60
84
  "rehype-stringify": "^10.0.1",
61
85
  "remark-gfm": "^4.0.1",
@@ -68,7 +92,6 @@
68
92
  "publishConfig": {
69
93
  "access": "public"
70
94
  },
71
- "gitHead": "52da4c48b9cb985c8e97d926aafa4041fc47f2eb",
72
95
  "scripts": {
73
96
  "build": "tsc -p tsconfig.build.json",
74
97
  "typecheck": "tsc --noEmit",
package/src/Head.astro ADDED
@@ -0,0 +1,58 @@
1
+ ---
2
+ // Everything a page's head says about the page, from its record in the
3
+ // build's `_ascribe/pages.json`: its `<title>` and description, its canonical
4
+ // link, `<meta name="robots">` (`noindex` when it isn't listed, `nosnippet`
5
+ // when it's listed and not to be quoted), a link to its Markdown version when
6
+ // the build publishes one and the page is offered, the Open Graph tags a link
7
+ // preview reads, and its breadcrumbs as structured data. Each comes from the
8
+ // record, as the sitemap and `llms.txt` do, so none can disagree with them. A
9
+ // layout includes it in `<head>`, given the page's collection entry:
10
+ //
11
+ // <Head
12
+ // entry={entry}
13
+ // title={(page, record) => (record.route === "/" ? "Loom" : `${page} · Loom`)}
14
+ // name="Loom"
15
+ // card={{ src: "/social-card.png", alt: "Loom's docs", width: 1200, height: 630 }}
16
+ // />
17
+ //
18
+ // What the site knows and the record doesn't is given by property: `title`,
19
+ // a function from the page's title and its record, which has its route, to
20
+ // the whole title; `name`, the site's name, for `og:site_name` and, on the
21
+ // page at the host's root, the site-name structured data; and `card`, the
22
+ // page's social card. The layout keeps the character set, the viewport,
23
+ // icons, theme colors, and styles. A page with no record, such as an Astro
24
+ // page of the site's own, gets nothing: it writes its own head.
25
+ import { headTags, pageRecord, scriptJson, type Card, type PageRecord } from "@ascribed/astro/content";
26
+
27
+ interface Props {
28
+ /** The page's collection entry, or `undefined` for a page that has none. */
29
+ entry?: { filePath?: string | undefined; data?: unknown } | undefined;
30
+ /** The whole title, from the page's title and its record. Default: the page's title. */
31
+ title?: (title: string, record: PageRecord) => string;
32
+ /** The site's name. */
33
+ name?: string;
34
+ /** The page's social card. Without one, no image tags. */
35
+ card?: Card;
36
+ }
37
+
38
+ const { entry, title, name, card } = Astro.props;
39
+ const record = entry === undefined ? undefined : pageRecord(entry);
40
+ const tags =
41
+ record === undefined
42
+ ? []
43
+ : headTags(record, { title, name, card, address: Astro.site ?? Astro.url });
44
+ ---
45
+
46
+ {
47
+ tags.map((tag) =>
48
+ tag.tag === "title" ? (
49
+ <title>{tag.text}</title>
50
+ ) : tag.tag === "meta" ? (
51
+ <meta {...tag.attributes} />
52
+ ) : tag.tag === "link" ? (
53
+ <link {...tag.attributes} />
54
+ ) : (
55
+ <script is:inline type="application/ld+json" set:html={scriptJson(tag.json)} />
56
+ ),
57
+ )
58
+ }
@@ -0,0 +1,59 @@
1
+ ---
2
+ // For a site's not-found page: on an address under the permalinks' path, it
3
+ // says the link came from a product that expected a section of these docs
4
+ // there, and that these docs don't have it. Anywhere else it shows nothing. A
5
+ // static host serves one not-found page for every missing address, so its
6
+ // script decides from the address, and without a script it stays hidden.
7
+ //
8
+ // import MissingPermalink from "@ascribed/astro/MissingPermalink.astro";
9
+ // <MissingPermalink />
10
+ //
11
+ // `home` is where it sends the reader: by default the root of the project whose
12
+ // permalinks it explains, which with several projects is the first that has
13
+ // any. Children replace its last paragraph, which offers the home page and
14
+ // searching.
15
+ import { home as root, permalinks } from "virtual:ascribe/site";
16
+ import { servedList } from "@ascribed/astro/permalinks";
17
+
18
+ interface Props {
19
+ home?: string;
20
+ }
21
+
22
+ const served = servedList(permalinks);
23
+ const { home = served?.home ?? root } = Astro.props;
24
+ const path = served?.path;
25
+ ---
26
+
27
+ {
28
+ path !== undefined && (
29
+ <section data-permalinks={path} hidden>
30
+ <p>
31
+ This address is a permalink, <code />: a name a product links to for a section of its
32
+ documentation. These docs have no section by that name. The link may come from a newer or
33
+ older version of the product than they describe.
34
+ </p>
35
+ <slot>
36
+ <p>Start from the <a href={home}>home page</a>, or search the docs for what you need.</p>
37
+ </slot>
38
+ </section>
39
+ )
40
+ }
41
+
42
+ <script>
43
+ for (const section of document.querySelectorAll<HTMLElement>("section[data-permalinks]")) {
44
+ const path = section.dataset["permalinks"] ?? "";
45
+ let address: string;
46
+ try {
47
+ address = decodeURIComponent(location.pathname);
48
+ } catch {
49
+ continue;
50
+ }
51
+ if (!address.startsWith(path)) continue;
52
+ // One segment after the path, as every permalink's address has.
53
+ const name = address.slice(path.length).replace(/\/$/, "");
54
+ if (name === "" || name.includes("/")) continue;
55
+ const code = section.querySelector("code");
56
+ if (code !== null) code.textContent = name;
57
+ section.hidden = false;
58
+ }
59
+ </script>
@@ -0,0 +1,78 @@
1
+ ---
2
+ // A permalink's address: a page that sends the reader on to the heading it
3
+ // names, wherever that is now. The integration adds it as a route at
4
+ // `<path>[id]`, one page for each entry of the site output's
5
+ // `_ascribe/permalinks.json`. It forwards without a script, links to the same
6
+ // place for a browser that doesn't follow, and isn't indexed. Its script
7
+ // first takes the reader's choices from the address's parameters
8
+ // (`?platform=macos`), as if they'd picked those tabs, then forwards.
9
+ //
10
+ // A retired permalink with nowhere to lead gets a page that says what was
11
+ // here was removed, gives the reason as its authors wrote it, as text, and
12
+ // links to the docs' home: the root of the project whose list named it.
13
+ import { home as root, permalinks } from "virtual:ascribe/site";
14
+ import { forwardingPaths, type Forwarding } from "@ascribed/astro/permalinks";
15
+
16
+ export function getStaticPaths() {
17
+ return forwardingPaths(permalinks);
18
+ }
19
+
20
+ const props = Astro.props as Forwarding;
21
+ const to = "to" in props ? props.to : undefined;
22
+ const dimensions = "to" in props ? props.dimensions : {};
23
+ const reason = "reason" in props ? props.reason : undefined;
24
+ const home = ("reason" in props ? props.home : undefined) ?? root;
25
+ const canonical =
26
+ to === undefined || Astro.site === undefined ? to : new URL(to, Astro.site).href;
27
+ ---
28
+
29
+ <!doctype html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8" />
33
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
34
+ {to === undefined ? <title>Removed from these docs</title> : <title>Redirecting to {to}</title>}
35
+ <meta name="robots" content="noindex" />
36
+ {to !== undefined && <meta http-equiv="refresh" content={`0;url=${to}`} />}
37
+ {canonical !== undefined && <link rel="canonical" href={canonical} />}
38
+ </head>
39
+ <body>
40
+ {
41
+ to === undefined ? (
42
+ <main>
43
+ <h1>Removed from these docs</h1>
44
+ <p>What this link led to was removed from these docs.</p>
45
+ <p data-reason>{reason}</p>
46
+ <p>
47
+ <a href={home}>Go to the docs' home page</a>
48
+ </p>
49
+ </main>
50
+ ) : (
51
+ <p>
52
+ This link leads to <a href={to} data-dimensions={JSON.stringify(dimensions)}>{to}</a>.
53
+ </p>
54
+ )
55
+ }
56
+ <script>
57
+ import { choose } from "@ascribed/elements";
58
+
59
+ // A parameter that names one of the content model's dimensions, with one of
60
+ // its values, is the reader's choice for it. Anything else is ignored, so a
61
+ // product sends what it knows without asking what the docs accept.
62
+ const link = document.querySelector<HTMLAnchorElement>("a[data-dimensions]");
63
+ if (link !== null) {
64
+ const dimensions = JSON.parse(link.dataset["dimensions"] ?? "{}") as Record<string, string[]>;
65
+ const chosen = new Set<string>();
66
+ for (const [name, value] of new URLSearchParams(location.search)) {
67
+ if (chosen.has(name) || !Object.hasOwn(dimensions, name)) continue;
68
+ if (dimensions[name]?.includes(value)) {
69
+ chosen.add(name);
70
+ choose(name, value);
71
+ }
72
+ }
73
+ // Sooner than the refresh, which waits for the page to load.
74
+ location.replace(link.href);
75
+ }
76
+ </script>
77
+ </body>
78
+ </html>
@@ -0,0 +1,8 @@
1
+ import { defineCollection } from "astro:content";
2
+ import { ascribeCollection } from "@ascribed/astro/content";
3
+ // Generated by `ascribe build --emit site`; the integration runs it first.
4
+ import { schema } from "../.ascribe/build/site/site/_ascribe/schema.ts";
5
+
6
+ export const collections = {
7
+ docs: defineCollection(ascribeCollection({ schema })),
8
+ };
@@ -0,0 +1,87 @@
1
+ ---
2
+ import { getCollection, type CollectionEntry } from "astro:content";
3
+ import type { ComponentProps } from "astro/types";
4
+ import Availability from "@ascribed/astro/Availability.astro";
5
+ import Elements from "@ascribed/astro/Elements.astro";
6
+ // Everything the head says about the page, from its record.
7
+ import Head from "@ascribed/astro/Head.astro";
8
+ import { sidebar } from "@ascribed/astro/content";
9
+ import { nav } from "../nav.ts";
10
+
11
+ interface Props {
12
+ entry: CollectionEntry<"docs">;
13
+ headings: { depth: number; slug: string; text: string }[];
14
+ }
15
+
16
+ /**
17
+ * What the layout reads of a page's frontmatter: `title`, which every content
18
+ * type has, and what a type may have. `title` is plain text; `formatted.title`
19
+ * is the title as HTML, with its code spans as <code>, when the content model
20
+ * sets `inline = "code"` on `title`.
21
+ */
22
+ interface Page {
23
+ title: string;
24
+ available?: ComponentProps<typeof Availability>["available"];
25
+ formatted?: { title?: string };
26
+ }
27
+
28
+ const { entry, headings } = Astro.props;
29
+ const page = (of: CollectionEntry<"docs">) => of.data as Page;
30
+ /** A page's title as HTML: its formatted form, or its plain text escaped. */
31
+ const titleHtml = (of: CollectionEntry<"docs">) =>
32
+ page(of).formatted?.title ??
33
+ page(of).title.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
34
+ const { available } = page(entry);
35
+ // The sidebar: src/nav.ts's groups, each page with its address. The build
36
+ // fails when the list leaves out a page, or names one that doesn't exist.
37
+ const groups = sidebar(nav, await getCollection("docs"));
38
+ ---
39
+
40
+ <!doctype html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="utf-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
45
+ <Head entry={entry} />
46
+ <Elements />
47
+ </head>
48
+ <body>
49
+ <nav aria-label="Docs">
50
+ {
51
+ groups.map((group) => (
52
+ <section>
53
+ <h2>{group.label}</h2>
54
+ <ul>
55
+ {group.pages.map((item) => (
56
+ <li>
57
+ <a
58
+ href={item.href}
59
+ aria-current={item.entry.id === entry.id ? "page" : undefined}
60
+ >
61
+ <Fragment set:html={titleHtml(item.entry)} />
62
+ </a>
63
+ </li>
64
+ ))}
65
+ </ul>
66
+ </section>
67
+ ))
68
+ }
69
+ </nav>
70
+ <main>
71
+ <h1 set:html={titleHtml(entry)} />
72
+ <Availability available={available} />
73
+ <nav aria-label="On this page">
74
+ <ul>
75
+ {
76
+ headings.map((heading) => (
77
+ <li data-depth={heading.depth}>
78
+ <a href={`#${heading.slug}`}>{heading.text}</a>
79
+ </li>
80
+ ))
81
+ }
82
+ </ul>
83
+ </nav>
84
+ <article><slot /></article>
85
+ </main>
86
+ </body>
87
+ </html>
@@ -0,0 +1,13 @@
1
+ // The sidebar's list: its groups, in order, each with its pages' source files,
2
+ // relative to the content root. `npx @ascribed/astro init` wrote it from the
3
+ // pages there were then; it's yours to reorder, relabel, and add to. Ascribe's
4
+ // content model has no page order or grouping yet, so the site keeps the list,
5
+ // and the layout's `sidebar()` keeps it complete: the build fails on a page the
6
+ // list leaves out, and on a file it names that isn't a page.
7
+ import type { NavGroup } from "@ascribed/astro/content";
8
+
9
+ export const nav: NavGroup[] = [
10
+ { label: "Start", pages: ["index.md"] },
11
+ { label: "Guides", pages: ["Guides/My Setup.md"] },
12
+ { label: "Reference", pages: ["Reference/Options.md"] },
13
+ ];
@@ -0,0 +1,20 @@
1
+ ---
2
+ import { getCollection, render } from "astro:content";
3
+ import Docs from "../layouts/Docs.astro";
4
+
5
+ // A page's route is the base path plus its entry id, and the root page (id
6
+ // `index`) is at the base path itself: what Ascribe's router computes for
7
+ // every link it writes.
8
+ export async function getStaticPaths() {
9
+ const entries = await getCollection("docs");
10
+ return entries.map((entry) => ({
11
+ params: { slug: entry.id === "index" ? undefined : entry.id },
12
+ props: { entry },
13
+ }));
14
+ }
15
+
16
+ const { entry } = Astro.props;
17
+ const { Content, headings } = await render(entry);
18
+ ---
19
+
20
+ <Docs entry={entry} headings={headings}><Content /></Docs>