rapier-html 1.2.33 → 1.2.42
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 +21 -21
- package/README.md +22 -13
- package/dist/ledger/data.mjs +57 -4
- package/dist/ledger/document-settings.mjs +1 -0
- package/dist/ledger/format.mjs +114 -47
- package/dist/ledger/hash.mjs +43 -2
- package/dist/ledger/history.mjs +6 -0
- package/dist/ledger/inverse-source.mjs +15 -2
- package/dist/ledger/journal-records.mjs +32 -9
- package/dist/ledger/md-assets.mjs +709 -0
- package/dist/ledger/will.mjs +239 -0
- package/dist/ledger/word-sidecar.mjs +5 -0
- package/dist/page.d.mts +2 -0
- package/dist/rapier.html +85 -84
- package/dist/wrap.mjs +11 -4
- package/package.json +1 -1
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// Rapier Will/1 grammar and range laws. SPDX-License-Identifier: MIT.
|
|
2
|
+
const _RAPIER_WILL_INTENT_LIMIT = 512;
|
|
3
|
+
|
|
4
|
+
const _RAPIER_WILL_LAWS = Object.freeze({ edit: true, append: true, keep: true });
|
|
5
|
+
const _RAPIER_WILL_OPENER_PREFIX = '<!-- will/';
|
|
6
|
+
const _RAPIER_WILL_CLOSER_PREFIX = '<!-- /will';
|
|
7
|
+
const _RAPIER_WILL_CLOSER_EXACT = '<!-- /will -->';
|
|
8
|
+
const _RAPIER_WILL_CLOSE_SUFFIX = ' -->';
|
|
9
|
+
const _RAPIER_WILL_UNSPACED_OPENER = '<!--will/';
|
|
10
|
+
const _RAPIER_WILL_UNSPACED_CLOSER = '<!--/will';
|
|
11
|
+
|
|
12
|
+
function hasWillMarkers(text) {
|
|
13
|
+
return /(?:^|[\r\n])<!-- ?(?:will\/|\/will)/.test(String(text == null ? '' : text));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function _rapierWillMarkerOf(content) {
|
|
17
|
+
const line = String(content == null ? '' : content);
|
|
18
|
+
if (line.startsWith(_RAPIER_WILL_CLOSER_PREFIX)) {
|
|
19
|
+
if (line !== _RAPIER_WILL_CLOSER_EXACT) {
|
|
20
|
+
return { kind: 'near', fault: 'malformed_marker', law: '', intent: null, content: line };
|
|
21
|
+
}
|
|
22
|
+
return { kind: 'close', law: '', intent: null, content: line };
|
|
23
|
+
}
|
|
24
|
+
if (!line.startsWith(_RAPIER_WILL_OPENER_PREFIX)) {
|
|
25
|
+
if (line.startsWith(_RAPIER_WILL_UNSPACED_OPENER) || line.startsWith(_RAPIER_WILL_UNSPACED_CLOSER)) {
|
|
26
|
+
return { kind: 'near', fault: 'malformed_marker', law: '', intent: null, content: line };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const near = fault => ({ kind: 'near', fault, law: '', intent: null, content: line });
|
|
31
|
+
const afterPrefix = line.slice(_RAPIER_WILL_OPENER_PREFIX.length);
|
|
32
|
+
const versionGap = afterPrefix.indexOf(' ');
|
|
33
|
+
if ((versionGap < 0 ? afterPrefix : afterPrefix.slice(0, versionGap)) !== '1') return near('unknown_version');
|
|
34
|
+
if (!line.endsWith(_RAPIER_WILL_CLOSE_SUFFIX)) return near('malformed_marker');
|
|
35
|
+
const middle = line.slice(_RAPIER_WILL_OPENER_PREFIX.length,
|
|
36
|
+
line.length - _RAPIER_WILL_CLOSE_SUFFIX.length);
|
|
37
|
+
const gap = middle.indexOf(' ');
|
|
38
|
+
if (gap < 0) return near('malformed_marker');
|
|
39
|
+
const rest = middle.slice(gap + 1);
|
|
40
|
+
const word = (/^[^:\s]*/.exec(rest))[0];
|
|
41
|
+
if (!word) return near('malformed_marker');
|
|
42
|
+
let intent;
|
|
43
|
+
if (rest === word) intent = null;
|
|
44
|
+
else if (rest.startsWith(word + ': ')) intent = rest.slice(word.length + 2);
|
|
45
|
+
else return near('malformed_marker');
|
|
46
|
+
if (!Object.prototype.hasOwnProperty.call(_RAPIER_WILL_LAWS, word)) return near('unknown_law');
|
|
47
|
+
if (intent !== null) {
|
|
48
|
+
if (!intent.length || intent.indexOf('--') >= 0) return near('malformed_marker');
|
|
49
|
+
if ([...intent].length > _RAPIER_WILL_INTENT_LIMIT) return near('intent_over_bound');
|
|
50
|
+
}
|
|
51
|
+
return { kind: 'open', law: word, intent, content: line };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _rapierWillParse(text) {
|
|
55
|
+
const source = String(text == null ? '' : text);
|
|
56
|
+
const will = {
|
|
57
|
+
present: false, fault: '', faults: [], blocks: [], markers: [], regions: [], text: source,
|
|
58
|
+
};
|
|
59
|
+
if (!hasWillMarkers(source)) return will;
|
|
60
|
+
let open = null;
|
|
61
|
+
for (let index = 0, start = 0; start <= source.length; index++) {
|
|
62
|
+
let contentEnd = start;
|
|
63
|
+
while (contentEnd < source.length && source.charCodeAt(contentEnd) !== 13 && source.charCodeAt(contentEnd) !== 10) contentEnd++;
|
|
64
|
+
let lineEnd = contentEnd;
|
|
65
|
+
if (lineEnd < source.length) {
|
|
66
|
+
lineEnd++;
|
|
67
|
+
if (source.charCodeAt(contentEnd) === 13 && source.charCodeAt(lineEnd) === 10) lineEnd++;
|
|
68
|
+
}
|
|
69
|
+
const row = { start, contentEnd, lineEnd };
|
|
70
|
+
start = contentEnd < source.length ? lineEnd : source.length + 1;
|
|
71
|
+
const marker = _rapierWillMarkerOf(source.slice(row.start, row.contentEnd));
|
|
72
|
+
if (!marker) continue;
|
|
73
|
+
will.present = true;
|
|
74
|
+
will.blocks.push({ start: row.start, end: row.lineEnd });
|
|
75
|
+
const stamped = {
|
|
76
|
+
...marker, line: index, start: row.start, end: row.lineEnd, contentEnd: row.contentEnd,
|
|
77
|
+
};
|
|
78
|
+
if (marker.kind === 'near') {
|
|
79
|
+
will.faults.push({ mode: marker.fault, line: index, start: row.start, end: row.lineEnd });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (marker.kind === 'close') {
|
|
83
|
+
if (!open) {
|
|
84
|
+
will.faults.push({ mode: 'unpaired_marker', line: index, start: row.start, end: row.lineEnd });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
will.markers.push(open, stamped);
|
|
88
|
+
will.regions.push({
|
|
89
|
+
index: will.regions.length, law: open.law, intent: open.intent,
|
|
90
|
+
openerLine: open.line, openerStart: open.start, openerEnd: open.end,
|
|
91
|
+
openerContentEnd: open.contentEnd,
|
|
92
|
+
closerLine: index, closerStart: stamped.start, closerEnd: stamped.end,
|
|
93
|
+
start: open.end, end: stamped.start,
|
|
94
|
+
});
|
|
95
|
+
open = null;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (open) {
|
|
99
|
+
will.faults.push({ mode: 'unpaired_marker', line: index, start: row.start, end: row.lineEnd });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
open = stamped;
|
|
103
|
+
}
|
|
104
|
+
if (open) will.faults.push({ mode: 'unpaired_marker', line: open.line, start: open.start, end: open.end });
|
|
105
|
+
will.faults.sort((a, b) => a.start - b.start);
|
|
106
|
+
will.fault = will.faults.length ? will.faults[0].mode : '';
|
|
107
|
+
return will;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _rapierWillRegionsIn(will, start, end) {
|
|
111
|
+
const found = [];
|
|
112
|
+
for (const region of will.regions) {
|
|
113
|
+
const touches = start === end
|
|
114
|
+
? start >= region.start && start <= region.end
|
|
115
|
+
: start < region.end && region.start < end;
|
|
116
|
+
if (touches) found.push(region);
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _rapierWillTouchesMarker(will, start, end) {
|
|
122
|
+
const touches = (from, to) => start === end
|
|
123
|
+
? start > from && start < to
|
|
124
|
+
: start < to && from < end;
|
|
125
|
+
for (const region of will.regions) {
|
|
126
|
+
if (touches(region.openerStart, region.openerEnd) ||
|
|
127
|
+
touches(region.closerStart, region.closerEnd)) return region;
|
|
128
|
+
}
|
|
129
|
+
for (const block of will.blocks) if (touches(block.start, block.end)) return { law: 'keep' };
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function _rapierWillGovern(will, start, end) {
|
|
134
|
+
if (will.faults.length) return 'keep';
|
|
135
|
+
if (_rapierWillTouchesMarker(will, start, end)) return 'keep';
|
|
136
|
+
let law = 'edit';
|
|
137
|
+
for (const region of _rapierWillRegionsIn(will, start, end)) {
|
|
138
|
+
if (region.law === 'keep') return 'keep';
|
|
139
|
+
if (region.law === 'append') law = 'append';
|
|
140
|
+
}
|
|
141
|
+
return law;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _rapierWillIntentOf(will, start, end) {
|
|
145
|
+
if (!will.present || will.faults.length) return '';
|
|
146
|
+
const found = _rapierWillRegionsIn(will, start, end);
|
|
147
|
+
if (found.length !== 1) return '';
|
|
148
|
+
const region = found[0];
|
|
149
|
+
return region.intent && start >= region.start && end <= region.end ? region.intent : '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
function _rapierWillTerminatorWidth(last, prior) {
|
|
154
|
+
if (last === 10) return prior === 13 ? 2 : 1;
|
|
155
|
+
if (last === 13) return 1;
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function _rapierWillStripOneTerminator(text) {
|
|
160
|
+
const end = text.length;
|
|
161
|
+
return end ? text.slice(0, end - _rapierWillTerminatorWidth(
|
|
162
|
+
text.charCodeAt(end - 1), end >= 2 ? text.charCodeAt(end - 2) : -1)) : text;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function willOffsetBefore(rows, offset) {
|
|
166
|
+
for (let index = rows.length - 1; index >= 0 && offset >= 0; index--) {
|
|
167
|
+
const {pos, removed, inserted} = rows[index];
|
|
168
|
+
if (offset >= pos) offset = offset >= pos + inserted.length ? offset - inserted.length + removed.length : -1;
|
|
169
|
+
}
|
|
170
|
+
return offset;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function _rapierWillRestores(restores, region, splices, text) {
|
|
174
|
+
if (!Array.isArray(restores) || region.law !== 'append') return null;
|
|
175
|
+
const reaching = splices.filter(row => row.pos <= region.end && row.pos + row.removed.length >= region.start &&
|
|
176
|
+
(row.removed || row.inserted)).toSorted((a, b) => a.pos - b.pos);
|
|
177
|
+
if (!reaching.length || reaching.some((row, index) => row.pos < region.start || row.pos + row.removed.length > region.end ||
|
|
178
|
+
(index && row.pos !== reaching[index - 1].pos + reaching[index - 1].removed.length) || !restores.includes(row)) ||
|
|
179
|
+
!/^(?:\r\n|\r|\n)?$/.test(text.slice(reaching.at(-1).pos + reaching.at(-1).removed.length - region.start))) return null;
|
|
180
|
+
return reaching;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function willTransition(beforeText, afterText, splices, markerSplices = splices, {restores = null, restored = null} = {}) {
|
|
184
|
+
const before = _rapierWillParse(beforeText), after = _rapierWillParse(afterText);
|
|
185
|
+
if (!before.present && !after.present) return null;
|
|
186
|
+
const bounds = region => ({start: region.start, end: region.end});
|
|
187
|
+
const faults = will => will.faults.slice(0, 4).map(({mode, line}) => ({mode, line}));
|
|
188
|
+
if (before.faults.length) return {law: 'keep', rule: 'before_faulted', faults: faults(before), start: 0, end: beforeText.length};
|
|
189
|
+
for (const region of before.regions) {
|
|
190
|
+
const markers = [[region.openerStart, region.openerEnd], [region.closerStart, region.closerEnd]];
|
|
191
|
+
for (let index = 0; index < splices.length; index++) {
|
|
192
|
+
const row = splices[index], end = row.pos + row.removed.length;
|
|
193
|
+
for (const marker of markers) {
|
|
194
|
+
if (row.removed.length ? row.pos < marker[1] && marker[0] < end : row.pos > marker[0] && row.pos < marker[1])
|
|
195
|
+
return {law: region.law, rule: 'marker_span_touched', editIndex: index, region: region.index, ...bounds(region)};
|
|
196
|
+
if (end <= marker[0]) { const shift = row.inserted.length - row.removed.length; marker[0] += shift; marker[1] += shift; }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (after.faults.length) return {law: 'keep', rule: 'result_faulted', faults: faults(after), start: 0, end: beforeText.length};
|
|
201
|
+
if (before.markers.length !== after.markers.length || before.markers.some((marker, index) => {
|
|
202
|
+
const next = after.markers[index];
|
|
203
|
+
return marker.kind !== next.kind || beforeText.slice(marker.start, marker.end) !== afterText.slice(next.start, next.end) ||
|
|
204
|
+
willOffsetBefore(markerSplices, next.start) !== marker.start || willOffsetBefore(markerSplices, next.end - 1) !== marker.end - 1;
|
|
205
|
+
})) return {law: 'keep', rule: 'marker_sequence_mismatch', start: 0, end: beforeText.length};
|
|
206
|
+
for (const region of before.regions) {
|
|
207
|
+
const was = beforeText.slice(region.start, region.end), next = after.regions[region.index], now = afterText.slice(next.start, next.end);
|
|
208
|
+
if (region.law === 'keep' && was !== now)
|
|
209
|
+
return {law: region.law, rule: 'law_violated', region: region.index, ...bounds(region)};
|
|
210
|
+
if (region.law !== 'append' || _rapierWillStripOneTerminator(now).startsWith(_rapierWillStripOneTerminator(was))) continue;
|
|
211
|
+
const reaching = _rapierWillRestores(restores, region, splices, was);
|
|
212
|
+
if (!reaching) return {law: region.law, rule: 'law_violated', region: region.index, ...bounds(region)};
|
|
213
|
+
restored?.set(region.index, reaching);
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function evaluateWill(beforeText, splices, {restores = null} = {}) {
|
|
219
|
+
const invalid = rule => ({outcome: 'invalid', reason: 'precondition', rule});
|
|
220
|
+
if (typeof beforeText !== 'string' || !beforeText.isWellFormed()) return invalid('invalid_unicode');
|
|
221
|
+
if (!Array.isArray(splices)) return invalid('malformed_splice');
|
|
222
|
+
const boundary = (text, at) => !(at > 0 && at < text.length &&
|
|
223
|
+
(text.charCodeAt(at - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(at) & 0xfc00) === 0xdc00);
|
|
224
|
+
let afterText = beforeText;
|
|
225
|
+
for (const row of splices) {
|
|
226
|
+
if (!row || !Number.isSafeInteger(row.pos) || row.pos < 0 || Object.is(row.pos, -0) ||
|
|
227
|
+
typeof row.removed !== 'string' || typeof row.inserted !== 'string' ||
|
|
228
|
+
!row.removed.isWellFormed() || !row.inserted.isWellFormed()) return invalid('malformed_splice');
|
|
229
|
+
const end = row.pos + row.removed.length;
|
|
230
|
+
if (end > afterText.length || !boundary(afterText, row.pos) || !boundary(afterText, end)) return invalid('out_of_range_splice');
|
|
231
|
+
if (afterText.slice(row.pos, end) !== row.removed) return invalid('source_mismatch');
|
|
232
|
+
afterText = afterText.slice(0, row.pos) + row.inserted + afterText.slice(end);
|
|
233
|
+
}
|
|
234
|
+
const refusal = splices.length && willTransition(beforeText, afterText, splices, splices, {restores});
|
|
235
|
+
return refusal ? {outcome: 'refused', reason: 'document_law', ...refusal} : {outcome: 'applied'};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
export { hasWillMarkers, _rapierWillParse as parseWill, _rapierWillMarkerOf as willMarkerOf, _rapierWillRegionsIn as willRegionsIn, _rapierWillTouchesMarker as willTouchesMarker, _rapierWillGovern as willGovern, _rapierWillIntentOf as willIntentOf, _rapierWillStripOneTerminator as stripOneTerminator, _rapierWillTerminatorWidth as terminatorWidth };
|
package/dist/page.d.mts
CHANGED
|
@@ -8,6 +8,8 @@ export interface WrapOptions {
|
|
|
8
8
|
view?: 'draw' | 'notes';
|
|
9
9
|
/** Opening appearance; the reader can change it without changing their saved preference on open. */
|
|
10
10
|
theme?: 'light' | 'dark';
|
|
11
|
+
/** HTML of the document's words for a reader that runs no scripts; omitted, the file carries the Markdown as written. Inserted as given: it must be sanitized HTML, never script. */
|
|
12
|
+
readable?: string;
|
|
11
13
|
/** An SVG drawing carried beside the document, opened on Draw. */
|
|
12
14
|
drawing?: string;
|
|
13
15
|
drawingName?: string;
|