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}