View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.parser;
20  
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.Map;
26  import java.util.function.UnaryOperator;
27  
28  import org.apache.maven.doxia.sink.Sink;
29  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
30  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
31  
32  /**
33   * Acts as bridge between legacy parsers relying on <a href="https://www.w3.org/TR/xhtml1/">XHTML 1.0 Transitional (based on HTML4)</a>
34   * and the {@link Xhtml5BaseParser} only supporting (X)HTML5 elements/attributes.
35   *
36   * Adds support for elements/attributes which <a href="https://html.spec.whatwg.org/#non-conforming-features">became obsolete in HTML5</a> but are
37   * commonly used for XDoc/FML.
38   *
39   * @see <a href="https://www.w3.org/TR/html5-diff/">HTML5 Differences from HTML4</a>
40   */
41  public class Xhtml1BaseParser extends Xhtml5BaseParser {
42  
43      private static final class AttributeMapping {
44          private final String sourceName;
45          private final String targetName;
46          private final UnaryOperator<String> valueMapper;
47          private final MergeSemantics mergeSemantics;
48  
49          enum MergeSemantics {
50              OVERWRITE,
51              IGNORE,
52              PREPEND
53          }
54  
55          AttributeMapping(String sourceAttribute, String targetAttribute, MergeSemantics mergeSemantics) {
56              this(sourceAttribute, targetAttribute, UnaryOperator.identity(), mergeSemantics);
57          }
58  
59          AttributeMapping(
60                  String sourceName,
61                  String targetName,
62                  UnaryOperator<String> valueMapper,
63                  MergeSemantics mergeSemantics) {
64              super();
65              this.sourceName = sourceName;
66              this.targetName = targetName;
67              this.valueMapper = valueMapper;
68              this.mergeSemantics = mergeSemantics;
69          }
70  
71          public String getSourceName() {
72              return sourceName;
73          }
74  
75          public String getTargetName() {
76              return targetName;
77          }
78  
79          public UnaryOperator<String> getValueMapper() {
80              return valueMapper;
81          }
82  
83          public String mergeValue(String oldValue, String newValue) {
84              final String mergedValue;
85              switch (mergeSemantics) {
86                  case IGNORE:
87                      mergedValue = oldValue;
88                      break;
89                  case OVERWRITE:
90                      mergedValue = newValue;
91                      break;
92                  default:
93                      mergedValue = newValue + " " + oldValue;
94              }
95              return mergedValue;
96          }
97      }
98  
99      static final String mapAlignToStyle(String alignValue) {
100         switch (alignValue) {
101             case "center":
102             case "left":
103             case "right":
104                 return "text-align: " + alignValue + ";";
105             default:
106                 return null;
107         }
108     }
109 
110     /**
111      * All obsolete attributes in a map with key = affected element name, value = collection if {@link AttributeMapping}s
112      */
113     private static final Map<String, Collection<AttributeMapping>> ATTRIBUTE_MAPPING_TABLE = new HashMap<>();
114 
115     /**
116      * All obsolete elements in a map with key = obsolete element name, value = non-obsolete replacement element name
117      */
118     private static final Map<String, String> ELEMENT_MAPPING_TABLE = new HashMap<>();
119 
120     static {
121         ATTRIBUTE_MAPPING_TABLE.put(
122                 "a", Collections.singleton(new AttributeMapping("name", "id", AttributeMapping.MergeSemantics.IGNORE)));
123 
124         Collection<AttributeMapping> tableMappings = Arrays.asList(
125                 new AttributeMapping(
126                         "border",
127                         "class",
128                         (v) -> (v != null && !v.equals("0")) ? "bodyTableBorder" : null,
129                         AttributeMapping.MergeSemantics.PREPEND),
130                 new AttributeMapping(
131                         "align", "style", Xhtml1BaseParser::mapAlignToStyle, AttributeMapping.MergeSemantics.PREPEND));
132         ATTRIBUTE_MAPPING_TABLE.put("table", tableMappings);
133         ATTRIBUTE_MAPPING_TABLE.put(
134                 "td",
135                 Collections.singleton(new AttributeMapping(
136                         "align", "style", Xhtml1BaseParser::mapAlignToStyle, AttributeMapping.MergeSemantics.PREPEND)));
137         ATTRIBUTE_MAPPING_TABLE.put(
138                 "th",
139                 Collections.singleton(new AttributeMapping(
140                         "align", "style", Xhtml1BaseParser::mapAlignToStyle, AttributeMapping.MergeSemantics.PREPEND)));
141         ELEMENT_MAPPING_TABLE.put("tt", "code");
142         ELEMENT_MAPPING_TABLE.put("strike", "del");
143     }
144 
145     /**
146      * Translates obsolete XHTML 1.0 attributes/elements to valid XHTML5 ones before calling the underlying {@link Xhtml5BaseParser}.
147      */
148     @Override
149     protected boolean baseStartTag(XmlPullParser parser, Sink sink) {
150         SinkEventAttributeSet attribs = getAttributesFromParser(parser);
151         String elementName = parser.getName();
152         Collection<AttributeMapping> attributeMappings = ATTRIBUTE_MAPPING_TABLE.get(elementName);
153         if (attributeMappings != null) {
154             for (AttributeMapping attributeMapping : attributeMappings) {
155                 String attributeValue = (String) attribs.getAttribute(attributeMapping.getSourceName());
156                 if (attributeValue != null) {
157                     String newValue = attributeMapping.getValueMapper().apply(attributeValue);
158                     if (newValue != null) {
159                         String oldValue = (String) attribs.getAttribute(attributeMapping.getTargetName());
160                         if (oldValue != null) {
161                             newValue = attributeMapping.mergeValue(oldValue, newValue);
162                         }
163                         attribs.addAttribute(attributeMapping.getTargetName(), newValue);
164                     }
165                     attribs.removeAttribute(attributeMapping.getSourceName());
166                 }
167             }
168         }
169         String mappedElementName = ELEMENT_MAPPING_TABLE.getOrDefault(elementName, elementName);
170         return super.baseStartTag(mappedElementName, attribs, sink);
171     }
172 
173     @Override
174     protected boolean baseEndTag(XmlPullParser parser, Sink sink) {
175         SinkEventAttributeSet attribs = getAttributesFromParser(parser);
176         String elementName = parser.getName();
177         String mappedElementName = ELEMENT_MAPPING_TABLE.getOrDefault(elementName, elementName);
178         return super.baseEndTag(mappedElementName, attribs, sink);
179     }
180 }