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 +30 -1
- package/dist/index.js +62 -16
- package/dist/template.d.ts +1 -0
- package/dist/template.js +476 -0
- package/package.json +2 -1
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
|
+
| [](https://share.yemyat.com/example-9afa27ba.md) | [](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
|
-
|
|
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 {
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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, "<").replace(/>/g, ">");
|
|
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
|
|
149
|
-
const
|
|
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
|
|
153
|
-
const
|
|
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>
|
|
172
|
-
voop --
|
|
173
|
-
voop --
|
|
174
|
-
voop --
|
|
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
|
-
|
|
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;
|
package/dist/template.js
ADDED
|
@@ -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
|
+
'&': '&',
|
|
470
|
+
'<': '<',
|
|
471
|
+
'>': '>',
|
|
472
|
+
'"': '"',
|
|
473
|
+
"'": '''
|
|
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.
|
|
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
|
}
|