@superdoc/sdk 2.21.0-next.9 → 2.22.0-next.1
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/dist/agent/actions.cjs +18 -127
- package/dist/agent/actions.js +18 -127
- package/dist/generated/client.cjs +4 -0
- package/dist/generated/client.d.ts +847 -3
- package/dist/generated/client.js +4 -0
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/package.json +6 -6
- package/tools/generation-receipt.json +10 -10
- package/tools/tools-policy.json +1 -1
package/dist/agent/actions.cjs
CHANGED
|
@@ -518,6 +518,17 @@ function evaluateChecks(pre, post, checks) {
|
|
|
518
518
|
}
|
|
519
519
|
return results;
|
|
520
520
|
}
|
|
521
|
+
function assertReplacementTextPayloads(edits) {
|
|
522
|
+
for (const [index, edit] of edits.entries()) {
|
|
523
|
+
const text = edit.replace ?? '';
|
|
524
|
+
for (let offset = 0; offset < text.length; offset += 1) {
|
|
525
|
+
const code = text.charCodeAt(offset);
|
|
526
|
+
if (code < 0x20 && code !== 0x09 && code !== 0x0a) {
|
|
527
|
+
throw new errors.SuperDocCliError(`edits[${index}].replace contains unsupported control character U+${code.toString(16).padStart(4, '0')}. Use plain text, tabs or preserved manual line breaks.`, { code: 'INVALID_PAYLOAD' });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
521
532
|
function preserveRewriteStyle() {
|
|
522
533
|
return {
|
|
523
534
|
inline: { mode: 'preserve' },
|
|
@@ -964,6 +975,7 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
964
975
|
? await docSnapshot.buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
965
976
|
: snapshotFromIdentity(preIdentity);
|
|
966
977
|
try {
|
|
978
|
+
assertReplacementTextPayloads(args.edits);
|
|
967
979
|
if (args.edits.length === 0) {
|
|
968
980
|
return failedReceipt('replace_text', new Error('edits must be non-empty'), pre);
|
|
969
981
|
}
|
|
@@ -2493,115 +2505,6 @@ async function applyInlineQuiet(formatFn, params) {
|
|
|
2493
2505
|
throw err;
|
|
2494
2506
|
}
|
|
2495
2507
|
}
|
|
2496
|
-
/** Best-effort read of a block's current run layout (null when unavailable). */
|
|
2497
|
-
async function captureBlockRuns(doc, nodeId, text) {
|
|
2498
|
-
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
2499
|
-
const trimmed = text.trim();
|
|
2500
|
-
if (!queryFn || !trimmed || text.includes('\n'))
|
|
2501
|
-
return null;
|
|
2502
|
-
try {
|
|
2503
|
-
const runs = await docSnapshot.matchRunsForBlock(queryFn, trimmed, nodeId);
|
|
2504
|
-
return runs.length > 0 ? runs : null;
|
|
2505
|
-
}
|
|
2506
|
-
catch {
|
|
2507
|
-
return null;
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
/**
|
|
2511
|
-
* Restore scoped replacements from their known spans: each untouched interval
|
|
2512
|
-
* keeps its run formatting, and replacements inherit the marks at their start.
|
|
2513
|
-
* `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
|
|
2514
|
-
*/
|
|
2515
|
-
async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
2516
|
-
if (!oldRuns || oldRuns.length === 0 || spans.length === 0)
|
|
2517
|
-
return null;
|
|
2518
|
-
if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
|
|
2519
|
-
return null;
|
|
2520
|
-
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
2521
|
-
if (!formatFn)
|
|
2522
|
-
return null;
|
|
2523
|
-
const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
|
|
2524
|
-
// Run offsets (captureBlockRuns) are relative to the TRIMMED text; spans are
|
|
2525
|
-
// full-text. Convert spans to trimmed coordinates and clamp.
|
|
2526
|
-
const oldTrimStart = oldText.length - oldText.trimStart().length;
|
|
2527
|
-
const oldTrimLen = oldText.trim().length;
|
|
2528
|
-
const trimSpans = spans
|
|
2529
|
-
.map((s) => ({
|
|
2530
|
-
start: Math.max(0, s.start - oldTrimStart),
|
|
2531
|
-
end: Math.min(oldTrimLen, s.end - oldTrimStart),
|
|
2532
|
-
replaceLen: (s.replace ?? '').length,
|
|
2533
|
-
}))
|
|
2534
|
-
.filter((s) => s.end > s.start || s.replaceLen > 0);
|
|
2535
|
-
if (trimSpans.length === 0)
|
|
2536
|
-
return null;
|
|
2537
|
-
const newText = spans.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), oldText);
|
|
2538
|
-
const newTrimStart = newText.length - newText.trimStart().length;
|
|
2539
|
-
// Cumulative old→new offset shift for positions AT OR AFTER a given old offset.
|
|
2540
|
-
const deltaBefore = (oldPos) => {
|
|
2541
|
-
let d = 0;
|
|
2542
|
-
for (const s of trimSpans) {
|
|
2543
|
-
if (s.end <= oldPos)
|
|
2544
|
-
d += s.replaceLen - (s.end - s.start);
|
|
2545
|
-
else
|
|
2546
|
-
break;
|
|
2547
|
-
}
|
|
2548
|
-
return d;
|
|
2549
|
-
};
|
|
2550
|
-
let applied = 0;
|
|
2551
|
-
const applyRange = async (start, end, inline) => {
|
|
2552
|
-
if (end <= start || Object.keys(inline).length === 0)
|
|
2553
|
-
return;
|
|
2554
|
-
// Always direct, even under tracked mode. This pass RESTORES the marks the
|
|
2555
|
-
// rewrite flattened; it is not new authorial formatting, and the text
|
|
2556
|
-
// revision already owns the span's accept/reject fate. Applying it tracked
|
|
2557
|
-
// adds a second reviewable change per rewrite (SD-4309's conformance matrix
|
|
2558
|
-
// counts one per action), and rejecting that change alone would strand the
|
|
2559
|
-
// text in the flattened look the rewrite produced.
|
|
2560
|
-
const changed = await applyInlineQuiet(formatFn, {
|
|
2561
|
-
blockId: nodeId,
|
|
2562
|
-
start: newTrimStart + start,
|
|
2563
|
-
end: newTrimStart + end,
|
|
2564
|
-
inline,
|
|
2565
|
-
});
|
|
2566
|
-
if (changed)
|
|
2567
|
-
applied += 1;
|
|
2568
|
-
};
|
|
2569
|
-
try {
|
|
2570
|
-
// Untouched intervals between/around the spans, in old trimmed coords.
|
|
2571
|
-
const intervals = [];
|
|
2572
|
-
let cursor = 0;
|
|
2573
|
-
for (const s of trimSpans) {
|
|
2574
|
-
if (s.start > cursor)
|
|
2575
|
-
intervals.push({ start: cursor, end: s.start });
|
|
2576
|
-
cursor = Math.max(cursor, s.end);
|
|
2577
|
-
}
|
|
2578
|
-
if (cursor < oldTrimLen)
|
|
2579
|
-
intervals.push({ start: cursor, end: oldTrimLen });
|
|
2580
|
-
for (const run of oldRuns) {
|
|
2581
|
-
const inline = inlineFromSnapshotRun(run, normalize);
|
|
2582
|
-
for (const iv of intervals) {
|
|
2583
|
-
const a = Math.max(run.start, iv.start);
|
|
2584
|
-
const b = Math.min(run.end, iv.end);
|
|
2585
|
-
if (b <= a)
|
|
2586
|
-
continue;
|
|
2587
|
-
const d = deltaBefore(a); // constant across an untouched interval
|
|
2588
|
-
await applyRange(a + d, b + d, inline);
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
// Each replacement span inherits the marks of the run it starts in.
|
|
2592
|
-
for (const s of trimSpans) {
|
|
2593
|
-
if (s.replaceLen === 0)
|
|
2594
|
-
continue;
|
|
2595
|
-
const anchorRun = oldRuns.find((run) => s.start >= run.start && s.start < run.end) ?? oldRuns[oldRuns.length - 1];
|
|
2596
|
-
const newStart = s.start + deltaBefore(s.start);
|
|
2597
|
-
await applyRange(newStart, newStart + s.replaceLen, inlineFromSnapshotRun(anchorRun, normalize));
|
|
2598
|
-
}
|
|
2599
|
-
return applied > 0 ? { restoredRunRanges: applied } : null;
|
|
2600
|
-
}
|
|
2601
|
-
catch (err) {
|
|
2602
|
-
return { skipped: reasonOf(err) };
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
2508
|
const NAMED_COLORS = {
|
|
2606
2509
|
black: '000000',
|
|
2607
2510
|
blue: '0000FF',
|
|
@@ -4397,6 +4300,7 @@ function parseScopedReplaceArgs(args, changeMode) {
|
|
|
4397
4300
|
};
|
|
4398
4301
|
}
|
|
4399
4302
|
async function runScopedReplaceUnchecked(doc, args) {
|
|
4303
|
+
assertReplacementTextPayloads(args.edits);
|
|
4400
4304
|
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4401
4305
|
const matched = docSnapshot.resolveSnapshotSelector(pre, args.selector);
|
|
4402
4306
|
if (matched.length !== 1) {
|
|
@@ -4462,12 +4366,8 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4462
4366
|
// "requires a ref produced by query.match/find".
|
|
4463
4367
|
const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
|
|
4464
4368
|
const tracked = (args.changeMode ?? 'direct') === 'tracked';
|
|
4465
|
-
//
|
|
4466
|
-
//
|
|
4467
|
-
// one-word edit in a full-paragraph redline. Each span becomes a
|
|
4468
|
-
// select-scoped rewrite pinned to this block via `within`; duplicate find
|
|
4469
|
-
// strings would re-resolve ambiguously against tracked-visible deleted
|
|
4470
|
-
// text, so those fall back to the whole-block path below.
|
|
4369
|
+
// Unique tracked finds can be resolved within the block. Repeated finds
|
|
4370
|
+
// use one whole-block rewrite, whose engine diff preserves unchanged runs.
|
|
4471
4371
|
let result = null;
|
|
4472
4372
|
// Dedupe on the same basis the selector will match on: under a
|
|
4473
4373
|
// case-sensitive request `Foo` and `foo` are distinct targets, and folding
|
|
@@ -4487,26 +4387,18 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4487
4387
|
require: 'first',
|
|
4488
4388
|
within: { kind: 'block', nodeType: block.nodeType, nodeId },
|
|
4489
4389
|
},
|
|
4490
|
-
args: { replacement: { text: span.replace } },
|
|
4390
|
+
args: { replacement: { text: span.replace }, style: preserveRewriteStyle() },
|
|
4491
4391
|
}));
|
|
4492
|
-
// Restore the known edit spans directly so the formatting pass adds no
|
|
4493
|
-
// separate tracked change.
|
|
4494
|
-
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4495
4392
|
try {
|
|
4496
4393
|
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
4497
|
-
if (result != null)
|
|
4498
|
-
await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
|
|
4499
4394
|
}
|
|
4500
4395
|
catch {
|
|
4501
4396
|
result = null; // select lane unavailable (e.g. case mismatch) — fall back
|
|
4502
4397
|
}
|
|
4503
4398
|
}
|
|
4504
4399
|
if (result == null) {
|
|
4505
|
-
//
|
|
4506
|
-
//
|
|
4507
|
-
// spans did NOT touch keeps its original run formatting at the mapped
|
|
4508
|
-
// offsets, and each replacement inherits the marks of the run it starts in.
|
|
4509
|
-
const preRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4400
|
+
// The engine preserves runs while applying the text diff. A separate
|
|
4401
|
+
// formatting pass would restyle unchanged text and split the undo unit.
|
|
4510
4402
|
const steps = [
|
|
4511
4403
|
{
|
|
4512
4404
|
id: 'scoped-replace-1',
|
|
@@ -4516,7 +4408,6 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4516
4408
|
},
|
|
4517
4409
|
];
|
|
4518
4410
|
result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
4519
|
-
await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
|
|
4520
4411
|
}
|
|
4521
4412
|
const post = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4522
4413
|
const postBlock = findTextTarget(post, nodeId);
|
package/dist/agent/actions.js
CHANGED
|
@@ -515,6 +515,17 @@ function evaluateChecks(pre, post, checks) {
|
|
|
515
515
|
}
|
|
516
516
|
return results;
|
|
517
517
|
}
|
|
518
|
+
function assertReplacementTextPayloads(edits) {
|
|
519
|
+
for (const [index, edit] of edits.entries()) {
|
|
520
|
+
const text = edit.replace ?? '';
|
|
521
|
+
for (let offset = 0; offset < text.length; offset += 1) {
|
|
522
|
+
const code = text.charCodeAt(offset);
|
|
523
|
+
if (code < 0x20 && code !== 0x09 && code !== 0x0a) {
|
|
524
|
+
throw new SuperDocCliError(`edits[${index}].replace contains unsupported control character U+${code.toString(16).padStart(4, '0')}. Use plain text, tabs or preserved manual line breaks.`, { code: 'INVALID_PAYLOAD' });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
518
529
|
function preserveRewriteStyle() {
|
|
519
530
|
return {
|
|
520
531
|
inline: { mode: 'preserve' },
|
|
@@ -961,6 +972,7 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
961
972
|
? await buildMutationSnapshot(doc, { includeDomains: selectorDomains ?? ['blocks'] })
|
|
962
973
|
: snapshotFromIdentity(preIdentity);
|
|
963
974
|
try {
|
|
975
|
+
assertReplacementTextPayloads(args.edits);
|
|
964
976
|
if (args.edits.length === 0) {
|
|
965
977
|
return failedReceipt('replace_text', new Error('edits must be non-empty'), pre);
|
|
966
978
|
}
|
|
@@ -2490,115 +2502,6 @@ async function applyInlineQuiet(formatFn, params) {
|
|
|
2490
2502
|
throw err;
|
|
2491
2503
|
}
|
|
2492
2504
|
}
|
|
2493
|
-
/** Best-effort read of a block's current run layout (null when unavailable). */
|
|
2494
|
-
async function captureBlockRuns(doc, nodeId, text) {
|
|
2495
|
-
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
2496
|
-
const trimmed = text.trim();
|
|
2497
|
-
if (!queryFn || !trimmed || text.includes('\n'))
|
|
2498
|
-
return null;
|
|
2499
|
-
try {
|
|
2500
|
-
const runs = await matchRunsForBlock(queryFn, trimmed, nodeId);
|
|
2501
|
-
return runs.length > 0 ? runs : null;
|
|
2502
|
-
}
|
|
2503
|
-
catch {
|
|
2504
|
-
return null;
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
/**
|
|
2508
|
-
* Restore scoped replacements from their known spans: each untouched interval
|
|
2509
|
-
* keeps its run formatting, and replacements inherit the marks at their start.
|
|
2510
|
-
* `spans` are FULL-TEXT coordinates, ascending, non-overlapping.
|
|
2511
|
-
*/
|
|
2512
|
-
async function preserveScopedRunPattern(doc, nodeId, oldText, oldRuns, spans) {
|
|
2513
|
-
if (!oldRuns || oldRuns.length === 0 || spans.length === 0)
|
|
2514
|
-
return null;
|
|
2515
|
-
if (!oldRuns.some((run) => Object.keys(inlineFromSnapshotRun(run, new Set())).length > 0))
|
|
2516
|
-
return null;
|
|
2517
|
-
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
2518
|
-
if (!formatFn)
|
|
2519
|
-
return null;
|
|
2520
|
-
const normalize = new Set(RUN_BOOL_MARKS.filter((key) => oldRuns.some((run) => run[key])));
|
|
2521
|
-
// Run offsets (captureBlockRuns) are relative to the TRIMMED text; spans are
|
|
2522
|
-
// full-text. Convert spans to trimmed coordinates and clamp.
|
|
2523
|
-
const oldTrimStart = oldText.length - oldText.trimStart().length;
|
|
2524
|
-
const oldTrimLen = oldText.trim().length;
|
|
2525
|
-
const trimSpans = spans
|
|
2526
|
-
.map((s) => ({
|
|
2527
|
-
start: Math.max(0, s.start - oldTrimStart),
|
|
2528
|
-
end: Math.min(oldTrimLen, s.end - oldTrimStart),
|
|
2529
|
-
replaceLen: (s.replace ?? '').length,
|
|
2530
|
-
}))
|
|
2531
|
-
.filter((s) => s.end > s.start || s.replaceLen > 0);
|
|
2532
|
-
if (trimSpans.length === 0)
|
|
2533
|
-
return null;
|
|
2534
|
-
const newText = spans.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), oldText);
|
|
2535
|
-
const newTrimStart = newText.length - newText.trimStart().length;
|
|
2536
|
-
// Cumulative old→new offset shift for positions AT OR AFTER a given old offset.
|
|
2537
|
-
const deltaBefore = (oldPos) => {
|
|
2538
|
-
let d = 0;
|
|
2539
|
-
for (const s of trimSpans) {
|
|
2540
|
-
if (s.end <= oldPos)
|
|
2541
|
-
d += s.replaceLen - (s.end - s.start);
|
|
2542
|
-
else
|
|
2543
|
-
break;
|
|
2544
|
-
}
|
|
2545
|
-
return d;
|
|
2546
|
-
};
|
|
2547
|
-
let applied = 0;
|
|
2548
|
-
const applyRange = async (start, end, inline) => {
|
|
2549
|
-
if (end <= start || Object.keys(inline).length === 0)
|
|
2550
|
-
return;
|
|
2551
|
-
// Always direct, even under tracked mode. This pass RESTORES the marks the
|
|
2552
|
-
// rewrite flattened; it is not new authorial formatting, and the text
|
|
2553
|
-
// revision already owns the span's accept/reject fate. Applying it tracked
|
|
2554
|
-
// adds a second reviewable change per rewrite (SD-4309's conformance matrix
|
|
2555
|
-
// counts one per action), and rejecting that change alone would strand the
|
|
2556
|
-
// text in the flattened look the rewrite produced.
|
|
2557
|
-
const changed = await applyInlineQuiet(formatFn, {
|
|
2558
|
-
blockId: nodeId,
|
|
2559
|
-
start: newTrimStart + start,
|
|
2560
|
-
end: newTrimStart + end,
|
|
2561
|
-
inline,
|
|
2562
|
-
});
|
|
2563
|
-
if (changed)
|
|
2564
|
-
applied += 1;
|
|
2565
|
-
};
|
|
2566
|
-
try {
|
|
2567
|
-
// Untouched intervals between/around the spans, in old trimmed coords.
|
|
2568
|
-
const intervals = [];
|
|
2569
|
-
let cursor = 0;
|
|
2570
|
-
for (const s of trimSpans) {
|
|
2571
|
-
if (s.start > cursor)
|
|
2572
|
-
intervals.push({ start: cursor, end: s.start });
|
|
2573
|
-
cursor = Math.max(cursor, s.end);
|
|
2574
|
-
}
|
|
2575
|
-
if (cursor < oldTrimLen)
|
|
2576
|
-
intervals.push({ start: cursor, end: oldTrimLen });
|
|
2577
|
-
for (const run of oldRuns) {
|
|
2578
|
-
const inline = inlineFromSnapshotRun(run, normalize);
|
|
2579
|
-
for (const iv of intervals) {
|
|
2580
|
-
const a = Math.max(run.start, iv.start);
|
|
2581
|
-
const b = Math.min(run.end, iv.end);
|
|
2582
|
-
if (b <= a)
|
|
2583
|
-
continue;
|
|
2584
|
-
const d = deltaBefore(a); // constant across an untouched interval
|
|
2585
|
-
await applyRange(a + d, b + d, inline);
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
// Each replacement span inherits the marks of the run it starts in.
|
|
2589
|
-
for (const s of trimSpans) {
|
|
2590
|
-
if (s.replaceLen === 0)
|
|
2591
|
-
continue;
|
|
2592
|
-
const anchorRun = oldRuns.find((run) => s.start >= run.start && s.start < run.end) ?? oldRuns[oldRuns.length - 1];
|
|
2593
|
-
const newStart = s.start + deltaBefore(s.start);
|
|
2594
|
-
await applyRange(newStart, newStart + s.replaceLen, inlineFromSnapshotRun(anchorRun, normalize));
|
|
2595
|
-
}
|
|
2596
|
-
return applied > 0 ? { restoredRunRanges: applied } : null;
|
|
2597
|
-
}
|
|
2598
|
-
catch (err) {
|
|
2599
|
-
return { skipped: reasonOf(err) };
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
2505
|
const NAMED_COLORS = {
|
|
2603
2506
|
black: '000000',
|
|
2604
2507
|
blue: '0000FF',
|
|
@@ -4394,6 +4297,7 @@ function parseScopedReplaceArgs(args, changeMode) {
|
|
|
4394
4297
|
};
|
|
4395
4298
|
}
|
|
4396
4299
|
async function runScopedReplaceUnchecked(doc, args) {
|
|
4300
|
+
assertReplacementTextPayloads(args.edits);
|
|
4397
4301
|
const pre = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4398
4302
|
const matched = resolveSnapshotSelector(pre, args.selector);
|
|
4399
4303
|
if (matched.length !== 1) {
|
|
@@ -4459,12 +4363,8 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4459
4363
|
// "requires a ref produced by query.match/find".
|
|
4460
4364
|
const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
|
|
4461
4365
|
const tracked = (args.changeMode ?? 'direct') === 'tracked';
|
|
4462
|
-
//
|
|
4463
|
-
//
|
|
4464
|
-
// one-word edit in a full-paragraph redline. Each span becomes a
|
|
4465
|
-
// select-scoped rewrite pinned to this block via `within`; duplicate find
|
|
4466
|
-
// strings would re-resolve ambiguously against tracked-visible deleted
|
|
4467
|
-
// text, so those fall back to the whole-block path below.
|
|
4366
|
+
// Unique tracked finds can be resolved within the block. Repeated finds
|
|
4367
|
+
// use one whole-block rewrite, whose engine diff preserves unchanged runs.
|
|
4468
4368
|
let result = null;
|
|
4469
4369
|
// Dedupe on the same basis the selector will match on: under a
|
|
4470
4370
|
// case-sensitive request `Foo` and `foo` are distinct targets, and folding
|
|
@@ -4484,26 +4384,18 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4484
4384
|
require: 'first',
|
|
4485
4385
|
within: { kind: 'block', nodeType: block.nodeType, nodeId },
|
|
4486
4386
|
},
|
|
4487
|
-
args: { replacement: { text: span.replace } },
|
|
4387
|
+
args: { replacement: { text: span.replace }, style: preserveRewriteStyle() },
|
|
4488
4388
|
}));
|
|
4489
|
-
// Restore the known edit spans directly so the formatting pass adds no
|
|
4490
|
-
// separate tracked change.
|
|
4491
|
-
const trackedPreRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4492
4389
|
try {
|
|
4493
4390
|
result = await executeMutations(doc, spanSteps, args.changeMode, pre.revision);
|
|
4494
|
-
if (result != null)
|
|
4495
|
-
await preserveScopedRunPattern(doc, nodeId, block.text, trackedPreRuns, planned);
|
|
4496
4391
|
}
|
|
4497
4392
|
catch {
|
|
4498
4393
|
result = null; // select lane unavailable (e.g. case mismatch) — fall back
|
|
4499
4394
|
}
|
|
4500
4395
|
}
|
|
4501
4396
|
if (result == null) {
|
|
4502
|
-
//
|
|
4503
|
-
//
|
|
4504
|
-
// spans did NOT touch keeps its original run formatting at the mapped
|
|
4505
|
-
// offsets, and each replacement inherits the marks of the run it starts in.
|
|
4506
|
-
const preRuns = await captureBlockRuns(doc, nodeId, block.text);
|
|
4397
|
+
// The engine preserves runs while applying the text diff. A separate
|
|
4398
|
+
// formatting pass would restyle unchanged text and split the undo unit.
|
|
4507
4399
|
const steps = [
|
|
4508
4400
|
{
|
|
4509
4401
|
id: 'scoped-replace-1',
|
|
@@ -4513,7 +4405,6 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4513
4405
|
},
|
|
4514
4406
|
];
|
|
4515
4407
|
result = await executeMutations(doc, steps, args.changeMode, pre.revision);
|
|
4516
|
-
await preserveScopedRunPattern(doc, nodeId, block.text, preRuns, planned);
|
|
4517
4408
|
}
|
|
4518
4409
|
const post = await buildMutationSnapshot(doc, { includeDomains: ['blocks', 'tables'] });
|
|
4519
4410
|
const postBlock = findTextTarget(post, nodeId);
|
|
@@ -108,7 +108,9 @@ function createDocApi(runtime) {
|
|
|
108
108
|
},
|
|
109
109
|
styles: {
|
|
110
110
|
apply: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.apply"], params, options),
|
|
111
|
+
create: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.create"], params, options),
|
|
111
112
|
getCatalog: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.getCatalog"], params, options),
|
|
113
|
+
getTheme: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.getTheme"], params, options),
|
|
112
114
|
paragraph: {
|
|
113
115
|
setStyle: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.paragraph.setStyle"], params, options),
|
|
114
116
|
setStyleRef: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.paragraph.setStyleRef"], params, options),
|
|
@@ -689,7 +691,9 @@ function createBoundDocApi(runtime) {
|
|
|
689
691
|
},
|
|
690
692
|
styles: {
|
|
691
693
|
apply: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.apply"], params, options),
|
|
694
|
+
create: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.create"], params, options),
|
|
692
695
|
getCatalog: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.getCatalog"], params, options),
|
|
696
|
+
getTheme: (params = {}, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.getTheme"], params, options),
|
|
693
697
|
paragraph: {
|
|
694
698
|
setStyle: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.paragraph.setStyle"], params, options),
|
|
695
699
|
setStyleRef: (params, options) => runtime.invoke(contract.CONTRACT.operations["doc.styles.paragraph.setStyleRef"], params, options),
|