+;;; multi-web-mode.el --- multiple major mode support for web editing
+
+;; Copyright (C) 2012 Fabián Ezequiel Gallina.
+
+;; Author: Fabián E. Gallina <fabian@anue.biz>
+;; URL: https://github.com/fgallina/multi-web-mode
+;; Version: 0.1
+;; Created: Feb 2009
+;; Keywords: convenience, languages, wp
+
+;; This file is part of Multi Web Mode
+
+;; Multi Web 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 3 of the
+;; License, or (at your option) any later version.
+
+;; Multi Web 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 Multi Web Mode. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Multi Web Mode is a minor mode wich makes web editing in Emacs much easier.
+
+;; Basically what it does is select the appropriate major mode
+;; automatically when you move the point and also calculates the
+;; correct indentation of chunks according to the indentation of the
+;; most relevant major mode.
+
+;;
+\f
+;;; Code:
+
+(eval-when-compile
+ (require 'cl)
+ (defvar multi-web-mode))
+
+(defvar mweb-mode-map
+ (let ((mweb-mode-map (make-sparse-keymap)))
+ (define-key mweb-mode-map (kbd "M-<f11>") 'mweb-set-default-major-mode)
+ (define-key mweb-mode-map (kbd "M-<f12>") 'mweb-set-extra-indentation)
+ (define-key mweb-mode-map [remap mark-whole-buffer] 'mweb-mark-whole-buffer)
+ mweb-mode-map)
+ "Keymaps for command `multi-web-mode'.")
+
+(defvar mweb-mode-hook nil
+ "Hooks to run when command `multi-web-mode' is initialized.")
+
+(defvar mweb-extra-indentation 0
+ "Extra indentation for chunks.
+Automatically calculated when the major mode has changed.")
+
+(defcustom mweb-default-major-mode nil
+ "Default major mode when not in chunk."
+ :type 'symbol
+ :group 'multi-web-mode
+ :safe 'symbolp)
+
+(defcustom mweb-filename-extensions
+ nil
+ "File extensions that trigger activation.
+
+This is an example configuration:
+'(\"php\" \"htm\" \"html\" \"ctp\" \"phtml\" \"php4\" \"php5\")"
+ :type '(list string)
+ :group 'multi-web-mode
+ :safe #'(lambda (extensions)
+ (not (catch 'fail
+ (dolist (ext extensions)
+ (when (not (stringp ext))
+ (throw 'fail t)))))))
+
+;; What you read in the docstring translates to:
+;; ((php-mode "<\\?php\\|<\\? \\|<\\?=" "\\?>")
+;; (js-mode "<script +\\(type=\"text/javascript\"\\|language=\"javascript\"\\)[^>]*>" "</script>")
+;; (css-mode "<style +type=\"text/css\"[^>]*>" "</style>"))
+(defcustom mweb-tags
+ nil
+ "Tags enabled for command `multi-web-mode'.
+This var is an alist on which each element has the form
+\(major-mode \"open tag regex\" \"close tag regex\").
+
+This is an example configuration:
+
+\(\(php-mode \"<\\\\?php\\|<\\\\? \\|<\\\\?=\" \"\\\\?>\")
+ \(js-mode
+ \"<script +\\\\(type=\\\"text/javascript\\\"\\\\
+|language=\\\"javascript\\\"\\\\)[^>]*>\"
+ \"</script>\")
+ \(css-mode \"<style +type=\\\"text/css\\\"[^>]*>\" \"</style>\"))"
+ :type '(repeat (symbol string string))
+ :group 'multi-web-mode
+ :safe #'(lambda (tags)
+ (not (catch 'fail
+ (dolist (tag tags)
+ (when (or
+ (not (symbolp (mweb-get-tag-attr tag 'mode)))
+ (not (stringp (mweb-get-tag-attr tag 'open)))
+ (not (stringp (mweb-get-tag-attr tag 'close))))
+ (throw 'fail t)))))))
+
+(defcustom mweb-submode-indent-offset 2
+ "Indentation offset for code inside chunks."
+ :type 'integer
+ :group 'multi-web-mode
+ :safe 'integerp)
+
+(defcustom mweb-ignored-commands
+ (list
+ 'undo
+ 'yas/expand
+ 'yas/next-field-or-maybe-expand
+ 'isearch-forward
+ 'isearch-backward
+ 'isearch-other-control-char)
+ "Commands that prevent changing the major mode."
+ :type '(repeat symbol)
+ :group 'multi-web-mode
+ :safe #'(lambda (names)
+ (not (catch 'fail
+ (dolist (name names)
+ (when (not (symbolp name))
+ (throw 'fail t)))))))
+
+(defun mweb-get-tag-attr (tag attribute)
+ "Get TAG ATTRIBUTE.
+ATTRIBUTE values can be 'mode to get the tag's major mode or
+'open/'close to get the open/close regexp respectively."
+ (case attribute
+ (mode (car tag))
+ (open (cadr tag))
+ (close (caddr tag))))
+
+(defun mweb-get-tag (tag-major-mode)
+ "Return tag from `mweb-tags' matching TAG-MAJOR-MODE."
+ (assoc tag-major-mode mweb-tags))
+
+(defun mweb--looking-at-tag (&optional type)
+ "Return non-nil if pointer is looking at an open or close tag.
+
+Possible values of TYPE are:
+ * nil: to check if point is looking at an open or close tag.
+ * 'open: to check if point is looking at an open tag
+ * 'close: to check if point is looking at a close tag"
+ (let ((index 0)
+ (looking)
+ (open-tag)
+ (close-tag)
+ (tag-regexp))
+ (save-excursion
+ (back-to-indentation)
+ (while (and (< index (length mweb-tags))
+ (not looking))
+ (setq open-tag (mweb-get-tag-attr (elt mweb-tags index) 'open))
+ (setq close-tag (mweb-get-tag-attr (elt mweb-tags index) 'close))
+ (case type
+ (open (setq tag-regexp open-tag))
+ (close (setq tag-regexp close-tag))
+ (otherwise (setq tag-regexp (concat open-tag "\\|" close-tag))))
+ (when (looking-at tag-regexp)
+ (setq looking t))
+ (setq index (+ 1 index))))
+ looking))
+
+(defsubst mweb-looking-at-open-tag-p ()
+ "Return t if point is looking at an open tag."
+ (mweb--looking-at-tag 'open))
+
+(defsubst mweb-looking-at-close-tag-p ()
+ "Return t if point is looking at a close tag."
+ (mweb--looking-at-tag 'close))
+
+(defsubst mweb-looking-at-tag-p ()
+ "Return t if point is looking at an open or close tag."
+ (mweb--looking-at-tag))
+
+(defun mweb-change-major-mode ()
+ "Call the appropriate major mode for the pointed chunk.
+If the current `major-mode' is the correct one it doesn't funcall the
+major mode and returns nil, otherwise changes the `major-mode' and
+returns a symbol with its name."
+ (let ((closest-chunk-point 0)
+ (closest-chunk-mode mweb-default-major-mode)
+ (result nil))
+ (save-restriction
+ (widen)
+ (dolist (tag mweb-tags)
+ (setq result (mweb-closest-starting-chunk-point tag))
+ (when (and (integerp result)
+ (<= closest-chunk-point result))
+ (setq closest-chunk-point result)
+ (setq closest-chunk-mode (mweb-get-tag-attr tag 'mode)))))
+ (when (not (equal closest-chunk-mode major-mode))
+ (funcall closest-chunk-mode)
+ closest-chunk-mode)))
+
+(defun mweb-change-indent-line-function ()
+ "Set the correct value for `indent-line-function'.
+Depending of `major-mode'."
+ (when (not (equal major-mode mweb-default-major-mode))
+ (setq indent-line-function 'mweb-indent-line)))
+
+(defun mweb-closest-starting-chunk-point (tag)
+ "Return the point of the closest chunk for TAG.
+Where TAG is one of the tags contained in the `mweb-tags'
+list. If the chunk is not found then it returns nil."
+ (let ((open-tag)
+ (close-tag))
+ (save-excursion
+ (setq open-tag (re-search-backward (mweb-get-tag-attr tag 'open) nil t)))
+ (save-excursion
+ (setq close-tag (re-search-backward (mweb-get-tag-attr tag 'close) nil t)))
+ (cond ((not open-tag)
+ nil)
+ ((and open-tag
+ (not close-tag))
+ open-tag)
+ ((> open-tag close-tag)
+ open-tag))))
+
+(defun mweb-multiple-chunks-p ()
+ "Check if multiple chunks exist in the current buffer."
+ (save-excursion
+ (save-restriction
+ (widen)
+ (goto-char (point-min))
+ (re-search-forward "[^\s\t\n]" nil t)
+ (or (not (mweb-looking-at-open-tag-p))
+ (catch 'break
+ (dolist (tag mweb-tags)
+ (when (re-search-forward (mweb-get-tag-attr tag 'close) nil t)
+ (throw 'break (not (not (re-search-forward "[^\s\t\n]" nil t)))))))))))
+
+(defun mweb-update-context ()
+ "Update extra indentation value for chunks."
+ (let ((changed-major-mode (mweb-change-major-mode)))
+ (if (and changed-major-mode
+ (not (equal major-mode mweb-default-major-mode)))
+ (setq mweb-extra-indentation (mweb-calculate-indentation))
+ (setq mweb-extra-indentation 0)))
+ (mweb-change-indent-line-function))
+
+(defun mweb-calculate-indentation ()
+ "Calculate the correct indentation given previous submode."
+ (let ((indentation 0)
+ (prev-line-pos)
+ (changed-major-mode major-mode)
+ (buffer-modified-flag (buffer-modified-p)))
+ (save-restriction
+ (widen)
+ (save-excursion
+ (mweb-goto-current-mode-open-tag)
+ (if (progn (mweb-forward-nonblank-line -1) (bobp))
+ (if (mweb-multiple-chunks-p)
+ (setq indentation 0)
+ (setq indentation (- mweb-submode-indent-offset)))
+ (end-of-line)
+ (setq prev-line-pos (point-marker))
+ (insert "\na")
+ (mweb-change-major-mode)
+ (indent-according-to-mode)
+ (setq indentation (current-indentation))
+ (delete-region prev-line-pos (line-end-position))))
+ (funcall changed-major-mode)
+ (set-buffer-modified-p buffer-modified-flag)
+ indentation)))
+
+(defun mweb-mark-whole-buffer ()
+ "Multi-web-mode's version of `mark-whole-buffer'."
+ (interactive)
+ (push-mark (point))
+ (goto-char (point-min))
+ (mweb-change-major-mode)
+ (push-mark (point-max) nil t))
+
+(defun mweb-indent-line ()
+ "Function to use when indenting a submode line."
+ (interactive)
+ ;; Yes, indent according to mode will do what we expect
+ (setq mweb-extra-indentation (mweb-calculate-indentation))
+ (if (not (mweb-looking-at-open-tag-p))
+ (if (not (mweb-looking-at-close-tag-p))
+ ;; Normal indentation
+ (if (equal major-mode mweb-default-major-mode)
+ (indent-according-to-mode)
+ (save-excursion
+ (beginning-of-line)
+ (delete-horizontal-space)
+ (unless (bobp)
+ (indent-according-to-mode)
+ (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset)))))
+ ;; Close tag indentation routine
+ (let ((open-tag-indentation 0))
+ (save-excursion
+ (mweb-goto-current-mode-open-tag)
+ (setq open-tag-indentation (current-indentation)))
+ (beginning-of-line)
+ (delete-horizontal-space)
+ (indent-to open-tag-indentation)))
+ ;; Open tag indentation routine
+ (beginning-of-line)
+ (delete-horizontal-space)
+ (insert "a")
+ (delete-horizontal-space)
+ (beginning-of-line)
+ (mweb-update-context)
+ (indent-according-to-mode)
+ (indent-to (+ mweb-extra-indentation mweb-submode-indent-offset))
+ (delete-char 1))
+ (and (bolp) (back-to-indentation)))
+
+(defun mweb-indent-region (start end)
+ "Indent a region taking care of chunks.
+This routine considers the relative position of the chunks within
+the buffer. It follows the same filosophy than
+`mweb-indent-line-forward' because that function is what is used
+to indent the chunks which are not for the default major mode.
+Called from a program, START and END specify the region to indent."
+ (interactive "r")
+ (let ((delete-active-region nil)
+ (line-end))
+ (save-excursion
+ (goto-char end)
+ (setq end (point-marker))
+ (goto-char start)
+ (mweb-change-major-mode)
+ (or (bolp) (forward-line 1))
+ (while (< (point) end)
+ (mweb-update-context)
+ (if (equal major-mode mweb-default-major-mode)
+ (indent-according-to-mode)
+ (mweb-indent-line))
+ (forward-line 1))
+ (move-marker end nil))))
+
+(defun mweb-get-current-mode-tag-point (type)
+ "Gets the point marker of current chunk's open/close tag.
+
+The TYPE argument can be a 'open for the open tag or 'close for
+the close tag."
+ (when (not (equal major-mode mweb-default-major-mode))
+ (let ((index 0)
+ (found nil)
+ (tag)
+ (result nil)
+ (re-search-func (if (equal type 'open)
+ 're-search-backward
+ 're-search-forward)))
+ (while (and (< index (length mweb-tags))
+ (not found))
+ (setq tag (elt mweb-tags index))
+ (when (or (equal (mweb-get-tag-attr tag 'mode) major-mode)
+ (equal major-mode mweb-default-major-mode))
+ (setq found t)
+ (save-excursion
+ (if (looking-at (mweb-get-tag-attr tag type))
+ (progn
+ (back-to-indentation)
+ (setq result (point)))
+ (setq result (funcall re-search-func
+ (mweb-get-tag-attr tag type)
+ nil t)))))
+ (setq index (+ 1 index)))
+ result)))
+
+(defun mweb-goto-current-mode-open-tag ()
+ "Move the point to the open tag of the current chunk."
+ (interactive)
+ (let ((tag-point (mweb-get-current-mode-tag-point 'open)))
+ (when tag-point
+ (goto-char tag-point))))
+
+(defun mweb-goto-current-mode-close-tag ()
+ "Move the point to the close tag of the current chunk."
+ (interactive)
+ (let ((tag-point (mweb-get-current-mode-tag-point 'close)))
+ (when tag-point
+ (goto-char tag-point))))
+
+(defun mweb-set-extra-indentation (number)
+ "Set the new value for `mweb-extra-indentation' to NUMBER."
+ (interactive "nNew mweb-extra-indentation value: ")
+ (setq mweb-extra-indentation number)
+ (message "mweb-extra-indentation = %d" mweb-extra-indentation))
+
+(defun mweb-set-default-major-mode (major-mode)
+ "Set the new value for `mweb-default-major-mode' to MAJOR-MODE."
+ (interactive "CNew default major mode: ")
+ (setq mweb-default-major-mode major-mode)
+ (mweb-change-major-mode)
+ (message "mweb-default-major-mode = %s" mweb-default-major-mode))
+
+(defun mweb-forward-nonblank-line (&optional number)
+ "Move the cursor to the next/previous non blank line.
+
+When NUMBER is positive it moves forward and when is negative
+it moves backwards."
+ (when (not number)
+ (setq number 1))
+ (when (> number 1)
+ (setq number 1))
+ (when (< number -1)
+ (setq number -1))
+ (forward-line number)
+ (while (and (equal (mweb-get-current-line-trimmed-contents) "")
+ (not (or (bobp) (eobp))))
+ (forward-line number)))
+
+(defun mweb-get-current-line-trimmed-contents ()
+ "Gets the contents of the current line.
+It trims all space characters at the beginning and end of the line."
+ (let ((start-point)
+ (end-point)
+ (contents))
+ (save-excursion
+ (beginning-of-line)
+ (setq start-point (point))
+ (end-of-line)
+ (setq end-point (point))
+ (setq contents (buffer-substring start-point end-point))
+ (when (string-match "[ \t]*$" contents)
+ (setq contents (replace-match "" nil nil contents)))
+ (when (string-match "^[ \t]*" contents)
+ (setq contents (replace-match "" nil nil contents))))
+ contents))
+
+(defun mweb-post-command-hook ()
+ "The function which is appended to the `post-command-hook'."
+ (when (and multi-web-mode
+ (not (region-active-p))
+ (not (member last-command mweb-ignored-commands)))
+ (mweb-update-context)))
+
+(defun mweb-enable ()
+ "Setup the minor mode."
+ (set (make-local-variable 'indent-region-function)
+ 'mweb-indent-region)
+ (make-local-variable 'indent-line-function)
+ (add-hook 'post-command-hook 'mweb-post-command-hook)
+ (assq-delete-all 'multi-web-mode minor-mode-map-alist)
+ (push (cons 'multi-web-mode mweb-mode-map)
+ minor-mode-map-alist)
+ (run-hooks 'mweb-mode-hook))
+
+(defun mweb-disable ()
+ "Disable the minor mode."
+ (assq-delete-all 'multi-web-mode minor-mode-map-alist))
+
+;;;###autoload
+(define-minor-mode multi-web-mode
+ "Enables the multi web mode chunk detection and indentation"
+ :lighter " Multi-Web" :group 'convenience
+ (if multi-web-mode
+ (mweb-enable)
+ (mweb-disable)))
+
+(defun multi-web-mode-maybe ()
+ "Used to turn on the globalized minor mode."
+ (when (member
+ (file-name-extension (or buffer-file-name ""))
+ mweb-filename-extensions)
+ (multi-web-mode 1)))
+
+(define-globalized-minor-mode multi-web-global-mode
+ multi-web-mode multi-web-mode-maybe
+ :group 'multi-web-mode
+ :require 'multi-web-mode)
+
+(provide 'multi-web-mode)
+;;; multi-web-mode.el ends here