【问题标题】:Grabbing Edits from two strings从两个字符串中获取编辑
【发布时间】:2015-08-21 17:30:21
【问题描述】:

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

我想要做什么

我需要存储一个用户可以编辑的“文件”(文本文档)。如果我有我的原始文件(可能很大)

Lorem ipsum dolor sit amet

并且用户要做出改变:

Foo ipsum amet_sit

基本上,我有原始字符串和用户编辑的字符串。我想找出不同之处,“edits”。防止存储 very large 字符串的重复项。我想存储原件和“编辑”。然后将编辑应用到原件。有点像重复数据删除。问题是我不知道编辑可以有多么不同,我还需要能够将这些编辑应用于字符串。

尝试

因为文本可能很大,我想知道在不存储两个单独版本的情况下存储文本编辑的最“有效”方式是什么。我的第一个猜测是:

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..."

基本上,我试图将文本按空格拆分为数组。比较数组并存储差异。然后应用差异生成修改后的版本

问题

但如果空格的数量发生变化,就会出现问题:

strOriginal String of text... modOriginalString 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>

TL;DR:

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


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

【问题讨论】:

  • 您使用的是什么操作系统?这是浏览器还是通用(服务器端)JavaScript? node.js 可用吗?
  • @orb 不,这是客户端 javascript。
  • @vihan1086 你为什么需要这个。您正在存储原件,然后用编辑替换原件。那么为什么不直接用用户编辑的文本替换原始文本呢?最后你会得到相同的结果。由用户修改的文本。为什么您需要将编辑保存在某个地方。尤其是当原版无论如何都会被销毁时。
  • 例如,原文:today was a good day。用户编辑:today was a OK day。 (现在你想)保存; -good +OK 然后应用到原文today was a good daytoday was a OK day。现在您有了全文:today was a OK day,并保存了“-good +OK”编辑。所以我问为什么?回到历史?
  • @MuhammadUmer 我确实需要同时使用原始版本和修改版本。

标签: javascript file storage edit safari-extension


【解决方案1】:

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

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

'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);
}

带有仅处理一个文本区域的原始脚本的原始帖子如下:

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

'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);
  });

});

【讨论】:

  • 有趣……我如何将它应用于两个字符串?我确实有一个很大的contentEditable 区域进行编辑。问题是,当我得到innerHTML 时,位置会不正确,因为它不包含标签
  • 上面我修改了脚本来处理多个可编辑的文本区域。享受吧!
  • 这看起来很酷,我看看我能不能整合这个,我会回复你
  • 相当有趣的答案——纯JS解决问题!我使用 ACE 提出了类似的建议,但我真的很喜欢这个解决方案,因为它不需要外部模块
【解决方案2】:

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

看看jsdiff

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

【讨论】:

    【解决方案3】:

    仅使用 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"}
    

    您可以挂钩到 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
                ])
        }
    });
    

    要了解它的工作原理,只需创建一些捕获更改的测试运行。基本上只有那些用于命令的:

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

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

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

    你在测试的时候会有一些问题,将字符串组合回文本并不是一件容易的事,而是相当可行的,不应该超过100行左右的代码。

    好消息是,当您完成后,您还可以轻松使用 undoredo 命令,因此您可以重播整个编辑过程。

    【讨论】:

    • 如果你有问题,请告诉我,我也在研究这个主题,可能也会在 GitHub 中为 ACE 或其他编辑器创建一些测试用例
    • 我在示例中将命令放置在 Array 中的原因是我希望在通过网络发送时使用一种不那么冗长的格式
    • 谢谢,我将使用Aloha Editor,但我会看看你的回答能做什么
    • @vihan1086 如果你能提供如何用 Aloha 做类似事情的例子,我可能也有兴趣知道......
    【解决方案4】:

    尝试创建基本比较标识符,例如,js 下方 "+""-" ;利用.map()比较原始o,编辑e输入字符串,返回数组diffoe之间的差异;将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);
    &lt;pre&gt;&lt;/pre&gt;

    【讨论】:

    • @vihan1086 “这个差异是为数组中的每个项目添加的。存储两个单独的编辑会更短”不确定解释“为每个项目添加数组”正确吗?似乎只有一个编辑,一个原始字符串?
    • @vihan1086 diff 数组不会为“字符串中的每个字符”添加。如果在 originaledited 之间没有进行任何更改,例如在索引 1 处,结果将是 "[edits:+|-, index:1]o";也就是说,+ 之后没有字符出现,- 之后没有字符出现。正如在方法的解释中所指出的,维护模式中最重要的部分可能是定义使用哪些符号来区分编辑和非编辑。空格字符可能难以区分,因为实际的空格字符显示在编辑位置;例如索引 3 "[edits:+ |-e, index:3] "
    猜你喜欢
    • 1970-01-01
    • 2022-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-25
    • 1970-01-01
    • 2015-11-03
    • 1970-01-01
    相关资源
    最近更新 更多