//! Markdown rendering for the TUI transcript. //! //! This renderer intentionally treats local file links differently from normal web links. For //! local paths, the displayed text comes from the destination, not the markdown label, so //! transcripts show the real file target (including normalized location suffixes) and can shorten //! absolute paths relative to a known working directory. 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 dirs::home_dir; 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; 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, /*width*/ None) } /// Render markdown using the current process working directory for local file-link display. pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) -> Text<'static> { let cwd = std::env::current_dir().ok(); render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref()) } /// Render markdown with an explicit working directory for local file links. /// /// The `cwd` parameter controls how absolute local targets are shortened before display. Passing /// the session cwd keeps full renders, history cells, and streamed deltas visually aligned even /// when rendering happens away from the process cwd. 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, /// Pre-rendered display text for local file links. /// /// When this is present, the markdown label is intentionally suppressed so the rendered /// transcript always reflects the real target path. local_target_display: 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}"), }); struct Writer<'a, I> where I: Iterator>, { iter: I, text: Text<'static>, styles: MarkdownStyles, inline_styles: Vec