1 ;;; css-mode.el --- Major mode to edit CSS files -*- lexical-binding: t -*-
3 ;; Copyright (C) 2006-2016 Free Software Foundation, Inc.
5 ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
6 ;; Maintainer: Simen Heggestøyl <simenheg@gmail.com>
7 ;; Keywords: hypermedia
9 ;; This file is part of GNU Emacs.
11 ;; GNU Emacs is free software: you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; GNU Emacs is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
26 ;; Yet another CSS mode.
31 ;; - filling code with auto-fill-mode
32 ;; - attribute value completion
33 ;; - fix font-lock errors with multi-line selectors
38 "Cascading Style Sheets (CSS) editing mode."
41 (defconst css-pseudo-class-ids
42 '("active" "checked" "disabled" "empty" "enabled" "first"
43 "first-child" "first-of-type" "focus" "hover" "indeterminate" "lang"
44 "last-child" "last-of-type" "left" "link" "not" "nth-child"
45 "nth-last-child" "nth-last-of-type" "nth-of-type" "only-child"
46 "only-of-type" "right" "root" "target" "visited")
47 "Identifiers for pseudo-classes.")
49 (defconst css-pseudo-element-ids
50 '("after" "before" "first-letter" "first-line")
51 "Identifiers for pseudo-elements.")
54 '("charset" "font-face" "import" "media" "namespace" "page")
55 "Identifiers that appear in the form @foo.")
57 (defconst css-bang-ids
59 "Identifiers that appear in the form !foo.")
61 (defconst scss-bang-ids
62 '("default" "global" "optional")
63 "Additional identifiers that appear in the form !foo in SCSS.")
65 (defconst css-descriptor-ids
66 '("ascent" "baseline" "bbox" "cap-height" "centerline" "definition-src"
67 "descent" "font-family" "font-size" "font-stretch" "font-style"
68 "font-variant" "font-weight" "mathline" "panose-1" "slope" "src" "stemh"
69 "stemv" "topline" "unicode-range" "units-per-em" "widths" "x-height")
70 "Identifiers for font descriptors.")
72 (defconst css-media-ids
73 '("all" "aural" "bitmap" "continuous" "grid" "paged" "static" "tactile"
75 "Identifiers for types of media.")
77 (defconst css-property-ids
78 '(;; CSS 2.1 properties (http://www.w3.org/TR/CSS21/propidx.html).
80 ;; Properties duplicated by any of the CSS3 modules below have
82 "azimuth" "border-collapse" "border-spacing" "bottom"
83 "caption-side" "clear" "clip" "content" "counter-increment"
84 "counter-reset" "cue" "cue-after" "cue-before" "direction" "display"
85 "elevation" "empty-cells" "float" "height" "left" "line-height"
86 "list-style" "list-style-image" "list-style-position"
87 "list-style-type" "margin" "margin-bottom" "margin-left"
88 "margin-right" "margin-top" "max-height" "max-width" "min-height"
89 "min-width" "padding" "padding-bottom" "padding-left"
90 "padding-right" "padding-top" "page-break-after"
91 "page-break-before" "page-break-inside" "pause" "pause-after"
92 "pause-before" "pitch" "pitch-range" "play-during" "position"
93 "quotes" "richness" "right" "speak" "speak-header" "speak-numeral"
94 "speak-punctuation" "speech-rate" "stress" "table-layout" "top"
95 "unicode-bidi" "vertical-align" "visibility" "voice-family" "volume"
99 ;; (http://www.w3.org/TR/css3-animations/#property-index)
100 "animation" "animation-delay" "animation-direction"
101 "animation-duration" "animation-fill-mode"
102 "animation-iteration-count" "animation-name"
103 "animation-play-state" "animation-timing-function"
105 ;; CSS Backgrounds and Borders Module Level 3
106 ;; (http://www.w3.org/TR/css3-background/#property-index)
107 "background" "background-attachment" "background-clip"
108 "background-color" "background-image" "background-origin"
109 "background-position" "background-repeat" "background-size"
110 "border" "border-bottom" "border-bottom-color"
111 "border-bottom-left-radius" "border-bottom-right-radius"
112 "border-bottom-style" "border-bottom-width" "border-color"
113 "border-image" "border-image-outset" "border-image-repeat"
114 "border-image-slice" "border-image-source" "border-image-width"
115 "border-left" "border-left-color" "border-left-style"
116 "border-left-width" "border-radius" "border-right"
117 "border-right-color" "border-right-style" "border-right-width"
118 "border-style" "border-top" "border-top-color"
119 "border-top-left-radius" "border-top-right-radius"
120 "border-top-style" "border-top-width" "border-width" "box-shadow"
122 ;; CSS Basic User Interface Module Level 3 (CSS3 UI)
123 ;; (http://www.w3.org/TR/css3-ui/#property-index)
124 "box-sizing" "caret-color" "cursor" "nav-down" "nav-left"
125 "nav-right" "nav-up" "outline" "outline-color" "outline-offset"
126 "outline-style" "outline-width" "resize" "text-overflow"
128 ;; CSS Color Module Level 3
129 ;; (http://www.w3.org/TR/css3-color/#property)
132 ;; CSS Flexible Box Layout Module Level 1
133 ;; (http://www.w3.org/TR/css-flexbox-1/#property-index)
134 "align-content" "align-items" "align-self" "flex" "flex-basis"
135 "flex-direction" "flex-flow" "flex-grow" "flex-shrink" "flex-wrap"
136 "justify-content" "order"
138 ;; CSS Fonts Module Level 3
139 ;; (http://www.w3.org/TR/css3-fonts/#property-index)
140 "font" "font-family" "font-feature-settings" "font-kerning"
141 "font-language-override" "font-size" "font-size-adjust"
142 "font-stretch" "font-style" "font-synthesis" "font-variant"
143 "font-variant-alternates" "font-variant-caps"
144 "font-variant-east-asian" "font-variant-ligatures"
145 "font-variant-numeric" "font-variant-position" "font-weight"
147 ;; CSS Fragmentation Module Level 3
148 ;; (https://www.w3.org/TR/css-break-3/#property-index)
149 "box-decoration-break" "break-after" "break-before" "break-inside"
152 ;; CSS Multi-column Layout Module
153 ;; (https://www.w3.org/TR/css3-multicol/#property-index)
154 ;; "break-after", "break-before", and "break-inside" are left out
155 ;; below, because they're already included in CSS Fragmentation
157 "column-count" "column-fill" "column-gap" "column-rule"
158 "column-rule-color" "column-rule-style" "column-rule-width"
159 "column-span" "column-width" "columns"
161 ;; CSS Overflow Module Level 3
162 ;; (http://www.w3.org/TR/css-overflow-3/#property-index)
163 "max-lines" "overflow" "overflow-x" "overflow-y"
165 ;; CSS Text Decoration Module Level 3
166 ;; (http://dev.w3.org/csswg/css-text-decor-3/#property-index)
167 "text-decoration" "text-decoration-color" "text-decoration-line"
168 "text-decoration-skip" "text-decoration-style" "text-emphasis"
169 "text-emphasis-color" "text-emphasis-position" "text-emphasis-style"
170 "text-shadow" "text-underline-position"
172 ;; CSS Text Module Level 3
173 ;; (http://www.w3.org/TR/css3-text/#property-index)
174 "hanging-punctuation" "hyphens" "letter-spacing" "line-break"
175 "overflow-wrap" "tab-size" "text-align" "text-align-last"
176 "text-indent" "text-justify" "text-transform" "white-space"
177 "word-break" "word-spacing" "word-wrap"
179 ;; CSS Transforms Module Level 1
180 ;; (http://www.w3.org/TR/css3-2d-transforms/#property-index)
181 "backface-visibility" "perspective" "perspective-origin"
182 "transform" "transform-origin" "transform-style"
185 ;; (http://www.w3.org/TR/css3-transitions/#property-index)
186 "transition" "transition-delay" "transition-duration"
187 "transition-property" "transition-timing-function"
189 ;; Filter Effects Module Level 1
190 ;; (http://www.w3.org/TR/filter-effects/#property-index)
191 "color-interpolation-filters" "filter" "flood-color"
192 "flood-opacity" "lighting-color")
193 "Identifiers for properties.")
195 (defcustom css-electric-keys
'(?\
} ?\
;) ;; '()
196 "Self inserting keys which should trigger re-indentation."
198 :type
'(repeat character
)
199 :options
'((?\
} ?\
;))
202 (defvar css-mode-syntax-table
203 (let ((st (make-syntax-table)))
205 (modify-syntax-entry ?
/ ". 14" st
)
206 (modify-syntax-entry ?
* ". 23b" st
)
208 (modify-syntax-entry ?
\" "\"" st
)
209 (modify-syntax-entry ?
\' "\"" st
)
211 (modify-syntax-entry ?\
{ "(}" st
)
212 (modify-syntax-entry ?\
} "){" st
)
213 ;; Args in url(...) thingies and other "function calls".
214 (modify-syntax-entry ?\
( "()" st
)
215 (modify-syntax-entry ?\
) ")(" st
)
216 ;; To match attributes in selectors.
217 (modify-syntax-entry ?\
[ "(]" st
)
218 (modify-syntax-entry ?\
] ")[" st
)
219 ;; Special chars that sometimes come at the beginning of words.
220 (modify-syntax-entry ?
@ "'" st
)
221 ;; (modify-syntax-entry ?: "'" st)
222 (modify-syntax-entry ?
# "'" st
)
223 ;; Distinction between words and symbols.
224 (modify-syntax-entry ?-
"_" st
)
228 (defconst css--uri-re
230 "url\\((\\)[[:space:]]*\\(?:\\\\.\\|[^()[:space:]\n'\"]\\)+"
231 "[[:space:]]*\\()\\)")))
233 (defconst css-syntax-propertize-function
234 (syntax-propertize-rules
235 (css--uri-re (1 "|") (2 "|"))))
237 (defconst css-escapes-re
238 "\\\\\\(?:[^\000-\037\177]\\|[0-9a-fA-F]+[ \n\t\r\f]?\\)")
239 (defconst css-nmchar-re
(concat "\\(?:[-[:alnum:]]\\|" css-escapes-re
"\\)"))
240 (defconst css-nmstart-re
(concat "\\(?:[[:alpha:]]\\|" css-escapes-re
"\\)"))
241 (defconst css-ident-re
;; (concat css-nmstart-re css-nmchar-re "*")
242 ;; Apparently, "at rules" names can start with a dash, e.g. @-moz-keyframes.
243 (concat css-nmchar-re
"+"))
244 (defconst css-proprietary-nmstart-re
;; Vendor-specific properties.
245 (concat "[-_]" (regexp-opt '("ms" "moz" "o" "khtml" "webkit")) "-"))
246 (defconst css-name-re
(concat css-nmchar-re
"+"))
248 (defconst scss--hash-re
"#\\(?:{[$-_[:alnum:]]+}\\|[[:alnum:]]+\\)")
250 (defface css-selector
'((t :inherit font-lock-function-name-face
))
251 "Face to use for selectors."
253 (defface css-property
'((t :inherit font-lock-variable-name-face
))
254 "Face to use for properties."
256 (defface css-proprietary-property
'((t :inherit
(css-property italic
)))
257 "Face to use for vendor-specific properties.")
259 (defun css--font-lock-keywords (&optional sassy
)
261 (regexp-opt (append (if sassy scss-bang-ids
)
263 (0 font-lock-builtin-face
))
264 ;; Atrules keywords. IDs not in css-at-ids are valid (ignored).
265 ;; In fact the regexp should probably be
266 ;; (,(concat "\\(@" css-ident-re "\\)\\([ \t\n][^;{]*\\)[;{]")
267 ;; (1 font-lock-builtin-face))
268 ;; Since "An at-rule consists of everything up to and including the next
269 ;; semicolon (;) or the next block, whichever comes first."
270 (,(concat "@" css-ident-re
) (0 font-lock-builtin-face
))
272 (,(concat "--" css-ident-re
) (0 font-lock-variable-name-face
))
274 ;; FIXME: attribute selectors don't work well because they may contain
275 ;; strings which have already been highlighted as f-l-string-face and
276 ;; thus prevent this highlighting from being applied (actually now that
277 ;; I use `keep' this should work better). But really the part of the
278 ;; selector between [...] should simply not be highlighted.
282 ;; We don't allow / as first char, so as not to
283 ;; take a comment as the beginning of a selector.
284 "[^@/:{}() \t\n][^:{}()]+"
285 ;; Same as for non-sassy except we do want to allow { and }
286 ;; chars in selectors in the case of #{$foo}
287 ;; variable interpolation!
288 (concat "\\(?:" scss--hash-re
289 "\\|[^@/:{}() \t\n#]\\)"
290 "[^:{}()#]*\\(?:" scss--hash-re
"[^:{}()#]*\\)*"))
291 ;; Even though pseudo-elements should be prefixed by ::, a
292 ;; single colon is accepted for backward compatibility.
293 "\\(?:\\(:" (regexp-opt (append css-pseudo-class-ids
294 css-pseudo-element-ids
) t
)
295 "\\|\\::" (regexp-opt css-pseudo-element-ids t
) "\\)"
299 (concat "[^:{}()\n#]*\\(?:" scss--hash-re
"[^:{}()\n#]*\\)*"))
301 "\\)\\(?:\n[ \t]*\\)*{")
302 (1 'css-selector keep
))
303 ;; In the above rule, we allow the open-brace to be on some subsequent
304 ;; line. This will only work if we properly mark the intervening text
305 ;; as being part of a multiline element (and even then, this only
306 ;; ensures proper refontification, but not proper discovery).
307 ("^[ \t]*{" (0 (save-excursion
308 (goto-char (match-beginning 0))
309 (skip-chars-backward " \n\t")
310 (put-text-property (point) (match-end 0)
311 'font-lock-multiline t
)
314 ;; Properties. Again, we don't limit ourselves to css-property-ids.
315 (,(concat "\\(?:[{;]\\|^\\)[ \t]*\\("
316 "\\(?:\\(" css-proprietary-nmstart-re
"\\)\\|"
317 css-nmstart-re
"\\)" css-nmchar-re
"*"
319 (1 (if (match-end 2) 'css-proprietary-property
'css-property
)))
320 ;; Make sure the parens in a url(...) expression receive the
321 ;; default face. This is done because the parens may sometimes
322 ;; receive generic string delimiter syntax (see
323 ;; `css-syntax-propertize-function').
325 (1 'default t
) (2 'default t
))))
327 (defvar css-font-lock-keywords
(css--font-lock-keywords))
329 (defvar css-font-lock-defaults
330 '(css-font-lock-keywords nil t
))
332 (defcustom css-indent-offset
4
333 "Basic size of one indentation step."
340 (defconst css-smie-grammar
342 (smie-precs->prec2
'((assoc ";") (assoc ",") (left ":")))))
344 (defun css-smie--forward-token ()
346 ((and (eq (char-before) ?\
})
347 (scss-smie--not-interpolation-p)
348 ;; FIXME: If the next char is not whitespace, what should we do?
349 (or (memq (char-after) '(?\s ?
\t ?
\n))
350 (looking-at comment-start-skip
)))
351 (if (memq (char-after) '(?\s ?
\t ?
\n))
352 (forward-char 1) (forward-comment 1))
354 ((progn (forward-comment (point-max))
355 (looking-at "[;,:]"))
356 (forward-char 1) (match-string 0))
357 (t (smie-default-forward-token))))
359 (defun css-smie--backward-token ()
361 (forward-comment (- (point)))
363 ;; FIXME: If the next char is not whitespace, what should we do?
364 ((and (eq (char-before) ?\
}) (scss-smie--not-interpolation-p)
365 (> pos
(point))) ";")
366 ((memq (char-before) '(?\
; ?\, ?\:))
367 (forward-char -
1) (string (char-after)))
368 (t (smie-default-backward-token)))))
370 (defun css-smie-rules (kind token
)
371 (pcase (cons kind token
)
372 (`(:elem . basic
) css-indent-offset
)
374 (`(:list-intro .
,(or `";" `"")) t
) ;"" stands for BOB (bug#15467).
376 (when (or (smie-rule-hanging-p) (smie-rule-bolp))
377 (smie-backward-sexp ";")
378 (smie-indent-virtual)))
379 (`(:before .
,(or "{" "("))
380 (if (smie-rule-hanging-p) (smie-rule-parent 0)))))
384 (defun css--complete-property ()
385 "Complete property at point."
388 (skip-chars-backward "-[:alnum:]")
389 (let ((start (point)))
390 (skip-chars-backward " \t\r\n")
391 (when (memq (char-before) '(?\
{ ?\
;))
392 (list start pos css-property-ids
))))))
394 (defun css--complete-pseudo-element-or-class ()
395 "Complete pseudo-element or pseudo-class at point."
398 (skip-chars-backward "-[:alnum:]")
399 (when (eq (char-before) ?\
:)
401 (if (eq (char-before (- (point) 1)) ?\
:)
402 css-pseudo-element-ids
403 css-pseudo-class-ids
))))))
405 (defun css--complete-at-rule ()
406 "Complete at-rule (statement beginning with `@') at point."
409 (skip-chars-backward "-[:alnum:]")
410 (when (eq (char-before) ?\
@)
411 (list (point) pos css-at-ids
)))))
413 (defun css-completion-at-point ()
414 "Complete current symbol at point.
415 Currently supports completion of CSS properties, pseudo-elements,
416 pseudo-classes, and at-rules."
417 (or (css--complete-property)
418 (css--complete-pseudo-element-or-class)
419 (css--complete-at-rule)))
422 (define-derived-mode css-mode prog-mode
"CSS"
423 "Major mode to edit Cascading Style Sheets."
424 (setq-local font-lock-defaults css-font-lock-defaults
)
425 (setq-local comment-start
"/*")
426 (setq-local comment-start-skip
"/\\*+[ \t]*")
427 (setq-local comment-end
"*/")
428 (setq-local comment-end-skip
"[ \t]*\\*+/")
429 (setq-local syntax-propertize-function
430 css-syntax-propertize-function
)
431 (setq-local fill-paragraph-function
#'css-fill-paragraph
)
432 (setq-local adaptive-fill-function
#'css-adaptive-fill
)
433 (setq-local add-log-current-defun-function
#'css-current-defun-name
)
434 (smie-setup css-smie-grammar
#'css-smie-rules
435 :forward-token
#'css-smie--forward-token
436 :backward-token
#'css-smie--backward-token
)
437 (setq-local electric-indent-chars
438 (append css-electric-keys electric-indent-chars
))
439 (add-hook 'completion-at-point-functions
440 #'css-completion-at-point nil
'local
))
442 (defvar comment-continue
)
444 (defun css-fill-paragraph (&optional justify
)
446 ;; Fill succeeding comment when invoked right before a multi-line
448 (when (save-excursion
450 (comment-search-forward (point-at-eol) t
))
451 (goto-char (match-end 0)))
452 (let ((ppss (syntax-ppss))
453 (eol (line-end-position)))
457 (goto-char (nth 8 ppss
))
460 (setq eol
(point)))))
461 ;; Filling inside a comment whose comment-end marker is not \n.
462 ;; This code is meant to be generic, so that it works not only for
463 ;; css-mode but for all modes.
465 (narrow-to-region (nth 8 ppss
) eol
)
466 (comment-normalize-vars) ;Will define comment-continue.
467 (let ((fill-paragraph-function nil
)
469 (if (and comment-continue
470 (string-match "[^ \t]" comment-continue
))
471 (concat "\\(?:[ \t]*\\(?:"
472 (regexp-quote comment-continue
) "\\|"
473 comment-start-skip
"\\|"
474 comment-end-skip
"\\)\\)?"
475 "\\(?:" paragraph-separate
"\\)")
478 (if (and comment-continue
479 (string-match "[^ \t]" comment-continue
))
480 (concat "\\(?:[ \t]*" (regexp-quote comment-continue
)
481 "\\)?\\(?:" paragraph-start
"\\)")
483 (fill-paragraph justify
)
484 ;; Don't try filling again.
487 ((and (null (nth 8 ppss
))
491 (when (<= (point) eol
)
492 (setq ppss
(syntax-ppss)))))))
493 (goto-char (nth 1 ppss
))
494 (let ((end (save-excursion
495 (ignore-errors (forward-sexp 1) (copy-marker (point) t
)))))
497 (while (re-search-forward "[{;}]" end t
)
499 ;; This is a false positive inside a string or comment.
500 ((nth 8 (syntax-ppss)) nil
)
501 ;; This is a false positive when encountering an
502 ;; interpolated variable (bug#19751).
503 ((eq (char-before (- (point) 1)) ?
#) nil
)
504 ((eq (char-before) ?\
})
507 (skip-chars-backward " \t")
508 (when (and (not (bolp))
509 (scss-smie--not-interpolation-p))
514 (setq eol
(line-end-position))
515 (and (forward-comment 1)
517 ;; A multi-line comment should be on its own line.
518 (save-excursion (forward-comment -
1)
519 (when (< (point) eol
)
522 (if (< (point) eol
) (newline)))))
523 (goto-char (nth 1 ppss
))
524 (indent-region (line-beginning-position 2) end
)
525 ;; Don't use the default filling code.
528 (defun css-adaptive-fill ()
529 (when (looking-at "[ \t]*/\\*[ \t]*")
530 (let ((str (match-string 0)))
531 (and (string-match "/\\*" str
)
532 (replace-match " *" t t str
)))))
534 (defun css-current-defun-name ()
535 "Return the name of the CSS section at point, or nil."
537 (let ((max (max (point-min) (- (point) 1600)))) ; approx 20 lines back
538 (when (search-backward "{" max t
)
539 (skip-chars-backward " \t\r\n")
541 (if (looking-at "^[ \t]*\\([^{\r\n]*[^ {\t\r\n]\\)")
542 (match-string-no-properties 1))))))
546 (defvar scss-mode-syntax-table
547 (let ((st (make-syntax-table css-mode-syntax-table
)))
548 (modify-syntax-entry ?
/ ". 124" st
)
549 (modify-syntax-entry ?
\n ">" st
)
552 (defvar scss-font-lock-keywords
553 (append `((,(concat "$" css-ident-re
) (0 font-lock-variable-name-face
)))
554 (css--font-lock-keywords 'sassy
)
555 `((,(concat "@mixin[ \t]+\\(" css-ident-re
"\\)[ \t]*(")
556 (1 font-lock-function-name-face
)))))
558 (defun scss-smie--not-interpolation-p ()
561 (or (zerop (skip-chars-backward "-[:alnum:]"))
562 (not (looking-back "#{\\$" (- (point) 3))))))
564 ;;;###autoload (add-to-list 'auto-mode-alist '("\\.scss\\'" . scss-mode))
566 (define-derived-mode scss-mode css-mode
"SCSS"
567 "Major mode to edit \"Sassy CSS\" files."
568 (setq-local comment-start
"// ")
569 (setq-local comment-end
"")
570 (setq-local comment-continue
" *")
571 (setq-local comment-start-skip
"/[*/]+[ \t]*")
572 (setq-local comment-end-skip
"[ \t]*\\(?:\n\\|\\*+/\\)")
573 (setq-local font-lock-defaults
'(scss-font-lock-keywords nil t
)))
576 ;;; css-mode.el ends here