001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.util;
029
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Iterator;
033import java.util.List;
034
035import org.htmlparser.Parser;
036import org.htmlparser.PrototypicalNodeFactory;
037import org.htmlparser.Remark;
038import org.htmlparser.Tag;
039import org.htmlparser.Text;
040import org.htmlparser.lexer.Lexer;
041import org.htmlparser.lexer.Page;
042import org.htmlparser.util.ParserException;
043import org.htmlparser.visitors.NodeVisitor;
044
045/**
046 * Base utility class for OpenCms <code>{@link org.htmlparser.visitors.NodeVisitor}</code>
047 * implementations, which provides some often used utility functions.
048 * <p>
049 *
050 * This base implementation is only a "pass through" class, that is the content is parsed, but the
051 * generated result is exactly identical to the input.
052 * <p>
053 *
054 * @since 6.2.0
055 */
056public class CmsHtmlParser extends NodeVisitor implements I_CmsHtmlNodeVisitor {
057
058    /** The array of supported tag names. */
059    // important: don't change the order of these tags in the source, subclasses may expect the tags
060    // at the exact indices give here
061    // if you want to add tags, add them at the end
062    protected static final String[] TAG_ARRAY = new String[] {
063        "H1",
064        "H2",
065        "H3",
066        "H4",
067        "H5",
068        "H6",
069        "P",
070        "DIV",
071        "SPAN",
072        "BR",
073        "OL",
074        "UL",
075        "LI",
076        "TABLE",
077        "TD",
078        "TR",
079        "TH",
080        "THEAD",
081        "TBODY",
082        "TFOOT"};
083
084    /** The list of supported tag names. */
085    protected static final List<String> TAG_LIST = Arrays.asList(TAG_ARRAY);
086
087    /** List of upper case tag name strings of tags that should not be auto-corrected if closing divs are missing. */
088    protected List<String> m_noAutoCloseTags;
089
090    /** Indicates if "echo" mode is on, that is all content is written to the result by default. */
091    protected boolean m_echo;
092
093    /** The buffer to write the out to. */
094    protected StringBuffer m_result;
095
096    /** The providable configuration - never null by contract of interface. */
097    private String m_configuration = "";
098
099    /**
100     * Creates a new instance of the html converter with echo mode set to <code>false</code>.
101     * <p>
102     */
103    public CmsHtmlParser() {
104
105        this(false);
106    }
107
108    /**
109     * Creates a new instance of the html converter.
110     * <p>
111     *
112     * @param echo indicates if "echo" mode is on, that is all content is written to the result
113     */
114    public CmsHtmlParser(boolean echo) {
115
116        m_result = new StringBuffer(1024);
117        m_echo = echo;
118        m_noAutoCloseTags = new ArrayList<String>(32);
119    }
120
121    /**
122     * @see org.opencms.util.I_CmsHtmlNodeVisitor#getConfiguration()
123     */
124    public String getConfiguration() {
125
126        return m_configuration;
127    }
128
129    /**
130     * Returns a list of upper case tag names for which parsing / visiting will not correct missing closing tags.<p>
131     *
132     * @return a List of upper case tag names for which parsing / visiting will not correct missing closing tags
133     */
134    public List<String> getNoAutoCloseTags() {
135
136        return m_noAutoCloseTags;
137    }
138
139    /**
140     * @see org.opencms.util.I_CmsHtmlNodeVisitor#getResult()
141     */
142    public String getResult() {
143
144        return m_result.toString();
145    }
146
147    /**
148     * Returns the HTML for the given tag itself (not the tag content).
149     * <p>
150     *
151     * @param tag the tag to create the HTML for
152     *
153     * @return the HTML for the given tag
154     */
155    public String getTagHtml(Tag tag) {
156
157        StringBuffer result = new StringBuffer(32);
158        result.append('<');
159        result.append(tag.getText());
160        result.append('>');
161        return result.toString();
162    }
163
164    /**
165     * @see org.opencms.util.I_CmsHtmlNodeVisitor#process(java.lang.String, java.lang.String)
166     */
167    public String process(String html, String encoding) throws ParserException {
168
169        m_result = new StringBuffer();
170        Parser parser = new Parser();
171        Lexer lexer = new Lexer();
172
173        // initialize the page with the given char set
174        Page page = new Page(html, encoding);
175        lexer.setPage(page);
176        parser.setLexer(lexer);
177
178        if ((m_noAutoCloseTags != null) && (m_noAutoCloseTags.size() > 0)) {
179            // Degrade Composite tags that do have children in the DOM tree
180            // to simple single tags: This allows to finish this tag with opened HTML tags without the effect
181            // that html parser will generate the closing tags.
182            PrototypicalNodeFactory factory = configureNoAutoCorrectionTags();
183            lexer.setNodeFactory(factory);
184        }
185
186        // process the page using the given visitor
187        parser.visitAllNodesWith(this);
188        // return the result
189        return getResult();
190    }
191
192    /**
193     *
194     * @see org.opencms.util.I_CmsHtmlNodeVisitor#setConfiguration(java.lang.String)
195     */
196    public void setConfiguration(String configuration) {
197
198        if (CmsStringUtil.isNotEmpty(configuration)) {
199            m_configuration = configuration;
200        }
201
202    }
203
204    /**
205     * Sets a list of upper case tag names for which parsing / visiting should not correct missing closing tags.<p>
206     *
207     * @param noAutoCloseTagList a list of upper case tag names for which parsing / visiting
208     *      should not correct missing closing tags to set.
209     */
210    public void setNoAutoCloseTags(List<String> noAutoCloseTagList) {
211
212        // ensuring upper case
213        m_noAutoCloseTags.clear();
214        if (noAutoCloseTagList != null) {
215            Iterator<String> it = noAutoCloseTagList.iterator();
216            while (it.hasNext()) {
217                m_noAutoCloseTags.add((it.next()).toUpperCase());
218            }
219        }
220    }
221
222    /**
223     * @see org.opencms.util.I_CmsHtmlNodeVisitor#visitEndTag(org.htmlparser.Tag)
224     */
225    @Override
226    public void visitEndTag(Tag tag) {
227
228        if (m_echo) {
229            m_result.append(getTagHtml(tag));
230        }
231    }
232
233    /**
234     * @see org.opencms.util.I_CmsHtmlNodeVisitor#visitRemarkNode(org.htmlparser.Remark)
235     */
236    @Override
237    public void visitRemarkNode(Remark remark) {
238
239        if (m_echo) {
240            m_result.append(remark.toHtml(true));
241        }
242    }
243
244    /**
245     * @see org.opencms.util.I_CmsHtmlNodeVisitor#visitStringNode(org.htmlparser.Text)
246     */
247    @Override
248    public void visitStringNode(Text text) {
249
250        if (m_echo) {
251            m_result.append(text.getText());
252        }
253    }
254
255    /**
256     * @see org.opencms.util.I_CmsHtmlNodeVisitor#visitTag(org.htmlparser.Tag)
257     */
258    @Override
259    public void visitTag(Tag tag) {
260
261        if (m_echo) {
262            m_result.append(getTagHtml(tag));
263        }
264    }
265
266    /**
267     * Collapse HTML whitespace in the given String.<p>
268     *
269     * @param string the string to collapse
270     *
271     * @return the input String with all HTML whitespace collapsed
272     */
273    protected String collapse(String string) {
274
275        int len = string.length();
276        StringBuffer result = new StringBuffer(len);
277        int state = 0;
278        for (int i = 0; i < len; i++) {
279            char c = string.charAt(i);
280            switch (c) {
281                // see HTML specification section 9.1 White space
282                // http://www.w3.org/TR/html4/struct/text.html#h-9.1
283                case '\u0020':
284                case '\u0009':
285                case '\u000C':
286                case '\u200B':
287                case '\r':
288                case '\n':
289                    if (0 != state) {
290                        state = 1;
291                    }
292                    break;
293                default:
294                    if (1 == state) {
295                        result.append(' ');
296                    }
297                    state = 2;
298                    result.append(c);
299            }
300        }
301        return result.toString();
302    }
303
304    /**
305     * Internally degrades Composite tags that do have children in the DOM tree
306     * to simple single tags. This allows to avoid auto correction of unclosed HTML tags.<p>
307     *
308     * @return A node factory that will not autocorrect open tags specified via <code>{@link #setNoAutoCloseTags(List)}</code>
309     */
310    protected PrototypicalNodeFactory configureNoAutoCorrectionTags() {
311
312        PrototypicalNodeFactory factory = new PrototypicalNodeFactory();
313
314        String tagName;
315        Iterator<String> it = m_noAutoCloseTags.iterator();
316        CmsNoAutoCloseTag noAutoCloseTag;
317        while (it.hasNext()) {
318            tagName = it.next();
319            noAutoCloseTag = new CmsNoAutoCloseTag(new String[] {tagName});
320            // TODO: This might break in case registering / unregistering  will change from name based to tag-type based approach:
321            factory.unregisterTag(noAutoCloseTag);
322            factory.registerTag(noAutoCloseTag);
323        }
324        return factory;
325    }
326}