Improve docs

This commit is contained in:
Andras Schmelczer 2025-07-10 21:52:03 +01:00
parent f48685237d
commit 9c79ebc653
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
5 changed files with 87 additions and 56 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "reconcile-text"
description = "Think diff3 or git merge, but with automated conflict resolution that requires no user intervention"
description = "Intelligent 3-way text merging with automated conflict resolution"
version = "0.4.8"
rust-version = "1.85"
authors = ["Andras Schmelczer <andras@schmelczer.dev>"]

View file

@ -1,23 +1,21 @@
# Reconcile-text: conflict-free 3-way text merging
# Reconcile-text: 3-way text merging with automatic conflict resolution
> Think [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) or `git merge`, but with intelligent conflict resolution that just works.
A library for merging conflicting text edits without manual intervention. Unlike traditional 3-way merge tools that produce conflict markers, `reconcile-text` automatically resolves conflicts by applying both sets of changes where possible using algorithms inspired by Operational Transformation.
Reconcile is a Rust and JavaScript (via WebAssembly) library that merges conflicting text edits without requiring manual intervention. Where traditional 3-way merge tools would leave you with conflict markers to resolve by hand, Reconcile automatically weaves changes together using sophisticated algorithms inspired by Operational Transformation.
**[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action!
**[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action.
Find it on:
- [reconcile-text on crates.io](https://crates.io/crates/reconcile-text)
- [reconcile-text on NPM](https://www.npmjs.com/package/reconcile-text)
## What makes Reconcile special?
## Key features
- **🚫 No conflict markers** — Clean, merged output without Git's `<<<<<<<` noise
- **📍 Cursor tracking** — Automatically repositions cursors and selections during merging
- **🔧 Flexible tokenisation** — Word-level (default), character-level, or custom strategies
- **🌍 Unicode-first** — Full UTF-8 support
- **🕸️ Cross-platform** — Native Rust performance with WebAssembly for JavaScript
- **No conflict markers** — Clean, merged output without Git's `<<<<<<<` markers
- **Cursor tracking** — Automatically repositions cursors and selections during merging
- **Flexible tokenisation** — Word-level (default), character-level, or custom strategies
- **Unicode support** — Full UTF-8 support with proper handling of complex scripts
- **Cross-platform** — Native Rust performance with WebAssembly for JavaScript
## Quick start
@ -57,7 +55,7 @@ npm install reconcile-text
Then use in your application:
```javascript
import { init, reconcile } from 'reconcile-text';
import { reconcile } from 'reconcile-text';
// Same example as above
const parent = 'Hello world';
@ -90,7 +88,7 @@ Reconcile offers different ways to split text for merging:
### Cursor tracking
Ideal for collaborative editors — Reconcile tracks cursor positions through merges:
Ideal for collaborative editors — Reconcile automatically tracks cursor positions through merges:
```javascript
const result = reconcile(
@ -105,7 +103,8 @@ const result = reconcile(
}
);
// Cursors are automatically repositioned in the merged text
// Result: "Hi beautiful world" with repositioned cursors
console.log(result.text); // "Hi beautiful world"
console.log(result.cursors); // [{ id: 1, position: 3 }, { id: 2, position: 0 }]
```
@ -120,17 +119,17 @@ Reconcile builds upon the foundation of `diff3` but adds intelligent conflict re
Whilst Reconcile's primary goal isn't implementing Operational Transformation, OT provides an elegant way to merge Myers' diff output. The same could be achieved with CRDTs, though the quality depends entirely on the underlying 2-way diffs. Note that `move` operations aren't supported, as Myers' algorithm decomposes them into separate `insert` and `delete` operations.
## Why Reconcile exists
## Background
Collaborative editing is everywhere — multiple users editing documents simultaneously, or the same person working across devices. This creates the inevitable challenge of conflicting changes.
Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously, or when synchronising edits across devices.
Traditional solutions like CRDTs or Operational Transformation work brilliantly when you control the entire editing environment. They capture every keystroke, cursor movement, and operation. But real-world workflows are messier: users love tools that don't lock them in. Take Obsidian's approach with plain Markdown files — users can edit with any tool they fancy, from Vim to Word.
Traditional solutions like CRDTs or Operational Transformation work well when you control the entire editing environment and can capture every operation. However, many workflows involve users editing with different tools — for example, Obsidian users editing Markdown files with various editors from Vim to Word.
This creates what's known as **Differential Synchronisation** [¹]: you only know the final state of each document, not how it got there. It's the same challenge Git tackles, but Git expects humans to resolve conflicts manually.
This creates **Differential Synchronisation** scenarios [¹]: you only know the final state of each document, not the sequence of operations that produced it. This is the same challenge Git addresses, but Git requires manual conflict resolution.
Here's the key insight: whilst incorrect merges in source code can introduce devastating bugs, human text is more forgiving. People excel at extracting meaning from imperfect text — a slightly clumsy sentence is preferable to conflict markers interrupting the flow.
The key insight is that whilst incorrect merges in source code can introduce bugs, human text is more forgiving. A slightly imperfect sentence is often preferable to conflict markers interrupting the flow.
> **Caveat**: Some text domains are less tolerant of imperfect merges. Legal contracts, for instance, could have unintended meaning changes from double-negations created by conflicting edits.
> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double-negations.
## Development

View file

@ -1,7 +1,7 @@
{
"name": "reconcile-text",
"version": "0.4.8",
"description": "Think diff3 or git merge, but with automated conflict resolution that requires no user intervention",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
"keywords": [
@ -48,4 +48,4 @@
"webpack-cli": "^6.0.1",
"webpack-merge": "^6.0.1"
}
}
}

View file

@ -1,15 +1,16 @@
//! # Reconcile: conflict-free 3-way text merging
//! # Reconcile: 3-way text merging with automatic conflict resolution
//!
//! Think [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) or `git merge`,
//! but with intelligent conflict resolution.
//! A library for merging conflicting text edits without manual intervention.
//! Unlike traditional 3-way merge tools that produce conflict markers, this library
//! automatically resolves conflicts by applying both sets of changes where possible.
//!
//! Reconcile is a Rust and JavaScript (via WebAssembly) library that merges
//! conflicting text edits without requiring manual intervention. Where
//! traditional 3-way merge tools would leave you with conflict markers to
//! resolve by hand, Reconcile automatically weaves changes together using
//! sophisticated algorithms inspired by Operational Transformation.
//! Based on a combination of Myers' diff algorithm and Operational Transformation
//! principles, it's designed for scenarios where you have a common parent text
//! and two modified versions that need to be intelligently combined.
//!
//! ✨ **[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action!
//! **[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action.
//!
//! ## Basic usage
//!
//! ```
//! use reconcile_text::{reconcile, BuiltinTokenizer};
@ -27,40 +28,46 @@
//!
//! ## Tokenisation strategies
//!
//! Merging happens at the token level, where you control the granularity.
//! By default, words serve as the atomic units for merging, ensuring words
//! remain intact during the reconciliation process.
//! Merging operates at the token level, where you control the granularity.
//! The choice of tokeniser significantly affects merge quality and behaviour.
//!
//! ### Built-in tokenisers
//!
//! - **`BuiltinTokenizer::Word`** (recommended): Splits on word boundaries, preserving word integrity
//! - **`BuiltinTokenizer::Character`**: Character-level merging for fine-grained control
//! - **`BuiltinTokenizer::Line`**: Line-based merging, similar to traditional diff tools
//!
//! ```
//! use reconcile_text::{reconcile, BuiltinTokenizer};
//!
//! let parent = "The quick brown fox\n";
//! let left = "The very quick brown fox\n"; // Added "very"
//! let right = "The quick red fox\n"; // Changed "brown" to "red"
//! let parent = "The quick brown fox\njumps over the lazy dog";
//! let left = "The very quick brown fox\njumps over the lazy dog"; // Added "very"
//! let right = "The quick red fox\njumps over the lazy dog"; // Changed "brown" to "red"
//!
//! // Using line-based tokenisation
//! // Word-level tokenisation (recommended for most text)
//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
//! assert_eq!(result.apply().text(), "The very quick red fox\njumps over the lazy dog");
//!
//! // Line-level tokenisation (similar to git merge)
//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Line);
//! assert_eq!(result.apply().text(), "The quick red foxThe very quick brown fox\n");
//! // Line-level produces different results as it treats each line as atomic
//! ```
//!
//! ### Custom tokenisation
//!
//! For specialised use cases—such as structured text like Markdown or HTML—
//! you can implement custom tokenisation logic:
//! For specialised use cases, implement custom tokenisation logic:
//!
//! ```
//! use reconcile_text::{reconcile, Token, BuiltinTokenizer};
//!
//! // Example: custom sentence-based tokeniser
//! // Example: sentence-based tokeniser function
//! let sentence_tokeniser = |text: &str| {
//! text.split(". ")
//! .map(|sentence| Token::new(
//! sentence.to_string(),
//! sentence.to_string(),
//! false, // don't allow joining with the preceding token
//! false, // don't allow joining with the following token
//! false, // don't allow joining with preceding token
//! false, // don't allow joining with following token
//! ))
//! .collect::<Vec<_>>()
//! };
@ -69,18 +76,19 @@
//! let left = "Hello beautiful world. This is a test."; // Added "beautiful"
//! let right = "Hello world. This is a great test."; // Changed "a" to "great"
//!
//! // For most cases, the built-in word tokeniser works perfectly
//! // For most cases, the built-in word tokeniser works well
//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
//! assert_eq!(result.apply().text(), "Hello beautiful world. This is a great test.");
//! ```
//! > **Tip**: Setting joinability to `false` causes longer runs of insertions
//! > to interleave (LRLRLR) rather than group together (LLLRRR), which can
//! > produce more natural-looking merged text.
//!
//! > **Note**: Setting token joinability to `false` causes insertions to interleave
//! > (LRLRLR) rather than group together (LLLRRR), which often produces more
//! > natural-looking merged text.
//!
//! ## Cursor tracking
//!
//! Perfect for collaborative editors—the library automatically repositions
//! cursors and selection ranges during merging:
//! Automatically repositions cursors and selection ranges during merging,
//! essential for collaborative editors:
//!
//! ```
//! use reconcile_text::{reconcile, BuiltinTokenizer, TextWithCursors, CursorPosition};
@ -101,12 +109,27 @@
//! assert_eq!(merged.text(), "Hi beautiful world");
//! // Cursors are automatically repositioned in the merged text
//! assert_eq!(merged.cursors().len(), 2);
//! // Cursor 1 moves from position 6 to position 3 (after "Hi ")
//! // Cursor 2 stays at position 0 (beginning)
//! ```
//!
//! ## How it works
//! ## Error handling
//!
//! For a detailed explanation of the algorithm and architecture, see the
//! [README](README.md#how-it-works).
//! The library is designed to be robust and will always produce a result, even
//! in edge cases. However, be aware that:
//!
//! - Binary data is detected and handled gracefully
//! - Unicode text is fully supported
//! - Extremely large diffs may have performance implications
//!
//! ## Algorithm overview
//!
//! 1. **Diff computation**: Myers' algorithm calculates differences between parent↔left and parent↔right
//! 2. **Tokenisation**: Text is split into meaningful units (words, characters, etc.)
//! 3. **Diff optimisation**: Operations are reordered and consolidated for coherent changes
//! 4. **Operational Transformation**: Edits are combined using OT principles
//!
//! For detailed algorithm explanation, see the [README](README.md#how-it-works).
mod operation_transformation;
mod raw_operation;

View file

@ -1,7 +1,16 @@
# Test Examples
This directory contains YAML test cases that demonstrate various reconcile scenarios.
This directory contains comprehensive YAML test cases that demonstrate various text reconciliation scenarios and edge cases. These examples serve both as regression tests and as documentation of the library's behaviour in different situations.
## Test Structure
Each YAML file contains test cases with the following structure:
- `parent`: The original text that both sides diverged from
- `left`: One version of the edited text
- `right`: Another version of the edited text
- `expected`: The expected merged result
- `description`: Human-readable explanation of what the test demonstrates
## Cursor Position Notation
In some test cases, the `|` character is used to denote cursor positions within the text. These characters are stripped before the actual reconcile logic is run, making it easier to visualize where cursors should be positioned.
In some test cases, the `|` character is used to denote cursor positions within the text. These characters are stripped before the actual reconcile logic is run, making it easier to visualise where cursors should be positioned in the test inputs and expected outputs.