text: (function(global) {
      
      var EPUBcfi = {};
  
      EPUBcfi.Parser = (function(){
    /*
     * Generated by PEG.js 0.7.0.
     *
     * http://pegjs.majda.cz/
     */
    
    function quote(s) {
      /*
       * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
       * string literal except for the closing quote character, backslash,
       * carriage return, line separator, paragraph separator, and line feed.
       * Any character may appear in the form of an escape sequence.
       *
       * For portability, we also escape escape all control and non-ASCII
       * characters. Note that "\0" and "\v" escape sequences are not used
       * because JSHint does not like the first and IE the second.
       */
       return '"' + s
        .replace(/\\/g, '\\\\')  // backslash
        .replace(/"/g, '\\"')    // closing quote character
        .replace(/\x08/g, '\\b') // backspace
        .replace(/\t/g, '\\t')   // horizontal tab
        .replace(/\n/g, '\\n')   // line feed
        .replace(/\f/g, '\\f')   // form feed
        .replace(/\r/g, '\\r')   // carriage return
        .replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape)
        + '"';
    }
    
    var result = {
      /*
       * Parses the input with a generated parser. If the parsing is successfull,
       * returns a value explicitly or implicitly specified by the grammar from
       * which the parser was generated (see |PEG.buildParser|). If the parsing is
       * unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
       */
      parse: function(input, startRule) {
        var parseFunctions = {
          "fragment": parse_fragment,
          "path": parse_path,
          "local_path": parse_local_path,
          "indexStep": parse_indexStep,
          "indirectionStep": parse_indirectionStep,
          "terminus": parse_terminus,
          "idAssertion": parse_idAssertion,
          "textLocationAssertion": parse_textLocationAssertion,
          "parameter": parse_parameter,
          "csv": parse_csv,
          "valueNoSpace": parse_valueNoSpace,
          "value": parse_value,
          "escapedSpecialChars": parse_escapedSpecialChars,
          "number": parse_number,
          "integer": parse_integer,
          "space": parse_space,
          "circumflex": parse_circumflex,
          "doubleQuote": parse_doubleQuote,
          "squareBracket": parse_squareBracket,
          "parentheses": parse_parentheses,
          "comma": parse_comma,
          "semicolon": parse_semicolon,
          "equal": parse_equal,
          "character": parse_character
        };
        
        if (startRule !== undefined) {
          if (parseFunctions[startRule] === undefined) {
            throw new Error("Invalid rule name: " + quote(startRule) + ".");
          }
        } else {
          startRule = "fragment";
        }
        
        var pos = 0;
        var reportFailures = 0;
        var rightmostFailuresPos = 0;
        var rightmostFailuresExpected = [];
        
        function padLeft(input, padding, length) {
          var result = input;
          
          var padLength = length - input.length;
          for (var i = 0; i < padLength; i++) {
            result = padding + result;
          }
          
          return result;
        }
        
        function escape(ch) {
          var charCode = ch.charCodeAt(0);
          var escapeChar;
          var length;
          
          if (charCode <= 0xFF) {
            escapeChar = 'x';
            length = 2;
          } else {
            escapeChar = 'u';
            length = 4;
          }
          
          return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
        }
        
        function matchFailed(failure) {
          if (pos < rightmostFailuresPos) {
            return;
          }
          
          if (pos > rightmostFailuresPos) {
            rightmostFailuresPos = pos;
            rightmostFailuresExpected = [];
          }
          
          rightmostFailuresExpected.push(failure);
        }
        
        function parse_fragment() {
          var result0, result1, result2;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          if (input.substr(pos, 8) === "epubcfi(") {
            result0 = "epubcfi(";
            pos += 8;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"epubcfi(\"");
            }
          }
          if (result0 !== null) {
            result1 = parse_path();
            if (result1 !== null) {
              if (input.charCodeAt(pos) === 41) {
                result2 = ")";
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("\")\"");
                }
              }
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, pathVal) { 
                  
                  return { type:"CFIAST", cfiString:pathVal }; 
              })(pos0, result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_path() {
          var result0, result1;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          result0 = parse_indexStep();
          if (result0 !== null) {
            result1 = parse_local_path();
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, stepVal, localPathVal) { 
          
                  return { type:"cfiString", path:stepVal, localPath:localPathVal }; 
              })(pos0, result0[0], result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_local_path() {
          var result0, result1;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          result1 = parse_indexStep();
          if (result1 === null) {
            result1 = parse_indirectionStep();
          }
          if (result1 !== null) {
            result0 = [];
            while (result1 !== null) {
              result0.push(result1);
              result1 = parse_indexStep();
              if (result1 === null) {
                result1 = parse_indirectionStep();
              }
            }
          } else {
            result0 = null;
          }
          if (result0 !== null) {
            result1 = parse_terminus();
            result1 = result1 !== null ? result1 : "";
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, localPathStepVal, termStepVal) { 
          
                  return { steps:localPathStepVal, termStep:termStepVal }; 
              })(pos0, result0[0], result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_indexStep() {
          var result0, result1, result2, result3, result4;
          var pos0, pos1, pos2;
          
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 47) {
            result0 = "/";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"/\"");
            }
          }
          if (result0 !== null) {
            result1 = parse_integer();
            if (result1 !== null) {
              pos2 = pos;
              if (input.charCodeAt(pos) === 91) {
                result2 = "[";
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("\"[\"");
                }
              }
              if (result2 !== null) {
                result3 = parse_idAssertion();
                if (result3 !== null) {
                  if (input.charCodeAt(pos) === 93) {
                    result4 = "]";
                    pos++;
                  } else {
                    result4 = null;
                    if (reportFailures === 0) {
                      matchFailed("\"]\"");
                    }
                  }
                  if (result4 !== null) {
                    result2 = [result2, result3, result4];
                  } else {
                    result2 = null;
                    pos = pos2;
                  }
                } else {
                  result2 = null;
                  pos = pos2;
                }
              } else {
                result2 = null;
                pos = pos2;
              }
              result2 = result2 !== null ? result2 : "";
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, stepLengthVal, assertVal) { 
          
                  return { type:"indexStep", stepLength:stepLengthVal, idAssertion:assertVal[1] };
              })(pos0, result0[1], result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_indirectionStep() {
          var result0, result1, result2, result3, result4;
          var pos0, pos1, pos2;
          
          pos0 = pos;
          pos1 = pos;
          if (input.substr(pos, 2) === "!/") {
            result0 = "!/";
            pos += 2;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"!/\"");
            }
          }
          if (result0 !== null) {
            result1 = parse_integer();
            if (result1 !== null) {
              pos2 = pos;
              if (input.charCodeAt(pos) === 91) {
                result2 = "[";
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("\"[\"");
                }
              }
              if (result2 !== null) {
                result3 = parse_idAssertion();
                if (result3 !== null) {
                  if (input.charCodeAt(pos) === 93) {
                    result4 = "]";
                    pos++;
                  } else {
                    result4 = null;
                    if (reportFailures === 0) {
                      matchFailed("\"]\"");
                    }
                  }
                  if (result4 !== null) {
                    result2 = [result2, result3, result4];
                  } else {
                    result2 = null;
                    pos = pos2;
                  }
                } else {
                  result2 = null;
                  pos = pos2;
                }
              } else {
                result2 = null;
                pos = pos2;
              }
              result2 = result2 !== null ? result2 : "";
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, stepLengthVal, assertVal) { 
          
                  return { type:"indirectionStep", stepLength:stepLengthVal, idAssertion:assertVal[1] };
              })(pos0, result0[1], result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_terminus() {
          var result0, result1, result2, result3, result4;
          var pos0, pos1, pos2;
          
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 58) {
            result0 = ":";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\":\"");
            }
          }
          if (result0 !== null) {
            result1 = parse_integer();
            if (result1 !== null) {
              pos2 = pos;
              if (input.charCodeAt(pos) === 91) {
                result2 = "[";
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("\"[\"");
                }
              }
              if (result2 !== null) {
                result3 = parse_textLocationAssertion();
                if (result3 !== null) {
                  if (input.charCodeAt(pos) === 93) {
                    result4 = "]";
                    pos++;
                  } else {
                    result4 = null;
                    if (reportFailures === 0) {
                      matchFailed("\"]\"");
                    }
                  }
                  if (result4 !== null) {
                    result2 = [result2, result3, result4];
                  } else {
                    result2 = null;
                    pos = pos2;
                  }
                } else {
                  result2 = null;
                  pos = pos2;
                }
              } else {
                result2 = null;
                pos = pos2;
              }
              result2 = result2 !== null ? result2 : "";
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, textOffsetValue, textLocAssertVal) { 
          
                  return { type:"textTerminus", offsetValue:textOffsetValue, textAssertion:textLocAssertVal[1] };
              })(pos0, result0[1], result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_idAssertion() {
          var result0;
          var pos0;
          
          pos0 = pos;
          result0 = parse_value();
          if (result0 !== null) {
            result0 = (function(offset, idVal) { 
          
                  return idVal; 
              })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_textLocationAssertion() {
          var result0, result1;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          result0 = parse_csv();
          result0 = result0 !== null ? result0 : "";
          if (result0 !== null) {
            result1 = parse_parameter();
            result1 = result1 !== null ? result1 : "";
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, csvVal, paramVal) { 
          
                  return { type:"textLocationAssertion", csv:csvVal, parameter:paramVal }; 
              })(pos0, result0[0], result0[1]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_parameter() {
          var result0, result1, result2, result3;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          if (input.charCodeAt(pos) === 59) {
            result0 = ";";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\";\"");
            }
          }
          if (result0 !== null) {
            result1 = parse_valueNoSpace();
            if (result1 !== null) {
              if (input.charCodeAt(pos) === 61) {
                result2 = "=";
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("\"=\"");
                }
              }
              if (result2 !== null) {
                result3 = parse_valueNoSpace();
                if (result3 !== null) {
                  result0 = [result0, result1, result2, result3];
                } else {
                  result0 = null;
                  pos = pos1;
                }
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, paramLHSVal, paramRHSVal) { 
          
                  return { type:"parameter", LHSValue:paramLHSVal, RHSValue:paramRHSVal }; 
              })(pos0, result0[1], result0[3]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_csv() {
          var result0, result1, result2;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          result0 = parse_value();
          result0 = result0 !== null ? result0 : "";
          if (result0 !== null) {
            if (input.charCodeAt(pos) === 44) {
              result1 = ",";
              pos++;
            } else {
              result1 = null;
              if (reportFailures === 0) {
                matchFailed("\",\"");
              }
            }
            if (result1 !== null) {
              result2 = parse_value();
              result2 = result2 !== null ? result2 : "";
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, preAssertionVal, postAssertionVal) { 
          
                  return { type:"csv", preAssertion:preAssertionVal, postAssertion:postAssertionVal }; 
              })(pos0, result0[0], result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_valueNoSpace() {
          var result0, result1;
          var pos0;
          
          pos0 = pos;
          result1 = parse_escapedSpecialChars();
          if (result1 === null) {
            result1 = parse_character();
          }
          if (result1 !== null) {
            result0 = [];
            while (result1 !== null) {
              result0.push(result1);
              result1 = parse_escapedSpecialChars();
              if (result1 === null) {
                result1 = parse_character();
              }
            }
          } else {
            result0 = null;
          }
          if (result0 !== null) {
            result0 = (function(offset, stringVal) { 
          
                  return stringVal.join(''); 
              })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_value() {
          var result0, result1;
          var pos0;
          
          pos0 = pos;
          result1 = parse_escapedSpecialChars();
          if (result1 === null) {
            result1 = parse_character();
            if (result1 === null) {
              result1 = parse_space();
            }
          }
          if (result1 !== null) {
            result0 = [];
            while (result1 !== null) {
              result0.push(result1);
              result1 = parse_escapedSpecialChars();
              if (result1 === null) {
                result1 = parse_character();
                if (result1 === null) {
                  result1 = parse_space();
                }
              }
            }
          } else {
            result0 = null;
          }
          if (result0 !== null) {
            result0 = (function(offset, stringVal) { 
          
                  return stringVal.join(''); 
              })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_escapedSpecialChars() {
          var result0, result1;
          var pos0, pos1;
          
          pos0 = pos;
          pos1 = pos;
          result0 = parse_circumflex();
          if (result0 !== null) {
            result1 = parse_circumflex();
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 === null) {
            pos1 = pos;
            result0 = parse_circumflex();
            if (result0 !== null) {
              result1 = parse_squareBracket();
              if (result1 !== null) {
                result0 = [result0, result1];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
            if (result0 === null) {
              pos1 = pos;
              result0 = parse_circumflex();
              if (result0 !== null) {
                result1 = parse_parentheses();
                if (result1 !== null) {
                  result0 = [result0, result1];
                } else {
                  result0 = null;
                  pos = pos1;
                }
              } else {
                result0 = null;
                pos = pos1;
              }
              if (result0 === null) {
                pos1 = pos;
                result0 = parse_circumflex();
                if (result0 !== null) {
                  result1 = parse_comma();
                  if (result1 !== null) {
                    result0 = [result0, result1];
                  } else {
                    result0 = null;
                    pos = pos1;
                  }
                } else {
                  result0 = null;
                  pos = pos1;
                }
                if (result0 === null) {
                  pos1 = pos;
                  result0 = parse_circumflex();
                  if (result0 !== null) {
                    result1 = parse_semicolon();
                    if (result1 !== null) {
                      result0 = [result0, result1];
                    } else {
                      result0 = null;
                      pos = pos1;
                    }
                  } else {
                    result0 = null;
                    pos = pos1;
                  }
                  if (result0 === null) {
                    pos1 = pos;
                    result0 = parse_circumflex();
                    if (result0 !== null) {
                      result1 = parse_equal();
                      if (result1 !== null) {
                        result0 = [result0, result1];
                      } else {
                        result0 = null;
                        pos = pos1;
                      }
                    } else {
                      result0 = null;
                      pos = pos1;
                    }
                  }
                }
              }
            }
          }
          if (result0 !== null) {
            result0 = (function(offset, escSpecCharVal) { 
                  
                  return escSpecCharVal[1]; 
              })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_number() {
          var result0, result1, result2, result3;
          var pos0, pos1, pos2;
          
          pos0 = pos;
          pos1 = pos;
          pos2 = pos;
          if (/^[1-9]/.test(input.charAt(pos))) {
            result0 = input.charAt(pos);
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("[1-9]");
            }
          }
          if (result0 !== null) {
            if (/^[0-9]/.test(input.charAt(pos))) {
              result2 = input.charAt(pos);
              pos++;
            } else {
              result2 = null;
              if (reportFailures === 0) {
                matchFailed("[0-9]");
              }
            }
            if (result2 !== null) {
              result1 = [];
              while (result2 !== null) {
                result1.push(result2);
                if (/^[0-9]/.test(input.charAt(pos))) {
                  result2 = input.charAt(pos);
                  pos++;
                } else {
                  result2 = null;
                  if (reportFailures === 0) {
                    matchFailed("[0-9]");
                  }
                }
              }
            } else {
              result1 = null;
            }
            if (result1 !== null) {
              result0 = [result0, result1];
            } else {
              result0 = null;
              pos = pos2;
            }
          } else {
            result0 = null;
            pos = pos2;
          }
          if (result0 !== null) {
            if (input.charCodeAt(pos) === 46) {
              result1 = ".";
              pos++;
            } else {
              result1 = null;
              if (reportFailures === 0) {
                matchFailed("\".\"");
              }
            }
            if (result1 !== null) {
              pos2 = pos;
              result2 = [];
              if (/^[0-9]/.test(input.charAt(pos))) {
                result3 = input.charAt(pos);
                pos++;
              } else {
                result3 = null;
                if (reportFailures === 0) {
                  matchFailed("[0-9]");
                }
              }
              while (result3 !== null) {
                result2.push(result3);
                if (/^[0-9]/.test(input.charAt(pos))) {
                  result3 = input.charAt(pos);
                  pos++;
                } else {
                  result3 = null;
                  if (reportFailures === 0) {
                    matchFailed("[0-9]");
                  }
                }
              }
              if (result2 !== null) {
                if (/^[1-9]/.test(input.charAt(pos))) {
                  result3 = input.charAt(pos);
                  pos++;
                } else {
                  result3 = null;
                  if (reportFailures === 0) {
                    matchFailed("[1-9]");
                  }
                }
                if (result3 !== null) {
                  result2 = [result2, result3];
                } else {
                  result2 = null;
                  pos = pos2;
                }
              } else {
                result2 = null;
                pos = pos2;
              }
              if (result2 !== null) {
                result0 = [result0, result1, result2];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          } else {
            result0 = null;
            pos = pos1;
          }
          if (result0 !== null) {
            result0 = (function(offset, intPartVal, fracPartVal) { 
          
                  return intPartVal.join('') + "." + fracPartVal.join(''); 
              })(pos0, result0[0], result0[2]);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_integer() {
          var result0, result1, result2;
          var pos0, pos1;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 48) {
            result0 = "0";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"0\"");
            }
          }
          if (result0 === null) {
            pos1 = pos;
            if (/^[1-9]/.test(input.charAt(pos))) {
              result0 = input.charAt(pos);
              pos++;
            } else {
              result0 = null;
              if (reportFailures === 0) {
                matchFailed("[1-9]");
              }
            }
            if (result0 !== null) {
              result1 = [];
              if (/^[0-9]/.test(input.charAt(pos))) {
                result2 = input.charAt(pos);
                pos++;
              } else {
                result2 = null;
                if (reportFailures === 0) {
                  matchFailed("[0-9]");
                }
              }
              while (result2 !== null) {
                result1.push(result2);
                if (/^[0-9]/.test(input.charAt(pos))) {
                  result2 = input.charAt(pos);
                  pos++;
                } else {
                  result2 = null;
                  if (reportFailures === 0) {
                    matchFailed("[0-9]");
                  }
                }
              }
              if (result1 !== null) {
                result0 = [result0, result1];
              } else {
                result0 = null;
                pos = pos1;
              }
            } else {
              result0 = null;
              pos = pos1;
            }
          }
          if (result0 !== null) {
            result0 = (function(offset, integerVal) { 
          
                  if (integerVal === "0") { 
                    return "0";
                  } 
                  else { 
                    return integerVal[0].concat(integerVal[1].join(''));
                  }
              })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_space() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 32) {
            result0 = " ";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\" \"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return " "; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_circumflex() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 94) {
            result0 = "^";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"^\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return "^"; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_doubleQuote() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 34) {
            result0 = "\"";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"\\\"\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return '"'; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_squareBracket() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 91) {
            result0 = "[";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"[\"");
            }
          }
          if (result0 === null) {
            if (input.charCodeAt(pos) === 93) {
              result0 = "]";
              pos++;
            } else {
              result0 = null;
              if (reportFailures === 0) {
                matchFailed("\"]\"");
              }
            }
          }
          if (result0 !== null) {
            result0 = (function(offset, bracketVal) { return bracketVal; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_parentheses() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 40) {
            result0 = "(";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"(\"");
            }
          }
          if (result0 === null) {
            if (input.charCodeAt(pos) === 41) {
              result0 = ")";
              pos++;
            } else {
              result0 = null;
              if (reportFailures === 0) {
                matchFailed("\")\"");
              }
            }
          }
          if (result0 !== null) {
            result0 = (function(offset, paraVal) { return paraVal; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_comma() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 44) {
            result0 = ",";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\",\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return ","; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_semicolon() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 59) {
            result0 = ";";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\";\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return ";"; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_equal() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (input.charCodeAt(pos) === 61) {
            result0 = "=";
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("\"=\"");
            }
          }
          if (result0 !== null) {
            result0 = (function(offset) { return "="; })(pos0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        function parse_character() {
          var result0;
          var pos0;
          
          pos0 = pos;
          if (/^[a-z]/.test(input.charAt(pos))) {
            result0 = input.charAt(pos);
            pos++;
          } else {
            result0 = null;
            if (reportFailures === 0) {
              matchFailed("[a-z]");
            }
          }
          if (result0 === null) {
            if (/^[A-Z]/.test(input.charAt(pos))) {
              result0 = input.charAt(pos);
              pos++;
            } else {
              result0 = null;
              if (reportFailures === 0) {
                matchFailed("[A-Z]");
              }
            }
            if (result0 === null) {
              if (/^[0-9]/.test(input.charAt(pos))) {
                result0 = input.charAt(pos);
                pos++;
              } else {
                result0 = null;
                if (reportFailures === 0) {
                  matchFailed("[0-9]");
                }
              }
              if (result0 === null) {
                if (input.charCodeAt(pos) === 45) {
                  result0 = "-";
                  pos++;
                } else {
                  result0 = null;
                  if (reportFailures === 0) {
                    matchFailed("\"-\"");
                  }
                }
                if (result0 === null) {
                  if (input.charCodeAt(pos) === 95) {
                    result0 = "_";
                    pos++;
                  } else {
                    result0 = null;
                    if (reportFailures === 0) {
                      matchFailed("\"_\"");
                    }
                  }
                }
              }
            }
          }
          if (result0 !== null) {
            result0 = (function(offset, charVal) { return charVal; })(pos0, result0);
          }
          if (result0 === null) {
            pos = pos0;
          }
          return result0;
        }
        
        
        function cleanupExpected(expected) {
          expected.sort();
          
          var lastExpected = null;
          var cleanExpected = [];
          for (var i = 0; i < expected.length; i++) {
            if (expected[i] !== lastExpected) {
              cleanExpected.push(expected[i]);
              lastExpected = expected[i];
            }
          }
          return cleanExpected;
        }
        
        function computeErrorPosition() {
          /*
           * The first idea was to use |String.split| to break the input up to the
           * error position along newlines and derive the line and column from
           * there. However IE's |split| implementation is so broken that it was
           * enough to prevent it.
           */
          
          var line = 1;
          var column = 1;
          var seenCR = false;
          
          for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) {
            var ch = input.charAt(i);
            if (ch === "\n") {
              if (!seenCR) { line++; }
              column = 1;
              seenCR = false;
            } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") {
              line++;
              column = 1;
              seenCR = true;
            } else {
              column++;
              seenCR = false;
            }
          }
          
          return { line: line, column: column };
        }
        
        
        var result = parseFunctions[startRule]();
        
        /*
         * The parser is now in one of the following three states:
         *
         * 1. The parser successfully parsed the whole input.
         *
         *    - |result !== null|
         *    - |pos === input.length|
         *    - |rightmostFailuresExpected| may or may not contain something
         *
         * 2. The parser successfully parsed only a part of the input.
         *
         *    - |result !== null|
         *    - |pos < input.length|
         *    - |rightmostFailuresExpected| may or may not contain something
         *
         * 3. The parser did not successfully parse any part of the input.
         *
         *   - |result === null|
         *   - |pos === 0|
         *   - |rightmostFailuresExpected| contains at least one failure
         *
         * All code following this comment (including called functions) must
         * handle these states.
         */
        if (result === null || pos !== input.length) {
          var offset = Math.max(pos, rightmostFailuresPos);
          var found = offset < input.length ? input.charAt(offset) : null;
          var errorPosition = computeErrorPosition();
          
          throw new this.SyntaxError(
            cleanupExpected(rightmostFailuresExpected),
            found,
            offset,
            errorPosition.line,
            errorPosition.column
          );
        }
        
        return result;
      },
      
      /* Returns the parser source code. */
      toSource: function() { return this._source; }
    };
    
    /* Thrown when a parser encounters a syntax error. */
    
    result.SyntaxError = function(expected, found, offset, line, column) {
      function buildMessage(expected, found) {
        var expectedHumanized, foundHumanized;
        
        switch (expected.length) {
          case 0:
            expectedHumanized = "end of input";
            break;
          case 1:
            expectedHumanized = expected[0];
            break;
          default:
            expectedHumanized = expected.slice(0, expected.length - 1).join(", ")
              + " or "
              + expected[expected.length - 1];
        }
        
        foundHumanized = found ? quote(found) : "end of input";
        
        return "Expected " + expectedHumanized + " but " + foundHumanized + " found.";
      }
      
      this.name = "SyntaxError";
      this.expected = expected;
      this.found = found;
      this.message = buildMessage(expected, found);
      this.offset = offset;
      this.line = line;
      this.column = column;
    };
    
    result.SyntaxError.prototype = Error.prototype;
    
    return result;
  })();
   
  
      // Description: This model contains the implementation for "instructions" included in the EPUB CFI domain specific language (DSL). 
  //   Lexing and parsing a CFI produces a set of executable instructions for processing a CFI (represented in the AST). 
  //   This object contains a set of functions that implement each of the executable instructions in the AST. 
  
  EPUBcfi.CFIInstructions = {
  
  	// ------------------------------------------------------------------------------------ //
  	//  "PUBLIC" METHODS (THE API)                                                          //
  	// ------------------------------------------------------------------------------------ //
  
  	// Description: Follows a step
  	// Rationale: The use of children() is important here, as this jQuery method returns a tree of xml nodes, EXCLUDING
  	//   CDATA and text nodes. When we index into the set of child elements, we are assuming that text nodes have been 
  	//   excluded.
  	// REFACTORING CANDIDATE: This should be called "followIndexStep"
  	getNextNode : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) {
  
  		// Find the jquery index for the current node
  		var $targetNode;
  		if (CFIStepValue % 2 == 0) {
  
  			$targetNode = this.elementNodeStep(CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist);
  		}
  		else {
  
  			$targetNode = this.inferTargetTextNode(CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist);
  		}
  
  		return $targetNode;
  	},
  
  	// Description: This instruction executes an indirection step, where a resource is retrieved using a 
  	//   link contained on a attribute of the target element. The attribute that contains the link differs
  	//   depending on the target. 
  	// Note: Iframe indirection will (should) fail if the iframe is not from the same domain as its containing script due to 
  	//   the cross origin security policy
  	followIndirectionStep : function (CFIStepValue, $currNode, $packageDocument, classBlacklist, elementBlacklist, idBlacklist) {
  
  		var that = this;
  		var $contentDocument; 
  		var $blacklistExcluded;
  		var $startElement;
  		var $targetNode;
  
  		// TODO: This check must be expanded to all the different types of indirection step
  		// Only expects iframes, at the moment
  		if ($currNode === undefined || !$currNode.is("iframe")) {
  
  			throw EPUBcfi.NodeTypeError($currNode, "expected an iframe element");
  		}
  
  		// Check node type; only iframe indirection is handled, at the moment
  		if ($currNode.is("iframe")) {
  
  			// Get content
  			$contentDocument = $currNode.contents();
  
  			// Go to the first XHTML element, which will be the first child of the top-level document object
  			$blacklistExcluded = this.applyBlacklist($contentDocument.children(), classBlacklist, elementBlacklist, idBlacklist);
  			$startElement = $($blacklistExcluded[0]);
  
  			// Follow an index step
  			$targetNode = this.getNextNode(CFIStepValue, $startElement, classBlacklist, elementBlacklist, idBlacklist);
  
  			// Return that shit!
  			return $targetNode; 
  		}
  
  		// TODO: Other types of indirection
  		// TODO: $targetNode.is("embed")) : src
  		// TODO: ($targetNode.is("object")) : data
  		// TODO: ($targetNode.is("image") || $targetNode.is("xlink:href")) : xlink:href
  	},
  
  	// Description: Injects an element at the specified text node
  	// Arguments: a cfi text termination string, a jquery object to the current node
  	textTermination : function ($currNode, textOffset, elementToInject) {
  
  		// Get the first node, this should be a text node
  		if ($currNode === undefined) {
  
  			throw EPUBcfi.NodeTypeError($currNode, "expected a terminating node, or node list");
  		} 
  		else if ($currNode.length === 0) {
  
  			throw EPUBcfi.TerminusError("Text", "Text offset:" + textOffset, "no nodes found for termination condition");
  		}
  
  		$currNode = this.injectCFIMarkerIntoText($currNode, textOffset, elementToInject);
  		return $currNode;
  	},
  
  	// Description: Checks that the id assertion for the node target matches that on 
  	//   the found node. 
  	targetIdMatchesIdAssertion : function ($foundNode, idAssertion) {
  
  		if ($foundNode.attr("id") === idAssertion) {
  
  			return true;
  		}
  		else {
  
  			return false;
  		}
  	},
  
  	// ------------------------------------------------------------------------------------ //
  	//  "PRIVATE" HELPERS                                                                   //
  	// ------------------------------------------------------------------------------------ //
  
  	// Description: Step reference for xml element node. Expected that CFIStepValue is an even integer
  	elementNodeStep : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) {
  
  		var $targetNode;
  		var $blacklistExcluded;
  		var numElements;
  		var jqueryTargetNodeIndex = (CFIStepValue / 2) - 1;
  
  		$blacklistExcluded = this.applyBlacklist($currNode.children(), classBlacklist, elementBlacklist, idBlacklist);
  		numElements = $blacklistExcluded.length;
  
  		if (this.indexOutOfRange(jqueryTargetNodeIndex, numElements)) {
  
  			throw EPUBcfi.OutOfRangeError(jqueryTargetNodeIndex, numElements - 1, "");
  		}
  
  	    $targetNode = $($blacklistExcluded[jqueryTargetNodeIndex]);
  		return $targetNode;
  	},
  
  	retrieveItemRefHref : function ($itemRefElement, $packageDocument) {
  
  		return $("#" + $itemRefElement.attr("idref"), $packageDocument).attr("href");
  	},
  
  	indexOutOfRange : function (targetIndex, numChildElements) {
  
  		return (targetIndex > numChildElements - 1) ? true : false;
  	},
  
  	// Rationale: In order to inject an element into a specific position, access to the parent object 
  	//   is required. This is obtained with the jquery parent() method. An alternative would be to 
  	//   pass in the parent with a filtered list containing only children that are part of the target text node.
  	injectCFIMarkerIntoText : function ($textNodeList, textOffset, elementToInject) {
  
  		var nodeNum;
  		var currNodeLength;
  		var currTextPosition = 0;
  		var nodeOffset;
  		var originalText;
  		var $injectedNode;
  		var $newTextNode;
  		// The iteration counter may be incorrect here (should be $textNodeList.length - 1 ??)
  		for (nodeNum = 0; nodeNum <= $textNodeList.length; nodeNum++) {
  
  			if ($textNodeList[nodeNum].nodeType === 3) {
  
  				currNodeMaxIndex = ($textNodeList[nodeNum].nodeValue.length - 1) + currTextPosition;
  				nodeOffset = textOffset - currTextPosition;
  
  				if (currNodeMaxIndex >= textOffset) {
  
  					// This node is going to be split and the components re-inserted
  					originalText = $textNodeList[nodeNum].nodeValue;	
  
  					// Before part
  				 	$textNodeList[nodeNum].nodeValue = originalText.slice(0, nodeOffset);
  
  					// Injected element
  					$injectedNode = $(elementToInject).insertAfter($textNodeList.eq(nodeNum));
  
  					// After part
  					$newTextNode = $(document.createTextNode(originalText.slice(nodeOffset, originalText.length)));
  					$($newTextNode).insertAfter($injectedNode);
  
  					return $textNodeList.parent();
  				}
  				else {
  
  					currTextPosition = currTextPosition + currNodeMaxIndex;
  				}
  			}
  		}
  
  		throw EPUBcfi.TerminusError("Text", "Text offset:" + textOffset, "The offset exceeded the length of the text");
  	},
  
  	// Description: This method finds a target text node and then injects an element into the appropriate node
  	// Arguments: A step value that is an odd integer. A current node with a set of child elements.
  	// Rationale: The possibility that cfi marker elements have been injected into a text node at some point previous to 
  	//   this method being called (and thus splitting the original text node into two separate text nodes) necessitates that
  	//   the set of nodes that compromised the original target text node are inferred and returned.
  	// Notes: Passed a current node. This node should have a set of elements under it. This will include at least one text node, 
  	//   element nodes (maybe), or possibly a mix. 
  	// REFACTORING CANDIDATE: This method is pretty long. Worth investigating to see if it can be refactored into something clearer.
  	inferTargetTextNode : function (CFIStepValue, $currNode, classBlacklist, elementBlacklist, idBlacklist) {
  		
  		var $elementsWithoutMarkers;
  		var currTextNodePosition;
  		var logicalTargetPosition;
  		var nodeNum;
  		var $targetTextNodeList;
  
  		// Remove any cfi marker elements from the set of elements. 
  		// Rationale: A filtering function is used, as simply using a class selector with jquery appears to 
  		//   result in behaviour where text nodes are also filtered out, along with the class element being filtered.
  		$elementsWithoutMarkers = this.applyBlacklist($currNode.contents(), classBlacklist, elementBlacklist, idBlacklist);
  
  		// Convert CFIStepValue to logical index; assumes odd integer for the step value
  		logicalTargetPosition = (parseInt(CFIStepValue) + 1) / 2;
  
  		// Set text node position counter
  		currTextNodePosition = 1;
  		$targetTextNodeList = $elementsWithoutMarkers.filter(
  			function () {
  
  				if (currTextNodePosition === logicalTargetPosition) {
  
  					// If it's a text node
  					if (this.nodeType === 3) {
  						return true; 
  					}
  					// Any other type of node, move onto the next text node
  					else {
  						currTextNodePosition++; 
  						return false;
  					}
  				}
  				// In this case, don't return any elements
  				else {
  
  					// If its the last child and it's not a text node, there are no text nodes after it
  					// and the currTextNodePosition shouldn't be incremented
  					if (this.nodeType !== 3 && this !== $elementsWithoutMarkers.lastChild) {
  						currTextNodePosition++;
  					}
  
  					return false;
  				}
  			}
  		);
  
  		// The filtering above should have counted the number of "logical" text nodes; this can be used to 
  		// detect out of range errors
  		if ($targetTextNodeList.length === 0) {
  
  			throw EPUBcfi.OutOfRangeError(logicalTargetPosition, currTextNodePosition - 1, "Index out of range");
  		}
  
  		// return the text node list
  		return $targetTextNodeList;
  	},
  
  	applyBlacklist : function ($elements, classBlacklist, elementBlacklist, idBlacklist) {
  
          var $filteredElements;
  
          $filteredElements = $elements.filter(
              function () {
  
                  var $currElement = $(this);
                  var includeInList = true;
  
                  if (classBlacklist) {
  
                  	// Filter each element with the class type
                  	$.each(classBlacklist, function (index, value) {
  
  	                    if ($currElement.hasClass(value)) {
  	                    	includeInList = false;
  
  	                    	// Break this loop
  	                        return false;
  	                    }
                  	});
                  }
  
                  if (elementBlacklist) {
                  	
  	                // For each type of element
  	                $.each(elementBlacklist, function (index, value) {
  
  	                    if ($currElement.is(value)) {
  	                    	includeInList = false;
  
  	                    	// Break this loop
  	                        return false;
  	                    }
  	                });
  				}
  
  				if (idBlacklist) {
                  	
  	                // For each type of element
  	                $.each(idBlacklist, function (index, value) {
  
  	                    if ($currElement.attr("id") === value) {
  	                    	includeInList = false;
  
  	                    	// Break this loop
  	                        return false;
  	                    }
  	                });
  				}
  
                  return includeInList;
              }
          );
  
          return $filteredElements;
      }
  };
  
  
  
  
      // Description: This is an interpreter that inteprets an Abstract Syntax Tree (AST) for a CFI. The result of executing the interpreter
  //   is to inject an element, or set of elements, into an EPUB content document (which is just an XHTML document). These element(s) will
  //   represent the position or area in the EPUB referenced by a CFI.
  // Rationale: The AST is a clean and readable expression of the step-terminus structure of a CFI. Although building an interpreter adds to the
  //   CFI infrastructure, it provides a number of benefits. First, it emphasizes a clear separation of concerns between lexing/parsing a
  //   CFI, which involves some complexity related to escaped and special characters, and the execution of the underlying set of steps 
  //   represented by the CFI. Second, it will be easier to extend the interpreter to account for new/altered CFI steps (say for references
  //   to vector objects or multiple CFIs) than if lexing, parsing and interpretation were all handled in a single step. Finally, Readium's objective is 
  //   to demonstrate implementation of the EPUB 3.0 spec. An implementation with a strong separation of concerns that conforms to 
  //   well-understood patterns for DSL processing should be easier to communicate, analyze and understand. 
  // REFACTORING CANDIDATE: node type errors shouldn't really be possible if the CFI syntax is correct and the parser is error free. 
  //   Might want to make the script die in those instances, once the grammar and interpreter are more stable. 
  // REFACTORING CANDIDATE: The use of the 'nodeType' property is confusing as this is a DOM node property and the two are unrelated. 
  //   Whoops. There shouldn't be any interference, however, I think this should be changed. 
  
  EPUBcfi.Interpreter = {
  
      // ------------------------------------------------------------------------------------ //
      //  "PUBLIC" METHODS (THE API)                                                          //
      // ------------------------------------------------------------------------------------ //
  
      // Description: Find the content document referenced by the spine item. This should be the spine item 
      //   referenced by the first indirection step in the CFI.
      // Rationale: This method is a part of the API so that the reading system can "interact" the content document 
      //   pointed to by a CFI. If this is not a separate step, the processing of the CFI must be tightly coupled with 
      //   the reading system, as it stands now. 
      getContentDocHref : function (CFI, packageDocument, classBlacklist, elementBlacklist, idBlacklist) {
  
          // Decode for URI/IRI escape characters
          var $packageDocument = $(packageDocument);
          var decodedCFI = decodeURI(CFI);
          var CFIAST = EPUBcfi.Parser.parse(decodedCFI);
  
          // Check node type; throw error if wrong type
          if (CFIAST === undefined || CFIAST.type !== "CFIAST") { 
  
              throw EPUBcfi.NodeTypeError(CFIAST, "expected CFI AST root node");
          }
  
          var $packageElement = $($("package", $packageDocument)[0]);
  
          // Interpet the path node (the package document step)
          var $currElement = this.interpretIndexStepNode(CFIAST.cfiString.path, $packageElement, classBlacklist, elementBlacklist, idBlacklist);
  
          // Interpret the local_path node, which is a set of steps and and a terminus condition
          var stepNum = 0;
          var nextStepNode;
          for (stepNum = 0 ; stepNum <= CFIAST.cfiString.localPath.steps.length - 1 ; stepNum++) {
          
              nextStepNode = CFIAST.cfiString.localPath.steps[stepNum];
              if (nextStepNode.type === "indexStep") {
  
                  $currElement = this.interpretIndexStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist);
              }
              else if (nextStepNode.type === "indirectionStep") {
  
                  $currElement = this.interpretIndirectionStepNode(nextStepNode, $currElement, $packageDocument, classBlacklist, elementBlacklist, idBlacklist);
              }
  
              // Found the content document href referenced by the spine item 
              if ($currElement.is("itemref")) {
  
                  return EPUBcfi.CFIInstructions.retrieveItemRefHref($currElement, $packageDocument);
              }
          }
  
          // TODO: If you get to here, an itemref element was never found - a runtime error. The cfi is misspecified or 
          //   the package document is messed up.
      },
  
      // Description: Inject an arbirtary html element into a position in a content document referenced by a CFI
      injectElement : function (CFI, contentDocument, elementToInject, classBlacklist, elementBlacklist, idBlacklist) {
  
          var decodedCFI = decodeURI(CFI);
          var CFIAST = EPUBcfi.Parser.parse(decodedCFI);
  
          // Find the first indirection step in the local path; follow it like a regular step, as the content document it 
          //   references is already loaded and has been passed to this method
          var stepNum = 0;
          var nextStepNode;
          for (stepNum; stepNum <= CFIAST.cfiString.localPath.steps.length - 1 ; stepNum++) {
          
              nextStepNode = CFIAST.cfiString.localPath.steps[stepNum];
              if (nextStepNode.type === "indirectionStep") {
  
                  // This is now assuming that indirection steps and index steps conform to an interface: an object with stepLength, idAssertion
                  nextStepNode.type = "indexStep";
                  // Getting the html element and creating a jquery object for it; excluding cfiMarkers
                  $currElement = this.interpretIndexStepNode(nextStepNode, $("html", contentDocument), classBlacklist, elementBlacklist, idBlacklist);
                  stepNum++ // Increment the step num as this will be passed as the starting point for continuing interpretation
                  break;
              }
          }
  
          // Interpret the rest of the steps
          $currElement = this.interpretLocalPath(CFIAST.cfiString, stepNum, $currElement, classBlacklist, elementBlacklist, idBlacklist);
  
          // TODO: detect what kind of terminus; for now, text node termini are the only kind implemented
          $currElement = this.interpretTextTerminusNode(CFIAST.cfiString.localPath.termStep, $currElement, elementToInject);
  
          // Return the element that was injected into
          return $currElement;
      },
  
      // ------------------------------------------------------------------------------------ //
      //  "PRIVATE" HELPERS                                                                   //
      // ------------------------------------------------------------------------------------ //
  
      interpretLocalPath : function (cfiStringNode, startStepNum, $currElement, classBlacklist, elementBlacklist, idBlacklist) {
  
          var stepNum = startStepNum;
          var nextStepNode;
          for (stepNum; stepNum <= cfiStringNode.localPath.steps.length - 1 ; stepNum++) {
          
              nextStepNode = cfiStringNode.localPath.steps[stepNum];
              if (nextStepNode.type === "indexStep") {
  
                  $currElement = this.interpretIndexStepNode(nextStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist);
              }
              else if (nextStepNode.type === "indirectionStep") {
  
                  $currElement = this.interpretIndirectionStepNode(nextStepNode, $currElement, $packageDocument, classBlacklist, elementBlacklist, idBlacklist);
              }
          }
  
          return $currElement;
      },
  
      interpretIndexStepNode : function (indexStepNode, $currElement, classBlacklist, elementBlacklist, idBlacklist) {
  
          // Check node type; throw error if wrong type
          if (indexStepNode === undefined || indexStepNode.type !== "indexStep") {
  
              throw EPUBcfi.NodeTypeError(indexStepNode, "expected index step node");
          }
  
          // Index step
          var $stepTarget = EPUBcfi.CFIInstructions.getNextNode(indexStepNode.stepLength, $currElement, classBlacklist, elementBlacklist, idBlacklist);
  
          // Check the id assertion, if it exists
          if (indexStepNode.idAssertion) {
  
              if (!EPUBcfi.CFIInstructions.targetIdMatchesIdAssertion($stepTarget, indexStepNode.idAssertion)) {
  
                  throw EPUBcfi.CFIAssertionError(indexStepNode.idAssertion, $stepTarget.attr('id'), "Id assertion failed");
              }
          }
  
          return $stepTarget;
      },
  
      interpretIndirectionStepNode : function (indirectionStepNode, $currElement, $packageDocument, classBlacklist, elementBlacklist, idBlacklist) {
  
          // Check node type; throw error if wrong type
          if (indirectionStepNode === undefined || indirectionStepNode.type !== "indirectionStep") {
  
              throw EPUBcfi.NodeTypeError(indirectionStepNode, "expected indirection step node");
          }
  
          // Indirection step
          var $stepTarget = EPUBcfi.CFIInstructions.followIndirectionStep(
              indirectionStepNode.stepLength, 
              $currElement,
              $packageDocument, 
              classBlacklist, 
              elementBlacklist);
  
          // Check the id assertion, if it exists
          if (indirectionStepNode.idAssertion) {
  
              if (!EPUBcfi.CFIInstructions.targetIdMatchesIdAssertion($stepTarget, indirectionStepNode.idAssertion)) {
  
                  throw EPUBcfi.CFIAssertionError(indirectionStepNode.idAssertion, $stepTarget.attr('id'), "Id assertion failed");
              }
          }
  
          return $stepTarget;
      },
  
      interpretTextTerminusNode : function (terminusNode, $currElement, elementToInject) {
  
          if (terminusNode === undefined || terminusNode.type !== "textTerminus") {
  
              throw EPUBcfi.NodeTypeError(terminusNode, "expected text terminus node");
          }
  
          var $elementInjectedInto = EPUBcfi.CFIInstructions.textTermination(
              $currElement, 
              terminusNode.offsetValue, 
              elementToInject);
  
          return $elementInjectedInto;
      }
  };
  
      // Description: This is a set of runtime errors that the CFI interpreter can throw. 
  // Rationale: These error types extend the basic javascript error object so error things like the stack trace are 
  //   included with the runtime errors. 
  
  // REFACTORING CANDIDATE: This type of error may not be required in the long run. The parser should catch any syntax errors, 
  //   provided it is error-free, and as such, the AST should never really have any node type errors, which are essentially errors
  //   in the structure of the AST. This error should probably be refactored out when the grammar and interpreter are more stable.
  EPUBcfi.NodeTypeError = function (node, message) {
  
      function NodeTypeError () {
  
          this.node = node;
      }
  
      NodeTypeError.prototype = new Error(message);
      NodeTypeError.constructor = NodeTypeError;
  
      return new NodeTypeError();
  };
  
  // REFACTORING CANDIDATE: Might make sense to include some more specifics about the out-of-rangeyness.
  EPUBcfi.OutOfRangeError = function (targetIndex, maxIndex, message) {
  
      function OutOfRangeError () {
  
          this.targetIndex = targetIndex;
          this.maxIndex = maxIndex;
      }
  
      OutOfRangeError.prototype = new Error(message);
      OutOfRangeError.constructor = OutOfRangeError()
  
      return new OutOfRangeError();
  };
  
  // REFACTORING CANDIDATE: This is a bit too general to be useful. When I have a better understanding of the type of errors
  //   that can occur with the various terminus conditions, it'll make more sense to revisit this. 
  EPUBcfi.TerminusError = function (terminusType, terminusCondition, message) {
  
      function TerminusError () {
  
          this.terminusType = terminusType;
          this.terminusCondition = terminusCondition;
      }
  
      TerminusError.prototype = new Error(message);
      TerminusError.constructor = TerminusError();
  
      return new TerminusError();
  };
  
  EPUBcfi.CFIAssertionError = function (expectedAssertion, targetElementAssertion, message) {
  
      function CFIAssertionError () {
  
          this.expectedAssertion = expectedAssertion;
          this.targetElementAssertion = targetElementAssertion;
      }
  
      CFIAssertionError.prototype = new Error(message);
      CFIAssertionError.constructor = CFIAssertionError();
  
      return new CFIAssertionError();
  };
  
  
      EPUBcfi.Generator = {
  
      // Description: Generates a character offset CFI 
      // Arguments: The text node that contains the offset referenced by the cfi, the offset value, the name of the 
      //   content document that contains the text node, the package document for this EPUB.
      generateCharacterOffsetCFI : function (startTextNode, characterOffset, contentDocumentName, packageDocument, classBlacklist, elementBlacklist, idBlacklist) {
  
          // ------------------------------------------------------------------------------------ //
          //  "PUBLIC" METHODS (THE API)                                                          //
          // ------------------------------------------------------------------------------------ //
  
          var contentDocCFI;
          var $itemRefStartNode;
          var packageDocCFI;
  
          // Check that the text node to start from IS a text node
          if (!startTextNode) {
              throw new EPUBcfi.NodeTypeError(startTextNode, "Cannot generate a character offset from a starting point that is not a text node");
          } else if (startTextNode.nodeType != 3) {
              throw new EPUBcfi.NodeTypeError(startTextNode, "Cannot generate a character offset from a starting point that is not a text node");
          }
  
          // Check that the character offset is within a valid range for the text node supplied
          if (characterOffset < 0) {
              throw new EPUBcfi.OutOfRangeError(characterOffset, 0, "Character offset cannot be less than 0");
          }
          else if (characterOffset > startTextNode.nodeValue.length) {
              throw new EPUBcfi.OutOfRangeError(characterOffset, startTextNode.nodeValue.length - 1, "character offset cannot be greater than the length of the text node");
          }
  
          // Check that the idref for the content document has been provided
          if (!contentDocumentName) {
              throw new Error("The idref for the content document, as found in the spine, must be supplied");
          }
  
          // Check that the package document is non-empty and contains an itemref element for the supplied idref
          if (!packageDocument) {
              throw new Error("A package document must be supplied to generate a CFI");
          }
          else if ($($("itemref[idref='" + contentDocumentName + "']", packageDocument)[0]).length === 0) {
              throw new Error("The idref of the content document could not be found in the spine");
          }
  
          // Call the recursive method to create all the steps up to the head element of the content document (the "html" element)
          contentDocCFI = this.createCFIElementSteps($(startTextNode), characterOffset, "html", classBlacklist, elementBlacklist, idBlacklist);
  
          // Get the start node (itemref element) that references the content document
          $itemRefStartNode = $("itemref[idref='" + contentDocumentName + "']", $(packageDocument));
  
          // Create the steps up to the top element of the package document (the "package" element)
          packageDocCFI = this.createCFIElementSteps($itemRefStartNode, characterOffset, "package", classBlacklist, elementBlacklist, idBlacklist);
  
          // Return the CFI wrapped with "epubcfi()"
          return "epubcfi(" + packageDocCFI + contentDocCFI + ")";
      },
  
      // ------------------------------------------------------------------------------------ //
      //  "PRIVATE" HELPERS                                                                   //
      // ------------------------------------------------------------------------------------ //
  
      // Description: Creates a CFI terminating step, to a text node, with a character offset
      // Arguments:
      // Rationale:
      // Notes:
      // REFACTORING CANDIDATE: Some of the parts of this method could be refactored into their own methods
      createCFITextNodeStep : function ($startTextNode, characterOffset, classBlacklist, elementBlacklist, idBlacklist) {
  
          var $parentNode;
          var $contentsExcludingMarkers;
          var CFIIndex;
          var indexOfTextNode;
          var preAssertion;
          var preAssertionStartIndex;
          var textLength;
          var postAssertion;
          var postAssertionEndIndex;
  
          // Find text node position in the set of child elements, ignoring any blacklisted elements 
          $parentNode = $startTextNode.parent();
          $contentsExcludingMarkers = EPUBcfi.CFIInstructions.applyBlacklist($parentNode.contents(), classBlacklist, elementBlacklist, idBlacklist);
  
          // Find the text node index in the parent list, inferring nodes that were originally a single text node
          var prevNodeWasTextNode;
          var indexOfFirstInSequence;
          $.each($contentsExcludingMarkers, 
              function (index) {
  
                  // If this is a text node, check if it matches and return the current index
                  if (this.nodeType === 3) {
  
                      if (this === $startTextNode[0]) {
  
                          // Set index as the first in the adjacent sequence of text nodes, or as the index of the current node if this 
                          //   node is a standard one sandwiched between two element nodes. 
                          if (prevNodeWasTextNode) {
                              indexOfTextNode = indexOfFirstInSequence;
                          }
                          else {
                              indexOfTextNode = index;
                          }
                          
                          // Break out of .each loop
                          return false; 
                      }
  
                      // Save this index as the first in sequence of adjacent text nodes, if it is not already set by this point
                      prevNodeWasTextNode = true;
                      if (!indexOfFirstInSequence) {
                          indexOfFirstInSequence = index;
                      }
                  }
                  // This node is not a text node
                  else {
                      prevNodeWasTextNode = false;
                      indexOfFirstInSequence = undefined;
                  }
              }
          );
  
          // Convert the text node index to a CFI odd-integer representation
          CFIIndex = (indexOfTextNode * 2) + 1;
  
          // TODO: text assertions are not in the grammar yet, I think, or they're just causing problems. This has
          //   been temporarily removed. 
  
          // Add pre- and post- text assertions
          // preAssertionStartIndex = (characterOffset - 3 >= 0) ? characterOffset - 3 : 0;
          // preAssertion = $startTextNode[0].nodeValue.substring(preAssertionStartIndex, characterOffset);
  
          // textLength = $startTextNode[0].nodeValue.length;
          // postAssertionEndIndex = (characterOffset + 3 <= textLength) ? characterOffset + 3 : textLength;
          // postAssertion = $startTextNode[0].nodeValue.substring(characterOffset, postAssertionEndIndex);
  
          // Gotta infer the correct character offset, as well
  
          // Return the constructed CFI text node step
          return "/" + CFIIndex + ":" + characterOffset;
           // + "[" + preAssertion + "," + postAssertion + "]";
      },
  
      // Description: A set of adjacent text nodes can be inferred to have been a single text node in the original document. As such, 
      //   if the character offset is specified for one of the adjacent text nodes, the true offset for the original node must be
      //   inferred.
      findOriginalTextNodeCharOffset : function ($startTextNode, specifiedCharacterOffset, classBlacklist, elementBlacklist, idBlacklist) {
  
          var $parentNode;
          var $contentsExcludingMarkers;
          var textLength;
          
          // Find text node position in the set of child elements, ignoring any cfi markers 
          $parentNode = $startTextNode.parent();
          $contentsExcludingMarkers = EPUBcfi.CFIInstructions.applyBlacklist($parentNode.contents(), classBlacklist, elementBlacklist, idBlacklist);
  
          // Find the text node number in the list, inferring nodes that were originally a single text node
          var prevNodeWasTextNode;
          var originalCharOffset = -1; // So the character offset is a 0-based index; we'll be adding lengths of text nodes to this number
          $.each($contentsExcludingMarkers, 
              function (index) {
  
                  // If this is a text node, check if it matches and return the current index
                  if (this.nodeType === 3) {
  
                      if (this === $startTextNode[0]) {
  
                          if (prevNodeWasTextNode) {
                              originalCharOffset = originalCharOffset + specifiedCharacterOffset;
                          }
                          else {
                              originalCharOffset = specifiedCharacterOffset;
                          }
  
                          return false; // Break out of .each loop
                      }
                      else {
  
                          originalCharOffset = originalCharOffset + this.length;
                      }
  
                      // save this index as the first in sequence of adjacent text nodes, if not set
                      prevNodeWasTextNode = true;
                  }
                  // This node is not a text node
                  else {
                      prevNodeWasTextNode = false;
                  }
              }
          );
  
          return originalCharOffset;
      },
  
      // REFACTORING CANDIDATE: Consider putting the handling of the starting text node into the body of the 
      //   generateCharacterOffsetCfi() method; this way the characterOffset argument could be removed, which 
      //   would clarify the abstraction
      createCFIElementSteps : function ($currNode, characterOffset, topLevelElement, classBlacklist, elementBlacklist, idBlacklist) {
  
          var textNodeStep;
          var $blacklistExcluded;
          var $parentNode;
          var currNodePosition;
          var CFIPosition;
          var idAssertion;
          var elementStep; 
  
          if ($currNode[0].nodeType === 3) {
  
              textNodeStep = this.createCFITextNodeStep($currNode, characterOffset, classBlacklist, elementBlacklist, idBlacklist);
              return this.createCFIElementSteps($currNode.parent(), characterOffset, topLevelElement, classBlacklist, elementBlacklist, idBlacklist) + textNodeStep; 
          }
  
          // Find position of current node in parent list
          $blacklistExcluded = EPUBcfi.CFIInstructions.applyBlacklist($currNode.parent().children(), classBlacklist, elementBlacklist, idBlacklist);
          $.each($blacklistExcluded, 
              function (index, value) {
  
                  if (this === $currNode[0]) {
  
                      currNodePosition = index;
  
                      // Break loop
                      return false;
                  }
          });
  
          // Convert position to the CFI even-integer representation
          CFIPosition = (currNodePosition + 1) * 2;
  
          // Create CFI step with id assertion, if the element has an id
          if ($currNode.attr("id")) {
              elementStep = "/" + CFIPosition + "[" + $currNode.attr("id") + "]";
          }
          else {
              elementStep = "/" + CFIPosition;
          }
  
          // If a parent is an html element return the (last) step for this content document, otherwise, continue
          $parentNode = $currNode.parent();
          if ($parentNode.is(topLevelElement)) {
              
              // If the top level node is a type from which an indirection step, add an indirection step character (!)
              // REFACTORING CANDIDATE: It is possible that this should be changed to if (topLevelElement = 'package') do
              //   not return an indirection character. Every other type of top-level element may require an indirection
              //   step to navigate to, thus requiring that ! is always prepended. 
              if (topLevelElement === 'html') {
                  return "!" + elementStep;
              }
              else {
                  return elementStep;
              }
          }
          else {
              return this.createCFIElementSteps($parentNode, characterOffset, topLevelElement, classBlacklist, elementBlacklist, idBlacklist) + elementStep;
          }
      }
  };
  
      if (global.EPUBcfi) {
  
          throw new Error('The EPUB cfi library has already been defined');
      }
      else {
  
          global.EPUBcfi = EPUBcfi;
      }
  }) (typeof window === 'undefined' ? this : window);
  