html-to-markdown 3.4.0 → 3.6.0.pre.rc.24

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,597 +0,0 @@
1
- //! Helper functions for visitor pattern integration.
2
- //!
3
- //! This module provides efficient utilities for building visitor contexts,
4
- //! dispatching visitor callbacks, and handling visitor results during the
5
- //! HTML→Markdown conversion process.
6
- //!
7
- //! # Design Goals
8
- //!
9
- //! - **Zero allocation when possible**: Reuse existing data structures
10
- //! - **Minimal overhead**: Inline hot paths, avoid unnecessary clones
11
- //! - **Type safety**: Leverage Rust's type system for correct visitor handling
12
- //! - **Ergonomics**: Reduce boilerplate for common visitor patterns
13
- //!
14
- //! # Usage
15
- //!
16
- //! These helpers are designed to be used within the converter module during
17
- //! the DOM traversal. They bridge the gap between the internal conversion
18
- //! state and the public visitor API.
19
-
20
- use std::cell::RefCell;
21
- use std::collections::BTreeMap;
22
- use std::rc::Rc;
23
-
24
- use crate::error::{ConversionError, Result};
25
- use crate::visitor::{HtmlVisitor, NodeContext, NodeType, VisitResult};
26
-
27
- /// Build a `NodeContext` from current parsing state.
28
- ///
29
- /// Creates a complete `NodeContext` suitable for passing to visitor callbacks.
30
- /// This function collects metadata about the current node from various sources:
31
- /// - Tag name and attributes from the HTML element
32
- /// - Depth and parent information from the DOM tree
33
- /// - Index among siblings for positional awareness
34
- /// - Inline/block classification
35
- ///
36
- /// # Parameters
37
- ///
38
- /// - `node_type`: Coarse-grained classification (Link, Image, Heading, etc.)
39
- /// - `tag_name`: Raw HTML tag name (e.g., "div", "h1", "custom-element")
40
- /// - `attributes`: All HTML attributes as key-value pairs
41
- /// - `depth`: Nesting depth in the DOM tree (0 = root)
42
- /// - `index_in_parent`: Zero-based index among siblings
43
- /// - `parent_tag`: Parent element's tag name (None if root)
44
- /// - `is_inline`: Whether this element is treated as inline vs block
45
- ///
46
- /// # Returns
47
- ///
48
- /// A fully populated `NodeContext` ready for visitor dispatch.
49
- ///
50
- /// # Performance
51
- ///
52
- /// This function performs minimal allocations:
53
- /// - Clones `tag_name` (typically 2-10 bytes)
54
- /// - Clones `parent_tag` if present (typically 2-10 bytes)
55
- /// - Clones the attributes `BTreeMap` (heap allocation if non-empty)
56
- ///
57
- /// For text nodes and simple elements without attributes, allocations are minimal.
58
- ///
59
- /// # Examples
60
- ///
61
- /// ```text
62
- /// let ctx = build_node_context(
63
- /// NodeType::Heading,
64
- /// "h1",
65
- /// &attrs,
66
- /// 1,
67
- /// 0,
68
- /// Some("body"),
69
- /// false,
70
- /// );
71
- /// ```
72
- #[allow(dead_code)]
73
- #[inline]
74
- pub fn build_node_context(
75
- node_type: NodeType,
76
- tag_name: &str,
77
- attributes: &BTreeMap<String, String>,
78
- depth: usize,
79
- index_in_parent: usize,
80
- parent_tag: Option<&str>,
81
- is_inline: bool,
82
- ) -> NodeContext {
83
- NodeContext {
84
- node_type,
85
- tag_name: tag_name.to_string(),
86
- attributes: attributes.clone(),
87
- depth,
88
- index_in_parent,
89
- parent_tag: parent_tag.map(String::from),
90
- is_inline,
91
- }
92
- }
93
-
94
- /// Dispatch a visitor callback and handle the result.
95
- ///
96
- /// This is the core dispatcher for all visitor callbacks. It safely handles the
97
- /// optional visitor, calls the callback function, and translates the `VisitResult`
98
- /// into concrete control flow decisions.
99
- ///
100
- /// # Type Parameters
101
- ///
102
- /// - `F`: Visitor callback function type
103
- ///
104
- /// # Parameters
105
- ///
106
- /// - `visitor`: Optional visitor (wrapped in Rc<`RefCell`<>>)
107
- /// - `callback`: Closure that invokes the appropriate visitor method
108
- ///
109
- /// # Returns
110
- ///
111
- /// - `Ok(Some(String))`: Custom markdown output from `VisitResult::Custom`
112
- /// - `Ok(None)`: Continue with default behavior (`VisitResult::Continue`)
113
- /// - `Err(Error)`: Stop conversion with error (`VisitResult::Error`)
114
- ///
115
- /// The `VisitResult::Skip` and `VisitResult::PreserveHtml` variants are handled
116
- /// by the caller based on context.
117
- ///
118
- /// # Error Handling
119
- ///
120
- /// - If the visitor panics during callback, the panic propagates normally
121
- /// - If the visitor returns `VisitResult::Error`, this is converted to `Error::Visitor`
122
- /// - `RefCell` borrow failures panic (should never happen with correct usage)
123
- ///
124
- /// # Performance
125
- ///
126
- /// - Zero-cost when visitor is None (common case)
127
- /// - Single dynamic dispatch when visitor is present
128
- /// - No allocations except for error messages
129
- ///
130
- /// # Examples
131
- ///
132
- /// ```text
133
- /// let result = dispatch_visitor(
134
- /// &visitor,
135
- /// |v| v.visit_heading(&ctx, level, text, id),
136
- /// )?;
137
- ///
138
- /// match result {
139
- /// Some(custom_output) => return Ok(custom_output),
140
- /// None => { /* proceed with default conversion */ }
141
- /// }
142
- /// ```
143
- #[allow(dead_code)]
144
- #[inline]
145
- /// # Errors
146
- ///
147
- /// Returns an error if visitor dispatch fails.
148
- pub fn dispatch_visitor<F>(visitor: &Option<Rc<RefCell<dyn HtmlVisitor>>>, callback: F) -> Result<VisitorDispatch>
149
- where
150
- F: FnOnce(&mut dyn HtmlVisitor) -> VisitResult,
151
- {
152
- let Some(visitor_rc) = visitor else {
153
- return Ok(VisitorDispatch::Continue);
154
- };
155
-
156
- let mut visitor_ref = visitor_rc.borrow_mut();
157
- let result = callback(&mut *visitor_ref);
158
-
159
- match result {
160
- VisitResult::Continue => Ok(VisitorDispatch::Continue),
161
- VisitResult::Custom(output) => Ok(VisitorDispatch::Custom(output)),
162
- VisitResult::Skip => Ok(VisitorDispatch::Skip),
163
- VisitResult::PreserveHtml => Ok(VisitorDispatch::PreserveHtml),
164
- VisitResult::Error(msg) => Err(ConversionError::Visitor(msg)),
165
- }
166
- }
167
-
168
- /// Result of dispatching a visitor callback.
169
- ///
170
- /// This enum represents the outcome of a visitor callback dispatch,
171
- /// providing a more ergonomic interface for control flow than the
172
- /// raw `VisitResult` type.
173
- #[allow(dead_code)]
174
- #[derive(Debug)]
175
- pub enum VisitorDispatch {
176
- /// Continue with default conversion behavior
177
- Continue,
178
-
179
- /// Replace default output with custom markdown
180
- Custom(String),
181
-
182
- /// Skip this element entirely (don't output anything)
183
- Skip,
184
-
185
- /// Preserve original HTML (don't convert to markdown)
186
- PreserveHtml,
187
- }
188
-
189
- impl VisitorDispatch {
190
- /// Check if this dispatch result indicates continuation.
191
- #[allow(dead_code)]
192
- #[inline]
193
- #[must_use]
194
- pub const fn is_continue(&self) -> bool {
195
- matches!(self, Self::Continue)
196
- }
197
-
198
- /// Check if this dispatch result contains custom output.
199
- #[allow(dead_code)]
200
- #[inline]
201
- #[must_use]
202
- pub const fn is_custom(&self) -> bool {
203
- matches!(self, Self::Custom(_))
204
- }
205
-
206
- /// Check if this dispatch result indicates skipping.
207
- #[allow(dead_code)]
208
- #[inline]
209
- #[must_use]
210
- pub const fn is_skip(&self) -> bool {
211
- matches!(self, Self::Skip)
212
- }
213
-
214
- /// Check if this dispatch result indicates HTML preservation.
215
- #[allow(dead_code)]
216
- #[inline]
217
- #[must_use]
218
- pub const fn is_preserve_html(&self) -> bool {
219
- matches!(self, Self::PreserveHtml)
220
- }
221
-
222
- /// Extract custom output if present.
223
- #[allow(dead_code)]
224
- #[inline]
225
- #[must_use]
226
- pub fn into_custom(self) -> Option<String> {
227
- match self {
228
- Self::Custom(output) => Some(output),
229
- _ => None,
230
- }
231
- }
232
-
233
- /// Extract custom output reference if present.
234
- #[allow(dead_code)]
235
- #[inline]
236
- #[must_use]
237
- pub fn as_custom(&self) -> Option<&str> {
238
- match self {
239
- Self::Custom(output) => Some(output),
240
- _ => None,
241
- }
242
- }
243
- }
244
-
245
- /// Macro to reduce boilerplate when calling visitor methods.
246
- ///
247
- /// This macro wraps the common pattern of:
248
- /// 1. Check if visitor is present
249
- /// 2. Call visitor method
250
- /// 3. Handle early return for Custom/Skip/PreserveHtml/Error
251
- /// 4. Continue with default behavior if visitor returns Continue
252
- ///
253
- /// # Syntax
254
- ///
255
- /// ```text
256
- /// try_visitor!(visitor_option, method_name, ctx, arg1, arg2, ...);
257
- /// ```
258
- ///
259
- /// # Returns
260
- ///
261
- /// - Returns early with custom output if visitor returns Custom/Skip/PreserveHtml
262
- /// - Returns early with Err if visitor returns Error
263
- /// - Continues execution if visitor returns Continue or is None
264
- ///
265
- /// # Examples
266
- ///
267
- /// ```text
268
- /// // Before (verbose):
269
- /// let dispatch = dispatch_visitor(&visitor, |v| v.visit_heading(&ctx, level, text, id))?;
270
- /// match dispatch {
271
- /// VisitorDispatch::Custom(output) => return Ok(output),
272
- /// VisitorDispatch::Skip => return Ok(String::new()),
273
- /// VisitorDispatch::PreserveHtml => return Ok(preserve_html_output),
274
- /// VisitorDispatch::Continue => { /* proceed */ }
275
- /// }
276
- ///
277
- /// // After (concise):
278
- /// try_visitor!(visitor, visit_heading, &ctx, level, text, id);
279
- /// // Default conversion logic continues here...
280
- /// ```
281
- #[macro_export]
282
- macro_rules! try_visitor {
283
- ($visitor:expr, $method:ident, $ctx:expr $(, $arg:expr)*) => {{
284
- let dispatch = $crate::visitor_helpers::dispatch_visitor(
285
- $visitor,
286
- |v| v.$method($ctx $(, $arg)*),
287
- )?;
288
-
289
- match dispatch {
290
- $crate::visitor_helpers::VisitorDispatch::Continue => {
291
- }
292
- $crate::visitor_helpers::VisitorDispatch::Custom(output) => {
293
- return Ok(output);
294
- }
295
- $crate::visitor_helpers::VisitorDispatch::Skip => {
296
- return Ok(String::new());
297
- }
298
- $crate::visitor_helpers::VisitorDispatch::PreserveHtml => {
299
- // Falls through to default conversion — full HTML preservation requires
300
- // the node handle and parser context which aren't available in this macro.
301
- // Callers that need PreserveHtml support should match on the dispatch
302
- // result directly and call serialize_tag_to_html.
303
- }
304
- }
305
- }};
306
- }
307
-
308
- /// Convenience macro for `element_start` visitor calls with early return.
309
- ///
310
- /// This specialized macro handles the common pattern of calling `visit_element_start`
311
- /// at the beginning of element processing. Unlike `try_visitor!`, this macro
312
- /// understands that `element_start` callbacks typically want to abort processing
313
- /// entirely if they return anything other than Continue.
314
- ///
315
- /// # Syntax
316
- ///
317
- /// ```text
318
- /// try_visitor_element_start!(visitor_option, ctx);
319
- /// ```
320
- ///
321
- /// # Examples
322
- ///
323
- /// ```text
324
- /// fn process_heading(...) -> Result<String> {
325
- /// let ctx = build_node_context(...);
326
- /// try_visitor_element_start!(visitor, &ctx)?;
327
- ///
328
- /// // Default heading processing continues here...
329
- /// }
330
- /// ```
331
- #[macro_export]
332
- macro_rules! try_visitor_element_start {
333
- ($visitor:expr, $ctx:expr) => {{
334
- $crate::try_visitor!($visitor, visit_element_start, $ctx);
335
- }};
336
- }
337
-
338
- /// Convenience macro for `element_end` visitor calls with output inspection.
339
- ///
340
- /// This specialized macro handles the common pattern of calling `visit_element_end`
341
- /// after generating default markdown output. The visitor receives the default
342
- /// output and can choose to replace it or let it pass through.
343
- ///
344
- /// # Syntax
345
- ///
346
- /// ```text
347
- /// try_visitor_element_end!(visitor_option, ctx, default_output_string);
348
- /// ```
349
- ///
350
- /// # Examples
351
- ///
352
- /// ```text
353
- /// fn process_heading(...) -> Result<String> {
354
- /// let ctx = build_node_context(...);
355
- /// let mut output = String::from("# Heading");
356
- ///
357
- /// try_visitor_element_end!(visitor, &ctx, &output)?;
358
- /// Ok(output)
359
- /// }
360
- /// ```
361
- #[macro_export]
362
- macro_rules! try_visitor_element_end {
363
- ($visitor:expr, $ctx:expr, $output:expr) => {{
364
- $crate::try_visitor!($visitor, visit_element_end, $ctx, $output);
365
- }};
366
- }
367
-
368
- #[cfg(test)]
369
- mod tests {
370
- use super::*;
371
-
372
- #[test]
373
- fn test_build_node_context() {
374
- let mut attrs = BTreeMap::new();
375
- attrs.insert("id".to_string(), "main".to_string());
376
- attrs.insert("class".to_string(), "container".to_string());
377
-
378
- let ctx = build_node_context(NodeType::Div, "div", &attrs, 2, 3, Some("body"), false);
379
-
380
- assert_eq!(ctx.node_type, NodeType::Div);
381
- assert_eq!(ctx.tag_name, "div");
382
- assert_eq!(ctx.depth, 2);
383
- assert_eq!(ctx.index_in_parent, 3);
384
- assert_eq!(ctx.parent_tag, Some("body".to_string()));
385
- assert!(!ctx.is_inline);
386
- assert_eq!(ctx.attributes.len(), 2);
387
- assert_eq!(ctx.attributes.get("id"), Some(&"main".to_string()));
388
- }
389
-
390
- #[test]
391
- fn test_build_node_context_no_parent() {
392
- let attrs = BTreeMap::new();
393
-
394
- let ctx = build_node_context(NodeType::Html, "html", &attrs, 0, 0, None, false);
395
-
396
- assert_eq!(ctx.node_type, NodeType::Html);
397
- assert_eq!(ctx.parent_tag, None);
398
- assert!(ctx.attributes.is_empty());
399
- }
400
-
401
- #[test]
402
- fn test_dispatch_visitor_none() {
403
- let visitor: Option<Rc<RefCell<dyn HtmlVisitor>>> = None;
404
-
405
- let result = dispatch_visitor(&visitor, |v| {
406
- let ctx = NodeContext {
407
- node_type: NodeType::Text,
408
- tag_name: String::new(),
409
- attributes: BTreeMap::new(),
410
- depth: 0,
411
- index_in_parent: 0,
412
- parent_tag: None,
413
- is_inline: true,
414
- };
415
- v.visit_text(&ctx, "test")
416
- })
417
- .unwrap();
418
-
419
- assert!(result.is_continue());
420
- }
421
-
422
- #[derive(Debug)]
423
- struct TestVisitor {
424
- mode: TestMode,
425
- }
426
-
427
- #[derive(Debug)]
428
- enum TestMode {
429
- Continue,
430
- Custom,
431
- Skip,
432
- PreserveHtml,
433
- Error,
434
- }
435
-
436
- impl HtmlVisitor for TestVisitor {
437
- fn visit_text(&mut self, _ctx: &NodeContext, text: &str) -> VisitResult {
438
- match self.mode {
439
- TestMode::Continue => VisitResult::Continue,
440
- TestMode::Custom => VisitResult::Custom(format!("CUSTOM: {}", text)),
441
- TestMode::Skip => VisitResult::Skip,
442
- TestMode::PreserveHtml => VisitResult::PreserveHtml,
443
- TestMode::Error => VisitResult::Error("test error".to_string()),
444
- }
445
- }
446
- }
447
-
448
- #[test]
449
- fn test_dispatch_visitor_continue() {
450
- let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {
451
- mode: TestMode::Continue,
452
- }));
453
- let visitor_opt = Some(visitor);
454
-
455
- let ctx = NodeContext {
456
- node_type: NodeType::Text,
457
- tag_name: String::new(),
458
- attributes: BTreeMap::new(),
459
- depth: 0,
460
- index_in_parent: 0,
461
- parent_tag: None,
462
- is_inline: true,
463
- };
464
-
465
- let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
466
-
467
- assert!(result.is_continue());
468
- }
469
-
470
- #[test]
471
- fn test_dispatch_visitor_custom() {
472
- let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Custom }));
473
- let visitor_opt = Some(visitor);
474
-
475
- let ctx = NodeContext {
476
- node_type: NodeType::Text,
477
- tag_name: String::new(),
478
- attributes: BTreeMap::new(),
479
- depth: 0,
480
- index_in_parent: 0,
481
- parent_tag: None,
482
- is_inline: true,
483
- };
484
-
485
- let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
486
-
487
- assert!(result.is_custom());
488
- assert_eq!(result.as_custom(), Some("CUSTOM: hello"));
489
- }
490
-
491
- #[test]
492
- fn test_dispatch_visitor_skip() {
493
- let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Skip }));
494
- let visitor_opt = Some(visitor);
495
-
496
- let ctx = NodeContext {
497
- node_type: NodeType::Text,
498
- tag_name: String::new(),
499
- attributes: BTreeMap::new(),
500
- depth: 0,
501
- index_in_parent: 0,
502
- parent_tag: None,
503
- is_inline: true,
504
- };
505
-
506
- let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
507
-
508
- assert!(result.is_skip());
509
- }
510
-
511
- #[test]
512
- fn test_dispatch_visitor_preserve_html() {
513
- let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor {
514
- mode: TestMode::PreserveHtml,
515
- }));
516
- let visitor_opt = Some(visitor);
517
-
518
- let ctx = NodeContext {
519
- node_type: NodeType::Text,
520
- tag_name: String::new(),
521
- attributes: BTreeMap::new(),
522
- depth: 0,
523
- index_in_parent: 0,
524
- parent_tag: None,
525
- is_inline: true,
526
- };
527
-
528
- let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello")).unwrap();
529
-
530
- assert!(result.is_preserve_html());
531
- }
532
-
533
- #[test]
534
- fn test_dispatch_visitor_error() {
535
- let visitor: Rc<RefCell<dyn HtmlVisitor>> = Rc::new(RefCell::new(TestVisitor { mode: TestMode::Error }));
536
- let visitor_opt = Some(visitor);
537
-
538
- let ctx = NodeContext {
539
- node_type: NodeType::Text,
540
- tag_name: String::new(),
541
- attributes: BTreeMap::new(),
542
- depth: 0,
543
- index_in_parent: 0,
544
- parent_tag: None,
545
- is_inline: true,
546
- };
547
-
548
- let result = dispatch_visitor(&visitor_opt, |v| v.visit_text(&ctx, "hello"));
549
-
550
- assert!(result.is_err());
551
- if let Err(ConversionError::Visitor(msg)) = result {
552
- assert_eq!(msg, "test error");
553
- } else {
554
- panic!("Expected Visitor error");
555
- }
556
- }
557
-
558
- #[test]
559
- fn test_visitor_dispatch_predicates() {
560
- let continue_dispatch = VisitorDispatch::Continue;
561
- assert!(continue_dispatch.is_continue());
562
- assert!(!continue_dispatch.is_custom());
563
- assert!(!continue_dispatch.is_skip());
564
-
565
- let custom_dispatch = VisitorDispatch::Custom("output".to_string());
566
- assert!(!custom_dispatch.is_continue());
567
- assert!(custom_dispatch.is_custom());
568
- assert!(!custom_dispatch.is_skip());
569
-
570
- let skip_dispatch = VisitorDispatch::Skip;
571
- assert!(!skip_dispatch.is_continue());
572
- assert!(!skip_dispatch.is_custom());
573
- assert!(skip_dispatch.is_skip());
574
-
575
- let preserve_dispatch = VisitorDispatch::PreserveHtml;
576
- assert!(!preserve_dispatch.is_continue());
577
- assert!(preserve_dispatch.is_preserve_html());
578
- }
579
-
580
- #[test]
581
- fn test_visitor_dispatch_into_custom() {
582
- let custom = VisitorDispatch::Custom("test".to_string());
583
- assert_eq!(custom.into_custom(), Some("test".to_string()));
584
-
585
- let continue_dispatch = VisitorDispatch::Continue;
586
- assert_eq!(continue_dispatch.into_custom(), None);
587
- }
588
-
589
- #[test]
590
- fn test_visitor_dispatch_as_custom() {
591
- let custom = VisitorDispatch::Custom("test".to_string());
592
- assert_eq!(custom.as_custom(), Some("test"));
593
-
594
- let skip = VisitorDispatch::Skip;
595
- assert_eq!(skip.as_custom(), None);
596
- }
597
- }