diff --git a/after/syntax/html.vim b/after/syntax/html.vim index 56ba64a..fd69968 100644 --- a/after/syntax/html.vim +++ b/after/syntax/html.vim @@ -1,2 +1,42 @@ " default html syntax should already be including the css syntax -call css_color#init('none', 'none', 'htmlCommentPart,htmlString') +" but tag style attributes don't properly apply CSS syntax (as of vim 9.0.1378). +" Correct the bug by replacing htmlCssDefinition with a new htmlCssContent: {{{ +syn keyword htmlCssArg contained containedin=htmlTag nextgroup=htmlCssEq style +syn match htmlCssEq contained +=+ nextgroup=htmlCssQuote +syn match htmlCssQuote contained +["']+ nextgroup=htmlCssContent +syn region htmlCssContent contained start=+\%("\)\@<=.+ end=+"+ keepend contains=cssTagName,cssAttributeSelector,cssClassName,cssIdentifier,cssAtRule,cssAttrRegion,css.*Prop,cssComment,cssValue.*,cssColor,cssURL,cssImportant,cssCustomProp,cssError,cssStringQ,cssFunction,cssUnicodeEscape,cssVendor,cssHacks,cssNoise +syn region htmlCssContent contained start=+\%('\)\@<=.+ end=+'+ keepend contains=cssTagName,cssAttributeSelector,cssClassName,cssIdentifier,cssAtRule,cssAttrRegion,css.*Prop,cssComment,cssValue.*,cssColor,cssURL,cssImportant,cssCustomProp,cssError,cssStringQ,cssFunction,cssUnicodeEscape,cssVendor,cssHacks,cssNoise + +hi def link htmlCssArg htmlArg +hi def link htmlCssEq htmlTag +hi def link htmlCssQuote htmlString +" end bugfix }}} + +call css_color#init('css', 'extended', 'htmlCssContent,htmlCommentPart') + +" SVG in HTML +"syn keyword htmlTagName contained svg text textPath tspan +syn include @htmlSvgColors after/syntax/svg.vim +syn region htmlSvg start=+<\s*svg\>+ end=+<\s*/svg\s*>+ keepend contains=@htmlXml,htmlTagName,@htmlSvgColors + +" Legacy HTML 3-style color declarations (depcecated) {{{ +" Search https://www.w3.org/TR/html4/index/attributes.html for `%Color;` +syn region htmlTag start=+<\s*body\s+ end=+>+ fold contains=htmlTagN,htmlString,htmlArg,htmlValue,htmlTagError,htmlEvent,htmlCssDefinition,@htmlPreproc,@htmlArgCluster,htmlTagBodyColors +syn keyword htmlTagBodyColors contained nextgroup=htmlLegacyColor text bgcolor link alink vlink +syn region htmlTag start=+<\s*\%(table\|t[rdh]\)\s+ end=+>+ fold contains=htmlTagN,htmlString,htmlArg,htmlValue,htmlTagError,htmlEvent,htmlCssDefinition,@htmlPreproc,@htmlArgCluster,htmlBgColors +syn keyword htmlBgColors contained nextgroup=htmlLegacyColor bgcolor +syn region htmlTag start=+<\s*\%(base\)\?font\s+ end=+>+ fold contains=htmlTagN,htmlString,htmlArg,htmlValue,htmlTagError,htmlEvent,htmlCssDefinition,@htmlPreproc,@htmlArgCluster,htmlFgColors +syn keyword htmlFgColors contained nextgroup=htmlLegacyColor color +" The spec only supports named colors & hash-prefixed RGB hex with 3 or 6 chars. +" (Both FF & Chrome use IE's crazy "flex hex" formula to convert anything else to hex: +" https://scrappy-do.blogspot.com/2004/08/little-rant-about-microsoft-internet.html -- we're NOT doing that.) +" BUG: I should have been able to specify this as +="[^"]\+"+ and +="[^"]\+"+ and set what +" matches with the args to css_color#init(), but that doesn't seem to work (overridden?) +syn match htmlLegacyColor contained +=\(['"]\)\%(#\x\{3\}\%(\x\{3\}\)\?\|[A-Za-z]\+\)\1+ contains=cssColor + +hi def link htmlTagBodyColors htmlArg +hi def link htmlBgColors htmlArg +hi def link htmlFgColors htmlArg + +call css_color#init('hex', 'extended', 'htmlLegacyColor') +" end legacy HTML-3 color attributes }}} \ No newline at end of file diff --git a/after/syntax/svg.vim b/after/syntax/svg.vim index be932c2..55634eb 100644 --- a/after/syntax/svg.vim +++ b/after/syntax/svg.vim @@ -1 +1,9 @@ -call css_color#init('rgba', 'basic', 'xmlComment,xmlCommentPart,xmlString') +syn region xmlTag start=+<\s*\%(circle\|ellipse\|line\|path\|poly\%(gon\|line\)\|rect\|text\%(path\)\?\|tref\|tspan\)\s+ end=+>+ fold contains=xmlTagName,xmlAttrib,xmlEqual,xmlString,@xmlStartTagHook,htmlString,htmlArg,htmlValue,htmlTagError,htmlEvent,htmlCssDefinition,@htmlPreproc,@htmlArgCluster,svgAttrib +syn keyword svgAttrib contained containedin=xmlTag nextgroup=svgColor fill color stroke sop-color flood-color lighting-color +syn match svgColor contained +="[^"]\+"+ +syn match svgColor contained +='[^']\+'+ + +hi def link svgAttrib Type +hi def link svgColor String + +call css_color#init('css', 'extended', 'svgColor') \ No newline at end of file diff --git a/autoload/css_color.vim b/autoload/css_color.vim index ebfc705..fa2aadc 100644 --- a/autoload/css_color.vim +++ b/autoload/css_color.vim @@ -11,17 +11,60 @@ if ! ( v:version >= 700 && has('syntax') && ( has('gui_running') || has('nvim') finish endif +let s:_invalid = -9999 " used after bounding/wrapping, so this is safe + +" Add open-parens as a keyword char to avoid coloring function names like tan(). +" This is ... not advised; it's unclear what it might break +"syn iskeyword @,40,48-57,_,192-255,- + +function! s:bound(n, min, max) + let n = a:n + if n < a:min | let n = a:min + elseif n > a:max | let n = a:max | endif + return str2nr( string(n + 0.5) ) +endfunction + +function! s:rgb2hex(r, g, b) + let [r,g,b] = map( [a:r,a:g,a:b], 's:bound(v:val, 0, 255)' ) + return printf( '%02x%02x%02x', r, g, b ) +endfunction + function! s:rgb2color(r,g,b) - " Convert 80% -> 204, 100% -> 255, etc. - let rgb = map( [a:r,a:g,a:b], 'v:val =~ "%$" ? ( 255 * v:val ) / 100 : v:val' ) - return printf( '%02x%02x%02x', rgb[0], rgb[1], rgb[2] ) + " Convert 35.6% -> 90.78, 80% -> 204, 100% -> 255, etc. + let [r,g,b] = map( [a:r,a:g,a:b], 'v:val =~ "%$" ? str2float(v:val) * 2.55 : str2float(v:val)' ) + return s:rgb2hex(r, g, b) +endfunction + +" Vimscript modulo can't handle decimals. Fix that. +function! s:modulo(a, b) + let num = str2nr( string(a:a) ) + let extra = a:a - num + return str2nr(a:a - extra) % a:b + extra +endfunction + +function! s:angle2deg(angle, units) + let deg = str2float(a:angle) + " angles: https://developer.mozilla.org/en-US/docs/Web/CSS/angle + if a:units == "rad" + let deg = deg * 57.29577951308232 " deg = rad * 180 / pi, 360deg == 6.2832rad + elseif a:units == "grad" + let deg = deg * 0.9 " 360deg = 400grad + elseif a:units == "turn" + let deg = deg * 360.0 " 360deg = 1turn + elseif a:units != "" && a:units != "deg" + return s:_invalid " percent is invalid here + endif + let deg = s:modulo(deg, 360) + if deg < 0 | let deg = deg + 360 | endif + return deg endfunction -function! s:hsl2color(h,s,l) - " Convert 80% -> 0.8, 100% -> 1.0, etc. - let [s,l] = map( [a:s, a:l], 'v:val =~ "%$" ? v:val / 100.0 : v:val + 0.0' ) +function! s:hsl2rgb(h,u,s,l) + let [s,l] = map( [a:s, a:l], 'str2nr(v:val) >= 1 ? 1.0 : str2float(v:val)' ) " algorithm transcoded to vim from http://www.w3.org/TR/css3-color/#hsl-color - let hh = ( a:h % 360 ) / 360.0 + let hh = s:angle2deg(a:h, a:u) + if hh == s:_invalid | return [hh,hh,hh] | endif + let hh = hh / 360.0 let m2 = l <= 0.5 ? l * ( s + 1 ) : l + s - l * s let m1 = l * 2 - m2 let rgb = [] @@ -32,12 +75,49 @@ function! s:hsl2color(h,s,l) \ h * 2 < 1 ? m2 : \ h * 3 < 2 ? m1 + ( m2 - m1 ) * ( 2/3.0 - h ) * 6 : \ m1 - if v > 1.0 | return '' | endif + if v > 1.0 | return [s:_invalid, s:_invalid, s:_invalid] | endif let rgb += [ float2nr( 255 * v ) ] endfor - return printf( '%02x%02x%02x', rgb[0], rgb[1], rgb[2] ) + return rgb endfunction +" vet hsl and hwb values +function! s:pctvet(h,s,l) + " hue cannot be a percent, others must be percents (except for sass & scss) + if a:h =~ "%$" || b:current_syntax !~ "s[ac]ss$" && (a:s !~ "%$" || a:l !~ "%$") + return [s:_invalid, s:_invalid, s:_invalid] + endif + let [s,l] = map( [a:s, a:l], 'v:val >= 100 ? 1.0 : v:val / 100.0' ) + return [a:h, s, l] +endfunction + +function! s:hsl2color(h,u,s,l) + let [h,s,l] = s:pctvet(a:h, a:s, a:l) " convert saturation & luminance % -> num + if h == s:_invalid | return '' | endif + let [r,g,b] = s:hsl2rgb(h, a:u, s, l) + if r == s:_invalid | return '' | endif + return s:rgb2hex(r, g, b) +endfunction + +function! s:hwb2color(h,u,w,b) + let [h,w,b] = s:pctvet(a:h, a:w, a:b) " convert whiteness & blackness % -> num + if h == s:_invalid | return '' | endif + " algorithm transcoded to vim from https://drafts.csswg.org/css-color/#hwb-to-rgb + if w + b >= 1 + let gray = w / (w + b) + let [r,g,b] = [gray,gray,gray] + else + let [r,g,b] = map( s:hsl2rgb(a:h, a:u, 1.0, 0.5), 'v:val * (1 - w - b) + w' ) + endif + return s:rgb2hex(r, g, b) +endfunction + +" TODO (probably not): new "device-independent colors": +" lch(), oklch(), lab(), oklab(), and color(). +" These look hard and I don't see reference algorithms to convert to sRGB. +" https://developer.mozilla.org/en-US/docs/Web/CSS/color_value +" https://drafts.csswg.org/css-color/#lab-colors + let s:_1_3 = 1.0/3 let s:_16_116 = 16.0/116.0 let s:cos16 = cos(16*(180/atan2(0,-1))) @@ -170,14 +250,17 @@ function! s:create_syn_match() let hex = submatch(1) let funcname = submatch(2) + if funcname =~ 'rgb' && submatch(4) != '' | return '' | endif " rgb() doesn't support units + let rgb_color - \ = funcname == 'rgb' ? s:rgb2color(submatch(3),submatch(4),submatch(5)) - \ : funcname == 'hsl' ? s:hsl2color(submatch(3),submatch(4),submatch(5)) + \ = funcname =~ '^rgb' ? s:rgb2color(submatch(3),submatch(5),submatch(6)) + \ : funcname =~ '^hsl' ? s:hsl2color(submatch(3),submatch(4),submatch(5),submatch(6)) + \ : funcname == 'hwb' ? s:hwb2color(submatch(3),submatch(4),submatch(5),submatch(6)) \ : strlen(hex) >= 6 ? tolower(hex[0:5]) \ : strlen(hex) >= 3 ? tolower(hex[0].hex[0].hex[1].hex[1].hex[2].hex[2]) \ : '' - if rgb_color == '' | throw 'css_color: create_syn_match invoked on bad match data' | endif + if rgb_color == '' | return '' | endif let s:pattern_color[pattern] = rgb_color endif @@ -229,12 +312,14 @@ endfunction let s:_hexcolor = '#\(\x\{3}\%(\>\|\x\{3}\>\)\)' " submatch 1 let s:_rgbacolor = '#\(\x\{3}\%(\>\|\x\%(\>\|\x\{2}\%(\>\|\x\{2}\>\)\)\)\)' " submatch 1 -let s:_funcname = '\(rgb\|hsl\)a\?' " submatch 2 +let s:_funcname = '\(rgb\a\?\|hsla\?\|hwb\)' " submatch 2 +let s:_consistent = '\%(\%(\s\+[0-9.%]\+\)\{2}\s*\%(\/\s*[-0-9.%]\+\)\?[)]\|\%(\s*,\s*[-0-9.%]\+\)\{2,3}[)]\)\@=' " lookahead: 1 2 3 or 1 2 3/4 or 1,2,3 or 1,2,3,4 after 1 let s:_ws_ = '\s*' -let s:_numval = s:_ws_ . '\(\d\{1,3}%\?\)' " submatch 3,4,5 -let s:_listsep = s:_ws_ . ',' -let s:_otherargs_ = '\%(,[^)]*\)\?' -let s:_funcexpr = s:_funcname . '[(]' . s:_numval . s:_listsep . s:_numval . s:_listsep . s:_numval . s:_ws_ . s:_otherargs_ . '[)]' +let s:_numval = '\(-\?\d\{1,3}\%(\.\d*\)\?%\?\)' " submatch 3,5,6 +let s:_units = '\(deg\|g\?rad\|turn\)\?' " submatch 4 +let s:_listsep = s:_ws_ . '[,[:space:]]\+' +let s:_otherargs_ = s:_ws_ . '\%([,\/][^)]*\)\?' " ignore alpha +let s:_funcexpr = s:_funcname . '[(]' . s:_ws_ . s:_numval . s:_units . s:_consistent . s:_listsep . s:_numval . s:_listsep . s:_numval . s:_otherargs_ . '[)]' let s:_csscolor = s:_rgbacolor . '\|' . s:_funcexpr " N.B. sloppy heuristic constants for performance reasons: " a) start somewhere left of screen in case of partially visible colorref diff --git a/syntax/colornames/basic.vim b/syntax/colornames/basic.vim index 37730ce..688ba89 100644 --- a/syntax/colornames/basic.vim +++ b/syntax/colornames/basic.vim @@ -32,7 +32,7 @@ syn case ignore syn keyword BG000000 black contained containedin=@colorableGroup syn keyword BGc0c0c0 silver contained containedin=@colorableGroup syn keyword BG808080 gray contained containedin=@colorableGroup -syn match BGffffff "\c\" contained containedin=@colorableGroup +syn keyword BGffffff white contained containedin=@colorableGroup syn keyword BG800000 maroon contained containedin=@colorableGroup syn keyword BGff0000 red contained containedin=@colorableGroup syn keyword BG800080 purple contained containedin=@colorableGroup diff --git a/tests/bench b/tests/bench index 21e6eea..48a0384 100755 --- a/tests/bench +++ b/tests/bench @@ -1,6 +1,15 @@ #!/bin/sh -set -e s=startuptime.txt -vim --startuptime $s -o -c qa "$@" -perl -lane'$sum += $F[1] if m!/\.vim/after/syntax/css\.vim$!; END {print $sum}' $s -rm $s +this=$(realpath "$0" 2>/dev/null || readlink -f "$0") +[ $# = 0 ] && cd "${this%/*}" && set -- *.* # default to all files with a dot +target="${this%/tests/*}/after/syntax/css.vim" +[ $# -gt 1 ] && multi=1 || multi= +for sample in "$@"; do + vim --startuptime $s -c qa "$sample" + run=$(awk -v t="$target" 'index($0, t) {sum += $2} END {print sum}' "$s") + rm $s + echo "$run${multi:+\t$sample}" + total="${total:-0} + ${run:-0}" +done +# if multi, show the sum total. otherwise, return true only if we had a run. +[ -n "$multi" ] && echo "$(echo "$total" |bc -ql)\t(total)" || [ -n "$run" ] diff --git a/tests/example.html b/tests/example.html new file mode 100644 index 0000000..fcc3437 --- /dev/null +++ b/tests/example.html @@ -0,0 +1,22 @@ + + + + + +

#faa

+ +

#ff0000

+ + +

Text that is red.

+ +

+ Here's a visited link + and an unvisited link. +

+ + diff --git a/tests/example.sass b/tests/example.sass index e1c8cd0..71cb87c 100644 --- a/tests/example.sass +++ b/tests/example.sass @@ -10,7 +10,7 @@ s !color1 = #359 !color2 = #335599 !color3 = rgba(144, 0, 0, .5) -!color4 = hsl(0, 100%, 50%) +!color4 = hsl(0, 100, 50) /* * #123, #456 diff --git a/tests/example.scss b/tests/example.scss index a93f4da..91b47fc 100644 --- a/tests/example.scss +++ b/tests/example.scss @@ -6,7 +6,7 @@ s { background: hsl(0, 100%, 50%) } $color1: #359; $color2: #335599; $color3: rgba(144, 0, 0, .5); -$color4: hsl(0, 100%, 50%); +$color4: hsl(0, 100, 50); /* * #123, #456 diff --git a/tests/example.svg b/tests/example.svg new file mode 100644 index 0000000..bb19de4 --- /dev/null +++ b/tests/example.svg @@ -0,0 +1,14 @@ + + + + + + + SVG + + + diff --git a/tests/suite.css b/tests/suite.css new file mode 100644 index 0000000..5b4cd26 --- /dev/null +++ b/tests/suite.css @@ -0,0 +1,102 @@ +/* see also https://developer.mozilla.org/en-US/docs/Web/CSS/color + * and https://www.w3schools.com/colors/colors_converter.asp */ + +i { background: #359 } +b { background: #335599 } +u { color: #12345678; background: rgba(144, 0, 0, .5) } +s { color: #1234; background: hsl(0, 100%, 50%) } +p { color:rebeccapurple; background-color:rgb(222 255 202 / 0.2) } + +/* These are all pretty much the same color (barring rounding and alpha) */ +a.rgb { color: rgb(0, 17, 136); } /* no alpha */ +a.rgb1 { color: rgb(0, 17, 136, 1); } /* alpha but fully opaque */ +a.rgba { color: rgba(0, 17, 136, 1); } /* explicit alpha but fully opaque */ +a.rgbp { color: rgb(0, 17, 136, 50%); } /* half transparent (via percent) */ +a.rgb0 { color: rgb(0, 17, 136, 0.1); } /* very transparent */ +a.rgbz { color: rgb(0, 17, 136, 0); } /* fully transparent */ +a.sp { color:rgb(0 , 17, 136, 0.3) } /* extra spaces */ +a.rgbd { color: rgb(0, 16.99, 136); } /* CSS supports decimals! */ +a.cc{color:rgb(0,17,136,.3)} /* compact with commas */ +a.cs{color:rgb(0 17 136/.3)} /* compact with spaces and slash */ +a.h3 { color: #018; } /* same color but in hex (3char) */ +a.h6 { color: #001188; } /* same color but in hex (6char) */ +a.h8 { color: #001188ff; } /* same color but in hex (8char) */ +a.hlcs { color:hsl(232.5 100% 27%); } /* same color but hsl with spaces */ +a.hslc { color:hsl(232.5, 100%, 27%) } /* same color but hsl, commas */ +a.hlca { color:hsl(232.5, 100%, 27%, 100%); } /* same color but hsla */ +a.hwb { color:hwb(232.5, 0%, 46.5%); } /* same color but hwb */ + +/* out of bounds but accepted by browsers */ +a.rgboob { color:rgb(0,17,256); } +a.rgbaoob1 { color:rgb(0,17,255, 1.1); } +a.rgbaoob2 { color:rgb(0,17,255, 2); } +a.hsloob101 { color:hsl(256, 200%, 27%); } + +/* are we rounding too much? these should be different */ +u.round1 { color: rgb(10.9% 123 122.5) } /* should be #1c7b7b */ +u.round2 { color: rgb(10.0% 123 123.4) } /* should be #1a7b7b */ +u.round3 { color: hsl(180 65% 29%) } /* should be #197a7a */ + +*::color1 { -x-: #359 } +*::color2 { -x-: #335599 } +*::color3 { -x-: rgba(144, 0, 0, .5) } +*::color4 { -x-: hsl(0, 100%, 50%) } + +/* + * #123, #456 + */ + +/* #123456 */ + +td { border: solid red 4px; } +td { border: solid #f00d 4px; } + +:root { --variable-test: rgb(1, 2, 3); } +button { color: yellow; background: var(--variable-test); } + +/* These are all the same color: */ +h1.hex { color: #7ab899; } +h1.num { color: hsl(150, 30%, 60%); } +h1.num { color: hsla(870, 30%, 60%, 1); } +h1.num { color: hsl(-210, 30%, 60%); } +h1.deg { color: hsl(150deg, 30%, 60%); } +h1.grad { color: hsl(166.7grad, 30%, 60%); } +h1.turn { color: hsl(0.41667turn, 30%, 60%); } +h1.tuac { color: hsl(0.41667turn, 30%, 60%, 1); } +h1.tuas { color: hsl(0.41667turn 30% 60% / 1); } + +/* hwb(), hue/whiteness/blackness + * https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hwb + * These are all the same color: */ +a.a { color: hwb(90 10% 10%); } +a.b { color: hwb(90 10% 10% / 0.5); } +a.c { color: hwb(90deg 10% 10%); } +a.d { color: hwb(1.5708rad 10% 10%); } +a.e { color: hwb(0.25turn 10% 10% / 50%); } + +.do_color_me::after { color: rgb(123, 234, 0) } +q.tan { border-color: tan; } +q.white { border-color: white; } +q.abcdef { border-color: #abcdef; } + +/* these should NOT be colored */ +.dont_color_me::after { content: "color: rgb(123, 234, 0)" } +p.tan { color: inherit; width: calc(100px * tan(45deg)); } +p.white { color: currentColor; white-space: inherit; } +#abcdef { border-color: unset; } +a.cs1 { color: rgb(0, 17, 136 / 100%); } /* you can't mix commas and slash */ +a.cs2 { color: rgb(0 17 136, 100%); } /* you can't mix spaces and commas */ +a.cs3 { color: rgb(0 17 136 1); } /* alpha with spaces must use slash */ +a.cs4 { color: rgb(0 17 136 100%); } /* alpha with spaces must use slash */ +a.cs4 { color: rgb(0turn 17 136); } /* angular units aren't allowed in rgb */ +h1.nonpct1 { color: hsl(150%, 30%, 60%); } +h1.nonpct1 { color: hsl(150, 30, 60%); } +h1.nonpct2 { color: hsl(150, 30%, 60); } +h1.nonpct3 { color: hsl(150, 30, 60); } +a.a { color: hwb(90% 10% 10%); } +a.a { color: hwb(90 10 10%); } +a.a { color: hwb(90 10% 10); } +a.h5 { color: #00118; } +a.h7 { color: #001188f; } +a.h9 { color: #001188fff; } +