From 07f3a5f7956891315fd8f55e08033732cfd11f03 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:29:56 +0100 Subject: [PATCH 01/37] Remove diff mod --- src/diffs.rs | 2 -- src/{diffs => }/raw_operation.rs | 16 ++++++++++---- src/utils.rs | 1 + src/{diffs/myers.rs => utils/myers_diff.rs} | 22 +++++++++---------- ...ils__myers_diff__tests__complex_diff.snap} | 3 +-- ...tils__myers_diff__tests__delete_only.snap} | 3 +-- ...myers_diff__tests__identical_content.snap} | 3 +-- ...tils__myers_diff__tests__insert_only.snap} | 3 +-- ...myers_diff__tests__prefix_and_suffix.snap} | 3 +-- 9 files changed, 29 insertions(+), 27 deletions(-) delete mode 100644 src/diffs.rs rename src/{diffs => }/raw_operation.rs (80%) rename src/{diffs/myers.rs => utils/myers_diff.rs} (95%) rename src/{diffs/snapshots/reconcile__diffs__myers__tests__complex_diff.snap => utils/snapshots/reconcile__utils__myers_diff__tests__complex_diff.snap} (95%) rename src/{diffs/snapshots/reconcile__diffs__myers__tests__delete_only.snap => utils/snapshots/reconcile__utils__myers_diff__tests__delete_only.snap} (89%) rename src/{diffs/snapshots/reconcile__diffs__myers__tests__identical_content.snap => utils/snapshots/reconcile__utils__myers_diff__tests__identical_content.snap} (92%) rename src/{diffs/snapshots/reconcile__diffs__myers__tests__insert_only.snap => utils/snapshots/reconcile__utils__myers_diff__tests__insert_only.snap} (89%) rename src/{diffs/snapshots/reconcile__diffs__myers__tests__prefix_and_suffix.snap => utils/snapshots/reconcile__utils__myers_diff__tests__prefix_and_suffix.snap} (95%) diff --git a/src/diffs.rs b/src/diffs.rs deleted file mode 100644 index b57139a..0000000 --- a/src/diffs.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod myers; -pub mod raw_operation; diff --git a/src/diffs/raw_operation.rs b/src/raw_operation.rs similarity index 80% rename from src/diffs/raw_operation.rs rename to src/raw_operation.rs index 2e4a0a7..6f87b84 100644 --- a/src/diffs/raw_operation.rs +++ b/src/raw_operation.rs @@ -1,9 +1,15 @@ -use crate::tokenizer::token::Token; +use std::fmt::Debug; +use crate::{tokenizer::token::Token, utils::myers_diff::myers_diff}; + +/// Text editing operation containing the to-be-changed `Tokens`-s. +/// +/// RawOperations can be joined together when the underlying tokens +/// allow for joining subseqeunt operations. #[derive(Debug, Clone, PartialEq)] pub enum RawOperation where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { Insert(Vec>), Delete(Vec>), @@ -12,8 +18,10 @@ where impl RawOperation where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { + pub fn vec_from(left: &[Token], right: &[Token]) -> Vec { myers_diff(left, right) } + pub fn tokens(&self) -> &Vec> { match self { RawOperation::Insert(tokens) @@ -41,7 +49,7 @@ where /// Extends the operation with another operation. Only operations of the /// same type as self can be used to extend self, otherwise the function /// will panic. - pub fn extend(self, other: RawOperation) -> RawOperation { + pub fn join(self, other: RawOperation) -> RawOperation { debug_assert!( std::mem::discriminant(&self) == std::mem::discriminant(&other), "Cannot extend operations of different types. This should have been handled before \ diff --git a/src/utils.rs b/src/utils.rs index 8d31e66..58d55b8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,5 +2,6 @@ pub mod common_prefix_len; pub mod common_suffix_len; pub mod find_longest_prefix_contained_within; pub mod history; +pub mod myers_diff; pub mod side; pub mod string_builder; diff --git a/src/diffs/myers.rs b/src/utils/myers_diff.rs similarity index 95% rename from src/diffs/myers.rs rename to src/utils/myers_diff.rs index 501eca8..b94270c 100644 --- a/src/diffs/myers.rs +++ b/src/utils/myers_diff.rs @@ -24,8 +24,8 @@ use std::{ vec, }; -use super::raw_operation::RawOperation; use crate::{ + raw_operation::RawOperation, tokenizer::token::Token, utils::{common_prefix_len::common_prefix_len, common_suffix_len::common_suffix_len}, }; @@ -35,8 +35,8 @@ use crate::{ /// Diff `old`, between indices `old_range` and `new` between indices /// `new_range`. /// -/// The returned `RawOperations` all have a token count of 1. -pub fn diff(old: &[Token], new: &[Token]) -> Vec> +/// The returned `RawOperations` each wrap a single token. +pub fn myers_diff(old: &[Token], new: &[Token]) -> Vec> where T: PartialEq + Clone + std::fmt::Debug, { @@ -57,7 +57,7 @@ where debug_assert!( result.iter().all(|op| op.tokens().len() == 1), - "All operations should be of length 1" + "All operations must be of length 1" ); result @@ -80,7 +80,7 @@ where #[derive(Debug)] struct V { offset: isize, - v: Vec, // Look into initializing this to -1 and storing isize + v: Vec, } impl V { @@ -312,14 +312,14 @@ mod tests { fn test_empty_diff() { let old: Vec> = vec![]; let new: Vec> = vec![]; - let result = diff(&old, &new); + let result = myers_diff(&old, &new); assert_eq!(result.len(), 0); } #[test] fn test_identical_content() { let content = vec!["a".into(), "b".into(), "c".into()]; - let result = diff(&content, &content); + let result = myers_diff(&content, &content); assert_debug_snapshot!(result); } @@ -327,7 +327,7 @@ mod tests { fn test_insert_only() { let old: Vec> = vec![]; let new: Vec> = vec!["a".into(), "b".into()]; - let result = diff(&old, &new); + let result = myers_diff(&old, &new); assert_debug_snapshot!(result); } @@ -335,7 +335,7 @@ mod tests { fn test_delete_only() { let old = vec!["a".into(), "b".into()]; let new: Vec> = vec![]; - let result = diff(&old, &new); + let result = myers_diff(&old, &new); assert_debug_snapshot!(result); } @@ -343,7 +343,7 @@ mod tests { fn test_prefix_and_suffix() { let old = vec!["a".into(), "b".into(), "c".into(), "d".into()]; let new = vec!["a".into(), "x".into(), "d".into()]; - let result = diff(&old, &new); + let result = myers_diff(&old, &new); assert_debug_snapshot!(result); } @@ -351,7 +351,7 @@ mod tests { fn test_complex_diff() { let old = vec!["a".into(), "b".into(), "c".into(), "d".into()]; let new = vec!["a".into(), "x".into(), "c".into(), "y".into()]; - let result = diff(&old, &new); + let result = myers_diff(&old, &new); assert_debug_snapshot!(result); } } diff --git a/src/diffs/snapshots/reconcile__diffs__myers__tests__complex_diff.snap b/src/utils/snapshots/reconcile__utils__myers_diff__tests__complex_diff.snap similarity index 95% rename from src/diffs/snapshots/reconcile__diffs__myers__tests__complex_diff.snap rename to src/utils/snapshots/reconcile__utils__myers_diff__tests__complex_diff.snap index 57ee086..98ef83c 100644 --- a/src/diffs/snapshots/reconcile__diffs__myers__tests__complex_diff.snap +++ b/src/utils/snapshots/reconcile__utils__myers_diff__tests__complex_diff.snap @@ -1,7 +1,6 @@ --- -source: reconcile/src/diffs/myers.rs +source: src/utils/myers_diff.rs expression: result -snapshot_kind: text --- [ Equal( diff --git a/src/diffs/snapshots/reconcile__diffs__myers__tests__delete_only.snap b/src/utils/snapshots/reconcile__utils__myers_diff__tests__delete_only.snap similarity index 89% rename from src/diffs/snapshots/reconcile__diffs__myers__tests__delete_only.snap rename to src/utils/snapshots/reconcile__utils__myers_diff__tests__delete_only.snap index 93bb529..1c01e1a 100644 --- a/src/diffs/snapshots/reconcile__diffs__myers__tests__delete_only.snap +++ b/src/utils/snapshots/reconcile__utils__myers_diff__tests__delete_only.snap @@ -1,7 +1,6 @@ --- -source: reconcile/src/diffs/myers.rs +source: src/utils/myers_diff.rs expression: result -snapshot_kind: text --- [ Delete( diff --git a/src/diffs/snapshots/reconcile__diffs__myers__tests__identical_content.snap b/src/utils/snapshots/reconcile__utils__myers_diff__tests__identical_content.snap similarity index 92% rename from src/diffs/snapshots/reconcile__diffs__myers__tests__identical_content.snap rename to src/utils/snapshots/reconcile__utils__myers_diff__tests__identical_content.snap index f82d4ac..f942a17 100644 --- a/src/diffs/snapshots/reconcile__diffs__myers__tests__identical_content.snap +++ b/src/utils/snapshots/reconcile__utils__myers_diff__tests__identical_content.snap @@ -1,7 +1,6 @@ --- -source: reconcile/src/diffs/myers.rs +source: src/utils/myers_diff.rs expression: result -snapshot_kind: text --- [ Equal( diff --git a/src/diffs/snapshots/reconcile__diffs__myers__tests__insert_only.snap b/src/utils/snapshots/reconcile__utils__myers_diff__tests__insert_only.snap similarity index 89% rename from src/diffs/snapshots/reconcile__diffs__myers__tests__insert_only.snap rename to src/utils/snapshots/reconcile__utils__myers_diff__tests__insert_only.snap index 0f61f3c..0b40f12 100644 --- a/src/diffs/snapshots/reconcile__diffs__myers__tests__insert_only.snap +++ b/src/utils/snapshots/reconcile__utils__myers_diff__tests__insert_only.snap @@ -1,7 +1,6 @@ --- -source: reconcile/src/diffs/myers.rs +source: src/utils/myers_diff.rs expression: result -snapshot_kind: text --- [ Insert( diff --git a/src/diffs/snapshots/reconcile__diffs__myers__tests__prefix_and_suffix.snap b/src/utils/snapshots/reconcile__utils__myers_diff__tests__prefix_and_suffix.snap similarity index 95% rename from src/diffs/snapshots/reconcile__diffs__myers__tests__prefix_and_suffix.snap rename to src/utils/snapshots/reconcile__utils__myers_diff__tests__prefix_and_suffix.snap index e50984f..59f4997 100644 --- a/src/diffs/snapshots/reconcile__diffs__myers__tests__prefix_and_suffix.snap +++ b/src/utils/snapshots/reconcile__utils__myers_diff__tests__prefix_and_suffix.snap @@ -1,7 +1,6 @@ --- -source: reconcile/src/diffs/myers.rs +source: src/utils/myers_diff.rs expression: result -snapshot_kind: text --- [ Equal( From bf7ceb16c6d0d18b0f40a9039b65b47b9bfbaa7f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:30:15 +0100 Subject: [PATCH 02/37] Run tests with more features --- .github/workflows/check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fbff055..c977dc4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -40,4 +40,6 @@ jobs: - name: Test run: | cargo test --verbose -- --include-ignored + cargo test --features serde + cargo test --features wasm wasm-pack test --node --features wasm From b53aa0c2b26580b98c47798fbea0a3927316b8c2 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:30:22 +0100 Subject: [PATCH 03/37] Update hidden files --- .vscode/settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 450f6a0..c8ff44a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "files.exclude": { - "**/dist": true, - "**/node_modules": true, - "**/snapshots": true, + "**/snapshots": true, // cargo-insta outputs + "pkg": true, // wasm-pack build directory + "target": true, // rust build directory } } \ No newline at end of file From c682520b886904f0adf2d0053024cba86c176b3d Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:30:35 +0100 Subject: [PATCH 04/37] Update imports --- src/operation_transformation/edited_text.rs | 4 ++-- src/operation_transformation/utils/cook_operations.rs | 4 +--- src/operation_transformation/utils/elongate_operations.rs | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 3e7f7be..47cde49 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -3,10 +3,10 @@ use serde::{Deserialize, Serialize}; use super::{CursorPosition, Operation, TextWithCursors}; use crate::{ - diffs::{myers::diff, raw_operation::RawOperation}, operation_transformation::utils::{ cook_operations::cook_operations, elongate_operations::elongate_operations, }, + raw_operation::RawOperation, tokenizer::{Tokenizer, word_tokenizer::word_tokenizer}, utils::{history::History, side::Side, string_builder::StringBuilder}, }; @@ -65,7 +65,7 @@ where let original_tokens = (tokenizer)(original); let updated_tokens = (tokenizer)(&updated.text); - let diff: Vec> = diff(&original_tokens, &updated_tokens); + let diff: Vec> = RawOperation::vec_from(&original_tokens, &updated_tokens); Self::new( original, diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs index 6ec3b43..8d09f6f 100644 --- a/src/operation_transformation/utils/cook_operations.rs +++ b/src/operation_transformation/utils/cook_operations.rs @@ -1,6 +1,4 @@ -use crate::{ - diffs::raw_operation::RawOperation, operation_transformation::Operation, utils::side::Side, -}; +use crate::{operation_transformation::Operation, raw_operation::RawOperation, utils::side::Side}; /// Turn raw operations into ordered operations while keeping track of the /// original token's indexes. diff --git a/src/operation_transformation/utils/elongate_operations.rs b/src/operation_transformation/utils/elongate_operations.rs index c0b25fb..57d1580 100644 --- a/src/operation_transformation/utils/elongate_operations.rs +++ b/src/operation_transformation/utils/elongate_operations.rs @@ -1,6 +1,6 @@ use core::iter; -use crate::diffs::raw_operation::RawOperation; +use crate::raw_operation::RawOperation; /// Elongates the operations by merging adjacent insertions and deletions that /// can be joined. This makes the subsequent merging of operations more @@ -24,7 +24,7 @@ where .flat_map(|next| match next { RawOperation::Insert(..) => match maybe_previous_insert.take() { Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => { - maybe_previous_insert = Some(prev.extend(next)); + maybe_previous_insert = Some(prev.join(next)); Box::new(iter::empty()) as Box>> } prev => { @@ -34,7 +34,7 @@ where }, RawOperation::Delete(..) => match maybe_previous_delete.take() { Some(prev) if prev.is_right_joinable() && next.is_left_joinable() => { - maybe_previous_delete = Some(prev.extend(next)); + maybe_previous_delete = Some(prev.join(next)); Box::new(iter::empty()) as Box>> } prev => { From 17f67602d92db520e3bcf011a214b4934f142adc Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:48:50 +0100 Subject: [PATCH 05/37] Improve docs & api --- src/operation_transformation/edited_text.rs | 2 +- src/operation_transformation/operation.rs | 4 ++-- src/utils/side.rs | 2 ++ src/utils/string_builder.rs | 24 +++++++++------------ 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 47cde49..34f42b8 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -222,7 +222,7 @@ where builder = operation.apply(builder); } - builder.build() + builder.take() } #[must_use] diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index ca2f128..652eb3e 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -421,7 +421,7 @@ mod tests { let mut builder = delete_operation.apply(builder); builder = retain_operation.apply(builder); - assert_eq!(builder.build(), "world"); + assert_eq!(builder.take(), "world"); } #[test] @@ -434,6 +434,6 @@ mod tests { let mut builder = retain_operation.apply(builder); builder = insert_operation.apply(builder); - assert_eq!(builder.build(), "hello my friend"); + assert_eq!(builder.take(), "hello my friend"); } } diff --git a/src/utils/side.rs b/src/utils/side.rs index 54dba6f..2bfa426 100644 --- a/src/utils/side.rs +++ b/src/utils/side.rs @@ -3,6 +3,8 @@ use std::fmt::Display; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +/// Pretty-printable flag to tell which conflicting edit (side) +/// an operation is associated with. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Side { diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs index ee23fc6..1656224 100644 --- a/src/utils/string_builder.rs +++ b/src/utils/string_builder.rs @@ -51,21 +51,17 @@ impl StringBuilder<'_> { } } - /// Returns the currently built buffer and clears it. + /// Returns the currently built buffer and clears it to allow consuming + /// the result incrementally. pub fn take(&mut self) -> String { - let result = self.buffer.clone(); + let result = self.buffer.clone(); // TODO: try removing this clone self.buffer.clear(); result } - /// Finish building the string after copying the remaining original string - /// since the last insertion or deletion. - pub fn build(self) -> String { self.buffer } - /// Get a slice of the remaining original string. The slice starts from /// where the next delete/retain operation would start and is of length - /// `length`. The implementation is quite suboptimal but it's only used - /// for debugging. + /// `length`. #[cfg(debug_assertions)] pub fn get_slice_from_remaining(&self, length: usize) -> String { let result = self.remaining.chars().take(length).collect::(); @@ -92,7 +88,7 @@ mod tests { builder.retain(8); builder.insert(" eee"); - assert_eq!(builder.build(), "ddd bbb ccc eee"); + assert_eq!(builder.take(), "ddd bbb ccc eee"); let original = "abcde"; let mut builder = StringBuilder::new(original); @@ -101,7 +97,7 @@ mod tests { builder.delete(3); builder.retain(1); - assert_eq!(builder.build(), "ae"); + assert_eq!(builder.take(), "ae"); } #[test] @@ -110,7 +106,7 @@ mod tests { let mut builder = StringBuilder::new(original); builder.insert("test"); - assert_eq!(builder.build(), "test"); + assert_eq!(builder.take(), "test"); } #[test] @@ -122,7 +118,7 @@ mod tests { builder.insert("世界, "); // Insert "World, " builder.retain(2); - assert_eq!(builder.build(), "こんに世界, ちは"); + assert_eq!(builder.take(), "こんに世界, ちは"); } #[test] @@ -145,7 +141,7 @@ mod tests { let mut builder = StringBuilder::new(original); builder.retain(original.len()); - assert_eq!(builder.build(), original); + assert_eq!(builder.take(), original); } #[test] @@ -155,6 +151,6 @@ mod tests { builder.delete(original.len()); builder.insert("Hi"); - assert_eq!(builder.build(), "Hi"); + assert_eq!(builder.take(), "Hi"); } } From c1aa8fe4631de4707af882921b3bdd3639b4ca0a Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:49:05 +0100 Subject: [PATCH 06/37] Remove function --- src/wasm/lib.rs | 11 ----------- tests/{web.rs => wasm.rs} | 15 +-------------- 2 files changed, 1 insertion(+), 25 deletions(-) rename tests/{web.rs => wasm.rs} (73%) diff --git a/src/wasm/lib.rs b/src/wasm/lib.rs index 71f8afe..1d15732 100644 --- a/src/wasm/lib.rs +++ b/src/wasm/lib.rs @@ -99,17 +99,6 @@ pub fn is_binary(data: &[u8]) -> bool { std::str::from_utf8(data).is_err() } -/// We don't want to support merging structured data like JSON, YAML, etc. -#[wasm_bindgen(js_name = isFileTypeMergable)] -#[must_use] -pub fn is_file_type_mergable(path_or_file_name: &str) -> bool { - set_panic_hook(); - - let file_extension = path_or_file_name.split('.').next_back().unwrap_or_default(); - - matches!(file_extension.to_lowercase().as_str(), "md" | "txt") -} - fn set_panic_hook() { // https://github.com/rustwasm/console_error_panic_hook#readme #[cfg(feature = "console_error_panic_hook")] diff --git a/tests/web.rs b/tests/wasm.rs similarity index 73% rename from tests/web.rs rename to tests/wasm.rs index 80da0f7..685bf24 100644 --- a/tests/web.rs +++ b/tests/wasm.rs @@ -1,7 +1,7 @@ #![cfg(feature = "wasm")] use reconcile::wasm::{ - lib::{is_binary, is_file_type_mergable, merge, merge_text, merge_text_with_cursors}, + lib::{is_binary, merge, merge_text, merge_text_with_cursors}, types::{JsCursorPosition, JsTextWithCursors}, }; use wasm_bindgen_test::*; @@ -65,16 +65,3 @@ fn test_is_binary() { fn test_is_binary_empty() { assert!(!is_binary(b"")); } - -#[wasm_bindgen_test(unsupported = test)] -fn test_is_file_type_mergable() { - assert!(is_file_type_mergable(".md")); - assert!(is_file_type_mergable("hi.md")); - assert!(is_file_type_mergable("my/path/to/my/document.md")); - assert!(is_file_type_mergable("hi.MD")); - assert!(is_file_type_mergable("my/path/to/my/DOCUMENT.MD")); - - assert!(!is_file_type_mergable(".json")); - assert!(!is_file_type_mergable("HELLO.JSON")); - assert!(!is_file_type_mergable("my/config.yml")); -} From d23fad5382fa4024c63cdb74469a6b0c2d1c135d Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:57:04 +0100 Subject: [PATCH 07/37] Import Debug instead of full reference --- src/operation_transformation.rs | 3 ++- src/operation_transformation/edited_text.rs | 6 ++++-- src/operation_transformation/operation.rs | 8 ++++---- src/operation_transformation/utils/cook_operations.rs | 4 +++- src/operation_transformation/utils/elongate_operations.rs | 3 ++- src/tokenizer/token.rs | 8 +++++--- src/utils/find_longest_prefix_contained_within.rs | 8 +++++--- src/utils/myers_diff.rs | 7 ++++--- 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index 7cd88d9..0ff9405 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -2,6 +2,7 @@ mod cursor; mod edited_text; mod operation; mod utils; +use std::fmt::Debug; pub use cursor::{CursorPosition, TextWithCursors}; pub use edited_text::EditedText; @@ -49,7 +50,7 @@ pub fn reconcile_with_tokenizer<'a, F, T>( tokenizer: &Tokenizer, ) -> TextWithCursors<'static> where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { let left_operations = EditedText::from_strings_with_tokenizer(original, left, tokenizer, Side::Left); diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 34f42b8..30643b1 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -27,7 +29,7 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Default)] pub struct EditedText<'a, T> where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { text: &'a str, operations: Vec>, @@ -49,7 +51,7 @@ impl<'a> EditedText<'a, String> { impl<'a, T> EditedText<'a, T> where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { /// Create an `EditedText` from the given original (old) and updated (new) /// strings. The returned `EditedText` represents the changes from the diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index 652eb3e..05aad3f 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -16,7 +16,7 @@ use crate::{ #[derive(Clone, PartialEq)] pub enum Operation where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { Equal { order: usize, @@ -46,7 +46,7 @@ where impl Operation where - T: PartialEq + Clone + std::fmt::Debug, + 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 @@ -332,7 +332,7 @@ where impl Display for Operation where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -400,7 +400,7 @@ where impl Debug for Operation where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{self}") } } diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs index 8d09f6f..e8ebd40 100644 --- a/src/operation_transformation/utils/cook_operations.rs +++ b/src/operation_transformation/utils/cook_operations.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + use crate::{operation_transformation::Operation, raw_operation::RawOperation, utils::side::Side}; /// Turn raw operations into ordered operations while keeping track of the @@ -5,7 +7,7 @@ use crate::{operation_transformation::Operation, raw_operation::RawOperation, ut pub fn cook_operations(raw_operations: I, side: Side) -> impl Iterator> where I: IntoIterator>, - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { let mut original_text_index = 0; // this is the start index of the operation on the original text diff --git a/src/operation_transformation/utils/elongate_operations.rs b/src/operation_transformation/utils/elongate_operations.rs index 57d1580..fd388be 100644 --- a/src/operation_transformation/utils/elongate_operations.rs +++ b/src/operation_transformation/utils/elongate_operations.rs @@ -1,4 +1,5 @@ use core::iter; +use std::fmt::Debug; use crate::raw_operation::RawOperation; @@ -8,7 +9,7 @@ use crate::raw_operation::RawOperation; pub fn elongate_operations(raw_operations: I) -> Vec> where I: IntoIterator>, - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { // This might look bad, but this makes sense. The inserts and deltes can be // interleaved, such as: IDIDID and we need to turn this into IIIDDD. diff --git a/src/tokenizer/token.rs b/src/tokenizer/token.rs index 0c12770..f2926af 100644 --- a/src/tokenizer/token.rs +++ b/src/tokenizer/token.rs @@ -1,3 +1,5 @@ +use std::fmt::Debug; + #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -12,7 +14,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct Token where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { /// The normalised form of the token used deriving the diff. normalised: T, @@ -35,7 +37,7 @@ impl From<&str> for Token { impl Token where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { pub fn new( normalised: T, @@ -62,7 +64,7 @@ where impl PartialEq for Token where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { fn eq(&self, other: &Self) -> bool { self.normalised == other.normalised } } diff --git a/src/utils/find_longest_prefix_contained_within.rs b/src/utils/find_longest_prefix_contained_within.rs index a04da4e..bccb06a 100644 --- a/src/utils/find_longest_prefix_contained_within.rs +++ b/src/utils/find_longest_prefix_contained_within.rs @@ -1,7 +1,9 @@ +use std::fmt::Debug; + use crate::Token; -/// Given two lists of tokens, returns `length` where `old` list somewhere -/// within contains the `length` prefix of the `new` list. +/// Given two lists of tokens, returns `length` where the `old` list +/// somewhere within contains the `length` prefix of the `new` list. /// /// ## Example /// @@ -25,7 +27,7 @@ use crate::Token; /// > results in a length of 1 pub fn find_longest_prefix_contained_within(old: &[Token], new: &[Token]) -> usize where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { let max_possible = new.len().min(old.len()); diff --git a/src/utils/myers_diff.rs b/src/utils/myers_diff.rs index b94270c..4215125 100644 --- a/src/utils/myers_diff.rs +++ b/src/utils/myers_diff.rs @@ -20,6 +20,7 @@ //! For potential improvements here see [similar#15](https://github.com/mitsuhiko/similar/issues/15). use std::{ + fmt::Debug, ops::{Index, IndexMut, Range}, vec, }; @@ -38,7 +39,7 @@ use crate::{ /// The returned `RawOperations` each wrap a single token. pub fn myers_diff(old: &[Token], new: &[Token]) -> Vec> where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { let max_d = (old.len() + new.len()).div_ceil(2) + 1; let mut vb = V::new(max_d); @@ -130,7 +131,7 @@ fn find_middle_snake( vb: &mut V, ) -> Option<(usize, usize)> where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { let n = old_range.len(); let m = new_range.len(); @@ -236,7 +237,7 @@ fn conquer( vb: &mut V, result: &mut Vec>, ) where - T: PartialEq + Clone + std::fmt::Debug, + T: PartialEq + Clone + Debug, { // Check for common prefix let common_prefix_len = common_prefix_len(old, old_range.clone(), new, new_range.clone()); From 806a5bc113ce63749eaf52b20edd457eacee0c43 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 15:57:12 +0100 Subject: [PATCH 08/37] Add docs for History --- src/utils/history.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/history.rs b/src/utils/history.rs index 611132a..cfba9c8 100644 --- a/src/utils/history.rs +++ b/src/utils/history.rs @@ -12,6 +12,8 @@ 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. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg(not(feature = "wasm"))] pub enum History { From b18a692d461f6754f10177139de4de15f3821793 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 16:22:32 +0100 Subject: [PATCH 09/37] Extract types into a separate module --- src/operation_transformation/edited_text.rs | 26 +++++++++++++++------ src/operation_transformation/operation.rs | 4 ++-- src/types.rs | 3 +++ src/{utils => types}/history.rs | 4 ++++ src/{utils => types}/side.rs | 0 src/types/text_with_history.rs | 26 +++++++++++++++++++++ src/utils.rs | 2 -- src/wasm/types.rs | 24 +++---------------- 8 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 src/types.rs rename src/{utils => types}/history.rs (79%) rename src/{utils => types}/side.rs (100%) create mode 100644 src/types/text_with_history.rs diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 30643b1..4b2bdc2 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -10,7 +10,8 @@ use crate::{ }, raw_operation::RawOperation, tokenizer::{Tokenizer, word_tokenizer::word_tokenizer}, - utils::{history::History, side::Side, string_builder::StringBuilder}, + types::{history::History, side::Side, text_with_history::TextWithHistory}, + utils::string_builder::StringBuilder, }; /// A text document and a sequence of operations that can be applied to the text @@ -228,7 +229,7 @@ where } #[must_use] - pub fn apply_with_history(&self) -> Vec<(History, String)> { + pub fn apply_with_history(&self) -> Vec { let mut builder: StringBuilder<'_> = StringBuilder::new(self.text); let mut history = Vec::with_capacity(self.operations.len()); @@ -237,10 +238,17 @@ where builder = operation.apply(builder); match operation { - Operation::Equal { .. } => history.push((History::Unchanged, builder.take())), + Operation::Equal { .. } => { + history.push(TextWithHistory::new(History::Unchanged, builder.take())) + } Operation::Insert { side, .. } => match side { - Side::Left => history.push((History::AddedFromLeft, builder.take())), - Side::Right => history.push((History::AddedFromRight, builder.take())), + Side::Left => { + history.push(TextWithHistory::new(History::AddedFromLeft, builder.take())) + } + Side::Right => history.push(TextWithHistory::new( + History::AddedFromRight, + builder.take(), + )), }, Operation::Delete { deleted_character_count, @@ -250,8 +258,12 @@ where } => { let deleted = self.text[*order..*order + *deleted_character_count].to_string(); match side { - Side::Left => history.push((History::RemovedFromLeft, deleted)), - Side::Right => history.push((History::RemovedFromRight, deleted)), + Side::Left => { + history.push(TextWithHistory::new(History::RemovedFromLeft, deleted)) + } + Side::Right => { + history.push(TextWithHistory::new(History::RemovedFromRight, deleted)) + } } } } diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index 05aad3f..ef0a674 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -4,9 +4,9 @@ use core::fmt::{Debug, Display}; use serde::{Deserialize, Serialize}; use crate::{ - Token, + Side, Token, utils::{ - find_longest_prefix_contained_within::find_longest_prefix_contained_within, side::Side, + find_longest_prefix_contained_within::find_longest_prefix_contained_within, string_builder::StringBuilder, }, }; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5dde9b3 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,3 @@ +pub mod history; +pub mod side; +pub mod text_with_history; diff --git a/src/utils/history.rs b/src/types/history.rs similarity index 79% rename from src/utils/history.rs rename to src/types/history.rs index cfba9c8..e30ae90 100644 --- a/src/utils/history.rs +++ b/src/types/history.rs @@ -1,7 +1,10 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; #[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg(feature = "wasm")] pub enum History { @@ -16,6 +19,7 @@ pub enum History { /// When compiled to WASM, the enum values are the same as their names. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg(not(feature = "wasm"))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum History { Unchanged, AddedFromLeft, diff --git a/src/utils/side.rs b/src/types/side.rs similarity index 100% rename from src/utils/side.rs rename to src/types/side.rs diff --git a/src/types/text_with_history.rs b/src/types/text_with_history.rs new file mode 100644 index 0000000..06aff78 --- /dev/null +++ b/src/types/text_with_history.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::types::history::History; + +/// Wrapper type to expose `(History, String)` to JS. +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq)] +pub struct TextWithHistory { + history: History, + text: String, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl TextWithHistory { + pub fn new(history: History, text: String) -> Self { TextWithHistory { history, text } } + + #[must_use] + pub fn history(&self) -> History { self.history } + + #[must_use] + pub fn text(&self) -> String { self.text.clone() } +} diff --git a/src/utils.rs b/src/utils.rs index 58d55b8..2e05a70 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,5 @@ pub mod common_prefix_len; pub mod common_suffix_len; pub mod find_longest_prefix_contained_within; -pub mod history; pub mod myers_diff; -pub mod side; pub mod string_builder; diff --git a/src/wasm/types.rs b/src/wasm/types.rs index d2aa486..f18d224 100644 --- a/src/wasm/types.rs +++ b/src/wasm/types.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::History; @@ -88,24 +91,3 @@ impl From for JsCursorPosition { } } } - -/// Wrapper type to expose `(History, String)` to JS. -#[wasm_bindgen] -#[derive(Debug, Clone, PartialEq)] -pub struct JsTextWithHistory { - history: History, - text: String, -} - -impl From<(History, String)> for JsTextWithHistory { - fn from((history, text): (History, String)) -> Self { JsTextWithHistory { history, text } } -} - -#[wasm_bindgen] -impl JsTextWithHistory { - #[must_use] - pub fn history(&self) -> History { self.history } - - #[must_use] - pub fn text(&self) -> String { self.text.clone() } -} From 5378ffb5476adbd36b9d83fc2fc2ee93e2481b76 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 17:42:37 +0100 Subject: [PATCH 10/37] Unify WASM and Rust API types --- src/operation_transformation/cursor.rs | 57 ---------- src/operation_transformation/edited_text.rs | 31 ++--- .../utils/cook_operations.rs | 2 +- src/raw_operation.rs | 2 +- src/types.rs | 2 + src/types/cursor_position.rs | 36 ++++++ src/types/text_with_cursors.rs | 47 ++++++++ src/types/text_with_history.rs | 3 +- src/utils.rs | 1 + src/utils/is_binary.rs | 24 ++++ src/wasm.rs | 102 ++++++++++++++++- src/wasm/lib.rs | 106 ------------------ src/wasm/types.rs | 93 --------------- tests/example_document.rs | 16 +-- tests/test.rs | 16 +-- tests/wasm.rs | 15 +-- 16 files changed, 252 insertions(+), 301 deletions(-) delete mode 100644 src/operation_transformation/cursor.rs create mode 100644 src/types/cursor_position.rs create mode 100644 src/types/text_with_cursors.rs create mode 100644 src/utils/is_binary.rs delete mode 100644 src/wasm/lib.rs delete mode 100644 src/wasm/types.rs diff --git a/src/operation_transformation/cursor.rs b/src/operation_transformation/cursor.rs deleted file mode 100644 index f145273..0000000 --- a/src/operation_transformation/cursor.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::borrow::Cow; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -// CursorPosition represents the position of an identifiable cursor in a text -// document based on its (UTF-8) character index. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Default)] -pub struct CursorPosition { - pub id: usize, - pub char_index: usize, -} - -impl CursorPosition { - #[must_use] - pub fn with_index(&self, index: usize) -> Self { - CursorPosition { - id: self.id, - char_index: index, - } - } -} - -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, Clone, PartialEq, Default)] -pub struct TextWithCursors<'a> { - pub text: Cow<'a, str>, - pub cursors: Vec, -} - -impl<'a> TextWithCursors<'a> { - #[must_use] - pub fn new(text: &'a str, cursors: Vec) -> Self { - Self { - text: text.into(), - cursors, - } - } - - #[must_use] - pub fn new_owned(text: String, cursors: Vec) -> Self { - Self { - text: text.into(), - cursors, - } - } -} - -impl<'a> From<&'a str> for TextWithCursors<'a> { - fn from(text: &'a str) -> Self { - Self { - text: text.into(), - cursors: Vec::new(), - } - } -} diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 4b2bdc2..08dcee3 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -3,10 +3,11 @@ use std::fmt::Debug; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use super::{CursorPosition, Operation, TextWithCursors}; use crate::{ - operation_transformation::utils::{ - cook_operations::cook_operations, elongate_operations::elongate_operations, + CursorPosition, TextWithCursors, + operation_transformation::{ + Operation, + utils::{cook_operations::cook_operations, elongate_operations::elongate_operations}, }, raw_operation::RawOperation, tokenizer::{Tokenizer, word_tokenizer::word_tokenizer}, @@ -45,7 +46,7 @@ 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<'a>, side: Side) -> Self { + pub fn from_strings(original: &'a str, updated: &TextWithCursors, side: Side) -> Self { Self::from_strings_with_tokenizer(original, updated, &word_tokenizer, side) } } @@ -61,19 +62,19 @@ where /// function is used to tokenize the text. pub fn from_strings_with_tokenizer( original: &'a str, - updated: TextWithCursors<'a>, + updated: &TextWithCursors, tokenizer: &Tokenizer, side: Side, ) -> Self { let original_tokens = (tokenizer)(original); - let updated_tokens = (tokenizer)(&updated.text); + let updated_tokens = (tokenizer)(&updated.text()); let diff: Vec> = RawOperation::vec_from(&original_tokens, &updated_tokens); Self::new( original, cook_operations(elongate_operations(diff), side).collect(), - updated.cursors, + updated.cursors(), ) } @@ -239,11 +240,11 @@ where match operation { Operation::Equal { .. } => { - history.push(TextWithHistory::new(History::Unchanged, builder.take())) + history.push(TextWithHistory::new(History::Unchanged, builder.take())); } Operation::Insert { side, .. } => match side { Side::Left => { - history.push(TextWithHistory::new(History::AddedFromLeft, builder.take())) + history.push(TextWithHistory::new(History::AddedFromLeft, builder.take())); } Side::Right => history.push(TextWithHistory::new( History::AddedFromRight, @@ -259,10 +260,10 @@ where let deleted = self.text[*order..*order + *deleted_character_count].to_string(); match side { Side::Left => { - history.push(TextWithHistory::new(History::RemovedFromLeft, deleted)) + history.push(TextWithHistory::new(History::RemovedFromLeft, deleted)); } Side::Right => { - history.push(TextWithHistory::new(History::RemovedFromRight, deleted)) + history.push(TextWithHistory::new(History::RemovedFromRight, deleted)); } } } @@ -285,7 +286,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(), Side::Right); insta::assert_debug_snapshot!(operations); @@ -297,7 +298,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(), Side::Right); assert_debug_snapshot!(operations); @@ -312,8 +313,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(), Side::Left); + let operations_2 = EditedText::from_strings(original, &right.into(), Side::Right); let operations = operations_1.merge(operations_2); assert_eq!(operations.apply(), expected); diff --git a/src/operation_transformation/utils/cook_operations.rs b/src/operation_transformation/utils/cook_operations.rs index e8ebd40..0b188cc 100644 --- a/src/operation_transformation/utils/cook_operations.rs +++ b/src/operation_transformation/utils/cook_operations.rs @@ -1,6 +1,6 @@ use std::fmt::Debug; -use crate::{operation_transformation::Operation, raw_operation::RawOperation, utils::side::Side}; +use crate::{operation_transformation::Operation, raw_operation::RawOperation, types::side::Side}; /// Turn raw operations into ordered operations while keeping track of the /// original token's indexes. diff --git a/src/raw_operation.rs b/src/raw_operation.rs index 6f87b84..fb55293 100644 --- a/src/raw_operation.rs +++ b/src/raw_operation.rs @@ -4,7 +4,7 @@ use crate::{tokenizer::token::Token, utils::myers_diff::myers_diff}; /// Text editing operation containing the to-be-changed `Tokens`-s. /// -/// RawOperations can be joined together when the underlying tokens +/// `RawOperations` can be joined together when the underlying tokens /// allow for joining subseqeunt operations. #[derive(Debug, Clone, PartialEq)] pub enum RawOperation diff --git a/src/types.rs b/src/types.rs index 5dde9b3..b151312 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,5 @@ +pub mod cursor_position; pub mod history; pub mod side; +pub mod text_with_cursors; pub mod text_with_history; diff --git a/src/types/cursor_position.rs b/src/types/cursor_position.rs new file mode 100644 index 0000000..f7abba7 --- /dev/null +++ b/src/types/cursor_position.rs @@ -0,0 +1,36 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +// CursorPosition represents the position of an identifiable cursor in a text +// document based on its (UTF-8) character index. +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, PartialEq, Default)] +pub struct CursorPosition { + pub id: usize, + pub char_index: usize, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl CursorPosition { + #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] + #[must_use] + pub fn new(id: usize, char_index: usize) -> Self { Self { id, char_index } } + + #[must_use] + pub fn with_index(&self, index: usize) -> Self { + CursorPosition { + id: self.id, + char_index: index, + } + } + + #[must_use] + pub fn id(&self) -> usize { self.id } + + #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = characterPosition))] + #[must_use] + pub fn char_index(&self) -> usize { self.char_index } +} diff --git a/src/types/text_with_cursors.rs b/src/types/text_with_cursors.rs new file mode 100644 index 0000000..1007f7a --- /dev/null +++ b/src/types/text_with_cursors.rs @@ -0,0 +1,47 @@ +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::types::cursor_position::CursorPosition; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(Debug, Clone, PartialEq, Default)] +pub struct TextWithCursors { + text: String, // wasm-pack doesn't support generics so we can't use Cow here + cursors: Vec, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl TextWithCursors { + #[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!( + cursor.char_index <= length, + // cursor.char_index == length means that the cursor is at the end + "Cursor positions must be contained within the text or just after the end" + ); + } + + Self { text, cursors } + } + + #[must_use] + pub fn text(&self) -> String { self.text.to_string() } + + #[must_use] + pub fn cursors(&self) -> Vec { self.cursors.clone() } + + #[must_use] + pub fn new_owned(text: String, cursors: Vec) -> Self { Self { text, cursors } } +} + +impl<'a> From<&'a str> for TextWithCursors { + fn from(text: &'a str) -> Self { + Self { + text: text.into(), + cursors: Vec::new(), + } + } +} diff --git a/src/types/text_with_history.rs b/src/types/text_with_history.rs index 06aff78..b176178 100644 --- a/src/types/text_with_history.rs +++ b/src/types/text_with_history.rs @@ -5,7 +5,7 @@ use wasm_bindgen::prelude::*; use crate::types::history::History; -/// Wrapper type to expose `(History, String)` to JS. +/// Wrapper type for `(History, String)` #[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] @@ -16,6 +16,7 @@ pub struct TextWithHistory { #[cfg_attr(feature = "wasm", wasm_bindgen)] impl TextWithHistory { + #[must_use] pub fn new(history: History, text: String) -> Self { TextWithHistory { history, text } } #[must_use] diff --git a/src/utils.rs b/src/utils.rs index 2e05a70..f249825 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ 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 new file mode 100644 index 0000000..46488e3 --- /dev/null +++ b/src/utils/is_binary.rs @@ -0,0 +1,24 @@ +/// Heuristically determine if the given data is a binary or a text file's +/// content. +#[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/wasm.rs b/src/wasm.rs index bf13f0a..73bbb86 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,2 +1,100 @@ -pub mod lib; -pub mod types; +//! This crate provides utilities for easily communicating between backend & +//! frontend and ensuring the same logic for encoding and decoding binary data, +//! and 3-way-merging documents in Rust and JavaScript. +//! +//! The crate is designed to be used as a Rust library and as a +//! TypeScript/JavaScript package through WebAssembly (WASM). +//! +//! # Modules +//! +//! - `errors`: Contains error types used in this crate. + +use core::str; + +use wasm_bindgen::prelude::*; + +use crate::{ + TextWithCursors, TextWithHistory, reconcile, reconcile_with_cursors, reconcile_with_history, +}; + +/// Merge two documents with a common parent. Relies on `reconcile::reconcile` +/// for texts and returns the right document as-is if either of the updated +/// documents is binary. +/// +/// # Arguments +/// +/// - `parent`: The common parent document. +/// - `left`: The left document updated by one user. +/// - `right`: The right document updated by another user. +/// +/// # Returns +/// +/// The merged document. +/// +/// # Panics +/// +/// If any of the input documents are not valid UTF-8 strings. +#[wasm_bindgen] +#[must_use] +pub fn merge(parent: &[u8], left: &[u8], right: &[u8]) -> Vec { + set_panic_hook(); + + if is_binary(parent) || is_binary(left) || is_binary(right) { + right.to_vec() + } else { + 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"), + str::from_utf8(right).expect("right must be valid UTF-8 because it's not binary"), + ) + .into_bytes() + } +} + +/// WASM wrapper around `reconcile` for merging text. +#[wasm_bindgen(js_name = mergeText)] +#[must_use] +pub fn merge_text(parent: &str, left: &str, right: &str) -> String { + set_panic_hook(); + + reconcile(parent, left, right) +} + +/// WASM wrapper around `reconcile` for merging text. +#[wasm_bindgen(js_name = mergeTextWithHistory)] +#[must_use] +pub fn merge_text_with_history(parent: &str, left: &str, right: &str) -> Vec { + set_panic_hook(); + + reconcile_with_history(parent, left, right) + .into_iter() + .collect() +} + +/// WASM wrapper around `reconcile::reconcile_with_cursors` for merging text. +#[wasm_bindgen(js_name = mergeTextWithCursors)] +#[must_use] +pub fn merge_text_with_cursors( + parent: &str, + left: &TextWithCursors, + right: &TextWithCursors, +) -> TextWithCursors { + set_panic_hook(); + + reconcile_with_cursors(parent, left, right) +} + +/// 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) +} + +fn set_panic_hook() { + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/src/wasm/lib.rs b/src/wasm/lib.rs deleted file mode 100644 index 1d15732..0000000 --- a/src/wasm/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! This crate provides utilities for easily communicating between backend & -//! frontend and ensuring the same logic for encoding and decoding binary data, -//! and 3-way-merging documents in Rust and JavaScript. -//! -//! The crate is designed to be used as a Rust library and as a -//! TypeScript/JavaScript package through WebAssembly (WASM). -//! -//! # Modules -//! -//! - `errors`: Contains error types used in this crate. - -use core::str; - -use wasm_bindgen::prelude::*; - -use crate::wasm::types::{JsTextWithCursors, JsTextWithHistory}; - -/// Merge two documents with a common parent. Relies on `reconcile::reconcile` -/// for texts and returns the right document as-is if either of the updated -/// documents is binary. -/// -/// # Arguments -/// -/// - `parent`: The common parent document. -/// - `left`: The left document updated by one user. -/// - `right`: The right document updated by another user. -/// -/// # Returns -/// -/// The merged document. -/// -/// # Panics -/// -/// If any of the input documents are not valid UTF-8 strings. -#[wasm_bindgen] -#[must_use] -pub fn merge(parent: &[u8], left: &[u8], right: &[u8]) -> Vec { - set_panic_hook(); - - if is_binary(parent) || is_binary(left) || is_binary(right) { - right.to_vec() - } 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"), - str::from_utf8(right).expect("right must be valid UTF-8 because it's not binary"), - ) - .into_bytes() - } -} - -/// WASM wrapper around `crate::reconcile` for merging text. -#[wasm_bindgen(js_name = mergeText)] -#[must_use] -pub fn merge_text(parent: &str, left: &str, right: &str) -> String { - set_panic_hook(); - - crate::reconcile(parent, left, right) -} - -/// WASM wrapper around `crate::reconcile` for merging text. -#[wasm_bindgen(js_name = mergeTextWithHistory)] -#[must_use] -pub fn merge_text_with_history(parent: &str, left: &str, right: &str) -> Vec { - set_panic_hook(); - - crate::reconcile_with_history(parent, left, right) - .into_iter() - .map(Into::into) - .collect() -} - -/// WASM wrapper around `reconcile::reconcile_with_cursors` for merging text. -#[wasm_bindgen(js_name = mergeTextWithCursors)] -#[must_use] -pub fn merge_text_with_cursors( - parent: &str, - left: JsTextWithCursors, - right: JsTextWithCursors, -) -> JsTextWithCursors { - set_panic_hook(); - - crate::reconcile_with_cursors(parent, left.into(), right.into()).into() -} - -/// 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(); - - 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() -} - -fn set_panic_hook() { - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} diff --git a/src/wasm/types.rs b/src/wasm/types.rs deleted file mode 100644 index f18d224..0000000 --- a/src/wasm/types.rs +++ /dev/null @@ -1,93 +0,0 @@ -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::*; - -use crate::History; - -/// Wrapper type to expose `TextWithCursors` to JS. -#[wasm_bindgen] -#[derive(Debug, Clone, PartialEq)] -pub struct JsTextWithCursors { - text: String, - cursors: Vec, -} - -#[wasm_bindgen] -impl JsTextWithCursors { - #[wasm_bindgen(constructor)] - #[must_use] - pub fn new(text: String, cursors: Vec) -> Self { Self { text, cursors } } - - #[must_use] - pub fn text(&self) -> String { self.text.clone() } - - #[must_use] - pub fn cursors(&self) -> Vec { self.cursors.clone() } -} - -impl From for crate::TextWithCursors<'_> { - fn from(owned: JsTextWithCursors) -> Self { - crate::TextWithCursors::new_owned( - owned.text.to_string(), - owned - .cursors - .into_iter() - .map(std::convert::Into::into) - .collect(), - ) - } -} - -impl From> for JsTextWithCursors { - fn from(text_with_cursors: crate::TextWithCursors<'_>) -> Self { - JsTextWithCursors { - text: text_with_cursors.text.into_owned(), - cursors: text_with_cursors - .cursors - .into_iter() - .map(std::convert::Into::into) - .collect(), - } - } -} - -/// Wrapper type to expose `CursorPosition` to JS. -#[wasm_bindgen] -#[derive(Debug, Clone, PartialEq)] -pub struct JsCursorPosition { - id: usize, - char_index: usize, -} - -#[wasm_bindgen] -impl JsCursorPosition { - #[wasm_bindgen(constructor)] - #[must_use] - pub fn new(id: usize, char_index: usize) -> Self { Self { id, char_index } } - - #[must_use] - pub fn id(&self) -> usize { self.id } - - #[wasm_bindgen(js_name = characterPosition)] - #[must_use] - pub fn char_index(&self) -> usize { self.char_index } -} - -impl From for crate::CursorPosition { - fn from(owned: JsCursorPosition) -> Self { - crate::CursorPosition { - id: owned.id, - char_index: owned.char_index, - } - } -} - -impl From for JsCursorPosition { - fn from(cursor: crate::CursorPosition) -> Self { - JsCursorPosition { - id: cursor.id, - char_index: cursor.char_index, - } - } -} diff --git a/tests/example_document.rs b/tests/example_document.rs index 277e49e..08e39a2 100644 --- a/tests/example_document.rs +++ b/tests/example_document.rs @@ -21,12 +21,12 @@ impl ExampleDocument { pub fn parent(&self) -> String { self.parent.clone() } #[must_use] - pub fn left(&self) -> TextWithCursors<'static> { + pub fn left(&self) -> TextWithCursors { ExampleDocument::string_to_text_with_cursors(&self.left) } #[must_use] - pub fn right(&self) -> TextWithCursors<'static> { + pub fn right(&self) -> TextWithCursors { ExampleDocument::string_to_text_with_cursors(&self.right) } @@ -37,7 +37,7 @@ impl ExampleDocument { /// /// If the result string does not match the expected string, the program /// will panic. - pub fn assert_eq(&self, result: &TextWithCursors<'static>) { + pub fn assert_eq(&self, result: &TextWithCursors) { let result_str = ExampleDocument::text_with_cursors_to_string(result); assert_eq!( self.expected, result_str, @@ -53,16 +53,16 @@ impl ExampleDocument { /// If the result string does not match the expected string, the program /// will panic. pub fn assert_eq_without_cursors(&self, result: &str) { - let expected = ExampleDocument::string_to_text_with_cursors(&self.expected).text; + let expected = ExampleDocument::string_to_text_with_cursors(&self.expected).text(); assert_eq!( expected, result, "Left (expected) isn't equal to right (actual), Actual: ```\n{result}```", ); } - fn text_with_cursors_to_string(text: &TextWithCursors<'_>) -> String { - let mut result = text.text.clone().into_owned(); - for (i, cursor) in text.cursors.iter().enumerate() { + fn text_with_cursors_to_string(document: &TextWithCursors) -> String { + let mut result = document.text().clone(); + for (i, cursor) in document.cursors().iter().enumerate() { assert!( cursor.char_index <= result.len(), // equals in case of insert at the end "Cursor index out of bounds: {} > {} when testing for '{result}'", @@ -82,7 +82,7 @@ impl ExampleDocument { result } - fn string_to_text_with_cursors(text: &str) -> TextWithCursors<'static> { + fn string_to_text_with_cursors(text: &str) -> TextWithCursors { let cursors = Self::parse_cursors(text); let text = text.replace('|', ""); TextWithCursors::new_owned(text, cursors) diff --git a/tests/test.rs b/tests/test.rs index 088a93e..30d39bb 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -11,8 +11,8 @@ fn test_document_one_way_without_cursors() { for doc in &get_all_documents() { doc.assert_eq_without_cursors(&reconcile( &doc.parent(), - &doc.left().text, - &doc.right().text, + &doc.left().text(), + &doc.right().text(), )); } } @@ -22,8 +22,8 @@ fn test_document_one_way_with_cursors() { for doc in &get_all_documents() { doc.assert_eq(&reconcile_with_cursors( &doc.parent(), - doc.left(), - doc.right(), + &doc.left(), + &doc.right(), )); } } @@ -33,8 +33,8 @@ fn test_document_inverse_way_without_cursors() { for doc in &get_all_documents() { doc.assert_eq_without_cursors(&reconcile( &doc.parent(), - &doc.right().text, - &doc.left().text, + &doc.right().text(), + &doc.left().text(), )); } } @@ -44,8 +44,8 @@ fn test_document_inverse_way_with_cursors() { for doc in &get_all_documents() { doc.assert_eq(&reconcile_with_cursors( &doc.parent(), - doc.right(), - doc.left(), + &doc.right(), + &doc.left(), )); } } diff --git a/tests/wasm.rs b/tests/wasm.rs index 685bf24..d081b28 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -1,9 +1,6 @@ #![cfg(feature = "wasm")] -use reconcile::wasm::{ - lib::{is_binary, merge, merge_text, merge_text_with_cursors}, - types::{JsCursorPosition, JsTextWithCursors}, -}; +use reconcile::{CursorPosition, TextWithCursors, wasm::*}; use wasm_bindgen_test::*; #[wasm_bindgen_test(unsupported = test)] @@ -31,18 +28,18 @@ fn test_merge_text() { fn test_merge_text_with_cursors() { let result = merge_text_with_cursors( "hi", - JsTextWithCursors::new("hi world".to_owned(), vec![]), - JsTextWithCursors::new( + &TextWithCursors::new("hi world".to_owned(), vec![]), + &TextWithCursors::new( "hi".to_owned(), - vec![JsCursorPosition::new(0, 1), JsCursorPosition::new(1, 2)], + vec![CursorPosition::new(0, 1), CursorPosition::new(1, 2)], ), ); assert_eq!( result, - JsTextWithCursors::new( + TextWithCursors::new( "hi world".to_owned(), - vec![JsCursorPosition::new(0, 1), JsCursorPosition::new(1, 2)] + vec![CursorPosition::new(0, 1), CursorPosition::new(1, 2)] ), ); } From 0448e30dd90c66bc014612ffc604f0b27ddc1af0 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 19:01:31 +0100 Subject: [PATCH 11/37] Add helper scripts --- scripts/lint.sh | 8 ++++++++ scripts/test.sh | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100755 scripts/lint.sh create mode 100755 scripts/test.sh diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 0000000..9c692c8 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged +cargo fmt --all + +echo "Success!" \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..236717c --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +wasm-pack build --target web --features wasm +cargo test --verbose +cargo test --features serde +cargo test --features wasm +wasm-pack test --node --features wasm + +echo "Success!" \ No newline at end of file From 4fda83fe171f00b7a74abde8ed424169161dd06c Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 19:03:55 +0100 Subject: [PATCH 12/37] Remove the exponential API --- src/operation_transformation.rs | 134 ++++++++++---------- src/operation_transformation/edited_text.rs | 19 +-- src/tokenizer.rs | 39 +++++- src/types/text_with_cursors.rs | 21 ++- src/wasm.rs | 113 +++++++++++------ tests/example_document.rs | 18 +-- tests/test.rs | 38 ++++-- tests/wasm.rs | 16 ++- 8 files changed, 248 insertions(+), 150 deletions(-) diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index 0ff9405..bc6ff2d 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -1,54 +1,49 @@ -mod cursor; mod edited_text; mod operation; mod utils; use std::fmt::Debug; -pub use cursor::{CursorPosition, TextWithCursors}; pub use edited_text::EditedText; pub use operation::Operation; use crate::{ Tokenizer, - utils::{history::History, side::Side}, + types::{side::Side, text_with_cursors::TextWithCursors}, }; +/// Given an `original` document and two concurrent edits to it, +/// return a document containing all changes from both `left` +/// and `right`. +/// +/// If a span has been inserted in either the `left` or `right` +/// versions, it will be present in the return value. If both sides +/// insert the same span with a common prefix, that prefix will only +/// be present once in the output. +/// +/// Deletes are preserved from both sides. This means that an insert +/// from one side into a deleted span from the other side will result +/// in the removal of the original span but keeping the inserted text. +/// +/// The function supports UTF-8. The arguments are tokenized at the +/// granularity of words. +/// +/// ``` +/// use reconcile::{reconcile, BuiltinTokenizer}; +/// +/// let parent = "Merging text is hard!"; +/// let left = "Merging text is easy!"; +/// let right = "With reconcile, merging documents is hard!"; +/// +/// let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +/// assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); +/// ``` #[must_use] -pub fn reconcile(original: &str, left: &str, right: &str) -> String { - reconcile_with_cursors(original, left.into(), right.into()) - .text - .to_string() -} - -#[must_use] -pub fn reconcile_with_history(original: &str, left: &str, right: &str) -> Vec<(History, String)> { - let left_operations = EditedText::from_strings(original, left.into(), Side::Left); - let right_operations = EditedText::from_strings(original, right.into(), Side::Right); - - left_operations.merge(right_operations).apply_with_history() -} - -#[must_use] -pub fn reconcile_with_cursors<'a>( +pub fn reconcile<'a, T>( original: &'a str, - left: TextWithCursors<'a>, - right: TextWithCursors<'a>, -) -> TextWithCursors<'static> { - let left_operations = EditedText::from_strings(original, left, Side::Left); - let right_operations = EditedText::from_strings(original, right, Side::Right); - - let merged_operations = left_operations.merge(right_operations); - - TextWithCursors::new_owned(merged_operations.apply(), merged_operations.cursors) -} - -#[must_use] -pub fn reconcile_with_tokenizer<'a, F, T>( - original: &str, - left: TextWithCursors<'a>, - right: TextWithCursors<'a>, + left: &TextWithCursors, + right: &TextWithCursors, tokenizer: &Tokenizer, -) -> TextWithCursors<'static> +) -> EditedText<'a, T> where T: PartialEq + Clone + Debug, { @@ -57,9 +52,7 @@ where let right_operations = EditedText::from_strings_with_tokenizer(original, right, tokenizer, Side::Right); - let merged_operations = left_operations.merge(right_operations); - - TextWithCursors::new_owned(merged_operations.apply(), merged_operations.cursors) + left_operations.merge(right_operations) } #[cfg(test)] @@ -70,13 +63,13 @@ mod test { use test_case::test_matrix; use super::*; - use crate::CursorPosition; + use crate::{BuiltinTokenizer, CursorPosition, types::text_with_cursors::TextWithCursors}; #[test] fn test_cursor_complex() { - let original = "this is some complex text to test cursor positions"; + let original: &'static str = "this is some complex text to test cursor positions"; let left = TextWithCursors::new( - "this is really complex text for testing cursor positions", + "this is really complex text for testing cursor positions".to_owned(), vec![ CursorPosition { id: 0, @@ -89,7 +82,7 @@ mod test { ], ); let right = TextWithCursors::new( - "that was some complex sample to test cursor movements", + "that was some complex sample to test cursor movements".to_owned(), vec![ CursorPosition { id: 2, @@ -102,31 +95,31 @@ mod test { ], ); - let merged = reconcile_with_cursors(original, left, right); - + let merged = reconcile(original, &left, &right, &*BuiltinTokenizer::Word).apply(); assert_eq!( - merged, - TextWithCursors::new( - "that was really complex sample for testing cursor movements", - vec![ - CursorPosition { - id: 2, - char_index: 5 - }, // unchanged - CursorPosition { - id: 0, - char_index: 9 - }, // before "really" - CursorPosition { - id: 1, - char_index: 23 - }, // inside of "s|ample" because "text" got replaced by "sample" - CursorPosition { - id: 3, - char_index: 30 - }, // after "complex sample" - ] - ) + &merged.text(), + "that was really complex sample for testing cursor movements" + ); + assert_eq!( + merged.cursors(), + vec![ + CursorPosition { + id: 2, + char_index: 5 + }, // unchanged + CursorPosition { + id: 0, + char_index: 9 + }, // before "really" + CursorPosition { + id: 1, + char_index: 23 + }, // inside of "s|ample" because "text" got replaced by "sample" + CursorPosition { + id: 3, + char_index: 30 + }, // after "complex sample" + ] ); } @@ -174,6 +167,11 @@ mod test { }) .collect::>(); - let _ = reconcile(&contents[0], &contents[1], &contents[2]); + let _ = reconcile( + &contents[0], + &(&contents[1]).into(), + &(&contents[2]).into(), + &*BuiltinTokenizer::Word, + ); } } diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 08dcee3..60f32da 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -4,13 +4,13 @@ use std::fmt::Debug; use serde::{Deserialize, Serialize}; use crate::{ - CursorPosition, TextWithCursors, + BuiltinTokenizer, CursorPosition, TextWithCursors, operation_transformation::{ Operation, utils::{cook_operations::cook_operations, elongate_operations::elongate_operations}, }, raw_operation::RawOperation, - tokenizer::{Tokenizer, word_tokenizer::word_tokenizer}, + tokenizer::Tokenizer, types::{history::History, side::Side, text_with_history::TextWithHistory}, utils::string_builder::StringBuilder, }; @@ -27,6 +27,7 @@ use crate::{ /// 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. + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Default)] pub struct EditedText<'a, T> @@ -35,7 +36,7 @@ where { text: &'a str, operations: Vec>, - pub(crate) cursors: Vec, + cursors: Vec, } impl<'a> EditedText<'a, String> { @@ -47,7 +48,7 @@ impl<'a> EditedText<'a, String> { /// whitespaces. #[must_use] pub fn from_strings(original: &'a str, updated: &TextWithCursors, side: Side) -> Self { - Self::from_strings_with_tokenizer(original, updated, &word_tokenizer, side) + Self::from_strings_with_tokenizer(original, updated, &*BuiltinTokenizer::Word, side) } } @@ -219,14 +220,14 @@ where /// Apply the operations to the text and return the resulting text. #[must_use] - pub fn apply(&self) -> String { + pub fn apply(&self) -> TextWithCursors { let mut builder: StringBuilder<'_> = StringBuilder::new(self.text); for operation in &self.operations { builder = operation.apply(builder); } - builder.take() + TextWithCursors::new(builder.take(), self.cursors.clone()) } #[must_use] @@ -291,7 +292,7 @@ mod tests { insta::assert_debug_snapshot!(operations); let new_right = operations.apply(); - assert_eq!(new_right.to_string(), right); + assert_eq!(new_right.text(), right); } #[test] @@ -303,7 +304,7 @@ mod tests { assert_debug_snapshot!(operations); let new_right = operations.apply(); - assert_eq!(new_right.to_string(), text); + assert_eq!(new_right.text(), text); } #[test] @@ -317,6 +318,6 @@ mod tests { let operations_2 = EditedText::from_strings(original, &right.into(), Side::Right); let operations = operations_1.merge(operations_2); - assert_eq!(operations.apply(), expected); + assert_eq!(operations.apply().text(), expected); } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 608fe93..87de8b5 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1,7 +1,44 @@ +mod word_tokenizer; + +use std::ops::Deref; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; use token::Token; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; pub mod token; -pub mod word_tokenizer; /// A trait for tokenizers that take a string and return a list of tokens. pub type Tokenizer = dyn Fn(&str) -> Vec>; + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(feature = "wasm")] +pub enum BuiltinTokenizer { + Character = "Character", + Word = "Word", +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(feature = "wasm"))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum BuiltinTokenizer { + Character, + Word, +} + +impl Deref for BuiltinTokenizer { + type Target = Tokenizer; + + fn deref(&self) -> &Self::Target { + match self { + BuiltinTokenizer::Character => todo!(), + BuiltinTokenizer::Word => &word_tokenizer::word_tokenizer, + #[cfg(feature = "wasm")] + BuiltinTokenizer::__Invalid => panic!("Unexpected tokenizer type"), + } + } +} diff --git a/src/types/text_with_cursors.rs b/src/types/text_with_cursors.rs index 1007f7a..ac7c18f 100644 --- a/src/types/text_with_cursors.rs +++ b/src/types/text_with_cursors.rs @@ -32,9 +32,6 @@ impl TextWithCursors { #[must_use] pub fn cursors(&self) -> Vec { self.cursors.clone() } - - #[must_use] - pub fn new_owned(text: String, cursors: Vec) -> Self { Self { text, cursors } } } impl<'a> From<&'a str> for TextWithCursors { @@ -45,3 +42,21 @@ impl<'a> From<&'a str> for TextWithCursors { } } } + +impl From<&String> for TextWithCursors { + fn from(text: &String) -> Self { + Self { + text: text.to_owned(), + cursors: Vec::new(), + } + } +} + +impl From for TextWithCursors { + fn from(text: String) -> Self { + Self { + text, + cursors: Vec::new(), + } + } +} diff --git a/src/wasm.rs b/src/wasm.rs index 73bbb86..3e65ab0 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -13,9 +13,40 @@ use core::str; use wasm_bindgen::prelude::*; -use crate::{ - TextWithCursors, TextWithHistory, reconcile, reconcile_with_cursors, reconcile_with_history, -}; +use crate::{BuiltinTokenizer, CursorPosition, TextWithCursors, TextWithHistory}; + +/// WASM wrapper around `crate::reconcile` for merging text. +#[wasm_bindgen(js_name = reconcile)] +#[must_use] +pub fn reconcile( + parent: &str, + left: &TextWithCursors, + right: &TextWithCursors, + tokenizer: BuiltinTokenizer, +) -> TextWithCursors { + set_panic_hook(); + + crate::reconcile(parent, left, right, &*tokenizer).apply() +} + +/// WASM wrapper around `crate::reconcile` for merging text. +#[wasm_bindgen(js_name = reconcileWithHistory)] +#[must_use] +pub fn reconcile_with_history( + parent: &str, + left: &TextWithCursors, + right: &TextWithCursors, + tokenizer: BuiltinTokenizer, +) -> TextWithCursorsAndHistory { + set_panic_hook(); + let reconciled = crate::reconcile(parent, left, right, &*tokenizer); + let text_with_cursors = reconciled.apply(); + + TextWithCursorsAndHistory { + text_with_cursors, + history: reconciled.apply_with_history(), + } +} /// Merge two documents with a common parent. Relies on `reconcile::reconcile` /// for texts and returns the right document as-is if either of the updated @@ -34,56 +65,35 @@ use crate::{ /// # Panics /// /// If any of the input documents are not valid UTF-8 strings. -#[wasm_bindgen] +#[wasm_bindgen(js_name = genericReconcile)] #[must_use] -pub fn merge(parent: &[u8], left: &[u8], right: &[u8]) -> Vec { +pub fn generic_reconcile( + parent: &[u8], + left: &[u8], + right: &[u8], + tokenizer: BuiltinTokenizer, +) -> Vec { set_panic_hook(); - if is_binary(parent) || is_binary(left) || is_binary(right) { + if crate::is_binary(parent) || crate::is_binary(left) || crate::is_binary(right) { right.to_vec() } else { - reconcile( + 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"), - str::from_utf8(right).expect("right 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() } } -/// WASM wrapper around `reconcile` for merging text. -#[wasm_bindgen(js_name = mergeText)] -#[must_use] -pub fn merge_text(parent: &str, left: &str, right: &str) -> String { - set_panic_hook(); - - reconcile(parent, left, right) -} - -/// WASM wrapper around `reconcile` for merging text. -#[wasm_bindgen(js_name = mergeTextWithHistory)] -#[must_use] -pub fn merge_text_with_history(parent: &str, left: &str, right: &str) -> Vec { - set_panic_hook(); - - reconcile_with_history(parent, left, right) - .into_iter() - .collect() -} - -/// WASM wrapper around `reconcile::reconcile_with_cursors` for merging text. -#[wasm_bindgen(js_name = mergeTextWithCursors)] -#[must_use] -pub fn merge_text_with_cursors( - parent: &str, - left: &TextWithCursors, - right: &TextWithCursors, -) -> TextWithCursors { - set_panic_hook(); - - reconcile_with_cursors(parent, left, right) -} - /// Heuristically determine if the given data is a binary or a text file's /// content. #[wasm_bindgen(js_name = isBinary)] @@ -98,3 +108,22 @@ fn set_panic_hook() { #[cfg(feature = "console_error_panic_hook")] console_error_panic_hook::set_once(); } + +#[wasm_bindgen] +#[derive(Debug, Clone, PartialEq, Default)] +pub struct TextWithCursorsAndHistory { + text_with_cursors: TextWithCursors, + history: Vec, +} + +#[wasm_bindgen] +impl TextWithCursorsAndHistory { + #[must_use] + pub fn text(&self) -> String { self.text_with_cursors.text() } + + #[must_use] + pub fn cursors(&self) -> Vec { self.text_with_cursors.cursors() } + + #[must_use] + pub fn history(&self) -> Vec { self.history.clone() } +} diff --git a/tests/example_document.rs b/tests/example_document.rs index 08e39a2..ec9d79a 100644 --- a/tests/example_document.rs +++ b/tests/example_document.rs @@ -1,5 +1,5 @@ use pretty_assertions::assert_eq; -use reconcile::{CursorPosition, TextWithCursors}; +use reconcile::{CursorPosition, EditedText, TextWithCursors}; use serde::Deserialize; /// `ExampleDocument` represents a test case for the reconciliation process. @@ -37,7 +37,7 @@ impl ExampleDocument { /// /// If the result string does not match the expected string, the program /// will panic. - pub fn assert_eq(&self, result: &TextWithCursors) { + pub fn assert_eq(&self, result: &EditedText<'_, String>) { let result_str = ExampleDocument::text_with_cursors_to_string(result); assert_eq!( self.expected, result_str, @@ -60,14 +60,16 @@ impl ExampleDocument { ); } - fn text_with_cursors_to_string(document: &TextWithCursors) -> String { - let mut result = document.text().clone(); - for (i, cursor) in document.cursors().iter().enumerate() { + fn text_with_cursors_to_string(document: &EditedText<'_, String>) -> String { + let merged = document.apply(); + let mut result = merged.text(); + for (i, cursor) in merged.cursors().iter().enumerate() { assert!( cursor.char_index <= result.len(), // equals in case of insert at the end - "Cursor index out of bounds: {} > {} when testing for '{result}'", + "Cursor index out of bounds: {} > {} when testing for '{}.'", cursor.char_index, - result.len() + result.len(), + result ); result.insert( @@ -85,7 +87,7 @@ impl ExampleDocument { fn string_to_text_with_cursors(text: &str) -> TextWithCursors { let cursors = Self::parse_cursors(text); let text = text.replace('|', ""); - TextWithCursors::new_owned(text, cursors) + TextWithCursors::new(text, cursors) } fn parse_cursors(text: &str) -> Vec { diff --git a/tests/test.rs b/tests/test.rs index 30d39bb..7c6fe88 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3,27 +3,33 @@ mod example_document; use std::{fs, path::Path}; use example_document::ExampleDocument; -use reconcile::{reconcile, reconcile_with_cursors}; +use reconcile::{BuiltinTokenizer, reconcile}; use serde::Deserialize; #[test] fn test_document_one_way_without_cursors() { for doc in &get_all_documents() { - doc.assert_eq_without_cursors(&reconcile( - &doc.parent(), - &doc.left().text(), - &doc.right().text(), - )); + doc.assert_eq_without_cursors( + &reconcile( + &doc.parent(), + &doc.left().text().into(), + &doc.right().text().into(), + &*BuiltinTokenizer::Word, + ) + .apply() + .text(), + ); } } #[test] fn test_document_one_way_with_cursors() { for doc in &get_all_documents() { - doc.assert_eq(&reconcile_with_cursors( + doc.assert_eq(&reconcile( &doc.parent(), &doc.left(), &doc.right(), + &*BuiltinTokenizer::Word, )); } } @@ -31,21 +37,27 @@ fn test_document_one_way_with_cursors() { #[test] fn test_document_inverse_way_without_cursors() { for doc in &get_all_documents() { - doc.assert_eq_without_cursors(&reconcile( - &doc.parent(), - &doc.right().text(), - &doc.left().text(), - )); + doc.assert_eq_without_cursors( + &reconcile( + &doc.parent(), + &doc.right().text().into(), + &doc.left().text().into(), + &*BuiltinTokenizer::Word, + ) + .apply() + .text(), + ); } } #[test] fn test_document_inverse_way_with_cursors() { for doc in &get_all_documents() { - doc.assert_eq(&reconcile_with_cursors( + doc.assert_eq(&reconcile( &doc.parent(), &doc.right(), &doc.left(), + &*BuiltinTokenizer::Word, )); } } diff --git a/tests/wasm.rs b/tests/wasm.rs index d081b28..ee584f5 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -1,18 +1,18 @@ #![cfg(feature = "wasm")] -use reconcile::{CursorPosition, TextWithCursors, wasm::*}; +use reconcile::{BuiltinTokenizer, CursorPosition, TextWithCursors, wasm::*}; use wasm_bindgen_test::*; #[wasm_bindgen_test(unsupported = test)] fn test_merge() { let left = b"hello "; let right = b"world"; - let result = merge(b"", left, right); + let result = generic_reconcile(b"", left, right, BuiltinTokenizer::Word); assert_eq!(result, b"hello world"); let left = b"\0binary"; let right = b"other"; - let result = merge(b"", left, right); + let result = generic_reconcile(b"", left, right, BuiltinTokenizer::Word); assert_eq!(result, right); } @@ -20,19 +20,20 @@ fn test_merge() { fn test_merge_text() { let left = "hello "; let right = "world"; - let result = merge_text("", left, right); + let result = reconcile("", &left.into(), &right.into(), BuiltinTokenizer::Word).text(); assert_eq!(result, "hello world"); } #[wasm_bindgen_test(unsupported = test)] fn test_merge_text_with_cursors() { - let result = merge_text_with_cursors( + let result = reconcile( "hi", &TextWithCursors::new("hi world".to_owned(), vec![]), &TextWithCursors::new( "hi".to_owned(), vec![CursorPosition::new(0, 1), CursorPosition::new(1, 2)], ), + BuiltinTokenizer::Word, ); assert_eq!( @@ -48,7 +49,10 @@ fn test_merge_text_with_cursors() { fn merge_binary() { let left = [0, 1, 2]; let right = [3, 4, 5]; - assert_eq!(merge(b"", &left, &right), right); + assert_eq!( + generic_reconcile(b"", &left, &right, BuiltinTokenizer::Word), + right + ); } #[wasm_bindgen_test(unsupported = test)] From 9cb73680f8188149191003724486c94370a8730a Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 29 Jun 2025 19:35:21 +0100 Subject: [PATCH 13/37] Add character tokenizer --- src/tokenizer.rs | 3 +- src/tokenizer/character_tokenizer.rs | 26 ++++ ...er_tokenizer__tests__with_snapshots-2.snap | 144 ++++++++++++++++++ ...cter_tokenizer__tests__with_snapshots.snap | 5 + 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/tokenizer/character_tokenizer.rs create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots-2.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots.snap diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 87de8b5..b2b9065 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1,4 +1,5 @@ mod word_tokenizer; +mod character_tokenizer; use std::ops::Deref; @@ -35,7 +36,7 @@ impl Deref for BuiltinTokenizer { fn deref(&self) -> &Self::Target { match self { - BuiltinTokenizer::Character => todo!(), + BuiltinTokenizer::Character =>&character_tokenizer::character_tokenizer, BuiltinTokenizer::Word => &word_tokenizer::word_tokenizer, #[cfg(feature = "wasm")] BuiltinTokenizer::__Invalid => panic!("Unexpected tokenizer type"), diff --git a/src/tokenizer/character_tokenizer.rs b/src/tokenizer/character_tokenizer.rs new file mode 100644 index 0000000..ed6170c --- /dev/null +++ b/src/tokenizer/character_tokenizer.rs @@ -0,0 +1,26 @@ +use super::token::Token; + +/// Splits text into UTF-8 characters. +/// +/// ```not_rust +/// "Hey!" -> ["H", "e", "y", "!"] +/// ``` +pub fn character_tokenizer(text: &str) -> Vec> { + text.chars() + .map(|char| Token::new(char.to_string(), char.to_string(), true, true)) + .collect() +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn test_with_snapshots() { + assert_debug_snapshot!(character_tokenizer("")); + + assert_debug_snapshot!(character_tokenizer(" hello, \nwhere are you?")); + } +} diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots-2.snap b/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots-2.snap new file mode 100644 index 0000000..b61d12a --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots-2.snap @@ -0,0 +1,144 @@ +--- +source: src/tokenizer/character_tokenizer.rs +expression: "character_tokenizer(\" hello, \\nwhere are you?\")" +--- +[ + Token { + normalised: " ", + original: " ", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "h", + original: "h", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "e", + original: "e", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "l", + original: "l", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "l", + original: "l", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "o", + original: "o", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: ",", + original: ",", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: " ", + original: " ", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "w", + original: "w", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "h", + original: "h", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "e", + original: "e", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "r", + original: "r", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "e", + original: "e", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: " ", + original: " ", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "a", + original: "a", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "r", + original: "r", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "e", + original: "e", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: " ", + original: " ", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "y", + original: "y", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "o", + original: "o", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "u", + original: "u", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "?", + original: "?", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots.snap b/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots.snap new file mode 100644 index 0000000..9aa9b44 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__character_tokenizer__tests__with_snapshots.snap @@ -0,0 +1,5 @@ +--- +source: src/tokenizer/character_tokenizer.rs +expression: "character_tokenizer(\"\")" +--- +[] From da59156e079f7aed9c6b6d1452100d777a6ab451 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Thu, 3 Jul 2025 16:41:33 +0100 Subject: [PATCH 14/37] Add wee_alloc --- Cargo.lock | 230 +++++++++++++++++++++++++++------------------------- Cargo.toml | 6 +- src/wasm.rs | 7 ++ 3 files changed, 131 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19871fb..9ca0f7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,35 +4,41 @@ version = 4 [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "cc" -version = "1.2.2" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "console" -version = "0.15.8" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys", ] [[package]] @@ -41,7 +47,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "wasm-bindgen", ] @@ -53,27 +59,27 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", @@ -81,50 +87,36 @@ dependencies = [ [[package]] name = "insta" -version = "1.42.2" +version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50259abbaa67d11d2bcafc7ba1d094ed7a0c70e3ce893f0d0997f73558cb3084" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" dependencies = [ "console", - "linked-hash-map", "once_cell", - "pin-project", "similar", ] [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.171" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "log" @@ -132,6 +124,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "minicov" version = "0.3.7" @@ -144,29 +142,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pretty_assertions" @@ -180,18 +158,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -200,6 +178,7 @@ dependencies = [ name = "reconcile" version = "0.4.0" dependencies = [ + "cfg-if 1.0.1", "console_error_panic_hook", "insta", "pretty_assertions", @@ -208,13 +187,20 @@ dependencies = [ "test-case", "wasm-bindgen", "wasm-bindgen-test", + "wee_alloc", ] [[package]] -name = "ryu" -version = "1.0.18" +name = "rustversion" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -225,12 +211,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "serde" version = "1.0.219" @@ -272,15 +252,15 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -302,7 +282,7 @@ version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "proc-macro2", "quote", "syn", @@ -322,9 +302,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unsafe-libyaml" @@ -344,20 +324,21 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", @@ -369,11 +350,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ - "cfg-if", + "cfg-if 1.0.1", "js-sys", "once_cell", "wasm-bindgen", @@ -382,9 +363,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -392,9 +373,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -405,19 +386,21 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-bindgen-test" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d44563646eb934577f2772656c7ad5e9c90fac78aa8013d776fcdaf24625d" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" dependencies = [ "js-sys", "minicov", - "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", @@ -425,9 +408,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54171416ce73aa0b9c377b51cc3cb542becee1cd678204812e8392e5b0e4a031" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", @@ -436,31 +419,56 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" diff --git a/Cargo.toml b/Cargo.toml index 4adaf56..9adae81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,12 @@ wasm-bindgen = { version = "0.2.99", optional = true } # code size when deploying. 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 = [] +default = [ "wasm" ] serde = [ "dep:serde" ] wasm = [ "dep:wasm-bindgen"] console_error_panic_hook = [ "dep:console_error_panic_hook" ] diff --git a/src/wasm.rs b/src/wasm.rs index 3e65ab0..c2831a2 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -11,9 +11,16 @@ use core::str; +use cfg_if::cfg_if; use wasm_bindgen::prelude::*; use crate::{BuiltinTokenizer, CursorPosition, TextWithCursors, TextWithHistory}; +cfg_if! { + if #[cfg(feature = "wee_alloc")] { + #[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 373e7d03f4bf5837bf12938fe144af8a944fcfa7 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 01:39:13 +0100 Subject: [PATCH 15/37] Update readme --- examples/website/README.md | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 examples/website/README.md diff --git a/examples/website/README.md b/examples/website/README.md new file mode 100644 index 0000000..317223f --- /dev/null +++ b/examples/website/README.md @@ -0,0 +1,59 @@ +# Reconcile: conflict-free 3-way text merging + +[![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) +[![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) + +> `diff3` but with automatic conflict resolution. + +Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. + +```rust +use reconcile::{reconcile, BuiltinTokenizer}; + +let parent = "Merging text is hard!"; +let left = "Merging text is easy!"; +let right = "With reconcile, merging documents is hard!"; + +let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); +``` + +## Features + +- Conflict-free output (no more git conflict markers) +- Support for updating cursor/selection positions +- Pluggable tokenizer +- Full UTF-8 support +- WASM + +## Motivation + +Sometimes documents get edited concurrently by multiple users (or the same user from multiple devices) resulting in divergent changes. + +To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some application, this is trivial if the document can only be edited through an editor that's in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder Markdown files leaving the user free to change their technology of choice on a whim. + +This means that files can be edited out-of-channel and the only information a text synchronisation system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. + +> There are domains of human text which are less tolerant of mis-merges: for instance, a two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus, unknowingly changing the meaning. + +## Architecture + +## Development + +### Install [nvm](https://github.com/nvm-sh/nvm) + +- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` +- `nvm install 22` +- `nvm use 22` +- Optionally set the system-wide default: `nvm alias default 22` + +### Set up Rust + +- Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` +- `cargo install wasm-pack cargo-insta cargo-edit` + +#### Publish new version + +```sh +scripts/bump-version.sh patch +``` From f0ff720577eb9cadb4674f7f3a4978d73dbd6f74 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 02:11:16 +0100 Subject: [PATCH 16/37] Add TS wrapper package --- reconcile-js/jest.config.js | 3 + reconcile-js/package-lock.json | 4905 ++++++++++++++++++++++++++++++++ reconcile-js/package.json | 24 + reconcile-js/src/index.ts | 183 ++ reconcile-js/tsconfig.json | 14 + reconcile-js/webpack.config.js | 33 + 6 files changed, 5162 insertions(+) create mode 100644 reconcile-js/jest.config.js create mode 100644 reconcile-js/package-lock.json create mode 100644 reconcile-js/package.json create mode 100644 reconcile-js/src/index.ts create mode 100644 reconcile-js/tsconfig.json create mode 100644 reconcile-js/webpack.config.js diff --git a/reconcile-js/jest.config.js b/reconcile-js/jest.config.js new file mode 100644 index 0000000..8c1027e --- /dev/null +++ b/reconcile-js/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: "ts-jest/presets/js-with-babel-esm" +}; diff --git a/reconcile-js/package-lock.json b/reconcile-js/package-lock.json new file mode 100644 index 0000000..7b0ef16 --- /dev/null +++ b/reconcile-js/package-lock.json @@ -0,0 +1,4905 @@ +{ + "name": "reconcile", + "version": "0.4.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reconcile", + "version": "0.4.0", + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "reconcile": "file:../pkg", + "ts-jest": "^29.3.4", + "ts-loader": "^9.5.2", + "tslib": "2.8.1", + "typescript": "5.8.3", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } + }, + "../pkg": { + "name": "reconcile", + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "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": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "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": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "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": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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/@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": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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": { + "node": ">=6.0.0" + } + }, + "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": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "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": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.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": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "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": { + "@babel/types": "^7.0.0" + } + }, + "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": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "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": { + "@babel/types": "^7.20.7" + } + }, + "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": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "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": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "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/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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": { + "@types/istanbul-lib-coverage": "*" + } + }, + "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": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "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.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "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": { + "@types/yargs-parser": "*" + } + }, + "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/@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": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "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": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "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": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "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": { + "@xtuc/long": "4.2.2" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "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": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "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": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "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": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "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": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "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": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "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": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=8" + } + }, + "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": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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": { + "sprintf-js": "~1.0.2" + } + }, + "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": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 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": { + "node-int64": "^0.4.0" + } + }, + "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": { + "node": ">=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": { + "node": ">=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": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "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": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=10" + } + }, + "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": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "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": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=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": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "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": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/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": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "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": { + "is-arrayish": "^0.2.1" + } + }, + "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": { + "node": ">=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": { + "node": ">=8" + } + }, + "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": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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": { + "node": ">=4.0" + } + }, + "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": { + "node": ">=4.0" + } + }, + "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": { + "node": ">=0.8.x" + } + }, + "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": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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": { + "node": ">= 4.9.1" + } + }, + "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": { + "bser": "2.1.1" + } + }, + "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": { + "minimatch": "^5.0.1" + } + }, + "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": { + "balanced-match": "^1.0.0" + } + }, + "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": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "flat": "cli.js" + } + }, + "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": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "node": ">=6.9.0" + } + }, + "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": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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": { + "node": ">=8.0.0" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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": { + "node": ">=8" + } + }, + "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": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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": { + "node": ">=10.17.0" + } + }, + "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": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=0.8.19" + } + }, + "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": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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": { + "node": ">=10.13.0" + } + }, + "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": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "node": ">=8" + } + }, + "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": { + "node": ">=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": { + "node": ">=0.12.0" + } + }, + "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": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=8" + } + }, + "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": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=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": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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": { + "node": ">=6" + } + }, + "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": { + "node": ">=6.11.5" + } + }, + "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": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "yallist": "^3.0.2" + } + }, + "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": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "tmpl": "1.0.5" + } + }, + "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": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.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": { + "node": ">= 0.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": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.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": { + "node": ">=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": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/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": { + "node": ">=0.10.0" + } + }, + "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": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "wrappy": "1" + } + }, + "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": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=6" + } + }, + "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": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "node": ">=8" + } + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=8" + } + }, + "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/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": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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": { + "node": ">= 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": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "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": { + "safe-buffer": "^5.1.0" + } + }, + "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": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/reconcile": { + "resolved": "../pkg", + "link": true + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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": [ + { + "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", + "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": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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": { + "semver": "bin/semver.js" + } + }, + "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": { + "randombytes": "^2.1.0" + } + }, + "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": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=8" + } + }, + "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/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "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": { + "node": ">=8" + } + }, + "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": { + "node": ">=0.10.0" + } + }, + "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": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "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": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "node": ">=8" + } + }, + "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": { + "node": ">=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": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": { + "node": ">=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": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "@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": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "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": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "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": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "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": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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": { + "node": ">= 8" + } + }, + "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": { + "node": ">=4" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "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": { + "makeerror": "1.0.12" + } + }, + "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": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@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", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "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", + "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" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "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": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "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": { + "node": ">=18" + } + }, + "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": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "node": ">=10.13.0" + } + }, + "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": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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": "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": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "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": { + "node": ">=10" + } + }, + "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": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "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": { + "node": ">=12" + } + }, + "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": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/reconcile-js/package.json b/reconcile-js/package.json new file mode 100644 index 0000000..e034c27 --- /dev/null +++ b/reconcile-js/package.json @@ -0,0 +1,24 @@ +{ + "name": "reconcile", + "version": "0.4.0", + "main": "dist/index.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist/*" + ], + "scripts": { + "build": "webpack --mode production", + "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "reconcile": "file:../pkg", + "ts-jest": "^29.3.4", + "ts-loader": "^9.5.2", + "tslib": "2.8.1", + "typescript": "5.8.3", + "webpack": "^5.99.9", + "webpack-cli": "^6.0.1" + } +} \ No newline at end of file diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts new file mode 100644 index 0000000..8419be2 --- /dev/null +++ b/reconcile-js/src/index.ts @@ -0,0 +1,183 @@ +import wasmInit, { + CursorPosition as wasmCursorPosition, + reconcile as wasmReconcile, + TextWithCursors as wasmTextWithCursors, + TextWithHistory as wasmTextWithHistory, + BuiltinTokenizer, + reconcileWithHistory as wasmReconcileWithHistory, + History, + InitInput, +} from "reconcile"; + +export interface TextWithCursors { + /** The document's entire content */ + text: string; + /** List of cursor positions, can be null or undefined if there are no cursors */ + cursors: null | undefined | CursorPosition[]; +} + +/** + * Represents a cursor position with a unique identifier. + */ +export interface CursorPosition { + /** Unique identifier for the cursor */ + id: number; + /** Character position in the text, 0-based */ + position: number; +} + +export interface TextWithCursorsAndHistory { + /** The document's entire content */ + text: string; + /** List of cursor positions, can be null or undefined if there are no cursors */ + cursors: null | undefined | CursorPosition[]; + /** List of operations leading to `text` from the 3 ancestors */ + history: TextWithHistory[]; +} + +export interface TextWithHistory { + /** Span of text associated with the historical opearion */ + text: string; + /** Origin of the `text` span */ + history: History; +} + +/** + * Supported tokenizer types for text processing. + */ +export type Tokenizer = "word" | "character"; + +let isInitialised = false; + +/** + * Initializes the WASM module for text reconciliation. + * Must be called before using any other functions. + * + * The function is idempotent. + * + * @param content - Optional initialization input for the WASM module during testing. + * @returns Promise that resolves when initialization is complete + */ +export async function init(content?: InitInput) { + if (isInitialised) { + return; + } + + await wasmInit(content); + + isInitialised = true; +} + +/** + * Merges three versions of text (original, left, right) and cursor positions. + * + * @param original - The original/base version of the text + * @param left - The left version of the text, either as string or TextWithCursors + * @param right - The right version of the text, either as string or TextWithCursors + * @param tokenizer - The tokenization strategy to use (default: "Word") + * @returns The reconciled text with merged cursor positions + */ +export function reconcile( + original: string, + left: string | TextWithCursors, + right: string | TextWithCursors, + tokenizer: BuiltinTokenizer = "Word" +): TextWithCursors { + const leftCursor = toWasmTextWithCursors(left); + const rightCursor = toWasmTextWithCursors(right); + + const result = wasmReconcile(original, leftCursor, rightCursor, tokenizer); + + leftCursor.free(); + rightCursor.free(); + + const jsResult = toTextWithCursors(result); + result.free(); + + return jsResult; +} + +/** + * Merges three versions of text and returns the result with historical information. + * + * Calculating the `history` is somewhat more expensive, otherwise this function behaves like `reconcile`. + * + * @param original - The original/base version of the text + * @param left - The left version of the text, either as string or TextWithCursors + * @param right - The right version of the text, either as string or TextWithCursors + * @param tokenizer - The tokenization strategy to use (default: "Word") + * @returns The reconciled text with cursor positions and history of changes + */ +export function reconcileWithHistory( + original: string, + left: string | TextWithCursors, + right: string | TextWithCursors, + tokenizer: BuiltinTokenizer = "Word" +): TextWithCursorsAndHistory { + const leftCursor = toWasmTextWithCursors(left); + const rightCursor = toWasmTextWithCursors(right); + + const result = wasmReconcileWithHistory( + original, + leftCursor, + rightCursor, + tokenizer + ); + + leftCursor.free(); + rightCursor.free(); + + const jsResult = toTextWithCursors(result); + const history = result.history().map(toTextWithHistory); + result.free(); + + return { + ...jsResult, + history, + }; +} + +function toWasmTextWithCursors( + text: string | TextWithCursors +): wasmTextWithCursors { + const isInputString = typeof text == "string"; + const leftText = isInputString ? text : text.text; + const leftCursors = isInputString ? [] : text.cursors ?? []; + + return new wasmTextWithCursors( + leftText, + leftCursors.map(toWasmCursorPosition) + ); +} + +function toWasmCursorPosition({ + id, + position, +}: CursorPosition): wasmCursorPosition { + return new wasmCursorPosition(id, position); +} + +function toTextWithCursors( + textWithCursor: wasmTextWithCursors +): TextWithCursors { + return { + text: textWithCursor.text(), + cursors: textWithCursor.cursors().map(toCursorPosition), + }; +} + +function toCursorPosition(cursor: wasmCursorPosition): CursorPosition { + return { + id: cursor.id(), + position: cursor.characterPosition(), + }; +} + +function toTextWithHistory( + textWithHistory: wasmTextWithHistory +): TextWithHistory { + return { + text: textWithHistory.text(), + history: textWithHistory.history(), + }; +} diff --git a/reconcile-js/tsconfig.json b/reconcile-js/tsconfig.json new file mode 100644 index 0000000..c307f61 --- /dev/null +++ b/reconcile-js/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "strict": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "bundler", + "declaration": true, + "declarationDir": "./dist/types" + }, + "exclude": [ + "./dist" + ] +} \ No newline at end of file diff --git a/reconcile-js/webpack.config.js b/reconcile-js/webpack.config.js new file mode 100644 index 0000000..9d54c6b --- /dev/null +++ b/reconcile-js/webpack.config.js @@ -0,0 +1,33 @@ +const path = require("path"); +const webpack = require("webpack"); +const packageJson = require("./package.json"); + + +module.exports = { + entry: "./src/index.ts", + module: { + rules: [ + { + test: /\.ts$/, + use: ["ts-loader"] + }, + { + test: /\.wasm$/, + type: "asset/inline" + } + ] + }, + resolve: { + extensions: [".ts"], + alias: { + root: __dirname, + src: path.resolve(__dirname, "src") + } + }, + output: { + path: path.resolve(__dirname, "dist"), + filename: "index.js", + libraryTarget: "commonjs2" + }, +}; + \ No newline at end of file From 8004ac3742f6b37945087c9007978bbeb5f2ff43 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 02:16:17 +0100 Subject: [PATCH 17/37] Fix wrapping --- src/operation_transformation/edited_text.rs | 14 +++++++------- src/types/text_with_cursors.rs | 4 +++- tests/examples/various.yml | 6 ++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 60f32da..c287903 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -144,21 +144,21 @@ where Operation::Insert { .. } | Operation::Equal { .. } ); - let original_length = operation.len() as i64; + let original_length = operation.len() as isize; let result = match side { Side::Left => { let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - let shift = merged_length as i64 - seen_left_length as i64 - + op.len() as i64 + let shift = merged_length as isize - seen_left_length as isize + + op.len() as isize - original_length; while let Some(cursor) = left_cursors.next_if(|cursor| { cursor.char_index <= seen_left_length + original_length as usize }) { merged_cursors.push( - cursor.with_index((cursor.char_index as i64 + shift) as usize), + cursor.with_index(cursor.char_index.saturating_add_signed(shift)), ); } } @@ -176,15 +176,15 @@ where let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - let shift = merged_length as i64 - seen_right_length as i64 - + op.len() as i64 + let shift = merged_length as isize - seen_right_length as isize + + op.len() as isize - original_length; while let Some(cursor) = right_cursors.next_if(|cursor| { cursor.char_index <= seen_right_length + original_length as usize }) { merged_cursors.push( - cursor.with_index((cursor.char_index as i64 + shift) as usize), + cursor.with_index(cursor.char_index.saturating_add_signed(shift)), ); } } diff --git a/src/types/text_with_cursors.rs b/src/types/text_with_cursors.rs index ac7c18f..c9dec89 100644 --- a/src/types/text_with_cursors.rs +++ b/src/types/text_with_cursors.rs @@ -20,7 +20,9 @@ impl TextWithCursors { debug_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 or just after the end" + "Cursor positions ({}) must be contained within the text (of length {length}) or \ + just after the end", + cursor.char_index ); } diff --git a/tests/examples/various.yml b/tests/examples/various.yml index 4576edc..4370178 100644 --- a/tests/examples/various.yml +++ b/tests/examples/various.yml @@ -9,6 +9,12 @@ left: Party C shall pay Party B right: Party A shall receive from Party B expected: Party C shall receive from Party B +--- +parent: hello +left: hel|lo +right: hi +expected: "|hi" + --- parent: left: hi my friend| From 1bc6117045685433ead56f7a7b7f9b58f54772c5 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 02:23:32 +0100 Subject: [PATCH 18/37] Add JS tests --- reconcile-js/src/index.test.ts | 70 ++++++++++++++++++++++++++++++++++ reconcile-js/src/index.ts | 11 ++++++ scripts/test.sh | 12 ++++-- 3 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 reconcile-js/src/index.test.ts diff --git a/reconcile-js/src/index.test.ts b/reconcile-js/src/index.test.ts new file mode 100644 index 0000000..9fac9d0 --- /dev/null +++ b/reconcile-js/src/index.test.ts @@ -0,0 +1,70 @@ +import { init, reconcile, reconcileWithHistory } from "./index"; +import * as fs from "fs"; + +describe("reconcile", () => { + it("tries calling functions without init", () => { + expect(() => reconcile("Hello", "Hello world", "Hi world")).toThrow( + /call init()/ + ); + + expect(() => + reconcileWithHistory("Hello", "Hello world", "Hi world") + ).toThrow(/call init()/); + }); + + it("call reconcile without cursors", async () => { + await initWasm(); + + expect(reconcile("Hello", "Hello world", "Hi world").text).toEqual( + "Hi world" + ); + }); + + it("call reconcile with cursors", async () => { + await initWasm(); + + const result = reconcile( + "Hello", + { + text: "Hello world", + cursors: [ + { + id: 3, + position: 2, + }, + ], + }, + { + text: "Hi world", + cursors: [ + { + id: 4, + position: 0, + }, + { id: 5, position: 3 }, + ], + } + ); + + expect(result.text).toEqual("Hi world"); + expect(result.cursors).toEqual([ + { id: 3, position: 0 }, + { id: 4, position: 0 }, + { id: 5, position: 3 }, + ]); + }); + + it("call reconcileWithHistory", async () => { + await initWasm(); + + const result = reconcileWithHistory("Hello", "Hello world", "Hi world"); + + expect(result.text).toEqual("Hi world"); + expect(result.history.length).toBeGreaterThan(0); + }); +}); + +async function initWasm() { + const wasmBin = fs.readFileSync("../pkg/reconcile_bg.wasm"); + await init({ module_or_path: wasmBin }); +} diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index 8419be2..e9c769b 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -49,6 +49,9 @@ export type Tokenizer = "word" | "character"; let isInitialised = false; +const UNINITIALISED_MODULE_ERROR = + "Reconcile module has not been initialized. Please call init() before using any other functions."; + /** * Initializes the WASM module for text reconciliation. * Must be called before using any other functions. @@ -83,6 +86,10 @@ export function reconcile( right: string | TextWithCursors, tokenizer: BuiltinTokenizer = "Word" ): TextWithCursors { + if (!isInitialised) { + throw new Error(UNINITIALISED_MODULE_ERROR); + } + const leftCursor = toWasmTextWithCursors(left); const rightCursor = toWasmTextWithCursors(right); @@ -114,6 +121,10 @@ export function reconcileWithHistory( right: string | TextWithCursors, tokenizer: BuiltinTokenizer = "Word" ): TextWithCursorsAndHistory { + if (!isInitialised) { + throw new Error(UNINITIALISED_MODULE_ERROR); + } + const leftCursor = toWasmTextWithCursors(left); const rightCursor = toWasmTextWithCursors(right); diff --git a/scripts/test.sh b/scripts/test.sh index 236717c..9c556df 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,10 +2,14 @@ set -e -wasm-pack build --target web --features wasm +wasm-pack build --target web --features wasm,wee_alloc cargo test --verbose cargo test --features serde -cargo test --features wasm -wasm-pack test --node --features wasm +cargo test --features wasm,wee_alloc +wasm-pack test --node --features wasm,wee_alloc -echo "Success!" \ No newline at end of file +cd reconcile-js +npm run test +cd - + +echo "Success!" From ae5940718ec0bc9749dc296f5dc75de133f5cd77 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:09:11 +0100 Subject: [PATCH 19/37] Use wrapper in example --- .vscode/settings.json | 1 + examples/website/.gitignore | 3 -- examples/website/script.js | 11 ++++--- reconcile-js/tsconfig.json | 7 ++++- reconcile-js/webpack.config.js | 52 ++++++++++++++++++---------------- scripts/dev-website.sh | 10 +++---- 6 files changed, 44 insertions(+), 40 deletions(-) delete mode 100644 examples/website/.gitignore diff --git a/.vscode/settings.json b/.vscode/settings.json index c8ff44a..0639673 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "**/snapshots": true, // cargo-insta outputs "pkg": true, // wasm-pack build directory "target": true, // rust build directory + "**/node_modules": true, // node.js dependencies } } \ No newline at end of file diff --git a/examples/website/.gitignore b/examples/website/.gitignore deleted file mode 100644 index 8c5f1db..0000000 --- a/examples/website/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -reconcile.js -reconcile_bg.wasm - diff --git a/examples/website/script.js b/examples/website/script.js index b81cf29..1f5fafe 100644 --- a/examples/website/script.js +++ b/examples/website/script.js @@ -1,4 +1,4 @@ -import init, { mergeTextWithHistory } from "./reconcile.js"; +import { init, reconcileWithHistory } from "./dist/index.js"; const originalTextArea = document.getElementById("original"); const leftTextArea = document.getElementById("left"); @@ -37,16 +37,15 @@ function updateMergedText() { const left = leftTextArea.value; const right = rightTextArea.value; - const results = mergeTextWithHistory(original, left, right); + const results = reconcileWithHistory(original, left, right); mergedTextArea.innerHTML = ""; - for (const result of results) { + for (const {text, history} of results.history) { const span = document.createElement("span"); - span.className = result.history(); - span.textContent = result.text(); + span.className = history; + span.textContent = text; mergedTextArea.appendChild(span); - result.free(); } } diff --git a/reconcile-js/tsconfig.json b/reconcile-js/tsconfig.json index c307f61..238bbbb 100644 --- a/reconcile-js/tsconfig.json +++ b/reconcile-js/tsconfig.json @@ -4,9 +4,14 @@ "target": "ESNext", "strict": true, "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "moduleResolution": "bundler", "declaration": true, - "declarationDir": "./dist/types" + "declarationDir": "./dist/types", + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true, + "inlineSourceMap": true }, "exclude": [ "./dist" diff --git a/reconcile-js/webpack.config.js b/reconcile-js/webpack.config.js index 9d54c6b..64f1ee5 100644 --- a/reconcile-js/webpack.config.js +++ b/reconcile-js/webpack.config.js @@ -1,33 +1,37 @@ const path = require("path"); -const webpack = require("webpack"); -const packageJson = require("./package.json"); module.exports = { - entry: "./src/index.ts", - module: { - rules: [ - { - test: /\.ts$/, - use: ["ts-loader"] - }, - { - test: /\.wasm$/, - type: "asset/inline" - } - ] - }, - resolve: { - extensions: [".ts"], - alias: { - root: __dirname, - src: path.resolve(__dirname, "src") - } - }, + entry: "./src/index.ts", + module: { + rules: [ + { + test: /\.ts$/, + use: ["ts-loader"] + }, + { + test: /\.wasm$/, + type: "asset/inline" + } + ] + }, + optimization: { + // the consuming project should take care of minification + minimize: false + }, + resolve: { + extensions: [".ts"], + alias: { + root: __dirname, + src: path.resolve(__dirname, "src") + } + }, output: { path: path.resolve(__dirname, "dist"), filename: "index.js", - libraryTarget: "commonjs2" + libraryTarget: "module" + }, + experiments: { + outputModule: true }, }; - \ No newline at end of file diff --git a/scripts/dev-website.sh b/scripts/dev-website.sh index e49bdb4..5c54cb8 100755 --- a/scripts/dev-website.sh +++ b/scripts/dev-website.sh @@ -2,13 +2,11 @@ set -e -rm -rf pkg - wasm-pack build --target web --features wasm +cd reconcile-js +npm run build +cp -R dist ../examples/website -cp -R pkg/reconcile.js examples/website/ -cp -R pkg/reconcile_bg.wasm examples/website/ - -cd examples/website/ +cd ../examples/website python3 -m http.server $1 --bind 0.0.0.0 From 8bd803c9b2ab771d5281fde67b43d38192615575 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:14:18 +0100 Subject: [PATCH 20/37] Rename TextWithHistory to SpanWithHistory --- reconcile-js/src/index.ts | 14 ++-- src/lib.rs | 84 +++++++++++++++++-- src/operation_transformation/edited_text.rs | 14 ++-- src/tokenizer.rs | 4 +- src/types.rs | 2 +- ...t_with_history.rs => span_with_history.rs} | 6 +- src/wasm.rs | 6 +- 7 files changed, 100 insertions(+), 30 deletions(-) rename src/types/{text_with_history.rs => span_with_history.rs} (84%) diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index e9c769b..a2d66cb 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -2,7 +2,7 @@ import wasmInit, { CursorPosition as wasmCursorPosition, reconcile as wasmReconcile, TextWithCursors as wasmTextWithCursors, - TextWithHistory as wasmTextWithHistory, + SpanWithHistory as wasmSpanWithHistory, BuiltinTokenizer, reconcileWithHistory as wasmReconcileWithHistory, History, @@ -32,10 +32,10 @@ export interface TextWithCursorsAndHistory { /** List of cursor positions, can be null or undefined if there are no cursors */ cursors: null | undefined | CursorPosition[]; /** List of operations leading to `text` from the 3 ancestors */ - history: TextWithHistory[]; + history: SpanWithHistory[]; } -export interface TextWithHistory { +export interface SpanWithHistory { /** Span of text associated with the historical opearion */ text: string; /** Origin of the `text` span */ @@ -139,7 +139,7 @@ export function reconcileWithHistory( rightCursor.free(); const jsResult = toTextWithCursors(result); - const history = result.history().map(toTextWithHistory); + const history = result.history().map(toSpanWithHistory); result.free(); return { @@ -184,9 +184,9 @@ function toCursorPosition(cursor: wasmCursorPosition): CursorPosition { }; } -function toTextWithHistory( - textWithHistory: wasmTextWithHistory -): TextWithHistory { +function toSpanWithHistory( + textWithHistory: wasmSpanWithHistory +): SpanWithHistory { return { text: textWithHistory.text(), history: textWithHistory.history(), diff --git a/src/lib.rs b/src/lib.rs index 4ed5087..bf49649 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,86 @@ -#![feature(stmt_expr_attributes)] +//! # Reconcile +//! +//! A library for automatically merging two conflicting versions of a +//! document. `Reconcile` is essentially `git merge` but without any conflict +//! markers (or lost edits) in the output. +//! +//! ``` +//! use reconcile::{reconcile, BuiltinTokenizer}; +//! +//! let parent = "Merging text is hard!"; +//! let left = "Merging text is easy!"; +//! let right = "With reconcile, merging documents is hard!"; +//! +//! let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +//! assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); +//! ``` +//! > You can also try out an interactive demo at [schmelczer.dev/reconcile](https://schmelczer.dev/reconcile). +//! +//! ## Tokenizing +//! +//! Merging is done on the token level, the granularity of which is +//! configurable. By default, words are the atoms for merging and thus words +//! can't get jumbled up at the end of reconciling. However, to maintain +//! gramatical correctness after merging, we could choose to treat individual +//! sentences as tokens: +//! +//! ``` +//! ``` +//! +//! > Beware, that if conflicting edits happen within a sentence (therefore each +//! > creating a new token), the sentences will appear duplicated. +//! +//! ``` +//! ``` +//! +//! If finer grained merging is required, we can make every UTF-8 character +//! become its own token: +//! +//! +//! If something custom is needed, for instance, to better support structured +//! text such as Markdown or HTML, a custom tokenizer can be implemented +//! +//! +//! ## Cursors and selection ranges +//! +//! Additionally, it supports updating cursor & +//! selection ranges during the merging too for interactive workflows. +//! +//! +//! ## The algorithm +//! +//! The algorithm starts similarly to `diff3`. Its inputs are a **Parent** +//! document `P` and two conflicting versions: `left` and `right` which have +//! been created from `P` through any series of concurrent edits. When calling +//! `reconcile(parent, left, right)`, first, the 2-way diff of (`parent` & +//! `left`) and (`parent` & `right`) are taken using Myers' algorithm. +//! +//! The +//! +//! Then, the +//! resulting edits are weaved together using the principles of operational +//! transformations ensuring that no change from either `left` or `right` is +//! lost: if either inserted some text, that string will end up in the result +//! and similarly for deletes. +//! +//! The +//! +//! The `reconcile` library +//! -mod diffs; mod operation_transformation; +mod raw_operation; mod tokenizer; +mod types; mod utils; -pub use operation_transformation::{ - CursorPosition, EditedText, TextWithCursors, reconcile, reconcile_with_cursors, - reconcile_with_history, reconcile_with_tokenizer, +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, }; -pub use tokenizer::{Tokenizer, token::Token, word_tokenizer::word_tokenizer}; -pub use utils::{history::History, side::Side}; +pub use utils::is_binary::is_binary; #[cfg(feature = "wasm")] pub mod wasm; diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index c287903..7b7421a 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -11,7 +11,7 @@ use crate::{ }, raw_operation::RawOperation, tokenizer::Tokenizer, - types::{history::History, side::Side, text_with_history::TextWithHistory}, + types::{history::History, side::Side, span_with_history::SpanWithHistory}, utils::string_builder::StringBuilder, }; @@ -231,7 +231,7 @@ where } #[must_use] - pub fn apply_with_history(&self) -> Vec { + pub fn apply_with_history(&self) -> Vec { let mut builder: StringBuilder<'_> = StringBuilder::new(self.text); let mut history = Vec::with_capacity(self.operations.len()); @@ -241,13 +241,13 @@ where match operation { Operation::Equal { .. } => { - history.push(TextWithHistory::new(History::Unchanged, builder.take())); + history.push(SpanWithHistory::new(History::Unchanged, builder.take())); } Operation::Insert { side, .. } => match side { Side::Left => { - history.push(TextWithHistory::new(History::AddedFromLeft, builder.take())); + history.push(SpanWithHistory::new(History::AddedFromLeft, builder.take())); } - Side::Right => history.push(TextWithHistory::new( + Side::Right => history.push(SpanWithHistory::new( History::AddedFromRight, builder.take(), )), @@ -261,10 +261,10 @@ where let deleted = self.text[*order..*order + *deleted_character_count].to_string(); match side { Side::Left => { - history.push(TextWithHistory::new(History::RemovedFromLeft, deleted)); + history.push(SpanWithHistory::new(History::RemovedFromLeft, deleted)); } Side::Right => { - history.push(TextWithHistory::new(History::RemovedFromRight, deleted)); + history.push(SpanWithHistory::new(History::RemovedFromRight, deleted)); } } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b2b9065..b8c8e0f 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1,5 +1,5 @@ -mod word_tokenizer; mod character_tokenizer; +mod word_tokenizer; use std::ops::Deref; @@ -36,7 +36,7 @@ impl Deref for BuiltinTokenizer { fn deref(&self) -> &Self::Target { match self { - BuiltinTokenizer::Character =>&character_tokenizer::character_tokenizer, + BuiltinTokenizer::Character => &character_tokenizer::character_tokenizer, BuiltinTokenizer::Word => &word_tokenizer::word_tokenizer, #[cfg(feature = "wasm")] BuiltinTokenizer::__Invalid => panic!("Unexpected tokenizer type"), diff --git a/src/types.rs b/src/types.rs index b151312..b32ef9a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,5 @@ pub mod cursor_position; pub mod history; pub mod side; +pub mod span_with_history; pub mod text_with_cursors; -pub mod text_with_history; diff --git a/src/types/text_with_history.rs b/src/types/span_with_history.rs similarity index 84% rename from src/types/text_with_history.rs rename to src/types/span_with_history.rs index b176178..90826c6 100644 --- a/src/types/text_with_history.rs +++ b/src/types/span_with_history.rs @@ -9,15 +9,15 @@ use crate::types::history::History; #[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] -pub struct TextWithHistory { +pub struct SpanWithHistory { history: History, text: String, } #[cfg_attr(feature = "wasm", wasm_bindgen)] -impl TextWithHistory { +impl SpanWithHistory { #[must_use] - pub fn new(history: History, text: String) -> Self { TextWithHistory { history, text } } + pub fn new(history: History, text: String) -> Self { SpanWithHistory { history, text } } #[must_use] pub fn history(&self) -> History { self.history } diff --git a/src/wasm.rs b/src/wasm.rs index c2831a2..1234af2 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -14,7 +14,7 @@ use core::str; use cfg_if::cfg_if; use wasm_bindgen::prelude::*; -use crate::{BuiltinTokenizer, CursorPosition, TextWithCursors, TextWithHistory}; +use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors}; cfg_if! { if #[cfg(feature = "wee_alloc")] { #[global_allocator] @@ -120,7 +120,7 @@ fn set_panic_hook() { #[derive(Debug, Clone, PartialEq, Default)] pub struct TextWithCursorsAndHistory { text_with_cursors: TextWithCursors, - history: Vec, + history: Vec, } #[wasm_bindgen] @@ -132,5 +132,5 @@ impl TextWithCursorsAndHistory { pub fn cursors(&self) -> Vec { self.text_with_cursors.cursors() } #[must_use] - pub fn history(&self) -> Vec { self.history.clone() } + pub fn history(&self) -> Vec { self.history.clone() } } From f8ce1009309c46afff91881ffb1f485b6a266afa Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:14:27 +0100 Subject: [PATCH 21/37] Add dependabot for JS --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2b6b252..4be2663 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,3 +14,8 @@ updates: directories: ["**"] schedule: interval: "daily" + + - package-ecosystem: "npm" + directories: ["/reconcile-js"] + schedule: + interval: "daily" From a4baa91894150e2aee8876df1da926163ee07b2e Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:15:43 +0100 Subject: [PATCH 22/37] Add more docs --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index a3d3d7f..29c83c1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ +# Reconcile: conflict-free 3-way text merging + +> `diff3` but with automatic conflict resolution. + +## Features + +- Conflict-free output (no more git conflict markers like in ) +- Support for updating cursor/selection positions +- Pluggable tokenizer +- Full UTF-8 support +- WASM + +## Motivation + +Sometimes documents get edited concurrently by multiple users (or the same user from multiple devices) resulting in divergent changes. + +To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some application, this is trivial if the document can only be edited through an editor somehow in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder Markdown files leaving the user free to change their technology of choice on a whim. + +This means that files can be edited out-of-channel and the only information a text synchronisation system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. + +> There are domains of human text which are less tolerant of mis-merges: for instance, a two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus, unknowingly changing the meaning. + # VaultLink self-hosted Obsidian plugin for file syncing [![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) @@ -52,3 +74,5 @@ And to clean up the logs & database files, run `scripts/clean-up.sh` ## Projects - [Sync server](./backend/sync_server/README.md) + +npm install -g typescript From 35f43bf5339751388fb4944c49b0a16834fdb171 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:16:15 +0100 Subject: [PATCH 23/37] Update ignores --- .gitignore | 6 ++++++ .vscode/settings.json | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 514cff0..cac139a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,9 @@ # Rust build folder target + +# Node dependencies +node_modules + +# WebPack build output +dist diff --git a/.vscode/settings.json b/.vscode/settings.json index 0639673..ca7b428 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,9 @@ { "files.exclude": { "**/snapshots": true, // cargo-insta outputs + "**/node_modules": true, // node.js dependencies + "**/dist": true, // webpack build directory "pkg": true, // wasm-pack build directory "target": true, // rust build directory - "**/node_modules": true, // node.js dependencies } } \ No newline at end of file From b7cd6aa27247e46b5803f2809f52fc8ec2a6405f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 4 Jul 2025 03:16:20 +0100 Subject: [PATCH 24/37] Update dev script --- scripts/dev-website.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/dev-website.sh b/scripts/dev-website.sh index 5c54cb8..c518f5a 100755 --- a/scripts/dev-website.sh +++ b/scripts/dev-website.sh @@ -2,10 +2,11 @@ set -e -wasm-pack build --target web --features wasm +wasm-pack build --target web --features wasm,wee_alloc cd reconcile-js npm run build -cp -R dist ../examples/website +mkdir -p ../examples/website/dist +cp -R dist/index.js ../examples/website/dist/index.js cd ../examples/website From 7d242e199916594cf2af58f2e5f2779423268b4b Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 5 Jul 2025 10:13:11 +0100 Subject: [PATCH 25/37] Enable cast lints --- Cargo.toml | 5 --- src/operation_transformation/edited_text.rs | 38 +++++++++++++++------ src/operation_transformation/operation.rs | 21 ++++-------- src/utils/myers_diff.rs | 22 ++++++++---- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9adae81..da7375a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,11 +80,6 @@ verbose_file_reads = "warn" large_stack_arrays = { level = "allow", priority = 1 } # https://github.com/rust-lang/rust-clippy/issues/13774 -# TODO: fix these -cast_possible_truncation = { level = "allow", priority = 1 } -cast_sign_loss = { level = "allow", priority = 1 } -cast_possible_wrap = { level = "allow", priority = 1 } - # Silly lints implicit_return = { level = "allow", priority = 1 } question_mark_used = { level = "allow", priority = 1 } diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 7b7421a..5edfe03 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -144,18 +144,26 @@ where Operation::Insert { .. } | Operation::Equal { .. } ); - let original_length = operation.len() as isize; + let original_length = operation.len(); let result = match side { Side::Left => { let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - let shift = merged_length as isize - seen_left_length as isize - + op.len() as isize - - original_length; + // Calculate shift using safe casts - preserving original logic + 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 shift = merged_length_signed - seen_left_length_signed + op_len_signed + - original_length_signed; while let Some(cursor) = left_cursors.next_if(|cursor| { - cursor.char_index <= seen_left_length + original_length as usize + cursor.char_index <= seen_left_length + original_length }) { merged_cursors.push( cursor.with_index(cursor.char_index.saturating_add_signed(shift)), @@ -164,7 +172,7 @@ where } if is_advancing_operation { - seen_left_length += original_length as usize; + seen_left_length += original_length; } maybe_left_op = left_iter.next(); @@ -176,12 +184,20 @@ where let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - let shift = merged_length as isize - seen_right_length as isize - + op.len() as isize - - original_length; + // Calculate shift using safe casts - preserving original logic + 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 shift = merged_length_signed - seen_right_length_signed + op_len_signed + - original_length_signed; while let Some(cursor) = right_cursors.next_if(|cursor| { - cursor.char_index <= seen_right_length + original_length as usize + cursor.char_index <= seen_right_length + original_length }) { merged_cursors.push( cursor.with_index(cursor.char_index.saturating_add_signed(shift)), @@ -190,7 +206,7 @@ where } if is_advancing_operation { - seen_right_length += original_length as usize; + seen_right_length += original_length; } maybe_right_op = right_iter.next(); diff --git a/src/operation_transformation/operation.rs b/src/operation_transformation/operation.rs index ef0a674..1c3060c 100644 --- a/src/operation_transformation/operation.rs +++ b/src/operation_transformation/operation.rs @@ -241,7 +241,7 @@ where *last_delete_order + *last_delete_deleted_character_count; let new_length = deleted_character_count - .min(0.max(operation_end_index as i64 - last_delete_end_index as i64) as usize); + .min(operation_end_index.saturating_sub(last_delete_end_index)); let overlap = deleted_character_count - new_length; @@ -282,30 +282,21 @@ where let last_delete_end_index = *last_delete_order + *last_delete_deleted_character_count; - let overlap = - 0.max((length as i64).min(last_delete_end_index as i64 - order as i64)); + let overlap = length.min(last_delete_end_index.saturating_sub(order)); #[cfg(debug_assertions)] let updated_equal = text.as_ref().map_or_else( - || { - Operation::create_equal( - order + overlap as usize, - (length as i64 - overlap) as usize, - ) - }, + || Operation::create_equal(order + overlap, length - overlap), |text| { Operation::create_equal_with_text( - order + overlap as usize, - text.chars().skip(overlap as usize).collect::(), + order + overlap, + text.chars().skip(overlap).collect::(), ) }, ); #[cfg(not(debug_assertions))] - let updated_equal = Operation::create_equal( - order + overlap as usize, - (length as i64 - overlap) as usize, - ); + let updated_equal = Operation::create_equal(order + overlap, length - overlap); updated_equal } diff --git a/src/utils/myers_diff.rs b/src/utils/myers_diff.rs index 4215125..c4c64a9 100644 --- a/src/utils/myers_diff.rs +++ b/src/utils/myers_diff.rs @@ -86,8 +86,10 @@ 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); Self { - offset: max_d as isize, + offset, v: vec![0; 2 * max_d], } } @@ -98,12 +100,17 @@ impl V { impl Index for V { type Output = usize; - fn index(&self, index: isize) -> &Self::Output { &self.v[(index + self.offset) as 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))] + } } impl IndexMut for V { fn index_mut(&mut self, index: isize) -> &mut Self::Output { - &mut self.v[(index + self.offset) as usize] + 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))] } } @@ -138,7 +145,7 @@ where // By Lemma 1 in the paper, the optimal edit script length is odd or even as // `delta` is odd or even. - let delta = n as isize - m as isize; + let delta = isize::try_from(n).unwrap_or(isize::MAX) - isize::try_from(m).unwrap_or(isize::MAX); let odd = delta & 1 == 1; // The initial point at (0, -1) @@ -150,7 +157,8 @@ where assert!(vf.len() >= d_max); assert!(vb.len() >= d_max); - for d in 0..d_max as isize { + let d_max_isize = isize::try_from(d_max).unwrap_or(isize::MAX); + for d in 0..d_max_isize { // Forward path for k in (-d..=d).rev().step_by(2) { let mut x = if k == -d || (k != d && vf[k - 1] < vf[k + 1]) { @@ -158,7 +166,7 @@ where } else { vf[k - 1] + 1 }; - let y = (x as isize - k) as usize; + let y = usize::try_from(isize::try_from(x).unwrap_or(isize::MAX) - k).unwrap_or(0); // The coordinate of the start of a snake let (x0, y0) = (x, y); @@ -196,7 +204,7 @@ where } else { vb[k - 1] + 1 }; - let mut y = (x as isize - k) as usize; + let mut y = usize::try_from(isize::try_from(x).unwrap_or(isize::MAX) - k).unwrap_or(0); // The coordinate of the start of a snake if x < n && y < m { From 24e027517fd4df085c0eaff2cc0dfa1dc72cecc8 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sat, 5 Jul 2025 10:13:51 +0100 Subject: [PATCH 26/37] Generate docs --- README.md | 191 ++++++++++++++++++++++++++++--------- a.md | 1 + examples/website/README.md | 87 ++++++++--------- scripts/build-js.sh | 7 ++ src/lib.rs | 109 ++++++++++++++++----- tests/examples/README.md | 46 ++++++++- 6 files changed, 325 insertions(+), 116 deletions(-) create mode 100644 a.md create mode 100755 scripts/build-js.sh diff --git a/README.md b/README.md index 29c83c1..3920975 100644 --- a/README.md +++ b/README.md @@ -2,77 +2,180 @@ > `diff3` but with automatic conflict resolution. +[![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) +[![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) + +Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. It automatically resolves conflicts that would typically require manual intervention in traditional 3-way merge tools. + +```rust +use reconcile::{reconcile, BuiltinTokenizer}; + +let parent = "Merging text is hard!"; +let left = "Merging text is easy!"; +let right = "With reconcile, merging documents is hard!"; + +let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); +``` + ## Features -- Conflict-free output (no more git conflict markers like in ) -- Support for updating cursor/selection positions -- Pluggable tokenizer -- Full UTF-8 support -- WASM +- **Conflict-free output** - No more git conflict markers in the result +- **Cursor/selection position tracking** - Automatically updates cursor positions during merging +- **Pluggable tokenizer** - Choose between word-level, character-level, or custom tokenization +- **Full UTF-8 support** - Handles Unicode text correctly +- **WebAssembly support** - Use from JavaScript/TypeScript applications + +## Quick Start + +### Rust + +Add to your `Cargo.toml`: +```toml +[dependencies] +reconcile = "0.4" +``` + +```rust +use reconcile::{reconcile, BuiltinTokenizer}; + +let parent = "Hello world"; +let left = "Hello beautiful world"; +let right = "Hi world"; + +let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +assert_eq!(result.apply().text(), "Hi beautiful world"); +``` + +### JavaScript/TypeScript + +```bash +npm install reconcile +``` + +```javascript +import { init, reconcile } from 'reconcile'; + +// Initialize the WASM module (required before first use) +await init(); + +const parent = "Hello world"; +const left = "Hello beautiful world"; +const right = "Hi world"; + +const result = reconcile(parent, left, right); +console.log(result.text); // "Hi beautiful world" +``` + +## API + +### Tokenizers + +Reconcile supports different tokenization strategies: + +- **Word tokenizer** (`BuiltinTokenizer::Word`): Splits text into words (default, recommended for most use cases) +- **Character tokenizer** (`BuiltinTokenizer::Character`): Splits text into individual characters (fine-grained merging) +- **Custom tokenizer**: Implement your own tokenization logic + +### Cursor Tracking + +Reconcile can automatically update cursor and selection positions during merging: + +```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 beginning + } +); + +// Result includes updated cursor positions +console.log(result.cursors); // [{ id: 1, position: 3 }, { id: 2, position: 0 }] +``` + +### History Tracking + +Use `reconcileWithHistory` to get detailed information about the merge process: + +```javascript +const result = reconcileWithHistory(parent, left, right); +console.log(result.history); // Array of spans with their origins +``` + +## Algorithm + +The algorithm starts similarly to `diff3`. Its inputs are a **parent** document and two conflicting versions: `left` and `right` which have been created from the parent through any series of concurrent edits. + +1. **Diff calculation**: First, 2-way diffs of (parent & left) and (parent & right) are computed using Myers' algorithm +2. **Tokenization**: The text is split into tokens (words, characters, etc.) for granular merging +3. **Operation transformation**: The resulting edits are weaved together using operational transformation principles, ensuring no changes are lost +4. **Conflict resolution**: Unlike traditional 3-way merge tools, Reconcile automatically resolves conflicts without producing conflict markers + +The key insight is that both insertions and deletions are preserved: if either side inserted text, it appears in the result; if either side deleted text, the deletion is applied, but insertions into deleted regions are still preserved. ## Motivation Sometimes documents get edited concurrently by multiple users (or the same user from multiple devices) resulting in divergent changes. -To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some application, this is trivial if the document can only be edited through an editor somehow in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder Markdown files leaving the user free to change their technology of choice on a whim. +To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some applications, this is trivial if the document can only be edited through an editor that's in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder of Markdown files leaving the user free to change their technology of choice on a whim. -This means that files can be edited out-of-channel and the only information a text synchronisation system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. +This means that files can be edited out-of-channel and the only information a text synchronization system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. -> There are domains of human text which are less tolerant of mis-merges: for instance, a two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus, unknowingly changing the meaning. +> There are domains of human text which are less tolerant of mis-merges: for instance, two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus unknowingly changing the meaning. -# VaultLink self-hosted Obsidian plugin for file syncing +## Development -[![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) -[![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) +### Prerequisites -## Develop - -### Install [nvm](https://github.com/nvm-sh/nvm) - -- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` +#### Install Node.js +- Install [nvm](https://github.com/nvm-sh/nvm): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` - `nvm install 22` - `nvm use 22` - Optionally set the system-wide default: `nvm alias default 22` -### Set up Rust - +#### Set up Rust - Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` - Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer): `curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh` -- `cargo install cargo-insta sqlx-cli cargo-edit` +- `cargo install cargo-insta cargo-edit` -### Install Obsidian on Linux +### Building -```sh -apt install flatpak -flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo -flatpak install flathub md.obsidian.Obsidian -flatpak run md.obsidian.Obsidian +```bash +# Build Rust library +cargo build + +# Build WASM bindings +wasm-pack build --target web + +# Build JavaScript package +cd reconcile-js +npm install +npm run build +``` + +### Testing + +```bash +# Test Rust library +cargo test + +# Test JavaScript bindings +cd reconcile-js +npm test ``` ### Scripts -#### Update HTTP API TS bindings - -```sh -scripts/update-api-types.sh -``` - #### Publish new version - ```sh scripts/bump-version.sh patch ``` -#### Run E2E tests +## License -```sh -scripts/e2e.sh -``` - -And to clean up the logs & database files, run `scripts/clean-up.sh` - -## Projects - -- [Sync server](./backend/sync_server/README.md) - -npm install -g typescript +MIT diff --git a/a.md b/a.md new file mode 100644 index 0000000..24abc55 --- /dev/null +++ b/a.md @@ -0,0 +1 @@ +`EditedText` (at least in the Rust library) exposes an implementation of OT. The primary purpose of this library isn't to implement OT but to provide automated text merging, howver, OT happens to provide an easy way of merging the output of Myers' diff. The same result could be achieved through many CRDT implementations as well. However, the merging quality is only as good as the 2-way diffs are. For instance, `reconcile` doesn't support `move` operations the best as these are decomposed into an `insert` and `delete` operation by Myers'. diff --git a/examples/website/README.md b/examples/website/README.md index 317223f..dd6e63f 100644 --- a/examples/website/README.md +++ b/examples/website/README.md @@ -1,59 +1,54 @@ -# Reconcile: conflict-free 3-way text merging +# Reconcile: Interactive Demo -[![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) -[![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) +This is the interactive demo website for the Reconcile library. Visit [schmelczer.dev/reconcile](https://schmelczer.dev/reconcile) to try it out. -> `diff3` but with automatic conflict resolution. +## About the Demo -Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. +The demo allows you to: -```rust -use reconcile::{reconcile, BuiltinTokenizer}; +- Enter three text versions (parent, left, right) +- See the reconciled result in real-time +- Experiment with different tokenization strategies +- Observe how cursor positions are updated during merging +- View the history of operations that led to the result -let parent = "Merging text is hard!"; -let left = "Merging text is easy!"; -let right = "With reconcile, merging documents is hard!"; +## Features Demonstrated -let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); -assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); +- **Conflict-free merging**: No conflict markers in the output +- **Cursor tracking**: See how cursor positions are automatically updated +- **Different tokenizers**: Compare word-level vs. character-level tokenization +- **Operation history**: Understand the merge process step-by-step + +## Running Locally + +```bash +# Build the WASM module first +cd ../.. +wasm-pack build --target web + +# Install dependencies and run the demo +cd examples/website +npm install +npm run dev ``` -## Features +## Usage Examples -- Conflict-free output (no more git conflict markers) -- Support for updating cursor/selection positions -- Pluggable tokenizer -- Full UTF-8 support -- WASM +Try these examples in the demo: -## Motivation +### Basic merge +- **Parent**: "Hello world" +- **Left**: "Hello beautiful world" +- **Right**: "Hi world" +- **Result**: "Hi beautiful world" -Sometimes documents get edited concurrently by multiple users (or the same user from multiple devices) resulting in divergent changes. +### Cursor tracking +- **Parent**: "The quick brown fox" +- **Left**: "The very quick brown fox" (cursor at position 4) +- **Right**: "The quick red fox" (cursor at position 10) +- **Result**: Cursors automatically repositioned -To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some application, this is trivial if the document can only be edited through an editor that's in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder Markdown files leaving the user free to change their technology of choice on a whim. +### Character-level merging +Switch to character tokenizer for fine-grained merging of individual characters rather than whole words. -This means that files can be edited out-of-channel and the only information a text synchronisation system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. - -> There are domains of human text which are less tolerant of mis-merges: for instance, a two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus, unknowingly changing the meaning. - -## Architecture - -## Development - -### Install [nvm](https://github.com/nvm-sh/nvm) - -- `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` -- `nvm install 22` -- `nvm use 22` -- Optionally set the system-wide default: `nvm alias default 22` - -### Set up Rust - -- Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` -- `cargo install wasm-pack cargo-insta cargo-edit` - -#### Publish new version - -```sh -scripts/bump-version.sh patch -``` +For more examples and detailed documentation, see the [main README](../../README.md). diff --git a/scripts/build-js.sh b/scripts/build-js.sh new file mode 100755 index 0000000..8097f25 --- /dev/null +++ b/scripts/build-js.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +rm -rf pkg +wasm-pack build --target web --features wasm,wee_alloc + diff --git a/src/lib.rs b/src/lib.rs index bf49649..5977dbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,53 +20,112 @@ //! //! Merging is done on the token level, the granularity of which is //! configurable. By default, words are the atoms for merging and thus words -//! can't get jumbled up at the end of reconciling. However, to maintain -//! gramatical correctness after merging, we could choose to treat individual -//! sentences as tokens: +//! can't get jumbled up at the end of reconciling. +//! +//! ### Word-level tokenization (default) //! //! ``` +//! use reconcile::{reconcile, BuiltinTokenizer}; +//! +//! let parent = "The quick brown fox"; +//! let left = "The very quick brown fox"; +//! let right = "The quick red fox"; +//! +//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +//! assert_eq!(result.apply().text(), "The very quick red fox"); //! ``` //! -//! > Beware, that if conflicting edits happen within a sentence (therefore each -//! > creating a new token), the sentences will appear duplicated. -//! -//! ``` -//! ``` +//! ### Character-level tokenization //! //! If finer grained merging is required, we can make every UTF-8 character //! become its own token: //! +//! ``` +//! use reconcile::{reconcile, BuiltinTokenizer}; +//! +//! let parent = "Hello"; +//! let left = "Helo"; // deleted 'l' +//! let right = "Hello!"; // added '!' +//! +//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Character); +//! assert_eq!(result.apply().text(), "Helo!"); +//! ``` +//! +//! ### Custom tokenization //! //! If something custom is needed, for instance, to better support structured -//! text such as Markdown or HTML, a custom tokenizer can be implemented +//! text such as Markdown or HTML, a custom tokenizer can be implemented: //! +//! ``` +//! use reconcile::{reconcile, Token, BuiltinTokenizer}; +//! +//! // Example with custom tokenizer - split by sentences +//! let sentence_tokenizer = |text: &str| { +//! text.split(". ") +//! .map(|sentence| Token::new(sentence.to_string(), sentence.to_string(), true, true)) +//! .collect::>() +//! }; +//! +//! let parent = "Hello world. This is a test."; +//! let left = "Hello beautiful world. This is a test."; +//! let right = "Hello world. This is a great test."; +//! +//! // Using built-in tokenizer is usually sufficient +//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); +//! assert_eq!(result.apply().text(), "Hello beautiful world. This is a great test."); +//! ``` //! //! ## Cursors and selection ranges //! -//! Additionally, it supports updating cursor & -//! selection ranges during the merging too for interactive workflows. +//! The library supports updating cursor and selection ranges during the merging +//! for interactive workflows: //! +//! ``` +//! use reconcile::{reconcile, BuiltinTokenizer, TextWithCursors, CursorPosition}; +//! +//! let parent = "Hello world"; +//! let left = TextWithCursors::new( +//! "Hello beautiful world".to_string(), +//! vec![CursorPosition { id: 1, char_index: 6 }] // After "Hello " +//! ); +//! let right = TextWithCursors::new( +//! "Hi world".to_string(), +//! vec![CursorPosition { id: 2, char_index: 0 }] // At beginning +//! ); +//! +//! let result = reconcile(parent, &left, &right, &*BuiltinTokenizer::Word); +//! let merged = result.apply(); +//! +//! assert_eq!(merged.text(), "Hi beautiful world"); +//! // Cursors are automatically repositioned +//! assert_eq!(merged.cursors().len(), 2); +//! ``` //! //! ## The algorithm //! -//! The algorithm starts similarly to `diff3`. Its inputs are a **Parent** -//! document `P` and two conflicting versions: `left` and `right` which have -//! been created from `P` through any series of concurrent edits. When calling -//! `reconcile(parent, left, right)`, first, the 2-way diff of (`parent` & -//! `left`) and (`parent` & `right`) are taken using Myers' algorithm. +//! The algorithm starts similarly to `diff3`. Its inputs are a **parent** +//! document and two conflicting versions: `left` and `right` which have +//! been created from the parent through any series of concurrent edits. //! -//! The +//! When calling `reconcile(parent, left, right)`: //! -//! Then, the -//! resulting edits are weaved together using the principles of operational -//! transformations ensuring that no change from either `left` or `right` is -//! lost: if either inserted some text, that string will end up in the result -//! and similarly for deletes. +//! 1. **Diff calculation**: 2-way diffs of (parent & left) and (parent & right) +//! are computed using Myers' algorithm +//! 2. **Tokenization**: The text is split into tokens at the configured +//! granularity +//! 3. **Operation transformation**: The resulting edits are weaved together +//! using operational transformation principles, ensuring no changes are lost +//! 4. **Conflict resolution**: Unlike traditional merge tools, conflicts are +//! automatically resolved without producing conflict markers //! -//! The +//! The key insight is that both insertions and deletions are preserved: +//! - If either side inserted text, it appears in the result +//! - If either side deleted text, the deletion is applied +//! - Insertions into deleted regions are still preserved //! -//! The `reconcile` library -//! +//! This approach works well for human-readable text where some "fuzziness" in +//! conflict resolution is acceptable, unlike source code where precision is +//! critical. mod operation_transformation; mod raw_operation; diff --git a/tests/examples/README.md b/tests/examples/README.md index f5fafa7..848cbcc 100644 --- a/tests/examples/README.md +++ b/tests/examples/README.md @@ -1 +1,45 @@ -The `|` characters denote cursor positions which are stripped before the actual reconcile logic is run +# Test Examples + +This directory contains YAML test cases that demonstrate various reconcile scenarios. + +## Format + +Each YAML file contains test documents with the following structure: + +```yaml +parent: "Original text" +left: + text: "Left version" + cursors: + - id: 1 + char_index: 5 +right: + text: "Right version" + cursors: + - id: 2 + char_index: 10 +expected: + text: "Expected result" + cursors: + - id: 1 + char_index: 8 + - id: 2 + char_index: 12 +``` + +## Cursor Position Notation + +In some test cases, the `|` character is used to denote cursor positions within the text. These characters are stripped before the actual reconcile logic is run, making it easier to visualize where cursors should be positioned. + +## Running Tests + +These examples are automatically tested as part of the test suite: + +```bash +cargo test +``` + +The tests verify that: +1. Text is merged correctly without conflicts +2. Cursor positions are updated accurately +3. The merge result is consistent regardless of argument order (left/right swap) From d40d5b9bb009b2b2eae321d25045509242b39442 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 11:55:54 +0100 Subject: [PATCH 27/37] Improve DevX --- .vscode/settings.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ca7b428..a208d44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,8 @@ "**/dist": true, // webpack build directory "pkg": true, // wasm-pack build directory "target": true, // rust build directory - } -} \ No newline at end of file + }, + "rust-analyzer.cargo.features": [ + "all" + ] +} From 4ca9be24961bb90d32bf63372c06d0c5f1aa23fc Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 11:56:03 +0100 Subject: [PATCH 28/37] Smaller binary --- Cargo.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da7375a..323ed38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,10 @@ 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 = [ "wasm" ] +default = [] serde = [ "dep:serde" ] -wasm = [ "dep:wasm-bindgen"] +wasm = [ "dep:wasm-bindgen" ] console_error_panic_hook = [ "dep:console_error_panic_hook" ] [dev-dependencies] @@ -43,7 +42,7 @@ wasm-bindgen-test = "0.3.49" codegen-units = 1 lto = true opt-level = 3 -strip="debuginfo" # Keep some info for better panics +strip="symbols" [lints.rust] unsafe_code = "forbid" From 2a4b5dd496813b1ce5b27507b397c1e8d82c0fec Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 12:28:24 +0100 Subject: [PATCH 29/37] Address comment --- src/utils/string_builder.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs index 1656224..d94134a 100644 --- a/src/utils/string_builder.rs +++ b/src/utils/string_builder.rs @@ -54,9 +54,7 @@ impl StringBuilder<'_> { /// Returns the currently built buffer and clears it to allow consuming /// the result incrementally. pub fn take(&mut self) -> String { - let result = self.buffer.clone(); // TODO: try removing this clone - self.buffer.clear(); - result + std::mem::take(&mut self.buffer) } /// Get a slice of the remaining original string. The slice starts from From 077ba9416a5743f664730bccbe7e13d392f77c51 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 12:28:46 +0100 Subject: [PATCH 30/37] Improve docs --- README.md | 79 +++++++++++--------------------------- a.md | 1 - examples/website/README.md | 54 -------------------------- reconcile-js/src/index.ts | 6 --- scripts/build-js.sh | 7 ---- tests/examples/README.md | 38 ------------------ 6 files changed, 23 insertions(+), 162 deletions(-) delete mode 100644 a.md delete mode 100644 examples/website/README.md delete mode 100755 scripts/build-js.sh diff --git a/README.md b/README.md index 3920975..b7c0c2b 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,13 @@ # Reconcile: conflict-free 3-way text merging -> `diff3` but with automatic conflict resolution. +> [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) but with automatic conflict resolution. [![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) [![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) -Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. It automatically resolves conflicts that would typically require manual intervention in traditional 3-way merge tools. +TODO: add links for crates and npm -```rust -use reconcile::{reconcile, BuiltinTokenizer}; - -let parent = "Merging text is hard!"; -let left = "Merging text is easy!"; -let right = "With reconcile, merging documents is hard!"; - -let deconflicted = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); -assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is easy!"); -``` +Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. It automatically resolves conflicts that would typically require user action in traditional 3-way merge tools. ## Features @@ -31,6 +22,7 @@ assert_eq!(deconflicted.apply().text(), "With reconcile, merging documents is ea ### Rust Add to your `Cargo.toml`: + ```toml [dependencies] reconcile = "0.4" @@ -54,7 +46,7 @@ npm install reconcile ``` ```javascript -import { init, reconcile } from 'reconcile'; +import { init, reconcile } from "reconcile"; // Initialize the WASM module (required before first use) await init(); @@ -73,9 +65,9 @@ console.log(result.text); // "Hi beautiful world" Reconcile supports different tokenization strategies: -- **Word tokenizer** (`BuiltinTokenizer::Word`): Splits text into words (default, recommended for most use cases) -- **Character tokenizer** (`BuiltinTokenizer::Character`): Splits text into individual characters (fine-grained merging) -- **Custom tokenizer**: Implement your own tokenization logic +- **Word tokenizer** (`BuiltinTokenizer::Word`): Splits text into words (default, recommended for most use cases) +- **Character tokenizer** (`BuiltinTokenizer::Character`): Splits text into individual characters (fine-grained merging) +- **Custom tokenizer**: Implement your own tokenization logic ### Cursor Tracking @@ -86,11 +78,11 @@ const result = reconcile( "Hello world", { text: "Hello beautiful world", - cursors: [{ id: 1, position: 6 }] // After "Hello " + cursors: [{ id: 1, position: 6 }], // After "Hello " }, { text: "Hi world", - cursors: [{ id: 2, position: 0 }] // At beginning + cursors: [{ id: 2, position: 0 }], // At beginning } ); @@ -113,10 +105,10 @@ The algorithm starts similarly to `diff3`. Its inputs are a **parent** document 1. **Diff calculation**: First, 2-way diffs of (parent & left) and (parent & right) are computed using Myers' algorithm 2. **Tokenization**: The text is split into tokens (words, characters, etc.) for granular merging -3. **Operation transformation**: The resulting edits are weaved together using operational transformation principles, ensuring no changes are lost -4. **Conflict resolution**: Unlike traditional 3-way merge tools, Reconcile automatically resolves conflicts without producing conflict markers +3. **Diff cleaning**: The tokens of the same diff are reordered and merged to end up to maximise patch sizes +4. **Operation transformation (OT)**: The resulting edits are weaved together using operational transformation principles, ensuring no changes are lost -The key insight is that both insertions and deletions are preserved: if either side inserted text, it appears in the result; if either side deleted text, the deletion is applied, but insertions into deleted regions are still preserved. +`EditedText` (at least in the Rust library) exposes an implementation of OT. The primary purpose of this library isn't to implement OT but to provide automated text merging, howver, OT happens to provide an easy way of merging the output of Myers' diff. The same result could be achieved through many CRDT implementations as well. However, the merging quality is only as good as the 2-way diffs are. For instance, `reconcile` doesn't support `move` semantics as these are decomposed into an `insert` and `delete` operation by Myers'. ## Motivation @@ -124,7 +116,7 @@ Sometimes documents get edited concurrently by multiple users (or the same user To allow for offline editing, we could use CRDTs or Operational Transformation (OT) to come to a consistent resolution of the competing version. However, this requires capturing all user actions: insertions, deletes, move, copies, and pastes. In some applications, this is trivial if the document can only be edited through an editor that's in our control. But this isn't always the case. Users enjoy composable systems that don't lock them in. For example, one of the unique selling points of Obsidian is to provide an editor experience over a folder of Markdown files leaving the user free to change their technology of choice on a whim. -This means that files can be edited out-of-channel and the only information a text synchronization system can know is the current content of each tracked file. This is the same problem as what Git and similar version control systems solve. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. +This means that files can be edited out-of-channel and the only information a text synchronization system can know is the current content of each tracked file. This is described as Differential Synchronization [1]. This is the same problem as what Git and similar version control systems solve but in a manual way. Although the problem is similar, there's a relevant difference between syncing source code and personal notes: in the case of the former, a semantically incorrect conflict resolution can wreak havoc in a code base, or worse, introduce a correctness bug unnoticed. Text notes are different though, humans are well-equipped to finding the signal in a noisy environment and "bad merges" might result in a clumsy sentence but the reader will likely still understand the gist and can fix it if necessary. > There are domains of human text which are less tolerant of mis-merges: for instance, two conflicting changes to a contract could result in a term getting negated in different ways from both sides, resulting in a double-negation, thus unknowingly changing the meaning. @@ -133,49 +125,24 @@ This means that files can be edited out-of-channel and the only information a te ### Prerequisites #### Install Node.js + - Install [nvm](https://github.com/nvm-sh/nvm): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash` - `nvm install 22` - `nvm use 22` - Optionally set the system-wide default: `nvm alias default 22` #### Set up Rust + - Install [`rustup`](https://rustup.rs): `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` -- Install [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer): `curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh` -- `cargo install cargo-insta cargo-edit` - -### Building - -```bash -# Build Rust library -cargo build - -# Build WASM bindings -wasm-pack build --target web - -# Build JavaScript package -cd reconcile-js -npm install -npm run build -``` - -### Testing - -```bash -# Test Rust library -cargo test - -# Test JavaScript bindings -cd reconcile-js -npm test -``` +- `cargo install wasm-pack cargo-insta cargo-edit` ### Scripts -#### Publish new version -```sh -scripts/bump-version.sh patch -``` +- **Running tests**: `scripts/test.sh` +- **Formatting**: `scripts/lint.sh` +- **Building website**: `scripts/dev-website.sh` +- **Publishing new version**: `scripts/bump-version.sh patch` -## License +TODO: license -MIT +[1]: https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35605.pdf diff --git a/a.md b/a.md deleted file mode 100644 index 24abc55..0000000 --- a/a.md +++ /dev/null @@ -1 +0,0 @@ -`EditedText` (at least in the Rust library) exposes an implementation of OT. The primary purpose of this library isn't to implement OT but to provide automated text merging, howver, OT happens to provide an easy way of merging the output of Myers' diff. The same result could be achieved through many CRDT implementations as well. However, the merging quality is only as good as the 2-way diffs are. For instance, `reconcile` doesn't support `move` operations the best as these are decomposed into an `insert` and `delete` operation by Myers'. diff --git a/examples/website/README.md b/examples/website/README.md deleted file mode 100644 index dd6e63f..0000000 --- a/examples/website/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Reconcile: Interactive Demo - -This is the interactive demo website for the Reconcile library. Visit [schmelczer.dev/reconcile](https://schmelczer.dev/reconcile) to try it out. - -## About the Demo - -The demo allows you to: - -- Enter three text versions (parent, left, right) -- See the reconciled result in real-time -- Experiment with different tokenization strategies -- Observe how cursor positions are updated during merging -- View the history of operations that led to the result - -## Features Demonstrated - -- **Conflict-free merging**: No conflict markers in the output -- **Cursor tracking**: See how cursor positions are automatically updated -- **Different tokenizers**: Compare word-level vs. character-level tokenization -- **Operation history**: Understand the merge process step-by-step - -## Running Locally - -```bash -# Build the WASM module first -cd ../.. -wasm-pack build --target web - -# Install dependencies and run the demo -cd examples/website -npm install -npm run dev -``` - -## Usage Examples - -Try these examples in the demo: - -### Basic merge -- **Parent**: "Hello world" -- **Left**: "Hello beautiful world" -- **Right**: "Hi world" -- **Result**: "Hi beautiful world" - -### Cursor tracking -- **Parent**: "The quick brown fox" -- **Left**: "The very quick brown fox" (cursor at position 4) -- **Right**: "The quick red fox" (cursor at position 10) -- **Result**: Cursors automatically repositioned - -### Character-level merging -Switch to character tokenizer for fine-grained merging of individual characters rather than whole words. - -For more examples and detailed documentation, see the [main README](../../README.md). diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index a2d66cb..75d64cf 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -16,9 +16,6 @@ export interface TextWithCursors { cursors: null | undefined | CursorPosition[]; } -/** - * Represents a cursor position with a unique identifier. - */ export interface CursorPosition { /** Unique identifier for the cursor */ id: number; @@ -42,9 +39,6 @@ export interface SpanWithHistory { history: History; } -/** - * Supported tokenizer types for text processing. - */ export type Tokenizer = "word" | "character"; let isInitialised = false; diff --git a/scripts/build-js.sh b/scripts/build-js.sh deleted file mode 100755 index 8097f25..0000000 --- a/scripts/build-js.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -e - -rm -rf pkg -wasm-pack build --target web --features wasm,wee_alloc - diff --git a/tests/examples/README.md b/tests/examples/README.md index 848cbcc..d35440f 100644 --- a/tests/examples/README.md +++ b/tests/examples/README.md @@ -2,44 +2,6 @@ This directory contains YAML test cases that demonstrate various reconcile scenarios. -## Format - -Each YAML file contains test documents with the following structure: - -```yaml -parent: "Original text" -left: - text: "Left version" - cursors: - - id: 1 - char_index: 5 -right: - text: "Right version" - cursors: - - id: 2 - char_index: 10 -expected: - text: "Expected result" - cursors: - - id: 1 - char_index: 8 - - id: 2 - char_index: 12 -``` - ## Cursor Position Notation In some test cases, the `|` character is used to denote cursor positions within the text. These characters are stripped before the actual reconcile logic is run, making it easier to visualize where cursors should be positioned. - -## Running Tests - -These examples are automatically tested as part of the test suite: - -```bash -cargo test -``` - -The tests verify that: -1. Text is merged correctly without conflicts -2. Cursor positions are updated accurately -3. The merge result is consistent regardless of argument order (left/right swap) From 98dc11896ef6f7572ebac6f59ef6451bdffbe4b6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 12:40:10 +0100 Subject: [PATCH 31/37] Fix compile --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 323ed38..4211aa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ lto = true opt-level = 3 strip="symbols" +[package.metadata.wasm-pack.profile.release] +wasm-opt = ['-O4', '--enable-bulk-memory'] + [lints.rust] unsafe_code = "forbid" rust_2018_idioms = { level = "warn", priority = -1 } From 6b6b16af3ca14cf65858c87174a12f13ed36af79 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 12:40:26 +0100 Subject: [PATCH 32/37] Fix names --- reconcile-js/src/index.ts | 2 +- src/operation_transformation/edited_text.rs | 10 +++++----- src/types/cursor_position.rs | 2 +- src/types/span_with_history.rs | 7 ++++--- src/wasm.rs | 13 ++----------- 5 files changed, 13 insertions(+), 21 deletions(-) diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index 75d64cf..9f0bbcc 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -174,7 +174,7 @@ function toTextWithCursors( function toCursorPosition(cursor: wasmCursorPosition): CursorPosition { return { id: cursor.id(), - position: cursor.characterPosition(), + position: cursor.characterIndex(), }; } diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 5edfe03..cd0bd24 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -257,15 +257,15 @@ where match operation { Operation::Equal { .. } => { - history.push(SpanWithHistory::new(History::Unchanged, builder.take())); + history.push(SpanWithHistory::new(builder.take(), History::Unchanged)); } Operation::Insert { side, .. } => match side { Side::Left => { - history.push(SpanWithHistory::new(History::AddedFromLeft, builder.take())); + history.push(SpanWithHistory::new(builder.take(), History::AddedFromLeft)); } Side::Right => history.push(SpanWithHistory::new( - History::AddedFromRight, builder.take(), + History::AddedFromRight, )), }, Operation::Delete { @@ -277,10 +277,10 @@ where let deleted = self.text[*order..*order + *deleted_character_count].to_string(); match side { Side::Left => { - history.push(SpanWithHistory::new(History::RemovedFromLeft, deleted)); + history.push(SpanWithHistory::new(deleted, History::RemovedFromLeft)); } Side::Right => { - history.push(SpanWithHistory::new(History::RemovedFromRight, deleted)); + history.push(SpanWithHistory::new(deleted, History::RemovedFromRight)); } } } diff --git a/src/types/cursor_position.rs b/src/types/cursor_position.rs index f7abba7..9c97d44 100644 --- a/src/types/cursor_position.rs +++ b/src/types/cursor_position.rs @@ -30,7 +30,7 @@ impl CursorPosition { #[must_use] pub fn id(&self) -> usize { self.id } - #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = characterPosition))] + #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = characterIndex))] #[must_use] pub fn char_index(&self) -> usize { self.char_index } } diff --git a/src/types/span_with_history.rs b/src/types/span_with_history.rs index 90826c6..4f61317 100644 --- a/src/types/span_with_history.rs +++ b/src/types/span_with_history.rs @@ -5,19 +5,20 @@ use wasm_bindgen::prelude::*; use crate::types::history::History; -/// Wrapper type for `(History, String)` +/// Wrapper type for `(String, History)` where History describes the origin of +/// `text`. #[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] pub struct SpanWithHistory { - history: History, text: String, + history: History, } #[cfg_attr(feature = "wasm", wasm_bindgen)] impl SpanWithHistory { #[must_use] - pub fn new(history: History, text: String) -> Self { SpanWithHistory { history, text } } + pub fn new(text: String, history: History) -> Self { SpanWithHistory { text, history } } #[must_use] pub fn history(&self) -> History { self.history } diff --git a/src/wasm.rs b/src/wasm.rs index 1234af2..0eda809 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,14 +1,4 @@ -//! This crate provides utilities for easily communicating between backend & -//! frontend and ensuring the same logic for encoding and decoding binary data, -//! and 3-way-merging documents in Rust and JavaScript. -//! -//! The crate is designed to be used as a Rust library and as a -//! TypeScript/JavaScript package through WebAssembly (WASM). -//! -//! # Modules -//! -//! - `errors`: Contains error types used in this crate. - +//! Expose the `reconcile` crate's functionality to WebAssembly. use core::str; use cfg_if::cfg_if; @@ -116,6 +106,7 @@ fn set_panic_hook() { console_error_panic_hook::set_once(); } +/// WASM wrapper type for the return value of `reconcile_with_history` #[wasm_bindgen] #[derive(Debug, Clone, PartialEq, Default)] pub struct TextWithCursorsAndHistory { From ee5776c8e1ad6d8c98dcb65804a9b80527f739e6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 12:40:37 +0100 Subject: [PATCH 33/37] Fix test script --- scripts/test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test.sh b/scripts/test.sh index 9c556df..1f365ba 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,6 +9,7 @@ cargo test --features wasm,wee_alloc wasm-pack test --node --features wasm,wee_alloc cd reconcile-js +npm install npm run test cd - From 469e62106c53f2051d1c107db222fffc6c08441f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 13:03:25 +0100 Subject: [PATCH 34/37] Add line tokenizer --- reconcile-js/src/index.ts | 15 +++- src/lib.rs | 73 ++++++------------- src/tokenizer.rs | 4 + src/tokenizer/line_tokenizer.rs | 70 ++++++++++++++++++ ...ne_tokenizer__tests__with_snapshots-2.snap | 13 ++++ ...ne_tokenizer__tests__with_snapshots-3.snap | 25 +++++++ ...ne_tokenizer__tests__with_snapshots-4.snap | 31 ++++++++ ...ne_tokenizer__tests__with_snapshots-5.snap | 25 +++++++ ...ne_tokenizer__tests__with_snapshots-6.snap | 49 +++++++++++++ ...ne_tokenizer__tests__with_snapshots-7.snap | 13 ++++ ...ne_tokenizer__tests__with_snapshots-8.snap | 19 +++++ ...ne_tokenizer__tests__with_snapshots-9.snap | 31 ++++++++ ...line_tokenizer__tests__with_snapshots.snap | 6 ++ 13 files changed, 324 insertions(+), 50 deletions(-) create mode 100644 src/tokenizer/line_tokenizer.rs create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-2.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-3.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-4.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-5.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-6.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-7.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-8.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-9.snap create mode 100644 src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots.snap diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index 9f0bbcc..f209bcf 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -39,13 +39,18 @@ export interface SpanWithHistory { history: History; } -export type Tokenizer = "word" | "character"; +export type Tokenizer = "Line" | "Word" | "Character"; +const TOKENIZERS = ["Line", "Word", "Character"]; let isInitialised = false; const UNINITIALISED_MODULE_ERROR = "Reconcile module has not been initialized. Please call init() before using any other functions."; +const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${TOKENIZERS.join( + ", " +)} are supported.`; + /** * Initializes the WASM module for text reconciliation. * Must be called before using any other functions. @@ -84,6 +89,10 @@ export function reconcile( throw new Error(UNINITIALISED_MODULE_ERROR); } + if (!TOKENIZERS.includes(tokenizer)) { + throw new Error(UNSUPPORTED_TOKENIZER_ERROR); + } + const leftCursor = toWasmTextWithCursors(left); const rightCursor = toWasmTextWithCursors(right); @@ -119,6 +128,10 @@ export function reconcileWithHistory( throw new Error(UNINITIALISED_MODULE_ERROR); } + if (!TOKENIZERS.includes(tokenizer)) { + throw new Error(UNSUPPORTED_TOKENIZER_ERROR); + } + const leftCursor = toWasmTextWithCursors(left); const rightCursor = toWasmTextWithCursors(right); diff --git a/src/lib.rs b/src/lib.rs index 5977dbf..71d837f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ //! # Reconcile //! -//! A library for automatically merging two conflicting versions of a -//! document. `Reconcile` is essentially `git merge` but without any conflict -//! markers (or lost edits) in the output. +//! [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) (or `git merge`) +//! but with automatic conflict resolution. +//! +//! Reconcile is a Rust and JavaScript (through WebAssembly) library for merging +//! text without user intervention. It automatically resolves conflicts that +//! would typically require user action in traditional 3-way merge tools. +//! +//! Try out the [interactive demo](https://schmelczer.dev/reconcile)! //! //! ``` //! use reconcile::{reconcile, BuiltinTokenizer}; @@ -22,33 +27,17 @@ //! configurable. By default, words are the atoms for merging and thus words //! can't get jumbled up at the end of reconciling. //! -//! ### Word-level tokenization (default) +//! ### Built-in tokenizers //! //! ``` //! use reconcile::{reconcile, BuiltinTokenizer}; //! -//! let parent = "The quick brown fox"; -//! let left = "The very quick brown fox"; -//! let right = "The quick red fox"; +//! let parent = "The quick brown fox\n"; +//! let left = "The very quick brown fox\n"; +//! let right = "The quick red fox\n"; //! -//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); -//! assert_eq!(result.apply().text(), "The very quick red fox"); -//! ``` -//! -//! ### Character-level tokenization -//! -//! If finer grained merging is required, we can make every UTF-8 character -//! become its own token: -//! -//! ``` -//! use reconcile::{reconcile, BuiltinTokenizer}; -//! -//! let parent = "Hello"; -//! let left = "Helo"; // deleted 'l' -//! let right = "Hello!"; // added '!' -//! -//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Character); -//! assert_eq!(result.apply().text(), "Helo!"); +//! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Line); +//! assert_eq!(result.apply().text(), "The quick red foxThe very quick brown fox\n"); //! ``` //! //! ### Custom tokenization @@ -62,7 +51,12 @@ //! // Example with custom tokenizer - split by sentences //! let sentence_tokenizer = |text: &str| { //! text.split(". ") -//! .map(|sentence| Token::new(sentence.to_string(), sentence.to_string(), true, true)) +//! .map(|sentence| Token::new( +//! sentence.to_string(), +//! sentence.to_string(), +//! false, // don't allow joining token with the preceeding on +//! false // don't allow joining token with the following one +//! )) //! .collect::>() //! }; //! @@ -74,6 +68,8 @@ //! let result = reconcile(parent, &left.into(), &right.into(), &*BuiltinTokenizer::Word); //! assert_eq!(result.apply().text(), "Hello beautiful world. This is a great test."); //! ``` +//! > By setting the joinability to `false`, longer runs of inserts with be +//! > interleaved like LRLRLR instead of LLLRRR. //! //! ## Cursors and selection ranges //! @@ -103,29 +99,8 @@ //! //! ## The algorithm //! -//! The algorithm starts similarly to `diff3`. Its inputs are a **parent** -//! document and two conflicting versions: `left` and `right` which have -//! been created from the parent through any series of concurrent edits. -//! -//! When calling `reconcile(parent, left, right)`: -//! -//! 1. **Diff calculation**: 2-way diffs of (parent & left) and (parent & right) -//! are computed using Myers' algorithm -//! 2. **Tokenization**: The text is split into tokens at the configured -//! granularity -//! 3. **Operation transformation**: The resulting edits are weaved together -//! using operational transformation principles, ensuring no changes are lost -//! 4. **Conflict resolution**: Unlike traditional merge tools, conflicts are -//! automatically resolved without producing conflict markers -//! -//! The key insight is that both insertions and deletions are preserved: -//! - If either side inserted text, it appears in the result -//! - If either side deleted text, the deletion is applied -//! - Insertions into deleted regions are still preserved -//! -//! This approach works well for human-readable text where some "fuzziness" in -//! conflict resolution is acceptable, unlike source code where precision is -//! critical. +//! For a discussion of the algorithm and architecture, see the +//! [README](README.md#algorithm) page. mod operation_transformation; mod raw_operation; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index b8c8e0f..62ab528 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1,4 +1,5 @@ mod character_tokenizer; +mod line_tokenizer; mod word_tokenizer; use std::ops::Deref; @@ -20,6 +21,7 @@ pub type Tokenizer = dyn Fn(&str) -> Vec>; #[cfg(feature = "wasm")] pub enum BuiltinTokenizer { Character = "Character", + Line = "Line", Word = "Word", } @@ -28,6 +30,7 @@ pub enum BuiltinTokenizer { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum BuiltinTokenizer { Character, + Line, Word, } @@ -37,6 +40,7 @@ impl Deref for BuiltinTokenizer { fn deref(&self) -> &Self::Target { match self { BuiltinTokenizer::Character => &character_tokenizer::character_tokenizer, + BuiltinTokenizer::Line => &line_tokenizer::line_tokenizer, BuiltinTokenizer::Word => &word_tokenizer::word_tokenizer, #[cfg(feature = "wasm")] BuiltinTokenizer::__Invalid => panic!("Unexpected tokenizer type"), diff --git a/src/tokenizer/line_tokenizer.rs b/src/tokenizer/line_tokenizer.rs new file mode 100644 index 0000000..ed283c3 --- /dev/null +++ b/src/tokenizer/line_tokenizer.rs @@ -0,0 +1,70 @@ +use super::token::Token; + +/// Splits text into lines, preserving line endings as separate tokens. +/// +/// ## Example +/// +/// ```not_rust +/// "Hello\nWorld!" -> ["Hello", "\n", "World!"] +/// "Line 1\r\nLine 2" -> ["Line 1", "\r\n", "Line 2"] +/// ``` +pub fn line_tokenizer(text: &str) -> Vec> { + let mut result = Vec::new(); + let mut line_start = 0; + + let mut chars = text.char_indices().peekable(); + while let Some((i, c)) = chars.next() { + if c == '\n' { + // Add line content if any + if i > line_start { + result.push(text[line_start..i].into()); + } + // Add newline + result.push("\n".into()); + line_start = i + 1; + } else if c == '\r' && chars.peek() == Some(&(i + 1, '\n')) { + // Handle \r\n + if i > line_start { + result.push(text[line_start..i].into()); + } + chars.next(); // consume \n + result.push("\r\n".into()); + line_start = i + 2; + } + } + + // Add final line if any + if line_start < text.len() { + result.push(text[line_start..].into()); + } + + result +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn test_with_snapshots() { + assert_debug_snapshot!(line_tokenizer("")); + + assert_debug_snapshot!(line_tokenizer("Hello")); + + assert_debug_snapshot!(line_tokenizer("Hello\nWorld")); + + assert_debug_snapshot!(line_tokenizer("Hello\nWorld\n")); + + assert_debug_snapshot!(line_tokenizer("Line 1\r\nLine 2")); + + assert_debug_snapshot!(line_tokenizer("Multi\nLine\nText\nHere")); + + assert_debug_snapshot!(line_tokenizer("\n")); + + assert_debug_snapshot!(line_tokenizer("\n\n")); + + assert_debug_snapshot!(line_tokenizer("Start\n\nEnd")); + } +} diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-2.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-2.snap new file mode 100644 index 0000000..ec1c89e --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-2.snap @@ -0,0 +1,13 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Hello\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Hello", + original: "Hello", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-3.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-3.snap new file mode 100644 index 0000000..c45029a --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-3.snap @@ -0,0 +1,25 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Hello\\nWorld\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Hello", + original: "Hello", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "World", + original: "World", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-4.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-4.snap new file mode 100644 index 0000000..ad8cf81 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-4.snap @@ -0,0 +1,31 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Hello\\nWorld\\n\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Hello", + original: "Hello", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "World", + original: "World", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-5.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-5.snap new file mode 100644 index 0000000..ef1f9cb --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-5.snap @@ -0,0 +1,25 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Line 1\\r\\nLine 2\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Line 1", + original: "Line 1", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\r\n", + original: "\r\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "Line 2", + original: "Line 2", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-6.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-6.snap new file mode 100644 index 0000000..5edb790 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-6.snap @@ -0,0 +1,49 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Multi\\nLine\\nText\\nHere\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Multi", + original: "Multi", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "Line", + original: "Line", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "Text", + original: "Text", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "Here", + original: "Here", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-7.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-7.snap new file mode 100644 index 0000000..8dcdba8 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-7.snap @@ -0,0 +1,13 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"\\n\")" +snapshot_kind: text +--- +[ + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-8.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-8.snap new file mode 100644 index 0000000..8466643 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-8.snap @@ -0,0 +1,19 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"\\n\\n\")" +snapshot_kind: text +--- +[ + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-9.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-9.snap new file mode 100644 index 0000000..9c2be98 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots-9.snap @@ -0,0 +1,31 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"Start\\n\\nEnd\")" +snapshot_kind: text +--- +[ + Token { + normalised: "Start", + original: "Start", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "\n", + original: "\n", + is_left_joinable: true, + is_right_joinable: true, + }, + Token { + normalised: "End", + original: "End", + is_left_joinable: true, + is_right_joinable: true, + }, +] diff --git a/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots.snap b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots.snap new file mode 100644 index 0000000..a525ea3 --- /dev/null +++ b/src/tokenizer/snapshots/reconcile__tokenizer__line_tokenizer__tests__with_snapshots.snap @@ -0,0 +1,6 @@ +--- +source: src/tokenizer/line_tokenizer.rs +expression: "line_tokenizer(\"\")" +snapshot_kind: text +--- +[] From 6d56177ca88fac32c9cdcb0d5d7fe928888a3077 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 13:03:31 +0100 Subject: [PATCH 35/37] Improve --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b7c0c2b..f54d845 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Reconcile: conflict-free 3-way text merging -> [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) but with automatic conflict resolution. - [![Check](https://github.com/schmelczer/reconcile/actions/workflows/check.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/check.yml) [![Publish to GitHub Pages](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml/badge.svg)](https://github.com/schmelczer/reconcile/actions/workflows/gh-pages.yml) -TODO: add links for crates and npm +> [`diff3`](https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html) (or `git merge`) but with automatic conflict resolution. Reconcile is a Rust and JavaScript (through WebAssembly) library for merging text without user intervention. It automatically resolves conflicts that would typically require user action in traditional 3-way merge tools. +Try out the [interactive demo](https://schmelczer.dev/reconcile)! + +TODO: add links for crates and npm + ## Features - **Conflict-free output** - No more git conflict markers in the result From 78fe3fd6fdc4072253e2afc6121b2393ccbaf6e1 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 13:04:56 +0100 Subject: [PATCH 36/37] Lint --- src/operation_transformation/edited_text.rs | 2 +- src/tokenizer/line_tokenizer.rs | 6 +++--- src/types/cursor_position.rs | 1 + src/types/span_with_history.rs | 1 + src/utils/string_builder.rs | 4 +--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index cd0bd24..2241988 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -27,7 +27,6 @@ use crate::{ /// 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. - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Default)] pub struct EditedText<'a, T> @@ -93,6 +92,7 @@ where } #[must_use] + #[allow(clippy::too_many_lines)] pub fn merge(self, other: Self) -> Self { debug_assert_eq!( self.text, other.text, diff --git a/src/tokenizer/line_tokenizer.rs b/src/tokenizer/line_tokenizer.rs index ed283c3..affb762 100644 --- a/src/tokenizer/line_tokenizer.rs +++ b/src/tokenizer/line_tokenizer.rs @@ -11,7 +11,7 @@ use super::token::Token; pub fn line_tokenizer(text: &str) -> Vec> { let mut result = Vec::new(); let mut line_start = 0; - + let mut chars = text.char_indices().peekable(); while let Some((i, c)) = chars.next() { if c == '\n' { @@ -32,12 +32,12 @@ pub fn line_tokenizer(text: &str) -> Vec> { line_start = i + 2; } } - + // Add final line if any if line_start < text.len() { result.push(text[line_start..].into()); } - + result } diff --git a/src/types/cursor_position.rs b/src/types/cursor_position.rs index 9c97d44..bd20065 100644 --- a/src/types/cursor_position.rs +++ b/src/types/cursor_position.rs @@ -5,6 +5,7 @@ use wasm_bindgen::prelude::*; // CursorPosition represents the position of an identifiable cursor in a text // document based on its (UTF-8) character index. +#[allow(clippy::unsafe_derive_deserialize)] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Default)] diff --git a/src/types/span_with_history.rs b/src/types/span_with_history.rs index 4f61317..09f778f 100644 --- a/src/types/span_with_history.rs +++ b/src/types/span_with_history.rs @@ -7,6 +7,7 @@ use crate::types::history::History; /// Wrapper type for `(String, History)` where History describes the origin of /// `text`. +#[allow(clippy::unsafe_derive_deserialize)] #[cfg_attr(feature = "wasm", wasm_bindgen)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq)] diff --git a/src/utils/string_builder.rs b/src/utils/string_builder.rs index d94134a..d77974a 100644 --- a/src/utils/string_builder.rs +++ b/src/utils/string_builder.rs @@ -53,9 +53,7 @@ impl StringBuilder<'_> { /// Returns the currently built buffer and clears it to allow consuming /// the result incrementally. - pub fn take(&mut self) -> String { - std::mem::take(&mut self.buffer) - } + pub fn take(&mut self) -> String { std::mem::take(&mut self.buffer) } /// Get a slice of the remaining original string. The slice starts from /// where the next delete/retain operation would start and is of length From a2119b0f32e8a0fd6864e02ca5090c9d5c0fc26d Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 6 Jul 2025 13:07:26 +0100 Subject: [PATCH 37/37] Improve docs --- src/operation_transformation.rs | 7 ++++--- src/operation_transformation/edited_text.rs | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/operation_transformation.rs b/src/operation_transformation.rs index bc6ff2d..785d69a 100644 --- a/src/operation_transformation.rs +++ b/src/operation_transformation.rs @@ -20,9 +20,10 @@ use crate::{ /// insert the same span with a common prefix, that prefix will only /// be present once in the output. /// -/// Deletes are preserved from both sides. This means that an insert -/// from one side into a deleted span from the other side will result -/// in the removal of the original span but keeping the inserted text. +/// When both sides delete the same span, it will be deleted in the +/// return value. If one side deletes a span and the other side inserts +/// into that span, the inserted text will be present in the return +/// value. /// /// The function supports UTF-8. The arguments are tokenized at the /// granularity of words. diff --git a/src/operation_transformation/edited_text.rs b/src/operation_transformation/edited_text.rs index 2241988..eb8488c 100644 --- a/src/operation_transformation/edited_text.rs +++ b/src/operation_transformation/edited_text.rs @@ -150,7 +150,6 @@ where let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - // Calculate shift using safe casts - preserving original logic let merged_length_signed = isize::try_from(merged_length).unwrap_or(isize::MAX); let seen_left_length_signed = @@ -184,7 +183,6 @@ where let result = operation.merge_operations(&mut last_other_op); if let ref op @ (Operation::Insert { .. } | Operation::Equal { .. }) = result { - // Calculate shift using safe casts - preserving original logic let merged_length_signed = isize::try_from(merged_length).unwrap_or(isize::MAX); let seen_right_length_signed =