From f9fcd4e8c488e649437b7dfcaf72ed76f8844d7c Mon Sep 17 00:00:00 2001 From: Ian Prest Date: Thu, 30 Jul 2015 21:28:17 -0400 Subject: [PATCH] Wrote my own CSS parser + unit-tests --- Makefile | 13 +- bower.json | 3 +- cssparser.y | 189 +++++++++++++++++++ kb.sublime-project | 2 +- spec/kb-css-spec.js | 118 ++++++++++++ tests/kb-serial.js => spec/kb-serial-spec.js | 2 +- spec/support/jasmine.json | 9 + 7 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 cssparser.y create mode 100644 spec/kb-css-spec.js rename tests/kb-serial.js => spec/kb-serial-spec.js (99%) create mode 100644 spec/support/jasmine.json diff --git a/Makefile b/Makefile index 799f499..86ed67f 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,15 @@ all: js_files css_files bower_copy .PHONY: js_files css_files bower_copy # Rules to minify our .js files -js_files: js/jsonl.min.js +js_files: js/jsonl.min.js js/cssparser.js js/%.min.js: js/%.js $(call mkdir,$(dir $@)) uglifyjs "$^" > "$@" js/%.js: %.grammar.js $(call mkdir,$(dir $@)) node "$^" > "$@" +js/%.js: %.y + jison "$^" -o "$@" .PRECIOUS: js/%.js @@ -74,7 +76,6 @@ $(call BOWER,bower_components/marked/marked.min.js) $(call BOWER,bower_components/FileSaver/FileSaver.min.js) $(call BOWER,bower_components/doT/doT.min.js) $(call BOWER,bower_components/URLON/src/urlon.js) -$(call BOWER,bower_components/cssparser/lib/cssparser.js) # Rules to generate a webfont from our source .svg files @@ -106,7 +107,13 @@ CUSTOM_FONT = $(eval $(call _CUSTOM_FONT,$(1).ttf,$(2)))$(eval $(call _CUSTOM_FO $(call CUSTOM_FONT,kbd-custom,$(kbd-custom-glyphs)) -test: +test: e2e-test unit-test + protractor tests/conf.js + +unit-test: + jasmine + +e2e-test: protractor tests/conf.js install: diff --git a/bower.json b/bower.json index 12bae0a..466b8bf 100644 --- a/bower.json +++ b/bower.json @@ -41,7 +41,6 @@ "ng-file-upload": "5.0.9", "angular-ui-bootstrap": "0.12.0", "fontawesome": "4.3.0", - "hint.css": "1.3.5", - "cssparser": "a4adfd2f5f62bb9c19bcfead974ac0f253a0fb82" + "hint.css": "1.3.5" } } diff --git a/cssparser.y b/cssparser.y new file mode 100644 index 0000000..f08432e --- /dev/null +++ b/cssparser.y @@ -0,0 +1,189 @@ +/* Parser for CSS3, as defined by http://www.w3.org/TR/css-syntax-3 */ + +%lex +/* Lexer Macros */ +comment \/\*([^\*](\*[^\/])?)*\*\/ +newline \r\n|\n|\r|\f +whitespace [ \t\n\r\f] +hex [0-9a-fA-F] +escape \\([^0-9a-fA-F\n\r\f]|({hex}{1,6}{whitespace}?)) +ws {whitespace}* + +nonascii [\200-\377] +token_start ({escape}|[a-zA-Z_]|{nonascii}) +token_char ({escape}|[a-zA-Z_0-9-]|{nonascii}) +identifier \-?{token_start}{token_char}* + +string \"({escape}|\\{newline}|[^"\\\r\n\f])*\"|\'({escape}|\\{newline}|[^'\\\r\n\f])*\' +urlu ({escape}|[^'"\(\)\\ \t\n\r\f\000-\010\016-\037\177])+ +number [+-]?(\d+\.\d+|\.\d+|\d+)([eE][+-]?\d+)? +range [uU]'+'({hex}{1,6}('-'{hex}{1,6})?|'?'{6}|{hex}'?'{5}|{hex}{2}'?'{4}|{hex}{3}'?'{3}|{hex}{4}'?'{2}|{hex}{5}'?'{1}) + +%% + +/* Lexical Tokens */ +{comment} return 'WHITESPACE'; +{whitespace}+ return 'WHITESPACE'; +{identifier}'(' return 'FUNCTION'; +{identifier} return 'IDENTIFIER'; +'#'{token_char}+ return 'HASH'; +'@'{identifier} return 'AT_KEYWORD'; +'!'{ws}'important'{ws} return 'IMPORTANT'; +{string} return 'STRING'; +'url('{w}({string}|{urlu})?{w}')' return 'URL'; +{number}{identifier} return 'DIMENSION'; +{number}'%' return 'PERCENTAGE'; +{number} return 'NUMBER'; +{range} return 'UNICODE_RANGE'; +[~|^$\*]?'=' return 'MATCH_TOKEN'; +'||' return 'COLUMN_TOKEN'; +'' return 'CDC'; +<> return 'EOF'; +'\'' return 'SQUOTE'; +. return yytext; + +/lex + +/* language grammar */ +%start stylesheet +%ebnf +%% + +stylesheet + : WS EOF { return $$ = {}; } + | stylesheet_content WS EOF { return $$ = ($stylesheet_content && $stylesheet_content.length) ? { rules:$stylesheet_content } : {}; } + ; + +stylesheet_content + : stylesheet_item { $$ = []; if($stylesheet_item !== null) $$.push($stylesheet_item); } + | stylesheet_content stylesheet_item { $$ = $stylesheet_content; if($stylesheet_item !== null) $$.push($stylesheet_item); } + ; + +stylesheet_item + : WS CDO { $$ = null; } + | WS CDC { $$ = null; } + | qualified_rule { $$ = $qualified_rule; } + | at_rule { $$ = $at_rule; } + ; + +at_rule + : WS AT_KEYWORD ';' { $$ = { name: $AT_KEYWORD }} + | WS AT_KEYWORD at_rule_selector ';' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim() }; if(!$$.selector) delete $$.selector; } + | WS AT_KEYWORD at_rule_selector '{' '}' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim() }; if(!$$.selector) delete $$.selector; } + | WS AT_KEYWORD at_rule_selector '{' at_rule_content '}' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim(), content: $at_rule_content.trim() }; if(!$$.selector) delete $$.selector; if(!$$.content) delete $$.content; } + ; + +at_rule_selector + : any_token { $$ = $any_token; } + | at_rule_selector any_token { $$ = $at_rule_selector + $any_token; } + ; + +at_rule_content + : any_token { $$ = $any_token; } + | at_rule_content ';' { $$ = $at_rule_content + ';'; } + | at_rule_content any_token { $$ = $at_rule_content + $any_token; } + ; + +qualified_rule + : qualified_rule_prelude '{' WS '}' { $$ = { selector: $qualified_rule_prelude } } + | qualified_rule_prelude '{' WS declaration_list '}' { $$ = { selector: $qualified_rule_prelude, decls: $declaration_list } } + | qualified_rule_prelude '{' WS declaration_list ';' WS '}' { $$ = { selector: $qualified_rule_prelude, decls: $declaration_list } } + ; + +declaration_list + : declaration { $$ = [$declaration]; } + | declaration_list ';' WS declaration { $$ = $declaration_list; $$.push($declaration); } + ; + +declaration + : IDENTIFIER WS ':' declaration_value { $$ = [$IDENTIFIER,$declaration_value.trim()]; } + ; + +// Anything other than a semi-colon or a closing brace +declaration_value + : any_token { $$ = $any_token; } + | declaration_value any_token { $$ = $declaration_value + $any_token; } + ; + +// Does not include semi-colon, or open/close braces +any_token + : (WHITESPACE | FUNCTION | IDENTIFIER | HASH | AT_KEYWORD | + IMPORTANT | STRING | URL | DIMENSION | PERCENTAGE | NUMBER | + UNICODE_RANGE | MATCH_TOKEN | COLUMN_TOKEN | CDO | CDC | EOF | SQUOTE | + '!'|'"'|'#'|'$'|'%'|'&'|'('|')'|'*'|'+'|','|'-'|'.'|'/'| + ':'|'<'|'='|'>'|'?'|'@'|'['|'\'|']'|'^'|'_'|'`'|'|'|'~') { $$ = $1; } + ; + +qualified_rule_prelude + : WS selector WS { $$ = [$selector]; } + | qualified_rule_prelude ',' WS selector WS { $$ = $qualified_rule_prelude; $$.push($selector); } + ; + +selector + : single_selector { $$ = $single_selector; } + | selector WHITESPACE single_selector { $$ = $selector + ' ' + $single_selector; } + | selector WS '>' WS single_selector { $$ = $selector + '>' + $single_selector; } + | selector WS '+' WS single_selector { $$ = $selector + '+' + $single_selector; } + | selector WS '~' WS single_selector { $$ = $selector + '~' + $single_selector; } + | ':' IDENTIFIER { $$ = ':' + $IDENTIFIER; } + | ':' ':' IDENTIFIER { $$ = '::' + $IDENTIFIER; } + | selector ':' IDENTIFIER { $$ = $selector + ':' + $IDENTIFIER; } + | selector ':' ':' IDENTIFIER { $$ = $selector + '::' + $IDENTIFIER; } + | ':' FUNCTION WS selector_function_params { $$ = ':' + $FUNCTION + $selector_function_params + ')'; } + | selector ':' FUNCTION WS selector_function_params { $$ = $selector + ':' + $FUNCTION + $selector_function_params + ')'; } + ; + +selector_function_params + : IDENTIFIER WS ')' { $$ = $IDENTIFIER; } + | NUMBER WS ')' { $$ = $NUMBER; } + | DIMENSION WS ')' { $$ = $DIMENSION; } // 2n + | IDENTIFIER WS NUMBER WS ')' { $$ = $IDENTIFIER + $NUMBER; } // n+1 + | IDENTIFIER WS '+' WS NUMBER WS ')' { $$ = $IDENTIFIER + '+' + $NUMBER; } // n+1 + | IDENTIFIER WS '-' WS NUMBER WS ')' { $$ = $IDENTIFIER + '-' + $NUMBER; } // n+1 + | DIMENSION WS NUMBER WS ')' { $$ = $DIMENSION + $NUMBER; } // 2n+1 + | DIMENSION WS '+' WS NUMBER WS ')' { $$ = $DIMENSION + '+' + $NUMBER; } // 2n+1 + | DIMENSION WS '-' WS NUMBER WS ')' { $$ = $DIMENSION + '-' + $NUMBER; } // 2n+1 + ; + +single_selector + : '*' { $$ = $1; } + | '.' IDENTIFIER { $$ = '.' + $IDENTIFIER; } + | HASH { $$ = $HASH; } + | IDENTIFIER { $$ = $IDENTIFIER; } + | single_selector '.' IDENTIFIER { $$ = $single_selector + '.' + $IDENTIFIER; } + | single_selector HASH { $$ = $single_selector + $HASH; } + | single_selector '[' WS attribute_selector ']' { $$ = $single_selector + '[' + $attribute_selector + ']'; } + ; + +attribute_selector + : attribute_name WS { $$ = $attribute_name; } + | attribute_name WS MATCH_TOKEN WS attribute_value { $$ = $attribute_name + $MATCH_TOKEN + $attribute_value; } + ; + +attribute_name + : IDENTIFIER { $$ = $IDENTIFIER; } + ; + +attribute_value + : unquoted_attribute_value { $$ = $unquoted_attribute_value; } + | STRING { $$ = $STRING; } + ; + +unquoted_attribute_value + : unquoted_attribute_value_char { $$ = $unquoted_attribute_value_char; } + | IDENTIFIER { $$ = $IDENTIFIER; } + | unquoted_attribute_value unquoted_attribute_value_char { $$ = $unquoted_attribute_value + $unquoted_attribute_value_char; } + | unquoted_attribute_value IDENTIFIER { $$ = $unquoted_attribute_value + $IDENTIFIER; } + ; + +unquoted_attribute_value_char + : ('.'|':'|'-'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|NUMBER) { $$ = $1; } // technically shouldn't allow full number syntax + ; + +WS + : /*empty*/ { $$ = ''; } + | WHITESPACE { $$ = ''; } + | ws WHITESPACE { $$ = ''; } + ; + diff --git a/kb.sublime-project b/kb.sublime-project index a33302c..674cdd1 100644 --- a/kb.sublime-project +++ b/kb.sublime-project @@ -13,7 +13,7 @@ "build_system": "Automatic", //SublimeOnSaveBuild package (optional) - "filename_filter": "\\.(css|js|sass|less|scss)$", + "filename_filter": "\\.(css|js|sass|less|scss|y)$", "build_on_save": 1 }, "build_systems": diff --git a/spec/kb-css-spec.js b/spec/kb-css-spec.js new file mode 100644 index 0000000..84fe889 --- /dev/null +++ b/spec/kb-css-spec.js @@ -0,0 +1,118 @@ + +css = require('../js/cssparser.js'); + +describe('css parser', function() { + + it('should parse an empty file', function() { + expect(css.parse('')).toEqual({}); + }); + + it('should ignore HTML comments', function() { + expect(css.parse('')).toEqual({}); + expect(css.parse('')).toEqual({}); + expect(css.parse('-->