generated from ElnuDev/rust-project
Get code blocks working
This commit is contained in:
parent
59f0f8258a
commit
f178baf1e6
4 changed files with 226 additions and 99 deletions
|
@ -1,3 +1,13 @@
|
||||||
"Bob sat on the bench."
|
"Bob sat on the bench."
|
||||||
"Bob" "Good morning!"
|
"Bob" "Good morning!"
|
||||||
eat "potato" True
|
"Bob" "I am in a block now!"
|
||||||
|
"Bob" "Isn't this cool?"
|
||||||
|
"Bob" "And now we're back in normal indentation."
|
||||||
|
"Bob" "We can even go multiple levels in!"
|
||||||
|
"Bob" "How far does the rabbit hole go?"
|
||||||
|
"Bob" "rabbit hole go?"
|
||||||
|
"Bob" "go?"
|
||||||
|
"Bob" "Not sure what came of me there."
|
||||||
|
"Bob" "I suppose I should eat a potato."
|
||||||
|
eat "potato" True
|
||||||
|
"Bob" "Yum!"
|
|
@ -81,7 +81,7 @@ impl App {
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut complete = false;
|
let mut complete = false;
|
||||||
for i in 0..len {
|
for i in 0..(len + 1) {
|
||||||
if *kill.lock().unwrap() {
|
if *kill.lock().unwrap() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
279
renrs/src/lib.rs
279
renrs/src/lib.rs
|
@ -1,3 +1,4 @@
|
||||||
|
use core::panic;
|
||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use pest::Parser;
|
use pest::Parser;
|
||||||
|
@ -8,8 +9,8 @@ use pest_derive::Parser;
|
||||||
struct RpyParser;
|
struct RpyParser;
|
||||||
|
|
||||||
// Raw script tokens
|
// Raw script tokens
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Token {
|
enum Token {
|
||||||
Keyword(String),
|
Keyword(String),
|
||||||
Str(String),
|
Str(String),
|
||||||
Array(Vec<Token>),
|
Array(Vec<Token>),
|
||||||
|
@ -31,6 +32,73 @@ impl Token {
|
||||||
|
|
||||||
use Token::*;
|
use Token::*;
|
||||||
|
|
||||||
|
// Parsing types
|
||||||
|
|
||||||
|
type Line = Vec<Token>;
|
||||||
|
|
||||||
|
// Indented command block
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Block<T> {
|
||||||
|
elements: Vec<BlockElement<T>>,
|
||||||
|
next: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Block<T> {
|
||||||
|
fn next(&mut self) -> Option<&T> {
|
||||||
|
let mut next = match self.next {
|
||||||
|
Some(next) => next,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
let mut result = None;
|
||||||
|
let count = self.elements.len();
|
||||||
|
for element in &mut self.elements[next..] {
|
||||||
|
match element {
|
||||||
|
BlockElement::Command(command) => {
|
||||||
|
result = Some(&*command);
|
||||||
|
next += 1;
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
BlockElement::Block(block) => {
|
||||||
|
match block.next() {
|
||||||
|
Some(command) => {
|
||||||
|
result = Some(command);
|
||||||
|
break;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
next += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.next = if count >= next {
|
||||||
|
Some(next)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for Block<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
elements: Vec::new(),
|
||||||
|
next: Some(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum BlockElement<T> {
|
||||||
|
Command(T),
|
||||||
|
Block(Block<T>),
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineBlock = Block<Line>;
|
||||||
|
type CommandBlock = Block<Command>;
|
||||||
|
type Script = CommandBlock;
|
||||||
|
|
||||||
// Parsed script commands
|
// Parsed script commands
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -55,76 +123,144 @@ pub enum Event {
|
||||||
Say { name: Option<String>, text: String },
|
Say { name: Option<String>, text: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Step 1 parsing
|
||||||
|
// converting from pest pairs to Token blocks
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
// Tokenize raw script string
|
// Tokenize raw script string
|
||||||
fn tokenize(script: &str) -> Vec<Vec<Token>> {
|
fn parse(script: &str) -> LineBlock {
|
||||||
let file = RpyParser::parse(Rule::file, script)
|
let file = RpyParser::parse(Rule::file, script)
|
||||||
.expect("unsuccessful parse")
|
.expect("unsuccessful parse")
|
||||||
.next()
|
.next()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// TODO: Init with capacity
|
parse_block(file)
|
||||||
let mut lines = Vec::new();
|
|
||||||
for line in file.into_inner() {
|
|
||||||
let mut tokens = Vec::new();
|
|
||||||
match line.as_rule() {
|
|
||||||
Rule::line => {
|
|
||||||
for token in line.into_inner() {
|
|
||||||
tokens.push(parse_pair(token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Rule::EOI => (),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
// TODO: For some a blank final line is always parsed
|
|
||||||
if tokens.len() > 0 {
|
|
||||||
lines.push(tokens);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse raw pest data into Token
|
type Pair<'a> = pest::iterators::Pair<'a, Rule>;
|
||||||
fn parse_pair(pair: pest::iterators::Pair<Rule>) -> Token {
|
|
||||||
|
// Tokenize block
|
||||||
|
fn parse_block(pair: Pair) -> LineBlock {
|
||||||
|
let mut block = LineBlock::default();
|
||||||
|
for element in pair.into_inner() {
|
||||||
|
block.elements.push(match element.as_rule() {
|
||||||
|
Rule::block => BlockElement::Block(parse_block(element)),
|
||||||
|
Rule::line => {
|
||||||
|
let line = parse_line(element);
|
||||||
|
// TODO: For some reason a blank final line is always parsed
|
||||||
|
if line.len() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BlockElement::Command(line)
|
||||||
|
},
|
||||||
|
Rule::EOI => break, // end
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
block
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize line
|
||||||
|
fn parse_line(pair: Pair) -> Line {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
for token in pair.into_inner() {
|
||||||
|
tokens.push(parse_token(token));
|
||||||
|
}
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize token
|
||||||
|
fn parse_token(pair: Pair) -> Token {
|
||||||
let token = pair.as_rule();
|
let token = pair.as_rule();
|
||||||
|
macro_rules! contents {
|
||||||
|
() => {
|
||||||
|
pair.into_inner().next().unwrap()
|
||||||
|
};
|
||||||
|
}
|
||||||
match token {
|
match token {
|
||||||
Rule::token => {}
|
|
||||||
_ => panic!("Not a token!"),
|
|
||||||
};
|
|
||||||
let contents = pair.into_inner().next().unwrap();
|
|
||||||
let contents_rule = contents.as_rule();
|
|
||||||
match contents_rule {
|
|
||||||
Rule::string => {
|
Rule::string => {
|
||||||
let data = contents.into_inner().next().unwrap();
|
let contents = contents!();
|
||||||
Token::Str(match data.as_rule() {
|
Token::Str(match contents.as_rule() {
|
||||||
Rule::single_quote_string_data => data.as_str().replace("\\'", "'"),
|
Rule::single_quote_string_data => contents.as_str().replace("\\'", "'"),
|
||||||
Rule::double_quote_string_data => data.as_str().replace("\\\"", "\""),
|
Rule::double_quote_string_data => contents.as_str().replace("\\\"", "\""),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Rule::array => {
|
Rule::array => {
|
||||||
|
let contents = contents!();
|
||||||
let mut array = Vec::new();
|
let mut array = Vec::new();
|
||||||
for token in contents.into_inner() {
|
for token in contents.into_inner() {
|
||||||
array.push(parse_pair(token));
|
array.push(parse_token(token));
|
||||||
}
|
}
|
||||||
Token::Array(array)
|
Token::Array(array)
|
||||||
}
|
}
|
||||||
Rule::boolean => Token::Boolean(match contents.as_str() {
|
Rule::boolean => Token::Boolean(match pair.as_str() {
|
||||||
"True" => true,
|
"True" => true,
|
||||||
"False" => false,
|
"False" => false,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}),
|
}),
|
||||||
Rule::number => Token::Number(contents.as_str().parse().unwrap()),
|
Rule::number => Token::Number(pair.as_str().parse().unwrap()),
|
||||||
Rule::keyword => Token::Keyword(contents.as_str().to_owned()),
|
Rule::keyword => Token::Keyword(pair.as_str().to_owned()),
|
||||||
__ => unreachable!(),
|
__ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenize file
|
|
||||||
fn tokenize_file(file_path: PathBuf) -> Vec<Vec<Token>> {
|
// ==============================================
|
||||||
let unparsed_file = fs::read_to_string(file_path).expect("cannot find file");
|
// Step 2 reading
|
||||||
tokenize(&unparsed_file)
|
// converting from Token blocks to Command blocks
|
||||||
|
// ==============================================
|
||||||
|
|
||||||
|
// Read file into commands
|
||||||
|
fn read_file(file_path: &PathBuf) -> CommandBlock {
|
||||||
|
let line_block = {
|
||||||
|
let unparsed_file = fs::read_to_string(file_path).expect("cannot find file");
|
||||||
|
parse(&unparsed_file)
|
||||||
|
};
|
||||||
|
read_block(&line_block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn describe_line(line: &Vec<Token>) -> String {
|
// Read line block into command block
|
||||||
|
fn read_block(block: &LineBlock) -> CommandBlock {
|
||||||
|
CommandBlock {
|
||||||
|
elements: block.elements.iter().map(|element| match element {
|
||||||
|
BlockElement::Command(line) => BlockElement::Command(read_command(&line)),
|
||||||
|
BlockElement::Block(block) => BlockElement::Block(read_block(&block)),
|
||||||
|
}).collect(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read token array to command
|
||||||
|
fn read_command(line: &Line) -> Command {
|
||||||
|
macro_rules! unknown {
|
||||||
|
() => {
|
||||||
|
panic!("Unknown command {}", describe_line(&line))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match line.as_slice() {
|
||||||
|
[Str(text)] => Say {
|
||||||
|
name: None,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
[Str(name), Str(text)] => Say {
|
||||||
|
name: Some(name.to_owned()),
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
[Keyword(keyword), Str(food), tail @ ..] if keyword.eq("eat") => Eat {
|
||||||
|
food: food.to_owned(),
|
||||||
|
politely: match tail {
|
||||||
|
[Boolean(politely)] => *politely,
|
||||||
|
_ => unknown!(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => unknown!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line description e.g. [String, Keyword, Array]
|
||||||
|
// Used in parse_command as feedback for invalid commands
|
||||||
|
fn describe_line(line: &Line) -> String {
|
||||||
let mut description = "[".to_owned();
|
let mut description = "[".to_owned();
|
||||||
let mut iter = line.iter();
|
let mut iter = line.iter();
|
||||||
description.push_str(&format!("{}", iter.next().unwrap().print()));
|
description.push_str(&format!("{}", iter.next().unwrap().print()));
|
||||||
|
@ -135,50 +271,18 @@ fn describe_line(line: &Vec<Token>) -> String {
|
||||||
description
|
description
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse file into commands
|
// =====
|
||||||
fn parse_file(file_path: PathBuf) -> Vec<Command> {
|
// State
|
||||||
let token_lines = tokenize_file(file_path);
|
// =====
|
||||||
let mut commands = Vec::new();
|
|
||||||
for line in token_lines {
|
|
||||||
macro_rules! unknown {
|
|
||||||
() => {
|
|
||||||
panic!("Unknown command {}", describe_line(&line))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
commands.push(match line.as_slice() {
|
|
||||||
[Str(text)] => Say {
|
|
||||||
name: None,
|
|
||||||
text: text.to_owned(),
|
|
||||||
},
|
|
||||||
[Str(name), Str(text)] => Say {
|
|
||||||
name: Some(name.to_owned()),
|
|
||||||
text: text.to_owned(),
|
|
||||||
},
|
|
||||||
[Keyword(keyword), Str(food), tail @ ..] if keyword.eq("eat") => Eat {
|
|
||||||
food: food.to_owned(),
|
|
||||||
politely: match tail {
|
|
||||||
[Boolean(politely)] => *politely,
|
|
||||||
_ => unknown!(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_ => unknown!(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
commands
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
command_queue: Vec<Command>,
|
script: Script,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub fn from_file(file: PathBuf) -> State {
|
pub fn from_file(file: PathBuf) -> State {
|
||||||
State {
|
State {
|
||||||
command_queue: {
|
script: read_file(&file),
|
||||||
let mut q = parse_file(file);
|
|
||||||
q.reverse();
|
|
||||||
q
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,19 +290,18 @@ impl State {
|
||||||
while let Some(command) = self.next_command() {
|
while let Some(command) = self.next_command() {
|
||||||
if command.is_blocking() {
|
if command.is_blocking() {
|
||||||
return Some(match command {
|
return Some(match command {
|
||||||
Say { name, text } => Event::Say { name, text },
|
Say { name, text } => Event::Say {
|
||||||
|
name: name.clone(),
|
||||||
|
text: text.clone()
|
||||||
|
},
|
||||||
_ => unimplemented!(),
|
_ => unimplemented!(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_command(&mut self) -> Option<Command> {
|
fn next_command(&mut self) -> Option<&Command> {
|
||||||
if self.command_queue.len() == 0 {
|
self.script.next()
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(self.command_queue.remove(self.command_queue.len() - 1))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
// underscores mark are silent rules, are ignored
|
|
||||||
WHITESPACE = _{ " " }
|
|
||||||
|
|
||||||
// characters are anything but newlines
|
// characters are anything but newlines
|
||||||
char = { !NEWLINE ~ ANY }
|
char = { !NEWLINE ~ ANY }
|
||||||
|
|
||||||
|
@ -12,7 +9,7 @@ token = { string | array | boolean | number | keyword }
|
||||||
|
|
||||||
// KEYWORDS
|
// KEYWORDS
|
||||||
// has to be atomic for no implicit separate (spaces)
|
// has to be atomic for no implicit separate (spaces)
|
||||||
keyword = @{ (!(WHITESPACE | NEWLINE) ~ ANY)+ }
|
keyword = ${ (!(whitespace | NEWLINE) ~ ANY)+ }
|
||||||
|
|
||||||
// STRING
|
// STRING
|
||||||
single_quote_string_data = @{ (
|
single_quote_string_data = @{ (
|
||||||
|
@ -29,13 +26,13 @@ string = ${
|
||||||
}
|
}
|
||||||
|
|
||||||
// ARRAY
|
// ARRAY
|
||||||
array = {
|
array = ${
|
||||||
"[" ~ "]"
|
"[" ~ "]"
|
||||||
| "[" ~ NEWLINE* ~ token ~ ("," ~ NEWLINE* ~ token)* ~ NEWLINE* ~ "]"
|
| "[" ~ whitespace* ~ NEWLINE* ~ whitespace* ~ token ~ ("," ~ whitespace* ~ NEWLINE* ~ whitespace* ~ token)* ~ NEWLINE* ~ "]"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOOLEAN
|
// BOOLEAN
|
||||||
boolean = { "True" | "False" }
|
boolean = ${ "True" | "False" }
|
||||||
|
|
||||||
// NUMBER
|
// NUMBER
|
||||||
number = @{
|
number = @{
|
||||||
|
@ -49,6 +46,23 @@ number = @{
|
||||||
COMMENT = _{ "#" ~ char* }
|
COMMENT = _{ "#" ~ char* }
|
||||||
|
|
||||||
// lines are comprised of a statement
|
// lines are comprised of a statement
|
||||||
line = { token+ }
|
line = @{ (token ~ whitespace+)* ~ token }
|
||||||
|
|
||||||
file = { SOI ~ (line ~ (NEWLINE+ ~ line)*)? ~ NEWLINE* ~ EOI }
|
file = { SOI ~ NEWLINE* ~ block_content* ~ NEWLINE* ~ EOI }
|
||||||
|
|
||||||
|
block = {
|
||||||
|
// The first line in the block
|
||||||
|
PEEK_ALL ~ PUSH(" "+ | "\t"+) ~ block_content ~
|
||||||
|
// Subsequent lines in the block
|
||||||
|
(PEEK_ALL ~ block_content)* ~
|
||||||
|
// Remove the last layer of indentation from the stack when exiting the block
|
||||||
|
DROP
|
||||||
|
}
|
||||||
|
|
||||||
|
// can't be called WHITESPACE
|
||||||
|
// as this would mess up NewBlock
|
||||||
|
whitespace = _{ " " }
|
||||||
|
|
||||||
|
block_content = _{
|
||||||
|
line ~ (whitespace+ ~ line)* ~ (NEWLINE | EOI) ~ block*
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue