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,413 +0,0 @@
1
- //! Synchronous text wrapping for Markdown output.
2
-
3
- use super::utils::{
4
- is_heading, is_list_like, is_numbered_list, parse_blockquote_line, parse_list_item, wrap_blockquote_paragraph,
5
- wrap_line, wrap_list_item,
6
- };
7
- use crate::options::ConversionOptions;
8
-
9
- /// Wrap text at specified width while preserving Markdown formatting.
10
- ///
11
- /// This function wraps paragraphs of text at the specified width, but:
12
- /// - Does not break long words
13
- /// - Does not break on hyphens
14
- /// - Preserves Markdown formatting (links, bold, etc.)
15
- /// - Only wraps paragraph content, not headers, lists, code blocks, etc.
16
- #[must_use]
17
- #[allow(clippy::too_many_lines)]
18
- pub fn wrap_markdown(markdown: &str, options: &ConversionOptions) -> String {
19
- if !options.wrap {
20
- return markdown.to_string();
21
- }
22
-
23
- let mut result = String::with_capacity(markdown.len());
24
- let mut in_code_block = false;
25
- let mut in_paragraph = false;
26
- let mut paragraph_buffer = String::new();
27
- let mut in_blockquote_paragraph = false;
28
- let mut blockquote_prefix = String::new();
29
- let mut blockquote_buffer = String::new();
30
-
31
- for line in markdown.lines() {
32
- let trimmed = line.trim_start();
33
- let is_code_fence = trimmed.starts_with("```");
34
- let is_indented_code = line.starts_with(" ")
35
- && !is_list_like(trimmed)
36
- && !is_numbered_list(trimmed)
37
- && !is_heading(trimmed)
38
- && !trimmed.starts_with('>')
39
- && !trimmed.starts_with('|');
40
-
41
- if is_code_fence || is_indented_code {
42
- if in_paragraph && !paragraph_buffer.is_empty() {
43
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
44
- result.push_str("\n\n");
45
- paragraph_buffer.clear();
46
- in_paragraph = false;
47
- }
48
-
49
- if is_code_fence {
50
- in_code_block = !in_code_block;
51
- }
52
- result.push_str(line);
53
- result.push('\n');
54
- continue;
55
- }
56
-
57
- if in_code_block {
58
- result.push_str(line);
59
- result.push('\n');
60
- continue;
61
- }
62
-
63
- if let Some((prefix, content)) = parse_blockquote_line(line) {
64
- if in_paragraph && !paragraph_buffer.is_empty() {
65
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
66
- result.push_str("\n\n");
67
- paragraph_buffer.clear();
68
- in_paragraph = false;
69
- }
70
-
71
- let mut normalized_prefix = prefix;
72
- if !normalized_prefix.ends_with(' ') {
73
- normalized_prefix.push(' ');
74
- }
75
-
76
- if content.is_empty() {
77
- if in_blockquote_paragraph && !blockquote_buffer.is_empty() {
78
- result.push_str(&wrap_blockquote_paragraph(
79
- &blockquote_prefix,
80
- &blockquote_buffer,
81
- options.wrap_width,
82
- ));
83
- result.push('\n');
84
- blockquote_buffer.clear();
85
- in_blockquote_paragraph = false;
86
- }
87
- result.push_str(normalized_prefix.trim_end());
88
- result.push('\n');
89
- continue;
90
- }
91
-
92
- if in_blockquote_paragraph && normalized_prefix != blockquote_prefix {
93
- result.push_str(&wrap_blockquote_paragraph(
94
- &blockquote_prefix,
95
- &blockquote_buffer,
96
- options.wrap_width,
97
- ));
98
- result.push('\n');
99
- blockquote_buffer.clear();
100
- in_blockquote_paragraph = false;
101
- }
102
-
103
- if in_blockquote_paragraph {
104
- blockquote_buffer.push(' ');
105
- blockquote_buffer.push_str(&content);
106
- } else {
107
- blockquote_prefix = normalized_prefix;
108
- blockquote_buffer.push_str(&content);
109
- in_blockquote_paragraph = true;
110
- }
111
- continue;
112
- } else if in_blockquote_paragraph && !blockquote_buffer.is_empty() {
113
- result.push_str(&wrap_blockquote_paragraph(
114
- &blockquote_prefix,
115
- &blockquote_buffer,
116
- options.wrap_width,
117
- ));
118
- result.push('\n');
119
- blockquote_buffer.clear();
120
- in_blockquote_paragraph = false;
121
- }
122
-
123
- if let Some((indent, marker, content)) = parse_list_item(line) {
124
- if in_paragraph && !paragraph_buffer.is_empty() {
125
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
126
- result.push_str("\n\n");
127
- paragraph_buffer.clear();
128
- in_paragraph = false;
129
- }
130
-
131
- result.push_str(&wrap_list_item(&indent, &marker, &content, options.wrap_width));
132
- continue;
133
- }
134
-
135
- let is_structural =
136
- is_heading(trimmed) || trimmed.starts_with('>') || trimmed.starts_with('|') || trimmed.starts_with('=');
137
-
138
- if is_structural {
139
- if in_paragraph && !paragraph_buffer.is_empty() {
140
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
141
- result.push_str("\n\n");
142
- paragraph_buffer.clear();
143
- in_paragraph = false;
144
- }
145
-
146
- result.push_str(line);
147
- result.push('\n');
148
- continue;
149
- }
150
-
151
- if line.trim().is_empty() {
152
- if in_paragraph && !paragraph_buffer.is_empty() {
153
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
154
- result.push_str("\n\n");
155
- paragraph_buffer.clear();
156
- in_paragraph = false;
157
- } else if !in_paragraph {
158
- result.push('\n');
159
- }
160
- continue;
161
- }
162
-
163
- if in_paragraph {
164
- paragraph_buffer.push(' ');
165
- }
166
- paragraph_buffer.push_str(line.trim());
167
- in_paragraph = true;
168
- }
169
-
170
- if in_blockquote_paragraph && !blockquote_buffer.is_empty() {
171
- result.push_str(&wrap_blockquote_paragraph(
172
- &blockquote_prefix,
173
- &blockquote_buffer,
174
- options.wrap_width,
175
- ));
176
- result.push('\n');
177
- }
178
-
179
- if in_paragraph && !paragraph_buffer.is_empty() {
180
- result.push_str(&wrap_line(&paragraph_buffer, options.wrap_width));
181
- result.push_str("\n\n");
182
- }
183
-
184
- result
185
- }
186
-
187
- #[cfg(test)]
188
- mod tests {
189
- use super::*;
190
-
191
- #[test]
192
- fn test_wrap_markdown_disabled() {
193
- let markdown = "This is a very long line that would normally be wrapped at 40 characters";
194
- let options = ConversionOptions {
195
- wrap: false,
196
- ..Default::default()
197
- };
198
- let result = wrap_markdown(markdown, &options);
199
- assert_eq!(result, markdown);
200
- }
201
-
202
- #[test]
203
- fn test_wrap_markdown_paragraph() {
204
- let markdown = "This is a very long line that would normally be wrapped at 40 characters\n\n";
205
- let options = ConversionOptions {
206
- wrap: true,
207
- wrap_width: 40,
208
- ..Default::default()
209
- };
210
- let result = wrap_markdown(markdown, &options);
211
- assert!(result.lines().all(|line| line.len() <= 40 || line.trim().is_empty()));
212
- }
213
-
214
- #[test]
215
- fn test_wrap_markdown_blockquote_paragraph() {
216
- let markdown = "> This is a very long blockquote line that should wrap at 30 characters\n";
217
- let options = ConversionOptions {
218
- wrap: true,
219
- wrap_width: 30,
220
- ..Default::default()
221
- };
222
- let result = wrap_markdown(markdown, &options);
223
- assert!(
224
- result.lines().all(|line| line.len() <= 30 || line.trim().is_empty()),
225
- "Some lines exceed wrap width. Got: {}",
226
- result
227
- );
228
- assert!(
229
- result.contains("> This is a very"),
230
- "Missing expected wrapped content. Got: {}",
231
- result
232
- );
233
- assert!(
234
- result.lines().filter(|l| l.starts_with("> ")).count() >= 2,
235
- "Expected multiple wrapped blockquote lines. Got: {}",
236
- result
237
- );
238
- }
239
-
240
- #[test]
241
- fn test_wrap_markdown_preserves_code() {
242
- let markdown = "```\nThis is a very long line in a code block that should not be wrapped\n```\n";
243
- let options = ConversionOptions {
244
- wrap: true,
245
- wrap_width: 40,
246
- ..Default::default()
247
- };
248
- let result = wrap_markdown(markdown, &options);
249
- assert!(result.contains("This is a very long line in a code block that should not be wrapped"));
250
- }
251
-
252
- #[test]
253
- fn test_wrap_markdown_preserves_headings() {
254
- let markdown = "# This is a very long heading that should not be wrapped even if it exceeds the width\n\n";
255
- let options = ConversionOptions {
256
- wrap: true,
257
- wrap_width: 40,
258
- ..Default::default()
259
- };
260
- let result = wrap_markdown(markdown, &options);
261
- assert!(
262
- result.contains("# This is a very long heading that should not be wrapped even if it exceeds the width")
263
- );
264
- }
265
-
266
- #[test]
267
- fn wrap_markdown_wraps_long_list_items() {
268
- let markdown = "- This is a very long list item that should definitely be wrapped when it exceeds the specified wrap width\n- Short item\n";
269
- let options = ConversionOptions {
270
- wrap: true,
271
- wrap_width: 60,
272
- ..Default::default()
273
- };
274
-
275
- let result = wrap_markdown(markdown, &options);
276
-
277
- assert!(
278
- result.contains("- This is a very long list item that should definitely be\n wrapped"),
279
- "First list item not properly wrapped. Got: {}",
280
- result
281
- );
282
- assert!(
283
- result.contains("- Short item"),
284
- "Short list item incorrectly modified. Got: {}",
285
- result
286
- );
287
- }
288
-
289
- #[test]
290
- fn wrap_markdown_wraps_ordered_lists() {
291
- let markdown = "1. This is a numbered list item with a very long text that should be wrapped at the specified width\n2. Short\n";
292
- let options = ConversionOptions {
293
- wrap: true,
294
- wrap_width: 60,
295
- ..Default::default()
296
- };
297
-
298
- let result = wrap_markdown(markdown, &options);
299
-
300
- assert!(
301
- result.lines().all(|line| line.len() <= 60 || line.trim().is_empty()),
302
- "Some lines exceed wrap width. Got: {}",
303
- result
304
- );
305
- assert!(result.contains("1."), "Lost ordered list marker. Got: {}", result);
306
- assert!(
307
- result.contains("2."),
308
- "Lost second ordered list marker. Got: {}",
309
- result
310
- );
311
- }
312
-
313
- #[test]
314
- fn wrap_markdown_preserves_nested_list_structure() {
315
- let markdown = "- Item one with some additional text that will need to be wrapped across multiple lines\n - Nested item with long text that also needs wrapping at the specified width\n - Short nested\n";
316
- let options = ConversionOptions {
317
- wrap: true,
318
- wrap_width: 50,
319
- ..Default::default()
320
- };
321
-
322
- let result = wrap_markdown(markdown, &options);
323
-
324
- assert!(result.contains("- Item"), "Lost top-level list marker. Got: {}", result);
325
- assert!(
326
- result.contains(" - Nested"),
327
- "Lost nested list structure. Got: {}",
328
- result
329
- );
330
- assert!(
331
- result.lines().all(|line| line.len() <= 50 || line.trim().is_empty()),
332
- "Some lines exceed wrap width. Got: {}",
333
- result
334
- );
335
- }
336
-
337
- #[test]
338
- fn wrap_markdown_handles_list_with_links() {
339
- let markdown = "- [A](#a) with additional text that is long enough to require wrapping at the configured width\n - [B](#b) also has more content that needs wrapping\n - [C](#c)\n";
340
- let options = ConversionOptions {
341
- wrap: true,
342
- wrap_width: 50,
343
- ..Default::default()
344
- };
345
-
346
- let result = wrap_markdown(markdown, &options);
347
-
348
- assert!(result.contains("[A](#a)"), "Lost link in list. Got: {}", result);
349
- assert!(result.contains("[B](#b)"), "Lost nested link. Got: {}", result);
350
- assert!(result.contains("[C](#c)"), "Lost short nested link. Got: {}", result);
351
- assert!(
352
- result.contains("- [A](#a)"),
353
- "Lost list structure with link. Got: {}",
354
- result
355
- );
356
- assert!(
357
- result.contains(" - [B](#b)"),
358
- "Lost nested list structure. Got: {}",
359
- result
360
- );
361
- }
362
-
363
- #[test]
364
- fn wrap_markdown_handles_empty_list_items() {
365
- let markdown = "- \n- Item with text\n- \n";
366
- let options = ConversionOptions {
367
- wrap: true,
368
- wrap_width: 40,
369
- ..Default::default()
370
- };
371
-
372
- let result = wrap_markdown(markdown, &options);
373
-
374
- assert!(result.contains("- "), "Lost list markers. Got: {}", result);
375
- assert!(result.contains("Item with text"), "Lost item text. Got: {}", result);
376
- }
377
-
378
- #[test]
379
- fn wrap_markdown_preserves_indented_lists_with_wrapping() {
380
- let markdown = "- [A](#a) with some additional text that makes this line very long and should be wrapped\n - [B](#b)\n - [C](#c) with more text that is also quite long and needs wrapping\n";
381
- let options = ConversionOptions {
382
- wrap: true,
383
- wrap_width: 50,
384
- ..Default::default()
385
- };
386
-
387
- let result = wrap_markdown(markdown, &options);
388
-
389
- assert!(result.contains("- [A](#a)"), "Lost top-level link. Got: {}", result);
390
- assert!(result.contains(" - [B](#b)"), "Lost nested link B. Got: {}", result);
391
- assert!(result.contains(" - [C](#c)"), "Lost nested link C. Got: {}", result);
392
- assert!(
393
- result.lines().all(|line| line.len() <= 50),
394
- "Some lines exceed wrap width:\n{}",
395
- result
396
- );
397
- }
398
-
399
- #[test]
400
- fn wrap_markdown_does_not_wrap_link_only_items() {
401
- let markdown = "- [A very long link label that would exceed wrap width](#a-very-long-link-label)\n - [Nested very long link label that would also exceed](#nested)\n";
402
- let options = ConversionOptions {
403
- wrap: true,
404
- wrap_width: 30,
405
- ..Default::default()
406
- };
407
-
408
- let result = wrap_markdown(markdown, &options);
409
-
410
- assert!(result.contains("- [A very long link label that would exceed wrap width](#a-very-long-link-label)"));
411
- assert!(result.contains(" - [Nested very long link label that would also exceed](#nested)"));
412
- }
413
- }
@@ -1,290 +0,0 @@
1
- //! Utility functions for text wrapping.
2
- //!
3
- //! This module contains helper functions for parsing and wrapping Markdown elements.
4
-
5
- /// Parse a blockquote line into its prefix and content.
6
- ///
7
- /// Returns Some((prefix, content)) if the line is a blockquote, None otherwise.
8
- pub fn parse_blockquote_line(line: &str) -> Option<(String, String)> {
9
- let trimmed = line.trim_start();
10
- if !trimmed.starts_with('>') {
11
- return None;
12
- }
13
-
14
- let indent_len = line.len() - trimmed.len();
15
- let bytes = line.as_bytes();
16
- let mut i = indent_len;
17
-
18
- while i < bytes.len() {
19
- if bytes[i] != b'>' {
20
- break;
21
- }
22
- i += 1;
23
- if i < bytes.len() && bytes[i] == b' ' {
24
- i += 1;
25
- }
26
- while i + 1 < bytes.len() && bytes[i] == b' ' && bytes[i + 1] == b'>' {
27
- i += 1;
28
- }
29
- }
30
-
31
- let prefix = line[..i].to_string();
32
- let content = line[i..].trim().to_string();
33
- Some((prefix, content))
34
- }
35
-
36
- /// Wrap a blockquote paragraph while preserving its prefix.
37
- ///
38
- /// # Arguments
39
- /// - `prefix`: The blockquote prefix (e.g., "> " or "> > ")
40
- /// - `content`: The text content to wrap
41
- /// - `width`: The maximum line width
42
- pub fn wrap_blockquote_paragraph(prefix: &str, content: &str, width: usize) -> String {
43
- let prefix_len = prefix.len();
44
- let inner_width = if width > prefix_len { width - prefix_len } else { 1 };
45
-
46
- let wrapped = wrap_line(content, inner_width);
47
- let mut out = String::new();
48
- for (idx, part) in wrapped.split('\n').enumerate() {
49
- if idx > 0 {
50
- out.push('\n');
51
- }
52
- out.push_str(prefix);
53
- out.push_str(part);
54
- }
55
- out
56
- }
57
-
58
- /// Check if a line looks like an unordered list item (-, *, or +).
59
- pub fn is_list_like(trimmed: &str) -> bool {
60
- matches!(trimmed.chars().next(), Some('-' | '*' | '+'))
61
- }
62
-
63
- /// Check if a line is a numbered list item.
64
- pub fn is_numbered_list(trimmed: &str) -> bool {
65
- let token = trimmed.split_whitespace().next().unwrap_or("");
66
- if token.is_empty() || !(token.ends_with('.') || token.ends_with(')')) {
67
- return false;
68
- }
69
-
70
- let digits = token.trim_end_matches(['.', ')']);
71
- !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
72
- }
73
-
74
- /// Check if a line is a Markdown heading.
75
- pub fn is_heading(trimmed: &str) -> bool {
76
- trimmed.starts_with('#')
77
- }
78
-
79
- /// Parse a list item into its components: (indent, marker, content)
80
- ///
81
- /// Returns Some((indent, marker, content)) if the line is a valid list item,
82
- /// None otherwise.
83
- ///
84
- /// Examples:
85
- /// - "- text" -> ("", "- ", "text")
86
- /// - " - text" -> (" ", "- ", "text")
87
- /// - "1. text" -> ("", "1. ", "text")
88
- /// - " 42) text" -> (" ", "42) ", "text")
89
- pub fn parse_list_item(line: &str) -> Option<(String, String, String)> {
90
- let trimmed = line.trim_start();
91
- let indent = &line[..line.len() - trimmed.len()];
92
-
93
- if let Some(rest) = trimmed.strip_prefix('-') {
94
- if rest.starts_with(' ') || rest.is_empty() {
95
- return Some((indent.to_string(), "- ".to_string(), rest.trim_start().to_string()));
96
- }
97
- }
98
- if let Some(rest) = trimmed.strip_prefix('*') {
99
- if rest.starts_with(' ') || rest.is_empty() {
100
- return Some((indent.to_string(), "* ".to_string(), rest.trim_start().to_string()));
101
- }
102
- }
103
- if let Some(rest) = trimmed.strip_prefix('+') {
104
- if rest.starts_with(' ') || rest.is_empty() {
105
- return Some((indent.to_string(), "+ ".to_string(), rest.trim_start().to_string()));
106
- }
107
- }
108
-
109
- let first_token = trimmed.split_whitespace().next()?;
110
- if first_token.ends_with('.') || first_token.ends_with(')') {
111
- let digits = first_token.trim_end_matches(['.', ')']);
112
- if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {
113
- let marker_len = first_token.len();
114
- let rest = trimmed[marker_len..].trim_start();
115
- return Some((
116
- indent.to_string(),
117
- trimmed[..marker_len].to_string() + " ",
118
- rest.to_string(),
119
- ));
120
- }
121
- }
122
-
123
- None
124
- }
125
-
126
- /// Check if content is a single inline link (e.g., "[text](#anchor)").
127
- pub fn is_single_inline_link(content: &str) -> bool {
128
- let trimmed = content.trim();
129
- if !(trimmed.starts_with('[') && trimmed.ends_with(')')) {
130
- return false;
131
- }
132
-
133
- let Some(mid) = trimmed.find("](") else {
134
- return false;
135
- };
136
-
137
- let url_part = &trimmed[mid + 2..trimmed.len() - 1];
138
- if url_part.chars().any(char::is_whitespace) {
139
- return false;
140
- }
141
-
142
- !trimmed[mid + 2..].contains("](")
143
- }
144
-
145
- /// Wrap a single line of text at the specified width.
146
- ///
147
- /// This function wraps text without breaking long words or on hyphens,
148
- /// similar to Python's `textwrap.fill()` with `break_long_words=False` and `break_on_hyphens=False`.
149
- pub fn wrap_line(text: &str, width: usize) -> String {
150
- if text.len() <= width {
151
- return text.to_string();
152
- }
153
-
154
- let mut result = String::new();
155
- let mut current_line = String::new();
156
- let words: Vec<&str> = text.split_whitespace().collect();
157
-
158
- for word in words {
159
- if current_line.is_empty() {
160
- current_line.push_str(word);
161
- } else if current_line.len() + 1 + word.len() <= width {
162
- current_line.push(' ');
163
- current_line.push_str(word);
164
- } else {
165
- if !result.is_empty() {
166
- result.push('\n');
167
- }
168
- result.push_str(&current_line);
169
- current_line.clear();
170
- current_line.push_str(word);
171
- }
172
- }
173
-
174
- if !current_line.is_empty() {
175
- if !result.is_empty() {
176
- result.push('\n');
177
- }
178
- result.push_str(&current_line);
179
- }
180
-
181
- result
182
- }
183
-
184
- /// Wrap a list item while preserving its structure.
185
- ///
186
- /// The first line of output will be: `<indent><marker><content_start>`
187
- /// Continuation lines will be: `<indent><spaces_matching_marker><content_continued>`
188
- ///
189
- /// # Arguments
190
- /// - `indent`: The leading whitespace (for nested lists)
191
- /// - `marker`: The list marker (e.g., "- ", "1. ")
192
- /// - `content`: The text content after the marker
193
- /// - `width`: The maximum line width
194
- pub fn wrap_list_item(indent: &str, marker: &str, content: &str, width: usize) -> String {
195
- if content.is_empty() {
196
- return format!("{}{}\n", indent, marker.trim_end());
197
- }
198
-
199
- if is_single_inline_link(content) {
200
- return format!("{}{}{}\n", indent, marker, content.trim());
201
- }
202
-
203
- let full_marker = format!("{indent}{marker}");
204
- let continuation_indent = format!("{}{}", indent, " ".repeat(marker.len()));
205
-
206
- let first_line_prefix_len = full_marker.len();
207
- let first_line_width = if width > first_line_prefix_len {
208
- width - first_line_prefix_len
209
- } else {
210
- width
211
- };
212
-
213
- let cont_line_prefix_len = continuation_indent.len();
214
- let cont_line_width = if width > cont_line_prefix_len {
215
- width - cont_line_prefix_len
216
- } else {
217
- width
218
- };
219
-
220
- let words: Vec<&str> = content.split_whitespace().collect();
221
- if words.is_empty() {
222
- return format!("{}\n", full_marker.trim_end());
223
- }
224
-
225
- let mut result = String::new();
226
- let mut current_line = String::new();
227
- let mut current_width = first_line_width;
228
- let mut is_first_line = true;
229
-
230
- for word in words {
231
- let word_len = word.len();
232
- let space_needed = usize::from(!current_line.is_empty());
233
-
234
- if !current_line.is_empty() && current_line.len() + space_needed + word_len > current_width {
235
- if is_first_line {
236
- result.push_str(&full_marker);
237
- is_first_line = false;
238
- } else {
239
- result.push_str(&continuation_indent);
240
- }
241
- result.push_str(&current_line);
242
- result.push('\n');
243
- current_line.clear();
244
- current_width = cont_line_width;
245
- }
246
-
247
- if !current_line.is_empty() {
248
- current_line.push(' ');
249
- }
250
- current_line.push_str(word);
251
- }
252
-
253
- if !current_line.is_empty() {
254
- if is_first_line {
255
- result.push_str(&full_marker);
256
- } else {
257
- result.push_str(&continuation_indent);
258
- }
259
- result.push_str(&current_line);
260
- result.push('\n');
261
- }
262
-
263
- result
264
- }
265
-
266
- #[cfg(test)]
267
- mod tests {
268
- use super::*;
269
-
270
- #[test]
271
- fn test_wrap_line_short() {
272
- let text = "Short text";
273
- let wrapped = wrap_line(text, 80);
274
- assert_eq!(wrapped, "Short text");
275
- }
276
-
277
- #[test]
278
- fn test_wrap_line_long() {
279
- let text = "123456789 123456789";
280
- let wrapped = wrap_line(text, 10);
281
- assert_eq!(wrapped, "123456789\n123456789");
282
- }
283
-
284
- #[test]
285
- fn test_wrap_line_no_break_long_words() {
286
- let text = "12345678901 12345";
287
- let wrapped = wrap_line(text, 10);
288
- assert_eq!(wrapped, "12345678901\n12345");
289
- }
290
- }