Compare commits

..

3 Commits

Author SHA1 Message Date
Gabriel Araújo
1660f63946 Improve documentation for page bleed 2025-06-25 23:06:41 -03:00
Gabriel Araújo
84ccaacba0 rename bleed_start with content_origin 2025-06-25 23:06:41 -03:00
Gabriel Araújo
daa9399124 Add bleed support to page layout 2025-06-25 23:06:41 -03:00
81 changed files with 429 additions and 1062 deletions

4
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=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -2911,7 +2911,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" 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=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"

View File

@ -33,7 +33,7 @@ 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 = "c1089b4" }
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 = "fddbf8b" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -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 = "56eb217" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"

View File

@ -240,26 +240,6 @@ instant preview. To achieve these goals, we follow three core design principles:
Luckily we have [`comemo`], a system for incremental compilation which does Luckily we have [`comemo`], a system for incremental compilation which does
most of the hard work in the background. most of the hard work in the background.
## Acknowledgements
We'd like to thank everyone who is supporting Typst's development, be it via
[GitHub sponsors] or elsewhere. In particular, special thanks[^1] go to:
- [Posit](https://posit.co/blog/posit-and-typst/) for financing a full-time
compiler engineer
- [NLnet](https://nlnet.nl/) for supporting work on Typst via multiple grants
through the [NGI Zero Core](https://nlnet.nl/core) fund:
- Work on [HTML export](https://nlnet.nl/project/Typst-HTML/)
- Work on [PDF accessibility](https://nlnet.nl/project/Typst-Accessibility/)
- [Science & Startups](https://www.science-startups.berlin/) for having financed
Typst development from January through June 2023 via the Berlin Startup
Scholarship
- [Zerodha](https://zerodha.tech/blog/1-5-million-pdfs-in-25-minutes/) for their
generous one-time sponsorship
[^1]: This list only includes contributions for our open-source work that exceed
or are expected to exceed €10K.
[docs]: https://typst.app/docs/ [docs]: https://typst.app/docs/
[app]: https://typst.app/ [app]: https://typst.app/
[discord]: https://discord.gg/2uDybryKPe [discord]: https://discord.gg/2uDybryKPe
@ -279,4 +259,3 @@ We'd like to thank everyone who is supporting Typst's development, be it via
[packages]: https://github.com/typst/packages/ [packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/ [`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst [snap]: https://snapcraft.io/typst
[GitHub sponsors]: https://github.com/sponsors/typst/

View File

@ -491,7 +491,7 @@ pub enum PdfStandard {
/// PDF/A-2u. /// PDF/A-2u.
#[value(name = "a-2u")] #[value(name = "a-2u")]
A_2u, A_2u,
/// PDF/A-3b. /// PDF/A-3u.
#[value(name = "a-3b")] #[value(name = "a-3b")]
A_3b, A_3b,
/// PDF/A-3u. /// PDF/A-3u.

View File

@ -205,9 +205,7 @@ impl Eval for ast::Label<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Label( Ok(Value::Label(Label::new(PicoStr::intern(self.get()))))
Label::new(PicoStr::intern(self.get())).expect("unexpected empty label"),
))
} }
} }
@ -215,8 +213,7 @@ impl Eval for ast::Ref<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let target = Label::new(PicoStr::intern(self.target())) let target = Label::new(PicoStr::intern(self.target()));
.expect("unexpected empty reference");
let mut elem = RefElem::new(target); let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() { if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content( elem.push_supplement(Smart::Custom(Some(Supplement::Content(

View File

@ -3,8 +3,9 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr; use typst_library::foundations::Repr;
use typst_library::html::{ use typst_library::html::{
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag,
}; };
use typst_library::layout::Frame;
use typst_syntax::Span; use typst_syntax::Span;
/// Encodes an HTML document into a string. /// Encodes an HTML document into a string.
@ -303,15 +304,9 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
} }
/// Encode a laid out frame into the writer. /// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &HtmlFrame) { fn write_frame(w: &mut Writer, frame: &Frame) {
// FIXME: This string replacement is obviously a hack. // FIXME: This string replacement is obviously a hack.
let svg = typst_svg::svg_frame(&frame.inner).replace( let svg = typst_svg::svg_frame(frame)
"<svg class", .replace("<svg class", "<svg style=\"overflow: visible;\" class");
&format!(
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
frame.inner.width() / frame.text_size,
frame.inner.height() / frame.text_size,
),
);
w.buf.push_str(&svg); w.buf.push_str(&svg);
} }

View File

@ -9,7 +9,7 @@ use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{ use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
}; };
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem, Introspector, Locator, LocatorLink, SplitLocator, TagElem,
@ -246,10 +246,7 @@ fn handle(
styles.chain(&style), styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)), Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?; )?;
output.push(HtmlNode::Frame(HtmlFrame { output.push(HtmlNode::Frame(frame));
inner: frame,
text_size: TextElem::size_in(styles),
}));
} else { } else {
engine.sink.warn(warning!( engine.sink.warn(warning!(
child.span(), child.span(),

View File

@ -448,7 +448,7 @@ fn field_access_completions(
match value { match value {
Value::Symbol(symbol) => { Value::Symbol(symbol) => {
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {
if let Ok(modified) = symbol.clone().modified((), modifier) { if let Ok(modified) = symbol.clone().modified(modifier) {
ctx.completions.push(Completion { ctx.completions.push(Completion {
kind: CompletionKind::Symbol(modified.get()), kind: CompletionKind::Symbol(modified.get()),
label: modifier.into(), label: modifier.into(),

View File

@ -72,8 +72,7 @@ pub fn definition(
// Try to jump to the referenced content. // Try to jump to the referenced content.
DerefTarget::Ref(node) => { DerefTarget::Ref(node) => {
let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target())) let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target()));
.expect("unexpected empty reference");
let selector = Selector::Label(label); let selector = Selector::Label(label);
let elem = document?.introspector.query_first(&selector)?; let elem = document?.introspector.query_first(&selector)?;
return Some(Definition::Span(elem.span())); return Some(Definition::Span(elem.span()));

View File

@ -199,7 +199,7 @@ impl PackageStorage {
// The place at which the specific package version will live in the end. // The place at which the specific package version will live in the end.
let package_dir = base_dir.join(format!("{}", spec.version)); let package_dir = base_dir.join(format!("{}", spec.version));
// To prevent multiple Typst instances from interfering, we download // To prevent multiple Typst instances from interferring, we download
// into a temporary directory first and then move this directory to // into a temporary directory first and then move this directory to
// its final destination. // its final destination.
// //

View File

@ -206,7 +206,7 @@ pub fn collect<'a>(
} }
InlineItem::Frame(mut frame) => { InlineItem::Frame(mut frame) => {
frame.modify(&FrameModifiers::get_in(styles)); frame.modify(&FrameModifiers::get_in(styles));
apply_shift(&engine.world, &mut frame, styles); apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame)); collector.push_item(Item::Frame(frame));
} }
} }
@ -221,7 +221,7 @@ pub fn collect<'a>(
let mut frame = layout_and_modify(styles, |styles| { let mut frame = layout_and_modify(styles, |styles| {
layout_box(elem, engine, loc, styles, region) layout_box(elem, engine, loc, styles, region)
})?; })?;
apply_shift(&engine.world, &mut frame, styles); apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame)); collector.push_item(Item::Frame(frame));
} }
} else if let Some(elem) = child.to_packed::<TagElem>() { } else if let Some(elem) = child.to_packed::<TagElem>() {

View File

@ -5,7 +5,7 @@ use typst_library::engine::Engine;
use typst_library::introspection::{SplitLocator, Tag}; use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::ParLineMarker; use typst_library::model::ParLineMarker;
use typst_library::text::{variant, Lang, TextElem}; use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric; use typst_utils::Numeric;
use super::*; use super::*;
@ -219,7 +219,7 @@ fn collect_items<'a>(
// Add fallback text to expand the line height, if necessary. // Add fallback text to expand the line height, if necessary.
if !items.iter().any(|item| matches!(item, Item::Text(_))) { if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback { if let Some(fallback) = fallback {
items.push(fallback, usize::MAX); items.push(fallback);
} }
} }
@ -270,10 +270,10 @@ fn collect_range<'a>(
items: &mut Items<'a>, items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>, fallback: &mut Option<ItemEntry<'a>>,
) { ) {
for (idx, (subrange, item)) in p.slice(range.clone()).enumerate() { for (subrange, item) in p.slice(range.clone()) {
// All non-text items are just kept, they can't be split. // All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else { let Item::Text(shaped) = item else {
items.push(item, idx); items.push(item);
continue; continue;
}; };
@ -293,10 +293,10 @@ fn collect_range<'a>(
} else if split { } else if split {
// When the item is split in half, reshape it. // When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced); let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped), idx); items.push(Item::Text(reshaped));
} else { } else {
// When the item is fully contained, just keep it. // When the item is fully contained, just keep it.
items.push(item, idx); items.push(item);
} }
} }
} }
@ -330,7 +330,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(glyph.size); shaped.width -= shrink.at(shaped.size);
} else if p.config.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& glyph.x_offset > Em::zero() && glyph.x_offset > Em::zero()
@ -342,7 +342,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
glyph.x_advance -= shrink; glyph.x_advance -= shrink;
glyph.x_offset = Em::zero(); glyph.x_offset = Em::zero();
glyph.adjustability.shrinkability.0 = Em::zero(); glyph.adjustability.shrinkability.0 = Em::zero();
shaped.width -= shrink.at(glyph.size); shaped.width -= shrink.at(shaped.size);
} }
} }
@ -360,7 +360,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().1; let shrink = glyph.shrinkability().1;
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(punct.size); shaped.width -= shrink.at(shaped.size);
} else if p.config.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
@ -371,7 +371,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let glyph = shaped.glyphs.to_mut().last_mut().unwrap(); let glyph = shaped.glyphs.to_mut().last_mut().unwrap();
glyph.x_advance -= shrink; glyph.x_advance -= shrink;
glyph.adjustability.shrinkability.1 = Em::zero(); glyph.adjustability.shrinkability.1 = Em::zero();
shaped.width -= shrink.at(glyph.size); shaped.width -= shrink.at(shaped.size);
} }
} }
@ -412,30 +412,9 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
} }
} }
/// Apply the current baseline shift and italic compensation to a frame. /// Apply the current baseline shift to a frame.
pub fn apply_shift<'a>( pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
world: &Tracked<'a, dyn World + 'a>, frame.translate(Point::with_y(TextElem::baseline_in(styles)));
frame: &mut Frame,
styles: StyleChain,
) {
let mut baseline = TextElem::baseline_in(styles);
let mut compensation = Abs::zero();
if let Some(scripts) = TextElem::shift_settings_in(styles) {
let font_metrics = TextElem::font_in(styles)
.into_iter()
.find_map(|family| {
world
.book()
.select(family.as_str(), variant(styles))
.and_then(|id| world.font(id))
})
.map_or(*scripts.kind.default_metrics(), |f| {
*scripts.kind.read_metrics(f.metrics())
});
baseline -= scripts.shift.unwrap_or(font_metrics.vertical_offset).resolve(styles);
compensation += font_metrics.horizontal_offset.resolve(styles);
}
frame.translate(Point::new(compensation, baseline));
} }
/// Commit to a line and build its frame. /// Commit to a line and build its frame.
@ -465,7 +444,7 @@ pub fn commit(
&& TextElem::overhang_in(text.styles) && TextElem::overhang_in(text.styles)
&& (line.items.len() > 1 || text.glyphs.len() > 1) && (line.items.len() > 1 || text.glyphs.len() > 1)
{ {
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size); let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
offset -= amount; offset -= amount;
remaining += amount; remaining += amount;
} }
@ -479,7 +458,7 @@ pub fn commit(
&& TextElem::overhang_in(text.styles) && TextElem::overhang_in(text.styles)
&& (line.items.len() > 1 || text.glyphs.len() > 1) && (line.items.len() > 1 || text.glyphs.len() > 1)
{ {
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size); let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
remaining += amount; remaining += amount;
} }
} }
@ -520,16 +499,16 @@ pub fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for &(idx, ref item) in line.items.indexed_iter() { for item in line.items.iter() {
let mut push = |offset: &mut Abs, frame: Frame, idx: usize| { let mut push = |offset: &mut Abs, frame: Frame| {
let width = frame.width(); let width = frame.width();
top.set_max(frame.baseline()); top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline()); bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame, idx)); frames.push((*offset, frame));
*offset += width; *offset += width;
}; };
match &**item { match item {
Item::Absolute(v, _) => { Item::Absolute(v, _) => {
offset += *v; offset += *v;
} }
@ -540,8 +519,8 @@ pub fn commit(
let mut frame = layout_and_modify(*styles, |styles| { let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), styles, region) layout_box(elem, engine, loc.relayout(), styles, region)
})?; })?;
apply_shift(&engine.world, &mut frame, *styles); apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame, idx); push(&mut offset, frame);
} else { } else {
offset += amount; offset += amount;
} }
@ -553,15 +532,15 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame, idx); push(&mut offset, frame);
} }
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone(), idx); push(&mut offset, frame.clone());
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame, idx)); frames.push((offset, frame));
} }
Item::Skip(_) => {} Item::Skip(_) => {}
} }
@ -580,13 +559,8 @@ pub fn commit(
add_par_line_marker(&mut output, marker, engine, locator, top); add_par_line_marker(&mut output, marker, engine, locator, top);
} }
// Ensure that the final frame's items are in logical order rather than in
// visual order. This is important because it affects the order of elements
// during introspection and thus things like counters.
frames.sort_unstable_by_key(|(_, _, idx)| *idx);
// Construct the line's frame. // Construct the line's frame.
for (offset, frame, _) in frames { for (offset, frame) in frames {
let x = offset + p.config.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
@ -653,7 +627,7 @@ fn overhang(c: char) -> f64 {
} }
/// A collection of owned or borrowed inline items. /// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<(usize, ItemEntry<'a>)>); pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> { impl<'a> Items<'a> {
/// Create empty items. /// Create empty items.
@ -662,38 +636,33 @@ impl<'a> Items<'a> {
} }
/// Push a new item. /// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) { pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) {
self.0.push((idx, entry.into())); self.0.push(entry.into());
} }
/// Iterate over the items. /// Iterate over the items.
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> { pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|(_, item)| &**item) self.0.iter().map(|item| &**item)
}
/// Iterate over the items with indices
pub fn indexed_iter(&self) -> impl Iterator<Item = &(usize, ItemEntry<'a>)> {
self.0.iter()
} }
/// Access the first item. /// Access the first item.
pub fn first(&self) -> Option<&Item<'a>> { pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|(_, item)| &**item) self.0.first().map(|item| &**item)
} }
/// Access the last item. /// Access the last item.
pub fn last(&self) -> Option<&Item<'a>> { pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|(_, item)| &**item) self.0.last().map(|item| &**item)
} }
/// Access the first item mutably, if it is text. /// Access the first item mutably, if it is text.
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.1.text_mut() self.0.first_mut()?.text_mut()
} }
/// Access the last item mutably, if it is text. /// Access the last item mutably, if it is text.
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.1.text_mut() self.0.last_mut()?.text_mut()
} }
/// Reorder the items starting at the given index to RTL. /// Reorder the items starting at the given index to RTL.
@ -704,12 +673,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> { impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().enumerate().collect()) Self(iter.into_iter().collect())
} }
} }
impl<'a> Deref for Items<'a> { impl<'a> Deref for Items<'a> {
type Target = Vec<(usize, ItemEntry<'a>)>; type Target = Vec<ItemEntry<'a>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0

View File

@ -927,9 +927,9 @@ impl Estimates {
let byte_len = g.range.len(); let byte_len = g.range.len();
let stretch = g.stretchability().0 + g.stretchability().1; let stretch = g.stretchability().0 + g.stretchability().1;
let shrink = g.shrinkability().0 + g.shrinkability().1; let shrink = g.shrinkability().0 + g.shrinkability().1;
widths.push(byte_len, g.x_advance.at(g.size)); widths.push(byte_len, g.x_advance.at(shaped.size));
stretchability.push(byte_len, stretch.at(g.size)); stretchability.push(byte_len, stretch.at(shaped.size));
shrinkability.push(byte_len, shrink.at(g.size)); shrinkability.push(byte_len, shrink.at(shaped.size));
justifiables.push(byte_len, g.is_justifiable() as usize); justifiables.push(byte_len, g.is_justifiable() as usize);
} }
} else { } else {

View File

@ -29,7 +29,7 @@ use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate; use self::deco::decorate;
use self::finalize::finalize; use self::finalize::finalize;
use self::line::{apply_shift, commit, line, Line}; use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint}; use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation}; use self::prepare::{prepare, Preparation};
use self::shaping::{ use self::shaping::{

View File

@ -144,7 +144,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
// The spacing is default to 1/4 em, and can be shrunk to 1/8 em. // The spacing is default to 1/4 em, and can be shrunk to 1/8 em.
glyph.x_advance += Em::new(0.25); glyph.x_advance += Em::new(0.25);
glyph.adjustability.shrinkability.1 += Em::new(0.125); glyph.adjustability.shrinkability.1 += Em::new(0.125);
text.width += Em::new(0.25).at(glyph.size); text.width += Em::new(0.25).at(text.size);
} }
// Case 2: Latin followed by a CJ character // Case 2: Latin followed by a CJ character
@ -152,7 +152,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
glyph.x_advance += Em::new(0.25); glyph.x_advance += Em::new(0.25);
glyph.x_offset += Em::new(0.25); glyph.x_offset += Em::new(0.25);
glyph.adjustability.shrinkability.0 += Em::new(0.125); glyph.adjustability.shrinkability.0 += Em::new(0.125);
text.width += Em::new(0.25).at(glyph.size); text.width += Em::new(0.25).at(text.size);
} }
prev = Some(glyph); prev = Some(glyph);

View File

@ -3,15 +3,14 @@ use std::fmt::{self, Debug, Formatter};
use std::sync::Arc; use std::sync::Arc;
use az::SaturatingAs; use az::SaturatingAs;
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer}; use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use ttf_parser::gsub::SubstitutionSubtable;
use ttf_parser::Tag; use ttf_parser::Tag;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain}; use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{ use typst_library::text::{
families, features, is_default_ignorable, language, variant, Font, FontFamily, families, features, is_default_ignorable, language, variant, Font, FontFamily,
FontVariant, Glyph, Lang, Region, ShiftSettings, TextEdgeBounds, TextElem, TextItem, FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
}; };
use typst_library::World; use typst_library::World;
use typst_utils::SliceExt; use typst_utils::SliceExt;
@ -42,6 +41,8 @@ pub struct ShapedText<'a> {
pub styles: StyleChain<'a>, pub styles: StyleChain<'a>,
/// The font variant. /// The font variant.
pub variant: FontVariant, pub variant: FontVariant,
/// The font size.
pub size: Abs,
/// The width of the text's bounding box. /// The width of the text's bounding box.
pub width: Abs, pub width: Abs,
/// The shaped glyphs. /// The shaped glyphs.
@ -61,8 +62,6 @@ pub struct ShapedGlyph {
pub x_offset: Em, pub x_offset: Em,
/// The vertical offset of the glyph. /// The vertical offset of the glyph.
pub y_offset: Em, pub y_offset: Em,
/// The font size for the glyph.
pub size: Abs,
/// The adjustability of the glyph. /// The adjustability of the glyph.
pub adjustability: Adjustability, pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full inline layout. A /// The byte range of this glyph's cluster in the full inline layout. A
@ -223,17 +222,14 @@ impl<'a> ShapedText<'a> {
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
frame.set_baseline(top); frame.set_baseline(top);
let size = TextElem::size_in(self.styles);
let shift = TextElem::baseline_in(self.styles); let shift = TextElem::baseline_in(self.styles);
let decos = TextElem::deco_in(self.styles); let decos = TextElem::deco_in(self.styles);
let fill = TextElem::fill_in(self.styles); let fill = TextElem::fill_in(self.styles);
let stroke = TextElem::stroke_in(self.styles); let stroke = TextElem::stroke_in(self.styles);
let span_offset = TextElem::span_offset_in(self.styles); let span_offset = TextElem::span_offset_in(self.styles);
for ((font, y_offset, glyph_size), group) in self for ((font, y_offset), group) in
.glyphs self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
.as_ref()
.group_by_key(|g| (g.font.clone(), g.y_offset, g.size))
{ {
let mut range = group[0].range.clone(); let mut range = group[0].range.clone();
for glyph in group { for glyph in group {
@ -241,7 +237,7 @@ impl<'a> ShapedText<'a> {
range.end = range.end.max(glyph.range.end); range.end = range.end.max(glyph.range.end);
} }
let pos = Point::new(offset, top + shift - y_offset.at(size)); let pos = Point::new(offset, top + shift - y_offset.at(self.size));
let glyphs: Vec<Glyph> = group let glyphs: Vec<Glyph> = group
.iter() .iter()
.map(|shaped: &ShapedGlyph| { .map(|shaped: &ShapedGlyph| {
@ -261,11 +257,11 @@ impl<'a> ShapedText<'a> {
adjustability_right * justification_ratio; adjustability_right * justification_ratio;
if shaped.is_justifiable() { if shaped.is_justifiable() {
justification_right += justification_right +=
Em::from_abs(extra_justification, glyph_size) Em::from_length(extra_justification, self.size)
} }
frame.size_mut().x += justification_left.at(glyph_size) frame.size_mut().x += justification_left.at(self.size)
+ justification_right.at(glyph_size); + justification_right.at(self.size);
// We may not be able to reach the offset completely if // We may not be able to reach the offset completely if
// it exceeds u16, but better to have a roughly correct // it exceeds u16, but better to have a roughly correct
@ -308,7 +304,7 @@ impl<'a> ShapedText<'a> {
let item = TextItem { let item = TextItem {
font, font,
size: glyph_size, size: self.size,
lang: self.lang, lang: self.lang,
region: self.region, region: self.region,
fill: fill.clone(), fill: fill.clone(),
@ -340,13 +336,12 @@ impl<'a> ShapedText<'a> {
let mut top = Abs::zero(); let mut top = Abs::zero();
let mut bottom = Abs::zero(); let mut bottom = Abs::zero();
let size = TextElem::size_in(self.styles);
let top_edge = TextElem::top_edge_in(self.styles); let top_edge = TextElem::top_edge_in(self.styles);
let bottom_edge = TextElem::bottom_edge_in(self.styles); let bottom_edge = TextElem::bottom_edge_in(self.styles);
// Expand top and bottom by reading the font's vertical metrics. // Expand top and bottom by reading the font's vertical metrics.
let mut expand = |font: &Font, bounds: TextEdgeBounds| { let mut expand = |font: &Font, bounds: TextEdgeBounds| {
let (t, b) = font.edges(top_edge, bottom_edge, size, bounds); let (t, b) = font.edges(top_edge, bottom_edge, self.size, bounds);
top.set_max(t); top.set_max(t);
bottom.set_max(b); bottom.set_max(b);
}; };
@ -393,16 +388,18 @@ impl<'a> ShapedText<'a> {
pub fn stretchability(&self) -> Abs { pub fn stretchability(&self) -> Abs {
self.glyphs self.glyphs
.iter() .iter()
.map(|g| (g.stretchability().0 + g.stretchability().1).at(g.size)) .map(|g| g.stretchability().0 + g.stretchability().1)
.sum() .sum::<Em>()
.at(self.size)
} }
/// The shrinkability of the text /// The shrinkability of the text
pub fn shrinkability(&self) -> Abs { pub fn shrinkability(&self) -> Abs {
self.glyphs self.glyphs
.iter() .iter()
.map(|g| (g.shrinkability().0 + g.shrinkability().1).at(g.size)) .map(|g| g.shrinkability().0 + g.shrinkability().1)
.sum() .sum::<Em>()
.at(self.size)
} }
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
@ -421,8 +418,9 @@ impl<'a> ShapedText<'a> {
lang: self.lang, lang: self.lang,
region: self.region, region: self.region,
styles: self.styles, styles: self.styles,
size: self.size,
variant: self.variant, variant: self.variant,
width: glyphs_width(glyphs), width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
glyphs: Cow::Borrowed(glyphs), glyphs: Cow::Borrowed(glyphs),
} }
} else { } else {
@ -486,15 +484,13 @@ impl<'a> ShapedText<'a> {
// that subtracting either of the endpoints by self.base doesn't // that subtracting either of the endpoints by self.base doesn't
// underflow. See <https://github.com/typst/typst/issues/2283>. // underflow. See <https://github.com/typst/typst/issues/2283>.
.unwrap_or_else(|| self.base..self.base); .unwrap_or_else(|| self.base..self.base);
let size = TextElem::size_in(self.styles); self.width += x_advance.at(self.size);
self.width += x_advance.at(size);
let glyph = ShapedGlyph { let glyph = ShapedGlyph {
font, font,
glyph_id: glyph_id.0, glyph_id: glyph_id.0,
x_advance, x_advance,
x_offset: Em::zero(), x_offset: Em::zero(),
y_offset: Em::zero(), y_offset: Em::zero(),
size,
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range, range,
safe_to_break: true, safe_to_break: true,
@ -670,7 +666,6 @@ fn shape<'a>(
region: Option<Region>, region: Option<Region>,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
let size = TextElem::size_in(styles); let size = TextElem::size_in(styles);
let shift_settings = TextElem::shift_settings_in(styles);
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
engine, engine,
size, size,
@ -681,7 +676,6 @@ fn shape<'a>(
features: features(styles), features: features(styles),
fallback: TextElem::fallback_in(styles), fallback: TextElem::fallback_in(styles),
dir, dir,
shift_settings,
}; };
if !text.is_empty() { if !text.is_empty() {
@ -704,17 +698,12 @@ fn shape<'a>(
region, region,
styles, styles,
variant: ctx.variant, variant: ctx.variant,
width: glyphs_width(&ctx.glyphs), size,
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
glyphs: Cow::Owned(ctx.glyphs), glyphs: Cow::Owned(ctx.glyphs),
} }
} }
/// Computes the width of a run of glyphs relative to the font size, accounting
/// for their individual scaling factors and other font metrics.
fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs {
glyphs.iter().map(|g| g.x_advance.at(g.size)).sum()
}
/// Holds shaping results and metadata common to all shaped segments. /// Holds shaping results and metadata common to all shaped segments.
struct ShapingContext<'a, 'v> { struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>, engine: &'a Engine<'v>,
@ -726,7 +715,6 @@ struct ShapingContext<'a, 'v> {
features: Vec<rustybuzz::Feature>, features: Vec<rustybuzz::Feature>,
fallback: bool, fallback: bool,
dir: Dir, dir: Dir,
shift_settings: Option<ShiftSettings>,
} }
/// Shape text with font fallback using the `families` iterator. /// Shape text with font fallback using the `families` iterator.
@ -801,18 +789,6 @@ fn shape_segment<'a>(
// text extraction. // text extraction.
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES); buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
let (script_shift, script_compensation, scale, shift_feature) = ctx
.shift_settings
.map_or((Em::zero(), Em::zero(), Em::one(), None), |settings| {
determine_shift(text, &font, settings)
});
let has_shift_feature = shift_feature.is_some();
if let Some(feat) = shift_feature {
// Temporarily push the feature.
ctx.features.push(feat)
}
// Prepare the shape plan. This plan depends on direction, script, language, // Prepare the shape plan. This plan depends on direction, script, language,
// and features, but is independent from the text and can thus be memoized. // and features, but is independent from the text and can thus be memoized.
let plan = create_shape_plan( let plan = create_shape_plan(
@ -823,10 +799,6 @@ fn shape_segment<'a>(
&ctx.features, &ctx.features,
); );
if has_shift_feature {
ctx.features.pop();
}
// Shape! // Shape!
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
let infos = buffer.glyph_infos(); let infos = buffer.glyph_infos();
@ -897,9 +869,8 @@ fn shape_segment<'a>(
glyph_id: info.glyph_id as u16, glyph_id: info.glyph_id as u16,
// TODO: Don't ignore y_advance. // TODO: Don't ignore y_advance.
x_advance, x_advance,
x_offset: font.to_em(pos[i].x_offset) + script_compensation, x_offset: font.to_em(pos[i].x_offset),
y_offset: font.to_em(pos[i].y_offset) + script_shift, y_offset: font.to_em(pos[i].y_offset),
size: scale.at(ctx.size),
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range: start..end, range: start..end,
safe_to_break: !info.unsafe_to_break(), safe_to_break: !info.unsafe_to_break(),
@ -961,64 +932,6 @@ fn shape_segment<'a>(
ctx.used.pop(); ctx.used.pop();
} }
/// Returns a `(script_shift, script_compensation, scale, feature)` quadruplet
/// describing how to produce scripts.
///
/// Those values determine how the rendered text should be transformed to
/// display sub-/super-scripts. If the OpenType feature can be used, the
/// rendered text should not be transformed in any way, and so those values are
/// neutral (`(0, 0, 1, None)`). If scripts should be synthesized, those values
/// determine how to transform the rendered text to display scripts as expected.
fn determine_shift(
text: &str,
font: &Font,
settings: ShiftSettings,
) -> (Em, Em, Em, Option<Feature>) {
settings
.typographic
.then(|| {
// If typographic scripts are enabled (i.e., we want to use the
// OpenType feature instead of synthesizing if possible), we add
// "subs"/"sups" to the feature list if supported by the font.
// In case of a problem, we just early exit
let gsub = font.rusty().tables().gsub?;
let subtable_index =
gsub.features.find(settings.kind.feature())?.lookup_indices.get(0)?;
let coverage = gsub
.lookups
.get(subtable_index)?
.subtables
.get::<SubstitutionSubtable>(0)?
.coverage();
text.chars()
.all(|c| {
font.rusty().glyph_index(c).is_some_and(|i| coverage.contains(i))
})
.then(|| {
// If we can use the OpenType feature, we can keep the text
// as is.
(
Em::zero(),
Em::zero(),
Em::one(),
Some(Feature::new(settings.kind.feature(), 1, ..)),
)
})
})
// Reunite the cases where `typographic` is `false` or where using the
// OpenType feature would not work.
.flatten()
.unwrap_or_else(|| {
let script_metrics = settings.kind.read_metrics(font.metrics());
(
settings.shift.unwrap_or(script_metrics.vertical_offset),
script_metrics.horizontal_offset,
settings.size.unwrap_or(script_metrics.height),
None,
)
})
}
/// Create a shape plan. /// Create a shape plan.
#[comemo::memoize] #[comemo::memoize]
pub fn create_shape_plan( pub fn create_shape_plan(
@ -1050,7 +963,6 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
x_advance, x_advance,
x_offset: Em::zero(), x_offset: Em::zero(),
y_offset: Em::zero(), y_offset: Em::zero(),
size: ctx.size,
adjustability: Adjustability::default(), adjustability: Adjustability::default(),
range: start..end, range: start..end,
safe_to_break: true, safe_to_break: true,
@ -1073,8 +985,9 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
/// Apply tracking and spacing to the shaped glyphs. /// Apply tracking and spacing to the shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) { fn track_and_space(ctx: &mut ShapingContext) {
let tracking = Em::from_abs(TextElem::tracking_in(ctx.styles), ctx.size); let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size);
let spacing = TextElem::spacing_in(ctx.styles).map(|abs| Em::from_abs(abs, ctx.size)); let spacing =
TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size));
let mut glyphs = ctx.glyphs.iter_mut().peekable(); let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() { while let Some(glyph) = glyphs.next() {

View File

@ -215,7 +215,7 @@ impl MathFragment {
&glyph.item.font, &glyph.item.font,
GlyphId(glyph.item.glyphs[glyph_index].id), GlyphId(glyph.item.glyphs[glyph_index].id),
corner, corner,
Em::from_abs(height, glyph.item.size), Em::from_length(height, glyph.item.size),
) )
.unwrap_or_default() .unwrap_or_default()
.at(glyph.item.size) .at(glyph.item.size)
@ -767,8 +767,8 @@ fn assemble(
advance += ratio * (max_overlap - min_overlap); advance += ratio * (max_overlap - min_overlap);
} }
let (x, y) = match axis { let (x, y) = match axis {
Axis::X => (Em::from_abs(advance, base.item.size), Em::zero()), Axis::X => (Em::from_length(advance, base.item.size), Em::zero()),
Axis::Y => (Em::zero(), Em::from_abs(advance, base.item.size)), Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)),
}; };
glyphs.push(Glyph { glyphs.push(Glyph {
id: part.glyph_id.0, id: part.glyph_id.0,

View File

@ -94,7 +94,7 @@ impl Array {
} }
/// Iterate over references to the contained values. /// Iterate over references to the contained values.
pub fn iter(&self) -> std::slice::Iter<'_, Value> { pub fn iter(&self) -> std::slice::Iter<Value> {
self.0.iter() self.0.iter()
} }
@ -604,7 +604,7 @@ impl Array {
Ok(acc) Ok(acc)
} }
/// Calculates the product of all items (works for all types that can be /// Calculates the product all items (works for all types that can be
/// multiplied). /// multiplied).
#[func] #[func]
pub fn product( pub fn product(

View File

@ -207,9 +207,9 @@ pub fn sqrt(
/// ``` /// ```
#[func] #[func]
pub fn root( pub fn root(
/// The expression to take the root of. /// The expression to take the root of
radicand: f64, radicand: f64,
/// Which root of the radicand to take. /// Which root of the radicand to take
index: Spanned<i64>, index: Spanned<i64>,
) -> SourceResult<f64> { ) -> SourceResult<f64> {
if index.v == 0 { if index.v == 0 {
@ -317,7 +317,7 @@ pub fn asin(
/// ``` /// ```
#[func(title = "Arccosine")] #[func(title = "Arccosine")]
pub fn acos( pub fn acos(
/// The number whose arccosine to calculate. Must be between -1 and 1. /// The number whose arcsine to calculate. Must be between -1 and 1.
value: Spanned<Num>, value: Spanned<Num>,
) -> SourceResult<Angle> { ) -> SourceResult<Angle> {
let val = value.v.float(); let val = value.v.float();
@ -387,7 +387,7 @@ pub fn cosh(
value.cosh() value.cosh()
} }
/// Calculates the hyperbolic tangent of a hyperbolic angle. /// Calculates the hyperbolic tangent of an hyperbolic angle.
/// ///
/// ```example /// ```example
/// #calc.tanh(0) \ /// #calc.tanh(0) \

View File

@ -114,7 +114,7 @@ impl Dict {
} }
/// Iterate over pairs of references to the contained keys and values. /// Iterate over pairs of references to the contained keys and values.
pub fn iter(&self) -> indexmap::map::Iter<'_, Str, Value> { pub fn iter(&self) -> indexmap::map::Iter<Str, Value> {
self.0.iter() self.0.iter()
} }

View File

@ -1,8 +1,7 @@
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_utils::{PicoStr, ResolvedPicoStr}; use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::StrResult; use crate::foundations::{func, scope, ty, Repr, Str};
use crate::foundations::{bail, func, scope, ty, Repr, Str};
/// A label for an element. /// A label for an element.
/// ///
@ -28,8 +27,7 @@ use crate::foundations::{bail, func, scope, ty, Repr, Str};
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: You can create a label by enclosing /// This function also has dedicated syntax: You can create a label by enclosing
/// its name in angle brackets. This works both in markup and code. A label's /// its name in angle brackets. This works both in markup and code. A label's
/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. A label cannot /// name can contain letters, numbers, `_`, `-`, `:`, and `.`.
/// be empty.
/// ///
/// Note that there is a syntactical difference when using the dedicated syntax /// Note that there is a syntactical difference when using the dedicated syntax
/// for this function. In the code below, the `[<a>]` terminates the heading and /// for this function. In the code below, the `[<a>]` terminates the heading and
@ -52,11 +50,8 @@ pub struct Label(PicoStr);
impl Label { impl Label {
/// Creates a label from an interned string. /// Creates a label from an interned string.
/// pub fn new(name: PicoStr) -> Self {
/// Returns `None` if the given string is empty. Self(name)
pub fn new(name: PicoStr) -> Option<Self> {
const EMPTY: PicoStr = PicoStr::constant("");
(name != EMPTY).then_some(Self(name))
} }
/// Resolves the label to a string. /// Resolves the label to a string.
@ -75,14 +70,10 @@ impl Label {
/// Creates a label from a string. /// Creates a label from a string.
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The name of the label. Must not be empty. /// The name of the label.
name: Str, name: Str,
) -> StrResult<Label> { ) -> Label {
if name.is_empty() { Self(PicoStr::intern(name.as_str()))
bail!("label name must not be empty");
}
Ok(Self(PicoStr::intern(name.as_str())))
} }
} }

View File

@ -19,8 +19,11 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
/// ///
/// You can access definitions from the module using [field access /// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and /// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). /// include syntaxes]($scripting/#modules). Alternatively, it is possible to
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
/// ///
/// # Example
/// ```example /// ```example
/// <<< #import "utils.typ" /// <<< #import "utils.typ"
/// <<< #utils.add(2, 5) /// <<< #utils.add(2, 5)
@ -31,20 +34,6 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
/// >>> /// >>>
/// >>> #(-3) /// >>> #(-3)
/// ``` /// ```
///
/// You can check whether a definition is present in a module using the `{in}`
/// operator, with a string on the left-hand side. This can be useful to
/// [conditionally access]($category/foundations/std/#conditional-access)
/// definitions in a module.
///
/// ```example
/// #("table" in std) \
/// #("nope" in std)
/// ```
///
/// Alternatively, it is possible to convert a module to a dictionary, and
/// therefore access its contents dynamically, using the [dictionary
/// constructor]($dictionary/#constructor).
#[ty(cast)] #[ty(cast)]
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -558,7 +558,6 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
(Str(a), Dict(b)) => Some(b.contains(a)), (Str(a), Dict(b)) => Some(b.contains(a)),
(Str(a), Module(b)) => Some(b.scope().get(a).is_some()),
(a, Array(b)) => Some(b.contains(a.clone())), (a, Array(b)) => Some(b.contains(a.clone())),
_ => Option::None, _ => Option::None,

View File

@ -8,7 +8,7 @@ use serde::{Serialize, Serializer};
use typst_syntax::{is_ident, Span, Spanned}; use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128; use typst_utils::hash128;
use crate::diag::{bail, DeprecationSink, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _, PlainText, Repr as _,
@ -54,22 +54,18 @@ enum Repr {
/// A native symbol that has no named variant. /// A native symbol that has no named variant.
Single(char), Single(char),
/// A native symbol with multiple named variants. /// A native symbol with multiple named variants.
Complex(&'static [Variant<&'static str>]), Complex(&'static [(ModifierSet<&'static str>, char)]),
/// A symbol with multiple named variants, where some modifiers may have /// A symbol with multiple named variants, where some modifiers may have
/// been applied. Also used for symbols defined at runtime by the user with /// been applied. Also used for symbols defined at runtime by the user with
/// no modifier applied. /// no modifier applied.
Modified(Arc<(List, ModifierSet<EcoString>)>), Modified(Arc<(List, ModifierSet<EcoString>)>),
} }
/// A symbol variant, consisting of a set of modifiers, a character, and an
/// optional deprecation message.
type Variant<S> = (ModifierSet<S>, char, Option<S>);
/// A collection of symbols. /// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum List { enum List {
Static(&'static [Variant<&'static str>]), Static(&'static [(ModifierSet<&'static str>, char)]),
Runtime(Box<[Variant<EcoString>]>), Runtime(Box<[(ModifierSet<EcoString>, char)]>),
} }
impl Symbol { impl Symbol {
@ -80,14 +76,14 @@ impl Symbol {
/// Create a symbol with a static variant list. /// Create a symbol with a static variant list.
#[track_caller] #[track_caller]
pub const fn list(list: &'static [Variant<&'static str>]) -> Self { pub const fn list(list: &'static [(ModifierSet<&'static str>, char)]) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Complex(list)) Self(Repr::Complex(list))
} }
/// Create a symbol with a runtime variant list. /// Create a symbol with a runtime variant list.
#[track_caller] #[track_caller]
pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self { pub fn runtime(list: Box<[(ModifierSet<EcoString>, char)]>) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
} }
@ -97,11 +93,9 @@ impl Symbol {
match &self.0 { match &self.0 {
Repr::Single(c) => *c, Repr::Single(c) => *c,
Repr::Complex(_) => ModifierSet::<&'static str>::default() Repr::Complex(_) => ModifierSet::<&'static str>::default()
.best_match_in(self.variants().map(|(m, c, _)| (m, c))) .best_match_in(self.variants())
.unwrap(), .unwrap(),
Repr::Modified(arc) => { Repr::Modified(arc) => arc.1.best_match_in(self.variants()).unwrap(),
arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap()
}
} }
} }
@ -134,11 +128,7 @@ impl Symbol {
} }
/// Apply a modifier to the symbol. /// Apply a modifier to the symbol.
pub fn modified( pub fn modified(mut self, modifier: &str) -> StrResult<Self> {
mut self,
sink: impl DeprecationSink,
modifier: &str,
) -> StrResult<Self> {
if let Repr::Complex(list) = self.0 { if let Repr::Complex(list) = self.0 {
self.0 = self.0 =
Repr::Modified(Arc::new((List::Static(list), ModifierSet::default()))); Repr::Modified(Arc::new((List::Static(list), ModifierSet::default())));
@ -147,12 +137,7 @@ impl Symbol {
if let Repr::Modified(arc) = &mut self.0 { if let Repr::Modified(arc) = &mut self.0 {
let (list, modifiers) = Arc::make_mut(arc); let (list, modifiers) = Arc::make_mut(arc);
modifiers.insert_raw(modifier); modifiers.insert_raw(modifier);
if let Some(deprecation) = if modifiers.best_match_in(list.variants()).is_some() {
modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d)))
{
if let Some(message) = deprecation {
sink.emit(message)
}
return Ok(self); return Ok(self);
} }
} }
@ -161,7 +146,7 @@ impl Symbol {
} }
/// The characters that are covered by this symbol. /// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> { pub fn variants(&self) -> impl Iterator<Item = (ModifierSet<&str>, char)> {
match &self.0 { match &self.0 {
Repr::Single(c) => Variants::Single(Some(*c).into_iter()), Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
Repr::Complex(list) => Variants::Static(list.iter()), Repr::Complex(list) => Variants::Static(list.iter()),
@ -176,7 +161,7 @@ impl Symbol {
_ => ModifierSet::default(), _ => ModifierSet::default(),
}; };
self.variants() self.variants()
.flat_map(|(m, _, _)| m) .flat_map(|(m, _)| m)
.filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier)) .filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier))
.collect::<BTreeSet<_>>() .collect::<BTreeSet<_>>()
.into_iter() .into_iter()
@ -271,7 +256,7 @@ impl Symbol {
let list = variants let list = variants
.into_iter() .into_iter()
.map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1, None)) .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1))
.collect(); .collect();
Ok(Symbol::runtime(list)) Ok(Symbol::runtime(list))
} }
@ -331,17 +316,17 @@ impl crate::foundations::Repr for Symbol {
} }
fn repr_variants<'a>( fn repr_variants<'a>(
variants: impl Iterator<Item = Variant<&'a str>>, variants: impl Iterator<Item = (ModifierSet<&'a str>, char)>,
applied_modifiers: ModifierSet<&str>, applied_modifiers: ModifierSet<&str>,
) -> String { ) -> String {
crate::foundations::repr::pretty_array_like( crate::foundations::repr::pretty_array_like(
&variants &variants
.filter(|(modifiers, _, _)| { .filter(|(modifiers, _)| {
// Only keep variants that can still be accessed, i.e., variants // Only keep variants that can still be accessed, i.e., variants
// that contain all applied modifiers. // that contain all applied modifiers.
applied_modifiers.iter().all(|am| modifiers.contains(am)) applied_modifiers.iter().all(|am| modifiers.contains(am))
}) })
.map(|(modifiers, c, _)| { .map(|(modifiers, c)| {
let trimmed_modifiers = let trimmed_modifiers =
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
if trimmed_modifiers.clone().all(|m| m.is_empty()) { if trimmed_modifiers.clone().all(|m| m.is_empty()) {
@ -394,20 +379,18 @@ cast! {
/// Iterator over variants. /// Iterator over variants.
enum Variants<'a> { enum Variants<'a> {
Single(std::option::IntoIter<char>), Single(std::option::IntoIter<char>),
Static(std::slice::Iter<'static, Variant<&'static str>>), Static(std::slice::Iter<'static, (ModifierSet<&'static str>, char)>),
Runtime(std::slice::Iter<'a, Variant<EcoString>>), Runtime(std::slice::Iter<'a, (ModifierSet<EcoString>, char)>),
} }
impl<'a> Iterator for Variants<'a> { impl<'a> Iterator for Variants<'a> {
type Item = Variant<&'a str>; type Item = (ModifierSet<&'a str>, char);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)), Self::Single(iter) => Some((ModifierSet::default(), iter.next()?)),
Self::Static(list) => list.next().copied(), Self::Static(list) => list.next().copied(),
Self::Runtime(list) => { Self::Runtime(list) => list.next().map(|(m, c)| (m.as_deref(), *c)),
list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref()))
}
} }
} }
} }

View File

@ -157,9 +157,7 @@ impl Value {
/// Try to access a field on the value. /// Try to access a field on the value.
pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> { pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> {
match self { match self {
Self::Symbol(symbol) => { Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
symbol.clone().modified(sink, field).map(Self::Symbol)
}
Self::Version(version) => version.component(field).map(Self::Int), Self::Version(version) => version.component(field).map(Self::Int),
Self::Dict(dict) => dict.get(field).cloned(), Self::Dict(dict) => dict.get(field).cloned(),
Self::Content(content) => content.field_by_name(field), Self::Content(content) => content.field_by_name(field),

View File

@ -7,7 +7,7 @@ use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str}; use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag}; use crate::introspection::{Introspector, Tag};
use crate::layout::{Abs, Frame}; use crate::layout::Frame;
use crate::model::DocumentInfo; use crate::model::DocumentInfo;
/// An HTML document. /// An HTML document.
@ -30,8 +30,8 @@ pub enum HtmlNode {
Text(EcoString, Span), Text(EcoString, Span),
/// Another element. /// Another element.
Element(HtmlElement), Element(HtmlElement),
/// Layouted content that will be embedded into HTML as an SVG. /// A frame that will be displayed as an embedded SVG.
Frame(HtmlFrame), Frame(Frame),
} }
impl HtmlNode { impl HtmlNode {
@ -263,17 +263,6 @@ cast! {
v: Str => Self::intern(&v)?, v: Str => Self::intern(&v)?,
} }
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
}
/// Defines syntactical properties of HTML tags, attributes, and text. /// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets { pub mod charsets {
/// Check whether a character is in a tag name. /// Check whether a character is in a tag name.

View File

@ -446,7 +446,7 @@ impl IntrospectorBuilder {
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame( HtmlNode::Frame(frame) => self.discover_in_frame(
sink, sink,
&frame.inner, frame,
NonZeroUsize::ONE, NonZeroUsize::ONE,
Transform::identity(), Transform::identity(),
), ),

View File

@ -497,8 +497,7 @@ mod callbacks {
macro_rules! callback { macro_rules! callback {
($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => { ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => {
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct $name { pub struct $name {
captured: Content, captured: Content,
f: fn(&Content, $($param_ty),*) -> $ret, f: fn(&Content, $($param_ty),*) -> $ret,
@ -536,19 +535,6 @@ mod callbacks {
(self.f)(&self.captured, $($param),*) (self.f)(&self.captured, $($param),*)
} }
} }
impl PartialEq for $name {
fn eq(&self, other: &Self) -> bool {
// Comparing function pointers is problematic. Since for
// each type of content, there is typically just one
// callback, we skip it. It barely matters anyway since
// getting into a comparison codepath for inline & block
// elements containing callback bodies is close to
// impossible (as these are generally generated in show
// rules).
self.captured.eq(&other.captured)
}
}
}; };
} }

View File

@ -6,7 +6,7 @@ use ecow::EcoString;
use typst_utils::{Numeric, Scalar}; use typst_utils::{Numeric, Scalar};
use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value}; use crate::foundations::{cast, repr, Repr, Resolve, StyleChain, Value};
use crate::layout::{Abs, Length}; use crate::layout::Abs;
use crate::text::TextElem; use crate::text::TextElem;
/// A length that is relative to the font size. /// A length that is relative to the font size.
@ -26,18 +26,18 @@ impl Em {
Self(Scalar::ONE) Self(Scalar::ONE)
} }
/// Creates a font-relative length. /// Create a font-relative length.
pub const fn new(em: f64) -> Self { pub const fn new(em: f64) -> Self {
Self(Scalar::new(em)) Self(Scalar::new(em))
} }
/// Creates an em length from font units at the given units per em. /// Create an em length from font units at the given units per em.
pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self { pub fn from_units(units: impl Into<f64>, units_per_em: f64) -> Self {
Self(Scalar::new(units.into() / units_per_em)) Self(Scalar::new(units.into() / units_per_em))
} }
/// Creates an em length from an absolute length at the given font size. /// Create an em length from a length at the given font size.
pub fn from_abs(length: Abs, font_size: Abs) -> Self { pub fn from_length(length: Abs, font_size: Abs) -> Self {
let result = length / font_size; let result = length / font_size;
if result.is_finite() { if result.is_finite() {
Self(Scalar::new(result)) Self(Scalar::new(result))
@ -46,11 +46,6 @@ impl Em {
} }
} }
/// Creates an em length from a length at the given font size.
pub fn from_length(length: Length, font_size: Abs) -> Em {
length.em + Self::from_abs(length.abs, font_size)
}
/// The number of em units. /// The number of em units.
pub const fn get(self) -> f64 { pub const fn get(self) -> f64 {
(self.0).get() (self.0).get()
@ -61,7 +56,7 @@ impl Em {
Self::new(self.get().abs()) Self::new(self.get().abs())
} }
/// Converts to an absolute length at the given font size. /// Convert to an absolute length at the given font size.
pub fn at(self, font_size: Abs) -> Abs { pub fn at(self, font_size: Abs) -> Abs {
let resolved = font_size * self.get(); let resolved = font_size * self.get();
if resolved.is_finite() { if resolved.is_finite() {

View File

@ -47,12 +47,12 @@ impl Fragment {
} }
/// Iterate over the contained frames. /// Iterate over the contained frames.
pub fn iter(&self) -> std::slice::Iter<'_, Frame> { pub fn iter(&self) -> std::slice::Iter<Frame> {
self.0.iter() self.0.iter()
} }
/// Iterate over the contained frames. /// Iterate over the contained frames.
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Frame> { pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> {
self.0.iter_mut() self.0.iter_mut()
} }
} }

View File

@ -321,11 +321,7 @@ impl Bibliography {
for d in data.iter() { for d in data.iter() {
let library = decode_library(d)?; let library = decode_library(d)?;
for entry in library { for entry in library {
let label = Label::new(PicoStr::intern(entry.key())) match map.entry(Label::new(PicoStr::intern(entry.key()))) {
.ok_or("bibliography contains entry with empty key")
.at(d.source.span)?;
match map.entry(label) {
indexmap::map::Entry::Vacant(vacant) => { indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry); vacant.insert(entry);
} }

View File

@ -2,10 +2,7 @@ use smallvec::smallvec;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Abs, Corners, Length, Rel, Sides}; use crate::layout::{Abs, Corners, Length, Rel, Sides};
use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::visualize::{Color, FixedStroke, Paint, Stroke}; use crate::visualize::{Color, FixedStroke, Paint, Stroke};
@ -84,16 +81,6 @@ pub struct UnderlineElem {
impl Show for Packed<UnderlineElem> { impl Show for Packed<UnderlineElem> {
#[typst_macros::time(name = "underline", span = self.span())] #[typst_macros::time(name = "underline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
// Note: In modern HTML, `<u>` is not the underline element, but
// rather an "Unarticulated Annotation" element (see HTML spec
// 4.5.22). Using `text-decoration` instead is recommended by MDN.
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: underline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Underline { line: DecoLine::Underline {
stroke: self.stroke(styles).unwrap_or_default(), stroke: self.stroke(styles).unwrap_or_default(),
@ -186,13 +173,6 @@ pub struct OverlineElem {
impl Show for Packed<OverlineElem> { impl Show for Packed<OverlineElem> {
#[typst_macros::time(name = "overline", span = self.span())] #[typst_macros::time(name = "overline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: overline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Overline { line: DecoLine::Overline {
stroke: self.stroke(styles).unwrap_or_default(), stroke: self.stroke(styles).unwrap_or_default(),
@ -270,10 +250,6 @@ pub struct StrikeElem {
impl Show for Packed<StrikeElem> { impl Show for Packed<StrikeElem> {
#[typst_macros::time(name = "strike", span = self.span())] #[typst_macros::time(name = "strike", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
// Note that we do not support evade option for strikethrough. // Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough { line: DecoLine::Strikethrough {
@ -369,12 +345,6 @@ pub struct HighlightElem {
impl Show for Packed<HighlightElem> { impl Show for Packed<HighlightElem> {
#[typst_macros::time(name = "highlight", span = self.span())] #[typst_macros::time(name = "highlight", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::mark)
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Highlight { line: DecoLine::Highlight {
fill: self.fill(styles), fill: self.fill(styles),

View File

@ -228,10 +228,6 @@ pub struct FontMetrics {
pub underline: LineMetrics, pub underline: LineMetrics,
/// Recommended metrics for an overline. /// Recommended metrics for an overline.
pub overline: LineMetrics, pub overline: LineMetrics,
/// Metrics for subscripts, if provided by the font.
pub subscript: Option<ScriptMetrics>,
/// Metrics for superscripts, if provided by the font.
pub superscript: Option<ScriptMetrics>,
} }
impl FontMetrics { impl FontMetrics {
@ -244,7 +240,6 @@ impl FontMetrics {
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em); let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em); let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender())); let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
let strikeout = ttf.strikeout_metrics(); let strikeout = ttf.strikeout_metrics();
let underline = ttf.underline_metrics(); let underline = ttf.underline_metrics();
@ -267,20 +262,6 @@ impl FontMetrics {
thickness: underline.thickness, thickness: underline.thickness,
}; };
let subscript = ttf.subscript_metrics().map(|metrics| ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: -to_em(metrics.y_offset),
});
let superscript = ttf.superscript_metrics().map(|metrics| ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: to_em(metrics.y_offset),
});
Self { Self {
units_per_em, units_per_em,
ascender, ascender,
@ -290,8 +271,6 @@ impl FontMetrics {
strikethrough, strikethrough,
underline, underline,
overline, overline,
superscript,
subscript,
} }
} }
@ -317,24 +296,6 @@ pub struct LineMetrics {
pub thickness: Em, pub thickness: Em,
} }
/// Metrics for subscripts or superscripts.
#[derive(Debug, Copy, Clone)]
pub struct ScriptMetrics {
/// The width of those scripts, relative to the outer font size.
pub width: Em,
/// The height of those scripts, relative to the outer font size.
pub height: Em,
/// The horizontal (to the right) offset of those scripts, relative to the
/// outer font size.
///
/// This is used for italic correction.
pub horizontal_offset: Em,
/// The vertical (to the top) offset of those scripts, relative to the outer font size.
///
/// For superscripts, this is positive. For subscripts, this is negative.
pub vertical_offset: Em,
}
/// Identifies a vertical metric of a font. /// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VerticalFontMetric { pub enum VerticalFontMetric {

View File

@ -755,12 +755,6 @@ pub struct TextElem {
#[internal] #[internal]
#[ghost] #[ghost]
pub smallcaps: Option<Smallcaps>, pub smallcaps: Option<Smallcaps>,
/// The configuration for superscripts or subscripts, if one of them is
/// enabled.
#[internal]
#[ghost]
pub shift_settings: Option<ShiftSettings>,
} }
impl TextElem { impl TextElem {
@ -936,7 +930,7 @@ cast! {
} }
/// Resolve a prioritized iterator over the font families. /// Resolve a prioritized iterator over the font families.
pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily> + Clone { pub fn families(styles: StyleChain) -> impl Iterator<Item = &FontFamily> + Clone {
let fallbacks = singleton!(Vec<FontFamily>, { let fallbacks = singleton!(Vec<FontFamily>, {
[ [
"libertinus serif", "libertinus serif",

View File

@ -1,13 +1,14 @@
use ecow::EcoString;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, elem, Content, NativeElement, Packed, SequenceElem, Show, StyleChain, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::layout::{Em, Length}; use crate::layout::{Em, Length};
use crate::text::{FontMetrics, TextElem, TextSize}; use crate::text::{variant, SpaceElem, TextElem, TextSize};
use ttf_parser::Tag; use crate::World;
use typst_library::text::ScriptMetrics;
/// Renders text in subscript. /// Renders text in subscript.
/// ///
@ -19,16 +20,11 @@ use typst_library::text::ScriptMetrics;
/// ``` /// ```
#[elem(title = "Subscript", Show)] #[elem(title = "Subscript", Show)]
pub struct SubElem { pub struct SubElem {
/// Whether to create artificial subscripts by lowering and scaling down /// Whether to prefer the dedicated subscript characters of the font.
/// regular glyphs.
/// ///
/// Ideally, subscripts glyphs are provided by the font (using the `subs` /// If this is enabled, Typst first tries to transform the text to subscript
/// OpenType feature). Otherwise, Typst is able to synthesize subscripts. /// codepoints. If that fails, it falls back to rendering lowered and shrunk
/// /// normal letters.
/// When this is set to `{false}`, synthesized glyphs will be used
/// regardless of whether the font provides dedicated subscript glyphs. When
/// `{true}`, synthesized glyphs may still be used in case the font does not
/// provide the necessary subscript glyphs.
/// ///
/// ```example /// ```example
/// N#sub(typographic: true)[1] /// N#sub(typographic: true)[1]
@ -37,27 +33,17 @@ pub struct SubElem {
#[default(true)] #[default(true)]
pub typographic: bool, pub typographic: bool,
/// The downward baseline shift for synthesized subscripts. /// The baseline shift for synthetic subscripts. Does not apply if
/// /// `typographic` is true and the font has subscript codepoints for the
/// This only applies to synthesized subscripts. In other words, this has no /// given `body`.
/// effect if `typographic` is `{true}` and the font provides the necessary #[default(Em::new(0.2).into())]
/// subscript glyphs. pub baseline: Length,
///
/// If set to `{auto}`, the baseline is shifted according to the metrics
/// provided by the font, with a fallback to `{0.2em}` in case the font does
/// not define the necessary metrics.
pub baseline: Smart<Length>,
/// The font size for synthesized subscripts. /// The font size for synthetic subscripts. Does not apply if
/// /// `typographic` is true and the font has subscript codepoints for the
/// This only applies to synthesized subscripts. In other words, this has no /// given `body`.
/// effect if `typographic` is `{true}` and the font provides the necessary #[default(TextSize(Em::new(0.6).into()))]
/// subscript glyphs. pub size: TextSize,
///
/// If set to `{auto}`, the size is scaled according to the metrics provided
/// by the font, with a fallback to `{0.6em}` in case the font does not
/// define the necessary metrics.
pub size: Smart<TextSize>,
/// The text to display in subscript. /// The text to display in subscript.
#[required] #[required]
@ -66,7 +52,7 @@ pub struct SubElem {
impl Show for Packed<SubElem> { impl Show for Packed<SubElem> {
#[typst_macros::time(name = "sub", span = self.span())] #[typst_macros::time(name = "sub", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone(); let body = self.body.clone();
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
@ -76,14 +62,17 @@ impl Show for Packed<SubElem> {
.spanned(self.span())); .spanned(self.span()));
} }
show_script( if self.typographic(styles) {
styles, if let Some(text) = convert_script(&body, true) {
body, if is_shapable(engine, &text, styles) {
self.typographic(styles), return Ok(TextElem::packed(text));
self.baseline(styles), }
self.size(styles), }
ScriptKind::Sub, };
)
Ok(body
.styled(TextElem::set_baseline(self.baseline(styles)))
.styled(TextElem::set_size(self.size(styles))))
} }
} }
@ -97,16 +86,11 @@ impl Show for Packed<SubElem> {
/// ``` /// ```
#[elem(title = "Superscript", Show)] #[elem(title = "Superscript", Show)]
pub struct SuperElem { pub struct SuperElem {
/// Whether to create artificial superscripts by raising and scaling down /// Whether to prefer the dedicated superscript characters of the font.
/// regular glyphs.
/// ///
/// Ideally, superscripts glyphs are provided by the font (using the `sups` /// If this is enabled, Typst first tries to transform the text to
/// OpenType feature). Otherwise, Typst is able to synthesize superscripts. /// superscript codepoints. If that fails, it falls back to rendering
/// /// raised and shrunk normal letters.
/// When this is set to `{false}`, synthesized glyphs will be used
/// regardless of whether the font provides dedicated superscript glyphs.
/// When `{true}`, synthesized glyphs may still be used in case the font
/// does not provide the necessary superscript glyphs.
/// ///
/// ```example /// ```example
/// N#super(typographic: true)[1] /// N#super(typographic: true)[1]
@ -115,31 +99,17 @@ pub struct SuperElem {
#[default(true)] #[default(true)]
pub typographic: bool, pub typographic: bool,
/// The downward baseline shift for synthesized superscripts. /// The baseline shift for synthetic superscripts. Does not apply if
/// /// `typographic` is true and the font has superscript codepoints for the
/// This only applies to synthesized superscripts. In other words, this has /// given `body`.
/// no effect if `typographic` is `{true}` and the font provides the #[default(Em::new(-0.5).into())]
/// necessary superscript glyphs. pub baseline: Length,
///
/// If set to `{auto}`, the baseline is shifted according to the metrics
/// provided by the font, with a fallback to `{-0.5em}` in case the font
/// does not define the necessary metrics.
///
/// Note that, since the baseline shift is applied downward, you will need
/// to provide a negative value for the content to appear as raised above
/// the normal baseline.
pub baseline: Smart<Length>,
/// The font size for synthesized superscripts. /// The font size for synthetic superscripts. Does not apply if
/// /// `typographic` is true and the font has superscript codepoints for the
/// This only applies to synthesized superscripts. In other words, this has /// given `body`.
/// no effect if `typographic` is `{true}` and the font provides the #[default(TextSize(Em::new(0.6).into()))]
/// necessary superscript glyphs. pub size: TextSize,
///
/// If set to `{auto}`, the size is scaled according to the metrics provided
/// by the font, with a fallback to `{0.6em}` in case the font does not
/// define the necessary metrics.
pub size: Smart<TextSize>,
/// The text to display in superscript. /// The text to display in superscript.
#[required] #[required]
@ -148,7 +118,7 @@ pub struct SuperElem {
impl Show for Packed<SuperElem> { impl Show for Packed<SuperElem> {
#[typst_macros::time(name = "super", span = self.span())] #[typst_macros::time(name = "super", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone(); let body = self.body.clone();
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
@ -158,102 +128,104 @@ impl Show for Packed<SuperElem> {
.spanned(self.span())); .spanned(self.span()));
} }
show_script( if self.typographic(styles) {
styles, if let Some(text) = convert_script(&body, false) {
body, if is_shapable(engine, &text, styles) {
self.typographic(styles), return Ok(TextElem::packed(text));
self.baseline(styles), }
self.size(styles), }
ScriptKind::Super, };
)
Ok(body
.styled(TextElem::set_baseline(self.baseline(styles)))
.styled(TextElem::set_size(self.size(styles))))
} }
} }
fn show_script( /// Find and transform the text contained in `content` to the given script kind
styles: StyleChain, /// if and only if it only consists of `Text`, `Space`, and `Empty` leaves.
body: Content, fn convert_script(content: &Content, sub: bool) -> Option<EcoString> {
typographic: bool, if content.is::<SpaceElem>() {
baseline: Smart<Length>, Some(' '.into())
size: Smart<TextSize>, } else if let Some(elem) = content.to_packed::<TextElem>() {
kind: ScriptKind, if sub {
) -> SourceResult<Content> { elem.text.chars().map(to_subscript_codepoint).collect()
let font_size = TextElem::size_in(styles); } else {
Ok(body.styled(TextElem::set_shift_settings(Some(ShiftSettings { elem.text.chars().map(to_superscript_codepoint).collect()
typographic, }
shift: baseline.map(|l| -Em::from_length(l, font_size)), } else if let Some(sequence) = content.to_packed::<SequenceElem>() {
size: size.map(|t| Em::from_length(t.0, font_size)), sequence
kind, .children
})))) .iter()
.map(|item| convert_script(item, sub))
.collect()
} else {
None
}
} }
/// Configuration values for sub- or superscript text. /// Checks whether the first retrievable family contains all code points of the
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] /// given string.
pub struct ShiftSettings { fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
/// Whether the OpenType feature should be used if possible. let world = engine.world;
pub typographic: bool, for family in TextElem::font_in(styles) {
/// The baseline shift of the script, relative to the outer text size. if let Some(font) = world
/// .book()
/// For superscripts, this is positive. For subscripts, this is negative. A .select(family.as_str(), variant(styles))
/// value of [`Smart::Auto`] indicates that the value should be obtained .and_then(|id| world.font(id))
/// from font metrics. {
pub shift: Smart<Em>, let covers = family.covers();
/// The size of the script, relative to the outer text size. return text.chars().all(|c| {
/// covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
/// A value of [`Smart::Auto`] indicates that the value should be obtained && font.ttf().glyph_index(c).is_some()
/// from font metrics. });
pub size: Smart<Em>,
/// The kind of script (either a subscript, or a superscript).
///
/// This is used to know which OpenType table to use to resolve
/// [`Smart::Auto`] values.
pub kind: ScriptKind,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ScriptKind {
Sub,
Super,
}
impl ScriptKind {
/// Returns the default metrics for this script kind.
///
/// This can be used as a last resort if neither the user nor the font
/// provided those metrics.
pub fn default_metrics(self) -> &'static ScriptMetrics {
match self {
Self::Sub => &DEFAULT_SUBSCRIPT_METRICS,
Self::Super => &DEFAULT_SUPERSCRIPT_METRICS,
} }
} }
/// Reads the script metrics from the font table for to this script kind. false
pub fn read_metrics(self, font_metrics: &FontMetrics) -> &ScriptMetrics { }
match self {
Self::Sub => font_metrics.subscript.as_ref(),
Self::Super => font_metrics.superscript.as_ref(),
}
.unwrap_or(self.default_metrics())
}
/// The corresponding OpenType feature. /// Convert a character to its corresponding Unicode superscript.
pub const fn feature(self) -> Tag { fn to_superscript_codepoint(c: char) -> Option<char> {
match self { match c {
Self::Sub => Tag::from_bytes(b"subs"), '1' => Some('¹'),
Self::Super => Tag::from_bytes(b"sups"), '2' => Some('²'),
} '3' => Some('³'),
'0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32),
'+' => Some('⁺'),
'' => Some('⁻'),
'=' => Some('⁼'),
'(' => Some('⁽'),
')' => Some('⁾'),
'n' => Some('ⁿ'),
'i' => Some('ⁱ'),
' ' => Some(' '),
_ => None,
} }
} }
static DEFAULT_SUBSCRIPT_METRICS: ScriptMetrics = ScriptMetrics {
width: Em::new(0.6),
height: Em::new(0.6),
horizontal_offset: Em::zero(),
vertical_offset: Em::new(-0.2),
};
static DEFAULT_SUPERSCRIPT_METRICS: ScriptMetrics = ScriptMetrics { /// Convert a character to its corresponding Unicode subscript.
width: Em::new(0.6), fn to_subscript_codepoint(c: char) -> Option<char> {
height: Em::new(0.6), match c {
horizontal_offset: Em::zero(), '0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32),
vertical_offset: Em::new(0.5), '+' => Some('₊'),
}; '' => Some('₋'),
'=' => Some('₌'),
'(' => Some('₍'),
')' => Some('₎'),
'a' => Some('ₐ'),
'e' => Some('ₑ'),
'o' => Some('ₒ'),
'x' => Some('ₓ'),
'h' => Some('ₕ'),
'k' => Some('ₖ'),
'l' => Some('ₗ'),
'm' => Some('ₘ'),
'n' => Some('ₙ'),
'p' => Some('ₚ'),
's' => Some('ₛ'),
't' => Some('ₜ'),
' ' => Some(' '),
_ => None,
}
}

View File

@ -1285,17 +1285,24 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
/// Sample the stops at a given position. /// Sample the stops at a given position.
fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color { fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color {
let t = t.clamp(0.0, 1.0); let t = t.clamp(0.0, 1.0);
let mut j = stops.partition_point(|(_, ratio)| ratio.get() < t); let mut low = 0;
let mut high = stops.len();
if j == 0 { while low < high {
while stops.get(j + 1).is_some_and(|(_, r)| r.is_zero()) { let mid = (low + high) / 2;
j += 1; if stops[mid].1.get() < t {
low = mid + 1;
} else {
high = mid;
} }
return stops[j].0;
} }
let (col_0, pos_0) = stops[j - 1]; if low == 0 {
let (col_1, pos_1) = stops[j]; low = 1;
}
let (col_0, pos_0) = stops[low - 1];
let (col_1, pos_1) = stops[low];
let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get()); let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get());
Color::mix_iter( Color::mix_iter(

View File

@ -172,7 +172,7 @@ pub enum PdfStandard {
/// PDF/A-2u. /// PDF/A-2u.
#[serde(rename = "a-2u")] #[serde(rename = "a-2u")]
A_2u, A_2u,
/// PDF/A-3b. /// PDF/A-3u.
#[serde(rename = "a-3b")] #[serde(rename = "a-3b")]
A_3b, A_3b,
/// PDF/A-3u. /// PDF/A-3u.

View File

@ -22,6 +22,8 @@ pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata {
.keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect()) .keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect())
.authors(gc.document.info.author.iter().map(EcoString::to_string).collect()); .authors(gc.document.info.author.iter().map(EcoString::to_string).collect());
let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l);
if let Some(lang) = lang { if let Some(lang) = lang {
metadata = metadata.language(lang.as_str().to_string()); metadata = metadata.language(lang.as_str().to_string());
} }

View File

@ -18,7 +18,7 @@ use typst_library::foundations::{
SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem,
Synthesize, Transformation, Synthesize, Transformation,
}; };
use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::html::{tag, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
@ -237,9 +237,9 @@ fn visit<'a>(
return Ok(()); return Ok(());
} }
// Transformations for content based on the realization kind. Needs // Transformations for math content based on the realization kind. Needs
// to happen before show rules. // to happen before show rules.
if visit_kind_rules(s, content, styles)? { if visit_math_rules(s, content, styles)? {
return Ok(()); return Ok(());
} }
@ -280,8 +280,9 @@ fn visit<'a>(
Ok(()) Ok(())
} }
// Handles transformations based on the realization kind. // Handles special cases for math in normal content and nested equations in
fn visit_kind_rules<'a>( // math.
fn visit_math_rules<'a>(
s: &mut State<'a, '_, '_, '_>, s: &mut State<'a, '_, '_, '_>,
content: &'a Content, content: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
@ -334,13 +335,6 @@ fn visit_kind_rules<'a>(
} }
} }
if !s.kind.is_html() {
if let Some(elem) = content.to_packed::<FrameElem>() {
visit(s, &elem.body, styles)?;
return Ok(true);
}
}
Ok(false) Ok(false)
} }

View File

@ -724,8 +724,6 @@ node! {
impl<'a> Ref<'a> { impl<'a> Ref<'a> {
/// Get the target. /// Get the target.
///
/// Will not be empty.
pub fn target(self) -> &'a str { pub fn target(self) -> &'a str {
self.0 self.0
.children() .children()

View File

@ -185,7 +185,7 @@ impl Lexer<'_> {
'h' if self.s.eat_if("ttp://") => self.link(), 'h' if self.s.eat_if("ttp://") => self.link(),
'h' if self.s.eat_if("ttps://") => self.link(), 'h' if self.s.eat_if("ttps://") => self.link(),
'<' if self.s.at(is_id_continue) => self.label(), '<' if self.s.at(is_id_continue) => self.label(),
'@' if self.s.at(is_id_continue) => self.ref_marker(), '@' => self.ref_marker(),
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
'-' if self.s.eat_if("--") => SyntaxKind::Shorthand, '-' if self.s.eat_if("--") => SyntaxKind::Shorthand,

View File

@ -395,10 +395,6 @@ pub fn default_math_class(c: char) -> Option<MathClass> {
// https://github.com/typst/typst/issues/5764 // https://github.com/typst/typst/issues/5764
'⟇' => Some(MathClass::Binary), '⟇' => Some(MathClass::Binary),
// Arabic comma.
// https://github.com/latex3/unicode-math/pull/633#issuecomment-2028936135
'،' => Some(MathClass::Punctuation),
c => unicode_math_class::class(c), c => unicode_math_class::class(c),
} }
} }

View File

@ -256,8 +256,8 @@ In Typst, the same function can be used both to affect the appearance for the
remainder of the document, a block (or scope), or just its arguments. For remainder of the document, a block (or scope), or just its arguments. For
example, `[#text(weight: "bold")[bold text]]` will only embolden its argument, example, `[#text(weight: "bold")[bold text]]` will only embolden its argument,
while `[#set text(weight: "bold")]` will embolden any text until the end of the while `[#set text(weight: "bold")]` will embolden any text until the end of the
current block, or the end of the document, if there is none. The effects of a current block, or, if there is none, document. The effects of a function are
function are immediately obvious based on whether it is used in a call or a immediately obvious based on whether it is used in a call or a
[set rule.]($styling/#set-rules) [set rule.]($styling/#set-rules)
```example ```example

View File

@ -181,7 +181,11 @@
[`sys.version`]($category/foundations/sys) can also be very useful. [`sys.version`]($category/foundations/sys) can also be very useful.
```typ ```typ
#let tiling = if "tiling" in std { tiling } else { pattern } #let tiling = if "tiling" in dictionary(std) {
tiling
} else {
pattern
}
... ...
``` ```

View File

@ -720,12 +720,18 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
} }
}; };
for (variant, c, deprecation) in symbol.variants() { for (variant, c) in symbol.variants() {
let shorthand = |list: &[(&'static str, char)]| { let shorthand = |list: &[(&'static str, char)]| {
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
}; };
let name = complete(variant); let name = complete(variant);
let deprecation = match name.as_str() {
"integral.sect" => {
Some("`integral.sect` is deprecated, use `integral.inter` instead")
}
_ => binding.deprecation(),
};
list.push(SymbolModel { list.push(SymbolModel {
name, name,
@ -736,10 +742,10 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
accent: typst::math::Accent::combine(c).is_some(), accent: typst::math::Accent::combine(c).is_some(),
alternates: symbol alternates: symbol
.variants() .variants()
.filter(|(other, _, _)| other != &variant) .filter(|(other, _)| other != &variant)
.map(|(other, _, _)| complete(other)) .map(|(other, _)| complete(other))
.collect(), .collect(),
deprecation: deprecation.or_else(|| binding.deprecation()), deprecation,
}); });
} }
} }

View File

@ -79,7 +79,7 @@ the right.
Last but not least is the `numbering` argument. Here, we can provide a Last but not least is the `numbering` argument. Here, we can provide a
[numbering pattern]($numbering) that defines how to number the pages. By [numbering pattern]($numbering) that defines how to number the pages. By
setting it to `{"1"}`, Typst only displays the bare page number. Setting it to setting into to `{"1"}`, Typst only displays the bare page number. Setting it to
`{"(1/1)"}` would have displayed the current page and total number of pages `{"(1/1)"}` would have displayed the current page and total number of pages
surrounded by parentheses. And we could even have provided a completely custom surrounded by parentheses. And we could even have provided a completely custom
function here to format things to our liking. function here to format things to our liking.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p><s>Struck</s> <mark>Highlighted</mark> <span style="text-decoration: underline">Underlined</span> <span style="text-decoration: overline">Overlined</span></p>
<p><span style="text-decoration: overline"><span style="text-decoration: underline"><mark><s>Mixed</s></mark></span></span></p>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -92,7 +92,3 @@ _Visible_
--- label-non-existent-error --- --- label-non-existent-error ---
// Error: 5-10 sequence does not have field "label" // Error: 5-10 sequence does not have field "label"
#[].label #[].label
--- label-empty ---
// Error: 23-32 label name must not be empty
= Something to label #label("")

View File

@ -1,8 +0,0 @@
// No proper HTML tests here yet because we don't want to test SVG export just
// yet. We'll definitely add tests at some point.
--- html-frame-in-layout ---
// Ensure that HTML frames are transparent in layout. This is less important for
// actual paged export than for _nested_ HTML frames, which take the same code
// path.
#html.frame[A]

View File

@ -34,7 +34,7 @@ To the right! Where the sunlight peeks behind the mountain.
#align(start)[Start] #align(start)[Start]
#align(end)[Ende] #align(end)[Ende]
#set text(lang: "ar", font: "Noto Sans Arabic") #set text(lang: "ar")
#align(start)[يبدأ] #align(start)[يبدأ]
#align(end)[نهاية] #align(end)[نهاية]

View File

@ -45,7 +45,6 @@ Lריווח #h(1cm) R
--- bidi-whitespace-reset --- --- bidi-whitespace-reset ---
// Test whether L1 whitespace resetting destroys stuff. // Test whether L1 whitespace resetting destroys stuff.
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
الغالب #h(70pt) ن#" الغالب #h(70pt) ن#"
--- bidi-explicit-dir --- --- bidi-explicit-dir ---
@ -88,7 +87,7 @@ Lריווח #h(1cm) R
columns: (1fr, 1fr), columns: (1fr, 1fr),
lines(6), lines(6),
[ [
#text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic"))[مجرد نص مؤقت لأغراض العرض التوضيحي. ] #text(lang: "ar")[مجرد نص مؤقت لأغراض العرض التوضيحي. ]
#text(lang: "ar")[سلام] #text(lang: "ar")[سلام]
], ],
) )

View File

@ -29,7 +29,6 @@ ABCअपार्टमेंट
\ ט \ ט
--- shaping-font-fallback --- --- shaping-font-fallback ---
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
// Font fallback for emoji. // Font fallback for emoji.
A😀B A😀B

View File

@ -80,7 +80,7 @@ I'm in#text(tracking: 0.15em + 1.5pt)[ spaace]!
--- text-tracking-arabic --- --- text-tracking-arabic ---
// Test tracking in arabic text (makes no sense whatsoever) // Test tracking in arabic text (makes no sense whatsoever)
#set text(tracking: 0.3em, font: "Noto Sans Arabic") #set text(tracking: 0.3em)
النص النص
--- text-spacing --- --- text-spacing ---

View File

@ -17,7 +17,7 @@
--- repeat-dots-rtl --- --- repeat-dots-rtl ---
// Test dots with RTL. // Test dots with RTL.
#set text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic")) #set text(lang: "ar")
مقدمة #box(width: 1fr, repeat[.]) 15 مقدمة #box(width: 1fr, repeat[.]) 15
--- repeat-empty --- --- repeat-empty ---
@ -35,7 +35,7 @@ A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
#set align(center) #set align(center)
A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
#set text(dir: rtl, font: "Noto Sans Arabic") #set text(dir: rtl)
ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون
--- repeat-unrestricted --- --- repeat-unrestricted ---

View File

@ -121,8 +121,8 @@ $a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.lo
--- math-attach-integral --- --- math-attach-integral ---
// Test default of scripts attachments on integrals at display size. // Test default of scripts attachments on integrals at display size.
$ integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ $ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $
$integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ $integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$
--- math-attach-large-operator --- --- math-attach-large-operator ---
// Test default of limit attachments on large operators at display size only. // Test default of limit attachments on large operators at display size only.
@ -179,7 +179,7 @@ $ a0 + a1 + a0_2 \
#{ #{
let var = $x^1$ let var = $x^1$
for i in range(24) { for i in range(24) {
var = $var$ var = $var$
} }
$var_2$ $var_2$
} }

View File

@ -75,14 +75,6 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read
// Error: 2-62 CSL style "Alphanumeric" is not suitable for bibliographies // Error: 2-62 CSL style "Alphanumeric" is not suitable for bibliographies
#bibliography("/assets/bib/works.bib", style: "alphanumeric") #bibliography("/assets/bib/works.bib", style: "alphanumeric")
--- bibliography-empty-key ---
#let src = ```yaml
"":
type: Book
```
// Error: 15-30 bibliography contains entry with empty key
#bibliography(bytes(src.text))
--- issue-4618-bibliography-set-heading-level --- --- issue-4618-bibliography-set-heading-level ---
// Test that the bibliography block's heading is set to 2 by the show rule, // Test that the bibliography block's heading is set to 2 by the show rule,
// and therefore should be rendered like a level-2 heading. Notably, this // and therefore should be rendered like a level-2 heading. Notably, this

View File

@ -147,16 +147,3 @@ B #cite(<netwok>) #cite(<arrgh>).
// Error: 7-17 expected label, found string // Error: 7-17 expected label, found string
// Hint: 7-17 use `label("%@&#*!\\")` to create a label // Hint: 7-17 use `label("%@&#*!\\")` to create a label
#cite("%@&#*!\\") #cite("%@&#*!\\")
--- issue-5775-cite-order-rtl ---
// Test citation order in RTL text.
#set page(width: 300pt)
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
@netwok
aaa
این است
@tolkien54
و این یکی هست
@arrgh
#bibliography("/assets/bib/works.bib")

View File

@ -231,7 +231,7 @@ Welcome \ here. Does this work well?
--- par-hanging-indent-rtl --- --- par-hanging-indent-rtl ---
#set par(hanging-indent: 2em) #set par(hanging-indent: 2em)
#set text(dir: rtl, font: ("Libertinus Serif", "Noto Sans Arabic")) #set text(dir: rtl)
لآن وقد أظلم الليل وبدأت النجوم لآن وقد أظلم الليل وبدأت النجوم
تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار

View File

@ -2,7 +2,6 @@
--- quote-dir-author-pos --- --- quote-dir-author-pos ---
// Text direction affects author positioning // Text direction affects author positioning
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
#set text(lang: "ar") #set text(lang: "ar")
@ -10,7 +9,6 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
--- quote-dir-align --- --- quote-dir-align ---
// Text direction affects block alignment // Text direction affects block alignment
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
#set quote(block: true) #set quote(block: true)
#quote(attribution: [René Descartes])[cogito, ergo sum] #quote(attribution: [René Descartes])[cogito, ergo sum]

View File

@ -86,14 +86,3 @@ Text seen on #ref(<text>, form: "page", supplement: "Page").
// Test reference with non-whitespace before it. // Test reference with non-whitespace before it.
#figure[] <1> #figure[] <1>
#test([(#ref(<1>))], [(@1)]) #test([(#ref(<1>))], [(@1)])
--- ref-to-empty-label-not-possible ---
// @ without any following label should just produce the symbol in the output
// and not produce a reference to a label with an empty name.
@
--- ref-function-empty-label ---
// using ref() should also not be possible
// Error: 6-7 unexpected less-than operator
// Error: 7-8 unexpected greater-than operator
#ref(<>)

View File

@ -264,8 +264,6 @@
#test("Hey" not in "abheyCd", true) #test("Hey" not in "abheyCd", true)
#test("a" not #test("a" not
/* fun comment? */ in "abc", false) /* fun comment? */ in "abc", false)
#test("sys" in std, true)
#test("system" in std, false)
--- ops-not-trailing --- --- ops-not-trailing ---
// Error: 10 expected keyword `in` // Error: 10 expected keyword `in`

View File

@ -2,7 +2,6 @@
--- numbers --- --- numbers ---
// Test numbers in text mode. // Test numbers in text mode.
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
12 \ 12 \
12.0 \ 12.0 \
3.14 \ 3.14 \

View File

@ -83,11 +83,3 @@ We can also specify a customized value
#highlight(stroke: 2pt + blue)[abc] #highlight(stroke: 2pt + blue)[abc]
#highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc] #highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc]
#highlight(stroke: 1pt, radius: 3pt)[#lorem(5)] #highlight(stroke: 1pt, radius: 3pt)[#lorem(5)]
--- html-deco html ---
#strike[Struck]
#highlight[Highlighted]
#underline[Underlined]
#overline[Overlined]
#(strike, highlight, underline, overline).fold([Mixed], (it, f) => f(it))

View File

@ -1,89 +1,22 @@
// Test sub- and superscript shifts. // Test sub- and superscript shifts.
--- sub-super --- --- sub-super ---
#let sq = box(square(size: 4pt))
#table( #table(
columns: 3, columns: 3,
[Typo.], [Fallb.], [Synth.], [Typo.], [Fallb.], [Synth],
[x#super[1#sq]], [x#super[5: #sq]], [x#super(typographic: false)[2 #sq]], [x#super[1]], [x#super[5n]], [x#super[2 #box(square(size: 6pt))]],
[x#sub[1#sq]], [x#sub[5: #sq]], [x#sub(typographic: false)[2 #sq]], [x#sub[1]], [x#sub[5n]], [x#sub[2 #box(square(size: 6pt))]],
) )
--- sub-super-typographic ---
#set text(size: 20pt)
// Libertinus Serif supports "subs" and "sups" for `typo` and `sq`, but not for
// `synth`.
#let synth = [1,2,3]
#let typo = [123]
#let sq = [1#box(square(size: 4pt))2]
x#super(synth) x#super(typo) x#super(sq) \
x#sub(synth) x#sub(typo) x#sub(sq)
--- sub-super-italic-compensation ---
#set text(size: 20pt, style: "italic")
// Libertinus Serif supports "subs" and "sups" for `typo`, but not for `synth`.
#let synth = [1,2,3]
#let typo = [123]
#let sq = [1#box(square(size: 4pt))2]
x#super(synth) x#super(typo) x#super(sq) \
x#sub(synth) x#sub(typo) x#sub(sq)
--- sub-super-non-typographic --- --- sub-super-non-typographic ---
#set super(typographic: false, baseline: -0.25em, size: 0.7em) #set super(typographic: false, baseline: -0.25em, size: 0.7em)
n#super[1], n#sub[2], ... n#super[N] n#super[1], n#sub[2], ... n#super[N]
--- super-underline --- --- super-underline ---
#set underline(stroke: 0.5pt, offset: 0.15em) #set underline(stroke: 0.5pt, offset: 0.15em)
#set super(typographic: false) #underline[The claim#super[\[4\]]] has been disputed. \
#underline[A#super[4]] B \ The claim#super[#underline[\[4\]]] has been disputed. \
A#super[#underline[4]] B \ It really has been#super(box(text(baseline: 0pt, underline[\[4\]]))) \
A #underline(super[4]) B \
#set super(typographic: true)
#underline[A#super[4]] B \
A#super[#underline[4]] B \
A #underline(super[4]) B
--- super-highlight ---
#set super(typographic: false)
#highlight[A#super[4]] B \
A#super[#highlight[4]] B \
A#super(highlight[4]) \
#set super(typographic: true)
#highlight[A#super[4]] B \
A#super[#highlight[4]] B \
A#super(highlight[4])
--- super-1em ---
#set text(size: 10pt)
#super(context test(1em.to-absolute(), 10pt))
--- long-scripts ---
|longscript| \
|#super(typographic: true)[longscript]| \
|#super(typographic: false)[longscript]| \
|#sub(typographic: true)[longscript]| \
|#sub(typographic: false)[longscript]|
--- script-metrics-bundeled-fonts ---
// Tests whether the script metrics are used properly by synthesizing
// superscripts and subscripts for all bundled fonts.
#set super(typographic: false)
#set sub(typographic: false)
#let test(font, weights, styles) = {
for weight in weights {
for style in styles {
text(font: font, weight: weight, style: style)[Xx#super[Xx]#sub[Xx]]
linebreak()
}
}
}
#test("DejaVu Sans Mono", ("regular", "bold"), ("normal", "oblique"))
#test("Libertinus Serif", ("regular", "semibold", "bold"), ("normal", "italic"))
#test("New Computer Modern", ("regular", "bold"), ("normal", "italic"))
#test("New Computer Modern Math", (400, 450, "bold"), ("normal",))
--- basic-sup-sub html --- --- basic-sup-sub html ---
1#super[st], 2#super[nd], 3#super[rd]. 1#super[st], 2#super[nd], 3#super[rd].

View File

@ -666,29 +666,3 @@ $ A = mat(
#let _ = gradient.linear(..my-gradient.stops()) #let _ = gradient.linear(..my-gradient.stops())
#let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true) #let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true)
#let _ = gradient.linear(..my-gradient2.stops()) #let _ = gradient.linear(..my-gradient2.stops())
--- issue-6162-coincident-gradient-stops-export-png ---
// Ensure that multiple gradient stops with the same position
// don't cause a panic.
#rect(
fill: gradient.linear(
(red, 0%),
(green, 0%),
(blue, 100%),
)
)
#rect(
fill: gradient.linear(
(red, 0%),
(green, 100%),
(blue, 100%),
)
)
#rect(
fill: gradient.linear(
(white, 0%),
(red, 50%),
(green, 50%),
(blue, 100%),
)
)

View File

@ -94,12 +94,9 @@
"watch": "tsc -watch -p ./" "watch": "tsc -watch -p ./"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.4", "@types/node": "18.x",
"@types/vscode": "^1.101.0", "@types/vscode": "^1.88.0",
"typescript": "^5.8.3" "typescript": "^5.3.3"
},
"dependencies": {
"shiki": "^3.7.0"
}, },
"engines": { "engines": {
"vscode": "^1.88.0" "vscode": "^1.88.0"
@ -107,4 +104,4 @@
"__metadata": { "__metadata": {
"size": 35098973 "size": 35098973
} }
} }

View File

@ -1,7 +1,6 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import * as cp from "child_process"; import * as cp from "child_process";
import { clearInterval } from "timers"; import { clearInterval } from "timers";
const shiki = import("shiki"); // Normal import causes TypeScript problems.
// Called when an activation event is triggered. Our activation event is the // Called when an activation event is triggered. Our activation event is the
// presence of "tests/suite/playground.typ". // presence of "tests/suite/playground.typ".
@ -18,8 +17,6 @@ class TestHelper {
opened?: { opened?: {
// The tests's name. // The tests's name.
name: string; name: string;
// The test's attributes.
attrs: string[];
// The WebView panel that displays the test images and output. // The WebView panel that displays the test images and output.
panel: vscode.WebviewPanel; panel: vscode.WebviewPanel;
}; };
@ -47,18 +44,18 @@ class TestHelper {
); );
// Triggered when clicking "View" in the lens. // Triggered when clicking "View" in the lens.
this.registerCommand("typst-test-helper.viewFromLens", (name, attrs) => this.registerCommand("typst-test-helper.viewFromLens", (name) =>
this.viewFromLens(name, attrs) this.viewFromLens(name)
); );
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
this.registerCommand("typst-test-helper.runFromLens", (name, attrs) => this.registerCommand("typst-test-helper.runFromLens", (name) =>
this.runFromLens(name, attrs) this.runFromLens(name)
); );
// Triggered when clicking "Save" in the lens. // Triggered when clicking "Save" in the lens.
this.registerCommand("typst-test-helper.saveFromLens", (name, attrs) => this.registerCommand("typst-test-helper.saveFromLens", (name) =>
this.saveFromLens(name, attrs) this.saveFromLens(name)
); );
// Triggered when clicking "Terminal" in the lens. // Triggered when clicking "Terminal" in the lens.
@ -124,32 +121,31 @@ class TestHelper {
const lenses = []; const lenses = [];
for (let nr = 0; nr < document.lineCount; nr++) { for (let nr = 0; nr < document.lineCount; nr++) {
const line = document.lineAt(nr); const line = document.lineAt(nr);
const re = /^--- ([\d\w-]+)(( [\d\w-]+)*) ---$/; const re = /^--- ([\d\w-]+)( [\d\w-]+)* ---$/;
const m = line.text.match(re); const m = line.text.match(re);
if (!m) { if (!m) {
continue; continue;
} }
const name = m[1]; const name = m[1];
const attrs = m[2].trim().split(" ");
lenses.push( lenses.push(
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "View", title: "View",
tooltip: "View the test output and reference in a new tab", tooltip: "View the test output and reference in a new tab",
command: "typst-test-helper.viewFromLens", command: "typst-test-helper.viewFromLens",
arguments: [name, attrs], arguments: [name],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Run", title: "Run",
tooltip: "Run the test and view the results in a new tab", tooltip: "Run the test and view the results in a new tab",
command: "typst-test-helper.runFromLens", command: "typst-test-helper.runFromLens",
arguments: [name, attrs], arguments: [name],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Save", title: "Save",
tooltip: "Run and view the test and save the reference output", tooltip: "Run and view the test and save the reference output",
command: "typst-test-helper.saveFromLens", command: "typst-test-helper.saveFromLens",
arguments: [name, attrs], arguments: [name],
}), }),
new vscode.CodeLens(line.range, { new vscode.CodeLens(line.range, {
title: "Terminal", title: "Terminal",
@ -163,49 +159,40 @@ class TestHelper {
} }
// Triggered when clicking "View" in the lens. // Triggered when clicking "View" in the lens.
private viewFromLens(name: string, attrs: string[]) { private viewFromLens(name: string) {
if ( if (this.opened?.name == name) {
this.opened?.name == name &&
this.opened.attrs.join(" ") == attrs.join(" ")
) {
this.opened.panel.reveal(); this.opened.panel.reveal();
return; return;
} }
if (this.opened) { if (this.opened) {
this.opened.name = name; this.opened.name = name;
this.opened.attrs = attrs;
this.opened.panel.title = name; this.opened.panel.title = name;
} else { } else {
const panel = vscode.window.createWebviewPanel( const panel = vscode.window.createWebviewPanel(
"typst-test-helper.preview", "typst-test-helper.preview",
name, name,
vscode.ViewColumn.Beside, vscode.ViewColumn.Beside,
{ enableFindWidget: true, enableScripts: true } { enableFindWidget: true }
); );
panel.onDidDispose(() => (this.opened = undefined)); panel.onDidDispose(() => (this.opened = undefined));
panel.webview.onDidReceiveMessage((message) => {
if (message.command === "openFile") {
vscode.env.openExternal(vscode.Uri.parse(message.uri));
}
});
this.opened = { name, attrs, panel }; this.opened = { name, panel };
} }
this.refreshWebView(); this.refreshWebView();
} }
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
private runFromLens(name: string, attrs: string[]) { private runFromLens(name: string) {
this.viewFromLens(name, attrs); this.viewFromLens(name);
this.runFromPreview(); this.runFromPreview();
} }
// Triggered when clicking "Run" in the lens. // Triggered when clicking "Run" in the lens.
private saveFromLens(name: string, attrs: string[]) { private saveFromLens(name: string) {
this.viewFromLens(name, attrs); this.viewFromLens(name);
this.saveFromPreview(); this.saveFromPreview();
} }
@ -301,37 +288,41 @@ class TestHelper {
private copyImageFilePathFromPreviewContext(webviewSection: string) { private copyImageFilePathFromPreviewContext(webviewSection: string) {
if (!this.opened) return; if (!this.opened) return;
const { name } = this.opened; const { name } = this.opened;
const [bucket, format] = webviewSection.split("/"); const { png, ref } = getImageUris(name);
vscode.env.clipboard.writeText( switch (webviewSection) {
getUri(name, bucket as Bucket, format as Format).fsPath case "png":
); vscode.env.clipboard.writeText(png.fsPath);
break;
case "ref":
vscode.env.clipboard.writeText(ref.fsPath);
break;
default:
break;
}
} }
// Reloads the web view. // Reloads the web view.
private refreshWebView(output?: { stdout: string; stderr: string }) { private refreshWebView(output?: { stdout: string; stderr: string }) {
if (!this.opened) return; if (!this.opened) return;
const { name, attrs, panel } = this.opened; const { name, panel } = this.opened;
const { png, ref } = getImageUris(name);
if (panel) { if (panel) {
console.log( console.log(
`Refreshing WebView for ${name}` + `Refreshing WebView for ${name}` + (panel.visible ? " in background" : ""));
(panel.visible ? " in background" : "") const webViewSrcs = {
); png: panel.webview.asWebviewUri(png),
ref: panel.webview.asWebviewUri(ref),
};
panel.webview.html = ""; panel.webview.html = "";
// Make refresh notable. // Make refresh notable.
setTimeout(async () => { setTimeout(() => {
if (!panel) { if (!panel) {
throw new Error("panel to refresh is falsy after waiting"); throw new Error("panel to refresh is falsy after waiting");
} }
panel.webview.html = await getWebviewContent( panel.webview.html = getWebviewContent(webViewSrcs, output);
panel,
name,
attrs,
output
);
}, 50); }, 50);
} }
} }
@ -395,43 +386,30 @@ function getWorkspaceRoot() {
return vscode.workspace.workspaceFolders![0].uri; return vscode.workspace.workspaceFolders![0].uri;
} }
const EXTENSION = { html: "html", render: "png" }; // Returns the URIs for a test's images.
function getImageUris(name: string) {
type Bucket = "store" | "ref"; const root = getWorkspaceRoot();
type Format = "html" | "render"; const png = vscode.Uri.joinPath(root, `tests/store/render/${name}.png`);
const ref = vscode.Uri.joinPath(root, `tests/ref/${name}.png`);
function getUri(name: string, bucket: Bucket, format: Format) { return { png, ref };
let path;
if (bucket === "ref" && format === "render") {
path = `tests/ref/${name}.png`;
} else {
path = `tests/${bucket}/${format}/${name}.${EXTENSION[format]}`;
}
return vscode.Uri.joinPath(getWorkspaceRoot(), path);
} }
// Produces the content of the WebView. // Produces the content of the WebView.
async function getWebviewContent( function getWebviewContent(
panel: vscode.WebviewPanel, webViewSrcs: { png: vscode.Uri; ref: vscode.Uri },
name: string,
attrs: string[],
output?: { output?: {
stdout: string; stdout: string;
stderr: string; stderr: string;
} }
): Promise<string> { ): string {
const showHtml = attrs.includes("html"); const escape = (text: string) =>
const showRender = !showHtml || attrs.includes("render"); text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const stdout = output?.stdout const stdoutHtml = output?.stdout
? `<h2>Standard output</h2><pre class="output">${escape( ? `<h1>Standard output</h1><pre>${escape(output.stdout)}</pre>`
output.stdout
)}</pre>`
: ""; : "";
const stderr = output?.stderr const stderrHtml = output?.stderr
? `<h2>Standard error</h2><pre class="output">${escape( ? `<h1>Standard error</h1><pre>${escape(output.stderr)}</pre>`
output.stderr
)}</pre>`
: ""; : "";
return ` return `
@ -471,169 +449,46 @@ async function getWebviewContent(
color: #bebebe; color: #bebebe;
content: "Not present"; content: "Not present";
} }
h2 { pre {
margin-bottom: 12px; display: inline-block;
} font-family: var(--vscode-editor-font-family);
h2 a { text-align: left;
color: var(--vscode-editor-foreground); width: 80%;
text-decoration: underline;
}
h2 a:hover {
cursor: pointer;
} }
.flex { .flex {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
} }
.vertical {
flex-direction: column;
}
.top-bottom {
display: flex;
flex-direction: column;
padding-inline: 32px;
width: calc(100vw - 64px);
}
pre {
font-family: var(--vscode-editor-font-family);
text-align: left;
white-space: pre-wrap;
}
pre.output {
display: inline-block;
width: 80%;
margin-block-start: 0;
}
pre.shiki {
background-color: transparent !important;
padding: 12px;
margin-block-start: 0;
}
pre.shiki code {
--vscode-textPreformat-background: transparent;
}
iframe, pre.shiki {
border: 1px solid rgb(189, 191, 204);
border-radius: 6px;
}
iframe {
background: white;
}
.vscode-dark iframe {
filter: invert(1) hue-rotate(180deg);
}
</style> </style>
<script>
const api = acquireVsCodeApi()
function openFile(uri) {
api.postMessage({ command: 'openFile', uri });
}
function sizeIframe(obj){
obj.style.height = 0;
obj.style.height = obj.contentWindow.document.body.scrollHeight + 'px';
}
</script>
</head> </head>
<body> <body>
${showRender ? renderSection(panel, name) : ""} <div
${showHtml ? await htmlSection(name) : ""} class="flex"
${stdout} data-vscode-context='{"preventDefaultContextMenuItems": true}'
${stderr} >
<div>
<h1>Output</h1>
<img
class="output"
data-vscode-context='{"webviewSection":"png"}'
src="${webViewSrcs.png}"
alt="Placeholder"
>
</div>
<div>
<h1>Reference</h1>
<img
class="ref"
data-vscode-context='{"webviewSection":"ref"}'
src="${webViewSrcs.ref}"
alt="Placeholder"
>
</div>
</div>
${stdoutHtml}
${stderrHtml}
</body> </body>
</html>`; </html>`;
} }
function renderSection(panel: vscode.WebviewPanel, name: string) {
const outputUri = getUri(name, "store", "render");
const refUri = getUri(name, "ref", "render");
return `<div
class="flex"
data-vscode-context='{"preventDefaultContextMenuItems": true}'
>
<div>
${linkedTitle("Output", outputUri)}
<img
class="output"
data-vscode-context='{"bucket":"store", format: "render"}'
src="${panel.webview.asWebviewUri(outputUri)}"
alt="Placeholder"
>
</div>
<div>
${linkedTitle("Reference", refUri)}
<img
class="ref"
data-vscode-context='{"bucket":"ref", format: "render"}'
src="${panel.webview.asWebviewUri(refUri)}"
alt="Placeholder"
>
</div>
</div>`;
}
async function htmlSection(name: string) {
const storeHtml = await htmlSnippet(
"HTML Output",
getUri(name, "store", "html")
);
const refHtml = await htmlSnippet(
"HTML Reference",
getUri(name, "ref", "html")
);
return `<div
class="flex vertical"
data-vscode-context='{"preventDefaultContextMenuItems": true}'
>
${storeHtml}
${refHtml}
</div>`;
}
async function htmlSnippet(title: string, uri: vscode.Uri): Promise<string> {
try {
const data = await vscode.workspace.fs.readFile(uri);
const code = new TextDecoder("utf-8").decode(data);
return `<div>
${linkedTitle(title, uri)}
<div class="top-bottom">
${await highlight(code)}
<iframe srcdoc="${escape(code)}"></iframe>
</div>
</div>`;
} catch {
return `<div><h2>${title}</h2>Not present</div>`;
}
}
function linkedTitle(title: string, uri: vscode.Uri) {
return `<h2><a onclick="openFile('${uri.toString()}')">${title}</a></h2>`;
}
async function highlight(code: string): Promise<string> {
return (await shiki).codeToHtml(code, {
lang: "html",
theme: selectTheme(),
});
}
function selectTheme() {
switch (vscode.window.activeColorTheme.kind) {
case vscode.ColorThemeKind.Light:
case vscode.ColorThemeKind.HighContrastLight:
return "github-light";
case vscode.ColorThemeKind.Dark:
case vscode.ColorThemeKind.HighContrast:
return "github-dark";
}
}
function escape(text: string) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}

View File

@ -1,10 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "nodenext", "module": "Node16",
"lib": ["ES2022", "DOM"],
"target": "ES2022", "target": "ES2022",
"moduleResolution": "nodenext",
"outDir": "dist", "outDir": "dist",
"lib": ["ES2022"],
"sourceMap": true, "sourceMap": true,
"rootDir": "src", "rootDir": "src",
"strict": true "strict": true