mod_css_cond
以下の仕様は今後変更される可能性があります。
ビルド方法
$ /usr/sbin/apxs -c mod_trimxml.c $ sudo /usr/sbin/apxs -i -A mod_trimxml.la
設定
Filter として CSSCOND を設定します。
.htaccess の設定例
# LoadModule css_cond_module modules/mod_css_cond.so <IfModule mod_css_cond.c> AddOutputFilterByType CSSCOND text/css </IfModule>
文法
- オープンタグ
/*#文
/#文
- クローズタグ
*/
/
クローズタグの先頭には,見映えをあわせるため「#
」をプレフィックスさせることもできます。
と,以上の表記だけだとよくわからないでしょうから実例をあげます。
/*#if IE */
/*#if IE /
/#if IE */
/#if IE #*/
これらはすべて有効な文となります。ただし,(このフィルタが適用されない状況を考えると)たとえば 1 番目の例はコメントとしては完結した形式ですし,2 番目の例はコメントが開始され引き続き続きがあることになります。3, 4 番目の例は開いているコメントを閉じることになります。このことに注意してオープンタグとクローズタグを組み合わせるのがよいでしょう。
たとえば
blockquote { /*#if ! IE */ opacity: 0.8; /*#else / filter: alpha(opacity=80); /#endif*/ } p { /*#if IE/ filter: alpha(opacity=80); /#else */ opacity: 0.8; /*#endif*/ } li { color: /*#if IE/ #00f /#elsif FX*/ #0f0 /*#else/ #f00 /#endif*/; }
のようになります。シンタックスハイライト機能付きのエディタで編集するのがいいかと。
構文
条件分岐
#if 評価式
- 〜
#elsif 評価式
- 〜
#else
- 〜
#endif
見ての通りです。もちろん,#elsif
や #else
節は省略できます。#if
節は必ず最終的には #endif
節で閉じなければなりません。
コメント
*
どのような条件でも CSS として出力されないコメントを記述できます。内部管理用にコメントをつけているけれど,プロダクション環境では出力したくない場合にお使いください。
- 例
/*#* blah, blah, blah *#*/
むろん終端部の *#*
という部分は省略できますが,見映えをあわせる意味でつけてみました。
評価式
説明を書くのが面倒なので例示のみします。
User-Agent
により定義される変数
バージョン値はすべて浮動小数点に変換されます。すなわち,マイナーバージョンまでしか取得できないことに注意してください(例:Firefox 2.0.0.9 ⇒ FX = 2.0
)。
IE
- Internet Explorer のバージョンです。
MACIE
- Macintosh 版 IE の場合に
IE
と同じ値が設定されます。 OPERA
- Opera のバージョンです。
SAFARI
- Safari のバージョンです。いわゆる WebKit のバージョンではなく,Safari のバージョン番号を推定・概算します。
FX
- Firefox のバージョンです。
NN
- Netscape のバージョンです。上記のいずれにも当てはまらない
User-Agent
の場合に(後述する)MOZ
と同じ値が設定されます。ただし Firefox の場合にも5.0
等が設定されます。 MOZ
Mozilla/x.x
のx.x
の部分です。GECKO
- Gecko のバージョン(例:20071105)です。
WIN
- Windows の場合に 1 が設定されます。
MAC
- Macintosh の場合に 1 が設定されます。
LINUX
- Linux の場合に 1 が設定されます。
制限
#if
〜#endif
ブロックのネストはできません。- 複雑な条件式を与えることはできません。
- トークン区切りとして white space が必要になります。
- つまり,
/*#if IE>5*/
のように詰めて書くことはできません。/*#if IE > 5*/
のように空白で分かち書きをしてください。
- つまり,
- 文法上間違いがあった場合には,fallback or そのまま出力したりしません。Internal Server Error となってしまいます。
いいわけ
IE の条件付きコメントのように [if 〜]
のような形式にしなかったのは,[
, ]
のコードが Shift_JIS の 2 バイト目と重なるのでこれを回避したためです。
でも C プリプロセッサにあわせて #
としたのはまずかったかも。CSS で頻出するんで見づらい。$
にしたほうがいいかなぁ。
仕様策定上参考にしたサイト
ソースコード
#include "apr_general.h" #include "apr_lib.h" #include "apr_strings.h" #if APR_HAVE_STDLIB_H #include <stdlib.h> #endif #include "ap_config.h" #include "httpd.h" #include "http_config.h" #include "http_protocol.h" #include "http_log.h" #include "http_main.h" #include "http_request.h" #include "mod_core.h" #define APR_BUCKET_MOVE_TO_BRIGADE(e, b) \ do { \ APR_BUCKET_REMOVE((e)); \ APR_BRIGADE_INSERT_TAIL((b), (e)); \ } while (0) module AP_MODULE_DECLARE_DATA css_cond_module; static apr_table_t *create_browser_table(const char *agent, apr_pool_t *pool) { static const char *agents[][2] = { /* order is important */ { "Opera ", "OPERA" }, { "Opera/", "OPERA" }, { "MSIE ", "IE" }, { "Gecko/", "GECKO" }, { "Netscape6/", "NN" }, { "Netscape/", "NN" }, { "Firefox/", "FX" }, { NULL, NULL }, }; apr_table_t *browser; char *p, *q; double d; int i; int mac = 0; const char *moz = NULL; browser = apr_table_make(pool, 2); if (! agent || *agent == '\0') return browser; /* OS */ if (strstr(agent, "Windows")) { apr_table_setn(browser, "WIN", "1"); } if (strstr(agent, "Macintosh") || strstr(agent, "Mac_")) { apr_table_setn(browser, "MAC", "1"); mac = 1; } if (strstr(agent, "Linux")) { apr_table_setn(browser, "LINUX", "1"); } /* Mozilla is common for many cases */ p = strstr(agent, "Mozilla/"); if (p) { p += 8; d = strtod(p, &q); if (q > p) { moz = apr_pstrmemdup(pool, p, q - p); apr_table_setn(browser, "MOZ", moz); } } /* Safari is special */ p = strstr(agent, "Safari/"); if (p) { p += 7; d = strtod(p, &q); if (q > p) { p = strstr(agent, "Version/"); /* posterior to 3.0 */ if (p) { double dd; /* avoid warning of strtod() */ p += 8; dd = strtod(p, &q); if (q > p) { apr_table_setn(browser, "SAFARI", apr_pstrmemdup(pool, p, q - p)); return browser; } } if (0) ; else if (d >= 400.0) { d = 2.0; } else if (d >= 300.0) { d = 1.3; } else { d = ((int) (d / 10.0)) / 10.0; } apr_table_setn(browser, "SAFARI", apr_psprintf(pool, "%.1f", d)); return browser; } } for (i = 0; agents[i][0]; i ++) { p = strstr(agent, agents[i][0]); if (p) { p += strlen(agents[i][0]); d = strtod(p, &q); if (q > p) { const char *s = apr_pstrmemdup(pool, p, q - p); apr_table_setn(browser, agents[i][1], s); if (i == 2 && mac) /* MacIE might be special */ apr_table_setn(browser, "MACIE", s); /* set NN for Firefox; GECKO is special */ if (i != 6 && i != 3) return browser; } } } /* fallback: NN from MOZ */ if (moz) apr_table_setn(browser, "NN", moz); return browser; } typedef enum { CC_VARIANT_NULL, CC_VARIANT_BOOL, CC_VARIANT_INT, CC_VARIANT_DOUBLE } cc_variant_type; typedef struct { cc_variant_type type; union { int i; double d; } value; } cc_variant_t; static int cc_variant_get_bool(cc_variant_t *v) { switch (v->type) { case CC_VARIANT_BOOL: case CC_VARIANT_INT: return v->value.i; case CC_VARIANT_DOUBLE: return v->value.d != 0.0; default: return 0; } } static double cc_variant_get_double(cc_variant_t *v) { switch (v->type) { case CC_VARIANT_BOOL: case CC_VARIANT_INT: return v->value.i; case CC_VARIANT_DOUBLE: return v->value.d; default: return 0.0; } } static const char *cc_variant_get_string(cc_variant_t *v, apr_pool_t *pool) { switch (v ? v->type : CC_VARIANT_NULL) { case CC_VARIANT_BOOL: return v->value.i ? "true" : "false"; case CC_VARIANT_INT: return apr_psprintf(pool, "%d", v->value.i); case CC_VARIANT_DOUBLE: { char *p, *q; /* '%g' is not reliable */ p = apr_psprintf(pool, "%.8f", v->value.d); q = p + strlen(p) - 1; while (q > p) { if (*(q - 1) == '.') break; if (*q != '0') break; *q-- = '\0'; } return p; } default: return "(null)"; } } enum css_cond_stage_t { STAGE_WAIT_OPEN, STAGE_READ_OPEN1, STAGE_READ_OPEN2, STAGE_READ_OPEN3, STAGE_COLLECT_STATEMENT, STAGE_START = STAGE_WAIT_OPEN, STAGE_DONE = -1 }; typedef struct { int strict_open_comment; int stage; int open_comment; int may_be_open_comment; int current_cond; int cond_is_archived; /* for #elsif */ apr_table_t *browser; apr_bucket_brigade *bb_out; apr_bucket_brigade *bb_work; } css_cond_ctx_t; static apr_status_t eval_val(const char *str, cc_variant_t *v) { char *p; if ((! str) || (*str == '\0')) { v->type = CC_VARIANT_NULL; return APR_SUCCESS; } if (! strcasecmp(str, "true")) { v->value.i = 1; v->type = CC_VARIANT_BOOL; return APR_SUCCESS; } if (! strcasecmp(str, "false")) { v->value.i = 0; v->type = CC_VARIANT_BOOL; return APR_SUCCESS; } v->value.i = (int) strtol(str, &p, 10); if (*p == '\0') { v->type = CC_VARIANT_INT; return APR_SUCCESS; } v->value.d = strtod(str, &p); if (*p == '\0') { v->type = CC_VARIANT_DOUBLE; return APR_SUCCESS; } return APR_EGENERAL; } static apr_status_t eval_token(request_rec *r, css_cond_ctx_t *ctx, const char *str, cc_variant_t *v) { apr_status_t rv; const char *p; /* raw value */ rv = eval_val(str, v); if (rv == APR_SUCCESS) return APR_SUCCESS; /* search environment */ p = apr_table_get(r->subprocess_env, str); /* search browser */ if (! p) { p = apr_table_get(ctx->browser, str); } if (p) { rv = eval_val(p, v); if (rv == APR_SUCCESS) return APR_SUCCESS; } v->type = CC_VARIANT_BOOL; v->value.i = 0; return APR_SUCCESS; } static apr_status_t eval_exp(request_rec *r, css_cond_ctx_t *ctx, int argc, char **argv, cc_variant_t *result) { apr_status_t rv; if (argc < 1) return APR_EGENERAL; if (argc == 1) { cc_variant_t v; rv = eval_token(r, ctx, argv[0], &v); if (rv != APR_SUCCESS) return rv; memcpy(result, &v, sizeof(v)); return APR_SUCCESS; } if (argc >= 2) { if (! strcmp(argv[0], "!") || ! strcmp(argv[0], "not")) { cc_variant_t v; rv = eval_exp(r, ctx, argc - 1, argv + 1, &v); if (rv != APR_SUCCESS) return rv; result->type = CC_VARIANT_BOOL; result->value.i = ! cc_variant_get_bool(&v); return APR_SUCCESS; } } if (argc == 3) { if (0 || ! strcmp(argv[1], "==") || ! strcmp(argv[1], "!=") || ! strcmp(argv[1], "<") || ! strcmp(argv[1], "<=") || ! strcmp(argv[1], ">") || ! strcmp(argv[1], ">=") ) { cc_variant_t v1, v2; double d1, d2; rv = eval_exp(r, ctx, 1, argv + 0, &v1); if (rv != APR_SUCCESS) return rv; rv = eval_exp(r, ctx, 1, argv + 2, &v2); if (rv != APR_SUCCESS) return rv; d1 = cc_variant_get_double(&v1); d2 = cc_variant_get_double(&v2); result->type = CC_VARIANT_BOOL; if (0) ; else if (! strcmp(argv[1], "==")) result->value.i = d1 == d2; else if (! strcmp(argv[1], "!=")) result->value.i = d1 != d2; else if (! strcmp(argv[1], "<")) result->value.i = d1 < d2; else if (! strcmp(argv[1], "<=")) result->value.i = d1 <= d2; else if (! strcmp(argv[1], ">")) result->value.i = d1 > d2; else if (! strcmp(argv[1], ">=")) result->value.i = d1 >= d2; return APR_SUCCESS; } if (0 #if 0 || ! strcmp(argv[0], "eq") || ! strcmp(argv[0], "ne") #endif || ! strcmp(argv[0], "lt") || ! strcmp(argv[0], "lte") || ! strcmp(argv[0], "gt") || ! strcmp(argv[0], "gte") ) { cc_variant_t v1, v2; double d1, d2; rv = eval_exp(r, ctx, 1, argv + 1, &v1); if (rv != APR_SUCCESS) return rv; rv = eval_exp(r, ctx, 1, argv + 2, &v2); if (rv != APR_SUCCESS) return rv; d1 = cc_variant_get_double(&v1); d2 = cc_variant_get_double(&v2); result->type = CC_VARIANT_BOOL; if (0) ; #if 0 else if (! strcmp(argv[1], "eq")) result->value.i = d1 == d2; else if (! strcmp(argv[1], "ne")) result->value.i = d1 != d2; #endif else if (! strcmp(argv[1], "lt")) result->value.i = d1 < d2; else if (! strcmp(argv[1], "lte")) result->value.i = d1 <= d2; else if (! strcmp(argv[1], "gt")) result->value.i = d1 > d2; else if (! strcmp(argv[1], "gte")) result->value.i = d1 >= d2; return APR_SUCCESS; } } if (argc == 2) { cc_variant_t v1, v2; double d1, d2; rv = eval_exp(r, ctx, 1, argv + 0, &v1); if (rv != APR_SUCCESS) return rv; rv = eval_exp(r, ctx, 1, argv + 1, &v2); if (rv != APR_SUCCESS) return rv; d1 = cc_variant_get_double(&v1); d2 = cc_variant_get_double(&v2); /* equality judgement is not simple */ result->type = CC_VARIANT_BOOL; if (v2.type == CC_VARIANT_INT) { result->value.i = (int) d1 == (int) d2; } else { result->value.i = ((int) d1 == (int) d2) && (d1 - d2 < 0.1); } return APR_SUCCESS; } return APR_EGENERAL; } static apr_status_t eval_expression(request_rec *r, css_cond_ctx_t *ctx, apr_pool_t *pool, char *str, cc_variant_t *v) { apr_status_t rv; char **argv, **avp; int argc; if (! pool) pool = r->pool; rv = apr_tokenize_to_argv(str, &argv, pool); if (rv != APR_SUCCESS) return rv; for (argc = 0, avp = argv; *avp; avp ++) ++ argc; rv = eval_exp(r, ctx, argc, argv, v); if (rv != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "invalid expression: '%s'", str); return rv; } return APR_SUCCESS; } #define CHECK_DIRECTIVE(b, d, l) \ (! strncasecmp((b), (d), (l)) && ((b)[(l)] == 0 || apr_isspace((b)[(l)]))) static apr_status_t eval_statement(request_rec *r, css_cond_ctx_t *ctx) { apr_status_t rv; apr_pool_t *pool; rv = apr_pool_create(&pool, r->pool); if (rv != APR_SUCCESS) return rv; do { apr_off_t total; apr_size_t len; char *buf, *p; cc_variant_t vcond; apr_brigade_length(ctx->bb_work, 1, &total); buf = apr_palloc(pool, total + 1); len = total; rv = apr_brigade_flatten(ctx->bb_work, buf, &len); if (rv != APR_SUCCESS) break; buf[len] = '\0'; p = buf + len - 1; if (*p == '*') { /* closed comment */ *p-- = '\0'; ctx->open_comment = 0; } if (*p == '#') { /* you can place trailing '#' for readability */ *p-- = '\0'; } /* Now, check directives */ if (*buf == '*') { ; /* comment to be snipped; do nothing */ } else if (CHECK_DIRECTIVE(buf, "echo", 4)) { rv = eval_expression(r, ctx, pool, buf + 4, &vcond); if (rv != APR_SUCCESS) break; rv = apr_brigade_puts(ctx->bb_out, NULL, NULL, cc_variant_get_string(&vcond, pool)); if (rv != APR_SUCCESS) break; } else if (CHECK_DIRECTIVE(buf, "if", 2)) { /** @todo: push current state to stack */ rv = eval_expression(r, ctx, pool, buf + 2, &vcond); if (rv != APR_SUCCESS) break; ctx->current_cond = cc_variant_get_bool(&vcond); ctx->cond_is_archived = ctx->current_cond; } #if 0 else if (CHECK_DIRECTIVE(buf, "ifdef", 2)) { /** @todo: push current state to stack */ } #endif else if (CHECK_DIRECTIVE(buf, "elsif", 5)) { rv = eval_expression(r, ctx, pool, buf + 5, &vcond); if (rv != APR_SUCCESS) break; ctx->current_cond = cc_variant_get_bool(&vcond); if (ctx->current_cond) ctx->cond_is_archived = 1; } #if 0 else if (CHECK_DIRECTIVE(buf, "elsifdef", 5)) { } #endif else if (CHECK_DIRECTIVE(buf, "else", 4)) { ctx->current_cond = ! ctx->cond_is_archived; if (ctx->current_cond) ctx->cond_is_archived = 1; } else if (CHECK_DIRECTIVE(buf, "endif", 5)) { /** @todo: pop state from stack */ ctx->current_cond = 1; ctx->cond_is_archived = 1; } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "invalid directive: near '#%s...'", buf); rv = APR_EGENERAL; break; } } while (0); apr_pool_destroy(pool); return rv; } static apr_status_t css_cond_filter(ap_filter_t *f, apr_bucket_brigade *bb) { request_rec *r = f->r; css_cond_ctx_t *ctx = f->ctx; if ((APR_BRIGADE_EMPTY(bb)) /* fast exit */ || (ctx && ctx->stage == STAGE_DONE)) /* already done */ return ap_pass_brigade(f->next, bb); if (! ctx) { /* initialization */ f->ctx = ctx = apr_pcalloc(r->pool, sizeof(*ctx)); ctx->strict_open_comment = 0; ctx->browser = create_browser_table(apr_table_get(r->headers_in, "User-Agent"), r->pool); ctx->bb_out = apr_brigade_create(r->pool, f->c->bucket_alloc); ctx->bb_work = apr_brigade_create(r->pool, f->c->bucket_alloc); ctx->open_comment = 0; ctx->current_cond = 1; ctx->stage = STAGE_START; } while (! APR_BRIGADE_EMPTY(bb)) { apr_status_t rv; apr_bucket *e; apr_size_t len; const char *str, *pos; e = APR_BRIGADE_FIRST(bb); if (APR_BUCKET_IS_EOS(e)) { ctx->stage = STAGE_DONE; break; } rv = apr_bucket_read(e, &str, &len, APR_BLOCK_READ); if (rv != APR_SUCCESS) return rv; /** @todo: */ switch (ctx->stage) { case STAGE_WAIT_OPEN: for (pos = str; len > 0; pos ++, len --) if (*pos == '/') break; if (len > 0) { apr_bucket_split(e, pos - str); apr_brigade_cleanup(ctx->bb_work); /* safety */ ctx->may_be_open_comment = 0; ctx->stage = STAGE_READ_OPEN1; } if (ctx->current_cond) APR_BUCKET_MOVE_TO_BRIGADE(e, ctx->bb_out); else APR_BUCKET_REMOVE(e); /* to trash */ break; case STAGE_READ_OPEN1: if (len > 0) { /* assert(*str == '/'); */ apr_bucket_split(e, 1); APR_BUCKET_MOVE_TO_BRIGADE(e, ctx->bb_work); ctx->stage = STAGE_READ_OPEN2; } break; case STAGE_READ_OPEN2: if (len > 0) { if (*str == '#' && (ctx->open_comment || ! ctx->strict_open_comment)) { /* Now, cleanup working pad */ apr_brigade_cleanup(ctx->bb_work); apr_bucket_split(e, 1); APR_BUCKET_REMOVE(e); /* to trash */ ctx->stage = STAGE_COLLECT_STATEMENT; } else if (*str == '*') { apr_bucket_split(e, 1); APR_BUCKET_MOVE_TO_BRIGADE(e, ctx->bb_work); ctx->may_be_open_comment = 1; ctx->stage = STAGE_READ_OPEN3; } else { /* It wasn't a open tag */ if (ctx->current_cond) APR_BRIGADE_CONCAT(ctx->bb_out, ctx->bb_work); ctx->stage = STAGE_WAIT_OPEN; /* reintroduce */ } } break; case STAGE_READ_OPEN3: if (len > 0) { if (*str == '#') { /* Now, cleanup working pad */ apr_brigade_cleanup(ctx->bb_work); apr_bucket_split(e, 1); APR_BUCKET_REMOVE(e); /* to trash */ ctx->stage = STAGE_COLLECT_STATEMENT; break; } else { /* It wasn't a open tag */ if (ctx->current_cond) APR_BRIGADE_CONCAT(ctx->bb_out, ctx->bb_work); ctx->stage = STAGE_WAIT_OPEN; /* reintroduce */ } } break; case STAGE_COLLECT_STATEMENT: for (pos = str; len > 0; pos ++, len --) if (*pos == '/') break; if (len > 0) { apr_bucket_split(e, pos - str); APR_BUCKET_MOVE_TO_BRIGADE(e, ctx->bb_work); /* trailing slash '/' to trash */ e = APR_BRIGADE_FIRST(bb); apr_bucket_split(e, 1); APR_BUCKET_REMOVE(e); /* to trash */ /* Now, check condition */ if (ctx->may_be_open_comment) ctx->open_comment = 1; rv = eval_statement(r, ctx); /* Now, enter next new stage */ ctx->stage = STAGE_WAIT_OPEN; if (rv != APR_SUCCESS) return rv; } else { APR_BUCKET_MOVE_TO_BRIGADE(e, ctx->bb_work); } break; default: APR_BUCKET_REMOVE(e); /* to trash */ } } APR_BRIGADE_PREPEND(bb, ctx->bb_out); return ap_pass_brigade(f->next, bb); } static void register_hooks(apr_pool_t *p) { ap_register_output_filter("CSSCOND", css_cond_filter, NULL, AP_FTYPE_CONTENT_SET); /* applied after all of AP_FTYPE_RESOURCE filters; doesn't modify content-type */ } module AP_MODULE_DECLARE_DATA css_cond_module = { STANDARD20_MODULE_STUFF, NULL, /* create per-directory config structure */ NULL, /* merge per-directory config structure */ NULL, /* create per-server config structure */ NULL, /* merge per-directory config structure */ NULL, /* command apr_table_t */ register_hooks /* register hooks */ };
ライセンス
とりあえず NYSL