css-mode.el 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. ;;; css-mode.el --- Major mode to edit CSS files -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2006-2012 Free Software Foundation, Inc.
  3. ;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
  4. ;; Keywords: hypermedia
  5. ;; This file is part of GNU Emacs.
  6. ;; GNU Emacs is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; GNU Emacs is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; Yet another CSS mode.
  18. ;;; Todo:
  19. ;; - electric ; and }
  20. ;; - filling code with auto-fill-mode
  21. ;; - completion
  22. ;; - fix font-lock errors with multi-line selectors
  23. ;;; Code:
  24. (defgroup css nil
  25. "Cascading Style Sheets (CSS) editing mode."
  26. :group 'languages)
  27. (eval-when-compile (require 'cl))
  28. (defun css-extract-keyword-list (res)
  29. (with-temp-buffer
  30. (url-insert-file-contents "http://www.w3.org/TR/REC-CSS2/css2.txt")
  31. (goto-char (point-max))
  32. (search-backward "Appendix H. Index")
  33. (forward-line)
  34. (delete-region (point-min) (point))
  35. (let ((result nil)
  36. keys)
  37. (dolist (re res)
  38. (goto-char (point-min))
  39. (setq keys nil)
  40. (while (re-search-forward (cdr re) nil t)
  41. (push (match-string 1) keys))
  42. (push (cons (car re) (sort keys 'string-lessp)) result))
  43. (nreverse result))))
  44. (defun css-extract-parse-val-grammar (string env)
  45. (let ((start 0)
  46. (elems ())
  47. name)
  48. (while (string-match
  49. (concat "\\(?:"
  50. (concat "<a [^>]+><span [^>]+>\\(?:"
  51. "&lt;\\([^&]+\\)&gt;\\|'\\([^']+\\)'"
  52. "\\)</span></a>")
  53. "\\|" "\\(\\[\\)"
  54. "\\|" "\\(]\\)"
  55. "\\|" "\\(||\\)"
  56. "\\|" "\\(|\\)"
  57. "\\|" "\\([*+?]\\)"
  58. "\\|" "\\({[^}]+}\\)"
  59. "\\|" "\\(\\w+\\(?:-\\w+\\)*\\)"
  60. "\\)[ \t\n]*")
  61. string start)
  62. ;; (assert (eq start (match-beginning 0)))
  63. (setq start (match-end 0))
  64. (cond
  65. ;; Reference to a type of value.
  66. ((setq name (match-string-no-properties 1 string))
  67. (push (intern name) elems))
  68. ;; Reference to another property's values.
  69. ((setq name (match-string-no-properties 2 string))
  70. (setq elems (delete-dups (append (cdr (assoc name env)) elems))))
  71. ;; A literal
  72. ((setq name (match-string-no-properties 9 string))
  73. (push name elems))
  74. ;; We just ignore the rest. I.e. we ignore the structure because
  75. ;; it's too difficult to exploit anyway (it would allow us to only
  76. ;; complete top/center/bottom after one of left/center/right and
  77. ;; vice-versa).
  78. (t nil)))
  79. elems))
  80. (defun css-extract-props-and-vals ()
  81. (with-temp-buffer
  82. (url-insert-file-contents "http://www.w3.org/TR/CSS21/propidx.html")
  83. (goto-char (point-min))
  84. (let ((props ()))
  85. (while (re-search-forward "#propdef-\\([^\"]+\\)\"><span class=\"propinst-\\1 xref\">'\\1'</span></a>" nil t)
  86. (let ((prop (match-string-no-properties 1)))
  87. (save-excursion
  88. (goto-char (match-end 0))
  89. (search-forward "<td>")
  90. (let ((vals-string (buffer-substring (point)
  91. (progn
  92. (re-search-forward "[ \t\n]+|[ \t\n]+<a href=\"cascade.html#value-def-inherit\" class=\"noxref\"><span class=\"value-inst-inherit\">inherit</span></a>")
  93. (match-beginning 0)))))
  94. ;;
  95. (push (cons prop (css-extract-parse-val-grammar vals-string props))
  96. props)))))
  97. props)))
  98. ;; Extraction was done with:
  99. ;; (css-extract-keyword-list
  100. ;; '((pseudo . "^ +\\* :\\([^ \n,]+\\)")
  101. ;; (at . "^ +\\* @\\([^ \n,]+\\)")
  102. ;; (descriptor . "^ +\\* '\\([^ '\n]+\\)' (descriptor)")
  103. ;; (media . "^ +\\* '\\([^ '\n]+\\)' media group")
  104. ;; (property . "^ +\\* '\\([^ '\n]+\\)',")))
  105. (defconst css-pseudo-ids
  106. '("active" "after" "before" "first" "first-child" "first-letter" "first-line"
  107. "focus" "hover" "lang" "left" "link" "right" "visited")
  108. "Identifiers for pseudo-elements and pseudo-classes.")
  109. (defconst css-at-ids
  110. '("charset" "font-face" "import" "media" "page")
  111. "Identifiers that appear in the form @foo.")
  112. (defconst css-descriptor-ids
  113. '("ascent" "baseline" "bbox" "cap-height" "centerline" "definition-src"
  114. "descent" "font-family" "font-size" "font-stretch" "font-style"
  115. "font-variant" "font-weight" "mathline" "panose-1" "slope" "src" "stemh"
  116. "stemv" "topline" "unicode-range" "units-per-em" "widths" "x-height")
  117. "Identifiers for font descriptors.")
  118. (defconst css-media-ids
  119. '("all" "aural" "bitmap" "continuous" "grid" "paged" "static" "tactile"
  120. "visual")
  121. "Identifiers for types of media.")
  122. (defconst css-property-ids
  123. '("azimuth" "background" "background-attachment" "background-color"
  124. "background-image" "background-position" "background-repeat" "block"
  125. "border" "border-bottom" "border-bottom-color" "border-bottom-style"
  126. "border-bottom-width" "border-collapse" "border-color" "border-left"
  127. "border-left-color" "border-left-style" "border-left-width" "border-right"
  128. "border-right-color" "border-right-style" "border-right-width"
  129. "border-spacing" "border-style" "border-top" "border-top-color"
  130. "border-top-style" "border-top-width" "border-width" "bottom"
  131. "caption-side" "clear" "clip" "color" "compact" "content"
  132. "counter-increment" "counter-reset" "cue" "cue-after" "cue-before"
  133. "cursor" "dashed" "direction" "display" "dotted" "double" "elevation"
  134. "empty-cells" "float" "font" "font-family" "font-size" "font-size-adjust"
  135. "font-stretch" "font-style" "font-variant" "font-weight" "groove" "height"
  136. "hidden" "inline" "inline-table" "inset" "left" "letter-spacing"
  137. "line-height" "list-item" "list-style" "list-style-image"
  138. "list-style-position" "list-style-type" "margin" "margin-bottom"
  139. "margin-left" "margin-right" "margin-top" "marker-offset" "marks"
  140. "max-height" "max-width" "min-height" "min-width" "orphans" "outline"
  141. "outline-color" "outline-style" "outline-width" "outset" "overflow"
  142. "padding" "padding-bottom" "padding-left" "padding-right" "padding-top"
  143. "page" "page-break-after" "page-break-before" "page-break-inside" "pause"
  144. "pause-after" "pause-before" "pitch" "pitch-range" "play-during" "position"
  145. "quotes" "richness" "ridge" "right" "run-in" "size" "solid" "speak"
  146. "speak-header" "speak-numeral" "speak-punctuation" "speech-rate" "stress"
  147. "table" "table-caption" "table-cell" "table-column" "table-column-group"
  148. "table-footer-group" "table-header-group" "table-layout" "table-row"
  149. "table-row-group" "text-align" "text-decoration" "text-indent"
  150. "text-shadow" "text-transform" "top" "unicode-bidi" "vertical-align"
  151. "visibility" "voice-family" "volume" "white-space" "widows" "width"
  152. "word-spacing" "z-index")
  153. "Identifiers for properties.")
  154. (defcustom css-electric-keys '(?\} ?\;) ;; '()
  155. "Self inserting keys which should trigger re-indentation."
  156. :version "22.2"
  157. :type '(repeat character)
  158. :options '((?\} ?\;))
  159. :group 'css)
  160. (defvar css-mode-syntax-table
  161. (let ((st (make-syntax-table)))
  162. ;; C-style comments.
  163. (modify-syntax-entry ?/ ". 14" st)
  164. (modify-syntax-entry ?* ". 23" st)
  165. ;; Strings.
  166. (modify-syntax-entry ?\" "\"" st)
  167. (modify-syntax-entry ?\' "\"" st)
  168. ;; Blocks.
  169. (modify-syntax-entry ?\{ "(}" st)
  170. (modify-syntax-entry ?\} "){" st)
  171. ;; Args in url(...) thingies and other "function calls".
  172. (modify-syntax-entry ?\( "()" st)
  173. (modify-syntax-entry ?\) ")(" st)
  174. ;; To match attributes in selectors.
  175. (modify-syntax-entry ?\[ "(]" st)
  176. (modify-syntax-entry ?\] ")[" st)
  177. ;; Special chars that sometimes come at the beginning of words.
  178. (modify-syntax-entry ?@ "'" st)
  179. ;; (modify-syntax-entry ?: "'" st)
  180. (modify-syntax-entry ?# "'" st)
  181. ;; Distinction between words and symbols.
  182. (modify-syntax-entry ?- "_" st)
  183. st))
  184. (defconst css-escapes-re
  185. "\\\\\\(?:[^\000-\037\177]\\|[0-9a-fA-F]+[ \n\t\r\f]?\\)")
  186. (defconst css-nmchar-re (concat "\\(?:[-[:alnum:]]\\|" css-escapes-re "\\)"))
  187. (defconst css-nmstart-re (concat "\\(?:[[:alpha:]]\\|" css-escapes-re "\\)"))
  188. (defconst css-ident-re (concat css-nmstart-re css-nmchar-re "*"))
  189. (defconst css-proprietary-nmstart-re ;; Vendor-specific properties.
  190. (concat "[-_]" (regexp-opt '("ms" "moz" "o" "khtml" "webkit")) "-"))
  191. (defconst css-name-re (concat css-nmchar-re "+"))
  192. (defface css-selector '((t :inherit font-lock-function-name-face))
  193. "Face to use for selectors."
  194. :group 'css)
  195. (defface css-property '((t :inherit font-lock-variable-name-face))
  196. "Face to use for properties."
  197. :group 'css)
  198. (defface css-proprietary-property '((t :inherit (css-property italic)))
  199. "Face to use for vendor-specific properties.")
  200. (defvar css-font-lock-keywords
  201. `(("!\\s-*important" . font-lock-builtin-face)
  202. ;; Atrules keywords. IDs not in css-at-ids are valid (ignored).
  203. ;; In fact the regexp should probably be
  204. ;; (,(concat "\\(@" css-ident-re "\\)\\([ \t\n][^;{]*\\)[;{]")
  205. ;; (1 font-lock-builtin-face))
  206. ;; Since "An at-rule consists of everything up to and including the next
  207. ;; semicolon (;) or the next block, whichever comes first."
  208. (,(concat "@" css-ident-re) . font-lock-builtin-face)
  209. ;; Selectors.
  210. ;; FIXME: attribute selectors don't work well because they may contain
  211. ;; strings which have already been highlighted as f-l-string-face and
  212. ;; thus prevent this highlighting from being applied (actually now that
  213. ;; I use `append' this should work better). But really the part of hte
  214. ;; selector between [...] should simply not be highlighted.
  215. (,(concat "^\\([ \t]*[^@:{}\n][^:{}]+\\(?::" (regexp-opt css-pseudo-ids t)
  216. "\\(?:([^)]+)\\)?[^:{\n]*\\)*\\)\\(?:\n[ \t]*\\)*{")
  217. (1 'css-selector append))
  218. ;; In the above rule, we allow the open-brace to be on some subsequent
  219. ;; line. This will only work if we properly mark the intervening text
  220. ;; as being part of a multiline element (and even then, this only
  221. ;; ensures proper refontification, but not proper discovery).
  222. ("^[ \t]*{" (0 (save-excursion
  223. (goto-char (match-beginning 0))
  224. (skip-chars-backward " \n\t")
  225. (put-text-property (point) (match-end 0)
  226. 'font-lock-multiline t)
  227. ;; No face.
  228. nil)))
  229. ;; Properties. Again, we don't limit ourselves to css-property-ids.
  230. (,(concat "\\(?:[{;]\\|^\\)[ \t]*\\("
  231. "\\(?:\\(" css-proprietary-nmstart-re "\\)\\|"
  232. css-nmstart-re "\\)" css-nmchar-re "*"
  233. "\\)\\s-*:")
  234. (1 (if (match-end 2) 'css-proprietary-property 'css-property)))))
  235. (defvar css-font-lock-defaults
  236. '(css-font-lock-keywords nil t))
  237. ;;;###autoload
  238. (define-derived-mode css-mode fundamental-mode "CSS"
  239. "Major mode to edit Cascading Style Sheets."
  240. (set (make-local-variable 'font-lock-defaults) css-font-lock-defaults)
  241. (set (make-local-variable 'comment-start) "/*")
  242. (set (make-local-variable 'comment-start-skip) "/\\*+[ \t]*")
  243. (set (make-local-variable 'comment-end) "*/")
  244. (set (make-local-variable 'comment-end-skip) "[ \t]*\\*+/")
  245. (set (make-local-variable 'forward-sexp-function) 'css-forward-sexp)
  246. (set (make-local-variable 'parse-sexp-ignore-comments) t)
  247. (set (make-local-variable 'indent-line-function) 'css-indent-line)
  248. (set (make-local-variable 'fill-paragraph-function)
  249. 'css-fill-paragraph)
  250. (when css-electric-keys
  251. (let ((fc (make-char-table 'auto-fill-chars)))
  252. (set-char-table-parent fc auto-fill-chars)
  253. (dolist (c css-electric-keys)
  254. (aset fc c 'indent-according-to-mode))
  255. (set (make-local-variable 'auto-fill-chars) fc))))
  256. (defvar comment-continue)
  257. (defun css-fill-paragraph (&optional justify)
  258. (save-excursion
  259. (let ((ppss (syntax-ppss))
  260. (eol (line-end-position)))
  261. (cond
  262. ((and (nth 4 ppss)
  263. (save-excursion
  264. (goto-char (nth 8 ppss))
  265. (forward-comment 1)
  266. (prog1 (not (bolp))
  267. (setq eol (point)))))
  268. ;; Filling inside a comment whose comment-end marker is not \n.
  269. ;; This code is meant to be generic, so that it works not only for
  270. ;; css-mode but for all modes.
  271. (save-restriction
  272. (narrow-to-region (nth 8 ppss) eol)
  273. (comment-normalize-vars) ;Will define comment-continue.
  274. (let ((fill-paragraph-function nil)
  275. (paragraph-separate
  276. (if (and comment-continue
  277. (string-match "[^ \t]" comment-continue))
  278. (concat "\\(?:[ \t]*" (regexp-quote comment-continue)
  279. "\\)?\\(?:" paragraph-separate "\\)")
  280. paragraph-separate))
  281. (paragraph-start
  282. (if (and comment-continue
  283. (string-match "[^ \t]" comment-continue))
  284. (concat "\\(?:[ \t]*" (regexp-quote comment-continue)
  285. "\\)?\\(?:" paragraph-start "\\)")
  286. paragraph-start)))
  287. (fill-paragraph justify)
  288. ;; Don't try filling again.
  289. t)))
  290. ((and (null (nth 8 ppss))
  291. (or (nth 1 ppss)
  292. (and (ignore-errors
  293. (down-list 1)
  294. (when (<= (point) eol)
  295. (setq ppss (syntax-ppss)))))))
  296. (goto-char (nth 1 ppss))
  297. (let ((end (save-excursion
  298. (ignore-errors (forward-sexp 1) (copy-marker (point) t)))))
  299. (when end
  300. (while (re-search-forward "[{;}]" end t)
  301. (cond
  302. ;; This is a false positive inside a string or comment.
  303. ((nth 8 (syntax-ppss)) nil)
  304. ((eq (char-before) ?\})
  305. (save-excursion
  306. (forward-char -1)
  307. (skip-chars-backward " \t")
  308. (unless (bolp) (newline))))
  309. (t
  310. (while
  311. (progn
  312. (setq eol (line-end-position))
  313. (and (forward-comment 1)
  314. (> (point) eol)
  315. ;; A multi-line comment should be on its own line.
  316. (save-excursion (forward-comment -1)
  317. (when (< (point) eol)
  318. (newline)
  319. t)))))
  320. (if (< (point) eol) (newline)))))
  321. (goto-char (nth 1 ppss))
  322. (indent-region (line-beginning-position 2) end)
  323. ;; Don't use the default filling code.
  324. t)))))))
  325. ;;; Navigation and indentation.
  326. (defconst css-navigation-syntax-table
  327. (let ((st (make-syntax-table css-mode-syntax-table)))
  328. (map-char-table (lambda (c v)
  329. ;; Turn punctuation (code = 1) into symbol (code = 1).
  330. (if (eq (car-safe v) 1)
  331. (set-char-table-range st c (cons 3 (cdr v)))))
  332. st)
  333. st))
  334. (defun css-backward-sexp (n)
  335. (let ((forward-sexp-function nil))
  336. (if (< n 0) (css-forward-sexp (- n))
  337. (while (> n 0)
  338. (setq n (1- n))
  339. (forward-comment (- (point-max)))
  340. (if (not (eq (char-before) ?\;))
  341. (backward-sexp 1)
  342. (while (progn (backward-sexp 1)
  343. (save-excursion
  344. (forward-comment (- (point-max)))
  345. ;; FIXME: We should also skip punctuation.
  346. (not (or (bobp) (memq (char-before) '(?\; ?\{))))))))))))
  347. (defun css-forward-sexp (n)
  348. (let ((forward-sexp-function nil))
  349. (if (< n 0) (css-backward-sexp (- n))
  350. (while (> n 0)
  351. (setq n (1- n))
  352. (forward-comment (point-max))
  353. (if (not (eq (char-after) ?\;))
  354. (forward-sexp 1)
  355. (while (progn (forward-sexp 1)
  356. (save-excursion
  357. (forward-comment (point-max))
  358. ;; FIXME: We should also skip punctuation.
  359. (not (memq (char-after) '(?\; ?\})))))))))))
  360. (defun css-indent-calculate-virtual ()
  361. (if (or (save-excursion (skip-chars-backward " \t") (bolp))
  362. (if (looking-at "\\s(")
  363. (save-excursion
  364. (forward-char 1) (skip-chars-forward " \t")
  365. (not (or (eolp) (looking-at comment-start-skip))))))
  366. (current-column)
  367. (css-indent-calculate)))
  368. (defcustom css-indent-offset 4
  369. "Basic size of one indentation step."
  370. :version "22.2"
  371. :type 'integer
  372. :group 'css)
  373. (defun css-indent-calculate ()
  374. (let ((ppss (syntax-ppss))
  375. pos)
  376. (with-syntax-table css-navigation-syntax-table
  377. (save-excursion
  378. (cond
  379. ;; Inside a string.
  380. ((nth 3 ppss) 'noindent)
  381. ;; Inside a comment.
  382. ((nth 4 ppss)
  383. (setq pos (point))
  384. (forward-line -1)
  385. (skip-chars-forward " \t")
  386. (if (>= (nth 8 ppss) (point))
  387. (progn
  388. (goto-char (nth 8 ppss))
  389. (if (eq (char-after pos) ?*)
  390. (forward-char 1)
  391. (if (not (looking-at comment-start-skip))
  392. (error "Internal css-mode error")
  393. (goto-char (match-end 0))))
  394. (current-column))
  395. (if (and (eq (char-after pos) ?*) (eq (char-after) ?*))
  396. (current-column)
  397. ;; 'noindent
  398. (current-column)
  399. )))
  400. ;; In normal code.
  401. (t
  402. (or
  403. (when (looking-at "\\s)")
  404. (forward-char 1)
  405. (backward-sexp 1)
  406. (css-indent-calculate-virtual))
  407. (when (looking-at comment-start-skip)
  408. (forward-comment (point-max))
  409. (css-indent-calculate))
  410. (when (save-excursion (forward-comment (- (point-max)))
  411. (setq pos (point))
  412. (eq (char-syntax (preceding-char)) ?\())
  413. (goto-char (1- pos))
  414. (if (not (looking-at "\\s([ \t]*"))
  415. (error "Internal css-mode error")
  416. (if (or (memq (char-after (match-end 0)) '(?\n nil))
  417. (save-excursion (goto-char (match-end 0))
  418. (looking-at comment-start-skip)))
  419. (+ (css-indent-calculate-virtual) css-indent-offset)
  420. (progn (goto-char (match-end 0)) (current-column)))))
  421. (progn
  422. (css-backward-sexp 1)
  423. (if (looking-at "\\s(")
  424. (css-indent-calculate)
  425. (css-indent-calculate-virtual))))))))))
  426. (defun css-indent-line ()
  427. "Indent current line according to CSS indentation rules."
  428. (interactive)
  429. (let* ((savep (point))
  430. (forward-sexp-function nil)
  431. (indent (condition-case nil
  432. (save-excursion
  433. (forward-line 0)
  434. (skip-chars-forward " \t")
  435. (if (>= (point) savep) (setq savep nil))
  436. (css-indent-calculate))
  437. (error nil))))
  438. (if (not (numberp indent)) 'noindent
  439. (if savep
  440. (save-excursion (indent-line-to indent))
  441. (indent-line-to indent)))))
  442. (provide 'css-mode)
  443. ;;; css-mode.el ends here