Advertisement
Alhadis

CSS Tokeniser

Mar 10th, 2014
254
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /** Parses a block of CSS code and returns it as a serialised object. Supports even unorthodox use such as nested rulesets and "junk" declarations. Comments are left intact. */
  2. function tokenise(e){var t=[],e=undefined==="a"[0]?e.split(""):e,n=/^\s+|\s+$/g,r,i=0,s,o,u,a=t;for(;;){r=e[i];++i;if(undefined===r){if(s){if(1===s.type){a.push(s);if(s.P){u=s;s=s.P;delete u.P}else s=null}else{o={2:"[",4:"(",8:'"',16:"'"};while(30&s.type)if(s.P){u=s;s=s.P;console.log(u.data,u.type);s.data+=o[u.type]+u.data}}if(s){if(32===s.type){if(s.data){if(o=s.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))s.push({name:o[1],value:o[2].replace(n,"")});else if(s.data=s.data.replace(n,""))s.push(s.data)}}while(s.P){u=s;a=s=u.P;delete u.P;delete u.data}t.push(s)}s=null;a=t;u=null}break}if(s){if(1===s.type){if("*"===r&&"/"===e[i]){++i;a.push(s);if(s.P){u=s;s=s.P;delete u.P}else s=null}else s.data+=r}else if(2===s.type){if("]"===r){if(s.P){u=s;s=s.P;s.data+="["+u.data+"]";u=null}else{s.data="["+s.data+"]";delete s.type}}else if('"'===r)s={type:8,P:s,data:""};else if("'"===r)s={type:16,P:s,data:""};else if("["===r){s={type:2,P:s,data:""}}else if("/"===r&&"*"===e[i]){++i;u=s;s={type:1,data:"",P:u};if((u.data||u.textBefore)&&(u=((u.textBefore||"")+u.data).replace(/^\s+/g,"")))s.textBefore=u;u=null}else s.data+=r}else if(4===s.type){if(")"===r){if(s.P){u=s;s=s.P;s.data+="("+u.data+")";u=null}else{s.data="("+s.data+")";delete s.type}}else if('"'===r)s={type:8,P:s,data:""};else if("'"===r)s={type:16,P:s,data:""};else if("["===r){s={type:2,P:s,data:""}}else if("/"===r&&"*"===e[i]){++i;u=s;s={type:1,data:"",P:u};if((u.data||u.textBefore)&&(u=((u.textBefore||"")+u.data).replace(/^\s+/g,"")))s.textBefore=u;u=null}else s.data+=r}else if(8===s.type){if('"'===r&&!("\\"===e[i-2]&&false===/"(?:[^\\"]|\\.)*"/g.test('"'+s.data+'"'))){if(s.P){u=s;s=s.P;s.data+='"'+u.data+'"';u=null}else{s.data='"'+s.data+'"';delete s.type}}else s.data+=r}else if(16===s.type){if("'"===r&&!("\\"===e[i-2]&&false===/'(?:[^\\']|\\.)*'/g.test("'"+s.data+"'"))){if(s.P){u=s;s=s.P;s.data+="'"+u.data+"'";u=null}else{s.data="'"+s.data+"'";delete s.type}}else s.data+=r}else{if("["===r){s={type:2,P:s,textBefore:s.data+"[",data:""}}else if("("===r){s={type:4,P:s,textBefore:s.data+"(",data:""}}else if('"'===r){s={type:8,P:s,textBefore:s.data+'"',data:""}}else if("'"===r){s={type:16,P:s,textBefore:s.data+"'",data:""}}else if("{"===r){a=[];a.type=32;a.data="";a.name=s.data.replace(n,"");s.data="";if(s instanceof Array){s.push(a);a.P=s}else a.P=s.P;s=a}else if(";"===r){if(o=s.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))s.push({name:o[1],value:o[2]});else(s instanceof Array?s:a).push(s.data.replace(/^\s+/g,"")+";");s.data=""}else if("}"===r){if(s.data){if(o=s.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))s.push({name:o[1],value:o[2].replace(n,"")});else{if(s.data=s.data.replace(n,""))s.push(s.data)}}u=s;if(s.P){a=s=u.P}else{t.push(s);s=null;a=t}delete u.P;delete u.data;u=null}else if("/"===r&&"*"===e[i]){++i;u=s;s={type:1,data:"",P:u};if(u.data&&(u=u.data.replace(/^\s+/g,"")))s.textBefore=u;u=null}else{s.data+=r}}}else{if("    "===r||"\n"===r||" "===r)continue;if("/"===r&&"*"===e[i]){++i;s={type:1,data:""}}else if("{"===r){a=s=[];s.type=32;s.data=s.name=""}else s={data:r}}}return t};
  3.  
  4.  
  5.  
  6. /** Uncompressed version of above function so y'all can see what the hell's going on. */
  7. function tokenise(string){
  8.  
  9.     /** Returned CSS array. */
  10.     var CSS =   [],
  11.  
  12.     /** Run a quick-and-dirty hack for browsers that don't support direct character access in strings (thanks, IE7) */
  13.         string  =   (undefined === "a"[0] ? string.split("") : string),
  14.  
  15.     /** Token type constants */
  16.         T_COMMENT           =   1,
  17.         T_BRACKETS_SQUARE   =   2,
  18.         T_BRACKETS_ROUND    =   4,
  19.         T_QUOTES_DOUBLE     =   8,
  20.         T_QUOTES_SINGLE     =   16,
  21.         T_BLOCK             =   32,
  22.  
  23.         /* Matches any bracket or quote-like token type. */
  24.         T_DELIMITER         =   30,
  25.  
  26.  
  27.  
  28.     /** RegExp for stripping leading/trailing whitespace. */
  29.         rTrim   =   /^\s+|\s+$/g,
  30.  
  31.  
  32.     /** Iterator variables */
  33.         char, index =   0, token,
  34.  
  35.  
  36.     /** Junk variables: used for juggling data within the loop. May be overwritten for whatever. */
  37.         prop, prev,
  38.  
  39.  
  40.     /**
  41.      *  Pointer to the last block that was opened in the token stack. Used for dumping injected comments that were found between parsable tokens.
  42.      *  Note that injected comments retain a copy of the leading character data (minus whitespace) so developers can use any injected commentary
  43.      *  to supply "custom properties" or metadata to their scripts.
  44.      */
  45.         dumpTo = CSS;
  46.  
  47.  
  48.  
  49.     for(;;){
  50.         char    =   string[index];
  51.         ++index;
  52.  
  53.  
  54.         /** EOT? Bail. */
  55.         if(undefined === char){
  56.            
  57.             /** We've still got a token hanging open, which means some idiot developer's forgotten to close a bracket or something. */
  58.             if(token){
  59.                
  60.                 /** Unclosed comment */
  61.                 if(T_COMMENT === token.type){
  62.                     dumpTo.push(token);
  63.                     if(token.parent){
  64.                         prev    =   token;
  65.                         token   =   token.parent;
  66.                         delete prev.parent;
  67.                     }
  68.                     else token  =   null;
  69.                 }
  70.  
  71.  
  72.                 /** Anything else that we were collecting that's supposed to be serialised into a string. */
  73.                 else{
  74.                    
  75.                     /**
  76.                      * Use an object literal for retrieving the leading delimiter characters inside
  77.                      * the following while loop. Saves us running four different checks per cycle.
  78.                      */
  79.                     prop    =   {
  80.                         2:      "[",    //  T_BRACKETS_SQUARE
  81.                         4:      "(",    //  T_BRACKETS_ROUND
  82.                         8:      '"',    //  T_QUOTES_DOUBLE
  83.                         16:     "'"     //  T_QUOTES_SINGLE
  84.                     };
  85.  
  86.                     while(T_DELIMITER & token.type)
  87.                         if(token.parent){
  88.                             prev        =   token;
  89.                             token       =   token.parent;
  90.                             console.log(prev.data, prev.type);
  91.                             token.data  +=  prop[prev.type] + prev.data;
  92.                         }
  93.                 }
  94.  
  95.                 /** Make sure we're not operating on a token that's been emptied from top-level (e.g., unclosed comment block at top-level) */
  96.                 if(token){
  97.  
  98.                     /** Unclosed block */
  99.                     if(T_BLOCK === token.type){
  100.  
  101.                         /** This token's still carrying unassigned data. */
  102.                         if(token.data){
  103.                             if(prop = token.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))
  104.                                 token.push({
  105.                                     name:   prop[1],
  106.                                     value:  prop[2].replace(rTrim, "")
  107.                                 });
  108.  
  109.                             /** Junk (that isn't whitespace) */
  110.                             else if(token.data = token.data.replace(rTrim, ""))
  111.                                 token.push(token.data);
  112.                         }
  113.                     }
  114.  
  115.  
  116.                     /** Right. Now wrap it up. */
  117.                     while(token.parent){
  118.                         prev    =   token;
  119.                         dumpTo  =
  120.                         token   =   prev.parent;
  121.                    
  122.                         delete prev.parent;
  123.                         delete prev.data;
  124.                     }
  125.  
  126.                     CSS.push(token);
  127.                 }
  128.  
  129.                 token   =   null;
  130.                 dumpTo  =   CSS;
  131.                 prev    =   null;
  132.             }
  133.             break;
  134.         }
  135.  
  136.  
  137.  
  138.         /** We've currently picked up a token. */
  139.         if(token){
  140.  
  141.  
  142.  
  143.             /** Comment */
  144.             if(T_COMMENT === token.type){
  145.                
  146.                 /** End of comment. */
  147.                 if("*" === char && "/" === string[index]){
  148.                     ++index;
  149.  
  150.                     /** Because comments are free to be inserted virtually anywhere in CSS, we need to use a special variable
  151.                      for appending them (since .parent may point to a string-only token like brackets). */
  152.                     dumpTo.push(token);
  153.  
  154.                     /** Comment somewhere inside a block */
  155.                     if(token.parent){
  156.                         prev    =   token;
  157.                         token   =   token.parent;
  158.                         delete prev.parent;
  159.                     }
  160.  
  161.                     /** This was a comment at top-level, so don't store any back-references. */
  162.                     else token  =   null;
  163.                 }
  164.  
  165.                 else token.data += char;
  166.             }
  167.  
  168.  
  169.  
  170.             /** [Square brackets] */
  171.             else if(T_BRACKETS_SQUARE === token.type){
  172.  
  173.                 if("]" === char){
  174.                     if(token.parent){
  175.                         prev    =   token;          // Store a reference to the current token so we can append the data after switching.
  176.                         token   =   token.parent;   // Move the focus back to the token's parent.
  177.                         token.data  +=  "[" + prev.data + "]";
  178.                         prev    =   null;
  179.                     }
  180.  
  181.                     /** This could have only been picked up with a selector like "[hidden]" or something without leading word characters. */
  182.                     else{
  183.                         token.data  =   "[" + token.data + "]";
  184.                         delete token.type;
  185.                     }
  186.                 }
  187.  
  188.                 /** Watch out for quotes. */
  189.                 else if('"' === char)   token   =   {type:  T_QUOTES_DOUBLE,    parent: token,  data:   ""};
  190.                 else if("'" === char)   token   =   {type:  T_QUOTES_SINGLE,    parent: token,  data:   ""};
  191.  
  192.  
  193.                 /** Look out for nesting, too. */
  194.                 else if("[" === char){
  195.                     token   =   {
  196.                         type:   T_BRACKETS_SQUARE,
  197.                         parent: token,
  198.                         data:   ""
  199.                     };
  200.                 }
  201.  
  202.  
  203.                 /** Start of an injected comment */
  204.                 else if("/" === char && "*" === string[index]){
  205.                     ++index;
  206.                     prev            =   token;
  207.                     token           =   {
  208.                         type:   T_COMMENT,
  209.                         data:   "",
  210.                         parent: prev
  211.                     };
  212.  
  213.                     if((prev.data || prev.textBefore) && (prev = ((prev.textBefore || "") + prev.data).replace(/^\s+/g, "")))
  214.                         token.textBefore    =   prev;
  215.                     prev            =   null;
  216.                 }
  217.  
  218.                 else token.data +=  char;
  219.             }
  220.  
  221.  
  222.  
  223.             /** (Round brackets) */
  224.             else if(T_BRACKETS_ROUND === token.type){
  225.  
  226.                 /** Exact same procedure with square brackets. Note that we're duplicating our code block to avoid carrying a few extra variables around in memory. */
  227.                 if(")" === char){
  228.                     if(token.parent){
  229.                         prev    =   token;
  230.                         token   =   token.parent;
  231.                         token.data  +=  "(" + prev.data + ")";
  232.                         prev    =   null;
  233.                     }
  234.  
  235.                     /** Absolutely no idea how this could've happened. Something like "(whatever)" as a selector is meaningless. Whatever. */
  236.                     else{
  237.                         token.data  =   "(" + token.data + ")";
  238.                         delete token.type;
  239.                     }
  240.                 }
  241.                
  242.                 /** Watch out for quotes. */
  243.                 else if('"' === char)   token   =   {type:  T_QUOTES_DOUBLE,    parent: token,  data:   ""};
  244.                 else if("'" === char)   token   =   {type:  T_QUOTES_SINGLE,    parent: token,  data:   ""};
  245.  
  246.  
  247.                 /** Look out for nesting, too. */
  248.                 else if("[" === char){
  249.                     token   =   {
  250.                         type:   T_BRACKETS_SQUARE,
  251.                         parent: token,
  252.                         data:   ""
  253.                     };
  254.                 }
  255.  
  256.  
  257.                 /** Start of an injected comment */
  258.                 else if("/" === char && "*" === string[index]){
  259.                     ++index;
  260.                     prev            =   token;
  261.                     token           =   {
  262.                         type:   T_COMMENT,
  263.                         data:   "",
  264.                         parent: prev
  265.                     };
  266.                     if((prev.data || prev.textBefore) && (prev = ((prev.textBefore || "") + prev.data).replace(/^\s+/g, "")))
  267.                         token.textBefore    =   prev;
  268.                     prev            =   null;
  269.                 }
  270.  
  271.                 else token.data +=  char;
  272.             }
  273.  
  274.  
  275.  
  276.             /** "Double quotes" */
  277.             else if(T_QUOTES_DOUBLE === token.type){
  278.  
  279.                 /** End of quote. */
  280.                 if('"' === char && !("\\" === string[index-2] && false === /"(?:[^\\"]|\\.)*"/g.test('"'+token.data+'"'))){
  281.  
  282.                     if(token.parent){
  283.                         prev    =   token;
  284.                         token   =   token.parent;
  285.                         token.data  +=  '"' + prev.data + '"';
  286.                         prev    =   null;
  287.                     }
  288.  
  289.                     /** Not entirely sure how this happened... */
  290.                     else{
  291.                         token.data  =   '"' + token.data + '"';
  292.                         delete token.type;
  293.                     }
  294.                 }
  295.  
  296.                 else token.data += char;
  297.             }
  298.  
  299.  
  300.             /** 'Single quotes' */
  301.             else if(T_QUOTES_SINGLE === token.type){
  302.  
  303.                 /** End of quote. */
  304.                 if("'" === char && !("\\" === string[index-2] && false === /'(?:[^\\']|\\.)*'/g.test("'"+token.data+"'"))){
  305.  
  306.                     if(token.parent){
  307.                         prev    =   token;
  308.                         token   =   token.parent;
  309.                         token.data  +=  "'" + prev.data + "'";
  310.                         prev    =   null;
  311.                     }
  312.  
  313.                     /** Not entirely sure how this happened... */
  314.                     else{
  315.                         token.data  =   "'" + token.data + "'";
  316.                         delete token.type;
  317.                     }
  318.                 }
  319.  
  320.                 else token.data += char;
  321.             }
  322.  
  323.  
  324.  
  325.             /** No token type currently assigned. */
  326.             else{
  327.                 /** [Square brackets] */
  328.                 if("[" === char){
  329.                     token   =   {
  330.                         type:       T_BRACKETS_SQUARE,
  331.                         parent:     token,
  332.                         textBefore: token.data + "[",
  333.                         data:       ""
  334.                     };
  335.                 }
  336.                
  337.                 /** (Round brackets) */
  338.                 else if("(" === char){
  339.                     token   =   {
  340.                         type:   T_BRACKETS_ROUND,
  341.                         parent: token,
  342.                         textBefore: token.data + "(",
  343.                         data:   ""
  344.                     };
  345.                 }
  346.  
  347.                 /** "Double "quotes" */
  348.                 else if('"' === char){
  349.                     token   =   {
  350.                         type:   T_QUOTES_DOUBLE,
  351.                         parent: token,
  352.                         textBefore: token.data + '"',
  353.                         data:   ""
  354.                     };
  355.                 }
  356.  
  357.                 /** 'Single quotes' */
  358.                 else if("'" === char){
  359.                     token   =   {
  360.                         type:   T_QUOTES_SINGLE,
  361.                         parent: token,
  362.                         textBefore: token.data + "'",
  363.                         data:   ""
  364.                     };
  365.                 }
  366.  
  367.  
  368.                 /** Block */
  369.                 else if("{" === char){
  370.                    
  371.                     /** Since we need to assign our dumpTo variable to the newly created block anyway,
  372.                      * we may as well commendere the variable for creating it from our existing token. */
  373.                     dumpTo          =   [];
  374.                     dumpTo.type     =   T_BLOCK;
  375.                     dumpTo.data     =   "";
  376.                     dumpTo.name     =   token.data.replace(rTrim, "");
  377.  
  378.                     token.data      =   "";
  379.                     if(token instanceof Array){
  380.                         token.push(dumpTo);
  381.                         dumpTo.parent   =   token;
  382.                     }
  383.  
  384.                     else dumpTo.parent  =   token.parent;
  385.  
  386.                     /** Switch focus to our newly-created block token. */
  387.                     token   =   dumpTo;
  388.                 }
  389.  
  390.  
  391.                 /** Semicolon: end of property declaration? */
  392.                 else if(";" === char){
  393.  
  394.                     /** Break the previous token apart by the first colon, and assign it as a new property value. */
  395.                     if(prop = token.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))
  396.                         token.push({
  397.                             name:   prop[1],
  398.                             value:  prop[2]
  399.                         });
  400.  
  401.                     /** If no colon was found, then this is a meaningless declaration. Store it anyway as junk. */
  402.                     else (token instanceof Array ? token : dumpTo).push(token.data.replace(/^\s+/g, "") + ";");
  403.  
  404.                     token.data  =   "";
  405.                 }
  406.  
  407.  
  408.                 /** End of block */
  409.                 else if("}" === char){
  410.  
  411.                     /** Since the last property declaration in a block may omit a trailing semicolon, check for any collected data first. */
  412.                     if(token.data){
  413.                         if(prop = token.data.match(/\s*([^:]+):\s*([^\x00]+)\s*/m))
  414.                             token.push({
  415.                                 name:   prop[1],
  416.                                 value:  prop[2].replace(rTrim, "")
  417.                             });
  418.  
  419.                         /** Junk, then. Stow it anyway unless it's whitespace. */
  420.                         else{
  421.                             if(token.data = token.data.replace(rTrim, ""))
  422.                                 token.push(token.data);
  423.                         }
  424.                     }
  425.  
  426.  
  427.                     /** Store a reference to the current token. */
  428.                     prev    =   token;
  429.  
  430.                     /** The current token's nested inside another token. */
  431.                     if(token.parent){
  432.                         dumpTo  =
  433.                         token   =   prev.parent;
  434.                     }
  435.  
  436.                     /** Token's at top-level, so push it onto the end of our returned CSS array. */
  437.                     else{
  438.                         CSS.push(token);
  439.                         token   =   null;
  440.                         dumpTo  =   CSS;
  441.                     }
  442.  
  443.                     /** Free up some memory by losing some properties we no longer need. */
  444.                     delete prev.parent;
  445.                     delete prev.data;
  446.                     prev    =   null;
  447.                 }
  448.  
  449.  
  450.                 /** Nested comment */
  451.                 else if("/" === char && "*" === string[index]){
  452.                     ++index;
  453.                     prev            =   token;
  454.                     token           =   {
  455.                         type:   T_COMMENT,
  456.                         data:   "",
  457.                         parent: prev
  458.                     };
  459.  
  460.                     if(prev.data && (prev = prev.data.replace(/^\s+/g, "")))
  461.                         token.textBefore    =   prev;
  462.                     prev            =   null;
  463.                 }
  464.  
  465.                 /** Still no known token type. */
  466.                 else{
  467.                     token.data  +=  char;
  468.                 }
  469.             }
  470.         }
  471.  
  472.  
  473.         /** No token currently being carried yet, which means we're cruising at top-level. */
  474.         else{
  475.  
  476.             /** Whitespace? Ignore. */
  477.             if("\t" === char || "\n" === char || " " === char) continue;
  478.  
  479.             /** Comment */
  480.             if("/" === char && "*" === string[index]){
  481.                 ++index;
  482.                 token   =   {
  483.                     type:   T_COMMENT,
  484.                     data:   ""
  485.                 };
  486.             }
  487.  
  488.             /** Selector-less block? WTF?!
  489.              * This shouldn't ever happen unless a weird coder's decided to put { ... } in the middle of nowhere. */
  490.             else if("{" === char){
  491.                 dumpTo  =
  492.                 token   =   [];
  493.                 token.type      =   T_BLOCK;
  494.                 token.data      =
  495.                 token.name      =   "";
  496.             }
  497.  
  498.             /** Something else? */
  499.             else token  =   {data: char};
  500.         }
  501.     }
  502.  
  503.     return CSS;
  504. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement