branch: elpa/php-mode
commit 7da776b1ff150dfd0e375ed30a02fc40253a0041
Author: USAMI Kenta <[email protected]>
Commit: USAMI Kenta <[email protected]>

    Add php-indent.el, a CC Mode-free indentation engine ported from js.el
    
    Extracted from the cc-mode independent php-mode work (#812) so that it
    can be reviewed and land independently of the Emacs 28.1 requirement
    that blocks that branch until the August release (#811).
    
    The engine is ported from the syntax-ppss based indenter of js.el in GNU
    Emacs (attributed in the file header); the JavaScript specific handling
    (JSX, C preprocessor macros, array comprehensions and regexp literals)
    is removed, and PHP specifics are added: -> / ?-> / :: method and
    property chains, the . concatenation operator aligned to the assignment,
    statement continuations, switch/case, the alternative syntax (endif and
    friends) and heredoc/nowdoc bodies.  Its entry points are php-indent-line
    and php-indent-region.
    
    The file is self-contained: it only requires php.el for the php
    customization group, and defines no symbol that collides with the
    current php-mode.
    
    This is purely additive: nothing sets php-indent-line as
    indent-line-function yet, so the engine is inert -- the current CC Mode
    based php-mode keeps indenting through cc-engine.  Registering both new
    files in Eask so they are byte-compiled by CI.
---
 Eask               |   2 +
 lisp/php-indent.el | 711 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 713 insertions(+)

diff --git a/Eask b/Eask
index 6fe9bb805e..1ec578047d 100644
--- a/Eask
+++ b/Eask
@@ -12,6 +12,8 @@
  "lisp/php.el"
  "lisp/php-complete.el"
  "lisp/php-defs.el"
+ "lisp/php-indent.el"
+ "lisp/php-keywords.el"
  "lisp/php-face.el"
  "lisp/php-format.el"
  "lisp/php-project.el"
diff --git a/lisp/php-indent.el b/lisp/php-indent.el
new file mode 100644
index 0000000000..00dd541986
--- /dev/null
+++ b/lisp/php-indent.el
@@ -0,0 +1,711 @@
+;;; php-indent.el --- Indentation engine for PHP Mode  -*- lexical-binding: t; 
-*-
+
+;; Copyright (C) 2026 Friends of Emacs-PHP development
+;; Copyright (C) 1999-2026 Free Software Foundation, Inc.
+
+;; Author: USAMI Kenta <[email protected]>
+;; Keywords: languages, php
+;; Homepage: https://github.com/emacs-php/php-mode
+;; License: GPL-3.0-or-later
+
+;; This program 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.
+
+;; This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file implements the indentation engine used by PHP Mode.
+;;
+;; The engine is ported from the `syntax-ppss'-based indenter of js.el
+;; in GNU Emacs (originally derived from js.el in GNU Emacs).  It does
+;; NOT depend on CC Mode.  The JavaScript-specific handling of js.el
+;; (JSX, C preprocessor macros, array comprehensions and regular
+;; expression literals) has been removed, and PHP-specific behaviour has
+;; been added: `->', `?->' and `::' method/property chains, the `.'
+;; string concatenation operator, PHP `switch'/`case' indentation, a
+;; simplified alternative-syntax (`endif' etc.) alignment, and leaving
+;; heredoc/nowdoc and string bodies untouched.
+;;
+;; The public entry points are `php-indent-line' (suitable as
+;; `indent-line-function') and `php-indent-region'.
+
+;;; Code:
+
+(require 'php)
+
+;;; Customization
+
+(defcustom php-indent-offset 4
+  "Number of columns for one level of PHP indentation."
+  :group 'php
+  :tag "PHP Indent Offset"
+  :type 'integer
+  :safe #'integerp)
+
+(defcustom php-indent-switch-case-offset 4
+  "Indentation of `case'/`default' labels relative to their `switch'.
+Statements inside a `case' are indented by a further `php-indent-offset'."
+  :group 'php
+  :tag "PHP Indent Switch Case Offset"
+  :type 'integer
+  :safe #'integerp)
+
+(defcustom php-indent-chain-indent t
+  "If non-nil, align successive method/property chain lines.
+A chained line begins with `->', `?->' or `::'."
+  :group 'php
+  :tag "PHP Indent Chain Indent"
+  :type 'boolean
+  :safe #'booleanp)
+
+(defcustom php-indent-paren-offset 0
+  "Additional indentation for lines inside a `(...)' group."
+  :group 'php
+  :tag "PHP Indent Paren Offset"
+  :type 'integer
+  :safe #'integerp)
+
+(defcustom php-indent-square-offset 0
+  "Additional indentation for lines inside a `[...]' group."
+  :group 'php
+  :tag "PHP Indent Square Offset"
+  :type 'integer
+  :safe #'integerp)
+
+(defcustom php-indent-curly-offset 0
+  "Additional indentation for lines inside a `{...}' group."
+  :group 'php
+  :tag "PHP Indent Curly Offset"
+  :type 'integer
+  :safe #'integerp)
+
+(defcustom php-indent-switch-offset 0
+  "Additional indentation applied to the whole body of a `switch'."
+  :group 'php
+  :tag "PHP Indent Switch Offset"
+  :type 'integer
+  :safe #'integerp)
+
+;;; Constants
+
+(defconst php-indent--name-start-re "[[:alpha:]_$]"
+  "Regexp matching the start of a PHP identifier, without grouping.")
+
+(defconst php-indent--name-re
+  (concat php-indent--name-start-re "\\(?:\\s_\\|\\sw\\)*")
+  "Regexp matching a PHP identifier, without grouping.")
+
+(defun php-indent--regexp-opt-symbol (list)
+  "Like `regexp-opt', but surround the result with symbol boundaries.
+LIST is a list of strings to match as whole symbols."
+  (concat "\\_<" (regexp-opt list t) "\\_>"))
+
+(defconst php-indent--possibly-braceless-keyword-re
+  (php-indent--regexp-opt-symbol
+   '("if" "else" "elseif" "for" "foreach" "while" "do" "switch" "declare"))
+  "Regexp matching keywords optionally followed by an opening brace.")
+
+(defconst php-indent--declaration-keyword-re
+  (regexp-opt '("const") 'words)
+  "Regexp matching declaration keywords for multi-line alignment.")
+
+(defconst php-indent--indent-operator-re
+  (concat "[-+*/%<>&^|?:.]\\([^-+*/.]\\|$\\)\\|!?=\\([^>]\\|$\\)\\|"
+          (php-indent--regexp-opt-symbol '("instanceof")))
+  "Regexp matching operators that affect indentation of continued lines.
+Note that `=>' (the array/arrow key operator) is intentionally excluded.")
+
+(defconst php-indent--line-terminating-arrow-re "=>\\s-*\\(/[/*]\\|$\\)"
+  "Regexp matching a `=>' token that terminates a line.
+Whitespace and comments around the arrow are ignored.")
+
+(defconst php-indent--altsyntax-re
+  
"\\(end\\(?:if\\|foreach\\|while\\|for\\|switch\\|declare\\)\\|else\\(?:if\\)?\\)\\_>"
+  "Regexp matching alternative-syntax closing/continuation keywords.")
+
+;;; Low-level scanning helpers (ported from js.el)
+
+(defun php-indent--re-search-forward-inner (regexp &optional bound count)
+  "Helper for `php-indent--re-search-forward'.
+Search for REGEXP up to BOUND, skipping COUNT matches inside strings,
+comments and heredocs."
+  (let (parse str-terminator)
+    (while (> count 0)
+      (re-search-forward regexp bound)
+      (setq parse (syntax-ppss))
+      (cond ((setq str-terminator (nth 3 parse))
+             (when (eq str-terminator t)
+               (setq str-terminator ?/))
+             (re-search-forward
+              (concat "\\([^\\]\\|^\\)" (string str-terminator))
+              (line-end-position) t))
+            ((nth 7 parse)
+             (forward-line))
+            ((or (nth 4 parse)
+                 (and (eq (char-before) ?/) (eq (char-after) ?*)))
+             (re-search-forward "\\*/"))
+            (t
+             (setq count (1- count))))))
+  (point))
+
+(defun php-indent--re-search-forward (regexp &optional bound noerror count)
+  "Search forward, ignoring strings and comments.
+This invokes `re-search-forward' but treats the buffer as if strings and
+comments have been removed.  REGEXP, BOUND, NOERROR and COUNT are as for
+`re-search-forward'."
+  (unless count (setq count 1))
+  (let ((saved-point (point))
+        (search-fun
+         (cond ((< count 0) (setq count (- count))
+                #'php-indent--re-search-backward-inner)
+               ((> count 0) #'php-indent--re-search-forward-inner)
+               (t #'ignore))))
+    (condition-case err
+        (funcall search-fun regexp bound count)
+      (search-failed
+       (goto-char saved-point)
+       (unless noerror
+         (signal (car err) (cdr err)))))))
+
+(defun php-indent--re-search-backward-inner (regexp &optional bound count)
+  "Helper for `php-indent--re-search-backward'.
+Search backward for REGEXP up to BOUND, skipping COUNT matches inside
+strings and comments."
+  (let (parse)
+    (while (> count 0)
+      (re-search-backward regexp bound)
+      (when (and (> (point) (point-min))
+                 (save-excursion (backward-char) (looking-at "/[/*]")))
+        (forward-char))
+      (setq parse (syntax-ppss))
+      (cond ((nth 8 parse)
+             (goto-char (nth 8 parse)))
+            ((or (nth 4 parse)
+                 (and (eq (char-before) ?/) (eq (char-after) ?*)))
+             (re-search-backward "/\\*"))
+            (t
+             (setq count (1- count))))))
+  (point))
+
+(defun php-indent--re-search-backward (regexp &optional bound noerror count)
+  "Search backward, ignoring strings and comments.
+REGEXP, BOUND, NOERROR and COUNT are as for `re-search-backward'."
+  (php-indent--re-search-forward regexp bound noerror (if count (- count) -1)))
+
+(defun php-indent--backward-syntactic-ws (&optional lim)
+  "Move backward over whitespace and comments, not before LIM."
+  (save-restriction
+    (when lim (narrow-to-region lim (point-max)))
+    (let ((pos (point)))
+      (while (progn
+               (forward-comment most-negative-fixnum)
+               (/= (point) (prog1 pos (setq pos (point)))))))))
+
+(defun php-indent--forward-syntactic-ws (&optional lim)
+  "Move forward over whitespace and comments, not past LIM."
+  (save-restriction
+    (when lim (narrow-to-region (point-min) lim))
+    (let ((pos (point)))
+      (while (progn
+               (forward-comment most-positive-fixnum)
+               (/= (point) (prog1 pos (setq pos (point)))))))))
+
+(defun php-indent--same-line (pos)
+  "Return non-nil if POS is on the current line."
+  (and (>= pos (line-beginning-position))
+       (<= pos (line-end-position))))
+
+(defun php-indent--find-newline-backward ()
+  "Move backward to the nearest newline that is not in a block comment.
+Return non-nil on success."
+  (let ((continue t)
+        (result t))
+    (while continue
+      (setq continue nil)
+      (if (search-backward "\n" nil t)
+          (let ((parse (syntax-ppss)))
+            (when (nth 4 parse)
+              (goto-char (nth 8 parse))
+              (unless (nth 7 parse)
+                (setq continue t))))
+        (setq result nil)))
+    result))
+
+;;; Operator / expression predicates
+
+(defun php-indent--looking-at-operator-p ()
+  "Return non-nil if point is on a PHP operator, other than a comma."
+  (save-match-data
+    (and (looking-at php-indent--indent-operator-re)
+         (or (not (eq (char-after) ?:))
+             (save-excursion
+               (php-indent--backward-syntactic-ws)
+               (when (eq (char-before) ?\)) (backward-list))
+               (and (php-indent--re-search-backward "[?:{]\\|\\_<case\\_>" nil 
t)
+                    (eq (char-after) ??)))))))
+
+(defun php-indent--continued-expression-p ()
+  "Return non-nil if the current line continues an expression."
+  (save-excursion
+    (back-to-indentation)
+    ;; Skip a leading block comment so that a chain line whose operator is
+    ;; preceded by `/* ... */' is still recognized (e.g. `/* c */ ->foo()').
+    (while (looking-at "/\\*")
+      (forward-comment 1)
+      (skip-syntax-forward " "))
+    (cond
+     ;; Lines starting with a chain operator continue the previous line.
+     ((looking-at "->\\|\\?->\\|::") t)
+     ((php-indent--looking-at-operator-p)
+      (or
+       (not (memq (char-after) '(?- ?+)))
+       (progn
+         (forward-comment (- (point)))
+         (not (memq (char-before) '(?, ?\[ ?\())))))
+     (t
+      (and (php-indent--find-newline-backward)
+           (progn
+             (skip-chars-backward " \t")
+             (progn
+               (or (bobp) (backward-char))
+               (and (> (point) (point-min))
+                    (save-excursion
+                      (backward-char)
+                      (not (looking-at "[/*]/\\|=>")))
+                    (php-indent--looking-at-operator-p)
+                    (progn (backward-char)
+                           (not (looking-at "\\+\\+\\|--\\|/[/*]")))))))))))
+
+(defun php-indent--skip-term-backward ()
+  "Skip a term before point.
+Return non-nil if the skipped term is preceded by a `->', `?->' or `::'
+chain operator, leaving point at the start of that operator.  Whitespace
+and comments (including whole comment lines between chain links) are
+skipped."
+  (let ((term-skipped nil))
+    ;; Skip backward over balanced parens.
+    (let ((progress t))
+      (while progress
+        (setq progress nil)
+        (php-indent--backward-syntactic-ws)
+        (when (memq (char-before) '(?\] ?\) ?\}))
+          (setq progress t term-skipped t)
+          (backward-list))))
+    ;; Maybe skip over a symbol.
+    (let ((save-point (point)))
+      (if (and (< (skip-syntax-backward "w_") 0)
+               (looking-at php-indent--name-re))
+          (setq term-skipped t)
+        (goto-char save-point)))
+    (when (and term-skipped (> (point) (point-min)))
+      (php-indent--backward-syntactic-ws)
+      (let ((end (point)))
+        (cond
+         ((and (>= (- end 3) (point-min))
+               (save-excursion (goto-char (- end 3)) (looking-at "\\?->")))
+          (goto-char (- end 3)) t)
+         ((and (>= (- end 2) (point-min))
+               (save-excursion (goto-char (- end 2)) (looking-at "->\\|::")))
+          (goto-char (- end 2)) t)
+         (t nil))))))
+
+(defun php-indent--skip-terms-backward ()
+  "Skip any number of chained terms backward.
+Move point to the earliest chain operator without changing paren levels.
+Return non-nil if at least one term was skipped."
+  (when (php-indent--skip-term-backward)
+    (let ((last-point (point)))
+      (while (php-indent--skip-term-backward)
+        (setq last-point (point)))
+      (goto-char last-point)
+      t)))
+
+(defun php-indent--chained-expression-p ()
+  "Return the indentation column for a method/property chain line.
+Return nil when the current line is not a continued chain line, or when
+the chain is rooted in a `::' static access (CC Mode indented those as
+plain statement continuations rather than aligning to the operator).
+Point is expected to be at `back-to-indentation'."
+  (when php-indent-chain-indent
+    (save-excursion
+      (when (and (looking-at "->\\|\\?->\\|::")
+                 (php-indent--continued-expression-p))
+        (php-indent--backward-syntactic-ws)
+        (when (and (php-indent--skip-terms-backward)
+                   (not (looking-at "::")))
+          (current-column))))))
+
+(defun php-indent--static-chain-in-list-p (parse-status)
+  "Return non-nil for a `::'-rooted chain line inside a paren/bracket group.
+PARSE-STATUS is the result of `syntax-ppss'.  CC Mode did not recognize
+these as statement continuations inside argument lists, so they are
+indented as plain list elements without the continued-expression bonus."
+  (let ((open (nth 1 parse-status)))
+    (and php-indent-chain-indent
+         open
+         (memq (char-after open) '(?\( ?\[))
+         (save-excursion
+           (back-to-indentation)
+           (when (looking-at "->\\|\\?->\\|::")
+             (php-indent--backward-syntactic-ws)
+             (and (php-indent--skip-terms-backward)
+                  (looking-at "::")))))))
+
+(defun php-indent--end-of-do-while-loop-p ()
+  "Return non-nil if point is on the \"while\" of a do-while statement."
+  (save-excursion
+    (save-match-data
+      (when (looking-at "\\s-*\\_<while\\_>")
+        (if (save-excursion
+              (skip-chars-backward " \t\n}")
+              (looking-at "[ \t\n]*}"))
+            (save-excursion
+              (backward-list) (forward-symbol -1) (looking-at "\\_<do\\_>"))
+          (php-indent--re-search-backward "\\_<do\\_>" 
(line-beginning-position) t)
+          (or (looking-at "\\_<do\\_>")
+              (let ((saved-indent (current-indentation)))
+                (while (and (php-indent--re-search-backward "^\\s-*\\_<" nil t)
+                            (/= (current-indentation) saved-indent)))
+                (and (looking-at "\\s-*\\_<do\\_>")
+                     (not (php-indent--re-search-forward
+                           "\\_<while\\_>" (line-end-position) t))
+                     (= (current-indentation) saved-indent)))))))))
+
+(defun php-indent--ctrl-statement-indentation ()
+  "Return the indentation for a braceless control-statement body, or nil."
+  (save-excursion
+    (back-to-indentation)
+    (when (save-excursion
+            (and (not (eq (line-beginning-position) (point-min)))
+                 (not (looking-at "[{]"))
+                 (php-indent--re-search-backward "[[:graph:]]" nil t)
+                 (progn
+                   (or (eobp) (forward-char))
+                   (when (= (char-before) ?\)) (backward-list))
+                   (skip-syntax-backward " ")
+                   (skip-syntax-backward "w_")
+                   (looking-at php-indent--possibly-braceless-keyword-re))
+                 (memq (char-before) '(?\s ?\t ?\n ?\}))
+                 (not (php-indent--end-of-do-while-loop-p))))
+      (save-excursion
+        (goto-char (match-beginning 0))
+        (+ (current-indentation) php-indent-offset)))))
+
+(defun php-indent--multi-line-declaration-indentation ()
+  "Return the indentation for a continued `const' declaration, or nil."
+  (let (forward-sexp-function ; Use Lisp version.
+        at-opening-bracket)
+    (save-excursion
+      (back-to-indentation)
+      (when (not (looking-at php-indent--declaration-keyword-re))
+        (when (looking-at php-indent--indent-operator-re)
+          (goto-char (match-end 0)))
+        (while (and (not at-opening-bracket)
+                    (not (bobp))
+                    (let ((pos (point)))
+                      (save-excursion
+                        (php-indent--backward-syntactic-ws)
+                        (or (eq (char-before) ?,)
+                            (and (not (eq (char-before) ?\;))
+                                 (prog2
+                                     (skip-syntax-backward ".")
+                                     (looking-at 
php-indent--indent-operator-re)
+                                   (php-indent--backward-syntactic-ws))
+                                 (not (eq (char-before) ?\;)))
+                            (php-indent--same-line pos)))))
+          (condition-case nil
+              (backward-sexp)
+            (scan-error (setq at-opening-bracket t))))
+        (when (looking-at php-indent--declaration-keyword-re)
+          (goto-char (match-end 0))
+          (1+ (current-column)))))))
+
+(defun php-indent--open-tag-before-p ()
+  "Return non-nil when point is immediately after a PHP open tag."
+  (looking-back "<\\?\\(?:php\\|=\\)?" (max (point-min) (- (point) 5))))
+
+(defun php-indent--case-label-colon-p ()
+  "Return non-nil when the `:' before point ends a case/default label."
+  (save-excursion
+    (beginning-of-line)
+    (looking-at "[ \t]*\\(?:case\\_>\\|default[ \t]*:\\)")))
+
+(defun php-indent--goto-statement-start ()
+  "Move point to the first token of the statement around point.
+The statement start is found by scanning balanced expressions backward
+until a statement terminator (`;', `{', `}', an opening `(' or `[', a
+`,', a case label `:' or a PHP open tag) is reached."
+  (let (forward-sexp-function done)
+    (while (not done)
+      (let ((last (point)))
+        (php-indent--backward-syntactic-ws)
+        (if (or (bobp)
+                (memq (char-before) '(?\; ?\{ ?\} ?\( ?\[ ?,))
+                (php-indent--open-tag-before-p)
+                (and (eq (char-before) ?:)
+                     (php-indent--case-label-colon-p)))
+            (progn (goto-char last) (setq done t))
+          (skip-syntax-backward ".")
+          (condition-case nil
+              (backward-sexp)
+            (scan-error (goto-char last) (setq done t))))))))
+
+(defun php-indent--statement-continuation-indentation ()
+  "Return the indentation for a continuation line of a broken statement.
+A continuation line is one whose previous code token leaves the
+statement unterminated: an identifier or keyword (as in stacked member
+modifiers, `extends' on its own line or a broken `return'), a closing
+parenthesis, or a return-type `):'.  Such lines are indented one
+`php-indent-offset' beyond the first line of the statement.  Return nil
+when the current line does not continue a statement this way."
+  (save-excursion
+    (back-to-indentation)
+    (unless (or (memq (char-after) '(?\{ ?\} ?\) ?\]))
+                (looking-at "/[/*]\\|#"))
+      (let ((pos (point)))
+        (php-indent--backward-syntactic-ws)
+        (when (and (not (bobp))
+                   (not (php-indent--open-tag-before-p))
+                   (or (memq (char-syntax (char-before)) '(?w ?_))
+                       (eq (char-before) ?\))
+                       ;; A `):' return-type (or alternative-syntax `):')
+                       ;; also leaves the statement open.
+                       (and (eq (char-before) ?:)
+                            (eq (char-before (1- (point))) ?\)))))
+          (goto-char pos)
+          (php-indent--goto-statement-start)
+          (unless (php-indent--same-line pos)
+            (+ (current-indentation) php-indent-offset)))))))
+
+(defun php-indent--concat-continuation-indentation ()
+  "Return the alignment column for a leading-`.' concatenation line.
+CC Mode aligned a continuation line beginning with the `.' string
+concatenation operator to the `=' of the assignment it continues.
+Return nil when the line does not start with `.' or the statement's
+first line contains no plain assignment operator."
+  (save-excursion
+    (back-to-indentation)
+    (when (looking-at "\\.[^.=0-9]")
+      (php-indent--goto-statement-start)
+      (let ((eol (line-end-position)) col)
+        (while (and (not col) (re-search-forward "=" eol t))
+          (let ((pos (match-beginning 0)))
+            (when (and (not (nth 8 (save-excursion (syntax-ppss pos))))
+                       (not (memq (char-before pos)
+                                  '(?= ?! ?< ?> ?+ ?- ?* ?/ ?. ?% ?& ?| ?^ 
??)))
+                       (not (memq (char-after (1+ pos)) '(?= ?>))))
+              (goto-char pos)
+              (setq col (current-column)))))
+        col))))
+
+(defun php-indent--broken-arrow-terminates-line-p ()
+  "Return non-nil if the current line's last real token is a `=>' arrow."
+  (let ((from (point)))
+    (end-of-line)
+    (re-search-backward php-indent--line-terminating-arrow-re from t)))
+
+;;; switch/case and alternative syntax
+
+(defun php-indent--switch-block-p (open)
+  "Return non-nil if OPEN is the opening brace of a `switch' block."
+  (and (eq (char-after open) ?\{)
+       (save-excursion
+         (goto-char open)
+         (skip-syntax-backward " ")
+         (when (eq (char-before) ?\)) (backward-list))
+         (skip-syntax-backward " ")
+         (skip-syntax-backward "w_")
+         (looking-at "\\_<switch\\_>"))))
+
+(defun php-indent--switch-line-indent (open)
+  "Return the indentation of the `switch' line whose brace is at OPEN."
+  (save-excursion
+    (goto-char open)
+    (skip-syntax-backward " ")
+    (when (eq (char-before) ?\)) (backward-list))
+    (back-to-indentation)
+    (current-column)))
+
+(defun php-indent--altsyntax-indentation ()
+  "Return the indentation for an alternative-syntax line, or nil.
+Handles `endif', `endforeach', `endwhile', `endfor', `endswitch',
+`enddeclare' and the colon forms of `else'/`elseif'."
+  (save-excursion
+    (back-to-indentation)
+    (when (and (looking-at php-indent--altsyntax-re)
+               (let ((kw (match-string-no-properties 1)))
+                 (or (string-prefix-p "end" kw)
+                     ;; else:/elseif (...): must end the line with a colon.
+                     (save-excursion
+                       (end-of-line)
+                       (skip-syntax-backward " ")
+                       (eq (char-before) ?:)))))
+      (let* ((kw (match-string-no-properties 1))
+             (open-re (cond
+                       ((member kw '("else" "elseif" "endif")) "\\_<if\\_>")
+                       ((string= kw "endforeach") "\\_<foreach\\_>")
+                       ((string= kw "endwhile") "\\_<while\\_>")
+                       ((string= kw "endfor") "\\_<for\\_>")
+                       ((string= kw "endswitch") "\\_<switch\\_>")
+                       ((string= kw "enddeclare") "\\_<declare\\_>"))))
+        (when (and open-re
+                   (php-indent--re-search-backward open-re nil t))
+          (current-indentation))))))
+
+;;; Core indentation
+
+(defun php-indent--proper-indentation (parse-status)
+  "Return the proper indentation for the current line.
+PARSE-STATUS is the result of `syntax-ppss' at the line beginning."
+  (save-excursion
+    (back-to-indentation)
+    (cond ((nth 4 parse-status)      ; inside comment
+           (php-indent--comment-indentation parse-status))
+          ((nth 3 parse-status) 0)   ; inside string/heredoc (kept by line fn)
+          ;; PHP open/close tags anchor to column zero.
+          ((looking-at "<\\?\\|\\?>") (prog-first-column))
+          ((php-indent--altsyntax-indentation))
+          ((php-indent--chained-expression-p))
+          ((php-indent--ctrl-statement-indentation))
+          ((php-indent--multi-line-declaration-indentation))
+          ((php-indent--concat-continuation-indentation))
+          ((and (not (php-indent--static-chain-in-list-p parse-status))
+                (php-indent--statement-continuation-indentation)))
+          ((nth 1 parse-status)
+           (php-indent--bracket-indentation parse-status))
+          ((php-indent--continued-expression-p)
+           php-indent-offset)
+          (t (prog-first-column)))))
+
+(defun php-indent--comment-indentation (parse-status)
+  "Return the indentation of a line inside a block comment.
+PARSE-STATUS is the result of `syntax-ppss'.  Continuation lines of a
+`/* ... */' comment are aligned one column past the star."
+  (let ((start (nth 8 parse-status)))
+    (save-excursion
+      (goto-char start)
+      (if (looking-at "/\\*")
+          (1+ (current-column))
+        (current-column)))))
+
+(defun php-indent--bracket-indentation (parse-status)
+  "Return the indentation for a line inside a bracketed group.
+PARSE-STATUS is the result of `syntax-ppss'; point is at the line's
+indentation."
+  (let ((open (nth 1 parse-status))
+        (same-indent-p (looking-at "[]})]"))
+        (switch-label-p (looking-at "\\(?:case\\_>\\|default\\_>\\)"))
+        (continued-expr-p (and (not (php-indent--static-chain-in-list-p 
parse-status))
+                               (php-indent--continued-expression-p))))
+    (if (php-indent--switch-block-p open)
+        ;; PHP switch/case specific handling.
+        (let ((switch-indent (php-indent--switch-line-indent open)))
+          (cond
+           (same-indent-p (+ switch-indent php-indent-switch-offset))
+           (switch-label-p (+ switch-indent
+                              php-indent-switch-case-offset
+                              php-indent-switch-offset))
+           (t (+ switch-indent
+                 php-indent-switch-case-offset
+                 php-indent-offset
+                 php-indent-switch-offset))))
+      ;; Generic bracket handling (ported from js.el).
+      (goto-char open)
+      (if (or (looking-at "[({[]\\s-*\\(/[/*]\\|$\\)")
+              (save-excursion (forward-char)
+                              (php-indent--broken-arrow-terminates-line-p)))
+          (progn ; nothing following the opening paren/bracket
+            (skip-syntax-backward " ")
+            (when (eq (char-before) ?\))
+              ;; The brace is preceded by a parameter/condition list; the
+              ;; anchor is the start of the whole statement (which may be
+              ;; on an earlier line than the `(' when the parameter list
+              ;; is broken onto its own line).
+              (backward-list)
+              (php-indent--goto-statement-start))
+            (back-to-indentation)
+            (+ (current-column)
+               (cond (same-indent-p 0)
+                     (continued-expr-p (* 2 php-indent-offset))
+                     (t (+ php-indent-offset
+                           (pcase (char-after open)
+                             (?\( php-indent-paren-offset)
+                             (?\[ php-indent-square-offset)
+                             (?\{ php-indent-curly-offset)
+                             (_ 0)))))))
+        ;; Something follows the opening bracket: align to it.
+        (unless same-indent-p
+          (forward-char)
+          (skip-chars-forward " \t"))
+        (current-column)))))
+
+;;; Heredoc / HTML helpers
+
+(defun php-indent--in-html-p ()
+  "Return non-nil if the current line lies in an HTML (non-PHP) region.
+A region between a `?>' and the next `<?php'/`<?='/`<?' is HTML.  When no
+PHP open/close tag precedes the line, it is treated as PHP."
+  (save-excursion
+    (goto-char (line-beginning-position))
+    (and (re-search-backward "<\\?php\\|<\\?=\\|<\\?\\|\\?>" nil t)
+         (string= (match-string 0) "?>"))))
+
+(defun php-indent--heredoc-closing-line-p ()
+  "Return non-nil if the current line is a heredoc/nowdoc closing marker.
+The closing marker is a bare identifier, optionally followed by `;', `,'
+or `)', on its own line."
+  (save-excursion
+    (beginning-of-line)
+    (looking-at "[ \t]*[A-Za-z_][A-Za-z0-9_]*[ \t]*[;,)]?[ \t]*$")))
+
+;;; Entry points
+
+;;;###autoload
+(defun php-indent-line ()
+  "Indent the current line as PHP.
+Intended for use as `indent-line-function'.  Lines inside an HTML region
+or inside a string/heredoc body are left unchanged; heredoc closing
+markers are forced to column zero."
+  (interactive)
+  (let* ((parse-status
+          (save-excursion (syntax-ppss (line-beginning-position))))
+         (offset (- (point) (save-excursion (back-to-indentation) (point)))))
+    (cond
+     ;; Do not touch lines that belong to an HTML (?> ... <?php) region.
+     ((php-indent--in-html-p) nil)
+     ;; Heredoc/nowdoc closing marker: force column zero.
+     ((and (nth 3 parse-status)
+           (php-indent--heredoc-closing-line-p))
+      (indent-line-to 0)
+      (when (> offset 0) (forward-char offset)))
+     ;; Inside a string or heredoc body: keep the current indentation.
+     ((nth 3 parse-status) nil)
+     (t
+      (indent-line-to (php-indent--proper-indentation parse-status))
+      (when (> offset 0) (forward-char offset))))))
+
+;;;###autoload
+(defun php-indent-region (start end)
+  "Indent each PHP line between START and END."
+  (interactive "r")
+  (save-excursion
+    (goto-char start)
+    (let ((end-marker (copy-marker end)))
+      (unwind-protect
+          (while (< (point) end-marker)
+            (unless (and (bolp) (eolp))
+              (php-indent-line))
+            (forward-line 1))
+        (set-marker end-marker nil)))))
+
+(provide 'php-indent)
+;;; php-indent.el ends here

Reply via email to