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}