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}