CSS 2.1 在解析的錯誤處理方面,有一個乍看之有點道理的所謂的核心語法[1]:

  # 使用者代理會根據解析錯誤的處理規則,忽略樣式表中可以用下面文法但是不
  # 能用附錄 G 的文法解析的部份。

  # CSS 與未來的所有擴展不使用 "unused" 產生式,它的作用僅是幫助錯誤處
  # 理。(參見 4.2 解析錯誤的處理規則。)

  # 在有些情形中,使用者代理必須忽略不合法樣式表的一部分。本規範定義忽略
  # 的意義如下:使用者代理解析不合法的部份(為了找到不合法部份的起點和終
  # 點),但是當作這部份不存在來處理。

這些話可以用來解釋規範裡某些比較難懂的例子,例如:

  # 以下全部相等:
  #   p { color:green }
  #   ...
  #   p { color:green; color{;color:maroon} }

因為按照

  # block      : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
  # ...
  # declaration: property S* ':' S* value;
  # ...
  # value      : [ any | block | ATKEYWORD S* ]+;

 "{;color:maroon}" 可以被 tokenize 成 '{' ';' IDENT ':' IDENT '}',解析
成 '{' ';' any any any '}' → block → value,所以會被一起丟掉。

但是這個核心語法其實對規範*自己*的某些例子就不適用了,例如:

  p @here {color: red}

因為 ATKEYWORD(@here)不能解析 any,所以 "p @here" 也不能解析成一個
selector。也就是,這個核心語法不能解釋這個例子,事實上,這個核心語法不能
解釋「敘述格式異常」裡的所有例子。


這個乍看有用其實不完全的核心語法一直受到批評,在 www-style 可以看到從
2009 直到 2011 年 CSS 2.1 變成 REC 都有看到對這個地方不滿的回饋。

我最近發現這個核心語法其實可以擴展成一個普遍文法[2],所謂的普遍語法(我
也沒修過編譯器的課),是指任何輸入都可以解析的語法。我把這個文法寫到 CSS
2.1 翻譯的討論頁面[3]了,也留了一些說明和其他參考資料。


附上實作這個語法的 bison+flex 檔案。在 Mac OS X 上用

  flex scan3.l
  bison -v -d css.y
  clang css.tab.c lex.yy.c
  cat EXAMPLE.css | ./a.out

就可以跑了。舉個例子,上面的例子,用這個解析器就會給出:

[[
declaration:
color : green

declaration:
color {
 ; color : maroon}


ruleset:
p  {
  color : green;
  color {
 ; color : maroon}
}
]]

也就是 "color: maroon" 不是一個 declaration。其他例子 CSS 2.1 的測試資料
[4]裡還有很多奇奇怪怪的,對這方面想多了解的可以找來玩。

(我用來搞定 bison 花的時間比寫下這個文法還來的長,所以假如執行這個有問
題的歡迎私信問我。)


我在討論頁留了一些還沒有做得事的列表:

* 完整測試這個語法的瀏覽器兼容狀況(特別是 BAD_URI、CDO、CDC 的部份)
* 寫出 @media 的 block 內容語法(其實就是 at-rule、ruleset 前面都不能有
'}' 的 stylesheet。)也要測試一下有沒有在這個地方不省略 CDO、CDC 的瀏覽器。
* 用 EBNF 的 '-' 號讓這個文法更好讀。
* 了解這是 L-?
* 了解各 CSS 解析引擎用(例如:SASS)的合不合這個文法,還是拋錯。

有興趣的可以試試看再來討論。

[1] http://www.w3.org/html/ig/zh/wiki/CSS2#core-grammar
[2] http://en.wikipedia.org/wiki/Context-free_grammar#Universality
[3] http://www.w3.org/html/ig/zh/wiki/Talk:CSS2#core-grammar
[4]
http://test.csswg.org/suites/css2.1/nightly-unstable/xhtml1/chapter-4.xht#s4.1.6

%error-verbose

%{
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

extern int yylex(void);
extern int yylineno;

static void yyerror(char *s) {fprintf(stderr, "%d: %s\n", yylineno, s);}

static char* cat(char *s,...)
{
  va_list ap;
  char *t = NULL;
  t = (char*) malloc(1);
  *t = '\0';
  size_t len = 0;

  assert(s);
  va_start(ap, s);
  while (s) {
    len += strlen(s);
    t = (char*) realloc(t, len + 1);
    if (!t) {fprintf(stderr, "Out of memory."); exit(1);}
    strcat(t, s);
    s = va_arg(ap, char *);
  }
  va_end(ap);
  return t;
}

%}

%union {
  char *s;
}

/* All terminals return the matching string from the source */

%token <s> CDO CDC ATKEYWORD DELIM IDENT NUMBER PERCENTAGE DIMENSION
STRING URI HASH UNICODE_RANGE FUNCTION S DASHMATCH INCLUDES BAD_STRING
BAD_COMMENT BAD_URI EOF ';' ')' ']' '}' COMMENT

%type <s> statement ruleset at_rule block declaration_chain selector 
selector_start atkeyword_middleopt selector_middleopt atkeyword_middle 
selector_middle declaration any s_seqopt declaration_opt declaration_middle 
selector_opt bracket_middleopt pare_middleopt brace_middleopt bracket_middle 
pare_middle brace_middle no_close

%%

stylesheet
  : s_seqopt statement_ignore_seq selector EOF { printf("remaining items: \n");
                                 printf("%s\n", $3); YYACCEPT;}
  | s_seqopt statement_ignore_seq EOF {YYACCEPT;}
  ;
statement_ignore_seq
  : statement_ignore_seq statement
  | statement_ignore_seq ignore s_seqopt
  | /* empty */
  ;
ignore
  : CDO
  | CDC
  ;
statement
  : ruleset { printf("ruleset: \n"); printf("%s\n", $1); } 
  | at_rule { printf("at_rule: \n"); printf("%s\n", $1); } 
  ;
at_rule
  : ATKEYWORD s_seqopt atkeyword_middleopt block        {$$ = cat($1, $2, $3, 
$4,  NULL);}
| ATKEYWORD s_seqopt atkeyword_middleopt ';' s_seqopt {$$ = cat($1, $2, $3, 
strdup(";\n"), NULL);}
  | ATKEYWORD s_seqopt atkeyword_middleopt EOF    {$$ = cat($1, $2, $3, $4, 0);}
  ;
ruleset
  : selector_opt '{' s_seqopt declaration_opt declaration_chain '}' s_seqopt
    {$$ = cat($1, strdup("{\n  "), $4, $5, strdup("}\n"), NULL);}
  | selector_opt '{' s_seqopt declaration_opt declaration_chain EOF
    {$$ = cat($1, strdup("{\n  "), $4, $5, strdup("}\n"), NULL);}
  ;
declaration_chain
  : declaration_chain ';' s_seqopt declaration_opt
    {$$ = cat($1, strdup(";\n  "), $4, NULL);}
  | /* empty */
    {$$ = strdup("");}
  ;
selector_opt
  : selector
  | /* empty */ {$$ = strdup("");}
  ;
selector
  : selector_start selector_middleopt {$$ = cat($1, strdup(" "), $2, NULL);}
  ;
selector_start
  : any
  | ';' s_seqopt
  | ']' s_seqopt
  | ')' s_seqopt
  | '}' s_seqopt
  ;
atkeyword_middleopt
  : atkeyword_middleopt atkeyword_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | /* empty */ {$$ = strdup("");}
  ;
selector_middleopt
  : selector_middleopt selector_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | /* empty */ {$$ = strdup("");}
  ;
atkeyword_middle
  : any {$$ = cat($1, 0);}
  | ']' s_seqopt {$$ = cat($1, 0);}
  | ')' s_seqopt {$$ = cat($1, 0);}
  | '}' s_seqopt {$$ = cat($1, 0);}
  | ATKEYWORD s_seqopt {$$ = cat($1, 0);}
  | CDO s_seqopt {$$ = cat($1, 0);}
  | CDC s_seqopt {$$ = cat($1, 0);}
  ;   
selector_middle
  : selector_start
  | ATKEYWORD s_seqopt
  | CDO s_seqopt
  | CDC s_seqopt
  ;
declaration_opt
  : declaration { printf("declaration: \n"); printf("%s\n\n", $$);}
  | /* empty */ {$$ = strdup("");}
  ;
declaration
  : declaration declaration_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | declaration_middle
  ;
declaration_middle
  : no_close
  | ']' s_seqopt
  | ')' s_seqopt
  ; 
any
  : IDENT s_seqopt {$$ = cat($1, $2, 0);}
  | NUMBER s_seqopt {$$ = cat($1, $2, 0);}
  | PERCENTAGE s_seqopt {$$ = cat($1, $2, 0);}
  | DIMENSION s_seqopt {$$ = cat($1, $2, 0);}
  | STRING s_seqopt {$$ = cat($1, $2, 0);}
  | BAD_STRING EOF {$$ = cat($1, $1[0] == '"' ? strdup("\"") : strdup("'"), 0);}
  | DELIM s_seqopt {if (!strcmp($1, strdup("\\"))) $$ = strdup("\\\n"); else $$ 
= cat($1, $2, 0);}
  | URI s_seqopt {$$ = cat($1, $2, 0);}
  | HASH s_seqopt {$$ = cat($1, $2, 0);}
  | UNICODE_RANGE s_seqopt {$$ = cat($1, $2, 0);}
  | INCLUDES s_seqopt {$$ = cat($1, $2, 0);}
  | DASHMATCH s_seqopt {$$ = cat($1, $2, 0);}
  | ':' s_seqopt {$$ = cat(strdup(":"), $2, 0);}
  | BAD_STRING S s_seqopt {$$ = cat($1, strdup("\n"), 0);}
  | BAD_COMMENT EOF {$$ = strdup("");}
  | BAD_URI s_seqopt pare_middleopt ')' s_seqopt {$$ = cat($1, $3, strdup(")"),
                                                           $5, 0);}
  | BAD_URI s_seqopt pare_middleopt EOF {$$ = cat($1, $3, ")", 0);}
  | FUNCTION s_seqopt pare_middleopt ')' s_seqopt {$$ = cat($1, $3, strdup(")"),
                                                            $5, 0);}
  | FUNCTION s_seqopt pare_middleopt EOF {$$ = cat($1, $3, ")", 0);}
  | '(' s_seqopt pare_middleopt ')' s_seqopt {$$ = cat(strdup("("),
                                                       $3, strdup(")"), $5, 0);}
  | '(' s_seqopt pare_middleopt EOF {$$ = cat(strdup("("), $3, strdup(")"), 0);}
  | '[' s_seqopt bracket_middleopt ']' s_seqopt {$$ = cat(strdup("["), $3, 
                                                        strdup("]"), $5, 0);}
  | '[' s_seqopt bracket_middleopt EOF {$$ = cat(strdup("["), $3, strdup("]"), 
0);}
  ;
block
  : '{' s_seqopt brace_middleopt '}' s_seqopt {$$ = cat(strdup("{\n"), 
                                                      $3, strdup("}\n"), 0);}
  | '{' s_seqopt brace_middleopt EOF          {$$ = cat(strdup("{\n"), $3, 
                                                      strdup("}\n"), 0);}
  ;
bracket_middleopt
  : bracket_middleopt bracket_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | /* empty */ {$$ = strdup("");}
  ;
pare_middleopt
  : pare_middleopt pare_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | /* empty */ {$$ = strdup("");}
  ;
brace_middleopt
  : brace_middleopt brace_middle {$$ = cat($1, strdup(" "), $2, 0);}
  | /* empty */ {$$ = strdup("");}
  ;
bracket_middle
  : no_close
  | ')' s_seqopt
  | ';' s_seqopt
  | '}' s_seqopt
  ;
pare_middle
  : no_close
  | ']' s_seqopt
  | ';' s_seqopt
  | '}' s_seqopt
  ;
brace_middle
  : no_close
  | ']' s_seqopt
  | ';' s_seqopt
  | ')' s_seqopt
  ;
no_close
  : any
  | block
  | ATKEYWORD s_seqopt
  | CDO s_seqopt
  | CDC s_seqopt
  ;
s_seqopt
  : s_seqopt S {$$ = strdup(" ");}
  | s_seqopt COMMENT {}
  | /* empty */ {$$ = strdup("");}
  ;

%%

int
main(int argc, char **argv)
{
    yyparse();
}

%option 8bit caseless nodefault noyywrap noinput yylineno

%{
#undef EOF
#include "css.tab.h"
#include <string.h>
#define return_token(V) yylval.s = strdup(yytext); return V
%}

/* Currently only for ASCII-compatible encodings, such as ISO-8859-1 or UTF-8 */

ident           [-]?{nmstart}{nmchar}*
name            {nmchar}+
nmstart         [_a-z]|{nonascii}|{escape}
nonascii        [^\0-\240]
unicode         \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
escape          {unicode}|\\[^\n\r\f0-9a-f]
nmchar          [_a-z0-9-]|{nonascii}|{escape}
num             [0-9]+|[0-9]*\.[0-9]+

string          {string1}|{string2}
string1         \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
string2         \'([^\n\r\f\\']|\\{nl}|{escape})*\'
badstring       {badstring1}|{badstring2}
badstring1      \"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
badstring2      \'([^\n\r\f\\']|\\{nl}|{escape})*\\?

nl              \n|\r\n|\r|\f
w               [ \t\r\n\f]*

uri             {uri1}|{uri2}
uri1            url\({w}{string}{w}\)
uri2            url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}\)
baduri          {baduri1}|{baduri2}|{baduri3}
baduri1         url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
baduri2         url\({w}{string}{w}
baduri3         url\({w}{badstring}

comment         \/\*[^*]*\*+([^/*][^*]*\*+)*\/
badcomment      {badcomment1}|{badcomment2}
badcomment1     \/\*[^*]*\*+([^/*][^*]*\*+)*
badcomment2     \/\*[^*]*(\*+[^/*][^*]*)*

/* At EOF, we return an infinite number of EOD tokens. */

%x eof

%%

<<EOF>>                                 { return_token(EOF); yyterminate(); }
{uri}                                   return_token(URI);
{baduri}                                return_token(BAD_URI);
u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})?      return_token(UNICODE_RANGE);
{ident}                                 return_token(IDENT);
@{ident}                                return_token(ATKEYWORD);
#{name}                                 return_token(HASH);
{num}                                   return_token(NUMBER);
{num}%                                  return_token(PERCENTAGE);
{num}{ident}                            return_token(DIMENSION);
"<!--"                                  return_token(CDO);
"-->"                                   return_token(CDC);
":"                                     return_token(':');
";"                                     return_token(';');
"{"                                     return_token('{');
"}"                                     return_token('}');
"("                                     return_token('(');
")"                                     return_token(')');
"["                                     return_token('[');
"]"                                     return_token(']');
[ \t\r\n\f]+                            return_token(S);
{ident}\(                               return_token(FUNCTION);
"~="                                    return_token(INCLUDES);
"|="                                    return_token(DASHMATCH);

{comment}                               return_token(COMMENT);
{badcomment}                            return_token(BAD_COMMENT);

{string}                                return_token(STRING);
{badstring}                             return_token(BAD_STRING);

.                                       return_token(DELIM);

{ident}/\\                              return_token(IDENT);
#{name}/\\                              return_token(HASH);
@{ident}/\\                             return_token(ATKEYWORD);
#/\\                                    return_token(DELIM);
@/\\                                    return_token(DELIM);
@/-                                     return_token(DELIM);
@/-\\                                   return_token(DELIM);
-/\\                                    return_token(DELIM);
-/-                                     return_token(DELIM);
\</!                                    return_token(DELIM);
\</!-                                   return_token(DELIM);
{num}{ident}/\\                         return_token(DIMENSION);
{num}/\\                                return_token(NUMBER);
{num}/-                                 return_token(NUMBER);
{num}/-\\                               return_token(NUMBER);
[0-9]+/\.                               return_token(NUMBER);
u/\+                                    return_token(IDENT);
u\+[0-9a-f?]{1,6}/-                     return_token(UNICODE_RANGE);

回复