From e25ae8a13e7509533e552bd7ee3b411894650526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Thebault?= Date: Mon, 29 Jun 2026 23:14:35 +0200 Subject: [PATCH] drop support for DSL serde custom implementation has far more advantages --- .vscode/launch.json | 48 -- Cargo.lock | 108 +--- Cargo.toml | 23 +- README.md | 3 - dsl/Cargo.toml | 16 - dsl/pdsl-grammar.ebnf | 64 --- dsl/src/ast.rs | 130 ----- dsl/src/diag.rs | 148 ------ dsl/src/input.rs | 92 ---- dsl/src/lex.rs | 529 ------------------ dsl/src/lib.rs | 54 -- dsl/src/parse.rs | 946 --------------------------------- examples/bode-rlc.plotive | 16 - examples/bode_rlc_dsl.rs | 68 --- examples/iris.plotive | 45 -- examples/iris_dsl.rs | 107 ---- examples/multiple_axes.plotive | 20 - examples/multiple_axes_dsl.rs | 22 - examples/subplots.plotive | 23 - examples/subplots_dsl.rs | 24 - run_all_examples.sh | 6 +- src/dsl.rs | 929 -------------------------------- src/lib.rs | 4 - 23 files changed, 3 insertions(+), 3422 deletions(-) delete mode 100644 dsl/Cargo.toml delete mode 100644 dsl/pdsl-grammar.ebnf delete mode 100644 dsl/src/ast.rs delete mode 100644 dsl/src/diag.rs delete mode 100644 dsl/src/input.rs delete mode 100644 dsl/src/lex.rs delete mode 100644 dsl/src/lib.rs delete mode 100644 dsl/src/parse.rs delete mode 100644 examples/bode-rlc.plotive delete mode 100644 examples/bode_rlc_dsl.rs delete mode 100644 examples/iris.plotive delete mode 100644 examples/iris_dsl.rs delete mode 100644 examples/multiple_axes.plotive delete mode 100644 examples/multiple_axes_dsl.rs delete mode 100644 examples/subplots.plotive delete mode 100644 examples/subplots_dsl.rs delete mode 100644 src/dsl.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 1d6002f1..971da867 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -37,18 +37,6 @@ }, "args": ["png", "svg"] }, - { - "type": "lldb", - "request": "launch", - "name": "bode_rlc_dsl example", - "cargo": { - "args": [ - "build", "--example", "bode_rlc", - "--features", "dsl-diag" - ] - }, - "args": ["png", "svg"] - }, { "type": "lldb", "request": "launch", @@ -71,18 +59,6 @@ }, "args": ["png", "svg"] }, - { - "type": "lldb", - "request": "launch", - "name": "iris_dsl example", - "cargo": { - "args": [ - "build", "--example", "iris_dsl", - "--features", "dsl-diag" - ] - }, - "args": ["png", "svg"] - }, { "type": "lldb", "request": "launch", @@ -105,18 +81,6 @@ }, "args": ["png", "svg"] }, - { - "type": "lldb", - "request": "launch", - "name": "multiple_axes_dsl example", - "cargo": { - "args": [ - "build", "--example", "multiple_axes_dsl", - "--features", "dsl-diag" - ] - }, - "args": ["png", "svg"] - }, { "type": "lldb", "request": "launch", @@ -150,18 +114,6 @@ }, "args": ["png", "svg"] }, - { - "type": "lldb", - "request": "launch", - "name": "subplots_dsl example", - "cargo": { - "args": [ - "build", "--example", "subplots_dsl", - "--features", "dsl-diag" - ] - }, - "args": ["png", "svg"] - }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index 02b9107e..7d126d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,15 +420,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -680,7 +671,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.1", + "unicode-width", ] [[package]] @@ -1810,12 +1801,6 @@ dependencies = [ "libc", ] -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - [[package]] name = "itertools" version = "0.14.0" @@ -2151,36 +2136,6 @@ dependencies = [ "paste", ] -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2925,12 +2880,6 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "owo-colors" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" - [[package]] name = "parking" version = "2.2.1" @@ -3034,7 +2983,6 @@ dependencies = [ "noyalib", "ode_solvers", "plotive-base", - "plotive-dsl", "plotive-iced", "plotive-pxl", "plotive-svg", @@ -3057,13 +3005,6 @@ dependencies = [ "tiny-skia-path", ] -[[package]] -name = "plotive-dsl" -version = "0.6.0-dev" -dependencies = [ - "miette", -] - [[package]] name = "plotive-iced" version = "0.6.0-dev" @@ -3958,27 +3899,6 @@ dependencies = [ "float-cmp", ] -[[package]] -name = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - [[package]] name = "svg" version = "0.18.0" @@ -4044,26 +3964,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminal_size" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" -dependencies = [ - "rustix 1.1.3", - "windows-sys 0.60.2", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.1", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4332,12 +4232,6 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index c0a58377..94a4fa6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ all-features = true [dependencies] plotive-base.workspace = true plotive-text.workspace = true -# feature dsl -plotive-dsl = { workspace = true, optional = true } # feature serde serde = { workspace = true, optional = true } serde-value = { version = "0.7", optional = true } @@ -37,7 +35,6 @@ noyalib = "0.0.6" default = ["noto-sans"] data-csv = [] -dsl = ["plotive-dsl/diag"] noto-mono = ["plotive-text/noto-mono"] noto-sans = ["plotive-text/noto-sans"] noto-sans-italic = ["plotive-text/noto-sans-italic"] @@ -61,10 +58,6 @@ required-features = ["data-csv", "time"] name = "bode_rlc" required-features = ["noto-serif-italic", "utils"] -[[example]] -name = "bode_rlc_dsl" -required-features = ["dsl", "noto-serif-italic", "utils"] - [[example]] name = "bouncing_ball" required-features = ["time"] @@ -76,10 +69,6 @@ name = "gauss" name = "iris" required-features = ["data-csv"] -[[example]] -name = "iris_dsl" -required-features = ["data-csv", "dsl"] - [[example]] name = "minimal" @@ -87,10 +76,6 @@ name = "minimal" name = "multiple_axes" required-features = ["utils"] -[[example]] -name = "multiple_axes_dsl" -required-features = ["dsl", "utils"] - [[example]] name = "readme" @@ -105,12 +90,8 @@ required-features = ["data-csv"] name = "subplots" required-features = ["utils"] -[[example]] -name = "subplots_dsl" -required-features = ["dsl", "utils"] - [workspace] -members = ["base", "dsl", "iced", "pxl", "svg", "text", "tests"] +members = ["base", "iced", "pxl", "svg", "text", "tests"] resolver = "3" [workspace.package] @@ -126,7 +107,6 @@ keywords = ["data", "visualization", "plotting"] [workspace.dependencies] plotive = { version = "0.6.0-dev", path = "." } plotive-base = { version = "0.6.0-dev", path = "base" } -plotive-dsl = { version = "0.6.0-dev", path = "dsl" } plotive-pxl = { version = "0.6.0-dev", path = "pxl" } plotive-svg = { version = "0.6.0-dev", path = "svg" } plotive-text = { version = "0.6.0-dev", path = "text" } @@ -140,7 +120,6 @@ iced = { version = "0.14.0", features = [ ] } iced_font_awesome = "0.4.0" log = "0.4" -miette = { version = "7.6.0", features = ["fancy"] } rand = "0.9.2" rand_distr = "0.5.1" rand_chacha = "0.9.0" diff --git a/README.md b/README.md index e7822a4d..1b04d8f3 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,6 @@ More will come. Don't hesitate to open an issue to request a feature! - Design of figure is entirely declarative and decoupled from data and drawing primitives - Sensible defaults. Most types that populates design implement carefully crafted `Default` trait. - Figure units are decorrelated from pixel size for easy scaling - - `.plotive` DSL language for concise figure description. -This DSL is still fairly incomplete, but all examples in the repo are working. - **Data sources** (`plotive::data`) - Flexible, column-friendly data source system @@ -142,7 +140,6 @@ During execution, the following window shows: ## Crate features - `data-csv`: enables CSV data source support ([`plotive::data::csv`](https://docs.rs/plotive/latest/plotive/data/csv/index.html)) - - `dsl`: enables the support for `.plotive` DSL. - `noto-mono`, `noto-sans`, `noto-sans-italic`, `noto-serif`, `noto-serif-italic`: bundles the corresponding fonts from Google in the final executable, and enables `plotive::bundled_font_db()`.
`noto-sans` is enabled by default - `time`: enables support for time series, CSV date-time parsing etc. ([`plotive::time`](https://docs.rs/plotive/latest/plotive/time/index.html)) diff --git a/dsl/Cargo.toml b/dsl/Cargo.toml deleted file mode 100644 index 82089934..00000000 --- a/dsl/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "plotive-dsl" -description = "DSL library for plotive" -version.workspace = true -authors.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true -categories.workspace = true -keywords.workspace = true - -[dependencies] -miette = { workspace = true, optional = true } - -[features] -diag = ["dep:miette"] diff --git a/dsl/pdsl-grammar.ebnf b/dsl/pdsl-grammar.ebnf deleted file mode 100644 index 48853b71..00000000 --- a/dsl/pdsl-grammar.ebnf +++ /dev/null @@ -1,64 +0,0 @@ -grammar = prop-list ; - -eol = '\n' | "\r\n"; - -digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ; -lowercase = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | - 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | - 'u' | 'v' | 'w' | 'x' | 'y' | 'z' ; -uppercase = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | - 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | - 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' ; -minus = '-' ; -plus = '+' ; -sign = plus | minus ; -open-par = '(' ; -close-par = ')' ; -open-bracket = '[' ; -close-bracket = ']' ; -open-brace = '{' ; -close-brace = '}' ; -double-quote = '"' ; -comma = ',' ; -space = ' ' | '\t' ; - -opt-sp = { space } ; -comment = "//", anything-but-eol, eol ; -com-eol = opt-sp , eol | comment ; - -prop-list = { com-eol | prop } ; -prop = opt-sp, prop-name , [ ':' , opt-sp , prop-value ] , opt-sp , com-eol ; -prop-value = scalar-value | seq-value | array-value | struct-value ; - -prop-name = kebab-case-ident ; -ident = pascal-case-ident ; - -scalar-value = enum-value | str-concatenation | int-literal | float-literal | func-value ; - -enum-value = ident ; - -string-literal = double-quote, - { any-unicode-except-double-quote-backslash-or-eol | esc-seq } , - double-quote ; -esc-seq = ( '\\', '\\' ) | ( '\\', 'n' ) | ( '\\', 'r' ) | ( '\\', 't' ) ; -str-concatenation = string-literal , { com-eol | string-literal } ; - -int-literal = [ sign ] , digit , { digit } ; -float-literal = [ sign ] , digit , { digit } , '.' , digit , { digit } , - [ 'e' , [ sign ] , digit , { digit } ] ; - -func-value = kebab-case-ident , opt-sp , open-par , [ seq-value ] , close-par ; - -seq-value = scalar-value , { comma , scalar-value } ; - -array-value = open-bracket , - [ int-sequence | float-sequnce | str-sequence ] , - close-bracket ; -int-sequence = int-literal , { comma , { com-eol } , int-literal } , [ comma , { com-eol } ] ; -float-sequence = float-literal , { comma , { com-eol } , float-literal } , [ comma , { com-eol } ] ; -str-sequence = str-concatenation , { comma , { com-eol } , str-concatenation } , [ comma , { com-eol } ] ; - -struct-value = [ ident, opt-sp ], open-brace , prop-list, close-brace ; - -kebab-case-ident = lowercase , { lowercase | digit | '-' } ; -pascal-case-ident = uppercase , { lowercase | digit | uppercase } ; diff --git a/dsl/src/ast.rs b/dsl/src/ast.rs deleted file mode 100644 index 994ddd6e..00000000 --- a/dsl/src/ast.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::lex::Span; - -#[derive(Debug, Clone, PartialEq)] -pub struct Ident { - pub span: Span, - pub name: String, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Prop { - pub name: Ident, - pub value: Option, -} - -impl Prop { - pub fn span(&self) -> Span { - let start_span = self.name.span; - if let Some(value) = &self.value { - (start_span.0, value.span().1) - } else { - start_span - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Value { - Scalar(Scalar), - Seq(Seq), - Array(Array), - Struct(Struct), -} - -impl Value { - pub fn span(&self) -> Span { - match self { - Value::Scalar(scalar) => scalar.span, - Value::Seq(seq) => seq.span, - Value::Array(array) => array.span, - Value::Struct(struct_) => struct_.span, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Scalar { - pub span: Span, - pub kind: ScalarKind, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum ScalarKind { - Enum(String), - Str(String), - Int(i64), - Float(f64), - Func(Func), -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Func { - pub name: Ident, - pub args: Seq, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Seq { - pub span: Span, - pub scalars: Vec, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Array { - pub span: Span, - pub kind: ArrayKind, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayKind { - Empty, - Int(Vec), - Float(Vec), - Str(Vec), -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Struct { - pub span: Span, - pub typ: Option, - pub props: Vec, -} - -impl Struct { - pub fn has_prop(&self, name: &str) -> bool { - self.props.iter().any(|p| p.name.name == name) - } - - pub fn prop(&self, name: &str) -> Option<&Prop> { - self.props.iter().find(|p| p.name.name == name) - } - - pub fn take_prop(&mut self, name: &str) -> Option { - if let Some(pos) = self.props.iter().position(|p| p.name.name == name) { - Some(self.props.remove(pos)) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_prop_span() { - let prop = Prop { - name: Ident { - span: (0, 3), - name: "foo".to_string(), - }, - value: Some(Value::Array(Array { - span: (5, 14), - kind: ArrayKind::Int(vec![1, 2, 3]), - })), - }; - - assert_eq!(prop.span(), (0, 14)); - } -} diff --git a/dsl/src/diag.rs b/dsl/src/diag.rs deleted file mode 100644 index 7e4cf666..00000000 --- a/dsl/src/diag.rs +++ /dev/null @@ -1,148 +0,0 @@ -use core::fmt; - -use miette::MietteSpanContents; - -use crate::{Span, lex, parse}; - -/// Export miette::Result as DiagResult, to avoid requiring dependency on miette. -pub type DiagResult = miette::Result; - -/// Export miette::Report as DiagReport, to avoid requiring dependency on miette. -pub type DiagReport = miette::Report; - -pub trait DiagTrait: fmt::Debug + fmt::Display { - fn span(&self) -> Span; - fn message(&self) -> String; - fn help(&self) -> Option { - None - } -} - -impl DiagTrait for lex::Error { - fn span(&self) -> Span { - match self { - lex::Error::UnexpectedChar { pos, .. } => (*pos, *pos + 1), - lex::Error::UnexpectedEndOfFile(pos) => (*pos, *pos), - lex::Error::UnterminatedString { span, .. } => *span, - lex::Error::InvalidEscSequence(span, _) => *span, - lex::Error::InvalidNumber(span, _) => *span, - lex::Error::InvalidKebabIdent(span, _) => *span, - lex::Error::InvalidPascalIdent(span, _) => *span, - } - } - - fn message(&self) -> String { - format!("{}", self) - } -} - -impl DiagTrait for parse::Error { - fn span(&self) -> Span { - match self { - parse::Error::Lex(err) => err.span(), - parse::Error::UnexpectedEndOfInput(span) => *span, - parse::Error::UnexpectedToken(tok, _) => tok.span, - } - } - - fn message(&self) -> String { - format!("{}", self) - } -} - -#[derive(Debug, Clone)] -pub struct Source { - pub name: Option, - pub src: String, -} - -impl miette::SourceCode for Source { - fn read_span<'a>( - &'a self, - span: &miette::SourceSpan, - context_lines_before: usize, - context_lines_after: usize, - ) -> Result + 'a>, miette::MietteError> { - let start = span.offset(); - let end = start + span.len(); - - if start > self.src.len() || end > self.src.len() || start > end { - return Err(miette::MietteError::OutOfBounds); - } - - let content = ::read_span( - &self.src, - span, - context_lines_before, - context_lines_after, - )?; - if let Some(name) = self.name.as_deref() { - let content = MietteSpanContents::new_named( - name.to_string(), - content.data(), - *content.span(), - content.line(), - content.column(), - content.line_count(), - ) - .with_language("pdsl"); - Ok(Box::new(content)) - } else { - Ok(content) - } - } -} - -#[derive(Debug)] -pub struct Diagnostic { - diag: Box, - source: Source, -} - -impl<'a> Diagnostic { - pub fn new(diag: Box, source: Source) -> Self { - Self { diag, source } - } -} - -impl fmt::Display for Diagnostic { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.diag.message())?; - if let Some(help) = self.diag.help() { - write!(f, "\nHelp: {}", help)?; - } - Ok(()) - } -} - -impl std::error::Error for Diagnostic {} - -unsafe impl Send for Diagnostic {} -unsafe impl Sync for Diagnostic {} - -impl miette::Diagnostic for Diagnostic { - fn code<'a>(&'a self) -> Option> { - None - } - - fn severity(&self) -> Option { - Some(miette::Severity::Error) - } - - fn help<'a>(&'a self) -> Option> { - self.diag - .help() - .map(|h| Box::new(h) as Box) - } - - fn labels<'a>(&'a self) -> Option + 'a>> { - let (start, end) = self.diag.span(); - let labeled_span = - miette::LabeledSpan::new(Some(self.diag.message()), start.into(), end - start); - Some(Box::new(std::iter::once(labeled_span))) - } - - fn source_code(&self) -> Option<&dyn miette::SourceCode> { - Some(&self.source as &dyn miette::SourceCode) - } -} diff --git a/dsl/src/input.rs b/dsl/src/input.rs deleted file mode 100644 index 52978a1e..00000000 --- a/dsl/src/input.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::iter::FusedIterator; - -/// Position into an input stream -pub type Pos = usize; - -/// A cursor over an input stream of characters. -/// It keeps track of the current position in the stream. -#[derive(Debug, Clone)] -pub struct Cursor { - // input iterator - input: I, - // current position in the stream - pos: Pos, -} - -impl Cursor { - pub fn new(input: I) -> Self { - Self { - input, - pos: Pos::default(), - } - } - - pub fn pos(&self) -> Pos { - self.pos - } -} - -impl Cursor -where - I: Iterator + Clone, -{ - pub fn first(&self) -> Option { - self.input.clone().next() - } -} - -impl Iterator for Cursor -where - I: Iterator, -{ - type Item = char; - - fn next(&mut self) -> Option { - let next = self.input.next(); - if let Some(c) = next { - self.pos += c.len_utf8(); - } - next - } -} - -impl FusedIterator for Cursor where I: FusedIterator {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_input_cursor() { - let mut c = Cursor::new("some string\na second line\n".chars()); - assert_eq!(c.pos(), Default::default()); - assert_eq!(c.next(), Some('s')); - assert_eq!(c.next(), Some('o')); - assert_eq!(c.next(), Some('m')); - assert_eq!(c.next(), Some('e')); - assert_eq!(c.pos(), 4); - let string: String = c.by_ref().take(7).collect(); - assert_eq!(string, " string"); - - assert_eq!(c.pos(), 11); - assert_eq!(c.next(), Some('\n')); - assert_eq!(c.pos(), 12); - - let cloned = c.clone(); - let cloned_pos = cloned.pos(); - - let a_second_line: String = c.by_ref().take(13).collect(); - assert_eq!(a_second_line, "a second line"); - assert_eq!(c.pos(), 25); - - // checking independence of cloned cursor - assert_eq!(cloned.pos(), cloned_pos); - let a_second_line: String = cloned.take(13).collect(); - assert_eq!(a_second_line, "a second line"); - - assert_eq!(c.pos(), 25); - assert_eq!(c.next(), Some('\n')); - assert_eq!(c.pos(), 26); - assert_eq!(c.next(), None); - } -} diff --git a/dsl/src/lex.rs b/dsl/src/lex.rs deleted file mode 100644 index 0c66fe5b..00000000 --- a/dsl/src/lex.rs +++ /dev/null @@ -1,529 +0,0 @@ -use std::fmt; -use std::iter::FusedIterator; - -use crate::input::{Cursor, Pos}; - -/// Byte span into an input stream -/// (first pos, one past last pos) -pub type Span = (Pos, Pos); - -/// A lexical error -#[derive(Debug, Clone)] -pub enum Error { - UnexpectedChar { - pos: Pos, - expected: char, - found: char, - }, - UnexpectedEndOfFile(Pos), - UnterminatedString { - span: Span, - help: String, - }, - InvalidEscSequence(Span, char), - InvalidNumber(Span, String), - InvalidKebabIdent(Span, String), - InvalidPascalIdent(Span, String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::UnexpectedChar { - expected, found, .. - } => write!( - f, - "Unexpected character: expected '{}', found '{}'", - expected, found - ), - Error::UnexpectedEndOfFile(..) => write!(f, "Unexpected end of file"), - Error::UnterminatedString { help, .. } => { - write!(f, "Unterminated string. {}", help) - } - Error::InvalidEscSequence(_, c) => { - write!(f, "Invalid escape sequence: \\{}", c) - } - Error::InvalidNumber(_, s) => write!(f, "Invalid number: {}", s), - Error::InvalidKebabIdent(_, s) => { - write!(f, "Invalid kebab-case identifier {}", s) - } - Error::InvalidPascalIdent(_, s) => write!(f, "Invalid pascal-case identifier {}", s,), - } - } -} - -impl std::error::Error for Error {} - -pub type Result = std::result::Result; - -#[derive(Debug, Clone)] -pub struct Token { - pub span: Span, - pub kind: TokenKind, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum TokenKind { - KebabCaseIdent(String), - PascalCaseIdent(String), - OpenPar, - ClosePar, - OpenBracket, - CloseBracket, - OpenBrace, - CloseBrace, - Colon, - Comma, - StrLit(String), - IntLit(i64), - FloatLit(f64), - Space, - Comment, - Eol, -} - -pub fn tokenize(chars: I) -> Tokenizer -where - I: IntoIterator, -{ - Tokenizer::new(Cursor::new(chars.into_iter())) -} - -#[derive(Debug, Clone)] -pub struct Tokenizer { - cursor: Cursor, -} - -impl Tokenizer { - pub fn new(cursor: Cursor) -> Tokenizer { - Tokenizer { cursor } - } -} - -impl Iterator for Tokenizer -where - I: Iterator + Clone, -{ - type Item = Result; - - fn next(&mut self) -> Option> { - let pos = self.cursor.pos(); - let kind = match self.next_token_kind(pos) { - Ok(Some(kind)) => kind, - Ok(None) => return None, - Err(err) => return Some(Err(err)), - }; - let end = self.cursor.pos(); - Some(Ok(Token { - kind, - span: (pos, end), - })) - } -} - -impl FusedIterator for Tokenizer where I: FusedIterator + Clone {} - -const STR_BREAK_HELP: &str = concat!( - "To include a newline in a string, use \\n. ", - "To breakdown long strings over multiple lines, concatenate them." -); - -impl Tokenizer -where - I: Iterator + Clone, -{ - fn next_char(&mut self) -> Result<(Pos, char)> { - let pos = self.cursor.pos(); - let Some(c) = self.cursor.next() else { - return Err(Error::UnexpectedEndOfFile(pos)); - }; - Ok((pos, c)) - } - - fn expect_next(&mut self, c: char) -> Result<()> { - let (pos, next) = self.next_char()?; - if next != c { - Err(Error::UnexpectedChar { - pos, - expected: c, - found: next, - }) - } else { - Ok(()) - } - } - - fn next_token_kind(&mut self, start_pos: Pos) -> Result> { - let Some(c) = self.cursor.next() else { - return Ok(None); - }; - match c { - '\n' => Ok(Some(TokenKind::Eol)), - '\r' => { - self.expect_next('\n')?; - Ok(Some(TokenKind::Eol)) - } - '(' => Ok(Some(TokenKind::OpenPar)), - ')' => Ok(Some(TokenKind::ClosePar)), - '[' => Ok(Some(TokenKind::OpenBracket)), - ']' => Ok(Some(TokenKind::CloseBracket)), - '{' => Ok(Some(TokenKind::OpenBrace)), - '}' => Ok(Some(TokenKind::CloseBrace)), - ':' => Ok(Some(TokenKind::Colon)), - ',' => Ok(Some(TokenKind::Comma)), - '"' => { - let buf = self.parse_string(start_pos)?; - Ok(Some(TokenKind::StrLit(buf))) - } - '-' | '+' | '0'..='9' => { - let kind = self.parse_number(start_pos, c)?; - Ok(Some(kind)) - } - 'a'..='z' => { - let buf = self.parse_kebab_case_ident(start_pos, c)?; - Ok(Some(TokenKind::KebabCaseIdent(buf))) - } - 'A'..='Z' => { - let buf = self.parse_pascal_case_ident(start_pos, c)?; - Ok(Some(TokenKind::PascalCaseIdent(buf))) - } - '/' => { - self.expect_next('/')?; - loop { - match self.cursor.next() { - None => break, - Some('\n') => break, - Some('\r') => { - self.expect_next('\n')?; - break; - } - Some(_) => (), - } - } - Ok(Some(TokenKind::Comment)) - } - c if c.is_ascii_whitespace() => { - loop { - let c = self.cursor.first(); - match c { - Some(c) if c.is_ascii_whitespace() => { - self.cursor.next(); - } - _ => break, - } - } - Ok(Some(TokenKind::Space)) - } - _ => Ok(None), - } - } - - fn parse_esc_sequence(&mut self, start_pos: Pos) -> Result { - let Some(c) = self.cursor.next() else { - return Err(Error::UnexpectedEndOfFile(start_pos)); - }; - match c { - '\\' => Ok('\\'), - 'n' => Ok('\n'), - 'r' => Ok('\r'), - 't' => Ok('\t'), - _ => Err(Error::InvalidEscSequence((start_pos, self.cursor.pos()), c)), - } - } - - fn parse_string(&mut self, start_pos: Pos) -> Result { - let mut buf = String::new(); - loop { - let pos = self.cursor.pos(); - match self.cursor.next() { - None => return Err(Error::UnexpectedEndOfFile(pos)), - Some('"') => break, - Some('\n') => { - return Err(Error::UnterminatedString { - span: (start_pos, self.cursor.pos()), - help: STR_BREAK_HELP.to_string(), - }); - } - Some('\r') => { - self.expect_next('\n')?; - return Err(Error::UnterminatedString { - span: (start_pos, self.cursor.pos()), - help: STR_BREAK_HELP.to_string(), - }); - } - Some('\\') => { - buf.push(self.parse_esc_sequence(pos)?); - } - Some(c) => buf.push(c), - } - } - Ok(buf) - } - - fn parse_number(&mut self, start_pos: Pos, first: char) -> Result { - let mut s = String::from(first); - let mut was_e = false; - loop { - let c = self.cursor.first(); - match c { - Some(c @ ('0'..='9' | '.')) => { - self.cursor.next(); - s.push(c); - was_e = false; - } - Some(c @ ('e' | 'E')) => { - self.cursor.next(); - s.push(c); - was_e = true; - } - Some(c @ ('+' | '-')) if was_e => { - self.cursor.next(); - s.push(c); - was_e = false; - } - _ => break, - } - } - - match s.parse::() { - Ok(n) => return Ok(TokenKind::IntLit(n)), - _ => {} - } - - match s.parse::() { - Ok(n) => return Ok(TokenKind::FloatLit(n)), - _ => {} - } - - Err(Error::InvalidNumber((start_pos, self.cursor.pos()), s)) - } - - fn parse_kebab_case_ident(&mut self, start_pos: Pos, first: char) -> Result { - let mut buf = String::from(first); - let mut last_was_hyphen = false; - let mut invalid = false; - loop { - let c = self.cursor.first(); - match c { - Some(c @ ('a'..='z')) => { - self.cursor.next(); - buf.push(c); - last_was_hyphen = false; - } - Some(c @ ('0'..='9')) => { - self.cursor.next(); - buf.push(c); - last_was_hyphen = false; - } - Some(c @ ('A'..='Z')) => { - self.cursor.next(); - buf.push(c); - invalid = true; - last_was_hyphen = false; - } - Some(c @ '-') => { - if last_was_hyphen { - invalid = true; - } - self.cursor.next(); - buf.push(c); - last_was_hyphen = true; - } - Some(c @ '_') => { - invalid = true; - self.cursor.next(); - buf.push(c); - last_was_hyphen = false; - } - _ => break, - } - } - if invalid { - return Err(Error::InvalidKebabIdent( - (start_pos, self.cursor.pos()), - buf, - )); - } - Ok(buf) - } - - fn parse_pascal_case_ident(&mut self, start_pos: Pos, first: char) -> Result { - let mut buf = String::from(first); - let mut invalid = false; - loop { - let c = self.cursor.first(); - match c { - Some(c @ ('a'..='z')) => { - self.cursor.next(); - buf.push(c); - } - Some(c @ ('A'..='Z')) => { - self.cursor.next(); - buf.push(c); - } - Some(c @ ('0'..='9')) => { - self.cursor.next(); - buf.push(c); - } - Some(c @ ('-' | '_')) => { - invalid = true; - self.cursor.next(); - buf.push(c); - } - _ => break, - } - } - if invalid { - return Err(Error::InvalidPascalIdent( - (start_pos, self.cursor.pos()), - buf, - )); - } - Ok(buf) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn tokenize_str(s: &str) -> Vec { - tokenize(s.chars()).map(|r| r.unwrap().kind).collect() - } - - #[test] - fn test_kebab_case_ident() { - let toks = tokenize_str("foo-bar baz42"); - assert_eq!( - toks, - vec![ - TokenKind::KebabCaseIdent("foo-bar".into()), - TokenKind::Space, - TokenKind::KebabCaseIdent("baz42".into()) - ] - ); - } - - #[test] - fn test_pascal_case_ident() { - let toks = tokenize_str("Foo Bar42"); - assert_eq!( - toks, - vec![ - TokenKind::PascalCaseIdent("Foo".into()), - TokenKind::Space, - TokenKind::PascalCaseIdent("Bar42".into()) - ] - ); - } - - #[test] - fn test_numbers() { - let toks = tokenize_str("123 -42 3.14 +2.7e-3"); - assert_eq!( - toks, - vec![ - TokenKind::IntLit(123), - TokenKind::Space, - TokenKind::IntLit(-42), - TokenKind::Space, - TokenKind::FloatLit(3.14), - TokenKind::Space, - TokenKind::FloatLit(2.7e-3) - ] - ); - } - - #[test] - fn test_string_literal_and_escape() { - let toks = tokenize_str(r#""hello" "world\n" "foo\\bar""#); - assert_eq!( - toks, - vec![ - TokenKind::StrLit("hello".into()), - TokenKind::Space, - TokenKind::StrLit("world\n".into()), - TokenKind::Space, - TokenKind::StrLit("foo\\bar".into()) - ] - ); - } - - #[test] - fn test_structural_tokens() { - let toks = tokenize_str("{ } [ ] : ,"); - assert_eq!( - toks, - vec![ - TokenKind::OpenBrace, - TokenKind::Space, - TokenKind::CloseBrace, - TokenKind::Space, - TokenKind::OpenBracket, - TokenKind::Space, - TokenKind::CloseBracket, - TokenKind::Space, - TokenKind::Colon, - TokenKind::Space, - TokenKind::Comma - ] - ); - } - - #[test] - fn test_comments_and_eol() { - let toks = tokenize_str("// comment\nfoo\n//x\r\nbar"); - assert_eq!( - toks, - vec![ - TokenKind::Comment, - TokenKind::KebabCaseIdent("foo".into()), - TokenKind::Eol, - TokenKind::Comment, - TokenKind::KebabCaseIdent("bar".into()) - ] - ); - } - - #[test] - fn test_spaces_and_tabs() { - let toks = tokenize_str("foo \t bar"); - assert_eq!( - toks, - vec![ - TokenKind::KebabCaseIdent("foo".into()), - TokenKind::Space, - TokenKind::KebabCaseIdent("bar".into()) - ] - ); - } - - #[test] - fn test_comment_without_eol() { - let toks = tokenize_str("// bar"); - assert_eq!(toks, vec![TokenKind::Comment,]); - } -} - -#[cfg(test)] -mod fail_tests { - use super::*; - - fn tokenize_str(s: &str) -> Result> { - tokenize(s.chars()).collect() - } - - #[test] - fn test_malformed_comment() { - let toks = tokenize_str("foo: 1\n / bar"); - assert!(toks.is_err()); - assert!(matches!( - toks.unwrap_err(), - Error::UnexpectedChar { - pos: 9, - expected: '/', - found: ' ' - } - )); - } -} diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs deleted file mode 100644 index 8a2da227..00000000 --- a/dsl/src/lib.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! # plotive-dsl -//! -//! This crate provides a general-purpose and recursive DSL that describes -//! dictionaries, lists, sequences, or scalars such as numbers and strings. -//! -//! It is similar in spirit to JSON, YAML or TOML. -//! It enforces however a few conventions for naming and structuring data that -//! allow clearer and more concise descriptions. -//! -//! It is used by the [plotive](https://crates.io/crates/plotive) crate -//! to describe plots. -//! -//! Here is a simple example of a DSL document as used in plotive: -//! ```dsl -//! figure: { -//! title: "Subplots" -//! space: 10 -//! subplots: 2, 1 -//! plot: { -//! subplot: 1, 1 -//! x-axis: shared("x"), Grid -//! y-axis: "y1", Ticks -//! series: Line { -//! x-data: "x1" -//! y-data: "y1" -//! } -//! } -//! plot: { -//! subplot: 2, 1 -//! x-axis: "x", PiMultipleTicks, Grid, id("x-axis") -//! y-axis: "y2", Ticks -//! series: Line { -//! x-data: "x2" -//! y-data: "y2" -//! } -//! } -//! } -//! ``` -//! -//! Plotive DSL documents are parsed into an abstract syntax tree (AST) -//! defined in the [`ast`] module. -//! The AST can then be parsed by applications. -pub mod ast; -#[cfg(feature = "diag")] -mod diag; -mod input; -mod lex; -mod parse; - -#[cfg(feature = "diag")] -pub use diag::{DiagReport, DiagResult, DiagTrait, Diagnostic, Source}; -pub use input::Pos; -pub use lex::Span; -pub use parse::{Error, parse}; diff --git a/dsl/src/parse.rs b/dsl/src/parse.rs deleted file mode 100644 index c242f4e4..00000000 --- a/dsl/src/parse.rs +++ /dev/null @@ -1,946 +0,0 @@ -use std::fmt; - -use crate::ast; -use crate::lex::{self, Span, Token, TokenKind}; - -#[derive(Debug, Clone)] -pub enum Error { - Lex(lex::Error), - UnexpectedEndOfInput(Span), - UnexpectedToken(Token, Option), -} - -impl Error {} - -impl From for Error { - fn from(e: lex::Error) -> Self { - Error::Lex(e) - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Lex(err) => err.fmt(f), - Error::UnexpectedEndOfInput(_) => { - write!(f, "Unexpected end of input") - } - Error::UnexpectedToken(tok, expected) => { - write!(f, "Unexpected token: {:?}", tok.kind)?; - if let Some(expected) = expected { - write!(f, " (expected {})", expected)?; - } - Ok(()) - } - } - } -} - -pub type Result = std::result::Result; - -pub fn parse(input: I) -> Result> -where - I: Iterator + Clone, -{ - let tokens = lex::tokenize(input); - let mut parser = Parser::new(tokens); - parser.parse_prop_list() -} - -pub struct Parser { - tokens: T, - last_span: Span, -} - -impl Parser { - pub fn new(tokens: T) -> Self { - Self { - tokens, - last_span: Span::default(), - } - } -} - -impl Parser -where - T: Iterator> + Clone, -{ - fn parse_prop_list(&mut self) -> Result> { - let mut props = Vec::new(); - loop { - self.ignore_com_eol(); - let Some(prop) = self.parse_prop()? else { - break; - }; - props.push(prop); - } - Ok(props) - } - - fn parse_prop(&mut self) -> Result> { - self.ignore_opt_sp(); - let Some(tok) = self.first_token()? else { - return Ok(None); - }; - - let name = match tok.kind { - TokenKind::KebabCaseIdent(s) => { - self.bump_token(); - ast::Ident { - name: s, - span: tok.span, - } - } - _ => return Ok(None), - }; - - self.ignore_opt_sp(); - - // Check for optional colon and value - let value = match self.first_token()? { - Some(Token { - kind: TokenKind::Colon, - .. - }) => { - self.bump_token(); - self.ignore_opt_sp(); - Some(self.parse_prop_value()?) - } - _ => None, - }; - - self.ignore_opt_sp(); - // com-eol is handled by parse_prop_list - - Ok(Some(ast::Prop { name, value })) - } - - fn parse_prop_value(&mut self) -> Result { - let tok = self.expect_next_token()?; - - match tok.kind { - TokenKind::StrLit(s) => { - let (span, val) = self.parse_str_concatenation(tok.span, s)?; - let scalar = ast::Scalar { - span, - kind: ast::ScalarKind::Str(val), - }; - self.parse_scalar_or_seq(scalar) - } - TokenKind::IntLit(val) => { - let scalar = ast::Scalar { - span: tok.span, - kind: ast::ScalarKind::Int(val), - }; - self.parse_scalar_or_seq(scalar) - } - TokenKind::FloatLit(val) => { - let scalar = ast::Scalar { - span: tok.span, - kind: ast::ScalarKind::Float(val), - }; - self.parse_scalar_or_seq(scalar) - } - TokenKind::KebabCaseIdent(name) => { - let (end_span, func) = self.parse_func(ast::Ident { - span: tok.span, - name, - })?; - let scalar = ast::Scalar { - span: (tok.span.0, end_span), - kind: ast::ScalarKind::Func(func), - }; - self.parse_scalar_or_seq(scalar) - } - TokenKind::PascalCaseIdent(name) => { - // both struct and enums can start with a pascal case identifier - Ok(self.parse_struct_or_enum_or_seq(ast::Ident { - span: tok.span, - name, - })?) - } - TokenKind::OpenBrace => Ok(ast::Value::Struct(self.parse_struct(tok.span, None)?)), - TokenKind::OpenBracket => Ok(ast::Value::Array(self.parse_array(tok.span)?)), - _ => Err(Error::UnexpectedToken(tok, Some("value".to_string()))), - } - } - - fn parse_scalar_or_seq(&mut self, starter: ast::Scalar) -> Result { - self.ignore_opt_sp(); - match self.first_token()? { - Some(Token { - kind: TokenKind::Comma, - .. - }) => { - self.bump_token(); - } - _ => return Ok(ast::Value::Scalar(starter)), - } - - let seq = self.parse_seq(starter.span.0, Some(starter))?; - - Ok(ast::Value::Seq(seq)) - } - - fn parse_seq(&mut self, start_span: usize, starter: Option) -> Result { - debug_assert!(starter.as_ref().iter().all(|s| s.span.0 == start_span)); - - let mut res_span = starter - .as_ref() - .map(|s| s.span) - .unwrap_or((start_span, start_span)); - let mut res_scalars: Vec<_> = starter.into_iter().collect(); - - loop { - self.ignore_opt_sp(); - - match self.expect_first_token()? { - Token { - kind: TokenKind::StrLit(val), - span, - } => { - self.bump_token(); - let (span, val) = self.parse_str_concatenation(span, val)?; - let scalar = ast::Scalar { - span, - kind: ast::ScalarKind::Str(val), - }; - res_scalars.push(scalar); - res_span.1 = span.1; - } - Token { - kind: TokenKind::IntLit(val), - span, - .. - } => { - self.bump_token(); - let scalar = ast::Scalar { - span, - kind: ast::ScalarKind::Int(val), - }; - res_scalars.push(scalar); - res_span.1 = span.1; - } - Token { - kind: TokenKind::FloatLit(val), - span, - .. - } => { - self.bump_token(); - let scalar = ast::Scalar { - span, - kind: ast::ScalarKind::Float(val), - }; - res_scalars.push(scalar); - res_span.1 = span.1; - } - Token { - kind: TokenKind::KebabCaseIdent(name), - span, - } => { - self.bump_token(); - let (end_span, func) = self.parse_func(ast::Ident { span: span, name })?; - let scalar = ast::Scalar { - span: (span.0, end_span), - kind: ast::ScalarKind::Func(func), - }; - res_span.1 = scalar.span.1; - res_scalars.push(scalar); - } - Token { - kind: TokenKind::PascalCaseIdent(name), - span, - } => { - self.bump_token(); - let scalar = ast::Scalar { - span, - kind: ast::ScalarKind::Enum(name), - }; - res_scalars.push(scalar); - res_span.1 = span.1; - } - _ => (), - } - - self.ignore_opt_sp(); - match self.first_token()? { - Some(Token { - kind: TokenKind::Comma, - span, - }) => { - self.bump_token(); - res_span.1 = span.1; - } - _ => break, - } - } - - Ok(ast::Seq { - span: res_span, - scalars: res_scalars, - }) - } - - fn parse_str_concatenation( - &mut self, - start_span: Span, - starter: String, - ) -> Result<(Span, String)> { - let mut res_str = starter; - let mut res_span = start_span; - - loop { - self.ignore_com_eol(); - match self.first_token()? { - Some(Token { - kind: TokenKind::StrLit(s), - span, - }) => { - res_span.1 = span.1; - res_str.push_str(&s); - self.bump_token(); - } - _ => break, - } - } - - Ok((res_span, res_str)) - } - - fn parse_func(&mut self, ident: ast::Ident) -> Result<(usize, ast::Func)> { - self.ignore_opt_sp(); - - let open_par_span = match self.next_token()? { - Some(Token { - span, - kind: TokenKind::OpenPar, - }) => span, - Some(tok) => { - return Err(Error::UnexpectedToken( - tok, - Some("function-like value".to_string()), - )); - } - None => return Err(Error::UnexpectedEndOfInput(ident.span)), - }; - - let args = self.parse_seq(open_par_span.1, None)?; - - let close_par_span = match self.next_token()? { - Some(Token { - span, - kind: TokenKind::ClosePar, - }) => span, - Some(tok) => { - return Err(Error::UnexpectedToken( - tok, - Some("close parenthesis".to_string()), - )); - } - None => return Err(Error::UnexpectedEndOfInput(args.span)), - }; - - Ok((close_par_span.1, ast::Func { name: ident, args })) - } - - fn parse_struct_or_enum_or_seq(&mut self, ident: ast::Ident) -> Result { - self.ignore_opt_sp(); - match self.first_token()? { - Some(Token { - kind: TokenKind::OpenBrace, - .. - }) => { - self.bump_token(); - Ok(ast::Value::Struct( - self.parse_struct(ident.span, Some(ident))?, - )) - } - _ => { - let starter = ast::Scalar { - span: ident.span, - kind: ast::ScalarKind::Enum(ident.name), - }; - self.parse_scalar_or_seq(starter) - } - } - } - - fn parse_struct(&mut self, start_span: Span, typ: Option) -> Result { - let props = self.parse_prop_list()?; - match self.expect_next_token()? { - Token { - span, - kind: TokenKind::CloseBrace, - } => Ok(ast::Struct { - span: (start_span.0, span.1), - typ, - props, - }), - tok => Err(Error::UnexpectedToken(tok, Some("}".to_string()))), - } - } - - fn parse_array(&mut self, start_span: Span) -> Result { - self.ignore_com_eol(); - let array_kind = match self.expect_next_token()? { - Token { - kind: TokenKind::StrLit(val), - span, - } => { - let (_, val) = self.parse_str_concatenation(span, val)?; - self.parse_str_sequence(vec![val])? - } - Token { - kind: TokenKind::IntLit(val), - .. - } => self.parse_int_or_float_sequence(vec![val])?, - Token { - kind: TokenKind::FloatLit(val), - .. - } => self.parse_float_sequence(vec![val])?, - _ => ast::ArrayKind::Empty, - }; - self.ignore_com_eol(); - let span = self.expect_token(TokenKind::CloseBracket)?; - Ok(ast::Array { - span: (start_span.0, span.1), - kind: array_kind, - }) - } - - fn parse_int_or_float_sequence(&mut self, starter: Vec) -> Result { - let mut vec = starter; - loop { - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::Comma, - .. - } => { - self.bump_token(); - } - _ => break, - } - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::IntLit(val), - .. - } => { - self.bump_token(); - vec.push(val); - } - Token { - kind: TokenKind::FloatLit(val), - .. - } => { - self.bump_token(); - let mut fvec: Vec = vec.into_iter().map(|v| v as f64).collect(); - fvec.push(val); - return self.parse_float_sequence(fvec); - } - _ => (), - } - } - return Ok(ast::ArrayKind::Int(vec)); - } - - fn parse_float_sequence(&mut self, starter: Vec) -> Result { - let mut vec = starter; - loop { - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::Comma, - .. - } => { - self.bump_token(); - } - _ => break, - } - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::IntLit(val), - .. - } => { - self.bump_token(); - vec.push(val as f64); - } - Token { - kind: TokenKind::FloatLit(val), - .. - } => { - self.bump_token(); - vec.push(val); - } - _ => (), - } - } - return Ok(ast::ArrayKind::Float(vec)); - } - - fn parse_str_sequence(&mut self, starter: Vec) -> Result { - let mut vec = starter; - loop { - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::Comma, - .. - } => { - self.bump_token(); - } - _ => break, - } - self.ignore_com_eol(); - match self.expect_first_token()? { - Token { - kind: TokenKind::StrLit(val), - span, - } => { - self.bump_token(); - let (_, val) = self.parse_str_concatenation(span, val)?; - vec.push(val); - } - _ => (), - } - } - return Ok(ast::ArrayKind::Str(vec)); - } - - fn ignore_opt_sp(&mut self) { - loop { - match self.first_token() { - Ok(Some(Token { - kind: TokenKind::Space, - .. - })) => self.bump_token(), - _ => break, - } - } - } - - fn ignore_com_eol(&mut self) { - loop { - self.ignore_opt_sp(); - match self.first_token() { - Ok(Some(Token { - kind: TokenKind::Eol, - .. - })) => self.bump_token(), - Ok(Some(Token { - kind: TokenKind::Comment, - .. - })) => self.bump_token(), - _ => break, - } - } - } -} - -impl Parser -where - T: Iterator> + Clone, -{ - fn next_token(&mut self) -> lex::Result> { - let tok = self.tokens.next().transpose()?; - if let Some(tok) = &tok { - self.last_span = tok.span; - } - Ok(tok) - } - - fn expect_next_token(&mut self) -> Result { - let Some(tok) = self.next_token()? else { - return Err(Error::UnexpectedEndOfInput(self.last_span)); - }; - Ok(tok) - } - fn first_token(&self) -> lex::Result> { - self.tokens.clone().next().transpose() - } - - fn bump_token(&mut self) { - self.next_token().unwrap(); - } - - fn expect_first_token(&mut self) -> Result { - let Some(tok) = self.first_token()? else { - return Err(Error::UnexpectedEndOfInput(self.last_span)); - }; - Ok(tok) - } - - fn expect_token(&mut self, tok_kind: TokenKind) -> Result { - let tok = self.expect_next_token()?; - if tok.kind != tok_kind { - Err(Error::UnexpectedToken(tok, None)) - } else { - Ok(tok.span) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_empty() { - let dsl = ""; - let props = parse(dsl.chars()).unwrap(); - assert!(props.is_empty()); - } - - #[test] - fn test_empty_prop() { - let dsl = "foo"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props, - &[ast::Prop { - name: ast::Ident { - name: "foo".to_string(), - span: (0, 3,), - }, - value: None, - }] - ); - } - - #[test] - fn test_int() { - let dsl = "foo: 1234"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props, - &[ast::Prop { - name: ast::Ident { - name: "foo".to_string(), - span: (0, 3,), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (5, 9,), - kind: ast::ScalarKind::Int(1234), - })), - }] - ); - } - - #[test] - fn test_float() { - let dsl = "foo: 12.34"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props, - &[ast::Prop { - name: ast::Ident { - name: "foo".to_string(), - span: (0, 3,), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (5, 10,), - kind: ast::ScalarKind::Float(12.34), - })), - }] - ); - } - - #[test] - fn test_str() { - let dsl = "foo: \"string\""; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props, - &[ast::Prop { - name: ast::Ident { - name: "foo".to_string(), - span: (0, 3,), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (5, 13,), - kind: ast::ScalarKind::Str("string".into()), - })), - }] - ); - } - - #[test] - fn test_str_concatenation() { - let dsl = r#"foo: "a" "b" "c""#; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Scalar(ast::Scalar { - span: (5, 16), - kind: ast::ScalarKind::Str("abc".into()), - })) - ); - } - - #[test] - fn test_enum_value() { - let dsl = "foo: Bar"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props, - &[ast::Prop { - name: ast::Ident { - name: "foo".to_string(), - span: (0, 3), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (5, 8), - kind: ast::ScalarKind::Enum("Bar".into()), - })), - }] - ); - } - - #[test] - fn test_seq_of_scalars() { - let dsl = "foo: 1, 2, 3"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Seq(ast::Seq { - span: (5, 12), - scalars: vec![ - ast::Scalar { - span: (5, 6), - kind: ast::ScalarKind::Int(1), - }, - ast::Scalar { - span: (8, 9), - kind: ast::ScalarKind::Int(2), - }, - ast::Scalar { - span: (11, 12), - kind: ast::ScalarKind::Int(3), - }, - ] - })) - ); - } - - #[test] - fn test_seq_with_func() { - let dsl = "foo: 1, fun(2, \"3\"), 4"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Seq(ast::Seq { - span: (5, 22), - scalars: vec![ - ast::Scalar { - span: (5, 6), - kind: ast::ScalarKind::Int(1), - }, - ast::Scalar { - span: (8, 19), - kind: ast::ScalarKind::Func(ast::Func { - name: ast::Ident { - name: "fun".into(), - span: (8, 11), - }, - args: ast::Seq { - span: (12, 18), - scalars: vec![ - ast::Scalar { - span: (12, 13), - kind: ast::ScalarKind::Int(2), - }, - ast::Scalar { - span: (15, 18), - kind: ast::ScalarKind::Str("3".into()), - } - ] - } - }), - }, - ast::Scalar { - span: (21, 22), - kind: ast::ScalarKind::Int(4), - }, - ] - })) - ); - } - - #[test] - fn test_array_of_ints() { - let dsl = "foo: [1, 2, 3]"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Array(ast::Array { - span: (5, 14), - kind: ast::ArrayKind::Int(vec![1, 2, 3]), - })) - ); - } - - #[test] - fn test_array_of_floats() { - let dsl = "foo: [1.1, 2.2, 3.3]"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Array(ast::Array { - span: (5, 20,), - kind: ast::ArrayKind::Float(vec![1.1, 2.2, 3.3]), - })) - ); - } - - #[test] - fn test_array_of_strings() { - let dsl = r#"foo: ["a", "b", "c"]"#; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Array(ast::Array { - span: (5, 20,), - kind: ast::ArrayKind::Str(vec!["a".into(), "b".into(), "c".into()]), - })) - ); - } - - #[test] - fn test_struct_with_type() { - let dsl = "foo: Bar { baz: 1 }"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Struct(ast::Struct { - span: (5, 19), - typ: Some(ast::Ident { - name: "Bar".into(), - span: (5, 8), - }), - props: vec![ast::Prop { - name: ast::Ident { - name: "baz".into(), - span: (11, 14), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (16, 17), - kind: ast::ScalarKind::Int(1), - })), - }] - })) - ); - } - - #[test] - fn test_struct_without_type() { - let dsl = "foo: { bar: 2 }"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Struct(ast::Struct { - span: (5, 15), - typ: None, - props: vec![ast::Prop { - name: ast::Ident { - name: "bar".into(), - span: (7, 10), - }, - value: Some(ast::Value::Scalar(ast::Scalar { - span: (12, 13), - kind: ast::ScalarKind::Int(2), - })), - }] - })) - ); - } - - #[test] - fn test_nested_seq() { - let dsl = "foo: { bar: 2, \"3\" }"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Struct(ast::Struct { - span: (5, 20), - typ: None, - props: vec![ast::Prop { - name: ast::Ident { - name: "bar".into(), - span: (7, 10), - }, - value: Some(ast::Value::Seq(ast::Seq { - span: (12, 18), - scalars: vec![ - ast::Scalar { - span: (12, 13), - kind: ast::ScalarKind::Int(2), - }, - ast::Scalar { - span: (15, 18), - kind: ast::ScalarKind::Str("3".into()), - } - ] - })), - }] - })) - ); - } - - #[test] - fn test_empty_struct() { - let dsl = "foo: { }"; - let props = parse(dsl.chars()).unwrap(); - assert_eq!( - props[0].value, - Some(ast::Value::Struct(ast::Struct { - span: (5, 8), - typ: None, - props: vec![], - })) - ); - } - - #[test] - fn test_comments_and_eol() { - let dsl = r#" -// comment -foo: 1 - -bar: 2 // another comment -"#; - let props = parse(dsl.chars()).unwrap(); - assert_eq!(props.len(), 2); - assert_eq!(props[0].name.name, "foo"); - assert_eq!(props[1].name.name, "bar"); - } -} - -#[cfg(test)] -mod fail_tests { - use super::*; - - #[test] - fn test_unterminated_string() { - let dsl = "foo: \"bar\nbaz"; - let res = parse(dsl.chars()); - assert!(res.is_err()); - let err = res.unwrap_err(); - assert!(matches!( - err, - Error::Lex(lex::Error::UnterminatedString { .. }) - )); - assert!(err.to_string().contains("Unterminated string")); - } - - #[test] - fn test_unterminated_struct() { - let dsl = "foo: {\n bar: 1\n"; - let res = parse(dsl.chars()); - assert!(res.is_err()); - let err = res.unwrap_err(); - assert!(matches!(err, Error::UnexpectedEndOfInput(_))); - assert!(err.to_string().contains("Unexpected end of input")); - } -} diff --git a/examples/bode-rlc.plotive b/examples/bode-rlc.plotive deleted file mode 100644 index e9fe5eaf..00000000 --- a/examples/bode-rlc.plotive +++ /dev/null @@ -1,16 +0,0 @@ -figure: { - title: "Bode diagram of RLC circuit\n[size=18;italic;font=serif]L = 0.1 mH / C = 1 µF[/size;italic;font]" - legend: Right - plot: { - x-axis: shared("freq"), Ticks, MinorTicks - y-axis: "Magnitude [italic]\\[dB][/italic]", Ticks, Grid - } - plot: { - x-axis: "Freq. [italic]\\[Hz][/italic]", id("freq"), LogScale, Ticks, MinorTicks - y-axis: { - title: "Phase [italic]\\[rad][/italic]" - ticks: PiMultiple - grid - } - } -} diff --git a/examples/bode_rlc_dsl.rs b/examples/bode_rlc_dsl.rs deleted file mode 100644 index a642a8ca..00000000 --- a/examples/bode_rlc_dsl.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::f64::consts::PI; - -use plotive::{data, des, dsl, utils}; - -mod common; - -/// Computes the transfer function of a series RLC circuit, with output across the capacitor. -/// The input vector is the frequencies in Hz -/// The returned vectors are the magnitude in dB and the phase in radians -fn rlc_load_response(frequencies: &[f64], r: f64, l: f64, c: f64) -> (Vec, Vec) { - let mut mags = Vec::with_capacity(frequencies.len()); - let mut phases = Vec::with_capacity(frequencies.len()); - - for &f in frequencies { - let pulse = 2.0 * PI * f; - - let num = 1.0; - let denom_real = 1.0 - pulse * pulse * l * c; - let denom_imag = pulse * r * c; - - let mag = num / (denom_real.powi(2) + denom_imag.powi(2)).sqrt(); - let ph = -(denom_imag / denom_real).atan(); - - mags.push(20.0 * mag.log10()); - phases.push(ph); - } - - (mags, phases) -} - -fn main() { - const L: f64 = 1e-4; // 100 µH - const C: f64 = 1e-6; // 1 uF - - let series = [ - (1.0, "mag1", "phase1", "R = 1 Ω"), - (10.0, "mag2", "phase2", "R = 10 Ω"), - (100.0, "mag3", "phase3", "R = 100 Ω"), - ]; - - let mut source = data::NamedOwnedColumns::new(); - - let filename = common::example_res("bode-rlc.plotive"); - let content = std::fs::read_to_string(&filename).unwrap(); - - let figs = dsl::parse_diag(&content, Some(&filename)).unwrap(); - let mut fig = figs.into_iter().next().unwrap(); - - let freq = utils::logspace(100.0, 1000000.0, 500); - for (r, mag_col, phase_col, name) in series { - let (mag, phase) = rlc_load_response(&freq, r, L, C); - - source.add_column(mag_col, Box::new(mag)); - source.add_column(phase_col, Box::new(phase)); - - let plots = fig.plots_mut(); - plots.plot_mut((0, 0)).unwrap().push_series( - des::series::Line::new(des::data_src_ref("freq"), des::data_src_ref(mag_col)) - .with_name(name) - .into(), - ); - plots.plot_mut((1, 0)).unwrap().push_series( - des::series::Line::new(des::data_src_ref("freq"), des::data_src_ref(phase_col)).into(), - ); - } - source.add_column("freq", Box::new(freq)); - common::save_figure(&fig, &source, None, "bode_rlc_dsl"); -} diff --git a/examples/iris.plotive b/examples/iris.plotive deleted file mode 100644 index 3137469f..00000000 --- a/examples/iris.plotive +++ /dev/null @@ -1,45 +0,0 @@ -// figure is one of the root properties. -// it can be assigned multiple times to generate multiple figures -figure: { - title: "Iris dataset" - plot: Plot { - // 'x-axis' is a property of type 'Axis' - // the type is optional though as it can be inferred from - // the property name - x-axis: Axis { - title: "Sepal Length \\[cm]" - // properties without value get default values - ticks - grid - } - - // axis can also be specified as a sequence - // Ticks and Grid are enums, not property name - // (hence the capital) - y-axis: "Petal Length \\[cm]", Ticks, Grid - - // the legend is constructed from its position - // and get default values for other properties - legend: InBottomRight - - // multiple series are created by assigning multiple times - // the 'series' property - series: Scatter { - name: "Setosa" - // a string assigned to a data property - // is a reference to a column in an external data source - x-data: "setosa_sepal_length" - y-data: "setosa_petal_length" - } - series: Scatter { - name: "Virginica" - x-data: "virginica_sepal_length" - y-data: "virginica_petal_length" - } - series: Scatter { - name: "Versicolor" - x-data: "versicolor_sepal_length" - y-data: "versicolor_petal_length" - } - } -} diff --git a/examples/iris_dsl.rs b/examples/iris_dsl.rs deleted file mode 100644 index 5ea94e6c..00000000 --- a/examples/iris_dsl.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::path; - -use plotive::data::Source; -use plotive::{data, dsl}; - -mod common; - -fn iris_csv_path() -> path::PathBuf { - let iris_csv = path::Path::new(file!()); - let parent = iris_csv.parent().unwrap(); - parent.join("Iris.csv") -} - -/// Returns a boolean mask where the column matches the given category -/// Returns None if the column is not string-like -fn category_mask(column: &C, category: &str) -> Option> -where - C: data::Column + ?Sized, -{ - let mask = column - .str()? - .str_iter() - .map(|v| v == Some(category)) - .collect(); - Some(mask) -} - -/// Filters a numeric column by a boolean mask -/// Returns None if the column is not numeric and panics if the lengths do not match -fn filter_numeric_by_mask(num_col: &C, mask: &[bool]) -> Option -where - C: data::Column + ?Sized, -{ - assert_eq!(num_col.len(), mask.len()); - - let vec: Vec = num_col - .f64()? - .f64_iter() - .zip(mask.iter()) - .filter_map(|(v, &m)| if m { Some(v) } else { None }) - .map(|v| v.unwrap_or(f64::NAN)) - .collect(); - Some(vec.into()) -} - -fn main() { - let iris_csv = iris_csv_path(); - let csv_data = std::fs::read_to_string(&iris_csv).unwrap(); - - let table = data::csv::parse_str(&csv_data, Default::default()).unwrap(); - - let species = table.column("Species").unwrap(); - let sepal_length = table.column("SepalLengthCm").unwrap(); - let petal_length = table.column("PetalLengthCm").unwrap(); - - let setosa_mask = category_mask(species, "Iris-setosa").unwrap(); - let versicolor_mask = category_mask(species, "Iris-versicolor").unwrap(); - let virginica_mask = category_mask(species, "Iris-virginica").unwrap(); - - let setosa_sepal_length = filter_numeric_by_mask(sepal_length, &setosa_mask).unwrap(); - let setosa_petal_length = filter_numeric_by_mask(petal_length, &setosa_mask).unwrap(); - - let versicolor_sepal_length = filter_numeric_by_mask(sepal_length, &versicolor_mask).unwrap(); - let versicolor_petal_length = filter_numeric_by_mask(petal_length, &versicolor_mask).unwrap(); - - let virginica_sepal_length = filter_numeric_by_mask(sepal_length, &virginica_mask).unwrap(); - let virginica_petal_length = filter_numeric_by_mask(petal_length, &virginica_mask).unwrap(); - - let mut source = data::NamedColumns::new(); - - source.add_column( - "setosa_sepal_length", - &setosa_sepal_length as &dyn data::Column, - ); - source.add_column( - "setosa_petal_length", - &setosa_petal_length as &dyn data::Column, - ); - - source.add_column( - "versicolor_sepal_length", - &versicolor_sepal_length as &dyn data::Column, - ); - source.add_column( - "versicolor_petal_length", - &versicolor_petal_length as &dyn data::Column, - ); - - source.add_column( - "virginica_sepal_length", - &virginica_sepal_length as &dyn data::Column, - ); - source.add_column( - "virginica_petal_length", - &virginica_petal_length as &dyn data::Column, - ); - - let file_name = file!(); - let dsl_file = path::Path::new(file_name) - .parent() - .unwrap() - .join("iris.plotive"); - let dsl = std::fs::read_to_string(&dsl_file).unwrap(); - - let figs = dsl::parse_diag(&dsl, Some(&dsl_file)).unwrap(); - common::save_figure(&figs[0], &source, None, "iris_dsl"); -} diff --git a/examples/multiple_axes.plotive b/examples/multiple_axes.plotive deleted file mode 100644 index af312ed8..00000000 --- a/examples/multiple_axes.plotive +++ /dev/null @@ -1,20 +0,0 @@ -figure: { - title: "Multiple axes" - legend - plot: { - x-axis: "x", PiMultipleTicks - y-axis: "sin(x)", Ticks - y-axis: "exp(x)", Ticks, LogScale - series: Line { - name: "sin(x)" - x-data: "x" - y-data: "sin(x)" - } - series: Line { - name: "exp(x)" - x-data: "x" - y-data: "exp(x)" - y-axis: "exp(x)" - } - } -} diff --git a/examples/multiple_axes_dsl.rs b/examples/multiple_axes_dsl.rs deleted file mode 100644 index f0a089e0..00000000 --- a/examples/multiple_axes_dsl.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::f64::consts::PI; - -use plotive::{data, dsl, utils}; - -mod common; - -fn main() { - let x = utils::linspace(0.0, 6.0 * PI, 500); - let sin_x = x.iter().map(|x| x.sin()).collect::>(); - let exp_x = x.iter().map(|x| x.exp()).collect::>(); - - let mut data_src = data::NamedColumns::new(); - data_src.add_column("x", &x as &dyn data::Column); - data_src.add_column("sin(x)", &sin_x as &dyn data::Column); - data_src.add_column("exp(x)", &exp_x as &dyn data::Column); - - let filename = common::example_res("multiple_axes.plotive"); - let content = std::fs::read_to_string(&filename).unwrap(); - let figs = dsl::parse_diag(&content, Some(&filename)).unwrap(); - - common::save_figure(&figs[0], &data_src, None, "multiple_axes_dsl"); -} diff --git a/examples/subplots.plotive b/examples/subplots.plotive deleted file mode 100644 index d1ef847c..00000000 --- a/examples/subplots.plotive +++ /dev/null @@ -1,23 +0,0 @@ -figure: { - title: "Subplots" - space: 10 - subplots: 2, 1 - plot: { - subplot: 1, 1 - x-axis: shared("x"), Grid - y-axis: "y1", Ticks - series: Line { - x-data: "x1" - y-data: "y1" - } - } - plot: { - subplot: 2, 1 - x-axis: "x", PiMultipleTicks, Grid, id("x-axis") - y-axis: "y2", Ticks - series: Line { - x-data: "x2" - y-data: "y2" - } - } -} diff --git a/examples/subplots_dsl.rs b/examples/subplots_dsl.rs deleted file mode 100644 index 9fbdfaee..00000000 --- a/examples/subplots_dsl.rs +++ /dev/null @@ -1,24 +0,0 @@ -use plotive::{data, dsl, utils}; - -mod common; - -use std::f64::consts::PI; - -fn main() { - let x1 = utils::linspace(0.0, 2.0 * PI, 400); - let y1: Vec = x1.iter().map(|x| (x * x).sin()).collect(); - let x2 = utils::linspace(0.5 * PI, 2.5 * PI, 400); - let y2: Vec = x1.iter().map(|x| -(x * x).sin()).collect(); - - let mut data_source = data::NamedColumns::new(); - data_source.add_column("x1", &x1 as &dyn data::Column); - data_source.add_column("y1", &y1 as &dyn data::Column); - data_source.add_column("x2", &x2 as &dyn data::Column); - data_source.add_column("y2", &y2 as &dyn data::Column); - - let filename = common::example_res("subplots.plotive"); - let content = std::fs::read_to_string(&filename).unwrap(); - let figs = dsl::parse_diag(&content, Some(&filename)).unwrap(); - - common::save_figure(&figs[0], &data_source, None, "subplots_dsl"); -} diff --git a/run_all_examples.sh b/run_all_examples.sh index e04e6a42..6369ba67 100755 --- a/run_all_examples.sh +++ b/run_all_examples.sh @@ -9,21 +9,17 @@ cargo run --example text_line --package plotive-text --features noto-sans cargo run --example text_rich --package plotive-text --features noto-sans,noto-serif # passing all needed feature to each example to avoid unnecessary recompilation -features="data-csv,dsl,noto-sans,noto-serif-italic,serde,time,utils" +features="data-csv,noto-sans,noto-serif-italic,serde,time,utils" cargo run --example area --features $features -- $@ cargo run --example bars --features $features -- $@ cargo run --example bitcoin --features $features -- $@ cargo run --example bode_rlc --features $features -- $@ -cargo run --example bode_rlc_dsl --features $features -- $@ cargo run --example bouncing_ball --features $features -- $@ cargo run --example gauss --features $features -- $@ cargo run --example iris --features $features -- $@ -cargo run --example iris_dsl --features $features -- $@ cargo run --example minimal --features $features -- $@ cargo run --example multiple_axes --features $features -- $@ -cargo run --example multiple_axes_dsl --features $features -- $@ cargo run --example sine --features $features -- $@ cargo run --example stars --features $features -- $@ cargo run --example subplots --features $features -- $@ -cargo run --example subplots_dsl --features $features -- $@ diff --git a/src/dsl.rs b/src/dsl.rs deleted file mode 100644 index 00e342bc..00000000 --- a/src/dsl.rs +++ /dev/null @@ -1,929 +0,0 @@ -//! Plotive DSL parser -//! -//! Plotive-DSL is a domain-specific language for defining plots and figures. -//! The DSL is based on the [plotive-dsl](https://crates.io/crates/plotive-dsl) crate. -//! A `*.plotive*` file contains one or more [`des::Figure`] definitions. -//! -//! Here is a simple example that defines a figure with two subplots, sharing the x-axis: -//! ```dsl -//! figure: { -//! title: "Subplots" -//! space: 10 -//! subplots: 2, 1 -//! plot: { -//! subplot: 1, 1 -//! x-axis: shared("x"), Grid -//! y-axis: "y1", Ticks -//! series: Line { -//! x-data: "x1" -//! y-data: "y1" -//! } -//! } -//! plot: { -//! subplot: 2, 1 -//! x-axis: "x", PiMultipleTicks, Grid, id("x-axis") -//! y-axis: "y2", Ticks -//! series: Line { -//! x-data: "x2" -//! y-data: "y2" -//! } -//! } -//! } -//! ``` -//! -//! The main entry points are the [`parse`] and [`parse_diag`] functions. -//! The former returns a simple `Result`, while the latter returns -//! rich diagnostics powered by [`miette`](https://crates.io/crates/miette), -//! printable to console in case of errors. -//! -//! Here is an example of what can be printed in case of errors: -//! ```text -//! × unknown axis property enum: PiMultipleTcks -//! ╭─[/home/remi/dev/plotive/examples/subplots.plotive:16:22] -//! 15 │ subplot: 2, 1 -//! 16 │ x-axis: "x", PiMultipleTcks, Grid, id("x-axis") -//! · ───────┬────── -//! · ╰── unknown axis property enum: PiMultipleTcks -//! 17 │ y-axis: "y2", Ticks -//! ╰──── -//! ``` -#![allow(unused)] -use std::{fmt, path}; - -use plotive_dsl::{self, Span, ast}; -pub use plotive_dsl::{Diagnostic, Source}; - -use crate::text::{self, ParseRichTextError, ParsedRichText}; -use crate::{des, style}; - -/// Errors that can occur during EPLT parsing -#[derive(Debug, Clone)] -pub enum Error { - /// DSL parsing error - Dsl(plotive_dsl::Error), - /// Rich text parsing error with offset - ParseRichText(usize, ParseRichTextError), - /// General parse error - Parse { - /// Span of the error - span: Span, - /// Reason for the error - reason: String, - /// Optional help message - help: Option, - }, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Dsl(err) => err.fmt(f), - Error::ParseRichText(_, err) => err.fmt(f), - Error::Parse { reason, help, .. } => { - write!(f, "Parse error: {reason}")?; - if let Some(help) = help { - write!(f, "\nHelp: {help}")?; - } - Ok(()) - } - } - } -} - -impl From for Error { - fn from(err: plotive_dsl::Error) -> Self { - Error::Dsl(err) - } -} - -impl plotive_dsl::DiagTrait for Error { - fn span(&self) -> Span { - match self { - Error::Dsl(err) => err.span(), - Error::ParseRichText(offset, err) => { - let span = err.span(); - (span.0 + *offset, span.1 + *offset) - } - Error::Parse { span, .. } => *span, - } - } - - fn message(&self) -> String { - match self { - Error::Dsl(err) => err.message(), - Error::ParseRichText(_, err) => format!("{err}"), - Error::Parse { reason, .. } => format!("{reason}"), - } - } - - fn help(&self) -> Option { - match self { - Error::Dsl(err) => err.help(), - Error::ParseRichText(..) => None, - Error::Parse { help, .. } => help.clone(), - } - } -} - -/// Parse EPLT DSL input into a list of design figures -pub fn parse>(input: S) -> Result, Error> { - let props = plotive_dsl::parse(input.as_ref().chars())?; - - let mut figs = vec![]; - for prop in props { - if prop.name.name == "figure" { - figs.push(parse_fig(expect_struct_val(prop)?)?); - } else { - return Err(Error::Parse { - span: prop.span(), - reason: format!("unknown top-level property: {}", prop.name.name), - help: None, - }); - } - } - - Ok(figs) -} - -/// Parse EPLT DSL input into a list of design figures, returning diagnostics on error. -pub fn parse_diag<'a>( - input: &'a str, - file_name: Option<&'a path::Path>, -) -> plotive_dsl::DiagResult> { - match parse(input) { - Ok(figs) => Ok(figs), - Err(err) => { - let src = Source { - name: file_name.map(|s| s.to_str().unwrap_or("(non-utf8 filename)").to_string()), - src: input.to_string(), - }; - let diag = Diagnostic::new(Box::new(err), src); - let report = plotive_dsl::DiagReport::new(diag); - Err(report) - } - } -} - -fn expect_int_scalar(scalar: ast::Scalar) -> Result { - let ast::Scalar { - kind: ast::ScalarKind::Int(val), - .. - } = scalar - else { - return Err(Error::Parse { - span: scalar.span, - reason: "expected integer value".to_string(), - help: None, - }); - }; - Ok(val) -} - -fn expect_float_val(prop: ast::Prop) -> Result { - match prop.value { - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Float(val), - .. - })) => Ok(val), - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Int(val), - .. - })) => Ok(val as f64), - _ => Err(Error::Parse { - span: prop.span(), - reason: format!("expected float value (i.e. {}: 2.0 )", prop.name.name), - help: None, - }), - } -} - -fn expect_string_val(prop: ast::Prop) -> Result<(Span, String), Error> { - let Some(ast::Value::Scalar(ast::Scalar { - span, - kind: ast::ScalarKind::Str(val), - })) = prop.value - else { - return Err(Error::Parse { - span: prop.span(), - reason: format!("expected string value (i.e. {}: \"...\" )", prop.name.name), - help: None, - }); - }; - Ok((span, val)) -} - -fn expect_axis_ref_val(prop: ast::Prop) -> Result { - match prop.value { - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Str(val), - .. - })) => Ok(des::axis::Ref::Id(val)), - - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Int(val), - .. - })) => Ok(des::axis::Ref::Idx(val as usize)), - - _ => Err(Error::Parse { - span: prop.span(), - reason: format!("expected string value (i.e. {}: \"...\" )", prop.name.name), - help: None, - }), - } -} - -fn expect_struct_val(prop: ast::Prop) -> Result { - let Some(ast::Value::Struct(val)) = prop.value else { - return Err(Error::Parse { - span: prop.span(), - reason: format!("expected struct value (i.e. {}: {{ ... }}", prop.name.name), - help: None, - }); - }; - Ok(val) -} - -fn check_opt_type(val: &ast::Struct, type_name: &str) -> Result<(), Error> { - if let Some(typ) = &val.typ { - if typ.name != type_name { - return Err(Error::Parse { - span: typ.span, - reason: format!( - "expected struct of type '{type_name}', found '{}'", - typ.name - ), - help: Some(format!( - "In this case, '{type_name}' can be inferred from context and is optional" - )), - }); - } - } - Ok(()) -} - -fn parse_fig(mut val: ast::Struct) -> Result { - check_opt_type(&val, "Figure")?; - - let mut row_cols: Option<(u32, u32)> = None; - let mut plots = vec![]; - - while let Some(prop) = val.take_prop("plot") { - let (rc, plot) = parse_plot(expect_struct_val(prop)?)?; - match (rc, &mut row_cols) { - (None, None) => (), - (Some(rc), Some(row_cols)) => { - row_cols.0 = rc.0.max(row_cols.0); - row_cols.1 = rc.1.max(row_cols.1); - } - (Some(rc), None) => row_cols = Some(rc), - (None, Some(..)) => (), - } - plots.push((rc, plot)); - } - - let nplots = plots.len() as u32; - while let Some(prop) = val.take_prop("subplots") { - let span = prop.span(); - let rc = parse_subplots_val(prop.value)?; - if let Some(row_cols) = row_cols { - if rc.0 < row_cols.0 || rc.1 < row_cols.1 { - return Err(Error::Parse { - span, - reason: "figure subplots value is incompatible with the plots subplot values" - .to_string(), - help: Some( - "You may want to only use figure subplots or only plot subplot".to_string(), - ), - }); - } - } - } - - if row_cols.is_none() { - row_cols = Some((nplots, 1)); - } - let row_cols = row_cols.unwrap(); - - let plots = if nplots == 1 && row_cols == (1, 1) { - let (_, plot) = plots.into_iter().next().unwrap(); - plot.into() - } else { - let (rows, cols) = row_cols; - let mut subplots = des::Subplots::new(rows, cols); - // dsl has rows and cols starting at 1, - // but des has rows and cols starting at 0 - let mut row = 0; - let mut col = 0; - for (rc, plot) in plots { - let (r, c) = match rc { - Some((r, c)) => (r - 1, c - 1), - None => (row, col), - }; - subplots = subplots.with_plot((r, c), plot); - row += 1; - if row >= rows { - row = 0; - col += 1; - } - } - if let Some(prop) = val.take_prop("space") { - subplots = subplots.with_space(expect_float_val(prop)? as _); - } - subplots.into() - }; - - let mut fig = des::Figure::new(plots); - - for prop in val.props { - match prop.name.name.as_str() { - "title" => { - todo!("delete this module") - } - "legend" => { - fig = fig.with_legend(parse_fig_legend(prop.value)?); - } - // Subplots props that were not parsed for single plot - // or stated multiple times for subplots. - // We just ignore them. - "cols" | "space" | "share-x" | "share-y" => (), - _ => { - return Err(Error::Parse { - span: prop.span(), - reason: format!("Unknown figure property: '{}'", prop.name.name), - help: None, - }); - } - } - } - - Ok(fig) -} - -fn parse_subplots_val(value: Option) -> Result<(u32, u32), Error> { - match value { - Some(ast::Value::Seq(ast::Seq { scalars, span })) => { - if scalars.len() == 2 { - let mut scalars = scalars.into_iter(); - let rows = expect_int_scalar(scalars.next().unwrap())? as u32; - let cols = expect_int_scalar(scalars.next().unwrap())? as u32; - Ok((rows, cols)) - } else { - Err(Error::Parse { - span, - reason: "Expected 2 values for subplot size or position".into(), - help: None, - }) - } - } - Some(_) => Err(Error::Parse { - span: value.as_ref().unwrap().span(), - reason: "Could not parse subplot size or position".into(), - help: None, - }), - None => Ok((1, 1)), - } -} - -fn parse_fig_legend(value: Option) -> Result { - let mut legend = des::FigLegend::default(); - - match value { - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - })) => match ident.as_str() { - "Top" => legend = legend.with_pos(des::figure::LegendPos::Top), - "Right" => legend = legend.with_pos(des::figure::LegendPos::Right), - "Bottom" => legend = legend.with_pos(des::figure::LegendPos::Bottom), - "Left" => legend = legend.with_pos(des::figure::LegendPos::Left), - _ => { - return Err(Error::Parse { - span, - reason: format!("unknown legend position: {}", ident), - help: None, - }); - } - }, - Some(_) => { - return Err(Error::Parse { - span: value.as_ref().unwrap().span(), - reason: "Could not parse legend".into(), - help: None, - }); - } - None => (), - } - - Ok(legend) -} - -fn parse_plot(mut val: ast::Struct) -> Result<(Option<(u32, u32)>, des::plot::Plot), Error> { - check_opt_type(&val, "Plot")?; - - let mut series = vec![]; - loop { - let Some(prop) = val.take_prop("series") else { - break; - }; - series.push(parse_series(expect_struct_val(prop)?)?); - } - let mut row_cols = None; - let mut plot = des::Plot::new(series); - - for prop in val.props { - match prop.name.name.as_str() { - "subplot" => { - row_cols = Some(parse_subplots_val(prop.value)?); - } - "x-axis" => plot = plot.with_x_axis(parse_axis(prop, false)?), - "y-axis" => plot = plot.with_y_axis(parse_axis(prop, true)?), - "title" => todo!("delete this module"), - "legend" => plot = plot.with_legend(parse_plot_legend(prop.value)?), - _ => { - return Err(Error::Parse { - span: prop.span(), - reason: format!("Unknown plot property: '{}'", prop.name.name), - help: None, - }); - } - } - } - - Ok((row_cols, plot)) -} - -fn parse_plot_legend(value: Option) -> Result { - let mut legend = des::plot::PlotLegend::default(); - - match value { - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - })) => match ident.as_str() { - "OutTop" | "Top" => legend = legend.with_pos(des::plot::LegendPos::OutTop), - "OutRight" | "Right" => legend = legend.with_pos(des::plot::LegendPos::OutRight), - "OutBottom" | "Bottom" => legend = legend.with_pos(des::plot::LegendPos::OutBottom), - "OutLeft" | "Left" => legend = legend.with_pos(des::plot::LegendPos::OutLeft), - "InTop" => legend = legend.with_pos(des::plot::LegendPos::InTop), - "InTopRight" => legend = legend.with_pos(des::plot::LegendPos::InTopRight), - "InRight" => legend = legend.with_pos(des::plot::LegendPos::InRight), - "InBottomRight" => legend = legend.with_pos(des::plot::LegendPos::InBottomRight), - "InBottom" => legend = legend.with_pos(des::plot::LegendPos::InBottom), - "InBottomLeft" => legend = legend.with_pos(des::plot::LegendPos::InBottomLeft), - "InLeft" => legend = legend.with_pos(des::plot::LegendPos::InLeft), - "InTopLeft" => legend = legend.with_pos(des::plot::LegendPos::InTopLeft), - _ => { - return Err(Error::Parse { - span, - reason: format!("unknown legend position: {}", ident), - help: None, - }); - } - }, - Some(_) => { - return Err(Error::Parse { - span: value.as_ref().unwrap().span(), - reason: "Could not parse legend".into(), - help: None, - }); - } - None => (), - } - - Ok(legend) -} - -fn parse_series(val: ast::Struct) -> Result { - let Some(ident) = &val.typ else { - return Err(Error::Parse { - span: val.span, - reason: "series type can't be inferred and must be speficied".into(), - help: None, - }); - }; - - match ident.name.as_str() { - "Line" => Ok(parse_line(val)?.into()), - "Scatter" => Ok(parse_scatter(val)?.into()), - "Histogram" => Ok(parse_histogram(val)?.into()), - "Bars" => Ok(parse_bars(val)?.into()), - "BarsGroup" => Ok(parse_bars_group(val)?.into()), - _ => Err(Error::Parse { - span: ident.span, - reason: format!("unknown series type: {}", ident.name), - help: None, - }), - } -} - -fn expect_prop(val: &mut ast::Struct, name: &str) -> Result { - val.take_prop(name).ok_or(Error::Parse { - span: val.span, - reason: format!("expected '{name}' property"), - help: None, - }) -} - -fn expect_data_prop(val: &mut ast::Struct, prop_name: &str) -> Result { - let prop = expect_prop(val, prop_name)?; - match prop.value { - Some(ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Str(val), - .. - })) => Ok(des::DataCol::SrcRef(val)), - Some(ast::Value::Array(ast::Array { - kind: ast::ArrayKind::Int(vals), - .. - })) => Ok(des::DataCol::Inline(vals.into())), - Some(ast::Value::Array(ast::Array { - kind: ast::ArrayKind::Float(vals), - .. - })) => Ok(des::DataCol::Inline(vals.into())), - Some(ast::Value::Array(ast::Array { - kind: ast::ArrayKind::Str(vals), - .. - })) => Ok(des::DataCol::Inline(vals.into())), - _ => Err(Error::Parse { - span: prop.span(), - reason: format!("Could not parse '{prop_name}' as a data column"), - help: None, - }), - } -} - -fn parse_line(mut val: ast::Struct) -> Result { - let x_data = expect_data_prop(&mut val, "x-data")?; - let y_data = expect_data_prop(&mut val, "y-data")?; - - let mut line = des::series::Line::new(x_data, y_data); - - if let Some(prop) = val.take_prop("name") { - line = line.with_name(expect_string_val(prop)?.1); - } - if let Some(prop) = val.take_prop("x-axis") { - line = line.with_x_axis(expect_axis_ref_val(prop)?); - } - if let Some(prop) = val.take_prop("y-axis") { - line = line.with_y_axis(expect_axis_ref_val(prop)?); - } - - Ok(line) -} - -fn parse_scatter(mut val: ast::Struct) -> Result { - let x_data = expect_data_prop(&mut val, "x-data")?; - let y_data = expect_data_prop(&mut val, "y-data")?; - - let mut series = des::series::Scatter::new(x_data, y_data); - - if let Some(prop) = val.take_prop("name") { - series = series.with_name(expect_string_val(prop)?.1); - } - if let Some(prop) = val.take_prop("x-axis") { - series = series.with_x_axis(expect_axis_ref_val(prop)?); - } - if let Some(prop) = val.take_prop("y-axis") { - series = series.with_y_axis(expect_axis_ref_val(prop)?); - } - - Ok(series) -} - -fn parse_histogram(mut val: ast::Struct) -> Result { - let data = expect_data_prop(&mut val, "data")?; - - let mut series = des::series::Histogram::new(data); - - if let Some(prop) = val.take_prop("name") { - series = series.with_name(expect_string_val(prop)?.1); - } - if let Some(prop) = val.take_prop("x-axis") { - series = series.with_x_axis(expect_axis_ref_val(prop)?); - } - if let Some(prop) = val.take_prop("y-axis") { - series = series.with_y_axis(expect_axis_ref_val(prop)?); - } - - Ok(series) -} - -fn parse_bars(mut val: ast::Struct) -> Result { - let x_data = expect_data_prop(&mut val, "x-data")?; - let y_data = expect_data_prop(&mut val, "y-data")?; - - let mut bars = des::series::Bars::new(x_data, y_data); - - if let Some(prop) = val.take_prop("name") { - bars = bars.with_name(expect_string_val(prop)?.1); - } - - Ok(bars) -} - -fn parse_bars_group(_val: ast::Struct) -> Result { - todo!() -} - -fn parse_axis(prop: ast::Prop, is_y: bool) -> Result { - let Some(val) = prop.value else { - return Ok(Default::default()); - }; - match val { - ast::Value::Scalar(ast::Scalar { .. }) => { - todo!("delete this module") - } - - ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - }) => axis_set_enum_field(Default::default(), is_y, span, ident.as_str()), - - ast::Value::Seq(seq) => parse_axis_seq(seq, is_y), - - ast::Value::Struct(val) => parse_axis_struct(val, is_y), - - _ => Err(Error::Parse { - span: val.span(), - reason: "Could not parse axis".into(), - help: None, - }), - } -} - -fn axis_set_enum_field( - axis: des::Axis, - is_y: bool, - span: Span, - ident: &str, -) -> Result { - match ident { - "LogScale" => Ok(axis.with_scale(des::axis::LogScale::default().into())), - "Ticks" => Ok(axis.with_ticks(Default::default())), - "PiMultipleTicks" => Ok(axis.with_ticks( - des::axis::Ticks::default() - .with_locator(des::axis::ticks::PiMultipleLocator::default().into()), - )), - "MinorTicks" => Ok(axis.with_minor_ticks(Default::default())), - "Grid" => Ok(axis.with_grid(Default::default())), - "MinorGrid" => Ok(axis.with_minor_grid(Default::default())), - "MainSide" | "OppositeSide" | "LeftSide" | "RightSide" | "TopSide" | "BottomSide" => { - axis_set_side_enum(axis, is_y, span, ident) - } - _ => Err(Error::Parse { - span, - reason: format!("unknown axis property enum: {}", ident), - help: None, - }), - } -} - -fn axis_set_side_enum( - axis: des::Axis, - is_y: bool, - span: Span, - ident: &str, -) -> Result { - match ident { - "MainSide" => Ok(axis), - "OppositeSide" => Ok(axis.with_opposite_side()), - "LeftSide" if is_y => Ok(axis), - "RightSide" if is_y => Ok(axis.with_opposite_side()), - "TopSide" if !is_y => Ok(axis.with_opposite_side()), - "BottomSide" if !is_y => Ok(axis), - "LeftSide" | "RightSide" if !is_y => Err(Error::Parse { - span, - reason: format!("axis side '{}' is invalid for x-axis", ident), - help: Some("Valid enums are BottomSide and MainSide (default) as well as TopSide and OppositeSide".into()), - }), - "TopSide" | "BottomSide" if is_y => Err(Error::Parse { - span, - reason: format!("axis side '{}' is invalid for y-axis", ident), - help: Some("Valid enums are LeftSide and MainSide (default) as well as RightSide and OppositeSide".into()), - }), - _ => unreachable!(), - } -} - -fn axis_set_side_prop( - axis: des::Axis, - is_y: bool, - span: Span, - ident: &str, -) -> Result { - match ident { - "main-side" => Ok(axis), - "opposote-side" => Ok(axis.with_opposite_side()), - "left-side" if is_y => Ok(axis), - "right-side" if is_y => Ok(axis.with_opposite_side()), - "top-side" if !is_y => Ok(axis.with_opposite_side()), - "bottom-side" if !is_y => Ok(axis), - "left-side" | "right-side" if !is_y => Err(Error::Parse { - span, - reason: format!("axis property '{}' is invalid for x-axis", ident), - help: Some("Valid side properties are bottom-side or main-side (default) as well as top-side or opposite-side".into()), - }), - "top-side" | "bottom-side" if is_y => Err(Error::Parse { - span, - reason: format!("axis property '{}' is invalid for y-axis", ident), - help: Some("Valid side properties are left-side or main-side (default) as well as right-side or opposite-side".into()), - }), - _ => unreachable!(), - } -} - -fn parse_axis_seq(seq: ast::Seq, is_y: bool) -> Result { - let mut axis = des::Axis::default(); - for scalar in seq.scalars { - match scalar { - ast::Scalar { .. } => { - todo!("delete this module") - } - ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - } => axis = axis_set_enum_field(axis, is_y, span, ident.as_str())?, - ast::Scalar { - kind: ast::ScalarKind::Func(ast::Func { name, args }), - span, - } => { - let mut args_iter = args.scalars.into_iter(); - let arg1 = args_iter.next(); - if name.name == "id" { - let id = match arg1 { - Some(ast::Scalar { - kind: ast::ScalarKind::Str(id), - .. - }) => id, - _ => { - return Err(Error::Parse { - span, - reason: "Could not parse axis id".into(), - help: Some("Expected a single string argument".to_string()), - }); - } - }; - axis = axis.with_id(id); - } else if name.name == "shared" { - let ax_ref = match arg1 { - Some(ast::Scalar { - kind: ast::ScalarKind::Str(id), - .. - }) => des::axis::Ref::Id(id), - Some(ast::Scalar { - kind: ast::ScalarKind::Int(idx), - .. - }) => des::axis::Ref::Idx(idx as usize), - _ => { - return Err(Error::Parse { - span, - reason: "Could not parse axis shared reference".into(), - help: Some( - "Expected a single string or integer argument".to_string(), - ), - }); - } - }; - axis = axis.with_scale(des::axis::Scale::Shared(ax_ref)); - } else { - return Err(Error::Parse { - span, - reason: "Unknown axis attribute".into(), - help: None, - }); - } - if args_iter.next().is_some() { - return Err(Error::Parse { - span, - reason: format!("Too many arguments for {}", name.name), - help: None, - }); - } - } - _ => { - return Err(Error::Parse { - span: seq.span, - reason: "Could not parse axis".into(), - help: None, - }); - } - } - } - Ok(axis) -} - -fn parse_axis_struct(val: ast::Struct, is_y: bool) -> Result { - check_opt_type(&val, "Axis")?; - let mut axis = des::Axis::default(); - for prop in val.props { - match prop.name.name.as_str() { - "title" => { - todo!("delete this module") - } - "ticks" => { - axis = axis.with_ticks(parse_ticks(prop)?); - } - "minor-ticks" => { - axis = axis.with_minor_ticks(Default::default()); - } - "grid" => { - axis = axis.with_grid(Default::default()); - } - "minor-grid" => { - axis = axis.with_minor_grid(Default::default()); - } - "main-side" | "opposite-side" | "left-side" | "right-side" | "top-side" - | "bottom-side" => { - axis = axis_set_side_prop(axis, is_y, prop.span(), prop.name.name.as_str())?; - } - _ => { - return Err(Error::Parse { - span: prop.span(), - reason: format!("unknown axis property: {}", prop.name.name), - help: None, - }); - } - } - } - Ok(axis) -} - -fn parse_ticks(prop: ast::Prop) -> Result { - let Some(val) = prop.value else { - return Ok(Default::default()); - }; - match val { - ast::Value::Scalar(ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - }) => Ok(ticks_set_enum_field( - des::axis::Ticks::default(), - span, - &ident, - )?), - ast::Value::Seq(val) => parse_ticks_seq(val), - ast::Value::Struct(val) => parse_ticks_struct(val), - _ => Err(Error::Parse { - span: val.span(), - reason: "Could not parse ticks".into(), - help: None, - }), - } -} - -fn parse_ticks_seq(val: ast::Seq) -> Result { - let mut ticks = des::axis::Ticks::default(); - for scalar in val.scalars { - match scalar { - ast::Scalar { - kind: ast::ScalarKind::Enum(ident), - span, - } => ticks = ticks_set_enum_field(ticks, span, ident.as_str())?, - _ => { - return Err(Error::Parse { - span: val.span, - reason: "Could not parse ticks".into(), - help: None, - }); - } - } - } - Ok(ticks) -} - -fn ticks_set_enum_field( - ticks: des::axis::Ticks, - span: Span, - ident: &str, -) -> Result { - match ident { - "Locator" => Ok(ticks.with_locator(des::axis::ticks::Locator::default())), - "PiMultiple" => { - Ok(ticks.with_locator(des::axis::ticks::PiMultipleLocator::default().into())) - } - _ => Err(Error::Parse { - span, - reason: format!("unknown ticks property enum: {}", ident), - help: None, - }), - } -} - -fn parse_ticks_struct(val: ast::Struct) -> Result { - check_opt_type(&val, "Ticks")?; - let mut ticks = des::axis::Ticks::default(); - for prop in val.props { - match prop.name.name.as_str() { - "locator" => { - ticks = ticks.with_locator(des::axis::ticks::Locator::default()); - } - _ => { - return Err(Error::Parse { - span: prop.span(), - reason: format!("unknown ticks property: {}", prop.name.name), - help: None, - }); - } - } - } - Ok(ticks) -} diff --git a/src/lib.rs b/src/lib.rs index 2b917d7d..319bef6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,6 @@ * ## Crate features * * - `data-csv`: enables CSV data source support (See [`data::csv`]) - * - `dsl`: enables the support for `.plotive` DSL. (See [`dsl`] and [`plotive-dsl` crate](https://crates.io/crates/plotive-dsl)) * - `noto-mono`, `noto-sans`, `noto-sans-italic`, `noto-serif`, `noto-serif-italic`: bundles the corresponding fonts from Google in the final executable, and enables `plotive::bundled_font_db()`.
* `noto-sans` is enabled by default * - `time`: enables support for time series, CSV date-time parsing etc. (See [`time`]) @@ -153,9 +152,6 @@ pub mod drawing; pub mod render; pub mod style; -#[cfg(feature = "dsl")] -pub mod dsl; - #[cfg(feature = "time")] pub mod time;