boxworks/lang/
mod.rs

1//! # Boxworks language
2//!
3//! This module 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//! | Character | A string containing exactly one UTF-8 character. | `"A"`, `"ñ"` |
159//! | Glue order | One of the strings `"normal"`, `"fil"`, `"fill"`, or `"filll"`. | `"normal"`, `"fill"` |
160//! | Glue ratio | A floating-point number represented as a string. | `"1.5"`, `"-0.25"` |
161//! | Dimension or running | Either a dimension, or the string `"running"` to indicate the value is determined by context. | `1pt`, `"running"` |
162//! | Horizontal list | A bracket-enclosed list of horizontal-mode function calls. | `[chars("Hi") glue()]` |
163//! | Vertical list | A bracket-enclosed list of vertical-mode function calls. | `[glue() kern(1pt)]` |
164//! | Discretionary list | A bracket-enclosed list of function calls valid in discretionary pre/post-break lists. | `[chars("-") kern(0.5pt)]` |
165//!
166//! ### Available functions
167//!
168//! More functions will be added over time.
169//! These are the currently supported functions.
170//!
171//! #### `chars`: typeset some characters
172//!
173//! Adds a value of the Rust type [`super::ds::Char`] for each character in the
174//! input string.
175//!
176//! Only available in horizontal and discretionary lists, not vertical lists.
177//!
178//! Parameters:
179//!
180//! | Number | Name      | Type    | Default |
181//! |--------|-----------|---------|---------|
182//! | 1      | `content` | string  | `""`    |
183//! | 2      | `font`    | integer | `0`     |
184//!
185//! #### `glue`: add a glue node to the current list
186//!
187//! Adds a value of the Rust type [`super::ds::Glue`]
188//! to the current list.
189//!
190//! Only available in horizontal and vertical lists, not discretionary lists.
191//!
192//! Parameters:
193//!
194//! | Number | Name      | Type                   | Default |
195//! |--------|-----------|------------------------|---------|
196//! | 1      | `width`   | dimension              | `0pt`   |
197//! | 2      | `stretch` | glue stretch or shrink | `0pt`   |
198//! | 3      | `shrink`  | glue stretch or shrink | `0pt`   |
199//!
200//! #### `penalty`: add a penalty node to the current list
201//!
202//! Adds a value of the Rust type [`super::ds::Penalty`]
203//! to the current list.
204//!
205//! Only available in horizontal and vertical lists, not discretionary lists.
206//!
207//! Parameters:
208//!
209//! | Number | Name    | Type    | Default |
210//! |--------|---------|---------|---------|
211//! | 1      | `value` | integer | `0`     |
212//!
213//! #### `kern`: add a kern node to the current list
214//!
215//! Adds a value of the Rust type [`super::ds::Kern`]
216//! to the current list.
217//!
218//! Parameters:
219//!
220//! | Number | Name    | Type      | Default |
221//! |--------|---------|-----------|---------|
222//! | 1      | `width` | dimension | `0pt`   |
223//!
224//! #### `hbox`: add a horizontal box to the current list
225//!
226//! Adds a value of the Rust type [`super::ds::HBox`]
227//! to the current list.
228//!
229//! Parameters:
230//!
231//! | Number | Name           | Type            | Default    |
232//! |--------|----------------|-----------------|------------|
233//! | 1      | `height`       | dimension       | `0pt`      |
234//! | 2      | `width`        | dimension       | `0pt`      |
235//! | 3      | `depth`        | dimension       | `0pt`      |
236//! | 4      | `shift_amount` | dimension       | `0pt`      |
237//! | 5      | `glue_ratio`   | glue ratio      | `"0.0"`    |
238//! | 6      | `glue_order`   | glue order      | `"normal"` |
239//! | 7      | `content`      | horizontal list | `[]`       |
240//!
241//! #### `lig`: add a ligature node to the current list
242//!
243//! Adds a value of the Rust type [`super::ds::Ligature`]
244//! to the current list.
245//!
246//! Only available in horizontal and discretionary lists, not vertical lists.
247//!
248//! Parameters:
249//!
250//! | Number | Name             | Type      | Default  |
251//! |--------|------------------|-----------|----------|
252//! | 1      | `char`           | character | `"\0"`   |
253//! | 2      | `original_chars` | string    | `""`     |
254//! | 3      | `font`           | integer   | `0`      |
255//!
256//! #### `vbox`: add a vertical box to the current list
257//!
258//! Adds a value of the Rust type [`super::ds::VBox`]
259//! to the current list.
260//!
261//! Parameters:
262//!
263//! | Number | Name           | Type          | Default |
264//! |--------|----------------|---------------|---------|
265//! | 1      | `height`       | dimension     | `0pt`   |
266//! | 2      | `width`        | dimension     | `0pt`   |
267//! | 3      | `depth`        | dimension     | `0pt`   |
268//! | 4      | `shift_amount` | dimension     | `0pt`   |
269//! | 5      | `content`      | vertical list | `[]`    |
270//!
271//! #### `disc`: add a discretionary node to the current list
272//!
273//! Adds a value of the Rust type [`super::ds::Discretionary`]
274//! to the current list.
275//!
276//! Only available in horizontal lists.
277//!
278//! Parameters:
279//!
280//! | Number | Name            | Type               | Default |
281//! |--------|-----------------|--------------------|---------|
282//! | 1      | `pre_break`     | discretionary list | `[]`    |
283//! | 2      | `post_break`    | discretionary list | `[]`    |
284//! | 3      | `replace_count` | integer            | `0`     |
285//!
286//! #### `rule`: add a rule to the current list
287//!
288//! Adds a value of the Rust type [`super::ds::Rule`]
289//! to the current list.
290//!
291//! Parameters:
292//!
293//! | Number | Name     | Type                 | Default |
294//! |--------|----------|----------------------|---------|
295//! | 1      | `height` | dimension or running | `0pt`   |
296//! | 2      | `width`  | dimension or running | `0pt`   |
297//! | 3      | `depth`  | dimension or running | `0pt`   |
298//!
299//! #### `mark`: add a mark node to the current list
300//!
301//! Adds a value of the Rust type [`super::ds::Mark`]
302//! to the current list.
303//!
304//! Only available in horizontal and vertical lists, not discretionary lists.
305//!
306//! Parameters: none.
307//!
308//! #### `adjust`: add an adjust node to the current list
309//!
310//! Adds a value of the Rust type [`super::ds::Adjust`]
311//! to the current list.
312//!
313//! Only available in horizontal lists.
314//!
315//! Parameters:
316//!
317//! | Number | Name      | Type          | Default |
318//! |--------|-----------|---------------|---------|
319//! | 1      | `content` | vertical list | `[]`    |
320//!
321//! #### `insertion`: add an insertion node to the current list
322//!
323//! Adds a value of the Rust type [`super::ds::Insertion`]
324//! to the current list.
325//!
326//! Only available in horizontal and vertical lists, not discretionary lists.
327//!
328//! Parameters:
329//!
330//! | Number | Name                      | Type                   | Default |
331//! |--------|---------------------------|------------------------|---------|
332//! | 1      | `box_number`              | integer                | `0`     |
333//! | 2      | `height`                  | dimension              | `0pt`   |
334//! | 3      | `split_max_depth`         | dimension              | `0pt`   |
335//! | 4      | `split_top_skip_width`    | dimension              | `0pt`   |
336//! | 5      | `split_top_skip_stretch`  | glue stretch or shrink | `0pt`   |
337//! | 6      | `split_top_skip_shrink`   | glue stretch or shrink | `0pt`   |
338//! | 7      | `float_penalty`           | integer                | `0`     |
339//! | 8      | `vbox`                    | vertical list          | `[]`    |
340//!
341//! #### `math`: add a math node to the current list
342//!
343//! Adds a value of the Rust type [`super::ds::Math`]
344//! to the current list.
345//!
346//! Only available in horizontal and vertical lists, not discretionary lists.
347//!
348//! Parameters:
349//!
350//! | Number | Name   | Type   | Default |
351//! |--------|--------|--------|---------|
352//! | 1      | `kind` | string | `""`    |
353pub mod ast;
354pub mod convert;
355pub mod cst;
356mod error;
357pub mod lexer;
358use convert::ToBoxworks;
359pub use error::{Error, ErrorAccumulator, ErrorLabel};
360
361use crate::ds;
362
363/// String type used in the crate's public API.
364#[derive(Debug, Clone)]
365pub struct Str<'a> {
366    value: &'a str,
367    start: usize,
368    end: usize,
369}
370
371impl<'a> Str<'a> {
372    fn new(value: &'a str) -> Str<'a> {
373        Str {
374            value,
375            start: 0,
376            end: value.len(),
377        }
378    }
379    fn span(&self) -> std::ops::Range<usize> {
380        self.start..self.end
381    }
382    fn str(&self) -> &'a str {
383        &self.value[self.span()]
384    }
385    fn is_empty(&self) -> bool {
386        self.start == self.end
387    }
388}
389
390impl<'a> From<&'a str> for Str<'a> {
391    fn from(value: &'a str) -> Self {
392        Str {
393            value,
394            start: 0,
395            end: value.len(),
396        }
397    }
398}
399
400impl<'a> std::fmt::Display for Str<'a> {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        write!(f, "{}", self.str())
403    }
404}
405
406impl<'a> PartialEq for Str<'a> {
407    fn eq(&self, other: &Self) -> bool {
408        self.str() == other.str()
409    }
410}
411
412impl<'a> Eq for Str<'a> {}
413
414/// Pretty-format Box source code.
415pub fn format(source: &str) -> Result<String, Vec<error::Error<'_>>> {
416    let errs: ErrorAccumulator = Default::default();
417    let l = lexer::Lexer::new(source, errs.clone());
418    let func_calls = cst::parse_using_lexer(l, errs.clone());
419    errs.check()?;
420    let mut s = String::new();
421    cst::pretty_print(&mut s, func_calls).expect("no errors writing to string");
422    Ok(s)
423}
424
425/// Parse Box language source code into a horizontal list.
426pub fn parse_horizontal_list(source: &str) -> Result<Vec<ds::Horizontal>, Vec<Error<'_>>> {
427    let ast_nodes = ast::parse_hbox(source)?;
428    Ok(ast_nodes.to_boxworks())
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_format() {
437        let input = r#"# This is a
438#  list of things
439hlist
440
441        (
442    1.0pt, height =2.0pt,
443
444    contents = [ # glue is good
445        glue(  ) 
446    
447chars("Hello", font = 
448# we use an unusual font here
4491)
450
451    chars("Hello", font =  
452
453
454    0) chars("World")] ,
455        # Infinite glue
456    other=3.0fill,
457    # there are no more arguments
458)
459"#;
460        let want = r#"# This is a
461#  list of things
462hlist(
463  1.0pt,
464  height=2.0pt,
465  contents=[
466    # glue is good
467    glue()
468    chars(
469      "Hello",
470      # we use an unusual font here
471      font=1,
472    )
473    chars("Hello", font=0)
474    chars("World")
475  ],
476  # Infinite glue
477  other=3.0fill,
478  # there are no more arguments
479)
480"#;
481        let got = format(&input).unwrap();
482        pretty_assertions::assert_eq!(got, want);
483    }
484}