fast_html_parser/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! # fast-html-parser — SIMD-Optimized HTML Parser
3//!
4//! A high-performance HTML parser designed for web scraping workloads.
5//! Uses SIMD instructions (SSE4.2, AVX2, NEON) for tokenization and builds
6//! a cache-line aligned arena-based DOM tree for fast traversal.
7//!
8//! ## Quick Start
9//!
10//! ```
11//! use fast_html_parser::HtmlParser;
12//!
13//! let doc = HtmlParser::parse("<div><p>Hello</p></div>").unwrap();
14//! assert_eq!(doc.root().text_content(), "Hello");
15//! ```
16//!
17//! ## Builder Pattern
18//!
19//! ```
20//! use fast_html_parser::HtmlParser;
21//!
22//! let doc = HtmlParser::builder()
23//! .max_input_size(64 * 1024 * 1024) // 64 MiB
24//! .build()
25//! .parse_str("<div>Hello</div>")
26//! .unwrap();
27//! ```
28//!
29//! ## CSS Selectors
30//!
31//! ```
32//! # #[cfg(feature = "css-selector")]
33//! # {
34//! use fast_html_parser::prelude::*;
35//!
36//! let doc = HtmlParser::parse("<ul><li>one</li><li>two</li></ul>").unwrap();
37//! let items = doc.select("li").unwrap();
38//! assert_eq!(items.len(), 2);
39//! # }
40//! ```
41//!
42//! ## Streaming
43//!
44//! ```
45//! use fast_html_parser::streaming::parse_stream;
46//!
47//! let html = b"<div><p>Hello</p></div>";
48//! let doc = parse_stream(html.chunks(8)).unwrap();
49//! assert_eq!(doc.root().text_content(), "Hello");
50//! ```
51//!
52//! ## Feature Flags
53//!
54//! | Feature | Default | Description |
55//! |---|---|---|
56//! | `css-selector` | Yes | CSS selector engine |
57//! | `entity-decode` | Yes | HTML entity decoding |
58//! | `xpath` | No | XPath expression support |
59//! | `encoding` | No | Auto-detect encoding from raw bytes |
60//! | `async-tokio` | No | Async parsing via Tokio |
61
62// ---------------------------------------------------------------------------
63// Re-exports: core types
64// ---------------------------------------------------------------------------
65
66/// Core types: interned tags, entity table, error definitions.
67pub use fhp_core as core_types;
68
69/// Interned HTML tag enum.
70pub use fhp_core::tag::Tag;
71
72/// Tokenizer (low-level).
73pub use fhp_tokenizer as tokenizer;
74
75/// DOM tree types.
76pub use fhp_tree as tree;
77
78/// Parsed document and node reference.
79pub use fhp_tree::{Document, HtmlError, NodeRef};
80
81/// Node identity type.
82pub use fhp_tree::node::NodeId;
83
84/// Streaming and incremental parsing.
85pub mod streaming {
86 pub use fhp_tree::streaming::{EarlyStopParser, ParseStatus, StreamParser, parse_stream};
87}
88
89// ---------------------------------------------------------------------------
90// Conditional re-exports
91// ---------------------------------------------------------------------------
92
93/// CSS selector and XPath engine.
94#[cfg(any(feature = "css-selector", feature = "xpath"))]
95#[cfg_attr(docsrs, doc(cfg(any(feature = "css-selector", feature = "xpath"))))]
96pub use fhp_selector::{CompiledSelector, DocumentIndex, Selectable, Selection};
97
98/// XPath types (re-exported from selector crate).
99#[cfg(feature = "xpath")]
100#[cfg_attr(docsrs, doc(cfg(feature = "xpath")))]
101pub mod xpath {
102 pub use fhp_selector::xpath::ast::XPathResult;
103}
104
105/// Encoding detection and conversion.
106#[cfg(feature = "encoding")]
107#[cfg_attr(docsrs, doc(cfg(feature = "encoding")))]
108pub mod encoding {
109 pub use fhp_encoding::{Encoding, decode, decode_or_detect, detect};
110}
111
112/// Async parser (requires `async-tokio` feature).
113#[cfg(feature = "async-tokio")]
114#[cfg_attr(docsrs, doc(cfg(feature = "async-tokio")))]
115pub mod async_parser {
116 pub use fhp_tree::async_parser::{AsyncParser, parse_async};
117}
118
119// ---------------------------------------------------------------------------
120// Prelude
121// ---------------------------------------------------------------------------
122
123/// Convenience prelude that imports the most commonly used types.
124///
125/// ```
126/// use fast_html_parser::prelude::*;
127/// ```
128pub mod prelude {
129 pub use fhp_tree::node::NodeId;
130 pub use fhp_tree::{Document, HtmlError, NodeRef};
131
132 #[cfg(any(feature = "css-selector", feature = "xpath"))]
133 #[cfg_attr(docsrs, doc(cfg(any(feature = "css-selector", feature = "xpath"))))]
134 pub use fhp_selector::{CompiledSelector, Selectable, Selection};
135
136 pub use crate::HtmlParser;
137}
138
139// ---------------------------------------------------------------------------
140// Builder + HtmlParser
141// ---------------------------------------------------------------------------
142
143/// Default maximum input size (256 MiB).
144const DEFAULT_MAX_INPUT_SIZE: usize = 256 * 1024 * 1024;
145
146/// Configuration builder for the HTML parser.
147///
148/// # Example
149///
150/// ```
151/// use fast_html_parser::HtmlParser;
152///
153/// let parser = HtmlParser::builder()
154/// .max_input_size(128 * 1024 * 1024)
155/// .fragment_mode(true)
156/// .build();
157///
158/// let doc = parser.parse_str("<p>fragment</p>").unwrap();
159/// assert_eq!(doc.root().text_content(), "fragment");
160/// ```
161pub struct ParserBuilder {
162 max_input_size: usize,
163 fragment_mode: bool,
164}
165
166impl Default for ParserBuilder {
167 fn default() -> Self {
168 Self {
169 max_input_size: DEFAULT_MAX_INPUT_SIZE,
170 fragment_mode: false,
171 }
172 }
173}
174
175impl ParserBuilder {
176 /// Set the maximum input size in bytes.
177 ///
178 /// Inputs exceeding this limit will return [`HtmlError::InputTooLarge`].
179 /// Default: 256 MiB.
180 pub fn max_input_size(mut self, size: usize) -> Self {
181 self.max_input_size = size;
182 self
183 }
184
185 /// Enable fragment mode.
186 ///
187 /// In fragment mode the parser treats input as an HTML fragment rather
188 /// than a full document. Currently this behaves identically to normal
189 /// mode (the parser already handles fragments gracefully).
190 pub fn fragment_mode(mut self, enabled: bool) -> Self {
191 self.fragment_mode = enabled;
192 self
193 }
194
195 /// Consume the builder and create a configured [`HtmlParser`].
196 pub fn build(self) -> HtmlParser {
197 HtmlParser {
198 max_input_size: self.max_input_size,
199 _fragment_mode: self.fragment_mode,
200 }
201 }
202}
203
204/// A configured HTML parser instance.
205///
206/// Create via [`HtmlParser::builder()`] for custom configuration, or use the
207/// convenience methods [`HtmlParser::parse()`] and [`HtmlParser::parse_bytes()`]
208/// for defaults.
209///
210/// # Example
211///
212/// ```
213/// use fast_html_parser::HtmlParser;
214///
215/// // One-shot convenience
216/// let doc = HtmlParser::parse("<p>Hello</p>").unwrap();
217///
218/// // Builder pattern
219/// let parser = HtmlParser::builder()
220/// .max_input_size(1024 * 1024)
221/// .build();
222/// let doc = parser.parse_str("<p>World</p>").unwrap();
223/// ```
224pub struct HtmlParser {
225 max_input_size: usize,
226 _fragment_mode: bool,
227}
228
229impl HtmlParser {
230 /// Create a new [`ParserBuilder`].
231 pub fn builder() -> ParserBuilder {
232 ParserBuilder::default()
233 }
234
235 /// Parse an HTML string with default settings.
236 ///
237 /// This is a convenience wrapper around `fhp_tree::parse()`.
238 ///
239 /// # Errors
240 ///
241 /// Returns [`HtmlError::InputTooLarge`] if the input exceeds 256 MiB.
242 ///
243 /// # Example
244 ///
245 /// ```
246 /// use fast_html_parser::HtmlParser;
247 ///
248 /// let doc = HtmlParser::parse("<div><p>Hello</p></div>").unwrap();
249 /// assert_eq!(doc.root().text_content(), "Hello");
250 /// ```
251 pub fn parse(input: &str) -> Result<Document, HtmlError> {
252 fhp_tree::parse(input)
253 }
254
255 /// Parse an owned `String` with default settings, transferring the allocation.
256 ///
257 /// Avoids a memcpy of the source bytes when the caller already owns the
258 /// input (e.g., from an HTTP response body).
259 ///
260 /// # Errors
261 ///
262 /// Returns [`HtmlError::InputTooLarge`] if the input exceeds 256 MiB.
263 ///
264 /// # Example
265 ///
266 /// ```
267 /// use fast_html_parser::HtmlParser;
268 ///
269 /// let html = String::from("<div><p>Hello</p></div>");
270 /// let doc = HtmlParser::parse_owned(html).unwrap();
271 /// assert_eq!(doc.root().text_content(), "Hello");
272 /// ```
273 pub fn parse_owned(input: String) -> Result<Document, HtmlError> {
274 fhp_tree::parse_owned(input)
275 }
276
277 /// Parse raw bytes with default settings, auto-detecting encoding.
278 ///
279 /// # Errors
280 ///
281 /// Returns [`HtmlError::InputTooLarge`] or [`HtmlError::Encoding`] on
282 /// failure.
283 ///
284 /// # Example
285 ///
286 /// ```
287 /// use fast_html_parser::HtmlParser;
288 ///
289 /// let doc = HtmlParser::parse_bytes(b"<p>Hello</p>").unwrap();
290 /// assert_eq!(doc.root().text_content(), "Hello");
291 /// ```
292 pub fn parse_bytes(input: &[u8]) -> Result<Document, HtmlError> {
293 fhp_tree::parse_bytes(input)
294 }
295
296 /// Parse an HTML string with the current configuration.
297 ///
298 /// # Errors
299 ///
300 /// Returns [`HtmlError::InputTooLarge`] if the input exceeds the
301 /// configured limit.
302 pub fn parse_str(&self, input: &str) -> Result<Document, HtmlError> {
303 if input.len() > self.max_input_size {
304 return Err(HtmlError::InputTooLarge {
305 size: input.len(),
306 max: self.max_input_size,
307 });
308 }
309 fhp_tree::parse(input)
310 }
311
312 /// Parse an owned `String` with the current configuration.
313 ///
314 /// Avoids a memcpy of the source bytes when the caller already owns the
315 /// input (e.g., from an HTTP response body).
316 ///
317 /// # Errors
318 ///
319 /// Returns [`HtmlError::InputTooLarge`] if the input exceeds the
320 /// configured limit.
321 pub fn parse_str_owned(&self, input: String) -> Result<Document, HtmlError> {
322 if input.len() > self.max_input_size {
323 return Err(HtmlError::InputTooLarge {
324 size: input.len(),
325 max: self.max_input_size,
326 });
327 }
328 fhp_tree::parse_owned(input)
329 }
330
331 /// Parse raw bytes with the current configuration, auto-detecting encoding.
332 ///
333 /// # Errors
334 ///
335 /// Returns [`HtmlError::InputTooLarge`] or [`HtmlError::Encoding`] on
336 /// failure.
337 pub fn parse_raw(&self, input: &[u8]) -> Result<Document, HtmlError> {
338 if input.len() > self.max_input_size {
339 return Err(HtmlError::InputTooLarge {
340 size: input.len(),
341 max: self.max_input_size,
342 });
343 }
344 fhp_tree::parse_bytes(input)
345 }
346}
347
348/// Parse an HTML string with default settings (convenience alias).
349///
350/// # Example
351///
352/// ```
353/// let doc = fast_html_parser::parse("<p>Quick</p>").unwrap();
354/// assert_eq!(doc.root().text_content(), "Quick");
355/// ```
356pub fn parse(input: &str) -> Result<Document, HtmlError> {
357 HtmlParser::parse(input)
358}
359
360/// Parse an owned `String` with default settings, transferring the allocation.
361///
362/// # Example
363///
364/// ```
365/// let doc = fast_html_parser::parse_owned(String::from("<p>Quick</p>")).unwrap();
366/// assert_eq!(doc.root().text_content(), "Quick");
367/// ```
368pub fn parse_owned(input: String) -> Result<Document, HtmlError> {
369 HtmlParser::parse_owned(input)
370}
371
372/// Parse raw bytes with default settings, auto-detecting encoding.
373///
374/// # Example
375///
376/// ```
377/// let doc = fast_html_parser::parse_bytes(b"<p>Quick</p>").unwrap();
378/// assert_eq!(doc.root().text_content(), "Quick");
379/// ```
380pub fn parse_bytes(input: &[u8]) -> Result<Document, HtmlError> {
381 HtmlParser::parse_bytes(input)
382}
383
384// ---------------------------------------------------------------------------
385// Tests
386// ---------------------------------------------------------------------------
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn parse_convenience() {
394 let doc = parse("<div><p>Hello</p></div>").unwrap();
395 assert_eq!(doc.root().text_content(), "Hello");
396 }
397
398 #[test]
399 fn parse_bytes_convenience() {
400 let doc = parse_bytes(b"<div><p>Hello</p></div>").unwrap();
401 assert_eq!(doc.root().text_content(), "Hello");
402 }
403
404 #[test]
405 fn builder_default() {
406 let parser = HtmlParser::builder().build();
407 let doc = parser.parse_str("<p>ok</p>").unwrap();
408 assert_eq!(doc.root().text_content(), "ok");
409 }
410
411 #[test]
412 fn builder_max_input_size() {
413 let parser = HtmlParser::builder().max_input_size(10).build();
414 let result = parser.parse_str("<p>this is too long</p>");
415 assert!(result.is_err());
416 }
417
418 #[test]
419 fn builder_fragment_mode() {
420 let parser = HtmlParser::builder().fragment_mode(true).build();
421 let doc = parser.parse_str("<li>item</li>").unwrap();
422 assert_eq!(doc.root().text_content(), "item");
423 }
424
425 #[test]
426 fn builder_parse_raw() {
427 let parser = HtmlParser::builder().build();
428 let doc = parser.parse_raw(b"<p>bytes</p>").unwrap();
429 assert_eq!(doc.root().text_content(), "bytes");
430 }
431
432 #[test]
433 fn builder_parse_raw_too_large() {
434 let parser = HtmlParser::builder().max_input_size(5).build();
435 let result = parser.parse_raw(b"<p>too large</p>");
436 assert!(result.is_err());
437 }
438
439 #[test]
440 fn static_parse_method() {
441 let doc = HtmlParser::parse("<b>bold</b>").unwrap();
442 assert_eq!(doc.root().text_content(), "bold");
443 }
444
445 #[test]
446 fn static_parse_bytes_method() {
447 let doc = HtmlParser::parse_bytes(b"<i>italic</i>").unwrap();
448 assert_eq!(doc.root().text_content(), "italic");
449 }
450
451 #[cfg(feature = "css-selector")]
452 #[test]
453 fn selector_reexport() {
454 let doc = HtmlParser::parse("<div><p>Hello</p></div>").unwrap();
455 let sel = doc.select("p").unwrap();
456 assert_eq!(sel.len(), 1);
457 }
458
459 #[test]
460 fn streaming_reexport() {
461 let doc = streaming::parse_stream(b"<p>stream</p>".chunks(4)).unwrap();
462 assert_eq!(doc.root().text_content(), "stream");
463 }
464
465 #[test]
466 fn node_ref_access() {
467 let doc = parse("<a href=\"url\">link</a>").unwrap();
468 let root = doc.root();
469 let a = root.first_child().unwrap();
470 assert_eq!(a.tag(), Tag::A);
471 assert_eq!(a.attr("href"), Some("url"));
472 }
473
474 #[test]
475 fn prelude_works() {
476 use crate::prelude::*;
477 let doc = HtmlParser::parse("<p>prelude</p>").unwrap();
478 let _root: NodeRef<'_> = doc.root();
479 }
480}