Initial Commit
This commit is contained in:
commit
e9ff1c9e87
6 changed files with 336 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
95
Cargo.lock
generated
Normal file
95
Cargo.lock
generated
Normal file
|
@ -0,0 +1,95 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "gemtext-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "gemtext-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.197", optional = true }
|
||||
textwrap = "0.16.1"
|
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
#![warn(clippy::all, clippy::pedantic)]
|
||||
|
||||
mod node;
|
||||
mod parser;
|
||||
|
||||
pub use {node::Node, parser::Parser};
|
103
src/node.rs
Normal file
103
src/node.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use std::fmt::{self, Write};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Node {
|
||||
Text(String),
|
||||
H1(String),
|
||||
H2(String),
|
||||
H3(String),
|
||||
ListItem(String),
|
||||
Quote(String),
|
||||
PreStart {
|
||||
alt: Option<String>,
|
||||
},
|
||||
PreBody(String),
|
||||
PreEnd,
|
||||
Link {
|
||||
url: String,
|
||||
display: Option<String>,
|
||||
},
|
||||
Prompt {
|
||||
url: String,
|
||||
display: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Text(s) | Self::PreBody(s) => writeln!(f, "{s}"),
|
||||
Self::H1(s) => writeln!(f, "# {s}"),
|
||||
Self::H2(s) => writeln!(f, "## {s}"),
|
||||
Self::H3(s) => writeln!(f, "### {s}"),
|
||||
Self::ListItem(s) => writeln!(f, "* {s}"),
|
||||
Self::Quote(s) => writeln!(f, "> {s}"),
|
||||
Self::PreStart { alt } => match alt {
|
||||
Some(s) => writeln!(f, "```{s}"),
|
||||
None => writeln!(f, "```"),
|
||||
},
|
||||
Self::PreEnd => writeln!(f, "```"),
|
||||
Self::Link {
|
||||
url,
|
||||
display: Some(display),
|
||||
} => writeln!(f, "=> {url} {display}"),
|
||||
Self::Link { url, display: None } => writeln!(f, "=> {url}"),
|
||||
Self::Prompt {
|
||||
url,
|
||||
display: Some(display),
|
||||
} => writeln!(f, "=: {url} {display}"),
|
||||
Self::Prompt { url, display: None } => writeln!(f, "=: {url}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn write_html<W: Write>(&self, mut writer: W) -> fmt::Result {
|
||||
match self {
|
||||
Self::Text(s) => {
|
||||
writeln!(writer, "<p>")?;
|
||||
textwrap::wrap(s, 80)
|
||||
.into_iter()
|
||||
.try_for_each(|line| writeln!(writer, "{line}"))?;
|
||||
writeln!(writer, "</p>")
|
||||
}
|
||||
Self::H1(s) => writeln!(writer, "<h1>{s}</h1>"),
|
||||
Self::H2(s) => writeln!(writer, "<h2>{s}</h3>"),
|
||||
Self::H3(s) => writeln!(writer, "<h3>{s}</h3>"),
|
||||
Self::ListItem(s) => {
|
||||
write!(writer, "<li>")?;
|
||||
textwrap::wrap(s, 80)
|
||||
.iter()
|
||||
.try_for_each(|line| write!(writer, "{line}"))?;
|
||||
writeln!(writer, "</li>")
|
||||
}
|
||||
Self::Quote(s) => {
|
||||
write!(writer, "<blockquote>")?;
|
||||
textwrap::wrap(s, 80)
|
||||
.iter()
|
||||
.try_for_each(|line| writeln!(writer, "{line}"))?;
|
||||
writeln!(writer, "</blockquote>")
|
||||
}
|
||||
Self::PreStart { alt: Some(s) } => {
|
||||
writeln!(writer, "<pre title=\"{s}\">")
|
||||
}
|
||||
Self::PreStart { alt: None } => writeln!(writer, "<pre>"),
|
||||
Self::PreBody(s) => writeln!(writer, "{s}"),
|
||||
Self::PreEnd => writeln!(writer, "</pre>"),
|
||||
Self::Link { url, display } | Self::Prompt { url, display } => {
|
||||
writeln!(
|
||||
writer,
|
||||
"<a href=\"{url}\">{}</a>",
|
||||
match display {
|
||||
Some(d) => d,
|
||||
None => url,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
118
src/parser.rs
Normal file
118
src/parser.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
use {
|
||||
crate::Node,
|
||||
std::{
|
||||
io::{self, BufRead, Write},
|
||||
string::ToString,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Preformatted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Parser<B: BufRead, W: Write> {
|
||||
reader: B,
|
||||
writer: Option<W>,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl<B: BufRead, W: Write> Iterator for Parser<B, W> {
|
||||
type Item = io::Result<Node>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut s = String::new();
|
||||
let node = match self.reader.read_line(&mut s) {
|
||||
Ok(0) => None,
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(_) if self.is_preformatted() => Some(Ok(self.parse_preformatted(&s))),
|
||||
Ok(_) => Some(Ok(self.parse_normal(&s))),
|
||||
};
|
||||
if let Some(ref mut w) = self.writer {
|
||||
if let Some(Ok(_)) = node {
|
||||
// We throw away the possible error here if writing to our cache
|
||||
// fails. TODO: Log this somehow
|
||||
let _s = writeln!(w, "{s}");
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BufRead, W: Write> Parser<B, W> {
|
||||
pub fn new(reader: B, writer: Option<W>) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
writer,
|
||||
mode: Mode::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_preformatted(&self) -> bool {
|
||||
matches!(self.mode, Mode::Preformatted)
|
||||
}
|
||||
|
||||
fn parse_normal(&mut self, s: &str) -> Node {
|
||||
if let Some(s) = s.strip_prefix("```").map(str::trim) {
|
||||
self.mode = Mode::Preformatted;
|
||||
Self::parse_pre_alt(s)
|
||||
} else if let Some(s) = s.strip_prefix("=>").map(str::trim) {
|
||||
Self::parse_link(s)
|
||||
} else if let Some(s) = s.strip_prefix("=:").map(str::trim) {
|
||||
Self::parse_prompt(s)
|
||||
} else if let Some(s) = s
|
||||
.strip_prefix("###")
|
||||
.map(str::trim)
|
||||
.map(ToString::to_string)
|
||||
{
|
||||
Node::H3(s)
|
||||
} else if let Some(s) = s.strip_prefix("##").map(str::trim).map(ToString::to_string) {
|
||||
Node::H2(s)
|
||||
} else if let Some(s) = s.strip_prefix('#').map(str::trim).map(ToString::to_string) {
|
||||
Node::H1(s)
|
||||
} else if let Some(s) = s.strip_prefix('*').map(str::trim).map(ToString::to_string) {
|
||||
Node::ListItem(s)
|
||||
} else if let Some(s) = s.strip_prefix('>').map(str::trim).map(ToString::to_string) {
|
||||
Node::Quote(s)
|
||||
} else {
|
||||
Node::Text(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_preformatted(&mut self, s: &str) -> Node {
|
||||
if s.starts_with("```") {
|
||||
self.mode = Mode::Normal;
|
||||
Node::PreEnd
|
||||
} else {
|
||||
Node::PreBody(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_link(s: &str) -> Node {
|
||||
let (url, display) = s
|
||||
.split_once(|x: char| x.is_ascii_whitespace())
|
||||
.map(|(u, d)| (u.trim().to_string(), Some(d.trim().to_string())))
|
||||
.unwrap_or((s.to_string(), None));
|
||||
Node::Link { url, display }
|
||||
}
|
||||
|
||||
fn parse_prompt(s: &str) -> Node {
|
||||
let (url, display) = s
|
||||
.split_once(|x: char| x.is_ascii_whitespace())
|
||||
.map(|(u, d)| (u.trim().to_string(), Some(d.trim().to_string())))
|
||||
.unwrap_or((s.to_string(), None));
|
||||
Node::Prompt { url, display }
|
||||
}
|
||||
|
||||
fn parse_pre_alt(s: &str) -> Node {
|
||||
let alt = if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s.to_string())
|
||||
};
|
||||
Node::PreStart { alt }
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue