5
$\begingroup$

What is LaTeX

If you already know what LaTeX is, then skip down the the section labeled "sloppy syntax".

There is a type-setting language known as LaTeX.

LaTeX even has its very own Stack Exchange Site located here.

You do not have have to be fluent in MathJax or LaTeX in order to answer this question.

However, it does help to see three examples of the syntax for LaTeX:

\kiwi{a+06}{b+09}{x+11}
\binom{n+1}{2k}
\foobar{100}
\jackfruit{1.1}{2.2}{3.33}{4.444}{5.555}{6}

LaTeX has various commands and those commands behave somewhat like the functions we find in other programming languages.

\binom{n+1}{2k} is an example of a LaTeX command with two parameters passed into it.

So, the binom command (like a function or procedure) accepts two input arguments. The input arguments are n+1 and 2k.


Sloppy Syntax

Suppose that you wanted to translate something named LearnTex into LaTeX.

Our imaginary LearnTex is a bit like a pre-processor, or macro, for LaTeX

The primary goal here is to make the LaTeX language easier for new people to learn.

LearnTex would allow correctly spelled LaTeX, so it is backwards compatible.

We can define wrapping delimiters as follows:

  • curly braces {}
  • square brackets []
  • parentheses ()

Below are the infix delimiters of interest within the current context:

  • commas ,
  • colons :
  • semi-colons ;

Ideally, any infix delimiter in LearnTex would be exchangeable (swappable) for any other infix delimiter.

For example, \binom{n+1, 2k} with a comma and \binom{n+1; 2k} with a semi-colon would eventually compile into the same thing.

Example No INPUT (LearnTex ) OUTPUT (LaTeX)
1 \dfrac{1}{x} \dfrac{1}{x}
2 \dfrac{1, x} \dfrac{1}{x}
3 \dfrac(1, x) \dfrac{1}{x}
4 \dfrac[1; x] \dfrac{1}{x}
5 \dfrac(1; x) \dfrac{1}{x}
6 \dfrac(1)(x) \dfrac{1}{x}
7 \dfrac[1][x] \dfrac{1}{x}

In additional to commas (,) being swappable with semi-colons (;) and colons (:), we need functions to look like they are curried.

For example,

\command{x}{y}{z}
\command{x, y, z}

I wrote, "look like curry", because we not have to have an actual programming curry implementation, like this:

SUM(1, 2, 3) is implemented as:

  • Function call f1(1) returns function f2
  • Function call f2(2) returns function f3
  • Function call f3(3) returns 6 which is 1+2+3.

Instead of implementing curry at a low-level, our goal is to simply replace some ASCII-encoded strings with some other ASCII-encoded strings.

For example, \cmd{1, 2} becomes \cmd{1}{2}

Pretend we don't support all of latex. Our interpreter or parser need only translate one command at a time.

In case you are curious, a regular expression for a command with a single input is:

\\[a-zA-Z]+[{(\[][a-zA-Z0-9]+[})\]]

In the end, a LaTeX compiler or interpreter will be used to either interpret or compile the LaTeX into a human-readable pdf file full of a mixture mathematical equations and English.

For example, \dfrac{n}{d} becomes the fraction n divided by d.


What approach would you recommend for converting LearnTex into LaTeX?

$\endgroup$
4
  • $\begingroup$ That regex doesn't look correct - at least not when \makeatletter. $\endgroup$
    – RubenVerg
    May 24, 2023 at 4:46
  • $\begingroup$ @RydwolfPrograms see languagedesign.meta.stackexchange.com/q/249 $\endgroup$ May 24, 2023 at 10:49
  • $\begingroup$ @RubenVerg: I would take the regex as prescribing syntax of LearnTeX rather than describing syntax of TeX. $\endgroup$
    – user570286
    Jul 8, 2023 at 0:42
  • 2
    $\begingroup$ I’m voting to close this question because it's requesting implementation advice for a very specific problem, not particularly transferrable to other contexts on the site. I think it would be better suited on StackOverflow (although read their How To Ask guide first) $\endgroup$
    – pxeger
    Jul 10, 2023 at 18:09

1 Answer 1

1
$\begingroup$

I would

  1. parse the LearnTeX syntax using a parsing library, such as Rust's combine or Haskell's parsec, or a parser generator, such as OCaml's menhir or Rust's lalrpop, and then
  2. generate the equivalent TeX syntax (LaTeX is essentially a standard library for TeX, not its own language). This does not necessarily mean parsing all the input into an intermediate representation that would live in memory in its entirety; the parser could stream information to the generator. For that, in Rust, one could use an iterator, whereas, in Haskell, I guess the language's lazy evaluation would make the program naturally do this.

However, for demonstration, here's a very manual implementation of a very direct, imperative translator of LearnTeX into TeX, written in Rust but in a style intended to be accessible to programmers who are familiar with C-family languages other than Rust. This implementation is tested at the end. It and its tests can be run in the Rust Playground (click the big red "TEST" button in the top left corner).

/// Translates LearnTeX, a syntactic variant of TeX, into TeX
///
/// See <https://langdev.stackexchange.com/questions/1045>.
///
/// This instead, of course, could be written in a functional style;
/// or as a lazy [iterator], which *might* be more efficient when
/// optimized by the compiler; but it is written in an imperative
/// style for clarity to most programmers.
///
/// It could be useful to extend this LearnTeX syntax to allow
/// escaping the argument separators by wrapping them in braces, so
/// that `\command{{a, b, c}}` is translated literally as
/// `\command{{a, b, c}}`, which is largely equivalent to `\command{a,
/// b, c}` for the commands that MathJax supports.
///
/// [iterator]: <https://doc.rust-lang.org/std/iter/index.html>
pub fn learntex_to_tex(input: &str) -> String {
    // `with_capacity` is merely an optimization: we guess that the
    // output will be about as long as the input, so we allocate an
    // output buffer as long as the input, to reduce the number of
    // times that `output`'s backing buffer will need to be grown (and
    // thus reallocated) as we append content from the input to it.
    let mut output = String::with_capacity(input.len());

    // Iterate over the characters in the input.  It would be much
    // more idiomatic and more efficient Rust to use the
    // `input.chars()` iterator to iterate over the characters
    // directly, but instead we iterate by character index, for
    // clarity to non-Rust programmers.
    let mut chars = input.chars();

    // While there is still `Some` character left, iterate over the
    // characters.  When there are no characters left, the `chars`
    // iterator's `next()` method will return `None` and the loop will
    // stop.
    'outer_loop: while let Some(c) = chars.next() {
        // Switch on what the character is here.
        match c {
            '\\' => {
                // We have encountered a command.  Append a backslash
                // to the output.
                output.push('\\');

                // Command names consist of, and only of, alphabetic
                // characters.  Loop, appending these to the output,
                // until we encounter a non-alphabetic character,
                // which is either the opening delimiter of the
                // command's arguments or (if the command has no
                // arguments) something that is not an opening
                // delimiter.
                let mut char_after_command_name = None;
                while let Some(c) = chars.next() {
                    if c.is_alphabetic() {
                        output.push(c);
                    } else {
                        char_after_command_name = Some(c);
                        break;
                    }
                }

                let opening_delimiter;
                match char_after_command_name {
                    Some(c @ ('{' | '[' | '(')) => {
                        // LearnTeX allows any of `{...}`, `[...]`,
                        // `(...)` to stand for TeX's `{...}`.  The
                        // `@` means to capture the opening delimiter
                        // as a variable named `c`.  Now we set the
                        // enclosing scope's `opening_delimiter`
                        // variable to that.
                        opening_delimiter = c;
                    }

                    Some(other_char) => {
                        // This command has ended.  Append the
                        // following character to the output and
                        // continue the outer loop.
                        output.push(other_char);
                        continue 'outer_loop;
                    }

                    None => {
                        // The input has ended.  End the outer loop.
                        break 'outer_loop;
                    }
                }

                let mut skip_space = false;

                // Loop, parsing the command's arguments.
                'args_loop: loop {
                    // We are in a command's argument.  Output the TeX
                    // opening delimiter for an argument.  LearnTeX
                    // supports only those TeX commands that use
                    // `{...}`-delimited arguments only, no
                    // `[...]`-delimited optional arguments or fancy
                    // `xparse` arguments.
                    output.push('{');

                    // Decide which delimiter will be needed to close
                    // this argument.
                    let closing_delimiter = match opening_delimiter {
                        '{' => '}',
                        '[' => ']',
                        '(' => ')',
                        _ => unreachable!(),
                    };

                    loop {
                        match chars.next() {
                            Some(',' | ';' | ':') => {
                                // Any of these three characters is
                                // allowed as an argument separator,
                                // so this argument ends here and a
                                // new one begins.  Output the TeX
                                // closing delimiter for an argument.
                                output.push('}');
                                // The argument separator may be
                                // followed by any amount of
                                // whitespace, which is ignored.
                                skip_space = true;
                                // Continue to the next argument.
                                continue 'args_loop;
                            }

                            Some(c) if c == closing_delimiter => {
                                // We have encountered the end of the
                                // argument list.  Output the TeX
                                // closing delimiter for an argument.
                                output.push('}');
                                break 'args_loop;
                            }

                            Some(c) if skip_space && c.is_whitespace() => {
                                // Ignore the whitespace.
                            }

                            Some(other_char) => {
                                // This character is part of an
                                // argument.  Output it.
                                output.push(other_char);
                                // Also, we encountered a non-space
                                // character, so stop skipping spaces.
                                skip_space = false;
                            }

                            None => {
                                // The argument was not closed.  In
                                // practice, there are much more
                                // graceful and helpful ways of
                                // handling such an error than
                                // throwing this exception.
                                panic!("unclosed argument");
                            }
                        }
                    }
                }
            }

            other_char => {
                // We encountered some character that is not part of a
                // command.  Append the character to the output.
                output.push(other_char);
            }
        }
    }

    // Return the output.
    output
}

#[test]
fn test() {
    assert_eq!(learntex_to_tex(r"abc"), r"abc");
    assert_eq!(learntex_to_tex(r"\abc"), r"\abc");
    assert_eq!(learntex_to_tex(r"\abc{}"), r"\abc{}");
    assert_eq!(learntex_to_tex(r"\abc{x}"), r"\abc{x}");
    assert_eq!(learntex_to_tex(r"\abc[x]"), r"\abc{x}");
    assert_eq!(learntex_to_tex(r"\abc(x)"), r"\abc{x}");
    assert_eq!(learntex_to_tex(r"\abc{x, y}"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc[x, y]"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc(x, y)"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc{x, y, z}"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc[x, y, z]"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x, y, z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc{x; y}"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc[x; y]"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc(x; y)"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc{x; y; z}"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc[x; y; z]"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x; y; z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc{x: y}"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc[x: y]"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc(x: y)"), r"\abc{x}{y}");
    assert_eq!(learntex_to_tex(r"\abc{x: y: z}"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc[x: y: z]"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x: y: z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc{x, y; z}"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc[x; y, z]"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x, y: z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x: y, z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x; y: z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc(x: y; z)"), r"\abc{x}{y}{z}");
    assert_eq!(learntex_to_tex(r"\abc{x} def \ghi[y]"), r"\abc{x} def \ghi{y}");
}

If you are interested in seeing this done with combine, I can write an example of that.

Edit: My manual implementation is wrong in that it does not translate commands with 'curried' arguments, i.e., it translates \abc(x)[y] as \abc{x}[y], seeing the [y] as not an argument to the command. I'm trying to fix this.

$\endgroup$

Not the answer you're looking for? Browse other questions tagged .