@superdoc/sdk 2.27.0-next.9 → 2.28.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 +34 -243
- package/dist/agent/actions.js +23 -232
- package/dist/agent/inserted-block-formatting.cjs +102 -0
- package/dist/agent/inserted-block-formatting.d.ts +22 -0
- package/dist/agent/inserted-block-formatting.js +83 -0
- package/dist/agent/lead-in-label.cjs +100 -0
- package/dist/agent/lead-in-label.d.ts +29 -0
- package/dist/agent/lead-in-label.js +90 -0
- package/dist/collaboration/write-admission.cjs +185 -0
- package/dist/collaboration/write-admission.d.ts +33 -0
- package/dist/collaboration/write-admission.js +180 -0
- package/dist/generated/client.d.ts +823 -37
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- 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 +8 -8
- package/tools/generation-receipt.json +17 -17
- package/tools/tools-policy.json +1 -1
package/dist/agent/actions.cjs
CHANGED
|
@@ -11,6 +11,8 @@ var structureInsert = require('../action-primitives/tools/structure-insert.cjs')
|
|
|
11
11
|
var textTransform = require('../action-primitives/tools/text-transform.cjs');
|
|
12
12
|
var docSnapshot = require('./doc-snapshot.cjs');
|
|
13
13
|
var guards = require('../guards.cjs');
|
|
14
|
+
var leadInLabel = require('./lead-in-label.cjs');
|
|
15
|
+
var insertedBlockFormatting = require('./inserted-block-formatting.cjs');
|
|
14
16
|
|
|
15
17
|
const ACTION_DEFINITIONS = actionDescriptors;
|
|
16
18
|
function projectActionDefinitions(project) {
|
|
@@ -2672,10 +2674,9 @@ async function runRewriteBlock(doc, args) {
|
|
|
2672
2674
|
return context.failure('rewrite_block', error);
|
|
2673
2675
|
}
|
|
2674
2676
|
}
|
|
2675
|
-
const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
2676
2677
|
function inlineFromSnapshotRun(run) {
|
|
2677
2678
|
const inline = {};
|
|
2678
|
-
for (const key of RUN_BOOL_MARKS) {
|
|
2679
|
+
for (const key of insertedBlockFormatting.RUN_BOOL_MARKS) {
|
|
2679
2680
|
if (run[key])
|
|
2680
2681
|
inline[key] = true;
|
|
2681
2682
|
}
|
|
@@ -5189,112 +5190,8 @@ async function runScopedReplace(doc, args) {
|
|
|
5189
5190
|
*/
|
|
5190
5191
|
/** Points → twips: the node read side reports points, mutations take twips. */
|
|
5191
5192
|
const TWIPS_PER_POINT = 20;
|
|
5192
|
-
/**
|
|
5193
|
-
* Delimiters that separate a list item's lead-in label from its body text.
|
|
5194
|
-
* Spaced dashes are listed separately from bare ones because the label run
|
|
5195
|
-
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
5196
|
-
*/
|
|
5197
|
-
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
5198
|
-
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
5199
|
-
const LEAD_IN_MAX_CHARS = 90;
|
|
5200
|
-
/** Sentence characters sampled past the label, to read the body's own look. */
|
|
5201
|
-
const LEAD_IN_BODY_SAMPLE_CHARS = 40;
|
|
5202
|
-
/**
|
|
5203
|
-
* Where an item's lead-in label ends.
|
|
5204
|
-
*
|
|
5205
|
-
* The SIBLINGS' delimiter wins whenever the item uses it: with a dash-style
|
|
5206
|
-
* list, "Rust: Advanced — details" splits at the dash, not at the colon inside
|
|
5207
|
-
* the label. Falling back to the earliest known delimiter is only for items
|
|
5208
|
-
* that do not use the siblings' one at all.
|
|
5209
|
-
*/
|
|
5210
|
-
function leadInEndOf(text, preferred) {
|
|
5211
|
-
const at = (delimiter) => {
|
|
5212
|
-
const index = text.indexOf(delimiter);
|
|
5213
|
-
return index > 0 && index <= LEAD_IN_MAX_CHARS ? index : null;
|
|
5214
|
-
};
|
|
5215
|
-
if (preferred) {
|
|
5216
|
-
const preferredAt = at(preferred);
|
|
5217
|
-
if (preferredAt != null)
|
|
5218
|
-
return preferredAt;
|
|
5219
|
-
}
|
|
5220
|
-
const found = LEAD_IN_DELIMITERS.map(at).filter((index) => index != null);
|
|
5221
|
-
return found.length > 0 ? Math.min(...found) : null;
|
|
5222
|
-
}
|
|
5223
|
-
/** Quote characters a defined term is wrapped in ("Deliverables" means …). */
|
|
5224
|
-
const LEAD_IN_QUOTES = ['"', '\u201c', '\u2018', "'"];
|
|
5225
|
-
/**
|
|
5226
|
-
* Delimiters allowed to sit INSIDE the label run. Wider than the set used to
|
|
5227
|
-
* split at a delimiter that follows the label, because a sentence period only
|
|
5228
|
-
* counts as a label boundary when the reference's own emphasised run ends with
|
|
5229
|
-
* it — "1.7. Records. " — which is evidence no bare text scan can provide.
|
|
5230
|
-
*/
|
|
5231
|
-
const LEAD_IN_TAIL_DELIMITERS = ['. ', ': ', ...LEAD_IN_DELIMITERS];
|
|
5232
|
-
/**
|
|
5233
|
-
* Read the label shape a reference block uses, from its text and its label run.
|
|
5234
|
-
*
|
|
5235
|
-
* The delimiter has to sit at the label's edge: immediately AFTER it ("Label:
|
|
5236
|
-
* sentence"), or at its own tail ("1.7. Records. " — a numbered clause whose
|
|
5237
|
-
* marker and label share one run). That adjacency is what distinguishes a label
|
|
5238
|
-
* from an ordinary emphasised opening clause.
|
|
5239
|
-
*/
|
|
5240
|
-
function leadInShapeOf(blockText, labelText, quote) {
|
|
5241
|
-
if (quote)
|
|
5242
|
-
return { kind: 'quoted', quote };
|
|
5243
|
-
const after = LEAD_IN_DELIMITERS.find((d) => blockText.startsWith(d, labelText.length));
|
|
5244
|
-
if (after)
|
|
5245
|
-
return { kind: 'before', delimiter: after };
|
|
5246
|
-
const trimmed = labelText.trimEnd();
|
|
5247
|
-
const inside = LEAD_IN_TAIL_DELIMITERS.find((d) => trimmed.endsWith(d.trimEnd()) && trimmed.length > d.trimEnd().length);
|
|
5248
|
-
if (inside) {
|
|
5249
|
-
const occurrence = labelText.split(inside).length - 1;
|
|
5250
|
-
if (occurrence > 0)
|
|
5251
|
-
return { kind: 'inside', delimiter: inside, occurrence };
|
|
5252
|
-
}
|
|
5253
|
-
// A single emphasised word followed by a space and plain prose: the token
|
|
5254
|
-
// itself is the label ("WHEREAS the Party…"). Multi-word runs stay
|
|
5255
|
-
// unrecognised — an emphasised opening clause is prose, not a label.
|
|
5256
|
-
if (/^\S+\s$/.test(labelText) && blockText.length > labelText.length && blockText.startsWith(labelText)) {
|
|
5257
|
-
return { kind: 'word', label: labelText.trimEnd() };
|
|
5258
|
-
}
|
|
5259
|
-
return null;
|
|
5260
|
-
}
|
|
5261
|
-
/** The span of `text` that carries the label look, or null when it has none. */
|
|
5262
5193
|
function leadInSpanOf(text, shape) {
|
|
5263
|
-
|
|
5264
|
-
return typedCaptionSpanOf(text, shape.delimiter);
|
|
5265
|
-
if (shape.kind === 'word') {
|
|
5266
|
-
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
5267
|
-
// "The Parties further agree…" does not (no guessing a label there).
|
|
5268
|
-
const next = text.charAt(shape.label.length);
|
|
5269
|
-
return text.startsWith(shape.label) && (next === '' || /\s/.test(next))
|
|
5270
|
-
? { start: 0, end: shape.label.length }
|
|
5271
|
-
: null;
|
|
5272
|
-
}
|
|
5273
|
-
if (shape.kind === 'quoted') {
|
|
5274
|
-
const opener = text.slice(0, 2).indexOf(shape.quote);
|
|
5275
|
-
if (opener < 0)
|
|
5276
|
-
return null;
|
|
5277
|
-
const close = text.indexOf(shape.quote, opener + 1);
|
|
5278
|
-
if (close < 0 || close - opener - 1 <= 0 || close > LEAD_IN_MAX_CHARS)
|
|
5279
|
-
return null;
|
|
5280
|
-
return { start: opener + 1, end: close };
|
|
5281
|
-
}
|
|
5282
|
-
if (shape.kind === 'inside') {
|
|
5283
|
-
// Mirror the reference's STRUCTURE: "1.7. Records. " ends at its second
|
|
5284
|
-
// ". ", so "1.8. Review. The parties…" must end at its second one too —
|
|
5285
|
-
// stopping at the first would emphasise the number alone.
|
|
5286
|
-
let at = -1;
|
|
5287
|
-
for (let seen = 0; seen < shape.occurrence; seen += 1) {
|
|
5288
|
-
at = text.indexOf(shape.delimiter, at + 1);
|
|
5289
|
-
if (at < 0)
|
|
5290
|
-
return null;
|
|
5291
|
-
}
|
|
5292
|
-
if (at <= 0 || at > LEAD_IN_MAX_CHARS)
|
|
5293
|
-
return null;
|
|
5294
|
-
return { start: 0, end: at + shape.delimiter.length };
|
|
5295
|
-
}
|
|
5296
|
-
const end = leadInEndOf(text, shape.delimiter);
|
|
5297
|
-
return end == null ? null : { start: 0, end };
|
|
5194
|
+
return shape.kind === 'typed' ? typedCaptionSpanOf(text, shape.delimiter) : leadInLabel.labelLeadInSpanOf(text, shape);
|
|
5298
5195
|
}
|
|
5299
5196
|
const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
|
|
5300
5197
|
const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
|
|
@@ -5454,11 +5351,11 @@ async function listItemRunPattern(doc, text, blockId) {
|
|
|
5454
5351
|
return null;
|
|
5455
5352
|
// Window: through the label's delimiter plus enough of the sentence to sample
|
|
5456
5353
|
// it, or a plain head when the item carries no label at all.
|
|
5457
|
-
const delimiterAt = leadInEndOf(text);
|
|
5458
|
-
let windowEnd = delimiterAt != null ? delimiterAt + LEAD_IN_BODY_SAMPLE_CHARS : LEAD_IN_BODY_SAMPLE_CHARS;
|
|
5354
|
+
const delimiterAt = leadInLabel.leadInEndOf(text);
|
|
5355
|
+
let windowEnd = delimiterAt != null ? delimiterAt + leadInLabel.LEAD_IN_BODY_SAMPLE_CHARS : leadInLabel.LEAD_IN_BODY_SAMPLE_CHARS;
|
|
5459
5356
|
const typedCaption = typedCaptionSpanOf(text);
|
|
5460
5357
|
if (typedCaption)
|
|
5461
|
-
windowEnd = Math.max(windowEnd, typedCaption.end + LEAD_IN_BODY_SAMPLE_CHARS);
|
|
5358
|
+
windowEnd = Math.max(windowEnd, typedCaption.end + leadInLabel.LEAD_IN_BODY_SAMPLE_CHARS);
|
|
5462
5359
|
const pattern = sliceWholeChars(text, windowEnd);
|
|
5463
5360
|
const result = asRecord(await queryFn({ select: { type: 'text', pattern } }));
|
|
5464
5361
|
const items = Array.isArray(result?.items) ? result.items : [];
|
|
@@ -5518,7 +5415,7 @@ function runPatternFromRuns(runs, blockText, runTextOf) {
|
|
|
5518
5415
|
// `"` + **Deliverables** + `" means …` — so the label is the SECOND run
|
|
5519
5416
|
// and the quotes stay unemphasised.
|
|
5520
5417
|
const opener = runTextOf(runs[0]).trim();
|
|
5521
|
-
const quote = LEAD_IN_QUOTES.find((q) => opener === q);
|
|
5418
|
+
const quote = leadInLabel.LEAD_IN_QUOTES.find((q) => opener === q);
|
|
5522
5419
|
const labelIndex = quote && runs.length > 2 ? 1 : 0;
|
|
5523
5420
|
const label = runs[labelIndex];
|
|
5524
5421
|
const labelText = runTextOf(label);
|
|
@@ -5530,8 +5427,8 @@ function runPatternFromRuns(runs, blockText, runTextOf) {
|
|
|
5530
5427
|
// would stamp its emphasis across the whole new block.
|
|
5531
5428
|
const bodyStyle = matchRunStyle(runs[labelIndex + 1]);
|
|
5532
5429
|
const differs = !sameMatchRunStyle(leadStyle, bodyStyle);
|
|
5533
|
-
const shape = leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
|
|
5534
|
-
const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= LEAD_IN_MAX_CHARS;
|
|
5430
|
+
const shape = leadInLabel.leadInShapeOf(blockText, labelText, labelIndex > 0 ? quote : undefined);
|
|
5431
|
+
const isLabel = differs && shape != null && labelText.length > 0 && labelText.length <= leadInLabel.LEAD_IN_MAX_CHARS;
|
|
5535
5432
|
return runPatternOf(bodyStyle, isLabel && shape ? { style: leadStyle, shape } : undefined);
|
|
5536
5433
|
}
|
|
5537
5434
|
function typedPrefixLabelOf(runs, runTextOf) {
|
|
@@ -5571,9 +5468,9 @@ function typedPrefixLabelOf(runs, runTextOf) {
|
|
|
5571
5468
|
if (bodyIndex >= runs.length)
|
|
5572
5469
|
return null;
|
|
5573
5470
|
const labelText = runs.slice(labelIndex, bodyIndex).map(runTextOf).join('').trim();
|
|
5574
|
-
if (labelText.length === 0 || labelText.length > LEAD_IN_MAX_CHARS)
|
|
5471
|
+
if (labelText.length === 0 || labelText.length > leadInLabel.LEAD_IN_MAX_CHARS)
|
|
5575
5472
|
return null;
|
|
5576
|
-
const delimiter = ['.', ...LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
|
|
5473
|
+
const delimiter = ['.', ...leadInLabel.LEAD_IN_DELIMITERS.map((d) => d.trim())].find((d) => labelText.endsWith(d) && labelText.length > d.length);
|
|
5577
5474
|
if (!delimiter)
|
|
5578
5475
|
return null;
|
|
5579
5476
|
return runPatternOf(matchRunStyle(runs[bodyIndex]), {
|
|
@@ -5611,7 +5508,7 @@ function paragraphIndentOf(payload) {
|
|
|
5611
5508
|
return null;
|
|
5612
5509
|
}
|
|
5613
5510
|
function emphasisedLookOf(look) {
|
|
5614
|
-
return RUN_BOOL_MARKS.some((key) => look[key] === true) ? look : null;
|
|
5511
|
+
return insertedBlockFormatting.RUN_BOOL_MARKS.some((key) => look[key] === true) ? look : null;
|
|
5615
5512
|
}
|
|
5616
5513
|
async function effectiveBaseLookOf(doc, text, block) {
|
|
5617
5514
|
const queryFn = maybeMethod(doc, ['query', 'match']);
|
|
@@ -5661,7 +5558,7 @@ function inlineLookFromRow(row) {
|
|
|
5661
5558
|
*/
|
|
5662
5559
|
function runInlineAgainstBaseline(marks, baseline) {
|
|
5663
5560
|
const inline = marksToInline(marks);
|
|
5664
|
-
for (const key of RUN_BOOL_MARKS) {
|
|
5561
|
+
for (const key of insertedBlockFormatting.RUN_BOOL_MARKS) {
|
|
5665
5562
|
if (baseline[key] === true && inline[key] === undefined)
|
|
5666
5563
|
inline[key] = false;
|
|
5667
5564
|
}
|
|
@@ -6567,26 +6464,12 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
6567
6464
|
applied.indent = refIndent;
|
|
6568
6465
|
}
|
|
6569
6466
|
}
|
|
6570
|
-
// Lead-marker pattern: manually lettered/numbered sections style the
|
|
6571
|
-
// marker run differently from the body ("(m) " bold, rest plain) — a
|
|
6572
|
-
// per-run pattern blocks.list rows cannot express. When the reference
|
|
6573
|
-
// block opens with such a styled marker and the created text opens with a
|
|
6574
|
-
// similar marker token, reproduce the reference's marker/body split on the
|
|
6575
|
-
// created block. Skipped when the caller supplied explicit runs/marks.
|
|
6576
6467
|
if (allowLeadPattern) {
|
|
6577
6468
|
const lead = await matchLeadMarkerPattern(doc, reference, created, createdText);
|
|
6578
6469
|
if (lead?.changed)
|
|
6579
6470
|
applied.leadMarker = lead;
|
|
6580
|
-
// matchLeadMarkerPattern only knows a shared literal lead ("WHEREAS") and
|
|
6581
|
-
// numbered tokens ("1.", "(m)"), and it needs the reference split across
|
|
6582
|
-
// two runs. Sample the reference's RUNS for everything else: a quoted
|
|
6583
|
-
// defined term (`"Deliverables" means …`), a "1.7. Records. " clause whose
|
|
6584
|
-
// marker and label share a run. A UNIFORM reference is not a split, so
|
|
6585
|
-
// its emphasis never transfers.
|
|
6586
6471
|
else {
|
|
6587
6472
|
const runPattern = await matchBlockRunPattern(doc, reference, created, createdText);
|
|
6588
|
-
// Assigning `undefined` would still create the key, and an `applied` with
|
|
6589
|
-
// keys reads as "this block was matched" — hiding a block that got nothing.
|
|
6590
6473
|
if (runPattern)
|
|
6591
6474
|
applied.runPattern = runPattern;
|
|
6592
6475
|
}
|
|
@@ -6636,21 +6519,6 @@ function typedShapeReference(postBlocks, preIds, created, createdIndex, createdT
|
|
|
6636
6519
|
nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
|
|
6637
6520
|
reference);
|
|
6638
6521
|
}
|
|
6639
|
-
/** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
|
|
6640
|
-
const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
6641
|
-
/**
|
|
6642
|
-
* Reproduce a reference block's lead/body run split on a freshly-created
|
|
6643
|
-
* block. Fires only when the reference genuinely has the pattern — its first
|
|
6644
|
-
* run is a SHORT lead-in styled differently from the body — and the created
|
|
6645
|
-
* text opens with a matching lead of its own. Two lead shapes are recognized:
|
|
6646
|
-
* - literal lead words shared by siblings ("WHEREAS", "NOW, THEREFORE"): the
|
|
6647
|
-
* created text starts with the reference's exact lead text;
|
|
6648
|
-
* - marker tokens ("(m)", "1.", "a)"): the token text differs per clause, so
|
|
6649
|
-
* the created lead is its OWN token matched by shape.
|
|
6650
|
-
* Applies the reference lead's marks to the created lead span and only the
|
|
6651
|
-
* reference body's font and size to the rest: the body never gains emphasis or
|
|
6652
|
-
* an explicit mark-off it did not already carry.
|
|
6653
|
-
*/
|
|
6654
6522
|
async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
6655
6523
|
const createdPreview = created.textPreview ?? '';
|
|
6656
6524
|
if (!reference.nodeId || !created.nodeId || !createdPreview)
|
|
@@ -6659,60 +6527,20 @@ async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
|
6659
6527
|
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
6660
6528
|
if (!queryFn || !formatFn)
|
|
6661
6529
|
return null;
|
|
6662
|
-
|
|
6663
|
-
// returned tile the MATCHED range, covering the lead and the body start.
|
|
6664
|
-
// blocks.list truncates textPreview with a trailing ellipsis — strip it, the
|
|
6665
|
-
// remaining prefix IS document text so query.match can find it.
|
|
6666
|
-
const refProbe = (reference.textPreview ?? '')
|
|
6667
|
-
.replace(/…\s*$/, '')
|
|
6668
|
-
.slice(0, 60)
|
|
6669
|
-
.trim();
|
|
6530
|
+
const refProbe = insertedBlockFormatting.referenceProbeOf(reference.textPreview ?? '');
|
|
6670
6531
|
if (!refProbe)
|
|
6671
6532
|
return null;
|
|
6672
|
-
const
|
|
6673
|
-
if (
|
|
6674
|
-
return null; // uniform reference — no split to copy
|
|
6675
|
-
const leadRun = refRuns[0];
|
|
6676
|
-
const bodyRun = refRuns[1];
|
|
6677
|
-
// The pattern requires a real FORMATTING transition between lead and body
|
|
6678
|
-
// (bold/italic/underline/strike). Runs split only by font/color/hyperlink
|
|
6679
|
-
// boundaries are ordinary prose — restyling from those would misfire.
|
|
6680
|
-
if (RUN_BOOL_MARKS.every((key) => Boolean(leadRun[key]) === Boolean(bodyRun[key])))
|
|
6681
|
-
return null;
|
|
6682
|
-
const refLead = leadRun.text.trimEnd();
|
|
6683
|
-
// The first run must be a short lead-in (a token or a word or two), not
|
|
6684
|
-
// half the sentence.
|
|
6685
|
-
if (refLead.length === 0 || refLead.length > 40 || leadRun.start !== 0)
|
|
6533
|
+
const split = insertedBlockFormatting.leadBodySplitOf(await docSnapshot.matchRunsForBlock(queryFn, refProbe, reference.nodeId));
|
|
6534
|
+
if (!split)
|
|
6686
6535
|
return null;
|
|
6687
|
-
|
|
6688
|
-
const
|
|
6689
|
-
// The created block's own lead span (ASCII markers only by design).
|
|
6690
|
-
let createdLeadLen = 0;
|
|
6691
|
-
const boundaryAfter = (len) => len >= fullText.length || !/[A-Za-z0-9]/.test(fullText[len]);
|
|
6692
|
-
if (fullText.startsWith(refLead) && boundaryAfter(refLead.length)) {
|
|
6693
|
-
// Literal shared lead word(s) — "WHEREAS, …" after a "WHEREAS, …" sibling.
|
|
6694
|
-
// Exact-case with a token boundary so "Note" never claims "Notebook".
|
|
6695
|
-
createdLeadLen = refLead.length;
|
|
6696
|
-
}
|
|
6697
|
-
else {
|
|
6698
|
-
const refMarker = LEAD_MARKER_PATTERN.exec(refLead);
|
|
6699
|
-
const createdMarker = LEAD_MARKER_PATTERN.exec(fullText);
|
|
6700
|
-
// Marker tokens: the reference's first run must BE the token, and the
|
|
6701
|
-
// created text must open with a token of the same category (letters
|
|
6702
|
-
// with letters, digits with digits — "(m)" doesn't restyle "1.").
|
|
6703
|
-
if (refMarker &&
|
|
6704
|
-
createdMarker &&
|
|
6705
|
-
refMarker[0] === refLead &&
|
|
6706
|
-
/\d/.test(refMarker[0]) === /\d/.test(createdMarker[0])) {
|
|
6707
|
-
createdLeadLen = createdMarker[0].length;
|
|
6708
|
-
}
|
|
6709
|
-
}
|
|
6536
|
+
const fullText = createdText || insertedBlockFormatting.withoutTruncationEllipsis(createdPreview);
|
|
6537
|
+
const createdLeadLen = insertedBlockFormatting.createdLeadLengthOf(split.leadText, fullText);
|
|
6710
6538
|
if (createdLeadLen === 0)
|
|
6711
6539
|
return null;
|
|
6712
|
-
const markerInline = emphasisedLookOf(inlineFromSnapshotRun(
|
|
6540
|
+
const markerInline = emphasisedLookOf(inlineFromSnapshotRun(split.lead));
|
|
6713
6541
|
if (!markerInline)
|
|
6714
6542
|
return null;
|
|
6715
|
-
const bodyInline = baseRunLookOf(
|
|
6543
|
+
const bodyInline = baseRunLookOf(split.body);
|
|
6716
6544
|
let changed = await applyInlineQuiet(formatFn, {
|
|
6717
6545
|
blockId: created.nodeId,
|
|
6718
6546
|
start: 0,
|
|
@@ -6730,21 +6558,10 @@ async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
|
6730
6558
|
}
|
|
6731
6559
|
return { marker: fullText.slice(0, createdLeadLen), markerInline, bodyInline, changed };
|
|
6732
6560
|
}
|
|
6733
|
-
/**
|
|
6734
|
-
* Copy a reference block's RUN pattern onto a created block: the body's font
|
|
6735
|
-
* and size over the whole block, then the label look over the new block's own
|
|
6736
|
-
* label span.
|
|
6737
|
-
*
|
|
6738
|
-
* Complements matchLeadMarkerPattern, which requires either a shared literal
|
|
6739
|
-
* lead word or a marker token AND a two-run reference. This one reads the
|
|
6740
|
-
* reference's runs (`query.match`, the only read that reports run styling) and
|
|
6741
|
-
* handles the shapes that leaves out. A single uniform run has no label, so
|
|
6742
|
-
* nothing but its font transfers.
|
|
6743
|
-
*/
|
|
6744
6561
|
async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
6745
6562
|
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
6746
|
-
const referenceText = (reference.textPreview ?? '').
|
|
6747
|
-
const fullText = createdText || (created.textPreview ?? '')
|
|
6563
|
+
const referenceText = insertedBlockFormatting.withoutTruncationEllipsis(reference.textPreview ?? '').trim();
|
|
6564
|
+
const fullText = createdText || insertedBlockFormatting.withoutTruncationEllipsis(created.textPreview ?? '');
|
|
6748
6565
|
if (!formatFn || !reference.nodeId || !created.nodeId || !referenceText || !fullText)
|
|
6749
6566
|
return null;
|
|
6750
6567
|
const pattern = await listItemRunPattern(doc, referenceText, reference.nodeId);
|
|
@@ -6756,11 +6573,7 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6756
6573
|
const own = Object.keys(sampled).length > 0
|
|
6757
6574
|
? await effectiveBaseLookOf(doc, fullText, { nodeId: created.nodeId, nodeType: created.nodeType })
|
|
6758
6575
|
: null;
|
|
6759
|
-
const body = own
|
|
6760
|
-
? {}
|
|
6761
|
-
: own
|
|
6762
|
-
? Object.fromEntries(Object.entries(sampled).filter(([key, value]) => own[key] !== value))
|
|
6763
|
-
: sampled;
|
|
6576
|
+
const body = insertedBlockFormatting.bodyLookToWrite(sampled, own);
|
|
6764
6577
|
if (Object.keys(body).length > 0) {
|
|
6765
6578
|
if (await applyInlineQuiet(formatFn, { blockId: created.nodeId, start: 0, end, inline: body })) {
|
|
6766
6579
|
applied.body = body;
|
|
@@ -6786,17 +6599,10 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6786
6599
|
.filter(({ row }) => row.nodeId && !preIds.has(row.nodeId) && !row.isEmpty);
|
|
6787
6600
|
if (createdRows.length === 0)
|
|
6788
6601
|
return receipt;
|
|
6789
|
-
|
|
6790
|
-
// statement of "blend in with THIS" — prefer it over type-matched siblings.
|
|
6791
|
-
const placement = guards.isRecord(args?.placement) ? args.placement : null;
|
|
6792
|
-
const anchorSelector = placement && guards.isRecord(placement.selector) ? placement.selector : null;
|
|
6793
|
-
const anchorId = anchorSelector && typeof anchorSelector.nodeId === 'string' ? anchorSelector.nodeId : null;
|
|
6602
|
+
const anchorId = insertedBlockFormatting.placementAnchorIdOf(args);
|
|
6794
6603
|
const anchorRow = anchorId ? (postBlocks.find((b) => b.nodeId === anchorId) ?? null) : null;
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
// pattern pass must not fight it.
|
|
6798
|
-
const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
|
|
6799
|
-
const hasExplicitStyling = rawTexts.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6604
|
+
const rawTexts = insertedBlockFormatting.insertedTextEntriesOf(args);
|
|
6605
|
+
const hasExplicitStyling = insertedBlockFormatting.callerStyledAnEntry(rawTexts);
|
|
6800
6606
|
const insertedTexts = rawTexts.map((entry) => {
|
|
6801
6607
|
if (typeof entry === 'string')
|
|
6802
6608
|
return entry;
|
|
@@ -6804,26 +6610,12 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6804
6610
|
return spec ? itemSpecText(spec) : '';
|
|
6805
6611
|
});
|
|
6806
6612
|
const allApplied = [];
|
|
6807
|
-
for (
|
|
6808
|
-
const { row: created, index: createdIndex } = createdRows[ci];
|
|
6809
|
-
// The first of several inserted blocks is the section TITLE — it should
|
|
6810
|
-
// read like a heading (flush-left), so it must NOT inherit the body
|
|
6811
|
-
// sibling's left indent even though it matches the same anchor.
|
|
6812
|
-
const isTitle = createdRows.length > 1 && ci === 0;
|
|
6613
|
+
for (const [ci, { row: created, index: createdIndex }] of createdRows.entries()) {
|
|
6813
6614
|
const createdText = insertedTexts[ci] ?? '';
|
|
6814
|
-
const match = (anchor) => matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anchor, createdRows.length === 1,
|
|
6815
|
-
// The texts the caller inserted, in order — the truncated preview
|
|
6816
|
-
// cannot measure a label span or the end of the block.
|
|
6817
|
-
createdText);
|
|
6615
|
+
const match = (anchor) => matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anchor, createdRows.length === 1, insertedBlockFormatting.isSectionTitle(ci, createdRows.length), !hasExplicitStyling, createdText);
|
|
6818
6616
|
let result = await match(anchorRow);
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
// the previous NAME line, not like the "Name:" line directly above it.
|
|
6822
|
-
// Only tried when matching against the neighbour found nothing, so this
|
|
6823
|
-
// cannot change a case that already had an answer.
|
|
6824
|
-
const applied = guards.isRecord(result?.applied) ? result.applied : null;
|
|
6825
|
-
if (createdRows.length > 1 && (!applied || Object.keys(applied).length === 0)) {
|
|
6826
|
-
const parallel = postBlocks[createdIndex - createdRows.length];
|
|
6617
|
+
if (insertedBlockFormatting.shouldTryParallelGroup(createdRows.length, result)) {
|
|
6618
|
+
const parallel = postBlocks[insertedBlockFormatting.parallelGroupIndexOf(createdIndex, createdRows.length)];
|
|
6827
6619
|
if (parallel && preIds.has(parallel.nodeId) && !parallel.isEmpty) {
|
|
6828
6620
|
result = (await match(parallel)) ?? result;
|
|
6829
6621
|
}
|
|
@@ -6831,13 +6623,12 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6831
6623
|
if (result)
|
|
6832
6624
|
allApplied.push(result);
|
|
6833
6625
|
}
|
|
6834
|
-
|
|
6626
|
+
const contextualFormatting = insertedBlockFormatting.contextualFormattingOf(allApplied);
|
|
6627
|
+
if (!contextualFormatting)
|
|
6835
6628
|
return receipt;
|
|
6836
|
-
return { ...receipt, contextualFormatting
|
|
6629
|
+
return { ...receipt, contextualFormatting };
|
|
6837
6630
|
}
|
|
6838
6631
|
catch (err) {
|
|
6839
|
-
// Formatting matching is best-effort polish: never fail a successful
|
|
6840
|
-
// insert because of it — but the receipt says it was skipped.
|
|
6841
6632
|
return {
|
|
6842
6633
|
...receipt,
|
|
6843
6634
|
contextualFormatting: { skipped: reasonOf(err) },
|