├── .github └── workflows │ └── test.yml ├── .gitignore ├── .mailmap ├── MIT-LICENSE ├── Makefile ├── README.md └── haml-mode.el /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | emacs_version: 15 | - 25.1 16 | - 25.3 17 | - 26.1 18 | - 26.3 19 | - 27.1 20 | - 27.2 21 | - 28.1 22 | - 28.2 23 | - snapshot 24 | steps: 25 | - uses: purcell/setup-emacs@master 26 | with: 27 | version: ${{ matrix.emacs_version }} 28 | 29 | - uses: actions/checkout@v3 30 | - name: Run tests 31 | run: make 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Natalie Weizenbaum Nathan Weizenbaum 2 | Natalie Weizenbaum Nathan Weizenbaum -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2009 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EMACS ?= emacs 2 | 3 | # A space-separated list of required package names 4 | NEEDED_PACKAGES = package-lint 5 | 6 | INIT_PACKAGES="(progn \ 7 | (require 'package) \ 8 | (push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) \ 9 | (package-initialize) \ 10 | (dolist (pkg '(${NEEDED_PACKAGES})) \ 11 | (unless (package-installed-p pkg) \ 12 | (unless (assoc pkg package-archive-contents) \ 13 | (package-refresh-contents)) \ 14 | (package-install pkg))) \ 15 | )" 16 | 17 | all: compile package-lint clean-elc 18 | 19 | package-lint: 20 | ${EMACS} -Q --eval ${INIT_PACKAGES} --eval '(setq package-lint-main-file "haml-mode.el")' -batch -f package-lint-batch-and-exit *.el 21 | 22 | compile: clean-elc 23 | ${EMACS} -Q --eval ${INIT_PACKAGES} -L . -batch -f batch-byte-compile *.el 24 | 25 | clean-elc: 26 | rm -f f.elc 27 | 28 | .PHONY: all compile clean-elc package-lint 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![MELPA Status](http://melpa.org/packages/haml-mode-badge.svg)](http://melpa.org/#/haml-mode) 2 | [![MELPA Stable Status](http://stable.melpa.org/packages/haml-mode-badge.svg)](http://stable.melpa.org/#/haml-mode) 3 | [![Build Status](https://github.com/nex3/haml-mode/actions/workflows/test.yml/badge.svg)](https://github.com/nex3/haml-mode/actions/workflows/test.yml) 4 | 5 | # haml-mode for Emacs 6 | 7 | `haml-mode` is an Emacs major mode for use with 8 | [HAML](http://haml.info/) source files. 9 | 10 | It provides syntax highlighting and support for syntax-aware 11 | indentation. 12 | 13 | Support for syntax checking is built into [Flycheck](https://github.com/flycheck/flycheck). 14 | For `flymake`, see [flymake-haml](https://github.com/purcell/flymake-haml). 15 | 16 | ## Installation 17 | 18 | ### Via ELPA (recommended) 19 | 20 | You can install `haml-mode` from the 21 | [MELPA](http://melpa.org). 22 | 23 | ### Manually 24 | 25 | Ensure `haml-mode.el` is in a directory on your load-path, and 26 | add the following to your `~/.emacs` or `~/.emacs.d/init.el`: 27 | 28 | ``` lisp 29 | (require 'haml-mode) 30 | ``` 31 | 32 | ## Optional functionality 33 | 34 | Certain nested `:filter` blocks are syntax-highlighted if additional 35 | libraries are available. Emacs 24's `js` library will be used for 36 | `:javascript` blocks, while `markdown-mode` and `textile-mode` will be 37 | used for `:markdown` and `:textile` blocks respectively. 38 | 39 | 40 | ## About 41 | 42 | Author: Natalie Weizenbaum 43 | 44 | Maintainer: [Steve Purcell](https://github.com/purcell) 45 | 46 | Homepage: https://github.com/nex3/haml-mode 47 | -------------------------------------------------------------------------------- /haml-mode.el: -------------------------------------------------------------------------------- 1 | ;;; haml-mode.el --- Major mode for editing Haml files 2 | 3 | ;; Copyright (c) 2007, 2008 Natalie Weizenbaum 4 | 5 | ;; Author: Natalie Weizenbaum 6 | ;; URL: https://github.com/nex3/haml-mode 7 | ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5")) 8 | ;; Package-Version: 3.2.1 9 | ;; Created: 2007-03-08 10 | ;; By: Natalie Weizenbaum 11 | ;; Keywords: markup, languages, html 12 | 13 | ;;; Commentary: 14 | 15 | ;; Because Haml's indentation schema is similar 16 | ;; to that of YAML and Python, many indentation-related 17 | ;; functions are similar to those in yaml-mode and python-mode. 18 | 19 | ;; To install, save this on your load path and add the following to 20 | ;; your .emacs file: 21 | ;; 22 | ;; (require 'haml-mode) 23 | 24 | ;;; Code: 25 | 26 | (require 'cl-lib) 27 | (require 'ruby-mode) 28 | 29 | ;; Additional (optional) libraries for fontification 30 | (require 'css-mode nil t) 31 | (require 'textile-mode nil t) 32 | (require 'markdown-mode nil t) 33 | (require 'js nil t) 34 | 35 | ;; User definable variables 36 | 37 | (defgroup haml nil 38 | "Support for the Haml template language." 39 | :group 'languages 40 | :prefix "haml-") 41 | 42 | (defcustom haml-mode-hook nil 43 | "Hook run when entering Haml mode." 44 | :type 'hook 45 | :group 'haml) 46 | 47 | (defcustom haml-indent-offset 2 48 | "Amount of offset per level of indentation." 49 | :type 'integer 50 | :group 'haml) 51 | 52 | (defcustom haml-backspace-backdents-nesting t 53 | "Non-nil to have `haml-electric-backspace' re-indent blocks of code. 54 | This means that all code nested beneath the backspaced line is 55 | re-indented along with the line itself." 56 | :type 'boolean 57 | :group 'haml) 58 | 59 | (defvar haml-indent-function 'haml-indent-p 60 | "A function for checking if nesting is allowed. 61 | This function should look at the current line and return t 62 | if the next line could be nested within this line. 63 | 64 | The function can also return a positive integer to indicate 65 | a specific level to which the current line could be indented.") 66 | 67 | (defconst haml-tag-beg-re 68 | "^[ \t]*\\([%\\.#][a-z0-9_:\\-]+\\)+\\(?:(.*)\\|{.*}\\|\\[.*\\]\\)*" 69 | "A regexp matching the beginning of a Haml tag, through (), {}, and [].") 70 | 71 | (defvar haml-block-openers 72 | `(,(concat haml-tag-beg-re "[><]*[ \t]*$") 73 | "^[ \t]*[&!]?[-=~].*do[ \t]*\\(|.*|[ \t]*\\)?$" 74 | ,(concat "^[ \t]*[&!]?[-=~][ \t]*\\(" 75 | (regexp-opt '("if" "unless" "while" "until" "else" "for" 76 | "begin" "elsif" "rescue" "ensure" "when")) 77 | "\\)") 78 | "^[ \t]*/\\(\\[.*\\]\\)?[ \t]*$" 79 | "^[ \t]*-#" 80 | "^[ \t]*:") 81 | "A list of regexps that match lines of Haml that open blocks. 82 | That is, a Haml line that can have text nested beneath it should 83 | be matched by a regexp in this list.") 84 | 85 | 86 | ;; Font lock 87 | 88 | (defun haml-nested-regexp (re) 89 | "Create a regexp to match a block starting with RE. 90 | The line containing RE is matched, as well as all lines indented beneath it." 91 | (concat "^\\([ \t]*\\)\\(" re "\\)\\([ \t]*\\(?:\n\\(?:\\1 +[^\n]*\\)?\\)*\n?\\)$")) 92 | 93 | (defconst haml-font-lock-keywords 94 | `((haml-highlight-interpolation 1 font-lock-variable-name-face prepend) 95 | (haml-highlight-ruby-tag 1 font-lock-preprocessor-face) 96 | (haml-highlight-ruby-script 1 font-lock-preprocessor-face) 97 | ;; TODO: distinguish between "/" comments, which can contain HAML 98 | ;; output directives, and "-#", which are completely ignored 99 | haml-highlight-comment 100 | haml-highlight-filter 101 | ("^!!!.*" 0 font-lock-constant-face) 102 | ("\\s| *$" 0 font-lock-string-face))) 103 | 104 | (defconst haml-filter-re (haml-nested-regexp ":[[:alnum:]_\\-]+")) 105 | (defconst haml-comment-re (haml-nested-regexp "\\(?:-\\#\\|/\\)[^\n]*")) 106 | 107 | (defun haml-highlight-comment (limit) 108 | "Highlight any -# or / comment found up to LIMIT." 109 | (when (re-search-forward haml-comment-re limit t) 110 | (let ((beg (match-beginning 0)) 111 | (end (match-end 0))) 112 | (put-text-property beg end 'face 'font-lock-comment-face) 113 | (goto-char end)))) 114 | 115 | ;; Fontifying sub-regions for other languages 116 | 117 | (defun haml-fontify-region 118 | (beg end keywords syntax-table syntax-propertize-fn) 119 | "Fontify a region between BEG and END using another mode's fontification. 120 | 121 | KEYWORDS, SYNTAX-TABLE, SYNTACTIC-KEYWORDS and 122 | SYNTAX-PROPERTIZE-FN are the values of that mode's 123 | `font-lock-keywords', `font-lock-syntax-table', 124 | `font-lock-syntactic-keywords', and `syntax-propertize-function' 125 | respectively." 126 | (save-excursion 127 | (save-match-data 128 | (let ((font-lock-keywords keywords) 129 | (font-lock-syntax-table syntax-table) 130 | (syntax-propertize-function syntax-propertize-fn) 131 | (font-lock-multiline 'undecided) 132 | (font-lock-dont-widen t) 133 | font-lock-keywords-only 134 | font-lock-extend-region-functions 135 | font-lock-keywords-case-fold-search) 136 | (save-restriction 137 | (narrow-to-region (1- beg) end) 138 | ;; font-lock-fontify-region apparently isn't inclusive, 139 | ;; so we have to move the beginning back one char 140 | (font-lock-fontify-region (1- beg) end)))))) 141 | 142 | (defun haml-fontify-region-as-ruby (beg end) 143 | "Use Ruby's font-lock variables to fontify the region between BEG and END." 144 | (haml-fontify-region beg end ruby-font-lock-keywords 145 | (if (boundp 'ruby-mode-syntax-table) 146 | ruby-mode-syntax-table 147 | ruby-font-lock-syntax-table) 148 | (if (fboundp 'ruby-syntax-propertize) 149 | 'ruby-syntax-propertize 150 | 'ruby-syntax-propertize-function))) 151 | 152 | (defun haml-fontify-region-as-css (beg end) 153 | "Fontify CSS code from BEG to END. 154 | 155 | This requires that `css-mode' is available. 156 | `css-mode' is included with Emacs 23." 157 | (when (boundp 'css-font-lock-keywords) 158 | (haml-fontify-region beg end 159 | css-font-lock-keywords 160 | css-mode-syntax-table 161 | 'css-syntax-propertize-function))) 162 | 163 | (defun haml-fontify-region-as-javascript (beg end) 164 | "Fontify javascript code from BEG to END. 165 | 166 | This requires that Karl Landström's javascript mode be available, either as the 167 | \"js.el\" bundled with Emacs >= 23, or as \"javascript.el\" found in ELPA and 168 | elsewhere." 169 | (when js--font-lock-keywords-3 170 | (when (and (fboundp 'js--update-quick-match-re) 171 | (null js--quick-match-re-func)) 172 | (js--update-quick-match-re)) 173 | (haml-fontify-region beg end 174 | js--font-lock-keywords-3 175 | js-mode-syntax-table 176 | #'js-syntax-propertize))) 177 | 178 | (defun haml-fontify-region-as-textile (beg end) 179 | "Highlight textile from BEG to END. 180 | 181 | This requires that `textile-mode' be available. 182 | 183 | Note that the results are not perfect, since `textile-mode' expects 184 | certain constructs such as \"h1.\" to be at the beginning of a line, 185 | and indented Haml filters always have leading whitespace." 186 | (if (boundp 'textile-font-lock-keywords) 187 | (haml-fontify-region beg end textile-font-lock-keywords textile-mode-syntax-table nil))) 188 | 189 | (defun haml-fontify-region-as-markdown (beg end) 190 | "Highlight markdown from BEG to END. 191 | 192 | This requires that `markdown-mode' be available." 193 | (if (boundp 'markdown-mode-font-lock-keywords) 194 | (haml-fontify-region beg end 195 | markdown-mode-font-lock-keywords 196 | markdown-mode-syntax-table 197 | nil))) 198 | 199 | (defvar haml-fontify-filter-functions-alist 200 | '(("ruby" . haml-fontify-region-as-ruby) 201 | ("css" . haml-fontify-region-as-css) 202 | ("javascript" . haml-fontify-region-as-javascript) 203 | ("textile" . haml-fontify-region-as-textile) 204 | ("markdown" . haml-fontify-region-as-markdown)) 205 | "An alist of (FILTER-NAME . FUNCTION) used to fontify code regions. 206 | FILTER-NAME is a string and FUNCTION is a function which will be 207 | used to fontify the filter's indented code region. FUNCTION will 208 | be passed the extents of that region in two arguments BEG and 209 | END.") 210 | 211 | (defun haml-highlight-filter (limit) 212 | "Highlight any :filter region found in the text up to LIMIT." 213 | (when (re-search-forward haml-filter-re limit t) 214 | ;; fontify the filter name 215 | (put-text-property (match-beginning 2) (1+ (match-end 2)) 216 | 'face font-lock-preprocessor-face) 217 | (let ((filter-name (substring (match-string 2) 1)) 218 | (code-start (1+ (match-beginning 3))) 219 | (code-end (match-end 3))) 220 | (save-match-data 221 | (funcall (or (cdr (assoc filter-name haml-fontify-filter-functions-alist)) 222 | #'(lambda (beg end) 223 | (put-text-property beg end 224 | 'face 225 | 'font-lock-string-face))) 226 | code-start code-end)) 227 | (goto-char (match-end 0))))) 228 | 229 | (defconst haml-possibly-multiline-code-re 230 | "\\(\\(?:.*?,[ \t]*\n\\)*.*\\)" 231 | "Regexp to match trailing ruby code which may continue onto subsequent lines.") 232 | 233 | (defconst haml-ruby-script-re 234 | (concat "^[ \t]*\\(-\\|[&!]?\\(?:=\\|~\\)\\)[^=]" haml-possibly-multiline-code-re) 235 | "Regexp to match -, = or ~ blocks and any continued code lines.") 236 | 237 | (defun haml-highlight-ruby-script (limit) 238 | "Highlight a Ruby script expression (-, =, or ~). 239 | LIMIT works as it does in `re-search-forward'." 240 | (when (re-search-forward haml-ruby-script-re limit t) 241 | (haml-fontify-region-as-ruby (match-beginning 2) (match-end 2)))) 242 | 243 | (defun haml-move (re) 244 | "Try matching and moving to the end of regular expression RE. 245 | Returns non-nil if the expression was sucessfully matched." 246 | (when (looking-at re) 247 | (goto-char (match-end 0)) 248 | t)) 249 | 250 | (defun haml-highlight-ruby-tag (limit) 251 | "Highlight Ruby code within a Haml tag. 252 | LIMIT works as it does in `re-search-forward'. 253 | 254 | This highlights the tag attributes and object refs of the tag, 255 | as well as the script expression (-, =, or ~) following the tag. 256 | 257 | For example, this will highlight all of the following: 258 | %p{:foo => 'bar'} 259 | %p[@bar] 260 | %p= 'baz' 261 | %p{:foo => 'bar'}[@bar]= 'baz'" 262 | (when (re-search-forward "^[ \t]*[%.#]" limit t) 263 | (forward-char -1) 264 | 265 | ;; Highlight tag, classes, and ids 266 | (while (haml-move "\\([.#%]\\)[a-z0-9_:\\-]*") 267 | (put-text-property (match-beginning 0) (match-end 0) 'face 268 | (cl-case (char-after (match-beginning 1)) 269 | (?% font-lock-keyword-face) 270 | (?# font-lock-function-name-face) 271 | (?. font-lock-variable-name-face)))) 272 | 273 | (cl-block loop 274 | (while t 275 | (let ((eol (save-excursion (end-of-line) (point)))) 276 | (cl-case (char-after) 277 | ;; Highlight obj refs 278 | (?\[ 279 | (forward-char 1) 280 | (let ((beg (point))) 281 | (haml-limited-forward-sexp eol) 282 | (haml-fontify-region-as-ruby beg (point)))) 283 | ;; Highlight new attr hashes 284 | (?\( 285 | (forward-char 1) 286 | (while 287 | (and (haml-parse-new-attr-hash 288 | (lambda (type beg end) 289 | (cl-case type 290 | (name (put-text-property beg end 291 | 'face 292 | font-lock-constant-face)) 293 | (value (haml-fontify-region-as-ruby beg end))))) 294 | (not (eobp))) 295 | (forward-line 1) 296 | (beginning-of-line))) 297 | ;; Highlight old attr hashes 298 | (?\{ 299 | (let ((beg (point))) 300 | (haml-limited-forward-sexp eol) 301 | 302 | ;; Check for multiline 303 | (while (and (eolp) (eq (char-before) ?,) (not (eobp))) 304 | (forward-line) 305 | (let ((eol (save-excursion (end-of-line) (point)))) 306 | ;; If no sexps are closed, 307 | ;; we're still continuing a multiline hash 308 | (if (>= (car (parse-partial-sexp (point) eol)) 0) 309 | (end-of-line) 310 | ;; If sexps have been closed, 311 | ;; set the point at the end of the total sexp 312 | (goto-char beg) 313 | (haml-limited-forward-sexp eol)))) 314 | 315 | (haml-fontify-region-as-ruby (1+ beg) (point)))) 316 | (t (cl-return-from loop)))))) 317 | 318 | ;; Move past end chars 319 | (haml-move "[<>&!]+") 320 | ;; Highlight script 321 | (if (looking-at (concat "\\([=~]\\) " haml-possibly-multiline-code-re)) 322 | (haml-fontify-region-as-ruby (match-beginning 2) (match-end 2)) 323 | ;; Give font-lock something to highlight 324 | (forward-char -1) 325 | (looking-at "\\(\\)")) 326 | t)) 327 | 328 | (defun haml-highlight-interpolation (limit) 329 | "Highlight Ruby interpolation (#{foo}). 330 | LIMIT works as it does in `re-search-forward'." 331 | (when (re-search-forward "\\(#{\\)" limit t) 332 | (save-match-data 333 | (forward-char -1) 334 | (let ((beg (point))) 335 | (haml-limited-forward-sexp limit) 336 | (haml-fontify-region-as-ruby (1+ beg) (point))) 337 | (when (eq (char-before) ?\}) 338 | (put-text-property (1- (point)) (point) 339 | 'face font-lock-variable-name-face)) 340 | t))) 341 | 342 | (defun haml-limited-forward-sexp (limit &optional arg) 343 | "Move forward using `forward-sexp' or to LIMIT, whichever comes first. 344 | With ARG, do it that many times." 345 | (let (forward-sexp-function) 346 | (condition-case err 347 | (save-restriction 348 | (narrow-to-region (point) limit) 349 | (forward-sexp arg)) 350 | (scan-error 351 | (unless (equal (nth 1 err) "Unbalanced parentheses") 352 | (signal 'scan-error (cdr err))) 353 | (goto-char limit))))) 354 | 355 | (defun haml-find-containing-block (re) 356 | "If point is inside a block matching RE, return (start . end) for the block." 357 | (save-excursion 358 | (let ((pos (point)) 359 | start end) 360 | (beginning-of-line) 361 | (when (and 362 | (or (looking-at re) 363 | (when (re-search-backward re nil t) 364 | (looking-at re))) 365 | (< pos (match-end 0))) 366 | (setq start (match-beginning 0) 367 | end (match-end 0))) 368 | (when start 369 | (cons start end))))) 370 | 371 | (defun haml-maybe-extend-region (extender) 372 | "Maybe extend the font lock region using EXTENDER. 373 | With point at the beginning of the font lock region, EXTENDER is called. 374 | If it returns a (START . END) pair, those positions are used to possibly 375 | extend the font lock region." 376 | (let ((old-beg font-lock-beg) 377 | (old-end font-lock-end)) 378 | (save-excursion 379 | (goto-char font-lock-beg) 380 | (let ((new-bounds (funcall extender))) 381 | (when new-bounds 382 | (setq font-lock-beg (min font-lock-beg (car new-bounds)) 383 | font-lock-end (max font-lock-end (cdr new-bounds)))))) 384 | (or (/= old-beg font-lock-beg) 385 | (/= old-end font-lock-end)))) 386 | 387 | (defun haml-extend-region-nested-below () 388 | "Extend the font-lock region to any subsequent indented lines." 389 | (haml-maybe-extend-region 390 | (lambda () 391 | (beginning-of-line) 392 | (when (looking-at (haml-nested-regexp "[^ \t].*")) 393 | (cons (match-beginning 0) (match-end 0)))))) 394 | 395 | (defun haml-extend-region-to-containing-block (re) 396 | "Extend the font-lock region to the smallest containing block matching RE." 397 | (haml-maybe-extend-region 398 | (lambda () 399 | (haml-find-containing-block re)))) 400 | 401 | (defun haml-extend-region-filter () 402 | "Extend the font-lock region to an enclosing filter." 403 | (haml-extend-region-to-containing-block haml-filter-re)) 404 | 405 | (defun haml-extend-region-comment () 406 | "Extend the font-lock region to an enclosing comment." 407 | (haml-extend-region-to-containing-block haml-comment-re)) 408 | 409 | (defun haml-extend-region-ruby-script () 410 | "Extend the font-lock region to encompass any current -/=/~ line." 411 | (haml-extend-region-to-containing-block haml-ruby-script-re)) 412 | 413 | (defun haml-extend-region-multiline-hashes () 414 | "Extend the font-lock region to encompass multiline attribute hashes." 415 | (haml-maybe-extend-region 416 | (lambda () 417 | (let ((attr-props (haml-parse-multiline-attr-hash)) 418 | multiline-end 419 | start) 420 | (when attr-props 421 | (setq start (cdr (assq 'point attr-props))) 422 | 423 | (end-of-line) 424 | ;; Move through multiline attrs 425 | (when (eq (char-before) ?,) 426 | (save-excursion 427 | (while (progn (end-of-line) 428 | (and (eq (char-before) ?,) (not (eobp)))) 429 | (forward-line)) 430 | 431 | (forward-line -1) 432 | (end-of-line) 433 | (setq multiline-end (point)))) 434 | 435 | (goto-char (+ (cdr (assq 'point attr-props)) 436 | (cdr (assq 'hash-indent attr-props)) 437 | -1)) 438 | (haml-limited-forward-sexp 439 | (or multiline-end 440 | (save-excursion (end-of-line) (point)))) 441 | (cons start (point))))))) 442 | 443 | (defun haml-extend-region-contextual () 444 | "Extend the font lock region piecemeal. 445 | 446 | The result of calling this function repeatedly until it returns 447 | nil is that (FONT-LOCK-BEG . FONT-LOCK-END) will be the smallest 448 | possible region in which font-locking could be affected by 449 | changes in the initial region." 450 | (or 451 | (haml-extend-region-filter) 452 | (haml-extend-region-comment) 453 | (haml-extend-region-ruby-script) 454 | (haml-extend-region-multiline-hashes) 455 | (haml-extend-region-nested-below) 456 | (font-lock-extend-region-multiline))) 457 | 458 | 459 | ;; Mode setup 460 | 461 | (defvar haml-mode-syntax-table 462 | (let ((table (make-syntax-table))) 463 | (modify-syntax-entry ?: "." table) 464 | (modify-syntax-entry ?' "\"" table) 465 | table) 466 | "Syntax table in use in `haml-mode' buffers.") 467 | 468 | (defvar haml-mode-map 469 | (let ((map (make-sparse-keymap))) 470 | (define-key map [backspace] 'haml-electric-backspace) 471 | (define-key map "\C-?" 'haml-electric-backspace) 472 | (define-key map "\C-c\C-f" 'haml-forward-sexp) 473 | (define-key map "\C-c\C-b" 'haml-backward-sexp) 474 | (define-key map "\C-c\C-u" 'haml-up-list) 475 | (define-key map "\C-c\C-d" 'haml-down-list) 476 | (define-key map "\C-c\C-k" 'haml-kill-line-and-indent) 477 | (define-key map "\C-c\C-r" 'haml-output-region) 478 | (define-key map "\C-c\C-l" 'haml-output-buffer) 479 | map)) 480 | 481 | ;;;###autoload 482 | (define-derived-mode haml-mode prog-mode "Haml" 483 | "Major mode for editing Haml files. 484 | 485 | \\{haml-mode-map}" 486 | (setq font-lock-extend-region-functions '(haml-extend-region-contextual)) 487 | (set (make-local-variable 'jit-lock-contextually) t) 488 | (set (make-local-variable 'font-lock-multiline) t) 489 | (set (make-local-variable 'indent-line-function) 'haml-indent-line) 490 | (set (make-local-variable 'indent-region-function) 'haml-indent-region) 491 | (set (make-local-variable 'parse-sexp-lookup-properties) t) 492 | (set (make-local-variable 'comment-start) "-#") 493 | (setq font-lock-defaults '((haml-font-lock-keywords) t t)) 494 | (when (boundp 'electric-indent-inhibit) 495 | (setq electric-indent-inhibit t)) 496 | (setq indent-tabs-mode nil)) 497 | 498 | ;; Useful functions 499 | 500 | (defun haml-comment-block () 501 | "Comment the current block of Haml code." 502 | (interactive) 503 | (save-excursion 504 | (let ((indent (current-indentation))) 505 | (back-to-indentation) 506 | (insert "-#") 507 | (newline) 508 | (indent-to indent) 509 | (beginning-of-line) 510 | (haml-mark-sexp) 511 | (haml-reindent-region-by haml-indent-offset)))) 512 | 513 | (defun haml-uncomment-block () 514 | "Uncomment the current block of Haml code." 515 | (interactive) 516 | (save-excursion 517 | (beginning-of-line) 518 | (while (not (looking-at haml-comment-re)) 519 | (haml-up-list) 520 | (beginning-of-line)) 521 | (haml-mark-sexp) 522 | (kill-line 1) 523 | (haml-reindent-region-by (- haml-indent-offset)))) 524 | 525 | (defun haml-replace-region (start end) 526 | "Replace the current block of Haml code with the HTML equivalent. 527 | Called from a program, START and END specify the region to indent." 528 | (interactive "r") 529 | (save-excursion 530 | (goto-char end) 531 | (setq end (point-marker)) 532 | (goto-char start) 533 | (let ((ci (current-indentation))) 534 | (while (re-search-forward "^ +" end t) 535 | (replace-match (make-string (- (current-indentation) ci) ? )))) 536 | (shell-command-on-region start end "haml" "haml-output" t))) 537 | 538 | (defun haml-output-region (start end) 539 | "Displays the HTML output for the current block of Haml code. 540 | Called from a program, START and END specify the region to indent." 541 | (interactive "r") 542 | (kill-new (buffer-substring start end)) 543 | (with-temp-buffer 544 | (yank) 545 | (haml-indent-region (point-min) (point-max)) 546 | (shell-command-on-region (point-min) (point-max) "haml" "haml-output"))) 547 | 548 | (defun haml-output-buffer () 549 | "Displays the HTML output for entire buffer." 550 | (interactive) 551 | (haml-output-region (point-min) (point-max))) 552 | 553 | ;; Navigation 554 | 555 | (defun haml-forward-through-whitespace (&optional backward) 556 | "Move the point forward through any whitespace. 557 | The point will move forward at least one line, until it reaches 558 | either the end of the buffer or a line with no whitespace. 559 | 560 | If BACKWARD is non-nil, move the point backward instead." 561 | (let ((arg (if backward -1 1)) 562 | (endp (if backward 'bobp 'eobp))) 563 | (cl-loop do (forward-line arg) 564 | while (and (not (funcall endp)) 565 | (looking-at "^[ \t]*$"))))) 566 | 567 | (defun haml-at-indent-p () 568 | "Return non-nil if the point is before any text on the line." 569 | (let ((opoint (point))) 570 | (save-excursion 571 | (back-to-indentation) 572 | (>= (point) opoint)))) 573 | 574 | (defun haml-forward-sexp (&optional arg) 575 | "Move forward across one nested expression. 576 | With ARG, do it that many times. Negative arg -N means move 577 | backward across N balanced expressions. 578 | 579 | A sexp in Haml is defined as a line of Haml code as well as any 580 | lines nested beneath it." 581 | (interactive "p") 582 | (or arg (setq arg 1)) 583 | (if (and (< arg 0) (not (haml-at-indent-p))) 584 | (back-to-indentation) 585 | (while (/= arg 0) 586 | (let ((indent (current-indentation))) 587 | (cl-loop do (haml-forward-through-whitespace (< arg 0)) 588 | while (and (not (eobp)) 589 | (not (bobp)) 590 | (> (current-indentation) indent))) 591 | (unless (eobp) 592 | (back-to-indentation)) 593 | (setq arg (+ arg (if (> arg 0) -1 1))))))) 594 | 595 | (defun haml-backward-sexp (&optional arg) 596 | "Move backward across one nested expression. 597 | With ARG, do it that many times. Negative arg -N means move 598 | forward across N balanced expressions. 599 | 600 | A sexp in Haml is defined as a line of Haml code as well as any 601 | lines nested beneath it." 602 | (interactive "p") 603 | (haml-forward-sexp (if arg (- arg) -1))) 604 | 605 | (defun haml-up-list (&optional arg) 606 | "Move out of one level of nesting. 607 | With ARG, do this that many times." 608 | (interactive "p") 609 | (or arg (setq arg 1)) 610 | (while (> arg 0) 611 | (let ((indent (current-indentation))) 612 | (cl-loop do (haml-forward-through-whitespace t) 613 | while (and (not (bobp)) 614 | (>= (current-indentation) indent))) 615 | (setq arg (1- arg)))) 616 | (back-to-indentation)) 617 | 618 | (defun haml-down-list (&optional arg) 619 | "Move down one level of nesting. 620 | With ARG, do this that many times." 621 | (interactive "p") 622 | (or arg (setq arg 1)) 623 | (while (> arg 0) 624 | (let ((indent (current-indentation))) 625 | (haml-forward-through-whitespace) 626 | (when (<= (current-indentation) indent) 627 | (haml-forward-through-whitespace t) 628 | (back-to-indentation) 629 | (error "Nothing is nested beneath this line")) 630 | (setq arg (1- arg)))) 631 | (back-to-indentation)) 632 | 633 | (defun haml-mark-sexp () 634 | "Mark the next Haml block." 635 | (let ((forward-sexp-function 'haml-forward-sexp)) 636 | (mark-sexp))) 637 | 638 | (defun haml-mark-sexp-but-not-next-line () 639 | "Mark the next Haml block, but not the next line. 640 | Put the mark at the end of the last line of the sexp rather than 641 | the first non-whitespace character of the next line." 642 | (haml-mark-sexp) 643 | (set-mark 644 | (save-excursion 645 | (goto-char (mark)) 646 | (unless (eobp) 647 | (forward-line -1) 648 | (end-of-line)) 649 | (point)))) 650 | 651 | ;; Indentation and electric keys 652 | 653 | (defvar haml-empty-elements 654 | '("area" "base" "br" "col" "command" "embed" "hr" "img" "input" 655 | "keygen" "link" "meta" "param" "source" "track" "wbr") 656 | "A list of html elements which may not contain content. 657 | 658 | See http://www.w3.org/TR/html-markup/syntax.html.") 659 | 660 | (defun haml-unnestable-tag-p () 661 | "Return t if the current line is an empty element tag, or one with content." 662 | (when (looking-at haml-tag-beg-re) 663 | (save-excursion 664 | (goto-char (match-end 0)) 665 | (or (string-match-p (concat "%" (regexp-opt haml-empty-elements) "\\b") 666 | (match-string 1)) 667 | (progn 668 | (when (looking-at "[{(]") 669 | (ignore-errors (forward-sexp))) 670 | (looking-at "\\(?:=\\|==\\| \\)[[:blank:]]*[^[:blank:]\r\n]+")))))) 671 | 672 | (defun haml-indent-p () 673 | "Return t if the current line can have lines nested beneath it." 674 | (let ((attr-props (haml-parse-multiline-attr-hash))) 675 | (if attr-props 676 | (if (haml-unclosed-attr-hash-p) 677 | (cdr (assq 'hash-indent attr-props)) 678 | (+ (cdr (assq 'indent attr-props)) haml-indent-offset)) 679 | (unless (or (haml-unnestable-tag-p)) 680 | (cl-loop for opener in haml-block-openers 681 | if (looking-at opener) return t 682 | finally return nil))))) 683 | 684 | (cl-defun haml-parse-multiline-attr-hash () 685 | "Parses a multiline attribute hash, and returns 686 | an alist with the following keys: 687 | 688 | INDENT is the indentation of the line beginning the hash. 689 | 690 | HASH-INDENT is the indentation of the first character 691 | within the attribute hash. 692 | 693 | POINT is the character position at the beginning of the line 694 | beginning the hash." 695 | (save-excursion 696 | (while t 697 | (beginning-of-line) 698 | (if (looking-at (concat haml-tag-beg-re "\\([{(]\\)")) 699 | (progn 700 | (goto-char (1- (match-end 0))) 701 | (haml-limited-forward-sexp (save-excursion (end-of-line) (point))) 702 | (cl-return-from haml-parse-multiline-attr-hash 703 | (when (or (string-equal (match-string 1) "(") (eq (char-before) ?,)) 704 | `((indent . ,(current-indentation)) 705 | (hash-indent . ,(- (match-end 0) (match-beginning 0))) 706 | (point . ,(match-beginning 0)))))) 707 | (when (bobp) (cl-return-from haml-parse-multiline-attr-hash)) 708 | (forward-line -1) 709 | (unless (haml-unclosed-attr-hash-p) 710 | (cl-return-from haml-parse-multiline-attr-hash)))))) 711 | 712 | (cl-defun haml-unclosed-attr-hash-p () 713 | "Return t if this line has an unclosed attribute hash, new or old." 714 | (save-excursion 715 | (end-of-line) 716 | (when (eq (char-before) ?,) (cl-return-from haml-unclosed-attr-hash-p t)) 717 | (re-search-backward "(\\|^") 718 | (haml-move "(") 719 | (haml-parse-new-attr-hash))) 720 | 721 | (cl-defun haml-parse-new-attr-hash (&optional (fn (lambda (type beg end) ()))) 722 | "Parse a new-style attribute hash on this line, and returns 723 | t if it's not finished on the current line. 724 | 725 | FN should take three parameters: TYPE, BEG, and END. 726 | TYPE is the type of text parsed ('name or 'value) 727 | and BEG and END delimit that text in the buffer." 728 | (let ((eol (save-excursion (end-of-line) (point)))) 729 | (while (not (haml-move ")")) 730 | (haml-move "[ \t]*") 731 | (unless (haml-move "[a-z0-9_:\\-]+") 732 | (cl-return-from haml-parse-new-attr-hash (haml-move "[ \t]*$"))) 733 | (funcall fn 'name (match-beginning 0) (match-end 0)) 734 | (haml-move "[ \t]*") 735 | (when (haml-move "=") 736 | (haml-move "[ \t]*") 737 | (unless (looking-at "[\"'@a-z0-9]") (cl-return-from haml-parse-new-attr-hash)) 738 | (let ((beg (point))) 739 | (haml-limited-forward-sexp eol) 740 | (funcall fn 'value beg (point))) 741 | (haml-move "[ \t]*"))) 742 | nil)) 743 | 744 | (defun haml-compute-indentation () 745 | "Calculate the maximum sensible indentation for the current line." 746 | (save-excursion 747 | (beginning-of-line) 748 | (if (bobp) (list 0 nil) 749 | (haml-forward-through-whitespace t) 750 | (let ((indent (funcall haml-indent-function))) 751 | (cond 752 | ((consp indent) indent) 753 | ((integerp indent) (list indent t)) 754 | (indent (list (+ (current-indentation) haml-indent-offset) nil)) 755 | (t (list (current-indentation) nil))))))) 756 | 757 | (defun haml-indent-region (start end) 758 | "Indent each nonblank line in the region. 759 | This is done by indenting the first line based on 760 | `haml-compute-indentation' and preserving the relative 761 | indentation of the rest of the region. START and END specify the 762 | region to indent. 763 | 764 | If this command is used multiple times in a row, it will cycle 765 | between possible indentations." 766 | (save-excursion 767 | (goto-char end) 768 | (setq end (point-marker)) 769 | (goto-char start) 770 | (let (this-line-column current-column 771 | (next-line-column 772 | (if (and (equal last-command this-command) (/= (current-indentation) 0)) 773 | (* (/ (1- (current-indentation)) haml-indent-offset) haml-indent-offset) 774 | (car (haml-compute-indentation))))) 775 | (while (< (point) end) 776 | (setq this-line-column next-line-column 777 | current-column (current-indentation)) 778 | ;; Delete whitespace chars at beginning of line 779 | (delete-horizontal-space) 780 | (unless (eolp) 781 | (setq next-line-column (save-excursion 782 | (cl-loop do (forward-line 1) 783 | while (and (not (eobp)) (looking-at "^[ \t]*$"))) 784 | (+ this-line-column 785 | (- (current-indentation) current-column)))) 786 | ;; Don't indent an empty line 787 | (unless (eolp) (indent-to this-line-column))) 788 | (forward-line 1))) 789 | (move-marker end nil))) 790 | 791 | (defun haml-indent-line () 792 | "Indent the current line. 793 | The first time this command is used, the line will be indented to the 794 | maximum sensible indentation. Each immediately subsequent usage will 795 | back-dent the line by `haml-indent-offset' spaces. On reaching column 796 | 0, it will cycle back to the maximum sensible indentation." 797 | (interactive "*") 798 | (let ((ci (current-indentation)) 799 | (cc (current-column))) 800 | (cl-destructuring-bind (need strict) (haml-compute-indentation) 801 | (save-excursion 802 | (beginning-of-line) 803 | (delete-horizontal-space) 804 | (if (and (not strict) (equal last-command this-command) (/= ci 0)) 805 | (indent-to (* (/ (1- ci) haml-indent-offset) haml-indent-offset)) 806 | (indent-to need)))) 807 | (when (< (current-column) (current-indentation)) 808 | (forward-to-indentation 0)))) 809 | 810 | (defun haml-reindent-region-by (n) 811 | "Add N spaces to the beginning of each line in the region. 812 | If N is negative, will remove the spaces instead. Assumes all 813 | lines in the region have indentation >= that of the first line." 814 | (let* ((ci (current-indentation)) 815 | (indent-rx 816 | (concat "^" 817 | (if indent-tabs-mode 818 | (concat (make-string (/ ci tab-width) ?\t) 819 | (make-string (mod ci tab-width) ?\t)) 820 | (make-string ci ?\s))))) 821 | (save-excursion 822 | (while (re-search-forward indent-rx (mark) t) 823 | (let ((ci (current-indentation))) 824 | (delete-horizontal-space) 825 | (beginning-of-line) 826 | (indent-to (max 0 (+ ci n)))))))) 827 | 828 | (defun haml-electric-backspace (arg) 829 | "Delete characters or back-dent the current line. 830 | If invoked following only whitespace on a line, will back-dent 831 | the line and all nested lines to the immediately previous 832 | multiple of `haml-indent-offset' spaces. With ARG, do it that 833 | many times. 834 | 835 | Set `haml-backspace-backdents-nesting' to nil to just back-dent 836 | the current line." 837 | (interactive "*p") 838 | (if (or (/= (current-indentation) (current-column)) 839 | (bolp) 840 | (save-excursion 841 | (beginning-of-line) 842 | (looking-at "^[ \t]+$"))) 843 | (backward-delete-char arg) 844 | (save-excursion 845 | (beginning-of-line) 846 | (unwind-protect 847 | (progn 848 | (if haml-backspace-backdents-nesting 849 | (haml-mark-sexp-but-not-next-line) 850 | (set-mark (save-excursion (end-of-line) (point)))) 851 | (haml-reindent-region-by (* (- arg) haml-indent-offset))) 852 | (pop-mark))) 853 | (back-to-indentation))) 854 | 855 | (defun haml-kill-line-and-indent () 856 | "Kill the current line, and re-indent all lines nested beneath it." 857 | (interactive) 858 | (beginning-of-line) 859 | (haml-mark-sexp-but-not-next-line) 860 | (kill-line 1) 861 | (haml-reindent-region-by (* -1 haml-indent-offset))) 862 | 863 | (defun haml-indent-string () 864 | "Return the indentation string for `haml-indent-offset'." 865 | (mapconcat 'identity (make-list haml-indent-offset " ") "")) 866 | 867 | ;;;###autoload 868 | (add-to-list 'auto-mode-alist '("\\.haml\\'" . haml-mode)) 869 | 870 | 871 | ;; Local Variables: 872 | ;; coding: utf-8 873 | ;; byte-compile-warnings: (not cl-functions) 874 | ;; eval: (checkdoc-minor-mode 1) 875 | ;; End: 876 | 877 | (provide 'haml-mode) 878 | ;;; haml-mode.el ends here 879 | --------------------------------------------------------------------------------