ippon-slides 0.1.0 → 0.1.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/README.md CHANGED
@@ -131,6 +131,19 @@ import { render, toPdf } from 'ippon-slides'
131
131
  const { html } = render(markdown, { baseDir: '.', outDir: 'dist' })
132
132
  ```
133
133
 
134
+ 型定義(`.d.ts`)も同梱しています。
135
+
136
+ ## 開発
137
+
138
+ TypeScript で書いています。`src/` が Node 側、`src/client/deck.ts` がブラウザで動くナビゲーションです。
139
+
140
+ ```sh
141
+ npm install
142
+ npm run build # dist/ と assets/deck.js を作る
143
+ npm run typecheck
144
+ npm test # ビルドしてからテスト(テストの実行には Node 22.18+ が必要)
145
+ ```
146
+
134
147
  ## License
135
148
 
136
149
  MIT
package/assets/deck.js CHANGED
@@ -1,211 +1,197 @@
1
+ "use strict";
1
2
  // スライドのナビゲーション。ippon が HTML に埋め込む。
2
3
  // → / Space / PageDown / クリック右側 次へ
3
4
  // ← / PageUp / クリック左側 前へ
4
5
  // Home / End 最初 / 最後
5
6
  // f フルスクリーン
6
7
  // o / Esc 一覧
7
- ;(function () {
8
- var slides = Array.prototype.slice.call(document.querySelectorAll('#deck .slide'))
9
- if (!slides.length) return
10
-
11
- var idx = 0
12
- var progress = document.getElementById('progress')
13
- var counter = document.getElementById('counter')
14
- var timerEl = document.getElementById('timer')
15
-
16
- function clamp(n) {
17
- return Math.max(0, Math.min(slides.length - 1, n))
18
- }
19
-
20
- // ---------- タイマー(表紙から進んだら開始、表紙に戻ったらリセット) ----------
21
-
22
- var startedAt = null
23
- var ticking = null
24
- function fmt(ms) {
25
- var s = Math.floor(ms / 1000)
26
- var m = Math.floor(s / 60)
27
- s = s % 60
28
- return m + ':' + (s < 10 ? '0' : '') + s
29
- }
30
- function syncTimer() {
31
- if (idx === 0) {
32
- startedAt = null
33
- clearInterval(ticking)
34
- ticking = null
35
- timerEl.textContent = ''
36
- return
8
+ (() => {
9
+ const slides = Array.from(document.querySelectorAll('#deck .slide'));
10
+ if (!slides.length)
11
+ return;
12
+ const $ = (id) => document.getElementById(id);
13
+ let idx = 0;
14
+ const progress = $('progress');
15
+ const counter = $('counter');
16
+ const timerEl = $('timer');
17
+ const clamp = (n) => Math.max(0, Math.min(slides.length - 1, n));
18
+ // ---------- タイマー(表紙から進んだら開始、表紙に戻ったらリセット) ----------
19
+ let startedAt = null;
20
+ let ticking;
21
+ function fmt(ms) {
22
+ const total = Math.floor(ms / 1000);
23
+ const m = Math.floor(total / 60);
24
+ const s = total % 60;
25
+ return m + ':' + (s < 10 ? '0' : '') + s;
37
26
  }
38
- if (startedAt === null) startedAt = Date.now()
39
- if (!ticking) {
40
- ticking = setInterval(function () {
41
- timerEl.textContent = fmt(Date.now() - startedAt)
42
- }, 500)
27
+ function syncTimer() {
28
+ if (idx === 0) {
29
+ startedAt = null;
30
+ clearInterval(ticking);
31
+ ticking = undefined;
32
+ timerEl.textContent = '';
33
+ return;
34
+ }
35
+ const t0 = (startedAt ?? (startedAt = Date.now()));
36
+ ticking ?? (ticking = window.setInterval(() => {
37
+ timerEl.textContent = fmt(Date.now() - t0);
38
+ }, 500));
39
+ timerEl.textContent = fmt(Date.now() - t0);
43
40
  }
44
- timerEl.textContent = fmt(Date.now() - startedAt)
45
- }
46
-
47
- // ---------- 描画 ----------
48
-
49
- function render() {
50
- slides.forEach(function (s, i) {
51
- s.classList.toggle('active', i === idx)
52
- })
53
- slides[idx].scrollTop = 0
54
- counter.textContent = idx + 1 + ' / ' + slides.length
55
- progress.style.width = ((idx + 1) / slides.length) * 100 + '%'
56
- syncTimer()
57
- }
58
- function go(n) {
59
- n = clamp(n)
60
- if (n === idx) return
61
- idx = n
62
- render()
63
- history.replaceState(null, '', '#' + (idx + 1))
64
- }
65
- window.addEventListener('hashchange', function () {
66
- var n = parseInt(location.hash.slice(1), 10)
67
- if (n) go(n - 1)
68
- })
69
-
70
- // ---------- 一覧 ----------
71
-
72
- var overview = document.getElementById('overview')
73
- var ovItems = null
74
- var ovOpen = false
75
- var THUMB = 240
76
-
77
- function buildOverview() {
78
- ovItems = slides.map(function (s, i) {
79
- var item = document.createElement('div')
80
- item.className = 'ov-item'
81
- var frame = document.createElement('div')
82
- frame.className = 'ov-frame'
83
- var scale = document.createElement('div')
84
- scale.className = 'ov-scale'
85
- var clone = s.cloneNode(true)
86
- clone.classList.remove('active')
87
- scale.appendChild(clone)
88
- frame.appendChild(scale)
89
- var num = document.createElement('div')
90
- num.className = 'ov-num'
91
- num.textContent = i + 1
92
- item.appendChild(frame)
93
- item.appendChild(num)
94
- item.addEventListener('click', function () {
95
- idx = i
96
- closeOverview()
97
- render()
98
- history.replaceState(null, '', '#' + (idx + 1))
99
- })
100
- overview.appendChild(item)
101
- return { item: item, frame: frame, scale: scale }
102
- })
103
- }
104
- function layoutOverview() {
105
- var w = window.innerWidth
106
- var h = window.innerHeight
107
- var k = THUMB / w
108
- ovItems.forEach(function (t) {
109
- t.frame.style.width = THUMB + 'px'
110
- t.frame.style.height = h * k + 'px'
111
- t.scale.style.width = w + 'px'
112
- t.scale.style.height = h + 'px'
113
- t.scale.style.transform = 'scale(' + k + ')'
114
- })
115
- }
116
- function openOverview() {
117
- if (!ovItems) buildOverview()
118
- layoutOverview()
119
- ovItems.forEach(function (t, i) {
120
- t.item.classList.toggle('current', i === idx)
121
- })
122
- ovOpen = true
123
- document.body.classList.add('overview')
124
- ovItems[idx].item.scrollIntoView({ block: 'center' })
125
- }
126
- function closeOverview() {
127
- ovOpen = false
128
- document.body.classList.remove('overview')
129
- }
130
- window.addEventListener('resize', function () {
131
- if (ovOpen) layoutOverview()
132
- })
133
-
134
- // ---------- 入力 ----------
135
-
136
- document.addEventListener('keydown', function (e) {
137
- if (e.metaKey || e.ctrlKey || e.altKey) return
138
- if (ovOpen) {
139
- if (e.key === 'Escape' || e.key === 'o') closeOverview()
140
- return
41
+ // ---------- 描画 ----------
42
+ function render() {
43
+ slides.forEach((s, i) => s.classList.toggle('active', i === idx));
44
+ slides[idx].scrollTop = 0;
45
+ counter.textContent = idx + 1 + ' / ' + slides.length;
46
+ progress.style.width = ((idx + 1) / slides.length) * 100 + '%';
47
+ syncTimer();
141
48
  }
142
- switch (e.key) {
143
- case 'ArrowRight':
144
- case 'PageDown':
145
- case ' ':
146
- e.preventDefault()
147
- go(idx + 1)
148
- break
149
- case 'ArrowLeft':
150
- case 'PageUp':
151
- e.preventDefault()
152
- go(idx - 1)
153
- break
154
- case 'Home':
155
- go(0)
156
- break
157
- case 'End':
158
- go(slides.length - 1)
159
- break
160
- case 'f':
161
- if (document.fullscreenElement) document.exitFullscreen()
162
- else document.documentElement.requestFullscreen()
163
- break
164
- case 'o':
165
- openOverview()
166
- break
167
- case 'Escape':
168
- if (!document.fullscreenElement) openOverview()
169
- break
49
+ function go(n) {
50
+ n = clamp(n);
51
+ if (n === idx)
52
+ return;
53
+ idx = n;
54
+ render();
55
+ history.replaceState(null, '', '#' + (idx + 1));
170
56
  }
171
- })
172
-
173
- // 画面の右 3 割で次へ、左 3 割で前へ(リンクと文字選択は邪魔しない)
174
- var deck = document.getElementById('deck')
175
- deck.addEventListener('click', function (e) {
176
- if (e.target.closest('a, button')) return
177
- if (String(window.getSelection() || '')) return
178
- var x = e.clientX / window.innerWidth
179
- if (x > 0.7) go(idx + 1)
180
- else if (x < 0.3) go(idx - 1)
181
- })
182
-
183
- // 横スワイプ
184
- var tx = null
185
- var ty = null
186
- deck.addEventListener(
187
- 'touchstart',
188
- function (e) {
189
- if (e.touches.length !== 1) return (tx = null)
190
- tx = e.touches[0].clientX
191
- ty = e.touches[0].clientY
192
- },
193
- { passive: true }
194
- )
195
- deck.addEventListener(
196
- 'touchend',
197
- function (e) {
198
- if (tx === null) return
199
- var t = e.changedTouches[0]
200
- var dx = t.clientX - tx
201
- var dy = t.clientY - ty
202
- tx = null
203
- if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5) go(dx < 0 ? idx + 1 : idx - 1)
204
- },
205
- { passive: true }
206
- )
207
-
208
- var start = parseInt(location.hash.slice(1), 10)
209
- if (start) idx = clamp(start - 1)
210
- render()
211
- })()
57
+ window.addEventListener('hashchange', () => {
58
+ const n = parseInt(location.hash.slice(1), 10);
59
+ if (n)
60
+ go(n - 1);
61
+ });
62
+ const overview = $('overview');
63
+ let ovItems = null;
64
+ let ovOpen = false;
65
+ const THUMB = 240;
66
+ const div = (className) => {
67
+ const el = document.createElement('div');
68
+ el.className = className;
69
+ return el;
70
+ };
71
+ function buildOverview() {
72
+ return slides.map((s, i) => {
73
+ const item = div('ov-item');
74
+ const frame = div('ov-frame');
75
+ const scale = div('ov-scale');
76
+ const clone = s.cloneNode(true);
77
+ clone.classList.remove('active');
78
+ scale.appendChild(clone);
79
+ frame.appendChild(scale);
80
+ const num = div('ov-num');
81
+ num.textContent = String(i + 1);
82
+ item.appendChild(frame);
83
+ item.appendChild(num);
84
+ item.addEventListener('click', () => {
85
+ idx = i;
86
+ closeOverview();
87
+ render();
88
+ history.replaceState(null, '', '#' + (idx + 1));
89
+ });
90
+ overview.appendChild(item);
91
+ return { item, frame, scale };
92
+ });
93
+ }
94
+ function layoutOverview(items) {
95
+ const w = window.innerWidth;
96
+ const h = window.innerHeight;
97
+ const k = THUMB / w;
98
+ for (const t of items) {
99
+ t.frame.style.width = THUMB + 'px';
100
+ t.frame.style.height = h * k + 'px';
101
+ t.scale.style.width = w + 'px';
102
+ t.scale.style.height = h + 'px';
103
+ t.scale.style.transform = 'scale(' + k + ')';
104
+ }
105
+ }
106
+ function openOverview() {
107
+ const items = (ovItems ?? (ovItems = buildOverview()));
108
+ layoutOverview(items);
109
+ items.forEach((t, i) => t.item.classList.toggle('current', i === idx));
110
+ ovOpen = true;
111
+ document.body.classList.add('overview');
112
+ items[idx].item.scrollIntoView({ block: 'center' });
113
+ }
114
+ function closeOverview() {
115
+ ovOpen = false;
116
+ document.body.classList.remove('overview');
117
+ }
118
+ window.addEventListener('resize', () => {
119
+ if (ovOpen && ovItems)
120
+ layoutOverview(ovItems);
121
+ });
122
+ // ---------- 入力 ----------
123
+ document.addEventListener('keydown', (e) => {
124
+ if (e.metaKey || e.ctrlKey || e.altKey)
125
+ return;
126
+ if (ovOpen) {
127
+ if (e.key === 'Escape' || e.key === 'o')
128
+ closeOverview();
129
+ return;
130
+ }
131
+ switch (e.key) {
132
+ case 'ArrowRight':
133
+ case 'PageDown':
134
+ case ' ':
135
+ e.preventDefault();
136
+ go(idx + 1);
137
+ break;
138
+ case 'ArrowLeft':
139
+ case 'PageUp':
140
+ e.preventDefault();
141
+ go(idx - 1);
142
+ break;
143
+ case 'Home':
144
+ go(0);
145
+ break;
146
+ case 'End':
147
+ go(slides.length - 1);
148
+ break;
149
+ case 'f':
150
+ if (document.fullscreenElement)
151
+ document.exitFullscreen();
152
+ else
153
+ document.documentElement.requestFullscreen();
154
+ break;
155
+ case 'o':
156
+ openOverview();
157
+ break;
158
+ case 'Escape':
159
+ if (!document.fullscreenElement)
160
+ openOverview();
161
+ break;
162
+ }
163
+ });
164
+ // 画面の右 3 割で次へ、左 3 割で前へ(リンクと文字選択は邪魔しない)
165
+ const deck = $('deck');
166
+ deck.addEventListener('click', (e) => {
167
+ if (e.target instanceof Element && e.target.closest('a, button'))
168
+ return;
169
+ if (String(window.getSelection() || ''))
170
+ return;
171
+ const x = e.clientX / window.innerWidth;
172
+ if (x > 0.7)
173
+ go(idx + 1);
174
+ else if (x < 0.3)
175
+ go(idx - 1);
176
+ });
177
+ // 横スワイプ
178
+ let touch = null;
179
+ deck.addEventListener('touchstart', (e) => {
180
+ const t = e.touches[0];
181
+ touch = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;
182
+ }, { passive: true });
183
+ deck.addEventListener('touchend', (e) => {
184
+ const t = e.changedTouches[0];
185
+ if (!touch || !t)
186
+ return;
187
+ const dx = t.clientX - touch.x;
188
+ const dy = t.clientY - touch.y;
189
+ touch = null;
190
+ if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5)
191
+ go(dx < 0 ? idx + 1 : idx - 1);
192
+ }, { passive: true });
193
+ const start = parseInt(location.hash.slice(1), 10);
194
+ if (start)
195
+ idx = clamp(start - 1);
196
+ render();
197
+ })();
package/bin/ippon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { main } from '../src/cli.js'
2
+ import { main } from '../dist/cli.js'
3
3
 
4
4
  main(process.argv.slice(2)).catch((e) => {
5
5
  console.error(`✗ ${e.message}`)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export interface Options {
2
+ /** 位置引数 */
3
+ _: string[];
4
+ html?: boolean;
5
+ pdf?: boolean;
6
+ watch?: boolean;
7
+ help?: boolean;
8
+ version?: boolean;
9
+ output?: string;
10
+ css?: string;
11
+ browserPath?: string;
12
+ }
13
+ export declare function parseArgs(argv: string[]): Options;
14
+ /** 出力先を決める */
15
+ export type Format = 'html' | 'pdf';
16
+ export declare function outputs(input: string, { html, pdf, output }: Pick<Options, 'html' | 'pdf' | 'output'>): {
17
+ format: Format;
18
+ path: string;
19
+ }[];
20
+ export declare function main(argv: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,236 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, extname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { toPdf } from './pdf.js';
5
+ import { render } from './render.js';
6
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'));
7
+ const HELP = `ippon v${pkg.version} — 1 枚 1 メッセージのスライドを Markdown から作る
8
+
9
+ 使い方:
10
+ ippon build <input.md> [options] スライドをビルドする
11
+ ippon <input.md> [options] build の省略形
12
+ ippon init [file] サンプルの Markdown を作る(デフォルト: slide.md)
13
+
14
+ build のオプション:
15
+ --html HTML を出力する(--pdf がなければデフォルト)
16
+ --pdf PDF を出力する(Chrome / Chromium / Edge が必要)
17
+ -o, --output <path> 出力先。--html と --pdf を両方指定したときは拡張子だけ差し替える
18
+ ディレクトリを指定するとその中に <input>.html / .pdf を作る
19
+ -w, --watch Markdown と --css の変更を監視して再ビルドする
20
+ --css <file> 追加の CSS(色やフォントの上書きなど)
21
+ --browser-path <path> PDF に使うブラウザ(環境変数 CHROME_PATH でも可)
22
+
23
+ -h, --help ヘルプ
24
+ -v, --version バージョン
25
+
26
+ 例:
27
+ ippon build slide.md --html
28
+ ippon build slide.md --pdf -o dist/
29
+ ippon build slide.md --html --pdf -o dist/talk
30
+ ippon build slide.md --html -w
31
+ `;
32
+ const FLAGS = { '--html': 'html', '--pdf': 'pdf', '-w': 'watch', '--watch': 'watch', '-h': 'help', '--help': 'help', '-v': 'version', '--version': 'version' };
33
+ const VALUES = { '-o': 'output', '--output': 'output', '--css': 'css', '--browser-path': 'browserPath' };
34
+ export function parseArgs(argv) {
35
+ const opts = { _: [] };
36
+ for (let i = 0; i < argv.length; i++) {
37
+ let a = argv[i];
38
+ let v;
39
+ const eq = a.match(/^(--[^=]+)=(.*)$/);
40
+ if (eq)
41
+ [, a, v] = eq;
42
+ const flag = FLAGS[a];
43
+ const value = VALUES[a];
44
+ if (flag)
45
+ opts[flag] = true;
46
+ else if (value) {
47
+ if (v === undefined)
48
+ v = argv[++i];
49
+ if (v === undefined)
50
+ throw new Error(`${a} には値が必要です`);
51
+ opts[value] = v;
52
+ }
53
+ else if (a.startsWith('-') && a !== '-')
54
+ throw new Error(`不明なオプション: ${a}`);
55
+ else
56
+ opts._.push(a);
57
+ }
58
+ return opts;
59
+ }
60
+ export function outputs(input, { html, pdf, output }) {
61
+ const formats = [];
62
+ if (html || !pdf)
63
+ formats.push('html');
64
+ if (pdf)
65
+ formats.push('pdf');
66
+ const name = basename(input, extname(input));
67
+ let base = ''; // 拡張子なしのパス
68
+ let exact = null; // 1 形式だけのときに -o をそのまま使う
69
+ if (!output)
70
+ base = join(dirname(input), name);
71
+ else if (output.endsWith('/') || output.endsWith('\\') || (existsSync(output) && statSync(output).isDirectory()))
72
+ base = join(output, name);
73
+ else if (formats.length === 1 && extname(output))
74
+ exact = output;
75
+ else
76
+ base = extname(output) ? output.slice(0, -extname(output).length) : output;
77
+ return formats.map((f) => ({ format: f, path: resolve(exact || `${base}.${f}`) }));
78
+ }
79
+ // ---------------------------------------------------------------
80
+ // build
81
+ // ---------------------------------------------------------------
82
+ const time = () => new Date().toLocaleTimeString();
83
+ async function build(input, opts) {
84
+ const md = readFileSync(input, 'utf8');
85
+ const css = opts.css ? readFileSync(opts.css, 'utf8') : '';
86
+ const baseDir = dirname(resolve(input));
87
+ for (const { format, path } of outputs(input, opts)) {
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ if (format === 'html') {
90
+ const { html, count } = render(md, { baseDir, outDir: dirname(path), css });
91
+ writeFileSync(path, html);
92
+ console.log(`${time()} ✓ ${count} slides → ${path}`);
93
+ }
94
+ else {
95
+ const { html, count } = render(md, { baseDir, css });
96
+ await toPdf(html, path, { browserPath: opts.browserPath });
97
+ console.log(`${time()} ✓ ${count} slides → ${path}`);
98
+ }
99
+ }
100
+ }
101
+ function watchBuild(input, opts) {
102
+ let timer;
103
+ let running = Promise.resolve();
104
+ const rebuild = () => {
105
+ clearTimeout(timer);
106
+ timer = setTimeout(() => {
107
+ running = running.then(() => build(input, opts)).catch((e) => console.error(`${time()} ✗ ${e.message}`));
108
+ }, 80);
109
+ };
110
+ // エディタによってはファイルを置き換えて保存するので、ディレクトリごと監視する
111
+ const files = [input, opts.css].filter((f) => f !== undefined).map((f) => resolve(f));
112
+ for (const dir of new Set(files.map(dirname))) {
113
+ watch(dir, (_, file) => {
114
+ if (file && files.includes(join(dir, file.toString())))
115
+ rebuild();
116
+ });
117
+ }
118
+ console.log(`watching ${files.join(', ')} ...`);
119
+ }
120
+ // ---------------------------------------------------------------
121
+ // init
122
+ // ---------------------------------------------------------------
123
+ const SAMPLE = `---
124
+ title: はじめての ippon
125
+ author: あなたの名前
126
+ ---
127
+
128
+ ::title::
129
+
130
+ # はじめての ippon
131
+
132
+ サブタイトル
133
+
134
+ あなたの名前 ・ 2026-01-01
135
+
136
+ ---
137
+
138
+ ::center::
139
+
140
+ 1 枚に
141
+
142
+ 1 メッセージ
143
+
144
+ ---
145
+
146
+ ::center size=2::
147
+
148
+ **大きく**
149
+
150
+ ---
151
+
152
+ ::center bg=blue::
153
+
154
+ 背景色も変えられる
155
+
156
+ -# blue / orange / purple / red / green / black / white、または #rrggbb
157
+
158
+ ---
159
+
160
+ ::center size=1.5 bg=white::
161
+
162
+ セクション
163
+
164
+ ---
165
+
166
+ ## 箇条書き
167
+
168
+ - **太字** はアクセントカラーになる
169
+ - \`inline code\` も書ける
170
+ - ネストもできる
171
+ - 矢印 → は少し薄くなる
172
+
173
+ ---
174
+
175
+ ## 2 カラム
176
+
177
+ - 左に説明
178
+ - 右にコード
179
+
180
+ ::right::
181
+
182
+ \`\`\`ts
183
+ const app = new Hono()
184
+ !! app.get('/', (c) => c.text('Hello!'))
185
+ \`\`\`
186
+
187
+ -# 行頭に "!! " を付けるとその行が強調される
188
+
189
+ ---
190
+
191
+ \`\`\`console
192
+ $ npx ippon-slides build slide.md --html --pdf
193
+ ✓ 9 slides → slide.html
194
+ ✓ 9 slides → slide.pdf
195
+ \`\`\`
196
+
197
+ ---
198
+
199
+ ::title::
200
+
201
+ # ありがとうございました
202
+
203
+ あなたの名前
204
+ `;
205
+ function init(file = 'slide.md') {
206
+ if (existsSync(file))
207
+ throw new Error(`${file} はすでにあります`);
208
+ writeFileSync(file, SAMPLE);
209
+ console.log(`✓ ${file} を作りました → ippon build ${file} --html -w`);
210
+ }
211
+ // ---------------------------------------------------------------
212
+ // main
213
+ // ---------------------------------------------------------------
214
+ export async function main(argv) {
215
+ const opts = parseArgs(argv);
216
+ if (opts.version)
217
+ return console.log(pkg.version);
218
+ const [cmd, ...args] = opts._;
219
+ if (opts.help || cmd === undefined)
220
+ return console.log(HELP);
221
+ let rest = args;
222
+ if (cmd === 'init')
223
+ return init(rest[0]);
224
+ if (cmd !== 'build')
225
+ rest = [cmd, ...rest]; // `ippon slide.md` は build の省略形
226
+ const input = rest[0];
227
+ if (!input)
228
+ throw new Error('入力の Markdown を指定してください(例: ippon build slide.md --html)');
229
+ if (rest.length > 1)
230
+ throw new Error(`入力は 1 つだけ指定してください: ${rest.join(' ')}`);
231
+ if (!existsSync(input))
232
+ throw new Error(`${input} が見つかりません`);
233
+ await build(input, opts);
234
+ if (opts.watch)
235
+ watchBuild(input, opts);
236
+ }
@@ -0,0 +1,2 @@
1
+ export declare const esc: (s: string) => string;
2
+ export declare function highlight(code: string, lang?: string): string;