@superdoc/sdk 2.27.0 → 2.28.0-next.16
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 +821 -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.js
CHANGED
|
@@ -9,6 +9,8 @@ import { runSuperdocStructureInsertWorkflow } from '../action-primitives/tools/s
|
|
|
9
9
|
import { runSuperdocTextTransformWorkflow } from '../action-primitives/tools/text-transform.js';
|
|
10
10
|
import { buildDocumentSnapshot, buildMutationSnapshot, matchRunsForBlock, MutationSnapshotError, resolveSnapshotSelector, } from './doc-snapshot.js';
|
|
11
11
|
import { isRecord } from '../guards.js';
|
|
12
|
+
import { LEAD_IN_BODY_SAMPLE_CHARS, LEAD_IN_DELIMITERS, LEAD_IN_MAX_CHARS, LEAD_IN_QUOTES, labelLeadInSpanOf, leadInEndOf, leadInShapeOf, } from './lead-in-label.js';
|
|
13
|
+
import { RUN_BOOL_MARKS, bodyLookToWrite, callerStyledAnEntry, contextualFormattingOf, createdLeadLengthOf, insertedTextEntriesOf, isSectionTitle, leadBodySplitOf, parallelGroupIndexOf, placementAnchorIdOf, referenceProbeOf, shouldTryParallelGroup, withoutTruncationEllipsis, } from './inserted-block-formatting.js';
|
|
12
14
|
export const ACTION_DEFINITIONS = actionDescriptors;
|
|
13
15
|
function projectActionDefinitions(project) {
|
|
14
16
|
return Object.fromEntries(Object.entries(ACTION_DEFINITIONS).map(([name, definition]) => [name, project(definition)]));
|
|
@@ -2669,7 +2671,6 @@ async function runRewriteBlock(doc, args) {
|
|
|
2669
2671
|
return context.failure('rewrite_block', error);
|
|
2670
2672
|
}
|
|
2671
2673
|
}
|
|
2672
|
-
const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
2673
2674
|
function inlineFromSnapshotRun(run) {
|
|
2674
2675
|
const inline = {};
|
|
2675
2676
|
for (const key of RUN_BOOL_MARKS) {
|
|
@@ -5186,112 +5187,8 @@ async function runScopedReplace(doc, args) {
|
|
|
5186
5187
|
*/
|
|
5187
5188
|
/** Points → twips: the node read side reports points, mutations take twips. */
|
|
5188
5189
|
const TWIPS_PER_POINT = 20;
|
|
5189
|
-
/**
|
|
5190
|
-
* Delimiters that separate a list item's lead-in label from its body text.
|
|
5191
|
-
* Spaced dashes are listed separately from bare ones because the label run
|
|
5192
|
-
* usually stops BEFORE the space ("JavaScript / TypeScript" + " — SuperDoc…").
|
|
5193
|
-
*/
|
|
5194
|
-
const LEAD_IN_DELIMITERS = [':', ' — ', ' – ', ' - ', '—', '–'];
|
|
5195
|
-
/** A lead-in is a LABEL, not a sentence — longer spans are prose, not a pattern. */
|
|
5196
|
-
const LEAD_IN_MAX_CHARS = 90;
|
|
5197
|
-
/** Sentence characters sampled past the label, to read the body's own look. */
|
|
5198
|
-
const LEAD_IN_BODY_SAMPLE_CHARS = 40;
|
|
5199
|
-
/**
|
|
5200
|
-
* Where an item's lead-in label ends.
|
|
5201
|
-
*
|
|
5202
|
-
* The SIBLINGS' delimiter wins whenever the item uses it: with a dash-style
|
|
5203
|
-
* list, "Rust: Advanced — details" splits at the dash, not at the colon inside
|
|
5204
|
-
* the label. Falling back to the earliest known delimiter is only for items
|
|
5205
|
-
* that do not use the siblings' one at all.
|
|
5206
|
-
*/
|
|
5207
|
-
function leadInEndOf(text, preferred) {
|
|
5208
|
-
const at = (delimiter) => {
|
|
5209
|
-
const index = text.indexOf(delimiter);
|
|
5210
|
-
return index > 0 && index <= LEAD_IN_MAX_CHARS ? index : null;
|
|
5211
|
-
};
|
|
5212
|
-
if (preferred) {
|
|
5213
|
-
const preferredAt = at(preferred);
|
|
5214
|
-
if (preferredAt != null)
|
|
5215
|
-
return preferredAt;
|
|
5216
|
-
}
|
|
5217
|
-
const found = LEAD_IN_DELIMITERS.map(at).filter((index) => index != null);
|
|
5218
|
-
return found.length > 0 ? Math.min(...found) : null;
|
|
5219
|
-
}
|
|
5220
|
-
/** Quote characters a defined term is wrapped in ("Deliverables" means …). */
|
|
5221
|
-
const LEAD_IN_QUOTES = ['"', '\u201c', '\u2018', "'"];
|
|
5222
|
-
/**
|
|
5223
|
-
* Delimiters allowed to sit INSIDE the label run. Wider than the set used to
|
|
5224
|
-
* split at a delimiter that follows the label, because a sentence period only
|
|
5225
|
-
* counts as a label boundary when the reference's own emphasised run ends with
|
|
5226
|
-
* it — "1.7. Records. " — which is evidence no bare text scan can provide.
|
|
5227
|
-
*/
|
|
5228
|
-
const LEAD_IN_TAIL_DELIMITERS = ['. ', ': ', ...LEAD_IN_DELIMITERS];
|
|
5229
|
-
/**
|
|
5230
|
-
* Read the label shape a reference block uses, from its text and its label run.
|
|
5231
|
-
*
|
|
5232
|
-
* The delimiter has to sit at the label's edge: immediately AFTER it ("Label:
|
|
5233
|
-
* sentence"), or at its own tail ("1.7. Records. " — a numbered clause whose
|
|
5234
|
-
* marker and label share one run). That adjacency is what distinguishes a label
|
|
5235
|
-
* from an ordinary emphasised opening clause.
|
|
5236
|
-
*/
|
|
5237
|
-
function leadInShapeOf(blockText, labelText, quote) {
|
|
5238
|
-
if (quote)
|
|
5239
|
-
return { kind: 'quoted', quote };
|
|
5240
|
-
const after = LEAD_IN_DELIMITERS.find((d) => blockText.startsWith(d, labelText.length));
|
|
5241
|
-
if (after)
|
|
5242
|
-
return { kind: 'before', delimiter: after };
|
|
5243
|
-
const trimmed = labelText.trimEnd();
|
|
5244
|
-
const inside = LEAD_IN_TAIL_DELIMITERS.find((d) => trimmed.endsWith(d.trimEnd()) && trimmed.length > d.trimEnd().length);
|
|
5245
|
-
if (inside) {
|
|
5246
|
-
const occurrence = labelText.split(inside).length - 1;
|
|
5247
|
-
if (occurrence > 0)
|
|
5248
|
-
return { kind: 'inside', delimiter: inside, occurrence };
|
|
5249
|
-
}
|
|
5250
|
-
// A single emphasised word followed by a space and plain prose: the token
|
|
5251
|
-
// itself is the label ("WHEREAS the Party…"). Multi-word runs stay
|
|
5252
|
-
// unrecognised — an emphasised opening clause is prose, not a label.
|
|
5253
|
-
if (/^\S+\s$/.test(labelText) && blockText.length > labelText.length && blockText.startsWith(labelText)) {
|
|
5254
|
-
return { kind: 'word', label: labelText.trimEnd() };
|
|
5255
|
-
}
|
|
5256
|
-
return null;
|
|
5257
|
-
}
|
|
5258
|
-
/** The span of `text` that carries the label look, or null when it has none. */
|
|
5259
5190
|
function leadInSpanOf(text, shape) {
|
|
5260
|
-
|
|
5261
|
-
return typedCaptionSpanOf(text, shape.delimiter);
|
|
5262
|
-
if (shape.kind === 'word') {
|
|
5263
|
-
// Only the SAME token inherits the look — "WHEREAS the Parties…" does,
|
|
5264
|
-
// "The Parties further agree…" does not (no guessing a label there).
|
|
5265
|
-
const next = text.charAt(shape.label.length);
|
|
5266
|
-
return text.startsWith(shape.label) && (next === '' || /\s/.test(next))
|
|
5267
|
-
? { start: 0, end: shape.label.length }
|
|
5268
|
-
: null;
|
|
5269
|
-
}
|
|
5270
|
-
if (shape.kind === 'quoted') {
|
|
5271
|
-
const opener = text.slice(0, 2).indexOf(shape.quote);
|
|
5272
|
-
if (opener < 0)
|
|
5273
|
-
return null;
|
|
5274
|
-
const close = text.indexOf(shape.quote, opener + 1);
|
|
5275
|
-
if (close < 0 || close - opener - 1 <= 0 || close > LEAD_IN_MAX_CHARS)
|
|
5276
|
-
return null;
|
|
5277
|
-
return { start: opener + 1, end: close };
|
|
5278
|
-
}
|
|
5279
|
-
if (shape.kind === 'inside') {
|
|
5280
|
-
// Mirror the reference's STRUCTURE: "1.7. Records. " ends at its second
|
|
5281
|
-
// ". ", so "1.8. Review. The parties…" must end at its second one too —
|
|
5282
|
-
// stopping at the first would emphasise the number alone.
|
|
5283
|
-
let at = -1;
|
|
5284
|
-
for (let seen = 0; seen < shape.occurrence; seen += 1) {
|
|
5285
|
-
at = text.indexOf(shape.delimiter, at + 1);
|
|
5286
|
-
if (at < 0)
|
|
5287
|
-
return null;
|
|
5288
|
-
}
|
|
5289
|
-
if (at <= 0 || at > LEAD_IN_MAX_CHARS)
|
|
5290
|
-
return null;
|
|
5291
|
-
return { start: 0, end: at + shape.delimiter.length };
|
|
5292
|
-
}
|
|
5293
|
-
const end = leadInEndOf(text, shape.delimiter);
|
|
5294
|
-
return end == null ? null : { start: 0, end };
|
|
5191
|
+
return shape.kind === 'typed' ? typedCaptionSpanOf(text, shape.delimiter) : labelLeadInSpanOf(text, shape);
|
|
5295
5192
|
}
|
|
5296
5193
|
const TYPED_MARKER_CORE = String.raw `(?:\d{1,3}(?:\.\d{1,3})*|[ivx]{1,6}|[IVX]{1,6}|[a-z]{1,2}|[A-Z])`;
|
|
5297
5194
|
const TYPED_MARKER_TOKEN = String.raw `(?:\(${TYPED_MARKER_CORE}\)|${TYPED_MARKER_CORE}[.)])`;
|
|
@@ -6564,26 +6461,12 @@ async function matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anc
|
|
|
6564
6461
|
applied.indent = refIndent;
|
|
6565
6462
|
}
|
|
6566
6463
|
}
|
|
6567
|
-
// Lead-marker pattern: manually lettered/numbered sections style the
|
|
6568
|
-
// marker run differently from the body ("(m) " bold, rest plain) — a
|
|
6569
|
-
// per-run pattern blocks.list rows cannot express. When the reference
|
|
6570
|
-
// block opens with such a styled marker and the created text opens with a
|
|
6571
|
-
// similar marker token, reproduce the reference's marker/body split on the
|
|
6572
|
-
// created block. Skipped when the caller supplied explicit runs/marks.
|
|
6573
6464
|
if (allowLeadPattern) {
|
|
6574
6465
|
const lead = await matchLeadMarkerPattern(doc, reference, created, createdText);
|
|
6575
6466
|
if (lead?.changed)
|
|
6576
6467
|
applied.leadMarker = lead;
|
|
6577
|
-
// matchLeadMarkerPattern only knows a shared literal lead ("WHEREAS") and
|
|
6578
|
-
// numbered tokens ("1.", "(m)"), and it needs the reference split across
|
|
6579
|
-
// two runs. Sample the reference's RUNS for everything else: a quoted
|
|
6580
|
-
// defined term (`"Deliverables" means …`), a "1.7. Records. " clause whose
|
|
6581
|
-
// marker and label share a run. A UNIFORM reference is not a split, so
|
|
6582
|
-
// its emphasis never transfers.
|
|
6583
6468
|
else {
|
|
6584
6469
|
const runPattern = await matchBlockRunPattern(doc, reference, created, createdText);
|
|
6585
|
-
// Assigning `undefined` would still create the key, and an `applied` with
|
|
6586
|
-
// keys reads as "this block was matched" — hiding a block that got nothing.
|
|
6587
6470
|
if (runPattern)
|
|
6588
6471
|
applied.runPattern = runPattern;
|
|
6589
6472
|
}
|
|
@@ -6633,21 +6516,6 @@ function typedShapeReference(postBlocks, preIds, created, createdIndex, createdT
|
|
|
6633
6516
|
nearest((rowShape) => markerShapeFits(rowShape, shape)) ??
|
|
6634
6517
|
reference);
|
|
6635
6518
|
}
|
|
6636
|
-
/** A lead-in marker token like "(m)", "1.", "a)", "(iv)" at the start of clause text. */
|
|
6637
|
-
const LEAD_MARKER_PATTERN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
6638
|
-
/**
|
|
6639
|
-
* Reproduce a reference block's lead/body run split on a freshly-created
|
|
6640
|
-
* block. Fires only when the reference genuinely has the pattern — its first
|
|
6641
|
-
* run is a SHORT lead-in styled differently from the body — and the created
|
|
6642
|
-
* text opens with a matching lead of its own. Two lead shapes are recognized:
|
|
6643
|
-
* - literal lead words shared by siblings ("WHEREAS", "NOW, THEREFORE"): the
|
|
6644
|
-
* created text starts with the reference's exact lead text;
|
|
6645
|
-
* - marker tokens ("(m)", "1.", "a)"): the token text differs per clause, so
|
|
6646
|
-
* the created lead is its OWN token matched by shape.
|
|
6647
|
-
* Applies the reference lead's marks to the created lead span and only the
|
|
6648
|
-
* reference body's font and size to the rest: the body never gains emphasis or
|
|
6649
|
-
* an explicit mark-off it did not already carry.
|
|
6650
|
-
*/
|
|
6651
6519
|
async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
6652
6520
|
const createdPreview = created.textPreview ?? '';
|
|
6653
6521
|
if (!reference.nodeId || !created.nodeId || !createdPreview)
|
|
@@ -6656,60 +6524,20 @@ async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
|
6656
6524
|
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
6657
6525
|
if (!queryFn || !formatFn)
|
|
6658
6526
|
return null;
|
|
6659
|
-
|
|
6660
|
-
// returned tile the MATCHED range, covering the lead and the body start.
|
|
6661
|
-
// blocks.list truncates textPreview with a trailing ellipsis — strip it, the
|
|
6662
|
-
// remaining prefix IS document text so query.match can find it.
|
|
6663
|
-
const refProbe = (reference.textPreview ?? '')
|
|
6664
|
-
.replace(/…\s*$/, '')
|
|
6665
|
-
.slice(0, 60)
|
|
6666
|
-
.trim();
|
|
6527
|
+
const refProbe = referenceProbeOf(reference.textPreview ?? '');
|
|
6667
6528
|
if (!refProbe)
|
|
6668
6529
|
return null;
|
|
6669
|
-
const
|
|
6670
|
-
if (
|
|
6671
|
-
return null; // uniform reference — no split to copy
|
|
6672
|
-
const leadRun = refRuns[0];
|
|
6673
|
-
const bodyRun = refRuns[1];
|
|
6674
|
-
// The pattern requires a real FORMATTING transition between lead and body
|
|
6675
|
-
// (bold/italic/underline/strike). Runs split only by font/color/hyperlink
|
|
6676
|
-
// boundaries are ordinary prose — restyling from those would misfire.
|
|
6677
|
-
if (RUN_BOOL_MARKS.every((key) => Boolean(leadRun[key]) === Boolean(bodyRun[key])))
|
|
6678
|
-
return null;
|
|
6679
|
-
const refLead = leadRun.text.trimEnd();
|
|
6680
|
-
// The first run must be a short lead-in (a token or a word or two), not
|
|
6681
|
-
// half the sentence.
|
|
6682
|
-
if (refLead.length === 0 || refLead.length > 40 || leadRun.start !== 0)
|
|
6530
|
+
const split = leadBodySplitOf(await matchRunsForBlock(queryFn, refProbe, reference.nodeId));
|
|
6531
|
+
if (!split)
|
|
6683
6532
|
return null;
|
|
6684
|
-
|
|
6685
|
-
const
|
|
6686
|
-
// The created block's own lead span (ASCII markers only by design).
|
|
6687
|
-
let createdLeadLen = 0;
|
|
6688
|
-
const boundaryAfter = (len) => len >= fullText.length || !/[A-Za-z0-9]/.test(fullText[len]);
|
|
6689
|
-
if (fullText.startsWith(refLead) && boundaryAfter(refLead.length)) {
|
|
6690
|
-
// Literal shared lead word(s) — "WHEREAS, …" after a "WHEREAS, …" sibling.
|
|
6691
|
-
// Exact-case with a token boundary so "Note" never claims "Notebook".
|
|
6692
|
-
createdLeadLen = refLead.length;
|
|
6693
|
-
}
|
|
6694
|
-
else {
|
|
6695
|
-
const refMarker = LEAD_MARKER_PATTERN.exec(refLead);
|
|
6696
|
-
const createdMarker = LEAD_MARKER_PATTERN.exec(fullText);
|
|
6697
|
-
// Marker tokens: the reference's first run must BE the token, and the
|
|
6698
|
-
// created text must open with a token of the same category (letters
|
|
6699
|
-
// with letters, digits with digits — "(m)" doesn't restyle "1.").
|
|
6700
|
-
if (refMarker &&
|
|
6701
|
-
createdMarker &&
|
|
6702
|
-
refMarker[0] === refLead &&
|
|
6703
|
-
/\d/.test(refMarker[0]) === /\d/.test(createdMarker[0])) {
|
|
6704
|
-
createdLeadLen = createdMarker[0].length;
|
|
6705
|
-
}
|
|
6706
|
-
}
|
|
6533
|
+
const fullText = createdText || withoutTruncationEllipsis(createdPreview);
|
|
6534
|
+
const createdLeadLen = createdLeadLengthOf(split.leadText, fullText);
|
|
6707
6535
|
if (createdLeadLen === 0)
|
|
6708
6536
|
return null;
|
|
6709
|
-
const markerInline = emphasisedLookOf(inlineFromSnapshotRun(
|
|
6537
|
+
const markerInline = emphasisedLookOf(inlineFromSnapshotRun(split.lead));
|
|
6710
6538
|
if (!markerInline)
|
|
6711
6539
|
return null;
|
|
6712
|
-
const bodyInline = baseRunLookOf(
|
|
6540
|
+
const bodyInline = baseRunLookOf(split.body);
|
|
6713
6541
|
let changed = await applyInlineQuiet(formatFn, {
|
|
6714
6542
|
blockId: created.nodeId,
|
|
6715
6543
|
start: 0,
|
|
@@ -6727,21 +6555,10 @@ async function matchLeadMarkerPattern(doc, reference, created, createdText) {
|
|
|
6727
6555
|
}
|
|
6728
6556
|
return { marker: fullText.slice(0, createdLeadLen), markerInline, bodyInline, changed };
|
|
6729
6557
|
}
|
|
6730
|
-
/**
|
|
6731
|
-
* Copy a reference block's RUN pattern onto a created block: the body's font
|
|
6732
|
-
* and size over the whole block, then the label look over the new block's own
|
|
6733
|
-
* label span.
|
|
6734
|
-
*
|
|
6735
|
-
* Complements matchLeadMarkerPattern, which requires either a shared literal
|
|
6736
|
-
* lead word or a marker token AND a two-run reference. This one reads the
|
|
6737
|
-
* reference's runs (`query.match`, the only read that reports run styling) and
|
|
6738
|
-
* handles the shapes that leaves out. A single uniform run has no label, so
|
|
6739
|
-
* nothing but its font transfers.
|
|
6740
|
-
*/
|
|
6741
6558
|
async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
6742
6559
|
const formatFn = maybeMethod(doc, ['format', 'apply']);
|
|
6743
|
-
const referenceText = (reference.textPreview ?? '').
|
|
6744
|
-
const fullText = createdText || (created.textPreview ?? '')
|
|
6560
|
+
const referenceText = withoutTruncationEllipsis(reference.textPreview ?? '').trim();
|
|
6561
|
+
const fullText = createdText || withoutTruncationEllipsis(created.textPreview ?? '');
|
|
6745
6562
|
if (!formatFn || !reference.nodeId || !created.nodeId || !referenceText || !fullText)
|
|
6746
6563
|
return null;
|
|
6747
6564
|
const pattern = await listItemRunPattern(doc, referenceText, reference.nodeId);
|
|
@@ -6753,11 +6570,7 @@ async function matchBlockRunPattern(doc, reference, created, createdText) {
|
|
|
6753
6570
|
const own = Object.keys(sampled).length > 0
|
|
6754
6571
|
? await effectiveBaseLookOf(doc, fullText, { nodeId: created.nodeId, nodeType: created.nodeType })
|
|
6755
6572
|
: null;
|
|
6756
|
-
const body = own
|
|
6757
|
-
? {}
|
|
6758
|
-
: own
|
|
6759
|
-
? Object.fromEntries(Object.entries(sampled).filter(([key, value]) => own[key] !== value))
|
|
6760
|
-
: sampled;
|
|
6573
|
+
const body = bodyLookToWrite(sampled, own);
|
|
6761
6574
|
if (Object.keys(body).length > 0) {
|
|
6762
6575
|
if (await applyInlineQuiet(formatFn, { blockId: created.nodeId, start: 0, end, inline: body })) {
|
|
6763
6576
|
applied.body = body;
|
|
@@ -6783,17 +6596,10 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6783
6596
|
.filter(({ row }) => row.nodeId && !preIds.has(row.nodeId) && !row.isEmpty);
|
|
6784
6597
|
if (createdRows.length === 0)
|
|
6785
6598
|
return receipt;
|
|
6786
|
-
|
|
6787
|
-
// statement of "blend in with THIS" — prefer it over type-matched siblings.
|
|
6788
|
-
const placement = isRecord(args?.placement) ? args.placement : null;
|
|
6789
|
-
const anchorSelector = placement && isRecord(placement.selector) ? placement.selector : null;
|
|
6790
|
-
const anchorId = anchorSelector && typeof anchorSelector.nodeId === 'string' ? anchorSelector.nodeId : null;
|
|
6599
|
+
const anchorId = placementAnchorIdOf(args);
|
|
6791
6600
|
const anchorRow = anchorId ? (postBlocks.find((b) => b.nodeId === anchorId) ?? null) : null;
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
// pattern pass must not fight it.
|
|
6795
|
-
const rawTexts = Array.isArray(args?.texts) ? args.texts : args?.text != null ? [args.text] : [];
|
|
6796
|
-
const hasExplicitStyling = rawTexts.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
6601
|
+
const rawTexts = insertedTextEntriesOf(args);
|
|
6602
|
+
const hasExplicitStyling = callerStyledAnEntry(rawTexts);
|
|
6797
6603
|
const insertedTexts = rawTexts.map((entry) => {
|
|
6798
6604
|
if (typeof entry === 'string')
|
|
6799
6605
|
return entry;
|
|
@@ -6801,26 +6607,12 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6801
6607
|
return spec ? itemSpecText(spec) : '';
|
|
6802
6608
|
});
|
|
6803
6609
|
const allApplied = [];
|
|
6804
|
-
for (
|
|
6805
|
-
const { row: created, index: createdIndex } = createdRows[ci];
|
|
6806
|
-
// The first of several inserted blocks is the section TITLE — it should
|
|
6807
|
-
// read like a heading (flush-left), so it must NOT inherit the body
|
|
6808
|
-
// sibling's left indent even though it matches the same anchor.
|
|
6809
|
-
const isTitle = createdRows.length > 1 && ci === 0;
|
|
6610
|
+
for (const [ci, { row: created, index: createdIndex }] of createdRows.entries()) {
|
|
6810
6611
|
const createdText = insertedTexts[ci] ?? '';
|
|
6811
|
-
const match = (anchor) => matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anchor, createdRows.length === 1,
|
|
6812
|
-
// The texts the caller inserted, in order — the truncated preview
|
|
6813
|
-
// cannot measure a label span or the end of the block.
|
|
6814
|
-
createdText);
|
|
6612
|
+
const match = (anchor) => matchOneBlock(doc, postBlocks, preIds, created, createdIndex, anchor, createdRows.length === 1, isSectionTitle(ci, createdRows.length), !hasExplicitStyling, createdText);
|
|
6815
6613
|
let result = await match(anchorRow);
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
// the previous NAME line, not like the "Name:" line directly above it.
|
|
6819
|
-
// Only tried when matching against the neighbour found nothing, so this
|
|
6820
|
-
// cannot change a case that already had an answer.
|
|
6821
|
-
const applied = isRecord(result?.applied) ? result.applied : null;
|
|
6822
|
-
if (createdRows.length > 1 && (!applied || Object.keys(applied).length === 0)) {
|
|
6823
|
-
const parallel = postBlocks[createdIndex - createdRows.length];
|
|
6614
|
+
if (shouldTryParallelGroup(createdRows.length, result)) {
|
|
6615
|
+
const parallel = postBlocks[parallelGroupIndexOf(createdIndex, createdRows.length)];
|
|
6824
6616
|
if (parallel && preIds.has(parallel.nodeId) && !parallel.isEmpty) {
|
|
6825
6617
|
result = (await match(parallel)) ?? result;
|
|
6826
6618
|
}
|
|
@@ -6828,13 +6620,12 @@ async function matchInsertedBlockFormatting(doc, preBlocks, receipt, args, avail
|
|
|
6828
6620
|
if (result)
|
|
6829
6621
|
allApplied.push(result);
|
|
6830
6622
|
}
|
|
6831
|
-
|
|
6623
|
+
const contextualFormatting = contextualFormattingOf(allApplied);
|
|
6624
|
+
if (!contextualFormatting)
|
|
6832
6625
|
return receipt;
|
|
6833
|
-
return { ...receipt, contextualFormatting
|
|
6626
|
+
return { ...receipt, contextualFormatting };
|
|
6834
6627
|
}
|
|
6835
6628
|
catch (err) {
|
|
6836
|
-
// Formatting matching is best-effort polish: never fail a successful
|
|
6837
|
-
// insert because of it — but the receipt says it was skipped.
|
|
6838
6629
|
return {
|
|
6839
6630
|
...receipt,
|
|
6840
6631
|
contextualFormatting: { skipped: reasonOf(err) },
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var guards = require('../guards.cjs');
|
|
4
|
+
|
|
5
|
+
const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
6
|
+
const TRUNCATED_PREVIEW_TAIL = /…\s*$/;
|
|
7
|
+
const REFERENCE_PROBE_CHARS = 60;
|
|
8
|
+
const LEAD_RUN_MAX_CHARS = 40;
|
|
9
|
+
const MARKER_TOKEN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
10
|
+
const ASCII_WORD_CHARACTER = /[A-Za-z0-9]/;
|
|
11
|
+
const DIGIT = /\d/;
|
|
12
|
+
function withoutTruncationEllipsis(preview) {
|
|
13
|
+
return preview.replace(TRUNCATED_PREVIEW_TAIL, '');
|
|
14
|
+
}
|
|
15
|
+
function referenceProbeOf(referencePreview) {
|
|
16
|
+
return withoutTruncationEllipsis(referencePreview).slice(0, REFERENCE_PROBE_CHARS).trim();
|
|
17
|
+
}
|
|
18
|
+
function differsInEmphasis(a, b) {
|
|
19
|
+
return RUN_BOOL_MARKS.some((key) => Boolean(a[key]) !== Boolean(b[key]));
|
|
20
|
+
}
|
|
21
|
+
function leadBodySplitOf(referenceRuns) {
|
|
22
|
+
const [lead, body] = referenceRuns;
|
|
23
|
+
if (!lead || !body || !differsInEmphasis(lead, body))
|
|
24
|
+
return null;
|
|
25
|
+
const leadText = lead.text.trimEnd();
|
|
26
|
+
const isShortOpeningRun = leadText.length > 0 && leadText.length <= LEAD_RUN_MAX_CHARS && lead.start === 0;
|
|
27
|
+
return isShortOpeningRun ? { lead, body, leadText } : null;
|
|
28
|
+
}
|
|
29
|
+
function endsAtTokenBoundary(text, length) {
|
|
30
|
+
return length >= text.length || !ASCII_WORD_CHARACTER.test(text.charAt(length));
|
|
31
|
+
}
|
|
32
|
+
function sharedLiteralLeadLength(referenceLead, createdText) {
|
|
33
|
+
const opensWithSameLead = createdText.startsWith(referenceLead) && endsAtTokenBoundary(createdText, referenceLead.length);
|
|
34
|
+
return opensWithSameLead ? referenceLead.length : 0;
|
|
35
|
+
}
|
|
36
|
+
function sameCategoryMarkerLength(referenceLead, createdText) {
|
|
37
|
+
const referenceMarker = MARKER_TOKEN.exec(referenceLead)?.[0];
|
|
38
|
+
const createdMarker = MARKER_TOKEN.exec(createdText)?.[0];
|
|
39
|
+
if (referenceMarker == null || createdMarker == null || referenceMarker !== referenceLead)
|
|
40
|
+
return 0;
|
|
41
|
+
return DIGIT.test(referenceMarker) === DIGIT.test(createdMarker) ? createdMarker.length : 0;
|
|
42
|
+
}
|
|
43
|
+
function createdLeadLengthOf(referenceLead, createdText) {
|
|
44
|
+
return sharedLiteralLeadLength(referenceLead, createdText) || sameCategoryMarkerLength(referenceLead, createdText);
|
|
45
|
+
}
|
|
46
|
+
function bodyLookToWrite(sampled, own) {
|
|
47
|
+
if (own === 'mixed')
|
|
48
|
+
return {};
|
|
49
|
+
if (!own)
|
|
50
|
+
return sampled;
|
|
51
|
+
return Object.fromEntries(Object.entries(sampled).filter(([key, value]) => own[key] !== value));
|
|
52
|
+
}
|
|
53
|
+
function placementAnchorIdOf(args) {
|
|
54
|
+
const placement = args?.placement;
|
|
55
|
+
const selector = guards.isRecord(placement) ? placement.selector : null;
|
|
56
|
+
return guards.isRecord(selector) && typeof selector.nodeId === 'string' ? selector.nodeId : null;
|
|
57
|
+
}
|
|
58
|
+
function insertedTextEntriesOf(args) {
|
|
59
|
+
const texts = args?.texts;
|
|
60
|
+
if (Array.isArray(texts))
|
|
61
|
+
return texts;
|
|
62
|
+
const text = args?.text;
|
|
63
|
+
return text != null ? [text] : [];
|
|
64
|
+
}
|
|
65
|
+
function callerStyledAnEntry(entries) {
|
|
66
|
+
return entries.some((entry) => guards.isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
67
|
+
}
|
|
68
|
+
function isSectionTitle(insertedIndex, insertedCount) {
|
|
69
|
+
return insertedCount > 1 && insertedIndex === 0;
|
|
70
|
+
}
|
|
71
|
+
function appliedNothing(result) {
|
|
72
|
+
const applied = result?.applied;
|
|
73
|
+
return !guards.isRecord(applied) || Object.keys(applied).length === 0;
|
|
74
|
+
}
|
|
75
|
+
function shouldTryParallelGroup(insertedCount, result) {
|
|
76
|
+
return insertedCount > 1 && appliedNothing(result);
|
|
77
|
+
}
|
|
78
|
+
function parallelGroupIndexOf(createdIndex, insertedCount) {
|
|
79
|
+
return createdIndex - insertedCount;
|
|
80
|
+
}
|
|
81
|
+
function contextualFormattingOf(results) {
|
|
82
|
+
const [only, ...rest] = results;
|
|
83
|
+
if (!only)
|
|
84
|
+
return null;
|
|
85
|
+
return rest.length === 0 ? only : results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
exports.LEAD_RUN_MAX_CHARS = LEAD_RUN_MAX_CHARS;
|
|
89
|
+
exports.REFERENCE_PROBE_CHARS = REFERENCE_PROBE_CHARS;
|
|
90
|
+
exports.RUN_BOOL_MARKS = RUN_BOOL_MARKS;
|
|
91
|
+
exports.bodyLookToWrite = bodyLookToWrite;
|
|
92
|
+
exports.callerStyledAnEntry = callerStyledAnEntry;
|
|
93
|
+
exports.contextualFormattingOf = contextualFormattingOf;
|
|
94
|
+
exports.createdLeadLengthOf = createdLeadLengthOf;
|
|
95
|
+
exports.insertedTextEntriesOf = insertedTextEntriesOf;
|
|
96
|
+
exports.isSectionTitle = isSectionTitle;
|
|
97
|
+
exports.leadBodySplitOf = leadBodySplitOf;
|
|
98
|
+
exports.parallelGroupIndexOf = parallelGroupIndexOf;
|
|
99
|
+
exports.placementAnchorIdOf = placementAnchorIdOf;
|
|
100
|
+
exports.referenceProbeOf = referenceProbeOf;
|
|
101
|
+
exports.shouldTryParallelGroup = shouldTryParallelGroup;
|
|
102
|
+
exports.withoutTruncationEllipsis = withoutTruncationEllipsis;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SnapshotRun } from './doc-snapshot.js';
|
|
2
|
+
export declare const RUN_BOOL_MARKS: readonly ["bold", "italic", "underline", "strike"];
|
|
3
|
+
export declare const REFERENCE_PROBE_CHARS = 60;
|
|
4
|
+
export declare const LEAD_RUN_MAX_CHARS = 40;
|
|
5
|
+
export declare function withoutTruncationEllipsis(preview: string): string;
|
|
6
|
+
export declare function referenceProbeOf(referencePreview: string): string;
|
|
7
|
+
export type LeadBodySplit = {
|
|
8
|
+
lead: SnapshotRun;
|
|
9
|
+
body: SnapshotRun;
|
|
10
|
+
leadText: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function leadBodySplitOf(referenceRuns: readonly SnapshotRun[]): LeadBodySplit | null;
|
|
13
|
+
export declare function createdLeadLengthOf(referenceLead: string, createdText: string): number;
|
|
14
|
+
export type BlockBaseLook = Record<string, unknown> | 'mixed' | null;
|
|
15
|
+
export declare function bodyLookToWrite(sampled: Record<string, unknown>, own: BlockBaseLook): Record<string, unknown>;
|
|
16
|
+
export declare function placementAnchorIdOf(args: Record<string, unknown> | undefined): string | null;
|
|
17
|
+
export declare function insertedTextEntriesOf(args: Record<string, unknown> | undefined): unknown[];
|
|
18
|
+
export declare function callerStyledAnEntry(entries: readonly unknown[]): boolean;
|
|
19
|
+
export declare function isSectionTitle(insertedIndex: number, insertedCount: number): boolean;
|
|
20
|
+
export declare function shouldTryParallelGroup(insertedCount: number, result: Record<string, unknown> | null): boolean;
|
|
21
|
+
export declare function parallelGroupIndexOf(createdIndex: number, insertedCount: number): number;
|
|
22
|
+
export declare function contextualFormattingOf(results: Record<string, unknown>[]): Record<string, unknown> | Record<string, unknown>[] | null;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isRecord } from '../guards.js';
|
|
2
|
+
export const RUN_BOOL_MARKS = ['bold', 'italic', 'underline', 'strike'];
|
|
3
|
+
const TRUNCATED_PREVIEW_TAIL = /…\s*$/;
|
|
4
|
+
export const REFERENCE_PROBE_CHARS = 60;
|
|
5
|
+
export const LEAD_RUN_MAX_CHARS = 40;
|
|
6
|
+
const MARKER_TOKEN = /^\(?(?:[a-zA-Z]{1,3}|\d{1,3})[).:\]]/;
|
|
7
|
+
const ASCII_WORD_CHARACTER = /[A-Za-z0-9]/;
|
|
8
|
+
const DIGIT = /\d/;
|
|
9
|
+
export function withoutTruncationEllipsis(preview) {
|
|
10
|
+
return preview.replace(TRUNCATED_PREVIEW_TAIL, '');
|
|
11
|
+
}
|
|
12
|
+
export function referenceProbeOf(referencePreview) {
|
|
13
|
+
return withoutTruncationEllipsis(referencePreview).slice(0, REFERENCE_PROBE_CHARS).trim();
|
|
14
|
+
}
|
|
15
|
+
function differsInEmphasis(a, b) {
|
|
16
|
+
return RUN_BOOL_MARKS.some((key) => Boolean(a[key]) !== Boolean(b[key]));
|
|
17
|
+
}
|
|
18
|
+
export function leadBodySplitOf(referenceRuns) {
|
|
19
|
+
const [lead, body] = referenceRuns;
|
|
20
|
+
if (!lead || !body || !differsInEmphasis(lead, body))
|
|
21
|
+
return null;
|
|
22
|
+
const leadText = lead.text.trimEnd();
|
|
23
|
+
const isShortOpeningRun = leadText.length > 0 && leadText.length <= LEAD_RUN_MAX_CHARS && lead.start === 0;
|
|
24
|
+
return isShortOpeningRun ? { lead, body, leadText } : null;
|
|
25
|
+
}
|
|
26
|
+
function endsAtTokenBoundary(text, length) {
|
|
27
|
+
return length >= text.length || !ASCII_WORD_CHARACTER.test(text.charAt(length));
|
|
28
|
+
}
|
|
29
|
+
function sharedLiteralLeadLength(referenceLead, createdText) {
|
|
30
|
+
const opensWithSameLead = createdText.startsWith(referenceLead) && endsAtTokenBoundary(createdText, referenceLead.length);
|
|
31
|
+
return opensWithSameLead ? referenceLead.length : 0;
|
|
32
|
+
}
|
|
33
|
+
function sameCategoryMarkerLength(referenceLead, createdText) {
|
|
34
|
+
const referenceMarker = MARKER_TOKEN.exec(referenceLead)?.[0];
|
|
35
|
+
const createdMarker = MARKER_TOKEN.exec(createdText)?.[0];
|
|
36
|
+
if (referenceMarker == null || createdMarker == null || referenceMarker !== referenceLead)
|
|
37
|
+
return 0;
|
|
38
|
+
return DIGIT.test(referenceMarker) === DIGIT.test(createdMarker) ? createdMarker.length : 0;
|
|
39
|
+
}
|
|
40
|
+
export function createdLeadLengthOf(referenceLead, createdText) {
|
|
41
|
+
return sharedLiteralLeadLength(referenceLead, createdText) || sameCategoryMarkerLength(referenceLead, createdText);
|
|
42
|
+
}
|
|
43
|
+
export function bodyLookToWrite(sampled, own) {
|
|
44
|
+
if (own === 'mixed')
|
|
45
|
+
return {};
|
|
46
|
+
if (!own)
|
|
47
|
+
return sampled;
|
|
48
|
+
return Object.fromEntries(Object.entries(sampled).filter(([key, value]) => own[key] !== value));
|
|
49
|
+
}
|
|
50
|
+
export function placementAnchorIdOf(args) {
|
|
51
|
+
const placement = args?.placement;
|
|
52
|
+
const selector = isRecord(placement) ? placement.selector : null;
|
|
53
|
+
return isRecord(selector) && typeof selector.nodeId === 'string' ? selector.nodeId : null;
|
|
54
|
+
}
|
|
55
|
+
export function insertedTextEntriesOf(args) {
|
|
56
|
+
const texts = args?.texts;
|
|
57
|
+
if (Array.isArray(texts))
|
|
58
|
+
return texts;
|
|
59
|
+
const text = args?.text;
|
|
60
|
+
return text != null ? [text] : [];
|
|
61
|
+
}
|
|
62
|
+
export function callerStyledAnEntry(entries) {
|
|
63
|
+
return entries.some((entry) => isRecord(entry) && (Array.isArray(entry.runs) || entry.marks != null));
|
|
64
|
+
}
|
|
65
|
+
export function isSectionTitle(insertedIndex, insertedCount) {
|
|
66
|
+
return insertedCount > 1 && insertedIndex === 0;
|
|
67
|
+
}
|
|
68
|
+
function appliedNothing(result) {
|
|
69
|
+
const applied = result?.applied;
|
|
70
|
+
return !isRecord(applied) || Object.keys(applied).length === 0;
|
|
71
|
+
}
|
|
72
|
+
export function shouldTryParallelGroup(insertedCount, result) {
|
|
73
|
+
return insertedCount > 1 && appliedNothing(result);
|
|
74
|
+
}
|
|
75
|
+
export function parallelGroupIndexOf(createdIndex, insertedCount) {
|
|
76
|
+
return createdIndex - insertedCount;
|
|
77
|
+
}
|
|
78
|
+
export function contextualFormattingOf(results) {
|
|
79
|
+
const [only, ...rest] = results;
|
|
80
|
+
if (!only)
|
|
81
|
+
return null;
|
|
82
|
+
return rest.length === 0 ? only : results;
|
|
83
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SPACED_DASH_DELIMITERS = [' — ', ' – ', ' - '];
|
|
4
|
+
const BARE_DASH_DELIMITERS = ['—', '–'];
|
|
5
|
+
const LEAD_IN_DELIMITERS = [':', ...SPACED_DASH_DELIMITERS, ...BARE_DASH_DELIMITERS];
|
|
6
|
+
const DELIMITERS_A_LABEL_RUN_MAY_END_WITH = ['. ', ': ', ...LEAD_IN_DELIMITERS];
|
|
7
|
+
const LEAD_IN_MAX_CHARS = 90;
|
|
8
|
+
const LEAD_IN_BODY_SAMPLE_CHARS = 40;
|
|
9
|
+
const LEAD_IN_QUOTES = ['"', '“', '‘', "'"];
|
|
10
|
+
const QUOTED_TERM_OPENER_WINDOW = 2;
|
|
11
|
+
const SINGLE_WORD_THEN_SPACE = /^\S+\s$/;
|
|
12
|
+
function labelEndAt(text, delimiter) {
|
|
13
|
+
const index = text.indexOf(delimiter);
|
|
14
|
+
return index > 0 && index <= LEAD_IN_MAX_CHARS ? index : null;
|
|
15
|
+
}
|
|
16
|
+
function earliestLeadInEnd(text) {
|
|
17
|
+
const found = LEAD_IN_DELIMITERS.map((delimiter) => labelEndAt(text, delimiter)).filter((index) => index != null);
|
|
18
|
+
return found.length > 0 ? Math.min(...found) : null;
|
|
19
|
+
}
|
|
20
|
+
function leadInEndOf(text, siblingDelimiter) {
|
|
21
|
+
const siblingEnd = siblingDelimiter ? labelEndAt(text, siblingDelimiter) : null;
|
|
22
|
+
return siblingEnd ?? earliestLeadInEnd(text);
|
|
23
|
+
}
|
|
24
|
+
function labelRunTailShapeOf(labelText) {
|
|
25
|
+
const trimmed = labelText.trimEnd();
|
|
26
|
+
const delimiter = DELIMITERS_A_LABEL_RUN_MAY_END_WITH.find((d) => trimmed.endsWith(d.trimEnd()) && trimmed.length > d.trimEnd().length);
|
|
27
|
+
if (!delimiter)
|
|
28
|
+
return null;
|
|
29
|
+
const occurrence = labelText.split(delimiter).length - 1;
|
|
30
|
+
return occurrence > 0 ? { kind: 'inside', delimiter, occurrence } : null;
|
|
31
|
+
}
|
|
32
|
+
function emphasisedOpeningWordOf(blockText, labelText) {
|
|
33
|
+
const opensWithSingleWord = SINGLE_WORD_THEN_SPACE.test(labelText) && blockText.length > labelText.length && blockText.startsWith(labelText);
|
|
34
|
+
return opensWithSingleWord ? { kind: 'word', label: labelText.trimEnd() } : null;
|
|
35
|
+
}
|
|
36
|
+
function leadInShapeOf(blockText, labelText, quote) {
|
|
37
|
+
if (quote)
|
|
38
|
+
return { kind: 'quoted', quote };
|
|
39
|
+
const delimiterAfterLabel = LEAD_IN_DELIMITERS.find((d) => blockText.startsWith(d, labelText.length));
|
|
40
|
+
if (delimiterAfterLabel)
|
|
41
|
+
return { kind: 'before', delimiter: delimiterAfterLabel };
|
|
42
|
+
return labelRunTailShapeOf(labelText) ?? emphasisedOpeningWordOf(blockText, labelText);
|
|
43
|
+
}
|
|
44
|
+
function isWordBoundary(character) {
|
|
45
|
+
return character === '' || /\s/.test(character);
|
|
46
|
+
}
|
|
47
|
+
function openingWordSpanOf(text, shape) {
|
|
48
|
+
const opensWithSameWord = text.startsWith(shape.label) && isWordBoundary(text.charAt(shape.label.length));
|
|
49
|
+
return opensWithSameWord ? { start: 0, end: shape.label.length } : null;
|
|
50
|
+
}
|
|
51
|
+
function quotedTermSpanOf(text, shape) {
|
|
52
|
+
const opener = text.slice(0, QUOTED_TERM_OPENER_WINDOW).indexOf(shape.quote);
|
|
53
|
+
if (opener < 0)
|
|
54
|
+
return null;
|
|
55
|
+
const termStart = opener + 1;
|
|
56
|
+
const close = text.indexOf(shape.quote, termStart);
|
|
57
|
+
const closesAfterNonEmptyTerm = close > termStart;
|
|
58
|
+
if (!closesAfterNonEmptyTerm || close > LEAD_IN_MAX_CHARS)
|
|
59
|
+
return null;
|
|
60
|
+
return { start: termStart, end: close };
|
|
61
|
+
}
|
|
62
|
+
function nthIndexOf(text, search, occurrence) {
|
|
63
|
+
let at = -1;
|
|
64
|
+
for (let seen = 0; seen < occurrence; seen += 1) {
|
|
65
|
+
at = text.indexOf(search, at + 1);
|
|
66
|
+
if (at < 0)
|
|
67
|
+
return -1;
|
|
68
|
+
}
|
|
69
|
+
return at;
|
|
70
|
+
}
|
|
71
|
+
function spanThroughSameDelimiterCountOf(text, shape) {
|
|
72
|
+
const at = nthIndexOf(text, shape.delimiter, shape.occurrence);
|
|
73
|
+
if (at <= 0 || at > LEAD_IN_MAX_CHARS)
|
|
74
|
+
return null;
|
|
75
|
+
return { start: 0, end: at + shape.delimiter.length };
|
|
76
|
+
}
|
|
77
|
+
function spanBeforeDelimiterOf(text, shape) {
|
|
78
|
+
const end = leadInEndOf(text, shape.delimiter);
|
|
79
|
+
return end == null ? null : { start: 0, end };
|
|
80
|
+
}
|
|
81
|
+
function labelLeadInSpanOf(text, shape) {
|
|
82
|
+
switch (shape.kind) {
|
|
83
|
+
case 'word':
|
|
84
|
+
return openingWordSpanOf(text, shape);
|
|
85
|
+
case 'quoted':
|
|
86
|
+
return quotedTermSpanOf(text, shape);
|
|
87
|
+
case 'inside':
|
|
88
|
+
return spanThroughSameDelimiterCountOf(text, shape);
|
|
89
|
+
case 'before':
|
|
90
|
+
return spanBeforeDelimiterOf(text, shape);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exports.LEAD_IN_BODY_SAMPLE_CHARS = LEAD_IN_BODY_SAMPLE_CHARS;
|
|
95
|
+
exports.LEAD_IN_DELIMITERS = LEAD_IN_DELIMITERS;
|
|
96
|
+
exports.LEAD_IN_MAX_CHARS = LEAD_IN_MAX_CHARS;
|
|
97
|
+
exports.LEAD_IN_QUOTES = LEAD_IN_QUOTES;
|
|
98
|
+
exports.labelLeadInSpanOf = labelLeadInSpanOf;
|
|
99
|
+
exports.leadInEndOf = leadInEndOf;
|
|
100
|
+
exports.leadInShapeOf = leadInShapeOf;
|