View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.10 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   * <p>
31   * The class can be chained with other {@link ScriptPreProcessor} instances via its constructor.
32   * </p>
33   * <p>
34   * Supported patches include:
35   * </p>
36   * <ul>
37   *   <li>Replacing <code>result.push(...toArray(...))</code> with <code>result.push.apply(result, toArray(...))</code></li>
38   *   <li>Replacing <code>result.push(...findAttributeTargets(...))</code> with <code>result.push.apply(result, findAttributeTargets(...))</code></li>
39   *   <li>Replacing <code>for (const preservedElt of [...pantry.children])</code> with <code>for (const preservedElt of Array.from(pantry.children))</code></li>
40   *   <li>Similar replacements for minified htmx scripts (e.g., <code>htmx.min.js</code>)</li>
41   * </ul>
42   * <p>
43   * <b>Usage Example:</b>
44   * </p>
45   * <pre>
46   * try (WebClient webClient = new WebClient()) {
47   *     webClient.setScriptPreProcessor(new HtmxTwoZeroSevenScriptPreProcessor());
48   *     // use webClient as needed
49   * }
50   * </pre>
51   *
52   * @author Ronald Brill
53   * @see ScriptPreProcessor
54   */
55  public class HtmxTwoZeroSevenScriptPreProcessor implements ScriptPreProcessor {
56  
57      private final ScriptPreProcessor nextScriptPreProcessor_;
58  
59      /**
60       * Ctor.
61       */
62      public HtmxTwoZeroSevenScriptPreProcessor() {
63          nextScriptPreProcessor_ = null;
64      }
65  
66      /**
67       * Ctor.
68       * @param nextScriptPreProcessor the next {@link ScriptPreProcessor}
69       */
70      public HtmxTwoZeroSevenScriptPreProcessor(final ScriptPreProcessor nextScriptPreProcessor) {
71          nextScriptPreProcessor_ = nextScriptPreProcessor;
72      }
73  
74      /**
75       * {@inheritDoc}
76       */
77      @Override
78      public String preProcess(final HtmlPage htmlPage, final String sourceCode, final String sourceName,
79              final int lineNumber, final HtmlElement htmlElement) {
80  
81          String patchedSourceCode = sourceCode;
82  
83          if (sourceName.contains("/htmx.js") && !sourceName.contains("/htmx.js#")) {
84              patchedSourceCode = sourceCode.replace(
85                      "result.push(...toArray(rootNode.querySelectorAll(standardSelector)))",
86                      "result.push.apply(result, toArray(rootNode.querySelectorAll(standardSelector)))");
87  
88              patchedSourceCode = patchedSourceCode.replace(
89                      "result.push(...findAttributeTargets(eltToInheritFrom, attrName))",
90                      "result.push.apply(result, findAttributeTargets(eltToInheritFrom, attrName))");
91  
92              patchedSourceCode = patchedSourceCode.replace(
93                      "for (const preservedElt of [...pantry.children]) {",
94                      "for (const preservedElt of Array.from(pantry.children)) {");
95          }
96          else if (sourceName.contains("/htmx.min.js") && !sourceName.contains("/htmx.min.js#")) {
97              // 2.0.4
98              patchedSourceCode = sourceCode.replace(
99                      "i.push(...M(c.querySelectorAll(e)))",
100                     "i.push.apply(i,M(c.querySelectorAll(e)))");
101 
102             // 2.0.7
103             patchedSourceCode = patchedSourceCode.replace(
104                     "i.push(...F(c.querySelectorAll(e)))",
105                     "i.push.apply(i,F(c.querySelectorAll(e)))");
106 
107             patchedSourceCode = patchedSourceCode.replace(
108                     "r.push(...we(i,n))",
109                     "r.push.apply(r,we(i,n))");
110 
111             patchedSourceCode = patchedSourceCode.replace(
112                     "for(const t of[...e.children]){",
113                     "for(const t of Array.from(e.children)){");
114 
115             // 2.0.10
116             patchedSourceCode = patchedSourceCode.replace(
117                 "i.push(...F(u.querySelectorAll(e)))",
118                 "i.push.apply(i,F(u.querySelectorAll(e)))");
119 
120             patchedSourceCode = patchedSourceCode.replace(
121                 "r.push(...ve(i,n))",
122                 "r.push.apply(r,ve(i,n))");
123         }
124 
125         if (nextScriptPreProcessor_ != null) {
126             return nextScriptPreProcessor_.preProcess(htmlPage, patchedSourceCode, sourceName, lineNumber, htmlElement);
127         }
128 
129         return patchedSourceCode;
130     }
131 }