use crate::converter::Context;
use crate::converter::dom_context::DomContext;
use crate::converter::main::walk_node;
use crate::converter::text::dedent_code_block;
use crate::options::ConversionOptions;
#[cfg(feature = "visitor")]
use crate::converter::utility::serialization::serialize_node;
#[cfg(feature = "visitor")]
use std::borrow::Cow;
const MIN_FENCE_LENGTH: usize = 3;
fn longest_consecutive_run(content: &str, marker: char) -> usize {
content
.chars()
.fold((0usize, 0usize), |(max, current), c| {
if c == marker {
let next = current + 1;
(max.max(next), next)
} else {
(max, 0)
}
})
.0
}
fn min_safe_code_span_delimiter_length(content: &str) -> usize {
let mut run_lengths = std::collections::HashSet::new();
let mut current = 0usize;
for c in content.chars() {
if c == '`' {
current += 1;
} else {
if current > 0 {
run_lengths.insert(current);
}
current = 0;
}
}
if current > 0 {
run_lengths.insert(current);
}
let mut candidate = 1usize;
while run_lengths.contains(&candidate) {
candidate += 1;
}
candidate
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
#[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
pub fn handle_code(
node_handle: &tl::NodeHandle,
tag: &tl::HTMLTag,
parser: &tl::Parser,
output: &mut String,
options: &ConversionOptions,
ctx: &Context,
depth: usize,
dom_ctx: &DomContext,
) {
let code_ctx = Context {
in_code: true,
..ctx.clone()
};
if ctx.in_code {
let children = tag.children();
{
for child_handle in children.top().iter() {
walk_node(child_handle, parser, output, options, &code_ctx, depth + 1, dom_ctx);
}
}
} else {
let mut content = String::with_capacity(32);
let children = tag.children();
{
for child_handle in children.top().iter() {
walk_node(
child_handle,
parser,
&mut content,
options,
&code_ctx,
depth + 1,
dom_ctx,
);
}
}
let trimmed = &content;
if !content.trim().is_empty() {
#[cfg(feature = "visitor")]
let code_output = if let Some(ref visitor_handle) = ctx.visitor {
use crate::visitor::{NodeContext, NodeType, VisitResult};
let node_id = node_handle.get_inner();
let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
let node_ctx = NodeContext::with_lazy_attributes(
NodeType::Code,
Cow::Borrowed("code"),
tag,
depth,
index_in_parent,
parent_tag.map(Cow::Borrowed),
true,
);
let visit_result = {
let mut visitor = visitor_handle.lock().expect("visitor mutex poisoned");
visitor.visit_code_inline(&node_ctx, trimmed)
};
match visit_result {
VisitResult::Continue => None,
VisitResult::Custom(custom) => Some(custom),
VisitResult::Skip => Some(String::new()),
VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),
VisitResult::Error(err) => {
if ctx.visitor_error.borrow().is_none() {
*ctx.visitor_error.borrow_mut() = Some(err);
}
None
}
}
} else {
None
};
#[cfg(feature = "visitor")]
if let Some(custom_output) = code_output {
output.push_str(&custom_output);
} else {
format_inline_code(trimmed, output);
}
#[cfg(not(feature = "visitor"))]
{
format_inline_code(trimmed, output);
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
#[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
pub fn handle_pre(
node_handle: &tl::NodeHandle,
tag: &tl::HTMLTag,
parser: &tl::Parser,
output: &mut String,
options: &ConversionOptions,
ctx: &Context,
depth: usize,
dom_ctx: &DomContext,
) {
let code_ctx = Context {
in_code: true,
..ctx.clone()
};
#[cfg_attr(not(feature = "visitor"), allow(unused_variables))]
let language: Option<String> = {
let mut lang: Option<String> = None;
if let Some(class_attr) = tag.attributes().get("class") {
if let Some(class_bytes) = class_attr {
let class_str = class_bytes.as_utf8_str();
for cls in class_str.split_whitespace() {
if let Some(stripped) = cls.strip_prefix("language-") {
lang = Some(String::from(stripped));
break;
} else if let Some(stripped) = cls.strip_prefix("lang-") {
lang = Some(String::from(stripped));
break;
}
}
}
}
if lang.is_none() {
let children = tag.children();
for child_handle in children.top().iter() {
if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {
if child_tag.name() == "code" {
if let Some(class_attr) = child_tag.attributes().get("class") {
if let Some(class_bytes) = class_attr {
let class_str = class_bytes.as_utf8_str();
for cls in class_str.split_whitespace() {
if let Some(stripped) = cls.strip_prefix("language-") {
lang = Some(String::from(stripped));
break;
} else if let Some(stripped) = cls.strip_prefix("lang-") {
lang = Some(String::from(stripped));
break;
}
}
}
}
break;
}
}
}
}
lang
};
let mut content = String::with_capacity(256);
let children = tag.children();
{
for child_handle in children.top().iter() {
walk_node(
child_handle,
parser,
&mut content,
options,
&code_ctx,
depth + 1,
dom_ctx,
);
}
}
if !content.is_empty() {
let leading_newlines = content.chars().take_while(|&c| c == '\n').count();
let trailing_newlines = content.chars().rev().take_while(|&c| c == '\n').count();
let core = content.trim_matches('\n');
let is_whitespace_only = core.trim().is_empty();
let processed_content = if options.whitespace_mode == crate::options::WhitespaceMode::Strict {
content
} else {
let mut core_text = dedent_code_block(core);
if is_whitespace_only {
let mut rebuilt = String::new();
for _ in 0..leading_newlines {
rebuilt.push('\n');
}
rebuilt.push_str(&core_text);
for _ in 0..trailing_newlines {
rebuilt.push('\n');
}
rebuilt
} else {
for _ in 0..trailing_newlines {
core_text.push('\n');
}
core_text
}
};
#[cfg(feature = "visitor")]
let code_block_output = if let Some(ref visitor_handle) = ctx.visitor {
use crate::visitor::{NodeContext, NodeType, VisitResult};
let node_id = node_handle.get_inner();
let parent_tag = dom_ctx.parent_tag_name(node_id, parser);
let index_in_parent = dom_ctx.get_sibling_index(node_id).unwrap_or(0);
let node_ctx = NodeContext::with_lazy_attributes(
NodeType::Pre,
Cow::Borrowed("pre"),
tag,
depth,
index_in_parent,
parent_tag.map(Cow::Borrowed),
false,
);
let visit_result = {
let mut visitor = visitor_handle.lock().expect("visitor mutex poisoned");
visitor.visit_code_block(&node_ctx, language.as_deref(), &processed_content)
};
match visit_result {
VisitResult::Continue => None,
VisitResult::Custom(custom) => Some(custom),
VisitResult::Skip => Some(String::new()),
VisitResult::PreserveHtml => Some(serialize_node(node_handle, parser)),
VisitResult::Error(err) => {
if ctx.visitor_error.borrow().is_none() {
*ctx.visitor_error.borrow_mut() = Some(err);
}
None
}
}
} else {
None
};
#[cfg(feature = "visitor")]
if let Some(custom_output) = code_block_output {
output.push_str(&custom_output);
} else {
format_code_block(&processed_content, language.as_deref(), output, options, ctx);
}
#[cfg(not(feature = "visitor"))]
{
format_code_block(&processed_content, language.as_deref(), output, options, ctx);
}
if let Some(ref sc) = ctx.structure_collector {
sc.borrow_mut().push_code(&processed_content, language.as_deref());
}
}
}
fn format_inline_code(content: &str, output: &mut String) {
let contains_backtick = content.contains('`');
let needs_delimiter_spaces = {
let first_char = content.chars().next();
let last_char = content.chars().last();
let starts_with_space = first_char == Some(' ');
let ends_with_space = last_char == Some(' ');
let starts_with_backtick = first_char == Some('`');
let ends_with_backtick = last_char == Some('`');
let all_spaces = content.chars().all(|c| c == ' ');
all_spaces
|| starts_with_backtick
|| ends_with_backtick
|| (starts_with_space && ends_with_space && contains_backtick)
};
let (num_backticks, needs_spaces) = if contains_backtick {
(min_safe_code_span_delimiter_length(content), needs_delimiter_spaces)
} else {
(1, needs_delimiter_spaces)
};
for _ in 0..num_backticks {
output.push('`');
}
if needs_spaces {
output.push(' ');
}
output.push_str(content);
if needs_spaces {
output.push(' ');
}
for _ in 0..num_backticks {
output.push('`');
}
}
fn format_code_block(
content: &str,
language: Option<&str>,
output: &mut String,
options: &ConversionOptions,
ctx: &Context,
) {
match options.code_block_style {
crate::options::CodeBlockStyle::Indented => {
if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with("\n\n") {
if output.ends_with('\n') {
output.push('\n');
} else {
output.push_str("\n\n");
}
}
let indented = content
.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n");
output.push_str(&indented);
output.push_str("\n\n");
}
crate::options::CodeBlockStyle::Backticks | crate::options::CodeBlockStyle::Tildes => {
if !ctx.convert_as_inline && !output.is_empty() && !output.ends_with("\n\n") {
if output.ends_with('\n') {
output.push('\n');
} else {
output.push_str("\n\n");
}
}
let fence_char = if options.code_block_style == crate::options::CodeBlockStyle::Backticks {
'`'
} else {
'~'
};
let fence_length = (longest_consecutive_run(content, fence_char) + 1).max(MIN_FENCE_LENGTH);
let fence: String = std::iter::repeat_n(fence_char, fence_length).collect();
output.push_str(&fence);
if let Some(lang) = language {
output.push_str(lang);
} else if !options.code_language.is_empty() {
output.push_str(&options.code_language);
}
output.push('\n');
output.push_str(content.trim_end_matches('\n'));
output.push('\n');
output.push_str(&fence);
output.push_str("\n\n");
}
}
}