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.io.Reader; 022import java.text.CharacterIterator; 023import java.text.StringCharacterIterator; 024import java.util.HashSet; 025import java.util.LinkedList; 026import java.util.Set; 027import java.util.Stack; 028import java.util.regex.Pattern; 029 030import org.apache.maven.doxia.macro.MacroExecutionException; 031import org.apache.maven.doxia.markup.HtmlMarkup; 032import org.apache.maven.doxia.sink.Sink; 033import org.apache.maven.doxia.sink.SinkEventAttributes; 034import org.apache.maven.doxia.sink.impl.EventCapturingSinkProxy; 035import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet; 036import org.apache.maven.doxia.util.DoxiaUtils; 037import org.codehaus.plexus.util.xml.pull.XmlPullParser; 038import org.codehaus.plexus.util.xml.pull.XmlPullParserException; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * Common base parser for XHTML5 (now <a href="https://html.spec.whatwg.org/multipage/#toc-the-xhtml-syntax">HTML Living standard, XML syntax</a>) elements and attributes. 044 * 045 * @see <a href="https://html.spec.whatwg.org/multipage/introduction.html#history-2">HTML Living standard, history</a> 046 */ 047public class Xhtml5BaseParser extends AbstractXmlParser implements HtmlMarkup { 048 private static final Logger LOGGER = LoggerFactory.getLogger(Xhtml5BaseParser.class); 049 050 /** Used to identify if a class string contains `bodyTableBorder` */ 051 private static final Pattern BODYTABLEBORDER_CLASS_PATTERN = 052 Pattern.compile("(?:.*\\s|^)bodyTableBorder(?:\\s.*|$)"); 053 054 private static final Set<String> UNMATCHED_XHTML5_ELEMENTS = new HashSet<>(); 055 private static final Set<String> UNMATCHED_XHTML5_SIMPLE_ELEMENTS = new HashSet<>(); 056 057 static { 058 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.AREA.toString()); 059 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.AUDIO.toString()); 060 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.BUTTON.toString()); 061 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.CANVAS.toString()); 062 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.COL.toString()); 063 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.COLGROUP.toString()); 064 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.COMMAND.toString()); 065 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.DATA.toString()); 066 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.DATALIST.toString()); 067 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.DETAILS.toString()); 068 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.DIALOG.toString()); 069 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.EMBED.toString()); 070 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.FIELDSET.toString()); 071 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.FORM.toString()); 072 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.HGROUP.toString()); 073 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.IFRAME.toString()); 074 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.INPUT.toString()); 075 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.KEYGEN.toString()); 076 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.LABEL.toString()); 077 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.LEGEND.toString()); 078 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.MAP.toString()); 079 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.MENU.toString()); 080 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.MENUITEM.toString()); 081 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.METER.toString()); 082 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.NOSCRIPT.toString()); 083 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.OBJECT.toString()); 084 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.OPTGROUP.toString()); 085 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.OPTION.toString()); 086 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.OUTPUT.toString()); 087 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.PARAM.toString()); 088 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.PICTURE.toString()); 089 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.PROGRESS.toString()); 090 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.SELECT.toString()); 091 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.SOURCE.toString()); 092 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.SUMMARY.toString()); 093 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.SVG.toString()); 094 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.TEMPLATE.toString()); 095 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.TEXTAREA.toString()); 096 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.TBODY.toString()); 097 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.THEAD.toString()); 098 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.TFOOT.toString()); 099 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.TIME.toString()); 100 UNMATCHED_XHTML5_SIMPLE_ELEMENTS.add(HtmlMarkup.TRACK.toString()); 101 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.VAR.toString()); 102 UNMATCHED_XHTML5_ELEMENTS.add(HtmlMarkup.VIDEO.toString()); 103 } 104 105 /** 106 * True if a <script></script> or <style></style> block is read. CDATA sections within are 107 * handled as rawText. 108 */ 109 private boolean scriptBlock; 110 111 /** Used to distinguish <a href=""> from <a name="">. */ 112 private boolean isLink; 113 114 /** If true, the next text event is at the beginning of a line inside a block element, i.e. after a block tag or a line break/end block tag. */ 115 protected boolean isBeginningOfLineInsideBlock = true; 116 117 /** Used to distinguish <a href=""> from <a name="">. */ 118 private boolean isAnchor; 119 120 /** Used for nested lists. */ 121 private int orderedListDepth = 0; 122 123 /** Counts section nesting level of the sections manually set in the HTML document */ 124 private int sectionLevel; 125 126 /** Counts current heading level. This is either the {@link #sectionLevel} if no artificial sections are currently open 127 * for headings or a number higher or lower than {@link #sectionLevel} (for all section currently opened/closed for a preceding heading). 128 * The heading level only changes when a new heading starts, or a section starts or ends. */ 129 private int headingLevel; 130 131 /** Verbatim flag, true whenever we are inside a <pre> tag. */ 132 private boolean inVerbatim; 133 134 /** Used to keep track of closing tags for content events */ 135 private Stack<String> divStack = new Stack<>(); 136 137 /** Used to wrap the definedTerm with its definition, even when one is omitted */ 138 boolean hasDefinitionListItem = false; 139 140 private LinkedList<String> capturedSinkEventNames; 141 142 @Override 143 public void parse(Reader source, Sink sink, String reference) throws ParseException { 144 init(); 145 146 try { 147 capturedSinkEventNames = new LinkedList<>(); 148 Sink capturingSink = EventCapturingSinkProxy.newInstance(sink, capturedSinkEventNames); 149 super.parse(source, capturingSink, reference); 150 } finally { 151 setSecondParsing(false); 152 init(); 153 } 154 } 155 156 /** 157 * {@inheritDoc} 158 * 159 * Adds all XHTML (HTML 5.2) entities to the parser so that they can be recognized and resolved 160 * without additional DTD. 161 */ 162 @Override 163 protected void initXmlParser(XmlPullParser parser) throws XmlPullParserException { 164 super.initXmlParser(parser); 165 } 166 167 /** 168 * <p> 169 * Goes through a common list of possible html5 start tags. These include only tags that can go into 170 * the body of an xhtml5 document and so should be re-usable by different xhtml-based parsers. 171 * </p> 172 * <p> 173 * The currently handled tags are: 174 * </p> 175 * <p> 176 * <code> 177 * <article>, <nav>, <aside>, <section>, <h1>, <h2>, <h3>, 178 * <h4>, <h5>, <header>, <main>, <footer>, <em>, <strong>, 179 * <small>, <s>, <cite>, <q>, <dfn>, <abbr>, <i>, 180 * <b>, <code>, <samp>, <kbd>, <sub>, <sup>, <u>, 181 * <mark>, <ruby>, <rb>, <rt>, <rtc>, <rp>, <bdi>, 182 * <bdo>, <span>, <ins>, <del>, <p>, <pre>, <ul>, 183 * <ol>, <li>, <dl>, <dt>, <dd>, <a>, <table>, 184 * <tr>, <th>, <td>, <caption>, <br/>, <wbr/>, <hr/>, 185 * <img/>. 186 * </code> 187 * </p> 188 * 189 * @param parser A parser. 190 * @param sink the sink to receive the events. 191 * @return True if the event has been handled by this method, i.e. the tag was recognized, false otherwise. 192 */ 193 @SuppressWarnings("deprecation") 194 protected boolean baseStartTag(XmlPullParser parser, Sink sink) { 195 SinkEventAttributeSet attribs = getAttributesFromParser(parser); 196 return baseStartTag(parser.getName(), attribs, sink); 197 } 198 199 /** 200 * Handles a start tag by element name, so that a subclass can map an element name before the common handling 201 * runs. This is what {@link Xhtml1BaseParser} uses to translate obsolete XHTML 1.0 element names. 202 * 203 * @param elementName the name of the element. 204 * @param attribs the attributes of the element, may be null. 205 * @param sink the sink to receive the events. 206 * @return True if the event has been handled by this method, i.e. the tag was recognized, false otherwise. 207 * @deprecated The {@code attribs} parameter will become {@link SinkEventAttributes} in 2.3.0. 208 * An override of this signature will stop being called once the parameter type changes, with no error 209 * at build or run time, so move the override to the new signature when it lands. See <a href="https://github.com/apache/maven-doxia/issues/1074">issue 1074</a>. 210 */ 211 @Deprecated 212 protected boolean baseStartTag(String elementName, SinkEventAttributeSet attribs, Sink sink) { 213 boolean visited = true; 214 isBeginningOfLineInsideBlock = true; 215 switch (elementName) { 216 case "article": 217 sink.article(attribs); 218 break; 219 case "nav": 220 sink.navigation(attribs); 221 break; 222 case "aside": 223 sink.sidebar(attribs); 224 break; 225 case "section": 226 handleSectionStart(sink, attribs); 227 break; 228 case "h1": 229 handleHeadingStart(sink, Sink.SECTION_LEVEL_1, attribs); 230 break; 231 case "h2": 232 handleHeadingStart(sink, Sink.SECTION_LEVEL_2, attribs); 233 break; 234 case "h3": 235 handleHeadingStart(sink, Sink.SECTION_LEVEL_3, attribs); 236 break; 237 case "h4": 238 handleHeadingStart(sink, Sink.SECTION_LEVEL_4, attribs); 239 break; 240 case "h5": 241 handleHeadingStart(sink, Sink.SECTION_LEVEL_5, attribs); 242 break; 243 case "h6": 244 handleHeadingStart(sink, Sink.SECTION_LEVEL_6, attribs); 245 break; 246 case "header": 247 sink.header(attribs); 248 break; 249 case "main": 250 sink.content(attribs); 251 break; 252 case "footer": 253 sink.footer(attribs); 254 break; 255 case "em": 256 attribs.addAttributes(SinkEventAttributeSet.Semantics.EMPHASIS); 257 sink.inline(attribs); 258 isBeginningOfLineInsideBlock = false; 259 break; 260 case "strong": 261 attribs.addAttributes(SinkEventAttributeSet.Semantics.STRONG); 262 sink.inline(attribs); 263 isBeginningOfLineInsideBlock = false; 264 break; 265 case "small": 266 attribs.addAttributes(SinkEventAttributeSet.Semantics.SMALL); 267 sink.inline(attribs); 268 isBeginningOfLineInsideBlock = false; 269 break; 270 case "s": 271 /* deprecated line-through support */ 272 attribs.addAttributes(SinkEventAttributeSet.Semantics.LINE_THROUGH); 273 sink.inline(attribs); 274 isBeginningOfLineInsideBlock = false; 275 break; 276 case "cite": 277 attribs.addAttributes(SinkEventAttributeSet.Semantics.CITATION); 278 sink.inline(attribs); 279 isBeginningOfLineInsideBlock = false; 280 break; 281 case "q": 282 attribs.addAttributes(SinkEventAttributeSet.Semantics.QUOTE); 283 sink.inline(attribs); 284 break; 285 case "dfn": 286 attribs.addAttributes(SinkEventAttributeSet.Semantics.DEFINITION); 287 sink.inline(attribs); 288 isBeginningOfLineInsideBlock = false; 289 break; 290 case "abbr": 291 attribs.addAttributes(SinkEventAttributeSet.Semantics.ABBREVIATION); 292 sink.inline(attribs); 293 isBeginningOfLineInsideBlock = false; 294 break; 295 case "i": 296 attribs.addAttributes(SinkEventAttributeSet.Semantics.ITALIC); 297 sink.inline(attribs); 298 break; 299 case "b": 300 attribs.addAttributes(SinkEventAttributeSet.Semantics.BOLD); 301 sink.inline(attribs); 302 isBeginningOfLineInsideBlock = false; 303 break; 304 case "code": 305 attribs.addAttributes(SinkEventAttributeSet.Semantics.CODE); 306 sink.inline(attribs); 307 isBeginningOfLineInsideBlock = false; 308 break; 309 case "var": 310 attribs.addAttributes(SinkEventAttributeSet.Semantics.VARIABLE); 311 sink.inline(attribs); 312 isBeginningOfLineInsideBlock = false; 313 break; 314 case "samp": 315 attribs.addAttributes(SinkEventAttributeSet.Semantics.SAMPLE); 316 sink.inline(attribs); 317 isBeginningOfLineInsideBlock = false; 318 break; 319 case "kbd": 320 attribs.addAttributes(SinkEventAttributeSet.Semantics.KEYBOARD); 321 sink.inline(attribs); 322 isBeginningOfLineInsideBlock = false; 323 break; 324 case "sup": 325 attribs.addAttributes(SinkEventAttributeSet.Semantics.SUPERSCRIPT); 326 sink.inline(attribs); 327 isBeginningOfLineInsideBlock = false; 328 break; 329 case "sub": 330 attribs.addAttributes(SinkEventAttributeSet.Semantics.SUBSCRIPT); 331 sink.inline(attribs); 332 isBeginningOfLineInsideBlock = false; 333 break; 334 case "u": 335 attribs.addAttributes(SinkEventAttributeSet.Semantics.ANNOTATION); 336 sink.inline(attribs); 337 isBeginningOfLineInsideBlock = false; 338 break; 339 case "mark": 340 attribs.addAttributes(SinkEventAttributeSet.Semantics.HIGHLIGHT); 341 sink.inline(attribs); 342 break; 343 case "ruby": 344 attribs.addAttributes(SinkEventAttributeSet.Semantics.RUBY); 345 sink.inline(attribs); 346 isBeginningOfLineInsideBlock = false; 347 break; 348 case "rb": 349 attribs.addAttributes(SinkEventAttributeSet.Semantics.RUBY_BASE); 350 sink.inline(attribs); 351 isBeginningOfLineInsideBlock = false; 352 break; 353 case "rt": 354 attribs.addAttributes(SinkEventAttributeSet.Semantics.RUBY_TEXT); 355 sink.inline(attribs); 356 isBeginningOfLineInsideBlock = false; 357 break; 358 case "rtc": 359 attribs.addAttributes(SinkEventAttributeSet.Semantics.RUBY_TEXT_CONTAINER); 360 sink.inline(attribs); 361 isBeginningOfLineInsideBlock = false; 362 break; 363 case "rp": 364 attribs.addAttributes(SinkEventAttributeSet.Semantics.RUBY_PARANTHESES); 365 sink.inline(attribs); 366 isBeginningOfLineInsideBlock = false; 367 break; 368 case "bdi": 369 attribs.addAttributes(SinkEventAttributeSet.Semantics.BIDIRECTIONAL_ISOLATION); 370 sink.inline(attribs); 371 isBeginningOfLineInsideBlock = false; 372 break; 373 case "bdo": 374 attribs.addAttributes(SinkEventAttributeSet.Semantics.BIDIRECTIONAL_OVERRIDE); 375 sink.inline(attribs); 376 isBeginningOfLineInsideBlock = false; 377 break; 378 case "span": 379 attribs.addAttributes(SinkEventAttributeSet.Semantics.PHRASE); 380 sink.inline(attribs); 381 isBeginningOfLineInsideBlock = false; 382 break; 383 case "ins": 384 attribs.addAttributes(SinkEventAttributeSet.Semantics.INSERT); 385 sink.inline(attribs); 386 isBeginningOfLineInsideBlock = false; 387 break; 388 case "del": 389 attribs.addAttributes(SinkEventAttributeSet.Semantics.DELETE); 390 sink.inline(attribs); 391 isBeginningOfLineInsideBlock = false; 392 break; 393 case "p": 394 handlePStart(sink, attribs); 395 break; 396 case "div": 397 handleDivStart(attribs, sink); 398 break; 399 case "pre": 400 handlePreStart(attribs, sink); 401 break; 402 case "ul": 403 sink.list(attribs); 404 break; 405 case "ol": 406 handleOLStart(sink, attribs); 407 break; 408 case "li": 409 handleLIStart(sink, attribs); 410 break; 411 case "dl": 412 sink.definitionList(attribs); 413 break; 414 case "dt": 415 if (hasDefinitionListItem) { 416 // close previous listItem 417 sink.definitionListItem_(); 418 } 419 sink.definitionListItem(attribs); 420 hasDefinitionListItem = true; 421 sink.definedTerm(attribs); 422 break; 423 case "dd": 424 if (!hasDefinitionListItem) { 425 sink.definitionListItem(attribs); 426 } 427 sink.definition(attribs); 428 break; 429 case "figure": 430 sink.figure(attribs); 431 break; 432 case "figcaption": 433 sink.figureCaption(attribs); 434 break; 435 case "a": 436 isBeginningOfLineInsideBlock = false; 437 handleAStart(sink, attribs); 438 break; 439 case "table": 440 handleTableStart(sink, attribs); 441 break; 442 case "tr": 443 sink.tableRow(attribs); 444 break; 445 case "th": 446 sink.tableHeaderCell(attribs); 447 break; 448 case "td": 449 sink.tableCell(attribs); 450 break; 451 case "caption": 452 sink.tableCaption(attribs); 453 break; 454 case "br": 455 sink.lineBreak(attribs); 456 break; 457 case "wbr": 458 sink.lineBreakOpportunity(attribs); 459 break; 460 case "hr": 461 sink.horizontalRule(attribs); 462 break; 463 case "img": 464 isBeginningOfLineInsideBlock = false; 465 handleImgStart(sink, attribs); 466 break; 467 case "blockquote": 468 sink.blockquote(attribs); 469 break; 470 case "script": 471 case "style": 472 handleUnknown(elementName, attribs, sink, TAG_TYPE_START); 473 scriptBlock = true; 474 break; 475 default: 476 if (UNMATCHED_XHTML5_ELEMENTS.contains(elementName)) { 477 handleUnknown(elementName, attribs, sink, TAG_TYPE_START); 478 } else if (UNMATCHED_XHTML5_SIMPLE_ELEMENTS.contains(elementName)) { 479 handleUnknown(elementName, attribs, sink, TAG_TYPE_SIMPLE); 480 } else { 481 visited = false; 482 } 483 break; 484 } 485 486 return visited; 487 } 488 489 /** 490 * <p> 491 * Goes through a common list of possible html end tags. 492 * These should be re-usable by different xhtml-based parsers. 493 * The tags handled here are the same as for {@link #baseStartTag(XmlPullParser,Sink)}, 494 * except for the empty elements ({@code <br/>, <hr/>, <img/>}). 495 * </p> 496 * 497 * @param parser A parser. 498 * @param sink the sink to receive the events. 499 * @return True if the event has been handled by this method, false otherwise. 500 */ 501 @SuppressWarnings("deprecation") 502 protected boolean baseEndTag(XmlPullParser parser, Sink sink) { 503 SinkEventAttributeSet attribs = getAttributesFromParser(parser); 504 return baseEndTag(parser.getName(), attribs, sink); 505 } 506 507 /** 508 * Handles an end tag by element name, so that a subclass can map an element name before the common handling 509 * runs. This is what {@link Xhtml1BaseParser} uses to translate obsolete XHTML 1.0 element names. 510 * 511 * @param elementName the name of the element. 512 * @param attribs the attributes of the element, may be null. 513 * @param sink the sink to receive the events. 514 * @return True if the event has been handled by this method, false otherwise. 515 * @deprecated The {@code attribs} parameter will become {@link SinkEventAttributes} in 2.3.0. 516 * An override of this signature will stop being called once the parameter type changes, with no error 517 * at build or run time, so move the override to the new signature when it lands. See <a href="https://github.com/apache/maven-doxia/issues/1074">issue 1074</a>. 518 */ 519 @Deprecated 520 protected boolean baseEndTag(String elementName, SinkEventAttributeSet attribs, Sink sink) { 521 boolean visited = true; 522 isBeginningOfLineInsideBlock = true; 523 524 switch (elementName) { 525 case "p": 526 sink.paragraph_(); 527 break; 528 case "div": 529 handleDivEnd(sink); 530 break; 531 case "pre": 532 verbatim_(); 533 sink.verbatim_(); 534 break; 535 case "ul": 536 sink.list_(); 537 break; 538 case "ol": 539 sink.numberedList_(); 540 orderedListDepth--; 541 break; 542 case "li": 543 handleListItemEnd(sink); 544 break; 545 case "dl": 546 if (hasDefinitionListItem) { 547 sink.definitionListItem_(); 548 hasDefinitionListItem = false; 549 } 550 sink.definitionList_(); 551 break; 552 case "dt": 553 sink.definedTerm_(); 554 break; 555 case "dd": 556 sink.definition_(); 557 sink.definitionListItem_(); 558 hasDefinitionListItem = false; 559 break; 560 case "figure": 561 sink.figure_(); 562 break; 563 case "figcaption": 564 sink.figureCaption_(); 565 break; 566 case "a": 567 isBeginningOfLineInsideBlock = false; 568 handleAEnd(sink); 569 break; 570 case "em": 571 case "strong": 572 case "small": 573 case "s": 574 case "cite": 575 case "q": 576 case "dfn": 577 case "abbr": 578 case "i": 579 case "b": 580 case "code": 581 case "var": 582 case "samp": 583 case "kbd": 584 case "sup": 585 case "sub": 586 case "u": 587 case "mark": 588 case "ruby": 589 case "rb": 590 case "rt": 591 case "rtc": 592 case "rp": 593 case "bdi": 594 case "bdo": 595 case "span": 596 case "ins": 597 case "del": 598 sink.inline_(); 599 isBeginningOfLineInsideBlock = false; 600 break; 601 602 // ---------------------------------------------------------------------- 603 // Tables 604 // ---------------------------------------------------------------------- 605 606 case "table": 607 sink.tableRows_(); 608 sink.table_(); 609 break; 610 case "tr": 611 sink.tableRow_(); 612 break; 613 case "th": 614 sink.tableHeaderCell_(); 615 break; 616 case "td": 617 sink.tableCell_(); 618 break; 619 case "caption": 620 sink.tableCaption_(); 621 break; 622 case "article": 623 sink.article_(); 624 break; 625 case "nav": 626 sink.navigation_(); 627 break; 628 case "aside": 629 sink.sidebar_(); 630 break; 631 case "section": 632 handleSectionEnd(sink); 633 break; 634 case "h1": 635 sink.sectionTitle1_(); 636 break; 637 case "h2": 638 sink.sectionTitle2_(); 639 break; 640 case "h3": 641 sink.sectionTitle3_(); 642 break; 643 case "h4": 644 sink.sectionTitle4_(); 645 break; 646 case "h5": 647 sink.sectionTitle5_(); 648 break; 649 case "h6": 650 sink.sectionTitle6_(); 651 break; 652 case "header": 653 sink.header_(); 654 break; 655 case "main": 656 sink.content_(); 657 break; 658 case "footer": 659 sink.footer_(); 660 break; 661 case "img": 662 isBeginningOfLineInsideBlock = false; 663 break; 664 case "blockquote": 665 sink.blockquote_(); 666 break; 667 case "script": 668 case "style": 669 handleUnknown(elementName, attribs, sink, TAG_TYPE_END); 670 scriptBlock = false; 671 break; 672 default: 673 if (UNMATCHED_XHTML5_ELEMENTS.contains(elementName)) { 674 handleUnknown(elementName, attribs, sink, TAG_TYPE_END); 675 } else { 676 visited = false; 677 } 678 break; 679 } 680 681 return visited; 682 } 683 684 /** 685 * {@inheritDoc} 686 * 687 * Just calls {@link #baseStartTag(XmlPullParser,Sink)}, this should be 688 * overridden by implementing parsers to include additional tags. 689 */ 690 protected void handleStartTag(XmlPullParser parser, Sink sink) 691 throws XmlPullParserException, MacroExecutionException { 692 if (!baseStartTag(parser, sink)) { 693 LOGGER.warn( 694 "Unrecognized xml tag <{}> at [{}:{}]", 695 parser.getName(), 696 parser.getLineNumber(), 697 parser.getColumnNumber()); 698 } 699 } 700 701 /** 702 * {@inheritDoc} 703 * 704 * Just calls {@link #baseEndTag(XmlPullParser,Sink)}, this should be 705 * overridden by implementing parsers to include additional tags. 706 */ 707 protected void handleEndTag(XmlPullParser parser, Sink sink) 708 throws XmlPullParserException, MacroExecutionException { 709 if (!baseEndTag(parser, sink)) { 710 // unrecognized tag is already logged in StartTag 711 } 712 } 713 714 @Override 715 protected void handleText(XmlPullParser parser, Sink sink) throws XmlPullParserException { 716 String text = getText(parser); 717 718 if (!inVerbatim && text != null) { 719 // do special whitespace processing as outlined in 720 // https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Text/Whitespace 721 if (isBeginningOfLineInsideBlock) { 722 // normalize linebreaks 723 processInsignificantLineBreaks(sink, text); 724 // trim leading whitespace from text being emitted 725 // https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Text/Whitespace#trimming_and_positioning 726 String regex = "^\\s+"; 727 text = text.replaceAll(regex, ""); 728 } 729 730 // assume white-space-collapse: collapse for all non-verbatim text (outside of <pre>) 731 text = collapseWhitespace(text); 732 } 733 if ((text != null && !text.isEmpty()) && !isScriptBlock()) { 734 sink.text(text); 735 isBeginningOfLineInsideBlock = false; 736 } 737 } 738 739 /** 740 * Process all line-breaks in the given text which are not significant for the output, i.e. all line-breaks which are not within a verbatim block and 741 * are at the beginning of the given text. 742 * In addition it emits information about the whitespace characters following the line-breaks as they may be relevant for the output (e.g. for indentation). 743 * 744 * @param sink the sink to receive the events. 745 * @param text the text to process. 746 */ 747 protected void processInsignificantLineBreaks(Sink sink, String text) { 748 CharacterIterator it = new StringCharacterIterator(text.replaceAll("\\r\\n?", "\n")); 749 750 boolean wasNewLine = false; 751 int indentLevel = 0; 752 // 753 while (it.current() != CharacterIterator.DONE) { 754 char c = it.current(); 755 if (c == '\n') { 756 if (wasNewLine) { 757 sink.markupLineBreak(indentLevel); 758 } 759 indentLevel = 0; 760 wasNewLine = true; 761 } else if (Character.isWhitespace(c)) { 762 indentLevel++; 763 } else { 764 // once non-whitespace character is reached we assume everything following is relevant and emitted 765 // within the text event 766 break; 767 } 768 it.next(); 769 } 770 if (wasNewLine) { 771 // if the text ends with a newline, we need to emit the last line break 772 sink.markupLineBreak(indentLevel); 773 } 774 } 775 776 /** 777 * @see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Text/Whitespace#how_does_css_process_whitespace">How does CSS process whitespace?</a> 778 * @see <a href="https://drafts.csswg.org/css-text-4/#white-space-processing">CSS Text Module Level 4 - White Space Processing</a> 779 * 780 * @param text 781 * @return 782 */ 783 private static String collapseWhitespace(String text) { 784 // replace all sequences of whitespace characters with a single space (this includes newlines, tabs, etc.) 785 return text.replaceAll("\\s+", " "); 786 } 787 788 @Override 789 protected void handleComment(XmlPullParser parser, Sink sink) throws XmlPullParserException { 790 isBeginningOfLineInsideBlock = false; 791 String text = getText(parser); 792 793 if ("PB".equals(text.trim())) { 794 sink.pageBreak(); 795 } else { 796 if (isEmitComments()) { 797 sink.comment(text); 798 } 799 } 800 } 801 802 @Override 803 protected void handleCdsect(XmlPullParser parser, Sink sink) throws XmlPullParserException { 804 isBeginningOfLineInsideBlock = false; 805 String text = getText(parser); 806 807 if (isScriptBlock()) { 808 sink.unknown(CDATA, new Object[] {CDATA_TYPE, text}, null); 809 } else { 810 sink.text(text); 811 } 812 } 813 814 /** 815 * Shortcut for {@link #emitHeadingSections(int, Sink, boolean)} with last argument being {@code true} 816 * @param newLevel 817 * @param sink 818 * @param attribs 819 * @deprecated Use {@link #emitHeadingSections(int, Sink, boolean)} instead. 820 */ 821 @Deprecated 822 protected void consecutiveSections(int newLevel, Sink sink, SinkEventAttributeSet attribs) { 823 emitHeadingSections(newLevel, sink, true); 824 } 825 826 /** 827 * Make sure sections are nested consecutively and correctly inserted for the given heading level 828 * 829 * <p> 830 * HTML5 heading tags H1 to H5 imply same level sections in Sink API (compare with {@link Sink#sectionTitle(int, SinkEventAttributes)}). 831 * However (X)HTML5 allows headings without explicit surrounding section elements and is also 832 * less strict with non-consecutive heading levels. 833 * This methods both closes open sections which have been added for previous headings and/or opens 834 * sections necessary for the new heading level. 835 * At least one section needs to be opened directly prior the heading due to Sink API restrictions. 836 * </p> 837 * 838 * <p> 839 * For instance, if the following sequence is parsed: 840 * </p> 841 * <pre> 842 * <h2></h2> 843 * <h5></h5> 844 * </pre> 845 * <p> 846 * we have to insert two section starts before we open the <code><h5></code>. 847 * In the following sequence 848 * </p> 849 * <pre> 850 * <h5></h5> 851 * <h2></h2> 852 * </pre> 853 * <p> 854 * we have to close two sections before we open the <code><h2></code>. 855 * </p> 856 * 857 * <p>The current heading level is set to newLevel afterwards.</p> 858 * 859 * @param newLevel the new section level, all upper levels have to be closed. 860 * @param sink the sink to receive the events. 861 * @param enforceNewSection whether to enforce a new section or not 862 */ 863 protected void emitHeadingSections(int newLevel, Sink sink, boolean enforceNewSection) { 864 int lowerBoundSectionLevel = newLevel; 865 if (enforceNewSection) { 866 // close one more if either last event was not section start or the new level is lower than the current one 867 // (in this case the last event may be a section start event but for another level) 868 if (!isLastEventSectionStart() || newLevel < this.headingLevel) { 869 lowerBoundSectionLevel--; 870 } 871 } 872 closeOpenHeadingSections(lowerBoundSectionLevel, sink); 873 openMissingHeadingSections(newLevel, sink); 874 875 this.headingLevel = newLevel; 876 } 877 878 private boolean isLastEventSectionStart() { 879 String lastEventName = capturedSinkEventNames.pollLast(); 880 if (lastEventName == null) { 881 return false; 882 } 883 return lastEventName.startsWith("section") 884 && !lastEventName.endsWith("_") 885 && !lastEventName.startsWith("sectionTitle"); 886 } 887 888 /** 889 * Close open heading sections. 890 * 891 * @param newLevel the new section level, all upper levels have to be closed. 892 * @param sink the sink to receive the events. 893 */ 894 private void closeOpenHeadingSections(int newLevel, Sink sink) { 895 while (this.headingLevel > newLevel) { 896 if (headingLevel >= Sink.SECTION_LEVEL_1 && headingLevel <= Sink.SECTION_LEVEL_6) { 897 sink.section_(headingLevel); 898 } 899 900 this.headingLevel--; 901 } 902 // enforce the previous element is a section 903 } 904 905 /** 906 * Open missing heading sections. 907 * 908 * @param newLevel the new section level, all lower levels have to be opened. 909 * @param sink the sink to receive the events. 910 */ 911 private void openMissingHeadingSections(int newLevel, Sink sink) { 912 while (this.headingLevel < newLevel) { 913 this.headingLevel++; 914 915 if (headingLevel >= Sink.SECTION_LEVEL_1 && headingLevel <= Sink.SECTION_LEVEL_6) { 916 sink.section(headingLevel, null); 917 } 918 } 919 } 920 921 /** 922 * Return the current section level. 923 * 924 * @return the current section level. 925 */ 926 protected int getSectionLevel() { 927 return this.headingLevel; 928 } 929 930 /** 931 * Set the current section level. 932 * 933 * @param newLevel the new section level. 934 */ 935 protected void setSectionLevel(int newLevel) { 936 this.headingLevel = newLevel; 937 } 938 939 /** 940 * Stop verbatim mode. 941 */ 942 protected void verbatim_() { 943 this.inVerbatim = false; 944 } 945 946 /** 947 * Start verbatim mode. 948 */ 949 protected void verbatim() { 950 this.inVerbatim = true; 951 } 952 953 /** 954 * Checks if we are currently inside a <pre> tag. 955 * 956 * @return true if we are currently in verbatim mode. 957 */ 958 protected boolean isVerbatim() { 959 return this.inVerbatim; 960 } 961 962 /** 963 * Checks if we are currently inside a <script> tag. 964 * 965 * @return true if we are currently inside <code><script></code> tags. 966 * @since 1.1.1. 967 */ 968 protected boolean isScriptBlock() { 969 return this.scriptBlock; 970 } 971 972 /** 973 * Checks if the given id is a valid Doxia id and if not, returns a transformed one. 974 * 975 * @param id The id to validate. 976 * @return A transformed id or the original id if it was already valid. 977 * @see DoxiaUtils#encodeId(String) 978 */ 979 protected String validAnchor(String id) { 980 if (!DoxiaUtils.isValidId(id)) { 981 String linkAnchor = DoxiaUtils.encodeId(id); 982 983 LOGGER.debug("Modified invalid link '{}' to '{}'", id, linkAnchor); 984 985 return linkAnchor; 986 } 987 988 return id; 989 } 990 991 @Override 992 protected void init() { 993 super.init(); 994 995 this.scriptBlock = false; 996 this.isLink = false; 997 this.isAnchor = false; 998 this.orderedListDepth = 0; 999 this.headingLevel = 0; 1000 this.inVerbatim = false; 1001 } 1002 1003 private void handleAEnd(Sink sink) { 1004 if (isLink) { 1005 sink.link_(); 1006 isLink = false; 1007 } else if (isAnchor) { 1008 sink.anchor_(); 1009 isAnchor = false; 1010 } 1011 } 1012 1013 private void handleAStart(Sink sink, SinkEventAttributeSet attribs) { 1014 String href = (String) attribs.getAttribute(SinkEventAttributes.HREF); 1015 1016 if (href != null) { 1017 int hashIndex = href.indexOf('#'); 1018 if (hashIndex != -1 1019 && !DoxiaUtils.isExternalLink(href) 1020 && !"external".equals(attribs.getAttribute(SinkEventAttributes.REL))) { 1021 String hash = href.substring(hashIndex + 1); 1022 1023 if (!DoxiaUtils.isValidId(hash)) { 1024 href = href.substring(0, hashIndex) + "#" + DoxiaUtils.encodeId(hash); 1025 1026 LOGGER.debug("Modified invalid link '{}' to '{}'", hash, href); 1027 } 1028 } 1029 sink.link(href, attribs); 1030 isLink = true; 1031 } else { 1032 String id = (String) attribs.getAttribute(SinkEventAttributes.ID); 1033 if (id == null) { 1034 // although the "name" attribute is obsolete in HTML5, it is still allowed 1035 // (https://www.w3.org/TR/html5-diff/#obsolete-attributes) 1036 id = (String) attribs.getAttribute(SinkEventAttributes.NAME); 1037 } 1038 if (id != null) { 1039 sink.anchor(validAnchor(id), attribs); 1040 isAnchor = true; 1041 } 1042 } 1043 } 1044 1045 private boolean handleDivStart(SinkEventAttributeSet attribs, Sink sink) { 1046 String divClass = (String) attribs.getAttribute(SinkEventAttributes.CLASS); 1047 1048 this.divStack.push(divClass); 1049 1050 if ("content".equals(divClass)) { 1051 SinkEventAttributeSet atts = new SinkEventAttributeSet(attribs); 1052 atts.removeAttribute(SinkEventAttributes.CLASS); 1053 sink.content(atts); 1054 } 1055 if ("verbatim".equals(divClass) || "verbatim source".equals(divClass)) { 1056 return false; 1057 } else { 1058 sink.division(attribs); 1059 } 1060 1061 return true; 1062 } 1063 1064 private boolean handleDivEnd(Sink sink) { 1065 String divClass = divStack.pop(); 1066 1067 if ("content".equals(divClass)) { 1068 sink.content_(); 1069 } 1070 if ("verbatim".equals(divClass) || "verbatim source".equals(divClass)) { 1071 return false; 1072 } else { 1073 sink.division_(); 1074 } 1075 1076 return true; 1077 } 1078 1079 private void handleImgStart(Sink sink, SinkEventAttributeSet attribs) { 1080 String src = (String) attribs.getAttribute(SinkEventAttributes.SRC); 1081 1082 if (src != null) { 1083 sink.figureGraphics(src, attribs); 1084 } 1085 } 1086 1087 private void handleLIStart(Sink sink, SinkEventAttributeSet attribs) { 1088 if (orderedListDepth == 0) { 1089 sink.listItem(attribs); 1090 } else { 1091 sink.numberedListItem(attribs); 1092 } 1093 } 1094 1095 private void handleListItemEnd(Sink sink) { 1096 if (orderedListDepth == 0) { 1097 sink.listItem_(); 1098 } else { 1099 sink.numberedListItem_(); 1100 } 1101 } 1102 1103 private void handleOLStart(Sink sink, SinkEventAttributeSet attribs) { 1104 int numbering = Sink.NUMBERING_DECIMAL; 1105 // this will have to be generalized if we handle styles 1106 String style = (String) attribs.getAttribute(SinkEventAttributes.STYLE); 1107 1108 if (style != null) { 1109 switch (style) { 1110 case "list-style-type: upper-alpha;": 1111 numbering = Sink.NUMBERING_UPPER_ALPHA; 1112 break; 1113 case "list-style-type: lower-alpha;": 1114 numbering = Sink.NUMBERING_LOWER_ALPHA; 1115 break; 1116 case "list-style-type: upper-roman;": 1117 numbering = Sink.NUMBERING_UPPER_ROMAN; 1118 break; 1119 case "list-style-type: lower-roman;": 1120 numbering = Sink.NUMBERING_LOWER_ROMAN; 1121 break; 1122 case "list-style-type: decimal;": 1123 numbering = Sink.NUMBERING_DECIMAL; 1124 break; 1125 default: 1126 // ignore all other 1127 } 1128 } 1129 1130 sink.numberedList(numbering, attribs); 1131 orderedListDepth++; 1132 } 1133 1134 private void handlePStart(Sink sink, SinkEventAttributeSet attribs) { 1135 sink.paragraph(attribs); 1136 } 1137 1138 /* 1139 * The PRE element tells visual user agents that the enclosed text is 1140 * "preformatted". When handling preformatted text, visual user agents: 1141 * - May leave white space intact. 1142 * - May render text with a fixed-pitch font. 1143 * - May disable automatic word wrap. 1144 * - Must not disable bidirectional processing. 1145 * Non-visual user agents are not required to respect extra white space 1146 * in the content of a PRE element. 1147 */ 1148 private void handlePreStart(SinkEventAttributeSet attribs, Sink sink) { 1149 verbatim(); 1150 sink.verbatim(attribs); 1151 } 1152 1153 private void handleSectionStart(Sink sink, SinkEventAttributeSet attribs) { 1154 emitHeadingSections(sectionLevel, sink, false); 1155 sink.section(++sectionLevel, attribs); 1156 this.headingLevel = sectionLevel; 1157 } 1158 1159 private void handleHeadingStart(Sink sink, int level, SinkEventAttributeSet attribs) { 1160 emitHeadingSections(level, sink, true); 1161 sink.sectionTitle(level, attribs); 1162 } 1163 1164 private void handleSectionEnd(Sink sink) { 1165 emitHeadingSections(sectionLevel, sink, false); 1166 sink.section_(sectionLevel--); 1167 this.headingLevel = sectionLevel; 1168 } 1169 1170 private void handleTableStart(Sink sink, SinkEventAttributeSet attribs) { 1171 sink.table(attribs); 1172 String givenTableClass = (String) attribs.getAttribute(SinkEventAttributes.CLASS); 1173 boolean grid = false; 1174 if (givenTableClass != null 1175 && BODYTABLEBORDER_CLASS_PATTERN.matcher(givenTableClass).matches()) { 1176 grid = true; 1177 } 1178 1179 sink.tableRows(null, grid); 1180 } 1181}