Merge pull request #5 from schmelczer/asch/better-api
Prepare to publish
This commit is contained in:
commit
752f685a7f
21 changed files with 12171 additions and 5421 deletions
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
|
|
@ -5,17 +5,17 @@
|
|||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories: ["**"]
|
||||
- package-ecosystem: 'cargo'
|
||||
directories: ['**']
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directories: ["**"]
|
||||
- package-ecosystem: 'github-actions'
|
||||
directories: ['**']
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: 'daily'
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directories: ["/reconcile-js"]
|
||||
- package-ecosystem: 'npm'
|
||||
directories: ['/reconcile-js', '/examples/website']
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: 'daily'
|
||||
|
|
|
|||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 90,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf",
|
||||
"importOrder": ["^[./]", ".*", ".scss$"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -9,4 +9,4 @@
|
|||
"rust-analyzer.cargo.features": [
|
||||
"all"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ console_error_panic_hook = [ "dep:console_error_panic_hook" ]
|
|||
insta = "1.42.2"
|
||||
pretty_assertions = "1.4.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_yaml ="0.9.34"
|
||||
serde_yaml = "0.9.34"
|
||||
test-case = "3.3.1"
|
||||
wasm-bindgen-test = "0.3.49"
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ wasm-bindgen-test = "0.3.49"
|
|||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 3
|
||||
strip="symbols"
|
||||
strip = "symbols"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ['-O4', '--enable-bulk-memory']
|
||||
|
|
@ -84,7 +84,7 @@ large_stack_arrays = { level = "allow", priority = 1 } # https://github.com/rust
|
|||
|
||||
# Silly lints
|
||||
implicit_return = { level = "allow", priority = 1 }
|
||||
question_mark_used = { level = "allow", priority = 1 }
|
||||
question_mark_used = { level = "allow", priority = 1 }
|
||||
struct_field_names = { level = "allow", priority = 1 }
|
||||
single_char_lifetime_names = { level = "allow", priority = 1 }
|
||||
single_call_fn = { level = "allow", priority = 1 }
|
||||
|
|
|
|||
|
|
@ -1,161 +1,213 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
||||
/>
|
||||
<meta property="og:title" content="3-Way Text Merge" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://github.com/schmelczer/reconcile"
|
||||
/>
|
||||
<meta property="og:image" content="/favicon.ico" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<title>3-Way Text Merge</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
||||
/>
|
||||
<meta property="og:title" content="3-Way Text Merge" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://github.com/schmelczer/reconcile" />
|
||||
<meta property="og:image" content="/favicon.ico" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<title>3-Way Text Merge</title>
|
||||
<link inline inline-asset="index.css" inline-asset-delete />
|
||||
</head>
|
||||
<body>
|
||||
<div class="background"></div>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<header>
|
||||
<h1>3-Way Text Merge</h1>
|
||||
<p>
|
||||
The
|
||||
<a
|
||||
href="https://github.com/schmelczer/reconcile"
|
||||
target="_blank"
|
||||
>reconcile</a
|
||||
>
|
||||
solves a fundamental challenge in collaborative editing:
|
||||
what happens when multiple people edit the same text
|
||||
simultaneously?
|
||||
<code
|
||||
>reconcile(parent: str, left: str, right: str) ->
|
||||
str</code
|
||||
>
|
||||
takes conflicting concurrent edits and intelligently merges
|
||||
them into a unified result. Beyond basic conflict
|
||||
resolution, it offers sophisticated merging heuristics,
|
||||
flexible tokenization options, and cursor position tracking.
|
||||
</p>
|
||||
<p>
|
||||
The algorithm begins with your chosen tokenizer, then
|
||||
applies Myers' diff algorithm to compare the original text
|
||||
with both conflicting versions. These diffs undergo
|
||||
transformation to preserve meaningful change sequences,
|
||||
before a final merge strategy—inspired by Operational
|
||||
Transformation (OT)—reconciles all conflicting modifications
|
||||
without losing any edits.
|
||||
</p>
|
||||
<p>
|
||||
For more details, see the
|
||||
<a
|
||||
href="https://github.com/schmelczer/reconcile"
|
||||
target="_blank"
|
||||
>README</a
|
||||
>.
|
||||
</p>
|
||||
</header>
|
||||
<div class="scroll-container">
|
||||
<div class="page-wrapper">
|
||||
<header>
|
||||
<h1>Reconcile: automated 3-way text merge</h1>
|
||||
<p>
|
||||
The
|
||||
<a
|
||||
href="https://github.com/schmelczer/reconcile"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>reconcile</a
|
||||
>
|
||||
library solves a fundamental challenge in collaborative editing: what happens
|
||||
when multiple users edit the same text simultaneously but we can only capture
|
||||
the end result, not the intermediary edits? Essentially, it's
|
||||
<a
|
||||
href="https://www.gnu.org/software/diffutils/manual/html_node/Invoking-diff3.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>diff3</a
|
||||
>
|
||||
(or <code>git merge</code>) but with automatic conflict resolution.
|
||||
</p>
|
||||
<p>
|
||||
The
|
||||
<code>reconcile(parent: str, left: str, right: str) -> str</code>
|
||||
takes conflicting concurrent edits and intelligently merges them into a
|
||||
unified result. Beyond basic conflict resolution, it offers sophisticated
|
||||
merging heuristics, flexible tokenization options, and cursor position
|
||||
tracking.
|
||||
</p>
|
||||
<p>
|
||||
The algorithm begins with your chosen tokenizer, then applies Myers' diff
|
||||
algorithm to compare the original text with both conflicting versions. These
|
||||
diffs undergo transformation to preserve meaningful change sequences, before a
|
||||
final merge strategy—inspired by Operational Transformation reconciles all
|
||||
conflicting modifications without losing any edits.
|
||||
</p>
|
||||
<p>
|
||||
For more details, see the
|
||||
<a href="https://github.com/schmelczer/reconcile" target="_blank">README</a>.
|
||||
</p>
|
||||
|
||||
<main>
|
||||
<div class="text-area-card diamond-parent">
|
||||
<label
|
||||
for="original"
|
||||
title="The text document's content before any concurrent edits occurred."
|
||||
>Original</label
|
||||
>
|
||||
<textarea id="original" name="original"></textarea>
|
||||
<p>
|
||||
Use the tokenization options below to experiment with different strategies.
|
||||
The library supports user-defined tokenizers as well.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="tokenizer-selector">
|
||||
<div class="radio-group" role="radiogroup" aria-label="Tokenization strategy">
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="tokenizer"
|
||||
value="Character"
|
||||
id="tokenizer-character"
|
||||
/>
|
||||
<span class="radio-custom" aria-hidden="true"></span>
|
||||
<div class="radio-content">
|
||||
<span class="radio-label">Character</span>
|
||||
<span class="radio-description">Split by individual characters</span>
|
||||
</div>
|
||||
|
||||
<div class="text-area-card diamond-left">
|
||||
<label
|
||||
for="left"
|
||||
title="Colour-coded tokens mark the origin of each token in the result. This text box is marked with the colour green."
|
||||
>
|
||||
First concurrent edit
|
||||
<div class="box Left"></div>
|
||||
</label>
|
||||
<textarea id="left" name="left"></textarea>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="tokenizer"
|
||||
value="Word"
|
||||
id="tokenizer-word"
|
||||
checked
|
||||
/>
|
||||
<span class="radio-custom" aria-hidden="true"></span>
|
||||
<div class="radio-content">
|
||||
<span class="radio-label">Word</span>
|
||||
<span class="radio-description">Split by words (default)</span>
|
||||
</div>
|
||||
|
||||
<div class="text-area-card diamond-right">
|
||||
<label
|
||||
for="right"
|
||||
title="Colour-coded tokens mark the origin of each token in the result. This text box is marked with the colour blue."
|
||||
>
|
||||
Second concurrent edit
|
||||
<div class="box Right"></div>
|
||||
</label>
|
||||
<textarea id="right" name="right"></textarea>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="tokenizer" value="Line" id="tokenizer-line" />
|
||||
<span class="radio-custom" aria-hidden="true"></span>
|
||||
<div class="radio-content">
|
||||
<span class="radio-label">Line</span>
|
||||
<span class="radio-description"
|
||||
>Split by lines similarly to <code>git merge</code></span
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="text-area-card diamond-result">
|
||||
<label
|
||||
title="Read-only. Change the above text boxes to change the content of this box."
|
||||
>
|
||||
Deconflicted result
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M10 10l-6 6v4h4l6 -6m1.99 -1.99l2.504 -2.504a2.828 2.828 0 1 0 -4 -4l-2.5 2.5"
|
||||
/>
|
||||
<path d="M13.5 6.5l4 4" />
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
</label>
|
||||
<div id="merged"></div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="text-area-card diamond-parent">
|
||||
<label
|
||||
for="original"
|
||||
title="The text document's content before any concurrent edits occurred."
|
||||
>Original</label
|
||||
>
|
||||
<textarea id="original" name="original"></textarea>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>2025 Andras Schmelczer</p>
|
||||
<a
|
||||
href="https://github.com/schmelczer/reconcile"
|
||||
class="github-link"
|
||||
aria-label="GitHub repository"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<div class="text-area-card diamond-left">
|
||||
<label
|
||||
for="left"
|
||||
title="Colour-coded tokens mark the origin of each token in the result. This text box is marked with the colour green."
|
||||
>
|
||||
First concurrent edit
|
||||
<div class="box Left"></div>
|
||||
</label>
|
||||
<textarea id="left" name="left"></textarea>
|
||||
</div>
|
||||
|
||||
<script type="module" src="script.js"></script>
|
||||
</body>
|
||||
<div class="text-area-card diamond-right">
|
||||
<label
|
||||
for="right"
|
||||
title="Colour-coded tokens mark the origin of each token in the result. This text box is marked with the colour blue."
|
||||
>
|
||||
Second concurrent edit
|
||||
<div class="box Right"></div>
|
||||
</label>
|
||||
<textarea id="right" name="right"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-area-card diamond-result">
|
||||
<label
|
||||
for="merged"
|
||||
title="Read-only. Change the above text boxes to change the content of this box."
|
||||
>
|
||||
Deconflicted result
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path
|
||||
d="M10 10l-6 6v4h4l6 -6m1.99 -1.99l2.504 -2.504a2.828 2.828 0 1 0 -4 -4l-2.5 2.5"
|
||||
></path>
|
||||
<path d="M13.5 6.5l4 4"></path>
|
||||
<path d="M3 3l18 18"></path>
|
||||
</svg>
|
||||
</label>
|
||||
<div id="merged" role="textbox" aria-readonly="true" aria-live="polite"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>2025 Andras Schmelczer</p>
|
||||
<a
|
||||
href="https://github.com/schmelczer/reconcile"
|
||||
class="github-link"
|
||||
aria-label="GitHub repository"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<noscript>JavaScript is required for this website to function properly.</noscript>
|
||||
|
||||
<script inline inline-asset="index.js" inline-asset-delete></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
88
examples/website/index.ts
Normal file
88
examples/website/index.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { init, reconcileWithHistory } from 'reconcile';
|
||||
import type { Tokenizer } from 'reconcile';
|
||||
import './style.scss';
|
||||
|
||||
const originalTextArea = document.getElementById('original') as HTMLTextAreaElement;
|
||||
const leftTextArea = document.getElementById('left') as HTMLTextAreaElement;
|
||||
const rightTextArea = document.getElementById('right') as HTMLTextAreaElement;
|
||||
const mergedTextArea = document.getElementById('merged') as HTMLDivElement;
|
||||
const tokenizerRadios = document.querySelectorAll(
|
||||
'input[name="tokenizer"]'
|
||||
) as NodeListOf<HTMLInputElement>;
|
||||
|
||||
const sampleText = `The \`reconcile\` Rust library is embedded on this page as a WASM module and powers these text boxes. Experiment with changing the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to see competing changes get merged in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenization strategy, for example, deciding how casing or whitespace is taken into account.`;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await init();
|
||||
|
||||
originalTextArea.addEventListener('input', updateMergedText);
|
||||
leftTextArea.addEventListener('input', updateMergedText);
|
||||
rightTextArea.addEventListener('input', updateMergedText);
|
||||
window.addEventListener('resize', resizeTextAreas);
|
||||
|
||||
tokenizerRadios.forEach((radio) => {
|
||||
radio.addEventListener('change', updateMergedText);
|
||||
});
|
||||
|
||||
loadSample();
|
||||
updateMergedText();
|
||||
focusTextArea(leftTextArea);
|
||||
}
|
||||
|
||||
function loadSample(): void {
|
||||
originalTextArea.value = sampleText;
|
||||
leftTextArea.value =
|
||||
sampleText.replace('color', 'colour') +
|
||||
" Check out what's the most complex conflict you can come up with!";
|
||||
rightTextArea.value = sampleText
|
||||
.replace(', for example,', ' such as')
|
||||
.replace('WASM', 'WebAssembly');
|
||||
}
|
||||
|
||||
function updateMergedText(): void {
|
||||
resizeTextAreas();
|
||||
|
||||
const original = originalTextArea.value;
|
||||
const left = leftTextArea.value;
|
||||
const right = rightTextArea.value;
|
||||
|
||||
const selectedTokenizer = getSelectedTokenizer();
|
||||
|
||||
const results = reconcileWithHistory(original, left, right, selectedTokenizer);
|
||||
|
||||
mergedTextArea.innerHTML = '';
|
||||
|
||||
for (const { text, history } of results.history) {
|
||||
const span = document.createElement('span');
|
||||
span.className = history;
|
||||
span.textContent = text;
|
||||
mergedTextArea.appendChild(span);
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedTokenizer(): Tokenizer {
|
||||
const selectedRadio = Array.from(tokenizerRadios).find((radio) => radio.checked);
|
||||
return selectedRadio?.value as Tokenizer;
|
||||
}
|
||||
|
||||
function resizeTextAreas(): void {
|
||||
// Only auto-resize if field-sizing CSS property is not supported, like in Safari as of now
|
||||
if (!CSS.supports('field-sizing', 'content')) {
|
||||
autoResize(originalTextArea);
|
||||
autoResize(leftTextArea);
|
||||
autoResize(rightTextArea);
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize(textarea: HTMLTextAreaElement): void {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = textarea.scrollHeight + 'px';
|
||||
}
|
||||
|
||||
function focusTextArea(textarea: HTMLTextAreaElement): void {
|
||||
textarea.focus();
|
||||
textarea.selectionStart = textarea.value.length;
|
||||
textarea.selectionEnd = textarea.value.length;
|
||||
}
|
||||
|
||||
main();
|
||||
5555
examples/website/package-lock.json
generated
Normal file
5555
examples/website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
48
examples/website/package.json
Normal file
48
examples/website/package.json
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"name": "portfolio",
|
||||
"description": "An easily configurable timeline of projects.",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "webpack serve --open --mode development",
|
||||
"format": "prettier --write \"src/**/*.(ts|scss|json|html)\"",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/schmelczer/schmelczer.github.io.git"
|
||||
},
|
||||
"keywords": [
|
||||
"CV",
|
||||
"curriculum",
|
||||
"vitae",
|
||||
"portfolio",
|
||||
"resumé"
|
||||
],
|
||||
"author": "Andras Schmelczer",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/schmelczer/schmelczer.github.io/issues"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"homepage": "https://github.com/schmelczer/schmelczer.github.io#readme",
|
||||
"devDependencies": {
|
||||
"reconcile": "file:../../reconcile-js",
|
||||
"css-loader": "^7.1.2",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"inline-source-webpack-plugin": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"prettier": "^3.6.2",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.89.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"terser-webpack-plugin": "^5.3.14",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { init, reconcileWithHistory } from "./dist/index.js";
|
||||
|
||||
const originalTextArea = document.getElementById("original");
|
||||
const leftTextArea = document.getElementById("left");
|
||||
const rightTextArea = document.getElementById("right");
|
||||
const mergedTextArea = document.getElementById("merged");
|
||||
|
||||
const sampleText = `The \`reconcile\` Rust library is embedded on this page a WASM module and it powers these text boxes. Experiment with the "Original", "First concurrent edit", and "Second concurrent edit" text boxes to watch competing changes merge in real-time within the "Deconflicted result" box. Here, you will see color-coded tokens marking the origin of each token, including ones that got deleted. The result highly depends on the tokenization strategy, for example, deciding how casing or white-spacing is taken into account.`;
|
||||
|
||||
async function main() {
|
||||
await init();
|
||||
|
||||
originalTextArea.addEventListener("input", updateMergedText);
|
||||
leftTextArea.addEventListener("input", updateMergedText);
|
||||
rightTextArea.addEventListener("input", updateMergedText);
|
||||
window.addEventListener("resize", resizeTextAreas);
|
||||
|
||||
loadSample();
|
||||
updateMergedText();
|
||||
focusTextArea(leftTextArea);
|
||||
}
|
||||
|
||||
function loadSample() {
|
||||
originalTextArea.value = sampleText;
|
||||
leftTextArea.value =
|
||||
sampleText.replace("color", "colour") +
|
||||
" Check out what's the most complex conflict you can come up with!";
|
||||
rightTextArea.value = sampleText
|
||||
.replace(", for example,", " such as")
|
||||
.replace("WASM", "WebAssembly");
|
||||
}
|
||||
|
||||
function updateMergedText() {
|
||||
resizeTextAreas();
|
||||
|
||||
const original = originalTextArea.value;
|
||||
const left = leftTextArea.value;
|
||||
const right = rightTextArea.value;
|
||||
|
||||
const results = reconcileWithHistory(original, left, right);
|
||||
|
||||
mergedTextArea.innerHTML = "";
|
||||
|
||||
for (const {text, history} of results.history) {
|
||||
const span = document.createElement("span");
|
||||
span.className = history;
|
||||
span.textContent = text;
|
||||
mergedTextArea.appendChild(span);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeTextAreas() {
|
||||
if (!CSS.supports("field-sizing", "content")) {
|
||||
autoResize(originalTextArea);
|
||||
autoResize(leftTextArea);
|
||||
autoResize(rightTextArea);
|
||||
}
|
||||
}
|
||||
|
||||
function autoResize(textarea) {
|
||||
textarea.style.height = "auto";
|
||||
textarea.style.height = textarea.scrollHeight + "px";
|
||||
}
|
||||
|
||||
function focusTextArea(textarea) {
|
||||
textarea.focus();
|
||||
textarea.selectionStart = textarea.value.length;
|
||||
textarea.selectionEnd = textarea.value.length;
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
color: #23272f;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.background {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ef 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
header > h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #2451a6;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p,
|
||||
p * {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
header > p {
|
||||
color: #5a6272;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
header > p:not(:first-of-type) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-rows: min-content;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.diamond-parent {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.diamond-left {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.diamond-right {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.diamond-result {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.diamond-result label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.diamond-result svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.text-area-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 12px 0 rgba(36, 81, 166, 0.06);
|
||||
padding: 18px 20px 16px 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #2451a6;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 1ch;
|
||||
height: 1ch;
|
||||
border-radius: 50%;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: #23272f;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
outline: none;
|
||||
margin-bottom: 0;
|
||||
field-sizing: content; /* Doesn't work in Safari yet */
|
||||
}
|
||||
|
||||
#merged {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.Unchanged {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.AddedFromLeft,
|
||||
.RemovedFromLeft {
|
||||
user-select: text;
|
||||
background: #12d197;
|
||||
}
|
||||
|
||||
.Right,
|
||||
.AddedFromRight,
|
||||
.RemovedFromRight {
|
||||
user-select: text;
|
||||
background: #85bff7;
|
||||
}
|
||||
|
||||
.RemovedFromLeft,
|
||||
.RemovedFromRight {
|
||||
user-select: none;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
header {
|
||||
padding: 32px 18px 0 18px;
|
||||
}
|
||||
|
||||
header > h1 {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
header > p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
}
|
||||
|
||||
.diamond-parent {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.diamond-left {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.diamond-right {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.diamond-result {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #5a6272;
|
||||
}
|
||||
|
||||
.github-link > svg {
|
||||
position: absolute;
|
||||
color: #5a6272;
|
||||
top: 50%;
|
||||
right: 36px;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.github-link > svg:hover {
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%) scale(1.15);
|
||||
}
|
||||
359
examples/website/style.scss
Normal file
359
examples/website/style.scss
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
color: #23272f;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.background {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ef 100%);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 100%;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
header > h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #2451a6;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p,
|
||||
p * {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
header > p {
|
||||
color: #5a6272;
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
header > p:not(:first-of-type) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-rows: min-content min-content min-content;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.tokenizer-selector {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(36, 81, 166, 0.08);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid transparent;
|
||||
min-width: 180px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radio-option:hover {
|
||||
box-shadow: 0 4px 16px rgba(36, 81, 166, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.radio-option:has(input:checked) {
|
||||
background: #f0f7ff;
|
||||
border-color: #2451a6;
|
||||
box-shadow: 0 4px 16px rgba(36, 81, 166, 0.16);
|
||||
}
|
||||
|
||||
.radio-option input[type='radio'] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.radio-custom {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #d1d5db;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.radio-option:has(input:checked) .radio-custom {
|
||||
border-color: #2451a6;
|
||||
background: #2451a6;
|
||||
}
|
||||
|
||||
.radio-custom::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.radio-option:has(input:checked) .radio-custom::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.radio-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
font-weight: 600;
|
||||
color: #2451a6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.radio-description {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.diamond-parent {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.diamond-left {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.diamond-right {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.diamond-result {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
.diamond-result label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.diamond-result svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.text-area-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 12px 0 rgba(36, 81, 166, 0.06);
|
||||
padding: 18px 20px 16px 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #2451a6;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 1ch;
|
||||
height: 1ch;
|
||||
border-radius: 50%;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
color: #23272f;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
outline: none;
|
||||
margin-bottom: 0;
|
||||
field-sizing: content; /* Doesn't work in Safari yet */
|
||||
}
|
||||
|
||||
#merged {
|
||||
width: 100%;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.Unchanged {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.Left,
|
||||
.AddedFromLeft,
|
||||
.RemovedFromLeft {
|
||||
user-select: text;
|
||||
background: #12d197;
|
||||
}
|
||||
|
||||
.Right,
|
||||
.AddedFromRight,
|
||||
.RemovedFromRight {
|
||||
user-select: text;
|
||||
background: #85bff7;
|
||||
}
|
||||
|
||||
.RemovedFromLeft,
|
||||
.RemovedFromRight {
|
||||
user-select: none;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
header {
|
||||
padding: 32px 18px 0 18px;
|
||||
}
|
||||
|
||||
header > h1 {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
header > p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
}
|
||||
|
||||
.tokenizer-selector {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.diamond-parent {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.diamond-left {
|
||||
grid-column: 1;
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
.diamond-right {
|
||||
grid-column: 1;
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.diamond-result {
|
||||
grid-column: 1;
|
||||
grid-row: 5;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #5a6272;
|
||||
}
|
||||
|
||||
.github-link > svg {
|
||||
position: absolute;
|
||||
color: #5a6272;
|
||||
top: 50%;
|
||||
right: 36px;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.github-link > svg:hover {
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%) scale(1.15);
|
||||
}
|
||||
19
examples/website/tsconfig.json
Normal file
19
examples/website/tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationDir": "./dist/types",
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"inlineSourceMap": true
|
||||
},
|
||||
"exclude": [
|
||||
"./dist"
|
||||
]
|
||||
}
|
||||
81
examples/website/webpack.config.js
Normal file
81
examples/website/webpack.config.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const InlineSourceWebpackPlugin = require('inline-source-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
module.exports = (_env, argv) => ({
|
||||
devtool: argv.mode === 'development' ? 'inline-source-map' : false,
|
||||
entry: {
|
||||
index: './index.ts',
|
||||
},
|
||||
devServer: {
|
||||
allowedHosts: 'all',
|
||||
},
|
||||
watchOptions: {
|
||||
ignored: '**/node_modules',
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
terserOptions: {
|
||||
module: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
performance: {
|
||||
assetFilter: (f) => !/\.(webm|mp4|pdf)$/.test(f),
|
||||
maxEntrypointSize: 100000,
|
||||
maxAssetSize: 512000,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './index.html',
|
||||
}),
|
||||
new MiniCssExtractPlugin(),
|
||||
argv.mode === 'production'
|
||||
? new InlineSourceWebpackPlugin({
|
||||
compress: true,
|
||||
})
|
||||
: null,
|
||||
].filter(Boolean),
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.svg$/i,
|
||||
use: 'svg-inline-loader',
|
||||
},
|
||||
{
|
||||
test: /\.scss$/i,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'resolve-url-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
sourceMap: true, // required by resolve-url-loader
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: 'ts-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [
|
||||
'.ts',
|
||||
'.js', // required for development
|
||||
],
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
publicPath: '',
|
||||
},
|
||||
});
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "reconcile",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
10645
reconcile-js/package-lock.json
generated
10645
reconcile-js/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,24 +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"
|
||||
}
|
||||
"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": "^30.0.0",
|
||||
"jest": "^30.0.4",
|
||||
"reconcile": "file:../pkg",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"tslib": "2.8.1",
|
||||
"typescript": "5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,9 +5,5 @@ set -e
|
|||
wasm-pack build --target web --features wasm,wee_alloc
|
||||
cd reconcile-js
|
||||
npm run build
|
||||
mkdir -p ../examples/website/dist
|
||||
cp -R dist/index.js ../examples/website/dist/index.js
|
||||
|
||||
cd ../examples/website
|
||||
|
||||
python3 -m http.server $1 --bind 0.0.0.0
|
||||
npm run start
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@
|
|||
//! .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
|
||||
//! false, // don't allow joining token with the preceding one
|
||||
//! false, // don't allow joining token with the following one
|
||||
//! ))
|
||||
//! .collect::<Vec<_>>()
|
||||
//! };
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
//! 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
|
||||
//! > By setting the joinability to `false`, longer runs of inserts will be
|
||||
//! > interleaved like LRLRLR instead of LLLRRR.
|
||||
//!
|
||||
//! ## Cursors and selection ranges
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ use std::fmt::Debug;
|
|||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A token is a string that has been normalised in some way.
|
||||
/// A token is a string that has been normalized in some way.
|
||||
///
|
||||
/// A token consists of the normalised form is used for comparison, and the
|
||||
/// A token consists of the normalized form is used for comparison, and the
|
||||
/// original form used for subsequently applying `Operation`-s to a text
|
||||
/// document.
|
||||
///
|
||||
|
|
@ -16,8 +16,8 @@ pub struct Token<T>
|
|||
where
|
||||
T: PartialEq + Clone + Debug,
|
||||
{
|
||||
/// The normalised form of the token used deriving the diff.
|
||||
normalised: T,
|
||||
/// The normalized form of the token used deriving the diff.
|
||||
normalized: T,
|
||||
|
||||
/// The original string, that should be inserted or deleted in the document.
|
||||
original: String,
|
||||
|
|
@ -29,7 +29,7 @@ where
|
|||
pub is_right_joinable: bool,
|
||||
}
|
||||
|
||||
/// Trivial implementation of Token when the normalised form is the same as the
|
||||
/// Trivial implementation of Token when the normalized form is the same as the
|
||||
/// original string.
|
||||
impl From<&str> for Token<String> {
|
||||
fn from(text: &str) -> Self { Token::new(text.to_owned(), text.to_owned(), true, true) }
|
||||
|
|
@ -40,13 +40,13 @@ where
|
|||
T: PartialEq + Clone + Debug,
|
||||
{
|
||||
pub fn new(
|
||||
normalised: T,
|
||||
normalized: T,
|
||||
original: String,
|
||||
is_left_joinable: bool,
|
||||
is_right_joinable: bool,
|
||||
) -> Self {
|
||||
Token {
|
||||
normalised,
|
||||
normalized,
|
||||
original,
|
||||
is_left_joinable,
|
||||
is_right_joinable,
|
||||
|
|
@ -55,9 +55,9 @@ where
|
|||
|
||||
pub fn original(&self) -> &str { &self.original }
|
||||
|
||||
pub fn set_normalised(&mut self, normalised: T) { self.normalised = normalised; }
|
||||
pub fn set_normalized(&mut self, normalized: T) { self.normalized = normalized; }
|
||||
|
||||
pub fn normalised(&self) -> &T { &self.normalised }
|
||||
pub fn normalized(&self) -> &T { &self.normalized }
|
||||
|
||||
pub fn get_original_length(&self) -> usize { self.original.chars().count() }
|
||||
}
|
||||
|
|
@ -66,5 +66,5 @@ impl<T> PartialEq for Token<T>
|
|||
where
|
||||
T: PartialEq + Clone + Debug,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool { self.normalised == other.normalised }
|
||||
fn eq(&self, other: &Self) -> bool { self.normalized == other.normalized }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use super::token::Token;
|
||||
|
||||
/// Splits text on word boundaries creating tokens of alternating words and
|
||||
/// whitespaces with the whitespaces getting unique IDs.
|
||||
/// Splits text on word boundaries, creating tokens of alternating words and
|
||||
/// whitespace with the whitespace getting unique IDs.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
|
|
@ -9,7 +9,7 @@ use super::token::Token;
|
|||
/// "Hi there!" -> ["Hi", " ", "there!"]
|
||||
/// ```
|
||||
pub fn word_tokenizer(text: &str) -> Vec<Token<String>> {
|
||||
let mut result: Vec<Token<String>> = Vec::new();
|
||||
let mut result = Vec::new();
|
||||
|
||||
let mut previous_boundary_index = 0;
|
||||
let mut previous_char_is_whitespace = text.chars().next().is_none_or(char::is_whitespace);
|
||||
|
|
@ -32,10 +32,11 @@ pub fn word_tokenizer(text: &str) -> Vec<Token<String>> {
|
|||
return result;
|
||||
}
|
||||
|
||||
// normalize whitespace tokens by concatenating with the following token
|
||||
for i in 0..result.len() - 1 {
|
||||
if result[i].original().chars().all(char::is_whitespace) {
|
||||
let normalised = result[i].normalised().to_owned() + result[i + 1].original();
|
||||
result[i].set_normalised(normalised);
|
||||
let normalized = result[i].normalized().to_owned() + result[i + 1].original();
|
||||
result[i].set_normalized(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ where
|
|||
let max_d = (old.len() + new.len()).div_ceil(2) + 1;
|
||||
let mut vb = V::new(max_d);
|
||||
let mut vf = V::new(max_d);
|
||||
let mut result: Vec<RawOperation<T>> = vec![];
|
||||
let mut result = Vec::new();
|
||||
|
||||
conquer(
|
||||
old,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue