<scriptlet>
<implements type="Automation" id="dispatcher">
  <property name="PluginEvent">
    <get/>
  </property>
  <property name="PluginDescription">
    <get/>
  </property>
  <property name="PluginExtendedProperties">
    <get/>
  </property>
  <property name="PluginFileFilters">
    <get/>
  </property>
  <property name="PluginArguments">
    <get/>
    <put/>
  </property>
  <property name="PluginVariables">
    <get/>
    <put/>
  </property>
  <method name="PluginOnEvent"/>
  <method name="ExecFilterCommand"/>
  <method name="MakeUpper"/>
  <method name="MakeLower"/>
  <method name="RemoveDuplicates"/>
  <method name="CountDuplicates"/>
  <method name="SortAscending"/>
  <method name="SortDescending"/>
  <method name="ReverseColumns"/>
  <method name="ReverseLines"/>
  <method name="SelectColumns"/>
  <method name="SelectLines"/>
  <method name="Replace" internalName="ReplaceText"/>
  <method name="Tokenize"/>
  <method name="Trim"/>
</implements>

<script language="JScript">

var REGKEY_PATH = "Plugins\\editor addin.sct/";

var wsh = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var pluginArguments = "";
var variables = new Array();
var mergeApp;

function get_PluginEvent() {
  return "EDITOR_SCRIPT";
}

function get_PluginDescription() {
  return "Basic text functions";
}

function get_PluginFileFilters() {
  return ".*";
}

function get_PluginExtendedProperties() {
  return "GenerateUnpacker;" +
  "MakeUpper.MenuCaption=Make Uppercase;" +
  "MakeUpper.Description=Make characters uppercase;" +
  "MakeLower.MenuCaption=Make Lowercase;" +
  "MakeLower.Description=Make characters lowercase;" +
  "RemoveDuplicates.MenuCaption=Remove Duplicate Lines;" +
  "RemoveDuplicates.Description=Remove duplicate lines;" +
  "CountDuplicates.MenuCaption=Count Duplicate Lines;" +
  "CountDuplicates.Description=Count duplicate lines;" +
  "SortAscending.MenuCaption=Sort Lines Ascending;" +
  "SortAscending.Description=Sort lines ascending;" +
  "SortDescending.MenuCaption=Sort Lines Descending;" +
  "SortDescending.Description=Sort lines descending;" +
  "ExecFilterCommand.MenuCaption=Apply Filter Command...;" +
  "ExecFilterCommand.Description=Apply filter. \r\n" +
    "Usage: ExecFilterCommand COMMAND\r\n"+ 
    " COMMAND - command to execute. %1 = filename.;" +
  "ExecFilterCommand.ArgumentsRequired;" +
  "Tokenize.MenuCaption=Tokenize...;" +
  "Tokenize.Description=Tokenize. \r\n" +
    "Usage: Tokenize PATTERNS\r\n" +
    " PATTERNS - regex (e.g. [^\\w]+);" +
  "Tokenize.ArgumentsRequired;" +
  "Tokenize.Arguments=[^\\w]+;" +
  "Trim.MenuCaption=Trim Spaces;" +
  "Trim.Description=Trim spaces;" +
  "SelectColumns.MenuCaption=Select Columns...;" +
  "SelectColumns.Description=Select columns.\r\n" +
    "Usage: SelectColumns RANGES\r\n" +
    " or: SelectColumns [-v] [-i] [-g] -e PATTERNS\r\n" +
    " RANGES - column ranges (-3,5-10,30-)\r\n" +
    " PATTERNS - regex\r\n" +  
    " -v - select non-matching\r\n" +  
    " -i - ignore case\r\n" +  
    " -g - enable global flag\r\n" +  
    " -e - use PATTERNS;" +
  "SelectColumns.ArgumentsRequired;" +
  "SelectLines.MenuCaption=Select Lines...;" +
  "SelectLines.Description=Select lines.\r\n" +  
    "Usage: SelectLines RANGES\r\n" +  
    " or: SelectLines [-v] [-i] -e PATTERNS\r\n" +  
    " RANGES - line ranges (-3,5-10,30-)\r\n" +  
    " PATTERNS - regex\r\n" +  
    " -v - select non-matching\r\n" +  
    " -i - ignore case\r\n" +  
    " -e - use PATTERNS;" +
  "SelectLines.ArgumentsRequired;" +
  "ReverseColumns.MenuCaption=Reverse Columns;" +
  "ReverseColumns.Description=Reverse columns;" +
  "ReverseLines.MenuCaption=Reverse Lines;" +
  "ReverseLines.Description=Reverse lines;" +
  "Replace.MenuCaption=Replace...;" +
  "Replace.Description=Replace text.\r\n" +  
    "Usage: Replace [-i] [-e] FIND REPLACE\r\n" +  
    " or: Replace -s\r\n" +  
    " FIND - text to find\r\n" +  
    " REPLACE - text to replace\r\n" +  
    " -i - ignore case (only for -e)\r\n" +  
    " -e - regex\r\n" +
    " -s - use Substitution Filters;" +
  "Replace.ArgumentsRequired;";
}

function get_PluginArguments() {
  return pluginArguments;
}

function put_PluginArguments(NewValue) {
  pluginArguments = NewValue;
}

function get_PluginVariables() {
  return variables.join("\0");
}

function put_PluginVariables(NewValue) {
  variables = NewValue.split("\0");
}

function regRead(Key, DefaultValue) {
  return mergeApp.GetOption(Key, DefaultValue);
}

function regWrite(Key, Value, TypeNm) {
  mergeApp.SaveOption(Key, (TypeNm === "REG_DWORD") ? parseInt(Value, 10) : String(Value));
}

function ReplaceVariables(str) {
  var newstr = "";
  var pos = 0;
  while (true) {
    var foundpos = str.indexOf("%", pos);
    if (foundpos > 0) {
      var ch = str.charAt(foundpos + 1);
      if (ch === "%") {
        newstr = newstr + "%";
        pos = foundpos + 2;
      } else if (!isNaN(ch)) {
        newstr = newstr + str.substr(pos, foundpos - pos);
        if (parseInt(ch, 10) < variables.length) {
          newstr = newstr + variables(parseInt(ch, 10));
        }
        pos = foundpos + 2;
      } else {
        newstr = newstr + str.substr(pos, foundpos - pos + 1);
        pos = foundpos + 1;
      }
    } else {
      newstr = newstr + str.substr(pos);
      break;
    }
  }
  return newstr;
}

function IsFirstArgumentEmpty() {
  return (pluginArguments.match(/^\s*$/) !== null);
}

function ParseArguments(args) {
  var ary = new Array();
  var token = "";
  var argsLen = args.length;
  var inQuotes = false;
  var hadQuotes = false;

  for (var i = 0; i < argsLen; i++) {
    var ch = args.charAt(i);
    if (!inQuotes) {
      if (ch === " " || ch === "\t") {
        if (token.length > 0 || hadQuotes) {
          ary.push(token);
          token = "";
          hadQuotes = false;
        }
      } else if (ch === "\"") {
        inQuotes = true;
        hadQuotes = true;
      } else {
        token += ch;
      }
    } else {
      if (ch === "\"") {
        if (i + 1 < argsLen && args.charAt(i + 1) === "\"") {
          token += ch;
          i++;
        } else {
          inQuotes = false;
        }
      } else {
        token += ch;
      }
    }
  }
  if (token.length > 0 || hadQuotes) {
    ary.push(token);
  }
  return ary;
}

function ParseRanges(rangeText) {
  var aryRanges = new Array();
  var ary = rangeText.split(",");
  for (var i = 0, j = 0; i < ary.length; i++) {
    var ary2 = ary[i].split("-");
    var rangeFrom = "";
    if (ary2.length > 0) {
      rangeFrom = ary2[0].replace(/^\s+|\s+$/g, "");
    }
    var rangeTo = rangeFrom;
    if (ary2.length > 1) {
      rangeTo = ary2[1].replace(/^\s+|\s+$/g, "");
    }
    if ((rangeFrom !== "" && !isNaN(rangeFrom)) || (rangeTo !== "" && !isNaN(rangeTo))) {
      if (rangeFrom === "" || isNaN(rangeFrom)) { rangeFrom = 1; }
      if (rangeTo === "" || isNaN(rangeTo)) { rangeTo = 1000000000; }
      if (parseFloat(rangeFrom) < 1 || parseFloat(rangeFrom) > 1000000000) { rangeFrom = 1; }
      if (parseFloat(rangeTo) < 1 || parseFloat(rangeTo) > 1000000000) { rangeTo = 1; }
      if (parseFloat(rangeFrom) > parseFloat(rangeTo)) { rangeTo = rangeFrom; }
      aryRanges.push([parseInt(rangeFrom, 10), parseInt(rangeTo, 10)]);
    }
    }
  return aryRanges;
}

function ParseSelectColumnsLinesArguments(args) {
  var rangesOrRegExps = new Array();
  var argAry = ParseArguments(args);
  var invert = false;
  var flags = "";
  var isRegExp = false;
  for (var i = 0; i < argAry.length; i++) {
    var isOption = false;
    if (argAry[i].length >= 2) {
      switch (argAry[i].charAt(0)) {
      case "-":
      case "/":
        switch (argAry[i].charAt(1)) {
        case "e":
        case "i":
        case "g":
          isRegExp = true;
          isOption = true;
          if (argAry[i].indexOf("g") >= 0) {
            flags += "g";
          }
          if (argAry[i].indexOf("i") >= 0) {
            flags += "i";
          }
          break;
        case "v":
          isOption = true;
          invert = true;
          break;
        }
        break;
      }
    }
    if (!isOption) {
      if (isRegExp) {
        rangesOrRegExps.push(new RegExp(argAry[i], flags));
        isRegExp = false;
      } else {
        rangesOrRegExps.push(ParseRanges(argAry[i]));
      }
    }
  }
  return { "rangesOrRegExps": rangesOrRegExps, "invert": invert };
}

function ParseReplaceArguments(args) {
  var list = new Array();
  var patterns = new Array();
  var isOption = false;
  var argAry = ParseArguments(args);
  var regex = false;
  var ignoreCase = false;
  var useSubstitutionFilters = false;
  for (var i = 0; i < argAry.length; i++) {
    isOption = false;
    if (argAry[i].length >= 2) {
      switch (argAry[i].charAt(0)) {
      case "-":
      case "/":
        switch (argAry[i].charAt(1)) {
        case "e":
        case "i":
          isOption = true;
          regex = true;
          if (argAry[i].indexOf("i") >= 0) {
            ignoreCase = true;
          }
          break;
        case "s":
          useSubstitutionFilters = true;
          break;
        }
        break;
      }
    }
    if (!isOption) {
      patterns.push(argAry[i]);
      if (patterns.length === 1) {
        list.push({"patterns": patterns, "regex": regex, "ignoreCase": ignoreCase});
      } else {
        list[list.length - 1].patterns.push(argAry[i]);
        patterns = new Array();
      }
    }
  }
  if (useSubstitutionFilters) {
    var count = regRead("SubstitutionFilters/Values", 0);
    for (var i = 0; i < count; i++) {
      var ii = "00" + i;
      ii = ii.substring(ii.length - 2);
      if (regRead("SubstitutionFilters/Enabled" + ii, 0)) {
        patterns = new Array();
        patterns.push(regRead("SubstitutionFilters/Pattern" + ii, ""));
        patterns.push(regRead("SubstitutionFilters/Replacement" + ii, ""));
        regex = regRead("SubstitutionFilters/UseRegExp" + ii, 0);
        ignoreCase = !regRead("SubstitutionFilters/CaseSensitive" + ii, 1);
        if (regRead("SubstitutionFilters/MatchWholeWordOnly" + ii, 0)) {
          regex = true;
          patterns[0] = "\\b" + Escape(patterns[0]) + "\\b";
          patterns[1] = Escape(patterns[1]);
        }
        list.push({"patterns": patterns, "regex": regex, "ignoreCase": ignoreCase});
      }
    }
  }
  return list;
}

function Escape(text) {
  var result = "";
  for (var i = 0; i < text.length; i++) {
    var c = text.charAt(i);
    switch (c) {
    case '\\': case '.': case '^': case '$': case '|':
    case '[': case ']': case '(': case ')': case '!':
    case '?': case '*': case '+': case '{': case '}':
        result += '\\';
        break;
    default:
        break;
    }
    result += c;
  }
  return result;
}

function Unescape(text) {
  var result = "";
  var textLen = text.length;
  var i = 0;
  while (i < textLen) {
    var ch = text.charAt(i);
    switch (ch) {
    case "\\":
      if (i < textLen - 1) {
        i++;
        ch = text.charAt(i);
        switch (ch) {
        case "a":
          result += String.fromCharCode(0x07);
          break;
        case "b":
          result += "\b";
          break;
        case "t":
          result += "\t";
          break;
        case "n":
          result += "\n";
          break;
        case "v":
          result += String.fromCharCode(0x0b);
          break;
        case "f":
          result += "\f";
          break;
        case "r":
          result += "\r";
          break;
        case "\\":
          result += "\\";
          break;
        case "x":
          if (i + 2 < textLen) {
            var hexValue = text.substring(i + 1, i + 3);
            try {
              var intValue = parseInt(hexValue, 16);
              result += String.fromCharCode(intValue);
              i += 2;
            } catch (e) {
              result += "\\x";
            }
          } else {
            result += "\\x";
          }
          break;
        default:
          if (!isNaN(ch)) {
            if (ch !== '0') {
              result += '$' + ch;
            } else {
              result += '$&';
            }
          } else {
            result += '\\' + ch;
          }
          break;
        }
      } else {
        result += ch;
      }
      break;
    default:
      result += ch;
      break;
    }
    i++;
  }
  return result;
}

function PluginOnEvent(eventType, obj) {
  mergeApp = obj;
}

// transformation functions
function MakeUpper(Text) {
  return Text.toUpperCase();
}

function MakeLower(Text) {
  return Text.toLowerCase();
}

function ExecFilterCommand(Text) {
  var cmd;
  if (IsFirstArgumentEmpty()) {
    cmd = regRead(REGKEY_PATH + "ExecFilterCommand", "");
    cmd = mergeApp.InputBox("Enter filter command", "ExecFilterCommand", cmd);
    if (cmd !== "") {
      regWrite(REGKEY_PATH + "ExecFilterCommand", cmd, "REG_SZ");
    }
    cmd = ReplaceVariables(cmd);
  } else {
    cmd = pluginArguments;
  }
  if (cmd === "") {
    throw new Error(30001, "Canceled");
  }

  var path = wsh.ExpandEnvironmentStrings("%TEMP%\\_winmerge_addin_temp_.txt");
  try {
    var ts = fso.CreateTextFile(path, true, true);
    ts.Write(Text);
    ts.Close();
  } catch (e) {
    throw new Error(30001, "Failed to create the file '" + path + "':" + e.message);
  }
  var result = "";
  try {
    var exe = wsh.Exec("cmd /c type \"" + path + "\" | " + cmd + " 2>&1");
    while (!exe.StdOut.AtEndOfStream) {
      result += exe.StdOut.ReadLine() + "\r\n";
    }
  } catch (e) {
    throw new Error(30001, "Failed to execute the command '" + cmd + "':" + e.message);
  } finally {
    fso.DeleteFile(path);
  }
  return result;
}

function SelectColumns(Text) {
  var args;
  if (IsFirstArgumentEmpty()) {
    args = regRead(REGKEY_PATH + "SelectColumns", "");
    args = mergeApp.InputBox("Enter the list of column ranges to select (e.g. -3,5-10,30-)\r\nor regular expression:", "SelectColumns", args);
    if (args !== "") {
      regWrite(REGKEY_PATH + "SelectColumns", args, "REG_SZ");
    }
  } else {
    args = pluginArguments;
  }
  if (args === "") {
    throw new Error(30001, "Canceled");
  }
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var parseResult = ParseSelectColumnsLinesArguments(args);
  var rangesOrRegExps = parseResult.rangesOrRegExps;
  var invert = parseResult.invert;
  for (var i = 0; i < lines.length; i++) {
    var line = "";
    for (var j = 0; j < rangesOrRegExps.length; j++) {
      if (rangesOrRegExps[j] instanceof Array) {
        for (var k = 0; k < rangesOrRegExps[j].length; k++) {
          if (rangesOrRegExps[j][k][0] - 1 < lines[i].length) {
            line += lines[i].substring(rangesOrRegExps[j][k][0] - 1, rangesOrRegExps[j][k][1]);
          }
        }
      } else {
        if (!invert) {
          var matches = lines[i].match(rangesOrRegExps[j]);
          if (matches) {
            for (var k = 0; k < matches.length; k++) {
              line += matches[k];
            }
          }
        } else {
          line += lines[i].replace(rangesOrRegExps[j], "");
        }
      }
    }
    lines[i] = line;
  }
  return lines.join(eol);
}

function SelectLines(Text) {
  var args;
  var newlines = new Array();
  if (IsFirstArgumentEmpty()) {
    args = regRead(REGKEY_PATH + "SelectLines", "");
    args = mergeApp.InputBox("Enter the list of line ranges to select (e.g. -3,5-10,30-)\r\nor regular expression:", "SelectLines", args);
    if (args !== "") {
      regWrite(REGKEY_PATH + "SelectLines", args, "REG_SZ");
    }
  } else {
    args = pluginArguments;
  }
  if (args === "") {
    throw new Error(30001, "Canceled");
  }
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var parseResult = ParseSelectColumnsLinesArguments(args);
  var invert = parseResult.invert;
  var rangesOrRegExps = parseResult.rangesOrRegExps;
  for (var i = 0; i < rangesOrRegExps.length; i++) {
    if (rangesOrRegExps[i] instanceof Array) {
      for (var j = 0; j < rangesOrRegExps[i].length; j++) {
        var max = rangesOrRegExps[i][j][1];
        if (max > lines.length) { max = lines.length; }
        for (var k = rangesOrRegExps[i][j][0] - 1; k < max; k++) {
          if (k < lines.length) {
            newlines.push(lines[k]);
          }
        }
      }
    } else {
      for (var j = 0; j < lines.length; j++) {
        var matched = (lines[j].match(rangesOrRegExps[i]) !== null);
        if (invert) { matched = !matched; }
        if (matched) {
          newlines.push(lines[j]);
        }
      }
    }
  }
  if (lines.length > 0) {
    if (lines[lines.length - 1] === "") {
      if (newlines.length > 0) {
        if (newlines[newlines.length - 1] !== "") {
          newlines.push("");
        }
      }
    }
  }
  return newlines.join(eol);
}

function replacei(text, find, replace, ignorecase) {
  if (!ignorecase)
    return text.split(find).join(replace);
  var textLower = text.toLowerCase();
  var findLower = find.toLowerCase();
  var pos = textLower.length;
  while (pos >= 0 && (pos = textLower.lastIndexOf(findLower, pos)) >= 0) {
    text = text.substr(0, pos) + replace + text.substr(pos + findLower.length);
    pos--;
  }
  return text;
}

function ReplaceText(Text) {
  var args;
  var result = Text;
  if (IsFirstArgumentEmpty()) {
    args = regRead(REGKEY_PATH + "Replace", "");
    args = mergeApp.InputBox("Enter the search text and replacement text:", "Replace", args);
    if (args !== "") {
      regWrite(REGKEY_PATH + "Replace", args, "REG_SZ");
    }
  } else {
    args = pluginArguments;
  }
  if (args === "") {
    throw new Error(30001, "Canceled");
  }
  var parseResult = ParseReplaceArguments(args);
  for (var i = 0; i < parseResult.length; i++) {
    var patterns = parseResult[i].patterns;
    var regex = parseResult[i].regex;
    var ignoreCase = parseResult[i].ignoreCase;
    if (regex) {
      var re = new RegExp(patterns[0], ignoreCase ? "gmi" : "gm");
      if (patterns.length > 1) {
        result = result.replace(re, Unescape(patterns[1]));
      } else {
        result = result.replace(re, result, "");
      }
    } else {
      if (patterns.length > 1) {
        result = replacei(result, patterns[0], patterns[1], ignoreCase);
      } else {
        result = replacei(result, patterns[0], "", ignoreCase);
      }
    }
  }
  return result;
}

// port from WinMerge2011
function Tokenize(Text) {
  var pattern;
  if (IsFirstArgumentEmpty()) {
    pattern = regRead(REGKEY_PATH + "Tokenize", "");
    pattern = mergeApp.InputBox("Enter regex to tokenize (e.g. [^\\w]+):", "Tokenize", pattern);
    if (pattern !== "") {
      regWrite(REGKEY_PATH + "Tokenize", pattern, "REG_SZ");
    }
  } else {
    pattern = pluginArguments;
  }
  if (pattern === "") {
    throw new Error(30001, "Canceled");
  }
  var re = new RegExp(pattern, "gi");
  return Text.replace(re, "\r\n");
}

function RemoveDuplicates(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var newlines = new Array();
  var dic = {};
  var lasteol = false;

  if (!lines[lines.length - 1]) {
    lines.pop();
    lasteol = true;
  }
  for (var i = 0, j = 0; i < lines.length; i++) {
    var line = lines[i];
    if (typeof dic[line] === 'undefined') {
      dic[line] = 1;
      newlines.push(line);
    }
  }
  if (lasteol)
    newlines.push("");
  return newlines.join(eol);
}

function CountDuplicates(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var newlines = new Array();
  var dic = {};

  if (!lines[lines.length - 1]) {
    lines.pop();
  }
  
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    if (typeof dic[line] === 'undefined') {
      dic[line] = 1;
      newlines.push(line);
    } else {
      dic[line]++;
    }
  }
  var j = 0;
  for (var line in dic) {
    newlines[j] = line + '\t' + dic[line];
    j++;
  }
  if (eol)
    newlines[j] = "";
  return newlines.join(eol);
}

function ReverseColumns(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var lasteol = false;
  if (!lines[lines.length - 1]) {
    lines.pop();
    lasteol = true;
  }
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    var newline = "";
    for (var j = 0; j < line.length; j++)
      newline += line.substr(line.length - j - 1, 1);
    lines[i] = newline;
  }
  if (lasteol)
    lines.push("");
  return lines.join(eol);
}

function ReverseLines(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  var lasteol = false;
  if (!lines[lines.length - 1]) {
    lines.pop();
    lasteol = true;
  }
  var m = parseInt(lines.length / 2, 10);
  for (var i = 0; i < m; i++) {
    var line = lines[i];
    lines[i] = lines[lines.length - i - 1];
    lines[lines.length - i - 1] = line;
  }
  if (lasteol)
    lines.push("");
  return lines.join(eol);
}

function SortAscending(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);

  if (lines.length == 1) {
    return Text;
  } else if (lines[lines.length - 1] == "") {
    lines.pop();
    return lines.sort().join(eol) + eol;
  } else {
    return lines.sort().join(eol);
  }
}

function SortDescending(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);

  if (lines.length == 1) {
    return Text;
  } else if (lines[lines.length - 1] == "") {
    lines.pop();
    lines.sort(function(a, b) { return a < b ? 1 : -1; });
    return lines.join(eol) + eol;
  } else {
    return lines.sort(function(a, b) { return a < b ? 1 : -1; }).join(eol);
  }
}

function Trim(Text) {
  var eol = Text.match(/\r\n|\n|\r/);
  var lines = Text.split(eol);
  for (var i = 0; i < lines.length; i++) {
    lines[i] = lines[i].replace(/^\s+|\s+$/g, "");
  }
  return lines.join(eol);
}

</script>

</scriptlet>
