281 lines
9.7 KiB
EmacsLisp
281 lines
9.7 KiB
EmacsLisp
;;; sass-mode.el --- Major mode for editing Sass files
|
|
|
|
;; Copyright (c) 2007, 2008 Natalie Weizenbaum
|
|
|
|
;; Author: Natalie Weizenbaum
|
|
;; URL: http://github.com/nex3/haml/tree/master
|
|
;; Package-Version: 20161006.2326
|
|
;; Version: 3.0.16
|
|
;; Created: 2007-03-15
|
|
;; By: Natalie Weizenbaum
|
|
;; Keywords: markup, language, css
|
|
;; Package-Requires: ((haml-mode "3.0.15") (cl-lib "0.5"))
|
|
|
|
;;; Commentary:
|
|
|
|
;; Because Sass's indentation schema is similar
|
|
;; to that of YAML and Python, many indentation-related
|
|
;; functions are similar to those in yaml-mode and python-mode.
|
|
|
|
;; To install, save this on your load path and add the following to
|
|
;; your .emacs file:
|
|
;;
|
|
;; (require 'sass-mode)
|
|
|
|
;; sass-mode requires haml-mode, which can be found at http://github.com/nex3/haml-mode.
|
|
|
|
;;; Code:
|
|
|
|
(require 'haml-mode)
|
|
(require 'cl-lib)
|
|
|
|
;; User definable variables
|
|
|
|
(defgroup sass nil
|
|
"Support for the Sass template language."
|
|
:group 'languages
|
|
:prefix "sass-")
|
|
|
|
(defcustom sass-mode-hook nil
|
|
"Hook run when entering Sass mode."
|
|
:type 'hook
|
|
:group 'sass)
|
|
|
|
(defcustom sass-indent-offset 2
|
|
"Amount of offset per level of indentation."
|
|
:type 'integer
|
|
:group 'sass)
|
|
|
|
(defcustom sass-command-options nil
|
|
"Options to pass to the `sass' command."
|
|
:type '(repeat string)
|
|
:group 'sass)
|
|
|
|
(defcustom sass-before-eval-hook nil
|
|
"Hook run in the buffer used as input to the `sass' command."
|
|
:type 'hook
|
|
:group 'sass)
|
|
|
|
(defvar sass-non-block-openers
|
|
'("^.*,$" ;; Continued selectors
|
|
"^ *@\\(extend\\|debug\\|warn\\|include\\|import\\)" ;; Single-line mixins
|
|
"^ *[$!]" ;; Variables
|
|
"^ *[[:alnum:]\\-]+ *:" ;; Plain attribute values
|
|
)
|
|
"A list of regexps that match lines of Sass that couldn't have
|
|
text nested beneath them.")
|
|
|
|
;; Font lock
|
|
|
|
(defconst sass-selector-font-lock-keywords
|
|
'(;; Attribute selectors (e.g. p[foo=bar])
|
|
("\\[\\([^]=]+\\)" (1 font-lock-variable-name-face)
|
|
("[~|$^*]?=\\([^]=]+\\)" nil nil (1 font-lock-string-face)))
|
|
("&" 0 font-lock-constant-face)
|
|
("\\.\\w+" 0 font-lock-type-face)
|
|
("#\\w+" 0 font-lock-keyword-face)
|
|
;; Pseudo-selectors, optionally with arguments (e.g. :first, :nth-child(12))
|
|
("\\(::?\\w+\\)" (1 font-lock-function-name-face)
|
|
("(\\([^)]+\\))" nil nil (1 font-lock-string-face)))))
|
|
|
|
(defconst sass-script-font-lock-keywords
|
|
`(("\"\\([^\"\\\\]\\|\\\\.\\)*\"" 0 font-lock-string-face)
|
|
("!\\(\\w\\|_\\)+" 0 font-lock-variable-name-face)
|
|
("#[0-9a-fA-F]\\{0,6\\}" 0 font-lock-preprocessor-face)
|
|
(,(regexp-opt
|
|
'("true" "false" "black" "silver" "gray" "white" "maroon" "red"
|
|
"purple" "fuchsia" "green" "lime" "olive" "yellow" "navy"
|
|
"blue" "teal" "aqua"))
|
|
0 font-lock-constant-face)
|
|
(,(regexp-opt '("and" "or" "not")) 0 font-lock-keyword-face)))
|
|
|
|
(defconst sass-syntax-table
|
|
(let ((st (make-syntax-table)))
|
|
(modify-syntax-entry ?- "w" st)
|
|
(modify-syntax-entry ?_ "w" st)
|
|
st))
|
|
|
|
(defconst sass-script-syntax-table
|
|
(let ((st (make-syntax-table sass-syntax-table)))
|
|
(modify-syntax-entry ?- "." st)
|
|
st))
|
|
|
|
(defconst sass-font-lock-keywords
|
|
'((sass-highlight-line 1 nil nil t)))
|
|
|
|
(defconst sass-line-keywords
|
|
'(("@\\(\\w+\\)" 0 font-lock-keyword-face sass-highlight-directive)
|
|
("/[/*].*" 0 font-lock-comment-face)
|
|
("[=+]\\w+" 0 font-lock-function-name-face sass-highlight-script-after-match)
|
|
("!\\w+" 0 font-lock-variable-name-face sass-highlight-script-after-match)
|
|
(":\\w+" 0 font-lock-variable-name-face)
|
|
("\\w+\s*:" 0 font-lock-variable-name-face)
|
|
("\\(\\w+\\)\s*=" 1 font-lock-variable-name-face sass-highlight-script-after-match)
|
|
("\\(:\\w+\\)\s*=" 1 font-lock-variable-name-face sass-highlight-script-after-match)
|
|
(".*" sass-highlight-selector))
|
|
"A list of full-line Sass syntax to highlight, used by `sass-highlight-line'.
|
|
|
|
Each item is either of the form (REGEXP SUBEXP FACE), (REGEXP FN),
|
|
or (REGEXP SUBEXP FACE FN). Each REGEXP is run successively on the
|
|
beginning of non-whitespace on the current line until one matches.
|
|
If it has SUBEXP and FACE, then SUBEXP is highlighted using FACE.
|
|
If it has FN, FN is run.")
|
|
|
|
(defun sass-highlight-line (limit)
|
|
"Highlight a single line using some Sass single-line syntax.
|
|
This syntax is taken from `sass-line-keywords'.
|
|
LIMIT is the limit of the search."
|
|
(save-match-data
|
|
(when (re-search-forward "^ *\\(.+\\)$" limit t)
|
|
(goto-char (match-beginning 1))
|
|
(dolist (keyword sass-line-keywords)
|
|
(cl-destructuring-bind
|
|
(keyword subexp-or-fn &optional face fn) keyword
|
|
(when (looking-at keyword)
|
|
(if (integerp subexp-or-fn)
|
|
(put-text-property (match-beginning subexp-or-fn)
|
|
(match-end subexp-or-fn)
|
|
'face face)
|
|
(setq fn subexp-or-fn))
|
|
(when fn (funcall fn))
|
|
(end-of-line)
|
|
(cl-return t)))))))
|
|
|
|
(defun sass-highlight-selector ()
|
|
"Highlight a CSS selector starting at `point' and ending at `end-of-line'."
|
|
(let ((font-lock-keywords sass-selector-font-lock-keywords)
|
|
font-lock-multiline)
|
|
(font-lock-fontify-region
|
|
(point) (progn (end-of-line) (point))))
|
|
t)
|
|
|
|
(defun sass-highlight-script (beg end)
|
|
"Highlight a section of SassScript between BEG and END."
|
|
(save-match-data
|
|
(with-syntax-table sass-script-syntax-table
|
|
(let ((font-lock-keywords sass-script-font-lock-keywords)
|
|
font-lock-syntax-table
|
|
font-lock-extend-region-functions)
|
|
(font-lock-fontify-region beg end)))))
|
|
|
|
(defun sass-highlight-script-after-match ()
|
|
"Highlight a section of SassScript after the last match."
|
|
(end-of-line)
|
|
(sass-highlight-script (match-end 0) (point)))
|
|
|
|
(defun sass-highlight-directive ()
|
|
"Highlight a Sass directive."
|
|
(goto-char (match-end 0))
|
|
(cl-block nil
|
|
(cl-case (intern (match-string 1))
|
|
(for
|
|
(unless (looking-at " +!\\w+") (cl-return))
|
|
(put-text-property (match-beginning 0) (match-end 0)
|
|
'face font-lock-variable-name-face)
|
|
(goto-char (match-end 0))
|
|
(unless (looking-at " +from") (cl-return))
|
|
(put-text-property (match-beginning 0) (match-end 0)
|
|
'face font-lock-keyword-face)
|
|
(goto-char (match-end 0))
|
|
(when (looking-at " +\\(.+?\\) +\\(to\\|through\\)")
|
|
(sass-highlight-script (match-beginning 1) (match-end 1))
|
|
(put-text-property (match-beginning 2) (match-end 2)
|
|
'face font-lock-keyword-face))
|
|
(sass-highlight-script-after-match))
|
|
|
|
(else
|
|
(unless (looking-at " +if") (cl-return))
|
|
(put-text-property (match-beginning 0) (match-end 0)
|
|
'face font-lock-keyword-face)
|
|
(sass-highlight-script-after-match))
|
|
|
|
((if while debug) (sass-highlight-script-after-match)))))
|
|
|
|
;; Constants
|
|
|
|
;; Mode setup
|
|
|
|
(defvar sass-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map "\C-c\C-r" 'sass-output-region)
|
|
(define-key map "\C-c\C-l" 'sass-output-buffer)
|
|
map))
|
|
|
|
;;;###autoload
|
|
(define-derived-mode sass-mode haml-mode "Sass"
|
|
"Major mode for editing Sass files.
|
|
|
|
\\{sass-mode-map}"
|
|
(set-syntax-table sass-syntax-table)
|
|
(setq font-lock-extend-region-functions
|
|
'(font-lock-extend-region-wholelines font-lock-extend-region-multiline))
|
|
(set (make-local-variable 'font-lock-multiline) nil)
|
|
(set (make-local-variable 'comment-start) "/*")
|
|
(set (make-local-variable 'comment-start-skip) "/[/*]\s*")
|
|
(set (make-local-variable 'haml-indent-function) 'sass-indent-p)
|
|
(set (make-local-variable 'haml-indent-offset) sass-indent-offset)
|
|
(setq font-lock-defaults '(sass-font-lock-keywords t t)))
|
|
|
|
;; Indentation
|
|
|
|
(defun sass-indent-p ()
|
|
"Return non-nil if the current line can have lines nested beneath it."
|
|
(cl-loop for opener in sass-non-block-openers
|
|
if (looking-at opener) return nil
|
|
finally return t))
|
|
|
|
(defun sass--remove-leading-indent ()
|
|
"Reindent buffer so that the first line content begins in the first column.
|
|
This assumes that the buffer is valid SASS source, such that no
|
|
subsequent line has a lesser indent."
|
|
(let ((min-indent nil))
|
|
(goto-char (point-min))
|
|
(back-to-indentation)
|
|
(setq min-indent (1- (point)))
|
|
(indent-rigidly (point-min) (point-max) (- min-indent))))
|
|
|
|
;; Command
|
|
|
|
(defun sass-output-region (start end)
|
|
"Displays the CSS output for the current block of Sass code.
|
|
Called from a program, START and END specify the region to indent."
|
|
(interactive "r")
|
|
(let ((output-buffer "*sass-output*")
|
|
(errors-buffer "*sass-errors*")
|
|
(region-contents (buffer-substring start end)))
|
|
(let ((exit-code
|
|
(with-temp-buffer
|
|
(insert region-contents)
|
|
(newline-and-indent)
|
|
(run-hooks 'sass-before-eval-hook)
|
|
(sass--remove-leading-indent)
|
|
(shell-command-on-region (point-min)
|
|
(point-max)
|
|
(mapconcat #'identity
|
|
(append '("sass") sass-command-options '("--stdin")) " ")
|
|
output-buffer
|
|
nil
|
|
errors-buffer
|
|
t))))
|
|
|
|
(if (zerop exit-code)
|
|
(progn
|
|
(when (fboundp 'css-mode)
|
|
(with-current-buffer output-buffer
|
|
(css-mode)))
|
|
(switch-to-buffer-other-window output-buffer))
|
|
(with-current-buffer errors-buffer
|
|
(view-mode))))))
|
|
|
|
(defun sass-output-buffer ()
|
|
"Displays the CSS output for entire buffer."
|
|
(interactive)
|
|
(sass-output-region (point-min) (point-max)))
|
|
|
|
;;;###autoload
|
|
(add-to-list 'auto-mode-alist '("\\.sass\\'" . sass-mode))
|
|
|
|
;; Setup/Activation
|
|
(provide 'sass-mode)
|
|
;;; sass-mode.el ends here
|