@superdoc/sdk 2.23.0-next.12 → 2.23.0-next.13
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 +396 -8
- package/dist/agent/actions.d.ts +13 -0
- package/dist/agent/actions.js +393 -8
- package/dist/embedded-prompts.generated.cjs +1 -1
- package/dist/embedded-prompts.generated.js +1 -1
- 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/generation-receipt.json +5 -5
package/dist/agent/actions.cjs
CHANGED
|
@@ -878,6 +878,7 @@ async function styleTableCells(doc, cells, changeMode) {
|
|
|
878
878
|
await applyItemStyling(doc, nodeIds, specs, changeMode);
|
|
879
879
|
return styled.length;
|
|
880
880
|
}
|
|
881
|
+
const TYPED_SIBLING_WINDOW = 20;
|
|
881
882
|
async function runInsertParagraphs(doc, args, formatArgs) {
|
|
882
883
|
const context = new executionContext.ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
|
|
883
884
|
try {
|
|
@@ -890,7 +891,7 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
890
891
|
kind: 'placement',
|
|
891
892
|
at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
|
|
892
893
|
});
|
|
893
|
-
|
|
894
|
+
let preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
|
|
894
895
|
let currentPlacement = placement;
|
|
895
896
|
const nodeIds = [];
|
|
896
897
|
const specs = [];
|
|
@@ -930,6 +931,16 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
930
931
|
if (!pre.complete) {
|
|
931
932
|
const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
|
|
932
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
|
+
}
|
|
933
944
|
}
|
|
934
945
|
const count = context.executedOperations.length;
|
|
935
946
|
formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
|
|
@@ -1279,6 +1290,172 @@ async function runDeleteText(doc, args) {
|
|
|
1279
1290
|
return failedReceipt('delete_text', err, pre);
|
|
1280
1291
|
}
|
|
1281
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
|
+
}
|
|
1282
1459
|
/** Block node types `doc.blocks.delete` removes as a whole paragraph-shaped block. */
|
|
1283
1460
|
const DELETABLE_BLOCK_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']);
|
|
1284
1461
|
/**
|
|
@@ -1448,6 +1625,11 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1448
1625
|
// an error, so the `errors.length` term still catches a short request.
|
|
1449
1626
|
const allApplied = deleted.length === targets.length && errors$1.length === 0;
|
|
1450
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 { }
|
|
1451
1633
|
return {
|
|
1452
1634
|
status: !verified || deleted.length === 0 ? 'failed' : allApplied ? 'ok' : 'partial',
|
|
1453
1635
|
intent: `delete_blocks: ${deleted.length} block(s)`,
|
|
@@ -1457,6 +1639,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1457
1639
|
executedOperations,
|
|
1458
1640
|
verification,
|
|
1459
1641
|
deletedBlocks: deleted,
|
|
1642
|
+
...(listPunctuation.length > 0 ? { listPunctuation } : {}),
|
|
1460
1643
|
...(errors$1.length ? { errors: errors$1 } : {}),
|
|
1461
1644
|
...(errors$1.length
|
|
1462
1645
|
? { nextStep: 'Re-inspect the document and retry the selectors that did not resolve to one block.' }
|
|
@@ -4401,8 +4584,9 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4401
4584
|
occurrences: spans.length,
|
|
4402
4585
|
...(edit.occurrence != null ? { occurrence: edit.occurrence } : {}),
|
|
4403
4586
|
});
|
|
4404
|
-
for (const span of spans)
|
|
4405
|
-
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
|
+
}
|
|
4406
4590
|
}
|
|
4407
4591
|
const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
|
|
4408
4592
|
const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
|
|
@@ -4439,6 +4623,23 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4439
4623
|
planned.push(span);
|
|
4440
4624
|
lastEnd = span.end;
|
|
4441
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
|
+
});
|
|
4442
4643
|
// The block text once every planned span is replaced: the whole-block
|
|
4443
4644
|
// rewrite writes it, and exact-target writes must produce it.
|
|
4444
4645
|
const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
|
|
@@ -4725,6 +4926,8 @@ function leadInShapeOf(blockText, labelText, quote) {
|
|
|
4725
4926
|
}
|
|
4726
4927
|
/** The span of `text` that carries the label look, or null when it has none. */
|
|
4727
4928
|
function leadInSpanOf(text, shape) {
|
|
4929
|
+
if (shape.kind === 'typed')
|
|
4930
|
+
return typedCaptionSpanOf(text, shape.delimiter);
|
|
4728
4931
|
if (shape.kind === 'word') {
|
|
4729
4932
|
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
4730
4933
|
// "The Parties further agree…" does not (no guessing a label there).
|
|
@@ -4759,6 +4962,81 @@ function leadInSpanOf(text, shape) {
|
|
|
4759
4962
|
const end = leadInEndOf(text, shape.delimiter);
|
|
4760
4963
|
return end == null ? null : { start: 0, end };
|
|
4761
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
|
+
}
|
|
4762
5040
|
/**
|
|
4763
5041
|
* Cut `text` at `end` without splitting a surrogate pair: half an emoji is not
|
|
4764
5042
|
* document text, and `query.match` finds nothing when handed a lone surrogate —
|
|
@@ -4835,7 +5113,10 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4835
5113
|
// Window: through the label's delimiter plus enough of the sentence to sample
|
|
4836
5114
|
// it, or a plain head when the item carries no label at all.
|
|
4837
5115
|
const delimiterAt = leadInEndOf(text);
|
|
4838
|
-
|
|
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);
|
|
4839
5120
|
const pattern = sliceWholeChars(text, windowEnd);
|
|
4840
5121
|
const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
|
|
4841
5122
|
const items = Array.isArray(result?.items) ? result.items : [];
|
|
@@ -4849,6 +5130,9 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4849
5130
|
const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
|
|
4850
5131
|
if (runs.length === 0)
|
|
4851
5132
|
continue;
|
|
5133
|
+
const typedLead = typedPrefixLabelOf(runs, runTextOf);
|
|
5134
|
+
if (typedLead)
|
|
5135
|
+
return typedLead;
|
|
4852
5136
|
const blockText = asString(rec?.text, '') ?? '';
|
|
4853
5137
|
// A quoted defined term keeps its opening quote in a run of its own —
|
|
4854
5138
|
// `"` + **Deliverables** + `" means …` — so the label is the SECOND run
|
|
@@ -4884,6 +5168,59 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4884
5168
|
}
|
|
4885
5169
|
return null;
|
|
4886
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
|
+
}
|
|
4887
5224
|
/**
|
|
4888
5225
|
* Pull a paragraph's direct indentation out of a `getNodeById` payload and
|
|
4889
5226
|
* convert it to the twips that `format.paragraph.setIndentation` expects.
|
|
@@ -5762,6 +6099,7 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5762
6099
|
}
|
|
5763
6100
|
if (!reference)
|
|
5764
6101
|
return null;
|
|
6102
|
+
reference = typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatibleHeading);
|
|
5765
6103
|
const applied = {};
|
|
5766
6104
|
// Numbering: when the reference participates in a numbering scheme and
|
|
5767
6105
|
// the new block does not, attach it to the same sequence — this is what
|
|
@@ -5787,7 +6125,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5787
6125
|
}
|
|
5788
6126
|
}
|
|
5789
6127
|
if (reference.styleId && reference.styleId !== created.styleId) {
|
|
5790
|
-
const
|
|
6128
|
+
const keepRunMarks = !allowLeadPattern;
|
|
6129
|
+
const setStyleFn = (keepRunMarks ? maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']) : null) ??
|
|
6130
|
+
maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
|
|
5791
6131
|
if (setStyleFn) {
|
|
5792
6132
|
const target = { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId };
|
|
5793
6133
|
let canCopyStyle = true;
|
|
@@ -5890,6 +6230,42 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5890
6230
|
return { matchedSibling: null, skipped: reasonOf(err) };
|
|
5891
6231
|
}
|
|
5892
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
|
+
}
|
|
5893
6269
|
/** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
|
|
5894
6270
|
const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
5895
6271
|
/**
|
|
@@ -6020,7 +6396,8 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6020
6396
|
const span = leadInSpanOf(fullText, pattern.lead.shape);
|
|
6021
6397
|
// A uniform reference has no body look and no delimiter of its own: its look
|
|
6022
6398
|
// belongs to the whole created block, not to a label span inside it.
|
|
6023
|
-
const range = span ??
|
|
6399
|
+
const range = span ??
|
|
6400
|
+
(Object.keys(pattern.body).length === 0 && pattern.lead.shape.kind !== 'typed' ? { start: 0, end } : null);
|
|
6024
6401
|
if (range && (await applyInlineQuiet(formatFn, { blockId: created.nodeId, ...range, inline: pattern.lead.look }))) {
|
|
6025
6402
|
applied.lead = { ...pattern.lead.look, range: [range.start, range.end] };
|
|
6026
6403
|
}
|
|
@@ -6047,7 +6424,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6047
6424
|
// When the caller styled any entry explicitly (runs/marks), it took
|
|
6048
6425
|
// ownership of intra-paragraph formatting — the automatic lead-marker
|
|
6049
6426
|
// pattern pass must not fight it.
|
|
6050
|
-
const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
|
|
6427
|
+
const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
|
|
6051
6428
|
const hasExplicitStyling = rawTexts.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6052
6429
|
const insertedTexts = rawTexts.map((entry) => {
|
|
6053
6430
|
if (typeof entry === 'string')
|
|
@@ -6248,7 +6625,15 @@ async function runAttachNumbering(doc, args) {
|
|
|
6248
6625
|
errors: [
|
|
6249
6626
|
{
|
|
6250
6627
|
code: 'TARGET_NOT_FOUND',
|
|
6251
|
-
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.`,
|
|
6252
6637
|
recovery: { kind: 'reinspect' },
|
|
6253
6638
|
},
|
|
6254
6639
|
],
|
|
@@ -7258,4 +7643,7 @@ exports.ACTION_NAMES_LIST = ACTION_NAMES_LIST;
|
|
|
7258
7643
|
exports.getActionDispositions = getActionDispositions;
|
|
7259
7644
|
exports.getUnavailableTrackedActions = getUnavailableTrackedActions;
|
|
7260
7645
|
exports.isActionName = isActionName;
|
|
7646
|
+
exports.markerShapeOf = markerShapeOf;
|
|
7261
7647
|
exports.superdocPerformAction = superdocPerformAction;
|
|
7648
|
+
exports.typedCaptionSpanOf = typedCaptionSpanOf;
|
|
7649
|
+
exports.typedMarkerPrefixOf = typedMarkerPrefixOf;
|
package/dist/agent/actions.d.ts
CHANGED
|
@@ -491,6 +491,19 @@ export type AddHyperlinkArgs = {
|
|
|
491
491
|
url: string;
|
|
492
492
|
tooltip?: string;
|
|
493
493
|
};
|
|
494
|
+
export declare function typedMarkerPrefixOf(text: string): {
|
|
495
|
+
prefix: string;
|
|
496
|
+
marker: string;
|
|
497
|
+
} | null;
|
|
498
|
+
export type MarkerShape = {
|
|
499
|
+
wrapper: 'paren' | 'period' | 'close';
|
|
500
|
+
kind: 'digit' | 'lower-roman' | 'lower-alpha' | 'upper-roman' | 'upper-alpha';
|
|
501
|
+
};
|
|
502
|
+
export declare function markerShapeOf(text: string): MarkerShape | null;
|
|
503
|
+
export declare function typedCaptionSpanOf(text: string, delimiter?: string): {
|
|
504
|
+
start: number;
|
|
505
|
+
end: number;
|
|
506
|
+
} | null;
|
|
494
507
|
export declare function superdocPerformAction(doc: BoundDocApi, args: unknown): Promise<AgentReceipt>;
|
|
495
508
|
export declare const ACTION_NAMES_LIST: readonly ActionName[];
|
|
496
509
|
export {};
|