Compare commits

..

2 Commits

Author SHA1 Message Date
446a27c040 refactor md parser, TODO: parse_str 2025-11-14 02:22:51 +02:00
d74613aa93 added gitea cargo test workflow 2025-11-09 21:16:54 +02:00
31 changed files with 309 additions and 750 deletions

View File

@@ -1,5 +0,0 @@
[registry]
default = "gitea"
[registries.gitea]
index = "sparse+https://git.jlux.dev/api/packages/scrac/cargo/"

View File

@@ -1,22 +1,23 @@
name: Test the running changes
name: Test
on:
push:
branches: [ "dev" ]
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test
runs-on: rust-latest
jobs:
build:
name: Cargo test
runs-on: ubuntu-latest
steps:
- run: apt-get update -y && apt-get install nodejs -y
- uses: actions/checkout@v4
- run: cargo build --verbose
- run: cargo clippy -- -D warnings
- run: cargo test --verbose
steps:
- name: Build
uses: actions/checkout@v4
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View File

@@ -1,32 +0,0 @@
name: Build and deploy Docker image
on:
push:
branches:
- master
env:
CARGO_TERM_COLOR: always
jobs:
deploy:
name: Deploy
runs-on: rust-latest
steps:
- run: apt-get update -y && apt-get install nodejs -y
- uses: actions/checkout@v4
- run: cargo build --release
- uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea-http.apps.svc.cluster.local:3000"]
http = true
insecure = true
- uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: "gitea-http.apps.svc.cluster.local:3000".....

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ web/
target/
result
.cargo/credentials.toml
Cargo.lock
flake.lock

22
Cargo.lock generated
View File

@@ -1,22 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cracked_md"
version = "0.1.0"
dependencies = [
"fstools",
]
[[package]]
name = "fstools"
version = "0.1.0"
[[package]]
name = "stdsrv"
version = "0.1.0"
dependencies = [
"cracked_md",
"fstools",
]

View File

@@ -1,9 +0,0 @@
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

@@ -1,132 +0,0 @@
# This is a configuration file for the bacon tool
#
# Complete help on configuration: https://dystroy.org/bacon/config/
#
# You may check the current default at
# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml
default_job = "check"
env.CARGO_TERM_COLOR = "always"
[jobs.check]
command = ["cargo", "check"]
need_stdout = false
[jobs.check-all]
command = ["cargo", "check", "--all-targets"]
need_stdout = false
# Run clippy on the default target
[jobs.clippy]
command = ["cargo", "clippy"]
need_stdout = false
# Run clippy on all targets
# To disable some lints, you may change the job this way:
# [jobs.clippy-all]
# command = [
# "cargo", "clippy",
# "--all-targets",
# "--",
# "-A", "clippy::bool_to_int_with_if",
# "-A", "clippy::collapsible_if",
# "-A", "clippy::derive_partial_eq_without_eq",
# ]
# need_stdout = false
[jobs.clippy-all]
command = ["cargo", "clippy", "--all-targets"]
need_stdout = false
# Run clippy in pedantic mode
# The 'dismiss' feature may come handy
[jobs.pedantic]
command = [
"cargo", "clippy",
"--",
"-W", "clippy::pedantic",
]
need_stdout = false
# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = [
"cargo", "nextest", "run",
"--hide-progress-bar",
"--failure-output", "final",
"--no-fail-fast"
]
need_stdout = true
analyzer = "nextest"
[jobs.nextest]
command = [
"cargo", "nextest", "run",
"--hide-progress-bar",
"--failure-output", "final",
]
need_stdout = true
analyzer = "nextest"
[jobs.doc]
command = ["cargo", "doc", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You can run your application and have the result displayed in bacon,
# if it makes sense for this crate.
[jobs.run]
command = [
"cargo", "run",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true
# Run your long-running application (eg server) and have the result displayed in bacon.
# For programs that never stop (eg a server), `background` is set to false
# to have the cargo run output immediately displayed instead of waiting for
# program's end.
# 'on_change_strategy' is set to `kill_then_restart` to have your program restart
# on every change (an alternative would be to use the 'F5' key manually in bacon).
# If you often use this job, it makes sense to override the 'r' key by adding
# a binding `r = job:run-long` at the end of this file .
# A custom kill command such as the one suggested below is frequently needed to kill
# long running programs (uncomment it if you need it)
[jobs.run-long]
command = [
"cargo", "run",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = false
on_change_strategy = "kill_then_restart"
# kill = ["pkill", "-TERM", "-P"]
# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
# bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--example"]
need_stdout = true
allow_warnings = true
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
p = "job:pedantic"

14
cracked_md/Cargo.lock generated
View File

@@ -1,14 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cracked_md"
version = "0.1.0"
dependencies = [
"fstools",
]
[[package]]
name = "fstools"
version = "0.1.0"

View File

@@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
fstools = { version = "0.1.0", path = "../fstools" }
fstools = { path = "../fstools" }

View File

@@ -1,10 +1,19 @@
//! Abstract syntax tree of "Markdown".
use crate::to_html::ToHtml;
#[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>),
@@ -20,6 +29,24 @@ pub enum 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>,
@@ -33,3 +60,167 @@ 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 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),
};
}
}

View File

@@ -1,8 +1,4 @@
//! A "Markdown" parser and HTML generator. Part of a static site generator `marksmith-rs`.
//! Not following any standards, only vibes.
#![deny(unused_imports)]
#![allow(clippy::needless_pass_by_value)]
use fstools::crawl_fs;
use parser::parse;
@@ -61,7 +57,6 @@ impl MdParseError {
}
*/
#[must_use]
pub fn set_line(self, line: usize) -> Self {
Self {
file: self.file,
@@ -72,7 +67,6 @@ impl MdParseError {
}
}
#[must_use]
pub fn set_file(self, file: PathBuf) -> Self {
Self {
file: Some(file),
@@ -104,9 +98,7 @@ impl std::error::Error for MdParseError {}
#[derive(Debug)]
pub enum Error {
InDirIsNotDir,
OutDirIsNotEmpty,
OutDirIsNotDir,
OutDirFileDeleteNotAllowed,
OutDirDirectoryInPlaceOfFile,
FileRead,
@@ -133,18 +125,7 @@ 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<()> {
if !indir.is_dir() {
Err(Error::InDirIsNotDir)?;
}
if !outdir.is_dir() {
Err(Error::OutDirIsNotDir)?;
}
let files = crawl_fs(indir);
for path in files {

View File

@@ -65,7 +65,7 @@ pub trait ParsePattern: Iterator + Clone {
}
*/
pub trait Parse: Iterator + Clone {
pub trait Parse: Iterator {
fn follows(&mut self, token: char) -> bool;
fn parse_token(&mut self, token: char) -> bool {
@@ -77,32 +77,23 @@ pub trait Parse: Iterator + Clone {
}
}
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
fn parse_str(&mut self, _tokens: &str) -> bool {
todo!()
}
}
impl Parse for std::iter::Peekable<std::str::Chars<'_>> {
fn follows(&mut self, token: char) -> bool {
self.peek().is_some_and(|c| c == &token)
self.peek().map(|c| c == &token).unwrap_or(false)
}
}
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)
self.peekable()
.peek()
.map(|&(_i, c)| c == token)
.unwrap_or(false)
}
}
@@ -117,60 +108,4 @@ mod test {
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,5 +1,3 @@
//! Parse "Markdown" to AST.
mod block;
mod inline;
@@ -7,15 +5,13 @@ use block::parse_blocks;
use crate::{MdParseError, ast::Document};
/// 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)]
mod test {
use crate::ast::*;
@@ -25,7 +21,7 @@ mod test {
fn only_paragraph() {
let md = "testing paragraph";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
Document {
@@ -40,7 +36,7 @@ mod test {
fn different_headers() {
let md = "# Header 1\n## Header 2";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
@@ -63,7 +59,7 @@ mod test {
fn inline_bold_and_italics() {
let md = "some *bold* and _italic_ text";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
@@ -83,7 +79,7 @@ mod test {
fn inline_code() {
let md = "run command `sudo rm -rf /`";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
@@ -100,7 +96,7 @@ mod test {
fn bold_header() {
let md = "# Header is *bold*";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
@@ -120,7 +116,7 @@ mod test {
fn anonymous_code_block() {
let md = "```\necho hello\n```";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,
@@ -137,7 +133,7 @@ mod test {
fn rust_code_block() {
let md = "```rust\nfn main() {\n\tprintln!(\"Hello world!\");\n}\n```";
let doc = parse(md).unwrap();
let doc = parse(md);
assert_eq!(
doc,

View File

@@ -10,11 +10,6 @@ pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
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('#') {
@@ -58,7 +53,6 @@ pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
};
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
@@ -69,31 +63,23 @@ pub fn parse_blocks(input: &str) -> Result<Vec<Block>, MdParseError> {
language: lang,
content: code,
});
successful = true;
break;
} else {
Err(MdParseError::from_line(
j + 1,
"```",
format!("```{}", remaining),
))?;
}
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)

View File

@@ -36,12 +36,11 @@ pub fn parse_inlines(input: &str) -> Result<Vec<Inline>, MdParseError> {
_ => {
let mut text = String::new();
text.push(c);
while let Some(&nc) = chars.peek() {
while let Some(nc) = chars.next() {
if matches!(nc, '*' | '_' | '`' | '[') {
break;
}
let c = chars.next().ok_or(MdParseError::new("a character", ""))?;
text.push(c);
text.push(nc);
}
inlines.push(Inline::Text(text));
}
@@ -56,7 +55,7 @@ fn collect_until<I: Iterator<Item = char>>(
end: char,
) -> Result<String, MdParseError> {
let mut s = String::new();
for c in chars.by_ref() {
while let Some(c) = chars.next() {
if c == end {
return Ok(s);
}
@@ -64,68 +63,3 @@ fn collect_until<I: Iterator<Item = char>>(
}
Err(MdParseError::new(end, ""))
}
#[cfg(test)]
mod test {
use crate::ast::Inline;
use super::parse_inlines;
#[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())
]
);
}
}

View File

@@ -1,198 +1,4 @@
//! 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),
};
}
}

View File

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

60
flake.lock generated
View File

@@ -1,60 +0,0 @@
{
"nodes": {
"flake-utlis": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1762596750,
"narHash": "sha256-rXXuz51Bq7DHBlfIjN7jO8Bu3du5TV+3DSADBX7/9YQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b6a8526db03f735b89dd5ff348f53f752e7ddc8e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utlis": "flake-utlis",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

7
fstools/Cargo.lock generated
View File

@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "fstools"
version = "0.1.0"

View File

@@ -2,14 +2,13 @@ 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);

View File

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

22
stdsrv/Cargo.lock generated
View File

@@ -1,22 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cracked_md"
version = "0.1.0"
dependencies = [
"fstools",
]
[[package]]
name = "fstools"
version = "0.1.0"
[[package]]
name = "stdsrv"
version = "0.1.0"
dependencies = [
"cracked_md",
"fstools",
]

View File

@@ -3,11 +3,7 @@ name = "stdsrv"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "gravel"
path = "src/main.rs"
# local dependencies
[dependencies]
cracked_md = { version = "0.1.0", path = "../cracked_md" }
fstools = { version = "0.1.0", path = "../fstools" }
cracked_md = { path = "../cracked_md" }
fstools = { path = "../fstools" }

View File

@@ -1,5 +1,3 @@
//! Simple and program specific command line argument parsing solution.
use std::path::PathBuf;
use crate::error::Error;

View File

@@ -1,11 +1,10 @@
//! A simple server implementation that just responds with the contents of the file requested in
//! the provided directory.
//! FileServer
use std::fs;
use std::path::PathBuf;
use crate::{
error::{Error, ErrorKind, Result},
error::*,
request::{HttpMethod, HttpRequest},
responder::Responder,
response::{HttpResponse, HttpStatus},
@@ -52,6 +51,7 @@ 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,38 +1,36 @@
//! 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 {
write!(f, "{k}: {v}\r\n")?;
for (k, v) in self._inner.iter() {
write!(f, "{}: {}\r\n", k, v)?;
}
Ok(())
}

View File

@@ -24,9 +24,6 @@ impl Display for Level {
}
}
/// A logging macro. Takes a [`Level`] and a formatted string.
///
/// [`Level`]: ./logger/enum.Level.html
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)*) => {{

View File

@@ -1,4 +1,6 @@
//! A simple web server with 0 dependencies (other than Rust's stdlib).
//! 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).
use std::{
@@ -28,10 +30,9 @@ mod response;
fn main() -> Result<(), Box<dyn std::error::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(),
@@ -49,7 +50,7 @@ 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)?;
@@ -58,7 +59,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
let outdir = args.outdir.clone();
let outdir = args.outdir.to_owned();
std::thread::spawn(move || {
log!(Level::Debug, "TcpStream handler spawned");
let mut reader = BufReader::new(&stream);

View File

@@ -32,7 +32,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)
}
}
@@ -154,7 +154,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,12 +1,55 @@
//! Helper trait(s).
//! Traits helping HTTP connections
use crate::request::HttpRequest;
use crate::response::HttpResponse;
/// Responder trait. Just a respond method that turns a [`HttpRequest`] to a [`HttpResponse`].
///
/// [`HttpRequest`]: ../request/struct.HttpRequest.html
/// [`HttpResponse`]: ../response/struct.HttpResponse.html
/// Responder trait. Just a respond method that turns a HttpRequest to a HttpResponse.
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

@@ -146,7 +146,7 @@ impl Display for HttpResponse {
write!(
f,
"{}\r\n",
String::from_utf8(s.clone()).unwrap_or("<binary data>".to_string())
String::from_utf8(s.to_vec()).unwrap_or("<binary data>".to_string())
)?;
}
Ok(())