ippon-slides 0.0.0-stage → 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 +21 -0
- package/README.md +135 -2
- package/assets/deck.js +211 -0
- package/assets/style.css +517 -0
- package/bin/ippon.js +7 -0
- package/package.json +39 -4
- package/src/cli.js +236 -0
- package/src/highlight.js +113 -0
- package/src/index.js +3 -0
- package/src/markdown.js +232 -0
- package/src/pdf.js +89 -0
- package/src/render.js +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tanahiro2010
|
|
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
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ippon-slides
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**1 枚 1 メッセージ** のスライドを Markdown から作る CLI。
|
|
4
|
+
|
|
5
|
+
- 黒背景・大きな文字・切り替え時のフェードイン
|
|
6
|
+
- 出力は CSS / JS まで全部入った **HTML 1 ファイル**(会場がオフラインでも開ける)
|
|
7
|
+
- `--pdf` で 16:9 の **PDF** も出せる
|
|
8
|
+
- 依存パッケージなし
|
|
9
|
+
|
|
10
|
+
## 使い方
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx ippon-slides init # サンプルの slide.md を作る
|
|
14
|
+
npx ippon-slides build slide.md --html -w # 保存するたびに slide.html を作り直す
|
|
15
|
+
npx ippon-slides build slide.md --pdf # slide.pdf
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
グローバルに入れると `ippon` コマンドとして使えます。
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm i -g ippon-slides
|
|
22
|
+
ippon build slide.md --html --pdf -o dist/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### オプション
|
|
26
|
+
|
|
27
|
+
| オプション | |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `--html` | HTML を出力する(`--pdf` がなければデフォルト) |
|
|
30
|
+
| `--pdf` | PDF を出力する(Chrome / Chromium / Edge が必要) |
|
|
31
|
+
| `-o, --output <path>` | 出力先。ディレクトリを指定するとその中に `<input>.html` / `.pdf` を作る。`--html --pdf` を両方指定したときは拡張子だけ差し替える |
|
|
32
|
+
| `-w, --watch` | Markdown と `--css` の変更を監視して再ビルドする |
|
|
33
|
+
| `--css <file>` | 追加の CSS(色やフォントの上書きなど) |
|
|
34
|
+
| `--browser-path <path>` | PDF に使うブラウザ。環境変数 `CHROME_PATH` でも指定できる |
|
|
35
|
+
|
|
36
|
+
`ippon slide.md` は `ippon build slide.md` の省略形です。
|
|
37
|
+
|
|
38
|
+
## 書き方
|
|
39
|
+
|
|
40
|
+
````md
|
|
41
|
+
---
|
|
42
|
+
title: タイトル
|
|
43
|
+
author: 名前
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
::title::
|
|
47
|
+
|
|
48
|
+
# タイトル
|
|
49
|
+
|
|
50
|
+
サブタイトル
|
|
51
|
+
|
|
52
|
+
名前 ・ 日付
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
::center::
|
|
57
|
+
|
|
58
|
+
1 枚に
|
|
59
|
+
|
|
60
|
+
1 メッセージ
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
::center size=2 bg=blue::
|
|
65
|
+
|
|
66
|
+
**大きく**
|
|
67
|
+
|
|
68
|
+
-# 小さい注釈
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 見出しと箇条書き
|
|
73
|
+
|
|
74
|
+
- **太字** はアクセントカラー
|
|
75
|
+
- ネストもできる
|
|
76
|
+
|
|
77
|
+
::right::
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const app = new Hono()
|
|
81
|
+
!! app.get('/', (c) => c.text('Hello!'))
|
|
82
|
+
```
|
|
83
|
+
````
|
|
84
|
+
|
|
85
|
+
| 記法 | |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `---` | スライドの区切り |
|
|
88
|
+
| `::center::` | 1 枚 1 メッセージ(中央寄せの大きな文字) |
|
|
89
|
+
| `::title::` | 表紙。最後の段落はページ下に寄る |
|
|
90
|
+
| `::wide::` | 左右の余白を詰める(スクリーンショット向け) |
|
|
91
|
+
| `::bg=blue::` | 背景色。`blue` / `orange` / `purple` / `red` / `green` / `black` / `white`、または `#rrggbb` |
|
|
92
|
+
| `::size=1.5::` | 文字サイズの倍率 |
|
|
93
|
+
| `::right::` | ここから右カラム。先頭の見出しはカラムの上に出る |
|
|
94
|
+
| `-# 注釈` | 小さい注釈 |
|
|
95
|
+
| ` ```hcl ` | コード。`hcl` / `console` / `sh` / `json` / `js` / `ts` をハイライト |
|
|
96
|
+
| `!! ` | コードの行頭に付けるとその行を強調 |
|
|
97
|
+
|
|
98
|
+
指示は `::center bg=blue size=2::` のようにまとめて書けます。
|
|
99
|
+
`console` のコードは `$ ` で始まる行をコマンド、`Error` を含む行をエラーとして色分けします。
|
|
100
|
+
|
|
101
|
+
## 発表中の操作
|
|
102
|
+
|
|
103
|
+
| キー | |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `→` / `Space` / 画面右側クリック | 次へ |
|
|
106
|
+
| `←` / 画面左側クリック | 前へ |
|
|
107
|
+
| `Home` / `End` | 最初 / 最後 |
|
|
108
|
+
| `f` | フルスクリーン |
|
|
109
|
+
| `o` / `Esc` | スライド一覧 |
|
|
110
|
+
|
|
111
|
+
表紙から進むと右下でタイマーが動き、表紙に戻るとリセットされます。
|
|
112
|
+
URL の `#12` で 12 枚目から開けます。
|
|
113
|
+
|
|
114
|
+
## 見た目を変える
|
|
115
|
+
|
|
116
|
+
`--css` で CSS 変数を上書きできます。
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
:root {
|
|
120
|
+
--slide-accent: #7b42bc; /* 進捗バー */
|
|
121
|
+
--slide-accent-2: #ff6a00; /* 太字・強調 */
|
|
122
|
+
--slide-display-font: 'Hiragino Sans', sans-serif;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## ライブラリとして使う
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { render, toPdf } from 'ippon-slides'
|
|
130
|
+
|
|
131
|
+
const { html } = render(markdown, { baseDir: '.', outDir: 'dist' })
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
package/assets/deck.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// スライドのナビゲーション。ippon が HTML に埋め込む。
|
|
2
|
+
// → / Space / PageDown / クリック右側 次へ
|
|
3
|
+
// ← / PageUp / クリック左側 前へ
|
|
4
|
+
// Home / End 最初 / 最後
|
|
5
|
+
// f フルスクリーン
|
|
6
|
+
// 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
|
|
37
|
+
}
|
|
38
|
+
if (startedAt === null) startedAt = Date.now()
|
|
39
|
+
if (!ticking) {
|
|
40
|
+
ticking = setInterval(function () {
|
|
41
|
+
timerEl.textContent = fmt(Date.now() - startedAt)
|
|
42
|
+
}, 500)
|
|
43
|
+
}
|
|
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
|
|
141
|
+
}
|
|
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
|
|
170
|
+
}
|
|
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
|
+
})()
|