feat: markdown export

This commit is contained in:
PoiScript 2024-04-29 17:28:49 +08:00
parent 545db900cd
commit caa7c0aacd
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
4 changed files with 228 additions and 1 deletions

23
examples/markdown.rs Normal file
View file

@ -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]);
}

View file

@ -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::<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 {

186
src/export/markdown.rs Normal file
View file

@ -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(),
_ => {}
}
}
}

View file

@ -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};