Add table support
parent
0ac9cb168f
commit
fc2d95137f
|
@ -12,5 +12,6 @@ keywords = ["gemini", "markdown"]
|
||||||
maintenance = { status = "experimental" }
|
maintenance = { status = "experimental" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
comfy-table = { version = "7.1.1", default-features = false }
|
||||||
gemtext = "0.2.1"
|
gemtext = "0.2.1"
|
||||||
pulldown-cmark = { version = "0.8", default-features = false }
|
pulldown-cmark = { version = "0.8", default-features = false }
|
||||||
|
|
149
src/lib.rs
149
src/lib.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use comfy_table::Table;
|
||||||
use pulldown_cmark as md;
|
use pulldown_cmark as md;
|
||||||
|
|
||||||
/// Converts a given string of Markdown to semi-equivalent gemtext.
|
/// Converts a given string of Markdown to semi-equivalent gemtext.
|
||||||
|
@ -7,7 +8,9 @@ use pulldown_cmark as md;
|
||||||
/// Will panic if gemtext::render somehow produces invalid UTF-8.
|
/// Will panic if gemtext::render somehow produces invalid UTF-8.
|
||||||
/// Since gemtext::render only produces valid UTF-8, this should never happen.
|
/// Since gemtext::render only produces valid UTF-8, this should never happen.
|
||||||
pub fn convert(markdown_text: &str) -> String {
|
pub fn convert(markdown_text: &str) -> String {
|
||||||
let parser = md::Parser::new_ext(markdown_text, md::Options::empty());
|
let mut opts = md::Options::empty();
|
||||||
|
opts.toggle(md::Options::ENABLE_TABLES);
|
||||||
|
let parser = md::Parser::new_ext(markdown_text, opts);
|
||||||
let mut state = State::new();
|
let mut state = State::new();
|
||||||
|
|
||||||
for event in parser {
|
for event in parser {
|
||||||
|
@ -18,11 +21,13 @@ pub fn convert(markdown_text: &str) -> String {
|
||||||
md::Event::Start(md::Tag::CodeBlock(_)) => state.start_code_block(),
|
md::Event::Start(md::Tag::CodeBlock(_)) => state.start_code_block(),
|
||||||
md::Event::Start(md::Tag::List(_)) => (),
|
md::Event::Start(md::Tag::List(_)) => (),
|
||||||
md::Event::Start(md::Tag::Item) => state.start_list_item(),
|
md::Event::Start(md::Tag::Item) => state.start_list_item(),
|
||||||
md::Event::Start(md::Tag::FootnoteDefinition(_)) => unimplemented!("footnotes disabled"),
|
md::Event::Start(md::Tag::FootnoteDefinition(_)) => {
|
||||||
md::Event::Start(md::Tag::Table(_)) => unimplemented!("tables disabled"),
|
unimplemented!("footnotes disabled")
|
||||||
md::Event::Start(md::Tag::TableHead) => unimplemented!("tables disabled"),
|
}
|
||||||
md::Event::Start(md::Tag::TableRow) => unimplemented!("tables disabled"),
|
md::Event::Start(md::Tag::Table(_)) => state.start_table_building(),
|
||||||
md::Event::Start(md::Tag::TableCell) => unimplemented!("tables disabled"),
|
md::Event::Start(md::Tag::TableHead) => state.add_table_row(),
|
||||||
|
md::Event::Start(md::Tag::TableRow) => state.add_table_row(),
|
||||||
|
md::Event::Start(md::Tag::TableCell) => (),
|
||||||
md::Event::Start(md::Tag::Emphasis) => state.toggle_emphasis(),
|
md::Event::Start(md::Tag::Emphasis) => state.toggle_emphasis(),
|
||||||
md::Event::Start(md::Tag::Strong) => state.toggle_strong(),
|
md::Event::Start(md::Tag::Strong) => state.toggle_strong(),
|
||||||
md::Event::Start(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
|
md::Event::Start(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
|
||||||
|
@ -36,10 +41,10 @@ pub fn convert(markdown_text: &str) -> String {
|
||||||
md::Event::End(md::Tag::List(_)) => state.finish_list(),
|
md::Event::End(md::Tag::List(_)) => state.finish_list(),
|
||||||
md::Event::End(md::Tag::Item) => state.finish_node(),
|
md::Event::End(md::Tag::Item) => state.finish_node(),
|
||||||
md::Event::End(md::Tag::FootnoteDefinition(_)) => unimplemented!("footnotes disabled"),
|
md::Event::End(md::Tag::FootnoteDefinition(_)) => unimplemented!("footnotes disabled"),
|
||||||
md::Event::End(md::Tag::Table(_)) => unimplemented!("tables disabled"),
|
md::Event::End(md::Tag::Table(_)) => state.finish_table_building(),
|
||||||
md::Event::End(md::Tag::TableHead) => unimplemented!("tables disabled"),
|
md::Event::End(md::Tag::TableHead) => (),
|
||||||
md::Event::End(md::Tag::TableRow) => unimplemented!("tables disabled"),
|
md::Event::End(md::Tag::TableRow) => (),
|
||||||
md::Event::End(md::Tag::TableCell) => unimplemented!("tables disabled"),
|
md::Event::End(md::Tag::TableCell) => (),
|
||||||
md::Event::End(md::Tag::Emphasis) => state.toggle_emphasis(),
|
md::Event::End(md::Tag::Emphasis) => state.toggle_emphasis(),
|
||||||
md::Event::End(md::Tag::Strong) => state.toggle_strong(),
|
md::Event::End(md::Tag::Strong) => state.toggle_strong(),
|
||||||
md::Event::End(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
|
md::Event::End(md::Tag::Strikethrough) => unimplemented!("strikethrough disabled"),
|
||||||
|
@ -57,7 +62,8 @@ pub fn convert(markdown_text: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodes = state.nodes
|
let nodes = state
|
||||||
|
.nodes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|cluster| !cluster.is_empty())
|
.filter(|cluster| !cluster.is_empty())
|
||||||
.map(condense)
|
.map(condense)
|
||||||
|
@ -72,7 +78,9 @@ type NodeCluster = Vec<gemtext::Node>;
|
||||||
|
|
||||||
fn condense(original: NodeCluster) -> NodeCluster {
|
fn condense(original: NodeCluster) -> NodeCluster {
|
||||||
match original.as_slice() {
|
match original.as_slice() {
|
||||||
[gemtext::Node::Text(text), gemtext::Node::Link { name: Some(name), .. }] if text == name => vec![original[1].clone()],
|
[gemtext::Node::Text(text), gemtext::Node::Link {
|
||||||
|
name: Some(name), ..
|
||||||
|
}] if text == name => vec![original[1].clone()],
|
||||||
_ => original,
|
_ => original,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +116,8 @@ struct State {
|
||||||
pending_node_type: NodeType,
|
pending_node_type: NodeType,
|
||||||
pending_other: Vec<gemtext::Node>,
|
pending_other: Vec<gemtext::Node>,
|
||||||
link_text_stack: Vec<String>,
|
link_text_stack: Vec<String>,
|
||||||
|
table: Vec<Vec<String>>,
|
||||||
|
building_table: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
@ -118,6 +128,8 @@ impl State {
|
||||||
pending_node_type: NodeType::Text,
|
pending_node_type: NodeType::Text,
|
||||||
pending_other: vec![],
|
pending_other: vec![],
|
||||||
link_text_stack: vec![],
|
link_text_stack: vec![],
|
||||||
|
table: vec![],
|
||||||
|
building_table: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +142,14 @@ impl State {
|
||||||
self.pending_node_type = NodeType::Heading { level };
|
self.pending_node_type = NodeType::Heading { level };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_table_building(&mut self) {
|
||||||
|
self.building_table = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_table_row(&mut self) {
|
||||||
|
self.table.push(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
fn start_block_quote(&mut self) {
|
fn start_block_quote(&mut self) {
|
||||||
self.pending_node_type = NodeType::Quote;
|
self.pending_node_type = NodeType::Quote;
|
||||||
}
|
}
|
||||||
|
@ -159,25 +179,57 @@ impl State {
|
||||||
self.pending_node_content += "[image: ";
|
self.pending_node_content += "[image: ";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn finish_table_building(&mut self) {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
if let Some(header) = self.table.first() {
|
||||||
|
table.set_header(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.add_rows(self.table[1..].into_iter());
|
||||||
|
|
||||||
|
self.building_table = false;
|
||||||
|
self.table = vec![];
|
||||||
|
|
||||||
|
self.finish_node();
|
||||||
|
self.pending_node_content += &table.to_string();
|
||||||
|
self.finish_node();
|
||||||
|
}
|
||||||
|
|
||||||
fn finish_list(&mut self) {
|
fn finish_list(&mut self) {
|
||||||
self.nodes.push(vec![]);
|
self.nodes.push(vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_link(&mut self, href: &str) {
|
fn end_link(&mut self, href: &str) {
|
||||||
let text = self.link_text_stack.pop().unwrap_or_else(|| href.to_string());
|
let text = self
|
||||||
self.pending_other.push(gemtext::Node::Link { to: href.to_string(), name: Some(text) });
|
.link_text_stack
|
||||||
|
.pop()
|
||||||
|
.unwrap_or_else(|| href.to_string());
|
||||||
|
self.pending_other.push(gemtext::Node::Link {
|
||||||
|
to: href.to_string(),
|
||||||
|
name: Some(text),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_image(&mut self, src: &str) {
|
fn end_image(&mut self, src: &str) {
|
||||||
let text = self.link_text_stack.pop().unwrap_or_else(|| src.to_string());
|
let text = self
|
||||||
|
.link_text_stack
|
||||||
|
.pop()
|
||||||
|
.unwrap_or_else(|| src.to_string());
|
||||||
let text = format!("[image: {}]", text);
|
let text = format!("[image: {}]", text);
|
||||||
self.pending_other.push(gemtext::Node::Link { to: src.to_string(), name: Some(text) });
|
self.pending_other.push(gemtext::Node::Link {
|
||||||
|
to: src.to_string(),
|
||||||
|
name: Some(text),
|
||||||
|
});
|
||||||
self.pending_node_content += "]";
|
self.pending_node_content += "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// will create an empty paragraph if pending_text is empty
|
// will create an empty paragraph if pending_text is empty
|
||||||
fn finish_node(&mut self) {
|
fn finish_node(&mut self) {
|
||||||
match (&self.pending_node_type, self.nodes.last().and_then(|cluster| cluster.last())) {
|
match (
|
||||||
|
&self.pending_node_type,
|
||||||
|
self.nodes.last().and_then(|cluster| cluster.last()),
|
||||||
|
) {
|
||||||
(NodeType::ListItem, Some(gemtext::Node::ListItem(_))) => (),
|
(NodeType::ListItem, Some(gemtext::Node::ListItem(_))) => (),
|
||||||
_ => self.nodes.push(vec![]),
|
_ => self.nodes.push(vec![]),
|
||||||
}
|
}
|
||||||
|
@ -191,11 +243,17 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_text(&mut self, text: &str) {
|
fn add_text(&mut self, text: &str) {
|
||||||
|
if self.building_table {
|
||||||
|
if let Some(last_row) = self.table.last_mut() {
|
||||||
|
last_row.push(text.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for link_text in &mut self.link_text_stack {
|
for link_text in &mut self.link_text_stack {
|
||||||
*link_text += text;
|
*link_text += text;
|
||||||
}
|
}
|
||||||
self.pending_node_content += text;
|
self.pending_node_content += text;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn add_inline_code(&mut self, code: &str) {
|
fn add_inline_code(&mut self, code: &str) {
|
||||||
self.pending_node_content += "`";
|
self.pending_node_content += "`";
|
||||||
|
@ -284,3 +342,60 @@ fn test_readme() {
|
||||||
let gemtext = include_str!("../README.gmi");
|
let gemtext = include_str!("../README.gmi");
|
||||||
assert_eq!(convert(markdown), gemtext);
|
assert_eq!(convert(markdown), gemtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn test_single_table() {
|
||||||
|
let markdown = r#"
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
| -------- | ----- |
|
||||||
|
| ten | 10 |
|
||||||
|
| veinte | 20 |
|
||||||
|
"#;
|
||||||
|
let gemtext = r#"
|
||||||
|
+----------+-------+
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
+==================+
|
||||||
|
| ten | 10 |
|
||||||
|
|----------+-------|
|
||||||
|
| veinte | 20 |
|
||||||
|
+----------+-------+
|
||||||
|
"#;
|
||||||
|
assert_eq!(convert(markdown).trim(), gemtext.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn test_multi_tables() {
|
||||||
|
let markdown = r#"
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
| -------- | ----- |
|
||||||
|
| ten | 10 |
|
||||||
|
| veinte | 20 |
|
||||||
|
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
| -------- | ----- |
|
||||||
|
| 30 | 40 |
|
||||||
|
| 50 | 60 |
|
||||||
|
"#;
|
||||||
|
let gemtext = r#"
|
||||||
|
+----------+-------+
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
+==================+
|
||||||
|
| ten | 10 |
|
||||||
|
|----------+-------|
|
||||||
|
| veinte | 20 |
|
||||||
|
+----------+-------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
+----------+-------+
|
||||||
|
| Column 1 | Col 2 |
|
||||||
|
+==================+
|
||||||
|
| 30 | 40 |
|
||||||
|
|----------+-------|
|
||||||
|
| 50 | 60 |
|
||||||
|
+----------+-------+
|
||||||
|
"#;
|
||||||
|
assert_eq!(convert(markdown).trim(), gemtext.trim());
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue