feat: handle <thead> in html export

This commit is contained in:
PoiScript 2023-11-15 12:55:35 +08:00
parent 1362624083
commit db7fb70724
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
5 changed files with 300 additions and 14 deletions

View file

@ -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!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
self.0.push_str(format!(
"<h{level}><a id=\"{0}\" href=\"#{0}\">",
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!("</a></h{level}>");
self.0.push_str(format!("</a></h{level}>"));
}
}
}

View file

@ -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::<OrgTable>().unwrap();
/// assert!(table.has_header());
///
/// let org = Org::parse(r#"
/// | a | b |
/// | 0 | 1 |
/// |---+---|
/// | a | w |"#);
/// let table = org.first_node::<OrgTable>().unwrap();
/// assert!(table.has_header());
///
/// let org = Org::parse(r#"
/// | a | b |
/// | c | d |"#);
/// let table = org.first_node::<OrgTable>().unwrap();
/// assert!(!table.has_header());
///
/// let org = Org::parse(r#"
/// |---+---|
/// | a | b |
/// | c | d |
/// |---+---|"#);
/// let table = org.first_node::<OrgTable>().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
///

View file

@ -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!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
/// self.0.push_str(format!("<h{level}><a id=\"{0}\" href=\"#{0}\">", 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!("</a></h{level}>");
/// self.0.push_str(format!("</a></h{level}>"));
/// }
/// }
/// }

View file

@ -47,11 +47,27 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
#[derive(Default)]
pub struct HtmlExport {
pub output: String,
output: String,
in_descriptive_list: Vec<bool>,
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<str>) {
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(_) => "<table><tbody>",
WalkEvent::Leave(_) => "</tbody></table>",
};
match event {
WalkEvent::Enter(table) => {
self.output += "<table>";
self.table_row = if table.has_header() {
TableRow::HeaderRule
} else {
TableRow::BodyRule
}
}
WalkEvent::Leave(_) => {
match self.table_row {
TableRow::Body => {
self.output += "</tbody>";
}
TableRow::Header => {
self.output += "</thead>";
}
_ => {}
}
self.output += "</table>";
}
}
}
#[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 += "</tbody>";
self.table_row = TableRow::BodyRule;
}
TableRow::Header => {
self.output += "</thead>";
self.table_row = TableRow::BodyRule;
}
_ => {}
}
return ctx.skip();
}
self.output += match event {
WalkEvent::Enter(_) => "<tr>",
WalkEvent::Leave(_) => "</tr>",
};
match event {
WalkEvent::Enter(_) => {
match self.table_row {
TableRow::HeaderRule => {
self.table_row = TableRow::Header;
self.output += "<thead>";
}
TableRow::BodyRule => {
self.table_row = TableRow::Body;
self.output += "<tbody>";
}
_ => {}
}
self.output += "<tr>";
}
WalkEvent::Leave(_) => {
self.output += "</tr>";
}
}
}
#[tracing::instrument(skip(self, _ctx))]

172
tests/html.rs Normal file
View file

@ -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###"
<main><section><p><b>bold</b>, <i>italic</i>,
<u>underlined</u>, <code>verbatim</code> and <code>code</code></p></section></main>
"###
);
}
#[test]
fn link() {
insta::assert_snapshot!(
Org::parse("Visit[[http://example.com][link1]]or[[http://example.com][link1]].").to_html(),
@r###"<main><section><p>Visit<a href="http://example.com">link1</a>or<a href="http://example.com">link1</a>.</p></section></main>"###
);
}
#[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###"
<main><h1>title 1</h1><section><p>section 1
</p></section><h2>title 2</h2><section><p>section 2
</p></section><h1>title 3</h1><section><p>section 3
</p></section><h1>title 4</h1><section><p>section 4
</p></section></main>
"###
);
}
#[test]
fn list() {
insta::assert_snapshot!(
Org::parse(r#"
+ 1
+ 2
- 3
- 4
+ 5
"#).to_html(),
@r###"
<main><section><ul><li><p>1
</p></li><li><p>2
</p><ul><li><p>3
</p></li><li><p>4
</p></li></ul></li><li><p>5
</p></li></ul></section></main>
"###
);
}
#[test]
fn snippet() {
insta::assert_snapshot!(
Org::parse("@@html:<del>@@delete this@@html:</del>@@").to_html(),
@"<main><section><p><del>delete this</del></p></section></main>"
);
}
#[test]
fn paragraphs() {
insta::assert_snapshot!(
Org::parse(r#"
* title
paragraph 1
paragraph 2
paragraph 3
paragraph 4
"#).to_html(),
@r###"
<main><h1>title</h1><section><p></p><p>paragraph 1
</p><p>paragraph 2
</p><p>paragraph 3
</p><p>paragraph 4
</p></section></main>
"###
);
}
#[test]
fn table() {
// don't has table header
insta::assert_snapshot!(
Org::parse(r#"
|-----+-----+-----|
| 0 | 1 | 2 |
| 4 | 5 | 6 |
|-----+-----+-----|
"#).to_html(),
@"<main><section><table><tbody><tr><td>0</td><td>1</td><td>2</td></tr><tr><td>4</td><td>5</td><td>6</td></tr></tbody></table></section></main>"
);
// has table header
insta::assert_snapshot!(
Org::parse(r#"
| 0 | 1 | 2 |
|-----+-----+-----|
| 4 | 5 | 6 |
|-----+-----+-----|
"#).to_html(),
@"<main><section><table><thead><tr><td>0</td><td>1</td><td>2</td></tr></thead><tbody><tr><td>4</td><td>5</td><td>6</td></tr></tbody></table></section></main>"
);
// has two table body
insta::assert_snapshot!(
Org::parse(r#"
| 0 | 1 | 2 |
|-----+-----+-----|
| 4 | 5 | 6 |
|-----+-----+-----|
| 7 | 8 | 9 |
"#).to_html(),
@"<main><section><table><thead><tr><td>0</td><td>1</td><td>2</td></tr></thead><tbody><tr><td>4</td><td>5</td><td>6</td></tr></tbody><tbody><tr><td>7</td><td>8</td><td>9</td></tr></tbody></table></section></main>"
);
// multiple row rule
insta::assert_snapshot!(
Org::parse(r#"
| 0 | 1 | 2 |
|-----+-----+-----|
|-----+-----+-----|
| 4 | 5 | 6 |
"#).to_html(),
@"<main><section><table><thead><tr><td>0</td><td>1</td><td>2</td></tr></thead><tbody><tr><td>4</td><td>5</td><td>6</td></tr></tbody></table></section></main>"
);
// empty
insta::assert_snapshot!(
Org::parse(r#"
|-----+-----+-----|
|-----+-----+-----|
"#).to_html(),
@"<main><section><table></table></section></main>"
);
insta::assert_snapshot!(
Org::parse(r#"
|
|-
|
|-
|
"#).to_html(),
@"<main><section><table><thead><tr></tr></thead><tbody><tr></tr></tbody><tbody><tr></tr></tbody></table></section></main>"
);
}