mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
236 lines
6.7 KiB
Rust
236 lines
6.7 KiB
Rust
use std::borrow::Borrow;
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use ecow::EcoString;
|
|
use typst::diag::{FileError, FileResult};
|
|
use typst::foundations::{Bytes, Datetime, Smart};
|
|
use typst::layout::{Abs, Margin, PageElem};
|
|
use typst::syntax::package::{PackageSpec, PackageVersion};
|
|
use typst::syntax::{FileId, Source, VirtualPath};
|
|
use typst::text::{Font, FontBook, TextElem, TextSize};
|
|
use typst::utils::{singleton, LazyHash};
|
|
use typst::{Library, World};
|
|
|
|
use crate::IdeWorld;
|
|
|
|
/// A world for IDE testing.
|
|
#[derive(Clone)]
|
|
pub struct TestWorld {
|
|
pub main: Source,
|
|
files: Arc<TestFiles>,
|
|
base: &'static TestBase,
|
|
}
|
|
|
|
impl TestWorld {
|
|
/// Create a new world for a single test.
|
|
///
|
|
/// This is cheap because the shared base for all test runs is lazily
|
|
/// initialized just once.
|
|
pub fn new(text: &str) -> Self {
|
|
let main = Source::new(Self::main_id(), text.into());
|
|
Self {
|
|
main,
|
|
files: Arc::new(TestFiles::default()),
|
|
base: singleton!(TestBase, TestBase::default()),
|
|
}
|
|
}
|
|
|
|
/// Add an additional source file to the test world.
|
|
pub fn with_source(mut self, path: &str, text: &str) -> Self {
|
|
let id = FileId::new(None, VirtualPath::new(path));
|
|
let source = Source::new(id, text.into());
|
|
Arc::make_mut(&mut self.files).sources.insert(id, source);
|
|
self
|
|
}
|
|
|
|
/// Add an additional asset file to the test world.
|
|
#[track_caller]
|
|
pub fn with_asset(self, filename: &str) -> Self {
|
|
self.with_asset_at(filename, filename)
|
|
}
|
|
|
|
/// Add an additional asset file to the test world.
|
|
#[track_caller]
|
|
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
|
|
let id = FileId::new(None, VirtualPath::new(path));
|
|
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
|
let bytes = Bytes::new(data);
|
|
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
|
|
self
|
|
}
|
|
|
|
/// The ID of the main file in a `TestWorld`.
|
|
pub fn main_id() -> FileId {
|
|
*singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ")))
|
|
}
|
|
}
|
|
|
|
impl World for TestWorld {
|
|
fn library(&self) -> &LazyHash<Library> {
|
|
&self.base.library
|
|
}
|
|
|
|
fn book(&self) -> &LazyHash<FontBook> {
|
|
&self.base.book
|
|
}
|
|
|
|
fn main(&self) -> FileId {
|
|
self.main.id()
|
|
}
|
|
|
|
fn source(&self, id: FileId) -> FileResult<Source> {
|
|
if id == self.main.id() {
|
|
Ok(self.main.clone())
|
|
} else if let Some(source) = self.files.sources.get(&id) {
|
|
Ok(source.clone())
|
|
} else {
|
|
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
|
}
|
|
}
|
|
|
|
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
|
match self.files.assets.get(&id) {
|
|
Some(bytes) => Ok(bytes.clone()),
|
|
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
|
|
}
|
|
}
|
|
|
|
fn font(&self, index: usize) -> Option<Font> {
|
|
Some(self.base.fonts[index].clone())
|
|
}
|
|
|
|
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl IdeWorld for TestWorld {
|
|
fn upcast(&self) -> &dyn World {
|
|
self
|
|
}
|
|
|
|
fn files(&self) -> Vec<FileId> {
|
|
std::iter::once(self.main.id())
|
|
.chain(self.files.sources.keys().copied())
|
|
.chain(self.files.assets.keys().copied())
|
|
.collect()
|
|
}
|
|
|
|
fn packages(&self) -> &[(PackageSpec, Option<EcoString>)] {
|
|
const LIST: &[(PackageSpec, Option<EcoString>)] = &[(
|
|
PackageSpec {
|
|
// NOTE: This literal, `"preview"`, should match the const, `DEFAULT_NAMESPACE`,
|
|
// defined in `crates/typst-kit/src/package.rs`. However, we should always use the
|
|
// literal here, not `DEFAULT_NAMESPACE`, so that this test fails if its value
|
|
// changes in an unexpected way.
|
|
namespace: EcoString::inline("preview"),
|
|
name: EcoString::inline("example"),
|
|
version: PackageVersion { major: 0, minor: 1, patch: 0 },
|
|
},
|
|
None,
|
|
)];
|
|
LIST
|
|
}
|
|
}
|
|
|
|
/// Test-specific files.
|
|
#[derive(Default, Clone)]
|
|
struct TestFiles {
|
|
assets: HashMap<FileId, Bytes>,
|
|
sources: HashMap<FileId, Source>,
|
|
}
|
|
|
|
/// Shared foundation of all test worlds.
|
|
struct TestBase {
|
|
library: LazyHash<Library>,
|
|
book: LazyHash<FontBook>,
|
|
fonts: Vec<Font>,
|
|
}
|
|
|
|
impl Default for TestBase {
|
|
fn default() -> Self {
|
|
let fonts: Vec<_> = typst_assets::fonts()
|
|
.chain(typst_dev_assets::fonts())
|
|
.flat_map(|data| Font::iter(Bytes::new(data)))
|
|
.collect();
|
|
|
|
Self {
|
|
library: LazyHash::new(library()),
|
|
book: LazyHash::new(FontBook::from_fonts(&fonts)),
|
|
fonts,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The extended standard library for testing.
|
|
fn library() -> Library {
|
|
// Set page width to 120pt with 10pt margins, so that the inner page is
|
|
// exactly 100pt wide. Page height is unbounded and font size is 10pt so
|
|
// that it multiplies to nice round numbers.
|
|
let mut lib = typst::Library::default();
|
|
lib.styles
|
|
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
|
|
lib.styles.set(PageElem::set_height(Smart::Auto));
|
|
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
|
|
Abs::pt(10.0).into(),
|
|
)))));
|
|
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
|
|
lib
|
|
}
|
|
|
|
/// The input to a test: Either just a string or a full `TestWorld`.
|
|
pub trait WorldLike {
|
|
type World: Borrow<TestWorld>;
|
|
|
|
fn acquire(self) -> Self::World;
|
|
}
|
|
|
|
impl<'a> WorldLike for &'a TestWorld {
|
|
type World = &'a TestWorld;
|
|
|
|
fn acquire(self) -> Self::World {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl WorldLike for &str {
|
|
type World = TestWorld;
|
|
|
|
fn acquire(self) -> Self::World {
|
|
TestWorld::new(self)
|
|
}
|
|
}
|
|
|
|
/// Specifies a position in a file for a test.
|
|
pub trait FilePos {
|
|
fn resolve(self, world: &TestWorld) -> (Source, usize);
|
|
}
|
|
|
|
impl FilePos for isize {
|
|
#[track_caller]
|
|
fn resolve(self, world: &TestWorld) -> (Source, usize) {
|
|
(world.main.clone(), cursor(&world.main, self))
|
|
}
|
|
}
|
|
|
|
impl FilePos for (&str, isize) {
|
|
#[track_caller]
|
|
fn resolve(self, world: &TestWorld) -> (Source, usize) {
|
|
let id = FileId::new(None, VirtualPath::new(self.0));
|
|
let source = world.source(id).unwrap();
|
|
let cursor = cursor(&source, self.1);
|
|
(source, cursor)
|
|
}
|
|
}
|
|
|
|
/// Resolve a signed index (negative from the back) to a unsigned index.
|
|
#[track_caller]
|
|
fn cursor(source: &Source, cursor: isize) -> usize {
|
|
if cursor < 0 {
|
|
source.len_bytes().checked_add_signed(cursor + 1).unwrap()
|
|
} else {
|
|
cursor as usize
|
|
}
|
|
}
|