rapier-html 1.1.99 → 1.1.102

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 CHANGED
@@ -10,6 +10,8 @@ const END = '</script>';
10
10
  // The page's search words (rapier.html's RAPIER_SEO regions: description, canonical address, previews, structured data and the plain guide
11
11
  // a crawler reads) are rapier.website's own. A page that carries someone's document never claims that address or that description.
12
12
  const SEARCH_WORDS = /<!-- RAPIER_SEO_BEGIN -->[\s\S]*?<!-- RAPIER_SEO_END -->\n?/g;
13
+ const OFFLINE_POLICY = "default-src 'none'; script-src 'unsafe-inline' 'wasm-unsafe-eval' blob:; style-src 'unsafe-inline'; " +
14
+ "img-src data: blob:; media-src data: blob:; font-src data: blob:; connect-src 'none'; worker-src blob:; manifest-src 'none'; base-uri 'none'; form-action 'none'";
13
15
 
14
16
  // One encoding, exactly reversible: no `<` before `/` or `!`, no CR, no NUL. engine.js _rapierDecodeCarried is its
15
17
  // inverse.
@@ -19,6 +21,26 @@ function safeName(name, fallback) {
19
21
  return [...String(name || fallback).replace(/[\\/\0"<>&]/g, '-')].slice(0, 256).join('').replace(/[\uD800-\uDBFF]$/, '') || fallback;
20
22
  }
21
23
 
24
+ // Only the trusted shell head changes. Carried source and inert application bytes stay exact.
25
+ export function offlineTemplate(pageHtml, address = null) {
26
+ const end = pageHtml.indexOf('</head>');
27
+ if (end < 0) throw new Error('not a Rapier page: no </head>');
28
+ const attribute = (tag, name) => {
29
+ const match = new RegExp('\\b' + name + '\\s*=\\s*(?:"([^"]*)"|\'([^\']*)\'|([^\\s>]+))', 'i').exec(tag);
30
+ return match ? match[1] ?? match[2] ?? match[3] : '';
31
+ };
32
+ const head = pageHtml.slice(0, end).replace(/<meta\b[^>]*>/gi, tag =>
33
+ attribute(tag, 'http-equiv').toLowerCase() === 'content-security-policy' ? '' : tag).replace(/<link\b[^>]*>/gi, tag =>
34
+ attribute(tag, 'rel').toLowerCase().split(/\s+/).some(rel => ['icon', 'apple-touch-icon', 'manifest', 'preconnect', 'dns-prefetch'].includes(rel)) ? '' : tag);
35
+ const anchor = /<meta charset[^>]*>\n?/i.exec(head) || /<head[^>]*>\n?/.exec(head);
36
+ if (!anchor) throw new Error('not a Rapier page: no <head>');
37
+ const at = anchor.index + anchor[0].length;
38
+ // A minted return is an explicit Send back destination; boot and rendering stay local.
39
+ const policy = address == null ? OFFLINE_POLICY : OFFLINE_POLICY.replace("connect-src 'none'", 'connect-src ' + new URL(returnAddress(address)).origin);
40
+ return head.slice(0, at) + '<meta http-equiv="Content-Security-Policy" content="' + policy + '">\n' +
41
+ head.slice(at) + pageHtml.slice(end);
42
+ }
43
+
22
44
  export function wrap(pageHtml, text, name, options) {
23
45
  if (typeof pageHtml !== 'string' || !pageHtml.includes('</body>')) throw new Error('not a Rapier page: no </body>');
24
46
  if (typeof text !== 'string') throw new Error('the document must be text');
@@ -27,12 +49,13 @@ export function wrap(pageHtml, text, name, options) {
27
49
  const previous = unwrap(pageHtml);
28
50
  const opts = {...saved, ...(options || {})};
29
51
  const docName = safeName(name, 'document.md');
52
+ if (opts.kind != null && !['markdown', 'text', 'code'].includes(opts.kind)) throw new Error('invalid document kind');
30
53
  // `view`: the view the page opens on with the document behind it, `draw` or `notes`.
31
54
  if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
32
55
  const address = opts.return == null ? null : returnAddress(opts.return);
33
56
  const expiry = opts.return_expires_at == null ? null : returnExpiresAt(opts.return_expires_at);
34
57
  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 + '"' : '') +
58
+ const block = START + ' data-name="' + docName + '"' + (opts.kind ? ' data-kind="' + opts.kind + '"' : '') + (opts.view ? ' data-view="' + opts.view + '"' : '') +
36
59
  (address ? ' data-return="' + address + '" data-return-expires-at="' + expiry + '"' : '') + '>' + encodeCarried(text) + END;
37
60
  let carriedBlocks = '\n' + writeParts(text, opts);
38
61
  if (opts.drawing != null) {
@@ -60,7 +83,7 @@ export function wrap(pageHtml, text, name, options) {
60
83
  if (baseline) baseline = readBase({...baseline, ...(opts.by != null ? {by: String(opts.by).trim()} : {}), ...(opts.at != null ? {at: String(opts.at)} : {})});
61
84
  carriedBlocks += '\n' + writeBase(text, baseline);
62
85
  // 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('');
86
+ const unseeded = offlineTemplate(previous.html.replace(SEARCH_WORDS, '').split(PAGE_SEED + '\n').join(''), address);
64
87
  const headTag = /<meta charset[^>]*>\n?/i.exec(unseeded) || /<head[^>]*>\n?/.exec(unseeded);
65
88
  if (!headTag) throw new Error('not a Rapier page: no <head>');
66
89
  const seededAt = headTag.index + headTag[0].length;
@@ -75,12 +98,13 @@ export function wrap(pageHtml, text, name, options) {
75
98
  return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
76
99
  }
77
100
  export function unwrap(pageHtml) {
78
- let html = pageHtml, text = null, name = null, view = null, address = null, expiry = null;
101
+ let html = pageHtml, text = null, name = null, kind = null, view = null, address = null, expiry = null;
79
102
  const a = pageHtml.indexOf(START);
80
103
  if (a >= 0) {
81
104
  const open = pageHtml.indexOf('>', a), b = pageHtml.indexOf(END, open);
82
105
  if (open < 0 || b < 0) throw new Error('a carried document without its end');
83
106
  name = /data-name="([^"]*)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
107
+ kind = /data-kind="(markdown|text|code)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
84
108
  view = /data-view="(draw|notes)"/.exec(pageHtml.slice(a, open))?.[1] ?? null;
85
109
  const carriedReturn = /data-return="([^"]*)"/.exec(pageHtml.slice(a, open));
86
110
  if (carriedReturn) address = returnAddress(carriedReturn[1]);
@@ -104,5 +128,5 @@ export function unwrap(pageHtml) {
104
128
  const carriedBase = takeBase(html); html = carriedBase.html;
105
129
  const comparisonBase = carriedBase.base;
106
130
  const parts = takeParts(html, text);
107
- return {...parts, text, name, view, drawing, drawingName, comparisonBase, base: comparisonBase?.text ?? null, baseName: comparisonBase?.name ?? null, baseSha256: comparisonBase?.sha256 ?? null, baseRevision: comparisonBase?.revision ?? null, by: comparisonBase?.by ?? null, at: comparisonBase?.at ?? null, return: address, return_expires_at: expiry};
131
+ return {...parts, text, name, kind, view, drawing, drawingName, comparisonBase, base: comparisonBase?.text ?? null, baseName: comparisonBase?.name ?? null, baseSha256: comparisonBase?.sha256 ?? null, baseRevision: comparisonBase?.revision ?? null, by: comparisonBase?.by ?? null, at: comparisonBase?.at ?? null, return: address, return_expires_at: expiry};
108
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-html",
3
- "version": "1.1.99",
3
+ "version": "1.1.102",
4
4
  "description": "One command turns a Markdown file into one offline HTML file: the whole Rapier editor with the document inside. Opens in any browser, phone or desktop, with no install or account. Write, draw and paint watercolor, then Send back the edits to your AI agent.",
5
5
  "keywords": [
6
6
  "markdown",