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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +347 -0
- data/Steepfile +10 -2
- data/ext/html_to_markdown_rb/Cargo.toml +3 -2
- data/ext/html_to_markdown_rb/extconf.rb +5 -5
- data/ext/html_to_markdown_rb/native/Cargo.lock +962 -0
- data/ext/html_to_markdown_rb/native/Cargo.toml +6 -11
- data/ext/html_to_markdown_rb/native/extconf.rb +14 -0
- data/ext/html_to_markdown_rb/src/lib.rs +1715 -646
- data/lib/html_to_markdown/native.rb +913 -37
- data/lib/html_to_markdown/version.rb +3 -3
- data/lib/html_to_markdown.rb +9 -4
- data/lib/html_to_markdown_rb.so +0 -0
- data/sig/types.rbs +59 -292
- metadata +32 -179
- data/ext/html_to_markdown_rb/Makefile +0 -592
- data/lib/bin/html-to-markdown +0 -0
- data/vendor/Cargo.toml +0 -33
- data/vendor/html-to-markdown-rs/Cargo.toml +0 -54
- data/vendor/html-to-markdown-rs/README.md +0 -278
- data/vendor/html-to-markdown-rs/examples/basic.rs +0 -24
- data/vendor/html-to-markdown-rs/examples/table.rs +0 -25
- data/vendor/html-to-markdown-rs/examples/test_deser.rs +0 -12
- data/vendor/html-to-markdown-rs/examples/test_escape.rs +0 -58
- data/vendor/html-to-markdown-rs/examples/test_inline_formatting.rs +0 -113
- data/vendor/html-to-markdown-rs/examples/test_lists.rs +0 -39
- data/vendor/html-to-markdown-rs/examples/test_semantic_tags.rs +0 -89
- data/vendor/html-to-markdown-rs/examples/test_tables.rs +0 -100
- data/vendor/html-to-markdown-rs/examples/test_task_lists.rs +0 -61
- data/vendor/html-to-markdown-rs/examples/test_whitespace.rs +0 -34
- data/vendor/html-to-markdown-rs/src/convert_api.rs +0 -349
- data/vendor/html-to-markdown-rs/src/converter/block/blockquote.rs +0 -178
- data/vendor/html-to-markdown-rs/src/converter/block/container.rs +0 -114
- data/vendor/html-to-markdown-rs/src/converter/block/div.rs +0 -149
- data/vendor/html-to-markdown-rs/src/converter/block/heading.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/block/horizontal_rule.rs +0 -103
- data/vendor/html-to-markdown-rs/src/converter/block/line_break.rs +0 -89
- data/vendor/html-to-markdown-rs/src/converter/block/mod.rs +0 -10
- data/vendor/html-to-markdown-rs/src/converter/block/paragraph.rs +0 -140
- data/vendor/html-to-markdown-rs/src/converter/block/preformatted.rs +0 -298
- data/vendor/html-to-markdown-rs/src/converter/block/table/builder.rs +0 -453
- data/vendor/html-to-markdown-rs/src/converter/block/table/caption.rs +0 -44
- data/vendor/html-to-markdown-rs/src/converter/block/table/cell.rs +0 -276
- data/vendor/html-to-markdown-rs/src/converter/block/table/cells.rs +0 -336
- data/vendor/html-to-markdown-rs/src/converter/block/table/layout.rs +0 -58
- data/vendor/html-to-markdown-rs/src/converter/block/table/mod.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/block/table/scanner.rs +0 -146
- data/vendor/html-to-markdown-rs/src/converter/block/table/utils.rs +0 -34
- data/vendor/html-to-markdown-rs/src/converter/block/unknown.rs +0 -138
- data/vendor/html-to-markdown-rs/src/converter/context.rs +0 -208
- data/vendor/html-to-markdown-rs/src/converter/dom_context.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/form/elements.rs +0 -770
- data/vendor/html-to-markdown-rs/src/converter/form/mod.rs +0 -82
- data/vendor/html-to-markdown-rs/src/converter/format/djot.rs +0 -64
- data/vendor/html-to-markdown-rs/src/converter/format/markdown.rs +0 -59
- data/vendor/html-to-markdown-rs/src/converter/format/mod.rs +0 -43
- data/vendor/html-to-markdown-rs/src/converter/handlers/blockquote.rs +0 -173
- data/vendor/html-to-markdown-rs/src/converter/handlers/code_block.rs +0 -434
- data/vendor/html-to-markdown-rs/src/converter/handlers/graphic.rs +0 -234
- data/vendor/html-to-markdown-rs/src/converter/handlers/image.rs +0 -282
- data/vendor/html-to-markdown-rs/src/converter/handlers/link.rs +0 -316
- data/vendor/html-to-markdown-rs/src/converter/handlers/mod.rs +0 -26
- data/vendor/html-to-markdown-rs/src/converter/inline/code.rs +0 -306
- data/vendor/html-to-markdown-rs/src/converter/inline/emphasis.rs +0 -345
- data/vendor/html-to-markdown-rs/src/converter/inline/link.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/inline/mod.rs +0 -237
- data/vendor/html-to-markdown-rs/src/converter/inline/ruby.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/marks.rs +0 -566
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/mod.rs +0 -86
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/typography.rs +0 -558
- data/vendor/html-to-markdown-rs/src/converter/list/definition.rs +0 -232
- data/vendor/html-to-markdown-rs/src/converter/list/item.rs +0 -332
- data/vendor/html-to-markdown-rs/src/converter/list/mod.rs +0 -70
- data/vendor/html-to-markdown-rs/src/converter/list/ordered.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/list/unordered.rs +0 -195
- data/vendor/html-to-markdown-rs/src/converter/list/utils.rs +0 -314
- data/vendor/html-to-markdown-rs/src/converter/main.rs +0 -710
- data/vendor/html-to-markdown-rs/src/converter/main_helpers.rs +0 -452
- data/vendor/html-to-markdown-rs/src/converter/media/embedded.rs +0 -393
- data/vendor/html-to-markdown-rs/src/converter/media/graphic.rs +0 -4
- data/vendor/html-to-markdown-rs/src/converter/media/image.rs +0 -183
- data/vendor/html-to-markdown-rs/src/converter/media/mod.rs +0 -87
- data/vendor/html-to-markdown-rs/src/converter/media/svg.rs +0 -280
- data/vendor/html-to-markdown-rs/src/converter/metadata.rs +0 -220
- data/vendor/html-to-markdown-rs/src/converter/mod.rs +0 -156
- data/vendor/html-to-markdown-rs/src/converter/plain_text.rs +0 -516
- data/vendor/html-to-markdown-rs/src/converter/preprocessing_helpers.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/reference_collector.rs +0 -69
- data/vendor/html-to-markdown-rs/src/converter/semantic/attributes.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/semantic/definition_list.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/semantic/figure.rs +0 -391
- data/vendor/html-to-markdown-rs/src/converter/semantic/mod.rs +0 -112
- data/vendor/html-to-markdown-rs/src/converter/semantic/sectioning.rs +0 -85
- data/vendor/html-to-markdown-rs/src/converter/semantic/summary.rs +0 -324
- data/vendor/html-to-markdown-rs/src/converter/text/mod.rs +0 -8
- data/vendor/html-to-markdown-rs/src/converter/text/processing.rs +0 -56
- data/vendor/html-to-markdown-rs/src/converter/text_node.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/utility/attributes.rs +0 -151
- data/vendor/html-to-markdown-rs/src/converter/utility/caching.rs +0 -74
- data/vendor/html-to-markdown-rs/src/converter/utility/content.rs +0 -271
- data/vendor/html-to-markdown-rs/src/converter/utility/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/converter/utility/preprocessing.rs +0 -1002
- data/vendor/html-to-markdown-rs/src/converter/utility/serialization.rs +0 -126
- data/vendor/html-to-markdown-rs/src/converter/utility/siblings.rs +0 -97
- data/vendor/html-to-markdown-rs/src/converter/visitor_hooks.rs +0 -189
- data/vendor/html-to-markdown-rs/src/error.rs +0 -43
- data/vendor/html-to-markdown-rs/src/exports.rs +0 -24
- data/vendor/html-to-markdown-rs/src/inline_images.rs +0 -336
- data/vendor/html-to-markdown-rs/src/lib.rs +0 -139
- data/vendor/html-to-markdown-rs/src/metadata/collector.rs +0 -457
- data/vendor/html-to-markdown-rs/src/metadata/config.rs +0 -394
- data/vendor/html-to-markdown-rs/src/metadata/extraction.rs +0 -398
- data/vendor/html-to-markdown-rs/src/metadata/mod.rs +0 -288
- data/vendor/html-to-markdown-rs/src/metadata/types.rs +0 -477
- data/vendor/html-to-markdown-rs/src/options/conversion.rs +0 -559
- data/vendor/html-to-markdown-rs/src/options/inline_image.rs +0 -111
- data/vendor/html-to-markdown-rs/src/options/mod.rs +0 -20
- data/vendor/html-to-markdown-rs/src/options/preprocessing.rs +0 -201
- data/vendor/html-to-markdown-rs/src/options/validation.rs +0 -416
- data/vendor/html-to-markdown-rs/src/prelude.rs +0 -1
- data/vendor/html-to-markdown-rs/src/rcdom.rs +0 -487
- data/vendor/html-to-markdown-rs/src/text.rs +0 -358
- data/vendor/html-to-markdown-rs/src/types/document.rs +0 -191
- data/vendor/html-to-markdown-rs/src/types/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/types/result.rs +0 -54
- data/vendor/html-to-markdown-rs/src/types/structure_builder.rs +0 -791
- data/vendor/html-to-markdown-rs/src/types/structure_collector.rs +0 -483
- data/vendor/html-to-markdown-rs/src/types/tables.rs +0 -52
- data/vendor/html-to-markdown-rs/src/types/warnings.rs +0 -33
- data/vendor/html-to-markdown-rs/src/validation.rs +0 -158
- data/vendor/html-to-markdown-rs/src/visitor/default_impl.rs +0 -63
- data/vendor/html-to-markdown-rs/src/visitor/mod.rs +0 -41
- data/vendor/html-to-markdown-rs/src/visitor/traits.rs +0 -370
- data/vendor/html-to-markdown-rs/src/visitor/types.rs +0 -319
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/callbacks/mod.rs +0 -1
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/content.rs +0 -126
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/mod.rs +0 -27
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/state.rs +0 -110
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/traversal.rs +0 -250
- data/vendor/html-to-markdown-rs/src/visitor_helpers.rs +0 -597
- data/vendor/html-to-markdown-rs/src/wrapper/sync.rs +0 -413
- data/vendor/html-to-markdown-rs/src/wrapper/utils.rs +0 -290
- data/vendor/html-to-markdown-rs/src/wrapper.rs +0 -9
- data/vendor/html-to-markdown-rs/tests/br_in_inline_test.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/commonmark_compliance_test.rs +0 -297
- data/vendor/html-to-markdown-rs/tests/djot_output_test.rs +0 -153
- data/vendor/html-to-markdown-rs/tests/exclude_selectors_test.rs +0 -132
- data/vendor/html-to-markdown-rs/tests/integration_test.rs +0 -631
- data/vendor/html-to-markdown-rs/tests/issue_121_regressions.rs +0 -49
- data/vendor/html-to-markdown-rs/tests/issue_127_regressions.rs +0 -58
- data/vendor/html-to-markdown-rs/tests/issue_128_regressions.rs +0 -17
- data/vendor/html-to-markdown-rs/tests/issue_131_regressions.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/issue_134_regressions.rs +0 -40
- data/vendor/html-to-markdown-rs/tests/issue_139_regressions.rs +0 -26
- data/vendor/html-to-markdown-rs/tests/issue_140_regressions.rs +0 -185
- data/vendor/html-to-markdown-rs/tests/issue_143_regressions.rs +0 -100
- data/vendor/html-to-markdown-rs/tests/issue_145_regressions.rs +0 -133
- data/vendor/html-to-markdown-rs/tests/issue_146_regressions.rs +0 -144
- data/vendor/html-to-markdown-rs/tests/issue_176_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_190_regressions.rs +0 -128
- data/vendor/html-to-markdown-rs/tests/issue_199_regressions.rs +0 -20
- data/vendor/html-to-markdown-rs/tests/issue_200_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_212_regressions.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/issue_216_217_regressions.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/issue_336_regressions.rs +0 -74
- data/vendor/html-to-markdown-rs/tests/issue_339_regressions.rs +0 -92
- data/vendor/html-to-markdown-rs/tests/issue_347_regressions.rs +0 -154
- data/vendor/html-to-markdown-rs/tests/issue_348_visitor_plain.rs +0 -93
- data/vendor/html-to-markdown-rs/tests/json_ld_script_extraction.rs +0 -44
- data/vendor/html-to-markdown-rs/tests/lists_test.rs +0 -199
- data/vendor/html-to-markdown-rs/tests/plain_output_test.rs +0 -273
- data/vendor/html-to-markdown-rs/tests/preprocessing_tests.rs +0 -61
- data/vendor/html-to-markdown-rs/tests/reference_links_test.rs +0 -169
- data/vendor/html-to-markdown-rs/tests/sectioning_elements_test.rs +0 -137
- data/vendor/html-to-markdown-rs/tests/skip_images_test.rs +0 -522
- data/vendor/html-to-markdown-rs/tests/tables_test.rs +0 -743
- data/vendor/html-to-markdown-rs/tests/test_custom_elements.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/test_issue_187.rs +0 -204
- data/vendor/html-to-markdown-rs/tests/test_issue_218.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/test_issue_277.rs +0 -77
- data/vendor/html-to-markdown-rs/tests/test_max_depth.rs +0 -82
- data/vendor/html-to-markdown-rs/tests/test_nested_simple.rs +0 -45
- data/vendor/html-to-markdown-rs/tests/test_script_style_stripping.rs +0 -396
- data/vendor/html-to-markdown-rs/tests/test_spa_bisect.rs +0 -34
- data/vendor/html-to-markdown-rs/tests/visitor_code_integration_test.rs +0 -121
- data/vendor/html-to-markdown-rs/tests/visitor_integration_test.rs +0 -1190
- data/vendor/html-to-markdown-rs/tests/xml_tables_test.rs +0 -372
|
@@ -1,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
|
-
}
|