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}