kpdecker / jsdiff

A javascript text differencing implementation.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bug report: Why doesn't jsdiff show leading/trailing whitespace differences?

DanKaplanSES opened this issue · comments

This is a stackoverflow question I copied over to github per the recommendation of a commenter that believes this is a bug.

  • This may be a duplicate of #402 but it's hard to say because it doesn't go into detail. That issue was closed with a comment suggesting these are unrelated issues.
  • I have tested and reproduced this with diffChars, diffWordsWithSpace, and diffLines.
  • I've toggled every option I've seen documented, but it has no affect with respect to this issue.
  • I am running this code in node v20.10.0 on Windows 10.
  • I've tested and reproduced it in JavaScript and TypeScript.
  • I've tested and reproduced it in jsdiff 5.1.0, 5.0.0, 4.0.2, and 4.0.1.

Question:

Why doesn't jsdiff show leading/trailing whitespace differences?

Details:

I found this popular npm library named jsdiff (github). I'm using it in a node.js environment and it provides this example on how to do so (note: I've modified commonjs requires to esm imports):

import 'colors';
import * as Diff from 'diff';

const one = 'beep boop';
const other = 'beep boob blah';

const diff = Diff.diffChars(one, other);

diff.forEach((part) => {
  // green for additions, red for deletions
  // grey for common parts
  const color = part.added ? 'green' : part.removed ? 'red' : 'grey';
  process.stderr.write(part.value[color]);
});

console.log();

Output:

enter image description here

But if I modify one and/or other to have trailing whitespace, it isn't printed as a difference between the strings:

import 'colors';
import * as Diff from 'diff';

const one = 'beep boop ';
const other = '\nbeep boob blah\t';

const diff = Diff.diffChars(one, other);

diff.forEach((part) => {
  // green for additions, red for deletions
  // grey for common parts
  const color = part.added ? 'green' : part.removed ? 'red' : 'grey';
  process.stderr.write(part.value[color]);
});

console.log();

Output:

enter image description here

The README doesn't say whether this is expected or unexpected behavior of Diff.diffChars, but trailing whitespace is ignored when I change Diff.diffChars to Diff.diffWordsWithSpace:

enter image description here

The documentation for Diff.diffWordsWithSpace is more clear on the matter (emphasis mine):

diffs two blocks of text, comparing word by word, treating whitespace as significant.

I thought it could have something to do with the colors modifying the font foreground instead of background; whitespace has no foreground, so it wouldn't show up. To test that hypothesis, I switched from colors to chalk (after reading many recommendations to do so) and changed the background instead of foreground:

import chalk from 'chalk';
import * as Diff from 'diff';

const one = 'beep boop ';
const other = '\nbeep boob blah\t';

const diff = Diff.diffChars(one, other); // note: I went back to diffChars here

diff.forEach((part) => {
  // green for additions, red for deletions
  // grey for common parts
  const color = part.added
    ? chalk.bgGreen
    : part.removed
      ? chalk.bgRed
      : (x) => x; // identity function, return with normal color
  const coloredText = color(part.value);
  process.stderr.write(coloredText);
});

console.log();

But this didn't make a difference in output:

enter image description here

Why doesn't jsdiff print leading/trailing whitespace differences?

Notes:

You can find a working repo with this last example here: https://github.com/DanKaplanSES/jsdom-sandbox/tree/jsdiff-trailing-whitespace-so

Workaround:

This will print differences in leading and trailing whitespace:

import chalk from 'chalk';
import * as Diff from 'diff';

const one = 'beep boop ';
const other = '\nbeep boob blah\t';

const diff = Diff.diffChars(JSON.stringify(one), JSON.stringify( other));

diff.forEach((part, index) => {
  // green for additions, red for deletions
  // grey for common parts
  const color = part.added
    ? chalk.bgGreen
    : part.removed
      ? chalk.bgRed
      : (x) => x; // identity function, return with normal color
  const coloredText = color(part.value);
  process.stderr.write(coloredText);
});

console.log();

Output:

enter image description here

I consider this to be a workaround, not a solution: JSON.stringify converts whitespace into character escape codes (except for the space character) and that's the only reason this library prints a difference.


Better logs:

I modified the example to print better logs. This shows that the differences are recognized, they just aren't displayed:

import chalk from 'chalk';
import * as Diff from 'diff';

const one = 'beep  boop ';
const other = '\nbeep boob blah\t';

const diff = Diff.diffChars(one, other);

let buffer = "";
diff.forEach((part, index) => {
  // green for additions, red for deletions
  // grey for common parts
  const color = part.added
    ? chalk.bgGreen
    : part.removed
      ? chalk.bgRed
      : (x) => x; // identity function, return with normal color
  const coloredText = color(part.value);
  console.log(`JSON.stringify(part.value)`, JSON.stringify(part.value), `part.added`, part.added, `part.removed`, part.removed);
  buffer += (coloredText);
});

console.log(buffer);

Output:

enter image description here

I want to preemptively add that even if this is working as intended, I think some documentation should be added to explain this behavior.

Even better, I think the README examples should be updated to print leading/trailing whitespace: I looked for a library like this because I needed to diff two strings that appeared equal. The difference turned out to be leading/trailing whitespaces. This library's README implies that it was built for use cases like this, but the documented examples don't provide hints on how to use it for this use case.

I have the same issue, also two spaces between words are not shown as difference. Any solution ??

@psmits1567 That is a separate issue as I am not experiencing those symptoms. I bring this up because I don't want this thread to go off topic.


That said, I may be able to provide some help if you tell me what function you are using. I don't think the diffWords (sp?) considers whitespace by default.

I am indeed using diffWords because I want to see what text is changed. In my case the length of the lines differ, due to double blanks between words and blanks before and after the end of the sentence. So it would be great if that is shown somehow

@psmits1567 I think you want Diff.diffWordsWithSpace instead. If that doesn't work, I don't know what else will. Sorry.

I am not using node, so Diff.diffWordsWithSpace does not work for me.

@ExplodingCabbage I hope you don't mind my direct ping, but I noticed you closed the linked issue. Based on your reply, I don't think this issue relates to that one anymore. Since nobody on StackOverflow seems to know why jsdiff functions as described in my original post, I was hoping to get some feedback from an expert such as yourself. Thank you for understanding!

Agree that issue and this one are separate. I haven't dug into this one yet. I'm currently going through jsdiff's issue backlog issue by issue; I'll get to this one eventually! :)

As mentioned in the comments of the Stack Overflow post (and the “Better logs” section of the original issue, even), there’s no bug with jsdiff here; the issue was displaying the output in a way that made the whitespace invisible.

As mentioned in the comments of the Stack Overflow post (and the “Better logs” section of the original issue, even), there’s no bug with jsdiff here

I agree. jsdiff is working as it should. Whitespace differences are returned in its response. The way those differences are rendered is outside of the project's scope.

That said, the jsdiff documentation, examples, and public API (e.g., diffWordsWithSpace, diffTrimmedLines, etc.) imply that leading/trailing whitespaces will render like any other difference—and that is not the case.

In other words, while I agree that jsdiff is working as intended, I'd argue it's not working as expected; I propose adding/updating the example to include a newline in its input that explains why the output appears the way it does. I'd be happy to submit a PR for this if there's an interest.

So, as others have agreed above, there's definitely nothing wrong with the behaviour of jsdiff itself here. The bug - if we think it's right to call it a bug - is instead in node_example.js, which doesn't take care to ensure that changes to whitespace characters are visible.

I suppose the way to fix that example would be to use background colors as well as text colors, like the HTML version does:

image

Lemme see if I can figure out how to do that...

Ugh. The colors package has some support for background colors, and it works alright for insertions, but doing red text on a "bright red" background gives results that are almost illegible, at least on my (Ubuntu) machine:

image

There's no support for arbitrary text and background colors (e.g. specified by hex code), probably because terminals themselves don't consistently support such a feature (though I am not an expert on terminals, haven't looked into this much, and am speculating & extrapolating from half-understood Stack Overflow answers - I welcome being set right by someone who knows better).

Maybe black text on a green/red background will work?

Just using background colors seems to do the trick, and should be legible no matter whether the user's default text color in their terminal is white, black, or grey - as illustrated by me trying all three:

image

I suppose the way to fix that example would be to use background colors as well as text colors, like the HTML version does:

I had the same thought, but this does not work for newlines or tabs: they don't have a foreground or background when they render.