From caa7c0aacd5e117fcba8d970cfa77f36af0902c7 Mon Sep 17 00:00:00 2001 From: PoiScript Date: Mon, 29 Apr 2024 17:28:49 +0800 Subject: [PATCH] feat: markdown export --- examples/markdown.rs | 23 +++++ src/export/html.rs | 18 +++- src/export/markdown.rs | 186 +++++++++++++++++++++++++++++++++++++++++ src/export/mod.rs | 2 + 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 examples/markdown.rs create mode 100644 src/export/markdown.rs diff --git a/examples/markdown.rs b/examples/markdown.rs new file mode 100644 index 0000000..353785a --- /dev/null +++ b/examples/markdown.rs @@ -0,0 +1,23 @@ +//! ```bash +//! cargo run --example markdown test.org +//! ``` + +use orgize::{export::MarkdownExport, Org}; +use std::{env::args, fs}; + +fn main() { + let args: Vec<_> = args().collect(); + + if args.len() < 2 { + panic!("Usage: {} ", args[0]); + } + + let content = fs::read_to_string(&args[1]).unwrap(); + + let mut export = MarkdownExport::default(); + Org::parse(&content).traverse(&mut export); + + fs::write(format!("{}.md", &args[1]), export.finish()).unwrap(); + + println!("Wrote to {}.md", &args[1]); +} diff --git a/src/export/html.rs b/src/export/html.rs index bcd89cb..084b74c 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -6,7 +6,7 @@ use std::fmt::Write as _; use super::event::{Container, Event}; use super::TraversalContext; use super::Traverser; -use crate::SyntaxKind; +use crate::{SyntaxElement, SyntaxKind, SyntaxNode}; /// A wrapper for escaping sensitive characters in html. /// @@ -73,6 +73,22 @@ impl HtmlExport { pub fn finish(self) -> String { self.output } + + /// Render syntax node to html string + /// + /// ```rust + /// use orgize::{Org, ast::Bold, export::HtmlExport, rowan::ast::AstNode}; + /// + /// let org = Org::parse("* /hello/ *world*"); + /// let bold = org.first_node::().unwrap(); + /// let mut html = HtmlExport::default(); + /// html.render(bold.syntax()); + /// assert_eq!(html.finish(), "world"); + /// ``` + pub fn render(&mut self, node: &SyntaxNode) { + let mut ctx = TraversalContext::default(); + self.element(SyntaxElement::Node(node.clone()), &mut ctx); + } } impl Traverser for HtmlExport { diff --git a/src/export/markdown.rs b/src/export/markdown.rs new file mode 100644 index 0000000..b75b1ee --- /dev/null +++ b/src/export/markdown.rs @@ -0,0 +1,186 @@ +use std::cmp::min; +use std::fmt::Write as _; + +use crate::{SyntaxElement, SyntaxNode}; + +use super::event::{Container, Event}; +use super::TraversalContext; +use super::Traverser; + +#[derive(Default)] +pub struct MarkdownExport { + output: String, + + inside_blockquote: bool, +} + +impl MarkdownExport { + pub fn push_str(&mut self, s: impl AsRef) { + self.output += s.as_ref(); + } + + /// Render syntax node to markdown string + /// + /// ```rust + /// use orgize::{Org, ast::Bold, export::MarkdownExport, rowan::ast::AstNode}; + /// + /// let org = Org::parse("* /hello/ *world*"); + /// let bold = org.first_node::().unwrap(); + /// let mut markdown = MarkdownExport::default(); + /// markdown.render(bold.syntax()); + /// assert_eq!(markdown.finish(), "**world**"); + /// ``` + pub fn render(&mut self, node: &SyntaxNode) { + let mut ctx = TraversalContext::default(); + self.element(SyntaxElement::Node(node.clone()), &mut ctx); + } + + pub fn finish(self) -> String { + self.output + } + + fn follows_newline(&mut self) { + if !self.output.is_empty() && !self.output.ends_with(['\n', '\r']) { + self.output += "\n"; + } + } +} + +impl Traverser for MarkdownExport { + fn event(&mut self, event: Event, ctx: &mut TraversalContext) { + match event { + Event::Enter(Container::Document(_)) => {} + Event::Leave(Container::Document(_)) => {} + + Event::Enter(Container::Headline(headline)) => { + self.follows_newline(); + let level = min(headline.level(), 6); + let _ = write!(&mut self.output, "{} ", "#".repeat(level)); + for elem in headline.title() { + self.element(elem, ctx); + } + } + Event::Leave(Container::Headline(_)) => {} + + Event::Enter(Container::Paragraph(_)) => {} + Event::Leave(Container::Paragraph(_)) => self.output += "\n", + + Event::Enter(Container::Section(_)) => self.follows_newline(), + Event::Leave(Container::Section(_)) => {} + + Event::Enter(Container::Italic(_)) => self.output += "*", + Event::Leave(Container::Italic(_)) => self.output += "*", + + Event::Enter(Container::Bold(_)) => self.output += "**", + Event::Leave(Container::Bold(_)) => self.output += "**", + + Event::Enter(Container::Strike(_)) => self.output += "~~", + Event::Leave(Container::Strike(_)) => self.output += "~~", + + Event::Enter(Container::Underline(_)) => {} + Event::Leave(Container::Underline(_)) => {} + + Event::Enter(Container::Verbatim(_)) + | Event::Leave(Container::Verbatim(_)) + | Event::Enter(Container::Code(_)) + | Event::Leave(Container::Code(_)) => self.output += "`", + + Event::Enter(Container::SourceBlock(block)) => { + self.follows_newline(); + self.output += "```"; + if let Some(language) = block.language() { + self.output += &language; + } + } + Event::Leave(Container::SourceBlock(_)) => self.output += "```\n", + + Event::Enter(Container::QuoteBlock(_)) => { + self.inside_blockquote = true; + self.follows_newline(); + self.output += "> "; + } + Event::Leave(Container::QuoteBlock(_)) => self.inside_blockquote = false, + + Event::Enter(Container::CommentBlock(_)) => self.output += "", + + Event::Enter(Container::Comment(_)) => self.output += "", + + Event::Enter(Container::Subscript(_)) => self.output += "", + Event::Leave(Container::Subscript(_)) => self.output += "", + + Event::Enter(Container::Superscript(_)) => self.output += "", + Event::Leave(Container::Superscript(_)) => self.output += "", + + Event::Enter(Container::List(_list)) => {} + Event::Leave(Container::List(_list)) => {} + + Event::Enter(Container::ListItem(list_item)) => { + self.follows_newline(); + self.output += &" ".repeat(list_item.indent()); + self.output += &list_item.bullet(); + } + Event::Leave(Container::ListItem(_)) => {} + + Event::Enter(Container::OrgTable(_table)) => {} + Event::Leave(Container::OrgTable(_)) => {} + Event::Enter(Container::OrgTableRow(_row)) => {} + Event::Leave(Container::OrgTableRow(_row)) => {} + Event::Enter(Container::OrgTableCell(_)) => {} + Event::Leave(Container::OrgTableCell(_)) => {} + + Event::Enter(Container::Link(link)) => { + let path = link.path(); + let path = path.trim_start_matches("file:"); + + if link.is_image() { + let _ = write!(&mut self.output, "![]({path})"); + return ctx.skip(); + } + + if !link.has_description() { + let _ = write!(&mut self.output, r#"[{}]({})"#, &path, &path); + return ctx.skip(); + } + + self.output += "["; + } + Event::Leave(Container::Link(link)) => { + let _ = write!(&mut self.output, r#"]({})"#, &*link.path()); + } + + Event::Text(text) => { + if self.inside_blockquote { + for (idx, line) in text.split('\n').enumerate() { + if idx != 0 { + self.output += "\n> "; + } + self.output += line; + } + } else { + self.output += &*text; + } + } + + Event::LineBreak(_) => {} + + Event::Snippet(_snippet) => {} + + Event::Rule(_) => self.output += "\n-----\n", + + Event::Timestamp(_timestamp) => {} + + Event::LatexFragment(latex) => { + let _ = write!(&mut self.output, "{}", &latex.syntax); + } + Event::LatexEnvironment(latex) => { + let _ = write!(&mut self.output, "{}", &latex.syntax); + } + + Event::Entity(entity) => self.output += entity.utf8(), + + _ => {} + } + } +} diff --git a/src/export/mod.rs b/src/export/mod.rs index 74837cc..afada80 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -2,8 +2,10 @@ mod event; mod html; +mod markdown; mod traverse; pub use event::{Container, Event}; pub use html::{HtmlEscape, HtmlExport}; +pub use markdown::MarkdownExport; pub use traverse::{from_fn, from_fn_with_ctx, FromFn, FromFnWithCtx, TraversalContext, Traverser};