Skip to content

Commit

Permalink
feat: markdown export
Browse files Browse the repository at this point in the history
  • Loading branch information
PoiScript committed Apr 29, 2024
1 parent 545db90 commit caa7c0a
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 1 deletion.
23 changes: 23 additions & 0 deletions examples/markdown.rs
Original file line number Diff line number Diff line change
@@ -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: {} <org-mode-file>", 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]);
}
18 changes: 17 additions & 1 deletion src/export/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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::<Bold>().unwrap();
/// let mut html = HtmlExport::default();
/// html.render(bold.syntax());
/// assert_eq!(html.finish(), "<b>world</b>");
/// ```
pub fn render(&mut self, node: &SyntaxNode) {
let mut ctx = TraversalContext::default();
self.element(SyntaxElement::Node(node.clone()), &mut ctx);
}
}

impl Traverser for HtmlExport {
Expand Down
186 changes: 186 additions & 0 deletions src/export/markdown.rs
Original file line number Diff line number Diff line change
@@ -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<str>) {
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::<Bold>().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::Leave(Container::CommentBlock(_)) => self.output += "-->",

Event::Enter(Container::Comment(_)) => self.output += "<!--",
Event::Leave(Container::Comment(_)) => self.output += "-->",

Event::Enter(Container::Subscript(_)) => self.output += "<sub>",
Event::Leave(Container::Subscript(_)) => self.output += "</sub>",

Event::Enter(Container::Superscript(_)) => self.output += "<sup>",
Event::Leave(Container::Superscript(_)) => self.output += "</sup>",

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(),

_ => {}
}
}
}
2 changes: 2 additions & 0 deletions src/export/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit caa7c0a

Please sign in to comment.