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 &lt;script&gt;&lt;/script&gt; or &lt;style&gt;&lt;/style&gt; block is read. CDATA sections within are
107     * handled as rawText.
108     */
109    private boolean scriptBlock;
110
111    /** Used to distinguish &lt;a href=""&gt; from &lt;a name=""&gt;. */
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 &lt;a href=""&gt; from &lt;a name=""&gt;. */
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 &lt;pre&gt; 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     *      &lt;article&gt;, &lt;nav&gt;, &lt;aside&gt;, &lt;section&gt;, &lt;h1&gt;, &lt;h2&gt;, &lt;h3&gt;,
178     *      &lt;h4&gt;, &lt;h5&gt;, &lt;header&gt;, &lt;main&gt;, &lt;footer&gt;, &lt;em&gt;, &lt;strong&gt;,
179     *      &lt;small&gt;, &lt;s&gt;, &lt;cite&gt;, &lt;q&gt;, &lt;dfn&gt;, &lt;abbr&gt;, &lt;i&gt;,
180     *      &lt;b&gt;, &lt;code&gt;, &lt;samp&gt;, &lt;kbd&gt;, &lt;sub&gt;, &lt;sup&gt;, &lt;u&gt;,
181     *      &lt;mark&gt;, &lt;ruby&gt;, &lt;rb&gt;, &lt;rt&gt;, &lt;rtc&gt;, &lt;rp&gt;, &lt;bdi&gt;,
182     *      &lt;bdo&gt;, &lt;span&gt;, &lt;ins&gt;, &lt;del&gt;, &lt;p&gt;, &lt;pre&gt;, &lt;ul&gt;,
183     *      &lt;ol&gt;, &lt;li&gt;, &lt;dl&gt;, &lt;dt&gt;, &lt;dd&gt;, &lt;a&gt;, &lt;table&gt;,
184     *      &lt;tr&gt;, &lt;th&gt;, &lt;td&gt;, &lt;caption&gt;, &lt;br/&gt;, &lt;wbr/&gt;, &lt;hr/&gt;,
185     *      &lt;img/&gt;.
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     * &lt;h2&gt;&lt;/h2&gt;
843     * &lt;h5&gt;&lt;/h5&gt;
844     * </pre>
845     * <p>
846     * we have to insert two section starts before we open the <code>&lt;h5&gt;</code>.
847     * In the following sequence
848     * </p>
849     * <pre>
850     * &lt;h5&gt;&lt;/h5&gt;
851     * &lt;h2&gt;&lt;/h2&gt;
852     * </pre>
853     * <p>
854     * we have to close two sections before we open the <code>&lt;h2&gt;</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 &lt;pre&gt; 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 &lt;script&gt; tag.
964     *
965     * @return true if we are currently inside <code>&lt;script&gt;</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}