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.
@@ -0,0 +1,105 @@
1
+ // コードのハイライト。1 行ずつ処理する(複数行にまたがるトークンは扱わない)。
2
+ // 行頭の "!! " はその行の強調マーカー。
3
+ export const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4
+ function tokenize(line, rules) {
5
+ let out = '';
6
+ let rest = line;
7
+ outer: while (rest.length) {
8
+ for (const [re, cls] of rules) {
9
+ const m = rest.match(re);
10
+ if (m && m[0].length) {
11
+ out += cls ? `<span class="${cls}">${esc(m[0])}</span>` : esc(m[0]);
12
+ rest = rest.slice(m[0].length);
13
+ continue outer;
14
+ }
15
+ }
16
+ out += esc(rest[0]);
17
+ rest = rest.slice(1);
18
+ }
19
+ return out;
20
+ }
21
+ const HCL_KEYWORDS = 'resource|data|variable|module|output|locals|terraform|provisioner|lifecycle|precondition|postcondition|backend|for|in|if|for_each|count|depends_on|when';
22
+ const HCL = [
23
+ [/^(#|\/\/).*/, 'hl-c'],
24
+ [/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
25
+ [/^<<-?EOT/, 'hl-s'],
26
+ [/^(?:var|local|module|each|self|path|data)(?=\.)/, 'hl-i'],
27
+ [new RegExp(`^(?:${HCL_KEYWORDS})\\b`), 'hl-k'],
28
+ [/^(?:true|false|null)\b/, 'hl-n'],
29
+ [/^\d+(\.\d+)?\b/, 'hl-n'],
30
+ [/^[a-z_][a-z0-9_]*(?=\()/, 'hl-f'],
31
+ [/^[a-z_][a-z0-9_]*(?=\s*=[^=>])/, 'hl-t'],
32
+ [/^[A-Za-z_][A-Za-z0-9_]*/, null],
33
+ ];
34
+ const SH = [
35
+ [/^#.*/, 'hl-c'],
36
+ [/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
37
+ [/^'[^']*'/, 'hl-s'],
38
+ [/^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/, 'hl-i'],
39
+ [/^(?:if|then|else|elif|fi|for|while|do|done|case|esac|set|echo|exit|export)\b/, 'hl-k'],
40
+ [/^--?[a-z][a-z-]*/, 'hl-t'],
41
+ [/^[A-Za-z_][A-Za-z0-9_]*/, null],
42
+ ];
43
+ const JSON_ = [
44
+ [/^"(?:[^"\\]|\\.)*"(?=\s*:)/, 'hl-t'],
45
+ [/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
46
+ [/^(?:true|false|null)\b/, 'hl-n'],
47
+ [/^-?\d+(\.\d+)?/, 'hl-n'],
48
+ ];
49
+ const JS_KEYWORDS = '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';
50
+ const JS = [
51
+ [/^\/\/.*/, 'hl-c'],
52
+ [/^"(?:[^"\\]|\\.)*"/, 'hl-s'],
53
+ [/^'(?:[^'\\]|\\.)*'/, 'hl-s'],
54
+ [/^`(?:[^`\\]|\\.)*`/, 'hl-s'],
55
+ [new RegExp(`^(?:${JS_KEYWORDS})\\b`), 'hl-k'],
56
+ [/^(?:true|false|null|undefined)\b/, 'hl-n'],
57
+ [/^\d+(\.\d+)?\b/, 'hl-n'],
58
+ [/^[A-Za-z_$][\w$]*(?=\()/, 'hl-f'],
59
+ [/^[A-Z][\w$]*/, 'hl-t'],
60
+ [/^[A-Za-z_$][\w$]*/, null],
61
+ ];
62
+ const LANGS = {
63
+ hcl: HCL,
64
+ tf: HCL,
65
+ terraform: HCL,
66
+ sh: SH,
67
+ bash: SH,
68
+ shell: SH,
69
+ zsh: SH,
70
+ json: JSON_,
71
+ js: JS,
72
+ javascript: JS,
73
+ ts: JS,
74
+ typescript: JS,
75
+ jsx: JS,
76
+ tsx: JS,
77
+ };
78
+ function consoleLine(line) {
79
+ const m = line.match(/^(\$ )(.*)$/);
80
+ if (m)
81
+ return `<span class="hl-prompt">$ </span><span class="hl-cmd">${esc(m[2])}</span>`;
82
+ if (/(^|\s)(Error|panic)\b|^make: \*\*\*/.test(line))
83
+ return `<span class="hl-err">${esc(line)}</span>`;
84
+ if (/^(✅|Apply complete|Destroy complete)/.test(line))
85
+ return `<span class="hl-ok">${esc(line)}</span>`;
86
+ return `<span class="hl-out">${esc(line)}</span>`;
87
+ }
88
+ export function highlight(code, lang = '') {
89
+ const rules = LANGS[lang.toLowerCase()];
90
+ return code
91
+ .split('\n')
92
+ .map((raw) => {
93
+ const marked = raw.startsWith('!! ');
94
+ const line = marked ? raw.slice(3) : raw;
95
+ let html;
96
+ if (lang === 'console')
97
+ html = consoleLine(line);
98
+ else if (rules)
99
+ html = tokenize(line, rules);
100
+ else
101
+ html = esc(line);
102
+ return marked ? `<span class="hl-mark">${html}</span>` : html;
103
+ })
104
+ .join('\n');
105
+ }
@@ -0,0 +1,3 @@
1
+ export { parse, type Meta, type Parsed } from './markdown.ts';
2
+ export { render, type RenderOptions, type RenderResult } from './render.ts';
3
+ export { toPdf, findBrowser, type PdfOptions } from './pdf.ts';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { parse } from './markdown.js';
2
+ export { render } from './render.js';
3
+ export { toPdf, findBrowser } from './pdf.js';
@@ -0,0 +1,15 @@
1
+ export declare const BG: Record<string, string>;
2
+ /** frontmatter(`key: value` だけの簡易 YAML) */
3
+ export type Meta = Record<string, string>;
4
+ export interface Parsed {
5
+ meta: Meta;
6
+ /** スライドごとの `<section>` */
7
+ sections: string[];
8
+ }
9
+ export declare function inline(text: string): string;
10
+ export declare function blocks(src: string): string;
11
+ export declare function splitSlides(md: string): string[];
12
+ export declare function slide(src: string): string;
13
+ export declare function frontmatter(md: string): [Meta, string];
14
+ /** Markdown 全体 → { meta, sections } */
15
+ export declare function parse(md: string): Parsed;
@@ -0,0 +1,219 @@
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
+ import { esc, highlight } from './highlight.js';
10
+ export const BG = {
11
+ blue: '#0d76bc',
12
+ orange: '#ff6a00',
13
+ purple: '#5c2d91',
14
+ red: '#c0392b',
15
+ green: '#188038',
16
+ black: '#000000',
17
+ white: '#fafafa',
18
+ };
19
+ const DIRECTIVES = /^(center|title|wide|bg=.+|size=.+)$/;
20
+ // ---------------------------------------------------------------
21
+ // inline
22
+ // ---------------------------------------------------------------
23
+ export function inline(text) {
24
+ const codes = [];
25
+ let s = text.replace(/`([^`]+)`/g, (_, c) => {
26
+ codes.push(`<code>${esc(c)}</code>`);
27
+ return `\u0000${codes.length - 1}\u0000`;
28
+ });
29
+ s = esc(s);
30
+ s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+&quot;([^&]*)&quot;)?\)/g, (_, alt, src, title) => `<img src="${src}" alt="${alt}">` + (title ? `<span class="img-caption">${title}</span>` : ''));
31
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
32
+ s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
33
+ s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*/g, '$1<em>$2</em>');
34
+ s = s.replace(/→/g, '<span class="arrow">→</span>');
35
+ return s.replace(/\u0000(\d+)\u0000/g, (_, i) => codes[Number(i)]);
36
+ }
37
+ function list(items) {
38
+ const base = items[0].indent;
39
+ const tag = items[0].ordered ? 'ol' : 'ul';
40
+ let html = `<${tag}>`;
41
+ let i = 0;
42
+ while (i < items.length) {
43
+ const item = items[i++];
44
+ const children = [];
45
+ while (i < items.length && items[i].indent > base)
46
+ children.push(items[i++]);
47
+ html += `<li>${inline(item.text)}${children.length ? list(children) : ''}</li>`;
48
+ }
49
+ return html + `</${tag}>`;
50
+ }
51
+ function table(rows) {
52
+ // `\|` はセルの区切りではなく文字としての |
53
+ const cells = (r) => r
54
+ .trim()
55
+ .replace(/^\||\|$/g, '')
56
+ .split(/(?<!\\)\|/)
57
+ .map((c) => c.trim().replace(/\\\|/g, '|'));
58
+ const head = cells(rows[0]);
59
+ const align = cells(rows[1]).map((c) => (/:$/.test(c) ? ' class="num"' : ''));
60
+ let html = '<table><thead><tr>';
61
+ head.forEach((c, i) => (html += `<th${align[i] || ''}>${inline(c)}</th>`));
62
+ html += '</tr></thead><tbody>';
63
+ for (const r of rows.slice(2)) {
64
+ html += '<tr>';
65
+ cells(r).forEach((c, i) => (html += `<td${align[i] || ''}>${inline(c)}</td>`));
66
+ html += '</tr>';
67
+ }
68
+ return html + '</tbody></table>';
69
+ }
70
+ const LIST_ITEM = /^(\s*)([-*]|\d+\.)\s+(.*)$/;
71
+ export function blocks(src) {
72
+ const lines = src.split('\n');
73
+ let html = '';
74
+ let para = [];
75
+ const flush = () => {
76
+ if (!para.length)
77
+ return;
78
+ const text = para.join('\n');
79
+ para = [];
80
+ if (text.startsWith('-# '))
81
+ html += `<p class="note">${inline(text.slice(3)).replace(/\n/g, '<br>')}</p>`;
82
+ else
83
+ html += `<p>${inline(text).replace(/\n/g, '<br>')}</p>`;
84
+ };
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i];
87
+ const fence = line.match(/^```([\w-]*)\s*$/);
88
+ if (fence) {
89
+ flush();
90
+ const lang = fence[1];
91
+ const body = [];
92
+ while (++i < lines.length && !/^```\s*$/.test(lines[i]))
93
+ body.push(lines[i]);
94
+ const attr = lang && lang !== 'console' ? ` data-lang="${esc(lang)}"` : '';
95
+ html += `<pre${attr}><code>${highlight(body.join('\n'), lang)}</code></pre>`;
96
+ continue;
97
+ }
98
+ if (!line.trim()) {
99
+ flush();
100
+ continue;
101
+ }
102
+ const h = line.match(/^(#{1,3})\s+(.*)$/);
103
+ if (h) {
104
+ flush();
105
+ const level = h[1].length;
106
+ html += `<h${level}>${inline(h[2])}</h${level}>`;
107
+ continue;
108
+ }
109
+ if (LIST_ITEM.test(line) && !line.startsWith('-# ')) {
110
+ flush();
111
+ const items = [];
112
+ let m;
113
+ while (i < lines.length && (m = lines[i].match(LIST_ITEM))) {
114
+ items.push({ indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] });
115
+ i++;
116
+ }
117
+ i--;
118
+ html += list(items);
119
+ continue;
120
+ }
121
+ if (line.startsWith('|') && /^\|?\s*:?-+/.test(lines[i + 1] || '')) {
122
+ flush();
123
+ const rows = [];
124
+ while (i < lines.length && lines[i].startsWith('|'))
125
+ rows.push(lines[i++]);
126
+ i--;
127
+ html += table(rows);
128
+ continue;
129
+ }
130
+ if (line.startsWith('> ')) {
131
+ flush();
132
+ const q = [];
133
+ while (i < lines.length && lines[i].startsWith('> '))
134
+ q.push(lines[i++].slice(2));
135
+ i--;
136
+ html += `<blockquote>${blocks(q.join('\n'))}</blockquote>`;
137
+ continue;
138
+ }
139
+ para.push(line);
140
+ }
141
+ flush();
142
+ return html;
143
+ }
144
+ // ---------------------------------------------------------------
145
+ // slide
146
+ // ---------------------------------------------------------------
147
+ export function splitSlides(md) {
148
+ const slides = [];
149
+ let cur = [];
150
+ let inFence = false;
151
+ for (const line of md.split('\n')) {
152
+ if (/^```/.test(line))
153
+ inFence = !inFence;
154
+ if (!inFence && /^---\s*$/.test(line)) {
155
+ slides.push(cur.join('\n'));
156
+ cur = [];
157
+ }
158
+ else
159
+ cur.push(line);
160
+ }
161
+ slides.push(cur.join('\n'));
162
+ return slides.filter((s) => s.trim());
163
+ }
164
+ export function slide(src) {
165
+ const classes = ['slide'];
166
+ let style = '';
167
+ let body = src.replace(/^\s+/, '');
168
+ const d = body.match(/^::([^:]+)::[ \t]*(\n|$)/);
169
+ const tokens = d ? d[1].trim().split(/\s+/) : [];
170
+ if (d && tokens.every((t) => DIRECTIVES.test(t))) {
171
+ for (const tok of tokens) {
172
+ const bg = tok.match(/^bg=(.+)$/);
173
+ const size = tok.match(/^size=(.+)$/);
174
+ if (bg) {
175
+ const name = bg[1];
176
+ style += `background:${esc(BG[name] || name)};`;
177
+ if (name === 'white')
178
+ classes.push('light');
179
+ }
180
+ else if (size)
181
+ style += `--slide-size:${esc(size[1])};`;
182
+ else
183
+ classes.push(tok);
184
+ }
185
+ body = body.slice(d[0].length);
186
+ }
187
+ let html;
188
+ const parts = body.split(/^::right::[ \t]*$/m);
189
+ if (parts.length === 2) {
190
+ const [first, right] = parts;
191
+ // 先頭の見出しはカラムの外(上)に出す
192
+ const head = first.match(/^\s*(#{1,3} .*)\n/);
193
+ const left = head ? first.replace(head[0], '') : first;
194
+ html =
195
+ (head ? blocks(head[1]) : '') +
196
+ `<div class="cols"><div>${blocks(left)}</div><div>${blocks(right)}</div></div>`;
197
+ }
198
+ else {
199
+ html = blocks(body);
200
+ }
201
+ return `<section class="${classes.join(' ')}"${style ? ` style="${style}"` : ''}>${html}</section>`;
202
+ }
203
+ export function frontmatter(md) {
204
+ const m = md.replace(/^\uFEFF/, '').match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
205
+ if (!m)
206
+ return [{}, md];
207
+ const meta = {};
208
+ for (const line of m[1].split(/\r?\n/)) {
209
+ const kv = line.match(/^(\w+):\s*(.*?)\s*$/);
210
+ if (kv)
211
+ meta[kv[1]] = kv[2].replace(/^(["'])(.*)\1$/, '$2');
212
+ }
213
+ return [meta, md.slice(m[0].length)];
214
+ }
215
+ /** Markdown 全体 → { meta, sections } */
216
+ export function parse(md) {
217
+ const [meta, body] = frontmatter(md.replace(/\r\n/g, '\n'));
218
+ return { meta, sections: splitSlides(body).map(slide) };
219
+ }
package/dist/pdf.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** --browser-path → CHROME_PATH → よくある場所 の順に探す */
2
+ export declare function findBrowser(explicit?: string): string | null;
3
+ export interface PdfOptions {
4
+ /** 使うブラウザ。省略すると CHROME_PATH → よくある場所 の順に探す */
5
+ browserPath?: string;
6
+ }
7
+ /**
8
+ * @param html file:// URL の画像で組み立てた HTML
9
+ * @param out 出力する PDF のパス
10
+ */
11
+ export declare function toPdf(html: string, out: string, { browserPath }?: PdfOptions): Promise<void>;
package/dist/pdf.js ADDED
@@ -0,0 +1,83 @@
1
+ // PDF 出力。ローカルの Chrome / Chromium / Edge をヘッドレスで起動して印刷する。
2
+ // 依存パッケージなし(puppeteer も使わない)。
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { delimiter, join } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ const CANDIDATES = {
9
+ darwin: [
10
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
11
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
12
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
13
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
14
+ ],
15
+ linux: ['google-chrome-stable', 'google-chrome', 'chromium', 'chromium-browser', 'microsoft-edge', 'brave-browser'],
16
+ win32: [
17
+ join(process.env['PROGRAMFILES'] || 'C:\\Program Files', 'Google\\Chrome\\Application\\chrome.exe'),
18
+ join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google\\Chrome\\Application\\chrome.exe'),
19
+ join(process.env['LOCALAPPDATA'] || '', 'Google\\Chrome\\Application\\chrome.exe'),
20
+ join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Microsoft\\Edge\\Application\\msedge.exe'),
21
+ ],
22
+ };
23
+ function which(cmd) {
24
+ for (const dir of (process.env.PATH || '').split(delimiter)) {
25
+ const p = join(dir, cmd);
26
+ if (existsSync(p))
27
+ return p;
28
+ }
29
+ return null;
30
+ }
31
+ /** --browser-path → CHROME_PATH → よくある場所 の順に探す */
32
+ export function findBrowser(explicit) {
33
+ if (explicit)
34
+ return existsSync(explicit) ? explicit : null;
35
+ if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH))
36
+ return process.env.CHROME_PATH;
37
+ for (const c of CANDIDATES[process.platform] || []) {
38
+ const p = c.includes('/') || c.includes('\\') ? (existsSync(c) ? c : null) : which(c);
39
+ if (p)
40
+ return p;
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * @param html file:// URL の画像で組み立てた HTML
46
+ * @param out 出力する PDF のパス
47
+ */
48
+ export async function toPdf(html, out, { browserPath } = {}) {
49
+ const browser = findBrowser(browserPath);
50
+ if (!browser) {
51
+ throw new Error('PDF の出力には Chrome / Chromium / Edge が必要です。--browser-path か環境変数 CHROME_PATH で場所を指定してください。');
52
+ }
53
+ const dir = mkdtempSync(join(tmpdir(), 'ippon-'));
54
+ const src = join(dir, 'slide.html');
55
+ writeFileSync(src, html);
56
+ rmSync(out, { force: true });
57
+ try {
58
+ const args = [
59
+ '--headless=new',
60
+ '--disable-gpu',
61
+ '--no-first-run',
62
+ '--no-default-browser-check',
63
+ `--user-data-dir=${join(dir, 'profile')}`,
64
+ '--no-pdf-header-footer',
65
+ '--virtual-time-budget=3000', // Web フォントの読み込みを待つ
66
+ `--print-to-pdf=${out}`,
67
+ pathToFileURL(src).href,
68
+ ];
69
+ const stderr = await new Promise((ok, ng) => {
70
+ const p = spawn(browser, args, { stdio: ['ignore', 'ignore', 'pipe'] });
71
+ let err = '';
72
+ p.stderr.on('data', (d) => (err += d));
73
+ p.on('error', ng);
74
+ p.on('close', () => ok(err));
75
+ });
76
+ if (!existsSync(out) || statSync(out).size === 0) {
77
+ throw new Error(`PDF の出力に失敗しました(${browser})\n${stderr.trim()}`);
78
+ }
79
+ }
80
+ finally {
81
+ rmSync(dir, { recursive: true, force: true });
82
+ }
83
+ }
@@ -0,0 +1,14 @@
1
+ export interface RenderOptions {
2
+ /** 画像の相対パスの基準(= Markdown のあるディレクトリ) */
3
+ baseDir: string;
4
+ /** 出力先ディレクトリ。省略すると画像は file:// URL になる */
5
+ outDir?: string;
6
+ /** 追加の CSS(--css) */
7
+ css?: string;
8
+ }
9
+ export interface RenderResult {
10
+ html: string;
11
+ /** スライドの枚数 */
12
+ count: number;
13
+ }
14
+ export declare function render(md: string, { baseDir, outDir, css }: RenderOptions): RenderResult;
package/dist/render.js ADDED
@@ -0,0 +1,51 @@
1
+ // スライドの HTML を 1 ファイルに組み立てる(CSS / JS も全部埋め込む)
2
+ import { readFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { esc } from './highlight.js';
6
+ import { parse } from './markdown.js';
7
+ const assets = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets');
8
+ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i;
9
+ /**
10
+ * 相対パスの画像を、出力先から見たパスに書き換える。
11
+ * - outDir を渡す → 出力先からの相対パス(HTML 用)
12
+ * - outDir なし → file:// の絶対 URL(PDF の一時ファイル用)
13
+ */
14
+ function rewriteImages(html, baseDir, outDir) {
15
+ return html.replace(/(<img src=")([^"]+)(")/g, (all, pre, src, post) => {
16
+ if (EXTERNAL.test(src) || isAbsolute(src))
17
+ return all;
18
+ const abs = resolve(baseDir, decodeURI(src));
19
+ const url = outDir ? encodeURI(relative(outDir, abs).split(sep).join('/')) : pathToFileURL(abs).href;
20
+ return pre + url + post;
21
+ });
22
+ }
23
+ export function render(md, { baseDir, outDir, css = '' }) {
24
+ const { meta, sections } = parse(md);
25
+ const style = readFileSync(join(assets, 'style.css'), 'utf8') + (css ? `\n/* --css */\n${css}` : '');
26
+ const script = readFileSync(join(assets, 'deck.js'), 'utf8');
27
+ const body = rewriteImages(sections.join('\n'), baseDir, outDir);
28
+ const html = `<!doctype html>
29
+ <html lang="${esc(meta.lang || 'ja')}">
30
+ <head>
31
+ <meta charset="utf-8">
32
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
33
+ <title>${esc(meta.title || 'slides')}</title>
34
+ ${meta.description ? `<meta name="description" content="${esc(meta.description)}">\n` : ''}${meta.author ? `<meta name="author" content="${esc(meta.author)}">\n` : ''}<meta name="generator" content="ippon-slides">
35
+ <style>${style}</style>
36
+ </head>
37
+ <body>
38
+ <div id="deck">
39
+ ${body}
40
+ </div>
41
+ <div id="overview"></div>
42
+ <div id="progress"></div>
43
+ <div id="timer"></div>
44
+ <div id="counter"></div>
45
+ <div id="hint">← → 移動 ・ f フルスクリーン ・ o 一覧</div>
46
+ <script>${script}</script>
47
+ </body>
48
+ </html>
49
+ `;
50
+ return { html, count: sections.length };
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ippon-slides",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "1 枚 1 メッセージのスライドを Markdown から作る CLI。HTML 1 ファイルと PDF を出力",
5
5
  "keywords": [
6
6
  "slides",
@@ -25,17 +25,28 @@
25
25
  "ippon": "bin/ippon.js"
26
26
  },
27
27
  "exports": {
28
- ".": "./src/index.js"
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
29
32
  },
33
+ "types": "./dist/index.d.ts",
30
34
  "files": [
31
35
  "bin",
32
- "src",
36
+ "dist",
33
37
  "assets"
34
38
  ],
35
39
  "engines": {
36
40
  "node": ">=18.17"
37
41
  },
38
42
  "scripts": {
39
- "test": "node --test"
43
+ "build": "tsc -p tsconfig.build.json && tsc -p src/client",
44
+ "typecheck": "tsc && tsc -p src/client --noEmit",
45
+ "test": "npm run build && node --test test/*.test.ts",
46
+ "prepack": "npm run build"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.20.5",
50
+ "typescript": "^7.0.2"
40
51
  }
41
52
  }