seam

Symbolic-Expressions As Markup.
git clone git://git.knutsen.co/seam
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 7+++++++
MCargo.toml | 1+
MREADME.md | 56++++++++++++++++++++++++++++++--------------------------
Msamples/js-concept.sex | 5-----
Msamples/xml-example-1.sex | 2--
Msamples/xml-example-2.sex | 3---
Msrc/assemble/sexp.rs | 8++++----
Msrc/bin.rs | 1-
Msrc/parse/expander.rs | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/parse/lexer.rs | 7+++----
Msrc/parse/parser.rs | 20++++++++++----------
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() }