seam

Symbolic-Expressions As Markup.
git clone git://git.knutsen.co/seam
Log | Files | Refs | README | LICENSE

commit 49f18b078a290fdb5f90664db2a523f51c603021
parent 98c103924fd3dd5f666cafaf7193d639de6d444d
Author: Demonstrandum <moi@knutsen.co>
Date:   Sun, 28 Jun 2020 12:18:20 +0100

Better error messages.

Diffstat:
Csrc/assemble/html.rs -> src/assemble/css.rs | 0
Msrc/assemble/html.rs | 63+++++++++++++++++++++++++++++++++++----------------------------
Msrc/assemble/mod.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/assemble/xml.rs | 61++++++++++++++++++++++++++++++++-----------------------------
Msrc/bin.rs | 43++++++++++++++++++++++++++++++++-----------
Msrc/parse/lexer.rs | 47+++++++++++++++++++++++++++++++++++------------
Msrc/parse/parser.rs | 31+++++++++++++++++++++++++++----
Msrc/parse/tokens.rs | 21+++++++++++++++++++++
Mtest.sex | 1-
9 files changed, 255 insertions(+), 87 deletions(-)

diff --git a/src/assemble/html.rs b/src/assemble/css.rs diff --git a/src/assemble/html.rs b/src/assemble/html.rs @@ -1,17 +1,19 @@ //! Assembles an expanded tree into valid HTML. -use super::Documentise; +use super::{GenerationError, MarkupDisplay}; use crate::parse::parser::{self, ParseNode, ParseTree}; -use std::fmt::{self, Display}; +use std::fmt; #[derive(Debug, Clone)] pub struct HTMLFormatter { - pub tree : ParseTree + pub tree : ParseTree, } impl HTMLFormatter { pub fn new(tree : ParseTree) -> Self { - Self { tree } + Self { + tree, + } } } @@ -24,11 +26,11 @@ pub const DEFAULT : &str = </body>\n\ </html>\n"; -impl Documentise for HTMLFormatter { - fn document(&self) -> String { +impl MarkupDisplay for HTMLFormatter { + fn document(&self) -> Result<String, GenerationError> { let mut doc = String::new(); if self.tree.is_empty() { - return String::from(DEFAULT); + return Ok(String::from(DEFAULT)); } let stripped = parser::strip(&self.tree, true); let mut current_node = stripped.get(0); @@ -96,7 +98,7 @@ impl Documentise for HTMLFormatter { } // Populate. - doc += &self.to_string(); + self.generate(&mut doc)?; doc += "<!-- Generated by SEAM, from symbolic-expressions \ into HTML. -->\n"; // Cloes all new tags. @@ -104,26 +106,14 @@ impl Documentise for HTMLFormatter { doc += "</body>\n"; } if !html_tag { - doc += "</html>"; + doc += "</html>\n"; } - doc + Ok(doc) } -} - - -// TODO: Convert special characters to HTML compatible ones. -// e.g. -// < => &lt; -// > => &gt; -// & => &amp; -// " => &quot; -// ! => &excl; -// etc. -/// Converting the tree to an HTML string. -impl Display for HTMLFormatter { - fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + fn generate(&self, f : &mut dyn fmt::Write) + -> Result<(), GenerationError> { let mut tree_iter = self.tree.iter().peekable(); while let Some(node) = tree_iter.next() { match node { @@ -161,7 +151,9 @@ impl Display for HTMLFormatter { if let Some(node) = rest[0].symbolic() { write!(f, " {}", node.value)?; } else { - // TODO: Make and send error (can only be symbolic). + return Err(GenerationError::new("HTML", + "Non-symbolic item in declaration", + &rest[0].site())); } rest = &rest[1..]; } @@ -175,17 +167,32 @@ impl Display for HTMLFormatter { rest = &rest[1..]; } else { // Error! Cannot be non atomic. + return Err(GenerationError::new("HTML", + "Attribute cannot contain non-atomic data.", + &(*attr.node).site())); } } write!(f, ">")?; let html_fmt = HTMLFormatter::new(rest.to_owned()); - write!(f, "{}", html_fmt)?; + html_fmt.generate(f)?; write!(f, "</{}>", tag)?; }, - _ => panic!("Uh {:?}", node), + _ => return Err(GenerationError::new("HTML", + "Unknown node encountered.", &node.site())) } } - write!(f, "") + Ok(()) } } + + +// TODO: Convert special characters to HTML compatible ones. +// e.g. +// < => &lt; +// > => &gt; +// & => &amp; +// " => &quot; +// ! => &excl; +// etc. + diff --git a/src/assemble/mod.rs b/src/assemble/mod.rs @@ -1,6 +1,77 @@ -pub trait Documentise { - fn document(&self) -> String; +use crate::parse::tokens::Site; +use std::{convert, fmt, error::Error}; + +use colored::*; + +#[derive(Debug, Clone)] +pub struct GenerationError { + pub markup : String, + pub message : String, + pub site : Site +} + +impl GenerationError { + pub fn new(ml : &str, msg : &str, site : &Site) -> Self { + Self { + markup: ml.to_owned(), + message: msg.to_owned(), + site: site.to_owned() + } + } + pub fn unknown(ml : &str) -> Self { + Self { + markup: ml.to_owned(), + message: String::from("Unknown generation error (bug)."), + site: Site::fake() + } + } +} + +impl fmt::Display for GenerationError { + fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", + format!("[{}] Error Generating {} {}", + "**".red().bold(), + self.markup.bold(), self.site).white(), + self.message) + } +} + +impl Error for GenerationError { } + +impl convert::From<fmt::Error> for GenerationError { + fn from(_ : fmt::Error) -> Self { + Self { + markup: String::from("Unknown"), + message: String::from( + "Unknown error while writing to format buffer"), + site: Site::fake() + } + } +} + +pub trait MarkupDisplay { + // Required definitions: + fn generate(&self, buf : &mut dyn fmt::Write) + -> Result<(), GenerationError>; + fn document(&self) -> Result<String, GenerationError>; + // Default definitions: + fn display(&self) -> Result<String, GenerationError> { + let mut s_buf = String::new(); + self.generate(&mut s_buf).map(|_| s_buf) + } } +impl fmt::Display for dyn MarkupDisplay { + fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + self.generate(f).map_err(|_| fmt::Error) + } +} +/// XML generation. pub mod xml; + +/// HTML5 CSS generation. +//pub mod css; +/// HTML5 HTML generation. pub mod html; + diff --git a/src/assemble/xml.rs b/src/assemble/xml.rs @@ -1,8 +1,8 @@ //! Assembles an expanded tree into valid XML. -use super::Documentise; +use super::{MarkupDisplay, GenerationError}; use crate::parse::parser::{self, ParseNode, ParseTree}; -use std::fmt::{self, Display}; +use std::fmt; #[derive(Debug, Clone)] pub struct XMLFormatter { @@ -14,12 +14,14 @@ impl XMLFormatter { Self { tree } } - fn show_attribute(&self, attr : &parser::AttributeNode) - -> Result<String, fmt::Error> { + fn display_attribute(&self, attr : &parser::AttributeNode) + -> Result<String, GenerationError> { if let Some(symbol) = (*attr.node).atomic() { Ok(format!("{}=\"{}\"", attr.keyword, symbol.value)) } else { - Err(fmt::Error) + Err(GenerationError::new("XML", + "Attribute can only contain symbols, numbers or strings", + &(*attr.node).site())) } } @@ -28,11 +30,11 @@ impl XMLFormatter { pub const DEFAULT : &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; -impl Documentise for XMLFormatter { - fn document(&self) -> String { +impl MarkupDisplay for XMLFormatter { + fn document(&self) -> Result<String, GenerationError> { let mut doc = String::new(); if self.tree.is_empty() { - return String::from(DEFAULT); + return Ok(String::from(DEFAULT)); } let stripped = parser::strip(&self.tree, true); let current_node = stripped.get(0); @@ -52,26 +54,14 @@ impl Documentise for XMLFormatter { } // Populate. - doc += &self.to_string(); + self.generate(&mut doc)?; doc += "<!-- Generated by SEAM, from symbolic-expressions \ into XML. -->\n"; - doc + Ok(doc) } -} - - -// TODO: Convert special characters to HTML compatible ones. -// e.g. -// < => &lt; -// > => &gt; -// & => &amp; -// " => &quot; -// ! => &excl; -// etc. -/// Converting the tree to an HTML string. -impl Display for XMLFormatter { - fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + fn generate(&self, f : &mut dyn fmt::Write) + -> Result<(), GenerationError> { let mut tree_iter = self.tree.iter().peekable(); while let Some(node) = tree_iter.next() { match node { @@ -113,7 +103,7 @@ impl Display for XMLFormatter { if let Some(node) = rest[0].symbolic() { write!(f, "{}", node.value)?; } else if let ParseNode::Attribute(a) = &rest[0] { - write!(f, " {}", self.show_attribute(a)?)?; + write!(f, " {}", self.display_attribute(a)?)?; } else { // Error. } @@ -129,18 +119,31 @@ impl Display for XMLFormatter { } while let Some(ParseNode::Attribute(attr)) = rest.first() { - write!(f, " {}", self.show_attribute(&attr)?)?; + write!(f, " {}", self.display_attribute(&attr)?)?; rest = &rest[1..]; } write!(f, ">")?; let xml_fmt = XMLFormatter::new(rest.to_owned()); - write!(f, "{}", xml_fmt)?; + xml_fmt.generate(f)?; write!(f, "</{}>", tag)?; }, - _ => panic!("Uh {:?}", node), + _ => return Err(GenerationError::new("XML", + "Unknonw node encountered.", &node.site())) } } - write!(f, "") + Ok(()) } } + + +// TODO: Convert special characters to HTML compatible ones. +// e.g. +// < => &lt; +// > => &gt; +// & => &amp; +// " => &quot; +// ! => &excl; +// etc. + + diff --git a/src/bin.rs b/src/bin.rs @@ -1,5 +1,5 @@ use seam; -use seam::assemble::Documentise; +use seam::assemble::MarkupDisplay; use std::env; use std::path::PathBuf; @@ -7,20 +7,16 @@ use std::error::Error; use colored::*; -fn argument_fatal(msg : &str) -> ! { +fn argument_fatal(msg : impl std::fmt::Display) -> ! { eprintln!("{} {}", format!("[{}]", "**".red()).white().bold(), - msg.bold()); + msg.to_string().bold()); std::process::exit(1) } const SUPPORTED_TARGETS : [&str; 2] = ["html", "xml"]; fn main() -> Result<(), Box<dyn Error>> { - let (major, minor, tiny) = seam::VERSION; - eprintln!("{}", format!("SEAM v{}.{}.{}", - major, minor, tiny).bold()); - let mut args = env::args(); args.next(); // Discard. @@ -33,10 +29,20 @@ fn main() -> Result<(), Box<dyn Error>> { target = Box::leak(opt.to_owned().into_boxed_str()); } continue; + } else if let Some(opt) = arg.split("-").nth(1) { + match opt { + "v" => { + let (major, minor, tiny) = seam::VERSION; + eprintln!("{}", format!("SEAM v{}.{}.{}", + major, minor, tiny).bold()); + std::process::exit(0); + }, + _ => argument_fatal( + format!("Unknown argument (`-{}').", opt)) + } } let path = PathBuf::from(&arg); if path.exists() { - eprintln!("Reading file `{}'.", &path.display()); files.push(path); } } @@ -49,7 +55,13 @@ fn main() -> Result<(), Box<dyn Error>> { } for file in files { - let tree = seam::parse_file(&file)?; + let tree = match seam::parse_file(&file) { + Ok(tree) => tree, + Err(e) => { + eprintln!("{}", e); + std::process::exit(1) + } + }; #[cfg(feature="debug")] eprintln!("{}", &tree .iter().fold(String::new(), @@ -63,9 +75,18 @@ fn main() -> Result<(), Box<dyn Error>> { let fmt = seam::assemble::xml::XMLFormatter::new(tree); fmt.document() }, - _ => continue + _ => { + argument_fatal( + format!("Target `{}', does not exist.", target)) + } }; - print!("{}", result); + match result { + Ok(generated) => print!("{}", generated), + Err(e) => { + eprintln!("{}", e); + std::process::exit(1) + } + } } diff --git a/src/parse/lexer.rs b/src/parse/lexer.rs @@ -8,8 +8,8 @@ pub struct LexError(tokens::Site, String); impl fmt::Display for LexError { fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[**] Lexical Error: `{}'.\nAt: {:#?}", - self.1, self.0) + write!(f, "[**] Lexical Error {}: {}", + self.0, self.1) } } @@ -42,6 +42,9 @@ fn character_kind(character : char, prev : Option<tokens::Kind>) } } +// TODO: Post-tokeniser parenthesis balancer, give +// nice and exact error messages. + pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) -> Result<TokenStream, LexError> { @@ -56,6 +59,7 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) let mut token_start : usize = 0; let mut current_kind = None; let mut old_kind = None; + let mut string_open = false; let mut escaped = false; while bytes < eof { @@ -71,20 +75,21 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) let character = current_byte as char; // Tripple quoted string: - if character == '"' && &string[bytes..bytes + 3] == "\"\"\"" { + if character == '"' + && string.get(bytes..bytes + 3) == Some("\"\"\"") { token_start = line_bytes; let start_line = lines; bytes += 3; line_bytes += 3; - while &string[bytes..bytes + 3] != "\"\"\"" { - if string[bytes..].is_empty() { - let mut site = tokens::Site::from_line( - lines, line_bytes, 1); - site.source = source - .map(|e| e.as_ref().display().to_string()); - return Err(LexError(site, - String::from("Unclosed tripple-quoted string."))); + + let mut found_end_quote = false; + + while let Some(quote) = string.get(bytes..bytes + 3) { + if quote == "\"\"\"" { + found_end_quote = true; + break; } + let c = string.as_bytes()[bytes]; if c == '\n' as u8 { lines += 1; @@ -94,6 +99,17 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) bytes += 1; line_bytes += 1; } + + if !found_end_quote { + let mut site = tokens::Site::from_line( + lines, line_bytes, 1); + site.source = source + .map(|e| e.as_ref().display().to_string()); + return Err(LexError(site, + String::from("Unclosed tripple-quoted string."))); + } + + bytes += 3; line_bytes += 3; current_kind = None; @@ -151,6 +167,7 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) && prev_kind != Some(tokens::Kind::String) && !escaped; if string_start { + string_open = true; current_kind = None; } @@ -181,6 +198,7 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) peek_kind = None; } else if peek_string { peek_kind = None; + string_open = false; } // If we're on a whitespace, and there's a bracket (or quote) ahead, @@ -254,6 +272,11 @@ pub fn lex<P: AsRef<Path>>(string : String, source : Option<P>) } escaped = false; } - + if string_open { + let mut site = tokens::Site::from_line(lines, line_bytes, 1); + site.source = source.map(|p| p.as_ref().display().to_string()); + return Err(LexError(site, + "Unclosed double-quoted string.".to_string())) + } Ok(tokens) } diff --git a/src/parse/parser.rs b/src/parse/parser.rs @@ -19,7 +19,8 @@ impl Node { #[derive(Debug, Clone)] pub struct AttributeNode { pub keyword : String, - pub node : Box<ParseNode> + pub node : Box<ParseNode>, + pub site : Site } #[derive(Debug, Clone)] @@ -47,6 +48,21 @@ impl ParseNode { _ => None } } + pub fn site(&self) -> Site { + match self { + Self::Symbol(node) + | Self::Number(node) + | Self::String(node) => node.site.to_owned(), + Self::List(list) => { + if let Some(head) = list.first() { + head.site() + } else { + panic!("No empty lists should be allowed.") + } + }, + Self::Attribute(attr) => attr.site.to_owned(), + } + } } pub type ParseTree = Vec<ParseNode>; @@ -56,8 +72,8 @@ pub struct ParseError(pub String, pub Site); impl fmt::Display for ParseError { fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[**] Parse Error: `{}',\nAt: {:#?}", - self.0, self.1) + write!(f, "[**] Parse Error {}: {}", + self.1, self.0) } } @@ -82,6 +98,7 @@ pub fn parse(tokens : &[Token]) match token.kind { Kind::LParen => { // Parse list. + let open_paren = token.site.clone(); let mut slice = &tokens[1..]; if slice.is_empty() { return Err(ParseError( @@ -117,6 +134,11 @@ pub fn parse(tokens : &[Token]) slice = left; } slice = &slice[1..]; // Ignore last r-paren. + if elements.is_empty() { + // Empty lists have an invisible empty symbol in them. + let node = Node::new("", &open_paren); + elements.push(ParseNode::Symbol(node)); + } Ok((ParseNode::List(elements), slice)) }, Kind::Keyword => { @@ -124,7 +146,8 @@ pub fn parse(tokens : &[Token]) let (node, mut slice) = parse(&tokens[1..])?; let attribute = AttributeNode { keyword: token.value[1..].to_owned(), - node: Box::new(node) + node: Box::new(node), + site: token.site.to_owned() }; // White space after attributes don't count. if let Some(next) = slice.first() { diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs @@ -1,3 +1,5 @@ +use std::fmt::{self, Display}; + #[derive(Debug, Clone)] pub struct Site { pub source : Option<String>, @@ -17,6 +19,15 @@ impl Site { } } + pub fn fake() -> Self { + Self { + source: None, + line: 0, + bytes_from_start: 0, + bytes_span: 0 + } + } + pub fn from_line(line : usize, bytes_from_start : usize, bytes_span : usize) -> Self { @@ -28,6 +39,16 @@ impl Site { } } +impl Display for Site { + fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "(")?; + if let Some(source) = &self.source { + write!(f, "`{}':", source)?; + } + write!(f, "{}:{})", self.line, self.bytes_from_start + 1) + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum Kind { LParen, diff --git a/test.sex b/test.sex @@ -13,4 +13,3 @@ :alt "Cute cat" :src "https://static.insider.com/image/5d24d6b921a861093e71fef3.jpg" :width 300))) -