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,358 +0,0 @@
1
- #![allow(clippy::cast_precision_loss, clippy::cast_sign_loss, clippy::unused_self)]
2
- //! Text processing utilities for Markdown conversion.
3
-
4
- use regex::Regex;
5
- use std::borrow::Cow;
6
- use std::sync::LazyLock;
7
-
8
- /// Regex for escaping miscellaneous characters
9
- static ESCAPE_MISC_RE: LazyLock<Regex> =
10
- LazyLock::new(|| Regex::new(r"([\\&<`\[\]>~#=+|\-])").expect("valid regex pattern"));
11
-
12
- /// Regex for escaping numbered lists
13
- static ESCAPE_NUMBERED_LIST_RE: LazyLock<Regex> =
14
- LazyLock::new(|| Regex::new(r"([0-9])([.)])").expect("valid regex pattern"));
15
-
16
- /// Regex for escaping ASCII punctuation (CommonMark spec example 12)
17
- /// Matches: `! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ \` { | } ~`
18
- static ESCAPE_ASCII_RE: LazyLock<Regex> =
19
- LazyLock::new(|| Regex::new(r"([!\x22#$%&\x27()*+,\-./:;<=>?@\[\\\]^_`{|}~])").expect("valid regex pattern"));
20
-
21
- /// Escape Markdown special characters in text.
22
- ///
23
- /// # Arguments
24
- ///
25
- /// * `text` - Text to escape
26
- /// * `escape_misc` - Escape miscellaneous characters (`\` `&` `<` `` ` `` `[` `>` `~` `#` `=` `+` `|` `-`)
27
- /// * `escape_asterisks` - Escape asterisks (`*`)
28
- /// * `escape_underscores` - Escape underscores (`_`)
29
- /// * `escape_ascii` - Escape all ASCII punctuation (for `CommonMark` spec compliance)
30
- ///
31
- /// # Returns
32
- ///
33
- /// Escaped text
34
- #[allow(clippy::fn_params_excessive_bools)]
35
- pub fn escape(
36
- text: &str,
37
- escape_misc: bool,
38
- escape_asterisks: bool,
39
- escape_underscores: bool,
40
- escape_ascii: bool,
41
- ) -> Cow<'_, str> {
42
- if text.is_empty() {
43
- return Cow::Borrowed("");
44
- }
45
-
46
- if !escape_misc && !escape_asterisks && !escape_underscores && !escape_ascii {
47
- return Cow::Borrowed(text);
48
- }
49
-
50
- if escape_ascii
51
- && !text.as_bytes().iter().any(|b| {
52
- matches!(
53
- b,
54
- b'!' | b'"'
55
- | b'#'
56
- | b'$'
57
- | b'%'
58
- | b'&'
59
- | b'\''
60
- | b'('
61
- | b')'
62
- | b'*'
63
- | b'+'
64
- | b','
65
- | b'-'
66
- | b'.'
67
- | b'/'
68
- | b':'
69
- | b';'
70
- | b'<'
71
- | b'='
72
- | b'>'
73
- | b'?'
74
- | b'@'
75
- | b'['
76
- | b'\\'
77
- | b']'
78
- | b'^'
79
- | b'_'
80
- | b'`'
81
- | b'{'
82
- | b'|'
83
- | b'}'
84
- | b'~'
85
- )
86
- })
87
- {
88
- return Cow::Borrowed(text);
89
- }
90
-
91
- if !escape_ascii && escape_misc && !escape_asterisks && !escape_underscores {
92
- let needs_misc = text.as_bytes().iter().any(|b| {
93
- matches!(
94
- b,
95
- b'\\' | b'&' | b'<' | b'`' | b'[' | b']' | b'>' | b'~' | b'#' | b'=' | b'+' | b'|' | b'-'
96
- )
97
- });
98
- let needs_numbered = text.as_bytes().iter().any(|b| matches!(b, b'.' | b')'));
99
- if !needs_misc && !needs_numbered {
100
- return Cow::Borrowed(text);
101
- }
102
- }
103
-
104
- let mut result: Cow<'_, str> = Cow::Borrowed(text);
105
-
106
- if escape_ascii {
107
- result = match ESCAPE_ASCII_RE.replace_all(result.as_ref(), r"\$1") {
108
- Cow::Borrowed(_) => result,
109
- Cow::Owned(s) => Cow::Owned(s),
110
- };
111
- return result;
112
- }
113
-
114
- if escape_misc {
115
- result = match ESCAPE_MISC_RE.replace_all(result.as_ref(), r"\$1") {
116
- Cow::Borrowed(_) => result,
117
- Cow::Owned(s) => Cow::Owned(s),
118
- };
119
-
120
- result = match ESCAPE_NUMBERED_LIST_RE.replace_all(result.as_ref(), r"$1\$2") {
121
- Cow::Borrowed(_) => result,
122
- Cow::Owned(s) => Cow::Owned(s),
123
- };
124
- }
125
-
126
- if escape_asterisks && result.contains('*') {
127
- result = Cow::Owned(result.replace('*', r"\*"));
128
- }
129
-
130
- if escape_underscores && result.contains('_') {
131
- result = Cow::Owned(result.replace('_', r"\_"));
132
- }
133
-
134
- result
135
- }
136
-
137
- /// Extract boundary whitespace from text (chomp).
138
- ///
139
- /// Returns (prefix, suffix, `trimmed_text`) tuple.
140
- /// Prefix/suffix are " " if original text had leading/trailing whitespace.
141
- /// However, suffix is "" if the trailing whitespace is only newlines (not spaces/tabs).
142
- /// This prevents trailing newlines from becoming trailing spaces in the output.
143
- /// The trimmed text has all leading/trailing whitespace removed.
144
- #[must_use]
145
- pub fn chomp(text: &str) -> (&str, &str, &str) {
146
- if text.is_empty() {
147
- return ("", "", "");
148
- }
149
-
150
- let prefix = if text.starts_with(|c: char| c.is_whitespace()) {
151
- " "
152
- } else {
153
- ""
154
- };
155
-
156
- let suffix = if text.ends_with("\n\n") || text.ends_with("\r\n\r\n") {
157
- "\n\n"
158
- } else if text.ends_with([' ', '\t']) {
159
- " "
160
- } else {
161
- ""
162
- };
163
-
164
- let trimmed = if suffix == "\n\n" {
165
- text.trim_end_matches("\n\n").trim_end_matches("\r\n\r\n").trim()
166
- } else {
167
- text.trim()
168
- };
169
-
170
- (prefix, suffix, trimmed)
171
- }
172
-
173
- /// Normalize whitespace by collapsing consecutive spaces and tabs.
174
- ///
175
- /// Multiple spaces and tabs are replaced with a single space.
176
- /// Newlines are preserved.
177
- /// Unicode spaces are normalized to ASCII spaces.
178
- ///
179
- /// # Arguments
180
- ///
181
- /// * `text` - The text to normalize
182
- ///
183
- /// # Returns
184
- ///
185
- /// Normalized text with collapsed spaces/tabs but preserved newlines
186
- #[must_use]
187
- pub fn normalize_whitespace(text: &str) -> String {
188
- let mut result = String::with_capacity(text.len());
189
- let mut prev_was_space = false;
190
-
191
- for ch in text.chars() {
192
- let is_space = ch == ' ' || ch == '\t' || is_unicode_space(ch);
193
-
194
- if is_space {
195
- if !prev_was_space {
196
- result.push(' ');
197
- prev_was_space = true;
198
- }
199
- } else {
200
- result.push(ch);
201
- prev_was_space = false;
202
- }
203
- }
204
-
205
- result
206
- }
207
-
208
- /// Normalize whitespace in text, returning borrowed or owned result as needed.
209
- ///
210
- /// This function optimizes memory by returning a borrowed reference when no normalization
211
- /// is needed, and only allocating a new string when whitespace changes are necessary.
212
- ///
213
- /// Multiple consecutive spaces, tabs, and Unicode space characters are replaced with
214
- /// a single ASCII space. Newlines are preserved as-is.
215
- ///
216
- /// # Arguments
217
- ///
218
- /// * `text` - The text to normalize
219
- ///
220
- /// # Returns
221
- ///
222
- /// `Cow::Borrowed` if text is already normalized, or `Cow::Owned` with normalized text
223
- #[must_use]
224
- pub fn normalize_whitespace_cow(text: &str) -> Cow<'_, str> {
225
- let mut prev_was_space = false;
226
-
227
- for ch in text.chars() {
228
- let is_space = ch == ' ' || ch == '\t' || is_unicode_space(ch);
229
- if is_space {
230
- if prev_was_space || ch != ' ' {
231
- return Cow::Owned(normalize_whitespace(text));
232
- }
233
- prev_was_space = true;
234
- } else {
235
- prev_was_space = false;
236
- }
237
- }
238
-
239
- Cow::Borrowed(text)
240
- }
241
-
242
- /// Decode common HTML entities.
243
- ///
244
- /// Decodes the most common HTML entities to their character equivalents:
245
- /// - `&quot;` → `"`
246
- /// - `&apos;` → `'`
247
- /// - `&lt;` → `<`
248
- /// - `&gt;` → `>`
249
- /// - `&amp;` → `&` (must be last to avoid double-decoding)
250
- ///
251
- /// # Arguments
252
- ///
253
- /// * `text` - Text containing HTML entities
254
- ///
255
- /// # Returns
256
- ///
257
- /// Text with entities decoded
258
- #[must_use]
259
- pub fn decode_html_entities(text: &str) -> String {
260
- html_escape::decode_html_entities(text).into_owned()
261
- }
262
-
263
- /// Decode HTML entities in text, returning borrowed or owned result as needed.
264
- ///
265
- /// This function optimizes memory by returning a borrowed reference when no HTML
266
- /// entities are present, and only allocating a new string when entity decoding
267
- /// is necessary.
268
- ///
269
- /// Decodes common HTML entities like:
270
- /// - `&quot;` → `"`
271
- /// - `&apos;` → `'`
272
- /// - `&lt;` → `<`
273
- /// - `&gt;` → `>`
274
- /// - `&amp;` → `&` (decoded last to avoid double-decoding)
275
- ///
276
- /// # Arguments
277
- ///
278
- /// * `text` - Text potentially containing HTML entities
279
- ///
280
- /// # Returns
281
- ///
282
- /// `Cow::Borrowed` if no entities found, or `Cow::Owned` with entities decoded
283
- #[must_use]
284
- pub fn decode_html_entities_cow(text: &str) -> Cow<'_, str> {
285
- if !text.contains('&') {
286
- return Cow::Borrowed(text);
287
- }
288
-
289
- html_escape::decode_html_entities(text)
290
- }
291
-
292
- /// Check if a character is a unicode space character.
293
- ///
294
- /// Includes: non-breaking space, various width spaces, etc.
295
- const fn is_unicode_space(ch: char) -> bool {
296
- matches!(
297
- ch,
298
- '\u{00A0}'
299
- | '\u{1680}'
300
- | '\u{2000}'
301
- | '\u{2001}'
302
- | '\u{2002}'
303
- | '\u{2003}'
304
- | '\u{2004}'
305
- | '\u{2005}'
306
- | '\u{2006}'
307
- | '\u{2007}'
308
- | '\u{2008}'
309
- | '\u{2009}'
310
- | '\u{200A}'
311
- | '\u{202F}'
312
- | '\u{205F}'
313
- | '\u{3000}'
314
- )
315
- }
316
-
317
- #[cfg(test)]
318
- mod tests {
319
- use super::*;
320
-
321
- #[test]
322
- fn test_escape_misc() {
323
- assert_eq!(escape("foo & bar", true, false, false, false), r"foo \& bar");
324
- assert_eq!(escape("foo [bar]", true, false, false, false), r"foo \[bar\]");
325
- assert_eq!(escape("1. Item", true, false, false, false), r"1\. Item");
326
- assert_eq!(escape("1) Item", true, false, false, false), r"1\) Item");
327
- }
328
-
329
- #[test]
330
- fn test_escape_asterisks() {
331
- assert_eq!(escape("foo * bar", false, true, false, false), r"foo \* bar");
332
- assert_eq!(escape("**bold**", false, true, false, false), r"\*\*bold\*\*");
333
- }
334
-
335
- #[test]
336
- fn test_escape_underscores() {
337
- assert_eq!(escape("foo_bar", false, false, true, false), r"foo\_bar");
338
- assert_eq!(escape("__bold__", false, false, true, false), r"\_\_bold\_\_");
339
- }
340
-
341
- #[test]
342
- fn test_escape_ascii() {
343
- assert_eq!(escape(r##"!"#$%&"##, false, false, false, true), r#"\!\"\#\$\%\&"#);
344
- assert_eq!(escape("*+,-./", false, false, false, true), r"\*\+\,\-\.\/");
345
- assert_eq!(escape("<=>?@", false, false, false, true), r"\<\=\>\?\@");
346
- assert_eq!(escape(r"[\]^_`", false, false, false, true), r"\[\\\]\^\_\`");
347
- assert_eq!(escape("{|}~", false, false, false, true), r"\{\|\}\~");
348
- }
349
-
350
- #[test]
351
- fn test_chomp() {
352
- assert_eq!(chomp(" text "), (" ", " ", "text"));
353
- assert_eq!(chomp("text"), ("", "", "text"));
354
- assert_eq!(chomp(" text"), (" ", "", "text"));
355
- assert_eq!(chomp("text "), ("", " ", "text"));
356
- assert_eq!(chomp(""), ("", "", ""));
357
- }
358
- }
@@ -1,191 +0,0 @@
1
- //! Structured document tree types aligned with kreuzberg's `DocumentStructure`.
2
-
3
- use std::collections::HashMap;
4
-
5
- #[cfg(feature = "serde")]
6
- use serde::{Deserialize, Serialize};
7
-
8
- use super::tables::TableGrid;
9
-
10
- /// A structured document tree representing the semantic content of an HTML document.
11
- ///
12
- /// Uses a flat node array with index-based parent/child references for efficient traversal.
13
- #[derive(Debug, Clone, PartialEq, Eq)]
14
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15
- pub struct DocumentStructure {
16
- /// All nodes in document reading order.
17
- pub nodes: Vec<DocumentNode>,
18
- /// The source format (always "html" for this crate).
19
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
20
- pub source_format: Option<String>,
21
- }
22
-
23
- /// A single node in the document tree.
24
- #[derive(Debug, Clone, PartialEq, Eq)]
25
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26
- pub struct DocumentNode {
27
- /// Deterministic node identifier.
28
- pub id: String,
29
- /// The semantic content of this node.
30
- pub content: NodeContent,
31
- /// Index of the parent node (None for root nodes).
32
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
33
- pub parent: Option<u32>,
34
- /// Indices of child nodes in reading order.
35
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty", default))]
36
- pub children: Vec<u32>,
37
- /// Inline formatting annotations (bold, italic, links, etc.) with byte offsets into the text.
38
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Vec::is_empty", default))]
39
- pub annotations: Vec<TextAnnotation>,
40
- /// Format-specific attributes (e.g. class, id, data-* attributes).
41
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
42
- pub attributes: Option<HashMap<String, String>>,
43
- }
44
-
45
- /// The semantic content type of a document node.
46
- ///
47
- /// Uses internally tagged representation (`"node_type": "heading"`) for JSON serialization.
48
- #[derive(Debug, Clone, PartialEq, Eq)]
49
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50
- #[cfg_attr(feature = "serde", serde(tag = "node_type", rename_all = "snake_case"))]
51
- pub enum NodeContent {
52
- /// A heading element (h1-h6).
53
- Heading {
54
- /// Heading level (1-6).
55
- level: u8,
56
- /// The heading text content.
57
- text: String,
58
- },
59
- /// A paragraph of text.
60
- Paragraph {
61
- /// The paragraph text content.
62
- text: String,
63
- },
64
- /// A list container (ordered or unordered). Children are `ListItem` nodes.
65
- List {
66
- /// Whether this is an ordered list.
67
- ordered: bool,
68
- },
69
- /// A single list item.
70
- ListItem {
71
- /// The list item text content.
72
- text: String,
73
- },
74
- /// A table with structured cell data.
75
- Table {
76
- /// The table grid structure.
77
- grid: TableGrid,
78
- },
79
- /// An image element.
80
- Image {
81
- /// Alt text or caption.
82
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
83
- description: Option<String>,
84
- /// Image source URL.
85
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
86
- src: Option<String>,
87
- /// Index into `ConversionResult.images` when image extraction is enabled.
88
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
89
- image_index: Option<u32>,
90
- },
91
- /// A code block or inline code.
92
- Code {
93
- /// The code text content.
94
- text: String,
95
- /// Programming language (from class="language-*" or similar).
96
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
97
- language: Option<String>,
98
- },
99
- /// A block quote container.
100
- Quote,
101
- /// A definition list container.
102
- DefinitionList,
103
- /// A definition list entry with term and description.
104
- DefinitionItem {
105
- /// The term being defined.
106
- term: String,
107
- /// The definition text.
108
- definition: String,
109
- },
110
- /// A raw block preserved as-is (e.g. `<script>`, `<style>` content).
111
- RawBlock {
112
- /// The format of the raw content (e.g. "html", "css", "javascript").
113
- format: String,
114
- /// The raw content.
115
- content: String,
116
- },
117
- /// A block of key-value metadata pairs (from `<head>` meta tags).
118
- MetadataBlock {
119
- /// Key-value metadata pairs.
120
- entries: Vec<(String, String)>,
121
- },
122
- /// A section grouping container (auto-generated from heading hierarchy).
123
- Group {
124
- /// Optional section label.
125
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
126
- label: Option<String>,
127
- /// The heading level that created this group.
128
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
129
- heading_level: Option<u8>,
130
- /// The heading text that created this group.
131
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
132
- heading_text: Option<String>,
133
- },
134
- }
135
-
136
- /// An inline text annotation with byte-range offsets.
137
- ///
138
- /// Annotations describe formatting (bold, italic, etc.) and links within a node's text content.
139
- #[derive(Debug, Clone, PartialEq, Eq)]
140
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
141
- pub struct TextAnnotation {
142
- /// Start byte offset (inclusive) into the parent node's text.
143
- pub start: u32,
144
- /// End byte offset (exclusive) into the parent node's text.
145
- pub end: u32,
146
- /// The type of annotation.
147
- pub kind: AnnotationKind,
148
- }
149
-
150
- /// The type of an inline text annotation.
151
- ///
152
- /// Uses internally tagged representation (`"annotation_type": "bold"`) for JSON serialization.
153
- #[derive(Debug, Clone, PartialEq, Eq, Default)]
154
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
155
- #[cfg_attr(feature = "serde", serde(tag = "annotation_type", rename_all = "snake_case"))]
156
- pub enum AnnotationKind {
157
- /// Bold / strong emphasis.
158
- #[default]
159
- Bold,
160
- /// Italic / emphasis.
161
- Italic,
162
- /// Underline.
163
- Underline,
164
- /// Strikethrough / deleted text.
165
- Strikethrough,
166
- /// Inline code.
167
- Code,
168
- /// Subscript text.
169
- Subscript,
170
- /// Superscript text.
171
- Superscript,
172
- /// Highlighted / marked text.
173
- Highlight,
174
- /// A hyperlink.
175
- Link {
176
- /// The link URL.
177
- url: String,
178
- /// Optional link title attribute.
179
- #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
180
- title: Option<String>,
181
- },
182
- }
183
-
184
- impl Default for NodeContent {
185
- fn default() -> Self {
186
- Self::Heading {
187
- level: 1,
188
- text: String::new(),
189
- }
190
- }
191
- }
@@ -1,17 +0,0 @@
1
- //! Core types for structured HTML extraction results.
2
- //!
3
- //! These types are aligned with kreuzberg's `DocumentStructure` model for seamless integration.
4
-
5
- mod document;
6
- mod result;
7
- pub mod structure_builder;
8
- pub mod structure_collector;
9
- mod tables;
10
- mod warnings;
11
-
12
- pub use document::{AnnotationKind, DocumentNode, DocumentStructure, NodeContent, TextAnnotation};
13
- pub use result::ConversionResult;
14
- pub use structure_builder::build_document_structure;
15
- pub use structure_collector::{StructureCollector, StructureCollectorHandle};
16
- pub use tables::{GridCell, TableData, TableGrid};
17
- pub use warnings::{ProcessingWarning, WarningKind};
@@ -1,54 +0,0 @@
1
- //! The primary result type for HTML conversion and extraction.
2
-
3
- #[cfg(feature = "serde")]
4
- use serde::{Deserialize, Serialize};
5
-
6
- use super::document::DocumentStructure;
7
- use super::tables::TableData;
8
- use super::warnings::ProcessingWarning;
9
-
10
- /// The primary result of HTML conversion and extraction.
11
- ///
12
- /// Contains the converted text output, optional structured document tree,
13
- /// metadata, extracted tables, images, and processing warnings.
14
- ///
15
- /// # Example
16
- ///
17
- /// ```text
18
- /// use html_to_markdown_rs::{convert, ConversionOptions};
19
- ///
20
- /// let result = convert("<h1>Hello</h1><p>World</p>", None)?;
21
- /// assert!(result.content.is_some());
22
- /// assert!(result.warnings.is_empty());
23
- /// ```
24
- #[derive(Debug, Clone, Default)]
25
- #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26
- pub struct ConversionResult {
27
- /// Converted text output (markdown, djot, or plain text).
28
- ///
29
- /// `None` when `output_format` is set to `OutputFormat::None`,
30
- /// indicating extraction-only mode.
31
- pub content: Option<String>,
32
-
33
- /// Structured document tree with semantic elements.
34
- ///
35
- /// Populated when `include_document_structure` is `true` in options.
36
- pub document: Option<DocumentStructure>,
37
-
38
- /// Extracted HTML metadata (title, OG, links, images, structured data).
39
- #[cfg(feature = "metadata")]
40
- pub metadata: crate::metadata::HtmlMetadata,
41
-
42
- /// Extracted tables with structured cell data and markdown representation.
43
- pub tables: Vec<TableData>,
44
-
45
- /// Extracted inline images (data URIs and SVGs).
46
- ///
47
- /// Populated when `extract_images` is `true` in options.
48
- #[cfg(feature = "inline-images")]
49
- #[cfg_attr(feature = "serde", serde(skip))]
50
- pub images: Vec<crate::inline_images::InlineImage>,
51
-
52
- /// Non-fatal processing warnings.
53
- pub warnings: Vec<ProcessingWarning>,
54
- }