Compare commits

...

15 Commits

Author SHA1 Message Date
Neven Villani
9c7d548c92
Merge 143732d2a82c057f9efe48895fc798e422894fec into ac77fdbb6ee9c4a33813a75e056cb5953d14b1db 2025-07-09 22:06:58 +02:00
Laurenz
ac77fdbb6e
Fix tooltip for figure reference (#6580) 2025-07-09 13:50:54 +00:00
Laurenz
3aa7e861e7
Support images in HTML export (#6578) 2025-07-09 13:48:43 +00:00
Laurenz
a45c3388a6
More consistent Packed<T> to Content conversion methods (#6579) 2025-07-09 13:40:22 +00:00
Max
f9b01f595d
Move math styling to codex and add math.scr (#6309) 2025-07-09 13:08:49 +00:00
Robin
eed3407051
Update Swedish translations based on defaults used for LaTeX and cleveref (#6519) 2025-07-09 12:44:42 +00:00
Jassiel Ovando
1bbb58c43f
Add completions subcommand (#6568) 2025-07-09 12:41:40 +00:00
Neven Villani
143732d2a8 One comment out of date 2025-06-24 14:42:23 +02:00
Neven Villani
efe81a1aae
Merge branch 'main' into main 2025-06-12 16:06:33 +02:00
Neven Villani
3b5ee3c488 Update in tests/ too the PathBuf in FileError 2025-06-12 15:54:09 +02:00
Neven Villani
f3d5cdc6a5 Report path on a FileError 2025-06-12 15:47:47 +02:00
Neven Villani
85e03abe45 Improve messages
- restore ability to override a package locally
- cut down on some repeated logic
- more detailed errors
2025-06-12 13:36:13 +02:00
Neven Villani
c354369b05 What's with the namespacing on the unit tests ? 2025-06-11 22:43:14 +02:00
Neven Villani
91ec946283 first attempt 2025-06-11 22:32:10 +02:00
Neven Villani
a5ef75a9c2 I want to get all of this working 2025-06-11 22:31:56 +02:00
37 changed files with 500 additions and 423 deletions

5
Cargo.lock generated
View File

@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -2861,7 +2861,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a" source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -3032,6 +3032,7 @@ version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bumpalo", "bumpalo",
"codex",
"comemo", "comemo",
"ecow", "ecow",
"hypher", "hypher",

View File

@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"

View File

@ -29,6 +29,7 @@ typst-svg = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
clap_complete = { workspace = true }
codespan-reporting = { workspace = true } codespan-reporting = { workspace = true }
color-print = { workspace = true } color-print = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }

View File

@ -7,6 +7,7 @@ use std::str::FromStr;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use clap::builder::{TypedValueParser, ValueParser}; use clap::builder::{TypedValueParser, ValueParser};
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::Shell;
use semver::Version; use semver::Version;
/// The character typically used to separate path components /// The character typically used to separate path components
@ -81,6 +82,9 @@ pub enum Command {
/// Self update the Typst CLI. /// Self update the Typst CLI.
#[cfg_attr(not(feature = "self-update"), clap(hide = true))] #[cfg_attr(not(feature = "self-update"), clap(hide = true))]
Update(UpdateCommand), Update(UpdateCommand),
/// Generates shell completion scripts.
Completions(CompletionsCommand),
} }
/// Compiles an input file into a supported output format. /// Compiles an input file into a supported output format.
@ -198,6 +202,14 @@ pub struct UpdateCommand {
pub backup_path: Option<PathBuf>, pub backup_path: Option<PathBuf>,
} }
/// Generates shell completion scripts.
#[derive(Debug, Clone, Parser)]
pub struct CompletionsCommand {
/// The shell to generate completions for.
#[arg(value_enum)]
pub shell: Shell,
}
/// Arguments for compilation and watching. /// Arguments for compilation and watching.
#[derive(Debug, Clone, Args)] #[derive(Debug, Clone, Args)]
pub struct CompileArgs { pub struct CompileArgs {

View File

@ -0,0 +1,13 @@
use std::io::stdout;
use clap::CommandFactory;
use clap_complete::generate;
use crate::args::{CliArguments, CompletionsCommand};
/// Execute the completions command.
pub fn completions(command: &CompletionsCommand) {
let mut cmd = CliArguments::command();
let bin_name = cmd.get_name().to_string();
generate(command.shell, &mut cmd, bin_name, &mut stdout());
}

View File

@ -1,5 +1,6 @@
mod args; mod args;
mod compile; mod compile;
mod completions;
mod download; mod download;
mod fonts; mod fonts;
mod greet; mod greet;
@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> {
Command::Query(command) => crate::query::query(command)?, Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command), Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command)?, Command::Update(command) => crate::update::update(command)?,
Command::Completions(command) => crate::completions::completions(command),
} }
Ok(()) Ok(())

View File

@ -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)
} }

View File

@ -1,11 +1,72 @@
//! Conversion from Typst data types into CSS data types. //! Conversion from Typst data types into CSS data types.
use std::fmt::{self, Display}; use std::fmt::{self, Display, Write};
use typst_library::layout::Length; use ecow::EcoString;
use typst_library::html::{attr, HtmlElem};
use typst_library::layout::{Length, Rel};
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
use typst_utils::Numeric; use typst_utils::Numeric;
/// Additional methods for [`HtmlElem`].
pub trait HtmlElemExt {
/// Adds the styles to an element if the property list is non-empty.
fn with_styles(self, properties: Properties) -> Self;
}
impl HtmlElemExt for HtmlElem {
/// Adds CSS styles to an element.
fn with_styles(self, properties: Properties) -> Self {
if let Some(value) = properties.into_inline_styles() {
self.with_attr(attr::style, value)
} else {
self
}
}
}
/// A list of CSS properties with values.
#[derive(Debug, Default)]
pub struct Properties(EcoString);
impl Properties {
/// Creates an empty list.
pub fn new() -> Self {
Self::default()
}
/// Adds a new property to the list.
pub fn push(&mut self, property: &str, value: impl Display) {
if !self.0.is_empty() {
self.0.push_str("; ");
}
write!(&mut self.0, "{property}: {value}").unwrap();
}
/// Adds a new property in builder-style.
#[expect(unused)]
pub fn with(mut self, property: &str, value: impl Display) -> Self {
self.push(property, value);
self
}
/// Turns this into a string suitable for use as an inline `style`
/// attribute.
pub fn into_inline_styles(self) -> Option<EcoString> {
(!self.0.is_empty()).then_some(self.0)
}
}
pub fn rel(rel: Rel) -> impl Display {
typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) {
(false, false) => {
write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs))
}
(true, false) => write!(f, "{}%", rel.rel.get()),
(_, true) => write!(f, "{}", length(rel.abs)),
})
}
pub fn length(length: Length) -> impl Display { pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => { (false, false) => {

View File

@ -3,12 +3,12 @@ use std::num::NonZeroUsize;
use ecow::{eco_format, EcoVec}; use ecow::{eco_format, EcoVec};
use typst_library::diag::warning; use typst_library::diag::warning;
use typst_library::foundations::{ use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target, Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
}; };
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::introspection::{Counter, Locator}; use typst_library::introspection::{Counter, Locator};
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use typst_library::layout::OuterVAlignment; use typst_library::layout::{OuterVAlignment, Sizing};
use typst_library::model::{ use typst_library::model::{
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem, FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
@ -18,6 +18,9 @@ use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
SubElem, SuperElem, UnderlineElem, SubElem, SuperElem, UnderlineElem,
}; };
use typst_library::visualize::ImageElem;
use crate::css::{self, HtmlElemExt};
/// Register show rules for the [HTML target](Target::Html). /// Register show rules for the [HTML target](Target::Html).
pub fn register(rules: &mut NativeRuleMap) { pub fn register(rules: &mut NativeRuleMap) {
@ -47,6 +50,9 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register(Html, HIGHLIGHT_RULE); rules.register(Html, HIGHLIGHT_RULE);
rules.register(Html, RAW_RULE); rules.register(Html, RAW_RULE);
rules.register(Html, RAW_LINE_RULE); rules.register(Html, RAW_LINE_RULE);
// Visualize.
rules.register(Html, IMAGE_RULE);
} }
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| { const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
@ -338,7 +344,7 @@ fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content {
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone(); let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell }; let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::default(); let mut attrs = HtmlAttrs::new();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan.get(styles)) { if let Some(colspan) = span(cell.colspan.get(styles)) {
attrs.push(attr::colspan, colspan); attrs.push(attr::colspan, colspan);
@ -409,3 +415,36 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
}; };
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone()); const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {
let image = elem.decode(engine, styles)?;
let mut attrs = HtmlAttrs::new();
attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image));
if let Some(alt) = elem.alt.get_cloned(styles) {
attrs.push(attr::alt, alt);
}
let mut inline = css::Properties::new();
// TODO: Exclude in semantic profile.
if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) {
inline.push("image-rendering", value);
}
// TODO: Exclude in semantic profile?
match elem.width.get(styles) {
Smart::Auto => {}
Smart::Custom(rel) => inline.push("width", css::rel(rel)),
}
// TODO: Exclude in semantic profile?
match elem.height.get(styles) {
Sizing::Auto => {}
Sizing::Rel(rel) => inline.push("height", css::rel(rel)),
Sizing::Fr(_) => {}
}
Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack())
};

View File

@ -2,7 +2,7 @@ use comemo::Track;
use ecow::{eco_vec, EcoString, EcoVec}; use ecow::{eco_vec, EcoString, EcoVec};
use typst::foundations::{Label, Styles, Value}; use typst::foundations::{Label, Styles, Value};
use typst::layout::PagedDocument; use typst::layout::PagedDocument;
use typst::model::BibliographyElem; use typst::model::{BibliographyElem, FigureElem};
use typst::syntax::{ast, LinkedNode, SyntaxKind}; use typst::syntax::{ast, LinkedNode, SyntaxKind};
use crate::IdeWorld; use crate::IdeWorld;
@ -75,8 +75,13 @@ pub fn analyze_labels(
for elem in document.introspector.all() { for elem in document.introspector.all() {
let Some(label) = elem.label() else { continue }; let Some(label) = elem.label() else { continue };
let details = elem let details = elem
.get_by_name("caption") .to_packed::<FigureElem>()
.or_else(|_| elem.get_by_name("body")) .and_then(|figure| match figure.caption.as_option() {
Some(Some(caption)) => Some(caption.pack_ref()),
_ => None,
})
.unwrap_or(elem)
.get_by_name("body")
.ok() .ok()
.and_then(|field| match field { .and_then(|field| match field {
Value::Content(content) => Some(content), Value::Content(content) => Some(content),

View File

@ -378,4 +378,9 @@ mod tests {
.with_source("other.typ", "#let f = (x) => 1"); .with_source("other.typ", "#let f = (x) => 1");
test(&world, -4, Side::After).must_be_code("(..) => .."); test(&world, -4, Side::After).must_be_code("(..) => ..");
} }
#[test]
fn test_tooltip_reference() {
test("#figure(caption: [Hi])[]<f> @f", -1, Side::Before).must_be_text("Hi");
}
} }

View File

@ -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)),
} }
} }

View File

@ -21,6 +21,7 @@ typst-timing = { workspace = true }
typst-utils = { workspace = true } typst-utils = { workspace = true }
az = { workspace = true } az = { workspace = true }
bumpalo = { workspace = true } bumpalo = { workspace = true }
codex = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
hypher = { workspace = true } hypher = { workspace = true }

View File

@ -1,18 +1,11 @@
use std::ffi::OsStr; use typst_library::diag::SourceResult;
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
}; };
use typst_library::loading::DataSource; use typst_library::visualize::{Curve, Image, ImageElem, ImageFit};
use typst_library::text::families;
use typst_library::visualize::{
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
RasterImage, SvgImage, VectorFormat,
};
/// Layout the image. /// Layout the image.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -23,53 +16,7 @@ pub fn layout_image(
styles: StyleChain, styles: StyleChain,
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let span = elem.span(); let image = elem.decode(engine, styles)?;
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let Derived { source, derived: loaded } = &elem.source;
let format = match elem.format.get(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
};
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
if has_foreign_object {
engine.sink.warn(warning!(
span,
"image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information"
));
}
}
// Construct the image itself.
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
loaded.data.clone(),
format,
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.within(loaded)?,
),
};
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
// Determine the image's pixel aspect ratio. // Determine the image's pixel aspect ratio.
let pxw = image.width(); let pxw = image.width();
@ -122,7 +69,7 @@ pub fn layout_image(
// the frame to the target size, center aligning the image in the // the frame to the target size, center aligning the image in the
// process. // process.
let mut frame = Frame::soft(fitted); let mut frame = Frame::soft(fitted);
frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span()));
frame.resize(target, Axes::splat(FixedAlignment::Center)); frame.resize(target, Axes::splat(FixedAlignment::Center));
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
@ -132,25 +79,3 @@ pub fn layout_image(
Ok(frame) Ok(frame)
} }
/// Try to determine the image format based on the data.
fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
if let DataSource::Path(path) = source {
let ext = std::path::Path::new(path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
match ext.as_str() {
"png" => return Ok(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
"webp" => return Ok(ExchangeFormat::Webp.into()),
_ => {}
}
}
Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
}

View File

@ -1,10 +1,11 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use codex::styling::{to_style, MathStyle};
use ecow::EcoString; use ecow::EcoString;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size}; use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{ use typst_library::text::{
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric, BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
}; };
@ -64,12 +65,21 @@ fn layout_inline_text(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<FrameFragment> { ) -> SourceResult<FrameFragment> {
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
// Disable auto-italic.
let italic = styles.get(EquationElem::italic).or(Some(false));
if text.chars().all(|c| c.is_ascii_digit() || c == '.') { if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Small optimization for numbers. Note that this lays out slightly // Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future. // differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![]; let mut fragments = vec![];
for unstyled_c in text.chars() { for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false); // This is fine as ascii digits and '.' can never end up as more
// than a single char after styling.
let style = MathStyle::select(unstyled_c, variant, bold, italic);
let c = to_style(unstyled_c, style).next().unwrap();
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
fragments.push(glyph.into()); fragments.push(glyph.into());
} }
@ -83,8 +93,10 @@ fn layout_inline_text(
.map(|p| p.wrap()); .map(|p| p.wrap());
let styles = styles.chain(&local); let styles = styles.chain(&local);
let styled_text: EcoString = let styled_text: EcoString = text
text.chars().map(|c| styled_char(styles, c, false)).collect(); .chars()
.flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic)))
.collect();
let spaced = styled_text.graphemes(true).nth(1).is_some(); let spaced = styled_text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(styled_text).spanned(span); let elem = TextElem::packed(styled_text).spanned(span);
@ -124,9 +136,16 @@ pub fn layout_symbol(
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles), _ => (elem.text, styles),
}; };
let c = styled_char(styles, unstyled_c, true);
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic);
let style = MathStyle::select(unstyled_c, variant, bold, italic);
let text: EcoString = to_style(unstyled_c, style).collect();
let fragment: MathFragment = let fragment: MathFragment =
match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) { match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
Ok(mut glyph) => { Ok(mut glyph) => {
adjust_glyph_layout(&mut glyph, ctx, styles); adjust_glyph_layout(&mut glyph, ctx, styles);
glyph.into() glyph.into()
@ -134,8 +153,7 @@ pub fn layout_symbol(
Err(_) => { Err(_) => {
// Not in the math font, fallback to normal inline text layout. // Not in the math font, fallback to normal inline text layout.
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. // TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? layout_inline_text(&text, elem.span(), ctx, styles)?.into()
.into()
} }
}; };
ctx.push(fragment); ctx.push(fragment);
@ -161,226 +179,6 @@ fn adjust_glyph_layout(
} }
} }
/// Style the character by selecting the unicode codepoint for italic, bold,
/// caligraphic, etc.
///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
use MathVariant::*;
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic).unwrap_or(
auto_italic
&& matches!(
c,
'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' |
'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
)
&& matches!(variant, Sans | Serif),
);
if let Some(c) = basic_exception(c) {
return c;
}
if let Some(c) = latin_exception(c, variant, bold, italic) {
return c;
}
if let Some(c) = greek_exception(c, variant, bold, italic) {
return c;
}
let base = match c {
'A'..='Z' => 'A',
'a'..='z' => 'a',
'Α'..='Ω' => 'Α',
'α'..='ω' => 'α',
'0'..='9' => '0',
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => '\u{05D0}',
_ => return c,
};
let tuple = (variant, bold, italic);
let start = match c {
// Latin upper.
'A'..='Z' => match tuple {
(Serif, false, false) => 0x0041,
(Serif, true, false) => 0x1D400,
(Serif, false, true) => 0x1D434,
(Serif, true, true) => 0x1D468,
(Sans, false, false) => 0x1D5A0,
(Sans, true, false) => 0x1D5D4,
(Sans, false, true) => 0x1D608,
(Sans, true, true) => 0x1D63C,
(Cal, false, _) => 0x1D49C,
(Cal, true, _) => 0x1D4D0,
(Frak, false, _) => 0x1D504,
(Frak, true, _) => 0x1D56C,
(Mono, _, _) => 0x1D670,
(Bb, _, _) => 0x1D538,
},
// Latin lower.
'a'..='z' => match tuple {
(Serif, false, false) => 0x0061,
(Serif, true, false) => 0x1D41A,
(Serif, false, true) => 0x1D44E,
(Serif, true, true) => 0x1D482,
(Sans, false, false) => 0x1D5BA,
(Sans, true, false) => 0x1D5EE,
(Sans, false, true) => 0x1D622,
(Sans, true, true) => 0x1D656,
(Cal, false, _) => 0x1D4B6,
(Cal, true, _) => 0x1D4EA,
(Frak, false, _) => 0x1D51E,
(Frak, true, _) => 0x1D586,
(Mono, _, _) => 0x1D68A,
(Bb, _, _) => 0x1D552,
},
// Greek upper.
'Α'..='Ω' => match tuple {
(Serif, false, false) => 0x0391,
(Serif, true, false) => 0x1D6A8,
(Serif, false, true) => 0x1D6E2,
(Serif, true, true) => 0x1D71C,
(Sans, _, false) => 0x1D756,
(Sans, _, true) => 0x1D790,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Greek lower.
'α'..='ω' => match tuple {
(Serif, false, false) => 0x03B1,
(Serif, true, false) => 0x1D6C2,
(Serif, false, true) => 0x1D6FC,
(Serif, true, true) => 0x1D736,
(Sans, _, false) => 0x1D770,
(Sans, _, true) => 0x1D7AA,
(Cal | Frak | Mono | Bb, _, _) => return c,
},
// Hebrew Alef -> Dalet.
'\u{05D0}'..='\u{05D3}' => 0x2135,
// Numbers.
'0'..='9' => match tuple {
(Serif, false, _) => 0x0030,
(Serif, true, _) => 0x1D7CE,
(Bb, _, _) => 0x1D7D8,
(Sans, false, _) => 0x1D7E2,
(Sans, true, _) => 0x1D7EC,
(Mono, _, _) => 0x1D7F6,
(Cal | Frak, _, _) => return c,
},
_ => unreachable!(),
};
std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
}
fn basic_exception(c: char) -> Option<char> {
Some(match c {
'〈' => '⟨',
'〉' => '⟩',
'《' => '⟪',
'》' => '⟫',
_ => return None,
})
}
fn latin_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
Some(match (c, variant, bold, italic) {
('B', Cal, false, _) => '',
('E', Cal, false, _) => '',
('F', Cal, false, _) => '',
('H', Cal, false, _) => '',
('I', Cal, false, _) => '',
('L', Cal, false, _) => '',
('M', Cal, false, _) => '',
('R', Cal, false, _) => '',
('C', Frak, false, _) => '',
('H', Frak, false, _) => '',
('I', Frak, false, _) => '',
('R', Frak, false, _) => '',
('Z', Frak, false, _) => '',
('C', Bb, ..) => '',
('H', Bb, ..) => '',
('N', Bb, ..) => '',
('P', Bb, ..) => '',
('Q', Bb, ..) => '',
('R', Bb, ..) => '',
('Z', Bb, ..) => '',
('D', Bb, _, true) => '',
('d', Bb, _, true) => '',
('e', Bb, _, true) => '',
('i', Bb, _, true) => '',
('j', Bb, _, true) => '',
('h', Serif, false, true) => '',
('e', Cal, false, _) => '',
('g', Cal, false, _) => '',
('o', Cal, false, _) => '',
('ħ', Serif, .., true) => 'ℏ',
('ı', Serif, .., true) => '𝚤',
('ȷ', Serif, .., true) => '𝚥',
_ => return None,
})
}
fn greek_exception(
c: char,
variant: MathVariant,
bold: bool,
italic: bool,
) -> Option<char> {
use MathVariant::*;
if c == 'Ϝ' && variant == Serif && bold {
return Some('𝟊');
}
if c == 'ϝ' && variant == Serif && bold {
return Some('𝟋');
}
let list = match c {
'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
'∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
'∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', ''],
'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
'∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
_ => return None,
};
Some(match (variant, bold, italic) {
(Serif, true, false) => list[0],
(Serif, false, true) => list[1],
(Serif, true, true) => list[2],
(Sans, _, false) => list[3],
(Sans, _, true) => list[4],
(Bb, ..) => list[5],
_ => return None,
})
}
/// The non-dotless version of a dotless character that can be used with the /// The non-dotless version of a dotless character that can be used with the
/// `dtls` OpenType feature. /// `dtls` OpenType feature.
pub fn try_dotless(c: char) -> Option<char> { pub fn try_dotless(c: char) -> Option<char> {

View File

@ -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})")

View File

@ -64,6 +64,16 @@ impl<T: NativeElement> Packed<T> {
self.0 self.0
} }
/// Pack back into a reference to content.
pub fn pack_ref(&self) -> &Content {
&self.0
}
/// Pack back into a mutable reference to content.
pub fn pack_mut(&mut self) -> &mut Content {
&mut self.0
}
/// Extract the raw underlying element. /// Extract the raw underlying element.
pub fn unpack(self) -> T { pub fn unpack(self) -> T {
// This function doesn't yet need owned self, but might in the future. // This function doesn't yet need owned self, but might in the future.
@ -94,10 +104,6 @@ impl<T: NativeElement> Packed<T> {
pub fn set_location(&mut self, location: Location) { pub fn set_location(&mut self, location: Location) {
self.0.set_location(location); self.0.set_location(location);
} }
pub fn as_content(&self) -> &Content {
&self.0
}
} }
impl<T: NativeElement> AsRef<T> for Packed<T> { impl<T: NativeElement> AsRef<T> for Packed<T> {

View File

@ -141,7 +141,7 @@ impl RawContent {
/// Clones a packed element into new raw content. /// Clones a packed element into new raw content.
pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self { pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self {
let raw = &elem.as_content().0; let raw = &elem.pack_ref().0;
let header = raw.header(); let header = raw.header();
RawContent::create( RawContent::create(
elem.as_ref().clone(), elem.as_ref().clone(),

View File

@ -165,6 +165,11 @@ cast! {
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
impl HtmlAttrs { impl HtmlAttrs {
/// Creates an empty attribute list.
pub fn new() -> Self {
Self::default()
}
/// Add an attribute. /// Add an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) { pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into())); self.0.push((attr, value.into()));

View File

@ -1,5 +1,6 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use codex::styling::MathVariant;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
@ -12,7 +13,7 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ use crate::layout::{
AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment,
}; };
use crate::math::{MathSize, MathVariant}; use crate::math::MathSize;
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
@ -111,7 +112,7 @@ pub struct EquationElem {
/// The style variant to select. /// The style variant to select.
#[internal] #[internal]
#[ghost] #[ghost]
pub variant: MathVariant, pub variant: Option<MathVariant>,
/// Affects the height of exponents. /// Affects the height of exponents.
#[internal] #[internal]
@ -128,7 +129,7 @@ pub struct EquationElem {
/// Whether to use italic glyphs. /// Whether to use italic glyphs.
#[internal] #[internal]
#[ghost] #[ghost]
pub italic: Smart<bool>, pub italic: Option<bool>,
/// A forced class to use for all fragment. /// A forced class to use for all fragment.
#[internal] #[internal]

View File

@ -80,6 +80,7 @@ pub fn module() -> Module {
math.define_func::<italic>(); math.define_func::<italic>();
math.define_func::<serif>(); math.define_func::<serif>();
math.define_func::<sans>(); math.define_func::<sans>();
math.define_func::<scr>();
math.define_func::<cal>(); math.define_func::<cal>();
math.define_func::<frak>(); math.define_func::<frak>();
math.define_func::<mono>(); math.define_func::<mono>();

View File

@ -1,4 +1,6 @@
use crate::foundations::{func, Cast, Content, Smart}; use codex::styling::MathVariant;
use crate::foundations::{func, Cast, Content};
use crate::math::EquationElem; use crate::math::EquationElem;
/// Bold font style in math. /// Bold font style in math.
@ -24,7 +26,7 @@ pub fn upright(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::italic, Smart::Custom(false)) body.set(EquationElem::italic, Some(false))
} }
/// Italic font style in math. /// Italic font style in math.
@ -35,7 +37,7 @@ pub fn italic(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::italic, Smart::Custom(true)) body.set(EquationElem::italic, Some(true))
} }
/// Serif (roman) font style in math. /// Serif (roman) font style in math.
@ -46,7 +48,7 @@ pub fn serif(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Serif) body.set(EquationElem::variant, Some(MathVariant::Plain))
} }
/// Sans-serif font style in math. /// Sans-serif font style in math.
@ -59,23 +61,39 @@ pub fn sans(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Sans) body.set(EquationElem::variant, Some(MathVariant::SansSerif))
} }
/// Calligraphic font style in math. /// Calligraphic (chancery) font style in math.
/// ///
/// ```example /// ```example
/// Let $cal(P)$ be the set of ... /// Let $cal(P)$ be the set of ...
/// ``` /// ```
/// ///
/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these /// This is the default calligraphic/script style for most math fonts. See
/// styles share the same Unicode codepoints. Switching between the styles is /// [`scr`]($math.scr) for more on how to get the other style (roundhand).
/// thus only possible if supported by the font via #[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])]
/// [font features]($text.features). pub fn cal(
/// The content to style.
body: Content,
) -> Content {
body.set(EquationElem::variant, Some(MathVariant::Chancery))
}
/// Script (roundhand) font style in math.
/// ///
/// For the default math font, the roundhand style is available through the /// ```example
/// `ss01` feature. Therefore, you could define your own version of `\mathscr` /// $ scr(S) $
/// like this: /// ```
///
/// There are two ways that fonts can support differentiating `cal` and `scr`.
/// The first is using Unicode variation sequences. This works out of the box
/// in Typst, however only a few math fonts currently support this.
///
/// The other way is using [font features]($text.features). For example, the
/// roundhand style might be available in a font through the `ss01` feature.
/// To use it in Typst, you could then define your own version of `scr` like
/// this:
/// ///
/// ```example /// ```example
/// #let scr(it) = text( /// #let scr(it) = text(
@ -88,12 +106,12 @@ pub fn sans(
/// ///
/// (The box is not conceptually necessary, but unfortunately currently needed /// (The box is not conceptually necessary, but unfortunately currently needed
/// due to limitations in Typst's text style handling in math.) /// due to limitations in Typst's text style handling in math.)
#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] #[func(title = "Script Style", keywords = ["mathscr", "roundhand"])]
pub fn cal( pub fn scr(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Cal) body.set(EquationElem::variant, Some(MathVariant::Roundhand))
} }
/// Fraktur font style in math. /// Fraktur font style in math.
@ -106,7 +124,7 @@ pub fn frak(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Frak) body.set(EquationElem::variant, Some(MathVariant::Fraktur))
} }
/// Monospace font style in math. /// Monospace font style in math.
@ -119,7 +137,7 @@ pub fn mono(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Mono) body.set(EquationElem::variant, Some(MathVariant::Monospace))
} }
/// Blackboard bold (double-struck) font style in math. /// Blackboard bold (double-struck) font style in math.
@ -137,7 +155,7 @@ pub fn bb(
/// The content to style. /// The content to style.
body: Content, body: Content,
) -> Content { ) -> Content {
body.set(EquationElem::variant, MathVariant::Bb) body.set(EquationElem::variant, Some(MathVariant::DoubleStruck))
} }
/// Forced display style in math. /// Forced display style in math.
@ -240,15 +258,3 @@ pub enum MathSize {
/// Math on its own line. /// Math on its own line.
Display, Display,
} }
/// A mathematical style variant, as defined by Unicode.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)]
pub enum MathVariant {
#[default]
Serif,
Sans,
Cal,
Frak,
Mono,
Bb,
}

View File

@ -8,6 +8,7 @@ pub use self::raster::{
}; };
pub use self::svg::SvgImage; pub use self::svg::SvgImage;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::sync::Arc; use std::sync::Arc;
@ -15,14 +16,16 @@ use ecow::EcoString;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash; use typst_utils::LazyHash;
use crate::diag::StrResult; use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
StyleChain,
}; };
use crate::layout::{Length, Rel, Sizing}; use crate::layout::{Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable; use crate::model::Figurable;
use crate::text::LocalName; use crate::text::{families, LocalName};
/// A raster or vector graphic. /// A raster or vector graphic.
/// ///
@ -217,6 +220,81 @@ impl ImageElem {
} }
} }
impl Packed<ImageElem> {
/// Decodes the image.
pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Image> {
let span = self.span();
let loaded = &self.source.derived;
let format = self.determine_format(styles).at(span)?;
// Warn the user if the image contains a foreign object. Not perfect
// because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object =
memchr::memmem::find(&loaded.data, b"<foreignObject").is_some();
if has_foreign_object {
engine.sink.warn(warning!(
span,
"image contains foreign object";
hint: "SVG images with foreign objects might render incorrectly in typst";
hint: "see https://github.com/typst/typst/issues/1421 for more information"
));
}
}
// Construct the image itself.
let kind = match format {
ImageFormat::Raster(format) => ImageKind::Raster(
RasterImage::new(
loaded.data.clone(),
format,
self.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
loaded.data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.within(loaded)?,
),
};
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
}
/// Tries to determine the image format based on the format that was
/// explicitly defined, or else the extension, or else the data.
fn determine_format(&self, styles: StyleChain) -> StrResult<ImageFormat> {
if let Smart::Custom(v) = self.format.get(styles) {
return Ok(v);
};
let Derived { source, derived: loaded } = &self.source;
if let DataSource::Path(path) = source {
let ext = std::path::Path::new(path.as_str())
.extension()
.and_then(OsStr::to_str)
.unwrap_or_default()
.to_lowercase();
match ext.as_str() {
"png" => return Ok(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
"webp" => return Ok(ExchangeFormat::Webp.into()),
_ => {}
}
}
Ok(ImageFormat::detect(&loaded.data).ok_or("unknown image format")?)
}
}
impl LocalName for Packed<ImageElem> { impl LocalName for Packed<ImageElem> {
const KEY: &'static str = "figure"; const KEY: &'static str = "figure";
} }

View File

@ -1,8 +1,8 @@
figure = Figur figure = Figur
table = Tabell table = Tabell
equation = Ekvation equation = Ekvation
bibliography = Bibliografi bibliography = Referenser
heading = Kapitel heading = Avsnitt
outline = Innehåll outline = Innehåll
raw = Listing raw = Kodlistning
page = sida page = sida

View File

@ -18,21 +18,27 @@ impl SVGRenderer {
self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("width", &size.x.to_pt());
self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("height", &size.y.to_pt());
self.xml.write_attribute("preserveAspectRatio", "none"); self.xml.write_attribute("preserveAspectRatio", "none");
match image.scaling() { if let Some(value) = convert_image_scaling(image.scaling()) {
Smart::Auto => {} self.xml
Smart::Custom(ImageScaling::Smooth) => { .write_attribute("style", &format_args!("image-rendering: {value}"))
// This is still experimental and not implemented in all major browsers.
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
self.xml.write_attribute("style", "image-rendering: smooth")
}
Smart::Custom(ImageScaling::Pixelated) => {
self.xml.write_attribute("style", "image-rendering: pixelated")
}
} }
self.xml.end_element(); self.xml.end_element();
} }
} }
/// Converts an image scaling to a CSS `image-rendering` propery value.
pub fn convert_image_scaling(scaling: Smart<ImageScaling>) -> Option<&'static str> {
match scaling {
Smart::Auto => None,
Smart::Custom(ImageScaling::Smooth) => {
// This is still experimental and not implemented in all major browsers.
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility
Some("smooth")
}
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
}
}
/// Encode an image into a data URL. The format of the URL is /// Encode an image into a data URL. The format of the URL is
/// `data:image/{format};base64,`. /// `data:image/{format};base64,`.
#[comemo::memoize] #[comemo::memoize]

View File

@ -5,6 +5,8 @@ mod paint;
mod shape; mod shape;
mod text; mod text;
pub use image::{convert_image_scaling, convert_image_to_base64_url};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{self, Display, Formatter, Write}; use std::fmt::{self, Display, Formatter, Write};

View File

@ -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));

View File

@ -5,7 +5,7 @@
title: Variants title: Variants
category: math category: math
path: ["math"] path: ["math"]
filter: ["serif", "sans", "frak", "mono", "bb", "cal"] filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"]
details: | details: |
Alternate typefaces within formulas. Alternate typefaces within formulas.

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body><img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAA3ADcAAD/4QBiRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAEAAAITAAMAAAABAAEAAAAAAAAAAADcAAAAAQAAANwAAAAB/8AACwgAUAAwAQERAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAkLicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/aAAgBAQAAPwD5/ooooooooooooor7+ooor4Bor7+ooor4Booor7+or4Booor7+or4Bor7+ooor4Bor7+ooor4Bor7+ooor4Bor7+ooor/2Q==" alt="The letter F"></body>
</html>

View 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>
<div style="display: flex; flex-direction: row; gap: 4pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: smooth; width: 28.346456692913385pt"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAADCAIAAADZSiLoAAAAKUlEQVR4AQEeAOH/AP8AAAD/AAAA/wCAAAAAgAAAAIAAgIAAAICAgACAcFMHfiTGz0oAAAAASUVORK5CYII=" style="image-rendering: pixelated; width: 28.346456692913385pt"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 B

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

View File

@ -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)
} }

View File

@ -12,6 +12,15 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \
bb("hello") + bold(cal("world")), \ bb("hello") + bold(cal("world")), \
mono("SQRT")(x) wreath mono(123 + 456)$ mono("SQRT")(x) wreath mono(123 + 456)$
--- math-style-fallback ---
// Test how math styles fallback.
$upright(frak(bold(alpha))) = upright(bold(alpha)) \
bold(mono(ϝ)) = bold(ϝ) \
sans(Theta) = bold(sans(Theta)) \
bold(upright(planck)) != planck \
bb(e) != italic(bb(e)) \
serif(sans(A)) != serif(A)$
--- math-style-dotless --- --- math-style-dotless ---
// Test styling dotless i and j. // Test styling dotless i and j.
$ dotless.i dotless.j, $ dotless.i dotless.j,
@ -38,7 +47,15 @@ $bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$
--- math-style-hebrew-exceptions --- --- math-style-hebrew-exceptions ---
// Test hebrew exceptions. // Test hebrew exceptions.
$aleph, beth, gimel, daleth$ $aleph, beth, gimel, daleth$ \
$upright(aleph), upright(beth), upright(gimel), upright(daleth)$
--- math-style-script ---
// Test variation selectors for scr and cal.
$cal(A) scr(A) bold(cal(O)) scr(bold(O))$
#show math.equation: set text(font: "Noto Sans Math")
$scr(E) cal(E) bold(scr(Y)) cal(bold(Y))$
--- issue-3650-italic-equation --- --- issue-3650-italic-equation ---
_abc $sin(x) "abc"$_ \ _abc $sin(x) "abc"$_ \

View File

@ -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": *

View File

@ -9,6 +9,9 @@
#set page(height: 60pt) #set page(height: 60pt)
#image("/assets/images/tiger.jpg") #image("/assets/images/tiger.jpg")
--- image-jpg-html-base64 html ---
#image("/assets/images/f2t.jpg", alt: "The letter F")
--- image-sizing --- --- image-sizing ---
// Test configuring the size and fitting behaviour of images. // Test configuring the size and fitting behaviour of images.
@ -128,7 +131,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
width: 1cm, width: 1cm,
) )
--- image-scaling-methods --- --- image-scaling-methods render html ---
#let img(scaling) = image( #let img(scaling) = image(
bytes(( bytes((
0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF,
@ -144,14 +147,26 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
scaling: scaling, scaling: scaling,
) )
#stack( #let images = (
dir: ltr,
spacing: 4pt,
img(auto), img(auto),
img("smooth"), img("smooth"),
img("pixelated"), img("pixelated"),
) )
#context if target() == "html" {
// TODO: Remove this once `stack` is supported in HTML export.
html.div(
style: "display: flex; flex-direction: row; gap: 4pt",
images.join(),
)
} else {
stack(
dir: ltr,
spacing: 4pt,
..images,
)
}
--- image-natural-dpi-sizing --- --- image-natural-dpi-sizing ---
// Test that images aren't upscaled. // Test that images aren't upscaled.
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page // Image is just 48x80 at 220dpi. It should not be scaled to fit the page