@exor404/mdslides 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eXor404
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # mdslides
2
+
3
+ > Markdown in, presentation out.
4
+
5
+ The slides sibling of [mdstack](https://github.com/eXor404/mdstack). Point it
6
+ at a folder — `mdslides` scaffolds a starter `slides.md`, then presents it in a
7
+ dev server or builds a self-contained `dist/` you can host anywhere static.
8
+
9
+ > **Status:** pre-1.0, provisional. CLI flags and markdown conventions may
10
+ > change before `0.x → 1.0`.
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ mdslides ./chemistry # scaffolds a project, then presents it
16
+ ```
17
+
18
+ Point `mdslides` at a folder and it scaffolds a project: it **asks for a
19
+ name**, writes `<name>.md` (e.g. `chemistry.md`) with three sample slides — a
20
+ title page, a list, and a help slide — plus a `mdslides.config.js`, then opens
21
+ the dev server. The name defaults to the folder, so pressing Enter at
22
+ `Name your presentation (chemistry):` gives you `chemistry/chemistry.md`. That
23
+ file *is* your deck — edit it and the slides live-reload.
24
+
25
+ ```bash
26
+ mdslides ./chemistry # present (dev server, live-reload)
27
+ mdslides build ./chemistry # static build → ./chemistry/dist/
28
+ mdslides preview ./chemistry # serve the built dist/
29
+ mdslides export ./chemistry # → chemistry.pdf (one page per slide)
30
+ ```
31
+
32
+ Already have a deck? Point straight at the file: `mdslides ./chemistry.md`.
33
+
34
+ Try the bundled example:
35
+
36
+ ```bash
37
+ npm install
38
+ npm run dev # opens ./example
39
+ ```
40
+
41
+ ## The deck model
42
+
43
+ **One markdown file is the whole deck.** Every `#` (H1) heading starts a new
44
+ slide — write plain markdown in between. A standalone `---` line is an
45
+ explicit break (for a slide with no heading, or to split one section).
46
+
47
+ ```markdown
48
+ # Presentation title
49
+ ## A subtitle
50
+ **Your Name** · {{date}}
51
+
52
+ # Second slide
53
+ - a point
54
+ - another (bullets reveal one at a time)
55
+ ```
56
+
57
+ ### Title slide
58
+
59
+ The **first slide is the title slide** automatically: the `#` renders large,
60
+ `##` is the subtitle, and the last line becomes a byline pinned to the bottom —
61
+ write your presenter name(s) in markdown there. The `{{date}}` token resolves to
62
+ today's date. Opt a slide out with `<!-- slide: plain -->`, or mark another
63
+ slide as a title with `<!-- slide: title -->`.
64
+
65
+ ## Supported markdown (the lean subset)
66
+
67
+ Only what slides actually need:
68
+
69
+ - Headings, paragraphs, **bold**, *italic*, ~~strike~~, ==highlight==
70
+ - Lists (reveal one item at a time — see [Reveal & motion](#reveal--motion)), tables, blockquotes
71
+ - `inline code` and fenced code blocks (syntax-highlighted via [Shiki](https://shiki.style))
72
+ - Math — `$inline$` and `$$block$$` via KaTeX
73
+ - Images (relative paths resolve from the deck folder)
74
+
75
+ Fence a block with a language for syntax highlighting:
76
+
77
+ ````markdown
78
+ ```js
79
+ export default { theme: 'angular' };
80
+ ```
81
+ ````
82
+
83
+ ## Presenting
84
+
85
+ | Key | Action |
86
+ | --- | --- |
87
+ | `→` `space` `PageDn` | next slide / reveal next fragment |
88
+ | `←` `PageUp` | previous |
89
+ | `Home` / `End` | first / last slide |
90
+ | `O` | overview grid (click a slide to jump) |
91
+ | `F` | fullscreen |
92
+ | `.` | black screen |
93
+ | `?` | help |
94
+
95
+ The URL hash tracks the current slide (`#/3`) so deep links work.
96
+
97
+ ### Reveal & motion
98
+
99
+ Slides build themselves as you talk:
100
+
101
+ - **Content fades in** when a slide opens — headings, paragraphs, images, code,
102
+ tables and quotes rise into place with a light stagger.
103
+ - **Lists reveal step by step.** Every list item is hidden when the slide opens
104
+ and appears one at a time on `→` / `space`; `←` steps back. Once all items
105
+ are shown, the next press advances to the following slide. This is automatic —
106
+ you don't need to mark anything.
107
+ - **Manual fragments.** Append `{.fragment}` to any other line (a paragraph,
108
+ a heading) to hold it back and reveal it on a later press, in document order
109
+ alongside the list items.
110
+
111
+ In the **overview grid** and **PDF export**, everything is shown at once. Motion
112
+ respects the OS "reduce motion" setting.
113
+
114
+ ### Per-slide directives
115
+
116
+ A leading comment configures one slide:
117
+
118
+ ```markdown
119
+ # A slide
120
+ <!-- slide: center bg=#1a0b2e -->
121
+ ```
122
+
123
+ - `center` — vertically + horizontally centered
124
+ - `bg=<color|url(...)>` — slide background
125
+
126
+ ## Configuration
127
+
128
+ First run drops a `mdslides.config.js` next to your deck:
129
+
130
+ ```js
131
+ export default {
132
+ brand: { text: 'mdslides' }, // bottom-left wordmark; '' to hide
133
+ theme: 'angular', // built-in: 'angular'
134
+ transition: 'fade', // 'fade' | 'slide' | 'none'
135
+ };
136
+ ```
137
+
138
+ ## PDF export
139
+
140
+ `mdslides export` renders the `/print` route (one slide per page, all
141
+ fragments shown). It uses [Playwright](https://playwright.dev) if installed
142
+ (`npm i -D playwright && npx playwright install chromium`); otherwise open
143
+ `/print` in a browser and use **Save as PDF**.
144
+
145
+ ## Releasing
146
+
147
+ Releases go to the **public npm registry** (npmjs.com), so anyone can
148
+ `npx @exor404/mdslides`. A tag-triggered GitHub Actions pipeline
149
+ ([`.github/workflows/package-publish.yml`](.github/workflows/package-publish.yml))
150
+ publishes via **OIDC Trusted Publishing** — GitHub mints a one-time identity
151
+ token for the run, so **no npm token or secret is ever stored**.
152
+
153
+ **One-time setup** (needed once, because Trusted Publishing attaches to an
154
+ existing package):
155
+
156
+ ```bash
157
+ npm login # browser sign-in to npmjs.com
158
+ npm publish --access public # publish the first version by hand
159
+ ```
160
+
161
+ Then on npmjs.com open the package → **Settings → Trusted Publisher → GitHub
162
+ Actions** and register:
163
+
164
+ - **Repository:** `eXor404/mdslides`
165
+ - **Workflow filename:** `package-publish.yml`
166
+
167
+ After that, every release is automatic:
168
+
169
+ ```bash
170
+ npm version patch # bumps package.json (0.1.0 → 0.1.1) and tags v0.1.1
171
+ git push origin main # push the version-bump commit
172
+ git push origin v0.1.1 # push the tag → the pipeline publishes, tokenlessly
173
+ ```
174
+
175
+ `npm version` creates the matching `vX.Y.Z` tag for you. The workflow refuses
176
+ to publish if the tag and `package.json` version disagree, so they can't drift.
177
+
178
+ ## Requirements
179
+
180
+ Node 18+.
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from 'astro/config';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, resolve } from 'node:path';
4
+ import mdslidesAssets from './integrations/mdslides-assets.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const theme = process.env.MD_THEME || 'angular';
8
+ const themeFile = resolve(__dirname, 'src', 'themes', `${theme}.css`);
9
+
10
+ export default defineConfig({
11
+ // Markdown is rendered by src/lib/deck.js (a dedicated unified pipeline),
12
+ // not Astro's content layer — so there's no markdown config here.
13
+ integrations: [mdslidesAssets()],
14
+ vite: {
15
+ resolve: {
16
+ alias: {
17
+ '~theme': themeFile,
18
+ },
19
+ },
20
+ server: {
21
+ // mdslides is a local CLI; deps live above APP_ROOT (or hoisted under
22
+ // npx). Disable strict fs serving so KaTeX fonts and source-folder
23
+ // assets resolve regardless of where they're installed.
24
+ fs: { strict: false },
25
+ },
26
+ },
27
+ });
@@ -0,0 +1,167 @@
1
+ import { promises as fs, mkdtempSync, rmSync } from 'node:fs';
2
+ import { resolve, relative, extname, join, dirname, basename, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { tmpdir } from 'node:os';
5
+ import { findChrome, printUrlToPdf } from './pdf-export.js';
6
+
7
+ const MIME = {
8
+ '.png': 'image/png',
9
+ '.jpg': 'image/jpeg',
10
+ '.jpeg': 'image/jpeg',
11
+ '.gif': 'image/gif',
12
+ '.svg': 'image/svg+xml',
13
+ '.webp': 'image/webp',
14
+ '.avif': 'image/avif',
15
+ '.ico': 'image/x-icon',
16
+ '.mp4': 'video/mp4',
17
+ '.webm': 'video/webm',
18
+ '.mp3': 'audio/mpeg',
19
+ '.woff': 'font/woff',
20
+ '.woff2': 'font/woff2',
21
+ '.ttf': 'font/ttf',
22
+ '.otf': 'font/otf',
23
+ };
24
+
25
+ const EXCLUDED_TOP = new Set(['dist', 'node_modules', '.git', '.astro', '.vscode', '.idea']);
26
+ const EXCLUDED_FILES = new Set(['mdslides.config.js']);
27
+
28
+ function isExcludedRel(rel) {
29
+ if (!rel || rel === '.' || rel === '..') return true;
30
+ if (EXCLUDED_FILES.has(rel)) return true;
31
+ const parts = rel.split(/[\\/]/);
32
+ if (parts[0].startsWith('.')) return true;
33
+ if (EXCLUDED_TOP.has(parts[0])) return true;
34
+ return false;
35
+ }
36
+
37
+ // Copy every non-.md file from the source folder so a built deck is
38
+ // self-contained (images, fonts, video…).
39
+ async function* walkAssets(root, current = root) {
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(current, { withFileTypes: true });
43
+ } catch {
44
+ return;
45
+ }
46
+ for (const entry of entries) {
47
+ const full = join(current, entry.name);
48
+ const rel = relative(root, full);
49
+ if (isExcludedRel(rel)) continue;
50
+ if (entry.isDirectory()) {
51
+ yield* walkAssets(root, full);
52
+ } else if (entry.isFile() && extname(entry.name).toLowerCase() !== '.md') {
53
+ yield { full, rel };
54
+ }
55
+ }
56
+ }
57
+
58
+ export default function mdslidesAssets() {
59
+ const source = process.env.MD_SOURCE;
60
+ const deckFile = process.env.MD_FILE;
61
+ if (!source) {
62
+ throw new Error('mdslides: MD_SOURCE env var not set — run via the mdslides CLI.');
63
+ }
64
+
65
+ return {
66
+ name: 'mdslides:assets',
67
+ hooks: {
68
+ 'astro:server:setup': ({ server }) => {
69
+ // PDF export endpoint: prints the live /print route to PDF with
70
+ // headless Chrome and streams it back as a download. This is the deck
71
+ // PDF button's primary path — browser-independent and pixel-accurate
72
+ // (Safari/Chrome ⌘P can't reliably size or color slide pages).
73
+ server.middlewares.use(async (req, res, next) => {
74
+ const path = (req.url || '').split('?')[0];
75
+ if (path !== '/__export.pdf') return next();
76
+ const chrome = findChrome();
77
+ if (!chrome) {
78
+ res.statusCode = 501;
79
+ res.end('No headless Chrome found. Install Google Chrome or set CHROME_PATH.');
80
+ return;
81
+ }
82
+ const host = req.headers.host || 'localhost';
83
+ const name = deckFile ? basename(deckFile).replace(/\.md$/i, '') : 'slides';
84
+ const dir = mkdtempSync(join(tmpdir(), 'mdslides-pdf-'));
85
+ const pdfPath = join(dir, `${name}.pdf`);
86
+ try {
87
+ await printUrlToPdf(chrome, `http://${host}/print/`, pdfPath);
88
+ const data = await fs.readFile(pdfPath);
89
+ res.setHeader('Content-Type', 'application/pdf');
90
+ res.setHeader('Content-Disposition', `attachment; filename="${name}.pdf"`);
91
+ res.setHeader('Content-Length', String(data.length));
92
+ res.end(data);
93
+ } catch (err) {
94
+ res.statusCode = 500;
95
+ res.end(`PDF export failed: ${err.message}`);
96
+ } finally {
97
+ try { rmSync(dir, { recursive: true, force: true }); } catch {}
98
+ }
99
+ });
100
+
101
+ // Live-reload when the deck markdown changes (it lives outside Astro's
102
+ // own watched tree, so add it explicitly).
103
+ if (deckFile) {
104
+ server.watcher.add(deckFile);
105
+ server.watcher.on('change', (changed) => {
106
+ if (resolve(changed) === resolve(deckFile)) {
107
+ server.ws.send({ type: 'full-reload' });
108
+ }
109
+ });
110
+ }
111
+
112
+ // Serve source-folder assets in dev.
113
+ server.middlewares.use(async (req, res, next) => {
114
+ if (!req.url || (req.method && req.method !== 'GET' && req.method !== 'HEAD')) {
115
+ return next();
116
+ }
117
+ let pathname;
118
+ try {
119
+ pathname = decodeURIComponent(req.url.split('?')[0].split('#')[0]);
120
+ } catch {
121
+ return next();
122
+ }
123
+ if (!pathname || pathname === '/' || pathname.endsWith('/')) return next();
124
+ const ext = extname(pathname).toLowerCase();
125
+ if (!ext || ext === '.md') return next();
126
+ if (pathname.startsWith('/@') || pathname.startsWith('/_astro/') || pathname.startsWith('/node_modules/')) {
127
+ return next();
128
+ }
129
+
130
+ const rel = pathname.replace(/^\/+/, '');
131
+ if (isExcludedRel(rel)) return next();
132
+
133
+ const filePath = resolve(source, rel);
134
+ const sourceWithSep = source.endsWith(sep) ? source : source + sep;
135
+ if (filePath !== source && !filePath.startsWith(sourceWithSep)) return next();
136
+
137
+ try {
138
+ const stat = await fs.stat(filePath);
139
+ if (!stat.isFile()) return next();
140
+ res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream');
141
+ res.setHeader('Content-Length', String(stat.size));
142
+ res.setHeader('Cache-Control', 'no-cache');
143
+ if (req.method === 'HEAD') {
144
+ res.end();
145
+ return;
146
+ }
147
+ res.end(await fs.readFile(filePath));
148
+ } catch {
149
+ return next();
150
+ }
151
+ });
152
+ },
153
+ 'astro:build:done': async ({ dir, logger }) => {
154
+ const outDir = fileURLToPath(dir);
155
+ const log = logger?.info?.bind(logger) ?? console.log;
156
+ let count = 0;
157
+ for await (const { full, rel } of walkAssets(source)) {
158
+ const dest = join(outDir, rel);
159
+ await fs.mkdir(dirname(dest), { recursive: true });
160
+ await fs.copyFile(full, dest);
161
+ count++;
162
+ }
163
+ if (count > 0) log(`copied ${count} asset${count === 1 ? '' : 's'} from source folder`);
164
+ },
165
+ },
166
+ };
167
+ }
@@ -0,0 +1,86 @@
1
+ // Shared headless-Chrome PDF helper, used by both the `mdslides export` CLI
2
+ // command and the dev server's /__export.pdf endpoint.
3
+ import { spawn, execSync } from 'node:child_process';
4
+ import { existsSync, statSync, mkdtempSync, rmSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+
8
+ function whichSync(bin) {
9
+ try {
10
+ return execSync(`command -v ${bin}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() || null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export function findChrome() {
17
+ if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH;
18
+ const candidates = [
19
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
20
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
21
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
22
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
23
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
24
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
25
+ ];
26
+ for (const c of candidates) if (existsSync(c)) return c;
27
+ for (const bin of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser']) {
28
+ const found = whichSync(bin);
29
+ if (found) return found;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ // Print a URL to `pdfPath` with headless Chrome. Recent Chrome
35
+ // (`--headless=new`) does the print but doesn't reliably exit, so we wait for
36
+ // the PDF's size to settle, then kill the process — robust across versions.
37
+ export function printUrlToPdf(chrome, url, pdfPath) {
38
+ return new Promise((res, rej) => {
39
+ try { if (existsSync(pdfPath)) rmSync(pdfPath); } catch {}
40
+ const profile = mkdtempSync(join(tmpdir(), 'mdslides-'));
41
+ const args = [
42
+ '--headless=new',
43
+ '--disable-gpu',
44
+ '--no-sandbox',
45
+ '--hide-scrollbars',
46
+ '--no-pdf-header-footer',
47
+ '--run-all-compositor-stages-before-draw',
48
+ '--virtual-time-budget=5000',
49
+ `--user-data-dir=${profile}`,
50
+ `--print-to-pdf=${pdfPath}`,
51
+ url,
52
+ ];
53
+ const ps = spawn(chrome, args, { stdio: 'ignore' });
54
+
55
+ let done = false;
56
+ let lastSize = -1;
57
+ let stable = 0;
58
+ const finish = (err) => {
59
+ if (done) return;
60
+ done = true;
61
+ clearInterval(poll);
62
+ clearTimeout(timer);
63
+ try { ps.kill('SIGKILL'); } catch {}
64
+ try { rmSync(profile, { recursive: true, force: true }); } catch {}
65
+ err ? rej(err) : res(pdfPath);
66
+ };
67
+ const poll = setInterval(() => {
68
+ if (!existsSync(pdfPath)) return;
69
+ let size = 0;
70
+ try { size = statSync(pdfPath).size; } catch { return; }
71
+ if (size > 0 && size === lastSize) {
72
+ if (++stable >= 3) finish();
73
+ } else {
74
+ stable = 0;
75
+ lastSize = size;
76
+ }
77
+ }, 300);
78
+ const timer = setTimeout(() => finish(new Error('Chrome timed out after 45s')), 45000);
79
+
80
+ ps.on('error', finish);
81
+ ps.on('exit', (code) => {
82
+ if (existsSync(pdfPath)) finish();
83
+ else finish(new Error(`Chrome exited with code ${code} without producing a PDF`));
84
+ });
85
+ });
86
+ }
@@ -0,0 +1,59 @@
1
+ // Syntax-highlight fenced code blocks with Shiki.
2
+ //
3
+ // We keep the deck's own `<pre><code>` wrapper (so the theme's border, radius,
4
+ // padding and dark background still apply) and only swap the code's children
5
+ // for Shiki's colored token spans. Spans carry inline `color` styles, so no
6
+ // extra theme CSS is needed. Code blocks with no language — or an unknown one —
7
+ // are left untouched.
8
+ import { codeToHast } from 'shiki';
9
+
10
+ const THEME = 'github-dark';
11
+
12
+ function rawText(node) {
13
+ if (node.type === 'text') return node.value;
14
+ if (Array.isArray(node.children)) return node.children.map(rawText).join('');
15
+ return '';
16
+ }
17
+
18
+ function langOf(codeEl) {
19
+ const cls = codeEl.properties?.className;
20
+ const list = Array.isArray(cls) ? cls : cls ? [cls] : [];
21
+ const hit = list.map(String).find((c) => c.startsWith('language-'));
22
+ return hit ? hit.slice('language-'.length) : null;
23
+ }
24
+
25
+ export default function rehypeShiki() {
26
+ // Collect <pre><code> blocks first, then highlight them (async).
27
+ return async (tree) => {
28
+ const blocks = [];
29
+ const visit = (node) => {
30
+ if (
31
+ node.type === 'element' &&
32
+ node.tagName === 'pre' &&
33
+ node.children?.[0]?.type === 'element' &&
34
+ node.children[0].tagName === 'code'
35
+ ) {
36
+ blocks.push(node.children[0]);
37
+ return; // don't descend into code
38
+ }
39
+ if (Array.isArray(node.children)) node.children.forEach(visit);
40
+ };
41
+ visit(tree);
42
+
43
+ await Promise.all(
44
+ blocks.map(async (code) => {
45
+ const lang = langOf(code);
46
+ if (!lang) return;
47
+ const source = rawText(code).replace(/\n$/, '');
48
+ try {
49
+ const hast = await codeToHast(source, { lang, theme: THEME });
50
+ const pre = hast.children.find((c) => c.tagName === 'pre');
51
+ const inner = pre?.children.find((c) => c.tagName === 'code');
52
+ if (inner) code.children = inner.children;
53
+ } catch {
54
+ // Unknown language → leave the code block as plain text.
55
+ }
56
+ })
57
+ );
58
+ };
59
+ }
@@ -0,0 +1,45 @@
1
+ // ==highlight== → <mark>highlight</mark>
2
+ const HIGHLIGHT_RE = /==([^=\n]+?)==/g;
3
+
4
+ function transformChildren(parent) {
5
+ if (!Array.isArray(parent.children)) return;
6
+ const out = [];
7
+ for (const child of parent.children) {
8
+ if (Array.isArray(child.children)) transformChildren(child);
9
+
10
+ if (child.type !== 'text' || typeof child.value !== 'string' || !child.value.includes('==')) {
11
+ out.push(child);
12
+ continue;
13
+ }
14
+
15
+ const value = child.value;
16
+ let lastIdx = 0;
17
+ let matched = false;
18
+ HIGHLIGHT_RE.lastIndex = 0;
19
+ let m;
20
+ while ((m = HIGHLIGHT_RE.exec(value)) !== null) {
21
+ matched = true;
22
+ if (m.index > lastIdx) {
23
+ out.push({ type: 'text', value: value.slice(lastIdx, m.index) });
24
+ }
25
+ // Custom mdast node → <mark> via data.hName (no raw HTML, so it
26
+ // survives remark-rehype without allowDangerousHtml).
27
+ out.push({
28
+ type: 'highlight',
29
+ data: { hName: 'mark' },
30
+ children: [{ type: 'text', value: m[1] }],
31
+ });
32
+ lastIdx = m.index + m[0].length;
33
+ }
34
+ if (!matched) {
35
+ out.push(child);
36
+ } else if (lastIdx < value.length) {
37
+ out.push({ type: 'text', value: value.slice(lastIdx) });
38
+ }
39
+ }
40
+ parent.children = out;
41
+ }
42
+
43
+ export default function remarkHighlight() {
44
+ return (tree) => transformChildren(tree);
45
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "mdslides-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28">
2
+ <defs>
3
+ <linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#ff0080"/>
5
+ <stop offset="50%" stop-color="#ff4500"/>
6
+ <stop offset="100%" stop-color="#ffa500"/>
7
+ </linearGradient>
8
+ <style>
9
+ .frame { fill: #0d0d0d; }
10
+ @media (prefers-color-scheme: light) { .frame { fill: #ffffff; } }
11
+ </style>
12
+ </defs>
13
+ <rect x="0" y="0" width="28" height="28" rx="7" fill="url(#g)"/>
14
+ <rect x="4" y="9" width="20" height="13" rx="2" class="frame"/>
15
+ <rect x="7" y="12" width="14" height="2.4" rx="1.2" fill="url(#g)"/>
16
+ <rect x="7" y="16.5" width="9" height="2.4" rx="1.2" fill="url(#g)"/>
17
+ </svg>