001package org.jsoup.parser; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.nodes.Attributes; 005import org.jsoup.nodes.Document; 006import org.jsoup.nodes.Element; 007import org.jsoup.nodes.Node; 008import org.jspecify.annotations.Nullable; 009 010import java.io.Reader; 011import java.io.StringReader; 012import java.util.List; 013import java.util.concurrent.locks.ReentrantLock; 014 015/** 016 Parses HTML or XML into a {@link org.jsoup.nodes.Document}. Generally, it is simpler to use one of the parse methods in 017 {@link org.jsoup.Jsoup}. 018 <p>Note that a given Parser instance object is threadsafe, but not concurrent. (Concurrent parse calls will 019 synchronize.) To reuse a Parser configuration in a multithreaded environment, use {@link #newInstance()} to make 020 copies.</p> 021 */ 022public class Parser implements Cloneable { 023 public static final String NamespaceHtml = "http://www.w3.org/1999/xhtml"; 024 public static final String NamespaceXml = "http://www.w3.org/XML/1998/namespace"; 025 public static final String NamespaceMathml = "http://www.w3.org/1998/Math/MathML"; 026 public static final String NamespaceSvg = "http://www.w3.org/2000/svg"; 027 028 private final TreeBuilder treeBuilder; 029 private ParseErrorList errors; 030 private ParseSettings settings; 031 private boolean trackPosition = false; 032 private @Nullable TagSet tagSet; 033 private final ReentrantLock lock = new ReentrantLock(); 034 private int maxDepth; 035 036 /** 037 * Create a new Parser, using the specified TreeBuilder 038 * @param treeBuilder TreeBuilder to use to parse input into Documents. 039 */ 040 public Parser(TreeBuilder treeBuilder) { 041 this.treeBuilder = treeBuilder; 042 settings = treeBuilder.defaultSettings(); 043 errors = ParseErrorList.noTracking(); 044 maxDepth = treeBuilder.defaultMaxDepth(); 045 } 046 047 /** 048 Creates a new Parser as a deep copy of this; including initializing a new TreeBuilder. Allows independent (multi-threaded) use. 049 @return a copied parser 050 */ 051 public Parser newInstance() { 052 return new Parser(this); 053 } 054 055 @SuppressWarnings("MethodDoesntCallSuperMethod") // because we use the copy constructor instead 056 @Override 057 public Parser clone() { 058 return new Parser(this); 059 } 060 061 private Parser(Parser copy) { 062 treeBuilder = copy.treeBuilder.newInstance(); // because extended 063 errors = new ParseErrorList(copy.errors); // only copies size, not contents 064 settings = new ParseSettings(copy.settings); 065 trackPosition = copy.trackPosition; 066 maxDepth = copy.maxDepth; 067 tagSet = new TagSet(copy.tagSet()); 068 } 069 070 /** 071 Parse the contents of a String. 072 073 @param html HTML to parse 074 @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 075 @return parsed Document 076 */ 077 public Document parseInput(String html, String baseUri) { 078 return parseInput(new StringReader(html), baseUri); 079 } 080 081 /** 082 Parse the contents of Reader. 083 084 @param inputHtml HTML to parse 085 @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 086 @return parsed Document 087 @throws java.io.UncheckedIOException if an I/O error occurs in the Reader 088 */ 089 public Document parseInput(Reader inputHtml, String baseUri) { 090 try { 091 lock.lock(); // using a lock vs synchronized to support loom threads 092 return treeBuilder.parse(inputHtml, baseUri, this); 093 } finally { 094 lock.unlock(); 095 } 096 } 097 098 /** 099 Parses attribute names and values without a surrounding tag, using this parser's settings. 100 Parsing stops at a tag closer, which is reported if error tracking is enabled. 101 102 @param input the attribute string to parse 103 @return the parsed attributes 104 @see #setTrackErrors(int) 105 @since 1.24.1 106 */ 107 public Attributes parseAttributes(String input) { 108 try { 109 lock.lock(); 110 return treeBuilder.parseAttributes(new StringReader(input), this); 111 } finally { 112 lock.unlock(); 113 } 114 } 115 116 /** 117 Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context. 118 119 @param fragment the fragment of HTML to parse 120 @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). 121 @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 122 @return list of nodes parsed from the input HTML. 123 */ 124 public List<Node> parseFragmentInput(String fragment, @Nullable Element context, String baseUri) { 125 return parseFragmentInput(new StringReader(fragment), context, baseUri); 126 } 127 128 /** 129 Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context. 130 131 @param fragment the fragment of HTML to parse 132 @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). 133 @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 134 @return list of nodes parsed from the input HTML. 135 @throws java.io.UncheckedIOException if an I/O error occurs in the Reader 136 */ 137 public List<Node> parseFragmentInput(Reader fragment, @Nullable Element context, String baseUri) { 138 try { 139 lock.lock(); 140 return treeBuilder.parseFragment(fragment, context, baseUri, this); 141 } finally { 142 lock.unlock(); 143 } 144 } 145 146 // gets & sets 147 /** 148 * Get the TreeBuilder currently in use. 149 * @return current TreeBuilder. 150 */ 151 public TreeBuilder getTreeBuilder() { 152 return treeBuilder; 153 } 154 155 /** 156 * Check if parse error tracking is enabled. 157 * @return current track error state. 158 */ 159 public boolean isTrackErrors() { 160 return errors.getMaxSize() > 0; 161 } 162 163 /** 164 * Enable or disable parse error tracking for the next parse. 165 * @param maxErrors the maximum number of errors to track. Set to 0 to disable. 166 * @return this, for chaining 167 */ 168 public Parser setTrackErrors(int maxErrors) { 169 errors = maxErrors > 0 ? ParseErrorList.tracking(maxErrors) : ParseErrorList.noTracking(); 170 return this; 171 } 172 173 /** 174 * Retrieve the parse errors, if any, from the last parse. 175 * @return list of parse errors, up to the size of the maximum errors tracked. 176 * @see #setTrackErrors(int) 177 */ 178 public ParseErrorList getErrors() { 179 return errors; 180 } 181 182 /** 183 Test if position tracking is enabled. If it is, Nodes will have a Position to track where in the original input 184 source they were created from. By default, tracking is not enabled. 185 * @return current track position setting 186 */ 187 public boolean isTrackPosition() { 188 return trackPosition; 189 } 190 191 /** 192 Enable or disable source position tracking. If enabled, Nodes will have a Position to track where in the original 193 input source they were created from. 194 @param trackPosition position tracking setting; {@code true} to enable 195 @return this Parser, for chaining 196 */ 197 public Parser setTrackPosition(boolean trackPosition) { 198 this.trackPosition = trackPosition; 199 return this; 200 } 201 202 /** 203 Update the ParseSettings of this Parser, to control the case sensitivity of tags and attributes. 204 * @param settings the new settings 205 * @return this Parser 206 */ 207 public Parser settings(ParseSettings settings) { 208 this.settings = settings; 209 return this; 210 } 211 212 /** 213 Gets the current ParseSettings for this Parser 214 * @return current ParseSettings 215 */ 216 public ParseSettings settings() { 217 return settings; 218 } 219 220 /** 221 Set the parser's maximum stack depth (maximum number of open elements). When reached, new open elements will be 222 removed to prevent excessive nesting. Defaults to 512. 223 224 @param maxDepth maximum parser depth; must be >= 1 225 @return this Parser, for chaining 226 */ 227 public Parser setMaxDepth(int maxDepth) { 228 Validate.isTrue(maxDepth >= 1, "maxDepth must be >= 1"); 229 this.maxDepth = maxDepth; 230 return this; 231 } 232 233 /** 234 * Get the maximum parser depth (maximum number of open elements). 235 * @return the current max parser depth 236 */ 237 public int getMaxDepth() { 238 return maxDepth; 239 } 240 241 /** 242 Set a custom TagSet to use for this Parser. This allows you to define your own tags, and control how they are 243 parsed. For example, you can set a tag to preserve whitespace, or to be treated as a block tag. 244 <p>You can start with the {@link TagSet#Html()} defaults and customize, or a new empty TagSet.</p> 245 246 @param tagSet the TagSet to use. This gets copied, so that changes that the parse makes (tags found in the document will be added) do not clobber the original TagSet. 247 @return this Parser 248 @since 1.20.1 249 */ 250 public Parser tagSet(TagSet tagSet) { 251 Validate.notNull(tagSet); 252 this.tagSet = new TagSet(tagSet); // copy it as we are going to mutate it 253 return this; 254 } 255 256 /** 257 Get the current TagSet for this Parser, which will be either this parser's default, or one that you have set. 258 @return the current TagSet. After the parse, this will contain any new tags that were found in the document. 259 @since 1.20.1 260 */ 261 public TagSet tagSet() { 262 if (tagSet == null) 263 tagSet = treeBuilder.defaultTagSet(); 264 return tagSet; 265 } 266 267 public String defaultNamespace() { 268 return getTreeBuilder().defaultNamespace(); 269 } 270 271 // static parse functions below 272 /** 273 * Parse HTML into a Document. 274 * 275 * @param html HTML to parse 276 * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 277 * 278 * @return parsed Document 279 */ 280 public static Document parse(String html, String baseUri) { 281 TreeBuilder treeBuilder = new HtmlTreeBuilder(); 282 return treeBuilder.parse(new StringReader(html), baseUri, new Parser(treeBuilder)); 283 } 284 285 /** 286 * Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context. 287 * 288 * @param fragmentHtml the fragment of HTML to parse 289 * @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). This 290 * provides stack context (for implicit element creation). 291 * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 292 * 293 * @return list of nodes parsed from the input HTML. Note that the context element, if supplied, is not modified. 294 */ 295 public static List<Node> parseFragment(String fragmentHtml, Element context, String baseUri) { 296 HtmlTreeBuilder treeBuilder = new HtmlTreeBuilder(); 297 return treeBuilder.parseFragment(new StringReader(fragmentHtml), context, baseUri, new Parser(treeBuilder)); 298 } 299 300 /** 301 * Parse a fragment of HTML into a list of nodes. The context element, if supplied, supplies parsing context. 302 * 303 * @param fragmentHtml the fragment of HTML to parse 304 * @param context (optional) the element that this HTML fragment is being parsed for (i.e. for inner HTML). This 305 * provides stack context (for implicit element creation). 306 * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 307 * @param errorList list to add errors to 308 * 309 * @return list of nodes parsed from the input HTML. Note that the context element, if supplied, is not modified. 310 */ 311 public static List<Node> parseFragment(String fragmentHtml, Element context, String baseUri, ParseErrorList errorList) { 312 HtmlTreeBuilder treeBuilder = new HtmlTreeBuilder(); 313 Parser parser = new Parser(treeBuilder); 314 parser.errors = errorList; 315 return treeBuilder.parseFragment(new StringReader(fragmentHtml), context, baseUri, parser); 316 } 317 318 /** 319 * Parse a fragment of XML into a list of nodes. 320 * 321 * @param fragmentXml the fragment of XML to parse 322 * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 323 * @return list of nodes parsed from the input XML. 324 */ 325 public static List<Node> parseXmlFragment(String fragmentXml, String baseUri) { 326 XmlTreeBuilder treeBuilder = new XmlTreeBuilder(); 327 return treeBuilder.parseFragment(new StringReader(fragmentXml), null, baseUri, new Parser(treeBuilder)); 328 } 329 330 /** 331 * Parse a fragment of HTML into the {@code body} of a Document. 332 * 333 * @param bodyHtml fragment of HTML 334 * @param baseUri base URI of document (i.e. original fetch location), for resolving relative URLs. 335 * 336 * @return Document, with empty head, and HTML parsed into body 337 */ 338 public static Document parseBodyFragment(String bodyHtml, String baseUri) { 339 Document doc = Document.createShell(baseUri); 340 Element body = doc.body(); 341 List<Node> nodeList = parseFragment(bodyHtml, body, baseUri); 342 body.appendChildren(nodeList); 343 return doc; 344 } 345 346 /** 347 Utility method to unescape HTML entities from a string. 348 <p>To track errors while unescaping, use 349 {@link #unescape(String, boolean)} with a Parser instance that has error tracking enabled.</p> 350 351 @param string HTML escaped string 352 @param inAttribute if the string is to be escaped in strict mode (as attributes are) 353 @return an unescaped string 354 @see #unescape(String, boolean) 355 */ 356 public static String unescapeEntities(String string, boolean inAttribute) { 357 Validate.notNull(string); 358 if (string.indexOf('&') < 0) return string; // nothing to unescape 359 return Parser.htmlParser().unescape(string, inAttribute); 360 } 361 362 /** 363 Utility method to unescape HTML entities from a string, using this {@code Parser}'s configuration (for example, to 364 collect errors while unescaping). 365 366 @param string HTML escaped string 367 @param inAttribute if the string is to be escaped in strict mode (as attributes are) 368 @return an unescaped string 369 @see #setTrackErrors(int) 370 @see #unescapeEntities(String, boolean) 371 */ 372 public String unescape(String string, boolean inAttribute) { 373 Validate.notNull(string); 374 if (string.indexOf('&') < 0) return string; // nothing to unescape 375 this.treeBuilder.initialiseParse(new StringReader(string), "", this); 376 Tokeniser tokeniser = new Tokeniser(this.treeBuilder); 377 return tokeniser.unescapeEntities(inAttribute); 378 } 379 380 // builders 381 382 /** 383 * Create a new HTML parser. This parser treats input as HTML5, and enforces the creation of a normalised document, 384 * based on a knowledge of the semantics of the incoming tags. 385 * @return a new HTML parser. 386 */ 387 public static Parser htmlParser() { 388 return new Parser(new HtmlTreeBuilder()); 389 } 390 391 /** 392 * Create a new XML parser. This parser assumes no knowledge of the incoming tags and does not treat it as HTML, 393 * rather creates a simple tree directly from the input. 394 * @return a new simple XML parser. 395 */ 396 public static Parser xmlParser() { 397 return new Parser(new XmlTreeBuilder()); 398 } 399}