mirror of
https://github.com/typst/typst
synced 2025-05-15 01:25:28 +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>> {
|
pub fn iter(self) -> impl DoubleEndedIterator<Item = ImportItem<'a>> {
|
||||||
self.0.children().filter_map(|child| match child.kind() {
|
self.0.children().filter_map(|child| match child.kind() {
|
||||||
SyntaxKind::RenamedImportItem => child.cast().map(ImportItem::Renamed),
|
SyntaxKind::RenamedImportItem => child.cast().map(ImportItem::Renamed),
|
||||||
SyntaxKind::Ident => child.cast().map(ImportItem::Simple),
|
SyntaxKind::ImportItemPath => child.cast().map(ImportItem::Simple),
|
||||||
_ => Option::None,
|
_ => 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.
|
/// An imported item, potentially renamed to another identifier.
|
||||||
#[derive(Debug, Copy, Clone, Hash)]
|
#[derive(Debug, Copy, Clone, Hash)]
|
||||||
pub enum ImportItem<'a> {
|
pub enum ImportItem<'a> {
|
||||||
/// A non-renamed import (the item's name in the scope is the same as its
|
/// A non-renamed import (the item's name in the scope is the same as its
|
||||||
/// name).
|
/// name).
|
||||||
Simple(Ident<'a>),
|
Simple(ImportItemPath<'a>),
|
||||||
/// A renamed import (the item was bound to a different name in the scope
|
/// A renamed import (the item was bound to a different name in the scope
|
||||||
/// than the one it was defined as).
|
/// than the one it was defined as).
|
||||||
Renamed(RenamedImportItem<'a>),
|
Renamed(RenamedImportItem<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ImportItem<'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
|
/// 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'.
|
/// equal to the bound name if the item wasn't renamed with 'as'.
|
||||||
pub fn original_name(self) -> Ident<'a> {
|
pub fn original_name(self) -> Ident<'a> {
|
||||||
match self {
|
match self {
|
||||||
Self::Simple(name) => name,
|
Self::Simple(path) => path.name(),
|
||||||
Self::Renamed(renamed_item) => renamed_item.original_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.
|
/// name, if it was renamed; otherwise, it's just its original name.
|
||||||
pub fn bound_name(self) -> Ident<'a> {
|
pub fn bound_name(self) -> Ident<'a> {
|
||||||
match self {
|
match self {
|
||||||
Self::Simple(name) => name,
|
Self::Simple(path) => path.name(),
|
||||||
Self::Renamed(renamed_item) => renamed_item.new_name(),
|
Self::Renamed(renamed_item) => renamed_item.new_name(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2083,17 +2108,22 @@ node! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RenamedImportItem<'a> {
|
impl<'a> RenamedImportItem<'a> {
|
||||||
/// The original name of the imported item (`a` in `a as d`).
|
/// The path to the imported item.
|
||||||
pub fn original_name(self) -> Ident<'a> {
|
pub fn path(self) -> ImportItemPath<'a> {
|
||||||
self.0.cast_first_match().unwrap_or_default()
|
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`).
|
/// The new name of the imported item (`d` in `a as d`).
|
||||||
pub fn new_name(self) -> Ident<'a> {
|
pub fn new_name(self) -> Ident<'a> {
|
||||||
self.0
|
self.0
|
||||||
.children()
|
.children()
|
||||||
.filter_map(SyntaxNode::cast)
|
.filter_map(SyntaxNode::cast)
|
||||||
.nth(1)
|
.last()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,6 +277,7 @@ pub fn highlight(node: &LinkedNode) -> Option<Tag> {
|
|||||||
SyntaxKind::ForLoop => None,
|
SyntaxKind::ForLoop => None,
|
||||||
SyntaxKind::ModuleImport => None,
|
SyntaxKind::ModuleImport => None,
|
||||||
SyntaxKind::ImportItems => None,
|
SyntaxKind::ImportItems => None,
|
||||||
|
SyntaxKind::ImportItemPath => None,
|
||||||
SyntaxKind::RenamedImportItem => None,
|
SyntaxKind::RenamedImportItem => None,
|
||||||
SyntaxKind::ModuleInclude => None,
|
SyntaxKind::ModuleInclude => None,
|
||||||
SyntaxKind::LoopBreak => None,
|
SyntaxKind::LoopBreak => None,
|
||||||
|
@ -262,6 +262,8 @@ pub enum SyntaxKind {
|
|||||||
ModuleImport,
|
ModuleImport,
|
||||||
/// Items to import from a module: `a, b, c`.
|
/// Items to import from a module: `a, b, c`.
|
||||||
ImportItems,
|
ImportItems,
|
||||||
|
/// A path to an imported name from a submodule: `a.b.c`.
|
||||||
|
ImportItemPath,
|
||||||
/// A renamed import item: `a as d`.
|
/// A renamed import item: `a as d`.
|
||||||
RenamedImportItem,
|
RenamedImportItem,
|
||||||
/// A module include: `include "chapter1.typ"`.
|
/// A module include: `include "chapter1.typ"`.
|
||||||
@ -488,6 +490,7 @@ impl SyntaxKind {
|
|||||||
Self::ForLoop => "for-loop expression",
|
Self::ForLoop => "for-loop expression",
|
||||||
Self::ModuleImport => "`import` expression",
|
Self::ModuleImport => "`import` expression",
|
||||||
Self::ImportItems => "import items",
|
Self::ImportItems => "import items",
|
||||||
|
Self::ImportItemPath => "imported item path",
|
||||||
Self::RenamedImportItem => "renamed import item",
|
Self::RenamedImportItem => "renamed import item",
|
||||||
Self::ModuleInclude => "`include` expression",
|
Self::ModuleInclude => "`include` expression",
|
||||||
Self::LoopBreak => "`break` expression",
|
Self::LoopBreak => "`break` expression",
|
||||||
|
@ -1003,6 +1003,13 @@ fn import_items(p: &mut Parser) {
|
|||||||
p.unexpected();
|
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.
|
// Rename imported item.
|
||||||
if p.eat_if(SyntaxKind::As) {
|
if p.eat_if(SyntaxKind::As) {
|
||||||
p.expect(SyntaxKind::Ident);
|
p.expect(SyntaxKind::Ident);
|
||||||
|
@ -63,23 +63,62 @@ impl Eval for ast::ModuleImport<'_> {
|
|||||||
Some(ast::Imports::Items(items)) => {
|
Some(ast::Imports::Items(items)) => {
|
||||||
let mut errors = eco_vec![];
|
let mut errors = eco_vec![];
|
||||||
for item in items.iter() {
|
for item in items.iter() {
|
||||||
let original_ident = item.original_name();
|
let mut path = item.path().iter().peekable();
|
||||||
if let Some(value) = scope.get(&original_ident) {
|
let mut scope = scope;
|
||||||
// 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());
|
while let Some(component) = &path.next() {
|
||||||
} else {
|
let Some(value) = scope.get(component) else {
|
||||||
errors.push(error!(original_ident.span(), "unresolved import"));
|
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() {
|
if !errors.is_empty() {
|
||||||
|
@ -45,6 +45,16 @@
|
|||||||
#test(val, 1)
|
#test(val, 1)
|
||||||
#test(other(1, 2), 3)
|
#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 ---
|
--- import-from-function-scope ---
|
||||||
// Test importing from function scopes.
|
// Test importing from function scopes.
|
||||||
|
|
||||||
@ -63,6 +73,28 @@
|
|||||||
#import assert: eq as aseq
|
#import assert: eq as aseq
|
||||||
#aseq(10, 10)
|
#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 ---
|
--- import-from-file-bare ---
|
||||||
// A module import without items.
|
// A module import without items.
|
||||||
#import "module.typ"
|
#import "module.typ"
|
||||||
@ -225,6 +257,10 @@ This is never reached.
|
|||||||
// Error: 7-12 unknown variable: chap1
|
// Error: 7-12 unknown variable: chap1
|
||||||
#test(chap1.b, "Klaus")
|
#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 ---
|
--- import-incomplete ---
|
||||||
// Error: 8 expected expression
|
// Error: 8 expected expression
|
||||||
#import
|
#import
|
||||||
@ -262,6 +298,15 @@ This is never reached.
|
|||||||
// Error: 16-17 unexpected integer
|
// Error: 16-17 unexpected integer
|
||||||
#import "": a: 1
|
#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 ---
|
--- import-missing-comma ---
|
||||||
// Error: 14 expected comma
|
// Error: 14 expected comma
|
||||||
#import "": a b
|
#import "": a b
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// SKIP
|
// SKIP
|
||||||
// A file to import in import / include tests.
|
// A file to import in import / include tests.
|
||||||
|
#import "modules/chap2.typ"
|
||||||
|
|
||||||
#let a
|
#let a
|
||||||
#let b = 1
|
#let b = 1
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// SKIP
|
// SKIP
|
||||||
|
#import "chap1.typ"
|
||||||
#let name = "Klaus"
|
#let name = "Klaus"
|
||||||
|
|
||||||
== Chapter 2
|
== Chapter 2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user