tinymist_query/analysis/
link_expr.rs

1//! Analyze link expressions in a source file.
2
3use std::str::FromStr;
4
5use lsp_types::Url;
6use tinymist_world::package::PackageSpec;
7
8use super::prelude::*;
9
10/// Get link expressions from a source.
11#[typst_macros::time(span = src.root().span())]
12#[comemo::memoize]
13pub fn get_link_exprs(src: &Source) -> Arc<LinkInfo> {
14    let root = LinkedNode::new(src.root());
15    Arc::new(get_link_exprs_in(&root).unwrap_or_default())
16}
17
18/// Get link expressions in a source node.
19pub fn get_link_exprs_in(node: &LinkedNode) -> Option<LinkInfo> {
20    let mut worker = LinkStrWorker {
21        info: LinkInfo::default(),
22    };
23    worker.collect_links(node)?;
24    Some(worker.info)
25}
26
27/// Link information in a source file.
28#[derive(Debug, Default)]
29pub struct LinkInfo {
30    /// The link objects in a source file.
31    pub objects: Vec<LinkObject>,
32}
33
34/// A link object in a source file.
35#[derive(Debug)]
36pub struct LinkObject {
37    /// The range of the link expression.
38    pub range: Range<usize>,
39    /// The span of the link expression.
40    pub span: Span,
41    /// The target of the link.
42    pub target: LinkTarget,
43}
44
45/// A valid link target.
46#[derive(Debug)]
47pub enum LinkTarget {
48    /// A package specification.
49    Package(Box<PackageSpec>),
50    /// A URL.
51    Url(Box<Url>),
52    /// A file path.
53    Path(TypstFileId, EcoString),
54}
55
56impl LinkTarget {
57    pub(crate) fn resolve(&self, ctx: &mut LocalContext) -> Option<Url> {
58        match self {
59            LinkTarget::Package(..) => None,
60            LinkTarget::Url(url) => Some(url.as_ref().clone()),
61            LinkTarget::Path(id, path) => {
62                // Avoid creating new ids here.
63                let root = ctx.path_for_id(id.join("/")).ok()?;
64                let path_in_workspace = id.vpath().join(Path::new(path.as_str()));
65                let path = root.resolve_to(&path_in_workspace)?;
66                crate::path_res_to_url(path).ok()
67            }
68        }
69    }
70}
71
72struct LinkStrWorker {
73    info: LinkInfo,
74}
75
76impl LinkStrWorker {
77    fn collect_links(&mut self, node: &LinkedNode) -> Option<()> {
78        match node.kind() {
79            // SyntaxKind::Link => { }
80            SyntaxKind::FuncCall => {
81                let fc = self.analyze_call(node);
82                if fc.is_some() {
83                    return Some(());
84                }
85            }
86            SyntaxKind::Include => {
87                let inc = node.cast::<ast::ModuleInclude>()?;
88                let path = inc.source();
89                self.analyze_path_expr(node, path);
90            }
91            // early exit
92            kind if kind.is_trivia() || kind.is_keyword() || kind.is_error() => return Some(()),
93            _ => {}
94        };
95
96        for child in node.children() {
97            self.collect_links(&child);
98        }
99
100        Some(())
101    }
102
103    fn analyze_call(&mut self, node: &LinkedNode) -> Option<()> {
104        let call = node.cast::<ast::FuncCall>()?;
105        let mut callee = call.callee();
106        'check_link_fn: loop {
107            match callee {
108                ast::Expr::FieldAccess(fa) => {
109                    let target = fa.target();
110                    let ast::Expr::Ident(ident) = target else {
111                        return None;
112                    };
113                    if ident.get().as_str() != "std" {
114                        return None;
115                    }
116                    callee = ast::Expr::Ident(fa.field());
117                    continue 'check_link_fn;
118                }
119                ast::Expr::Ident(ident) => match ident.get().as_str() {
120                    "raw" => {
121                        self.analyze_reader(node, call, "theme", false);
122                        self.analyze_reader(node, call, "syntaxes", false);
123                    }
124                    "bibliography" => {
125                        self.analyze_reader(node, call, "cite", false);
126                        self.analyze_bibliography_style(node, call);
127                        self.analyze_reader(node, call, "path", true);
128                    }
129                    "cbor" | "csv" | "image" | "read" | "json" | "yaml" | "xml" => {
130                        self.analyze_reader(node, call, "path", true);
131                    }
132                    _ => return None,
133                },
134                _ => return None,
135            }
136            return None;
137        }
138    }
139
140    fn analyze_bibliography_style(&mut self, node: &LinkedNode, call: ast::FuncCall) -> Option<()> {
141        for item in call.args().items() {
142            match item {
143                ast::Arg::Named(named) if named.name().get().as_str() == "style" => {
144                    if let ast::Expr::Str(style) = named.expr()
145                        && hayagriva::archive::ArchivedStyle::by_name(&style.get()).is_some()
146                    {
147                        return Some(());
148                    }
149                    self.analyze_path_expr(node, named.expr());
150                    return Some(());
151                }
152                _ => {}
153            }
154        }
155        Some(())
156    }
157
158    fn analyze_reader(
159        &mut self,
160        node: &LinkedNode,
161        call: ast::FuncCall,
162        key: &str,
163        pos: bool,
164    ) -> Option<()> {
165        let arg = call.args().items().next()?;
166        match arg {
167            ast::Arg::Pos(s) if pos => {
168                self.analyze_path_expr(node, s);
169            }
170            _ => {}
171        }
172        for item in call.args().items() {
173            match item {
174                ast::Arg::Named(named) if named.name().get().as_str() == key => {
175                    self.analyze_path_expr(node, named.expr());
176                }
177                _ => {}
178            }
179        }
180        Some(())
181    }
182
183    fn analyze_path_expr(&mut self, node: &LinkedNode, path_expr: ast::Expr) -> Option<()> {
184        match path_expr {
185            ast::Expr::Str(s) => self.analyze_path_str(node, s),
186            ast::Expr::Array(a) => {
187                for item in a.items() {
188                    if let ast::ArrayItem::Pos(ast::Expr::Str(s)) = item {
189                        self.analyze_path_str(node, s);
190                    }
191                }
192                Some(())
193            }
194            _ => None,
195        }
196    }
197
198    fn analyze_path_str(&mut self, node: &LinkedNode, s: ast::Str<'_>) -> Option<()> {
199        let str_node = node.find(s.span())?;
200        let str_range = str_node.range();
201        let range = str_range.start + 1..str_range.end - 1;
202        if range.is_empty() {
203            return None;
204        }
205
206        let content = s.get();
207        if content.starts_with('@') {
208            let pkg_spec = PackageSpec::from_str(&content).ok()?;
209            self.info.objects.push(LinkObject {
210                range,
211                span: s.span(),
212                target: LinkTarget::Package(Box::new(pkg_spec)),
213            });
214            return Some(());
215        }
216
217        let id = node.span().id()?;
218        self.info.objects.push(LinkObject {
219            range,
220            span: s.span(),
221            target: LinkTarget::Path(id, content),
222        });
223        Some(())
224    }
225}