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,158 +0,0 @@
1
- //! Input validation module for HTML to Markdown conversion.
2
- //!
3
- //! Provides validation functions to detect and reject binary data,
4
- //! corrupted input, and other non-text content.
5
-
6
- use crate::error::{ConversionError, Result};
7
-
8
- const BINARY_SCAN_LIMIT: usize = 8192;
9
- const BINARY_CONTROL_RATIO: f64 = 0.3;
10
- const BINARY_UTF16_NULL_RATIO: f64 = 0.2;
11
- const BINARY_NUL_RATIO: f64 = 0.01;
12
- const BINARY_NUL_MAX: usize = 8;
13
-
14
- const BINARY_MAGIC_PREFIXES: &[(&[u8], &str)] = &[
15
- (b"\x1F\x8B", "gzip-compressed data"),
16
- (b"\x28\xB5\x2F\xFD", "zstd-compressed data"),
17
- (b"PK\x03\x04", "zip archive"),
18
- (b"PK\x05\x06", "zip archive"),
19
- (b"PK\x07\x08", "zip archive"),
20
- (b"%PDF-", "PDF data"),
21
- ];
22
-
23
- #[derive(Debug, Clone, Copy, PartialEq, Eq)]
24
- pub enum Utf16Encoding {
25
- BomLe,
26
- BomBe,
27
- NoBomLe,
28
- NoBomBe,
29
- }
30
-
31
- /// Validate HTML input and reject binary/corrupted data.
32
- ///
33
- /// # Errors
34
- ///
35
- /// Returns `ConversionError::InvalidInput` if the input is detected as binary,
36
- /// compressed, or contains too many control characters.
37
- #[allow(clippy::cast_precision_loss)]
38
- pub fn validate_input(html: &str) -> Result<()> {
39
- let bytes = html.as_bytes();
40
- if bytes.is_empty() {
41
- return Ok(());
42
- }
43
-
44
- if let Some(label) = detect_binary_magic(bytes) {
45
- return Err(ConversionError::InvalidInput(format!(
46
- "binary data detected ({label}); decode/decompress to UTF-8 HTML first"
47
- )));
48
- }
49
-
50
- let sample_len = bytes.len().min(BINARY_SCAN_LIMIT);
51
- let mut control_count = 0usize;
52
- let mut nul_count = 0usize;
53
-
54
- for &byte in &bytes[..sample_len] {
55
- if byte == 0 {
56
- nul_count += 1;
57
- }
58
- let is_control = (byte < 0x09) || (0x0E..0x20).contains(&byte);
59
- if is_control {
60
- control_count += 1;
61
- }
62
- }
63
-
64
- if nul_count > 0 {
65
- if let Some(encoding) = detect_utf16_encoding(bytes) {
66
- let label = utf16_label(encoding);
67
- return Err(ConversionError::InvalidInput(format!(
68
- "binary data detected ({label}); decode to UTF-8 HTML first"
69
- )));
70
- }
71
- let nul_ratio = nul_count as f64 / sample_len as f64;
72
- if nul_ratio > BINARY_NUL_RATIO && nul_count > BINARY_NUL_MAX {
73
- return Err(ConversionError::InvalidInput(
74
- "binary data detected (excess NUL bytes)".to_string(),
75
- ));
76
- }
77
- }
78
-
79
- if sample_len >= 32 {
80
- let control_ratio = control_count as f64 / sample_len as f64;
81
- if control_ratio > BINARY_CONTROL_RATIO {
82
- return Err(ConversionError::InvalidInput(
83
- "binary data detected (excess control bytes)".to_string(),
84
- ));
85
- }
86
- }
87
-
88
- Ok(())
89
- }
90
-
91
- fn detect_binary_magic(bytes: &[u8]) -> Option<&'static str> {
92
- for (prefix, label) in BINARY_MAGIC_PREFIXES {
93
- if bytes.starts_with(prefix) {
94
- return Some(*label);
95
- }
96
- }
97
- None
98
- }
99
-
100
- #[allow(clippy::cast_precision_loss)]
101
- pub fn detect_utf16_encoding(bytes: &[u8]) -> Option<Utf16Encoding> {
102
- if bytes.len() >= 2 {
103
- if bytes.starts_with(b"\xFF\xFE") {
104
- return Some(Utf16Encoding::BomLe);
105
- }
106
- if bytes.starts_with(b"\xFE\xFF") {
107
- return Some(Utf16Encoding::BomBe);
108
- }
109
- }
110
-
111
- let sample_len = bytes.len().min(BINARY_SCAN_LIMIT);
112
- if sample_len < 4 {
113
- return None;
114
- }
115
-
116
- let mut nul_count = 0usize;
117
- let mut even_nul_count = 0usize;
118
- let mut odd_nul_count = 0usize;
119
-
120
- for (idx, &byte) in bytes[..sample_len].iter().enumerate() {
121
- if byte == 0 {
122
- nul_count += 1;
123
- if idx % 2 == 0 {
124
- even_nul_count += 1;
125
- } else {
126
- odd_nul_count += 1;
127
- }
128
- }
129
- }
130
-
131
- if nul_count < 2 {
132
- return None;
133
- }
134
-
135
- let nul_ratio = nul_count as f64 / sample_len as f64;
136
- if nul_ratio < BINARY_UTF16_NULL_RATIO {
137
- return None;
138
- }
139
-
140
- let dominant_ratio = (even_nul_count.max(odd_nul_count) as f64) / nul_count as f64;
141
- if dominant_ratio < 0.9 {
142
- return None;
143
- }
144
-
145
- if odd_nul_count >= even_nul_count {
146
- Some(Utf16Encoding::NoBomLe)
147
- } else {
148
- Some(Utf16Encoding::NoBomBe)
149
- }
150
- }
151
-
152
- fn utf16_label(encoding: Utf16Encoding) -> &'static str {
153
- match encoding {
154
- Utf16Encoding::BomLe => "UTF-16LE BOM",
155
- Utf16Encoding::BomBe => "UTF-16BE BOM",
156
- Utf16Encoding::NoBomLe | Utf16Encoding::NoBomBe => "UTF-16 data without BOM",
157
- }
158
- }
@@ -1,63 +0,0 @@
1
- //! Default visitor implementations and utilities.
2
- //!
3
- //! This module provides standard visitor patterns and helpers for common use cases.
4
-
5
- use std::cell::RefCell;
6
- use std::rc::Rc;
7
-
8
- /// Type alias for a visitor handle (Rc-wrapped `RefCell` for interior mutability).
9
- ///
10
- /// This allows visitors to be passed around and shared while still being mutable.
11
- pub type VisitorHandle = Rc<RefCell<dyn super::traits::HtmlVisitor>>;
12
-
13
- #[cfg(test)]
14
- mod tests {
15
- use super::super::traits::HtmlVisitor;
16
- use super::super::types::{NodeContext, NodeType, VisitResult};
17
- use std::collections::BTreeMap;
18
-
19
- #[derive(Debug)]
20
- struct TrackingVisitor {
21
- element_count: usize,
22
- text_count: usize,
23
- }
24
-
25
- impl HtmlVisitor for TrackingVisitor {
26
- fn visit_element_start(&mut self, _ctx: &NodeContext) -> VisitResult {
27
- self.element_count += 1;
28
- VisitResult::Continue
29
- }
30
-
31
- fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
32
- self.text_count += 1;
33
- VisitResult::Continue
34
- }
35
- }
36
-
37
- #[test]
38
- fn test_visitor_handle_creation() {
39
- let visitor = TrackingVisitor {
40
- element_count: 0,
41
- text_count: 0,
42
- };
43
-
44
- let handle = std::rc::Rc::new(std::cell::RefCell::new(visitor));
45
-
46
- {
47
- let mut v = handle.borrow_mut();
48
- let ctx = NodeContext {
49
- node_type: NodeType::Text,
50
- tag_name: "p".to_string(),
51
- attributes: BTreeMap::new(),
52
- depth: 1,
53
- index_in_parent: 0,
54
- parent_tag: Some("div".to_string()),
55
- is_inline: false,
56
- };
57
- v.visit_text(&ctx, "test");
58
- }
59
-
60
- let v = handle.borrow();
61
- assert_eq!(v.text_count, 1);
62
- }
63
- }
@@ -1,41 +0,0 @@
1
- //! Visitor pattern for HTML to Markdown conversion.
2
- //!
3
- //! This module provides a comprehensive visitor trait that allows users to intervene
4
- //! in the HTML→Markdown transformation logic at any point. Visitors can inspect,
5
- //! modify, or replace the default conversion behavior for any HTML element type.
6
- //!
7
- //! # Design Philosophy
8
- //!
9
- //! - **Flexibility over performance**: Visitors prioritize giving users full control
10
- //! - **Zero-cost when unused**: No performance impact if visitor feature is disabled
11
- //! - **Comprehensive coverage**: All 60+ HTML element types have dedicated visitor methods
12
- //! - **Pre/post hooks**: Both element entry and exit points are exposed
13
- //!
14
- //! # Example
15
- //!
16
- //! ```text
17
- //! use html_to_markdown_rs::visitor::{HtmlVisitor, NodeContext, VisitResult};
18
- //!
19
- //! struct CustomVisitor;
20
- //!
21
- //! impl HtmlVisitor for CustomVisitor {
22
- //! fn visit_link(&mut self, ctx: &NodeContext, href: &str, text: &str, title: Option<&str>) -> VisitResult {
23
- //! // Convert all links to plain text with URL in parentheses
24
- //! VisitResult::Custom(format!("{} ({})", text, href))
25
- //! }
26
- //!
27
- //! fn visit_image(&mut self, ctx: &NodeContext, src: &str, alt: &str, title: Option<&str>) -> VisitResult {
28
- //! // Skip all images
29
- //! VisitResult::Skip
30
- //! }
31
- //! }
32
- //! ```
33
-
34
- mod default_impl;
35
- mod traits;
36
- mod types;
37
-
38
- // Re-export all public items from submodules
39
- pub use default_impl::VisitorHandle;
40
- pub use traits::HtmlVisitor;
41
- pub use types::{NodeContext, NodeType, VisitResult};
@@ -1,370 +0,0 @@
1
- //! Visitor traits for HTML to Markdown conversion.
2
- //!
3
- //! This module contains the synchronous visitor trait.
4
-
5
- use super::types::{NodeContext, VisitResult};
6
-
7
- /// Visitor trait for HTML→Markdown conversion.
8
- ///
9
- /// Implement this trait to customize the conversion behavior for any HTML element type.
10
- /// All methods have default implementations that return `VisitResult::Continue`, allowing
11
- /// selective override of only the elements you care about.
12
- ///
13
- /// # Method Naming Convention
14
- ///
15
- /// - `visit_*_start`: Called before entering an element (pre-order traversal)
16
- /// - `visit_*_end`: Called after exiting an element (post-order traversal)
17
- /// - `visit_*`: Called for specific element types (e.g., `visit_link`, `visit_image`)
18
- ///
19
- /// # Execution Order
20
- ///
21
- /// For a typical element like `<div><p>text</p></div>`:
22
- /// 1. `visit_element_start` for `<div>`
23
- /// 2. `visit_element_start` for `<p>`
24
- /// 3. `visit_text` for "text"
25
- /// 4. `visit_element_end` for `<p>`
26
- /// 5. `visit_element_end` for `</div>`
27
- ///
28
- /// # Performance Notes
29
- ///
30
- /// - `visit_text` is the most frequently called method (~100+ times per document)
31
- /// - Return `VisitResult::Continue` quickly for elements you don't need to customize
32
- /// - Avoid heavy computation in visitor methods; consider caching if needed
33
- pub trait HtmlVisitor: std::fmt::Debug {
34
- /// Called before entering any element.
35
- ///
36
- /// This is the first callback invoked for every HTML element, allowing
37
- /// visitors to implement generic element handling before tag-specific logic.
38
- fn visit_element_start(&mut self, _ctx: &NodeContext) -> VisitResult {
39
- VisitResult::Continue
40
- }
41
-
42
- /// Called after exiting any element.
43
- ///
44
- /// Receives the default markdown output that would be generated.
45
- /// Visitors can inspect or replace this output.
46
- fn visit_element_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {
47
- VisitResult::Continue
48
- }
49
-
50
- /// Visit text nodes (most frequent callback - ~100+ per document).
51
- ///
52
- /// # Arguments
53
- /// - `ctx`: Node context (will have `node_type: NodeType::Text`)
54
- /// - `text`: The raw text content (HTML entities already decoded)
55
- fn visit_text(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
56
- VisitResult::Continue
57
- }
58
-
59
- /// Visit anchor links `<a href="...">`.
60
- ///
61
- /// # Arguments
62
- /// - `ctx`: Node context with link element metadata
63
- /// - `href`: The link URL (from `href` attribute)
64
- /// - `text`: The link text content (already converted to markdown)
65
- /// - `title`: Optional title attribute
66
- fn visit_link(&mut self, _ctx: &NodeContext, _href: &str, _text: &str, _title: Option<&str>) -> VisitResult {
67
- VisitResult::Continue
68
- }
69
-
70
- /// Visit images `<img src="...">`.
71
- ///
72
- /// # Arguments
73
- /// - `ctx`: Node context with image element metadata
74
- /// - `src`: The image source URL
75
- /// - `alt`: The alt text
76
- /// - `title`: Optional title attribute
77
- fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
78
- VisitResult::Continue
79
- }
80
-
81
- /// Visit heading elements `<h1>` through `<h6>`.
82
- ///
83
- /// # Arguments
84
- /// - `ctx`: Node context with heading metadata
85
- /// - `level`: Heading level (1-6)
86
- /// - `text`: The heading text content
87
- /// - `id`: Optional id attribute (for anchor links)
88
- fn visit_heading(&mut self, _ctx: &NodeContext, _level: u32, _text: &str, _id: Option<&str>) -> VisitResult {
89
- VisitResult::Continue
90
- }
91
-
92
- /// Visit code blocks `<pre><code>`.
93
- ///
94
- /// # Arguments
95
- /// - `ctx`: Node context
96
- /// - `lang`: Optional language specifier (from class attribute)
97
- /// - `code`: The code content
98
- fn visit_code_block(&mut self, _ctx: &NodeContext, _lang: Option<&str>, _code: &str) -> VisitResult {
99
- VisitResult::Continue
100
- }
101
-
102
- /// Visit inline code `<code>`.
103
- ///
104
- /// # Arguments
105
- /// - `ctx`: Node context
106
- /// - `code`: The code content
107
- fn visit_code_inline(&mut self, _ctx: &NodeContext, _code: &str) -> VisitResult {
108
- VisitResult::Continue
109
- }
110
-
111
- /// Visit list items `<li>`.
112
- ///
113
- /// # Arguments
114
- /// - `ctx`: Node context
115
- /// - `ordered`: Whether this is an ordered list item
116
- /// - `marker`: The list marker (e.g., "-", "1.", "a)")
117
- /// - `text`: The list item content (already converted)
118
- fn visit_list_item(&mut self, _ctx: &NodeContext, _ordered: bool, _marker: &str, _text: &str) -> VisitResult {
119
- VisitResult::Continue
120
- }
121
-
122
- /// Called before processing a list `<ul>` or `<ol>`.
123
- fn visit_list_start(&mut self, _ctx: &NodeContext, _ordered: bool) -> VisitResult {
124
- VisitResult::Continue
125
- }
126
-
127
- /// Called after processing a list `</ul>` or `</ol>`.
128
- fn visit_list_end(&mut self, _ctx: &NodeContext, _ordered: bool, _output: &str) -> VisitResult {
129
- VisitResult::Continue
130
- }
131
-
132
- /// Called before processing a table `<table>`.
133
- fn visit_table_start(&mut self, _ctx: &NodeContext) -> VisitResult {
134
- VisitResult::Continue
135
- }
136
-
137
- /// Visit table rows `<tr>`.
138
- ///
139
- /// # Arguments
140
- /// - `ctx`: Node context
141
- /// - `cells`: Cell contents (already converted to markdown)
142
- /// - `is_header`: Whether this row is in `<thead>`
143
- fn visit_table_row(&mut self, _ctx: &NodeContext, _cells: &[String], _is_header: bool) -> VisitResult {
144
- VisitResult::Continue
145
- }
146
-
147
- /// Called after processing a table `</table>`.
148
- fn visit_table_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {
149
- VisitResult::Continue
150
- }
151
-
152
- /// Visit blockquote elements `<blockquote>`.
153
- ///
154
- /// # Arguments
155
- /// - `ctx`: Node context
156
- /// - `content`: The blockquote content (already converted)
157
- /// - `depth`: Nesting depth (for nested blockquotes)
158
- fn visit_blockquote(&mut self, _ctx: &NodeContext, _content: &str, _depth: usize) -> VisitResult {
159
- VisitResult::Continue
160
- }
161
-
162
- /// Visit strong/bold elements `<strong>`, `<b>`.
163
- fn visit_strong(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
164
- VisitResult::Continue
165
- }
166
-
167
- /// Visit emphasis/italic elements `<em>`, `<i>`.
168
- fn visit_emphasis(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
169
- VisitResult::Continue
170
- }
171
-
172
- /// Visit strikethrough elements `<s>`, `<del>`, `<strike>`.
173
- fn visit_strikethrough(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
174
- VisitResult::Continue
175
- }
176
-
177
- /// Visit underline elements `<u>`, `<ins>`.
178
- fn visit_underline(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
179
- VisitResult::Continue
180
- }
181
-
182
- /// Visit subscript elements `<sub>`.
183
- fn visit_subscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
184
- VisitResult::Continue
185
- }
186
-
187
- /// Visit superscript elements `<sup>`.
188
- fn visit_superscript(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
189
- VisitResult::Continue
190
- }
191
-
192
- /// Visit mark/highlight elements `<mark>`.
193
- fn visit_mark(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
194
- VisitResult::Continue
195
- }
196
-
197
- /// Visit line break elements `<br>`.
198
- fn visit_line_break(&mut self, _ctx: &NodeContext) -> VisitResult {
199
- VisitResult::Continue
200
- }
201
-
202
- /// Visit horizontal rule elements `<hr>`.
203
- fn visit_horizontal_rule(&mut self, _ctx: &NodeContext) -> VisitResult {
204
- VisitResult::Continue
205
- }
206
-
207
- /// Visit custom elements (web components) or unknown tags.
208
- ///
209
- /// # Arguments
210
- /// - `ctx`: Node context
211
- /// - `tag_name`: The custom element's tag name
212
- /// - `html`: The raw HTML of this element
213
- fn visit_custom_element(&mut self, _ctx: &NodeContext, _tag_name: &str, _html: &str) -> VisitResult {
214
- VisitResult::Continue
215
- }
216
-
217
- /// Visit definition list `<dl>`.
218
- fn visit_definition_list_start(&mut self, _ctx: &NodeContext) -> VisitResult {
219
- VisitResult::Continue
220
- }
221
-
222
- /// Visit definition term `<dt>`.
223
- fn visit_definition_term(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
224
- VisitResult::Continue
225
- }
226
-
227
- /// Visit definition description `<dd>`.
228
- fn visit_definition_description(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
229
- VisitResult::Continue
230
- }
231
-
232
- /// Called after processing a definition list `</dl>`.
233
- fn visit_definition_list_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {
234
- VisitResult::Continue
235
- }
236
-
237
- /// Visit form elements `<form>`.
238
- fn visit_form(&mut self, _ctx: &NodeContext, _action: Option<&str>, _method: Option<&str>) -> VisitResult {
239
- VisitResult::Continue
240
- }
241
-
242
- /// Visit input elements `<input>`.
243
- fn visit_input(
244
- &mut self,
245
- _ctx: &NodeContext,
246
- _input_type: &str,
247
- _name: Option<&str>,
248
- _value: Option<&str>,
249
- ) -> VisitResult {
250
- VisitResult::Continue
251
- }
252
-
253
- /// Visit button elements `<button>`.
254
- fn visit_button(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
255
- VisitResult::Continue
256
- }
257
-
258
- /// Visit audio elements `<audio>`.
259
- fn visit_audio(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {
260
- VisitResult::Continue
261
- }
262
-
263
- /// Visit video elements `<video>`.
264
- fn visit_video(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {
265
- VisitResult::Continue
266
- }
267
-
268
- /// Visit iframe elements `<iframe>`.
269
- fn visit_iframe(&mut self, _ctx: &NodeContext, _src: Option<&str>) -> VisitResult {
270
- VisitResult::Continue
271
- }
272
-
273
- /// Visit details elements `<details>`.
274
- fn visit_details(&mut self, _ctx: &NodeContext, _open: bool) -> VisitResult {
275
- VisitResult::Continue
276
- }
277
-
278
- /// Visit summary elements `<summary>`.
279
- fn visit_summary(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
280
- VisitResult::Continue
281
- }
282
-
283
- /// Visit figure elements `<figure>`.
284
- fn visit_figure_start(&mut self, _ctx: &NodeContext) -> VisitResult {
285
- VisitResult::Continue
286
- }
287
-
288
- /// Visit figcaption elements `<figcaption>`.
289
- fn visit_figcaption(&mut self, _ctx: &NodeContext, _text: &str) -> VisitResult {
290
- VisitResult::Continue
291
- }
292
-
293
- /// Called after processing a figure `</figure>`.
294
- fn visit_figure_end(&mut self, _ctx: &NodeContext, _output: &str) -> VisitResult {
295
- VisitResult::Continue
296
- }
297
- }
298
-
299
- #[cfg(test)]
300
- mod tests {
301
- use super::*;
302
- use std::collections::BTreeMap;
303
-
304
- #[derive(Debug)]
305
- struct NoOpVisitor;
306
-
307
- impl HtmlVisitor for NoOpVisitor {}
308
-
309
- #[test]
310
- fn test_default_visitor_implementation() {
311
- let mut visitor = NoOpVisitor;
312
-
313
- let ctx = NodeContext {
314
- node_type: super::super::types::NodeType::Text,
315
- tag_name: String::new(),
316
- attributes: BTreeMap::new(),
317
- depth: 0,
318
- index_in_parent: 0,
319
- parent_tag: None,
320
- is_inline: true,
321
- };
322
-
323
- matches!(visitor.visit_element_start(&ctx), VisitResult::Continue);
324
- matches!(visitor.visit_element_end(&ctx, "output"), VisitResult::Continue);
325
- matches!(visitor.visit_text(&ctx, "text"), VisitResult::Continue);
326
- matches!(visitor.visit_link(&ctx, "href", "text", None), VisitResult::Continue);
327
- matches!(visitor.visit_image(&ctx, "src", "alt", None), VisitResult::Continue);
328
- matches!(visitor.visit_heading(&ctx, 1, "text", None), VisitResult::Continue);
329
- matches!(visitor.visit_code_block(&ctx, None, "code"), VisitResult::Continue);
330
- matches!(visitor.visit_code_inline(&ctx, "code"), VisitResult::Continue);
331
- }
332
-
333
- #[derive(Debug)]
334
- struct CustomLinkVisitor;
335
-
336
- impl HtmlVisitor for CustomLinkVisitor {
337
- fn visit_link(&mut self, _ctx: &NodeContext, href: &str, text: &str, _title: Option<&str>) -> VisitResult {
338
- VisitResult::Custom(format!("{} ({})", text, href))
339
- }
340
-
341
- fn visit_image(&mut self, _ctx: &NodeContext, _src: &str, _alt: &str, _title: Option<&str>) -> VisitResult {
342
- VisitResult::Skip
343
- }
344
- }
345
-
346
- #[test]
347
- fn test_custom_visitor_implementation() {
348
- let mut visitor = CustomLinkVisitor;
349
-
350
- let ctx = NodeContext {
351
- node_type: super::super::types::NodeType::Link,
352
- tag_name: "a".to_string(),
353
- attributes: BTreeMap::new(),
354
- depth: 1,
355
- index_in_parent: 0,
356
- parent_tag: Some("p".to_string()),
357
- is_inline: true,
358
- };
359
-
360
- let result = visitor.visit_link(&ctx, "https://example.com", "Example", None);
361
- if let VisitResult::Custom(output) = result {
362
- assert_eq!(output, "Example (https://example.com)");
363
- } else {
364
- panic!("Expected Custom result");
365
- }
366
-
367
- let img_result = visitor.visit_image(&ctx, "image.jpg", "Alt text", None);
368
- matches!(img_result, VisitResult::Skip);
369
- }
370
- }