voop 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
@@ -8,6 +8,20 @@ Publish files to the web instantly with a single command.
8
8
 
9
9
  I use coding agents to create interactive HTML pages instead of static markdown docs. When you can just say "make me a visualization of this data" and get a working HTML page, you need a fast way to share it. This tool lets me go from local file to public URL in one command.
10
10
 
11
+ **Markdown files get beautifully rendered too.** Tired of sharing `.md` files that people have to open in a text editor? voop automatically converts markdown to styled HTML pages with:
12
+ - Syntax highlighting for code blocks
13
+ - Mermaid diagram rendering
14
+ - Table of contents sidebar
15
+ - Dark mode support
16
+ - Clean typography
17
+
18
+ Share docs with your team and they'll see properly formatted content, not raw markdown.
19
+
20
+ | Raw Markdown | Rendered HTML |
21
+ |:---:|:---:|
22
+ | [![Raw](https://share.yemyat.com/voop-raw-version-d4fda207.png)](https://share.yemyat.com/example-9afa27ba.md) | [![Rendered](https://share.yemyat.com/voop-html-version-09b5b6c8.png)](https://share.yemyat.com/example-20a6d65b.html) |
23
+ | `voop doc.md --raw` | `voop doc.md` |
24
+
11
25
  ## Alternatives
12
26
 
13
27
  | Tool | Pros | Cons |
@@ -35,15 +49,28 @@ Uses Cloudflare R2 for storage with free egress and global CDN.
35
49
 
36
50
  ## Install
37
51
 
52
+ Run directly without installing:
53
+
38
54
  ```bash
39
55
  npx voop <file>
56
+ # or
57
+ bunx voop <file>
40
58
  ```
41
59
 
42
60
  Or install globally:
43
61
 
44
62
  ```bash
45
63
  npm install -g voop
46
- voop <file>
64
+ # or
65
+ bun install -g voop
66
+ ```
67
+
68
+ ## AI Agent Skill
69
+
70
+ Install the voop skill so AI agents can use voop automatically when you ask to upload files:
71
+
72
+ ```bash
73
+ npx add-skill yemyat/voop
47
74
  ```
48
75
 
49
76
  ## Setup
@@ -76,6 +103,8 @@ Config is stored at `~/.config/voop/config.json`.
76
103
 
77
104
  ```bash
78
105
  voop mypage.html # Upload a file
106
+ voop docs.md # Markdown → styled HTML with ToC
107
+ voop docs.md --raw # Upload raw markdown (no conversion)
79
108
  voop --setup # Reconfigure credentials
80
109
  voop --test # Test R2 connection
81
110
  voop --help # Show help
package/dist/index.js CHANGED
@@ -4,8 +4,10 @@ import { lookup } from "mime-types";
4
4
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
5
5
  import { basename, extname } from "path";
6
6
  import { randomBytes } from "crypto";
7
- import { spawn } from "child_process";
7
+ import { spawnSync } from "child_process";
8
8
  import * as p from "@clack/prompts";
9
+ import { marked } from "marked";
10
+ import { createHtmlTemplate } from "./template.js";
9
11
  const CONFIG_PATH = `${process.env.HOME}/.config/voop/config.json`;
10
12
  function loadConfig() {
11
13
  if (!existsSync(CONFIG_PATH)) {
@@ -122,15 +124,37 @@ async function setup(existingConfig) {
122
124
  return config;
123
125
  }
124
126
  function copyToClipboard(text) {
127
+ const platform = process.platform;
128
+ const cmd = platform === "darwin" ? "pbcopy"
129
+ : platform === "win32" ? "clip"
130
+ : "xclip";
131
+ const args = cmd === "xclip" ? ["-selection", "clipboard"] : [];
125
132
  try {
126
- const proc = spawn("pbcopy", [], { stdio: ["pipe", "ignore", "ignore"] });
127
- proc.stdin?.write(text);
128
- proc.stdin?.end();
133
+ const result = spawnSync(cmd, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
134
+ if (result.error)
135
+ return;
129
136
  console.log(`(copied)`);
130
137
  }
131
138
  catch { }
132
139
  }
133
- async function publish(filePath) {
140
+ function convertMarkdownToHtml(markdown, title) {
141
+ marked.setOptions({
142
+ gfm: true,
143
+ breaks: false,
144
+ });
145
+ const renderer = new marked.Renderer();
146
+ renderer.code = ({ text, lang }) => {
147
+ if (lang === "mermaid") {
148
+ return `<pre class="mermaid">${text}</pre>`;
149
+ }
150
+ const langClass = lang ? ` class="language-${lang}"` : "";
151
+ const escaped = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
152
+ return `<pre><code${langClass}>${escaped}</code></pre>`;
153
+ };
154
+ const content = marked.parse(markdown, { renderer });
155
+ return createHtmlTemplate(title, content);
156
+ }
157
+ async function publish(filePath, raw = false) {
134
158
  let config = loadConfig();
135
159
  if (!isConfigValid(config)) {
136
160
  console.log("No configuration found. Let's set things up first.\n");
@@ -145,12 +169,25 @@ async function publish(filePath) {
145
169
  process.exit(1);
146
170
  }
147
171
  const client = createS3Client(config);
148
- const fileContent = readFileSync(filePath);
149
- const ext = extname(filePath);
150
- const baseName = basename(filePath, ext);
172
+ const ext = extname(filePath).toLowerCase();
173
+ const baseName = basename(filePath, extname(filePath));
151
174
  const shortId = randomBytes(4).toString("hex");
152
- const key = `${baseName}-${shortId}${ext}`;
153
- const contentType = lookup(filePath) || "application/octet-stream";
175
+ const isMarkdown = ext === ".md" || ext === ".markdown";
176
+ const shouldConvert = isMarkdown && !raw;
177
+ let fileContent;
178
+ let key;
179
+ let contentType;
180
+ if (shouldConvert) {
181
+ const markdown = readFileSync(filePath, "utf-8");
182
+ fileContent = convertMarkdownToHtml(markdown, baseName);
183
+ key = `${baseName}-${shortId}.html`;
184
+ contentType = "text/html; charset=utf-8";
185
+ }
186
+ else {
187
+ fileContent = readFileSync(filePath);
188
+ key = `${baseName}-${shortId}${ext}`;
189
+ contentType = lookup(filePath) || "application/octet-stream";
190
+ }
154
191
  await client.send(new PutObjectCommand({
155
192
  Bucket: config.bucketName,
156
193
  Key: key,
@@ -168,15 +205,22 @@ if (args.length === 0 || args[0] === "--help" || args[0] === "-h" || args[0] ===
168
205
  voop - Upload files to the web instantly
169
206
 
170
207
  USAGE
171
- voop <file> Upload a file and get a public URL
172
- voop --setup Configure or reconfigure credentials
173
- voop --test Test connection to R2
174
- voop --help Show this help
208
+ voop <file> Upload a file and get a public URL
209
+ voop <file> --raw Upload markdown without HTML conversion
210
+ voop --setup Configure or reconfigure credentials
211
+ voop --test Test connection to R2
212
+ voop --help Show this help
213
+
214
+ MARKDOWN
215
+ Markdown files (.md) are automatically converted to beautifully
216
+ rendered HTML pages with syntax highlighting and Mermaid support.
217
+ Use --raw to upload the original markdown file instead.
175
218
 
176
219
  EXAMPLES
177
220
  voop screenshot.png
221
+ voop README.md Uploads as styled HTML
222
+ voop README.md --raw Uploads as raw markdown
178
223
  voop document.pdf
179
- voop index.html
180
224
 
181
225
  SETUP
182
226
  On first run, you'll be guided through an interactive setup.
@@ -207,5 +251,7 @@ else if (args[0] === "--test" || args[0] === "test") {
207
251
  process.exit(success ? 0 : 1);
208
252
  }
209
253
  else {
210
- await publish(args[0]);
254
+ const raw = args.includes("--raw");
255
+ const filePath = args.find(arg => !arg.startsWith("--")) || args[0];
256
+ await publish(filePath, raw);
211
257
  }
@@ -0,0 +1 @@
1
+ export declare function createHtmlTemplate(title: string, content: string): string;
@@ -0,0 +1,476 @@
1
+ export function createHtmlTemplate(title, content) {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>${escapeHtml(title)}</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --font-heading: 'Space Grotesk', system-ui, sans-serif;
14
+ --font-body: 'Source Serif 4', Georgia, serif;
15
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
16
+ --color-bg: #fafafa;
17
+ --color-surface: #ffffff;
18
+ --color-text: #1a1a1a;
19
+ --color-text-muted: #666666;
20
+ --color-border: #e5e5e5;
21
+ --color-accent: #0055ff;
22
+ --color-code-bg: #f5f5f5;
23
+ --color-code-text: #1a1a1a;
24
+ --toc-width: 260px;
25
+ --max-width: 72ch;
26
+ --space-xs: 0.25rem;
27
+ --space-sm: 0.5rem;
28
+ --space-md: 1rem;
29
+ --space-lg: 2rem;
30
+ --space-xl: 4rem;
31
+ --space-2xl: 6rem;
32
+ }
33
+
34
+ @media (prefers-color-scheme: dark) {
35
+ :root {
36
+ --color-bg: #0a0a0a;
37
+ --color-surface: #141414;
38
+ --color-text: #f0f0f0;
39
+ --color-text-muted: #888888;
40
+ --color-border: #2a2a2a;
41
+ --color-accent: #4d8bff;
42
+ --color-code-bg: #1e1e1e;
43
+ --color-code-text: #d4d4d4;
44
+ }
45
+ }
46
+
47
+ *, *::before, *::after {
48
+ box-sizing: border-box;
49
+ margin: 0;
50
+ padding: 0;
51
+ }
52
+
53
+ html {
54
+ font-size: 18px;
55
+ -webkit-font-smoothing: antialiased;
56
+ -moz-osx-font-smoothing: grayscale;
57
+ scroll-behavior: smooth;
58
+ }
59
+
60
+ body {
61
+ font-family: var(--font-body);
62
+ font-optical-sizing: auto;
63
+ background: var(--color-bg);
64
+ color: var(--color-text);
65
+ line-height: 1.7;
66
+ min-height: 100dvh;
67
+ }
68
+
69
+ .layout {
70
+ display: flex;
71
+ max-width: calc(var(--max-width) + var(--toc-width) + var(--space-2xl));
72
+ margin: 0 auto;
73
+ }
74
+
75
+ .container {
76
+ flex: 1;
77
+ max-width: var(--max-width);
78
+ padding: var(--space-2xl) var(--space-lg);
79
+ }
80
+
81
+ article {
82
+ position: relative;
83
+ }
84
+
85
+ /* Table of Contents */
86
+ .toc {
87
+ position: sticky;
88
+ top: var(--space-lg);
89
+ align-self: flex-start;
90
+ width: var(--toc-width);
91
+ max-height: calc(100vh - var(--space-xl));
92
+ overflow-y: auto;
93
+ padding: var(--space-lg);
94
+ margin-top: var(--space-2xl);
95
+ font-family: var(--font-heading);
96
+ font-size: 0.8rem;
97
+ display: none;
98
+ }
99
+
100
+ @media (min-width: 1100px) {
101
+ .toc {
102
+ display: block;
103
+ }
104
+ }
105
+
106
+ .toc-title {
107
+ font-weight: 600;
108
+ font-size: 0.7rem;
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.05em;
111
+ color: var(--color-text-muted);
112
+ margin-bottom: var(--space-md);
113
+ }
114
+
115
+ .toc-list {
116
+ list-style: none;
117
+ padding: 0;
118
+ margin: 0;
119
+ }
120
+
121
+ .toc-list li {
122
+ margin-bottom: var(--space-xs);
123
+ }
124
+
125
+ .toc-list a {
126
+ color: var(--color-text-muted);
127
+ text-decoration: none;
128
+ display: block;
129
+ padding: 2px 0;
130
+ border-left: 2px solid transparent;
131
+ padding-left: var(--space-sm);
132
+ transition: color 0.15s, border-color 0.15s;
133
+ }
134
+
135
+ .toc-list a:hover {
136
+ color: var(--color-text);
137
+ }
138
+
139
+ .toc-list a.active {
140
+ color: var(--color-accent);
141
+ border-left-color: var(--color-accent);
142
+ }
143
+
144
+ .toc-list .toc-h2 { padding-left: var(--space-sm); }
145
+ .toc-list .toc-h3 { padding-left: calc(var(--space-sm) + 0.75rem); font-size: 0.75rem; }
146
+ .toc-list .toc-h4 { padding-left: calc(var(--space-sm) + 1.5rem); font-size: 0.7rem; }
147
+
148
+ /* Typography */
149
+ h1, h2, h3, h4, h5, h6 {
150
+ font-family: var(--font-heading);
151
+ font-weight: 600;
152
+ line-height: 1.25;
153
+ text-wrap: balance;
154
+ letter-spacing: -0.02em;
155
+ margin-top: var(--space-xl);
156
+ margin-bottom: var(--space-md);
157
+ scroll-margin-top: var(--space-lg);
158
+ }
159
+
160
+ h1 {
161
+ font-size: 2.5rem;
162
+ font-weight: 700;
163
+ margin-top: 0;
164
+ margin-bottom: var(--space-lg);
165
+ padding-bottom: var(--space-lg);
166
+ border-bottom: 3px solid var(--color-text);
167
+ }
168
+
169
+ h2 {
170
+ font-size: 1.75rem;
171
+ margin-top: var(--space-2xl);
172
+ padding-top: var(--space-lg);
173
+ border-top: 1px solid var(--color-border);
174
+ }
175
+
176
+ h3 { font-size: 1.35rem; }
177
+ h4 { font-size: 1.15rem; }
178
+ h5, h6 { font-size: 1rem; }
179
+
180
+ p {
181
+ margin-bottom: var(--space-md);
182
+ text-wrap: pretty;
183
+ }
184
+
185
+ a {
186
+ color: var(--color-accent);
187
+ text-decoration: underline;
188
+ text-underline-offset: 2px;
189
+ text-decoration-thickness: 1px;
190
+ transition: text-decoration-thickness 0.15s ease;
191
+ }
192
+
193
+ a:hover {
194
+ text-decoration-thickness: 2px;
195
+ }
196
+
197
+ strong {
198
+ font-weight: 600;
199
+ }
200
+
201
+ em {
202
+ font-style: italic;
203
+ }
204
+
205
+ /* Lists */
206
+ ul, ol {
207
+ margin-bottom: var(--space-md);
208
+ padding-left: var(--space-lg);
209
+ }
210
+
211
+ li {
212
+ margin-bottom: var(--space-xs);
213
+ }
214
+
215
+ li > ul, li > ol {
216
+ margin-top: var(--space-xs);
217
+ margin-bottom: 0;
218
+ }
219
+
220
+ /* Code - custom theme that works for both light and dark */
221
+ code {
222
+ font-family: var(--font-mono);
223
+ font-size: 0.85em;
224
+ background: var(--color-code-bg);
225
+ color: var(--color-code-text);
226
+ padding: 0.15em 0.4em;
227
+ border-radius: 4px;
228
+ }
229
+
230
+ pre {
231
+ background: var(--color-code-bg) !important;
232
+ border-radius: 6px;
233
+ padding: var(--space-md);
234
+ overflow-x: auto;
235
+ margin-bottom: var(--space-lg);
236
+ border: 1px solid var(--color-border);
237
+ }
238
+
239
+ pre code {
240
+ background: none;
241
+ padding: 0;
242
+ font-size: 0.875rem;
243
+ line-height: 1.6;
244
+ color: var(--color-code-text);
245
+ }
246
+
247
+ /* Syntax highlighting - works in both modes */
248
+ .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a9955; }
249
+ .token.punctuation { color: var(--color-code-text); }
250
+ .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #4fc1ff; }
251
+ .token.boolean, .token.number { color: #b5cea8; }
252
+ .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #ce9178; }
253
+ .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #d4d4d4; }
254
+ .token.atrule, .token.attr-value, .token.keyword { color: #c586c0; }
255
+ .token.function { color: #dcdcaa; }
256
+ .token.class-name { color: #4ec9b0; }
257
+ .token.regex, .token.important, .token.variable { color: #d16969; }
258
+
259
+ @media (prefers-color-scheme: light) {
260
+ .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #008000; }
261
+ .token.punctuation { color: #393a34; }
262
+ .token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #36acaa; }
263
+ .token.boolean, .token.number { color: #36acaa; }
264
+ .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #9a050f; }
265
+ .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #393a34; }
266
+ .token.atrule, .token.attr-value, .token.keyword { color: #0000ff; }
267
+ .token.function { color: #393a34; }
268
+ .token.class-name { color: #2b91af; }
269
+ .token.regex, .token.important, .token.variable { color: #d16969; }
270
+ }
271
+
272
+ /* Blockquotes */
273
+ blockquote {
274
+ font-style: italic;
275
+ border-left: 4px solid var(--color-text);
276
+ margin: var(--space-lg) 0;
277
+ padding: var(--space-md) var(--space-lg);
278
+ background: var(--color-surface);
279
+ }
280
+
281
+ blockquote p:last-child {
282
+ margin-bottom: 0;
283
+ }
284
+
285
+ /* Tables */
286
+ table {
287
+ width: 100%;
288
+ border-collapse: collapse;
289
+ margin: var(--space-lg) 0;
290
+ font-family: var(--font-heading);
291
+ font-size: 0.9rem;
292
+ }
293
+
294
+ th, td {
295
+ text-align: left;
296
+ padding: var(--space-sm) var(--space-md);
297
+ border-bottom: 1px solid var(--color-border);
298
+ }
299
+
300
+ th {
301
+ font-weight: 600;
302
+ background: var(--color-surface);
303
+ border-bottom-width: 2px;
304
+ }
305
+
306
+ tr:hover td {
307
+ background: var(--color-surface);
308
+ }
309
+
310
+ /* Images */
311
+ img {
312
+ max-width: 100%;
313
+ height: auto;
314
+ border-radius: 6px;
315
+ margin: var(--space-lg) 0;
316
+ }
317
+
318
+ /* Horizontal Rule */
319
+ hr {
320
+ border: none;
321
+ border-top: 1px solid var(--color-border);
322
+ margin: var(--space-xl) 0;
323
+ }
324
+
325
+ /* Task Lists */
326
+ ul.contains-task-list {
327
+ list-style: none;
328
+ padding-left: 0;
329
+ }
330
+
331
+ .task-list-item {
332
+ display: flex;
333
+ align-items: baseline;
334
+ gap: var(--space-sm);
335
+ }
336
+
337
+ .task-list-item input[type="checkbox"] {
338
+ margin: 0;
339
+ accent-color: var(--color-accent);
340
+ }
341
+
342
+ /* Mermaid */
343
+ .mermaid {
344
+ text-align: center;
345
+ margin: var(--space-lg) 0;
346
+ background: var(--color-surface);
347
+ padding: var(--space-md);
348
+ border-radius: 6px;
349
+ border: 1px solid var(--color-border);
350
+ }
351
+
352
+ /* Footer */
353
+ .footer {
354
+ margin-top: var(--space-2xl);
355
+ padding-top: var(--space-lg);
356
+ border-top: 1px solid var(--color-border);
357
+ font-family: var(--font-heading);
358
+ font-size: 0.75rem;
359
+ color: var(--color-text-muted);
360
+ text-align: right;
361
+ }
362
+
363
+ .footer a {
364
+ color: var(--color-text-muted);
365
+ text-decoration: none;
366
+ }
367
+
368
+ .footer a:hover {
369
+ color: var(--color-text);
370
+ }
371
+
372
+ /* Print styles */
373
+ @media print {
374
+ body { font-size: 12pt; }
375
+ .layout { display: block; }
376
+ .container { max-width: none; padding: 0; }
377
+ .toc, .footer { display: none; }
378
+ }
379
+ </style>
380
+ </head>
381
+ <body>
382
+ <div class="layout">
383
+ <div class="container">
384
+ <article>
385
+ ${content}
386
+ </article>
387
+ <footer class="footer">
388
+ Published with <a href="https://github.com/yemyat/voop" target="_blank" rel="noopener">voop</a>
389
+ </footer>
390
+ </div>
391
+ <nav class="toc" aria-label="Table of contents">
392
+ <div class="toc-title">On this page</div>
393
+ <ul class="toc-list" id="toc-list"></ul>
394
+ </nav>
395
+ </div>
396
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
397
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
398
+ <script type="module">
399
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
400
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
401
+ mermaid.initialize({
402
+ startOnLoad: true,
403
+ theme: isDark ? 'dark' : 'neutral',
404
+ securityLevel: 'loose',
405
+ fontFamily: 'Space Grotesk, system-ui, sans-serif',
406
+ });
407
+
408
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
409
+ window.location.reload();
410
+ });
411
+ </script>
412
+ <script>
413
+ (function() {
414
+ const article = document.querySelector('article');
415
+ const tocList = document.getElementById('toc-list');
416
+ const headings = Array.from(article.querySelectorAll('h1, h2, h3, h4'));
417
+
418
+ headings.forEach((heading, index) => {
419
+ if (!heading.id) {
420
+ heading.id = 'heading-' + index;
421
+ }
422
+
423
+ const li = document.createElement('li');
424
+ const a = document.createElement('a');
425
+ a.href = '#' + heading.id;
426
+ a.textContent = heading.textContent;
427
+ a.className = 'toc-' + heading.tagName.toLowerCase();
428
+ li.appendChild(a);
429
+ tocList.appendChild(li);
430
+ });
431
+
432
+ const tocLinks = tocList.querySelectorAll('a');
433
+
434
+ function updateActiveLink() {
435
+ const scrollTop = window.scrollY;
436
+ const windowHeight = window.innerHeight;
437
+ const docHeight = document.documentElement.scrollHeight;
438
+ const isAtBottom = scrollTop + windowHeight >= docHeight - 50;
439
+
440
+ let activeIndex = 0;
441
+
442
+ if (isAtBottom) {
443
+ activeIndex = headings.length - 1;
444
+ } else {
445
+ for (let i = headings.length - 1; i >= 0; i--) {
446
+ const heading = headings[i];
447
+ const rect = heading.getBoundingClientRect();
448
+ if (rect.top <= 100) {
449
+ activeIndex = i;
450
+ break;
451
+ }
452
+ }
453
+ }
454
+
455
+ tocLinks.forEach((link, i) => {
456
+ link.classList.toggle('active', i === activeIndex);
457
+ });
458
+ }
459
+
460
+ window.addEventListener('scroll', updateActiveLink, { passive: true });
461
+ updateActiveLink();
462
+ })();
463
+ </script>
464
+ </body>
465
+ </html>`;
466
+ }
467
+ function escapeHtml(text) {
468
+ const map = {
469
+ '&': '&amp;',
470
+ '<': '&lt;',
471
+ '>': '&gt;',
472
+ '"': '&quot;',
473
+ "'": '&#039;'
474
+ };
475
+ return text.replace(/[&<>"']/g, c => map[c]);
476
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voop",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Publish files to the web instantly with a single command using Coudflare R2",
5
5
  "author": "Ye Myat Min",
6
6
  "license": "MIT",
@@ -37,6 +37,7 @@
37
37
  "dependencies": {
38
38
  "@aws-sdk/client-s3": "^3.940.0",
39
39
  "@clack/prompts": "^0.11.0",
40
+ "marked": "^17.0.1",
40
41
  "mime-types": "^3.0.2"
41
42
  }
42
43
  }