accessible-marp-decks 2.0.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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +15 -0
- package/NOTICE +21 -0
- package/README.md +181 -0
- package/package.json +68 -0
- package/src/cli.js +247 -0
- package/src/core/escape.js +16 -0
- package/src/core/marpit.js +29 -0
- package/src/core/render.js +87 -0
- package/src/core/runtime-script.js +99 -0
- package/src/core/template.js +36 -0
- package/src/core/themes.js +56 -0
- package/src/core/transforms/_code.js +15 -0
- package/src/core/transforms/_img.js +98 -0
- package/src/core/transforms/_section.js +59 -0
- package/src/core/transforms/index.js +23 -0
- package/src/eleventy.js +53 -0
- package/src/index.js +19 -0
- package/themes/_template.css +249 -0
- package/themes/basic.css +316 -0
- package/themes/document.css +68 -0
- package/themes/high-contrast.css +237 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [2.0.0] — 2026
|
|
6
|
+
|
|
7
|
+
Version 2 is a ground-up modernisation. The rendered output is now a single, self-contained, responsive, accessible file, and the project ships as an ESM package usable as a CLI, a library, an Eleventy plugin, or a GitHub Action.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Single-file output** — local images are base64-inlined into the page, so a built deck is one self-contained HTML file (`inlineAssets`, `basePath` render options).
|
|
12
|
+
- **Responsive 16:9 scaling** — each fixed-size slide is scaled to the window as one rigid unit via CSS `zoom` (a small inlined script sets the scale factor), so text, spacing, and layout shrink together and nothing reflows. Browser page zoom still enlarges the deck (the script compensates via `devicePixelRatio`), per WCAG 1.4.4.
|
|
13
|
+
- **Layout helpers** — slide-native `box`, `stack`, `cluster`, `columns`, `grid`, `center`, `frame`, `cover`, `sidebar`, and `overlay-centre` classes.
|
|
14
|
+
- **Smarter code-block focus** — code blocks are keyboard-scrollable by default (no-JS safe), and a tiny inlined script removes the tab stop from blocks that don't actually overflow, re-checking on resize.
|
|
15
|
+
- **`build-all <dir>`** CLI command — builds every deck in a folder and writes an accessible landing page linking them. A failing deck doesn't abort the site: the rest are built, the failure is reported, and the command exits non-zero with a summary.
|
|
16
|
+
- **GitHub Action** — a reusable composite `action.yml` plus an example Pages-deploy workflow, so decks can be published without a command line.
|
|
17
|
+
- **Theme template** — `themes/_template.css`, a documented, self-contained starter to copy and recolour.
|
|
18
|
+
- **`high-contrast` theme** — a second bundled theme (pure black/white, auto light/dark).
|
|
19
|
+
- **`runtimeScript` render option** — set `false` to omit the inlined code-block script for strict-CSP hosts.
|
|
20
|
+
- **Tests** (`node:test`) and **CI** covering accessibility invariants, image inlining, scaling, layout helpers, theme resolution, the CLI, and the Eleventy plugin — with 100% line, branch, and function coverage enforced in CI.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Rewritten as **ESM** with package `exports` (`.` and `./eleventy`) and an `accessible-marp` bin.
|
|
25
|
+
- Transforms are **element-scoped** files — `_section.js`, `_img.js`, `_code.js`.
|
|
26
|
+
- The `basic` theme is re-authored in `em` units with automatic light/dark modes.
|
|
27
|
+
- Dependencies trimmed to the five that are actually used.
|
|
28
|
+
- js-beautify is loaded on demand — rendering with `prettify: false` never loads it.
|
|
29
|
+
- Node 24 (LTS) is the development, CI, and Action runtime, pinned in `.nvmrc`; the npm `engines` field is no longer set.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Code fences without a language (or with an unrecognised one) now escape their content instead of injecting it into the page as raw HTML.
|
|
34
|
+
- Slides with several headings no longer emit duplicate `slide-N` ids or concatenate every heading into the slide's `aria-label` — only the first heading is used. Heading-less slides are labelled `"Slide N"` with no dangling colon.
|
|
35
|
+
- Slide background images (``) are now base64-inlined like every other image, so decks with backgrounds stay a single self-contained file. The consumed `data-background-image` and `data-marpit-pagination-total` attributes are stripped from the output.
|
|
36
|
+
- Applying a helper to a whole slide with `<!-- _class: … -->` now actually works: Marpit scopes bare class selectors to descendants of the slide, so the documented pattern silently did nothing. Themes now ship slide-level variants (`cover`, `center`, `stack`) written as `section.X`, and Marpit's bookkeeping `data-class` attribute is stripped.
|
|
37
|
+
- The `css` render option no longer requires a `/* @theme */` comment — one is prepended automatically, instead of Marpit rejecting the CSS.
|
|
38
|
+
- Image srcs are treated as URLs when inlining: percent-escapes are decoded (`my%20image.png` finds `my image.png` on disk) and query strings/fragments are ignored, instead of silently leaving a broken reference.
|
|
39
|
+
- The runtime script re-evaluates code-block focus after web fonts finish loading and observes the blocks directly — slides are fixed-size, so a block's overflow can change without the deck resizing.
|
|
40
|
+
- The CLI recognises backslash-separated (Windows) paths as paths rather than deck names, and CI now also runs the suite on Windows.
|
|
41
|
+
- Slides in decks without `paginate: true` are now numbered by position instead of being labelled "Slide undefined".
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
|
|
45
|
+
- The GitHub Action passes its inputs to the build script via environment variables instead of interpolating `${{ }}` expressions into bash, closing a shell-injection surface.
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
|
|
49
|
+
- The `imageWidth` option (images are now sized responsively by CSS).
|
|
50
|
+
- The SASS build and the dead dependencies from 1.x (`node-sass`, `html-minifier`, and others).
|
|
51
|
+
|
|
52
|
+
[2.0.0]: https://github.com/abbott567/accessible-marp-decks/releases/tag/v2.0.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Craig Abbott
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Accessible Marp Decks
|
|
2
|
+
Copyright (c) 2026 Craig Abbott
|
|
3
|
+
|
|
4
|
+
This product is licensed under the ISC License (see LICENSE).
|
|
5
|
+
|
|
6
|
+
It builds on, and bundles or depends on, the following third-party work:
|
|
7
|
+
|
|
8
|
+
- Marp / Marpit (https://marp.app/) — MIT License.
|
|
9
|
+
Used to render Marp markdown into slide HTML and CSS.
|
|
10
|
+
|
|
11
|
+
- highlight.js (https://highlightjs.org/) — BSD 3-Clause License.
|
|
12
|
+
Used for syntax highlighting of fenced code blocks.
|
|
13
|
+
|
|
14
|
+
- The "a11y-light" and "a11y-dark" syntax-highlighting colour schemes by
|
|
15
|
+
Eric Bailey (https://github.com/ericwbailey/a11y-syntax-highlighting) —
|
|
16
|
+
MIT License. The bundled themes' code colours are derived from these.
|
|
17
|
+
|
|
18
|
+
The layout helpers in the bundled themes (box, stack, cluster, columns, grid,
|
|
19
|
+
center, frame, cover, sidebar, overlay-centre) are original CSS written for this
|
|
20
|
+
project using standard CSS grid and flexbox. They are conceptually inspired by
|
|
21
|
+
common layout patterns but do not copy any third-party implementation.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Accessible Marp Decks
|
|
2
|
+
|
|
3
|
+
Render [Marp](https://marp.app/) markdown slide decks into a **single, self-contained, accessible HTML page** — every slide is a labelled landmark, headings have stable ids, code blocks that scroll are keyboard-focusable, pagination is announced to screen readers, images are inlined, and the whole thing scales to any window in 16:9 without reflowing.
|
|
4
|
+
|
|
5
|
+
Use it four ways:
|
|
6
|
+
|
|
7
|
+
- **CLI** — build a deck (or a whole folder) into shareable HTML.
|
|
8
|
+
- **Library** — `import { renderDeck }` and render Marp markdown anywhere.
|
|
9
|
+
- **Eleventy plugin** — drop deck files into an [Eleventy](https://www.11ty.dev/) site.
|
|
10
|
+
- **GitHub Action** — publish decks to GitHub Pages on push, no command line.
|
|
11
|
+
|
|
12
|
+
This project isn't a replacement for Marp or the VSCode preview extension — it's for **sharing** your slides in an accessible format.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node.js 24 (LTS) — the version pinned in [`.nvmrc`](.nvmrc). Older versions may work but aren't tested.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install accessible-marp-decks
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Writing slides
|
|
25
|
+
|
|
26
|
+
Slides are [Marpit markdown](https://marpit.marp.app/markdown). A deck starts with front matter and separates slides with `---`:
|
|
27
|
+
|
|
28
|
+
```markdown
|
|
29
|
+
---
|
|
30
|
+
title: My Talk
|
|
31
|
+
description: A short description used for the page <title> and meta description.
|
|
32
|
+
paginate: true
|
|
33
|
+
marp: true
|
|
34
|
+
theme: basic
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
# My Talk
|
|
38
|
+
|
|
39
|
+
Intro text.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Second slide
|
|
44
|
+
|
|
45
|
+
More content.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Compose richer slides with the [layout helpers](docs/layouts.md) (`columns`, `grid`, `cluster`, `frame`, …). To preview as you write, install the [Marp for VSCode extension](https://marketplace.visualstudio.com/items?itemName=marp-team.marp-vscode).
|
|
49
|
+
|
|
50
|
+
## What you get out
|
|
51
|
+
|
|
52
|
+
A built deck is **one HTML file**. CSS is inlined, images are base64-encoded into the page, and the slides scale to fit the window. Nothing else needs to ship alongside it (except any linked `demos/` pages), so it's trivial to email, host, or attach.
|
|
53
|
+
|
|
54
|
+
## CLI usage
|
|
55
|
+
|
|
56
|
+
The package installs an `accessible-marp` binary.
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# Build a named deck (looked up under examples/decks/<name>/slides.md by default)
|
|
60
|
+
accessible-marp build layouts --theme basic
|
|
61
|
+
|
|
62
|
+
# Build a specific markdown file to a chosen output directory
|
|
63
|
+
accessible-marp build ./slides.md --out ./public
|
|
64
|
+
|
|
65
|
+
# Build every deck in a folder into a browsable site with a landing page
|
|
66
|
+
accessible-marp build-all decks --out _site
|
|
67
|
+
|
|
68
|
+
# List the bundled themes
|
|
69
|
+
accessible-marp themes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Options:
|
|
73
|
+
|
|
74
|
+
| Flag | Description |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `--theme`, `-t` | Theme to use. Defaults to the deck's front-matter `theme`. |
|
|
77
|
+
| `--out`, `-o` | Output directory. Defaults to `dist/decks/<deck>` (or `dist/site` for `build-all`). |
|
|
78
|
+
| `--decks-dir` | Where named decks live. Defaults to `examples/decks`. |
|
|
79
|
+
|
|
80
|
+
`build` writes a single `slides.html` (plus any `demos/` folder). `build-all` writes each deck to `<out>/<name>/index.html` and an accessible landing page at `<out>/index.html`; if a deck fails, the rest are still built and the command exits non-zero with a summary. The legacy `npm run build deck=<name> theme=<name>` form still works.
|
|
81
|
+
|
|
82
|
+
## Library usage
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { renderDeck, renderDeckFile, listThemes } from 'accessible-marp-decks'
|
|
86
|
+
|
|
87
|
+
const html = await renderDeck(markdownString, { theme: 'basic', basePath: './my-deck' })
|
|
88
|
+
|
|
89
|
+
// or straight from a file (basePath defaults to the file's directory)
|
|
90
|
+
const html2 = await renderDeckFile('./my-deck/slides.md', { theme: 'basic' })
|
|
91
|
+
|
|
92
|
+
await listThemes() // ['basic', 'high-contrast']
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `renderDeck(markdown, options)` → `Promise<string>`
|
|
96
|
+
|
|
97
|
+
| Option | Default | Description |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `theme` | front matter, else `basic` | A bundled theme name. |
|
|
100
|
+
| `css` | — | Raw theme CSS, used instead of a bundled theme. An `/* @theme */` comment is added automatically if missing. |
|
|
101
|
+
| `documentCss` | bundled `document.css` | Override the base accessible-layout CSS. |
|
|
102
|
+
| `basePath` | — | Directory used to resolve and inline relative image paths. |
|
|
103
|
+
| `inlineAssets` | `true` | Base64-inline local images for a single-file output. |
|
|
104
|
+
| `lang` | `'en'` | Document `lang` attribute. |
|
|
105
|
+
| `runtimeScript` | `true` | Inline the code-block scrolling enhancement script. Set `false` for strict-CSP hosts. |
|
|
106
|
+
| `prettify` | `true` | Pretty-print the HTML output. |
|
|
107
|
+
|
|
108
|
+
## Eleventy plugin
|
|
109
|
+
|
|
110
|
+
Render Marp decks as accessible pages inside an [Eleventy](https://www.11ty.dev/) (3.0+) site.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// eleventy.config.js
|
|
114
|
+
import accessibleMarp from 'accessible-marp-decks/eleventy'
|
|
115
|
+
|
|
116
|
+
export default function (eleventyConfig) {
|
|
117
|
+
eleventyConfig.addPlugin(accessibleMarp, {
|
|
118
|
+
// theme: 'basic', // force one theme for all decks (optional)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
By default the plugin registers a **`.deck`** extension, so any `*.deck` file (containing Marp markdown) is built as an accessible deck page. Using a dedicated extension means your site's normal `.md` files are left untouched.
|
|
124
|
+
|
|
125
|
+
| Option | Default | Description |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `extension` | `'deck'` | File extension that marks a deck. Set to `'md'` to treat **every** markdown file as a deck (fully overrides Eleventy's markdown rendering). |
|
|
128
|
+
| `theme` | per-deck front matter | Force a theme for all decks. |
|
|
129
|
+
| `lang` | `'en'` | Document language. |
|
|
130
|
+
|
|
131
|
+
Images are inlined into each page automatically. Companion `demos/` folders (standalone HTML pages your slides link to) are **not** copied by the plugin — add a passthrough copy for them in your Eleventy config if you use them:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
eleventyConfig.addPassthroughCopy('content/**/demos/**')
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A working demo lives in [`examples/eleventy-demo`](examples/eleventy-demo) — run it with `npm run demo`.
|
|
138
|
+
|
|
139
|
+
## GitHub Action
|
|
140
|
+
|
|
141
|
+
Publish decks to GitHub Pages on every push — no command line. Add one workflow and enable Pages; full walkthrough in [docs/github-action.md](docs/github-action.md).
|
|
142
|
+
|
|
143
|
+
```yaml
|
|
144
|
+
- name: Build decks
|
|
145
|
+
uses: abbott567/accessible-marp-decks@v2
|
|
146
|
+
with:
|
|
147
|
+
decks-dir: decks
|
|
148
|
+
out: _site
|
|
149
|
+
- uses: actions/upload-pages-artifact@v5
|
|
150
|
+
with:
|
|
151
|
+
path: _site
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Layouts and themes
|
|
155
|
+
|
|
156
|
+
- **Layout helpers** — `box`, `stack`, `cluster`, `columns`, `grid`, `center`, `frame`, `cover`, `sidebar`, `overlay-centre`. See [docs/layouts.md](docs/layouts.md).
|
|
157
|
+
- **Themes** live in [`themes/`](themes). Two are bundled: **`basic`** (the default) and **`high-contrast`** — both neutral designs with automatic light/dark modes via `prefers-color-scheme`, using the reader's `system-ui` font. To make your own, copy [`themes/_template.css`](themes/_template.css) and recolour it — see [docs/creating-themes.md](docs/creating-themes.md).
|
|
158
|
+
|
|
159
|
+
## Accessibility
|
|
160
|
+
|
|
161
|
+
Every rendered deck is a labelled-landmark structure with screen-reader pagination, keyboard-scrollable code blocks, and reflow-free 16:9 scaling. The full list of guarantees is in [docs/accessibility.md](docs/accessibility.md).
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```sh
|
|
166
|
+
npm install
|
|
167
|
+
npm test # node:test — accessibility, images, scaling, layouts, CLI, Eleventy
|
|
168
|
+
npm run lint # standard
|
|
169
|
+
npm run build:site # build the example decks into _site/
|
|
170
|
+
npm run demo # build the Eleventy demo site
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The publishable package is the engine (`src/`) and `themes/`. The `layouts` deck under `examples/decks` is a demo of the layouts the renderer supports and is excluded from the npm tarball.
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT.md). Security reports: [SECURITY.md](SECURITY.md).
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
[ISC](LICENSE). Third-party credits are in [NOTICE](NOTICE).
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "accessible-marp-decks",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Render Marp markdown slide decks into a standalone, accessible HTML format. Usable as a CLI, an importable function, or an Eleventy plugin.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./eleventy": "./src/eleventy.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"accessible-marp": "src/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"themes",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"NOTICE",
|
|
19
|
+
"CHANGELOG.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node src/cli.js build",
|
|
23
|
+
"build:site": "node src/cli.js build-all examples/decks --out _site",
|
|
24
|
+
"test": "node --test",
|
|
25
|
+
"coverage": "node --test --experimental-test-coverage --test-coverage-exclude=\"test/**\" --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100",
|
|
26
|
+
"lint": "standard src test",
|
|
27
|
+
"demo": "cd examples/eleventy-demo && eleventy"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"marp",
|
|
31
|
+
"marpit",
|
|
32
|
+
"accessibility",
|
|
33
|
+
"a11y",
|
|
34
|
+
"slides",
|
|
35
|
+
"presentation",
|
|
36
|
+
"eleventy",
|
|
37
|
+
"eleventy-plugin"
|
|
38
|
+
],
|
|
39
|
+
"author": "Craig Abbott",
|
|
40
|
+
"license": "ISC",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/abbott567/accessible-marp-decks.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/abbott567/accessible-marp-decks/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/abbott567/accessible-marp-decks#readme",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@marp-team/marpit": "^3.1.3",
|
|
51
|
+
"cheerio": "^1.0.0",
|
|
52
|
+
"gray-matter": "^4.0.3",
|
|
53
|
+
"highlight.js": "^11.10.0",
|
|
54
|
+
"js-beautify": "^1.15.1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@11ty/eleventy": ">=3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@11ty/eleventy": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@11ty/eleventy": "^3.0.0",
|
|
66
|
+
"standard": "^17.1.2"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import { readFile, writeFile, mkdir, cp, stat, readdir } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join, basename, resolve } from 'node:path'
|
|
5
|
+
import { renderDeck, readDeckInfo } from './core/render.js'
|
|
6
|
+
import { listThemes } from './core/themes.js'
|
|
7
|
+
import { escapeHtml } from './core/escape.js'
|
|
8
|
+
|
|
9
|
+
const USAGE = `accessible-marp — build accessible HTML decks from Marp markdown
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
accessible-marp build <deck|path> [--theme <name>] [--out <dir>] [--decks-dir <dir>]
|
|
13
|
+
accessible-marp build-all <dir> [--theme <name>] [--out <dir>]
|
|
14
|
+
accessible-marp themes
|
|
15
|
+
accessible-marp --help
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
<deck|path> A deck name (resolved under --decks-dir) or a path to a .md file.
|
|
19
|
+
<dir> A directory of decks (each a folder containing slides.md).
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--theme, -t <name> Theme to use. Defaults to the deck's front-matter theme.
|
|
23
|
+
--out, -o <dir> Output directory. Defaults to dist/decks/<deck> (build)
|
|
24
|
+
or dist/site (build-all).
|
|
25
|
+
--decks-dir <dir> Where named decks live. Defaults to examples/decks.
|
|
26
|
+
--help, -h Show this help.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
accessible-marp build layouts --theme basic
|
|
30
|
+
accessible-marp build ./slides.md --out ./public
|
|
31
|
+
accessible-marp build-all examples/decks --out _site
|
|
32
|
+
npm run build deck=layouts theme=basic`
|
|
33
|
+
|
|
34
|
+
async function exists (p) {
|
|
35
|
+
try {
|
|
36
|
+
await stat(p)
|
|
37
|
+
return true
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Accept legacy `key=value` tokens (e.g. `deck=x theme=y`) alongside flags. */
|
|
44
|
+
function extractLegacyTokens (argv) {
|
|
45
|
+
const legacy = {}
|
|
46
|
+
const rest = []
|
|
47
|
+
for (const arg of argv) {
|
|
48
|
+
const m = /^(deck|theme|out)=(.*)$/.exec(arg)
|
|
49
|
+
if (m) legacy[m[1]] = m[2]
|
|
50
|
+
else rest.push(arg)
|
|
51
|
+
}
|
|
52
|
+
return { legacy, rest }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render one deck's markdown to `<outDir>/<htmlName>` and copy any `demos/`.
|
|
57
|
+
* Returns metadata about the built deck.
|
|
58
|
+
*/
|
|
59
|
+
async function buildOne ({ mdPath, sourceDir, deckName, outDir, theme, htmlName }) {
|
|
60
|
+
const markdown = await readFile(mdPath, 'utf8')
|
|
61
|
+
const html = await renderDeck(markdown, { theme, basePath: sourceDir })
|
|
62
|
+
|
|
63
|
+
await mkdir(outDir, { recursive: true })
|
|
64
|
+
await writeFile(join(outDir, htmlName), html)
|
|
65
|
+
|
|
66
|
+
// Images are base64-inlined into the page, so only companion `demos/`
|
|
67
|
+
// (standalone linked HTML pages) need copying next to the output.
|
|
68
|
+
const demosSrc = join(sourceDir, 'demos')
|
|
69
|
+
if (await exists(demosSrc)) {
|
|
70
|
+
await cp(demosSrc, join(outDir, 'demos'), { recursive: true })
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const info = readDeckInfo(markdown)
|
|
74
|
+
return { deckName, title: info.title || deckName, description: info.description || '' }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function buildCommand (deckArg, opts) {
|
|
78
|
+
const decksDir = opts['decks-dir'] || 'examples/decks'
|
|
79
|
+
|
|
80
|
+
// Resolve the markdown source: a path, or a named deck under decksDir.
|
|
81
|
+
// Anything containing a separator (either flavour, so Windows paths work)
|
|
82
|
+
// or ending in .md is a path; a bare word is a deck name.
|
|
83
|
+
let mdPath
|
|
84
|
+
let deckName
|
|
85
|
+
let sourceDir
|
|
86
|
+
if (deckArg.endsWith('.md') || /[\\/]/.test(deckArg)) {
|
|
87
|
+
mdPath = resolve(deckArg)
|
|
88
|
+
sourceDir = dirname(mdPath)
|
|
89
|
+
deckName = basename(mdPath, '.md')
|
|
90
|
+
// A conventional `<name>/slides.md` should be named after its folder.
|
|
91
|
+
if (deckName === 'slides') deckName = basename(sourceDir)
|
|
92
|
+
} else {
|
|
93
|
+
deckName = deckArg
|
|
94
|
+
sourceDir = resolve(decksDir, deckName)
|
|
95
|
+
mdPath = join(sourceDir, 'slides.md')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!(await exists(mdPath))) {
|
|
99
|
+
throw new Error(`Deck not found: ${mdPath}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const outDir = resolve(opts.out || join('dist', 'decks', deckName))
|
|
103
|
+
await buildOne({ mdPath, sourceDir, deckName, outDir, theme: opts.theme, htmlName: 'slides.html' })
|
|
104
|
+
console.log(`Built "${deckName}" → ${join(outDir, 'slides.html')}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function buildAllCommand (dir, opts) {
|
|
108
|
+
const root = resolve(dir)
|
|
109
|
+
if (!(await exists(root))) throw new Error(`Directory not found: ${root}`)
|
|
110
|
+
|
|
111
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
112
|
+
const outRoot = resolve(opts.out || join('dist', 'site'))
|
|
113
|
+
|
|
114
|
+
const built = []
|
|
115
|
+
const failures = []
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
if (!entry.isDirectory()) continue
|
|
118
|
+
const sourceDir = join(root, entry.name)
|
|
119
|
+
const mdPath = join(sourceDir, 'slides.md')
|
|
120
|
+
if (!(await exists(mdPath))) continue
|
|
121
|
+
|
|
122
|
+
// Each deck becomes /<name>/index.html for clean URLs on a static host.
|
|
123
|
+
// One broken deck must not abort the site — collect failures and keep going.
|
|
124
|
+
try {
|
|
125
|
+
const meta = await buildOne({
|
|
126
|
+
mdPath,
|
|
127
|
+
sourceDir,
|
|
128
|
+
deckName: entry.name,
|
|
129
|
+
outDir: join(outRoot, entry.name),
|
|
130
|
+
theme: opts.theme,
|
|
131
|
+
htmlName: 'index.html'
|
|
132
|
+
})
|
|
133
|
+
built.push(meta)
|
|
134
|
+
console.log(`Built "${entry.name}" → ${join(outRoot, entry.name, 'index.html')}`)
|
|
135
|
+
} catch (err) {
|
|
136
|
+
failures.push(entry.name)
|
|
137
|
+
console.error(`Failed "${entry.name}": ${err.message}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (built.length === 0 && failures.length === 0) {
|
|
142
|
+
throw new Error(`No decks (folders containing slides.md) found in ${root}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (built.length > 0) {
|
|
146
|
+
await writeFile(join(outRoot, 'index.html'), renderIndex(built))
|
|
147
|
+
console.log(`Wrote index → ${join(outRoot, 'index.html')} (${built.length} decks)`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (failures.length > 0) {
|
|
151
|
+
throw new Error(`${failures.length} of ${built.length + failures.length} decks failed to build`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A minimal, accessible landing page linking to each built deck. */
|
|
156
|
+
function renderIndex (decks) {
|
|
157
|
+
const items = decks
|
|
158
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
159
|
+
.map(d => ` <li>
|
|
160
|
+
<a href="./${encodeURIComponent(d.deckName)}/">${escapeHtml(d.title)}</a>
|
|
161
|
+
${d.description ? `<p>${escapeHtml(d.description)}</p>` : ''}
|
|
162
|
+
</li>`)
|
|
163
|
+
.join('\n')
|
|
164
|
+
|
|
165
|
+
return `<!DOCTYPE html>
|
|
166
|
+
<html lang="en">
|
|
167
|
+
<head>
|
|
168
|
+
<meta charset="UTF-8">
|
|
169
|
+
<title>Slide decks</title>
|
|
170
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
171
|
+
<style>
|
|
172
|
+
body { font-family: system-ui, sans-serif; max-width: 44rem; margin: 0 auto; padding: 2rem 1rem; line-height: 1.5; }
|
|
173
|
+
ul { list-style: none; padding: 0; }
|
|
174
|
+
li { margin: 0 0 1.5rem; }
|
|
175
|
+
a { font-size: 1.25rem; }
|
|
176
|
+
p { margin: .25rem 0 0; color: #555; }
|
|
177
|
+
@media (prefers-color-scheme: dark) {
|
|
178
|
+
body { background: #111; color: #eee; } p { color: #aaa; } a { color: #8ab4f8; }
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
181
|
+
</head>
|
|
182
|
+
<body>
|
|
183
|
+
<main>
|
|
184
|
+
<h1>Slide decks</h1>
|
|
185
|
+
<ul>
|
|
186
|
+
${items}
|
|
187
|
+
</ul>
|
|
188
|
+
</main>
|
|
189
|
+
</body>
|
|
190
|
+
</html>
|
|
191
|
+
`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function main () {
|
|
195
|
+
const { legacy, rest } = extractLegacyTokens(process.argv.slice(2))
|
|
196
|
+
|
|
197
|
+
const { values, positionals } = parseArgs({
|
|
198
|
+
args: rest,
|
|
199
|
+
allowPositionals: true,
|
|
200
|
+
options: {
|
|
201
|
+
theme: { type: 'string', short: 't' },
|
|
202
|
+
out: { type: 'string', short: 'o' },
|
|
203
|
+
'decks-dir': { type: 'string' },
|
|
204
|
+
help: { type: 'boolean', short: 'h' }
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
if (values.help) {
|
|
209
|
+
console.log(USAGE)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const command = positionals[0]
|
|
214
|
+
|
|
215
|
+
if (command === 'themes') {
|
|
216
|
+
const themes = await listThemes()
|
|
217
|
+
console.log(themes.join('\n'))
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const opts = {
|
|
222
|
+
theme: values.theme || legacy.theme,
|
|
223
|
+
out: values.out || legacy.out,
|
|
224
|
+
'decks-dir': values['decks-dir']
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (command === 'build-all') {
|
|
228
|
+
const dir = positionals[1]
|
|
229
|
+
if (!dir) throw new Error('No directory specified. See --help.')
|
|
230
|
+
await buildAllCommand(dir, opts)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (command === 'build' || legacy.deck) {
|
|
235
|
+
const deckArg = positionals[1] || legacy.deck
|
|
236
|
+
if (!deckArg) throw new Error('No deck specified. See --help.')
|
|
237
|
+
await buildCommand(deckArg, opts)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(USAGE)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
main().catch((err) => {
|
|
245
|
+
console.error(`Error: ${err.message}`)
|
|
246
|
+
process.exitCode = 1
|
|
247
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a value for safe insertion into HTML text or attribute content.
|
|
3
|
+
* Nullish values become the empty string. The one escape helper for the whole
|
|
4
|
+
* codebase — template shell, CLI index page, and highlight fallback all share
|
|
5
|
+
* the same rules.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function escapeHtml (value) {
|
|
11
|
+
return String(value ?? '')
|
|
12
|
+
.replace(/&/g, '&')
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>')
|
|
15
|
+
.replace(/"/g, '"')
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hljs from 'highlight.js'
|
|
2
|
+
import { Marpit } from '@marp-team/marpit'
|
|
3
|
+
import { escapeHtml } from './escape.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a Marpit instance configured the way this project expects:
|
|
7
|
+
* HTML enabled, smart typography, and syntax highlighting via highlight.js.
|
|
8
|
+
*
|
|
9
|
+
* @returns {import('@marp-team/marpit').Marpit}
|
|
10
|
+
*/
|
|
11
|
+
export function createMarpit () {
|
|
12
|
+
return new Marpit({
|
|
13
|
+
markdown: {
|
|
14
|
+
html: true,
|
|
15
|
+
linkify: true,
|
|
16
|
+
breaks: true,
|
|
17
|
+
typographer: true,
|
|
18
|
+
highlight (str, lang) {
|
|
19
|
+
// `ignoreIllegals` means highlight() won't throw for a language we've
|
|
20
|
+
// already confirmed is registered, so no defensive catch is needed.
|
|
21
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
22
|
+
const highlighted = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
|
|
23
|
+
return `<pre class="hljs"><code>${highlighted}</code></pre>`
|
|
24
|
+
}
|
|
25
|
+
return `<pre class="hljs"><code>${escapeHtml(str)}</code></pre>`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|