Repository: typst/typst
File: crates/typst-syntax/src/parser.rs (Rust)

Code before:
        p.convert_and_eat(SyntaxKind::MathText);
    }
    let m_body = p.marker();
    math_exprs(p, syntax_set!(Dollar, End, RightBrace, RightParen));
    if p.at_set(syntax_set!(RightBrace, RightParen)) {
        p.wrap(m_body, SyntaxKind::Math);
        if p.current_text() == "|]" {
            p.convert_and_eat(SyntaxKind::MathShorthand);
        } else {
            p.convert_and_eat(SyntaxKind::MathText);
        }
        p.wrap(m, SyntaxKind::MathDelimited);
    } else {
        // If we had no closing delimiter, just produce a math sequence.
        if prec > 0 {
            // Unless we were to the right of an operator, then we error.
            let common_closing = match p[m].text().as_str() {
                "(" => Some(')'),
                "[" => Some(']'),
                "{" => Some('}'),
                _ => None,
            };
            p[m].convert_to_error("unclosed delimiter");
            p[m].hint("delimiters must be correctly matched when used for grouping");
            if let Some(c) = common_closing {
                p[m].hint(eco_format!("try adding a closing delimiter: `{c}`"));
            }
        }
        p.wrap(m, SyntaxKind::Math);
    }
}

/// Remove one set of parentheses (if any) from a previously parsed expression
/// by converting to non-expression SyntaxKinds.
fn math_unparen(p: &mut Parser, m: Marker) {
    let Some(node) = p.nodes.get_mut(m.0) else { return };
    if node.kind() != SyntaxKind::MathDelimited {
        return;
    }

    if let [first, .., last] = node.children_mut()
        && first.text() == "("
        && last.text() == ")"
    {
        first.convert_to_kind(SyntaxKind::LeftParen);
        last.convert_to_kind(SyntaxKind::RightParen);
        // Only convert if we did have regular parens.
        node.convert_to_kind(SyntaxKind::Math);
    }
}


Review comment (security):
perhaps we should add a second hint like this?
```
or escape the delimiter with a backslash to display it verbatim: `\\{c}`
```
though I suppose this hint would also make sense on some of the other errors and I'm not sure whether it's too much overall

Code after:

/// Parse matched delimiters in math: `[x + y]`.
///
/// The lexer produces `{Left,Right}{Brace,Paren}` for delimiters, and it's our
/// job to convert them back to `MathText` or `MathShorthand` before eating.
fn math_delimited(p: &mut Parser, prec: u8) {
    let m = p.marker();
    if p.current_text() == "[|" {
        p.convert_and_eat(SyntaxKind::MathShorthand);
    } else {
        p.convert_and_eat(SyntaxKind::MathText);
    }
    let m_body = p.marker();
    math_exprs(p, syntax_set!(Dollar, End, RightBrace, RightParen));
    if p.at_set(syntax_set!(RightBrace, RightParen)) {
        p.wrap(m_body, SyntaxKind::Math);
        if p.current_text() == "|]" {
            p.convert_and_eat(SyntaxKind::MathShorthand);
        } else {
            p.convert_and_eat(SyntaxKind::MathText);
        }
        p.wrap(m, SyntaxKind::MathDelimited);
    } else {
        // If we had no closing delimiter, just produce a math sequence.
        if prec > 0 {
            // Unless we were to the right of an operator, then we error.
            p[m].convert_to_error("unclosed delimiter");
            p[m].hint("delimiters must be correctly matched when used for grouping");
            let open = p[m].text().as_str().chars().next().unwrap();
            if let Some(close) = matching_delim(open) {
                p[m].hint(eco_format!("try adding a closing delimiter: `{close}`"));
                p[m].hint(eco_format!(
                    "or escape the delimiter with a backslash \
                     to display it verbatim: `\\{open}`"
                ));
            }
        }
        p.wrap(m, SyntaxKind::Math);
    }
}

/// Remove one set of parentheses (if any) from a previously parsed expression
/// by converting to non-expression SyntaxKinds.
fn math_unparen(p: &mut Parser, m: Marker) {
    let Some(node) = p.nodes.get_mut(m.0) else { return };
    if node.kind() != SyntaxKind::MathDelimited {
        return;
    }

    if let [first, .., last] = node.children_mut()
        && first.text() == "("