rapier-html 1.1.0 → 1.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/BUILD.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "package": "rapier-html",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "path": "rapier.html",
5
- "bytes": 1733515,
6
- "sha256": "b75dedcb78f9bd60ae04d896bbe3f4606b2127e6fa4275ada92c0b668c7e5694",
7
- "builtAt": "2026-09-26T13:30:00.640Z",
5
+ "bytes": 1736480,
6
+ "sha256": "e040386098ff5cbb2234780c6068ba0f3a1a5b3a8ac64e061ce9e52e15cc0504",
7
+ "builtAt": "2026-09-26T16:17:27.420Z",
8
8
  "node": "v22.22.2",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
package/README.md CHANGED
@@ -1,87 +1,90 @@
1
1
  # rapier-html
2
2
 
3
- Rapier with your document inside it. One command turns a Markdown file into a single HTML page
4
- that is the whole Rapier editor, carrying the document's exact text: it opens anywhere a browser
5
- is, offline, with no account. Read it, edit it, save it to the device, share it on. Agents hand
6
- people documents this way; people hand them back the same way.
3
+ Rapier, the offline Markdown editor, as one HTML page with your document inside it. No account, no
4
+ install, nothing fetched to open, edit or save. Node 22 or newer.
7
5
 
8
- Nothing is fetched to open, edit or save the page, and nothing reports on its use. It goes to the
9
- network only when the document or the person asks: a picture or video the document links to on the
10
- web, or the math, diagram and PDF-import helpers, fetched once from a pinned CDN when first needed.
6
+ ```sh
7
+ npx -- rapier-html notes.md # the editor, notes.md inside it, written beside the file
8
+ npx -- rapier-html notes.md --view draw # opens on Draw: sketch and paint, the document behind it
9
+ npx -- rapier-html notes.md --view notes # opens on Notes: the document as cards
10
+ npx -- rapier-html notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
11
+ npx -- rapier-html proposal.md --base original.md # opens on the diff of a proposed change
12
+ npx -- rapier-html notes.md out.html # a named output
13
+ npm install rapier-html # as a library: wrap, unwrap
14
+ ```
11
15
 
12
- ## Use it
16
+ The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
13
17
 
14
- Node 22 or newer:
18
+ ## What the page can do
15
19
 
16
- ```sh
17
- npx -- rapier-html notes.md # writes notes.rapier.html beside it
18
- npx -- rapier-html notes.md out.html # a named output
19
- npx -- rapier-html notes.md --drawing sketch.svg # carries a drawing, opened on Draw as the page boots
20
- npx -- rapier-html --help
21
- ```
20
+ **Write**
21
+ - Markdown the way it reads: headings, lists, checklists, tables, code, quotes, footnotes, pictures placed
22
+ around text.
23
+ - Find and replace, Undo, a source view of the exact bytes.
24
+ - Math and diagrams on demand, fetched once from a pinned address only when a document asks.
25
+
26
+ **Draw and paint**
27
+ - Shapes, lines, arrows, text on a canvas that goes into the document as a picture.
28
+ - Paint with real brushes: pencil, pen, ink, watercolour that runs and dries, oil that mixes, a smudge
29
+ finger; dip the brush for more paint or more water.
30
+ - Your own brush files, a dropper, paint laid over the drawing as a layer of its own.
31
+
32
+ **Notes**
33
+ - The document as cards: sections, drag to reorder, search across every note.
34
+ - Voice recordings, reminders, a recycle bin that keeps seven days.
35
+ - Import from Keep, Notion, Evernote, Joplin, Bear, Simplenote, OneNote, Obsidian and more.
36
+
37
+ **Review a change**
38
+ - Open on the diff of a proposal against the original; keep or drop each change; nothing applies until
39
+ the person says so.
40
+ - Will markers in the Markdown say what an agent may change: `keep`, `append`, `edit`.
22
41
 
23
- The first `--` is for npm: without it `npx` keeps `--help`, and a later `--`, for itself.
42
+ **Keep it**
43
+ - Save to the device, share the page on, open it again anywhere a browser is; offline throughout.
44
+ - Export the document as Markdown or as a web page; print it.
24
45
 
25
- **It never overwrites a file.** An output that already exists -- the document itself included, or a
26
- link to it -- is refused and nothing is written; name a new one. An option it does not know, a
27
- `--drawing` with no file and a third filename are refused as well, so a typo cannot quietly make a
28
- different page. `--` makes everything after it a filename: `npx -- rapier-html -- --draft.md`.
46
+ **For agents**
47
+ - Hand a person a document, a sketch or a change as one file that opens with one click.
48
+ - What they save comes back byte for byte through `unwrap`.
29
49
 
30
50
  ## As a library
31
51
 
32
- Save this as `example.mjs` in the folder you installed into (`npm install rapier-html`) and run
33
- `node example.mjs`:
52
+ Save this as `example.mjs` in the folder you installed into and run `node example.mjs`:
34
53
 
35
54
  ```js
36
55
  import {readFile, writeFile} from 'node:fs/promises';
37
56
  import {wrap, unwrap} from 'rapier-html';
38
57
 
39
- // The page inside the installed package.
40
58
  const rapier = await readFile(new URL('./rapier.html', import.meta.resolve('rapier-html')), 'utf8');
41
-
42
59
  const words = '# Hello\r\n\r\nA note about </script> and C:\\Users\\me.\n';
43
- const page = wrap(rapier, words, 'hello.md'); // Rapier carrying the document
44
- // A drawing rides along, opened on Draw right after the document lands (Done places it in).
45
- const svg = '<svg xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="6"/></svg>';
46
- const both = wrap(rapier, words, 'hello.md', {drawing: svg, drawingName: 'sketch.svg'});
47
-
48
- console.log('words back exactly:', unwrap(page).text === words, '| drawing back exactly:', unwrap(both).drawing === svg);
49
- await writeFile('hello.rapier.html', page, {flag: 'wx'}); // 'wx': never over an existing file
60
+ const page = wrap(rapier, words, 'hello.md', {view: 'draw'}); // opens on Draw, the note behind it
61
+ console.log('words back exactly:', unwrap(page).text === words);
62
+ await writeFile('hello.rapier.html', page, {flag: 'wx'}); // 'wx': never over an existing file
50
63
  ```
51
64
 
52
- It prints `words back exactly: true | drawing back exactly: true` and writes `hello.rapier.html`.
53
- The package exports `wrap`, `unwrap`, `encodeCarried`, `decodeCarried` and the command's `main`;
54
- `unwrap` returns `{html, text, name, drawing, drawingName}`, with `null` for what a page does not
55
- carry.
56
-
57
- ## What the page carries
58
-
59
- The page carries the document as one `<script type="text/markdown" id="rapier-document">` block
60
- at the top of the body; the browser never executes it and Rapier reads it once at boot in place
61
- of the Welcome. A second, optional `<script type="text/plain" id="rapier-drawing">` block carries
62
- a drawing Rapier made (its SVG carries the drawing's recipe in its own metadata); Rapier opens Draw
63
- on that recipe once the document is up, the document behind it. Wrapping is a byte insertion, not
64
- a build: a second wrap replaces the blocks it is given.
65
-
66
- **Your words come back byte for byte, whatever is in them.** A script element's text is not inert
67
- to an HTML parser: `<!--` followed by `<script` puts it in a state where the block's own
68
- `</script>` stops closing it and the rest of the page is swallowed, CRLF and a lone carriage
69
- return are rewritten to a newline, and NUL becomes U+FFFD. A note *about* HTML is enough to hit
70
- the first. So a carried block holds no `<` before `/` or `!`, no carriage return and no NUL: a
71
- backslash is written `\\`, `</` is `\/`, `<!` is `\!`, a carriage return is `\r`, a NUL is
72
- `\0`, and nothing else changes. One pass each way, exactly reversible, and the same size as your
73
- words except where they already held a backslash. A byte-order mark is kept as a fact of the file
74
- and written back on Save, not folded into the text.
75
-
76
- `unwrap` reverses exactly what `wrap` wrote, and the editor inside the page reads by the same law,
77
- so a page always reads its own bytes correctly. The command reads the document as UTF-8 text.
65
+ `wrap(rapierHtml, text, name, {view, drawing, drawingName, base, baseName})` and `unwrap(pageHtml)`,
66
+ which returns `{html, text, name, view, drawing, drawingName, base, baseName}` with `null` for what a
67
+ page does not carry. Also exported: `encodeCarried`, `decodeCarried`, `main`.
78
68
 
79
- ## Licence
69
+ ## How the page carries the document
80
70
 
81
- AGPL-3.0-only, the full text in `LICENSE`: the page is the Rapier editor, with the notices of the
82
- libraries it carries. For the Markdown standard without the editor there is `rapier-markdown-kit`
83
- (MIT). Rapier is https://rapier.website.
71
+ One `<script type="text/markdown" id="rapier-document">` block at the top of the body, never executed,
72
+ read once at boot. A drawing rides in `id="rapier-drawing"`, the text a change was proposed against in
73
+ `id="rapier-base"`, the view to open on in the document block's `data-view`. The block holds no `<`
74
+ before `/` or `!`, no carriage return and no NUL: `\\`, `\/`, `\!`, `\r` and `\0` stand for them, one
75
+ reversible pass each way, so a note about HTML is as safe as any other. A byte-order mark is kept as a
76
+ fact of the file and written back on Save.
84
77
 
85
- ## The page inside
78
+ ## Safety
79
+
80
+ - It never overwrites: an output that exists, the document itself included, is refused. Unknown options,
81
+ a `--view` that is not `draw` or `notes`, a `--drawing` or `--base` with no file and a third filename are
82
+ refused too.
83
+ - Nothing reports on the page's use. It reaches the network only when the document or the person asks:
84
+ a picture or video the document links to, or the math, diagram and PDF-import helpers on first use.
85
+
86
+ ## Licence
86
87
 
87
- Its version is the version of the Rapier inside it. `BUILD.json` records the page's size, SHA-256, build time and Node version.
88
+ AGPL-3.0-only (`LICENSE`): the page is the Rapier editor with the notices of the libraries it carries.
89
+ The Markdown standard without the editor is `rapier-markdown-kit` (MIT). Rapier is https://rapier.website.
90
+ `BUILD.json` records the page's size, SHA-256, build time and Node version.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rapier-html",
3
- "version": "1.1.0",
4
- "description": "Rapier with your document inside it: wrap a Markdown file into a single offline HTML page that is the whole Rapier editor, opened anywhere a browser is.",
3
+ "version": "1.1.1",
4
+ "description": "Rapier, the offline Markdown editor, as one HTML page with your document inside it: write, draw and paint, notes, the diff of a change; no account, nothing fetched.",
5
5
  "keywords": [
6
6
  "markdown",
7
7
  "editor",
package/page.mjs CHANGED
@@ -3,6 +3,8 @@ import {basename, dirname, join} from 'node:path';
3
3
 
4
4
  const START = '<script type="text/markdown" id="rapier-document"';
5
5
  const DRAWING_START = '<script type="text/plain" id="rapier-drawing"';
6
+ // The text the document was proposed against (docs/page-door.md, "2. A change carried"): the page opens on its diff.
7
+ const BASE_START = '<script type="text/markdown" id="rapier-base"';
6
8
  const END = '</script>';
7
9
 
8
10
  // One encoding, exactly reversible (docs/page-door.md, "What a block may hold"): no `<` before `/` or `!`, no CR, no NUL.
@@ -26,12 +28,18 @@ export function wrap(pageHtml, text, name, options) {
26
28
  if (typeof text !== 'string') throw new Error('the document must be text');
27
29
  const opts = options || {};
28
30
  const docName = safeName(name, 'document.md');
29
- const block = START + ' data-name="' + docName + '">' + encodeCarried(text) + END;
30
- let drawingBlock = '';
31
+ // `view`: the view the page opens on with the document behind it, `draw` or `notes` (docs/page-door.md).
32
+ if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
33
+ const block = START + ' data-name="' + docName + '"' + (opts.view ? ' data-view="' + opts.view + '"' : '') + '>' + encodeCarried(text) + END;
34
+ let carriedBlocks = '';
31
35
  if (opts.drawing != null) {
32
36
  if (typeof opts.drawing !== 'string') throw new Error('the drawing must be SVG text');
33
37
  const drawingName = safeName(opts.drawingName, 'drawing.svg');
34
- drawingBlock = '\n' + DRAWING_START + ' data-name="' + drawingName + '">' + encodeCarried(opts.drawing) + END;
38
+ carriedBlocks = '\n' + DRAWING_START + ' data-name="' + drawingName + '">' + encodeCarried(opts.drawing) + END;
39
+ }
40
+ if (opts.base != null) {
41
+ if (typeof opts.base !== 'string') throw new Error('the base must be text');
42
+ carriedBlocks += '\n' + BASE_START + ' data-name="' + safeName(opts.baseName, docName) + '">' + encodeCarried(opts.base) + END;
35
43
  }
36
44
  const stripped = unwrap(pageHtml).html;
37
45
  // Before Rapier's scripts: parsed before the engine boots.
@@ -41,15 +49,16 @@ export function wrap(pageHtml, text, name, options) {
41
49
  // A function, not a string: `$'`, `$&` and `` $` `` are replacement patterns.
42
50
  const titled = stripped.replace(/<title>[^<]*<\/title>/, () => '<title>' + docName.replace(/&/g, '&amp;').replace(/</g, '&lt;') + ' — Rapier</title>');
43
51
  const shift = titled.length - stripped.length;
44
- return titled.slice(0, at + shift) + '\n' + block + drawingBlock + titled.slice(at + shift);
52
+ return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
45
53
  }
46
54
  export function unwrap(pageHtml) {
47
- let html = pageHtml, text = null, name = null;
55
+ let html = pageHtml, text = null, name = null, view = null;
48
56
  const a = pageHtml.indexOf(START);
49
57
  if (a >= 0) {
50
58
  const open = pageHtml.indexOf('>', a), b = pageHtml.indexOf(END, open);
51
59
  if (open < 0 || b < 0) throw new Error('a carried document without its end');
52
60
  name = /data-name="([^"]*)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
61
+ view = /data-view="(draw|notes)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
53
62
  text = decodeCarried(pageHtml.slice(open + 1, b));
54
63
  html = pageHtml.slice(0, a) + pageHtml.slice(b + END.length);
55
64
  if (a > 0 && html[a - 1] === '\n') html = html.slice(0, a - 1) + html.slice(a);
@@ -64,23 +73,43 @@ export function unwrap(pageHtml) {
64
73
  html = html.slice(0, d) + html.slice(b + END.length);
65
74
  if (d > 0 && html[d - 1] === '\n') html = html.slice(0, d - 1) + html.slice(d);
66
75
  }
67
- return {html, text, name, drawing, drawingName};
76
+ let base = null, baseName = null;
77
+ const e = html.indexOf(BASE_START);
78
+ if (e >= 0) {
79
+ const open = html.indexOf('>', e), b = html.indexOf(END, open);
80
+ if (open < 0 || b < 0) throw new Error('a carried base without its end');
81
+ baseName = /data-name="([^"]*)"/.exec(html.slice(e, open))?.[1] ?? null;
82
+ base = decodeCarried(html.slice(open + 1, b));
83
+ html = html.slice(0, e) + html.slice(b + END.length);
84
+ if (e > 0 && html[e - 1] === '\n') html = html.slice(0, e - 1) + html.slice(e);
85
+ }
86
+ return {html, text, name, view, drawing, drawingName, base, baseName};
68
87
  }
69
- // Unknown options, a bare --drawing and a third name are refused, never dropped.
70
- const USAGE = 'usage: rapier-html <document.md> [out.html] [--drawing sketch.svg]\n' +
88
+ // Unknown options, a bare --drawing or --base and a third name are refused, never dropped.
89
+ const USAGE = 'usage: rapier-html <document.md> [out.html] [--view draw|notes] [--drawing sketch.svg] [--base original.md]\n' +
71
90
  ' writes <document>.rapier.html beside the file: Rapier with the document inside it, one file, offline.\n' +
72
91
  ' It never overwrites: an output that already exists, the document itself included, is refused.\n' +
92
+ ' --view opens the page on Draw (sketch and paint, the document behind it) or on Notes\n' +
73
93
  ' --drawing carries an SVG drawing alongside the document, opened on Draw as the page boots\n' +
94
+ ' --base carries the text the document was proposed against; the page opens on their diff\n' +
74
95
  ' -- everything after it is a filename, even one that starts with a dash\n' +
75
96
  ' --help this text';
76
97
  export async function main(argv, pagesDir, {log = console.log} = {}) {
77
98
  const names = [];
78
- let drawingPath = null, onlyNames = false;
99
+ let drawingPath = null, basePath = null, view = null, onlyNames = false;
79
100
  for (let i = 0; i < argv.length; i++) {
80
101
  const arg = argv[i];
81
102
  if (onlyNames || !arg.startsWith('-')) names.push(arg);
82
103
  else if (arg === '--') onlyNames = true;
83
104
  else if (arg === '--help') { log(USAGE); return null; }
105
+ else if (arg === '--view') {
106
+ if (view !== null || !['draw', 'notes'].includes(argv[i + 1])) throw new Error('--view takes draw or notes');
107
+ view = argv[++i];
108
+ }
109
+ else if (arg === '--base') {
110
+ if (basePath !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--base takes one text file (write a name that starts with a dash as ./-original.md)');
111
+ basePath = argv[++i];
112
+ }
84
113
  else if (arg !== '--drawing') throw new Error('unknown option ' + arg + '\n' + USAGE);
85
114
  else if (drawingPath !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--drawing takes one SVG file (write a name that starts with a dash as ./-sketch.svg)');
86
115
  else drawingPath = argv[++i];
@@ -93,10 +122,12 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
93
122
  const out = named || join(dirname(input), name.replace(/\.(md|markdown|txt)$/i, '') + '.rapier.html');
94
123
  const wrapOptions = {};
95
124
  if (drawingPath) { wrapOptions.drawing = await readFile(drawingPath, 'utf8'); wrapOptions.drawingName = basename(drawingPath); }
125
+ if (basePath) { wrapOptions.base = await readFile(basePath, 'utf8'); wrapOptions.baseName = basename(basePath); }
126
+ if (view) wrapOptions.view = view;
96
127
  // R85b: always a new file; 'wx' refuses any existing path, links included.
97
128
  try { await writeFile(out, wrap(page, text, name, wrapOptions), {flag: 'wx'}); }
98
129
  catch (error) { throw error.code === 'EEXIST' ? new Error(out + ' already exists, and rapier-html never overwrites a file: name a new one') : error; }
99
130
  log(out + ' (' + name + ', ' + Buffer.byteLength(text) + ' bytes of document' +
100
- (drawingPath ? ', ' + basename(drawingPath) + ' drawing' : '') + ')');
131
+ (view ? ', opens on ' + (view === 'draw' ? 'Draw' : 'Notes') : '') + (drawingPath ? ', ' + basename(drawingPath) + ' drawing' : '') + (basePath ? ', its diff from ' + basename(basePath) : '') + ')');
101
132
  return out;
102
133
  }