rapier-markdown-kit 1.2.3 → 1.2.5
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 +3 -2
- package/README.md +16 -4
- package/dist/images/gif.mjs +3 -1
- package/dist/images/playback.mjs +6 -4
- package/dist/kit/conformance/run.mjs +1 -1
- package/dist/kit/ledger/authored-origin.mjs +49 -0
- package/dist/kit/ledger/authorship.mjs +18 -9
- package/dist/kit/ledger/data.mjs +7 -4
- package/dist/kit/ledger/format.mjs +25 -8
- package/dist/kit/ledger/history.mjs +31 -5
- package/dist/kit/ledger/inverse-source.mjs +111 -49
- package/dist/kit/ledger/journal-records.mjs +24 -29
- package/dist/kit/ledger/merge.mjs +48 -13
- package/dist/kit/ledger/text-merge.mjs +75 -29
- package/dist/kit/ledger/transport.mjs +30 -45
- package/dist/kit/pdf-vectors.mjs +2 -1
- package/dist/kit/print-capitals.mjs +0 -1
- package/dist/kit/rapier-ledger.mjs +2 -0
- package/dist/kit/render-markdown.mjs +10 -1
- package/dist/kit/render-print.mjs +43 -7
- package/dist/kit/render-sanitize.mjs +19 -12
- package/dist/kit/render-styles.mjs +3 -3
- package/dist/kit/render.mjs +5 -40
- package/dist/layout/paragraph-fit.mjs +1 -15
- package/dist/layout/typography.mjs +2 -1
- package/dist/packages/rapier-diff/concerns.mjs +163 -0
- package/dist/packages/rapier-diff/diff.mjs +68 -0
- package/dist/packages/rapier-diff/index.mjs +4 -744
- package/dist/packages/rapier-diff/matching.mjs +157 -0
- package/dist/packages/rapier-diff/merge.mjs +226 -0
- package/dist/packages/rapier-diff/moves.mjs +130 -0
- package/dist/packages/rapier-diff/structure.mjs +50 -0
- package/dist/packages/rapier-diff/tokens.mjs +72 -0
- package/dist/packages/rapier-diff/walk.mjs +219 -0
- package/dist/spec/html-blocks.mjs +3 -3
- package/dist/spec/md-layout.mjs +1 -1
- package/dist/spec/md-marks.mjs +1 -1
- package/dist/spec/md-source.mjs +15 -7
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {fields, scalar, plainPrototype} from './data.mjs';
|
|
2
3
|
|
|
3
4
|
const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
|
|
4
5
|
const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
|
|
@@ -32,32 +33,15 @@ function _rapierTransformSplices(text, splices, inverse = false) {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function _rapierPlainLedgerData(record) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
45
|
-
if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null ||
|
|
46
|
-
Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) return false;
|
|
47
|
-
const keys = Reflect.ownKeys(value);
|
|
48
|
-
if (array && keys.length !== value.length + 1) return false;
|
|
49
|
-
active.add(value); pending.push([value, true]);
|
|
50
|
-
for (const key of keys) {
|
|
51
|
-
if (array && key === 'length') continue;
|
|
52
|
-
if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) ||
|
|
53
|
-
(array && (!Number.isSafeInteger(Number(key)) || Number(key) < 0 || Number(key) >= value.length || String(Number(key)) !== key))) return false;
|
|
54
|
-
const field = Object.getOwnPropertyDescriptor(value, key);
|
|
55
|
-
if (!field || !Object.hasOwn(field, 'value') || !field.enumerable) return false;
|
|
56
|
-
pending.push([field.value, false]);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
} catch (_) { return false; }
|
|
36
|
+
const active = new Set();
|
|
37
|
+
const walk = (value, depth) => {
|
|
38
|
+
if (scalar(value)) return;
|
|
39
|
+
const {descriptors, keys} = fields(value, depth, active, false);
|
|
40
|
+
active.add(value);
|
|
41
|
+
for (const key of keys) walk(descriptors[key].value, depth + 1);
|
|
42
|
+
active.delete(value);
|
|
43
|
+
};
|
|
44
|
+
try { walk(record, 0); return true; } catch (_) { return false; }
|
|
61
45
|
}
|
|
62
46
|
|
|
63
47
|
const _rapierMetadataValue = (key, value) => key === 'filename'
|
|
@@ -156,9 +140,16 @@ function _rapierRecordSplices(entry, ledger) {
|
|
|
156
140
|
}
|
|
157
141
|
|
|
158
142
|
// Source identifies a retained checkpoint even when host revision numbers differ.
|
|
143
|
+
// The History Slider compares now → earlier: that answer is the earlier → now walk, inverted.
|
|
159
144
|
function journalOperations(before, after, journal) {
|
|
160
145
|
if (typeof before !== 'string' || typeof after !== 'string' || !Array.isArray(journal)) return;
|
|
161
146
|
if (before === after) return [];
|
|
147
|
+
const forward = journalWalk(before, after, journal);
|
|
148
|
+
if (forward) return forward;
|
|
149
|
+
return journalWalk(after, before, journal)?.reverse().map(({pos, removed, inserted}) => ({pos, removed: inserted, inserted: removed}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function journalWalk(before, after, journal) {
|
|
162
153
|
try {
|
|
163
154
|
let text = after;
|
|
164
155
|
const lookup = _rapierRecordIndex(journal), groups = [];
|
|
@@ -480,8 +471,7 @@ const _rapierPrepareHistoryRecords = (() => {
|
|
|
480
471
|
// input later cannot retroactively certify an earlier view of its properties.
|
|
481
472
|
frame.frozen = frame.immutable = Object.isFrozen(value);
|
|
482
473
|
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
483
|
-
if (!array && proto
|
|
484
|
-
Object.getOwnPropertyDescriptor(Object.getOwnPropertyDescriptor(proto, 'constructor')?.value || {}, 'name')?.value !== 'Object')) throw Error('history_data');
|
|
474
|
+
if (!array && !plainPrototype(proto)) throw Error('history_data');
|
|
485
475
|
const descriptors = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(descriptors);
|
|
486
476
|
if (array && keys.length !== descriptors.length.value + 1) throw Error('history_data');
|
|
487
477
|
for (const key of keys) {
|
|
@@ -588,7 +578,12 @@ function _rapierCheckLedgerRecord(record, prior, documentAuthority, maxDocumentB
|
|
|
588
578
|
const own = Array.isArray(record.splices), navigation = !!(record.transaction?.reverts || record.transaction?.reapplies);
|
|
589
579
|
if ((!own && !navigation) || (own && record.splices.length > 64) ||
|
|
590
580
|
(record.transaction?.reverts && record.transaction?.reapplies)) return false;
|
|
591
|
-
|
|
581
|
+
const metadataChanged = Object.values(metadata || {}).some(pair => pair.before !== pair.after);
|
|
582
|
+
// An identical concurrent replacement keeps its authored rows and an empty
|
|
583
|
+
// physical splice. The origin proof, not this gate, shows the effect is already present.
|
|
584
|
+
const coincidentWitness = !splices?.length && record.beforeHash === record.afterHash &&
|
|
585
|
+
record.authored && _rapierValidAuthored(record.authored) && record.authored.splices.some(row => row.removed.length);
|
|
586
|
+
if (metadata === undefined || (!splices?.length && !metadataChanged && !semantic && !coincidentWitness) ||
|
|
592
587
|
(record.beforeHash === record.afterHash && splices?.length) ||
|
|
593
588
|
(record.beforeHash !== record.afterHash && !splices?.length) ||
|
|
594
589
|
((record.transaction?.sourceTransactionId || record.transaction?.sourceTransactionIds?.length || navigation) && !semantic)) return false;
|
|
@@ -3,7 +3,7 @@ import {readLedger, exportLedger, rootAfter} from './format.mjs';
|
|
|
3
3
|
import {_rapierTransformSplices, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState} from './journal-records.mjs';
|
|
4
4
|
import {mergeText} from './text-merge.mjs';
|
|
5
5
|
import {transposeAuthored, authoredPlacements} from './transport.mjs';
|
|
6
|
-
import {parseWill} from '../../agent/will.mjs';
|
|
6
|
+
import {parseWill, stripOneTerminator} from '../../agent/will.mjs';
|
|
7
7
|
import {sha256} from './hash.mjs';
|
|
8
8
|
import {canonicalJSON, fault} from './data.mjs';
|
|
9
9
|
// The same interval transport used by the agent kernel, now shared with the public ledger.
|
|
@@ -46,6 +46,16 @@ function actFingerprint(record, effects) {
|
|
|
46
46
|
metadata: references.length ? null : Object.fromEntries(Object.entries(record.metadata || {}).map(([key, pair]) => [key, pair.after]))}));
|
|
47
47
|
}
|
|
48
48
|
const mergeIdentity = (localDigest, incomingDigest) => 'merge:' + sha256(canonicalJSON([localDigest, incomingDigest]));
|
|
49
|
+
// One physical replacement can carry two authored acts when both removed the same
|
|
50
|
+
// units and wrote the same text. An empty list is the act that does not repeat it.
|
|
51
|
+
function coincidentEffect(origin, left, right) {
|
|
52
|
+
const authored = origin?.splices;
|
|
53
|
+
if (!authored?.some(row => row.removed.length)) return false;
|
|
54
|
+
const same = (rows, expected) => rows.length === expected.length && rows.every((row, index) =>
|
|
55
|
+
row.pos === expected[index].pos && row.removed === expected[index].removed && row.inserted === expected[index].inserted);
|
|
56
|
+
const lists = [left, right];
|
|
57
|
+
return lists.every(rows => !rows.length || same(rows, authored)) && lists.some(rows => same(rows, authored));
|
|
58
|
+
}
|
|
49
59
|
function checkpointList(read) {
|
|
50
60
|
const list = [{root: read.ledger.start.root, text: read.ledger.start.text, metadata: read.ledger.start.metadata,
|
|
51
61
|
revision: read.ledger.start.revision, identity: null, index: 0}], records = read.ledger.records;
|
|
@@ -58,10 +68,18 @@ function checkpointList(read) {
|
|
|
58
68
|
}
|
|
59
69
|
return list;
|
|
60
70
|
}
|
|
71
|
+
// Keep and append regions with their intents, and the kept text: what the other copy may not change unseen.
|
|
61
72
|
function kept(text) {
|
|
62
73
|
const will = parseWill(text);
|
|
63
74
|
if (will.fault) return {fault: will.fault, text};
|
|
64
|
-
return will.regions.filter(region => region.law
|
|
75
|
+
return will.regions.filter(region => region.law !== 'edit').map(({law, intent, start, end}) =>
|
|
76
|
+
law === 'keep' ? {law, intent, text: text.slice(start, end)} : {law, intent});
|
|
77
|
+
}
|
|
78
|
+
// Every append region of `from` still begins its counterpart in `to`.
|
|
79
|
+
function appended(from, to) {
|
|
80
|
+
const regions = text => parseWill(text).regions.filter(region => region.law === 'append');
|
|
81
|
+
const was = regions(from), now = regions(to), core = (text, region) => stripOneTerminator(text.slice(region.start, region.end));
|
|
82
|
+
return was.length === now.length && was.every((region, index) => core(to, now[index]).startsWith(core(from, region)));
|
|
65
83
|
}
|
|
66
84
|
// A retained suffix can reuse a prefix only when an existing ledger proves its
|
|
67
85
|
// exact source, metadata, revision and path root. Export then rechecks the whole
|
|
@@ -104,7 +122,11 @@ export function merge(first, second) {
|
|
|
104
122
|
conflicts: [{kind: 'identity', code: 'act_order_conflict', actId: record.transaction.id}],
|
|
105
123
|
review: {required: true, base: ours.text, proposed: theirs.text}};
|
|
106
124
|
if (local && (canonicalJSON(local.splices) !== canonicalJSON(record.splices) ||
|
|
107
|
-
canonicalJSON(local.metadata || {}) !== canonicalJSON(record.metadata || {})))
|
|
125
|
+
canonicalJSON(local.metadata || {}) !== canonicalJSON(record.metadata || {}))) {
|
|
126
|
+
const metadataSame = canonicalJSON(local.metadata || {}) === canonicalJSON(record.metadata || {});
|
|
127
|
+
if (!(metadataSame && coincidentEffect(localOrigins.get(local.transaction.id), local.splices, record.splices)))
|
|
128
|
+
unproved.add(record.transaction.id);
|
|
129
|
+
}
|
|
108
130
|
}
|
|
109
131
|
const a = checkpointList(ours), b = checkpointList(theirs);
|
|
110
132
|
// A synthetic conflict act has one identity derived from both original envelopes.
|
|
@@ -192,7 +214,8 @@ export function merge(first, second) {
|
|
|
192
214
|
}
|
|
193
215
|
// Kept text arriving from the other copy waits for the person; this copy's own kept edits merge like any other.
|
|
194
216
|
const incomingKept = canonicalJSON(kept(theirs.text));
|
|
195
|
-
const keepDiffers = incomingKept !== canonicalJSON(kept(ours.text)) && incomingKept !== canonicalJSON(kept(common.text))
|
|
217
|
+
const keepDiffers = incomingKept !== canonicalJSON(kept(ours.text)) && incomingKept !== canonicalJSON(kept(common.text)) ||
|
|
218
|
+
!appended(common.text, theirs.text) && !appended(ours.text, theirs.text);
|
|
196
219
|
let conflicted = keepDiffers && unseen.length > 0, incomingSource = common.text;
|
|
197
220
|
if (!conflicted) for (const record of incoming) {
|
|
198
221
|
const beforeSource = incomingSource;
|
|
@@ -239,19 +262,31 @@ export function merge(first, second) {
|
|
|
239
262
|
return settled(theirs);
|
|
240
263
|
}
|
|
241
264
|
}
|
|
242
|
-
let conflicts = [];
|
|
243
265
|
if (conflicted) {
|
|
244
266
|
if (unseen.length !== incoming.length) return refused('history', 'record_transport_conflict');
|
|
245
267
|
if (incomingFields.size) return metadataConflict('record_transport_conflict', {fields: [...incomingFields]});
|
|
246
|
-
//
|
|
247
|
-
|
|
268
|
+
// A display of both alternatives is not an accepted history. The caller's envelope
|
|
269
|
+
// stays whole; neither original act is replaced by a synthetic copy.
|
|
248
270
|
const merged = mergeText(keepDiffers ? null : common.text, ours.text, theirs.text, {oursId: 'copy-' + ours.ledger.sha256.slice(0, 16), theirsId: 'copy-' + theirs.ledger.sha256.slice(0, 16)});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
if (!merged.conflicts.length) return refused('history', 'record_transport_conflict');
|
|
272
|
+
return {text: ours.text, metadata: ours.metadata, ledger: ours.ledger, clean: false,
|
|
273
|
+
conflicts: merged.conflicts.map(row => ({...row, code: 'text_overlap_unpreserved'})),
|
|
274
|
+
review: {required: true, base: ours.text, proposed: theirs.text}};
|
|
275
|
+
}
|
|
276
|
+
let conflicts = [];
|
|
277
|
+
// The act that keeps the physical splice needs the same authored origin as its
|
|
278
|
+
// witness. Copy it onto this result only; the caller's record stays untouched.
|
|
279
|
+
if (records.some(row => !row.splices.length && row.authored?.splices?.some(splice => splice.removed.length))) {
|
|
280
|
+
const origins = authoredPlacements(ours.ledger.start.text, records);
|
|
281
|
+
for (let index = 0; index < records.length; index++) {
|
|
282
|
+
const record = records[index];
|
|
283
|
+
if (record.authored) continue;
|
|
284
|
+
const origin = origins.get(record.transaction.id);
|
|
285
|
+
if (!origin?.splices.some(splice => splice.removed.length)) continue;
|
|
286
|
+
const key = canonicalJSON(origin);
|
|
287
|
+
if (records.some(other => other.transaction.id !== record.transaction.id && canonicalJSON(origins.get(other.transaction.id)) === key))
|
|
288
|
+
records[index] = {...record, authored: origin};
|
|
289
|
+
}
|
|
255
290
|
}
|
|
256
291
|
const ledger = exportLedger({text, metadata, records, documentAuthority: ours.ledger.documentAuthority, root,
|
|
257
292
|
revision: records.at(-1)?.transaction.revision ?? ours.revision, complete: ours.complete});
|
|
@@ -151,39 +151,81 @@ function textUnits(text) {
|
|
|
151
151
|
return out;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
154
|
+
const TEXT_WINDOW_CELLS = 1000000, TEXT_CELLS = 4000000;
|
|
155
|
+
// One window's authored matches, or false when two optimal alignments disagree on any of them.
|
|
156
|
+
function textWindowMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
157
|
+
const n = b1 - b0, m = s1 - s0, cells = (n + 1) * (m + 1);
|
|
158
|
+
if (cells > TEXT_WINDOW_CELLS || (budget.cells -= cells) < 0) return false;
|
|
159
|
+
// Only authored units score; a blank line matches only beside a matched neighbour.
|
|
160
|
+
const width = m + 1, score = new Uint32Array(cells), content = i => !textSpace(base[b0 + i]), same = (i, j) => base[b0 + i] === side[s0 + j];
|
|
158
161
|
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) {
|
|
159
162
|
const p = i * width + j;
|
|
160
|
-
score[p] =
|
|
163
|
+
score[p] = content(i) && same(i, j) ? score[p + width + 1] + 1 : Math.max(score[p + width], score[p + 1]);
|
|
164
|
+
}
|
|
165
|
+
const meaningful = [];
|
|
166
|
+
for (let i = 0, j = 0; i < n && j < m;) {
|
|
167
|
+
const p = i * width + j; if (!score[p]) break;
|
|
168
|
+
if (content(i) && same(i, j) && score[p] === score[p + width + 1] + 1) meaningful.push([i++, j++]);
|
|
169
|
+
else if (score[p + width] >= score[p + 1]) i++; else j++;
|
|
161
170
|
}
|
|
162
|
-
const walk = () => {
|
|
163
|
-
const pairs = []; let i = 0, j = 0;
|
|
164
|
-
while (i < n && j < m) {
|
|
165
|
-
const value = score[i * width + j]; if (!value) break;
|
|
166
|
-
if (base[i] === side[j]) { pairs.push([i++, j++]); continue; }
|
|
167
|
-
if (score[(i + 1) * width + j] >= score[i * width + j + 1]) i++; else j++;
|
|
168
|
-
}
|
|
169
|
-
return pairs;
|
|
170
|
-
};
|
|
171
|
-
const pairs = walk(), meaningful = pairs.filter(([i]) => !textSpace(base[i]));
|
|
172
171
|
// Every optimal path must agree on every authored anchor, not just two greedy walks.
|
|
173
|
-
const seen = new Uint32Array(
|
|
172
|
+
const seen = new Uint32Array(cells); seen.fill(0xffffffff); seen[0] = 0;
|
|
174
173
|
for (let i = 0; i <= n; i++) for (let j = 0; j <= m; j++) {
|
|
175
174
|
const p = i * width + j, at = seen[p]; if (at === 0xffffffff) continue;
|
|
176
175
|
const visit = (next, count) => { if (seen[next] !== 0xffffffff && seen[next] !== count) return false; seen[next] = count; return true; };
|
|
177
|
-
if (i < n && score[p + width] === score[p] && !visit(p + width, at)) return
|
|
178
|
-
if (j < m && score[p + 1] === score[p] && !visit(p + 1, at)) return
|
|
179
|
-
if (i < n && j < m &&
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
if (!visit(p + width + 1, at + (content ? 1 : 0))) return null;
|
|
176
|
+
if (i < n && score[p + width] === score[p] && !visit(p + width, at)) return false;
|
|
177
|
+
if (j < m && score[p + 1] === score[p] && !visit(p + 1, at)) return false;
|
|
178
|
+
if (i < n && j < m && content(i) && same(i, j) && score[p] === score[p + width + 1] + 1) {
|
|
179
|
+
if (meaningful[at]?.[0] !== i || meaningful[at]?.[1] !== j) return false;
|
|
180
|
+
if (!visit(p + width + 1, at + 1)) return false;
|
|
183
181
|
}
|
|
184
182
|
}
|
|
185
|
-
if (seen[n * width + m] !== meaningful.length) return
|
|
186
|
-
const
|
|
183
|
+
if (seen[n * width + m] !== meaningful.length) return false;
|
|
184
|
+
for (const [i, j] of meaningful) out.push([b0 + i, s0 + j]);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// A window too large to prove whole splits only at authored units found once on each side of it:
|
|
189
|
+
// such a unit names its own occurrence. The strict rule above still decides every window between them.
|
|
190
|
+
function textMatches(base, side, b0, b1, s0, s1, out, budget) {
|
|
191
|
+
if ((b1 - b0 + 1) * (s1 - s0 + 1) <= TEXT_WINDOW_CELLS) return textWindowMatches(base, side, b0, b1, s0, s1, out, budget);
|
|
192
|
+
const once = (units, from, to) => {
|
|
193
|
+
const found = new Map();
|
|
194
|
+
for (let k = from; k < to; k++) if (!textSpace(units[k])) found.set(units[k], found.has(units[k]) ? -1 : k);
|
|
195
|
+
return found;
|
|
196
|
+
};
|
|
197
|
+
const inBase = once(base, b0, b1), pairs = [];
|
|
198
|
+
for (const [unit, j] of once(side, s0, s1)) { const i = inBase.get(unit); if (j >= 0 && i >= 0) pairs.push([i, j]); }
|
|
199
|
+
// The longest run of them in the same order on both sides.
|
|
200
|
+
const tails = [], prev = new Int32Array(pairs.length);
|
|
201
|
+
for (let k = 0; k < pairs.length; k++) {
|
|
202
|
+
let lo = 0, hi = tails.length;
|
|
203
|
+
while (lo < hi) { const mid = (lo + hi) >>> 1; if (pairs[tails[mid]][0] < pairs[k][0]) lo = mid + 1; else hi = mid; }
|
|
204
|
+
prev[k] = lo ? tails[lo - 1] : -1; tails[lo] = k;
|
|
205
|
+
}
|
|
206
|
+
if (!tails.length) return false;
|
|
207
|
+
const chain = [];
|
|
208
|
+
for (let k = tails[tails.length - 1]; k >= 0; k = prev[k]) chain.push(pairs[k]);
|
|
209
|
+
let i = b0, j = s0;
|
|
210
|
+
for (const [bi, sj] of chain.reverse()) {
|
|
211
|
+
if (!textMatches(base, side, i, bi, j, sj, out, budget)) return false;
|
|
212
|
+
out.push([bi, sj]); i = bi + 1; j = sj + 1;
|
|
213
|
+
}
|
|
214
|
+
return textMatches(base, side, i, b1, j, s1, out, budget);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function textEdits(base, side) {
|
|
218
|
+
const n = base.length, m = side.length, meaningful = [];
|
|
219
|
+
if (!textMatches(base, side, 0, n, 0, m, meaningful, {cells: TEXT_CELLS})) return null;
|
|
220
|
+
const pairs = [];
|
|
221
|
+
let i = 0, j = 0;
|
|
222
|
+
for (let k = 0; k < meaningful.length; k++) {
|
|
223
|
+
const [bi, sj] = meaningful[k], [ni, nj] = meaningful[k + 1] || [n, m];
|
|
224
|
+
if (bi > i && sj > j && textSpace(base[bi - 1]) && base[bi - 1] === side[sj - 1]) pairs.push([bi - 1, sj - 1]);
|
|
225
|
+
pairs.push([bi, sj]); i = bi + 1; j = sj + 1;
|
|
226
|
+
if (i < ni && j < nj && textSpace(base[i]) && base[i] === side[j]) pairs.push([i++, j++]);
|
|
227
|
+
}
|
|
228
|
+
const edits = []; i = 0; j = 0;
|
|
187
229
|
for (const [bi, sj] of [...pairs, [n, m]]) {
|
|
188
230
|
if (i !== bi || j !== sj) edits.push({start: i, end: bi, text: side.slice(j, sj).join('')});
|
|
189
231
|
i = bi + 1; j = sj + 1;
|
|
@@ -234,11 +276,13 @@ function textEnvelope(id, variants) {
|
|
|
234
276
|
return out + '<!-- /note-conflict:v1 -->\n';
|
|
235
277
|
}
|
|
236
278
|
|
|
237
|
-
|
|
238
|
-
|
|
279
|
+
// Takes the text's units, scanned once by the caller; a text without conflict envelopes is not scanned again.
|
|
280
|
+
function textDeclarations(units) {
|
|
281
|
+
const kept = units.filter(unit => !textEnvelopeAt(unit)).map(unit =>
|
|
282
|
+
textFence(unit.split(/\r\n|\r|\n/, 1)[0].replace(/^\ufeff/, '')) ? '\n' : unit), text = kept.join('');
|
|
239
283
|
// A definition can move a destination/title onto the next unindented line, or span its label.
|
|
240
284
|
const definitions = text.match(/^\ufeff? {0,3}\[(?:\\.|[^\]\\])+\]:[^\r\n]*(?:(?:\r\n|\r|\n)(?![ ]*(?:\r\n|\r|\n|$))[^\r\n]*)*/gm) || [];
|
|
241
|
-
const continuations = textUnits(text).filter(unit => /^\ufeff? {0,3}\[\^/.test(unit));
|
|
285
|
+
const continuations = (kept.length === units.length ? kept : textUnits(text)).filter(unit => /^\ufeff? {0,3}\[\^/.test(unit));
|
|
242
286
|
return [...definitions, ...continuations, ...(text.match(/<!--[\s\S]*?(?:-->|$)/g) || [])].join('\0');
|
|
243
287
|
}
|
|
244
288
|
|
|
@@ -284,10 +328,12 @@ export function mergeText(base, ours, theirs, options = {}) {
|
|
|
284
328
|
conflicts.push({kind, base: before, variants, start, end: start + block.length, block});
|
|
285
329
|
text += block;
|
|
286
330
|
};
|
|
331
|
+
const [units, oursUnits, theirsUnits] = base === null ? [] : [base, ours, theirs].map(textUnits);
|
|
332
|
+
const declared = base !== null && textDeclarations(units);
|
|
287
333
|
if (base === null) conflict(null, ours, theirs, 'no-ancestor');
|
|
288
|
-
else if (textDeclarations(
|
|
334
|
+
else if (textDeclarations(oursUnits) !== declared || textDeclarations(theirsUnits) !== declared) conflict(base, ours, theirs, 'document-bindings');
|
|
289
335
|
else {
|
|
290
|
-
const
|
|
336
|
+
const a = textEdits(units, oursUnits), b = textEdits(units, theirsUnits);
|
|
291
337
|
if (!a || !b) conflict(base, ours, theirs, 'ambiguous-alignment-or-budget');
|
|
292
338
|
else {
|
|
293
339
|
const changes = [...a.map(e => ({...e, side: 0})), ...b.map(e => ({...e, side: 1}))].sort((x, y) => x.start - y.start || x.end - y.end || x.side - y.side);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Paired source placement, shared by live editing and canonical-history proof.
|
|
3
3
|
import {_rapierTransformSplices, _rapierValidAuthored, _rapierHistoryEffects} from './journal-records.mjs';
|
|
4
4
|
import {projectInverseSource} from './inverse-source.mjs';
|
|
5
|
+
import {authoredBasis, authoredPlacement, authoredPlacements} from './authored-origin.mjs';
|
|
5
6
|
import {sha256} from './hash.mjs';
|
|
6
7
|
import {canonicalJSON} from './data.mjs';
|
|
7
8
|
|
|
@@ -74,6 +75,18 @@ export function transposeRecord(base, splices, id, records) {
|
|
|
74
75
|
if (next === null) throw new RangeError('canonical_transport_invalid');
|
|
75
76
|
current = next;
|
|
76
77
|
}
|
|
78
|
+
// A concurrent act that already removed these same authored units and wrote the
|
|
79
|
+
// same replacement is one physical edit. This act stays in history with no second
|
|
80
|
+
// deletion. Pure insertions are never collapsed: each one is its own unit. An
|
|
81
|
+
// inverse of that act means the replacement is not still in force.
|
|
82
|
+
const sameRows = (left, right) => left.length === right.length && left.every((row, index) =>
|
|
83
|
+
row.pos === right[index].pos && row.removed === right[index].removed && row.inserted === right[index].inserted);
|
|
84
|
+
const matched = crossed.filter(record => sameRows(record.splices, splices));
|
|
85
|
+
const targetsOf = record => record.transaction.sourceTransactionIds || (record.transaction.sourceTransactionId ? [record.transaction.sourceTransactionId] : []);
|
|
86
|
+
const withdrawn = matched.some(record => crossed.some(other => targetsOf(other).includes(record.transaction.id) ||
|
|
87
|
+
other.transaction.reverts === record.transaction.id || other.transaction.reapplies === record.transaction.id));
|
|
88
|
+
if (splices.some(row => row.removed.length) && matched.length && !withdrawn)
|
|
89
|
+
return {splices: [], sourceIndices: [], against: crossed, text: current};
|
|
77
90
|
const moved = [], sourceIndices = [];
|
|
78
91
|
for (const [sourceIndex, row] of splices.entries()) {
|
|
79
92
|
const submitted = _rapierTransformSplices(source, [row]);
|
|
@@ -120,49 +133,7 @@ export function transposeSource(base, splices, client, log) {
|
|
|
120
133
|
return transposeRecord(base, splices, client, records);
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
// and concurrent predecessors remain separate until an act observes both.
|
|
125
|
-
function authoredFrontier(records, visit = null) {
|
|
126
|
-
const dependencies = new Map(), frontier = new Set();
|
|
127
|
-
for (const record of records) {
|
|
128
|
-
const id = record.transaction.id, basis = record.authored?.basis;
|
|
129
|
-
if (visit) visit(record, basis ?? [...frontier].sort());
|
|
130
|
-
if (!basis) { dependencies.set(id, [...frontier]); frontier.clear(); }
|
|
131
|
-
else {
|
|
132
|
-
const pending = [...basis], seen = new Set();
|
|
133
|
-
while (pending.length) {
|
|
134
|
-
const prior = pending.pop();
|
|
135
|
-
if (seen.has(prior)) continue;
|
|
136
|
-
if (!dependencies.has(prior)) throw new RangeError('canonical_origin_invalid');
|
|
137
|
-
seen.add(prior); frontier.delete(prior); pending.push(...dependencies.get(prior));
|
|
138
|
-
}
|
|
139
|
-
dependencies.set(id, basis);
|
|
140
|
-
}
|
|
141
|
-
frontier.add(id);
|
|
142
|
-
}
|
|
143
|
-
return [...frontier].sort();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function authoredBasis(records) { return authoredFrontier(records); }
|
|
147
|
-
|
|
148
|
-
export function authoredPlacements(base, records) {
|
|
149
|
-
const origins = new Map(); let source = base;
|
|
150
|
-
authoredFrontier(records, (record, basis) => {
|
|
151
|
-
origins.set(record.transaction.id, record.authored ?? authoredPlacement(source, record.splices, basis));
|
|
152
|
-
source = _rapierTransformSplices(source, record.splices);
|
|
153
|
-
if (source === null) throw new RangeError('canonical_origin_invalid');
|
|
154
|
-
});
|
|
155
|
-
return origins;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Immutable authored placement stays on its own act. Basis IDs refer only to this
|
|
159
|
-
// canonical history; no source snapshot or duplicate history is stored.
|
|
160
|
-
export function authoredPlacement(source, splices, basis) {
|
|
161
|
-
const origin = {basis: basis.slice().sort(), source: sha256(source), splices: splices.map(row => ({...row}))};
|
|
162
|
-
if (!_rapierValidAuthored(origin) || _rapierTransformSplices(source, splices) === null)
|
|
163
|
-
throw new RangeError('canonical_origin_invalid');
|
|
164
|
-
return origin;
|
|
165
|
-
}
|
|
136
|
+
export {authoredBasis, authoredPlacement, authoredPlacements};
|
|
166
137
|
|
|
167
138
|
// Reconstruct an authored view from the same records, then prove its exact physical
|
|
168
139
|
// placement. Equality of resulting text alone cannot prove a repeated-text anchor.
|
|
@@ -233,6 +204,19 @@ function authoredReplay(base, records, append = null) {
|
|
|
233
204
|
if (source === null) throw new RangeError('canonical_origin_invalid');
|
|
234
205
|
}
|
|
235
206
|
}
|
|
207
|
+
const coverKey = origin => origin?.splices.some(row => row.removed.length)
|
|
208
|
+
? canonicalJSON({basis: origin.basis, splices: origin.splices}) : null;
|
|
209
|
+
const coverCounts = new Map();
|
|
210
|
+
for (const [id, origin] of origins) {
|
|
211
|
+
if (effects.references.get(id)?.length) continue;
|
|
212
|
+
const key = coverKey(origin);
|
|
213
|
+
if (key) coverCounts.set(key, (coverCounts.get(key) || 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
// Two inverses can name different acts and still withdraw one physical replacement.
|
|
216
|
+
const sharesCover = id => (effects.references.get(id) || []).some(ref => {
|
|
217
|
+
const key = coverKey(origins.get(ref));
|
|
218
|
+
return !!key && coverCounts.get(key) > 1;
|
|
219
|
+
});
|
|
236
220
|
const ordered = (ids, selected = null) => {
|
|
237
221
|
const order = [], known = new Set(ids), waiting = new Map(), dependents = new Map(), ready = [];
|
|
238
222
|
const rank = id => selected ? Number(selected.has(id)) : Number(origins.get(id).splices.some(row => row.removed.length));
|
|
@@ -351,10 +335,11 @@ function authoredReplay(base, records, append = null) {
|
|
|
351
335
|
if (aligned.text !== text) throw new RangeError('canonical_origin_invalid');
|
|
352
336
|
const crossed = aligned.records.slice(basisOrder.length);
|
|
353
337
|
let moved;
|
|
354
|
-
if (
|
|
338
|
+
if (effects.references.get(id)?.length && (sharedInverses || sharesCover(id))) {
|
|
355
339
|
// Each inverse retains its own targets. First prove the exact intention
|
|
356
340
|
// in its causal source, then place that choice in the accepted effect
|
|
357
|
-
// graph. Another inverse may already suppress the same original bytes
|
|
341
|
+
// graph. Another inverse may already suppress the same original bytes,
|
|
342
|
+
// including when the two inverses name different authors of one replacement.
|
|
358
343
|
const authored = projectInverse(basis.text, basis.records, id);
|
|
359
344
|
if (!same(origin.splices, authored.splices) ||
|
|
360
345
|
_rapierTransformSplices(basis.text, origin.splices) !== authored.after)
|
package/dist/kit/pdf-vectors.mjs
CHANGED
|
@@ -132,7 +132,8 @@ export async function collectPdfVectors(main, {width, height, count, capture, si
|
|
|
132
132
|
root = document.importNode(parsed.documentElement, true);
|
|
133
133
|
root.style.setProperty('width', originalBox.width + 'px', 'important'); root.style.setProperty('height', originalBox.height + 'px', 'important');
|
|
134
134
|
mount = document.createElement('div'); mount.style.cssText = `position:fixed;left:${originalBox.left}px;top:${originalBox.top}px;width:${originalBox.width}px;height:${originalBox.height}px;pointer-events:none`;
|
|
135
|
-
|
|
135
|
+
// A shadow root keeps the picture's own <style> on the picture, as an <img> does.
|
|
136
|
+
mount.attachShadow({mode: 'open'}).append(root); document.body.append(mount); temporary.push(mount); await document.fonts.ready;
|
|
136
137
|
textRoots.push(root);
|
|
137
138
|
}
|
|
138
139
|
const box = root.getBoundingClientRect(), nodes = [root, ...root.querySelectorAll('*')], sequence = [];
|
|
@@ -82,4 +82,3 @@ export function createPrintCapitals(lineMetrics, linesHeight, followingProse) {
|
|
|
82
82
|
|
|
83
83
|
const capitals = createPrintCapitals(lineMetrics, linesHeight, followingPrintProse);
|
|
84
84
|
export const applyPrintCapitals = capitals.apply;
|
|
85
|
-
export const clearPrintCapitals = capitals.clear;
|
|
@@ -10,6 +10,8 @@ export {_rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord,
|
|
|
10
10
|
import {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
|
|
11
11
|
export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices};
|
|
12
12
|
|
|
13
|
+
import {sha256State} from './ledger/hash.mjs';
|
|
14
|
+
export {sha256State};
|
|
13
15
|
import {transposeSource} from './ledger/transport.mjs';
|
|
14
16
|
export {transposeSource};
|
|
15
17
|
|
|
@@ -1716,6 +1716,15 @@ function* _rapierResolveSoftBreakTokensSteps(root) {
|
|
|
1716
1716
|
return root;
|
|
1717
1717
|
}
|
|
1718
1718
|
|
|
1719
|
+
// Markup parsed in a document of its own, which runs no script and loads nothing (Copy's clipboard tree): one element per
|
|
1720
|
+
// markup, or no boxes where the joined parse did not keep them apart.
|
|
1721
|
+
function _rapierInertParse(markups) {
|
|
1722
|
+
const doc = new DOMParser().parseFromString(markups.map((html, at) => '<div data-rapier-batch="' + at + '">' + html + '</div>').join(''), 'text/html');
|
|
1723
|
+
const boxes = [...doc.body.childNodes];
|
|
1724
|
+
const intact = boxes.length === markups.length && boxes.every((box, at) => box.getAttribute?.('data-rapier-batch') === String(at));
|
|
1725
|
+
return {doc, boxes: intact ? boxes : null};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1719
1728
|
function _rapierUnwrapElement(element) {
|
|
1720
1729
|
while (element.firstChild) element.parentNode.insertBefore(element.firstChild, element);
|
|
1721
1730
|
element.remove();
|
|
@@ -1742,7 +1751,7 @@ function renderWithMathML(source, metadata = {}) {
|
|
|
1742
1751
|
return {html, equations};
|
|
1743
1752
|
}
|
|
1744
1753
|
|
|
1745
|
-
return {renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
|
|
1754
|
+
return {renderWithMathML, _rapierClipboardPayloadAsync, _rapierPlainTextFileAsync, _rapierCloneRootAsync, _rapierPortablePlainTextAsync, _rapierHeldPicturesAsWordsAsync, _rapierRenderSemanticRootAsync, _rapierProjectPortableRootAsync, _rapierPortableHtmlAsync, _rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierInertParse, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierApplyImageGeometry, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
|
|
1746
1755
|
}
|
|
1747
1756
|
export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
|
|
1748
1757
|
|
|
@@ -75,7 +75,7 @@ function _rapierPrintWillFont(markers) {
|
|
|
75
75
|
const bounds = [0, -400, 10, 0];
|
|
76
76
|
const outline = cat(u16(1), bounds.map(u16), u16(2), u16(0), [1, 1, 1], u16(0), u16(10), u16(-5), u16(bounds[1]), u16(bounds[3] - bounds[1]), u16((bounds[1] - bounds[3]) / 2), [0]);
|
|
77
77
|
const utf16 = text => Array.from(text, ch => u16(ch.charCodeAt(0)));
|
|
78
|
-
const names = [[1, '
|
|
78
|
+
const names = [[1, 'RapierWillCarrier'], [2, 'Regular'], [4, 'RapierWillCarrier Regular'], [6, 'RapierWillCarrier-Regular']];
|
|
79
79
|
let offset = 0;
|
|
80
80
|
const tables = {
|
|
81
81
|
'OS/2': cat(u16(4), u16(advance), u16(400), u16(5), u16(0), u16(650), u16(600), u16(0), u16(75), u16(650), u16(600), u16(0), u16(350),
|
|
@@ -449,7 +449,17 @@ async function _rapierPrintTextPages(main, pageWidth, count, check, origin = mai
|
|
|
449
449
|
prev = run;
|
|
450
450
|
}
|
|
451
451
|
}
|
|
452
|
-
|
|
452
|
+
// Each run carries the tags of its block's structure (outermost first): a PDF reader finds the document's headings, lists and
|
|
453
|
+
// tables from them rather than from positions.
|
|
454
|
+
const ROLES = {TABLE: 'Table', TR: 'TR', TD: 'TD', TH: 'TH', UL: 'L', OL: 'L', LI: 'LI', BLOCKQUOTE: 'BlockQuote', P: 'P', PRE: 'Code', FIGCAPTION: 'Caption',
|
|
455
|
+
H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6'};
|
|
456
|
+
const tags = block => {
|
|
457
|
+
const chain = [];
|
|
458
|
+
for (let node = block; node && node !== main; node = node.parentElement)
|
|
459
|
+
if (ROLES[node.tagName] || node === block && node.matches('.rapier-will-carrier')) chain.unshift({node, role: ROLES[node.tagName] || 'P'});
|
|
460
|
+
return chain;
|
|
461
|
+
};
|
|
462
|
+
return pages.map(page => page.map(({block, ...run}) => ({...run, tags: tags(block)})));
|
|
453
463
|
}
|
|
454
464
|
|
|
455
465
|
function _rapierPrintLinkPages(main, width, height, count) {
|
|
@@ -542,6 +552,9 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
542
552
|
const descriptor = add('<< /Type /FontDescriptor /FontName /RapierText /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
543
553
|
const descendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierText /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + descriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
544
554
|
const type = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierText /Encoding /Identity-H /DescendantFonts [' + descendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
555
|
+
const carrierDescriptor = add('<< /Type /FontDescriptor /FontName /RapierWillCarrier /Flags 4 /FontBBox [' + font.bounds.join(' ') + '] /ItalicAngle 0 /Ascent 500 /Descent -500 /CapHeight 500 /StemV 80 /FontFile2 ' + fontFile + ' 0 R >>');
|
|
556
|
+
const carrierDescendant = add('<< /Type /Font /Subtype /CIDFontType2 /BaseFont /RapierWillCarrier /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> /FontDescriptor ' + carrierDescriptor + ' 0 R /DW 500 /CIDToGIDMap /Identity >>');
|
|
557
|
+
const carrierType = add('<< /Type /Font /Subtype /Type0 /BaseFont /RapierWillCarrier /Encoding /Identity-H /DescendantFonts [' + carrierDescendant + ' 0 R] /ToUnicode ' + mapping + ' 0 R >>');
|
|
545
558
|
const numberFont = pageNumbers ? add('<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>') : null;
|
|
546
559
|
const kids = pages.map(() => { objects.push(null); return objects.length - 1; }), decimal = value => Number(value.toFixed(4));
|
|
547
560
|
const nativeFonts = new Map();
|
|
@@ -571,12 +584,26 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
571
584
|
nativeResources.push(entry.name + ' ' + type + ' 0 R');
|
|
572
585
|
}
|
|
573
586
|
const sharedImages = new Map();
|
|
587
|
+
// The structure tree (PDF 1.7, 14.7): one element per tagged DOM element, each run marked with its page's next MCID.
|
|
588
|
+
const structs = new Map(), documentElement = {role: 'Document', kids: []}, parentTree = [];
|
|
589
|
+
const structOf = tags => {
|
|
590
|
+
let parent = documentElement;
|
|
591
|
+
for (const {node, role} of tags) {
|
|
592
|
+
let element = structs.get(node);
|
|
593
|
+
if (!element) {element = {role, kids: [], parent}; structs.set(node, element); parent.kids.push(element);}
|
|
594
|
+
parent = element;
|
|
595
|
+
}
|
|
596
|
+
return parent === documentElement ? null : parent;
|
|
597
|
+
};
|
|
574
598
|
for (let index = 0; index < pages.length; index++) {
|
|
575
599
|
check();
|
|
600
|
+
const marked = [];
|
|
601
|
+
parentTree.push(marked);
|
|
576
602
|
const held = pages[index], flate = !!held?.rgb;
|
|
577
603
|
const picture = stream('/Type /XObject /Subtype /Image /Width ' + pixelWidth + ' /Height ' + pixelHeight + ' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (flate ? 'FlateDecode' : 'DCTDecode'), flate ? held.rgb : held);
|
|
578
604
|
pages[index] = null;
|
|
579
|
-
|
|
605
|
+
// The page's picture is how its tagged text looks, not a figure of its own: an artifact.
|
|
606
|
+
let commands = '/Artifact BMC\nq\n' + [decimal(pixelWidth * .75), 0, 0, decimal(pixelHeight * .75), decimal(margins.left), decimal(height - margins.top - pixelHeight * .75)].join(' ') + ' cm\n/Im0 Do\nQ\nEMC\n';
|
|
580
607
|
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
581
608
|
for (const drawing of vectorPages[index] || []) {
|
|
582
609
|
commands += `q\n.75 0 0 -.75 ${decimal(margins.left)} ${decimal(height - margins.top)} cm\n0 0 ${pixelWidth} ${pixelHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
@@ -609,6 +636,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
609
636
|
// Keep one logical Unicode text object, resetting its font before each scalar's direction
|
|
610
637
|
// can change. Readers that ignore ActualText otherwise discard earlier words in mixed RTL/LTR.
|
|
611
638
|
// Express width in the font size: some readers ignore horizontal text scaling when joining.
|
|
639
|
+
const element = structOf(run.tags || []);
|
|
640
|
+
if (element) {element.kids.push({page: index, mcid: marked.length}); commands += `/${element.role} << /MCID ${marked.length} >> BDC\n`; marked.push(element);}
|
|
612
641
|
commands += `q\n/Span << /ActualText <feff${hex(run.text)}> >> BDC\nBT\n100 Tz\n`;
|
|
613
642
|
for (const part of run.parts || [run]) {
|
|
614
643
|
if (part.font) {
|
|
@@ -623,11 +652,11 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
623
652
|
const baseline = decimal(height - margins.top - (part.y + part.height * .8) * .75);
|
|
624
653
|
for (let letter = 0; letter < characters.length; letter++) {
|
|
625
654
|
const value = codes.get(characters[letter].codePointAt(0)).toString(16).padStart(4, '0');
|
|
626
|
-
commands +=
|
|
655
|
+
commands += `${run.carrier ? '/Fc' : '/F1'} ${decimal(size)} Tf\n3 Tr\n1 0 0 ${decimal(scaleY)} ${decimal(margins.left + part.x * .75 + slots[letter] * advance)} ${baseline} Tm\n<${value}> Tj\n`;
|
|
627
656
|
}
|
|
628
657
|
}
|
|
629
658
|
}
|
|
630
|
-
commands += 'ET\nEMC\nQ\n';
|
|
659
|
+
commands += 'ET\nEMC\nQ\n' + (element ? 'EMC\n' : '');
|
|
631
660
|
}
|
|
632
661
|
if (numberFont) commands += `BT\n/F2 9 Tf\n0 Tr\n1 0 0 1 ${decimal(width / 2 - String(index + 1).length * 2.5)} ${decimal(margins.bottom / 2)} Tm\n(${index + 1}) Tj\nET\n`;
|
|
633
662
|
const content = stream('/Filter /FlateDecode', await compressed(encode(commands)));
|
|
@@ -638,10 +667,17 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
638
667
|
: '/Dest [' + kids[link.target.page] + ' 0 R /XYZ ' + decimal(margins.left + link.target.x * .75) + ' ' + decimal(height - margins.top - link.target.y * .75) + ' null]';
|
|
639
668
|
return add('<< /Type /Annot /Subtype /Link /Border [0 0 0] /Rect [' + rect.join(' ') + '] ' + target + ' >>');
|
|
640
669
|
});
|
|
641
|
-
put(kids[index], '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
|
|
670
|
+
put(kids[index], '<< /Type /Page /Parent 2 0 R /StructParents ' + index + ' /MediaBox [0 0 ' + decimal(width) + ' ' + decimal(height) + '] /Resources << /XObject << ' + pictures.join(' ') + ' >> /ExtGState << ' + states.join(' ') + ' >> /Font << /F1 ' + type + ' 0 R /Fc ' + carrierType + ' 0 R' + (numberFont ? ' /F2 ' + numberFont + ' 0 R' : '') + ' ' + nativeResources.join(' ') + ' >> >> /Contents ' + content + ' 0 R' + (annotations.length ? ' /Annots [' + annotations.map(id => id + ' 0 R').join(' ') + ']' : '') + ' >>');
|
|
642
671
|
}
|
|
643
672
|
put(2, '<< /Type /Pages /Count ' + kids.length + ' /Kids [' + kids.map(id => id + ' 0 R').join(' ') + '] >>');
|
|
644
|
-
|
|
673
|
+
const tree = add(''), ids = new Map([[documentElement, add('')]]);
|
|
674
|
+
const reserve = element => { for (const kid of element.kids) if (kid.kids) { ids.set(kid, add('')); reserve(kid); } };
|
|
675
|
+
reserve(documentElement);
|
|
676
|
+
for (const [element, id] of ids) put(id, '<< /Type /StructElem /S /' + element.role + ' /P ' + (element.parent ? ids.get(element.parent) : tree) + ' 0 R /K [' +
|
|
677
|
+
element.kids.map(kid => kid.kids ? ids.get(kid) + ' 0 R' : '<< /Type /MCR /Pg ' + kids[kid.page] + ' 0 R /MCID ' + kid.mcid + ' >>').join(' ') + '] >>');
|
|
678
|
+
const parents = add('<< /Nums [' + parentTree.map((marked, index) => index + ' [' + marked.map(element => ids.get(element) + ' 0 R').join(' ') + ']').join(' ') + '] >>');
|
|
679
|
+
put(tree, '<< /Type /StructTreeRoot /K [' + ids.get(documentElement) + ' 0 R] /ParentTree ' + parents + ' 0 R /ParentTreeNextKey ' + parentTree.length + ' >>');
|
|
680
|
+
put(1, '<< /Type /Catalog /Pages 2 0 R /MarkInfo << /Marked true >> /StructTreeRoot ' + tree + ' 0 R /Names << /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + file + ' 0 R] >> >> /AF [' + file + ' 0 R] >>');
|
|
645
681
|
const info = add('<< /Producer (Rapier) /Title <feff' + hex(title || filename.replace(/\.[^.]*$/, '')) + '> >>');
|
|
646
682
|
const header = encode('%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n'), offsets = new Array(objects.length);
|
|
647
683
|
let cursor = header.length;
|