rapier-html 1.2.8 → 1.2.9

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.
@@ -1,9 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- // Shared by the writer and the page. Every page Rapier writes (page.mjs's wrap, the editor's Share and HTML export) carries
3
- // PAGE_SEED once, after its charset (which must stay in the first 1024 bytes): an HTML comment no person sees that tells an
4
- // agent what the file is and how to make one, never part of the carried document.
5
2
  export const PAGE_SEED = '<!-- This page is a Rapier document: Markdown in one HTML file, its source inside, offline. Read it in any browser; edit it in Rapier, https://rapier.website. Agents: https://rapier.website/agents. `npx rapier-html document.md` makes a page like this. -->';
6
- // A file can only return to this worker's one-use route.
7
3
  export const RETURN_ORIGIN = 'https://mcp.rapier.website';
8
4
  export function returnAddress(value) {
9
5
  if (typeof value !== 'string' || /[\s\u0000-\u001f\u007f-\u009f\\@?#]/u.test(value))
package/dist/wrap.mjs CHANGED
@@ -5,23 +5,47 @@ import {PAGE_SEED, returnAddress, returnExpiresAt} from './return-address.mjs';
5
5
 
6
6
  const START = '<script type="text/markdown" id="rapier-document"';
7
7
  const DRAWING_START = '<script type="text/plain" id="rapier-drawing"';
8
- // The original text is a display reference: the page opens on its diff from the current document.
9
8
  const END = '</script>';
10
9
  // 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
10
  const SEARCH_WORDS = /<!-- RAPIER_SEO_BEGIN -->[\s\S]*?<!-- RAPIER_SEO_END -->\n?/g;
13
11
  const OFFLINE_POLICY = "default-src 'none'; script-src 'unsafe-inline' 'wasm-unsafe-eval' blob:; style-src 'unsafe-inline'; " +
14
12
  "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'";
15
13
 
16
- // One encoding, exactly reversible: no `<` before `/` or `!`, no CR, no NUL. engine.js _rapierDecodeCarried is its
17
- // inverse.
18
14
  export {encodeCarried, decodeCarried};
19
15
 
16
+ const PREVIEW = /<meta (?:name="(?:description|generator)"|property="og:(?:title|description)") content="[^"]*" data-rapier-preview>\n?/g;
17
+ const plainWords = text => text.replace(/!\[[^\]]*\]\[[^\]]*\]|!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]*)\](?:\([^)]*\)|\[[^\]]*\])/g, '$1')
18
+ .replace(/<[^>]*>/g, '').replace(/[*_`~=]+|\[\^[^\]]*\]/g, '').replace(/\s+/g, ' ').trim();
19
+ export function pagePreview(markdown) {
20
+ const lines = String(markdown).replace(/^\uFEFF/, '').split(/\r?\n/);
21
+ let title = '', paragraph = [], fence = null, at = 0;
22
+ if (/^---\s*$/.test(lines[0] || '')) { at = lines.indexOf('---', 1) + 1 || lines.length; }
23
+ for (; at < lines.length; at++) {
24
+ const line = lines[at], marker = /^\s*(`{3,}|~{3,})/.exec(line)?.[1];
25
+ if (fence) { if (marker && marker[0] === fence[0] && marker.length >= fence.length) fence = null; continue; }
26
+ if (marker) { fence = marker; if (paragraph.length) break; continue; }
27
+ const heading = /^ {0,3}#{1,6}\s+(.*?)\s*#*\s*$/.exec(line);
28
+ if (heading) { if (paragraph.length) break; if (!title) title = plainWords(heading[1]); continue; }
29
+ if (!/\S/.test(line)) { if (paragraph.length) break; continue; }
30
+ if (!paragraph.length && /^\s*(?:[>|<]|[-*+]\s|\d+[.)]\s|\[[^\]]+\]:|!\[|\$\$|[-*_]{3,}\s*$)/.test(line)) continue;
31
+ paragraph.push(line);
32
+ if (title && paragraph.length > 8) break;
33
+ }
34
+ const words = plainWords(paragraph.join(' '));
35
+ const sentence = /^.{20,300}?[.!?](?=\s|$)/.exec(words)?.[0] || (words.length <= 200 ? words : words.slice(0, 200).replace(/\s+\S*$/, '') + '…');
36
+ return {title, description: sentence};
37
+ }
38
+ function previewHead(markdown, fallbackTitle) {
39
+ const {title, description} = pagePreview(markdown);
40
+ const meta = (key, name, value) => '<meta ' + key + '="' + name + '" content="' + value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;') + '" data-rapier-preview>\n';
41
+ return (description ? meta('name', 'description', description) : '') + meta('property', 'og:title', title || fallbackTitle) +
42
+ (description ? meta('property', 'og:description', description) : '') + meta('name', 'generator', 'Rapier (https://rapier.website)');
43
+ }
44
+
20
45
  function safeName(name, fallback) {
21
46
  return [...String(name || fallback).replace(/[\\/\0"<>&]/g, '-')].slice(0, 256).join('').replace(/[\uD800-\uDBFF]$/, '') || fallback;
22
47
  }
23
48
 
24
- // Only the trusted shell head changes. Carried source and inert application bytes stay exact.
25
49
  export function offlineTemplate(pageHtml, address = null) {
26
50
  const end = pageHtml.indexOf('</head>');
27
51
  if (end < 0) throw new Error('not a Rapier page: no </head>');
@@ -35,14 +59,11 @@ export function offlineTemplate(pageHtml, address = null) {
35
59
  const anchor = /<meta charset[^>]*>\n?/i.exec(head) || /<head[^>]*>\n?/.exec(head);
36
60
  if (!anchor) throw new Error('not a Rapier page: no <head>');
37
61
  const at = anchor.index + anchor[0].length;
38
- // A minted return is an explicit Send back destination; boot and rendering stay local.
39
62
  const policy = address == null ? OFFLINE_POLICY : OFFLINE_POLICY.replace("connect-src 'none'", 'connect-src ' + new URL(returnAddress(address)).origin);
40
63
  return head.slice(0, at) + '<meta http-equiv="Content-Security-Policy" content="' + policy + '">\n' +
41
64
  head.slice(at) + pageHtml.slice(end);
42
65
  }
43
66
 
44
- // What a carried page may hold beyond the base editor: Draw (its part and its letter sets) and the plug-ins a document
45
- // displays with. Notes and the font subsetter never travel; without the subsetter a page keeps its fonts whole.
46
67
  export const CHOICES = Object.freeze(['drawing', 'math', 'mermaid']);
47
68
  const INVENTORY = '<script type="application/json" id="rapier-builtin-plugins">';
48
69
  const element = (html, id) => {
@@ -52,15 +73,12 @@ const element = (html, id) => {
52
73
  return {start, end: html[end] === '\n' ? end + 1 : end};
53
74
  };
54
75
 
55
- // The full carried editor cut to the base editor and `include` (ids from CHOICES). Only whole inert elements and the
56
- // inventory rows naming them are removed; every kept byte is unchanged.
57
76
  export function carry(pageHtml, include = []) {
58
77
  const kept = new Set(include);
59
78
  for (const id of kept) if (!CHOICES.includes(id)) throw new Error('a carried page includes ' + CHOICES.join(', ') + '; not ' + id);
60
79
  if (kept.has('drawing')) kept.add('letters');
61
80
  let html = pageHtml;
62
81
  const cut = id => { const at = element(html, id); if (at) html = html.slice(0, at.start) + html.slice(at.end); };
63
- // Parts beyond the base editor (Notes, page layout) never travel, as in the person's HTML editor file; Draw when chosen.
64
82
  for (const id of [...new Set([...html.matchAll(/<script type="application\/rapier-runtime" id="(rapier-part-[a-z-]+)">/g)].map(match => match[1]))])
65
83
  if (id !== 'rapier-part-drawing' || !kept.has('drawing')) cut(id);
66
84
  const start = html.indexOf(INVENTORY);
@@ -72,8 +90,6 @@ export function carry(pageHtml, include = []) {
72
90
  return html;
73
91
  }
74
92
 
75
- // The plug-ins a Markdown text displays with, read without a Markdown parser, so it leans toward carrying: math for a `$`
76
- // or `$$` span outside code, Mermaid for a mermaid fence (`flowchart(fence)` true marks one Rapier draws natively).
77
93
  export function portableNeeds(text, {base, flowchart} = {}) {
78
94
  const needs = new Set();
79
95
  for (const source of [text, base]) {
@@ -101,7 +117,6 @@ export function wrap(pageHtml, text, name, options) {
101
117
  const opts = {...saved, ...(options || {})};
102
118
  const docName = safeName(name, 'document.md');
103
119
  if (opts.kind != null && !['markdown', 'text', 'code'].includes(opts.kind)) throw new Error('invalid document kind');
104
- // `view`: the view the page opens on with the document behind it, `draw` or `notes`.
105
120
  if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
106
121
  const address = opts.return == null ? null : returnAddress(opts.return);
107
122
  const expiry = opts.return_expires_at == null ? null : returnExpiresAt(opts.return_expires_at);
@@ -120,8 +135,6 @@ export function wrap(pageHtml, text, name, options) {
120
135
  if (typeof opts.base === 'object') baseline = readBase(opts.base);
121
136
  else {
122
137
  if (typeof opts.base !== 'string') throw new Error('the base must be text');
123
- // A returned Share page uses its own source dialect; its original base is the same carrier.
124
- // Read that base before optional current-document history, which belongs to the current text.
125
138
  const carriedBase = /^\s*<!doctype html/i.test(opts.base) ? takeBase(opts.base).base : null;
126
139
  const pageBase = !carriedBase && /^\s*<!doctype html/i.test(opts.base) ? unwrap(opts.base) : null;
127
140
  const source = carriedBase ? {text: carriedBase.text} : pageBase?.text != null ? readDocument(pageBase.text) : readDocument(opts.base);
@@ -130,21 +143,18 @@ export function wrap(pageHtml, text, name, options) {
130
143
  by: opts.by == null ? 'Agent' : String(opts.by).trim(), at: opts.at == null ? new Date().toISOString() : String(opts.at)});
131
144
  }
132
145
  }
133
- // Rewrapping retains the original text, hash and revision; attribution names this comparison.
134
146
  if (baseline) baseline = readBase({...baseline, ...(opts.by != null ? {by: String(opts.by).trim()} : {}), ...(opts.at != null ? {at: String(opts.at)} : {})});
135
147
  carriedBlocks += '\n' + writeBase(text, baseline);
136
- // The agent seed goes after the charset (or <head>), once: a page that already carries it, wrapped again, loses its old one.
137
- const unseeded = offlineTemplate(previous.html.replace(SEARCH_WORDS, '').split(PAGE_SEED + '\n').join(''), address);
148
+ const unseeded = offlineTemplate(previous.html.replace(SEARCH_WORDS, '').replace(PREVIEW, '').split(PAGE_SEED + '\n').join(''), address);
138
149
  const headTag = /<meta charset[^>]*>\n?/i.exec(unseeded) || /<head[^>]*>\n?/.exec(unseeded);
139
150
  if (!headTag) throw new Error('not a Rapier page: no <head>');
140
151
  const seededAt = headTag.index + headTag[0].length;
141
152
  const stripped = unseeded.slice(0, seededAt) + PAGE_SEED + '\n' + unseeded.slice(seededAt);
142
- // Before Rapier's scripts: parsed before the engine boots.
143
153
  const bodyTag = /<body[^>]*>/.exec(stripped);
144
154
  if (!bodyTag) throw new Error('not a Rapier page: no <body>');
145
155
  const at = bodyTag.index + bodyTag[0].length;
146
- // A function, not a string: `$'`, `$&` and `` $` `` are replacement patterns.
147
- const titled = stripped.replace(/<title>[^<]*<\/title>/, () => '<title>' + docName.replace(/&/g, '&amp;').replace(/</g, '&lt;') + ' — Rapier</title>');
156
+ const titled = stripped.replace(/<title>[^<]*<\/title>\n?/, all => '<title>' + docName.replace(/&/g, '&amp;').replace(/</g, '&lt;') + ' — Rapier</title>' +
157
+ (all.endsWith('\n') ? '\n' : '') + previewHead(text, docName.replace(/\.[^.]*$/, '')));
148
158
  const shift = titled.length - stripped.length;
149
159
  return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
150
160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-html",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
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",