Skip to content

Instantly share code, notes, and snippets.

@mmpataki
Created August 5, 2025 18:23
Show Gist options
  • Select an option

  • Save mmpataki/eb8c88dce971c805581ebd3235053fc3 to your computer and use it in GitHub Desktop.

Select an option

Save mmpataki/eb8c88dce971c805581ebd3235053fc3 to your computer and use it in GitHub Desktop.
render_N_Diff
<html lang="en">
<body>
<div id="output"></div>
<script>
let o1 = { a: 1, b: 2, c: { d: 3, e: 4 }, f: [5, 6] };
let o2 = { a: 1, b: 3, c: { d: 3, e: 5 }, f: [5, 7], g: "haha" };
let o3 = { a: 1, c: { d: 3, e: 5 }, f: [5, 7] };
render_N_Diff([o1, o2, o3], ['Object 1', 'Object 2', 'Object 3'], document.getElementById('output'));
</script>
</body>
</html>
function render(name, spec, elemCreated, container) {
let e;
if (!spec.preBuilt) {
e = document.createElement(spec.ele);
spec.iden && elemCreated(spec.iden, e)
if (spec.text) e.innerHTML = spec.text;
if (spec.classList) {
e.classList = `${name}-` + spec.classList.split(/\s+/).join(` ${name}-`)
}
spec.attribs && Object.keys(spec.attribs).forEach(key => {
e[key] = spec.attribs[key]
})
spec.styles && Object.keys(spec.styles).forEach(key => {
e.style[key] = spec.styles[key]
})
spec.evnts && Object.keys(spec.evnts).forEach(key => {
e.addEventListener(key, spec.evnts[key])
})
if (spec.children) {
if (spec.children instanceof Function) {
spec.children().map(x => e.appendChild(x))
}
else spec.children.forEach(child => render(name, child, elemCreated, e))
}
} else {
e = spec.ele;
}
if (container) {
let lbl;
if (spec.label || spec.postlabel) {
let rgid = "id_" + Math.random();
e.id = rgid
lbl = document.createElement('label')
lbl.innerHTML = spec.label || spec.postlabel
lbl.setAttribute('for', rgid)
}
if (spec.label) container.appendChild(lbl)
container.appendChild(e)
if (spec.postlabel) container.appendChild(lbl)
return container;
}
return e;
}
function syncScrolls(els) {
els = els.map(e => typeof e === 'string' ? document.querySelector(e) : e).filter(Boolean);
let sync = false;
els.forEach(el => el.addEventListener('scroll', () => {
if (sync) return;
sync = true;
const tp = el.scrollTop / (el.scrollHeight - el.clientHeight) || 0;
const lp = el.scrollLeft / (el.scrollWidth - el.clientWidth) || 0;
els.forEach(o => o !== el && (o.scrollTop = tp * (o.scrollHeight - o.clientHeight), o.scrollLeft = lp * (o.scrollWidth - o.clientWidth)));
setTimeout(() => sync = false, 10);
}));
}
function render_N_Diff(objects, titles, container) {
let dboxes = [], lineIndex = 0;
render('diff', {
ele: 'div',
styles: { display: 'flex', flexDirection: 'row', width: '100%', height: '100%' },
children: titles.map((title, i) => ({
ele: 'div',
styles: { flex: 1, flexGrow: 1, position: 'relative', minWidth: 0 },
children: [
{ ele: 'strong', text: title, styles: { display: 'block', padding: '5px', backgroundColor: '#f5f5f5', borderBottom: '1px solid #ddd' } },
{ ele: 'div', iden: i + 1, styles: { overflowY: 'auto', overflowX: 'auto', border: 'solid 1px #eee', height: 'calc(100% - 32px)', minHeight: '400px' } }
]
}))
}, (i, e) => dboxes[i - 1] = e, container);
syncScrolls(dboxes)
function isObject(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function typeOf(value) {
if (Array.isArray(value)) return 'array';
if (isObject(value)) return 'object';
return 'primitive';
}
function diff(objects, node = {}) {
let firstNonNull = objects.filter(Boolean)[0]
node.type = firstNonNull ? typeOf(firstNonNull) : 'null';
node.children = {}
node.values = objects
if (!firstNonNull) return node
if (isObject(firstNonNull)) {
let keys = new Set(objects.flatMap(obj => Object.keys(obj || {})));
keys.forEach(k => {
diff(objects.map(obj => (obj || {})[k]), node.children[k] = {});
});
} else if (Array.isArray(firstNonNull)) {
[...new Set(objects.flat())].filter(Boolean).forEach((value, i) => {
let values = objects.map(arr => (arr || []).includes(value) ? value : undefined);
diff(values, node.children[value] = {});
})
} else {
let valueCounts = new Map(), max = [0, 0];
objects.filter(Boolean).forEach(value => valueCounts.set(value, (valueCounts.get(value) || 0) + 1));
valueCounts.forEach((count, value) => { if (count > max[0]) max = [count, value] });
node.markers = objects.map((value, i) => ({ text: value, isMajority: value === max[1] }))
}
return node;
}
function escapeJSON(value) {
if (typeof value === 'string') {
return '"' + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') + '"';
}
return String(value);
}
function outText(text, indent, container, color) {
const bgColor = color ? color : (Math.floor(lineIndex / dboxes.length) % 2 == 0 ? '#f7f7f7' : '#fff');
render('diff', { ele: 'pre', styles: { lineHeight: '1.2', margin: '0px', backgroundColor: bgColor, width: '100%', minWidth: 'max-content' }, text: (' '.repeat(4 * indent)) + (text || '') }, undefined, container);
lineIndex++;
}
function forAll(text, indent, containers) {
containers.forEach(container => {
outText(text, indent, container);
});
}
function _renderDiff(key, node, dboxes, level = 0) {
let keyPref = key ? `${escapeJSON(key)}: ` : '';
let wrappers = { 'array': '[]', 'object': '{}' };
if (node.type == 'primitive') {
node.markers.forEach((marker, i) => {
let displayText = marker.text ? `${keyPref}${escapeJSON(marker.text)}` : '';
outText(displayText, level, dboxes[i], marker.text ? (marker.isMajority ? '#e0f8e0' : '#fffeb4') : '#ffe8e8');
});
} else if (node.type != 'null') {
node.values.forEach((v, i) => outText(v ? `${keyPref}${wrappers[node.type].charAt(0)}` : undefined, level, dboxes[i], v ? undefined : '#ffe8e8'))
Object.keys(node.children).forEach(childKey => {
_renderDiff(node.type == 'object' ? childKey : undefined, node.children[childKey], dboxes, level + 1);
});
node.values.forEach((v, i) => outText(v ? wrappers[node.type].charAt(1) : undefined, level, dboxes[i], v ? undefined : '#ffe8e8'))
}
}
let root = diff(objects);
console.log('Root:', JSON.stringify(root, undefined, 2));
_renderDiff(undefined, root, dboxes);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment