Use wbpack for website
This commit is contained in:
parent
a2119b0f32
commit
f07aa5faa7
13 changed files with 6189 additions and 481 deletions
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
|
|
@ -5,17 +5,17 @@
|
||||||
|
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "cargo"
|
- package-ecosystem: 'cargo'
|
||||||
directories: ["**"]
|
directories: ['**']
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: 'daily'
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: 'github-actions'
|
||||||
directories: ["**"]
|
directories: ['**']
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: 'daily'
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: 'npm'
|
||||||
directories: ["/reconcile-js"]
|
directories: ['/reconcile-js', '/examples/website']
|
||||||
schedule:
|
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
|
||||||
|
}
|
||||||
|
|
@ -1,161 +1,144 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
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:title" content="3-Way Text Merge" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Easily merge three versions of a text document with this 3-way text merge tool."
|
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:type" content="website" />
|
||||||
<meta
|
<meta property="og:url" content="https://github.com/schmelczer/reconcile" />
|
||||||
property="og:url"
|
<meta property="og:image" content="/favicon.ico" />
|
||||||
content="https://github.com/schmelczer/reconcile"
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
/>
|
<title>3-Way Text Merge</title>
|
||||||
<meta property="og:image" content="/favicon.ico" />
|
<link inline inline-asset="index.css" inline-asset-delete />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
</head>
|
||||||
<title>3-Way Text Merge</title>
|
<body>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<div class="background"></div>
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="background"></div>
|
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<header>
|
<header>
|
||||||
<h1>3-Way Text Merge</h1>
|
<h1>3-Way Text Merge</h1>
|
||||||
<p>
|
<p>
|
||||||
The
|
The
|
||||||
<a
|
<a href="https://github.com/schmelczer/reconcile" target="_blank">reconcile</a>
|
||||||
href="https://github.com/schmelczer/reconcile"
|
solves a fundamental challenge in collaborative editing: what happens when
|
||||||
target="_blank"
|
multiple people edit the same text simultaneously?
|
||||||
>reconcile</a
|
<code>reconcile(parent: str, left: str, right: str) -> str</code>
|
||||||
>
|
takes conflicting concurrent edits and intelligently merges them into a unified
|
||||||
solves a fundamental challenge in collaborative editing:
|
result. Beyond basic conflict resolution, it offers sophisticated merging
|
||||||
what happens when multiple people edit the same text
|
heuristics, flexible tokenization options, and cursor position tracking.
|
||||||
simultaneously?
|
</p>
|
||||||
<code
|
<p>
|
||||||
>reconcile(parent: str, left: str, right: str) ->
|
The algorithm begins with your chosen tokenizer, then applies Myers' diff
|
||||||
str</code
|
algorithm to compare the original text with both conflicting versions. These
|
||||||
>
|
diffs undergo transformation to preserve meaningful change sequences, before a
|
||||||
takes conflicting concurrent edits and intelligently merges
|
final merge strategy—inspired by Operational Transformation (OT)—reconciles all
|
||||||
them into a unified result. Beyond basic conflict
|
conflicting modifications without losing any edits.
|
||||||
resolution, it offers sophisticated merging heuristics,
|
</p>
|
||||||
flexible tokenization options, and cursor position tracking.
|
<p>
|
||||||
</p>
|
For more details, see the
|
||||||
<p>
|
<a href="https://github.com/schmelczer/reconcile" target="_blank">README</a>.
|
||||||
The algorithm begins with your chosen tokenizer, then
|
</p>
|
||||||
applies Myers' diff algorithm to compare the original text
|
</header>
|
||||||
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>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="text-area-card diamond-parent">
|
<div class="text-area-card diamond-parent">
|
||||||
<label
|
<label
|
||||||
for="original"
|
for="original"
|
||||||
title="The text document's content before any concurrent edits occurred."
|
title="The text document's content before any concurrent edits occurred."
|
||||||
>Original</label
|
>Original</label
|
||||||
>
|
>
|
||||||
<textarea id="original" name="original"></textarea>
|
<textarea id="original" name="original"></textarea>
|
||||||
</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>
|
|
||||||
|
|
||||||
<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
|
|
||||||
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>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<script type="module" src="script.js"></script>
|
<div class="text-area-card diamond-left">
|
||||||
</body>
|
<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>
|
||||||
|
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<noscript>JavaScript is required for this website.</noscript>
|
||||||
|
<script inline inline-asset="index.js" inline-asset-delete></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
80
examples/website/index.ts
Normal file
80
examples/website/index.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { init, reconcileWithHistory } 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 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(): Promise<void> {
|
||||||
|
await init();
|
||||||
|
|
||||||
|
originalTextArea?.addEventListener('input', updateMergedText);
|
||||||
|
leftTextArea?.addEventListener('input', updateMergedText);
|
||||||
|
rightTextArea?.addEventListener('input', updateMergedText);
|
||||||
|
window.addEventListener('resize', resizeTextAreas);
|
||||||
|
|
||||||
|
loadSample();
|
||||||
|
updateMergedText();
|
||||||
|
if (leftTextArea) focusTextArea(leftTextArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSample(): void {
|
||||||
|
if (originalTextArea) originalTextArea.value = sampleText;
|
||||||
|
if (leftTextArea) {
|
||||||
|
leftTextArea.value =
|
||||||
|
sampleText.replace('color', 'colour') +
|
||||||
|
" Check out what's the most complex conflict you can come up with!";
|
||||||
|
}
|
||||||
|
if (rightTextArea) {
|
||||||
|
rightTextArea.value = sampleText
|
||||||
|
.replace(', for example,', ' such as')
|
||||||
|
.replace('WASM', 'WebAssembly');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMergedText(): void {
|
||||||
|
resizeTextAreas();
|
||||||
|
|
||||||
|
if (!originalTextArea || !leftTextArea || !rightTextArea || !mergedTextArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(): void {
|
||||||
|
if (!CSS.supports('field-sizing', 'content')) {
|
||||||
|
if (originalTextArea) autoResize(originalTextArea);
|
||||||
|
if (leftTextArea) autoResize(leftTextArea);
|
||||||
|
if (rightTextArea) 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);
|
|
||||||
}
|
|
||||||
242
examples/website/style.scss
Normal file
242
examples/website/style.scss
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
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": {}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,5 @@ set -e
|
||||||
wasm-pack build --target web --features wasm,wee_alloc
|
wasm-pack build --target web --features wasm,wee_alloc
|
||||||
cd reconcile-js
|
cd reconcile-js
|
||||||
npm run build
|
npm run build
|
||||||
mkdir -p ../examples/website/dist
|
|
||||||
cp -R dist/index.js ../examples/website/dist/index.js
|
|
||||||
|
|
||||||
cd ../examples/website
|
cd ../examples/website
|
||||||
|
npm run start
|
||||||
python3 -m http.server $1 --bind 0.0.0.0
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue