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 +4 -4
- package/README.md +66 -63
- package/package.json +2 -2
- package/page.mjs +41 -10
- package/rapier.html +6 -6
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"path": "rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-09-
|
|
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
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
16
|
+
The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
## What the page can do
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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');
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
##
|
|
69
|
+
## How the page carries the document
|
|
80
70
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Rapier
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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, '&').replace(/</g, '<') + ' — Rapier</title>');
|
|
43
51
|
const shift = titled.length - stripped.length;
|
|
44
|
-
return titled.slice(0, at + shift) + '\n' + block +
|
|
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
|
-
|
|
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
|
}
|