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/src/cli.js
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
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
|
-
|
|
7
|
-
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
8
|
-
|
|
9
|
-
const HELP = `ippon v${pkg.version} — 1 枚 1 メッセージのスライドを Markdown から作る
|
|
10
|
-
|
|
11
|
-
使い方:
|
|
12
|
-
ippon build <input.md> [options] スライドをビルドする
|
|
13
|
-
ippon <input.md> [options] build の省略形
|
|
14
|
-
ippon init [file] サンプルの Markdown を作る(デフォルト: slide.md)
|
|
15
|
-
|
|
16
|
-
build のオプション:
|
|
17
|
-
--html HTML を出力する(--pdf がなければデフォルト)
|
|
18
|
-
--pdf PDF を出力する(Chrome / Chromium / Edge が必要)
|
|
19
|
-
-o, --output <path> 出力先。--html と --pdf を両方指定したときは拡張子だけ差し替える
|
|
20
|
-
ディレクトリを指定するとその中に <input>.html / .pdf を作る
|
|
21
|
-
-w, --watch Markdown と --css の変更を監視して再ビルドする
|
|
22
|
-
--css <file> 追加の CSS(色やフォントの上書きなど)
|
|
23
|
-
--browser-path <path> PDF に使うブラウザ(環境変数 CHROME_PATH でも可)
|
|
24
|
-
|
|
25
|
-
-h, --help ヘルプ
|
|
26
|
-
-v, --version バージョン
|
|
27
|
-
|
|
28
|
-
例:
|
|
29
|
-
ippon build slide.md --html
|
|
30
|
-
ippon build slide.md --pdf -o dist/
|
|
31
|
-
ippon build slide.md --html --pdf -o dist/talk
|
|
32
|
-
ippon build slide.md --html -w
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------
|
|
36
|
-
// args
|
|
37
|
-
// ---------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
const FLAGS = { '--html': 'html', '--pdf': 'pdf', '-w': 'watch', '--watch': 'watch', '-h': 'help', '--help': 'help', '-v': 'version', '--version': 'version' }
|
|
40
|
-
const VALUES = { '-o': 'output', '--output': 'output', '--css': 'css', '--browser-path': 'browserPath' }
|
|
41
|
-
|
|
42
|
-
export function parseArgs(argv) {
|
|
43
|
-
const opts = { _: [] }
|
|
44
|
-
for (let i = 0; i < argv.length; i++) {
|
|
45
|
-
let a = argv[i]
|
|
46
|
-
let v
|
|
47
|
-
const eq = a.match(/^(--[^=]+)=(.*)$/)
|
|
48
|
-
if (eq) [, a, v] = eq
|
|
49
|
-
if (FLAGS[a]) opts[FLAGS[a]] = true
|
|
50
|
-
else if (VALUES[a]) {
|
|
51
|
-
if (v === undefined) v = argv[++i]
|
|
52
|
-
if (v === undefined) throw new Error(`${a} には値が必要です`)
|
|
53
|
-
opts[VALUES[a]] = v
|
|
54
|
-
} else if (a.startsWith('-') && a !== '-') throw new Error(`不明なオプション: ${a}`)
|
|
55
|
-
else opts._.push(a)
|
|
56
|
-
}
|
|
57
|
-
return opts
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 出力先を決める */
|
|
61
|
-
export function outputs(input, { html, pdf, output }) {
|
|
62
|
-
const formats = []
|
|
63
|
-
if (html || !pdf) formats.push('html')
|
|
64
|
-
if (pdf) formats.push('pdf')
|
|
65
|
-
|
|
66
|
-
const name = basename(input, extname(input))
|
|
67
|
-
let base // 拡張子なしのパス
|
|
68
|
-
let exact = null // 1 形式だけのときに -o をそのまま使う
|
|
69
|
-
|
|
70
|
-
if (!output) 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)) exact = output
|
|
74
|
-
else base = extname(output) ? output.slice(0, -extname(output).length) : output
|
|
75
|
-
|
|
76
|
-
return formats.map((f) => ({ format: f, path: resolve(exact || `${base}.${f}`) }))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---------------------------------------------------------------
|
|
80
|
-
// build
|
|
81
|
-
// ---------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
const time = () => new Date().toLocaleTimeString()
|
|
84
|
-
|
|
85
|
-
async function build(input, opts) {
|
|
86
|
-
const md = readFileSync(input, 'utf8')
|
|
87
|
-
const css = opts.css ? readFileSync(opts.css, 'utf8') : ''
|
|
88
|
-
const baseDir = dirname(resolve(input))
|
|
89
|
-
|
|
90
|
-
for (const { format, path } of outputs(input, opts)) {
|
|
91
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
92
|
-
if (format === 'html') {
|
|
93
|
-
const { html, count } = render(md, { baseDir, outDir: dirname(path), css })
|
|
94
|
-
writeFileSync(path, html)
|
|
95
|
-
console.log(`${time()} ✓ ${count} slides → ${path}`)
|
|
96
|
-
} else {
|
|
97
|
-
const { html, count } = render(md, { baseDir, css })
|
|
98
|
-
await toPdf(html, path, { browserPath: opts.browserPath })
|
|
99
|
-
console.log(`${time()} ✓ ${count} slides → ${path}`)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function watchBuild(input, opts) {
|
|
105
|
-
let timer = null
|
|
106
|
-
let running = Promise.resolve()
|
|
107
|
-
const rebuild = () => {
|
|
108
|
-
clearTimeout(timer)
|
|
109
|
-
timer = setTimeout(() => {
|
|
110
|
-
running = running.then(() => build(input, opts)).catch((e) => console.error(`${time()} ✗ ${e.message}`))
|
|
111
|
-
}, 80)
|
|
112
|
-
}
|
|
113
|
-
// エディタによってはファイルを置き換えて保存するので、ディレクトリごと監視する
|
|
114
|
-
const files = [input, opts.css].filter(Boolean).map((f) => resolve(f))
|
|
115
|
-
for (const dir of new Set(files.map(dirname))) {
|
|
116
|
-
watch(dir, (_, file) => {
|
|
117
|
-
if (file && files.includes(join(dir, file.toString()))) rebuild()
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
console.log(`watching ${files.join(', ')} ...`)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------
|
|
124
|
-
// init
|
|
125
|
-
// ---------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
const SAMPLE = `---
|
|
128
|
-
title: はじめての ippon
|
|
129
|
-
author: あなたの名前
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
::title::
|
|
133
|
-
|
|
134
|
-
# はじめての ippon
|
|
135
|
-
|
|
136
|
-
サブタイトル
|
|
137
|
-
|
|
138
|
-
あなたの名前 ・ 2026-01-01
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
::center::
|
|
143
|
-
|
|
144
|
-
1 枚に
|
|
145
|
-
|
|
146
|
-
1 メッセージ
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
::center size=2::
|
|
151
|
-
|
|
152
|
-
**大きく**
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
::center bg=blue::
|
|
157
|
-
|
|
158
|
-
背景色も変えられる
|
|
159
|
-
|
|
160
|
-
-# blue / orange / purple / red / green / black / white、または #rrggbb
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
::center size=1.5 bg=white::
|
|
165
|
-
|
|
166
|
-
セクション
|
|
167
|
-
|
|
168
|
-
---
|
|
169
|
-
|
|
170
|
-
## 箇条書き
|
|
171
|
-
|
|
172
|
-
- **太字** はアクセントカラーになる
|
|
173
|
-
- \`inline code\` も書ける
|
|
174
|
-
- ネストもできる
|
|
175
|
-
- 矢印 → は少し薄くなる
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## 2 カラム
|
|
180
|
-
|
|
181
|
-
- 左に説明
|
|
182
|
-
- 右にコード
|
|
183
|
-
|
|
184
|
-
::right::
|
|
185
|
-
|
|
186
|
-
\`\`\`ts
|
|
187
|
-
const app = new Hono()
|
|
188
|
-
!! app.get('/', (c) => c.text('Hello!'))
|
|
189
|
-
\`\`\`
|
|
190
|
-
|
|
191
|
-
-# 行頭に "!! " を付けるとその行が強調される
|
|
192
|
-
|
|
193
|
-
---
|
|
194
|
-
|
|
195
|
-
\`\`\`console
|
|
196
|
-
$ npx ippon-slides build slide.md --html --pdf
|
|
197
|
-
✓ 9 slides → slide.html
|
|
198
|
-
✓ 9 slides → slide.pdf
|
|
199
|
-
\`\`\`
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
::title::
|
|
204
|
-
|
|
205
|
-
# ありがとうございました
|
|
206
|
-
|
|
207
|
-
あなたの名前
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
function init(file = 'slide.md') {
|
|
211
|
-
if (existsSync(file)) throw new Error(`${file} はすでにあります`)
|
|
212
|
-
writeFileSync(file, SAMPLE)
|
|
213
|
-
console.log(`✓ ${file} を作りました → ippon build ${file} --html -w`)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ---------------------------------------------------------------
|
|
217
|
-
// main
|
|
218
|
-
// ---------------------------------------------------------------
|
|
219
|
-
|
|
220
|
-
export async function main(argv) {
|
|
221
|
-
const opts = parseArgs(argv)
|
|
222
|
-
if (opts.version) return console.log(pkg.version)
|
|
223
|
-
if (opts.help || opts._.length === 0) return console.log(HELP)
|
|
224
|
-
|
|
225
|
-
let [cmd, ...rest] = opts._
|
|
226
|
-
if (cmd === 'init') return init(rest[0])
|
|
227
|
-
if (cmd !== 'build') rest = [cmd, ...rest] // `ippon slide.md` は build の省略形
|
|
228
|
-
|
|
229
|
-
const input = rest[0]
|
|
230
|
-
if (!input) throw new Error('入力の Markdown を指定してください(例: ippon build slide.md --html)')
|
|
231
|
-
if (rest.length > 1) throw new Error(`入力は 1 つだけ指定してください: ${rest.join(' ')}`)
|
|
232
|
-
if (!existsSync(input)) throw new Error(`${input} が見つかりません`)
|
|
233
|
-
|
|
234
|
-
await build(input, opts)
|
|
235
|
-
if (opts.watch) watchBuild(input, opts)
|
|
236
|
-
}
|
package/src/highlight.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
// コードのハイライト。1 行ずつ処理する(複数行にまたがるトークンは扱わない)。
|
|
2
|
-
// 行頭の "!! " はその行の強調マーカー。
|
|
3
|
-
|
|
4
|
-
export const esc = (s) =>
|
|
5
|
-
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
6
|
-
|
|
7
|
-
function tokenize(line, rules) {
|
|
8
|
-
let out = ''
|
|
9
|
-
let rest = line
|
|
10
|
-
outer: while (rest.length) {
|
|
11
|
-
for (const [re, cls] of rules) {
|
|
12
|
-
const m = rest.match(re)
|
|
13
|
-
if (m && m[0].length) {
|
|
14
|
-
out += cls ? `<span class="${cls}">${esc(m[0])}</span>` : esc(m[0])
|
|
15
|
-
rest = rest.slice(m[0].length)
|
|
16
|
-
continue outer
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
out += esc(rest[0])
|
|
20
|
-
rest = rest.slice(1)
|
|
21
|
-
}
|
|
22
|
-
return out
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const HCL_KEYWORDS =
|
|
26
|
-
'resource|data|variable|module|output|locals|terraform|provisioner|lifecycle|precondition|postcondition|backend|for|in|if|for_each|count|depends_on|when'
|
|
27
|
-
|
|
28
|
-
const HCL = [
|
|
29
|
-
[/^(#|\/\/).*/, 'hl-c'],
|
|
30
|
-
[/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
|
|
31
|
-
[/^<<-?EOT/, 'hl-s'],
|
|
32
|
-
[/^(?:var|local|module|each|self|path|data)(?=\.)/, 'hl-i'],
|
|
33
|
-
[new RegExp(`^(?:${HCL_KEYWORDS})\\b`), 'hl-k'],
|
|
34
|
-
[/^(?:true|false|null)\b/, 'hl-n'],
|
|
35
|
-
[/^\d+(\.\d+)?\b/, 'hl-n'],
|
|
36
|
-
[/^[a-z_][a-z0-9_]*(?=\()/, 'hl-f'],
|
|
37
|
-
[/^[a-z_][a-z0-9_]*(?=\s*=[^=>])/, 'hl-t'],
|
|
38
|
-
[/^[A-Za-z_][A-Za-z0-9_]*/, null],
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
const SH = [
|
|
42
|
-
[/^#.*/, 'hl-c'],
|
|
43
|
-
[/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
|
|
44
|
-
[/^'[^']*'/, 'hl-s'],
|
|
45
|
-
[/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/, 'hl-i'],
|
|
46
|
-
[/^(?:if|then|else|elif|fi|for|while|do|done|case|esac|set|echo|exit|export)\b/, 'hl-k'],
|
|
47
|
-
[/^--?[a-z][a-z-]*/, 'hl-t'],
|
|
48
|
-
[/^[A-Za-z_][A-Za-z0-9_]*/, null],
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
const JSON_ = [
|
|
52
|
-
[/^"(?:[^"\\]|\\.)*"(?=\s*:)/, 'hl-t'],
|
|
53
|
-
[/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
|
|
54
|
-
[/^(?:true|false|null)\b/, 'hl-n'],
|
|
55
|
-
[/^-?\d+(\.\d+)?/, 'hl-n'],
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
const JS_KEYWORDS =
|
|
59
|
-
'const|let|var|function|return|if|else|for|while|of|in|new|class|extends|import|from|export|default|async|await|try|catch|throw|type|interface|as'
|
|
60
|
-
|
|
61
|
-
const JS = [
|
|
62
|
-
[/^\/\/.*/, 'hl-c'],
|
|
63
|
-
[/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
|
|
64
|
-
[/^'(?:[^'\\]|\\.)*'/, 'hl-s'],
|
|
65
|
-
[/^`(?:[^`\\]|\\.)*`/, 'hl-s'],
|
|
66
|
-
[new RegExp(`^(?:${JS_KEYWORDS})\\b`), 'hl-k'],
|
|
67
|
-
[/^(?:true|false|null|undefined)\b/, 'hl-n'],
|
|
68
|
-
[/^\d+(\.\d+)?\b/, 'hl-n'],
|
|
69
|
-
[/^[A-Za-z_$][\w$]*(?=\()/, 'hl-f'],
|
|
70
|
-
[/^[A-Z][\w$]*/, 'hl-t'],
|
|
71
|
-
[/^[A-Za-z_$][\w$]*/, null],
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
const LANGS = {
|
|
75
|
-
hcl: HCL,
|
|
76
|
-
tf: HCL,
|
|
77
|
-
terraform: HCL,
|
|
78
|
-
sh: SH,
|
|
79
|
-
bash: SH,
|
|
80
|
-
shell: SH,
|
|
81
|
-
zsh: SH,
|
|
82
|
-
json: JSON_,
|
|
83
|
-
js: JS,
|
|
84
|
-
javascript: JS,
|
|
85
|
-
ts: JS,
|
|
86
|
-
typescript: JS,
|
|
87
|
-
jsx: JS,
|
|
88
|
-
tsx: JS,
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function consoleLine(line) {
|
|
92
|
-
const m = line.match(/^(\$ )(.*)$/)
|
|
93
|
-
if (m) return `<span class="hl-prompt">$ </span><span class="hl-cmd">${esc(m[2])}</span>`
|
|
94
|
-
if (/(^|\s)(Error|panic)\b|^make: \*\*\*/.test(line)) return `<span class="hl-err">${esc(line)}</span>`
|
|
95
|
-
if (/^(✅|Apply complete|Destroy complete)/.test(line)) return `<span class="hl-ok">${esc(line)}</span>`
|
|
96
|
-
return `<span class="hl-out">${esc(line)}</span>`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function highlight(code, lang = '') {
|
|
100
|
-
const rules = LANGS[lang.toLowerCase()]
|
|
101
|
-
return code
|
|
102
|
-
.split('\n')
|
|
103
|
-
.map((raw) => {
|
|
104
|
-
const marked = raw.startsWith('!! ')
|
|
105
|
-
const line = marked ? raw.slice(3) : raw
|
|
106
|
-
let html
|
|
107
|
-
if (lang === 'console') html = consoleLine(line)
|
|
108
|
-
else if (rules) html = tokenize(line, rules)
|
|
109
|
-
else html = esc(line)
|
|
110
|
-
return marked ? `<span class="hl-mark">${html}</span>` : html
|
|
111
|
-
})
|
|
112
|
-
.join('\n')
|
|
113
|
-
}
|
package/src/index.js
DELETED
package/src/markdown.js
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
// Markdown → スライドの <section> 群
|
|
2
|
-
//
|
|
3
|
-
// --- スライドの区切り
|
|
4
|
-
// ::center:: 1 行目に置くとスライドの指示(center / title / wide / bg=xxx / size=1.5)
|
|
5
|
-
// ::right:: ここから右カラム
|
|
6
|
-
// -# 注釈 小さい注釈
|
|
7
|
-
// ```hcl コード(hcl / console / sh / json / js / ts)
|
|
8
|
-
// !! 行 コードの行を強調(行頭の "!! " は消える)
|
|
9
|
-
|
|
10
|
-
import { esc, highlight } from './highlight.js'
|
|
11
|
-
|
|
12
|
-
export const BG = {
|
|
13
|
-
blue: '#0d76bc',
|
|
14
|
-
orange: '#ff6a00',
|
|
15
|
-
purple: '#5c2d91',
|
|
16
|
-
red: '#c0392b',
|
|
17
|
-
green: '#188038',
|
|
18
|
-
black: '#000000',
|
|
19
|
-
white: '#fafafa',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const DIRECTIVES = /^(center|title|wide|bg=.+|size=.+)$/
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------
|
|
25
|
-
// inline
|
|
26
|
-
// ---------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
export function inline(text) {
|
|
29
|
-
const codes = []
|
|
30
|
-
let s = text.replace(/`([^`]+)`/g, (_, c) => {
|
|
31
|
-
codes.push(`<code>${esc(c)}</code>`)
|
|
32
|
-
return `\u0000${codes.length - 1}\u0000`
|
|
33
|
-
})
|
|
34
|
-
s = esc(s)
|
|
35
|
-
s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^&]*)")?\)/g, (_, alt, src, title) =>
|
|
36
|
-
`<img src="${src}" alt="${alt}">` + (title ? `<span class="img-caption">${title}</span>` : '')
|
|
37
|
-
)
|
|
38
|
-
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
|
39
|
-
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
40
|
-
s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*/g, '$1<em>$2</em>')
|
|
41
|
-
s = s.replace(/→/g, '<span class="arrow">→</span>')
|
|
42
|
-
return s.replace(/\u0000(\d+)\u0000/g, (_, i) => codes[i])
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------
|
|
46
|
-
// block
|
|
47
|
-
// ---------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
function list(items) {
|
|
50
|
-
const base = items[0].indent
|
|
51
|
-
const tag = items[0].ordered ? 'ol' : 'ul'
|
|
52
|
-
let html = `<${tag}>`
|
|
53
|
-
let i = 0
|
|
54
|
-
while (i < items.length) {
|
|
55
|
-
const item = items[i++]
|
|
56
|
-
const children = []
|
|
57
|
-
while (i < items.length && items[i].indent > base) children.push(items[i++])
|
|
58
|
-
html += `<li>${inline(item.text)}${children.length ? list(children) : ''}</li>`
|
|
59
|
-
}
|
|
60
|
-
return html + `</${tag}>`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function table(rows) {
|
|
64
|
-
// `\|` はセルの区切りではなく文字としての |
|
|
65
|
-
const cells = (r) =>
|
|
66
|
-
r
|
|
67
|
-
.trim()
|
|
68
|
-
.replace(/^\||\|$/g, '')
|
|
69
|
-
.split(/(?<!\\)\|/)
|
|
70
|
-
.map((c) => c.trim().replace(/\\\|/g, '|'))
|
|
71
|
-
const head = cells(rows[0])
|
|
72
|
-
const align = cells(rows[1]).map((c) => (/:$/.test(c) ? ' class="num"' : ''))
|
|
73
|
-
let html = '<table><thead><tr>'
|
|
74
|
-
head.forEach((c, i) => (html += `<th${align[i] || ''}>${inline(c)}</th>`))
|
|
75
|
-
html += '</tr></thead><tbody>'
|
|
76
|
-
for (const r of rows.slice(2)) {
|
|
77
|
-
html += '<tr>'
|
|
78
|
-
cells(r).forEach((c, i) => (html += `<td${align[i] || ''}>${inline(c)}</td>`))
|
|
79
|
-
html += '</tr>'
|
|
80
|
-
}
|
|
81
|
-
return html + '</tbody></table>'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const LIST_ITEM = /^(\s*)([-*]|\d+\.)\s+(.*)$/
|
|
85
|
-
|
|
86
|
-
export function blocks(src) {
|
|
87
|
-
const lines = src.split('\n')
|
|
88
|
-
let html = ''
|
|
89
|
-
let para = []
|
|
90
|
-
|
|
91
|
-
const flush = () => {
|
|
92
|
-
if (!para.length) return
|
|
93
|
-
const text = para.join('\n')
|
|
94
|
-
para = []
|
|
95
|
-
if (text.startsWith('-# ')) html += `<p class="note">${inline(text.slice(3)).replace(/\n/g, '<br>')}</p>`
|
|
96
|
-
else html += `<p>${inline(text).replace(/\n/g, '<br>')}</p>`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
for (let i = 0; i < lines.length; i++) {
|
|
100
|
-
const line = lines[i]
|
|
101
|
-
|
|
102
|
-
const fence = line.match(/^```([\w-]*)\s*$/)
|
|
103
|
-
if (fence) {
|
|
104
|
-
flush()
|
|
105
|
-
const lang = fence[1]
|
|
106
|
-
const body = []
|
|
107
|
-
while (++i < lines.length && !/^```\s*$/.test(lines[i])) body.push(lines[i])
|
|
108
|
-
const attr = lang && lang !== 'console' ? ` data-lang="${esc(lang)}"` : ''
|
|
109
|
-
html += `<pre${attr}><code>${highlight(body.join('\n'), lang)}</code></pre>`
|
|
110
|
-
continue
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!line.trim()) {
|
|
114
|
-
flush()
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const h = line.match(/^(#{1,3})\s+(.*)$/)
|
|
119
|
-
if (h) {
|
|
120
|
-
flush()
|
|
121
|
-
html += `<h${h[1].length}>${inline(h[2])}</h${h[1].length}>`
|
|
122
|
-
continue
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (LIST_ITEM.test(line) && !line.startsWith('-# ')) {
|
|
126
|
-
flush()
|
|
127
|
-
const items = []
|
|
128
|
-
while (i < lines.length && LIST_ITEM.test(lines[i])) {
|
|
129
|
-
const m = lines[i].match(LIST_ITEM)
|
|
130
|
-
items.push({ indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] })
|
|
131
|
-
i++
|
|
132
|
-
}
|
|
133
|
-
i--
|
|
134
|
-
html += list(items)
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (line.startsWith('|') && /^\|?\s*:?-+/.test(lines[i + 1] || '')) {
|
|
139
|
-
flush()
|
|
140
|
-
const rows = []
|
|
141
|
-
while (i < lines.length && lines[i].startsWith('|')) rows.push(lines[i++])
|
|
142
|
-
i--
|
|
143
|
-
html += table(rows)
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (line.startsWith('> ')) {
|
|
148
|
-
flush()
|
|
149
|
-
const q = []
|
|
150
|
-
while (i < lines.length && lines[i].startsWith('> ')) q.push(lines[i++].slice(2))
|
|
151
|
-
i--
|
|
152
|
-
html += `<blockquote>${blocks(q.join('\n'))}</blockquote>`
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
para.push(line)
|
|
157
|
-
}
|
|
158
|
-
flush()
|
|
159
|
-
return html
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------
|
|
163
|
-
// slide
|
|
164
|
-
// ---------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
export function splitSlides(md) {
|
|
167
|
-
const slides = []
|
|
168
|
-
let cur = []
|
|
169
|
-
let inFence = false
|
|
170
|
-
for (const line of md.split('\n')) {
|
|
171
|
-
if (/^```/.test(line)) inFence = !inFence
|
|
172
|
-
if (!inFence && /^---\s*$/.test(line)) {
|
|
173
|
-
slides.push(cur.join('\n'))
|
|
174
|
-
cur = []
|
|
175
|
-
} else cur.push(line)
|
|
176
|
-
}
|
|
177
|
-
slides.push(cur.join('\n'))
|
|
178
|
-
return slides.filter((s) => s.trim())
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function slide(src) {
|
|
182
|
-
const classes = ['slide']
|
|
183
|
-
let style = ''
|
|
184
|
-
let body = src.replace(/^\s+/, '')
|
|
185
|
-
|
|
186
|
-
const d = body.match(/^::([^:]+)::[ \t]*(\n|$)/)
|
|
187
|
-
const tokens = d ? d[1].trim().split(/\s+/) : []
|
|
188
|
-
if (d && tokens.every((t) => DIRECTIVES.test(t))) {
|
|
189
|
-
for (const tok of tokens) {
|
|
190
|
-
const bg = tok.match(/^bg=(.+)$/)
|
|
191
|
-
const size = tok.match(/^size=(.+)$/)
|
|
192
|
-
if (bg) {
|
|
193
|
-
style += `background:${esc(BG[bg[1]] || bg[1])};`
|
|
194
|
-
if (bg[1] === 'white') classes.push('light')
|
|
195
|
-
} else if (size) style += `--slide-size:${esc(size[1])};`
|
|
196
|
-
else classes.push(tok)
|
|
197
|
-
}
|
|
198
|
-
body = body.slice(d[0].length)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let html
|
|
202
|
-
const parts = body.split(/^::right::[ \t]*$/m)
|
|
203
|
-
if (parts.length === 2) {
|
|
204
|
-
// 先頭の見出しはカラムの外(上)に出す
|
|
205
|
-
const head = parts[0].match(/^\s*(#{1,3} .*)\n/)
|
|
206
|
-
const left = head ? parts[0].replace(head[0], '') : parts[0]
|
|
207
|
-
html =
|
|
208
|
-
(head ? blocks(head[1]) : '') +
|
|
209
|
-
`<div class="cols"><div>${blocks(left)}</div><div>${blocks(parts[1])}</div></div>`
|
|
210
|
-
} else {
|
|
211
|
-
html = blocks(body)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return `<section class="${classes.join(' ')}"${style ? ` style="${style}"` : ''}>${html}</section>`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function frontmatter(md) {
|
|
218
|
-
const m = md.replace(/^/, '').match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
|
|
219
|
-
if (!m) return [{}, md]
|
|
220
|
-
const meta = {}
|
|
221
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
222
|
-
const kv = line.match(/^(\w+):\s*(.*?)\s*$/)
|
|
223
|
-
if (kv) meta[kv[1]] = kv[2].replace(/^(["'])(.*)\1$/, '$2')
|
|
224
|
-
}
|
|
225
|
-
return [meta, md.slice(m[0].length)]
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Markdown 全体 → { meta, sections } */
|
|
229
|
-
export function parse(md) {
|
|
230
|
-
const [meta, body] = frontmatter(md.replace(/\r\n/g, '\n'))
|
|
231
|
-
return { meta, sections: splitSlides(body).map(slide) }
|
|
232
|
-
}
|
package/src/pdf.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// PDF 出力。ローカルの Chrome / Chromium / Edge をヘッドレスで起動して印刷する。
|
|
2
|
-
// 依存パッケージなし(puppeteer も使わない)。
|
|
3
|
-
|
|
4
|
-
import { spawn } from 'node:child_process'
|
|
5
|
-
import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
6
|
-
import { tmpdir } from 'node:os'
|
|
7
|
-
import { delimiter, join } from 'node:path'
|
|
8
|
-
import { pathToFileURL } from 'node:url'
|
|
9
|
-
|
|
10
|
-
const CANDIDATES = {
|
|
11
|
-
darwin: [
|
|
12
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
13
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
14
|
-
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
15
|
-
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
16
|
-
],
|
|
17
|
-
linux: ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser', 'microsoft-edge', 'brave-browser'],
|
|
18
|
-
win32: [
|
|
19
|
-
join(process.env['PROGRAMFILES'] || 'C:\\Program Files', 'Google\\Chrome\\Application\\chrome.exe'),
|
|
20
|
-
join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google\\Chrome\\Application\\chrome.exe'),
|
|
21
|
-
join(process.env['LOCALAPPDATA'] || '', 'Google\\Chrome\\Application\\chrome.exe'),
|
|
22
|
-
join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Microsoft\\Edge\\Application\\msedge.exe'),
|
|
23
|
-
],
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function which(cmd) {
|
|
27
|
-
for (const dir of (process.env.PATH || '').split(delimiter)) {
|
|
28
|
-
const p = join(dir, cmd)
|
|
29
|
-
if (existsSync(p)) return p
|
|
30
|
-
}
|
|
31
|
-
return null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** --browser-path → CHROME_PATH → よくある場所 の順に探す */
|
|
35
|
-
export function findBrowser(explicit) {
|
|
36
|
-
if (explicit) return existsSync(explicit) ? explicit : null
|
|
37
|
-
if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH
|
|
38
|
-
for (const c of CANDIDATES[process.platform] || []) {
|
|
39
|
-
const p = c.includes('/') || c.includes('\\') ? (existsSync(c) ? c : null) : which(c)
|
|
40
|
-
if (p) return p
|
|
41
|
-
}
|
|
42
|
-
return null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {string} html file:// URL の画像で組み立てた HTML
|
|
47
|
-
* @param {string} out 出力する PDF のパス
|
|
48
|
-
* @param {object} opts
|
|
49
|
-
* @param {string} [opts.browserPath]
|
|
50
|
-
*/
|
|
51
|
-
export async function toPdf(html, out, { browserPath } = {}) {
|
|
52
|
-
const browser = findBrowser(browserPath)
|
|
53
|
-
if (!browser) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
'PDF の出力には Chrome / Chromium / Edge が必要です。--browser-path か環境変数 CHROME_PATH で場所を指定してください。'
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const dir = mkdtempSync(join(tmpdir(), 'ippon-'))
|
|
60
|
-
const src = join(dir, 'slide.html')
|
|
61
|
-
writeFileSync(src, html)
|
|
62
|
-
rmSync(out, { force: true })
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const args = [
|
|
66
|
-
'--headless=new',
|
|
67
|
-
'--disable-gpu',
|
|
68
|
-
'--no-first-run',
|
|
69
|
-
'--no-default-browser-check',
|
|
70
|
-
`--user-data-dir=${join(dir, 'profile')}`,
|
|
71
|
-
'--no-pdf-header-footer',
|
|
72
|
-
'--virtual-time-budget=3000', // Web フォントの読み込みを待つ
|
|
73
|
-
`--print-to-pdf=${out}`,
|
|
74
|
-
pathToFileURL(src).href,
|
|
75
|
-
]
|
|
76
|
-
const stderr = await new Promise((ok, ng) => {
|
|
77
|
-
const p = spawn(browser, args, { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
78
|
-
let err = ''
|
|
79
|
-
p.stderr.on('data', (d) => (err += d))
|
|
80
|
-
p.on('error', ng)
|
|
81
|
-
p.on('close', () => ok(err))
|
|
82
|
-
})
|
|
83
|
-
if (!existsSync(out) || statSync(out).size === 0) {
|
|
84
|
-
throw new Error(`PDF の出力に失敗しました(${browser})\n${stderr.trim()}`)
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
rmSync(dir, { recursive: true, force: true })
|
|
88
|
-
}
|
|
89
|
-
}
|