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 +13 -0
- package/assets/deck.js +187 -201
- package/bin/ippon.js +1 -1
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +236 -0
- package/dist/highlight.d.ts +2 -0
- package/dist/highlight.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +219 -0
- package/dist/pdf.d.ts +11 -0
- package/dist/pdf.js +83 -0
- package/dist/render.d.ts +14 -0
- package/dist/render.js +51 -0
- package/package.json +15 -4
- package/src/cli.js +0 -236
- package/src/highlight.js +0 -113
- package/src/index.js +0 -3
- package/src/markdown.js +0 -232
- package/src/pdf.js +0 -89
- package/src/render.js +0 -64
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
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
|
+
}
|