'use strict';

/**
 * CSS to XPath converter - A utility to transform CSS selectors into XPath expressions
 */

import { 
  convertToLowercase, 
  createEndsWith, 
  extractUrl, 
  extractUrlPath, 
  extractUrlDomain,
  validateSelector,
  generateRepeatedString,
  isNumericValue,
  createXPathExpression
} from './xpath-utils.js';

/**
 * Converts CSS selectors to XPath expressions
 * @param {string} selector - CSS selector to convert
 * @param {boolean} [isNested=false] - Whether this is a nested call
 * @param {Object} [options] - Configuration options
 * @param {string} [options.prefix='./'] - XPath prefix to use
 * @param {boolean} [options.strict=false] - Throw error on invalid selectors
 * @returns {string} - XPath expression
 */
export function css2xpath(selector, isNested, options = {}) {
  const { 
    prefix = './/', 
    strict = false 
  } = options;
  
  // Handle empty selector case
  if (!selector || selector.trim() === '') {
    return './/*';
  }
  
  // Validate selector if strict mode is enabled
  if (strict) {
    if (!validateSelector(selector)) {
      throw new Error(`Invalid CSS selector: ${selector}`);
    }
  }
  
  // Trim whitespace from the selector
  selector = selector.trim();

  // Pre-process to remove pseudo-elements like ::before, ::after, ::placeholder
  // as they don't have direct XPath 1.0 equivalents for the pseudo-part itself.
  selector = selector.replace(/::[a-zA-Z\-]+(\([^\)]*\))?/g, '');
  // Re-trim if changes were made
  selector = selector.trim();
  // If selector becomes empty after stripping pseudo-elements, treat as universal.
  if (selector === '') {
    return './/*';
  }

  // XPath constants
  const urlAttributes = '@href|@src';
  const lowercaseExpression = convertToLowercase();
  const namespaceUri = 'ancestor-or-self::*[last()]/@url';
  const namespacePath = extractUrlPath(extractUrl(namespaceUri, urlAttributes), urlAttributes);
  const hasProtocol = '(starts-with(' + urlAttributes + ',"http://") or starts-with(' + urlAttributes + ',"https://"))';
  const isInternalLink = 'starts-with(' + extractUrl(null, urlAttributes) + ',' + extractUrlDomain(namespaceUri, urlAttributes) + ') or ' + createEndsWith(extractUrlDomain(null, urlAttributes), extractUrlDomain(namespaceUri, urlAttributes));
  const isLocalLink = '(' + hasProtocol + ' and starts-with(' + extractUrl() + ',' + extractUrl(namespaceUri) + '))';
  const isPathLink = 'starts-with(' + urlAttributes + ',"/")';
  const isLocalPathLink = 'starts-with(' + extractUrlPath() + ',' + namespacePath + ')';
  const normalizeSpaceExpr = 'normalize-space()';
  const internalLinkFilter = '[not(' + hasProtocol + ') or ' + isInternalLink + ']';
  const externalLinkFilter = '[' + hasProtocol + ' and not(' + isInternalLink + ')]';

  // Escape characters
  const literalEscapeChar = String.fromCharCode(30);
  const parenEscapeChar = String.fromCharCode(31);

  // Regular expressions
  const stringLiteralRegex = /("[^"\x1E]*"|'[^'\x1E]*'|=\s*[^\s\]\'\"]+)/g;
  const escapedLiteralRegex = /['"]?(\x1E+)['"]?/g;
  const wrapPseudoRegex = /(\x1F\)|[^\)])\:(first|limit|last|gt|lt|eq|nth)([^\-]|$)/;
  const specialCharsRegex = /[\x1C-\x1F]+/g;
  const firstAxisRegex = /^([\s\(\x1F]*)(\.?[^\.\/\(]{1,2}[a-z]*:*)/;
  const filterPrefixRegex = /(^|\/|\:)\[/g;
  const attrPrefixRegex = /([^\(\[\/\|\s\x1F])\@/g;
  const nthEquationRegex = /^([-0-9]*)n.*?([0-9]*)$/;

  // CSS combinators regex and callback
  const combinatorsRegex = /\s*(!?[+>~,^ ])\s*(\.?\/+|[a-z\-]+::)?([a-z\-]+\()?((and\s*|or\s*|mod\s*)?[^+>~,\s'"\]\|\^\$\!\<\=\x1C-\x1F]+)?/g;
  
  const processCombinators = function (match, operator, axis, func, literal, exclude, offset, orig) {
    let prefix = ''; // If we can, we'll prefix a '.'

    // XPath operators can look like node-name selectors
    // Detect false positive for " and", " or", " mod"
    if (operator === ' ' && exclude !== undefined) {
      return match;
    }

    if (axis === undefined) {
      // Only allow node-selecting XPath functions
      // Detect false positive for " + count(...)", " count(...)", " > position()", etc.
      if (func !== undefined && (func !== 'node(' && func !== 'text(' && func !== 'comment(')) {
        return;
      } else if (literal === undefined) {
        literal = func;
      } // Handle case " + text()", " > comment()", etc. where "func" is our "literal"

      // XPath math operators match some CSS combinators
      // Detect false positive for " + 1", " > 1", etc.
      if (isNumericValue(literal)) {
        return match;
      }

      const prevChar = orig.charAt(offset - 1);

      if (prevChar.length === 0 ||
          prevChar === '(' ||
          prevChar === '|' ||
          prevChar === ':') {
        prefix = '.';
      }
    }

    // Return if we don't have a selector to follow the axis
    if (literal === undefined) {
      if (offset + match.length === orig.length) {
        literal = '*';
      } else {
        return match;
      }
    }

    switch (operator) {
      case ' ':
        return '//' + literal;
      case '>':
        return '/' + literal;
      case '+':
        return prefix + '/following-sibling::*[1]/self::' + literal;
      case '~':
        return prefix + '/following-sibling::' + literal;
      case ',':
        if (axis === undefined) {
          // Fix for comma separator
          return '|.//' + literal;
        }
        // Fallback to original behavior
        axis = './/';
        return '|' + axis + literal;
      case '^': // first child
        return '/child::*[1]/self::' + literal;
      case '!^': // last child (for test compatibility)
        return '/child::*[last()]/self::' + literal;
      case '! ': // ancestor-or-self
        return '/ancestor-or-self::' + literal;
      case '!>': // direct parent
        return '/parent::' + literal;
      case '!+': // adjacent preceding sibling
        return '/preceding-sibling::*[1]/self::' + literal;
      case '!~': // preceding sibling
        return '/preceding-sibling::' + literal;
      // case '~~'
      // return '/following-sibling::*/self::|'+findSelectorStart(orig, offset)+'/preceding-sibling::*/self::'+literal;
    }
  };

  // CSS attributes regex and callback
  const attributesRegex = /\[([^\@\|\*\=\^\~\$\!\(\/\s\x1C-\x1F]+)\s*(([\|\*\~\^\$\!]?)=?\s*(\x1E+))?(\s*i)?\]/g;
  
  /**
   * Processes CSS attribute selectors and converts them to XPath
   * @param {string} str - The full attribute selector string
   * @param {string} attr - The attribute name
   * @param {string} comp - The comparison operation
   * @param {string} op - The operator (!, $, ^, ~, *, |)
   * @param {string} val - The attribute value to compare against
   * @param {string} [caseInsensitiveFlag] - Optional 'i' flag for case-insensitivity
   * @param {number} offset - The offset in the original string
   * @param {string} orig - The original full selector string
   * @returns {string} The XPath expression for this attribute selector
   */
  const processAttributes = function (str, attr, comp, op, val, caseInsensitiveFlag, offset, orig) {
    const axis = '';
    const prevChar = orig.charAt(offset - 1);
    
    // Define these constants at this scope level
    const uc = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ"';
    const lc = '"abcdefghijklmnopqrstuvwxyz"';
    
    if (op === undefined) {
      if (attr.charAt(attr.length - 1) === '(' || 
          attr.search(/^[0-9]+$/) !== -1 || 
          attr.indexOf(':') !== -1) {
        return str;
      }
      return axis + '[@' + attr + ']';
    } else if (op === '=') {
      if (caseInsensitiveFlag && caseInsensitiveFlag.trim() === 'i') {
        return axis + '[translate(normalize-space(@' + attr + '), ' + uc + ', ' + lc + ') = translate(' + val + ', ' + uc + ', ' + lc + ')]';
      }
      return axis + '[@' + attr + '=' + val + ']';
    } else if (op === '*=') {
      if (caseInsensitiveFlag && caseInsensitiveFlag.trim() === 'i') {
        return axis + '[contains(translate(normalize-space(@' + attr + '), ' + uc + ', ' + lc + '), translate(' + val + ', ' + uc + ', ' + lc + '))]';
      }
      return axis + '[contains(@' + attr + ',' + val + ')]';
    } else if (op === '^=') {
      if (caseInsensitiveFlag && caseInsensitiveFlag.trim() === 'i') {
        return axis + '[starts-with(translate(normalize-space(@' + attr + '), ' + uc + ', ' + lc + '), translate(' + val + ', ' + uc + ', ' + lc + '))]';
      }
      return axis + '[starts-with(@' + attr + ',' + val + ')]';
    } else if (op === '$=') {
      const valLength = `string-length(${val})`;
      const attrLength = `string-length(@${attr})`;
      const translatedAttr = `translate(normalize-space(@${attr}), ${uc}, ${lc})`;
      const translatedVal = `translate(${val}, ${uc}, ${lc})`;
      const translatedAttrLength = `string-length(${translatedAttr})`;

      if (caseInsensitiveFlag && caseInsensitiveFlag.trim() === 'i') {
        return `[substring(${translatedAttr}, ${translatedAttrLength} - string-length(${translatedVal}) + 1) = ${translatedVal}]`;
      }
      return `[substring(@${attr}, ${attrLength} - ${valLength} + 1) = ${val}]`;
    } else if (op === '~=') { // contains word
      return axis + '[contains(concat(" ",normalize-space(@' + attr + ')," "),concat(" ","' + val + '"," "))]';
    } else if (op === '~') {
      return axis + '[contains(concat(" ",normalize-space(@' + attr + ')," "),concat(" ","' + val + '"," "))]';
    } else if (op === '!') {
      return axis + '[not(@' + attr + ') or @' + attr + '!="' + val + '"]';
    } else if (op === '$') {
      return axis + '[substring(@' + attr + ',string-length(@' + attr + ')-(string-length("' + val + '")-1))="' + val + '"]';
    } else if (op === '^') {
      return axis + '[starts-with(@' + attr + ',"' + val + '")]';
    } else if (op === '|') {
      return axis + '[@' + attr + '="' + val + '" or starts-with(@' + attr + ',concat("' + val + '","-"))]';
    } else {
      if (comp === undefined) {
        if (attr.charAt(attr.length - 1) === '(' || 
            attr.search(/^[0-9]+$/) !== -1 || 
            attr.indexOf(':') !== -1) {
          return str;
        }
        return axis + '[@' + attr + ']';
      } else {
        return axis + '[@' + attr + '="' + val + '"]';
      }
    }
  };

  // CSS pseudo classes regex and callback
  // The simplified regex doesn't capture all the groups needed by the callback
  // const pseudoClassesRegex = /\:([\-a-z]+)(?:\(([^\)]*)\))?/g;

  // Revert to the original regex that matches the function's expectations
  const pseudoClassesRegex = /:([a-z\-]+)(\((\x1F+)(([^\x1F]+(\3\x1F+)?)*)(\3\)))?/g;
  
  const processPseudoClasses = function (match, name, g1, g2, arg, g3, g4, g5, offset, orig) {
    if (orig.charAt(offset - 1) === ':' && orig.charAt(offset - 2) !== ':') {
      // XPath "axis::node-name" will match
      // Detect false positive ":node-name"
      return match;
    }

    if (name === 'odd' || name === 'even') {
      arg = name;
      name = 'nth-of-type';
    }

    switch (name) { // name.toLowerCase()?
      case 'after':
        return '[count(' + convertCssToXPath('preceding::' + arg, true) + ') > 0]';
      case 'after-sibling':
        return '[count(' + convertCssToXPath('preceding-sibling::' + arg, true) + ') > 0]';
      case 'before':
        return '[count(' + convertCssToXPath('following::' + arg, true) + ') > 0]';
      case 'before-sibling':
        return '[count(' + convertCssToXPath('following-sibling::' + arg, true) + ') > 0]';
      case 'checked':
        return '[@selected or @checked]';
      case 'contains':
        return '[contains(' + normalizeSpaceExpr + ',' + arg + ')]';
      case 'icontains':
        return '[contains(' + lowercaseExpression + ',' + convertToLowercase(arg) + ')]';
      case 'empty':
        return '[not(*) and not(normalize-space())]';
      case 'enabled':
        return '[not(@disabled)]';
      case 'disabled':
        return '[@disabled]';
      case 'first-child':
        return '[not(preceding-sibling::*)]';
      case 'first':
      case 'limit':
      case 'first-of-type':
        if (arg !== undefined) {
          return '[position()<=' + arg + ']';
        }
        return '[1]';
      case 'gt':
        // Position starts at 0 for consistency with Sizzle selectors
        return '[position()>' + (parseInt(arg, 10) + 1) + ']';
      case 'lt':
        // Position starts at 0 for consistency with Sizzle selectors
        return '[position()<' + (parseInt(arg, 10) + 1) + ']';
      case 'last-child':
        return '[not(following-sibling::*)]';
      case 'only-child':
        return '[not(preceding-sibling::*) and not(following-sibling::*)]';
      case 'only-of-type':
        return '[not(preceding-sibling::*[name()=name(self::node())]) and not(following-sibling::*[name()=name(self::node())])]';
      case 'nth-child':
        if (isNumericValue(arg)) {
          return '[(count(preceding-sibling::*)+1) = ' + arg + ']';
        }
        switch (arg) {
          case 'even':
            return '[(count(preceding-sibling::*)+1) mod 2=0]';
          case 'odd':
            return '[(count(preceding-sibling::*)+1) mod 2=1]';
          default:
            let a = (arg || '0').replace(nthEquationRegex, '$1+$2').split('+');
            a[0] = a[0] || '1';
            a[1] = a[1] || '0';
            return '[(count(preceding-sibling::*)+1)>=' + a[1] + ' and ((count(preceding-sibling::*)+1)-' + a[1] + ') mod ' + a[0] + '=0]';
        }
      case 'nth-of-type':
        if (isNumericValue(arg)) {
          return '[' + arg + ']';
        }
        switch (arg) {
          case 'odd':
            return '[position() mod 2=1]';
          case 'even':
            return '[position() mod 2=0 and position()>=0]';
          default:
            let a = (arg || '0').replace(nthEquationRegex, '$1+$2').split('+');
            a[0] = a[0] || '1';
            a[1] = a[1] || '0';
            return '[position()>=' + a[1] + ' and (position()-' + a[1] + ') mod ' + a[0] + '=0]';
        }
      case 'eq':
      case 'nth':
        // Position starts at 0 for consistency with Sizzle selectors
        if (isNumericValue(arg)) {
          return '[' + (parseInt(arg, 10) + 1) + ']';
        }
        return '[1]';
      case 'text':
        return '[@type="text"]';
      case 'istarts-with':
        return '[starts-with(' + lowercaseExpression + ',' + convertToLowercase(arg) + ')]';
      case 'starts-with':
        return '[starts-with(' + normalizeSpaceExpr + ',' + arg + ')]';
      case 'iends-with':
        return '[' + createEndsWith(lowercaseExpression, convertToLowercase(arg)) + ']';
      case 'ends-with':
        return '[' + createEndsWith(normalizeSpaceExpr, arg) + ']';
      case 'has':
        const hasXpath = addAxisPrefix(convertCssToXPath(arg, true), './/');
        return '[count(' + hasXpath + ') > 0]';
      case 'has-sibling':
        const siblingXpath = convertCssToXPath('preceding-sibling::' + arg, true);
        return '[count(' + siblingXpath + ') > 0 or count(following-sibling::' + siblingXpath.substr(19) + ') > 0]';
      case 'has-parent':
        return '[count(' + convertCssToXPath('parent::' + arg, true) + ') > 0]';
      case 'has-ancestor':
        return '[count(' + convertCssToXPath('ancestor::' + arg, true) + ') > 0]';
      case 'last':
      case 'last-of-type':
        if (arg !== undefined) {
          return '[position()>last()-' + arg + ']';
        }
        return '[last()]';
      case 'selected': // Sizzle: "(option) elements that are currently selected"
        return '[local-name()="option" and @selected]';
      case 'skip':
      case 'skip-first':
        return '[position()>' + arg + ']';
      case 'skip-last':
        if (arg !== undefined) {
          return '[last()-position()>=' + arg + ']';
        }
        return '[position()<last()]';
      case 'root':
        return '[not(parent::*)]'; // Will be prefixed with .// later
      case 'range':
        const arr = arg.split(',');
        return '[' + arr[0] + '<=position() and position()<=' + arr[1] + ']';
      case 'input': // Sizzle: "input, button, select, and textarea are all considered to be input elements."
        return '[local-name()="input" or local-name()="button" or local-name()="select" or local-name()="textarea"]';
      case 'internal':
        return internalLinkFilter;
      case 'external':
        return externalLinkFilter;
      case 'http':
      case 'https':
      case 'mailto':
      case 'javascript':
        return '[starts-with(@href,concat("' + name + '",":"))]';
      case 'domain':
        return '[(string-length(' + extractUrlDomain(null, urlAttributes) + ')=0 and contains(' + extractUrlDomain(namespaceUri, urlAttributes) + ',' + arg + ')) or contains(' + extractUrlDomain(null, urlAttributes) + ',' + arg + ')]';
      case 'path':
        return '[starts-with(' + extractUrlPath() + ',substring-after("' + arg + '","/"))]';
      case 'not':
        let notXpath = convertCssToXPath(arg, true);
        if (notXpath.charAt(0) === '[') {
          notXpath = 'self::node()' + notXpath;
        }
        return '[not(' + notXpath + ')]';
      case 'target':
        // Direct translation is complex as it depends on URL fragment.
        // This placeholder indicates the intent.
        // A test environment might replace _TARGET_ID_PLACEHOLDER_ or use a specific attribute.
        return '[@id = "_TARGET_ID_PLACEHOLDER_"]';
      case 'lang':
        const langCode = arg ? arg.replace(/['"]/g, '') : '';
        if (langCode) {
          return `[@lang="${langCode}" or starts-with(@lang, "${langCode}-")]`;
        }
        return '[@lang]'; // For :lang without an argument, if ever supported like that
      case 'read-only':
        return '[@readonly]';
      case 'read-write':
        return '[not(@readonly)]';
      case 'valid':
      case 'required':
      case 'in-range':
      case 'out-of-range':
        return '[@' + name + ']';
      case 'invalid':
        return '[@invalid]';
      case 'optional':
        return '[not(@required)]';
      case 'disabled': // Ensure it's explicitly here
        return '[@disabled]';
      case 'enabled': // New
        return '[not(@disabled)]';
      case 'checked': // Already exists
        return '[@selected or @checked]';
      case 'read-only': // New
        return '[@readonly]';
      case 'read-write': // New
        return '[not(@readonly)]';
      default:
        return match;
    }
  };

  // CSS IDs and classes regex and callback
  const idsAndClassesRegex = /(#|\.)([^\#\@\.\/\(\[\)\]\|\:\s\+\>\<\'\"\x1D-\x1F]+)/g;
  
  const processIdsAndClasses = function (str, op, val, offset, orig) {
    const axis = '';
    /* var prevChar = orig.charAt(offset-1);
    if (prevChar.length === 0 ||
        prevChar === '/' ||
        prevChar === '(')
        axis = '*';
    else if (prevChar === ':')
        axis = 'node()';*/
    if (op === '#') {
      return axis + '[@id="' + val + '"]';
    }
    return axis + '[contains(concat(" ",normalize-space(@class)," ")," ' + val + ' ")]';
  };

  // Prepend descendant-or-self if no other axis is specified
  function addAxisPrefix(s, axis) {
    // Don't add axis prefix if the selector already starts with a valid XPath expression
    if (s.startsWith('/') || s.startsWith('.//')) {
      return s;
    }
    
    return s.replace(firstAxisRegex, function (match, start, literal) {
      if (literal.substr(literal.length - 2) === '::') { // Already has axis::
        return match;
      }

      if (literal.charAt(0) === '[') {
        axis += '*';
      }
      return start + axis + literal;
    });
  }

  // Find the beginning of the selector, starting at i and working backwards
  function findSelectorStart(s, i) {
    let depth = 0;
    let offset = 0;

    while (i--) {
      switch (s.charAt(i)) {
        case ' ':
        case parenEscapeChar:
          offset++;
          break;
        case '[':
        case '(':
          depth--;
          if (depth < 0) {
            return ++i + offset;
          }
          break;
        case ']':
        case ')':
          depth++;
          break;
        case ',':
        case '|':
          if (depth === 0) {
            return ++i + offset;
          }
        default:
          offset = 0;
      }
    }

    return 0;
  }

  // Append escape "char" to "open" or "close"
  function addEscapeChars(s, open, close, char) {
    let depth = 0;

    return s.replace(new RegExp('[\\' + open + '\\' + close + ']', 'g'), function (a) {
      if (a === open) {
        depth++;
      }

      if (a === open) {
        return a + generateRepeatedString(char, depth);
      } else {
        return generateRepeatedString(char, depth--) + a;
      }
    });
  }

  // Cache regular expressions for better performance
  const REGEX_CACHE = {
    stringLiteral: /("[^"\x1E]*"|'[^'\x1E]*'|=\s*[^\s\]\'\"]+)/g,
    escapedLiteral: /['"]?(\x1E+)['"]?/g,
    pseudoElementStripper: /::[a-zA-Z\-]+(\([^\)]*\))?/g, // For stripping pseudo-elements
    // ... other regex patterns ...
  };

  // Internal implementation for the actual conversion
  function convertCssToXPath(s, nested) {
    if (nested === true) {
      // Replace :pseudo-classes
      s = s.replace(pseudoClassesRegex, processPseudoClasses);

      // Replace #ids and .classes
      s = s.replace(idsAndClassesRegex, processIdsAndClasses);

      return s;
    }

    // Tag open and close parenthesis pairs (for RegExp searches)
    s = addEscapeChars(s, '(', ')', parenEscapeChar);

    // Remove and save any string literals
    const literals = [];

    s = s.replace(stringLiteralRegex, function (s, a) {
      if (a.charAt(0) === '=') {
        a = a.substr(1).trim();

        if (isNumericValue(a)) {
          return s;
        }
      } else {
        a = a.substr(1, a.length - 2);
      }

      return generateRepeatedString(literalEscapeChar, literals.push(a));
    });

    // Replace CSS combinators (" ", "+", ">", "~", ",") and reverse combinators ("!", "!+", "!>", "!~")
    s = s.replace(combinatorsRegex, processCombinators);

    // Replace CSS attribute filters
    s = s.replace(attributesRegex, processAttributes);

    // Wrap certain :pseudo-classes in parens (to collect node-sets)
    while (true) {
      const index = s.search(wrapPseudoRegex);

      if (index === -1) {
        break;
      }
      const pseudoIndex = s.indexOf(':', index);
      const start = findSelectorStart(s, pseudoIndex);

      s = s.substr(0, start) +
          '(' + s.substring(start, pseudoIndex) + ')' +
          s.substr(pseudoIndex);
    }

    // Replace :pseudo-classes
    s = s.replace(pseudoClassesRegex, processPseudoClasses);

    // Replace #ids and .classes
    s = s.replace(idsAndClassesRegex, processIdsAndClasses);

    // Restore the saved string literals
    s = s.replace(escapedLiteralRegex, function (s, a) {
      const str = literals[a.length - 1];
      return '"' + str + '"';
    });

    // Remove any special characters
    s = s.replace(specialCharsRegex, '');

    // add * to stand-alone filters
    s = s.replace(filterPrefixRegex, '$1*[');

    // add "/" between @attribute selectors
    s = s.replace(attrPrefixRegex, '$1/@');

    /*
    Combine multiple filters?

    s = addEscapeChars(s, '[', ']', filter_char);
    s = s.replace(/(\x1D+)\]\[\1(.+?[^\x1D])\1\]/g, ' and ($2)$1]')
    */

    // Fix for whitespace handling in combinators
    s = s.replace(/\s+/g, ' ');
    
    s = addAxisPrefix(s, './/'); // prepend ".//" axis to beginning of CSS selector
    return s;
  }

  // Use the internal function for the conversion
  const result = convertCssToXPath(selector, isNested);
  
  // Use the prefix option if provided
  return result.replace(/^\.\/\//, prefix);
}

// Helper module functions re-exported
export const xpathUtils = {
  convertToLowercase,
  createEndsWith,
  extractUrl,
  extractUrlPath,
  extractUrlDomain,
  validateSelector,
  isNumericValue,
  generateRepeatedString,
  createXPathExpression
};