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//! use common::font;
22//!
23//! let source = r#"
24//!     ## The chars() function typesets characters.
25//!     chars("Box")
26//!     ## Glue can be added manually.
27//!     glue(1pt, 5fil, 0.075in)
28//!     ## The following elements illustrate the prototypical example of a kern.
29//!     chars("A")
30//!     kern(-0.1pt)
31//!     chars("V")
32//! "#;
33//! let got = bwl::parse_horizontal_list(&source);
34//! let want: Vec<ds::Horizontal> = vec![
35//!     ds::Char{char: 'B', font: font::Id::ONE}.into(),
36//!     ds::Char{char: 'o', font: font::Id::ONE}.into(),
37//!     ds::Char{char: 'x', font: font::Id::ONE}.into(),
38//!     ds::Glue{
39//!         kind: ds::GlueKind::Normal,
40//!         value: common::Glue{
41//!             width: common::Scaled::new(
42//!                 1,  // integer part
43//!                 common::Scaled::ZERO,  // fractional part
44//!                 common::ScaledUnit::Point,  // units
45//!             ).unwrap(),
46//!             stretch: common::Scaled::new(
47//!                 5,  // integer part
48//!                 common::Scaled::ZERO,  // fractional part
49//!                 common::ScaledUnit::Point,  // units
50//!             ).unwrap(),
51//!             stretch_order: common::GlueOrder::Fil,
52//!             shrink: common::Scaled::new(
53//!                 0,  // integer part
54//!                 common::Scaled::from_decimal_digits(&[0, 7, 5]),  // fractional part
55//!                 common::ScaledUnit::Inch,  // units
56//!             ).unwrap(),
57//!             shrink_order: common::GlueOrder::Normal,
58//!         }
59//!     }.into(),
60//!     ds::Char{char: 'A', font: font::Id::ONE}.into(),
61//!     ds::Kern{
62//!         kind: ds::KernKind::Normal,
63//!         width: -common::Scaled::new(
64//!                 0,  // integer part
65//!                 common::Scaled::from_decimal_digits(&[1]),  // fractional part
66//!                 common::ScaledUnit::Point,  // units
67//!             ).unwrap(),
68//!     }.into(),
69//!     ds::Char{char: 'V', font: font::Id::ONE}.into(),
70//! ];
71//! assert_eq![got, Ok(want)];
72//! ```
73//!
74//! The main takeaway from this example is that you start with a very
75//! terse description of the horizontal list, and the library outputs
76//! the long and tedious Rust struct definitions.
77//!
78//! ## Language specification
79//!
80//! A Boxworks language program is a sequence of a function calls
81//! like `chars("ABC")` or `glue(10pt, 3pt, 2pt)`.
82//! Most function calls add an item or items to the current
83//! box-and-glue list.
84//!
85//! ### Function arguments
86//!
87//! Each function accepts a number of arguments.
88//! For simplicity, every argument to every function is optional.
89//!
90//! Arguments can be provided positionally:
91//!
92//! ```
93//! # use boxworks::lang as bwl;
94//! # use boxworks::ds;
95//! # use common::font;
96//! let source = r#"
97//!     chars("A", 1)
98//! "#;
99//! assert_eq![
100//!     bwl::parse_horizontal_list(&source),
101//!     Ok(vec![ds::Char{char: 'A', font: font::Id::ONE}.into()])
102//! ];
103//! ```
104//!
105//! Or by keyword, potentially out of order:
106//!
107//! ```
108//! # use boxworks::lang as bwl;
109//! # use boxworks::ds;
110//! # use common::font;
111//! let source = r#"
112//!     chars(font=2, content="B")
113//! "#;
114//! assert_eq![
115//!     bwl::parse_horizontal_list(&source),
116//!     Ok(vec![ds::Char{char: 'B', font: font::Id(2)}.into()])
117//! ];
118//! ```
119//!
120//! Or by a combination of positional and by keyword:
121//!
122//! ```
123//! # use boxworks::lang as bwl;
124//! # use boxworks::ds;
125//! # use common::font;
126//! let source = r#"
127//!     chars("C", font=3)
128//! "#;
129//! assert_eq![
130//!     bwl::parse_horizontal_list(&source),
131//!     Ok(vec![ds::Char{char: 'C', font: font::Id(3)}.into()])
132//! ];
133//! ```
134//!
135//! However, all positional arguments must be provided before
136//! keyword arguments:
137//!
138//! ```
139//! # use boxworks::lang as bwl;
140//! # use boxworks::ds;
141//! # use common::font;
142//! let source = r#"
143//!     chars(content="C", 3)
144//! "#;
145//! let errs = bwl::parse_horizontal_list(&source).unwrap_err();
146//! assert![matches![
147//!     errs[0],
148//!     bwl::Error::PositionalArgAfterKeywordArg{..}
149//! ]];
150//! ```
151//!
152//! ### Function argument types
153//!
154//! Every function argument expects a specific concrete type.
155//! These are the types:
156//!
157//! | Name | Description | Examples |
158//! |------|-------------|---------|
159//! | String | Arbitrary UTF-8 characters between double quotes. Currently the string can't contain a double quote character. | `"a string"` |
160//! | Integer | Decimal integer in the range (-2^31,2^31). | `123`, `-456` |
161//! | Boolean | True or false, provided as string. | `"true"`, `"false"`   |
162//! | Dimension | Decimal number with a unit attached. The format and the allowable units are the same as in TeX. | `1pt`, `2.04in`, `-10sp` |
163//! | Glue stretch or shrink | A dimension where the unit can alternatively be an infinite stretch/shrink unit. | `1fil`, `-2fill`, `3filll` |
164//! | Character | A string containing exactly one UTF-8 character. | `"A"`, `"ñ"` |
165//! | Glue order | One of the strings `"normal"`, `"fil"`, `"fill"`, or `"filll"`. | `"normal"`, `"fill"` |
166//! | Glue ratio | A floating-point number represented as a string. | `"1.5"`, `"-0.25"` |
167//! | Dimension or running | Either a dimension, or the string `"running"` to indicate the value is determined by context. | `1pt`, `"running"` |
168//! | Horizontal list | A bracket-enclosed list of horizontal-mode function calls. | `[chars("Hi") glue()]` |
169//! | Vertical list | A bracket-enclosed list of vertical-mode function calls. | `[glue() kern(1pt)]` |
170//! | Discretionary list | A bracket-enclosed list of function calls valid in discretionary pre/post-break lists. | `[chars("-") kern(0.5pt)]` |
171//!
172//! ### Available functions
173//!
174//! More functions will be added over time.
175//! These are the currently supported functions.
176//!
177//! #### `chars`: typeset some characters
178//!
179//! Adds a value of the Rust type [`super::ds::Char`] for each character in the
180//! input string.
181//!
182//! Only available in horizontal and discretionary lists, not vertical lists.
183//!
184//! Parameters:
185//!
186//! | Number | Name      | Type    | Default |
187//! |--------|-----------|---------|---------|
188//! | 1      | `content` | string  | `""`    |
189//! | 2      | `font`    | font ID (a non-negative integer) | `1`     |
190//!
191//! #### `glue`: add a glue node to the current list
192//!
193//! Adds a value of the Rust type [`super::ds::Glue`]
194//! to the current list.
195//!
196//! Only available in horizontal and vertical lists, not discretionary lists.
197//!
198//! Parameters:
199//!
200//! | Number | Name      | Type                   | Default |
201//! |--------|-----------|------------------------|---------|
202//! | 1      | `width`   | dimension              | `0pt`   |
203//! | 2      | `stretch` | glue stretch or shrink | `0pt`   |
204//! | 3      | `shrink`  | glue stretch or shrink | `0pt`   |
205//!
206//! #### `penalty`: add a penalty node to the current list
207//!
208//! Adds a value of the Rust type [`super::ds::Penalty`]
209//! to the current list.
210//!
211//! Only available in horizontal and vertical lists, not discretionary lists.
212//!
213//! Parameters:
214//!
215//! | Number | Name    | Type    | Default |
216//! |--------|---------|---------|---------|
217//! | 1      | `value` | integer | `0`     |
218//!
219//! #### `kern`: add a kern node to the current list
220//!
221//! Adds a value of the Rust type [`super::ds::Kern`]
222//! to the current list.
223//!
224//! Parameters:
225//!
226//! | Number | Name    | Type      | Default |
227//! |--------|---------|-----------|---------|
228//! | 1      | `width` | dimension | `0pt`   |
229//!
230//! #### `hbox`: add a horizontal box to the current list
231//!
232//! Adds a value of the Rust type [`super::ds::HBox`]
233//! to the current list.
234//!
235//! Parameters:
236//!
237//! | Number | Name           | Type            | Default    |
238//! |--------|----------------|-----------------|------------|
239//! | 1      | `height`       | dimension       | `0pt`      |
240//! | 2      | `width`        | dimension       | `0pt`      |
241//! | 3      | `depth`        | dimension       | `0pt`      |
242//! | 4      | `shift_amount` | dimension       | `0pt`      |
243//! | 5      | `glue_ratio`   | glue ratio      | `"0.0"`    |
244//! | 6      | `glue_order`   | glue order      | `"normal"` |
245//! | 7      | `content`      | horizontal list | `[]`       |
246//!
247//! #### `lig`: add a ligature node to the current list
248//!
249//! Adds a value of the Rust type [`super::ds::Ligature`]
250//! to the current list.
251//!
252//! Only available in horizontal and discretionary lists, not vertical lists.
253//!
254//! Parameters:
255//!
256//! | Number | Name             | Type      | Default  |
257//! |--------|------------------|-----------|----------|
258//! | 1      | `char`           | character | `"\0"`   |
259//! | 2      | `original_chars` | string    | `""`     |
260//! | 3      | `font`           | font ID (a non-negative integer)   | `1`      |
261//! | 4      | `includes_left_char`  | boolean   | false      |
262//! | 5      | `includes_right_char` | boolean   | false      |
263//!
264//! #### `vbox`: add a vertical box to the current list
265//!
266//! Adds a value of the Rust type [`super::ds::VBox`]
267//! to the current list.
268//!
269//! Parameters:
270//!
271//! | Number | Name           | Type          | Default |
272//! |--------|----------------|---------------|---------|
273//! | 1      | `height`       | dimension     | `0pt`   |
274//! | 2      | `width`        | dimension     | `0pt`   |
275//! | 3      | `depth`        | dimension     | `0pt`   |
276//! | 4      | `shift_amount` | dimension     | `0pt`   |
277//! | 5      | `content`      | vertical list | `[]`    |
278//!
279//! #### `disc`: add a discretionary node to the current list
280//!
281//! Adds a value of the Rust type [`super::ds::Discretionary`]
282//! to the current list.
283//!
284//! Only available in horizontal lists.
285//!
286//! Parameters:
287//!
288//! | Number | Name            | Type               | Default |
289//! |--------|-----------------|--------------------|---------|
290//! | 1      | `pre_break`     | discretionary list | `[]`    |
291//! | 2      | `post_break`    | discretionary list | `[]`    |
292//! | 3      | `replace_count` | integer            | `0`     |
293//!
294//! #### `rule`: add a rule to the current list
295//!
296//! Adds a value of the Rust type [`super::ds::Rule`]
297//! to the current list.
298//!
299//! Parameters:
300//!
301//! | Number | Name     | Type                 | Default |
302//! |--------|----------|----------------------|---------|
303//! | 1      | `height` | dimension or running | `0pt`   |
304//! | 2      | `width`  | dimension or running | `0pt`   |
305//! | 3      | `depth`  | dimension or running | `0pt`   |
306//!
307//! #### `mark`: add a mark node to the current list
308//!
309//! Adds a value of the Rust type [`super::ds::Mark`]
310//! to the current list.
311//!
312//! Only available in horizontal and vertical lists, not discretionary lists.
313//!
314//! Parameters: none.
315//!
316//! #### `adjust`: add an adjust node to the current list
317//!
318//! Adds a value of the Rust type [`super::ds::Adjust`]
319//! to the current list.
320//!
321//! Only available in horizontal lists.
322//!
323//! Parameters:
324//!
325//! | Number | Name      | Type          | Default |
326//! |--------|-----------|---------------|---------|
327//! | 1      | `content` | vertical list | `[]`    |
328//!
329//! #### `insertion`: add an insertion node to the current list
330//!
331//! Adds a value of the Rust type [`super::ds::Insertion`]
332//! to the current list.
333//!
334//! Only available in horizontal and vertical lists, not discretionary lists.
335//!
336//! Parameters:
337//!
338//! | Number | Name                      | Type                   | Default |
339//! |--------|---------------------------|------------------------|---------|
340//! | 1      | `box_number`              | integer                | `0`     |
341//! | 2      | `height`                  | dimension              | `0pt`   |
342//! | 3      | `split_max_depth`         | dimension              | `0pt`   |
343//! | 4      | `split_top_skip_width`    | dimension              | `0pt`   |
344//! | 5      | `split_top_skip_stretch`  | glue stretch or shrink | `0pt`   |
345//! | 6      | `split_top_skip_shrink`   | glue stretch or shrink | `0pt`   |
346//! | 7      | `float_penalty`           | integer                | `0`     |
347//! | 8      | `vbox`                    | vertical list          | `[]`    |
348//!
349//! #### `math`: add a math node to the current list
350//!
351//! Adds a value of the Rust type [`super::ds::Math`]
352//! to the current list.
353//!
354//! Only available in horizontal and vertical lists, not discretionary lists.
355//!
356//! Parameters:
357//!
358//! | Number | Name   | Type   | Default |
359//! |--------|--------|--------|---------|
360//! | 1      | `kind` | string | `""`    |
361pub mod ast;
362pub mod convert;
363pub mod cst;
364mod error;
365pub mod lexer;
366use convert::ToBoxworks;
367pub use error::{Error, ErrorAccumulator, ErrorLabel};
368
369use crate::ds;
370
371/// String type used in the crate's public API.
372#[derive(Debug, Clone)]
373pub struct Str<'a> {
374    value: &'a str,
375    start: usize,
376    end: usize,
377}
378
379impl<'a> Str<'a> {
380    fn new(value: &'a str) -> Str<'a> {
381        Str {
382            value,
383            start: 0,
384            end: value.len(),
385        }
386    }
387    fn span(&self) -> std::ops::Range<usize> {
388        self.start..self.end
389    }
390    fn str(&self) -> &'a str {
391        &self.value[self.span()]
392    }
393    fn is_empty(&self) -> bool {
394        self.start == self.end
395    }
396}
397
398impl<'a> From<&'a str> for Str<'a> {
399    fn from(value: &'a str) -> Self {
400        Str {
401            value,
402            start: 0,
403            end: value.len(),
404        }
405    }
406}
407
408impl<'a> std::fmt::Display for Str<'a> {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        write!(f, "{}", self.str())
411    }
412}
413
414impl<'a> PartialEq for Str<'a> {
415    fn eq(&self, other: &Self) -> bool {
416        self.str() == other.str()
417    }
418}
419
420impl<'a> Eq for Str<'a> {}
421
422/// Pretty-format Box source code.
423pub fn format(source: &str) -> Result<String, Vec<error::Error<'_>>> {
424    let errs: ErrorAccumulator = Default::default();
425    let l = lexer::Lexer::new(source, errs.clone());
426    let func_calls = cst::parse_using_lexer(l, errs.clone());
427    errs.check()?;
428    let mut s = String::new();
429    cst::pretty_print(&mut s, func_calls).expect("no errors writing to string");
430    Ok(s)
431}
432
433/// Parse Box language source code into a horizontal list.
434pub fn parse_horizontal_list(source: &str) -> Result<Vec<ds::Horizontal>, Vec<Error<'_>>> {
435    let ast_nodes = ast::parse_hbox(source)?;
436    Ok(ast_nodes.to_boxworks())
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn test_format() {
445        let input = r#"# This is a
446#  list of things
447hlist
448
449        (
450    1.0pt, height =2.0pt,
451
452    contents = [ # glue is good
453        glue(  ) 
454    
455chars("Hello", font = 
456# we use an unusual font here
4571)
458
459    chars("Hello", font =  
460
461
462    0) chars("World")] ,
463        # Infinite glue
464    other=3.0fill,
465    # there are no more arguments
466)
467"#;
468        let want = r#"# This is a
469#  list of things
470hlist(
471  1.0pt,
472  height=2.0pt,
473  contents=[
474    # glue is good
475    glue()
476    chars(
477      "Hello",
478      # we use an unusual font here
479      font=1,
480    )
481    chars("Hello", font=0)
482    chars("World")
483  ],
484  # Infinite glue
485  other=3.0fill,
486  # there are no more arguments
487)
488"#;
489        let got = format(&input).unwrap();
490        pretty_assertions::assert_eq!(got, want);
491    }
492}