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 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
- Colors can be set per-theme using \`light:\` and \`dark:\` sub-blocks under \`styles:\`.
194
- Non-color properties (fonts, sizes, spacing, weights) remain at the top level
195
- and are shared across both themes.
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
- fontFamily: Lora
200
- baseFontSize: 17
201
- light:
202
- background: "#ffffff"
203
- color: "#1a1a2e"
204
- h1: { color: "#c0392b" }
205
- link: { color: "#2563eb" }
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: "#2c2a26"
208
- color: "#e7e5e2"
209
- h1: { color: "#ef6f5e" }
210
- link: { color: "#60a5fa" }
258
+ background: "#1a1210"
259
+ blocks:
260
+ background: "#2a1a1a"
211
261
  ---
212
262
 
213
- Top-level colors (old format) are treated as light-theme colors for
214
- backwards compatibility.
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
- h1: { fontSize: 2.3, fontWeight: 700 }
228
- h2: { fontSize: 1.55, fontWeight: 600 }
229
- h3: { fontSize: 1.2, fontWeight: 600 }
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
- light:
232
- background: "#fffaf5"
233
- color: "#1a1a2e"
234
- headers: { color: "#2c3e50" }
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
- h3: { color: "#5ed4b8" }
247
- link: { color: "#f0a860", decoration: "underline" }
248
- blockquote: { borderColor: "#ef6f5e", background: "#221a28", color: "#9e9590" }
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
- function slugify(text) {
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.2.0",
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.58.2"
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
- export const BrotliStreamResultCode = Object.freeze({ ResultSuccess:1,"1":"ResultSuccess",NeedsMoreInput:2,"2":"NeedsMoreInput",NeedsMoreOutput:3,"3":"NeedsMoreOutput", });
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
- export class BrotliStreamResult {
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
- export class CompressStream {
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
- export class DecompressStream {
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 = new URL('brotli_wasm_bg.wasm', import.meta.url);
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 = {
@@ -119,8 +119,8 @@ body {
119
119
 
120
120
  .toggle-group {
121
121
  display: flex;
122
- background: var(--bg-surface);
123
- border: 1px solid var(--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;