Skip to main content

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}