rapier-html 1.1.51 → 1.1.54

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.51",
3
+ "version": "1.1.54",
4
4
  "path": "rapier.html",
5
- "bytes": 1695238,
6
- "sha256": "0c0bee8e05c7bc5854d937ba04bbc9259e05992a8d487b04b4aaab289c8b6361",
7
- "builtAt": "2026-10-03T15:36:59.572Z",
5
+ "bytes": 1767512,
6
+ "sha256": "25ba82eb3e89dae0820504205c5d0ffd29c1d14b573c0869bd06404a562e5da2",
7
+ "builtAt": "2026-10-03T21:55:34.551Z",
8
8
  "node": "v22.22.2",
9
9
  "canonical": true,
10
10
  "packing": "zopfli",
package/README.md CHANGED
@@ -8,20 +8,20 @@ editor, offline, with the document inside.
8
8
  - `--return` adds a one-use Send back address from the agent door: the person edits offline, presses Send back,
9
9
  and the agent reads the returned copy.
10
10
  - `--view draw` opens on Draw; `--base original.md` opens on the diff of a proposed change, and the person keeps
11
- or drops each change.
11
+ or drops each change; `--by <name>` says on that diff who proposed it and when.
12
12
  - `wrap` and `unwrap` carry a document into a page and out again, exactly, for programs.
13
13
 
14
14
  Node 22 or newer.
15
15
 
16
16
  ```sh
17
- npx -- rapier-html@1.1.51 notes.md # the editor, notes.md inside it, written beside the file
18
- npx -- rapier-html@1.1.51 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
19
- npx -- rapier-html@1.1.51 notes.md --view notes # opens the Notes library
20
- npx -- rapier-html@1.1.51 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
21
- npx -- rapier-html@1.1.51 proposal.md --base original.md # opens on the diff of a proposed change
22
- npx -- rapier-html@1.1.51 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
23
- npx -- rapier-html@1.1.51 notes.md out.html # a named output
24
- npm install rapier-html@1.1.51 # as a library: wrap, unwrap
17
+ npx -- rapier-html@1.1.54 notes.md # the editor, notes.md inside it, written beside the file
18
+ npx -- rapier-html@1.1.54 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
19
+ npx -- rapier-html@1.1.54 notes.md --view notes # opens the Notes library
20
+ npx -- rapier-html@1.1.54 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
21
+ npx -- rapier-html@1.1.54 proposal.md --base original.md # opens on the diff of a proposed change
22
+ npx -- rapier-html@1.1.54 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
23
+ npx -- rapier-html@1.1.54 notes.md out.html # a named output
24
+ npm install rapier-html@1.1.54 # as a library: wrap, unwrap
25
25
  ```
26
26
 
27
27
  The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-html",
3
- "version": "1.1.51",
3
+ "version": "1.1.54",
4
4
  "description": "Give a person a document they can read, edit and keep: one command writes one offline HTML file that is the whole Markdown editor with the document inside; draw, paint, review a diff, Send back to the agent.",
5
5
  "keywords": [
6
6
  "markdown",
package/page.mjs CHANGED
@@ -23,6 +23,9 @@ function decodeCarried(text) {
23
23
  }
24
24
  export {encodeCarried, decodeCarried};
25
25
 
26
+ const PROPOSED_AT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/;
27
+ const attribute = text => text.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
28
+ const unattribute = text => text.replace(/&(quot|lt|gt|amp);/g, (_, e) => ({quot: '"', lt: '<', gt: '>', amp: '&'})[e]);
26
29
  function safeName(name, fallback) {
27
30
  return String(name || fallback).replace(/[\\/\0"<>&]/g, '-').slice(0, 255) || fallback;
28
31
  }
@@ -45,9 +48,15 @@ export function wrap(pageHtml, text, name, options) {
45
48
  const drawingName = safeName(opts.drawingName, 'drawing.svg');
46
49
  carriedBlocks = '\n' + DRAWING_START + ' data-name="' + drawingName + '">' + encodeCarried(opts.drawing) + END;
47
50
  }
51
+ if ((opts.by != null || opts.at != null) && opts.base == null) throw new Error('a proposer needs the base it proposes over');
48
52
  if (opts.base != null) {
49
53
  if (typeof opts.base !== 'string') throw new Error('the base must be text');
50
- carriedBlocks += '\n' + BASE_START + ' data-name="' + safeName(opts.baseName, docName) + '">' + encodeCarried(opts.base) + END;
54
+ // A proposal page (docs/briefs/ledger.md section 3): who proposed it, in the host's own words, and when.
55
+ const by = opts.by == null ? null : String(opts.by).replace(/[\0-\x1f\x7f]/g, ' ').trim().slice(0, 96);
56
+ const at = opts.at == null ? null : String(opts.at);
57
+ if (at != null && !(PROPOSED_AT.test(at) && Number.isFinite(Date.parse(at)))) throw new Error('the time of a proposal is an ISO 8601 UTC timestamp');
58
+ carriedBlocks += '\n' + BASE_START + ' data-name="' + safeName(opts.baseName, docName) + '"' + (by ? ' data-by="' + attribute(by) + '"' : '') +
59
+ (at ? ' data-at="' + at + '"' : '') + '>' + encodeCarried(opts.base) + END;
51
60
  }
52
61
  // The agent seed goes after the charset (or <head>), once: a page that already carries it, wrapped again, loses its old one.
53
62
  const unseeded = unwrap(pageHtml).html.replace(SEARCH_WORDS, '').split(PAGE_SEED + '\n').join('');
@@ -91,32 +100,36 @@ export function unwrap(pageHtml) {
91
100
  html = html.slice(0, d) + html.slice(b + END.length);
92
101
  if (d > 0 && html[d - 1] === '\n') html = html.slice(0, d - 1) + html.slice(d);
93
102
  }
94
- let base = null, baseName = null;
103
+ let base = null, baseName = null, by = null, at = null;
95
104
  const e = html.indexOf(BASE_START);
96
105
  if (e >= 0) {
97
106
  const open = html.indexOf('>', e), b = html.indexOf(END, open);
98
107
  if (open < 0 || b < 0) throw new Error('a carried base without its end');
99
108
  baseName = /data-name="([^"]*)"/.exec(html.slice(e, open))?.[1] ?? null;
109
+ const proposer = /data-by="([^"]*)"/.exec(html.slice(e, open))?.[1];
110
+ by = proposer == null ? null : unattribute(proposer);
111
+ at = /data-at="([^"]*)"/.exec(html.slice(e, open))?.[1] ?? null;
100
112
  base = decodeCarried(html.slice(open + 1, b));
101
113
  html = html.slice(0, e) + html.slice(b + END.length);
102
114
  if (e > 0 && html[e - 1] === '\n') html = html.slice(0, e - 1) + html.slice(e);
103
115
  }
104
- return {html, text, name, view, drawing, drawingName, base, baseName, return: address, return_expires_at: expiry};
116
+ return {html, text, name, view, drawing, drawingName, base, baseName, by, at, return: address, return_expires_at: expiry};
105
117
  }
106
118
  // Unknown options, a bare --drawing or --base and a third name are refused, never dropped.
107
- const USAGE = 'usage: rapier-html <document.md> [out.html] [--view draw|notes] [--drawing sketch.svg] [--base original.md] [--return <url> --return-expires-at <timestamp>]\n' +
119
+ const USAGE = 'usage: rapier-html <document.md> [out.html] [--view draw|notes] [--drawing sketch.svg] [--base original.md [--by <your name>]] [--return <url> --return-expires-at <timestamp>]\n' +
108
120
  ' writes <document>.rapier.html beside the file: Rapier with the document inside it, one file, offline.\n' +
109
121
  ' It never overwrites: an output that already exists, the document itself included, is refused.\n' +
110
122
  ' --view opens the page on Draw (sketch and paint, the document behind it) or on Notes\n' +
111
123
  ' --drawing carries an SVG drawing alongside the document, opened on Draw as the page boots\n' +
112
124
  ' --base carries the text the document was proposed against; the page opens on their diff\n' +
125
+ ' --by with --base: who proposes it, shown on the diff with the time the page was made\n' +
113
126
  ' --return carries a one-use return URL from document.create_return; Share offers Send back\n' +
114
127
  ' --return-expires-at carries the same mint\'s expiry; Share offers Save once the return expires\n' +
115
128
  ' -- everything after it is a filename, even one that starts with a dash\n' +
116
129
  ' --help this text';
117
130
  export async function main(argv, pagesDir, {log = console.log} = {}) {
118
131
  const names = [];
119
- let drawingPath = null, basePath = null, view = null, address = null, expiry = null, onlyNames = false;
132
+ let drawingPath = null, basePath = null, by = null, view = null, address = null, expiry = null, onlyNames = false;
120
133
  for (let i = 0; i < argv.length; i++) {
121
134
  const arg = argv[i];
122
135
  if (onlyNames || !arg.startsWith('-')) names.push(arg);
@@ -130,6 +143,10 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
130
143
  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)');
131
144
  basePath = argv[++i];
132
145
  }
146
+ else if (arg === '--by') {
147
+ if (by !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--by takes one name');
148
+ by = argv[++i];
149
+ }
133
150
  else if (arg === '--return') {
134
151
  if (address !== null || !argv[i + 1] || argv[i + 1].startsWith('-')) throw new Error('--return takes one HTTPS return URL');
135
152
  address = returnAddress(argv[++i]);
@@ -150,7 +167,9 @@ export async function main(argv, pagesDir, {log = console.log} = {}) {
150
167
  const out = named || join(dirname(input), name.replace(/\.(md|markdown|txt)$/i, '') + '.rapier.html');
151
168
  const wrapOptions = {};
152
169
  if (drawingPath) { wrapOptions.drawing = await readFile(drawingPath, 'utf8'); wrapOptions.drawingName = basename(drawingPath); }
170
+ if (by !== null && !basePath) throw new Error('--by names who proposes a change: give the --base it proposes over');
153
171
  if (basePath) { wrapOptions.base = await readFile(basePath, 'utf8'); wrapOptions.baseName = basename(basePath); }
172
+ if (by !== null) { wrapOptions.by = by; wrapOptions.at = new Date().toISOString(); }
154
173
  if (view) wrapOptions.view = view;
155
174
  if (address) wrapOptions.return = address;
156
175
  if (expiry) wrapOptions.return_expires_at = expiry;