From 870f20513011a2c2614e7cf01bca02861103ca16 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 8 Jul 2025 22:06:28 +0000
Subject: [PATCH 001/107] Bump actions/setup-node from 4.2.0 to 4.4.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.2.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.2.0...v4.4.0)
---
updated-dependencies:
- dependency-name: actions/setup-node
dependency-version: 4.4.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
.github/workflows/check.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 7108333..dbe0d51 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js environment
- uses: actions/setup-node@v4.2.0
+ uses: actions/setup-node@v4.4.0
with:
node-version: '22.x'
check-latest: true
@@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js environment
- uses: actions/setup-node@v4.2.0
+ uses: actions/setup-node@v4.4.0
with:
node-version: '22.x'
check-latest: true
From 6342e64df779981d259bce6dcbe9290b56e1d4cf Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 11:27:28 +0100
Subject: [PATCH 002/107] Fix website
---
examples/website/src/index.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/examples/website/src/index.ts b/examples/website/src/index.ts
index 0bc55a8..739d5af 100644
--- a/examples/website/src/index.ts
+++ b/examples/website/src/index.ts
@@ -1,5 +1,5 @@
import { reconcileWithHistory } from 'reconcile-text';
-import type { Tokenizer } from 'reconcile-text';
+import type { BuiltinTokenizer } from 'reconcile-text';
import './style.scss';
const originalTextArea = document.getElementById('original') as HTMLTextAreaElement;
@@ -149,9 +149,9 @@ function createCaret(isLeft: boolean): HTMLSpanElement {
return caretSpan;
}
-function getSelectedTokenizer(): Tokenizer {
+function getSelectedTokenizer(): BuiltinTokenizer {
const selectedRadio = Array.from(tokenizerRadios).find((radio) => radio.checked);
- return selectedRadio?.value as Tokenizer;
+ return selectedRadio?.value as BuiltinTokenizer;
}
function resizeTextAreas(): void {
From 65acd9076cc4beddef7549f4b2eb62e3ee9bb1d2 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 11:29:19 +0100
Subject: [PATCH 003/107] Run publish on all tags
---
.github/workflows/check.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index f4ffdf6..d5849c2 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -3,7 +3,7 @@ name: Check
on:
push:
branches: ['main']
- tags: ['v*']
+ tags: ['*']
pull_request:
branches: ['main']
From 6e274350926c8cd6ba11f7370ded546109cf5557 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 11:35:06 +0100
Subject: [PATCH 004/107] Fix tests
---
.github/workflows/check.yml | 6 +-----
reconcile-js/__mocks__/wasm.js | 2 +-
reconcile-js/jest.config.js | 2 +-
reconcile-js/package-lock.json | 2 +-
4 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index d5849c2..a786129 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -53,11 +53,7 @@ jobs:
cargo machete
- name: Test
- run: |
- cargo test --verbose
- cargo test --features serde
- cargo test --features wasm
- wasm-pack test --node --features wasm
+ run: scripts/test.sh
publish-crate:
needs: build
diff --git a/reconcile-js/__mocks__/wasm.js b/reconcile-js/__mocks__/wasm.js
index e67f920..1724448 100644
--- a/reconcile-js/__mocks__/wasm.js
+++ b/reconcile-js/__mocks__/wasm.js
@@ -2,7 +2,7 @@ const fs = require('fs');
const path = require('path');
// Read the actual WASM file and convert to base64 for testing
-const wasmPath = path.join(__dirname, '../../pkg/reconcile_bg.wasm');
+const wasmPath = path.join(__dirname, '../../pkg/reconcile_text_bg.wasm');
const wasmBuffer = fs.readFileSync(wasmPath);
const wasmBase64 = wasmBuffer.toString('base64');
diff --git a/reconcile-js/jest.config.js b/reconcile-js/jest.config.js
index d411a6e..358968a 100644
--- a/reconcile-js/jest.config.js
+++ b/reconcile-js/jest.config.js
@@ -1,6 +1,6 @@
module.exports = {
preset: 'ts-jest/presets/js-with-babel-esm',
moduleNameMapper: {
- '^reconcile-text/reconcile_bg\\.wasm$': `/__mocks__/wasm.js`,
+ '^reconcile-text/reconcile_text_bg\\.wasm$': `/__mocks__/wasm.js`,
},
};
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index a5a1797..0468a34 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.4.3",
+ "version": "0.4.9",
"dev": true,
"license": "MIT"
},
From edac74e1a7c979ef1936fdf590a21a44383c6007 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 11:35:16 +0100
Subject: [PATCH 005/107] Bump versions to 0.4.10
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
reconcile-js/package-lock.json | 4 ++--
reconcile-js/package.json | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 914c0c6..e417f3c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -176,7 +176,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.4.9"
+version = "0.4.10"
dependencies = [
"cfg-if 1.0.1",
"console_error_panic_hook",
diff --git a/Cargo.toml b/Cargo.toml
index d3b05ec..8e3bd6c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.4.9"
+version = "0.4.10"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 0468a34..ae081ce 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.4.9",
+ "version": "0.4.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.4.9",
+ "version": "0.4.10",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index ce28979..65d6fa0 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.4.9",
+ "version": "0.4.10",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From eadc58eeaa92b8e5699afa1fe921d6068fc161a4 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 12:10:52 +0100
Subject: [PATCH 006/107] Re-declare public API's enums
---
reconcile-js/package-lock.json | 2 +-
reconcile-js/src/index.ts | 42 +++++++++++++++++++++-------------
2 files changed, 27 insertions(+), 17 deletions(-)
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index ae081ce..f02abcf 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.4.9",
+ "version": "0.4.10",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index 1fcbb7b..f86aeac 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -3,17 +3,36 @@ import {
reconcile as wasmReconcile,
TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory,
- BuiltinTokenizer,
reconcileWithHistory as wasmReconcileWithHistory,
isBinary as wasmIsBinary,
- History,
initSync,
} from 'reconcile-text';
import wasmBytes from 'reconcile-text/reconcile_text_bg.wasm';
-// Re-export types from the WASM module as these are part of our API
-export { History, BuiltinTokenizer };
+// Define the enum values as const arrays to avoid duplication
+const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Word'] as const;
+const HISTORY_VALUES = [
+ 'Unchanged',
+ 'AddedFromLeft',
+ 'AddedFromRight',
+ 'RemovedFromLeft',
+ 'RemovedFromRight',
+] as const;
+
+/**
+ * Tokenisation strategies for text merging.
+ *
+ * These correspond to the built-in tokenizers available in the underlying WASM module.
+ */
+export type BuiltinTokenizer = (typeof BUILTIN_TOKENIZERS)[number];
+
+/**
+ * History classification for text spans in merge results.
+ *
+ * Indicates the origin of each text span in the merged document.
+ */
+export type History = (typeof HISTORY_VALUES)[number];
/**
* Represents a text document with associated cursor positions.
@@ -84,16 +103,7 @@ export interface SpanWithHistory {
history: History;
}
-/**
- * Tokenisation strategies for text merging.
- *
- * - "Word": Splits text on word boundaries (recommended for prose and most text)
- * - "Character": Splits text into individual characters (fine-grained control)
- * - "Line": Splits text into lines (similar to git merge or diff3)
- */
-const TOKENIZERS = ['Line', 'Word', 'Character'];
-
-const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${TOKENIZERS.join(
+const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${BUILTIN_TOKENIZERS.join(
', '
)} are supported.`;
@@ -131,7 +141,7 @@ export function reconcile(
): TextWithCursors {
init();
- if (!TOKENIZERS.includes(tokenizer)) {
+ if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
@@ -185,7 +195,7 @@ export function reconcileWithHistory(
): TextWithCursorsAndHistory {
init();
- if (!TOKENIZERS.includes(tokenizer)) {
+ if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
From 7d160cd9fdd551f1f054160bd68a76dcf403b5bd Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 12:14:48 +0100
Subject: [PATCH 007/107] Improve types by removing nullability from return
value
---
reconcile-js/src/index.ts | 46 +++++++++++++++++++++++++++++----------
1 file changed, 34 insertions(+), 12 deletions(-)
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index f86aeac..efc18ae 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -44,6 +44,25 @@ export type History = (typeof HISTORY_VALUES)[number];
export interface TextWithCursors {
/** The document's entire content as a string */
text: string;
+
+ /**
+ * Array of cursor positions within the text. Can be empty if there are no cursors to track.
+ * Each cursor has a unique ID and position.
+ */
+ cursors: CursorPosition[];
+}
+
+/**
+ * Represents a text document with associated cursor positions.
+ *
+ * This interface is used both as input to reconcile functions (to specify where
+ * cursors are positioned in the original documents) and as output (with cursors
+ * automatically repositioned after merging).
+ */
+export interface TextWithOptionalCursors {
+ /** The document's entire content as a string */
+ text: string;
+
/**
* Array of cursor positions within the text. Can be null, undefined, or empty
* if there are no cursors to track. Each cursor has a unique ID and position.
@@ -60,6 +79,7 @@ export interface TextWithCursors {
export interface CursorPosition {
/** Unique identifier for the cursor (can be any number, must be unique within the document) */
id: number;
+
/** Character position in the text, 0-based index from the beginning of the document */
position: number;
}
@@ -74,11 +94,13 @@ export interface CursorPosition {
export interface TextWithCursorsAndHistory {
/** The merged document's entire content */
text: string;
+
/**
- * Array of cursor positions within the merged text. Can be null, undefined, or empty
- * if there are no cursors to track. All cursors are automatically repositioned.
+ * Array of cursor positions within the merged text. Can empty if there are no cursors to track.
+ * All cursors are automatically repositioned from the left and right documents.
*/
- cursors: null | undefined | CursorPosition[];
+ cursors: CursorPosition[];
+
/**
* Detailed provenance information showing the origin of each text span in the result.
* Each span indicates whether it was unchanged, added from left, added from right, etc.
@@ -96,10 +118,8 @@ export interface TextWithCursorsAndHistory {
export interface SpanWithHistory {
/** The text content of this span */
text: string;
- /**
- * The origin of this text span: "Unchanged" (from original), "AddedFromLeft",
- * "AddedFromRight", "RemovedFromLeft", or "RemovedFromRight"
- */
+
+ /** The origin of this text span in the merge result */
history: History;
}
@@ -135,8 +155,8 @@ let isInitialised = false;
*/
export function reconcile(
original: string,
- left: string | TextWithCursors,
- right: string | TextWithCursors,
+ left: string | TextWithOptionalCursors,
+ right: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
): TextWithCursors {
init();
@@ -189,8 +209,8 @@ export function reconcile(
*/
export function reconcileWithHistory(
original: string,
- left: string | TextWithCursors,
- right: string | TextWithCursors,
+ left: string | TextWithOptionalCursors,
+ right: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
): TextWithCursorsAndHistory {
init();
@@ -243,7 +263,9 @@ function init() {
isInitialised = true;
}
-function toWasmTextWithCursors(text: string | TextWithCursors): wasmTextWithCursors {
+function toWasmTextWithCursors(
+ text: string | TextWithOptionalCursors
+): wasmTextWithCursors {
const isInputString = typeof text === 'string';
const leftText = isInputString ? text : text.text;
const leftCursors = isInputString ? [] : (text.cursors ?? []);
From cf21219424a069f4d553e3ed2c799228111e13f2 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 15:25:12 +0100
Subject: [PATCH 008/107] Add favicon
---
.github/workflows/check.yml | 2 +-
examples/website/package-lock.json | 85 ++++++++++++++++++++++++++++-
examples/website/package.json | 5 +-
examples/website/src/favicon.ico | Bin 0 -> 4286 bytes
examples/website/src/index.html | 39 ++++++-------
examples/website/src/og-image.png | Bin 0 -> 48041 bytes
examples/website/webpack.config.js | 13 +++++
7 files changed, 121 insertions(+), 23 deletions(-)
create mode 100644 examples/website/src/favicon.ico
create mode 100644 examples/website/src/og-image.png
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index a786129..7da7139 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -1,4 +1,4 @@
-name: Check
+name: Check & publish
on:
push:
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index 4c0716e..ca35520 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -7,6 +7,7 @@
"name": "reconcile-example-website",
"license": "GPL-3.0-or-later",
"devDependencies": {
+ "copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.3",
"inline-source-webpack-plugin": "^3.0.1",
@@ -27,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.4.3",
+ "version": "0.4.10",
"dev": true,
"license": "MIT",
"devDependencies": {
@@ -1536,6 +1537,43 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/copy-webpack-plugin": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz",
+ "integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-parent": "^6.0.1",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^6.0.2",
+ "tinyglobby": "^0.2.12"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/copy-webpack-plugin/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -4987,6 +5025,51 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
diff --git a/examples/website/package.json b/examples/website/package.json
index ea75200..23588ff 100644
--- a/examples/website/package.json
+++ b/examples/website/package.json
@@ -22,12 +22,13 @@
],
"homepage": "https://github.com/schmelczer/reconcile#readme",
"devDependencies": {
- "reconcile-text": "file:../../reconcile-js",
+ "copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.3",
- "mini-css-extract-plugin": "^2.9.2",
"inline-source-webpack-plugin": "^3.0.1",
+ "mini-css-extract-plugin": "^2.9.2",
"prettier": "^3.6.2",
+ "reconcile-text": "file:../../reconcile-js",
"resolve-url-loader": "^5.0.0",
"sass": "^1.89.2",
"sass-loader": "^16.0.5",
diff --git a/examples/website/src/favicon.ico b/examples/website/src/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..b345eb5fb023954ef7de48ed1a01c4ffb953cfd1
GIT binary patch
literal 4286
zcmeHLOHWf#5WfBcSMH1jL{TEa=tkngjc$xQ6$NDj?!*l$F2qOTPE8~}qCiZ1(9#w|
zi=b4|2-Ezv$q}3cbZ-*w$OW_xM7;pJDvHynRCxMoja`{tYa^g
zO2E6IT`ve51VM;ngPDYVY{1Ti`C<}$!)Fi#T9cqF;Bn89#Uz`LJ=13;kUAxbI?Hd!=7R$+
zVmPhx_uu6lrJ7Uwsr=9;dR>%7v##;5=KyT|g)Gjk%|Adc=eMxAexO$rspVE7sgt8f
zcWejW55I4zEA|c?a9eMmC$C3biW1as|7fPl6FYev)46n-unz{s1sW&pHUbv8!TsK8
z4s^@X#km=3yH`YNb_Q_{-(Jn5@zF0KW55Rtu;8@azewWTY)E`K4%d$nw*T4uEU8nY
zsqRcN**luZ>-9)<==P3A?gIwr4jh+bM8Ygb85`=OHZo32|}OaQ$pjleMaZ*E-ZYtD%J1H1nA3L`6MUsQIFo?R);%7e84=PjhyW>1-
-
+
+
+
- Reconcile: conflict-free text merging
+ reconcile-text: conflict-free 3-way text merging
@@ -37,13 +39,13 @@
rel="noopener noreferrer"
>diff3
- or git merge, but with intelligent conflict resolution that
- requires no user intervention. The
+ (or more specifically, git merge), but with intelligent conflict
+ resolution that requires no user intervention. The
Reconcilereconcile-text
library tackles a fundamental challenge in collaborative editing: what happens
when multiple users edit the same text simultaneously, but the conflict
@@ -52,18 +54,20 @@
Where traditional merge tools leave you with conflict markers to resolve
- manually, Reconcile automatically weaves changes together. The
- reconcile(parent, left, right) function takes conflicting edits
- and produces clean, unified results using an algorithm inspired by Operational
- Transformation. No more <<<<<<< markers
- cluttering your text.
+ manually, reconcile-text automatically weaves changes together.
+ The reconcile(parent, left, right) function takes conflicting
+ edits and produces clean, unified results using an algorithm inspired by
+ Operational Transformation. No more
+ <<<<<<< markers jumbling your text.
The process starts with your chosen tokenisation strategy, then applies Myers'
- diff algorithm to compare the original with both modified versions. These
- diffs are optimised and transformed to preserve meaningful changes, before a
- final merge strategy combines all modifications without losing any edits.
+ 2-way diff algorithm to compare the original with both modified versions.
+ These diffs are optimised and transformed to preserve the longest meaningful
+ changes, before a final merge strategy combines all inserts and deletes
+ without losing any edits. Cursor positions may be tracked and updated during
+ merging too.
@@ -74,12 +78,9 @@
rel="noopener noreferrer"
>documentation
- or try editing the text boxes below to see Reconcile in action.
-
-
-
- Use the tokenisation options below to experiment with different approaches —
- the library also supports custom tokenisers.
+ or try editing the text boxes below to see reconcile-text in
+ action. Use the tokenisation options below to experiment with different
+ approaches — the Rust library also supports providing a custom tokeniser.
diff --git a/examples/website/src/index.ts b/examples/website/src/index.ts
index 9ed8470..c9fcb46 100644
--- a/examples/website/src/index.ts
+++ b/examples/website/src/index.ts
@@ -10,7 +10,7 @@ const tokenizerRadios = document.querySelectorAll(
'input[name="tokenizer"]'
) as NodeListOf;
-const sampleText = `The \`reconcile\` Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to see competing changes get merged in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`;
+const sampleText = `The "reconcile-text" Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First user's edit", and "Second user's edit" text boxes to see competing changes get merged in real-time within the "Merged result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`;
async function main(): Promise {
originalTextArea.addEventListener('input', updateMergedText);
From 55b37039ef4ce0776522358cb2cc7ba54919307d Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 21:58:05 +0100
Subject: [PATCH 012/107] Improve docs
---
README.md | 127 ++++++++------------
docs/advanced-ts.md | 70 +++++++++++
examples/website/src/index.ts | 16 ++-
src/lib.rs | 83 ++++++++-----
src/operation_transformation/edited_text.rs | 34 ++++++
5 files changed, 225 insertions(+), 105 deletions(-)
create mode 100644 docs/advanced-ts.md
diff --git a/README.md b/README.md
index 45481c9..a659488 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,20 @@
-# Reconcile-text: 3-way text merging with automatic conflict resolution
+# `reconcile-text`: conflict-free 3-way text merging
-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.
+A Rust and TypeScript 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 (while updating cursor positions) using an algorithm inspired by Operational Transformation.
-**[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action.
+## Try it
-Find it on:
+✨ **[Try the interactive demo](https://schmelczer.dev/reconcile)** to see it in action!
-- [reconcile-text on crates.io](https://crates.io/crates/reconcile-text)
-- [reconcile-text on NPM](https://www.npmjs.com/package/reconcile-text)
+### Install it in your project
+
+- `cargo add reconcile-text` ([reconcile-text on crates.io](https://crates.io/crates/reconcile-text))
+- `npm install reconcile-text` ([reconcile-text on NPM](https://www.npmjs.com/package/reconcile-text))
## Key features
- **No conflict markers** — Clean, merged output without Git's `<<<<<<<` markers
-- **Cursor tracking** — Automatically repositions cursors and selections during merging
+- **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
@@ -21,11 +23,16 @@ Find it on:
### Rust
-Run `cargo add reconcile-text` or add `reconcile-text` to your `Cargo.toml`:
+Install via crates.io:
+```sh
+cargo add reconcile-text
+```
+
+or add `reconcile-text` to your `Cargo.toml`:
```toml
[dependencies]
-reconcile-text = "0.4"
+reconcile-text = "0.5"
```
Then merge away:
@@ -35,30 +42,33 @@ use reconcile_text::{reconcile, BuiltinTokenizer};
// Start with original text
let parent = "Hello world";
-// Two people edit simultaneously
+// Two users edit simultaneously
let left = "Hello beautiful world"; // Added "beautiful"
let right = "Hi world"; // Changed "Hello" to "Hi"
-// Reconcile combines both changes intelligently
+// Reconcile combines both changes
let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
assert_eq!(result.apply().text(), "Hi beautiful world");
```
+See [merge-file](examples/merge-file.rs) for another example or the [library's documentation](https://docs.rs/reconcile-text/latest/reconcile_text).
+
### JavaScript/TypeScript
-Install via npm:
+Install via NPM:
-```bash
+```sh
npm install reconcile-text
```
-Then use in your application:
+Then use it in your application:
```javascript
import { reconcile } from 'reconcile-text';
-// Same example as above
+// Start with original text
const parent = 'Hello world';
+// Two users edit simultaneously
const left = 'Hello beautiful world';
const right = 'Hi world';
@@ -66,86 +76,51 @@ const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
```
-## Advanced usage
+See the [example website](examples/website/src/index.ts) for a more complex example or the [advanced examples document](https://github.com/schmelczer/reconcile/blob/main/docs/advanced-ts.md).
-### Edit provenance
+## Motivation
-Track which changes came from where using `reconcileWithHistory`:
+Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously (or offline). Traditional solutions like Conflict-free Replicated Data Types (CRDTs) or Operational Transformation (OT) works well when you control the entire editing environment and can capture every operation ([1]). However, many workflows involve users editing with different tools — for example, Obsidian users editing Markdown files with various editors from Vim to VS Code.
-```javascript
-const result = reconcileWithHistory(parent, left, right);
-console.log(result.history); // Detailed breakdown of each text span's origin
-```
+This creates **Differential Synchronisation** scenarios ([2], [3]): we 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. The key insight is that while 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.
-### Tokenisation strategies
+> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double-negations. At the same time, semantic conflicts can still arise when merging code, even in the absence of syntactical conflicts.
-Reconcile offers different ways to split text for merging:
-
-- **Word tokeniser** (`BuiltinTokenizer::Word`) — Splits on word boundaries (recommended for prose)
-- **Character tokeniser** (`BuiltinTokenizer::Character`) — Individual characters (fine-grained control)
-- **Line tokeniser** (`BuiltinTokenizer::Line`) — Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
-- **Custom tokeniser** — Roll your own for specialised use cases
-
-### Cursor tracking
-
-Ideal for collaborative editors — Reconcile automatically tracks cursor positions through merges:
-
-```javascript
-const result = reconcile(
- 'Hello world',
- {
- text: 'Hello beautiful world',
- cursors: [{ id: 1, position: 6 }], // After "Hello "
- },
- {
- text: 'Hi world',
- cursors: [{ id: 2, position: 0 }], // At the beginning
- }
-);
-
-// 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 }]
-```
+Differenctial sync is implemented by [universal-sync](https://github.com/invisible-college/universal-sync) and my Obsidian plugin, [vault-link](https://github.com/schmelczer/vault-link) and it requires a merging tool which creates conflict free results for the best user experience.
## How it works
-Reconcile builds upon the foundation of `diff3` but adds intelligent conflict resolution. Given a **parent** document and two modified versions (`left` and `right`), here's what happens:
+`reconcile-text` starts off similarly to `diff3` ([4], [5]) but adds automated conflict resolution. Given a **parent** document and two modified versions (`left` and `right`), the following happens:
-1. **Diff computation** — Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
-2. **Tokenisation** — Text splits into meaningful units (words, characters, etc.) for granular merging
-3. **Diff optimisation** — Operations are reordered and consolidated to maximise coherent changes
-4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications
+1. **Tokenisation** — Input texts get split into meaningful units (words, characters, etc.) for granular merging
+2. **Diff computation** — Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
+3. **Diff optimisation** — Operations are reordered and consolidated to maximise chained changes
+4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications and updating cursors
-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.
+While the primary goal of `reconcile-text` isn't to implement OT (you can check out [operational-transform-rs](https://github.com/spebern/operational-transform-rs) for a Rust implementation of it), OT provides an elegant way to merge Myers' diff outputs. The same could be achieved with CRDTs which many libraries implement well for text: see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as a few great examples.
-## Background
-
-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 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 **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.
-
-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.
-
-> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double-negations.
+However, the quality of a merge, if only the end result of concurrent changes is observable, depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations can't be supported as Myers' algorithm decomposes them into separate `insert` and `delete` operations regardless the merging algorithm.
## Development
-### Prerequisites
+Contributions are welcome!
+
+### Environment
#### Node.js setup
1. Install [nvm](https://github.com/nvm-sh/nvm):
- ```bash
+ ```sh
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
```
2. Install and use Node 22:
- ```bash
+ ```sh
nvm install 22 && nvm use 22
```
-3. Optionally set as default: `nvm alias default 22`
+3. Optionally set as default:
+ ```sh
+ nvm alias default 22
+ ```
#### Rust toolchain
@@ -158,7 +133,7 @@ The key insight is that whilst incorrect merges in source code can introduce bug
cargo install wasm-pack cargo-insta cargo-edit
```
-### Development scripts
+### Scripts
- **Run tests**: `scripts/test.sh`
- **Lint and format**: `scripts/lint.sh`
@@ -170,4 +145,8 @@ The key insight is that whilst incorrect merges in source code can introduce bug
[MIT](./LICENSE)
-[¹]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf
+[1]:https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html
+[2]: https://neil.fraser.name/writing/sync/
+[3]: https://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf
+[4]: https://blog.jcoglan.com/2017/05/08/merging-with-diff3/
+[5]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf
diff --git a/docs/advanced-ts.md b/docs/advanced-ts.md
new file mode 100644
index 0000000..b4b03d8
--- /dev/null
+++ b/docs/advanced-ts.md
@@ -0,0 +1,70 @@
+# Advanced usage (TypeScript)
+
+## Edit provenance
+
+Track which changes came from where using `reconcileWithHistory`:
+
+```javascript
+const result = reconcileWithHistory(
+ 'Hello world',
+ 'Hello beautiful world',
+ 'Hi world'
+);
+
+console.log(result.text); // "Hi beautiful world"
+console.log(result.history); /*
+[
+ {
+ "text": "Hello",
+ "history": "RemovedFromRight"
+ },
+ {
+ "text": "Hi",
+ "history": "AddedFromRight"
+ },
+ {
+ "text": " beautiful",
+ "history": "AddedFromLeft"
+ },
+ {
+ "text": " ",
+ "history": "Unchanged"
+ },
+ {
+ "text": "world",
+ "history": "Unchanged"
+ }
+]
+*/
+```
+
+## Tokenisation strategies
+
+Reconcile offers different ways to split text for merging:
+
+- **Word tokeniser** (`"Word"`) — Splits on word boundaries (recommended for prose)
+- **Character tokeniser** (`"Character"`) — Individual characters (fine-grained control)
+- **Line tokeniser** (`"Line"`) — Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
+
+## Cursor tracking
+
+Reconcile automatically tracks cursor positions through merges which is handy in a collaborative editor.
+
+```javascript
+const result = reconcile(
+ 'Hello world',
+ {
+ text: 'Hello beautiful world',
+ cursors: [{ id: 1, position: 6 }], // After "Hello "
+ },
+ {
+ text: 'Hi world',
+ cursors: [{ id: 2, position: 0 }], // At the beginning
+ }
+);
+
+// Result: "Hi beautiful world" with repositioned cursors
+console.log(result.text); // "Hi beautiful world"
+console.log(result.cursors); // [{ id: 2, position: 0 }, { id: 1, position: 3 }]
+```
+> The `cursors` list is sorted by the character position (not id-s).
diff --git a/examples/website/src/index.ts b/examples/website/src/index.ts
index c9fcb46..43f1a3b 100644
--- a/examples/website/src/index.ts
+++ b/examples/website/src/index.ts
@@ -1,4 +1,4 @@
-import { reconcileWithHistory } from 'reconcile-text';
+import { reconcile, reconcileWithHistory } from 'reconcile-text';
import type { BuiltinTokenizer } from 'reconcile-text';
import './style.scss';
@@ -22,6 +22,20 @@ async function main(): Promise {
leftTextArea.addEventListener('select', updateMergedText);
rightTextArea.addEventListener('select', updateMergedText);
+ console.info(
+ reconcile(
+ 'Hello world',
+ {
+ text: 'Hello beautiful world',
+ cursors: [{ id: 1, position: 6 }], // After "Hello "
+ },
+ {
+ text: 'Hi world',
+ cursors: [{ id: 2, position: 0 }], // At the beginning
+ }
+ )
+ );
+
window.addEventListener('resize', resizeTextAreas);
tokenizerRadios.forEach((radio) => {
diff --git a/src/lib.rs b/src/lib.rs
index 424615a..aed8ede 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,18 +1,14 @@
-//! # Reconcile: 3-way text merging with automatic conflict resolution
+//! # Reconcile: conflict-free 3-way text merging
//!
//! 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.
+//! Unlike traditional 3-way merge tools that produce conflict markers,
+//! reconcile-text automatically resolves conflicts by applying both sets of
+//! changes (while updating cursor positions) using an algorithm 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
+//! ## Simple example
//!
//! ```
//! use reconcile_text::{reconcile, BuiltinTokenizer};
@@ -30,8 +26,8 @@
//!
//! ## Tokenisation strategies
//!
-//! Merging operates at the token level, where you control the granularity.
-//! The choice of tokeniser significantly affects merge quality and behaviour.
+//! Merging happens at the token level, and the choice of tokeniser
+//! significantly affects merge quality and behaviour.
//!
//! ### Built-in tokenisers
//!
@@ -56,18 +52,21 @@
//! // Line-level tokenisation (similar to git merge)
//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Line);
//! // Line-level produces different results as it treats each line as atomic
+//! assert_eq!(result.apply().text(), "The quick red foxThe very quick brown fox\njumps over the lazy dog");
//! ```
//!
//! ### Custom tokenisation
//!
-//! For specialised use cases, implement custom tokenisation logic:
+//! For specialised use cases, such as structured languages, a custom
+//! tokenisation logic can be implemented by providing a function with the
+//! signature `Fn(&str) -> Vec>`::
//!
//! ```
//! use reconcile_text::{reconcile, Token, BuiltinTokenizer};
//!
//! // Example: sentence-based tokeniser function
//! let sentence_tokeniser = |text: &str| {
-//! text.split(". ")
+//! text.split_inclusive(". ")
//! .map(|sentence| Token::new(
//! sentence.to_string(),
//! sentence.to_string(),
@@ -82,7 +81,7 @@
//! let right = "Hello world. This is a great test."; // Changed "a" to "great"
//!
//! // For most cases, the built-in word tokeniser works well
-//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word);
+//! let result = reconcile(parent, &left.into(), &right.into(), &sentence_tokeniser);
//! assert_eq!(result.apply().text(), "Hello beautiful world. This is a great test.");
//! ```
//!
@@ -118,28 +117,52 @@
//! // Cursor 1 moves from position 6 to position 3 (after "Hi ")
//! // Cursor 2 stays at position 0 (beginning)
//! ```
+//! > The `cursors` list is sorted by the character position (not id-s).
+//!
+//! ## Change provenance
+//!
+//! Track which changes came from where:
+//!
+//! ```rust
+//! use reconcile_text::{History, SpanWithHistory, BuiltinTokenizer, reconcile};
+//!
+//! let parent = "Merging text is hard!";
+//! let left = "Merging text is easy!"; // Changed "hard" to "easy"
+//! let right = "With reconcile, merging documents is hard!"; // Added prefix and changed word
+//!
+//! let result = reconcile(
+//! parent,
+//! &left.into(),
+//! &right.into(),
+//! &*BuiltinTokenizer::Word,
+//! );
+//!
+//! assert_eq!(
+//! result.apply_with_history(),
+//! vec![
+//! SpanWithHistory::new("Merging text".to_string(), History::RemovedFromRight),
+//! SpanWithHistory::new(
+//! "With reconcile, merging documents".to_string(),
+//! History::AddedFromRight
+//! ),
+//! SpanWithHistory::new(" ".to_string(), History::Unchanged),
+//! SpanWithHistory::new("is".to_string(), History::Unchanged),
+//! SpanWithHistory::new(" hard!".to_string(), History::RemovedFromLeft),
+//! SpanWithHistory::new(" easy!".to_string(), History::AddedFromLeft),
+//! ]
+//! );
+//! ```
//!
//! ## Error handling
//!
//! 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
+//! in edge cases. However, be aware that 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).
+//! [README](https://github.com/schmelczer/reconcile/blob/main/README.md#how-it-works).
mod operation_transformation;
mod raw_operation;
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 4a79fb8..174cfaa 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -248,6 +248,40 @@ where
TextWithCursors::new(builder.take(), self.cursors.clone())
}
+ /// Apply the operations to the text and return the resulting text in chunks
+ /// together with the provenance describing where each chunk came from.
+ ///
+ /// The result includes deleted spans as well.
+ ///
+ /// ```
+ /// use reconcile_text::{History, SpanWithHistory, BuiltinTokenizer, reconcile};
+ ///
+ /// let parent = "Merging text is hard!";
+ /// let left = "Merging text is easy!"; // Changed "hard" to "easy"
+ /// let right = "With reconcile, merging documents is hard!"; // Added prefix and changed word
+ ///
+ /// let result = reconcile(
+ /// parent,
+ /// &left.into(),
+ /// &right.into(),
+ /// &*BuiltinTokenizer::Word,
+ /// );
+ ///
+ /// assert_eq!(
+ /// result.apply_with_history(),
+ /// vec![
+ /// SpanWithHistory::new("Merging text".to_string(), History::RemovedFromRight,),
+ /// SpanWithHistory::new(
+ /// "With reconcile, merging documents".to_string(),
+ /// History::AddedFromRight,
+ /// ),
+ /// SpanWithHistory::new(" ".to_string(), History::Unchanged,),
+ /// SpanWithHistory::new("is".to_string(), History::Unchanged,),
+ /// SpanWithHistory::new(" hard!".to_string(), History::RemovedFromLeft,),
+ /// SpanWithHistory::new(" easy!".to_string(), History::AddedFromLeft,),
+ /// ]
+ /// );
+ /// ```
#[must_use]
pub fn apply_with_history(&self) -> Vec {
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
From e02410ba91be80022ed030dd5a3eb31828b00a76 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 21:58:17 +0100
Subject: [PATCH 013/107] Run ignored test
---
scripts/test.sh | 8 ++++----
src/operation_transformation.rs | 5 +----
2 files changed, 5 insertions(+), 8 deletions(-)
diff --git a/scripts/test.sh b/scripts/test.sh
index 1f365ba..7089ee8 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -2,11 +2,11 @@
set -e
-wasm-pack build --target web --features wasm,wee_alloc
-cargo test --verbose
+wasm-pack build --target web --features wasm
+cargo test --verbose -- --include-ignored
cargo test --features serde
-cargo test --features wasm,wee_alloc
-wasm-pack test --node --features wasm,wee_alloc
+cargo test --features wasm
+wasm-pack test --node --features wasm
cd reconcile-js
npm install
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index 82dedef..10bda6d 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -133,14 +133,11 @@ mod test {
], [
"pride_and_prejudice.txt",
"room_with_a_view.txt",
- "kun_lu.txt",
- "blns.txt"
], [
- "pride_and_prejudice.txt",
"room_with_a_view.txt",
"kun_lu.txt",
"blns.txt"
- ], [0..10000, 10000..20000], [0..10000, 10000..20000], [0..10000, 10000..20000])]
+ ], [0..10000], [0..10000, 10000..20000], [0..10000, 10000..20000])]
fn test_merge_files_without_panic(
file_name_1: &str,
file_name_2: &str,
From 61bea53b7c1960a9c5fc0aec456b2c91fa1f4461 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 21:59:18 +0100
Subject: [PATCH 014/107] Always use wee_alloc for wasm
---
Cargo.lock | 1 -
Cargo.toml | 3 +--
scripts/bump-version.sh | 2 +-
scripts/dev-website.sh | 2 +-
src/wasm.rs | 10 +++-------
5 files changed, 6 insertions(+), 12 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index e417f3c..b7ca5f1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -178,7 +178,6 @@ dependencies = [
name = "reconcile-text"
version = "0.4.10"
dependencies = [
- "cfg-if 1.0.1",
"console_error_panic_hook",
"insta",
"pretty_assertions",
diff --git a/Cargo.toml b/Cargo.toml
index 8e3bd6c..28a5eb6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,12 +32,11 @@ wasm-bindgen = { version = "0.2.99", optional = true }
console_error_panic_hook = { version = "0.1.7", optional = true }
wee_alloc = { version = "0.4.2", optional = true }
-cfg-if = "1.0.1"
[features]
default = []
serde = [ "dep:serde" ]
-wasm = [ "dep:wasm-bindgen" ]
+wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
[dev-dependencies]
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
index fc3eac7..0ab7b7e 100755
--- a/scripts/bump-version.sh
+++ b/scripts/bump-version.sh
@@ -25,7 +25,7 @@ fi
echo "Bumping versions"
cargo set-version --bump $1
-wasm-pack build --target web --features wasm,wee_alloc
+wasm-pack build --target web --features wasm
cd reconcile-js
npm version $1
diff --git a/scripts/dev-website.sh b/scripts/dev-website.sh
index 9f88272..572bdf4 100755
--- a/scripts/dev-website.sh
+++ b/scripts/dev-website.sh
@@ -2,7 +2,7 @@
set -e
-wasm-pack build --target web --features wasm,wee_alloc
+wasm-pack build --target web --features wasm
cd reconcile-js
npm run build
cd ../examples/website
diff --git a/src/wasm.rs b/src/wasm.rs
index 0eda809..8cf080a 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -1,16 +1,12 @@
//! Expose the `reconcile` crate's functionality to WebAssembly.
use core::str;
-use cfg_if::cfg_if;
use wasm_bindgen::prelude::*;
use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors};
-cfg_if! {
- if #[cfg(feature = "wee_alloc")] {
- #[global_allocator]
- static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT;
- }
-}
+
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT;
/// WASM wrapper around `crate::reconcile` for merging text.
#[wasm_bindgen(js_name = reconcile)]
From 86825c177bf39241d0f0fc669536ac085c020a9a Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 22:12:19 +0100
Subject: [PATCH 015/107] Fix typos and clarify
---
README.md | 36 ++++++++++++++++-----------------
docs/advanced-ts.md | 14 ++++++-------
examples/website/src/index.html | 12 +++++------
reconcile-js/package.json | 2 +-
src/lib.rs | 15 +++++++-------
5 files changed, 39 insertions(+), 40 deletions(-)
diff --git a/README.md b/README.md
index a659488..c3675c5 100644
--- a/README.md
+++ b/README.md
@@ -14,10 +14,10 @@ A Rust and TypeScript library for merging conflicting text edits without manual
## Key features
- **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
+- **Cursor tracking** — Automatically repositions cursors and selections throughout the merging process
+- **Flexible tokenisation** — Word-level (default), character-level, line-level, or custom tokenisation strategies
+- **Unicode support** — Full UTF-8 support with proper handling of complex scripts and grapheme clusters
+- **Cross-platform** — Native Rust performance with WebAssembly bindings for JavaScript environments
## Quick start
@@ -28,19 +28,19 @@ Install via crates.io:
cargo add reconcile-text
```
-or add `reconcile-text` to your `Cargo.toml`:
+Alternatively, add `reconcile-text` to your `Cargo.toml`:
```toml
[dependencies]
reconcile-text = "0.5"
```
-Then merge away:
+Then start merging:
```rust
use reconcile_text::{reconcile, BuiltinTokenizer};
-// Start with original text
+// Start with the original text
let parent = "Hello world";
// Two users edit simultaneously
let left = "Hello beautiful world"; // Added "beautiful"
@@ -51,7 +51,7 @@ let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::
assert_eq!(result.apply().text(), "Hi beautiful world");
```
-See [merge-file](examples/merge-file.rs) for another example or the [library's documentation](https://docs.rs/reconcile-text/latest/reconcile_text).
+See the [merge-file example](examples/merge-file.rs) for another example or the [library's documentation](https://docs.rs/reconcile-text/latest/reconcile_text).
### JavaScript/TypeScript
@@ -66,7 +66,7 @@ Then use it in your application:
```javascript
import { reconcile } from 'reconcile-text';
-// Start with original text
+// Start with the original text
const parent = 'Hello world';
// Two users edit simultaneously
const left = 'Hello beautiful world';
@@ -76,17 +76,17 @@ const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
```
-See the [example website](examples/website/src/index.ts) for a more complex example or the [advanced examples document](https://github.com/schmelczer/reconcile/blob/main/docs/advanced-ts.md).
+See the [example website source](examples/website/src/index.ts) for a more complex example or the [advanced examples document](https://github.com/schmelczer/reconcile/blob/main/docs/advanced-ts.md).
## Motivation
-Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously (or offline). Traditional solutions like Conflict-free Replicated Data Types (CRDTs) or Operational Transformation (OT) works well when you control the entire editing environment and can capture every operation ([1]). However, many workflows involve users editing with different tools — for example, Obsidian users editing Markdown files with various editors from Vim to VS Code.
+Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously or asynchronously whilst offline. Traditional solutions like Conflict-free Replicated Data Types (CRDTs) or Operational Transformation (OT) work well when you control the complete editing infrastructure and can capture every individual operation ([1]). However, many workflows involve users editing with various tools, for example, Obsidian users editing Markdown files with various editors ranging from Vim to VS Code.
-This creates **Differential Synchronisation** scenarios ([2], [3]): we 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. The key insight is that while 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.
+This creates **Differential Synchronisation** scenarios ([2], [3]): we 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. The key insight is that while 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.
-> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double-negations. At the same time, semantic conflicts can still arise when merging code, even in the absence of syntactical conflicts.
+> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double negations. At the same time, semantic conflicts can still arise when merging code, even in the absence of syntactic conflicts.
-Differenctial sync is implemented by [universal-sync](https://github.com/invisible-college/universal-sync) and my Obsidian plugin, [vault-link](https://github.com/schmelczer/vault-link) and it requires a merging tool which creates conflict free results for the best user experience.
+Differential sync is implemented by [universal-sync](https://github.com/invisible-college/universal-sync) and my Obsidian plugin [vault-link](https://github.com/schmelczer/vault-link), and it requires a merging tool which creates conflict-free results for the best user experience.
## How it works
@@ -97,9 +97,9 @@ Differenctial sync is implemented by [universal-sync](https://github.com/invisib
3. **Diff optimisation** — Operations are reordered and consolidated to maximise chained changes
4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications and updating cursors
-While the primary goal of `reconcile-text` isn't to implement OT (you can check out [operational-transform-rs](https://github.com/spebern/operational-transform-rs) for a Rust implementation of it), OT provides an elegant way to merge Myers' diff outputs. The same could be achieved with CRDTs which many libraries implement well for text: see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as a few great examples.
+Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see [operational-transform-rs](https://github.com/spebern/operational-transform-rs).) The same could be achieved with CRDTs, which many libraries implement well for text—see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as excellent examples.
-However, the quality of a merge, if only the end result of concurrent changes is observable, depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations can't be supported as Myers' algorithm decomposes them into separate `insert` and `delete` operations regardless the merging algorithm.
+However, when only the end result of concurrent changes is observable, merge quality depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations cannot be supported because Myers' algorithm decomposes them into separate `insert` and `delete` operations, regardless of the merging algorithm used.
## Development
@@ -117,7 +117,7 @@ Contributions are welcome!
```sh
nvm install 22 && nvm use 22
```
-3. Optionally set as default:
+3. Optionally, set as default:
```sh
nvm alias default 22
```
@@ -137,7 +137,7 @@ Contributions are welcome!
- **Run tests**: `scripts/test.sh`
- **Lint and format**: `scripts/lint.sh`
-- **Build demo website**: `scripts/dev-website.sh`
+- **Develop demo website**: `scripts/dev-website.sh`
- **Build demo website**: `scripts/build-website.sh`
- **Publish new version**: `scripts/bump-version.sh patch`
diff --git a/docs/advanced-ts.md b/docs/advanced-ts.md
index b4b03d8..5ecf065 100644
--- a/docs/advanced-ts.md
+++ b/docs/advanced-ts.md
@@ -1,6 +1,6 @@
-# Advanced usage (TypeScript)
+# Advanced Usage (TypeScript)
-## Edit provenance
+## Edit Provenance
Track which changes came from where using `reconcileWithHistory`:
@@ -38,17 +38,17 @@ console.log(result.history); /*
*/
```
-## Tokenisation strategies
+## Tokenisation Strategies
-Reconcile offers different ways to split text for merging:
+Reconcile offers different approaches to split text for merging:
- **Word tokeniser** (`"Word"`) — Splits on word boundaries (recommended for prose)
- **Character tokeniser** (`"Character"`) — Individual characters (fine-grained control)
- **Line tokeniser** (`"Line"`) — Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
-## Cursor tracking
+## Cursor Tracking
-Reconcile automatically tracks cursor positions through merges which is handy in a collaborative editor.
+Reconcile automatically tracks cursor positions through merges, which is handy in collaborative editors. Selections can be tracked by providing them as a pair of cursors.
```javascript
const result = reconcile(
@@ -67,4 +67,4 @@ const result = reconcile(
console.log(result.text); // "Hi beautiful world"
console.log(result.cursors); // [{ id: 2, position: 0 }, { id: 1, position: 3 }]
```
-> The `cursors` list is sorted by the character position (not id-s).
+> The `cursors` list is sorted by character position (not IDs).
diff --git a/examples/website/src/index.html b/examples/website/src/index.html
index fd1a299..b64c864 100644
--- a/examples/website/src/index.html
+++ b/examples/website/src/index.html
@@ -58,16 +58,16 @@
The reconcile(parent, left, right) function takes conflicting
edits and produces clean, unified results using an algorithm inspired by
Operational Transformation. No more
- <<<<<<< markers jumbling your text.
+ <<<<<<< markers cluttering your text.
The process starts with your chosen tokenisation strategy, then applies Myers'
2-way diff algorithm to compare the original with both modified versions.
These diffs are optimised and transformed to preserve the longest meaningful
- changes, before a final merge strategy combines all inserts and deletes
- without losing any edits. Cursor positions may be tracked and updated during
- merging too.
+ changes, before a final merge strategy combines all insertions and deletions
+ without losing any edits. Cursor positions can be tracked and updated during
+ merging as well.
@@ -79,8 +79,8 @@
>documentation
or try editing the text boxes below to see reconcile-text in
- action. Use the tokenisation options to experiment with different approaches —
- the Rust library also supports providing a custom tokeniser.
+ action. Use the tokenisation options to experiment with different approaches—
+ the Rust library also supports custom tokenisers.
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index 65d6fa0..5b1dd42 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -24,7 +24,7 @@
"url": "https://github.com/schmelczer/reconcile/issues",
"email": "andras@schmelczer.dev"
},
- "author": "András Schmelczer",
+ "author": "András Schmelczer ",
"license": "MIT",
"types": "dist/types/index.d.ts",
"files": [
diff --git a/src/lib.rs b/src/lib.rs
index aed8ede..f745c17 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,7 +16,7 @@
//! // Start with original text
//! let parent = "Merging text is hard!";
//! // Two people edit simultaneously
-//! let left = "Merging text is easy!"; // Changed "hard" to "easy"
+//! let left = "Merging text is easy!"; // Changed "hard" to "easy"
//! let right = "With reconcile, merging documents is hard!"; // Added prefix and changed word
//!
//! // Reconcile combines both changes intelligently
@@ -57,7 +57,7 @@
//!
//! ### Custom tokenisation
//!
-//! For specialised use cases, such as structured languages, a custom
+//! For specialised use cases, such as structured languages, custom
//! tokenisation logic can be implemented by providing a function with the
//! signature `Fn(&str) -> Vec>`::
//!
@@ -86,14 +86,13 @@
//! ```
//!
//! > **Note**: Setting token joinability to `false` causes insertions to
-//! > interleave
-//! > (LRLRLR) rather than group together (LLLRRR), which often produces more
-//! > natural-looking merged text.
+//! > interleave (LRLRLR) rather than group together (LLLRRR), which often
+//! > produces more natural-looking merged text.
//!
//! ## Cursor tracking
//!
//! Automatically repositions cursors and selection ranges during merging,
-//! essential for collaborative editors:
+//! which is essential for collaborative editors:
//!
//! ```
//! use reconcile_text::{reconcile, BuiltinTokenizer, TextWithCursors, CursorPosition};
@@ -115,9 +114,9 @@
//! // 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)
+//! // Cursor 2 stays at position 0 (at the beginning)
//! ```
-//! > The `cursors` list is sorted by the character position (not id-s).
+//! > The `cursors` list is sorted by character position (not IDs).
//!
//! ## Change provenance
//!
From 1ed568237ad7a7fe7c428ffe01544711ea40a104 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 12 Jul 2025 22:17:59 +0100
Subject: [PATCH 016/107] Bump versions to 0.5.0
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
reconcile-js/package-lock.json | 4 ++--
reconcile-js/package.json | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index b7ca5f1..bf51cb7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -176,7 +176,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.4.10"
+version = "0.5.0"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index 28a5eb6..8ee40fa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.4.10"
+version = "0.5.0"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index f02abcf..8647e28 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.4.10",
+ "version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.4.10",
+ "version": "0.5.0",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index 5b1dd42..9614238 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.4.10",
+ "version": "0.5.0",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From d87dd58b606199454e200ade21b5c69933bc1880 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 13 Jul 2025 09:28:39 +0100
Subject: [PATCH 017/107] Fix many cursors showing up in deletes in the example
website
---
examples/website/src/index.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/examples/website/src/index.ts b/examples/website/src/index.ts
index 43f1a3b..c6ca70b 100644
--- a/examples/website/src/index.ts
+++ b/examples/website/src/index.ts
@@ -110,13 +110,14 @@ function updateMergedText(): void {
fragment.appendChild(span);
- if (currentPosition === selectionEnd - 1) {
+ const isDelete = history === 'RemovedFromLeft' || history === 'RemovedFromRight';
+ if (currentPosition === selectionEnd - 1 && !isDelete) {
fragment.appendChild(
createSelectionOverlay(selectionSide === 'left', isSelection)
);
}
- if (history !== 'RemovedFromLeft' && history !== 'RemovedFromRight') {
+ if (!isDelete) {
// Only increment currentPosition for non-removed characters
currentPosition++;
}
From da1c78e5bf645a5c950a9abfe7c12dfa2768a1fc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 Aug 2025 03:40:07 +0000
Subject: [PATCH 018/107] Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: '5'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/check.yml | 6 +++---
.github/workflows/gh-pages.yml | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 703de43..bcc5fda 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Setup Node.js environment
uses: actions/setup-node@v4.4.0
@@ -61,7 +61,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Cache Rust dependencies
uses: actions/cache@v4
@@ -85,7 +85,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Setup Node.js environment
uses: actions/setup-node@v4.4.0
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 864ecb9..4382698 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -25,7 +25,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Cache Rust dependencies
uses: actions/cache@v4
From 6b2aaa032835f85ef4d601822cd57c0c6a7aa8f7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 Aug 2025 19:27:59 +0000
Subject: [PATCH 019/107] Bump actions/upload-pages-artifact from 3 to 4
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
dependency-version: '4'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/gh-pages.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 864ecb9..fb5e246 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -56,7 +56,7 @@ jobs:
scripts/build-website.sh
- name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@v4
with:
path: examples/website/dist
From d6b94672266228f28db588a933c1e506546a7918 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 4 Sep 2025 19:21:04 +0000
Subject: [PATCH 020/107] Bump insta from 1.43.1 to 1.43.2
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.43.1 to 1.43.2.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.43.1...1.43.2)
---
updated-dependencies:
- dependency-name: insta
dependency-version: 1.43.2
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
Cargo.lock | 4 ++--
Cargo.toml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bf51cb7..f51e805 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -87,9 +87,9 @@ dependencies = [
[[package]]
name = "insta"
-version = "1.43.1"
+version = "1.43.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
+checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
dependencies = [
"console",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 8ee40fa..52fa417 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,7 +40,7 @@ wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
[dev-dependencies]
-insta = "1.42.2"
+insta = "1.43.2"
pretty_assertions = "1.4.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
From cbd240a70335f29e05255818b03834581e7d58d2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 19:49:25 +0000
Subject: [PATCH 021/107] Bump wasm-bindgen-test from 0.3.50 to 0.3.54
Bumps [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen) from 0.3.50 to 0.3.54.
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)
---
updated-dependencies:
- dependency-name: wasm-bindgen-test
dependency-version: 0.3.54
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
Cargo.lock | 41 +++++++++++++++++++++--------------------
Cargo.toml | 2 +-
2 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bf51cb7..2b4638f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -104,9 +104,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
-version = "0.3.77"
+version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -323,21 +323,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.100"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
dependencies = [
"cfg-if 1.0.1",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
+ "wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-backend"
-version = "0.2.100"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
dependencies = [
"bumpalo",
"log",
@@ -349,9 +350,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.50"
+version = "0.4.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
dependencies = [
"cfg-if 1.0.1",
"js-sys",
@@ -362,9 +363,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.100"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -372,9 +373,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.100"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
dependencies = [
"proc-macro2",
"quote",
@@ -385,18 +386,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.100"
+version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.50"
+version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
+checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865"
dependencies = [
"js-sys",
"minicov",
@@ -407,9 +408,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.50"
+version = "0.3.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
+checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8"
dependencies = [
"proc-macro2",
"quote",
@@ -418,9 +419,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.77"
+version = "0.3.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
dependencies = [
"js-sys",
"wasm-bindgen",
diff --git a/Cargo.toml b/Cargo.toml
index 8ee40fa..145025a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,7 +45,7 @@ pretty_assertions = "1.4.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
test-case = "3.3.1"
-wasm-bindgen-test = "0.3.49"
+wasm-bindgen-test = "0.3.54"
[profile.release]
codegen-units = 1
From 27f071d8db15ecfab202232165884e1e75f514d4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 14 Oct 2025 19:21:49 +0000
Subject: [PATCH 022/107] Bump actions/setup-node from 4.4.0 to 6.0.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...v6.0.0)
---
updated-dependencies:
- dependency-name: actions/setup-node
dependency-version: 6.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/check.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 703de43..fd9711d 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js environment
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.0.0
with:
node-version: '22.x'
check-latest: true
@@ -88,7 +88,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup Node.js environment
- uses: actions/setup-node@v4.4.0
+ uses: actions/setup-node@v6.0.0
with:
node-version: '22.x'
check-latest: true
From 6f002459ae4447cc54e252ee8c98db5a24d2a9b3 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 20:22:03 +0000
Subject: [PATCH 023/107] Fix LS in VS Code
---
Cargo.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/Cargo.toml b/Cargo.toml
index 8ee40fa..50a861f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ default = []
serde = [ "dep:serde" ]
wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
+all = [ "serde", "wasm", "console_error_panic_hook" ]
[dev-dependencies]
insta = "1.42.2"
From 8a520344266eb4afe5da981563ed990aa0d54526 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 20:22:16 +0000
Subject: [PATCH 024/107] Remove trailing spaces
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c3675c5..a5e5a46 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ A Rust and TypeScript library for merging conflicting text edits without manual
### Rust
Install via crates.io:
+
```sh
cargo add reconcile-text
```
@@ -97,7 +98,7 @@ Differential sync is implemented by [universal-sync](https://github.com/invisibl
3. **Diff optimisation** — Operations are reordered and consolidated to maximise chained changes
4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications and updating cursors
-Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see [operational-transform-rs](https://github.com/spebern/operational-transform-rs).) The same could be achieved with CRDTs, which many libraries implement well for text—see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as excellent examples.
+Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see [operational-transform-rs](https://github.com/spebern/operational-transform-rs).) The same could be achieved with CRDTs, which many libraries implement well for text—see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as excellent examples.
However, when only the end result of concurrent changes is observable, merge quality depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations cannot be supported because Myers' algorithm decomposes them into separate `insert` and `delete` operations, regardless of the merging algorithm used.
@@ -146,7 +147,7 @@ Contributions are welcome!
[MIT](./LICENSE)
[1]:https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html
-[2]: https://neil.fraser.name/writing/sync/
+[2]: https://neil.fraser.name/writing/sync/
[3]: https://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf
[4]: https://blog.jcoglan.com/2017/05/08/merging-with-diff3/
[5]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf
From de89532880c6c4a3b559f4d88e0539c9b4dd1220 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 20:23:22 +0000
Subject: [PATCH 025/107] Move Side to EditedText from Operation
---
src/operation_transformation.rs | 13 +--
src/operation_transformation/edited_text.rs | 100 +++++++++++++++---
src/operation_transformation/operation.rs | 33 +++---
...ted_text__tests__calculate_operations.snap | 11 +-
...ts__calculate_operations_with_no_diff.snap | 6 +-
.../utils/cook_operations.rs | 11 +-
6 files changed, 119 insertions(+), 55 deletions(-)
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index 10bda6d..a2ac1c5 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -3,13 +3,10 @@ mod operation;
mod utils;
use std::fmt::Debug;
-pub use edited_text::EditedText;
+pub use edited_text::{ChangeSet, EditedText};
pub use operation::Operation;
-use crate::{
- Tokenizer,
- types::{side::Side, text_with_cursors::TextWithCursors},
-};
+use crate::{Tokenizer, types::text_with_cursors::TextWithCursors};
/// Given an `original` document and two concurrent edits to it,
/// return a document containing all changes from both `left`
@@ -48,10 +45,8 @@ pub fn reconcile<'a, T>(
where
T: PartialEq + Clone + Debug,
{
- let left_operations =
- EditedText::from_strings_with_tokenizer(original, left, tokenizer, Side::Left);
- let right_operations =
- EditedText::from_strings_with_tokenizer(original, right, tokenizer, Side::Right);
+ let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer);
+ let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer);
left_operations.merge(right_operations)
}
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 174cfaa..3894aae 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -1,4 +1,4 @@
-use std::fmt::Debug;
+use std::{fmt::Debug, vec};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@@ -35,9 +35,35 @@ where
{
text: &'a str,
operations: Vec>,
+ operation_sides: Vec,
cursors: Vec,
}
+/// A serializable representation of the changes made to a text document
+/// without the original text.
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct ChangeSet
+where
+ T: PartialEq + Clone + Debug,
+{
+ operations: Vec>,
+ cursors: Vec,
+}
+
+impl<'a, T> ChangeSet
+where
+ T: PartialEq + Clone + Debug,
+{
+ #[must_use]
+ pub fn new(operations: Vec>, cursors: Vec) -> Self {
+ Self {
+ operations,
+ cursors,
+ }
+ }
+}
+
impl<'a> EditedText<'a, String> {
/// Create an `EditedText` from the given original (old) and updated (new)
/// strings. The returned `EditedText` represents the changes from the
@@ -46,8 +72,8 @@ impl<'a> EditedText<'a, String> {
/// word tokenizer is used to tokenize the text which splits the text on
/// whitespaces.
#[must_use]
- pub fn from_strings(original: &'a str, updated: &TextWithCursors, side: Side) -> Self {
- Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word, side)
+ pub fn from_strings(original: &'a str, updated: &TextWithCursors) -> Self {
+ Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word)
}
}
@@ -64,16 +90,18 @@ where
original: &'a str,
updated: &TextWithCursors,
tokenizer: &Tokenizer,
- side: Side,
) -> Self {
let original_tokens = (tokenizer)(original);
let updated_tokens = (tokenizer)(&updated.text());
let diff: Vec> = RawOperation::vec_from(&original_tokens, &updated_tokens);
+ let operations: Vec> = cook_operations(elongate_operations(diff)).collect();
+ let operation_count = operations.len();
Self::new(
original,
- cook_operations(elongate_operations(diff), side).collect(),
+ operations,
+ vec![Side::Left; operation_count],
updated.cursors(),
)
}
@@ -81,12 +109,18 @@ where
/// Create a new `EditedText` with the given operations.
/// The operations must be in the order in which they are meant to be
/// applied. The operations must not overlap.
- fn new(text: &'a str, operations: Vec>, mut cursors: Vec) -> Self {
+ fn new(
+ text: &'a str,
+ operations: Vec>,
+ operation_sides: Vec,
+ mut cursors: Vec,
+ ) -> Self {
cursors.sort_by_key(|cursor| cursor.char_index);
Self {
text,
operations,
+ operation_sides,
cursors,
}
}
@@ -109,6 +143,8 @@ where
let mut merged_operations: Vec> =
Vec::with_capacity(self.operations.len() + other.operations.len());
+ let mut merged_operation_sides: Vec =
+ Vec::with_capacity(self.operations.len() + other.operations.len());
let mut left_iter = self.operations.into_iter();
let mut right_iter = other.operations.into_iter();
@@ -149,7 +185,7 @@ where
);
let original_length = operation.len();
- let result = match side {
+ let (side, result) = match side {
Side::Left => {
let result = operation.merge_operations(&mut last_other_op);
@@ -181,7 +217,7 @@ where
maybe_left_op = left_iter.next();
last_left_op = Some(result.clone());
- result
+ (Side::Left, result)
}
Side::Right => {
let result = operation.merge_operations(&mut last_other_op);
@@ -214,7 +250,7 @@ where
maybe_right_op = right_iter.next();
last_right_op = Some(result.clone());
- result
+ (Side::Right, result)
}
};
@@ -227,13 +263,21 @@ where
}
merged_operations.push(result);
+ merged_operation_sides.push(side);
}
for cursor in left_cursors.chain(right_cursors) {
merged_cursors.push(cursor.with_index(merged_length));
}
- Self::new(self.text, merged_operations, merged_cursors)
+ debug_assert_eq!(merged_operations.len(), merged_operation_sides.len());
+
+ Self::new(
+ self.text,
+ merged_operations,
+ merged_operation_sides,
+ merged_cursors,
+ )
}
/// Apply the operations to the text and return the resulting text.
@@ -288,14 +332,14 @@ where
let mut history = Vec::with_capacity(self.operations.len());
- for operation in &self.operations {
+ for (operation, side) in self.operations.iter().zip(self.operation_sides.iter()) {
builder = operation.apply(builder);
match operation {
Operation::Equal { .. } => {
history.push(SpanWithHistory::new(builder.take(), History::Unchanged));
}
- Operation::Insert { side, .. } => match side {
+ Operation::Insert { .. } => match side {
Side::Left => {
history.push(SpanWithHistory::new(builder.take(), History::AddedFromLeft));
}
@@ -307,7 +351,6 @@ where
Operation::Delete {
deleted_character_count,
order,
- side,
..
} => {
let deleted = self.text[*order..*order + *deleted_character_count].to_string();
@@ -325,6 +368,29 @@ where
history
}
+
+ /// Serialize the `EditedText` as a `ChangeSet`, which contains only
+ /// the operations and cursor positions, without the original text.
+ /// This is useful for sending changes over the network if there's
+ /// a clear consensus on the original text.
+ #[must_use]
+ pub fn serialise_as_change_set(&self) -> ChangeSet {
+ ChangeSet::new(self.operations.clone(), self.cursors.clone())
+ }
+
+ /// Deserialize an `EditedText` from a `ChangeSet` and the original text.
+ /// This is useful for reconstructing the `EditedText` on the receiving
+ /// end after sending only the `ChangeSet` over the network.
+ #[must_use]
+ pub fn from_change_set(text: &'a str, change_set: ChangeSet) -> EditedText<'a, T> {
+ let operation_count = change_set.operations.len();
+ EditedText::new(
+ text,
+ change_set.operations,
+ vec![Side::Left; operation_count],
+ change_set.cursors,
+ )
+ }
}
#[cfg(test)]
@@ -339,7 +405,7 @@ mod tests {
let left = "hello world! How are you? Adam";
let right = "Hello, my friend! How are you doing? Albert";
- let operations = EditedText::from_strings(left, &right.into(), Side::Right);
+ let operations = EditedText::from_strings(left, &right.into());
insta::assert_debug_snapshot!(operations);
@@ -351,7 +417,7 @@ mod tests {
fn test_calculate_operations_with_no_diff() {
let text = "hello world!";
- let operations = EditedText::from_strings(text, &text.into(), Side::Right);
+ let operations = EditedText::from_strings(text, &text.into());
assert_debug_snapshot!(operations);
@@ -366,8 +432,8 @@ mod tests {
let right = "Hello world! How are you?";
let expected = "Hello world! How are you? I'm Andras.";
- let operations_1 = EditedText::from_strings(original, &left.into(), Side::Left);
- let operations_2 = EditedText::from_strings(original, &right.into(), Side::Right);
+ let operations_1 = EditedText::from_strings(original, &left.into());
+ let operations_2 = EditedText::from_strings(original, &right.into());
let operations = operations_1.merge(operations_2);
assert_eq!(operations.apply().text(), expected);
diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs
index 1c3060c..7a8f92a 100644
--- a/src/operation_transformation/operation.rs
+++ b/src/operation_transformation/operation.rs
@@ -4,7 +4,7 @@ use core::fmt::{Debug, Display};
use serde::{Deserialize, Serialize};
use crate::{
- Side, Token,
+ Token,
utils::{
find_longest_prefix_contained_within::find_longest_prefix_contained_within,
string_builder::StringBuilder,
@@ -23,23 +23,21 @@ where
length: usize,
#[cfg(debug_assertions)]
+ #[cfg_attr(feature = "serde", serde(skip_serializing))]
text: Option,
},
Insert {
- side: Side,
-
order: usize,
text: Vec>,
},
Delete {
- side: Side,
-
order: usize,
deleted_character_count: usize,
#[cfg(debug_assertions)]
+ #[cfg_attr(feature = "serde", serde(skip_serializing))]
deleted_text: Option,
},
}
@@ -72,15 +70,14 @@ where
}
/// Creates an insert operation with the given index and text.
- pub fn create_insert(order: usize, text: Vec>, side: Side) -> Self {
- Operation::Insert { side, order, text }
+ pub fn create_insert(order: usize, text: Vec>) -> Self {
+ Operation::Insert { order, text }
}
/// Creates a delete operation with the given index and number of
/// to-be-deleted characters.
- pub fn create_delete(order: usize, deleted_character_count: usize, side: Side) -> Self {
+ pub fn create_delete(order: usize, deleted_character_count: usize) -> Self {
Operation::Delete {
- side,
order,
deleted_character_count,
@@ -89,9 +86,8 @@ where
}
}
- pub fn create_delete_with_text(order: usize, text: String, side: Side) -> Self {
+ pub fn create_delete_with_text(order: usize, text: String) -> Self {
Operation::Delete {
- side,
order,
deleted_character_count: text.chars().count(),
@@ -206,7 +202,7 @@ where
match (operation, previous_operation) {
(
- Operation::Insert { side, order, text },
+ Operation::Insert { order, text },
Some(Operation::Insert {
text: previous_inserted_text,
..
@@ -218,12 +214,11 @@ where
let offset_in_tokens =
find_longest_prefix_contained_within(previous_inserted_text, &text);
- Operation::create_insert(order, text[offset_in_tokens..].to_vec(), side)
+ Operation::create_insert(order, text[offset_in_tokens..].to_vec())
}
(
Operation::Delete {
- side,
order,
deleted_character_count,
@@ -247,20 +242,19 @@ where
#[cfg(debug_assertions)]
let updated_delete = deleted_text.as_ref().map_or_else(
- || Operation::create_delete(order + overlap, new_length, side),
+ || Operation::create_delete(order + overlap, new_length),
|text| {
Operation::create_delete_with_text(
order + overlap,
text.chars()
.skip(deleted_character_count - new_length)
.collect::(),
- side,
)
},
);
#[cfg(not(debug_assertions))]
- let updated_delete = Operation::create_delete(order + overlap, new_length, side);
+ let updated_delete = Operation::create_delete(order + overlap, new_length);
updated_delete
}
@@ -405,8 +399,7 @@ mod tests {
#[test]
fn test_apply_delete_with_create() {
let builder = StringBuilder::new("hello world");
- let delete_operation =
- Operation::<()>::create_delete_with_text(0, "hello ".to_owned(), Side::Left);
+ let delete_operation = Operation::<()>::create_delete_with_text(0, "hello ".to_owned());
let retain_operation = Operation::<()>::create_equal(6, 5);
let mut builder = delete_operation.apply(builder);
@@ -420,7 +413,7 @@ mod tests {
let builder = StringBuilder::new("hello");
let retain_operation = Operation::<()>::create_equal(0, 5);
- let insert_operation = Operation::create_insert(5, vec![" my friend".into()], Side::Right);
+ let insert_operation = Operation::create_insert(5, vec![" my friend".into()]);
let mut builder = retain_operation.apply(builder);
builder = insert_operation.apply(builder);
diff --git a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap
index abbabbd..0096f0e 100644
--- a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap
+++ b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations.snap
@@ -1,7 +1,6 @@
---
source: src/operation_transformation/edited_text.rs
expression: operations
-snapshot_kind: text
---
EditedText {
text: "hello world! How are you? Adam",
@@ -15,5 +14,15 @@ EditedText {
,
,
],
+ operation_sides: [
+ Left,
+ Left,
+ Left,
+ Left,
+ Left,
+ Left,
+ Left,
+ Left,
+ ],
cursors: [],
}
diff --git a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap
index 275a552..cf6a674 100644
--- a/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap
+++ b/src/operation_transformation/snapshots/reconcile_text__operation_transformation__edited_text__tests__calculate_operations_with_no_diff.snap
@@ -1,7 +1,6 @@
---
source: src/operation_transformation/edited_text.rs
expression: operations
-snapshot_kind: text
---
EditedText {
text: "hello world!",
@@ -10,5 +9,10 @@ EditedText {
,
,
],
+ operation_sides: [
+ Left,
+ Left,
+ Left,
+ ],
cursors: [],
}
diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs
index 0b188cc..2f1d0ac 100644
--- a/src/operation_transformation/utils/cook_operations.rs
+++ b/src/operation_transformation/utils/cook_operations.rs
@@ -1,10 +1,10 @@
use std::fmt::Debug;
-use crate::{operation_transformation::Operation, raw_operation::RawOperation, types::side::Side};
+use crate::{operation_transformation::Operation, raw_operation::RawOperation};
/// Turn raw operations into ordered operations while keeping track of the
/// original token's indexes.
-pub fn cook_operations(raw_operations: I, side: Side) -> impl Iterator>
+pub fn cook_operations(raw_operations: I) -> impl Iterator>
where
I: IntoIterator>,
T: PartialEq + Clone + Debug,
@@ -29,18 +29,15 @@ where
op
}
- RawOperation::Insert(tokens) => {
- Operation::create_insert(original_text_index, tokens, side)
- }
+ RawOperation::Insert(tokens) => Operation::create_insert(original_text_index, tokens),
RawOperation::Delete(..) => {
let op = if cfg!(debug_assertions) {
Operation::create_delete_with_text(
original_text_index,
raw_operation.get_original_text(),
- side,
)
} else {
- Operation::create_delete(original_text_index, length, side)
+ Operation::create_delete(original_text_index, length)
};
original_text_index += length;
From e052aa46c41ccbdcedffc460e5d0bbea1956074e Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 21:17:02 +0000
Subject: [PATCH 026/107] Fix version
---
reconcile-js/package-lock.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 8647e28..b213742 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.4.10",
+ "version": "0.5.0",
"dev": true,
"license": "MIT"
},
From 3da0673af6e65bfde8a70beda3da9a566134a1d2 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 21:19:56 +0000
Subject: [PATCH 027/107] Add optimal representation
---
src/lib.rs | 44 ++++-
src/operation_transformation.rs | 5 +-
src/operation_transformation/edited_text.rs | 81 ++++----
src/operation_transformation/transport.rs | 198 ++++++++++++++++++++
tests/test.rs | 32 +++-
5 files changed, 325 insertions(+), 35 deletions(-)
create mode 100644 src/operation_transformation/transport.rs
diff --git a/src/lib.rs b/src/lib.rs
index f745c17..cbe354f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -151,6 +151,48 @@
//! ]
//! );
//! ```
+//! ## Efficiently serialize changes
+//!
+//! The edits can be serialized into a compact representation without the full
+//! original text, making the size only depends on the changes made.
+//!
+//! ```rust
+//! use reconcile_text::{EditedText, BuiltinTokenizer};
+//! use serde_yaml;
+//! use pretty_assertions::assert_eq;
+//!
+//!
+//! let original = "Merging text is hard!";
+//! let changes = "Merging text is easy with reconcile!";
+//!
+//! let result = EditedText::from_strings(
+//! original,
+//! &changes.into()
+//! );
+//!
+//! let serialized = serde_yaml::to_string(&result.serialise_as_change_set()).unwrap();
+//! assert_eq!(
+//! serialized,
+//! concat!(
+//! "operations:\n",
+//! "- 15\n",
+//! "- -6\n",
+//! "- ' easy with reconcile!'\n",
+//! "cursors: []\n"
+//! )
+//! );
+//!
+//! let deserialized = serde_yaml::from_str(&serialized).unwrap();
+//! let reconstructed = EditedText::from_change_set(
+//! original,
+//! deserialized,
+//! &*BuiltinTokenizer::Word
+//! );
+//! assert_eq!(
+//! reconstructed.apply().text(),
+//! "Merging text is easy with reconcile!"
+//! );
+//! ```
//!
//! ## Error handling
//!
@@ -169,7 +211,7 @@ mod tokenizer;
mod types;
mod utils;
-pub use operation_transformation::{EditedText, reconcile};
+pub use operation_transformation::{ChangeSet, EditedText, reconcile};
pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token};
pub use types::{
cursor_position::CursorPosition, history::History, side::Side,
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index a2ac1c5..0d99ca4 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -1,10 +1,13 @@
mod edited_text;
mod operation;
mod utils;
+mod transport;
use std::fmt::Debug;
-pub use edited_text::{ChangeSet, EditedText};
+
+pub use transport::{ChangeSet};
pub use operation::Operation;
+pub use edited_text::{EditedText};
use crate::{Tokenizer, types::text_with_cursors::TextWithCursors};
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 3894aae..ed60515 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -4,9 +4,10 @@ use std::{fmt::Debug, vec};
use serde::{Deserialize, Serialize};
use crate::{
- BuiltinTokenizer, CursorPosition, TextWithCursors,
+ BuiltinTokenizer, ChangeSet, CursorPosition, TextWithCursors,
operation_transformation::{
Operation,
+ transport::SimpleOperation,
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
},
raw_operation::RawOperation,
@@ -39,31 +40,6 @@ where
cursors: Vec,
}
-/// A serializable representation of the changes made to a text document
-/// without the original text.
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[derive(Debug, Clone, PartialEq, Default)]
-pub struct ChangeSet
-where
- T: PartialEq + Clone + Debug,
-{
- operations: Vec>,
- cursors: Vec,
-}
-
-impl<'a, T> ChangeSet
-where
- T: PartialEq + Clone + Debug,
-{
- #[must_use]
- pub fn new(operations: Vec>, cursors: Vec) -> Self {
- Self {
- operations,
- cursors,
- }
- }
-}
-
impl<'a> EditedText<'a, String> {
/// Create an `EditedText` from the given original (old) and updated (new)
/// strings. The returned `EditedText` represents the changes from the
@@ -370,23 +346,31 @@ where
}
/// Serialize the `EditedText` as a `ChangeSet`, which contains only
- /// the operations and cursor positions, without the original text.
+ /// the operations and cursor positions, but without the original text.
/// This is useful for sending changes over the network if there's
/// a clear consensus on the original text.
#[must_use]
- pub fn serialise_as_change_set(&self) -> ChangeSet {
- ChangeSet::new(self.operations.clone(), self.cursors.clone())
+ pub fn serialise_as_change_set(&self) -> ChangeSet {
+ ChangeSet::new(
+ SimpleOperation::from_operations(&self.operations),
+ self.cursors.clone(),
+ )
}
/// Deserialize an `EditedText` from a `ChangeSet` and the original text.
/// This is useful for reconstructing the `EditedText` on the receiving
/// end after sending only the `ChangeSet` over the network.
#[must_use]
- pub fn from_change_set(text: &'a str, change_set: ChangeSet) -> EditedText<'a, T> {
- let operation_count = change_set.operations.len();
+ pub fn from_change_set(
+ text: &'a str,
+ change_set: ChangeSet,
+ tokenizer: &Tokenizer,
+ ) -> EditedText<'a, T> {
+ let operations = SimpleOperation::to_operations(change_set.operations, text, tokenizer);
+ let operation_count = operations.len();
EditedText::new(
text,
- change_set.operations,
+ operations,
vec![Side::Left; operation_count],
change_set.cursors,
)
@@ -397,6 +381,7 @@ where
mod tests {
use insta::assert_debug_snapshot;
use pretty_assertions::assert_eq;
+ use serde_yaml;
use super::*;
@@ -438,4 +423,36 @@ mod tests {
let operations = operations_1.merge(operations_2);
assert_eq!(operations.apply().text(), expected);
}
+
+ #[test]
+ fn test_change_set_deserialisation() {
+ let original = "Merging text is hard!";
+ let changes = "Merging text is easy with reconcile!";
+ let result = EditedText::from_strings(original, &changes.into());
+ let serialized = serde_yaml::to_string(&result.serialise_as_change_set()).unwrap();
+
+ let expected = concat!(
+ "operations:\n",
+ "- 15\n",
+ "- -6\n",
+ "- ' easy with reconcile!'\n",
+ "cursors: []\n"
+ );
+
+ assert_eq!(serialized, expected);
+ }
+
+ #[test]
+ fn test_change_set_serialization() {
+ let original = "The quick brown fox jumps over the lazy dog.";
+ let updated = "The quick red fox jumped over the very lazy dog!";
+
+ let edited_text = EditedText::from_strings(original, &updated.into());
+
+ let change_set = edited_text.serialise_as_change_set();
+ let deserialized_edited_text =
+ EditedText::from_change_set(original, change_set, &*BuiltinTokenizer::Word);
+
+ assert_eq!(deserialized_edited_text.apply().text(), updated);
+ }
}
diff --git a/src/operation_transformation/transport.rs b/src/operation_transformation/transport.rs
new file mode 100644
index 0000000..4f8fee4
--- /dev/null
+++ b/src/operation_transformation/transport.rs
@@ -0,0 +1,198 @@
+use std::fmt::Debug;
+
+#[cfg(feature = "serde")]
+use serde::{
+ Deserialize, Serialize,
+ de::{self, Deserializer, SeqAccess, Visitor},
+ ser::{SerializeSeq, Serializer},
+};
+
+use crate::{CursorPosition, Tokenizer, operation_transformation::Operation};
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum SimpleOperation {
+ Equal { length: usize },
+
+ Insert { text: String },
+
+ Delete { length: usize },
+}
+
+impl SimpleOperation {
+ pub fn from_operations(operation: &Vec>) -> Vec
+ where
+ T: PartialEq + Clone + Debug,
+ {
+ let mut result: Vec = Vec::with_capacity(operation.len());
+ let mut previous_equal: Option = None;
+
+ for operation in operation {
+ match operation {
+ Operation::Equal { length, .. } => {
+ if let Some(prev_length) = previous_equal {
+ previous_equal = Some(prev_length + *length);
+ } else {
+ previous_equal = Some(*length);
+ }
+ }
+ Operation::Insert { text, .. } => {
+ if let Some(prev_length) = previous_equal {
+ result.push(SimpleOperation::Equal {
+ length: prev_length,
+ });
+ previous_equal = None;
+ }
+
+ let text: String = text
+ .iter()
+ .map(super::super::tokenizer::token::Token::original)
+ .collect();
+ result.push(SimpleOperation::Insert { text });
+ }
+ Operation::Delete {
+ deleted_character_count,
+ ..
+ } => {
+ if let Some(prev_length) = previous_equal {
+ result.push(SimpleOperation::Equal {
+ length: prev_length,
+ });
+ previous_equal = None;
+ }
+
+ result.push(SimpleOperation::Delete {
+ length: *deleted_character_count,
+ });
+ }
+ }
+ }
+
+ if let Some(prev_length) = previous_equal {
+ result.push(SimpleOperation::Equal {
+ length: prev_length,
+ });
+ }
+
+ result
+ }
+
+ pub fn to_operations(
+ simple_operations: Vec,
+ original_text: &str,
+ tokenizer: &Tokenizer,
+ ) -> Vec>
+ where
+ T: PartialEq + Clone + Debug,
+ {
+ let mut operations: Vec> = Vec::with_capacity(simple_operations.len());
+
+ let mut order = 0;
+
+ for simple_operation in simple_operations {
+ match simple_operation {
+ SimpleOperation::Equal { length } => {
+ let original_characters: String =
+ original_text.chars().skip(order).take(length).collect();
+
+ let original_tokens = tokenizer(&original_characters);
+ for token in original_tokens {
+ operations
+ .push(Operation::create_equal(order, token.get_original_length()));
+ order += token.get_original_length();
+ }
+ }
+ SimpleOperation::Insert { text } => {
+ let tokens = tokenizer(&text);
+ operations.push(Operation::create_insert(order, tokens));
+ }
+ SimpleOperation::Delete { length } => {
+ operations.push(Operation::create_delete(order, length));
+ order += length;
+ }
+ }
+ }
+
+ operations
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for SimpleOperation {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ // neat idea from https://github.com/spebern/operational-transform-rs/blob/9faa17f0a2b282ac2e09dbb2d29fdaf2ae0bbb4a/operational-transform/src/serde.rs#L14
+ match self {
+ SimpleOperation::Equal { length } => serializer.serialize_u64(*length as u64),
+ SimpleOperation::Insert { text } => serializer.serialize_str(text),
+ SimpleOperation::Delete { length } => serializer.serialize_i64(-(*length as i64)),
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for SimpleOperation {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: Deserializer<'de>,
+ {
+ struct OperationVisitor;
+
+ impl<'de> Visitor<'de> for OperationVisitor {
+ type Value = SimpleOperation;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("an integer between -2^64 and 2^63 or a string")
+ }
+
+ fn visit_u64(self, value: u64) -> Result
+ where
+ E: de::Error,
+ {
+ Ok(SimpleOperation::Equal {
+ length: value as usize,
+ })
+ }
+
+ fn visit_i64(self, value: i64) -> Result
+ where
+ E: de::Error,
+ {
+ Ok(SimpleOperation::Delete {
+ length: (-value) as usize,
+ })
+ }
+
+ fn visit_str(self, value: &str) -> Result
+ where
+ E: de::Error,
+ {
+ Ok(SimpleOperation::Insert {
+ text: value.to_owned(),
+ })
+ }
+ }
+
+ deserializer.deserialize_any(OperationVisitor)
+ }
+}
+
+/// A serializable representation of the changes made to a text document
+/// without the original text.
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct ChangeSet {
+ pub operations: Vec,
+ pub cursors: Vec,
+}
+
+impl ChangeSet {
+ #[must_use]
+ pub fn new(operations: Vec, cursors: Vec) -> Self {
+ Self {
+ operations,
+ cursors,
+ }
+ }
+}
diff --git a/tests/test.rs b/tests/test.rs
index e9968b9..00f5163 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -3,7 +3,7 @@ mod example_document;
use std::{fs, path::Path};
use example_document::ExampleDocument;
-use reconcile_text::{BuiltinTokenizer, reconcile};
+use reconcile_text::{BuiltinTokenizer, EditedText, reconcile};
use serde::Deserialize;
#[test]
@@ -34,6 +34,36 @@ fn test_document_one_way_with_cursors() {
}
}
+#[test]
+fn test_document_one_way_with_cursors_and_serialisation() {
+ for doc in &get_all_documents() {
+ let parent = doc.parent();
+ let left_operations =
+ EditedText::from_strings_with_tokenizer(&parent, &doc.left(), &*BuiltinTokenizer::Word);
+ let right_operations = EditedText::from_strings_with_tokenizer(
+ &parent,
+ &doc.right(),
+ &*BuiltinTokenizer::Word,
+ );
+
+ let serialised_left = serde_yaml::from_str(
+ &serde_yaml::to_string(&left_operations.serialise_as_change_set()).unwrap(),
+ )
+ .unwrap();
+ let serialised_right = serde_yaml::from_str(
+ &serde_yaml::to_string(&right_operations.serialise_as_change_set()).unwrap(),
+ )
+ .unwrap();
+
+ let restored_left_operations =
+ EditedText::from_change_set(&parent, serialised_left, &*BuiltinTokenizer::Word);
+ let restored_right_operations =
+ EditedText::from_change_set(&parent, serialised_right, &*BuiltinTokenizer::Word);
+
+ doc.assert_eq(&restored_left_operations.merge(restored_right_operations));
+ }
+}
+
#[test]
fn test_document_inverse_way_without_cursors() {
for doc in &get_all_documents() {
From 450eaaff05be2a4d89294606da1e7294c8fee6d4 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 21:29:52 +0000
Subject: [PATCH 028/107] Fix lints
---
scripts/test.sh | 5 ++---
src/operation_transformation.rs | 7 +++----
src/operation_transformation/edited_text.rs | 1 -
src/operation_transformation/transport.rs | 18 +++++++++++-------
src/utils/string_builder.rs | 14 +++++++++++++-
5 files changed, 29 insertions(+), 16 deletions(-)
diff --git a/scripts/test.sh b/scripts/test.sh
index 7089ee8..7eb816c 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -3,9 +3,8 @@
set -e
wasm-pack build --target web --features wasm
-cargo test --verbose -- --include-ignored
-cargo test --features serde
-cargo test --features wasm
+cargo test --verbose --features serde -- --include-ignored
+cargo test --features serde,wasm
wasm-pack test --node --features wasm
cd reconcile-js
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index 0d99ca4..e1f173a 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -1,13 +1,12 @@
mod edited_text;
mod operation;
-mod utils;
mod transport;
+mod utils;
use std::fmt::Debug;
-
-pub use transport::{ChangeSet};
+pub use edited_text::EditedText;
pub use operation::Operation;
-pub use edited_text::{EditedText};
+pub use transport::ChangeSet;
use crate::{Tokenizer, types::text_with_cursors::TextWithCursors};
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index ed60515..8e1ddfe 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -381,7 +381,6 @@ where
mod tests {
use insta::assert_debug_snapshot;
use pretty_assertions::assert_eq;
- use serde_yaml;
use super::*;
diff --git a/src/operation_transformation/transport.rs b/src/operation_transformation/transport.rs
index 4f8fee4..eef44fd 100644
--- a/src/operation_transformation/transport.rs
+++ b/src/operation_transformation/transport.rs
@@ -3,8 +3,8 @@ use std::fmt::Debug;
#[cfg(feature = "serde")]
use serde::{
Deserialize, Serialize,
- de::{self, Deserializer, SeqAccess, Visitor},
- ser::{SerializeSeq, Serializer},
+ de::{self, Deserializer, Visitor},
+ ser::Serializer,
};
use crate::{CursorPosition, Tokenizer, operation_transformation::Operation};
@@ -126,7 +126,9 @@ impl Serialize for SimpleOperation {
match self {
SimpleOperation::Equal { length } => serializer.serialize_u64(*length as u64),
SimpleOperation::Insert { text } => serializer.serialize_str(text),
- SimpleOperation::Delete { length } => serializer.serialize_i64(-(*length as i64)),
+ SimpleOperation::Delete { length } => {
+ serializer.serialize_i64(-(i64::try_from(*length).unwrap_or(i64::MAX)))
+ }
}
}
}
@@ -137,12 +139,14 @@ impl<'de> Deserialize<'de> for SimpleOperation {
where
D: Deserializer<'de>,
{
+ use std::fmt;
+
struct OperationVisitor;
- impl<'de> Visitor<'de> for OperationVisitor {
+ impl Visitor<'_> for OperationVisitor {
type Value = SimpleOperation;
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("an integer between -2^64 and 2^63 or a string")
}
@@ -151,7 +155,7 @@ impl<'de> Deserialize<'de> for SimpleOperation {
E: de::Error,
{
Ok(SimpleOperation::Equal {
- length: value as usize,
+ length: usize::try_from(value).unwrap_or(usize::MAX),
})
}
@@ -160,7 +164,7 @@ impl<'de> Deserialize<'de> for SimpleOperation {
E: de::Error,
{
Ok(SimpleOperation::Delete {
- length: (-value) as usize,
+ length: usize::try_from(-value).unwrap_or(usize::MAX),
})
}
diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs
index d77974a..34110d8 100644
--- a/src/utils/string_builder.rs
+++ b/src/utils/string_builder.rs
@@ -1,4 +1,4 @@
-use std::iter::Iterator;
+use std::{fmt, iter::Iterator};
/// A helper for building a string in-order based on an original string and a
/// series of insertions, deletions, and copies applied to it. It is safe to use
@@ -12,6 +12,18 @@ pub struct StringBuilder<'a> {
remaining: String,
}
+impl fmt::Debug for StringBuilder<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut debug_struct = f.debug_struct("StringBuilder");
+ debug_struct.field("buffer", &self.buffer);
+
+ #[cfg(debug_assertions)]
+ debug_struct.field("remaining", &self.remaining);
+
+ debug_struct.finish_non_exhaustive()
+ }
+}
+
impl StringBuilder<'_> {
pub fn new(original: &str) -> StringBuilder<'_> {
StringBuilder {
From 1b46e5d2370c8abfca30d3d1019c6bea00f3a8f3 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 21:44:43 +0000
Subject: [PATCH 029/107] Expose to JS
---
Cargo.lock | 38 ++++++++++++++++++---
Cargo.toml | 5 +--
reconcile-js/src/index.ts | 21 ++++++++++++
src/lib.rs | 2 +-
src/operation_transformation/edited_text.rs | 6 ++--
src/operation_transformation/transport.rs | 2 +-
src/wasm.rs | 19 +++++++++++
tests/test.rs | 9 +++--
tests/wasm.rs | 10 +++++-
9 files changed, 95 insertions(+), 17 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bf51cb7..83a551f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -124,6 +124,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
[[package]]
name = "memory_units"
version = "0.4.0"
@@ -182,6 +188,7 @@ dependencies = [
"insta",
"pretty_assertions",
"serde",
+ "serde_json",
"serde_yaml",
"test-case",
"wasm-bindgen",
@@ -212,24 +219,47 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.219"
+version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
diff --git a/Cargo.toml b/Cargo.toml
index 50a861f..0d625a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,6 +24,7 @@ path = "examples/merge-file.rs"
serde = { version = "1.0.219", optional = true, features = ["derive"] }
wasm-bindgen = { version = "0.2.99", optional = true }
+serde_json = { version = "1.0.145", optional = true }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
@@ -36,9 +37,9 @@ wee_alloc = { version = "0.4.2", optional = true }
[features]
default = []
serde = [ "dep:serde" ]
-wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
+wasm = [ "dep:wasm-bindgen", "dep:wee_alloc", "dep:serde_json", "serde" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
-all = [ "serde", "wasm", "console_error_panic_hook" ]
+all = [ "wasm", "console_error_panic_hook" ]
[dev-dependencies]
insta = "1.42.2"
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index efc18ae..3b267ce 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -5,6 +5,7 @@ import {
SpanWithHistory as wasmSpanWithHistory,
reconcileWithHistory as wasmReconcileWithHistory,
isBinary as wasmIsBinary,
+ getCompactDiff as wasmGetCompactDiff,
initSync,
} from 'reconcile-text';
@@ -179,6 +180,26 @@ export function reconcile(
return jsResult;
}
+export function getCompactDiff(
+ original: string,
+ changed: string | TextWithOptionalCursors,
+ tokenizer: BuiltinTokenizer = 'Word'
+): string {
+ init();
+
+ if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
+ throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
+ }
+
+ const changedWasm = toWasmTextWithCursors(changed);
+
+ const result = wasmGetCompactDiff(original, changedWasm, tokenizer);
+
+ changedWasm.free();
+
+ return result;
+}
+
/**
* Merges three versions of text and returns detailed provenance information.
*
diff --git a/src/lib.rs b/src/lib.rs
index cbe354f..1dd78ff 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -170,7 +170,7 @@
//! &changes.into()
//! );
//!
-//! let serialized = serde_yaml::to_string(&result.serialise_as_change_set()).unwrap();
+//! let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap();
//! assert_eq!(
//! serialized,
//! concat!(
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 8e1ddfe..a6465fe 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -350,7 +350,7 @@ where
/// This is useful for sending changes over the network if there's
/// a clear consensus on the original text.
#[must_use]
- pub fn serialise_as_change_set(&self) -> ChangeSet {
+ pub fn to_change_set(&self) -> ChangeSet {
ChangeSet::new(
SimpleOperation::from_operations(&self.operations),
self.cursors.clone(),
@@ -428,7 +428,7 @@ mod tests {
let original = "Merging text is hard!";
let changes = "Merging text is easy with reconcile!";
let result = EditedText::from_strings(original, &changes.into());
- let serialized = serde_yaml::to_string(&result.serialise_as_change_set()).unwrap();
+ let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap();
let expected = concat!(
"operations:\n",
@@ -448,7 +448,7 @@ mod tests {
let edited_text = EditedText::from_strings(original, &updated.into());
- let change_set = edited_text.serialise_as_change_set();
+ let change_set = edited_text.to_change_set();
let deserialized_edited_text =
EditedText::from_change_set(original, change_set, &*BuiltinTokenizer::Word);
diff --git a/src/operation_transformation/transport.rs b/src/operation_transformation/transport.rs
index eef44fd..97212f9 100644
--- a/src/operation_transformation/transport.rs
+++ b/src/operation_transformation/transport.rs
@@ -147,7 +147,7 @@ impl<'de> Deserialize<'de> for SimpleOperation {
type Value = SimpleOperation;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str("an integer between -2^64 and 2^63 or a string")
+ formatter.write_str("an integer between -2^63 and 2^64-1 or a string")
}
fn visit_u64(self, value: u64) -> Result
diff --git a/src/wasm.rs b/src/wasm.rs
index 8cf080a..0fd0aca 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -87,6 +87,25 @@ pub fn generic_reconcile(
}
}
+/// WASM wrapper around getting a compact diff representation as a JSON string
+///
+/// # Panics
+///
+/// If serialization to JSON fails which should not happen
+#[wasm_bindgen(js_name = getCompactDiff)]
+#[must_use]
+pub fn get_compact_diff(
+ parent: &str,
+ changed: &TextWithCursors,
+ tokenizer: BuiltinTokenizer,
+) -> String {
+ set_panic_hook();
+ let edited_text = crate::EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
+ let change_set = edited_text.to_change_set();
+
+ serde_json::to_string(&change_set).expect("Failed to serialize change set")
+}
+
/// Heuristically determine if the given data is a binary or a text file's
/// content.
#[wasm_bindgen(js_name = isBinary)]
diff --git a/tests/test.rs b/tests/test.rs
index 00f5163..e8fae7d 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -46,12 +46,11 @@ fn test_document_one_way_with_cursors_and_serialisation() {
&*BuiltinTokenizer::Word,
);
- let serialised_left = serde_yaml::from_str(
- &serde_yaml::to_string(&left_operations.serialise_as_change_set()).unwrap(),
- )
- .unwrap();
+ let serialised_left =
+ serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_change_set()).unwrap())
+ .unwrap();
let serialised_right = serde_yaml::from_str(
- &serde_yaml::to_string(&right_operations.serialise_as_change_set()).unwrap(),
+ &serde_yaml::to_string(&right_operations.to_change_set()).unwrap(),
)
.unwrap();
diff --git a/tests/wasm.rs b/tests/wasm.rs
index 5ec5f35..6a9d556 100644
--- a/tests/wasm.rs
+++ b/tests/wasm.rs
@@ -46,7 +46,7 @@ fn test_merge_text_with_cursors() {
}
#[wasm_bindgen_test(unsupported = test)]
-fn merge_binary() {
+fn test_merge_binary() {
let left = [0, 1, 2];
let right = [3, 4, 5];
assert_eq!(
@@ -62,6 +62,14 @@ fn test_is_binary() {
assert!(!is_binary(b"hello"));
}
+#[wasm_bindgen_test(unsupported = test)]
+fn test_get_compact_diff() {
+ let parent = "hello ";
+ let changed = "world";
+ let result = get_compact_diff(parent, &changed.into(), BuiltinTokenizer::Word);
+ assert_eq!(result, "{\"operations\":[-6,\"world\"],\"cursors\":[]}");
+}
+
#[wasm_bindgen_test(unsupported = test)]
fn test_is_binary_empty() {
assert!(!is_binary(b""));
From 3d49eb88590c0ecfee40324014b4b37d538dec09 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 26 Oct 2025 22:05:52 +0000
Subject: [PATCH 030/107] Add link
---
examples/website/src/index.html | 1 +
1 file changed, 1 insertion(+)
diff --git a/examples/website/src/index.html b/examples/website/src/index.html
index b64c864..0d5d71f 100644
--- a/examples/website/src/index.html
+++ b/examples/website/src/index.html
@@ -23,6 +23,7 @@
reconcile-text: conflict-free 3-way text merging
+
From a1a339b23da9c68f6cedff4cbb39999723d43b2f Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 06:59:08 +0000
Subject: [PATCH 031/107] Improve formatting
---
reconcile-js/src/index.ts | 14 ++++++++++++++
src/operation_transformation/transport.rs | 8 +++++---
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index 3b267ce..247db26 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -180,6 +180,20 @@ export function reconcile(
return jsResult;
}
+/**
+ * Generates a compact diff representation between an original and changed text.
+ *
+ * These can be parsed and unpacked using Rust crate's EditedText::from_change_set.
+ *
+ * This function computes the differences between two versions of text and returns
+ * a compact string representation of those changes. The returned format is
+ * serialised JSON.
+ *
+ * @param original - The original/base version of the text
+ * @param changed - The modified version of the text (either string or TextWithCursors with cursor positions)
+ * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
+ * @returns A compact string representation of the diff between original and changed text
+ */
export function getCompactDiff(
original: string,
changed: string | TextWithOptionalCursors,
diff --git a/src/operation_transformation/transport.rs b/src/operation_transformation/transport.rs
index 97212f9..67c25e5 100644
--- a/src/operation_transformation/transport.rs
+++ b/src/operation_transformation/transport.rs
@@ -12,9 +12,7 @@ use crate::{CursorPosition, Tokenizer, operation_transformation::Operation};
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum SimpleOperation {
Equal { length: usize },
-
Insert { text: String },
-
Delete { length: usize },
}
@@ -35,6 +33,7 @@ impl SimpleOperation {
previous_equal = Some(*length);
}
}
+
Operation::Insert { text, .. } => {
if let Some(prev_length) = previous_equal {
result.push(SimpleOperation::Equal {
@@ -49,6 +48,7 @@ impl SimpleOperation {
.collect();
result.push(SimpleOperation::Insert { text });
}
+
Operation::Delete {
deleted_character_count,
..
@@ -76,6 +76,7 @@ impl SimpleOperation {
result
}
+ // This is similar to `crate::operation_transformation::utils::cook_operations`
pub fn to_operations(
simple_operations: Vec,
original_text: &str,
@@ -85,7 +86,6 @@ impl SimpleOperation {
T: PartialEq + Clone + Debug,
{
let mut operations: Vec> = Vec::with_capacity(simple_operations.len());
-
let mut order = 0;
for simple_operation in simple_operations {
@@ -101,10 +101,12 @@ impl SimpleOperation {
order += token.get_original_length();
}
}
+
SimpleOperation::Insert { text } => {
let tokens = tokenizer(&text);
operations.push(Operation::create_insert(order, tokens));
}
+
SimpleOperation::Delete { length } => {
operations.push(Operation::create_delete(order, length));
order += length;
From 24666f34356dc7d20266c17774014c23acf1c1ba Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 07:00:35 +0000
Subject: [PATCH 032/107] Bump versions to 0.6.0
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
reconcile-js/package-lock.json | 4 ++--
reconcile-js/package.json | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 83a551f..fcddb18 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,7 +182,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index 0d625a1..38bbf4c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.5.0"
+version = "0.6.0"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index b213742..35ab9a0 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.5.0",
+ "version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.5.0",
+ "version": "0.6.0",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index 9614238..eede8ff 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.5.0",
+ "version": "0.6.0",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From 9e8c5ef52407a12799c39447f92d184e422a1a48 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 07:02:50 +0000
Subject: [PATCH 033/107] Improve versioning script
---
reconcile-js/package-lock.json | 2 +-
scripts/bump-version.sh | 3 +++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 35ab9a0..6aa36f4 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.5.0",
+ "version": "0.6.0",
"dev": true,
"license": "MIT"
},
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
index 0ab7b7e..6b49fb2 100755
--- a/scripts/bump-version.sh
+++ b/scripts/bump-version.sh
@@ -2,6 +2,8 @@
set -e
+git pull --rebase
+
if [[ -z $1 ]]; then
echo "Usage: $0 {patch|minor|major}"
exit 1
@@ -29,6 +31,7 @@ wasm-pack build --target web --features wasm
cd reconcile-js
npm version $1
+npm install
cd -
From feed8826351f03f263133978afdb5e8fed11258e Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 07:17:17 +0000
Subject: [PATCH 034/107] Update link
---
examples/website/src/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/website/src/index.html b/examples/website/src/index.html
index 0d5d71f..dee8c53 100644
--- a/examples/website/src/index.html
+++ b/examples/website/src/index.html
@@ -23,7 +23,7 @@
reconcile-text: conflict-free 3-way text merging
-
+
From 7d060f22ca910774b6b791bdc94b6042e68f03f5 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 07:18:50 +0000
Subject: [PATCH 035/107] Improve dev scripts
---
examples/website/package-lock.json | 2 +-
scripts/bump-version.sh | 5 ++++-
scripts/dev-website.sh | 4 ++++
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index ca35520..9443773 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -28,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.4.10",
+ "version": "0.6.0",
"dev": true,
"license": "MIT",
"devDependencies": {
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
index 6b49fb2..2803cbd 100755
--- a/scripts/bump-version.sh
+++ b/scripts/bump-version.sh
@@ -33,7 +33,10 @@ cd reconcile-js
npm version $1
npm install
-cd -
+cd ../examples/website
+npm install
+
+cd ../..
git add .
TAG=$(node -p "require('./reconcile-js/package.json').version")
diff --git a/scripts/dev-website.sh b/scripts/dev-website.sh
index 572bdf4..bff09f6 100755
--- a/scripts/dev-website.sh
+++ b/scripts/dev-website.sh
@@ -3,7 +3,11 @@
set -e
wasm-pack build --target web --features wasm
+
cd reconcile-js
+npm install
npm run build
+
cd ../examples/website
+npm install
npm run start
From 6191d1adb31bc7802c6d82cb9717aa28242083af Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Mon, 27 Oct 2025 07:22:28 +0000
Subject: [PATCH 036/107] Bump versions to 0.6.1
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
examples/website/package-lock.json | 2 +-
reconcile-js/package-lock.json | 6 +++---
reconcile-js/package.json | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 4039507..b739f33 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,7 +182,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.6.0"
+version = "0.6.1"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index 8c9d6df..f960633 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.6.0"
+version = "0.6.1"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index 9443773..7393047 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -28,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.6.0",
+ "version": "0.6.1",
"dev": true,
"license": "MIT",
"devDependencies": {
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 6aa36f4..573aba2 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.6.0",
+ "version": "0.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.6.0",
+ "version": "0.6.1",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.6.0",
+ "version": "0.6.1",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index eede8ff..c3ecadd 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.6.0",
+ "version": "0.6.1",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From e85eb485e8898ab4908c45aa9ac7c11cc827de46 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 16 Nov 2025 15:43:19 +0000
Subject: [PATCH 037/107] Improve compact diff API (#24)
* Remove is_binary from API
* Format
* Rename file
* Test with more feature combinations
* Don't depend on serde for wasm
* Fix lint & tests
* Don't unwrap to MAX number
* Expose undiff to JS
* Add undiff tests
* Lint
* Change name
---
Cargo.lock | 20 --
Cargo.toml | 5 +-
examples/website/src/index.html | 7 +-
reconcile-js/package-lock.json | 14 +-
reconcile-js/src/index.test.ts | 39 +++-
reconcile-js/src/index.ts | 56 +++---
reconcile-js/tsconfig.json | 3 +-
scripts/test.sh | 7 +-
src/lib.rs | 25 +--
src/operation_transformation.rs | 2 -
src/operation_transformation/edited_text.rs | 188 +++++++++++++-----
src/operation_transformation/transport.rs | 204 --------------------
src/types.rs | 1 +
src/types/number_or_string.rs | 74 +++++++
src/utils.rs | 1 -
src/utils/is_binary.rs | 26 ---
src/utils/myers_diff.rs | 22 ++-
src/wasm.rs | 109 +++++++----
tests/test.rs | 27 ++-
tests/wasm.rs | 24 +--
20 files changed, 430 insertions(+), 424 deletions(-)
delete mode 100644 src/operation_transformation/transport.rs
create mode 100644 src/types/number_or_string.rs
delete mode 100644 src/utils/is_binary.rs
diff --git a/Cargo.lock b/Cargo.lock
index b739f33..5e187e7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -124,12 +124,6 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
-[[package]]
-name = "memchr"
-version = "2.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
-
[[package]]
name = "memory_units"
version = "0.4.0"
@@ -188,7 +182,6 @@ dependencies = [
"insta",
"pretty_assertions",
"serde",
- "serde_json",
"serde_yaml",
"test-case",
"wasm-bindgen",
@@ -247,19 +240,6 @@ dependencies = [
"syn",
]
-[[package]]
-name = "serde_json"
-version = "1.0.145"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
-dependencies = [
- "itoa",
- "memchr",
- "ryu",
- "serde",
- "serde_core",
-]
-
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
diff --git a/Cargo.toml b/Cargo.toml
index f960633..74820a7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,6 @@ path = "examples/merge-file.rs"
serde = { version = "1.0.219", optional = true, features = ["derive"] }
wasm-bindgen = { version = "0.2.99", optional = true }
-serde_json = { version = "1.0.145", optional = true }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
@@ -37,9 +36,9 @@ wee_alloc = { version = "0.4.2", optional = true }
[features]
default = []
serde = [ "dep:serde" ]
-wasm = [ "dep:wasm-bindgen", "dep:wee_alloc", "dep:serde_json", "serde" ]
+wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
-all = [ "wasm", "console_error_panic_hook" ]
+all = [ "wasm", "console_error_panic_hook", "serde" ]
[dev-dependencies]
insta = "1.43.2"
diff --git a/examples/website/src/index.html b/examples/website/src/index.html
index dee8c53..71d5cbc 100644
--- a/examples/website/src/index.html
+++ b/examples/website/src/index.html
@@ -23,7 +23,12 @@
reconcile-text: conflict-free 3-way text merging
-
+
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 573aba2..38dcb9c 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1231,13 +1231,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.0.10",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz",
- "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==",
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.8.0"
+ "undici-types": "~7.16.0"
}
},
"node_modules/@types/stack-utils": {
@@ -5274,9 +5274,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
- "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/src/index.test.ts b/reconcile-js/src/index.test.ts
index fa1e41d..1a4394f 100644
--- a/reconcile-js/src/index.test.ts
+++ b/reconcile-js/src/index.test.ts
@@ -1,4 +1,9 @@
-import { reconcile, reconcileWithHistory } from './index';
+import { reconcile, reconcileWithHistory, diff, undiff } from './index';
+import * as fs from 'fs';
+import * as path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe('reconcile', () => {
it('call reconcile without cursors', () => {
@@ -44,3 +49,35 @@ describe('reconcile', () => {
expect(result.history.length).toBeGreaterThan(0);
});
});
+
+describe('test_diff_and_undiff_are_inverse', () => {
+ const resourcesPath = path.join(__dirname, '../../tests/resources');
+
+ const readFileSlice = (fileName: string, start: number, end: number): string => {
+ const filePath = path.join(resourcesPath, fileName);
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const chars = Array.from(content); // Handle unicode properly
+ return chars.slice(start, Math.min(end, chars.length)).join('');
+ };
+
+ const files = ['pride_and_prejudice.txt', 'room_with_a_view.txt', 'blns.txt'];
+
+ const ranges = [{ start: 0, end: 50000 }];
+
+ files.forEach((file1) => {
+ files.forEach((file2) => {
+ ranges.forEach((range1) => {
+ ranges.forEach((range2) => {
+ it(`should diff & undiff ${file1}[${range1.start}..${range1.end}], ${file2}[${range2.start}..${range2.end}] without panic`, () => {
+ const content1 = readFileSlice(file1, range1.start, range1.end);
+ const content2 = readFileSlice(file2, range2.start, range2.end);
+
+ const changes = diff(content1, content2);
+ const actual = undiff(content1, changes);
+ expect(actual).toEqual(content2);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index 247db26..be7ea8f 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -4,8 +4,8 @@ import {
TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory,
reconcileWithHistory as wasmReconcileWithHistory,
- isBinary as wasmIsBinary,
- getCompactDiff as wasmGetCompactDiff,
+ diff as wasmDiff,
+ undiff as wasmUndiff,
initSync,
} from 'reconcile-text';
@@ -183,22 +183,22 @@ export function reconcile(
/**
* Generates a compact diff representation between an original and changed text.
*
- * These can be parsed and unpacked using Rust crate's EditedText::from_change_set.
+ * These can be parsed and unpacked using the `undiff` function or the Rust crate's EditedText::from_diff.
+ * Cursor positions are omitted from the diff result.
*
* This function computes the differences between two versions of text and returns
- * a compact string representation of those changes. The returned format is
- * serialised JSON.
+ * a compact representation of those changes.
*
* @param original - The original/base version of the text
* @param changed - The modified version of the text (either string or TextWithCursors with cursor positions)
* @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
- * @returns A compact string representation of the diff between original and changed text
+ * @returns An array representing the compact diff, with inserts as strings and deletes as negative integers.
*/
-export function getCompactDiff(
+export function diff(
original: string,
changed: string | TextWithOptionalCursors,
tokenizer: BuiltinTokenizer = 'Word'
-): string {
+): Array {
init();
if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
@@ -207,13 +207,38 @@ export function getCompactDiff(
const changedWasm = toWasmTextWithCursors(changed);
- const result = wasmGetCompactDiff(original, changedWasm, tokenizer);
+ const result = wasmDiff(original, changedWasm, tokenizer);
changedWasm.free();
return result;
}
+/**
+ * Applies a compact diff to an original text to reconstruct the changed version.
+ *
+ * This function takes an original text and a compact diff representation (as produced
+ * by the `diff` function) and reconstructs the modified text.
+ *
+ * @param original - The original/base version of the text
+ * @param diff - The compact diff array representing changes (inserts as strings, deletes as negative integers)
+ * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`.
+ * @returns The reconstructed changed text as a string.
+ */
+export function undiff(
+ original: string,
+ diff: Array,
+ tokenizer: BuiltinTokenizer = 'Word'
+): string {
+ init();
+
+ if (!BUILTIN_TOKENIZERS.includes(tokenizer)) {
+ throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
+ }
+
+ return wasmUndiff(original, diff, tokenizer);
+}
+
/**
* Merges three versions of text and returns detailed provenance information.
*
@@ -272,19 +297,6 @@ export function reconcileWithHistory(
};
}
-/**
- * Check (using heuristics) if the given data is binary or text content.
- *
- * Only text inputs can be reconciled using the library's functions.
- *
- * @param data - The data to check for binary content. This should be a Uint8Array.
- * @returns True if the data is likely binary, false if it is likely text.
- */
-export function isBinary(data: Uint8Array): boolean {
- init();
- return wasmIsBinary(data);
-}
-
function init() {
if (isInitialised) {
return;
diff --git a/reconcile-js/tsconfig.json b/reconcile-js/tsconfig.json
index c462052..08dee60 100644
--- a/reconcile-js/tsconfig.json
+++ b/reconcile-js/tsconfig.json
@@ -9,6 +9,5 @@
"declarationDir": "./dist/types",
"skipLibCheck": true,
"inlineSourceMap": true
- },
- "exclude": ["./dist", "**/*.test.ts"]
+ }
}
diff --git a/scripts/test.sh b/scripts/test.sh
index 7eb816c..d5b1ab4 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -4,7 +4,12 @@ set -e
wasm-pack build --target web --features wasm
cargo test --verbose --features serde -- --include-ignored
-cargo test --features serde,wasm
+
+cargo test
+cargo test --features serde
+cargo test --features wasm
+cargo test --features all
+
wasm-pack test --node --features wasm
cd reconcile-js
diff --git a/src/lib.rs b/src/lib.rs
index 1dd78ff..2119bea 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -157,6 +157,8 @@
//! original text, making the size only depends on the changes made.
//!
//! ```rust
+//! # #[cfg(feature = "serde")]
+//! # {
//! use reconcile_text::{EditedText, BuiltinTokenizer};
//! use serde_yaml;
//! use pretty_assertions::assert_eq;
@@ -170,20 +172,18 @@
//! &changes.into()
//! );
//!
-//! let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap();
+//! let serialized = serde_yaml::to_string(&result.to_diff()).unwrap();
//! assert_eq!(
//! serialized,
//! concat!(
-//! "operations:\n",
//! "- 15\n",
//! "- -6\n",
-//! "- ' easy with reconcile!'\n",
-//! "cursors: []\n"
+//! "- ' easy with reconcile!'\n"
//! )
//! );
//!
//! let deserialized = serde_yaml::from_str(&serialized).unwrap();
-//! let reconstructed = EditedText::from_change_set(
+//! let reconstructed = EditedText::from_diff(
//! original,
//! deserialized,
//! &*BuiltinTokenizer::Word
@@ -192,13 +192,17 @@
//! reconstructed.apply().text(),
//! "Merging text is easy with reconcile!"
//! );
+//! # }
//! ```
//!
//! ## Error handling
//!
//! The library is designed to be robust and will always produce a result, even
-//! in edge cases. However, be aware that extremely large diffs may have
-//! performance implications.
+//! for edge cases.
+//!
+//! ## Performance
+//!
+//! Be aware that extremely large diffs may have performance implications.
//!
//! ## Algorithm overview
//!
@@ -211,13 +215,12 @@ mod tokenizer;
mod types;
mod utils;
-pub use operation_transformation::{ChangeSet, EditedText, reconcile};
+pub use operation_transformation::{EditedText, reconcile};
pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token};
pub use types::{
- cursor_position::CursorPosition, history::History, side::Side,
- span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
+ cursor_position::CursorPosition, history::History, number_or_string::NumberOrString,
+ side::Side, span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
};
-pub use utils::is_binary::is_binary;
#[cfg(feature = "wasm")]
pub mod wasm;
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index e1f173a..85e3995 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -1,12 +1,10 @@
mod edited_text;
mod operation;
-mod transport;
mod utils;
use std::fmt::Debug;
pub use edited_text::EditedText;
pub use operation::Operation;
-pub use transport::ChangeSet;
use crate::{Tokenizer, types::text_with_cursors::TextWithCursors};
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index a6465fe..f27fea4 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -4,15 +4,17 @@ use std::{fmt::Debug, vec};
use serde::{Deserialize, Serialize};
use crate::{
- BuiltinTokenizer, ChangeSet, CursorPosition, TextWithCursors,
+ BuiltinTokenizer, CursorPosition, TextWithCursors,
operation_transformation::{
Operation,
- transport::SimpleOperation,
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
},
raw_operation::RawOperation,
tokenizer::Tokenizer,
- types::{history::History, side::Side, span_with_history::SpanWithHistory},
+ types::{
+ history::History, number_or_string::NumberOrString, side::Side,
+ span_with_history::SpanWithHistory,
+ },
utils::string_builder::StringBuilder,
};
@@ -105,6 +107,11 @@ where
/// from the same original text. The operations are merged using the
/// principles of Operational Transformation. The cursors are updated
/// accordingly to reflect the changes made by the merged operations.
+ ///
+ /// # Panics
+ ///
+ /// Panics if there's an integer overflow (in i64) when calculating new
+ /// cursor positions.
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn merge(self, other: Self) -> Self {
@@ -166,13 +173,14 @@ where
let result = operation.merge_operations(&mut last_other_op);
if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result {
- let merged_length_signed =
- isize::try_from(merged_length).unwrap_or(isize::MAX);
- let seen_left_length_signed =
- isize::try_from(seen_left_length).unwrap_or(isize::MAX);
- let op_len_signed = isize::try_from(op.len()).unwrap_or(isize::MAX);
- let original_length_signed =
- isize::try_from(original_length).unwrap_or(isize::MAX);
+ let merged_length_signed = isize::try_from(merged_length)
+ .expect("merged_length must fit in isize");
+ let seen_left_length_signed = isize::try_from(seen_left_length)
+ .expect("seen_left_length must fit in isize");
+ let op_len_signed =
+ isize::try_from(op.len()).expect("op.len() must fit in isize");
+ let original_length_signed = isize::try_from(original_length)
+ .expect("original_length must fit in isize");
let shift = merged_length_signed - seen_left_length_signed + op_len_signed
- original_length_signed;
@@ -199,13 +207,14 @@ where
let result = operation.merge_operations(&mut last_other_op);
if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result {
- let merged_length_signed =
- isize::try_from(merged_length).unwrap_or(isize::MAX);
- let seen_right_length_signed =
- isize::try_from(seen_right_length).unwrap_or(isize::MAX);
- let op_len_signed = isize::try_from(op.len()).unwrap_or(isize::MAX);
- let original_length_signed =
- isize::try_from(original_length).unwrap_or(isize::MAX);
+ let merged_length_signed = isize::try_from(merged_length)
+ .expect("merged_length must fit in isize");
+ let seen_right_length_signed = isize::try_from(seen_right_length)
+ .expect("seen_right_length must fit in isize");
+ let op_len_signed =
+ isize::try_from(op.len()).expect("op.len() must fit in isize");
+ let original_length_signed = isize::try_from(original_length)
+ .expect("original_length must fit in isize");
let shift = merged_length_signed - seen_right_length_signed + op_len_signed
- original_length_signed;
@@ -345,34 +354,122 @@ where
history
}
- /// Serialize the `EditedText` as a `ChangeSet`, which contains only
- /// the operations and cursor positions, but without the original text.
- /// This is useful for sending changes over the network if there's
- /// a clear consensus on the original text.
+ /// Convert the `EditedText` into a terse representation ready for
+ /// serialization. The result omits cursor positions and the original text.
+ /// This is useful for sending text diffs over the network if there's a
+ /// clear consensus on the original text.
+ ///
+ /// Inserts are represented as strings, deletes as negative integers,
+ /// and equal spans as positive integers.
+ ///
+ /// # Panics
+ ///
+ /// Panics if there's an integer overflow in i64.
#[must_use]
- pub fn to_change_set(&self) -> ChangeSet {
- ChangeSet::new(
- SimpleOperation::from_operations(&self.operations),
- self.cursors.clone(),
- )
+ pub fn to_diff(&self) -> Vec {
+ let mut result: Vec = Vec::with_capacity(self.operations.len());
+ let mut previous_equal: Option = None;
+
+ for operation in &self.operations {
+ match operation {
+ Operation::Equal { length, .. } => {
+ if let Some(prev_length) = previous_equal {
+ previous_equal = Some(prev_length + *length);
+ } else {
+ previous_equal = Some(*length);
+ }
+ }
+
+ Operation::Insert { text, .. } => {
+ if let Some(prev_length) = previous_equal {
+ result.push(NumberOrString::Number(
+ i64::try_from(prev_length).expect("prev_length must fit in i64"),
+ ));
+ previous_equal = None;
+ }
+
+ let text: String = text
+ .iter()
+ .map(super::super::tokenizer::token::Token::original)
+ .collect();
+ result.push(NumberOrString::Text(text));
+ }
+
+ Operation::Delete {
+ deleted_character_count,
+ ..
+ } => {
+ if let Some(prev_length) = previous_equal {
+ result.push(NumberOrString::Number(
+ i64::try_from(prev_length).expect("prev_length must fit in i64"),
+ ));
+ previous_equal = None;
+ }
+
+ let count = i64::try_from(*deleted_character_count)
+ .expect("deleted_character_count must fit in i64");
+ result.push(NumberOrString::Number(-count));
+ }
+ }
+ }
+
+ if let Some(prev_length) = previous_equal {
+ result.push(NumberOrString::Number(
+ i64::try_from(prev_length).expect("prev_length must fit in i64"),
+ ));
+ }
+
+ result
}
- /// Deserialize an `EditedText` from a `ChangeSet` and the original text.
- /// This is useful for reconstructing the `EditedText` on the receiving
- /// end after sending only the `ChangeSet` over the network.
+ /// Deserialize an `EditedText` from a change list and the original text.
+ ///
+ /// # Panics
+ ///
+ /// Panics if there's an integer overflow in i64.
#[must_use]
- pub fn from_change_set(
- text: &'a str,
- change_set: ChangeSet,
+ pub fn from_diff(
+ original_text: &'a str,
+ diff: Vec,
tokenizer: &Tokenizer,
) -> EditedText<'a, T> {
- let operations = SimpleOperation::to_operations(change_set.operations, text, tokenizer);
+ let mut operations: Vec> = Vec::with_capacity(diff.len());
+ let mut order = 0;
+
+ for item in diff {
+ match item {
+ NumberOrString::Number(length) => {
+ if length >= 0 {
+ let length = usize::try_from(length).expect("length must fit in usize");
+ let original_characters: String =
+ original_text.chars().skip(order).take(length).collect();
+
+ let original_tokens = tokenizer(&original_characters);
+ for token in original_tokens {
+ operations
+ .push(Operation::create_equal(order, token.get_original_length()));
+ order += token.get_original_length();
+ }
+ } else {
+ let length =
+ usize::try_from(-length).expect("negative length must fit in usize");
+ operations.push(Operation::create_delete(order, length));
+ order += length;
+ }
+ }
+ NumberOrString::Text(text) => {
+ let tokens = tokenizer(&text);
+ operations.push(Operation::create_insert(order, tokens));
+ }
+ }
+ }
+
let operation_count = operations.len();
EditedText::new(
- text,
+ original_text,
operations,
vec![Side::Left; operation_count],
- change_set.cursors,
+ vec![],
)
}
}
@@ -423,34 +520,29 @@ mod tests {
assert_eq!(operations.apply().text(), expected);
}
+ #[cfg(feature = "serde")]
#[test]
- fn test_change_set_deserialisation() {
+ fn test_changes_deserialisation() {
let original = "Merging text is hard!";
let changes = "Merging text is easy with reconcile!";
let result = EditedText::from_strings(original, &changes.into());
- let serialized = serde_yaml::to_string(&result.to_change_set()).unwrap();
-
- let expected = concat!(
- "operations:\n",
- "- 15\n",
- "- -6\n",
- "- ' easy with reconcile!'\n",
- "cursors: []\n"
- );
+ let serialized = serde_yaml::to_string(&result.to_diff()).unwrap();
+ let expected = concat!("- 15\n", "- -6\n", "- ' easy with reconcile!'\n",);
assert_eq!(serialized, expected);
}
+ #[cfg(feature = "serde")]
#[test]
- fn test_change_set_serialization() {
+ fn test_changes_serialization() {
let original = "The quick brown fox jumps over the lazy dog.";
let updated = "The quick red fox jumped over the very lazy dog!";
let edited_text = EditedText::from_strings(original, &updated.into());
- let change_set = edited_text.to_change_set();
+ let changes = edited_text.to_diff();
let deserialized_edited_text =
- EditedText::from_change_set(original, change_set, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word);
assert_eq!(deserialized_edited_text.apply().text(), updated);
}
diff --git a/src/operation_transformation/transport.rs b/src/operation_transformation/transport.rs
deleted file mode 100644
index 67c25e5..0000000
--- a/src/operation_transformation/transport.rs
+++ /dev/null
@@ -1,204 +0,0 @@
-use std::fmt::Debug;
-
-#[cfg(feature = "serde")]
-use serde::{
- Deserialize, Serialize,
- de::{self, Deserializer, Visitor},
- ser::Serializer,
-};
-
-use crate::{CursorPosition, Tokenizer, operation_transformation::Operation};
-
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub enum SimpleOperation {
- Equal { length: usize },
- Insert { text: String },
- Delete { length: usize },
-}
-
-impl SimpleOperation {
- pub fn from_operations(operation: &Vec>) -> Vec
- where
- T: PartialEq + Clone + Debug,
- {
- let mut result: Vec = Vec::with_capacity(operation.len());
- let mut previous_equal: Option = None;
-
- for operation in operation {
- match operation {
- Operation::Equal { length, .. } => {
- if let Some(prev_length) = previous_equal {
- previous_equal = Some(prev_length + *length);
- } else {
- previous_equal = Some(*length);
- }
- }
-
- Operation::Insert { text, .. } => {
- if let Some(prev_length) = previous_equal {
- result.push(SimpleOperation::Equal {
- length: prev_length,
- });
- previous_equal = None;
- }
-
- let text: String = text
- .iter()
- .map(super::super::tokenizer::token::Token::original)
- .collect();
- result.push(SimpleOperation::Insert { text });
- }
-
- Operation::Delete {
- deleted_character_count,
- ..
- } => {
- if let Some(prev_length) = previous_equal {
- result.push(SimpleOperation::Equal {
- length: prev_length,
- });
- previous_equal = None;
- }
-
- result.push(SimpleOperation::Delete {
- length: *deleted_character_count,
- });
- }
- }
- }
-
- if let Some(prev_length) = previous_equal {
- result.push(SimpleOperation::Equal {
- length: prev_length,
- });
- }
-
- result
- }
-
- // This is similar to `crate::operation_transformation::utils::cook_operations`
- pub fn to_operations(
- simple_operations: Vec,
- original_text: &str,
- tokenizer: &Tokenizer,
- ) -> Vec>
- where
- T: PartialEq + Clone + Debug,
- {
- let mut operations: Vec> = Vec::with_capacity(simple_operations.len());
- let mut order = 0;
-
- for simple_operation in simple_operations {
- match simple_operation {
- SimpleOperation::Equal { length } => {
- let original_characters: String =
- original_text.chars().skip(order).take(length).collect();
-
- let original_tokens = tokenizer(&original_characters);
- for token in original_tokens {
- operations
- .push(Operation::create_equal(order, token.get_original_length()));
- order += token.get_original_length();
- }
- }
-
- SimpleOperation::Insert { text } => {
- let tokens = tokenizer(&text);
- operations.push(Operation::create_insert(order, tokens));
- }
-
- SimpleOperation::Delete { length } => {
- operations.push(Operation::create_delete(order, length));
- order += length;
- }
- }
- }
-
- operations
- }
-}
-
-#[cfg(feature = "serde")]
-impl Serialize for SimpleOperation {
- fn serialize(&self, serializer: S) -> Result
- where
- S: Serializer,
- {
- // neat idea from https://github.com/spebern/operational-transform-rs/blob/9faa17f0a2b282ac2e09dbb2d29fdaf2ae0bbb4a/operational-transform/src/serde.rs#L14
- match self {
- SimpleOperation::Equal { length } => serializer.serialize_u64(*length as u64),
- SimpleOperation::Insert { text } => serializer.serialize_str(text),
- SimpleOperation::Delete { length } => {
- serializer.serialize_i64(-(i64::try_from(*length).unwrap_or(i64::MAX)))
- }
- }
- }
-}
-
-#[cfg(feature = "serde")]
-impl<'de> Deserialize<'de> for SimpleOperation {
- fn deserialize(deserializer: D) -> Result
- where
- D: Deserializer<'de>,
- {
- use std::fmt;
-
- struct OperationVisitor;
-
- impl Visitor<'_> for OperationVisitor {
- type Value = SimpleOperation;
-
- fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str("an integer between -2^63 and 2^64-1 or a string")
- }
-
- fn visit_u64(self, value: u64) -> Result
- where
- E: de::Error,
- {
- Ok(SimpleOperation::Equal {
- length: usize::try_from(value).unwrap_or(usize::MAX),
- })
- }
-
- fn visit_i64(self, value: i64) -> Result
- where
- E: de::Error,
- {
- Ok(SimpleOperation::Delete {
- length: usize::try_from(-value).unwrap_or(usize::MAX),
- })
- }
-
- fn visit_str(self, value: &str) -> Result
- where
- E: de::Error,
- {
- Ok(SimpleOperation::Insert {
- text: value.to_owned(),
- })
- }
- }
-
- deserializer.deserialize_any(OperationVisitor)
- }
-}
-
-/// A serializable representation of the changes made to a text document
-/// without the original text.
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-#[derive(Debug, Clone, PartialEq, Default)]
-pub struct ChangeSet {
- pub operations: Vec,
- pub cursors: Vec,
-}
-
-impl ChangeSet {
- #[must_use]
- pub fn new(operations: Vec, cursors: Vec) -> Self {
- Self {
- operations,
- cursors,
- }
- }
-}
diff --git a/src/types.rs b/src/types.rs
index b32ef9a..b5c2f7c 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,5 +1,6 @@
pub mod cursor_position;
pub mod history;
+pub mod number_or_string;
pub mod side;
pub mod span_with_history;
pub mod text_with_cursors;
diff --git a/src/types/number_or_string.rs b/src/types/number_or_string.rs
new file mode 100644
index 0000000..7272a60
--- /dev/null
+++ b/src/types/number_or_string.rs
@@ -0,0 +1,74 @@
+use std::fmt::Debug;
+
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Serialize};
+#[cfg(feature = "wasm")]
+use wasm_bindgen::prelude::*;
+
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(untagged))]
+#[derive(Debug, Clone, PartialEq)]
+pub enum NumberOrString {
+ Number(i64),
+ Text(String),
+}
+
+#[cfg(feature = "wasm")]
+impl TryFrom for NumberOrString {
+ type Error = DeserialisationError;
+
+ fn try_from(value: JsValue) -> Result {
+ if let Ok(num) = value.clone().try_into() {
+ return Ok(NumberOrString::Number(num));
+ }
+
+ if let Ok(text) = value.try_into() {
+ return Ok(NumberOrString::Text(text));
+ }
+
+ Err(DeserialisationError::new(
+ "Could not parse JsValue as either number or string",
+ ))
+ }
+}
+
+#[cfg(feature = "wasm")]
+impl From for JsValue {
+ fn from(value: NumberOrString) -> Self {
+ match value {
+ NumberOrString::Number(num) => JsValue::from(num),
+ NumberOrString::Text(text) => JsValue::from(text),
+ }
+ }
+}
+
+/// Error type for deserialisation failures
+#[cfg(feature = "wasm")]
+#[derive(Debug, Clone)]
+pub struct DeserialisationError {
+ pub message: String,
+}
+
+#[cfg(feature = "wasm")]
+impl DeserialisationError {
+ pub fn new(message: impl Into) -> Self {
+ Self {
+ message: message.into(),
+ }
+ }
+}
+
+#[cfg(feature = "wasm")]
+impl std::fmt::Display for DeserialisationError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "Deserialisation error: {}", self.message)
+ }
+}
+
+#[cfg(feature = "wasm")]
+impl std::error::Error for DeserialisationError {}
+
+#[cfg(feature = "wasm")]
+impl From for JsValue {
+ fn from(error: DeserialisationError) -> Self { JsValue::from_str(&error.message) }
+}
diff --git a/src/utils.rs b/src/utils.rs
index f249825..2e05a70 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,6 +1,5 @@
pub mod common_prefix_len;
pub mod common_suffix_len;
pub mod find_longest_prefix_contained_within;
-pub mod is_binary;
pub mod myers_diff;
pub mod string_builder;
diff --git a/src/utils/is_binary.rs b/src/utils/is_binary.rs
deleted file mode 100644
index 09bfcf9..0000000
--- a/src/utils/is_binary.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-/// Heuristically determine if the given data is a binary or a text file's
-/// content.
-///
-/// Only text inputs can be reconciled using the crate's functions.
-#[must_use]
-pub fn is_binary(data: &[u8]) -> bool {
- if data.contains(&0) {
- // Even though the NUL character is valid in UTF-8, it's highly suspicious in
- // human-readable text.
- return true;
- }
-
- std::str::from_utf8(data).is_err()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_is_binary() {
- assert!(is_binary(&[0, 159, 146, 150]));
- assert!(is_binary(&[0, 12]));
- assert!(!is_binary(b"hello"));
- }
-}
diff --git a/src/utils/myers_diff.rs b/src/utils/myers_diff.rs
index 776e448..c89788d 100644
--- a/src/utils/myers_diff.rs
+++ b/src/utils/myers_diff.rs
@@ -87,7 +87,7 @@ struct V {
impl V {
fn new(max_d: usize) -> Self {
// max_d should fit in isize for the algorithm to work correctly
- let offset = isize::try_from(max_d).unwrap_or(isize::MAX);
+ let offset = isize::try_from(max_d).expect("max_d must fit in isize");
Self {
offset,
v: vec![0; 2 * max_d],
@@ -101,16 +101,15 @@ impl Index for V {
type Output = usize;
fn index(&self, index: isize) -> &Self::Output {
- let idx = usize::try_from(index + self.offset).unwrap_or(usize::MAX);
- &self.v[idx.min(self.v.len().saturating_sub(1))]
+ let idx = usize::try_from(index + self.offset).expect("index + offset must fit in usize");
+ &self.v[idx]
}
}
impl IndexMut for V {
fn index_mut(&mut self, index: isize) -> &mut Self::Output {
- let idx = usize::try_from(index + self.offset).unwrap_or(usize::MAX);
- let len = self.v.len();
- &mut self.v[idx.min(len.saturating_sub(1))]
+ let idx = usize::try_from(index + self.offset).expect("index + offset must fit in usize");
+ &mut self.v[idx]
}
}
@@ -145,7 +144,8 @@ where
// By Lemma 1 in the paper, the optimal edit script length is odd or even as
// `delta` is odd or even.
- let delta = isize::try_from(n).unwrap_or(isize::MAX) - isize::try_from(m).unwrap_or(isize::MAX);
+ let delta = isize::try_from(n).expect("n must fit in isize")
+ - isize::try_from(m).expect("m must fit in isize");
let odd = delta & 1 == 1;
// The initial point at (0, -1)
@@ -157,7 +157,7 @@ where
assert!(vf.len() >= d_max);
assert!(vb.len() >= d_max);
- let d_max_isize = isize::try_from(d_max).unwrap_or(isize::MAX);
+ let d_max_isize = isize::try_from(d_max).expect("d_max must fit in isize");
for d in 0..d_max_isize {
// Forward path
for k in (-d..=d).rev().step_by(2) {
@@ -166,7 +166,8 @@ where
} else {
vf[k - 1] + 1
};
- let y = usize::try_from(isize::try_from(x).unwrap_or(isize::MAX) - k).unwrap_or(0);
+ let y = usize::try_from(isize::try_from(x).expect("x must fit in isize") - k)
+ .expect("x - k must be non-negative and fit in usize");
// The coordinate of the start of a snake
let (x0, y0) = (x, y);
@@ -204,7 +205,8 @@ where
} else {
vb[k - 1] + 1
};
- let mut y = usize::try_from(isize::try_from(x).unwrap_or(isize::MAX) - k).unwrap_or(0);
+ let mut y = usize::try_from(isize::try_from(x).expect("x must fit in isize") - k)
+ .expect("x - k must be non-negative and fit in usize");
// The coordinate of the start of a snake
if x < n && y < m {
diff --git a/src/wasm.rs b/src/wasm.rs
index 0fd0aca..1b7a24b 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -3,7 +3,7 @@ use core::str;
use wasm_bindgen::prelude::*;
-use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors};
+use crate::{BuiltinTokenizer, CursorPosition, EditedText, SpanWithHistory, TextWithCursors};
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT;
@@ -32,6 +32,7 @@ pub fn reconcile_with_history(
tokenizer: BuiltinTokenizer,
) -> TextWithCursorsAndHistory {
set_panic_hook();
+
let reconciled = crate::reconcile(parent, left, right, &*tokenizer);
let text_with_cursors = reconciled.apply();
@@ -54,10 +55,6 @@ pub fn reconcile_with_history(
/// # Returns
///
/// The merged document.
-///
-/// # Panics
-///
-/// If any of the input documents are not valid UTF-8 strings.
#[wasm_bindgen(js_name = genericReconcile)]
#[must_use]
pub fn generic_reconcile(
@@ -68,51 +65,56 @@ pub fn generic_reconcile(
) -> Vec {
set_panic_hook();
- if crate::is_binary(parent) || crate::is_binary(left) || crate::is_binary(right) {
- right.to_vec()
+ if let (Some(parent), Some(left), Some(right)) = (
+ string_or_nothing(parent),
+ string_or_nothing(left),
+ string_or_nothing(right),
+ ) {
+ crate::reconcile(&parent, &left.into(), &right.into(), &*tokenizer)
+ .apply()
+ .text()
+ .into_bytes()
} else {
- crate::reconcile(
- str::from_utf8(parent).expect("parent must be valid UTF-8 because it's not binary"),
- &str::from_utf8(left)
- .expect("left must be valid UTF-8 because it's not binary")
- .into(),
- &str::from_utf8(right)
- .expect("right must be valid UTF-8 because it's not binary")
- .into(),
- &*tokenizer,
- )
- .apply()
- .text()
- .into_bytes()
+ right.to_vec()
}
}
-/// WASM wrapper around getting a compact diff representation as a JSON string
+/// WASM wrapper around getting a compact diff representation of two texts as a
+/// list of numbers and strings.
+#[wasm_bindgen(js_name = diff)]
+#[must_use]
+pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer) -> Vec {
+ set_panic_hook();
+
+ let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
+ edited_text
+ .to_diff()
+ .into_iter()
+ .map(std::convert::Into::into)
+ .collect()
+}
+
+/// Inverse of `diff`, applies a compact diff representation to a parent text
///
/// # Panics
///
-/// If serialization to JSON fails which should not happen
-#[wasm_bindgen(js_name = getCompactDiff)]
+/// Panics if the diff format is invalid or there's an integer overflow when
+/// applying the diff.
+#[wasm_bindgen(js_name = undiff)]
#[must_use]
-pub fn get_compact_diff(
- parent: &str,
- changed: &TextWithCursors,
- tokenizer: BuiltinTokenizer,
-) -> String {
+pub fn undiff(parent: &str, diff: Vec, tokenizer: BuiltinTokenizer) -> String {
set_panic_hook();
- let edited_text = crate::EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer);
- let change_set = edited_text.to_change_set();
- serde_json::to_string(&change_set).expect("Failed to serialize change set")
-}
-
-/// Heuristically determine if the given data is a binary or a text file's
-/// content.
-#[wasm_bindgen(js_name = isBinary)]
-#[must_use]
-pub fn is_binary(data: &[u8]) -> bool {
- set_panic_hook();
- crate::is_binary(data)
+ EditedText::from_diff(
+ parent,
+ diff.into_iter()
+ .map(std::convert::TryInto::try_into)
+ .collect::>()
+ .expect("Invalid diff format"),
+ &*tokenizer,
+ )
+ .apply()
+ .text()
}
fn set_panic_hook() {
@@ -140,3 +142,30 @@ impl TextWithCursorsAndHistory {
#[must_use]
pub fn history(&self) -> Vec { self.history.clone() }
}
+
+/// Returns the UTF8 parsed string if it's a text, or `None` if it's likely
+/// binary.
+#[must_use]
+fn string_or_nothing(data: &[u8]) -> Option {
+ if data.contains(&0) {
+ // Even though the NUL character is valid in UTF-8, it's highly suspicious in
+ // human-readable text.
+ return None;
+ }
+
+ std::str::from_utf8(data)
+ .map(std::borrow::ToOwned::to_owned)
+ .ok()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_string_or_nothing() {
+ assert_eq!(string_or_nothing(&[0, 159, 146, 150]), None);
+ assert_eq!(string_or_nothing(&[0, 12]), None);
+ assert_eq!(string_or_nothing(b"hello"), Some("hello".into()));
+ }
+}
diff --git a/tests/test.rs b/tests/test.rs
index e8fae7d..2b14b86 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -3,7 +3,7 @@ mod example_document;
use std::{fs, path::Path};
use example_document::ExampleDocument;
-use reconcile_text::{BuiltinTokenizer, EditedText, reconcile};
+use reconcile_text::{BuiltinTokenizer, reconcile};
use serde::Deserialize;
#[test]
@@ -34,8 +34,11 @@ fn test_document_one_way_with_cursors() {
}
}
+#[cfg(feature = "serde")]
#[test]
-fn test_document_one_way_with_cursors_and_serialisation() {
+fn test_document_one_way_with_serialisation() {
+ use reconcile_text::EditedText;
+
for doc in &get_all_documents() {
let parent = doc.parent();
let left_operations =
@@ -47,19 +50,23 @@ fn test_document_one_way_with_cursors_and_serialisation() {
);
let serialised_left =
- serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_change_set()).unwrap())
+ serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_diff()).unwrap())
+ .unwrap();
+ let serialised_right =
+ serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_diff()).unwrap())
.unwrap();
- let serialised_right = serde_yaml::from_str(
- &serde_yaml::to_string(&right_operations.to_change_set()).unwrap(),
- )
- .unwrap();
let restored_left_operations =
- EditedText::from_change_set(&parent, serialised_left, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word);
let restored_right_operations =
- EditedText::from_change_set(&parent, serialised_right, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word);
- doc.assert_eq(&restored_left_operations.merge(restored_right_operations));
+ doc.assert_eq_without_cursors(
+ &restored_left_operations
+ .merge(restored_right_operations)
+ .apply()
+ .text(),
+ );
}
}
diff --git a/tests/wasm.rs b/tests/wasm.rs
index 6a9d556..304ee6e 100644
--- a/tests/wasm.rs
+++ b/tests/wasm.rs
@@ -55,22 +55,16 @@ fn test_merge_binary() {
);
}
-#[wasm_bindgen_test(unsupported = test)]
-fn test_is_binary() {
- assert!(is_binary(&[0, 159, 146, 150]));
- assert!(is_binary(&[0, 12]));
- assert!(!is_binary(b"hello"));
-}
-
-#[wasm_bindgen_test(unsupported = test)]
-fn test_get_compact_diff() {
+#[wasm_bindgen_test] // JsValue isn't supported outside of wasm
+fn test_diff() {
let parent = "hello ";
let changed = "world";
- let result = get_compact_diff(parent, &changed.into(), BuiltinTokenizer::Word);
- assert_eq!(result, "{\"operations\":[-6,\"world\"],\"cursors\":[]}");
-}
-#[wasm_bindgen_test(unsupported = test)]
-fn test_is_binary_empty() {
- assert!(!is_binary(b""));
+ let result = diff(parent, &changed.into(), BuiltinTokenizer::Word);
+
+ assert_eq!(result.len(), 2);
+ let first: i64 = result[0].clone().try_into().unwrap();
+ let second: String = result[1].clone().try_into().unwrap();
+ assert_eq!(first, -6);
+ assert_eq!(second, "world");
}
From b7b22a63cd71a5358e7b62b68530976935e6e9e2 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 16 Nov 2025 15:43:56 +0000
Subject: [PATCH 038/107] Bump versions to 0.7.0
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
examples/website/package-lock.json | 2 +-
reconcile-js/package-lock.json | 6 +++---
reconcile-js/package.json | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 5e187e7..1c44740 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -176,7 +176,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.6.1"
+version = "0.7.0"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index 74820a7..4c191b4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.6.1"
+version = "0.7.0"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index 7393047..d81f70e 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -28,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.6.1",
+ "version": "0.7.0",
"dev": true,
"license": "MIT",
"devDependencies": {
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 38dcb9c..0e226ad 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.6.1",
+ "version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.6.1",
+ "version": "0.7.0",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.6.1",
+ "version": "0.7.0",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index c3ecadd..2d4c20b 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.6.1",
+ "version": "0.7.0",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From 2bb647cdac5f5439d24ed581d6b717bf48167582 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 16 Nov 2025 19:52:43 +0000
Subject: [PATCH 039/107] Always return numbers instead of bigint
---
Cargo.lock | 138 +++++++++++++++++-----------------
Cargo.toml | 8 +-
reconcile-js/src/index.ts | 4 +-
scripts/test.sh | 4 +-
src/types/number_or_string.rs | 15 ++++
5 files changed, 93 insertions(+), 76 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 1c44740..a322ccd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10,10 +10,11 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "cc"
-version = "1.2.27"
+version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [
+ "find-msvc-tools",
"shlex",
]
@@ -25,9 +26,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
-version = "1.0.1"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "console"
@@ -38,7 +39,7 @@ dependencies = [
"encode_unicode",
"libc",
"once_cell",
- "windows-sys",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -47,7 +48,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
- "cfg-if 1.0.1",
+ "cfg-if 1.0.4",
"wasm-bindgen",
]
@@ -70,16 +71,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
-name = "hashbrown"
-version = "0.15.4"
+name = "find-msvc-tools"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "indexmap"
-version = "2.10.0"
+version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
@@ -104,9 +111,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
-version = "0.3.81"
+version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
+checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -114,15 +121,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.174"
+version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
-
-[[package]]
-name = "log"
-version = "0.4.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "memory_units"
@@ -158,18 +159,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.40"
+version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -191,9 +192,9 @@ dependencies = [
[[package]]
name = "rustversion"
-version = "1.0.21"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
@@ -267,9 +268,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "syn"
-version = "2.0.104"
+version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -291,7 +292,7 @@ version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f"
dependencies = [
- "cfg-if 1.0.1",
+ "cfg-if 1.0.4",
"proc-macro2",
"quote",
"syn",
@@ -311,9 +312,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
-version = "1.0.18"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unsafe-libyaml"
@@ -333,38 +334,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
+checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
- "cfg-if 1.0.1",
+ "cfg-if 1.0.4",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.54"
+version = "0.4.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
+checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
dependencies = [
- "cfg-if 1.0.1",
+ "cfg-if 1.0.4",
"js-sys",
"once_cell",
"wasm-bindgen",
@@ -373,9 +360,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
+checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -383,31 +370,31 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
+checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
+ "bumpalo",
"proc-macro2",
"quote",
"syn",
- "wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
+checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.54"
+version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e381134e148c1062f965a42ed1f5ee933eef2927c3f70d1812158f711d39865"
+checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373"
dependencies = [
"js-sys",
"minicov",
@@ -418,9 +405,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.54"
+version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b673bca3298fe582aeef8352330ecbad91849f85090805582400850f8270a2e8"
+checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1"
dependencies = [
"proc-macro2",
"quote",
@@ -429,9 +416,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.81"
+version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
+checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -467,11 +454,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
-version = "0.1.9"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -480,6 +467,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -489,6 +482,15 @@ dependencies = [
"windows-targets",
]
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows-targets"
version = "0.52.6"
diff --git a/Cargo.toml b/Cargo.toml
index 4c191b4..eb1366f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,9 +36,9 @@ wee_alloc = { version = "0.4.2", optional = true }
[features]
default = []
serde = [ "dep:serde" ]
-wasm = [ "dep:wasm-bindgen", "dep:wee_alloc" ]
+wasm = [ "dep:wasm-bindgen", "dep:wee_alloc", "console_error_panic_hook" ]
console_error_panic_hook = [ "dep:console_error_panic_hook" ]
-all = [ "wasm", "console_error_panic_hook", "serde" ]
+all = [ "wasm", "serde" ]
[dev-dependencies]
insta = "1.43.2"
@@ -46,7 +46,7 @@ pretty_assertions = "1.4.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
test-case = "3.3.1"
-wasm-bindgen-test = "0.3.54"
+wasm-bindgen-test = "0.3.55"
[profile.release]
codegen-units = 1
@@ -55,7 +55,7 @@ opt-level = 3
strip = "symbols"
[package.metadata.wasm-pack.profile.release]
-wasm-opt = ['-O4', '--enable-bulk-memory']
+wasm-opt = ['-O4', '--enable-bulk-memory', '--enable-nontrapping-float-to-int']
[lints.rust]
unsafe_code = "forbid"
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index be7ea8f..3fa998c 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -211,7 +211,7 @@ export function diff(
changedWasm.free();
- return result;
+ return result.map((item) => (typeof item === 'bigint' ? Number(item) : item));
}
/**
@@ -227,7 +227,7 @@ export function diff(
*/
export function undiff(
original: string,
- diff: Array,
+ diff: Array,
tokenizer: BuiltinTokenizer = 'Word'
): string {
init();
diff --git a/scripts/test.sh b/scripts/test.sh
index d5b1ab4..fb25d3c 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -2,7 +2,7 @@
set -e
-wasm-pack build --target web --features wasm
+wasm-pack build --target web --features wasm,console_error_panic_hook
cargo test --verbose --features serde -- --include-ignored
cargo test
@@ -10,7 +10,7 @@ cargo test --features serde
cargo test --features wasm
cargo test --features all
-wasm-pack test --node --features wasm
+wasm-pack test --node --features wasm,console_error_panic_hook
cd reconcile-js
npm install
diff --git a/src/types/number_or_string.rs b/src/types/number_or_string.rs
index 7272a60..2bb1531 100644
--- a/src/types/number_or_string.rs
+++ b/src/types/number_or_string.rs
@@ -5,6 +5,10 @@ use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
+#[cfg(feature = "wasm")]
+#[allow(clippy::cast_precision_loss)]
+const INTEGRAL_LIMIT: f64 = (1u64 << 53) as f64;
+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[derive(Debug, Clone, PartialEq)]
@@ -22,6 +26,17 @@ impl TryFrom for NumberOrString {
return Ok(NumberOrString::Number(num));
}
+ if let Some(num) = value.clone().as_f64() {
+ if num.abs() > INTEGRAL_LIMIT {
+ return Err(DeserialisationError::new(
+ "Floating-point number exceeds safe integer limit, use BigInt instead",
+ ));
+ }
+
+ #[allow(clippy::cast_possible_truncation)]
+ return Ok(NumberOrString::Number(num.round() as i64));
+ }
+
if let Ok(text) = value.try_into() {
return Ok(NumberOrString::Text(text));
}
From b4774c8cfdce90c61fb867e69d7306e3ce3d0803 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sun, 16 Nov 2025 19:53:14 +0000
Subject: [PATCH 040/107] Bump versions to 0.7.1
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
examples/website/package-lock.json | 2 +-
reconcile-js/package-lock.json | 6 +++---
reconcile-js/package.json | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index a322ccd..bcbefae 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -177,7 +177,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.7.0"
+version = "0.7.1"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index eb1366f..86730b6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.7.0"
+version = "0.7.1"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index d81f70e..98e8eb5 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -28,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.7.0",
+ "version": "0.7.1",
"dev": true,
"license": "MIT",
"devDependencies": {
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index 0e226ad..a3319ff 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.7.0",
+ "version": "0.7.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.7.0",
+ "version": "0.7.1",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.7.0",
+ "version": "0.7.1",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index 2d4c20b..da0c4fc 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.7.0",
+ "version": "0.7.1",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From 065b689103cec5a4e6bd877383c965fdea25ff11 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 22:12:44 +0000
Subject: [PATCH 041/107] Bump insta from 1.43.2 to 1.44.3 (#30)
Bumps [insta](https://github.com/mitsuhiko/insta) from 1.43.2 to 1.44.3.
- [Release notes](https://github.com/mitsuhiko/insta/releases)
- [Changelog](https://github.com/mitsuhiko/insta/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/insta/compare/1.43.2...1.44.3)
---
updated-dependencies:
- dependency-name: insta
dependency-version: 1.44.3
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Cargo.lock | 4 ++--
Cargo.toml | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bcbefae..0bf8c8c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -94,9 +94,9 @@ dependencies = [
[[package]]
name = "insta"
-version = "1.43.2"
+version = "1.44.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0"
+checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698"
dependencies = [
"console",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 86730b6..27626df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,7 +41,7 @@ console_error_panic_hook = [ "dep:console_error_panic_hook" ]
all = [ "wasm", "serde" ]
[dev-dependencies]
-insta = "1.43.2"
+insta = "1.44.3"
pretty_assertions = "1.4.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
From 9f597ab8ae23a6e77412394a96651dfbf8fe3972 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 22:12:52 +0000
Subject: [PATCH 042/107] Bump wasm-bindgen-test from 0.3.55 to 0.3.56 (#29)
Bumps [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen) from 0.3.55 to 0.3.56.
- [Release notes](https://github.com/wasm-bindgen/wasm-bindgen/releases)
- [Changelog](https://github.com/wasm-bindgen/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wasm-bindgen/wasm-bindgen/commits)
---
updated-dependencies:
- dependency-name: wasm-bindgen-test
dependency-version: 0.3.56
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Cargo.lock | 117 ++++++++++++++++++++++++++++++++++++++++++++---------
Cargo.toml | 2 +-
2 files changed, 100 insertions(+), 19 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 0bf8c8c..e553412 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,12 +2,35 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
[[package]]
name = "bumpalo"
version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
[[package]]
name = "cc"
version = "1.2.46"
@@ -111,9 +134,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -125,6 +148,18 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
[[package]]
name = "memory_units"
version = "0.4.0"
@@ -141,12 +176,37 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -241,6 +301,19 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
@@ -334,9 +407,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if 1.0.4",
"once_cell",
@@ -347,9 +420,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.55"
+version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if 1.0.4",
"js-sys",
@@ -360,9 +433,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -370,9 +443,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -383,21 +456,29 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.55"
+version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373"
+checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c"
dependencies = [
+ "async-trait",
+ "cast",
"js-sys",
+ "libm",
"minicov",
+ "nu-ansi-term",
+ "num-traits",
+ "oorandom",
+ "serde",
+ "serde_json",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
@@ -405,9 +486,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.55"
+version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1"
+checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1"
dependencies = [
"proc-macro2",
"quote",
@@ -416,9 +497,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
diff --git a/Cargo.toml b/Cargo.toml
index 27626df..842631d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -46,7 +46,7 @@ pretty_assertions = "1.4.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
test-case = "3.3.1"
-wasm-bindgen-test = "0.3.55"
+wasm-bindgen-test = "0.3.56"
[profile.release]
codegen-units = 1
From ff5421d023df5df36f388c7167360ebb4ecd74ad Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 22:12:59 +0000
Subject: [PATCH 043/107] Bump actions/checkout from 5 to 6 (#25)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: '6'
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/check.yml | 6 +++---
.github/workflows/gh-pages.yml | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index c9991da..cdfc47f 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Setup Node.js environment
uses: actions/setup-node@v6.0.0
@@ -61,7 +61,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Cache Rust dependencies
uses: actions/cache@v4
@@ -85,7 +85,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Setup Node.js environment
uses: actions/setup-node@v6.0.0
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 6956525..abfdda4 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -25,7 +25,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Cache Rust dependencies
uses: actions/cache@v4
From e03b9147df2a649d9ddc9d01f0de9fcd04eb7db9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 5 Dec 2025 22:13:06 +0000
Subject: [PATCH 044/107] Bump actions/setup-node from 6.0.0 to 6.1.0 (#31)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.0.0...v6.1.0)
---
updated-dependencies:
- dependency-name: actions/setup-node
dependency-version: 6.1.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/check.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index cdfc47f..8ca6cf9 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v6
- name: Setup Node.js environment
- uses: actions/setup-node@v6.0.0
+ uses: actions/setup-node@v6.1.0
with:
node-version: '22.x'
check-latest: true
@@ -88,7 +88,7 @@ jobs:
- uses: actions/checkout@v6
- name: Setup Node.js environment
- uses: actions/setup-node@v6.0.0
+ uses: actions/setup-node@v6.1.0
with:
node-version: '22.x'
check-latest: true
From 88d48afce3af793c851217f5dc508f5e586558a1 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 6 Dec 2025 21:54:08 +0000
Subject: [PATCH 045/107] Add diff applying error & improve CI (#32)
* Use stable rust
* Add From impls
* Revert to nightly
* Improve dev env & CI setup
* Update lock
* Add thiserror
* Add diff error
* Fix tests
* Lint
* Rename NumberOrString
* Format
* Fix lint script
---
.github/workflows/check.yml | 29 +-
Cargo.lock | 21 +
Cargo.toml | 1 +
README.md | 6 +-
reconcile-js/package-lock.json | 1124 +----------------
scripts/build-website.sh | 2 +
scripts/bump-version.sh | 4 +
scripts/lint.sh | 5 +
scripts/test.sh | 12 +-
src/lib.rs | 8 +-
src/operation_transformation.rs | 2 +
src/operation_transformation/diff_error.rs | 19 +
src/operation_transformation/edited_text.rs | 103 +-
src/types.rs | 2 +-
...{number_or_string.rs => number_or_text.rs} | 36 +-
src/wasm.rs | 9 +-
tests/test.rs | 4 +-
17 files changed, 195 insertions(+), 1192 deletions(-)
create mode 100644 src/operation_transformation/diff_error.rs
rename src/types/{number_or_string.rs => number_or_text.rs} (68%)
diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 8ca6cf9..4561abc 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -37,20 +37,8 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-
- - name: Setup rust
- run: |
- which wasm-pack || cargo install wasm-pack
- which cargo-machete || cargo install cargo-machete
-
- - name: Build wasm
- run: |
- wasm-pack build --target web --features wasm
-
- name: Lint
- run: |
- cargo clippy --all-targets --all-features
- cargo fmt --all -- --check
- cargo machete
+ run: scripts/lint.sh
- name: Test
run: scripts/test.sh
@@ -117,19 +105,8 @@ jobs:
restore-keys: |
${{ runner.os }}-npm-
- - name: Setup rust
- run: |
- which wasm-pack || cargo install wasm-pack
-
- - name: Build wasm
- run: |
- wasm-pack build --target web --features wasm
-
- - name: Build reconcile-js
- run: |
- cd reconcile-js
- npm ci
- npm run build
+ - name: Build website
+ run: scripts/build-website.sh
- name: Publish reconcile-js to NPM
run: |
diff --git a/Cargo.lock b/Cargo.lock
index e553412..bb63c76 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -245,6 +245,7 @@ dependencies = [
"serde",
"serde_yaml",
"test-case",
+ "thiserror",
"wasm-bindgen",
"wasm-bindgen-test",
"wee_alloc",
@@ -383,6 +384,26 @@ dependencies = [
"test-case-core",
]
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.22"
diff --git a/Cargo.toml b/Cargo.toml
index 842631d..733b8d1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ path = "examples/merge-file.rs"
[dependencies]
serde = { version = "1.0.219", optional = true, features = ["derive"] }
+thiserror = "2.0.17"
wasm-bindgen = { version = "0.2.99", optional = true }
diff --git a/README.md b/README.md
index a5e5a46..7925ed5 100644
--- a/README.md
+++ b/README.md
@@ -125,14 +125,10 @@ Contributions are welcome!
#### Rust toolchain
-1. Install [rustup](https://rustup.rs):
+Install [rustup](https://rustup.rs):
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
-2. Install additional tools:
- ```bash
- cargo install wasm-pack cargo-insta cargo-edit
- ```
### Scripts
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index a3319ff..e9fa242 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -30,8 +30,6 @@
},
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
- "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -44,8 +42,6 @@
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
- "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -59,8 +55,6 @@
},
"node_modules/@babel/compat-data": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
- "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -69,8 +63,6 @@
},
"node_modules/@babel/core": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz",
- "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -100,8 +92,6 @@
},
"node_modules/@babel/generator": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
- "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -117,8 +107,6 @@
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
- "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -134,8 +122,6 @@
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
- "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -144,8 +130,6 @@
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
- "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -158,8 +142,6 @@
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.27.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
- "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -176,8 +158,6 @@
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
- "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -186,8 +166,6 @@
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -196,8 +174,6 @@
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
- "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
@@ -206,8 +182,6 @@
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -216,8 +190,6 @@
},
"node_modules/@babel/helpers": {
"version": "7.27.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
- "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -230,8 +202,6 @@
},
"node_modules/@babel/parser": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
- "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -246,8 +216,6 @@
},
"node_modules/@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
- "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -259,8 +227,6 @@
},
"node_modules/@babel/plugin-syntax-bigint": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
- "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -272,8 +238,6 @@
},
"node_modules/@babel/plugin-syntax-class-properties": {
"version": "7.12.13",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
- "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -285,8 +249,6 @@
},
"node_modules/@babel/plugin-syntax-class-static-block": {
"version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
- "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -301,8 +263,6 @@
},
"node_modules/@babel/plugin-syntax-import-attributes": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
- "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -317,8 +277,6 @@
},
"node_modules/@babel/plugin-syntax-import-meta": {
"version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
- "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -330,8 +288,6 @@
},
"node_modules/@babel/plugin-syntax-json-strings": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
- "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -343,8 +299,6 @@
},
"node_modules/@babel/plugin-syntax-jsx": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
- "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -359,8 +313,6 @@
},
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
"version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
- "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -372,8 +324,6 @@
},
"node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
- "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -385,8 +335,6 @@
},
"node_modules/@babel/plugin-syntax-numeric-separator": {
"version": "7.10.4",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
- "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -398,8 +346,6 @@
},
"node_modules/@babel/plugin-syntax-object-rest-spread": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
- "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -411,8 +357,6 @@
},
"node_modules/@babel/plugin-syntax-optional-catch-binding": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
- "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -424,8 +368,6 @@
},
"node_modules/@babel/plugin-syntax-optional-chaining": {
"version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
- "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -437,8 +379,6 @@
},
"node_modules/@babel/plugin-syntax-private-property-in-object": {
"version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
- "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -453,8 +393,6 @@
},
"node_modules/@babel/plugin-syntax-top-level-await": {
"version": "7.14.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
- "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -469,8 +407,6 @@
},
"node_modules/@babel/plugin-syntax-typescript": {
"version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
- "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -485,8 +421,6 @@
},
"node_modules/@babel/template": {
"version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
- "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -500,8 +434,6 @@
},
"node_modules/@babel/traverse": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
- "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -519,8 +451,6 @@
},
"node_modules/@babel/types": {
"version": "7.28.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz",
- "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -533,59 +463,19 @@
},
"node_modules/@bcoe/v8-coverage": {
"version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
- "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true,
"license": "MIT"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.6.3",
- "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
- "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.17.0"
}
},
- "node_modules/@emnapi/core": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
- "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.0.2",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
- "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz",
- "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -602,8 +492,6 @@
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
- "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -619,8 +507,6 @@
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
- "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -629,8 +515,6 @@
},
"node_modules/@jest/console": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz",
- "integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -647,8 +531,6 @@
},
"node_modules/@jest/core": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.4.tgz",
- "integrity": "sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -695,8 +577,6 @@
},
"node_modules/@jest/diff-sequences": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
- "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -705,8 +585,6 @@
},
"node_modules/@jest/environment": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz",
- "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -721,8 +599,6 @@
},
"node_modules/@jest/expect": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz",
- "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -735,8 +611,6 @@
},
"node_modules/@jest/expect-utils": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz",
- "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -748,8 +622,6 @@
},
"node_modules/@jest/fake-timers": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz",
- "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -766,8 +638,6 @@
},
"node_modules/@jest/get-type": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz",
- "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -776,8 +646,6 @@
},
"node_modules/@jest/globals": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz",
- "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -792,8 +660,6 @@
},
"node_modules/@jest/pattern": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
- "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -806,8 +672,6 @@
},
"node_modules/@jest/reporters": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz",
- "integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -849,8 +713,6 @@
},
"node_modules/@jest/reporters/node_modules/jest-worker": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz",
- "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -866,8 +728,6 @@
},
"node_modules/@jest/schemas": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz",
- "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -879,8 +739,6 @@
},
"node_modules/@jest/snapshot-utils": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz",
- "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -895,8 +753,6 @@
},
"node_modules/@jest/source-map": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz",
- "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -910,8 +766,6 @@
},
"node_modules/@jest/test-result": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz",
- "integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -926,8 +780,6 @@
},
"node_modules/@jest/test-sequencer": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.4.tgz",
- "integrity": "sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -942,8 +794,6 @@
},
"node_modules/@jest/transform": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz",
- "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -969,8 +819,6 @@
},
"node_modules/@jest/types": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz",
- "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -988,8 +836,6 @@
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.12",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
- "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -999,8 +845,6 @@
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1009,8 +853,6 @@
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.10",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
- "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1020,15 +862,11 @@
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
- "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.29",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
- "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1036,23 +874,8 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "0.2.11",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz",
- "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.4.3",
- "@emnapi/runtime": "^1.4.3",
- "@tybys/wasm-util": "^0.9.0"
- }
- },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -1062,8 +885,6 @@
},
"node_modules/@pkgr/core": {
"version": "0.2.7",
- "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz",
- "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1075,15 +896,11 @@
},
"node_modules/@sinclair/typebox": {
"version": "0.34.37",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz",
- "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==",
"dev": true,
"license": "MIT"
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
- "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1092,29 +909,14 @@
},
"node_modules/@sinonjs/fake-timers": {
"version": "13.0.5",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
- "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
}
},
- "node_modules/@tybys/wasm-util": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
- "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@types/babel__core": {
"version": "7.20.5",
- "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
- "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1127,8 +929,6 @@
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
- "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1137,8 +937,6 @@
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
- "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
- "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1148,8 +946,6 @@
},
"node_modules/@types/babel__traverse": {
"version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
- "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1158,8 +954,6 @@
},
"node_modules/@types/eslint": {
"version": "9.6.1",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
- "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1169,8 +963,6 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
- "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1180,22 +972,16 @@
},
"node_modules/@types/estree": {
"version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
- "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
- "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1204,8 +990,6 @@
},
"node_modules/@types/istanbul-reports": {
"version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
- "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1214,8 +998,6 @@
},
"node_modules/@types/jest": {
"version": "30.0.0",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz",
- "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1225,15 +1007,11 @@
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.10.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
- "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1242,15 +1020,11 @@
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
- "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
- "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
- "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1259,204 +1033,16 @@
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
- "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
- "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
- "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"dev": true,
"license": "ISC"
},
- "node_modules/@unrs/resolver-binding-android-arm-eabi": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.0.tgz",
- "integrity": "sha512-LRw5BW29sYj9NsQC6QoqeLVQhEa+BwVINYyMlcve+6stwdBsSt5UB7zw4UZB4+4PNqIVilHoMaPWCb/KhABHQw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@unrs/resolver-binding-android-arm64": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.0.tgz",
- "integrity": "sha512-zYX8D2zcWCAHqghA8tPjbp7LwjVXbIZP++mpU/Mrf5jUVlk3BWIxkeB8yYzZi5GpFSlqMcRZQxQqbMI0c2lASQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ]
- },
- "node_modules/@unrs/resolver-binding-darwin-arm64": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.0.tgz",
- "integrity": "sha512-YsYOT049hevAY/lTYD77GhRs885EXPeAfExG5KenqMJ417nYLS2N/kpRpYbABhFZBVQn+2uRPasTe4ypmYoo3w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@unrs/resolver-binding-darwin-x64": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.0.tgz",
- "integrity": "sha512-PSjvk3OZf1aZImdGY5xj9ClFG3bC4gnSSYWrt+id0UAv+GwwVldhpMFjAga8SpMo2T1GjV9UKwM+QCsQCQmtdA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@unrs/resolver-binding-freebsd-x64": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.0.tgz",
- "integrity": "sha512-KC/iFaEN/wsTVYnHClyHh5RSYA9PpuGfqkFua45r4sweXpC0KHZ+BYY7ikfcGPt5w1lMpR1gneFzuqWLQxsRKg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.0.tgz",
- "integrity": "sha512-CDh/0v8uot43cB4yKtDL9CVY8pbPnMV0dHyQCE4lFz6PW/+9tS0i9eqP5a91PAqEBVMqH1ycu+k8rP6wQU846w==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.0.tgz",
- "integrity": "sha512-+TE7epATDSnvwr3L/hNHX3wQ8KQYB+jSDTdywycg3qDqvavRP8/HX9qdq/rMcnaRDn4EOtallb3vL/5wCWGCkw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.0.tgz",
- "integrity": "sha512-VBAYGg3VahofpQ+L4k/ZO8TSICIbUKKTaMYOWHWfuYBFqPbSkArZZLezw3xd27fQkxX4BaLGb/RKnW0dH9Y/UA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.0.tgz",
- "integrity": "sha512-9IgGFUUb02J1hqdRAHXpZHIeUHRrbnGo6vrRbz0fREH7g+rzQy53/IBSyadZ/LG5iqMxukriNPu4hEMUn+uWEg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.0.tgz",
- "integrity": "sha512-LR4iQ/LPjMfivpL2bQ9kmm3UnTas3U+umcCnq/CV7HAkukVdHxrDD1wwx74MIWbbgzQTLPYY7Ur2MnnvkYJCBQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.0.tgz",
- "integrity": "sha512-HCupFQwMrRhrOg7YHrobbB5ADg0Q8RNiuefqMHVsdhEy9lLyXm/CxsCXeLJdrg27NAPsCaMDtdlm8Z2X8x91Tg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.0.tgz",
- "integrity": "sha512-Ckxy76A5xgjWa4FNrzcKul5qFMWgP5JSQ5YKd0XakmWOddPLSkQT+uAvUpQNnFGNbgKzv90DyQlxPDYPQ4nd6A==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.0.tgz",
- "integrity": "sha512-HfO0PUCCRte2pMJmVyxPI+eqT7KuV3Fnvn2RPvMe5mOzb2BJKf4/Vth8sSt9cerQboMaTVpbxyYjjLBWIuI5BQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/@unrs/resolver-binding-linux-x64-gnu": {
"version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.0.tgz",
- "integrity": "sha512-9PZdjP7tLOEjpXHS6+B/RNqtfVUyDEmaViPOuSqcbomLdkJnalt5RKQ1tr2m16+qAufV0aDkfhXtoO7DQos/jg==",
"cpu": [
"x64"
],
@@ -1469,8 +1055,6 @@
},
"node_modules/@unrs/resolver-binding-linux-x64-musl": {
"version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.0.tgz",
- "integrity": "sha512-qkE99ieiSKMnFJY/EfyGKVtNra52/k+lVF/PbO4EL5nU6AdvG4XhtJ+WHojAJP7ID9BNIra/yd75EHndewNRfA==",
"cpu": [
"x64"
],
@@ -1481,69 +1065,8 @@
"linux"
]
},
- "node_modules/@unrs/resolver-binding-wasm32-wasi": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.0.tgz",
- "integrity": "sha512-MjXek8UL9tIX34gymvQLecz2hMaQzOlaqYJJBomwm1gsvK2F7hF+YqJJ2tRyBDTv9EZJGMt4KlKkSD/gZWCOiw==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@napi-rs/wasm-runtime": "^0.2.11"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.0.tgz",
- "integrity": "sha512-9LT6zIGO7CHybiQSh7DnQGwFMZvVr0kUjah6qQfkH2ghucxPV6e71sUXJdSM4Ba0MaGE6DC/NwWf7mJmc3DAng==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.0.tgz",
- "integrity": "sha512-HYchBYOZ7WN266VjoGm20xFv5EonG/ODURRgwl9EZT7Bq1nLEs6VKJddzfFdXEAho0wfFlt8L/xIiE29Pmy1RA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
- "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.0.tgz",
- "integrity": "sha512-+oLKLHw3I1UQo4MeHfoLYF+e6YBa8p5vYUw3Rgt7IDzCs+57vIZqQlIo62NDpYM0VG6BjWOwnzBczMvbtH8hag==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
- "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1553,29 +1076,21 @@
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
- "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
- "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
- "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
- "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1586,15 +1101,11 @@
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
- "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
- "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1606,8 +1117,6 @@
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
- "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1616,8 +1125,6 @@
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
- "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -1626,15 +1133,11 @@
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
- "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
- "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1650,8 +1153,6 @@
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
- "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1664,8 +1165,6 @@
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
- "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1677,8 +1176,6 @@
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
- "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1692,8 +1189,6 @@
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
- "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1703,8 +1198,6 @@
},
"node_modules/@webpack-cli/configtest": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
- "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1717,8 +1210,6 @@
},
"node_modules/@webpack-cli/info": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz",
- "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1731,8 +1222,6 @@
},
"node_modules/@webpack-cli/serve": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz",
- "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1750,22 +1239,16 @@
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/acorn": {
"version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1777,8 +1260,6 @@
},
"node_modules/ajv": {
"version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1794,8 +1275,6 @@
},
"node_modules/ajv-formats": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1812,8 +1291,6 @@
},
"node_modules/ajv-keywords": {
"version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1825,8 +1302,6 @@
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
- "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1841,8 +1316,6 @@
},
"node_modules/ansi-regex": {
"version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1854,8 +1327,6 @@
},
"node_modules/ansi-styles": {
"version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1870,8 +1341,6 @@
},
"node_modules/anymatch": {
"version": "3.1.3",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
- "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1884,8 +1353,6 @@
},
"node_modules/argparse": {
"version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1894,15 +1361,11 @@
},
"node_modules/async": {
"version": "3.2.6",
- "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
- "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT"
},
"node_modules/babel-jest": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.4.tgz",
- "integrity": "sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1923,8 +1386,6 @@
},
"node_modules/babel-plugin-istanbul": {
"version": "7.0.0",
- "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz",
- "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -1940,8 +1401,6 @@
},
"node_modules/babel-plugin-jest-hoist": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz",
- "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1955,8 +1414,6 @@
},
"node_modules/babel-preset-current-node-syntax": {
"version": "1.1.0",
- "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz",
- "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1982,8 +1439,6 @@
},
"node_modules/babel-preset-jest": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz",
- "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1999,15 +1454,11 @@
},
"node_modules/balanced-match": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.12",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
- "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2017,8 +1468,6 @@
},
"node_modules/braces": {
"version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2030,8 +1479,6 @@
},
"node_modules/browserslist": {
"version": "4.25.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
- "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"dev": true,
"funding": [
{
@@ -2063,8 +1510,6 @@
},
"node_modules/bs-logger": {
"version": "0.2.6",
- "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
- "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2076,8 +1521,6 @@
},
"node_modules/bser": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
- "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2086,15 +1529,11 @@
},
"node_modules/buffer-from": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/callsites": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2103,8 +1542,6 @@
},
"node_modules/camelcase": {
"version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2113,8 +1550,6 @@
},
"node_modules/caniuse-lite": {
"version": "1.0.30001726",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001726.tgz",
- "integrity": "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==",
"dev": true,
"funding": [
{
@@ -2134,8 +1569,6 @@
},
"node_modules/chalk": {
"version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2151,8 +1584,6 @@
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2164,8 +1595,6 @@
},
"node_modules/char-regex": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
- "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2174,8 +1603,6 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
- "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2184,8 +1611,6 @@
},
"node_modules/ci-info": {
"version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
- "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
"dev": true,
"funding": [
{
@@ -2200,15 +1625,11 @@
},
"node_modules/cjs-module-lexer": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz",
- "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2222,8 +1643,6 @@
},
"node_modules/cliui/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2232,15 +1651,11 @@
},
"node_modules/cliui/node_modules/emoji-regex": {
"version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui/node_modules/string-width": {
"version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2254,8 +1669,6 @@
},
"node_modules/cliui/node_modules/strip-ansi": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2267,8 +1680,6 @@
},
"node_modules/cliui/node_modules/wrap-ansi": {
"version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2285,8 +1696,6 @@
},
"node_modules/clone-deep": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
- "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2300,8 +1709,6 @@
},
"node_modules/co": {
"version": "4.6.0",
- "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
- "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2311,15 +1718,11 @@
},
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
- "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2331,43 +1734,31 @@
},
"node_modules/color-name": {
"version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/colorette": {
"version": "2.0.20",
- "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
- "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2381,8 +1772,6 @@
},
"node_modules/debug": {
"version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2399,8 +1788,6 @@
},
"node_modules/dedent": {
"version": "1.6.0",
- "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz",
- "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -2414,8 +1801,6 @@
},
"node_modules/deepmerge": {
"version": "4.3.1",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
- "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2424,8 +1809,6 @@
},
"node_modules/detect-newline": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
- "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2434,15 +1817,11 @@
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT"
},
"node_modules/ejs": {
"version": "3.1.10",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
- "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2457,15 +1836,11 @@
},
"node_modules/electron-to-chromium": {
"version": "1.5.179",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz",
- "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==",
"dev": true,
"license": "ISC"
},
"node_modules/emittery": {
"version": "0.13.1",
- "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
- "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2477,15 +1852,11 @@
},
"node_modules/emoji-regex": {
"version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
- "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2498,8 +1869,6 @@
},
"node_modules/envinfo": {
"version": "7.14.0",
- "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
- "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2511,8 +1880,6 @@
},
"node_modules/error-ex": {
"version": "1.3.2",
- "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
- "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2521,15 +1888,11 @@
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
- "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2538,8 +1901,6 @@
},
"node_modules/escape-string-regexp": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
- "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2548,8 +1909,6 @@
},
"node_modules/eslint-scope": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -2562,8 +1921,6 @@
},
"node_modules/esprima": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
@@ -2576,8 +1933,6 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -2589,8 +1944,6 @@
},
"node_modules/esrecurse/node_modules/estraverse": {
"version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -2599,8 +1952,6 @@
},
"node_modules/estraverse": {
"version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -2609,8 +1960,6 @@
},
"node_modules/events": {
"version": "3.3.0",
- "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
- "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2619,8 +1968,6 @@
},
"node_modules/execa": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
- "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2643,8 +1990,6 @@
},
"node_modules/exit-x": {
"version": "0.2.2",
- "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
- "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2653,8 +1998,6 @@
},
"node_modules/expect": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz",
- "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2671,22 +2014,16 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
- "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
"dev": true,
"funding": [
{
@@ -2702,8 +2039,6 @@
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
- "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
- "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2712,8 +2047,6 @@
},
"node_modules/fb-watchman": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
- "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2722,8 +2055,6 @@
},
"node_modules/filelist": {
"version": "1.0.4",
- "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
- "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2732,8 +2063,6 @@
},
"node_modules/filelist/node_modules/brace-expansion": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2742,8 +2071,6 @@
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
- "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2755,8 +2082,6 @@
},
"node_modules/fill-range": {
"version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2768,8 +2093,6 @@
},
"node_modules/find-up": {
"version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2782,8 +2105,6 @@
},
"node_modules/flat": {
"version": "5.0.2",
- "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
- "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true,
"license": "BSD-3-Clause",
"bin": {
@@ -2792,8 +2113,6 @@
},
"node_modules/foreground-child": {
"version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2809,8 +2128,6 @@
},
"node_modules/foreground-child/node_modules/signal-exit": {
"version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -2822,30 +2139,11 @@
},
"node_modules/fs.realpath": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
"node_modules/function-bind": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -2854,8 +2152,6 @@
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2864,8 +2160,6 @@
},
"node_modules/get-caller-file": {
"version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
@@ -2874,8 +2168,6 @@
},
"node_modules/get-package-type": {
"version": "0.1.0",
- "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
- "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2884,8 +2176,6 @@
},
"node_modules/get-stream": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2896,9 +2186,7 @@
}
},
"node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2918,15 +2206,11 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
- "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2935,8 +2219,6 @@
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
- "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2951,15 +2233,11 @@
},
"node_modules/graceful-fs": {
"version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/has-flag": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2968,8 +2246,6 @@
},
"node_modules/hasown": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2981,15 +2257,11 @@
},
"node_modules/html-escaper": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
- "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/human-signals": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
- "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -2998,8 +2270,6 @@
},
"node_modules/import-local": {
"version": "3.2.0",
- "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
- "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3018,8 +2288,6 @@
},
"node_modules/imurmurhash": {
"version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3028,9 +2296,6 @@
},
"node_modules/inflight": {
"version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
- "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3040,15 +2305,11 @@
},
"node_modules/inherits": {
"version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/interpret": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
- "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3057,15 +2318,11 @@
},
"node_modules/is-arrayish": {
"version": "0.2.1",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"dev": true,
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3080,8 +2337,6 @@
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3090,8 +2345,6 @@
},
"node_modules/is-generator-fn": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
- "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3100,8 +2353,6 @@
},
"node_modules/is-number": {
"version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3110,8 +2361,6 @@
},
"node_modules/is-plain-object": {
"version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
- "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3123,8 +2372,6 @@
},
"node_modules/is-stream": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
- "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3136,15 +2383,11 @@
},
"node_modules/isexe": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/isobject": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3153,8 +2396,6 @@
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
- "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
- "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -3163,8 +2404,6 @@
},
"node_modules/istanbul-lib-instrument": {
"version": "6.0.3",
- "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
- "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3180,8 +2419,6 @@
},
"node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3193,8 +2430,6 @@
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
- "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3208,8 +2443,6 @@
},
"node_modules/istanbul-lib-report/node_modules/supports-color": {
"version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3221,8 +2454,6 @@
},
"node_modules/istanbul-lib-source-maps": {
"version": "5.0.6",
- "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
- "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3236,8 +2467,6 @@
},
"node_modules/istanbul-reports": {
"version": "3.1.7",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
- "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -3250,8 +2479,6 @@
},
"node_modules/jackspeak": {
"version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -3266,8 +2493,6 @@
},
"node_modules/jake": {
"version": "10.9.2",
- "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
- "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -3285,8 +2510,6 @@
},
"node_modules/jest": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz",
- "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3312,8 +2535,6 @@
},
"node_modules/jest-changed-files": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz",
- "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3327,8 +2548,6 @@
},
"node_modules/jest-circus": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.4.tgz",
- "integrity": "sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3359,8 +2578,6 @@
},
"node_modules/jest-cli": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.4.tgz",
- "integrity": "sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3392,8 +2609,6 @@
},
"node_modules/jest-config": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.4.tgz",
- "integrity": "sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3444,8 +2659,6 @@
},
"node_modules/jest-diff": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz",
- "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3460,8 +2673,6 @@
},
"node_modules/jest-docblock": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz",
- "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3473,8 +2684,6 @@
},
"node_modules/jest-each": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz",
- "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3490,8 +2699,6 @@
},
"node_modules/jest-environment-node": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz",
- "integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3509,8 +2716,6 @@
},
"node_modules/jest-haste-map": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz",
- "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3534,8 +2739,6 @@
},
"node_modules/jest-haste-map/node_modules/jest-worker": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz",
- "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3551,8 +2754,6 @@
},
"node_modules/jest-leak-detector": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz",
- "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3565,8 +2766,6 @@
},
"node_modules/jest-matcher-utils": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz",
- "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3581,8 +2780,6 @@
},
"node_modules/jest-message-util": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz",
- "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3602,8 +2799,6 @@
},
"node_modules/jest-mock": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz",
- "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3617,8 +2812,6 @@
},
"node_modules/jest-pnp-resolver": {
"version": "1.2.3",
- "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
- "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3635,8 +2828,6 @@
},
"node_modules/jest-regex-util": {
"version": "30.0.1",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
- "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3645,8 +2836,6 @@
},
"node_modules/jest-resolve": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz",
- "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3665,8 +2854,6 @@
},
"node_modules/jest-resolve-dependencies": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.4.tgz",
- "integrity": "sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3679,8 +2866,6 @@
},
"node_modules/jest-runner": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.4.tgz",
- "integrity": "sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3713,8 +2898,6 @@
},
"node_modules/jest-runner/node_modules/jest-worker": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz",
- "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3730,8 +2913,6 @@
},
"node_modules/jest-runner/node_modules/source-map-support": {
"version": "0.5.13",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
- "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3741,8 +2922,6 @@
},
"node_modules/jest-runtime": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.4.tgz",
- "integrity": "sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3775,8 +2954,6 @@
},
"node_modules/jest-snapshot": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz",
- "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3808,8 +2985,6 @@
},
"node_modules/jest-snapshot/node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3821,8 +2996,6 @@
},
"node_modules/jest-util": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz",
- "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3839,8 +3012,6 @@
},
"node_modules/jest-util/node_modules/picomatch": {
"version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3852,8 +3023,6 @@
},
"node_modules/jest-validate": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz",
- "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3870,8 +3039,6 @@
},
"node_modules/jest-validate/node_modules/camelcase": {
"version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3883,8 +3050,6 @@
},
"node_modules/jest-watcher": {
"version": "30.0.4",
- "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.4.tgz",
- "integrity": "sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3903,8 +3068,6 @@
},
"node_modules/jest-worker": {
"version": "27.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
- "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3918,15 +3081,11 @@
},
"node_modules/js-tokens": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3939,8 +3098,6 @@
},
"node_modules/jsesc": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3952,22 +3109,16 @@
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/json5": {
"version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3979,8 +3130,6 @@
},
"node_modules/kind-of": {
"version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3989,8 +3138,6 @@
},
"node_modules/leven": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
- "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3999,15 +3146,11 @@
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
- "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/loader-runner": {
"version": "4.3.0",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
- "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4016,8 +3159,6 @@
},
"node_modules/locate-path": {
"version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4029,15 +3170,11 @@
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
- "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true,
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4046,8 +3183,6 @@
},
"node_modules/make-dir": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
- "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4062,8 +3197,6 @@
},
"node_modules/make-dir/node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4075,15 +3208,11 @@
},
"node_modules/make-error": {
"version": "1.3.6",
- "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
- "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/makeerror": {
"version": "1.0.12",
- "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
- "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4092,15 +3221,11 @@
},
"node_modules/merge-stream": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4113,8 +3238,6 @@
},
"node_modules/mime-db": {
"version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4123,8 +3246,6 @@
},
"node_modules/mime-types": {
"version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4136,8 +3257,6 @@
},
"node_modules/mimic-fn": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4146,8 +3265,6 @@
},
"node_modules/minimatch": {
"version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4159,8 +3276,6 @@
},
"node_modules/minipass": {
"version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -4169,15 +3284,11 @@
},
"node_modules/ms": {
"version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/napi-postinstall": {
"version": "0.3.0",
- "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz",
- "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4192,36 +3303,26 @@
},
"node_modules/natural-compare": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/neo-async": {
"version": "2.6.2",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
- "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-int64": {
"version": "0.4.0",
- "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
- "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4230,8 +3331,6 @@
},
"node_modules/npm-run-path": {
"version": "4.0.1",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
- "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4243,8 +3342,6 @@
},
"node_modules/once": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -4253,8 +3350,6 @@
},
"node_modules/onetime": {
"version": "5.1.2",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
- "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4269,8 +3364,6 @@
},
"node_modules/p-limit": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4285,8 +3378,6 @@
},
"node_modules/p-locate": {
"version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4298,8 +3389,6 @@
},
"node_modules/p-locate/node_modules/p-limit": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4314,8 +3403,6 @@
},
"node_modules/p-try": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4324,15 +3411,11 @@
},
"node_modules/package-json-from-dist": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/parse-json": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
- "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4350,8 +3433,6 @@
},
"node_modules/path-exists": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4360,8 +3441,6 @@
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4370,8 +3449,6 @@
},
"node_modules/path-key": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4380,15 +3457,11 @@
},
"node_modules/path-parse": {
"version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -4404,22 +3477,16 @@
},
"node_modules/path-scurry/node_modules/lru-cache": {
"version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/picocolors": {
"version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4431,8 +3498,6 @@
},
"node_modules/pirates": {
"version": "4.0.7",
- "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
- "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4441,8 +3506,6 @@
},
"node_modules/pkg-dir": {
"version": "4.2.0",
- "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
- "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4454,8 +3517,6 @@
},
"node_modules/prettier": {
"version": "3.6.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
- "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4470,8 +3531,6 @@
},
"node_modules/pretty-format": {
"version": "30.0.2",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz",
- "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4485,8 +3544,6 @@
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4498,8 +3555,6 @@
},
"node_modules/pure-rand": {
"version": "7.0.1",
- "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
- "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
"dev": true,
"funding": [
{
@@ -4515,8 +3570,6 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4525,15 +3578,11 @@
},
"node_modules/react-is": {
"version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"dev": true,
"license": "MIT"
},
"node_modules/rechoir": {
"version": "0.8.0",
- "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
- "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4549,8 +3598,6 @@
},
"node_modules/require-directory": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4559,8 +3606,6 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4569,8 +3614,6 @@
},
"node_modules/resolve": {
"version": "1.22.10",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
- "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4590,8 +3633,6 @@
},
"node_modules/resolve-cwd": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
- "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4603,8 +3644,6 @@
},
"node_modules/resolve-from": {
"version": "5.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
- "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4613,8 +3652,6 @@
},
"node_modules/safe-buffer": {
"version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
@@ -4634,8 +3671,6 @@
},
"node_modules/schema-utils": {
"version": "4.3.2",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
- "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4654,8 +3689,6 @@
},
"node_modules/semver": {
"version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -4664,8 +3697,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -4674,8 +3705,6 @@
},
"node_modules/shallow-clone": {
"version": "3.0.1",
- "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
- "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4687,8 +3716,6 @@
},
"node_modules/shebang-command": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4700,8 +3727,6 @@
},
"node_modules/shebang-regex": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4710,15 +3735,11 @@
},
"node_modules/signal-exit": {
"version": "3.0.7",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"dev": true,
"license": "ISC"
},
"node_modules/slash": {
"version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4727,8 +3748,6 @@
},
"node_modules/source-map": {
"version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -4737,8 +3756,6 @@
},
"node_modules/source-map-support": {
"version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4748,15 +3765,11 @@
},
"node_modules/sprintf-js": {
"version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/stack-utils": {
"version": "2.0.6",
- "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
- "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4768,8 +3781,6 @@
},
"node_modules/string-length": {
"version": "4.0.2",
- "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
- "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4782,8 +3793,6 @@
},
"node_modules/string-length/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4792,8 +3801,6 @@
},
"node_modules/string-length/node_modules/strip-ansi": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4805,8 +3812,6 @@
},
"node_modules/string-width": {
"version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4824,8 +3829,6 @@
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4839,8 +3842,6 @@
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4849,15 +3850,11 @@
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4869,8 +3866,6 @@
},
"node_modules/strip-ansi": {
"version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4886,8 +3881,6 @@
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4899,8 +3892,6 @@
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4909,8 +3900,6 @@
},
"node_modules/strip-bom": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
- "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4919,8 +3908,6 @@
},
"node_modules/strip-final-newline": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
- "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4929,8 +3916,6 @@
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4942,8 +3927,6 @@
},
"node_modules/supports-color": {
"version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4958,8 +3941,6 @@
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4971,8 +3952,6 @@
},
"node_modules/synckit": {
"version": "0.11.8",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz",
- "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4987,8 +3966,6 @@
},
"node_modules/tapable": {
"version": "2.2.2",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
- "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4997,8 +3974,6 @@
},
"node_modules/terser": {
"version": "5.43.1",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
- "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -5016,8 +3991,6 @@
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
- "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5051,8 +4024,6 @@
},
"node_modules/test-exclude": {
"version": "6.0.0",
- "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
- "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5066,9 +4037,6 @@
},
"node_modules/test-exclude/node_modules/glob": {
"version": "7.2.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
- "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
- "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5088,15 +4056,11 @@
},
"node_modules/tmpl": {
"version": "1.0.5",
- "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
- "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/to-regex-range": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5108,8 +4072,6 @@
},
"node_modules/ts-jest": {
"version": "29.4.0",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.0.tgz",
- "integrity": "sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5161,8 +4123,6 @@
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5174,8 +4134,6 @@
},
"node_modules/ts-jest/node_modules/type-fest": {
"version": "4.41.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
- "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
@@ -5187,8 +4145,6 @@
},
"node_modules/ts-loader": {
"version": "9.5.2",
- "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz",
- "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5208,8 +4164,6 @@
},
"node_modules/ts-loader/node_modules/semver": {
"version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5221,8 +4175,6 @@
},
"node_modules/ts-loader/node_modules/source-map": {
"version": "0.7.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
- "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -5231,15 +4183,11 @@
},
"node_modules/tslib": {
"version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-detect": {
"version": "4.0.8",
- "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
- "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5248,8 +4196,6 @@
},
"node_modules/type-fest": {
"version": "0.21.3",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
- "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
@@ -5261,8 +4207,6 @@
},
"node_modules/typescript": {
"version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5275,15 +4219,11 @@
},
"node_modules/undici-types": {
"version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
"version": "1.11.0",
- "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.0.tgz",
- "integrity": "sha512-uw3hCGO/RdAEAb4zgJ3C/v6KIAFFOtBoxR86b2Ejc5TnH7HrhTWJR2o0A9ullC3eWMegKQCw/arQ/JivywQzkg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -5317,8 +4257,6 @@
},
"node_modules/update-browserslist-db": {
"version": "1.1.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
- "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true,
"funding": [
{
@@ -5348,8 +4286,6 @@
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
- "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
- "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5363,8 +4299,6 @@
},
"node_modules/walker": {
"version": "1.0.8",
- "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
- "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -5373,8 +4307,6 @@
},
"node_modules/watchpack": {
"version": "2.4.4",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
- "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5387,8 +4319,6 @@
},
"node_modules/webpack": {
"version": "5.99.9",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz",
- "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5435,8 +4365,6 @@
},
"node_modules/webpack-cli": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz",
- "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5478,8 +4406,6 @@
},
"node_modules/webpack-cli/node_modules/commander": {
"version": "12.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
- "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5488,8 +4414,6 @@
},
"node_modules/webpack-merge": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
- "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5503,8 +4427,6 @@
},
"node_modules/webpack-sources": {
"version": "3.3.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
- "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5513,8 +4435,6 @@
},
"node_modules/which": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5529,15 +4449,11 @@
},
"node_modules/wildcard": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
- "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5555,8 +4471,6 @@
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5573,8 +4487,6 @@
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5583,15 +4495,11 @@
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5605,8 +4513,6 @@
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5618,8 +4524,6 @@
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
- "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5631,15 +4535,11 @@
},
"node_modules/wrappy": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true,
"license": "ISC"
},
"node_modules/write-file-atomic": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
- "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5652,8 +4552,6 @@
},
"node_modules/write-file-atomic/node_modules/signal-exit": {
"version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5665,8 +4563,6 @@
},
"node_modules/y18n": {
"version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5675,15 +4571,11 @@
},
"node_modules/yallist": {
"version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/yargs": {
"version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5701,8 +4593,6 @@
},
"node_modules/yargs-parser": {
"version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
@@ -5711,8 +4601,6 @@
},
"node_modules/yargs/node_modules/ansi-regex": {
"version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5721,15 +4609,11 @@
},
"node_modules/yargs/node_modules/emoji-regex": {
"version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/yargs/node_modules/string-width": {
"version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5743,8 +4627,6 @@
},
"node_modules/yargs/node_modules/strip-ansi": {
"version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5756,8 +4638,6 @@
},
"node_modules/yocto-queue": {
"version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/scripts/build-website.sh b/scripts/build-website.sh
index c55f95f..734d0d4 100755
--- a/scripts/build-website.sh
+++ b/scripts/build-website.sh
@@ -2,7 +2,9 @@
set -e
+which wasm-pack || cargo install wasm-pack
wasm-pack build --target web --features wasm
+
cd reconcile-js
npm ci
npm run build
diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh
index 2803cbd..4163c85 100755
--- a/scripts/bump-version.sh
+++ b/scripts/bump-version.sh
@@ -25,8 +25,12 @@ else
fi
echo "Bumping versions"
+
+which cargo-set-version || cargo install cargo-edit
cargo set-version --bump $1
+which wasm-pack || cargo install wasm-pack
+
wasm-pack build --target web --features wasm
cd reconcile-js
diff --git a/scripts/lint.sh b/scripts/lint.sh
index 480d85d..358449a 100755
--- a/scripts/lint.sh
+++ b/scripts/lint.sh
@@ -2,13 +2,18 @@
set -e
+which cargo-machete || cargo install cargo-machete
+cargo machete
+
cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged
cargo fmt --all
cd reconcile-js
+npm ci
npm run format
cd ../examples/website
+npm ci
npm run format
echo "Success!"
diff --git a/scripts/test.sh b/scripts/test.sh
index fb25d3c..b7678ee 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -2,6 +2,15 @@
set -e
+which cargo-insta || cargo install cargo-insta
+which wasm-pack || cargo install wasm-pack
+
+node_version=$(node --version | cut -d'.' -f1 | tr -d 'v')
+if [ "$node_version" != "22" ]; then
+ echo "Error: Node.js version 22 is required, but found version $node_version"
+ exit 1
+fi
+
wasm-pack build --target web --features wasm,console_error_panic_hook
cargo test --verbose --features serde -- --include-ignored
@@ -13,7 +22,8 @@ cargo test --features all
wasm-pack test --node --features wasm,console_error_panic_hook
cd reconcile-js
-npm install
+npm ci
+npm run build
npm run test
cd -
diff --git a/src/lib.rs b/src/lib.rs
index 2119bea..654daa7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -187,7 +187,7 @@
//! original,
//! deserialized,
//! &*BuiltinTokenizer::Word
-//! );
+//! ).unwrap();
//! assert_eq!(
//! reconstructed.apply().text(),
//! "Merging text is easy with reconcile!"
@@ -215,11 +215,11 @@ mod tokenizer;
mod types;
mod utils;
-pub use operation_transformation::{EditedText, reconcile};
+pub use operation_transformation::{DiffError, EditedText, reconcile};
pub use tokenizer::{BuiltinTokenizer, Tokenizer, token::Token};
pub use types::{
- cursor_position::CursorPosition, history::History, number_or_string::NumberOrString,
- side::Side, span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
+ cursor_position::CursorPosition, history::History, number_or_text::NumberOrText, side::Side,
+ span_with_history::SpanWithHistory, text_with_cursors::TextWithCursors,
};
#[cfg(feature = "wasm")]
diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs
index 85e3995..bc01e34 100644
--- a/src/operation_transformation.rs
+++ b/src/operation_transformation.rs
@@ -1,8 +1,10 @@
+mod diff_error;
mod edited_text;
mod operation;
mod utils;
use std::fmt::Debug;
+pub use diff_error::DiffError;
pub use edited_text::EditedText;
pub use operation::Operation;
diff --git a/src/operation_transformation/diff_error.rs b/src/operation_transformation/diff_error.rs
new file mode 100644
index 0000000..d10065f
--- /dev/null
+++ b/src/operation_transformation/diff_error.rs
@@ -0,0 +1,19 @@
+use thiserror::Error;
+
+/// Error type for invalid diff operations
+#[derive(Error, Debug, Clone, PartialEq)]
+pub enum DiffError {
+ /// The diff references a range that exceeds the original text length
+ #[error(
+ "Invalid diff: attempting to access {requested} characters starting at position \
+ {position}, but original text only has {available} characters remaining"
+ )]
+ LengthExceedsOriginal {
+ /// The position where the operation starts
+ position: usize,
+ /// The number of characters requested
+ requested: usize,
+ /// The number of characters available from the position
+ available: usize,
+ },
+}
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index f27fea4..1f41cb1 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -6,13 +6,13 @@ use serde::{Deserialize, Serialize};
use crate::{
BuiltinTokenizer, CursorPosition, TextWithCursors,
operation_transformation::{
- Operation,
+ DiffError, Operation,
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
},
raw_operation::RawOperation,
tokenizer::Tokenizer,
types::{
- history::History, number_or_string::NumberOrString, side::Side,
+ history::History, number_or_text::NumberOrText, side::Side,
span_with_history::SpanWithHistory,
},
utils::string_builder::StringBuilder,
@@ -366,8 +366,8 @@ where
///
/// Panics if there's an integer overflow in i64.
#[must_use]
- pub fn to_diff(&self) -> Vec {
- let mut result: Vec = Vec::with_capacity(self.operations.len());
+ pub fn to_diff(&self) -> Vec {
+ let mut result: Vec = Vec::with_capacity(self.operations.len());
let mut previous_equal: Option = None;
for operation in &self.operations {
@@ -382,7 +382,7 @@ where
Operation::Insert { text, .. } => {
if let Some(prev_length) = previous_equal {
- result.push(NumberOrString::Number(
+ result.push(NumberOrText::Number(
i64::try_from(prev_length).expect("prev_length must fit in i64"),
));
previous_equal = None;
@@ -392,7 +392,7 @@ where
.iter()
.map(super::super::tokenizer::token::Token::original)
.collect();
- result.push(NumberOrString::Text(text));
+ result.push(NumberOrText::Text(text));
}
Operation::Delete {
@@ -400,7 +400,7 @@ where
..
} => {
if let Some(prev_length) = previous_equal {
- result.push(NumberOrString::Number(
+ result.push(NumberOrText::Number(
i64::try_from(prev_length).expect("prev_length must fit in i64"),
));
previous_equal = None;
@@ -408,13 +408,13 @@ where
let count = i64::try_from(*deleted_character_count)
.expect("deleted_character_count must fit in i64");
- result.push(NumberOrString::Number(-count));
+ result.push(NumberOrText::Number(-count));
}
}
}
if let Some(prev_length) = previous_equal {
- result.push(NumberOrString::Number(
+ result.push(NumberOrText::Number(
i64::try_from(prev_length).expect("prev_length must fit in i64"),
));
}
@@ -424,23 +424,38 @@ where
/// Deserialize an `EditedText` from a change list and the original text.
///
+ /// # Errors
+ ///
+ /// Returns `DiffError::LengthExceedsOriginal` if the diff references a
+ /// range that exceeds the original text length.
+ ///
/// # Panics
///
/// Panics if there's an integer overflow in i64.
- #[must_use]
pub fn from_diff(
original_text: &'a str,
- diff: Vec,
+ diff: Vec,
tokenizer: &Tokenizer,
- ) -> EditedText<'a, T> {
+ ) -> Result, DiffError> {
let mut operations: Vec> = Vec::with_capacity(diff.len());
let mut order = 0;
for item in diff {
match item {
- NumberOrString::Number(length) => {
+ NumberOrText::Number(length) => {
if length >= 0 {
let length = usize::try_from(length).expect("length must fit in usize");
+
+ // Validate that the range doesn't exceed the original text
+ let text_length = original_text.chars().count();
+ if order + length > text_length {
+ return Err(DiffError::LengthExceedsOriginal {
+ position: order,
+ requested: length,
+ available: text_length.saturating_sub(order),
+ });
+ }
+
let original_characters: String =
original_text.chars().skip(order).take(length).collect();
@@ -453,11 +468,22 @@ where
} else {
let length =
usize::try_from(-length).expect("negative length must fit in usize");
+
+ // Validate that the delete range doesn't exceed the original text
+ let text_length = original_text.chars().count();
+ if order + length > text_length {
+ return Err(DiffError::LengthExceedsOriginal {
+ position: order,
+ requested: length,
+ available: text_length.saturating_sub(order),
+ });
+ }
+
operations.push(Operation::create_delete(order, length));
order += length;
}
}
- NumberOrString::Text(text) => {
+ NumberOrText::Text(text) => {
let tokens = tokenizer(&text);
operations.push(Operation::create_insert(order, tokens));
}
@@ -465,12 +491,12 @@ where
}
let operation_count = operations.len();
- EditedText::new(
+ Ok(EditedText::new(
original_text,
operations,
vec![Side::Left; operation_count],
vec![],
- )
+ ))
}
}
@@ -520,6 +546,49 @@ mod tests {
assert_eq!(operations.apply().text(), expected);
}
+ #[test]
+ fn test_from_diff_length_exceeds_original() {
+ let result = EditedText::from_diff(
+ "hello",
+ vec![
+ 10.into(), // too large equal span - should error
+ " world".into(),
+ ],
+ &*BuiltinTokenizer::Word,
+ );
+
+ assert!(result.is_err());
+ match result {
+ Err(DiffError::LengthExceedsOriginal {
+ position,
+ requested,
+ available,
+ }) => {
+ assert_eq!(position, 0);
+ assert_eq!(requested, 10);
+ assert_eq!(available, 5);
+ }
+ _ => panic!("Expected LengthExceedsOriginal error"),
+ }
+ }
+
+ #[test]
+ fn test_from_diff_valid() {
+ let edited_text = EditedText::from_diff(
+ "hello",
+ vec![
+ 5.into(), // exact length
+ " world".into(),
+ ],
+ &*BuiltinTokenizer::Word,
+ )
+ .unwrap();
+
+ let content = edited_text.apply().text();
+
+ assert_eq!(content, "hello world");
+ }
+
#[cfg(feature = "serde")]
#[test]
fn test_changes_deserialisation() {
@@ -542,7 +611,7 @@ mod tests {
let changes = edited_text.to_diff();
let deserialized_edited_text =
- EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word).unwrap();
assert_eq!(deserialized_edited_text.apply().text(), updated);
}
diff --git a/src/types.rs b/src/types.rs
index b5c2f7c..03bb7b6 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -1,6 +1,6 @@
pub mod cursor_position;
pub mod history;
-pub mod number_or_string;
+pub mod number_or_text;
pub mod side;
pub mod span_with_history;
pub mod text_with_cursors;
diff --git a/src/types/number_or_string.rs b/src/types/number_or_text.rs
similarity index 68%
rename from src/types/number_or_string.rs
rename to src/types/number_or_text.rs
index 2bb1531..bb42bc7 100644
--- a/src/types/number_or_string.rs
+++ b/src/types/number_or_text.rs
@@ -1,4 +1,4 @@
-use std::fmt::Debug;
+use std::{borrow::Cow, fmt::Debug};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
@@ -12,18 +12,18 @@ const INTEGRAL_LIMIT: f64 = (1u64 << 53) as f64;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
#[derive(Debug, Clone, PartialEq)]
-pub enum NumberOrString {
+pub enum NumberOrText {
Number(i64),
Text(String),
}
#[cfg(feature = "wasm")]
-impl TryFrom for NumberOrString {
+impl TryFrom for NumberOrText {
type Error = DeserialisationError;
fn try_from(value: JsValue) -> Result {
if let Ok(num) = value.clone().try_into() {
- return Ok(NumberOrString::Number(num));
+ return Ok(NumberOrText::Number(num));
}
if let Some(num) = value.clone().as_f64() {
@@ -34,11 +34,11 @@ impl TryFrom for NumberOrString {
}
#[allow(clippy::cast_possible_truncation)]
- return Ok(NumberOrString::Number(num.round() as i64));
+ return Ok(NumberOrText::Number(num.round() as i64));
}
if let Ok(text) = value.try_into() {
- return Ok(NumberOrString::Text(text));
+ return Ok(NumberOrText::Text(text));
}
Err(DeserialisationError::new(
@@ -48,15 +48,31 @@ impl TryFrom for NumberOrString {
}
#[cfg(feature = "wasm")]
-impl From for JsValue {
- fn from(value: NumberOrString) -> Self {
+impl From for JsValue {
+ fn from(value: NumberOrText) -> Self {
match value {
- NumberOrString::Number(num) => JsValue::from(num),
- NumberOrString::Text(text) => JsValue::from(text),
+ NumberOrText::Number(num) => JsValue::from(num),
+ NumberOrText::Text(text) => JsValue::from(text),
}
}
}
+impl From for NumberOrText {
+ fn from(value: i64) -> Self { NumberOrText::Number(value) }
+}
+
+impl From for NumberOrText {
+ fn from(value: String) -> Self { NumberOrText::Text(value) }
+}
+
+impl From<&str> for NumberOrText {
+ fn from(value: &str) -> Self { NumberOrText::Text(value.to_owned()) }
+}
+
+impl<'a> From> for NumberOrText {
+ fn from(value: Cow<'a, str>) -> Self { NumberOrText::Text(value.into_owned()) }
+}
+
/// Error type for deserialisation failures
#[cfg(feature = "wasm")]
#[derive(Debug, Clone)]
diff --git a/src/wasm.rs b/src/wasm.rs
index 1b7a24b..6fc02f2 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -105,16 +105,17 @@ pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer
pub fn undiff(parent: &str, diff: Vec, tokenizer: BuiltinTokenizer) -> String {
set_panic_hook();
- EditedText::from_diff(
+ match EditedText::from_diff(
parent,
diff.into_iter()
.map(std::convert::TryInto::try_into)
.collect::>()
.expect("Invalid diff format"),
&*tokenizer,
- )
- .apply()
- .text()
+ ) {
+ Ok(edited_text) => edited_text.apply().text(),
+ Err(e) => panic!("{}", e),
+ }
}
fn set_panic_hook() {
diff --git a/tests/test.rs b/tests/test.rs
index 2b14b86..2e54e6c 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -57,9 +57,9 @@ fn test_document_one_way_with_serialisation() {
.unwrap();
let restored_left_operations =
- EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word).unwrap();
let restored_right_operations =
- EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word);
+ EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word).unwrap();
doc.assert_eq_without_cursors(
&restored_left_operations
From 5962feb90a9f224a8423dde8a264758f52407d60 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Sat, 6 Dec 2025 21:54:55 +0000
Subject: [PATCH 046/107] Bump versions to 0.8.0
---
Cargo.lock | 2 +-
Cargo.toml | 2 +-
examples/website/package-lock.json | 2 +-
reconcile-js/package-lock.json | 6 +++---
reconcile-js/package.json | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index bb63c76..c512cbc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -237,7 +237,7 @@ dependencies = [
[[package]]
name = "reconcile-text"
-version = "0.7.1"
+version = "0.8.0"
dependencies = [
"console_error_panic_hook",
"insta",
diff --git a/Cargo.toml b/Cargo.toml
index 733b8d1..83aff23 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "reconcile-text"
description = "Intelligent 3-way text merging with automated conflict resolution"
-version = "0.7.1"
+version = "0.8.0"
rust-version = "1.85"
authors = ["Andras Schmelczer "]
edition = "2024"
diff --git a/examples/website/package-lock.json b/examples/website/package-lock.json
index 98e8eb5..97002d9 100644
--- a/examples/website/package-lock.json
+++ b/examples/website/package-lock.json
@@ -28,7 +28,7 @@
},
"../../reconcile-js": {
"name": "reconcile-text",
- "version": "0.7.1",
+ "version": "0.8.0",
"dev": true,
"license": "MIT",
"devDependencies": {
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index e9fa242..a2fbafa 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "reconcile-text",
- "version": "0.7.1",
+ "version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "reconcile-text",
- "version": "0.7.1",
+ "version": "0.8.0",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
@@ -24,7 +24,7 @@
},
"../pkg": {
"name": "reconcile-text",
- "version": "0.7.1",
+ "version": "0.8.0",
"dev": true,
"license": "MIT"
},
diff --git a/reconcile-js/package.json b/reconcile-js/package.json
index da0c4fc..451d6fe 100644
--- a/reconcile-js/package.json
+++ b/reconcile-js/package.json
@@ -1,6 +1,6 @@
{
"name": "reconcile-text",
- "version": "0.7.1",
+ "version": "0.8.0",
"description": "Intelligent 3-way text merging with automated conflict resolution",
"main": "dist/reconcile.node.js",
"browser": "dist/reconcile.web.js",
From 3d382ad7415c1a8d5945cb8524763515ba0e501a Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:29:35 +0000
Subject: [PATCH 047/107] Improve docs and compare with alternatives
---
README.md | 56 ++++++++++++++++++---
docs/advanced-ts.md | 4 +-
src/lib.rs | 5 +-
src/operation_transformation/edited_text.rs | 39 ++++++--------
src/operation_transformation/operation.rs | 23 ++++-----
src/raw_operation.rs | 4 +-
src/tokenizer.rs | 2 +-
src/tokenizer/token.rs | 10 ++--
src/types/history.rs | 3 +-
src/types/number_or_text.rs | 4 ++
src/types/span_with_history.rs | 3 +-
src/types/text_with_cursors.rs | 5 +-
src/utils/string_builder.rs | 7 ++-
src/wasm.rs | 10 ++--
14 files changed, 106 insertions(+), 69 deletions(-)
diff --git a/README.md b/README.md
index 7925ed5..bdb6c39 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ Alternatively, add `reconcile-text` to your `Cargo.toml`:
```toml
[dependencies]
-reconcile-text = "0.5"
+reconcile-text = "0.8"
```
Then start merging:
@@ -52,7 +52,7 @@ let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::
assert_eq!(result.apply().text(), "Hi beautiful world");
```
-See the [merge-file example](examples/merge-file.rs) for another example or the [library's documentation](https://docs.rs/reconcile-text/latest/reconcile_text).
+See the [merge-file example](examples/merge-file.rs) for another example, or the [library's documentation](https://docs.rs/reconcile-text/latest/reconcile_text).
### JavaScript/TypeScript
@@ -77,7 +77,7 @@ const result = reconcile(parent, left, right);
console.log(result.text); // "Hi beautiful world"
```
-See the [example website source](examples/website/src/index.ts) for a more complex example or the [advanced examples document](https://github.com/schmelczer/reconcile/blob/main/docs/advanced-ts.md).
+See the [example website source](examples/website/src/index.ts) for a more complex example, or the [advanced examples document](https://github.com/schmelczer/reconcile/blob/main/docs/advanced-ts.md).
## Motivation
@@ -87,13 +87,13 @@ This creates **Differential Synchronisation** scenarios ([2], [3]): we only know
> **Note**: Some text domains require more careful handling. Legal contracts, for instance, could have unintended meaning changes from conflicting edits that create double negations. At the same time, semantic conflicts can still arise when merging code, even in the absence of syntactic conflicts.
-Differential sync is implemented by [universal-sync](https://github.com/invisible-college/universal-sync) and my Obsidian plugin [vault-link](https://github.com/schmelczer/vault-link), and it requires a merging tool which creates conflict-free results for the best user experience.
+Differential sync is implemented by [universal-sync](https://github.com/invisible-college/universal-sync) and my Obsidian plugin [vault-link](https://github.com/schmelczer/vault-link), and it requires a merging tool that creates conflict-free results for the best user experience.
## How it works
`reconcile-text` starts off similarly to `diff3` ([4], [5]) but adds automated conflict resolution. Given a **parent** document and two modified versions (`left` and `right`), the following happens:
-1. **Tokenisation** — Input texts get split into meaningful units (words, characters, etc.) for granular merging
+1. **Tokenisation** — Input texts are split into meaningful units (words, characters, etc.) for granular merging
2. **Diff computation** — Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
3. **Diff optimisation** — Operations are reordered and consolidated to maximise chained changes
4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications and updating cursors
@@ -102,6 +102,48 @@ Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides a
However, when only the end result of concurrent changes is observable, merge quality depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations cannot be supported because Myers' algorithm decomposes them into separate `insert` and `delete` operations, regardless of the merging algorithm used.
+## Comparison with other approaches
+
+### Traditional 3-way merge (diff3, Git)
+
+Tools like `diff3` ([4]) and Git produce **conflict markers** (`<<<<<<<` / `=======` / `>>>>>>>`) when both sides modify the same region. This works for source code where a human must verify correctness, but breaks the reading flow for prose. `reconcile-text` uses the same diff3-like foundation but adds an OT-inspired resolution step that eliminates conflict markers entirely. Libraries like [diffy](https://crates.io/crates/diffy), [merge3](https://github.com/breezy-team/merge3-rs) (Rust), and [node-diff3](https://github.com/bhousel/node-diff3) (JavaScript) all fall into this category.
+
+### diff-match-patch
+
+[diff-match-patch](https://github.com/google/diff-match-patch) ([6]) is a widely-used library created by Neil Fraser at Google in 2006, providing character-level diffing (Myers' algorithm), fuzzy string matching (Bitap algorithm), and patch application. It powers Fraser's **Differential Synchronisation** protocol ([2]): compute a diff between two texts, apply the patch to a third text that may have drifted, and repeat until convergence. If a patch fails, the failure self-corrects in the next sync cycle.
+
+The key differences from `reconcile-text`:
+
+- **2-way vs 3-way** — diff-match-patch diffs two texts and applies the result as a patch. It has no concept of a common ancestor and cannot reason about "left changes" vs "right changes". `reconcile-text` performs true 3-way merging, understanding the intent behind each side's edits.
+
+- **Character-level only** — Word-level and line-level diffs require encoding tokens as single Unicode characters before diffing ([7]). `reconcile-text` supports word, character, line, and custom tokenisation natively.
+
+- **Patches can fail** — `patch_apply` returns a boolean array indicating success per patch; failed patches are silently dropped. In Differential Synchronisation, failures self-correct in the next cycle, but for one-shot merges edits can be lost. `reconcile-text` always produces a complete merged result.
+
+- **No cursor tracking or change provenance** — diff-match-patch does not reposition cursors or track which side made which edit. `reconcile-text` does both automatically.
+
+See the [comparison example](examples/compare-with-diff-match-patch.rs) for concrete cases where diff-match-patch garbles adjacent edits and silently drops an entire sentence, while `reconcile-text` merges both users' changes correctly.
+
+> **When to use diff-match-patch instead**: when you don't have a common ancestor—for example, synchronising texts that have diverged through an unknown sequence of edits. If you have a common ancestor (as in most version control and collaborative editing scenarios), `reconcile-text` produces more reliable results.
+
+### CRDTs (Yjs, Automerge, Loro, diamond-types)
+
+Conflict-free Replicated Data Types guarantee convergence by mathematical construction: every operation commutes, so the order of application doesn't matter. Libraries like [Yjs](https://github.com/yjs/yjs) (and its Rust port [Yrs](https://github.com/y-crdt/y-crdt)), [Automerge](https://github.com/automerge/automerge), [Loro](https://github.com/loro-dev/loro), [cola](https://github.com/nomad/cola), and [diamond-types](https://github.com/josephg/diamond-types) implement this approach.
+
+CRDTs capture every individual keystroke or operation, assigning each a unique identity. This makes them ideal when you control the complete editing infrastructure: the editor, the transport layer, and the storage format. They work peer-to-peer, handle arbitrary numbers of concurrent editors, and never lose an edit.
+
+The trade-off is that CRDTs require **maintaining document state over time**—an operation log or internal data structure that grows with the document's edit history. You cannot simply hand a CRDT library three plain strings and get a merged result. This makes them unsuitable for Differential Synchronisation scenarios where you only observe the final state of each document, which is exactly the niche `reconcile-text` fills.
+
+> **When to use CRDTs instead**: if you control the complete editing stack and can capture every operation as it happens, CRDTs provide stronger convergence guarantees. They also support more than two concurrent editors naturally, whereas `reconcile-text` merges exactly two forks at a time (though merges can be chained).
+
+### Operational Transformation (OT)
+
+OT libraries like [ot.js](https://ot.js.org/) and [ShareJS](https://github.com/josephg/ShareJS) transform concurrent operations against each other so that applying them in any order produces the same result. Like CRDTs, they capture individual operations and require infrastructure to coordinate them—typically a central server that determines the canonical operation order.
+
+`reconcile-text` borrows the *concept* of OT (transforming one side's edits against the other) but applies it to a different problem. Instead of transforming individual keystrokes in real time, it transforms the consolidated diff output of two complete edits. This means it doesn't need a server, doesn't need to capture operations as they happen, and works entirely offline.
+
+> **When to use OT instead**: if you need real-time collaboration with sub-second latency and can run a coordination server, dedicated OT libraries handle this well. `reconcile-text` is designed for merge points, not live keystroke-by-keystroke synchronisation.
+
## Development
Contributions are welcome!
@@ -142,8 +184,10 @@ Install [rustup](https://rustup.rs):
[MIT](./LICENSE)
-[1]:https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html
+[1]: https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html
[2]: https://neil.fraser.name/writing/sync/
[3]: https://www.cis.upenn.edu/~bcpierce/papers/diff3-short.pdf
[4]: https://blog.jcoglan.com/2017/05/08/merging-with-diff3/
[5]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf
+[6]: https://github.com/google/diff-match-patch
+[7]: https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs
diff --git a/docs/advanced-ts.md b/docs/advanced-ts.md
index 5ecf065..0480bd4 100644
--- a/docs/advanced-ts.md
+++ b/docs/advanced-ts.md
@@ -40,7 +40,7 @@ console.log(result.history); /*
## Tokenisation Strategies
-Reconcile offers different approaches to split text for merging:
+`reconcile-text` offers different approaches to split text for merging:
- **Word tokeniser** (`"Word"`) — Splits on word boundaries (recommended for prose)
- **Character tokeniser** (`"Character"`) — Individual characters (fine-grained control)
@@ -48,7 +48,7 @@ Reconcile offers different approaches to split text for merging:
## Cursor Tracking
-Reconcile automatically tracks cursor positions through merges, which is handy in collaborative editors. Selections can be tracked by providing them as a pair of cursors.
+`reconcile-text` automatically tracks cursor positions through merges, which is useful for collaborative editors. Selections can be tracked by providing them as a pair of cursors.
```javascript
const result = reconcile(
diff --git a/src/lib.rs b/src/lib.rs
index 654daa7..e759cc2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -59,7 +59,7 @@
//!
//! For specialised use cases, such as structured languages, custom
//! tokenisation logic can be implemented by providing a function with the
-//! signature `Fn(&str) -> Vec>`::
+//! signature `Fn(&str) -> Vec>`:
//!
//! ```
//! use reconcile_text::{reconcile, Token, BuiltinTokenizer};
@@ -151,10 +151,11 @@
//! ]
//! );
//! ```
+//!
//! ## Efficiently serialize changes
//!
//! The edits can be serialized into a compact representation without the full
-//! original text, making the size only depends on the changes made.
+//! original text, making the size depend only on the changes made.
//!
//! ```rust
//! # #[cfg(feature = "serde")]
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 1f41cb1..bc30137 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -18,18 +18,16 @@ use crate::{
utils::string_builder::StringBuilder,
};
-/// A text document and a sequence of operations that can be applied to the text
-/// document. `EditedText` supports merging two sequences of operations using
-/// the principles of Operational Transformation.
+/// A text document with a sequence of operations derived from diffing it
+/// against an updated version. Supports merging two `EditedText` instances
+/// (from the same original) via Operational Transformation.
///
-/// It's mainly created through the `from_strings` method, then merged with
-/// another `EditedText` derived from the same original text and then applied to
-/// the original text to get the reconciled text of concurrent edits.
+/// Created via `from_strings`, `from_strings_with_tokenizer`, or `from_diff`,
+/// then merged with another `EditedText` and applied to get the reconciled
+/// text.
///
-/// In addition to text and operations, it also keeps track of cursor positions
-/// in the original text. The cursor positions are updated when the operations
-/// are applied, so that the cursor positions can be used to restore the
-/// cursor positions in the updated text.
+/// Also tracks cursor positions from the updated text, repositioning them
+/// when operations are applied.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct EditedText<'a, T>
@@ -43,12 +41,8 @@ where
}
impl<'a> EditedText<'a, String> {
- /// Create an `EditedText` from the given original (old) and updated (new)
- /// strings. The returned `EditedText` represents the changes from the
- /// original to the updated text. When the return value is applied to
- /// the original text, it will result in the updated text. The default
- /// word tokenizer is used to tokenize the text which splits the text on
- /// whitespaces.
+ /// Create an `EditedText` from the given original and updated strings.
+ /// Uses the default word tokenizer (splits on word boundaries).
#[must_use]
pub fn from_strings(original: &'a str, updated: &TextWithCursors) -> Self {
Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word)
@@ -59,11 +53,8 @@ impl<'a, T> EditedText<'a, T>
where
T: PartialEq + Clone + Debug,
{
- /// Create an `EditedText` from the given original (old) and updated (new)
- /// strings. The returned `EditedText` represents the changes from the
- /// original to the updated text. When the return value is applied to
- /// the original text, it will result in the updated text. The tokenizer
- /// function is used to tokenize the text.
+ /// Create an `EditedText` from the given original and updated strings
+ /// using the provided tokenizer.
pub fn from_strings_with_tokenizer(
original: &'a str,
updated: &TextWithCursors,
@@ -110,7 +101,7 @@ where
///
/// # Panics
///
- /// Panics if there's an integer overflow (in i64) when calculating new
+ /// Panics if there's an integer overflow (in isize) when calculating new
/// cursor positions.
#[must_use]
#[allow(clippy::too_many_lines)]
@@ -280,7 +271,7 @@ where
/// Apply the operations to the text and return the resulting text in chunks
/// together with the provenance describing where each chunk came from.
///
- /// The result includes deleted spans as well.
+ /// Returns all spans including deletions (not present in the merged text).
///
/// ```
/// use reconcile_text::{History, SpanWithHistory, BuiltinTokenizer, reconcile};
@@ -422,7 +413,7 @@ where
result
}
- /// Deserialize an `EditedText` from a change list and the original text.
+ /// Reconstruct an `EditedText` from a diff and the original text.
///
/// # Errors
///
diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs
index 7a8f92a..823a50b 100644
--- a/src/operation_transformation/operation.rs
+++ b/src/operation_transformation/operation.rs
@@ -46,9 +46,8 @@ impl Operation
where
T: PartialEq + Clone + Debug,
{
- /// Creates an equal operation with the given index.
- /// This operation is used to indicate that the text at the given index
- /// is unchanged.
+ /// Creates an equal (retain) operation starting at the given character
+ /// offset in the original text.
pub fn create_equal(order: usize, length: usize) -> Self {
Operation::Equal {
order,
@@ -69,13 +68,14 @@ where
}
}
- /// Creates an insert operation with the given index and text.
+ /// Creates an insert operation at the given character offset with the
+ /// given tokens.
pub fn create_insert(order: usize, text: Vec>) -> Self {
Operation::Insert { order, text }
}
- /// Creates a delete operation with the given index and number of
- /// to-be-deleted characters.
+ /// Creates a delete operation at the given character offset for the
+ /// specified number of characters.
pub fn create_delete(order: usize, deleted_character_count: usize) -> Self {
Operation::Delete {
order,
@@ -179,8 +179,8 @@ where
builder
}
- /// Returns the number of affected characters. It is always greater than 0
- /// because empty operations cannot be created.
+ /// Returns the number of affected characters. May be 0 after
+ /// `merge_operations`.
pub fn len(&self) -> usize {
match self {
Operation::Equal { length, .. } => *length,
@@ -192,10 +192,9 @@ where
}
}
- /// Merges the operation with the given context, producing a new operation
- /// and updating the context. This implements a comples FSM that handles
- /// the merging of operations in a way that is consistent with the text.
- /// The contexts are updated in-place.
+ /// Adjusts this operation based on `previous_operation` from the other side
+ /// to avoid duplicating or conflicting changes. Updates
+ /// `previous_operation` in-place.
#[allow(clippy::too_many_lines)]
pub fn merge_operations(self, previous_operation: &mut Option) -> Operation {
let operation = self;
diff --git a/src/raw_operation.rs b/src/raw_operation.rs
index aa3ab8f..1572e88 100644
--- a/src/raw_operation.rs
+++ b/src/raw_operation.rs
@@ -2,9 +2,9 @@ use std::fmt::Debug;
use crate::{tokenizer::token::Token, utils::myers_diff::myers_diff};
-/// Text editing operation containing the to-be-changed `Tokens`-s.
+/// Text editing operation containing the affected tokens.
///
-/// `RawOperations` can be joined together when the underlying tokens
+/// `RawOperation`s can be joined together when the underlying tokens
/// allow for joining subsequent operations.
#[derive(Debug, Clone, PartialEq)]
pub enum RawOperation
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
index 62ab528..fabafcd 100644
--- a/src/tokenizer.rs
+++ b/src/tokenizer.rs
@@ -12,7 +12,7 @@ use wasm_bindgen::prelude::*;
pub mod token;
-/// A trait for tokenizers that take a string and return a list of tokens.
+/// Type alias for tokenizer functions that split a string into tokens.
pub type Tokenizer = dyn Fn(&str) -> Vec>;
#[cfg_attr(feature = "wasm", wasm_bindgen)]
diff --git a/src/tokenizer/token.rs b/src/tokenizer/token.rs
index 58e6ab6..2f2dc82 100644
--- a/src/tokenizer/token.rs
+++ b/src/tokenizer/token.rs
@@ -3,13 +3,11 @@ use std::fmt::Debug;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
-/// A token is a string that has been normalized in some way.
+/// A token with a normalized form (used for diffing) and an original form
+/// (used when applying operations). Joinability flags control whether
+/// adjacent insertions interleave or group.
///
-/// A token consists of the normalized form is used for comparison, and the
-/// original form used for subsequently applying `Operation`-s to a text
-/// document.
-///
-/// It's UTF-8 compatible.
+/// UTF-8 compatible.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Token
diff --git a/src/types/history.rs b/src/types/history.rs
index e30ae90..431a481 100644
--- a/src/types/history.rs
+++ b/src/types/history.rs
@@ -15,8 +15,7 @@ pub enum History {
RemovedFromRight = "RemovedFromRight",
}
-/// Simple enum for describing the result of `reconcile` in a flat list.
-/// When compiled to WASM, the enum values are the same as their names.
+/// Provenance label for each span returned by `apply_with_history`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg(not(feature = "wasm"))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
diff --git a/src/types/number_or_text.rs b/src/types/number_or_text.rs
index bb42bc7..fe17af4 100644
--- a/src/types/number_or_text.rs
+++ b/src/types/number_or_text.rs
@@ -27,6 +27,10 @@ impl TryFrom for NumberOrText {
}
if let Some(num) = value.clone().as_f64() {
+ if num.is_nan() {
+ return Err(DeserialisationError::new("NaN is not a valid number"));
+ }
+
if num.abs() > INTEGRAL_LIMIT {
return Err(DeserialisationError::new(
"Floating-point number exceeds safe integer limit, use BigInt instead",
diff --git a/src/types/span_with_history.rs b/src/types/span_with_history.rs
index 09f778f..1e4481c 100644
--- a/src/types/span_with_history.rs
+++ b/src/types/span_with_history.rs
@@ -5,8 +5,7 @@ use wasm_bindgen::prelude::*;
use crate::types::history::History;
-/// Wrapper type for `(String, History)` where History describes the origin of
-/// `text`.
+/// A text span annotated with its origin in a merge result.
#[allow(clippy::unsafe_derive_deserialize)]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
diff --git a/src/types/text_with_cursors.rs b/src/types/text_with_cursors.rs
index c9dec89..8f4af19 100644
--- a/src/types/text_with_cursors.rs
+++ b/src/types/text_with_cursors.rs
@@ -12,12 +12,15 @@ pub struct TextWithCursors {
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl TextWithCursors {
+ /// # Panics
+ ///
+ /// Panics if any cursor's `char_index` exceeds the text's character length.
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
#[must_use]
pub fn new(text: String, cursors: Vec) -> Self {
let length = text.chars().count();
for cursor in &cursors {
- debug_assert!(
+ assert!(
cursor.char_index <= length,
// cursor.char_index == length means that the cursor is at the end
"Cursor positions ({}) must be contained within the text (of length {length}) or \
diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs
index 34110d8..40928c3 100644
--- a/src/utils/string_builder.rs
+++ b/src/utils/string_builder.rs
@@ -1,9 +1,8 @@
use std::{fmt, iter::Iterator};
-/// A helper for building a string in-order based on an original string and a
-/// series of insertions, deletions, and copies applied to it. It is safe to use
-/// with UTF-8 strings as all operations are based on character indices. The
-/// methods must be called in-order.
+/// A helper for building a string sequentially from an original string via
+/// insertions, deletions, and copies. All operations use character counts,
+/// safe for UTF-8. Methods must be called in-order.
pub struct StringBuilder<'a> {
original: Box + 'a>,
buffer: String,
diff --git a/src/wasm.rs b/src/wasm.rs
index 6fc02f2..959f1a6 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -22,7 +22,7 @@ pub fn reconcile(
crate::reconcile(parent, left, right, &*tokenizer).apply()
}
-/// WASM wrapper around `crate::reconcile` for merging text.
+/// WASM wrapper around `crate::reconcile` that also returns provenance history.
#[wasm_bindgen(js_name = reconcileWithHistory)]
#[must_use]
pub fn reconcile_with_history(
@@ -94,12 +94,12 @@ pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer
.collect()
}
-/// Inverse of `diff`, applies a compact diff representation to a parent text
+/// Inverse of `diff`, applies a compact diff representation to a parent text.
///
-/// # Panics
+/// # Errors
///
-/// Panics if the diff format is invalid or there's an integer overflow when
-/// applying the diff.
+/// Returns a JS error if the diff format is invalid or references ranges
+/// exceeding the original text length.
#[wasm_bindgen(js_name = undiff)]
#[must_use]
pub fn undiff(parent: &str, diff: Vec, tokenizer: BuiltinTokenizer) -> String {
From b012330a365bcd59b3b1233bf9bd2e8ec3b63182 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:31:31 +0000
Subject: [PATCH 048/107] Add comparison example
---
Cargo.toml | 5 ++
examples/compare-with-diff-match-patch.rs | 92 +++++++++++++++++++++++
2 files changed, 97 insertions(+)
create mode 100644 examples/compare-with-diff-match-patch.rs
diff --git a/Cargo.toml b/Cargo.toml
index 83aff23..ba89963 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,10 @@ crate-type = ["cdylib", "rlib"]
name = "merge-file"
path = "examples/merge-file.rs"
+[[example]]
+name = "compare-with-diff-match-patch"
+path = "examples/compare-with-diff-match-patch.rs"
+
[dependencies]
serde = { version = "1.0.219", optional = true, features = ["derive"] }
thiserror = "2.0.17"
@@ -48,6 +52,7 @@ serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"
test-case = "3.3.1"
wasm-bindgen-test = "0.3.56"
+diff-match-patch-rs = "0.5"
[profile.release]
codegen-units = 1
diff --git a/examples/compare-with-diff-match-patch.rs b/examples/compare-with-diff-match-patch.rs
new file mode 100644
index 0000000..f748e6f
--- /dev/null
+++ b/examples/compare-with-diff-match-patch.rs
@@ -0,0 +1,92 @@
+use std::panic;
+
+use diff_match_patch_rs::{Compat, DiffMatchPatch, PatchInput};
+use reconcile_text::{BuiltinTokenizer, reconcile};
+
+fn dmp_merge(parent: &str, left: &str, right: &str) -> Option {
+ let parent = parent.to_owned();
+ let left = left.to_owned();
+ let right = right.to_owned();
+
+ // diff-match-patch-rs can panic on some inputs, so we catch that.
+ panic::catch_unwind(|| {
+ let dmp = DiffMatchPatch::new();
+ let diffs = dmp.diff_main::(&parent, &left).ok()?;
+ let patches = dmp
+ .patch_make(PatchInput::new_text_diffs(&parent, &diffs))
+ .ok()?;
+ let (result, _) = dmp.patch_apply(&patches, &right).ok()?;
+ Some(result)
+ })
+ .ok()
+ .flatten()
+}
+
+fn try_merge(parent: &str, left: &str, right: &str) {
+ let dmp_result = dmp_merge(parent, left, right);
+
+ let reconcile_result = reconcile(
+ parent,
+ &left.into(),
+ &right.into(),
+ &*BuiltinTokenizer::Word,
+ )
+ .apply()
+ .text();
+
+ println!("Parent: {parent:?}");
+ println!("Left: {left:?}");
+ println!("Right: {right:?}");
+ println!();
+ match dmp_result {
+ Some(r) => println!("diff-match-patch: {r:?}"),
+ None => println!("diff-match-patch: "),
+ }
+ println!("reconcile-text: {reconcile_result:?}");
+ println!();
+}
+
+/// Demonstrates cases where diff-match-patch silently produces incorrect
+/// output, while reconcile-text preserves both users' edits correctly.
+///
+/// Run it with:
+/// `cargo run --example compare-with-diff-match-patch`
+fn main() {
+ // Example 1
+ // Two users edit the same short phrase. Alice replaces "old(!)" with
+ // "new improved", Bob replaces "broken" with "working". These are
+ // independent changes to adjacent words.
+ //
+ // diff-match-patch has no common ancestor, so it diffs parent → left
+ // and applies the patch to right. The character-level patches overlap
+ // and produce garbled text ("impovind"). It reports success.
+ //
+ // reconcile-text sees both changes relative to the parent and merges
+ // them cleanly.
+
+ println!("── Example 1: adjacent edits ──");
+ try_merge(
+ "old(!) broken code",
+ "new improved code",
+ "old(!) working code",
+ );
+
+ // Example 2
+ // Alice adds a sentence. Bob rewrites the surrounding text. Because
+ // diff-match-patch works without a common ancestor, Alice's entire
+ // sentence is silently lost.
+
+ println!("── Example 2: sentence lost ──");
+ // Alice adds a sentence in the middle of a paragraph. Bob rephrases
+ // the same paragraph. Because the patch context from Alice's edit no
+ // longer appears in Bob's version, diff-match-patch silently drops
+ // Alice's entire sentence.
+ //
+ // reconcile-text understands both edits relative to the common ancestor
+ // and keeps both.
+ try_merge(
+ "We used the existing parsing approach for processing. The output was saved to the database.",
+ "We used the existing parsing approach for processing. Always validate the schema! The output was saved to the database.",
+ "We adopted a brand new analysis pipeline for execution. The results were written to cloud storage.",
+ );
+}
From 776571bc5e90aedf4c89852b8b0e3c3e6278291f Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:34:07 +0000
Subject: [PATCH 049/107] Return result instead of panicking
---
src/wasm.rs | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/src/wasm.rs b/src/wasm.rs
index 959f1a6..809bf79 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -101,21 +101,22 @@ pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer
/// Returns a JS error if the diff format is invalid or references ranges
/// exceeding the original text length.
#[wasm_bindgen(js_name = undiff)]
-#[must_use]
-pub fn undiff(parent: &str, diff: Vec, tokenizer: BuiltinTokenizer) -> String {
+pub fn undiff(
+ parent: &str,
+ diff: Vec,
+ tokenizer: BuiltinTokenizer,
+) -> Result {
set_panic_hook();
- match EditedText::from_diff(
- parent,
- diff.into_iter()
- .map(std::convert::TryInto::try_into)
- .collect::>()
- .expect("Invalid diff format"),
- &*tokenizer,
- ) {
- Ok(edited_text) => edited_text.apply().text(),
- Err(e) => panic!("{}", e),
- }
+ let parsed_diff: Vec<_> = diff
+ .into_iter()
+ .map(std::convert::TryInto::try_into)
+ .collect::>()
+ .map_err(|e: crate::types::number_or_text::DeserialisationError| -> JsValue { e.into() })?;
+
+ EditedText::from_diff(parent, parsed_diff, &*tokenizer)
+ .map(|edited_text| edited_text.apply().text())
+ .map_err(|e| JsValue::from_str(&e.to_string()))
}
fn set_panic_hook() {
From 3abc45cb867ba85400e324e09c32b2c805905daa Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:38:07 +0000
Subject: [PATCH 050/107] Add wasm leak detector & fix leak
---
reconcile-js/src/index.test.ts | 12 +++++
reconcile-js/src/index.ts | 18 ++++++--
reconcile-js/src/wasm-leak-detector.ts | 63 ++++++++++++++++++++++++++
3 files changed, 88 insertions(+), 5 deletions(-)
create mode 100644 reconcile-js/src/wasm-leak-detector.ts
diff --git a/reconcile-js/src/index.test.ts b/reconcile-js/src/index.test.ts
index 1a4394f..0de924c 100644
--- a/reconcile-js/src/index.test.ts
+++ b/reconcile-js/src/index.test.ts
@@ -1,10 +1,22 @@
import { reconcile, reconcileWithHistory, diff, undiff } from './index';
+import { installWasmLeakDetector, checkForWasmLeaks } from './wasm-leak-detector';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
+installWasmLeakDetector();
+
+afterEach(() => {
+ const leaks = checkForWasmLeaks();
+ if (leaks.length > 0) {
+ throw new Error(
+ `WASM memory leak: ${leaks.length} object(s) not freed:\n ${leaks.join('\n ')}`
+ );
+ }
+});
+
describe('reconcile', () => {
it('call reconcile without cursors', () => {
expect(reconcile('Hello', 'Hello world', 'Hi world').text).toEqual('Hi world');
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index 3fa998c..006e5d1 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -325,9 +325,15 @@ function toWasmCursorPosition({ id, position }: CursorPosition): wasmCursorPosit
}
function toTextWithCursors(textWithCursor: wasmTextWithCursors): TextWithCursors {
+ const wasmCursors = textWithCursor.cursors();
+ const cursors = wasmCursors.map(toCursorPosition);
+ for (const cursor of wasmCursors) {
+ cursor.free();
+ }
+
return {
text: textWithCursor.text(),
- cursors: textWithCursor.cursors().map(toCursorPosition),
+ cursors,
};
}
@@ -338,9 +344,11 @@ function toCursorPosition(cursor: wasmCursorPosition): CursorPosition {
};
}
-function toSpanWithHistory(textWithHistory: wasmSpanWithHistory): SpanWithHistory {
- return {
- text: textWithHistory.text(),
- history: textWithHistory.history(),
+function toSpanWithHistory(span: wasmSpanWithHistory): SpanWithHistory {
+ const result = {
+ text: span.text(),
+ history: span.history(),
};
+ span.free();
+ return result;
}
diff --git a/reconcile-js/src/wasm-leak-detector.ts b/reconcile-js/src/wasm-leak-detector.ts
new file mode 100644
index 0000000..a8bcec4
--- /dev/null
+++ b/reconcile-js/src/wasm-leak-detector.ts
@@ -0,0 +1,63 @@
+/**
+ * Test utility for detecting WASM memory leaks.
+ *
+ * wasm-bindgen registers every JS-side object with a `FinalizationRegistry`.
+ * This detector patches `FinalizationRegistry.prototype.register` to collect
+ * references to all WASM objects. After each test, {@link checkForWasmLeaks}
+ * inspects `__wbg_ptr` on every tracked object - a non-zero pointer means
+ * `.free()` was never called, i.e. a leak.
+ *
+ * Install once (before any WASM calls) and call {@link checkForWasmLeaks}
+ * in an `afterEach` hook.
+ */
+
+let trackedObjects: object[] = [];
+let originalRegister: Function | null = null;
+
+interface WasmBindgenObject {
+ __wbg_ptr: number;
+ constructor: { name?: string };
+}
+
+function isWasmBindgenObject(target: unknown): target is WasmBindgenObject {
+ return (
+ target !== null &&
+ typeof target === 'object' &&
+ '__wbg_ptr' in (target as Record)
+ );
+}
+
+/**
+ * Patches `FinalizationRegistry.prototype.register` to track all wasm-bindgen
+ * objects. Safe to call multiple times (idempotent).
+ */
+export function installWasmLeakDetector(): void {
+ if (originalRegister) return;
+
+ originalRegister = FinalizationRegistry.prototype.register;
+
+ FinalizationRegistry.prototype.register = function (
+ target: object,
+ heldValue: unknown,
+ unregisterToken?: object
+ ) {
+ if (isWasmBindgenObject(target)) {
+ trackedObjects.push(target);
+ }
+ return originalRegister!.call(this, target, heldValue, unregisterToken);
+ };
+}
+
+/**
+ * Returns any tracked WASM objects whose `__wbg_ptr` is still non-zero
+ * (i.e. `.free()` was never called). Clears the tracked set afterwards.
+ */
+export function checkForWasmLeaks(): string[] {
+ const leaks = trackedObjects
+ .filter(isWasmBindgenObject)
+ .filter((obj) => obj.__wbg_ptr !== 0)
+ .map((obj) => `${obj.constructor?.name ?? 'Unknown'} (ptr=${obj.__wbg_ptr})`);
+
+ trackedObjects = [];
+ return leaks;
+}
From 6d1d5ca3bc6ba1712f84274de38ddc8bec246a46 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:38:42 +0000
Subject: [PATCH 051/107] Fix utf8 handling
---
src/operation_transformation/edited_text.rs | 25 ++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index bc30137..48c854a 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -329,7 +329,12 @@ where
order,
..
} => {
- let deleted = self.text[*order..*order + *deleted_character_count].to_string();
+ let deleted: String = self
+ .text
+ .chars()
+ .skip(*order)
+ .take(*deleted_character_count)
+ .collect();
match side {
Side::Left => {
history.push(SpanWithHistory::new(deleted, History::RemovedFromLeft));
@@ -592,6 +597,24 @@ mod tests {
assert_eq!(serialized, expected);
}
+ #[test]
+ fn test_apply_with_history_utf8() {
+ let parent = "こんにちは世界"; // "Hello World" in Japanese (7 chars, 21 bytes)
+ let left = "こんにちは宇宙"; // Changed 世界 to 宇宙
+ let right = parent;
+
+ let result = crate::reconcile(
+ parent,
+ &left.into(),
+ &right.into(),
+ &*BuiltinTokenizer::Word,
+ );
+
+ let history = result.apply_with_history();
+ assert!(!history.is_empty());
+ assert_eq!(result.apply().text(), "こんにちは宇宙");
+ }
+
#[cfg(feature = "serde")]
#[test]
fn test_changes_serialization() {
From 408ce5268fc2fbf73f214703c1476aef5f44bcb5 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:40:14 +0000
Subject: [PATCH 052/107] Mini perf optimisation
---
src/operation_transformation/edited_text.rs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 48c854a..93779fb 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -435,6 +435,7 @@ where
) -> Result, DiffError> {
let mut operations: Vec> = Vec::with_capacity(diff.len());
let mut order = 0;
+ let text_length = original_text.chars().count();
for item in diff {
match item {
@@ -443,7 +444,6 @@ where
let length = usize::try_from(length).expect("length must fit in usize");
// Validate that the range doesn't exceed the original text
- let text_length = original_text.chars().count();
if order + length > text_length {
return Err(DiffError::LengthExceedsOriginal {
position: order,
@@ -466,7 +466,6 @@ where
usize::try_from(-length).expect("negative length must fit in usize");
// Validate that the delete range doesn't exceed the original text
- let text_length = original_text.chars().count();
if order + length > text_length {
return Err(DiffError::LengthExceedsOriginal {
position: order,
From deffa195b3e1e2de341620029e3326d4bc810a75 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Tue, 10 Mar 2026 20:42:09 +0000
Subject: [PATCH 053/107] Change style
---
README.md | 34 +++++++++----------
docs/advanced-ts.md | 6 ++--
examples/compare-with-diff-match-patch.rs | 2 +-
examples/website/src/index.html | 12 +++----
reconcile-js/src/index.ts | 19 +++++------
src/lib.rs | 2 +-
src/operation_transformation.rs | 3 +-
src/operation_transformation/edited_text.rs | 8 ++---
src/operation_transformation/operation.rs | 8 ++---
.../utils/cook_operations.rs | 2 +-
src/tokenizer.rs | 2 +-
src/tokenizer/character_tokenizer.rs | 2 +-
src/tokenizer/line_tokenizer.rs | 2 +-
src/tokenizer/token.rs | 10 +++---
src/tokenizer/word_tokenizer.rs | 2 +-
src/types/cursor_position.rs | 2 +-
src/types/history.rs | 2 +-
src/types/side.rs | 2 +-
src/types/span_with_history.rs | 2 +-
.../find_longest_prefix_contained_within.rs | 2 +-
src/utils/myers_diff.rs | 2 +-
src/utils/string_builder.rs | 6 ++--
src/wasm.rs | 16 ++++-----
23 files changed, 72 insertions(+), 76 deletions(-)
diff --git a/README.md b/README.md
index bdb6c39..480087c 100644
--- a/README.md
+++ b/README.md
@@ -13,11 +13,11 @@ A Rust and TypeScript library for merging conflicting text edits without manual
## Key features
-- **No conflict markers** — Clean, merged output without Git's `<<<<<<<` markers
-- **Cursor tracking** — Automatically repositions cursors and selections throughout the merging process
-- **Flexible tokenisation** — Word-level (default), character-level, line-level, or custom tokenisation strategies
-- **Unicode support** — Full UTF-8 support with proper handling of complex scripts and grapheme clusters
-- **Cross-platform** — Native Rust performance with WebAssembly bindings for JavaScript environments
+- **No conflict markers** - Clean, merged output without Git's `<<<<<<<` markers
+- **Cursor tracking** - Automatically repositions cursors and selections throughout the merging process
+- **Flexible tokenisation** - Word-level (default), character-level, line-level, or custom tokenisation strategies
+- **Unicode support** - Full UTF-8 support with proper handling of complex scripts and grapheme clusters
+- **Cross-platform** - Native Rust performance with WebAssembly bindings for JavaScript environments
## Quick start
@@ -93,12 +93,12 @@ Differential sync is implemented by [universal-sync](https://github.com/invisibl
`reconcile-text` starts off similarly to `diff3` ([4], [5]) but adds automated conflict resolution. Given a **parent** document and two modified versions (`left` and `right`), the following happens:
-1. **Tokenisation** — Input texts are split into meaningful units (words, characters, etc.) for granular merging
-2. **Diff computation** — Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
-3. **Diff optimisation** — Operations are reordered and consolidated to maximise chained changes
-4. **Operational Transformation** — Edits are woven together using OT principles, preserving all modifications and updating cursors
+1. **Tokenisation** - Input texts are split into meaningful units (words, characters, etc.) for granular merging
+2. **Diff computation** - Myers' algorithm calculates differences between (parent ↔ left) and (parent ↔ right)
+3. **Diff optimisation** - Operations are reordered and consolidated to maximise chained changes
+4. **Operational Transformation** - Edits are woven together using OT principles, preserving all modifications and updating cursors
-Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see [operational-transform-rs](https://github.com/spebern/operational-transform-rs).) The same could be achieved with CRDTs, which many libraries implement well for text—see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge) as excellent examples.
+Whilst the primary goal of `reconcile-text` isn't to implement OT, it provides an elegant way to merge Myers' diff outputs. (For a dedicated Rust OT implementation, see [operational-transform-rs](https://github.com/spebern/operational-transform-rs).) The same could be achieved with CRDTs, which many libraries implement well for text (see [Loro](https://github.com/loro-dev/loro/), [cola](https://github.com/nomad/cola), and [automerge](https://github.com/automerge/automerge)).
However, when only the end result of concurrent changes is observable, merge quality depends entirely on the quality of the underlying 2-way diffs. For instance, `move` operations cannot be supported because Myers' algorithm decomposes them into separate `insert` and `delete` operations, regardless of the merging algorithm used.
@@ -114,17 +114,17 @@ Tools like `diff3` ([4]) and Git produce **conflict markers** (`<<<<<<<` / `====
The key differences from `reconcile-text`:
-- **2-way vs 3-way** — diff-match-patch diffs two texts and applies the result as a patch. It has no concept of a common ancestor and cannot reason about "left changes" vs "right changes". `reconcile-text` performs true 3-way merging, understanding the intent behind each side's edits.
+- **2-way vs 3-way** - diff-match-patch diffs two texts and applies the result as a patch. It has no concept of a common ancestor and cannot reason about "left changes" vs "right changes". `reconcile-text` performs true 3-way merging, understanding the intent behind each side's edits.
-- **Character-level only** — Word-level and line-level diffs require encoding tokens as single Unicode characters before diffing ([7]). `reconcile-text` supports word, character, line, and custom tokenisation natively.
+- **Character-level only** - Word-level and line-level diffs require encoding tokens as single Unicode characters before diffing ([7]). `reconcile-text` supports word, character, line, and custom tokenisation natively.
-- **Patches can fail** — `patch_apply` returns a boolean array indicating success per patch; failed patches are silently dropped. In Differential Synchronisation, failures self-correct in the next cycle, but for one-shot merges edits can be lost. `reconcile-text` always produces a complete merged result.
+- **Patches can fail** - `patch_apply` returns a boolean array indicating success per patch; failed patches are silently dropped. In Differential Synchronisation, failures self-correct in the next cycle, but for one-shot merges edits can be lost. `reconcile-text` always produces a complete merged result.
-- **No cursor tracking or change provenance** — diff-match-patch does not reposition cursors or track which side made which edit. `reconcile-text` does both automatically.
+- **No cursor tracking or change provenance** - diff-match-patch does not reposition cursors or track which side made which edit. `reconcile-text` does both automatically.
See the [comparison example](examples/compare-with-diff-match-patch.rs) for concrete cases where diff-match-patch garbles adjacent edits and silently drops an entire sentence, while `reconcile-text` merges both users' changes correctly.
-> **When to use diff-match-patch instead**: when you don't have a common ancestor—for example, synchronising texts that have diverged through an unknown sequence of edits. If you have a common ancestor (as in most version control and collaborative editing scenarios), `reconcile-text` produces more reliable results.
+> **When to use diff-match-patch instead**: when you don't have a common ancestor, for example synchronising texts that have diverged through an unknown sequence of edits. If you have a common ancestor (as in most version control and collaborative editing scenarios), `reconcile-text` produces more reliable results.
### CRDTs (Yjs, Automerge, Loro, diamond-types)
@@ -132,13 +132,13 @@ Conflict-free Replicated Data Types guarantee convergence by mathematical constr
CRDTs capture every individual keystroke or operation, assigning each a unique identity. This makes them ideal when you control the complete editing infrastructure: the editor, the transport layer, and the storage format. They work peer-to-peer, handle arbitrary numbers of concurrent editors, and never lose an edit.
-The trade-off is that CRDTs require **maintaining document state over time**—an operation log or internal data structure that grows with the document's edit history. You cannot simply hand a CRDT library three plain strings and get a merged result. This makes them unsuitable for Differential Synchronisation scenarios where you only observe the final state of each document, which is exactly the niche `reconcile-text` fills.
+The trade-off is that CRDTs require **maintaining document state over time** - an operation log or internal data structure that grows with the document's edit history. You cannot simply hand a CRDT library three plain strings and get a merged result. This makes them unsuitable for Differential Synchronisation scenarios where you only observe the final state of each document, which is exactly the niche `reconcile-text` fills.
> **When to use CRDTs instead**: if you control the complete editing stack and can capture every operation as it happens, CRDTs provide stronger convergence guarantees. They also support more than two concurrent editors naturally, whereas `reconcile-text` merges exactly two forks at a time (though merges can be chained).
### Operational Transformation (OT)
-OT libraries like [ot.js](https://ot.js.org/) and [ShareJS](https://github.com/josephg/ShareJS) transform concurrent operations against each other so that applying them in any order produces the same result. Like CRDTs, they capture individual operations and require infrastructure to coordinate them—typically a central server that determines the canonical operation order.
+OT libraries like [ot.js](https://ot.js.org/) and [ShareJS](https://github.com/josephg/ShareJS) transform concurrent operations against each other so that applying them in any order produces the same result. Like CRDTs, they capture individual operations and require infrastructure to coordinate them, typically a central server that determines the canonical operation order.
`reconcile-text` borrows the *concept* of OT (transforming one side's edits against the other) but applies it to a different problem. Instead of transforming individual keystrokes in real time, it transforms the consolidated diff output of two complete edits. This means it doesn't need a server, doesn't need to capture operations as they happen, and works entirely offline.
diff --git a/docs/advanced-ts.md b/docs/advanced-ts.md
index 0480bd4..7e53bf5 100644
--- a/docs/advanced-ts.md
+++ b/docs/advanced-ts.md
@@ -42,9 +42,9 @@ console.log(result.history); /*
`reconcile-text` offers different approaches to split text for merging:
-- **Word tokeniser** (`"Word"`) — Splits on word boundaries (recommended for prose)
-- **Character tokeniser** (`"Character"`) — Individual characters (fine-grained control)
-- **Line tokeniser** (`"Line"`) — Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
+- **Word tokeniser** (`"Word"`) - Splits on word boundaries (recommended for prose)
+- **Character tokeniser** (`"Character"`) - Individual characters (fine-grained control)
+- **Line tokeniser** (`"Line"`) - Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
## Cursor Tracking
diff --git a/examples/compare-with-diff-match-patch.rs b/examples/compare-with-diff-match-patch.rs
index f748e6f..4e1eede 100644
--- a/examples/compare-with-diff-match-patch.rs
+++ b/examples/compare-with-diff-match-patch.rs
@@ -47,7 +47,7 @@ fn try_merge(parent: &str, left: &str, right: &str) {
}
/// Demonstrates cases where diff-match-patch silently produces incorrect
-/// output, while reconcile-text preserves both users' edits correctly.
+/// output, while reconcile-text preserves both users' edits correctly
///
/// Run it with:
/// `cargo run --example compare-with-diff-match-patch`
diff --git a/examples/website/src/index.html b/examples/website/src/index.html
index 71d5cbc..488d99c 100644
--- a/examples/website/src/index.html
+++ b/examples/website/src/index.html
@@ -8,12 +8,12 @@
/>
@@ -85,7 +85,7 @@
>documentation
or try editing the text boxes below to see reconcile-text in
- action. Use the tokenisation options to experiment with different approaches—
+ action. Use the tokenisation options to experiment with different approaches -
the Rust library also supports custom tokenisers.
@@ -145,7 +145,7 @@
-
+
Word
@@ -122,13 +116,17 @@
-
+
- Line
- Line-by-line, like git merge
+ Markdown
+ Preserve formatting
diff --git a/examples/website/src/index.ts b/examples/website/src/index.ts
index 9ce99a5..01edfd8 100644
--- a/examples/website/src/index.ts
+++ b/examples/website/src/index.ts
@@ -10,7 +10,11 @@ const tokenizerRadios = document.querySelectorAll(
'input[name="tokenizer"]'
) as NodeListOf;
-const sampleText = `The "reconcile-text" Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First user's edit", and "Second user's edit" text boxes to see competing changes get merged in real-time within the "Merged result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy, for example, deciding how casing or whitespace is taken into account.`;
+const sampleText = `The reconcile-text library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First user's edit", and "Second user's edit" text boxes to see competing changes get merged in real-time within the "Merged result" box.
+
+Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenisation strategy which may be:
+- Character-based
+- Word-based`;
let pendingUpdate: number | null = null;
function scheduleUpdate(): void {
@@ -52,10 +56,10 @@ function loadSample(): void {
originalTextArea.value = sampleText;
leftTextArea.value =
sampleText.replace('color', 'colour') +
- " Check out what's the most complex conflict you can come up with!";
- rightTextArea.value = sampleText
- .replace(', for example,', ' such as')
- .replace('WASM', 'WebAssembly');
+ "\n- Line-based\n\nCheck out what's the most complex conflict you can come up with!";
+ rightTextArea.value =
+ sampleText.replace(', for example,', ' such as').replace('WASM', 'WebAssembly') +
+ '\n- Or your custom tokeniser';
}
function updateMergedText(): void {
@@ -191,7 +195,7 @@ function createSelectionOverlay(isLeft: boolean, isSelection: boolean): HTMLSpan
function getSelectedTokenizer(): BuiltinTokenizer {
const selectedRadio = Array.from(tokenizerRadios).find((radio) => radio.checked);
- return (selectedRadio?.value ?? 'Word') as BuiltinTokenizer;
+ return (selectedRadio?.value ?? 'Markdown') as BuiltinTokenizer;
}
function resizeTextAreas(): void {
diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts
index 08edfc4..d00051c 100644
--- a/reconcile-js/src/index.ts
+++ b/reconcile-js/src/index.ts
@@ -12,7 +12,7 @@ import {
import wasmBytes from 'reconcile-text/reconcile_text_bg.wasm';
// Define the enum values as const arrays to avoid duplication
-const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Word'] as const;
+const BUILTIN_TOKENIZERS = ['Character', 'Line', 'Markdown', 'Word'] as const;
const HISTORY_VALUES = [
'Unchanged',
'AddedFromLeft',
From 1a984427abdd5bd84da5705d62d72d11269d5d47 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Wed, 11 Mar 2026 20:43:34 +0000
Subject: [PATCH 063/107] Minimise allocations
---
src/operation_transformation/edited_text.rs | 177 ++++++++++++--------
src/operation_transformation/operation.rs | 148 ++++++++++------
2 files changed, 211 insertions(+), 114 deletions(-)
diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs
index 602ae74..2a98259 100644
--- a/src/operation_transformation/edited_text.rs
+++ b/src/operation_transformation/edited_text.rs
@@ -1,10 +1,10 @@
-use std::{fmt::Debug, vec};
+use std::fmt::Debug;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{
- BuiltinTokenizer, CursorPosition, TextWithCursors,
+ BuiltinTokenizer, CursorPosition, TextWithCursors, Token,
operation_transformation::{
DiffError, Operation,
utils::{cook_operations::cook_operations, elongate_operations::elongate_operations},
@@ -55,6 +55,7 @@ where
{
/// Create an `EditedText` from the given original and updated strings
/// using the provided tokenizer
+ #[must_use]
pub fn from_strings_with_tokenizer(
original: &'a str,
updated: &TextWithCursors,
@@ -134,24 +135,21 @@ where
let mut last_right_op = None;
loop {
- let (side, operation, mut last_other_op) =
- match (maybe_left_op.clone(), maybe_right_op.clone()) {
- (Some(left_op), Some(right_op)) => {
- if left_op
- .get_sort_key(seen_left_length)
- .partial_cmp(&right_op.get_sort_key(seen_right_length))
- == Some(std::cmp::Ordering::Less)
- {
- (Side::Left, left_op, last_right_op.clone())
- } else {
- (Side::Right, right_op, last_left_op.clone())
- }
+ let (side, operation) = match (maybe_left_op.as_ref(), maybe_right_op.as_ref()) {
+ (Some(left_op), Some(right_op)) => {
+ if left_op.cmp_priority(seen_left_length, right_op, seen_right_length)
+ == std::cmp::Ordering::Less
+ {
+ (Side::Left, maybe_left_op.take().unwrap())
+ } else {
+ (Side::Right, maybe_right_op.take().unwrap())
}
+ }
- (Some(left_op), None) => (Side::Left, left_op, last_right_op.clone()),
- (None, Some(right_op)) => (Side::Right, right_op, last_left_op.clone()),
- (None, None) => break,
- };
+ (Some(_), None) => (Side::Left, maybe_left_op.take().unwrap()),
+ (None, Some(_)) => (Side::Right, maybe_right_op.take().unwrap()),
+ (None, None) => break,
+ };
let is_advancing_operation = matches!(
operation,
@@ -161,7 +159,7 @@ where
let original_length = operation.len();
let (side, result) = match side {
Side::Left => {
- let result = operation.merge_operations(&mut last_other_op);
+ let result = operation.merge_operations(last_right_op.as_ref());
if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result {
let merged_length_signed = isize::try_from(merged_length)
@@ -195,7 +193,7 @@ where
(Side::Left, result)
}
Side::Right => {
- let result = operation.merge_operations(&mut last_other_op);
+ let result = operation.merge_operations(last_left_op.as_ref());
if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result {
let merged_length_signed = isize::try_from(merged_length)
@@ -304,6 +302,7 @@ where
/// ```
#[must_use]
pub fn apply_with_history(&self) -> Vec {
+ let chars: Vec = self.text.chars().collect();
let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
let mut history = Vec::with_capacity(self.operations.len());
@@ -315,34 +314,26 @@ where
Operation::Equal { .. } => {
history.push(SpanWithHistory::new(builder.take(), History::Unchanged));
}
- Operation::Insert { .. } => match side {
- Side::Left => {
- history.push(SpanWithHistory::new(builder.take(), History::AddedFromLeft));
- }
- Side::Right => history.push(SpanWithHistory::new(
- builder.take(),
- History::AddedFromRight,
- )),
- },
+ Operation::Insert { .. } => {
+ let h = match side {
+ Side::Left => History::AddedFromLeft,
+ Side::Right => History::AddedFromRight,
+ };
+ history.push(SpanWithHistory::new(builder.take(), h));
+ }
Operation::Delete {
deleted_character_count,
order,
..
} => {
- let deleted: String = self
- .text
- .chars()
- .skip(*order)
- .take(*deleted_character_count)
+ let deleted: String = chars[*order..*order + *deleted_character_count]
+ .iter()
.collect();
- match side {
- Side::Left => {
- history.push(SpanWithHistory::new(deleted, History::RemovedFromLeft));
- }
- Side::Right => {
- history.push(SpanWithHistory::new(deleted, History::RemovedFromRight));
- }
- }
+ let h = match side {
+ Side::Left => History::RemovedFromLeft,
+ Side::Right => History::RemovedFromRight,
+ };
+ history.push(SpanWithHistory::new(deleted, h));
}
}
}
@@ -350,6 +341,56 @@ where
history
}
+ /// Apply the operations and return both the merged text with cursors and
+ /// the provenance history in a single pass
+ #[must_use]
+ pub fn apply_with_all(&self) -> (TextWithCursors, Vec) {
+ let chars: Vec = self.text.chars().collect();
+ let mut builder: StringBuilder<'_> = StringBuilder::new(self.text);
+ let mut history = Vec::with_capacity(self.operations.len());
+ let mut full_text = String::new();
+
+ for (operation, side) in self.operations.iter().zip(self.operation_sides.iter()) {
+ builder = operation.apply(builder);
+
+ match operation {
+ Operation::Equal { .. } => {
+ let span = builder.take();
+ full_text.push_str(&span);
+ history.push(SpanWithHistory::new(span, History::Unchanged));
+ }
+ Operation::Insert { .. } => {
+ let span = builder.take();
+ full_text.push_str(&span);
+ let h = match side {
+ Side::Left => History::AddedFromLeft,
+ Side::Right => History::AddedFromRight,
+ };
+ history.push(SpanWithHistory::new(span, h));
+ }
+ Operation::Delete {
+ deleted_character_count,
+ order,
+ ..
+ } => {
+ let deleted: String = chars[*order..*order + *deleted_character_count]
+ .iter()
+ .collect();
+ let h = match side {
+ Side::Left => History::RemovedFromLeft,
+ Side::Right => History::RemovedFromRight,
+ };
+ history.push(SpanWithHistory::new(deleted, h));
+ }
+ }
+ }
+
+ (
+ TextWithCursors::new(full_text, self.cursors.clone()),
+ history,
+ )
+ }
+
/// Convert the `EditedText` into a terse representation ready for
/// serialization. The result omits cursor positions and the original text.
/// This is useful for sending text diffs over the network if there's a
@@ -358,11 +399,11 @@ where
/// Inserts are strings, deletes are negative integers (character count),
/// and retained spans are positive integers (character count).
///
- /// # Panics
+ /// # Errors
///
- /// Panics if there's an integer overflow in i64.
- #[must_use]
- pub fn to_diff(&self) -> Vec {
+ /// Returns `DiffError::IntegerOverflow` if a character count exceeds
+ /// `i64::MAX`.
+ pub fn to_diff(&self) -> Result, DiffError> {
let mut result: Vec = Vec::with_capacity(self.operations.len());
let mut previous_equal: Option = None;
@@ -378,16 +419,14 @@ where
Operation::Insert { text, .. } => {
if let Some(prev_length) = previous_equal {
- result.push(NumberOrText::Number(
- i64::try_from(prev_length).expect("prev_length must fit in i64"),
- ));
+ result
+ .push(NumberOrText::Number(i64::try_from(prev_length).map_err(
+ |_| DiffError::IntegerOverflow { value: prev_length },
+ )?));
previous_equal = None;
}
- let text: String = text
- .iter()
- .map(super::super::tokenizer::token::Token::original)
- .collect();
+ let text: String = text.iter().map(Token::original).collect();
result.push(NumberOrText::Text(text));
}
@@ -396,26 +435,31 @@ where
..
} => {
if let Some(prev_length) = previous_equal {
- result.push(NumberOrText::Number(
- i64::try_from(prev_length).expect("prev_length must fit in i64"),
- ));
+ result
+ .push(NumberOrText::Number(i64::try_from(prev_length).map_err(
+ |_| DiffError::IntegerOverflow { value: prev_length },
+ )?));
previous_equal = None;
}
- let count = i64::try_from(*deleted_character_count)
- .expect("deleted_character_count must fit in i64");
+ let count = i64::try_from(*deleted_character_count).map_err(|_| {
+ DiffError::IntegerOverflow {
+ value: *deleted_character_count,
+ }
+ })?;
result.push(NumberOrText::Number(-count));
}
}
}
if let Some(prev_length) = previous_equal {
- result.push(NumberOrText::Number(
- i64::try_from(prev_length).expect("prev_length must fit in i64"),
- ));
+ result
+ .push(NumberOrText::Number(i64::try_from(prev_length).map_err(
+ |_| DiffError::IntegerOverflow { value: prev_length },
+ )?));
}
- result
+ Ok(result)
}
/// Reconstruct an `EditedText` from a diff and the original text.
@@ -435,7 +479,8 @@ where
) -> Result, DiffError> {
let mut operations: Vec> = Vec::with_capacity(diff.len());
let mut order = 0;
- let text_length = original_text.chars().count();
+ let chars: Vec = original_text.chars().collect();
+ let text_length = chars.len();
for item in diff {
match item {
@@ -453,7 +498,7 @@ where
}
let original_characters: String =
- original_text.chars().skip(order).take(length).collect();
+ chars[order..order + length].iter().collect();
let original_tokens = tokenizer(&original_characters);
for token in original_tokens {
@@ -590,7 +635,7 @@ mod tests {
let original = "Merging text is hard!";
let changes = "Merging text is easy with reconcile!";
let result = EditedText::from_strings(original, &changes.into());
- let serialized = serde_yaml::to_string(&result.to_diff()).unwrap();
+ let serialized = serde_yaml::to_string(&result.to_diff().unwrap()).unwrap();
let expected = concat!("- 15\n", "- -6\n", "- ' easy with reconcile!'\n",);
assert_eq!(serialized, expected);
@@ -622,7 +667,7 @@ mod tests {
let edited_text = EditedText::from_strings(original, &updated.into());
- let changes = edited_text.to_diff();
+ let changes = edited_text.to_diff().unwrap();
let deserialized_edited_text =
EditedText::from_diff(original, changes, &*BuiltinTokenizer::Word).unwrap();
diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs
index 28409f7..9d06639 100644
--- a/src/operation_transformation/operation.rs
+++ b/src/operation_transformation/operation.rs
@@ -104,28 +104,55 @@ where
}
}
- pub fn get_sort_key(&self, insertion_index: usize) -> (usize, usize, usize, String) {
- (
- self.order(),
- match self {
- Operation::Delete { .. } => 1,
- Operation::Insert { .. } => 2,
- Operation::Equal { .. } => 3,
- },
- insertion_index,
- // Make sure that the ordering is deterministic regardless of which text
- // is left or right.
- match self {
- Operation::Equal { length, .. } => length.to_string(),
- Operation::Insert { text, .. } => {
- text.iter().map(Token::original).collect::()
- }
+ fn type_priority(&self) -> u8 {
+ match self {
+ Operation::Delete { .. } => 1,
+ Operation::Insert { .. } => 2,
+ Operation::Equal { .. } => 3,
+ }
+ }
+
+ /// Compare two operations for processing order during merging. Uses
+ /// (order, type, `insertion_index`) with a deterministic content
+ /// tiebreaker that avoids allocating.
+ pub fn cmp_priority(
+ &self,
+ self_index: usize,
+ other: &Self,
+ other_index: usize,
+ ) -> std::cmp::Ordering {
+ self.order()
+ .cmp(&other.order())
+ .then_with(|| self.type_priority().cmp(&other.type_priority()))
+ .then_with(|| self_index.cmp(&other_index))
+ .then_with(|| self.deterministic_content_cmp(other))
+ }
+
+ /// Deterministic tiebreaker based on operation content, so that merge
+ /// results are identical regardless of which side is left vs right
+ fn deterministic_content_cmp(&self, other: &Self) -> std::cmp::Ordering {
+ match (self, other) {
+ (Operation::Insert { text: t1, .. }, Operation::Insert { text: t2, .. }) => {
+ let s1 = t1.iter().flat_map(|t| t.original().chars());
+ let s2 = t2.iter().flat_map(|t| t.original().chars());
+ s1.cmp(s2)
+ }
+ (Operation::Equal { length: l1, .. }, Operation::Equal { length: l2, .. }) => {
+ l1.cmp(l2)
+ }
+ (
Operation::Delete {
- deleted_character_count,
+ deleted_character_count: c1,
..
- } => deleted_character_count.to_string(),
- },
- )
+ },
+ Operation::Delete {
+ deleted_character_count: c2,
+ ..
+ },
+ ) => c1.cmp(c2),
+ // Different types are already ordered by type_priority
+ _ => std::cmp::Ordering::Equal,
+ }
}
/// Applies the operation to the given `StringBuilder`, returning the
@@ -193,10 +220,9 @@ where
}
/// Adjusts this operation based on `previous_operation` from the other side
- /// to avoid duplicating or conflicting changes. Updates
- /// `previous_operation` in-place.
+ /// to avoid duplicating or conflicting changes
#[allow(clippy::too_many_lines)]
- pub fn merge_operations(self, previous_operation: &mut Option) -> Operation {
+ pub fn merge_operations(self, previous_operation: Option<&Self>) -> Operation {
let operation = self;
match (operation, previous_operation) {
@@ -295,14 +321,36 @@ where
}
(
- ref operation @ Operation::Equal { ref order, .. },
+ ref operation @ Operation::Equal {
+ ref order,
+ #[cfg(debug_assertions)]
+ ref text,
+ ..
+ },
Some(Operation::Equal {
order: last_equal_order,
length: last_equal_length,
+ #[cfg(debug_assertions)]
+ text: last_equal_text,
..
}),
) => {
if operation.len() == *last_equal_length && *order == *last_equal_order {
+ // Both sides retained the same span from the original text,
+ // so we deduplicate by zeroing one out. This is safe because
+ // both EditedTexts are derived from the same original, and
+ // matching (order, length) means they cover the same substring
+ #[cfg(debug_assertions)]
+ debug_assert_eq!(
+ text, last_equal_text,
+ "Equal operations with same order and length should have the same text, \
+ but got {operation:?} vs {:?}",
+ Operation::::Equal {
+ order: *last_equal_order,
+ length: *last_equal_length,
+ text: last_equal_text.clone(),
+ },
+ );
Operation::create_equal(*order, 0)
} else {
operation.clone()
@@ -329,18 +377,20 @@ where
..
} => {
#[cfg(debug_assertions)]
- write!(
- f,
- "",
- text.as_ref()
- .map(|text| format!("'{}'", text.replace('\n', "\\n")))
- .unwrap_or(format!("{length} characters")),
- )?;
+ {
+ write!(
+ f,
+ "",
+ text.as_ref()
+ .map(|text| format!("'{}'", text.replace('\n', "\\n")))
+ .unwrap_or(format!("{length} characters")),
+ )
+ }
#[cfg(not(debug_assertions))]
- write!(f, "")?;
-
- Ok(())
+ {
+ write!(f, "")
+ }
}
Operation::Insert { order, text, .. } => {
write!(
@@ -361,22 +411,24 @@ where
..
} => {
#[cfg(debug_assertions)]
- write!(
- f,
- "",
- deleted_text
- .as_ref()
- .map(|text| format!("'{}'", text.replace('\n', "\\n")))
- .unwrap_or(format!("{deleted_character_count} characters")),
- )?;
+ {
+ write!(
+ f,
+ "",
+ deleted_text
+ .as_ref()
+ .map(|text| format!("'{}'", text.replace('\n', "\\n")))
+ .unwrap_or(format!("{deleted_character_count} characters")),
+ )
+ }
#[cfg(not(debug_assertions))]
- write!(
- f,
- "",
- )?;
-
- Ok(())
+ {
+ write!(
+ f,
+ "",
+ )
+ }
}
}
}
From 7b81034625a13f9d36c9a25e6bc361966bc71164 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Wed, 11 Mar 2026 20:43:41 +0000
Subject: [PATCH 064/107] Update tests
---
...kenizer__tests__with_snapshots-11.snap.new | 37 +++++++++++++++++++
...down_tokenizer__tests__headings-2.snap.new | 25 +++++++++++++
2 files changed, 62 insertions(+)
create mode 100644 src/tokenizer/snapshots/reconcile_text__tokenizer__line_tokenizer__tests__with_snapshots-11.snap.new
create mode 100644 src/tokenizer/snapshots/reconcile_text__tokenizer__markdown_tokenizer__tests__headings-2.snap.new
diff --git a/src/tokenizer/snapshots/reconcile_text__tokenizer__line_tokenizer__tests__with_snapshots-11.snap.new b/src/tokenizer/snapshots/reconcile_text__tokenizer__line_tokenizer__tests__with_snapshots-11.snap.new
new file mode 100644
index 0000000..36f12f9
--- /dev/null
+++ b/src/tokenizer/snapshots/reconcile_text__tokenizer__line_tokenizer__tests__with_snapshots-11.snap.new
@@ -0,0 +1,37 @@
+---
+source: src/tokenizer/line_tokenizer.rs
+assertion_line: 78
+expression: "line_tokenizer(\"Mixed\\r\\nand\\rbare\")"
+---
+[
+ Token {
+ normalized: "Mixed",
+ original: "Mixed",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: "\r\n",
+ original: "\r\n",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: "and",
+ original: "and",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: "\r",
+ original: "\r",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: "bare",
+ original: "bare",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+]
diff --git a/src/tokenizer/snapshots/reconcile_text__tokenizer__markdown_tokenizer__tests__headings-2.snap.new b/src/tokenizer/snapshots/reconcile_text__tokenizer__markdown_tokenizer__tests__headings-2.snap.new
new file mode 100644
index 0000000..0f35315
--- /dev/null
+++ b/src/tokenizer/snapshots/reconcile_text__tokenizer__markdown_tokenizer__tests__headings-2.snap.new
@@ -0,0 +1,25 @@
+---
+source: src/tokenizer/markdown_tokenizer.rs
+assertion_line: 199
+expression: "markdown_tokenizer(\"## Sub heading\")"
+---
+[
+ Token {
+ normalized: "## Sub",
+ original: "## Sub",
+ is_left_joinable: false,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: " heading",
+ original: " ",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+ Token {
+ normalized: "heading",
+ original: "heading",
+ is_left_joinable: true,
+ is_right_joinable: true,
+ },
+]
From 545be141d80f242d6ec107bc6fe6155c0ba89e54 Mon Sep 17 00:00:00 2001
From: Andras Schmelczer
Date: Wed, 11 Mar 2026 20:48:44 +0000
Subject: [PATCH 065/107] Add Python bindings
---
.gitignore | 3 +
Cargo.toml | 2 +-
README.md | 41 ++-
docs/advanced-python.md | 88 ++++++
reconcile-js/package-lock.json | 214 +++++++++-----
reconcile-python/.gitignore | 9 +
reconcile-python/Cargo.lock | 208 +++++++++++++
reconcile-python/Cargo.toml | 16 +
reconcile-python/pyproject.toml | 52 ++++
.../python/reconcile_text/__init__.py | 165 +++++++++++
.../python/reconcile_text/_native.pyi | 24 ++
.../python/reconcile_text/py.typed | 0
reconcile-python/src/lib.rs | 235 +++++++++++++++
reconcile-python/tests/test_reconcile.py | 132 +++++++++
reconcile-python/uv.lock | 279 ++++++++++++++++++
scripts/bump-version.sh | 5 +
scripts/lint.sh | 7 +
scripts/test.sh | 5 +
18 files changed, 1406 insertions(+), 79 deletions(-)
create mode 100644 docs/advanced-python.md
create mode 100644 reconcile-python/.gitignore
create mode 100644 reconcile-python/Cargo.lock
create mode 100644 reconcile-python/Cargo.toml
create mode 100644 reconcile-python/pyproject.toml
create mode 100644 reconcile-python/python/reconcile_text/__init__.py
create mode 100644 reconcile-python/python/reconcile_text/_native.pyi
create mode 100644 reconcile-python/python/reconcile_text/py.typed
create mode 100644 reconcile-python/src/lib.rs
create mode 100644 reconcile-python/tests/test_reconcile.py
create mode 100644 reconcile-python/uv.lock
diff --git a/.gitignore b/.gitignore
index cac139a..0957a69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ node_modules
# WebPack build output
dist
+
+# Python virtual environment
+.venv
diff --git a/Cargo.toml b/Cargo.toml
index 7b657d4..4b5e11c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ repository = "https://github.com/schmelczer/reconcile"
homepage = "https://schmelczer.dev/reconcile"
keywords = ["merge", "OT", "CRDT", "3-way", "diff"]
categories = ["wasm", "text-processing", "text-editors", "algorithms", "data-structures"]
-exclude = ["reconcile-js", ".*", "examples/website"]
+exclude = ["reconcile-js", "reconcile-python", ".*", "examples/website"]
[lib]
crate-type = ["cdylib", "rlib"]
diff --git a/README.md b/README.md
index e035caf..8ee4dc7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# `reconcile-text`: conflict-free 3-way text merging
-A Rust and TypeScript 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 (while updating cursor positions) using an algorithm inspired by Operational Transformation.
+A Rust, TypeScript, and Python 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 (while updating cursor positions) using an algorithm inspired by Operational Transformation.
## Try it
@@ -10,6 +10,7 @@ A Rust and TypeScript library for merging conflicting text edits without manual
- `cargo add reconcile-text` ([reconcile-text on crates.io][9])
- `npm install reconcile-text` ([reconcile-text on NPM][10])
+- `uv add reconcile-text` or `pip install reconcile-text` ([reconcile-text on PyPI][27])
## Key features
@@ -17,7 +18,7 @@ A Rust and TypeScript library for merging conflicting text edits without manual
- **Cursor tracking** - Automatically repositions cursors and selections throughout the merging process
- **Flexible tokenisation** - Word-level (default), character-level, line-level, or custom tokenisation strategies
- **Unicode support** - Full UTF-8 support with proper handling of complex scripts and grapheme clusters
-- **Cross-platform** - Native Rust performance with WebAssembly bindings for JavaScript environments
+- **Cross-platform** - Native Rust performance with WebAssembly bindings for JavaScript and native bindings for Python
## Quick start
@@ -79,6 +80,32 @@ console.log(result.text); // "Hi beautiful world"
See the [example website source](examples/website/src/index.ts) for a more complex example, or the [advanced examples document](docs/advanced-ts.md).
+### Python
+
+Install via uv or pip:
+
+```sh
+uv add reconcile-text
+# or: pip install reconcile-text
+```
+
+Then use it in your application:
+
+```python
+from reconcile_text import reconcile
+
+# Start with the original text
+parent = "Hello world"
+# Two users edit simultaneously
+left = "Hello beautiful world"
+right = "Hi world"
+
+result = reconcile(parent, left, right)
+print(result["text"]) # "Hi beautiful world"
+```
+
+See the [advanced Python examples](docs/advanced-python.md) for cursor tracking, change provenance, and compact diffs.
+
## Motivation
Collaborative editing presents the challenge of merging conflicting changes when multiple users edit documents simultaneously or asynchronously whilst offline. Traditional solutions like Conflict-free Replicated Data Types (CRDTs) or Operational Transformation (OT) work well when you control the complete editing infrastructure and can capture every individual operation ([1]). However, many workflows involve users editing with various tools, for example, Obsidian users editing Markdown files with various editors ranging from Vim to VS Code.
@@ -150,6 +177,15 @@ Contributions are welcome!
### Environment
+#### Python setup
+
+Install [uv](https://docs.astral.sh/uv/getting-started/installation/) and build the extension for development:
+
+```sh
+cd reconcile-python
+uv run maturin develop
+```
+
#### Node.js setup
1. Install [nvm][25]:
@@ -210,3 +246,4 @@ Install [rustup][26]:
[24]: https://github.com/josephg/ShareJS
[25]: https://github.com/nvm-sh/nvm
[26]: https://rustup.rs
+[27]: https://pypi.org/project/reconcile-text/
diff --git a/docs/advanced-python.md b/docs/advanced-python.md
new file mode 100644
index 0000000..1e21907
--- /dev/null
+++ b/docs/advanced-python.md
@@ -0,0 +1,88 @@
+# Advanced Usage (Python)
+
+## Edit Provenance
+
+Track which changes came from where using `reconcile_with_history`:
+
+```python
+from reconcile_text import reconcile_with_history
+
+result = reconcile_with_history(
+ "Hello world",
+ "Hello beautiful world",
+ "Hi world",
+)
+
+print(result["text"]) # "Hi beautiful world"
+print(result["history"]) #
+# [
+# {"text": "Hello", "history": "RemovedFromRight"},
+# {"text": "Hi", "history": "AddedFromRight"},
+# {"text": " beautiful", "history": "AddedFromLeft"},
+# {"text": " ", "history": "Unchanged"},
+# {"text": "world", "history": "Unchanged"},
+# ]
+```
+
+## Tokenization Strategies
+
+`reconcile-text` offers different approaches to split text for merging:
+
+- **Word tokenizer** (`"Word"`) - Splits on word boundaries (recommended for prose)
+- **Character tokenizer** (`"Character"`) - Individual characters (fine-grained control)
+- **Line tokenizer** (`"Line"`) - Line-by-line (similar to `git merge` or more precisely [`git merge-file`](https://git-scm.com/docs/git-merge-file))
+- **Markdown tokenizer** (`"Markdown"`) - Splits on Markdown structural boundaries (headings, list items, paragraphs)
+
+```python
+from reconcile_text import reconcile
+
+result = reconcile("abc", "axc", "abyc", "Character")
+print(result["text"]) # "axyc"
+```
+
+## Cursor Tracking
+
+`reconcile-text` automatically tracks cursor positions through merges, which is useful for collaborative editors. Selections can be tracked by providing them as a pair of cursors.
+
+```python
+from reconcile_text import reconcile
+
+result = reconcile(
+ "Hello world",
+ {
+ "text": "Hello beautiful world",
+ "cursors": [{"id": 1, "position": 6}], # After "Hello "
+ },
+ {
+ "text": "Hi world",
+ "cursors": [{"id": 2, "position": 0}], # At the beginning
+ },
+)
+
+# Result: "Hi beautiful world" with repositioned cursors
+print(result["text"]) # "Hi beautiful world"
+print(result["cursors"]) # [{"id": 2, "position": 0}, {"id": 1, "position": 3}]
+```
+
+> The `cursors` list is sorted by character position (not IDs).
+
+## Compact Diffs
+
+Generate and apply compact diff representations:
+
+```python
+from reconcile_text import diff, undiff
+
+original = "Hello world"
+changed = "Hello beautiful world"
+
+# Generate a compact diff
+d = diff(original, changed)
+print(d) # [5, ' beautiful world']
+
+# Reconstruct the changed text from the diff
+reconstructed = undiff(original, d)
+assert reconstructed == changed
+```
+
+Diff entries are positive integers (retain N characters), negative integers (delete N characters), and strings (insert text).
diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json
index a2fbafa..cbb37dd 100644
--- a/reconcile-js/package-lock.json
+++ b/reconcile-js/package-lock.json
@@ -852,7 +852,9 @@
}
},
"node_modules/@jridgewell/source-map": {
- "version": "0.3.10",
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1248,7 +1250,9 @@
"license": "Apache-2.0"
},
"node_modules/acorn": {
- "version": "8.15.0",
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1258,8 +1262,23 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
"node_modules/ajv": {
- "version": "8.17.1",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1275,6 +1294,8 @@
},
"node_modules/ajv-formats": {
"version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1291,6 +1312,8 @@
},
"node_modules/ajv-keywords": {
"version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1457,6 +1480,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"dev": true,
@@ -1478,7 +1514,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.25.1",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -1496,10 +1534,11 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001726",
- "electron-to-chromium": "^1.5.173",
- "node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -1549,7 +1588,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001726",
+ "version": "1.0.30001777",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz",
+ "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==",
"dev": true,
"funding": [
{
@@ -1744,6 +1785,8 @@
},
"node_modules/commander": {
"version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"license": "MIT"
},
@@ -1835,7 +1878,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.179",
+ "version": "1.5.307",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
+ "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==",
"dev": true,
"license": "ISC"
},
@@ -1856,12 +1901,14 @@
"license": "MIT"
},
"node_modules/enhanced-resolve": {
- "version": "5.18.2",
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
+ "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.2.0"
+ "tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
@@ -1887,7 +1934,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.7.0",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
"dev": true,
"license": "MIT"
},
@@ -2014,6 +2063,8 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
@@ -2023,7 +2074,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.0.6",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
@@ -2070,7 +2123,9 @@
}
},
"node_modules/filelist/node_modules/minimatch": {
- "version": "5.1.6",
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
+ "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -2206,6 +2261,8 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
@@ -2218,11 +2275,13 @@
}
},
"node_modules/glob/node_modules/minimatch": {
- "version": "9.0.5",
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -3068,6 +3127,8 @@
},
"node_modules/jest-worker": {
"version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3114,6 +3175,8 @@
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
@@ -3150,11 +3213,17 @@
"license": "MIT"
},
"node_modules/loader-runner": {
- "version": "4.3.0",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
+ "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/locate-path": {
@@ -3264,7 +3333,9 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -3317,7 +3388,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.19",
+ "version": "2.0.36",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
"dev": true,
"license": "MIT"
},
@@ -3568,14 +3641,6 @@
],
"license": "MIT"
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"dev": true,
@@ -3606,6 +3671,8 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3650,27 +3717,10 @@
"node": ">=8"
}
},
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
"node_modules/schema-utils": {
- "version": "4.3.2",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3695,14 +3745,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/shallow-clone": {
"version": "3.0.1",
"dev": true,
@@ -3756,6 +3798,8 @@
},
"node_modules/source-map-support": {
"version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3965,20 +4009,28 @@
}
},
"node_modules/tapable": {
- "version": "2.2.2",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
- "version": "5.43.1",
+ "version": "5.46.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
+ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.14.0",
+ "acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -3990,14 +4042,15 @@
}
},
"node_modules/terser-webpack-plugin": {
- "version": "5.3.14",
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
+ "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
@@ -4256,7 +4309,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.1.3",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"dev": true,
"funding": [
{
@@ -4306,7 +4361,9 @@
}
},
"node_modules/watchpack": {
- "version": "2.4.4",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+ "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4318,34 +4375,37 @@
}
},
"node_modules/webpack": {
- "version": "5.99.9",
+ "version": "5.105.4",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
+ "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
- "@types/estree": "^1.0.6",
+ "@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.14.0",
- "browserslist": "^4.24.0",
+ "acorn": "^8.16.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.28.1",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.1",
- "es-module-lexer": "^1.2.1",
+ "enhanced-resolve": "^5.20.0",
+ "es-module-lexer": "^2.0.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1",
- "loader-runner": "^4.2.0",
+ "loader-runner": "^4.3.1",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^4.3.2",
- "tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.11",
- "watchpack": "^2.4.1",
- "webpack-sources": "^3.2.3"
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.3.17",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.3.4"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -4426,7 +4486,9 @@
}
},
"node_modules/webpack-sources": {
- "version": "3.3.3",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
+ "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/reconcile-python/.gitignore b/reconcile-python/.gitignore
new file mode 100644
index 0000000..772a74a
--- /dev/null
+++ b/reconcile-python/.gitignore
@@ -0,0 +1,9 @@
+.venv/
+.pytest_cache/
+.ruff_cache/
+__pycache__/
+*.egg-info/
+*.so
+*.dylib
+*.dSYM/
+dist/
diff --git a/reconcile-python/Cargo.lock b/reconcile-python/Cargo.lock
new file mode 100644
index 0000000..ed40d8b
--- /dev/null
+++ b/reconcile-python/Cargo.lock
@@ -0,0 +1,208 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.183"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "reconcile-text"
+version = "0.8.0"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "reconcile-text-python"
+version = "0.8.0"
+dependencies = [
+ "pyo3",
+ "reconcile-text",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
diff --git a/reconcile-python/Cargo.toml b/reconcile-python/Cargo.toml
new file mode 100644
index 0000000..48ab072
--- /dev/null
+++ b/reconcile-python/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "reconcile-text-python"
+version = "0.8.0"
+edition = "2024"
+rust-version = "1.85"
+authors = ["Andras Schmelczer "]
+license = "MIT"
+publish = false
+
+[lib]
+name = "_native"
+crate-type = ["cdylib"]
+
+[dependencies]
+reconcile-text = { path = ".." }
+pyo3 = { version = "0.24", features = ["extension-module"] }
diff --git a/reconcile-python/pyproject.toml b/reconcile-python/pyproject.toml
new file mode 100644
index 0000000..92f185d
--- /dev/null
+++ b/reconcile-python/pyproject.toml
@@ -0,0 +1,52 @@
+[build-system]
+requires = ["maturin>=1.0,<2.0"]
+build-backend = "maturin"
+
+[project]
+name = "reconcile-text"
+version = "0.8.0"
+description = "Intelligent 3-way text merging with automated conflict resolution"
+readme = "../README.md"
+license = { text = "MIT" }
+authors = [{ name = "Andras Schmelczer", email = "andras@schmelczer.dev" }]
+requires-python = ">=3.9"
+classifiers = [
+ "Programming Language :: Rust",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Typing :: Typed",
+]
+keywords = ["merge", "OT", "CRDT", "3-way", "diff", "text"]
+
+[dependency-groups]
+dev = ["maturin>=1.0,<2.0", "pytest>=8", "ruff>=0.15", "pyright>=1"]
+
+[project.urls]
+Homepage = "https://schmelczer.dev/reconcile"
+Repository = "https://github.com/schmelczer/reconcile"
+Issues = "https://github.com/schmelczer/reconcile/issues"
+
+[tool.maturin]
+manifest-path = "Cargo.toml"
+module-name = "reconcile_text._native"
+python-source = "python"
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+
+[tool.ruff]
+target-version = "py39"
+line-length = 100
+
+[tool.ruff.lint]
+select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
+
+[tool.ruff.lint.isort]
+known-first-party = ["reconcile_text"]
+
+[tool.pyright]
+pythonVersion = "3.9"
+typeCheckingMode = "strict"
+include = ["python", "tests"]
diff --git a/reconcile-python/python/reconcile_text/__init__.py b/reconcile-python/python/reconcile_text/__init__.py
new file mode 100644
index 0000000..fca9c56
--- /dev/null
+++ b/reconcile-python/python/reconcile_text/__init__.py
@@ -0,0 +1,165 @@
+"""Intelligent 3-way text merging with automated conflict resolution."""
+
+from __future__ import annotations
+
+from typing import Literal, TypedDict, Union
+
+from reconcile_text._native import diff as _diff
+from reconcile_text._native import reconcile as _reconcile
+from reconcile_text._native import reconcile_with_history as _reconcile_with_history
+from reconcile_text._native import undiff as _undiff
+
+BuiltinTokenizer = Literal["Character", "Line", "Markdown", "Word"]
+"""Tokenization strategy for text merging."""
+
+History = Literal[
+ "Unchanged", "AddedFromLeft", "AddedFromRight", "RemovedFromLeft", "RemovedFromRight"
+]
+"""Provenance label for each span in a merge result."""
+
+
+class CursorPosition(TypedDict):
+ """A cursor position within a text document."""
+
+ id: int
+ """Unique identifier for the cursor."""
+ position: int
+ """Character position in the text (0-based)."""
+
+
+class TextWithCursors(TypedDict):
+ """A text document with associated cursor positions."""
+
+ text: str
+ """The document content."""
+ cursors: list[CursorPosition]
+ """Cursor positions within the text."""
+
+
+class SpanWithHistory(TypedDict):
+ """A text span annotated with its origin in a merge result."""
+
+ text: str
+ """The text content of this span."""
+ history: History
+ """Which source this span came from."""
+
+
+class TextWithCursorsAndHistory(TypedDict):
+ """A merged text document with cursor positions and change provenance."""
+
+ text: str
+ """The merged document content."""
+ cursors: list[CursorPosition]
+ """Repositioned cursor positions."""
+ history: list[SpanWithHistory]
+ """Provenance information for each text span."""
+
+
+TextInput = Union[str, TextWithCursors]
+"""Input type for text arguments: either a plain string or a dict with text and cursors."""
+
+
+def reconcile(
+ parent: str,
+ left: TextInput,
+ right: TextInput,
+ tokenizer: BuiltinTokenizer = "Word",
+) -> TextWithCursors:
+ """Merge three versions of text using conflict-free resolution.
+
+ Takes a parent text and two concurrent edits (left and right), returning
+ the merged result with automatically repositioned cursors.
+
+ Args:
+ parent: The original text that both sides diverged from.
+ left: The left edit (string or dict with "text" and "cursors").
+ right: The right edit (string or dict with "text" and "cursors").
+ tokenizer: Tokenization strategy. Defaults to "Word".
+
+ Returns:
+ A dict with "text" (merged string) and "cursors" (repositioned cursor list).
+ """
+ return _reconcile(parent, left, right, tokenizer) # type: ignore[return-value]
+
+
+def reconcile_with_history(
+ parent: str,
+ left: TextInput,
+ right: TextInput,
+ tokenizer: BuiltinTokenizer = "Word",
+) -> TextWithCursorsAndHistory:
+ """Merge three versions of text and return provenance history.
+
+ Like `reconcile`, but also returns which source each text span came from.
+
+ Args:
+ parent: The original text that both sides diverged from.
+ left: The left edit (string or dict with "text" and "cursors").
+ right: The right edit (string or dict with "text" and "cursors").
+ tokenizer: Tokenization strategy. Defaults to "Word".
+
+ Returns:
+ A dict with "text", "cursors", and "history".
+ """
+ return _reconcile_with_history(parent, left, right, tokenizer) # type: ignore[return-value]
+
+
+def diff(
+ parent: str,
+ changed: TextInput,
+ tokenizer: BuiltinTokenizer = "Word",
+) -> list[int | str]:
+ """Generate a compact diff between two texts.
+
+ Returns retain counts (positive ints), delete counts (negative ints),
+ and inserted strings.
+
+ Args:
+ parent: The original text.
+ changed: The modified text (string or dict with "text" and "cursors").
+ tokenizer: Tokenization strategy. Defaults to "Word".
+
+ Returns:
+ A list of ints and strings representing the diff.
+
+ Raises:
+ ValueError: If the diff computation overflows.
+ """
+ return _diff(parent, changed, tokenizer) # type: ignore[return-value]
+
+
+def undiff(
+ parent: str,
+ diff: list[int | str],
+ tokenizer: BuiltinTokenizer = "Word",
+) -> str:
+ """Apply a compact diff to reconstruct the changed text.
+
+ Args:
+ parent: The original text.
+ diff: A list of ints and strings (as produced by `diff`).
+ tokenizer: Tokenization strategy. Defaults to "Word".
+
+ Returns:
+ The reconstructed text.
+
+ Raises:
+ ValueError: If the diff format is invalid.
+ """
+ return _undiff(parent, diff, tokenizer)
+
+
+__all__ = [
+ "BuiltinTokenizer",
+ "CursorPosition",
+ "History",
+ "SpanWithHistory",
+ "TextInput",
+ "TextWithCursors",
+ "TextWithCursorsAndHistory",
+ "diff",
+ "reconcile",
+ "reconcile_with_history",
+ "undiff",
+]
diff --git a/reconcile-python/python/reconcile_text/_native.pyi b/reconcile-python/python/reconcile_text/_native.pyi
new file mode 100644
index 0000000..897513d
--- /dev/null
+++ b/reconcile-python/python/reconcile_text/_native.pyi
@@ -0,0 +1,24 @@
+from typing import Any
+
+def reconcile(
+ parent: str,
+ left: Any,
+ right: Any,
+ tokenizer: str = "Word",
+) -> dict[str, Any]: ...
+def reconcile_with_history(
+ parent: str,
+ left: Any,
+ right: Any,
+ tokenizer: str = "Word",
+) -> dict[str, Any]: ...
+def diff(
+ parent: str,
+ changed: Any,
+ tokenizer: str = "Word",
+) -> list[int | str]: ...
+def undiff(
+ parent: str,
+ diff: list[int | str],
+ tokenizer: str = "Word",
+) -> str: ...
diff --git a/reconcile-python/python/reconcile_text/py.typed b/reconcile-python/python/reconcile_text/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/reconcile-python/src/lib.rs b/reconcile-python/src/lib.rs
new file mode 100644
index 0000000..809e0b0
--- /dev/null
+++ b/reconcile-python/src/lib.rs
@@ -0,0 +1,235 @@
+use pyo3::prelude::*;
+use pyo3::types::{PyDict, PyList};
+use reconcile_text::{
+ BuiltinTokenizer, CursorPosition, EditedText, NumberOrText, TextWithCursors,
+};
+
+fn parse_tokenizer(tokenizer: &str) -> PyResult {
+ match tokenizer {
+ "Character" => Ok(BuiltinTokenizer::Character),
+ "Line" => Ok(BuiltinTokenizer::Line),
+ "Markdown" => Ok(BuiltinTokenizer::Markdown),
+ "Word" => Ok(BuiltinTokenizer::Word),
+ _ => Err(pyo3::exceptions::PyValueError::new_err(format!(
+ "Unknown tokenizer '{tokenizer}', expected Character, Line, Markdown, or Word"
+ ))),
+ }
+}
+
+fn extract_text_with_cursors(input: &Bound<'_, PyAny>) -> PyResult {
+ if let Ok(text) = input.extract::() {
+ return Ok(TextWithCursors::from(text));
+ }
+
+ let dict = input.downcast::()?;
+
+ let text: String = dict
+ .get_item("text")?
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("text"))?
+ .extract()?;
+
+ let cursors = match dict.get_item("cursors")? {
+ Some(obj) if !obj.is_none() => {
+ let list = obj.downcast::()?;
+ let mut cursors = Vec::with_capacity(list.len());
+ for item in list {
+ let cursor_dict = item.downcast::()?;
+ let id: usize = cursor_dict
+ .get_item("id")?
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("id"))?
+ .extract()?;
+ let position: usize = cursor_dict
+ .get_item("position")?
+ .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err("position"))?
+ .extract()?;
+ cursors.push(CursorPosition::new(id, position));
+ }
+ cursors
+ }
+ _ => Vec::new(),
+ };
+
+ Ok(TextWithCursors::new(text, cursors))
+}
+
+fn text_with_cursors_to_dict<'py>(
+ py: Python<'py>,
+ twc: &TextWithCursors,
+) -> PyResult> {
+ let dict = PyDict::new(py);
+ dict.set_item("text", twc.text())?;
+
+ let cursors = PyList::new(
+ py,
+ twc.cursors().iter().map(|c| {
+ let d = PyDict::new(py);
+ d.set_item("id", c.id()).unwrap();
+ d.set_item("position", c.char_index()).unwrap();
+ d
+ }),
+ )?;
+ dict.set_item("cursors", cursors)?;
+
+ Ok(dict)
+}
+
+/// Merge three versions of text using conflict-free resolution.
+///
+/// Takes a parent text and two concurrent edits (left and right), returning
+/// the merged result with automatically repositioned cursors.
+///
+/// Args:
+/// parent: The original text that both sides diverged from.
+/// left: The left edit, either a string or a dict with "text" and "cursors" keys.
+/// right: The right edit, either a string or a dict with "text" and "cursors" keys.
+/// tokenizer: Tokenization strategy - "Word" (default), "Character", "Line", or "Markdown".
+///
+/// Returns:
+/// A dict with "text" (merged string) and "cursors" (list of repositioned cursors).
+#[pyfunction]
+#[pyo3(signature = (parent, left, right, tokenizer = "Word"))]
+fn reconcile<'py>(
+ py: Python<'py>,
+ parent: &str,
+ left: &Bound<'py, PyAny>,
+ right: &Bound<'py, PyAny>,
+ tokenizer: &str,
+) -> PyResult> {
+ let tokenizer = parse_tokenizer(tokenizer)?;
+ let left = extract_text_with_cursors(left)?;
+ let right = extract_text_with_cursors(right)?;
+
+ let result = reconcile_text::reconcile(parent, &left, &right, &*tokenizer).apply();
+ text_with_cursors_to_dict(py, &result)
+}
+
+/// Merge three versions of text and return provenance history.
+///
+/// Like `reconcile`, but also returns which source each text span came from.
+///
+/// Args:
+/// parent: The original text that both sides diverged from.
+/// left: The left edit, either a string or a dict with "text" and "cursors" keys.
+/// right: The right edit, either a string or a dict with "text" and "cursors" keys.
+/// tokenizer: Tokenization strategy - "Word" (default), "Character", "Line", or "Markdown".
+///
+/// Returns:
+/// A dict with "text", "cursors", and "history" (list of dicts with "text" and "history" keys).
+#[pyfunction]
+#[pyo3(signature = (parent, left, right, tokenizer = "Word"))]
+fn reconcile_with_history<'py>(
+ py: Python<'py>,
+ parent: &str,
+ left: &Bound<'py, PyAny>,
+ right: &Bound<'py, PyAny>,
+ tokenizer: &str,
+) -> PyResult> {
+ let tokenizer = parse_tokenizer(tokenizer)?;
+ let left = extract_text_with_cursors(left)?;
+ let right = extract_text_with_cursors(right)?;
+
+ let reconciled = reconcile_text::reconcile(parent, &left, &right, &*tokenizer);
+ let (text_with_cursors, history_spans) = reconciled.apply_with_all();
+
+ let dict = text_with_cursors_to_dict(py, &text_with_cursors)?;
+
+ let history = PyList::new(
+ py,
+ history_spans.iter().map(|span| {
+ let d = PyDict::new(py);
+ d.set_item("text", span.text()).unwrap();
+ d.set_item("history", format!("{:?}", span.history()))
+ .unwrap();
+ d
+ }),
+ )?;
+ dict.set_item("history", history)?;
+
+ Ok(dict)
+}
+
+/// Generate a compact diff between two texts.
+///
+/// Returns a list of retain counts (positive ints), delete counts (negative ints),
+/// and inserted strings.
+///
+/// Args:
+/// parent: The original text.
+/// changed: The modified text, either a string or a dict with "text" and "cursors" keys.
+/// tokenizer: Tokenization strategy - "Word" (default), "Character", "Line", or "Markdown".
+///
+/// Returns:
+/// A list of ints and strings representing the diff.
+///
+/// Raises:
+/// ValueError: If the diff computation overflows.
+#[pyfunction]
+#[pyo3(signature = (parent, changed, tokenizer = "Word"))]
+fn diff<'py>(
+ py: Python<'py>,
+ parent: &str,
+ changed: &Bound<'py, PyAny>,
+ tokenizer: &str,
+) -> PyResult