html-to-markdown 3.4.0 → 3.6.0.pre.rc.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +347 -0
- data/Steepfile +10 -2
- data/ext/html_to_markdown_rb/Cargo.toml +3 -2
- data/ext/html_to_markdown_rb/extconf.rb +5 -5
- data/ext/html_to_markdown_rb/native/Cargo.lock +962 -0
- data/ext/html_to_markdown_rb/native/Cargo.toml +6 -11
- data/ext/html_to_markdown_rb/native/extconf.rb +14 -0
- data/ext/html_to_markdown_rb/src/lib.rs +1715 -646
- data/lib/html_to_markdown/native.rb +913 -37
- data/lib/html_to_markdown/version.rb +3 -3
- data/lib/html_to_markdown.rb +9 -4
- data/lib/html_to_markdown_rb.so +0 -0
- data/sig/types.rbs +59 -292
- metadata +32 -179
- data/ext/html_to_markdown_rb/Makefile +0 -592
- data/lib/bin/html-to-markdown +0 -0
- data/vendor/Cargo.toml +0 -33
- data/vendor/html-to-markdown-rs/Cargo.toml +0 -54
- data/vendor/html-to-markdown-rs/README.md +0 -278
- data/vendor/html-to-markdown-rs/examples/basic.rs +0 -24
- data/vendor/html-to-markdown-rs/examples/table.rs +0 -25
- data/vendor/html-to-markdown-rs/examples/test_deser.rs +0 -12
- data/vendor/html-to-markdown-rs/examples/test_escape.rs +0 -58
- data/vendor/html-to-markdown-rs/examples/test_inline_formatting.rs +0 -113
- data/vendor/html-to-markdown-rs/examples/test_lists.rs +0 -39
- data/vendor/html-to-markdown-rs/examples/test_semantic_tags.rs +0 -89
- data/vendor/html-to-markdown-rs/examples/test_tables.rs +0 -100
- data/vendor/html-to-markdown-rs/examples/test_task_lists.rs +0 -61
- data/vendor/html-to-markdown-rs/examples/test_whitespace.rs +0 -34
- data/vendor/html-to-markdown-rs/src/convert_api.rs +0 -349
- data/vendor/html-to-markdown-rs/src/converter/block/blockquote.rs +0 -178
- data/vendor/html-to-markdown-rs/src/converter/block/container.rs +0 -114
- data/vendor/html-to-markdown-rs/src/converter/block/div.rs +0 -149
- data/vendor/html-to-markdown-rs/src/converter/block/heading.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/block/horizontal_rule.rs +0 -103
- data/vendor/html-to-markdown-rs/src/converter/block/line_break.rs +0 -89
- data/vendor/html-to-markdown-rs/src/converter/block/mod.rs +0 -10
- data/vendor/html-to-markdown-rs/src/converter/block/paragraph.rs +0 -140
- data/vendor/html-to-markdown-rs/src/converter/block/preformatted.rs +0 -298
- data/vendor/html-to-markdown-rs/src/converter/block/table/builder.rs +0 -453
- data/vendor/html-to-markdown-rs/src/converter/block/table/caption.rs +0 -44
- data/vendor/html-to-markdown-rs/src/converter/block/table/cell.rs +0 -276
- data/vendor/html-to-markdown-rs/src/converter/block/table/cells.rs +0 -336
- data/vendor/html-to-markdown-rs/src/converter/block/table/layout.rs +0 -58
- data/vendor/html-to-markdown-rs/src/converter/block/table/mod.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/block/table/scanner.rs +0 -146
- data/vendor/html-to-markdown-rs/src/converter/block/table/utils.rs +0 -34
- data/vendor/html-to-markdown-rs/src/converter/block/unknown.rs +0 -138
- data/vendor/html-to-markdown-rs/src/converter/context.rs +0 -208
- data/vendor/html-to-markdown-rs/src/converter/dom_context.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/form/elements.rs +0 -770
- data/vendor/html-to-markdown-rs/src/converter/form/mod.rs +0 -82
- data/vendor/html-to-markdown-rs/src/converter/format/djot.rs +0 -64
- data/vendor/html-to-markdown-rs/src/converter/format/markdown.rs +0 -59
- data/vendor/html-to-markdown-rs/src/converter/format/mod.rs +0 -43
- data/vendor/html-to-markdown-rs/src/converter/handlers/blockquote.rs +0 -173
- data/vendor/html-to-markdown-rs/src/converter/handlers/code_block.rs +0 -434
- data/vendor/html-to-markdown-rs/src/converter/handlers/graphic.rs +0 -234
- data/vendor/html-to-markdown-rs/src/converter/handlers/image.rs +0 -282
- data/vendor/html-to-markdown-rs/src/converter/handlers/link.rs +0 -316
- data/vendor/html-to-markdown-rs/src/converter/handlers/mod.rs +0 -26
- data/vendor/html-to-markdown-rs/src/converter/inline/code.rs +0 -306
- data/vendor/html-to-markdown-rs/src/converter/inline/emphasis.rs +0 -345
- data/vendor/html-to-markdown-rs/src/converter/inline/link.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/inline/mod.rs +0 -237
- data/vendor/html-to-markdown-rs/src/converter/inline/ruby.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/marks.rs +0 -566
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/mod.rs +0 -86
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/typography.rs +0 -558
- data/vendor/html-to-markdown-rs/src/converter/list/definition.rs +0 -232
- data/vendor/html-to-markdown-rs/src/converter/list/item.rs +0 -332
- data/vendor/html-to-markdown-rs/src/converter/list/mod.rs +0 -70
- data/vendor/html-to-markdown-rs/src/converter/list/ordered.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/list/unordered.rs +0 -195
- data/vendor/html-to-markdown-rs/src/converter/list/utils.rs +0 -314
- data/vendor/html-to-markdown-rs/src/converter/main.rs +0 -710
- data/vendor/html-to-markdown-rs/src/converter/main_helpers.rs +0 -452
- data/vendor/html-to-markdown-rs/src/converter/media/embedded.rs +0 -393
- data/vendor/html-to-markdown-rs/src/converter/media/graphic.rs +0 -4
- data/vendor/html-to-markdown-rs/src/converter/media/image.rs +0 -183
- data/vendor/html-to-markdown-rs/src/converter/media/mod.rs +0 -87
- data/vendor/html-to-markdown-rs/src/converter/media/svg.rs +0 -280
- data/vendor/html-to-markdown-rs/src/converter/metadata.rs +0 -220
- data/vendor/html-to-markdown-rs/src/converter/mod.rs +0 -156
- data/vendor/html-to-markdown-rs/src/converter/plain_text.rs +0 -516
- data/vendor/html-to-markdown-rs/src/converter/preprocessing_helpers.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/reference_collector.rs +0 -69
- data/vendor/html-to-markdown-rs/src/converter/semantic/attributes.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/semantic/definition_list.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/semantic/figure.rs +0 -391
- data/vendor/html-to-markdown-rs/src/converter/semantic/mod.rs +0 -112
- data/vendor/html-to-markdown-rs/src/converter/semantic/sectioning.rs +0 -85
- data/vendor/html-to-markdown-rs/src/converter/semantic/summary.rs +0 -324
- data/vendor/html-to-markdown-rs/src/converter/text/mod.rs +0 -8
- data/vendor/html-to-markdown-rs/src/converter/text/processing.rs +0 -56
- data/vendor/html-to-markdown-rs/src/converter/text_node.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/utility/attributes.rs +0 -151
- data/vendor/html-to-markdown-rs/src/converter/utility/caching.rs +0 -74
- data/vendor/html-to-markdown-rs/src/converter/utility/content.rs +0 -271
- data/vendor/html-to-markdown-rs/src/converter/utility/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/converter/utility/preprocessing.rs +0 -1002
- data/vendor/html-to-markdown-rs/src/converter/utility/serialization.rs +0 -126
- data/vendor/html-to-markdown-rs/src/converter/utility/siblings.rs +0 -97
- data/vendor/html-to-markdown-rs/src/converter/visitor_hooks.rs +0 -189
- data/vendor/html-to-markdown-rs/src/error.rs +0 -43
- data/vendor/html-to-markdown-rs/src/exports.rs +0 -24
- data/vendor/html-to-markdown-rs/src/inline_images.rs +0 -336
- data/vendor/html-to-markdown-rs/src/lib.rs +0 -139
- data/vendor/html-to-markdown-rs/src/metadata/collector.rs +0 -457
- data/vendor/html-to-markdown-rs/src/metadata/config.rs +0 -394
- data/vendor/html-to-markdown-rs/src/metadata/extraction.rs +0 -398
- data/vendor/html-to-markdown-rs/src/metadata/mod.rs +0 -288
- data/vendor/html-to-markdown-rs/src/metadata/types.rs +0 -477
- data/vendor/html-to-markdown-rs/src/options/conversion.rs +0 -559
- data/vendor/html-to-markdown-rs/src/options/inline_image.rs +0 -111
- data/vendor/html-to-markdown-rs/src/options/mod.rs +0 -20
- data/vendor/html-to-markdown-rs/src/options/preprocessing.rs +0 -201
- data/vendor/html-to-markdown-rs/src/options/validation.rs +0 -416
- data/vendor/html-to-markdown-rs/src/prelude.rs +0 -1
- data/vendor/html-to-markdown-rs/src/rcdom.rs +0 -487
- data/vendor/html-to-markdown-rs/src/text.rs +0 -358
- data/vendor/html-to-markdown-rs/src/types/document.rs +0 -191
- data/vendor/html-to-markdown-rs/src/types/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/types/result.rs +0 -54
- data/vendor/html-to-markdown-rs/src/types/structure_builder.rs +0 -791
- data/vendor/html-to-markdown-rs/src/types/structure_collector.rs +0 -483
- data/vendor/html-to-markdown-rs/src/types/tables.rs +0 -52
- data/vendor/html-to-markdown-rs/src/types/warnings.rs +0 -33
- data/vendor/html-to-markdown-rs/src/validation.rs +0 -158
- data/vendor/html-to-markdown-rs/src/visitor/default_impl.rs +0 -63
- data/vendor/html-to-markdown-rs/src/visitor/mod.rs +0 -41
- data/vendor/html-to-markdown-rs/src/visitor/traits.rs +0 -370
- data/vendor/html-to-markdown-rs/src/visitor/types.rs +0 -319
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/callbacks/mod.rs +0 -1
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/content.rs +0 -126
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/mod.rs +0 -27
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/state.rs +0 -110
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/traversal.rs +0 -250
- data/vendor/html-to-markdown-rs/src/visitor_helpers.rs +0 -597
- data/vendor/html-to-markdown-rs/src/wrapper/sync.rs +0 -413
- data/vendor/html-to-markdown-rs/src/wrapper/utils.rs +0 -290
- data/vendor/html-to-markdown-rs/src/wrapper.rs +0 -9
- data/vendor/html-to-markdown-rs/tests/br_in_inline_test.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/commonmark_compliance_test.rs +0 -297
- data/vendor/html-to-markdown-rs/tests/djot_output_test.rs +0 -153
- data/vendor/html-to-markdown-rs/tests/exclude_selectors_test.rs +0 -132
- data/vendor/html-to-markdown-rs/tests/integration_test.rs +0 -631
- data/vendor/html-to-markdown-rs/tests/issue_121_regressions.rs +0 -49
- data/vendor/html-to-markdown-rs/tests/issue_127_regressions.rs +0 -58
- data/vendor/html-to-markdown-rs/tests/issue_128_regressions.rs +0 -17
- data/vendor/html-to-markdown-rs/tests/issue_131_regressions.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/issue_134_regressions.rs +0 -40
- data/vendor/html-to-markdown-rs/tests/issue_139_regressions.rs +0 -26
- data/vendor/html-to-markdown-rs/tests/issue_140_regressions.rs +0 -185
- data/vendor/html-to-markdown-rs/tests/issue_143_regressions.rs +0 -100
- data/vendor/html-to-markdown-rs/tests/issue_145_regressions.rs +0 -133
- data/vendor/html-to-markdown-rs/tests/issue_146_regressions.rs +0 -144
- data/vendor/html-to-markdown-rs/tests/issue_176_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_190_regressions.rs +0 -128
- data/vendor/html-to-markdown-rs/tests/issue_199_regressions.rs +0 -20
- data/vendor/html-to-markdown-rs/tests/issue_200_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_212_regressions.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/issue_216_217_regressions.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/issue_336_regressions.rs +0 -74
- data/vendor/html-to-markdown-rs/tests/issue_339_regressions.rs +0 -92
- data/vendor/html-to-markdown-rs/tests/issue_347_regressions.rs +0 -154
- data/vendor/html-to-markdown-rs/tests/issue_348_visitor_plain.rs +0 -93
- data/vendor/html-to-markdown-rs/tests/json_ld_script_extraction.rs +0 -44
- data/vendor/html-to-markdown-rs/tests/lists_test.rs +0 -199
- data/vendor/html-to-markdown-rs/tests/plain_output_test.rs +0 -273
- data/vendor/html-to-markdown-rs/tests/preprocessing_tests.rs +0 -61
- data/vendor/html-to-markdown-rs/tests/reference_links_test.rs +0 -169
- data/vendor/html-to-markdown-rs/tests/sectioning_elements_test.rs +0 -137
- data/vendor/html-to-markdown-rs/tests/skip_images_test.rs +0 -522
- data/vendor/html-to-markdown-rs/tests/tables_test.rs +0 -743
- data/vendor/html-to-markdown-rs/tests/test_custom_elements.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/test_issue_187.rs +0 -204
- data/vendor/html-to-markdown-rs/tests/test_issue_218.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/test_issue_277.rs +0 -77
- data/vendor/html-to-markdown-rs/tests/test_max_depth.rs +0 -82
- data/vendor/html-to-markdown-rs/tests/test_nested_simple.rs +0 -45
- data/vendor/html-to-markdown-rs/tests/test_script_style_stripping.rs +0 -396
- data/vendor/html-to-markdown-rs/tests/test_spa_bisect.rs +0 -34
- data/vendor/html-to-markdown-rs/tests/visitor_code_integration_test.rs +0 -121
- data/vendor/html-to-markdown-rs/tests/visitor_integration_test.rs +0 -1190
- data/vendor/html-to-markdown-rs/tests/xml_tables_test.rs +0 -372
|
@@ -1,1190 +0,0 @@
|
|
|
1
|
-
#![allow(missing_docs)]
|
|
2
|
-
|
|
3
|
-
//! Integration tests for the visitor pattern
|
|
4
|
-
//!
|
|
5
|
-
//! These tests verify that visitor callbacks are properly invoked during
|
|
6
|
-
//! HTML→Markdown conversion and that all `VisitResult` variants work correctly.
|
|
7
|
-
|
|
8
|
-
#![cfg(feature = "visitor")]
|
|
9
|
-
|
|
10
|
-
use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult, VisitorHandle};
|
|
11
|
-
use html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};
|
|
12
|
-
use std::cell::RefCell;
|
|
13
|
-
use std::rc::Rc;
|
|
14
|
-
|
|
15
|
-
/// Test shim that bridges the legacy 3-arg call shape used throughout this file
|
|
16
|
-
/// onto the public 2-arg `convert(html, options)` API. The visitor (if any) is
|
|
17
|
-
/// folded into `options.visitor`.
|
|
18
|
-
fn convert(
|
|
19
|
-
html: &str,
|
|
20
|
-
options: Option<ConversionOptions>,
|
|
21
|
-
visitor: Option<VisitorHandle>,
|
|
22
|
-
) -> Result<ConversionResult, ConversionError> {
|
|
23
|
-
let mut opts = options.unwrap_or_default();
|
|
24
|
-
if visitor.is_some() {
|
|
25
|
-
opts.visitor = visitor;
|
|
26
|
-
}
|
|
27
|
-
html_to_markdown_rs::convert(html, Some(opts))
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// Visitor receives `visit_form` callback even when preprocessing would normally drop the form
|
|
31
|
-
#[test]
|
|
32
|
-
fn test_visitor_form_custom_overrides_preprocessing() {
|
|
33
|
-
#[derive(Debug, Default)]
|
|
34
|
-
struct FormVisitor;
|
|
35
|
-
impl HtmlVisitor for FormVisitor {
|
|
36
|
-
fn visit_form(&mut self, _ctx: &NodeContext, action: Option<&str>, _method: Option<&str>) -> VisitResult {
|
|
37
|
-
VisitResult::Custom(format!("[FORM:{}]", action.unwrap_or("none")))
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
let html = r#"<div><form action="/submit" method="POST"><label>Name: <input type="text" name="name"></label><button type="submit">Submit</button></form></div>"#;
|
|
41
|
-
let visitor: VisitorHandle = Rc::new(RefCell::new(FormVisitor));
|
|
42
|
-
let result = convert(html, None, Some(visitor))
|
|
43
|
-
.expect("conversion failed")
|
|
44
|
-
.content
|
|
45
|
-
.unwrap_or_default();
|
|
46
|
-
assert!(
|
|
47
|
-
result.contains("[FORM:/submit]"),
|
|
48
|
-
"Should contain custom form output, got: {result:?}"
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/// Visitor receives `visit_input` callback for inputs inside a form that preprocessing would drop
|
|
53
|
-
#[test]
|
|
54
|
-
fn test_visitor_input_custom_inside_preprocessed_form() {
|
|
55
|
-
#[derive(Debug, Default)]
|
|
56
|
-
struct InputVisitor;
|
|
57
|
-
impl HtmlVisitor for InputVisitor {
|
|
58
|
-
fn visit_input(
|
|
59
|
-
&mut self,
|
|
60
|
-
_ctx: &NodeContext,
|
|
61
|
-
input_type: &str,
|
|
62
|
-
_name: Option<&str>,
|
|
63
|
-
_value: Option<&str>,
|
|
64
|
-
) -> VisitResult {
|
|
65
|
-
VisitResult::Custom(format!("[INPUT:{input_type}]"))
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
let html = r#"<form><input type="text" name="username"></form>"#;
|
|
69
|
-
let visitor: VisitorHandle = Rc::new(RefCell::new(InputVisitor));
|
|
70
|
-
let result = convert(html, None, Some(visitor))
|
|
71
|
-
.expect("conversion failed")
|
|
72
|
-
.content
|
|
73
|
-
.unwrap_or_default();
|
|
74
|
-
assert!(
|
|
75
|
-
result.contains("[INPUT:text]"),
|
|
76
|
-
"Should contain custom input output, got: {result:?}"
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/// Visitor `figure_start` and `figure_end` are both called; `figure_end` receives `figure_start` output
|
|
81
|
-
#[test]
|
|
82
|
-
fn test_visitor_figure_start_end_both_called() {
|
|
83
|
-
#[derive(Debug, Default)]
|
|
84
|
-
struct FigureVisitor;
|
|
85
|
-
impl HtmlVisitor for FigureVisitor {
|
|
86
|
-
fn visit_figure_start(&mut self, _ctx: &NodeContext) -> VisitResult {
|
|
87
|
-
VisitResult::Custom("[FIGURE_START]".to_string())
|
|
88
|
-
}
|
|
89
|
-
fn visit_figure_end(&mut self, _ctx: &NodeContext, output: &str) -> VisitResult {
|
|
90
|
-
VisitResult::Custom(format!("{output}[/FIGURE]"))
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
let html = r#"<figure><img src="test.jpg" alt="test"><figcaption>Caption</figcaption></figure>"#;
|
|
94
|
-
let visitor: VisitorHandle = Rc::new(RefCell::new(FigureVisitor));
|
|
95
|
-
let result = convert(html, None, Some(visitor))
|
|
96
|
-
.expect("conversion failed")
|
|
97
|
-
.content
|
|
98
|
-
.unwrap_or_default();
|
|
99
|
-
assert!(
|
|
100
|
-
result.contains("[FIGURE_START]"),
|
|
101
|
-
"Should contain figure start marker, got: {result:?}"
|
|
102
|
-
);
|
|
103
|
-
assert!(
|
|
104
|
-
result.contains("[/FIGURE]"),
|
|
105
|
-
"Should contain figure end marker, got: {result:?}"
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/// Test visitor that customizes all output
|
|
110
|
-
#[derive(Debug, Default)]
|
|
111
|
-
struct CustomizingVisitor;
|
|
112
|
-
|
|
113
|
-
impl HtmlVisitor for CustomizingVisitor {
|
|
114
|
-
fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
|
|
115
|
-
VisitResult::Custom(format!("[TEXT:{text}]"))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {
|
|
119
|
-
VisitResult::Custom(format!("[LINK:{text} -> {href}]"))
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fn visit_image(&mut self, _ctx: &NodeContext, src: &str, alt: &str, _title: Option<&str>) -> VisitResult {
|
|
123
|
-
VisitResult::Custom(format!("[IMAGE:{alt} @ {src}]"))
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
fn visit_heading(&mut self, _ctx: &NodeContext, level: u32, text: &str, _id: Option<&str>) -> VisitResult {
|
|
127
|
-
VisitResult::Custom(format!("[H{level}: {text}]"))
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/// Test visitor that skips certain elements
|
|
132
|
-
#[derive(Debug, Default)]
|
|
133
|
-
struct SkippingVisitor {
|
|
134
|
-
skip_images: bool,
|
|
135
|
-
skip_links: bool,
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
impl HtmlVisitor for SkippingVisitor {
|
|
139
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
140
|
-
if self.skip_links {
|
|
141
|
-
VisitResult::Skip
|
|
142
|
-
} else {
|
|
143
|
-
VisitResult::Continue
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
|
|
148
|
-
if self.skip_images {
|
|
149
|
-
VisitResult::Skip
|
|
150
|
-
} else {
|
|
151
|
-
VisitResult::Continue
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/// Test visitor that preserves HTML for certain elements
|
|
157
|
-
#[derive(Debug, Default)]
|
|
158
|
-
struct PreservingVisitor {
|
|
159
|
-
preserve_links: bool,
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
impl HtmlVisitor for PreservingVisitor {
|
|
163
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
164
|
-
if self.preserve_links {
|
|
165
|
-
VisitResult::PreserveHtml
|
|
166
|
-
} else {
|
|
167
|
-
VisitResult::Continue
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/// Test visitor that validates node context
|
|
173
|
-
#[derive(Debug, Default)]
|
|
174
|
-
struct ContextCheckingVisitor {
|
|
175
|
-
saw_heading_with_id: bool,
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
impl HtmlVisitor for ContextCheckingVisitor {
|
|
179
|
-
fn visit_heading(&mut self, ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
|
|
180
|
-
assert_eq!(ctx.node_type, NodeType::Heading);
|
|
181
|
-
assert_eq!(ctx.tag_name, "h1");
|
|
182
|
-
|
|
183
|
-
if ctx.attributes.contains_key("id") {
|
|
184
|
-
self.saw_heading_with_id = true;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
VisitResult::Continue
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
#[test]
|
|
192
|
-
fn test_custom_visitor_transforms_text() {
|
|
193
|
-
let html = r"<p>Hello world</p>";
|
|
194
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
195
|
-
|
|
196
|
-
let result = convert(html, None, Some(visitor))
|
|
197
|
-
.expect("conversion failed")
|
|
198
|
-
.content
|
|
199
|
-
.unwrap_or_default();
|
|
200
|
-
|
|
201
|
-
assert!(result.contains("[TEXT:"), "Should contain custom text format");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
#[test]
|
|
205
|
-
fn test_custom_visitor_transforms_links() {
|
|
206
|
-
let html = r#"<a href="https://example.com">Example</a>"#;
|
|
207
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
208
|
-
|
|
209
|
-
let result = convert(html, None, Some(visitor))
|
|
210
|
-
.expect("conversion failed")
|
|
211
|
-
.content
|
|
212
|
-
.unwrap_or_default();
|
|
213
|
-
|
|
214
|
-
assert!(
|
|
215
|
-
result.contains("[LINK:Example -> https://example.com]"),
|
|
216
|
-
"Should contain custom link format, got: {result}"
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
#[test]
|
|
221
|
-
fn test_custom_visitor_transforms_images() {
|
|
222
|
-
let html = r#"<img src="/test.png" alt="Test">"#;
|
|
223
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
224
|
-
|
|
225
|
-
let result = convert(html, None, Some(visitor))
|
|
226
|
-
.expect("conversion failed")
|
|
227
|
-
.content
|
|
228
|
-
.unwrap_or_default();
|
|
229
|
-
|
|
230
|
-
assert!(
|
|
231
|
-
result.contains("[IMAGE:Test @ /test.png]"),
|
|
232
|
-
"Should contain custom image format, got: {result}"
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
#[test]
|
|
237
|
-
fn test_custom_visitor_transforms_headings() {
|
|
238
|
-
let html = r"<h2>My Heading</h2>";
|
|
239
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
240
|
-
|
|
241
|
-
let result = convert(html, None, Some(visitor))
|
|
242
|
-
.expect("conversion failed")
|
|
243
|
-
.content
|
|
244
|
-
.unwrap_or_default();
|
|
245
|
-
|
|
246
|
-
assert!(
|
|
247
|
-
result.contains("[H2: My Heading]"),
|
|
248
|
-
"Should contain custom heading format, got: {result}"
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#[test]
|
|
253
|
-
fn test_skipping_visitor_removes_links() {
|
|
254
|
-
let html = r#"<p>Text with <a href="https://example.com">a link</a> inside.</p>"#;
|
|
255
|
-
let visitor = Rc::new(RefCell::new(SkippingVisitor {
|
|
256
|
-
skip_links: true,
|
|
257
|
-
skip_images: false,
|
|
258
|
-
}));
|
|
259
|
-
|
|
260
|
-
let result = convert(html, None, Some(visitor))
|
|
261
|
-
.expect("conversion failed")
|
|
262
|
-
.content
|
|
263
|
-
.unwrap_or_default();
|
|
264
|
-
|
|
265
|
-
assert!(
|
|
266
|
-
!result.contains("example.com"),
|
|
267
|
-
"Should not contain link URL when skipped, got: {result}"
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
#[test]
|
|
272
|
-
fn test_skipping_visitor_removes_images() {
|
|
273
|
-
let html = r#"<p>Text <img src="/test.png" alt="Test"> more text</p>"#;
|
|
274
|
-
let visitor = Rc::new(RefCell::new(SkippingVisitor {
|
|
275
|
-
skip_links: false,
|
|
276
|
-
skip_images: true,
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
let result = convert(html, None, Some(visitor))
|
|
280
|
-
.expect("conversion failed")
|
|
281
|
-
.content
|
|
282
|
-
.unwrap_or_default();
|
|
283
|
-
|
|
284
|
-
assert!(
|
|
285
|
-
!result.contains("test.png") && !result.contains("!["),
|
|
286
|
-
"Should not contain image when skipped, got: {result}"
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
#[test]
|
|
291
|
-
fn test_preserving_visitor_keeps_html() {
|
|
292
|
-
let html = r#"<a href="https://example.com" class="special">Example</a>"#;
|
|
293
|
-
let visitor = Rc::new(RefCell::new(PreservingVisitor { preserve_links: true }));
|
|
294
|
-
|
|
295
|
-
let result = convert(html, None, Some(visitor))
|
|
296
|
-
.expect("conversion failed")
|
|
297
|
-
.content
|
|
298
|
-
.unwrap_or_default();
|
|
299
|
-
|
|
300
|
-
assert!(
|
|
301
|
-
result.contains("<a") && result.contains("href"),
|
|
302
|
-
"Should preserve HTML tags when PreserveHtml is returned, got: {result}"
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
#[test]
|
|
307
|
-
fn test_visitor_receives_node_context() {
|
|
308
|
-
let html = r#"<h1 id="title" class="main">Title</h1>"#;
|
|
309
|
-
let visitor = Rc::new(RefCell::new(ContextCheckingVisitor::default()));
|
|
310
|
-
|
|
311
|
-
let _result = convert(html, None, Some(visitor)).expect("conversion failed");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
#[test]
|
|
315
|
-
fn test_visitor_works_with_complex_document() {
|
|
316
|
-
let html = r#"
|
|
317
|
-
<!DOCTYPE html>
|
|
318
|
-
<html>
|
|
319
|
-
<head><title>Test</title></head>
|
|
320
|
-
<body>
|
|
321
|
-
<h1>Main Title</h1>
|
|
322
|
-
<p>Introduction with <a href="/link">a link</a>.</p>
|
|
323
|
-
<h2>Section</h2>
|
|
324
|
-
<p>Text with <strong>bold</strong> and <em>italic</em>.</p>
|
|
325
|
-
<img src="/image.png" alt="Diagram">
|
|
326
|
-
<h3>Subsection</h3>
|
|
327
|
-
<p>More content.</p>
|
|
328
|
-
</body>
|
|
329
|
-
</html>
|
|
330
|
-
"#;
|
|
331
|
-
|
|
332
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
333
|
-
|
|
334
|
-
let result = convert(html, None, Some(visitor))
|
|
335
|
-
.expect("conversion failed")
|
|
336
|
-
.content
|
|
337
|
-
.unwrap_or_default();
|
|
338
|
-
|
|
339
|
-
assert!(result.contains("[H1:"));
|
|
340
|
-
assert!(result.contains("[H2:"));
|
|
341
|
-
assert!(result.contains("[H3:"));
|
|
342
|
-
assert!(result.contains("[LINK:"));
|
|
343
|
-
assert!(result.contains("[IMAGE:"));
|
|
344
|
-
assert!(result.contains("[TEXT:"));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
#[test]
|
|
348
|
-
fn test_visitor_with_conversion_options() {
|
|
349
|
-
#[derive(Debug, Default)]
|
|
350
|
-
struct ContinueVisitor;
|
|
351
|
-
|
|
352
|
-
impl HtmlVisitor for ContinueVisitor {}
|
|
353
|
-
|
|
354
|
-
let html = r"<h1>Title</h1><p>Text with *asterisks* and _underscores_.</p>";
|
|
355
|
-
|
|
356
|
-
let options = ConversionOptions {
|
|
357
|
-
escape_asterisks: true,
|
|
358
|
-
escape_underscores: true,
|
|
359
|
-
..Default::default()
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
let visitor = Rc::new(RefCell::new(ContinueVisitor));
|
|
363
|
-
|
|
364
|
-
let result = convert(html, Some(options), Some(visitor))
|
|
365
|
-
.expect("conversion failed")
|
|
366
|
-
.content
|
|
367
|
-
.unwrap_or_default();
|
|
368
|
-
|
|
369
|
-
assert!(
|
|
370
|
-
result.contains(r"\*") || result.contains(r"\_"),
|
|
371
|
-
"Should respect escape options with visitor, got: {result}"
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
#[test]
|
|
376
|
-
fn test_visitor_continue_result_produces_default_markdown() {
|
|
377
|
-
#[derive(Debug, Default)]
|
|
378
|
-
struct ContinueVisitor;
|
|
379
|
-
|
|
380
|
-
impl HtmlVisitor for ContinueVisitor {
|
|
381
|
-
fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
|
|
382
|
-
VisitResult::Continue
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let html = r"<h1>Title</h1>";
|
|
387
|
-
let visitor = Rc::new(RefCell::new(ContinueVisitor));
|
|
388
|
-
|
|
389
|
-
let result = convert(html, None, Some(visitor))
|
|
390
|
-
.expect("conversion failed")
|
|
391
|
-
.content
|
|
392
|
-
.unwrap_or_default();
|
|
393
|
-
|
|
394
|
-
assert!(
|
|
395
|
-
result.contains("# Title"),
|
|
396
|
-
"Continue should produce default markdown, got: {result}"
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
#[test]
|
|
401
|
-
fn test_visitor_skip_vs_continue() {
|
|
402
|
-
#[derive(Debug)]
|
|
403
|
-
struct SelectiveSkipper {
|
|
404
|
-
skip_first_link: bool,
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
impl HtmlVisitor for SelectiveSkipper {
|
|
408
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
409
|
-
if self.skip_first_link {
|
|
410
|
-
self.skip_first_link = false;
|
|
411
|
-
VisitResult::Skip
|
|
412
|
-
} else {
|
|
413
|
-
VisitResult::Continue
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
let html = r#"<p><a href="/first">First</a> and <a href="/second">Second</a></p>"#;
|
|
419
|
-
let visitor = Rc::new(RefCell::new(SelectiveSkipper { skip_first_link: true }));
|
|
420
|
-
|
|
421
|
-
let result = convert(html, None, Some(visitor))
|
|
422
|
-
.expect("conversion failed")
|
|
423
|
-
.content
|
|
424
|
-
.unwrap_or_default();
|
|
425
|
-
|
|
426
|
-
assert!(!result.contains("/first"));
|
|
427
|
-
assert!(result.contains("/second"));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
#[test]
|
|
431
|
-
fn test_multiple_elements_of_same_type() {
|
|
432
|
-
let html = r"<h1>First</h1><h2>Second</h2><h3>Third</h3>";
|
|
433
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
434
|
-
|
|
435
|
-
let result = convert(html, None, Some(visitor))
|
|
436
|
-
.expect("conversion failed")
|
|
437
|
-
.content
|
|
438
|
-
.unwrap_or_default();
|
|
439
|
-
|
|
440
|
-
assert!(result.contains("[H1: First]"));
|
|
441
|
-
assert!(result.contains("[H2: Second]"));
|
|
442
|
-
assert!(result.contains("[H3: Third]"));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
#[test]
|
|
446
|
-
fn test_nested_elements_invoke_visitor() {
|
|
447
|
-
let html = r#"<p>Text with <a href="/url">a <strong>bold</strong> link</a></p>"#;
|
|
448
|
-
let visitor = Rc::new(RefCell::new(CustomizingVisitor));
|
|
449
|
-
|
|
450
|
-
let result = convert(html, None, Some(visitor))
|
|
451
|
-
.expect("conversion failed")
|
|
452
|
-
.content
|
|
453
|
-
.unwrap_or_default();
|
|
454
|
-
|
|
455
|
-
assert!(result.contains("[TEXT:"));
|
|
456
|
-
assert!(result.contains("[LINK:"));
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
#[test]
|
|
460
|
-
fn test_visitor_error_stops_conversion() {
|
|
461
|
-
#[derive(Debug, Default)]
|
|
462
|
-
struct ErrorVisitor;
|
|
463
|
-
|
|
464
|
-
impl HtmlVisitor for ErrorVisitor {
|
|
465
|
-
fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
|
|
466
|
-
VisitResult::Error("test error".to_string())
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
let html = "<p>text</p>";
|
|
471
|
-
let visitor = Rc::new(RefCell::new(ErrorVisitor));
|
|
472
|
-
let result = convert(html, None, Some(visitor));
|
|
473
|
-
|
|
474
|
-
assert!(result.is_err(), "Should return error when visitor returns Error");
|
|
475
|
-
assert!(
|
|
476
|
-
result.unwrap_err().to_string().contains("test error"),
|
|
477
|
-
"Error message should contain visitor's error"
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
#[test]
|
|
482
|
-
fn test_visitor_code_block() {
|
|
483
|
-
#[derive(Debug, Default)]
|
|
484
|
-
struct CodeBlockVisitor;
|
|
485
|
-
|
|
486
|
-
impl HtmlVisitor for CodeBlockVisitor {
|
|
487
|
-
fn visit_code_block(&mut self, _ctx: &NodeContext, language: Option<&str>, code: &str) -> VisitResult {
|
|
488
|
-
let lang = language.unwrap_or("text");
|
|
489
|
-
VisitResult::Custom(format!("[CODE_BLOCK:{} -> {}]", lang, code.trim()))
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
let html = r#"<pre><code class="language-rust">fn main() {}</code></pre>"#;
|
|
494
|
-
let visitor = Rc::new(RefCell::new(CodeBlockVisitor));
|
|
495
|
-
let result = convert(html, None, Some(visitor))
|
|
496
|
-
.expect("conversion failed")
|
|
497
|
-
.content
|
|
498
|
-
.unwrap_or_default();
|
|
499
|
-
|
|
500
|
-
assert!(
|
|
501
|
-
result.contains("[CODE_BLOCK:rust -> fn main() {}]"),
|
|
502
|
-
"Should contain custom code block format, got: {result}"
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
#[test]
|
|
507
|
-
fn test_visitor_code_inline() {
|
|
508
|
-
#[derive(Debug, Default)]
|
|
509
|
-
struct InlineCodeVisitor;
|
|
510
|
-
|
|
511
|
-
impl HtmlVisitor for InlineCodeVisitor {
|
|
512
|
-
fn visit_code_inline(&mut self, _ctx: &NodeContext, code: &str) -> VisitResult {
|
|
513
|
-
VisitResult::Custom(format!("[CODE:{code}]"))
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
let html = r"<p>Use <code>println!</code> macro</p>";
|
|
518
|
-
let visitor = Rc::new(RefCell::new(InlineCodeVisitor));
|
|
519
|
-
let result = convert(html, None, Some(visitor))
|
|
520
|
-
.expect("conversion failed")
|
|
521
|
-
.content
|
|
522
|
-
.unwrap_or_default();
|
|
523
|
-
|
|
524
|
-
assert!(
|
|
525
|
-
result.contains("[CODE:println!]"),
|
|
526
|
-
"Should contain custom inline code format, got: {result}"
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
#[test]
|
|
531
|
-
fn test_visitor_list_callbacks() {
|
|
532
|
-
#[derive(Debug, Default)]
|
|
533
|
-
struct ListVisitor {
|
|
534
|
-
list_depth: usize,
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
impl HtmlVisitor for ListVisitor {
|
|
538
|
-
fn visit_list_start(&mut self, _ctx: &NodeContext, ordered: bool) -> VisitResult {
|
|
539
|
-
self.list_depth += 1;
|
|
540
|
-
VisitResult::Custom(format!(
|
|
541
|
-
"[LIST_START:{}:{}]",
|
|
542
|
-
if ordered { "OL" } else { "UL" },
|
|
543
|
-
self.list_depth
|
|
544
|
-
))
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
fn visit_list_item(&mut self, _ctx: &NodeContext, _ordered: bool, _marker: &str, text: &str) -> VisitResult {
|
|
548
|
-
VisitResult::Custom(format!("[LI:{}:{}]", self.list_depth, text.trim()))
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
fn visit_list_end(&mut self, _ctx: &NodeContext, _ordered: bool, _output: &str) -> VisitResult {
|
|
552
|
-
let result = VisitResult::Custom(format!("[LIST_END:{}]", self.list_depth));
|
|
553
|
-
self.list_depth = self.list_depth.saturating_sub(1);
|
|
554
|
-
result
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
let html = r"<ul><li>First</li><li>Second</li></ul>";
|
|
559
|
-
let visitor = Rc::new(RefCell::new(ListVisitor::default()));
|
|
560
|
-
let result = convert(html, None, Some(visitor))
|
|
561
|
-
.expect("conversion failed")
|
|
562
|
-
.content
|
|
563
|
-
.unwrap_or_default();
|
|
564
|
-
|
|
565
|
-
assert!(
|
|
566
|
-
result.contains("[LIST_START:UL:1]"),
|
|
567
|
-
"Should see list start, got: {result}"
|
|
568
|
-
);
|
|
569
|
-
assert!(result.contains("[LI:1:First]"), "Should see first item, got: {result}");
|
|
570
|
-
assert!(
|
|
571
|
-
result.contains("[LI:1:Second]"),
|
|
572
|
-
"Should see second item, got: {result}"
|
|
573
|
-
);
|
|
574
|
-
assert!(result.contains("[LIST_END:1]"), "Should see list end, got: {result}");
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
#[test]
|
|
578
|
-
fn test_visitor_table_callbacks() {
|
|
579
|
-
#[derive(Debug, Default)]
|
|
580
|
-
struct TableVisitor {
|
|
581
|
-
row_count: usize,
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
impl HtmlVisitor for TableVisitor {
|
|
585
|
-
fn visit_table_start(&mut self, _ctx: &NodeContext) -> VisitResult {
|
|
586
|
-
self.row_count = 0;
|
|
587
|
-
VisitResult::Custom("[TABLE_START]".to_string())
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
fn visit_table_row(&mut self, _ctx: &NodeContext, cells: &[String], is_header: bool) -> VisitResult {
|
|
591
|
-
self.row_count += 1;
|
|
592
|
-
VisitResult::Custom(format!(
|
|
593
|
-
"[ROW:{}:{}:{}]",
|
|
594
|
-
if is_header { "HEADER" } else { "DATA" },
|
|
595
|
-
self.row_count,
|
|
596
|
-
cells.join("|")
|
|
597
|
-
))
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
fn visit_table_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {
|
|
601
|
-
VisitResult::Custom(format!("[TABLE_END:{}]", self.row_count))
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
let html = r"<table><tr><th>Name</th><th>Age</th></tr><tr><td>Alice</td><td>30</td></tr></table>";
|
|
606
|
-
let visitor = Rc::new(RefCell::new(TableVisitor::default()));
|
|
607
|
-
let result = convert(html, None, Some(visitor))
|
|
608
|
-
.expect("conversion failed")
|
|
609
|
-
.content
|
|
610
|
-
.unwrap_or_default();
|
|
611
|
-
|
|
612
|
-
assert!(
|
|
613
|
-
result.contains("[TABLE_START]"),
|
|
614
|
-
"Should see table start, got: {result}"
|
|
615
|
-
);
|
|
616
|
-
assert!(
|
|
617
|
-
result.contains("[ROW:HEADER:1:Name|Age]"),
|
|
618
|
-
"Should see header row, got: {result}"
|
|
619
|
-
);
|
|
620
|
-
assert!(
|
|
621
|
-
result.contains("[ROW:DATA:2:Alice|30]"),
|
|
622
|
-
"Should see data row, got: {result}"
|
|
623
|
-
);
|
|
624
|
-
assert!(result.contains("[TABLE_END:2]"), "Should see table end, got: {result}");
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
#[test]
|
|
628
|
-
fn test_visitor_blockquote() {
|
|
629
|
-
#[derive(Debug, Default)]
|
|
630
|
-
struct BlockquoteVisitor;
|
|
631
|
-
|
|
632
|
-
impl HtmlVisitor for BlockquoteVisitor {
|
|
633
|
-
fn visit_blockquote(&mut self, _ctx: &NodeContext, content: &str, _depth: usize) -> VisitResult {
|
|
634
|
-
VisitResult::Custom(format!("[QUOTE:{}]", content.trim()))
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
let html = r"<blockquote>This is a quote</blockquote>";
|
|
639
|
-
let visitor = Rc::new(RefCell::new(BlockquoteVisitor));
|
|
640
|
-
let result = convert(html, None, Some(visitor))
|
|
641
|
-
.expect("conversion failed")
|
|
642
|
-
.content
|
|
643
|
-
.unwrap_or_default();
|
|
644
|
-
|
|
645
|
-
assert!(
|
|
646
|
-
result.contains("[QUOTE:This is a quote]"),
|
|
647
|
-
"Should contain custom blockquote format, got: {result}"
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
#[test]
|
|
652
|
-
fn test_visitor_inline_formatting() {
|
|
653
|
-
#[derive(Debug, Default)]
|
|
654
|
-
struct FormattingVisitor;
|
|
655
|
-
|
|
656
|
-
impl HtmlVisitor for FormattingVisitor {
|
|
657
|
-
fn visit_strong(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
|
|
658
|
-
VisitResult::Custom(format!("[STRONG:{text}]"))
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
fn visit_emphasis(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
|
|
662
|
-
VisitResult::Custom(format!("[EM:{text}]"))
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
fn visit_strikethrough(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
|
|
666
|
-
VisitResult::Custom(format!("[DEL:{text}]"))
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
let html = r"<p><strong>bold</strong> <em>italic</em> <del>struck</del></p>";
|
|
671
|
-
let visitor = Rc::new(RefCell::new(FormattingVisitor));
|
|
672
|
-
let result = convert(html, None, Some(visitor))
|
|
673
|
-
.expect("conversion failed")
|
|
674
|
-
.content
|
|
675
|
-
.unwrap_or_default();
|
|
676
|
-
|
|
677
|
-
assert!(result.contains("[STRONG:bold]"), "Should see strong, got: {result}");
|
|
678
|
-
assert!(result.contains("[EM:italic]"), "Should see emphasis, got: {result}");
|
|
679
|
-
assert!(
|
|
680
|
-
result.contains("[DEL:struck]"),
|
|
681
|
-
"Should see strikethrough, got: {result}"
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
#[test]
|
|
686
|
-
fn test_no_double_visit_in_links() {
|
|
687
|
-
#[derive(Debug, Default)]
|
|
688
|
-
struct CountingVisitor {
|
|
689
|
-
text_visits: usize,
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
impl HtmlVisitor for CountingVisitor {
|
|
693
|
-
fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
|
|
694
|
-
self.text_visits += 1;
|
|
695
|
-
VisitResult::Continue
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
699
|
-
VisitResult::Continue
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
let html = r#"<a href="/url">link text</a>"#;
|
|
704
|
-
let visitor = Rc::new(RefCell::new(CountingVisitor::default()));
|
|
705
|
-
let _result = convert(html, None, Some(visitor.clone())).expect("conversion failed");
|
|
706
|
-
|
|
707
|
-
assert_eq!(
|
|
708
|
-
visitor.borrow().text_visits,
|
|
709
|
-
1,
|
|
710
|
-
"Text nodes inside links should only be visited once, got {} visits",
|
|
711
|
-
visitor.borrow().text_visits
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
#[test]
|
|
716
|
-
fn test_no_double_visit_in_headings() {
|
|
717
|
-
#[derive(Debug, Default)]
|
|
718
|
-
struct CountingVisitor {
|
|
719
|
-
text_visits: usize,
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
impl HtmlVisitor for CountingVisitor {
|
|
723
|
-
fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
|
|
724
|
-
self.text_visits += 1;
|
|
725
|
-
VisitResult::Continue
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
|
|
729
|
-
VisitResult::Continue
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
let html = r"<h1>heading text</h1>";
|
|
734
|
-
let visitor = Rc::new(RefCell::new(CountingVisitor::default()));
|
|
735
|
-
let _result = convert(html, None, Some(visitor.clone())).expect("conversion failed");
|
|
736
|
-
|
|
737
|
-
assert_eq!(
|
|
738
|
-
visitor.borrow().text_visits,
|
|
739
|
-
1,
|
|
740
|
-
"Text nodes inside headings should only be visited once, got {} visits",
|
|
741
|
-
visitor.borrow().text_visits
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// ============================================================================
|
|
746
|
-
// Integration tests: Visitor + Feature combinations
|
|
747
|
-
// ============================================================================
|
|
748
|
-
|
|
749
|
-
/// Test that visitor callbacks work correctly when `skip_images` option is enabled
|
|
750
|
-
#[test]
|
|
751
|
-
fn test_visitor_with_skip_images() {
|
|
752
|
-
#[derive(Debug, Default)]
|
|
753
|
-
struct SkipImageVisitor {
|
|
754
|
-
image_visits: usize,
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
impl HtmlVisitor for SkipImageVisitor {
|
|
758
|
-
fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
|
|
759
|
-
self.image_visits += 1;
|
|
760
|
-
VisitResult::Continue
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
let html = r#"
|
|
765
|
-
<p>Some text</p>
|
|
766
|
-
<img src="/image1.png" alt="Image 1">
|
|
767
|
-
<img src="/image2.png" alt="Image 2">
|
|
768
|
-
<p>More text</p>
|
|
769
|
-
"#;
|
|
770
|
-
|
|
771
|
-
// Test with skip_images enabled and visitor
|
|
772
|
-
let options = ConversionOptions {
|
|
773
|
-
skip_images: true,
|
|
774
|
-
..Default::default()
|
|
775
|
-
};
|
|
776
|
-
|
|
777
|
-
let visitor = Rc::new(RefCell::new(SkipImageVisitor::default()));
|
|
778
|
-
let result = convert(html, Some(options), Some(visitor))
|
|
779
|
-
.expect("conversion with skip_images and visitor should succeed")
|
|
780
|
-
.content
|
|
781
|
-
.unwrap_or_default();
|
|
782
|
-
|
|
783
|
-
// When skip_images is true, images should not appear in output
|
|
784
|
-
assert!(
|
|
785
|
-
!result.contains("!["),
|
|
786
|
-
"skip_images should prevent image markdown in output, got: {result}"
|
|
787
|
-
);
|
|
788
|
-
assert!(
|
|
789
|
-
!result.contains("image1.png"),
|
|
790
|
-
"skip_images should prevent image src in output, got: {result}"
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
// When skip_images is true, the conversion still happens correctly
|
|
794
|
-
// Images are filtered at the conversion level based on the option
|
|
795
|
-
// This verifies that skip_images option and visitor parameters work together
|
|
796
|
-
// without conflicts - both are optional and can be combined
|
|
797
|
-
assert!(
|
|
798
|
-
result.contains("Some text") && result.contains("More text"),
|
|
799
|
-
"Other content should still be present in output, got: {result}"
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/// Test that the main `convert()` function accepts optional visitor parameter
|
|
804
|
-
#[test]
|
|
805
|
-
fn test_convert_accepts_visitor_parameter() {
|
|
806
|
-
#[derive(Debug, Default)]
|
|
807
|
-
struct CountingVisitor {
|
|
808
|
-
text_count: usize,
|
|
809
|
-
link_count: usize,
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
impl HtmlVisitor for CountingVisitor {
|
|
813
|
-
fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
|
|
814
|
-
self.text_count += 1;
|
|
815
|
-
VisitResult::Continue
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
819
|
-
self.link_count += 1;
|
|
820
|
-
VisitResult::Continue
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
let html = r#"<p>Visit <a href="https://example.com">our site</a> for more info.</p>"#;
|
|
825
|
-
let visitor = Rc::new(RefCell::new(CountingVisitor::default()));
|
|
826
|
-
|
|
827
|
-
// Test using the main convert() function with visitor parameter
|
|
828
|
-
let _result = convert(html, None, Some(visitor.clone())).expect("convert with visitor should work");
|
|
829
|
-
|
|
830
|
-
let borrowed = visitor.borrow();
|
|
831
|
-
assert!(
|
|
832
|
-
borrowed.text_count >= 2,
|
|
833
|
-
"Should visit text nodes, got {} visits",
|
|
834
|
-
borrowed.text_count
|
|
835
|
-
);
|
|
836
|
-
assert_eq!(
|
|
837
|
-
borrowed.link_count, 1,
|
|
838
|
-
"Should visit exactly 1 link, got {}",
|
|
839
|
-
borrowed.link_count
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/// Test visitor + `inline_images` feature combination
|
|
844
|
-
///
|
|
845
|
-
/// In v3, `convert()` handles inline-image extraction via `ConversionResult.images`,
|
|
846
|
-
/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths
|
|
847
|
-
/// work on the same HTML.
|
|
848
|
-
#[cfg(feature = "inline-images")]
|
|
849
|
-
#[test]
|
|
850
|
-
fn test_convert_with_inline_images_accepts_visitor() {
|
|
851
|
-
#[derive(Debug, Default)]
|
|
852
|
-
struct ImageTrackingVisitor {
|
|
853
|
-
images_seen: usize,
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
impl HtmlVisitor for ImageTrackingVisitor {
|
|
857
|
-
fn visit_image(&mut self, _ctx: &NodeContext, src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
|
|
858
|
-
if !src.starts_with("data:") {
|
|
859
|
-
self.images_seen += 1;
|
|
860
|
-
}
|
|
861
|
-
VisitResult::Continue
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
let html = r#"
|
|
866
|
-
<h1>Test Page</h1>
|
|
867
|
-
<img src="/image.png" alt="Test Image">
|
|
868
|
-
<p>Some content</p>
|
|
869
|
-
"#;
|
|
870
|
-
|
|
871
|
-
// Verify visitor callbacks fire via convert_with_visitor
|
|
872
|
-
let visitor = Rc::new(RefCell::new(ImageTrackingVisitor::default()));
|
|
873
|
-
let markdown = convert(html, None, Some(visitor.clone()))
|
|
874
|
-
.expect("convert should work")
|
|
875
|
-
.content
|
|
876
|
-
.unwrap_or_default();
|
|
877
|
-
|
|
878
|
-
assert_eq!(
|
|
879
|
-
visitor.borrow().images_seen,
|
|
880
|
-
1,
|
|
881
|
-
"Visitor should count 1 non-data-uri image"
|
|
882
|
-
);
|
|
883
|
-
|
|
884
|
-
// Markdown should still be generated
|
|
885
|
-
assert!(!markdown.is_empty(), "Should produce markdown output");
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
/// Test visitor + metadata: visitor callbacks fire and metadata is collected.
|
|
889
|
-
///
|
|
890
|
-
/// In v3, `convert()` always extracts metadata into `ConversionResult.metadata`,
|
|
891
|
-
/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths
|
|
892
|
-
/// work on the same HTML.
|
|
893
|
-
#[cfg(feature = "metadata")]
|
|
894
|
-
#[test]
|
|
895
|
-
fn test_visitor_and_metadata_both_work() {
|
|
896
|
-
#[derive(Debug, Default)]
|
|
897
|
-
struct MetadataAwareVisitor {
|
|
898
|
-
heading_count: usize,
|
|
899
|
-
link_count: usize,
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
impl HtmlVisitor for MetadataAwareVisitor {
|
|
903
|
-
fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
|
|
904
|
-
self.heading_count += 1;
|
|
905
|
-
VisitResult::Continue
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
909
|
-
self.link_count += 1;
|
|
910
|
-
VisitResult::Continue
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
let html = r#"
|
|
915
|
-
<html>
|
|
916
|
-
<head><title>Test Page</title></head>
|
|
917
|
-
<body>
|
|
918
|
-
<h1>Main Title</h1>
|
|
919
|
-
<p>Visit <a href="https://example.com">our site</a>.</p>
|
|
920
|
-
<h2>Section</h2>
|
|
921
|
-
<p>More <a href="/page">links</a> here.</p>
|
|
922
|
-
</body>
|
|
923
|
-
</html>
|
|
924
|
-
"#;
|
|
925
|
-
|
|
926
|
-
// Verify visitor callbacks fire via convert_with_visitor
|
|
927
|
-
let visitor = Rc::new(RefCell::new(MetadataAwareVisitor::default()));
|
|
928
|
-
let markdown = convert(html, None, Some(visitor.clone()))
|
|
929
|
-
.expect("convert should work")
|
|
930
|
-
.content
|
|
931
|
-
.unwrap_or_default();
|
|
932
|
-
|
|
933
|
-
let borrowed = visitor.borrow();
|
|
934
|
-
assert!(
|
|
935
|
-
borrowed.heading_count >= 2,
|
|
936
|
-
"Visitor should see at least 2 headings, got {}",
|
|
937
|
-
borrowed.heading_count
|
|
938
|
-
);
|
|
939
|
-
assert_eq!(
|
|
940
|
-
borrowed.link_count, 2,
|
|
941
|
-
"Visitor should see 2 links, got {}",
|
|
942
|
-
borrowed.link_count
|
|
943
|
-
);
|
|
944
|
-
assert!(!markdown.is_empty(), "Should produce markdown output");
|
|
945
|
-
drop(borrowed);
|
|
946
|
-
|
|
947
|
-
// Verify metadata extraction via convert()
|
|
948
|
-
let result = html_to_markdown_rs::convert(html, None).expect("convert should work");
|
|
949
|
-
let metadata = result.metadata;
|
|
950
|
-
|
|
951
|
-
assert_eq!(
|
|
952
|
-
metadata.document.title,
|
|
953
|
-
Some("Test Page".to_string()),
|
|
954
|
-
"Metadata should extract title"
|
|
955
|
-
);
|
|
956
|
-
assert!(
|
|
957
|
-
metadata.headers.len() >= 2,
|
|
958
|
-
"Metadata should extract at least 2 headers, got {}",
|
|
959
|
-
metadata.headers.len()
|
|
960
|
-
);
|
|
961
|
-
assert_eq!(
|
|
962
|
-
metadata.links.len(),
|
|
963
|
-
2,
|
|
964
|
-
"Metadata should extract 2 links, got {}",
|
|
965
|
-
metadata.links.len()
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/// Test visitor + both `inline_images` and `metadata` features together
|
|
970
|
-
///
|
|
971
|
-
/// In v3, `convert()` handles metadata and inline-image extraction via `ConversionResult`,
|
|
972
|
-
/// and `convert_with_visitor()` handles visitor callbacks. We verify both paths
|
|
973
|
-
/// work on the same HTML.
|
|
974
|
-
#[cfg(all(feature = "inline-images", feature = "metadata"))]
|
|
975
|
-
#[test]
|
|
976
|
-
fn test_convert_with_all_features_and_visitor() {
|
|
977
|
-
#[derive(Debug, Default)]
|
|
978
|
-
struct ComprehensiveVisitor {
|
|
979
|
-
headings: usize,
|
|
980
|
-
images: usize,
|
|
981
|
-
links: usize,
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
impl HtmlVisitor for ComprehensiveVisitor {
|
|
985
|
-
fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
|
|
986
|
-
self.headings += 1;
|
|
987
|
-
VisitResult::Continue
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
|
|
991
|
-
self.images += 1;
|
|
992
|
-
VisitResult::Continue
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
|
|
996
|
-
self.links += 1;
|
|
997
|
-
VisitResult::Continue
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
let html = r#"
|
|
1002
|
-
<html>
|
|
1003
|
-
<body>
|
|
1004
|
-
<h1>Gallery</h1>
|
|
1005
|
-
<img src="/gallery/image1.jpg" alt="Pic 1">
|
|
1006
|
-
<p>See <a href="/more">more</a> content.</p>
|
|
1007
|
-
<h2>Details</h2>
|
|
1008
|
-
<img src="/gallery/image2.jpg" alt="Pic 2">
|
|
1009
|
-
<p>Check <a href="/details">this link</a>.</p>
|
|
1010
|
-
</body>
|
|
1011
|
-
</html>
|
|
1012
|
-
"#;
|
|
1013
|
-
|
|
1014
|
-
// Verify visitor callbacks fire via convert_with_visitor
|
|
1015
|
-
let visitor = Rc::new(RefCell::new(ComprehensiveVisitor::default()));
|
|
1016
|
-
let markdown = convert(html, None, Some(visitor.clone()))
|
|
1017
|
-
.expect("convert should work")
|
|
1018
|
-
.content
|
|
1019
|
-
.unwrap_or_default();
|
|
1020
|
-
|
|
1021
|
-
// Verify all visitor callbacks were invoked
|
|
1022
|
-
let borrowed = visitor.borrow();
|
|
1023
|
-
assert!(
|
|
1024
|
-
borrowed.headings >= 2,
|
|
1025
|
-
"Visitor should see at least 2 headings, got {}",
|
|
1026
|
-
borrowed.headings
|
|
1027
|
-
);
|
|
1028
|
-
assert_eq!(
|
|
1029
|
-
borrowed.images, 2,
|
|
1030
|
-
"Visitor should see 2 images, got {}",
|
|
1031
|
-
borrowed.images
|
|
1032
|
-
);
|
|
1033
|
-
assert_eq!(borrowed.links, 2, "Visitor should see 2 links, got {}", borrowed.links);
|
|
1034
|
-
drop(borrowed);
|
|
1035
|
-
|
|
1036
|
-
// Verify markdown was produced
|
|
1037
|
-
assert!(!markdown.is_empty(), "Should produce markdown output");
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
/// Regression test: image visitor returning Custom with metadata extraction used to panic
|
|
1041
|
-
/// with an out-of-bounds slice.
|
|
1042
|
-
///
|
|
1043
|
-
/// When metadata extraction prepends a YAML frontmatter block to `output`, every element's
|
|
1044
|
-
/// saved `element_output_start` is offset by the frontmatter length. If a child visitor
|
|
1045
|
-
/// then returns Custom and truncates the buffer, the parent's saved offset can point
|
|
1046
|
-
/// past `output.len()`.
|
|
1047
|
-
#[test]
|
|
1048
|
-
fn test_image_visitor_with_metadata_does_not_panic() {
|
|
1049
|
-
#[derive(Debug)]
|
|
1050
|
-
struct ImageVisitor;
|
|
1051
|
-
|
|
1052
|
-
impl HtmlVisitor for ImageVisitor {
|
|
1053
|
-
fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
|
|
1054
|
-
VisitResult::Custom("".to_string())
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
let html = r#"<html><head><meta name="description" content="x"></head><body><p><img src="a.png" alt="a"></p></body></html>"#;
|
|
1059
|
-
let options = ConversionOptions {
|
|
1060
|
-
extract_metadata: true,
|
|
1061
|
-
..Default::default()
|
|
1062
|
-
};
|
|
1063
|
-
|
|
1064
|
-
let result = convert(html, Some(options), Some(Rc::new(RefCell::new(ImageVisitor))));
|
|
1065
|
-
assert!(result.is_ok(), "conversion panicked or errored: {:?}", result.err());
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/// Regression test: `visit_element_end` returning Custom/Skip with metadata extraction used
|
|
1069
|
-
/// to produce stale parent offsets and either panic or silently drop subsequent content.
|
|
1070
|
-
#[test]
|
|
1071
|
-
fn test_element_end_replacement_with_metadata_preserves_subsequent_content() {
|
|
1072
|
-
#[derive(Debug)]
|
|
1073
|
-
struct FigureReplacingVisitor;
|
|
1074
|
-
|
|
1075
|
-
impl HtmlVisitor for FigureReplacingVisitor {
|
|
1076
|
-
fn visit_element_end(&mut self, ctx: &NodeContext, _content: &str) -> VisitResult {
|
|
1077
|
-
if ctx.tag_name == "figure" {
|
|
1078
|
-
return VisitResult::Custom("[figure]".to_string());
|
|
1079
|
-
}
|
|
1080
|
-
VisitResult::Continue
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
let html = r#"<html><head><meta name="description" content="x"></head><body><figure><img src="a.png"></figure><p>after</p></body></html>"#;
|
|
1085
|
-
let options = ConversionOptions {
|
|
1086
|
-
extract_metadata: true,
|
|
1087
|
-
..Default::default()
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
let result = convert(html, Some(options), Some(Rc::new(RefCell::new(FigureReplacingVisitor))));
|
|
1091
|
-
assert!(result.is_ok(), "conversion panicked or errored: {:?}", result.err());
|
|
1092
|
-
assert!(
|
|
1093
|
-
result.unwrap().content.unwrap_or_default().contains("after"),
|
|
1094
|
-
"content after replaced element should not be lost"
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
/// Regression test for issue #331: visitor receives mismatched start/end events for
|
|
1099
|
-
/// hyphenated tag names that contain XML-style self-closing children.
|
|
1100
|
-
///
|
|
1101
|
-
/// When `<ac:parameter ac:name="foo" />` appears inside a hyphenated custom element, the
|
|
1102
|
-
/// `repair_with_html5ever` fallback (triggered because the outer tag contains a hyphen) used
|
|
1103
|
-
/// to re-parse with HTML5 semantics. HTML5 does NOT honour XML-style self-closing on unknown
|
|
1104
|
-
/// elements, so `<ac:parameter ... />` was treated as an open tag and subsequent siblings were
|
|
1105
|
-
/// nested inside it. That caused `visit_element_start("ac:parameter")` for "foo" to be
|
|
1106
|
-
/// followed by `visit_element_start("ac:parameter")` for "quux", then both ends in reversed
|
|
1107
|
-
/// order — violating the expected pre-order/post-order pairing.
|
|
1108
|
-
#[test]
|
|
1109
|
-
fn test_issue_331_hyphenated_tags_xml_self_closing_visitor_events() {
|
|
1110
|
-
#[derive(Debug, Default)]
|
|
1111
|
-
struct EventRecorder {
|
|
1112
|
-
events: Vec<String>,
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
impl HtmlVisitor for EventRecorder {
|
|
1116
|
-
fn visit_element_start(&mut self, ctx: &NodeContext) -> VisitResult {
|
|
1117
|
-
self.events.push(format!("start({})", ctx.tag_name));
|
|
1118
|
-
VisitResult::Continue
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
fn visit_element_end(&mut self, ctx: &NodeContext, _output: &str) -> VisitResult {
|
|
1122
|
-
self.events.push(format!("end({})", ctx.tag_name));
|
|
1123
|
-
VisitResult::Continue
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
let html = r#"
|
|
1128
|
-
<structured-macro>
|
|
1129
|
-
<ac:parameter ac:name="foo" />
|
|
1130
|
-
<ac:parameter ac:name="quux">lalaland</ac:parameter>
|
|
1131
|
-
</structured-macro>
|
|
1132
|
-
"#;
|
|
1133
|
-
|
|
1134
|
-
let visitor = Rc::new(RefCell::new(EventRecorder::default()));
|
|
1135
|
-
let result = convert(html, None, Some(visitor.clone()));
|
|
1136
|
-
assert!(result.is_ok(), "conversion should succeed: {:?}", result.err());
|
|
1137
|
-
|
|
1138
|
-
let events = visitor.borrow().events.clone();
|
|
1139
|
-
|
|
1140
|
-
// Find the indices of start/end pairs for the two ac:parameter elements.
|
|
1141
|
-
// With correct XML self-closing handling:
|
|
1142
|
-
// start(ac:parameter)[foo] → end(ac:parameter)[foo] → start(ac:parameter)[quux] → end(ac:parameter)[quux]
|
|
1143
|
-
// With the bug (html5ever treats `/>` as open tag):
|
|
1144
|
-
// start(ac:parameter)[foo] → start(ac:parameter)[quux] → end(ac:parameter)[quux] → end(ac:parameter)[foo]
|
|
1145
|
-
|
|
1146
|
-
// Collect positions of start/end events for ac:parameter
|
|
1147
|
-
let ac_param_starts: Vec<usize> = events
|
|
1148
|
-
.iter()
|
|
1149
|
-
.enumerate()
|
|
1150
|
-
.filter(|(_, e)| e.starts_with("start(ac:parameter)"))
|
|
1151
|
-
.map(|(i, _)| i)
|
|
1152
|
-
.collect();
|
|
1153
|
-
let ac_param_ends: Vec<usize> = events
|
|
1154
|
-
.iter()
|
|
1155
|
-
.enumerate()
|
|
1156
|
-
.filter(|(_, e)| e.starts_with("end(ac:parameter)"))
|
|
1157
|
-
.map(|(i, _)| i)
|
|
1158
|
-
.collect();
|
|
1159
|
-
|
|
1160
|
-
assert_eq!(
|
|
1161
|
-
ac_param_starts.len(),
|
|
1162
|
-
2,
|
|
1163
|
-
"expected exactly 2 ac:parameter start events, got: {events:?}"
|
|
1164
|
-
);
|
|
1165
|
-
assert_eq!(
|
|
1166
|
-
ac_param_ends.len(),
|
|
1167
|
-
2,
|
|
1168
|
-
"expected exactly 2 ac:parameter end events, got: {events:?}"
|
|
1169
|
-
);
|
|
1170
|
-
|
|
1171
|
-
// Each start must come before the corresponding end: start[0] < end[0] < start[1] < end[1]
|
|
1172
|
-
assert!(
|
|
1173
|
-
ac_param_starts[0] < ac_param_ends[0],
|
|
1174
|
-
"first ac:parameter: start must precede end (got start@{}, end@{}); events: {events:?}",
|
|
1175
|
-
ac_param_starts[0],
|
|
1176
|
-
ac_param_ends[0],
|
|
1177
|
-
);
|
|
1178
|
-
assert!(
|
|
1179
|
-
ac_param_ends[0] < ac_param_starts[1],
|
|
1180
|
-
"first ac:parameter end must precede second ac:parameter start (got end@{}, start@{}); events: {events:?}",
|
|
1181
|
-
ac_param_ends[0],
|
|
1182
|
-
ac_param_starts[1],
|
|
1183
|
-
);
|
|
1184
|
-
assert!(
|
|
1185
|
-
ac_param_starts[1] < ac_param_ends[1],
|
|
1186
|
-
"second ac:parameter: start must precede end (got start@{}, end@{}); events: {events:?}",
|
|
1187
|
-
ac_param_starts[1],
|
|
1188
|
-
ac_param_ends[1],
|
|
1189
|
-
);
|
|
1190
|
-
}
|