View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.preprocessor;
16  
17  import org.htmlunit.ScriptPreProcessor;
18  import org.htmlunit.html.HtmlElement;
19  import org.htmlunit.html.HtmlPage;
20  
21  /**
22   * A {@link ScriptPreProcessor} implementation that applies compatibility patches to
23   * htmx 2.0.3 - 2.0.7 versions of the htmx JavaScript library.
24   * <p>
25   * This preprocessor rewrites certain ECMAScript syntax constructs
26   * (such as spread operator usage in array push and for-of loops over array copies)
27   * to equivalent ES5-compatible code. This is necessary because these features are
28   * not yet supported by the JavaScript engine htmlunit-corejs (Rhino).
29   * <p>
30   * The class can be chained with other {@link ScriptPreProcessor} instances via its constructor.
31   * <p>
32   * Supported patches include:
33   * <ul>
34   *   <li>Replacing <code>result.push(...toArray(...))</code> with <code>result.push.apply(result, toArray(...))</code></li>
35   *   <li>Replacing <code>result.push(...findAttributeTargets(...))</code> with <code>result.push.apply(result, findAttributeTargets(...))</code></li>
36   *   <li>Replacing <code>for (const preservedElt of [...pantry.children])</code> with <code>for (const preservedElt of Array.from(pantry.children))</code></li>
37   *   <li>Similar replacements for minified htmx scripts (e.g., <code>htmx.min.js</code>)</li>
38   * </ul>
39   * <p>
40   * <b>Usage Example:</b>
41   * <pre>
42   * try (WebClient webClient = new WebClient()) {
43   *     webClient.setScriptPreProcessor(new HtmxTwoZeroSevenScriptPreProcessor());
44   *     // use webClient as needed
45   * }
46   * </pre>
47   *
48   * @author Ronald Brill
49   * @see ScriptPreProcessor
50   */
51  public class HtmxTwoZeroSevenScriptPreProcessor implements ScriptPreProcessor {
52  
53      private final ScriptPreProcessor nextScriptPreProcessor_;
54  
55      /**
56       * Ctor.
57       */
58      public HtmxTwoZeroSevenScriptPreProcessor() {
59          nextScriptPreProcessor_ = null;
60      }
61  
62      /**
63       * Ctor.
64       * @param nextScriptPreProcessor the next {@link ScriptPreProcessor}
65       */
66      public HtmxTwoZeroSevenScriptPreProcessor(final ScriptPreProcessor nextScriptPreProcessor) {
67          nextScriptPreProcessor_ = nextScriptPreProcessor;
68      }
69  
70      /**
71       * {@inheritDoc}
72       */
73      @Override
74      public String preProcess(final HtmlPage htmlPage, final String sourceCode, final String sourceName,
75              final int lineNumber, final HtmlElement htmlElement) {
76  
77          String patchedSourceCode = sourceCode;
78  
79          if (sourceName.contains("/htmx.js") && !sourceName.contains("/htmx.js#")) {
80              patchedSourceCode = sourceCode.replace(
81                      "result.push(...toArray(rootNode.querySelectorAll(standardSelector)))",
82                      "result.push.apply(result, toArray(rootNode.querySelectorAll(standardSelector)))");
83  
84              patchedSourceCode = patchedSourceCode.replace(
85                      "result.push(...findAttributeTargets(eltToInheritFrom, attrName))",
86                      "result.push.apply(result, findAttributeTargets(eltToInheritFrom, attrName))");
87  
88              patchedSourceCode = patchedSourceCode.replace(
89                      "for (const preservedElt of [...pantry.children]) {",
90                      "for (const preservedElt of Array.from(pantry.children)) {");
91          }
92          else if (sourceName.contains("/htmx.min.js") && !sourceName.contains("/htmx.min.js#")) {
93              // 2.0.4
94              patchedSourceCode = sourceCode.replace(
95                      "i.push(...M(c.querySelectorAll(e)))",
96                      "i.push.apply(i,M(c.querySelectorAll(e)))");
97  
98              // 2.0.7
99              patchedSourceCode = patchedSourceCode.replace(
100                     "i.push(...F(c.querySelectorAll(e)))",
101                     "i.push.apply(i,F(c.querySelectorAll(e)))");
102 
103             patchedSourceCode = patchedSourceCode.replace(
104                     "r.push(...we(i,n))",
105                     "r.push.apply(r,we(i,n))");
106 
107             patchedSourceCode = patchedSourceCode.replace(
108                     "for(const t of[...e.children]){",
109                     "for(const t of Array.from(e.children)){");
110         }
111 
112         if (nextScriptPreProcessor_ != null) {
113             return nextScriptPreProcessor_.preProcess(htmlPage, patchedSourceCode, sourceName, lineNumber, htmlElement);
114         }
115 
116         return patchedSourceCode;
117     }
118 }