Fully include simplexpr
This commit is contained in:
parent
5899489250
commit
8405d01303
12 changed files with 412 additions and 27 deletions
172
Cargo.lock
generated
172
Cargo.lock
generated
|
@ -11,6 +11,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi_term"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
|
@ -128,6 +137,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "convert_case"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -145,6 +160,29 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor"
|
||||||
|
version = "0.1.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "0.99.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df"
|
||||||
|
dependencies = [
|
||||||
|
"convert_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -194,13 +232,20 @@ name = "eww_config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
|
"derive_more",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lalrpop",
|
"lalrpop",
|
||||||
"lalrpop-util",
|
"lalrpop-util",
|
||||||
|
"lazy_static",
|
||||||
"logos",
|
"logos",
|
||||||
"maplit",
|
"maplit",
|
||||||
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simplexpr",
|
||||||
|
"smart-default",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -233,6 +278,15 @@ version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-segmentation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
|
@ -383,6 +437,24 @@ version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "output_vt100"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pest"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
|
||||||
|
dependencies = [
|
||||||
|
"ucd-trie",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -414,6 +486,18 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b"
|
||||||
|
dependencies = [
|
||||||
|
"ansi_term",
|
||||||
|
"ctor",
|
||||||
|
"diff",
|
||||||
|
"output_vt100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
|
@ -478,12 +562,39 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||||
|
dependencies = [
|
||||||
|
"semver-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver-parser"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||||
|
dependencies = [
|
||||||
|
"pest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.126"
|
version = "1.0.126"
|
||||||
|
@ -533,12 +644,40 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
|
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simplexpr"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"codespan-reporting",
|
||||||
|
"itertools",
|
||||||
|
"lalrpop",
|
||||||
|
"lalrpop-util",
|
||||||
|
"logos",
|
||||||
|
"maplit",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
|
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smart-default"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -551,6 +690,27 @@ dependencies = [
|
||||||
"precomputed-hash",
|
"precomputed-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.73"
|
version = "1.0.73"
|
||||||
|
@ -621,6 +781,18 @@ dependencies = [
|
||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-trie"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-segmentation"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -17,6 +17,16 @@ maplit = "1.0"
|
||||||
codespan-reporting = "0.11"
|
codespan-reporting = "0.11"
|
||||||
logos = "0.12"
|
logos = "0.12"
|
||||||
|
|
||||||
|
derive_more = "0.99"
|
||||||
|
smart-default = "0.6"
|
||||||
|
serde = {version = "1.0", features = ["derive"]}
|
||||||
|
serde_json = "1.0"
|
||||||
|
lazy_static = "1.4"
|
||||||
|
pretty_assertions = "0.7"
|
||||||
|
|
||||||
|
|
||||||
|
simplexpr = { path = "../../projects/simplexpr" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
lalrpop = "0.19.5"
|
lalrpop = "0.19.5"
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use eww_config::{ast::*, config::*};
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut files = codespan_reporting::files::SimpleFiles::new();
|
let mut files = codespan_reporting::files::SimpleFiles::new();
|
||||||
|
|
||||||
let input = "(12 :bar 22 (foo) (baz))";
|
let input = r#"(hi :bar 22 :baz {"hi" asdfasdf * 2} (foo) (baz))"#;
|
||||||
|
|
||||||
let file_id = files.add("foo.eww", input);
|
let file_id = files.add("foo.eww", input);
|
||||||
let ast = eww_config::parse_string(file_id, input);
|
let ast = eww_config::parse_string(file_id, input);
|
||||||
|
|
14
src/ast.rs
14
src/ast.rs
|
@ -1,4 +1,5 @@
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use simplexpr::ast::SimplExpr;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{config::FromAst, error::*};
|
use crate::{config::FromAst, error::*};
|
||||||
|
@ -22,9 +23,11 @@ impl std::fmt::Debug for Span {
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
pub enum AstType {
|
pub enum AstType {
|
||||||
List,
|
List,
|
||||||
|
Array,
|
||||||
Keyword,
|
Keyword,
|
||||||
Symbol,
|
Symbol,
|
||||||
Value,
|
Value,
|
||||||
|
SimplExpr,
|
||||||
Comment,
|
Comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +40,11 @@ impl Display for AstType {
|
||||||
#[derive(PartialEq, Eq, Clone)]
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
pub enum Ast {
|
pub enum Ast {
|
||||||
List(Span, Vec<Ast>),
|
List(Span, Vec<Ast>),
|
||||||
// ArgList(Span, Vec<Ast>),
|
Array(Span, Vec<Ast>),
|
||||||
Keyword(Span, String),
|
Keyword(Span, String),
|
||||||
Symbol(Span, String),
|
Symbol(Span, String),
|
||||||
Value(Span, String),
|
Value(Span, String),
|
||||||
|
SimplExpr(Span, SimplExpr),
|
||||||
Comment(Span),
|
Comment(Span),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,9 +78,11 @@ impl Ast {
|
||||||
pub fn expr_type(&self) -> AstType {
|
pub fn expr_type(&self) -> AstType {
|
||||||
match self {
|
match self {
|
||||||
Ast::List(..) => AstType::List,
|
Ast::List(..) => AstType::List,
|
||||||
|
Ast::Array(..) => AstType::Array,
|
||||||
Ast::Keyword(..) => AstType::Keyword,
|
Ast::Keyword(..) => AstType::Keyword,
|
||||||
Ast::Symbol(..) => AstType::Symbol,
|
Ast::Symbol(..) => AstType::Symbol,
|
||||||
Ast::Value(..) => AstType::Value,
|
Ast::Value(..) => AstType::Value,
|
||||||
|
Ast::SimplExpr(..) => AstType::SimplExpr,
|
||||||
Ast::Comment(_) => AstType::Comment,
|
Ast::Comment(_) => AstType::Comment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,9 +90,11 @@ impl Ast {
|
||||||
pub fn span(&self) -> Span {
|
pub fn span(&self) -> Span {
|
||||||
match self {
|
match self {
|
||||||
Ast::List(span, _) => *span,
|
Ast::List(span, _) => *span,
|
||||||
|
Ast::Array(span, _) => *span,
|
||||||
Ast::Keyword(span, _) => *span,
|
Ast::Keyword(span, _) => *span,
|
||||||
Ast::Symbol(span, _) => *span,
|
Ast::Symbol(span, _) => *span,
|
||||||
Ast::Value(span, _) => *span,
|
Ast::Value(span, _) => *span,
|
||||||
|
Ast::SimplExpr(span, _) => *span,
|
||||||
Ast::Comment(span) => *span,
|
Ast::Comment(span) => *span,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,9 +112,11 @@ impl std::fmt::Display for Ast {
|
||||||
use Ast::*;
|
use Ast::*;
|
||||||
match self {
|
match self {
|
||||||
List(_, x) => write!(f, "({})", x.iter().map(|e| format!("{}", e)).join(" ")),
|
List(_, x) => write!(f, "({})", x.iter().map(|e| format!("{}", e)).join(" ")),
|
||||||
|
Array(_, x) => write!(f, "({})", x.iter().map(|e| format!("{}", e)).join(" ")),
|
||||||
Keyword(_, x) => write!(f, "{}", x),
|
Keyword(_, x) => write!(f, "{}", x),
|
||||||
Symbol(_, x) => write!(f, "{}", x),
|
Symbol(_, x) => write!(f, "{}", x),
|
||||||
Value(_, x) => write!(f, "{}", x),
|
Value(_, x) => write!(f, "{}", x),
|
||||||
|
SimplExpr(_, x) => write!(f, "{{{}}}", x),
|
||||||
Comment(_) => write!(f, ""),
|
Comment(_) => write!(f, ""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,9 +126,11 @@ impl std::fmt::Debug for Ast {
|
||||||
use Ast::*;
|
use Ast::*;
|
||||||
match self {
|
match self {
|
||||||
List(span, x) => f.debug_tuple(&format!("List<{}>", span)).field(x).finish(),
|
List(span, x) => f.debug_tuple(&format!("List<{}>", span)).field(x).finish(),
|
||||||
|
Array(span, x) => f.debug_tuple(&format!("Array<{}>", span)).field(x).finish(),
|
||||||
Keyword(span, x) => write!(f, "Number<{}>({})", span, x),
|
Keyword(span, x) => write!(f, "Number<{}>({})", span, x),
|
||||||
Symbol(span, x) => write!(f, "Symbol<{}>({})", span, x),
|
Symbol(span, x) => write!(f, "Symbol<{}>({})", span, x),
|
||||||
Value(span, x) => write!(f, "Value<{}>({})", span, x),
|
Value(span, x) => write!(f, "Value<{}>({})", span, x),
|
||||||
|
SimplExpr(span, x) => write!(f, "SimplExpr<{}>({})", span, x),
|
||||||
Comment(span) => write!(f, "Comment<{}>", span),
|
Comment(span) => write!(f, "Comment<{}>", span),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ mod test {
|
||||||
fn test() {
|
fn test() {
|
||||||
let parser = parser::AstParser::new();
|
let parser = parser::AstParser::new();
|
||||||
insta::with_settings!({sort_maps => true}, {
|
insta::with_settings!({sort_maps => true}, {
|
||||||
let lexer = lexer::Lexer::new("(box :bar 12 :baz \"hi\" foo (bar))");
|
let lexer = lexer::Lexer::new(0, "(box :bar 12 :baz \"hi\" foo (bar))");
|
||||||
insta::assert_debug_snapshot!(
|
insta::assert_debug_snapshot!(
|
||||||
Element::<Ast, Ast>::from_ast(parser.parse(0, lexer).unwrap()).unwrap()
|
Element::<Ast, Ast>::from_ast(parser.parse(0, lexer).unwrap()).unwrap()
|
||||||
);
|
);
|
||||||
|
|
18
src/error.rs
18
src/error.rs
|
@ -1,13 +1,13 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{Ast, AstType, Span},
|
ast::{Ast, AstType, Span},
|
||||||
lexer,
|
lexer, parse_error,
|
||||||
};
|
};
|
||||||
use codespan_reporting::{diagnostic, files};
|
use codespan_reporting::{diagnostic, files};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub type AstResult<T> = Result<T, AstError>;
|
pub type AstResult<T> = Result<T, AstError>;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum AstError {
|
pub enum AstError {
|
||||||
#[error("Definition invalid")]
|
#[error("Definition invalid")]
|
||||||
InvalidDefinition(Option<Span>),
|
InvalidDefinition(Option<Span>),
|
||||||
|
@ -17,7 +17,7 @@ pub enum AstError {
|
||||||
WrongExprType(Option<Span>, AstType, AstType),
|
WrongExprType(Option<Span>, AstType, AstType),
|
||||||
|
|
||||||
#[error("Parse error: {source}")]
|
#[error("Parse error: {source}")]
|
||||||
ParseError { file_id: Option<usize>, source: lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError> },
|
ParseError { file_id: Option<usize>, source: lalrpop_util::ParseError<usize, lexer::Token, parse_error::ParseError> },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AstError {
|
impl AstError {
|
||||||
|
@ -39,21 +39,27 @@ impl AstError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_parse_error(file_id: usize, err: lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError>) -> AstError {
|
pub fn from_parse_error(
|
||||||
|
file_id: usize,
|
||||||
|
err: lalrpop_util::ParseError<usize, lexer::Token, parse_error::ParseError>,
|
||||||
|
) -> AstError {
|
||||||
AstError::ParseError { file_id: Some(file_id), source: err }
|
AstError::ParseError { file_id: Some(file_id), source: err }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_parse_error_span(
|
fn get_parse_error_span(
|
||||||
file_id: usize,
|
file_id: usize,
|
||||||
err: &lalrpop_util::ParseError<usize, lexer::Token, lexer::LexicalError>,
|
err: &lalrpop_util::ParseError<usize, lexer::Token, parse_error::ParseError>,
|
||||||
) -> Option<Span> {
|
) -> Option<Span> {
|
||||||
match err {
|
match err {
|
||||||
lalrpop_util::ParseError::InvalidToken { location } => Some(Span(*location, *location, file_id)),
|
lalrpop_util::ParseError::InvalidToken { location } => Some(Span(*location, *location, file_id)),
|
||||||
lalrpop_util::ParseError::UnrecognizedEOF { location, expected } => Some(Span(*location, *location, file_id)),
|
lalrpop_util::ParseError::UnrecognizedEOF { location, expected } => Some(Span(*location, *location, file_id)),
|
||||||
lalrpop_util::ParseError::UnrecognizedToken { token, expected } => Some(Span(token.0, token.2, file_id)),
|
lalrpop_util::ParseError::UnrecognizedToken { token, expected } => Some(Span(token.0, token.2, file_id)),
|
||||||
lalrpop_util::ParseError::ExtraToken { token } => Some(Span(token.0, token.2, file_id)),
|
lalrpop_util::ParseError::ExtraToken { token } => Some(Span(token.0, token.2, file_id)),
|
||||||
lalrpop_util::ParseError::User { error } => None,
|
lalrpop_util::ParseError::User { error } => match error {
|
||||||
|
parse_error::ParseError::SimplExpr(span, error) => *span,
|
||||||
|
parse_error::ParseError::LexicalError(span) => Some(*span),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
src/lexer.rs
31
src/lexer.rs
|
@ -1,12 +1,17 @@
|
||||||
use logos::Logos;
|
use logos::Logos;
|
||||||
|
|
||||||
|
use crate::{ast::Span, parse_error};
|
||||||
|
|
||||||
#[derive(Logos, Debug, PartialEq, Eq, Clone)]
|
#[derive(Logos, Debug, PartialEq, Eq, Clone)]
|
||||||
pub enum Token {
|
pub enum Token {
|
||||||
#[token("(")]
|
#[token("(")]
|
||||||
LPren,
|
LPren,
|
||||||
|
|
||||||
#[token(")")]
|
#[token(")")]
|
||||||
RPren,
|
RPren,
|
||||||
|
#[token("[")]
|
||||||
|
LBrack,
|
||||||
|
#[token("]")]
|
||||||
|
RBrack,
|
||||||
|
|
||||||
#[token("true")]
|
#[token("true")]
|
||||||
True,
|
True,
|
||||||
|
@ -26,6 +31,9 @@ pub enum Token {
|
||||||
#[regex(r#":\S+"#, |x| x.slice().to_string())]
|
#[regex(r#":\S+"#, |x| x.slice().to_string())]
|
||||||
Keyword(String),
|
Keyword(String),
|
||||||
|
|
||||||
|
#[regex(r#"\{[^}]*\}"#, |x| x.slice().to_string())]
|
||||||
|
SimplExpr(String),
|
||||||
|
|
||||||
#[regex(r#";.*"#)]
|
#[regex(r#";.*"#)]
|
||||||
Comment,
|
Comment,
|
||||||
|
|
||||||
|
@ -39,46 +47,41 @@ impl std::fmt::Display for Token {
|
||||||
match self {
|
match self {
|
||||||
Token::LPren => write!(f, "'('"),
|
Token::LPren => write!(f, "'('"),
|
||||||
Token::RPren => write!(f, "')'"),
|
Token::RPren => write!(f, "')'"),
|
||||||
|
Token::LBrack => write!(f, "'['"),
|
||||||
|
Token::RBrack => write!(f, "']'"),
|
||||||
Token::True => write!(f, "true"),
|
Token::True => write!(f, "true"),
|
||||||
Token::False => write!(f, "false"),
|
Token::False => write!(f, "false"),
|
||||||
Token::StrLit(x) => write!(f, "\"{}\"", x),
|
Token::StrLit(x) => write!(f, "\"{}\"", x),
|
||||||
Token::NumLit(x) => write!(f, "{}", x),
|
Token::NumLit(x) => write!(f, "{}", x),
|
||||||
Token::Symbol(x) => write!(f, "{}", x),
|
Token::Symbol(x) => write!(f, "{}", x),
|
||||||
Token::Keyword(x) => write!(f, "{}", x),
|
Token::Keyword(x) => write!(f, "{}", x),
|
||||||
|
Token::SimplExpr(x) => write!(f, "{{{}}}", x),
|
||||||
Token::Comment => write!(f, ""),
|
Token::Comment => write!(f, ""),
|
||||||
Token::Error => write!(f, ""),
|
Token::Error => write!(f, ""),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
|
||||||
pub struct LexicalError(usize, usize);
|
|
||||||
|
|
||||||
impl std::fmt::Display for LexicalError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Lexical error at {}..{}", self.0, self.1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type SpannedResult<Tok, Loc, Error> = Result<(Loc, Tok, Loc), Error>;
|
pub type SpannedResult<Tok, Loc, Error> = Result<(Loc, Tok, Loc), Error>;
|
||||||
|
|
||||||
pub struct Lexer<'input> {
|
pub struct Lexer<'input> {
|
||||||
|
file_id: usize,
|
||||||
lexer: logos::SpannedIter<'input, Token>,
|
lexer: logos::SpannedIter<'input, Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'input> Lexer<'input> {
|
impl<'input> Lexer<'input> {
|
||||||
pub fn new(text: &'input str) -> Self {
|
pub fn new(file_id: usize, text: &'input str) -> Self {
|
||||||
Lexer { lexer: logos::Lexer::new(text).spanned() }
|
Lexer { file_id, lexer: logos::Lexer::new(text).spanned() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'input> Iterator for Lexer<'input> {
|
impl<'input> Iterator for Lexer<'input> {
|
||||||
type Item = SpannedResult<Token, usize, LexicalError>;
|
type Item = SpannedResult<Token, usize, parse_error::ParseError>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let (token, range) = self.lexer.next()?;
|
let (token, range) = self.lexer.next()?;
|
||||||
if token == Token::Error {
|
if token == Token::Error {
|
||||||
Some(Err(LexicalError(range.start, range.end)))
|
Some(Err(parse_error::ParseError::LexicalError(Span(range.start, range.end, self.file_id))))
|
||||||
} else {
|
} else {
|
||||||
Some(Ok((range.start, token, range.end)))
|
Some(Ok((range.start, token, range.end)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ pub mod ast;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
|
mod parse_error;
|
||||||
|
pub mod value;
|
||||||
use ast::Ast;
|
use ast::Ast;
|
||||||
use error::{AstError, AstResult};
|
use error::{AstError, AstResult};
|
||||||
|
|
||||||
|
@ -18,7 +20,7 @@ use lalrpop_util::lalrpop_mod;
|
||||||
lalrpop_mod!(pub parser);
|
lalrpop_mod!(pub parser);
|
||||||
|
|
||||||
pub fn parse_string(file_id: usize, s: &str) -> AstResult<Ast> {
|
pub fn parse_string(file_id: usize, s: &str) -> AstResult<Ast> {
|
||||||
let lexer = lexer::Lexer::new(s);
|
let lexer = lexer::Lexer::new(file_id, s);
|
||||||
let parser = parser::AstParser::new();
|
let parser = parser::AstParser::new();
|
||||||
Ok(parser.parse(file_id, lexer).map_err(|e| AstError::from_parse_error(file_id, e))?)
|
Ok(parser.parse(file_id, lexer).map_err(|e| AstError::from_parse_error(file_id, e))?)
|
||||||
}
|
}
|
||||||
|
@ -30,7 +32,7 @@ macro_rules! test_parser {
|
||||||
|
|
||||||
::insta::with_settings!({sort_maps => true}, {
|
::insta::with_settings!({sort_maps => true}, {
|
||||||
$(
|
$(
|
||||||
::insta::assert_debug_snapshot!(p.parse(0, Lexer::new($text)));
|
::insta::assert_debug_snapshot!(p.parse(0, Lexer::new(0, $text)));
|
||||||
)*
|
)*
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
10
src/parse_error.rs
Normal file
10
src/parse_error.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use crate::ast::Span;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("{1}")]
|
||||||
|
SimplExpr(Option<Span>, simplexpr::error::Error),
|
||||||
|
|
||||||
|
#[error("Unknown token")]
|
||||||
|
LexicalError(Span),
|
||||||
|
}
|
|
@ -1,22 +1,28 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use crate::lexer::{Token, LexicalError};
|
use crate::lexer::{Token};
|
||||||
use crate::ast::{Ast, Span};
|
use crate::ast::{Ast, Span};
|
||||||
|
use simplexpr::ast::SimplExpr;
|
||||||
|
use simplexpr;
|
||||||
|
use lalrpop_util::ParseError;
|
||||||
|
|
||||||
grammar(file_id: usize);
|
grammar(file_id: usize);
|
||||||
|
|
||||||
extern {
|
extern {
|
||||||
type Location = usize;
|
type Location = usize;
|
||||||
type Error = LexicalError;
|
type Error = crate::parse_error::ParseError;
|
||||||
|
|
||||||
enum Token {
|
enum Token {
|
||||||
"(" => Token::LPren,
|
"(" => Token::LPren,
|
||||||
")" => Token::RPren,
|
")" => Token::RPren,
|
||||||
|
"[" => Token::LBrack,
|
||||||
|
"]" => Token::RBrack,
|
||||||
"true" => Token::True,
|
"true" => Token::True,
|
||||||
"false" => Token::False,
|
"false" => Token::False,
|
||||||
"string" => Token::StrLit(<String>),
|
"string" => Token::StrLit(<String>),
|
||||||
"number" => Token::NumLit(<String>),
|
"number" => Token::NumLit(<String>),
|
||||||
"symbol" => Token::Symbol(<String>),
|
"symbol" => Token::Symbol(<String>),
|
||||||
"keyword" => Token::Keyword(<String>),
|
"keyword" => Token::Keyword(<String>),
|
||||||
|
"simplexpr" => Token::SimplExpr(<String>),
|
||||||
"comment" => Token::Comment,
|
"comment" => Token::Comment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +30,8 @@ extern {
|
||||||
|
|
||||||
pub Ast: Ast = {
|
pub Ast: Ast = {
|
||||||
<l:@L> "(" <elems:(<Ast>)+> ")" <r:@R> => Ast::List(Span(l, r, file_id), elems),
|
<l:@L> "(" <elems:(<Ast>)+> ")" <r:@R> => Ast::List(Span(l, r, file_id), elems),
|
||||||
|
<l:@L> "[" <elems:(<Ast>)+> "]" <r:@R> => Ast::Array(Span(l, r, file_id), elems),
|
||||||
|
<l:@L> <expr:SimplExpr> <r:@R> => Ast::SimplExpr(Span(l, r, file_id), expr),
|
||||||
<x:Keyword> => x,
|
<x:Keyword> => x,
|
||||||
<x:Symbol> => x,
|
<x:Symbol> => x,
|
||||||
<l:@L> <x:Value> <r:@R> => Ast::Value(Span(l, r, file_id), x),
|
<l:@L> <x:Value> <r:@R> => Ast::Value(Span(l, r, file_id), x),
|
||||||
|
@ -45,6 +53,15 @@ StrLit: String = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SimplExpr: SimplExpr = {
|
||||||
|
<l:@L> <x:"simplexpr"> =>? {
|
||||||
|
let expr = x[1..x.len() - 1].to_string();
|
||||||
|
simplexpr::parse_string(&expr).map_err(|e| {
|
||||||
|
let span = e.get_span().map(|simplexpr::Span(simpl_l, simpl_r)| Span(1 + l + simpl_l, 1 + l + simpl_r, file_id));
|
||||||
|
ParseError::User { error: crate::parse_error::ParseError::SimplExpr(span, e) }})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Num: String = <"number"> => <>.to_string();
|
Num: String = <"number"> => <>.to_string();
|
||||||
Bool: String = {
|
Bool: String = {
|
||||||
|
|
112
src/value/coords.rs
Normal file
112
src/value/coords.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use derive_more::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use smart_default::SmartDefault;
|
||||||
|
use std::{fmt, str::FromStr};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Failed to parse \"{0}\" as a length value")]
|
||||||
|
NumParseFailed(String),
|
||||||
|
#[error("Inalid unit \"{0}\", must be either % or px")]
|
||||||
|
InvalidUnit(String),
|
||||||
|
#[error("Invalid format. Coordinates must be formated like 200x100")]
|
||||||
|
MalformedCoords,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, DebugCustom, SmartDefault)]
|
||||||
|
pub enum NumWithUnit {
|
||||||
|
#[display(fmt = "{}%", .0)]
|
||||||
|
#[debug(fmt = "{}%", .0)]
|
||||||
|
Percent(i32),
|
||||||
|
#[display(fmt = "{}px", .0)]
|
||||||
|
#[debug(fmt = "{}px", .0)]
|
||||||
|
#[default]
|
||||||
|
Pixels(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumWithUnit {
|
||||||
|
pub fn relative_to(&self, max: i32) -> i32 {
|
||||||
|
match *self {
|
||||||
|
NumWithUnit::Percent(n) => ((max as f64 / 100.0) * n as f64) as i32,
|
||||||
|
NumWithUnit::Pixels(n) => n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for NumWithUnit {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
static ref PATTERN: regex::Regex = regex::Regex::new("^(-?\\d+)(.*)$").unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
let captures = PATTERN.captures(s).ok_or_else(|| Error::NumParseFailed(s.to_string()))?;
|
||||||
|
let value = captures.get(1).unwrap().as_str().parse::<i32>().map_err(|_| Error::NumParseFailed(s.to_string()))?;
|
||||||
|
match captures.get(2).unwrap().as_str() {
|
||||||
|
"px" | "" => Ok(NumWithUnit::Pixels(value)),
|
||||||
|
"%" => Ok(NumWithUnit::Percent(value)),
|
||||||
|
unit => Err(Error::InvalidUnit(unit.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Display, Default)]
|
||||||
|
#[display(fmt = "{}*{}", x, y)]
|
||||||
|
pub struct Coords {
|
||||||
|
pub x: NumWithUnit,
|
||||||
|
pub y: NumWithUnit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Coords {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (x, y) = s
|
||||||
|
.split_once(|x: char| x.to_ascii_lowercase() == 'x' || x.to_ascii_lowercase() == '*')
|
||||||
|
.ok_or_else(|| Error::MalformedCoords)?;
|
||||||
|
Coords::from_strs(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Coords {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "CoordsWithUnits({}, {})", self.x, self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coords {
|
||||||
|
pub fn from_pixels(x: i32, y: i32) -> Self {
|
||||||
|
Coords { x: NumWithUnit::Pixels(x), y: NumWithUnit::Pixels(y) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parse a string for x and a string for y into a [`Coords`] object.
|
||||||
|
pub fn from_strs(x: &str, y: &str) -> Result<Coords, Error> {
|
||||||
|
Ok(Coords { x: x.parse()?, y: y.parse()? })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resolve the possibly relative coordinates relative to a given containers size
|
||||||
|
pub fn relative_to(&self, width: i32, height: i32) -> (i32, i32) {
|
||||||
|
(self.x.relative_to(width), self.y.relative_to(height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_num_with_unit() {
|
||||||
|
assert_eq!(NumWithUnit::Pixels(55), NumWithUnit::from_str("55").unwrap());
|
||||||
|
assert_eq!(NumWithUnit::Pixels(55), NumWithUnit::from_str("55px").unwrap());
|
||||||
|
assert_eq!(NumWithUnit::Percent(55), NumWithUnit::from_str("55%").unwrap());
|
||||||
|
assert!(NumWithUnit::from_str("55pp").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_coords() {
|
||||||
|
assert_eq!(Coords { x: NumWithUnit::Pixels(50), y: NumWithUnit::Pixels(60) }, Coords::from_str("50x60").unwrap());
|
||||||
|
assert!(Coords::from_str("5060").is_err());
|
||||||
|
}
|
||||||
|
}
|
41
src/value/mod.rs
Normal file
41
src/value/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use derive_more::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod coords;
|
||||||
|
pub use coords::*;
|
||||||
|
|
||||||
|
/// The name of a variable
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)]
|
||||||
|
#[debug(fmt = "VarName({})", .0)]
|
||||||
|
pub struct VarName(pub String);
|
||||||
|
|
||||||
|
impl std::borrow::Borrow<str> for VarName {
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for VarName {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
VarName(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of an attribute
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Hash, PartialEq, Eq, Serialize, Deserialize, AsRef, From, FromStr, Display, DebugCustom)]
|
||||||
|
#[debug(fmt="AttrName({})", .0)]
|
||||||
|
pub struct AttrName(pub String);
|
||||||
|
|
||||||
|
impl std::borrow::Borrow<str> for AttrName {
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for AttrName {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
AttrName(s.to_owned())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue