Initial Commit
[packages] / xemacs-packages / text-modes / css-mode.el
1 ;;;; A major mode for editing CSS.
2
3 ;;; Adds font locking, some rather primitive indentation handling and
4 ;;; some typing help.
5 ;;;
6
7 (defvar cssm-version "0.11"
8   "The current version number of `css-mode'.")
9
10 ;;; copyright (c) 1998 Lars Marius Garshol, larsga@ifi.uio.no
11 ;;; $Id: css-mode.el,v 1.9 2000/01/05 21:21:56 larsga Exp $
12
13 ;;; css-mode is free software; you can redistribute it and/or
14 ;;; modify it under the terms of the GNU General Public License
15 ;;; as published by the Free Software Foundation; either version 2
16 ;;; of the License, or (at your option) any later version.
17 ;;;
18 ;;; css-mode is distributed in the hope that it will be useful,
19 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 ;;; GNU General Public License for more details.
22 ;;;
23 ;;; You should have received a copy of the GNU General Public License
24 ;;; along with css-mode; if not, write to the Free Software
25 ;;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
26
27 ; Send me an email if you want new features (or if you add them yourself).
28 ; I will do my best to preserve the API to functions not explicitly marked
29 ; as internal and variables shown as customizable. I make no promises about
30 ; the rest.
31
32 ; Bug reports are very welcome. New versions of the package will appear at
33 ; http://www.garshol.priv.no/download/software/css-mode/
34 ; You can register at the same address if you want to be notified when a
35 ; new version appears.
36
37 ; Thanks to Philippe Le Hegaret, Kjetil Kjernsmo, Alf-Ivar Holm and
38 ; Alfred Correira for much useful feedback. Alf-Ivar Holm also contributed
39 ; patches.
40
41 ; This version should be up to date with the following:
42 ; CSS1: http://www.w3.org/TR/1999/REC-CSS1-19990111
43 ; CSS2: http://www.w3.org/TR/1998/REC-CSS2-19980512
44
45 ; To install, put this in your .emacs:
46 ;
47 ; (autoload 'css-mode "css-mode")
48 ; (setq auto-mode-alist       
49 ;      (cons '("\\.css\\'" . css-mode) auto-mode-alist))
50
51 ;; Todo:
52
53 ; - must not color URL file name extensions as class selectors (*.css)
54 ; - color [] and url() constructs correctly, even if quoted strings present
55 ; - disregard anything inside strings
56
57 ;; Possible later additions:
58 ;
59 ; - forward/backward style/@media rule commands
60 ; - more complete syntax table
61
62 ;; Required modules
63
64 (require 'apropos)
65 (require 'font-lock)
66 (require 'cl)
67
68 ;;; The code itself
69
70 ; Customizable variables:
71
72 (defgroup css nil
73   "Major mode for editing CSS files."
74   :group 'languages
75   :group 'hypermedia
76   :prefix "cssm-")
77
78 (defcustom cssm-indent-level 2
79   "*The indentation level inside @media rules."
80   :type 'integer
81   :group 'css)
82
83 (defcustom cssm-mirror-mode t
84   "*Whether brackets, quotes etc should be mirrored automatically on insertion."
85   :type 'boolean
86   :group 'css)
87
88 (defcustom cssm-newline-before-closing-bracket nil
89   "*In `cssm-mirror-mode', controls whether a newline should be inserted
90 before the closing bracket or not."
91   :type 'boolean
92   :group 'css)
93
94 (defcustom cssm-indent-function #'cssm-c-style-indenter
95   "*Which function to use when deciding which column to indent to.
96 Built-in choices are `cssm-old-style-indenter' and `cssm-c-style-indenter'."
97   :type 'function
98   :group 'css)
99   
100 ; The rest of the code:
101
102 (defvar cssm-properties
103   '(
104     "ascent"                        ;   2
105     "azimuth"                       ;   2
106     "background"                    ; 1 2
107     "background-attachment"         ; 1 2
108     "background-color"              ; 1 2
109     "background-image"              ; 1 2
110     "background-position"           ; 1 2
111     "background-repeat"             ; 1 2
112     "baseline"                      ;   2
113     "bbox"                          ;   2
114     "border"                        ; 1 2
115     "border-bottom"                 ; 1 2
116     "border-bottom-color"           ;   2
117     "border-bottom-style"           ;   2
118     "border-bottom-width"           ; 1 2
119     "border-color"                  ; 1 2
120     "border-collapse"               ;   2
121     "border-left"                   ; 1 2
122     "border-left-color"             ;   2
123     "border-left-style"             ;   2
124     "border-left-width"             ; 1 2
125     "border-right"                  ; 1 2
126     "border-right-color"            ;   2
127     "border-right-style"            ;   2
128     "border-right-width"            ; 1 2
129     "border-spacing"                ;   2
130     "border-style"                  ; 1 2
131     "border-top"                    ; 1 2
132     "border-top-color"              ;   2
133     "border-top-style"              ;   2
134     "border-top-width"              ; 1 2
135     "border-width"                  ; 1 2
136     "bottom"                        ;   2
137     "cap-height"                    ;   2
138     "caption-size"                  ;   2
139     ;"cell-spacing"                 ;   2         removed in PR-CSS2-19980324
140     "centerline"                    ;   2
141     "clear"                         ; 1 2
142     "clip"                          ;   2
143     "color"                         ; 1 2
144     ;"column-span"                  ;   2         removed in REC-CSS2-19980512
145     "content"                       ;   2
146     "counter-increment"             ;   2
147     "counter-reset"                 ;   2
148     "cue"                           ;   2
149     "cue-after"                     ;   2
150     "cue-before"                    ;   2
151     "cursor"                        ;   2
152     "definition-src"                ;   2
153     "descent"                       ;   2
154     "direction"                     ;   2
155     "display"                       ; 1 2
156     "elevation"                     ;   2
157     "empty-cells"                   ;   2
158     "float"                         ; 1 2
159     "font"                          ; 1 2
160     "font-family"                   ; 1 2
161     "font-size"                     ; 1 2
162     "font-size-adjust"              ;   2
163     "font-stretch"                  ;   2
164     "font-style"                    ; 1 2
165     "font-variant"                  ; 1 2
166     "font-weight"                   ; 1 2
167     "height"                        ; 1 2
168     "left"                          ;   2
169     "letter-spacing"                ; 1 2
170     "line-height"                   ; 1 2
171     "list-style"                    ; 1 2
172     "list-style-image"              ; 1 2
173     "list-style-position"           ; 1 2
174     "list-style-type"               ; 1 2
175     "margin"                        ; 1 2
176     "margin-bottom"                 ; 1 2
177     "margin-left"                   ; 1 2
178     "margin-right"                  ; 1 2
179     "margin-top"                    ; 1 2
180     "marker-offset"                 ;   2
181     "marks"                         ;   2
182     "mathline"                      ;   2
183     "max-height"                    ;   2
184     "max-width"                     ;   2
185     "min-height"                    ;   2
186     "min-width"                     ;   2
187     "orphans"                       ;   2
188     "outline"                       ;   2
189     "outline-color"                 ;   2
190     "outline-style"                 ;   2
191     "outline-width"                 ;   2
192     "overflow"                      ;   2
193     "padding"                       ; 1 2
194     "padding-bottom"                ; 1 2
195     "padding-left"                  ; 1 2
196     "padding-right"                 ; 1 2
197     "padding-top"                   ; 1 2
198     "page"                          ;   2
199     "page-break-after"              ;   2
200     "page-break-before"             ;   2
201     "page-break-inside"             ;   2
202     "panose-1"                      ;   2
203     "pause"                         ;   2
204     "pause-after"                   ;   2
205     "pause-before"                  ;   2
206     "pitch"                         ;   2
207     "pitch-range"                   ;   2
208     "play-during"                   ;   2
209     "position"                      ;   2
210     "quotes"                        ;   2
211     "richness"                      ;   2
212     "right"                         ;   2
213     ;"row-span"                     ;   2         removed in REC-CSS2-19980512
214     "size"                          ;   2
215     "slope"                         ;   2
216     "speak"                         ;   2
217     ;"speak-date"                   ;   2         removed in PR-CSS2-19980324
218     "speak-header"                  ;   2
219     "speak-numeral"                 ;   2
220     "speak-punctuation"             ;   2
221     ;"speak-time"                   ;   2         removed in PR-CSS2-19980324
222     "speech-rate"                   ;   2
223     "src"                           ;   2
224     "stemh"                         ;   2
225     "stemv"                         ;   2
226     "stress"                        ;   2
227     "table-layout"                  ;   2
228     "text-align"                    ; 1 2
229     "text-decoration"               ; 1 2
230     "text-indent"                   ; 1 2
231     "text-shadow"                   ;   2
232     "text-transform"                ; 1 2
233     "top"                           ;   2
234     "topline"                       ;   2
235     "unicode-bidi"                  ;   2
236     "unicode-range"                 ;   2
237     "units-per-em"                  ;   2
238     "vertical-align"                ; 1 2
239     "visibility"                    ;   2
240     "voice-family"                  ;   2
241     "volume"                        ;   2
242     "white-space"                   ; 1 2
243     "widows"                        ;   2
244     "width"                         ; 1 2
245     "widths"                        ;   2
246     "word-spacing"                  ; 1 2
247     "x-height"                      ;   2
248     "z-index"                       ;   2
249     )
250   "A list of all CSS properties and descriptors.")
251
252 (defvar cssm-properties-alist
253   (mapcar (lambda(prop)
254             (cons (concat prop ":") nil)) cssm-properties)
255   "An association list of the CSS properties for completion use.")
256
257 (defvar cssm-keywords
258   (append '(
259             "@charset"              ;   2
260             "@font-face"            ;   2
261             "@import"               ;   2
262             "!\\s-*important"       ; 1 2
263             "@media"                ;   2
264             "@page"                 ;   2
265             )
266           (mapcar (lambda(property)
267                     (concat property "\\s-*:"))
268                   cssm-properties)
269           )
270   "A list of all CSS keywords.")
271
272 (defvar cssm-pseudos '(
273     "active"                        ; 1 2
274     "after"                         ;   2
275     "before"                        ;   2
276     "first-child"                   ;   2
277     "first-letter"                  ; 1 2
278     "first-line"                    ; 1 2
279     "focus"                         ;   2
280     "hover"                         ;   2
281     "lang"                          ;   2
282     "link"                          ; 1 2
283     "visited"                       ; 1 2
284     )
285   "A list of all CSS pseudo-classes and elements.")
286
287 ; internal
288 (defun cssm-list-2-regexp(altlist)
289   "Takes a list and returns the regexp \\(elem1\\|elem2\\|...\\)"
290   (let ((regexp "\\("))
291     (mapcar (lambda(elem)
292               (setq regexp (concat regexp elem "\\|")))
293             altlist)
294     (concat (substring regexp 0 -2) ; cutting the last "\\|"
295             "\\)")
296     ))
297
298 (defvar cssm-font-lock-keywords
299   (list
300    (cons (cssm-list-2-regexp cssm-keywords) font-lock-keyword-face)
301    (cons "\\.[a-zA-Z][-a-zA-Z0-9.]+" font-lock-variable-name-face)
302    (cons (concat ":" (cssm-list-2-regexp cssm-pseudos))
303          font-lock-variable-name-face)
304    (cons "#[a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]\\([a-fA-F0-9][a-fA-F0-9][a-fA-F0-9]\\)?"
305          font-lock-reference-face)
306    (cons "\\[.*\\]" font-lock-variable-name-face)
307    (cons "#[-a-zA-Z0-9]*" font-lock-function-name-face)
308    (cons "rgb(\\s-*[0-9]+\\(\\.[0-9]+\\s-*%\\s-*\\)?\\s-*,\\s-*[0-9]+\\(\\.[0-9]+\\s-*%\\s-*\\)?\\s-*,\\s-*[0-9]+\\(\\.[0-9]+\\s-*%\\s-*\\)?\\s-*)"
309          font-lock-reference-face)
310    )
311   "Rules for highlighting CSS style sheets.")
312
313 (defvar cssm-mode-map ()
314   "Keymap used in CSS mode.")
315 (when (not cssm-mode-map)
316   (setq cssm-mode-map (make-sparse-keymap))
317   (define-key cssm-mode-map (read-kbd-macro "C-c C-c") 'cssm-insert-comment)
318   (define-key cssm-mode-map (read-kbd-macro "C-c C-u") 'cssm-insert-url)
319   (define-key cssm-mode-map (read-kbd-macro "}") 'cssm-insert-right-brace-and-indent)
320   (define-key cssm-mode-map (read-kbd-macro "M-TAB") 'cssm-complete-property))
321
322 ;;; Cross-version compatibility layer
323
324 (when (not (or (apropos-macrop 'kbd)
325                (fboundp 'kbd)))
326   (defmacro kbd (keys)
327     "Convert KEYS to the internal Emacs key representation.
328 KEYS should be a string constant in the format used for
329 saving keyboard macros (see `insert-kbd-macro')."
330     (read-kbd-macro keys)))
331
332 ;;; Auto-indentation support
333
334 ; internal
335 (defun cssm-insert-right-brace-and-indent()
336   (interactive)
337   (insert "}")
338   (cssm-indent-line))
339
340 ; internal
341 (defun cssm-inside-atmedia-rule()
342   "Decides if point is currently inside an @media rule."
343   (let ((orig-pos (point))
344         (atmedia (re-search-backward "@media" 0 t))
345         (balance 1)   ; used to keep the {} balance, 1 because we start on a {
346         )
347     ; Going to the accompanying {
348     (re-search-forward "{" (point-max) t)
349     (if (null atmedia)
350         nil  ; no @media before this point => not inside
351       (while (and (< (point) orig-pos)
352                   (< 0 balance))
353         (if (null (re-search-forward "[{}]" (point-max) 0))
354             (goto-char (point-max)) ; break
355           (setq balance
356                 (if (string= (match-string 0) "{")
357                     (+ balance 1)
358                   (- balance 1)))))
359       (= balance 1))
360     ))
361
362 ; internal
363 (defun cssm-rule-is-atmedia()
364   "Decides if point is currently on the { of an @media or ordinary style rule."
365   (let ((result (re-search-backward "[@}{]" 0 t)))
366     (if (null result)
367         nil
368       (string= (match-string 0) "@"))))
369
370 ; internal
371 (defun cssm-find-column(first-char)
372   "Find which column to indent to." 
373
374   ; Find out where to indent to by looking at previous lines
375   ; spinning backwards over comments
376   (let (pos)
377     (while (and (setq pos (re-search-backward (cssm-list-2-regexp
378                                                '("/\\*" "\\*/" "{" "}"))
379                                               (point-min) t))
380                 (string= (match-string 0) "*/"))
381       (search-backward "/*" (point-min) t))
382
383     ; did the last search find anything?
384     (if pos
385         (save-excursion
386           (let ((construct      (match-string 0))
387                 (column         (current-column)))
388             (apply cssm-indent-function
389                    (list (cond
390                           ((string= construct "{")
391                            (cond
392                             ((cssm-rule-is-atmedia)
393                              'inside-atmedia)
394                             ((cssm-inside-atmedia-rule)
395                              'inside-rule-and-atmedia)
396                             (t
397                              'inside-rule)))
398                           ((string= construct "/*")
399                            'inside-comment)
400                           ((string= construct "}")
401                            (if (cssm-inside-atmedia-rule)
402                                'inside-atmedia
403                              'outside))
404                           (t 'outside))
405                          column
406                          first-char))))
407       
408       (apply cssm-indent-function
409              (list 'outside
410                    (current-column)
411                    first-char)))))
412
413 (defun cssm-indent-line()
414   "Indents the current line."
415   (interactive)
416   (beginning-of-line)
417   (let* ((beg-of-line (point))
418          (pos (re-search-forward "[]@#a-zA-Z0-9;,.\"{}/*\n:[]" (point-max) t))
419          (first (match-string 0))
420          (start (match-beginning 0)))
421
422     (goto-char beg-of-line)
423
424     (let ((indent-column (cssm-find-column first)))
425       (goto-char beg-of-line)
426
427       ; Remove all leading whitespace on this line (
428       (if (not (or (null pos)
429                    (= beg-of-line start)))
430           (kill-region beg-of-line start))
431
432       (goto-char beg-of-line)
433     
434       ; Indent
435       (indent-to indent-column))))
436
437 ;;; Indent-style functions
438
439 (defun cssm-old-style-indenter(position column first-char-on-line)
440   "Old-style indentation for CSS buffers."
441   (cond
442    ((eq position 'inside-atmedia)
443     (if (string= "}" first-char-on-line)
444         0
445       cssm-indent-level))
446    
447    ((eq position 'inside-rule)
448     (+ column 2))
449
450    ((eq position 'inside-rule-and-atmedia)
451     (+ column 2))
452
453    ((eq position 'inside-comment)
454     (+ column 3))
455
456    ((eq position 'outside)
457     0)))
458
459 (defun cssm-c-style-indenter(position column first-char-on-line)
460   "C-style indentation for CSS buffers."
461   (cond
462    ((or (eq position 'inside-atmedia)
463         (eq position 'inside-rule))
464     (if (string= "}" first-char-on-line)
465         0
466       cssm-indent-level))
467
468    ((eq position 'inside-rule-and-atmedia)
469     (if (string= "}" first-char-on-line)
470         cssm-indent-level
471       (* 2 cssm-indent-level)))
472
473    ((eq position 'inside-comment)
474     (+ column 3))
475
476    ((eq position 'outside)
477     0)))
478
479 ;;; Typing shortcuts
480
481 (define-skeleton cssm-insert-curlies
482   "Inserts a pair of matching curly parenthesises." nil
483   "{ " _ (if cssm-newline-before-closing-bracket "\n" " ")
484   "}")
485
486 (define-skeleton cssm-insert-quotes
487   "Inserts a pair of matching quotes." nil
488   "\"" _ "\"")
489
490 (define-skeleton cssm-insert-parenthesises
491   "Inserts a pair of matching parenthesises." nil
492   "(" _ ")")
493
494 (define-skeleton cssm-insert-comment
495   "Inserts a full comment." nil
496   "/* " _ " */")
497
498 (define-skeleton cssm-insert-url
499   "Inserts a URL." nil
500   "url(" _ ")")
501
502 (define-skeleton cssm-insert-brackets
503   "Inserts a pair of matching brackets." nil
504   "[" _ "]")
505
506 (defun cssm-enter-mirror-mode()
507   "Turns on mirror mode, where quotes, brackets etc are mirrored automatically
508    on insertion."
509   (interactive)
510   (define-key cssm-mode-map (read-kbd-macro "{")  'cssm-insert-curlies)
511   (define-key cssm-mode-map (read-kbd-macro "\"") 'cssm-insert-quotes)
512   (define-key cssm-mode-map (read-kbd-macro "(")  'cssm-insert-parenthesises)
513   (define-key cssm-mode-map (read-kbd-macro "[")  'cssm-insert-brackets))
514
515 (defun cssm-leave-mirror-mode()
516   "Turns off mirror mode."
517   (interactive)
518   (define-key cssm-mode-map (read-kbd-macro "{")  'self-insert-command)
519   (define-key cssm-mode-map (read-kbd-macro "\"") 'self-insert-command)
520   (define-key cssm-mode-map (read-kbd-macro "(")  'self-insert-command)
521   (define-key cssm-mode-map (read-kbd-macro "[")  'self-insert-command))
522
523 ;;; Property completion
524
525 (defun cssm-property-at-point()
526   "If point is at the end of a property name: returns it."
527   (let ((end (point))
528         (start (+ (re-search-backward "[^-A-Za-z]") 1)))
529     (goto-char end)
530     (buffer-substring start end)))
531
532 ; internal
533 (defun cssm-maximum-common(alt1 alt2)
534   "Returns the maximum common starting substring of alt1 and alt2."
535   (let* ((maxlen (min (length alt1) (length alt2)))
536          (alt1 (substring alt1 0 maxlen))
537          (alt2 (substring alt2 0 maxlen)))
538     (while (not (string= (substring alt1 0 maxlen)
539                          (substring alt2 0 maxlen)))
540       (setq maxlen (- maxlen 1)))
541     (substring alt1 0 maxlen)))
542
543 ; internal
544 (defun cssm-common-beginning(alts)
545   "Returns the maximum common starting substring of all alts elements."
546   (let ((common (car alts)))
547     (dolist (alt (cdr alts) common)
548       (setq common (cssm-maximum-common alt common)))))
549
550 (defun cssm-complete-property-frame(completions)
551   ; This code stolen from message.el. Kudos to larsi.
552   (let ((cur (current-buffer)))
553     (pop-to-buffer "*Completions*")
554     (buffer-disable-undo (current-buffer))
555     (let ((buffer-read-only nil))
556       (erase-buffer)
557       (let ((standard-output (current-buffer)))
558         (display-completion-list (sort completions 'string<)))
559       (goto-char (point-min))
560       (pop-to-buffer cur))))
561
562 (defun cssm-complete-property()
563   "Completes the CSS property being typed at point."
564   (interactive)
565   (let* ((prop   (cssm-property-at-point))
566          (alts   (all-completions prop cssm-properties-alist))
567          (proplen (length prop)))
568     (if (= (length alts) 1)
569         (insert (substring (car alts) proplen))
570       (let ((beg (cssm-common-beginning alts)))
571         (if (not (string= beg prop))
572             (insert (substring beg proplen))
573           (insert (substring
574                    (completing-read "Property: " cssm-properties-alist nil
575                                     nil prop)
576                    proplen)))))))
577
578 ;;;###autoload
579 (defun css-mode()
580   "Major mode for editing CSS style sheets.
581 \\{cssm-mode-map}"
582   (interactive)
583
584   ; Initializing
585   (kill-all-local-variables)
586
587   ; Setting up indentation handling
588   (make-local-variable 'indent-line-function)
589   (setq indent-line-function 'cssm-indent-line)
590   
591   ; Setting up font-locking
592   (make-local-variable 'font-lock-defaults)
593   (setq font-lock-defaults '(cssm-font-lock-keywords nil t nil nil))
594
595   ; Setting up typing shortcuts
596   (make-local-variable 'skeleton-end-hook)
597   (setq skeleton-end-hook nil)
598   
599   (when cssm-mirror-mode
600     (cssm-enter-mirror-mode))
601   
602   (use-local-map cssm-mode-map)
603   
604   ; Setting up syntax recognition
605   (make-local-variable 'comment-start)
606   (make-local-variable 'comment-end)
607   (make-local-variable 'comment-start-skip)
608
609   (setq comment-start "/* "
610         comment-end " */"
611         comment-start-skip "/\\*[ \n\t]+")
612
613   ; Setting up syntax table
614   (modify-syntax-entry ?* ". 23")
615   (modify-syntax-entry ?/ ". 14")
616   
617   ; Final stuff, then we're done
618   (setq mode-name "CSS"
619         major-mode 'css-mode)
620   (run-hooks 'css-mode-hook))
621
622 ;;;###autoload(add-to-list 'auto-mode-alist '("\\.css$" . css-mode))
623
624 (provide 'css-mode)
625
626 ;; CSS-mode ends here