boxworks_lang/
lib.rs

1//! # Boxworks language
2//!
3//! This crate defines a domain-specific language (DSL) for Boxworks.
4//! This language is used to describe Boxworks elements in Knuth's box-and-glue model.
5//! The initial motivation for the language is to make it
6//!     easy to create Boxworks primitives,
7//!     like horizontal and vertical lists,
8//!     for use in unit testing.
9//!
10//! In the long run, the language will probably support running the Boxworks engine
11//!     and actually performing typesetting.
12//! If this happens, this language will be a sort of "intermediate representation"
13//!     for the Texcraft project.
14//!
15//! This is a basic example of converting some of the language into a
16//! horizontal list:
17//!
18//! ```
19//! use boxworks::ds;
20//! use boxworks_lang as bwl;
21//!
22//! let source = r#"
23//!     ## The chars() function typesets characters.
24//!     chars("Box")
25//!     ## Glue can be added manually.
26//!     glue(1pt, 5fil, 0.075in)
27//!     ## The following elements illustrate the prototypical example of a kern.
28//!     chars("A")
29//!     kern(-0.1pt)
30//!     chars("V")
31//! "#;
32//! let got = bwl::parse_horizontal_list(&source);
33//! let want: Vec<ds::Horizontal> = vec![
34//!     ds::Char{char: 'B', font: 0}.into(),
35//!     ds::Char{char: 'o', font: 0}.into(),
36//!     ds::Char{char: 'x', font: 0}.into(),
37//!     ds::Glue{
38//!         kind: ds::GlueKind::Normal,
39//!         value: common::Glue{
40//!             width: common::Scaled::new(
41//!                 1,  // integer part
42//!                 common::Scaled::ZERO,  // fractional part
43//!                 common::ScaledUnit::Point,  // units
44//!             ).unwrap(),
45//!             stretch: common::Scaled::new(
46//!                 5,  // integer part
47//!                 common::Scaled::ZERO,  // fractional part
48//!                 common::ScaledUnit::Point,  // units
49//!             ).unwrap(),
50//!             stretch_order: common::GlueOrder::Fil,
51//!             shrink: common::Scaled::new(
52//!                 0,  // integer part
53//!                 common::Scaled::from_decimal_digits(&[0, 7, 5]),  // fractional part
54//!                 common::ScaledUnit::Inch,  // units
55//!             ).unwrap(),
56//!             shrink_order: common::GlueOrder::Normal,
57//!         }
58//!     }.into(),
59//!     ds::Char{char: 'A', font: 0}.into(),
60//!     ds::Kern{
61//!         kind: ds::KernKind::Normal,
62//!         width: -common::Scaled::new(
63//!                 0,  // integer part
64//!                 common::Scaled::from_decimal_digits(&[1]),  // fractional part
65//!                 common::ScaledUnit::Point,  // units
66//!             ).unwrap(),
67//!     }.into(),
68//!     ds::Char{char: 'V', font: 0}.into(),
69//! ];
70//! assert_eq![got, Ok(want)];
71//! ```
72//!
73//! The main takeaway from this example is that you start with a very
74//! terse description of the horizontal list, and the library outputs
75//! the long and tedious Rust struct definitions.
76//!
77//! ## Language specification
78//!
79//! A Boxworks language program is a sequence of a function calls
80//! like `chars("ABC")` or `glue(10pt, 3pt, 2pt)`.
81//! Most function calls add an item or items to the current
82//! box-and-glue list.
83//!
84//! ### Function arguments
85//!
86//! Each function accepts a number of arguments.
87//! For simplicity, every argument to every function is optional.
88//!
89//! Arguments can be provided positionally:
90//!
91//! ```
92//! # use boxworks_lang as bwl;
93//! # use boxworks::ds;
94//! let source = r#"
95//!     chars("A", 1)
96//! "#;
97//! assert_eq![
98//!     bwl::parse_horizontal_list(&source),
99//!     Ok(vec![ds::Char{char: 'A', font: 1}.into()])
100//! ];
101//! ```
102//!
103//! Or by keyword, potentially out of order:
104//!
105//! ```
106//! # use boxworks_lang as bwl;
107//! # use boxworks::ds;
108//! let source = r#"
109//!     chars(font=2, content="B")
110//! "#;
111//! assert_eq![
112//!     bwl::parse_horizontal_list(&source),
113//!     Ok(vec![ds::Char{char: 'B', font: 2}.into()])
114//! ];
115//! ```
116//!
117//! Or by a combination of positional and by keyword:
118//!
119//! ```
120//! # use boxworks_lang as bwl;
121//! # use boxworks::ds;
122//! let source = r#"
123//!     chars("C", font=3)
124//! "#;
125//! assert_eq![
126//!     bwl::parse_horizontal_list(&source),
127//!     Ok(vec![ds::Char{char: 'C', font: 3}.into()])
128//! ];
129//! ```
130//!
131//! However, all positional arguments must be provided before
132//! keyword arguments:
133//!
134//! ```
135//! # use boxworks_lang as bwl;
136//! # use boxworks::ds;
137//! let source = r#"
138//!     chars(content="C", 3)
139//! "#;
140//! let errs = bwl::parse_horizontal_list(&source).unwrap_err();
141//! assert![matches![
142//!     errs[0],
143//!     bwl::Error::PositionalArgAfterKeywordArg{..}
144//! ]];
145//! ```
146//!
147//! ### Function argument types
148//!
149//! Every function argument expects a specific concrete type.
150//! These are the types:
151//!
152//! | Name | Description | Examples
153//! |------|-------------|---------
154//! | String | Arbitrary UTF-8 characters between double quotes. Currently the string can't contain a double quote character. | `"a string"`
155//! | Integer | Decimal integer in the range (-2^31,2^31). | `123`, `-456`
156//! | Dimension | Decimal number with a unit attached. The format and the allowable units are the same as in TeX. | `1pt`, `2.04in`, `-10sp`
157//! | Glue stretch or shrink | A dimension where the unit can alternatively be an infinite stretch/shrink unit. | `1fil`, `-2fill`, `3filll`
158//!
159//! ### Available functions
160//!
161//! More functions will be added over time.
162//! These are the currently supported functions.
163//!
164//! #### `glue`: add a glue node to the current list
165//!
166//! Adds a value of the Rust type [`boxworks::ds::Glue`]
167//! to the current list.
168//!
169//! Parameters:
170//!
171//! | Number | Name    | Type      | Default |
172//! |--------|---------|-----------|---------|
173//! | 1      | `width` | dimension | `0pt`   |
174//! | 2      | `stretch` | glue stretch or shrink | `0pt`   |
175//! | 3      | `shrink` | glue stretch or shrink | `0pt`   |
176//!
177//! #### `kern`: add a kern node to the current list
178//!
179//! Adds a value of the Rust type [`boxworks::ds::Kern`]
180//! to the current list.
181//!
182//! Parameters:
183//!
184//! | Number | Name    | Type      | Default |
185//! |--------|---------|-----------|---------|
186//! | 1      | `width` | dimension | `0pt`   |
187//!
188//! #### `text`: typeset some text
189//!
190//! The goal of this function is to add character and glue nodes
191//! like TeX does when it is processing normal text.
192//! In TeX this is actually a complicated process that includes:
193//!
194//! - Adding kerns between characters.
195//! - Applying ligature rules.
196//! - Adjusting the space factor that determines the size of inter-word glue.
197//!
198//! Right now the `text` function is much simpler.
199//! It iterates over all characters in the string and:
200//!
201//! - For space characters, adds a glue node corresponding
202//!   to the glue `10pt plus 4pt minus 4pt`.
203//!
204//! - For all other characters, adds a value of the Rust type
205//!   [`boxworks::ds::Char`].
206//!
207//! Parameters:
208//!
209//! | Number | Name    | Type      | Default |
210//! |--------|---------|-----------|---------|
211//! | 1      | `content` | string | `""`   |
212//! | 2      | `font` | integer | `0`   |
213pub mod ast;
214pub mod convert;
215pub mod cst;
216mod error;
217pub mod lexer;
218use convert::ToBoxworks;
219pub use error::{Error, ErrorAccumulator, ErrorLabel};
220
221use boxworks::ds;
222
223/// String type used in the crate's public API.
224#[derive(Debug, Clone)]
225pub struct Str<'a> {
226    value: &'a str,
227    start: usize,
228    end: usize,
229}
230
231impl<'a> Str<'a> {
232    fn new(value: &'a str) -> Str<'a> {
233        Str {
234            value,
235            start: 0,
236            end: value.len(),
237        }
238    }
239    fn span(&self) -> std::ops::Range<usize> {
240        self.start..self.end
241    }
242    fn str(&self) -> &'a str {
243        &self.value[self.span()]
244    }
245    fn is_empty(&self) -> bool {
246        self.start == self.end
247    }
248}
249
250impl<'a> From<&'a str> for Str<'a> {
251    fn from(value: &'a str) -> Self {
252        Str {
253            value,
254            start: 0,
255            end: value.len(),
256        }
257    }
258}
259
260impl<'a> std::fmt::Display for Str<'a> {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        write!(f, "{}", self.str())
263    }
264}
265
266impl<'a> PartialEq for Str<'a> {
267    fn eq(&self, other: &Self) -> bool {
268        println!("Checking {} == {}", self.str(), other.str());
269        self.str() == other.str()
270    }
271}
272
273impl<'a> Eq for Str<'a> {}
274
275/// Pretty-format Box source code.
276pub fn format(source: &str) -> Result<String, Vec<error::Error<'_>>> {
277    let errs: ErrorAccumulator = Default::default();
278    let l = lexer::Lexer::new(source, errs.clone());
279    let func_calls = cst::parse_using_lexer(l, errs.clone());
280    errs.check()?;
281    let mut s = String::new();
282    cst::pretty_print(&mut s, func_calls).expect("no errors writing to string");
283    Ok(s)
284}
285
286/// Parse Box language source code into a horizontal list.
287pub fn parse_horizontal_list(source: &str) -> Result<Vec<ds::Horizontal>, Vec<Error<'_>>> {
288    let ast_nodes = ast::parse_hlist(source)?;
289    Ok(ast_nodes.to_boxworks())
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn test_format() {
298        let input = r#"# This is a
299#  list of things
300hlist
301
302        (
303    1.0pt, height =2.0pt,
304
305    contents = [ # glue is good
306        glue(  ) 
307    
308chars("Hello", font = 
309# we use an unusual font here
3101)
311
312    chars("Hello", font =  
313
314
315    0) chars("World")] ,
316        # Infinite glue
317    other=3.0fill,
318    # there are no more arguments
319)
320"#;
321        let want = r#"# This is a
322#  list of things
323hlist(
324  1.0pt,
325  height=2.0pt,
326  contents=[
327    # glue is good
328    glue()
329    chars(
330      "Hello",
331      # we use an unusual font here
332      font=1,
333    )
334    chars("Hello", font=0)
335    chars("World")
336  ],
337  # Infinite glue
338  other=3.0fill,
339  # there are no more arguments
340)
341"#;
342        let got = format(&input).unwrap();
343        assert_eq!(got, want);
344    }
345}