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.module.xhtml5; 020 021import javax.inject.Named; 022import javax.inject.Singleton; 023import javax.swing.text.html.HTML.Attribute; 024 025import java.io.IOException; 026import java.io.Reader; 027import java.io.StringReader; 028import java.io.StringWriter; 029import java.util.LinkedHashMap; 030import java.util.Map; 031 032import org.apache.commons.io.IOUtils; 033import org.apache.maven.doxia.macro.MacroExecutionException; 034import org.apache.maven.doxia.macro.MacroRequest; 035import org.apache.maven.doxia.macro.manager.MacroNotFoundException; 036import org.apache.maven.doxia.parser.ParseException; 037import org.apache.maven.doxia.parser.Xhtml5BaseParser; 038import org.apache.maven.doxia.sink.Sink; 039import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 040import org.apache.maven.doxia.util.DoxiaStringUtils; 041import org.codehaus.plexus.util.xml.pull.XmlPullParser; 042import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046/** 047 * Parse an xhtml5 model and emit events into a Doxia Sink. 048 */ 049@Singleton 050@Named("xhtml") 051public class Xhtml5Parser extends Xhtml5BaseParser implements Xhtml5Markup { 052 private static final Logger LOGGER = LoggerFactory.getLogger(Xhtml5Parser.class); 053 054 /** For verbatim source. */ 055 protected boolean source; 056 057 /** Empty elements don't write a closing tag. */ 058 private boolean isEmptyElement; 059 060 /** 061 * The source content of the input reader. Used to pass into macros. 062 */ 063 private String sourceContent; 064 065 protected void handleStartTag(XmlPullParser parser, Sink sink) 066 throws XmlPullParserException, MacroExecutionException { 067 isEmptyElement = parser.isEmptyElementTag(); 068 069 SinkEventAttributeSet attribs = getAttributesFromParser(parser); 070 071 if (parser.getName().equals(HTML.toString())) { 072 // Do nothing 073 return; 074 } else if (parser.getName().equals(HEAD.toString())) { 075 sink.head(attribs); 076 } else if (parser.getName().equals(TITLE.toString())) { 077 sink.title(attribs); 078 } else if (parser.getName().equals(META.toString())) { 079 String name = parser.getAttributeValue(null, Attribute.NAME.toString()); 080 String content = parser.getAttributeValue(null, Attribute.CONTENT.toString()); 081 082 if ("author".equals(name)) { 083 sink.author(null); 084 085 sink.text(content); 086 087 sink.author_(); 088 } else if ("date".equals(name)) { 089 sink.date(null); 090 091 sink.text(content); 092 093 sink.date_(); 094 } else { 095 sink.unknown("meta", new Object[] {TAG_TYPE_SIMPLE}, attribs); 096 } 097 } 098 /* 099 * The ADDRESS element may be used by authors to supply contact information 100 * for a model or a major part of a model such as a form. This element 101 * often appears at the beginning or end of a model. 102 */ 103 else if (parser.getName().equals(ADDRESS.toString())) { 104 sink.address(attribs); 105 } else if (parser.getName().equals(BODY.toString())) { 106 sink.body(attribs); 107 } else if (parser.getName().equals(DIV.toString())) { 108 String divClass = parser.getAttributeValue(null, Attribute.CLASS.toString()); 109 110 if ("verbatim source".equals(divClass)) { 111 this.source = true; 112 } 113 114 baseStartTag(parser, sink); // pick up other divs 115 } 116 /* 117 * The PRE element tells visual user agents that the enclosed text is 118 * "preformatted". When handling preformatted text, visual user agents: 119 * - May leave white space intact. 120 * - May render text with a fixed-pitch font. 121 * - May disable automatic word wrap. 122 * - Must not disable bidirectional processing. 123 * Non-visual user agents are not required to respect extra white space 124 * in the content of a PRE element. 125 */ 126 else if (parser.getName().equals(PRE.toString())) { 127 if (source) { 128 attribs.addAttributes(SinkEventAttributeSet.SOURCE); 129 } 130 131 verbatim(); 132 133 sink.verbatim(attribs); 134 } else if (!baseStartTag(parser, sink)) { 135 if (isEmptyElement) { 136 handleUnknown(parser, sink, TAG_TYPE_SIMPLE); 137 } else { 138 handleUnknown(parser, sink, TAG_TYPE_START); 139 } 140 141 LOGGER.warn( 142 "Unrecognized xhtml5 tag <{}> at [{}:{}]", 143 parser.getName(), 144 parser.getLineNumber(), 145 parser.getColumnNumber()); 146 } 147 } 148 149 protected void handleEndTag(XmlPullParser parser, Sink sink) 150 throws XmlPullParserException, MacroExecutionException { 151 if (parser.getName().equals(HTML.toString())) { 152 // Do nothing 153 return; 154 } else if (parser.getName().equals(HEAD.toString())) { 155 sink.head_(); 156 } else if (parser.getName().equals(TITLE.toString())) { 157 sink.title_(); 158 } else if (parser.getName().equals(BODY.toString())) { 159 emitHeadingSections(0, sink, false); 160 161 sink.body_(); 162 } else if (parser.getName().equals(ADDRESS.toString())) { 163 sink.address_(); 164 } else if (parser.getName().equals(DIV.toString())) { 165 this.source = false; 166 baseEndTag(parser, sink); 167 } else if (!baseEndTag(parser, sink)) { 168 if (!isEmptyElement) { 169 handleUnknown(parser, sink, TAG_TYPE_END); 170 } 171 } 172 173 isEmptyElement = false; 174 } 175 176 @Override 177 protected void handleComment(XmlPullParser parser, Sink sink) throws XmlPullParserException { 178 String text = getText(parser).trim(); 179 180 if (text.startsWith("MACRO") && !isSecondParsing()) { 181 processMacro(text, sink); 182 } else { 183 super.handleComment(parser, sink); 184 } 185 } 186 187 /** process macro embedded in XHTML commment */ 188 private void processMacro(String text, Sink sink) throws XmlPullParserException { 189 String s = text.substring(text.indexOf('{') + 1, text.indexOf('}')); 190 s = escapeForMacro(s); 191 String[] params = DoxiaStringUtils.split(s, "|"); 192 String macroName = params[0]; 193 194 Map<String, Object> parameters = new LinkedHashMap<>(); 195 for (int i = 1; i < params.length; i++) { 196 String[] param = DoxiaStringUtils.split(params[i], "="); 197 if (param.length == 1) { 198 throw new XmlPullParserException("Missing 'key=value' pair for macro parameter: " + params[i]); 199 } 200 201 String key = unescapeForMacro(param[0]); 202 String value = unescapeForMacro(param[1]); 203 parameters.put(key, value); 204 } 205 206 MacroRequest request = new MacroRequest(sourceContent, new Xhtml5Parser(), parameters, getBasedir()); 207 208 try { 209 executeMacro(macroName, request, sink); 210 } catch (MacroExecutionException e) { 211 throw new XmlPullParserException("Unable to execute macro in the document: " + macroName); 212 } catch (MacroNotFoundException me) { 213 throw new XmlPullParserException("Macro not found: " + macroName); 214 } 215 } 216 217 /** 218 * escapeForMacro 219 * 220 * @param s String 221 * @return String 222 */ 223 private String escapeForMacro(String s) { 224 if (s == null || s.length() < 1) { 225 return s; 226 } 227 228 String result = s; 229 230 // use some outrageously out-of-place chars for text 231 // (these are device control one/two in unicode) 232 result = DoxiaStringUtils.replace(result, "\\=", "\u0011"); 233 result = DoxiaStringUtils.replace(result, "\\|", "\u0012"); 234 235 return result; 236 } 237 238 /** 239 * unescapeForMacro 240 * 241 * @param s String 242 * @return String 243 */ 244 private String unescapeForMacro(String s) { 245 if (s == null || s.length() < 1) { 246 return s; 247 } 248 249 String result = s; 250 251 result = DoxiaStringUtils.replace(result, "\u0011", "="); 252 result = DoxiaStringUtils.replace(result, "\u0012", "|"); 253 254 return result; 255 } 256 257 /** 258 * {@inheritDoc} 259 */ 260 protected void init() { 261 super.init(); 262 263 this.source = false; 264 this.isEmptyElement = false; 265 } 266 267 public void parse(Reader source, Sink sink, String reference) throws ParseException { 268 this.sourceContent = null; 269 270 try (Reader reader = source) { 271 StringWriter contentWriter = new StringWriter(); 272 IOUtils.copy(reader, contentWriter); 273 sourceContent = contentWriter.toString(); 274 } catch (IOException ex) { 275 throw new ParseException("Error reading the input source", ex); 276 } 277 278 try { 279 super.parse(new StringReader(sourceContent), sink, reference); 280 } finally { 281 this.sourceContent = null; 282 } 283 } 284}