Skip to content

Elisions: Add for objects and custom localization  #109

Open
@unclechu

Description

@unclechu

Hi, we forked your library in order to make a React component based on it:

We made a couple of changes to the original library behavior in relation to elisions rendering you might be also interested in adding to back to your library:

  1. Rendering elisions for objects the similar way they are rendered for arrays. For our use case we found it confusing that the untouched keys are just not showing up in the diff like they didn’t exist. And we though it would be very intuitive to render the placeholders/elisions for extra untouched fields the same way it’s already done for arrays.

    We added an extra option showElisionsForObjects which is a boolean we keep turned on by default. Setting it to false allows to use the original behavior of json-diff.

  2. Customizable elisions renderer. The “entries” word is hard-coded into the elision template. We though it would be nice to have it customizable in case anyone would need to add a localization other than English. So we added a generic way of customizing the elisions renderer by providing a renderer function.

    Custom renderer takes the elisions counter value, and just forwarded maxElisions value for convenience which can be just ignored. It can return a list of strings which will translate into multiple lines. Or just a single string which will transform into a single line (the same can be achieved by providing a list of one single string).

    renderElision?: (elisionCount: number, maxElisions: number) => string | string[];

Here is a patch with the essentials of the change (the patch has a couple of unrelated lines for the piece of React-related code, just ignore them): json-diff-react-elisions.patch.zip

The patch is produced by this command ran against https://github.com/relex/json-diff-react repo:

git diff 6246112 3077bf9 src/JsonDiff/Internal/json-diff.js src/JsonDiff/Internal/colorize.jsx src/JsonDiff/Internal/utils.ts 

The patch contents:

diff --git a/src/JsonDiff/Internal/colorize.jsx b/src/JsonDiff/Internal/colorize.jsx
index 7ba5ccd..c29bc50 100644
--- a/src/JsonDiff/Internal/colorize.jsx
+++ b/src/JsonDiff/Internal/colorize.jsx
@@ -3,7 +3,7 @@
 // The 'colorize' function was adapted from console rendering to browser
 // rendering - it now returns a JSX.Element.
 
-import { extendedTypeOf } from './utils';
+import { extendedTypeOf, elisionMarker } from './utils';
 import React from 'react';
 
 const subcolorizeToCallback = function (options, key, diff, output, color, indent) {
@@ -11,14 +11,20 @@ const subcolorizeToCallback = function (options, key, diff, output, color, inden
   const prefix = key ? `${key}: ` : '';
   const subindent = indent + '  ';
 
+  const maxElisions = options.maxElisions === undefined ? Infinity : options.maxElisions;
+
+  const renderElision =
+    options.renderElision ??
+    ((n, max) => (n < max ? [...Array(n)].map(() => '...') : `... (${n} entries)`));
+
   const outputElisions = (n) => {
-    const maxElisions = options.maxElisions === undefined ? Infinity : options.maxElisions;
-    if (n < maxElisions) {
-      for (let i = 0; i < n; i++) {
-        output(' ', subindent + '...');
-      }
+    const elisions = renderElision(n, maxElisions);
+    if (typeof elisions === 'string') {
+      output(' ', subindent + elisions);
     } else {
-      output(' ', subindent + `... (${n} entries)`);
+      elisions.forEach((x) => {
+        output(' ', subindent + x);
+      });
     }
   };
 
@@ -29,9 +35,23 @@ const subcolorizeToCallback = function (options, key, diff, output, color, inden
         return subcolorizeToCallback(options, key, diff.__new, output, '+', indent);
       } else {
         output(color, `${indent}${prefix}{`);
+
+        // Elisions are added in “json-diff” module depending on the option.
+        let elisionCount = 0;
+
         for (const subkey of Object.keys(diff)) {
           let m;
           subvalue = diff[subkey];
+
+          // Handle elisions
+          if (subvalue === elisionMarker) {
+            elisionCount++;
+            continue;
+          } else if (elisionCount > 0) {
+            outputElisions(elisionCount);
+            elisionCount = 0;
+          }
+
           if ((m = subkey.match(/^(.*)__deleted$/))) {
             subcolorizeToCallback(options, m[1], subvalue, output, '-', subindent);
           } else if ((m = subkey.match(/^(.*)__added$/))) {
@@ -40,6 +60,10 @@ const subcolorizeToCallback = function (options, key, diff, output, color, inden
             subcolorizeToCallback(options, subkey, subvalue, output, color, subindent);
           }
         }
+
+        // Handle elisions
+        if (elisionCount > 0) outputElisions(elisionCount);
+
         return output(color, `${indent}}`);
       }
 
@@ -105,10 +129,10 @@ const colorizeToCallback = (diff, options, output) =>
 export const colorize = function (diff, options = {}, customization) {
   const output = [];
 
-  let className;
-  let style;
-
   colorizeToCallback(diff, options, function (color, line) {
+    let className;
+    let style;
+
     if (color === ' ') {
       className = customization.unchangedClassName;
       style = customization.unchangedLineStyle;
@@ -122,7 +146,7 @@ export const colorize = function (diff, options = {}, customization) {
 
     let renderedLine = (
       <div className={className} style={style} key={output.length}>
-        {line + '\r\n'}
+        {line}
       </div>
     );
 
@@ -131,7 +155,7 @@ export const colorize = function (diff, options = {}, customization) {
 
   return (
     <div className={customization.frameClassName} style={customization.frameStyle}>
-      <pre>{output}</pre>
+      {output}
     </div>
   );
 };
diff --git a/src/JsonDiff/Internal/json-diff.js b/src/JsonDiff/Internal/json-diff.js
index f5037b8..39da9a5 100644
--- a/src/JsonDiff/Internal/json-diff.js
+++ b/src/JsonDiff/Internal/json-diff.js
@@ -1,13 +1,17 @@
 // This is copied from 'json-diff' package ('lib/index.js') with minor
 // modifications.
 
-import { extendedTypeOf } from './utils';
+import { extendedTypeOf, elisionMarker } from './utils';
 import { SequenceMatcher } from '@ewoudenberg/difflib';
 
 export default class JsonDiff {
   constructor(options) {
     options.outputKeys = options.outputKeys || [];
     options.excludeKeys = options.excludeKeys || [];
+
+    // Rendering ”...” elisions in the same way as for arrays
+    options.showElisionsForObjects = options.showElisionsForObjects ?? true;
+
     this.options = options;
   }
 
@@ -55,6 +59,8 @@ export default class JsonDiff {
           equal = false;
         } else if (this.options.full || this.options.outputKeys.includes(key)) {
           result[key] = value1;
+        } else if (this.options.showElisionsForObjects) {
+          result[key] = elisionMarker;
         }
         // console.log(`key ${key} change.score=${change.score} ${change.result}`)
         score += Math.min(20, Math.max(-10, change.score / 5)); // BATMAN!
diff --git a/src/JsonDiff/Internal/utils.ts b/src/JsonDiff/Internal/utils.ts
index 29fba60..a9005b3 100644
--- a/src/JsonDiff/Internal/utils.ts
+++ b/src/JsonDiff/Internal/utils.ts
@@ -27,3 +27,11 @@ export const roundObj = function (data: any, precision: number) {
     return data;
   }
 };
+
+// A hacky marker for “...” elisions for object keys.
+// This feature wasn’t present in the original “json-diff” library.
+// A unique identifier used as a value for the “elisioned” object keys.
+//
+// Read more about “Symbol”s here:
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
+export const elisionMarker = Symbol('json-diff-react--elision-marker');

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions