sdocs-dev 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/bin/sdocs-dev.js +282 -38
- package/package.json +6 -3
- package/public/{brotli-wasm.js → brotli-wasm-v1.js} +5 -6
- package/public/css/layout.css +3 -4
- package/public/css/mobile.css +7 -0
- package/public/css/panel.css +1 -1
- package/public/css/rendered.css +184 -1
- package/public/index.html +137 -43
- package/public/sdoc.md +542 -0
- package/public/sdocs-app.js +222 -63
- package/public/sdocs-charts.js +905 -0
- package/public/sdocs-controls.js +135 -53
- package/public/sdocs-export.js +939 -61
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-state.js +14 -0
- package/public/sdocs-styles.js +180 -254
- package/public/sdocs-theme.js +21 -19
- package/public/sdocs-write.js +4 -1
- package/public/sw.js +5 -3
- package/public/vendor/purify.min.js +3 -0
- package/server.js +51 -3
- package/public/default.md +0 -255
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# SDocs
|
|
2
|
+
|
|
3
|
+
**Read, style, and share markdown files — privately.**
|
|
4
|
+
|
|
5
|
+
SDocs is a lightweight, stateless markdown editor with live styling. Your entire document lives in the URL hash — nothing is ever sent to a server.
|
|
6
|
+
|
|
7
|
+
**[sdocs.dev](https://sdocs.dev)** | **[npm](https://www.npmjs.com/package/sdocs-dev)** | **[MIT License](LICENSE)**
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Read** — open any `.md` file with clean, styled formatting
|
|
14
|
+
- **Style** — customize fonts, colors, sizes, and spacing via a visual panel or YAML front matter
|
|
15
|
+
- **Share** — compress a document into a URL and share it with anyone (no server, no account)
|
|
16
|
+
- **Export** — PDF, Word (.docx), raw `.md`, or styled `.md` with front matter
|
|
17
|
+
|
|
18
|
+
Everything runs client-side. The server is ~60 lines of Node.js that serves static files. No database, no logging, no analytics.
|
|
19
|
+
|
|
20
|
+
## CLI
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i -g sdocs-dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
sdoc README.md # open styled in browser
|
|
28
|
+
sdoc share report.md # copy shareable link to clipboard
|
|
29
|
+
sdoc share report.md --dark # link opens in dark theme
|
|
30
|
+
sdoc share report.md --section "Results" # deep-link to a heading
|
|
31
|
+
sdoc new # blank document in write mode
|
|
32
|
+
sdoc schema # print all style properties
|
|
33
|
+
cat notes.md | sdoc # pipe markdown to browser
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
Documents are compressed with [Brotli](https://en.wikipedia.org/wiki/Brotli) and encoded as [base64url](https://en.wikipedia.org/wiki/Base64#URL_applications) in the URL hash fragment. The hash fragment is never sent to the server by the browser — it stays entirely client-side.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
https://sdocs.dev/#md={brotli compressed + base64url encoded .md}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Styles are stored as [YAML front matter](https://jekyllrb.com/docs/front-matter/) in the `.md` file using a `styles:` key. Default style values are omitted from URLs to keep them short. Run `sdoc schema` to see all available properties.
|
|
45
|
+
|
|
46
|
+
## Stack
|
|
47
|
+
|
|
48
|
+
- **Server**: `server.js` — pure Node.js `http` module
|
|
49
|
+
- **Frontend**: plain HTML, CSS, and JS in `public/` — no build step, no framework
|
|
50
|
+
- **Markdown parsing**: [marked](https://github.com/markedjs/marked) (the only runtime dependency)
|
|
51
|
+
- **Compression**: [brotli-wasm](https://github.com/nicolo-ribaudo/brotli-wasm) (WebAssembly, loaded in browser)
|
|
52
|
+
- **Tests**: `node test/run.js` — custom red/green harness using Node `assert`
|
|
53
|
+
- **Browser tests**: [Playwright](https://playwright.dev/) (Chromium)
|
|
54
|
+
|
|
55
|
+
## Contributing
|
|
56
|
+
|
|
57
|
+
### Setup
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/JoshInLisbon/SDocs.git
|
|
61
|
+
cd SDocs
|
|
62
|
+
npm install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Run locally
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node server.js # http://localhost:3000
|
|
69
|
+
PORT=8080 node server.js # custom port
|
|
70
|
+
SDOCS_DEV=1 node server.js # dev mode: no-cache headers, service worker disabled
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`SDOCS_DEV=1` (or `NODE_ENV=development`) disables browser caching for CSS/JS and tells the app to unregister the service worker and clear its caches on load, so edits to frontend code are picked up instantly without hard-refreshing. Leave it unset in production.
|
|
74
|
+
|
|
75
|
+
### Run tests
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node test/run.js # unit + integration tests
|
|
79
|
+
npx playwright test test/write-mode.spec.js # browser tests (needs Chromium)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Point the CLI at your local server
|
|
83
|
+
|
|
84
|
+
By default the CLI opens URLs on `https://sdocs.dev`. When developing, point it at your local instance:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Per-command
|
|
88
|
+
sdoc README.md --url http://localhost:3000
|
|
89
|
+
sdoc share README.md --url http://localhost:3000
|
|
90
|
+
|
|
91
|
+
# Or set once for your session
|
|
92
|
+
export SDOCS_URL=http://localhost:3000
|
|
93
|
+
sdoc README.md
|
|
94
|
+
sdoc share README.md
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Project structure
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
server.js # Node.js static file server (~60 lines)
|
|
101
|
+
bin/sdocs-dev.js # CLI entry point
|
|
102
|
+
public/
|
|
103
|
+
index.html # Single HTML file (markup only)
|
|
104
|
+
css/ # Modular CSS (tokens, layout, rendered, panel, mobile)
|
|
105
|
+
sdocs-yaml.js # YAML front matter parser (UMD, shared with Node)
|
|
106
|
+
sdocs-styles.js # Style data tables + logic (UMD, shared with tests)
|
|
107
|
+
sdocs-state.js # Shared mutable state namespace (window.SDocs)
|
|
108
|
+
sdocs-theme.js # Fonts, dark mode, theme toggle
|
|
109
|
+
sdocs-controls.js # CSS variable management, color cascade
|
|
110
|
+
sdocs-export.js # PDF/Word/MD export
|
|
111
|
+
sdocs-write.js # Write mode (contentEditable)
|
|
112
|
+
sdocs-app.js # Core app: render, sync, modes, compression, init
|
|
113
|
+
brotli-wasm-v1.js # Brotli WASM wrapper (IIFE)
|
|
114
|
+
brotli_wasm_bg.wasm # Brotli WebAssembly binary
|
|
115
|
+
vendor/marked.min.js # Markdown parser
|
|
116
|
+
sw.js # Service worker (stale-while-revalidate + version check)
|
|
117
|
+
sdoc.md # Landing page content
|
|
118
|
+
test/
|
|
119
|
+
run.js # Test runner entry point
|
|
120
|
+
runner.js # Shared test harness
|
|
121
|
+
test-yaml.js # YAML parser tests
|
|
122
|
+
test-styles.js # Style system tests (including stripStyleDefaults)
|
|
123
|
+
test-cli.js # CLI argument parsing + URL building tests
|
|
124
|
+
test-slugify.js # Slugify + heading dedup tests
|
|
125
|
+
test-base64.js # Base64 UTF-8 roundtrip tests
|
|
126
|
+
test-files.js # File existence + content assertions
|
|
127
|
+
test-http.js # HTTP server tests
|
|
128
|
+
write-mode.spec.js # Playwright write mode tests
|
|
129
|
+
sample.smd # Test fixture (styled markdown)
|
|
130
|
+
bench-compression.js # Compression benchmark (deflate vs brotli)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Architecture notes
|
|
134
|
+
|
|
135
|
+
The app is entirely stateless. All state lives in `window.SDocs` in the browser. There is no build step — all JS loads as plain `<script>` tags. Modules that need to run in both the browser and Node (for tests) use a UMD IIFE pattern.
|
|
136
|
+
|
|
137
|
+
Styles are driven by CSS custom properties on `#rendered`. Every control in the style panel maps to a `--md-*` CSS variable. When exporting, `collectStyles()` reads the current control values from the DOM.
|
|
138
|
+
|
|
139
|
+
### Guidelines
|
|
140
|
+
|
|
141
|
+
- No build step. All browser JS must work as plain `<script>` tags.
|
|
142
|
+
- One runtime dependency (`marked`). Add dependencies only when there's no reasonable alternative.
|
|
143
|
+
- Keep the server simple. It serves static files — no API routes, no database.
|
|
144
|
+
- Test everything that's testable without a browser in `test/run.js`. Use Playwright for browser-specific behavior.
|
|
145
|
+
- Style properties that match defaults should be omitted (see `stripStyleDefaults` in `sdocs-styles.js`).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](LICENSE)
|
package/bin/sdocs-dev.js
CHANGED
|
@@ -75,6 +75,7 @@ USAGE
|
|
|
75
75
|
sdoc share <file> Copy shareable link to clipboard
|
|
76
76
|
sdoc share <file> --section "X" Link with section anchor
|
|
77
77
|
sdoc schema Print the full styles schema
|
|
78
|
+
sdoc charts Chart types, options, and styling guide
|
|
78
79
|
sdoc defaults Show ~/.sdocs/styles.yaml
|
|
79
80
|
sdoc defaults --reset Remove default styles
|
|
80
81
|
sdoc help Show this help
|
|
@@ -97,6 +98,20 @@ OPTIONS
|
|
|
97
98
|
ENVIRONMENT
|
|
98
99
|
SDOCS_URL Fallback base URL if --url is not passed.
|
|
99
100
|
|
|
101
|
+
FILE INFO CARD
|
|
102
|
+
When you \`sdoc <file>\`, the browser shows a small info card
|
|
103
|
+
above the document with:
|
|
104
|
+
file The filename — included in the share URL.
|
|
105
|
+
path Relative path from the cwd — local only.
|
|
106
|
+
fullPath Absolute path on your machine — local only.
|
|
107
|
+
|
|
108
|
+
Local fields (path, fullPath) are passed to the browser via a
|
|
109
|
+
separate URL parameter that JS reads into memory and then strips
|
|
110
|
+
from the address bar on load. They never appear in any URL the
|
|
111
|
+
user can copy, and \`sdoc share <file>\` never includes them in
|
|
112
|
+
the generated link. If someone opens your shared URL, only
|
|
113
|
+
\`file\` is visible.
|
|
114
|
+
|
|
100
115
|
STYLED MARKDOWN FORMAT
|
|
101
116
|
SDocs extends standard .md files with an optional YAML
|
|
102
117
|
front matter block (the same standard used by Jekyll, Hugo, Obsidian).
|
|
@@ -114,7 +129,11 @@ STYLED MARKDOWN FORMAT
|
|
|
114
129
|
# My Document
|
|
115
130
|
Content here...
|
|
116
131
|
|
|
132
|
+
Colors work in both themes automatically — dark mode versions
|
|
133
|
+
are generated by inverting lightness. Use \`dark:\` to override.
|
|
134
|
+
|
|
117
135
|
Run \`sdoc schema\` for the complete list of style properties.
|
|
136
|
+
Run \`sdoc charts\` for chart types, options, and styling.
|
|
118
137
|
`;
|
|
119
138
|
|
|
120
139
|
const SCHEMA = `
|
|
@@ -183,35 +202,66 @@ BLOCKQUOTE
|
|
|
183
202
|
background string Quote background color. Default: "#f7f5f2"
|
|
184
203
|
color string Quote text color. Default: "#6b6560"
|
|
185
204
|
|
|
205
|
+
BLOCKS (shared styling for code, blockquote, and chart blocks)
|
|
206
|
+
blocks:
|
|
207
|
+
background string Background for all block types. Cascades to code,
|
|
208
|
+
blockquote, and chart backgrounds unless overridden.
|
|
209
|
+
color string Text color for all block types. Cascades to code,
|
|
210
|
+
blockquote, and chart text unless overridden.
|
|
211
|
+
|
|
212
|
+
CHARTS (see also: \`sdoc charts\` for the full chart reference)
|
|
213
|
+
chart:
|
|
214
|
+
accent string Palette base color (hex). Default: "#3b82f6"
|
|
215
|
+
palette string Palette mode. Default: "monochrome"
|
|
216
|
+
Options: monochrome, complementary, analogous, triadic,
|
|
217
|
+
pastel, warm, cool, earth
|
|
218
|
+
background string Chart background. Default: inherits blocks.background
|
|
219
|
+
textColor string Chart labels/axes. Default: inherits blocks.color
|
|
220
|
+
|
|
186
221
|
COLOR CASCADE
|
|
187
222
|
Colors cascade from general → specific:
|
|
188
223
|
color → headers.color → h1.color, h2.color, h3.color, h4.color
|
|
189
224
|
color → p.color → list.color
|
|
225
|
+
blocks.background → code.background, blockquote.background, chart.background
|
|
226
|
+
blocks.color → code.color, blockquote.color, chart.textColor
|
|
190
227
|
Set a child color only when you want it to differ from its parent.
|
|
191
228
|
|
|
192
229
|
THEME COLORS
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
Top-level colors are light-mode colors. Dark mode is auto-generated
|
|
231
|
+
by inverting lightness (same hue, flipped brightness). Light backgrounds
|
|
232
|
+
become dark, dark text becomes light. Colors already very dark (like a
|
|
233
|
+
dark code block background) are kept as-is.
|
|
234
|
+
|
|
235
|
+
This means you only need to specify colors ONCE:
|
|
196
236
|
|
|
197
237
|
---
|
|
198
238
|
styles:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
color: "#2d1810"
|
|
240
|
+
background: "#fdf6f0"
|
|
241
|
+
headers: { color: "#8b2500" }
|
|
242
|
+
blocks:
|
|
243
|
+
background: "#f5e6d8"
|
|
244
|
+
color: "#5a3e2e"
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
Dark mode will automatically get inverted versions of all colors above.
|
|
248
|
+
|
|
249
|
+
To override specific dark-mode colors, add a \`dark:\` block:
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
styles:
|
|
253
|
+
color: "#2d1810"
|
|
254
|
+
background: "#fdf6f0"
|
|
255
|
+
blocks:
|
|
256
|
+
background: "#f5e6d8"
|
|
206
257
|
dark:
|
|
207
|
-
background: "#
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
link: { color: "#60a5fa" }
|
|
258
|
+
background: "#1a1210"
|
|
259
|
+
blocks:
|
|
260
|
+
background: "#2a1a1a"
|
|
211
261
|
---
|
|
212
262
|
|
|
213
|
-
|
|
214
|
-
|
|
263
|
+
Non-color properties (fonts, sizes, spacing, weights) remain at the
|
|
264
|
+
top level and are shared across both themes.
|
|
215
265
|
|
|
216
266
|
FONTS (24 supported, loaded lazily from Google Fonts)
|
|
217
267
|
Inter · Roboto · Open Sans · Lato · Montserrat · Source Sans 3
|
|
@@ -224,29 +274,190 @@ EXAMPLE — editorial article with colored heading tiers
|
|
|
224
274
|
styles:
|
|
225
275
|
fontFamily: Lora
|
|
226
276
|
baseFontSize: 17
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
277
|
+
background: "#fffaf5"
|
|
278
|
+
color: "#1a1a2e"
|
|
279
|
+
h1: { fontSize: 2.3, fontWeight: 700, color: "#c0392b" }
|
|
280
|
+
h2: { fontSize: 1.55, fontWeight: 600, color: "#8e44ad" }
|
|
281
|
+
h3: { fontSize: 1.2, fontWeight: 600, color: "#16a085" }
|
|
230
282
|
p: { lineHeight: 1.9, marginBottom: 1.2 }
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
h1: { color: "#c0392b" }
|
|
236
|
-
h2: { color: "#8e44ad" }
|
|
237
|
-
h3: { color: "#16a085" }
|
|
238
|
-
link: { color: "#e67e22", decoration: "underline" }
|
|
239
|
-
blockquote: { borderColor: "#c0392b", background: "#faf0eb", color: "#7f8c8d" }
|
|
283
|
+
link: { color: "#e67e22" }
|
|
284
|
+
blocks:
|
|
285
|
+
background: "#faf0eb"
|
|
286
|
+
blockquote: { borderColor: "#c0392b", color: "#7f8c8d" }
|
|
240
287
|
dark:
|
|
241
288
|
background: "#1a1520"
|
|
242
|
-
color: "#e7e5e2"
|
|
243
|
-
headers: { color: "#b0aaa5" }
|
|
244
289
|
h1: { color: "#ef6f5e" }
|
|
245
290
|
h2: { color: "#c490e4" }
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
blockquote: { borderColor: "#ef6f5e" }
|
|
292
|
+
---
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
const CHARTS_HELP = `
|
|
296
|
+
SDocs — Charts
|
|
297
|
+
==============
|
|
298
|
+
Render beautiful charts in markdown using \`\`\`chart code blocks.
|
|
299
|
+
Charts are powered by Chart.js, loaded lazily from CDN only when needed.
|
|
300
|
+
|
|
301
|
+
BASIC SYNTAX
|
|
302
|
+
Wrap a JSON object in a \`\`\`chart fenced code block:
|
|
303
|
+
|
|
304
|
+
\`\`\`chart
|
|
305
|
+
{
|
|
306
|
+
"type": "bar",
|
|
307
|
+
"title": "Monthly Revenue",
|
|
308
|
+
"labels": ["Jan", "Feb", "Mar"],
|
|
309
|
+
"values": [100, 150, 130]
|
|
310
|
+
}
|
|
311
|
+
\`\`\`
|
|
312
|
+
|
|
313
|
+
CHART TYPES
|
|
314
|
+
pie Circular segments (use "color" for monochrome shading)
|
|
315
|
+
doughnut Hollow-center pie (alias: donut)
|
|
316
|
+
bar Vertical bars
|
|
317
|
+
horizontal_bar Horizontal bars (alias: hbar)
|
|
318
|
+
stacked_bar Stacked vertical bars
|
|
319
|
+
line Line graph with data points
|
|
320
|
+
area Line with filled area beneath
|
|
321
|
+
stacked_area Multiple filled areas stacked (alias: stacked_line)
|
|
322
|
+
radar Spider/web chart for multi-axis comparison
|
|
323
|
+
polarArea Like pie but equal angles, varying radius
|
|
324
|
+
scatter X/Y point plots
|
|
325
|
+
bubble Like scatter with size dimension
|
|
326
|
+
mixed Combo chart — bar + line on same plot (alias: combo)
|
|
327
|
+
|
|
328
|
+
DATA FORMATS
|
|
329
|
+
Simple (single dataset):
|
|
330
|
+
"labels": ["A", "B", "C"],
|
|
331
|
+
"values": [10, 20, 15]
|
|
332
|
+
|
|
333
|
+
Multi-dataset:
|
|
334
|
+
"labels": ["Q1", "Q2"],
|
|
335
|
+
"datasets": [
|
|
336
|
+
{ "label": "2024", "values": [10, 20] },
|
|
337
|
+
{ "label": "2025", "values": [12, 25] }
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
Scatter/Bubble:
|
|
341
|
+
"datasets": [
|
|
342
|
+
{ "label": "Group", "data": [{"x": 1, "y": 2}, {"x": 3, "y": 5}] }
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
CHART OPTIONS
|
|
346
|
+
title string Chart heading
|
|
347
|
+
subtitle string Smaller text below title
|
|
348
|
+
labels string[] Category labels
|
|
349
|
+
values number[] Data for a single dataset
|
|
350
|
+
datasets array Multiple datasets (see above)
|
|
351
|
+
color string Single accent color (hex)
|
|
352
|
+
colors string[] Per-segment/bar custom colors
|
|
353
|
+
|
|
354
|
+
AXIS OPTIONS
|
|
355
|
+
xAxis / xLabel string X-axis label
|
|
356
|
+
yAxis / yLabel string Y-axis label
|
|
357
|
+
y2Axis string Right y-axis label (enables dual axis)
|
|
358
|
+
min number Minimum value on value axis
|
|
359
|
+
max number Maximum value on value axis
|
|
360
|
+
stepSize number Tick interval
|
|
361
|
+
beginAtZero boolean Default true. Set false for auto-range.
|
|
362
|
+
|
|
363
|
+
NUMBER FORMATTING
|
|
364
|
+
format string "currency" ($), "euro" (€), "pound" (£),
|
|
365
|
+
"percent" (%), "comma" (1,000)
|
|
366
|
+
prefix string Custom value prefix (e.g. "£")
|
|
367
|
+
suffix string Custom value suffix (e.g. " kg", "°C")
|
|
368
|
+
y2Format string Format for right y-axis
|
|
369
|
+
y2Prefix string Prefix for right y-axis
|
|
370
|
+
y2Suffix string Suffix for right y-axis
|
|
371
|
+
|
|
372
|
+
DISPLAY OPTIONS
|
|
373
|
+
legend boolean Show/hide legend (auto by default)
|
|
374
|
+
legendPosition string "top", "bottom" (default), "left", "right"
|
|
375
|
+
dataLabels boolean Show values on chart (default true). Set false for clean look.
|
|
376
|
+
aspectRatio number Width/height ratio (e.g. 2 for wide, 0.8 for tall)
|
|
377
|
+
stacked boolean Force stacking on bar/line charts
|
|
378
|
+
|
|
379
|
+
DATASET OPTIONS (inside each dataset object)
|
|
380
|
+
label string Name shown in legend
|
|
381
|
+
values number[] Data points
|
|
382
|
+
data object[] For scatter: [{x, y}], for bubble: [{x, y, r}]
|
|
383
|
+
color string Dataset color (hex)
|
|
384
|
+
colors string[] Per-bar colors within dataset
|
|
385
|
+
type string Override type in mixed charts ("bar" or "line")
|
|
386
|
+
yAxisID string "y" (left) or "y2" (right) for dual-axis charts
|
|
387
|
+
fill boolean Fill area under line
|
|
388
|
+
tension number Line smoothing (0 = straight, 0.4 = smooth)
|
|
389
|
+
order number Draw order (lower = rendered on top)
|
|
390
|
+
|
|
391
|
+
ANNOTATIONS (reference lines)
|
|
392
|
+
"annotations": [
|
|
393
|
+
{ "y": 60, "label": "Target", "color": "#ef4444" },
|
|
394
|
+
{ "x": "Mar", "label": "Launch", "dashed": true }
|
|
395
|
+
]
|
|
396
|
+
|
|
397
|
+
y / x number/string Position of the reference line
|
|
398
|
+
label string Text label on the line
|
|
399
|
+
color string Line color
|
|
400
|
+
width number Line thickness (default 2)
|
|
401
|
+
dashed boolean Dashed style (default true)
|
|
402
|
+
position string Label position: "start", "center", "end"
|
|
403
|
+
|
|
404
|
+
CHART STYLING (via front matter or style panel)
|
|
405
|
+
Charts inherit background and text colors from the block cascade:
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
styles:
|
|
409
|
+
blocks:
|
|
410
|
+
background: "#1a1a2e" # all blocks: code, blockquote, charts
|
|
411
|
+
color: "#c8c3bc" # text in all blocks
|
|
412
|
+
chart:
|
|
413
|
+
accent: "#6366f1" # palette base color
|
|
414
|
+
palette: monochrome # palette generation mode
|
|
415
|
+
background: "#0e4a1a" # override blocks.background for charts only
|
|
416
|
+
textColor: "#c8f0d8" # override blocks.color for charts only
|
|
249
417
|
---
|
|
418
|
+
|
|
419
|
+
COLOR CASCADE FOR BLOCKS
|
|
420
|
+
blocks.background → code.background, blockquote.background, chart.background
|
|
421
|
+
blocks.color → code.color, blockquote.color, chart.textColor
|
|
422
|
+
Set a child value only when you want it to differ from the parent.
|
|
423
|
+
|
|
424
|
+
DARK MODE
|
|
425
|
+
All colors auto-generate dark-mode counterparts (lightness inverted).
|
|
426
|
+
Add a \`dark:\` block to override specific values:
|
|
427
|
+
dark:
|
|
428
|
+
blocks:
|
|
429
|
+
background: "#2a1a1a"
|
|
430
|
+
|
|
431
|
+
PALETTE MODES
|
|
432
|
+
monochrome Same hue, varying lightness (default)
|
|
433
|
+
complementary Hues spread evenly around the color wheel
|
|
434
|
+
analogous Neighboring hues for a harmonious feel
|
|
435
|
+
triadic Three base hues 120° apart
|
|
436
|
+
pastel Soft, light colors
|
|
437
|
+
warm Reds, oranges, yellows
|
|
438
|
+
cool Blues, teals, purples
|
|
439
|
+
earth Browns, olives, muted greens
|
|
440
|
+
|
|
441
|
+
Per-chart override: set "accent" and/or "palette" directly in the chart JSON.
|
|
442
|
+
Per-chart colors: set "colors": ["#hex", ...] to override the palette entirely.
|
|
443
|
+
Single-color pie: set "color": "#hex" on a pie/doughnut for monochrome shading.
|
|
444
|
+
|
|
445
|
+
MIXED CHART EXAMPLE (dual y-axis)
|
|
446
|
+
\`\`\`chart
|
|
447
|
+
{
|
|
448
|
+
"type": "mixed",
|
|
449
|
+
"title": "Revenue vs Growth",
|
|
450
|
+
"labels": ["Q1", "Q2", "Q3", "Q4"],
|
|
451
|
+
"datasets": [
|
|
452
|
+
{ "label": "Revenue", "type": "bar", "values": [50, 65, 80, 95], "yAxisID": "y" },
|
|
453
|
+
{ "label": "Growth", "type": "line", "values": [12, 30, 23, 19], "yAxisID": "y2" }
|
|
454
|
+
],
|
|
455
|
+
"yAxis": "Revenue ($M)",
|
|
456
|
+
"y2Axis": "Growth %",
|
|
457
|
+
"format": "currency",
|
|
458
|
+
"y2Format": "percent"
|
|
459
|
+
}
|
|
460
|
+
\`\`\`
|
|
250
461
|
`;
|
|
251
462
|
|
|
252
463
|
// ── Compression (brotli + base64url) ─────────────────
|
|
@@ -282,15 +493,13 @@ function decompressFromBase64Url(b64url) {
|
|
|
282
493
|
}
|
|
283
494
|
}
|
|
284
495
|
|
|
285
|
-
// ── Slugify
|
|
496
|
+
// ── Slugify (shared module) ───────────────────────────────
|
|
286
497
|
|
|
287
|
-
|
|
288
|
-
return text.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
289
|
-
}
|
|
498
|
+
var slugify = require('../public/sdocs-slugify').slugify;
|
|
290
499
|
|
|
291
500
|
// ── Parse args ────────────────────────────────────────────
|
|
292
501
|
|
|
293
|
-
const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help']);
|
|
502
|
+
const SUBCOMMANDS = new Set(['new', 'share', 'schema', 'defaults', 'help', 'charts']);
|
|
294
503
|
|
|
295
504
|
function parseArgs(argv) {
|
|
296
505
|
const args = argv || process.argv.slice(2);
|
|
@@ -354,6 +563,15 @@ function buildUrl(content, opts) {
|
|
|
354
563
|
const baseUrl = opts.url || process.env.SDOCS_URL || DEFAULT_URL;
|
|
355
564
|
const params = new URLSearchParams();
|
|
356
565
|
|
|
566
|
+
// Runtime-only metadata (paths). Stripped from the URL by the browser on load,
|
|
567
|
+
// so anything the user copies from the address bar won't contain them.
|
|
568
|
+
if (opts.local && Object.keys(opts.local).length > 0) {
|
|
569
|
+
const json = JSON.stringify(opts.local);
|
|
570
|
+
const b64 = Buffer.from(json, 'utf-8').toString('base64')
|
|
571
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
572
|
+
params.set('local', b64);
|
|
573
|
+
}
|
|
574
|
+
|
|
357
575
|
if (content) {
|
|
358
576
|
// Strip default style values to produce shorter URLs
|
|
359
577
|
const parsed = SDocYaml.parseFrontMatter(content);
|
|
@@ -510,6 +728,7 @@ if (require.main === module) {
|
|
|
510
728
|
// Subcommand dispatch
|
|
511
729
|
if (opts.subcommand === 'help') { console.log(HELP); process.exit(0); }
|
|
512
730
|
if (opts.subcommand === 'schema') { console.log(SCHEMA); process.exit(0); }
|
|
731
|
+
if (opts.subcommand === 'charts') { console.log(CHARTS_HELP); process.exit(0); }
|
|
513
732
|
if (opts.subcommand === 'defaults') {
|
|
514
733
|
if (opts.resetFlag) resetDefaults();
|
|
515
734
|
else showDefaults();
|
|
@@ -532,12 +751,37 @@ if (require.main === module) {
|
|
|
532
751
|
content = applyDefaultStyles(content);
|
|
533
752
|
}
|
|
534
753
|
|
|
754
|
+
// Inject `file:` into front matter (basename only — safe to share).
|
|
755
|
+
// Respects user-set file: if already present.
|
|
756
|
+
if (content && opts.file) {
|
|
757
|
+
const parsed = parseFrontMatter(content);
|
|
758
|
+
if (!parsed.meta.file) {
|
|
759
|
+
parsed.meta.file = path.basename(opts.file);
|
|
760
|
+
content = serializeFrontMatter(parsed.meta) + '\n' + parsed.body;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Runtime-only local metadata for the opener's view.
|
|
765
|
+
// `share` omits it so shared URLs never carry paths.
|
|
766
|
+
let local = null;
|
|
767
|
+
if (opts.file && opts.subcommand !== 'share') {
|
|
768
|
+
const abs = path.resolve(opts.file);
|
|
769
|
+
const rel = path.relative(process.cwd(), abs);
|
|
770
|
+
local = { fullPath: abs };
|
|
771
|
+
// Only include a relative path if the file is inside cwd, otherwise
|
|
772
|
+
// `path` would just duplicate `fullPath`.
|
|
773
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel)) {
|
|
774
|
+
local.path = './' + rel;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
535
778
|
const url = buildUrl(content, {
|
|
536
779
|
url: opts.url,
|
|
537
780
|
mode: opts.mode,
|
|
538
781
|
theme: opts.theme,
|
|
539
782
|
defaultStyles: !content ? defaults : null,
|
|
540
783
|
section: opts.section,
|
|
784
|
+
local: local,
|
|
541
785
|
});
|
|
542
786
|
|
|
543
787
|
// Share: copy to clipboard
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdocs-dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Open, share, and style markdown files from the terminal",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
"markdown",
|
|
21
21
|
"editor",
|
|
22
22
|
"styling",
|
|
23
|
+
"charts",
|
|
23
24
|
"pdf",
|
|
24
25
|
"docx",
|
|
25
|
-
"cli"
|
|
26
|
+
"cli",
|
|
27
|
+
"agents"
|
|
26
28
|
],
|
|
27
29
|
"author": "Josh Summers",
|
|
28
30
|
"license": "MIT",
|
|
@@ -32,12 +34,13 @@
|
|
|
32
34
|
},
|
|
33
35
|
"homepage": "https://sdocs.dev",
|
|
34
36
|
"dependencies": {
|
|
37
|
+
"better-sqlite3": "^12.8.0",
|
|
35
38
|
"brotli": "^1.3.3",
|
|
36
39
|
"brotli-dec-wasm": "^2.3.2",
|
|
37
40
|
"brotli-wasm": "^3.0.1",
|
|
38
41
|
"marked": "^11.0.0"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
|
-
"@playwright/test": "^1.
|
|
44
|
+
"@playwright/test": "^1.59.1"
|
|
42
45
|
}
|
|
43
46
|
}
|
|
@@ -195,11 +195,11 @@ function isLikeNone(x) {
|
|
|
195
195
|
* `ResultFailure` is removed
|
|
196
196
|
* because we will convert the failure to an actual negative error code (if available) and pass it elsewhere.
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
const BrotliStreamResultCode = Object.freeze({ ResultSuccess:1,"1":"ResultSuccess",NeedsMoreInput:2,"2":"NeedsMoreInput",NeedsMoreOutput:3,"3":"NeedsMoreOutput", });
|
|
199
199
|
/**
|
|
200
200
|
* Returned by every successful (de)compression.
|
|
201
201
|
*/
|
|
202
|
-
|
|
202
|
+
class BrotliStreamResult {
|
|
203
203
|
|
|
204
204
|
static __wrap(ptr) {
|
|
205
205
|
const obj = Object.create(BrotliStreamResult.prototype);
|
|
@@ -283,7 +283,7 @@ export class BrotliStreamResult {
|
|
|
283
283
|
}
|
|
284
284
|
/**
|
|
285
285
|
*/
|
|
286
|
-
|
|
286
|
+
class CompressStream {
|
|
287
287
|
|
|
288
288
|
static __wrap(ptr) {
|
|
289
289
|
const obj = Object.create(CompressStream.prototype);
|
|
@@ -342,7 +342,7 @@ export class CompressStream {
|
|
|
342
342
|
}
|
|
343
343
|
/**
|
|
344
344
|
*/
|
|
345
|
-
|
|
345
|
+
class DecompressStream {
|
|
346
346
|
|
|
347
347
|
static __wrap(ptr) {
|
|
348
348
|
const obj = Object.create(DecompressStream.prototype);
|
|
@@ -432,7 +432,7 @@ async function load(module, imports) {
|
|
|
432
432
|
|
|
433
433
|
async function init(input) {
|
|
434
434
|
if (typeof input === 'undefined') {
|
|
435
|
-
input =
|
|
435
|
+
input = '/public/brotli_wasm_bg.wasm';
|
|
436
436
|
}
|
|
437
437
|
const imports = {};
|
|
438
438
|
imports.wbg = {};
|
|
@@ -503,7 +503,6 @@ async function init(input) {
|
|
|
503
503
|
|
|
504
504
|
|
|
505
505
|
|
|
506
|
-
|
|
507
506
|
// Auto-init: fetch WASM from same directory
|
|
508
507
|
var wasmUrl = '/public/brotli_wasm_bg.wasm';
|
|
509
508
|
window.BrotliWasm = {
|
package/public/css/layout.css
CHANGED
|
@@ -119,8 +119,8 @@ body {
|
|
|
119
119
|
|
|
120
120
|
.toggle-group {
|
|
121
121
|
display: flex;
|
|
122
|
-
background:
|
|
123
|
-
border:
|
|
122
|
+
background: none;
|
|
123
|
+
border: none;
|
|
124
124
|
border-radius: var(--radius);
|
|
125
125
|
overflow: hidden;
|
|
126
126
|
padding: 2px;
|
|
@@ -157,7 +157,6 @@ body {
|
|
|
157
157
|
gap: 4px;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
#btn-theme,
|
|
161
160
|
.toolbar-icon-btn {
|
|
162
161
|
display: inline-flex;
|
|
163
162
|
align-items: center;
|
|
@@ -174,7 +173,6 @@ body {
|
|
|
174
173
|
flex-shrink: 0;
|
|
175
174
|
}
|
|
176
175
|
@media (hover: hover) {
|
|
177
|
-
#btn-theme:hover,
|
|
178
176
|
.toolbar-icon-btn:hover {
|
|
179
177
|
color: var(--text-2);
|
|
180
178
|
background: var(--bg-hover);
|
|
@@ -186,6 +184,7 @@ body {
|
|
|
186
184
|
overflow: auto;
|
|
187
185
|
position: relative;
|
|
188
186
|
background-color: #FFFFFF;
|
|
187
|
+
outline: none;
|
|
189
188
|
}
|
|
190
189
|
html[data-theme="dark"] #content-area {
|
|
191
190
|
background-color: #2c2a26;
|