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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +347 -0
  4. data/Steepfile +10 -2
  5. data/ext/html_to_markdown_rb/Cargo.toml +3 -2
  6. data/ext/html_to_markdown_rb/extconf.rb +5 -5
  7. data/ext/html_to_markdown_rb/native/Cargo.lock +962 -0
  8. data/ext/html_to_markdown_rb/native/Cargo.toml +6 -11
  9. data/ext/html_to_markdown_rb/native/extconf.rb +14 -0
  10. data/ext/html_to_markdown_rb/src/lib.rs +1715 -646
  11. data/lib/html_to_markdown/native.rb +913 -37
  12. data/lib/html_to_markdown/version.rb +3 -3
  13. data/lib/html_to_markdown.rb +9 -4
  14. data/lib/html_to_markdown_rb.so +0 -0
  15. data/sig/types.rbs +59 -292
  16. metadata +32 -179
  17. data/ext/html_to_markdown_rb/Makefile +0 -592
  18. data/lib/bin/html-to-markdown +0 -0
  19. data/vendor/Cargo.toml +0 -33
  20. data/vendor/html-to-markdown-rs/Cargo.toml +0 -54
  21. data/vendor/html-to-markdown-rs/README.md +0 -278
  22. data/vendor/html-to-markdown-rs/examples/basic.rs +0 -24
  23. data/vendor/html-to-markdown-rs/examples/table.rs +0 -25
  24. data/vendor/html-to-markdown-rs/examples/test_deser.rs +0 -12
  25. data/vendor/html-to-markdown-rs/examples/test_escape.rs +0 -58
  26. data/vendor/html-to-markdown-rs/examples/test_inline_formatting.rs +0 -113
  27. data/vendor/html-to-markdown-rs/examples/test_lists.rs +0 -39
  28. data/vendor/html-to-markdown-rs/examples/test_semantic_tags.rs +0 -89
  29. data/vendor/html-to-markdown-rs/examples/test_tables.rs +0 -100
  30. data/vendor/html-to-markdown-rs/examples/test_task_lists.rs +0 -61
  31. data/vendor/html-to-markdown-rs/examples/test_whitespace.rs +0 -34
  32. data/vendor/html-to-markdown-rs/src/convert_api.rs +0 -349
  33. data/vendor/html-to-markdown-rs/src/converter/block/blockquote.rs +0 -178
  34. data/vendor/html-to-markdown-rs/src/converter/block/container.rs +0 -114
  35. data/vendor/html-to-markdown-rs/src/converter/block/div.rs +0 -149
  36. data/vendor/html-to-markdown-rs/src/converter/block/heading.rs +0 -428
  37. data/vendor/html-to-markdown-rs/src/converter/block/horizontal_rule.rs +0 -103
  38. data/vendor/html-to-markdown-rs/src/converter/block/line_break.rs +0 -89
  39. data/vendor/html-to-markdown-rs/src/converter/block/mod.rs +0 -10
  40. data/vendor/html-to-markdown-rs/src/converter/block/paragraph.rs +0 -140
  41. data/vendor/html-to-markdown-rs/src/converter/block/preformatted.rs +0 -298
  42. data/vendor/html-to-markdown-rs/src/converter/block/table/builder.rs +0 -453
  43. data/vendor/html-to-markdown-rs/src/converter/block/table/caption.rs +0 -44
  44. data/vendor/html-to-markdown-rs/src/converter/block/table/cell.rs +0 -276
  45. data/vendor/html-to-markdown-rs/src/converter/block/table/cells.rs +0 -336
  46. data/vendor/html-to-markdown-rs/src/converter/block/table/layout.rs +0 -58
  47. data/vendor/html-to-markdown-rs/src/converter/block/table/mod.rs +0 -266
  48. data/vendor/html-to-markdown-rs/src/converter/block/table/scanner.rs +0 -146
  49. data/vendor/html-to-markdown-rs/src/converter/block/table/utils.rs +0 -34
  50. data/vendor/html-to-markdown-rs/src/converter/block/unknown.rs +0 -138
  51. data/vendor/html-to-markdown-rs/src/converter/context.rs +0 -208
  52. data/vendor/html-to-markdown-rs/src/converter/dom_context.rs +0 -337
  53. data/vendor/html-to-markdown-rs/src/converter/form/elements.rs +0 -770
  54. data/vendor/html-to-markdown-rs/src/converter/form/mod.rs +0 -82
  55. data/vendor/html-to-markdown-rs/src/converter/format/djot.rs +0 -64
  56. data/vendor/html-to-markdown-rs/src/converter/format/markdown.rs +0 -59
  57. data/vendor/html-to-markdown-rs/src/converter/format/mod.rs +0 -43
  58. data/vendor/html-to-markdown-rs/src/converter/handlers/blockquote.rs +0 -173
  59. data/vendor/html-to-markdown-rs/src/converter/handlers/code_block.rs +0 -434
  60. data/vendor/html-to-markdown-rs/src/converter/handlers/graphic.rs +0 -234
  61. data/vendor/html-to-markdown-rs/src/converter/handlers/image.rs +0 -282
  62. data/vendor/html-to-markdown-rs/src/converter/handlers/link.rs +0 -316
  63. data/vendor/html-to-markdown-rs/src/converter/handlers/mod.rs +0 -26
  64. data/vendor/html-to-markdown-rs/src/converter/inline/code.rs +0 -306
  65. data/vendor/html-to-markdown-rs/src/converter/inline/emphasis.rs +0 -345
  66. data/vendor/html-to-markdown-rs/src/converter/inline/link.rs +0 -428
  67. data/vendor/html-to-markdown-rs/src/converter/inline/mod.rs +0 -237
  68. data/vendor/html-to-markdown-rs/src/converter/inline/ruby.rs +0 -337
  69. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/marks.rs +0 -566
  70. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/mod.rs +0 -86
  71. data/vendor/html-to-markdown-rs/src/converter/inline/semantic/typography.rs +0 -558
  72. data/vendor/html-to-markdown-rs/src/converter/list/definition.rs +0 -232
  73. data/vendor/html-to-markdown-rs/src/converter/list/item.rs +0 -332
  74. data/vendor/html-to-markdown-rs/src/converter/list/mod.rs +0 -70
  75. data/vendor/html-to-markdown-rs/src/converter/list/ordered.rs +0 -201
  76. data/vendor/html-to-markdown-rs/src/converter/list/unordered.rs +0 -195
  77. data/vendor/html-to-markdown-rs/src/converter/list/utils.rs +0 -314
  78. data/vendor/html-to-markdown-rs/src/converter/main.rs +0 -710
  79. data/vendor/html-to-markdown-rs/src/converter/main_helpers.rs +0 -452
  80. data/vendor/html-to-markdown-rs/src/converter/media/embedded.rs +0 -393
  81. data/vendor/html-to-markdown-rs/src/converter/media/graphic.rs +0 -4
  82. data/vendor/html-to-markdown-rs/src/converter/media/image.rs +0 -183
  83. data/vendor/html-to-markdown-rs/src/converter/media/mod.rs +0 -87
  84. data/vendor/html-to-markdown-rs/src/converter/media/svg.rs +0 -280
  85. data/vendor/html-to-markdown-rs/src/converter/metadata.rs +0 -220
  86. data/vendor/html-to-markdown-rs/src/converter/mod.rs +0 -156
  87. data/vendor/html-to-markdown-rs/src/converter/plain_text.rs +0 -516
  88. data/vendor/html-to-markdown-rs/src/converter/preprocessing_helpers.rs +0 -201
  89. data/vendor/html-to-markdown-rs/src/converter/reference_collector.rs +0 -69
  90. data/vendor/html-to-markdown-rs/src/converter/semantic/attributes.rs +0 -269
  91. data/vendor/html-to-markdown-rs/src/converter/semantic/definition_list.rs +0 -266
  92. data/vendor/html-to-markdown-rs/src/converter/semantic/figure.rs +0 -391
  93. data/vendor/html-to-markdown-rs/src/converter/semantic/mod.rs +0 -112
  94. data/vendor/html-to-markdown-rs/src/converter/semantic/sectioning.rs +0 -85
  95. data/vendor/html-to-markdown-rs/src/converter/semantic/summary.rs +0 -324
  96. data/vendor/html-to-markdown-rs/src/converter/text/mod.rs +0 -8
  97. data/vendor/html-to-markdown-rs/src/converter/text/processing.rs +0 -56
  98. data/vendor/html-to-markdown-rs/src/converter/text_node.rs +0 -269
  99. data/vendor/html-to-markdown-rs/src/converter/utility/attributes.rs +0 -151
  100. data/vendor/html-to-markdown-rs/src/converter/utility/caching.rs +0 -74
  101. data/vendor/html-to-markdown-rs/src/converter/utility/content.rs +0 -271
  102. data/vendor/html-to-markdown-rs/src/converter/utility/mod.rs +0 -17
  103. data/vendor/html-to-markdown-rs/src/converter/utility/preprocessing.rs +0 -1002
  104. data/vendor/html-to-markdown-rs/src/converter/utility/serialization.rs +0 -126
  105. data/vendor/html-to-markdown-rs/src/converter/utility/siblings.rs +0 -97
  106. data/vendor/html-to-markdown-rs/src/converter/visitor_hooks.rs +0 -189
  107. data/vendor/html-to-markdown-rs/src/error.rs +0 -43
  108. data/vendor/html-to-markdown-rs/src/exports.rs +0 -24
  109. data/vendor/html-to-markdown-rs/src/inline_images.rs +0 -336
  110. data/vendor/html-to-markdown-rs/src/lib.rs +0 -139
  111. data/vendor/html-to-markdown-rs/src/metadata/collector.rs +0 -457
  112. data/vendor/html-to-markdown-rs/src/metadata/config.rs +0 -394
  113. data/vendor/html-to-markdown-rs/src/metadata/extraction.rs +0 -398
  114. data/vendor/html-to-markdown-rs/src/metadata/mod.rs +0 -288
  115. data/vendor/html-to-markdown-rs/src/metadata/types.rs +0 -477
  116. data/vendor/html-to-markdown-rs/src/options/conversion.rs +0 -559
  117. data/vendor/html-to-markdown-rs/src/options/inline_image.rs +0 -111
  118. data/vendor/html-to-markdown-rs/src/options/mod.rs +0 -20
  119. data/vendor/html-to-markdown-rs/src/options/preprocessing.rs +0 -201
  120. data/vendor/html-to-markdown-rs/src/options/validation.rs +0 -416
  121. data/vendor/html-to-markdown-rs/src/prelude.rs +0 -1
  122. data/vendor/html-to-markdown-rs/src/rcdom.rs +0 -487
  123. data/vendor/html-to-markdown-rs/src/text.rs +0 -358
  124. data/vendor/html-to-markdown-rs/src/types/document.rs +0 -191
  125. data/vendor/html-to-markdown-rs/src/types/mod.rs +0 -17
  126. data/vendor/html-to-markdown-rs/src/types/result.rs +0 -54
  127. data/vendor/html-to-markdown-rs/src/types/structure_builder.rs +0 -791
  128. data/vendor/html-to-markdown-rs/src/types/structure_collector.rs +0 -483
  129. data/vendor/html-to-markdown-rs/src/types/tables.rs +0 -52
  130. data/vendor/html-to-markdown-rs/src/types/warnings.rs +0 -33
  131. data/vendor/html-to-markdown-rs/src/validation.rs +0 -158
  132. data/vendor/html-to-markdown-rs/src/visitor/default_impl.rs +0 -63
  133. data/vendor/html-to-markdown-rs/src/visitor/mod.rs +0 -41
  134. data/vendor/html-to-markdown-rs/src/visitor/traits.rs +0 -370
  135. data/vendor/html-to-markdown-rs/src/visitor/types.rs +0 -319
  136. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/callbacks/mod.rs +0 -1
  137. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/content.rs +0 -126
  138. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/mod.rs +0 -27
  139. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/state.rs +0 -110
  140. data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/traversal.rs +0 -250
  141. data/vendor/html-to-markdown-rs/src/visitor_helpers.rs +0 -597
  142. data/vendor/html-to-markdown-rs/src/wrapper/sync.rs +0 -413
  143. data/vendor/html-to-markdown-rs/src/wrapper/utils.rs +0 -290
  144. data/vendor/html-to-markdown-rs/src/wrapper.rs +0 -9
  145. data/vendor/html-to-markdown-rs/tests/br_in_inline_test.rs +0 -87
  146. data/vendor/html-to-markdown-rs/tests/commonmark_compliance_test.rs +0 -297
  147. data/vendor/html-to-markdown-rs/tests/djot_output_test.rs +0 -153
  148. data/vendor/html-to-markdown-rs/tests/exclude_selectors_test.rs +0 -132
  149. data/vendor/html-to-markdown-rs/tests/integration_test.rs +0 -631
  150. data/vendor/html-to-markdown-rs/tests/issue_121_regressions.rs +0 -49
  151. data/vendor/html-to-markdown-rs/tests/issue_127_regressions.rs +0 -58
  152. data/vendor/html-to-markdown-rs/tests/issue_128_regressions.rs +0 -17
  153. data/vendor/html-to-markdown-rs/tests/issue_131_regressions.rs +0 -41
  154. data/vendor/html-to-markdown-rs/tests/issue_134_regressions.rs +0 -40
  155. data/vendor/html-to-markdown-rs/tests/issue_139_regressions.rs +0 -26
  156. data/vendor/html-to-markdown-rs/tests/issue_140_regressions.rs +0 -185
  157. data/vendor/html-to-markdown-rs/tests/issue_143_regressions.rs +0 -100
  158. data/vendor/html-to-markdown-rs/tests/issue_145_regressions.rs +0 -133
  159. data/vendor/html-to-markdown-rs/tests/issue_146_regressions.rs +0 -144
  160. data/vendor/html-to-markdown-rs/tests/issue_176_regressions.rs +0 -62
  161. data/vendor/html-to-markdown-rs/tests/issue_190_regressions.rs +0 -128
  162. data/vendor/html-to-markdown-rs/tests/issue_199_regressions.rs +0 -20
  163. data/vendor/html-to-markdown-rs/tests/issue_200_regressions.rs +0 -62
  164. data/vendor/html-to-markdown-rs/tests/issue_212_regressions.rs +0 -68
  165. data/vendor/html-to-markdown-rs/tests/issue_216_217_regressions.rs +0 -87
  166. data/vendor/html-to-markdown-rs/tests/issue_336_regressions.rs +0 -74
  167. data/vendor/html-to-markdown-rs/tests/issue_339_regressions.rs +0 -92
  168. data/vendor/html-to-markdown-rs/tests/issue_347_regressions.rs +0 -154
  169. data/vendor/html-to-markdown-rs/tests/issue_348_visitor_plain.rs +0 -93
  170. data/vendor/html-to-markdown-rs/tests/json_ld_script_extraction.rs +0 -44
  171. data/vendor/html-to-markdown-rs/tests/lists_test.rs +0 -199
  172. data/vendor/html-to-markdown-rs/tests/plain_output_test.rs +0 -273
  173. data/vendor/html-to-markdown-rs/tests/preprocessing_tests.rs +0 -61
  174. data/vendor/html-to-markdown-rs/tests/reference_links_test.rs +0 -169
  175. data/vendor/html-to-markdown-rs/tests/sectioning_elements_test.rs +0 -137
  176. data/vendor/html-to-markdown-rs/tests/skip_images_test.rs +0 -522
  177. data/vendor/html-to-markdown-rs/tests/tables_test.rs +0 -743
  178. data/vendor/html-to-markdown-rs/tests/test_custom_elements.rs +0 -41
  179. data/vendor/html-to-markdown-rs/tests/test_issue_187.rs +0 -204
  180. data/vendor/html-to-markdown-rs/tests/test_issue_218.rs +0 -68
  181. data/vendor/html-to-markdown-rs/tests/test_issue_277.rs +0 -77
  182. data/vendor/html-to-markdown-rs/tests/test_max_depth.rs +0 -82
  183. data/vendor/html-to-markdown-rs/tests/test_nested_simple.rs +0 -45
  184. data/vendor/html-to-markdown-rs/tests/test_script_style_stripping.rs +0 -396
  185. data/vendor/html-to-markdown-rs/tests/test_spa_bisect.rs +0 -34
  186. data/vendor/html-to-markdown-rs/tests/visitor_code_integration_test.rs +0 -121
  187. data/vendor/html-to-markdown-rs/tests/visitor_integration_test.rs +0 -1190
  188. data/vendor/html-to-markdown-rs/tests/xml_tables_test.rs +0 -372
@@ -1,266 +0,0 @@
1
- //! Handlers for HTML5 definition list and heading group elements.
2
- //!
3
- //! Processes list and heading semantic elements:
4
- //! - `<hgroup>` - Groups related headings together
5
- //! - `<dl>` - Definition list container
6
- //! - `<dt>` - Definition term
7
- //! - `<dd>` - Definition description
8
- //! - `<menu>` - Semantic list (typically unordered)
9
- //!
10
- //! These elements have special formatting requirements for proper Markdown output.
11
-
12
- // Note: Context and DomContext are defined in converter.rs
13
- // walk_node is also defined there and must be called via the parent module
14
- use super::walk_node;
15
-
16
- /// Handles the `<hgroup>` element.
17
- ///
18
- /// An hgroup element groups related headings together (e.g., a title and subtitle).
19
- /// In Markdown, we simply process all children sequentially, allowing nested
20
- /// headings to maintain their individual formatting.
21
- ///
22
- /// # Behavior
23
- ///
24
- /// - Children are processed sequentially in the current context
25
- /// - No special formatting is applied at the hgroup level
26
- pub fn handle_hgroup(
27
- _tag_name: &str,
28
- node_handle: &tl::NodeHandle,
29
- parser: &tl::Parser,
30
- output: &mut String,
31
- options: &crate::options::ConversionOptions,
32
- ctx: &super::Context,
33
- depth: usize,
34
- dom_ctx: &super::DomContext,
35
- ) {
36
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
37
- let children = tag.children();
38
- {
39
- for child_handle in children.top().iter() {
40
- walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);
41
- }
42
- }
43
- }
44
- }
45
-
46
- /// Handles the `<dl>` element.
47
- ///
48
- /// A definition list contains terms and their definitions. Terms and definitions
49
- /// are output as plain blocks without Pandoc-style colon syntax, since standard
50
- /// Markdown and GFM do not support definition lists.
51
- ///
52
- /// # Behavior
53
- ///
54
- /// - **Inline mode**: Children are processed inline without block spacing
55
- /// - **Block mode**: Content is collected and wrapped with proper spacing
56
- pub fn handle_dl(
57
- _tag_name: &str,
58
- node_handle: &tl::NodeHandle,
59
- parser: &tl::Parser,
60
- output: &mut String,
61
- options: &crate::options::ConversionOptions,
62
- ctx: &super::Context,
63
- depth: usize,
64
- dom_ctx: &super::DomContext,
65
- ) {
66
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
67
- // In inline context, just process children inline
68
- if ctx.convert_as_inline {
69
- let children = tag.children();
70
- {
71
- for child_handle in children.top().iter() {
72
- walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);
73
- }
74
- }
75
- return;
76
- }
77
-
78
- // Collect content from children
79
- let mut content = String::new();
80
- let children = tag.children();
81
- {
82
- for child_handle in children.top().iter() {
83
- walk_node(child_handle, parser, &mut content, options, ctx, depth, dom_ctx);
84
- }
85
- }
86
-
87
- // Output collected content with proper spacing
88
- let trimmed = content.trim();
89
- if !trimmed.is_empty() {
90
- if !output.is_empty() && !output.ends_with("\n\n") {
91
- output.push_str("\n\n");
92
- }
93
- output.push_str(trimmed);
94
- output.push_str("\n\n");
95
- }
96
- }
97
- }
98
-
99
- /// Handles the `<dt>` element.
100
- ///
101
- /// A dt element contains a term being defined. Terms are output on their own line,
102
- /// with definitions following on subsequent lines.
103
- ///
104
- /// # Behavior
105
- ///
106
- /// - **Inline mode**: Content is output as-is
107
- /// - **Block mode**: Content is followed by a newline
108
- pub fn handle_dt(
109
- _tag_name: &str,
110
- node_handle: &tl::NodeHandle,
111
- parser: &tl::Parser,
112
- output: &mut String,
113
- options: &crate::options::ConversionOptions,
114
- ctx: &super::Context,
115
- depth: usize,
116
- dom_ctx: &super::DomContext,
117
- ) {
118
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
119
- let mut content = String::with_capacity(64);
120
- let children = tag.children();
121
- {
122
- for child_handle in children.top().iter() {
123
- walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);
124
- }
125
- }
126
-
127
- let trimmed = content.trim();
128
- if !trimmed.is_empty() {
129
- if ctx.convert_as_inline {
130
- output.push_str(trimmed);
131
- } else {
132
- output.push_str(trimmed);
133
- output.push('\n');
134
- }
135
- }
136
- }
137
- }
138
-
139
- /// Handles the `<dd>` element.
140
- ///
141
- /// A dd element contains the definition for a term. It is output as a plain
142
- /// block since standard Markdown and GFM do not support definition list syntax.
143
- ///
144
- /// # Behavior
145
- ///
146
- /// - **Inline mode**: Content is output as-is
147
- /// - **Block mode**: Content is output as a block
148
- pub fn handle_dd(
149
- _tag_name: &str,
150
- node_handle: &tl::NodeHandle,
151
- parser: &tl::Parser,
152
- output: &mut String,
153
- options: &crate::options::ConversionOptions,
154
- ctx: &super::Context,
155
- depth: usize,
156
- dom_ctx: &super::DomContext,
157
- ) {
158
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
159
- let mut content = String::with_capacity(128);
160
- let children = tag.children();
161
- {
162
- for child_handle in children.top().iter() {
163
- walk_node(child_handle, parser, &mut content, options, ctx, depth + 1, dom_ctx);
164
- }
165
- }
166
-
167
- let trimmed = content.trim();
168
-
169
- if ctx.convert_as_inline {
170
- if !trimmed.is_empty() {
171
- output.push_str(trimmed);
172
- }
173
- } else if !trimmed.is_empty() {
174
- output.push_str(trimmed);
175
- output.push_str("\n\n");
176
- }
177
- }
178
- }
179
-
180
- /// Handles the `<menu>` element.
181
- ///
182
- /// A menu element is a semantic list, typically used for command menus or
183
- /// navigation. It is rendered as an unordered list with dashes.
184
- ///
185
- /// # Behavior
186
- ///
187
- /// - **Inline mode**: Children are processed inline without list formatting
188
- /// - **Block mode**: Content is rendered as an unordered list
189
- /// - Uses `-` as the list bullet (overrides configured bullets)
190
- /// - Proper blank-line spacing is maintained
191
- pub fn handle_menu(
192
- _tag_name: &str,
193
- node_handle: &tl::NodeHandle,
194
- parser: &tl::Parser,
195
- output: &mut String,
196
- options: &crate::options::ConversionOptions,
197
- ctx: &super::Context,
198
- depth: usize,
199
- dom_ctx: &super::DomContext,
200
- ) {
201
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
202
- let content_start = output.len();
203
-
204
- // Create options with menu-specific bullet style
205
- let menu_options = crate::options::ConversionOptions {
206
- bullets: "-".to_string(),
207
- ..options.clone()
208
- };
209
-
210
- // Create context for list rendering
211
- let list_ctx = super::Context {
212
- in_ordered_list: false,
213
- list_counter: 0,
214
- in_list: true,
215
- list_depth: ctx.list_depth,
216
- ..ctx.clone()
217
- };
218
-
219
- let children = tag.children();
220
- {
221
- for child_handle in children.top().iter() {
222
- walk_node(child_handle, parser, output, &menu_options, &list_ctx, depth, dom_ctx);
223
- }
224
- }
225
-
226
- // Ensure proper spacing after menu
227
- if !ctx.convert_as_inline && output.len() > content_start {
228
- if !output.ends_with("\n\n") {
229
- if output.ends_with('\n') {
230
- output.push('\n');
231
- } else {
232
- output.push_str("\n\n");
233
- }
234
- }
235
- } else if ctx.convert_as_inline {
236
- // In inline mode, remove trailing newlines
237
- while output.ends_with('\n') {
238
- output.pop();
239
- }
240
- }
241
- }
242
- }
243
-
244
- /// Dispatcher for definition list and related elements.
245
- ///
246
- /// Routes `<hgroup>`, `<dl>`, `<dt>`, `<dd>`, and `<menu>` elements
247
- /// to their respective handlers.
248
- pub fn handle(
249
- tag_name: &str,
250
- node_handle: &tl::NodeHandle,
251
- parser: &tl::Parser,
252
- output: &mut String,
253
- options: &crate::options::ConversionOptions,
254
- ctx: &super::Context,
255
- depth: usize,
256
- dom_ctx: &super::DomContext,
257
- ) {
258
- match tag_name {
259
- "hgroup" => handle_hgroup(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
260
- "dl" => handle_dl(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
261
- "dt" => handle_dt(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
262
- "dd" => handle_dd(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
263
- "menu" => handle_menu(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
264
- _ => {}
265
- }
266
- }
@@ -1,391 +0,0 @@
1
- //! Handlers for HTML5 figure elements.
2
- //!
3
- //! Processes figure-related semantic elements:
4
- //! - `<figure>` - Self-contained illustration, diagram, photo, code listing, etc.
5
- //! - `<figcaption>` - Caption or legend for a figure
6
- //!
7
- //! Figure elements group an image (or other media) with its associated caption.
8
- //! The Markdown output preserves this relationship through content organization.
9
-
10
- // Note: Context and DomContext are defined in converter.rs
11
- // walk_node is also defined there and must be called via the parent module
12
-
13
- /// Handles the `<figure>` element.
14
- ///
15
- /// A figure element contains content (typically images) and optionally a figcaption.
16
- /// The handler collects all content and cleans up extra line breaks.
17
- ///
18
- /// # Behavior
19
- ///
20
- /// - **Inline mode**: Children are processed inline without block spacing
21
- /// - **Block mode**: Content is collected, line breaks normalized, and wrapped with blank lines
22
- /// - **Image normalization**: Removes extra spaces before `![` to improve Markdown formatting
23
- ///
24
- /// # Implementation Details
25
- ///
26
- /// The handler performs the following on the collected content:
27
- /// 1. Normalizes newline + image sequences: `\n![` → `![`
28
- /// 2. Normalizes space + image sequences: ` ![` → `![`
29
- /// 3. Trims the final content and wraps it with blank lines
30
- #[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
31
- pub fn handle_figure(
32
- _tag_name: &str,
33
- node_handle: &tl::NodeHandle,
34
- parser: &tl::Parser,
35
- output: &mut String,
36
- options: &crate::options::ConversionOptions,
37
- ctx: &super::Context,
38
- depth: usize,
39
- dom_ctx: &super::DomContext,
40
- ) {
41
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
42
- // In inline context, just process children inline
43
- if ctx.convert_as_inline {
44
- let children = tag.children();
45
- {
46
- for child_handle in children.top().iter() {
47
- super::walk_node(child_handle, parser, output, options, ctx, depth, dom_ctx);
48
- }
49
- }
50
- return;
51
- }
52
-
53
- // Check visit_figure_start before processing children.
54
- #[cfg(feature = "visitor")]
55
- if let Some(ref visitor_handle) = ctx.visitor {
56
- use crate::converter::utility::content::collect_tag_attributes;
57
- use crate::visitor::{NodeContext, NodeType, VisitResult};
58
-
59
- let attributes = collect_tag_attributes(tag);
60
- let node_id = node_handle.get_inner();
61
- let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
62
- let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
63
- let node_ctx = NodeContext {
64
- node_type: NodeType::Figure,
65
- tag_name: "figure".to_string(),
66
- attributes,
67
- depth,
68
- index_in_parent,
69
- parent_tag,
70
- is_inline: false,
71
- };
72
- let visit_result = {
73
- let mut visitor = visitor_handle.borrow_mut();
74
- visitor.visit_figure_start(&node_ctx)
75
- };
76
- match visit_result {
77
- VisitResult::Continue => {}
78
- VisitResult::Skip => return,
79
- VisitResult::Custom(custom) => {
80
- // Record where custom content starts so visit_figure_end can see it
81
- let start_pos = output.len();
82
- if !output.is_empty() && !output.ends_with("\n\n") {
83
- output.push_str("\n\n");
84
- }
85
- output.push_str(&custom);
86
- // Still call visit_figure_end with the custom content
87
- let safe_start = start_pos.min(output.len());
88
- let figure_output = output[safe_start..].to_owned();
89
- let end_result = {
90
- let mut visitor = visitor_handle.borrow_mut();
91
- visitor.visit_figure_end(&node_ctx, &figure_output)
92
- };
93
- match end_result {
94
- VisitResult::Continue => {
95
- if !output.ends_with('\n') {
96
- output.push_str("\n\n");
97
- } else if !output.ends_with("\n\n") {
98
- output.push('\n');
99
- }
100
- }
101
- VisitResult::Custom(end_custom) => {
102
- output.truncate(safe_start);
103
- output.push_str(&end_custom);
104
- }
105
- VisitResult::Skip => {
106
- output.truncate(safe_start);
107
- }
108
- VisitResult::PreserveHtml => {
109
- use crate::converter::utility::serialization::serialize_node;
110
- output.truncate(safe_start);
111
- output.push_str(&serialize_node(node_handle, parser));
112
- }
113
- VisitResult::Error(err) => {
114
- if ctx.visitor_error.borrow().is_none() {
115
- *ctx.visitor_error.borrow_mut() = Some(err);
116
- }
117
- }
118
- }
119
- return;
120
- }
121
- VisitResult::PreserveHtml => {
122
- use crate::converter::utility::serialization::serialize_node;
123
- output.push_str(&serialize_node(node_handle, parser));
124
- return;
125
- }
126
- VisitResult::Error(err) => {
127
- if ctx.visitor_error.borrow().is_none() {
128
- *ctx.visitor_error.borrow_mut() = Some(err);
129
- }
130
- return;
131
- }
132
- }
133
- }
134
-
135
- // Ensure spacing before the figure
136
- if !output.is_empty() && !output.ends_with("\n\n") {
137
- output.push_str("\n\n");
138
- }
139
-
140
- let figure_start = output.len();
141
-
142
- // Collect content in a separate buffer
143
- let mut figure_content = String::new();
144
- let children = tag.children();
145
- {
146
- for child_handle in children.top().iter() {
147
- super::walk_node(child_handle, parser, &mut figure_content, options, ctx, depth, dom_ctx);
148
- }
149
- }
150
-
151
- // Normalize image syntax
152
- figure_content = figure_content.replace("\n![", "![");
153
- figure_content = figure_content.replace(" ![", "![");
154
-
155
- // Trim and output
156
- let trimmed = figure_content.trim_matches(|c| c == '\n' || c == ' ' || c == '\t');
157
- if !trimmed.is_empty() {
158
- output.push_str(trimmed);
159
- if !output.ends_with('\n') {
160
- output.push('\n');
161
- }
162
- if !output.ends_with("\n\n") {
163
- output.push('\n');
164
- }
165
- }
166
-
167
- // Call visit_figure_end after collecting content.
168
- #[cfg(feature = "visitor")]
169
- if let Some(ref visitor_handle) = ctx.visitor {
170
- use crate::converter::utility::content::collect_tag_attributes;
171
- use crate::visitor::{NodeContext, NodeType, VisitResult};
172
-
173
- let attributes = collect_tag_attributes(tag);
174
- let node_id = node_handle.get_inner();
175
- let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
176
- let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
177
- let node_ctx = NodeContext {
178
- node_type: NodeType::Figure,
179
- tag_name: "figure".to_string(),
180
- attributes,
181
- depth,
182
- index_in_parent,
183
- parent_tag,
184
- is_inline: false,
185
- };
186
- let safe_start = figure_start.min(output.len());
187
- let figure_output = output[safe_start..].to_owned();
188
- let visit_result = {
189
- let mut visitor = visitor_handle.borrow_mut();
190
- visitor.visit_figure_end(&node_ctx, &figure_output)
191
- };
192
- match visit_result {
193
- VisitResult::Continue => {}
194
- VisitResult::Skip => {
195
- output.truncate(safe_start);
196
- }
197
- VisitResult::Custom(custom) => {
198
- output.truncate(safe_start);
199
- output.push_str(&custom);
200
- }
201
- VisitResult::PreserveHtml => {
202
- use crate::converter::utility::serialization::serialize_node;
203
- output.truncate(safe_start);
204
- output.push_str(&serialize_node(node_handle, parser));
205
- }
206
- VisitResult::Error(err) => {
207
- if ctx.visitor_error.borrow().is_none() {
208
- *ctx.visitor_error.borrow_mut() = Some(err);
209
- }
210
- }
211
- }
212
- }
213
- }
214
- }
215
-
216
- /// Handles the `<figcaption>` element.
217
- ///
218
- /// A figcaption element contains text that describes or supplements the figure.
219
- /// It is rendered as emphasized (italic) text to distinguish it from regular content.
220
- ///
221
- /// # Behavior
222
- ///
223
- /// - Content is collected and trimmed
224
- /// - Non-empty content is wrapped in `*text*` (emphasis) markers
225
- /// - Proper spacing is maintained around the caption
226
- ///
227
- /// # Implementation Details
228
- ///
229
- /// The handler:
230
- /// 1. Collects and processes all children
231
- /// 2. Checks for existing output and adds spacing as needed
232
- /// 3. Wraps content in emphasis markers: `*caption*`
233
- /// 4. Ensures proper blank-line spacing after the caption
234
- #[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
235
- pub fn handle_figcaption(
236
- _tag_name: &str,
237
- node_handle: &tl::NodeHandle,
238
- parser: &tl::Parser,
239
- output: &mut String,
240
- options: &crate::options::ConversionOptions,
241
- ctx: &super::Context,
242
- depth: usize,
243
- dom_ctx: &super::DomContext,
244
- ) {
245
- if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
246
- let mut text = String::new();
247
- let children = tag.children();
248
- {
249
- for child_handle in children.top().iter() {
250
- super::walk_node(child_handle, parser, &mut text, options, ctx, depth + 1, dom_ctx);
251
- }
252
- }
253
-
254
- let text = text.trim().to_owned();
255
- if text.is_empty() {
256
- return;
257
- }
258
-
259
- #[cfg(feature = "visitor")]
260
- if let Some(ref visitor_handle) = ctx.visitor {
261
- use crate::converter::utility::content::collect_tag_attributes;
262
- use crate::visitor::{NodeContext, NodeType, VisitResult};
263
-
264
- let attributes = collect_tag_attributes(tag);
265
- let node_id = node_handle.get_inner();
266
- let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
267
- let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
268
- let node_ctx = NodeContext {
269
- node_type: NodeType::Figcaption,
270
- tag_name: "figcaption".to_string(),
271
- attributes,
272
- depth,
273
- index_in_parent,
274
- parent_tag,
275
- is_inline: false,
276
- };
277
- let visit_result = {
278
- let mut visitor = visitor_handle.borrow_mut();
279
- visitor.visit_figcaption(&node_ctx, &text)
280
- };
281
- match visit_result {
282
- VisitResult::Continue => {}
283
- VisitResult::Skip => return,
284
- VisitResult::Custom(custom) => {
285
- // Add spacing before caption if needed
286
- if !output.is_empty() {
287
- if output.ends_with("```\n") {
288
- output.push('\n');
289
- } else {
290
- while output.ends_with(' ') || output.ends_with('\t') {
291
- output.pop();
292
- }
293
- if output.ends_with('\n') && !output.ends_with("\n\n") {
294
- output.push('\n');
295
- } else if !output.ends_with('\n') {
296
- output.push_str("\n\n");
297
- }
298
- }
299
- }
300
- output.push_str(&custom);
301
- if !custom.ends_with('\n') {
302
- output.push_str("\n\n");
303
- }
304
- return;
305
- }
306
- VisitResult::PreserveHtml => {
307
- use crate::converter::utility::serialization::serialize_node;
308
- output.push_str(&serialize_node(node_handle, parser));
309
- return;
310
- }
311
- VisitResult::Error(err) => {
312
- if ctx.visitor_error.borrow().is_none() {
313
- *ctx.visitor_error.borrow_mut() = Some(err);
314
- }
315
- return;
316
- }
317
- }
318
- }
319
-
320
- // Add spacing before caption if needed
321
- if !output.is_empty() {
322
- if output.ends_with("```\n") {
323
- output.push('\n');
324
- } else {
325
- // Trim trailing whitespace and ensure single blank line
326
- while output.ends_with(' ') || output.ends_with('\t') {
327
- output.pop();
328
- }
329
- if output.ends_with('\n') && !output.ends_with("\n\n") {
330
- output.push('\n');
331
- } else if !output.ends_with('\n') {
332
- output.push_str("\n\n");
333
- }
334
- }
335
- }
336
-
337
- // Output caption as emphasized text
338
- output.push('*');
339
- output.push_str(&text);
340
- output.push_str("*\n\n");
341
- }
342
- }
343
-
344
- /// Dispatcher for figure-related elements.
345
- ///
346
- /// Routes `<figure>` and `<figcaption>` elements to their respective handlers.
347
- pub fn handle(
348
- tag_name: &str,
349
- node_handle: &tl::NodeHandle,
350
- parser: &tl::Parser,
351
- output: &mut String,
352
- options: &crate::options::ConversionOptions,
353
- ctx: &super::Context,
354
- depth: usize,
355
- dom_ctx: &super::DomContext,
356
- ) {
357
- match tag_name {
358
- "figure" => handle_figure(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
359
- "figcaption" => handle_figcaption(tag_name, node_handle, parser, output, options, ctx, depth, dom_ctx),
360
- _ => {}
361
- }
362
- }
363
-
364
- #[cfg(test)]
365
- mod tests {
366
- #[test]
367
- fn figure_caption_separated_from_image() {
368
- let html = r#"<figure><img src="photo.jpg" alt="Photo"><figcaption>A nice photo</figcaption></figure>"#;
369
- let result = crate::convert(html, None).unwrap();
370
- let content = result.content.unwrap_or_default();
371
- assert!(
372
- content.contains("![Photo](photo.jpg)"),
373
- "image should be present: {}",
374
- content
375
- );
376
- assert!(
377
- content.contains("A nice photo"),
378
- "caption should be present: {}",
379
- content
380
- );
381
- // Image and caption should not be on the same line
382
- let lines: Vec<&str> = content.lines().filter(|l| !l.trim().is_empty()).collect();
383
- let img_line = lines.iter().position(|l| l.contains("![")).unwrap_or(999);
384
- let cap_line = lines.iter().position(|l| l.contains("A nice photo")).unwrap_or(999);
385
- assert!(
386
- cap_line > img_line,
387
- "caption should be on a separate line after image, lines: {:?}",
388
- lines
389
- );
390
- }
391
- }