commit c45853486777b489adfaa03c2334836646fd09f4
parent 26c383a4f19380cad5ea86274b3ceee2ea2d4f3b
Author: Demonstrandum <samuel@knutsen.co>
Date: Sat, 13 Jul 2024 15:01:18 +0100
Added `(%os/env ...)` and `(%format ...)` macros.
* `(%os/env ENV_VAR)` looks up an environment variable `$ENV_VAR` and
replace the call with a string containing its contents.
* `(%format fmt-string var1 var2 :named var3 ...)` uses the Rust `format!`
syntax to replace placeholders, either positional (`"... {} ..."`) or named
(`"... {name} ..."`) variants via listed arguments or via
keyword/attribute arguments.
Diffstat:
11 files changed, 142 insertions(+), 60 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -78,6 +78,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396a0a312bef78b5f62b0251d7162c4b8af162949b8b104d2967e41b26c1b68c"
[[package]]
+name = "formatx"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db0f0c49aba98a3b2578315766960bd242885ff672fd62610c5557cd6c6efe03"
+
+[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -167,6 +173,7 @@ dependencies = [
"chrono",
"colored",
"descape",
+ "formatx",
"unicode-width",
]
diff --git a/Cargo.toml b/Cargo.toml
@@ -25,3 +25,4 @@ colored = "2.1"
chrono = "0.4"
unicode-width = "0.1.12"
descape = "1.1.2"
+formatx = "0.2.2"
diff --git a/README.md b/README.md
@@ -23,7 +23,8 @@ Read the [USAGE.md](USAGE.md) file for code examples and documentation.
- XML (`--xml`; including: SVG, MathML)
- HTML (`--html`; SGML)
- CSS (`--css`)
- - SEXP (`--sexp`; S-expression, basically a macro expansion utility)
+ - SExp (`--sexp`; S-expression, basically a macro expansion utility)
+ - Plain Text (`--text`; renders escaped strings to text)
### Installation
@@ -35,7 +36,7 @@ cargo build --release
cargo install --path .
```
-Or install it from crates.io
+Or install it from [crates.io](https://crates.io/crates/seam)
```sh
cargo install seam
```
@@ -89,36 +90,39 @@ seam --sexp <<< '(hello (%define subject world) %subject)'
# (hello world)
```
-## TODO
- - Escape evaluating macros with `\%`.
- - `(%format "{}")` macro with Rust's `format` syntax.
- - Implement lexical scope by letting macros store a copy of the scope they were defined in (or a reference?).
- - `(%embed "/path")` macro, like `%include`, but just returns the file contents as a string.
- - Variadic arguments via `&rest` syntax.
- - Delayed evaluation of macros by `%(...)` synatx.
- For example `%(f x y)` is the same as `(%f x y)`, so you can have `(%define uneval f x)` and then write `%(%uneval y)`.
- - `%list` macro which expands from `(p (%list a b c))` to `(p a b c)`.
- Defined as such:
- ```lisp
- (%define (list &rest) rest)
- ```
- - `%for`-loop macro, iterating over `%list`s.
- - `%glob` which returns a list of files/directories matching a glob.
- - `%markdown` renders Markdown given to it as html.
- - `%html`, `%xml`, `%css`, etc. macros which goes into the specific rendering mode.
- - Add variadic and keyword macro arguments.
- - Caching or checking time-stamps as to not regenerate unmodified source files.
- - HTML object `style="..."` object should handle s-expressions well, (e.g. `(p :style (:color red :border none) Hello World)`)
- - Add more supported formats (`JSON`, `JS`, `TOML`, &c.).
- - Maybe: a whole JavaScript front-end, e.g.
+## Checklist
+ - [ ] First argument (of body) in a macro invocation should have its whitespace stripped.
+ - [x] `(%os/env ENV_VAR)` environment variable macro.
+ - [ ] `(%to-string ...)`, `(%join ...)`, `(%map ...)`, `(%filter ...)` macros.
+ - [ ] Escape evaluating macros with `\%`.
+ - [x] `(%format "{}")` macro with Rust's `format` syntax. e.g. `(%format "Hello {}, age {age:0>2}" "Sam" :age 9)`
+ - [ ] Implement lexical scope by letting macros store a copy of the scope they were defined in (or a reference?).
+ - [ ] `(%embed "/path")` macro, like `%include`, but just returns the file contents as a string.
+ - [ ] Variadic arguments via `&rest` syntax.
+ - [ ] Delayed evaluation of macros by `%(...)` syntax.
+ [ ] For example `%(f x y)` is the same as `(%f x y)`, so you can have `(%define uneval f x)` and then write `%(%uneval y)`.
+ - [ ] `%list` macro which expands from `(p (%list a b c))` to `(p a b c)`.
+ Defined as such:
+ ```lisp
+ (%define (list &rest) rest)
+ ```
+ - [ ] `%for`-loop macro, iterating over `%list`s.
+ - [ ] `%glob` which returns a list of files/directories matching a glob.
+ - [ ] `%markdown` renders Markdown given to it as html.
+ - [ ] `%html`, `%xml`, `%css`, etc. macros which goes into the specific rendering mode.
+ - [ ] Add variadic and keyword macro arguments.
+ - [ ] Caching or checking time-stamps as to not regenerate unmodified source files.
+ - [ ] HTML object `style="..."` object should handle s-expressions well, (e.g. `(p :style (:color red :border none) Hello World)`)
+ - [ ] Add more supported formats (`JSON`, `JS`, `TOML`, &c.).
+ - [ ] Maybe: a whole JavaScript front-end, e.g.
```lisp
(let x 2)
(let (y 1) (z 1))
(const f (=> (a b) (+ a b))
((. console log) (== (f y z) x))
```
- - Add more helpful/generic macros (e.g. `(%include ...)`, which already exists).
- - Allow for arbitrary embedding of code, that can be run by
+ - [ ] Add more helpful/generic macros (e.g. `(%include ...)`, which already exists).
+ - [ ] Allow for arbitrary embedding of code, that can be run by
a LISP interpreter (or any other langauge), for example. (e.g. `(%chez (+ 1 2))` executes
`(+ 1 2)` with Chez-Scheme LISP, and places the result in the source
(i.e. `3`).
diff --git a/samples/js-concept.sex b/samples/js-concept.sex
@@ -26,7 +26,6 @@
;const points = [];
;const r = 0.4;
-
(import * as BC from "../lib/BasicCanvas.js")
(import { grid, ellipse } from "../lib/BasicCanvas.js")
@@ -51,7 +50,3 @@
(const points [])
(const r 0.4)
-
-
-
-
diff --git a/samples/xml-example-1.sex b/samples/xml-example-1.sex
@@ -3,5 +3,3 @@
(from Jani)
(heading A reminder \(Again!\) for you)
(body Don't \"forget\" me this weekend!))
-
-
diff --git a/samples/xml-example-2.sex b/samples/xml-example-2.sex
@@ -21,6 +21,3 @@
Two eggs, bacon or sausage, toast,
and our ever-popular hash browns.)
(calories "950")))
-
-
-
diff --git a/src/assemble/sexp.rs b/src/assemble/sexp.rs
@@ -35,11 +35,11 @@ impl<'a> SExpFormatter<'a> {
write!(f, "{}", node.value)?;
},
ParseNode::String(node) => {
- // We actually don't want the rendered string,
- // we want the escaped string, so we retrieve
- // it from source.
+ // We actually don't want the rendered string with the escpaes
+ // that we parsed, so we send it to the debug view to get a string
+ // with quotes and escaped special characters.
write!(f, "{}", node.leading_whitespace)?;
- write!(f, "{}", node.site.view())?;
+ write!(f, "{:?}", node.value)?;
},
ParseNode::List { nodes, leading_whitespace, end_token, .. } => {
write!(f, "{}", leading_whitespace)?;
diff --git a/src/bin.rs b/src/bin.rs
@@ -82,7 +82,6 @@ fn main() -> Result<(), Box<dyn Error>> {
generate_and_print(&builder, &target, is_doc);
}
-
Ok(())
}
diff --git a/src/parse/expander.rs b/src/parse/expander.rs
@@ -14,6 +14,7 @@ use std::{
};
use colored::*;
+use formatx;
use unicode_width::UnicodeWidthStr;
/// Error type for errors while expanding macros.
@@ -95,7 +96,7 @@ impl<'a> Expander<'a> {
}
/// Get underlying source-code of the active parser for current unit.
- pub fn get_source(&'a self) -> &'a str {
+ pub fn get_source(&self) -> &str {
self.parser.get_source()
}
@@ -229,7 +230,7 @@ impl<'a> Expander<'a> {
node.site().to_owned()));
}
let symbol = if let Some(node) = params[0].atomic() {
- node.value
+ node.value.to_owned()
} else {
// FIXME: Borrow-checker won't let me use params[0].site() as site!
return Err(ExpansionError(
@@ -342,7 +343,7 @@ impl<'a> Expander<'a> {
/// `(%log ...)` logs to `STDERR` when called and leaves *no* node behind.
/// This means whitespace preceeding `(%log ...)` will be removed!
- fn expand_log_macro(&'a self, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+ fn expand_log_macro(&'a self, node: &'a ParseNode<'a>, params: ParseTree<'a>)
-> Result<ParseTree<'a>, ExpansionError<'a>> {
let mut words = Vec::with_capacity(params.len());
for param in self.expand_nodes(params)? {
@@ -360,7 +361,86 @@ impl<'a> Expander<'a> {
Ok(Box::new([]))
}
- fn expand_macro(&'a self, name: &str, node: &'a ParseNode<'a>, params: Box<[ParseNode<'a>]>)
+ fn expand_os_env_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>)
+ -> Result<ParseTree<'a>, ExpansionError<'a>> {
+ let [ref var] = *params else {
+ return Err(ExpansionError::new(
+ "`%os/env' expects excatly one argument.",
+ node.site()));
+ };
+ let Some(var) = var.atomic() else {
+ return Err(ExpansionError::new(
+ "`%os/env' argument must be atomic (not a list).",
+ var.site()));
+ };
+ let Node { site, leading_whitespace, .. } = var.clone();
+ let Ok(value) = std::env::var(&var.value) else {
+ return Err(ExpansionError(
+ format!("No such environment variable ($`{}') visible.", &var.value),
+ site));
+ };
+ Ok(Box::new([
+ ParseNode::String(Node { value, site, leading_whitespace }),
+ ]))
+ }
+
+ fn expand_format_macro(&self, node: &ParseNode<'a>, params: ParseTree<'a>)
+ -> Result<ParseTree<'a>, ExpansionError<'a>> {
+ let [format_str, ..] = &*params else {
+ return Err(ExpansionError::new(
+ "`%format' expects at a format-string.",
+ node.site()));
+ };
+ let ParseNode::String(format_str) = format_str else {
+ return Err(ExpansionError::new(
+ "First argument to `%format' must be a string.",
+ format_str.site()));
+ };
+ // Iterate and collect format arguments.
+ let mut arguments = params.iter();
+ let _ = arguments.next(); // Skip the format-string.
+ let Ok(mut template) = formatx::Template::new(&format_str.value) else {
+ return Err(ExpansionError::new(
+ "Invalid format string.",
+ &format_str.site));
+ };
+ for mut var in arguments {
+ // Check if we're replacing a named or positional placeholder.
+ let mut named: Option<&str> = None;
+ if let ParseNode::Attribute { keyword, node, .. } = var {
+ named = Some(keyword.as_str());
+ var = node;
+ }
+ // TODO: Somehow let non-atomic values be formattable?
+ let Some(Node { value, .. }) = var.atomic() else {
+ return Err(ExpansionError(
+ format!("In `%format', the compound {} type is not formattable.",
+ var.node_type()),
+ var.site().clone()));
+ };
+ // Replace the placeholder.
+ match named {
+ Some(name) => template.replace(name, value),
+ None => template.replace_positional(value),
+ }
+ }
+ // Template has been constructed, so now attempt to do subsitituions and
+ // render the formatted string.
+ match template.text() {
+ Ok(value) => Ok(Box::new([
+ ParseNode::String(Node {
+ value,
+ site: node.owned_site(),
+ leading_whitespace: node.leading_whitespace().to_owned(),
+ })
+ ])),
+ Err(err) => Err(ExpansionError(
+ format!("Failed to format string: {}", err.message()),
+ format_str.site.clone()))
+ }
+ }
+
+ fn expand_macro(&'a self, name: &str, node: &'a ParseNode<'a>, params: ParseTree<'a>)
-> Result<ParseTree<'a>, ExpansionError<'a>> {
// Eagerly evaluate parameters passed to macro invocation.
let params = self.expand_nodes(params)?;
@@ -410,6 +490,8 @@ impl<'a> Expander<'a> {
"include" => self.expand_include_macro(node, params),
"date" => self.expand_date_macro(node, params),
"log" => self.expand_log_macro(node, params),
+ "format" => self.expand_format_macro(node, params),
+ "os/env" => self.expand_os_env_macro(node, params),
_ => self.expand_macro(name, node, params),
}
}
@@ -449,7 +531,7 @@ impl<'a> Expander<'a> {
// Pathway: (%_ _ _) macro invocation.
if let Some(ref symbol@ParseNode::Symbol(..)) = head {
let node = self.register_invocation(node.clone());
- let name = symbol.atomic().unwrap().value;
+ let name = symbol.atomic().unwrap().value.clone();
if name.starts_with("%") {
// Rebuild node...
let name = &name[1..];
diff --git a/src/parse/lexer.rs b/src/parse/lexer.rs
@@ -54,7 +54,6 @@ pub struct Lexer {
byte_offset_line: Cell<usize>,
}
-
impl<'a> Lexer {
pub fn new(source_path: String, source: String) -> Self {
Self {
@@ -66,18 +65,18 @@ impl<'a> Lexer {
}
}
- pub fn get_source(&'a self) -> &'a str {
+ pub fn get_source(&self) -> &str {
&self.source
}
- fn increment_byte_offsets(&'a self, offset: usize) {
+ fn increment_byte_offsets(&self, offset: usize) {
let i = self.byte_offset.get();
let j = self.byte_offset_line.get();
self.byte_offset.set(i + offset);
self.byte_offset_line.set(j + offset);
}
- fn next_line(&'a self) {
+ fn next_line(&self) {
let l = self.line.get();
self.line.set(l + 1);
self.byte_offset_line.set(0);
diff --git a/src/parse/parser.rs b/src/parse/parser.rs
@@ -41,19 +41,19 @@ pub enum ParseNode<'a> {
}
impl<'a> ParseNode<'a> {
- pub fn symbolic(&self) -> Option<Node> {
+ pub fn symbolic(&self) -> Option<&Node<'a>> {
match self {
- Self::Symbol(node)
- | Self::Number(node) => Some(node.to_owned()),
+ Self::Symbol(ref node)
+ | Self::Number(ref node) => Some(node),
_ => None
}
}
- pub fn atomic(&self) -> Option<Node> {
+ pub fn atomic(&self) -> Option<&Node<'a>> {
match self {
- Self::Symbol(node)
- | Self::Number(node)
- | Self::String(node) => Some(node.to_owned()),
+ Self::Symbol(ref node)
+ | Self::Number(ref node)
+ | Self::String(ref node) => Some(node),
_ => None
}
}
@@ -68,7 +68,7 @@ impl<'a> ParseNode<'a> {
}
}
- pub fn owned_site(&self) -> Site {
+ pub fn owned_site(&self) -> Site<'a> {
match self {
Self::Symbol(node)
| Self::Number(node)
@@ -78,7 +78,7 @@ impl<'a> ParseNode<'a> {
}
}
- pub fn leading_whitespace(&'a self) -> &'a str {
+ pub fn leading_whitespace(&self) -> &str {
match self {
Self::Symbol(ref node)
| Self::Number(ref node)
@@ -142,7 +142,7 @@ impl<'a> Parser {
Self { lexer }
}
- pub fn get_source(&'a self) -> &'a str {
+ pub fn get_source(&self) -> &str {
self.lexer.get_source()
}