@superdoc/sdk 2.23.0-next.9 → 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/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/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/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.js
CHANGED
|
@@ -875,6 +875,7 @@ async function styleTableCells(doc, cells, changeMode) {
|
|
|
875
875
|
await applyItemStyling(doc, nodeIds, specs, changeMode);
|
|
876
876
|
return styled.length;
|
|
877
877
|
}
|
|
878
|
+
const TYPED_SIBLING_WINDOW = 20;
|
|
878
879
|
async function runInsertParagraphs(doc, args, formatArgs) {
|
|
879
880
|
const context = new ExecutionContext(doc, { evidence: args.evidence, selectors: placementSelectors(args.placement) });
|
|
880
881
|
try {
|
|
@@ -887,7 +888,7 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
887
888
|
kind: 'placement',
|
|
888
889
|
at: placement.kind === 'documentStart' ? 'document_start' : 'document_end',
|
|
889
890
|
});
|
|
890
|
-
|
|
891
|
+
let preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
|
|
891
892
|
let currentPlacement = placement;
|
|
892
893
|
const nodeIds = [];
|
|
893
894
|
const specs = [];
|
|
@@ -927,6 +928,16 @@ async function runInsertParagraphs(doc, args, formatArgs) {
|
|
|
927
928
|
if (!pre.complete) {
|
|
928
929
|
const created = await post.page({ nodeIds, limit: Math.max(nodeIds.length, 1) });
|
|
929
930
|
localRows = [...(anchor ? [anchor] : []), ...created].sort((a, b) => a.ordinal - b.ordinal);
|
|
931
|
+
const placed = anchor && texts.some((text) => typedMarkerPrefixOf(text)) ? await post.block(anchor.nodeId) : null;
|
|
932
|
+
if (placed) {
|
|
933
|
+
const span = TYPED_SIBLING_WINDOW + nodeIds.length;
|
|
934
|
+
const window = await post.page({ offset: Math.max(0, placed.ordinal - 1 - span), limit: 2 * span + 1 });
|
|
935
|
+
const createdIds = new Set(nodeIds);
|
|
936
|
+
if (nodeIds.every((id) => window.some((row) => row.nodeId === id))) {
|
|
937
|
+
preRows = window.filter((row) => !createdIds.has(row.nodeId));
|
|
938
|
+
localRows = window;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
930
941
|
}
|
|
931
942
|
const count = context.executedOperations.length;
|
|
932
943
|
formatted = await matchInsertedBlockFormatting(context.document, preRows, context.receipt('insert_paragraphs', []), formatArgs, localRows);
|
|
@@ -1276,6 +1287,172 @@ async function runDeleteText(doc, args) {
|
|
|
1276
1287
|
return failedReceipt('delete_text', err, pre);
|
|
1277
1288
|
}
|
|
1278
1289
|
}
|
|
1290
|
+
const CONJUNCTION_TAIL = /[;,]\s*(and|or)\s*$/i;
|
|
1291
|
+
const ITEM_ENDING = /(?:[;,]\s*(?:and|or)|[.;,:])\s*$/i;
|
|
1292
|
+
const ITEM_CLOSERS = /["'”’)\]]+$/;
|
|
1293
|
+
function enumerationSlotOf(block) {
|
|
1294
|
+
const numbering = blockNumbering(block);
|
|
1295
|
+
const path = numbering?.path;
|
|
1296
|
+
if (Array.isArray(path) && path.length > 0) {
|
|
1297
|
+
return { key: `scheme:${path.slice(0, -1).join('.')}:${path.length}`, depth: path.length, typed: null };
|
|
1298
|
+
}
|
|
1299
|
+
const markerShape = numbering?.marker ? markerShapeOf(numbering.marker) : null;
|
|
1300
|
+
if (markerShape)
|
|
1301
|
+
return { key: `scheme-marker:${markerShape.wrapper}:${markerShape.kind}`, depth: null, typed: null };
|
|
1302
|
+
const text = block.text || block.textPreview || '';
|
|
1303
|
+
const typedMarker = typedMarkerPrefixOf(text)?.marker;
|
|
1304
|
+
const typedShape = typedMarker ? markerShapeOf(text) : null;
|
|
1305
|
+
if (!typedMarker || !typedShape)
|
|
1306
|
+
return null;
|
|
1307
|
+
const parent = typedShape.kind === 'digit' ? typedDigitParentOf(typedMarker) : '';
|
|
1308
|
+
return { key: `typed:${typedShape.wrapper}:${typedShape.kind}:${parent}`, depth: null, typed: typedShape };
|
|
1309
|
+
}
|
|
1310
|
+
function typedDigitParentOf(marker) {
|
|
1311
|
+
const segments = marker.replace(/^\(/, '').replace(/[.)]$/, '').split('.');
|
|
1312
|
+
return `${segments.length}:${segments.slice(0, -1).join('.')}`;
|
|
1313
|
+
}
|
|
1314
|
+
const ROMAN_DIGITS = { i: 1, v: 5, x: 10 };
|
|
1315
|
+
function markerValueOf(token, shape) {
|
|
1316
|
+
const core = token.replace(/^\(/, '').replace(/[.)]$/, '').toLowerCase();
|
|
1317
|
+
if (shape.kind === 'digit')
|
|
1318
|
+
return Number(core.split('.').pop());
|
|
1319
|
+
if (shape.kind === 'lower-roman' || shape.kind === 'upper-roman') {
|
|
1320
|
+
let total = 0;
|
|
1321
|
+
for (let i = 0; i < core.length; i += 1) {
|
|
1322
|
+
const value = ROMAN_DIGITS[core[i]] ?? 0;
|
|
1323
|
+
total += value < (ROMAN_DIGITS[core[i + 1] ?? ''] ?? 0) ? -value : value;
|
|
1324
|
+
}
|
|
1325
|
+
return total;
|
|
1326
|
+
}
|
|
1327
|
+
const letter = core.charCodeAt(0) - 96;
|
|
1328
|
+
return core.length === 1 ? letter : 26 * (core.length - 1) + letter;
|
|
1329
|
+
}
|
|
1330
|
+
function enumerationRunOf(blocks, index, slot, textOf) {
|
|
1331
|
+
const typed = slot.typed;
|
|
1332
|
+
const valueOf = (block) => {
|
|
1333
|
+
if (!typed) {
|
|
1334
|
+
const path = blockNumbering(block)?.path;
|
|
1335
|
+
return Array.isArray(path) && path.length > 0 ? Number(path[path.length - 1]) : null;
|
|
1336
|
+
}
|
|
1337
|
+
const token = typed ? typedMarkerPrefixOf(textOf(block))?.marker : null;
|
|
1338
|
+
return token && typed ? markerValueOf(token, typed) : null;
|
|
1339
|
+
};
|
|
1340
|
+
const walk = (step) => {
|
|
1341
|
+
const found = [];
|
|
1342
|
+
let edge = valueOf(blocks[index]);
|
|
1343
|
+
let crossed = false;
|
|
1344
|
+
for (let i = index + step; i >= 0 && i < blocks.length; i += step) {
|
|
1345
|
+
const block = blocks[i];
|
|
1346
|
+
if (textOf(block).length === 0)
|
|
1347
|
+
continue;
|
|
1348
|
+
const other = enumerationSlotOf(block);
|
|
1349
|
+
const continuesAsLetter = typed != null &&
|
|
1350
|
+
other?.typed != null &&
|
|
1351
|
+
markerShapeFits(typed, other.typed) &&
|
|
1352
|
+
edge != null &&
|
|
1353
|
+
valueOf(block) === edge + step;
|
|
1354
|
+
if (other?.key === slot.key || continuesAsLetter) {
|
|
1355
|
+
const value = valueOf(block);
|
|
1356
|
+
if (value != null && edge != null) {
|
|
1357
|
+
const advance = (value - edge) * step;
|
|
1358
|
+
if (advance <= 0 || ((crossed || !typed) && advance !== 1))
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
found.push(block);
|
|
1362
|
+
edge = value;
|
|
1363
|
+
crossed = false;
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
if (typed && other?.typed) {
|
|
1367
|
+
if (step === -1 && edge != null) {
|
|
1368
|
+
const prefix = typedMarkerPrefixOf(textOf(block)).prefix;
|
|
1369
|
+
const inline = new RegExp(String.raw `(?:^|\s)(${TYPED_MARKER_TOKEN}) `).exec(textOf(block).slice(prefix.length));
|
|
1370
|
+
const inlineShape = inline ? markerShapeOf(inline[1]) : null;
|
|
1371
|
+
if (inline && sameMarkerShape(inlineShape, typed) && markerValueOf(inline[1], typed) === edge - 1) {
|
|
1372
|
+
found.push(block);
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
crossed = true;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (slot.depth != null && other?.depth != null && other.depth > slot.depth)
|
|
1380
|
+
continue;
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
return found;
|
|
1384
|
+
};
|
|
1385
|
+
return [...walk(-1).reverse(), blocks[index], ...walk(1)];
|
|
1386
|
+
}
|
|
1387
|
+
function listPunctuationAfterDelete(blocks, deletedIds) {
|
|
1388
|
+
const textOf = (block) => (block.text || (block.textPreview ?? '').replace(/…\s*$/, '')).trimEnd();
|
|
1389
|
+
const endingOf = (block) => {
|
|
1390
|
+
const text = textOf(block);
|
|
1391
|
+
const closers = ITEM_CLOSERS.exec(text)?.[0] ?? '';
|
|
1392
|
+
const body = text.slice(0, text.length - closers.length).trimEnd();
|
|
1393
|
+
const ending = ITEM_ENDING.exec(body);
|
|
1394
|
+
return {
|
|
1395
|
+
core: ending?.[0].trim() ?? '',
|
|
1396
|
+
real: ending ? `${ending[0].trim()}${closers}` : closers,
|
|
1397
|
+
closers,
|
|
1398
|
+
conjunction: CONJUNCTION_TAIL.exec(body)?.[0].trim() ?? null,
|
|
1399
|
+
};
|
|
1400
|
+
};
|
|
1401
|
+
const quote = (block) => {
|
|
1402
|
+
const text = textOf(block);
|
|
1403
|
+
return JSON.stringify(text.length > 40 ? `...${text.slice(-40)}` : text);
|
|
1404
|
+
};
|
|
1405
|
+
const change = (block, to) => {
|
|
1406
|
+
const ending = endingOf(block);
|
|
1407
|
+
return `change the ending of ${quote(block)} (nodeId ${block.nodeId}) from "${ending.real}" to "${to}${ending.closers}"`;
|
|
1408
|
+
};
|
|
1409
|
+
const entries = [];
|
|
1410
|
+
const covered = new Set();
|
|
1411
|
+
blocks.forEach((block, index) => {
|
|
1412
|
+
if (!deletedIds.has(block.nodeId) || covered.has(block.nodeId))
|
|
1413
|
+
return;
|
|
1414
|
+
const slot = enumerationSlotOf(block);
|
|
1415
|
+
if (!slot)
|
|
1416
|
+
return;
|
|
1417
|
+
const run = enumerationRunOf(blocks, index, slot, textOf);
|
|
1418
|
+
for (const item of run)
|
|
1419
|
+
covered.add(item.nodeId);
|
|
1420
|
+
const survivors = run.filter((item) => !deletedIds.has(item.nodeId));
|
|
1421
|
+
if (survivors.length === 0)
|
|
1422
|
+
return;
|
|
1423
|
+
const last = survivors[survivors.length - 1];
|
|
1424
|
+
const penultimate = survivors.length >= 2 ? survivors[survivors.length - 2] : undefined;
|
|
1425
|
+
const originalPenultimate = run[run.length - 2];
|
|
1426
|
+
if (run.some((item) => item !== originalPenultimate && endingOf(item).conjunction))
|
|
1427
|
+
return;
|
|
1428
|
+
const runEnd = run[run.length - 1];
|
|
1429
|
+
if (!deletedIds.has(runEnd.nodeId))
|
|
1430
|
+
return;
|
|
1431
|
+
const tail = originalPenultimate ? endingOf(originalPenultimate).conjunction : null;
|
|
1432
|
+
const separator = tail ? tail.charAt(0) : ';';
|
|
1433
|
+
const closing = endingOf(runEnd).core || '.';
|
|
1434
|
+
const lastEnding = endingOf(last);
|
|
1435
|
+
if (lastEnding.core === ':')
|
|
1436
|
+
return;
|
|
1437
|
+
const needsClose = lastEnding.core !== closing && (lastEnding.conjunction != null || lastEnding.core === separator);
|
|
1438
|
+
const needsTail = penultimate != null && tail != null && endingOf(penultimate).core !== ':' && !endingOf(penultimate).conjunction;
|
|
1439
|
+
if (!needsClose && !(needsTail && lastEnding.core !== closing))
|
|
1440
|
+
return;
|
|
1441
|
+
const edits = [
|
|
1442
|
+
`end ${quote(last)} (nodeId ${last.nodeId}) with "${closing}${lastEnding.closers}" instead of "${lastEnding.real}"`,
|
|
1443
|
+
];
|
|
1444
|
+
if (needsTail)
|
|
1445
|
+
edits.push(change(penultimate, tail));
|
|
1446
|
+
entries.push({
|
|
1447
|
+
deleted: runEnd.nodeId,
|
|
1448
|
+
lastItem: last.nodeId,
|
|
1449
|
+
lastItemEnds: lastEnding.real,
|
|
1450
|
+
...(penultimate ? { penultimate: penultimate.nodeId, penultimateEnds: endingOf(penultimate).real } : {}),
|
|
1451
|
+
suggestion: `The run now ends at ${quote(last)}: ${edits.join(', and ')}, using replace_text.`,
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
return entries;
|
|
1455
|
+
}
|
|
1279
1456
|
/** Block node types `doc.blocks.delete` removes as a whole paragraph-shaped block. */
|
|
1280
1457
|
const DELETABLE_BLOCK_NODE_TYPES = new Set(['paragraph', 'heading', 'listItem']);
|
|
1281
1458
|
/**
|
|
@@ -1445,6 +1622,11 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1445
1622
|
// an error, so the `errors.length` term still catches a short request.
|
|
1446
1623
|
const allApplied = deleted.length === targets.length && errors.length === 0;
|
|
1447
1624
|
const verified = verification.every((v) => v.passed);
|
|
1625
|
+
let listPunctuation = [];
|
|
1626
|
+
try {
|
|
1627
|
+
listPunctuation = listPunctuationAfterDelete(pre.blocks, new Set(deleted.map((entry) => entry.nodeId)));
|
|
1628
|
+
}
|
|
1629
|
+
catch { }
|
|
1448
1630
|
return {
|
|
1449
1631
|
status: !verified || deleted.length === 0 ? 'failed' : allApplied ? 'ok' : 'partial',
|
|
1450
1632
|
intent: `delete_blocks: ${deleted.length} block(s)`,
|
|
@@ -1454,6 +1636,7 @@ async function runDeleteBlocks(doc, args) {
|
|
|
1454
1636
|
executedOperations,
|
|
1455
1637
|
verification,
|
|
1456
1638
|
deletedBlocks: deleted,
|
|
1639
|
+
...(listPunctuation.length > 0 ? { listPunctuation } : {}),
|
|
1457
1640
|
...(errors.length ? { errors } : {}),
|
|
1458
1641
|
...(errors.length
|
|
1459
1642
|
? { nextStep: 'Re-inspect the document and retry the selectors that did not resolve to one block.' }
|
|
@@ -4398,8 +4581,9 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4398
4581
|
occurrences: spans.length,
|
|
4399
4582
|
...(edit.occurrence != null ? { occurrence: edit.occurrence } : {}),
|
|
4400
4583
|
});
|
|
4401
|
-
for (const span of spans)
|
|
4402
|
-
allSpans.push({ ...span, replace: edit.replace, find: edit.find });
|
|
4584
|
+
for (const span of spans) {
|
|
4585
|
+
allSpans.push({ ...span, replace: edit.replace, find: edit.find, editIndex: perEdit.length - 1 });
|
|
4586
|
+
}
|
|
4403
4587
|
}
|
|
4404
4588
|
const appliedEdits = perEdit.filter((e) => e.occurrences > 0);
|
|
4405
4589
|
const skippedEdits = perEdit.filter((e) => e.occurrences === 0);
|
|
@@ -4436,6 +4620,23 @@ async function runScopedReplaceUnchecked(doc, args) {
|
|
|
4436
4620
|
planned.push(span);
|
|
4437
4621
|
lastEnd = span.end;
|
|
4438
4622
|
}
|
|
4623
|
+
planned.forEach((span, at) => {
|
|
4624
|
+
if (span.replace.trim().length > 0)
|
|
4625
|
+
return;
|
|
4626
|
+
const text = block.text;
|
|
4627
|
+
if (text.charAt(span.start - 1) !== ' ' || text.charAt(span.end) !== ' ')
|
|
4628
|
+
return;
|
|
4629
|
+
if (text.charAt(span.start - 2) === ' ' || text.charAt(span.end + 1) === ' ')
|
|
4630
|
+
return;
|
|
4631
|
+
if (/[.!?]["'\u201d\u2019)\]]*$/.test(text.slice(0, span.start - 1)))
|
|
4632
|
+
return;
|
|
4633
|
+
if ((planned[at + 1]?.start ?? Infinity) <= span.end || (planned[at - 1]?.end ?? -1) >= span.start - 1)
|
|
4634
|
+
return;
|
|
4635
|
+
if (span.replace.length > 0)
|
|
4636
|
+
span.start -= 1;
|
|
4637
|
+
span.end += 1;
|
|
4638
|
+
perEdit[span.editIndex].collapsedSpace = true;
|
|
4639
|
+
});
|
|
4439
4640
|
// The block text once every planned span is replaced: the whole-block
|
|
4440
4641
|
// rewrite writes it, and exact-target writes must produce it.
|
|
4441
4642
|
const expected = planned.reduceRight((text, span) => text.slice(0, span.start) + span.replace + text.slice(span.end), block.text);
|
|
@@ -4722,6 +4923,8 @@ function leadInShapeOf(blockText, labelText, quote) {
|
|
|
4722
4923
|
}
|
|
4723
4924
|
/** The span of `text` that carries the label look, or null when it has none. */
|
|
4724
4925
|
function leadInSpanOf(text, shape) {
|
|
4926
|
+
if (shape.kind === 'typed')
|
|
4927
|
+
return typedCaptionSpanOf(text, shape.delimiter);
|
|
4725
4928
|
if (shape.kind === 'word') {
|
|
4726
4929
|
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
4727
4930
|
// "The Parties further agree…" does not (no guessing a label there).
|
|
@@ -4756,6 +4959,81 @@ function leadInSpanOf(text, shape) {
|
|
|
4756
4959
|
const end = leadInEndOf(text, shape.delimiter);
|
|
4757
4960
|
return end == null ? null : { start: 0, end };
|
|
4758
4961
|
}
|
|
4962
|
+
const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
|
|
4963
|
+
const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
|
|
4964
|
+
const TYPED_MARKER_PREFIX = new RegExp(String.raw `^[ \t]*(${TYPED_MARKER_TOKEN})\t[\t ]*`);
|
|
4965
|
+
const LEADING_MARKER_TOKEN = new RegExp(String.raw `^\s*(${TYPED_MARKER_TOKEN})(?=\s|$)`);
|
|
4966
|
+
export function typedMarkerPrefixOf(text) {
|
|
4967
|
+
const match = TYPED_MARKER_PREFIX.exec(text);
|
|
4968
|
+
return match ? { prefix: match[0], marker: match[1] } : null;
|
|
4969
|
+
}
|
|
4970
|
+
export function markerShapeOf(text) {
|
|
4971
|
+
const match = LEADING_MARKER_TOKEN.exec(text);
|
|
4972
|
+
if (!match)
|
|
4973
|
+
return null;
|
|
4974
|
+
const token = match[1];
|
|
4975
|
+
const wrapper = token.startsWith('(') ? 'paren' : token.endsWith('.') ? 'period' : 'close';
|
|
4976
|
+
const core = token.replace(/^\(/, '').replace(/[.)]$/, '');
|
|
4977
|
+
let kind;
|
|
4978
|
+
if (/^\d/.test(core))
|
|
4979
|
+
kind = 'digit';
|
|
4980
|
+
else if (/^[ivx]+$/.test(core))
|
|
4981
|
+
kind = 'lower-roman';
|
|
4982
|
+
else if (/^[IVX]+$/.test(core))
|
|
4983
|
+
kind = 'upper-roman';
|
|
4984
|
+
else if (/^[a-z]+$/.test(core))
|
|
4985
|
+
kind = 'lower-alpha';
|
|
4986
|
+
else if (/^[A-Z]+$/.test(core))
|
|
4987
|
+
kind = 'upper-alpha';
|
|
4988
|
+
else
|
|
4989
|
+
return null;
|
|
4990
|
+
return { wrapper, kind };
|
|
4991
|
+
}
|
|
4992
|
+
function sameMarkerShape(a, b) {
|
|
4993
|
+
return a != null && b != null && a.wrapper === b.wrapper && a.kind === b.kind;
|
|
4994
|
+
}
|
|
4995
|
+
function markerShapeFits(reference, created) {
|
|
4996
|
+
if (!reference || reference.wrapper !== created.wrapper)
|
|
4997
|
+
return false;
|
|
4998
|
+
if (reference.kind === created.kind)
|
|
4999
|
+
return true;
|
|
5000
|
+
return ((created.kind === 'lower-roman' && reference.kind === 'lower-alpha') ||
|
|
5001
|
+
(created.kind === 'upper-roman' && reference.kind === 'upper-alpha'));
|
|
5002
|
+
}
|
|
5003
|
+
function continuesAlphabet(referenceText, createdText, referenceBefore) {
|
|
5004
|
+
const letterOf = (text) => /^[a-zA-Z]$/.exec(LEADING_MARKER_TOKEN.exec(text)?.[1]?.replace(/[().]/g, '') ?? '')?.[0];
|
|
5005
|
+
const reference = letterOf(referenceText);
|
|
5006
|
+
const created = letterOf(createdText);
|
|
5007
|
+
return (reference != null &&
|
|
5008
|
+
created != null &&
|
|
5009
|
+
created.charCodeAt(0) - reference.charCodeAt(0) === (referenceBefore ? 1 : -1));
|
|
5010
|
+
}
|
|
5011
|
+
const TYPED_CAPTION_MAX_CHARS = 60;
|
|
5012
|
+
function isTitleLike(caption) {
|
|
5013
|
+
const words = caption.match(/\p{L}+/gu) ?? [];
|
|
5014
|
+
const capitalised = words.filter((word) => /^\p{Lu}/u.test(word)).length;
|
|
5015
|
+
return (words.length > 0 &&
|
|
5016
|
+
words.every((word) => word.length < 4 || /^\p{Lu}/u.test(word)) &&
|
|
5017
|
+
capitalised * 2 >= words.length);
|
|
5018
|
+
}
|
|
5019
|
+
export function typedCaptionSpanOf(text, delimiter = '.') {
|
|
5020
|
+
const typed = typedMarkerPrefixOf(text);
|
|
5021
|
+
const mark = delimiter.trim();
|
|
5022
|
+
if (!typed || !mark)
|
|
5023
|
+
return null;
|
|
5024
|
+
const start = typed.prefix.length;
|
|
5025
|
+
for (let at = text.indexOf(mark, start); at >= 0; at = text.indexOf(mark, at + 1)) {
|
|
5026
|
+
const end = at + mark.length;
|
|
5027
|
+
if (end - start > TYPED_CAPTION_MAX_CHARS)
|
|
5028
|
+
return null;
|
|
5029
|
+
if (at === start || !/\s/.test(text.charAt(end)))
|
|
5030
|
+
continue;
|
|
5031
|
+
if (!isTitleLike(text.slice(start, at)))
|
|
5032
|
+
return null;
|
|
5033
|
+
return text.slice(end).trim().length > 0 ? { start, end } : null;
|
|
5034
|
+
}
|
|
5035
|
+
return null;
|
|
5036
|
+
}
|
|
4759
5037
|
/**
|
|
4760
5038
|
* Cut `text` at `end` without splitting a surrogate pair: half an emoji is not
|
|
4761
5039
|
* document text, and `query.match` finds nothing when handed a lone surrogate —
|
|
@@ -4832,7 +5110,10 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4832
5110
|
// Window: through the label's delimiter plus enough of the sentence to sample
|
|
4833
5111
|
// it, or a plain head when the item carries no label at all.
|
|
4834
5112
|
const delimiterAt = leadInEndOf(text);
|
|
4835
|
-
|
|
5113
|
+
let windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
|
|
5114
|
+
const typedCaption = typedCaptionSpanOf(text);
|
|
5115
|
+
if (typedCaption)
|
|
5116
|
+
windowEnd = Math.max(windowEnd, typedCaption.end + LEAD_IN_BODY_SAMPLE_CHARS);
|
|
4836
5117
|
const pattern = sliceWholeChars(text, windowEnd);
|
|
4837
5118
|
const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
|
|
4838
5119
|
const items = Array.isArray(result?.items) ? result.items : [];
|
|
@@ -4846,6 +5127,9 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4846
5127
|
const runs = (Array.isArray(rec?.runs) ? rec.runs : []).filter((r) => runTextOf(r).length > 0);
|
|
4847
5128
|
if (runs.length === 0)
|
|
4848
5129
|
continue;
|
|
5130
|
+
const typedLead = typedPrefixLabelOf(runs, runTextOf);
|
|
5131
|
+
if (typedLead)
|
|
5132
|
+
return typedLead;
|
|
4849
5133
|
const blockText = asString(rec?.text, '') ?? '';
|
|
4850
5134
|
// A quoted defined term keeps its opening quote in a run of its own —
|
|
4851
5135
|
// `"` + **Deliverables** + `" means …` — so the label is the SECOND run
|
|
@@ -4881,6 +5165,59 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
4881
5165
|
}
|
|
4882
5166
|
return null;
|
|
4883
5167
|
}
|
|
5168
|
+
function typedPrefixLabelOf(runs, runTextOf) {
|
|
5169
|
+
const typed = typedMarkerPrefixOf(runs.map(runTextOf).join(''));
|
|
5170
|
+
if (!typed)
|
|
5171
|
+
return null;
|
|
5172
|
+
const markerEnd = typed.prefix.indexOf(typed.marker) + typed.marker.length;
|
|
5173
|
+
let covered = 0;
|
|
5174
|
+
let labelIndex = 0;
|
|
5175
|
+
while (labelIndex < runs.length && covered + runTextOf(runs[labelIndex]).length <= typed.prefix.length) {
|
|
5176
|
+
covered += runTextOf(runs[labelIndex]).length;
|
|
5177
|
+
labelIndex += 1;
|
|
5178
|
+
}
|
|
5179
|
+
if (covered < markerEnd || labelIndex + 1 >= runs.length)
|
|
5180
|
+
return null;
|
|
5181
|
+
const underlined = (run) => asRecord(asRecord(asRecord(run)?.styles)?.effective)?.underline === true;
|
|
5182
|
+
const emphasisOf = (run) => {
|
|
5183
|
+
const style = matchRunStyle(run);
|
|
5184
|
+
return `${style.bold}/${style.italic}/${underlined(run)}`;
|
|
5185
|
+
};
|
|
5186
|
+
const label = runs[labelIndex];
|
|
5187
|
+
if (emphasisOf(label) === 'false/false/false')
|
|
5188
|
+
return null;
|
|
5189
|
+
const isBlank = (run) => runTextOf(run).trim().length === 0;
|
|
5190
|
+
const continuesLabel = (index) => {
|
|
5191
|
+
if (emphasisOf(runs[index]) === emphasisOf(label))
|
|
5192
|
+
return true;
|
|
5193
|
+
let next = index;
|
|
5194
|
+
while (next < runs.length && isBlank(runs[next]))
|
|
5195
|
+
next += 1;
|
|
5196
|
+
return next > index && next < runs.length && emphasisOf(runs[next]) === emphasisOf(label);
|
|
5197
|
+
};
|
|
5198
|
+
let bodyIndex = labelIndex + 1;
|
|
5199
|
+
while (bodyIndex < runs.length && continuesLabel(bodyIndex))
|
|
5200
|
+
bodyIndex += 1;
|
|
5201
|
+
while (bodyIndex < runs.length - 1 && isBlank(runs[bodyIndex]))
|
|
5202
|
+
bodyIndex += 1;
|
|
5203
|
+
if (bodyIndex >= runs.length)
|
|
5204
|
+
return null;
|
|
5205
|
+
const labelText = runs.slice(labelIndex, bodyIndex).map(runTextOf).join('').trim();
|
|
5206
|
+
if (labelText.length === 0 || labelText.length > LEAD_IN_MAX_CHARS)
|
|
5207
|
+
return null;
|
|
5208
|
+
const delimiter = ['.', ...LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
|
|
5209
|
+
if (!delimiter)
|
|
5210
|
+
return null;
|
|
5211
|
+
const body = runs[bodyIndex];
|
|
5212
|
+
const leadStyle = matchRunStyle(label);
|
|
5213
|
+
const bodyStyle = matchRunStyle(body);
|
|
5214
|
+
const leadUnderline = underlined(label);
|
|
5215
|
+
const bodyUnderline = underlined(body);
|
|
5216
|
+
const look = inlineFromMatchStyle(leadStyle, bodyStyle);
|
|
5217
|
+
if (leadUnderline || bodyUnderline)
|
|
5218
|
+
look.underline = leadUnderline;
|
|
5219
|
+
return { body: inlineFromMatchStyle(bodyStyle), lead: { look, shape: { kind: 'typed', delimiter } } };
|
|
5220
|
+
}
|
|
4884
5221
|
/**
|
|
4885
5222
|
* Pull a paragraph's direct indentation out of a `getNodeById` payload and
|
|
4886
5223
|
* convert it to the twips that `format.paragraph.setIndentation` expects.
|
|
@@ -5759,6 +6096,7 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5759
6096
|
}
|
|
5760
6097
|
if (!reference)
|
|
5761
6098
|
return null;
|
|
6099
|
+
reference = typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatibleHeading);
|
|
5762
6100
|
const applied = {};
|
|
5763
6101
|
// Numbering: when the reference participates in a numbering scheme and
|
|
5764
6102
|
// the new block does not, attach it to the same sequence — this is what
|
|
@@ -5784,7 +6122,9 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5784
6122
|
}
|
|
5785
6123
|
}
|
|
5786
6124
|
if (reference.styleId && reference.styleId !== created.styleId) {
|
|
5787
|
-
const
|
|
6125
|
+
const keepRunMarks = !allowLeadPattern;
|
|
6126
|
+
const setStyleFn = (keepRunMarks ? maybeMethod(doc, ['styles', 'paragraph', 'setStyleRef']) : null) ??
|
|
6127
|
+
maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
|
|
5788
6128
|
if (setStyleFn) {
|
|
5789
6129
|
const target = { kind: 'block', nodeType: created.nodeType, nodeId: created.nodeId };
|
|
5790
6130
|
let canCopyStyle = true;
|
|
@@ -5887,6 +6227,42 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
5887
6227
|
return { matchedSibling: null, skipped: reasonOf(err) };
|
|
5888
6228
|
}
|
|
5889
6229
|
}
|
|
6230
|
+
function typedShapeReference(postBlocks, preIds, created, createdIndex, createdText, reference, compatible) {
|
|
6231
|
+
const typed = createdText ? typedMarkerPrefixOf(createdText) : null;
|
|
6232
|
+
if (!typed || createdText.slice(typed.prefix.length).trim().length === 0)
|
|
6233
|
+
return reference;
|
|
6234
|
+
const shape = markerShapeOf(createdText);
|
|
6235
|
+
const referenceShape = markerShapeOf(reference.textPreview ?? '');
|
|
6236
|
+
if (!shape || sameMarkerShape(referenceShape, shape))
|
|
6237
|
+
return reference;
|
|
6238
|
+
const usable = (row) => !!row && preIds.has(row.nodeId) && row.nodeType === created.nodeType && !row.isEmpty && compatible(row);
|
|
6239
|
+
const before = postBlocks.slice(0, createdIndex).reverse().find(usable);
|
|
6240
|
+
const after = postBlocks.slice(createdIndex + 1).find(usable);
|
|
6241
|
+
const continued = [reference, before, after].find((row) => !!row &&
|
|
6242
|
+
markerShapeFits(markerShapeOf(row.textPreview ?? ''), shape) &&
|
|
6243
|
+
continuesAlphabet(row.textPreview ?? '', createdText, postBlocks.findIndex((b) => b.nodeId === row.nodeId) < createdIndex));
|
|
6244
|
+
if (continued)
|
|
6245
|
+
return continued;
|
|
6246
|
+
const candidate = (row) => {
|
|
6247
|
+
if (!usable(row))
|
|
6248
|
+
return null;
|
|
6249
|
+
const preview = row.textPreview ?? '';
|
|
6250
|
+
return typedMarkerPrefixOf(preview) ? markerShapeOf(preview) : null;
|
|
6251
|
+
};
|
|
6252
|
+
const nearest = (accepts) => {
|
|
6253
|
+
for (let distance = 1; distance < postBlocks.length; distance += 1) {
|
|
6254
|
+
for (const index of [createdIndex - distance, createdIndex + distance]) {
|
|
6255
|
+
const row = postBlocks[index];
|
|
6256
|
+
if (index >= 0 && accepts(candidate(row)))
|
|
6257
|
+
return row;
|
|
6258
|
+
}
|
|
6259
|
+
}
|
|
6260
|
+
return null;
|
|
6261
|
+
};
|
|
6262
|
+
return (nearest((rowShape) => sameMarkerShape(rowShape, shape)) ??
|
|
6263
|
+
nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
|
|
6264
|
+
reference);
|
|
6265
|
+
}
|
|
5890
6266
|
/** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
|
|
5891
6267
|
const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
5892
6268
|
/**
|
|
@@ -6017,7 +6393,8 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6017
6393
|
const span = leadInSpanOf(fullText, pattern.lead.shape);
|
|
6018
6394
|
// A uniform reference has no body look and no delimiter of its own: its look
|
|
6019
6395
|
// belongs to the whole created block, not to a label span inside it.
|
|
6020
|
-
const range = span ??
|
|
6396
|
+
const range = span ??
|
|
6397
|
+
(Object.keys(pattern.body).length === 0 && pattern.lead.shape.kind !== 'typed' ? { start: 0, end } : null);
|
|
6021
6398
|
if (range && (await applyInlineQuiet(formatFn, { blockId: created.nodeId, ...range, inline: pattern.lead.look }))) {
|
|
6022
6399
|
applied.lead = { ...pattern.lead.look, range: [range.start, range.end] };
|
|
6023
6400
|
}
|
|
@@ -6044,7 +6421,7 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6044
6421
|
// When the caller styled any entry explicitly (runs/marks), it took
|
|
6045
6422
|
// ownership of intra-paragraph formatting — the automatic lead-marker
|
|
6046
6423
|
// pattern pass must not fight it.
|
|
6047
|
-
const rawTexts = Array.isArray(args?.texts) ? args.texts : [];
|
|
6424
|
+
const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
|
|
6048
6425
|
const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6049
6426
|
const insertedTexts = rawTexts.map((entry) => {
|
|
6050
6427
|
if (typeof entry === 'string')
|
|
@@ -6245,7 +6622,15 @@ async function runAttachNumbering(doc, args) {
|
|
|
6245
6622
|
errors: [
|
|
6246
6623
|
{
|
|
6247
6624
|
code: 'TARGET_NOT_FOUND',
|
|
6248
|
-
message:
|
|
6625
|
+
message: available.length > 0
|
|
6626
|
+
? `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`
|
|
6627
|
+
: rows.some((r) => typedMarkerPrefixOf(r.textPreview ?? '') != null)
|
|
6628
|
+
? `no block renders marker ${JSON.stringify(likeMarker)}: this document has no numbering scheme; its clause numbers are typed text. ` +
|
|
6629
|
+
`Copy the sibling's typed prefix (tab, next number, tab) into the paragraph text with insert_paragraphs or replace_text, ` +
|
|
6630
|
+
`then renumber the following typed markers with replace_text.`
|
|
6631
|
+
: `no block renders marker ${JSON.stringify(likeMarker)}: no block in this document carries numbering. ` +
|
|
6632
|
+
`To add a paragraph that matches its neighbours use insert_paragraphs next to them; ` +
|
|
6633
|
+
`to start a numbered list use append_list or convert_list.`,
|
|
6249
6634
|
recovery: { kind: 'reinspect' },
|
|
6250
6635
|
},
|
|
6251
6636
|
],
|