dW

Apr 28th 2011

Highlighting

I personally don’t know what I’d do without TextMate. I use it as my primary development environment. I even use it to publish on this weblog. It has some faults especially when handling gargantuan files, and its newer version is looking to be the Duke Nukem Forever of desktop software; but, it’s still the best text editor out there in my honest opinion. The current version gets occasional updates when they’re needed, but as a whole there’s no real active development on the application. It presents a problem because now some of the bundles are outdated, especially CSS. I don’t use all the autocompletion and snippets that are in the CSS bundle. I’m just sick of writing CSS3 code and having everything half-assed highlighted.

I’ve looked high and low for a pre-made CSS3 syntax highlighting language grammar file for TextMate. Unfortunately, none of them are really worth a damn. Usually they would fall apart simply due to the way I write my code which does appear to be somewhat unusual, would contain only Webkit proprietary crap, or would just be missing entire usable parts of CSS3. It’s not possible to syntax highlight everything in CSS3 because parts of it still are in flux, but there are quite a few modules that have syntaxes that are somewhat stable. My only option was to create my own CSS3 language grammar file.

TextMate CSS3 Language Grammar

To put it simply TextMate assigns scope selectors to text which has been selected by regular expressions. Since the scope selectors are language agnostic text can be colored and styled for all languages using a single theme. Since TextMate’s release many applications have copied the syntax highlighting — some nearly verbatim.

Without an actual CSS tokenizer it’s quite difficult to really accurately syntax highlight the language. Problems are going to be encountered no matter what is done, and there’s a limitation in the way TextMate uses regular expressions that makes it a bit more difficult: regular expressions are only matched against single lines of text. That means that a multi-line pattern cannot be used. This sounds like a stupid limitation at first, but it actually makes perfect sense. The document needs to be updated in real-time, and if a multiple line regular expression is used it will severely slow down the parser because it’d have to refresh the entire document upon every keystroke. Fortunately, there’s a mechanism where two single-line regexes are used to grab their matching lines and everything in-between them. Still, it’s nigh-impossible to cover every possible case in CSS. My document is no exception.

My goal with this was to properly syntax highlight CSS3, including paged media and SVG features. I’ve even included support for Opera’s proposed @viewport at-rule. The most recent update to the language grammar for CSS in TextMate contains in my opinion many superfluous scope selectors such as separate scopes for each vendor-specific property among others. I’ve simplified things where I deemed it necessary while adding support for many more features of CSS3.

{   scopeName = 'source.css';
    comment = '';
    fileTypes = ( 'css' );
    foldingStartMarker = '/\*\*(?!\*)|\{\s*($|/\*(?!.*?\*/.*\S))';
    foldingStopMarker = '(?<!\*)\*\*/|^\s*\}';
    patterns = (
        {   name = 'meta.at-rule.media.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)media)';
            end = '\s*(?=</style)|\s*\{';
            captures = {
                1 = { name = 'comment.block.css'; };
                2 = { name = 'keyword.control.at-rule.css'; };
                3 = { name = 'punctuation.definition.keyword.css'; };
            };
            patterns = (
                {   include = '#comment-block'; },
                {   include = '#media-feature'; },
                {   include = '#media-token'; },
                {   include = '#media-constant'; },
                {   include = '#media-and'; },
            );
        },
        {   name = 'meta.at-rule.page.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)page)';
            end = '\s*(?=</style)|\s*(?=\{)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; };
            };
            patterns = (
                {   include = '#comment-block'; },
                {   match = '\s*([\w\d]+)?((:)\b(first|last|left|right))?';
                    captures = { 
                        1 = { name = 'constant.other.page-name.css'; }; 
                        2 = { name = 'entity.other.attribute-name.pseudo-class.css';};
                        3 = { name = 'punctuation.definition.entity.css'; }; 
                    };
                },
            );
        },

        {   name = 'meta.at-rule.import.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)import)';
            end = '\s*(?=</style)|\s*(?=;)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.import.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; }; 
            };
            patterns = (
                {   begin = '(url)\s*(\()\s*';
                    end = '\s*(?=</style)|\s*(\))\s*';
                    beginCaptures = {
                        1 = { name = 'support.function.css'; };
                        2 = { name = 'punctuation.section.function.css'; };
                    };
                    endCaptures = { 1 = { name = 'punctuation.section.function.css'; }; };
                    patterns = (
                        {   include = '#string-single'; },
                        {   include = '#string-double'; },
                        {   include = '#string-unquoted'; },
                    );
                },
                    {   include = '#media-feature'; },
                    {   include = '#media-token'; },
                    {   include = '#media-constant'; },
                    {   include = '#media-and'; }
            );
        },
        {   name = 'meta.at-rule.charset.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)charset)';
            end = '\s*(?=</style)|\s*(?=;)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.charset.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; }; 
            };
            patterns = (
                {   include = '#string-single'; },
                {   include = '#string-double'; },
            );
        },
        {   name = 'meta.at-rule.namespace.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)namespace)(\s+([\w]+)\s+)?';
            end = '\s*(?=</style)|\s*(?=;)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.namespace.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; };
                5 = { name = 'constant.namespace.prefix.css';};
            };
            patterns = (
                {   include = '#comment-block'; },
                {   include = '#string-single'; },
                {   include = '#string-double'; },
            );
        },
        {   name = 'meta.at-rule.viewport.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)(-o-)?viewport)';
            end = '\s*(?=</style)|\s*(?=\{)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.viewport.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; };
            };
            patterns = (
                {   include = '#comment-block'; },
            );
        },
        {   name = 'meta.at-rule.font-face.css';
            begin = '\s*(/\*.*?\*/)?\s*((@)font-face)';
            end = '\s*(?=</style)|\s*(?=\{)';
            captures = {
                1 = { name = 'comment.block.css';}; 
                2 = { name = 'keyword.control.at-rule.font-face.css'; }; 
                3 = { name = 'punctuation.definition.keyword.css'; };
            };
            patterns = (
                {   include = '#comment-block'; },
            );
        },
        {   name = 'meta.property-list.css';
            begin = '\{';
            end = '\s*(?=</style)|\}';
            patterns = (
                {   include = '#comment-block'; },
                {   name = 'meta.property-name.css';
                    begin = '(?<![-_*a-z:])(?=[-_*a-z])';
                    end = '\s*(?=</style)|\s*(:)?(?![-_*a-z])';
                    endCaptures = { 1 = { name = 'punctuation.separator.key-value.css'; }; };
                    patterns = (
                        {   include = '#comment-block'; },
                        {   name = 'meta.at-rule.page.context.css';
                            comment = 'This is stupid, but there''s no other way to do it...';
                            begin = '((@)(bottom-center|bottom-left-corner|bottom-left|bottom-right-corner|bottom-right|left-middle|left-top|right-bottom|right-middle|right-top|top-center|top-left-corner|top-left|top-right-corner|top-right))';
                            end = '\s*(?=</style)|\s*(?=\{)';
                            captures = {
                                1 = { name = 'keyword.control.at-rule.css'; }; 
                                2 = { name = 'punctuation.definition.keyword.css'; };
                            };
                            patterns = (
                                {   include = '#comment-block'; },
                            );
                        },
                        {   name = 'support.type.property-name.vendor.css';
                            match = '(mso|-(ah|atsc|hp|khtml|moz|ms|ro|o|prince|rim|tc|wap|webkit|[-\w]+))-[-\w]+';
                        },
                        {   name = 'support.type.property-name.svg.css';
                            match = '\b(alignment-baseline|baseline-shift|clip-path|clip-rule|color-interpolation-filters|color-interpolation|color-profile|color-rendering|dominant-baseline|enable-background|fill-opacity|fill-rule|fill|filter|flood-color|flood-opacity|glyph-orientation-horizontal|glyph-orientation-vertical|image-rendering|kerning|lighting-color|marker-end|marker-mid|marker-start|marker|mask|pointer-events|shape-rendering|stop-color|stop-opacity|stroke-dasharray|stroke-dashoffset|stroke-linecap|stroke-linejoin|stroke-miterlimit|stroke-opacity|stroke-width|stroke|text-anchor|text-rendering|writing-mode)\b';
                        },
                        {   name = 'support.type.property-name.css';
                            match = '\b(animation-delay|animation-direction|animation-duration|animation-iteration-count|animation-name|animation-play-state|animation-timing-function|animation|azimuth|backface-visibility|background-attachment|background-clip|background-color|background-image|background-origin|background-position|background-repeat|background-size|background|bikeshedding|bleed|bookmark-label|bookmark-level|bookmark-state|bookmark-target|border-bottom-color|border-bottom-left-radius|border-bottom-right-radius|border-bottom-style|border-bottom-width|border-bottom|border-collapse|border-color|border-image-outset|border-image-repeat|border-image-slice|border-image-source|border-image-width|border-image|border-left-color|border-left-style|border-left-width|border-left|border-radius|border-right-color|border-right-style|border-right-width|border-right|border-spacing|border-style|border-top-color|border-top-left-radius|border-top-right-radius|border-top-style|border-top-width|border-top|border-width|border|bottom|box-decoration-break|box-shadow|box-sizing|break-after|break-before|break-inside|caption-side|clear|clip|color|column-count|column-fill|column-gap|column-rule-color|column-rule-style|column-rule-width|column-rule|column-span|column-width|columns|content|counter-increment|counter-reset|cue-after|cue-before|cue|cursor|display|elevation|empty-cells|flex-align|flex-direction|direction|flex-order|flex-pack|float-offset|float|font-family|font-feature-settings|font-kerning|font-language-override|font-size-adjust|font-size|font-stretch|font-style|font-synthesis|font-variant-alternates|font-variant-caps|font-variant-east-asian|font-variant-ligatures|font-variant-numeric|font-variant|font-weight|font|grid-columns|grid-rows|hanging-punctuation|height|hyphenate-after|hyphenate-before|hyphenate-character|hyphenate-limit-chars|hyphenate-limit-last|hyphenate-limit-lines|hyphenate-limit-zone|hyphenate-lines|hyphenate-resource|hyphens|image-resolution|left|letter-spacing|line-break|line-height|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|margin|marks|marquee-direction|marquee-loop|marquee-speed|marquee-style|max-height|max-width|min-height|min-width|opacity|orphans|outline-color|outline-style|outline-width|outline|overflow-style|overflow-x|overflow-y|overflow|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|pause-after|pause-before|pause|perspective-origin|perspective|phonemes|pitch-range|pitch|play-during|position|quotes|rest-after|rest-before|rest|richness|right|rotation-point|rotation|ruby-align|ruby-overhang|ruby-position|ruby-span|speak-header|speak-numeral|speak-punctuation|speakability|speak|speech-rate|src|stress|string-set|tab-size|table-layout|text-align-last|text-align|text-autospace|text-decoration-color|text-decoration-line|text-decoration-skip|text-decoration-style|text-decoration|text-emphasis-color|text-emphasis-position|text-emphasis-style|text-emphasis|text-indent|text-justify|text-outline|text-shadow|text-transform|text-trim|text-underline-position|text-wrap|top|transform-origin|transform-style|transform|transition-delay|transition-duration|transition-property|transition-timing-function|transition|unicode-bidi|vertical-align|vertical-position|visibility|voice-balance|voice-duration|voice-family|voice-pitch-range|voice-pitch|voice-rate|voice-stress|voice-volume|volume|white-space|widows|width|word-break|word-spacing|word-wrap|z-index)\b';
                        },
                    );
                },
                {   name = 'meta.property-value.css';
                    begin = '(?<=:)\s*';
                    end = '\s*(?=</style)|\s*(?=;)|(?=\})';
                    patterns = (
                        {   include = '#comment-block'; },
                        {   name = 'meta.function.attr.css';
                            begin = '(attr)\s*(\()\s*';
                            end = '\s*(\))\s*';
                            beginCaptures = {
                                1 = { name = 'support.function.css'; };
                                2 = { name = 'punctuation.section.function.css'; };
                            };

                        },
                        {   name = 'meta.function.counter.css';
                            begin = '(counter)\s*(\()\s*';
                            end = '\s*(?=</style)|\s*(\))\s*';
                            beginCaptures = {
                                1 = { name = 'support.function.css'; };
                                2 = { name = 'punctuation.section.function.css'; };
                            };
                            endCaptures = { 1 = { name = 'punctuation.section.function.css'; }; };
                            patterns = (
                                {   name = 'constant.other.counter.css';
                                    match = '[-\w\d]+';
                                },
                            );
                        },
                        {   begin = '(cubic-bezier|rgb(a)?|hsl(a)?)\s*(\()\s*';
                            end = '\s*(?=</style)|\s*(\))\s*';
                            beginCaptures = {
                                1 = { name = 'support.function.css'; };
                                2 = { name = 'punctuation.section.function.css'; };
                            };
                            endCaptures = { 1 = { name = 'punctuation.section.function.css'; }; };
                            patterns = (
                                {   name = 'punctuation.separator.argument.function.css';
                                    match = ',';
                                },
                                {   include = '#number'; },
                            );
                        },
                        {   begin = '(local|url)\s*(\()\s*';
                            end = '\s*(?=</style)|\s*(\))\s*';
                            beginCaptures = {
                                1 = { name = 'support.function.css'; };
                                2 = { name = 'punctuation.section.function.css'; };
                            };
                            endCaptures = { 1 = { name = 'punctuation.section.function.css'; }; };
                            patterns = (
                                {   include = '#string-single'; },
                                {   include = '#string-double'; },
                                {   include = '#string-unquoted'; },
                            );
                        },
                        {   name = 'support.constant.property-value.svg.css';
                            match = '\b(accumulate|after-edge|before-edge|bevel|butt|central|crispEdges|currentColor|end|evenodd|geometricPrecision|ideographic|linearRGB|lr-tb|lr|mathematical|middle|miter|no-change|nonzero|optimizeQuality|optimizeSpeed|painted|reset-size|rl-tb|rl|sRGB|small-caption|start|status-bar|stroke|sub|super|tb-rl|tb|text-after-edge|text-before-edge|use-script|visibleFill|visiblePainted|visibleStroke)\b';
                        },
                        {   name = 'support.constant.property-value.css';
                            match = '\b(A3|A4|A5|B4|B5|above|absolute|afar|after|alias|all-scroll|allow-end|all|alphabetic|alternate|always|amharic-abegede|amharic|annotation|arabic-indic|armenian|asterisks|auto|avoid-column|avoid-page|avoid|back|balance|baseline|before|behind|below|bengali|binary|blink|block-reverse|block|bolder|bold|border-box|both|bottom|box|break-all|break-word|bt|cambodian|cancel-all|cancel-line-through|cancel-overline|cancel-underline|capitalize|caption|cell|center|character-variant|check|circled-decimal|circled-lower-latin|circled-upper-latin|circle|cjk-earthly-branch|cjk-heavenly-stem|cjk-ideographic|clone|closed|col-resize|collapse|column|compact|condensed|consume-after|consume-before|contain|content-box|context-menu|copy|cover|crop|crosshair|cross|current|dashed|decimal-leading-zero|decimal|default|devanagari|diamond|digits|discard|disc|distribute-letter|distribute-space|distribute|dotted-decimal|dotted|dot|double-circled-decimal|double-circle|double|e-resize|each-line|ease-in-out|ease-in|ease-out|ease|end|ethiopic-abegede-am-et|ethiopic-abegede-gez|ethiopic-abegede-ti-er|ethiopic-abegede-ti-et|ethiopic-abegede|ethiopic-halehame-aa-er|ethiopic-halehame-aa-et|ethiopic-halehame-am-et|ethiopic-halehame-gez|ethiopic-halehame-om-et|ethiopic-halehame-sid-et|ethiopic-halehame-ti-er|ethiopic-halehame-ti-et|ethiopic-halehame-tig|ethiopic-numeric|ethiopic|ew-resize|extra-condensed|extra-expanded|expanded|fast|filed-circled-decimal|filled|fill|first|fixed|flat|flexbox|footnotes|force-end|force-start|forward|from-image|front|fullsize-kana|fullwidth|georgian|groove|gujarati|gurmukhi|hanging|hangul-consonant|hangul|hebrew|help|here|hidden|high|hiragana-iroha|hiragana|historical-forms|horizontal|hyphenate|hyphen|icon|ideograph-alpha|ideograph-numeric|infinite|inherit|ink|inline-block|inline-flexbox|inline-reverse|inline-table|inline|inset|inside|inter-cluster|inter-ideograph|inter-word|italic|japanese-formal|japanese-informal|justify|kannada|kashida|katakana-iroha|katakana|keep-all|keep-end|khmer|lao|last|ledger|leftwards|left|legal|letter|lighter|line-edge|line-through|linear|list-item|literal-punctuation|local|loose|loud|lower-alpha|lower-armenian|lower-greek|lower-hexadecimal|lower-latin|lower-norwegian|lower-roman|lowercase|low|lr|malayalam|manual|match-parent|medium|meet|menu|message-box|modal|moderate|mongolian|move|myanmar|n-resize|ne-resize|nesw-resize|new|no-content|no-display|no-drop|no-justify|no-limit|no-punctuation|no-repeat|none|normal|not-allowed|nowrap|ns-resize|nw-resize|nwse-resize|objects|oblique|octal|open|ordinal|oriya|ornament|oromo|outset|outside|overline|padding-box|page|parenthesised-decimal|parenthesised-lower-latin|parent|paused|persian|pointer|pre-line|pre-wrap|preserve-3d|preserve-breaks|preserve|pre|progress|punctuation|reduced|relative|repeat-x|repeat-y|repeat|reverse|ridge|rightwards|right|rl|root|round|row-resize|ruby-base-group|ruby-base|ruby-text-group|ruby-text|ruby|run-in|running|s-resize|scoll|scroll|se-resize|semi-condensed|semi-expanded|sesame|sidama|silent|simp-chinese-formal|simp-chinese-informal|slashed-zero|slice|slide|slow|small-caption|smaller|soft|solid|spaces|space|spell-out|spread|square|start|status-bar|stretch|strict|strong|style|stylistic|subscript|superscript|sw-resize|swash|syriac|table-caption|table-cell|table-column-group|table-column|table-footer-group|table-header-group|table-row-group|table-row|table|tab|tamil|tb|telugu|text|thai|tibetan|tigre|tigrinya-er-abegede|tigrinya-er|tigrinya-et-abegede|tigrinya-et|top|trad-chinese-formal|trad-chinese-informal|transparent|triangle|trim-inner|ultra-condensed|ultra-expanded|underline|upper-alpha|upper-armenian|upper-greek|upper-hexadecimal|upper-latin|upper-norwegian|upper-roman|uppercase|urdu|vertical-text|vertical|visible|w-resize|wait|wavy|weak|weight|window|x-fast|x-high|x-loud|x-low|x-slow|x-soft|x-strong|x-weak)\b';
                        },
                        {   name = 'support.constant.font-name.css';
                            match = '(\b((?i:arial|cambria|century|comic|consolas|constantia|corbel|cordia|courier|garamond|georgia|helvetica|impact|inconsolata|lucida|menlo|symbol|system|tahoma|times|trebuchet|utopia|verdana|webdings)|cursive|fantasy|monospace|sans-serif|serif)\b)';
                        },
                        {   include = '#string-single'; },
                        {   include = '#string-double'; },
                        {   name = 'support.constant.color.css';
                            match = '\b(?i:aliceblue|antiquewhite|aquamarine|aqua|azure|beige|bisque|black|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|darkcyan|cyan|darkblue|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|blue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|goldenrod|gold|lightgray|gray|greenyellow|green|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgrey|grey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|lime|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olivedrab|olive|orangered|orange|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|whitesmoke|white|yellowgreen|yellow)\b';
                        },
                        {   name = 'constant.other.color.rgb-value.css';
                            match = '(#)([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b';
                            captures = { 1 = { name = 'punctuation.definition.constant.css'; }; };
                        },
                        {   include = '#number'; },
                        {   include = '#units'; },
                        {   name = 'keyword.other.important.css';
                            match = '\!\s*important';
                        },
                    );
                },
            );
        }, 
        {   name = 'meta.selector.css';
            begin = '((?<=\})|^)\s*(/\*.*?\*/)?\s*(?=\s*[\|\[:.*#a-zA-Z])';
            end = '\s*(?=</style)|(?=\{)';
            beginCaptures = { 1 = { name = 'comment.block.css'; }; };
            patterns = (
                {   include = '#comment-block'; },
                {   begin = '([\w\d]+)\s*(?=(\|))';
                    end = '(\|)';
                    beginCaptures = { 1 = { name = 'constant.namespace.prefix.css'; }; };
                    endCaptures = { 1 = { name = 'punctuation.definition.namespace.css'; }; };
                },
                {   begin = '(\[)\s*([\w\d]+)\s*((\~|\^|\$|\*|\|)?\=)?\s*';
                    end = '\s*(?=</style)|(\])';
                    beginCaptures = {
                        1 = { name = 'punctuation.definition.attribute-selector.begin.css'; };
                        2 = { name = 'entity.other.attribute-name.css'; };
                        3 = { name = 'keyword.operator.attribute-selector.css'; };
                    };
                    endCaptures = { 1 = { name = 'punctuation.definition.attribute-selector.end.css'; }; };
                    patterns = (
                        {   include = '#string-single'; },
                        {   include = '#string-double'; },
                    );
                },
                {   include = '#string-single'; },
                {   include = '#string-double'; },
                {   name = 'entity.name.tag.css';
                    match = '\b(abbr|acronym|address|applet|area|article|aside|audio|basefont|base|a|bdo|big|blockquote|body|br|button|b|canvas|caption|center|cite|code|colgroup|col|command|datalist|dd|defs|del|desc|details|dfn|dir|div|dl|dt|embed|em|fieldset|figcaption|figure|font|footer|form|frameset|frame|h1|h2|h3|h4|h5|h6|header|head|hgroup|hr|html|iframe|image|img|input|ins|isindex|i|kbd|keygen|label|legend|g|link|listing|li|map|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|param|plaintext|pre|progress|q|rp|rt|ruby|samp|script|p|section|select|small|source|span|strike|strong|style|sub|summary|sup|svg|switch|symbol|s|table|tbody|td|textarea|tfoot|thead|th|time|title|track|tr|tt|ul|use|u|var|video|wbr|xmp)\b';
                },
                {   name = 'entity.other.attribute-name.class.css';
                    match = '(\.)[a-zA-Z0-9_-]+';
                    captures = { 1 = { name = 'punctuation.definition.entity.css'; }; };
                },
                {   name = 'entity.other.attribute-name.id.css';
                    match = '(#)[a-zA-Z][a-zA-Z0-9_-]*';
                    captures = { 1 = { name = 'punctuation.definition.entity.css'; }; };
                },
                {   name = 'entity.name.tag.wildcard.css';
                    match = '\*';
                },
                {   name = 'entity.other.attribute-name.pseudo-element.css';
                    match = '(:{1,2})\b(after|before|first-child|first-letter|first-line|last-child|marker|only-child|selection)\b';
                    captures = { 1 = { name = 'punctuation.definition.entity.css'; }; };
                },
                {   name = 'entity.other.attribute-name.pseudo-class.css';
                    match = '(:)\b(active|disabled|enabled|focus|hover|indeterminate|invalid|link|required|root|target|valid|visited)\b';
                    captures = { 1 = { name = 'punctuation.definition.entity.css'; }; };
                },
                {   begin = '(:)\b(nth(-last)?(-(child|of-type)))\s*\(\s*';
                    end = '\s*(?=</style)|\s*\)\s*';
                    captures = {
                        1 = { name = 'punctuation.definition.entity.css'; };
                        2 = { name = 'entity.other.attribute-name.pseudo-class.css'; };
                    };
                    patterns = (
                        {   name = 'constant.numeric.css';
                            match = '[^''") \t]+';
                        },
                    );
                },
                {   begin = '(:)\b(lang|not)\s*\(\s*';
                    end = '\s*(?=</style)|\s*\)\s*';
                    captures = {
                        1 = { name = 'punctuation.definition.entity.css'; };
                        2 = { name = 'entity.other.attribute-name.pseudo-class.css'; };
                    };
                    patterns = ( { include = '#string-unquoted'; } );
                },
            );
        },
        {   include = '#comment-block'; },
    );
    repository = {
        comment-block = {
            name = 'comment.block.css';
            begin = '/\*';
            end = '\*/';
            captures = { 0 = { name = 'punctuation.definition.comment.css'; }; };
        };
        media-and = {
            name = 'keyword.and.media.css';
            match = '\s+and\s+';
        };
        media-constant = {
            name = 'support.constant.media.css';
            match = 'all|braille|handheld|print|projection|screen|tty|tv';
        };
        media-feature = {
            name = 'meta.feature.media.css';
            begin = '(?<!url)(\()\s*';
            end = '\s*(?=</style)|\s*(\))';
            beginCaptures = { 1 = { name = 'punctuation.definition.feature.media.begin.css'; }; };
            endCaptures = { 1 = { name = 'punctuation.definition.feature.media.begin.css'; }; };
            patterns = (
                {   name = 'support.type.property-name.media.css';
                    match = '(((min|max)-)?(device-)?(width|height|aspect-ratio)|orientation|((min|max)-)?color(-index)?|((min|max)-)?(monochrome|resolution)|scan|grid|view-mode)';
                },
                {   name = 'meta.property-value.media.css';
                    begin = '(?<=:)';
                    end = '\s*(?=</style)|(?=(\s*\)))';
                    patterns = (
                        {   include = '#number'; },
                        {   include = '#units'; },
                        {   name = 'support.constant.property-value.css';
                            match = '\b(portrait|landscape|progressive|interlace|windowed|floating|fullscreen|maximized|minimized)\b';
                        },
                        {   name = 'punctuation.separator.ratio.css';
                            match = '/';
                        },
                        {   name = 'punctuation.separator.key-value.css';
                            match = ':';
                        },
                    );
                },
            );
        };
        media-token = {
            name = 'constant.token.media.css';
            match = '\s+(only|not)\s+';
        };
        number = {
            name = 'constant.numeric.css';
            match = '(-|\+)?\s*[0-9]+(\.[0-9]+)?';
        };
        string-double = {
            name = 'string.quoted.double.css';
            begin = '"';
            end = '"';
            beginCaptures = { 0 = { name = 'punctuation.definition.string.begin.css'; }; };
            endCaptures = { 0 = { name = 'punctuation.definition.string.end.css'; }; };
            patterns = (
                {   name = 'constant.character.escape.css';
                    match = '\\.';
                },
            );
        };
        string-single = {
            name = 'string.quoted.single.css';
            begin = "'";
            end = "'";
            beginCaptures = { 0 = { name = 'punctuation.definition.string.begin.css'; }; };
            endCaptures = { 0 = { name = 'punctuation.definition.string.end.css'; }; };
            patterns = (
                {   name = 'constant.character.escape.css';
                    match = '\\.';
                },
            );
        };
        string-unquoted = {
            name = 'string.unquoted.css';
            match = '[^''") \t]+';
        };
        units = {
            name = 'keyword.other.unit.css';
            match = '(?<=[\d])(deg|dpi|dpcm|px|pt|cm|mm|in|em|ex|pc|rad|s|ms)\b|%';
        };
    };
}

Caveats

As I mentioned before there are certain problems because of the difficulty in accurately highlighting CSS. The good news is that I’ve really only found one:

Example of a CSS Syntax Highlighting Error

As can be seen in the image the selectors in that first media query aren’t being highlighted properly. This is because of the left curly bracket in front of them. If the selectors start a line or are preceded by a right curly bracket they will highlight properly. I’ve yet to figure out a way around this; perhaps someone else will. Of course, there’s probably other caveats that I haven’t discovered yet. I’ll attempt to fix them as they’re discovered.

Install

Open TextMate and follow these directions to install:

  1. In the main menus go to Bundles → Bundle Editor → Show Bundle Editor.
  2. On the top on the left will be a select box. Click on it and go to “Languages”.
  3. In the listing on the left click on the dropdown arrow to the left of CSS and click on the language grammar item that has been made visible.
  4. In the textarea to the right paste what you see above into the box.
  5. Click the “Test” button and close the Bundle Editor.

I’ve been playing around with and using this language grammar document for a bit now, and I find it at least stable enough for everyday usage. If there’s problems don’t hesitate to contact me. However, I release this to the public without implying that I have any responsibility for any damage it does to your computer. I offer no warranty for this software. If you don’t understand much of what’s written here it’d be best to not attempt installation at all.