// regexer.js
// regex powertoy javascript
// by Gordon Mohr 
// (c) 2005-2008
// offered under the GNU General Public License 

// Holder for 'globals'
var RegexPowertoy = new Object();

function appletInitialized(applet) {

}

function appletStart(applet) {
  // only now is it safe to initialize page
  // attempted initialization before applet is ready fouls things up
  RegexPowertoy.Checker = applet; 
  pageInitialize(); 
}

function qsParams() {
  var qs = location.search.split("?")[1];
  if(!qs) return [];
  var retVal = [];
  $.each(qs.split("&"),function(i,v) { 
    var kv = v.split("=");
    retVal[kv[0]] = unescape(kv[1]);
  });
  return retVal;
}

function pageInitialize() {
  // workaround for IE
  if(document.CheckerReal) {
    document.Checker = document.CheckerReal;
  }
  // resize bottom 
  window.onresize = layout;
  layout();
  
  installRegexHelpMouseovers();
  
  // initial 'global' state
  RegexPowertoy.currentMatchPattern = "";
  RegexPowertoy.currentReplacePattern = "";
  RegexPowertoy.oldSyntax = $("#patternSyntax").val();
  RegexPowertoy.currentHighlightMode = $("#highlightStyle").val();
  RegexPowertoy.stepDelay = 0;
  RegexPowertoy.matcher = null;
  RegexPowertoy.lastStep = -1;
  
  // setup from query-string or leftover form state (after reload)
  var qs = qsParams(); 
  var input = qs["in"];
  if(input) {
    $('#inputEntry').val(unescape(input));
  }
  var pattern = qs["pat"];
  if(pattern) {
    $('#patternEntry').val(unescape(pattern));
  }
  var replace = qs["rep"];
  if(replace) {
    $('#replaceEntry').val(unescape(replace));
  } else {
    $('#replaceEntry').val("");
  }
  var syntax = qs["syn"];
  if(syntax) {
    RegexPowertoy.oldSyntax = syntax;
    $("#patternSyntax").val(syntax);
  }
  var highlight = qs["hl"];
  if(highlight) {
    $("#highlightStyle").val(highlight);
  }
  changeTargetHighlight();
  var delay = qs['anim'];
  if(delay) {
    $('#animateBox').show();
  	updateAnimate(true,delay);
  } 
  buildMatchDisplay();
  changePatternSyntax(); // also does an updatePattern
}

// show help def
function showDd(event) {
  if (!event) var event = window.event;
  var dd;
  var prev;
  var dt = $(event.target).parents("DT");
  var dd = dt.nextAll("DD:first");
  $('#regexDefinition').html(dd.html());
  $('#regexDefinition').show();
  $('#regexDefinitionExplain').hide();
}

// hide help def
function hideDd(event) {
  if (!event) var event = window.event;
  var dt;
  var prev;
  dt = $(event.target).parents("DT");
  prev = dt.nextAll("DD:first");
  $('#regexDefinition').hide();
  $('#regexDefinitionExplain').show();
}

// install mouseover/mouseout handlers for regex help dt/dd
function installRegexHelpMouseovers() {
  var dts = document.getElementsByTagName("DT");
  for(var i = 0; i < dts.length; i++) {
    dts[i].onmouseover = showDd;
    dts[i].onmouseout = hideDd;
  }
}

// resize bottom target input div to match viewport
function layout() {
  var div = $("#inputDiv");
  var newHeight = (getWindowHeight() - div.offset().top - 14);
  if(newHeight < 100) {
    // IE problem
    return; 
  }
  div.height(newHeight+"px");
}

// utility function to get viewport height
function getWindowHeight() {
	var windowHeight=0;
	if (typeof(window.innerHeight)=='number') {
		windowHeight=window.innerHeight;
	}
	else {
		if (document.documentElement&&
			document.documentElement.clientHeight) {
			windowHeight=document.documentElement.clientHeight;
		}
		else {
			if (document.body&&document.body.clientHeight) {
				windowHeight=document.body.clientHeight;
			}
		}
	}
	return windowHeight;
}

// show/hide 'about'
function clickAbout() {
  toggleQBox("#about");
}

// rotate a '?' and show/hide accompanying element
function toggleQBox(name) {
  var q = $(name+"Q");
  var box = $(name+"Box");
  if(q.html() == "?") {
    q.html("&iquest;");
    if(box.hasClass("fader"))
      box.fadeIn(300,layout);
    else if (box.hasClass("slider")) 
      box.slideDown(300,layout);
    else
      box.show(300,layout);
  } else {
    q.html("?");
    if(box.hasClass("fader"))
      box.fadeOut(300,layout);
    else if (box.hasClass("slider"))
      box.slideUp(300,layout);
    else
      box.hide(300,layout); 
  }
  layout();
}

// show/hide regex help
function clickRegexHelp() {
  toggleQBox("#regexHelp");
}

// show-enable/hide-disable 'animate'
function clickAnimate() {
  toggleQBox("#animate");
  updateAnimate($('#animateQ').text()!="?");
}

// update for editted regex; normalize and trigger new highlighting
function updatePattern() {
  updateMatchmark();
  var pat = $('#patternEntry').val();
  var rep = $('#replaceEntry').val();
  switch($("#patternSyntax").val()) {
    case "javaReplaceSyntax":
      // nothing necessary
      break;
    case "javaReplaceLiteralSyntax":
      pat = javaLiteralUnescape(pat);
      rep = javaLiteralUnescape(rep);
      break;
    case "javaLiteralSyntax":
      pat = javaLiteralUnescape(pat);
      rep = "";
      break;
    case "javaSyntax":
      rep = "";
      break;
    case "perlSyntax":
      var patRep = javaRegexFromPerl(pat);
      if(patRep) {
        rep = patRep[1];
        pat = patRep[0];
      } else {
        pat = null;
        rep = null;
      }
      // TODO: global?
      break;
    default:
      // do nothing;
  }
  if (pat == RegexPowertoy.currentMatchPattern && rep == RegexPowertoy.currentReplacePattern) {
    return; // avoid extraneous updates
  }
  RegexPowertoy.currentMatchPattern = pat;
  RegexPowertoy.currentReplacePattern = rep;
  if(pat == null) {
    // error previously reported
    RegexPowertoy.matcher = null; // cancel any match in progress
    return;
  }
  // load applet with pattern
  var err = RegexPowertoy.Checker.checkPattern(pat);

  if(err!=null) {
    indicateError(err);
    RegexPowertoy.matcher = null; // cancel any match in progress
    return;
  }
  restartHighlight();
}

// clear all matches & rebuild unmarked-up display area
function clearMatches() {
  clearMatchDetails();
  buildMatchDisplay();
  $('#advisoryBar').html("&nbsp;");
  $('#matchesBar').html("");
  $('#probesBar').html("");
  $('#timingBar').html("");
  return;
}

// reseet the count of matches/probes/time to blank
function resetStatusLine() {
  $('#matchesBar').html("");
  $('#probesBar').html("");
  $('#errorBar').html("");
  $('#timingBar').html("");
  $('#advisoryBar').html("&nbsp;");
}

// select a matched group in the detail display
function selectGroup(event) {
  deselectGroup();
  if (!event) var event = window.event;
  var groupSpan = $(event.target).parents(".group:first");
  $('#selectedMatch').data("selectedGroup",groupSpan);
  $('#groupDetail').html(groupSpan.attr("title"));
  groupSpan.addClass("selected");
  dontBubble(event);
}

// deselect a group in the detail display
function deselectGroup() {
  var selectedGroup = $('#selectedMatch').data("selectedGroup");
  if(selectedGroup) {
    selectedGroup.removeClass("selected");
    $('#selectedMatch').removeData("selectedGroup");
    $('#groupDetail').html("");
  }
}

// build the detail display shown when clicking a specific match
function buildDetailDisplay(result) {
  var selectedMatch = $('#selectedMatch');
  buildCellDisplay(selectedMatch,$('#inputEntry').val().substr(result[0][0],result[0][1]-result[0][0]),"dc","dcell");
  for(var g = 1; g < result.length; g++) {
    if(result[g][0]==-1) {
      // group not matched
      continue;
    }
    var groupSpan = $(document.createElement('A'));
    groupSpan.addClass("encl"+result[g][2]);
    groupSpan.addClass("group");
    groupSpan.attr("title","group #"+g+": "+result[g][0]+".."+result[g][1]);
    var groupId = "g"+g;
    groupSpan.attr("id",groupId);
    insertAround(groupSpan,
                 "#dc",
                 result[g][0]-result[0][0],
                 result[g][1]-result[0][0]);
    
    groupSpan = $("#"+groupId); // use DOM'd one
    // add parens for all but 0 group
    if(g!=0) {
      var openParen = $(document.createElement('SPAN'));
      openParen.text("(");
      openParen.addClass("paren");
      var closeParen = $(document.createElement('SPAN'));
      closeParen.text(")");
      closeParen.addClass("paren");
      groupSpan.prepend(openParen);
      groupSpan.append(closeParen);
    }
    groupSpan.data("groupNumber",g);
    groupSpan.click(function(e) {selectGroup(e)});
    
  }
  // add gray start/end index indicators
  selectedMatch.prepend(indexSpan(result[0][0]));
  selectedMatch.append(indexSpan(result[0][1]));
  return;
  /**
  // fill replace area
  var selectedReplace = $('selectedReplace');
  var replaceText;
  try {
    replaceText = result.replace(RegexPowertoy.currentReplacePattern);
  } catch (e) {
//    indicateError("replace pattern syntax error");
    replaceText = RegexPowertoy.currentReplacePattern;
  }
  selectedReplace.innerHTML = replaceText;
  // add quote-brackets
  var openAquo = document.createElement('SPAN');
  openAquo.innerHTML="&laquo;";
  Element.addClassName(openAquo,"paren");
  var closeAquo = document.createElement('SPAN');
  closeAquo.innerHTML="&raquo;";
  Element.addClassName(closeAquo,"paren");
  selectedReplace.insertBefore(openAquo,selectedReplace.firstChild);
  selectedReplace.appendChild(closeAquo);
  /**/
}

// create a small index-number span
function indexSpan(number) {
  var indexSpan = $(document.createElement('SPAN'));
  indexSpan.text(number);
  indexSpan.addClass('detailIndex');
  return indexSpan;
}

// build the main display of matched target text
function buildMatchDisplay() {
  var targetText = $.browser.msie 
  					 ? $('#inputEntry').text() 
  					 : $('#inputEntry').val();
  buildCellDisplay('#matchDisplay',targetText,'c');
}

// build a representation of supplied 'content' inside 'elementId'
// where every character is its own addressable cell (SPAN)
function buildCellDisplay(elementId, content, cellPrefix, cellClass) {
  var element = $(elementId);
  element.empty();
  var i;
  // head empty span
  var cell = $(document.createElement("SPAN"));
  cell.attr("id", "-1");
  if(cellClass) {
    cell.addClass(cellClass);
  }
  element.append(cell);
  for(i=0;i<content.length;i++) {
     // actual character
     var cell;
     cell = $(document.createElement("SPAN"));
     cell.attr("id", cellPrefix+i);
     cell.text(content.charAt(i));
     if(cellClass) {
       cell.addClass(cellClass);
     }
     element.append(cell);
  }
  // tail empty span
  var cell = $(document.createElement("SPAN"));
  cell.attr("id", cellPrefix+i);
  if(cellClass) {
    cell.addClass(cellClass);
  }
  element.append(cell);
}

// let 'esc' end target input entry
function inputEntryKeyup(event) {
  if (!event) var event = window.event;
  if(event.which == 27) {
    $("#inputEntry").blur();
  }
}

// switch from entry to match-display
function toggleToMatchDisplay() {
  $("#inputEntry").hide();
  buildMatchDisplay();
  $("#matchDisplay").show();
  highlightMatches();
}

// switch to target input entry (as after double-click)
function toggleToInputEntry() {
  clearMatchDetails();
  $("#matchDisplay").hide();
  $("#inputEntry").height($("#inputDiv").height()); // IE workaround
  $("#inputEntry").show();
  $("#inputEntry").focus();
  resetStatusLine();
}

// begin the match-highlight process from start
function restartHighlight() {
   clearMatches();
   highlightMatches();
}

// start find/animate highlighting
function highlightMatches() {
  RegexPowertoy.matchCount = 0;
  RegexPowertoy.stepbuffer = [];
  RegexPowertoy.findbuffer = [];
  RegexPowertoy.startTime = (new Date()).getTime();
  RegexPowertoy.lastMatchEnd = 0;
  var inputValue = $('#inputEntry').val();
  var matchWasInProgress = (RegexPowertoy.matcher != null); 
  RegexPowertoy.matcher = RegexPowertoy.Checker.getMatcher(inputValue);
  RegexPowertoy.stepTally = 0;
  reportMatchesSoFar(RegexPowertoy.matchCount);
  resetStatusLine();
  $('#probesBar').text("(finding)");
  if(RegexPowertoy.stepDelay<0||!matchWasInProgress) {
    setStep();
  }
}

// arrange for next find/animate step to occur (asap or after delay)
function setStep() {
  // trigger next step to happen
  if(RegexPowertoy.stepDelay==0) {
    // clear step buffer
    while(RegexPowertoy.stepbuffer.length>0) {
       animateInner();
    }
    setTimeout('findStep()',0);
    return;
  } else {
    // clear find buffer
    while(RegexPowertoy.findbuffer.length>0) {
       findInner();
    }
    setTimeout('animateStep()',RegexPowertoy.stepDelay);
  }
}

// JS buffer of next N found but not yet displayed matches
RegexPowertoy.findbuffer = [];
function getFind(matcher) {
  if(RegexPowertoy.findbuffer.length==0) {
  	fillBufferFinds(matcher); 
  } 
  if(!RegexPowertoy.findbuffer) return RegexPowertoy.findbuffer;
  return ieArrayFix(RegexPowertoy.findbuffer.shift());
}

function ieArrayFix(array) {
  if (!array) return array; 
  if (!array[array.length-1]) return array.slice(0,array.length-1);
  return array;
}

// fill the findbuffer (Java applet callout) 
function fillBufferFinds(matcher) {
  RegexPowertoy.findbuffer = eval(""+matcher.jsonFind(20)); ///JAVA
}

// skip to next found match & highlight it
function findStep() {
  findInner();
  if(RegexPowertoy.matcher!=null) {
    setStep();
  }
}

// highlight next found match, if any 
function findInner() {
  var localMatcher = RegexPowertoy.matcher; // in case reassigned in background
  if(localMatcher==null) return;
  // clear animate in progress, if any
  if(RegexPowertoy.lastStep>-1) {
    $("#c"+RegexPowertoy.lastStep).removeClass('probeSpan');
    RegexPowertoy.lastStep=-1;
  }
  var found = getFind(localMatcher); 
  if(found) { 
    RegexPowertoy.matchCount++;
    if(!highlightMatch(eval(found),RegexPowertoy.matchCount)) {
//      alert("err, restarting highlight");
      restartHighlight();
    }
  } else {
    reportMatches(RegexPowertoy.matchCount);
  	RegexPowertoy.matcher = null;
  	return;
  }
}

// highlight the match in the given result array, as the 'index'th match
function highlightMatch(result,index) {
  var start = result[0][0];
  var end = result[0][1];
  
  highlightSplit(index,start);

  var matchId = "match"+index
  var matchSpan = "<SPAN ID='"+matchId+"' CLASS='match'></SPAN>";
  insertAround(matchSpan,"#c",start,end);
  
  matchSpan = $("#"+matchId); // use version in DOM
  matchSpan.data("matchResult",result); 
  matchSpan.click(function(e) {reportMatchDetails(matchSpan);dontBubble(e);});
  
  var replaceText = doReplace(result);
  matchSpan.after("<SPAN ID='replace"+index+"' CLASS='replace'>"+replaceText+"</SPAN>");
  
  RegexPowertoy.lastMatchEnd = end;

  return true;
}

// get the string represented by the rangeArry from the source text
function getMatch(source,rangeArray) {
  if(!rangeArray || rangeArray[0]==-1) {
    return "";
  }
  return source.substr(rangeArray[0],rangeArray[1]-rangeArray[0]);
}

// perform a replace with backreference substitution
function doReplace(result) {
  var segs = RegexPowertoy.currentReplacePattern.split("$");
  var source = $("#inputEntry").val();
  for(var i = 0; i< segs.length; i++) {
    if(i>0 && segs[i-1].charAt(segs[i-1].length-1)=='\\') {
      // escaped
      segs[i] = "$"+segs[i];
      continue;
    }
    var test = /^(\d+)/.exec(segs[i]);
    if(test) {
      var backref = test[1]; // number
      segs[i]=segs[i].replace(/^(\d+)/,getMatch(source,result[backref]));
    } else if (i>0) {
      segs[i] = "$"+segs[i];
    }
  }
  return segs.join("").split("\\").join("");
}

// insert the 'around' element around range of elements 
// running from id '[prefix][start]' to '[prefix][end]'
function insertAround(around,prefix,start,end) {
  var range = selectRange(prefix,start,end);
  if(range.size()==0) {
    $(prefix+start).before(around);
  } else {
    range.wrapAll(around);
  }
}

// select range of elements running from id 
// '[prefix][start]' to '[prefix][end]'
function selectRange(prefix,start,end) {
  if(start==end) return $([]);
  var selector = "";
  for(var i = start; i<end; i++) {
    selector += prefix+i+",";
  }
  return $(selector);
}

// apply the 'split' SPAN around unmatched range between matches
function highlightSplit(index,to) {
  var splitSpan = "<SPAN ID='split"+index+"' CLASS='split'></SPAN>";
  insertAround(splitSpan,"#c",RegexPowertoy.lastMatchEnd,to);
}

// prevent event from bubbling
function dontBubble(e) {
  if (!e) var e = window.event;
  e.cancelBubble = true; 
  if (e.stopPropagation) e.stopPropagation();
}

// JS buffer of next N reported-from-Java steps/matches not yet displayed
RegexPowertoy.stepbuffer = [];
function getStep(matcher) {
  if(RegexPowertoy.stepbuffer.length==0) {
  	fillBufferSteps(matcher); 
  }
  return RegexPowertoy.stepbuffer.shift();
}

// fill stepbuffer  -- Java callout
function fillBufferSteps(matcher) {
  RegexPowertoy.stepbuffer = eval(matcher.jsonStepFind(2000));
}

// skip to next probe or find, updating cursor highlighting as necessary
function animateStep() {
	animateInner();
    if(RegexPowertoy.matcher!=null && RegexPowertoy.stepDelay>-1) {
      setStep();
    }
}

// animate next step/match
function animateInner() {
    var localMatcher = RegexPowertoy.matcher; // in case reassigned in background
    if(localMatcher==null) return;
    var step = getStep(localMatcher);
    if(RegexPowertoy.lastStep>-1) {
      $("#c"+RegexPowertoy.lastStep).removeClass('probeSpan');
    }
    if(step==null) {
  		reportMatches(RegexPowertoy.matchCount);
  		RegexPowertoy.matcher = null;
  		return;
    }
    if(!(step>-1)) {
        //console.log(typeof step.group() == "string");
        // match found
    	RegexPowertoy.matchCount++;
    	if(!highlightMatch(step,RegexPowertoy.matchCount)) {
    	  restartHighlight();
    	}
    	reportMatchesSoFar(RegexPowertoy.matchCount);
    } else {
    	// nothing for now
    	RegexPowertoy.stepTally++;
    	if(RegexPowertoy.stepDelay!=0) {
    	   // only animate for nonzero delays
    	   $("#c"+step).addClass('probeSpan');
    	   RegexPowertoy.lastStep = step;
    	} else {
    	  RegexPowertoy.lastStep = -1;
    	}
    }
}

// update status line with final values
function reportMatches(matches) {
  // highlight last tail split
  highlightSplit(matches+1,$("#inputEntry").val().length);
  $('#matchesBar').text(matches+" matches");
  RegexPowertoy.stepTally = RegexPowertoy.matcher.getStepTally()
  $('#probesBar').text(
    RegexPowertoy.stepTally+" probes (finished)");
  var endTime = (new Date()).getTime();
  $('#timingBar').text(endTime-RegexPowertoy.startTime+"ms");
  $('#errorBar').text("");
  $('#advisoryBar').text("");
}

// update status line with interim data
function reportMatchesSoFar(matches) {
  $('#matchesBar').text(matches+" matches");
  $('#probesBar').text(
    RegexPowertoy.stepTally+" probes (in progress)");
  $('#timingBar').text("");
  $('#errorBar').text("");
  $('#advisoryBar').text("");
}

// show an error in status area 
function indicateError(exception) {
  var message;
  if(typeof exception == "string") {
    message = exception;
  } else {
    message = exception.getDescription();
  }
  $('#errorBar').text(message);
  clearMatches();
//  index = exception.getIndex();
//  if(index>0) {
//    highlight = $('patternEntry').value;
//    //highlight = highlight.replace(/[^\S]/g,'&nbsp;');
//    highlight = highlight.substring(0,index) + "<SPAN CLASS='errSpan'> </SPAN>";
//    $('patternHighlight').innerHTML = highlight;
//  }
}

// set the animation delay, if any
// -1 is stopped; 0 is no delay (no animation); other value is ms delay between
// steps
function setStepDelay(d) {
  var oldDelay = RegexPowertoy.stepDelay;
  RegexPowertoy.stepDelay=d;
  if(oldDelay<0) setStep(); // restart
}

// change the highlighting mode of the target input display
function changeTargetHighlight() {
  var inputDiv = $("#inputDiv");
  var newHighlightMode = $('#highlightStyle').val();
//  inputDiv.removeClass(RegexPowertoy.currentHighlightMode);
  inputDiv.attr("class","");
  inputDiv.addClass(newHighlightMode);
  RegexPowertoy.currentHighlightMode = newHighlightMode;
  updateMatchmark();
}

// change pattern syntax: change page display classes AND
// convert input between syntaxes
function changePatternSyntax() {
    var newSyntax = $("#patternSyntax").val();
    var patternDiv = $("#patternDiv");
    patternDiv.removeClass(RegexPowertoy.oldSyntax);
    patternDiv.addClass(newSyntax);
    var nativeRegex;
    var replace;
    switch (RegexPowertoy.oldSyntax) {
      case "javaSyntax":
        nativeRegex = $('#patternEntry').val();
        replace = "";
        break;
      case "javaLiteralSyntax":
        nativeRegex = javaLiteralUnescape($('#patternEntry').val());
        replace = "";
        break;
      case "javaReplaceSyntax":
        nativeRegex = $('#patternEntry').val();
        replace = $('#replaceEntry').val();
        break;
      case "javaReplaceLiteralSyntax":
        nativeRegex = javaLiteralUnescape($('#patternEntry').val());
        replace = javaLiteralUnescape($('#replaceEntry').val());
        break;
      case "perlSyntax":
        var patRep = javaRegexFromPerl($('#patternEntry').val());
        if(patRep) {
          nativeRegex = patRep[0];
          replace = patRep[1];
        } else {
          // fallback for invalid pattern
          nativeRegex = $('#patternEntry').val();
          replace = "";
        }
        break;
      default:
        alert("powertoy bug: undefined source syntax: "+RegexPowertoy.oldSyntax);
        // should never get here
    }
    switch (newSyntax) {
      case "javaSyntax":
        $('#patternEntry').val(nativeRegex);
        $('#replaceEntry').val("");
        break;
      case "javaLiteralSyntax":
        $('#patternEntry').val(javaLiteralEscape(nativeRegex));
        $('#replaceEntry').val("");
        break;
      case "javaReplaceSyntax":
        $('#patternEntry').val(nativeRegex);
        $('#replaceEntry').val(replace);
        break;
      case "javaReplaceLiteralSyntax":
        $('#patternEntry').val(javaLiteralEscape(nativeRegex));
        $('#replaceEntry').val(javaLiteralEscape(replace));
        break;
      case "perlSyntax":
        $('#patternEntry').val(perlRegexFromJava(nativeRegex,replace));
        break;
      default:
        alert("powertoy bug: undefined target syntax");
        // should never get here
    }
    RegexPowertoy.oldSyntax = newSyntax;
    updatePattern();
}

// interpret a perl regex ('//','m//','s///') to java patterns
// returns a 2-element Array, first element is matchPattern,
// second is replacePattern (if any)
function javaRegexFromPerl(perlRegex) {
  var matchExtractor = new RegExp("^((m(\\S))|(\\S))([\\s\\S]*)(\\3\\4)([-\\w]*)\\s*$");
  var extract = matchExtractor.exec(perlRegex);
  var javaRegex;
  var flags;
  var javaReplace = "";
  if(extract) {
    flags = extract[7]
  } else {
    var replaceExtractor = new RegExp("^((s(\\S))())([\\s\\S]*)(\\3\\4)([\\s\\S]*)(\\3\\4)([-\\w]*)\\s*$");
    extract = replaceExtractor.exec(perlRegex);
    if(!extract) {
      indicateError("Pattern syntax error");
      return null;
    }
    javaReplace = extract[7];
    flags = extract[9];
  }
  javaRegex = extract[5];
  flags = flags.replace(/g/g,""); 
  if(flags.length>0) {
    javaRegex = "(?"+flags+")"+javaRegex;
  }
  return new Array(javaRegex,javaReplace);
}

// create a perl-style regex from the given java matchPattern and replacePattern
// 'g' flag is always added
function perlRegexFromJava(javaRegex,javaReplace) {
  // TODO: escape slashes
  var extract;
  // move prefix flags to suffix
  var flags = "";
  while(extract = /^\(\?([idmsux]+)\)([\w\W]*)/.exec(javaRegex)) {
    flags += extract[1];
    javaRegex = extract[2];
  }
  if(javaReplace) {
    return "s/"+javaRegex+"/"+javaReplace+"/g"+flags;
  } else {
    return "m/"+javaRegex+"/g"+flags;
  }
}

// perform a single manual step
function manualStep() {
    var elem = $('#paceForm');
    $("input[name=speed]").val(["s-1"]);
    setStepDelay(-1);
	if(RegexPowertoy.matcher==null) {
	  restartHighlight();
	} else {
	  reportMatchesSoFar(RegexPowertoy.matchCount);
	}
}

// update animation settings
function updateAnimate(enabled,speed) {
  var elem = $('#animateBox');
  if(enabled) {
    $('#animateQ').html("&iquest;");
    elem = $('#paceForm');
    var testFn;
    if(speed) {
      $("input[name=speed]").val(["s"+speed]); 
      setStepDelay(speed);
    } else {
      setStepDelay($("input[name=speed]:checked").val().substr(1));
    }
  } else {
    $('#animateQ').text("?");
    setStepDelay(0,'');
  }
  updateMatchmark();
}

// clear selected match
function clearMatchDetails() {
  deselectGroup();
  var currentMatchSpan = $('#selectedMatch').data("currentMatchSpan");
  if(currentMatchSpan) {
    $('#selectedMatch').removeData("currentMatchSpan");
    currentMatchSpan.removeClass('selected');
  }
  $('#matchDetail').hide();
}

// display details for the given matchSpan
function reportMatchDetails(matchSpan) {
  clearMatchDetails();
  var result = matchSpan.data("matchResult");
  matchSpan.addClass("selected");
  buildDetailDisplay(result);
  $('#selectedMatch').data("currentMatchSpan",matchSpan);
  $('#matchDetail').show();
}

// update matchmark HREF
function updateMatchmark() {
  $("#matchmark").attr("href",buildMatchmark());
}
  
// send browser to URL capturing all current settings ('matchmark')
function clickMatchmark() {
  var mark = buildMatchmark();
  if(mark.length>2083) {
    if(!confirm("Pattern plus target may be too long to encode in URL, "+
              "resulting in truncation on bookmarked page. Continue?")) {
       return;
    }
  }
  document.location.href = mark; 
}

// build URL which restores all current settings ('matchmark')
function buildMatchmark() {
   var mark = document.location.href;
   if(mark.indexOf("?")>=0) {
     mark = mark.substring(0,mark.indexOf("?"));
   }
   mark += "?pat="+escape($('#patternEntry').val());
   if($('#patternSyntax')!="perlSyntax") {
     mark += "&syn="+$('#patternSyntax').val();
   }
   if($("#animateQ").innerHTML!="?") {
     mark += "&anim="+RegexPowertoy.stepDelay;
   }
   if($('#highlightStyle').val()!='highlightMatches') {
     mark += "&hl="+$('#highlightStyle').val();
   }
   if($('#patternEntry').val().length>0) {
     mark += "&rep="+escape($('#replaceEntry').val());
   }
   mark += "&in="+escape($('#inputEntry').val());
   return mark;
}