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,428 +0,0 @@
1
- //! Handler for link elements (a, anchor).
2
- //!
3
- //! Converts HTML anchor tags to Markdown links with support for:
4
- //! - Standard Markdown link syntax `[label](href "title")`
5
- //! - Autolinks for simple URLs like `<https://example.com>`
6
- //! - Link label escaping for special Markdown characters
7
- //! - Heading-in-link special handling (wraps link around heading)
8
- //! - Visitor callbacks for custom link processing
9
- //! - Metadata collection for links (links, URLs, titles, rel attributes)
10
- //! - Block-level content within links (via inline context)
11
-
12
- #[cfg(feature = "visitor")]
13
- use crate::converter::utility::content::collect_tag_attributes;
14
- use crate::converter::utility::content::{collect_link_label_text, escape_link_label, normalize_link_label};
15
- use crate::converter::utility::preprocessing::sanitize_markdown_url;
16
- use crate::options::ConversionOptions;
17
- #[cfg(any(feature = "metadata", feature = "visitor"))]
18
- use std::collections::BTreeMap;
19
- use tl::{NodeHandle, Parser};
20
-
21
- // Type aliases for Context and DomContext to avoid circular imports
22
- // These are imported from converter.rs and should be made accessible
23
- type Context = crate::converter::Context;
24
- type DomContext = crate::converter::DomContext;
25
-
26
- /// Handler for anchor/link elements: `<a>`.
27
- ///
28
- /// Processes anchor tags to generate Markdown links:
29
- /// - Detects autolinks (link text matches href)
30
- /// - Extracts and normalizes link labels
31
- /// - Handles nested headings within links
32
- /// - Escapes special characters in labels
33
- /// - Collects metadata when feature is enabled
34
- /// - Supports visitor callbacks for custom processing
35
- ///
36
- /// # Link Label Extraction
37
- /// For links with block-level content, extracts text separately.
38
- /// Collapses newlines and normalizes whitespace per Markdown spec.
39
- ///
40
- /// # Autolinks
41
- /// When `autolinks` option is enabled, detects links where the text equals
42
- /// the href (e.g., `<a href="https://example.com">https://example.com</a>`)
43
- /// and outputs as `<https://example.com>` instead.
44
- ///
45
- /// # Note
46
- /// This function references helper functions from converter.rs
47
- /// which must be accessible (pub(crate)) for this module to work correctly.
48
- pub fn handle(
49
- node_handle: &NodeHandle,
50
- parser: &Parser,
51
- output: &mut String,
52
- options: &ConversionOptions,
53
- ctx: &Context,
54
- depth: usize,
55
- dom_ctx: &DomContext,
56
- ) {
57
- // Import helper functions from parent converter module
58
- use crate::converter::block::heading::{heading_allows_inline_images, push_heading};
59
- use crate::converter::utility::content::normalized_tag_name;
60
- #[allow(unused_imports)]
61
- use crate::converter::utility::serialization::serialize_node;
62
- use crate::converter::{find_single_heading_child, get_text_content, walk_node};
63
-
64
- let Some(node) = node_handle.get(parser) else {
65
- return;
66
- };
67
-
68
- let tl::Node::Tag(tag) = node else {
69
- return;
70
- };
71
-
72
- // Extract href and title attributes
73
- let href_attr = tag.attributes().get("href").flatten().map(|v| {
74
- let decoded = crate::text::decode_html_entities(&v.as_utf8_str());
75
- sanitize_markdown_url(&decoded).into_owned()
76
- });
77
- let title = tag
78
- .attributes()
79
- .get("title")
80
- .flatten()
81
- .map(|v| v.as_utf8_str().to_string());
82
-
83
- if let Some(href) = href_attr {
84
- let raw_text = crate::text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx))
85
- .trim()
86
- .to_string();
87
-
88
- // If we're already inside a link, just render the text content, don't create a nested link
89
- if ctx.in_link {
90
- let children = tag.children();
91
- for child_handle in children.top().iter() {
92
- walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);
93
- }
94
- return;
95
- }
96
-
97
- // Check if this should be rendered as an autolink
98
- let is_autolink = options.autolinks
99
- && !options.default_title
100
- && !href.is_empty()
101
- && (raw_text == href || (href.starts_with("mailto:") && raw_text == href[7..]));
102
-
103
- if is_autolink {
104
- output.push('<');
105
- if href.starts_with("mailto:") && raw_text == href[7..] {
106
- output.push_str(&raw_text);
107
- } else {
108
- output.push_str(&href);
109
- }
110
- output.push('>');
111
- return;
112
- }
113
-
114
- // Check if link contains a single heading child element
115
- if let Some((heading_level, heading_handle)) = find_single_heading_child(*node_handle, parser) {
116
- if let Some(heading_node) = heading_handle.get(parser) {
117
- if let tl::Node::Tag(heading_tag) = heading_node {
118
- let heading_name = normalized_tag_name(heading_tag.name().as_utf8_str()).into_owned();
119
- let mut heading_text = String::new();
120
- let heading_ctx = Context {
121
- in_heading: true,
122
- convert_as_inline: true,
123
- heading_allow_inline_images: heading_allows_inline_images(
124
- &heading_name,
125
- &ctx.keep_inline_images_in,
126
- ),
127
- ..ctx.clone()
128
- };
129
- walk_node(
130
- &heading_handle,
131
- parser,
132
- &mut heading_text,
133
- options,
134
- &heading_ctx,
135
- depth + 1,
136
- dom_ctx,
137
- );
138
- let trimmed_heading = heading_text.trim();
139
- if !trimmed_heading.is_empty() {
140
- let escaped_label = escape_link_label(trimmed_heading);
141
- let mut link_buffer = String::new();
142
- append_markdown_link(
143
- &mut link_buffer,
144
- &escaped_label,
145
- href.as_str(),
146
- title.as_deref(),
147
- raw_text.as_str(),
148
- options,
149
- ctx.reference_collector.as_ref(),
150
- );
151
- push_heading(output, ctx, options, heading_level, link_buffer.as_str());
152
- return;
153
- }
154
- }
155
- }
156
- }
157
-
158
- // Collect link label from children
159
- let children: Vec<_> = tag.children().top().iter().copied().collect();
160
- let (inline_label, _block_nodes, saw_block) = collect_link_label_text(&children, parser, dom_ctx);
161
- let mut label = if saw_block {
162
- let mut content = String::new();
163
- let link_ctx = Context {
164
- inline_depth: ctx.inline_depth + 1,
165
- convert_as_inline: true,
166
- in_link: true,
167
- ..ctx.clone()
168
- };
169
- for child_handle in &children {
170
- let mut child_buf = String::new();
171
- walk_node(
172
- child_handle,
173
- parser,
174
- &mut child_buf,
175
- options,
176
- &link_ctx,
177
- depth + 1,
178
- dom_ctx,
179
- );
180
- if !child_buf.trim().is_empty()
181
- && !content.is_empty()
182
- && !content.chars().last().is_none_or(char::is_whitespace)
183
- && !child_buf.chars().next().is_none_or(char::is_whitespace)
184
- {
185
- content.push(' ');
186
- }
187
- content.push_str(&child_buf);
188
- }
189
- if content.trim().is_empty() {
190
- normalize_link_label(&inline_label)
191
- } else {
192
- normalize_link_label(&content)
193
- }
194
- } else {
195
- let mut content = String::new();
196
- let link_ctx = Context {
197
- inline_depth: ctx.inline_depth + 1,
198
- in_link: true,
199
- ..ctx.clone()
200
- };
201
- for child_handle in &children {
202
- walk_node(
203
- child_handle,
204
- parser,
205
- &mut content,
206
- options,
207
- &link_ctx,
208
- depth + 1,
209
- dom_ctx,
210
- );
211
- }
212
- normalize_link_label(&content)
213
- };
214
-
215
- // Apply fallback label strategies
216
- if label.is_empty() && saw_block {
217
- let fallback = crate::text::normalize_whitespace(&get_text_content(node_handle, parser, dom_ctx));
218
- label = normalize_link_label(&fallback);
219
- }
220
-
221
- if label.is_empty() && !raw_text.is_empty() {
222
- label = normalize_link_label(&raw_text);
223
- }
224
-
225
- if label.is_empty() && !href.is_empty() && !children.is_empty() {
226
- label = href.clone();
227
- }
228
-
229
- // Truncate label if it exceeds maximum length
230
- let escaped_label = escape_link_label(&label);
231
-
232
- // Handle visitor callbacks if feature is enabled
233
- #[cfg(feature = "visitor")]
234
- let link_output = if let Some(ref visitor_handle) = ctx.visitor {
235
- use crate::visitor::{NodeContext, NodeType, VisitResult};
236
-
237
- let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);
238
-
239
- let node_id = node_handle.get_inner();
240
- let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
241
- let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
242
-
243
- let node_ctx = NodeContext {
244
- node_type: NodeType::Link,
245
- tag_name: "a".to_string(),
246
- attributes,
247
- depth,
248
- index_in_parent,
249
- parent_tag,
250
- is_inline: true,
251
- };
252
-
253
- let visit_result = {
254
- let mut visitor = visitor_handle.borrow_mut();
255
- visitor.visit_link(&node_ctx, &href, &label, title.as_deref())
256
- };
257
- match visit_result {
258
- VisitResult::Continue => {
259
- let mut buf = String::new();
260
- append_markdown_link(
261
- &mut buf,
262
- &escaped_label,
263
- href.as_str(),
264
- title.as_deref(),
265
- label.as_str(),
266
- options,
267
- ctx.reference_collector.as_ref(),
268
- );
269
- Some(buf)
270
- }
271
- VisitResult::Custom(custom) => Some(custom),
272
- VisitResult::Skip => None,
273
- VisitResult::Error(err) => {
274
- if ctx.visitor_error.borrow().is_none() {
275
- *ctx.visitor_error.borrow_mut() = Some(err);
276
- }
277
- None
278
- }
279
- VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),
280
- }
281
- } else {
282
- let mut buf = String::new();
283
- append_markdown_link(
284
- &mut buf,
285
- &escaped_label,
286
- href.as_str(),
287
- title.as_deref(),
288
- label.as_str(),
289
- options,
290
- ctx.reference_collector.as_ref(),
291
- );
292
- Some(buf)
293
- };
294
-
295
- #[cfg(not(feature = "visitor"))]
296
- let link_output = {
297
- let mut buf = String::new();
298
- append_markdown_link(
299
- &mut buf,
300
- &escaped_label,
301
- href.as_str(),
302
- title.as_deref(),
303
- label.as_str(),
304
- options,
305
- ctx.reference_collector.as_ref(),
306
- );
307
- Some(buf)
308
- };
309
-
310
- if let Some(link_text) = link_output {
311
- output.push_str(&link_text);
312
- }
313
-
314
- // Collect metadata if feature is enabled
315
- #[cfg(feature = "metadata")]
316
- if ctx.metadata_wants_links {
317
- if let Some(ref collector) = ctx.metadata_collector {
318
- let rel_attr = tag
319
- .attributes()
320
- .get("rel")
321
- .flatten()
322
- .map(|v| v.as_utf8_str().to_string());
323
- let mut attributes_map = BTreeMap::new();
324
- for (key, value_opt) in tag.attributes().iter() {
325
- let key_str = key.to_string();
326
- if key_str == "href" {
327
- continue;
328
- }
329
-
330
- let value = value_opt.map(|v| v.to_string()).unwrap_or_default();
331
- attributes_map.insert(key_str, value);
332
- }
333
- collector
334
- .borrow_mut()
335
- .add_link(href.clone(), label, title.clone(), rel_attr, attributes_map);
336
- }
337
- }
338
- } else {
339
- // No href: just process children as inline content
340
- let children = tag.children();
341
- for child_handle in children.top().iter() {
342
- walk_node(child_handle, parser, output, options, ctx, depth + 1, dom_ctx);
343
- }
344
- }
345
- }
346
-
347
- /// Format and append a Markdown link to the output string.
348
- ///
349
- /// Generates the link syntax: `[label](href "title")`
350
- /// Handles special cases:
351
- /// - Empty href renders as `[label]()`
352
- /// - Hrefs with spaces/newlines get wrapped in angle brackets: `[label](<URL with spaces>)`
353
- /// - Unbalanced parentheses in href get escaped: `[label](url\(example\))`
354
- /// - Titles are wrapped in quotes and quotes inside are escaped
355
- /// - When `default_title` option is true and raw_text equals href, adds href as title
356
- ///
357
- /// # Arguments
358
- /// * `output` - Output buffer to append the link to
359
- /// * `label` - The link text (already escaped)
360
- /// * `href` - The URL/destination
361
- /// * `title` - Optional link title attribute
362
- /// * `raw_text` - Original unprocessed text (for default_title option)
363
- /// * `options` - Conversion options
364
- pub fn append_markdown_link(
365
- output: &mut String,
366
- label: &str,
367
- href: &str,
368
- title: Option<&str>,
369
- raw_text: &str,
370
- options: &ConversionOptions,
371
- reference_collector: Option<&crate::converter::reference_collector::ReferenceCollectorHandle>,
372
- ) {
373
- if options.link_style == crate::options::validation::LinkStyle::Reference && !href.is_empty() {
374
- if let Some(collector) = reference_collector {
375
- let ref_num = collector.borrow_mut().get_or_insert(href, title);
376
- output.push('[');
377
- output.push_str(label);
378
- output.push_str("][");
379
- output.push_str(&ref_num.to_string());
380
- output.push(']');
381
- return;
382
- }
383
- }
384
-
385
- output.push('[');
386
- output.push_str(label);
387
- output.push_str("](");
388
-
389
- if href.is_empty() {
390
- output.push_str("<>");
391
- } else if href.contains(' ') || href.contains('\n') {
392
- output.push('<');
393
- output.push_str(href);
394
- output.push('>');
395
- } else {
396
- let open_count = href.chars().filter(|&c| c == '(').count();
397
- let close_count = href.chars().filter(|&c| c == ')').count();
398
-
399
- if open_count == close_count {
400
- output.push_str(href);
401
- } else {
402
- let escaped_href = href.replace('(', "\\(").replace(')', "\\)");
403
- output.push_str(&escaped_href);
404
- }
405
- }
406
-
407
- if let Some(title_text) = title {
408
- output.push_str(" \"");
409
- if title_text.contains('"') {
410
- let escaped_title = title_text.replace('"', "\\\"");
411
- output.push_str(&escaped_title);
412
- } else {
413
- output.push_str(title_text);
414
- }
415
- output.push('"');
416
- } else if options.default_title && raw_text == href {
417
- output.push_str(" \"");
418
- if href.contains('"') {
419
- let escaped_href = href.replace('"', "\\\"");
420
- output.push_str(&escaped_href);
421
- } else {
422
- output.push_str(href);
423
- }
424
- output.push('"');
425
- }
426
-
427
- output.push(')');
428
- }
@@ -1,237 +0,0 @@
1
- //! Inline element handlers for HTML to Markdown conversion.
2
- //!
3
- //! This module provides specialized handlers for inline HTML elements:
4
- //! - Emphasis elements (strong, b, em, i)
5
- //! - Links (a)
6
- //! - Code elements (code, kbd, samp)
7
- //! - Semantic elements (mark, del, s, ins, u, small, sub, sup, var, dfn, abbr)
8
- //! - Ruby annotation elements (ruby, rb, rt, rp, rtc)
9
- //!
10
- //! These handlers are designed to be extracted from the main `converter.rs`
11
- //! file and integrated through the dispatcher function.
12
- //!
13
- //! **Integration Pattern:**
14
- //! Each handler function takes the same signature:
15
- //! - `tag_name: &str` - The HTML tag being processed
16
- //! - `node_handle: &NodeHandle` - The DOM node handle
17
- //! - `parser: &Parser` - The HTML parser reference
18
- //! - `output: &mut String` - The output buffer to write to
19
- //! - `options: &ConversionOptions` - Conversion configuration
20
- //! - `ctx: &Context` - Processing context (state tracking)
21
- //! - `depth: usize` - Current DOM tree depth
22
- //! - `dom_ctx: &DomContext` - DOM context for tree relationships
23
- //!
24
- //! The main dispatcher function `dispatch_inline_handler` routes tags to
25
- //! their appropriate handlers and returns a boolean indicating success.
26
-
27
- pub mod code;
28
- pub mod emphasis;
29
- pub mod link;
30
- pub mod ruby;
31
- pub mod semantic;
32
-
33
- // Re-export types from parent module for submodule access
34
-
35
- // Re-export handler functions for internal use by dispatcher (crate-private)
36
- // pub(crate) use ruby::handle as handle_ruby;
37
-
38
- /// Dispatches inline element handling to the appropriate handler.
39
- ///
40
- /// This function routes inline HTML elements to their specialized handlers
41
- /// based on tag name. It is designed to be called from the main `walk_node`
42
- /// function in `converter.rs`.
43
- ///
44
- /// # Routing Table
45
- ///
46
- /// The following tag routes are supported:
47
- ///
48
- /// | Tag(s) | Handler | Description |
49
- /// |--------|---------|-------------|
50
- /// | `strong`, `b` | emphasis | Bold/strong text formatting |
51
- /// | `em`, `i` | emphasis | Italic/emphasis text formatting |
52
- /// | `a` | link | Hyperlinks and anchors |
53
- /// | `code`, `kbd`, `samp` | code | Inline code and keyboard input |
54
- /// | `mark`, `del`, `s`, `ins`, `u`, `small`, `sub`, `sup`, `var`, `dfn`, `abbr`, `span` | semantic | Semantic formatting |
55
- /// | `ruby`, `rb`, `rt`, `rp`, `rtc` | ruby | Ruby annotations (East Asian typography) |
56
- ///
57
- /// # Return Value
58
- ///
59
- /// Returns `true` if the tag was recognized and handled, `false` otherwise.
60
- /// This allows the caller to distinguish between:
61
- /// - Handled inline elements (return `true`)
62
- /// - Unhandled elements (return `false`) that should be processed as text or passed through
63
- ///
64
- /// # Usage in converter.rs
65
- ///
66
- /// ```text
67
- /// if crate::converter::inline::dispatch_inline_handler(
68
- /// &tag_name,
69
- /// &node_handle,
70
- /// parser,
71
- /// output,
72
- /// options,
73
- /// ctx,
74
- /// depth,
75
- /// dom_ctx,
76
- /// ) {
77
- /// return; // Element was handled, move to next sibling
78
- /// }
79
- /// // Element was not handled, process as default inline element
80
- /// ```
81
- ///
82
- /// # Parameters
83
- ///
84
- /// * `tag_name` - The normalized HTML tag name (lowercase)
85
- /// * `node_handle` - The DOM node handle from the parser
86
- /// * `parser` - Reference to the tl HTML parser
87
- /// * `output` - Output buffer to write converted content to
88
- /// * `options` - Conversion configuration options
89
- /// * `ctx` - Processing context with state tracking
90
- /// * `depth` - Current DOM tree depth for recursion tracking
91
- /// * `dom_ctx` - DOM context for accessing tree structure
92
- ///
93
- /// # Example
94
- ///
95
- /// For `<strong>Bold text</strong>`, the dispatcher:
96
- /// 1. Recognizes "strong" tag
97
- /// 2. Routes to emphasis handler
98
- /// 3. Returns `true`
99
- /// 4. Emphasis handler outputs `**Bold text**` to output buffer
100
- ///
101
- /// For `<span>Normal text</span>`, the dispatcher:
102
- /// 1. Fails to recognize "span" tag
103
- /// 2. Returns `false`
104
- /// 3. Caller processes as default inline content
105
- pub fn dispatch_inline_handler(
106
- tag_name: &str,
107
- node_handle: &tl::NodeHandle,
108
- parser: &tl::Parser,
109
- output: &mut String,
110
- options: &crate::options::ConversionOptions,
111
- ctx: &crate::converter::Context,
112
- depth: usize,
113
- dom_ctx: &crate::converter::DomContext,
114
- ) -> bool {
115
- match tag_name {
116
- // Emphasis elements: strong, b (bold) and em, i (italic)
117
- "strong" | "b" | "em" | "i" => {
118
- emphasis::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);
119
- true
120
- }
121
- // Link elements: a (anchor)
122
- "a" => {
123
- link::handle(node_handle, parser, output, options, ctx, depth, dom_ctx);
124
- true
125
- }
126
- // Code elements: code, kbd (keyboard input), samp (sample output)
127
- "code" | "kbd" | "samp" => {
128
- code::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);
129
- true
130
- }
131
- // Semantic elements: mark, del, s, ins, u, small, sub, sup, var, dfn, abbr, span
132
- "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr" | "span" => {
133
- semantic::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);
134
- true
135
- }
136
- // Ruby annotation elements: ruby, rb, rt, rp, rtc
137
- "ruby" | "rb" | "rt" | "rp" | "rtc" => {
138
- ruby::handle(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx);
139
- true
140
- }
141
- // Unknown element - not handled by inline dispatcher
142
- _ => false,
143
- }
144
- }
145
-
146
- #[cfg(test)]
147
- mod tests {
148
- /// Test that all expected tags are properly dispatched
149
- #[test]
150
- fn test_dispatcher_routes_emphasis_tags() {
151
- assert!(matches!(
152
- ("strong", "strong"),
153
- (tag, _) if matches!(tag, "strong" | "b" | "em" | "i")
154
- ));
155
- assert!(matches!(
156
- ("em", "em"),
157
- (tag, _) if matches!(tag, "strong" | "b" | "em" | "i")
158
- ));
159
- }
160
-
161
- #[test]
162
- fn test_dispatcher_routes_code_tags() {
163
- assert!(matches!(
164
- ("code", "code"),
165
- (tag, _) if matches!(tag, "code" | "kbd" | "samp")
166
- ));
167
- assert!(matches!(
168
- ("kbd", "kbd"),
169
- (tag, _) if matches!(tag, "code" | "kbd" | "samp")
170
- ));
171
- }
172
-
173
- #[test]
174
- fn test_dispatcher_routes_semantic_tags() {
175
- assert!(matches!(
176
- ("mark", "mark"),
177
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
178
- ));
179
- assert!(matches!(
180
- ("del", "del"),
181
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
182
- ));
183
- assert!(matches!(
184
- ("sub", "sub"),
185
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
186
- ));
187
- assert!(matches!(
188
- ("var", "var"),
189
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
190
- ));
191
- assert!(matches!(
192
- ("dfn", "dfn"),
193
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
194
- ));
195
- assert!(matches!(
196
- ("abbr", "abbr"),
197
- (tag, _) if matches!(tag, "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr")
198
- ));
199
- }
200
-
201
- #[test]
202
- fn test_dispatcher_recognizes_link_tag() {
203
- assert!(matches!(
204
- ("a", "a"),
205
- (tag, _) if tag == "a"
206
- ));
207
- }
208
-
209
- #[test]
210
- fn test_dispatcher_routes_ruby_tags() {
211
- assert!(matches!(
212
- ("ruby", "ruby"),
213
- (tag, _) if matches!(tag, "ruby" | "rb" | "rt" | "rp" | "rtc")
214
- ));
215
- assert!(matches!(
216
- ("rt", "rt"),
217
- (tag, _) if matches!(tag, "ruby" | "rb" | "rt" | "rp" | "rtc")
218
- ));
219
- }
220
-
221
- #[test]
222
- fn test_unknown_tags_not_routed() {
223
- // These should fall through to default handling
224
- let unknown_tags = vec!["div", "p", "section", "article", "table"];
225
- for tag in unknown_tags {
226
- assert!(matches!(
227
- (tag, tag),
228
- (tag, _) if !matches!(
229
- tag,
230
- "strong" | "b" | "em" | "i" | "a" | "code" | "kbd" | "samp"
231
- | "mark" | "del" | "s" | "ins" | "u" | "small" | "sub" | "sup" | "var" | "dfn" | "abbr"
232
- | "ruby" | "rb" | "rt" | "rp" | "rtc" | "span"
233
- )
234
- ));
235
- }
236
- }
237
- }