-
Notifications
You must be signed in to change notification settings - Fork 32
/
mmm-erb.el
243 lines (204 loc) · 9.53 KB
/
mmm-erb.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
;;; mmm-erb.el --- ERB templates editing support -*- lexical-binding: t; -*-
;; Copyright (C) 2012, 2013, 2018 Free Software Foundation, Inc.
;; Author: Dmitry Gutov <dmitry@gutov.dev>
;;{{{ GPL
;; This file 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, or (at your option)
;; any later version.
;; This file 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 GNU Emacs; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;}}}
;;; Commentary:
;; This file contains definitions of ERB and EJS submode classes, and well as
;; support functions for proper indentation.
;; Usage:
;; (require 'mmm-auto)
;; (setq mmm-global-mode 'auto)
;; (mmm-add-mode-ext-class 'html-erb-mode "\\.html\\.erb\\'" 'erb)
;; (mmm-add-mode-ext-class 'html-erb-mode "\\.jst\\.ejs\\'" 'ejs)
;; (mmm-add-mode-ext-class 'html-erb-mode nil 'html-js)
;; (mmm-add-mode-ext-class 'html-erb-mode nil 'html-css)
;; (add-to-list 'auto-mode-alist '("\\.html\\.erb\\'" . html-erb-mode))
;; (add-to-list 'auto-mode-alist '("\\.jst\\.ejs\\'" . html-erb-mode))
;; Optional settings:
;; (setq mmm-submode-decoration-level 2
;; mmm-parse-when-idle t)
;; nXML as primary mode (supports only JS and CSS subregions):
;; (mmm-add-mode-ext-class 'nxml-web-mode nil 'html-js)
;; (mmm-add-mode-ext-class 'nxml-web-mode nil 'html-css)
;; (add-to-list 'auto-mode-alist '("\\.xhtml\\'" . nxml-web-mode))
;;; Code:
(require 'sgml-mode)
(require 'cl-lib)
(require 'mmm-vars)
(require 'mmm-region)
(mmm-add-classes
'((erb :submode ruby-mode :front "<%[#=]?" :back "-?%>"
:match-face (("<%#" . mmm-comment-submode-face)
("<%=" . mmm-output-submode-face)
("<%" . mmm-code-submode-face))
:insert ((?% erb-code nil @ "<%" @ " " _ " " @ "%>" @)
(?# erb-comment nil @ "<%#" @ " " _ " " @ "%>" @)
(?= erb-expression nil @ "<%=" @ " " _ " " @ "%>" @))
:creation-hook mmm-erb-mark-as-special)
(ejs :submode js-mode :front "<%[#=]?" :back "-?%>"
:match-face (("<%#" . mmm-comment-submode-face)
("<%=" . mmm-output-submode-face)
("<%" . mmm-code-submode-face))
:insert ((?% ejs-code nil @ "<%" @ " " _ " " @ "%>" @)
(?# ejs-comment nil @ "<%#" @ " " _ " " @ "%>" @)
(?= ejs-expression nil @ "<%=" @ " " _ " " @ "%>" @))
:creation-hook mmm-erb-mark-as-special)))
(defun mmm-erb-mark-as-special ()
"Hook function to run in ERB and EJS tag regions."
(overlay-put mmm-current-overlay 'mmm-special-tag t))
;;;###autoload
(define-derived-mode html-erb-mode html-mode "ERB-HTML"
(setq sgml-unclosed-tags nil) ; Simplifies indentation logic.
(set (make-local-variable 'mmm-indent-line-function) #'mmm-erb-indent-line)
(add-hook 'mmm-after-syntax-propertize-functions
#'html-erb-after-syntax-propertize nil t))
(defun html-erb-after-syntax-propertize (overlay _mode beg end)
(when overlay
(with-silent-modifications
(funcall
(syntax-propertize-rules ("<\\|>" (0 ".")))
beg end))))
(defun mmm-erb-indent-line ()
"Indent the current line intelligently."
(interactive)
(let ((offset (- (current-column) (current-indentation))))
(back-to-indentation)
(mmm-update-submode-region)
(if (and mmm-current-overlay mmm-current-submode
(< (overlay-start mmm-current-overlay) (line-beginning-position)))
;; Region starts before the current line (and contains indentation).
;; If it starts on the current line, then either first part of the line
;; is in primary mode, or we're on the first line of a script or style
;; tag contents. In the latter case, better to also indent it according
;; to the primary mode (as text): `js-indent-line' ignores narrowing,
;; gets confused by the angle bracket on the previous line and thus
;; breaks our "top level" heuristic.
(mmm-erb-indent-line-submode)
(mmm-erb-indent-line-primary))
(when (> offset 0) (forward-char offset))))
(defun mmm-erb-indent-line-submode ()
"Indent line within a submode."
(let (added-whitespace)
(if (<= (overlay-end mmm-current-overlay)
(save-excursion (back-to-indentation) (point)))
;; We're at a closing tag.
(mmm-erb-indent-to-region-start)
(save-restriction
(save-excursion
(goto-char (overlay-start mmm-current-overlay))
(when (not (looking-at "^\\|\\s-*$"))
;; Submode region has text on the same line as the opening tag,
;; pad it with whitespace to make the following lines line up.
(setq added-whitespace (current-column))
(insert-char ?\s added-whitespace)))
(narrow-to-region (overlay-start mmm-current-overlay)
(overlay-end mmm-current-overlay))
(funcall (mmm-erb-orig-indent-function mmm-current-submode))
(when added-whitespace
;; Remove the padding.
(save-excursion
(goto-char (overlay-start mmm-current-overlay))
(delete-char added-whitespace))))
;; If submode indent function moved us to bol,
;; we're on the top level, indent according to the primary mode.
(when (zerop (current-indentation))
(mmm-erb-indent-to-region-start
(mmm-erb-indent-offset mmm-primary-mode))))))
(defun mmm-erb-indent-to-region-start (&optional additional-offset)
"Indent line to match start of region, possibly adding ADDITIONAL-OFFSET."
(indent-line-to
(save-excursion
(goto-char (1- (overlay-start mmm-current-overlay)))
(+ (current-indentation)
(or additional-offset 0)))))
(defun mmm-erb-indent-line-primary ()
"Indent line in primary mode."
(let* ((here (point))
;; Go before previous line's tag.
(start (progn (forward-line -1)
(back-to-indentation)
(let ((lcon (sgml-lexical-context)))
(when (eq (car lcon) 'tag)
;; Tag spreads several lines.
(goto-char (cdr lcon))
(back-to-indentation)))
(point)))
(regions (mmm-regions-in start here))
(n 0))
;; Collect indent modifier depending on type of tags.
(cl-loop for region in regions
for type = (mmm-erb-scan-region region)
when type do
(if (eq type 'close)
(when (cl-plusp n) (cl-decf n))
(cl-incf n (if (eq type 'close) 0 1))))
(let ((eol (progn (goto-char here) (end-of-line 1) (point))))
;; Look for "else" and "end" instructions to adjust modifier.
;; If a block start instruction comes first, abort.
(cl-loop for region in (mmm-regions-in here eol)
for type = (mmm-erb-scan-region region)
until (eq type 'open)
when (memq type '(middle close)) do (cl-decf n)))
(goto-char here)
(funcall (mmm-erb-orig-indent-function mmm-primary-mode))
(let* ((indent (current-indentation))
(indent-step (mmm-erb-indent-offset mmm-primary-mode)))
(indent-line-to (+ indent (if n (* indent-step n) 0))))))
(defun mmm-erb-scan-region (region)
(when region ; Can be nil if a line is empty, for example.
(cl-destructuring-bind (submode beg end ovl) region
(let ((scan-fn (plist-get '(ruby-mode mmm-erb-scan-erb
js-mode mmm-erb-scan-ejs)
submode)))
(and scan-fn
(overlay-get ovl 'mmm-special-tag)
(save-excursion
(goto-char beg)
(skip-syntax-forward "-")
(funcall scan-fn end)))))))
(defconst mmm-erb-ruby-close-re "\\<end\\>\\|}"
"Regexp to match the end of a Ruby block.")
(defun mmm-erb-scan-erb (limit)
(cond ((looking-at "\\(?:if\\|unless\\|for\\|while\\)\\b") 'open)
((looking-at "\\(?:else\\|elsif\\)\\b") 'middle)
((looking-at mmm-erb-ruby-close-re) 'close)
((and (re-search-forward (concat "\\(?: +do +\\| *{ *\\)"
"\\(?:|[A-Za-z0-9_, ]*|\\)? *")
limit t)
(let ((pt (point)))
(not (when (< pt limit)
(goto-char limit)
(skip-syntax-backward "-")
(looking-back mmm-erb-ruby-close-re pt)))))
'open)))
(defun mmm-erb-scan-ejs (limit)
(cond ((looking-at "\\(?:if\\|for\\|while\\)\\b") 'open)
((looking-at "} *else\\b") 'middle)
((looking-at "}") 'close)
((re-search-forward " *{ *" limit t) 'open)))
(defun mmm-erb-orig-indent-function (mode)
(get mode 'mmm-indent-line-function))
(defvar mmm-erb-offset-var-alist
'((html-erb-mode . sgml-basic-offset)
(nxml-web-mode . nxml-child-indent)))
(defun mmm-erb-indent-offset (mode)
(let ((name (cdr (assoc mode mmm-erb-offset-var-alist))))
(when name (symbol-value name))))
;;;###autoload
(define-derived-mode nxml-web-mode nxml-mode "nXML-Web"
(set (make-local-variable 'mmm-indent-line-function) #'mmm-erb-indent-line))
(provide 'mmm-erb)
;;; mmm-erb.el ends here