use crate::converter::main_helpers::{tag_name_eq, trim_trailing_whitespace};
use crate::options::{ConversionOptions, ListIndentType};
use tl;
type Context = crate::converter::Context;
type DomContext = crate::converter::DomContext;
pub const DEFAULT_ORDERED_LIST_START: i64 = 1;
pub fn parse_ordered_list_start(raw: &str) -> i64 {
let trimmed = raw.trim();
if let Ok(value) = trimmed.parse::<i64>() {
return value;
}
let (is_negative, digits) = match trimmed.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, trimmed.strip_prefix('+').unwrap_or(trimmed)),
};
if !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) {
let clamped = if is_negative { i64::MIN } else { i64::MAX };
tracing::warn!(
target: "html_to_markdown::list",
raw_value = trimmed,
clamped_value = clamped,
"ol start attribute magnitude out of range; clamping to i64 bounds"
);
return clamped;
}
if !trimmed.is_empty() {
tracing::warn!(
target: "html_to_markdown::list",
raw_value = trimmed,
default_value = DEFAULT_ORDERED_LIST_START,
"ol start attribute is not a valid integer; using default start"
);
}
DEFAULT_ORDERED_LIST_START
}
pub const fn calculate_list_continuation_indent(depth: usize) -> usize {
if depth > 0 { 2 * depth - 1 } else { 0 }
}
pub fn is_loose_list(node_handle: tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {
if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
let children = tag.children();
{
for child_handle in children.top().iter() {
let is_li = dom_ctx.tag_info(child_handle.get_inner(), parser).map_or_else(
|| {
matches!(
child_handle.get(parser),
Some(tl::Node::Tag(child_tag))
if tag_name_eq(child_tag.name().as_utf8_str(), "li")
)
},
|info| info.name == "li",
);
if !is_li {
continue;
}
if let Some(tl::Node::Tag(child_tag)) = child_handle.get(parser) {
let li_children = child_tag.children();
for li_child_handle in li_children.top().iter() {
let is_p = dom_ctx.tag_info(li_child_handle.get_inner(), parser).map_or_else(
|| {
matches!(
li_child_handle.get(parser),
Some(tl::Node::Tag(li_child_tag))
if tag_name_eq(li_child_tag.name().as_utf8_str(), "p")
)
},
|info| info.name == "p",
);
if is_p {
return true;
}
}
}
}
}
}
false
}
pub fn add_list_continuation_indent(
output: &mut String,
list_depth: usize,
list_indent_columns: usize,
blank_line: bool,
options: &ConversionOptions,
) {
trim_trailing_whitespace(output);
if blank_line {
if !output.ends_with("\n\n") {
if output.ends_with('\n') {
output.push('\n');
} else {
output.push_str("\n\n");
}
}
} else if !output.ends_with('\n') {
output.push('\n');
}
match options.list_indent_type {
ListIndentType::Tabs => {
let indent_level = calculate_list_continuation_indent(list_depth);
for _ in 0..indent_level {
output.push('\t');
}
}
ListIndentType::Spaces => {
for _ in 0..list_indent_columns {
output.push(' ');
}
}
}
}
pub fn continuation_indent_string(
list_depth: usize,
list_indent_columns: usize,
options: &ConversionOptions,
) -> Option<String> {
match options.list_indent_type {
ListIndentType::Tabs => {
let indent_level = calculate_list_continuation_indent(list_depth);
if indent_level == 0 {
return None;
}
Some("\t".repeat(indent_level))
}
ListIndentType::Spaces => {
if list_indent_columns == 0 {
return None;
}
Some(" ".repeat(list_indent_columns))
}
}
}
pub fn add_list_leading_separator(output: &mut String, ctx: &Context) {
if ctx.in_table_cell {
let is_table_continuation =
!output.is_empty() && !output.ends_with('|') && !output.ends_with(' ') && !output.ends_with("<br>");
if is_table_continuation {
output.push_str("<br>");
}
return;
}
if !output.is_empty() && !ctx.in_list {
let needs_newline =
!output.ends_with("\n\n") && !output.ends_with("* ") && !output.ends_with("- ") && !output.ends_with(". ");
if needs_newline {
output.push_str("\n\n");
}
return;
}
if ctx.in_list_item && !output.is_empty() {
let needs_newline =
!output.ends_with('\n') && !output.ends_with("* ") && !output.ends_with("- ") && !output.ends_with(". ");
if needs_newline {
trim_trailing_whitespace(output);
output.push('\n');
}
}
}
pub fn add_nested_list_trailing_separator(output: &mut String, ctx: &Context) {
if !ctx.in_list_item {
return;
}
if ctx.loose_list {
if !output.ends_with("\n\n") {
if !output.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
} else if !output.ends_with('\n') {
output.push('\n');
}
}
pub const fn calculate_list_nesting_depth(ctx: &Context) -> usize {
if ctx.in_list && !ctx.in_list_item {
ctx.list_depth + 1
} else {
ctx.list_depth
}
}
pub fn is_list_item(node_handle: tl::NodeHandle, parser: &tl::Parser, dom_ctx: &DomContext) -> bool {
if let Some(info) = dom_ctx.tag_info(node_handle.get_inner(), parser) {
return info.name == "li";
}
matches!(
node_handle.get(parser),
Some(tl::Node::Tag(tag)) if tag_name_eq(tag.name().as_utf8_str(), "li")
)
}
#[allow(clippy::too_many_arguments)]
pub fn process_list_children(
node_handle: tl::NodeHandle,
parser: &tl::Parser,
output: &mut String,
options: &ConversionOptions,
ctx: &Context,
depth: usize,
is_ordered: bool,
is_loose: bool,
nested_depth: usize,
start_counter: i64,
dom_ctx: &DomContext,
) {
let mut counter = start_counter;
let mut counter_saturated = false;
if let Some(tl::Node::Tag(tag)) = node_handle.get(parser) {
let children = tag.children();
{
let mut list_ctx = Context {
in_ordered_list: is_ordered,
list_counter: if is_ordered { counter } else { 0 },
in_list: true,
list_depth: nested_depth,
ul_depth: if is_ordered { ctx.ul_depth } else { ctx.ul_depth + 1 },
loose_list: is_loose,
prev_item_had_blocks: false,
..ctx.clone()
};
for child_handle in children.top().iter() {
if let Some(tl::Node::Raw(bytes)) = child_handle.get(parser) {
if bytes.as_utf8_str().trim().is_empty() {
continue;
}
}
if is_ordered {
list_ctx.list_counter = counter;
}
use crate::converter::walk_node;
walk_node(child_handle, parser, output, options, &list_ctx, depth + 1, dom_ctx);
if is_ordered && is_list_item(*child_handle, parser, dom_ctx) {
if counter == i64::MAX {
if !counter_saturated {
tracing::warn!(
target: "html_to_markdown::list",
counter,
"ordered list counter reached i64::MAX; subsequent items repeat this value"
);
counter_saturated = true;
}
} else {
counter += 1;
}
}
}
}
}
}