@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.
@@ -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
- const preRows = pre.complete ? await listBlockRows(doc) : anchor ? [anchor] : [];
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
- const windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
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 setStyleFn = maybeMethod(doc, ['styles', 'paragraph', 'setStyle']);
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 ?? (Object.keys(pattern.body).length === 0 ? { start: 0, end } : null);
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: `no block renders marker ${JSON.stringify(likeMarker)}. Available: ${available.join(' ')}`,
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;
@@ -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 {};