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