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.host.css;
16  
17  import java.io.IOException;
18  import java.io.StringReader;
19  import java.util.ArrayList;
20  import java.util.List;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.css.CssStyleSheet;
26  import org.htmlunit.cssparser.dom.AbstractCSSRuleImpl;
27  import org.htmlunit.cssparser.dom.CSSCharsetRuleImpl;
28  import org.htmlunit.cssparser.dom.CSSRuleListImpl;
29  import org.htmlunit.cssparser.parser.InputSource;
30  import org.htmlunit.javascript.JavaScriptEngine;
31  import org.htmlunit.javascript.configuration.JsxClass;
32  import org.htmlunit.javascript.configuration.JsxConstructor;
33  import org.htmlunit.javascript.configuration.JsxFunction;
34  import org.htmlunit.javascript.configuration.JsxGetter;
35  import org.htmlunit.javascript.host.Window;
36  import org.htmlunit.javascript.host.html.HTMLElement;
37  import org.w3c.dom.DOMException;
38  
39  /**
40   * A JavaScript object for {@code CSSStyleSheet}.
41   *
42   * @see <a href="http://msdn2.microsoft.com/en-us/library/ms535871.aspx">MSDN doc</a>
43   * @author Marc Guillemot
44   * @author Daniel Gredler
45   * @author Ahmed Ashour
46   * @author Ronald Brill
47   * @author Guy Burton
48   * @author Frank Danek
49   * @author Carsten Steul
50   */
51  @JsxClass
52  public class CSSStyleSheet extends StyleSheet {
53  
54      private static final Log LOG = LogFactory.getLog(CSSStyleSheet.class);
55  
56      /** The parsed stylesheet which this host object wraps. */
57      private CssStyleSheet styleSheet_;
58  
59      /** The collection of rules defined in this style sheet. */
60      private CSSRuleList cssRules_;
61      private List<Integer> cssRulesIndexFix_;
62  
63      /**
64       * Creates a new empty stylesheet.
65       */
66      public CSSStyleSheet() {
67          super(null);
68          styleSheet_ = new CssStyleSheet(null, (InputSource) null, null);
69      }
70  
71      /**
72       * Creates a new empty stylesheet.
73       */
74      @Override
75      @JsxConstructor
76      public void jsConstructor() {
77          super.jsConstructor();
78          styleSheet_ = new CssStyleSheet(null, (InputSource) null, null);
79      }
80  
81      /**
82       * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
83       * @param element the owning node
84       * @param source the input source which contains the CSS stylesheet which this stylesheet host object represents
85       * @param uri this stylesheet's URI (used to resolved contained @import rules)
86       */
87      public CSSStyleSheet(final HTMLElement element, final InputSource source, final String uri) {
88          super(element);
89  
90          setParentScope(element.getWindow());
91          setPrototype(getPrototype(CSSStyleSheet.class));
92  
93          styleSheet_ = new CssStyleSheet(element.getDomNodeOrDie(), source, uri);
94      }
95  
96      /**
97       * Creates a new stylesheet representing the CSS stylesheet for the specified input source.
98       * @param element the owning node
99       * @param styleSheet the source which contains the CSS stylesheet which this stylesheet host object represents
100      * @param uri this stylesheet's URI (used to resolved contained @import rules)
101      */
102     public CSSStyleSheet(final HTMLElement element, final String styleSheet, final String uri) {
103         super(element);
104 
105         final Window win = element.getWindow();
106 
107         CssStyleSheet css = null;
108         try (InputSource source = new InputSource(new StringReader(styleSheet))) {
109             css = new CssStyleSheet(element.getDomNodeOrDie(), source, uri);
110         }
111         catch (final IOException e) {
112             LOG.error(e.getMessage(), e);
113         }
114 
115         setParentScope(win);
116         setPrototype(getPrototype(CSSStyleSheet.class));
117 
118         styleSheet_ = css;
119     }
120 
121     /**
122      * Creates a new stylesheet representing the specified CSS stylesheet.
123      * @param element the owning node
124      * @param parentScope the parent scope
125      * @param cssStyleSheet the CSS stylesheet which this stylesheet host object represents
126      */
127     public CSSStyleSheet(final HTMLElement element, final Scriptable parentScope,
128             final CssStyleSheet cssStyleSheet) {
129         super(element);
130 
131         setParentScope(parentScope);
132         setPrototype(getPrototype(CSSStyleSheet.class));
133         styleSheet_ = cssStyleSheet;
134     }
135 
136     /**
137      * Returns the wrapped stylesheet.
138      * @return the wrapped stylesheet
139      */
140     public CssStyleSheet getCssStyleSheet() {
141         return styleSheet_;
142     }
143 
144     /**
145      * Retrieves the collection of rules defined in this style sheet.
146      * @return the collection of rules defined in this style sheet
147      */
148     @JsxGetter
149     public CSSRuleList getRules() {
150         return getCssRules();
151     }
152 
153     /**
154      * Returns the collection of rules defined in this style sheet.
155      * @return the collection of rules defined in this style sheet
156      */
157     @JsxGetter
158     public CSSRuleList getCssRules() {
159         initCssRules();
160         return cssRules_;
161     }
162 
163     /**
164      * Inserts a new rule.
165      * @param rule the CSS rule
166      * @param position the position at which to insert the rule
167      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
168      * @return the position of the inserted rule
169      */
170     @JsxFunction
171     public int insertRule(final String rule, final int position) {
172         try {
173             initCssRules();
174             getCssStyleSheet().getWrappedSheet().insertRule(rule, fixIndex(position));
175             refreshCssRules();
176             return position;
177         }
178         catch (final DOMException e) {
179             // in case of error try with an empty rule
180             final int pos = rule.indexOf('{');
181             if (pos > -1) {
182                 final String newRule = rule.substring(0, pos) + "{}";
183                 try {
184                     getCssStyleSheet().getWrappedSheet().insertRule(newRule, fixIndex(position));
185                     refreshCssRules();
186                     return position;
187                 }
188                 catch (final DOMException ex) {
189                     throw JavaScriptEngine.asJavaScriptException(getWindow(), ex.getMessage(), ex.code);
190                 }
191             }
192             throw JavaScriptEngine.asJavaScriptException(getWindow(), e.getMessage(), e.code);
193         }
194     }
195 
196     private void refreshCssRules() {
197         if (cssRules_ == null) {
198             return;
199         }
200 
201         cssRules_.clearRules();
202         cssRulesIndexFix_.clear();
203 
204         final CSSRuleListImpl ruleList = getCssStyleSheet().getWrappedSheet().getCssRules();
205         final List<AbstractCSSRuleImpl> rules = ruleList.getRules();
206         int pos = 0;
207         for (final AbstractCSSRuleImpl rule : rules) {
208             if (rule instanceof CSSCharsetRuleImpl) {
209                 cssRulesIndexFix_.add(pos);
210                 continue;
211             }
212 
213             final CSSRule cssRule = CSSRule.create(this, rule);
214             if (null == cssRule) {
215                 cssRulesIndexFix_.add(pos);
216             }
217             else {
218                 cssRules_.addRule(cssRule);
219             }
220             pos++;
221         }
222 
223         // reset our index also
224         getCssStyleSheet().getWrappedSheet().resetRuleIndex();
225     }
226 
227     private int fixIndex(int index) {
228         for (final int fix : cssRulesIndexFix_) {
229             if (fix > index) {
230                 return index;
231             }
232             index++;
233         }
234         return index;
235     }
236 
237     /**
238      * Deletes an existing rule.
239      * @param position the position of the rule to be deleted
240      * @see <a href="http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet">DOM level 2</a>
241      */
242     @JsxFunction
243     public void deleteRule(final int position) {
244         try {
245             initCssRules();
246             getCssStyleSheet().getWrappedSheet().deleteRule(fixIndex(position));
247             refreshCssRules();
248         }
249         catch (final DOMException e) {
250             throw JavaScriptEngine.asJavaScriptException(getWindow(), e.getMessage(), e.code);
251         }
252     }
253 
254     /**
255      * Adds a new rule.
256      * @see <a href="http://msdn.microsoft.com/en-us/library/aa358796.aspx">MSDN</a>
257      * @param selector the selector name
258      * @param rule the rule
259      * @return always return -1 as of MSDN documentation
260      */
261     @JsxFunction
262     public int addRule(final String selector, final String rule) {
263         String completeRule = selector + " {" + rule + "}";
264         try {
265             initCssRules();
266             getCssStyleSheet().getWrappedSheet().insertRule(completeRule,
267                     getCssStyleSheet().getWrappedSheet().getCssRules().getLength());
268             refreshCssRules();
269         }
270         catch (final DOMException e) {
271             // in case of error try with an empty rule
272             completeRule = selector + " {}";
273             try {
274                 getCssStyleSheet().getWrappedSheet().insertRule(completeRule,
275                         getCssStyleSheet().getWrappedSheet().getCssRules().getLength());
276                 refreshCssRules();
277             }
278             catch (final DOMException ex) {
279                 throw JavaScriptEngine.asJavaScriptException(getWindow(), ex.getMessage(), ex.code);
280             }
281         }
282         return -1;
283     }
284 
285     /**
286      * Deletes an existing rule.
287      * @param position the position of the rule to be deleted
288      * @see <a href="http://msdn.microsoft.com/en-us/library/ms531195(v=VS.85).aspx">MSDN</a>
289      */
290     @JsxFunction
291     public void removeRule(final int position) {
292         try {
293             initCssRules();
294             getCssStyleSheet().getWrappedSheet().deleteRule(fixIndex(position));
295             refreshCssRules();
296         }
297         catch (final DOMException e) {
298             throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
299         }
300     }
301 
302     /**
303      * Returns this stylesheet's URI (used to resolved contained @import rules).
304      * For inline styles this is the page uri.
305      * @return this stylesheet's URI (used to resolved contained @import rules)
306      */
307     @Override
308     public String getUri() {
309         return getCssStyleSheet().getUri();
310     }
311 
312     private void initCssRules() {
313         if (cssRules_ == null) {
314             cssRules_ = new CSSRuleList(this);
315             cssRulesIndexFix_ = new ArrayList<>();
316             refreshCssRules();
317         }
318     }
319 }