summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-syntax/src/ast.rs44
-rw-r--r--crates/typst-syntax/src/highlight.rs1
-rw-r--r--crates/typst-syntax/src/kind.rs3
-rw-r--r--crates/typst-syntax/src/parser.rs7
-rw-r--r--crates/typst/src/eval/import.rs69
-rw-r--r--tests/suite/scripting/import.typ45
-rw-r--r--tests/suite/scripting/module.typ1
-rw-r--r--tests/suite/scripting/modules/chap2.typ1
8 files changed, 149 insertions, 22 deletions
diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs
index 01e0944d..1cd9cd42 100644
--- a/crates/typst-syntax/src/ast.rs
+++ b/crates/typst-syntax/src/ast.rs
@@ -2040,29 +2040,54 @@ impl<'a> ImportItems<'a> {
pub fn iter(self) -> impl DoubleEndedIterator<Item = ImportItem<'a>> {
self.0.children().filter_map(|child| match child.kind() {
SyntaxKind::RenamedImportItem => child.cast().map(ImportItem::Renamed),
- SyntaxKind::Ident => child.cast().map(ImportItem::Simple),
+ SyntaxKind::ImportItemPath => child.cast().map(ImportItem::Simple),
_ => Option::None,
})
}
}
+node! {
+ /// A path to a submodule's imported name: `a.b.c`.
+ ImportItemPath
+}
+
+impl<'a> ImportItemPath<'a> {
+ /// An iterator over the path's components.
+ pub fn iter(self) -> impl DoubleEndedIterator<Item = Ident<'a>> {
+ self.0.children().filter_map(SyntaxNode::cast)
+ }
+
+ /// The name of the imported item. This is the last segment in the path.
+ pub fn name(self) -> Ident<'a> {
+ self.iter().last().unwrap_or_default()
+ }
+}
+
/// An imported item, potentially renamed to another identifier.
#[derive(Debug, Copy, Clone, Hash)]
pub enum ImportItem<'a> {
/// A non-renamed import (the item's name in the scope is the same as its
/// name).
- Simple(Ident<'a>),
+ Simple(ImportItemPath<'a>),
/// A renamed import (the item was bound to a different name in the scope
/// than the one it was defined as).
Renamed(RenamedImportItem<'a>),
}
impl<'a> ImportItem<'a> {
+ /// The path to the imported item.
+ pub fn path(self) -> ImportItemPath<'a> {
+ match self {
+ Self::Simple(path) => path,
+ Self::Renamed(renamed_item) => renamed_item.path(),
+ }
+ }
+
/// The original name of the imported item, at its source. This will be the
/// equal to the bound name if the item wasn't renamed with 'as'.
pub fn original_name(self) -> Ident<'a> {
match self {
- Self::Simple(name) => name,
+ Self::Simple(path) => path.name(),
Self::Renamed(renamed_item) => renamed_item.original_name(),
}
}
@@ -2071,7 +2096,7 @@ impl<'a> ImportItem<'a> {
/// name, if it was renamed; otherwise, it's just its original name.
pub fn bound_name(self) -> Ident<'a> {
match self {
- Self::Simple(name) => name,
+ Self::Simple(path) => path.name(),
Self::Renamed(renamed_item) => renamed_item.new_name(),
}
}
@@ -2083,17 +2108,22 @@ node! {
}
impl<'a> RenamedImportItem<'a> {
- /// The original name of the imported item (`a` in `a as d`).
- pub fn original_name(self) -> Ident<'a> {
+ /// The path to the imported item.
+ pub fn path(self) -> ImportItemPath<'a> {
self.0.cast_first_match().unwrap_or_default()
}
+ /// The original name of the imported item (`a` in `a as d` or `c.b.a as d`).
+ pub fn original_name(self) -> Ident<'a> {
+ self.path().name()
+ }
+
/// The new name of the imported item (`d` in `a as d`).
pub fn new_name(self) -> Ident<'a> {
self.0
.children()
.filter_map(SyntaxNode::cast)
- .nth(1)
+ .last()
.unwrap_or_default()
}
}
diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs
index b6aa9e8d..0c1f3d5f 100644
--- a/crates/typst-syntax/src/highlight.rs
+++ b/crates/typst-syntax/src/highlight.rs
@@ -277,6 +277,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
SyntaxKind::ForLoop => None,
SyntaxKind::ModuleImport => None,
SyntaxKind::ImportItems => None,
+ SyntaxKind::ImportItemPath => None,
SyntaxKind::RenamedImportItem => None,
SyntaxKind::ModuleInclude => None,
SyntaxKind::LoopBreak => None,
diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs
index 4c3d178f..7505dbc6 100644
--- a/crates/typst-syntax/src/kind.rs
+++ b/crates/typst-syntax/src/kind.rs
@@ -262,6 +262,8 @@ pub enum SyntaxKind {
ModuleImport,
/// Items to import from a module: `a, b, c`.
ImportItems,
+ /// A path to an imported name from a submodule: `a.b.c`.
+ ImportItemPath,
/// A renamed import item: `a as d`.
RenamedImportItem,
/// A module include: `include "chapter1.typ"`.
@@ -488,6 +490,7 @@ impl SyntaxKind {
Self::ForLoop => "for-loop expression",
Self::ModuleImport => "`import` expression",
Self::ImportItems => "import items",
+ Self::ImportItemPath => "imported item path",
Self::RenamedImportItem => "renamed import item",
Self::ModuleInclude => "`include` expression",
Self::LoopBreak => "`break` expression",
diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs
index d8ac1198..d341bca2 100644
--- a/crates/typst-syntax/src/parser.rs
+++ b/crates/typst-syntax/src/parser.rs
@@ -1003,6 +1003,13 @@ fn import_items(p: &mut Parser) {
p.unexpected();
}
+ // Nested import path: `a.b.c`
+ while p.eat_if(SyntaxKind::Dot) {
+ p.expect(SyntaxKind::Ident);
+ }
+
+ p.wrap(item_marker, SyntaxKind::ImportItemPath);
+
// Rename imported item.
if p.eat_if(SyntaxKind::As) {
p.expect(SyntaxKind::Ident);
diff --git a/crates/typst/src/eval/import.rs b/crates/typst/src/eval/import.rs
index 58857889..fbd55b7c 100644
--- a/crates/typst/src/eval/import.rs
+++ b/crates/typst/src/eval/import.rs
@@ -63,23 +63,62 @@ impl Eval for ast::ModuleImport<'_> {
Some(ast::Imports::Items(items)) => {
let mut errors = eco_vec![];
for item in items.iter() {
- let original_ident = item.original_name();
- if let Some(value) = scope.get(&original_ident) {
- // Warn on `import ...: x as x`
- if let ast::ImportItem::Renamed(renamed_item) = &item {
- if renamed_item.original_name().as_str()
- == renamed_item.new_name().as_str()
- {
- vm.engine.tracer.warn(warning!(
- renamed_item.new_name().span(),
- "unnecessary import rename to same name",
- ));
+ let mut path = item.path().iter().peekable();
+ let mut scope = scope;
+
+ while let Some(component) = &path.next() {
+ let Some(value) = scope.get(component) else {
+ errors.push(error!(component.span(), "unresolved import"));
+ break;
+ };
+
+ if path.peek().is_some() {
+ // Nested import, as this is not the last component.
+ // This must be a submodule.
+ let Some(submodule) = value.scope() else {
+ let error = if matches!(value, Value::Func(function) if function.scope().is_none())
+ {
+ error!(
+ component.span(),
+ "cannot import from user-defined functions"
+ )
+ } else if !matches!(
+ value,
+ Value::Func(_) | Value::Module(_) | Value::Type(_)
+ ) {
+ error!(
+ component.span(),
+ "expected module, function, or type, found {}",
+ value.ty()
+ )
+ } else {
+ panic!("unexpected nested import failure")
+ };
+ errors.push(error);
+ break;
+ };
+
+ // Walk into the submodule.
+ scope = submodule;
+ } else {
+ // Now that we have the scope of the innermost submodule
+ // in the import path, we may extract the desired item from
+ // it.
+
+ // Warn on `import ...: x as x`
+ if let ast::ImportItem::Renamed(renamed_item) = &item {
+ if renamed_item.original_name().as_str()
+ == renamed_item.new_name().as_str()
+ {
+ vm.engine.tracer.warn(warning!(
+ renamed_item.new_name().span(),
+ "unnecessary import rename to same name",
+ ));
+ }
}
- }
- vm.define(item.bound_name(), value.clone());
- } else {
- errors.push(error!(original_ident.span(), "unresolved import"));
+ vm.define(item.bound_name(), value.clone());
+ }
}
}
if !errors.is_empty() {
diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ
index 820f81d6..8bfa8ca6 100644
--- a/tests/suite/scripting/import.typ
+++ b/tests/suite/scripting/import.typ
@@ -45,6 +45,16 @@
#test(val, 1)
#test(other(1, 2), 3)
+--- import-nested-item ---
+// Nested item imports.
+#import "modules/chap1.typ" as orig-chap1
+#import "modules/chap2.typ" as orig-chap2
+#import "module.typ": chap2, chap2.name, chap2.chap1, chap2.chap1.name as othername
+#test(chap2, orig-chap2)
+#test(chap1, orig-chap1)
+#test(name, "Klaus")
+#test(othername, "Klaus")
+
--- import-from-function-scope ---
// Test importing from function scopes.
@@ -63,6 +73,28 @@
#import assert: eq as aseq
#aseq(10, 10)
+--- import-from-function-scope-nested-import ---
+// Test importing items from function scopes via nested import.
+#import std: grid.cell, table.cell as tcell
+#test(cell, grid.cell)
+#test(tcell, table.cell)
+
+--- import-from-type-scope ---
+// Test importing from a type's scope.
+#import array: zip
+#test(zip((1, 2), (3, 4)), ((1, 3), (2, 4)))
+
+--- import-from-type-scope-item-renamed ---
+// Test importing from a type's scope with renaming.
+#import array: pop as renamed-pop
+#test(renamed-pop((1, 2)), 2)
+
+--- import-from-type-scope-nested-import ---
+// Test importing from a type's scope with nested import.
+#import std: array.zip, array.pop as renamed-pop
+#test(zip((1, 2), (3, 4)), ((1, 3), (2, 4)))
+#test(renamed-pop((1, 2)), 2)
+
--- import-from-file-bare ---
// A module import without items.
#import "module.typ"
@@ -225,6 +257,10 @@ This is never reached.
// Error: 7-12 unknown variable: chap1
#test(chap1.b, "Klaus")
+--- import-nested-invalid-type ---
+// Error: 19-21 expected module, function, or type, found float
+#import std: calc.pi.something
+
--- import-incomplete ---
// Error: 8 expected expression
#import
@@ -262,6 +298,15 @@ This is never reached.
// Error: 16-17 unexpected integer
#import "": a: 1
+--- import-incomplete-nested ---
+// Error: 15 expected identifier
+#import "": a.
+
+--- import-wildcard-in-nested ---
+// Error: 15 expected identifier
+// Error: 15-16 unexpected star
+#import "": a.*
+
--- import-missing-comma ---
// Error: 14 expected comma
#import "": a b
diff --git a/tests/suite/scripting/module.typ b/tests/suite/scripting/module.typ
index 8a67d225..8e388bfb 100644
--- a/tests/suite/scripting/module.typ
+++ b/tests/suite/scripting/module.typ
@@ -1,5 +1,6 @@
// SKIP
// A file to import in import / include tests.
+#import "modules/chap2.typ"
#let a
#let b = 1
diff --git a/tests/suite/scripting/modules/chap2.typ b/tests/suite/scripting/modules/chap2.typ
index 9c9d12d7..f1a886d6 100644
--- a/tests/suite/scripting/modules/chap2.typ
+++ b/tests/suite/scripting/modules/chap2.typ
@@ -1,4 +1,5 @@
// SKIP
+#import "chap1.typ"
#let name = "Klaus"
== Chapter 2