sdocs-dev 1.1.2 → 1.2.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/bin/sdocs-dev.js CHANGED
@@ -249,21 +249,37 @@ EXAMPLE — editorial article with colored heading tiers
249
249
  ---
250
250
  `;
251
251
 
252
- // ── Compression (deflate-raw + base64url) ─────────────────
252
+ // ── Compression (brotli + base64url) ─────────────────
253
253
 
254
- function compressToBase64Url(text) {
255
- const deflated = zlib.deflateRawSync(Buffer.from(text, 'utf-8'));
256
- return deflated.toString('base64')
254
+ function toBase64Url(buf) {
255
+ return Buffer.from(buf).toString('base64')
257
256
  .replace(/\+/g, '-')
258
257
  .replace(/\//g, '_')
259
258
  .replace(/=+$/, '');
260
259
  }
261
260
 
262
- function decompressFromBase64Url(b64url) {
261
+ function fromBase64Url(b64url) {
263
262
  let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
264
263
  const pad = (4 - b64.length % 4) % 4;
265
264
  b64 += '='.repeat(pad);
266
- return zlib.inflateRawSync(Buffer.from(b64, 'base64')).toString('utf-8');
265
+ return Buffer.from(b64, 'base64');
266
+ }
267
+
268
+ function compressToBase64Url(text) {
269
+ const compressed = zlib.brotliCompressSync(Buffer.from(text, 'utf-8'), {
270
+ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 }
271
+ });
272
+ return toBase64Url(compressed);
273
+ }
274
+
275
+ function decompressFromBase64Url(b64url) {
276
+ const buf = fromBase64Url(b64url);
277
+ // Try brotli first, fall back to deflate for old URLs
278
+ try {
279
+ return zlib.brotliDecompressSync(buf).toString('utf-8');
280
+ } catch (_) {
281
+ return zlib.inflateRawSync(buf).toString('utf-8');
282
+ }
267
283
  }
268
284
 
269
285
  // ── Slugify ───────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs-dev",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Open, share, and style markdown files from the terminal",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -32,6 +32,9 @@
32
32
  },
33
33
  "homepage": "https://sdocs.dev",
34
34
  "dependencies": {
35
+ "brotli": "^1.3.3",
36
+ "brotli-dec-wasm": "^2.3.2",
37
+ "brotli-wasm": "^3.0.1",
35
38
  "marked": "^11.0.0"
36
39
  },
37
40
  "devDependencies": {
@@ -0,0 +1,519 @@
1
+ // brotli-wasm IIFE wrapper (auto-generated from brotli-wasm npm package)
2
+ // Provides window.BrotliWasm.ready (Promise), .compress(Uint8Array), .decompress(Uint8Array)
3
+ (function() {
4
+ 'use strict';
5
+
6
+
7
+ let wasm;
8
+
9
+ const heap = new Array(32).fill(undefined);
10
+
11
+ heap.push(undefined, null, true, false);
12
+
13
+ function getObject(idx) { return heap[idx]; }
14
+
15
+ const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
16
+
17
+ cachedTextDecoder.decode();
18
+
19
+ let cachegetUint8Memory0 = null;
20
+ function getUint8Memory0() {
21
+ if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
22
+ cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
23
+ }
24
+ return cachegetUint8Memory0;
25
+ }
26
+
27
+ function getStringFromWasm0(ptr, len) {
28
+ return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
29
+ }
30
+
31
+ let heap_next = heap.length;
32
+
33
+ function addHeapObject(obj) {
34
+ if (heap_next === heap.length) heap.push(heap.length + 1);
35
+ const idx = heap_next;
36
+ heap_next = heap[idx];
37
+
38
+ heap[idx] = obj;
39
+ return idx;
40
+ }
41
+
42
+ let WASM_VECTOR_LEN = 0;
43
+
44
+ const cachedTextEncoder = new TextEncoder('utf-8');
45
+
46
+ const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
47
+ ? function (arg, view) {
48
+ return cachedTextEncoder.encodeInto(arg, view);
49
+ }
50
+ : function (arg, view) {
51
+ const buf = cachedTextEncoder.encode(arg);
52
+ view.set(buf);
53
+ return {
54
+ read: arg.length,
55
+ written: buf.length
56
+ };
57
+ });
58
+
59
+ function passStringToWasm0(arg, malloc, realloc) {
60
+
61
+ if (realloc === undefined) {
62
+ const buf = cachedTextEncoder.encode(arg);
63
+ const ptr = malloc(buf.length);
64
+ getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
65
+ WASM_VECTOR_LEN = buf.length;
66
+ return ptr;
67
+ }
68
+
69
+ let len = arg.length;
70
+ let ptr = malloc(len);
71
+
72
+ const mem = getUint8Memory0();
73
+
74
+ let offset = 0;
75
+
76
+ for (; offset < len; offset++) {
77
+ const code = arg.charCodeAt(offset);
78
+ if (code > 0x7F) break;
79
+ mem[ptr + offset] = code;
80
+ }
81
+
82
+ if (offset !== len) {
83
+ if (offset !== 0) {
84
+ arg = arg.slice(offset);
85
+ }
86
+ ptr = realloc(ptr, len, len = offset + arg.length * 3);
87
+ const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
88
+ const ret = encodeString(arg, view);
89
+
90
+ offset += ret.written;
91
+ }
92
+
93
+ WASM_VECTOR_LEN = offset;
94
+ return ptr;
95
+ }
96
+
97
+ let cachegetInt32Memory0 = null;
98
+ function getInt32Memory0() {
99
+ if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
100
+ cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
101
+ }
102
+ return cachegetInt32Memory0;
103
+ }
104
+
105
+ function dropObject(idx) {
106
+ if (idx < 36) return;
107
+ heap[idx] = heap_next;
108
+ heap_next = idx;
109
+ }
110
+
111
+ function takeObject(idx) {
112
+ const ret = getObject(idx);
113
+ dropObject(idx);
114
+ return ret;
115
+ }
116
+
117
+ function passArray8ToWasm0(arg, malloc) {
118
+ const ptr = malloc(arg.length * 1);
119
+ getUint8Memory0().set(arg, ptr / 1);
120
+ WASM_VECTOR_LEN = arg.length;
121
+ return ptr;
122
+ }
123
+
124
+ let stack_pointer = 32;
125
+
126
+ function addBorrowedObject(obj) {
127
+ if (stack_pointer == 1) throw new Error('out of js stack');
128
+ heap[--stack_pointer] = obj;
129
+ return stack_pointer;
130
+ }
131
+
132
+ function getArrayU8FromWasm0(ptr, len) {
133
+ return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
134
+ }
135
+ /**
136
+ * @param {Uint8Array} buf
137
+ * @param {any} raw_options
138
+ * @returns {Uint8Array}
139
+ */
140
+ function compress(buf, raw_options) {
141
+ try {
142
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
143
+ const ptr0 = passArray8ToWasm0(buf, wasm.__wbindgen_malloc);
144
+ const len0 = WASM_VECTOR_LEN;
145
+ wasm.compress(retptr, ptr0, len0, addBorrowedObject(raw_options));
146
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
147
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
148
+ var r2 = getInt32Memory0()[retptr / 4 + 2];
149
+ var r3 = getInt32Memory0()[retptr / 4 + 3];
150
+ if (r3) {
151
+ throw takeObject(r2);
152
+ }
153
+ var v1 = getArrayU8FromWasm0(r0, r1).slice();
154
+ wasm.__wbindgen_free(r0, r1 * 1);
155
+ return v1;
156
+ } finally {
157
+ wasm.__wbindgen_add_to_stack_pointer(16);
158
+ heap[stack_pointer++] = undefined;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * @param {Uint8Array} buf
164
+ * @returns {Uint8Array}
165
+ */
166
+ function decompress(buf) {
167
+ try {
168
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
169
+ const ptr0 = passArray8ToWasm0(buf, wasm.__wbindgen_malloc);
170
+ const len0 = WASM_VECTOR_LEN;
171
+ wasm.decompress(retptr, ptr0, len0);
172
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
173
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
174
+ var r2 = getInt32Memory0()[retptr / 4 + 2];
175
+ var r3 = getInt32Memory0()[retptr / 4 + 3];
176
+ if (r3) {
177
+ throw takeObject(r2);
178
+ }
179
+ var v1 = getArrayU8FromWasm0(r0, r1).slice();
180
+ wasm.__wbindgen_free(r0, r1 * 1);
181
+ return v1;
182
+ } finally {
183
+ wasm.__wbindgen_add_to_stack_pointer(16);
184
+ }
185
+ }
186
+
187
+ function isLikeNone(x) {
188
+ return x === undefined || x === null;
189
+ }
190
+ /**
191
+ * Same as [`brotli::BrotliResult`] except [`brotli::BrotliResult::ResultFailure`].
192
+ *
193
+ * Always `> 0`.
194
+ *
195
+ * `ResultFailure` is removed
196
+ * because we will convert the failure to an actual negative error code (if available) and pass it elsewhere.
197
+ */
198
+ export const BrotliStreamResultCode = Object.freeze({ ResultSuccess:1,"1":"ResultSuccess",NeedsMoreInput:2,"2":"NeedsMoreInput",NeedsMoreOutput:3,"3":"NeedsMoreOutput", });
199
+ /**
200
+ * Returned by every successful (de)compression.
201
+ */
202
+ export class BrotliStreamResult {
203
+
204
+ static __wrap(ptr) {
205
+ const obj = Object.create(BrotliStreamResult.prototype);
206
+ obj.ptr = ptr;
207
+
208
+ return obj;
209
+ }
210
+
211
+ __destroy_into_raw() {
212
+ const ptr = this.ptr;
213
+ this.ptr = 0;
214
+
215
+ return ptr;
216
+ }
217
+
218
+ free() {
219
+ const ptr = this.__destroy_into_raw();
220
+ wasm.__wbg_brotlistreamresult_free(ptr);
221
+ }
222
+ /**
223
+ * Result code.
224
+ *
225
+ * See [`BrotliStreamResultCode`] for available values.
226
+ *
227
+ * When error, the error code is not passed here but rather goes to `Err`.
228
+ */
229
+ get code() {
230
+ const ret = wasm.__wbg_get_brotlistreamresult_code(this.ptr);
231
+ return ret >>> 0;
232
+ }
233
+ /**
234
+ * Result code.
235
+ *
236
+ * See [`BrotliStreamResultCode`] for available values.
237
+ *
238
+ * When error, the error code is not passed here but rather goes to `Err`.
239
+ * @param {number} arg0
240
+ */
241
+ set code(arg0) {
242
+ wasm.__wbg_set_brotlistreamresult_code(this.ptr, arg0);
243
+ }
244
+ /**
245
+ * Output buffer
246
+ */
247
+ get buf() {
248
+ try {
249
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
250
+ wasm.__wbg_get_brotlistreamresult_buf(retptr, this.ptr);
251
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
252
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
253
+ var v0 = getArrayU8FromWasm0(r0, r1).slice();
254
+ wasm.__wbindgen_free(r0, r1 * 1);
255
+ return v0;
256
+ } finally {
257
+ wasm.__wbindgen_add_to_stack_pointer(16);
258
+ }
259
+ }
260
+ /**
261
+ * Output buffer
262
+ * @param {Uint8Array} arg0
263
+ */
264
+ set buf(arg0) {
265
+ const ptr0 = passArray8ToWasm0(arg0, wasm.__wbindgen_malloc);
266
+ const len0 = WASM_VECTOR_LEN;
267
+ wasm.__wbg_set_brotlistreamresult_buf(this.ptr, ptr0, len0);
268
+ }
269
+ /**
270
+ * Consumed bytes of the input buffer
271
+ */
272
+ get input_offset() {
273
+ const ret = wasm.__wbg_get_brotlistreamresult_input_offset(this.ptr);
274
+ return ret >>> 0;
275
+ }
276
+ /**
277
+ * Consumed bytes of the input buffer
278
+ * @param {number} arg0
279
+ */
280
+ set input_offset(arg0) {
281
+ wasm.__wbg_set_brotlistreamresult_input_offset(this.ptr, arg0);
282
+ }
283
+ }
284
+ /**
285
+ */
286
+ export class CompressStream {
287
+
288
+ static __wrap(ptr) {
289
+ const obj = Object.create(CompressStream.prototype);
290
+ obj.ptr = ptr;
291
+
292
+ return obj;
293
+ }
294
+
295
+ __destroy_into_raw() {
296
+ const ptr = this.ptr;
297
+ this.ptr = 0;
298
+
299
+ return ptr;
300
+ }
301
+
302
+ free() {
303
+ const ptr = this.__destroy_into_raw();
304
+ wasm.__wbg_compressstream_free(ptr);
305
+ }
306
+ /**
307
+ * @param {number | undefined} quality
308
+ */
309
+ constructor(quality) {
310
+ const ret = wasm.compressstream_new(!isLikeNone(quality), isLikeNone(quality) ? 0 : quality);
311
+ return CompressStream.__wrap(ret);
312
+ }
313
+ /**
314
+ * @param {Uint8Array | undefined} input_opt
315
+ * @param {number} output_size
316
+ * @returns {BrotliStreamResult}
317
+ */
318
+ compress(input_opt, output_size) {
319
+ try {
320
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
321
+ var ptr0 = isLikeNone(input_opt) ? 0 : passArray8ToWasm0(input_opt, wasm.__wbindgen_malloc);
322
+ var len0 = WASM_VECTOR_LEN;
323
+ wasm.compressstream_compress(retptr, this.ptr, ptr0, len0, output_size);
324
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
325
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
326
+ var r2 = getInt32Memory0()[retptr / 4 + 2];
327
+ if (r2) {
328
+ throw takeObject(r1);
329
+ }
330
+ return BrotliStreamResult.__wrap(r0);
331
+ } finally {
332
+ wasm.__wbindgen_add_to_stack_pointer(16);
333
+ }
334
+ }
335
+ /**
336
+ * @returns {number}
337
+ */
338
+ total_out() {
339
+ const ret = wasm.compressstream_total_out(this.ptr);
340
+ return ret >>> 0;
341
+ }
342
+ }
343
+ /**
344
+ */
345
+ export class DecompressStream {
346
+
347
+ static __wrap(ptr) {
348
+ const obj = Object.create(DecompressStream.prototype);
349
+ obj.ptr = ptr;
350
+
351
+ return obj;
352
+ }
353
+
354
+ __destroy_into_raw() {
355
+ const ptr = this.ptr;
356
+ this.ptr = 0;
357
+
358
+ return ptr;
359
+ }
360
+
361
+ free() {
362
+ const ptr = this.__destroy_into_raw();
363
+ wasm.__wbg_decompressstream_free(ptr);
364
+ }
365
+ /**
366
+ */
367
+ constructor() {
368
+ const ret = wasm.decompressstream_new();
369
+ return DecompressStream.__wrap(ret);
370
+ }
371
+ /**
372
+ * @param {Uint8Array} input
373
+ * @param {number} output_size
374
+ * @returns {BrotliStreamResult}
375
+ */
376
+ decompress(input, output_size) {
377
+ try {
378
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
379
+ const ptr0 = passArray8ToWasm0(input, wasm.__wbindgen_malloc);
380
+ const len0 = WASM_VECTOR_LEN;
381
+ wasm.decompressstream_decompress(retptr, this.ptr, ptr0, len0, output_size);
382
+ var r0 = getInt32Memory0()[retptr / 4 + 0];
383
+ var r1 = getInt32Memory0()[retptr / 4 + 1];
384
+ var r2 = getInt32Memory0()[retptr / 4 + 2];
385
+ if (r2) {
386
+ throw takeObject(r1);
387
+ }
388
+ return BrotliStreamResult.__wrap(r0);
389
+ } finally {
390
+ wasm.__wbindgen_add_to_stack_pointer(16);
391
+ }
392
+ }
393
+ /**
394
+ * @returns {number}
395
+ */
396
+ total_out() {
397
+ const ret = wasm.decompressstream_total_out(this.ptr);
398
+ return ret >>> 0;
399
+ }
400
+ }
401
+
402
+ async function load(module, imports) {
403
+ if (typeof Response === 'function' && module instanceof Response) {
404
+ if (typeof WebAssembly.instantiateStreaming === 'function') {
405
+ try {
406
+ return await WebAssembly.instantiateStreaming(module, imports);
407
+
408
+ } catch (e) {
409
+ if (module.headers.get('Content-Type') != 'application/wasm') {
410
+ console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
411
+
412
+ } else {
413
+ throw e;
414
+ }
415
+ }
416
+ }
417
+
418
+ const bytes = await module.arrayBuffer();
419
+ return await WebAssembly.instantiate(bytes, imports);
420
+
421
+ } else {
422
+ const instance = await WebAssembly.instantiate(module, imports);
423
+
424
+ if (instance instanceof WebAssembly.Instance) {
425
+ return { instance, module };
426
+
427
+ } else {
428
+ return instance;
429
+ }
430
+ }
431
+ }
432
+
433
+ async function init(input) {
434
+ if (typeof input === 'undefined') {
435
+ input = new URL('brotli_wasm_bg.wasm', import.meta.url);
436
+ }
437
+ const imports = {};
438
+ imports.wbg = {};
439
+ imports.wbg.__wbindgen_is_undefined = function(arg0) {
440
+ const ret = getObject(arg0) === undefined;
441
+ return ret;
442
+ };
443
+ imports.wbg.__wbindgen_is_object = function(arg0) {
444
+ const val = getObject(arg0);
445
+ const ret = typeof(val) === 'object' && val !== null;
446
+ return ret;
447
+ };
448
+ imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
449
+ const ret = getStringFromWasm0(arg0, arg1);
450
+ return addHeapObject(ret);
451
+ };
452
+ imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
453
+ const ret = new Error(getStringFromWasm0(arg0, arg1));
454
+ return addHeapObject(ret);
455
+ };
456
+ imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) {
457
+ const obj = getObject(arg1);
458
+ const ret = JSON.stringify(obj === undefined ? null : obj);
459
+ const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
460
+ const len0 = WASM_VECTOR_LEN;
461
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
462
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
463
+ };
464
+ imports.wbg.__wbg_new_693216e109162396 = function() {
465
+ const ret = new Error();
466
+ return addHeapObject(ret);
467
+ };
468
+ imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) {
469
+ const ret = getObject(arg1).stack;
470
+ const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
471
+ const len0 = WASM_VECTOR_LEN;
472
+ getInt32Memory0()[arg0 / 4 + 1] = len0;
473
+ getInt32Memory0()[arg0 / 4 + 0] = ptr0;
474
+ };
475
+ imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) {
476
+ try {
477
+ console.error(getStringFromWasm0(arg0, arg1));
478
+ } finally {
479
+ wasm.__wbindgen_free(arg0, arg1);
480
+ }
481
+ };
482
+ imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
483
+ takeObject(arg0);
484
+ };
485
+ imports.wbg.__wbindgen_throw = function(arg0, arg1) {
486
+ throw new Error(getStringFromWasm0(arg0, arg1));
487
+ };
488
+
489
+ if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
490
+ input = fetch(input);
491
+ }
492
+
493
+
494
+
495
+ const { instance, module } = await load(await input, imports);
496
+
497
+ wasm = instance.exports;
498
+ init.__wbindgen_wasm_module = module;
499
+
500
+ return wasm;
501
+ }
502
+
503
+
504
+
505
+
506
+
507
+ // Auto-init: fetch WASM from same directory
508
+ var wasmUrl = '/public/brotli_wasm_bg.wasm';
509
+ window.BrotliWasm = {
510
+ ready: init(wasmUrl).then(function() {
511
+ window.BrotliWasm.compress = compress;
512
+ window.BrotliWasm.decompress = decompress;
513
+ return window.BrotliWasm;
514
+ }),
515
+ compress: function() { throw new Error('BrotliWasm not ready'); },
516
+ decompress: function() { throw new Error('BrotliWasm not ready'); },
517
+ };
518
+
519
+ })();
Binary file
package/public/default.md CHANGED
@@ -23,7 +23,7 @@ But while markdown is great for agents, it's a bit annoying for humans. Quickly
23
23
 
24
24
  SmallDocs is an [open source](https://github.com/JoshInLisbon/SDocs) attempt at something different. It lets you (or your agent) easily, elegantly and <ins>100% privately</ins> **read**, **format**, **share** and **export** `.md` files.
25
25
 
26
- Reading a `.md` file in SmallDocs feels just like this (you're reading markdown right now). And by playing with the styles, it can feel like [this](https://sdocs.dev/#md=fVbNbttGEL7zKQb2IQAhEaJky4oOKdIEboq2QFoHNXLTcHcobkXuMrtDyWpRoA_RJ-yTFLMkHcm1e5N2vpn55psfaTqdJoGPNYV1AlA6y7fYmPq4hh-dxwSgwEC3zvKd-Z3WkK8SgNpY-kBmW_Ea8mx1nQBUhJp8jHEe5WONxxKNh_cmtDUeI6BBvzX2W8fsmjXMshghX8Mf0bXPNM9Wk_j1fsh0M5vBnwKcnwPzF4GLp8DFOXA5AFvBPS1q8oRlni0iuDZ2J3hNynlk4-waOqvJS4CIKGqndl86x_Q0fz6BwnlN_t5ortYwRpSkUZgC1W7rXWf1Gi4uSyyX5ewiWpSrnZfHxWK-zLF_7DV7NK2KHHO8iEFHnR6NOLuez_WjcXFmXBZX8_lyNLZntoWeU16OtrH8F5Iqp2PV54VQSSutLyZfvW7UFeY0ep0r1kv0boSqq9fXS7yYPBVnQZquTmMui-vV1aqPqdHvnpM0x3yZP5WUZvpaXT8raTnD2c1LkuornC3xBUkH2s9KqpSaFbMXJT1P-rykc5wv8tVp-eds_lfSG1xeX8_-I-l8kVO-PI2Jq9erVSQ6nU6T5BI-VQQ_d4YYfqG9qzuZf3ClrLmx8IkeOElunZftQE1hAlwRaKe6hizDAQMg7E3osAb0bEpUDKXxgQGtBgTlLKOx5CWo0YQBAilndQb3BKpygeJGBSiodJ7gML4enNcholD_1gUmPSzwKfTRhH4bKYUslsTO1RAqbElHxlwZuzN2myXJT-h32h0sGLsnz0FsIYPvGTDswhrSQ4UMpox-kQQobKiv6ps0SS4v4d4bNnYL94Yr1zHceqNEuSSR5AUFhsMAESYBtAnYtoQ-g7dgHVPh3A60o2BfxcRwdB2wk9qleASmpq2RSRwOlREP9PrRxdOXzngB1kaRDZQNLWN6YKiwLwuCMP_SYW34CP_89TcYhi1xAGHthhrxmCXJG7gQ7kGhN8K_cbHDJgDWBzwGEKFH5YVsYPScwduSyQNXyJOo8lbksuBsfZRMUBAz-ewiZr9jaiuy8IOx2yS5l49H13konW8wJmsfi5gA7ckf-1YWpFxDMmynumbwWbyZfGMsjl_byll6FaLMAbBtRcLN3jQbCBRCHPA9ebi7-wDIMMemn5m-1yii9hWjUoIvapqMT63zjKcPQj9kMhSXcC-D89l18B0amyRTSNOPEW9E_jSNEnwtUBLuQl_loSJP8mPm5Wt0_ZV85Cob5F09uG-2hkGbstxAqNwhEgB6QMX1EeLkqgrtlnSM8c7VNRbDz9oQoSG_JYla1kZxX7An1H1Z1jEoZAzsXVsZFcPcOtWFwd26KH2BHhpCKzLLdLPHYQPOlPiBqE0S-aSdjG3d77t0W7oYOwZbUSQ1lslKCKzTM8jb-GdEei7DCJvLzQSoaSsMJsjTJsXA5E3YhXQzGe5ObQIP-Omm729g3ynuPEFt9hRA2iCnQVphOFBdZkmy2WziVexD45g6-frSosetx7aCg-EK0rRwtU7TmDc1jLVRaQyaJdOTQD0hpiZ5c_IaD3pMKmdFWL4fbysGeI-MSfJW7l5_skpTkziO_YLiCFXXSBskfYs-0PjeoKqMpXgQY7p4aaAx1nlp_56sIatoOAuvhE7ZWY2SHeV6mpKHlRoPfhgWETQyxqUH3Mr7BILyppUPQqQ_enIHZMZ7obTxJFOa9Udyk2mnHja9DEWNageFe-g7tcka3VssuJYsyLXMkn8B&theme=light), [this](https://sdocs.dev/#md=jVbdbuPKDb7XUxDKRQDDEWzH3nh90WB_uqc5OH9oUgS9W1pDWVOPZnSGVBy1KNCH6BP2SQqOrDjJZoFzY1iajxzyI_lRFxcXGUvviDcZQBW8fMHGun4D-S-dtxLyDGCLTF-Cl1v7T9rA_CoDcNbTX8juatnAvFhnADWhocgb-NdLN7-FtrWe8ylwiU7tp9Bg3Fn_MYiEZgOzYgX_Vg_z0Xi4aFGspunx_njR1Ww2ABcvgfPi3feQl6-Ri5fAdyNw-Qr4NqxV1KvcX6czL-ZDPs76veINlSGi2OA3kHfeUFQPecKUwdB48wby4eXWhXL_exckHW1DNBTvrZF6A8vpi2xm400sCuUWS-t3SulyCtYb8inGxRGlIWcAWtFyv4uh82YD-VmF1aqq8nRSBheivlyY-XZJw8uhMk9HV-UlkhliHavxdLil-Xp1Orx8cTgz75fr9Xg4EvQdxyM3r4Kd0VWF-fRk9f7y8pJwtHqLvE8jtJytl1WZT1_7vKQyvX7yudoul1fHQA3G_Vu0Ld8tF6vFK9qMMbNq9iZt5XK7qr5HW_Ue12b5HdpWRHg6_Ia2l47fpm0xm68uV89TNOvtsqI_RNt6uyqrd9_QNi_ni8vZc5-4fr8uZ-rz4uIiy87gM7HdefgtWl_a1hFDFSLcheAY7moU-AlZsuyuJtgSC0g6wUj-XEBqguCJ4WClTk9NYIGKULpIXMBdTf15pBNQ1GVF5GBiPT1Ywa2jCfzvP_8FZ_cJ2EMZOmfUf40Pei15MClMMoC-hyA1RThgX2TZ2RncCkaB-zGEL0TO-l2WfaQqRII-dHCIVggQ2PqdoyQQEKpUiSmg5wNFkNryBupwAK41AODQUPAEE413Aoea_BBfx8lrTGRcZ9mfID_SaBl8EPhHxwIHTdUKuBD2PGSH3qTkh8cCTlZ6rRU4hLjnIk983Ao9EPwYtpxlvwSB_DBwN3A7BmkHlgaTxG4ZGmJwKBRTAdKN1u_0GuWn0u5IalfAnx8o9lLrKTkmhdimddSQlwGiDJ-NnDLchyj1MW61-hJill3AZPIp-Moa8iVNhnLmN7D34QD0iKW4fqBDOYaDdQ5MyJPdbUtkRpPUcAdkqJAllQQ93AA9tlQKmcHiMyWdHG1-racp73MGBG9LAgldWQ_Yu9jxE_LnHgwKapKMFUFNkfLUQX-lJjwQ_M2LdXAj8DES7jnLEj0j5WqHUAaWAm6G61If2dQVYELJU0BoMfVhDN1u6Mdjk8HvHbFSOhp0TPGcobHeDHU6AVibPz9W-ECAxiTqrocyW709NyHNk2UgjB6ssJa-dfRopb_Oh8Kp37s6EsEPKMRPQ4FPWXFtW55qHzXatS0yAzqnKRBtsmxewGRywwogZvJi0V1PJnCfgkNoCLUTqs6B75otRR0szU1T42RnK_01Ns00PVqW62xxcmu9dFbsA6nbT-ifBk_nTIfCSh06gUhotOeU6evs8uRA0w6sSqIePisvx1kahGnQi3FyphAimCOojIRCwC2VFh2UyMTXWXZTPWOoQquS53vYoVCi6oBWuIAfQlCZqOSAQ3-0KJa8DLr0gVhqElsyfIgEOsO_tlpgdFn2KZhkoUlBE5JGoh9Kq2ol5Au48UKxwpKS4gKrEp6wchTX0tlyT8cmerDcoQOHPcVjI50-b07dU4am6bwtxyH_-vVr9gEO5NxFFWKDImSU6E6VABj79PmZ3x4rU2JULd5qWbQFizxTc-0brb5PX6wGhB6fGd9pr6YhMhErKeBn7LdU5Ony7K5vwy5iW_fT8TNpmgQz7a5x47jusYv2-WpR5VSJF0pNS4lRihuYpNlIqUQeXHEYCj-It21octwgtW2n6tHDT5aF_LDw0jIz6HcUQ8fQ1shpbWw768wzRVVGDqomQyC19Xv9p9GZ4KmAvw8PPkiR7kpW3KBzulIfKLIWJ6k3Ndtg7Bhj-7SWVeTIwwGlrFPOLYXWEWApHTrXH4elyLL7QWipB7vzqVnIOR7WYDoKEJPeFXCCssRup6tx2OTfWLCuBVv1z22Q9-lj4Vt0Igg8PWo8yqQuymM3VtZbrsk87dNI52lrpi3kqEoe0Jijyv4xvKCu1_RJ8H8&theme=dark) or even [this](https://sdocs.dev/#md=pVbdbtvMEb3nUwysiwCqKFiSJdkC2iBxkiZA2y-wnaa903B3KG603GF2llKUtkAfok_YJyl2KSqy41xVd-L87NkzZw6Z53km4WBJVhlAyS68w9rYwwruufWK4B6dwCwDKFDoHbtwb77TCibzDMAaR-_JbKqwgsl4mQFUhJp86vW42x0XHBjuLRYpVqPfGPeaQ-B6BZfjq1g8WcE_UlV3xnQ8HaW_n49nLC8v4V8xcfo4cTKe_yJx9jRx8iRzccxsYuKT-4x-QnmTchVr6vuuoGbH0qCiFLPGbWNMk2KPwbBbgWPXBQvLavu15ZDKC_aa_GejQ7WCq2O1hBiK_YzbxCNn82MkokrUFai2G8-t0yu4GJTzclniRYootuzjwwlOZ7Np97Bj9RS6VFe4oIvUtGfyR918Tsv5KTh7HKRFOVd9sL_oj8azxQ1O-nDP0WOwdE1Uzi9GP6oWenqtb_qq5wi6PR1AOKebi9HTnrq8KvG85xXO54vrrqdGv32Otks1mU4vn9CmrvWC5s_SttR6Vqpf0LZY0nV58wvaFkTLYvlL2h43fp62yWJ6OZueXxGX10WJ_w9tk8kEp49ou76-ucHUM8_zLBvA69ZYbdwGEG7_9AEeKgxwR9KQCgIfgsAnIS9Z9go2zBoU1zU6ncctgsBsQSpurYaSyII1WwIExW5HXtJmwN6EChC2jveW9IawsBTxWMJNS_Dff_8n5isjNIKKbFO2dgToNDjakY8xTaLIRZBjeE-eXghUvIfAUETwwI7GWTYYwEdvnDKNJZis4A3Db47goYqX-0zWZtnbHfkDSFscb9Fjr3AXYYtxG0sj4GJnuBVoWt-w0Bg-lHDgFhS6FwEiGm8Kgn2kCntGQDMJGBfRgJAL5BSNwIQXApojhsAMdauqcZat1-sCpcoG8EdmvQJlCX1_XiaaFUiFnuDu7as3f347rjWcfgNQ3ByO8ai0SISypikYvT4Wq4pqhJ9_A2i8cQHS2wAazw35YEiSElCvAOvCbFpupWvUeFYkcoYjz7kNTRt-n47O89PJkOeO88LzXsjHG2bZQ0XwyZlv0FTGsnBTHcBIJPFLKwEcS0C7MZhEkJhKGvv0t6fjnK7gnpyYKJ03VGJrg4zgXWst3LILnm132HfynCt2pdm0nTEDfWvImziMftoFAX1TZC25MIbXbYB9RQ7aqHNwRFHkqecINmZHYEJkOFRUJy1zG0CRp5rd4WyUiS5PDfvwaFw97-nGe_ZbSbf1hBpq1jQC3V2om4k8bZTn3axOjRLCCGhPuH0J3JCL4PqRoiP7cw-P-3Mwe3ShK0rv_5dQkU8XNdJN7g_wqtvuEEXu6WtrPAk8pjYuIEXeytaCEcCzkgoFSjSWNGAAEyQKr0Z_gC9cpPEOIE7sOE24D6i2WfZ3bn1PiAC5aJVx9zxwY5xhJ2O4rZiFuoFosqYgj4HsYZVlkzEMh78lfULJvsYwHCa6411rlpC2ldOwQaEQ7I2TbBrL_kq-YDHhcCz52hoKUBx6OCNY5_kuJdG6k0wcBelsFsvvsaRTrSYJvlUhygdVJEt6EjsOIzTDLruKpR_JJ7BO0bG-RIl6QB_aZgQWvx_AMiabLtlDNMZDSMZGVujJssxWvYGna380DUW3jhYeHT7NyJpd51bogBTLQQLVY_gQZ63oGEwseq6BQ0U-FUpy5gZFSKAzgiiDlCBjeM-OPYTKSLdDqMLZiiiMOx9Ioib_CUmkz3qUBG1ctyzPFnXe92zR787sXVKLLMonxw25cDIveAbAANAdouHpVpE_np6WIWr1rrUkifyeT7g1wXwnJ5VpsiyH4VCCjuZgujyNAYfDUcRF3vdPaxLBDUkqePvNhPQ1IFBjCOSP419frlOytCq67yh-XObR29LTuFatp9ThLwzGBYpER601nusmyHDY6bOjJG1mY5qu4j4Js_Wkj_Prk9d5_kXYrWN-GrBOuopL-oASCB5IQpbdta5byKSjMo2boDReAgRT06h74TsG9Ju2JhdkDJ87S2gacvKy-8aoOcZOy5mcWwIGOtrjjtDKudKxiOTGF398iaYXsj_KPOFJ_v0iYRhn_wM&theme=light):
26
+ Reading a `.md` file in SmallDocs feels just like this (you're reading markdown right now). And by playing with the styles, it can feel like [this](https://sdocs.dev/#md=jZTdbuNGDIXv9RQHykWBrS1Ijp04LlAgSRF021wEmwC5piRKM_BoqJ0ZxdAWBfoQfcI-STFSfnc3Re8MknN4-JHycrlMgg6Gd0jvFOPcBUiDWyMHfGKqtW3TxIfRsN8lQCM2XFGnzbhDei2O0gQoyfOV2HCrv_AOxTYBjLb8K-tWhR2K7CwBFFPNzu_wx1uRG0NjQ9rhF-17Q2O6gK8o-imyYoGOXKvthYQg3Q55tsGfUax40pl7rrL1HF-9jRfZ6RTvY_itVJEdTymj7T5ma67EUdBid0itWE6ndGmk2n8eJHAsKsXV7O51HdQO68WrTnl2tnnUi0MnQORS7Vsng613SI8aak6aPJ0ylRhxMbiqVutiOwdfEXrOb8v1pjierbx4fU5Tvlmt6qf097xePpXWazo9oXTxtasNN3ySLl40T8oNrflJs5J6Vnv7KudtXb1-dUrrZlXNr2py--8RKLigYv0VAc7rTbV-n8Cj7_cI8LZab-l_EdiWJ2fF-hsCK1qtiu3rWcotbc_y_ybw7asXo8vlMkmO8M7nlCQfLQgHcaaG9EF3-gvXaMTB73XXadsuEBQ7hvYgfB40Bzgu2ZjpOt2sA2_kYMYsSY6OcK9G3PbMNX7XxnhcStc7Vmy9FpskPyN97I5mcFZ7xT72QKdtDbFmxEEHhY4CO03GR9N7KwfDdcs_QYfoJSht91EjKAroaM8eh_jzwJMpyOB8luKfv_7Gb6IsrqXac5JEEPTAjlpGz86Lnco9Vps8cqh9DEcvQ-BsnmP5NGUVRdmjd9JpzygWeZ7_mOFiiFA8k6sUKrFe-8A2mBFeycHPHksexdY4yXMc-i4u6xUW1E56jyCwTA5f2MkM8045Ztw4qoKu2MfYEYoMn3hyNYHryVHrqFdJcsGNOEYnD9Gv2MUMQwdQS9pm0yF4rqKTnryH4weOjCd2UazRzgeIjfvwnutsbrnKcO7agefdxEIaghKXJB8bjDKA2mh0yvIDuzEuqF3E1A-OYSU830pcyWOYSi-u1LbNcDN4Nd11hnO_x4eDGj889j7OcGnE89S2FNnPayzZv4gq6nu2HtQEdpMhH6TPcM3zWLpm8vAcguEM906HyZSazlAcyELJ4Gbo57jSlgzulAytCnO7hnyIHQ80xj0NNv49BLI1vHQ8K-lpha1-4Ig86I6z5F8&theme=light), [this](https://sdocs.dev/#md=jVTtbtpIFP0_T3Fl1D8RNh4DDvaPlfJBdiO1aupktdKqUhg81zDFnqEzYxJaKu1D7BP2Saqx8YbQZlsLieGce-85Plzb931ihS0xBe_s5hoyLFCjzBE2kUeM3ZZoUgJQKGmvWCXKbQrerap1jnDLpIGhRwDmzOCVkvZWfMIU6JgAlELiHygWS5sCDWIHLZFx1CaFz8_HZWqurPL6UDG9EPJcWauqFMJgCF9cG-062vFRi0bPURqMWnx4jNNxQ6wdfqyQNFSuOHZdKXhXQjN4o6TyGnZeqnz1sVa2qZkrzVH_JbhdphD1D5S6aaUw1lWaNcuFXDiCjvsgJEfZhBHty1w2BMDFl68WWtWSp-D1iubyGiZXpdIOjEZREu3Bgxz_4ymjGI1aw034q2d0mMQJZx3d3e-RblxMCub1n7qS8XAShl3Xj3K4OBLo_3RmPI75adzO5EyvfpRAyCmlp0cJ5AmnPHk5gSIs4iJ_MYHxhMVF8f8J0JjOo-jQbVGweHz6SwnsBY4T-H7mZJ6MEnQzfd8npAfHzx0h58wg_Jm9TmG2tHZt0sGArUWAj6xalxjkqhpsohkhvR6c1XaJ0oqcWaEkIWdlCRo_1misaQ5CIzA4R6ZRg1UrlCAk2CXCzPUqLT41rbN9qikhs9mMPOPSrt-s7kuxwfsgCJoq52Aq-VoJaY371YObt7d3MOAqrytswAuNzDoPEh-gw4NG5YNRknwmAF7zDvJS8N7VTFvU5RYyXCttvb6jC6UrZh1fMb3i6kG2eK6kRdkQPXhHIUNTl9a8l-9lhhuUNcJC4wNEw1dBEHjkS-v65CRDs1bS4MkJfP3nX5hFIYXWKJ8dWRPcjecqv588Rquk2Cu3xfetqyiMYj8c-jS5o2Eaus_fbV2tS1fw8t_4FNXgQKNz6hL9fXoY6CAVnJAMrRa4cbF2DMy3cH0ZEPIbZC7wUlTCvW_C8Gkh1qihErK22Bzd5q1wG7Q6l9PX07vpd1I3qCsmUdpyCxxLtGgOVAO4WwoDLHd7AjmTUlmYI9SSK4lBuyFaKw0XiqMhZNccYAdvkEkhF7AjO7-59l_uRHYwCkPYwbXcsFLw7g5grvgWWpq6GcIYN0NpEPvKdsXbkhHs4LKLxxkr3GPZklECu4OgAB9zRI6O_QY&theme=dark) or even [this](https://sdocs.dev/#md=jVRLbtxGEN3zFA-jhQFBQ8yM9Z1FANmAEBt2spAAxcsiu0h2ptlFdxc1mQQBcohcwuscwTfJSYJuzsSSP0GWrGpW1ftUzefzQq06XmN2z7xhb2A93thYiZ8VUXeO47oAGvF6Q711uzVmP4zeqswKoKLIN-L11v7KaywvCsBZz9-zbTtdY1lenhVAx2Q4xDV-e1rntURurMct-Tg7QawpDbIsF2cn6Cm01r8QVenXWJSnZ_g9lVoeqkwtV-X5FF89jS_38SGFn9ZalqucqpzUm_ejKKc3lQTD4d4a7dY4O3lUalFeTb1dBlUACXe9aYOM3qwxO2qapmrOZjlTi5OQgiuzulqdT8FHBPybrxfPr1bVLFfOrG2epFdXl4vq6pD-2qwvD0_54rR-Xs9OPp_KNKtmMTv5VPOiOa_OF4eatZip2tO_LpsFXz7-6_GghsLmawwsaXm-PP2MAb7khbn8NgP7ub_FwJkhw6v_xcB-xM8ZWNFqtXrCQLWgxdXlfzPw5V-fBp3P50VxhC9WpSjuusAMQ7tY4geBVes5UNiVeD1GBaG2uoN2pAi8pWAituQNB-vbsiiOjnATrKEd_v7jT1y7hnoqinsGhWAfOLfSjuFIGdQoBy_iQd5gS27DBlEDJYPCepX8VpzhqPBpGSsJnYgpcddxesqsERQYFQV2O2ytYbCXse3QSIBuBQPL4LjEGxq9CTt05NuIinXL7FGN1hnr21jihoygH6OtYYJtNEJGhTSQgT2MSIhlUXyHdzLCiH-meLDR6h5mmeMtK5zEND6snmRkia1nMWMZxHotJ0aU0QbrXAJNwVjPEZQoHhzVjK3VhGEMUKpcynkDL-jZjxN-2XoOqEJCq9h2pPzAAVuKaALHbtLjlnQMB0VesPv4oU8yM3oJ3voWUSkom6nh8fFAUT9-sBGG4Unp-DhNlYZvqJcxoqINJ0O843iS4-9HHjm3deLbfcJqjmwlaAebMKeerzl8_MvbXiLeiqeoHHawEVTFMRi3Q8U0qm1Gl8dNeHaIY2gSIzWFvYXekh85XWgYVrKuxD0jDuw1K97JGDLfgTNrtYzOoKOH5Bnascm8ZnqOJieNPrJmWfY2pAQlo0u-Ddh78U5CSE33RCZm2uRPn3tOsuT7Ch2DZzMh0C7xvKXQ53GiNFriFVRkg9MLDJ2oxBI_-onFNjl80m70B-Xe_IQbqlXCriiuUYt_4Kya9WaMGiw51NIPjn9BMzoHadZFMccLkU3sZIiTvFX6TCTUCWTjRDK0mq2zvi3muEYQaVSGtFGQBw5OZGP3XFTBmpaLOW7z6oGCQvxeJnog65JVD4IVc7yUpmGe7gXVOpJzO2jS3cDZDaPODzLYxN1dZuq6Sot3OEmv0vJ4UWjY5UEEth8Cx4idjCVy_ud0nY4rtr49frJ1aS3Q04ZjsmRPrWe19aRUHca-crmmdcnfNZmDqW0_SIw2wemsc_vopC0pzoc-y-LFczoRVpON6zGQsnk80-OL2dEwsI8JQcWfrF4WxatnzuUY1Zuy-Ac&theme=light):
27
27
 
28
28
  ![Examples](https://sdocs.dev/public/images/examples.png)
29
29
 
@@ -46,7 +46,7 @@ https://sdocs.dev/#md={compressed & encoded .md}
46
46
 
47
47
  Your entire document (content and styles) lives in the URL hash.
48
48
 
49
- To keep URLs as short as possible, SmallDocs compresses your markdown using [deflate](https://en.wikipedia.org/wiki/Deflate) (a standard compression algorithm built into every browser) and then encodes the result with [base64url](https://en.wikipedia.org/wiki/Base64#URL_applications) (a URL-safe variant of base64 that avoids characters like `+`, `/`, and `=` which would otherwise need percent-encoding).
49
+ To keep URLs as short as possible, SmallDocs compresses your markdown using [deflate](https://en.wikipedia.org/wiki/Deflate) (a standard compression algorithm built into every browser) and then encodes the result with [base64url](https://en.wikipedia.org/wiki/Base64#URL_applications) (a URL-safe variant of base64 that avoids characters like `+`, `/`, and `=` which would otherwise need percent-encoding). Style properties that match built-in defaults (e.g. `fontFamily: Inter`, `baseFontSize: 16`) are omitted from the URL — only values that differ from defaults are included, which keeps URLs shorter.
50
50
 
51
51
  The `mode` parameter controls which view opens. Valid values are `read` (clean reading view, style panel hidden), `style` (style panel visible), and `raw` (raw markdown editor). When sharing a link for someone to read, use `mode=read`:
52
52
 
@@ -62,7 +62,7 @@ https://sdocs.dev/#md=...&sec=url-formatting
62
62
 
63
63
  The `sec` value is the heading text slugified (lowercased, spaces become hyphens, special characters stripped). The page will scroll to that section on load.
64
64
 
65
- The `theme` parameter forces a specific theme: `theme=light` or `theme=dark`. This overrides the reader's system preference, which is useful when sharing a link where the document looks best in a particular theme.
65
+ The `theme` parameter forces a specific theme: `theme=light` or `theme=dark`. This is useful when sharing a link where the document looks best in a particular theme. The override is view-only — it applies for that view but won't change the reader's saved theme preference.
66
66
 
67
67
  ### Privacy
68
68
 
Binary file
package/public/index.html CHANGED
@@ -574,6 +574,7 @@
574
574
  </span>
575
575
  </div>
576
576
 
577
+ <script src="/public/brotli-wasm.js"></script>
577
578
  <script src="/public/vendor/marked.min.js"></script>
578
579
  <script src="/public/sdocs-yaml.js"></script>
579
580
  <script src="/public/sdocs-styles.js"></script>
@@ -197,7 +197,7 @@ function loadText(text, filename) {
197
197
  syncAll('load');
198
198
  }
199
199
 
200
- // ── Compression helpers (deflate-raw + base64url) ──
200
+ // ── Compression helpers (brotli + base64url) ──
201
201
 
202
202
  function toBase64Url(bytes) {
203
203
  var bin = Array.from(new Uint8Array(bytes), function(b) { return String.fromCharCode(b); }).join('');
@@ -213,27 +213,13 @@ function fromBase64Url(str) {
213
213
  }
214
214
 
215
215
  async function compressText(text) {
216
+ await BrotliWasm.ready;
216
217
  var encoded = new TextEncoder().encode(text);
217
- var cs = new CompressionStream('deflate-raw');
218
- var writer = cs.writable.getWriter();
219
- writer.write(encoded);
220
- writer.close();
221
- var chunks = [];
222
- var reader = cs.readable.getReader();
223
- while (true) {
224
- var result = await reader.read();
225
- if (result.done) break;
226
- chunks.push(result.value);
227
- }
228
- var total = chunks.reduce(function(n, c) { return n + c.length; }, 0);
229
- var buf = new Uint8Array(total);
230
- var offset = 0;
231
- for (var i = 0; i < chunks.length; i++) { buf.set(chunks[i], offset); offset += chunks[i].length; }
232
- return toBase64Url(buf);
218
+ var compressed = BrotliWasm.compress(encoded, { quality: 11 });
219
+ return toBase64Url(compressed);
233
220
  }
234
221
 
235
- async function decompressText(b64url) {
236
- var bytes = fromBase64Url(b64url);
222
+ async function decompressDeflate(bytes) {
237
223
  var ds = new DecompressionStream('deflate-raw');
238
224
  var writer = ds.writable.getWriter();
239
225
  writer.write(bytes);
@@ -252,6 +238,18 @@ async function decompressText(b64url) {
252
238
  return new TextDecoder().decode(buf);
253
239
  }
254
240
 
241
+ async function decompressText(b64url) {
242
+ await BrotliWasm.ready;
243
+ var bytes = fromBase64Url(b64url);
244
+ // Try brotli first, fall back to deflate for old URLs
245
+ try {
246
+ var decompressed = BrotliWasm.decompress(bytes);
247
+ return new TextDecoder().decode(decompressed);
248
+ } catch (_) {
249
+ return decompressDeflate(bytes);
250
+ }
251
+ }
252
+
255
253
  // ── Auto-save to URL hash ──────────────────────────
256
254
 
257
255
  function updateHash() {
package/public/sw.js CHANGED
@@ -9,6 +9,8 @@ var APP_SHELL = [
9
9
  '/public/css/panel.css',
10
10
  '/public/css/write.css',
11
11
  '/public/css/mobile.css',
12
+ '/public/brotli-wasm.js',
13
+ '/public/brotli_wasm_bg.wasm',
12
14
  '/public/sdocs-yaml.js',
13
15
  '/public/sdocs-styles.js',
14
16
  '/public/sdocs-state.js',
package/server.js CHANGED
@@ -30,6 +30,7 @@ const MIME = {
30
30
  '.md': 'text/plain',
31
31
  '.smd': 'text/plain',
32
32
  '.woff2': 'font/woff2',
33
+ '.wasm': 'application/wasm',
33
34
  };
34
35
 
35
36
  function cacheHeader(ext) {