Compare commits

...

12 Commits

Author SHA1 Message Date
2d4f611de1 begun development for single file generation
All checks were successful
Test the running changes / Test (push) Successful in 40s
2025-11-26 19:31:21 +02:00
1c7504e3e0 added special character escaping to md
All checks were successful
Test the running changes / Test (push) Successful in 42s
2025-11-23 22:31:24 +02:00
7d602fffba added cargo-tarpaulin to flake, added some tests to cracked_md
All checks were successful
Test the running changes / Test (push) Successful in 40s
2025-11-23 20:52:05 +02:00
1aa6af3b1a refactored cli and logging
All checks were successful
Test the running changes / Test (push) Successful in 41s
2025-11-21 01:37:19 +02:00
f68b1bb276 modified toolchain, added helix editor 2025-11-20 23:57:53 +02:00
7b300987b7 refactor: split cli and logging to separate crates
All checks were successful
Test the running changes / Test (push) Successful in 42s
2025-11-18 23:41:45 +02:00
33bfff2e98 bugfix, implemented verbose logging cli option
Some checks failed
Test the running changes / Test (push) Failing after 38s
2025-11-17 00:48:23 +02:00
6fa26f8a33 updated workflows
All checks were successful
Test the running changes / Test (push) Successful in 39s
2025-11-16 13:38:35 +02:00
c0999b5e9f changed executable name to gravel
Some checks failed
Test the running changes / Test (push) Successful in 38s
Build and push Docker image / build (push) Has been cancelled
2025-11-16 13:32:09 +02:00
be58e2eb79 testing build workflow
Some checks failed
Test the running changes / Test (push) Successful in 1m51s
Build and push Docker image / build (push) Has been cancelled
2025-11-15 02:09:17 +02:00
8d47704b7e refactored markdown parsing, added some documentation
All checks were successful
Test the running changes / Test (push) Successful in 40s
2025-11-15 01:41:14 +02:00
05a0b32d9b refactor md parser, TODO: parse_str
Some checks failed
Test the running changes / Test (push) Failing after 44s
2025-11-14 02:28:22 +02:00
34 changed files with 1269 additions and 478 deletions

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
html/
web/
pebbles/
site/
target/
result
tarpaulin-report.html

18
Cargo.lock generated
View File

@@ -7,6 +7,7 @@ name = "cracked_md"
version = "0.1.0"
dependencies = [
"fstools",
"slogger",
]
[[package]]
@@ -14,9 +15,22 @@ name = "fstools"
version = "0.1.0"
[[package]]
name = "stdsrv"
name = "gravel_cli"
version = "0.1.0"
dependencies = [
"cracked_md",
"fstools",
"slogger",
"stdsrv",
]
[[package]]
name = "slogger"
version = "0.1.0"
[[package]]
name = "stdsrv"
version = "0.1.0"
dependencies = [
"fstools",
"slogger",
]

View File

@@ -3,6 +3,7 @@ members = [
"stdsrv",
"cracked_md",
"fstools",
"gravel_cli", "slogger",
]
resolver = "3"

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM rust:1.91.1-bookworm AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:12-slim
WORKDIR /app
COPY --from=builder /app/target/release/stdsrv .
CMD ["./stdsrv"]

View File

@@ -54,7 +54,9 @@ need_stdout = false
[jobs.test]
command = [
"cargo", "nextest", "run",
"--hide-progress-bar", "--failure-output", "final"
"--hide-progress-bar",
"--failure-output", "final",
"--no-fail-fast"
]
need_stdout = true
analyzer = "nextest"
@@ -62,7 +64,8 @@ analyzer = "nextest"
[jobs.nextest]
command = [
"cargo", "nextest", "run",
"--hide-progress-bar", "--failure-output", "final"
"--hide-progress-bar",
"--failure-output", "final",
]
need_stdout = true
analyzer = "nextest"

View File

@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
fstools = { path = "../fstools" }
slogger = { path = "../slogger" }

View File

@@ -1,19 +1,10 @@
use crate::to_html::ToHtml;
//! Abstract syntax tree of "Markdown".
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
pub blocks: Vec<Block>,
}
impl ToHtml for Document {
fn to_html(self) -> String {
format!(
"<!doctype html><html lang=en><head></head><body>{}</body></html>",
self.blocks.to_html()
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Block {
Paragraph(Vec<Inline>),
@@ -25,33 +16,10 @@ pub enum Block {
language: Option<String>,
content: String,
},
List(Vec<ListItem>),
List(Vec<Block>),
Quote(Vec<Block>),
}
impl ToHtml for Block {
fn to_html(self) -> String {
match self {
Self::Paragraph(content) => format!("<p>{}</p>", content.to_html()),
Self::Heading { level, content } => {
format!("<h{}>{}</h{}>", level, content.to_html(), level)
}
Self::Code {
language: _,
content,
} => {
format!("<pre><code>{}</code></pre>", content)
}
_ => todo!(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ListItem {
pub blocks: Vec<Block>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Inline {
Text(String),
@@ -60,135 +28,3 @@ pub enum Inline {
Code(String),
Link { text: Vec<Inline>, href: String },
}
impl ToHtml for Inline {
fn to_html(self) -> String {
match self {
Self::Text(s) => s,
Self::Bold(content) => format!("<b>{}</b>", content.to_html()),
Self::Italic(content) => format!("<i>{}</i>", content.to_html()),
Self::Code(s) => format!("<code>{}</code>", s),
Self::Link { text, href } => format!("<a href=\"{}\">{}</a>", href, text.to_html()),
}
}
}
impl<T> ToHtml for Vec<T>
where
T: ToHtml,
{
fn to_html(self) -> String {
let mut rendered = String::new();
for i in self {
rendered.push_str(&i.to_html());
}
rendered
}
}
// --------------------
// TESTS
// --------------------
#[cfg(test)]
mod unit_test {
use super::*;
#[test]
fn single_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![Inline::Text("Heading 1".to_string())],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Heading 1</h1></body></html>"
);
}
#[test]
fn inline_bold_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1></body></html>"
);
}
#[test]
fn headings_and_paragraph_nested_code() {
let ast = Document {
blocks: vec![
Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
},
Block::Heading {
level: 2,
content: vec![Inline::Text("Heading 2".to_string())],
},
Block::Paragraph(vec![
Inline::Text("run ".to_string()),
Inline::Code("sudo rm -rf /".to_string()),
Inline::Text(" on your computer".to_string()),
]),
],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1><h2>Heading 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}
#[cfg(test)]
mod convert_md_to_html_test {
use crate::parser::parse;
use crate::to_html::ToHtml;
#[test]
fn single_header() {
let md = "# Header 1";
let html = parse(md).to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Header 1</h1></body></html>"
);
}
#[test]
fn nested_bold_headers_and_nested_code_paragraph() {
let md = "# *Bold* header 1\n## Header 2\nrun `sudo rm -rf /` on your computer";
let html = parse(md).to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> header 1</h1><h2>Header 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}

View File

@@ -1,29 +1,113 @@
//! A "Markdown" parser and HTML generator. Part of a static site generator `marksmith-rs`.
//! Not following any standards, only vibes.
#![deny(dead_code, unused_imports)]
#![allow(clippy::needless_pass_by_value)]
use fstools::crawl_fs;
use parser::parse;
use slogger::{Level, log};
use std::{
fmt::Display,
fs::{self, File},
io::Write,
path::PathBuf,
time::Instant,
};
use to_html::ToHtml;
pub mod ast;
mod parse_trait;
pub mod parser;
pub mod to_html;
#[derive(Debug)]
pub struct MdParseError {
file: Option<PathBuf>,
line: Option<usize>,
//col: Option<usize>,
expected: String,
got: String,
}
impl MdParseError {
pub fn new(expected: impl ToString, got: impl ToString) -> Self {
Self {
file: None,
line: None,
//col: None,
expected: expected.to_string(),
got: got.to_string(),
}
}
pub fn from_line(line: usize, expected: impl ToString, got: impl ToString) -> Self {
Self {
file: None,
line: Some(line),
//col: None,
expected: expected.to_string(),
got: got.to_string(),
}
}
/*
pub fn from_col(col: usize, expected: impl ToString, got: impl ToString) -> Self {
Self {
file: None,
line: None,
col: Some(col),
expected: expected.to_string(),
got: got.to_string(),
}
}
*/
#[must_use]
pub fn set_line(self, line: usize) -> Self {
Self {
file: self.file,
line: Some(line),
//col: self.col,
expected: self.expected,
got: self.got,
}
}
#[must_use]
pub fn set_file(self, file: PathBuf) -> Self {
Self {
file: Some(file),
line: self.line,
//col: self.col,
expected: self.expected,
got: self.got,
}
}
}
impl Display for MdParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// no error message :/
let file = self.file.clone().unwrap_or("<unknown>".into());
write!(
f,
"Parse error in '{}' on line {}: expected '{}', got '{}'",
file.display(),
self.line.unwrap_or(0),
//self.col.unwrap_or(0),
self.expected,
self.got
)
}
}
impl std::error::Error for MdParseError {}
#[derive(Debug)]
pub enum Error {
OutDirIsNotEmpty,
OutDirFileDeleteNotAllowed,
OutDirDirectoryInPlaceOfFile,
FileRead,
DirRead,
FileWrite,
FileCreate,
DirCreate,
FSError(String),
Parse(MdParseError),
}
impl Display for Error {
@@ -32,45 +116,106 @@ impl Display for Error {
}
}
impl From<MdParseError> for Error {
fn from(value: MdParseError) -> Self {
Error::Parse(value)
}
}
impl std::error::Error for Error {}
type Result<T> = std::result::Result<T, crate::Error>;
/// Takes two directories and a force flag as parameters, generates html files to the outdir in the
/// same directory structure as the md files in indir.
///
/// # Errors
/// Anything wrong with reading files from the directories or parsing the files.
pub fn generate(indir: &PathBuf, outdir: &PathBuf, force: bool) -> Result<()> {
let start_time = Instant::now();
let mut generated_files = 0;
if !indir.is_dir() {
Err(Error::FSError("In directory not found".to_string()))?;
}
if !outdir.is_dir() {
Err(Error::FSError("Out directory not found".to_string()))?;
}
let files = crawl_fs(indir);
for path in files {
let fullpath = indir.as_path().join(&path);
// read and parse md file
let content = fs::read_to_string(&fullpath).map_err(|_e| Error::FileRead)?;
let html = parse(&content).to_html();
let content = fs::read_to_string(&fullpath)
.map_err(|_e| Error::FSError(format!("File `{}` read error", path.display())))?;
let html = parse(&content)?.to_html();
// write html data to file
let mut newpath = outdir.to_owned();
newpath.push(path);
newpath.push(&path);
newpath.set_extension("html");
// check if path exists
if newpath.exists() {
// remove if is file and if force, otherwise error
if newpath.is_file() && force {
fs::remove_file(&newpath).map_err(|_e| Error::OutDirFileDeleteNotAllowed)?;
if newpath.is_file() {
if force {
fs::remove_file(&newpath).map_err(|_e| {
Error::FSError(format!("File `{}` deleting not allowed", newpath.display()))
})?;
} else {
Err(Error::FSError(
"File overwrite denied, enable force overwrite".to_string(),
))?;
}
} else {
Err(Error::OutDirDirectoryInPlaceOfFile)?;
Err(Error::FSError(format!(
"Directory `{}` in place of file in out directory",
newpath.display()
)))?;
}
}
//println!("About to write file '{}'", newpath.display());
let parent = newpath.parent().ok_or(Error::DirCreate)?;
fs::create_dir_all(parent).map_err(|_e| Error::DirCreate)?;
let mut newfile = File::create_new(newpath).map_err(|_e| Error::FileCreate)?;
let parent = newpath.parent().ok_or(Error::FSError(format!(
"Access to parent directory of `{}` denied",
newpath.display()
)))?;
fs::create_dir_all(parent)
.map_err(|_e| Error::FSError("Creating directory tree failed".to_string()))?;
let mut newfile = File::create_new(&newpath).map_err(|_e| {
Error::FSError(format!("Creating file `{}` failed", &newpath.display()))
})?;
newfile
.write(html.as_bytes())
.map_err(|_e| Error::FileWrite)?;
newfile.write(html.as_bytes()).map_err(|_e| {
Error::FSError(format!("Writing to file `{}` failed", newpath.display()))
})?;
log!(
Level::Debug,
"File `{}` generation to `{}` successful",
path.display(),
newpath.display()
);
generated_files += 1;
}
let time = start_time.elapsed();
let time_report = if time.as_micros() < 10000 {
format!("{} μs", time.as_micros())
} else {
format!("{} ms", time.as_millis())
};
log!(
Level::Info,
"Generated {} files in {} without reported errors",
generated_files,
time_report
);
Ok(())
}

View File

@@ -0,0 +1,176 @@
/*
use crate::MdParseError;
pub type Pattern<T> = Vec<PatternToken<T>>;
pub enum PatternToken<T> {
Once(T),
Optional(T),
AtLeastOnce(T),
NTimes(T),
}
/// panics: on invalid pattern
pub fn char_pattern(s: &str) -> Pattern<char> {
let mut s_chars = s.chars().peekable();
let mut pat: Pattern<char> = Vec::new();
while let Some(token) = s_chars.next() {
pat.push(if let Some(&next) = s_chars.peek() {
match next {
'?' => {
s_chars.next().unwrap();
PatternToken::Optional(token)
}
'+' => {
s_chars.next().unwrap();
PatternToken::AtLeastOnce(token)
}
'*' => {
s_chars.next().unwrap();
PatternToken::NTimes(token)
}
_ => PatternToken::Once(token),
}
} else {
PatternToken::Once(token)
});
}
pat
}
pub trait ParsePattern: Iterator + Clone {
fn parse<T>(&mut self, expect: Pattern<T>) -> Result<Vec<Self::Item>, MdParseError>
where
T: PartialEq<<Self as Iterator>::Item>,
{
let mut consumed = Vec::new();
let mut cloned = self.clone();
for pat_token in expect {
match pat_token {
PatternToken::Once(c) => {
if !cloned.next().map(|v| c == v).unwrap_or(false) {
return None;
}
}
PatternToken::Optional(c) => if cloned.peek().map(|v| c == *v).unwrap_or(false) {},
}
}
*self = cloned;
Some(consumed)
}
}
*/
pub trait Parse: Iterator + Clone {
fn follows(&mut self, token: char) -> bool;
fn parse_token(&mut self, token: char) -> bool {
if self.follows(token) {
let _ = self.next();
true
} else {
false
}
}
fn parse_str(&mut self, tokens: &str) -> bool {
let mut cloned = self.clone();
for pat_token in tokens.chars() {
if cloned.follows(pat_token) {
cloned.next();
} else {
return false;
}
}
*self = cloned;
true
}
}
impl Parse for std::iter::Peekable<std::str::Chars<'_>> {
fn follows(&mut self, token: char) -> bool {
self.peek().is_some_and(|c| c == &token)
}
}
impl Parse for std::iter::Peekable<std::iter::Enumerate<std::str::Chars<'_>>> {
fn follows(&mut self, token: char) -> bool {
self.peek().is_some_and(|&(_i, c)| c == token)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn chars_parse_follows_double() {
let mut c = "abc".chars().peekable();
assert!(c.follows('a'));
assert!(c.follows('a'));
}
#[test]
fn chars_parse_tokens() {
let mut c = "abcdef".chars().peekable();
assert!(c.parse_token('a'));
assert!(c.parse_token('b'));
}
#[test]
fn chars_parse_str() {
let mut c = "abcdef".chars().peekable();
assert!(c.parse_str("abc"));
assert!(c.parse_str("def"));
}
#[test]
fn enumerate_parse_follows_double() {
let mut c = "abc".chars().enumerate().peekable();
assert!(c.follows('a'));
assert!(c.follows('a'));
}
#[test]
fn enumerate_parse_tokens() {
let mut c = "abcdef".chars().enumerate().peekable();
assert!(c.parse_token('a'));
assert!(c.parse_token('b'));
}
#[test]
fn enumerate_parse_str() {
let mut c = "abcdef".chars().enumerate().peekable();
assert!(c.parse_str("abc"));
assert!(c.parse_str("def"));
}
#[test]
fn enumerate_parse_token_failed_not_consume() {
let mut c = "abc".chars().enumerate().peekable();
assert!(!c.parse_token('b'));
assert!(c.parse_token('a'));
}
#[test]
fn enumerate_parse_str_failed_not_consume() {
let mut c = "abcdef".chars().enumerate().peekable();
assert!(!c.parse_str("def"));
assert!(c.parse_str("abc"));
}
}

View File

@@ -1,14 +1,19 @@
//! Parse "Markdown" to AST.
mod block;
mod inline;
use block::parse_blocks;
use crate::ast::Document;
use crate::{MdParseError, ast::Document};
pub fn parse(s: &str) -> Document {
Document {
blocks: parse_blocks(s),
}
/// Parses the incoming data to a Markdown abstract syntax tree.
/// # Errors
/// This function will return an `MdParseError` when any part of the input is invalid Markdown.
pub fn parse(s: &str) -> Result<Document, MdParseError> {
Ok(Document {
blocks: parse_blocks(s)?,
})
}
#[cfg(test)]
@@ -20,7 +25,7 @@ mod test {
fn only_paragraph() {
let md = "testing paragraph";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
Document {
@@ -35,7 +40,7 @@ mod test {
fn different_headers() {
let md = "# Header 1\n## Header 2";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -58,7 +63,7 @@ mod test {
fn inline_bold_and_italics() {
let md = "some *bold* and _italic_ text";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -78,7 +83,7 @@ mod test {
fn inline_code() {
let md = "run command `sudo rm -rf /`";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -95,7 +100,7 @@ mod test {
fn bold_header() {
let md = "# Header is *bold*";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -115,7 +120,7 @@ mod test {
fn anonymous_code_block() {
let md = "```\necho hello\n```";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -128,11 +133,25 @@ mod test {
);
}
#[test]
fn code_block_content_after_end() {
let md = "```\necho hello\n```abc";
let doc_res = parse(md);
assert!(doc_res.is_err());
}
#[test]
fn code_block_no_terminating() {
let md = "```\nabc\n";
let doc_res = parse(md);
assert!(doc_res.is_err());
}
#[test]
fn rust_code_block() {
let md = "```rust\nfn main() {\n\tprintln!(\"Hello world!\");\n}\n```";
let doc = parse(md);
let doc = parse(md).unwrap();
assert_eq!(
doc,
@@ -145,3 +164,4 @@ mod test {
);
}
}
// */

View File

@@ -1,22 +1,125 @@
use crate::ast::Block;
use super::inline::parse_inlines;
use crate::{MdParseError, ast::Block};
pub fn parse_blocks(input: &str) -> Vec<Block> {
use crate::parse_trait::Parse;
pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
let mut blocks = Vec::new();
let mut lines = input.lines().enumerate().peekable();
while let Some((i, line)) = lines.next() {
let mut line_chars = line.chars().peekable();
// empty line
if line_chars.peek().is_none() {
continue;
}
// header
let mut heading_level = 0;
while line_chars.parse_token('#') {
if heading_level < 6 {
heading_level += 1;
}
}
if heading_level > 0 {
if !line_chars.parse_token(' ') {
Err(MdParseError::from_line(
i + 1,
"<space> after #",
"no <space>",
))?;
}
let line_content: String = line_chars.collect();
blocks.push(Block::Heading {
level: heading_level,
content: parse_inlines(&line_content)?,
});
continue;
}
// quote TODO
/*
if line_chars.parse_str("> ") {
let content: String = line_chars.collect();
let quote_blocks = parse_blocks(&content).map_err(|e| e.set_line(i + 1))?;
blocks.push(Block::Quote(quote_blocks));
continue;
}
*/
// unordered list TODO
if line_chars.parse_str("- ") {
todo!()
}
// code
if line_chars.parse_str("```") {
let lang_line: String = line_chars.collect();
let lang = if lang_line.is_empty() {
None
} else {
Some(lang_line)
};
let mut code = String::new();
let mut successful = false;
for (j, line) in lines.by_ref() {
let mut code_line_chars = line.chars().peekable();
// code block end
if code_line_chars.parse_str("```") {
let remaining: String = code_line_chars.collect();
if remaining.is_empty() {
blocks.push(Block::Code {
language: lang,
content: code,
});
successful = true;
break;
}
Err(MdParseError::from_line(
j + 1,
"```",
format!("```{remaining}"),
))?;
} else {
code.push_str(line);
code.push('\n');
}
}
if successful {
continue;
}
Err(MdParseError::from_line(i + 1, "a terminating '```'", ""))?;
}
// lists TODO
// paragraph
blocks.push(Block::Paragraph(
parse_inlines(line).map_err(|e| e.set_line(i + 1))?,
));
}
Ok(blocks)
}
/*
pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
let mut blocks = Vec::new();
let mut lines = input.lines().peekable();
let mut lines = input.lines().enumerate().peekable();
while let Some(line) = lines.next() {
while let Some((i, line)) = lines.next() {
if line.starts_with("#") {
let level = line.chars().take_while(|&c| c == '#').count() as u8;
let text = line[level as usize..].trim();
blocks.push(Block::Heading {
level,
content: parse_inlines(text),
content: parse_inlines(text).map_err(|e| e.set_line(i + 1))?,
});
} else if let Some(quote_body) = line.strip_prefix(">") {
let quote_blocks = parse_blocks(quote_body);
let quote_blocks = parse_blocks(quote_body).map_err(|e| e.set_line(i + 1))?;
blocks.push(Block::Quote(quote_blocks));
} else if line.starts_with("```") {
let lang_line = line.strip_prefix("```").unwrap().to_string();
@@ -26,8 +129,16 @@ pub fn parse_blocks(input: &str) -> Vec<Block> {
Some(lang_line)
};
let mut code = String::new();
while lines.peek().is_some() && !lines.peek().unwrap().starts_with("```") {
code.push_str(&format!("{}\n", lines.next().unwrap()));
while lines.peek().is_some()
&& !lines
.peek()
.ok_or(MdParseError::from_line(i + 1, "a line", ""))?
.1
.starts_with("```")
{
if let Some((_i, l)) = lines.next() {
code.push_str(&format!("{}\n", l));
}
}
lines.next();
blocks.push(Block::Code {
@@ -37,9 +148,12 @@ pub fn parse_blocks(input: &str) -> Vec<Block> {
} else if line.trim().is_empty() {
continue;
} else {
blocks.push(Block::Paragraph(parse_inlines(line)));
blocks.push(Block::Paragraph(
parse_inlines(line).map_err(|e| e.set_line(i + 1))?,
));
}
}
blocks
Ok(blocks)
}
*/

View File

@@ -1,61 +1,183 @@
use crate::ast::Inline;
use crate::{MdParseError, ast::Inline};
pub fn parse_inlines(input: &str) -> Vec<Inline> {
pub fn parse_inlines(input: &str) -> Result<Vec<Inline>, MdParseError> {
let mut inlines = Vec::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'*' => {
let inner = collect_until(&mut chars, '*');
inlines.push(Inline::Bold(parse_inlines(&inner)));
let inner = collect_until(&mut chars, '*')?;
inlines.push(Inline::Bold(parse_inlines(&inner)?));
}
'_' => {
let inner = collect_until(&mut chars, '_');
inlines.push(Inline::Italic(parse_inlines(&inner)));
let inner = collect_until(&mut chars, '_')?;
inlines.push(Inline::Italic(parse_inlines(&inner)?));
}
'`' => {
let code = collect_until(&mut chars, '`');
let code = collect_until(&mut chars, '`')?;
inlines.push(Inline::Code(code));
}
'[' => {
let text = collect_until(&mut chars, ']');
if chars.next() == Some('(') {
let href = collect_until(&mut chars, ')');
let text = collect_until(&mut chars, ']')?;
if let Some('(') = chars.next() {
let href = collect_until(&mut chars, ')')?;
inlines.push(Inline::Link {
text: parse_inlines(&text),
text: parse_inlines(&text)?,
href,
});
} else {
Err(MdParseError::new(
"(<href>)",
chars.next().unwrap_or_default(),
))?;
}
}
_ => {
let mut text = String::new();
text.push(c);
let mut escaped = false;
while let Some(&nc) = chars.peek() {
if matches!(nc, '*' | '_' | '`' | '[') {
if matches!(nc, '*' | '_' | '`' | '[') && !escaped {
break;
}
text.push(chars.next().unwrap());
let next_c = chars.next().ok_or(MdParseError::new("a character", ""))?;
if next_c == '\\' && !escaped {
escaped = true;
} else {
escaped = false;
text.push(next_c);
}
}
inlines.push(Inline::Text(text));
}
}
}
inlines
Ok(inlines)
}
fn collect_until<I: Iterator<Item = char>>(
chars: &mut std::iter::Peekable<I>,
end: char,
) -> String {
) -> Result<String, MdParseError> {
let mut s = String::new();
while let Some(&c) = chars.peek() {
for c in chars.by_ref() {
if c == end {
chars.next();
break;
return Ok(s);
}
s.push(chars.next().unwrap());
s.push(c);
}
Err(MdParseError::new(end, ""))
}
#[cfg(test)]
mod test {
use crate::ast::Inline;
use super::{collect_until, parse_inlines};
#[test]
fn collect_until_without_end() {
let mut s = "abcdef".chars().peekable();
let res = collect_until(&mut s, '.');
assert!(res.is_err());
}
#[test]
fn bold_text() {
let md = "*abc*";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![Inline::Bold(vec![Inline::Text("abc".to_string())])]
);
}
#[test]
fn italic_text() {
let md = "_abc_";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![Inline::Italic(vec![Inline::Text("abc".to_string())])]
);
}
#[test]
fn bold_italic_text() {
let md = "*_abc_*";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![Inline::Bold(vec![Inline::Italic(vec![Inline::Text(
"abc".to_string()
)])])]
);
}
#[test]
fn code() {
let md = "`sudo rm -rf /`";
let inl = parse_inlines(md).unwrap();
assert_eq!(inl, vec![Inline::Code("sudo rm -rf /".to_string())]);
}
#[test]
fn text_and_code() {
let md = "run `sudo rm -rf /` on your computer";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![
Inline::Text("run ".to_string()),
Inline::Code("sudo rm -rf /".to_string()),
Inline::Text(" on your computer".to_string())
]
);
}
#[test]
fn single_hyperlink() {
let md = "a link to [my site](https://example.com)";
let inl = parse_inlines(md).unwrap();
assert_eq!(
inl,
vec![
Inline::Text("a link to ".to_string()),
Inline::Link {
text: vec![Inline::Text("my site".to_string())],
href: "https://example.com".to_string()
}
]
);
}
#[test]
fn hyperlink_without_link() {
let md = "[abc]";
let inl = parse_inlines(md);
assert!(inl.is_err());
}
#[test]
fn escape_brackets() {
let md = r"some \[text\]";
let inl = parse_inlines(md).unwrap();
assert_eq!(inl, vec![Inline::Text("some [text]".to_string())]);
}
#[test]
fn escape_escape() {
let md = r"backslash \\";
let inl = parse_inlines(md).unwrap();
assert_eq!(inl, vec![Inline::Text(r"backslash \".to_string())]);
}
s
}

View File

@@ -1,3 +1,198 @@
//! A trait + implementations for generating HTML.
use crate::ast::{Block, Document, Inline};
pub trait ToHtml {
fn to_html(self) -> String;
}
impl ToHtml for Document {
fn to_html(self) -> String {
format!(
"<!doctype html><html lang=en><head></head><body>{}</body></html>",
self.blocks.to_html()
)
}
}
impl ToHtml for Block {
fn to_html(self) -> String {
match self {
Self::Paragraph(content) => format!("<p>{}</p>", content.to_html()),
Self::Heading { level, content } => {
format!("<h{}>{}</h{}>", level, content.to_html(), level)
}
Self::Code {
language: _,
content,
} => {
format!("<pre><code>{content}</code></pre>")
}
_ => todo!(),
}
}
}
impl ToHtml for Inline {
fn to_html(self) -> String {
match self {
Self::Text(s) => s,
Self::Bold(content) => format!("<b>{}</b>", content.to_html()),
Self::Italic(content) => format!("<i>{}</i>", content.to_html()),
Self::Code(s) => format!("<code>{s}</code>"),
Self::Link { text, href } => format!("<a href=\"{}\">{}</a>", href, text.to_html()),
}
}
}
impl<T> ToHtml for Vec<T>
where
T: ToHtml,
{
fn to_html(self) -> String {
let mut rendered = String::new();
for i in self {
rendered.push_str(&i.to_html());
}
rendered
}
}
// --------------------
// TESTS
// --------------------
#[cfg(test)]
mod unit_test {
use super::*;
#[test]
fn single_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![Inline::Text("Heading 1".to_string())],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Heading 1</h1></body></html>"
);
}
#[test]
fn inline_bold_header() {
let ast = Document {
blocks: vec![Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
}],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1></body></html>"
);
}
#[test]
fn headings_and_paragraph_nested_code() {
let ast = Document {
blocks: vec![
Block::Heading {
level: 1,
content: vec![
Inline::Bold(vec![Inline::Text("Bold".to_string())]),
Inline::Text(" heading 1".to_string()),
],
},
Block::Heading {
level: 2,
content: vec![Inline::Text("Heading 2".to_string())],
},
Block::Paragraph(vec![
Inline::Text("run ".to_string()),
Inline::Code("sudo rm -rf /".to_string()),
Inline::Text(" on your computer".to_string()),
]),
],
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> heading 1</h1><h2>Heading 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}
#[cfg(test)]
mod convert_md_to_html_test {
use crate::parser::parse;
use crate::to_html::ToHtml;
#[test]
fn single_header() {
let md = "# Header 1";
let ast = match parse(md) {
Ok(a) => a,
Err(e) => panic!("{}", e),
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1>Header 1</h1></body></html>"
);
}
#[test]
fn single_header_wrong_format() {
let md = "#Whoops";
let ast = parse(md);
assert!(ast.is_err());
}
#[test]
fn nested_bold_headers_and_nested_code_paragraph() {
let md = "# *Bold* header 1\n## Header 2\nrun `sudo rm -rf /` on your computer";
let ast = match parse(md) {
Ok(a) => a,
Err(e) => panic!("{}", e),
};
let html = ast.to_html();
assert_eq!(
html,
"<!doctype html><html lang=en><head></head><body><h1><b>Bold</b> header 1</h1><h2>Header 2</h2><p>run <code>sudo rm -rf /</code> on your computer</p></body></html>"
);
}
}
#[cfg(test)]
mod parse_real_md {
use std::fs;
use crate::parser::parse;
#[test]
fn go() {
let file = "./test.md";
let md = fs::read_to_string(file).expect("reading ./test.md failed");
let _ast = match parse(&md).map_err(|e| e.set_file(file.into())) {
Ok(a) => a,
Err(e) => panic!("{}", e),
};
}
}

7
cracked_md/test.md Normal file
View File

@@ -0,0 +1,7 @@
# Header *1kkkkkkkkkkkkkkkkkkkkkk*
this is some code: `abc`
```code
oiajwefoijao089uaoisdjfoijasdfoijasdofij
```

View File

@@ -1,5 +1,5 @@
{
description = "stdsrv Flake file";
description = "gravel project flake";
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
@@ -14,16 +14,18 @@
flake-utlis.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {inherit system;};
stdsrv = import ./package.nix {inherit pkgs;};
gravel = import ./package.nix {inherit pkgs;};
in {
packages.default = stdsrv;
packages.default = gravel;
devShells.default = pkgs.mkShell {
packages = [
pkgs.rustup
pkgs.bacon
pkgs.cargo-nextest
pkgs.cargo-expand
pkgs.cargo-watch
packages = with pkgs; [
rustup
helix
bacon
cargo-nextest
cargo-expand
cargo-watch
cargo-tarpaulin
];
};
}

View File

@@ -2,13 +2,14 @@ use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
/// Recursively crawl through the directory given and aggregate all file handles to a HashMap with
/// Recursively crawl through the directory given and aggregate all file handles to a `HashMap` with
/// their respective (relative) paths as keys.
#[must_use]
pub fn crawl_fs(root: &PathBuf) -> HashSet<PathBuf> {
crawl_fs_rec(root, root)
}
/// Helper function: Recursively crawl through the directory given and aggregate all file handles to a HashMap with
/// Helper function: Recursively crawl through the directory given and aggregate all file handles to a `HashMap` with
/// their respective (relative) paths as keys.
fn crawl_fs_rec(root: &PathBuf, path: &PathBuf) -> HashSet<PathBuf> {
let mut subdirs = Vec::with_capacity(100);

14
gravel_cli/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "gravel_cli"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "gravel"
path = "src/main.rs"
[dependencies]
cracked_md = { path = "../cracked_md" }
stdsrv = { path = "../stdsrv" }
slogger = { path = "../slogger" }

173
gravel_cli/src/config.rs Normal file
View File

@@ -0,0 +1,173 @@
//! Simple and program specific command line argument parsing solution.
// todo: refactor to <command> <subcommand> [<options>]
use slogger::{LOG_LEVEL, Level, log};
use crate::error::Error;
use std::env::Args;
use std::net::Ipv4Addr;
use std::path::{Path, PathBuf};
pub enum Command {
Generate { force: bool, single: bool },
Serve { addr: Ipv4Addr, port: u16 },
Init,
}
impl Default for Command {
fn default() -> Self {
Self::Generate {
force: true,
single: false,
}
}
}
impl TryFrom<Args> for Command {
type Error = Error;
fn try_from(mut value: Args) -> Result<Self, Self::Error> {
let mut comm = Command::default();
let _ = value.next(); // ignore executable
let command = value.next();
// `gravel serve` command
if let Some("serve") = command.as_deref() {
let mut addr = Ipv4Addr::UNSPECIFIED;
let mut port = 8080;
while let Some(a) = value.next() {
match a.as_str() {
"-a" => {
let address_str = value.next().ok_or(Error::CommandLineArgsParse(
"Missing argument after `-a`. Expected IPv4 address.".to_string(),
))?;
addr = Ipv4Addr::parse_ascii(address_str.as_bytes()).map_err(|_e| {
Error::CommandLineArgsParse("Parsing IP address failed".to_string())
})?;
}
"-p" => {
let port_str = value.next().ok_or(Error::CommandLineArgsParse(
"Missing argument after `-p`. Expected TCP port number.".to_string(),
))?;
port = port_str.parse().map_err(|_e| {
Error::CommandLineArgsParse("Parsing TCP port failed".to_string())
})?;
}
&_ => Err(Error::CommandLineArgsParse(format!(
"Unknown argument: `{a}`"
)))?,
}
}
comm = Command::Serve { addr, port };
}
// `gravel init` command
else if let Some("init") = command.as_deref() {
if let Some(a) = value.next() {
Err(Error::CommandLineArgsParse(format!(
"Unexpected argument: `{a}`"
)))?;
}
comm = Command::Init;
}
// `gravel` command
for a in value {
match a.as_str() {
"-s" => {
comm = Command::Generate {
force: true,
single: true,
}
}
_ => Err(Error::CommandLineArgsParse(format!(
"Unknown argument: `{a}`"
)))?,
}
}
Ok(comm)
}
}
#[allow(unused)]
pub struct ProgramConfig {
pub outdir: PathBuf,
pub indir: PathBuf,
pub command: Command,
pub verbose: bool,
}
impl Default for ProgramConfig {
fn default() -> Self {
Self {
indir: PathBuf::from("./pebbles"),
outdir: PathBuf::from("./site"),
command: Command::default(),
verbose: true,
}
}
}
impl ProgramConfig {
pub fn new<P: AsRef<Path>>(_toml_file: P, args: Args) -> Result<Self, Error> {
let conf = Self {
command: args.try_into()?,
..Default::default()
};
LOG_LEVEL.get_or_init(|| Level::Debug);
log!(Level::Warn, "TOML parsing not implemented, skipping");
Ok(conf)
}
}
/*
impl TryFrom<std::env::Args> for ProgramConfig {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::CommandLineArgsParse(
"Expected input directory after option `-i`".to_string(),
))?
.into();
}
"-a" => {
let addr_string = value.next().ok_or(Error::CommandLineArgsParse(
"Expected listener IPv4 address after option `-a`".to_string(),
))?;
a.addr = Ipv4Addr::parse_ascii(addr_string.as_bytes()).map_err(|_e| {
Error::CommandLineArgsParse(
"Invalid IPv4 address after option `-a`".to_string(),
)
})?;
}
"-p" => {
let port_string = value.next().ok_or(Error::CommandLineArgsParse(
"Expected listener port after option `-p`".to_string(),
))?;
a.port = port_string.parse().map_err(|_e| {
Error::CommandLineArgsParse(
"Invalid 16-bit port number after option `-p`".to_string(),
)
})?;
}
"-g" => a.generate = true,
"-f" => a.force = true,
"-v" => {
a.verbose = true;
LOG_LEVEL.get_or_init(|| Level::Debug);
}
_ => {
a.outdir = v.into();
}
}
}
LOG_LEVEL.get_or_init(|| Level::Info);
Ok(a)
}
}
*/

32
gravel_cli/src/error.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::fmt::Display;
#[derive(Debug)]
pub enum Error {
Server(stdsrv::error::Error),
MdParse(cracked_md::Error),
CommandLineArgsParse(String),
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Server(e) => e.fmt(f),
Error::MdParse(e) => e.fmt(f),
Error::CommandLineArgsParse(s) => write!(f, "{s}"),
}
}
}
impl std::error::Error for Error {}
impl From<cracked_md::Error> for Error {
fn from(value: cracked_md::Error) -> Self {
Self::MdParse(value)
}
}
impl From<stdsrv::error::Error> for Error {
fn from(value: stdsrv::error::Error) -> Self {
Self::Server(value)
}
}

37
gravel_cli/src/main.rs Normal file
View File

@@ -0,0 +1,37 @@
#![feature(addr_parse_ascii, never_type)]
use std::process;
use config::{Command, ProgramConfig};
use cracked_md::generate;
use error::Error;
use slogger::{Level, log};
use stdsrv::serve;
mod config;
mod error;
fn run() -> Result<(), Error> {
let conf = ProgramConfig::new("gravel.toml", std::env::args())?;
match conf.command {
Command::Init => todo!("project init"),
Command::Serve { addr, port } => serve(addr, port, conf.outdir)?,
Command::Generate {
force,
single: false,
} => generate(&conf.indir, &conf.outdir, force)?,
Command::Generate {
force: _f,
single: true,
} => todo!("single file generation"),
}
Ok(())
}
fn main() {
let _ = run().map_err(|e| {
log!(Level::Error, "{}", e);
process::exit(1);
});
}

View File

@@ -5,5 +5,6 @@ targets = [
]
components = [
"clippy",
"rustfmt"
"rustfmt",
"rust-analyzer"
]

6
slogger/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "slogger"
version = "0.1.0"
edition = "2024"
[dependencies]

49
slogger/src/lib.rs Normal file
View File

@@ -0,0 +1,49 @@
use std::{fmt::Display, sync::OnceLock};
pub static LOG_LEVEL: OnceLock<Level> = OnceLock::new();
#[allow(dead_code)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Error,
Warn,
Info,
Debug,
}
impl Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}]\x1b[0m",
match self {
Self::Error => "\x1b[1;31m[ERROR",
Self::Warn => "\x1b[1;33m[WARN",
Self::Info => "\x1b[0;32m[INFO",
Self::Debug => "\x1b[0;36m[DEBUG",
}
)
}
}
/// A logging macro. Takes a [`Level`] and a formatted string.
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
let log_from = if &$level == &$crate::Level::Debug {
format!(" {}:{}", std::file!(), std::line!())
} else {
String::new()
};
if &$level <= $crate::LOG_LEVEL.get().unwrap_or(&$crate::Level::Info) {
println!(
"{}{}: {}",
$level,
log_from,
format!($($arg)*)
);
}
}};
}
// todo: implement clean verbose/short logging

View File

@@ -5,5 +5,5 @@ edition = "2024"
# local dependencies
[dependencies]
cracked_md = { path = "../cracked_md" }
fstools = { path = "../fstools" }
slogger = { path = "../slogger" }

View File

@@ -1,58 +0,0 @@
use std::path::PathBuf;
use crate::error::Error;
use crate::error::ErrorKind;
pub struct ProgramArgs {
pub outdir: PathBuf,
pub indir: PathBuf,
pub generate: bool,
pub force: bool,
pub addr: String,
}
impl Default for ProgramArgs {
fn default() -> Self {
Self {
indir: PathBuf::from("./web"),
outdir: PathBuf::from("./html"),
generate: false,
force: false,
addr: "0.0.0.0:8080".to_string(),
}
}
}
impl TryFrom<std::env::Args> for ProgramArgs {
type Error = crate::error::Error;
fn try_from(mut value: std::env::Args) -> Result<Self, Self::Error> {
let mut a = Self::default();
let _ = value.next(); // ignore executable path
while let Some(v) = value.next() {
match v.as_str() {
"-i" => {
a.indir = value
.next()
.ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected input directory after option `-i`",
))?
.into();
}
"-a" => {
a.addr = value.next().ok_or(Error::new(
ErrorKind::CommandLineArgsParse,
"Expected listener address after option `-a`",
))?;
}
"-g" => a.generate = true,
"-f" => a.force = true,
_ => {
a.outdir = v.into();
}
}
}
Ok(a)
}
}

View File

@@ -27,6 +27,7 @@ pub struct Error {
}
impl Error {
#[must_use]
pub fn new(kind: ErrorKind, msg: &str) -> Self {
Self {
kind,

View File

@@ -1,10 +1,11 @@
//! FileServer
//! A simple server implementation that just responds with the contents of the file requested in
//! the provided directory.
use std::fs;
use std::path::PathBuf;
use std::{fs, path::Path};
use crate::{
error::*,
error::{Error, ErrorKind, Result},
request::{HttpMethod, HttpRequest},
responder::Responder,
response::{HttpResponse, HttpStatus},
@@ -15,7 +16,7 @@ pub struct FileServer {
}
impl FileServer {
pub fn new(root: &PathBuf) -> Result<FileServer> {
pub fn new(root: &Path) -> Result<FileServer> {
if !root.is_dir() {
return Err(Error::new(
ErrorKind::DirNotFound,
@@ -51,7 +52,6 @@ impl Responder for FileServer {
let content_type = match req.path.extension() {
Some(s) => match s.as_encoded_bytes() {
b"html" | b"htm" => "text/html",
b"txt" => "text/plain",
b"css" => "text/css",
b"js" => "text/javascript",
b"pdf" => "application/pdf",

View File

@@ -1,36 +1,38 @@
//! Wrapper type of a `HashMap` to contain HTTP headers and format them correctly.
use std::{collections::HashMap, fmt::Display};
#[derive(Debug)]
pub struct HttpHeaders {
_inner: HashMap<String, String>,
inner: HashMap<String, String>,
}
impl HttpHeaders {
pub fn new() -> Self {
HttpHeaders {
_inner: HashMap::new(),
inner: HashMap::new(),
}
}
pub fn add(&mut self, k: &str, v: &str) {
self._inner.insert(k.to_string(), v.to_string());
self.inner.insert(k.to_string(), v.to_string());
}
#[cfg(test)]
pub fn get(&self, k: &str) -> Option<&String> {
self._inner.get(k)
self.inner.get(k)
}
#[cfg(test)]
pub fn len(&self) -> usize {
self._inner.len()
self.inner.len()
}
}
impl Display for HttpHeaders {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (k, v) in self._inner.iter() {
write!(f, "{}: {}\r\n", k, v)?;
for (k, v) in &self.inner {
write!(f, "{k}: {v}\r\n")?;
}
Ok(())
}

View File

@@ -1,43 +0,0 @@
use crate::error::ErrorKind;
use crate::log;
use crate::logger::Level;
use std::io::Read;
use std::net::{TcpListener, TcpStream};
use crate::{error::Error, request::HttpRequest};
pub struct HttpStream {
tcp_listener: TcpListener,
}
impl HttpStream {
pub fn new(addr: &str) -> Self {
let tcp_listener = TcpListener::bind(addr)
.unwrap_or_else(|e| panic!("Failed to bind on address `{}`: {}", addr, e));
log!(Level::Info, "Listening on `{}`", addr);
Self { tcp_listener }
}
}
impl Iterator for HttpStream {
type Item = (HttpRequest, TcpStream);
fn next(&mut self) -> Option<Self::Item> {
// safe to unwrap, because Incoming never returns None
let mut stream = self.tcp_listener.incoming().next().unwrap().ok()?;
let mut buf = [0; 1024];
let _read_bytes = stream
.read(&mut buf)
.or(Err(Error::new(
ErrorKind::StreamReadFailed,
"Reading from TCP stream failed",
)))
.ok()?;
Some((
String::from_utf8_lossy(&buf[..]).trim().try_into().ok()?,
stream,
))
}
}

View File

@@ -1,38 +1,41 @@
//! This is my very monolithic file server with zero dependencies (other than rust
//! stdlib).
//!
//! Documentation is a work in progress, go see my webpage at [jlux.dev](https://jlux.dev).
//! A simple web server with 0 dependencies (other than Rust's stdlib).
#![feature(never_type)]
#![allow(dead_code)]
use std::{
io::{BufReader, BufWriter},
net::TcpListener,
process,
net::{Ipv4Addr, TcpListener},
path::PathBuf,
};
use args::ProgramArgs;
use cracked_md::generate;
use error::Error;
use fileserver::FileServer;
use logger::Level;
use request::HttpRequest;
use responder::Responder;
use slogger::{Level, log};
mod args;
mod error;
pub mod error;
mod fileserver;
mod http_header;
//mod http_stream;
mod logger;
mod request;
mod responder;
mod response;
/// Entrypoint to the program.
fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Opens a file server on a specified address and port which serves all files in dir.
///
/// # Errors
/// Errors that come up while serving files. Look at [`Error`].
///
/// # Panics
/// Never. Added to allow compiler to check for ! type.
pub fn serve(addr: Ipv4Addr, port: u16, dir: PathBuf) -> Result<(), Error> {
/*
let args: ProgramArgs = std::env::args().try_into()?;
if args.generate {
match generate(&args.indir, &args.outdir, args.force) {
Ok(_) => log!(
Ok(()) => log!(
Level::Info,
"HTML generation from `{}` to `{}` successful",
args.indir.display(),
@@ -50,21 +53,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
log!(Level::Error, "HTML generation failed with error: {}", e,);
process::exit(1);
}
};
}
}
let listener = TcpListener::bind(&args.addr)?;
log!(Level::Info, "Listening on addr `{}`", &args.addr);
*/
let listener = TcpListener::bind((addr, port))?;
log!(Level::Info, "Listening on addr `{}:{}`", addr, port);
// todo: refactor this
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let outdir = args.outdir.to_owned();
let outdir = dir.clone();
std::thread::spawn(move || {
log!(Level::Debug, "TcpStream handler spawned");
let mut reader = BufReader::new(&stream);
let mut writer = BufWriter::new(&stream);
let server = match FileServer::new(&outdir) {
let server = match FileServer::new(outdir.as_path()) {
Ok(s) => s,
Err(_e) => return,
};
@@ -83,6 +88,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => log!(Level::Warn, "Connection failed: {}", e),
}
}
Ok(())
}

View File

@@ -1,39 +0,0 @@
use std::fmt::Display;
#[derive(Debug)]
#[allow(dead_code)]
pub enum Level {
Error,
Warn,
Info,
Debug,
}
impl Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}]\x1b[0m",
match self {
Self::Error => "\x1b[1;31m[ERROR",
Self::Warn => "\x1b[1;33m[WARN",
Self::Info => "\x1b[0;32m[INFO",
Self::Debug => "\x1b[0;36m[DEBUG",
}
)
}
}
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{
println!(
"{} {}:{}:{}: {}",
$level,
std::module_path!(),
std::file!(),
std::line!(),
format!($($arg)*)
);
}};
}

View File

@@ -2,8 +2,7 @@
use crate::error::{Error, ErrorKind, Result};
use crate::http_header::HttpHeaders;
use crate::log;
use crate::logger::Level;
use slogger::{Level, log};
use std::fmt::Display;
use std::io::{BufRead, BufReader};
use std::net::TcpStream;
@@ -32,7 +31,7 @@ impl TryFrom<&str> for HttpMethod {
impl Display for HttpMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
write!(f, "{self:?}")
}
}
@@ -111,6 +110,14 @@ impl TryFrom<&mut BufReader<&TcpStream>> for HttpRequest {
}
}
log!(
Level::Info,
"{} /{} {}",
req.method,
req.path.display(),
req.version
);
Ok(req)
}
}
@@ -154,7 +161,7 @@ impl TryFrom<&str> for HttpRequest {
for line in lines {
if let Some(v) = line.split_once(": ") {
headers.add(v.0, v.1)
headers.add(v.0, v.1);
}
}

View File

@@ -1,55 +1,12 @@
//! Traits helping HTTP connections
//! Helper trait(s).
use crate::request::HttpRequest;
use crate::response::HttpResponse;
/// Responder trait. Just a respond method that turns a HttpRequest to a HttpResponse.
/// Responder trait. Just a respond method that turns a [`HttpRequest`] to a [`HttpResponse`].
///
/// [`HttpRequest`]: ../request/struct.HttpRequest.html
/// [`HttpResponse`]: ../response/struct.HttpResponse.html
pub trait Responder {
fn respond(&self, req: HttpRequest) -> HttpResponse;
}
/*
/// Size trait. Number of bytes when encoded.
pub trait Size {
fn size(&self) -> usize;
}
// Standard implementations for Size trait
impl Size for u8 {
fn size(&self) -> usize {
1
}
}
impl<T> Size for Vec<T>
where
T: Size,
{
fn size(&self) -> usize {
if let Some(elem) = self.first() {
elem.size() * self.len()
} else {
0
}
}
}
impl<T> Size for Option<T>
where
T: Size,
{
fn size(&self) -> usize {
match self {
Some(t) => t.size(),
None => 0,
}
}
}
impl Size for String {
fn size(&self) -> usize {
self.len()
}
}
*/

View File

@@ -2,8 +2,7 @@
use crate::error::Result;
use crate::http_header::HttpHeaders;
use crate::log;
use crate::logger::Level;
use slogger::{Level, log};
use std::{fmt::Display, io::Write};
/// Macro for generating Http status codes (AI generated).
@@ -119,7 +118,9 @@ impl HttpResponse {
let _ = std::io::Read::read(stream, &mut [0u8; 1]);
*/
log!(Level::Info, "\n{}", &self);
// todo better verbose tracking
log!(Level::Info, "{} {}", self.version, self.status);
log!(Level::Debug, "\n{}", &self);
Ok(())
}
@@ -146,7 +147,7 @@ impl Display for HttpResponse {
write!(
f,
"{}\r\n",
String::from_utf8(s.to_vec()).unwrap_or("<binary data>".to_string())
String::from_utf8(s.clone()).unwrap_or("<binary data>".to_string())
)?;
}
Ok(())