@superdoc/sdk 2.23.0-next.8 → 2.23.0
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/action-primitives/tools/structure-insert.cjs +3 -5
- package/dist/action-primitives/tools/structure-insert.js +3 -5
- package/dist/action-primitives/tools/text-transform.cjs +3 -5
- package/dist/action-primitives/tools/text-transform.js +3 -5
- package/dist/actions/define.cjs +15 -17
- package/dist/actions/define.js +1 -3
- package/dist/agent/actions.cjs +438 -52
- package/dist/agent/actions.d.ts +13 -0
- package/dist/agent/actions.js +394 -11
- package/dist/agent/runtime.cjs +7 -9
- package/dist/agent/runtime.js +1 -3
- package/dist/agent/v2-preset-compat.cjs +5 -7
- package/dist/agent/v2-preset-compat.js +1 -3
- package/dist/embedded-prompts.generated.cjs +1 -1
- package/dist/embedded-prompts.generated.js +1 -1
- package/dist/embedded-tools.generated.cjs +5 -5
- package/dist/embedded-tools.generated.js +5 -5
- package/dist/generated/client.d.ts +193 -177
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/guards.cjs +7 -0
- package/dist/guards.d.ts +1 -0
- package/dist/guards.js +3 -0
- package/dist/presets/core.cjs +10 -12
- package/dist/presets/core.js +1 -3
- package/dist/presets/legacy.cjs +6 -8
- package/dist/presets/legacy.js +1 -3
- package/dist/prompts/system-prompt.md +6 -6
- 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/catalog.json +8 -0
- package/tools/generation-receipt.json +18 -18
- package/tools/tools-policy.json +1 -1
- package/tools/tools.anthropic.json +8 -0
- package/tools/tools.generic.json +8 -0
- package/tools/tools.openai.json +8 -0
- package/tools/tools.vercel.json +8 -0
package/dist/agent/actions.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var listTransform = require('../action-primitives/tools/list-transform.cjs');
|
|
|
9
9
|
var structureInsert = require('../action-primitives/tools/structure-insert.cjs');
|
|
10
10
|
var textTransform = require('../action-primitives/tools/text-transform.cjs');
|
|
11
11
|
var docSnapshot = require('./doc-snapshot.cjs');
|
|
12
|
+
var guards = require('../guards.cjs');
|
|
12
13
|
|
|
13
14
|
const ACTION_DEFINITIONS = actionDescriptors;
|
|
14
15
|
function projectActionDefinitions(project) {
|
|
@@ -72,9 +73,6 @@ function getActionDispositions() {
|
|
|
72
73
|
function isActionName(value) {
|
|
73
74
|
return typeof value === 'string' && ACTION_NAMES.includes(value);
|
|
74
75
|
}
|
|
75
|
-
function isRecord(value) {
|
|
76
|
-
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
77
|
-
}
|
|
78
76
|
/** The message to put in a receipt when something best-effort did not happen. */
|
|
79
77
|
function reasonOf(err) {
|
|
80
78
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -100,7 +98,7 @@ function isNoOp(err) {
|
|
|
100
98
|
return err?.code === 'NO_OP';
|
|
101
99
|
}
|
|
102
100
|
function asRecord(value) {
|
|
103
|
-
return isRecord(value) ? value : null;
|
|
101
|
+
return guards.isRecord(value) ? value : null;
|
|
104
102
|
}
|
|
105
103
|
function asString(value, fallback) {
|
|
106
104
|
return typeof value === 'string' ? value : fallback;
|
|
@@ -657,7 +655,7 @@ async function previewMutations(doc, steps, changeMode, expectedRevision) {
|
|
|
657
655
|
});
|
|
658
656
|
}
|
|
659
657
|
const result = await fn({ atomic: true, changeMode: changeMode ?? 'direct', expectedRevision, steps });
|
|
660
|
-
return isRecord(result) ? result : {};
|
|
658
|
+
return guards.isRecord(result) ? result : {};
|
|
661
659
|
}
|
|
662
660
|
async function executeCreateTable(doc, args, placement) {
|
|
663
661
|
const fn = maybeMethod(doc, ['create', 'table']);
|
|
@@ -880,6 +878,7 @@ async function styleTableCells(doc, cells, changeMode) {
|
|
|
880
878
|
await applyItemStyling(doc, nodeIds, specs, changeMode);
|
|
881
879
|
return styled.length;
|
|
882
880
|
}
|
|
881
|
+
const TYPED_SIBLING_WINDOW = 20;
|
|
883
882
|
async function runInsertParagraphs(doc, args, formatArgs) {
|
|
884
883
|
const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
|
|
885
884
|
try {
|
|
@@ -892,7 +891,7 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
892
891
|
kind: 'placement',
|
|
893
892
|
at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
|
|
894
893
|
});
|
|
895
|
-
|
|
894
|
+
let preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
|
|
896
895
|
let currentPlacement = placement;
|
|
897
896
|
const nodeIds = [];
|
|
898
897
|
const specs = [];
|
|
@@ -932,6 +931,16 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
932
931
|
if (!pre.complete) {
|
|
933
932
|
const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
|
|
934
933
|
localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
|
|
934
|
+
const placed = anchor && texts.some((text) => typedMarkerPrefixOf(text)) ? await post.block(anchor.nodeId) : null;
|
|
935
|
+
if (placed) {
|
|
936
|
+
const span = TYPED_SIBLING_WINDOW + nodeIds.length;
|
|
937
|
+
const window = await post.page({ offset: Math.max(0, placed.ordinal - 1 - span), limit: 2 * span + 1 });
|
|
938
|
+
const createdIds = new Set(nodeIds);
|
|
939
|
+
if (nodeIds.every((id) => window.some((row) => row.nodeId === id))) {
|
|
940
|
+
preRows = window.filter((row) => !createdIds.has(row.nodeId));
|
|
941
|
+
localRows = window;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
935
944
|
}
|
|
936
945
|
const count = context.executedOperations.length;
|
|
937
946
|
formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
|
|
@@ -1052,7 +1061,7 @@ async function runReplaceTextUnchecked(doc, args) {
|
|
|
1052
1061
|
const preview = await previewMutations(doc, candidates.map((candidate) => candidate.step), args.changeMode, pre.revision);
|
|
1053
1062
|
if (preview.valid === true)
|
|
1054
1063
|
break;
|
|
1055
|
-
const failures = Array.isArray(preview.failures) ? preview.failures.filter(isRecord) : [];
|
|
1064
|
+
const failures = Array.isArray(preview.failures) ? preview.failures.filter(guards.isRecord) : [];
|
|
1056
1065
|
const absent = failures.find((failure) => failure.code === 'MATCH_NOT_FOUND' && typeof failure.stepId === 'string');
|
|
1057
1066
|
if (!absent) {
|
|
1058
1067
|
throw new Error(typeof failures[0]?.message === 'string' ? failures[0].message : 'replacement plan could not be prepared');
|
|
@@ -1281,6 +1290,172 @@ async function runDeleteText(doc, args) {
|
|
|
1281
1290
|
return failedReceipt('delete_text', err, pre);
|
|
1282
1291
|
}
|
|
1283
1292
|
}
|
|
1293
|
+
const CONJUNCTION_TAIL = /[;,]\s*(and|or)\s*$/i;
|
|
1294
|
+
const ITEM_ENDING = /(?:[;,]\s*(?:and|or)|[.;,:])\s*$/i;
|
|
1295
|
+
const ITEM_CLOSERS = /["'”’)\]]+$/;
|
|
1296
|
+
function enumerationSlotOf(block) {
|
|
1297
|
+
const numbering = blockNumbering(block);
|
|
1298
|
+
const path = numbering?.path;
|
|
1299
|
+
if (Array.isArray(path) && path.length > 0) {
|
|
1300
|
+
return { key: `scheme:${path.slice(0, -1).join('.')}:${path.length}`, depth: path.length, typed: null };
|
|
1301
|
+
}
|
|
1302
|
+
const markerShape = numbering?.marker ? markerShapeOf(numbering.marker) : null;
|
|
1303
|
+
if (markerShape)
|
|
1304
|
+
return { key: `scheme-marker:${markerShape.wrapper}:${markerShape.kind}`, depth: null, typed: null };
|
|
1305
|
+
const text = block.text || block.textPreview || '';
|
|
1306
|
+
const typedMarker = typedMarkerPrefixOf(text)?.marker;
|
|
1307
|
+
const typedShape = typedMarker ? markerShapeOf(text) : null;
|
|
1308
|
+
if (!typedMarker || !typedShape)
|
|
1309
|
+
return null;
|
|
1310
|
+
const parent = typedShape.kind === 'digit' ? typedDigitParentOf(typedMarker) : '';
|
|
1311
|
+
return { key: `typed:${typedShape.wrapper}:${typedShape.kind}:${parent}`, depth: null, typed: typedShape };
|
|
1312
|
+
}
|
|
1313
|
+
function typedDigitParentOf(marker) {
|
|
1314
|
+
const segments = marker.replace(/^\(/, '').replace(/[.)]$/, '').split('.');
|
|
1315
|
+
return `${segments.length}:${segments.slice(0, -1).join('.')}`;
|
|
1316
|
+
}
|
|
1317
|
+
const ROMAN_DIGITS = { i: 1, v: 5, x: 10 };
|
|
1318
|
+
function markerValueOf(token, shape) {
|
|
1319
|
+
const core = token.replace(/^\(/, '').replace(/[.)]$/, '').toLowerCase();
|
|
1320
|
+
if (shape.kind === 'digit')
|
|
1321
|
+
return Number(core.split('.').pop());
|
|
1322
|
+
if (shape.kind === 'lower-roman' || shape.kind === 'upper-roman') {
|
|
1323
|
+
let total = 0;
|
|
1324
|
+
for (let i = 0; i < core.length; i += 1) {
|
|
1325
|
+
const value = ROMAN_DIGITS[core[i]] ?? 0;
|
|
1326
|
+
total += value < (ROMAN_DIGITS[core[i + 1] ?? ''] ?? 0) ? -value : value;
|
|
1327
|
+
}
|
|
1328
|
+
return total;
|
|
1329
|
+
}
|
|
1330
|
+
const letter = core.charCodeAt(0) - 96;
|
|
1331
|
+
return core.length === 1 ? letter : 26 * (core.length - 1) + letter;
|
|
1332
|
+
}
|
|
1333
|
+
function enumerationRunOf(blocks, index, slot, textOf) {
|
|
1334
|
+
const typed = slot.typed;
|
|
1335
|
+
const valueOf = (block) => {
|
|
1336
|
+
if (!typed) {
|
|
1337
|
+
const path = blockNumbering(block)?.path;
|
|
1338
|
+
return Array.isArray(path) && path.length > 0 ? Number(path[path.length - 1]) : null;
|
|
1339
|
+
}
|
|
1340
|
+
const token = typed ? typedMarkerPrefixOf(textOf(block))?.marker : null;
|
|
1341
|
+
return token && typed ? markerValueOf(token, typed) : null;
|
|
1342
|
+
};
|
|
1343
|
+
const walk = (step) => {
|
|
1344
|
+
const found = [];
|
|
1345
|
+
let edge = valueOf(blocks[index]);
|
|
1346
|
+
let crossed = false;
|
|
1347
|
+
for (let i = index + step; i >= 0 && i < blocks.length; i += step) {
|
|
1348
|
+
const block = blocks[i];
|
|
1349
|
+
if (textOf(block).length === 0)
|
|
1350
|
+
continue;
|
|
1351
|
+
const other = enumerationSlotOf(block);
|
|
1352
|
+
const continuesAsLetter = typed != null &&
|
|
1353
|
+
other?.typed != null &&
|
|
1354
|
+
markerShapeFits(typed, other.typed) &&
|
|
1355
|
+
edge != null &&
|
|
1356
|
+
valueOf(block) === edge + step;
|
|
1357
|
+
if (other?.key === slot.key || continuesAsLetter) {
|
|
1358
|
+
const value = valueOf(block);
|
|
1359
|
+
if (value != null && edge != null) {
|
|
1360
|
+
const advance = (value - edge) * step;
|
|
1361
|
+
if (advance <= 0 || ((crossed || !typed) && advance !== 1))
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
found.push(block);
|
|
1365
|
+
edge = value;
|
|
1366
|
+
crossed = false;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (typed && other?.typed) {
|
|
1370
|
+
if (step === -1 && edge != null) {
|
|
1371
|
+
const prefix = typedMarkerPrefixOf(textOf(block)).prefix;
|
|
1372
|
+
const inline = new RegExp(String.raw `(?:^|\s)(${TYPED_MARKER_TOKEN}) `).exec(textOf(block).slice(prefix.length));
|
|
1373
|
+
const inlineShape = inline ? markerShapeOf(inline[1]) : null;
|
|
1374
|
+
if (inline && sameMarkerShape(inlineShape, typed) && markerValueOf(inline[1], typed) === edge - 1) {
|
|
1375
|
+
found.push(block);
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
crossed = true;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (slot.depth != null && other?.depth != null && other.depth > slot.depth)
|
|
1383
|
+
continue;
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
return found;
|
|
1387
|
+
};
|
|
1388
|
+
return [...walk(-1).reverse(), blocks[index], ...walk(1)];
|
|
1389
|
+
}
|
|
1390
|
+
function listPunctuationAfterDelete(blocks, deletedIds) {
|
|
1391
|
+
const textOf = (block) => (block.text || (block.textPreview ?? '').replace(/…\s*$/, '')).trimEnd();
|
|
1392
|
+
const endingOf = (block) => {
|
|
1393
|
+
const text = textOf(block);
|
|
1394
|
+
const closers = ITEM_CLOSERS.exec(text)?.[0] ?? '';
|
|
1395
|
+
const body = text.slice(0, text.length - closers.length).trimEnd();
|
|
1396
|
+
const ending = ITEM_ENDING.exec(body);
|
|
1397
|
+
return {
|
|
1398
|
+
core: ending?.[0].trim() ?? '',
|
|
1399
|
+
real: ending ? `${ending[0].trim()}${closers}` : closers,
|
|
1400
|
+
closers,
|
|
1401
|
+
conjunction: CONJUNCTION_TAIL.exec(body)?.[0].trim() ?? null,
|
|
1402
|
+
};
|
|
1403
|
+
};
|
|
1404
|
+
const quote = (block) => {
|
|
1405
|
+
const text = textOf(block);
|
|
1406
|
+
return JSON.stringify(text.length > 40 ? `...${text.slice(-40)}` : text);
|
|
1407
|
+
};
|
|
1408
|
+
const change = (block, to) => {
|
|
1409
|
+
const ending = endingOf(block);
|
|
1410
|
+
return `change the ending of ${quote(block)} (nodeId ${block.nodeId}) from "${ending.real}" to "${to}${ending.closers}"`;
|
|
1411
|
+
};
|
|
1412
|
+
const entries = [];
|
|
1413
|
+
const covered = new Set();
|
|
1414
|
+
blocks.forEach((block, index) => {
|
|
1415
|
+
if (!deletedIds.has(block.nodeId) || covered.has(block.nodeId))
|
|
1416
|
+
return;
|
|
1417
|
+
const slot = enumerationSlotOf(block);
|
|
1418
|
+
if (!slot)
|
|
1419
|
+
return;
|
|
1420
|
+
const run = enumerationRunOf(blocks, index, slot, textOf);
|
|
1421
|
+
for (const item of run)
|
|
1422
|
+
covered.add(item.nodeId);
|
|
1423
|
+
const survivors = run.filter((item) => !deletedIds.has(item.nodeId));
|
|
1424
|
+
if (survivors.length === 0)
|
|
1425
|
+
return;
|
|
1426
|
+
const last = survivors[survivors.length - 1];
|
|
1427
|
+
const penultimate = survivors.length >= 2 ? survivors[survivors.length - 2] : undefined;
|
|
1428
|
+
const originalPenultimate = run[run.length - 2];
|
|
1429
|
+
if (run.some((item) => item !== originalPenultimate && endingOf(item).conjunction))
|
|
1430
|
+
return;
|
|
1431
|
+
const runEnd = run[run.length - 1];
|
|
1432
|
+
if (!deletedIds.has(runEnd.nodeId))
|
|
1433
|
+
return;
|
|
1434
|
+
const tail = originalPenultimate ? endingOf(originalPenultimate).conjunction : null;
|
|
1435
|
+
const separator = tail ? tail.charAt(0) : ';';
|
|
1436
|
+
const closing = endingOf(runEnd).core || '.';
|
|
1437
|
+
const lastEnding = endingOf(last);
|
|
1438
|
+
if (lastEnding.core === ':')
|
|
1439
|
+
return;
|
|
1440
|
+
const needsClose = lastEnding.core !== closing && (lastEnding.conjunction != null || lastEnding.core === separator);
|
|
1441
|
+
const needsTail = penultimate != null && tail != null && endingOf(penultimate).core !== ':' && !endingOf(penultimate).conjunction;
|
|
1442
|
+
if (!needsClose && !(needsTail && lastEnding.core !== closing))
|
|
1443
|
+
return;
|
|
1444
|
+
const edits = [
|
|
1445
|
+
`end ${quote(last)} (nodeId ${last.nodeId}) with "${closing}${lastEnding.closers}" instead of "${lastEnding.real}"`,
|
|
1446
|
+
];
|
|
1447
|
+
if (needsTail)
|
|
1448
|
+
edits.push(change(penultimate, tail));
|
|
1449
|
+
entries.push({
|
|
1450
|
+
deleted: runEnd.nodeId,
|
|
1451
|
+
lastItem: last.nodeId,
|
|
1452
|
+
lastItemEnds: lastEnding.real,
|
|
1453
|
+
...(penultimate ? { penultimate: penultimate.nodeId, penultimateEnds: endingOf(penultimate).real } : {}),
|
|
1454
|
+
suggestion: `The run now ends at ${quote(last)}: ${edits.join(', and ')}, using replace_text.`,
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
return entries;
|
|
1458
|
+
}
|
|
1284
1459
|
/** Block node types `doc.blocks.delete` removes as a whole paragraph-shaped block. */
|
|
1285
1460
|
const DELETABLE_BLOCK_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']);
|
|
1286
1461
|
/**
|
|
@@ -1450,6 +1625,11 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1450
1625
|
// an error, so the `errors.length` term still catches a short request.
|
|
1451
1626
|
const allApplied = deleted.length === targets.length && errors$1.length === 0;
|
|
1452
1627
|
const verified = verification.every((v) => v.passed);
|
|
1628
|
+
let listPunctuation = [];
|
|
1629
|
+
try {
|
|
1630
|
+
listPunctuation = listPunctuationAfterDelete(pre.blocks, new Set(deleted.map((entry) => entry.nodeId)));
|
|
1631
|
+
}
|
|
1632
|
+
catch { }
|
|
1453
1633
|
return {
|
|
1454
1634
|
status: !verified || deleted.length === 0 ? 'failed' : allApplied ? 'ok' : 'partial',
|
|
1455
1635
|
intent: `delete_blocks: ${deleted.length} block(s)`,
|
|
@@ -1459,6 +1639,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1459
1639
|
executedOperations,
|
|
1460
1640
|
verification,
|
|
1461
1641
|
deletedBlocks: deleted,
|
|
1642
|
+
...(listPunctuation.length > 0 ? { listPunctuation } : {}),
|
|
1462
1643
|
...(errors$1.length ? { errors: errors$1 } : {}),
|
|
1463
1644
|
...(errors$1.length
|
|
1464
1645
|
? { nextStep: 'Re-inspect the document and retry the selectors that did not resolve to one block.' }
|
|
@@ -2713,7 +2894,7 @@ async function listAllTrackedChanges(listFn) {
|
|
|
2713
2894
|
const page = asRecord(await listFn({ offset, limit: PAGE, in: 'all' }));
|
|
2714
2895
|
const items = Array.isArray(page?.items) ? page.items : [];
|
|
2715
2896
|
for (const item of items) {
|
|
2716
|
-
if (isRecord(item))
|
|
2897
|
+
if (guards.isRecord(item))
|
|
2717
2898
|
out.push(item);
|
|
2718
2899
|
}
|
|
2719
2900
|
const total = asNumber(page?.total, out.length) ?? out.length;
|
|
@@ -4290,12 +4471,12 @@ function findTextTarget(snapshot, nodeId) {
|
|
|
4290
4471
|
return null;
|
|
4291
4472
|
}
|
|
4292
4473
|
function compactOpResult(result) {
|
|
4293
|
-
if (!isRecord(result))
|
|
4474
|
+
if (!guards.isRecord(result))
|
|
4294
4475
|
return result;
|
|
4295
4476
|
const compact = {};
|
|
4296
4477
|
if (typeof result.success === 'boolean')
|
|
4297
4478
|
compact.success = result.success;
|
|
4298
|
-
if (isRecord(result.revision))
|
|
4479
|
+
if (guards.isRecord(result.revision))
|
|
4299
4480
|
compact.revision = result.revision;
|
|
4300
4481
|
if (Array.isArray(result.steps))
|
|
4301
4482
|
compact.stepCount = result.steps.length;
|
|
@@ -4311,7 +4492,7 @@ function trackedChangeRefIds(result) {
|
|
|
4311
4492
|
}));
|
|
4312
4493
|
}
|
|
4313
4494
|
function parseScopedReplaceArgs(args, edits, changeMode, expectedRevision) {
|
|
4314
|
-
if (!isRecord(args.selector))
|
|
4495
|
+
if (!guards.isRecord(args.selector))
|
|
4315
4496
|
return null;
|
|
4316
4497
|
return {
|
|
4317
4498
|
edits,
|
|
@@ -4341,9 +4522,9 @@ async function fieldResultRanges(doc, nodeId, blockText) {
|
|
|
4341
4522
|
const limit = 500;
|
|
4342
4523
|
for (let offset = 0; offset < 100_000; offset += limit) {
|
|
4343
4524
|
const page = await fn({ limit, offset });
|
|
4344
|
-
const items = isRecord(page) && Array.isArray(page.items) ? page.items.filter(isRecord) : [];
|
|
4525
|
+
const items = guards.isRecord(page) && Array.isArray(page.items) ? page.items.filter(guards.isRecord) : [];
|
|
4345
4526
|
for (const item of items) {
|
|
4346
|
-
const address = isRecord(item.address) ? item.address : null;
|
|
4527
|
+
const address = guards.isRecord(item.address) ? item.address : null;
|
|
4347
4528
|
const inBody = typeof address?.storyId !== 'string' || address.storyId.startsWith('main:');
|
|
4348
4529
|
if (address?.blockId !== nodeId || !inBody)
|
|
4349
4530
|
continue;
|
|
@@ -4352,7 +4533,7 @@ async function fieldResultRanges(doc, nodeId, blockText) {
|
|
|
4352
4533
|
}
|
|
4353
4534
|
results.push(item.resolvedText);
|
|
4354
4535
|
}
|
|
4355
|
-
const total = isRecord(page) && typeof page.total === 'number' ? page.total : null;
|
|
4536
|
+
const total = guards.isRecord(page) && typeof page.total === 'number' ? page.total : null;
|
|
4356
4537
|
if (items.length < limit || (total != null && offset + limit >= total))
|
|
4357
4538
|
break;
|
|
4358
4539
|
}
|
|
@@ -4403,8 +4584,9 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4403
4584
|
occurrences: spans.length,
|
|
4404
4585
|
...(edit.occurrence != null ? { occurrence: edit.occurrence } : {}),
|
|
4405
4586
|
});
|
|
4406
|
-
for (const span of spans)
|
|
4407
|
-
allSpans.push({ ...span, replace: edit.replace, find: edit.find });
|
|
4587
|
+
for (const span of spans) {
|
|
4588
|
+
allSpans.push({ ...span, replace: edit.replace, find: edit.find, editIndex: perEdit.length - 1 });
|
|
4589
|
+
}
|
|
4408
4590
|
}
|
|
4409
4591
|
const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
|
|
4410
4592
|
const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
|
|
@@ -4441,6 +4623,23 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4441
4623
|
planned.push(span);
|
|
4442
4624
|
lastEnd = span.end;
|
|
4443
4625
|
}
|
|
4626
|
+
planned.forEach((span, at) => {
|
|
4627
|
+
if (span.replace.trim().length > 0)
|
|
4628
|
+
return;
|
|
4629
|
+
const text = block.text;
|
|
4630
|
+
if (text.charAt(span.start - 1) !== ' ' || text.charAt(span.end) !== ' ')
|
|
4631
|
+
return;
|
|
4632
|
+
if (text.charAt(span.start - 2) === ' ' || text.charAt(span.end + 1) === ' ')
|
|
4633
|
+
return;
|
|
4634
|
+
if (/[.!?]["'\u201d\u2019)\]]*$/.test(text.slice(0, span.start - 1)))
|
|
4635
|
+
return;
|
|
4636
|
+
if ((planned[at + 1]?.start ?? Infinity) <= span.end || (planned[at - 1]?.end ?? -1) >= span.start - 1)
|
|
4637
|
+
return;
|
|
4638
|
+
if (span.replace.length > 0)
|
|
4639
|
+
span.start -= 1;
|
|
4640
|
+
span.end += 1;
|
|
4641
|
+
perEdit[span.editIndex].collapsedSpace = true;
|
|
4642
|
+
});
|
|
4444
4643
|
// The block text once every planned span is replaced: the whole-block
|
|
4445
4644
|
// rewrite writes it, and exact-target writes must produce it.
|
|
4446
4645
|
const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
|
|
@@ -4493,11 +4692,11 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4493
4692
|
// that each one still covers the text matched in the snapshot, so a
|
|
4494
4693
|
// projection mismatch never rewrites other text.
|
|
4495
4694
|
const preview = await previewMutations(doc, steps, args.changeMode, pre.revision);
|
|
4496
|
-
const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(isRecord) : [];
|
|
4695
|
+
const previewSteps = Array.isArray(preview.steps) ? preview.steps.filter(guards.isRecord) : [];
|
|
4497
4696
|
const aligned = preview.valid === true &&
|
|
4498
4697
|
steps.every((step, index) => {
|
|
4499
4698
|
const resolutions = previewSteps.find((candidate) => candidate.stepId === step.id)?.resolutions;
|
|
4500
|
-
const resolved = Array.isArray(resolutions) && isRecord(resolutions[0]) ? resolutions[0].text : undefined;
|
|
4699
|
+
const resolved = Array.isArray(resolutions) && guards.isRecord(resolutions[0]) ? resolutions[0].text : undefined;
|
|
4501
4700
|
const span = targetedSpans[index];
|
|
4502
4701
|
return resolved === block.text.slice(span.start, span.end);
|
|
4503
4702
|
});
|
|
@@ -4530,7 +4729,7 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4530
4729
|
// atomic history step. Only do so while the document is still at the
|
|
4531
4730
|
// revision this plan produced, so the undo cannot reach any other change.
|
|
4532
4731
|
let rolledBack = false;
|
|
4533
|
-
const appliedRevision = isRecord(result) && isRecord(result.revision) ? result.revision.after : undefined;
|
|
4732
|
+
const appliedRevision = guards.isRecord(result) && guards.isRecord(result.revision) ? result.revision.after : undefined;
|
|
4534
4733
|
if (wroteExactTargets &&
|
|
4535
4734
|
postBlock != null &&
|
|
4536
4735
|
postBlock.text !== expected &&
|
|
@@ -4645,13 +4844,13 @@ function markRunPropsFromMarks(marks) {
|
|
|
4645
4844
|
* paragraphIndentOf. Returns null when the paragraph mark carries nothing.
|
|
4646
4845
|
*/
|
|
4647
4846
|
function paragraphMarkRunPropsOf(payload) {
|
|
4648
|
-
const node = isRecord(payload) ? payload.node : null;
|
|
4649
|
-
if (!isRecord(node))
|
|
4847
|
+
const node = guards.isRecord(payload) ? payload.node : null;
|
|
4848
|
+
if (!guards.isRecord(node))
|
|
4650
4849
|
return null;
|
|
4651
|
-
const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
|
|
4850
|
+
const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
|
|
4652
4851
|
for (const props of candidates) {
|
|
4653
|
-
const mark = isRecord(props) ? props.markRunProps : null;
|
|
4654
|
-
if (isRecord(mark) && Object.keys(mark).length > 0)
|
|
4852
|
+
const mark = guards.isRecord(props) ? props.markRunProps : null;
|
|
4853
|
+
if (guards.isRecord(mark) && Object.keys(mark).length > 0)
|
|
4655
4854
|
return mark;
|
|
4656
4855
|
}
|
|
4657
4856
|
return null;
|
|
@@ -4727,6 +4926,8 @@ function leadInShapeOf(blockText, labelText, quote) {
|
|
|
4727
4926
|
}
|
|
4728
4927
|
/** The span of `text` that carries the label look, or null when it has none. */
|
|
4729
4928
|
function leadInSpanOf(text, shape) {
|
|
4929
|
+
if (shape.kind === 'typed')
|
|
4930
|
+
return typedCaptionSpanOf(text, shape.delimiter);
|
|
4730
4931
|
if (shape.kind === 'word') {
|
|
4731
4932
|
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
4732
4933
|
// "The Parties further agree…" does not (no guessing a label there).
|
|
@@ -4761,6 +4962,81 @@ function leadInSpanOf(text, shape) {
|
|
|
4761
4962
|
const end = leadInEndOf(text, shape.delimiter);
|
|
4762
4963
|
return end == null ? null : { start: 0, end };
|
|
4763
4964
|
}
|
|
4965
|
+
const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
|
|
4966
|
+
const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
|
|
4967
|
+
const TYPED_MARKER_PREFIX = new RegExp(String.raw `^[ \t]*(${TYPED_MARKER_TOKEN})\t[\t ]*`);
|
|
4968
|
+
const LEADING_MARKER_TOKEN = new RegExp(String.raw `^\s*(${TYPED_MARKER_TOKEN})(?=\s|$)`);
|
|
4969
|
+
function typedMarkerPrefixOf(text) {
|
|
4970
|
+
const match = TYPED_MARKER_PREFIX.exec(text);
|
|
4971
|
+
return match ? { prefix: match[0], marker: match[1] } : null;
|
|
4972
|
+
}
|
|
4973
|
+
function markerShapeOf(text) {
|
|
4974
|
+
const match = LEADING_MARKER_TOKEN.exec(text);
|
|
4975
|
+
if (!match)
|
|
4976
|
+
return null;
|
|
4977
|
+
const token = match[1];
|
|
4978
|
+
const wrapper = token.startsWith('(') ? 'paren' : token.endsWith('.') ? 'period' : 'close';
|
|
4979
|
+
const core = token.replace(/^\(/, '').replace(/[.)]$/, '');
|
|
4980
|
+
let kind;
|
|
4981
|
+
if (/^\d/.test(core))
|
|
4982
|
+
kind = 'digit';
|
|
4983
|
+
else if (/^[ivx]+$/.test(core))
|
|
4984
|
+
kind = 'lower-roman';
|
|
4985
|
+
else if (/^[IVX]+$/.test(core))
|
|
4986
|
+
kind = 'upper-roman';
|
|
4987
|
+
else if (/^[a-z]+$/.test(core))
|
|
4988
|
+
kind = 'lower-alpha';
|
|
4989
|
+
else if (/^[A-Z]+$/.test(core))
|
|
4990
|
+
kind = 'upper-alpha';
|
|
4991
|
+
else
|
|
4992
|
+
return null;
|
|
4993
|
+
return { wrapper, kind };
|
|
4994
|
+
}
|
|
4995
|
+
function sameMarkerShape(a, b) {
|
|
4996
|
+
return a != null && b != null && a.wrapper === b.wrapper && a.kind === b.kind;
|
|
4997
|
+
}
|
|
4998
|
+
function markerShapeFits(reference, created) {
|
|
4999
|
+
if (!reference || reference.wrapper !== created.wrapper)
|
|
5000
|
+
return false;
|
|
5001
|
+
if (reference.kind === created.kind)
|
|
5002
|
+
return true;
|
|
5003
|
+
return ((created.kind === 'lower-roman' && reference.kind === 'lower-alpha') ||
|
|
5004
|
+
(created.kind === 'upper-roman' && reference.kind === 'upper-alpha'));
|
|
5005
|
+
}
|
|
5006
|
+
function continuesAlphabet(referenceText, createdText, referenceBefore) {
|
|
5007
|
+
const letterOf = (text) => /^[a-zA-Z]$/.exec(LEADING_MARKER_TOKEN.exec(text)?.[1]?.replace(/[().]/g, '') ?? '')?.[0];
|
|
5008
|
+
const reference = letterOf(referenceText);
|
|
5009
|
+
const created = letterOf(createdText);
|
|
5010
|
+
return (reference != null &&
|
|
5011
|
+
created != null &&
|
|
5012
|
+
created.charCodeAt(0) - reference.charCodeAt(0) === (referenceBefore ? 1 : -1));
|
|
5013
|
+
}
|
|
5014
|
+
const TYPED_CAPTION_MAX_CHARS = 60;
|
|
5015
|
+
function isTitleLike(caption) {
|
|
5016
|
+
const words = caption.match(/\p{L}+/gu) ?? [];
|
|
5017
|
+
const capitalised = words.filter((word) => /^\p{Lu}/u.test(word)).length;
|
|
5018
|
+
return (words.length > 0 &&
|
|
5019
|
+
words.every((word) => word.length < 4 || /^\p{Lu}/u.test(word)) &&
|
|
5020
|
+
capitalised * 2 >= words.length);
|
|
5021
|
+
}
|
|
5022
|
+
function typedCaptionSpanOf(text, delimiter = '.') {
|
|
5023
|
+
const typed = typedMarkerPrefixOf(text);
|
|
5024
|
+
const mark = delimiter.trim();
|
|
5025
|
+
if (!typed || !mark)
|
|
5026
|
+
return null;
|
|
5027
|
+
const start = typed.prefix.length;
|
|
5028
|
+
for (let at = text.indexOf(mark, start); at >= 0; at = text.indexOf(mark, at + 1)) {
|
|
5029
|
+
const end = at + mark.length;
|
|
5030
|
+
if (end - start > TYPED_CAPTION_MAX_CHARS)
|
|
5031
|
+
return null;
|
|
5032
|
+
if (at === start || !/\s/.test(text.charAt(end)))
|
|
5033
|
+
continue;
|
|
5034
|
+
if (!isTitleLike(text.slice(start, at)))
|
|
5035
|
+
return null;
|
|
5036
|
+
return text.slice(end).trim().length > 0 ? { start, end } : null;
|
|
5037
|
+
}
|
|
5038
|
+
return null;
|
|
5039
|
+
}
|
|
4764
5040
|
/**
|
|
4765
5041
|
* Cut `text` at `end` without splitting a surrogate pair: half an emoji is not
|
|
4766
5042
|
* document text, and `query.match` finds nothing when handed a lone surrogate —
|
|
@@ -4837,7 +5113,10 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4837
5113
|
// Window: through the label's delimiter plus enough of the sentence to sample
|
|
4838
5114
|
// it, or a plain head when the item carries no label at all.
|
|
4839
5115
|
const delimiterAt = leadInEndOf(text);
|
|
4840
|
-
|
|
5116
|
+
let windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
|
|
5117
|
+
const typedCaption = typedCaptionSpanOf(text);
|
|
5118
|
+
if (typedCaption)
|
|
5119
|
+
windowEnd = Math.max(windowEnd, typedCaption.end + LEAD_IN_BODY_SAMPLE_CHARS);
|
|
4841
5120
|
const pattern = sliceWholeChars(text, windowEnd);
|
|
4842
5121
|
const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
|
|
4843
5122
|
const items = Array.isArray(result?.items) ? result.items : [];
|
|
@@ -4851,6 +5130,9 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4851
5130
|
const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
|
|
4852
5131
|
if (runs.length === 0)
|
|
4853
5132
|
continue;
|
|
5133
|
+
const typedLead = typedPrefixLabelOf(runs, runTextOf);
|
|
5134
|
+
if (typedLead)
|
|
5135
|
+
return typedLead;
|
|
4854
5136
|
const blockText = asString(rec?.text, '') ?? '';
|
|
4855
5137
|
// A quoted defined term keeps its opening quote in a run of its own —
|
|
4856
5138
|
// `"` + **Deliverables** + `" means …` — so the label is the SECOND run
|
|
@@ -4886,6 +5168,59 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4886
5168
|
}
|
|
4887
5169
|
return null;
|
|
4888
5170
|
}
|
|
5171
|
+
function typedPrefixLabelOf(runs, runTextOf) {
|
|
5172
|
+
const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
|
|
5173
|
+
if (!typed)
|
|
5174
|
+
return null;
|
|
5175
|
+
const markerEnd = typed.prefix.indexOf(typed.marker) + typed.marker.length;
|
|
5176
|
+
let covered = 0;
|
|
5177
|
+
let labelIndex = 0;
|
|
5178
|
+
while (labelIndex < runs.length && covered + runTextOf(runs[labelIndex]).length <= typed.prefix.length) {
|
|
5179
|
+
covered += runTextOf(runs[labelIndex]).length;
|
|
5180
|
+
labelIndex += 1;
|
|
5181
|
+
}
|
|
5182
|
+
if (covered < markerEnd || labelIndex + 1 >= runs.length)
|
|
5183
|
+
return null;
|
|
5184
|
+
const underlined = (run) => asRecord(asRecord(asRecord(run)?.styles)?.effective)?.underline === true;
|
|
5185
|
+
const emphasisOf = (run) => {
|
|
5186
|
+
const style = matchRunStyle(run);
|
|
5187
|
+
return `${style.bold}/${style.italic}/${underlined(run)}`;
|
|
5188
|
+
};
|
|
5189
|
+
const label = runs[labelIndex];
|
|
5190
|
+
if (emphasisOf(label) === 'false/false/false')
|
|
5191
|
+
return null;
|
|
5192
|
+
const isBlank = (run) => runTextOf(run).trim().length === 0;
|
|
5193
|
+
const continuesLabel = (index) => {
|
|
5194
|
+
if (emphasisOf(runs[index]) === emphasisOf(label))
|
|
5195
|
+
return true;
|
|
5196
|
+
let next = index;
|
|
5197
|
+
while (next < runs.length && isBlank(runs[next]))
|
|
5198
|
+
next += 1;
|
|
5199
|
+
return next > index && next < runs.length && emphasisOf(runs[next]) === emphasisOf(label);
|
|
5200
|
+
};
|
|
5201
|
+
let bodyIndex = labelIndex + 1;
|
|
5202
|
+
while (bodyIndex < runs.length && continuesLabel(bodyIndex))
|
|
5203
|
+
bodyIndex += 1;
|
|
5204
|
+
while (bodyIndex < runs.length - 1 && isBlank(runs[bodyIndex]))
|
|
5205
|
+
bodyIndex += 1;
|
|
5206
|
+
if (bodyIndex >= runs.length)
|
|
5207
|
+
return null;
|
|
5208
|
+
const labelText = runs.slice(labelIndex, bodyIndex).map(runTextOf).join('').trim();
|
|
5209
|
+
if (labelText.length === 0 || labelText.length > LEAD_IN_MAX_CHARS)
|
|
5210
|
+
return null;
|
|
5211
|
+
const delimiter = ['.', ...LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
|
|
5212
|
+
if (!delimiter)
|
|
5213
|
+
return null;
|
|
5214
|
+
const body = runs[bodyIndex];
|
|
5215
|
+
const leadStyle = matchRunStyle(label);
|
|
5216
|
+
const bodyStyle = matchRunStyle(body);
|
|
5217
|
+
const leadUnderline = underlined(label);
|
|
5218
|
+
const bodyUnderline = underlined(body);
|
|
5219
|
+
const look = inlineFromMatchStyle(leadStyle, bodyStyle);
|
|
5220
|
+
if (leadUnderline || bodyUnderline)
|
|
5221
|
+
look.underline = leadUnderline;
|
|
5222
|
+
return { body: inlineFromMatchStyle(bodyStyle), lead: { look, shape: { kind: 'typed', delimiter } } };
|
|
5223
|
+
}
|
|
4889
5224
|
/**
|
|
4890
5225
|
* Pull a paragraph's direct indentation out of a `getNodeById` payload and
|
|
4891
5226
|
* convert it to the twips that `format.paragraph.setIndentation` expects.
|
|
@@ -4896,13 +5231,13 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4896
5231
|
* indent copied" rather than a thrown read.
|
|
4897
5232
|
*/
|
|
4898
5233
|
function paragraphIndentOf(payload) {
|
|
4899
|
-
const node = isRecord(payload) ? payload.node : null;
|
|
4900
|
-
if (!isRecord(node))
|
|
5234
|
+
const node = guards.isRecord(payload) ? payload.node : null;
|
|
5235
|
+
if (!guards.isRecord(node))
|
|
4901
5236
|
return null;
|
|
4902
|
-
const candidates = [isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
|
|
5237
|
+
const candidates = [guards.isRecord(node.paragraph) ? node.paragraph.props : null, node.props];
|
|
4903
5238
|
for (const props of candidates) {
|
|
4904
|
-
const indent = isRecord(props) ? props.indent : null;
|
|
4905
|
-
if (!isRecord(indent))
|
|
5239
|
+
const indent = guards.isRecord(props) ? props.indent : null;
|
|
5240
|
+
if (!guards.isRecord(indent))
|
|
4906
5241
|
continue;
|
|
4907
5242
|
const twips = {};
|
|
4908
5243
|
for (const key of ['left', 'right', 'firstLine', 'hanging']) {
|
|
@@ -4979,7 +5314,7 @@ function marksToInline(marks) {
|
|
|
4979
5314
|
}
|
|
4980
5315
|
/** Parse a raw agent mark bag, keeping only recognized keys with valid types. */
|
|
4981
5316
|
function normalizeMarks(raw) {
|
|
4982
|
-
if (!isRecord(raw))
|
|
5317
|
+
if (!guards.isRecord(raw))
|
|
4983
5318
|
return undefined;
|
|
4984
5319
|
const m = {};
|
|
4985
5320
|
if (typeof raw.bold === 'boolean')
|
|
@@ -5009,7 +5344,7 @@ function normalizeMarks(raw) {
|
|
|
5009
5344
|
function normalizeItemSpec(raw) {
|
|
5010
5345
|
if (typeof raw === 'string')
|
|
5011
5346
|
return { text: raw, level: 0 };
|
|
5012
|
-
if (!isRecord(raw))
|
|
5347
|
+
if (!guards.isRecord(raw))
|
|
5013
5348
|
return null;
|
|
5014
5349
|
const spec = {};
|
|
5015
5350
|
const lvl = asNumber(raw.level);
|
|
@@ -5023,7 +5358,7 @@ function normalizeItemSpec(raw) {
|
|
|
5023
5358
|
if (typeof r === 'string') {
|
|
5024
5359
|
runs.push({ text: r });
|
|
5025
5360
|
}
|
|
5026
|
-
else if (isRecord(r) && typeof r.text === 'string') {
|
|
5361
|
+
else if (guards.isRecord(r) && typeof r.text === 'string') {
|
|
5027
5362
|
const rm = normalizeMarks(r.marks);
|
|
5028
5363
|
runs.push(rm ? { text: r.text, marks: rm } : { text: r.text });
|
|
5029
5364
|
}
|
|
@@ -5642,9 +5977,9 @@ async function appendListAtPlacement(doc, args) {
|
|
|
5642
5977
|
const items = Array.isArray(args.items)
|
|
5643
5978
|
? args.items.filter((t) => typeof t === 'string' && t.length > 0)
|
|
5644
5979
|
: [];
|
|
5645
|
-
const placement = isRecord(args.placement) ? args.placement : null;
|
|
5980
|
+
const placement = guards.isRecord(args.placement) ? args.placement : null;
|
|
5646
5981
|
const at = placement && typeof placement.at === 'string' ? placement.at : null;
|
|
5647
|
-
const selector = placement && isRecord(placement.selector) ? placement.selector : null;
|
|
5982
|
+
const selector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
|
|
5648
5983
|
if (items.length === 0 || !placement || (at !== 'after' && at !== 'before') || !selector)
|
|
5649
5984
|
return null;
|
|
5650
5985
|
const pre = await docSnapshot.buildMutationSnapshot(doc, { includeDomains: ['blocks'] });
|
|
@@ -5764,6 +6099,7 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5764
6099
|
}
|
|
5765
6100
|
if (!reference)
|
|
5766
6101
|
return null;
|
|
6102
|
+
reference = typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatibleHeading);
|
|
5767
6103
|
const applied = {};
|
|
5768
6104
|
// Numbering: when the reference participates in a numbering scheme and
|
|
5769
6105
|
// the new block does not, attach it to the same sequence — this is what
|
|
@@ -5789,7 +6125,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5789
6125
|
}
|
|
5790
6126
|
}
|
|
5791
6127
|
if (reference.styleId && reference.styleId !== created.styleId) {
|
|
5792
|
-
const
|
|
6128
|
+
const keepRunMarks = !allowLeadPattern;
|
|
6129
|
+
const setStyleFn = (keepRunMarks ? maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']) : null) ??
|
|
6130
|
+
maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
|
|
5793
6131
|
if (setStyleFn) {
|
|
5794
6132
|
const target = { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId };
|
|
5795
6133
|
let canCopyStyle = true;
|
|
@@ -5892,6 +6230,42 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5892
6230
|
return { matchedSibling: null, skipped: reasonOf(err) };
|
|
5893
6231
|
}
|
|
5894
6232
|
}
|
|
6233
|
+
function typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatible) {
|
|
6234
|
+
const typed = createdText ? typedMarkerPrefixOf(createdText) : null;
|
|
6235
|
+
if (!typed || createdText.slice(typed.prefix.length).trim().length === 0)
|
|
6236
|
+
return reference;
|
|
6237
|
+
const shape = markerShapeOf(createdText);
|
|
6238
|
+
const referenceShape = markerShapeOf(reference.textPreview ?? '');
|
|
6239
|
+
if (!shape || sameMarkerShape(referenceShape, shape))
|
|
6240
|
+
return reference;
|
|
6241
|
+
const usable = (row) => !!row && preIds.has(row.nodeId) && row.nodeType === created.nodeType && !row.isEmpty && compatible(row);
|
|
6242
|
+
const before = postBlocks.slice(0, createdIndex).reverse().find(usable);
|
|
6243
|
+
const after = postBlocks.slice(createdIndex + 1).find(usable);
|
|
6244
|
+
const continued = [reference, before, after].find((row) => !!row &&
|
|
6245
|
+
markerShapeFits(markerShapeOf(row.textPreview ?? ''), shape) &&
|
|
6246
|
+
continuesAlphabet(row.textPreview ?? '', createdText, postBlocks.findIndex((b) => b.nodeId === row.nodeId) < createdIndex));
|
|
6247
|
+
if (continued)
|
|
6248
|
+
return continued;
|
|
6249
|
+
const candidate = (row) => {
|
|
6250
|
+
if (!usable(row))
|
|
6251
|
+
return null;
|
|
6252
|
+
const preview = row.textPreview ?? '';
|
|
6253
|
+
return typedMarkerPrefixOf(preview) ? markerShapeOf(preview) : null;
|
|
6254
|
+
};
|
|
6255
|
+
const nearest = (accepts) => {
|
|
6256
|
+
for (let distance = 1; distance < postBlocks.length; distance += 1) {
|
|
6257
|
+
for (const index of [createdIndex - distance, createdIndex + distance]) {
|
|
6258
|
+
const row = postBlocks[index];
|
|
6259
|
+
if (index >= 0 && accepts(candidate(row)))
|
|
6260
|
+
return row;
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
return null;
|
|
6264
|
+
};
|
|
6265
|
+
return (nearest((rowShape) => sameMarkerShape(rowShape, shape)) ??
|
|
6266
|
+
nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
|
|
6267
|
+
reference);
|
|
6268
|
+
}
|
|
5895
6269
|
/** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
|
|
5896
6270
|
const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
5897
6271
|
/**
|
|
@@ -6022,7 +6396,8 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6022
6396
|
const span = leadInSpanOf(fullText, pattern.lead.shape);
|
|
6023
6397
|
// A uniform reference has no body look and no delimiter of its own: its look
|
|
6024
6398
|
// belongs to the whole created block, not to a label span inside it.
|
|
6025
|
-
const range = span ??
|
|
6399
|
+
const range = span ??
|
|
6400
|
+
(Object.keys(pattern.body).length === 0 && pattern.lead.shape.kind !== 'typed' ? { start: 0, end } : null);
|
|
6026
6401
|
if (range && (await applyInlineQuiet(formatFn, { blockId: created.nodeId, ...range, inline: pattern.lead.look }))) {
|
|
6027
6402
|
applied.lead = { ...pattern.lead.look, range: [range.start, range.end] };
|
|
6028
6403
|
}
|
|
@@ -6042,15 +6417,15 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6042
6417
|
return receipt;
|
|
6043
6418
|
// The placement anchor, when the action targeted one, is the user's own
|
|
6044
6419
|
// statement of "blend in with THIS" — prefer it over type-matched siblings.
|
|
6045
|
-
const placement = isRecord(args?.placement) ? args.placement : null;
|
|
6046
|
-
const anchorSelector = placement && isRecord(placement.selector) ? placement.selector : null;
|
|
6420
|
+
const placement = guards.isRecord(args?.placement) ? args.placement : null;
|
|
6421
|
+
const anchorSelector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
|
|
6047
6422
|
const anchorId = anchorSelector && typeof anchorSelector.nodeId === 'string' ? anchorSelector.nodeId : null;
|
|
6048
6423
|
const anchorRow = anchorId ? (postBlocks.find((b) => b.nodeId === anchorId) ?? null) : null;
|
|
6049
6424
|
// When the caller styled any entry explicitly (runs/marks), it took
|
|
6050
6425
|
// ownership of intra-paragraph formatting — the automatic lead-marker
|
|
6051
6426
|
// pattern pass must not fight it.
|
|
6052
|
-
const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
|
|
6053
|
-
const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6427
|
+
const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
|
|
6428
|
+
const hasExplicitStyling = rawTexts.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6054
6429
|
const insertedTexts = rawTexts.map((entry) => {
|
|
6055
6430
|
if (typeof entry === 'string')
|
|
6056
6431
|
return entry;
|
|
@@ -6075,7 +6450,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6075
6450
|
// the previous NAME line, not like the "Name:" line directly above it.
|
|
6076
6451
|
// Only tried when matching against the neighbour found nothing, so this
|
|
6077
6452
|
// cannot change a case that already had an answer.
|
|
6078
|
-
const applied = isRecord(result?.applied) ? result.applied : null;
|
|
6453
|
+
const applied = guards.isRecord(result?.applied) ? result.applied : null;
|
|
6079
6454
|
if (createdRows.length > 1 && (!applied || Object.keys(applied).length === 0)) {
|
|
6080
6455
|
const parallel = postBlocks[createdIndex - createdRows.length];
|
|
6081
6456
|
if (parallel && preIds.has(parallel.nodeId) && !parallel.isEmpty) {
|
|
@@ -6250,7 +6625,15 @@ async function runAttachNumbering(doc, args) {
|
|
|
6250
6625
|
errors: [
|
|
6251
6626
|
{
|
|
6252
6627
|
code: 'TARGET_NOT_FOUND',
|
|
6253
|
-
message:
|
|
6628
|
+
message: available.length > 0
|
|
6629
|
+
? `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`
|
|
6630
|
+
: rows.some((r) => typedMarkerPrefixOf(r.textPreview ?? '') != null)
|
|
6631
|
+
? `no block renders marker ${JSON.stringify(likeMarker)}: this document has no numbering scheme; its clause numbers are typed text. ` +
|
|
6632
|
+
`Copy the sibling's typed prefix (tab, next number, tab) into the paragraph text with insert_paragraphs or replace_text, ` +
|
|
6633
|
+
`then renumber the following typed markers with replace_text.`
|
|
6634
|
+
: `no block renders marker ${JSON.stringify(likeMarker)}: no block in this document carries numbering. ` +
|
|
6635
|
+
`To add a paragraph that matches its neighbours use insert_paragraphs next to them; ` +
|
|
6636
|
+
`to start a numbered list use append_list or convert_list.`,
|
|
6254
6637
|
recovery: { kind: 'reinspect' },
|
|
6255
6638
|
},
|
|
6256
6639
|
],
|
|
@@ -6291,7 +6674,7 @@ async function runAttachNumbering(doc, args) {
|
|
|
6291
6674
|
};
|
|
6292
6675
|
}
|
|
6293
6676
|
async function dispatchSuperdocPerformAction(doc, args) {
|
|
6294
|
-
if (!isRecord(args)) {
|
|
6677
|
+
if (!guards.isRecord(args)) {
|
|
6295
6678
|
throw new errors.SuperDocCliError('superdoc_perform_action arguments must be an object', {
|
|
6296
6679
|
code: 'INVALID_ARGUMENT',
|
|
6297
6680
|
});
|
|
@@ -6365,7 +6748,7 @@ async function dispatchSuperdocPerformAction(doc, args) {
|
|
|
6365
6748
|
if (expectedRevision !== undefined && (typeof expectedRevision !== 'string' || expectedRevision.length === 0)) {
|
|
6366
6749
|
throw new errors.SuperDocCliError('replace_text: expectedRevision must be a non-empty string from this live document handle', { code: 'INVALID_ARGUMENT' });
|
|
6367
6750
|
}
|
|
6368
|
-
if (edits.some((edit) => edit.occurrence != null) && !isRecord(args.selector)) {
|
|
6751
|
+
if (edits.some((edit) => edit.occurrence != null) && !guards.isRecord(args.selector)) {
|
|
6369
6752
|
throw new errors.SuperDocCliError('replace_text: explicit occurrence requires a selector resolving to one block', {
|
|
6370
6753
|
code: 'INVALID_ARGUMENT',
|
|
6371
6754
|
});
|
|
@@ -6995,13 +7378,13 @@ function parseExactTrackedChangeIdList(value) {
|
|
|
6995
7378
|
}
|
|
6996
7379
|
async function superdocPerformAction(doc, args) {
|
|
6997
7380
|
try {
|
|
6998
|
-
const changeMode = isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
|
|
7381
|
+
const changeMode = guards.isRecord(args) ? parseChangeMode(args.changeMode) : undefined;
|
|
6999
7382
|
return await dispatchSuperdocPerformAction(changeMode ? actionChangeMode.bindActionChangeMode(doc, changeMode) : doc, args);
|
|
7000
7383
|
}
|
|
7001
7384
|
catch (error) {
|
|
7002
7385
|
if (!(error instanceof docSnapshot.MutationSnapshotError))
|
|
7003
7386
|
throw error;
|
|
7004
|
-
const action = isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
|
|
7387
|
+
const action = guards.isRecord(args) && isActionName(args.action) ? args.action : 'superdoc_perform_action';
|
|
7005
7388
|
return {
|
|
7006
7389
|
status: 'failed',
|
|
7007
7390
|
intent: action,
|
|
@@ -7062,7 +7445,7 @@ function parseEdits(value) {
|
|
|
7062
7445
|
return undefined;
|
|
7063
7446
|
const result = [];
|
|
7064
7447
|
for (const entry of value) {
|
|
7065
|
-
if (!isRecord(entry))
|
|
7448
|
+
if (!guards.isRecord(entry))
|
|
7066
7449
|
return undefined;
|
|
7067
7450
|
const find = asString(entry.find);
|
|
7068
7451
|
const replace = asString(entry.replace) ?? '';
|
|
@@ -7082,7 +7465,7 @@ function parseEdits(value) {
|
|
|
7082
7465
|
function parseCellCandidate(cell) {
|
|
7083
7466
|
if (typeof cell === 'string')
|
|
7084
7467
|
return cell;
|
|
7085
|
-
if (isRecord(cell)) {
|
|
7468
|
+
if (guards.isRecord(cell)) {
|
|
7086
7469
|
const spec = normalizeItemSpec(cell);
|
|
7087
7470
|
if (spec)
|
|
7088
7471
|
return { text: spec.text, runs: spec.runs, marks: spec.marks };
|
|
@@ -7110,7 +7493,7 @@ function parsePlaceholderFields(value) {
|
|
|
7110
7493
|
return undefined;
|
|
7111
7494
|
const result = [];
|
|
7112
7495
|
for (const entry of value) {
|
|
7113
|
-
if (!isRecord(entry) || typeof entry.value !== 'string')
|
|
7496
|
+
if (!guards.isRecord(entry) || typeof entry.value !== 'string')
|
|
7114
7497
|
return undefined;
|
|
7115
7498
|
const label = asString(entry.label);
|
|
7116
7499
|
result.push(label ? { label, value: entry.value } : { value: entry.value });
|
|
@@ -7118,7 +7501,7 @@ function parsePlaceholderFields(value) {
|
|
|
7118
7501
|
return result;
|
|
7119
7502
|
}
|
|
7120
7503
|
function parsePlacement(value) {
|
|
7121
|
-
if (!isRecord(value))
|
|
7504
|
+
if (!guards.isRecord(value))
|
|
7122
7505
|
return undefined;
|
|
7123
7506
|
const at = asString(value.at);
|
|
7124
7507
|
if (at === 'document_end' || at === 'document_start') {
|
|
@@ -7133,7 +7516,7 @@ function parsePlacement(value) {
|
|
|
7133
7516
|
return undefined;
|
|
7134
7517
|
}
|
|
7135
7518
|
function parseSelector(value) {
|
|
7136
|
-
if (!isRecord(value))
|
|
7519
|
+
if (!guards.isRecord(value))
|
|
7137
7520
|
return undefined;
|
|
7138
7521
|
const kind = asString(value.kind);
|
|
7139
7522
|
if (kind === 'nodeId') {
|
|
@@ -7260,4 +7643,7 @@ exports.ACTION_NAMES_LIST = ACTION_NAMES_LIST;
|
|
|
7260
7643
|
exports.getActionDispositions = getActionDispositions;
|
|
7261
7644
|
exports.getUnavailableTrackedActions = getUnavailableTrackedActions;
|
|
7262
7645
|
exports.isActionName = isActionName;
|
|
7646
|
+
exports.markerShapeOf = markerShapeOf;
|
|
7263
7647
|
exports.superdocPerformAction = superdocPerformAction;
|
|
7648
|
+
exports.typedCaptionSpanOf = typedCaptionSpanOf;
|
|
7649
|
+
exports.typedMarkerPrefixOf = typedMarkerPrefixOf;
|