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}