Compare commits
6 Commits
3fb1ac587f
...
77f8fcf783
Author | SHA1 | Date | |
---|---|---|---|
|
77f8fcf783 | ||
|
6ab77ccc37 | ||
|
ac77fdbb6e | ||
|
3aa7e861e7 | ||
|
a45c3388a6 | ||
|
f9b01f595d |
7
Cargo.lock
generated
@ -413,7 +413,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "codex"
|
||||
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]]
|
||||
name = "color-print"
|
||||
@ -2861,7 +2861,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typst-assets"
|
||||
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]]
|
||||
name = "typst-cli"
|
||||
@ -2911,7 +2911,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "typst-dev-assets"
|
||||
version = "0.13.1"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
|
||||
source = "git+https://github.com/typst/typst-dev-assets?rev=64f8c71#64f8c7108db88323a9d3476e9750562de753f24e"
|
||||
|
||||
[[package]]
|
||||
name = "typst-docs"
|
||||
@ -3032,6 +3032,7 @@ version = "0.13.1"
|
||||
dependencies = [
|
||||
"az",
|
||||
"bumpalo",
|
||||
"codex",
|
||||
"comemo",
|
||||
"ecow",
|
||||
"hypher",
|
||||
|
@ -32,8 +32,8 @@ typst-svg = { path = "crates/typst-svg", 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-utils = { path = "crates/typst-utils", version = "0.13.1" }
|
||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" }
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
|
||||
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" }
|
||||
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "64f8c71" }
|
||||
arrayvec = "0.7.4"
|
||||
az = "1.2"
|
||||
base64 = "0.22"
|
||||
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.2.1"
|
||||
clap_mangen = "0.2.10"
|
||||
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"
|
||||
comemo = "0.4"
|
||||
csv = "1"
|
||||
|
@ -1,11 +1,72 @@
|
||||
//! 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_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 {
|
||||
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
|
||||
(false, false) => {
|
||||
|
@ -3,12 +3,12 @@ use std::num::NonZeroUsize;
|
||||
use ecow::{eco_format, EcoVec};
|
||||
use typst_library::diag::warning;
|
||||
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::introspection::{Counter, Locator};
|
||||
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::{
|
||||
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
|
||||
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
|
||||
@ -18,6 +18,9 @@ use typst_library::text::{
|
||||
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
|
||||
SubElem, SuperElem, UnderlineElem,
|
||||
};
|
||||
use typst_library::visualize::ImageElem;
|
||||
|
||||
use crate::css::{self, HtmlElemExt};
|
||||
|
||||
/// Register show rules for the [HTML target](Target::Html).
|
||||
pub fn register(rules: &mut NativeRuleMap) {
|
||||
@ -47,6 +50,9 @@ pub fn register(rules: &mut NativeRuleMap) {
|
||||
rules.register(Html, HIGHLIGHT_RULE);
|
||||
rules.register(Html, RAW_RULE);
|
||||
rules.register(Html, RAW_LINE_RULE);
|
||||
|
||||
// Visualize.
|
||||
rules.register(Html, IMAGE_RULE);
|
||||
}
|
||||
|
||||
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 {
|
||||
let cell = cell.body.clone();
|
||||
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());
|
||||
if let Some(colspan) = span(cell.colspan.get(styles)) {
|
||||
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 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())
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ use comemo::Track;
|
||||
use ecow::{eco_vec, EcoString, EcoVec};
|
||||
use typst::foundations::{Label, Styles, Value};
|
||||
use typst::layout::PagedDocument;
|
||||
use typst::model::BibliographyElem;
|
||||
use typst::model::{BibliographyElem, FigureElem};
|
||||
use typst::syntax::{ast, LinkedNode, SyntaxKind};
|
||||
|
||||
use crate::IdeWorld;
|
||||
@ -75,8 +75,13 @@ pub fn analyze_labels(
|
||||
for elem in document.introspector.all() {
|
||||
let Some(label) = elem.label() else { continue };
|
||||
let details = elem
|
||||
.get_by_name("caption")
|
||||
.or_else(|_| elem.get_by_name("body"))
|
||||
.to_packed::<FigureElem>()
|
||||
.and_then(|figure| match figure.caption.as_option() {
|
||||
Some(Some(caption)) => Some(caption.pack_ref()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(elem)
|
||||
.get_by_name("body")
|
||||
.ok()
|
||||
.and_then(|field| match field {
|
||||
Value::Content(content) => Some(content),
|
||||
|
@ -378,4 +378,9 @@ mod tests {
|
||||
.with_source("other.typ", "#let f = (x) => 1");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ typst-timing = { workspace = true }
|
||||
typst-utils = { workspace = true }
|
||||
az = { workspace = true }
|
||||
bumpalo = { workspace = true }
|
||||
codex = { workspace = true }
|
||||
comemo = { workspace = true }
|
||||
ecow = { workspace = true }
|
||||
hypher = { workspace = true }
|
||||
|
@ -1,18 +1,11 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
||||
use typst_library::diag::SourceResult;
|
||||
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::layout::{
|
||||
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
|
||||
};
|
||||
use typst_library::loading::DataSource;
|
||||
use typst_library::text::families;
|
||||
use typst_library::visualize::{
|
||||
Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
|
||||
RasterImage, SvgImage, VectorFormat,
|
||||
};
|
||||
use typst_library::visualize::{Curve, Image, ImageElem, ImageFit};
|
||||
|
||||
/// Layout the image.
|
||||
#[typst_macros::time(span = elem.span())]
|
||||
@ -23,53 +16,7 @@ pub fn layout_image(
|
||||
styles: StyleChain,
|
||||
region: Region,
|
||||
) -> SourceResult<Frame> {
|
||||
let span = elem.span();
|
||||
|
||||
// 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));
|
||||
let image = elem.decode(engine, styles)?;
|
||||
|
||||
// Determine the image's pixel aspect ratio.
|
||||
let pxw = image.width();
|
||||
@ -122,7 +69,7 @@ pub fn layout_image(
|
||||
// the frame to the target size, center aligning the image in the
|
||||
// process.
|
||||
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));
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
@ -132,25 +79,3 @@ pub fn layout_image(
|
||||
|
||||
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")?)
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Em, Frame, Point, Size};
|
||||
use typst_library::math::AccentElem;
|
||||
|
||||
use super::{
|
||||
style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext,
|
||||
MathFragment,
|
||||
style_cramped, style_dtls, style_flac, FrameFragment, MathContext, MathFragment,
|
||||
};
|
||||
|
||||
/// How much the accent can be shorter than the base.
|
||||
@ -27,14 +26,17 @@ pub fn layout_accent(
|
||||
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
|
||||
|
||||
let cramped = style_cramped();
|
||||
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
|
||||
let base_styles = base_styles.chain(&cramped);
|
||||
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
|
||||
|
||||
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
|
||||
|
||||
// Preserve class to preserve automatic spacing.
|
||||
let base_class = base.class();
|
||||
let base_attach = base.accent_attach();
|
||||
|
||||
// Try to replace the accent glyph with its flattened variant.
|
||||
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
|
||||
let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
|
||||
let flac = style_flac();
|
||||
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
|
||||
styles.chain(&flac)
|
||||
@ -42,23 +44,25 @@ pub fn layout_accent(
|
||||
styles
|
||||
};
|
||||
|
||||
let mut glyph =
|
||||
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
|
||||
let mut accent = ctx.layout_into_fragment(
|
||||
&SymbolElem::packed(accent.0).spanned(elem.span()),
|
||||
accent_styles,
|
||||
)?;
|
||||
|
||||
// Forcing the accent to be at least as large as the base makes it too wide
|
||||
// in many cases.
|
||||
let width = elem.size.resolve(styles).relative_to(base.width());
|
||||
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
|
||||
glyph.stretch_horizontal(ctx, width - short_fall);
|
||||
let accent_attach = glyph.accent_attach.0;
|
||||
let accent = glyph.into_frame();
|
||||
let short_fall = ACCENT_SHORT_FALL.at(size);
|
||||
accent.stretch_horizontal(ctx, width - short_fall);
|
||||
let accent_attach = accent.accent_attach().0;
|
||||
let accent = accent.into_frame();
|
||||
|
||||
let (gap, accent_pos, base_pos) = if top_accent {
|
||||
// Descent is negative because the accent's ink bottom is above the
|
||||
// baseline. Therefore, the default gap is the accent's negated descent
|
||||
// minus the accent base height. Only if the base is very small, we
|
||||
// need a larger gap so that the accent doesn't move too low.
|
||||
let accent_base_height = scaled!(ctx, styles, accent_base_height);
|
||||
let accent_base_height = value!(font, accent_base_height).at(size);
|
||||
let gap = -accent.descent() - base.ascent().min(accent_base_height);
|
||||
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
|
||||
let base_pos = Point::with_y(accent.height() + gap);
|
||||
|
@ -4,6 +4,8 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
|
||||
use typst_library::math::{
|
||||
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
|
||||
};
|
||||
use typst_library::text::Font;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::OptionExt;
|
||||
|
||||
use super::{
|
||||
@ -83,7 +85,7 @@ pub fn layout_attach(
|
||||
layout!(br, sub_style_chain)?,
|
||||
];
|
||||
|
||||
layout_attachments(ctx, styles, base, fragments)
|
||||
layout_attachments(ctx, styles, base, elem.base.span(), fragments)
|
||||
}
|
||||
|
||||
/// Lays out a [`PrimeElem`].
|
||||
@ -102,13 +104,19 @@ pub fn layout_primes(
|
||||
4 => '⁗',
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
|
||||
let f = ctx.layout_into_fragment(
|
||||
&SymbolElem::packed(c).spanned(elem.span()),
|
||||
styles,
|
||||
)?;
|
||||
ctx.push(f);
|
||||
}
|
||||
count => {
|
||||
// Custom amount of primes
|
||||
let prime = ctx
|
||||
.layout_into_fragment(&SymbolElem::packed('′'), styles)?
|
||||
.layout_into_fragment(
|
||||
&SymbolElem::packed('′').spanned(elem.span()),
|
||||
styles,
|
||||
)?
|
||||
.into_frame();
|
||||
let width = prime.width() * (count + 1) as f64 / 2.0;
|
||||
let mut frame = Frame::soft(Size::new(width, prime.height()));
|
||||
@ -170,22 +178,25 @@ fn layout_attachments(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
base: MathFragment,
|
||||
span: Span,
|
||||
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
|
||||
) -> SourceResult<()> {
|
||||
let base_class = base.class();
|
||||
let class = base.class();
|
||||
let (font, size) = base.font(ctx, styles, span)?;
|
||||
let cramped = styles.get(EquationElem::cramped);
|
||||
|
||||
// Calculate the distance from the base's baseline to the superscripts' and
|
||||
// subscripts' baseline.
|
||||
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
|
||||
(Abs::zero(), Abs::zero())
|
||||
} else {
|
||||
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
|
||||
compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
|
||||
};
|
||||
|
||||
// Calculate the distance from the base's baseline to the top attachment's
|
||||
// and bottom attachment's baseline.
|
||||
let (t_shift, b_shift) =
|
||||
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
|
||||
compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
|
||||
|
||||
// Calculate the final frame height.
|
||||
let ascent = base
|
||||
@ -215,7 +226,7 @@ fn layout_attachments(
|
||||
// `space_after_script` is extra spacing that is at the start before each
|
||||
// pre-script, and at the end after each post-script (see the MathConstants
|
||||
// table in the OpenType MATH spec).
|
||||
let space_after_script = scaled!(ctx, styles, space_after_script);
|
||||
let space_after_script = value!(font, space_after_script).at(size);
|
||||
|
||||
// Calculate the distance each pre-script extends to the left of the base's
|
||||
// width.
|
||||
@ -272,7 +283,7 @@ fn layout_attachments(
|
||||
layout!(b, b_x, b_y); // lower-limit
|
||||
|
||||
// Done! Note that we retain the class of the base.
|
||||
ctx.push(FrameFragment::new(styles, frame).with_class(base_class));
|
||||
ctx.push(FrameFragment::new(styles, frame).with_class(class));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -364,8 +375,8 @@ fn compute_limit_widths(
|
||||
/// Returns two lengths, the first being the distance to the upper-limit's
|
||||
/// baseline and the second being the distance to the lower-limit's baseline.
|
||||
fn compute_limit_shifts(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
font: &Font,
|
||||
font_size: Abs,
|
||||
base: &MathFragment,
|
||||
[t, b]: [Option<&MathFragment>; 2],
|
||||
) -> (Abs, Abs) {
|
||||
@ -373,16 +384,15 @@ fn compute_limit_shifts(
|
||||
// ascender of the limits respectively, whereas `upper_rise_min` and
|
||||
// `lower_drop_min` give gaps to each limit's baseline (see the
|
||||
// MathConstants table in the OpenType MATH spec).
|
||||
|
||||
let t_shift = t.map_or_default(|t| {
|
||||
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
|
||||
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
|
||||
let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size);
|
||||
let upper_rise_min = value!(font, upper_limit_baseline_rise_min).at(font_size);
|
||||
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
|
||||
});
|
||||
|
||||
let b_shift = b.map_or_default(|b| {
|
||||
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
|
||||
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
|
||||
let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size);
|
||||
let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size);
|
||||
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
|
||||
});
|
||||
|
||||
@ -393,25 +403,27 @@ fn compute_limit_shifts(
|
||||
/// Returns two lengths, the first being the distance to the superscripts'
|
||||
/// baseline and the second being the distance to the subscripts' baseline.
|
||||
fn compute_script_shifts(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
font: &Font,
|
||||
font_size: Abs,
|
||||
cramped: bool,
|
||||
base: &MathFragment,
|
||||
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
|
||||
) -> (Abs, Abs) {
|
||||
let sup_shift_up = if styles.get(EquationElem::cramped) {
|
||||
scaled!(ctx, styles, superscript_shift_up_cramped)
|
||||
let sup_shift_up = (if cramped {
|
||||
value!(font, superscript_shift_up_cramped)
|
||||
} else {
|
||||
scaled!(ctx, styles, superscript_shift_up)
|
||||
};
|
||||
value!(font, superscript_shift_up)
|
||||
})
|
||||
.at(font_size);
|
||||
|
||||
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
|
||||
let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size);
|
||||
let sup_bottom_max_with_sub =
|
||||
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
|
||||
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
|
||||
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
|
||||
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
|
||||
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
|
||||
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
|
||||
value!(font, superscript_bottom_max_with_subscript).at(font_size);
|
||||
let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size);
|
||||
let gap_min = value!(font, sub_superscript_gap_min).at(font_size);
|
||||
let sub_shift_down = value!(font, subscript_shift_down).at(font_size);
|
||||
let sub_top_max = value!(font, subscript_top_max).at(font_size);
|
||||
let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size);
|
||||
|
||||
let mut shift_up = Abs::zero();
|
||||
let mut shift_down = Abs::zero();
|
||||
|
@ -7,7 +7,7 @@ use typst_library::visualize::{FixedStroke, Geometry};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
|
||||
find_math_font, style_for_denominator, style_for_numerator, FrameFragment,
|
||||
MathContext, DELIM_SHORT_FALL,
|
||||
};
|
||||
|
||||
@ -49,29 +49,33 @@ fn layout_frac_like(
|
||||
binom: bool,
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
|
||||
let shift_up = scaled!(
|
||||
ctx, styles,
|
||||
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||
let axis = value!(font, axis_height).resolve(styles);
|
||||
let thickness = value!(font, fraction_rule_thickness).resolve(styles);
|
||||
let shift_up = value!(
|
||||
font, styles,
|
||||
text: fraction_numerator_shift_up,
|
||||
display: fraction_numerator_display_style_shift_up,
|
||||
);
|
||||
let shift_down = scaled!(
|
||||
ctx, styles,
|
||||
)
|
||||
.resolve(styles);
|
||||
let shift_down = value!(
|
||||
font, styles,
|
||||
text: fraction_denominator_shift_down,
|
||||
display: fraction_denominator_display_style_shift_down,
|
||||
);
|
||||
let num_min = scaled!(
|
||||
ctx, styles,
|
||||
)
|
||||
.resolve(styles);
|
||||
let num_min = value!(
|
||||
font, styles,
|
||||
text: fraction_numerator_gap_min,
|
||||
display: fraction_num_display_style_gap_min,
|
||||
);
|
||||
let denom_min = scaled!(
|
||||
ctx, styles,
|
||||
)
|
||||
.resolve(styles);
|
||||
let denom_min = value!(
|
||||
font, styles,
|
||||
text: fraction_denominator_gap_min,
|
||||
display: fraction_denom_display_style_gap_min,
|
||||
);
|
||||
)
|
||||
.resolve(styles);
|
||||
|
||||
let num_style = style_for_numerator(styles);
|
||||
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
|
||||
@ -82,7 +86,7 @@ fn layout_frac_like(
|
||||
// Add a comma between each element.
|
||||
denom
|
||||
.iter()
|
||||
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
|
||||
.flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
|
||||
.skip(1),
|
||||
),
|
||||
styles.chain(&denom_style),
|
||||
@ -109,12 +113,18 @@ fn layout_frac_like(
|
||||
frame.push_frame(denom_pos, denom);
|
||||
|
||||
if binom {
|
||||
let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?;
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
|
||||
let mut left =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(span), styles)?;
|
||||
left.stretch_vertical(ctx, height - short_fall);
|
||||
left.center_on_axis();
|
||||
ctx.push(left);
|
||||
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
|
||||
|
||||
let mut right =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(span), styles)?;
|
||||
right.stretch_vertical(ctx, height - short_fall);
|
||||
right.center_on_axis();
|
||||
ctx.push(right);
|
||||
|
@ -11,12 +11,16 @@ use typst_library::layout::{
|
||||
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
|
||||
};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem};
|
||||
use typst_library::text::{
|
||||
families, features, language, variant, Font, Glyph, TextElem, TextItem,
|
||||
};
|
||||
use typst_library::visualize::Paint;
|
||||
use typst_library::World;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{default_math_class, Get};
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use super::MathContext;
|
||||
use super::{find_math_font, MathContext};
|
||||
use crate::inline::create_shape_plan;
|
||||
use crate::modifiers::{FrameModifiers, FrameModify};
|
||||
|
||||
@ -108,6 +112,21 @@ impl MathFragment {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font(
|
||||
&self,
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
span: Span,
|
||||
) -> SourceResult<(Font, Abs)> {
|
||||
Ok((
|
||||
match self {
|
||||
Self::Glyph(glyph) => glyph.item.font.clone(),
|
||||
_ => find_math_font(ctx.engine.world, styles, span)?,
|
||||
},
|
||||
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn font_size(&self) -> Option<Abs> {
|
||||
match self {
|
||||
Self::Glyph(glyph) => Some(glyph.item.size),
|
||||
@ -192,6 +211,31 @@ impl MathFragment {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill(&self) -> Option<Paint> {
|
||||
match self {
|
||||
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
|
||||
if let Self::Glyph(glyph) = self {
|
||||
glyph.stretch_vertical(ctx, height)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
|
||||
if let Self::Glyph(glyph) = self {
|
||||
glyph.stretch_horizontal(ctx, width)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_on_axis(&mut self) {
|
||||
if let Self::Glyph(glyph) = self {
|
||||
glyph.center_on_axis()
|
||||
}
|
||||
}
|
||||
|
||||
/// If no kern table is provided for a corner, a kerning amount of zero is
|
||||
/// assumed.
|
||||
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
|
||||
@ -261,23 +305,70 @@ pub struct GlyphFragment {
|
||||
impl GlyphFragment {
|
||||
/// Calls `new` with the given character.
|
||||
pub fn new_char(
|
||||
font: &Font,
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
c: char,
|
||||
span: Span,
|
||||
) -> SourceResult<Self> {
|
||||
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||
) -> SourceResult<Option<Self>> {
|
||||
Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
|
||||
}
|
||||
|
||||
/// Selects a font to use and then shapes text.
|
||||
pub fn new(
|
||||
ctx: &MathContext,
|
||||
styles: StyleChain,
|
||||
text: &str,
|
||||
span: Span,
|
||||
) -> SourceResult<Option<Self>> {
|
||||
let families = families(styles);
|
||||
let variant = variant(styles);
|
||||
let fallback = styles.get(TextElem::fallback);
|
||||
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
|
||||
|
||||
// Find the next available family.
|
||||
let world = ctx.engine.world;
|
||||
let book = world.book();
|
||||
let mut selection = None;
|
||||
for family in families {
|
||||
selection = book
|
||||
.select(family.as_str(), variant)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|font| {
|
||||
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
||||
})
|
||||
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
|
||||
if selection.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do font fallback if the families are exhausted and fallback is enabled.
|
||||
if selection.is_none() && fallback {
|
||||
selection = book
|
||||
.select_fallback(None, variant, text)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|font| {
|
||||
font.ttf().tables().math.and_then(|math| math.constants).is_some()
|
||||
});
|
||||
}
|
||||
|
||||
// Error out if no math font could be found at all.
|
||||
let Some(font) = selection else {
|
||||
bail!(span, "current font does not support math");
|
||||
};
|
||||
|
||||
Self::shape(&font, styles, text, span)
|
||||
}
|
||||
|
||||
/// Try to create a new glyph out of the given string. Will bail if the
|
||||
/// result from shaping the string is not a single glyph or is a tofu.
|
||||
/// result from shaping the string is more than a single glyph.
|
||||
#[comemo::memoize]
|
||||
pub fn new(
|
||||
pub fn shape(
|
||||
font: &Font,
|
||||
styles: StyleChain,
|
||||
text: &str,
|
||||
span: Span,
|
||||
) -> SourceResult<GlyphFragment> {
|
||||
) -> SourceResult<Option<GlyphFragment>> {
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str(text);
|
||||
buffer.set_language(language(styles));
|
||||
@ -300,18 +391,15 @@ impl GlyphFragment {
|
||||
);
|
||||
|
||||
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
|
||||
if buffer.len() != 1 {
|
||||
bail!(span, "did not get a single glyph after shaping {}", text);
|
||||
match buffer.len() {
|
||||
0 => return Ok(None),
|
||||
1 => {}
|
||||
_ => bail!(span, "did not get a single glyph after shaping {}", text),
|
||||
}
|
||||
|
||||
let info = buffer.glyph_infos()[0];
|
||||
let pos = buffer.glyph_positions()[0];
|
||||
|
||||
// TODO: add support for coverage and fallback, like in normal text shaping.
|
||||
if info.glyph_id == 0 {
|
||||
bail!(span, "current font is missing a glyph for {}", text);
|
||||
}
|
||||
|
||||
let cluster = info.cluster as usize;
|
||||
let c = text[cluster..].chars().next().unwrap();
|
||||
let limits = Limits::for_char(c);
|
||||
@ -361,7 +449,7 @@ impl GlyphFragment {
|
||||
modifiers: FrameModifiers::get_in(styles),
|
||||
};
|
||||
fragment.update_glyph();
|
||||
Ok(fragment)
|
||||
Ok(Some(fragment))
|
||||
}
|
||||
|
||||
/// Sets element id and boxes in appropriate way without changing other
|
||||
|
@ -33,12 +33,13 @@ pub fn layout_lr(
|
||||
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
|
||||
let inner_fragments = &mut fragments[start_idx..end_idx];
|
||||
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let max_extent = inner_fragments
|
||||
.iter()
|
||||
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut max_extent = Abs::zero();
|
||||
for fragment in inner_fragments.iter() {
|
||||
let (font, size) = fragment.font(ctx, styles, elem.span())?;
|
||||
let axis = value!(font, axis_height).at(size);
|
||||
let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
|
||||
max_extent = max_extent.max(extent);
|
||||
}
|
||||
|
||||
let relative_to = 2.0 * max_extent;
|
||||
let height = elem.size.resolve(styles);
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::{bail, warning, SourceResult};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{
|
||||
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
|
||||
};
|
||||
@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment,
|
||||
LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
||||
alignments, find_math_font, style_for_denominator, AlignmentResult, FrameFragment,
|
||||
GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
|
||||
};
|
||||
|
||||
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
|
||||
@ -186,12 +186,10 @@ fn layout_body(
|
||||
// We pad ascent and descent with the ascent and descent of the paren
|
||||
// to ensure that normal matrices are aligned with others unless they are
|
||||
// way too big.
|
||||
let paren = GlyphFragment::new_char(
|
||||
ctx.font,
|
||||
styles.chain(&denom_style),
|
||||
'(',
|
||||
Span::detached(),
|
||||
)?;
|
||||
// This will never panic as a paren will never shape into nothing.
|
||||
let paren =
|
||||
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
|
||||
.unwrap();
|
||||
|
||||
for (column, col) in columns.iter().zip(&mut cols) {
|
||||
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
|
||||
@ -314,13 +312,15 @@ fn layout_delimiters(
|
||||
span: Span,
|
||||
) -> SourceResult<()> {
|
||||
let short_fall = DELIM_SHORT_FALL.resolve(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||
let axis = value!(font, axis_height).resolve(styles);
|
||||
let height = frame.height();
|
||||
let target = height + VERTICAL_PADDING.of(height);
|
||||
frame.set_baseline(height / 2.0 + axis);
|
||||
|
||||
if let Some(left_c) = left {
|
||||
let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?;
|
||||
let mut left =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?;
|
||||
left.stretch_vertical(ctx, target - short_fall);
|
||||
left.center_on_axis();
|
||||
ctx.push(left);
|
||||
@ -329,7 +329,8 @@ fn layout_delimiters(
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
if let Some(right_c) = right {
|
||||
let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?;
|
||||
let mut right =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?;
|
||||
right.stretch_vertical(ctx, target - short_fall);
|
||||
right.center_on_axis();
|
||||
ctx.push(right);
|
||||
|
@ -13,7 +13,7 @@ mod stretch;
|
||||
mod text;
|
||||
mod underover;
|
||||
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{
|
||||
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
|
||||
@ -27,11 +27,7 @@ use typst_library::layout::{
|
||||
use typst_library::math::*;
|
||||
use typst_library::model::ParElem;
|
||||
use typst_library::routines::{Arenas, RealizationKind};
|
||||
use typst_library::text::{
|
||||
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
|
||||
};
|
||||
use typst_library::World;
|
||||
use typst_syntax::Span;
|
||||
use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem};
|
||||
use typst_utils::Numeric;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
@ -53,12 +49,11 @@ pub fn layout_equation_inline(
|
||||
) -> SourceResult<Vec<InlineItem>> {
|
||||
assert!(!elem.block.get(styles));
|
||||
|
||||
let font = find_math_font(engine, styles, elem.span())?;
|
||||
|
||||
let mut locator = locator.split();
|
||||
let mut ctx = MathContext::new(engine, &mut locator, region, &font);
|
||||
let mut ctx = MathContext::new(engine, &mut locator, region);
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
|
||||
let scale_style = style_for_script_scale(&font);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let run = ctx.layout_into_run(&elem.body, styles)?;
|
||||
@ -108,12 +103,12 @@ pub fn layout_equation_block(
|
||||
assert!(elem.block.get(styles));
|
||||
|
||||
let span = elem.span();
|
||||
let font = find_math_font(engine, styles, span)?;
|
||||
|
||||
let mut locator = locator.split();
|
||||
let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font);
|
||||
let mut ctx = MathContext::new(engine, &mut locator, regions.base());
|
||||
|
||||
let scale_style = style_for_script_scale(&ctx);
|
||||
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
|
||||
let scale_style = style_for_script_scale(&font);
|
||||
let styles = styles.chain(&scale_style);
|
||||
|
||||
let full_equation_builder = ctx
|
||||
@ -234,24 +229,6 @@ pub fn layout_equation_block(
|
||||
Ok(Fragment::frames(frames))
|
||||
}
|
||||
|
||||
fn find_math_font(
|
||||
engine: &mut Engine<'_>,
|
||||
styles: StyleChain,
|
||||
span: Span,
|
||||
) -> SourceResult<Font> {
|
||||
let variant = variant(styles);
|
||||
let world = engine.world;
|
||||
let Some(font) = families(styles).find_map(|family| {
|
||||
let id = world.book().select(family.as_str(), variant)?;
|
||||
let font = world.font(id)?;
|
||||
let _ = font.ttf().tables().math?.constants?;
|
||||
Some(font)
|
||||
}) else {
|
||||
bail!(span, "current font does not support math");
|
||||
};
|
||||
Ok(font)
|
||||
}
|
||||
|
||||
fn add_equation_number(
|
||||
equation_builder: MathRunFrameBuilder,
|
||||
number: Frame,
|
||||
@ -370,9 +347,6 @@ struct MathContext<'a, 'v, 'e> {
|
||||
engine: &'v mut Engine<'e>,
|
||||
locator: &'v mut SplitLocator<'a>,
|
||||
region: Region,
|
||||
// Font-related.
|
||||
font: &'a Font,
|
||||
constants: ttf_parser::math::Constants<'a>,
|
||||
// Mutable.
|
||||
fragments: Vec<MathFragment>,
|
||||
}
|
||||
@ -383,19 +357,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
engine: &'v mut Engine<'e>,
|
||||
locator: &'v mut SplitLocator<'a>,
|
||||
base: Size,
|
||||
font: &'a Font,
|
||||
) -> Self {
|
||||
// These unwraps are safe as the font given is one returned by the
|
||||
// find_math_font function, which only returns fonts that have a math
|
||||
// constants table.
|
||||
let constants = font.ttf().tables().math.unwrap().constants.unwrap();
|
||||
|
||||
Self {
|
||||
engine,
|
||||
locator,
|
||||
region: Region::new(base, Axes::splat(false)),
|
||||
font,
|
||||
constants,
|
||||
fragments: vec![],
|
||||
}
|
||||
}
|
||||
@ -469,17 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
|
||||
styles,
|
||||
)?;
|
||||
|
||||
let outer = styles;
|
||||
for (elem, styles) in pairs {
|
||||
// Hack because the font is fixed in math.
|
||||
if styles != outer
|
||||
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
|
||||
{
|
||||
let frame = layout_external(elem, self, styles)?;
|
||||
self.push(FrameFragment::new(styles, frame).with_spaced(true));
|
||||
continue;
|
||||
}
|
||||
|
||||
layout_realized(elem, self, styles)?;
|
||||
}
|
||||
|
||||
@ -496,7 +452,10 @@ fn layout_realized(
|
||||
if let Some(elem) = elem.to_packed::<TagElem>() {
|
||||
ctx.push(MathFragment::Tag(elem.tag.clone()));
|
||||
} else if elem.is::<SpaceElem>() {
|
||||
let space_width = ctx.font.space_width().unwrap_or(THICK);
|
||||
let space_width = find_math_font(ctx.engine.world, styles, elem.span())
|
||||
.ok()
|
||||
.and_then(|font| font.space_width())
|
||||
.unwrap_or(THICK);
|
||||
ctx.push(MathFragment::Space(space_width.resolve(styles)));
|
||||
} else if elem.is::<LinebreakElem>() {
|
||||
ctx.push(MathFragment::Linebreak);
|
||||
@ -566,10 +525,12 @@ fn layout_realized(
|
||||
self::underover::layout_overshell(elem, ctx, styles)?
|
||||
} else {
|
||||
let mut frame = layout_external(elem, ctx, styles)?;
|
||||
if !frame.has_baseline() {
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
if !frame.has_baseline() && !elem.is::<RawElem>() {
|
||||
if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) {
|
||||
let axis = value!(font, axis_height).resolve(styles);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
}
|
||||
}
|
||||
ctx.push(
|
||||
FrameFragment::new(styles, frame)
|
||||
.with_spaced(true)
|
||||
|
@ -1,11 +1,11 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain};
|
||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{EquationElem, MathSize, RootElem};
|
||||
use typst_library::text::TextElem;
|
||||
use typst_library::visualize::{FixedStroke, Geometry};
|
||||
|
||||
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext};
|
||||
use super::{style_cramped, FrameFragment, MathContext};
|
||||
|
||||
/// Lays out a [`RootElem`].
|
||||
///
|
||||
@ -17,45 +17,62 @@ pub fn layout_root(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let index = elem.index.get_ref(styles);
|
||||
let span = elem.span();
|
||||
|
||||
let gap = scaled!(
|
||||
ctx, styles,
|
||||
text: radical_vertical_gap,
|
||||
display: radical_display_style_vertical_gap,
|
||||
);
|
||||
let thickness = scaled!(ctx, styles, radical_rule_thickness);
|
||||
let extra_ascender = scaled!(ctx, styles, radical_extra_ascender);
|
||||
let kern_before = scaled!(ctx, styles, radical_kern_before_degree);
|
||||
let kern_after = scaled!(ctx, styles, radical_kern_after_degree);
|
||||
let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
|
||||
|
||||
// Layout radicand.
|
||||
let radicand = {
|
||||
let cramped = style_cramped();
|
||||
let styles = styles.chain(&cramped);
|
||||
let run = ctx.layout_into_run(&elem.radicand, styles)?;
|
||||
let multiline = run.is_multiline();
|
||||
let mut radicand = run.into_fragment(styles).into_frame();
|
||||
let radicand = run.into_fragment(styles);
|
||||
if multiline {
|
||||
// Align the frame center line with the math axis.
|
||||
radicand.set_baseline(
|
||||
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height),
|
||||
);
|
||||
}
|
||||
let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?;
|
||||
let axis = value!(font, axis_height).at(size);
|
||||
let mut radicand = radicand.into_frame();
|
||||
radicand.set_baseline(radicand.height() / 2.0 + axis);
|
||||
radicand
|
||||
} else {
|
||||
radicand.into_frame()
|
||||
}
|
||||
};
|
||||
|
||||
// Layout root symbol.
|
||||
let mut sqrt =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
|
||||
|
||||
let (font, size) = sqrt.font(ctx, styles, span)?;
|
||||
let thickness = value!(font, radical_rule_thickness).at(size);
|
||||
let extra_ascender = value!(font, radical_extra_ascender).at(size);
|
||||
let kern_before = value!(font, radical_kern_before_degree).at(size);
|
||||
let kern_after = value!(font, radical_kern_after_degree).at(size);
|
||||
let raise_factor = percent!(font, radical_degree_bottom_raise_percent);
|
||||
let gap = value!(
|
||||
font, styles,
|
||||
text: radical_vertical_gap,
|
||||
display: radical_display_style_vertical_gap,
|
||||
)
|
||||
.at(size);
|
||||
|
||||
let line = FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair(
|
||||
sqrt.fill()
|
||||
.unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()),
|
||||
thickness,
|
||||
)),
|
||||
span,
|
||||
);
|
||||
|
||||
let target = radicand.height() + thickness + gap;
|
||||
let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
|
||||
sqrt.stretch_vertical(ctx, target);
|
||||
let sqrt = sqrt.into_frame();
|
||||
|
||||
// Layout the index.
|
||||
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
|
||||
let index = index
|
||||
let index = elem
|
||||
.index
|
||||
.get_ref(styles)
|
||||
.as_ref()
|
||||
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
|
||||
.transpose()?;
|
||||
@ -107,19 +124,7 @@ pub fn layout_root(
|
||||
}
|
||||
|
||||
frame.push_frame(sqrt_pos, sqrt);
|
||||
frame.push(
|
||||
line_pos,
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(radicand.width())).stroked(
|
||||
FixedStroke::from_pair(
|
||||
styles.get_ref(TextElem::fill).as_decoration(),
|
||||
thickness,
|
||||
),
|
||||
),
|
||||
span,
|
||||
),
|
||||
);
|
||||
|
||||
frame.push(line_pos, line);
|
||||
frame.push_frame(radicand_pos, radicand);
|
||||
ctx.push(FrameFragment::new(styles, frame));
|
||||
|
||||
|
@ -1,32 +1,47 @@
|
||||
use comemo::Tracked;
|
||||
use ttf_parser::math::MathValue;
|
||||
use ttf_parser::Tag;
|
||||
use typst_library::diag::{bail, SourceResult};
|
||||
use typst_library::foundations::{Style, StyleChain};
|
||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{FontFeatures, TextElem};
|
||||
use typst_library::text::{families, variant, Font, FontFeatures, TextElem};
|
||||
use typst_library::World;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::LazyHash;
|
||||
|
||||
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
|
||||
use super::{LeftRightAlternator, MathFragment, MathRun};
|
||||
|
||||
macro_rules! scaled {
|
||||
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
||||
macro_rules! value {
|
||||
($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
|
||||
match $styles.get(typst_library::math::EquationElem::size) {
|
||||
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
|
||||
_ => scaled!($ctx, $styles, $text),
|
||||
typst_library::math::MathSize::Display => value!($font, $display),
|
||||
_ => value!($font, $text),
|
||||
}
|
||||
};
|
||||
($ctx:expr, $styles:expr, $name:ident) => {
|
||||
$crate::math::Scaled::scaled(
|
||||
$ctx.constants.$name(),
|
||||
$ctx,
|
||||
$styles.resolve(typst_library::text::TextElem::size),
|
||||
)
|
||||
($font:expr, $name:ident) => {
|
||||
$font
|
||||
.ttf()
|
||||
.tables()
|
||||
.math
|
||||
.and_then(|math| math.constants)
|
||||
.map(|constants| {
|
||||
crate::math::shared::Scaled::scaled(constants.$name(), &$font)
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! percent {
|
||||
($ctx:expr, $name:ident) => {
|
||||
$ctx.constants.$name() as f64 / 100.0
|
||||
($font:expr, $name:ident) => {
|
||||
$font
|
||||
.ttf()
|
||||
.tables()
|
||||
.math
|
||||
.and_then(|math| math.constants)
|
||||
.map(|constants| constants.$name())
|
||||
.unwrap() as f64
|
||||
/ 100.0
|
||||
};
|
||||
}
|
||||
|
||||
@ -35,27 +50,47 @@ pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||
|
||||
/// Converts some unit to an absolute length with the current font & font size.
|
||||
pub trait Scaled {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
|
||||
fn scaled(self, font: &Font) -> Em;
|
||||
}
|
||||
|
||||
impl Scaled for i16 {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
ctx.font.to_em(self).at(font_size)
|
||||
fn scaled(self, font: &Font) -> Em {
|
||||
font.to_em(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scaled for u16 {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
ctx.font.to_em(self).at(font_size)
|
||||
fn scaled(self, font: &Font) -> Em {
|
||||
font.to_em(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Scaled for MathValue<'_> {
|
||||
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
|
||||
self.value.scaled(ctx, font_size)
|
||||
fn scaled(self, font: &Font) -> Em {
|
||||
self.value.scaled(font)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current math font.
|
||||
#[comemo::memoize]
|
||||
pub fn find_math_font(
|
||||
world: Tracked<dyn World + '_>,
|
||||
styles: StyleChain,
|
||||
span: Span,
|
||||
) -> SourceResult<Font> {
|
||||
let variant = variant(styles);
|
||||
let Some(font) = families(styles).find_map(|family| {
|
||||
let id = world.book().select(family.as_str(), variant)?;
|
||||
let font = world.font(id)?;
|
||||
let _ = font.ttf().tables().math?.constants?;
|
||||
// Take the base font as the "main" math font.
|
||||
family.covers().map_or(Some(font), |_| None)
|
||||
}) else {
|
||||
bail!(span, "current font does not support math");
|
||||
};
|
||||
Ok(font)
|
||||
}
|
||||
|
||||
/// Styles something as cramped.
|
||||
pub fn style_cramped() -> LazyHash<Style> {
|
||||
EquationElem::cramped.set(true).wrap()
|
||||
@ -107,11 +142,12 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
|
||||
}
|
||||
|
||||
/// Styles to add font constants to the style chain.
|
||||
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
|
||||
pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
|
||||
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
|
||||
EquationElem::script_scale
|
||||
.set((
|
||||
ctx.constants.script_percent_scale_down(),
|
||||
ctx.constants.script_script_percent_scale_down(),
|
||||
constants.script_percent_scale_down(),
|
||||
constants.script_script_percent_scale_down(),
|
||||
))
|
||||
.wrap()
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use std::f64::consts::SQRT_2;
|
||||
|
||||
use codex::styling::{to_style, MathStyle};
|
||||
use ecow::EcoString;
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
|
||||
use typst_library::foundations::{Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Abs, Size};
|
||||
use typst_library::math::{EquationElem, MathSize, MathVariant};
|
||||
use typst_library::math::{EquationElem, MathSize};
|
||||
use typst_library::text::{
|
||||
BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
|
||||
};
|
||||
@ -13,8 +14,8 @@ use unicode_math_class::MathClass;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{
|
||||
has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext, MathFragment,
|
||||
MathRun,
|
||||
find_math_font, has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext,
|
||||
MathFragment, MathRun,
|
||||
};
|
||||
|
||||
/// Lays out a [`TextElem`].
|
||||
@ -51,7 +52,8 @@ fn layout_text_lines<'a>(
|
||||
}
|
||||
}
|
||||
let mut frame = MathRun::new(fragments).into_frame(styles);
|
||||
let axis = scaled!(ctx, styles, axis_height);
|
||||
let font = find_math_font(ctx.engine.world, styles, span)?;
|
||||
let axis = value!(font, axis_height).resolve(styles);
|
||||
frame.set_baseline(frame.height() / 2.0 + axis);
|
||||
Ok(FrameFragment::new(styles, frame))
|
||||
}
|
||||
@ -64,13 +66,24 @@ fn layout_inline_text(
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) -> 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 == '.') {
|
||||
// Small optimization for numbers. Note that this lays out slightly
|
||||
// differently to normal text and is worth re-evaluating in the future.
|
||||
let mut fragments = vec![];
|
||||
for unstyled_c in text.chars() {
|
||||
let c = styled_char(styles, unstyled_c, false);
|
||||
let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
||||
// 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();
|
||||
|
||||
// This won't panic as ASCII digits and '.' will never end up as
|
||||
// nothing after shaping.
|
||||
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
|
||||
fragments.push(glyph.into());
|
||||
}
|
||||
let frame = MathRun::new(fragments).into_frame(styles);
|
||||
@ -83,8 +96,10 @@ fn layout_inline_text(
|
||||
.map(|p| p.wrap());
|
||||
|
||||
let styles = styles.chain(&local);
|
||||
let styled_text: EcoString =
|
||||
text.chars().map(|c| styled_char(styles, c, false)).collect();
|
||||
let styled_text: EcoString = text
|
||||
.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 elem = TextElem::packed(styled_text).spanned(span);
|
||||
@ -120,265 +135,37 @@ pub fn layout_symbol(
|
||||
// Switch dotless char to normal when we have the dtls OpenType feature.
|
||||
// This should happen before the main styling pass.
|
||||
let dtls = style_dtls();
|
||||
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
|
||||
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
|
||||
let (unstyled_c, symbol_styles) = match (
|
||||
try_dotless(elem.text),
|
||||
find_math_font(ctx.engine.world, styles, elem.span()),
|
||||
) {
|
||||
(Some(c), Ok(font)) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
|
||||
_ => (elem.text, styles),
|
||||
};
|
||||
let c = styled_char(styles, unstyled_c, true);
|
||||
let fragment: MathFragment =
|
||||
match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) {
|
||||
Ok(mut glyph) => {
|
||||
adjust_glyph_layout(&mut glyph, ctx, styles);
|
||||
glyph.into()
|
||||
}
|
||||
Err(_) => {
|
||||
// Not in the math font, fallback to normal inline text layout.
|
||||
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
|
||||
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
|
||||
.into()
|
||||
}
|
||||
};
|
||||
ctx.push(fragment);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Centers large glyphs vertically on the axis, scaling them if in display
|
||||
/// style.
|
||||
fn adjust_glyph_layout(
|
||||
glyph: &mut GlyphFragment,
|
||||
ctx: &mut MathContext,
|
||||
styles: StyleChain,
|
||||
) {
|
||||
if glyph.class == MathClass::Large {
|
||||
if styles.get(EquationElem::size) == MathSize::Display {
|
||||
let height = scaled!(ctx, styles, display_operator_min_height)
|
||||
.max(SQRT_2 * glyph.size.y);
|
||||
glyph.stretch_vertical(ctx, height);
|
||||
};
|
||||
// TeXbook p 155. Large operators are always vertically centered on the
|
||||
// axis.
|
||||
glyph.center_on_axis();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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),
|
||||
);
|
||||
let italic = styles.get(EquationElem::italic);
|
||||
|
||||
if let Some(c) = basic_exception(c) {
|
||||
return c;
|
||||
}
|
||||
let style = MathStyle::select(unstyled_c, variant, bold, italic);
|
||||
let text: EcoString = to_style(unstyled_c, style).collect();
|
||||
|
||||
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,
|
||||
if let Some(mut glyph) = GlyphFragment::new(ctx, symbol_styles, &text, elem.span())? {
|
||||
if glyph.class == MathClass::Large {
|
||||
if styles.get(EquationElem::size) == MathSize::Display {
|
||||
let height = value!(glyph.item.font, display_operator_min_height)
|
||||
.at(glyph.item.size)
|
||||
.max(SQRT_2 * glyph.size.y);
|
||||
glyph.stretch_vertical(ctx, height);
|
||||
};
|
||||
|
||||
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()
|
||||
// TeXbook p 155. Large operators are always vertically centered on
|
||||
// the axis.
|
||||
glyph.center_on_axis();
|
||||
}
|
||||
ctx.push(glyph);
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The non-dotless version of a dotless character that can be used with the
|
||||
|
@ -1,5 +1,5 @@
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
|
||||
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
|
||||
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
|
||||
use typst_library::math::{
|
||||
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
|
||||
@ -11,7 +11,7 @@ use typst_syntax::Span;
|
||||
|
||||
use super::{
|
||||
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
|
||||
GlyphFragment, LeftRightAlternator, MathContext, MathRun,
|
||||
LeftRightAlternator, MathContext, MathRun,
|
||||
};
|
||||
|
||||
const BRACE_GAP: Em = Em::new(0.25);
|
||||
@ -208,26 +208,29 @@ fn layout_underoverline(
|
||||
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
|
||||
match position {
|
||||
Position::Under => {
|
||||
let sep = scaled!(ctx, styles, underbar_extra_descender);
|
||||
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
|
||||
let gap = scaled!(ctx, styles, underbar_vertical_gap);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
content = ctx.layout_into_fragment(body, styles)?;
|
||||
|
||||
let (font, size) = content.font(ctx, styles, span)?;
|
||||
let sep = value!(font, underbar_extra_descender).at(size);
|
||||
bar_height = value!(font, underbar_rule_thickness).at(size);
|
||||
let gap = value!(font, underbar_vertical_gap).at(size);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
|
||||
content_pos = Point::zero();
|
||||
baseline = content.ascent();
|
||||
line_adjust = -content.italics_correction();
|
||||
}
|
||||
Position::Over => {
|
||||
let sep = scaled!(ctx, styles, overbar_extra_ascender);
|
||||
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
|
||||
let gap = scaled!(ctx, styles, overbar_vertical_gap);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
let cramped = style_cramped();
|
||||
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
|
||||
let styles = styles.chain(&cramped);
|
||||
content = ctx.layout_into_fragment(body, styles)?;
|
||||
|
||||
let (font, size) = content.font(ctx, styles, span)?;
|
||||
let sep = value!(font, overbar_extra_ascender).at(size);
|
||||
bar_height = value!(font, overbar_rule_thickness).at(size);
|
||||
let gap = value!(font, overbar_vertical_gap).at(size);
|
||||
extra_height = sep + bar_height + gap;
|
||||
|
||||
line_pos = Point::with_y(sep + bar_height / 2.0);
|
||||
content_pos = Point::with_y(extra_height);
|
||||
@ -285,7 +288,8 @@ fn layout_underoverspreader(
|
||||
let body = ctx.layout_into_run(body, styles)?;
|
||||
let body_class = body.class();
|
||||
let body = body.into_fragment(styles);
|
||||
let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
|
||||
let mut glyph =
|
||||
ctx.layout_into_fragment(&SymbolElem::packed(c).spanned(span), styles)?;
|
||||
glyph.stretch_horizontal(ctx, body.width());
|
||||
|
||||
let mut rows = vec![];
|
||||
|
@ -64,6 +64,16 @@ impl<T: NativeElement> Packed<T> {
|
||||
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.
|
||||
pub fn unpack(self) -> T {
|
||||
// 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) {
|
||||
self.0.set_location(location);
|
||||
}
|
||||
|
||||
pub fn as_content(&self) -> &Content {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: NativeElement> AsRef<T> for Packed<T> {
|
||||
|
@ -141,7 +141,7 @@ impl RawContent {
|
||||
|
||||
/// Clones a packed element into new raw content.
|
||||
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();
|
||||
RawContent::create(
|
||||
elem.as_ref().clone(),
|
||||
|
@ -165,6 +165,11 @@ cast! {
|
||||
pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>);
|
||||
|
||||
impl HtmlAttrs {
|
||||
/// Creates an empty attribute list.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add an attribute.
|
||||
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
|
||||
self.0.push((attr, value.into()));
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use codex::styling::MathVariant;
|
||||
use typst_utils::NonZeroExt;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
@ -12,7 +13,7 @@ use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
|
||||
use crate::layout::{
|
||||
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::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
|
||||
|
||||
@ -111,7 +112,7 @@ pub struct EquationElem {
|
||||
/// The style variant to select.
|
||||
#[internal]
|
||||
#[ghost]
|
||||
pub variant: MathVariant,
|
||||
pub variant: Option<MathVariant>,
|
||||
|
||||
/// Affects the height of exponents.
|
||||
#[internal]
|
||||
@ -128,7 +129,7 @@ pub struct EquationElem {
|
||||
/// Whether to use italic glyphs.
|
||||
#[internal]
|
||||
#[ghost]
|
||||
pub italic: Smart<bool>,
|
||||
pub italic: Option<bool>,
|
||||
|
||||
/// A forced class to use for all fragment.
|
||||
#[internal]
|
||||
|
@ -80,6 +80,7 @@ pub fn module() -> Module {
|
||||
math.define_func::<italic>();
|
||||
math.define_func::<serif>();
|
||||
math.define_func::<sans>();
|
||||
math.define_func::<scr>();
|
||||
math.define_func::<cal>();
|
||||
math.define_func::<frak>();
|
||||
math.define_func::<mono>();
|
||||
|
@ -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;
|
||||
|
||||
/// Bold font style in math.
|
||||
@ -24,7 +26,7 @@ pub fn upright(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::italic, Smart::Custom(false))
|
||||
body.set(EquationElem::italic, Some(false))
|
||||
}
|
||||
|
||||
/// Italic font style in math.
|
||||
@ -35,7 +37,7 @@ pub fn italic(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::italic, Smart::Custom(true))
|
||||
body.set(EquationElem::italic, Some(true))
|
||||
}
|
||||
|
||||
/// Serif (roman) font style in math.
|
||||
@ -46,7 +48,7 @@ pub fn serif(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, MathVariant::Serif)
|
||||
body.set(EquationElem::variant, Some(MathVariant::Plain))
|
||||
}
|
||||
|
||||
/// Sans-serif font style in math.
|
||||
@ -59,41 +61,54 @@ pub fn sans(
|
||||
/// The content to style.
|
||||
body: 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
|
||||
/// Let $cal(P)$ be the set of ...
|
||||
/// ```
|
||||
///
|
||||
/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these
|
||||
/// styles share the same Unicode codepoints. Switching between the styles is
|
||||
/// thus only possible if supported by the font via
|
||||
/// [font features]($text.features).
|
||||
///
|
||||
/// For the default math font, the roundhand style is available through the
|
||||
/// `ss01` feature. Therefore, you could define your own version of `\mathscr`
|
||||
/// like this:
|
||||
///
|
||||
/// ```example
|
||||
/// #let scr(it) = text(
|
||||
/// features: ("ss01",),
|
||||
/// box($cal(it)$),
|
||||
/// )
|
||||
///
|
||||
/// We establish $cal(P) != scr(P)$.
|
||||
/// ```
|
||||
///
|
||||
/// (The box is not conceptually necessary, but unfortunately currently needed
|
||||
/// due to limitations in Typst's text style handling in math.)
|
||||
#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])]
|
||||
/// This is the default calligraphic/script style for most math fonts. See
|
||||
/// [`scr`]($math.scr) for more on how to get the other style (roundhand).
|
||||
#[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])]
|
||||
pub fn cal(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, MathVariant::Cal)
|
||||
body.set(EquationElem::variant, Some(MathVariant::Chancery))
|
||||
}
|
||||
|
||||
/// Script (roundhand) font style in math.
|
||||
///
|
||||
/// ```example
|
||||
/// $ scr(S) $
|
||||
/// ```
|
||||
///
|
||||
/// 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
|
||||
/// #let scr(it) = text(
|
||||
/// features: ("ss01",),
|
||||
/// $cal(it)$,
|
||||
/// )
|
||||
///
|
||||
/// We establish $cal(P) != scr(P)$.
|
||||
/// ```
|
||||
#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])]
|
||||
pub fn scr(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, Some(MathVariant::Roundhand))
|
||||
}
|
||||
|
||||
/// Fraktur font style in math.
|
||||
@ -106,7 +121,7 @@ pub fn frak(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, MathVariant::Frak)
|
||||
body.set(EquationElem::variant, Some(MathVariant::Fraktur))
|
||||
}
|
||||
|
||||
/// Monospace font style in math.
|
||||
@ -119,7 +134,7 @@ pub fn mono(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, MathVariant::Mono)
|
||||
body.set(EquationElem::variant, Some(MathVariant::Monospace))
|
||||
}
|
||||
|
||||
/// Blackboard bold (double-struck) font style in math.
|
||||
@ -137,7 +152,7 @@ pub fn bb(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
) -> Content {
|
||||
body.set(EquationElem::variant, MathVariant::Bb)
|
||||
body.set(EquationElem::variant, Some(MathVariant::DoubleStruck))
|
||||
}
|
||||
|
||||
/// Forced display style in math.
|
||||
@ -240,15 +255,3 @@ pub enum MathSize {
|
||||
/// Math on its own line.
|
||||
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,
|
||||
}
|
||||
|
@ -937,6 +937,7 @@ pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily>
|
||||
"noto color emoji",
|
||||
"apple color emoji",
|
||||
"segoe ui emoji",
|
||||
"new computer modern math",
|
||||
]
|
||||
.into_iter()
|
||||
.map(FontFamily::new)
|
||||
|
@ -8,6 +8,7 @@ pub use self::raster::{
|
||||
};
|
||||
pub use self::svg::SvgImage;
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -15,14 +16,16 @@ use ecow::EcoString;
|
||||
use typst_syntax::{Span, Spanned};
|
||||
use typst_utils::LazyHash;
|
||||
|
||||
use crate::diag::StrResult;
|
||||
use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart,
|
||||
StyleChain,
|
||||
};
|
||||
use crate::layout::{Length, Rel, Sizing};
|
||||
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
|
||||
use crate::model::Figurable;
|
||||
use crate::text::LocalName;
|
||||
use crate::text::{families, LocalName};
|
||||
|
||||
/// 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> {
|
||||
const KEY: &'static str = "figure";
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ use typst_library::model::{
|
||||
ParElem, ParbreakElem, TermsElem,
|
||||
};
|
||||
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
|
||||
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||
use typst_library::text::{LinebreakElem, RawElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{SliceExt, SmallBitSet};
|
||||
|
||||
@ -287,6 +287,13 @@ fn visit_kind_rules<'a>(
|
||||
styles: StyleChain<'a>,
|
||||
) -> SourceResult<bool> {
|
||||
if let RealizationKind::Math = s.kind {
|
||||
// Deal with Raw later when it gets laid out externally, so that it
|
||||
// renders correctly in math.
|
||||
if content.is::<RawElem>() {
|
||||
s.sink.push((content, styles));
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Transparently recurse into equations nested in math, so that things
|
||||
// like this work:
|
||||
// ```
|
||||
|
@ -18,18 +18,24 @@ impl SVGRenderer {
|
||||
self.xml.write_attribute("width", &size.x.to_pt());
|
||||
self.xml.write_attribute("height", &size.y.to_pt());
|
||||
self.xml.write_attribute("preserveAspectRatio", "none");
|
||||
match image.scaling() {
|
||||
Smart::Auto => {}
|
||||
if let Some(value) = convert_image_scaling(image.scaling()) {
|
||||
self.xml
|
||||
.write_attribute("style", &format_args!("image-rendering: {value}"))
|
||||
}
|
||||
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
|
||||
self.xml.write_attribute("style", "image-rendering: smooth")
|
||||
Some("smooth")
|
||||
}
|
||||
Smart::Custom(ImageScaling::Pixelated) => {
|
||||
self.xml.write_attribute("style", "image-rendering: pixelated")
|
||||
}
|
||||
}
|
||||
self.xml.end_element();
|
||||
Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@ mod paint;
|
||||
mod shape;
|
||||
mod text;
|
||||
|
||||
pub use image::{convert_image_scaling, convert_image_to_base64_url};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
title: Variants
|
||||
category: math
|
||||
path: ["math"]
|
||||
filter: ["serif", "sans", "frak", "mono", "bb", "cal"]
|
||||
filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"]
|
||||
details: |
|
||||
Alternate typefaces within formulas.
|
||||
|
||||
|
8
tests/ref/html/image-jpg-html-base64.html
Normal 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>
|
10
tests/ref/html/image-scaling-methods.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>
|
||||
<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>
|
Before Width: | Height: | Size: 868 B After Width: | Height: | Size: 884 B |
BIN
tests/ref/math-font-covers.png
Normal file
After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 402 B After Width: | Height: | Size: 492 B |
BIN
tests/ref/math-font-features-switch.png
Normal file
After Width: | Height: | Size: 968 B |
Before Width: | Height: | Size: 611 B After Width: | Height: | Size: 607 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/math-op-font.png
Normal file
After Width: | Height: | Size: 440 B |
BIN
tests/ref/math-op-set-font.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
tests/ref/math-style-fallback.png
Normal file
After Width: | Height: | Size: 935 B |
Before Width: | Height: | Size: 296 B After Width: | Height: | Size: 450 B |
BIN
tests/ref/math-style-script.png
Normal file
After Width: | Height: | Size: 585 B |
@ -28,3 +28,19 @@ $ bold(op("bold", limits: #true))_x y $
|
||||
--- math-non-math-content ---
|
||||
// With non-text content
|
||||
$ op(#underline[ul]) a $
|
||||
|
||||
--- math-op-font ---
|
||||
// Test with different font.
|
||||
#let colim = math.op(
|
||||
text(font: "IBM Plex Sans", weight: "regular", size: 0.8em)[colim],
|
||||
limits: true,
|
||||
)
|
||||
$ colim_(x -> 0) inline(colim_(x -> 0)) $
|
||||
|
||||
--- math-op-set-font ---
|
||||
// Test setting font.
|
||||
#let lig = math.op("fi")
|
||||
#let test = $sin(x) lim_(x -> oo) lig_1(X)$
|
||||
#test
|
||||
#show math.op: set text(font: "Libertinus Serif")
|
||||
#test
|
||||
|
@ -12,6 +12,15 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \
|
||||
bb("hello") + bold(cal("world")), \
|
||||
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 ---
|
||||
// Test styling dotless i and j.
|
||||
$ dotless.i dotless.j,
|
||||
@ -38,7 +47,15 @@ $bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$
|
||||
|
||||
--- math-style-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 ---
|
||||
_abc $sin(x) "abc"$_ \
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
--- math-font-fallback ---
|
||||
// Test font fallback.
|
||||
$ よ and 🏳️🌈 $
|
||||
$ よ and "よ" and 🏳️🌈 $
|
||||
|
||||
--- math-text-color ---
|
||||
// Test text properties.
|
||||
@ -17,6 +17,21 @@ $ nothing $
|
||||
$ "hi ∅ hey" $
|
||||
$ sum_(i in NN) 1 + i $
|
||||
|
||||
--- math-font-features-switch ---
|
||||
#let scr(it) = text(features: ("ss01",), $cal(it)$)
|
||||
$cal(P)_i != scr(P)_i$, $cal(bold(I))_l != bold(scr(I))_l$
|
||||
$ product.co_(B in scr(B))^(B in scr(bold(B))) cal(B)(X) $
|
||||
|
||||
--- math-font-covers ---
|
||||
#show math.equation: set text(
|
||||
font: (
|
||||
// Ignore that this regex actually misses some of the script glyphs...
|
||||
(name: "XITS Math", covers: regex("[\u{1D49C}-\u{1D503}]")),
|
||||
),
|
||||
features: ("ss01",),
|
||||
)
|
||||
$ cal(P)_i (X) * cal(C)_1 $
|
||||
|
||||
--- math-optical-size-nested-scripts ---
|
||||
// Test transition from script to scriptscript.
|
||||
#[
|
||||
|
@ -9,6 +9,9 @@
|
||||
#set page(height: 60pt)
|
||||
#image("/assets/images/tiger.jpg")
|
||||
|
||||
--- image-jpg-html-base64 html ---
|
||||
#image("/assets/images/f2t.jpg", alt: "The letter F")
|
||||
|
||||
--- image-sizing ---
|
||||
// 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,
|
||||
)
|
||||
|
||||
--- image-scaling-methods ---
|
||||
--- image-scaling-methods render html ---
|
||||
#let img(scaling) = image(
|
||||
bytes((
|
||||
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,
|
||||
)
|
||||
|
||||
#stack(
|
||||
dir: ltr,
|
||||
spacing: 4pt,
|
||||
#let images = (
|
||||
img(auto),
|
||||
img("smooth"),
|
||||
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 ---
|
||||
// Test that images aren't upscaled.
|
||||
// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
|
||||
|