rapier-html 1.1.59 → 1.1.62

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/dist/wrap.mjs ADDED
@@ -0,0 +1,108 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {encodeCarried, decodeCarried, writeParts, takeParts, readDocument, readBase, writeBase, takeBase} from './ledger/carried.mjs';
3
+ import {sha256} from './ledger/hash.mjs';
4
+ import {PAGE_SEED, returnAddress, returnExpiresAt} from './return-address.mjs';
5
+
6
+ const START = '<script type="text/markdown" id="rapier-document"';
7
+ const DRAWING_START = '<script type="text/plain" id="rapier-drawing"';
8
+ // The text the document was proposed against (docs/page-door.md, "2. A change carried"): the page opens on its diff.
9
+ const END = '</script>';
10
+ // The page's search words (rapier.html's RAPIER_SEO regions: description, canonical address, previews, structured data and the plain guide
11
+ // a crawler reads) are rapier.website's own. A page that carries someone's document never claims that address or that description.
12
+ const SEARCH_WORDS = /<!-- RAPIER_SEO_BEGIN -->[\s\S]*?<!-- RAPIER_SEO_END -->\n?/g;
13
+
14
+ // One encoding, exactly reversible (docs/page-door.md, "What a block may hold"): no `<` before `/` or `!`, no CR, no NUL.
15
+ // engine.js _rapierDecodeCarried is its inverse.
16
+ export {encodeCarried, decodeCarried};
17
+
18
+ function safeName(name, fallback) {
19
+ return String(name || fallback).replace(/[\\/\0"<>&]/g, '-').slice(0, 255) || fallback;
20
+ }
21
+
22
+ export function wrap(pageHtml, text, name, options) {
23
+ if (typeof pageHtml !== 'string' || !pageHtml.includes('</body>')) throw new Error('not a Rapier page: no </body>');
24
+ if (typeof text !== 'string') throw new Error('the document must be text');
25
+ const saved = readDocument(text);
26
+ text = saved.text;
27
+ const previous = unwrap(pageHtml);
28
+ const opts = {...saved, ...(options || {})};
29
+ const docName = safeName(name, 'document.md');
30
+ // `view`: the view the page opens on with the document behind it, `draw` or `notes` (docs/page-door.md).
31
+ if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
32
+ const address = opts.return == null ? null : returnAddress(opts.return);
33
+ const expiry = opts.return_expires_at == null ? null : returnExpiresAt(opts.return_expires_at);
34
+ if (!!address !== !!expiry) throw new Error('a return needs its URL and return_expires_at from document.create_return');
35
+ const block = START + ' data-name="' + docName + '"' + (opts.view ? ' data-view="' + opts.view + '"' : '') +
36
+ (address ? ' data-return="' + address + '" data-return-expires-at="' + expiry + '"' : '') + '>' + encodeCarried(text) + END;
37
+ let carriedBlocks = '\n' + writeParts(text, opts);
38
+ if (opts.drawing != null) {
39
+ if (typeof opts.drawing !== 'string') throw new Error('the drawing must be SVG text');
40
+ const drawingName = safeName(opts.drawingName, 'drawing.svg');
41
+ carriedBlocks += '\n' + DRAWING_START + ' data-name="' + drawingName + '">' + encodeCarried(opts.drawing) + END;
42
+ }
43
+ if ((opts.by != null || opts.at != null) && opts.base == null && previous.base == null) throw new Error('a proposer needs the base it proposes over');
44
+ let baseline = previous.proposalBase;
45
+ if (!baseline && opts.base != null) {
46
+ if (typeof opts.base === 'object') baseline = readBase(opts.base);
47
+ else {
48
+ if (typeof opts.base !== 'string') throw new Error('the base must be text');
49
+ // A returned Share page uses its own source dialect; its original base is the same carrier.
50
+ // Read that base before optional current-document history, which belongs to the current text.
51
+ const carriedBase = /^\s*<!doctype html/i.test(opts.base) ? takeBase(opts.base).base : null;
52
+ const pageBase = !carriedBase && /^\s*<!doctype html/i.test(opts.base) ? unwrap(opts.base) : null;
53
+ const source = carriedBase ? {text: carriedBase.text} : pageBase?.text != null ? readDocument(pageBase.text) : readDocument(opts.base);
54
+ baseline = carriedBase || pageBase?.proposalBase || readBase({text: source.text, sha256: sha256(source.text),
55
+ revision: source.ledger?.head.root || pageBase?.ledger?.head.root || sha256(source.text), name: safeName(opts.baseName || pageBase?.name, docName),
56
+ by: opts.by == null ? 'Agent' : String(opts.by).trim(), at: opts.at == null ? new Date().toISOString() : String(opts.at)});
57
+ }
58
+ }
59
+ // Re-proposing retains the original text/hash/revision; the current proposer identifies this proposal.
60
+ if (baseline) baseline = readBase({...baseline, ...(opts.by != null ? {by: String(opts.by).trim()} : {}), ...(opts.at != null ? {at: String(opts.at)} : {})});
61
+ carriedBlocks += '\n' + writeBase(text, baseline);
62
+ // The agent seed goes after the charset (or <head>), once: a page that already carries it, wrapped again, loses its old one.
63
+ const unseeded = previous.html.replace(SEARCH_WORDS, '').split(PAGE_SEED + '\n').join('');
64
+ const headTag = /<meta charset[^>]*>\n?/i.exec(unseeded) || /<head[^>]*>\n?/.exec(unseeded);
65
+ if (!headTag) throw new Error('not a Rapier page: no <head>');
66
+ const seededAt = headTag.index + headTag[0].length;
67
+ const stripped = unseeded.slice(0, seededAt) + PAGE_SEED + '\n' + unseeded.slice(seededAt);
68
+ // Before Rapier's scripts: parsed before the engine boots.
69
+ const bodyTag = /<body[^>]*>/.exec(stripped);
70
+ if (!bodyTag) throw new Error('not a Rapier page: no <body>');
71
+ const at = bodyTag.index + bodyTag[0].length;
72
+ // A function, not a string: `$'`, `$&` and `` $` `` are replacement patterns.
73
+ const titled = stripped.replace(/<title>[^<]*<\/title>/, () => '<title>' + docName.replace(/&/g, '&amp;').replace(/</g, '&lt;') + ' — Rapier</title>');
74
+ const shift = titled.length - stripped.length;
75
+ return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
76
+ }
77
+ export function unwrap(pageHtml) {
78
+ let html = pageHtml, text = null, name = null, view = null, address = null, expiry = null;
79
+ const a = pageHtml.indexOf(START);
80
+ if (a >= 0) {
81
+ const open = pageHtml.indexOf('>', a), b = pageHtml.indexOf(END, open);
82
+ if (open < 0 || b < 0) throw new Error('a carried document without its end');
83
+ name = /data-name="([^"]*)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
84
+ view = /data-view="(draw|notes)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
85
+ const carriedReturn = /data-return="([^"]*)"/.exec(pageHtml.slice(a, open));
86
+ if (carriedReturn) address = returnAddress(carriedReturn[1]);
87
+ const carriedExpiry = /data-return-expires-at="([^"]*)"/.exec(pageHtml.slice(a, open));
88
+ if (carriedExpiry) expiry = returnExpiresAt(carriedExpiry[1]);
89
+ if (!!address !== !!expiry) throw new Error('a carried return needs its URL and return_expires_at');
90
+ text = decodeCarried(pageHtml.slice(open + 1, b));
91
+ html = pageHtml.slice(0, a) + pageHtml.slice(b + END.length);
92
+ if (a > 0 && html[a - 1] === '\n') html = html.slice(0, a - 1) + html.slice(a);
93
+ }
94
+ let drawing = null, drawingName = null;
95
+ const d = html.indexOf(DRAWING_START);
96
+ if (d >= 0) {
97
+ const open = html.indexOf('>', d), b = html.indexOf(END, open);
98
+ if (open < 0 || b < 0) throw new Error('a carried drawing without its end');
99
+ drawingName = /data-name="([^"]*)"/.exec(html.slice(d, open))?.[1] ?? null;
100
+ drawing = decodeCarried(html.slice(open + 1, b));
101
+ html = html.slice(0, d) + html.slice(b + END.length);
102
+ if (d > 0 && html[d - 1] === '\n') html = html.slice(0, d - 1) + html.slice(d);
103
+ }
104
+ const carriedBase = takeBase(html); html = carriedBase.html;
105
+ const proposalBase = carriedBase.base;
106
+ const parts = takeParts(html, text);
107
+ return {...parts, text, name, view, drawing, drawingName, proposalBase, base: proposalBase?.text ?? null, baseName: proposalBase?.name ?? null, baseSha256: proposalBase?.sha256 ?? null, baseRevision: proposalBase?.revision ?? null, by: proposalBase?.by ?? null, at: proposalBase?.at ?? null, return: address, return_expires_at: expiry};
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-html",
3
- "version": "1.1.59",
3
+ "version": "1.1.62",
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",