diff options
| author | Simen Heggestøyl | 2017-12-16 09:49:54 +0100 |
|---|---|---|
| committer | Simen Heggestøyl | 2017-12-17 10:26:04 +0100 |
| commit | bd9e8b31a1a38a2ffa5c2ff5e805a42ffccc36ec (patch) | |
| tree | 441203fb603da9188c2b96171abafbbffe0c8bac | |
| parent | ac0d6c06b805b8f05a854a69639531bf737fea3f (diff) | |
| download | emacs-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/NEWS | 7 | ||||
| -rw-r--r-- | lisp/textmodes/css-mode.el | 133 | ||||
| -rw-r--r-- | test/lisp/textmodes/css-mode-tests.el | 73 |
3 files changed, 207 insertions, 6 deletions
| @@ -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 | ||
| 84 | formats (e.g. "black" => "#000000" => "rgb(0, 0, 0)") has been added, | ||
| 85 | bound 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. |
| 941 | Point should be just after the open paren. | 943 | Point should be just after the open paren. |
| 942 | Returns a hex RGB color, or nil if the color could not be recognized. | 944 | Returns a hex RGB color, or nil if the color could not be recognized. |
| 943 | This recognizes CSS-color-4 extensions." | 945 | This recognizes CSS-color-4 extensions. |
| 946 | When INCLUDE-ALPHA is non-nil, the alpha component is included in | ||
| 947 | the 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. | ||
| 1052 | HEX can either be in the #RGBA or #RRGGBBAA format. Return nil | ||
| 1053 | if 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. |
| 1044 | Returns STR if it is a valid color. Special care is taken | 1060 | Returns 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. | ||
| 1402 | CSS colors use one or two digits per component for RGB hex | ||
| 1403 | values. Convert the given color to four digits per component. | ||
| 1404 | |||
| 1405 | Note that this function handles CSS colors specifically, and | ||
| 1406 | should 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. | ||
| 1417 | Return non-nil if a conversion was made. | ||
| 1418 | |||
| 1419 | Note that this function handles CSS colors specifically, and | ||
| 1420 | should 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. | ||
| 1441 | Return non-nil if a conversion was made. | ||
| 1442 | |||
| 1443 | Note that this function handles CSS colors specifically, and | ||
| 1444 | should 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. | ||
| 1468 | Convert to a named color if the color at point has a name, else | ||
| 1469 | convert to hex format. Return non-nil if a conversion was made. | ||
| 1470 | |||
| 1471 | Note that this function handles CSS colors specifically, and | ||
| 1472 | should 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. | ||
| 1497 | Supported formats are by name (if possible), hexadecimal, and | ||
| 1498 | rgb()/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 |