Skip to main content

harper_typst/
lib.rs

1mod offset_cursor;
2mod typst_translator;
3
4use typst_translator::TypstTranslator;
5
6use harper_core::{Token, parsers::Parser};
7use itertools::Itertools;
8use typst_syntax::{
9    Source, SyntaxNode,
10    ast::{AstNode, Expr, Markup},
11};
12
13/// A parser that wraps Harper's `PlainEnglish` parser allowing one to ingest Typst files.
14pub struct Typst;
15
16impl Parser for Typst {
17    fn parse(&self, source: &[char]) -> Vec<Token> {
18        let source_str: String = source.iter().collect();
19
20        // Transform the source into an AST through the `typst_syntax` crate
21        let typst_document = Source::detached(source_str);
22        let typst_tree = Markup::from_untyped(typst_document.root())
23            .expect("Unable to create typst document from parsed tree!");
24
25        // Recurse through AST to create tokens
26        let parse_helper = TypstTranslator::new(&typst_document);
27        let mut buf = Vec::new();
28        let exprs = typst_tree.exprs().collect_vec();
29        let exprs = convert_parbreaks(&mut buf, &exprs);
30        parse_helper.parse_exprs(&exprs)
31    }
32}
33
34/// Converts newlines after certain elements to paragraph breaks
35/// This is accomplished here instead of in the translating module because at this point there is
36/// still semantic information associated with the elements.
37///
38/// Newlines are separate expressions in the parse tree (as the Space variant)
39fn convert_parbreaks<'a>(buf: &'a mut Vec<SyntaxNode>, exprs: &'a [Expr]) -> Vec<Expr<'a>> {
40    // Owned collection of nodes forcibly casted to paragraph breaks
41    *buf = exprs
42        .iter()
43        .map(|e| {
44            let mut node = SyntaxNode::placeholder(typst_syntax::SyntaxKind::Parbreak);
45            node.synthesize(e.span());
46            node
47        })
48        .collect_vec();
49
50    let should_parbreak = |e1, e2, e3| {
51        matches!(e2, Expr::Space(_))
52            && (matches!(e1, Expr::Heading(_) | Expr::ListItem(_))
53                || matches!(e3, Expr::Heading(_) | Expr::ListItem(_)))
54    };
55
56    let mut res: Vec<Expr> = Vec::new();
57    let mut last_element: Option<Expr> = None;
58    for ((i, expr), (_, next_expr)) in exprs.iter().enumerate().tuple_windows() {
59        let mut current_expr = *expr;
60        if let Some(last_element) = last_element
61            && should_parbreak(last_element, *expr, *next_expr)
62        {
63            let pbreak = typst_syntax::ast::Parbreak::from_untyped(&buf[i])
64                .expect("Unable to convert expression to Parbreak");
65            current_expr = Expr::Parbreak(pbreak);
66        }
67        res.push(current_expr);
68        last_element = Some(*expr)
69    }
70    // Push last element because it will be excluded by tuple_windows() above
71    if let Some(last) = exprs.iter().last() {
72        res.push(*last);
73    }
74
75    res
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use harper_core::parsers::StrParser;
82
83    #[test]
84    fn issue_1898() {
85        Typst.parse_str("#for ");
86        Typst.parse_str("#(.$#$$$. ");
87        Typst.parse_str("=#{m\"\".'m\"\"#p#");
88    }
89}