This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::ast::Node;use crate::text::{concat, remove_text_presentation, trim};
pub fn normalize_nodes(nodes: &mut Vec<Node>, inside_blockquote: bool) { for node in nodes.iter_mut() { normalize_node(node); } combine_adjacent_text(nodes, inside_blockquote);}
pub fn normalize_child_nodes(nodes: &mut Vec<Node>, inside_blockquote: bool) { normalize_nodes(nodes, inside_blockquote); merge_adjacent_text_simple(nodes); compact_empty_text_nodes(nodes);}
pub fn normalize_node(node: &mut Node) { match node { Node::Text { content } => *content = remove_text_presentation(content), Node::Strong { children } | Node::Emphasis { children } | Node::Underline { children } | Node::Strikethrough { children } | Node::Subtext { children } | Node::Sequence { children } | Node::TableCell { children } | Node::Alert { children, .. } => normalize_child_nodes(children, false), Node::Spoiler { children, .. } => normalize_child_nodes(children, false), Node::Heading { children, .. } => normalize_child_nodes(children, false), Node::Blockquote { children, .. } => normalize_child_nodes(children, true), Node::List { items, .. } => { for item in items { normalize_child_nodes(&mut item.children, false); } } Node::Link { text, .. } => { if let Some(child) = text { normalize_node(child); } } Node::Table { header, rows, .. } => { normalize_node(header); for row in rows { normalize_node(row); } } Node::TableRow { cells } => { for cell in cells { normalize_node(cell); } } Node::CodeBlock { .. } | Node::InlineCode { .. } | Node::Mention { .. } | Node::Timestamp { .. } | Node::Emoji { .. } => {} }}
pub fn apply_text_presentation_node(node: &mut Node) { match node { Node::Text { content } => *content = remove_text_presentation(content), Node::Strong { children } | Node::Emphasis { children } | Node::Underline { children } | Node::Strikethrough { children } | Node::Subtext { children } | Node::Sequence { children } | Node::TableCell { children } | Node::Alert { children, .. } => { for child in children { apply_text_presentation_node(child); } } Node::Spoiler { children, .. } => { for child in children { apply_text_presentation_node(child); } } Node::Heading { children, .. } => { for child in children { apply_text_presentation_node(child); } } Node::Blockquote { children, .. } => { for child in children { apply_text_presentation_node(child); } } Node::List { items, .. } => { for item in items { for child in &mut item.children { apply_text_presentation_node(child); } } } Node::Link { text, .. } => { if let Some(child) = text { apply_text_presentation_node(child); } } Node::Table { header, rows, .. } => { apply_text_presentation_node(header); for row in rows { apply_text_presentation_node(row); } } Node::TableRow { cells } => { for cell in cells { apply_text_presentation_node(cell); } } Node::CodeBlock { .. } | Node::InlineCode { .. } | Node::Mention { .. } | Node::Timestamp { .. } | Node::Emoji { .. } => {} }}
pub fn flatten_top_level_formatting(nodes: &mut [Node]) { for node in nodes { match node { Node::Strong { children } => flatten_same_formatting_type(children, "Strong"), Node::Emphasis { children } => flatten_same_formatting_type(children, "Emphasis"), Node::Underline { children } => flatten_same_formatting_type(children, "Underline"), Node::Strikethrough { children } => { flatten_same_formatting_type(children, "Strikethrough") } Node::Spoiler { children, .. } => flatten_same_formatting_type(children, "Spoiler"), _ => continue, } if let Some(children) = formatting_children_mut(node) { combine_adjacent_text(children, false); compact_empty_text_nodes(children); } }}
fn formatting_children_mut(node: &mut Node) -> Option<&mut Vec<Node>> { match node { Node::Strong { children } | Node::Emphasis { children } | Node::Underline { children } | Node::Strikethrough { children } | Node::Spoiler { children, .. } => Some(children), _ => None, }}
fn flatten_same_formatting_type(children: &mut Vec<Node>, node_type: &'static str) { if children.len() <= 1 || !children.iter().any(|child| child.tag_name() == node_type) { return; } let mut out = Vec::with_capacity(children.len()); for child in std::mem::take(children) { if child.tag_name() == node_type { match child { Node::Strong { children } | Node::Emphasis { children } | Node::Underline { children } | Node::Strikethrough { children } | Node::Spoiler { children, .. } => out.extend(children), other => out.push(other), } } else { out.push(child); } } *children = out;}
pub fn combine_adjacent_text(nodes: &mut Vec<Node>, inside_blockquote: bool) { if nodes.len() <= 1 { return; } let mut last_was_text = false; let has_adjacent_text = nodes.iter().any(|node| { let is_text = matches!(node, Node::Text { .. }); let adjacent = is_text && last_was_text; last_was_text = is_text; adjacent }); if !has_adjacent_text && !inside_blockquote { return; }
let mut out = Vec::with_capacity(nodes.len()); if inside_blockquote { let mut current_text = String::new(); let mut non_text_seen = false; for node in std::mem::take(nodes) { match node { Node::Text { content } => { if non_text_seen { if !current_text.is_empty() { out.push(Node::Text { content: std::mem::take(&mut current_text), }); } non_text_seen = false; } current_text.push_str(&content); } other => { if !current_text.is_empty() { out.push(Node::Text { content: std::mem::take(&mut current_text), }); } out.push(other); non_text_seen = true; } } } if !current_text.is_empty() { out.push(Node::Text { content: current_text, }); } } else { let mut current_text: Option<String> = None; for node in std::mem::take(nodes) { match node { Node::Text { content } => { if content.is_empty() { continue; } if is_malformed_block_text(&content) { if let Some(value) = current_text.take() { out.push(Node::Text { content: value }); } out.push(Node::Text { content }); } else if let Some(value) = current_text.as_mut() { if content.contains("\n\n") { let previous = std::mem::take(value); out.push(Node::Text { content: previous }); out.push(Node::Text { content }); current_text = None; } else { value.push_str(&content); } } else { current_text = Some(content); } } other => { if let Some(value) = current_text.take() { out.push(Node::Text { content: value }); } out.push(other); } } } if let Some(value) = current_text { out.push(Node::Text { content: value }); } } *nodes = out;}
pub fn merge_adjacent_text_simple(nodes: &mut Vec<Node>) { if nodes.len() <= 1 { return; } let mut last_was_text = false; let has_adjacent_text = nodes.iter().any(|node| { let is_text = matches!(node, Node::Text { .. }); let adjacent = is_text && last_was_text; last_was_text = is_text; adjacent }); if !has_adjacent_text { return; } let mut out = Vec::with_capacity(nodes.len()); let mut current_text = String::new(); for node in std::mem::take(nodes) { match node { Node::Text { content } => current_text.push_str(&content), other => { if !current_text.is_empty() { out.push(Node::Text { content: std::mem::take(&mut current_text), }); } out.push(other); } } } if !current_text.is_empty() { out.push(Node::Text { content: current_text, }); } *nodes = out;}
pub fn compact_empty_text_nodes(nodes: &mut Vec<Node>) { if nodes.len() <= 1 { return; } if nodes.iter().any(Node::is_empty_text) { nodes.retain(|node| !node.is_empty_text()); }}
pub fn is_malformed_block_text(content: &str) -> bool { if content.is_empty() || !(content.starts_with('#') || content.starts_with("-#")) { return false; } let trimmed_content = trim(content); trimmed_content.starts_with('#') || trimmed_content.starts_with("-#")}
pub fn replace_trailing_whitespace_with_newline(content: &str) -> String { let trimmed = crate::text::trim_right(content); if trimmed.len() == content.len() { return content.to_owned(); } concat(trimmed, "\n")}