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

    Fall back on an HTML mode for Blade when web-mode is missing
    
    Opening a .blade.php file without web-mode installed did not warn and
    carry on -- it failed.  php-derivation-major-mode warned but left MODE
    set to the unavailable web-mode, so php-mode-maybe went on to funcall it
    and signalled void-function.  Every other template zeroed MODE and fell
    back on php-default-major-mode; Blade was the one case that could not
    open at all.
    
    Degrade instead.  A Blade template is mostly HTML plus Blade's own
    directives -- it is not PHP, so php-mode cannot read it either -- and an
    HTML mode renders the bulk of it correctly.  The new
    php-blade-template-major-mode-fallback names the modes to try
    (mhtml-mode then html-mode, both built in); set it to nil to opt out and
    get php-default-major-mode as before.  The warning now says which mode
    was substituted rather than claiming Blade is unsupported.
    
    The fallback deliberately applies to Blade only.  Other templates are
    PHP with HTML in them, which php-mode does read, and they share
    php-html-template-major-mode with the auto-detection path --
    php-project-php-file-as-template defaults to `auto', so any .php file
    holding an HTML tag reaches it.  Extending the fallback there would take
    most PHP files away from php-mode for everyone without web-mode.  A test
    pins that behaviour down.
    
    Uses cl-loop, as php-leading-tokens already does; php.el requires
    cl-lib at compile time and cl-macs is loaded by the time it runs.
---
 lisp/php.el            | 36 ++++++++++++++++++++++++++++++++----
 tests/php-mode-test.el | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 4 deletions(-)

diff --git a/lisp/php.el b/lisp/php.el
index 2e072376f2..b02fb41a85 100644
--- a/lisp/php.el
+++ b/lisp/php.el
@@ -180,6 +180,20 @@ a completion list."
   :tag "PHP Blade Template Major Mode"
   :type 'function)
 
+(defcustom php-blade-template-major-mode-fallback '(mhtml-mode html-mode)
+  "Major modes to fall back on for a Blade template.
+
+Used when `php-blade-template-major-mode' — `web-mode' by default — is
+not installed.  The first entry whose function is defined wins; when
+none is, `php-default-major-mode' is used.
+
+A Blade template is mostly HTML plus Blade's own directives, so an HTML
+mode reads it far better than `php-mode' does.  Set this to nil to opt
+out and get `php-default-major-mode' instead."
+  :group 'php
+  :tag "PHP Blade Template Major Mode Fallback"
+  :type '(repeat function))
+
 (defcustom php-template-mode-alist
   `(("\\.blade" . ,php-blade-template-major-mode)
     ("\\.phpt\\'" . ,(if (fboundp 'phpt-mode) 'phpt-mode 
php-default-major-mode))
@@ -623,6 +637,11 @@ indentation."
         (setq php--buffer-has-html-tag-cache (cons tick result))
         result))))
 
+(defun php--blade-template-fallback-mode ()
+  "Return the first available mode of 
`php-blade-template-major-mode-fallback'."
+  (cl-loop for mode in php-blade-template-major-mode-fallback
+           thereis (and (fboundp mode) mode)))
+
 (defun php-derivation-major-mode ()
   "Return major mode for PHP file by file-name and its content."
   (let ((mode (assoc-default buffer-file-name
@@ -638,10 +657,19 @@ indentation."
         (when (php-buffer-has-html-tag)
           (setq mode php-html-template-major-mode)))))
     (when (and mode (not (fboundp mode)))
-      (if (string-match-p "\\.blade\\." buffer-file-name)
-          (warn "php-mode is NOT support blade template. %s"
-                "Please install `web-mode' package")
-        (setq mode nil)))
+      ;; A Blade template is not PHP, so `php-default-major-mode' cannot
+      ;; read it at all; degrade to an HTML mode instead.  Every other
+      ;; template is PHP with HTML in it, and keeps falling back on
+      ;; `php-default-major-mode' below.
+      (setq mode (when (string-match-p "\\.blade\\." buffer-file-name)
+                   (let ((fallback (php--blade-template-fallback-mode)))
+                     (warn "`%s' is not available for this Blade template; %s.
+Install the `web-mode' package for full Blade support."
+                           mode
+                           (if fallback
+                               (format "using `%s' instead" fallback)
+                             (format "falling back on `%s'" 
php-default-major-mode)))
+                     fallback))))
     (or mode php-default-major-mode)))
 
 ;;;###autoload
diff --git a/tests/php-mode-test.el b/tests/php-mode-test.el
index da662856b1..09a33754f2 100644
--- a/tests/php-mode-test.el
+++ b/tests/php-mode-test.el
@@ -734,6 +734,55 @@ those going through `php-mode-maybe' did not: the same 
buffer became
           (set-buffer-modified-p nil)
           (setq buffer-file-name nil))))))
 
+(defun php-mode-test--derive (file-name &rest body-text)
+  "Return the mode `php-derivation-major-mode' picks for FILE-NAME.
+BODY-TEXT is inserted into the buffer first."
+  (with-temp-buffer
+    (setq buffer-file-name (expand-file-name file-name 
temporary-file-directory))
+    (unwind-protect
+        (progn
+          (apply #'insert body-text)
+          (php-derivation-major-mode))
+      (set-buffer-modified-p nil)
+      (setq buffer-file-name nil))))
+
+(ert-deftest php-mode-test-blade-template-fallback ()
+  "A Blade template degrades to an HTML mode when `web-mode' is missing.
+
+A `.blade.php' file is mostly HTML plus Blade directives, so it is not
+PHP and `php-default-major-mode' cannot read it.  Before the fallback
+existed, `php-derivation-major-mode' returned the unavailable
+`web-mode' anyway and `php-mode-maybe' then failed with
+`void-function'."
+  (let ((php-blade-template-major-mode 'php-mode-test--absent-web-mode)
+        (php-template-mode-alist '(("\\.blade" . 
php-mode-test--absent-web-mode))))
+    (should-not (fboundp 'php-mode-test--absent-web-mode))
+    ;; The chosen fallback must be usable: this is what `php-mode-maybe'
+    ;; funcalls, so an unavailable mode would signal `void-function'.
+    (let ((php-blade-template-major-mode-fallback '(html-mode)))
+      (should (eq 'html-mode (php-mode-test--derive "welcome.blade.php" 
"@extends('x')\n"))))
+    ;; Unavailable entries are skipped.
+    (let ((php-blade-template-major-mode-fallback 
'(php-mode-test--absent-web-mode html-mode)))
+      (should (eq 'html-mode (php-mode-test--derive "welcome.blade.php" 
"@extends('x')\n"))))
+    ;; Opting out falls back on `php-default-major-mode'.
+    (let ((php-blade-template-major-mode-fallback nil)
+          (php-default-major-mode 'php-mode))
+      (should (eq 'php-mode (php-mode-test--derive "welcome.blade.php" 
"@extends('x')\n"))))))
+
+(ert-deftest php-mode-test-html-template-fallback-unchanged ()
+  "A missing `php-html-template-major-mode' still derives to PHP.
+
+Only Blade degrades to an HTML mode.  `php-project-php-file-as-template'
+defaults to `auto', so any .php file holding an HTML tag reaches this
+path; sending those to an HTML mode would take most PHP files away from
+`php-mode' for anyone without `web-mode'."
+  (let ((php-html-template-major-mode 'php-mode-test--absent-web-mode)
+        (php-project-php-file-as-template 'auto)
+        (php-default-major-mode 'php-mode)
+        (php-template-mode-alist nil))
+    (should-not (fboundp 'php-mode-test--absent-web-mode))
+    (should (eq 'php-mode (php-mode-test--derive "page.php" "<div>\n<?php echo 
'hi'; ?>\n</div>\n")))))
+
 (ert-deftest php-mode-test-php74 ()
   "Test highlighting language constructs added in PHP 7.4."
   (with-php-mode-test ("7.4/arrow-function.php" :faces t))

Reply via email to