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}