CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-diff

A JavaScript text diff implementation based on the Myers algorithm for comparing text at different granularities.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

format-conversion.mddocs/

Format Conversion

Convert diff results to XML and Google diff-match-patch formats. Enables integration with other diff tools and systems that require specific output formats.

Capabilities

convertChangesToXML Function

Converts change objects to XML format with <ins> and <del> tags for added and removed content.

/**
 * Convert change objects to XML format
 * @param changes - Array of change objects from any diff function
 * @returns String containing XML with <ins> and <del> tags
 */
function convertChangesToXML(changes);

convertChangesToDMP Function

Converts change objects to Google diff-match-patch format using operation codes.

/**
 * Convert change objects to diff-match-patch format
 * @param changes - Array of change objects from any diff function
 * @returns Array of [operation, text] tuples where operation is -1 (delete), 0 (equal), or 1 (insert)
 */
function convertChangesToDMP(changes);

Usage Examples

XML Format Conversion

import { diffWords, convertChangesToXML } from "diff";

// Create a diff
const changes = diffWords("Hello world", "Hello universe");

// Convert to XML
const xmlOutput = convertChangesToXML(changes);
console.log(xmlOutput);
// Output: "Hello <del>world</del><ins>universe</ins>"

// Example with line diff
const lineChanges = diffLines(
  "line 1\nline 2\nline 3",
  "line 1\nmodified line 2\nline 3"
);

const xmlLines = convertChangesToXML(lineChanges);
console.log(xmlLines);
// Output: "line 1\n<del>line 2\n</del><ins>modified line 2\n</ins>line 3"

HTML Rendering

import { diffChars, convertChangesToXML } from "diff";

function renderDiffAsHTML(oldText, newText) {
  const changes = diffChars(oldText, newText);
  const xmlDiff = convertChangesToXML(changes);
  
  // Style the diff with CSS
  return `
    <div class="diff-container">
      <style>
        .diff-container ins { background-color: #d4edda; text-decoration: none; }
        .diff-container del { background-color: #f8d7da; text-decoration: line-through; }
      </style>
      ${xmlDiff}
    </div>
  `;
}

// Usage
const htmlDiff = renderDiffAsHTML("Hello world", "Hello universe");
document.getElementById('diff-output').innerHTML = htmlDiff;

Google diff-match-patch Format

import { diffWords, convertChangesToDMP } from "diff";

// Create a diff
const changes = diffWords("The quick brown fox", "The slow brown fox");

// Convert to DMP format
const dmpFormat = convertChangesToDMP(changes);
console.log(dmpFormat);
// Output: [
//   [0, "The "],
//   [-1, "quick"],
//   [1, "slow"],
//   [0, " brown fox"]
// ]

// DMP operation codes:
// -1 = DELETE (removed text)
//  0 = EQUAL (unchanged text)  
//  1 = INSERT (added text)

Advanced Usage

Integration with diff-match-patch Library

import { diffLines, convertChangesToDMP } from "diff";
// Assuming you also have the diff-match-patch library installed
// import { diff_match_patch } from "diff-match-patch";

function integrateWithDiffMatchPatch(oldText, newText) {
  // Use jsdiff for initial comparison
  const changes = diffLines(oldText, newText);
  const dmpFormat = convertChangesToDMP(changes);
  
  // Now you can use the DMP format with Google's diff-match-patch library
  // const dmp = new diff_match_patch();
  // const patches = dmp.patch_make(dmpFormat);
  // const patchText = dmp.patch_toText(patches);
  
  return {
    jsdiffChanges: changes,
    dmpFormat: dmpFormat,
    // patches: patches,
    // patchText: patchText
  };
}

// Usage
const integration = integrateWithDiffMatchPatch(originalCode, modifiedCode);
console.log("DMP format:", integration.dmpFormat);

Rich Text Editor Integration

import { diffWords, convertChangesToXML } from "diff";

function createRichTextDiff(originalContent, newContent) {
  const changes = diffWords(originalContent, newContent);
  const xmlDiff = convertChangesToXML(changes);
  
  // Convert to rich text editor format (example for a hypothetical editor)
  return xmlDiff
    .replace(/<del>/g, '<span class="deletion">')
    .replace(/<\/del>/g, '</span>')
    .replace(/<ins>/g, '<span class="insertion">')
    .replace(/<\/ins>/g, '</span>');
}

// Usage
const richTextDiff = createRichTextDiff(
  "The original document text",
  "The modified document text"
);

Multiple Format Export

import { diffLines, convertChangesToXML, convertChangesToDMP } from "diff";

function exportDiffInMultipleFormats(oldContent, newContent, filename) {
  const changes = diffLines(oldContent, newContent);
  
  const formats = {
    xml: convertChangesToXML(changes),
    dmp: convertChangesToDMP(changes),
    json: JSON.stringify(changes, null, 2),
    summary: {
      totalChanges: changes.filter(c => c.added || c.removed).length,
      additions: changes.filter(c => c.added).length,
      deletions: changes.filter(c => c.removed).length,
      unchanged: changes.filter(c => !c.added && !c.removed).length
    }
  };
  
  return formats;
}

// Usage
const multiFormat = exportDiffInMultipleFormats(oldFile, newFile, "changes");
console.log("XML format:", multiFormat.xml);
console.log("DMP format:", multiFormat.dmp);
console.log("Summary:", multiFormat.summary);

Web-based Diff Viewer

import { diffLines, convertChangesToXML } from "diff";

function createWebDiffViewer(oldText, newText, options = {}) {
  const changes = diffLines(oldText, newText);
  const xmlDiff = convertChangesToXML(changes);
  
  const viewerHTML = `
<!DOCTYPE html>
<html>
<head>
  <title>Diff Viewer</title>
  <style>
    body { font-family: monospace; line-height: 1.4; }
    .diff-container { 
      border: 1px solid #ddd; 
      padding: 20px; 
      background: #f9f9f9; 
    }
    ins { 
      background-color: #d4edda; 
      color: #155724; 
      text-decoration: none; 
      padding: 2px 4px;
      border-radius: 3px;
    }
    del { 
      background-color: #f8d7da; 
      color: #721c24; 
      text-decoration: line-through;
      padding: 2px 4px;
      border-radius: 3px;
    }
    .stats {
      margin-bottom: 20px;
      padding: 10px;
      background: #e9ecef;
      border-radius: 5px;
    }
  </style>
</head>
<body>
  <div class="stats">
    <h3>Diff Statistics</h3>
    <p>Changes: ${changes.filter(c => c.added || c.removed).length}</p>
    <p>Lines added: ${changes.filter(c => c.added).length}</p>
    <p>Lines removed: ${changes.filter(c => c.removed).length}</p>
  </div>
  <div class="diff-container">
    <h3>Changes</h3>
    <pre>${xmlDiff}</pre>
  </div>
</body>
</html>`;
  
  return viewerHTML;
}

// Usage
const diffViewer = createWebDiffViewer(oldCode, newCode);
// Save to file or serve via web server

Custom XML Styling

import { diffWords, convertChangesToXML } from "diff";

function convertToCustomXML(changes, options = {}) {
  const {
    insertTag = 'add',
    deleteTag = 'remove',
    attributes = {},
    escapeHtml = true
  } = options;
  
  let result = '';
  
  for (const change of changes) {
    let value = change.value;
    
    if (escapeHtml) {
      value = value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
    }
    
    if (change.added) {
      const attrs = Object.entries(attributes)
        .map(([k, v]) => `${k}="${v}"`)
        .join(' ');
      result += `<${insertTag}${attrs ? ' ' + attrs : ''}>${value}</${insertTag}>`;
    } else if (change.removed) {
      const attrs = Object.entries(attributes)
        .map(([k, v]) => `${k}="${v}"`)
        .join(' ');
      result += `<${deleteTag}${attrs ? ' ' + attrs : ''}>${value}</${deleteTag}>`;
    } else {
      result += value;
    }
  }
  
  return result;
}

// Usage
const changes = diffWords("old text", "new text");
const customXML = convertToCustomXML(changes, {
  insertTag: 'insertion',
  deleteTag: 'deletion',
  attributes: { class: 'diff-change', timestamp: Date.now() },
  escapeHtml: true
});

Markdown Diff Format

import { diffLines, convertChangesToXML } from "diff";

function convertToMarkdownDiff(oldText, newText) {
  const changes = diffLines(oldText, newText);
  
  let markdown = '```diff\n';
  
  changes.forEach(change => {
    const lines = change.value.split('\n');
    lines.forEach(line => {
      if (!line && lines.length === 1) return; // Skip empty single lines
      
      if (change.added) {
        markdown += `+ ${line}\n`;
      } else if (change.removed) {
        markdown += `- ${line}\n`;
      } else {
        markdown += `  ${line}\n`;
      }
    });
  });
  
  markdown += '```';
  return markdown;
}

// Usage
const markdownDiff = convertToMarkdownDiff(oldContent, newContent);
console.log("Markdown diff:\n", markdownDiff);

Performance Considerations

import { diffLines, convertChangesToXML, convertChangesToDMP } from "diff";

function efficientFormatConversion(oldText, newText, targetFormats) {
  // Compute diff once
  const changes = diffLines(oldText, newText);
  
  // Convert to requested formats
  const results = {};
  
  if (targetFormats.includes('xml')) {
    results.xml = convertChangesToXML(changes);
  }
  
  if (targetFormats.includes('dmp')) {
    results.dmp = convertChangesToDMP(changes);
  }
  
  if (targetFormats.includes('json')) {
    results.json = JSON.stringify(changes);
  }
  
  if (targetFormats.includes('stats')) {
    results.stats = {
      additions: changes.filter(c => c.added).length,
      deletions: changes.filter(c => c.removed).length,
      unchanged: changes.filter(c => !c.added && !c.removed).length
    };
  }
  
  return results;
}

// Usage - only compute what's needed
const formats = efficientFormatConversion(text1, text2, ['xml', 'stats']);

Install with Tessl CLI

npx tessl i tessl/npm-diff

docs

array-diffing.md

character-diffing.md

css-diffing.md

custom-diffing.md

format-conversion.md

index.md

json-diffing.md

line-diffing.md

patch-application.md

patch-creation.md

patch-utilities.md

sentence-diffing.md

word-diffing.md

tile.json