001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.parser;
020
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.Map;
026import java.util.function.UnaryOperator;
027
028import org.apache.maven.doxia.sink.Sink;
029import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
030import org.codehaus.plexus.util.xml.pull.XmlPullParser;
031
032/**
033 * Acts as bridge between legacy parsers relying on <a href="https://www.w3.org/TR/xhtml1/">XHTML 1.0 Transitional (based on HTML4)</a>
034 * and the {@link Xhtml5BaseParser} only supporting (X)HTML5 elements/attributes.
035 *
036 * Adds support for elements/attributes which <a href="https://html.spec.whatwg.org/#non-conforming-features">became obsolete in HTML5</a> but are
037 * commonly used for XDoc/FML.
038 *
039 * @see <a href="https://www.w3.org/TR/html5-diff/">HTML5 Differences from HTML4</a>
040 */
041public class Xhtml1BaseParser extends Xhtml5BaseParser {
042
043    private static final class AttributeMapping {
044        private final String sourceName;
045        private final String targetName;
046        private final UnaryOperator<String> valueMapper;
047        private final MergeSemantics mergeSemantics;
048
049        enum MergeSemantics {
050            OVERWRITE,
051            IGNORE,
052            PREPEND
053        }
054
055        AttributeMapping(String sourceAttribute, String targetAttribute, MergeSemantics mergeSemantics) {
056            this(sourceAttribute, targetAttribute, UnaryOperator.identity(), mergeSemantics);
057        }
058
059        AttributeMapping(
060                String sourceName,
061                String targetName,
062                UnaryOperator<String> valueMapper,
063                MergeSemantics mergeSemantics) {
064            super();
065            this.sourceName = sourceName;
066            this.targetName = targetName;
067            this.valueMapper = valueMapper;
068            this.mergeSemantics = mergeSemantics;
069        }
070
071        public String getSourceName() {
072            return sourceName;
073        }
074
075        public String getTargetName() {
076            return targetName;
077        }
078
079        public UnaryOperator<String> getValueMapper() {
080            return valueMapper;
081        }
082
083        public String mergeValue(String oldValue, String newValue) {
084            final String mergedValue;
085            switch (mergeSemantics) {
086                case IGNORE:
087                    mergedValue = oldValue;
088                    break;
089                case OVERWRITE:
090                    mergedValue = newValue;
091                    break;
092                default:
093                    mergedValue = newValue + " " + oldValue;
094            }
095            return mergedValue;
096        }
097    }
098
099    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}