从两个字符串中抓取编辑

Grabbing Edits from two strings

我将深入探讨我的问题,如果您不想阅读所有内容,可以跳至 TL;DR

我想做什么

我需要存储一个 "file" (文本文档),它可以被用户编辑。如果我有原始文件 (可能很大)

Lorem ipsum dolor sit amet

并且用户要进行更改:

Foo ipsum amet_ sit

基本上,我有原始字符串和用户编辑的字符串。我想找出差异,"edits"。防止存储 非常大的 字符串的副本。我想存储原件和 "edits"。然后将编辑应用到原件。有点像重复数据删除。问题是我不知道编辑有多么不同,我还需要能够将这些编辑应用于字符串。

尝试次数

因为文本可能很大,我想知道什么是最 "efficient" 存储文本编辑而不存储两个单独版本的方法。我的第一个猜测是:

var str = 'Original String of text...'.split(' ') || [],
    mod = 'Modified String of text...'.split(' ') || [], i, edits = [];

for (i = 0; i < str.length; i += 1) {
    edits.push(str[i]===mod[i] ? undefined : mod[i]);
}

console.log(edits); // ["Modified", null, null, null] (desired output)

然后返回:

for (i = 0; i < str.length; i += 1) {
    str[i] = edits[i] || str[i];
}
str.join(' '); // "Modified String of text..."

Basically, I'm trying to split the text by spaces into arrays. Compare the arrays and store the differences. Then apply the differences to generate the modified version

问题

但是如果改变空格的数量,就会出现问题:

str: Original String of text... mod: OriginalString of text...

输出:OriginalString of text... text...

我想要的输出:OriginalString of text...


即使我将 str.lengthmod.lengthedits.length 切换为:

// Get edits
var str = 'Original String of text...'.split(' ') || [],
    mod = 'Modified String of text...'.split(' ') || [], i, edits = [];

for (i = 0; i < mod.length; i += 1) {
    edits.push(str[i]===mod[i] ? undefined : mod[i]);
}

// Apply edits
var final = [];
for (i = 0; i < edits.length; i += 1) {
    final[i] = edits[i] || str[i];
}
final = final.join(' ');

edits 将是:["ModifiedString", "of", "text..."] 结果使整个“存储编辑”变得毫无用处。更糟糕的是,如果要添加/删除一个词。如果 str 变成 Original String of lots of text...。输出仍然是相同的。


我可以看出我这样做的方式有很多缺陷,但我想不出任何其他方式。

片段:

document.getElementById('go').onclick = function() {
  var str = document.getElementById('a').value.split(' ') || [],
    mod = document.getElementById('b').value.split(' ') || [],
    i, edits = [];

  for (i = 0; i < mod.length; i += 1) {
    edits.push(str[i] === mod[i] ? undefined : mod[i]);
  }

  // Apply edits
  var final = [];
  for (i = 0; i < edits.length; i += 1) {
    final[i] = edits[i] || str[i];
  }
  final = final.join(' ');
  alert(final);
};

document.getElementById('go2').onclick = function() {
  var str = document.getElementById('a').value.split(' ') || [],
    mod = document.getElementById('b').value.split(' ') || [],
    i, edits = [];

  for (i = 0; i < str.length; i += 1) {
    edits.push(str[i] === mod[i] ? undefined : mod[i]);
  }

  for (i = 0; i < str.length; i += 1) {
    str[i] = edits[i] || str[i];
  }
  alert(str.join(' ')); // "Modified String of text..."
};
Base String:
<input id="a">
<br/>Modified String:
<input id="b" />
<br/>
<button id="go">Second method</button>
<button id="go2">First Method</button>

长话短说:

你如何找到两个字符串之间的变化?


我正在处理大段文本,每个文本可能大约 兆字节 百千字节。这是浏览器上的运行

编辑:添加了可以处理多个文本区域的修改脚本。

Here is the JSFiddle 用于包含多个可编辑文本区域的页面。 (不要忘记打开开发工具来查看编辑。)您只需要为每个文本区域分配一个唯一的 ID。然后,使用这些 id 作为键并使用每个 textarea 的 edits 数组作为值创建一个映射。这是更新后的脚本:

'use strict';

function Edit(type, position, text) {
  this.type = type;
  this.position = position;
  this.text = text;
}

var ADD = 'add';
var DELETE = 'delete';

var textAreaEditsMap = {};

var cursorStart = -1;
var cursorEnd = -1;
var currentEdit = null;
var deleteOffset = 1;

window.addEventListener('load', function() {
  var textareas = document.getElementsByClassName('text-editable');

  for (var i = 0; i < textareas.length; ++i) {
    var textarea = textareas.item(i);
    var id = textarea.getAttribute('id');

    textAreaEditsMap[id] = [];
    textarea.addEventListener('mouseup', handleMouseUp);
    textarea.addEventListener('keydown', handleKeyDown);
    textarea.addEventListener('keypress', handleKeyPress);
  }
});

function handleMouseUp(event) {
  cursorStart = this.selectionStart;
  cursorEnd = this.selectionEnd;
  currentEdit = null;
}

function handleKeyDown(event) {

  cursorStart = this.selectionStart;
  cursorEnd = this.selectionEnd;

  if (event.keyCode >= 35 && event.keyCode <= 40) { // detect cursor movement keys
    currentEdit = null;
  }

  // deleting text
  if (event.keyCode === 8 || event.keyCode === 46) {
    if (currentEdit != null && currentEdit.type !== 'delete') {
      currentEdit = null;
    }

    if (cursorStart !== cursorEnd) { // Deleting highlighted text
      var edit = new Edit(DELETE, cursorStart, this.innerHTML.substring(cursorStart, cursorEnd));
      textAreaEditsMap[this.getAttribute('id')].push(edit);
      currentEdit = null;

    } else if (event.keyCode === 8) { // backspace
      if (currentEdit == null) {
        deleteOffset = 1;
        var edit = new Edit(DELETE, cursorStart, this.innerHTML[cursorStart - 1]);
        textAreaEditsMap[this.getAttribute('id')].push(edit);
        currentEdit = edit;
      } else {
        ++deleteOffset;
        currentEdit.text = this.innerHTML[cursorStart - 1] + currentEdit.text;
      }

    } else if (event.keyCode === 46) { // delete
      if (currentEdit == null) {
        deleteOffset = 1;
        var edit = new Edit(DELETE, cursorStart, this.innerHTML[cursorStart]);
        textAreaEditsMap[this.getAttribute('id')].push(edit);
        currentEdit = edit;

      } else {
        currentEdit.text += this.innerHTML[cursorStart + deleteOffset++];
      }
    }
  }

  console.log(textAreaEditsMap)
}

function handleKeyPress(event) {

  if (currentEdit != null && currentEdit.type !== 'add') {
    currentEdit = null;
  }

  if (currentEdit == null) {
    currentEdit = new Edit(ADD, cursorStart, String.fromCharCode(event.charCode));
    textAreaEditsMap[this.getAttribute('id')].push(currentEdit);
  } else {
    currentEdit.text += String.fromCharCode(event.charCode);
  }

  console.log(textAreaEditsMap);
}

原创post 仅处理一个文本区域的原创脚本如下:

我制作了一个示例脚本来满足您的需求。我在 JSFiddle 上放了一个 working example。确保在 JSFiddle 示例页面上按 ctrl+shift+J 以打开开发工具,以便您可以看到在进行编辑时记录的编辑数组。编辑按时间顺序添加到 edits 数组,因此您可以通过按相反的时间顺序(即向后迭代数组)应用逆向(即,将已删除的文本添加回来;删除添加的文本)恢复到原始文本。我没有从上下文菜单或通过键绑定处理复制、粘贴、撤消或重做,但我认为您应该能够使用此示例作为指南来处理这些事情。这是脚本:

'use strict';

function Edit(type, position, text) {
  this.type = type;
  this.position = position;
  this.text = text;
}

window.addEventListener('load', function() {
  var ADD = 'add';
  var DELETE = 'delete';

  var cursorStart = -1;
  var cursorEnd = -1;
  var edits = [];
  var currentEdit = null;
  var deleteOffset = 1;

  var textarea = document.getElementById('saved-text');

  textarea.addEventListener('mouseup', function(event) {
    cursorStart = this.selectionStart;
    cursorEnd = this.selectionEnd;
    currentEdit = null;
  });

  textarea.addEventListener('keydown', function(event) {

    cursorStart = this.selectionStart;
    cursorEnd = this.selectionEnd;

    if(event.keyCode >= 35 && event.keyCode <= 40) { // detect cursor movement keys
      currentEdit = null;
    }

    // deleting text
    if(event.keyCode === 8 || event.keyCode === 46) {
      if(currentEdit != null && currentEdit.type !== 'delete') {
        currentEdit = null;
      }

      if(cursorStart !== cursorEnd) {
        var edit = new Edit(DELETE, cursorStart, textarea.innerHTML.substring(cursorStart, cursorEnd));
        edits.push(edit);
        currentEdit = null;

      } else if (event.keyCode === 8) { // backspace
        if (currentEdit == null) {
          deleteOffset = 1;
          var edit = new Edit(DELETE, cursorStart, textarea.innerHTML[cursorStart - 1]);
          edits.push(edit);
          currentEdit = edit;
        } else {
          ++deleteOffset;
          currentEdit.text = textarea.innerHTML[cursorStart - 1] + currentEdit.text;
        }

      } else if (event.keyCode === 46) { // delete
        if(currentEdit == null) {
          deleteOffset = 1;
          var edit = new Edit(DELETE, cursorStart, textarea.innerHTML[cursorStart]);
          edits.push(edit);
          currentEdit = edit;

        } else {
          currentEdit.text += textarea.innerHTML[cursorStart + deleteOffset++];
        }
      }
    }

    console.log(edits)
  });

  textarea.addEventListener('keypress', function(event) {

    if(currentEdit != null && currentEdit.type !== 'add') {
      currentEdit = null;
    }

    // adding text
    if(currentEdit == null) {
      currentEdit = new Edit(ADD, cursorStart, String.fromCharCode(event.charCode));
      edits.push(currentEdit);
    } else {
      currentEdit.text += String.fromCharCode(event.charCode);
    }

    console.log(edits);
  });

});

这是一个类似于代码版本控制和仅保存版本之间更改的问题。

看看jsdiff

您可以创建一个补丁,保存它,稍后将其应用到原始文本以获得修改后的文本。

运行 仅使用 JavaScript 的适当 diff 可能会很慢,但这取决于性能要求和 diff 的质量,当然还有它必须多久一次 运行.

一种非常有效的方法是在用户实际编辑文档时跟踪编辑,并且仅在完成后才存储这些更改。为此,您可以使用例如 ACE 编辑器或任何其他支持更改跟踪的编辑器。

http://ace.c9.io/

ACE 在编辑文档时跟踪更改。 ACE 编辑器以易于理解的格式跟踪命令,例如:

{"action":"insertText","range":{"start":{"row":0,"column":0},
    "end":{"row":0,"column":1}},"text":"d"}

可以hook到ACE编辑器的变化,监听变化事件:

var changeList = []; // list of changes
// editor is here the ACE editor instance for example
var editor = ace.edit(document.getElementById("editorDivId"));
editor.setValue("original text contents");
editor.on("change", function(e) {
    // e.data has the change
    var cmd = e.data;
    var range = cmd.range;
    if(cmd.action=="insertText") {
        changeList.push([
            1, 
            range.start.row,
            range.start.column,
            range.end.row,
            range.end.column,
            cmd.text
        ])
    }
    if(cmd.action=="removeText") {
        changeList.push([
                2, 
                range.start.row,
                range.start.column,
                range.end.row,
                range.end.column,
                cmd.text
            ])
    }
    if(cmd.action=="insertLines") {
        changeList.push([
                3, 
                range.start.row,
                range.start.column,
                range.end.row,
                range.end.column,
                cmd.lines
            ])
    }
    if(cmd.action=="removeLines") {
        changeList.push([
                4, 
                range.start.row,
                range.start.column,
                range.end.row,
                range.end.column,
                cmd.lines,
                cmd.nl
            ])
    }
});

要了解它是如何工作的,只需创建一些测试 运行s 来捕捉变化。基本上只有那些命令:

  1. 插入文本
  2. 删除文本
  3. 插入线条
  4. 删除线条

从文本中删除换行符可能有点棘手。

当您有了这个更改列表后,您就可以对文本文件重播这些更改了。您甚至可以将相似或重叠的更改合并为一个更改 - 例如,对后续字符的插入可以合并为一个更改。

测试时会遇到一些问题,将字符串组合回文本并不简单,但非常可行,不应超过 100 行左右的代码。

令人高兴的是,完成后,您还可以轻松使用 undoredo 命令,因此您可以重播整个编辑过程。

尝试创建基本的比较标识符,例如,在 "+""-" 下面的 js;利用 .map() 比较原始 o、编辑后的 ​​e 输入字符串、return 数组 diff oe 之间的差异;将 oe,diff 设置为对象

的属性

var o = "Lorem ipsum dolor sit amet",
  e = "Foo ipsum amet_ sit"
, res = {
  "original": o,
  "edited": e,
  "diff": o.split("").map(function(val, key) {
    // log edits 
    // `+` preceding character: added character ,
    // `-`: preceding character: removed character;
    // `+` preceding "|": no changes ,
    // `-`: preceding "": no changes;
    // `"index"`: character `index` of original `o` input string
    return e[key] !== val 
           ? "[edits:" + "+" + (e[key] || "") + "|-" + val 
             + ", index:" + key + "]" + (e[key] || "") 
           : "[edits:+|-, index:" + key + "]" + val
  })
};

document.getElementsByTagName("pre")[0].textContent = JSON.stringify(res, null, 2);
<pre></pre>