rapier-html 1.2.7 → 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.
- package/BUILD.json +10 -10
- package/README.md +17 -16
- package/dist/cli.mjs +1 -1
- package/dist/ledger/authored-origin.mjs +0 -5
- package/dist/ledger/authorship.mjs +0 -2
- package/dist/ledger/carried.mjs +4 -7
- package/dist/ledger/data.mjs +0 -8
- package/dist/ledger/diff/concerns.mjs +0 -16
- package/dist/ledger/diff/diff.mjs +0 -7
- package/dist/ledger/diff/index.mjs +0 -1
- package/dist/ledger/diff/matching.mjs +0 -19
- package/dist/ledger/diff/merge.mjs +0 -9
- package/dist/ledger/diff/moves.mjs +0 -7
- package/dist/ledger/diff/structure.mjs +0 -2
- package/dist/ledger/diff/tokens.mjs +0 -6
- package/dist/ledger/diff/walk.mjs +0 -12
- package/dist/ledger/format.mjs +0 -25
- package/dist/ledger/hash.mjs +11 -7
- package/dist/ledger/history.mjs +10 -29
- package/dist/ledger/inverse-source.mjs +0 -46
- package/dist/ledger/journal-records.mjs +24 -56
- package/dist/ledger/transport.mjs +0 -47
- package/dist/page.d.mts +59 -0
- package/dist/page.mjs +7 -3
- package/dist/rapier.html +350 -100
- package/dist/return-address.mjs +0 -4
- package/dist/wrap.mjs +35 -24
- package/package.json +6 -2
package/dist/return-address.mjs
CHANGED
|
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<') + '" 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,16 +73,14 @@ 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
|
-
|
|
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]))])
|
|
83
|
+
if (id !== 'rapier-part-drawing' || !kept.has('drawing')) cut(id);
|
|
65
84
|
const start = html.indexOf(INVENTORY);
|
|
66
85
|
if (start < 0) return html;
|
|
67
86
|
const close = html.indexOf(END, start), end = html[close + END.length] === '\n' ? close + END.length + 1 : close + END.length;
|
|
@@ -71,8 +90,6 @@ export function carry(pageHtml, include = []) {
|
|
|
71
90
|
return html;
|
|
72
91
|
}
|
|
73
92
|
|
|
74
|
-
// The plug-ins a Markdown text displays with, read without a Markdown parser, so it leans toward carrying: math for a `$`
|
|
75
|
-
// or `$$` span outside code, Mermaid for a mermaid fence (`flowchart(fence)` true marks one Rapier draws natively).
|
|
76
93
|
export function portableNeeds(text, {base, flowchart} = {}) {
|
|
77
94
|
const needs = new Set();
|
|
78
95
|
for (const source of [text, base]) {
|
|
@@ -86,7 +103,7 @@ export function portableNeeds(text, {base, flowchart} = {}) {
|
|
|
86
103
|
for (at++; at < lines.length && !new RegExp('^ {0,3}' + open[1][0] + '{' + open[1].length + ',}\\s*$').test(lines[at]); at++) body.push(lines[at]);
|
|
87
104
|
if (open[2].toLowerCase() === 'mermaid' && flowchart?.(body.join('\n')) !== true) needs.add('mermaid');
|
|
88
105
|
}
|
|
89
|
-
if (/\$\$[\s\S]+?\$\$|(?:^|[^\\$])\$(?![\s$])[^$\n]*[^\s\\$]\$(?!\d)/.test(prose.replace(/(`+)[\s\S]*?\1/g, ''))) needs.add('math');
|
|
106
|
+
if (/\$\$[\s\S]+?\$\$|(?:^|[^\\$])\$(?![\s$])[^$\n]*[^\s\\$]\$(?!\d)/.test(prose.split(/\n[ \t]*\n/).map(paragraph => paragraph.replace(/(`+)[\s\S]*?\1/g, '')).join('\n\n'))) needs.add('math');
|
|
90
107
|
}
|
|
91
108
|
return CHOICES.filter(id => needs.has(id));
|
|
92
109
|
}
|
|
@@ -100,7 +117,6 @@ export function wrap(pageHtml, text, name, options) {
|
|
|
100
117
|
const opts = {...saved, ...(options || {})};
|
|
101
118
|
const docName = safeName(name, 'document.md');
|
|
102
119
|
if (opts.kind != null && !['markdown', 'text', 'code'].includes(opts.kind)) throw new Error('invalid document kind');
|
|
103
|
-
// `view`: the view the page opens on with the document behind it, `draw` or `notes`.
|
|
104
120
|
if (opts.view != null && !['draw', 'notes'].includes(opts.view)) throw new Error('the view is draw or notes');
|
|
105
121
|
const address = opts.return == null ? null : returnAddress(opts.return);
|
|
106
122
|
const expiry = opts.return_expires_at == null ? null : returnExpiresAt(opts.return_expires_at);
|
|
@@ -119,8 +135,6 @@ export function wrap(pageHtml, text, name, options) {
|
|
|
119
135
|
if (typeof opts.base === 'object') baseline = readBase(opts.base);
|
|
120
136
|
else {
|
|
121
137
|
if (typeof opts.base !== 'string') throw new Error('the base must be text');
|
|
122
|
-
// A returned Share page uses its own source dialect; its original base is the same carrier.
|
|
123
|
-
// Read that base before optional current-document history, which belongs to the current text.
|
|
124
138
|
const carriedBase = /^\s*<!doctype html/i.test(opts.base) ? takeBase(opts.base).base : null;
|
|
125
139
|
const pageBase = !carriedBase && /^\s*<!doctype html/i.test(opts.base) ? unwrap(opts.base) : null;
|
|
126
140
|
const source = carriedBase ? {text: carriedBase.text} : pageBase?.text != null ? readDocument(pageBase.text) : readDocument(opts.base);
|
|
@@ -129,21 +143,18 @@ export function wrap(pageHtml, text, name, options) {
|
|
|
129
143
|
by: opts.by == null ? 'Agent' : String(opts.by).trim(), at: opts.at == null ? new Date().toISOString() : String(opts.at)});
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
|
-
// Rewrapping retains the original text, hash and revision; attribution names this comparison.
|
|
133
146
|
if (baseline) baseline = readBase({...baseline, ...(opts.by != null ? {by: String(opts.by).trim()} : {}), ...(opts.at != null ? {at: String(opts.at)} : {})});
|
|
134
147
|
carriedBlocks += '\n' + writeBase(text, baseline);
|
|
135
|
-
|
|
136
|
-
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);
|
|
137
149
|
const headTag = /<meta charset[^>]*>\n?/i.exec(unseeded) || /<head[^>]*>\n?/.exec(unseeded);
|
|
138
150
|
if (!headTag) throw new Error('not a Rapier page: no <head>');
|
|
139
151
|
const seededAt = headTag.index + headTag[0].length;
|
|
140
152
|
const stripped = unseeded.slice(0, seededAt) + PAGE_SEED + '\n' + unseeded.slice(seededAt);
|
|
141
|
-
// Before Rapier's scripts: parsed before the engine boots.
|
|
142
153
|
const bodyTag = /<body[^>]*>/.exec(stripped);
|
|
143
154
|
if (!bodyTag) throw new Error('not a Rapier page: no <body>');
|
|
144
155
|
const at = bodyTag.index + bodyTag[0].length;
|
|
145
|
-
|
|
146
|
-
|
|
156
|
+
const titled = stripped.replace(/<title>[^<]*<\/title>\n?/, all => '<title>' + docName.replace(/&/g, '&').replace(/</g, '<') + ' — Rapier</title>' +
|
|
157
|
+
(all.endsWith('\n') ? '\n' : '') + previewHead(text, docName.replace(/\.[^.]*$/, '')));
|
|
147
158
|
const shift = titled.length - stripped.length;
|
|
148
159
|
return titled.slice(0, at + shift) + '\n' + block + carriedBlocks + titled.slice(at + shift);
|
|
149
160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-html",
|
|
3
|
-
"version": "1.2.
|
|
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",
|
|
@@ -40,8 +40,12 @@
|
|
|
40
40
|
"bin": {
|
|
41
41
|
"rapier-html": "dist/cli.mjs"
|
|
42
42
|
},
|
|
43
|
+
"types": "./dist/page.d.mts",
|
|
43
44
|
"exports": {
|
|
44
|
-
".":
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/page.d.mts",
|
|
47
|
+
"default": "./dist/page.mjs"
|
|
48
|
+
}
|
|
45
49
|
},
|
|
46
50
|
"files": [
|
|
47
51
|
"dist",
|