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.
Files changed (188) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +347 -0
  4. data/Steepfile +10 -2
  5. data/ext/html_to_markdown_rb/Cargo.toml +3 -2
  6. data/ext/html_to_markdown_rb/extconf.rb +5 -5
  7. data/ext/html_to_markdown_rb/native/Cargo.lock +962 -0
  8. data/ext/html_to_markdown_rb/native/Cargo.toml +6 -11
  9. data/ext/html_to_markdown_rb/native/extconf.rb +14 -0
  10. data/ext/html_to_markdown_rb/src/lib.rs +1715 -646
  11. data/lib/html_to_markdown/native.rb +913 -37
  12. data/lib/html_to_markdown/version.rb +3 -3
  13. data/lib/html_to_markdown.rb +9 -4
  14. data/lib/html_to_markdown_rb.so +0 -0
  15. data/sig/types.rbs +59 -292
  16. metadata +32 -179
  17. data/ext/html_to_markdown_rb/Makefile +0 -592
  18. data/lib/bin/html-to-markdown +0 -0
  19. data/vendor/Cargo.toml +0 -33
  20. data/vendor/html-to-markdown-rs/Cargo.toml +0 -54
  21. data/vendor/html-to-markdown-rs/README.md +0 -278
  22. data/vendor/html-to-markdown-rs/examples/basic.rs +0 -24
  23. data/vendor/html-to-markdown-rs/examples/table.rs +0 -25
  24. data/vendor/html-to-markdown-rs/examples/test_deser.rs +0 -12
  25. data/vendor/html-to-markdown-rs/examples/test_escape.rs +0 -58
  26. data/vendor/html-to-markdown-rs/examples/test_inline_formatting.rs +0 -113
  27. data/vendor/html-to-markdown-rs/examples/test_lists.rs +0 -39
  28. data/vendor/html-to-markdown-rs/examples/test_semantic_tags.rs +0 -89
  29. data/vendor/html-to-markdown-rs/examples/test_tables.rs +0 -100
  30. data/vendor/html-to-markdown-rs/examples/test_task_lists.rs +0 -61
  31. data/vendor/html-to-markdown-rs/examples/test_whitespace.rs +0 -34
  32. data/vendor/html-to-markdown-rs/src/convert_api.rs +0 -349
  33. data/vendor/html-to-markdown-rs/src/converter/block/blockquote.rs +0 -178
  34. data/vendor/html-to-markdown-rs/src/converter/block/container.rs +0 -114
  35. data/vendor/html-to-markdown-rs/src/converter/block/div.rs +0 -149
  36. data/vendor/html-to-markdown-rs/src/converter/block/heading.rs +0 -428
  37. data/vendor/html-to-markdown-rs/src/converter/block/horizontal_rule.rs +0 -103
  38. data/vendor/html-to-markdown-rs/src/converter/block/line_break.rs +0 -89
  39. data/vendor/html-to-markdown-rs/src/converter/block/mod.rs +0 -10
  40. data/vendor/html-to-markdown-rs/src/converter/block/paragraph.rs +0 -140
  41. data/vendor/html-to-markdown-rs/src/converter/block/preformatted.rs +0 -298
  42. data/vendor/html-to-markdown-rs/src/converter/block/table/builder.rs +0 -453
  43. data/vendor/html-to-markdown-rs/src/converter/block/table/caption.rs +0 -44
  44. data/vendor/html-to-markdown-rs/src/converter/block/table/cell.rs +0 -276
  45. data/vendor/html-to-markdown-rs/src/converter/block/table/cells.rs +0 -336
  46. data/vendor/html-to-markdown-rs/src/converter/block/table/layout.rs +0 -58
  47. data/vendor/html-to-markdown-rs/src/converter/block/table/mod.rs +0 -266
  48. data/vendor/html-to-markdown-rs/src/converter/block/table/scanner.rs +0 -146
  49. data/vendor/html-to-markdown-rs/src/converter/block/table/utils.rs +0 -34
  50. data/vendor/html-to-markdown-rs/src/converter/block/unknown.rs +0 -138
  51. data/vendor/html-to-markdown-rs/src/converter/context.rs +0 -208
  52. data/vendor/html-to-markdown-rs/src/converter/dom_context.rs +0 -337
  53. data/vendor/html-to-markdown-rs/src/converter/form/elements.rs +0 -770
  54. data/vendor/html-to-markdown-rs/src/converter/form/mod.rs +0 -82
  55. data/vendor/html-to-markdown-rs/src/converter/format/djot.rs +0 -64
  56. data/vendor/html-to-markdown-rs/src/converter/format/markdown.rs +0 -59
  57. data/vendor/html-to-markdown-rs/src/converter/format/mod.rs +0 -43
  58. data/vendor/html-to-markdown-rs/src/converter/handlers/blockquote.rs +0 -173
  59. data/vendor/html-to-markdown-rs/src/converter/handlers/code_block.rs +0 -434
  60. data/vendor/html-to-markdown-rs/src/converter/handlers/graphic.rs +0 -234
  61. data/vendor/html-to-markdown-rs/src/converter/handlers/image.rs +0 -282
  62. data/vendor/html-to-markdown-rs/src/converter/handlers/link.rs +0 -316
  63. data/vendor/html-to-markdown-rs/src/converter/handlers/mod.rs +0 -26
  64. data/vendor/html-to-markdown-rs/src/converter/inline/code.rs +0 -306
  65. data/vendor/html-to-markdown-rs/src/converter/inline/emphasis.rs +0 -345
  66. data/vendor/html-to-markdown-rs/src/converter/inline/link.rs +0 -428
  67. data/vendor/html-to-markdown-rs/src/converter/inline/mod.rs +0 -237
  68. data/vendor/html-to-markdown-rs/src/converter/inline/ruby.rs +0 -337
  69. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/marks.rs +0 -566
  70. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/mod.rs +0 -86
  71. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/typography.rs +0 -558
  72. data/vendor/html-to-markdown-rs/src/converter/list/definition.rs +0 -232
  73. data/vendor/html-to-markdown-rs/src/converter/list/item.rs +0 -332
  74. data/vendor/html-to-markdown-rs/src/converter/list/mod.rs +0 -70
  75. data/vendor/html-to-markdown-rs/src/converter/list/ordered.rs +0 -201
  76. data/vendor/html-to-markdown-rs/src/converter/list/unordered.rs +0 -195
  77. data/vendor/html-to-markdown-rs/src/converter/list/utils.rs +0 -314
  78. data/vendor/html-to-markdown-rs/src/converter/main.rs +0 -710
  79. data/vendor/html-to-markdown-rs/src/converter/main_helpers.rs +0 -452
  80. data/vendor/html-to-markdown-rs/src/converter/media/embedded.rs +0 -393
  81. data/vendor/html-to-markdown-rs/src/converter/media/graphic.rs +0 -4
  82. data/vendor/html-to-markdown-rs/src/converter/media/image.rs +0 -183
  83. data/vendor/html-to-markdown-rs/src/converter/media/mod.rs +0 -87
  84. data/vendor/html-to-markdown-rs/src/converter/media/svg.rs +0 -280
  85. data/vendor/html-to-markdown-rs/src/converter/metadata.rs +0 -220
  86. data/vendor/html-to-markdown-rs/src/converter/mod.rs +0 -156
  87. data/vendor/html-to-markdown-rs/src/converter/plain_text.rs +0 -516
  88. data/vendor/html-to-markdown-rs/src/converter/preprocessing_helpers.rs +0 -201
  89. data/vendor/html-to-markdown-rs/src/converter/reference_collector.rs +0 -69
  90. data/vendor/html-to-markdown-rs/src/converter/semantic/attributes.rs +0 -269
  91. data/vendor/html-to-markdown-rs/src/converter/semantic/definition_list.rs +0 -266
  92. data/vendor/html-to-markdown-rs/src/converter/semantic/figure.rs +0 -391
  93. data/vendor/html-to-markdown-rs/src/converter/semantic/mod.rs +0 -112
  94. data/vendor/html-to-markdown-rs/src/converter/semantic/sectioning.rs +0 -85
  95. data/vendor/html-to-markdown-rs/src/converter/semantic/summary.rs +0 -324
  96. data/vendor/html-to-markdown-rs/src/converter/text/mod.rs +0 -8
  97. data/vendor/html-to-markdown-rs/src/converter/text/processing.rs +0 -56
  98. data/vendor/html-to-markdown-rs/src/converter/text_node.rs +0 -269
  99. data/vendor/html-to-markdown-rs/src/converter/utility/attributes.rs +0 -151
  100. data/vendor/html-to-markdown-rs/src/converter/utility/caching.rs +0 -74
  101. data/vendor/html-to-markdown-rs/src/converter/utility/content.rs +0 -271
  102. data/vendor/html-to-markdown-rs/src/converter/utility/mod.rs +0 -17
  103. data/vendor/html-to-markdown-rs/src/converter/utility/preprocessing.rs +0 -1002
  104. data/vendor/html-to-markdown-rs/src/converter/utility/serialization.rs +0 -126
  105. data/vendor/html-to-markdown-rs/src/converter/utility/siblings.rs +0 -97
  106. data/vendor/html-to-markdown-rs/src/converter/visitor_hooks.rs +0 -189
  107. data/vendor/html-to-markdown-rs/src/error.rs +0 -43
  108. data/vendor/html-to-markdown-rs/src/exports.rs +0 -24
  109. data/vendor/html-to-markdown-rs/src/inline_images.rs +0 -336
  110. data/vendor/html-to-markdown-rs/src/lib.rs +0 -139
  111. data/vendor/html-to-markdown-rs/src/metadata/collector.rs +0 -457
  112. data/vendor/html-to-markdown-rs/src/metadata/config.rs +0 -394
  113. data/vendor/html-to-markdown-rs/src/metadata/extraction.rs +0 -398
  114. data/vendor/html-to-markdown-rs/src/metadata/mod.rs +0 -288
  115. data/vendor/html-to-markdown-rs/src/metadata/types.rs +0 -477
  116. data/vendor/html-to-markdown-rs/src/options/conversion.rs +0 -559
  117. data/vendor/html-to-markdown-rs/src/options/inline_image.rs +0 -111
  118. data/vendor/html-to-markdown-rs/src/options/mod.rs +0 -20
  119. data/vendor/html-to-markdown-rs/src/options/preprocessing.rs +0 -201
  120. data/vendor/html-to-markdown-rs/src/options/validation.rs +0 -416
  121. data/vendor/html-to-markdown-rs/src/prelude.rs +0 -1
  122. data/vendor/html-to-markdown-rs/src/rcdom.rs +0 -487
  123. data/vendor/html-to-markdown-rs/src/text.rs +0 -358
  124. data/vendor/html-to-markdown-rs/src/types/document.rs +0 -191
  125. data/vendor/html-to-markdown-rs/src/types/mod.rs +0 -17
  126. data/vendor/html-to-markdown-rs/src/types/result.rs +0 -54
  127. data/vendor/html-to-markdown-rs/src/types/structure_builder.rs +0 -791
  128. data/vendor/html-to-markdown-rs/src/types/structure_collector.rs +0 -483
  129. data/vendor/html-to-markdown-rs/src/types/tables.rs +0 -52
  130. data/vendor/html-to-markdown-rs/src/types/warnings.rs +0 -33
  131. data/vendor/html-to-markdown-rs/src/validation.rs +0 -158
  132. data/vendor/html-to-markdown-rs/src/visitor/default_impl.rs +0 -63
  133. data/vendor/html-to-markdown-rs/src/visitor/mod.rs +0 -41
  134. data/vendor/html-to-markdown-rs/src/visitor/traits.rs +0 -370
  135. data/vendor/html-to-markdown-rs/src/visitor/types.rs +0 -319
  136. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/callbacks/mod.rs +0 -1
  137. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/content.rs +0 -126
  138. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/mod.rs +0 -27
  139. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/state.rs +0 -110
  140. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/traversal.rs +0 -250
  141. data/vendor/html-to-markdown-rs/src/visitor_helpers.rs +0 -597
  142. data/vendor/html-to-markdown-rs/src/wrapper/sync.rs +0 -413
  143. data/vendor/html-to-markdown-rs/src/wrapper/utils.rs +0 -290
  144. data/vendor/html-to-markdown-rs/src/wrapper.rs +0 -9
  145. data/vendor/html-to-markdown-rs/tests/br_in_inline_test.rs +0 -87
  146. data/vendor/html-to-markdown-rs/tests/commonmark_compliance_test.rs +0 -297
  147. data/vendor/html-to-markdown-rs/tests/djot_output_test.rs +0 -153
  148. data/vendor/html-to-markdown-rs/tests/exclude_selectors_test.rs +0 -132
  149. data/vendor/html-to-markdown-rs/tests/integration_test.rs +0 -631
  150. data/vendor/html-to-markdown-rs/tests/issue_121_regressions.rs +0 -49
  151. data/vendor/html-to-markdown-rs/tests/issue_127_regressions.rs +0 -58
  152. data/vendor/html-to-markdown-rs/tests/issue_128_regressions.rs +0 -17
  153. data/vendor/html-to-markdown-rs/tests/issue_131_regressions.rs +0 -41
  154. data/vendor/html-to-markdown-rs/tests/issue_134_regressions.rs +0 -40
  155. data/vendor/html-to-markdown-rs/tests/issue_139_regressions.rs +0 -26
  156. data/vendor/html-to-markdown-rs/tests/issue_140_regressions.rs +0 -185
  157. data/vendor/html-to-markdown-rs/tests/issue_143_regressions.rs +0 -100
  158. data/vendor/html-to-markdown-rs/tests/issue_145_regressions.rs +0 -133
  159. data/vendor/html-to-markdown-rs/tests/issue_146_regressions.rs +0 -144
  160. data/vendor/html-to-markdown-rs/tests/issue_176_regressions.rs +0 -62
  161. data/vendor/html-to-markdown-rs/tests/issue_190_regressions.rs +0 -128
  162. data/vendor/html-to-markdown-rs/tests/issue_199_regressions.rs +0 -20
  163. data/vendor/html-to-markdown-rs/tests/issue_200_regressions.rs +0 -62
  164. data/vendor/html-to-markdown-rs/tests/issue_212_regressions.rs +0 -68
  165. data/vendor/html-to-markdown-rs/tests/issue_216_217_regressions.rs +0 -87
  166. data/vendor/html-to-markdown-rs/tests/issue_336_regressions.rs +0 -74
  167. data/vendor/html-to-markdown-rs/tests/issue_339_regressions.rs +0 -92
  168. data/vendor/html-to-markdown-rs/tests/issue_347_regressions.rs +0 -154
  169. data/vendor/html-to-markdown-rs/tests/issue_348_visitor_plain.rs +0 -93
  170. data/vendor/html-to-markdown-rs/tests/json_ld_script_extraction.rs +0 -44
  171. data/vendor/html-to-markdown-rs/tests/lists_test.rs +0 -199
  172. data/vendor/html-to-markdown-rs/tests/plain_output_test.rs +0 -273
  173. data/vendor/html-to-markdown-rs/tests/preprocessing_tests.rs +0 -61
  174. data/vendor/html-to-markdown-rs/tests/reference_links_test.rs +0 -169
  175. data/vendor/html-to-markdown-rs/tests/sectioning_elements_test.rs +0 -137
  176. data/vendor/html-to-markdown-rs/tests/skip_images_test.rs +0 -522
  177. data/vendor/html-to-markdown-rs/tests/tables_test.rs +0 -743
  178. data/vendor/html-to-markdown-rs/tests/test_custom_elements.rs +0 -41
  179. data/vendor/html-to-markdown-rs/tests/test_issue_187.rs +0 -204
  180. data/vendor/html-to-markdown-rs/tests/test_issue_218.rs +0 -68
  181. data/vendor/html-to-markdown-rs/tests/test_issue_277.rs +0 -77
  182. data/vendor/html-to-markdown-rs/tests/test_max_depth.rs +0 -82
  183. data/vendor/html-to-markdown-rs/tests/test_nested_simple.rs +0 -45
  184. data/vendor/html-to-markdown-rs/tests/test_script_style_stripping.rs +0 -396
  185. data/vendor/html-to-markdown-rs/tests/test_spa_bisect.rs +0 -34
  186. data/vendor/html-to-markdown-rs/tests/visitor_code_integration_test.rs +0 -121
  187. data/vendor/html-to-markdown-rs/tests/visitor_integration_test.rs +0 -1190
  188. data/vendor/html-to-markdown-rs/tests/xml_tables_test.rs +0 -372
@@ -1,41 +0,0 @@
1
- #![allow(missing_docs)]
2
-
3
- fn convert(
4
- html: &str,
5
- opts: Option<html_to_markdown_rs::ConversionOptions>,
6
- ) -> html_to_markdown_rs::error::Result<String> {
7
- html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())
8
- }
9
-
10
- use std::fs;
11
- use std::path::PathBuf;
12
-
13
- use html_to_markdown_rs::ConversionOptions;
14
-
15
- fn fixture_path(name: &str) -> PathBuf {
16
- [env!("CARGO_MANIFEST_DIR"), "../../test_documents/html/issues", name]
17
- .iter()
18
- .collect()
19
- }
20
-
21
- fn default_options() -> ConversionOptions {
22
- ConversionOptions {
23
- extract_metadata: false,
24
- autolinks: false,
25
- ..Default::default()
26
- }
27
- }
28
-
29
- #[test]
30
- fn test_custom_elements() {
31
- let html = fs::read_to_string(fixture_path("test-with-custom-elements.html")).expect("read html");
32
-
33
- eprintln!("HTML: {html}");
34
-
35
- let result = convert(&html, Some(default_options())).expect("convert html");
36
- eprintln!("Result: {result}");
37
-
38
- assert!(result.contains("Team Appraisal"));
39
- assert!(result.contains("Team"));
40
- assert!(result.contains("Pending"));
41
- }
@@ -1,204 +0,0 @@
1
- //! Test to reproduce issue #187: `visit_div` is not executed
2
- //!
3
- //! This test demonstrates that:
4
- //! 1. There is NO `visit_div`, `visit_script`, `visit_style` method
5
- //! 2. Instead, users should use `visit_element_start` with `tag_name` filtering
6
- //! 3. The documentation incorrectly shows these non-existent methods
7
-
8
- #![cfg(feature = "visitor")]
9
-
10
- use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult, VisitorHandle};
11
- use html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};
12
- use std::cell::RefCell;
13
- use std::rc::Rc;
14
-
15
- fn convert(
16
- html: &str,
17
- options: Option<ConversionOptions>,
18
- visitor: Option<VisitorHandle>,
19
- ) -> Result<ConversionResult, ConversionError> {
20
- let mut opts = options.unwrap_or_default();
21
- if visitor.is_some() {
22
- opts.visitor = visitor;
23
- }
24
- html_to_markdown_rs::convert(html, Some(opts))
25
- }
26
-
27
- /// This is what the documentation SHOWS (but doesn't work)
28
- /// The methods `visit_div`, `visit_script`, `visit_style` DO NOT EXIST
29
- #[allow(dead_code)]
30
- #[derive(Debug, Default)]
31
- struct DocumentedButBrokenVisitor {
32
- skipped_elements: Vec<(String, String)>,
33
- }
34
-
35
- // NOTE: These methods will NEVER be called because they don't exist in HtmlVisitor trait!
36
- // impl DocumentedButBrokenVisitor {
37
- // fn visit_div(&mut self, ctx: &NodeContext, _content: &str) -> VisitResult {
38
- // // This will NEVER be called!
39
- // let classes = ctx.attributes.get("class").map(|s| s.as_str()).unwrap_or("");
40
- // if classes.contains("ad") || classes.contains("advertisement") {
41
- // self.skipped_elements.push(("div".to_string(), classes.to_string()));
42
- // return VisitResult::Skip;
43
- // }
44
- // VisitResult::Continue
45
- // }
46
- //
47
- // fn visit_script(&mut self, ctx: &NodeContext) -> VisitResult {
48
- // // This will NEVER be called!
49
- // self.skipped_elements.push(("script".to_string(), "".to_string()));
50
- // VisitResult::Skip
51
- // }
52
- // }
53
-
54
- /// This is the CORRECT way to filter divs, scripts, and styles
55
- #[derive(Debug, Default)]
56
- struct ContentFilter {
57
- skipped_elements: Vec<(String, String)>,
58
- }
59
-
60
- impl HtmlVisitor for ContentFilter {
61
- /// Use `visit_element_start` to filter ANY element by tag name
62
- fn visit_element_start(&mut self, ctx: &NodeContext) -> VisitResult {
63
- let tag_name = ctx.tag_name.as_str();
64
-
65
- match tag_name {
66
- "div" => {
67
- // Filter divs with unwanted classes
68
- let classes = ctx.attributes.get("class").map_or("", std::string::String::as_str);
69
- if classes.contains("ad")
70
- || classes.contains("advertisement")
71
- || classes.contains("tracking")
72
- || classes.contains("analytics")
73
- {
74
- self.skipped_elements.push(("div".to_string(), classes.to_string()));
75
- return VisitResult::Skip;
76
- }
77
- }
78
- "script" => {
79
- // Always remove script tags
80
- self.skipped_elements.push(("script".to_string(), String::new()));
81
- return VisitResult::Skip;
82
- }
83
- "style" => {
84
- // Always remove style tags
85
- self.skipped_elements.push(("style".to_string(), String::new()));
86
- return VisitResult::Skip;
87
- }
88
- _ => {}
89
- }
90
-
91
- VisitResult::Continue
92
- }
93
-
94
- /// Still use specific methods for links and images
95
- fn visit_image(&mut self, ctx: &NodeContext, src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
96
- // Remove tracking pixels (1x1 images)
97
- let width = ctx.attributes.get("width").map_or("", std::string::String::as_str);
98
- let height = ctx.attributes.get("height").map_or("", std::string::String::as_str);
99
-
100
- if width == "1" && height == "1" {
101
- self.skipped_elements
102
- .push(("img".to_string(), format!("tracking pixel: {src}")));
103
- return VisitResult::Skip;
104
- }
105
-
106
- // Skip images with "tracking" or "analytics" in the URL
107
- if src.to_lowercase().contains("tracking") || src.to_lowercase().contains("analytics") {
108
- self.skipped_elements
109
- .push(("img".to_string(), format!("tracking URL: {src}")));
110
- return VisitResult::Skip;
111
- }
112
-
113
- VisitResult::Continue
114
- }
115
-
116
- fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {
117
- // Remove links with utm_* tracking parameters
118
- if href.to_lowercase().contains("utm_") {
119
- // Strip tracking params but keep the link
120
- if let Some(base_url) = href.split('?').next() {
121
- return VisitResult::Custom(format!("[{text}]({base_url})"));
122
- }
123
- }
124
-
125
- VisitResult::Continue
126
- }
127
- }
128
-
129
- #[test]
130
- fn test_issue_187_content_filter() {
131
- let html = r#"
132
- <article>
133
- <h1>Blog Post Title</h1>
134
- <p>This is the main content of the article.</p>
135
-
136
- <div class="ad advertisement">
137
- <p>This is an advertisement block that should be removed.</p>
138
- </div>
139
-
140
- <p>More content here.</p>
141
-
142
- <img src="https://tracking.example.com/pixel.gif" width="1" height="1" alt="">
143
-
144
- <div class="content">
145
- <p>Legitimate content in a div.</p>
146
- <img src="https://cdn.example.com/image.jpg" alt="Article image" width="800">
147
- </div>
148
-
149
- <script>
150
- console.log("This script should be removed");
151
- </script>
152
-
153
- <p>Read more on <a href="https://example.com/article?utm_source=newsletter&utm_medium=email">our website</a>.</p>
154
-
155
- <div class="tracking analytics">
156
- <img src="https://analytics.example.com/track.png" alt="">
157
- </div>
158
- </article>
159
- "#;
160
-
161
- let visitor = Rc::new(RefCell::new(ContentFilter::default()));
162
- let result = convert(html, None, Some(visitor.clone()))
163
- .unwrap()
164
- .content
165
- .unwrap_or_default();
166
-
167
- println!("Converted Markdown:\n{result}");
168
- println!("\nSkipped Elements:");
169
- for (tag, info) in &visitor.borrow().skipped_elements {
170
- println!("- {tag}: {info}");
171
- }
172
-
173
- // Verify that unwanted content was filtered out
174
- assert!(
175
- !result.contains("advertisement block that should be removed"),
176
- "Ad div should be removed"
177
- );
178
- assert!(!result.contains("console.log"), "Script tag should be removed");
179
-
180
- // Verify that legitimate content remains
181
- assert!(result.contains("Blog Post Title"), "Heading should be preserved");
182
- assert!(result.contains("main content"), "Main content should be preserved");
183
- assert!(result.contains("Legitimate content"), "Content div should be preserved");
184
- assert!(result.contains("Article image"), "Legitimate image should be preserved");
185
-
186
- // Verify tracking parameters were stripped from link
187
- assert!(
188
- result.contains("[our website](https://example.com/article)"),
189
- "Link tracking params should be stripped"
190
- );
191
-
192
- // Verify skipped elements were tracked
193
- let borrowed = visitor.borrow();
194
- assert!(
195
- borrowed.skipped_elements.iter().any(|(tag, _)| tag == "div"),
196
- "Should have skipped ad divs"
197
- );
198
- // Note: script and style tags are stripped during preprocessing before the visitor sees them,
199
- // so they won't appear in skipped_elements. Only the visitor can control div, img, etc.
200
- assert!(
201
- borrowed.skipped_elements.iter().any(|(tag, _)| tag == "img"),
202
- "Should have skipped tracking pixel"
203
- );
204
- }
@@ -1,68 +0,0 @@
1
- //! Test to reproduce issue #218: Rust panic with Cyrillic HTML
2
- //!
3
- //! When converting HTML containing multi-byte UTF-8 characters (e.g., Cyrillic)
4
- //! with tabs between block elements and any visitor, a panic occurs:
5
- //! "byte index N is not a char boundary"
6
-
7
- #![cfg(feature = "visitor")]
8
-
9
- use std::cell::RefCell;
10
- use std::rc::Rc;
11
-
12
- use html_to_markdown_rs::visitor::{HtmlVisitor, VisitorHandle};
13
- use html_to_markdown_rs::{ConversionError, ConversionOptions, ConversionResult};
14
-
15
- fn convert(
16
- html: &str,
17
- options: Option<ConversionOptions>,
18
- visitor: Option<VisitorHandle>,
19
- ) -> Result<ConversionResult, ConversionError> {
20
- let mut opts = options.unwrap_or_default();
21
- if visitor.is_some() {
22
- opts.visitor = visitor;
23
- }
24
- html_to_markdown_rs::convert(html, Some(opts))
25
- }
26
-
27
- /// Empty visitor — does nothing, just uses default implementations.
28
- #[derive(Debug, Default)]
29
- struct EmptyVisitor;
30
-
31
- impl HtmlVisitor for EmptyVisitor {}
32
-
33
- fn make_visitor() -> Rc<RefCell<dyn HtmlVisitor>> {
34
- Rc::new(RefCell::new(EmptyVisitor))
35
- }
36
-
37
- #[test]
38
- fn test_cyrillic_with_tabs_between_divs_and_visitor() {
39
- // Exact reproduction from the issue
40
- let html = "<div><span>А</span></div>\t\t\t<div><span>По";
41
- let result = convert(html, None, Some(make_visitor()));
42
- assert!(result.is_ok(), "Should not panic: {result:?}");
43
- }
44
-
45
- #[test]
46
- fn test_multibyte_utf8_with_tabs_and_visitor() {
47
- let cases = [
48
- "<div><span>日本語</span></div>\t\t\t<div><span>テスト",
49
- "<div><span>한국어</span></div>\t\t\t<div><span>테스트",
50
- "<div><span>Привет</span></div>\t\t\t<div><span>Мир",
51
- "<div><span>🎉</span></div>\t\t\t<div><span>🚀",
52
- ];
53
-
54
- for html in &cases {
55
- let result = convert(html, None, Some(make_visitor()));
56
- assert!(result.is_ok(), "Should not panic for: {html}\nError: {result:?}");
57
- }
58
- }
59
-
60
- #[test]
61
- fn test_cyrillic_with_varying_tab_counts_and_visitor() {
62
- for n in 1..=5 {
63
- let tabs = "\t".repeat(n);
64
- let html = format!("<div><span>А</span></div>{tabs}<div><span>По");
65
- let result = convert(&html, None, Some(make_visitor()));
66
- assert!(result.is_ok(), "Should not panic with {n} tabs: {result:?}");
67
- }
68
- }
@@ -1,77 +0,0 @@
1
- #![allow(missing_docs)]
2
-
3
- //! Regression test for issue #277: silent truncation on large HTML inputs.
4
- //!
5
- //! The bug was caused by `repair_with_html5ever` re-introducing `<script>` elements
6
- //! that had already been stripped, and `preprocess_html` failing to find the closing
7
- //! tag when script content contained unbalanced literal `<script>` strings.
8
-
9
- fn convert(html: &str) -> String {
10
- html_to_markdown_rs::convert(html, None)
11
- .expect("conversion should not fail")
12
- .content
13
- .unwrap_or_default()
14
- }
15
-
16
- /// When custom elements trigger html5ever repair, scripts must be re-stripped.
17
- /// Without the fix, content after a script with unbalanced `<script>` literals
18
- /// would be silently truncated.
19
- #[test]
20
- fn test_no_truncation_after_repair_with_scripts() {
21
- // Custom element triggers repair_with_html5ever
22
- // Script content has an unbalanced literal `<script>` that confuses depth tracking
23
- let html = r"<html>
24
- <head>
25
- <script>
26
- var example = '<script>';
27
- console.log(example);
28
- </script>
29
- </head>
30
- <body>
31
- <custom-widget>widget</custom-widget>
32
- <p>Content before</p>
33
- <p>Content after scripts that must not be truncated</p>
34
- <p>Final paragraph</p>
35
- </body>
36
- </html>";
37
-
38
- let result = convert(html);
39
- assert!(
40
- result.contains("Content before"),
41
- "Should contain content before script region"
42
- );
43
- assert!(
44
- result.contains("Content after scripts that must not be truncated"),
45
- "Content after scripts should NOT be silently truncated. Got:\n{result}"
46
- );
47
- assert!(
48
- result.contains("Final paragraph"),
49
- "Final content should be present. Got:\n{result}"
50
- );
51
- }
52
-
53
- /// Ensure `preprocess_html` doesn't truncate the rest of the document when
54
- /// `find_closing_tag` returns None (unmatched script opening).
55
- #[test]
56
- fn test_preprocess_unmatched_script_preserves_remaining_content() {
57
- // Even without custom elements, preprocess_html's unwrap_or fallback
58
- // should not consume the entire rest of the document.
59
- let html = r"<html><body>
60
- <p>Before</p>
61
- <script>var x = '<script>'; var y = '<script>';</script>
62
- <p>After first script</p>
63
- <script>var z = 1;</script>
64
- <p>After second script</p>
65
- </body></html>";
66
-
67
- let result = convert(html);
68
- assert!(result.contains("Before"), "Content before scripts should be present");
69
- assert!(
70
- result.contains("After first script"),
71
- "Content after first script should be present. Got:\n{result}"
72
- );
73
- assert!(
74
- result.contains("After second script"),
75
- "Content after second script should be present. Got:\n{result}"
76
- );
77
- }
@@ -1,82 +0,0 @@
1
- #![allow(missing_docs)]
2
-
3
- //! Tests for the `max_depth` recursion-safety option.
4
-
5
- use html_to_markdown_rs::ConversionOptions;
6
-
7
- fn convert_with_options(html: &str, options: ConversionOptions) -> String {
8
- html_to_markdown_rs::convert(html, Some(options))
9
- .expect("conversion should not fail")
10
- .content
11
- .unwrap_or_default()
12
- }
13
-
14
- /// With the default `max_depth: None`, deeply nested content should be fully converted.
15
- #[test]
16
- fn test_max_depth_none_converts_deeply_nested() {
17
- // Build 100 levels of nesting around a leaf text node.
18
- let mut html = String::from("<p>deep</p>");
19
- for _ in 0..100 {
20
- html = format!("<div>{html}</div>");
21
- }
22
-
23
- let options = ConversionOptions {
24
- extract_metadata: false,
25
- max_depth: None,
26
- ..Default::default()
27
- };
28
-
29
- let result = convert_with_options(&html, options);
30
- assert!(
31
- result.contains("deep"),
32
- "Deeply nested text should be present when max_depth is None. Got:\n{result}"
33
- );
34
- }
35
-
36
- /// With `max_depth: Some(2)`, block elements at depth 2 are not visited, so
37
- /// their text content is excluded from the output.
38
- #[test]
39
- fn test_max_depth_truncates_at_limit() {
40
- // Depth counting (each handler passes depth+1 to its children):
41
- // depth 0: outer <div> — visited
42
- // depth 1: <p> — visited, paragraph handler passes depth+1 to children
43
- // depth 2: "shallow" — visited (2 < 3), appears in output
44
- // depth 1: inner <div> — visited, div handler passes depth+1 to children
45
- // depth 2: <p> — visited, paragraph handler passes depth+1 to children
46
- // depth 3: "deep" — skipped (3 >= 3), absent from output
47
- let html = "<div><p>shallow</p><div><p>deep</p></div></div>";
48
-
49
- let options = ConversionOptions {
50
- extract_metadata: false,
51
- max_depth: Some(3),
52
- ..Default::default()
53
- };
54
-
55
- let result = convert_with_options(html, options);
56
- assert!(
57
- result.contains("shallow"),
58
- "Content at depth < max_depth should be present. Got:\n{result}"
59
- );
60
- assert!(
61
- !result.contains("deep"),
62
- "Content at depth >= max_depth should be absent. Got:\n{result}"
63
- );
64
- }
65
-
66
- /// With `max_depth: Some(0)`, no nodes are processed and the output is empty or whitespace only.
67
- #[test]
68
- fn test_max_depth_zero_produces_empty() {
69
- let html = "<p>hello</p>";
70
-
71
- let options = ConversionOptions {
72
- extract_metadata: false,
73
- max_depth: Some(0),
74
- ..Default::default()
75
- };
76
-
77
- let result = convert_with_options(html, options);
78
- assert!(
79
- result.trim().is_empty(),
80
- "max_depth: Some(0) should produce no output. Got:\n{result}"
81
- );
82
- }
@@ -1,45 +0,0 @@
1
- #![allow(missing_docs)]
2
-
3
- fn convert(
4
- html: &str,
5
- opts: Option<html_to_markdown_rs::ConversionOptions>,
6
- ) -> html_to_markdown_rs::error::Result<String> {
7
- html_to_markdown_rs::convert(html, opts).map(|r| r.content.unwrap_or_default())
8
- }
9
-
10
- use std::fs;
11
- use std::path::PathBuf;
12
-
13
- use html_to_markdown_rs::ConversionOptions;
14
-
15
- fn fixture_path(name: &str) -> PathBuf {
16
- [env!("CARGO_MANIFEST_DIR"), "../../test_documents/html/issues", name]
17
- .iter()
18
- .collect()
19
- }
20
-
21
- fn default_options() -> ConversionOptions {
22
- ConversionOptions {
23
- extract_metadata: false,
24
- autolinks: false,
25
- ..Default::default()
26
- }
27
- }
28
-
29
- fn normalize_newlines(input: &str) -> String {
30
- input.replace("\r\n", "\n").replace('\r', "\n")
31
- }
32
-
33
- #[test]
34
- fn test_nested_simple() {
35
- let html = fs::read_to_string(fixture_path("test-nested-simple.html")).expect("read html");
36
- let expected = fs::read_to_string(fixture_path("test-nested-simple.md")).expect("read markdown");
37
-
38
- eprintln!("HTML: {html}");
39
- eprintln!("Expected: {expected}");
40
-
41
- let result = convert(&html, Some(default_options())).expect("convert html");
42
- eprintln!("Result: {result}");
43
-
44
- assert_eq!(normalize_newlines(&result), normalize_newlines(&expected));
45
- }