orz-slides 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/dist/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * orz-slides — generate a self-contained, presentable .slides.html from a deck
4
+ * source (orz-markdown + the slide layout syntax).
5
+ *
6
+ * Usage:
7
+ * orz-slides <input.md> [options]
8
+ *
9
+ * Options:
10
+ * -o, --out <file> output path (default: <input>.slides.html)
11
+ * --theme <name> theme id (fallback; the deck's `<!-- deck theme: -->` wins)
12
+ * --inline embed the engine bundle + theme CSS in the file (default)
13
+ * --cdn reference the engine + theme from jsDelivr (small files)
14
+ * --title <text> document <title> (fallback; deck `title:` wins)
15
+ */
16
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
17
+ import { basename, extname, dirname, resolve, join } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { createRequire } from 'node:module';
20
+ import { randomUUID } from 'node:crypto';
21
+ import { getBrowserRuntimeScript } from 'orz-markdown/runtime';
22
+ import { parseDeck } from './slide-parser.js';
23
+ import { buildHtml } from './template.js';
24
+ /** The seven shipped slide themes (served from the orz-slides package on CDN). */
25
+ const THEME_DEFS = [
26
+ { id: 'paper', name: 'Paper', scheme: 'light' },
27
+ { id: 'architect', name: 'Architect', scheme: 'light' },
28
+ { id: 'executive', name: 'Executive', scheme: 'light' },
29
+ { id: 'sage', name: 'Sage', scheme: 'light' },
30
+ { id: 'poppy', name: 'Poppy', scheme: 'light' },
31
+ { id: 'neon', name: 'Neon', scheme: 'dark' },
32
+ { id: 'chalk', name: 'Chalk', scheme: 'dark' },
33
+ ];
34
+ const require = createRequire(import.meta.url);
35
+ const HERE = dirname(fileURLToPath(import.meta.url));
36
+ function pkgVersion(name, fallback = '0.0.0') {
37
+ try {
38
+ let dir = dirname(require.resolve(name));
39
+ while (!existsSync(join(dir, 'package.json')))
40
+ dir = dirname(dir);
41
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version || fallback;
42
+ }
43
+ catch {
44
+ return fallback;
45
+ }
46
+ }
47
+ /** orz-slides' own version — pins the engine bundle + theme CDN + version check. */
48
+ function selfVersion() {
49
+ for (const p of [join(HERE, '..', 'package.json'), join(HERE, '..', '..', 'package.json')]) {
50
+ try {
51
+ const j = JSON.parse(readFileSync(p, 'utf8'));
52
+ if (j.name === 'orz-slides' && j.version)
53
+ return j.version;
54
+ }
55
+ catch { /* keep looking */ }
56
+ }
57
+ return '0.0.0';
58
+ }
59
+ /** assets/ sits next to dist/ when published, and next to src/ in dev. */
60
+ function findAsset(rel) {
61
+ for (const p of [join(HERE, '..', 'assets', rel), join(HERE, '..', '..', 'assets', rel)]) {
62
+ if (existsSync(p))
63
+ return p;
64
+ }
65
+ throw new Error(`asset not found: ${rel}`);
66
+ }
67
+ /** A theme's CSS without its `@import url('./base.css')` (base is inlined once). */
68
+ function themeOnly(id) {
69
+ const css = readFileSync(findAsset(`themes/theme-${id}.css`), 'utf8');
70
+ return css.replace(/@import\s+url\(\s*['"]?\.\/base\.css['"]?\s*\)\s*;/, '');
71
+ }
72
+ function parseArgs(argv) {
73
+ const a = { delivery: 'inline' };
74
+ for (let i = 0; i < argv.length; i++) {
75
+ const arg = argv[i];
76
+ if (arg === '-o' || arg === '--out')
77
+ a.out = argv[++i];
78
+ else if (arg === '--theme')
79
+ a.theme = argv[++i];
80
+ else if (arg === '--inline')
81
+ a.delivery = 'inline';
82
+ else if (arg === '--cdn')
83
+ a.delivery = 'cdn';
84
+ else if (arg === '--title')
85
+ a.title = argv[++i];
86
+ else if (!arg.startsWith('-'))
87
+ a.input = arg;
88
+ }
89
+ return a;
90
+ }
91
+ function main() {
92
+ const args = parseArgs(process.argv.slice(2));
93
+ if (!args.input) {
94
+ console.error('Usage: orz-slides <input.md> [-o out] [--theme name] [--inline|--cdn]');
95
+ process.exit(1);
96
+ }
97
+ const inputPath = resolve(args.input);
98
+ const source = readFileSync(inputPath, 'utf8');
99
+ const base = basename(inputPath, extname(inputPath)).replace(/\.slides$/, '');
100
+ const outPath = args.out ? resolve(args.out) : join(dirname(inputPath), `${base}.slides.html`);
101
+ // The deck's own config wins; CLI flags are fallbacks.
102
+ const deck = parseDeck(source);
103
+ const ver = selfVersion();
104
+ const themeBase = `https://cdn.jsdelivr.net/npm/orz-slides@${ver}/assets/themes`;
105
+ const themes = THEME_DEFS.map((t) => ({ ...t, href: `${themeBase}/theme-${t.id}.css` }));
106
+ const wanted = deck.config.theme || args.theme || 'paper';
107
+ const defaultTheme = themes.some((t) => t.id === wanted) ? wanted : themes[0].id;
108
+ const ratio = deck.config.ratio || '16:9';
109
+ const title = deck.config.title || args.title || base;
110
+ // Engine + theme delivery.
111
+ let renderer;
112
+ let theme;
113
+ if (args.delivery === 'inline') {
114
+ const bundlePath = [
115
+ join(HERE, 'orz-slides.browser.js'),
116
+ join(HERE, '..', 'dist', 'orz-slides.browser.js'),
117
+ ].find(existsSync);
118
+ if (!bundlePath) {
119
+ console.error('Inline mode needs the engine bundle. Run: npm run bundle');
120
+ process.exit(1);
121
+ }
122
+ renderer = { mode: 'inline', js: readFileSync(bundlePath, 'utf8') };
123
+ theme = {
124
+ mode: 'inline',
125
+ base: readFileSync(findAsset('themes/base.css'), 'utf8'),
126
+ themes: THEME_DEFS.map((t) => ({ id: t.id, css: themeOnly(t.id) })),
127
+ };
128
+ }
129
+ else {
130
+ renderer = { mode: 'cdn', src: `https://cdn.jsdelivr.net/npm/orz-slides-browser@${ver}/orz-slides.browser.js` };
131
+ theme = { mode: 'cdn' };
132
+ }
133
+ const revealVer = pkgVersion('reveal.js', '5.0.4');
134
+ // reveal's reset.css + reveal.css contain only data: URIs (no external refs),
135
+ // so --inline embeds them and the deck presents fully offline.
136
+ let revealCss;
137
+ if (args.delivery === 'inline') {
138
+ const revealDist = dirname(require.resolve('reveal.js'));
139
+ revealCss = {
140
+ mode: 'inline',
141
+ reset: readFileSync(join(revealDist, 'reset.css'), 'utf8'),
142
+ core: readFileSync(join(revealDist, 'reveal.css'), 'utf8'),
143
+ };
144
+ }
145
+ else {
146
+ revealCss = {
147
+ mode: 'cdn',
148
+ resetUrl: `https://cdn.jsdelivr.net/npm/reveal.js@${revealVer}/dist/reset.css`,
149
+ coreUrl: `https://cdn.jsdelivr.net/npm/reveal.js@${revealVer}/dist/reveal.css`,
150
+ };
151
+ }
152
+ const appJs = readFileSync(findAsset('app.js'), 'utf8');
153
+ const CM = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16';
154
+ const html = buildHtml({
155
+ source,
156
+ title,
157
+ filename: base,
158
+ docId: randomUUID(),
159
+ rendererVersion: ver,
160
+ renderer,
161
+ theme,
162
+ defaultTheme,
163
+ themes,
164
+ ratio,
165
+ versionManifest: 'https://data.jsdelivr.com/v1/packages/npm/orz-slides-browser/resolved',
166
+ appJs,
167
+ runtime: getBrowserRuntimeScript(),
168
+ editorLibs: {
169
+ codemirrorCss: `${CM}/codemirror.min.css`,
170
+ codemirrorLightThemeCss: `${CM}/theme/eclipse.min.css`,
171
+ codemirrorDarkThemeCss: `${CM}/theme/material-darker.min.css`,
172
+ codemirrorJs: `${CM}/codemirror.min.js`,
173
+ codemirrorMarkdownJs: `${CM}/mode/markdown/markdown.min.js`,
174
+ codemirrorContinuelistJs: `${CM}/addon/edit/continuelist.min.js`,
175
+ },
176
+ revealCss,
177
+ cdn: {
178
+ katexCss: 'https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css',
179
+ mermaidJs: 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js',
180
+ smilesJs: 'https://cdn.jsdelivr.net/npm/smiles-drawer@1.0.10/dist/smiles-drawer.min.js',
181
+ chartJs: 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.js',
182
+ },
183
+ });
184
+ writeFileSync(outPath, html, 'utf8');
185
+ console.log(`Wrote ${outPath} (${args.delivery}, theme: ${defaultTheme}, ${deck.slides.length} slides)`);
186
+ }
187
+ main();
package/dist/layout.js ADDED
@@ -0,0 +1,42 @@
1
+ /** Escape a string for safe inclusion inside an HTML double-quoted attribute. */
2
+ function escapeAttr(value) {
3
+ return value
4
+ .replace(/&/g, '&amp;')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;');
8
+ }
9
+ /**
10
+ * Normalize a single track token: a bare number (`2`, `1.5`) becomes `<n>fr`;
11
+ * everything else (`auto`, `1fr`, `30%`, `200px`, `minmax(…)`, …) passes
12
+ * through unchanged.
13
+ */
14
+ function normalizeTrack(track) {
15
+ const trimmed = track.trim();
16
+ // Bare number (integer or decimal) → fr unit.
17
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
18
+ return `${trimmed}fr`;
19
+ }
20
+ return trimmed;
21
+ }
22
+ /** Render one node, appending region names to `regions` in document order. */
23
+ function renderNode(node, regions) {
24
+ if (node.kind === 'region') {
25
+ regions.push(node.name);
26
+ return `<div class="orz-region" data-region="${escapeAttr(node.name)}"></div>`;
27
+ }
28
+ // SplitNode.
29
+ const prop = node.dir === 'col' ? 'grid-template-columns' : 'grid-template-rows';
30
+ const tracks = node.tracks.map(normalizeTrack).join(' ');
31
+ const inner = node.children.map((child) => renderNode(child, regions)).join('');
32
+ return `<div class="orz-split" style="display:grid;${prop}:${tracks}">${inner}</div>`;
33
+ }
34
+ /**
35
+ * Render a layout tree to nested grid DOM with empty region cells.
36
+ * Returns the inner HTML for `.orz-content` and the region names in order.
37
+ */
38
+ export function renderLayout(node) {
39
+ const regions = [];
40
+ const html = renderNode(node, regions);
41
+ return { html, regions };
42
+ }