mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Add nested import syntax (#4228)
Co-authored-by: LuizAugustoPapa <luiz.papa@aluno.puc-rio.br> Co-authored-by: PepinhoJp <pepinho.jp@gmail.com> Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
This commit is contained in:
parent
5f6d942519
commit
06a925a0ee
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
vm.define(item.bound_name(), value.clone());
|
||||
} else {
|
||||
errors.push(error!(original_ident.span(), "unresolved import"));
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,6 @@
|
||||
// SKIP
|
||||
// A file to import in import / include tests.
|
||||
#import "modules/chap2.typ"
|
||||
|
||||
#let a
|
||||
#let b = 1
|
||||
|
@ -1,4 +1,5 @@
|
||||
// SKIP
|
||||
#import "chap1.typ"
|
||||
#let name = "Klaus"
|
||||
|
||||
== Chapter 2
|
||||
|
Loading…
x
Reference in New Issue
Block a user