#![allow(missing_docs)]
use html_to_markdown_rs::ConversionResult;
use html_to_markdown_rs::convert;
use html_to_markdown_rs::options::{ConversionOptions, OutputFormat};
use std::sync::{Mutex, MutexGuard};
use std::thread;
static TEST_MUTEX: Mutex<()> = Mutex::new(());
fn test_lock() -> MutexGuard<'static, ()> {
TEST_MUTEX.lock().expect("deep nesting test mutex poisoned")
}
fn converts_without_overflow(html: String, options: ConversionOptions) -> bool {
converts_without_overflow_on_stack(html, options, 256 * 1024)
}
fn converts_without_overflow_on_stack(html: String, options: ConversionOptions, stack_size: usize) -> bool {
thread::Builder::new()
.stack_size(stack_size)
.spawn(move || convert(&html, Some(options)).is_ok())
.expect("spawn conversion thread")
.join()
.expect("conversion thread overflowed the stack")
}
fn convert_without_overflow_on_stack(html: String, options: ConversionOptions, stack_size: usize) -> ConversionResult {
thread::Builder::new()
.stack_size(stack_size)
.spawn(move || convert(&html, Some(options)))
.expect("spawn conversion thread")
.join()
.expect("conversion thread overflowed the stack")
.expect("conversion should not fail")
}
#[test]
fn deep_unclosed_table_cells_do_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><head><title>t</title></head><body><table><tr>");
for _ in 0..20_000 {
html.push_str("<td>x");
}
html.push_str("</tr></table></body></html>");
let options = ConversionOptions::builder().max_depth(Some(200)).build();
assert!(converts_without_overflow(html, options));
}
#[test]
fn deep_markup_without_head_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><body><table><tr>");
for _ in 0..20_000 {
html.push_str("<td>x");
}
html.push_str("</tr></table></body></html>");
let options = ConversionOptions::builder().max_depth(Some(200)).build();
assert!(converts_without_overflow(html, options));
}
#[test]
fn deep_link_descendant_text_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><head><title>t</title></head><body><a href=\"/deep\">");
for _ in 0..1_000 {
html.push_str("<span>");
}
html.push_str("deep");
html.push_str("</a></body></html>");
let options = ConversionOptions::builder().max_depth(Some(200)).build();
assert!(converts_without_overflow_on_stack(html, options, 8 * 1024 * 1024));
}
#[test]
fn default_depth_uses_stack_safe_limit() {
let _guard = test_lock();
let mut html = String::from("<html><body>");
for _ in 0..1_000 {
html.push_str("<div>");
}
html.push_str("deep");
for _ in 0..1_000 {
html.push_str("</div>");
}
html.push_str("</body></html>");
assert!(converts_without_overflow(html, ConversionOptions::default()));
}
#[test]
fn plain_text_output_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><body>");
for _ in 0..1_000 {
html.push_str("<div>");
}
html.push_str("deep");
for _ in 0..1_000 {
html.push_str("</div>");
}
html.push_str("</body></html>");
let options = ConversionOptions {
output_format: OutputFormat::Plain,
max_depth: Some(200),
..Default::default()
};
assert!(converts_without_overflow_on_stack(html, options, 8 * 1024 * 1024));
}
#[test]
fn deep_svg_group_nesting_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><body><svg>");
for _ in 0..50_000 {
html.push_str("<g>");
}
for _ in 0..50_000 {
html.push_str("</g>");
}
html.push_str("</svg></body></html>");
let result = convert_without_overflow_on_stack(html, ConversionOptions::default(), 256 * 1024);
let content = result.content.as_deref().unwrap_or_default();
assert!(
content.contains("data:image/svg+xml;base64,"),
"expected a (truncated) base64 SVG data URI in the output. Got:\n{content}"
);
}
#[test]
fn deep_mathml_nesting_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><body><math>");
for _ in 0..50_000 {
html.push_str("<mrow>");
}
html.push_str("<mi>x</mi>");
for _ in 0..50_000 {
html.push_str("</mrow>");
}
html.push_str("</math></body></html>");
let result = convert_without_overflow_on_stack(html, ConversionOptions::default(), 256 * 1024);
let content = result.content.as_deref().unwrap_or_default();
assert!(
content.contains("<!-- MathML: "),
"expected a (truncated) MathML comment in the output. Got:\n{content}"
);
}
#[test]
fn deeply_nested_tables_do_not_overflow_stack() {
const NESTING_DEPTH: usize = 2_000;
let _guard = test_lock();
let mut html = String::from("<html><body>");
for _ in 0..NESTING_DEPTH {
html.push_str("<table><tr><td>");
}
html.push_str("leaf");
for _ in 0..NESTING_DEPTH {
html.push_str("</td></tr></table>");
}
html.push_str("</body></html>");
let result = convert_without_overflow_on_stack(html, ConversionOptions::default(), 256 * 1024);
let content = result.content.as_deref().unwrap_or_default();
assert!(
!content.trim().is_empty(),
"deeply nested table conversion must not silently return empty output"
);
assert!(
content.contains('|'),
"expected at least the outer table rows to render as pipe-delimited content. Got:\n{content}"
);
}
#[test]
fn document_structure_builder_does_not_overflow_stack() {
let _guard = test_lock();
let mut html = String::from("<html><body>");
for _ in 0..1_000 {
html.push_str("<section>");
}
html.push_str("<p>deep</p>");
for _ in 0..1_000 {
html.push_str("</section>");
}
html.push_str("</body></html>");
assert!(
thread::Builder::new()
.stack_size(8 * 1024 * 1024)
.spawn(move || {
let dom = tl::parse(&html, tl::ParserOptions::default()).expect("parse deep html");
let document = html_to_markdown_rs::types::build_document_structure(&dom);
!document.nodes.is_empty()
})
.expect("spawn structure thread")
.join()
.expect("structure thread overflowed the stack")
);
}