rapier-markdown-kit 1.2.32 → 1.2.40
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/README.md +16 -10
- package/dist/agent/vendor/pretext/NOTICE +2 -2
- package/dist/agent/vendor/pretext/SOURCE.json +62 -78
- package/dist/agent/vendor/pretext/analysis.js +24 -6
- package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
- package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
- package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
- package/dist/agent/vendor/pretext/graphemes.js +1 -1
- package/dist/agent/vendor/pretext/han-kerning.js +1 -1
- package/dist/agent/vendor/pretext/layout.js +14 -11
- package/dist/agent/vendor/pretext/line-break.js +170 -195
- package/dist/agent/vendor/pretext/line-breaks.js +1 -7
- package/dist/agent/vendor/pretext/line-text.js +2 -2
- package/dist/agent/vendor/pretext/measurement.js +454 -113
- package/dist/agent/vendor/pretext/prepare.js +164 -244
- package/dist/agent/vendor/pretext/rich-inline.js +57 -37
- package/dist/agent/will.mjs +73 -0
- package/dist/kit/ledger/data.mjs +2 -2
- package/dist/kit/ledger/format.mjs +86 -42
- package/dist/kit/ledger/hash.mjs +43 -2
- package/dist/kit/ledger/history.mjs +6 -0
- package/dist/kit/ledger/inverse-source.mjs +6 -2
- package/dist/kit/ledger/journal-records.mjs +4 -2
- package/dist/kit/ledger/merge.mjs +3 -17
- package/dist/kit/ledger/text-merge.mjs +191 -28
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-markdown.mjs +133 -10
- package/dist/kit/render-print.mjs +3 -3
- package/dist/kit/render-sanitize.mjs +66 -23
- package/dist/kit/render.mjs +10 -10
- package/dist/kit/wiki.mjs +34 -0
- package/dist/kit/will.d.mts +3 -0
- package/dist/layout/hyphenation.mjs +6 -4
- package/dist/layout/line-plan.mjs +96 -0
- package/dist/layout/model.mjs +2 -1
- package/dist/layout/paragraph-fit.mjs +4 -4
- package/dist/layout/prepared-paragraph.mjs +19 -2
- package/dist/layout/typography.mjs +57 -63
- package/dist/spec/document-fonts.mjs +1 -0
- package/dist/spec/document-settings.mjs +1 -0
- package/dist/spec/html-blocks.mjs +10 -7
- package/dist/spec/md-assets.mjs +52 -12
- package/package.json +1 -1
- package/style.css +37 -28
- package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
/*! Pretext
|
|
1
|
+
/*! Pretext 1.0.0 + main (0d102a5cd) | MIT; BSD/Unicode/ICU notices in LICENSE | Copyright (c) 2026 Pretext contributors */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
alignToSource,
|
|
5
5
|
analyzeText,
|
|
6
6
|
isCollapsibleSpaceCode,
|
|
7
|
+
DROPPED,
|
|
7
8
|
HARD_BREAK,
|
|
8
9
|
KIND_BITS,
|
|
9
10
|
OBJECT,
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
} from './analysis.js'
|
|
23
|
-
import { getSegmentEntryWidth, } from './entry-geometry.js'
|
|
24
24
|
import { buildLineTextFromRange, buildRangeText, getGraphemeEnds, } from './line-text.js'
|
|
25
25
|
import {
|
|
26
26
|
getItemTabAdvance,
|
|
@@ -166,7 +166,8 @@ const PRESERVED_WHITE_SPACE = 1 << PRESERVED_SPACE | 1 << TAB
|
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
|
|
170
|
+
|
|
170
171
|
|
|
171
172
|
|
|
172
173
|
|
|
@@ -300,8 +301,6 @@ export function prepareRichInline(items ,
|
|
|
300
301
|
// Firefox 156 and Safari 27 prepare CJK styled paragraphs again up to 8% faster with the lists
|
|
301
302
|
// made whole than with each filled by a call and a push a segment (RESEARCH.md, Keeping Work
|
|
302
303
|
// Bounded, JavaScript Engines, under A paragraph's sparse lists made whole).
|
|
303
|
-
let entryGeometry = null
|
|
304
|
-
let lineStartProhibitions = null
|
|
305
304
|
let breakableLineStartExtras = null
|
|
306
305
|
let lineStartExtras = null
|
|
307
306
|
let lineEndTrims = null
|
|
@@ -531,8 +530,6 @@ export function prepareRichInline(items ,
|
|
|
531
530
|
}
|
|
532
531
|
const units = getSourceUnits(offsets, normalizedStart, textEnd, starts[index] )
|
|
533
532
|
if (units !== null) (sourceUnits ??= new Array (flags.length).fill(null))[at] = units
|
|
534
|
-
if (sub.entryGeometry !== null && sub.entryGeometry[s] !== null) (entryGeometry ??= new Array (flags.length).fill(null))[at] = sub.entryGeometry[s]
|
|
535
|
-
if (sub.lineStartProhibitions !== null && sub.lineStartProhibitions[s] !== null) (lineStartProhibitions ??= new Array (flags.length).fill(null))[at] = sub.lineStartProhibitions[s]
|
|
536
533
|
if (sub.breakableLineStartExtras !== null && sub.breakableLineStartExtras[s] !== null) (breakableLineStartExtras ??= new Array (flags.length).fill(null))[at] = sub.breakableLineStartExtras[s]
|
|
537
534
|
const startExtra = (sub.lineStartExtras === null ? 0 : sub.lineStartExtras[s] ) + (i > first ? extraWidth : 0)
|
|
538
535
|
if (startExtra !== 0) (lineStartExtras ??= zeroList(flags.length))[at] = startExtra
|
|
@@ -621,7 +618,7 @@ export function prepareRichInline(items ,
|
|
|
621
618
|
itemEndHalts,
|
|
622
619
|
hyphenRooms: discretionaryHyphenContexts !== null && profile.unfitHyphenRetreat === 'reduced-width' ? getHyphenRooms(hyphenWidths, itemSegments, segmentItems) : null,
|
|
623
620
|
insideExtras, fillExtras, openingEdges,
|
|
624
|
-
emptyObjectSpaces: null, emptyObjectReturns: null,
|
|
621
|
+
emptyObjectSpaces: null, emptyObjectReturns: null, hangEnds: null,
|
|
625
622
|
}
|
|
626
623
|
// The properties measureAnalysis() gives a text's handle, in its order, then the paragraph's own.
|
|
627
624
|
const data = {
|
|
@@ -630,11 +627,9 @@ export function prepareRichInline(items ,
|
|
|
630
627
|
simpleLineWalkFastPath: simple,
|
|
631
628
|
simpleLineCountFastPath: false,
|
|
632
629
|
breakableFitAdvances,
|
|
633
|
-
entryGeometry,
|
|
634
630
|
letterSpacing: spacingsDiffer || sharedSpacing === null ? 0 : sharedSpacing,
|
|
635
631
|
discretionaryHyphenWidth: firstTextItem < 0 ? 0 : hyphenWidths[firstTextItem] ,
|
|
636
632
|
discretionaryHyphenContexts,
|
|
637
|
-
lineStartProhibitions,
|
|
638
633
|
breakableLineStartExtras,
|
|
639
634
|
lineStartExtras,
|
|
640
635
|
lineEndTrims,
|
|
@@ -645,6 +640,15 @@ export function prepareRichInline(items ,
|
|
|
645
640
|
}
|
|
646
641
|
data.segments = segments
|
|
647
642
|
if (hasEmptyObject && profile.emptyAtomicAlwaysFits) setEmptyObjectFacts(segmentData, data, items, itemSegments, segmentItems, source, starts)
|
|
643
|
+
// Each item is a text frame: a zero-width space after preserved spaces of its own item ends
|
|
644
|
+
// the run of them that hangs (ParagraphSegmentData, hangEnds).
|
|
645
|
+
if (preserve && profile.hangsSpacesPerTextFrame && source.includes('\u200B')) {
|
|
646
|
+
for (let i = 1; i < segmentCount; i++) {
|
|
647
|
+
if ((segmentFlags[i] & (KIND_BITS | DROPPED)) === ZERO_WIDTH_BREAK && (segmentFlags[i - 1] & KIND_BITS) === PRESERVED_SPACE && segmentItems[i] === segmentItems[i - 1]) {
|
|
648
|
+
;(segmentData.hangEnds ??= new Uint8Array(segmentCount))[i] = 1
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
648
652
|
|
|
649
653
|
let onlyItem = -1
|
|
650
654
|
for (let index = 0; index < items.length && !paddedOrObject; index++) {
|
|
@@ -894,19 +898,13 @@ function getOpeningFit(flags , from , to , extraWidth
|
|
|
894
898
|
// (`startsLine`), the first of them adds what a line that starts with it does
|
|
895
899
|
// (breakableLineStartExtras), as the walker's line does.
|
|
896
900
|
function getPartWidth(data , i , from , to , startsLine ) {
|
|
897
|
-
const { breakableFitAdvances, breakableLineStartExtras,
|
|
901
|
+
const { breakableFitAdvances, breakableLineStartExtras, letterSpacing, items } = data
|
|
898
902
|
const advances = breakableFitAdvances[i]
|
|
899
903
|
if (to < 0) to = advances.length
|
|
900
|
-
const entry = from > 0 && entryGeometry !== null ? entryGeometry[i] : null
|
|
901
|
-
const entryWidth = entry === null ? null : getSegmentEntryWidth(entry, from, to)
|
|
902
904
|
let w = 0
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
for (let g = from; g < to; g++) w += advances[g] + letterSpacing
|
|
907
|
-
const startExtras = startsLine && breakableLineStartExtras !== null ? breakableLineStartExtras[i] : null
|
|
908
|
-
if (startExtras !== null) w += startExtras[from]
|
|
909
|
-
}
|
|
905
|
+
for (let g = from; g < to; g++) w += advances[g] + letterSpacing
|
|
906
|
+
const startExtras = startsLine && breakableLineStartExtras !== null ? breakableLineStartExtras[i] : null
|
|
907
|
+
if (startExtras !== null) w += startExtras[from]
|
|
910
908
|
if (items !== undefined && (from > 0 ? items.insideExtras !== null : items.fillExtras !== null)) w += from > 0 ? items.insideExtras [i] : items.fillExtras [i]
|
|
911
909
|
return w
|
|
912
910
|
}
|
|
@@ -928,7 +926,7 @@ function createOnlyItemLine(
|
|
|
928
926
|
endGraphemeIndex ,
|
|
929
927
|
) {
|
|
930
928
|
const { onlyItem } = flow
|
|
931
|
-
const ended = endSegmentIndex >= flow.data.
|
|
929
|
+
const ended = endSegmentIndex >= flow.data.widths.length
|
|
932
930
|
return {
|
|
933
931
|
fragments: [{
|
|
934
932
|
itemIndex: onlyItem,
|
|
@@ -974,7 +972,7 @@ function createLine(
|
|
|
974
972
|
const segmentItems = flow.segmentItems
|
|
975
973
|
const gapSegments = flow.gapSegments
|
|
976
974
|
const itemCount = itemSegments.length - 1
|
|
977
|
-
const endItemIndex = endSegmentIndex >=
|
|
975
|
+
const endItemIndex = endSegmentIndex >= widths.length ? itemCount : segmentItems[endSegmentIndex]
|
|
978
976
|
const end = { itemIndex: endItemIndex, segmentIndex: endSegmentIndex - itemSegments[endItemIndex] , graphemeIndex: endGraphemeIndex }
|
|
979
977
|
const tabsInAppUnits = getEngineProfile().tabsInAppUnits
|
|
980
978
|
const itemEndHalts = items.itemEndHalts
|
|
@@ -1108,7 +1106,7 @@ function createLine(
|
|
|
1108
1106
|
// The paragraph's segment a cursor names, or the segment count for one past the last item.
|
|
1109
1107
|
function getSegmentIndex(flow , cursor ) {
|
|
1110
1108
|
const { itemSegments } = flow
|
|
1111
|
-
return cursor.itemIndex < itemSegments.length ? itemSegments[cursor.itemIndex] + cursor.segmentIndex : flow.data.
|
|
1109
|
+
return cursor.itemIndex < itemSegments.length ? itemSegments[cursor.itemIndex] + cursor.segmentIndex : flow.data.widths.length
|
|
1112
1110
|
}
|
|
1113
1111
|
|
|
1114
1112
|
export function layoutNextRichInlineLineRange(
|
|
@@ -1142,9 +1140,26 @@ function getSourceUnits(offsets , from , to , itemStart
|
|
|
1142
1140
|
return units
|
|
1143
1141
|
}
|
|
1144
1142
|
|
|
1143
|
+
// Per frozen handle of a paragraph that is one text's handle, the three lists of where its
|
|
1144
|
+
// segments are in the item's text (InternalPreparedRichInline), found when a line of it is
|
|
1145
|
+
// first materialized. A handle keeps them itself; a frozen one can't take them, so they are
|
|
1146
|
+
// kept beside it, as its segments' grapheme ends are (getGraphemeEnds): no line function writes
|
|
1147
|
+
// to a frozen handle or to anything it holds, so one the app froze, or a copy of one in a store
|
|
1148
|
+
// that freezes its state, materializes as any other. Kept beside every handle, the lookup took
|
|
1149
|
+
// a pass that walks and materializes 3,000 of the chat demo's one-item paragraphs, each asked
|
|
1150
|
+
// once, 35-47% longer with the handles out of the processor's caches and 7-25% with them in,
|
|
1151
|
+
// and 40 of them over and over 1-9% (JavaScriptCore of bun 1.4.2, V8 15.4 and node 23.10's,
|
|
1152
|
+
// SpiderMonkey 156, one build a process on the harness's stand-in Canvas, 2026-10-12). Found
|
|
1153
|
+
// while preparing, the lists took such paragraphs 6-13% longer to prepare and count once in
|
|
1154
|
+
// JavaScriptCore and 12-20% in V8, text seen before, and a third more heap a handle, for lines
|
|
1155
|
+
// a long list mostly never paints; found again at every line, walking and materializing took
|
|
1156
|
+
// 5-9 times as long, and over 100 times in a paragraph of 6,000 units.
|
|
1157
|
+
|
|
1158
|
+
const textSources = new WeakMap ()
|
|
1159
|
+
|
|
1145
1160
|
// Per segment of a paragraph that is one text's handle, where its text starts and ends in the
|
|
1146
1161
|
// item's.
|
|
1147
|
-
function findSegmentSources(flow )
|
|
1162
|
+
function findSegmentSources(flow ) {
|
|
1148
1163
|
const { segments } = flow.data
|
|
1149
1164
|
const offsets = alignToSource(flow.text, segments.join(''), getEngineProfile().lineBreakScan)
|
|
1150
1165
|
const sourceStarts = []
|
|
@@ -1156,15 +1171,21 @@ function findSegmentSources(flow ) {
|
|
|
1156
1171
|
sourceEnds.push(flow.text.charCodeAt(end - 1) === 0x0D && flow.text.charCodeAt(end) === 0x0A && segments[i] === '\n' ? end + 1 : end)
|
|
1157
1172
|
sourceUnits.push(getSourceUnits(offsets, at, at + segments[i] .length, 0))
|
|
1158
1173
|
}
|
|
1159
|
-
|
|
1160
|
-
flow
|
|
1161
|
-
|
|
1174
|
+
const sources = { sourceStarts, sourceEnds, sourceUnits }
|
|
1175
|
+
if (Object.isFrozen(flow)) {
|
|
1176
|
+
textSources.set(flow, sources)
|
|
1177
|
+
} else {
|
|
1178
|
+
flow.sourceStarts = sourceStarts
|
|
1179
|
+
flow.sourceEnds = sourceEnds
|
|
1180
|
+
flow.sourceUnits = sourceUnits
|
|
1181
|
+
}
|
|
1182
|
+
return sources
|
|
1162
1183
|
}
|
|
1163
1184
|
|
|
1164
1185
|
// Where unit `unit` of a segment's text is in its item's text.
|
|
1165
|
-
function getSourceOffset(
|
|
1166
|
-
const units =
|
|
1167
|
-
return units === null ?
|
|
1186
|
+
function getSourceOffset(sourceStarts , sourceUnits , segmentIndex , unit ) {
|
|
1187
|
+
const units = sourceUnits === null ? null : sourceUnits[segmentIndex]
|
|
1188
|
+
return units === null ? sourceStarts[segmentIndex] + unit : units[unit]
|
|
1168
1189
|
}
|
|
1169
1190
|
|
|
1170
1191
|
// Bridge from cheap range walking to full fragment text. Lets callers do
|
|
@@ -1176,9 +1197,8 @@ export function materializeRichInlineLineRange(
|
|
|
1176
1197
|
) {
|
|
1177
1198
|
const flow = getInternalPreparedRichInline(prepared)
|
|
1178
1199
|
const { data, itemSegments } = flow
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
const sourceEnds = flow.sourceEnds
|
|
1200
|
+
let { sourceStarts, sourceEnds, sourceUnits } = flow
|
|
1201
|
+
if (sourceStarts === null) ({ sourceStarts, sourceEnds, sourceUnits } = textSources.get(flow) ?? findSegmentSources(flow))
|
|
1182
1202
|
const lineEndSegmentIndex = getSegmentIndex(flow, line.end)
|
|
1183
1203
|
const fragments = []
|
|
1184
1204
|
|
|
@@ -1194,11 +1214,11 @@ export function materializeRichInlineLineRange(
|
|
|
1194
1214
|
const endGraphemeIndex = endSegmentIndex < itemEnd ? fragment.end.graphemeIndex : 0
|
|
1195
1215
|
// Only the fragment a line ends with shows the hyphen of a soft hyphen it ends at.
|
|
1196
1216
|
const endsLine = i === line.fragments.length - 1 && endSegmentIndex === lineEndSegmentIndex && endGraphemeIndex === line.end.graphemeIndex
|
|
1197
|
-
let sourceStart = startSegmentIndex < itemEnd ? sourceStarts[startSegmentIndex] : startSegmentIndex > first ? sourceEnds[startSegmentIndex - 1] : 0
|
|
1198
|
-
if (startGraphemeIndex > 0 && startSegmentIndex < itemEnd) sourceStart = getSourceOffset(
|
|
1217
|
+
let sourceStart = startSegmentIndex < itemEnd ? sourceStarts[startSegmentIndex] : startSegmentIndex > first ? sourceEnds [startSegmentIndex - 1] : 0
|
|
1218
|
+
if (startGraphemeIndex > 0 && startSegmentIndex < itemEnd) sourceStart = getSourceOffset(sourceStarts, sourceUnits, startSegmentIndex, getGraphemeEnds(data, startSegmentIndex)[startGraphemeIndex - 1] )
|
|
1199
1219
|
let sourceEnd = sourceStart
|
|
1200
|
-
if (endGraphemeIndex > 0) sourceEnd = getSourceOffset(
|
|
1201
|
-
else if (endSegmentIndex > startSegmentIndex) sourceEnd = sourceEnds[endSegmentIndex - 1]
|
|
1220
|
+
if (endGraphemeIndex > 0) sourceEnd = getSourceOffset(sourceStarts, sourceUnits, endSegmentIndex, getGraphemeEnds(data, endSegmentIndex)[endGraphemeIndex - 1] - 1) + 1
|
|
1221
|
+
else if (endSegmentIndex > startSegmentIndex) sourceEnd = sourceEnds [endSegmentIndex - 1]
|
|
1202
1222
|
fragments.push({
|
|
1203
1223
|
itemIndex: fragment.itemIndex,
|
|
1204
1224
|
text: endsLine ? buildLineTextFromRange(data, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex)
|
package/dist/agent/will.mjs
CHANGED
|
@@ -162,5 +162,78 @@ function _rapierWillStripOneTerminator(text) {
|
|
|
162
162
|
text.charCodeAt(end - 1), end >= 2 ? text.charCodeAt(end - 2) : -1)) : text;
|
|
163
163
|
}
|
|
164
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) {
|
|
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);
|
|
235
|
+
return refusal ? {outcome: 'refused', reason: 'document_law', ...refusal} : {outcome: 'applied'};
|
|
236
|
+
}
|
|
237
|
+
|
|
165
238
|
|
|
166
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/kit/ledger/data.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export function fields(value, depth, active, sorted = true) {
|
|
|
17
17
|
}
|
|
18
18
|
export function scalar(value) {
|
|
19
19
|
if (value === null || typeof value === 'boolean') return true;
|
|
20
|
-
if (typeof value === 'string') { if (
|
|
20
|
+
if (typeof value === 'string') { if (!value.isWellFormed()) throw fault('unpaired surrogate'); return true; }
|
|
21
21
|
return typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0);
|
|
22
22
|
}
|
|
23
23
|
export function plainData(value) {
|
|
@@ -39,7 +39,7 @@ function* quoted(text, whole = true) {
|
|
|
39
39
|
let end = Math.min(text.length, at + 16384);
|
|
40
40
|
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 && (text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
41
41
|
const part = text.slice(at, end);
|
|
42
|
-
if (whole &&
|
|
42
|
+
if (whole && !part.isWellFormed()) throw fault('unpaired surrogate');
|
|
43
43
|
yield JSON.stringify(part).slice(1, -1); at = end;
|
|
44
44
|
}
|
|
45
45
|
yield '"';
|
|
@@ -3,10 +3,12 @@ import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMeta
|
|
|
3
3
|
import {pictureHistorySplices} from './history.mjs';
|
|
4
4
|
import {HistorySource} from './source.mjs';
|
|
5
5
|
import {authoredHistory} from './transport.mjs';
|
|
6
|
-
import {textRoot, rootAfter, sha256,
|
|
6
|
+
import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
|
|
7
7
|
import {canonicalJSON, canonicalParts, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
|
|
8
|
+
import {diff} from '../../packages/rapier-diff/diff.mjs';
|
|
9
|
+
import {minimalHistorySplice} from './inverse-source.mjs';
|
|
10
|
+
import {evaluateWill} from '../../agent/will.mjs';
|
|
8
11
|
export {textRoot, rootAfter};
|
|
9
|
-
const digest = value => sha256Parts(canonicalParts(value));
|
|
10
12
|
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
11
13
|
function* checkedRecords(ledger, prepared) {
|
|
12
14
|
if (!prepared.ok || ledger.records.length !== prepared.records.length) throw fault('record chain differs');
|
|
@@ -26,42 +28,61 @@ function* checkedRecords(ledger, prepared) {
|
|
|
26
28
|
if (ledger.head.revision !== revision || ledger.head.root !== root) throw fault('history does not describe this document');
|
|
27
29
|
}
|
|
28
30
|
const proofs = new WeakMap();
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
+
function copiedLedger(input, proof) {
|
|
32
|
+
if (!proof || !input || typeof input !== 'object') return plainData(input);
|
|
33
|
+
const fields = Object.getOwnPropertyDescriptors(input), start = fields.start;
|
|
34
|
+
if (!start || !Object.hasOwn(start, 'value') || !start.value || typeof start.value !== 'object') return plainData(input);
|
|
35
|
+
const parts = Object.getOwnPropertyDescriptors(start.value), text = parts.text;
|
|
36
|
+
if (!text || !Object.hasOwn(text, 'value') || text.value !== proof.startText) return plainData(input);
|
|
37
|
+
const shell = Object.create(Object.getPrototypeOf(input), {...fields, start: {...start,
|
|
38
|
+
value: Object.create(Object.getPrototypeOf(start.value), {...parts, text: {...text, value: ''}})}});
|
|
39
|
+
const ledger = plainData(shell);
|
|
40
|
+
ledger.start.text = proof.startText;
|
|
41
|
+
return ledger;
|
|
42
|
+
}
|
|
43
|
+
function ledgerAdmission(input) {
|
|
44
|
+
const held = input && typeof input === 'object' ? proofs.get(input) : undefined;
|
|
45
|
+
const ledger = copiedLedger(input, held);
|
|
31
46
|
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/2' ||
|
|
32
47
|
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
33
48
|
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
34
49
|
!exactKeys(ledger.start, ['text','revision','root','sha256','metadata']) || !exactKeys(ledger.head, ['revision','root','sha256','metadata'])) throw fault('unsupported or invalid envelope');
|
|
35
50
|
const shell = canonicalJSON({...ledger, start: {...ledger.start, text: ''}});
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
51
|
+
const proof = held?.shell === shell && held.startText === ledger.start.text ? held : null;
|
|
52
|
+
return {ledger, shell, proof};
|
|
53
|
+
}
|
|
54
|
+
function ledgerResult(ledger, proof, expectedText, expectedMetadata) {
|
|
41
55
|
proofs.set(ledger, proof);
|
|
42
56
|
const {text, metadata, root, revision, prepared, source} = proof;
|
|
43
57
|
if (expectedMetadata !== undefined && (!_rapierValidMetadata(expectedMetadata) || canonicalJSON(metadata) !== canonicalJSON(expectedMetadata)))
|
|
44
58
|
throw fault('metadata does not describe this document');
|
|
45
59
|
if (expectedText !== undefined && text !== expectedText) throw fault('history does not describe this document');
|
|
46
|
-
return {value: {ledger, text, metadata: {...metadata}, root, revision, complete: ledger.complete}, prepared, source};
|
|
60
|
+
return {value: {ledger, text, metadata: {...metadata}, root, revision, complete: ledger.complete}, prepared, source, proof};
|
|
47
61
|
}
|
|
48
|
-
function
|
|
62
|
+
function readLedgerState(input, expectedText, expectedMetadata) {
|
|
63
|
+
const {ledger, shell, proof: held} = ledgerAdmission(input);
|
|
64
|
+
const proof = held || {shell, startText: ledger.start.text, ...finish(proveLedger(ledger))};
|
|
65
|
+
if (typeof input === 'object') proofs.set(input, proof);
|
|
66
|
+
return ledgerResult(ledger, proof, expectedText, expectedMetadata);
|
|
67
|
+
}
|
|
68
|
+
function* proveLedger(ledger, native = false) {
|
|
49
69
|
const {sha256: claimed, ...payload} = ledger;
|
|
50
|
-
if (claimed !==
|
|
51
|
-
let text =
|
|
70
|
+
if (claimed !== (yield* checksum(canonicalParts(payload), native))) throw fault('envelope checksum differs');
|
|
71
|
+
let text = ledger.start.text, root = ledger.start.root, revision = ledger.start.revision;
|
|
72
|
+
const startFacts = yield* sourceFacts(text, native);
|
|
52
73
|
if (!_rapierValidMetadata(ledger.start.metadata) || !_rapierValidMetadata(ledger.head.metadata)) throw fault('invalid document metadata');
|
|
53
|
-
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !==
|
|
74
|
+
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== startFacts.contentRoot) || ledger.start.sha256 !== startFacts.sha256 ||
|
|
54
75
|
(ledger.complete && revision !== 0)) throw fault('invalid start');
|
|
55
76
|
const prepared = _rapierPrepareHistoryRecords(ledger.records);
|
|
56
77
|
if (!prepared.ok) throw fault('record chain differs');
|
|
57
|
-
let source = new HistorySource(text), bytes =
|
|
78
|
+
let source = new HistorySource(text), bytes = startFacts.utf8Bytes;
|
|
58
79
|
for (const record of checkedRecords(ledger, prepared)) {
|
|
59
80
|
const next = source.apply(record.splices);
|
|
60
81
|
if (next === null) throw fault('splice does not replay');
|
|
61
82
|
source = next;
|
|
62
83
|
for (const row of record.splices) bytes += utf8Length(row.inserted) - utf8Length(row.removed);
|
|
63
84
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
64
|
-
root = record.afterHash; revision = record.transaction.revision;
|
|
85
|
+
root = record.afterHash; revision = record.transaction.revision; yield;
|
|
65
86
|
}
|
|
66
87
|
text = source.slice();
|
|
67
88
|
if (ledger.records.some(row => Object.hasOwn(row, 'authored'))) {
|
|
@@ -69,7 +90,8 @@ function proveLedger(ledger) {
|
|
|
69
90
|
}
|
|
70
91
|
const metadata = prepared.replayMetadata(ledger.start.metadata);
|
|
71
92
|
if (!metadata || canonicalJSON(metadata) !== canonicalJSON(ledger.head.metadata)) throw fault('metadata does not describe this document');
|
|
72
|
-
|
|
93
|
+
const headFacts = ledger.records.every(row => !row.splices.length) ? startFacts : yield* sourceFacts(text, native);
|
|
94
|
+
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== headFacts.sha256) throw fault('history does not describe this document');
|
|
73
95
|
return {text, metadata, root, revision, prepared, source};
|
|
74
96
|
}
|
|
75
97
|
export function ledgerWithStart(input, text) {
|
|
@@ -83,14 +105,25 @@ export function ledgerWithStart(input, text) {
|
|
|
83
105
|
export function readLedger(input, expectedText, expectedMetadata) {
|
|
84
106
|
return readLedgerState(input, expectedText, expectedMetadata).value;
|
|
85
107
|
}
|
|
86
|
-
function
|
|
108
|
+
export async function readLedgerAsync(input, expectedText, expectedMetadata, options = {}) {
|
|
109
|
+
options.signal?.throwIfAborted();
|
|
110
|
+
const {ledger, shell, proof: held} = ledgerAdmission(input);
|
|
111
|
+
const metadata = expectedMetadata === undefined ? undefined : plainData(expectedMetadata);
|
|
112
|
+
const proof = held || {shell, startText: ledger.start.text,
|
|
113
|
+
...await finishAsync(proveLedger(ledger, !!globalThis.crypto?.subtle), options)};
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
115
|
+
if (typeof input === 'object') proofs.set(input, proof);
|
|
116
|
+
return ledgerResult(ledger, proof, expectedText, metadata).value;
|
|
117
|
+
}
|
|
118
|
+
function* checksum(parts, native = false) {
|
|
119
|
+
if (native) return yield {parts};
|
|
87
120
|
const hash = createSHA256();
|
|
88
121
|
for (const part of parts) { hash.update(part); yield; }
|
|
89
122
|
return hash.digest();
|
|
90
123
|
}
|
|
91
|
-
function* sourceFacts(text) {
|
|
124
|
+
function* sourceFacts(text, native = false) {
|
|
92
125
|
if (typeof text !== 'string') throw fault('document is not whole-character text');
|
|
93
|
-
const hash = createSHA256();
|
|
126
|
+
const hash = native ? null : createSHA256();
|
|
94
127
|
let fnv = 0x811c9dc5, a = 1, b = 0, bytes = 0;
|
|
95
128
|
for (let at = 0; at < text.length;) {
|
|
96
129
|
const start = at, end = Math.min(text.length, at + 65536);
|
|
@@ -112,18 +145,19 @@ function* sourceFacts(text) {
|
|
|
112
145
|
a %= 65521; b %= 65521;
|
|
113
146
|
}
|
|
114
147
|
if (bytes > 64 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
115
|
-
hash
|
|
148
|
+
hash?.update(text.slice(start, end)); yield;
|
|
116
149
|
}
|
|
117
|
-
|
|
150
|
+
const digest = hash ? hash.digest() : yield {parts: [text], byteLength: bytes};
|
|
151
|
+
return {utf8Bytes: bytes, sha256: digest, contentRoot: text.length + ':' +
|
|
118
152
|
(fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36)};
|
|
119
153
|
}
|
|
120
|
-
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
154
|
+
function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
|
|
121
155
|
if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
|
|
122
156
|
metadata = {...metadata};
|
|
123
157
|
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
124
158
|
const prepared = _rapierPrepareHistoryRecords(records, earlier);
|
|
125
159
|
if (!prepared.ok) throw fault('navigation target was not retained');
|
|
126
|
-
const headFacts = yield* sourceFacts(text);
|
|
160
|
+
const headFacts = yield* sourceFacts(text, native);
|
|
127
161
|
let bytes = headFacts.utf8Bytes;
|
|
128
162
|
const expanded = [];
|
|
129
163
|
for (const {record, splices: rows, metadata: effect} of prepared.rows) {
|
|
@@ -153,7 +187,7 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
153
187
|
const startRevision = expanded[0]?.transaction.baseRevision ?? revision, startRoot = expanded[0]?.beforeHash ?? root;
|
|
154
188
|
if (typeof documentAuthority !== 'string' || !documentAuthority || documentAuthority.length > 256) throw fault('unsupported or invalid envelope');
|
|
155
189
|
if (!nonnegative(startRevision) || !rootShape(startRoot)) throw fault('invalid start');
|
|
156
|
-
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText);
|
|
190
|
+
const startFacts = unchangedText ? headFacts : yield* sourceFacts(startText, native);
|
|
157
191
|
if (startRevision === 0 && startRoot !== startFacts.contentRoot) throw fault('invalid start');
|
|
158
192
|
const headSHA = headFacts.sha256, startSHA = startFacts.sha256;
|
|
159
193
|
const payload = {format: 'rapier-ledger/2', documentAuthority,
|
|
@@ -165,7 +199,10 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
|
|
|
165
199
|
}
|
|
166
200
|
const replayedMetadata = prepared.replayMetadata(startMetadata);
|
|
167
201
|
if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
|
|
168
|
-
const ledger = {...payload, sha256: yield* checksum(canonicalParts(payload))};
|
|
202
|
+
const ledger = {...payload, sha256: yield* checksum(canonicalParts(payload), native)};
|
|
203
|
+
proofs.set(ledger, {shell: canonicalJSON({...ledger, start: {...ledger.start, text: ''}}), startText,
|
|
204
|
+
text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
|
|
205
|
+
source: new HistorySource(text)});
|
|
169
206
|
return {ledger, contentRoot, utf8Bytes: headBytes};
|
|
170
207
|
}
|
|
171
208
|
function finish(producer) {
|
|
@@ -174,13 +211,14 @@ function finish(producer) {
|
|
|
174
211
|
return step.value;
|
|
175
212
|
}
|
|
176
213
|
export function exportLedger(input) { return finish(produceLedger(input)).ledger; }
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
let step;
|
|
214
|
+
async function finishAsync(producer, {yield: yieldWork, signal} = {}) {
|
|
215
|
+
let step, digest;
|
|
180
216
|
do {
|
|
181
217
|
signal?.throwIfAborted();
|
|
182
|
-
step = producer.next();
|
|
183
|
-
if (!step.done &&
|
|
218
|
+
step = producer.next(digest); digest = undefined;
|
|
219
|
+
if (!step.done && step.value) digest = await sha256PartsAsync(step.value.parts,
|
|
220
|
+
{yield: yieldWork, signal, byteLength: step.value.byteLength});
|
|
221
|
+
else if (!step.done && yieldWork) {
|
|
184
222
|
const pending = yieldWork();
|
|
185
223
|
if (pending) await pending;
|
|
186
224
|
}
|
|
@@ -188,8 +226,10 @@ export async function prepareLedgerAsync(input, {yield: yieldWork, signal} = {})
|
|
|
188
226
|
signal?.throwIfAborted();
|
|
189
227
|
return step.value;
|
|
190
228
|
}
|
|
191
|
-
export function
|
|
192
|
-
|
|
229
|
+
export function prepareLedgerAsync(input, options = {}) {
|
|
230
|
+
return finishAsync(produceLedger(input, !!globalThis.crypto?.subtle), options);
|
|
231
|
+
}
|
|
232
|
+
function historyNavigation(ledger, text, metadata, prepared, source) {
|
|
193
233
|
const {byId, effects: {pictures}} = prepared;
|
|
194
234
|
let branch = [], cursor = 0;
|
|
195
235
|
for (const {transaction: tx} of ledger.records) {
|
|
@@ -232,8 +272,13 @@ export function historyEnvelope(input, segmentIdentity = [], expectedText, expec
|
|
|
232
272
|
if (active === null || !activeMetadata || (i + 1 === cursor && (active.length !== text.length || !active.matches(0, text) || canonicalJSON(activeMetadata) !== canonicalJSON(metadata)))) throw fault('complete branch differs');
|
|
233
273
|
}
|
|
234
274
|
}
|
|
275
|
+
return {branch, cursor};
|
|
276
|
+
}
|
|
277
|
+
export function historyEnvelope(input, segmentIdentity = [], expectedText, expectedMetadata) {
|
|
278
|
+
const {value: {ledger, text, metadata}, prepared, source, proof} = readLedgerState(input, expectedText, expectedMetadata);
|
|
279
|
+
const {branch, cursor} = proof.navigation ??= historyNavigation(ledger, text, metadata, prepared, source);
|
|
235
280
|
return {schemaVersion: 5, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
|
|
236
|
-
ledger: ledger.records, branch, cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
281
|
+
ledger: ledger.records, branch: branch.slice(), cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
237
282
|
sourceRootId: ledger.head.root, earliestMetadata: {...ledger.start.metadata}, documentMetadata: {...metadata}, segmentIdentity, historyComplete: ledger.complete,
|
|
238
283
|
trimReason: ledger.complete ? '' : 'carried_start', trimmedBytes: 0};
|
|
239
284
|
}
|
|
@@ -249,15 +294,14 @@ export function agentActorId(door, agent) {
|
|
|
249
294
|
export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'rapier'}, operation = 'document.replace', at} = {}) {
|
|
250
295
|
const current = readLedger(input); documentText(text);
|
|
251
296
|
if (current.text === text) return current.ledger;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
const revision = current.revision + 1, root = rootAfter(current.root, splices[0]);
|
|
297
|
+
const splices = diff(current.text, text, {_unit: 'word'}).edits.map(edit =>
|
|
298
|
+
minimalHistorySplice(current.text.slice(...edit.before), text.slice(...edit.after), edit.before[0])).reverse();
|
|
299
|
+
const revision = current.revision + 1, root = splices.reduce(rootAfter, current.root);
|
|
300
|
+
if (actor.kind === 'agent' && current.metadata.docKind === 'markdown' && evaluateWill(current.text, splices).outcome !== 'applied')
|
|
301
|
+
throw fault('will_violation');
|
|
302
|
+
const identity = sha256(canonicalJSON({before: current.ledger.sha256, text, actor, operation, at: at ?? null}));
|
|
259
303
|
const record = {beforeHash: current.root, afterHash: root, splices, transaction: {
|
|
260
|
-
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' +
|
|
304
|
+
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' + identity,
|
|
261
305
|
documentAuthority: current.ledger.documentAuthority, baseRevision: current.revision, revision,
|
|
262
306
|
actor, transport: 'platform', operation, requestId: null, sourceTransactionId: null,
|
|
263
307
|
affectedBlockIds: [], parent: current.ledger.records.at(-1)?.transaction.id ?? null,
|
package/dist/kit/ledger/hash.mjs
CHANGED
|
@@ -111,9 +111,50 @@ export function sha256Parts(parts) {
|
|
|
111
111
|
for (const part of parts) hash.update(part);
|
|
112
112
|
return hash.digest();
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
export async function sha256PartsAsync(parts, {yield: yieldWork, signal, byteLength} = {}) {
|
|
115
|
+
const encoder = new TextEncoder(), chunks = [];
|
|
116
|
+
let bytes = byteLength === undefined ? null : new Uint8Array(byteLength);
|
|
117
|
+
let high = '', length = 0;
|
|
118
|
+
const write = text => {
|
|
119
|
+
if (bytes) {
|
|
120
|
+
const result = encoder.encodeInto(text, bytes.subarray(length));
|
|
121
|
+
if (result.read !== text.length) throw new RangeError('SHA-256 byte length differs');
|
|
122
|
+
length += result.written;
|
|
123
|
+
} else {
|
|
124
|
+
const chunk = encoder.encode(text); chunks.push(chunk); length += chunk.length;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
for (const value of parts) {
|
|
128
|
+
let text = high + String(value === undefined ? '' : value); high = '';
|
|
129
|
+
if (text.length && (text.charCodeAt(text.length - 1) & 0xfc00) === 0xd800) {
|
|
130
|
+
high = text.slice(-1); text = text.slice(0, -1);
|
|
131
|
+
}
|
|
132
|
+
for (let at = 0; at < text.length;) {
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
let end = Math.min(text.length, at + 32768);
|
|
135
|
+
if (end < text.length && (text.charCodeAt(end - 1) & 0xfc00) === 0xd800 &&
|
|
136
|
+
(text.charCodeAt(end) & 0xfc00) === 0xdc00) end--;
|
|
137
|
+
write(text.slice(at, end)); at = end;
|
|
138
|
+
const pending = yieldWork?.(); if (pending) await pending;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (high) write(high);
|
|
142
|
+
if (bytes && length !== bytes.length) throw new RangeError('SHA-256 byte length differs');
|
|
143
|
+
bytes ||= new Uint8Array(length);
|
|
144
|
+
let at = 0;
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
signal?.throwIfAborted(); bytes.set(chunk, at); at += chunk.length;
|
|
147
|
+
const pending = yieldWork?.(); if (pending) await pending;
|
|
148
|
+
}
|
|
149
|
+
chunks.length = 0;
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
152
|
+
signal?.throwIfAborted();
|
|
153
|
+
return Array.from(new Uint8Array(digest), value => value.toString(16).padStart(2, '0')).join('');
|
|
154
|
+
}
|
|
155
|
+
const LONG_TEXT = 65536, DOCUMENT_TEXT = 1048576, DIGESTS = new Map();
|
|
115
156
|
export function sha256(text) {
|
|
116
|
-
if (typeof text !== 'string' || text.length < LONG_TEXT) return createSHA256().update(text).digest();
|
|
157
|
+
if (typeof text !== 'string' || text.length < LONG_TEXT || text.length >= DOCUMENT_TEXT) return createSHA256().update(text).digest();
|
|
117
158
|
let digest = DIGESTS.get(text);
|
|
118
159
|
if (digest === undefined) {
|
|
119
160
|
digest = createSHA256().update(text).digest();
|