diff --git a/Cargo.lock b/Cargo.lock index ffdc001..a0f76e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,19 @@ dependencies = [ "unicase", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.6.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + [[package]] name = "pulldown-cmark-escape" version = "0.11.0" @@ -1500,6 +1513,7 @@ dependencies = [ "csv", "futures", "ironcalc", + "pulldown-cmark 0.13.0", "ratatui", "serde_json", "slice-utils", @@ -1758,7 +1772,7 @@ dependencies = [ "ansi-to-tui", "itertools 0.13.0", "pretty_assertions", - "pulldown-cmark", + "pulldown-cmark 0.12.2", "ratatui", "rstest", "syntect", diff --git a/Cargo.toml b/Cargo.toml index d310e68..ca39db7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,4 @@ serde_json = "1.0.133" colorsys = "0.6.7" tui-markdown = { version = "0.3.1", features = [] } csv = "1.3.1" +pulldown-cmark = "0.13.0" diff --git a/src/ui/render/markdown.rs b/src/ui/render/markdown.rs new file mode 100644 index 0000000..ebb0013 --- /dev/null +++ b/src/ui/render/markdown.rs @@ -0,0 +1,211 @@ +use core::panic; +use std::collections::BTreeSet; + +use ratatui::{ + text::{Line, Span, Text}, + widgets::Widget, +}; + +use pulldown_cmark::{Event, HeadingLevel, LinkType, Parser, Tag, TagEnd, TextMergeStream}; + +enum State { + Para, + NumberList, + BulletList, + Heading, + BlockQuote, +} + +struct WidgetWriter<'i> { + input: &'i str, + state_stack: Vec, + heading_stack: Vec<&'static str>, + list_stack: Vec, + accumulator: String, + lines: Vec, + links: BTreeSet, +} + +impl<'i> WidgetWriter<'i> +{ + pub fn from_str(input: &'i str) -> Self { + Self { + input, + state_stack: Default::default(), + heading_stack: Default::default(), + list_stack: Default::default(), + accumulator: Default::default(), + lines: Default::default(), + links: Default::default(), + } + } + + pub fn parse(&mut self) { + let iter = TextMergeStream::new(Parser::new(self.input)); + for event in iter { + match event { + Event::Start(tag) => { + self.start_tag(&tag); + }, + Event::End(tag) => { + self.end_tag(tag); + }, + Event::Text(txt) + | Event::Code(txt) + | Event::InlineHtml(txt) + | Event::Html(txt) => { + let prefix = if let Some(State::BlockQuote) = self.state_stack.first() { + "| " + } else { + "" + }; + for ln in txt.lines() { + self.accumulator.push_str(prefix); + self.accumulator.push_str(ln); + } + }, + Event::Rule => { /* noop */ }, + Event::SoftBreak => { /* noop */ }, + Event::HardBreak => { /* noop */ }, + // We don't support these + Event::InlineMath(_) => todo!(), + Event::DisplayMath(_) => todo!(), + Event::FootnoteReference(_) => todo!(), + Event::TaskListMarker(_) => todo!(), + } + } + } + + fn start_tag(&mut self, tag: &Tag<'i>) { + match tag { + Tag::Paragraph => { + self.state_stack.push(State::Para); + }, + Tag::Heading { level, id: _id, classes: _classes, attrs: _attrs } => { + self.heading_stack.push(match level { + HeadingLevel::H1 => "1", + HeadingLevel::H2 => "2", + HeadingLevel::H3 => "3", + HeadingLevel::H4 => "4", + HeadingLevel::H5 => "5", + HeadingLevel::H6 => "6", + }); + self.state_stack.push(State::Heading); + let prefix = self.heading_stack.join("."); + self.accumulator.push_str(&prefix); + self.accumulator.push_str(" "); + }, + Tag::List(Some(first)) => { + self.list_stack.push(*first); + self.state_stack.push(State::NumberList); + }, + Tag::List(None) => { + self.state_stack.push(State::BulletList); + }, + Tag::Item => { + if let Some(State::BulletList) = self.state_stack.first() { + self.accumulator.push_str("- "); + } else if let Some(State::NumberList) = self.state_stack.first() { + let num = self.list_stack.pop().unwrap_or(1); + self.accumulator.push_str(&format!("{}. ", num)); + self.list_stack.push(num + 1); + } + panic!("No list type in our state stack"); + }, + Tag::Emphasis => { + self.accumulator.push_str("*"); + }, + Tag::Strong => { + self.accumulator.push_str("**"); + }, + Tag::Link { link_type, dest_url, title, id } => { + let dest = match link_type { + // [foo](bar) + LinkType::Inline => format!("({})", dest_url), + // [foo][bar] + LinkType::Reference => format!("[{}]", id), + // [foo] + LinkType::Shortcut => format!("[{}]", title), + // These are unsupported right now + LinkType::ReferenceUnknown => todo!(), + LinkType::Collapsed => todo!(), + LinkType::CollapsedUnknown => todo!(), + LinkType::ShortcutUnknown => todo!(), + LinkType::Autolink => todo!(), + LinkType::Email => todo!(), + LinkType::WikiLink { has_pothole: _ } => todo!(), + }; + self.accumulator.push_str(&format!("[{}]{}", title, dest)); + self.links.insert(dest); + }, + Tag::BlockQuote(_) => { + self.state_stack.push(State::BlockQuote); + }, + // these are all noops + Tag::CodeBlock(_) => {}, + Tag::HtmlBlock => {}, + Tag::FootnoteDefinition(_) => {}, + Tag::DefinitionList => {}, + Tag::DefinitionListTitle => {}, + Tag::DefinitionListDefinition => {}, + Tag::Table(_) => {}, + Tag::TableHead => {}, + Tag::TableRow => {}, + Tag::TableCell => {}, + Tag::Strikethrough => {}, + Tag::Superscript => {}, + Tag::Subscript => {} + Tag::Image { link_type: _link_type, dest_url: _dest_url, title: _title, id: _id } => {}, + Tag::MetadataBlock(_) => {}, + } + } + + fn end_tag(&mut self, tag: TagEnd) { + match tag { + TagEnd::Paragraph => { + self.state_stack.pop(); + self.lines.push("\n".to_owned()); + }, + TagEnd::Heading(_level) => { + self.heading_stack.pop(); + self.state_stack.pop(); + self.lines.extend(self.accumulator.lines().map(|s| s.to_owned())); + self.accumulator.clear(); + }, + TagEnd::List(_ordered) => { + self.state_stack.pop(); + }, + TagEnd::BlockQuote(_kind) => { + self.state_stack.pop(); + }, + TagEnd::CodeBlock => { + todo!() + }, + TagEnd::HtmlBlock => { + todo!() + }, + TagEnd::Item => { /* noop */ }, + TagEnd::Link => { /* noop */ }, + // We don't support these + TagEnd::FootnoteDefinition => todo!(), + TagEnd::DefinitionList => todo!(), + TagEnd::DefinitionListTitle => todo!(), + TagEnd::DefinitionListDefinition => todo!(), + TagEnd::Table => todo!(), + TagEnd::TableHead => todo!(), + TagEnd::TableRow => todo!(), + TagEnd::TableCell => todo!(), + TagEnd::Emphasis => { + self.accumulator.push_str("*"); + }, + TagEnd::Strong => { + self.accumulator.push_str("**"); + }, + TagEnd::Strikethrough => todo!(), + TagEnd::Superscript => todo!(), + TagEnd::Subscript => todo!(), + TagEnd::Image => todo!(), + TagEnd::MetadataBlock(_) => todo!(), + } + } +} diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index 8984c6e..447ce22 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -11,6 +11,7 @@ use super::*; pub mod viewport; pub use viewport::Viewport; pub mod dialog; +pub mod markdown; #[cfg(test)] mod test;