// ==UserScript==
// @name      CSSfile Highlighter for Opera
// @namespace http://misttrap.s101.xrea.com/
// @exclude   http://*http://
// ==/UserScript==

(function() {

	// checking loosely whether this page is a CSS file or not

	var path = location.pathname.split('/');
	if(!/\.css/i.test(path[path.length - 1])) return;

	var body = document.body;
	if(!body) return;
	var pre  = body.firstChild;
	if(!pre || pre.nodeName.toLowerCase() != 'pre') return;

	var Configure = {
		rotation : [2, 4, 0 /* raw display */],
		index    : 0,
		tabkey   : 116, // "t" key
		wrapped  : true,
		wrapkey  : 119  // "w" key
	};

	var Styles = {
		color_background : '#fafafa',
		color_base       : '#000000',
		fontsize_base    : '15px',
		lineheight_base  : '1',
		fontfamily_base  : 'monospace',
		width_underline  : '1px',
		color_underline  : '#808080',
		color_at_mark    : '#ffa500',
		color_propkey    : '#008000',
		color_propval    : '#800000',
		color_comment    : '#ff0000'
	};

	var CSSHighlighter = function(txt) {
		this.i    = 0;
		this.buf  = '';
		this.code = [];
		this.txt  = new String(txt.replace(/\r\n?/g, '\n'));
	};

	CSSHighlighter.prototype = {
		getText : function() {
			var methods = [
				'markup_base',  // 0 : 地の文
				'markup_at',    // 1 : @charset, @import, @namespace
				'markup_media', // 2 : @media
				'markup_key',   // 3 : プロパティ名
				'markup_value'  // 4 : プロパティ値
			];
			var flag = 0;
			for(var txt = this.txt, len = txt.length; this.i < len; this.i++) {
				var now = txt.charAt(this.i);
				if(now == '/' && txt.charAt(this.i + 1) == '*') {
					this[methods[flag]]('');
					// コメント3文字目("/*"の次)に対して終了判定を行わないようにする
					this.buf = '/*' + txt.charAt(this.i += 2);
					this.comment();
				} else if(now == "'" || now == '"') {
					this.buf += now;
					this.string(now);
				} else if(flag == 0) {
					if(now == '@') {
						var next = txt.charAt(++this.i);
						if(next == 'c' || next == 'i' || next == 'n') {
							// @charset, @import, @namespace
							this.markup_base();
							this.buf = now + next;
							flag = 1;
						} else if(next == 'm') {
							// @media
							this.markup_base();
							this.buf = now + next;
							flag = 2;
						} else {
							// @font-face, @page
							this.buf += now + next;
						}
					} else {
						this.buf += now;
						if(now == '{') {
							this.markup_base();
							this.buf = '';
							flag = 3;
						}
					}
				} else if(flag == 1) {
					this.buf += now;
					if(now == ';' && !/&(lt|gt|amp);$/.test(this.buf)) {
						this.markup_at();
						this.buf = '';
						flag = 0;
					}
				} else if(flag == 2) {
					// 波括弧が入れ子になるのでひとつスキップしておく
					this.buf += now;
					if(now == '{') {
						this.markup_media();
						this.buf = '';
						flag = 0;
					}
				} else if(flag == 3) {
					if(now == '}') {
						this.markup_key(now);
						this.buf = '';
						flag = 0;
					} else if(now == ':') {
						this.markup_key(now);
						this.buf = '';
						flag = 4;
					} else {
						this.buf += now;
					}
				} else {
					if(now == '}') {
						this.markup_value(now);
						this.buf = '';
						flag = 0;
					} else if(now == ';' && !/&(lt|gt|amp)$/.test(this.buf)) {
						this.markup_value(now);
						this.buf = '';
						flag = 3;
					} else {
						this.buf += now;
					}
				}
			}
			if(this.buf != '') {
				this[methods[flag]]('');
			}
			return this.code.join('');
		},
		comment : function() {
			for(var txt = this.txt, len = txt.length; ++this.i < len;) {
				var now = txt.charAt(this.i);
				this.buf += now;
				if(now == '/' && txt.charAt(this.i - 1) == '*') {
					break;
				}
			}
			this.code.push(this.addClass(this.buf, 'comment'));
			this.buf = '';
		},
		string : function(quo) {
			for(var txt = this.txt, len = txt.length, esc = 0; ++this.i < len;) {
				var now = txt.charAt(this.i);
				this.buf += now;
				if(now == '\\') {
					esc++;
				} else if(now == quo && (esc & 1) == 0) {
					// 引用符の直前に奇数個の"\"が存在していない
					break;
				} else {
					esc = 0;
				}
			}
		},
		markup_base : function() {
			this.code.push(this.buf.replace(/@(font-face|page)/, '<span class="at_mark">$&</span>'));
		},
		markup_at : function() {
			/*----------------------------------------------------------------------
				1. [@import or @namespace] + URL (引用符のみ、引用符付きURL関数)
				2. [@import or @namespace (1番から漏れた分)] or @charset
				3. URL (引用符無しURL関数)
			----------------------------------------------------------------------*/
			this.code.push(
				this.buf.replace(
					/^@(import|namespace)([^'"]+)(['"])([^'"]+)['"]/,
					'<span class="at_mark">@$1</span>$2$3<a href="$4">$4</a>$3'
				).replace(
					/^@(import|namespace|charset)/,
					'<span class="at_mark">$&</span>'
				).replace(
					/(url\(\s*)([^\s'")]+)(\s*\))/,
					'$1<a href="$2">$2</a>$3'
				)
			);
		},
		markup_media : function() {
			this.code.push(this.buf.replace(/^@media/, '<span class="at_mark">@media</span>'));
		},
		markup_key : function(now) {
			this.code.push(this.addClass(this.buf, 'propkey') + now);
		},
		markup_value : function(now) {
			var self = this;
			this.code.push(this.addClass(this.buf.replace(
				/(url\(\s*)(['"]?)([^\s'")]+)['"]?(\s*\))/g,
				'$1$2<a href="$3">$3</a>$2$4'
			).replace(/#[0-9a-f]{3,}/gi, function(s) {
				var n = parseInt(s.substr(1), 16);
				switch(s.length) {
					case 7  : return self.addColor(s, n >> 16, n >> 8 & 255, n & 255);
					case 4  : return self.addColor(s, (n >> 8) * 17, (n >> 4 & 15) * 17, (n & 15) * 17);
					default : return s;
				}
			}).replace(/rgb\([^)]+\)/g, function(s) {
				return /(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/.test(s)
					? self.addColor(s,
						parseInt(RegExp.$1),
						parseInt(RegExp.$2),
						parseInt(RegExp.$3))
					: /(\.\d+|\d{1,3}(\.\d+)?)%\s*,\s*(\.\d+|\d{1,3}(\.\d+)?)%\s*,\s*(\.\d+|\d{1,3}(\.\d+)?)%/.test(s)
					? self.addColor(s,
						parseFloat(RegExp.$1) * 2.55,
						parseFloat(RegExp.$3) * 2.55,
						parseFloat(RegExp.$5) * 2.55)
					: s;
			}).replace(/!\s*important/, function(s) {
				return self.addClass(s, 'at_mark');
			}), 'propval') + now);
		},
		addColor : function(str, r, g, b) {
			/*-------------------------------------
				輝度を計算して適切な文字色を決定
				閾値の"155 * 1000"は主観です
			-------------------------------------*/
			var color = 299 * r + 587 * g + 114 * b > 155000 ? '#000000' : '#ffffff';
			// 改行を含んでいるとタグが壊れるので消しておく
			var bgcolor = str.replace(/\n/g, '');
			return this.forEach(str, function() {
				return '<span style="color:' + color +
					';background-color:' + bgcolor + ';">' + this + '</span>';
			});
		},
		addClass : function(str, className) {
			return this.forEach(str, function() {
				return '<span class="' + className + '">' + this + '</span>';
			});
		},
		forEach : function(str, callback) {
			return str.replace(/\S.*\S|\S/gm, function(r0) {
				return callback.call(r0);
			});
		}
	};

	var TabChanger = function(args) {
		this.order = args.rotation;
		this.i     = args.index;
		this.key   = args.tabkey;
		this.width = 0;
		this.count = 0;
		this.spaces = this.getSpaces();
		this.exText = document.createExpression('//span[@class="tabchar"]/text()', null);
		[this.lists, this.times] = this.evaluate(document);
	};

	TabChanger.prototype = {
		setChars : function() {
			var chars = this.order[this.i], i = 0, len;
			if(chars > 0) {
				var lists = this.lists, times = this.times;
				for(len = lists.length; i < len;) {
					this.count = times[i];
					this.inspect(lists[i++], chars);
				}
			} else {
				var items = this.exText.evaluate(document, 7, null);
				for(len = items.snapshotLength; i < len;) {
					items.snapshotItem(i++).nodeValue = '\t';
				}
			}
		},
		inspect : function(parent, chars) {
			for(var i = 0, childs = parent.childNodes, len = childs.length;
				this.count > 0 && i < len;) {
				var child = childs[i++];
				if(child.nodeType == 3) {
					this.width += this.getWidth(child.nodeValue);
				} else if(child.className != 'tabchar') {
					this.inspect(child, chars);
				} else {
					child.firstChild.nodeValue = this.spaces[chars - this.width % chars];
					this.width = 0;
					this.count--;
				}
			}
		},
		getWidth : function(str) {
			for(var i = str.length, res = i, charCode; i-- > 0;)
				(charCode = str.charCodeAt(i)) > 0x7F    &&
				(charCode < 0xFF61 || charCode > 0xFF9F) && res++;
			return res;
		},
		getSpaces : function() {
			for(var i = 1, max = Math.max.apply(Math, this.order), res = ['']; i <= max;)
				res[i] = Array(++i).join(' ');
			return res;
		},
		evaluate : function(doc) {
			var lists = [], times = [];
			var items = doc.evaluate('//li[.//span[@class="tabchar"]]', doc, null, 7, null);
			var exTab = doc.createExpression('count(.//span[@class="tabchar"])', null);
			for(var i = 0, len = items.snapshotLength; i < len;)
				times[i] = exTab.evaluate(
					lists[i] = items.snapshotItem(i++), 1, null
				).numberValue;
			return [lists, times];
		},
		initEvent : function() {
			KeyBind.add(this.key, this.increment, this);
		},
		increment : function() {
			this.i = (this.i + 1) % this.order.length;
			this.setChars();
		}
	};

	var WrapChanger = function(args) {
		this.wrap  = args.wrapped;
		this.key   = args.wrapkey;
		this.style = this.getStyle();
	};

	WrapChanger.prototype = {
		getStyle : function() {
			var rules = document.styleSheets[0].cssRules;
			for(var i = rules.length; i-- > 0;) {
				var rule = rules[i];
				if(rule.selectorText.toLowerCase() == 'li') {
					return rule.style;
				}
			}
		},
		initEvent : function() {
			KeyBind.add(this.key, this.shift, this);
		},
		shift : function() {
			this.style.whiteSpace = (this.wrap = !this.wrap) ? 'pre-wrap' : 'pre';
		}
	};

	var KeyBind = {
		initialize : function() {
			var self = this;
			document.addEventListener('keypress', function(evt) {
				var funcs = self['key_' + evt.which];
				if(!funcs) return;
				for(var i = 0, len = funcs.length; i < len;) {
					var pair = funcs[i++]
					pair[0].call(pair[1], evt);
				}
				evt.preventDefault();
			}, false);
		},
		add : function(key, callback, thisObject) {
			var method = 'key_' + key;
			(this[method] || (this[method] = [])).push([callback, thisObject]);
		}
	};

	KeyBind.initialize();

	new function() {
		// add ">" mark (color : #cacaca, size : 5 x 9)
		var TAB_MARK = 'data:image/gif;base64,' +
			'R0lGODlhBQAJAIgAAP///8rKyiH5BAEAAAAALAAAAAAFAAkAAAIKDH6GoNjpIpynAAA7';

		// caching image data on hidden iframe
		var iframe = document.createElement('iframe');
		iframe.style.display = 'none';
		var ifrdoc = body.appendChild(iframe).contentDocument;
		ifrdoc.open('text/html');
		ifrdoc.write('<html><body><img src="' + TAB_MARK + '"></body></html>');
		ifrdoc.close();

		var html = document.documentElement;
		var head = html.firstChild;
		(head.nodeName.toLowerCase() == 'head'
			? head
			: html.insertBefore(document.createElement('head'), body)
		).innerHTML = [
			'<title>' + location.href + '</title>',
			'<style type="text/css">',
			'body{',
				'background-color:' + Styles.color_background + ';',
			'}',
			'li{',
				'color:'         + Styles.color_base      + ';',
				'font-size:'     + Styles.fontsize_base   + ';',
				'line-height:'   + Styles.lineheight_base + ';',
				'font-family:'   + Styles.fontfamily_base + ';',
				'border-bottom:' + Styles.width_underline + ' solid transparent;',
				'white-space:'   + (Configure.wrapped ? 'pre-wrap' : 'pre') + ';',
			'}',
			'li:hover{',
				'border-bottom-color:' + Styles.color_underline + ';',
			'}',
			'.empty{',
				'content:"\\A";',
				'white-space:pre;',
			'}',
			'.at_mark{',
				'color:' + Styles.color_at_mark + ';',
			'}',
			'.propkey{',
				'color:' + Styles.color_propkey + ';',
			'}',
			'.propval{',
				'color:' + Styles.color_propval + ';',
			'}',
			'.comment{',
				'color:' + Styles.color_comment + ';',
			'}',
			'.tabchar{',
				'background:transparent url("' + TAB_MARK + '") no-repeat scroll 1px 50%;',
			'}',
			'</style>'
		].join('');

		var txts = new CSSHighlighter(pre.innerHTML)
			.getText()
			.replace(/\t/g, '<span class="tabchar">\t</span>')
			.split('\n');
		for(var i = 0, len = txts.length, code = []; i < len; i++) {
			var txt = txts[i];
			code[i] = txt ? '<li>' + txt + '</li>' : '<li class="empty"></li>';
		}
		body.innerHTML = '<ol>' + code.join('') + '</ol>';

		var tc = new TabChanger(Configure);
		tc.setChars();
		tc.initEvent();

		new WrapChanger(Configure).initEvent();
	};

})();
