root/trunk/lisp/textmodes/css-mode.el

Revision 4220, 19.3 kB (checked in by miyoshi, 9 months ago)

Sync up with Emacs22.2.

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