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.svg;
16  
17  import java.util.HashMap;
18  import java.util.LinkedHashMap;
19  import java.util.Map;
20  import java.util.concurrent.ConcurrentHashMap;
21  
22  import org.htmlunit.SgmlPage;
23  import org.htmlunit.html.DomAttr;
24  import org.htmlunit.html.DomElement;
25  import org.htmlunit.html.ElementFactory;
26  import org.htmlunit.util.StringUtils;
27  import org.xml.sax.Attributes;
28  
29  /**
30   * Element factory which creates elements by calling the constructor on a
31   * given {@link org.htmlunit.svg.SvgElement} subclass.
32   *
33   * @author Ahmed Ashour
34   * @author Frank Danek
35   * @author Ronald Brill
36   */
37  public class SvgElementFactory implements ElementFactory {
38  
39      private static final Class<?>[] CLASSES_ = {SvgAltGlyph.class, SvgAltGlyphDef.class, SvgAltGlyphItem.class,
40          SvgAnchor.class, SvgAnimate.class, SvgAnimateColor.class, SvgAnimateMotion.class, SvgAnimateTransform.class,
41          SvgCircle.class, SvgClipPath.class, SvgColorProfile.class, SvgCursor.class, SvgDefs.class, SvgDesc.class,
42          SvgEllipse.class, SvgFeBlend.class, SvgFeColorMatrix.class, SvgFeComponentTransfer.class,
43          SvgFeComposite.class, SvgFeConvolveMatrix.class, SvgFeDiffuseLighting.class, SvgFeDisplacementMap.class,
44          SvgFeDistantLight.class, SvgFeFlood.class, SvgFeFuncA.class, SvgFeFuncB.class, SvgFeFuncG.class,
45          SvgFeFuncR.class, SvgFeGaussianBlur.class, SvgFeImage.class, SvgFeMerge.class, SvgFeMergeNode.class,
46          SvgFeMorphology.class, SvgFeOffset.class, SvgFePointLight.class, SvgFeSpecularLighting.class,
47          SvgFeSpotLight.class, SvgFeTile.class, SvgFeTurbulence.class, SvgFilter.class, SvgFont.class,
48          SvgFontFace.class, SvgFontFaceFormat.class, SvgFontFaceName.class, SvgFontFaceSrc.class,
49          SvgFontFaceURI.class, SvgForeignObject.class, SvgGlyph.class, SvgGlyphRef.class, SvgGroup.class,
50          SvgHKern.class, SvgImage.class, SvgLine.class, SvgLinearGradient.class, SvgMarker.class, SvgMask.class,
51          SvgMetadata.class, SvgMissingGlyph.class, SvgMPath.class, SvgPath.class, SvgPattern.class, SvgPolygon.class,
52          SvgPolyline.class, SvgRadialGradient.class, SvgRect.class, SvgScript.class, SvgSet.class, SvgStop.class,
53          SvgStyle.class, SvgSwitch.class, SvgSymbol.class, SvgText.class, SvgTextPath.class,
54          SvgTitle.class, SvgTRef.class, SvgTSpan.class, SvgUse.class, SvgView.class, SvgVKern.class
55      };
56  
57      private static final Map<String, Class<?>> ELEMENTS_ = new ConcurrentHashMap<>();
58  
59      /**
60       * The svg element names with upper case chars; the html parser lowercases the names of
61       * the elements and restores the case of these names.
62       * @see <a href="https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign">
63       *      HTML Living Standard - adjust SVG tag names</a>
64       */
65      private static final Map<String, String> CASE_SENSITIVE_TAG_NAMES_ = new HashMap<>();
66  
67      static {
68          for (final String name : new String[] {
69              "altGlyph", "altGlyphDef", "altGlyphItem", "animateColor", "animateMotion", "animateTransform",
70              "clipPath", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
71              "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA",
72              "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology",
73              "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence",
74              "foreignObject", "glyphRef", "linearGradient", "radialGradient", "textPath"}) {
75              CASE_SENSITIVE_TAG_NAMES_.put(StringUtils.toRootLowerCase(name), name);
76          }
77      }
78  
79      static {
80          try {
81              for (final Class<?> klass : CLASSES_) {
82                  final String key = klass.getField("TAG_NAME").get(null).toString();
83                  ELEMENTS_.put(StringUtils.toRootLowerCase(key), klass);
84              }
85          }
86          catch (final Exception e) {
87              throw new IllegalStateException(e);
88          }
89      }
90  
91      /**
92       * {@inheritDoc}
93       */
94      @Override
95      public DomElement createElement(final SgmlPage page, final String tagName, final Attributes attributes) {
96          throw new IllegalStateException("SVG.createElement not yet implemented!");
97      }
98  
99      /**
100      * {@inheritDoc}
101      */
102     @Override
103     public DomElement createElementNS(final SgmlPage page, final String namespaceURI, final String qualifiedName,
104             final Attributes attributes) {
105 
106         final Map<String, DomAttr> attributeMap = toMap(page, attributes);
107         // the lookup is case insensitive, but the element keeps the case of the name
108         // (like the browsers, e.g. 'foreignObject')
109         String tagNameLC = StringUtils.toRootLowerCase(qualifiedName);
110         if (tagNameLC.indexOf(':') != -1) {
111             tagNameLC = tagNameLC.substring(tagNameLC.indexOf(':') + 1);
112         }
113         DomElement element = null;
114 
115         final Class<?> klass = ELEMENTS_.get(tagNameLC);
116         if (klass != null) {
117             try {
118                 element = (DomElement) klass.getDeclaredConstructors()[0]
119                         .newInstance(namespaceURI, qualifiedName, page, attributeMap);
120             }
121             catch (final Exception e) {
122                 throw new IllegalStateException(e);
123             }
124         }
125         if (element == null) {
126             element = new SvgElement(namespaceURI, qualifiedName, page, attributeMap);
127         }
128         return element;
129     }
130 
131     private static Map<String, DomAttr> toMap(final SgmlPage page, final Attributes attributes) {
132         Map<String, DomAttr> attributeMap = null;
133         if (attributes != null) {
134             attributeMap = new LinkedHashMap<>(attributes.getLength());
135             for (int i = 0; i < attributes.getLength(); i++) {
136                 final String qName = attributes.getQName(i);
137                 // browsers consider only first attribute (ex: <div id='foo' id='something'>...</div>)
138                 if (!attributeMap.containsKey(qName)) {
139                     String namespaceURI = attributes.getURI(i);
140                     if (namespaceURI != null && namespaceURI.isEmpty()) {
141                         namespaceURI = null;
142                     }
143                     final DomAttr newAttr = new DomAttr(page, namespaceURI, qName, attributes.getValue(i), true);
144                     attributeMap.put(qName, newAttr);
145                 }
146             }
147         }
148         return attributeMap;
149     }
150 
151     /**
152      * Returns the name of an svg element created by the html parser; the name is lowercased
153      * and the case of some names is restored (e.g. 'foreignobject' becomes 'foreignObject')
154      * like the browsers do it.
155      *
156      * @param tagName the tag name from the html source
157      * @return the adjusted tag name
158      * @see <a href="https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign">
159      *      HTML Living Standard - adjust SVG tag names</a>
160      */
161     public static String adjustTagNameForHtmlParser(final String tagName) {
162         final String tagNameLC = StringUtils.toRootLowerCase(tagName);
163         return CASE_SENSITIVE_TAG_NAMES_.getOrDefault(tagNameLC, tagNameLC);
164     }
165 
166     /**
167      * Returns whether the specified name is a valid SVG tag name.
168      * @param tagNameLowerCase the tag name in lower case
169      * @return whether the specified name is a valid SVG tag name or not
170      */
171     public boolean isSupported(final String tagNameLowerCase) {
172         return ELEMENTS_.containsKey(tagNameLowerCase);
173     }
174 }