0

I have a function like this that's provided by a user:

function replace_function(string) {
    return string.replace(/:smile:/g, '⻇')
      .replace(/(foo|bar|baz)/g, 'text_$1');
}

and I have input string like this:

var input = 'foo bar :smile: xxxx';

and I have a number from 0 to length of the input string that I use to do substring to split the string.

I need to find number (position) that will match output string after replacement so the split is in the same visual place. The split is just for visualization I only need the number.

The output string can have the same length, this is only for the case when length of input and output is different (like width provided function and input string)

function replace_function(string) {
    return string.replace(/:smile:/g, '⻇')
      .replace(/(foo|bar|baz)/g, 'text_$1');
}

var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
  var input = textarea.value;
  var output = replace_function(input);
  // find position for output
  var position = textarea.selectionStart;
  var split = [
    output.substring(0, position),
    output.substring(position)
  ];
  pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>xxx foo xxx bar xxx :smile: xxxx</textarea>
<pre></pre>

when you click in the middle of the word that get replaced the position/split need to be after the output word. If you click before, between or after the word the position need to be in the same place (the position in each case will be different to match the correct place)

UPDATE: here is my code that work for :smile: only input, but there is need to be text before :smile: (input = ":smile: asdas" and position in the middle of smile and the position is off)

it work for foo replaced by text_foo but not in the case when there is :smile: before foo (input "asd :smile: asd foo").

var get_position = (function() {
  function common_string(formatted, normal) {
      function longer(str) {
          return found && length(str) > length(found) || !found;
      }
      var formatted_len = length(formatted);
      var normal_len = length(normal);
      var found;
      for (var i = normal_len; i > 0; i--) {
          var test_normal = normal.substring(0, i);
          var formatted_normal = replace_function(test_normal);
          for (var j = formatted_len; j > 0; j--) {
              var test_formatted = formatted.substring(0, j);
              if (test_formatted === formatted_normal &&
                  longer(test_normal)) {
                  found = test_normal;
              }
          }
      }
      return found || '';
  }
  function index_after_formatting(position, command) {
      var start = position === 0 ? 0 : position - 1;
      var command_len = length(command);
      for (var i = start; i < command_len; ++i) {
          var substr = command.substring(0, i);
          var next_substr = command.substring(0, i + 1);
          var formatted_substr = replace_function(substr);
          var formatted_next = replace_function(next_substr);
          var substr_len = length(formatted_substr);
          var next_len = length(formatted_next);
          var test_diff = Math.abs(next_len - substr_len);
          if (test_diff > 1) {
              console.log('return ' + i);
              return i;
          }
      }
  }
  return function get_formatted_position(position, command) {
      var formatted_position = position;
      var string = replace_function(command);
      var len = length(string);
      var command_len = length(command);
      if (len !== command_len) {
          var orig_sub = command.substring(0, position);
          var orig_len = length(orig_sub);
          var sub = replace_function(orig_sub);
          var sub_len = length(sub);
          var diff = Math.abs(orig_len - sub_len);
          if (false && orig_len > sub_len) {
              formatted_position -= diff;
          } else if (false && orig_len < sub_len) {
              formatted_position += diff;
          } else {
              var index = index_after_formatting(position, command);
              var to_end = command.substring(0, index + 1);
              //formatted_position -= length(to_end) - orig_len;
              formatted_position -= orig_len - sub_len;
              if (orig_sub && orig_sub !== to_end) {
                  var formatted_to_end = replace_function(to_end);
                  var common = common_string(formatted_to_end, orig_sub);
                  var re = new RegExp('^' + common);
                  var before_end = orig_sub.replace(re, '');
                  var to_end_rest = to_end.replace(re, '');
                  var to_end_rest_len = length(replace_function(to_end_rest));
                  if (before_end && orig_sub !== before_end) {
                      var commnon_len = length(replace_function(common));
                      formatted_position = position - length(before_end) + to_end_rest_len;
                  }
              }
          }
          if (formatted_position > len) {
              formatted_position = len;
          } else if (formatted_position < 0) {
              formatted_position = 0;
          }
      }
      return formatted_position;
  };
})();

function length(str) {
    return str.length;
}

function replace_function(string) {
    return string.replace(/:smile:/g, '⻇')
      .replace(/(foo|bar|baz)/g, 'text_$1');
}

var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
  var input = textarea.value;
  var output = replace_function(input);
  // find position for output
  var position = get_position(textarea.selectionStart, input);
  var split = [
    output.substring(0, position),
    output.substring(position)
  ];
  pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>xxxx :smile: xxxx :smile: xxx :smile:</textarea>
<pre></pre>
jcubic
  • 61,973
  • 54
  • 229
  • 402
  • Just split before replacing? – Bergi Oct 15 '17 at 13:35
  • @Bergi I need to do this after replacing, if I do this before replacing the split position will be in wrong place, that's why I need to calculate corrected position after replace. Also there would be a problem when split position is in the middle of replaced word. – jcubic Oct 15 '17 at 13:51

1 Answers1

1

To do this, you'll have to do the replace operation yourself with a RegExp#exec loop, and keep track of how the replacements affect the position, something along these lines (but this can probably be optimized):

function trackingReplace(rex, string, replacement, position) {
    var newString = "";
    var match;
    var index = 0;
    var repString;
    var newPosition = position;
    var start;
    rex.lastIndex = 0; // Just to be sure
    while (match = rex.exec(string)) {
        // Add any of the original string we just skipped
        if (rex.global) {
            start = rex.lastIndex - match[0].length;
        } else {
            start = match.index;
            rex.lastIndex = start + match[0].length;
        }
        if (index < start) {
            newString += string.substring(index, start);
        }
        index = rex.lastIndex;
        // Build the replacement string. This just handles $$ and $n,
        // you may want to add handling for $`, $', and $&.
        repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
            if (c0 == "$") return "$";
            return match[c0];
        });
        // Add on the replacement
        newString += repString;
        // If the position is affected...
        if (start < position) {
            // ... update it:
            if (rex.lastIndex < position) {
                // It's after the replacement, move it
                newPosition = Math.max(0, newPosition + repString.length - match[0].length);
            } else {
                // It's *in* the replacement, put it just after
                newPosition += repString.length - (position - start);
            }
        }

        // If the regular expression doesn't have the g flag, break here so
        // we do just one replacement (and so we don't have an endless loop!)
        if (!rex.global) {
            break;
        }
    }
    // Add on any trailing text in the string
    if (index < string.length) {
        newString += string.substring(index);
    }
    // Return the string and the updated position
    return [newString, newPosition];
}

Here's a snippet showing us testing that with various positions:

function trackingReplace(rex, string, replacement, position) {
    var newString = "";
    var match;
    var index = 0;
    var repString;
    var newPosition = position;
    var start;
    rex.lastIndex = 0; // Just to be sure
    while (match = rex.exec(string)) {
        // Add any of the original string we just skipped
        if (rex.global) {
            start = rex.lastIndex - match[0].length;
        } else {
            start = match.index;
            rex.lastIndex = start + match[0].length;
        }
        if (index < start) {
            newString += string.substring(index, start);
        }
        index = rex.lastIndex;
        // Build the replacement string. This just handles $$ and $n,
        // you may want to add handling for $`, $', and $&.
        repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
            if (c0 == "$") return "$";
            return match[c0];
        });
        // Add on the replacement
        newString += repString;
        // If the position is affected...
        if (start < position) {
            // ... update it:
            if (rex.lastIndex < position) {
                // It's after the replacement, move it
                newPosition = Math.max(0, newPosition + repString.length - match[0].length);
            } else {
                // It's *in* the replacement, put it just after
                newPosition += repString.length - (position - start);
            }
        }

        // If the regular expression doesn't have the g flag, break here so
        // we do just one replacement (and so we don't have an endless loop!)
        if (!rex.global) {
            break;
        }
    }
    // Add on any trailing text in the string
    if (index < string.length) {
        newString += string.substring(index);
    }
    // Return the string and the updated position
    return [newString, newPosition];
}

function show(str, pos) {
    console.log(str.substring(0, pos) + "|" + str.substring(pos));
}
function test(rex, str, replacement, pos) {
    show(str, pos);
    var result = trackingReplace(rex, str, replacement, pos);
    show(result[0], result[1]);
}
for (var n = 3; n < 22; ++n) {
    if (n > 3) {
       console.log("----");
    }
    test(/([f])([o])o/g, "test foo result foo x", "...$2...", n);
}
.as-console-wrapper {
  max-height: 100% !important;
}

And here's your snippet updated to use it:

function trackingReplace(rex, string, replacement, position) {
    var newString = "";
    var match;
    var index = 0;
    var repString;
    var newPosition = position;
    var start;
    rex.lastIndex = 0; // Just to be sure
    while (match = rex.exec(string)) {
        // Add any of the original string we just skipped
        if (rex.global) {
            start = rex.lastIndex - match[0].length;
        } else {
            start = match.index;
            rex.lastIndex = start + match[0].length;
        }
        if (index < start) {
            newString += string.substring(index, start);
        }
        index = rex.lastIndex;
        // Build the replacement string. This just handles $$ and $n,
        // you may want to add handling for $`, $', and $&.
        repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
            if (c0 == "$") return "$";
            return match[c0];
        });
        // Add on the replacement
        newString += repString;
        // If the position is affected...
        if (start < position) {
            // ... update it:
            if (rex.lastIndex < position) {
                // It's after the replacement, move it
                newPosition = Math.max(0, newPosition + repString.length - match[0].length);
            } else {
                // It's *in* the replacement, put it just after
                newPosition += repString.length - (position - start);
            }
        }

        // If the regular expression doesn't have the g flag, break here so
        // we do just one replacement (and so we don't have an endless loop!)
        if (!rex.global) {
            break;
        }
    }
    // Add on any trailing text in the string
    if (index < string.length) {
        newString += string.substring(index);
    }
    // Return the string and the updated position
    return [newString, newPosition];
}

function replace_function(string, position) {
    var result = trackingReplace(/:smile:/g, string, '⻇', position);
    result = trackingReplace(/(foo|bar|baz)/g, result[0], 'text_$1', result[1]);
    return result;
}

var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
  var position = textarea.selectionStart;
  var result = replace_function(textarea.value, position);
  var string = result[0];
  position = result[1];
  var split = [
    string.substring(0, position),
    string.substring(position)
  ];
  pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>:smile: foo</textarea>
<pre></pre>
jcubic
  • 61,973
  • 54
  • 229
  • 402
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thanks for the answer but I can't use this because I'm not controlling the replacement I only have a function that do all replacements, that function is provided by the user and I don't have control over it. Maybe it mean that for this to work I need to create new API for users array of pairs intead of a function. – jcubic Oct 15 '17 at 15:42
  • @jcubic: If you don't control the replacement function, I think you'd have to go with some kind of diff on the old and new text (do you have that information?), which it will be very difficult to do with high-fidelity results. Or preferably, take control of the replacement (by changing the API, if I'm reading you correctly). – T.J. Crowder Oct 15 '17 at 15:50
  • 1
    I've added new API `array[regex, string]` and it work great, many thanks. – jcubic Oct 15 '17 at 16:39
  • I've just notice that in your last snippet if the cursor is before or inside word xxx, at the end, the result position is wrong. – jcubic Oct 15 '17 at 16:44
  • @jcubic: Wrong how? I just tried it clicking between the 2nd and 3rd `x` in `bar xxx` and it split at exactly that location (`["xxx text_foo xxx text_bar xx","x :smile: xxxx"]`) – T.J. Crowder Oct 15 '17 at 16:48
  • You need to click on the last xxxx, but I don't see the issue in my code that use your function. – jcubic Oct 15 '17 at 17:27
  • @jcubic: Found and fixed it: `replace_string` was using `string` instead of `result[0]` in the second call to `trackingReplace`. You'll want to fix that at your end if you copied it. – T.J. Crowder Oct 15 '17 at 17:46
  • 1
    I was using only trackingReplace function from your first listing, that's why my code was working, thanks again. – jcubic Oct 15 '17 at 18:04
  • Just notice that your solution don't work for input like this `:smile: foo`. – jcubic Oct 16 '17 at 07:26
  • @jcubic: Found and fixed it, need to move the `index = rex.lastIndex;` *out* of the condition it's in. (You get to do the next fix. :-) ) – T.J. Crowder Oct 16 '17 at 07:39
  • @jcubic: Possibly overlapping comments there, see above. – T.J. Crowder Oct 16 '17 at 07:49
  • Why did you change `if (start <= position) {`? now when you click just before the word the position is after the word, it works with `<` – jcubic Oct 16 '17 at 10:26
  • @jcubic: That was a mistake, an early theory that didn't turn out to be right, which I forgot to remove when correctly fixing the problem. – T.J. Crowder Oct 16 '17 at 10:29
  • 1
    One issue that I've found is that if regex don't have g flag it create infinite loop so I've put break in last line of the while loop when there are no g flag. – jcubic Jun 18 '18 at 07:07
  • @jcubic - Thanks, I've updated that comment and incorporated the change into the snippets as well. Good call. – T.J. Crowder Jun 20 '18 at 16:19
  • found another issue with regex that don't have g flag `start` need to be taken from `match.index` and `index` from `start + match[0].length;` – jcubic Jul 06 '18 at 20:04
  • @jcubic - Thanks. Would you please be sure to edit the snippets as well as the visible code? – T.J. Crowder Jul 07 '18 at 08:45
  • 1
    Sorry forget about those. – jcubic Jul 07 '18 at 09:09
  • As thanks for this difficult very specific problem, I've included the link to your profile on https://github.com/jcubic/jquery.terminal before that there was only a link in the code. – jcubic Sep 18 '21 at 19:59