pub mod command_parser;
pub mod decode_buf_reader;
pub mod error;
pub mod input;
pub mod traceback;
use super::command::Command;
pub use error::{ErrorInfo, ParseError, ParseResult, ParserLineSource};
pub use input::{BufReadWrapper, FileInputSource, StringInputSource, TextInputSource};
use nom::Offset;
pub use traceback::TracebackEntry;
use input::Input;
use traceback::NomErrorNode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParserConfig {
pub command_threshold: usize,
pub skip_annotations: bool,
pub convert_number_command: bool,
pub preserve_indent: bool,
pub preserve_empty_lines: bool,
}
impl Default for ParserConfig {
fn default() -> Self {
Self {
command_threshold: 1,
skip_annotations: false,
convert_number_command: true,
preserve_indent: false,
preserve_empty_lines: false,
}
}
}
impl ParserConfig {
pub fn new(
threshold: usize,
skip_annotations: bool,
convert_number_command: bool,
preserve_indent: bool,
preserve_empty_lines: bool,
) -> Self {
Self {
command_threshold: threshold,
skip_annotations,
convert_number_command,
preserve_indent,
preserve_empty_lines,
}
}
pub fn with_command_threshold(mut self, threshold: usize) -> Self {
self.command_threshold = threshold;
self
}
pub fn with_skip_annotations(mut self, skip: bool) -> Self {
self.skip_annotations = skip;
self
}
pub fn with_convert_number_command(mut self, convert: bool) -> Self {
self.convert_number_command = convert;
self
}
pub fn with_preserve_indent(mut self, preserve: bool) -> Self {
self.preserve_indent = preserve;
self
}
pub fn with_preserve_empty_lines(mut self, preserve: bool) -> Self {
self.preserve_empty_lines = preserve;
self
}
}
pub struct Parser<T: TextInputSource> {
input: Input<T>,
config: ParserConfig,
}
impl<T: TextInputSource> Parser<T> {
pub fn new(input_source: T, config: ParserConfig) -> Self {
Self {
input: Input::new(input_source),
config,
}
}
pub fn next_command(&mut self) -> ParseResult<Option<Command>> {
self.next_command_with_source()
.map(|opt| opt.map(|(cmd, _)| cmd))
}
pub fn next_command_with_source(&mut self) -> ParseResult<Option<(Command, ParserLineSource)>> {
loop {
let (lineno, line_text) = match self.input.next_line() {
Ok(Some(line_info)) => line_info,
Ok(None) => {
return Ok(None);
}
Err(e) => {
let source = ParserLineSource {
filename: self.input.as_ref().source_name().to_string(),
lineno: self.input.line_number,
text: String::new(),
};
return Err(ParseError::io(e).with_line_source(source));
}
};
let source = ParserLineSource {
filename: self.input.as_ref().source_name().to_string(),
lineno,
text: line_text.clone(),
};
let trimmed = line_text.trim();
if trimmed.is_empty() {
if self.config.preserve_empty_lines {
break Ok(Some((Command::new_text(""), source)));
}
continue;
}
let hash_count = trimmed.chars().take_while(|&c| c == '#').count();
if hash_count < self.config.command_threshold {
let text_content = if self.config.preserve_indent {
line_text.trim_end().to_string()
} else {
trimmed.to_string()
};
break Ok(Some((Command::new_text(text_content), source)));
} else if hash_count > self.config.command_threshold {
if self.config.skip_annotations {
continue;
}
let annotation_content = if self.config.preserve_indent {
line_text.trim_end().to_string()
} else {
let content: String = trimmed.chars().skip(hash_count).collect();
content.trim().to_string()
};
break Ok(Some((Command::new_annotation(annotation_content), source)));
} else {
let column = line_text.offset(trimmed) + hash_count;
let command_str: String = trimmed.chars().skip(hash_count).collect();
break self
.parse_command_line(command_str, lineno, column)
.map_err(|e| e.with_line_source(source.clone()))
.map(|opt| opt.map(|cmd| (cmd, source)));
}
}
}
pub fn parse_command_line(
&self,
command_text: String,
lineno: usize,
column: usize,
) -> ParseResult<Option<Command>> {
if command_text.is_empty() {
return Err(ParseError::syntax_with_context(
"Empty command line".to_string(),
lineno,
column,
command_text,
));
}
let result = command_parser::parse_command_line::<NomErrorNode<&str>>(&command_text);
match result {
Ok(("", command)) => {
let num_name = command.name().parse();
match num_name {
Result::Err(_) => Ok(Some(command)),
Result::Ok(num) => {
if !self.config.convert_number_command {
Ok(Some(command))
} else {
Ok(Some(Command::new_number(num, command.params)))
}
}
}
}
Ok((remaining, _)) => Err(ParseError::unexpected_input(
remaining.to_string(),
lineno,
column,
command_text,
)),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => {
Err(ParseError::from_nom_error(
"Command parsing error".to_string(),
command_text.as_str(),
lineno,
column,
e,
))
}
Err(nom::Err::Incomplete(_)) => {
Err(ParseError::unexpected_eof(command_text, lineno, column))
}
}
}
pub fn process_with<F, E>(&mut self, mut handler: F) -> Result<bool, E>
where
F: FnMut(Command) -> Result<bool, E>,
E: From<Box<ParseError>>,
{
loop {
match self.next_command() {
Ok(Some(command)) => {
let should_continue = handler(command)?;
if !should_continue {
return Ok(false); }
}
Ok(None) => {
return Ok(true); } Err(e) => {
return Err(e.into());
} }
}
}
pub fn current_line(&self) -> usize {
self.input.line_number
}
}
impl<T: TextInputSource> AsRef<T> for Parser<T> {
fn as_ref(&self) -> &T {
&self.input.source
}
}
impl<T: TextInputSource> AsMut<T> for Parser<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.input.source
}
}
impl<T: TextInputSource> Iterator for Parser<T> {
type Item = ParseResult<Command>;
fn next(&mut self) -> Option<Self::Item> {
match self.next_command() {
Ok(Some(cmd)) => Some(Ok(cmd)),
Ok(None) => None,
Err(e) => Some(Err(e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::error::ParseError;
#[test]
fn test_parser_config() {
let config = ParserConfig::default();
assert_eq!(config.command_threshold, 1);
assert!(!config.skip_annotations);
assert!(config.convert_number_command);
assert!(!config.preserve_indent);
assert!(!config.preserve_empty_lines);
let config = ParserConfig::new(2, true, false, true, true);
assert_eq!(config.command_threshold, 2);
assert!(config.skip_annotations);
assert!(!config.convert_number_command);
assert!(config.preserve_indent);
assert!(config.preserve_empty_lines);
let config = ParserConfig::default()
.with_command_threshold(3)
.with_skip_annotations(true)
.with_convert_number_command(false)
.with_preserve_indent(true)
.with_preserve_empty_lines(true);
assert_eq!(config.command_threshold, 3);
assert!(config.skip_annotations);
assert!(!config.convert_number_command);
assert!(config.preserve_indent);
assert!(config.preserve_empty_lines);
}
#[test]
fn test_preserve_indent() {
let input = StringInputSource::new(" indented text\nnormal text");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let cmd = parser.next_command().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(cmd.params()[0].to_string(), "\"indented text\"");
let input = StringInputSource::new(" indented text\nnormal text");
let config = ParserConfig::default().with_preserve_indent(true);
let mut parser = Parser::new(input, config);
let cmd = parser.next_command().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(cmd.params()[0].to_string(), "\" indented text\"");
}
#[test]
fn test_preserve_empty_lines() {
let input = StringInputSource::new("text1\n\ntext2");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let cmd1 = parser.next_command().unwrap().unwrap();
assert_eq!(cmd1.name(), "@text");
assert_eq!(cmd1.params()[0].to_string(), "text1");
let cmd2 = parser.next_command().unwrap().unwrap();
assert_eq!(cmd2.name(), "@text");
assert_eq!(cmd2.params()[0].to_string(), "text2");
let input = StringInputSource::new("text1\n\ntext2");
let config = ParserConfig::default().with_preserve_empty_lines(true);
let mut parser = Parser::new(input, config);
let cmd1 = parser.next_command().unwrap().unwrap();
assert_eq!(cmd1.name(), "@text");
assert_eq!(cmd1.params()[0].to_string(), "text1");
let cmd_empty = parser.next_command().unwrap().unwrap();
assert_eq!(cmd_empty.name(), "@text");
assert_eq!(cmd_empty.params()[0].to_string(), "\"\"");
let cmd2 = parser.next_command().unwrap().unwrap();
assert_eq!(cmd2.name(), "@text");
assert_eq!(cmd2.params()[0].to_string(), "text2");
}
#[test]
fn test_preserve_indent_annotation() {
let input = StringInputSource::new("## annotation text");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let cmd = parser.next_command().unwrap().unwrap();
assert_eq!(cmd.name(), "@annotation");
assert_eq!(cmd.params()[0].to_string(), "\"annotation text\"");
let input = StringInputSource::new("## annotation text");
let config = ParserConfig::default().with_preserve_indent(true);
let mut parser = Parser::new(input, config);
let cmd = parser.next_command().unwrap().unwrap();
assert_eq!(cmd.name(), "@annotation");
assert_eq!(cmd.params()[0].to_string(), "\"## annotation text\"");
}
#[test]
fn test_parser_process_with() {
let input = StringInputSource::new("#cmd1\n#cmd2");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let mut commands = Vec::new();
let result: Result<bool, Box<ParseError>> = parser.process_with(|cmd| {
commands.push(cmd.name().to_string());
Ok(true)
});
assert!(result.is_ok());
assert!(result.unwrap()); assert_eq!(commands, vec!["cmd1", "cmd2"]);
}
#[test]
fn test_parser_process_with_early_stop() {
let input = StringInputSource::new("#cmd1\n#cmd2");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let mut commands = Vec::new();
let result: Result<bool, Box<ParseError>> = parser.process_with(|cmd| {
commands.push(cmd.name().to_string());
Ok(false) });
assert!(result.is_ok());
assert!(!result.unwrap()); assert_eq!(commands, vec!["cmd1"]);
let next = parser.next_command().unwrap();
assert!(next.is_some());
assert_eq!(next.unwrap().name(), "cmd2");
}
#[test]
fn test_parser_current_line() {
let input = StringInputSource::new("#cmd1\n#cmd2");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
assert_eq!(parser.current_line(), 1);
parser.next_command().unwrap();
assert_eq!(parser.current_line(), 2);
}
#[test]
fn test_next_command_with_source_command() {
let input = StringInputSource::new("#name \"Test\"\n#draw Line");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "name");
assert_eq!(source.filename, "<string>");
assert_eq!(source.lineno, 1);
assert_eq!(source.text, "#name \"Test\"\n");
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "draw");
assert_eq!(source.lineno, 2);
assert_eq!(source.text, "#draw Line");
}
#[test]
fn test_next_command_with_source_text() {
let input = StringInputSource::new("Hello World\nAnother line");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(source.filename, "<string>");
assert_eq!(source.lineno, 1);
assert_eq!(source.text, "Hello World\n");
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(source.lineno, 2);
assert_eq!(source.text, "Another line");
}
#[test]
fn test_next_command_with_source_annotation() {
let input = StringInputSource::new("## annotation text\n## another");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@annotation");
assert_eq!(source.filename, "<string>");
assert_eq!(source.lineno, 1);
assert_eq!(source.text, "## annotation text\n");
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@annotation");
assert_eq!(source.lineno, 2);
assert_eq!(source.text, "## another");
}
#[test]
fn test_next_command_with_source_eof() {
let input = StringInputSource::new("#cmd");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let result = parser.next_command_with_source().unwrap();
assert!(result.is_some());
let result = parser.next_command_with_source().unwrap();
assert!(result.is_none());
}
#[test]
fn test_next_command_with_source_error() {
let input = StringInputSource::new("#");
let config = ParserConfig::default();
let mut parser = Parser::new(input, config);
let result = parser.next_command_with_source();
assert!(result.is_err());
}
#[test]
fn test_next_command_with_source_preserve_empty_lines() {
let input = StringInputSource::new("text1\n\ntext2");
let config = ParserConfig::default().with_preserve_empty_lines(true);
let mut parser = Parser::new(input, config);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(source.lineno, 1);
assert_eq!(source.text, "text1\n");
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(source.lineno, 2);
assert_eq!(source.text, "\n");
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "@text");
assert_eq!(source.lineno, 3);
assert_eq!(source.text, "text2");
}
#[test]
fn test_next_command_with_source_skip_annotations() {
let input = StringInputSource::new("#cmd1\n##annotation\n#cmd2");
let config = ParserConfig::default().with_skip_annotations(true);
let mut parser = Parser::new(input, config);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "cmd1");
assert_eq!(source.lineno, 1);
let (cmd, source) = parser.next_command_with_source().unwrap().unwrap();
assert_eq!(cmd.name(), "cmd2");
assert_eq!(source.lineno, 3);
}
}