Expose undiff to JS

This commit is contained in:
Andras Schmelczer 2025-11-16 15:02:31 +00:00
parent c9c2b775a8
commit ee6277c13f
5 changed files with 64 additions and 21 deletions

View file

@ -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<number | string>,
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.
*

View file

@ -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

View file

@ -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<JsValue> {
pub fn diff(parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer) -> Vec<JsValue> {
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<JsValue>, tokenizer: BuiltinTokenizer) -> String {
set_panic_hook();
EditedText::from_diff(
parent,
diff.into_iter()
.map(|js_value| js_value.try_into())
.collect::<Result<_, _>>()
.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")]

View file

@ -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

View file

@ -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();