001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.parser.filter;
018
019import java.text.ParseException;
020import java.util.Arrays;
021import java.util.List;
022import java.util.Locale;
023
024import org.apache.wicket.markup.ComponentTag;
025import org.apache.wicket.markup.MarkupElement;
026import org.apache.wicket.markup.WicketTag;
027import org.apache.wicket.markup.parser.AbstractMarkupFilter;
028import org.apache.wicket.markup.parser.XmlTag.TagType;
029import org.apache.wicket.markup.resolver.HtmlHeaderResolver;
030
031/**
032 * MarkupFilter that expands certain open-close tag as separate open and close tags. Firefox, unless
033 * it gets text/xml mime type, treats these open-close tags as open tags which results in corrupted
034 * DOM. This happens even with xhtml doctype.
035 * 
036 * In addition, some tags are required open-body-close for Wicket to work properly.
037 * 
038 * @author Juergen Donnerstag
039 * @author Matej Knopp
040 */
041public class OpenCloseTagExpander extends AbstractMarkupFilter
042{
043        // A list of elements which should not be expanded from TagType.OPEN_CLOSE to TagType.OPEN + TagType.CLOSE
044        // http://www.w3.org/TR/html-markup/syntax.html#void-element
045        // area, base, br, col, command, embed, hr, img, input, keygen, link, meta, param, source, track, wbr
046
047        static final List<String> REPLACE_FOR_TAGS = Arrays.asList("a", "q", "sub", "sup",
048                "abbr", "acronym", "cite", "code", "del", "dfn", "em", "ins", "kbd", "samp", "var",
049                "label", "textarea", "tr", "td", "th", "caption", "thead", "tbody", "tfoot", "dl", "dt",
050                "dd", "li", "ol", "ul", "h1", "h2", "h3", "h4", "h5", "h6", "i",
051                "pre",
052                "title",
053                "div",
054
055                // tags from pre 1.5 days, shouldn't really be here but make this release more backwards
056                // compatible
057                "span", "p",
058                "strong",
059                "b",
060                "e",
061                "select",
062
063                // @TODO by now an exclude list is probably shorter
064                "article", "aside", "details", "summary", "figure", "figcaption", "footer",
065                "header", "hgroup", "mark", "meter", "nav", "progress", "ruby", "rt", "rp", "section",
066                "audio", "video", "canvas", "datalist", "output", HtmlHeaderResolver.HEADER_ITEMS);
067
068        // temporary storage. Introduce into flow on next request
069        private ComponentTag next = null;
070
071        @Override
072        public MarkupElement nextElement() throws ParseException
073        {
074                // Did we hold back an elem? Than return that first
075                if (next != null)
076                {
077                        MarkupElement rtn = next;
078                        next = null;
079                        return rtn;
080                }
081
082                return super.nextElement();
083        }
084
085        @Override
086        protected MarkupElement onComponentTag(final ComponentTag tag) throws ParseException
087        {
088                if (tag.isOpenClose())
089                {
090                        String name = tag.getName();
091
092                        if (contains(name))
093                        {
094                                if (onFound(tag))
095                                {
096                                        next = new ComponentTag(tag.getName(), TagType.CLOSE);
097                                        if (getWicketNamespace().equals(tag.getNamespace()))
098                                        {
099                                                next = new WicketTag(next);
100                                        }
101                                        next.setNamespace(tag.getNamespace());
102                                        next.setOpenTag(tag);
103                                        next.setModified(true);
104                                }
105                        }
106                }
107
108                return tag;
109        }
110
111        /**
112         * Can be subclassed to do other things. E.g. instead of changing it you may simply want to log
113         * a warning.
114         * 
115         * @param tag
116         * @return Must be true to automatically create and add a close tag.
117         */
118        protected boolean onFound(final ComponentTag tag)
119        {
120                tag.setType(TagType.OPEN);
121                tag.setModified(true);
122
123                return true;
124        }
125
126        /**
127         * Allows subclasses to easily expand the list of tag which needs to be expanded.
128         * 
129         * @param name
130         * @return true, if needs expansion
131         */
132        protected boolean contains(final String name)
133        {
134                return REPLACE_FOR_TAGS.contains(name.toLowerCase(Locale.ROOT));
135        }
136}