@chassis-ui/docs 0.6.0-next.0 → 0.6.1
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 +41 -0
- package/README.md +106 -1
- package/bin/chassis-docs.js +6 -0
- package/package.json +16 -2
- package/src/cli/assets.js +111 -0
- package/src/cli/files.js +97 -0
- package/src/cli/html-validate.js +111 -0
- package/src/cli/main.js +131 -0
- package/src/cli/shared.js +69 -0
- package/src/cli/vnu.js +94 -0
- package/src/components/shortcodes/Code.astro +2 -2
- package/src/layouts/BaseLayout.astro +5 -2
- package/src/layouts/footer/Consent.astro +135 -0
- package/src/layouts/footer/Footer.astro +11 -3
- package/src/layouts/head/Favicons.astro +0 -1
- package/src/layouts/head/Head.astro +8 -2
- package/src/layouts/header/Navigation.astro +1 -0
- package/src/libs/highlight.ts +2 -2
- package/src/libs/markdown.ts +13 -1
- package/src/libs/schema.ts +1 -1
- package/src/scss/_consent.scss +14 -0
- package/src/scss/_footer.scss +11 -1
- package/src/scss/main.scss +1 -1
- package/src/layouts/head/Analytics.astro +0 -31
- package/src/scss/fonts.scss +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @chassis-ui/docs
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- A code block can take focus, so that a keyboard can scroll one that is wider than the page. The Google Fonts stylesheet is linked from the head instead of imported by the styles, so the browser requests it with the page, not after the stylesheet that imported it.
|
|
8
|
+
- Google Analytics loads only after the visitor accepts. When `analytics.googleId` is set, production builds show a consent banner at the bottom of the page, and nothing is sent to Google and no cookie is set before the visitor accepts. The choice is kept in `localStorage`, so the sites served from one host share it. The footer has a "Cookie settings" button that opens the banner again, and a "Privacy" link to `/privacy/`. Declining stops Google Analytics and deletes its cookies.
|
|
9
|
+
- The head no longer links `safari-pinned-tab.svg` as a `mask-icon`. The docs build of chassis-assets has no such file, so every page asked for one that returned 404. Safari has used the regular favicon for pinned tabs since version 12.
|
|
10
|
+
- The header and the footer link Chassis React: "React" in the navigation of the header, "React Components" among the docs of the footer and "Chassis React" among its GitHub links. The footer lists the projects in the order of the header. The "Privacy" link and the "Cookie settings" button moved from the last column of the footer to below the license.
|
|
11
|
+
- The package installs a `chassis-docs` command with the build steps that every Chassis site shares, so that a site no longer needs its own copies of them. `chassis-docs vendor` builds the `vendor/assets` submodule at the pinned commit. `chassis-docs sync-submodules` moves it to the latest `app/docs` and builds it, and works on a fresh CI checkout too. `chassis-docs html-validate` and `chassis-docs vnu` validate the built HTML. The two validators need `html-validate` and `vnu-jar`, which are optional peer dependencies. See [Commands](https://github.com/chassis-ui/website/tree/main/packages/docs#commands) in the README.
|
|
12
|
+
- The "Skip to main content" link of the header has a target on every page. `BaseLayout` gives its `<main>` the id `content`, which only the docs layout had set, on its title. A page that fills the `main` slot of `BaseLayout` sets the id on an element of its own.
|
|
13
|
+
|
|
14
|
+
## 0.6.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- **Breaking.** The package no longer imports `@libs/config`, `@libs/content`, `@libs/data`, `@libs/path`, `@libs/clipboard` or `@scss/docs.scss` from the site. A site adds the integration instead. [UPGRADING.md](https://github.com/chassis-ui/website/blob/main/packages/docs/UPGRADING.md) has the steps.
|
|
19
|
+
- **Breaking.** The keys of `config.yml` are camelCase: `currentVersion`, `githubOrg`, `figmaHandle`, `xUsername` and `analytics.googleId`. An old name fails the build with a message that names the new one. The `docsDir` key is removed: the site root is the Astro root.
|
|
20
|
+
- **Breaking.** Three more keys are camelCase: `iconColor` in `data/sidebar.yml`, and `added.showBadge` and `extraJs` in the frontmatter of docs pages. An old name fails the build.
|
|
21
|
+
- **Breaking.** The schema of `config.yml` is strict. An unknown key fails the build. It was dropped silently.
|
|
22
|
+
- **Breaking.** `exports` lists the supported import paths only: the root entry, `integration`, `schema`, `site`, the four layouts and six components by name, `shortcodes/*`, `js/*`, `scss/main` and `scss/vars`. These no longer resolve: `libs/*`, the other partials under `scss/`, the folders under `layouts/`, and `components/shortcodes/*`. Import a shortcode as `@chassis-ui/docs/shortcodes/Icon.astro`.
|
|
23
|
+
- **Breaking.** `chassisAutoImport()` takes `{ root, dir, include, exclude }` and returns `{ imports, typeDefinitions, plugin }`. It finds the package's shortcodes from the package's own location, and the type declarations go to `.astro/`, not to `src/types/auto-import.d.ts`. Its `docsPath` and `modulesPath` options and its `integration()` are removed.
|
|
24
|
+
- **Breaking.** `getSourceFsPath()`, `getSourceUrl()` and `getSiteFileUrl()` moved from `libs/source` to `@chassis-ui/docs/site`.
|
|
25
|
+
- Add the `chassisDocs()` integration, at `@chassis-ui/docs/integration`. It reads `config.yml` and `data/sidebar.yml`, sets `site` and `markdown`, imports the shortcodes into MDX files and checks `[[docsref:]]` links. It finds every file from the Astro root, so the working directory of the build does not matter.
|
|
26
|
+
- `@chassis-ui/docs/schema` exports the schemas of `config.yml`, the sidebar and the `docs` and `callouts` collections, with their types. A site extends `configSchema` for keys of its own.
|
|
27
|
+
- `@chassis-ui/docs/site` exports what pages read from the site: `getConfig()`, `getSidebar()`, `getDocsPath()`, `getDocsPages()`, `getCallout()`, the path and URL helpers, `getPackageFilePath()` and `createDataLoader()`.
|
|
28
|
+
- The root entry exports `loadConfig()`, `loadData()`, `loadSidebar()`, the remark plugins `remarkCxConfig` and `remarkCxDocsref`, and one implementation of `getChassisTokensFsPath()`, `getChassisAssetsFsPath()`, `getChassisCSSFsPath()` and `getChassisIconsFsPath()`.
|
|
29
|
+
- The integration puts the default `chassis-tokens` of `@chassis-ui/css`, in its `scss/vendor` folder, on the Sass load path, after the site's own load paths. A site can delete the load path it set for it. A site with tokens of its own keeps the folder of its `_chassis-tokens.scss` in its config.
|
|
30
|
+
- Add `include` and `exclude` options for the shortcodes. A shortcode of the site replaces the package's shortcode of the same name.
|
|
31
|
+
- Add `js/clipboard.ts`, which every site carried a copy of.
|
|
32
|
+
- `githubOrg`, `figmaHandle`, `xUsername` and `analytics.googleId` are optional. The header links, the X meta tags and Google Analytics are left out when their key is not set.
|
|
33
|
+
- `anchors`, `toc`, `siteBranch` and `sourceDir` have defaults.
|
|
34
|
+
- Add a versioning policy to the README, and make `scss/vars` a public partial.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- The scripts of the package and of the site share one copy of `@chassis-ui/css`. In the dev server of a site that installed the package, `example-mode.js` was pre-bundled with a copy of its own, which registered every listener twice: a dialog opened and closed at once. The integration keeps both packages out of pre-bundling and dedupes `@chassis-ui/css`, so a site needs no alias for it.
|
|
39
|
+
- `<ScssDocs>`, `<ScssDocsSimple>` and `<JsDocs>` stop at the end marker of the part they show. A part whose name starts another name, such as `make-col` and `make-col-auto`, ran on to the end of the longer one.
|
|
40
|
+
- A table of contents whose first heading is deeper than a later one, such as an `<h3>` before the first `<h2>`, nested the `<h2>` under the `<h3>`.
|
|
41
|
+
- Packages whose types the shipped source imports are dependencies, not development dependencies.
|
|
42
|
+
- The README links a starter site, `packages/docs/starter` in the repository, to copy as the start of a new Chassis docs site.
|
|
43
|
+
|
|
3
44
|
## 0.6.0-next.0
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -34,6 +34,8 @@ pnpm add @chassis-ui/docs
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
Two more are optional. They are needed by the [commands](#commands) that validate HTML: `html-validate` `^11.0.0` and `vnu-jar` `>=26.0.0`.
|
|
38
|
+
|
|
37
39
|
A site also installs `@astrojs/mdx` and the rest of the Chassis stack, `@chassis-ui/tokens` and `@chassis-ui/icons`.
|
|
38
40
|
|
|
39
41
|
## Setup
|
|
@@ -114,7 +116,7 @@ currentVersion: '0.5.2'
|
|
|
114
116
|
| `githubOrg` | no | GitHub organisation name, not a URL. The header links to it |
|
|
115
117
|
| `figmaHandle` | no | Figma Community handle. The header links to it |
|
|
116
118
|
| `xUsername` | no | X handle. Used in the social meta tags |
|
|
117
|
-
| `analytics.googleId` | no | Google Analytics ID.
|
|
119
|
+
| `analytics.googleId` | no | Google Analytics ID. In production builds, loaded after the visitor accepts in a consent banner |
|
|
118
120
|
| `anchors.min`, `anchors.max` | no | Heading levels that get an anchor link. Default 2 to 5 |
|
|
119
121
|
| `toc.min`, `toc.max` | no | Heading levels in the table of contents. Default 2 to 6 |
|
|
120
122
|
| `sourceDir` | no | Directory that `file` props are relative to, from the site's root, e.g. `"../css"`. Default `"."` |
|
|
@@ -187,6 +189,8 @@ The layouts link to files under `/static/`, which the site puts into its `public
|
|
|
187
189
|
| `/static/icons/chassis-icons.svg`, `/static/icons/chassis-icons.css` | the `icons` folder of `@chassis-ui/icons` |
|
|
188
190
|
| `/static/images/*`: logo, favicons, social image | the docs build of `chassis-assets` |
|
|
189
191
|
|
|
192
|
+
`chassis-docs vendor` builds the docs build of `chassis-assets` in the `vendor/assets` submodule. See [Commands](#commands).
|
|
193
|
+
|
|
190
194
|
These helpers find the folders, whether `node_modules` is in the site's root or in the root of the repository: `getChassisCSSFsPath()`, `getChassisIconsFsPath()`, `getChassisAssetsFsPath()` and `getChassisTokensFsPath()`. Each takes `{ root, dir }`.
|
|
191
195
|
|
|
192
196
|
A site that type-checks with `astro check` declares the `@chassis-ui/css` module, which has no types: `declare module '@chassis-ui/css'`.
|
|
@@ -238,6 +242,88 @@ chassisDocs({
|
|
|
238
242
|
|
|
239
243
|
Code that runs while Astro loads its configuration uses the library functions of the root entry instead: `loadConfig()`, `loadData()`, `loadSidebar()` and the path helpers.
|
|
240
244
|
|
|
245
|
+
## Commands
|
|
246
|
+
|
|
247
|
+
The package installs a `chassis-docs` command with the build steps that every Chassis site shares. Call it from the scripts of `package.json`:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"scripts": {
|
|
252
|
+
"vendor": "chassis-docs vendor",
|
|
253
|
+
"sync-submodules": "chassis-docs sync-submodules",
|
|
254
|
+
"lint:html": "chassis-docs html-validate _site",
|
|
255
|
+
"lint:vnu": "chassis-docs vnu _site"
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Command | What it does |
|
|
261
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
262
|
+
| `chassis-docs vendor` | Checks out the `vendor/assets` submodule at the commit that the repository pins, pulls its Git LFS files, installs its dependencies and runs its `pnpm assets:site` |
|
|
263
|
+
| `chassis-docs sync-submodules` | Moves `vendor/assets` to the latest commit of the `app/docs` branch and builds it. Commit the new pointer afterwards |
|
|
264
|
+
| `chassis-docs html-validate [paths]` | Validates the HTML files in `paths` with [html-validate](https://html-validate.org) |
|
|
265
|
+
| `chassis-docs vnu [paths]` | Validates `paths` with the [Nu Html Checker](https://validator.github.io/validator/). Skipped, with a warning, when Java is missing |
|
|
266
|
+
| `chassis-docs help` | Lists the commands and their options |
|
|
267
|
+
|
|
268
|
+
`vendor` and `sync-submodules` work from any folder of the repository. They need Git, Git LFS and pnpm. `sync-submodules --branch <name>`, or the `SUBMODULE_BRANCH` variable, follows another branch.
|
|
269
|
+
|
|
270
|
+
Each command exits with 1 when it fails, so a script or CI job stops there.
|
|
271
|
+
|
|
272
|
+
### Validating HTML
|
|
273
|
+
|
|
274
|
+
`html-validate` and `vnu` take any number of files and folders, `_site` when none is given. Paths are relative to the folder the script runs in, which for a site in `packages/site` is that folder when the script is in its `package.json`, and the repository root when it is in the root one.
|
|
275
|
+
|
|
276
|
+
What a site can change:
|
|
277
|
+
|
|
278
|
+
| To | Use | Validator | Adds to the defaults or replaces them |
|
|
279
|
+
| ----------------------------------- | ------------------------------------- | ------------- | ------------------------------------- |
|
|
280
|
+
| Skip a file or folder | `--ignore <path>`, repeatable | both | adds |
|
|
281
|
+
| Check `static/icons` too | `--no-default-ignore` | both | removes the default |
|
|
282
|
+
| Turn a rule off or change it | `rules` in the file of `--config` | html-validate | overrides the rule |
|
|
283
|
+
| Use more presets or plugins | `extends` in the file of `--config` | html-validate | adds |
|
|
284
|
+
| Allow custom elements or attributes | `elements` in the file of `--config` | html-validate | replaces |
|
|
285
|
+
| Leave out a message | `--filter <regex>` or `--filter-file` | vnu | adds |
|
|
286
|
+
|
|
287
|
+
The defaults:
|
|
288
|
+
|
|
289
|
+
- Both skip `static/icons` in each path, where a site copies `@chassis-ui/icons`, whose preview page is not the site's.
|
|
290
|
+
- `html-validate` extends `html-validate:recommended` and `html-validate:document`. It turns off `void-style`, since Astro writes void elements with a slash, `no-inline-style`, since Shiki writes inline styles, and `require-sri`. `heading-level` lets a page start at `h2`, as the dialogs of the layouts do.
|
|
291
|
+
- `vnu` fails on warnings as well as errors, and leaves out "Trailing slash on void elements", which Astro writes.
|
|
292
|
+
|
|
293
|
+
A site with its own exceptions keeps them in files next to its `package.json`, and names them in its scripts:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"scripts": {
|
|
298
|
+
"lint:html": "chassis-docs html-validate _site --ignore _site/examples --config html-validate.json",
|
|
299
|
+
"lint:vnu": "chassis-docs vnu _site --ignore _site/examples --filter-file vnu-filters.txt"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`html-validate.json`:
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"rules": {
|
|
309
|
+
"prefer-native-element": ["error", { "exclude": ["region"] }],
|
|
310
|
+
"prefer-button": "off"
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`vnu-filters.txt`, one regular expression per line, where a line that starts with `#` is a comment:
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
# The docs show autocomplete on inputs of every type
|
|
319
|
+
Attribute “autocomplete” is only allowed when the input type is.*
|
|
320
|
+
Bad value “heading” for attribute “role” on element “summary”.
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
A filter is matched against the message as the output shows it, curly quotes included: `“role”`. A line copied from the output works.
|
|
324
|
+
|
|
325
|
+
A rule or filter applies to every file that the command checks. To make an exception for one page only, skip the page with `--ignore`, or disable the rule inside the page's markup with a comment of html-validate, such as `<!-- [html-validate-disable-next prefer-button] -->`.
|
|
326
|
+
|
|
241
327
|
## Exports
|
|
242
328
|
|
|
243
329
|
These import paths are the public API.
|
|
@@ -259,6 +345,24 @@ No other path resolves: `exports` in `package.json` lists these and nothing else
|
|
|
259
345
|
|
|
260
346
|
The integration provides four modules: `virtual:chassis-docs/config`, `virtual:chassis-docs/sidebar`, `virtual:chassis-docs/paths` and `virtual:chassis-docs/styles`. Use the functions of `@chassis-ui/docs/site` instead of importing them.
|
|
261
347
|
|
|
348
|
+
## Compatibility
|
|
349
|
+
|
|
350
|
+
"Requires" is what `package.json` of each version declares. "Built with" is what the website in this repository resolved and built when the version was released. CI also builds the [starter site](starter/) with the newest versions that its ranges accept.
|
|
351
|
+
|
|
352
|
+
| `@chassis-ui/docs` | Requires Astro | Requires `@chassis-ui/css` | Requires Node | Built with |
|
|
353
|
+
| ------------------ | -------------------------- | -------------------------- | ------------- | ------------------------------------ |
|
|
354
|
+
| 0.6.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
|
|
355
|
+
| 0.6.0 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
|
|
356
|
+
| 0.5.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
|
|
357
|
+
| 0.5.0 | `^7.0.0` | `^0.5.0-0` | not declared | not recorded |
|
|
358
|
+
| 0.4.0 | `^7.0.0` | `^0.4.0` | not declared | not recorded |
|
|
359
|
+
| 0.3.0 to 0.3.10 | `^7.0.0` | `>=0.3.1` to `>=0.3.4` | not declared | not recorded |
|
|
360
|
+
| 0.1.0 to 0.2.0 | `^5.0.0`, 0.1.0–0.1.1 `^4` | not declared | not declared | not recorded |
|
|
361
|
+
|
|
362
|
+
`@chassis-ui/tokens` is not a dependency of the package. The styles take the design tokens that `@chassis-ui/css` carries: css 0.5 is built on tokens 0.5, css 0.4 on tokens 0.4. A site's own version of `@chassis-ui/tokens` matters only when the site puts it on the Sass load path in place of those. Tokens 0.6 renamed and removed some tokens, listed in its changelog.
|
|
363
|
+
|
|
364
|
+
Before a release, the [canary](https://github.com/chassis-ui/website/blob/main/build/canary.js) builds the site of each Chassis repository whose range accepts the new version.
|
|
365
|
+
|
|
262
366
|
## Versioning
|
|
263
367
|
|
|
264
368
|
The package follows [Semantic Versioning](https://semver.org/). Before 1.0 that means:
|
|
@@ -270,6 +374,7 @@ A change is breaking when it changes something that this README documents:
|
|
|
270
374
|
|
|
271
375
|
- the import paths listed under [Exports](#exports), and what they export
|
|
272
376
|
- the options of the integration
|
|
377
|
+
- the commands of `chassis-docs`, their options and their defaults
|
|
273
378
|
- the keys of `config.yml`, and the schemas of the sidebar and the content collections
|
|
274
379
|
- the props and slots of the layouts, components and shortcodes
|
|
275
380
|
- the `[[config:]]` and `[[docsref:]]` syntax
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chassis-ui/docs",
|
|
3
3
|
"description": "Shared Astro components and layouts for Chassis documentation sites.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"design-system"
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
|
+
"bin",
|
|
26
27
|
"src",
|
|
27
28
|
"index.ts",
|
|
28
29
|
"virtual.d.ts",
|
|
@@ -31,6 +32,9 @@
|
|
|
31
32
|
"LICENSE",
|
|
32
33
|
"CHANGELOG.md"
|
|
33
34
|
],
|
|
35
|
+
"bin": {
|
|
36
|
+
"chassis-docs": "./bin/chassis-docs.js"
|
|
37
|
+
},
|
|
34
38
|
"main": "index.ts",
|
|
35
39
|
"exports": {
|
|
36
40
|
".": "./index.ts",
|
|
@@ -70,10 +74,20 @@
|
|
|
70
74
|
"@pagefind/component-ui": "^1.0.0",
|
|
71
75
|
"@shikijs/transformers": "^4.3.1",
|
|
72
76
|
"astro": "^7.0.0",
|
|
77
|
+
"html-validate": "^11.0.0",
|
|
73
78
|
"htmlparser2": "^12.0.0",
|
|
74
79
|
"rehype-autolink-headings": "^7.0.0",
|
|
75
80
|
"sass": "^1.101.0",
|
|
76
|
-
"shiki": "^4.3.1"
|
|
81
|
+
"shiki": "^4.3.1",
|
|
82
|
+
"vnu-jar": ">=26.0.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependenciesMeta": {
|
|
85
|
+
"html-validate": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"vnu-jar": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
77
91
|
},
|
|
78
92
|
"dependencies": {
|
|
79
93
|
"@types/hast": "^3.0.5",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { CommandError, log, run as defaultRun } from './shared.js'
|
|
4
|
+
|
|
5
|
+
/** Where every Chassis site has chassis-assets as a submodule. */
|
|
6
|
+
export const ASSETS_PATH = 'vendor/assets'
|
|
7
|
+
|
|
8
|
+
/** The docs build of chassis-assets, inside the submodule. `getChassisAssetsFsPath()` reads it. */
|
|
9
|
+
export const ASSETS_OUTPUT = 'dist/web/docs/chassis'
|
|
10
|
+
|
|
11
|
+
/** The branch of chassis-assets that the docs sites follow. */
|
|
12
|
+
export const DEFAULT_BRANCH = 'app/docs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The root of the repository that contains the working directory, so that the commands
|
|
16
|
+
* work from a site's folder too.
|
|
17
|
+
* @param {typeof defaultRun} run
|
|
18
|
+
*/
|
|
19
|
+
function repositoryRoot(run) {
|
|
20
|
+
return run('git', ['rev-parse', '--show-toplevel'], { quiet: true }).trim()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks out `vendor/assets` at the commit that the repository pins, and builds it.
|
|
25
|
+
* @param {{ root?: string, run?: typeof defaultRun }} [options]
|
|
26
|
+
*/
|
|
27
|
+
export function buildAssets({ run = defaultRun, root = repositoryRoot(run) } = {}) {
|
|
28
|
+
log(`Building ${ASSETS_PATH} at the pinned commit`)
|
|
29
|
+
run('git', ['submodule', 'update', '--init', ASSETS_PATH], { cwd: root })
|
|
30
|
+
buildCheckout(root, run)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Moves `vendor/assets` to the latest commit of `branch`, and builds it. The repository
|
|
35
|
+
* then has a changed submodule pointer to commit.
|
|
36
|
+
* @param {{ root?: string, branch?: string, run?: typeof defaultRun }} [options]
|
|
37
|
+
*/
|
|
38
|
+
export function syncAssets({
|
|
39
|
+
run = defaultRun,
|
|
40
|
+
root = repositoryRoot(run),
|
|
41
|
+
branch = DEFAULT_BRANCH
|
|
42
|
+
} = {}) {
|
|
43
|
+
const dir = path.join(root, ASSETS_PATH)
|
|
44
|
+
const git = (args, quiet = true) => run('git', args, { cwd: dir, quiet })
|
|
45
|
+
|
|
46
|
+
log(`Moving ${ASSETS_PATH} to the latest ${branch}`)
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(path.join(dir, '.git'))) {
|
|
49
|
+
run('git', ['submodule', 'update', '--init', ASSETS_PATH], { cwd: root })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (git(['rev-parse', '--abbrev-ref', 'HEAD']).trim() !== branch) {
|
|
53
|
+
if (git(['status', '--porcelain']).trim()) {
|
|
54
|
+
throw new CommandError(
|
|
55
|
+
`${ASSETS_PATH} has uncommitted changes. Commit or discard them, then run the command again.`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A fresh checkout, as on CI, has the submodule at a detached HEAD, fetched for the
|
|
60
|
+
// pinned commit only: there is neither a local branch nor a remote-tracking one.
|
|
61
|
+
git(['fetch', 'origin', branch])
|
|
62
|
+
|
|
63
|
+
if (hasLocalBranch(git, branch)) {
|
|
64
|
+
git(['checkout', branch])
|
|
65
|
+
} else {
|
|
66
|
+
git(['checkout', '-b', branch, 'FETCH_HEAD'])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
git(['pull', '--ff-only', 'origin', branch])
|
|
71
|
+
buildCheckout(root, run)
|
|
72
|
+
|
|
73
|
+
const commit = git(['rev-parse', '--short', 'HEAD']).trim()
|
|
74
|
+
const moved = run('git', ['status', '--porcelain', '--', ASSETS_PATH], {
|
|
75
|
+
cwd: root,
|
|
76
|
+
quiet: true
|
|
77
|
+
}).trim()
|
|
78
|
+
|
|
79
|
+
if (moved) {
|
|
80
|
+
log(`${ASSETS_PATH} is at ${commit}. Check the site, then commit ${ASSETS_PATH} on its own.`)
|
|
81
|
+
} else {
|
|
82
|
+
log(`${ASSETS_PATH} is already at the latest ${branch}, ${commit}.`, 'success')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasLocalBranch(git, branch) {
|
|
87
|
+
try {
|
|
88
|
+
git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`])
|
|
89
|
+
return true
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildCheckout(root, run) {
|
|
96
|
+
const dir = path.join(root, ASSETS_PATH)
|
|
97
|
+
|
|
98
|
+
// The images are stored with Git LFS. Without it they are pointer files.
|
|
99
|
+
run('git', ['lfs', 'install', '--local'], { cwd: dir, quiet: true })
|
|
100
|
+
run('git', ['lfs', 'pull'], { cwd: dir })
|
|
101
|
+
run('pnpm', ['install', '--ignore-workspace'], { cwd: dir })
|
|
102
|
+
run('pnpm', ['assets:site'], { cwd: dir })
|
|
103
|
+
|
|
104
|
+
const output = path.join(dir, ASSETS_OUTPUT)
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(output)) {
|
|
107
|
+
throw new CommandError(`The build of ${ASSETS_PATH} wrote nothing to ${ASSETS_OUTPUT}.`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
log(`Built ${path.join(ASSETS_PATH, ASSETS_OUTPUT)}`, 'success')
|
|
111
|
+
}
|
package/src/cli/files.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { CommandError } from './shared.js'
|
|
4
|
+
|
|
5
|
+
/** The default output folder of a Chassis site. */
|
|
6
|
+
export const DEFAULT_PATHS = ['_site']
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The folders to skip when no `--ignore` is given: `static/icons` of each path, the files
|
|
10
|
+
* that a site copies from `@chassis-ui/icons`. They include a preview page of their own.
|
|
11
|
+
* @param {string[]} paths
|
|
12
|
+
*/
|
|
13
|
+
export function defaultIgnores(paths) {
|
|
14
|
+
return paths.map((dir) => path.join(dir, 'static/icons'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether `file` is one of `ignores` or inside one of them.
|
|
19
|
+
* @param {string} file
|
|
20
|
+
* @param {string[]} ignores
|
|
21
|
+
*/
|
|
22
|
+
export function isIgnored(file, ignores) {
|
|
23
|
+
const target = path.resolve(file)
|
|
24
|
+
|
|
25
|
+
return ignores.some((ignore) => isInside(target, path.resolve(ignore)))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isInside(target, dir) {
|
|
29
|
+
const relative = path.relative(dir, target)
|
|
30
|
+
|
|
31
|
+
return relative === '' || (relative.split(path.sep)[0] !== '..' && !path.isAbsolute(relative))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkExists(paths) {
|
|
35
|
+
for (const entry of paths) {
|
|
36
|
+
if (!fs.existsSync(entry)) {
|
|
37
|
+
throw new CommandError(`${entry} does not exist. Build the site first.`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Lists the HTML files in `paths`, which are files or folders, leaving out `ignores` and
|
|
44
|
+
* `node_modules`.
|
|
45
|
+
* @param {string[]} paths
|
|
46
|
+
* @param {string[]} ignores
|
|
47
|
+
* @returns {string[]}
|
|
48
|
+
*/
|
|
49
|
+
export function findHtmlFiles(paths, ignores) {
|
|
50
|
+
const files = []
|
|
51
|
+
|
|
52
|
+
const visit = (entry) => {
|
|
53
|
+
if (isIgnored(entry, ignores)) return
|
|
54
|
+
|
|
55
|
+
if (fs.statSync(entry).isDirectory()) {
|
|
56
|
+
for (const name of fs.readdirSync(entry).sort()) {
|
|
57
|
+
if (name !== 'node_modules') visit(path.join(entry, name))
|
|
58
|
+
}
|
|
59
|
+
} else if (entry.endsWith('.html')) {
|
|
60
|
+
files.push(entry)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
checkExists(paths)
|
|
65
|
+
paths.forEach(visit)
|
|
66
|
+
|
|
67
|
+
return files
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns `paths` with `ignores` taken out. A folder that contains an ignored path is
|
|
72
|
+
* replaced by its entries, so that a checker that walks folders never reaches it.
|
|
73
|
+
* @param {string[]} paths
|
|
74
|
+
* @param {string[]} ignores
|
|
75
|
+
* @returns {string[]}
|
|
76
|
+
*/
|
|
77
|
+
export function expandPaths(paths, ignores) {
|
|
78
|
+
const result = []
|
|
79
|
+
|
|
80
|
+
const visit = (entry) => {
|
|
81
|
+
if (isIgnored(entry, ignores)) return
|
|
82
|
+
|
|
83
|
+
const target = path.resolve(entry)
|
|
84
|
+
const containsIgnored = ignores.some((ignore) => isInside(path.resolve(ignore), target))
|
|
85
|
+
|
|
86
|
+
if (containsIgnored && fs.statSync(entry).isDirectory()) {
|
|
87
|
+
for (const name of fs.readdirSync(entry).sort()) visit(path.join(entry, name))
|
|
88
|
+
} else {
|
|
89
|
+
result.push(entry)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
checkExists(paths)
|
|
94
|
+
paths.forEach(visit)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { CommandError, importPeer, log } from './shared.js'
|
|
3
|
+
import { findHtmlFiles } from './files.js'
|
|
4
|
+
|
|
5
|
+
/** What the layouts and shortcodes of the package need. A site adds its own on top. */
|
|
6
|
+
export const defaultConfig = {
|
|
7
|
+
extends: ['html-validate:recommended', 'html-validate:document'],
|
|
8
|
+
rules: {
|
|
9
|
+
// Astro writes void elements with a trailing slash
|
|
10
|
+
'void-style': 'off',
|
|
11
|
+
// Shiki highlights code with inline styles
|
|
12
|
+
'no-inline-style': 'off',
|
|
13
|
+
// The dialogs start at h2, and the `document` preset expects h1 in each sectioning root
|
|
14
|
+
'heading-level': ['error', { minInitialRank: 'h2', sectioningRoots: [] }],
|
|
15
|
+
// The layouts load their files from the site itself
|
|
16
|
+
'require-sri': 'off'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Adds a site's config to the default one: `extends` is appended, `rules` are merged,
|
|
22
|
+
* and `elements` is replaced.
|
|
23
|
+
* @param {Record<string, any>} base
|
|
24
|
+
* @param {Record<string, any>} [extra]
|
|
25
|
+
* @returns {Record<string, any>}
|
|
26
|
+
*/
|
|
27
|
+
export function mergeConfig(base, extra = {}) {
|
|
28
|
+
return {
|
|
29
|
+
...base,
|
|
30
|
+
...extra,
|
|
31
|
+
extends: [...(base.extends ?? []), ...(extra.extends ?? [])],
|
|
32
|
+
rules: { ...base.rules, ...extra.rules }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reads a config file of html-validate.
|
|
38
|
+
* @param {string} file
|
|
39
|
+
*/
|
|
40
|
+
export function readConfig(file) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new CommandError(`Could not read ${file}: ${error.message}`, { cause: error })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validating hundreds of files at once runs out of file descriptors.
|
|
49
|
+
async function mapWithConcurrency(limit, items, fn) {
|
|
50
|
+
const results = []
|
|
51
|
+
const running = new Set()
|
|
52
|
+
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const promise = fn(item).finally(() => running.delete(promise))
|
|
55
|
+
|
|
56
|
+
running.add(promise)
|
|
57
|
+
results.push(promise)
|
|
58
|
+
|
|
59
|
+
if (running.size >= limit) await Promise.race(running)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Promise.all(results)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validates the HTML files in `paths` with html-validate. Returns the exit code.
|
|
67
|
+
* @param {{ paths: string[], ignores: string[], config?: string }} options
|
|
68
|
+
*/
|
|
69
|
+
export async function htmlValidate({ paths, ignores, config }) {
|
|
70
|
+
const { HtmlValidate, Severity } = await importPeer('html-validate')
|
|
71
|
+
const validator = new HtmlValidate(mergeConfig(defaultConfig, config && readConfig(config)))
|
|
72
|
+
const files = findHtmlFiles(paths, ignores)
|
|
73
|
+
|
|
74
|
+
log(`Validating ${files.length} HTML files with html-validate`)
|
|
75
|
+
|
|
76
|
+
const reports = await mapWithConcurrency(10, files, async (file) => ({
|
|
77
|
+
file,
|
|
78
|
+
report: await validator.validateFile(file)
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
let issueCount = 0
|
|
82
|
+
let fileCount = 0
|
|
83
|
+
|
|
84
|
+
for (const { file, report } of reports) {
|
|
85
|
+
const messages = (report.results[0]?.messages ?? []).filter(
|
|
86
|
+
(message) => message.severity >= Severity.WARN
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (messages.length === 0) continue
|
|
90
|
+
|
|
91
|
+
fileCount++
|
|
92
|
+
issueCount += messages.length
|
|
93
|
+
console.error(`\n${file}`)
|
|
94
|
+
|
|
95
|
+
for (const message of messages) {
|
|
96
|
+
const level = message.severity === Severity.ERROR ? 'error' : 'warning'
|
|
97
|
+
|
|
98
|
+
console.error(
|
|
99
|
+
` ${message.line}:${message.column} ${level} ${message.message} (${message.ruleId})`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (issueCount > 0) {
|
|
105
|
+
log(`\n${issueCount} issues in ${fileCount} files.`, 'error')
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log('Every HTML file is valid.', 'success')
|
|
110
|
+
return 0
|
|
111
|
+
}
|
package/src/cli/main.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import { DEFAULT_BRANCH, buildAssets, syncAssets } from './assets.js'
|
|
3
|
+
import { DEFAULT_PATHS, defaultIgnores } from './files.js'
|
|
4
|
+
import { htmlValidate } from './html-validate.js'
|
|
5
|
+
import { CommandError, log } from './shared.js'
|
|
6
|
+
import { vnu } from './vnu.js'
|
|
7
|
+
|
|
8
|
+
export const usage = `Usage: chassis-docs <command> [options]
|
|
9
|
+
|
|
10
|
+
Paths are relative to the working directory. vendor and sync-submodules work from any
|
|
11
|
+
folder of the repository.
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
vendor Build vendor/assets at the commit that the repository pins
|
|
15
|
+
sync-submodules Move vendor/assets to the latest commit of a branch, and build it
|
|
16
|
+
--branch <name> The branch of chassis-assets (default: $SUBMODULE_BRANCH or ${DEFAULT_BRANCH})
|
|
17
|
+
html-validate [paths] Validate the HTML files in paths (default: _site) with html-validate
|
|
18
|
+
--config <file> A JSON config of html-validate, added to the defaults
|
|
19
|
+
vnu [paths] Validate paths (default: _site) with the Nu Html Checker. Needs Java
|
|
20
|
+
--filter <regex> Leave out the messages that match. Repeatable
|
|
21
|
+
--filter-file <f> Read filters from a file, one per line
|
|
22
|
+
|
|
23
|
+
Both validators:
|
|
24
|
+
--ignore <path> Skip a file or folder as well. Repeatable
|
|
25
|
+
--no-default-ignore Check static/icons of each path, which is skipped by default
|
|
26
|
+
help Show this message
|
|
27
|
+
|
|
28
|
+
html-validate and vnu need the html-validate and vnu-jar packages in the site's devDependencies.`
|
|
29
|
+
|
|
30
|
+
const ignoreOptions = {
|
|
31
|
+
ignore: { type: 'string', multiple: true },
|
|
32
|
+
'no-default-ignore': { type: 'boolean' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The static/icons folder of each path, unless left out, and the paths of --ignore.
|
|
36
|
+
function ignoresOf(values, paths) {
|
|
37
|
+
return [...(values['no-default-ignore'] ? [] : defaultIgnores(paths)), ...(values.ignore ?? [])]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const commands = {
|
|
41
|
+
vendor: {
|
|
42
|
+
options: {},
|
|
43
|
+
run: () => {
|
|
44
|
+
buildAssets()
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
'sync-submodules': {
|
|
49
|
+
options: { branch: { type: 'string' } },
|
|
50
|
+
run: ({ values }) => {
|
|
51
|
+
syncAssets({ branch: values.branch ?? process.env.SUBMODULE_BRANCH ?? DEFAULT_BRANCH })
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
'html-validate': {
|
|
56
|
+
options: { config: { type: 'string' }, ...ignoreOptions },
|
|
57
|
+
positionals: true,
|
|
58
|
+
run: ({ values, paths }) =>
|
|
59
|
+
htmlValidate({
|
|
60
|
+
paths,
|
|
61
|
+
ignores: ignoresOf(values, paths),
|
|
62
|
+
config: values.config
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
vnu: {
|
|
66
|
+
options: {
|
|
67
|
+
filter: { type: 'string', multiple: true },
|
|
68
|
+
'filter-file': { type: 'string' },
|
|
69
|
+
...ignoreOptions
|
|
70
|
+
},
|
|
71
|
+
positionals: true,
|
|
72
|
+
run: ({ values, paths }) =>
|
|
73
|
+
vnu({
|
|
74
|
+
paths,
|
|
75
|
+
ignores: ignoresOf(values, paths),
|
|
76
|
+
filters: values.filter,
|
|
77
|
+
filterFile: values['filter-file']
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runs a command and returns its exit code.
|
|
84
|
+
* @param {string[]} argv The arguments after `chassis-docs`
|
|
85
|
+
* @returns {Promise<number>}
|
|
86
|
+
*/
|
|
87
|
+
export async function main(argv) {
|
|
88
|
+
const [name, ...args] = argv
|
|
89
|
+
|
|
90
|
+
if (!name || ['help', '--help', '-h'].includes(name)) {
|
|
91
|
+
console.log(usage)
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const command = commands[name]
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (!Object.hasOwn(commands, name)) {
|
|
99
|
+
throw new CommandError(`Unknown command '${name}'. Run chassis-docs help for the list.`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let parsed
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
parsed = parseArgs({
|
|
106
|
+
args,
|
|
107
|
+
options: { ...command.options, help: { type: 'boolean', short: 'h' } },
|
|
108
|
+
allowPositionals: command.positionals ?? false,
|
|
109
|
+
strict: true
|
|
110
|
+
})
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new CommandError(`${error.message}. Run chassis-docs help for the options.`, {
|
|
113
|
+
cause: error
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parsed.values.help) {
|
|
118
|
+
console.log(usage)
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const paths = parsed.positionals.length > 0 ? parsed.positionals : DEFAULT_PATHS
|
|
123
|
+
|
|
124
|
+
return await command.run({ values: parsed.values, paths })
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!(error instanceof CommandError)) throw error
|
|
127
|
+
|
|
128
|
+
log(error.message, 'error')
|
|
129
|
+
return 1
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { styleText } from 'node:util'
|
|
3
|
+
|
|
4
|
+
/** An error that the command reports in one line, without a stack trace. */
|
|
5
|
+
export class CommandError extends Error {}
|
|
6
|
+
|
|
7
|
+
const colors = { info: 'cyan', success: 'green', warning: 'yellow', error: 'red' }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prints a message in the colour of its type. Errors and warnings go to stderr.
|
|
11
|
+
* @param {string} message
|
|
12
|
+
* @param {'info' | 'success' | 'warning' | 'error'} [type]
|
|
13
|
+
*/
|
|
14
|
+
export function log(message, type = 'info') {
|
|
15
|
+
const stream = type === 'error' || type === 'warning' ? process.stderr : process.stdout
|
|
16
|
+
|
|
17
|
+
stream.write(`${styleText(colors[type], message)}\n`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs a program without a shell, except on Windows, where `pnpm` is a `.cmd` file.
|
|
22
|
+
* Returns what it printed when `quiet` is set, and prints it otherwise.
|
|
23
|
+
* @param {string} command
|
|
24
|
+
* @param {string[]} args
|
|
25
|
+
* @param {{ cwd?: string, quiet?: boolean }} [options]
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function run(command, args, { cwd = process.cwd(), quiet = false } = {}) {
|
|
29
|
+
if (!quiet) {
|
|
30
|
+
log(`$ ${[command, ...args].join(' ')}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return (
|
|
35
|
+
execFileSync(command, args, {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: quiet ? 'pipe' : 'inherit',
|
|
39
|
+
shell: process.platform === 'win32'
|
|
40
|
+
}) ?? ''
|
|
41
|
+
)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr.trim() : ''
|
|
44
|
+
|
|
45
|
+
throw new CommandError(
|
|
46
|
+
`${[command, ...args].join(' ')} failed${stderr ? `:\n${stderr}` : ''}`,
|
|
47
|
+
{ cause: error }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Imports an optional peer dependency, or says how to install it.
|
|
54
|
+
* @param {string} name
|
|
55
|
+
*/
|
|
56
|
+
export async function importPeer(name) {
|
|
57
|
+
try {
|
|
58
|
+
return await import(name)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(`'${name}'`)) {
|
|
61
|
+
throw new CommandError(
|
|
62
|
+
`${name} is not installed. Add it to the devDependencies of the site: pnpm add -D ${name}`,
|
|
63
|
+
{ cause: error }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/cli/vnu.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { CommandError, importPeer, log } from './shared.js'
|
|
4
|
+
import { expandPaths } from './files.js'
|
|
5
|
+
|
|
6
|
+
/** Messages that every Chassis site produces. Astro writes void elements with a slash. */
|
|
7
|
+
export const DEFAULT_FILTERS = ['Trailing slash on void elements.*']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads a filter file: one regular expression per line. Blank lines and lines that start
|
|
11
|
+
* with `#` are skipped.
|
|
12
|
+
* @param {string} file
|
|
13
|
+
*/
|
|
14
|
+
export function readFilterFile(file) {
|
|
15
|
+
let text
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
text = fs.readFileSync(file, 'utf8')
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new CommandError(`Could not read ${file}: ${error.message}`, { cause: error })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return text
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The arguments for `java`.
|
|
31
|
+
* @param {{ jar: string, paths: string[], filters: string[], is32bit?: boolean }} options
|
|
32
|
+
*/
|
|
33
|
+
export function vnuArgs({ jar, paths, filters, is32bit = false }) {
|
|
34
|
+
return [
|
|
35
|
+
// 32-bit Java needs a smaller thread stack
|
|
36
|
+
...(is32bit ? ['-Xss512k'] : []),
|
|
37
|
+
'-jar',
|
|
38
|
+
jar,
|
|
39
|
+
'--skip-non-html',
|
|
40
|
+
'--Werror',
|
|
41
|
+
'--filterpattern',
|
|
42
|
+
filters.join('|'),
|
|
43
|
+
...paths
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function javaVersion() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
// `java -version` prints to stderr
|
|
50
|
+
execFile('java', ['-version'], (error, _stdout, stderr) => resolve(error ? null : stderr))
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates `paths` with the Nu Html Checker. Skips, and succeeds, when Java is missing.
|
|
56
|
+
* Returns the exit code.
|
|
57
|
+
* @param {{ paths: string[], ignores: string[], filters?: string[], filterFile?: string }} options
|
|
58
|
+
*/
|
|
59
|
+
export async function vnu({ paths, ignores, filters = [], filterFile }) {
|
|
60
|
+
const { default: jar } = await importPeer('vnu-jar')
|
|
61
|
+
const allFilters = [
|
|
62
|
+
...DEFAULT_FILTERS,
|
|
63
|
+
...filters,
|
|
64
|
+
...(filterFile ? readFilterFile(filterFile) : [])
|
|
65
|
+
]
|
|
66
|
+
const targets = expandPaths(paths, ignores)
|
|
67
|
+
const version = await javaVersion()
|
|
68
|
+
|
|
69
|
+
if (version === null) {
|
|
70
|
+
log('Java is not installed. Skipping the Nu Html Checker.', 'warning')
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log('Validating HTML with the Nu Html Checker')
|
|
75
|
+
|
|
76
|
+
const args = vnuArgs({
|
|
77
|
+
jar,
|
|
78
|
+
paths: targets,
|
|
79
|
+
filters: allFilters,
|
|
80
|
+
is32bit: !/64-Bit/.test(version)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const code = await new Promise((resolve) => {
|
|
84
|
+
spawn('java', args, { stdio: 'inherit' }).on('exit', (exitCode) => resolve(exitCode ?? 1))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (code === 0) {
|
|
88
|
+
log('Every HTML file is valid.', 'success')
|
|
89
|
+
} else {
|
|
90
|
+
log('The Nu Html Checker found errors.', 'error')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return code
|
|
94
|
+
}
|
|
@@ -226,7 +226,7 @@ if (codeToDisplay && detectedLang) {
|
|
|
226
226
|
) : (
|
|
227
227
|
<div class:list={['cxd-code', { 'line-numbers': lineNumbers }]}>
|
|
228
228
|
{/* prettier-ignore */}
|
|
229
|
-
<pre class={className}><slot /></pre>
|
|
229
|
+
<pre class={className} tabindex="0"><slot /></pre>
|
|
230
230
|
</div>
|
|
231
231
|
)}
|
|
232
232
|
</>
|
|
@@ -279,7 +279,7 @@ if (codeToDisplay && detectedLang) {
|
|
|
279
279
|
) : (
|
|
280
280
|
<div class:list={['cxd-code', { 'line-numbers': lineNumbers }]}>
|
|
281
281
|
{/* prettier-ignore */}
|
|
282
|
-
<pre class={className}><slot /></pre>
|
|
282
|
+
<pre class={className} tabindex="0"><slot /></pre>
|
|
283
283
|
</div>
|
|
284
284
|
)}
|
|
285
285
|
</div>
|
|
@@ -12,6 +12,7 @@ import Head from './head/Head.astro'
|
|
|
12
12
|
import Header from './header/Header.astro'
|
|
13
13
|
import Scripts from './footer/Scripts.astro'
|
|
14
14
|
import Footer from './footer/Footer.astro'
|
|
15
|
+
import Consent from './footer/Consent.astro'
|
|
15
16
|
import { stripMarkdown } from '../libs/utils'
|
|
16
17
|
|
|
17
18
|
// The following props can be directly passed to the base layout component from any page or layout extending it,
|
|
@@ -25,7 +26,8 @@ type Props = {
|
|
|
25
26
|
overrides?: {
|
|
26
27
|
body?: LayoutOverridesHTMLAttributes<'body'>
|
|
27
28
|
// Note that main can also be overridden by the Astro slot named "main" and that the slot will take precedence over
|
|
28
|
-
// any override.
|
|
29
|
+
// any override. `<main>` has `id="content"`, the target of the skip link in the header. A page that fills the "main"
|
|
30
|
+
// slot gives that id to an element of its own.
|
|
29
31
|
main?: LayoutOverridesHTMLAttributes<'main'>
|
|
30
32
|
}
|
|
31
33
|
// An override for the page title. If not defined, the title will either be the content of the `title` frontmatter
|
|
@@ -61,7 +63,7 @@ const thumbnail = frontmatter?.thumbnail
|
|
|
61
63
|
: 'static/images/social-image.png'
|
|
62
64
|
|
|
63
65
|
const bodyProps = overrides?.body ?? {}
|
|
64
|
-
const mainProps = overrides?.main
|
|
66
|
+
const mainProps = { id: 'content', ...overrides?.main }
|
|
65
67
|
---
|
|
66
68
|
|
|
67
69
|
<!doctype html>
|
|
@@ -90,6 +92,7 @@ const mainProps = overrides?.main ?? {}
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
<Footer />
|
|
95
|
+
<Consent />
|
|
93
96
|
|
|
94
97
|
<Scripts layout={layout}>
|
|
95
98
|
<slot name="scripts" slot="scripts" />
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Consent
|
|
4
|
+
*
|
|
5
|
+
* Asks before Google Analytics loads. Rendered in production builds, when
|
|
6
|
+
* `analytics.googleId` is set in `config.yml`. Nothing is loaded and no cookie is set until
|
|
7
|
+
* the visitor accepts. The choice is kept in `localStorage`, so every Chassis site served
|
|
8
|
+
* from the same host shares it. The "Cookie settings" button of the footer opens the
|
|
9
|
+
* banner again.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getConfig } from '../../site'
|
|
13
|
+
|
|
14
|
+
const isProd = import.meta.env.PROD
|
|
15
|
+
const { googleId } = getConfig().analytics
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
isProd && googleId && (
|
|
20
|
+
<section class="cxd-consent" aria-label="Cookie consent" data-google-id={googleId} hidden>
|
|
21
|
+
<div class="container 2xl">
|
|
22
|
+
<div class="notification default shadow-lg d-flex flex-wrap align-items-center gap-md p-md">
|
|
23
|
+
<p class="mb-0 flex-grow-1">
|
|
24
|
+
We would like to count visits with Google Analytics, which sets cookies. Nothing is sent
|
|
25
|
+
to Google unless you accept. <a href="/privacy/">Privacy notice</a>
|
|
26
|
+
</p>
|
|
27
|
+
<div class="d-flex gap-sm">
|
|
28
|
+
<button type="button" class="button primary" data-consent="granted">
|
|
29
|
+
Accept
|
|
30
|
+
</button>
|
|
31
|
+
<button type="button" class="button default" data-consent="denied">
|
|
32
|
+
Decline
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
type AnalyticsWindow = Window &
|
|
43
|
+
Record<`ga-disable-${string}`, boolean> & {
|
|
44
|
+
dataLayer?: unknown[]
|
|
45
|
+
gtag?: (...args: unknown[]) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KEY = 'chassis-analytics-consent'
|
|
49
|
+
const analytics = window as unknown as AnalyticsWindow
|
|
50
|
+
const banner = document.querySelector<HTMLElement>('.cxd-consent')
|
|
51
|
+
const googleId = banner?.dataset.googleId
|
|
52
|
+
|
|
53
|
+
function readChoice(): string | null {
|
|
54
|
+
try {
|
|
55
|
+
return localStorage.getItem(KEY)
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveChoice(choice: string) {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(KEY, choice)
|
|
64
|
+
} catch {
|
|
65
|
+
// Without storage the banner shows again on the next page
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadAnalytics(id: string) {
|
|
70
|
+
analytics[`ga-disable-${id}`] = false
|
|
71
|
+
if (analytics.gtag) return
|
|
72
|
+
|
|
73
|
+
analytics.dataLayer ??= []
|
|
74
|
+
analytics.gtag = function () {
|
|
75
|
+
// gtag.js reads the `arguments` object, not an array
|
|
76
|
+
// eslint-disable-next-line prefer-rest-params
|
|
77
|
+
analytics.dataLayer!.push(arguments)
|
|
78
|
+
}
|
|
79
|
+
analytics.gtag('js', new Date())
|
|
80
|
+
analytics.gtag('config', id)
|
|
81
|
+
|
|
82
|
+
const script = document.createElement('script')
|
|
83
|
+
script.async = true
|
|
84
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`
|
|
85
|
+
document.head.append(script)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stops Google Analytics on this page and deletes its cookies, which it sets on the
|
|
89
|
+
// registrable domain, such as `.chassis-ui.com`
|
|
90
|
+
function stopAnalytics(id: string) {
|
|
91
|
+
analytics[`ga-disable-${id}`] = true
|
|
92
|
+
|
|
93
|
+
const labels = location.hostname.split('.')
|
|
94
|
+
const domains = labels.slice(0, -1).map((_, index) => labels.slice(index).join('.'))
|
|
95
|
+
|
|
96
|
+
for (const cookie of document.cookie.split(';')) {
|
|
97
|
+
const name = cookie.split('=')[0].trim()
|
|
98
|
+
|
|
99
|
+
if (!name.startsWith('_ga')) continue
|
|
100
|
+
|
|
101
|
+
for (const domain of ['', ...domains.map((d) => `; domain=.${d}`)]) {
|
|
102
|
+
document.cookie = `${name}=; max-age=0; path=/${domain}`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (banner && googleId) {
|
|
108
|
+
const choice = readChoice()
|
|
109
|
+
|
|
110
|
+
if (choice === 'granted') loadAnalytics(googleId)
|
|
111
|
+
else if (choice !== 'denied') banner.hidden = false
|
|
112
|
+
|
|
113
|
+
banner.addEventListener('click', (event) => {
|
|
114
|
+
const button = (event.target as Element).closest<HTMLElement>('[data-consent]')
|
|
115
|
+
|
|
116
|
+
if (!button) return
|
|
117
|
+
|
|
118
|
+
const decision = button.dataset.consent!
|
|
119
|
+
|
|
120
|
+
saveChoice(decision)
|
|
121
|
+
banner.hidden = true
|
|
122
|
+
|
|
123
|
+
if (decision === 'granted') loadAnalytics(googleId)
|
|
124
|
+
else stopAnalytics(googleId)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
for (const opener of document.querySelectorAll<HTMLElement>('[data-consent-open]')) {
|
|
128
|
+
opener.hidden = false
|
|
129
|
+
opener.addEventListener('click', () => {
|
|
130
|
+
banner.hidden = false
|
|
131
|
+
banner.querySelector<HTMLElement>('[data-consent]')?.focus()
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
@@ -17,18 +17,25 @@
|
|
|
17
17
|
>
|
|
18
18
|
<img src="/static/images/site-logo.svg" alt="Chassis UI" height="32" />
|
|
19
19
|
</a>
|
|
20
|
-
<p>
|
|
20
|
+
<p class="mb-xs">
|
|
21
21
|
This project is licensed under the MIT License, which allows for free use, modification,
|
|
22
22
|
and distribution of the software.
|
|
23
23
|
</p>
|
|
24
|
+
<p>
|
|
25
|
+
<a href="/privacy/" class="fg-main">Privacy</a>
|
|
26
|
+
<button hidden data-consent-open type="button" class="cxd-footer-link fg-main ms-md"
|
|
27
|
+
>Cookie settings</button
|
|
28
|
+
>
|
|
29
|
+
</p>
|
|
24
30
|
</div>
|
|
25
31
|
<div class="sm:col-4 lg:col-2 lg:offset-1 mb-md">
|
|
26
32
|
<p class="h6 mb-xs">Docs</p>
|
|
27
33
|
<ul class="nav">
|
|
28
34
|
<li><a href="/tokens">Design Tokens</a></li>
|
|
29
|
-
<li><a href="/assets">Asset Distributor</a></li>
|
|
30
35
|
<li><a href="/figma">Figma Library</a></li>
|
|
31
36
|
<li><a href="/css">CSS Framework</a></li>
|
|
37
|
+
<li><a href="/react">React Components</a></li>
|
|
38
|
+
<li><a href="/assets">Asset Distributor</a></li>
|
|
32
39
|
<li><a href="/icons">Icon Generator</a></li>
|
|
33
40
|
</ul>
|
|
34
41
|
</div>
|
|
@@ -36,9 +43,10 @@
|
|
|
36
43
|
<p class="h6 mb-xs">Github</p>
|
|
37
44
|
<ul class="nav">
|
|
38
45
|
<li><a href="https://github.com/chassis-ui/tokens">Chassis Tokens</a></li>
|
|
39
|
-
<li><a href="https://github.com/chassis-ui/assets">Chassis Assets</a></li>
|
|
40
46
|
<li><a href="https://github.com/chassis-ui/figma">Chassis Figma</a></li>
|
|
41
47
|
<li><a href="https://github.com/chassis-ui/css">Chassis CSS</a></li>
|
|
48
|
+
<li><a href="https://github.com/chassis-ui/react">Chassis React</a></li>
|
|
49
|
+
<li><a href="https://github.com/chassis-ui/assets">Chassis Assets</a></li>
|
|
42
50
|
<li><a href="https://github.com/chassis-ui/icons">Chassis Icons</a></li>
|
|
43
51
|
</ul>
|
|
44
52
|
</div>
|
|
@@ -10,6 +10,5 @@
|
|
|
10
10
|
<link rel="icon" href={'/static/images/favicon-32x32.png'} sizes="32x32" type="image/png" />
|
|
11
11
|
<link rel="icon" href={'/static/images/favicon-16x16.png'} sizes="16x16" type="image/png" />
|
|
12
12
|
<link rel="manifest" href={'/static/images/manifest.json'} />
|
|
13
|
-
<link rel="mask-icon" href={'/static/images/safari-pinned-tab.svg'} color="#712cf9" />
|
|
14
13
|
<link rel="icon" href={'/static/images/favicon.png'} />
|
|
15
14
|
<meta name="theme-color" content="#00A4CC" />
|
|
@@ -10,7 +10,6 @@ import type { Layout } from '../../libs/layout'
|
|
|
10
10
|
import Stylesheet from './Stylesheet.astro'
|
|
11
11
|
import Favicons from './Favicons.astro'
|
|
12
12
|
import Social from './Social.astro'
|
|
13
|
-
import Analytics from './Analytics.astro'
|
|
14
13
|
import 'virtual:chassis-docs/styles'
|
|
15
14
|
|
|
16
15
|
interface Props {
|
|
@@ -41,6 +40,14 @@ const pageTitle = isHome
|
|
|
41
40
|
|
|
42
41
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
43
42
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
43
|
+
<!--
|
|
44
|
+
A link, not an `@import` in the styles, so that the browser finds it with the page and
|
|
45
|
+
not after the stylesheet that imports it
|
|
46
|
+
-->
|
|
47
|
+
<link
|
|
48
|
+
rel="stylesheet"
|
|
49
|
+
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Inter:wght@100..900&family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap"
|
|
50
|
+
/>
|
|
44
51
|
|
|
45
52
|
<meta name="description" content={description} />
|
|
46
53
|
|
|
@@ -59,4 +66,3 @@ const pageTitle = isHome
|
|
|
59
66
|
<slot name="styles" />
|
|
60
67
|
<Favicons />
|
|
61
68
|
<Social description={description} layout={layout} thumbnail={thumbnail} title={title} />
|
|
62
|
-
<Analytics />
|
|
@@ -94,6 +94,7 @@ const isActive = (path: string) => {
|
|
|
94
94
|
<NavLink active={isActive('/tokens')} href="/tokens">Tokens</NavLink>
|
|
95
95
|
<NavLink active={isActive('/figma')} href="/figma">Figma</NavLink>
|
|
96
96
|
<NavLink active={isActive('/css')} href="/css">CSS</NavLink>
|
|
97
|
+
<NavLink active={isActive('/react')} href="/react">React</NavLink>
|
|
97
98
|
<NavLink active={isActive('/assets')} href="/assets">Assets</NavLink>
|
|
98
99
|
<NavLink active={isActive('/icons')} href="/icons">Icons</NavLink>
|
|
99
100
|
<li class="nav-item col-12 lg:col-auto lg:d-flex">
|
package/src/libs/highlight.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { codeToHtml, type ShikiTransformer } from 'shiki'
|
|
2
|
-
import { DOCS_SHIKI_THEMES } from './markdown'
|
|
2
|
+
import { DOCS_SHIKI_THEMES, focusableTransformer } from './markdown'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Shiki transformer that renames the `shiki` CSS class on `<pre>` elements to
|
|
@@ -44,7 +44,7 @@ export async function highlightCode(
|
|
|
44
44
|
lang,
|
|
45
45
|
themes: DOCS_SHIKI_THEMES,
|
|
46
46
|
defaultColor: false,
|
|
47
|
-
transformers: [...extraTransformers, classTransformer]
|
|
47
|
+
transformers: [...extraTransformers, classTransformer, focusableTransformer]
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
return { html: replaceShikiClasses(highlighted) }
|
package/src/libs/markdown.ts
CHANGED
|
@@ -35,6 +35,17 @@ const addLanguageAttributeTransformer: ShikiTransformer = {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Shiki transformer that puts `<pre>` in the tab order. A code block that is wider than the
|
|
40
|
+
* page scrolls sideways, and a keyboard can scroll it only when it can take focus.
|
|
41
|
+
*/
|
|
42
|
+
export const focusableTransformer: ShikiTransformer = {
|
|
43
|
+
name: 'focusable-pre',
|
|
44
|
+
pre(node) {
|
|
45
|
+
node.properties['tabindex'] = '0'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Options accepted by `getDocsMarkdownConfig`.
|
|
40
51
|
*/
|
|
@@ -121,7 +132,8 @@ export function getDocsMarkdownConfig(options: DocsMarkdownConfigOptions): Markd
|
|
|
121
132
|
transformers: [
|
|
122
133
|
transformerNotationDiff(),
|
|
123
134
|
transformerNotationHighlight(),
|
|
124
|
-
addLanguageAttributeTransformer
|
|
135
|
+
addLanguageAttributeTransformer,
|
|
136
|
+
focusableTransformer
|
|
125
137
|
]
|
|
126
138
|
}
|
|
127
139
|
}
|
package/src/libs/schema.ts
CHANGED
|
@@ -38,7 +38,7 @@ const zHeadingRange = z.object({
|
|
|
38
38
|
* ```
|
|
39
39
|
*/
|
|
40
40
|
export const configSchema = z.strictObject({
|
|
41
|
-
/** Google Analytics is
|
|
41
|
+
/** Google Analytics is offered in production builds when `googleId` is set, and loads after the visitor accepts. */
|
|
42
42
|
analytics: z.strictObject({ googleId: z.string().optional() }).default({}),
|
|
43
43
|
/** Heading levels that get an anchor link. */
|
|
44
44
|
anchors: zHeadingRange.default({ min: 2, max: 5 }),
|
package/src/scss/_footer.scss
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
a
|
|
17
|
+
a,
|
|
18
|
+
.cxd-footer-link {
|
|
18
19
|
color: var(--cx-fg-color);
|
|
19
20
|
text-decoration: none;
|
|
20
21
|
|
|
@@ -24,5 +25,14 @@
|
|
|
24
25
|
text-decoration: underline;
|
|
25
26
|
}
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
// A button that looks like the links next to it
|
|
30
|
+
.cxd-footer-link {
|
|
31
|
+
padding: 0;
|
|
32
|
+
font: inherit;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
background: none;
|
|
35
|
+
border: 0;
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
38
|
}
|
package/src/scss/main.scss
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
@use "@chassis-ui/css/scss/mixins" as *;
|
|
2
2
|
|
|
3
|
-
@forward "./fonts";
|
|
4
3
|
@forward "./navbar";
|
|
5
4
|
@forward "./content";
|
|
6
5
|
@forward "./skippy";
|
|
@@ -8,6 +7,7 @@
|
|
|
8
7
|
@forward "./layout";
|
|
9
8
|
@forward "./toc";
|
|
10
9
|
@forward "./footer";
|
|
10
|
+
@forward "./consent";
|
|
11
11
|
@forward "./snippets";
|
|
12
12
|
@forward "./callouts";
|
|
13
13
|
@forward "./placeholder-img";
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Analytics
|
|
4
|
-
*
|
|
5
|
-
* Google Analytics script tags. Rendered in production builds, when `analytics.googleId` is
|
|
6
|
-
* set in `config.yml`.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { getConfig } from '../../site'
|
|
10
|
-
|
|
11
|
-
const isProd = import.meta.env.PROD
|
|
12
|
-
const { googleId } = getConfig().analytics
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
isProd && googleId && (
|
|
17
|
-
<>
|
|
18
|
-
<script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${googleId}`} />
|
|
19
|
-
<script
|
|
20
|
-
is:inline
|
|
21
|
-
set:html={`
|
|
22
|
-
window.dataLayer = window.dataLayer || [];
|
|
23
|
-
function gtag(){dataLayer.push(arguments);}
|
|
24
|
-
gtag('js', new Date());
|
|
25
|
-
|
|
26
|
-
gtag('config', '${googleId}');
|
|
27
|
-
`}
|
|
28
|
-
/>
|
|
29
|
-
</>
|
|
30
|
-
)
|
|
31
|
-
}
|
package/src/scss/fonts.scss
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@import "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Inter:wght@100..900&family=Archivo+Narrow:ital,wght@0,400..700;1,400..700&display=swap";
|