;;;; A major mode for editing CSS.

;;; Adds font locking, some rather primitive indentation handling and
;;; some typing help.
;;;

(defvar cssm-version "0.11"
  "The current version number of `css-mode'.")

;;; copyright (c) 1998 Lars Marius Garshol, larsga@ifi.uio.no
;;; $Id: css-mode.el,v 1.9 2000/01/05 21:21:56 larsga Exp $

;;; css-mode is free software; you can redistribute it and/or
;;; modify it under the terms of the GNU General Public License
;;; as published by the Free Software Foundation; either version 2
;;; of the License, or (at your option) any later version.
;;;
;;; css-mode is distributed in the hope that it will be useful,
;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with css-mode; if not, write to the Free Software
;;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

; Send me an email if you want new features (or if you add them yourself).
; I will do my best to preserve the API to functions not explicitly marked
; as internal and variables shown as customizable. I make no promises about
; the rest.

; Bug reports are very welcome. New versions of the package will appear at
; http://www.garshol.priv.no/download/software/css-mode/
; You can register at the same address if you want to be notified when a
; new version appears.

; Thanks to Philippe Le Hegaret, Kjetil Kjernsmo, Alf-Ivar Holm and
; Alfred Correira for much useful feedback. Alf-Ivar Holm also contributed
; patches.

; This version should be up to date with the following:
; CSS1: http://www.w3.org/TR/1999/REC-CSS1-19990111
; CSS2: http://www.w3.org/TR/1998/REC-CSS2-19980512

; To install, put this in your .emacs:
;
; (autoload 'css-mode "css-mode")
; (setq auto-mode-alist       
;      (cons '("\\.css\\'" . css-mode) auto-mode-alist))

;; Todo:

; - must not color URL file name extensions as class selectors (*.css)
; - color [] and url() constructs correctly, even if quoted strings present
; - disregard anything inside strings

;; Possible later additions:
;
; - forward/backward style/@media rule commands
; - more complete syntax table

;; Required modules

(require 'apropos)
(require 'font-lock)
(require 'cl)

;;; The code itself

; Customizable variables:

(defgroup css nil
  "Major mode for editing CSS files."
  :group 'languages
  :group 'hypermedia
  :prefix "cssm-")

(defcustom cssm-indent-level 2
  "*The indentation level inside @media rules."
  :type 'integer
  :group 'css)

(defcustom cssm-mirror-mode t
  "*Whether brackets, quotes etc should be mirrored automatically on insertion."
  :type 'boolean
  :group 'css)

(defcustom cssm-newline-before-closing-bracket nil
  "*In `cssm-mirror-mode', controls whether a newline should be inserted
before the closing bracket or not."
  :type 'boolean
  :group 'css)

(defcustom cssm-indent-function #'cssm-c-style-indenter
  "*Which function to use when deciding which column to indent to.
Built-in choices are `cssm-old-style-indenter' and `cssm-c-style-indenter'."
  :type 'function
  :group 'css)
  
; The rest of the code:

(defvar cssm-properties
  '(
    "ascent"                        ;   2
    "azimuth"                       ;   2
    "background"                    ; 1 2
    "background-attachment"         ; 1 2
    "background-color"              ; 1 2
    "background-image"              ; 1 2
    "background-position"           ; 1 2
    "background-repeat"             ; 1 2
    "baseline"                      ;   2
    "bbox"                          ;   2
    "border"                        ; 1 2
    "border-bottom"                 ; 1 2
    "border-bottom-color"           ;   2
    "border-bottom-style"           ;   2
    "border-bottom-width"           ; 1 2
    "border-color"                  ; 1 2
    "border-collapse"               ;   2
    "border-left"                   ; 1 2
    "border-left-color"             ;   2
    "border-left-style"             ;   2
    "border-left-width"             ; 1 2
    "border-right"                  ; 1 2
    "border-right-color"            ;   2
    "border-right-style"            ;   2
    "border-right-width"            ; 1 2
    "border-spacing"                ;   2
    "border-style"                  ; 1 2
    "border-top"                    ; 1 2
    "border-top-color"              ;   2
    "border-top-style"              ;   2
    "border-top-width"              ; 1 2
    "border-width"                  ; 1 2
    "bottom"                        ;   2
    "cap-height"                    ;   2
    "caption-size"                  ;   2
    ;"cell-spacing"                 ;   2         removed in PR-CSS2-19980324
    "centerline"                    ;   2
    "clear"                         ; 1 2
    "clip"                          ;   2
    "color"                         ; 1 2
    ;"column-span"                  ;   2         removed in REC-CSS2-19980512
    "content"                       ;   2
    "counter-increment"             ;   2
    "counter-reset"                 ;   2
    "cue"                           ;   2
    "cue-after"                     ;   2
    "cue-before"                    ;   2
    "cursor"                        ;   2
    "definition-src"                ;   2
    "descent"                       ;   2
    "direction"                     ;   2
    "display"                       ; 1 2
    "elevation"                     ;   2
    "empty-cells"                   ;   2
    "float"                         ; 1 2
    "font"                          ; 1 2
    "font-family"                   ; 1 2
    "font-size"                     ; 1 2
    "font-size-adjust"              ;   2
    "font-stretch"                  ;   2
    "font-style"                    ; 1 2
    "font-variant"                  ; 1 2
    "font-weight"                   ; 1 2
    "height"                        ; 1 2
    "left"                          ;   2
    "letter-spacing"                ; 1 2
    "line-height"                   ; 1 2
    "list-style"                    ; 1 2
    "list-style-image"              ; 1 2
    "list-style-position"           ; 1 2
    "list-style-type"               ; 1 2
    "margin"                        ; 1 2
    "margin-bottom"                 ; 1 2
    "margin-left"                   ; 1 2
    "margin-right"                  ; 1 2
    "margin-top"                    ; 1 2
    "marker-offset"                 ;   2
    "marks"                         ;   2
    "mathline"                      ;   2
    "max-height"                    ;   2
    "max-width"                     ;   2
    "min-height"                    ;   2
    "min-width"                     ;   2
    "orphans"                       ;   2
    "outline"                       ;   2
    "outline-color"                 ;   2
    "outline-style"                 ;   2
    "outline-width"                 ;   2
    "overflow"                      ;   2
    "padding"                       ; 1 2
    "padding-bottom"                ; 1 2
    "padding-left"                  ; 1 2
    "padding-right"                 ; 1 2
    "padding-top"                   ; 1 2
    "page"                          ;   2
    "page-break-after"              ;   2
    "page-break-before"             ;   2
    "page-break-inside"             ;   2
    "panose-1"                      ;   2
    "pause"                         ;   2
    "pause-after"                   ;   2
    "pause-before"                  ;   2
    "pitch"                         ;   2
    "pitch-range"                   ;   2
    "play-during"                   ;   2
    "position"                      ;   2
    "quotes"                        ;   2
    "richness"                      ;   2
    "right"                         ;   2
    ;"row-span"                     ;   2         removed in REC-CSS2-19980512
    "size"                          ;   2
    "slope"                         ;   2
    "speak"                         ;   2
    ;"speak-date"                   ;   2         removed in PR-CSS2-19980324
    "speak-header"                  ;   2
    "speak-numeral"                 ;   2
    "speak-punctuation"             ;   2
    ;"speak-time"                   ;   2         removed in PR-CSS2-19980324
    "speech-rate"                   ;   2
    "src"                           ;   2
    "stemh"                         ;   2
    "stemv"                         ;   2
    "stress"                        ;   2
    "table-layout"                  ;   2
    "text-align"                    ; 1 2
    "text-decoration"               ; 1 2
    "text-indent"                   ; 1 2
    "text-shadow"                   ;   2
    "text-transform"                ; 1 2
    "top"                           ;   2
    "topline"                       ;   2
    "unicode-bidi"                  ;   2
    "unicode-range"                 ;   2
    "units-per-em"                  ;   2
    "vertical-align"                ; 1 2
    "visibility"                    ;   2
    "voice-family"                  ;   2
    "volume"                        ;   2
    "white-space"                   ; 1 2
    "widows"                        ;   2
    "width"                         ; 1 2
    "widths"                        ;   2
    "word-spacing"                  ; 1 2
    "x-height"                      ;   2
    "z-index"                       ;   2
    )
  "A list of all CSS properties and descriptors.")

(defvar cssm-properties-alist
  (mapcar (lambda(prop)
            (cons (concat prop ":") nil)) cssm-properties)
  "An association list of the CSS properties for completion use.")

(defvar cssm-keywords
  (append '(
            "@charset"              ;   2
            "@font-face"            ;   2
            "@import"               ;   2
            "!\\s-*important"       ; 1 2
            "@media"                ;   2
            "@page"                 ;   2
            )
          (mapcar (lambda(property)
                    (concat property "\\s-*:"))
                  cssm-properties)
          )
  "A list of all CSS keywords.")

(defvar cssm-pseudos '(
    "active"                        ; 1 2
    "after"                         ;   2
    "before"                        ;   2
    "first-child"                   ;   2
    "first-letter"                  ; 1 2
    "first-line"                    ; 1 2
    "focus"                         ;   2
    "hover"                         ;   2
    "lang"                          ;   2
    "link"                          ; 1 2
    "visited"                       ; 1 2
    )
  "A list of all CSS pseudo-classes and elements.")

; internal
(defun cssm-list-2-regexp(altlist)
  "Takes a list and returns the regexp \\(elem1\\|elem2\\|...\\)"
  (let ((regexp "\\("))
    (mapcar (lambda(elem)
              (setq regexp (concat regexp elem "\\|")))
            altlist)
    (concat (substring regexp 0 -2) ; cutting the last "\\|"
            "\\)")
    ))

(defvar cssm-font-lock-keywords
  (list
   (cons (cssm-list-2-regexp cssm-keywords) font-lock-keyword-face)
   (cons "\\.[a-zA-Z][-a-zA-Z0-9.]+" font-lock-variable-name-face)
   (cons (concat ":" (cssm-list-2-regexp cssm-pseudos))
         font-lock-variable-name-face)
   (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]\\)?"
         font-lock-reference-face)
   (cons "\\[.*\\]" font-lock-variable-name-face)
   (cons "#[-a-zA-Z0-9]*" font-lock-function-name-face)
   (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-*)"
         font-lock-reference-face)
   )
  "Rules for highlighting CSS style sheets.")

(defvar cssm-mode-map ()
  "Keymap used in CSS mode.")
(when (not cssm-mode-map)
  (setq cssm-mode-map (make-sparse-keymap))
  (define-key cssm-mode-map (read-kbd-macro "C-c C-c") 'cssm-insert-comment)
  (define-key cssm-mode-map (read-kbd-macro "C-c C-u") 'cssm-insert-url)
  (define-key cssm-mode-map (read-kbd-macro "}") 'cssm-insert-right-brace-and-indent)
  (define-key cssm-mode-map (read-kbd-macro "M-TAB") 'cssm-complete-property))

;;; Cross-version compatibility layer

(when (not (or (apropos-macrop 'kbd)
               (fboundp 'kbd)))
  (defmacro kbd (keys)
    "Convert KEYS to the internal Emacs key representation.
KEYS should be a string constant in the format used for
saving keyboard macros (see `insert-kbd-macro')."
    (read-kbd-macro keys)))

;;; Auto-indentation support

; internal
(defun cssm-insert-right-brace-and-indent()
  (interactive)
  (insert "}")
  (cssm-indent-line))

; internal
(defun cssm-inside-atmedia-rule()
  "Decides if point is currently inside an @media rule."
  (let ((orig-pos (point))
        (atmedia (re-search-backward "@media" 0 t))
        (balance 1)   ; used to keep the {} balance, 1 because we start on a {
        )
    ; Going to the accompanying {
    (re-search-forward "{" (point-max) t)
    (if (null atmedia)
        nil  ; no @media before this point => not inside
      (while (and (< (point) orig-pos)
                  (< 0 balance))
        (if (null (re-search-forward "[{}]" (point-max) 0))
            (goto-char (point-max)) ; break
          (setq balance
                (if (string= (match-string 0) "{")
                    (+ balance 1)
                  (- balance 1)))))
      (= balance 1))
    ))

; internal
(defun cssm-rule-is-atmedia()
  "Decides if point is currently on the { of an @media or ordinary style rule."
  (let ((result (re-search-backward "[@}{]" 0 t)))
    (if (null result)
        nil
      (string= (match-string 0) "@"))))

; internal
(defun cssm-find-column(first-char)
  "Find which column to indent to." 

  ; Find out where to indent to by looking at previous lines
  ; spinning backwards over comments
  (let (pos)
    (while (and (setq pos (re-search-backward (cssm-list-2-regexp
                                               '("/\\*" "\\*/" "{" "}"))
                                              (point-min) t))
                (string= (match-string 0) "*/"))
      (search-backward "/*" (point-min) t))

    ; did the last search find anything?
    (if pos
        (save-excursion
          (let ((construct      (match-string 0))
                (column         (current-column)))
            (apply cssm-indent-function
                   (list (cond
                          ((string= construct "{")
                           (cond
                            ((cssm-rule-is-atmedia)
                             'inside-atmedia)
                            ((cssm-inside-atmedia-rule)
                             'inside-rule-and-atmedia)
                            (t
                             'inside-rule)))
                          ((string= construct "/*")
                           'inside-comment)
                          ((string= construct "}")
                           (if (cssm-inside-atmedia-rule)
                               'inside-atmedia
                             'outside))
                          (t 'outside))
                         column
                         first-char))))
      
      (apply cssm-indent-function
             (list 'outside
                   (current-column)
                   first-char)))))

(defun cssm-indent-line()
  "Indents the current line."
  (interactive)
  (beginning-of-line)
  (let* ((beg-of-line (point))
         (pos (re-search-forward "[]@#a-zA-Z0-9;,.\"{}/*\n:[]" (point-max) t))
         (first (match-string 0))
         (start (match-beginning 0)))

    (goto-char beg-of-line)

    (let ((indent-column (cssm-find-column first)))
      (goto-char beg-of-line)

      ; Remove all leading whitespace on this line (
      (if (not (or (null pos)
                   (= beg-of-line start)))
          (kill-region beg-of-line start))

      (goto-char beg-of-line)
    
      ; Indent
      (while (< 0 indent-column)
        (insert " ")
        (setq indent-column (- indent-column 1))))))

;;; Indent-style functions

(defun cssm-old-style-indenter(position column first-char-on-line)
  "Old-style indentation for CSS buffers."
  (cond
   ((eq position 'inside-atmedia)
    (if (string= "}" first-char-on-line)
        0
      cssm-indent-level))
   
   ((eq position 'inside-rule)
    (+ column 2))

   ((eq position 'inside-rule-and-atmedia)
    (+ column 2))

   ((eq position 'inside-comment)
    (+ column 3))

   ((eq position 'outside)
    0)))

(defun cssm-c-style-indenter(position column first-char-on-line)
  "C-style indentation for CSS buffers."
  (cond
   ((or (eq position 'inside-atmedia)
        (eq position 'inside-rule))
    (if (string= "}" first-char-on-line)
        0
      cssm-indent-level))

   ((eq position 'inside-rule-and-atmedia)
    (if (string= "}" first-char-on-line)
        cssm-indent-level
      (* 2 cssm-indent-level)))

   ((eq position 'inside-comment)
    (+ column 3))

   ((eq position 'outside)
    0)))

;;; Typing shortcuts

(define-skeleton cssm-insert-curlies
  "Inserts a pair of matching curly parenthesises." nil
  "{ " _ (if cssm-newline-before-closing-bracket "\n" " ")
  "}")

(define-skeleton cssm-insert-quotes
  "Inserts a pair of matching quotes." nil
  "\"" _ "\"")

(define-skeleton cssm-insert-parenthesises
  "Inserts a pair of matching parenthesises." nil
  "(" _ ")")

(define-skeleton cssm-insert-comment
  "Inserts a full comment." nil
  "/* " _ " */")

(define-skeleton cssm-insert-url
  "Inserts a URL." nil
  "url(" _ ")")

(define-skeleton cssm-insert-brackets
  "Inserts a pair of matching brackets." nil
  "[" _ "]")

(defun cssm-enter-mirror-mode()
  "Turns on mirror mode, where quotes, brackets etc are mirrored automatically
   on insertion."
  (interactive)
  (define-key cssm-mode-map (read-kbd-macro "{")  'cssm-insert-curlies)
  (define-key cssm-mode-map (read-kbd-macro "\"") 'cssm-insert-quotes)
  (define-key cssm-mode-map (read-kbd-macro "(")  'cssm-insert-parenthesises)
  (define-key cssm-mode-map (read-kbd-macro "[")  'cssm-insert-brackets))

(defun cssm-leave-mirror-mode()
  "Turns off mirror mode."
  (interactive)
  (define-key cssm-mode-map (read-kbd-macro "{")  'self-insert-command)
  (define-key cssm-mode-map (read-kbd-macro "\"") 'self-insert-command)
  (define-key cssm-mode-map (read-kbd-macro "(")  'self-insert-command)
  (define-key cssm-mode-map (read-kbd-macro "[")  'self-insert-command))

;;; Property completion

(defun cssm-property-at-point()
  "If point is at the end of a property name: returns it."
  (let ((end (point))
        (start (+ (re-search-backward "[^-A-Za-z]") 1)))
    (goto-char end)
    (buffer-substring start end)))

; internal
(defun cssm-maximum-common(alt1 alt2)
  "Returns the maximum common starting substring of alt1 and alt2."
  (let* ((maxlen (min (length alt1) (length alt2)))
         (alt1 (substring alt1 0 maxlen))
         (alt2 (substring alt2 0 maxlen)))
    (while (not (string= (substring alt1 0 maxlen)
                         (substring alt2 0 maxlen)))
      (setq maxlen (- maxlen 1)))
    (substring alt1 0 maxlen)))

; internal
(defun cssm-common-beginning(alts)
  "Returns the maximum common starting substring of all alts elements."
  (let ((common (car alts)))
    (dolist (alt (cdr alts) common)
      (setq common (cssm-maximum-common alt common)))))

(defun cssm-complete-property-frame(completions)
  ; This code stolen from message.el. Kudos to larsi.
  (let ((cur (current-buffer)))
    (pop-to-buffer "*Completions*")
    (buffer-disable-undo (current-buffer))
    (let ((buffer-read-only nil))
      (erase-buffer)
      (let ((standard-output (current-buffer)))
        (display-completion-list (sort completions 'string<)))
      (goto-char (point-min))
      (pop-to-buffer cur))))

(defun cssm-complete-property()
  "Completes the CSS property being typed at point."
  (interactive)
  (let* ((prop   (cssm-property-at-point))
         (alts   (all-completions prop cssm-properties-alist))
         (proplen (length prop)))
    (if (= (length alts) 1)
        (insert (substring (car alts) proplen))
      (let ((beg (cssm-common-beginning alts)))
        (if (not (string= beg prop))
            (insert (substring beg proplen))
          (insert (substring
                   (completing-read "Property: " cssm-properties-alist nil
                                    nil prop)
                   proplen)))))))

;;;###autoload
(defun css-mode()
  "Major mode for editing CSS style sheets.
\\{cssm-mode-map}"
  (interactive)

  ; Initializing
  (kill-all-local-variables)

  ; Setting up indentation handling
  (make-local-variable 'indent-line-function)
  (setq indent-line-function 'cssm-indent-line)
  
  ; Setting up font-locking
  (make-local-variable 'font-lock-defaults)
  (setq font-lock-defaults '(cssm-font-lock-keywords nil t nil nil))

  ; Setting up typing shortcuts
  (make-local-variable 'skeleton-end-hook)
  (setq skeleton-end-hook nil)
  
  (when cssm-mirror-mode
    (cssm-enter-mirror-mode))
  
  (use-local-map cssm-mode-map)
  
  ; Setting up syntax recognition
  (make-local-variable 'comment-start)
  (make-local-variable 'comment-end)
  (make-local-variable 'comment-start-skip)

  (setq comment-start "/* "
        comment-end " */"
        comment-start-skip "/\\*[ \n\t]+")

  ; Setting up syntax table
  (modify-syntax-entry ?* ". 23")
  (modify-syntax-entry ?/ ". 14")
  
  ; Final stuff, then we're done
  (setq mode-name "CSS"
        major-mode 'css-mode)
  (run-hooks 'css-mode-hook))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.css$" . css-mode))

(provide 'css-mode)

;; CSS-mode ends here
