html-to-markdown 3.4.0 → 3.6.0.pre.rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +347 -0
- data/Steepfile +10 -2
- data/ext/html_to_markdown_rb/Cargo.toml +3 -2
- data/ext/html_to_markdown_rb/extconf.rb +5 -5
- data/ext/html_to_markdown_rb/native/Cargo.lock +962 -0
- data/ext/html_to_markdown_rb/native/Cargo.toml +6 -11
- data/ext/html_to_markdown_rb/native/extconf.rb +14 -0
- data/ext/html_to_markdown_rb/src/lib.rs +1715 -646
- data/lib/html_to_markdown/native.rb +913 -37
- data/lib/html_to_markdown/version.rb +3 -3
- data/lib/html_to_markdown.rb +9 -4
- data/lib/html_to_markdown_rb.so +0 -0
- data/sig/types.rbs +59 -292
- metadata +32 -179
- data/ext/html_to_markdown_rb/Makefile +0 -592
- data/lib/bin/html-to-markdown +0 -0
- data/vendor/Cargo.toml +0 -33
- data/vendor/html-to-markdown-rs/Cargo.toml +0 -54
- data/vendor/html-to-markdown-rs/README.md +0 -278
- data/vendor/html-to-markdown-rs/examples/basic.rs +0 -24
- data/vendor/html-to-markdown-rs/examples/table.rs +0 -25
- data/vendor/html-to-markdown-rs/examples/test_deser.rs +0 -12
- data/vendor/html-to-markdown-rs/examples/test_escape.rs +0 -58
- data/vendor/html-to-markdown-rs/examples/test_inline_formatting.rs +0 -113
- data/vendor/html-to-markdown-rs/examples/test_lists.rs +0 -39
- data/vendor/html-to-markdown-rs/examples/test_semantic_tags.rs +0 -89
- data/vendor/html-to-markdown-rs/examples/test_tables.rs +0 -100
- data/vendor/html-to-markdown-rs/examples/test_task_lists.rs +0 -61
- data/vendor/html-to-markdown-rs/examples/test_whitespace.rs +0 -34
- data/vendor/html-to-markdown-rs/src/convert_api.rs +0 -349
- data/vendor/html-to-markdown-rs/src/converter/block/blockquote.rs +0 -178
- data/vendor/html-to-markdown-rs/src/converter/block/container.rs +0 -114
- data/vendor/html-to-markdown-rs/src/converter/block/div.rs +0 -149
- data/vendor/html-to-markdown-rs/src/converter/block/heading.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/block/horizontal_rule.rs +0 -103
- data/vendor/html-to-markdown-rs/src/converter/block/line_break.rs +0 -89
- data/vendor/html-to-markdown-rs/src/converter/block/mod.rs +0 -10
- data/vendor/html-to-markdown-rs/src/converter/block/paragraph.rs +0 -140
- data/vendor/html-to-markdown-rs/src/converter/block/preformatted.rs +0 -298
- data/vendor/html-to-markdown-rs/src/converter/block/table/builder.rs +0 -453
- data/vendor/html-to-markdown-rs/src/converter/block/table/caption.rs +0 -44
- data/vendor/html-to-markdown-rs/src/converter/block/table/cell.rs +0 -276
- data/vendor/html-to-markdown-rs/src/converter/block/table/cells.rs +0 -336
- data/vendor/html-to-markdown-rs/src/converter/block/table/layout.rs +0 -58
- data/vendor/html-to-markdown-rs/src/converter/block/table/mod.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/block/table/scanner.rs +0 -146
- data/vendor/html-to-markdown-rs/src/converter/block/table/utils.rs +0 -34
- data/vendor/html-to-markdown-rs/src/converter/block/unknown.rs +0 -138
- data/vendor/html-to-markdown-rs/src/converter/context.rs +0 -208
- data/vendor/html-to-markdown-rs/src/converter/dom_context.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/form/elements.rs +0 -770
- data/vendor/html-to-markdown-rs/src/converter/form/mod.rs +0 -82
- data/vendor/html-to-markdown-rs/src/converter/format/djot.rs +0 -64
- data/vendor/html-to-markdown-rs/src/converter/format/markdown.rs +0 -59
- data/vendor/html-to-markdown-rs/src/converter/format/mod.rs +0 -43
- data/vendor/html-to-markdown-rs/src/converter/handlers/blockquote.rs +0 -173
- data/vendor/html-to-markdown-rs/src/converter/handlers/code_block.rs +0 -434
- data/vendor/html-to-markdown-rs/src/converter/handlers/graphic.rs +0 -234
- data/vendor/html-to-markdown-rs/src/converter/handlers/image.rs +0 -282
- data/vendor/html-to-markdown-rs/src/converter/handlers/link.rs +0 -316
- data/vendor/html-to-markdown-rs/src/converter/handlers/mod.rs +0 -26
- data/vendor/html-to-markdown-rs/src/converter/inline/code.rs +0 -306
- data/vendor/html-to-markdown-rs/src/converter/inline/emphasis.rs +0 -345
- data/vendor/html-to-markdown-rs/src/converter/inline/link.rs +0 -428
- data/vendor/html-to-markdown-rs/src/converter/inline/mod.rs +0 -237
- data/vendor/html-to-markdown-rs/src/converter/inline/ruby.rs +0 -337
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/marks.rs +0 -566
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/mod.rs +0 -86
- data/vendor/html-to-markdown-rs/src/converter/inline/semantic/typography.rs +0 -558
- data/vendor/html-to-markdown-rs/src/converter/list/definition.rs +0 -232
- data/vendor/html-to-markdown-rs/src/converter/list/item.rs +0 -332
- data/vendor/html-to-markdown-rs/src/converter/list/mod.rs +0 -70
- data/vendor/html-to-markdown-rs/src/converter/list/ordered.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/list/unordered.rs +0 -195
- data/vendor/html-to-markdown-rs/src/converter/list/utils.rs +0 -314
- data/vendor/html-to-markdown-rs/src/converter/main.rs +0 -710
- data/vendor/html-to-markdown-rs/src/converter/main_helpers.rs +0 -452
- data/vendor/html-to-markdown-rs/src/converter/media/embedded.rs +0 -393
- data/vendor/html-to-markdown-rs/src/converter/media/graphic.rs +0 -4
- data/vendor/html-to-markdown-rs/src/converter/media/image.rs +0 -183
- data/vendor/html-to-markdown-rs/src/converter/media/mod.rs +0 -87
- data/vendor/html-to-markdown-rs/src/converter/media/svg.rs +0 -280
- data/vendor/html-to-markdown-rs/src/converter/metadata.rs +0 -220
- data/vendor/html-to-markdown-rs/src/converter/mod.rs +0 -156
- data/vendor/html-to-markdown-rs/src/converter/plain_text.rs +0 -516
- data/vendor/html-to-markdown-rs/src/converter/preprocessing_helpers.rs +0 -201
- data/vendor/html-to-markdown-rs/src/converter/reference_collector.rs +0 -69
- data/vendor/html-to-markdown-rs/src/converter/semantic/attributes.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/semantic/definition_list.rs +0 -266
- data/vendor/html-to-markdown-rs/src/converter/semantic/figure.rs +0 -391
- data/vendor/html-to-markdown-rs/src/converter/semantic/mod.rs +0 -112
- data/vendor/html-to-markdown-rs/src/converter/semantic/sectioning.rs +0 -85
- data/vendor/html-to-markdown-rs/src/converter/semantic/summary.rs +0 -324
- data/vendor/html-to-markdown-rs/src/converter/text/mod.rs +0 -8
- data/vendor/html-to-markdown-rs/src/converter/text/processing.rs +0 -56
- data/vendor/html-to-markdown-rs/src/converter/text_node.rs +0 -269
- data/vendor/html-to-markdown-rs/src/converter/utility/attributes.rs +0 -151
- data/vendor/html-to-markdown-rs/src/converter/utility/caching.rs +0 -74
- data/vendor/html-to-markdown-rs/src/converter/utility/content.rs +0 -271
- data/vendor/html-to-markdown-rs/src/converter/utility/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/converter/utility/preprocessing.rs +0 -1002
- data/vendor/html-to-markdown-rs/src/converter/utility/serialization.rs +0 -126
- data/vendor/html-to-markdown-rs/src/converter/utility/siblings.rs +0 -97
- data/vendor/html-to-markdown-rs/src/converter/visitor_hooks.rs +0 -189
- data/vendor/html-to-markdown-rs/src/error.rs +0 -43
- data/vendor/html-to-markdown-rs/src/exports.rs +0 -24
- data/vendor/html-to-markdown-rs/src/inline_images.rs +0 -336
- data/vendor/html-to-markdown-rs/src/lib.rs +0 -139
- data/vendor/html-to-markdown-rs/src/metadata/collector.rs +0 -457
- data/vendor/html-to-markdown-rs/src/metadata/config.rs +0 -394
- data/vendor/html-to-markdown-rs/src/metadata/extraction.rs +0 -398
- data/vendor/html-to-markdown-rs/src/metadata/mod.rs +0 -288
- data/vendor/html-to-markdown-rs/src/metadata/types.rs +0 -477
- data/vendor/html-to-markdown-rs/src/options/conversion.rs +0 -559
- data/vendor/html-to-markdown-rs/src/options/inline_image.rs +0 -111
- data/vendor/html-to-markdown-rs/src/options/mod.rs +0 -20
- data/vendor/html-to-markdown-rs/src/options/preprocessing.rs +0 -201
- data/vendor/html-to-markdown-rs/src/options/validation.rs +0 -416
- data/vendor/html-to-markdown-rs/src/prelude.rs +0 -1
- data/vendor/html-to-markdown-rs/src/rcdom.rs +0 -487
- data/vendor/html-to-markdown-rs/src/text.rs +0 -358
- data/vendor/html-to-markdown-rs/src/types/document.rs +0 -191
- data/vendor/html-to-markdown-rs/src/types/mod.rs +0 -17
- data/vendor/html-to-markdown-rs/src/types/result.rs +0 -54
- data/vendor/html-to-markdown-rs/src/types/structure_builder.rs +0 -791
- data/vendor/html-to-markdown-rs/src/types/structure_collector.rs +0 -483
- data/vendor/html-to-markdown-rs/src/types/tables.rs +0 -52
- data/vendor/html-to-markdown-rs/src/types/warnings.rs +0 -33
- data/vendor/html-to-markdown-rs/src/validation.rs +0 -158
- data/vendor/html-to-markdown-rs/src/visitor/default_impl.rs +0 -63
- data/vendor/html-to-markdown-rs/src/visitor/mod.rs +0 -41
- data/vendor/html-to-markdown-rs/src/visitor/traits.rs +0 -370
- data/vendor/html-to-markdown-rs/src/visitor/types.rs +0 -319
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/callbacks/mod.rs +0 -1
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/content.rs +0 -126
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/mod.rs +0 -27
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/state.rs +0 -110
- data/vendor/html-to-markdown-rs/src/visitor_helpers/helpers/traversal.rs +0 -250
- data/vendor/html-to-markdown-rs/src/visitor_helpers.rs +0 -597
- data/vendor/html-to-markdown-rs/src/wrapper/sync.rs +0 -413
- data/vendor/html-to-markdown-rs/src/wrapper/utils.rs +0 -290
- data/vendor/html-to-markdown-rs/src/wrapper.rs +0 -9
- data/vendor/html-to-markdown-rs/tests/br_in_inline_test.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/commonmark_compliance_test.rs +0 -297
- data/vendor/html-to-markdown-rs/tests/djot_output_test.rs +0 -153
- data/vendor/html-to-markdown-rs/tests/exclude_selectors_test.rs +0 -132
- data/vendor/html-to-markdown-rs/tests/integration_test.rs +0 -631
- data/vendor/html-to-markdown-rs/tests/issue_121_regressions.rs +0 -49
- data/vendor/html-to-markdown-rs/tests/issue_127_regressions.rs +0 -58
- data/vendor/html-to-markdown-rs/tests/issue_128_regressions.rs +0 -17
- data/vendor/html-to-markdown-rs/tests/issue_131_regressions.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/issue_134_regressions.rs +0 -40
- data/vendor/html-to-markdown-rs/tests/issue_139_regressions.rs +0 -26
- data/vendor/html-to-markdown-rs/tests/issue_140_regressions.rs +0 -185
- data/vendor/html-to-markdown-rs/tests/issue_143_regressions.rs +0 -100
- data/vendor/html-to-markdown-rs/tests/issue_145_regressions.rs +0 -133
- data/vendor/html-to-markdown-rs/tests/issue_146_regressions.rs +0 -144
- data/vendor/html-to-markdown-rs/tests/issue_176_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_190_regressions.rs +0 -128
- data/vendor/html-to-markdown-rs/tests/issue_199_regressions.rs +0 -20
- data/vendor/html-to-markdown-rs/tests/issue_200_regressions.rs +0 -62
- data/vendor/html-to-markdown-rs/tests/issue_212_regressions.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/issue_216_217_regressions.rs +0 -87
- data/vendor/html-to-markdown-rs/tests/issue_336_regressions.rs +0 -74
- data/vendor/html-to-markdown-rs/tests/issue_339_regressions.rs +0 -92
- data/vendor/html-to-markdown-rs/tests/issue_347_regressions.rs +0 -154
- data/vendor/html-to-markdown-rs/tests/issue_348_visitor_plain.rs +0 -93
- data/vendor/html-to-markdown-rs/tests/json_ld_script_extraction.rs +0 -44
- data/vendor/html-to-markdown-rs/tests/lists_test.rs +0 -199
- data/vendor/html-to-markdown-rs/tests/plain_output_test.rs +0 -273
- data/vendor/html-to-markdown-rs/tests/preprocessing_tests.rs +0 -61
- data/vendor/html-to-markdown-rs/tests/reference_links_test.rs +0 -169
- data/vendor/html-to-markdown-rs/tests/sectioning_elements_test.rs +0 -137
- data/vendor/html-to-markdown-rs/tests/skip_images_test.rs +0 -522
- data/vendor/html-to-markdown-rs/tests/tables_test.rs +0 -743
- data/vendor/html-to-markdown-rs/tests/test_custom_elements.rs +0 -41
- data/vendor/html-to-markdown-rs/tests/test_issue_187.rs +0 -204
- data/vendor/html-to-markdown-rs/tests/test_issue_218.rs +0 -68
- data/vendor/html-to-markdown-rs/tests/test_issue_277.rs +0 -77
- data/vendor/html-to-markdown-rs/tests/test_max_depth.rs +0 -82
- data/vendor/html-to-markdown-rs/tests/test_nested_simple.rs +0 -45
- data/vendor/html-to-markdown-rs/tests/test_script_style_stripping.rs +0 -396
- data/vendor/html-to-markdown-rs/tests/test_spa_bisect.rs +0 -34
- data/vendor/html-to-markdown-rs/tests/visitor_code_integration_test.rs +0 -121
- data/vendor/html-to-markdown-rs/tests/visitor_integration_test.rs +0 -1190
- data/vendor/html-to-markdown-rs/tests/xml_tables_test.rs +0 -372
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
//! Handler for heading elements (h1-h6).
|
|
2
|
-
//!
|
|
3
|
-
//! Converts HTML heading tags to Markdown heading syntax with support for:
|
|
4
|
-
//! - Multiple heading styles (ATX, underlined, closed ATX)
|
|
5
|
-
//! - Inline content processing with proper text normalization
|
|
6
|
-
//! - Metadata collection (headers, IDs)
|
|
7
|
-
//! - Visitor callbacks for custom heading processing
|
|
8
|
-
|
|
9
|
-
#[cfg(feature = "visitor")]
|
|
10
|
-
use crate::converter::utility::content::collect_tag_attributes;
|
|
11
|
-
use crate::options::{ConversionOptions, HeadingStyle};
|
|
12
|
-
use std::borrow::Cow;
|
|
13
|
-
#[allow(unused_imports)]
|
|
14
|
-
use std::collections::BTreeMap;
|
|
15
|
-
use tl::{NodeHandle, Parser};
|
|
16
|
-
|
|
17
|
-
// Type aliases for Context and DomContext to avoid circular imports
|
|
18
|
-
// These are imported from converter.rs and should be made accessible
|
|
19
|
-
type Context = crate::converter::Context;
|
|
20
|
-
type DomContext = crate::converter::DomContext;
|
|
21
|
-
|
|
22
|
-
/// Handle heading elements (h1, h2, h3, h4, h5, h6).
|
|
23
|
-
///
|
|
24
|
-
/// Extracts the heading level from the tag name, processes inline content,
|
|
25
|
-
/// normalizes text, and outputs formatted heading with proper spacing.
|
|
26
|
-
///
|
|
27
|
-
/// # Note
|
|
28
|
-
/// This function references `walk_node` from converter.rs which must be
|
|
29
|
-
/// accessible (pub(crate)) for this module to work correctly.
|
|
30
|
-
pub fn handle(
|
|
31
|
-
tag_name: &str,
|
|
32
|
-
node_handle: &NodeHandle,
|
|
33
|
-
parser: &Parser,
|
|
34
|
-
output: &mut String,
|
|
35
|
-
options: &ConversionOptions,
|
|
36
|
-
ctx: &Context,
|
|
37
|
-
depth: usize,
|
|
38
|
-
dom_ctx: &DomContext,
|
|
39
|
-
) {
|
|
40
|
-
// Import walk_node from parent converter module
|
|
41
|
-
use crate::converter::walk_node;
|
|
42
|
-
|
|
43
|
-
let level = tag_name.chars().last().and_then(|c| c.to_digit(10)).unwrap_or(1) as usize;
|
|
44
|
-
|
|
45
|
-
// Add spacing before heading if needed (similar to paragraph handling)
|
|
46
|
-
let needs_leading_sep = !ctx.in_table_cell
|
|
47
|
-
&& !ctx.in_list_item
|
|
48
|
-
&& !ctx.convert_as_inline
|
|
49
|
-
&& ctx.blockquote_depth == 0
|
|
50
|
-
&& !output.is_empty()
|
|
51
|
-
&& !output.ends_with("\n\n");
|
|
52
|
-
|
|
53
|
-
if needs_leading_sep {
|
|
54
|
-
crate::converter::trim_trailing_whitespace(output);
|
|
55
|
-
output.push_str("\n\n");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let mut text = String::new();
|
|
59
|
-
let heading_ctx = Context {
|
|
60
|
-
in_heading: true,
|
|
61
|
-
convert_as_inline: true,
|
|
62
|
-
heading_allow_inline_images: heading_allows_inline_images(tag_name, &ctx.keep_inline_images_in),
|
|
63
|
-
..ctx.clone()
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
if let Some(node) = node_handle.get(parser) {
|
|
67
|
-
if let tl::Node::Tag(tag) = node {
|
|
68
|
-
let children = tag.children();
|
|
69
|
-
for child_handle in children.top().iter() {
|
|
70
|
-
walk_node(
|
|
71
|
-
child_handle,
|
|
72
|
-
parser,
|
|
73
|
-
&mut text,
|
|
74
|
-
options,
|
|
75
|
-
&heading_ctx,
|
|
76
|
-
depth + 1,
|
|
77
|
-
dom_ctx,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let trimmed = text.trim();
|
|
84
|
-
if !trimmed.is_empty() {
|
|
85
|
-
let normalized = normalize_heading_text(trimmed);
|
|
86
|
-
|
|
87
|
-
#[cfg(feature = "visitor")]
|
|
88
|
-
let heading_output = visitor_heading_output(
|
|
89
|
-
node_handle,
|
|
90
|
-
parser,
|
|
91
|
-
tag_name,
|
|
92
|
-
level,
|
|
93
|
-
&normalized,
|
|
94
|
-
output,
|
|
95
|
-
options,
|
|
96
|
-
ctx,
|
|
97
|
-
depth,
|
|
98
|
-
dom_ctx,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
#[cfg(not(feature = "visitor"))]
|
|
102
|
-
let heading_output = {
|
|
103
|
-
let mut buf = String::new();
|
|
104
|
-
push_heading(&mut buf, ctx, options, level, normalized.as_ref());
|
|
105
|
-
Some(buf)
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
if let Some(heading_text) = heading_output {
|
|
109
|
-
output.push_str(&heading_text);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
#[cfg(feature = "metadata")]
|
|
113
|
-
if ctx.metadata_wants_headers {
|
|
114
|
-
if let Some(ref collector) = ctx.metadata_collector {
|
|
115
|
-
if let Some(node) = node_handle.get(parser) {
|
|
116
|
-
if let tl::Node::Tag(tag) = node {
|
|
117
|
-
let id = tag
|
|
118
|
-
.attributes()
|
|
119
|
-
.get("id")
|
|
120
|
-
.flatten()
|
|
121
|
-
.map(|v| v.as_utf8_str().to_string());
|
|
122
|
-
collector
|
|
123
|
-
.borrow_mut()
|
|
124
|
-
.add_header(level as u8, normalized.to_string(), id, depth, 0);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Notify the structure collector if present.
|
|
131
|
-
// Skip headings inside table cells — they are part of the table content,
|
|
132
|
-
// not standalone structural headings.
|
|
133
|
-
if !ctx.in_table_cell {
|
|
134
|
-
if let Some(ref sc) = ctx.structure_collector {
|
|
135
|
-
if let Some(node) = node_handle.get(parser) {
|
|
136
|
-
if let tl::Node::Tag(tag) = node {
|
|
137
|
-
let id = tag
|
|
138
|
-
.attributes()
|
|
139
|
-
.get("id")
|
|
140
|
-
.flatten()
|
|
141
|
-
.map(|v| v.as_utf8_str().to_string());
|
|
142
|
-
sc.borrow_mut()
|
|
143
|
-
.push_heading(level as u8, normalized.as_ref(), id.as_deref());
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/// Determine if a heading element should allow inline images.
|
|
152
|
-
pub fn heading_allows_inline_images(
|
|
153
|
-
tag_name: &str,
|
|
154
|
-
keep_inline_images_in: &std::rc::Rc<std::collections::HashSet<String>>,
|
|
155
|
-
) -> bool {
|
|
156
|
-
keep_inline_images_in.contains(tag_name)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/// Normalize heading text by replacing newlines with spaces.
|
|
160
|
-
fn normalize_heading_text(text: &str) -> Cow<'_, str> {
|
|
161
|
-
if !text.contains('\n') && !text.contains('\r') {
|
|
162
|
-
return Cow::Borrowed(text);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
let mut normalized = String::with_capacity(text.len());
|
|
166
|
-
let mut pending_space = false;
|
|
167
|
-
|
|
168
|
-
for ch in text.chars() {
|
|
169
|
-
match ch {
|
|
170
|
-
'\n' | '\r' => {
|
|
171
|
-
if !normalized.is_empty() {
|
|
172
|
-
pending_space = true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
' ' | '\t' if pending_space => {}
|
|
176
|
-
_ => {
|
|
177
|
-
if pending_space {
|
|
178
|
-
if !normalized.ends_with(' ') {
|
|
179
|
-
normalized.push(' ');
|
|
180
|
-
}
|
|
181
|
-
pending_space = false;
|
|
182
|
-
}
|
|
183
|
-
normalized.push(ch);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
Cow::Owned(normalized)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/// Format heading output with appropriate markdown syntax.
|
|
192
|
-
pub fn push_heading(output: &mut String, ctx: &Context, options: &ConversionOptions, level: usize, text: &str) {
|
|
193
|
-
if text.is_empty() {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if ctx.convert_as_inline {
|
|
198
|
-
output.push_str(text);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if ctx.in_table_cell {
|
|
203
|
-
let is_table_continuation =
|
|
204
|
-
!output.is_empty() && !output.ends_with('|') && !output.ends_with(' ') && !output.ends_with("<br>");
|
|
205
|
-
if is_table_continuation {
|
|
206
|
-
output.push_str("<br>");
|
|
207
|
-
}
|
|
208
|
-
output.push_str(text);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if ctx.in_list_item {
|
|
213
|
-
if output.ends_with('\n') {
|
|
214
|
-
if let Some(indent) = continuation_indent_string(ctx.list_depth, options) {
|
|
215
|
-
output.push_str(&indent);
|
|
216
|
-
}
|
|
217
|
-
} else if !output.ends_with(' ') && !output.is_empty() {
|
|
218
|
-
output.push(' ');
|
|
219
|
-
}
|
|
220
|
-
} else if !output.is_empty() && !output.ends_with("\n\n") {
|
|
221
|
-
if output.ends_with('\n') {
|
|
222
|
-
output.push('\n');
|
|
223
|
-
} else {
|
|
224
|
-
crate::converter::trim_trailing_whitespace(output);
|
|
225
|
-
output.push_str("\n\n");
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
let heading_suffix = if ctx.in_list_item || ctx.blockquote_depth > 0 {
|
|
230
|
-
"\n"
|
|
231
|
-
} else {
|
|
232
|
-
"\n\n"
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
match options.heading_style {
|
|
236
|
-
HeadingStyle::Underlined => {
|
|
237
|
-
if level == 1 {
|
|
238
|
-
output.push_str(text);
|
|
239
|
-
output.push('\n');
|
|
240
|
-
for _ in 0..text.len() {
|
|
241
|
-
output.push('=');
|
|
242
|
-
}
|
|
243
|
-
} else if level == 2 {
|
|
244
|
-
output.push_str(text);
|
|
245
|
-
output.push('\n');
|
|
246
|
-
for _ in 0..text.len() {
|
|
247
|
-
output.push('-');
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
for _ in 0..level {
|
|
251
|
-
output.push('#');
|
|
252
|
-
}
|
|
253
|
-
output.push(' ');
|
|
254
|
-
output.push_str(text);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
HeadingStyle::Atx => {
|
|
258
|
-
for _ in 0..level {
|
|
259
|
-
output.push('#');
|
|
260
|
-
}
|
|
261
|
-
output.push(' ');
|
|
262
|
-
output.push_str(text);
|
|
263
|
-
}
|
|
264
|
-
HeadingStyle::AtxClosed => {
|
|
265
|
-
for _ in 0..level {
|
|
266
|
-
output.push('#');
|
|
267
|
-
}
|
|
268
|
-
output.push(' ');
|
|
269
|
-
output.push_str(text);
|
|
270
|
-
output.push(' ');
|
|
271
|
-
for _ in 0..level {
|
|
272
|
-
output.push('#');
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
output.push_str(heading_suffix);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/// Get continuation indent string for list items.
|
|
280
|
-
fn continuation_indent_string(list_depth: usize, _options: &ConversionOptions) -> Option<String> {
|
|
281
|
-
if list_depth == 0 {
|
|
282
|
-
return None;
|
|
283
|
-
}
|
|
284
|
-
let mut indent = String::new();
|
|
285
|
-
for _ in 0..(4 * list_depth) {
|
|
286
|
-
indent.push(' ');
|
|
287
|
-
}
|
|
288
|
-
Some(indent)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/// Process heading with visitor callback if available.
|
|
292
|
-
#[cfg(feature = "visitor")]
|
|
293
|
-
fn visitor_heading_output(
|
|
294
|
-
node_handle: &NodeHandle,
|
|
295
|
-
parser: &Parser,
|
|
296
|
-
tag_name: &str,
|
|
297
|
-
level: usize,
|
|
298
|
-
normalized: &str,
|
|
299
|
-
_output: &mut String,
|
|
300
|
-
options: &ConversionOptions,
|
|
301
|
-
ctx: &Context,
|
|
302
|
-
depth: usize,
|
|
303
|
-
dom_ctx: &DomContext,
|
|
304
|
-
) -> Option<String> {
|
|
305
|
-
use crate::visitor::{NodeContext, NodeType, VisitResult};
|
|
306
|
-
|
|
307
|
-
if let Some(ref visitor_handle) = ctx.visitor {
|
|
308
|
-
if let Some(node) = node_handle.get(parser) {
|
|
309
|
-
if let tl::Node::Tag(tag) = node {
|
|
310
|
-
let id_attr = tag
|
|
311
|
-
.attributes()
|
|
312
|
-
.get("id")
|
|
313
|
-
.flatten()
|
|
314
|
-
.map(|v| v.as_utf8_str().to_string());
|
|
315
|
-
|
|
316
|
-
let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);
|
|
317
|
-
|
|
318
|
-
let node_id = node_handle.get_inner();
|
|
319
|
-
let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
|
|
320
|
-
let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
|
|
321
|
-
|
|
322
|
-
let node_ctx = NodeContext {
|
|
323
|
-
node_type: NodeType::Heading,
|
|
324
|
-
tag_name: tag_name.to_string(),
|
|
325
|
-
attributes,
|
|
326
|
-
depth,
|
|
327
|
-
index_in_parent,
|
|
328
|
-
parent_tag,
|
|
329
|
-
is_inline: false,
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
let visit_result = {
|
|
333
|
-
let mut visitor = visitor_handle.borrow_mut();
|
|
334
|
-
visitor.visit_heading(&node_ctx, level as u32, normalized, id_attr.as_deref())
|
|
335
|
-
};
|
|
336
|
-
match visit_result {
|
|
337
|
-
VisitResult::Continue => {
|
|
338
|
-
let mut buf = String::new();
|
|
339
|
-
push_heading(&mut buf, ctx, options, level, normalized);
|
|
340
|
-
Some(buf)
|
|
341
|
-
}
|
|
342
|
-
VisitResult::Custom(custom) => Some(custom),
|
|
343
|
-
VisitResult::Skip => None,
|
|
344
|
-
VisitResult::Error(err) => {
|
|
345
|
-
if ctx.visitor_error.borrow().is_none() {
|
|
346
|
-
*ctx.visitor_error.borrow_mut() = Some(err);
|
|
347
|
-
}
|
|
348
|
-
None
|
|
349
|
-
}
|
|
350
|
-
VisitResult::PreserveHtml => {
|
|
351
|
-
let mut buf = String::new();
|
|
352
|
-
push_heading(&mut buf, ctx, options, level, normalized);
|
|
353
|
-
Some(buf)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
} else {
|
|
357
|
-
None
|
|
358
|
-
}
|
|
359
|
-
} else {
|
|
360
|
-
None
|
|
361
|
-
}
|
|
362
|
-
} else {
|
|
363
|
-
let mut buf = String::new();
|
|
364
|
-
push_heading(&mut buf, ctx, options, level, normalized);
|
|
365
|
-
Some(buf)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/// Find a single heading element within a node, filtering out non-heading content.
|
|
370
|
-
///
|
|
371
|
-
/// Returns the heading level and node handle if the node contains exactly one
|
|
372
|
-
/// heading with no other non-whitespace content. Returns None if:
|
|
373
|
-
/// - The node is not a tag
|
|
374
|
-
/// - Multiple headings are found
|
|
375
|
-
/// - Non-whitespace non-heading content exists
|
|
376
|
-
/// - Non-text comments exist
|
|
377
|
-
pub fn find_single_heading_child(node_handle: NodeHandle, parser: &Parser) -> Option<(usize, NodeHandle)> {
|
|
378
|
-
let node = node_handle.get(parser)?;
|
|
379
|
-
|
|
380
|
-
let tl::Node::Tag(tag) = node else {
|
|
381
|
-
return None;
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
let children = tag.children();
|
|
385
|
-
let mut heading_data: Option<(usize, NodeHandle)> = None;
|
|
386
|
-
|
|
387
|
-
for child_handle in children.top().iter() {
|
|
388
|
-
let Some(child_node) = child_handle.get(parser) else {
|
|
389
|
-
continue;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
match child_node {
|
|
393
|
-
tl::Node::Raw(bytes) => {
|
|
394
|
-
if !bytes.as_utf8_str().trim().is_empty() {
|
|
395
|
-
return None;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
tl::Node::Tag(child_tag) => {
|
|
399
|
-
let name = crate::converter::utility::content::normalized_tag_name(child_tag.name().as_utf8_str());
|
|
400
|
-
{
|
|
401
|
-
let level = heading_level_from_name(name.as_ref())?;
|
|
402
|
-
if heading_data.is_some() {
|
|
403
|
-
return None;
|
|
404
|
-
}
|
|
405
|
-
heading_data = Some((level, *child_handle));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
tl::Node::Comment(_) => return None,
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
heading_data
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/// Extract heading level from tag name (h1-h6).
|
|
416
|
-
///
|
|
417
|
-
/// Returns Some(level) for valid heading tags, None otherwise.
|
|
418
|
-
fn heading_level_from_name(name: &str) -> Option<usize> {
|
|
419
|
-
match name {
|
|
420
|
-
"h1" => Some(1),
|
|
421
|
-
"h2" => Some(2),
|
|
422
|
-
"h3" => Some(3),
|
|
423
|
-
"h4" => Some(4),
|
|
424
|
-
"h5" => Some(5),
|
|
425
|
-
"h6" => Some(6),
|
|
426
|
-
_ => None,
|
|
427
|
-
}
|
|
428
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
//! Handler for horizontal rule elements (hr).
|
|
2
|
-
//!
|
|
3
|
-
//! Converts HTML horizontal rule tags to Markdown horizontal rules (---)
|
|
4
|
-
//! with appropriate spacing handling based on context.
|
|
5
|
-
|
|
6
|
-
use crate::converter::main_helpers::trim_trailing_whitespace;
|
|
7
|
-
use crate::converter::utility::siblings::get_previous_sibling_tag;
|
|
8
|
-
use tl::{NodeHandle, Parser};
|
|
9
|
-
|
|
10
|
-
// Type aliases for Context and DomContext to avoid circular imports
|
|
11
|
-
type Context = crate::converter::Context;
|
|
12
|
-
type DomContext = crate::converter::DomContext;
|
|
13
|
-
|
|
14
|
-
/// Handle horizontal rule elements (hr).
|
|
15
|
-
///
|
|
16
|
-
/// Converts to Markdown horizontal rule (---) with appropriate blank line
|
|
17
|
-
/// spacing based on context and previous siblings.
|
|
18
|
-
#[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
|
|
19
|
-
pub fn handle(
|
|
20
|
-
node_handle: &NodeHandle,
|
|
21
|
-
parser: &Parser,
|
|
22
|
-
output: &mut String,
|
|
23
|
-
_options: &crate::options::ConversionOptions,
|
|
24
|
-
ctx: &Context,
|
|
25
|
-
depth: usize,
|
|
26
|
-
dom_ctx: &DomContext,
|
|
27
|
-
) {
|
|
28
|
-
#[cfg(feature = "visitor")]
|
|
29
|
-
if let Some(ref visitor_handle) = ctx.visitor {
|
|
30
|
-
use crate::converter::utility::content::collect_tag_attributes;
|
|
31
|
-
use crate::visitor::{NodeContext, NodeType, VisitResult};
|
|
32
|
-
use std::collections::BTreeMap;
|
|
33
|
-
|
|
34
|
-
let tag = match node_handle.get(parser) {
|
|
35
|
-
Some(tl::Node::Tag(t)) => t,
|
|
36
|
-
_ => return,
|
|
37
|
-
};
|
|
38
|
-
let attributes: BTreeMap<String, String> = collect_tag_attributes(tag);
|
|
39
|
-
let node_id = node_handle.get_inner();
|
|
40
|
-
let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
|
|
41
|
-
let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
|
|
42
|
-
let node_ctx = NodeContext {
|
|
43
|
-
node_type: NodeType::Hr,
|
|
44
|
-
tag_name: "hr".to_string(),
|
|
45
|
-
attributes,
|
|
46
|
-
depth,
|
|
47
|
-
index_in_parent,
|
|
48
|
-
parent_tag,
|
|
49
|
-
is_inline: false,
|
|
50
|
-
};
|
|
51
|
-
let visit_result = {
|
|
52
|
-
let mut visitor = visitor_handle.borrow_mut();
|
|
53
|
-
visitor.visit_horizontal_rule(&node_ctx)
|
|
54
|
-
};
|
|
55
|
-
match visit_result {
|
|
56
|
-
VisitResult::Continue => {}
|
|
57
|
-
VisitResult::Skip => return,
|
|
58
|
-
VisitResult::Custom(custom) => {
|
|
59
|
-
output.push_str(&custom);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
VisitResult::PreserveHtml => {
|
|
63
|
-
use crate::converter::utility::serialization::serialize_node;
|
|
64
|
-
output.push_str(&serialize_node(node_handle, parser));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
VisitResult::Error(err) => {
|
|
68
|
-
if ctx.visitor_error.borrow().is_none() {
|
|
69
|
-
*ctx.visitor_error.borrow_mut() = Some(err);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if !output.is_empty() {
|
|
77
|
-
let prev_tag = get_previous_sibling_tag(node_handle, parser, dom_ctx);
|
|
78
|
-
let last_line_is_blockquote = output
|
|
79
|
-
.rsplit('\n')
|
|
80
|
-
.find(|line| !line.trim().is_empty())
|
|
81
|
-
.is_some_and(|line| line.trim_start().starts_with('>'));
|
|
82
|
-
let needs_blank_line = !ctx.in_paragraph && !matches!(prev_tag, Some("blockquote")) && !last_line_is_blockquote;
|
|
83
|
-
|
|
84
|
-
// If previous element was a blockquote, it added \n\n; reduce to \n
|
|
85
|
-
if matches!(prev_tag, Some("blockquote")) && output.ends_with("\n\n") {
|
|
86
|
-
output.truncate(output.len() - 1);
|
|
87
|
-
} else if ctx.in_paragraph || !needs_blank_line {
|
|
88
|
-
if !output.ends_with('\n') {
|
|
89
|
-
output.push('\n');
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
trim_trailing_whitespace(output);
|
|
93
|
-
if output.ends_with('\n') {
|
|
94
|
-
if !output.ends_with("\n\n") {
|
|
95
|
-
output.push('\n');
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
output.push_str("\n\n");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
output.push_str("---\n");
|
|
103
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
//! Handler for line break elements (br).
|
|
2
|
-
//!
|
|
3
|
-
//! Converts HTML line break tags to Markdown line breaks using the configured
|
|
4
|
-
//! newline style (spaces, backslash, or plain newline).
|
|
5
|
-
|
|
6
|
-
use crate::converter::main_helpers::trim_trailing_whitespace;
|
|
7
|
-
use crate::options::{ConversionOptions, NewlineStyle};
|
|
8
|
-
use tl::{NodeHandle, Parser};
|
|
9
|
-
|
|
10
|
-
// Type aliases for Context and DomContext to avoid circular imports
|
|
11
|
-
type Context = crate::converter::Context;
|
|
12
|
-
type DomContext = crate::converter::DomContext;
|
|
13
|
-
|
|
14
|
-
/// Handle line break elements (br).
|
|
15
|
-
///
|
|
16
|
-
/// Converts to appropriate Markdown line break syntax based on the configured
|
|
17
|
-
/// newline style and current context (e.g., in headings).
|
|
18
|
-
#[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
|
|
19
|
-
pub fn handle(
|
|
20
|
-
node_handle: &NodeHandle,
|
|
21
|
-
parser: &Parser,
|
|
22
|
-
output: &mut String,
|
|
23
|
-
options: &ConversionOptions,
|
|
24
|
-
ctx: &Context,
|
|
25
|
-
depth: usize,
|
|
26
|
-
dom_ctx: &DomContext,
|
|
27
|
-
) {
|
|
28
|
-
#[cfg(feature = "visitor")]
|
|
29
|
-
if let Some(ref visitor_handle) = ctx.visitor {
|
|
30
|
-
use crate::converter::utility::content::collect_tag_attributes;
|
|
31
|
-
use crate::visitor::{NodeContext, NodeType, VisitResult};
|
|
32
|
-
use std::collections::BTreeMap;
|
|
33
|
-
|
|
34
|
-
let attributes = if let Some(tl::Node::Tag(t)) = node_handle.get(parser) {
|
|
35
|
-
collect_tag_attributes(t)
|
|
36
|
-
} else {
|
|
37
|
-
BTreeMap::new()
|
|
38
|
-
};
|
|
39
|
-
let node_id = node_handle.get_inner();
|
|
40
|
-
let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
|
|
41
|
-
let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
|
|
42
|
-
let node_ctx = NodeContext {
|
|
43
|
-
node_type: NodeType::Br,
|
|
44
|
-
tag_name: "br".to_string(),
|
|
45
|
-
attributes,
|
|
46
|
-
depth,
|
|
47
|
-
index_in_parent,
|
|
48
|
-
parent_tag,
|
|
49
|
-
is_inline: true,
|
|
50
|
-
};
|
|
51
|
-
let visit_result = {
|
|
52
|
-
let mut visitor = visitor_handle.borrow_mut();
|
|
53
|
-
visitor.visit_line_break(&node_ctx)
|
|
54
|
-
};
|
|
55
|
-
match visit_result {
|
|
56
|
-
VisitResult::Continue => {}
|
|
57
|
-
VisitResult::Skip => return,
|
|
58
|
-
VisitResult::Custom(custom) => {
|
|
59
|
-
output.push_str(&custom);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
VisitResult::PreserveHtml => {
|
|
63
|
-
use crate::converter::utility::serialization::serialize_node;
|
|
64
|
-
output.push_str(&serialize_node(node_handle, parser));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
VisitResult::Error(err) => {
|
|
68
|
-
if ctx.visitor_error.borrow().is_none() {
|
|
69
|
-
*ctx.visitor_error.borrow_mut() = Some(err);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if ctx.in_heading {
|
|
77
|
-
trim_trailing_whitespace(output);
|
|
78
|
-
output.push_str(" ");
|
|
79
|
-
} else {
|
|
80
|
-
if output.is_empty() || output.ends_with('\n') {
|
|
81
|
-
output.push('\n');
|
|
82
|
-
} else {
|
|
83
|
-
match options.newline_style {
|
|
84
|
-
NewlineStyle::Spaces => output.push_str(" \n"),
|
|
85
|
-
NewlineStyle::Backslash => output.push_str("\\\n"),
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|