From ee6277c13fd4173289e9aed14187605988230a22 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 16 Nov 2025 15:02:31 +0000 Subject: [PATCH] Expose undiff to JS --- reconcile-js/src/index.ts | 35 +++++++++++++++++++++++++++++++---- src/lib.rs | 4 ++-- src/wasm.rs | 34 +++++++++++++++++++++++++--------- tests/test.rs | 8 ++++---- tests/wasm.rs | 4 ++-- 5 files changed, 64 insertions(+), 21 deletions(-) diff --git a/reconcile-js/src/index.ts b/reconcile-js/src/index.ts index 14da817..be7ea8f 100644 --- a/reconcile-js/src/index.ts +++ b/reconcile-js/src/index.ts @@ -4,7 +4,8 @@ import { TextWithCursors as wasmTextWithCursors, SpanWithHistory as wasmSpanWithHistory, reconcileWithHistory as wasmReconcileWithHistory, - getCompactDiff as wasmGetCompactDiff, + diff as wasmDiff, + undiff as wasmUndiff, initSync, } from 'reconcile-text'; @@ -182,7 +183,8 @@ export function reconcile( /** * Generates a compact diff representation between an original and changed text. * - * These can be parsed and unpacked using Rust crate's EditedText::from_changes. + * These can be parsed and unpacked using the `undiff` function or the Rust crate's EditedText::from_diff. + * Cursor positions are omitted from the diff result. * * This function computes the differences between two versions of text and returns * a compact representation of those changes. @@ -192,7 +194,7 @@ export function reconcile( * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. * @returns An array representing the compact diff, with inserts as strings and deletes as negative integers. */ -export function getCompactDiff( +export function diff( original: string, changed: string | TextWithOptionalCursors, tokenizer: BuiltinTokenizer = 'Word' @@ -205,13 +207,38 @@ export function getCompactDiff( const changedWasm = toWasmTextWithCursors(changed); - const result = wasmGetCompactDiff(original, changedWasm, tokenizer); + const result = wasmDiff(original, changedWasm, tokenizer); changedWasm.free(); return result; } +/** + * Applies a compact diff to an original text to reconstruct the changed version. + * + * This function takes an original text and a compact diff representation (as produced + * by the `diff` function) and reconstructs the modified text. + * + * @param original - The original/base version of the text + * @param diff - The compact diff array representing changes (inserts as strings, deletes as negative integers) + * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. + * @returns The reconstructed changed text as a string. + */ +export function undiff( + original: string, + diff: Array, + tokenizer: BuiltinTokenizer = 'Word' +): string { + init(); + + if (!BUILTIN_TOKENIZERS.includes(tokenizer)) { + throw new Error(UNSUPPORTED_TOKENIZER_ERROR); + } + + return wasmUndiff(original, diff, tokenizer); +} + /** * Merges three versions of text and returns detailed provenance information. * diff --git a/src/lib.rs b/src/lib.rs index 990febe..2119bea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,7 +172,7 @@ //! &changes.into() //! ); //! -//! let serialized = serde_yaml::to_string(&result.to_changes()).unwrap(); +//! let serialized = serde_yaml::to_string(&result.to_diff()).unwrap(); //! assert_eq!( //! serialized, //! concat!( @@ -183,7 +183,7 @@ //! ); //! //! let deserialized = serde_yaml::from_str(&serialized).unwrap(); -//! let reconstructed = EditedText::from_changes( +//! let reconstructed = EditedText::from_diff( //! original, //! deserialized, //! &*BuiltinTokenizer::Word diff --git a/src/wasm.rs b/src/wasm.rs index c92b176..54c256e 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -3,7 +3,7 @@ use core::str; use wasm_bindgen::prelude::*; -use crate::{BuiltinTokenizer, CursorPosition, SpanWithHistory, TextWithCursors}; +use crate::{BuiltinTokenizer, CursorPosition, EditedText, SpanWithHistory, TextWithCursors}; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc<'_> = wee_alloc::WeeAlloc::INIT; @@ -32,6 +32,7 @@ pub fn reconcile_with_history( tokenizer: BuiltinTokenizer, ) -> TextWithCursorsAndHistory { set_panic_hook(); + let reconciled = crate::reconcile(parent, left, right, &*tokenizer); let text_with_cursors = reconciled.apply(); @@ -80,22 +81,37 @@ pub fn generic_reconcile( /// WASM wrapper around getting a compact diff representation of two texts as a /// list of numbers and strings. -#[wasm_bindgen(js_name = getCompactDiff)] +#[wasm_bindgen(js_name = diff)] #[must_use] -pub fn get_compact_diff( - parent: &str, - changed: &TextWithCursors, - tokenizer: BuiltinTokenizer, -) -> Vec { +pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer) -> Vec { set_panic_hook(); - let edited_text = crate::EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer); + + let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer); edited_text - .to_changes() + .to_diff() .into_iter() .map(std::convert::Into::into) .collect() } +/// Inverse of `diff`, applies a compact diff representation to a parent text +#[wasm_bindgen(js_name = undiff)] +#[must_use] +pub fn undiff(parent: &str, diff: Vec, tokenizer: BuiltinTokenizer) -> String { + set_panic_hook(); + + EditedText::from_diff( + parent, + diff.into_iter() + .map(|js_value| js_value.try_into()) + .collect::>() + .expect("Invalid diff format"), + &*tokenizer, + ) + .apply() + .text() +} + fn set_panic_hook() { // https://github.com/rustwasm/console_error_panic_hook#readme #[cfg(feature = "console_error_panic_hook")] diff --git a/tests/test.rs b/tests/test.rs index ab4100a..2b14b86 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -50,16 +50,16 @@ fn test_document_one_way_with_serialisation() { ); let serialised_left = - serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_changes()).unwrap()) + serde_yaml::from_str(&serde_yaml::to_string(&left_operations.to_diff()).unwrap()) .unwrap(); let serialised_right = - serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_changes()).unwrap()) + serde_yaml::from_str(&serde_yaml::to_string(&right_operations.to_diff()).unwrap()) .unwrap(); let restored_left_operations = - EditedText::from_changes(&parent, serialised_left, &*BuiltinTokenizer::Word); + EditedText::from_diff(&parent, serialised_left, &*BuiltinTokenizer::Word); let restored_right_operations = - EditedText::from_changes(&parent, serialised_right, &*BuiltinTokenizer::Word); + EditedText::from_diff(&parent, serialised_right, &*BuiltinTokenizer::Word); doc.assert_eq_without_cursors( &restored_left_operations diff --git a/tests/wasm.rs b/tests/wasm.rs index 9c2a657..304ee6e 100644 --- a/tests/wasm.rs +++ b/tests/wasm.rs @@ -56,11 +56,11 @@ fn test_merge_binary() { } #[wasm_bindgen_test] // JsValue isn't supported outside of wasm -fn test_get_compact_diff() { +fn test_diff() { let parent = "hello "; let changed = "world"; - let result = get_compact_diff(parent, &changed.into(), BuiltinTokenizer::Word); + let result = diff(parent, &changed.into(), BuiltinTokenizer::Word); assert_eq!(result.len(), 2); let first: i64 = result[0].clone().try_into().unwrap();