From 4a11fd2bd6329af21f8a3c60d70afc973f6518a4 Mon Sep 17 00:00:00 2001 From: Stanisław Zagórowski Date: Fri, 12 Jan 2024 20:28:42 +0100 Subject: enough to run my website --- src/error.rs | 2 - src/luax/default_html_tags.txt | 127 +++++++++++++++++++++++++++++++++++++++++ src/luax/lexer.rs | 79 ++++++++++++++----------- src/luax/mod.rs | 36 ++++++++---- src/luax/preprocessor.rs | 97 ++++++++++++++++++++++++++----- src/luax/tests/preprocessor.rs | 79 ++++++++++++++++++++++++- src/luax/tokens.rs | 6 +- src/main.rs | 23 +++++++- src/server.rs | 99 +++++++++++++++++++++++++------- 9 files changed, 458 insertions(+), 90 deletions(-) create mode 100644 src/luax/default_html_tags.txt diff --git a/src/error.rs b/src/error.rs index 51803c7..bf44a21 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,7 +3,6 @@ use std::fmt::{self, Display, Formatter}; #[derive(Debug, Clone, Eq, PartialEq)] pub enum LuaXError { InvalidStart, // used internally - MissingField(String), NonTableChildren, NonTableAttrs, UnexpectedCharacter(char), @@ -19,7 +18,6 @@ impl Display for LuaXError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { LuaXError::InvalidStart => write!(f, "Invalid start"), - LuaXError::MissingField(field) => write!(f, "Missing required field: {}", field), LuaXError::NonTableChildren => write!(f, "Children must be tables"), LuaXError::NonTableAttrs => write!(f, "Attrs must be tables"), LuaXError::UnexpectedCharacter(c) => write!(f, "Unexpected character: '{}'", c), diff --git a/src/luax/default_html_tags.txt b/src/luax/default_html_tags.txt new file mode 100644 index 0000000..5d84cad --- /dev/null +++ b/src/luax/default_html_tags.txt @@ -0,0 +1,127 @@ +!--...-- +!DOCTYPE +a +abbr +acronym +address +applet +area +article +aside +audio +b +base +basefont +bdi +bdo +big +blockquote +body +br +button +canvas +caption +center +cite +code +col +colgroup +data +datalist +dd +del +details +dfn +dialog +dir +div +dl +dt +em +embed +fieldset +figcaption +figure +font +footer +form +frame +frameset +h1 +h2 +h3 +h4 +h5 +h6 +head +header +hgroup +hr +html +i +iframe +img +input +ins +kbd +label +legend +li +link +main +map +mark +menu +meta +meter +nav +noframes +noscript +object +ol +optgroup +option +output +p +param +picture +pre +progress +q +rp +rt +ruby +s +samp +script +search +section +select +small +source +span +strike +strong +style +sub +summary +sup +svg +table +tbody +td +template +textarea +tfoot +th +thead +time +title +tr +track +tt +u +ul +var +video +wbr diff --git a/src/luax/lexer.rs b/src/luax/lexer.rs index 7d4f20a..bae4003 100644 --- a/src/luax/lexer.rs +++ b/src/luax/lexer.rs @@ -37,8 +37,7 @@ pub struct Lexer<'s> { current_pos_in_bytes: usize, // EOF emitted_eof: bool, - allow_unknowns: bool, - emit_whitespace: bool, + html_text_mode: usize, } #[derive(Debug)] @@ -61,29 +60,32 @@ impl<'s> Lexer<'s> { current, current_pos_in_bytes: 0, emitted_eof: false, - allow_unknowns: false, - emit_whitespace: false, + html_text_mode: 0, } } pub fn next_token(&mut self) -> Result>> { - let token = self.lex(); - - match token { - TokenizeResult::Some(token) => Ok(Some(token)), - TokenizeResult::Error(error) => Err(error.into()), - TokenizeResult::None => match self.current { - Some(c) => { - if self.allow_unknowns { - self.advance(); - match c { - ' ' | '\t' | '\n' => Ok(Some(Token::Whitespace)), - _ => Ok(Some(Token::Unknown(c))), - } + if self.html_text_mode > 0 { + let c = self.current; + self.advance(); + + match c { + Some('<') => { + if self.match_char('/') { + Ok(Some(Token::OpenClosingTag)) } else { - Err(LuaXError::UnexpectedCharacter(c).into()) + Ok(Some(Token::Lt)) } } + Some(' ' | '\t' | '\n') => Ok(Some(Token::Whitespace)), + Some('{') => { + if self.match_char('$') { + Ok(Some(Token::LuaStart)) + } else { + Ok(Some(Token::HtmlTextChar('{'))) + } + } + Some(c) => Ok(Some(Token::HtmlTextChar(c))), None => { if self.emitted_eof { Ok(None) @@ -92,30 +94,38 @@ impl<'s> Lexer<'s> { Ok(Some(Token::Eof)) } } - }, + } + } else { + let token = self.lex(); + + match token { + TokenizeResult::Some(token) => Ok(Some(token)), + TokenizeResult::Error(error) => Err(error.into()), + TokenizeResult::None => match self.current { + Some(c) => Err(LuaXError::UnexpectedCharacter(c).into()), + None => { + if self.emitted_eof { + Ok(None) + } else { + self.emitted_eof = true; + Ok(Some(Token::Eof)) + } + } + }, + } } } - pub fn allow_unknowns(&mut self) { - self.allow_unknowns = true; + pub fn enable_html_text_mode(&mut self) { + self.html_text_mode += 1; } - pub fn disallow_unknowns(&mut self) { - self.allow_unknowns = false; - } - - pub fn emit_whitespace(&mut self) { - self.emit_whitespace = true; - } - - pub fn hide_whitespace(&mut self) { - self.emit_whitespace = false; + pub fn disable_html_text_mode(&mut self) { + self.html_text_mode -= 1; } fn lex(&mut self) -> TokenizeResult<'s> { - if !self.emit_whitespace { - self.skip_whitespace(); - } + self.skip_whitespace(); try_all_paths!( self.single_char_token(), @@ -181,6 +191,7 @@ impl<'s> Lexer<'s> { self.single_char_token_case(']', Token::CloseBracket), self.single_char_token_case(';', Token::Semicolon), self.single_char_token_case(',', Token::Comma), + self.single_char_token_case('!', Token::Bang), ) } diff --git a/src/luax/mod.rs b/src/luax/mod.rs index 1a3db12..ddbb2ef 100644 --- a/src/luax/mod.rs +++ b/src/luax/mod.rs @@ -9,13 +9,22 @@ mod tests; mod tokens; pub fn table_to_html(table: rlua::Table, f: &mut W) -> Result<()> { - let type_name: Option = table.get("tag").unwrap(); + let tag_name: Option = table.get("tag").unwrap(); + + if tag_name.is_none() { + // we might be in a list + for child in table.sequence_values::() { + match child? { + rlua::Value::Table(child) => table_to_html(child, f)?, + rlua::Value::String(s) => write!(f, "{}", s.to_str()?)?, + _ => return Err(LuaXError::NonTableChildren.into()), + } + } - if type_name.is_none() { - return Err(LuaXError::MissingField("tag".to_string()).into()); + return Ok(()); } - let type_name = type_name.unwrap(); + let type_name = tag_name.unwrap(); write!(f, "<{}", type_name)?; let mut children = None; @@ -69,7 +78,13 @@ pub fn preprocess(s: &str) -> Result { let mut buf = Vec::new(); let preprocessor = preprocessor::Preprocessor::new(s, &mut buf)?; - preprocessor.preprocess()?; + match preprocessor.preprocess() { + Ok(_) => {} + Err(e) => { + println!("got up to: {}", String::from_utf8_lossy(&buf)); + return Err(e); + } + } let s = String::from_utf8(buf).unwrap(); @@ -221,7 +236,6 @@ mod utils { param_name.push(pattern_char.unwrap()); pattern_char = pattern.next(); } - println!("param_name: {}", param_name); pattern_char = pattern.next(); let mut param_value = String::new(); while path_char != Some('/') && path_char.is_some() { @@ -295,16 +309,12 @@ mod utils { vec![("name", "a"), ("name2", "c")], ), ("/{name}/b/{name2}", "/a/b", vec![("name", "a")]), - ( - "/{name}/b/{name2}", - "/a/b/c/d/e", - vec![("name", "a"), ("name2", "c")], - ), ]; let lua = Lua::new(); - for (pattern, path, expected) in cases { + for (pattern, path, mut expected) in cases { + expected.sort_by_key(|(k, _)| k.to_owned()); lua.context(|ctx| { let res = super::url_extract(ctx, (pattern.to_string(), path.to_string())).unwrap(); @@ -313,6 +323,8 @@ mod utils { let (key, value) = pair.unwrap(); res_vec.push((key, value)); } + res_vec.sort_by_key(|(k, _)| k.clone()); + let equal = res_vec .iter() .zip(expected.iter()) diff --git a/src/luax/preprocessor.rs b/src/luax/preprocessor.rs index ba51a44..cd5d735 100644 --- a/src/luax/preprocessor.rs +++ b/src/luax/preprocessor.rs @@ -1,4 +1,5 @@ use std::io::Write; +use std::sync::OnceLock; use crate::error::LuaXError; @@ -799,37 +800,90 @@ impl<'s, W: Write> Preprocessor<'s, W> { } fn html_identifier(&mut self) -> Result { - if let Token::Identifier(s) = self.current { + if let Token::Bang = self.current { + self.next_token_silent()?; + let mut s = "!".to_string(); + if let Token::Identifier(s2) = self.current { + s.push_str(s2); + self.next_token_silent()?; + } + Ok(s.to_string()) + } else if let Token::Identifier(s) = self.current { self.next_token_silent()?; + let mut s = s.to_string(); + // we might have - in the identifier, so we loop until we don't + let mut last_was_id = true; + loop { + if let Token::Minus = self.current { + s.push('-'); + self.next_token_silent()?; + last_was_id = false; + } else if let Token::Identifier(s2) = self.current { + if last_was_id { + break; + } + s.push_str(s2); + self.next_token_silent()?; + last_was_id = true; + } else { + break; + } + } Ok(s.to_string()) } else { Err(LuaXError::InvalidStart.into()) } } + fn default_html_tag(tag: &str) -> bool { + static TAGS: OnceLock> = OnceLock::new(); + + let tags = TAGS.get_or_init(|| include_str!("default_html_tags.txt").split('\n').collect()); + + tags.contains(&tag) + } + fn html_template(&mut self) -> Result<()> { if !self.match_token_silent(Token::Lt)? { return Err(LuaXError::InvalidStart.into()); } - self.lexer.allow_unknowns(); - let tag = require!( self.html_identifier(), LuaXError::NeededToken("identifier".to_string()) ); - write!(self.out_stream, " {{ tag=\"{}\", ", tag)?; + if Self::default_html_tag(&tag) { + write!(self.out_stream, " {{ tag=\"{}\", ", tag)?; + } else { + write!(self.out_stream, " {} ({{ ", tag)?; + } self.html_attributes()?; if self.match_token_silent(Token::Slash)? { self.consume_token_silent(Token::Gt, LuaXError::NeededToken(Token::Gt.to_string()))?; - write!(self.out_stream, "children={{}} }}")?; + write!(self.out_stream, "children={{}}")?; + if Self::default_html_tag(&tag) { + write!(self.out_stream, " }}")?; + } else { + write!(self.out_stream, " }}) ")?; + } return Ok(()); } - self.consume_token_silent(Token::Gt, LuaXError::NeededToken(Token::Gt.to_string()))?; + // there might be an invalid token immediately after the tag name + match self.consume_token_silent(Token::Gt, LuaXError::NeededToken(Token::Gt.to_string())) { + Ok(()) => {} + Err(e) => match e.downcast_ref::() { + Some(LuaXError::UnexpectedCharacter(_)) => { + self.lexer.enable_html_text_mode(); + self.next_token_silent()?; + self.lexer.disable_html_text_mode(); + }, + _ => return Err(e), + }, + } self.html_children()?; @@ -849,9 +903,11 @@ impl<'s, W: Write> Preprocessor<'s, W> { self.consume_token_silent(Token::Gt, LuaXError::NeededToken(Token::Gt.to_string()))?; - write!(self.out_stream, " }}")?; - - self.lexer.disallow_unknowns(); + if Self::default_html_tag(&tag) { + write!(self.out_stream, " }}")?; + } else { + write!(self.out_stream, " }}) ")?; + } Ok(()) } @@ -869,7 +925,11 @@ impl<'s, W: Write> Preprocessor<'s, W> { self.consume_token_silent(Token::Eq, LuaXError::NeededToken(Token::Eq.to_string()))?; - write!(self.out_stream, "{}=", key)?; + if key.contains('-') { + write!(self.out_stream, "[\"{}\"]=", key)?; + } else { + write!(self.out_stream, "{}=", key)?; + } if self.match_token_silent(Token::OpenBrace)? { require!(self.expression(), LuaXError::ExpectedExpression); @@ -900,9 +960,7 @@ impl<'s, W: Write> Preprocessor<'s, W> { break; } if self.match_token_silent(Token::LuaStart)? { - self.lexer.disallow_unknowns(); require!(self.expression(), LuaXError::ExpectedExpression); - self.lexer.allow_unknowns(); self.consume_token_silent( Token::LuaEnd, LuaXError::NeededToken(Token::LuaEnd.to_string()), @@ -911,7 +969,16 @@ impl<'s, W: Write> Preprocessor<'s, W> { continue; } - if optionally!(self.html_template()).is_some() { + if match self.html_template() { + Ok(t) => Some(t), + Err(e) => match e.downcast_ref::() { + Some(LuaXError::InvalidStart) => None, + Some(LuaXError::UnexpectedCharacter(_)) => None, + _ => return Err(e), + }, + } + .is_some() + { write!(self.out_stream, ",")?; continue; } @@ -926,7 +993,7 @@ impl<'s, W: Write> Preprocessor<'s, W> { // handle plain HTML text, which can really be anything. Needs to become // a string literal write!(self.out_stream, " \"")?; - self.lexer.emit_whitespace(); + self.lexer.enable_html_text_mode(); loop { if self.current == Token::Lt || self.current == Token::LuaStart @@ -938,7 +1005,7 @@ impl<'s, W: Write> Preprocessor<'s, W> { write!(self.out_stream, "{}", self.current)?; self.next_token_silent()?; } - self.lexer.hide_whitespace(); + self.lexer.disable_html_text_mode(); write!(self.out_stream, "\",")?; } write!(self.out_stream, "}}")?; diff --git a/src/luax/tests/preprocessor.rs b/src/luax/tests/preprocessor.rs index d5c2f42..6769288 100644 --- a/src/luax/tests/preprocessor.rs +++ b/src/luax/tests/preprocessor.rs @@ -34,7 +34,7 @@ fn compare_output(input: &str, expected: &str) -> Result<()> { .unwrap(); println!( "first mismatch: index {}: {:?} != {:?}", - first_mismatch.0, first_mismatch.1.0, first_mismatch.1.1 + first_mismatch.0, first_mismatch.1 .0, first_mismatch.1 .1 ); println!("expected: {}", expected); println!("output: {}", output); @@ -196,3 +196,80 @@ fn site() -> Result<()> { "#, ) } + +#[test] +fn html_with_code() -> Result<()> { + compare_output( + "return
{$ hello $}
", + "return { tag=\"div\", attrs={}, children={ hello,} }", + ) +} + +#[test] +fn html_with_code_and_text() -> Result<()> { + compare_output( + "return
hello {$ world $}
", + "return { tag=\"div\", attrs={}, children={ \"hello \", world,} }", + ) +} + +#[test] +fn function_returning_html() -> Result<()> { + compare_output( + "function hello() return
end", + "function hello ( ) return { tag=\"div\", attrs={}, children={} } end", + ) +} + +#[test] +fn function_returning_nested_html() -> Result<()> { + compare_output( + "function hello() return
end", + "function hello ( ) return { tag=\"div\", attrs={}, children={ { tag=\"span\", attrs={}, children={} },} } end", + ) +} + +#[test] +fn function_returning_nested_html_with_code() -> Result<()> { + compare_output( + "function hello() return
{$ world $}
end", + "function hello ( ) return { tag=\"div\", attrs={}, children={ world,} } end", + ) +} + +#[test] +fn component() -> Result<()> { + compare_output("return ", "return Hello ({ attrs={}, children={} })") +} + +#[test] +fn component_with_attrs() -> Result<()> { + compare_output( + "return ", + "return Hello ({ attrs={name=\"world\", }, children={} })", + ) +} + +#[test] +fn component_with_children() -> Result<()> { + compare_output( + "return ", + "return Hello ({ attrs={}, children={ { tag=\"span\", attrs={}, children={} },} })", + ) +} + +#[test] +fn html_with_attrs_with_dash() -> Result<()> { + compare_output( + "return
", + "return { tag=\"div\", attrs={[\"data-hello\"]=\"world\", }, children={} }", + ) +} + +#[test] +fn weird_symbols_in_html() -> Result<()> { + compare_output( + "return
@everyone
", + "return { tag=\"div\", attrs={}, children={ \"@everyone\",} }", + ) +} diff --git a/src/luax/tokens.rs b/src/luax/tokens.rs index 2c9448d..9c23acd 100644 --- a/src/luax/tokens.rs +++ b/src/luax/tokens.rs @@ -71,11 +71,12 @@ pub enum Token<'s> { Identifier(&'s str), // HTML extensions + Bang, OpenClosingTag, LuaStart, LuaEnd, Eof, - Unknown(char), + HtmlTextChar(char), Whitespace, } @@ -148,8 +149,9 @@ impl std::fmt::Display for Token<'_> { Token::LuaStart => write!(f, "{{$"), Token::LuaEnd => write!(f, "$}}"), + Token::Bang => write!(f, "!"), Token::Eof => write!(f, "EOF"), - Token::Unknown(c) => write!(f, "{}", c), + Token::HtmlTextChar(c) => write!(f, "{}", c), Token::Whitespace => write!(f, " "), } } diff --git a/src/main.rs b/src/main.rs index 66949ab..29f2c88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ mod server; struct Args { #[clap(short = 'C', long = "current-dir", default_value = ".")] current_dir: std::path::PathBuf, + #[clap(short = 'P', long = "public-dir", default_value = "public")] + public_dir: std::path::PathBuf, #[clap(short = 'p', long = "port", default_value = "4310")] port: u16, #[clap(short = 'l', long = "local", default_value = "false")] @@ -20,7 +22,7 @@ struct Args { #[tokio::main] async fn main() -> color_eyre::Result<()> { color_eyre::install().unwrap(); - let args = Args::parse(); + let mut args = Args::parse(); if !args.current_dir.is_dir() { return Err(std::io::Error::new( @@ -35,6 +37,21 @@ async fn main() -> color_eyre::Result<()> { args.current_dir.display().bright_yellow() ); + if !args.public_dir.is_dir() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("{} is not a directory", args.public_dir.display()), + ) + .into()); + } + + println!( + "🌴 Public directory: {}", + args.public_dir.display().bright_yellow() + ); + + args.public_dir = args.public_dir.canonicalize()?; + if args.local { local(args).await } else { @@ -72,7 +89,7 @@ async fn temp(args: Args) -> Result<()> { ); println!( - "⏲️ Will serve from {}", + "⏲️ Will serve Lua from {}", tmp_dir.path().display().bright_blue() ); @@ -119,7 +136,7 @@ async fn serve(args: Args) -> Result<()> { println!("📦 Building Lua state..."); let lua = luax::prepare_lua()?; println!("🛫 Starting server..."); - server::Server::serve(lua, args.port).await + server::Server::serve(lua, args.port, args.public_dir).await } fn recurse_copy(from: &Path, to: &Path) -> Result { diff --git a/src/server.rs b/src/server.rs index 7e27a39..05f7f26 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,7 @@ use std::future::Future; +use std::io::Write; use std::net::SocketAddr; +use std::path::PathBuf; use std::pin::Pin; use std::sync::{Arc, Mutex}; @@ -18,19 +20,22 @@ use rlua::Lua; pub struct Server { port: u16, - lua: Arc>, + state: State, } +#[derive(Clone)] struct State { lua: Arc>, + public_dir: PathBuf, } impl Server { - pub async fn serve(lua: Lua, port: u16) -> Result<()> { - let server = Self { - port, + pub async fn serve(lua: Lua, port: u16, public_dir: PathBuf) -> Result<()> { + let state = State { lua: Arc::new(Mutex::new(lua)), + public_dir, }; + let server = Self { port, state }; server.start().await } @@ -38,14 +43,16 @@ impl Server { let addr = SocketAddr::from(([127, 0, 0, 1], self.port)); let listener = TcpListener::bind(addr).await?; + let state = self.state; + loop { let (stream, _) = listener.accept().await?; let io = TokioIo::new(stream); - let state = State { - lua: self.lua.clone(), - }; + let http = http1::Builder::new(); + let state = state.clone(); + tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new().serve_connection(io, state).await { + if let Err(err) = http.serve_connection(io, state).await { println!("Failed to serve connection: {:?}", err); } }); @@ -59,6 +66,28 @@ fn mk_response(status: StatusCode, s: String) -> Result>> { .body(Full::new(Bytes::from(s)))?) } +fn mk_file_response(path: PathBuf) -> Result>> { + let ext = path.extension().unwrap().to_str().unwrap(); + + let mime = match ext { + "css" => "text/css", + "js" => "text/javascript", + "html" => "text/html", + "png" => "image/png", + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg+xml", + _ => "text/plain", + }; + + let bytes = std::fs::read(path)?; + + Ok(Response::builder() + .header("Content-Type", mime) + .body(Full::new(Bytes::from(bytes)))?) +} + impl Service> for State { type Response = Response>; type Error = color_eyre::Report; @@ -67,39 +96,67 @@ impl Service> for State { fn call(&self, req: Request) -> Self::Future { let path = req.uri().path(); - let res = self - .serve(path) - .map(|(st, s)| mk_response(st, s)) - .unwrap_or_else(|err| { - println!("Error: {}", err); - Err(err) - }); + let res = self.serve(path); Box::pin(async { res }) } } impl State { - fn serve(&self, path: &str) -> Result<(StatusCode, String)> { + fn serve(&self, path: &str) -> Result>> { let lua = self.lua.lock().unwrap(); - let res = lua.context(|ctx| -> Result<(StatusCode, String)> { - let manifest: rlua::Table = ctx.load("require('reluax')").eval()?; + let res = lua.context(|ctx| -> Result>> { + let manifest: rlua::Result = ctx.load("require('reluax')").eval(); + + let manifest = match manifest { + Ok(m) => m, + Err(e) => { + eprintln!("Internal lua error: {}", e); + + return mk_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal lua error".to_string(), + ); + } + }; let route: rlua::Function = manifest.get("route")?; - let res: (rlua::Integer, rlua::Value) = route.call((path,))?; + let res: rlua::Result<(rlua::Integer, rlua::Value)> = route.call((path,)); + + let res = match res { + Ok(r) => r, + Err(e) => { + eprintln!("Internal lua error: {}", e); + + return mk_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal lua error".to_string(), + ); + } + }; let status = StatusCode::from_u16(res.0 as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + if status == StatusCode::NOT_FOUND { + // try to serve a static file + let path = self.public_dir.join(path.trim_start_matches('/')); + + if path.is_file() { + return mk_file_response(path); + } + } + match res.1 { - rlua::Value::String(s) => Ok((status, s.to_str()?.to_string())), + rlua::Value::String(s) => mk_response(status, s.to_str()?.to_string()), rlua::Value::Table(t) => { let mut buf = Vec::new(); + writeln!(&mut buf, "")?; table_to_html(t, &mut buf)?; let s = String::from_utf8(buf).unwrap(); - Ok((status, s)) + mk_response(status, s) } rlua::Value::Nil => Err(ReluaxError::Server("No route found".to_string()).into()), rlua::Value::Error(e) => Err(ReluaxError::Lua(e).into()), -- cgit v1.3.1