Add color highlighting to css-mode

Bug#25525
* lisp/textmodes/css-mode.el (css--color-map): New constant.
(css-value-class-alist): Use css--color-map.
(css--number-regexp, css--percent-regexp)
(css--number-or-percent-regexp, css--angle-regexp): New constants.
(css--color-skip-blanks, css--rgb-color, css--hsl-color): New
functions.
(css--colors-regexp): New constant.
(css--hex-color, css--named-color, css--compute-color)
(css--contrasty-color, css--fontify-colors)
(css--fontify-region): New functions.
(css-mode): Set font-lock-fontify-region-function.
(css-mode-syntax-table): Set syntax on more characters.
(css-fontify-colors): New defcustom.
(scss-mode-syntax-table): Define syntax for ?$ and ?%.
* test/lisp/textmodes/css-mode-tests.el (css-test-property-values):
Update.
(css-test-rgb-parser, css-test-hsl-parser)
(css-test-named-color): New tests.
* etc/NEWS: Add entry.
This commit is contained in:
Tom Tromey 2017-01-25 00:53:49 -07:00
parent a26e33a1a7
commit f0708fc5e4
3 changed files with 432 additions and 36 deletions

View file

@ -735,6 +735,11 @@ pseudo-element, with the default being guessed from context). By
default the information is looked up on the Mozilla Developer Network,
but this can be customized using 'css-lookup-url-format'.
---
*** CSS colors are fontified using the color they represent as the
background. For instance, #ff0000 would be fontified with a red
background.
+++
** Emacs now supports character name escape sequences in character and
string literals. The syntax variants \N{character name} and

View file

@ -33,6 +33,8 @@
;;; Code:
(require 'eww)
(require 'cl-lib)
(require 'color)
(require 'seq)
(require 'sgml-mode)
(require 'smie)
@ -487,8 +489,157 @@ further value candidates, since that list would be infinite.")
(mapcar #'car css-property-alist)
"Identifiers for properties.")
(defconst css--color-map
'(("black" . "#000000")
("silver" . "#c0c0c0")
("gray" . "#808080")
("white" . "#ffffff")
("maroon" . "#800000")
("red" . "#ff0000")
("purple" . "#800080")
("fuchsia" . "#ff00ff")
("green" . "#008000")
("lime" . "#00ff00")
("olive" . "#808000")
("yellow" . "#ffff00")
("navy" . "#000080")
("blue" . "#0000ff")
("teal" . "#008080")
("aqua" . "#00ffff")
("orange" . "#ffa500")
("aliceblue" . "#f0f8ff")
("antiquewhite" . "#faebd7")
("aquamarine" . "#7fffd4")
("azure" . "#f0ffff")
("beige" . "#f5f5dc")
("bisque" . "#ffe4c4")
("blanchedalmond" . "#ffebcd")
("blueviolet" . "#8a2be2")
("brown" . "#a52a2a")
("burlywood" . "#deb887")
("cadetblue" . "#5f9ea0")
("chartreuse" . "#7fff00")
("chocolate" . "#d2691e")
("coral" . "#ff7f50")
("cornflowerblue" . "#6495ed")
("cornsilk" . "#fff8dc")
("crimson" . "#dc143c")
("darkblue" . "#00008b")
("darkcyan" . "#008b8b")
("darkgoldenrod" . "#b8860b")
("darkgray" . "#a9a9a9")
("darkgreen" . "#006400")
("darkgrey" . "#a9a9a9")
("darkkhaki" . "#bdb76b")
("darkmagenta" . "#8b008b")
("darkolivegreen" . "#556b2f")
("darkorange" . "#ff8c00")
("darkorchid" . "#9932cc")
("darkred" . "#8b0000")
("darksalmon" . "#e9967a")
("darkseagreen" . "#8fbc8f")
("darkslateblue" . "#483d8b")
("darkslategray" . "#2f4f4f")
("darkslategrey" . "#2f4f4f")
("darkturquoise" . "#00ced1")
("darkviolet" . "#9400d3")
("deeppink" . "#ff1493")
("deepskyblue" . "#00bfff")
("dimgray" . "#696969")
("dimgrey" . "#696969")
("dodgerblue" . "#1e90ff")
("firebrick" . "#b22222")
("floralwhite" . "#fffaf0")
("forestgreen" . "#228b22")
("gainsboro" . "#dcdcdc")
("ghostwhite" . "#f8f8ff")
("gold" . "#ffd700")
("goldenrod" . "#daa520")
("greenyellow" . "#adff2f")
("grey" . "#808080")
("honeydew" . "#f0fff0")
("hotpink" . "#ff69b4")
("indianred" . "#cd5c5c")
("indigo" . "#4b0082")
("ivory" . "#fffff0")
("khaki" . "#f0e68c")
("lavender" . "#e6e6fa")
("lavenderblush" . "#fff0f5")
("lawngreen" . "#7cfc00")
("lemonchiffon" . "#fffacd")
("lightblue" . "#add8e6")
("lightcoral" . "#f08080")
("lightcyan" . "#e0ffff")
("lightgoldenrodyellow" . "#fafad2")
("lightgray" . "#d3d3d3")
("lightgreen" . "#90ee90")
("lightgrey" . "#d3d3d3")
("lightpink" . "#ffb6c1")
("lightsalmon" . "#ffa07a")
("lightseagreen" . "#20b2aa")
("lightskyblue" . "#87cefa")
("lightslategray" . "#778899")
("lightslategrey" . "#778899")
("lightsteelblue" . "#b0c4de")
("lightyellow" . "#ffffe0")
("limegreen" . "#32cd32")
("linen" . "#faf0e6")
("mediumaquamarine" . "#66cdaa")
("mediumblue" . "#0000cd")
("mediumorchid" . "#ba55d3")
("mediumpurple" . "#9370db")
("mediumseagreen" . "#3cb371")
("mediumslateblue" . "#7b68ee")
("mediumspringgreen" . "#00fa9a")
("mediumturquoise" . "#48d1cc")
("mediumvioletred" . "#c71585")
("midnightblue" . "#191970")
("mintcream" . "#f5fffa")
("mistyrose" . "#ffe4e1")
("moccasin" . "#ffe4b5")
("navajowhite" . "#ffdead")
("oldlace" . "#fdf5e6")
("olivedrab" . "#6b8e23")
("orangered" . "#ff4500")
("orchid" . "#da70d6")
("palegoldenrod" . "#eee8aa")
("palegreen" . "#98fb98")
("paleturquoise" . "#afeeee")
("palevioletred" . "#db7093")
("papayawhip" . "#ffefd5")
("peachpuff" . "#ffdab9")
("peru" . "#cd853f")
("pink" . "#ffc0cb")
("plum" . "#dda0dd")
("powderblue" . "#b0e0e6")
("rosybrown" . "#bc8f8f")
("royalblue" . "#4169e1")
("saddlebrown" . "#8b4513")
("salmon" . "#fa8072")
("sandybrown" . "#f4a460")
("seagreen" . "#2e8b57")
("seashell" . "#fff5ee")
("sienna" . "#a0522d")
("skyblue" . "#87ceeb")
("slateblue" . "#6a5acd")
("slategray" . "#708090")
("slategrey" . "#708090")
("snow" . "#fffafa")
("springgreen" . "#00ff7f")
("steelblue" . "#4682b4")
("tan" . "#d2b48c")
("thistle" . "#d8bfd8")
("tomato" . "#ff6347")
("turquoise" . "#40e0d0")
("violet" . "#ee82ee")
("wheat" . "#f5deb3")
("whitesmoke" . "#f5f5f5")
("yellowgreen" . "#9acd32")
("rebeccapurple" . "#663399"))
"Map CSS named colors to their hex RGB value.")
(defconst css-value-class-alist
'((absolute-size
`((absolute-size
"xx-small" "x-small" "small" "medium" "large" "x-large"
"xx-large")
(alphavalue number)
@ -550,36 +701,7 @@ further value candidates, since that list would be infinite.")
(line-width length "thin" "medium" "thick")
(linear-gradient "linear-gradient()")
(margin-width "auto" length percentage)
(named-color
"aliceblue" "antiquewhite" "aqua" "aquamarine" "azure" "beige"
"bisque" "black" "blanchedalmond" "blue" "blueviolet" "brown"
"burlywood" "cadetblue" "chartreuse" "chocolate" "coral"
"cornflowerblue" "cornsilk" "crimson" "cyan" "darkblue"
"darkcyan" "darkgoldenrod" "darkgray" "darkgreen" "darkkhaki"
"darkmagenta" "darkolivegreen" "darkorange" "darkorchid"
"darkred" "darksalmon" "darkseagreen" "darkslateblue"
"darkslategray" "darkturquoise" "darkviolet" "deeppink"
"deepskyblue" "dimgray" "dodgerblue" "firebrick" "floralwhite"
"forestgreen" "fuchsia" "gainsboro" "ghostwhite" "gold"
"goldenrod" "gray" "green" "greenyellow" "honeydew" "hotpink"
"indianred" "indigo" "ivory" "khaki" "lavender" "lavenderblush"
"lawngreen" "lemonchiffon" "lightblue" "lightcoral" "lightcyan"
"lightgoldenrodyellow" "lightgray" "lightgreen" "lightpink"
"lightsalmon" "lightseagreen" "lightskyblue" "lightslategray"
"lightsteelblue" "lightyellow" "lime" "limegreen" "linen"
"magenta" "maroon" "mediumaquamarine" "mediumblue" "mediumorchid"
"mediumpurple" "mediumseagreen" "mediumslateblue"
"mediumspringgreen" "mediumturquoise" "mediumvioletred"
"midnightblue" "mintcream" "mistyrose" "moccasin" "navajowhite"
"navy" "oldlace" "olive" "olivedrab" "orange" "orangered"
"orchid" "palegoldenrod" "palegreen" "paleturquoise"
"palevioletred" "papayawhip" "peachpuff" "peru" "pink" "plum"
"powderblue" "purple" "rebeccapurple" "red" "rosybrown"
"royalblue" "saddlebrown" "salmon" "sandybrown" "seagreen"
"seashell" "sienna" "silver" "skyblue" "slateblue" "slategray"
"snow" "springgreen" "steelblue" "tan" "teal" "thistle" "tomato"
"turquoise" "violet" "wheat" "white" "whitesmoke" "yellow"
"yellowgreen")
(named-color . ,(mapcar #'car css--color-map))
(number "calc()")
(numeric-figure-values "lining-nums" "oldstyle-nums")
(numeric-fraction-values "diagonal-fractions" "stacked-fractions")
@ -663,11 +785,23 @@ cannot be completed sensibly: `custom-ident',
(modify-syntax-entry ?\[ "(]" st)
(modify-syntax-entry ?\] ")[" st)
;; Special chars that sometimes come at the beginning of words.
(modify-syntax-entry ?@ "'" st)
;; (modify-syntax-entry ?: "'" st)
(modify-syntax-entry ?# "'" st)
;; We'll treat them as symbol constituents.
(modify-syntax-entry ?@ "_" st)
(modify-syntax-entry ?# "_" st)
(modify-syntax-entry ?. "_" st)
;; Distinction between words and symbols.
(modify-syntax-entry ?- "_" st)
(modify-syntax-entry ?! "." st)
(modify-syntax-entry ?$ "." st)
(modify-syntax-entry ?% "." st)
(modify-syntax-entry ?& "." st)
(modify-syntax-entry ?+ "." st)
(modify-syntax-entry ?, "." st)
(modify-syntax-entry ?< "." st)
(modify-syntax-entry ?> "." st)
(modify-syntax-entry ?= "." st)
(modify-syntax-entry ?? "." st)
st))
(defvar css-mode-map
@ -782,6 +916,217 @@ cannot be completed sensibly: `custom-ident',
(defvar css-font-lock-defaults
'(css-font-lock-keywords nil t))
(defconst css--number-regexp
"\\(\\(?:[0-9]*\\.[0-9]+\\(?:[eE][0-9]+\\)?\\)\\|[0-9]+\\)"
"A regular expression matching a CSS number.")
(defconst css--percent-regexp "\\([0-9]+\\)%"
"A regular expression matching a CSS percentage.")
(defconst css--number-or-percent-regexp
(concat "\\(?:" css--percent-regexp "\\)\\|\\(?:" css--number-regexp "\\)")
"A regular expression matching a CSS number or a CSS percentage.")
(defconst css--angle-regexp
(concat css--number-regexp
(regexp-opt '("deg" "grad" "rad" "turn") t)
"?")
"A regular expression matching a CSS angle.")
(defun css--color-skip-blanks ()
"Skip blanks and comments."
(while (forward-comment 1)))
(cl-defun css--rgb-color ()
"Parse a CSS rgb() or rgba() color.
Point should be just after the open paren.
Returns a hex RGB color, or nil if the color could not be recognized.
This recognizes CSS-color-4 extensions."
(let ((result '())
(iter 0))
(while (< iter 4)
(css--color-skip-blanks)
(unless (looking-at css--number-or-percent-regexp)
(cl-return-from css--rgb-color nil))
(let* ((is-percent (match-beginning 1))
(str (match-string (if is-percent 1 2)))
(number (string-to-number str)))
(when is-percent
(setq number (* 255 (/ number 100.0))))
;; Don't push the alpha.
(when (< iter 3)
(push (min (max 0 (truncate number)) 255) result))
(goto-char (match-end 0))
(css--color-skip-blanks)
(cl-incf iter)
;; Accept a superset of the CSS syntax since I'm feeling lazy.
(when (and (= (skip-chars-forward ",/") 0)
(= iter 3))
;; The alpha is optional.
(cl-incf iter))
(css--color-skip-blanks)))
(when (looking-at ")")
(forward-char)
(apply #'format "#%02x%02x%02x" (nreverse result)))))
(cl-defun css--hsl-color ()
"Parse a CSS hsl() or hsla() color.
Point should be just after the open paren.
Returns a hex RGB color, or nil if the color could not be recognized.
This recognizes CSS-color-4 extensions."
(let ((result '()))
;; First parse the hue.
(css--color-skip-blanks)
(unless (looking-at css--angle-regexp)
(cl-return-from css--hsl-color nil))
(let ((hue (string-to-number (match-string 1)))
(unit (match-string 2)))
(goto-char (match-end 0))
;; Note that here "turn" is just passed through.
(cond
((or (not unit) (equal unit "deg"))
;; Degrees.
(setq hue (/ hue 360.0)))
((equal unit "grad")
(setq hue (/ hue 400.0)))
((equal unit "rad")
(setq hue (/ hue (* 2 float-pi)))))
(push (mod hue 1.0) result))
(dotimes (_ 2)
(skip-chars-forward ",")
(css--color-skip-blanks)
(unless (looking-at css--percent-regexp)
(cl-return-from css--hsl-color nil))
(let ((number (string-to-number (match-string 1))))
(setq number (/ number 100.0))
(push (min (max number 0.0) 1.0) result)
(goto-char (match-end 0))
(css--color-skip-blanks)))
(css--color-skip-blanks)
;; Accept a superset of the CSS syntax since I'm feeling lazy.
(when (> (skip-chars-forward ",/") 0)
(css--color-skip-blanks)
(unless (looking-at css--number-or-percent-regexp)
(cl-return-from css--hsl-color nil))
(goto-char (match-end 0))
(css--color-skip-blanks))
(when (looking-at ")")
(forward-char)
(apply #'color-rgb-to-hex
(nconc (apply #'color-hsl-to-rgb (nreverse result)) '(2))))))
(defconst css--colors-regexp
(concat
;; Named colors.
(regexp-opt (mapcar #'car css--color-map) 'symbols)
"\\|"
;; Short hex. css-color-4 adds alpha.
"\\(#[0-9a-fA-F]\\{3,4\\}\\b\\)"
"\\|"
;; Long hex. css-color-4 adds alpha.
"\\(#\\(?:[0-9a-fA-F][0-9a-fA-F]\\)\\{3,4\\}\\b\\)"
"\\|"
;; RGB.
"\\(\\_<rgba?(\\)"
"\\|"
;; HSL.
"\\(\\_<hsla?(\\)")
"A regular expression that matches the start of a CSS color.")
(defun css--hex-color (str)
"Convert a CSS hex color to an Emacs hex color.
STR is the incoming CSS hex color.
This function simply drops any transparency."
;; Either #RGB or #RRGGBB, drop the "A" or "AA".
(if (> (length str) 4)
(substring str 0 7)
(substring str 0 4)))
(defun css--named-color (start-point str)
"Check whether STR, seen at point, is CSS named color.
Returns STR if it is a valid color. Special care is taken
to exclude some SCSS contructs."
(when-let ((color (assoc str css--color-map)))
(save-excursion
(goto-char start-point)
(forward-comment (- (point)))
(skip-chars-backward "@[:alpha:]")
(unless (looking-at-p "@\\(mixin\\|include\\)")
(cdr color)))))
(defun css--compute-color (start-point match)
"Return the CSS color at point.
Point should be just after the start of a CSS color, as recognized
by `css--colors-regexp'. START-POINT is the start of the color,
and MATCH is the string matched by the regexp.
This function will either return the color, as a hex RGB string;
or `nil' if no color could be recognized. When this function
returns, point will be at the end of the recognized color."
(cond
((eq (aref match 0) ?#)
(css--hex-color match))
((member match '("rgb(" "rgba("))
(css--rgb-color))
((member match '("hsl(" "hsla("))
(css--hsl-color))
;; Evaluate to the color if the name is found.
((css--named-color start-point match))))
(defun css--contrasty-color (name)
"Return a color that contrasts with NAME.
NAME is of any form accepted by `color-distance'.
The returned color will be usable by Emacs and will contrast
with NAME; in particular so that if NAME is used as a background
color, the returned color can be used as the foreground and still
be readable."
;; See bug#25525 for a discussion of this.
(if (> (color-distance name "black") 292485)
"black" "white"))
(defcustom css-fontify-colors t
"Whether CSS colors should be fontified using the color as the background.
When non-`nil', a text representing CSS color will be fontified
such that its background is the color itself. E.g., #ff0000 will
be fontified with a red background."
:version "26.1"
:group 'css
:type 'boolean
:safe 'booleanp)
(defun css--fontify-region (start end &optional loudly)
"Fontify a CSS buffer between START and END.
START and END are buffer positions."
(let ((extended-region (font-lock-default-fontify-region start end loudly)))
(when css-fontify-colors
(when (and (consp extended-region)
(eq (car extended-region) 'jit-lock-bounds))
(setq start (cadr extended-region))
(setq end (cddr extended-region)))
(save-excursion
(let ((case-fold-search t))
(goto-char start)
(while (re-search-forward css--colors-regexp end t)
;; Skip comments and strings.
(unless (nth 8 (syntax-ppss))
(let* ((start (match-beginning 0))
(color (css--compute-color start (match-string 0))))
(when color
(with-silent-modifications
;; Use the color as the background, to make it more
;; clear. Use a contrasting color as the foreground,
;; to make it readable. Finally, have a small box
;; using the existing foreground color, to make sure
;; it stands out a bit from any other text; in
;; particular this is nice when the color matches the
;; buffer's background color.
(add-text-properties
start (point)
(list 'face (list :background color
:foreground (css--contrasty-color color)
:box '(:line-width -1))))))))))))
extended-region))
(defcustom css-indent-offset 4
"Basic size of one indentation step."
:version "22.2"
@ -1048,6 +1393,7 @@ pseudo-elements, pseudo-classes, at-rules, and bang-rules."
:backward-token #'css-smie--backward-token)
(setq-local electric-indent-chars
(append css-electric-keys electric-indent-chars))
(setq-local font-lock-fontify-region-function #'css--fontify-region)
(add-hook 'completion-at-point-functions
#'css-completion-at-point nil 'local))
@ -1160,7 +1506,8 @@ pseudo-elements, pseudo-classes, at-rules, and bang-rules."
(modify-syntax-entry ?/ ". 124" st)
(modify-syntax-entry ?\n ">" st)
;; Variable names are prefixed by $.
(modify-syntax-entry ?$ "'" st)
(modify-syntax-entry ?$ "_" st)
(modify-syntax-entry ?% "_" st)
st))
(defun scss-font-lock-keywords ()

View file

@ -58,7 +58,7 @@
;; Check that the `color' property doesn't cause infinite recursion
;; because it refers to the value class of the same name.
(should (= (length (css--property-values "color")) 147)))
(should (= (length (css--property-values "color")) 152)))
(ert-deftest css-test-property-value-cache ()
"Test that `css--property-value-cache' is in use."
@ -234,5 +234,49 @@
(save-excursion (insert (nth 1 item)))
(should (equal (nth 2 item) (css--mdn-find-symbol))))))
(ert-deftest css-test-rgb-parser ()
(with-temp-buffer
(css-mode)
(dolist (input '("255, 0, 127"
"255, /* comment */ 0, 127"
"255 0 127"
"255, 0, 127, 0.75"
"255 0 127 / 0.75"
"100%, 0%, 50%"
"100%, 0%, 50%, 0.115"
"100% 0% 50%"
"100% 0% 50% / 0.115"))
(erase-buffer)
(save-excursion
(insert input ")"))
(should (equal (css--rgb-color) "#ff007f")))))
(ert-deftest css-test-hsl-parser ()
(with-temp-buffer
(css-mode)
(dolist (input '("0, 100%, 50%"
"0 100% 50%"
"0 /* two */ /* comments */100% 50%"
"0, 100%, 50%, 0.75"
"0 100% 50% / 0.75"
"0deg 100% 50%"
"360deg 100% 50%"
"0rad, 100%, 50%, 0.115"
"0grad, 100%, 50%, 0.115"
"1turn 100% 50% / 0.115"))
(erase-buffer)
(save-excursion
(insert input ")"))
(should (equal (css--hsl-color) "#ff0000")))))
(ert-deftest css-test-named-color ()
(dolist (text '("@mixin black" "@include black"))
(with-temp-buffer
(insert text)
(should-not (css--named-color (save-excursion
(backward-word)
(point))
"black")))))
(provide 'css-mode-tests)
;;; css-mode-tests.el ends here