reconcile/reconcile-js/src/index.ts
2025-07-07 22:30:33 +01:00

170 lines
4.8 KiB
TypeScript

import wasmInit, {
CursorPosition as wasmCursorPosition,
reconcile as wasmReconcile,
TextWithCursors as wasmTextWithCursors,
SpanWithHistory as wasmSpanWithHistory,
BuiltinTokenizer,
reconcileWithHistory as wasmReconcileWithHistory,
History,
initSync,
} from 'reconcile';
import wasm from 'reconcile/reconcile_bg.wasm';
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[];
}
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: SpanWithHistory[];
}
export interface SpanWithHistory {
/** Span of text associated with the historical opearion */
text: string;
/** Origin of the `text` span */
history: History;
}
export type Tokenizer = 'Line' | 'Word' | 'Character';
const TOKENIZERS = ['Line', 'Word', 'Character'];
let isInitialised = false;
const UNSUPPORTED_TOKENIZER_ERROR = `Unsupported tokenizer. Only ${TOKENIZERS.join(
', '
)} are supported.`;
/**
* 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 {
init();
if (!TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
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 {
init();
if (!TOKENIZERS.includes(tokenizer)) {
throw new Error(UNSUPPORTED_TOKENIZER_ERROR);
}
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(toSpanWithHistory);
result.free();
return {
...jsResult,
history,
};
}
function init() {
if (isInitialised) {
return;
}
initSync({ module: (wasm as any).default });
isInitialised = true;
}
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.characterIndex(),
};
}
function toSpanWithHistory(textWithHistory: wasmSpanWithHistory): SpanWithHistory {
return {
text: textWithHistory.text(),
history: textWithHistory.history(),
};
}