or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

collaboration.mdcommands-and-editing.mdcursors-and-enhancements.mdhistory.mdindex.mdinput-and-keymaps.mdmarkdown.mdmenus-and-ui.mdmodel-and-schema.mdschema-definitions.mdstate-management.mdtables.mdtransformations.mdview-and-rendering.md

transformations.mddocs/

0

# Transformations

1

2

The transformation system provides atomic document changes, position tracking, and mapping operations. It ensures consistency when multiple changes occur simultaneously and enables features like collaborative editing and undo/redo.

3

4

## Capabilities

5

6

### Transform Class

7

8

The main class for tracking and applying document changes.

9

10

```typescript { .api }

11

/**

12

* Tracks a series of steps that transform a document

13

*/

14

class Transform {

15

/**

16

* Create a new transform

17

*/

18

constructor(doc: Node);

19

20

/**

21

* The current document state

22

*/

23

doc: Node;

24

25

/**

26

* Array of steps applied to reach current state

27

*/

28

steps: Step[];

29

30

/**

31

* Array of documents after each step

32

*/

33

docs: Node[];

34

35

/**

36

* Current mapping from original positions

37

*/

38

mapping: Mapping;

39

40

/**

41

* Apply a step to the transform

42

*/

43

step(step: Step): Transform;

44

45

/**

46

* Apply a step if possible, return the transform

47

*/

48

maybeStep(step: Step): StepResult<Transform>;

49

}

50

```

51

52

### Step Class

53

54

Abstract base class for atomic document changes.

55

56

```typescript { .api }

57

/**

58

* Abstract base class for document transformation steps

59

*/

60

abstract class Step {

61

/**

62

* Apply this step to a document

63

*/

64

apply(doc: Node): StepResult<Node>;

65

66

/**

67

* Get the position mapping for this step

68

*/

69

getMap(): StepMap;

70

71

/**

72

* Create the inverse of this step

73

*/

74

invert(doc: Node): Step;

75

76

/**

77

* Map this step through a mapping

78

*/

79

map(mapping: Mappable): Step | null;

80

81

/**

82

* Try to merge this step with another step

83

*/

84

merge(other: Step): Step | null;

85

86

/**

87

* Convert step to JSON

88

*/

89

toJSON(): any;

90

91

/**

92

* Create step from JSON

93

*/

94

static fromJSON(schema: Schema, json: any): Step;

95

}

96

```

97

98

### Mapping Classes

99

100

Classes for tracking position changes through transformations.

101

102

```typescript { .api }

103

/**

104

* Maps positions through document changes

105

*/

106

class Mapping {

107

/**

108

* Create a new mapping

109

*/

110

constructor(maps?: StepMap[]);

111

112

/**

113

* Array of step maps

114

*/

115

maps: StepMap[];

116

117

/**

118

* Add a step map to the mapping

119

*/

120

appendMap(map: StepMap, mirrors?: number): void;

121

122

/**

123

* Append another mapping

124

*/

125

appendMapping(mapping: Mappable): void;

126

127

/**

128

* Map a position through the mapping

129

*/

130

map(pos: number, assoc?: number): number;

131

132

/**

133

* Map a position backwards

134

*/

135

mapResult(pos: number, assoc?: number): MapResult;

136

137

/**

138

* Get the slice of mapping from one index to another

139

*/

140

slice(from?: number, to?: number): Mapping;

141

}

142

143

/**

144

* Individual step's position mapping

145

*/

146

class StepMap {

147

/**

148

* Create a step map

149

*/

150

constructor(ranges: number[]);

151

152

/**

153

* Map a position through this step

154

*/

155

map(pos: number, assoc?: number): number;

156

157

/**

158

* Map a position with detailed result

159

*/

160

mapResult(pos: number, assoc?: number): MapResult;

161

162

/**

163

* Invert this step map

164

*/

165

invert(): StepMap;

166

167

/**

168

* Convert to string representation

169

*/

170

toString(): string;

171

172

/**

173

* Create empty step map

174

*/

175

static empty: StepMap;

176

177

/**

178

* Create offset step map

179

*/

180

static offset(n: number): StepMap;

181

}

182

```

183

184

### Built-in Step Types

185

186

Concrete step implementations for common operations.

187

188

```typescript { .api }

189

/**

190

* Step that replaces content between two positions

191

*/

192

class ReplaceStep extends Step {

193

/**

194

* Create a replace step

195

*/

196

constructor(from: number, to: number, slice: Slice, structure?: boolean);

197

198

/**

199

* From position

200

*/

201

from: number;

202

203

/**

204

* To position

205

*/

206

to: number;

207

208

/**

209

* Content to insert

210

*/

211

slice: Slice;

212

}

213

214

/**

215

* Step that replaces content around a position

216

*/

217

class ReplaceAroundStep extends Step {

218

/**

219

* Create a replace around step

220

*/

221

constructor(from: number, to: number, gapFrom: number, gapTo: number, slice: Slice, insert: number, structure?: boolean);

222

}

223

224

/**

225

* Step that adds or removes a mark

226

*/

227

class AddMarkStep extends Step {

228

/**

229

* Create an add mark step

230

*/

231

constructor(from: number, to: number, mark: Mark);

232

}

233

234

/**

235

* Step that removes a mark

236

*/

237

class RemoveMarkStep extends Step {

238

/**

239

* Create a remove mark step

240

*/

241

constructor(from: number, to: number, mark: Mark);

242

}

243

244

/**

245

* Step that adds node marks

246

*/

247

class AddNodeMarkStep extends Step {

248

/**

249

* Create an add node mark step

250

*/

251

constructor(pos: number, mark: Mark);

252

}

253

254

/**

255

* Step that removes node marks

256

*/

257

class RemoveNodeMarkStep extends Step {

258

/**

259

* Create a remove node mark step

260

*/

261

constructor(pos: number, mark: Mark);

262

}

263

```

264

265

### Transform Helper Functions

266

267

Utility functions for common transformation operations.

268

269

```typescript { .api }

270

/**

271

* Replace content between positions

272

*/

273

function replaceStep(doc: Node, from: number, to?: number, slice?: Slice): Step | null;

274

275

/**

276

* Lift content out of its parent

277

*/

278

function liftTarget(range: NodeRange): number | null;

279

280

/**

281

* Find wrapping for content

282

*/

283

function findWrapping(range: NodeRange, nodeType: NodeType, attrs?: Attrs, innerRange?: NodeRange): Transform | null;

284

285

/**

286

* Check if content can be split

287

*/

288

function canSplit(doc: Node, pos: number, depth?: number, typesAfter?: NodeType[]): boolean;

289

290

/**

291

* Split content at position

292

*/

293

function split(tr: Transform, pos: number, depth?: number, typesAfter?: NodeType[]): Transform;

294

295

/**

296

* Check if content can be joined

297

*/

298

function canJoin(doc: Node, pos: number): boolean;

299

300

/**

301

* Join content at position

302

*/

303

function joinPoint(doc: Node, pos: number, dir?: number): number | null;

304

305

/**

306

* Insert content at position

307

*/

308

function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null;

309

310

/**

311

* Drop point for content

312

*/

313

function dropPoint(doc: Node, pos: number, slice: Slice): number | null;

314

```

315

316

**Usage Examples:**

317

318

```typescript

319

import {

320

Transform,

321

Step,

322

ReplaceStep,

323

AddMarkStep,

324

RemoveMarkStep,

325

Mapping,

326

StepMap,

327

replaceStep,

328

canSplit,

329

canJoin

330

} from "@tiptap/pm/transform";

331

332

// Basic document transformation

333

function performTransformation(doc: Node, schema: Schema): Node {

334

const tr = new Transform(doc);

335

336

// Insert text at position 10

337

const insertStep = new ReplaceStep(

338

10, 10,

339

new Slice(Fragment.from(schema.text("Hello, ")), 0, 0)

340

);

341

tr.step(insertStep);

342

343

// Add bold mark to text from position 10-16

344

const boldMark = schema.marks.strong.create();

345

const markStep = new AddMarkStep(10, 16, boldMark);

346

tr.step(markStep);

347

348

// Replace content at position 20-25

349

const replaceSlice = new Slice(

350

Fragment.from(schema.text("World!")),

351

0, 0

352

);

353

const replaceStep = new ReplaceStep(20, 25, replaceSlice);

354

tr.step(replaceStep);

355

356

return tr.doc;

357

}

358

359

// Position mapping through transformations

360

function trackPositionThroughChanges(

361

doc: Node,

362

originalPos: number,

363

steps: Step[]

364

): number {

365

const mapping = new Mapping();

366

let currentDoc = doc;

367

368

for (const step of steps) {

369

const result = step.apply(currentDoc);

370

if (result.failed) {

371

throw new Error(`Step failed: ${result.failed}`);

372

}

373

374

currentDoc = result.doc;

375

mapping.appendMap(step.getMap());

376

}

377

378

return mapping.map(originalPos);

379

}

380

381

// Complex transformation with validation

382

class DocumentEditor {

383

private doc: Node;

384

private schema: Schema;

385

386

constructor(doc: Node, schema: Schema) {

387

this.doc = doc;

388

this.schema = schema;

389

}

390

391

insertText(pos: number, text: string): boolean {

392

const tr = new Transform(this.doc);

393

const slice = new Slice(

394

Fragment.from(this.schema.text(text)),

395

0, 0

396

);

397

398

const step = new ReplaceStep(pos, pos, slice);

399

const result = tr.maybeStep(step);

400

401

if (result.failed) {

402

console.error("Insert failed:", result.failed);

403

return false;

404

}

405

406

this.doc = tr.doc;

407

return true;

408

}

409

410

deleteRange(from: number, to: number): boolean {

411

const tr = new Transform(this.doc);

412

const step = new ReplaceStep(from, to, Slice.empty);

413

const result = tr.maybeStep(step);

414

415

if (result.failed) {

416

console.error("Delete failed:", result.failed);

417

return false;

418

}

419

420

this.doc = tr.doc;

421

return true;

422

}

423

424

toggleMark(from: number, to: number, markType: MarkType, attrs?: Attrs): boolean {

425

const tr = new Transform(this.doc);

426

const mark = markType.create(attrs);

427

428

// Check if mark exists in range

429

const hasMark = this.doc.rangeHasMark(from, to, markType);

430

431

let step: Step;

432

if (hasMark) {

433

// Remove mark

434

step = new RemoveMarkStep(from, to, mark);

435

} else {

436

// Add mark

437

step = new AddMarkStep(from, to, mark);

438

}

439

440

const result = tr.maybeStep(step);

441

if (result.failed) {

442

console.error("Toggle mark failed:", result.failed);

443

return false;

444

}

445

446

this.doc = tr.doc;

447

return true;

448

}

449

450

splitBlock(pos: number): boolean {

451

if (!canSplit(this.doc, pos)) {

452

return false;

453

}

454

455

const tr = new Transform(this.doc);

456

const $pos = this.doc.resolve(pos);

457

const nodeType = $pos.parent.type;

458

459

const splitStep = new ReplaceStep(

460

pos, pos,

461

new Slice(

462

Fragment.from([

463

nodeType.createAndFill(),

464

nodeType.createAndFill()

465

]),

466

1, 1

467

)

468

);

469

470

const result = tr.maybeStep(splitStep);

471

if (result.failed) {

472

console.error("Split failed:", result.failed);

473

return false;

474

}

475

476

this.doc = tr.doc;

477

return true;

478

}

479

480

joinBlocks(pos: number): boolean {

481

if (!canJoin(this.doc, pos)) {

482

return false;

483

}

484

485

const tr = new Transform(this.doc);

486

const $pos = this.doc.resolve(pos);

487

const before = $pos.nodeBefore;

488

const after = $pos.nodeAfter;

489

490

if (!before || !after) return false;

491

492

const joinStep = new ReplaceStep(

493

pos - before.nodeSize,

494

pos + after.nodeSize,

495

new Slice(

496

Fragment.from(before.content.append(after.content)),

497

0, 0

498

)

499

);

500

501

const result = tr.maybeStep(joinStep);

502

if (result.failed) {

503

console.error("Join failed:", result.failed);

504

return false;

505

}

506

507

this.doc = tr.doc;

508

return true;

509

}

510

511

getDocument(): Node {

512

return this.doc;

513

}

514

}

515

```

516

517

## Advanced Transformation Features

518

519

### Custom Step Types

520

521

Create specialized step types for specific operations.

522

523

```typescript

524

/**

525

* Custom step for atomic table operations

526

*/

527

class TableTransformStep extends Step {

528

constructor(

529

private tablePos: number,

530

private operation: "addColumn" | "addRow" | "deleteColumn" | "deleteRow",

531

private index: number

532

) {

533

super();

534

}

535

536

apply(doc: Node): StepResult<Node> {

537

try {

538

const $pos = doc.resolve(this.tablePos);

539

const table = $pos.nodeAfter;

540

541

if (!table || table.type.name !== "table") {

542

return StepResult.fail("No table found at position");

543

}

544

545

let newTable: Node;

546

547

switch (this.operation) {

548

case "addColumn":

549

newTable = this.addColumn(table);

550

break;

551

case "addRow":

552

newTable = this.addRow(table);

553

break;

554

case "deleteColumn":

555

newTable = this.deleteColumn(table);

556

break;

557

case "deleteRow":

558

newTable = this.deleteRow(table);

559

break;

560

default:

561

return StepResult.fail("Unknown table operation");

562

}

563

564

const newDoc = doc.copy(

565

doc.content.replaceChild(

566

this.tablePos,

567

newTable

568

)

569

);

570

571

return StepResult.ok(newDoc);

572

} catch (error) {

573

return StepResult.fail(error.message);

574

}

575

}

576

577

getMap(): StepMap {

578

// Calculate position changes based on operation

579

switch (this.operation) {

580

case "addColumn":

581

case "addRow":

582

return new StepMap([

583

this.tablePos, 0, 1 // Insert at table position

584

]);

585

case "deleteColumn":

586

case "deleteRow":

587

return new StepMap([

588

this.tablePos, 1, 0 // Delete at table position

589

]);

590

default:

591

return StepMap.empty;

592

}

593

}

594

595

invert(doc: Node): Step {

596

// Return inverse operation

597

const inverseOps = {

598

"addColumn": "deleteColumn",

599

"addRow": "deleteRow",

600

"deleteColumn": "addColumn",

601

"deleteRow": "addRow"

602

};

603

604

return new TableTransformStep(

605

this.tablePos,

606

inverseOps[this.operation] as any,

607

this.index

608

);

609

}

610

611

map(mapping: Mappable): Step | null {

612

const newPos = mapping.map(this.tablePos);

613

return new TableTransformStep(newPos, this.operation, this.index);

614

}

615

616

merge(other: Step): Step | null {

617

// Table steps don't merge with other operations

618

return null;

619

}

620

621

toJSON(): any {

622

return {

623

stepType: "tableTransform",

624

tablePos: this.tablePos,

625

operation: this.operation,

626

index: this.index

627

};

628

}

629

630

static fromJSON(schema: Schema, json: any): TableTransformStep {

631

return new TableTransformStep(

632

json.tablePos,

633

json.operation,

634

json.index

635

);

636

}

637

638

private addColumn(table: Node): Node {

639

// Implementation for adding column

640

return table; // Simplified

641

}

642

643

private addRow(table: Node): Node {

644

// Implementation for adding row

645

return table; // Simplified

646

}

647

648

private deleteColumn(table: Node): Node {

649

// Implementation for deleting column

650

return table; // Simplified

651

}

652

653

private deleteRow(table: Node): Node {

654

// Implementation for deleting row

655

return table; // Simplified

656

}

657

}

658

```

659

660

### Operational Transform

661

662

Handle conflicting simultaneous transformations.

663

664

```typescript

665

class OperationalTransform {

666

/**

667

* Transform two concurrent operations for conflict resolution

668

*/

669

static transform(

670

stepA: Step,

671

stepB: Step,

672

priority: "left" | "right" = "left"

673

): { stepA: Step | null; stepB: Step | null } {

674

// Handle different step type combinations

675

if (stepA instanceof ReplaceStep && stepB instanceof ReplaceStep) {

676

return this.transformReplaceSteps(stepA, stepB, priority);

677

}

678

679

if (stepA instanceof AddMarkStep && stepB instanceof AddMarkStep) {

680

return this.transformMarkSteps(stepA, stepB, priority);

681

}

682

683

// Handle mixed types

684

if (stepA instanceof ReplaceStep && stepB instanceof AddMarkStep) {

685

return this.transformReplaceAndMark(stepA, stepB);

686

}

687

688

if (stepA instanceof AddMarkStep && stepB instanceof ReplaceStep) {

689

const result = this.transformReplaceAndMark(stepB, stepA);

690

return { stepA: result.stepB, stepB: result.stepA };

691

}

692

693

// Default: apply mapping

694

const mapA = stepA.getMap();

695

const mapB = stepB.getMap();

696

697

return {

698

stepA: stepA.map(new Mapping([mapB])),

699

stepB: stepB.map(new Mapping([mapA]))

700

};

701

}

702

703

private static transformReplaceSteps(

704

stepA: ReplaceStep,

705

stepB: ReplaceStep,

706

priority: "left" | "right"

707

): { stepA: Step | null; stepB: Step | null } {

708

const aFrom = stepA.from;

709

const aTo = stepA.to;

710

const bFrom = stepB.from;

711

const bTo = stepB.to;

712

713

// No overlap - simple mapping

714

if (aTo <= bFrom) {

715

const offset = stepA.slice.size - (aTo - aFrom);

716

return {

717

stepA: stepA,

718

stepB: new ReplaceStep(bFrom + offset, bTo + offset, stepB.slice)

719

};

720

}

721

722

if (bTo <= aFrom) {

723

const offset = stepB.slice.size - (bTo - bFrom);

724

return {

725

stepA: new ReplaceStep(aFrom + offset, aTo + offset, stepA.slice),

726

stepB: stepB

727

};

728

}

729

730

// Overlapping changes - use priority

731

if (priority === "left") {

732

return {

733

stepA: stepA,

734

stepB: null // Discard conflicting step

735

};

736

} else {

737

return {

738

stepA: null,

739

stepB: stepB

740

};

741

}

742

}

743

744

private static transformMarkSteps(

745

stepA: AddMarkStep,

746

stepB: AddMarkStep,

747

priority: "left" | "right"

748

): { stepA: Step | null; stepB: Step | null } {

749

// Mark steps can usually coexist

750

if (stepA.mark.type !== stepB.mark.type) {

751

return { stepA, stepB };

752

}

753

754

// Same mark type - check for conflicts

755

const aFrom = stepA.from;

756

const aTo = stepA.to;

757

const bFrom = stepB.from;

758

const bTo = stepB.to;

759

760

// No overlap

761

if (aTo <= bFrom || bTo <= aFrom) {

762

return { stepA, stepB };

763

}

764

765

// Overlapping same mark - merge or prioritize

766

if (stepA.mark.eq(stepB.mark)) {

767

// Same mark - create merged step

768

const mergedStep = new AddMarkStep(

769

Math.min(aFrom, bFrom),

770

Math.max(aTo, bTo),

771

stepA.mark

772

);

773

return { stepA: mergedStep, stepB: null };

774

}

775

776

// Different attributes - use priority

777

return priority === "left"

778

? { stepA, stepB: null }

779

: { stepA: null, stepB };

780

}

781

782

private static transformReplaceAndMark(

783

replaceStep: ReplaceStep,

784

markStep: AddMarkStep

785

): { stepA: Step | null; stepB: Step | null } {

786

const replaceFrom = replaceStep.from;

787

const replaceTo = replaceStep.to;

788

const markFrom = markStep.from;

789

const markTo = markStep.to;

790

791

// Mark is completely before replace

792

if (markTo <= replaceFrom) {

793

return { stepA: replaceStep, stepB: markStep };

794

}

795

796

// Mark is completely after replace

797

if (markFrom >= replaceTo) {

798

const offset = replaceStep.slice.size - (replaceTo - replaceFrom);

799

return {

800

stepA: replaceStep,

801

stepB: new AddMarkStep(

802

markFrom + offset,

803

markTo + offset,

804

markStep.mark

805

)

806

};

807

}

808

809

// Mark overlaps with replace - complex transformation needed

810

// Simplified: apply mark to replacement content if applicable

811

return { stepA: replaceStep, stepB: null };

812

}

813

}

814

```

815

816

### Transform Validation

817

818

Validate transformations before applying them.

819

820

```typescript

821

class TransformValidator {

822

static validate(transform: Transform, schema: Schema): ValidationResult {

823

const errors: string[] = [];

824

const warnings: string[] = [];

825

826

// Validate each step

827

for (let i = 0; i < transform.steps.length; i++) {

828

const step = transform.steps[i];

829

const doc = i === 0 ? transform.docs[0] : transform.docs[i];

830

831

const stepResult = this.validateStep(step, doc, schema);

832

errors.push(...stepResult.errors);

833

warnings.push(...stepResult.warnings);

834

}

835

836

// Validate final document

837

const finalValidation = this.validateDocument(transform.doc, schema);

838

errors.push(...finalValidation.errors);

839

warnings.push(...finalValidation.warnings);

840

841

return {

842

valid: errors.length === 0,

843

errors,

844

warnings

845

};

846

}

847

848

private static validateStep(step: Step, doc: Node, schema: Schema): ValidationResult {

849

const errors: string[] = [];

850

const warnings: string[] = [];

851

852

try {

853

// Test applying the step

854

const result = step.apply(doc);

855

if (result.failed) {

856

errors.push(`Step application failed: ${result.failed}`);

857

}

858

859

// Validate step-specific constraints

860

if (step instanceof ReplaceStep) {

861

const validation = this.validateReplaceStep(step, doc, schema);

862

errors.push(...validation.errors);

863

warnings.push(...validation.warnings);

864

}

865

866

if (step instanceof AddMarkStep) {

867

const validation = this.validateMarkStep(step, doc, schema);

868

errors.push(...validation.errors);

869

warnings.push(...validation.warnings);

870

}

871

872

} catch (error) {

873

errors.push(`Step validation error: ${error.message}`);

874

}

875

876

return { valid: errors.length === 0, errors, warnings };

877

}

878

879

private static validateReplaceStep(

880

step: ReplaceStep,

881

doc: Node,

882

schema: Schema

883

): ValidationResult {

884

const errors: string[] = [];

885

const warnings: string[] = [];

886

887

// Check position bounds

888

if (step.from < 0 || step.to > doc.content.size) {

889

errors.push(`Replace step positions out of bounds: ${step.from}-${step.to}`);

890

}

891

892

if (step.from > step.to) {

893

errors.push(`Invalid replace range: from(${step.from}) > to(${step.to})`);

894

}

895

896

// Check content compatibility

897

try {

898

const $from = doc.resolve(step.from);

899

const $to = doc.resolve(step.to);

900

901

if (!$from.parent.canReplace($from.index(), $to.index(), step.slice.content)) {

902

errors.push("Replacement content not allowed at position");

903

}

904

} catch (error) {

905

errors.push(`Position resolution failed: ${error.message}`);

906

}

907

908

return { valid: errors.length === 0, errors, warnings };

909

}

910

911

private static validateMarkStep(

912

step: AddMarkStep,

913

doc: Node,

914

schema: Schema

915

): ValidationResult {

916

const errors: string[] = [];

917

const warnings: string[] = [];

918

919

// Check if mark can be applied to content in range

920

try {

921

doc.nodesBetween(step.from, step.to, (node, pos) => {

922

if (node.isText && !node.type.allowsMarkType(step.mark.type)) {

923

errors.push(`Mark ${step.mark.type.name} not allowed on text at ${pos}`);

924

}

925

return true;

926

});

927

} catch (error) {

928

errors.push(`Mark validation failed: ${error.message}`);

929

}

930

931

return { valid: errors.length === 0, errors, warnings };

932

}

933

934

private static validateDocument(doc: Node, schema: Schema): ValidationResult {

935

const errors: string[] = [];

936

const warnings: string[] = [];

937

938

try {

939

// Use ProseMirror's built-in validation

940

doc.check();

941

} catch (error) {

942

errors.push(`Document structure invalid: ${error.message}`);

943

}

944

945

return { valid: errors.length === 0, errors, warnings };

946

}

947

}

948

949

interface ValidationResult {

950

valid: boolean;

951

errors: string[];

952

warnings: string[];

953

}

954

```

955

956

## Types

957

958

```typescript { .api }

959

/**

960

* Result of applying a step

961

*/

962

interface StepResult<T> {

963

/**

964

* The resulting document/transform if successful

965

*/

966

doc?: T;

967

968

/**

969

* Error message if failed

970

*/

971

failed?: string;

972

}

973

974

/**

975

* Position mapping result with additional information

976

*/

977

interface MapResult {

978

/**

979

* Mapped position

980

*/

981

pos: number;

982

983

/**

984

* Whether position was deleted

985

*/

986

deleted: boolean;

987

988

/**

989

* Recovery information

990

*/

991

recover?: number;

992

}

993

994

/**

995

* Interface for mappable objects

996

*/

997

interface Mappable {

998

/**

999

* Map a position

1000

*/

1001

map(pos: number, assoc?: number): number;

1002

1003

/**

1004

* Map with detailed result

1005

*/

1006

mapResult(pos: number, assoc?: number): MapResult;

1007

}

1008

1009

/**

1010

* Step result helper

1011

*/

1012

class StepResult<T> {

1013

static ok<T>(value: T): StepResult<T>;

1014

static fail<T>(message: string): StepResult<T>;

1015

}

1016

```