use crate::lex::lex;
use crate::SyntaxKind;
use crate::SyntaxKind::*;
use crate::DEFAULT_VERSION;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ParseError(Vec<String>);
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for err in &self.0 {
writeln!(f, "{}", err)?;
}
Ok(())
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Lang {}
impl rowan::Language for Lang {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.into()
}
}
use rowan::GreenNode;
use rowan::GreenNodeBuilder;
struct Parse {
green_node: GreenNode,
#[allow(unused)]
errors: Vec<String>,
}
fn parse(text: &str) -> Parse {
struct Parser {
tokens: Vec<(SyntaxKind, String)>,
builder: GreenNodeBuilder<'static>,
errors: Vec<String>,
}
impl Parser {
fn parse_version(&mut self) -> Option<i32> {
let mut version = None;
if self.tokens.last() == Some(&(KEY, "version".to_string())) {
self.builder.start_node(VERSION.into());
self.bump();
self.skip_ws();
if self.current() != Some(EQUALS) {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
if self.current() != Some(VALUE) {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected value, got {:?}", self.current()));
self.bump();
self.builder.finish_node();
} else {
let version_str = self.tokens.last().unwrap().1.clone();
match version_str.parse() {
Ok(v) => {
version = Some(v);
self.bump();
}
Err(_) => {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("invalid version: {}", version_str));
self.bump();
self.builder.finish_node();
}
}
}
if self.current() != Some(NEWLINE) {
self.builder.start_node(ERROR.into());
self.errors.push("expected newline".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
self.builder.finish_node();
}
version
}
fn parse_watch_entry(&mut self) -> bool {
self.skip_ws();
if self.current().is_none() {
return false;
}
if self.current() == Some(NEWLINE) {
self.bump();
return false;
}
self.builder.start_node(ENTRY.into());
self.parse_options_list();
for i in 1..3 {
if self.current() == Some(NEWLINE) {
break;
}
if self.current() == Some(CONTINUATION) {
self.bump();
self.skip_ws();
continue;
}
if self.current() != Some(VALUE) && self.current() != Some(KEY) {
self.builder.start_node(ERROR.into());
self.errors.push(format!(
"expected value, got {:?} (i={})",
self.current(),
i
));
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
self.skip_ws();
}
if self.current() != Some(NEWLINE) {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected newline, not {:?}", self.current()));
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
self.builder.finish_node();
true
}
fn parse_option(&mut self) -> bool {
if self.current().is_none() {
return false;
}
if self.current() == Some(WHITESPACE) {
return false;
}
self.builder.start_node(OPTION.into());
if self.current() != Some(KEY) {
self.builder.start_node(ERROR.into());
self.errors.push("expected key".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
if self.current() != Some(EQUALS) {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
if self.current() != Some(VALUE) && self.current() != Some(KEY) {
self.builder.start_node(ERROR.into());
self.errors
.push(format!("expected value, got {:?}", self.current()));
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
self.builder.finish_node();
true
}
fn parse_options_list(&mut self) {
self.skip_ws();
if self.tokens.last() == Some(&(KEY, "opts".to_string()))
|| self.tokens.last() == Some(&(KEY, "options".to_string()))
{
self.builder.start_node(OPTS_LIST.into());
self.bump();
self.skip_ws();
if self.current() != Some(EQUALS) {
self.builder.start_node(ERROR.into());
self.errors.push("expected `=`".to_string());
self.bump();
self.builder.finish_node();
} else {
self.bump();
}
loop {
if !self.parse_option() {
break;
}
}
self.builder.finish_node();
self.skip_ws();
}
}
fn parse(mut self) -> Parse {
let mut version = 1;
self.builder.start_node(ROOT.into());
if let Some(v) = self.parse_version() {
version = v;
}
loop {
if !self.parse_watch_entry() {
break;
}
}
self.skip_ws();
self.builder.finish_node();
Parse {
green_node: self.builder.finish(),
errors: self.errors,
}
}
fn bump(&mut self) {
let (kind, text) = self.tokens.pop().unwrap();
self.builder.token(kind.into(), text.as_str());
}
fn current(&self) -> Option<SyntaxKind> {
self.tokens.last().map(|(kind, _)| *kind)
}
fn skip_ws(&mut self) {
while self.current() == Some(WHITESPACE)
|| self.current() == Some(CONTINUATION)
|| self.current() == Some(COMMENT)
{
self.bump()
}
}
}
let mut tokens = lex(text);
tokens.reverse();
Parser {
tokens,
builder: GreenNodeBuilder::new(),
errors: Vec::new(),
}
.parse()
}
type SyntaxNode = rowan::SyntaxNode<Lang>;
#[allow(unused)]
type SyntaxToken = rowan::SyntaxToken<Lang>;
#[allow(unused)]
type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
impl Parse {
fn syntax(&self) -> SyntaxNode {
SyntaxNode::new_root(self.green_node.clone())
}
fn root(&self) -> WatchFile {
WatchFile::cast(self.syntax()).unwrap()
}
}
macro_rules! ast_node {
($ast:ident, $kind:ident) => {
#[derive(PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct $ast(SyntaxNode);
impl $ast {
#[allow(unused)]
fn cast(node: SyntaxNode) -> Option<Self> {
if node.kind() == $kind {
Some(Self(node))
} else {
None
}
}
}
impl ToString for $ast {
fn to_string(&self) -> String {
self.0.text().to_string()
}
}
};
}
ast_node!(WatchFile, ROOT);
ast_node!(Version, VERSION);
ast_node!(Entry, ENTRY);
ast_node!(OptionList, OPTS_LIST);
ast_node!(_Option, OPTION);
impl WatchFile {
pub fn new(version: Option<u32>) -> WatchFile {
let mut builder = GreenNodeBuilder::new();
builder.start_node(ROOT.into());
if let Some(version) = version {
builder.start_node(VERSION.into());
builder.token(KEY.into(), "version");
builder.token(EQUALS.into(), "=");
builder.token(VALUE.into(), version.to_string().as_str());
builder.token(NEWLINE.into(), "\n");
builder.finish_node();
}
builder.finish_node();
WatchFile(SyntaxNode::new_root(builder.finish()))
}
pub fn version(&self) -> u32 {
self.0
.children()
.find_map(Version::cast)
.map(|it| it.version())
.unwrap_or(DEFAULT_VERSION)
}
pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
self.0.children().filter_map(Entry::cast)
}
}
impl FromStr for WatchFile {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parsed = parse(s);
if parsed.errors.is_empty() {
Ok(parsed.root())
} else {
Err(ParseError(parsed.errors))
}
}
}
impl Version {
pub fn version(&self) -> u32 {
self.0
.children_with_tokens()
.find_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE {
Some(token.text().parse().unwrap())
} else {
None
}
}
_ => None,
})
.unwrap_or(DEFAULT_VERSION)
}
}
impl Entry {
pub fn option_list(&self) -> Option<OptionList> {
self.0.children().find_map(OptionList::cast)
}
pub fn opts(&self) -> std::collections::HashMap<String, String> {
let mut options = std::collections::HashMap::new();
if let Some(ol) = self.option_list() {
for opt in ol.children() {
let key = opt.key();
let value = opt.value();
if let (Some(key), Some(value)) = (key, value) {
options.insert(key.to_string(), value.to_string());
}
}
}
options
}
fn items(&self) -> impl Iterator<Item = String> + '_ {
self.0.children_with_tokens().filter_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE || token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
_ => None,
})
}
pub fn url(&self) -> String {
self.items().next().unwrap()
}
pub fn matching_pattern(&self) -> Option<String> {
self.items().nth(1)
}
pub fn version(&self) -> Option<String> {
self.items().nth(2)
}
pub fn script(&self) -> Option<String> {
self.items().nth(3)
}
}
impl OptionList {
fn children(&self) -> impl Iterator<Item = _Option> + '_ {
self.0.children().filter_map(_Option::cast)
}
}
impl _Option {
pub fn key(&self) -> Option<String> {
self.0.children_with_tokens().find_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
_ => None,
})
}
pub fn value(&self) -> Option<String> {
self.0
.children_with_tokens()
.filter_map(|it| match it {
SyntaxElement::Token(token) => {
if token.kind() == VALUE || token.kind() == KEY {
Some(token.text().to_string())
} else {
None
}
}
_ => None,
})
.nth(1)
}
}
#[test]
fn test_parse_v1() {
const WATCHV1: &str = r#"version=4
opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/syncthing-gtk-$1\.tar\.gz/ \
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
"#;
let parsed = parse(WATCHV1);
assert_eq!(parsed.errors, Vec::<String>::new());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r#"ROOT@0..156
VERSION@0..10
KEY@0..7 "version"
EQUALS@7..8 "="
VALUE@8..9 "4"
NEWLINE@9..10 "\n"
ENTRY@10..156
OPTS_LIST@10..81
KEY@10..14 "opts"
EQUALS@14..15 "="
OPTION@15..81
KEY@15..29 "filenamemangle"
EQUALS@29..30 "="
VALUE@30..81 "s/.+\\/v?(\\d\\S+)\\.tar\\ ..."
WHITESPACE@81..82 " "
CONTINUATION@82..84 "\\\n"
WHITESPACE@84..86 " "
VALUE@86..133 "https://github.com/sy ..."
WHITESPACE@133..134 " "
VALUE@134..155 ".*/v?(\\d\\S+)\\.tar\\.gz"
NEWLINE@155..156 "\n"
"#
);
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
assert_eq!(
entry.matching_pattern(),
Some(".*/v?(\\d\\S+)\\.tar\\.gz".into())
);
assert_eq!(entry.version(), None);
assert_eq!(entry.script(), None);
assert_eq!(node.text(), WATCHV1);
}
#[test]
fn test_parse_v2() {
let parsed = parse(
r#"version=4
https://github.com/syncthing/syncthing-gtk/tags .*/v?(\d\S+)\.tar\.gz
# comment
"#,
);
assert_eq!(parsed.errors, Vec::<String>::new());
let node = parsed.syntax();
assert_eq!(
format!("{:#?}", node),
r###"ROOT@0..90
VERSION@0..10
KEY@0..7 "version"
EQUALS@7..8 "="
VALUE@8..9 "4"
NEWLINE@9..10 "\n"
ENTRY@10..80
VALUE@10..57 "https://github.com/sy ..."
WHITESPACE@57..58 " "
VALUE@58..79 ".*/v?(\\d\\S+)\\.tar\\.gz"
NEWLINE@79..80 "\n"
COMMENT@80..89 "# comment"
NEWLINE@89..90 "\n"
"###
);
let root = parsed.root();
assert_eq!(root.version(), 4);
let entries = root.entries().collect::<Vec<_>>();
assert_eq!(entries.len(), 1);
let entry = &entries[0];
assert_eq!(
entry.url(),
"https://github.com/syncthing/syncthing-gtk/tags"
);
}