feat: markdown export
This commit is contained in:
parent
545db900cd
commit
caa7c0aacd
4 changed files with 228 additions and 1 deletions
|
|
@ -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
186
src/export/markdown.rs
Normal 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, "");
|
||||
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,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};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue