aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimen Heggestøyl2017-12-16 09:49:54 +0100
committerSimen Heggestøyl2017-12-17 10:26:04 +0100
commitbd9e8b31a1a38a2ffa5c2ff5e805a42ffccc36ec (patch)
tree441203fb603da9188c2b96171abafbbffe0c8bac
parentac0d6c06b805b8f05a854a69639531bf737fea3f (diff)
downloademacs-bd9e8b31a1a38a2ffa5c2ff5e805a42ffccc36ec.tar.gz
emacs-bd9e8b31a1a38a2ffa5c2ff5e805a42ffccc36ec.zip
Add command for cycling between CSS color formats
* lisp/textmodes/css-mode.el (css-mode-map): Add keybinding for 'css-cycle-color-format'. (css--rgb-color): Add support for extracting alpha component. (css--hex-alpha, css--color-to-4-dpc, css--named-color-to-hex) (css--format-rgba-alpha, css--hex-to-rgb) (css--rgb-to-named-color-or-hex): New functions. (css-cycle-color-format): New command for cycling between color formats. * test/lisp/textmodes/css-mode-tests.el (css-test-color-to-4-dpc): (css-test-named-color-to-hex, css-test-format-rgba-alpha) (css-test-hex-to-rgb, css-test-rgb-to-named-color-or-hex) (css-test-cycle-color-format, css-test-hex-alpha): New tests for the changes mentioned above. * etc/NEWS: Mention the new command.
-rw-r--r--etc/NEWS7
-rw-r--r--lisp/textmodes/css-mode.el133
-rw-r--r--test/lisp/textmodes/css-mode-tests.el73
3 files changed, 207 insertions, 6 deletions
diff --git a/etc/NEWS b/etc/NEWS
index bec7753d192..1382f96a374 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -77,6 +77,13 @@ whether '"' is also replaced in 'electric-quote-mode'. If non-nil,
77 77
78* Changes in Specialized Modes and Packages in Emacs 27.1 78* Changes in Specialized Modes and Packages in Emacs 27.1
79 79
80** CSS mode
81
82---
83*** A new command 'css-cycle-color-format' for cycling between color
84formats (e.g. "black" => "#000000" => "rgb(0, 0, 0)") has been added,
85bound to 'C-c C-f'.
86
80** Dired 87** Dired
81 88
82+++ 89+++
diff --git a/lisp/textmodes/css-mode.el b/lisp/textmodes/css-mode.el
index b0e66d397f0..f0988827c31 100644
--- a/lisp/textmodes/css-mode.el
+++ b/lisp/textmodes/css-mode.el
@@ -32,12 +32,13 @@
32 32
33;;; Code: 33;;; Code:
34 34
35(require 'eww)
36(require 'cl-lib) 35(require 'cl-lib)
37(require 'color) 36(require 'color)
37(require 'eww)
38(require 'seq) 38(require 'seq)
39(require 'sgml-mode) 39(require 'sgml-mode)
40(require 'smie) 40(require 'smie)
41(require 'thingatpt)
41(eval-when-compile (require 'subr-x)) 42(eval-when-compile (require 'subr-x))
42 43
43(defgroup css nil 44(defgroup css nil
@@ -806,6 +807,7 @@ cannot be completed sensibly: `custom-ident',
806(defvar css-mode-map 807(defvar css-mode-map
807 (let ((map (make-sparse-keymap))) 808 (let ((map (make-sparse-keymap)))
808 (define-key map [remap info-lookup-symbol] 'css-lookup-symbol) 809 (define-key map [remap info-lookup-symbol] 'css-lookup-symbol)
810 (define-key map "\C-c\C-f" 'css-cycle-color-format)
809 map) 811 map)
810 "Keymap used in `css-mode'.") 812 "Keymap used in `css-mode'.")
811 813
@@ -936,11 +938,13 @@ cannot be completed sensibly: `custom-ident',
936 "Skip blanks and comments." 938 "Skip blanks and comments."
937 (while (forward-comment 1))) 939 (while (forward-comment 1)))
938 940
939(cl-defun css--rgb-color () 941(cl-defun css--rgb-color (&optional include-alpha)
940 "Parse a CSS rgb() or rgba() color. 942 "Parse a CSS rgb() or rgba() color.
941Point should be just after the open paren. 943Point should be just after the open paren.
942Returns a hex RGB color, or nil if the color could not be recognized. 944Returns a hex RGB color, or nil if the color could not be recognized.
943This recognizes CSS-color-4 extensions." 945This recognizes CSS-color-4 extensions.
946When INCLUDE-ALPHA is non-nil, the alpha component is included in
947the returned hex string."
944 (let ((result '()) 948 (let ((result '())
945 (iter 0)) 949 (iter 0))
946 (while (< iter 4) 950 (while (< iter 4)
@@ -952,8 +956,8 @@ This recognizes CSS-color-4 extensions."
952 (number (string-to-number str))) 956 (number (string-to-number str)))
953 (when is-percent 957 (when is-percent
954 (setq number (* 255 (/ number 100.0)))) 958 (setq number (* 255 (/ number 100.0))))
955 ;; Don't push the alpha. 959 (if (and include-alpha (= iter 3))
956 (when (< iter 3) 960 (push (round (* number 255)) result)
957 (push (min (max 0 (truncate number)) 255) result)) 961 (push (min (max 0 (truncate number)) 255) result))
958 (goto-char (match-end 0)) 962 (goto-char (match-end 0))
959 (css--color-skip-blanks) 963 (css--color-skip-blanks)
@@ -966,7 +970,11 @@ This recognizes CSS-color-4 extensions."
966 (css--color-skip-blanks))) 970 (css--color-skip-blanks)))
967 (when (looking-at ")") 971 (when (looking-at ")")
968 (forward-char) 972 (forward-char)
969 (apply #'format "#%02x%02x%02x" (nreverse result))))) 973 (apply #'format
974 (if (and include-alpha (= (length result) 4))
975 "#%02x%02x%02x%02x"
976 "#%02x%02x%02x")
977 (nreverse result)))))
970 978
971(cl-defun css--hsl-color () 979(cl-defun css--hsl-color ()
972 "Parse a CSS hsl() or hsla() color. 980 "Parse a CSS hsl() or hsla() color.
@@ -1039,6 +1047,14 @@ This function simply drops any transparency."
1039 ;; Either #RGB or #RRGGBB, drop the "A" or "AA". 1047 ;; Either #RGB or #RRGGBB, drop the "A" or "AA".
1040 (substring str 0 (if (> (length str) 5) 7 4))) 1048 (substring str 0 (if (> (length str) 5) 7 4)))
1041 1049
1050(defun css--hex-alpha (hex)
1051 "Return the alpha component of CSS color HEX.
1052HEX can either be in the #RGBA or #RRGGBBAA format. Return nil
1053if the color doesn't have an alpha component."
1054 (cl-case (length hex)
1055 (5 (string (elt hex 4)))
1056 (9 (substring hex 7 9))))
1057
1042(defun css--named-color (start-point str) 1058(defun css--named-color (start-point str)
1043 "Check whether STR, seen at point, is CSS named color. 1059 "Check whether STR, seen at point, is CSS named color.
1044Returns STR if it is a valid color. Special care is taken 1060Returns STR if it is a valid color. Special care is taken
@@ -1381,6 +1397,111 @@ tags, classes and IDs."
1381 (progn (insert ": ;") 1397 (progn (insert ": ;")
1382 (forward-char -1)))))))))) 1398 (forward-char -1))))))))))
1383 1399
1400(defun css--color-to-4-dpc (hex)
1401 "Convert the CSS color HEX to four digits per component.
1402CSS colors use one or two digits per component for RGB hex
1403values. Convert the given color to four digits per component.
1404
1405Note that this function handles CSS colors specifically, and
1406should not be mixed with those in color.el."
1407 (let ((six-digits (= (length hex) 7)))
1408 (apply
1409 #'concat
1410 `("#"
1411 ,@(seq-mapcat
1412 (apply-partially #'make-list (if six-digits 2 4))
1413 (seq-partition (seq-drop hex 1) (if six-digits 2 1)))))))
1414
1415(defun css--named-color-to-hex ()
1416 "Convert named CSS color at point to hex format.
1417Return non-nil if a conversion was made.
1418
1419Note that this function handles CSS colors specifically, and
1420should not be mixed with those in color.el."
1421 (save-excursion
1422 (unless (or (looking-at css--colors-regexp)
1423 (eq (char-before) ?#))
1424 (backward-word))
1425 (when (member (word-at-point) (mapcar #'car css--color-map))
1426 (looking-at css--colors-regexp)
1427 (let ((color (css--compute-color (point) (match-string 0))))
1428 (replace-match color))
1429 t)))
1430
1431(defun css--format-rgba-alpha (alpha)
1432 "Return ALPHA component formatted for use in rgba()."
1433 (let ((a (string-to-number (format "%.2f" alpha))))
1434 (if (or (= a 0)
1435 (= a 1))
1436 (format "%d" a)
1437 (string-remove-suffix "0" (number-to-string a)))))
1438
1439(defun css--hex-to-rgb ()
1440 "Convert CSS hex color at point to RGB format.
1441Return non-nil if a conversion was made.
1442
1443Note that this function handles CSS colors specifically, and
1444should not be mixed with those in color.el."
1445 (save-excursion
1446 (unless (or (eq (char-after) ?#)
1447 (eq (char-before) ?\())
1448 (backward-sexp))
1449 (when-let* ((hex (when (looking-at css--colors-regexp)
1450 (and (eq (elt (match-string 0) 0) ?#)
1451 (match-string 0))))
1452 (rgb (css--hex-color hex)))
1453 (seq-let (r g b)
1454 (mapcar (lambda (x) (round (* x 255)))
1455 (color-name-to-rgb (css--color-to-4-dpc rgb)))
1456 (replace-match
1457 (if-let* ((alpha (css--hex-alpha hex))
1458 (a (css--format-rgba-alpha
1459 (/ (string-to-number alpha 16)
1460 (float (expt 16 (length alpha)))))))
1461 (format "rgba(%d, %d, %d, %s)" r g b a)
1462 (format "rgb(%d, %d, %d)" r g b))
1463 t))
1464 t)))
1465
1466(defun css--rgb-to-named-color-or-hex ()
1467 "Convert CSS RGB color at point to a named color or hex format.
1468Convert to a named color if the color at point has a name, else
1469convert to hex format. Return non-nil if a conversion was made.
1470
1471Note that this function handles CSS colors specifically, and
1472should not be mixed with those in color.el."
1473 (save-excursion
1474 (when-let* ((open-paren-pos (nth 1 (syntax-ppss))))
1475 (when (save-excursion
1476 (goto-char open-paren-pos)
1477 (looking-back "rgba?" (- (point) 4)))
1478 (goto-char (nth 1 (syntax-ppss)))))
1479 (when (eq (char-before) ?\))
1480 (backward-sexp))
1481 (skip-chars-backward "rgba")
1482 (when (looking-at css--colors-regexp)
1483 (let* ((start (match-end 0))
1484 (color (save-excursion
1485 (goto-char start)
1486 (css--rgb-color t))))
1487 (when color
1488 (kill-sexp)
1489 (kill-sexp)
1490 (let ((named-color (seq-find (lambda (x) (equal (cdr x) color))
1491 css--color-map)))
1492 (insert (if named-color (car named-color) color)))
1493 t)))))
1494
1495(defun css-cycle-color-format ()
1496 "Cycle the color at point between different CSS color formats.
1497Supported formats are by name (if possible), hexadecimal, and
1498rgb()/rgba()."
1499 (interactive)
1500 (or (css--named-color-to-hex)
1501 (css--hex-to-rgb)
1502 (css--rgb-to-named-color-or-hex)
1503 (message "It doesn't look like a color at point")))
1504
1384;;;###autoload 1505;;;###autoload
1385(define-derived-mode css-mode prog-mode "CSS" 1506(define-derived-mode css-mode prog-mode "CSS"
1386 "Major mode to edit Cascading Style Sheets (CSS). 1507 "Major mode to edit Cascading Style Sheets (CSS).
diff --git a/test/lisp/textmodes/css-mode-tests.el b/test/lisp/textmodes/css-mode-tests.el
index 1e58751f140..2be57726256 100644
--- a/test/lisp/textmodes/css-mode-tests.el
+++ b/test/lisp/textmodes/css-mode-tests.el
@@ -244,6 +244,73 @@
244 (should (member "body" completions)) 244 (should (member "body" completions))
245 (should-not (member "article" completions))))) 245 (should-not (member "article" completions)))))
246 246
247(ert-deftest css-test-color-to-4-dpc ()
248 (should (equal (css--color-to-4-dpc "#ffffff")
249 (css--color-to-4-dpc "#fff")))
250 (should (equal (css--color-to-4-dpc "#aabbcc")
251 (css--color-to-4-dpc "#abc")))
252 (should (equal (css--color-to-4-dpc "#fab")
253 "#ffffaaaabbbb"))
254 (should (equal (css--color-to-4-dpc "#fafbfc")
255 "#fafafbfbfcfc")))
256
257(ert-deftest css-test-named-color-to-hex ()
258 (dolist (item '(("black" "#000000")
259 ("white" "#ffffff")
260 ("salmon" "#fa8072")))
261 (with-temp-buffer
262 (css-mode)
263 (insert (nth 0 item))
264 (css--named-color-to-hex)
265 (should (equal (buffer-string) (nth 1 item))))))
266
267(ert-deftest css-test-format-rgba-alpha ()
268 (should (equal (css--format-rgba-alpha 0) "0"))
269 (should (equal (css--format-rgba-alpha 0.0) "0"))
270 (should (equal (css--format-rgba-alpha 0.00001) "0"))
271 (should (equal (css--format-rgba-alpha 1) "1"))
272 (should (equal (css--format-rgba-alpha 1.0) "1"))
273 (should (equal (css--format-rgba-alpha 1.00001) "1"))
274 (should (equal (css--format-rgba-alpha 0.10000) "0.1"))
275 (should (equal (css--format-rgba-alpha 0.100001) "0.1"))
276 (should (equal (css--format-rgba-alpha 0.2524334) "0.25")))
277
278(ert-deftest css-test-hex-to-rgb ()
279 (dolist (item '(("#000" "rgb(0, 0, 0)")
280 ("#000000" "rgb(0, 0, 0)")
281 ("#fff" "rgb(255, 255, 255)")
282 ("#ffffff" "rgb(255, 255, 255)")
283 ("#ffffff80" "rgba(255, 255, 255, 0.5)")
284 ("#fff8" "rgba(255, 255, 255, 0.5)")))
285 (with-temp-buffer
286 (css-mode)
287 (insert (nth 0 item))
288 (css--hex-to-rgb)
289 (should (equal (buffer-string) (nth 1 item))))))
290
291(ert-deftest css-test-rgb-to-named-color-or-hex ()
292 (dolist (item '(("rgb(0, 0, 0)" "black")
293 ("rgb(255, 255, 255)" "white")
294 ("rgb(255, 255, 240)" "ivory")
295 ("rgb(18, 52, 86)" "#123456")
296 ("rgba(18, 52, 86, 0.5)" "#12345680")))
297 (with-temp-buffer
298 (css-mode)
299 (insert (nth 0 item))
300 (css--rgb-to-named-color-or-hex)
301 (should (equal (buffer-string) (nth 1 item))))))
302
303(ert-deftest css-test-cycle-color-format ()
304 (with-temp-buffer
305 (css-mode)
306 (insert "black")
307 (css-cycle-color-format)
308 (should (equal (buffer-string) "#000000"))
309 (css-cycle-color-format)
310 (should (equal (buffer-string) "rgb(0, 0, 0)"))
311 (css-cycle-color-format)
312 (should (equal (buffer-string) "black"))))
313
247(ert-deftest css-mdn-symbol-guessing () 314(ert-deftest css-mdn-symbol-guessing ()
248 (dolist (item '(("@med" "ia" "@media") 315 (dolist (item '(("@med" "ia" "@media")
249 ("@keyframes " "{" "@keyframes") 316 ("@keyframes " "{" "@keyframes")
@@ -301,6 +368,12 @@
301 (should (equal (css--hex-color "#aabbcc") "#aabbcc")) 368 (should (equal (css--hex-color "#aabbcc") "#aabbcc"))
302 (should (equal (css--hex-color "#aabbccdd") "#aabbcc"))) 369 (should (equal (css--hex-color "#aabbccdd") "#aabbcc")))
303 370
371(ert-deftest css-test-hex-alpha ()
372 (should (equal (css--hex-alpha "#abcd") "d"))
373 (should-not (css--hex-alpha "#abc"))
374 (should (equal (css--hex-alpha "#aabbccdd") "dd"))
375 (should-not (css--hex-alpha "#aabbcc")))
376
304(ert-deftest css-test-named-color () 377(ert-deftest css-test-named-color ()
305 (dolist (text '("@mixin black" "@include black")) 378 (dolist (text '("@mixin black" "@include black"))
306 (with-temp-buffer 379 (with-temp-buffer