From db7fb70724819843d26c15efbc217824bc7bf383 Mon Sep 17 00:00:00 2001 From: PoiScript Date: Wed, 15 Nov 2023 12:55:35 +0800 Subject: [PATCH] feat: handle in html export --- examples/html-slugify.rs | 7 +- src/ast/table.rs | 53 +++++++++++- src/export/forward.rs | 4 +- src/export/html.rs | 78 ++++++++++++++++-- tests/html.rs | 172 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 14 deletions(-) create mode 100644 tests/html.rs diff --git a/examples/html-slugify.rs b/examples/html-slugify.rs index cbad5c5..6f62d4d 100644 --- a/examples/html-slugify.rs +++ b/examples/html-slugify.rs @@ -30,12 +30,15 @@ impl Traverser for MyHtmlHandler { let level = title.headline().and_then(|h| h.level()).unwrap_or(1); let level = min(level, 6); let raw = title.syntax().to_string(); - self.0.output += &format!("", slugify!(&raw)); + self.0.push_str(format!( + "", + slugify!(&raw) + )); } WalkEvent::Leave(title) => { let level = title.headline().and_then(|h| h.level()).unwrap_or(1); let level = min(level, 6); - self.0.output += &format!(""); + self.0.push_str(format!("")); } } } diff --git a/src/ast/table.rs b/src/ast/table.rs index 2d1ebc4..bafc122 100644 --- a/src/ast/table.rs +++ b/src/ast/table.rs @@ -1,6 +1,57 @@ -use super::OrgTableRow; +use rowan::ast::AstNode; + +use super::{OrgTable, OrgTableRow}; use crate::syntax::SyntaxKind; +impl OrgTable { + /// Returns `true` if this table has a header + /// + /// A table has a header when it contains at least two row groups. + /// + /// ```rust + /// use orgize::{Org, ast::OrgTable}; + /// + /// let org = Org::parse(r#" + /// | a | b | + /// |---+---| + /// | c | d |"#); + /// let table = org.first_node::().unwrap(); + /// assert!(table.has_header()); + /// + /// let org = Org::parse(r#" + /// | a | b | + /// | 0 | 1 | + /// |---+---| + /// | a | w |"#); + /// let table = org.first_node::().unwrap(); + /// assert!(table.has_header()); + /// + /// let org = Org::parse(r#" + /// | a | b | + /// | c | d |"#); + /// let table = org.first_node::().unwrap(); + /// assert!(!table.has_header()); + /// + /// let org = Org::parse(r#" + /// |---+---| + /// | a | b | + /// | c | d | + /// |---+---|"#); + /// let table = org.first_node::().unwrap(); + /// assert!(!table.has_header()); + /// ``` + pub fn has_header(&self) -> bool { + self.syntax + .children() + .filter_map(OrgTableRow::cast) + .skip_while(|row| row.is_rule()) + .skip_while(|row| row.is_standard()) + .skip_while(|row| row.is_rule()) + .next() + .is_some() + } +} + impl OrgTableRow { /// Returns `true` if this row is a rule /// diff --git a/src/export/forward.rs b/src/export/forward.rs index e98e30e..602061f 100644 --- a/src/export/forward.rs +++ b/src/export/forward.rs @@ -31,12 +31,12 @@ /// let level = title.headline().and_then(|h| h.level()).unwrap_or(1); /// let level = min(level, 6); /// let raw = title.syntax().to_string(); -/// self.0.output += &format!("", slugify!(&raw)); +/// self.0.push_str(format!("", slugify!(&raw))); /// } /// WalkEvent::Leave(title) => { /// let level = title.headline().and_then(|h| h.level()).unwrap_or(1); /// let level = min(level, 6); -/// self.0.output += &format!(""); +/// self.0.push_str(format!("")); /// } /// } /// } diff --git a/src/export/html.rs b/src/export/html.rs index 0c5786b..0ee41df 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -47,11 +47,27 @@ impl> fmt::Display for HtmlEscape { #[derive(Default)] pub struct HtmlExport { - pub output: String, + output: String, + in_descriptive_list: Vec, + + table_row: TableRow, +} + +#[derive(Default, PartialEq, Eq)] +enum TableRow { + #[default] + HeaderRule, + Header, + BodyRule, + Body, } impl HtmlExport { + pub fn push_str(&mut self, s: impl AsRef) { + self.output += s.as_ref(); + } + pub fn finish(self) -> String { self.output } @@ -306,10 +322,28 @@ impl Traverser for HtmlExport { #[tracing::instrument(skip(self, _ctx))] fn org_table(&mut self, event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext) { - self.output += match event { - WalkEvent::Enter(_) => "", - WalkEvent::Leave(_) => "
", - }; + match event { + WalkEvent::Enter(table) => { + self.output += ""; + self.table_row = if table.has_header() { + TableRow::HeaderRule + } else { + TableRow::BodyRule + } + } + WalkEvent::Leave(_) => { + match self.table_row { + TableRow::Body => { + self.output += ""; + } + TableRow::Header => { + self.output += ""; + } + _ => {} + } + self.output += "
"; + } + } } #[tracing::instrument(skip(self, ctx))] @@ -317,13 +351,39 @@ impl Traverser for HtmlExport { if match event { WalkEvent::Enter(n) | WalkEvent::Leave(n) => n.is_rule(), } { + match self.table_row { + TableRow::Body => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + TableRow::Header => { + self.output += ""; + self.table_row = TableRow::BodyRule; + } + _ => {} + } return ctx.skip(); } - self.output += match event { - WalkEvent::Enter(_) => "", - WalkEvent::Leave(_) => "", - }; + match event { + WalkEvent::Enter(_) => { + match self.table_row { + TableRow::HeaderRule => { + self.table_row = TableRow::Header; + self.output += ""; + } + TableRow::BodyRule => { + self.table_row = TableRow::Body; + self.output += ""; + } + _ => {} + } + self.output += ""; + } + WalkEvent::Leave(_) => { + self.output += ""; + } + } } #[tracing::instrument(skip(self, _ctx))] diff --git a/tests/html.rs b/tests/html.rs new file mode 100644 index 0000000..f54808d --- /dev/null +++ b/tests/html.rs @@ -0,0 +1,172 @@ +use orgize::Org; + +#[test] +fn emphasis() { + insta::assert_snapshot!( + Org::parse("*bold*, /italic/,\n_underlined_, =verbatim= and ~code~").to_html(), + @r###" +

bold, italic, + underlined, verbatim and code

+ "### + ); +} + +#[test] +fn link() { + insta::assert_snapshot!( + Org::parse("Visit[[http://example.com][link1]]or[[http://example.com][link1]].").to_html(), + @r###"

Visitlink1orlink1.

"### + ); +} + +#[test] +fn section_and_headline() { + insta::assert_snapshot!( + Org::parse(r#" +* title 1 +section 1 +** title 2 +section 2 +* title 3 +section 3 +* title 4 +section 4 +"#).to_html(), + @r###" +

title 1

section 1 +

title 2

section 2 +

title 3

section 3 +

title 4

section 4 +

+ "### + ); +} + +#[test] +fn list() { + insta::assert_snapshot!( + Org::parse(r#" ++ 1 + ++ 2 + + - 3 + + - 4 + ++ 5 +"#).to_html(), + @r###" +
  • 1 + +

  • 2 + +

    • 3 + +

    • 4 + +

  • 5 +

+ "### + ); +} + +#[test] +fn snippet() { + insta::assert_snapshot!( + Org::parse("@@html:@@delete this@@html:@@").to_html(), + @"

delete this

" + ); +} + +#[test] +fn paragraphs() { + insta::assert_snapshot!( + Org::parse(r#" +* title + +paragraph 1 + +paragraph 2 + +paragraph 3 + +paragraph 4 +"#).to_html(), + @r###" +

title

paragraph 1 +

paragraph 2 +

paragraph 3 +

paragraph 4 +

+ "### + ); +} + +#[test] +fn table() { + // don't has table header + insta::assert_snapshot!( + Org::parse(r#" +|-----+-----+-----| +| 0 | 1 | 2 | +| 4 | 5 | 6 | +|-----+-----+-----| +"#).to_html(), + @"
012
456
" + ); + + // has table header + insta::assert_snapshot!( + Org::parse(r#" +| 0 | 1 | 2 | +|-----+-----+-----| +| 4 | 5 | 6 | +|-----+-----+-----| +"#).to_html(), + @"
012
456
" + ); + + // has two table body + insta::assert_snapshot!( + Org::parse(r#" +| 0 | 1 | 2 | +|-----+-----+-----| +| 4 | 5 | 6 | +|-----+-----+-----| +| 7 | 8 | 9 | +"#).to_html(), + @"
012
456
789
" + ); + + // multiple row rule + insta::assert_snapshot!( + Org::parse(r#" +| 0 | 1 | 2 | +|-----+-----+-----| +|-----+-----+-----| +| 4 | 5 | 6 | +"#).to_html(), + @"
012
456
" + ); + + // empty + insta::assert_snapshot!( + Org::parse(r#" +|-----+-----+-----| +|-----+-----+-----| +"#).to_html(), + @"
" + ); + + insta::assert_snapshot!( + Org::parse(r#" +| +|- +| +|- +| +"#).to_html(), + @"
" + ); +}