rapier-markdown-kit 1.1.55 → 1.1.57
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/LICENSE +6 -1
- package/README.md +197 -1
- package/dist/kit/index.mjs +2 -0
- package/dist/kit/ledger/authorship.mjs +59 -0
- package/dist/kit/ledger/carried.mjs +69 -0
- package/dist/kit/ledger/data.mjs +33 -0
- package/dist/kit/ledger/format.mjs +131 -0
- package/dist/kit/ledger/hash.mjs +47 -0
- package/dist/kit/ledger/journal-records.mjs +139 -0
- package/dist/kit/ledger/merge.mjs +101 -0
- package/dist/kit/ledger/text-merge.mjs +345 -0
- package/dist/kit/rapier-ledger.mjs +9 -0
- package/dist/kit/render-entry.mjs +7 -0
- package/dist/kit/render-markdown.mjs +829 -0
- package/dist/kit/render-print.mjs +165 -0
- package/dist/kit/render-sanitize.mjs +223 -0
- package/dist/kit/render-styles.mjs +118 -0
- package/dist/kit/render.mjs +1439 -0
- package/package.json +6 -3
package/LICENSE
CHANGED
|
@@ -25,7 +25,12 @@ grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `
|
|
|
25
25
|
(`layout/model.mjs`, `layout/line-plan.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
|
|
26
26
|
(`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
|
|
27
27
|
`tools/read-shared-page.py`), and the host helper `packages/rapier-embed/embed.mjs`. The vendored Pretext (`agent/vendor/pretext`) and micromark carry their
|
|
28
|
-
own MIT notices.
|
|
28
|
+
own MIT notices. The edit-ledger module (`kit/rapier-ledger.mjs`, `kit/ledger/*.mjs`) and
|
|
29
|
+
its editor compatibility import (`editor/journal-records.mjs`) are also covered. The
|
|
30
|
+
record validator/replayer and text-only conflict merger were moved, not copied,
|
|
31
|
+
into that module; the Notes index merger and the rest of the agent/editor remain
|
|
32
|
+
AGPL-3.0-only. The wrapper's carrier-staging helper
|
|
33
|
+
(`skills/rapier-html/package-files.mjs`) is MIT. Everything else in Rapier is AGPL-3.0-only (see `LICENSE`).
|
|
29
34
|
|
|
30
35
|
The picture appendix reader and writer used to be part of `images/assets.mjs`, a file that also
|
|
31
36
|
holds JPEG XL/raster header inspection and SVG sanitizing -- both of which reach the editor's own
|
package/README.md
CHANGED
|
@@ -26,6 +26,47 @@ import {parseWill} from 'rapier-markdown-kit/will'; // Will/1 markers: kee
|
|
|
26
26
|
import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendix
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## The shared renderer
|
|
30
|
+
|
|
31
|
+
`rapier-markdown-kit/render` exports `createRenderer`, `createMarkdownRenderer`,
|
|
32
|
+
`createRenderStyles`, `createRenderSanitizer` and `createPrintRenderer`. These are the
|
|
33
|
+
actual editor/Share owners, not a second Markdown dialect or a lookalike stylesheet.
|
|
34
|
+
Their host dependencies are explicit named properties in each factory's first argument;
|
|
35
|
+
no factory reaches into an editor session to find its document. `style.css` is the exact
|
|
36
|
+
MIT `spec/markdown-style.css`, including the numbered-list, checkbox, callout, table,
|
|
37
|
+
footnote and picture-layout treatment.
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import {createRenderer} from 'rapier-markdown-kit/render';
|
|
41
|
+
|
|
42
|
+
// host is your bound DOM/codec/Markdown/image/style environment. Its named ports are
|
|
43
|
+
// declared at the top of createRenderer; preparation returns a detached semantic root.
|
|
44
|
+
const writer = createRenderer(host);
|
|
45
|
+
const {html, filename} = await writer.render(source, {filename: 'proposal.md'});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`createMarkdownRenderer(host).render(source, metadata)` returns the semantic HTML.
|
|
49
|
+
The page writer's `render` returns `{html, filename}` asynchronously and preserves the
|
|
50
|
+
original source, including BOM and line endings, in the SHA-256-verified source carrier.
|
|
51
|
+
Every existing lower-level method is also returned for hosts that compose the stages.
|
|
52
|
+
Create a new bound instance when its parser or host state changes; do not share mutable
|
|
53
|
+
DOM objects between document realms.
|
|
54
|
+
|
|
55
|
+
This is a **DOM-hosted library**, not a bundled headless browser, DOM shim, parser or
|
|
56
|
+
image decoder. Supplying only `{source}` or running it in bare Node without those ports
|
|
57
|
+
is not supported. A host must provide a real DOM and sanitizer, the Rapier grammar,
|
|
58
|
+
portable image preparation, style access and the named codec/layout ports it uses.
|
|
59
|
+
Those dependencies keep their own licences: the MIT grant here does not relicense a
|
|
60
|
+
separately supplied host or vendor. The repository's `editor/render-host.js` binds the
|
|
61
|
+
editor; `server/` provides a complete local host with a pinned Chromium process. Its
|
|
62
|
+
Word exporter and filesystem service are AGPL, not part of this MIT package.
|
|
63
|
+
|
|
64
|
+
The extraction receipt in the existing `html-export-corpus` row fixes all 226 baseline
|
|
65
|
+
writer pages byte for byte. That corpus supplies a clean DOM and named style/sanitizer
|
|
66
|
+
adapters: it proves serialization and source recovery, not browser appearance or
|
|
67
|
+
sanitizer security. The server's integration cells additionally use a real DOM and
|
|
68
|
+
round-trip the actual HTTP output.
|
|
69
|
+
|
|
29
70
|
## What it gives you
|
|
30
71
|
|
|
31
72
|
| Import | What it is |
|
|
@@ -36,7 +77,9 @@ import {parseAssets} from 'rapier-markdown-kit/assets'; // the picture appendi
|
|
|
36
77
|
| `rapier-markdown-kit/will` | The Will/1 grammar (a separate, optional standard): the markers around an agent's intent regions. |
|
|
37
78
|
| `rapier-markdown-kit/assets` | The picture appendix: the reference definitions a document's pictures live in. It never decodes a picture. |
|
|
38
79
|
| `rapier-markdown-kit/style.css` | The reference style: type, lists, tables, tasks, pictures and the document's other markup, scoped to `.md-render`. |
|
|
39
|
-
| `rapier-markdown-kit` |
|
|
80
|
+
| `rapier-markdown-kit/rapier-ledger` | The MIT edit-ledger format, replay, authorship projection and offline merge. |
|
|
81
|
+
| `rapier-markdown-kit/ledger/carried` | Checked optional parts for pages and saved Markdown. |
|
|
82
|
+
| `rapier-markdown-kit` | The modules above as named namespaces. |
|
|
40
83
|
|
|
41
84
|
Every convention is plain Markdown or an HTML comment other readers ignore. Laying lines out as Rapier does takes
|
|
42
85
|
the host's font metrics (below).
|
|
@@ -240,3 +283,156 @@ value the editor's release reads.
|
|
|
240
283
|
|
|
241
284
|
Each module has one home in the Rapier source (`spec/`, `layout/`, `agent/`), where Rapier itself reads it; `dist/`
|
|
242
285
|
is that closure copied byte for byte, and `style.css` is the same `spec/markdown-style.css` Rapier bundles.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
## rapier-ledger
|
|
289
|
+
|
|
290
|
+
The edit ledger and its carried parts. MIT, no dependencies; part of
|
|
291
|
+
`rapier-markdown-kit`. The editor and this module use the **same** record validator,
|
|
292
|
+
splice replayer, root transition and interval transport. This is not a second history store.
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
import {exportLedger, readLedger, authorship, merge} from 'rapier-markdown-kit/rapier-ledger';
|
|
296
|
+
import {writeDocument, readDocument} from 'rapier-markdown-kit/ledger/carried';
|
|
297
|
+
|
|
298
|
+
const ledger = exportLedger({
|
|
299
|
+
text: 'Hello', records: [], documentAuthority: 'example-document', complete: true,
|
|
300
|
+
});
|
|
301
|
+
const saved = writeDocument('Hello', {ledger});
|
|
302
|
+
const reopened = readDocument(saved);
|
|
303
|
+
const verified = readLedger(reopened.ledger, reopened.text);
|
|
304
|
+
console.assert(verified.text === 'Hello');
|
|
305
|
+
const clean = writeDocument(verified.text, {authorship: authorship(ledger)});
|
|
306
|
+
console.assert(readDocument(clean).ledger === null);
|
|
307
|
+
const together = merge(ledger, reopened.ledger);
|
|
308
|
+
console.assert(together.text === 'Hello');
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## The format at launch
|
|
312
|
+
|
|
313
|
+
`rapier-ledger/1` is a plain JSON object:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
{format, documentAuthority, start: {text, revision, root, sha256}, records,
|
|
317
|
+
head: {revision, root, sha256}, complete, sha256}
|
|
318
|
+
records[]: {transaction, splices: [{pos, removed, inserted}], beforeHash, afterHash}
|
|
319
|
+
transaction: {id, documentAuthority, baseRevision, revision, actor: {kind, id},
|
|
320
|
+
transport, operation, requestId, sourceTransactionId, affectedBlockIds,
|
|
321
|
+
parent, reverts, reapplies, createdAt}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`format` is `rapier-ledger/1`. `start.text` is the exact text before the first
|
|
325
|
+
retained record. Every outgoing record carries its own splices, including Undo and
|
|
326
|
+
Redo. Offsets and ranges count UTF-16 code units; no boundary may cut a surrogate
|
|
327
|
+
pair. Splices execute in their recorded order. UTF-8 checksums preserve CRLF, tabs,
|
|
328
|
+
NUL and non-ASCII text. The canonical text limit is 25 MiB; each record has at most
|
|
329
|
+
64 splices. The existing record validator remains authoritative for field bounds.
|
|
330
|
+
|
|
331
|
+
`start.sha256` and `head.sha256` hash UTF-8 text. The envelope `sha256` hashes
|
|
332
|
+
canonical JSON of every other envelope field: recursively sorted object keys,
|
|
333
|
+
array order preserved, JSON string/number spelling. Plain finite scalar data only:
|
|
334
|
+
no accessors, sparse arrays, cycles, undefined, negative zero or unpaired surrogates.
|
|
335
|
+
Readers reject unknown envelope/start/head fields, bad checksums, invalid records,
|
|
336
|
+
invalid source boundaries, a replay/root mismatch, or a head different from the
|
|
337
|
+
actual document. `historyEnvelope` additionally proves the editor's Undo/Redo branch.
|
|
338
|
+
|
|
339
|
+
**Roots are path-dependent.** `textRoot(text)` mints the existing
|
|
340
|
+
`chars:fnv-base36:adler-base36` initial root. Each splice advances it with
|
|
341
|
+
`rootAfter(priorRoot, splice)`, matching the source store. A later root is **not**
|
|
342
|
+
a fresh hash of the whole current text. SHA-256 of the exact checkpoint text is
|
|
343
|
+
separate. This distinction preserves the editor's actual format rather than the
|
|
344
|
+
earlier design shorthand. A nonzero start names a retained checkpoint; it does not
|
|
345
|
+
claim to reconstruct discarded events. `complete: false` also permits a stated
|
|
346
|
+
revision gap from the editor's pruning of cancelling navigation pairs; each
|
|
347
|
+
remaining splice must still replay exactly. It is never a claim about missing edits.
|
|
348
|
+
|
|
349
|
+
`actor.kind` is `human`, `agent` or `system`. A named agent's `actor.id` is
|
|
350
|
+
`<door>/<host-given name>` (name at most 96 characters, combined id at most 160).
|
|
351
|
+
Without a name, the door stands alone. The name is a **host-supplied claim**, not a
|
|
352
|
+
verified identity. Timestamps are supplied record times, not independently attested.
|
|
353
|
+
Checksums detect inconsistency/corruption, **not forgery**: anyone able to rewrite a
|
|
354
|
+
file can rewrite its checksums and labels. Importing a label never grants a live
|
|
355
|
+
principal's read handles, review authority or undo permissions.
|
|
356
|
+
|
|
357
|
+
## Optional carried parts
|
|
358
|
+
|
|
359
|
+
The default is **neither part**. In a self-contained page these inert scripts stand
|
|
360
|
+
beside the document's existing carried source:
|
|
361
|
+
|
|
362
|
+
| Element | Type and contents | Choice |
|
|
363
|
+
| --- | --- | --- |
|
|
364
|
+
| `#rapier-authorship` | `application/json`, `rapier-authorship/1` | Who wrote what |
|
|
365
|
+
| `#rapier-ledger` | `application/json`, `rapier-ledger/1` | The whole history |
|
|
366
|
+
|
|
367
|
+
Each body uses the same reversible `encodeCarried`/`decodeCarried` escape as the
|
|
368
|
+
page's source. `writeParts`, `readParts` and `takeParts` implement it. Duplicate,
|
|
369
|
+
executable, malformed or wrong-document parts are refused. If both are present,
|
|
370
|
+
the authorship projection recomputed from the ledger must agree exactly.
|
|
371
|
+
|
|
372
|
+
`rapier-authorship/1` is `{format, sha256, runs}`. `sha256` hashes the **current**
|
|
373
|
+
text. Runs are `[start, end, actorKind, actorId, at]`, in order, nonoverlapping and
|
|
374
|
+
covering every current character. A start without earlier authorship is attributed
|
|
375
|
+
to `system/unattributed`, time zero. Insertions take the inserting record's actor
|
|
376
|
+
and time; deleted runs disappear. Thus the projection contains **no removed text**.
|
|
377
|
+
An authorship-only import becomes a clearly incomplete insertion ledger; future
|
|
378
|
+
attribution is again derived from that one ledger, never a mutable blame database.
|
|
379
|
+
|
|
380
|
+
A Markdown file carries the same optional JSON at EOF, outside its exact source:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
<!-- rapier-carried/1
|
|
386
|
+
{"ledger":...,"authorship":...}
|
|
387
|
+
-->
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The writer escapes `<` and `>` in that JSON. This versioned EOF marker is reserved;
|
|
392
|
+
an unfinished marker is refused, not silently imported as prose. The reader strips
|
|
393
|
+
only that exact suffix, validates it against the remaining exact text, and treats a
|
|
394
|
+
leading BOM as file metadata. Other Markdown readers ignore the comment. Plain
|
|
395
|
+
saves without a choice are unchanged byte for byte. For executable code, use a web
|
|
396
|
+
page to carry metadata rather than appending a Markdown comment to program source.
|
|
397
|
+
|
|
398
|
+
The existing proposal page's `#rapier-base` is independent: `text/markdown`, with
|
|
399
|
+
`data-revision`, `data-sha256`, `data-by` and `data-at`, carries the proposal's
|
|
400
|
+
baseline. It is **not** reconstructed from an optional ledger, and this module does
|
|
401
|
+
not implement the separate proposal-tool/CLI workflow.
|
|
402
|
+
|
|
403
|
+
## Merging copies without a server
|
|
404
|
+
|
|
405
|
+
`merge(first, second)` locates a common checkpoint with the same root **and exact
|
|
406
|
+
text**, then transports the second copy's record splices over the first's through
|
|
407
|
+
the shared interval transport. It preserves the first's ancestry and incoming
|
|
408
|
+
actors/times, minting local revision/root links. Both the public reader and the
|
|
409
|
+
editor's restore reader can replay the result. Both merge orders yield the same
|
|
410
|
+
text for independently edited nonoverlapping ranges; their ordered ledgers need
|
|
411
|
+
not be identical. Full and conflict receipts make repeated imports idempotent.
|
|
412
|
+
|
|
413
|
+
Overlaps fall back to the existing text three-way merge. Unresolved alternatives
|
|
414
|
+
are carried in the existing `note-conflict:v1` envelope, never silently selected.
|
|
415
|
+
Different Will `keep` regions require conflict review even when one copy was
|
|
416
|
+
unchanged. `review.required` names an unresolved result; a UI must not auto-accept
|
|
417
|
+
it. Rapier offers carried-copy imports from Compare through its existing review,
|
|
418
|
+
checks the live document again at commit, and installs the proven ledger before
|
|
419
|
+
publishing its receipt or opening the durable-save barrier. A rejected or stale
|
|
420
|
+
proposal mutates nothing. A failed install rolls the source and undo window back.
|
|
421
|
+
Copies without a proven common checkpoint are refused by this API.
|
|
422
|
+
|
|
423
|
+
## Host integration and retention
|
|
424
|
+
|
|
425
|
+
The embed helper accepts `agentName`; the checked `rapier-connect` message carries
|
|
426
|
+
`agent: {name}`. WebMCP uses the registration owner's supplied client name. Modern
|
|
427
|
+
MCP uses the request's client metadata, outside tool arguments; a legacy stateless
|
|
428
|
+
request without a supplied name stays unnamed. MCP editor snapshots include the
|
|
429
|
+
server's retained journal only in editor metadata, not in model-facing tool results.
|
|
430
|
+
The browser proves a replay suffix before preserving remote writers under local
|
|
431
|
+
revision links; missing remote events never become an invented complete history.
|
|
432
|
+
|
|
433
|
+
Rapier retains its existing 500-record / 4-MiB in-memory window. A carried file can
|
|
434
|
+
outlive that window; reopening may trim it again. The module preserves an explicitly
|
|
435
|
+
incomplete start. Full history includes removed text and can be much larger than
|
|
436
|
+
the current document; it is opt-in for each outgoing file, never a persistent
|
|
437
|
+
preference. Authorship is also opt-in. Neither option implies signatures, identity
|
|
438
|
+
verification, infinite retention, a relay server or automatic synchronization.
|
package/dist/kit/index.mjs
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {readLedger, exportLedger, textRoot, rootAfter} from './format.mjs';
|
|
3
|
+
import {_rapierTransformSplices} from './journal-records.mjs';
|
|
4
|
+
import {sha256} from './hash.mjs';
|
|
5
|
+
import {plainData, documentText, nonnegative, exactKeys, fault} from './data.mjs';
|
|
6
|
+
function coalesce(runs) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const run of runs) {
|
|
9
|
+
if (run[0] === run[1]) continue;
|
|
10
|
+
const last = out.at(-1);
|
|
11
|
+
if (last && last[1] === run[0] && last.slice(2).every((value, i) => value === run[i + 2])) last[1] = run[1];
|
|
12
|
+
else out.push(run.slice());
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function authorship(input) {
|
|
17
|
+
const {ledger, text} = readLedger(input);
|
|
18
|
+
let runs = ledger.start.text ? [[0, ledger.start.text.length, 'system', 'unattributed', 0]] : [];
|
|
19
|
+
for (const record of ledger.records) for (const row of record.splices) {
|
|
20
|
+
const cut = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length, before = [], after = [];
|
|
21
|
+
for (const run of runs) {
|
|
22
|
+
if (run[0] < row.pos) before.push([run[0], Math.min(run[1], row.pos), ...run.slice(2)]);
|
|
23
|
+
if (run[1] > cut) after.push([Math.max(run[0], cut) + delta, run[1] + delta, ...run.slice(2)]);
|
|
24
|
+
}
|
|
25
|
+
const tx = record.transaction;
|
|
26
|
+
runs = coalesce([...before, [row.pos, row.pos + row.inserted.length, tx.actor.kind, tx.actor.id, tx.createdAt], ...after]);
|
|
27
|
+
}
|
|
28
|
+
return {format: 'rapier-authorship/1', sha256: sha256(text), runs};
|
|
29
|
+
}
|
|
30
|
+
export function readAuthorship(input, text) {
|
|
31
|
+
documentText(text); const value = plainData(input);
|
|
32
|
+
if (!exactKeys(value, ['format','sha256','runs']) || value.format !== 'rapier-authorship/1' || value.sha256 !== sha256(text) || !Array.isArray(value.runs)) throw fault('authorship is not for this text');
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
for (const run of value.runs) {
|
|
35
|
+
if (!Array.isArray(run) || run.length !== 5 || run[0] !== cursor || !nonnegative(run[1]) || run[1] <= cursor || run[1] > text.length ||
|
|
36
|
+
!['human','agent','system'].includes(run[2]) || typeof run[3] !== 'string' || !run[3] || run[3].length > 160 || !nonnegative(run[4]) ||
|
|
37
|
+
/[\uD800-\uDFFF]/u.test(text.slice(cursor, run[1]))) throw fault('invalid authorship run');
|
|
38
|
+
cursor = run[1];
|
|
39
|
+
}
|
|
40
|
+
if (cursor !== text.length) throw fault('authorship does not cover the text');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
// Authorship-only imports become an incomplete insertion ledger. There is no mutable blame database.
|
|
44
|
+
export function ledgerFromAuthorship(input, text, documentAuthority) {
|
|
45
|
+
const value = readAuthorship(input, text), records = [];
|
|
46
|
+
let current = '', root = textRoot('');
|
|
47
|
+
for (const [start, end, kind, id, createdAt] of value.runs) {
|
|
48
|
+
const row = {pos: start, removed: '', inserted: text.slice(start, end)}, revision = records.length + 1;
|
|
49
|
+
const beforeHash = root; root = rootAfter(root, row);
|
|
50
|
+
current = _rapierTransformSplices(current, [row]);
|
|
51
|
+
records.push({beforeHash, afterHash: root, splices: [row], transaction: {
|
|
52
|
+
id: documentAuthority.slice(0, 48) + ':authorship:' + revision.toString(36), documentAuthority,
|
|
53
|
+
baseRevision: revision - 1, revision, actor: {kind, id}, transport: 'platform', operation: 'authorship.import',
|
|
54
|
+
requestId: null, sourceTransactionId: null, affectedBlockIds: [], parent: records.at(-1)?.transaction.id ?? null,
|
|
55
|
+
reverts: null, reapplies: null, createdAt,
|
|
56
|
+
}});
|
|
57
|
+
}
|
|
58
|
+
return exportLedger({text: current, records, documentAuthority, revision: records.length, root, complete: false});
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {readLedger} from './format.mjs';
|
|
3
|
+
import {authorship as projectAuthorship, readAuthorship} from './authorship.mjs';
|
|
4
|
+
import {canonicalJSON, fault} from './data.mjs';
|
|
5
|
+
export function encodeCarried(text) {
|
|
6
|
+
return String(text).replace(/[\\\r\0]|<[/!]/g, m => m === '\\' ? '\\\\' : m === '\r' ? '\\r' : m === '\0' ? '\\0' : m === '</' ? '\\/' : '\\!');
|
|
7
|
+
}
|
|
8
|
+
export function decodeCarried(text) {
|
|
9
|
+
return String(text).replace(/\\([\\/!r0])/g, (_, c) => c === '\\' ? '\\' : c === 'r' ? '\r' : c === '0' ? '\0' : c === '/' ? '</' : '<!');
|
|
10
|
+
}
|
|
11
|
+
export function validateParts(text, {ledger = null, authorship = null} = {}) {
|
|
12
|
+
// A file's leading BOM is metadata, as in the editor's source intake.
|
|
13
|
+
const canonical = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
14
|
+
const history = ledger === null ? null : readLedger(ledger, canonical).ledger;
|
|
15
|
+
const runs = authorship === null ? null : readAuthorship(authorship, canonical);
|
|
16
|
+
if (history && runs && canonicalJSON(projectAuthorship(history)) !== canonicalJSON(runs)) throw fault('authorship differs from ledger replay');
|
|
17
|
+
return {ledger: history, authorship: runs};
|
|
18
|
+
}
|
|
19
|
+
export function writeParts(text, options) {
|
|
20
|
+
const parts = validateParts(text, options);
|
|
21
|
+
return Object.entries(parts).filter(([, value]) => value !== null).map(([name, value]) =>
|
|
22
|
+
'<script type="application/json" id="rapier-' + name + '">' + encodeCarried(JSON.stringify(value)) + '</script>\n').join('');
|
|
23
|
+
}
|
|
24
|
+
// Pure adapter: the browser supplies inert script elements, Node supplies parsed script records.
|
|
25
|
+
export function readParts(text, elements) {
|
|
26
|
+
const parts = {ledger: null, authorship: null};
|
|
27
|
+
for (const element of elements) {
|
|
28
|
+
const name = element.id === 'rapier-ledger' ? 'ledger' : element.id === 'rapier-authorship' ? 'authorship' : null;
|
|
29
|
+
if (!name) continue;
|
|
30
|
+
if (parts[name] !== null || element.type !== 'application/json' || (element.tagName && element.tagName.toLowerCase() !== 'script')) throw fault('duplicate or executable carried part');
|
|
31
|
+
try { parts[name] = JSON.parse(decodeCarried(element.textContent)); }
|
|
32
|
+
catch (_) { throw fault('invalid carried JSON'); }
|
|
33
|
+
if (parts[name] === null) throw fault('empty carried part');
|
|
34
|
+
}
|
|
35
|
+
return validateParts(text, parts);
|
|
36
|
+
}
|
|
37
|
+
// The wrapper only removes whole script elements. Strings inside other scripts are not carriers.
|
|
38
|
+
export function takeParts(html, text) {
|
|
39
|
+
const elements = [];
|
|
40
|
+
const stripped = html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (whole, attrs, content) => {
|
|
41
|
+
const attributes = [...attrs.matchAll(/(?:^|\s)([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g)];
|
|
42
|
+
const ids = attributes.filter(m => m[1].toLowerCase() === 'id').map(m => m[2] ?? m[3] ?? m[4]);
|
|
43
|
+
if (!ids.some(id => id === 'rapier-ledger' || id === 'rapier-authorship')) return whole;
|
|
44
|
+
const types = attributes.filter(m => m[1].toLowerCase() === 'type').map(m => m[2] ?? m[3] ?? m[4]);
|
|
45
|
+
if (ids.length !== 1 || types.length !== 1) throw fault('ambiguous carried part');
|
|
46
|
+
elements.push({id: ids[0], type: types[0].toLowerCase(), textContent: content}); return '';
|
|
47
|
+
});
|
|
48
|
+
if (elements.length && text === null) throw fault('carried history without a document');
|
|
49
|
+
return {html: stripped, ...readParts(text ?? '', elements)};
|
|
50
|
+
}
|
|
51
|
+
const OPEN = '\n\n<!-- rapier-carried/1\n', CLOSE = '\n-->\n';
|
|
52
|
+
// One optional HTML comment at EOF; every other Markdown reader ignores it. Source is never rewritten.
|
|
53
|
+
export function writeDocument(text, options) {
|
|
54
|
+
const parts = validateParts(text, options);
|
|
55
|
+
if (!parts.ledger && !parts.authorship) return text;
|
|
56
|
+
const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
57
|
+
return text + OPEN + json + CLOSE;
|
|
58
|
+
}
|
|
59
|
+
export function readDocument(source) {
|
|
60
|
+
const at = source.lastIndexOf(OPEN);
|
|
61
|
+
if (at < 0) return {text: source, ledger: null, authorship: null};
|
|
62
|
+
if (!source.endsWith(CLOSE)) throw fault('unfinished document metadata');
|
|
63
|
+
let parts;
|
|
64
|
+
try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
|
|
65
|
+
catch (_) { throw fault('invalid document metadata'); }
|
|
66
|
+
if (!parts || Object.keys(parts).length !== 2 || !Object.hasOwn(parts, 'ledger') || !Object.hasOwn(parts, 'authorship')) throw fault('invalid document parts');
|
|
67
|
+
const text = source.slice(0, at);
|
|
68
|
+
return {text, ...validateParts(text, parts)};
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Only plain JSON data enters the public format. Accessors and toJSON are never executed.
|
|
3
|
+
export function fault(message) { return Object.assign(new Error('Ledger: ' + message), {code: 'ledger_invalid'}); }
|
|
4
|
+
export function canonicalJSON(value, depth = 0, active = new Set()) {
|
|
5
|
+
if (value === null || typeof value === 'boolean') return JSON.stringify(value);
|
|
6
|
+
if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return JSON.stringify(value); }
|
|
7
|
+
if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return JSON.stringify(value);
|
|
8
|
+
if (!value || typeof value !== 'object' || depth > 64 || active.has(value)) throw fault('not plain JSON data');
|
|
9
|
+
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
10
|
+
if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null || Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) throw fault('not a plain object');
|
|
11
|
+
const descriptors = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(descriptors);
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (array && key === 'length') continue;
|
|
14
|
+
const field = descriptors[key];
|
|
15
|
+
if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) || !Object.hasOwn(field, 'value') || !field.enumerable) throw fault('non-data property');
|
|
16
|
+
}
|
|
17
|
+
if (array && (keys.length !== value.length + 1 || keys.some(k => k !== 'length' && (!/^(0|[1-9]\d*)$/.test(k) || +k >= value.length)))) throw fault('sparse array');
|
|
18
|
+
active.add(value);
|
|
19
|
+
const encode = item => canonicalJSON(item, depth + 1, active);
|
|
20
|
+
const out = array ? '[' + Array.from({length: value.length}, (_, i) => encode(descriptors[i].value)).join(',') + ']'
|
|
21
|
+
: '{' + keys.sort().map(key => JSON.stringify(key) + ':' + encode(descriptors[key].value)).join(',') + '}';
|
|
22
|
+
active.delete(value); return out;
|
|
23
|
+
}
|
|
24
|
+
export function plainData(value) { return JSON.parse(canonicalJSON(typeof value === 'string' ? JSON.parse(value) : value)); }
|
|
25
|
+
export function exactKeys(value, keys) {
|
|
26
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
27
|
+
}
|
|
28
|
+
export function documentText(text) {
|
|
29
|
+
if (typeof text !== 'string' || /[\uD800-\uDFFF]/u.test(text)) throw fault('document is not whole-character text');
|
|
30
|
+
if (new TextEncoder().encode(text).length > 25 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
export const nonnegative = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {_rapierValidLedgerRecord, _rapierRecordSplices, _rapierTransformSplices} from './journal-records.mjs';
|
|
3
|
+
import {textRoot, rootAfter, sha256} from './hash.mjs';
|
|
4
|
+
import {canonicalJSON, plainData, documentText, exactKeys, nonnegative, fault} from './data.mjs';
|
|
5
|
+
export {textRoot, rootAfter};
|
|
6
|
+
const digest = value => sha256(canonicalJSON(value));
|
|
7
|
+
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
8
|
+
export function readLedger(input, expectedText) {
|
|
9
|
+
const ledger = plainData(input);
|
|
10
|
+
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/1' ||
|
|
11
|
+
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
12
|
+
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
13
|
+
!exactKeys(ledger.start, ['text','revision','root','sha256']) || !exactKeys(ledger.head, ['revision','root','sha256'])) throw fault('invalid envelope');
|
|
14
|
+
const {sha256: claimed, ...payload} = ledger;
|
|
15
|
+
if (claimed !== digest(payload)) throw fault('envelope checksum differs');
|
|
16
|
+
let text = documentText(ledger.start.text), root = ledger.start.root, revision = ledger.start.revision;
|
|
17
|
+
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== textRoot(text)) || ledger.start.sha256 !== sha256(text) ||
|
|
18
|
+
(ledger.complete && revision !== 0)) throw fault('invalid start');
|
|
19
|
+
const earlier = [];
|
|
20
|
+
for (const record of ledger.records) {
|
|
21
|
+
if (!Array.isArray(record.splices) || !record.splices.length || !_rapierValidLedgerRecord(record, earlier.at(-1) || null, earlier,
|
|
22
|
+
ledger.documentAuthority, 25 * 1024 * 1024, text => new TextEncoder().encode(text).length, !ledger.complete) ||
|
|
23
|
+
record.transaction.baseRevision < revision || (record.transaction.baseRevision !== revision && ledger.complete) ||
|
|
24
|
+
(record.transaction.baseRevision === revision && record.beforeHash !== root)) throw fault('record chain differs');
|
|
25
|
+
// A pruned cancelling pair leaves a stated revision gap, just as in the editor's restore reader.
|
|
26
|
+
if (record.transaction.baseRevision !== revision) root = record.beforeHash;
|
|
27
|
+
const next = _rapierTransformSplices(text, record.splices);
|
|
28
|
+
if (next === null) throw fault('splice does not replay');
|
|
29
|
+
text = documentText(next);
|
|
30
|
+
for (const row of record.splices) root = rootAfter(root, row);
|
|
31
|
+
if (root !== record.afterHash) throw fault('record root differs');
|
|
32
|
+
revision = record.transaction.revision; earlier.push(record);
|
|
33
|
+
}
|
|
34
|
+
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text) ||
|
|
35
|
+
(expectedText !== undefined && text !== expectedText)) throw fault('history does not describe this document');
|
|
36
|
+
return {ledger, text, root, revision, complete: ledger.complete};
|
|
37
|
+
}
|
|
38
|
+
export function exportLedger({text, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
39
|
+
documentText(text);
|
|
40
|
+
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
41
|
+
// Inspect before touching properties: exporting must not call accessors on an alleged record.
|
|
42
|
+
records = plainData(records); earlier = plainData(earlier);
|
|
43
|
+
const expanded = records.map(record => {
|
|
44
|
+
const rows = _rapierRecordSplices(record, earlier);
|
|
45
|
+
if (!rows) throw fault('navigation target was not retained');
|
|
46
|
+
return {transaction: record.transaction, splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
47
|
+
beforeHash: record.beforeHash, afterHash: record.afterHash};
|
|
48
|
+
});
|
|
49
|
+
let startText = text;
|
|
50
|
+
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
51
|
+
startText = _rapierTransformSplices(startText, expanded[i].splices, true);
|
|
52
|
+
if (startText === null) throw fault('history does not reverse from this document');
|
|
53
|
+
}
|
|
54
|
+
revision ??= expanded.at(-1)?.transaction.revision ?? 0;
|
|
55
|
+
root ??= expanded.at(-1)?.afterHash ?? textRoot(text);
|
|
56
|
+
documentAuthority ??= expanded[0]?.transaction.documentAuthority;
|
|
57
|
+
const startRevision = expanded[0]?.transaction.baseRevision ?? revision;
|
|
58
|
+
const payload = {format: 'rapier-ledger/1', documentAuthority,
|
|
59
|
+
start: {text: startText, revision: startRevision, root: expanded[0]?.beforeHash ?? root, sha256: sha256(startText)},
|
|
60
|
+
records: expanded, head: {revision, root, sha256: sha256(text)}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
61
|
+
const ledger = {...payload, sha256: digest(payload)};
|
|
62
|
+
readLedger(ledger, text); return ledger;
|
|
63
|
+
}
|
|
64
|
+
// Derived navigation, never a second history. A target older than start is intentionally unavailable.
|
|
65
|
+
export function historyEnvelope(input, segmentIdentity = []) {
|
|
66
|
+
const {ledger, text} = readLedger(input), ids = new Set(ledger.records.map(row => row.transaction.id));
|
|
67
|
+
let branch = [], cursor = 0;
|
|
68
|
+
for (const {transaction: tx} of ledger.records) {
|
|
69
|
+
const target = tx.reverts || tx.reapplies;
|
|
70
|
+
if (target) {
|
|
71
|
+
if (!ids.has(target)) continue;
|
|
72
|
+
if (tx.reverts && branch[cursor - 1] === target) cursor--;
|
|
73
|
+
else if (tx.reapplies && branch[cursor] === target) cursor++;
|
|
74
|
+
else throw fault('invalid navigation order');
|
|
75
|
+
} else { branch = branch.slice(0, cursor); branch.push(tx.id); cursor = branch.length; }
|
|
76
|
+
}
|
|
77
|
+
const byId = new Map(ledger.records.map(row => [row.transaction.id, row]));
|
|
78
|
+
let traversed = text;
|
|
79
|
+
for (let i = cursor - 1; i >= 0; i--) {
|
|
80
|
+
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices, true);
|
|
81
|
+
if (traversed === null) throw fault('undo branch does not reverse');
|
|
82
|
+
}
|
|
83
|
+
traversed = text;
|
|
84
|
+
for (let i = cursor; i < branch.length; i++) {
|
|
85
|
+
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices);
|
|
86
|
+
if (traversed === null) throw fault('redo branch does not replay');
|
|
87
|
+
}
|
|
88
|
+
if (ledger.complete) {
|
|
89
|
+
if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
|
|
90
|
+
let active = ledger.start.text;
|
|
91
|
+
if (!cursor && active !== text) throw fault('complete history start differs');
|
|
92
|
+
for (let i = 0; i < branch.length; i++) {
|
|
93
|
+
active = _rapierTransformSplices(active, byId.get(branch[i]).splices);
|
|
94
|
+
if (active === null || (i + 1 === cursor && active !== text)) throw fault('complete branch differs');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {schemaVersion: 4, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
|
|
98
|
+
ledger: ledger.records, branch, cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
99
|
+
sourceRootId: ledger.head.root, segmentIdentity, historyComplete: ledger.complete,
|
|
100
|
+
trimReason: ledger.complete ? '' : 'carried_start', trimmedBytes: 0};
|
|
101
|
+
}
|
|
102
|
+
export function agentActorId(door, agent) {
|
|
103
|
+
if (typeof door !== 'string' || !door || door.length > 160 || /[\u0000-\u001f\u007f]/.test(door)) throw fault('invalid door');
|
|
104
|
+
if (agent == null) return door;
|
|
105
|
+
agent = plainData(agent);
|
|
106
|
+
if (!exactKeys(agent, ['name']) || typeof agent.name !== 'string' || !agent.name.trim() || agent.name.length > 96 ||
|
|
107
|
+
/[\u0000-\u001f\u007f]/.test(agent.name) || door.length + agent.name.length + 1 > 160) throw fault('invalid host-given name');
|
|
108
|
+
return door + '/' + agent.name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A writer changing representation records that change, rather than attaching an unrelated head.
|
|
112
|
+
export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'rapier'}, operation = 'document.replace', at} = {}) {
|
|
113
|
+
const current = readLedger(input); documentText(text);
|
|
114
|
+
if (current.text === text) return current.ledger;
|
|
115
|
+
let start = 0, a = current.text.length, b = text.length;
|
|
116
|
+
while (start < a && start < b && current.text[start] === text[start]) start++;
|
|
117
|
+
if (start && /[\uDC00-\uDFFF]/.test(current.text[start] || text[start] || '')) start--;
|
|
118
|
+
while (a > start && b > start && current.text[a - 1] === text[b - 1]) { a--; b--; }
|
|
119
|
+
if (/[\uDC00-\uDFFF]/.test(current.text[a] || text[b] || '')) { a++; b++; }
|
|
120
|
+
const splices = [{pos: start, removed: current.text.slice(start, a), inserted: text.slice(start, b)}];
|
|
121
|
+
const revision = current.revision + 1, root = rootAfter(current.root, splices[0]);
|
|
122
|
+
const record = {beforeHash: current.root, afterHash: root, splices, transaction: {
|
|
123
|
+
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' + revision.toString(36),
|
|
124
|
+
documentAuthority: current.ledger.documentAuthority, baseRevision: current.revision, revision,
|
|
125
|
+
actor, transport: 'platform', operation, requestId: null, sourceTransactionId: null,
|
|
126
|
+
affectedBlockIds: [], parent: current.ledger.records.at(-1)?.transaction.id ?? null,
|
|
127
|
+
reverts: null, reapplies: null, createdAt: at ?? current.ledger.records.at(-1)?.transaction.createdAt ?? 0,
|
|
128
|
+
}};
|
|
129
|
+
return exportLedger({text, records: [...current.ledger.records, record], documentAuthority: current.ledger.documentAuthority,
|
|
130
|
+
revision, root, complete: current.complete});
|
|
131
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
|
|
3
|
+
export function textRoot(text) {
|
|
4
|
+
let fnv = 0x811c9dc5, a = 1, b = 0;
|
|
5
|
+
for (let i = 0; i < text.length; i++) {
|
|
6
|
+
const c = text.charCodeAt(i);
|
|
7
|
+
fnv = Math.imul(fnv ^ c, 16777619); a = (a + c) % 65521; b = (b + a) % 65521;
|
|
8
|
+
}
|
|
9
|
+
return text.length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
|
|
10
|
+
}
|
|
11
|
+
export function rootAfter(root, row) {
|
|
12
|
+
return textRoot(String(root || '') + ':' + row.pos + ':' + textRoot(row.removed) + ':' + textRoot(row.inserted));
|
|
13
|
+
}
|
|
14
|
+
const K = new Uint32Array([
|
|
15
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
16
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
17
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
18
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
19
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
20
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
21
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
22
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
23
|
+
]);
|
|
24
|
+
const rotate = (x, n) => (x >>> n) | (x << (32 - n));
|
|
25
|
+
export function sha256(text) {
|
|
26
|
+
const bytes = new TextEncoder().encode(text);
|
|
27
|
+
const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
|
|
28
|
+
padded.set(bytes); padded[bytes.length] = 0x80;
|
|
29
|
+
const data = new DataView(padded.buffer), bits = bytes.length * 8;
|
|
30
|
+
data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
|
|
31
|
+
const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
|
|
32
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
33
|
+
for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
|
|
34
|
+
for (let i = 16; i < 64; i++) {
|
|
35
|
+
const x = w[i - 15], y = w[i - 2];
|
|
36
|
+
w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
|
|
37
|
+
}
|
|
38
|
+
let [a,b,c,d,e,f,g,k] = h;
|
|
39
|
+
for (let i = 0; i < 64; i++) {
|
|
40
|
+
const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
41
|
+
const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
42
|
+
k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
43
|
+
}
|
|
44
|
+
for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
|
|
45
|
+
}
|
|
46
|
+
return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
|
|
47
|
+
}
|