mirror of
https://github.com/typst/typst
synced 2025-08-02 17:27:54 +08:00
Compare commits
13 Commits
1c310a91d3
...
71401406cd
Author | SHA1 | Date | |
---|---|---|---|
|
71401406cd | ||
|
b790c6d59c | ||
|
b1c79b50d4 | ||
|
4629ede020 | ||
|
627f5b9d4f | ||
|
143732d2a8 | ||
|
efe81a1aae | ||
|
3b5ee3c488 | ||
|
f3d5cdc6a5 | ||
|
85e03abe45 | ||
|
c354369b05 | ||
|
91ec946283 | ||
|
a5ef75a9c2 |
@ -174,7 +174,10 @@ typst help watch
|
|||||||
```
|
```
|
||||||
|
|
||||||
If you prefer an integrated IDE-like experience with autocompletion and instant
|
If you prefer an integrated IDE-like experience with autocompletion and instant
|
||||||
preview, you can also check out [Typst's free web app][app].
|
preview, you can also check out our [free web app][app]. Alternatively, there is
|
||||||
|
a community-created language server called
|
||||||
|
[Tinymist](https://myriad-dreamin.github.io/tinymist/) which is integrated into
|
||||||
|
various editor extensions.
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
The main places where the community gathers are our [Forum][forum] and our
|
The main places where the community gathers are our [Forum][forum] and our
|
||||||
|
@ -404,7 +404,9 @@ fn system_path(
|
|||||||
|
|
||||||
// Join the path to the root. If it tries to escape, deny
|
// Join the path to the root. If it tries to escape, deny
|
||||||
// access. Note: It can still escape via symlinks.
|
// access. Note: It can still escape via symlinks.
|
||||||
id.vpath().resolve(root).ok_or(FileError::AccessDenied)
|
id.vpath()
|
||||||
|
.resolve(root)
|
||||||
|
.ok_or_else(|| FileError::AccessDenied(id.vpath().as_rootless_path().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a file from a `FileId`.
|
/// Reads a file from a `FileId`.
|
||||||
@ -427,7 +429,7 @@ fn read(
|
|||||||
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
|
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
|
||||||
let f = |e| FileError::from_io(e, path);
|
let f = |e| FileError::from_io(e, path);
|
||||||
if fs::metadata(path).map_err(f)?.is_dir() {
|
if fs::metadata(path).map_err(f)?.is_dir() {
|
||||||
Err(FileError::IsDirectory)
|
Err(FileError::IsDirectory(path.into()))
|
||||||
} else {
|
} else {
|
||||||
fs::read(path).map_err(f)
|
fs::read(path).map_err(f)
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,8 @@ use typst_library::model::{
|
|||||||
RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
RefElem, StrongElem, TableCell, TableElem, TermsElem,
|
||||||
};
|
};
|
||||||
use typst_library::text::{
|
use typst_library::text::{
|
||||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
|
||||||
SubElem, SuperElem, UnderlineElem,
|
SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
|
||||||
};
|
};
|
||||||
use typst_library::visualize::ImageElem;
|
use typst_library::visualize::ImageElem;
|
||||||
|
|
||||||
@ -47,6 +47,7 @@ pub fn register(rules: &mut NativeRuleMap) {
|
|||||||
rules.register(Html, OVERLINE_RULE);
|
rules.register(Html, OVERLINE_RULE);
|
||||||
rules.register(Html, STRIKE_RULE);
|
rules.register(Html, STRIKE_RULE);
|
||||||
rules.register(Html, HIGHLIGHT_RULE);
|
rules.register(Html, HIGHLIGHT_RULE);
|
||||||
|
rules.register(Html, SMALLCAPS_RULE);
|
||||||
rules.register(Html, RAW_RULE);
|
rules.register(Html, RAW_RULE);
|
||||||
rules.register(Html, RAW_LINE_RULE);
|
rules.register(Html, RAW_LINE_RULE);
|
||||||
|
|
||||||
@ -390,6 +391,20 @@ const STRIKE_RULE: ShowFn<StrikeElem> =
|
|||||||
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|
||||||
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
|
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
|
||||||
|
|
||||||
|
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
|
||||||
|
Ok(HtmlElem::new(tag::span)
|
||||||
|
.with_attr(
|
||||||
|
attr::style,
|
||||||
|
if elem.all.get(styles) {
|
||||||
|
"font-variant-caps: all-small-caps"
|
||||||
|
} else {
|
||||||
|
"font-variant-caps: small-caps"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_body(Some(elem.body.clone()))
|
||||||
|
.pack())
|
||||||
|
};
|
||||||
|
|
||||||
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
|
||||||
let lines = elem.lines.as_deref().unwrap_or_default();
|
let lines = elem.lines.as_deref().unwrap_or_default();
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ impl PackageStorage {
|
|||||||
) -> PackageResult<PathBuf> {
|
) -> PackageResult<PathBuf> {
|
||||||
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
|
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
|
||||||
|
|
||||||
|
// By default, search for the package locally
|
||||||
if let Some(packages_dir) = &self.package_path {
|
if let Some(packages_dir) = &self.package_path {
|
||||||
let dir = packages_dir.join(&subdir);
|
let dir = packages_dir.join(&subdir);
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
@ -94,6 +95,7 @@ impl PackageStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As a fallback, look into the cache and possibly download from network.
|
||||||
if let Some(cache_dir) = &self.package_cache_path {
|
if let Some(cache_dir) = &self.package_cache_path {
|
||||||
let dir = cache_dir.join(&subdir);
|
let dir = cache_dir.join(&subdir);
|
||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
@ -102,14 +104,42 @@ impl PackageStorage {
|
|||||||
|
|
||||||
// Download from network if it doesn't exist yet.
|
// Download from network if it doesn't exist yet.
|
||||||
if spec.namespace == DEFAULT_NAMESPACE {
|
if spec.namespace == DEFAULT_NAMESPACE {
|
||||||
self.download_package(spec, cache_dir, progress)?;
|
return self.download_package(spec, cache_dir, progress);
|
||||||
if dir.exists() {
|
|
||||||
return Ok(dir);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(PackageError::NotFound(spec.clone()))
|
// None of the strategies above found the package, so all code paths
|
||||||
|
// from now on fail. The rest of the function is only to determine the
|
||||||
|
// cause of the failure.
|
||||||
|
// We try `namespace/` then `namespace/name/` then `namespace/name/version/`
|
||||||
|
// and see where the first error occurs.
|
||||||
|
let not_found = |msg| Err(PackageError::NotFound(spec.clone(), msg));
|
||||||
|
|
||||||
|
let Some(packages_dir) = &self.package_path else {
|
||||||
|
return not_found(eco_format!("cannot access local package storage"));
|
||||||
|
};
|
||||||
|
let namespace_dir = packages_dir.join(format!("{}", spec.namespace));
|
||||||
|
if !namespace_dir.exists() {
|
||||||
|
return not_found(eco_format!(
|
||||||
|
"namespace @{} should be located at {}",
|
||||||
|
spec.namespace,
|
||||||
|
namespace_dir.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let package_dir = namespace_dir.join(format!("{}", spec.name));
|
||||||
|
if !package_dir.exists() {
|
||||||
|
return not_found(eco_format!(
|
||||||
|
"{} does not have package '{}'",
|
||||||
|
namespace_dir.display(),
|
||||||
|
spec.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let latest = self.determine_latest_version(&spec.versionless()).ok();
|
||||||
|
Err(PackageError::VersionNotFound(
|
||||||
|
spec.clone(),
|
||||||
|
latest,
|
||||||
|
eco_format!("{}", namespace_dir.display()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to determine the latest version of a package.
|
/// Tries to determine the latest version of a package.
|
||||||
@ -171,21 +201,29 @@ impl PackageStorage {
|
|||||||
spec: &PackageSpec,
|
spec: &PackageSpec,
|
||||||
cache_dir: &Path,
|
cache_dir: &Path,
|
||||||
progress: &mut dyn Progress,
|
progress: &mut dyn Progress,
|
||||||
) -> PackageResult<()> {
|
) -> PackageResult<PathBuf> {
|
||||||
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
|
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
|
||||||
|
|
||||||
let url = format!(
|
let namespace_url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}");
|
||||||
"{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz",
|
let url = format!("{namespace_url}/{}-{}.tar.gz", spec.name, spec.version);
|
||||||
spec.name, spec.version
|
|
||||||
);
|
|
||||||
|
|
||||||
let data = match self.downloader.download_with_progress(&url, progress) {
|
let data = match self.downloader.download_with_progress(&url, progress) {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(ureq::Error::Status(404, _)) => {
|
Err(ureq::Error::Status(404, _)) => {
|
||||||
if let Ok(version) = self.determine_latest_version(&spec.versionless()) {
|
if let Ok(version) = self.determine_latest_version(&spec.versionless()) {
|
||||||
return Err(PackageError::VersionNotFound(spec.clone(), version));
|
return Err(PackageError::VersionNotFound(
|
||||||
|
spec.clone(),
|
||||||
|
Some(version),
|
||||||
|
eco_format!("{namespace_url}"),
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
return Err(PackageError::NotFound(spec.clone()));
|
return Err(PackageError::NotFound(
|
||||||
|
spec.clone(),
|
||||||
|
eco_format!(
|
||||||
|
"{namespace_url} does not have package '{}'",
|
||||||
|
spec.name
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -235,8 +273,8 @@ impl PackageStorage {
|
|||||||
// broken packages still occur even with the rename safeguard, we might
|
// broken packages still occur even with the rename safeguard, we might
|
||||||
// consider more complex solutions like file locking or checksums.
|
// consider more complex solutions like file locking or checksums.
|
||||||
match fs::rename(&tempdir, &package_dir) {
|
match fs::rename(&tempdir, &package_dir) {
|
||||||
Ok(()) => Ok(()),
|
Ok(()) => Ok(package_dir),
|
||||||
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()),
|
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(package_dir),
|
||||||
Err(err) => Err(error("failed to move downloaded package directory", err)),
|
Err(err) => Err(error("failed to move downloaded package directory", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -440,13 +440,13 @@ pub enum FileError {
|
|||||||
/// A file was not found at this path.
|
/// A file was not found at this path.
|
||||||
NotFound(PathBuf),
|
NotFound(PathBuf),
|
||||||
/// A file could not be accessed.
|
/// A file could not be accessed.
|
||||||
AccessDenied,
|
AccessDenied(PathBuf),
|
||||||
/// A directory was found, but a file was expected.
|
/// A directory was found, but a file was expected.
|
||||||
IsDirectory,
|
IsDirectory(PathBuf),
|
||||||
/// The file is not a Typst source file, but should have been.
|
/// The file is not a Typst source file, but should have been.
|
||||||
NotSource,
|
NotSource(PathBuf),
|
||||||
/// The file was not valid UTF-8, but should have been.
|
/// The file was not valid UTF-8, but should have been.
|
||||||
InvalidUtf8,
|
InvalidUtf8(Option<PathBuf>),
|
||||||
/// The package the file is part of could not be loaded.
|
/// The package the file is part of could not be loaded.
|
||||||
Package(PackageError),
|
Package(PackageError),
|
||||||
/// Another error.
|
/// Another error.
|
||||||
@ -460,11 +460,11 @@ impl FileError {
|
|||||||
pub fn from_io(err: io::Error, path: &Path) -> Self {
|
pub fn from_io(err: io::Error, path: &Path) -> Self {
|
||||||
match err.kind() {
|
match err.kind() {
|
||||||
io::ErrorKind::NotFound => Self::NotFound(path.into()),
|
io::ErrorKind::NotFound => Self::NotFound(path.into()),
|
||||||
io::ErrorKind::PermissionDenied => Self::AccessDenied,
|
io::ErrorKind::PermissionDenied => Self::AccessDenied(path.into()),
|
||||||
io::ErrorKind::InvalidData
|
io::ErrorKind::InvalidData
|
||||||
if err.to_string().contains("stream did not contain valid UTF-8") =>
|
if err.to_string().contains("stream did not contain valid UTF-8") =>
|
||||||
{
|
{
|
||||||
Self::InvalidUtf8
|
Self::InvalidUtf8(Some(path.into()))
|
||||||
}
|
}
|
||||||
_ => Self::Other(Some(eco_format!("{err}"))),
|
_ => Self::Other(Some(eco_format!("{err}"))),
|
||||||
}
|
}
|
||||||
@ -479,10 +479,19 @@ impl Display for FileError {
|
|||||||
Self::NotFound(path) => {
|
Self::NotFound(path) => {
|
||||||
write!(f, "file not found (searched at {})", path.display())
|
write!(f, "file not found (searched at {})", path.display())
|
||||||
}
|
}
|
||||||
Self::AccessDenied => f.pad("failed to load file (access denied)"),
|
Self::AccessDenied(path) => {
|
||||||
Self::IsDirectory => f.pad("failed to load file (is a directory)"),
|
write!(f, "failed to load file {} (access denied)", path.display())
|
||||||
Self::NotSource => f.pad("not a typst source file"),
|
}
|
||||||
Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
|
Self::IsDirectory(path) => {
|
||||||
|
write!(f, "failed to load file {} (is a directory)", path.display())
|
||||||
|
}
|
||||||
|
Self::NotSource(path) => {
|
||||||
|
write!(f, "{} is not a typst source file", path.display())
|
||||||
|
}
|
||||||
|
Self::InvalidUtf8(Some(path)) => {
|
||||||
|
write!(f, "file {} is not valid utf-8", path.display())
|
||||||
|
}
|
||||||
|
Self::InvalidUtf8(None) => f.pad("file is not valid utf-8"),
|
||||||
Self::Package(error) => error.fmt(f),
|
Self::Package(error) => error.fmt(f),
|
||||||
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
|
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
|
||||||
Self::Other(None) => f.pad("failed to load file"),
|
Self::Other(None) => f.pad("failed to load file"),
|
||||||
@ -492,13 +501,13 @@ impl Display for FileError {
|
|||||||
|
|
||||||
impl From<Utf8Error> for FileError {
|
impl From<Utf8Error> for FileError {
|
||||||
fn from(_: Utf8Error) -> Self {
|
fn from(_: Utf8Error) -> Self {
|
||||||
Self::InvalidUtf8
|
Self::InvalidUtf8(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FromUtf8Error> for FileError {
|
impl From<FromUtf8Error> for FileError {
|
||||||
fn from(_: FromUtf8Error) -> Self {
|
fn from(_: FromUtf8Error) -> Self {
|
||||||
Self::InvalidUtf8
|
Self::InvalidUtf8(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,13 +528,15 @@ pub type PackageResult<T> = Result<T, PackageError>;
|
|||||||
|
|
||||||
/// An error that occurred while trying to load a package.
|
/// An error that occurred while trying to load a package.
|
||||||
///
|
///
|
||||||
/// Some variants have an optional string can give more details, if available.
|
/// Some variants have an optional string that can give more details, if available.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub enum PackageError {
|
pub enum PackageError {
|
||||||
/// The specified package does not exist.
|
/// The specified package does not exist.
|
||||||
NotFound(PackageSpec),
|
/// Additionally provides information on where we tried to find the package.
|
||||||
|
NotFound(PackageSpec, EcoString),
|
||||||
/// The specified package found, but the version does not exist.
|
/// The specified package found, but the version does not exist.
|
||||||
VersionNotFound(PackageSpec, PackageVersion),
|
/// TODO: make the registry part of the error better typed
|
||||||
|
VersionNotFound(PackageSpec, Option<PackageVersion>, EcoString),
|
||||||
/// Failed to retrieve the package through the network.
|
/// Failed to retrieve the package through the network.
|
||||||
NetworkFailed(Option<EcoString>),
|
NetworkFailed(Option<EcoString>),
|
||||||
/// The package archive was malformed.
|
/// The package archive was malformed.
|
||||||
@ -539,15 +550,20 @@ impl std::error::Error for PackageError {}
|
|||||||
impl Display for PackageError {
|
impl Display for PackageError {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::NotFound(spec) => {
|
Self::NotFound(spec, detail) => {
|
||||||
write!(f, "package not found (searched for {spec})",)
|
write!(f, "package not found: {detail} (searching for {spec})",)
|
||||||
}
|
}
|
||||||
Self::VersionNotFound(spec, latest) => {
|
Self::VersionNotFound(spec, latest, registry) => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"package found, but version {} does not exist (latest is {})",
|
"package '{}' found, but version {} does not exist",
|
||||||
spec.version, latest,
|
spec.name, spec.version
|
||||||
)
|
)?;
|
||||||
|
if let Some(version) = latest {
|
||||||
|
write!(f, " (latest version provided by {registry} is {version})")
|
||||||
|
} else {
|
||||||
|
write!(f, " ({registry} contains no versions for this package)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Self::NetworkFailed(Some(err)) => {
|
Self::NetworkFailed(Some(err)) => {
|
||||||
write!(f, "failed to download package ({err})")
|
write!(f, "failed to download package ({err})")
|
||||||
|
@ -797,7 +797,9 @@ impl Color {
|
|||||||
components
|
components
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the constructor function for this color's space:
|
/// Returns the constructor function for this color's space.
|
||||||
|
///
|
||||||
|
/// Returns one of:
|
||||||
/// - [`luma`]($color.luma)
|
/// - [`luma`]($color.luma)
|
||||||
/// - [`oklab`]($color.oklab)
|
/// - [`oklab`]($color.oklab)
|
||||||
/// - [`oklch`]($color.oklch)
|
/// - [`oklch`]($color.oklch)
|
||||||
|
@ -191,7 +191,7 @@ fn hint_invalid_main_file(
|
|||||||
file_error: FileError,
|
file_error: FileError,
|
||||||
input: FileId,
|
input: FileId,
|
||||||
) -> EcoVec<SourceDiagnostic> {
|
) -> EcoVec<SourceDiagnostic> {
|
||||||
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
|
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_));
|
||||||
let mut diagnostic =
|
let mut diagnostic =
|
||||||
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
|
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
|
|||||||
items.push(CategoryItem {
|
items.push(CategoryItem {
|
||||||
name: group.name.clone(),
|
name: group.name.clone(),
|
||||||
route: subpage.route.clone(),
|
route: subpage.route.clone(),
|
||||||
oneliner: oneliner(docs).into(),
|
oneliner: oneliner(docs),
|
||||||
code: true,
|
code: true,
|
||||||
});
|
});
|
||||||
children.push(subpage);
|
children.push(subpage);
|
||||||
@ -296,7 +296,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
|
|||||||
items.push(CategoryItem {
|
items.push(CategoryItem {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
route: subpage.route.clone(),
|
route: subpage.route.clone(),
|
||||||
oneliner: oneliner(func.docs().unwrap_or_default()).into(),
|
oneliner: oneliner(func.docs().unwrap_or_default()),
|
||||||
code: true,
|
code: true,
|
||||||
});
|
});
|
||||||
children.push(subpage);
|
children.push(subpage);
|
||||||
@ -306,7 +306,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
|
|||||||
items.push(CategoryItem {
|
items.push(CategoryItem {
|
||||||
name: ty.short_name().into(),
|
name: ty.short_name().into(),
|
||||||
route: subpage.route.clone(),
|
route: subpage.route.clone(),
|
||||||
oneliner: oneliner(ty.docs()).into(),
|
oneliner: oneliner(ty.docs()),
|
||||||
code: true,
|
code: true,
|
||||||
});
|
});
|
||||||
children.push(subpage);
|
children.push(subpage);
|
||||||
@ -637,7 +637,7 @@ fn group_page(
|
|||||||
let item = CategoryItem {
|
let item = CategoryItem {
|
||||||
name: group.name.clone(),
|
name: group.name.clone(),
|
||||||
route: model.route.clone(),
|
route: model.route.clone(),
|
||||||
oneliner: oneliner(&group.details).into(),
|
oneliner: oneliner(&group.details),
|
||||||
code: false,
|
code: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -772,8 +772,24 @@ pub fn urlify(title: &str) -> EcoString {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the first line of documentation.
|
/// Extract the first line of documentation.
|
||||||
fn oneliner(docs: &str) -> &str {
|
fn oneliner(docs: &str) -> EcoString {
|
||||||
docs.lines().next().unwrap_or_default()
|
let paragraph = docs.split("\n\n").next().unwrap_or_default();
|
||||||
|
let mut depth = 0;
|
||||||
|
let mut period = false;
|
||||||
|
let mut end = paragraph.len();
|
||||||
|
for (i, c) in paragraph.char_indices() {
|
||||||
|
match c {
|
||||||
|
'(' | '[' | '{' => depth += 1,
|
||||||
|
')' | ']' | '}' => depth -= 1,
|
||||||
|
'.' if depth == 0 => period = true,
|
||||||
|
c if period && c.is_whitespace() && !docs[..i].ends_with("e.g.") => {
|
||||||
|
end = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => period = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EcoString::from(&docs[..end]).replace("\r\n", " ").replace("\n", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The order of types in the documentation.
|
/// The order of types in the documentation.
|
||||||
|
@ -86,7 +86,7 @@ pub struct FuncModel {
|
|||||||
pub name: EcoString,
|
pub name: EcoString,
|
||||||
pub title: &'static str,
|
pub title: &'static str,
|
||||||
pub keywords: &'static [&'static str],
|
pub keywords: &'static [&'static str],
|
||||||
pub oneliner: &'static str,
|
pub oneliner: EcoString,
|
||||||
pub element: bool,
|
pub element: bool,
|
||||||
pub contextual: bool,
|
pub contextual: bool,
|
||||||
pub deprecation: Option<&'static str>,
|
pub deprecation: Option<&'static str>,
|
||||||
@ -139,7 +139,7 @@ pub struct TypeModel {
|
|||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub title: &'static str,
|
pub title: &'static str,
|
||||||
pub keywords: &'static [&'static str],
|
pub keywords: &'static [&'static str],
|
||||||
pub oneliner: &'static str,
|
pub oneliner: EcoString,
|
||||||
pub details: Html,
|
pub details: Html,
|
||||||
pub constructor: Option<FuncModel>,
|
pub constructor: Option<FuncModel>,
|
||||||
pub scope: Vec<FuncModel>,
|
pub scope: Vec<FuncModel>,
|
||||||
|
@ -127,6 +127,10 @@
|
|||||||
checks = self'.checks;
|
checks = self'.checks;
|
||||||
inputsFrom = [ typst ];
|
inputsFrom = [ typst ];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
rust-analyzer
|
||||||
|
];
|
||||||
|
|
||||||
packages = [
|
packages = [
|
||||||
# A script for quickly running tests.
|
# A script for quickly running tests.
|
||||||
# See https://github.com/typst/typst/blob/main/tests/README.md#making-an-alias
|
# See https://github.com/typst/typst/blob/main/tests/README.md#making-an-alias
|
||||||
|
10
tests/ref/html/smallcaps-all.html
Normal file
10
tests/ref/html/smallcaps-all.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p><span style="font-variant-caps: small-caps">Test 012</span><br><span style="font-variant-caps: all-small-caps">Test 012</span></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -172,7 +172,9 @@ pub(crate) fn system_path(id: FileId) -> FileResult<PathBuf> {
|
|||||||
None => PathBuf::new(),
|
None => PathBuf::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
|
id.vpath()
|
||||||
|
.resolve(&root)
|
||||||
|
.ok_or_else(|| FileError::AccessDenied(id.vpath().as_rootless_path().into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read a file.
|
/// Read a file.
|
||||||
@ -186,7 +188,7 @@ pub(crate) fn read(path: &Path) -> FileResult<Cow<'static, [u8]>> {
|
|||||||
|
|
||||||
let f = |e| FileError::from_io(e, path);
|
let f = |e| FileError::from_io(e, path);
|
||||||
if fs::metadata(path).map_err(f)?.is_dir() {
|
if fs::metadata(path).map_err(f)?.is_dir() {
|
||||||
Err(FileError::IsDirectory)
|
Err(FileError::IsDirectory(path.into()))
|
||||||
} else {
|
} else {
|
||||||
fs::read(path).map(Cow::Owned).map_err(f)
|
fs::read(path).map(Cow::Owned).map_err(f)
|
||||||
}
|
}
|
||||||
|
@ -302,11 +302,11 @@
|
|||||||
#import 5 as x
|
#import 5 as x
|
||||||
|
|
||||||
--- import-from-string-invalid ---
|
--- import-from-string-invalid ---
|
||||||
// Error: 9-11 failed to load file (is a directory)
|
// Error: 9-11 failed to load file tests/suite/scripting (is a directory)
|
||||||
#import "": name
|
#import "": name
|
||||||
|
|
||||||
--- import-from-string-renamed-invalid ---
|
--- import-from-string-renamed-invalid ---
|
||||||
// Error: 9-11 failed to load file (is a directory)
|
// Error: 9-11 failed to load file tests/suite/scripting (is a directory)
|
||||||
#import "" as x
|
#import "" as x
|
||||||
|
|
||||||
--- import-file-not-found-invalid ---
|
--- import-file-not-found-invalid ---
|
||||||
@ -483,3 +483,4 @@ This is never reached.
|
|||||||
--- import-from-file-package-lookalike ---
|
--- import-from-file-package-lookalike ---
|
||||||
// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
|
// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
|
||||||
#import "#test/mypkg:1.0.0": *
|
#import "#test/mypkg:1.0.0": *
|
||||||
|
|
||||||
|
@ -11,6 +11,6 @@
|
|||||||
#show smallcaps: set text(fill: red)
|
#show smallcaps: set text(fill: red)
|
||||||
#smallcaps[Smallcaps]
|
#smallcaps[Smallcaps]
|
||||||
|
|
||||||
--- smallcaps-all ---
|
--- smallcaps-all render html ---
|
||||||
#smallcaps(all: false)[Test 012] \
|
#smallcaps(all: false)[Test 012] \
|
||||||
#smallcaps(all: true)[Test 012]
|
#smallcaps(all: true)[Test 012]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user