0

I have created this code to calculate css rules priority

var selectorText = "body";
A = selectorText.match(/#/gm); // id
A = !A ? 0 : A.length;
B1 = selectorText.match(/\\./gm); // class
B1 = !B1 ? 0 : B1.length;
B2 = selectorText.match(/[[]/gm); // attribute selector
B2 = !B2 ? 0 : B2.length;
B3 = selectorText.match(/[\\w\\d_\\s^]:(?!:)/gm); // pseudo třída
B3 = !B3 ? 0 : B3.length;
B = B1 + B2 + B3;
C1 = selectorText.match(/::/gm); // pseudo element
C1 = !C1 ? 0 : C1.length;
C2 = selectorText.match(/\\w+[$\\s#\\.:\\[]/gm); // element
C2 = !C2 ? 0 : C2.length;
C = C1 + C2;
A *= 10000;
B *= 100;
alert(C)

There must not be more than 100 matches (A, B or C) in a selector. For some reason the result of C is 0 (but I use "body" selector). Can you suggest how to correct the last match? Also do you see any mistakes in the code?

Note:

The original question stated just "body" selector, but I think about any selector or multiple selectors like this: "div#menu ul li, div#id, div.class" the selector can contain pseudo-classes, pseudo-elements, attribute selectors. In the case of C2 regex, I am trying to find count of elements which here in the last string is 5 elements total (then C2 should be 5).

John Boe
  • 3,501
  • 10
  • 37
  • 71
  • `selectorText` does not contain `::`, `.match(/::/,gm)` should return `null` – guest271314 Sep 04 '16 at 18:19
  • You can't parse CSS with regex. Or not like this, at least. Consider for example `#a, #b`, whose specificity is (1,0,0) but this says (2,0,0). And you can't split at commas ans calculate each part separately, because some operands have commas. So you need a CSS parser. – Oriol Sep 04 '16 at 19:39
  • @user1141649 You need a CSS parser written in JS which exposes this information. – Oriol Sep 04 '16 at 20:17
  • @Oriol Could I replace comma from string like this `var selectorText = "div#id, div.body;#a > :matches(.b, #c)";` for some character like "^"? `var selectorText = "div#id, div.body;#a > :matches(.b^ #c)";` I could use .replace to search the contents of brackets and replace it. – John Boe Sep 04 '16 at 20:25
  • @user1141649 You want to replace commas inside parentheses? That might work, but regular expressions in JS can't handle recursion, and the number of nested parentheses can be arbitrary. And then you would need to undo the replacement in order to calculate the specificity of `.b, #c` recursively. – Oriol Sep 04 '16 at 20:38
  • @Oriol: Should I use only () or []? Something like this is valid? – John Boe Sep 04 '16 at 20:53
  • @user1141649 `[]` are currently only used for attribute selectors, and shouldn't allow commas inside. A future version could change that, though. – Oriol Sep 04 '16 at 21:11

2 Answers2

2

selectorText does not contain ::, C1 = selectorText.match(/::/gm); should return null; C1 = !C1 ? 0 : C1.length; returns 0; C2 = selectorText.match(/\\w+[$\s#\\.:\\[]/gm); returns null, C2 = !C2 ? 0 : C2.length; returns 0; C = C1 + C2; is equal to 0; that is 0 + 0 = 0


The original question stated just "body" selector, but I think about any selector or multiple selectors like this: "div#menu ul li, div#id, div.class" the selector can contain pseudo-classes, pseudo-elements, attribute selectors. In the case of C2 regex, I am trying to find count of elements which here in the last string is 5 elements total (then C2 should be 5)

You can use .split() with RegExp /,|\s/ to split multiple selectors at , character; .map(), .filter(Boolean) to remove empty string, .match() with RegExp /^[a-z]+(?=[.#:\s]|$)/i to match element.

var selectorText = "div#menu ul li, div#id, div.class";
C2 = selectorText.split(/,|\s/).filter(Boolean)
     .map(el => el.trim().match(/^[a-z]+(?=[.#:\s]|$)/i)[0]);
console.log(C2);
guest271314
  • 1
  • 15
  • 104
  • 177
  • `C2` is actually set to `0`, initally missed next line where `C2` is re-defined as `0` – guest271314 Sep 04 '16 at 18:27
  • Was not correct, here, with suggestion to try `/\w+(?=[^a-zA-Z]|$)/gm` either. Try `/^[a-z]+(?=[.#:\s]|$)/i` to match word at beginning of string followed by `.`, `#`, `:` or space character or end of input – guest271314 Sep 04 '16 at 18:38
  • `C2 = "body".match(/^[a-z]+(?=[.#:\s]|$)/i);` should return expected result; e.g, `"body.aclass".match(/^[a-z]+(?=[.#:\s]|$)/i);`, `"body#aid".match(/^[a-z]+(?=[.#:\s]|$)/i);` , `"body::apseudo".match(/^[a-z]+(?=[.#:\s]|$)/i);`, `"body #achild".match(/^[a-z]+(?=[.#:\s]|$)/i);` – guest271314 Sep 04 '16 at 18:44
  • _"like this: "div#id, div.body"_ Original Question does not describe multiple selectors being matched. You can split `,` to retrieve correct result `"div#id, div.body".split(/,/).map(el => el.trim().match(/^[a-z]+(?=[.#:\s]|$)/i)[0])` – guest271314 Sep 04 '16 at 19:04
  • Without using `.trim()` `"div#id, div.body".split(/,?\s/).map(el => el.match(/^[a-z]+(?=[.#:\s]|$)/i)[0])` – guest271314 Sep 04 '16 at 19:11
  • 1
    Ah, but you cannot use \s to split. see `div ul li` is selector. Better .split(",").trim() – John Boe Sep 04 '16 at 19:28
  • @user1141649 Good catch. Though, again, note original Question does not describe matching multiple selectors – guest271314 Sep 04 '16 at 19:33
  • _"please delete old comments as I do to make it shorter"_ Why should comments be deleted? Without comments original Question would be requirement; that is, matching single element `"body"`, not matching multiple elements within selector string? As original Question only contains `"body"` as `selectorText`, without describing requirement to match multiple selectors within selector string? In future, instead of adding requirements at comment, perhaps consider Question well before posting Question, to avoid confusion as to actual possible inputs and different expected results? – guest271314 Sep 04 '16 at 19:38
  • 1
    Do not split at commas, it will break things like `#a > :matches(.b, #c)`. – Oriol Sep 04 '16 at 19:48
  • @Oriol Question continues to grow. Original selector was `"body"`, then `"div#id, div.body"`, then `div ul li`, now `"div#menu ul li, div#id, div.class"`. Believe OP is currently attempting to match element, e.g., `C2` at original Question, not `id` , `class` or `pseudo`; though not entirely certain if this is conclusion of possibilities which could emerge from current Question? – guest271314 Sep 04 '16 at 19:50
  • @Oriol There may be a Question at SO have previously read regarding `css` specificty, perhaps with good Answer by BoltClock, though have not read in some time. Was not certain is OP was attempting to use actual `css` specificity algorithm, or define own specificity algorithm – guest271314 Sep 04 '16 at 19:56
  • @user1141649 See updated post, which should meet requirement described at updated Question – guest271314 Sep 04 '16 at 19:59
  • @user1141649 See also http://stackoverflow.com/questions/5158631/sorting-a-set-of-css-selectors-on-the-basis-of-specificity/5158683#5158683 , http://stackoverflow.com/questions/21455250/css-specificity-defining-classes-ids/21456395#21456395 – guest271314 Sep 04 '16 at 20:05
0

First of all I needed to solve how to parse the multiple selectors. As Oriol said I cannot parse multiple selectors with regex. This function does what I need.

String.prototype.findStrings = function (s = ',') {
  var prev = null, prevEscaped = null, typq = null, 
      typp = null, depRB = 0, depSB = 0;  
  var obj = { length: 0, 
    results: { array: [], A: null, B: null, C: null }
    };
  var action = null;
  if (s.constructor === Array)
    {
    action = 1;
    }
  else
    {
    if (typeof s === 'string' || s instanceof String)
      action = 0;
    }
  if ( action === null)
    return false;
  for (var i=0; i<this.length; i++)
    {
    // not escaped:
    if (!prevEscaped)
      { // not escaped:
      switch (this[i])
        {
        case '\\':
          prevEscaped = true;
          prev = '\\'; // previous escaped
        break;
        case '"':
          if (!typq)
            {
              typq = '"';
            }
          else if ( typq=='"' ) 
            { // end quotes
              typq = null;
              continue;
            }
        break;
        case "'": 
          if (!typq)
            {
            typq = "'";
            }
          else if ( typq=="'" ) 
            { // end quotes
            typq = null;
            continue;
            }
        break;
        }
      }
    else
      {  // is escaped - do nothing
      prevEscaped = false;
      prev = null;
      continue;
      }

    if (!typq) // no quotes block
      {
      switch (this[i])
        {
        case '(':
          if (!typp)
              typp = '('; // defines higher priority of () parenthesis
          depRB++;
        break;
        case "[": 
          if (!typp)
              typp = '[';
          depSB++;
        break;
        case ")": 
          depRB--;
          if (!depRB)
            {
            if ( typp == "(" )
              {
              typp = null; // end block of parenthesis
              continue;
              }
            }
        break;
        case "]": 
          depSB--;
          if (!depSB)
            {
            if ( typp == "[" )
              {
              typp = null; // end block of parenthesis
              continue;
              }
            }
        break;
        }
      }
    if (typp)    // commas inside block of parenthesis of higher priority are skipped
      continue;

    if (!action) // Separate by string s
      {
      if ( this[i] == s )
        { 
        obj.results.array.push(i);
        obj.length++;
        }
      }
    else 
      {
      }    
    // Block of no quotes, no parenthesis follows

    } // end for

  // Last item
  obj.results.array.push(i);
  obj.length++;
  return obj;
};
/* Return array of strings */
String.prototype.splitFromArray = function (arr, autotrim = true) {
  var prev = 0, results = [];
  for (var i = 0; i<arr.length; i++)
    {
    var s = "";
    s = this.substring(prev+1,arr[i]).trim();    
    results.push( s );
    prev = arr[i];
    }
return results;
};

This is implemented like this:

var test = '#a > :matches(.b, #c) , input.match(title="\\City \\"of ' + "'" + 'silly' + "'" +' people.", data=' + "'alpha=" + '"' + "string" + '"' + ",beta=2'" + ' value="New York"), article.class1 ul li'; 
var separators = test.findStrings();
console.log(separators);
var myArr = test.splitFromArray(separators.results.array);
console.log(myArr);

Sorry that the selectors are not valid in the example but this just illustrates parsing. You can edit it to get valid selectors.

To add the search rules will not be hard.

John Boe
  • 3,501
  • 10
  • 37
  • 71