nwmatcher-pseudos.js 8.93 KB
/*
 * Copyright (C) 2007-2018 Diego Perini
 * All rights reserved.
 *
 * CSS3 pseudo-classes extension for NWMatcher
 *
 * Added capabilities:
 *
 * - structural pseudo-classes
 *
 * :root, :empty,
 * :nth-child(), nth-of-type(),
 * :nth-last-child(), nth-last-of-type(),
 * :first-child, :last-child, :only-child
 * :first-of-type, :last-of-type, :only-of-type
 *
 * - negation, language, target and UI element pseudo-classes
 *
 * :not(), :target, :lang(), :target
 * :link, :visited, :active, :focus, :hover,
 * :checked, :disabled, :enabled, :selected
 */

(function(global) {

  var LINK_NODES = {
    'a': 1, 'A': 1,
    'area': 1, 'AREA': 1,
    'link': 1, 'LINK': 1
  },

  root = document.documentElement,

  contains = 'compareDocumentPosition' in root ?
    function(container, element) {
      return (container.compareDocumentPosition(element) & 16) == 16;
    } : 'contains' in root ?
    function(container, element) {
      return element.nodeType == 1 && container.contains(element);
    } :
    function(container, element) {
      while ((element = element.parentNode) && element.nodeType == 1) {
        if (element === container) return true;
      }
      return false;
    },

  isLink =
    function(element) {
      return element.getAttribute('href') && LINK_NODES[element.nodeName];
    },

  isEmpty =
    function(node) {
      node = node.firstChild;
      while (node) {
        if (node.nodeType == 3 || node.nodeName > '@') return false;
        node = node.nextSibling;
      }
      return true;
    },

  nthElement =
    function(element, last) {
      var count = 1, succ = last ? 'nextSibling' : 'previousSibling';
      while ((element = element[succ])) {
        if (element.nodeName > '@') ++count;
      }
      return count;
    },

  nthOfType =
    function(element, last) {
      var count = 1, succ = last ? 'nextSibling' : 'previousSibling', type = element.nodeName;
      while ((element = element[succ])) {
        if (element.nodeName == type) ++count;
      }
      return count;
    };

  NW.Dom.Snapshot['contains'] = contains;

  NW.Dom.Snapshot['isLink'] = isLink;
  NW.Dom.Snapshot['isEmpty'] = isEmpty;
  NW.Dom.Snapshot['nthOfType'] = nthOfType;
  NW.Dom.Snapshot['nthElement'] = nthElement;

})(this);

NW.Dom.registerSelector(
  'nwmatcher:spseudos',
  /^\:(root|empty|(?:first|last|only)(?:-child|-of-type)|nth(?:-last)?(?:-child|-of-type)\(\s*(even|odd|(?:[-+]{0,1}\d*n\s*)?[-+]{0,1}\s*\d*)\s*\))?(.*)/i,
  (function(global) {

    return function(match, source) {

      var a, n, b, status = true, test, type;

      switch (match[1]) {

        case 'root':
          if (match[3])
            source = 'if(e===h||s.contains(h,e)){' + source + '}';
          else
            source = 'if(e===h){' + source + '}';
          break;

        case 'empty':
          source = 'if(s.isEmpty(e)){' + source + '}';
          break;

        default:
          if (match[1] && match[2]) {

            if (match[2] == 'n') {
              source = 'if(e!==h){' + source + '}';
              break;
            } else if (match[2] == 'even') {
              a = 2;
              b = 0;
            } else if (match[2] == 'odd') {
              a = 2;
              b = 1;
            } else {
              b = ((n = match[2].match(/(-?\d+)$/)) ? parseInt(n[1], 10) : 0);
              a = ((n = match[2].match(/(-?\d*)n/i)) ? parseInt(n[1], 10) : 0);
              if (n && n[1] == '-') a = -1;
            }
            test = a > 1 ?
              (/last/i.test(match[1])) ? '(n-(' + b + '))%' + a + '==0' :
              'n>=' + b + '&&(n-(' + b + '))%' + a + '==0' : a < -1 ?
              (/last/i.test(match[1])) ? '(n-(' + b + '))%' + a + '==0' :
              'n<=' + b + '&&(n-(' + b + '))%' + a + '==0' : a === 0 ?
              'n==' + b : a == -1 ? 'n<=' + b : 'n>=' + b;
            source =
              'if(e!==h){' +
                'n=s[' + (/-of-type/i.test(match[1]) ? '"nthOfType"' : '"nthElement"') + ']' +
                  '(e,' + (/last/i.test(match[1]) ? 'true' : 'false') + ');' +
                'if(' + test + '){' + source + '}' +
              '}';

          } else if (match[1]) {

            a = /first/i.test(match[1]) ? 'previous' : 'next';
            n = /only/i.test(match[1]) ? 'previous' : 'next';
            b = /first|last/i.test(match[1]);
            type = /-of-type/i.test(match[1]) ? '&&n.nodeName!==e.nodeName' : '&&n.nodeName<"@"';
            source = 'if(e!==h){' +
              ( 'n=e;while((n=n.' + a + 'Sibling)' + type + ');if(!n){' + (b ? source :
                'n=e;while((n=n.' + n + 'Sibling)' + type + ');if(!n){' + source + '}') + '}' ) + '}';

          } else {

            status = false;

          }
          break;
      }

      return {
        'source': source,
        'status': status
      };

    };

  })(this));

NW.Dom.registerSelector(
  'nwmatcher:dpseudos',
  /^\:(link|visited|target|active|focus|hover|checked|disabled|enabled|selected|lang\(([-\w]{2,})\)|not\(\s*(:nth(?:-last)?(?:-child|-of-type)\(\s*(?:even|odd|(?:[-+]{0,1}\d*n\s*)?[-+]{0,1}\s*\d*)\s*\)|[^()]*)\s*\))?(.*)/i,
  (function(global) {

    var doc = global.document,
    Config = NW.Dom.Config,
    Tokens = NW.Dom.Tokens,

    reTrimSpace = RegExp('^\\s+|\\s+$', 'g'),

    reSimpleNot = RegExp('^((?!:not)' +
      '(' + Tokens.prefixes + '|' + Tokens.identifier +
      '|\\([^()]*\\))+|\\[' + Tokens.attributes + '\\])$');

    return function(match, source) {

      var expr, status = true, test;

      switch (match[1].match(/^\w+/)[0]) {

        case 'not':
          expr = match[3].replace(reTrimSpace, '');
          if (Config.SIMPLENOT && !reSimpleNot.test(expr)) {
            NW.Dom.emit('Negation pseudo-class only accepts simple selectors "' + selector + '"');
          } else {
            if ('compatMode' in doc) {
              source = 'if(!' + NW.Dom.compile(expr, '', false) + '(e,s,d,h,g)){' + source + '}';
            } else {
              source = 'if(!s.match(e, "' + expr.replace(/\x22/g, '\\"') + '",g)){' + source +'}';
            }
          }
          break;

        case 'checked':
          source = 'if((typeof e.form!=="undefined"&&(/^(?:radio|checkbox)$/i).test(e.type)&&e.checked)' +
            (Config.USE_HTML5 ? '||(/^option$/i.test(e.nodeName)&&(e.selected||e.checked))' : '') +
            '){' + source + '}';
          break;

        case 'disabled':
          source = 'if(((typeof e.form!=="undefined"' +
            (Config.USE_HTML5 ? '' : '&&!(/^hidden$/i).test(e.type)') +
            ')||s.isLink(e))&&e.disabled===true){' + source + '}';
          break;

        case 'enabled':
          source = 'if(((typeof e.form!=="undefined"' +
            (Config.USE_HTML5 ? '' : '&&!(/^hidden$/i).test(e.type)') +
            ')||s.isLink(e))&&e.disabled===false){' + source + '}';
          break;

        case 'lang':
          test = '';
          if (match[2]) test = match[2].substr(0, 2) + '-';
          source = 'do{(n=e.lang||"").toLowerCase();' +
            'if((n==""&&h.lang=="' + match[2].toLowerCase() + '")||' +
            '(n&&(n=="' + match[2].toLowerCase() +
            '"||n.substr(0,3)=="' + test.toLowerCase() + '")))' +
            '{' + source + 'break;}}while((e=e.parentNode)&&e!==g);';
          break;

        case 'target':
          source = 'if(e.id==d.location.hash.slice(1)){' + source + '}';
          break;

        case 'link':
          source = 'if(s.isLink(e)&&!e.visited){' + source + '}';
          break;

        case 'visited':
          source = 'if(s.isLink(e)&&e.visited){' + source + '}';
          break;

        case 'active':
          source = 'if(e===d.activeElement){' + source + '}';
          break;

        case 'hover':
          source = 'if(e===d.hoverElement){' + source + '}';
          break;

        case 'focus':
          source = 'hasFocus' in doc ?
            'if(e===d.activeElement&&d.hasFocus()&&(e.type||e.href||typeof e.tabIndex=="number")){' + source + '}' :
            'if(e===d.activeElement&&(e.type||e.href)){' + source + '}';
          break;

        case 'selected':
          source = 'if(/^option$/i.test(e.nodeName)&&(e.selected||e.checked)){' + source + '}';
          break;

        default:
          status = false;
          break;
      }

      return {
        'source': source,
        'status': status
      };

    };

  })(this));

NW.Dom.registerSelector(
  'nwmatcher:epseudos',
  /^((?:[:]{1,2}(?:after|before|first-letter|first-line))|(?:[:]{2,2}(?:selection|backdrop|placeholder)))?(.*)/i,
  (function(global) {

    return function(match, source) {

      var status = true;

      switch (match[1].match(/(\w+)$/)[0]) {

        case 'after':
        case 'before':
        case 'first-letter':
        case 'first-line':
        case 'selection':
        case 'backdrop':
        case 'placeholder':
          source = 'if(!(/1|11/).test(e.nodeType)){' + source + '}';
          break;

        default:
          status = false;
          break;
      }

      return {
        'source': source,
        'status': status
      };

    };

  })(this));