use crate::render::highlight::highlight_code_to_lines; use crate::render::line_utils::line_to_static; use crate::wrapping::RtOptions; use crate::wrapping::adaptive_wrap_line; use codex_utils_string::normalize_markdown_hash_location_suffix; use pulldown_cmark::CodeBlockKind; use pulldown_cmark::CowStr; use pulldown_cmark::Event; use pulldown_cmark::HeadingLevel; use pulldown_cmark::Options; use pulldown_cmark::Parser; use pulldown_cmark::Tag; use pulldown_cmark::TagEnd; use ratatui::style::Style; use ratatui::text::Line; use ratatui::text::Span; use ratatui::text::Text; use regex_lite::Regex; use std::path::Path; use std::path::PathBuf; use std::sync::LazyLock; use url::Url; use urlencoding::decode; struct MarkdownStyles { h1: Style, h2: Style, h3: Style, h4: Style, h5: Style, h6: Style, code: Style, emphasis: Style, strong: Style, strikethrough: Style, ordered_list_marker: Style, unordered_list_marker: Style, link: Style, blockquote: Style, } impl Default for MarkdownStyles { fn default() -> Self { use ratatui::style::Stylize; Self { h1: Style::new().bold().underlined(), h2: Style::new().bold(), h3: Style::new().bold().italic(), h4: Style::new().italic(), h5: Style::new().italic(), h6: Style::new().italic(), code: Style::new().cyan(), emphasis: Style::new().italic(), strong: Style::new().bold(), strikethrough: Style::new().crossed_out(), ordered_list_marker: Style::new().light_blue(), unordered_list_marker: Style::new(), link: Style::new().cyan().underlined(), blockquote: Style::new().green(), } } } #[derive(Clone, Debug)] struct IndentContext { prefix: Vec>, marker: Option>>, is_list: bool, } impl IndentContext { fn new(prefix: Vec>, marker: Option>>, is_list: bool) -> Self { Self { prefix, marker, is_list, } } } pub fn render_markdown_text(input: &str) -> Text<'static> { render_markdown_text_with_width(input, None) } pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) -> Text<'static> { render_markdown_text_with_width_and_cwd(input, width, None) } pub(crate) fn render_markdown_text_with_width_and_cwd( input: &str, width: Option, cwd: Option<&Path>, ) -> Text<'static> { let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); let parser = Parser::new_ext(input, options); let mut w = Writer::new(parser, width, cwd); w.run(); w.text } #[derive(Clone, Debug)] struct LinkState { destination: String, show_destination: bool, local_display_text: Option, } fn should_render_link_destination(dest_url: &str) -> bool { !is_local_path_like_link(dest_url) } static COLON_LOCATION_SUFFIX_RE: LazyLock = LazyLock::new( || match Regex::new(r":\d+(?::\d+)?(?:[-–]\d+(?::\d+)?)?$") { Ok(regex) => regex, Err(error) => panic!("invalid location suffix regex: {error}"), }, ); // Covered by load_location_suffix_regexes. static HASH_LOCATION_SUFFIX_RE: LazyLock = LazyLock::new(|| match Regex::new(r"^L\d+(?:C\d+)?(?:-L\d+(?:C\d+)?)?$") { Ok(regex) => regex, Err(error) => panic!("invalid hash location regex: {error}"), }); fn is_local_path_like_link(dest_url: &str) -> bool { dest_url.starts_with("file://") || dest_url.starts_with('/') || dest_url.starts_with("~/") || dest_url.starts_with("./") || dest_url.starts_with("../") || dest_url.starts_with("\\\\") || matches!( dest_url.as_bytes(), [drive, b':', separator, ..] if drive.is_ascii_alphabetic() && matches!(separator, b'/' | b'\\') ) } fn local_link_display_text(dest_url: &str, cwd: Option<&Path>) -> Option { if !is_local_path_like_link(dest_url) { return None; } let (path, location_suffix) = split_local_link_destination(dest_url)?; let display_path = cwd.map_or_else( || path.display().to_string(), |cwd| match path.strip_prefix(cwd) { Ok(stripped) => stripped.display().to_string(), Err(_) => path.display().to_string(), }, ); Some(format!("{display_path}{location_suffix}")) } fn split_local_link_destination(dest_url: &str) -> Option<(PathBuf, String)> { if dest_url.starts_with("file://") { let url = Url::parse(dest_url).ok()?; let path = url.to_file_path().ok().or_else(|| { let decoded_path = decode(url.path()).ok()?.into_owned(); if decoded_path.is_empty() { return None; } let file_path = match url.host_str() { Some(host) if !host.is_empty() && host != "localhost" => { format!("//{host}{decoded_path}") } _ => decoded_path, }; Some(PathBuf::from(file_path)) })?; let location_suffix = url .fragment() .map(|fragment| format!("#{fragment}")) .and_then(|suffix| normalize_markdown_hash_location_suffix(&suffix)) .unwrap_or_default(); return Some((path, location_suffix)); } if let Some((path_part, fragment)) = dest_url.rsplit_once('#') && HASH_LOCATION_SUFFIX_RE.is_match(fragment) { let location_suffix = normalize_markdown_hash_location_suffix(&format!("#{fragment}")).unwrap_or_default(); return Some((PathBuf::from(path_part), location_suffix)); } if let Some(location_match) = COLON_LOCATION_SUFFIX_RE.find(dest_url) { let path_part = &dest_url[..location_match.start()]; return Some(( PathBuf::from(path_part), location_match.as_str().to_string(), )); } Some((PathBuf::from(dest_url), String::new())) } struct Writer<'a, I> where I: Iterator>, { iter: I, text: Text<'static>, styles: MarkdownStyles, inline_styles: Vec