From 0da7689b167dd92c0ac307122be0a742a10abfb4 Mon Sep 17 00:00:00 2001 From: Lars Ingebrigtsen Date: Sun, 15 May 2022 14:13:14 +0200 Subject: [PATCH] Don't freeze Emacs on colour codes in sccs-mode * lisp/textmodes/css-mode.el (css--font-lock-keywords): Don't freeze Emacs on #ffffff #ffffff, and be more strict in parsing selectors (bug#53203). --- lisp/textmodes/css-mode.el | 56 +++++++++------ .../css-mode-resources/css-selectors.txt | 56 +++++++++++++++ .../css-mode-resources/scss-selectors.txt | 6 ++ test/lisp/textmodes/css-mode-tests.el | 69 +++++++++++++++++++ 4 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 test/lisp/textmodes/css-mode-resources/css-selectors.txt create mode 100644 test/lisp/textmodes/css-mode-resources/scss-selectors.txt diff --git a/lisp/textmodes/css-mode.el b/lisp/textmodes/css-mode.el index ef7debc4bdb..99b4482dc53 100644 --- a/lisp/textmodes/css-mode.el +++ b/lisp/textmodes/css-mode.el @@ -928,6 +928,32 @@ cannot be completed sensibly: `custom-ident', (defface css-proprietary-property '((t :inherit (css-property italic))) "Face to use for vendor-specific properties.") +(defun css--selector-regexp (sassy) + (concat + "\\(?:" + (if (not sassy) + "[-_%*#.>[:alnum:]]+" + ;; Same as for non-sassy except we do want to allow { and } + ;; chars in selectors in the case of #{$foo} + ;; variable interpolation! + (concat "\\(?:[-_%*#.>[:alnum:]]*" scss--hash-re + "\\|[-_%*#.>[:alnum:]]+\\)")) + ;; Even though pseudo-elements should be prefixed by ::, a + ;; single colon is accepted for backward compatibility. + "\\(?:\\(:" (regexp-opt (append css-pseudo-class-ids + css-pseudo-element-ids) + t) + "\\|::" (regexp-opt css-pseudo-element-ids t) "\\)\\)?" + ;; Braces after selectors. + "\\(?:\\[[^]\n]+\\]\\)?" + ;; Parentheses after selectors. + "\\(?:([^)]+)\\)?" + ;; Main bit over. But perhaps just [target]? + "\\|\\[[^]\n]+\\]" + ;; :root, ::marker and the like. + "\\|::?[[:alnum:]]+\\(?:([^)]+)\\)?" + "\\)")) + (defun css--font-lock-keywords (&optional sassy) `((,(concat "!\\s-*" (regexp-opt css--bang-ids)) (0 font-lock-builtin-face)) @@ -948,28 +974,16 @@ cannot be completed sensibly: `custom-ident', ;; selector between [...] should simply not be highlighted. (,(concat "^[ \t]*\\(" - (if (not sassy) - ;; We don't allow / as first char, so as not to - ;; take a comment as the beginning of a selector. - "[^@/:{}() \t\n][^:{}()]*" - ;; Same as for non-sassy except we do want to allow { and } - ;; chars in selectors in the case of #{$foo} - ;; variable interpolation! - (concat "\\(?:" scss--hash-re - "\\|[^@/:{}() \t\n#]\\)" - "[^:{}()#]*\\(?:" scss--hash-re "[^:{}()#]*\\)*")) - ;; Even though pseudo-elements should be prefixed by ::, a - ;; single colon is accepted for backward compatibility. - "\\(?:\\(:" (regexp-opt (append css-pseudo-class-ids - css-pseudo-element-ids) - t) - "\\|::" (regexp-opt css-pseudo-element-ids t) "\\)" - "\\(?:([^)]+)\\)?" - (if (not sassy) - "[^:{}()\n]*" - (concat "[^:{}()\n#]*\\(?:" scss--hash-re "[^:{}()\n#]*\\)*")) + ;; We have at least one selector. + (css--selector-regexp sassy) + ;; And then possibly more. + "\\(?:" + ;; Separators between selectors. + "[ \n\t,+~>]+" + (css--selector-regexp sassy) "\\)*" - "\\)\\(?:\n[ \t]*\\)*{") + ;; And then a brace. + "\\)[ \n\t]*{") (1 'css-selector keep)) ;; In the above rule, we allow the open-brace to be on some subsequent ;; line. This will only work if we properly mark the intervening text diff --git a/test/lisp/textmodes/css-mode-resources/css-selectors.txt b/test/lisp/textmodes/css-mode-resources/css-selectors.txt new file mode 100644 index 00000000000..5b3d990f279 --- /dev/null +++ b/test/lisp/textmodes/css-mode-resources/css-selectors.txt @@ -0,0 +1,56 @@ +#firstname +* +p +p.intro +div, p +div p +div > p +div + p +p ~ ul +[target] +[target=_blank] +[title~=flower] +[lang|=en] +a[href^="https"] +a[href$=".pdf"] +a[href*="w3schools"] +a:active +p::after +p::before +input:checked +input:default +input:disabled +p:empty +input:enabled +p:first-child +p::first-letter +p::first-line +p:first-of-type +input:focus +:fullscreen +a:hover +input:in-range +input:indeterminate +input:invalid +p:lang(it) +p:last-child +p:last-of-type +a:link +::marker +:not(p) +p:nth-child(2) +p:nth-last-child(2) +p:nth-last-of-type(2) +p:nth-of-type(2) +p:only-of-type +p:only-child +input:optional +input:out-of-range +input:read-only +input:read-write +input:required +:root +::selection +#news:target +input:valid +a:visited diff --git a/test/lisp/textmodes/css-mode-resources/scss-selectors.txt b/test/lisp/textmodes/css-mode-resources/scss-selectors.txt new file mode 100644 index 00000000000..86e58110b72 --- /dev/null +++ b/test/lisp/textmodes/css-mode-resources/scss-selectors.txt @@ -0,0 +1,6 @@ +p.#{$name} var +p.#{$name}:active var +p.#{$name}::after var +f.#{$bar}::after p::after +p.#{$name} f.#{$bar} k.var #{$bar} #{$bar} +p.#{$name} diff --git a/test/lisp/textmodes/css-mode-tests.el b/test/lisp/textmodes/css-mode-tests.el index 0ae1593508d..555a73c3bbe 100644 --- a/test/lisp/textmodes/css-mode-tests.el +++ b/test/lisp/textmodes/css-mode-tests.el @@ -419,5 +419,74 @@ (indent-region (point-min) (point-max)) (should (equal (buffer-string) orig))))) +(ert-deftest css-mode-test-selectors () + (let ((selectors + (with-temp-buffer + (insert-file-contents (ert-resource-file "css-selectors.txt")) + (string-lines (buffer-string))))) + (with-suppressed-warnings ((interactive font-lock-debug-fontif)) + (dolist (selector selectors) + (with-temp-buffer + (css-mode) + (insert selector " {\n}\n") + (font-lock-debug-fontify) + (goto-char (point-min)) + (unless (eq (get-text-property (point) 'face) + 'css-selector) + (should-not (format "Didn't recognize %s as a selector" + (buffer-substring-no-properties + (point) (line-end-position))))))) + ;; Test many selectors. + (dolist (selector selectors) + (with-temp-buffer + (css-mode) + (insert selector " ") + (dotimes (_ (random 5)) + (insert (seq-random-elt '(" , " " > " " + ")) + (seq-random-elt selectors))) + (insert "{\n}\n") + (font-lock-debug-fontify) + (goto-char (point-min)) + (unless (eq (get-text-property (point) 'face) + 'css-selector) + (should-not (format "Didn't recognize %s as a selector" + (buffer-substring-no-properties + (point) (line-end-position))))))) + ;; Test wrong separators. + (dolist (selector selectors) + (with-temp-buffer + (css-mode) + (insert selector " ") + (dotimes (_ (1+ (random 5))) + (insert (seq-random-elt '("=" " @ ")) + (seq-random-elt selectors))) + (insert "{\n}\n") + (font-lock-debug-fontify) + (goto-char (point-min)) + (when (eq (get-text-property (point) 'face) + 'css-selector) + (should-not (format "Recognized %s as a selector" + (buffer-substring-no-properties + (point) (line-end-position)))))))))) + +(ert-deftest scss-mode-test-selectors () + (let ((selectors + (with-temp-buffer + (insert-file-contents (ert-resource-file "scss-selectors.txt")) + (string-lines (buffer-string))))) + (with-suppressed-warnings ((interactive font-lock-debug-fontif)) + (dolist (selector selectors) + (with-temp-buffer + (scss-mode) + (insert selector " {\n}\n") + (font-lock-debug-fontify) + (goto-char (point-min)) + (unless (eq (get-text-property (point) 'face) + 'css-selector) + (should-not (format "Didn't recognize %s as a selector" + (buffer-substring-no-properties + (point) (line-end-position)))))))))) + + (provide 'css-mode-tests) ;;; css-mode-tests.el ends here -- 2.39.2