Compare commits

...

15 Commits

Author SHA1 Message Date
Eric Biedert
78670e7fb9
Merge bef4e20434334d450a9d3cf3a41ada9c6cde1535 into 1dc4c248d1022dc9f3b6e3e899857404f6c680a1 2025-07-09 14:16:59 +02:00
Malo
1dc4c248d1
Add default argument for str.first and str.last (#6554)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-09 12:10:24 +00:00
frozolotl
9e6adb6f45
Ignore spans when checking for RawElem equality (#6560) 2025-07-09 12:04:22 +00:00
Robin
4534167656
Use "displayed" instead of "repeated" to avoid ambiguity in numbering docs (#6565) 2025-07-09 12:02:50 +00:00
Laurenz
9ad1879e9d
Anti-alias clip paths (#6570) 2025-07-09 12:02:13 +00:00
Robin
e5e813219e
Fix typo of Typst domain in quote docs (#6573) 2025-07-09 12:01:57 +00:00
Laurenz
52a708b988
Move html module to typst-html crate (#6577) 2025-07-09 09:46:40 +00:00
Laurenz
e71674f6b3
Construct library via extension trait instead of default & inherent impl (#6576) 2025-07-09 09:28:26 +00:00
Laurenz
e5e1dcd9c0
Target-specific native show rules (#6569) 2025-07-09 08:16:36 +00:00
Laurenz
0a3c6939dd
Rewrite foundations of native elements (#6547) 2025-07-08 08:52:43 +00:00
Eric Biedert
bef4e20434 Add test for location of migrated block
Previously, this would result in a position on the first page.
2025-05-28 13:03:17 +02:00
Eric Biedert
811996eb70 Update references of existing tests
In `grid-header-containing-rowspan`, the first region is now correctly
not stroked.

Not sure what happened in `grid-header-orphan-prevention`, but the "B"
in the first header was too bold before.
2025-05-27 15:27:04 +02:00
Eric Biedert
02f07e7912 Don't label empty orphan frames
Adding a label makes a previously empty frame non-empty, but we want to
keep orphans empty.
2025-05-27 15:27:04 +02:00
Eric Biedert
693edb475d Don't break blocks after empty frame
Instead, spill the whole child into the next region to prevent small
leftovers to influence layout. This is not done when all frames are
empty (e.g. for an explicitly sized block without content or fill).

This helps with the following cases:
- Previously, if a sticky block was followed by a leftover frame, the
  stickiness would be ignored, as the leftover was in fact sticking.
  This is not currently a problem, as sticky blocks aren't really
  breakable at the moment, but probably will be in the future.
- When ignoring stroke and fill for a first empty frame, a nested broken
  block would previously make the first frame not be considered empty
  anymore, which would lead to the leftover frame being filled.
- Similarly, when the fill of an explicitly sized block is ignored in
  the first empty frame, the leftover part would still be considered as
  laid out, making the actually visible block too small.
2025-05-27 15:21:15 +02:00
Eric Biedert
606183cd30 Add tests 2025-05-27 15:21:15 +02:00
163 changed files with 5065 additions and 4276 deletions

View File

@ -103,3 +103,15 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo install --locked cargo-fuzz@0.12.0
- run: cd tests/fuzz && cargo fuzz build --dev
miri:
name: Check unsafe code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
components: miri
toolchain: nightly-2024-10-29
- uses: Swatinem/rust-cache@v2
- run: cargo miri test -p typst-library test_miri

4
Cargo.lock generated
View File

@ -2971,8 +2971,12 @@ dependencies = [
name = "typst-html"
version = "0.13.1"
dependencies = [
"bumpalo",
"comemo",
"ecow",
"palette",
"time",
"typst-assets",
"typst-library",
"typst-macros",
"typst-svg",

View File

@ -12,7 +12,7 @@ use typst::foundations::{Bytes, Datetime, Dict, IntoValue};
use typst::syntax::{FileId, Lines, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, World};
use typst::{Library, LibraryExt, World};
use typst_kit::fonts::{FontSlot, Fonts};
use typst_kit::package::PackageStorage;
use typst_timing::timed;

View File

@ -186,7 +186,7 @@ impl Eval for ast::Raw<'_> {
let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect();
let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block());
if let Some(lang) = self.lang() {
elem.push_lang(Some(lang.get().clone()));
elem.lang.set(Some(lang.get().clone()));
}
Ok(elem.pack())
}
@ -219,9 +219,8 @@ impl Eval for ast::Ref<'_> {
.expect("unexpected empty reference");
let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content(
supplement.eval(vm)?,
))));
elem.supplement
.set(Smart::Custom(Some(Supplement::Content(supplement.eval(vm)?))));
}
Ok(elem.pack())
}
@ -252,7 +251,7 @@ impl Eval for ast::EnumItem<'_> {
let body = self.body().eval(vm)?;
let mut elem = EnumItem::new(body);
if let Some(number) = self.number() {
elem.push_number(Some(number));
elem.number.set(Some(number));
}
Ok(elem.pack())
}

View File

@ -80,17 +80,17 @@ impl Eval for ast::MathAttach<'_> {
let mut elem = AttachElem::new(base);
if let Some(expr) = self.top() {
elem.push_t(Some(expr.eval_display(vm)?));
elem.t.set(Some(expr.eval_display(vm)?));
}
// Always attach primes in scripts style (not limits style),
// i.e. at the top-right corner.
if let Some(primes) = self.primes() {
elem.push_tr(Some(primes.eval(vm)?));
elem.tr.set(Some(primes.eval(vm)?));
}
if let Some(expr) = self.bottom() {
elem.push_b(Some(expr.eval_display(vm)?));
elem.b.set(Some(expr.eval_display(vm)?));
}
Ok(elem.pack())

View File

@ -1,6 +1,6 @@
use typst_library::diag::{warning, At, SourceResult};
use typst_library::foundations::{
Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
Element, Func, Recipe, Selector, ShowableSelector, Styles, Transformation,
};
use typst_library::layout::BlockElem;
use typst_library::model::ParElem;
@ -62,8 +62,7 @@ fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) {
if let Some(Selector::Elem(elem, _)) = recipe.selector();
if *elem == Element::of::<ParElem>();
if let Transformation::Style(styles) = recipe.transform();
if styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Above as _) ||
styles.has::<BlockElem>(<BlockElem as Fields>::Enum::Below as _);
if styles.has(BlockElem::above) || styles.has(BlockElem::below);
then {
vm.engine.sink.warn(warning!(
recipe.span(),

View File

@ -13,14 +13,18 @@ keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-assets = { workspace = true }
typst-library = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
typst-svg = { workspace = true }
bumpalo = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
palette = { workspace = true }
time = { workspace = true }
[lints]
workspace = true

View File

@ -0,0 +1,135 @@
//! Conversion from Typst data types into CSS data types.
use std::fmt::{self, Display};
use typst_library::layout::Length;
use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
use typst_utils::Numeric;
pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => {
write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get())
}
(true, false) => write!(f, "{}em", length.em.get()),
(_, true) => write!(f, "{}pt", length.abs.to_pt()),
})
}
pub fn color(color: Color) -> impl Display {
typst_utils::display(move |f| match color {
Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()),
Color::Oklab(v) => oklab(f, v),
Color::Oklch(v) => oklch(f, v),
Color::LinearRgb(v) => linear_rgb(f, v),
Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()),
})
}
fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result {
write!(f, "oklab({} {} {}{})", percent(v.l), number(v.a), number(v.b), alpha(v.alpha))
}
fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result {
write!(
f,
"oklch({} {} {}deg{})",
percent(v.l),
number(v.chroma),
number(v.hue.into_degrees()),
alpha(v.alpha)
)
}
fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result {
if let Some(v) = rgb_to_8_bit_lossless(v) {
let (r, g, b, a) = v.into_components();
write!(f, "#{r:02x}{g:02x}{b:02x}")?;
if a != u8::MAX {
write!(f, "{a:02x}")?;
}
Ok(())
} else {
write!(
f,
"rgb({} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha)
)
}
}
/// Converts an f32 RGBA color to its 8-bit representation if the result is
/// [very close](is_very_close) to the original.
fn rgb_to_8_bit_lossless(
v: Rgb,
) -> Option<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
let l = v.into_format::<u8, u8>();
let h = l.into_format::<f32, f32>();
(is_very_close(v.red, h.red)
&& is_very_close(v.blue, h.blue)
&& is_very_close(v.green, h.green)
&& is_very_close(v.alpha, h.alpha))
.then_some(l)
}
fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result {
write!(
f,
"color(srgb-linear {} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha),
)
}
fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result {
write!(
f,
"hsl({}deg {} {}{})",
number(v.hue.into_degrees()),
percent(v.saturation),
percent(v.lightness),
alpha(v.alpha),
)
}
/// Displays an alpha component if it not 1.
fn alpha(value: f32) -> impl Display {
typst_utils::display(move |f| {
if !is_very_close(value, 1.0) {
write!(f, " / {}", percent(value))?;
}
Ok(())
})
}
/// Displays a rounded percentage.
///
/// For a percentage, two significant digits after the comma gives us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn percent(ratio: f32) -> impl Display {
typst_utils::display(move |f| {
write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2))
})
}
/// Rounds a number for display.
///
/// For a number between 0 and 1, four significant digits give us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn number(value: f32) -> impl Display {
typst_utils::round_with_precision(value as f64, 4)
}
/// Whether two component values are close enough that there is no
/// difference when encoding them with 12-bit. 12 bit is the highest
/// reasonable color bit depth found in the industry.
fn is_very_close(a: f32, b: f32) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32;
(a - b).abs() < EPS
}

View File

@ -1,13 +1,19 @@
//! Typst's HTML exporter.
mod css;
mod encode;
mod rules;
mod typed;
pub use self::encode::html;
pub use self::rules::register;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::foundations::{
Content, Module, Scope, StyleChain, Target, TargetElem,
};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
};
@ -18,9 +24,19 @@ use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Si
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_library::{Category, World};
use typst_syntax::Span;
/// Create a module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.start_category(Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
crate::typed::define(&mut html);
Module::new("html", html)
}
/// Produce an HTML document from content.
///
/// This first performs root-level realization and then turns the resulting
@ -177,12 +193,12 @@ fn handle(
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body(styles) {
if let Some(body) = elem.body.get_ref(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs(styles).clone(),
attrs: elem.attrs.get_cloned(styles),
children,
span: elem.span(),
};
@ -198,7 +214,7 @@ fn handle(
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// TODO: This is rather incomplete.
if let Some(body) = elem.body(styles) {
if let Some(body) = elem.body.get_ref(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
@ -212,7 +228,7 @@ fn handle(
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body(styles) {
.and_then(|elem| match elem.body.get_ref(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
@ -233,12 +249,12 @@ fn handle(
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double(styles) { '"' } else { '\'' },
if elem.double.get(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::set_target(Target::Paged).wrap();
let style = TargetElem::target.set(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
@ -248,7 +264,7 @@ fn handle(
)?;
output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: TextElem::size_in(styles),
text_size: styles.resolve(TextElem::size),
}));
} else {
engine.sink.warn(warning!(

View File

@ -0,0 +1,411 @@
use std::num::NonZeroUsize;
use ecow::{eco_format, EcoVec};
use typst_library::diag::warning;
use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, StyleChain, Target,
};
use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::introspection::{Counter, Locator};
use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use typst_library::layout::OuterVAlignment;
use typst_library::model::{
Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption,
FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem,
RefElem, StrongElem, TableCell, TableElem, TermsElem,
};
use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem,
SubElem, SuperElem, UnderlineElem,
};
/// Register show rules for the [HTML target](Target::Html).
pub fn register(rules: &mut NativeRuleMap) {
use Target::Html;
// Model.
rules.register(Html, STRONG_RULE);
rules.register(Html, EMPH_RULE);
rules.register(Html, LIST_RULE);
rules.register(Html, ENUM_RULE);
rules.register(Html, TERMS_RULE);
rules.register(Html, LINK_RULE);
rules.register(Html, HEADING_RULE);
rules.register(Html, FIGURE_RULE);
rules.register(Html, FIGURE_CAPTION_RULE);
rules.register(Html, QUOTE_RULE);
rules.register(Html, REF_RULE);
rules.register(Html, CITE_GROUP_RULE);
rules.register(Html, TABLE_RULE);
// Text.
rules.register(Html, SUB_RULE);
rules.register(Html, SUPER_RULE);
rules.register(Html, UNDERLINE_RULE);
rules.register(Html, OVERLINE_RULE);
rules.register(Html, STRIKE_RULE);
rules.register(Html, HIGHLIGHT_RULE);
rules.register(Html, RAW_RULE);
rules.register(Html, RAW_LINE_RULE);
}
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::strong)
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const EMPH_RULE: ShowFn<EmphElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::em)
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(elem.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !elem.tight.get(styles) {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
.pack()
.spanned(elem.span()))
};
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let mut ol = HtmlElem::new(tag::ol);
if elem.reversed.get(styles) {
ol = ol.with_attr(attr::reversed, "reversed");
}
if let Some(n) = elem.start.get(styles).custom() {
ol = ol.with_attr(attr::start, eco_format!("{n}"));
}
let body = Content::sequence(elem.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number.get(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !elem.tight.get(styles) {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
}));
Ok(ol.with_body(Some(body)).pack().spanned(elem.span()))
};
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(elem.children.iter().flat_map(|item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !elem.tight.get(styles) {
description += ParbreakElem::shared();
}
[
HtmlElem::new(tag::dt)
.with_body(Some(item.term.clone()))
.pack()
.spanned(item.term.span()),
HtmlElem::new(tag::dd)
.with_body(Some(description))
.pack()
.spanned(item.description.span()),
]
}))))
.pack())
};
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let body = elem.body.clone();
Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest {
HtmlElem::new(tag::a)
.with_attr(attr::href, url.clone().into_inner())
.with_body(Some(body))
.pack()
.spanned(elem.span())
} else {
engine.sink.warn(warning!(
elem.span(),
"non-URL links are not yet supported by HTML export"
));
body
})
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() {
let location = elem.location().unwrap();
let numbering = Counter::of(HeadingElem::ELEM)
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
realized = numbering + SpaceElem::shared().clone() + realized;
}
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
// reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`.
let level = elem.resolve_level(styles).get();
Ok(if level >= 6 {
engine.sink.warn(warning!(
span,
"heading of level {} was transformed to \
<div role=\"heading\" aria-level=\"{}\">, which is not \
supported by all assistive technology",
level, level + 1;
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
hint: "you may want to restructure your document so that \
it doesn't contain deep headings"
));
HtmlElem::new(tag::div)
.with_body(Some(realized))
.with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack()
.spanned(span)
} else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
})
};
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
// Build the caption, if any.
if let Some(caption) = elem.caption.get_cloned(styles) {
realized = match caption.position.get(styles) {
OuterVAlignment::Top => caption.pack() + realized,
OuterVAlignment::Bottom => realized + caption.pack(),
};
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
Ok(HtmlElem::new(tag::figure)
.with_body(Some(realized))
.pack()
.spanned(span))
};
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(HtmlElem::new(tag::figcaption)
.with_body(Some(elem.realize(engine, styles)?))
.pack()
.spanned(elem.span()))
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
let span = elem.span();
let block = elem.block.get(styles);
let mut realized = elem.body.clone();
if elem.quotes.get(styles).unwrap_or(!block) {
realized = QuoteElem::quoted(realized, styles);
}
let attribution = elem.attribution.get_ref(styles);
if block {
let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution {
if let Some(link) = attribution.to_packed::<LinkElem>() {
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
blockquote =
blockquote.with_attr(attr::cite, url.clone().into_inner());
}
}
}
realized = blockquote.pack().spanned(span);
if let Some(attribution) = attribution.as_ref() {
realized += attribution.realize(span);
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone();
realized += CiteElem::new(*label).pack().spanned(span);
}
Ok(realized)
};
const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
const TABLE_RULE: ShowFn<TableElem> = |elem, engine, styles| {
// The locator is not used by HTML export, so we can just fabricate one.
let locator = Locator::root();
Ok(show_cellgrid(table_to_cellgrid(elem, engine, locator, styles)?, styles))
};
fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| {
let row = row
.iter()
.flat_map(|entry| entry.as_cell())
.map(|cell| show_cell(tag, cell, styles));
elem(tag::tr, Content::sequence(row))
};
// TODO(subfooters): similarly to headers, take consecutive footers from
// the end for 'tfoot'.
let footer = grid.footer.map(|ft| {
let rows = rows.drain(ft.start..);
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
});
// Store all consecutive headers at the start in 'thead'. All remaining
// headers are just 'th' rows across the table body.
let mut consecutive_header_end = 0;
let first_mid_table_header = grid
.headers
.iter()
.take_while(|hd| {
let is_consecutive = hd.range.start == consecutive_header_end;
consecutive_header_end = hd.range.end;
is_consecutive
})
.count();
let (y_offset, header) = if first_mid_table_header > 0 {
let removed_header_rows =
grid.headers.get(first_mid_table_header - 1).unwrap().range.end;
let rows = rows.drain(..removed_header_rows);
(
removed_header_rows,
Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))),
)
} else {
(0, None)
};
// TODO: Consider improving accessibility properties of multi-level headers
// inside tables in the future, e.g. indicating which columns they are
// relative to and so on. See also:
// https://www.w3.org/WAI/tutorials/tables/multi-level/
let mut next_header = first_mid_table_header;
let mut body =
Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| {
let y = relative_y + y_offset;
if let Some(current_header) =
grid.headers.get(next_header).filter(|h| h.range.contains(&y))
{
if y + 1 == current_header.range.end {
next_header += 1;
}
tr(tag::th, row)
} else {
tr(tag::td, row)
}
}));
if header.is_some() || footer.is_some() {
body = elem(tag::tbody, body);
}
let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
elem(tag::table, Content::sequence(content))
}
fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
let cell = cell.body.clone();
let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
let mut attrs = HtmlAttrs::default();
let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
if let Some(colspan) = span(cell.colspan.get(styles)) {
attrs.push(attr::colspan, colspan);
}
if let Some(rowspan) = span(cell.rowspan.get(styles)) {
attrs.push(attr::rowspan, rowspan);
}
HtmlElem::new(tag)
.with_body(Some(cell.body.clone()))
.with_attrs(attrs)
.pack()
.spanned(cell.span())
}
const SUB_RULE: ShowFn<SubElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::sub)
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::sup)
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
// 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.
Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: underline")
.with_body(Some(elem.body.clone()))
.pack())
};
const OVERLINE_RULE: ShowFn<OverlineElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: overline")
.with_body(Some(elem.body.clone()))
.pack())
};
const STRIKE_RULE: ShowFn<StrikeElem> =
|elem, _, _| Ok(HtmlElem::new(tag::s).with_body(Some(elem.body.clone())).pack());
const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
|elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
let lines = elem.lines.as_deref().unwrap_or_default();
let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
for (i, line) in lines.iter().enumerate() {
if i != 0 {
seq.push(LinebreakElem::shared().clone());
}
seq.push(line.clone().pack());
}
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
.with_body(Some(Content::sequence(seq)))
.pack()
.spanned(elem.span()))
};
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());

View File

@ -11,19 +11,20 @@ use bumpalo::Bump;
use comemo::Tracked;
use ecow::{eco_format, eco_vec, EcoString};
use typst_assets::html as data;
use typst_macros::cast;
use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value,
};
use crate::html::tag;
use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use crate::layout::{Axes, Axis, Dir, Length};
use crate::visualize::Color;
use typst_library::html::tag;
use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::layout::{Axes, Axis, Dir, Length};
use typst_library::visualize::Color;
use typst_macros::cast;
use crate::css;
/// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) {
@ -127,12 +128,12 @@ fn construct(element: &'static data::ElemInfo, args: &mut Args) -> SourceResult<
let tag = HtmlTag::constant(element.name);
let mut elem = HtmlElem::new(tag);
if !attrs.0.is_empty() {
elem.push_attrs(attrs);
elem.attrs.set(attrs);
}
if !tag::is_void(tag) {
let body = args.eat::<Content>()?;
elem.push_body(body);
elem.body.set(body);
}
Ok(elem.into_value())
@ -705,153 +706,6 @@ impl IntoAttr for SourceSize {
}
}
/// Conversion from Typst data types into CSS data types.
///
/// This can be moved elsewhere once we start supporting more CSS stuff.
mod css {
use std::fmt::{self, Display};
use typst_utils::Numeric;
use crate::layout::Length;
use crate::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => {
write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get())
}
(true, false) => write!(f, "{}em", length.em.get()),
(_, true) => write!(f, "{}pt", length.abs.to_pt()),
})
}
pub fn color(color: Color) -> impl Display {
typst_utils::display(move |f| match color {
Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()),
Color::Oklab(v) => oklab(f, v),
Color::Oklch(v) => oklch(f, v),
Color::LinearRgb(v) => linear_rgb(f, v),
Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()),
})
}
fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result {
write!(
f,
"oklab({} {} {}{})",
percent(v.l),
number(v.a),
number(v.b),
alpha(v.alpha)
)
}
fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result {
write!(
f,
"oklch({} {} {}deg{})",
percent(v.l),
number(v.chroma),
number(v.hue.into_degrees()),
alpha(v.alpha)
)
}
fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result {
if let Some(v) = rgb_to_8_bit_lossless(v) {
let (r, g, b, a) = v.into_components();
write!(f, "#{r:02x}{g:02x}{b:02x}")?;
if a != u8::MAX {
write!(f, "{a:02x}")?;
}
Ok(())
} else {
write!(
f,
"rgb({} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha)
)
}
}
/// Converts an f32 RGBA color to its 8-bit representation if the result is
/// [very close](is_very_close) to the original.
fn rgb_to_8_bit_lossless(
v: Rgb,
) -> Option<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
let l = v.into_format::<u8, u8>();
let h = l.into_format::<f32, f32>();
(is_very_close(v.red, h.red)
&& is_very_close(v.blue, h.blue)
&& is_very_close(v.green, h.green)
&& is_very_close(v.alpha, h.alpha))
.then_some(l)
}
fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result {
write!(
f,
"color(srgb-linear {} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha),
)
}
fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result {
write!(
f,
"hsl({}deg {} {}{})",
number(v.hue.into_degrees()),
percent(v.saturation),
percent(v.lightness),
alpha(v.alpha),
)
}
/// Displays an alpha component if it not 1.
fn alpha(value: f32) -> impl Display {
typst_utils::display(move |f| {
if !is_very_close(value, 1.0) {
write!(f, " / {}", percent(value))?;
}
Ok(())
})
}
/// Displays a rounded percentage.
///
/// For a percentage, two significant digits after the comma gives us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn percent(ratio: f32) -> impl Display {
typst_utils::display(move |f| {
write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2))
})
}
/// Rounds a number for display.
///
/// For a number between 0 and 1, four significant digits give us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn number(value: f32) -> impl Display {
typst_utils::round_with_precision(value as f64, 4)
}
/// Whether two component values are close enough that there is no
/// difference when encoding them with 12-bit. 12 bit is the highest
/// reasonable color bit depth found in the industry.
fn is_very_close(a: f32, b: f32) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32;
(a - b).abs() < EPS
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -187,6 +187,6 @@ mod tests {
#[test]
fn test_definition_std() {
test("#table", 1, Side::After).must_be_value(typst::model::TableElem::elem());
test("#table", 1, Side::After).must_be_value(typst::model::TableElem::ELEM);
}
}

View File

@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash};
use typst::{Feature, Library, World};
use typst::{Feature, Library, LibraryExt, World};
use crate::IdeWorld;
@ -171,13 +171,11 @@ fn library() -> Library {
let mut lib = typst::Library::builder()
.with_features([Feature::Html].into_iter().collect())
.build();
lib.styles.set(PageElem::width, Smart::Custom(Abs::pt(120.0).into()));
lib.styles.set(PageElem::height, Smart::Auto);
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
Abs::pt(10.0).into(),
)))));
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
.set(PageElem::margin, Margin::splat(Some(Smart::Custom(Abs::pt(10.0).into()))));
lib.styles.set(TextElem::size, TextSize(Abs::pt(10.0).into()));
lib
}

View File

@ -24,15 +24,15 @@ pub fn layout_single_block(
region: Region,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod regions.
let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size);
// Layout the body.
let body = elem.body(styles);
let body = elem.body.get_ref(styles);
let mut frame = match body {
// If we have no body, just create one frame. Its size will be
// adjusted below.
@ -73,18 +73,19 @@ pub fn layout_single_block(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_cloned(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
if elem.clip.get(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
}
@ -111,9 +112,9 @@ pub fn layout_multi_block(
regions: Regions,
) -> SourceResult<Fragment> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Allocate a small vector for backlogs.
let mut buf = SmallVec::<[Abs; 2]>::new();
@ -122,7 +123,7 @@ pub fn layout_multi_block(
let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf);
// Layout the body.
let body = elem.body(styles);
let body = elem.body.get_ref(styles);
let mut fragment = match body {
// If we have no body, just create one frame plus one per backlog
// region. We create them zero-sized; if necessary, their size will
@ -188,29 +189,28 @@ pub fn layout_multi_block(
};
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_ref(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Fetch/compute these outside of the loop.
let clip = elem.clip(styles);
let clip = elem.clip.get(styles);
let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some);
let has_inset = !inset.is_zero();
let is_explicit = matches!(body, None | Some(BlockBody::Content(_)));
// Skip filling/stroking the first frame if it is empty and a non-empty
// one follows.
// Skip filling, stroking and labeling the first frame if it is empty and
// a non-empty one follows.
let mut skip_first = false;
if let [first, rest @ ..] = fragment.as_slice() {
skip_first = has_fill_or_stroke
&& first.is_empty()
&& rest.iter().any(|frame| !frame.is_empty());
skip_first = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
}
// Post-process to apply insets, clipping, fills, and strokes.
@ -242,7 +242,8 @@ pub fn layout_multi_block(
// Assign label to each frame in the fragment.
if let Some(label) = elem.label() {
for frame in fragment.iter_mut() {
// Skip empty orphan frames, as a label would make them non-empty.
for frame in fragment.iter_mut().skip(if skip_first { 1 } else { 0 }) {
frame.label(label);
}
}

View File

@ -89,7 +89,7 @@ impl<'a> Collector<'a, '_, '_> {
} else if child.is::<FlushElem>() {
self.output.push(Child::Flush);
} else if let Some(elem) = child.to_packed::<ColbreakElem>() {
self.output.push(Child::Break(elem.weak(styles)));
self.output.push(Child::Break(elem.weak.get(styles)));
} else if child.is::<PagebreakElem>() {
bail!(
child.span(), "pagebreaks are not allowed inside of containers";
@ -132,7 +132,7 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag));
}
let leading = ParElem::leading_in(styles);
let leading = styles.resolve(ParElem::leading);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] {
@ -146,7 +146,9 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount {
Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8),
Spacing::Rel(rel) => {
Child::Rel(rel.resolve(styles), elem.weak.get(styles) as u8)
}
Spacing::Fr(fr) => Child::Fr(fr),
});
}
@ -169,8 +171,8 @@ impl<'a> Collector<'a, '_, '_> {
)?
.into_frames();
let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
let spacing = elem.spacing.resolve(styles);
let leading = elem.leading.resolve(styles);
self.output.push(Child::Rel(spacing.into(), 4));
@ -184,8 +186,8 @@ impl<'a> Collector<'a, '_, '_> {
/// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles);
let costs = TextElem::costs_in(styles);
let align = styles.resolve(AlignElem::alignment);
let costs = styles.get(TextElem::costs);
// Determine whether to prevent widow and orphans.
let len = lines.len();
@ -231,23 +233,23 @@ impl<'a> Collector<'a, '_, '_> {
/// whether it is breakable.
fn block(&mut self, elem: &'a Packed<BlockElem>, styles: StyleChain<'a>) {
let locator = self.locator.next(&elem.span());
let align = AlignElem::alignment_in(styles).resolve(styles);
let align = styles.resolve(AlignElem::alignment);
let alone = self.children.len() == 1;
let sticky = elem.sticky(styles);
let breakable = elem.breakable(styles);
let fr = match elem.height(styles) {
let sticky = elem.sticky.get(styles);
let breakable = elem.breakable.get(styles);
let fr = match elem.height.get(styles) {
Sizing::Fr(fr) => Some(fr),
_ => None,
};
let fallback = LazyCell::new(|| ParElem::spacing_in(styles));
let fallback = LazyCell::new(|| styles.resolve(ParElem::spacing));
let spacing = |amount| match amount {
Smart::Auto => Child::Rel((*fallback).into(), 4),
Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3),
Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr),
};
self.output.push(spacing(elem.above(styles)));
self.output.push(spacing(elem.above.get(styles)));
if !breakable || fr.is_some() {
self.output.push(Child::Single(self.boxed(SingleChild {
@ -272,7 +274,7 @@ impl<'a> Collector<'a, '_, '_> {
})));
};
self.output.push(spacing(elem.below(styles)));
self.output.push(spacing(elem.below.get(styles)));
self.par_situation = ParSituation::Other;
}
@ -282,13 +284,13 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<PlaceElem>,
styles: StyleChain<'a>,
) -> SourceResult<()> {
let alignment = elem.alignment(styles);
let alignment = elem.alignment.get(styles);
let align_x = alignment.map_or(FixedAlignment::Center, |align| {
align.x().unwrap_or_default().resolve(styles)
});
let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles)));
let scope = elem.scope(styles);
let float = elem.float(styles);
let scope = elem.scope.get(styles);
let float = elem.float.get(styles);
match (float, align_y) {
(true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!(
@ -312,8 +314,8 @@ impl<'a> Collector<'a, '_, '_> {
}
let locator = self.locator.next(&elem.span());
let clearance = elem.clearance(styles);
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let clearance = elem.clearance.resolve(styles);
let delta = Axes::new(elem.dx.get(styles), elem.dy.get(styles)).resolve(styles);
self.output.push(Child::Placed(self.boxed(PlacedChild {
align_x,
align_y,
@ -457,6 +459,7 @@ impl<'a> MultiChild<'a> {
regions: Regions,
) -> SourceResult<(Frame, Option<MultiSpill<'a, 'b>>)> {
let fragment = self.layout_full(engine, regions)?;
let exist_non_empty_frame = fragment.iter().any(|f| !f.is_empty());
// Extract the first frame.
let mut frames = fragment.into_iter();
@ -466,6 +469,7 @@ impl<'a> MultiChild<'a> {
let mut spill = None;
if frames.next().is_some() {
spill = Some(MultiSpill {
exist_non_empty_frame,
multi: self,
full: regions.full,
first: regions.size.y,
@ -537,6 +541,7 @@ fn layout_multi_impl(
/// The spilled remains of a `MultiChild` that broke across two regions.
#[derive(Debug, Clone)]
pub struct MultiSpill<'a, 'b> {
pub(super) exist_non_empty_frame: bool,
multi: &'b MultiChild<'a>,
first: Abs,
full: Abs,
@ -631,7 +636,7 @@ impl PlacedChild<'_> {
pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult<Frame> {
self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap();
let aligned = AlignElem::alignment.set(align).wrap();
let styles = self.styles.chain(&aligned);
let mut frame = layout_and_modify(styles, |styles| {

View File

@ -851,7 +851,7 @@ fn layout_line_number_reset(
config: &Config,
locator: &mut SplitLocator,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Set(CounterState::init(false));
let content = counter.update(Span::detached(), update);
crate::layout_frame(
@ -879,7 +879,7 @@ fn layout_line_number(
locator: &mut SplitLocator,
numbering: &Numbering,
) -> SourceResult<Frame> {
let counter = Counter::of(ParLineMarker::elem());
let counter = Counter::of(ParLineMarker::ELEM);
let update = CounterUpdate::Step(NonZeroUsize::ONE);
let numbering = Smart::Custom(numbering.clone());

View File

@ -283,6 +283,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
// Lay out the block.
let (frame, spill) = multi.layout(self.composer.engine, self.regions)?;
if frame.is_empty() && spill.as_ref().is_some_and(|s| s.exist_non_empty_frame) {
// If the first frame is empty, but there are non-empty frames in
// the spill, the whole child should be put in the next region to
// avoid any invisible orphans at the end of this region.
return Err(Stop::Finish(false));
}
self.frame(frame, multi.align, multi.sticky, true)?;
// If the block didn't fully fit into the current region, save it into

View File

@ -98,8 +98,8 @@ pub fn layout_columns(
locator.track(),
styles,
regions,
elem.count(styles),
elem.gutter(styles),
elem.count.get(styles),
elem.gutter.resolve(styles),
)
}
@ -251,22 +251,22 @@ fn configuration<'x>(
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
let dir = shared.resolve(TextElem::dir);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
separator: shared.get_cloned(FootnoteEntry::separator),
clearance: shared.resolve(FootnoteEntry::clearance),
gap: shared.resolve(FootnoteEntry::gap),
expand: regions.expand.x,
},
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
scope: shared.get(ParLine::numbering_scope),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
let width = if shared.get(PageElem::flipped) {
shared.resolve(PageElem::height)
} else {
PageElem::width_in(shared)
shared.resolve(PageElem::width)
};
// Clamp below is safe (min <= max): if the font size is

View File

@ -249,7 +249,7 @@ impl<'a> GridLayouter<'a> {
rowspans: vec![],
finished: vec![],
finished_header_rows: vec![],
is_rtl: TextElem::dir_in(styles) == Dir::RTL,
is_rtl: styles.resolve(TextElem::dir) == Dir::RTL,
repeating_headers: vec![],
upcoming_headers: &grid.headers,
pending_headers: Default::default(),

View File

@ -28,7 +28,7 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension,
// or try to detect the format.
let Derived { source, derived: loaded } = &elem.source;
let format = match elem.format(styles) {
let format = match elem.format.get(styles) {
Smart::Custom(v) => v,
Smart::Auto => determine_format(source, &loaded.data).at(span)?,
};
@ -55,7 +55,7 @@ pub fn layout_image(
RasterImage::new(
loaded.data.clone(),
format,
elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
elem.icc.get_ref(styles).as_ref().map(|icc| icc.derived.clone()),
)
.at(span)?,
),
@ -69,7 +69,7 @@ pub fn layout_image(
),
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
let image = Image::new(kind, elem.alt.get_cloned(styles), elem.scaling.get(styles));
// Determine the image's pixel aspect ratio.
let pxw = image.width();
@ -106,7 +106,7 @@ pub fn layout_image(
};
// Compute the actual size of the fitted image.
let fit = elem.fit(styles);
let fit = elem.fit.get(styles);
let fitted = match fit {
ImageFit::Cover | ImageFit::Contain => {
if wide == (fit == ImageFit::Contain) {

View File

@ -21,15 +21,15 @@ pub fn layout_box(
region: Size,
) -> SourceResult<Frame> {
// Fetch sizing properties.
let width = elem.width(styles);
let height = elem.height(styles);
let inset = elem.inset(styles).unwrap_or_default();
let width = elem.width.get(styles);
let height = elem.height.get(styles);
let inset = elem.inset.resolve(styles).unwrap_or_default();
// Build the pod region.
let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region);
// Layout the body.
let mut frame = match elem.body(styles) {
let mut frame = match elem.body.get_ref(styles) {
// If we have no body, just create an empty frame. If necessary,
// its size will be adjusted below.
None => Frame::hard(Size::zero()),
@ -50,18 +50,19 @@ pub fn layout_box(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill = elem.fill.get_cloned(styles);
let stroke = elem
.stroke(styles)
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|s| s.map(Stroke::unwrap_or_default));
// Only fetch these if necessary (for clipping or filling/stroking).
let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default());
let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default());
let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default());
// Clip the contents, if requested.
if elem.clip(styles) {
if elem.clip.get(styles) {
frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset));
}
@ -78,7 +79,7 @@ pub fn layout_box(
// Apply baseline shift. Do this after setting the size and applying the
// inset, so that a relative shift is resolved relative to the final
// height.
let shift = elem.baseline(styles).relative_to(frame.height());
let shift = elem.baseline.resolve(styles).relative_to(frame.height());
if !shift.is_zero() {
frame.set_baseline(frame.baseline() - shift);
}

View File

@ -144,7 +144,7 @@ pub fn collect<'a>(
collector.push_text(" ", styles);
} else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles);
let dir = styles.resolve(TextElem::dir);
if dir != config.dir {
// Insert "Explicit Directional Embedding".
match dir {
@ -154,7 +154,7 @@ pub fn collect<'a>(
}
}
if let Some(case) = TextElem::case_in(styles) {
if let Some(case) = styles.get(TextElem::case) {
full.push_str(&case.apply(&elem.text));
} else {
full.push_str(&elem.text);
@ -174,20 +174,22 @@ pub fn collect<'a>(
Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x),
elem.weak(styles),
elem.weak.get(styles),
),
});
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
collector
.push_text(if elem.justify(styles) { "\u{2028}" } else { "\n" }, styles);
collector.push_text(
if elem.justify.get(styles) { "\u{2028}" } else { "\n" },
styles,
);
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
let double = elem.double(styles);
if elem.enabled(styles) {
let double = elem.double.get(styles);
if elem.enabled.get(styles) {
let quotes = SmartQuotes::get(
elem.quotes(styles),
TextElem::lang_in(styles),
TextElem::region_in(styles),
elem.alternative(styles),
elem.quotes.get_ref(styles),
styles.get(TextElem::lang),
styles.get(TextElem::region),
elem.alternative.get(styles),
);
let before =
collector.full.chars().rev().find(|&c| !is_default_ignorable(c));
@ -215,7 +217,7 @@ pub fn collect<'a>(
collector.push_item(Item::Skip(POP_ISOLATE));
} else if let Some(elem) = child.to_packed::<BoxElem>() {
let loc = locator.next(&elem.span());
if let Sizing::Fr(v) = elem.width(styles) {
if let Sizing::Fr(v) = elem.width.get(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else {
let mut frame = layout_and_modify(styles, |styles| {

View File

@ -2,6 +2,7 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine;
use typst_library::foundations::Resolve;
use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::ParLineMarker;
@ -418,10 +419,11 @@ pub fn apply_shift<'a>(
frame: &mut Frame,
styles: StyleChain,
) {
let mut baseline = TextElem::baseline_in(styles);
let mut baseline = styles.resolve(TextElem::baseline);
let mut compensation = Abs::zero();
if let Some(scripts) = TextElem::shift_settings_in(styles) {
let font_metrics = TextElem::font_in(styles)
if let Some(scripts) = styles.get_ref(TextElem::shift_settings) {
let font_metrics = styles
.get_ref(TextElem::font)
.into_iter()
.find_map(|family| {
world
@ -462,7 +464,7 @@ pub fn commit(
if let Some(Item::Text(text)) = line.items.first() {
if let Some(glyph) = text.glyphs.first() {
if !text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& text.styles.get(TextElem::overhang)
&& (line.items.len() > 1 || text.glyphs.len() > 1)
{
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size);
@ -476,7 +478,7 @@ pub fn commit(
if let Some(Item::Text(text)) = line.items.last() {
if let Some(glyph) = text.glyphs.last() {
if text.dir.is_positive()
&& TextElem::overhang_in(text.styles)
&& text.styles.get(TextElem::overhang)
&& (line.items.len() > 1 || text.glyphs.len() > 1)
{
let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size);

View File

@ -846,7 +846,9 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset);
match item.text() {
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
Some(text) => {
text.styles.get(TextElem::hyphenate).unwrap_or(p.config.justify)
}
None => false,
}
})
@ -857,7 +859,7 @@ fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset);
let styles = item.text()?.styles;
Some(TextElem::lang_in(styles))
Some(styles.get(TextElem::lang))
})?;
let bytes = lang.as_str().as_bytes().try_into().ok()?;

View File

@ -14,7 +14,7 @@ pub use self::shaping::create_shape_plan;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::foundations::{Packed, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::{
@ -113,10 +113,10 @@ fn layout_par_impl(
expand,
Some(situation),
&ConfigBase {
justify: elem.justify(styles),
linebreaks: elem.linebreaks(styles),
first_line_indent: elem.first_line_indent(styles),
hanging_indent: elem.hanging_indent(styles),
justify: elem.justify.get(styles),
linebreaks: elem.linebreaks.get(styles),
first_line_indent: elem.first_line_indent.get(styles),
hanging_indent: elem.hanging_indent.resolve(styles),
},
)
}
@ -139,10 +139,10 @@ pub fn layout_inline<'a>(
expand,
None,
&ConfigBase {
justify: ParElem::justify_in(shared),
linebreaks: ParElem::linebreaks_in(shared),
first_line_indent: ParElem::first_line_indent_in(shared),
hanging_indent: ParElem::hanging_indent_in(shared),
justify: shared.get(ParElem::justify),
linebreaks: shared.get(ParElem::linebreaks),
first_line_indent: shared.get(ParElem::first_line_indent),
hanging_indent: shared.resolve(ParElem::hanging_indent),
},
)
}
@ -184,8 +184,8 @@ fn configuration(
situation: Option<ParSituation>,
) -> Config {
let justify = base.justify;
let font_size = TextElem::size_in(shared);
let dir = TextElem::dir_in(shared);
let font_size = shared.resolve(TextElem::size);
let dir = shared.resolve(TextElem::dir);
Config {
justify,
@ -207,7 +207,7 @@ fn configuration(
Some(ParSituation::Other) => all,
None => false,
}
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
&& shared.resolve(AlignElem::alignment).x == dir.start().into()
{
amount.at(font_size)
} else {
@ -219,26 +219,26 @@ fn configuration(
} else {
Abs::zero()
},
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
numbering_marker: shared.get_cloned(ParLine::numbering).map(|numbering| {
Packed::new(ParLineMarker::new(
numbering,
ParLine::number_align_in(shared),
ParLine::number_margin_in(shared),
shared.get(ParLine::number_align),
shared.get(ParLine::number_margin),
// Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying
// font size.
ParLine::number_clearance_in(shared),
shared.get(ParLine::number_clearance),
))
}),
align: AlignElem::alignment_in(shared).fix(dir).x,
align: shared.get(AlignElem::alignment).fix(dir).x,
font_size,
dir,
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
hyphenate: shared_get(children, shared, |s| s.get(TextElem::hyphenate))
.map(|uniform| uniform.unwrap_or(justify)),
lang: shared_get(children, shared, TextElem::lang_in),
fallback: TextElem::fallback_in(shared),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
costs: TextElem::costs_in(shared),
lang: shared_get(children, shared, |s| s.get(TextElem::lang)),
fallback: shared.get(TextElem::fallback),
cjk_latin_spacing: shared.get(TextElem::cjk_latin_spacing).is_auto(),
costs: shared.get(TextElem::costs),
}
}
@ -314,7 +314,7 @@ fn shared_get<T: PartialEq>(
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
styles.get(ListElem::depth).0 > 0
|| !styles.get_cloned(EnumElem::parents).is_empty()
|| styles.get(TermsElem::within)
}

View File

@ -223,12 +223,12 @@ impl<'a> ShapedText<'a> {
let mut frame = Frame::soft(size);
frame.set_baseline(top);
let size = TextElem::size_in(self.styles);
let shift = TextElem::baseline_in(self.styles);
let decos = TextElem::deco_in(self.styles);
let fill = TextElem::fill_in(self.styles);
let stroke = TextElem::stroke_in(self.styles);
let span_offset = TextElem::span_offset_in(self.styles);
let size = self.styles.resolve(TextElem::size);
let shift = self.styles.resolve(TextElem::baseline);
let decos = self.styles.get_cloned(TextElem::deco);
let fill = self.styles.get_ref(TextElem::fill);
let stroke = self.styles.resolve(TextElem::stroke);
let span_offset = self.styles.get(TextElem::span_offset);
for ((font, y_offset, glyph_size), group) in self
.glyphs
@ -340,9 +340,9 @@ impl<'a> ShapedText<'a> {
let mut top = Abs::zero();
let mut bottom = Abs::zero();
let size = TextElem::size_in(self.styles);
let top_edge = TextElem::top_edge_in(self.styles);
let bottom_edge = TextElem::bottom_edge_in(self.styles);
let size = self.styles.resolve(TextElem::size);
let top_edge = self.styles.get(TextElem::top_edge);
let bottom_edge = self.styles.get(TextElem::bottom_edge);
// Expand top and bottom by reading the font's vertical metrics.
let mut expand = |font: &Font, bounds: TextEdgeBounds| {
@ -486,7 +486,7 @@ impl<'a> ShapedText<'a> {
// that subtracting either of the endpoints by self.base doesn't
// underflow. See <https://github.com/typst/typst/issues/2283>.
.unwrap_or_else(|| self.base..self.base);
let size = TextElem::size_in(self.styles);
let size = self.styles.resolve(TextElem::size);
self.width += x_advance.at(size);
let glyph = ShapedGlyph {
font,
@ -603,9 +603,9 @@ pub fn shape_range<'a>(
range: Range,
styles: StyleChain<'a>,
) {
let script = TextElem::script_in(styles);
let lang = TextElem::lang_in(styles);
let region = TextElem::region_in(styles);
let script = styles.get(TextElem::script);
let lang = styles.get(TextElem::lang);
let region = styles.get(TextElem::region);
let mut process = |range: Range, level: BidiLevel| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped =
@ -669,8 +669,8 @@ fn shape<'a>(
lang: Lang,
region: Option<Region>,
) -> ShapedText<'a> {
let size = TextElem::size_in(styles);
let shift_settings = TextElem::shift_settings_in(styles);
let size = styles.resolve(TextElem::size);
let shift_settings = styles.get(TextElem::shift_settings);
let mut ctx = ShapingContext {
engine,
size,
@ -679,7 +679,7 @@ fn shape<'a>(
styles,
variant: variant(styles),
features: features(styles),
fallback: TextElem::fallback_in(styles),
fallback: styles.get(TextElem::fallback),
dir,
shift_settings,
};
@ -783,7 +783,7 @@ fn shape_segment<'a>(
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_language(language(ctx.styles));
if let Some(script) = TextElem::script_in(ctx.styles).custom().and_then(|script| {
if let Some(script) = ctx.styles.get(TextElem::script).custom().and_then(|script| {
rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes()))
}) {
buffer.set_script(script)
@ -1073,8 +1073,11 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
/// Apply tracking and spacing to the shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) {
let tracking = Em::from_abs(TextElem::tracking_in(ctx.styles), ctx.size);
let spacing = TextElem::spacing_in(ctx.styles).map(|abs| Em::from_abs(abs, ctx.size));
let tracking = Em::from_abs(ctx.styles.resolve(TextElem::tracking), ctx.size);
let spacing = ctx
.styles
.resolve(TextElem::spacing)
.map(|abs| Em::from_abs(abs, ctx.size));
let mut glyphs = ctx.glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {

View File

@ -10,21 +10,11 @@ mod modifiers;
mod pad;
mod pages;
mod repeat;
mod rules;
mod shapes;
mod stack;
mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image;
pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad;
pub use self::flow::{layout_fragment, layout_frame};
pub use self::pages::layout_document;
pub use self::repeat::layout_repeat;
pub use self::shapes::{
layout_circle, layout_curve, layout_ellipse, layout_line, layout_path,
layout_polygon, layout_rect, layout_square,
};
pub use self::stack::layout_stack;
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};
pub use self::rules::register;

View File

@ -20,20 +20,21 @@ pub fn layout_list(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
let indent = elem.indent.get(styles);
let body_indent = elem.body_indent.get(styles);
let tight = elem.tight.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight {
ParElem::leading_in(styles).into()
styles.get(ParElem::leading)
} else {
ParElem::spacing_in(styles).into()
styles.get(ParElem::spacing)
}
});
let Depth(depth) = ListElem::depth_in(styles);
let Depth(depth) = styles.get(ListElem::depth);
let marker = elem
.marker(styles)
.marker
.get_ref(styles)
.resolve(engine, styles, depth)?
// avoid '#set align' interference with the list
.aligned(HAlignment::Start + VAlignment::Top);
@ -52,7 +53,7 @@ pub fn layout_list(
cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
body.styled(ListElem::set_depth(Depth(1))),
body.set(ListElem::depth, Depth(1)),
locator.next(&item.body.span()),
));
}
@ -81,40 +82,40 @@ pub fn layout_enum(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let numbering = elem.numbering(styles);
let reversed = elem.reversed(styles);
let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| {
let numbering = elem.numbering.get_ref(styles);
let reversed = elem.reversed.get(styles);
let indent = elem.indent.get(styles);
let body_indent = elem.body_indent.get(styles);
let tight = elem.tight.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight {
ParElem::leading_in(styles).into()
styles.get(ParElem::leading)
} else {
ParElem::spacing_in(styles).into()
styles.get(ParElem::spacing)
}
});
let mut cells = vec![];
let mut locator = locator.split();
let mut number = elem.start(styles).unwrap_or_else(|| {
let mut number = elem.start.get(styles).unwrap_or_else(|| {
if reversed {
elem.children.len() as u64
} else {
1
}
});
let mut parents = EnumElem::parents_in(styles);
let mut parents = styles.get_cloned(EnumElem::parents);
let full = elem.full(styles);
let full = elem.full.get(styles);
// Horizontally align based on the given respective parameter.
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
// alignment from the context and having the number be displaced in
// relation to the item it refers to.
let number_align = elem.number_align(styles);
let number_align = elem.number_align.get(styles);
for item in &elem.children {
number = item.number(styles).unwrap_or(number);
number = item.number.get(styles).unwrap_or(number);
let context = Context::new(None, Some(styles));
let resolved = if full {
@ -133,8 +134,7 @@ pub fn layout_enum(
// Disable overhang as a workaround to end-aligned dots glitching
// and decreasing spacing between numbers and items.
let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false));
let resolved = resolved.aligned(number_align).set(TextElem::overhang, false);
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
@ -146,7 +146,7 @@ pub fn layout_enum(
cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(
body.styled(EnumElem::set_parents(smallvec![number])),
body.set(EnumElem::parents, smallvec![number]),
locator.next(&item.body.span()),
));
number =

View File

@ -24,7 +24,7 @@ pub fn layout_accent(
// Try to replace the base glyph with its dotless variant.
let dtls = style_dtls();
let base_styles =
if top_accent && elem.dotless(styles) { styles.chain(&dtls) } else { styles };
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped();
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
@ -47,7 +47,7 @@ pub fn layout_accent(
// Forcing the accent to be at least as large as the base makes it too wide
// in many cases.
let width = elem.size(styles).relative_to(base.width());
let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
glyph.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0;

View File

@ -31,16 +31,16 @@ pub fn layout_attach(
let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain);
let tr = elem.tr(sup_style_chain);
let tl = elem.tl.get_cloned(sup_style_chain);
let tr = elem.tr.get_cloned(sup_style_chain);
let primed = tr.as_ref().is_some_and(|content| content.is::<PrimesElem>());
let t = elem.t(sup_style_chain);
let t = elem.t.get_cloned(sup_style_chain);
let sub_style = style_for_subscript(styles);
let sub_style_chain = styles.chain(&sub_style);
let bl = elem.bl(sub_style_chain);
let br = elem.br(sub_style_chain);
let b = elem.b(sub_style_chain);
let bl = elem.bl.get_cloned(sub_style_chain);
let br = elem.br.get_cloned(sub_style_chain);
let b = elem.b.get_cloned(sub_style_chain);
let limits = base.limits().active(styles);
let (t, tr) = match (t, tr) {
@ -146,7 +146,7 @@ pub fn layout_limits(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
let limits = if elem.inline.get(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits);
ctx.push(fragment);
@ -161,7 +161,8 @@ fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs
base = &equation.body;
}
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
base.to_packed::<StretchElem>()
.map(|stretch| stretch.size.resolve(styles))
}
/// Lay out the attachments.
@ -397,7 +398,7 @@ fn compute_script_shifts(
base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) {
let sup_shift_up = if EquationElem::cramped_in(styles) {
let sup_shift_up = if styles.get(EquationElem::cramped) {
scaled!(ctx, styles, superscript_shift_up_cramped)
} else {
scaled!(ctx, styles, superscript_shift_up)

View File

@ -27,16 +27,16 @@ pub fn layout_cancel(
let mut body = body.into_frame();
let body_size = body.size();
let span = elem.span();
let length = elem.length(styles);
let length = elem.length.resolve(styles);
let stroke = elem.stroke(styles).unwrap_or(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(),
let stroke = elem.stroke.resolve(styles).unwrap_or(FixedStroke {
paint: styles.get_ref(TextElem::fill).as_decoration(),
..Default::default()
});
let invert = elem.inverted(styles);
let cross = elem.cross(styles);
let angle = elem.angle(styles);
let invert = elem.inverted.get(styles);
let cross = elem.cross.get(styles);
let angle = elem.angle.get_ref(styles);
let invert_first_line = !cross && invert;
let first_line = draw_cancel_line(
@ -44,7 +44,7 @@ pub fn layout_cancel(
length,
stroke.clone(),
invert_first_line,
&angle,
angle,
body_size,
styles,
span,
@ -57,7 +57,7 @@ pub fn layout_cancel(
if cross {
// Draw the second line.
let second_line =
draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?;
draw_cancel_line(ctx, length, stroke, true, angle, body_size, styles, span)?;
body.push_frame(center, second_line);
}

View File

@ -124,7 +124,7 @@ fn layout_frac_like(
FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked(
FixedStroke::from_pair(
TextElem::fill_in(styles).as_decoration(),
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),

View File

@ -315,7 +315,8 @@ impl GlyphFragment {
let cluster = info.cluster as usize;
let c = text[cluster..].chars().next().unwrap();
let limits = Limits::for_char(c);
let class = EquationElem::class_in(styles)
let class = styles
.get(EquationElem::class)
.or_else(|| default_math_class(c))
.unwrap_or(MathClass::Normal);
@ -331,11 +332,11 @@ impl GlyphFragment {
let item = TextItem {
font: font.clone(),
size: TextElem::size_in(styles),
fill: TextElem::fill_in(styles).as_decoration(),
stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()),
lang: TextElem::lang_in(styles),
region: TextElem::region_in(styles),
size: styles.resolve(TextElem::size),
fill: styles.get_ref(TextElem::fill).as_decoration(),
stroke: styles.resolve(TextElem::stroke).map(|s| s.unwrap_or_default()),
lang: styles.get(TextElem::lang),
region: styles.get(TextElem::region),
text: text.into(),
glyphs: vec![glyph.clone()],
};
@ -344,7 +345,7 @@ impl GlyphFragment {
item,
base_glyph: glyph,
// Math
math_size: EquationElem::size_in(styles),
math_size: styles.get(EquationElem::size),
class,
limits,
mid_stretched: None,
@ -356,7 +357,7 @@ impl GlyphFragment {
baseline: None,
// Misc
align: Abs::zero(),
shift: TextElem::baseline_in(styles),
shift: styles.resolve(TextElem::baseline),
modifiers: FrameModifiers::get_in(styles),
};
fragment.update_glyph();
@ -541,9 +542,9 @@ impl FrameFragment {
let accent_attach = frame.width() / 2.0;
Self {
frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: TextElem::size_in(styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles),
font_size: styles.resolve(TextElem::size),
class: styles.get(EquationElem::class).unwrap_or(MathClass::Normal),
math_size: styles.get(EquationElem::size),
limits: Limits::Never,
spaced: false,
base_ascent,
@ -864,7 +865,7 @@ impl Limits {
pub fn active(&self, styles: StyleChain) -> bool {
match self {
Self::Always => true,
Self::Display => EquationElem::size_in(styles) == MathSize::Display,
Self::Display => styles.get(EquationElem::size) == MathSize::Display,
Self::Never => false,
}
}

View File

@ -22,7 +22,7 @@ pub fn layout_lr(
// Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_one() {
if lr.size.get(styles).is_one() {
body = &lr.body;
}
}
@ -41,7 +41,7 @@ pub fn layout_lr(
.unwrap_or_default();
let relative_to = 2.0 * max_extent;
let height = elem.size(styles);
let height = elem.size.resolve(styles);
// Scale up fragments at both ends.
match inner_fragments {

View File

@ -30,15 +30,15 @@ pub fn layout_vec(
ctx,
styles,
&[column],
elem.align(styles),
elem.align.resolve(styles),
LeftRightAlternator::Right,
None,
Axes::with_y(elem.gap(styles)),
Axes::with_y(elem.gap.resolve(styles)),
span,
"elements",
)?;
let delim = elem.delim(styles);
let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
}
@ -59,14 +59,17 @@ pub fn layout_cases(
FixedAlignment::Start,
LeftRightAlternator::None,
None,
Axes::with_y(elem.gap(styles)),
Axes::with_y(elem.gap.resolve(styles)),
span,
"branches",
)?;
let delim = elem.delim(styles);
let (open, close) =
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
let delim = elem.delim.get(styles);
let (open, close) = if elem.reverse.get(styles) {
(None, delim.close())
} else {
(delim.open(), None)
};
layout_delimiters(ctx, styles, frame, open, close, span)
}
@ -81,7 +84,7 @@ pub fn layout_mat(
let rows = &elem.rows;
let ncols = rows.first().map_or(0, |row| row.len());
let augment = elem.augment(styles);
let augment = elem.augment.resolve(styles);
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() {
@ -116,15 +119,15 @@ pub fn layout_mat(
ctx,
styles,
&columns,
elem.align(styles),
elem.align.resolve(styles),
LeftRightAlternator::Right,
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
Axes::new(elem.column_gap.resolve(styles), elem.row_gap.resolve(styles)),
span,
"cells",
)?;
let delim = elem.delim(styles);
let delim = elem.delim.get(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
}
@ -157,7 +160,7 @@ fn layout_body(
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles);
let default_stroke = FixedStroke {
thickness: default_stroke_thickness,
paint: TextElem::fill_in(styles).as_decoration(),
paint: styles.get_ref(TextElem::fill).as_decoration(),
cap: LineCap::Square,
..Default::default()
};

View File

@ -51,7 +51,7 @@ pub fn layout_equation_inline(
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block(styles));
assert!(!elem.block.get(styles));
let font = find_math_font(engine, styles, elem.span())?;
@ -78,12 +78,12 @@ pub fn layout_equation_inline(
for item in &mut items {
let InlineItem::Frame(frame) = item else { continue };
let slack = ParElem::leading_in(styles) * 0.7;
let slack = styles.resolve(ParElem::leading) * 0.7;
let (t, b) = font.edges(
TextElem::top_edge_in(styles),
TextElem::bottom_edge_in(styles),
TextElem::size_in(styles),
styles.get(TextElem::top_edge),
styles.get(TextElem::bottom_edge),
styles.resolve(TextElem::size),
TextEdgeBounds::Frame(frame),
);
@ -105,7 +105,7 @@ pub fn layout_equation_block(
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
assert!(elem.block(styles));
assert!(elem.block.get(styles));
let span = elem.span();
let font = find_math_font(engine, styles, span)?;
@ -121,7 +121,7 @@ pub fn layout_equation_block(
.multiline_frame_builder(styles);
let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) {
let equation_builders = if styles.get(BlockElem::breakable) {
let mut rows = full_equation_builder.frames.into_iter().peekable();
let mut equation_builders = vec![];
let mut last_first_pos = Point::zero();
@ -188,7 +188,7 @@ pub fn layout_equation_block(
vec![full_equation_builder]
};
let Some(numbering) = (**elem).numbering(styles) else {
let Some(numbering) = elem.numbering.get_ref(styles) else {
let frames = equation_builders
.into_iter()
.map(MathRunFrameBuilder::build)
@ -197,7 +197,7 @@ pub fn layout_equation_block(
};
let pod = Region::new(regions.base(), Axes::splat(false));
let counter = Counter::of(EquationElem::elem())
let counter = Counter::of(EquationElem::ELEM)
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span);
let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
@ -205,7 +205,7 @@ pub fn layout_equation_block(
static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
let number_align = match elem.number_align(styles) {
let number_align = match elem.number_align.get(styles) {
SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon),
SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v),
SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v),
@ -224,7 +224,7 @@ pub fn layout_equation_block(
builder,
number.clone(),
number_align.resolve(styles),
AlignElem::alignment_in(styles).resolve(styles).x,
styles.get(AlignElem::alignment).resolve(styles).x,
regions.size.x,
full_number_width,
)
@ -472,7 +472,9 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
let outer = styles;
for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
@ -603,7 +605,10 @@ fn layout_h(
) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount {
if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
ctx.push(MathFragment::Spacing(
rel.abs.resolve(styles),
elem.weak.get(styles),
));
}
}
Ok(())
@ -616,7 +621,7 @@ fn layout_class(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let style = EquationElem::set_class(Some(elem.class)).wrap();
let style = EquationElem::class.set(Some(elem.class)).wrap();
let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
fragment.set_class(elem.class);
fragment.set_limits(Limits::for_class(elem.class));
@ -642,7 +647,7 @@ fn layout_op(
.with_italics_correction(italics)
.with_accent_attach(accent_attach)
.with_text_like(text_like)
.with_limits(if elem.limits(styles) {
.with_limits(if elem.limits.get(styles) {
Limits::Display
} else {
Limits::Never

View File

@ -17,7 +17,7 @@ pub fn layout_root(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let index = elem.index(styles);
let index = elem.index.get_ref(styles);
let span = elem.span();
let gap = scaled!(
@ -54,7 +54,7 @@ pub fn layout_root(
let sqrt = sqrt.into_frame();
// Layout the index.
let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap();
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
let index = index
.as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
@ -112,7 +112,7 @@ pub fn layout_root(
FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(
FixedStroke::from_pair(
TextElem::fill_in(styles).as_decoration(),
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),

View File

@ -194,13 +194,13 @@ impl MathRun {
let row_count = rows.len();
let alignments = alignments(&rows);
let leading = if EquationElem::size_in(styles) >= MathSize::Text {
ParElem::leading_in(styles)
let leading = if styles.get(EquationElem::size) >= MathSize::Text {
styles.resolve(ParElem::leading)
} else {
TIGHT_LEADING.resolve(styles)
};
let align = AlignElem::alignment_in(styles).resolve(styles).x;
let align = styles.resolve(AlignElem::alignment).x;
let mut frames: Vec<(Frame, Point)> = vec![];
let mut size = Size::zero();
for (i, row) in rows.into_iter().enumerate() {

View File

@ -10,7 +10,7 @@ use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match typst_library::math::EquationElem::size_in($styles) {
match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
_ => scaled!($ctx, $styles, $text),
}
@ -19,7 +19,7 @@ macro_rules! scaled {
$crate::math::Scaled::scaled(
$ctx.constants.$name(),
$ctx,
typst_library::text::TextElem::size_in($styles),
$styles.resolve(typst_library::text::TextElem::size),
)
};
}
@ -58,55 +58,62 @@ impl Scaled for MathValue<'_> {
/// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap()
EquationElem::cramped.set(true).wrap()
}
/// Sets flac OpenType feature.
pub fn style_flac() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)])).wrap()
TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"flac"), 1)]))
.wrap()
}
/// Sets dtls OpenType feature.
pub fn style_dtls() -> LazyHash<Style> {
TextElem::set_features(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)])).wrap()
TextElem::features
.set(FontFeatures(vec![(Tag::from_bytes(b"dtls"), 1)]))
.wrap()
}
/// The style for subscripts in the current style.
pub fn style_for_subscript(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_superscript(styles), EquationElem::set_cramped(true).wrap()]
[style_for_superscript(styles), EquationElem::cramped.set(true).wrap()]
}
/// The style for superscripts in the current style.
pub fn style_for_superscript(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
EquationElem::size
.set(match styles.get(EquationElem::size) {
MathSize::Display | MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for numerators in the current style.
pub fn style_for_numerator(styles: StyleChain) -> LazyHash<Style> {
EquationElem::set_size(match EquationElem::size_in(styles) {
MathSize::Display => MathSize::Text,
MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
EquationElem::size
.set(match styles.get(EquationElem::size) {
MathSize::Display => MathSize::Text,
MathSize::Text => MathSize::Script,
MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
})
.wrap()
}
/// The style for denominators in the current style.
pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()]
[style_for_numerator(styles), EquationElem::cramped.set(true).wrap()]
}
/// Styles to add font constants to the style chain.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
EquationElem::set_script_scale((
ctx.constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(),
))
.wrap()
EquationElem::script_scale
.set((
ctx.constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(),
))
.wrap()
}
/// Stack rows on top of each other.

View File

@ -14,7 +14,14 @@ pub fn layout_stretch(
styles: StyleChain,
) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment(ctx, &mut fragment, None, None, elem.size(styles), Abs::zero());
stretch_fragment(
ctx,
&mut fragment,
None,
None,
elem.size.resolve(styles),
Abs::zero(),
);
ctx.push(fragment);
Ok(())
}

View File

@ -77,8 +77,8 @@ fn layout_inline_text(
Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
TextElem::top_edge.set(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::bottom_edge.set(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
]
.map(|p| p.wrap());
@ -150,7 +150,7 @@ fn adjust_glyph_layout(
styles: StyleChain,
) {
if glyph.class == MathClass::Large {
if EquationElem::size_in(styles) == MathSize::Display {
if styles.get(EquationElem::size) == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
@ -169,9 +169,9 @@ fn adjust_glyph_layout(
fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
use MathVariant::*;
let variant = EquationElem::variant_in(styles);
let bold = EquationElem::bold_in(styles);
let italic = EquationElem::italic_in(styles).unwrap_or(
let variant = styles.get(EquationElem::variant);
let bold = styles.get(EquationElem::bold);
let italic = styles.get(EquationElem::italic).unwrap_or(
auto_italic
&& matches!(
c,

View File

@ -56,7 +56,7 @@ pub fn layout_underbrace(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏟',
BRACE_GAP,
Position::Under,
@ -75,7 +75,7 @@ pub fn layout_overbrace(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏞',
BRACE_GAP,
Position::Over,
@ -94,7 +94,7 @@ pub fn layout_underbracket(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⎵',
BRACKET_GAP,
Position::Under,
@ -113,7 +113,7 @@ pub fn layout_overbracket(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⎴',
BRACKET_GAP,
Position::Over,
@ -132,7 +132,7 @@ pub fn layout_underparen(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏝',
PAREN_GAP,
Position::Under,
@ -151,7 +151,7 @@ pub fn layout_overparen(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏜',
PAREN_GAP,
Position::Over,
@ -170,7 +170,7 @@ pub fn layout_undershell(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏡',
SHELL_GAP,
Position::Under,
@ -189,7 +189,7 @@ pub fn layout_overshell(
ctx,
styles,
&elem.body,
&elem.annotation(styles),
elem.annotation.get_ref(styles),
'⏠',
SHELL_GAP,
Position::Over,
@ -251,7 +251,7 @@ fn layout_underoverline(
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(),
paint: styles.get_ref(TextElem::fill).as_decoration(),
thickness: bar_height,
..FixedStroke::default()
}),

View File

@ -29,8 +29,8 @@ impl FrameModifiers {
/// Retrieve all modifications that should be applied per-frame.
pub fn get_in(styles: StyleChain) -> Self {
Self {
dest: LinkElem::current_in(styles),
hidden: HideElem::hidden_in(styles),
dest: styles.get_cloned(LinkElem::current),
hidden: styles.get(HideElem::hidden),
}
}
}
@ -83,7 +83,7 @@ pub trait FrameModifyText {
impl FrameModifyText for Frame {
fn modify_text(&mut self, styles: StyleChain) {
let modifiers = FrameModifiers::get_in(styles);
let expand_y = 0.5 * ParElem::leading_in(styles);
let expand_y = 0.5 * styles.resolve(ParElem::leading);
let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y);
modify_frame(self, &modifiers, Some(outset));
}
@ -130,7 +130,7 @@ where
let outer = styles;
let mut styles = styles;
if modifiers.dest.is_some() {
reset = LinkElem::set_current(None).wrap();
reset = LinkElem::current.set(None).wrap();
styles = outer.chain(&reset);
}

View File

@ -1,6 +1,6 @@
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Resolve, StyleChain};
use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator;
use typst_library::layout::{
Abs, Fragment, Frame, PadElem, Point, Regions, Rel, Sides, Size,
@ -16,10 +16,10 @@ pub fn layout_pad(
regions: Regions,
) -> SourceResult<Fragment> {
let padding = Sides::new(
elem.left(styles).resolve(styles),
elem.top(styles).resolve(styles),
elem.right(styles).resolve(styles),
elem.bottom(styles).resolve(styles),
elem.left.resolve(styles),
elem.top.resolve(styles),
elem.right.resolve(styles),
elem.bottom.resolve(styles),
);
let mut backlog = vec![];

View File

@ -39,14 +39,14 @@ pub fn collect<'a>(
if let Some(pagebreak) = elem.to_packed::<PagebreakElem>() {
// Add a blank page if we encounter a strong pagebreak and there was
// a staged empty page.
let strong = !pagebreak.weak(styles);
let strong = !pagebreak.weak.get(styles);
if strong && staged_empty_page {
let locator = locator.next(&elem.span());
items.push(Item::Run(&[], initial, locator));
}
// Add an instruction to adjust the page parity if requested.
if let Some(parity) = pagebreak.to(styles) {
if let Some(parity) = pagebreak.to.get(styles) {
let locator = locator.next(&elem.span());
items.push(Item::Parity(parity, styles, locator));
}
@ -56,7 +56,7 @@ pub fn collect<'a>(
// the scope of a page set rule to ensure a page boundary. Its
// styles correspond to the styles _before_ the page set rule, so we
// don't want to apply it to a potential empty page.
if !pagebreak.boundary(styles) {
if !pagebreak.boundary.get(styles) {
initial = styles;
}
@ -94,7 +94,7 @@ pub fn collect<'a>(
if group.iter().all(|(c, _)| c.is::<TagElem>())
&& !(staged_empty_page
&& children.iter().all(|&(c, s)| {
c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary(s))
c.to_packed::<PagebreakElem>().is_some_and(|c| c.boundary.get(s))
}))
{
items.push(Item::Tags(group));

View File

@ -101,10 +101,10 @@ fn layout_page_run_impl(
// When one of the lengths is infinite the page fits its content along
// that axis.
let width = PageElem::width_in(styles).unwrap_or(Abs::inf());
let height = PageElem::height_in(styles).unwrap_or(Abs::inf());
let width = styles.resolve(PageElem::width).unwrap_or(Abs::inf());
let height = styles.resolve(PageElem::height).unwrap_or(Abs::inf());
let mut size = Size::new(width, height);
if PageElem::flipped_in(styles) {
if styles.get(PageElem::flipped) {
std::mem::swap(&mut size.x, &mut size.y);
}
@ -115,7 +115,7 @@ fn layout_page_run_impl(
// Determine the margins.
let default = Rel::<Length>::from((2.5 / 21.0) * min);
let margin = PageElem::margin_in(styles);
let margin = styles.get(PageElem::margin);
let two_sided = margin.two_sided.unwrap_or(false);
let margin = margin
.sides
@ -123,22 +123,24 @@ fn layout_page_run_impl(
.resolve(styles)
.relative_to(size);
let fill = PageElem::fill_in(styles);
let foreground = PageElem::foreground_in(styles);
let background = PageElem::background_in(styles);
let header_ascent = PageElem::header_ascent_in(styles).relative_to(margin.top);
let footer_descent = PageElem::footer_descent_in(styles).relative_to(margin.bottom);
let numbering = PageElem::numbering_in(styles);
let supplement = match PageElem::supplement_in(styles) {
let fill = styles.get_cloned(PageElem::fill);
let foreground = styles.get_ref(PageElem::foreground);
let background = styles.get_ref(PageElem::background);
let header_ascent = styles.resolve(PageElem::header_ascent).relative_to(margin.top);
let footer_descent =
styles.resolve(PageElem::footer_descent).relative_to(margin.bottom);
let numbering = styles.get_ref(PageElem::numbering);
let supplement = match styles.get_cloned(PageElem::supplement) {
Smart::Auto => TextElem::packed(PageElem::local_name_in(styles)),
Smart::Custom(content) => content.unwrap_or_default(),
};
let number_align = PageElem::number_align_in(styles);
let binding =
PageElem::binding_in(styles).unwrap_or_else(|| match TextElem::dir_in(styles) {
let number_align = styles.get(PageElem::number_align);
let binding = styles.get(PageElem::binding).unwrap_or_else(|| {
match styles.resolve(TextElem::dir) {
Dir::LTR => Binding::Left,
_ => Binding::Right,
});
}
});
// Construct the numbering (for header or footer).
let numbering_marginal = numbering.as_ref().map(|numbering| {
@ -163,8 +165,8 @@ fn layout_page_run_impl(
counter
});
let header = PageElem::header_in(styles);
let footer = PageElem::footer_in(styles);
let header = styles.get_ref(PageElem::header);
let footer = styles.get_ref(PageElem::footer);
let (header, footer) = if matches!(number_align.y(), Some(OuterVAlignment::Top)) {
(header.as_ref().unwrap_or(&numbering_marginal), footer.as_ref().unwrap_or(&None))
} else {
@ -179,15 +181,15 @@ fn layout_page_run_impl(
&mut locator,
styles,
Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles),
styles.get(PageElem::columns),
styles.get(ColumnsElem::gutter).resolve(styles),
FlowMode::Root,
)?;
// Layouts a single marginal.
let mut layout_marginal = |content: &Option<Content>, area, align| {
let Some(content) = content else { return Ok(None) };
let aligned = content.clone().styled(AlignElem::set_alignment(align));
let aligned = content.clone().set(AlignElem::alignment, align);
crate::layout_frame(
&mut engine,
&aligned,

View File

@ -29,7 +29,7 @@ pub fn layout_repeat(
frame.set_baseline(piece.baseline());
}
let mut gap = elem.gap(styles).resolve(styles);
let mut gap = elem.gap.resolve(styles);
let fill = region.size.x;
let width = piece.width();
@ -47,12 +47,12 @@ pub fn layout_repeat(
let count = ((fill + gap) / (width + gap)).floor();
let remaining = (fill + gap) % (width + gap);
let justify = elem.justify(styles);
let justify = elem.justify.get(styles);
if justify {
gap += remaining / (count - 1.0);
}
let align = AlignElem::alignment_in(styles).resolve(styles);
let align = styles.get(AlignElem::alignment).resolve(styles);
let mut offset = Abs::zero();
if count == 1.0 || !justify {
offset += align.x.position(remaining);

View File

@ -0,0 +1,890 @@
use std::num::NonZeroUsize;
use comemo::Track;
use ecow::{eco_format, EcoVec};
use smallvec::smallvec;
use typst_library::diag::{bail, At, SourceResult};
use typst_library::foundations::{
dict, Content, Context, NativeElement, NativeRuleMap, Packed, Resolve, ShowFn, Smart,
StyleChain, Target,
};
use typst_library::introspection::{Counter, Locator, LocatorLink};
use typst_library::layout::{
Abs, AlignElem, Alignment, Axes, BlockBody, BlockElem, ColumnsElem, Em, GridCell,
GridChild, GridElem, GridItem, HAlignment, HElem, HideElem, InlineElem, LayoutElem,
Length, MoveElem, OuterVAlignment, PadElem, PlaceElem, PlacementScope, Region, Rel,
RepeatElem, RotateElem, ScaleElem, Sides, Size, Sizing, SkewElem, Spacing,
StackChild, StackElem, TrackSizings, VElem,
};
use typst_library::math::EquationElem;
use typst_library::model::{
Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem,
EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem,
LinkElem, LinkTarget, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem,
ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
};
use typst_library::pdf::EmbedElem;
use typst_library::text::{
DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName,
OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem,
SpaceElem, StrikeElem, SubElem, SuperElem, TextElem, TextSize, UnderlineElem,
WeightDelta,
};
use typst_library::visualize::{
CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem,
RectElem, SquareElem, Stroke,
};
use typst_utils::{Get, NonZeroExt, Numeric};
/// Register show rules for the [paged target](Target::Paged).
pub fn register(rules: &mut NativeRuleMap) {
use Target::Paged;
// Model.
rules.register(Paged, STRONG_RULE);
rules.register(Paged, EMPH_RULE);
rules.register(Paged, LIST_RULE);
rules.register(Paged, ENUM_RULE);
rules.register(Paged, TERMS_RULE);
rules.register(Paged, LINK_RULE);
rules.register(Paged, HEADING_RULE);
rules.register(Paged, FIGURE_RULE);
rules.register(Paged, FIGURE_CAPTION_RULE);
rules.register(Paged, QUOTE_RULE);
rules.register(Paged, FOOTNOTE_RULE);
rules.register(Paged, FOOTNOTE_ENTRY_RULE);
rules.register(Paged, OUTLINE_RULE);
rules.register(Paged, OUTLINE_ENTRY_RULE);
rules.register(Paged, REF_RULE);
rules.register(Paged, CITE_GROUP_RULE);
rules.register(Paged, BIBLIOGRAPHY_RULE);
rules.register(Paged, TABLE_RULE);
rules.register(Paged, TABLE_CELL_RULE);
// Text.
rules.register(Paged, SUB_RULE);
rules.register(Paged, SUPER_RULE);
rules.register(Paged, UNDERLINE_RULE);
rules.register(Paged, OVERLINE_RULE);
rules.register(Paged, STRIKE_RULE);
rules.register(Paged, HIGHLIGHT_RULE);
rules.register(Paged, SMALLCAPS_RULE);
rules.register(Paged, RAW_RULE);
rules.register(Paged, RAW_LINE_RULE);
// Layout.
rules.register(Paged, ALIGN_RULE);
rules.register(Paged, PAD_RULE);
rules.register(Paged, COLUMNS_RULE);
rules.register(Paged, STACK_RULE);
rules.register(Paged, GRID_RULE);
rules.register(Paged, GRID_CELL_RULE);
rules.register(Paged, MOVE_RULE);
rules.register(Paged, SCALE_RULE);
rules.register(Paged, ROTATE_RULE);
rules.register(Paged, SKEW_RULE);
rules.register(Paged, REPEAT_RULE);
rules.register(Paged, HIDE_RULE);
rules.register(Paged, LAYOUT_RULE);
// Visualize.
rules.register(Paged, IMAGE_RULE);
rules.register(Paged, LINE_RULE);
rules.register(Paged, RECT_RULE);
rules.register(Paged, SQUARE_RULE);
rules.register(Paged, ELLIPSE_RULE);
rules.register(Paged, CIRCLE_RULE);
rules.register(Paged, POLYGON_RULE);
rules.register(Paged, CURVE_RULE);
rules.register(Paged, PATH_RULE);
// Math.
rules.register(Paged, EQUATION_RULE);
// PDF.
rules.register(Paged, EMBED_RULE);
}
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, styles| {
Ok(elem
.body
.clone()
.set(TextElem::delta, WeightDelta(elem.delta.get(styles))))
};
const EMPH_RULE: ShowFn<EmphElem> =
|elem, _, _| Ok(elem.body.clone().set(TextElem::emph, ItalicToggle(true)));
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
let tight = elem.tight.get(styles);
let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_list)
.pack()
.spanned(elem.span());
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
};
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let tight = elem.tight.get(styles);
let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_enum)
.pack()
.spanned(elem.span());
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
};
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
let span = elem.span();
let tight = elem.tight.get(styles);
let separator = elem.separator.get_ref(styles);
let indent = elem.indent.get(styles);
let hanging_indent = elem.hanging_indent.get(styles);
let gutter = elem.spacing.get(styles).unwrap_or_else(|| {
if tight {
styles.get(ParElem::leading)
} else {
styles.get(ParElem::spacing)
}
});
let pad = hanging_indent + indent;
let unpad = (!hanging_indent.is_zero())
.then(|| HElem::new((-hanging_indent).into()).pack().spanned(span));
let mut children = vec![];
for child in elem.children.iter() {
let mut seq = vec![];
seq.extend(unpad.clone());
seq.push(child.term.clone().strong());
seq.push(separator.clone());
seq.push(child.description.clone());
// Text in wide term lists shall always turn into paragraphs.
if !tight {
seq.push(ParbreakElem::shared().clone());
}
children.push(StackChild::Block(Content::sequence(seq)));
}
let padding =
Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into());
let mut realized = StackElem::new(children)
.with_spacing(Some(gutter.into()))
.pack()
.spanned(span)
.padded(padding)
.set(TermsElem::within, true);
if tight {
let spacing = elem
.spacing
.get(styles)
.unwrap_or_else(|| styles.get(ParElem::leading));
let v = VElem::new(spacing.into())
.with_weak(true)
.with_attach(true)
.pack()
.spanned(span);
realized = v + realized;
}
Ok(realized)
};
const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
let body = elem.body.clone();
Ok(match &elem.dest {
LinkTarget::Dest(dest) => body.linked(dest.clone()),
LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(elem.span())?;
let dest = Destination::Location(elem.location().unwrap());
body.linked(dest)
}
})
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = elem.span();
let mut realized = elem.body.clone();
let hanging_indent = elem.hanging_indent.get(styles);
let mut indent = match hanging_indent {
Smart::Custom(length) => length.resolve(styles),
Smart::Auto => Abs::zero(),
};
if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() {
let location = elem.location().unwrap();
let numbering = Counter::of(HeadingElem::ELEM)
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
if hanging_indent.is_auto() {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
// We don't have a locator for the numbering here, so we just
// use the measurement infrastructure for now.
let link = LocatorLink::measure(location);
let size = (engine.routines.layout_frame)(
engine,
&numbering,
Locator::link(&link),
styles,
pod,
)?
.size();
indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
}
let spacing = HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack();
realized = numbering + spacing + realized;
}
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(styles.resolve(TextElem::dir).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
Ok(block.pack().spanned(span))
};
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
// Build the caption, if any.
if let Some(caption) = elem.caption.get_cloned(styles) {
let (first, second) = match caption.position.get(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
};
realized = Content::sequence(vec![
first,
VElem::new(elem.gap.get(styles).into())
.with_weak(true)
.pack()
.spanned(span),
second,
]);
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
// Wrap the contents in a block.
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(span);
// Wrap in a float.
if let Some(align) = elem.placement.get(styles) {
realized = PlaceElem::new(realized)
.with_alignment(align.map(|align| HAlignment::Center + align))
.with_scope(elem.scope.get(styles))
.with_float(true)
.pack()
.spanned(span);
} else if elem.scope.get(styles) == PlacementScope::Parent {
bail!(
span,
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
}
Ok(realized)
};
const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
Ok(BlockElem::new()
.with_body(Some(BlockBody::Content(elem.realize(engine, styles)?)))
.pack()
.spanned(elem.span()))
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
let span = elem.span();
let block = elem.block.get(styles);
let mut realized = elem.body.clone();
if elem.quotes.get(styles).unwrap_or(!block) {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
let sticky = Content::sequence([hole.clone(), realized, hole]);
realized = QuoteElem::quoted(sticky, styles);
}
let attribution = elem.attribution.get_ref(styles);
if block {
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(span);
if let Some(attribution) = attribution.as_ref() {
// Bring the attribution a bit closer to the quote.
let gap = Spacing::Rel(Em::new(0.9).into());
let v = VElem::new(gap).with_weak(true).pack();
realized += v;
realized += BlockElem::new()
.with_body(Some(BlockBody::Content(attribution.realize(span))))
.pack()
.aligned(Alignment::END);
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone();
realized += CiteElem::new(*label).pack().spanned(span);
}
Ok(realized)
};
const FOOTNOTE_RULE: ShowFn<FootnoteElem> = |elem, engine, styles| {
let span = elem.span();
let loc = elem.declaration_location(engine).at(span)?;
let numbering = elem.numbering.get_ref(styles);
let counter = Counter::of(FootnoteElem::ELEM);
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1);
// Add zero-width weak spacing to make the footnote "sticky".
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
};
const FOOTNOTE_ENTRY_RULE: ShowFn<FootnoteEntry> = |elem, engine, styles| {
let span = elem.span();
let number_gap = Em::new(0.05);
let default = StyleChain::default();
let numbering = elem.note.numbering.get_ref(default);
let counter = Counter::of(FootnoteElem::ELEM);
let Some(loc) = elem.note.location() else {
bail!(
span, "footnote entry must have a location";
hint: "try using a query or a show rule to customize the footnote instead"
);
};
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num)
.pack()
.spanned(span)
.linked(Destination::Location(loc))
.located(loc.variant(1));
Ok(Content::sequence([
HElem::new(elem.indent.get(styles).into()).pack(),
sup,
HElem::new(number_gap.into()).with_weak(true).pack(),
elem.note.body_content().unwrap().clone(),
]))
};
const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
let span = elem.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = elem.title.get_cloned(styles).unwrap_or_else(|| {
Some(TextElem::packed(Packed::<OutlineElem>::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let elems = engine.introspector.query(&elem.target.get_ref(styles).0);
let depth = elem.depth.get(styles).unwrap_or(NonZeroUsize::MAX);
// Build the outline entries.
for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name());
};
let level = outlinable.level();
if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
};
const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {
let span = elem.span();
let context = Context::new(None, Some(styles));
let context = context.track();
let prefix = elem.prefix(engine, context, span)?;
let inner = elem.inner(engine, context, span)?;
let block = if elem.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
elem.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = elem.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
};
const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| elem.realize(engine);
const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let span = elem.span();
let mut seq = vec![];
if let Some(title) = elem.title.get_ref(styles).clone().unwrap_or_else(|| {
Some(
TextElem::packed(Packed::<BibliographyElem>::local_name_in(styles))
.spanned(span),
)
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let works = Works::generate(engine).at(span)?;
let references = works
.references
.as_ref()
.ok_or_else(|| match elem.style.get_ref(styles).source {
CslSource::Named(style) => eco_format!(
"CSL style \"{}\" is not suitable for bibliographies",
style.display_name()
),
CslSource::Normal(..) => {
"CSL style is not suitable for bibliographies".into()
}
})
.at(span)?;
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let row_gutter = styles.get(ParElem::spacing);
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
.spanned(span),
)));
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(reference.clone())).spanned(span),
)));
}
seq.push(
GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack()
.spanned(span),
);
} else {
for (_, reference) in references {
let realized = reference.clone();
let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default()
.with(styles.resolve(TextElem::dir).start(), Some(INDENT.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
seq.push(block.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
};
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table)
.pack()
.spanned(elem.span()))
};
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles))
};
const SUB_RULE: ShowFn<SubElem> = |elem, _, styles| {
show_script(
styles,
elem.body.clone(),
elem.typographic.get(styles),
elem.baseline.get(styles),
elem.size.get(styles),
ScriptKind::Sub,
)
};
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, styles| {
show_script(
styles,
elem.body.clone(),
elem.typographic.get(styles),
elem.baseline.get(styles),
elem.size.get(styles),
ScriptKind::Super,
)
};
fn show_script(
styles: StyleChain,
body: Content,
typographic: bool,
baseline: Smart<Length>,
size: Smart<TextSize>,
kind: ScriptKind,
) -> SourceResult<Content> {
let font_size = styles.resolve(TextElem::size);
Ok(body.set(
TextElem::shift_settings,
Some(ShiftSettings {
typographic,
shift: baseline.map(|l| -Em::from_length(l, font_size)),
size: size.map(|t| Em::from_length(t.0, font_size)),
kind,
}),
))
}
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Underline {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
evade: elem.evade.get(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const OVERLINE_RULE: ShowFn<OverlineElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Overline {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
evade: elem.evade.get(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const STRIKE_RULE: ShowFn<StrikeElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
// Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough {
stroke: elem.stroke.resolve(styles).unwrap_or_default(),
offset: elem.offset.resolve(styles),
background: elem.background.get(styles),
},
extent: elem.extent.resolve(styles),
}],
))
};
const HIGHLIGHT_RULE: ShowFn<HighlightElem> = |elem, _, styles| {
Ok(elem.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Highlight {
fill: elem.fill.get_cloned(styles),
stroke: elem
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|stroke| stroke.map(Stroke::unwrap_or_default)),
top_edge: elem.top_edge.get(styles),
bottom_edge: elem.bottom_edge.get(styles),
radius: elem.radius.resolve(styles).unwrap_or_default(),
},
extent: elem.extent.resolve(styles),
}],
))
};
const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
let sc = if elem.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules };
Ok(elem.body.clone().set(TextElem::smallcaps, Some(sc)))
};
const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
let lines = elem.lines.as_deref().unwrap_or_default();
let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
for (i, line) in lines.iter().enumerate() {
if i != 0 {
seq.push(LinebreakElem::shared().clone());
}
seq.push(line.clone().pack());
}
let mut realized = Content::sequence(seq);
if elem.block.get(styles) {
// Align the text before inserting it into the block.
realized = realized.aligned(elem.align.get(styles).into());
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(elem.span());
}
Ok(realized)
};
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
const ALIGN_RULE: ShowFn<AlignElem> =
|elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles)));
const PAD_RULE: ShowFn<PadElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad)
.pack()
.spanned(elem.span()))
};
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns)
.pack()
.spanned(elem.span()))
};
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack)
.pack()
.spanned(elem.span()))
};
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid)
.pack()
.spanned(elem.span()))
};
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles))
};
/// Function with common code to display a grid cell or table cell.
fn show_cell(
mut body: Content,
inset: Smart<Sides<Option<Rel<Length>>>>,
align: Smart<Alignment>,
) -> SourceResult<Content> {
let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
if inset != Sides::default() {
// Only pad if some inset is not 0pt.
// Avoids a bug where using .padded() in any way inside Show causes
// alignment in align(...) to break.
body = body.padded(inset);
}
if let Smart::Custom(alignment) = align {
body = body.aligned(alignment);
}
Ok(body)
}
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move)
.pack()
.spanned(elem.span()))
};
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale)
.pack()
.spanned(elem.span()))
};
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate)
.pack()
.spanned(elem.span()))
};
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew)
.pack()
.spanned(elem.span()))
};
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat)
.pack()
.spanned(elem.span()))
};
const HIDE_RULE: ShowFn<HideElem> =
|elem, _, _| Ok(elem.body.clone().set(HideElem::hidden, true));
const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(
elem.clone(),
|elem, engine, locator, styles, regions| {
// Gets the current region's base size, which will be the size of the
// outer container, or of the page if there is no such container.
let Size { x, y } = regions.base();
let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = elem
.func
.call(engine, context.track(), [dict! { "width" => x, "height" => y }])?
.display();
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
},
)
.pack()
.spanned(elem.span()))
};
const IMAGE_RULE: ShowFn<ImageElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack()
.spanned(elem.span()))
};
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line)
.pack()
.spanned(elem.span()))
};
const RECT_RULE: ShowFn<RectElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack()
.spanned(elem.span()))
};
const SQUARE_RULE: ShowFn<SquareElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack()
.spanned(elem.span()))
};
const ELLIPSE_RULE: ShowFn<EllipseElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack()
.spanned(elem.span()))
};
const CIRCLE_RULE: ShowFn<CircleElem> = |elem, _, styles| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle)
.with_width(elem.width.get(styles))
.with_height(elem.height.get(styles))
.pack()
.spanned(elem.span()))
};
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon)
.pack()
.spanned(elem.span()))
};
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve)
.pack()
.spanned(elem.span()))
};
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path)
.pack()
.spanned(elem.span()))
};
const EQUATION_RULE: ShowFn<EquationElem> = |elem, _, styles| {
if elem.block.get(styles) {
Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block)
.pack()
.spanned(elem.span()))
} else {
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline)
.pack()
.spanned(elem.span()))
}
};
const EMBED_RULE: ShowFn<EmbedElem> = |_, _, _| Ok(Content::empty());

View File

@ -27,16 +27,20 @@ pub fn layout_line(
region: Region,
) -> SourceResult<Frame> {
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
let start = resolve(elem.start(styles));
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
let length = elem.length(styles);
let angle = elem.angle(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let start = resolve(elem.start.resolve(styles));
let delta = elem
.end
.resolve(styles)
.map(|end| resolve(end) - start)
.unwrap_or_else(|| {
let length = elem.length.resolve(styles);
let angle = elem.angle.get(styles);
let x = angle.cos() * length;
let y = angle.sin() * length;
resolve(Axes::new(x, y))
});
let stroke = elem.stroke(styles).unwrap_or_default();
let stroke = elem.stroke.resolve(styles).unwrap_or_default();
let size = start.max(start + delta).max(Size::zero());
if !size.is_finite() {
@ -105,7 +109,7 @@ pub fn layout_path(
add_cubic(from_point, to_point, from, to);
}
if elem.closed(styles) {
if elem.closed.get(styles) {
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
let to = vertices[0];
let from_point = *points.last().unwrap();
@ -120,9 +124,9 @@ pub fn layout_path(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -153,19 +157,19 @@ pub fn layout_curve(
for item in &elem.components {
match item {
CurveComponent::Move(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let point = builder.resolve_point(element.start, relative);
builder.move_(point);
}
CurveComponent::Line(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let point = builder.resolve_point(element.end, relative);
builder.line(point);
}
CurveComponent::Quad(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative);
let control = match element.control {
Smart::Auto => {
@ -178,7 +182,7 @@ pub fn layout_curve(
}
CurveComponent::Cubic(element) => {
let relative = element.relative(styles);
let relative = element.relative.get(styles);
let end = builder.resolve_point(element.end, relative);
let c1 = match element.control_start {
Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
@ -193,7 +197,7 @@ pub fn layout_curve(
}
CurveComponent::Close(element) => {
builder.close(element.mode(styles));
builder.close(element.mode.get(styles));
}
}
}
@ -208,9 +212,9 @@ pub fn layout_curve(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -418,9 +422,9 @@ pub fn layout_polygon(
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
let fill = elem.fill.get_cloned(styles);
let fill_rule = elem.fill_rule.get(styles);
let stroke = match elem.stroke.resolve(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
@ -459,12 +463,12 @@ pub fn layout_rect(
styles,
region,
ShapeKind::Rect,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
elem.radius.resolve(styles),
elem.span(),
)
}
@ -484,12 +488,12 @@ pub fn layout_square(
styles,
region,
ShapeKind::Square,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles),
elem.inset(styles),
elem.outset(styles),
elem.radius(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
elem.radius.resolve(styles),
elem.span(),
)
}
@ -509,11 +513,11 @@ pub fn layout_ellipse(
styles,
region,
ShapeKind::Ellipse,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
Corners::splat(None),
elem.span(),
)
@ -534,11 +538,11 @@ pub fn layout_circle(
styles,
region,
ShapeKind::Circle,
elem.body(styles),
elem.fill(styles),
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
elem.inset(styles),
elem.outset(styles),
elem.body.get_ref(styles),
elem.fill.get_cloned(styles),
elem.stroke.resolve(styles).map(|s| Sides::splat(Some(s))),
elem.inset.resolve(styles),
elem.outset.resolve(styles),
Corners::splat(None),
elem.span(),
)

View File

@ -19,12 +19,12 @@ pub fn layout_stack(
regions: Regions,
) -> SourceResult<Fragment> {
let mut layouter =
StackLayouter::new(elem.span(), elem.dir(styles), locator, styles, regions);
StackLayouter::new(elem.span(), elem.dir.get(styles), locator, styles, regions);
let axis = layouter.dir.axis();
// Spacing to insert before the next block.
let spacing = elem.spacing(styles);
let spacing = elem.spacing.get(styles);
let mut deferred = None;
for child in &elem.children {
@ -167,11 +167,11 @@ impl<'a> StackLayouter<'a> {
// Block-axis alignment of the `AlignElem` is respected by stacks.
let align = if let Some(align) = block.to_packed::<AlignElem>() {
align.alignment(styles)
align.alignment.get(styles)
} else if let Some(styled) = block.to_packed::<StyledElem>() {
AlignElem::alignment_in(styles.chain(&styled.styles))
styles.chain(&styled.styles).get(AlignElem::alignment)
} else {
AlignElem::alignment_in(styles)
styles.get(AlignElem::alignment)
}
.resolve(styles);

View File

@ -20,7 +20,7 @@ pub fn layout_move(
region: Region,
) -> SourceResult<Frame> {
let mut frame = crate::layout_frame(engine, &elem.body, locator, styles, region)?;
let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles);
let delta = Axes::new(elem.dx.resolve(styles), elem.dy.resolve(styles));
let delta = delta.zip_map(region.size, Rel::relative_to);
frame.translate(delta.to_point());
Ok(frame)
@ -35,8 +35,8 @@ pub fn layout_rotate(
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let angle = elem.angle(styles);
let align = elem.origin(styles).resolve(styles);
let angle = elem.angle.get(styles);
let align = elem.origin.resolve(styles);
// Compute the new region's approximate size.
let is_finite = region.size.is_finite();
@ -55,7 +55,7 @@ pub fn layout_rotate(
&elem.body,
Transform::rotate(angle),
align,
elem.reflow(styles),
elem.reflow.get(styles),
)
}
@ -83,8 +83,8 @@ pub fn layout_scale(
styles,
&elem.body,
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
elem.origin.resolve(styles),
elem.reflow.get(styles),
)
}
@ -121,13 +121,13 @@ fn resolve_scale(
});
let x = resolve_axis(
elem.x(styles),
elem.x.get(styles),
|| size.as_ref().map(|size| size.x).map_err(Clone::clone),
styles,
)?;
let y = resolve_axis(
elem.y(styles),
elem.y.get(styles),
|| size.as_ref().map(|size| size.y).map_err(Clone::clone),
styles,
)?;
@ -152,9 +152,9 @@ pub fn layout_skew(
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let ax = elem.ax(styles);
let ay = elem.ay(styles);
let align = elem.origin(styles).resolve(styles);
let ax = elem.ax.get(styles);
let ay = elem.ay.get(styles);
let align = elem.origin.resolve(styles);
// Compute the new region's approximate size.
let size = if region.size.is_finite() {
@ -172,7 +172,7 @@ pub fn layout_skew(
&elem.body,
Transform::skew(ax, ay),
align,
elem.reflow(styles),
elem.reflow.get(styles),
)
}

View File

@ -2,52 +2,54 @@ use std::any::TypeId;
use std::cmp::Ordering;
use std::fmt::{self, Debug};
use std::hash::Hash;
use std::ptr::NonNull;
use std::sync::LazyLock;
use std::sync::OnceLock;
use ecow::EcoString;
use smallvec::SmallVec;
#[doc(inline)]
pub use typst_macros::elem;
use typst_utils::Static;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, Args, Content, Dict, FieldAccessError, Func, ParamInfo, Repr, Scope, Selector,
StyleChain, Styles, Value,
cast, Args, Content, ContentVtable, FieldAccessError, Func, ParamInfo, Repr, Scope,
Selector, StyleChain, Styles, Value,
};
use crate::text::{Lang, Region};
/// A document element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Element(Static<NativeElementData>);
pub struct Element(Static<ContentVtable>);
impl Element {
/// Get the element for `T`.
pub fn of<T: NativeElement>() -> Self {
T::elem()
pub const fn of<T: NativeElement>() -> Self {
T::ELEM
}
/// Get the element for `T`.
pub const fn from_vtable(vtable: &'static ContentVtable) -> Self {
Self(Static(vtable))
}
/// The element's normal name (e.g. `enum`).
pub fn name(self) -> &'static str {
self.0.name
self.vtable().name
}
/// The element's title case name, for use in documentation
/// (e.g. `Numbered List`).
pub fn title(&self) -> &'static str {
self.0.title
self.vtable().title
}
/// Documentation for the element (as Markdown).
pub fn docs(&self) -> &'static str {
self.0.docs
self.vtable().docs
}
/// Search keywords for the element.
pub fn keywords(&self) -> &'static [&'static str] {
self.0.keywords
self.vtable().keywords
}
/// Construct an instance of this element.
@ -56,12 +58,12 @@ impl Element {
engine: &mut Engine,
args: &mut Args,
) -> SourceResult<Content> {
(self.0.construct)(engine, args)
(self.vtable().construct)(engine, args)
}
/// Execute the set rule for the element and return the resulting style map.
pub fn set(self, engine: &mut Engine, mut args: Args) -> SourceResult<Styles> {
let styles = (self.0.set)(engine, &mut args)?;
let styles = (self.vtable().set)(engine, &mut args)?;
args.finish()?;
Ok(styles)
}
@ -77,12 +79,7 @@ impl Element {
/// Whether the element has the given capability where the capability is
/// given by a `TypeId`.
pub fn can_type_id(self, type_id: TypeId) -> bool {
(self.0.vtable)(type_id).is_some()
}
/// The VTable for capabilities dispatch.
pub fn vtable(self) -> fn(of: TypeId) -> Option<NonNull<()>> {
self.0.vtable
(self.vtable().capability)(type_id).is_some()
}
/// Create a selector for this element.
@ -98,12 +95,29 @@ impl Element {
/// The element's associated scope of sub-definition.
pub fn scope(&self) -> &'static Scope {
&(self.0).0.scope
(self.vtable().store)().scope.get_or_init(|| (self.vtable().scope)())
}
/// Details about the element's fields.
pub fn params(&self) -> &'static [ParamInfo] {
&(self.0).0.params
(self.vtable().store)().params.get_or_init(|| {
self.vtable()
.fields
.iter()
.filter(|field| !field.synthesized)
.map(|field| ParamInfo {
name: field.name,
docs: field.docs,
input: (field.input)(),
default: field.default,
positional: field.positional,
named: !field.positional,
variadic: field.variadic,
required: field.required,
settable: field.settable,
})
.collect()
})
}
/// Extract the field ID for the given field name.
@ -111,7 +125,7 @@ impl Element {
if name == "label" {
return Some(255);
}
(self.0.field_id)(name)
(self.vtable().field_id)(name)
}
/// Extract the field name for the given field ID.
@ -119,7 +133,7 @@ impl Element {
if id == 255 {
return Some("label");
}
(self.0.field_name)(id)
self.vtable().field(id).map(|data| data.name)
}
/// Extract the value of the field for the given field ID and style chain.
@ -128,12 +142,20 @@ impl Element {
id: u8,
styles: StyleChain,
) -> Result<Value, FieldAccessError> {
(self.0.field_from_styles)(id, styles)
self.vtable()
.field(id)
.and_then(|field| (field.get_from_styles)(styles))
.ok_or(FieldAccessError::Unknown)
}
/// The element's local name, if any.
pub fn local_name(&self, lang: Lang, region: Option<Region>) -> Option<&'static str> {
(self.0).0.local_name.map(|f| f(lang, region))
self.vtable().local_name.map(|f| f(lang, region))
}
/// Retrieves the element's vtable for dynamic dispatch.
pub(super) fn vtable(&self) -> &'static ContentVtable {
(self.0).0
}
}
@ -167,84 +189,34 @@ cast! {
v: Func => v.element().ok_or("expected element")?,
}
/// A Typst element that is defined by a native Rust type.
pub trait NativeElement:
Debug
+ Clone
+ PartialEq
+ Hash
+ Construct
+ Set
+ Capable
+ Fields
+ Repr
+ Send
+ Sync
+ 'static
{
/// Get the element for the native Rust element.
fn elem() -> Element
where
Self: Sized,
{
Element::from(Self::data())
}
/// Pack the element into type-erased content.
fn pack(self) -> Content
where
Self: Sized,
{
Content::new(self)
}
/// Get the element data for the native Rust element.
fn data() -> &'static NativeElementData
where
Self: Sized;
/// Lazily initialized data for an element.
#[derive(Default)]
pub struct LazyElementStore {
pub scope: OnceLock<Scope>,
pub params: OnceLock<Vec<ParamInfo>>,
}
/// Used to cast an element to a trait object for a trait it implements.
impl LazyElementStore {
/// Create an empty store.
pub const fn new() -> Self {
Self { scope: OnceLock::new(), params: OnceLock::new() }
}
}
/// A Typst element that is defined by a native Rust type.
///
/// # Safety
/// If the `vtable` function returns `Some(p)`, then `p` must be a valid pointer
/// to a vtable of `Packed<Self>` w.r.t to the trait `C` where `capability` is
/// `TypeId::of::<dyn C>()`.
pub unsafe trait Capable {
/// Get the pointer to the vtable for the given capability / trait.
fn vtable(capability: TypeId) -> Option<NonNull<()>>;
}
/// `ELEM` must hold the correct `Element` for `Self`.
pub unsafe trait NativeElement:
Debug + Clone + Hash + Construct + Set + Send + Sync + 'static
{
/// The associated element.
const ELEM: Element;
/// Defines how fields of an element are accessed.
pub trait Fields {
/// An enum with the fields of the element.
type Enum
where
Self: Sized;
/// Whether the element has the given field set.
fn has(&self, id: u8) -> bool;
/// Get the field with the given field ID.
fn field(&self, id: u8) -> Result<Value, FieldAccessError>;
/// Get the field with the given ID in the presence of styles.
fn field_with_styles(
&self,
id: u8,
styles: StyleChain,
) -> Result<Value, FieldAccessError>;
/// Get the field with the given ID from the styles.
fn field_from_styles(id: u8, styles: StyleChain) -> Result<Value, FieldAccessError>
where
Self: Sized;
/// Resolve all fields with the styles and save them in-place.
fn materialize(&mut self, styles: StyleChain);
/// Get the fields of the element.
fn fields(&self) -> Dict;
/// Pack the element into type-erased content.
fn pack(self) -> Content {
Content::new(self)
}
}
/// An element's constructor function.
@ -266,48 +238,6 @@ pub trait Set {
Self: Sized;
}
/// Defines a native element.
#[derive(Debug)]
pub struct NativeElementData {
/// The element's normal name (e.g. `align`), as exposed to Typst.
pub name: &'static str,
/// The element's title case name (e.g. `Align`).
pub title: &'static str,
/// The documentation for this element as a string.
pub docs: &'static str,
/// A list of alternate search terms for this element.
pub keywords: &'static [&'static str],
/// The constructor for this element (see [`Construct`]).
pub construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>,
/// Executes this element's set rule (see [`Set`]).
pub set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>,
/// Gets the vtable for one of this element's capabilities
/// (see [`Capable`]).
pub vtable: fn(capability: TypeId) -> Option<NonNull<()>>,
/// Gets the numeric index of this field by its name.
pub field_id: fn(name: &str) -> Option<u8>,
/// Gets the name of a field by its numeric index.
pub field_name: fn(u8) -> Option<&'static str>,
/// Get the field with the given ID in the presence of styles (see [`Fields`]).
pub field_from_styles: fn(u8, StyleChain) -> Result<Value, FieldAccessError>,
/// Gets the localized name for this element (see [`LocalName`][crate::text::LocalName]).
pub local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
pub scope: LazyLock<Scope>,
/// A list of parameter information for each field.
pub params: LazyLock<Vec<ParamInfo>>,
}
impl From<&'static NativeElementData> for Element {
fn from(data: &'static NativeElementData) -> Self {
Self(Static(data))
}
}
cast! {
&'static NativeElementData,
self => Element::from(self).into_value(),
}
/// Synthesize fields on an element. This happens before execution of any show
/// rule.
pub trait Synthesize {
@ -316,12 +246,6 @@ pub trait Synthesize {
-> SourceResult<()>;
}
/// Defines a built-in show rule for an element.
pub trait Show {
/// Execute the base recipe for this element.
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content>;
}
/// Defines built-in show set rules for an element.
///
/// This is a bit more powerful than a user-defined show-set because it can
@ -331,3 +255,9 @@ pub trait ShowSet {
/// that should work even in the face of a user-defined show rule.
fn show_set(&self, styles: StyleChain) -> Styles;
}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
fn plain_text(&self, text: &mut EcoString);
}

View File

@ -0,0 +1,564 @@
use std::fmt::{self, Debug};
use std::hash::Hash;
use std::marker::PhantomData;
use std::sync::OnceLock;
use ecow::{eco_format, EcoString};
use crate::foundations::{
Container, Content, FieldVtable, Fold, FoldFn, IntoValue, NativeElement, Packed,
Property, Reflect, Repr, Resolve, StyleChain,
};
/// An accessor for the `I`-th field of the element `E`. Values of this type are
/// generated for each field of an element can be used to interact with this
/// field programmatically, for example to access the style chain, as in
/// `styles.get(TextElem::size)`.
#[derive(Copy, Clone)]
pub struct Field<E: NativeElement, const I: u8>(pub PhantomData<E>);
impl<E: NativeElement, const I: u8> Field<E, I> {
/// Creates a new zero-sized accessor.
pub const fn new() -> Self {
Self(PhantomData)
}
/// The index of the projected field.
pub const fn index(self) -> u8 {
I
}
/// Creates a dynamic property instance for this field.
///
/// Prefer [`Content::set`] or
/// [`Styles::set`](crate::foundations::Styles::set) when working with
/// existing content or style value.
pub fn set(self, value: E::Type) -> Property
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
Property::new(self, value)
}
}
impl<E: NativeElement, const I: u8> Default for Field<E, I> {
fn default() -> Self {
Self::new()
}
}
/// A field that is present on every instance of the element.
pub trait RequiredField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: RequiredFieldData<Self, I>;
}
/// Metadata and routines for a [`RequiredField`].
pub struct RequiredFieldData<E: RequiredField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
get: fn(&E) -> &E::Type,
}
impl<E: RequiredField<I>, const I: u8> RequiredFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
get: fn(&E) -> &E::Type,
) -> Self {
Self { name, docs, get }
}
/// Creates the vtable for a `#[required]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: RequiredField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: true,
required: true,
variadic: false,
settable: false,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: None,
has: |_| true,
get: |elem| Some((E::FIELD.get)(elem).clone().into_value()),
get_with_styles: |elem, _| Some((E::FIELD.get)(elem).clone().into_value()),
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |a, b| (E::FIELD.get)(a) == (E::FIELD.get)(b),
}
}
/// Creates the vtable for a `#[variadic]` field.
pub const fn vtable_variadic() -> FieldVtable<Packed<E>>
where
E: RequiredField<I>,
E::Type: Container + IntoValue + PartialEq,
<E::Type as Container>::Inner: Reflect,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: true,
required: true,
variadic: true,
settable: false,
synthesized: false,
input: || <<E::Type as Container>::Inner as Reflect>::input(),
default: None,
has: |_| true,
get: |elem| Some((E::FIELD.get)(elem).clone().into_value()),
get_with_styles: |elem, _| Some((E::FIELD.get)(elem).clone().into_value()),
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |a, b| (E::FIELD.get)(a) == (E::FIELD.get)(b),
}
}
}
/// A field that is initially unset, but may be set through a
/// [`Synthesize`](crate::foundations::Synthesize) implementation.
pub trait SynthesizedField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SynthesizedFieldData<Self, I>;
}
/// Metadata and routines for a [`SynthesizedField`].
pub struct SynthesizedFieldData<E: SynthesizedField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
get: fn(&E) -> &Option<E::Type>,
}
impl<E: SynthesizedField<I>, const I: u8> SynthesizedFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
get: fn(&E) -> &Option<E::Type>,
) -> Self {
Self { name, docs, get }
}
/// Creates type-erased metadata and routines for a `#[synthesized]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SynthesizedField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: false,
required: false,
variadic: false,
settable: false,
synthesized: true,
input: || <E::Type as Reflect>::input(),
default: None,
has: |elem| (E::FIELD.get)(elem).is_some(),
get: |elem| (E::FIELD.get)(elem).clone().map(|v| v.into_value()),
get_with_styles: |elem, _| {
(E::FIELD.get)(elem).clone().map(|v| v.into_value())
},
get_from_styles: |_| None,
materialize: |_, _| {},
// Synthesized fields don't affect equality.
eq: |_, _| true,
}
}
}
/// A field that is not actually there. It's only visible in the docs.
pub trait ExternalField<const I: u8>: NativeElement {
type Type;
const FIELD: ExternalFieldData<Self, I>;
}
/// Metadata for an [`ExternalField`].
pub struct ExternalFieldData<E: ExternalField<I>, const I: u8> {
name: &'static str,
docs: &'static str,
default: fn() -> E::Type,
}
impl<E: ExternalField<I>, const I: u8> ExternalFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
default: fn() -> E::Type,
) -> Self {
Self { name, docs, default }
}
/// Creates type-erased metadata and routines for an `#[external]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: ExternalField<I>,
E::Type: Reflect + IntoValue,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: false,
required: false,
variadic: false,
settable: false,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| (E::FIELD.default)().into_value()),
has: |_| false,
get: |_| None,
get_with_styles: |_, _| None,
get_from_styles: |_| None,
materialize: |_, _| {},
eq: |_, _| true,
}
}
}
/// A field that has a default value and can be configured via a set rule, but
/// can also present on elements and be present in the constructor.
pub trait SettableField<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SettableFieldData<Self, I>;
}
/// Metadata and routines for a [`SettableField`].
pub struct SettableFieldData<E: SettableField<I>, const I: u8> {
get: fn(&E) -> &Settable<E, I>,
get_mut: fn(&mut E) -> &mut Settable<E, I>,
property: SettablePropertyData<E, I>,
}
impl<E: SettableField<I>, const I: u8> SettableFieldData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
positional: bool,
get: fn(&E) -> &Settable<E, I>,
get_mut: fn(&mut E) -> &mut Settable<E, I>,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
) -> Self {
Self {
get,
get_mut,
property: SettablePropertyData::new(name, docs, positional, default, slot),
}
}
/// Ensures that the property is folded on every access. See the
/// documentation of the [`Fold`] trait for more details.
pub const fn with_fold(mut self) -> Self
where
E::Type: Fold,
{
self.property.fold = Some(E::Type::fold);
self
}
/// Creates type-erased metadata and routines for a normal settable field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SettableField<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.property.name,
docs: E::FIELD.property.docs,
positional: E::FIELD.property.positional,
required: false,
variadic: false,
settable: true,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| E::default().into_value()),
has: |elem| (E::FIELD.get)(elem).is_set(),
get: |elem| (E::FIELD.get)(elem).as_option().clone().map(|v| v.into_value()),
get_with_styles: |elem, styles| {
Some((E::FIELD.get)(elem).get_cloned(styles).into_value())
},
get_from_styles: |styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
materialize: |elem, styles| {
if !(E::FIELD.get)(elem).is_set() {
(E::FIELD.get_mut)(elem).set(styles.get_cloned::<E, I>(Field::new()));
}
},
eq: |a, b| (E::FIELD.get)(a).as_option() == (E::FIELD.get)(b).as_option(),
}
}
}
/// A field that has a default value and can be configured via a set rule, but
/// is never present on elements.
///
/// This is provided for all `SettableField` impls through a blanket impl. In
/// the case of `#[ghost]` fields, which only live in the style chain and not in
/// elements, it is also implemented manually.
pub trait SettableProperty<const I: u8>: NativeElement {
type Type: Clone;
const FIELD: SettablePropertyData<Self, I>;
const FOLD: Option<FoldFn<Self::Type>> = Self::FIELD.fold;
/// Produces an instance of the property's default value.
fn default() -> Self::Type {
// Avoid recreating an expensive instance over and over, but also
// avoid unnecessary lazy initialization for cheap types.
if std::mem::needs_drop::<Self::Type>() {
Self::default_ref().clone()
} else {
(Self::FIELD.default)()
}
}
/// Produces a static reference to this property's default value.
fn default_ref() -> &'static Self::Type {
(Self::FIELD.slot)().get_or_init(Self::FIELD.default)
}
}
impl<T, const I: u8> SettableProperty<I> for T
where
T: SettableField<I>,
{
type Type = <Self as SettableField<I>>::Type;
const FIELD: SettablePropertyData<Self, I> =
<Self as SettableField<I>>::FIELD.property;
}
/// Metadata and routines for a [`SettableProperty`].
pub struct SettablePropertyData<E: SettableProperty<I>, const I: u8> {
name: &'static str,
docs: &'static str,
positional: bool,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
fold: Option<FoldFn<E::Type>>,
}
impl<E: SettableProperty<I>, const I: u8> SettablePropertyData<E, I> {
/// Creates the data from its parts. This is called in the `#[elem]` macro.
pub const fn new(
name: &'static str,
docs: &'static str,
positional: bool,
default: fn() -> E::Type,
slot: fn() -> &'static OnceLock<E::Type>,
) -> Self {
Self { name, docs, positional, default, slot, fold: None }
}
/// Ensures that the property is folded on every access. See the
/// documentation of the [`Fold`] trait for more details.
pub const fn with_fold(self) -> Self
where
E::Type: Fold,
{
Self { fold: Some(E::Type::fold), ..self }
}
/// Creates type-erased metadata and routines for a `#[ghost]` field.
pub const fn vtable() -> FieldVtable<Packed<E>>
where
E: SettableProperty<I>,
E::Type: Reflect + IntoValue + PartialEq,
{
FieldVtable {
name: E::FIELD.name,
docs: E::FIELD.docs,
positional: E::FIELD.positional,
required: false,
variadic: false,
settable: true,
synthesized: false,
input: || <E::Type as Reflect>::input(),
default: Some(|| E::default().into_value()),
has: |_| false,
get: |_| None,
get_with_styles: |_, styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
get_from_styles: |styles| {
Some(styles.get_cloned::<E, I>(Field::new()).into_value())
},
materialize: |_, _| {},
eq: |_, _| true,
}
}
}
/// A settable property that can be accessed by reference (because it is not
/// folded).
pub trait RefableProperty<const I: u8>: SettableProperty<I> {}
/// A settable field of an element.
///
/// The field can be in two states: Unset or present.
///
/// See [`StyleChain`] for more details about the available accessor methods.
#[derive(Copy, Clone, Hash)]
pub struct Settable<E: NativeElement, const I: u8>(Option<E::Type>)
where
E: SettableProperty<I>;
impl<E: NativeElement, const I: u8> Settable<E, I>
where
E: SettableProperty<I>,
{
/// Creates a new unset instance.
pub fn new() -> Self {
Self(None)
}
/// Sets the instance to a value.
pub fn set(&mut self, value: E::Type) {
self.0 = Some(value);
}
/// Clears the value from the instance.
pub fn unset(&mut self) {
self.0 = None;
}
/// Views the type as an [`Option`] which is `Some` if the type is set
/// and `None` if it is unset.
pub fn as_option(&self) -> &Option<E::Type> {
&self.0
}
/// Views the type as a mutable [`Option`].
pub fn as_option_mut(&mut self) -> &mut Option<E::Type> {
&mut self.0
}
/// Whether the field is set.
pub fn is_set(&self) -> bool {
self.0.is_some()
}
/// Retrieves the value given styles. The styles are used if the value is
/// unset.
pub fn get<'a>(&'a self, styles: StyleChain<'a>) -> E::Type
where
E::Type: Copy,
{
self.get_cloned(styles)
}
/// Retrieves and clones the value given styles. The styles are used if the
/// value is unset or if it needs folding.
pub fn get_cloned<'a>(&'a self, styles: StyleChain<'a>) -> E::Type {
if let Some(fold) = E::FOLD {
let mut res = styles.get_cloned::<E, I>(Field::new());
if let Some(value) = &self.0 {
res = fold(value.clone(), res);
}
res
} else if let Some(value) = &self.0 {
value.clone()
} else {
styles.get_cloned::<E, I>(Field::new())
}
}
/// Retrieves a reference to the value given styles. The styles are used if
/// the value is unset.
pub fn get_ref<'a>(&'a self, styles: StyleChain<'a>) -> &'a E::Type
where
E: RefableProperty<I>,
{
if let Some(value) = &self.0 {
value
} else {
styles.get_ref::<E, I>(Field::new())
}
}
/// Retrieves the value and then immediately [resolves](Resolve) it.
pub fn resolve<'a>(&'a self, styles: StyleChain<'a>) -> <E::Type as Resolve>::Output
where
E::Type: Resolve,
{
self.get_cloned(styles).resolve(styles)
}
}
impl<E: NativeElement, const I: u8> Debug for Settable<E, I>
where
E: SettableProperty<I>,
E::Type: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl<E: NativeElement, const I: u8> Default for Settable<E, I>
where
E: SettableProperty<I>,
{
fn default() -> Self {
Self(None)
}
}
impl<E: NativeElement, const I: u8> From<Option<E::Type>> for Settable<E, I>
where
E: SettableProperty<I>,
{
fn from(value: Option<E::Type>) -> Self {
Self(value)
}
}
/// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError {
Unknown,
Unset,
}
impl FieldAccessError {
/// Formats the error message given the content and the field name.
#[cold]
pub fn message(self, content: &Content, field: &str) -> EcoString {
let elem_name = content.elem().name();
match self {
FieldAccessError::Unknown => {
eco_format!("{elem_name} does not have field {}", field.repr())
}
FieldAccessError::Unset => {
eco_format!(
"field {} in {elem_name} is not known at this point",
field.repr()
)
}
}
}
/// Formats the error message for an `at` calls without a default value.
#[cold]
pub fn message_no_default(self, content: &Content, field: &str) -> EcoString {
let mut msg = self.message(content, field);
msg.push_str(" and no default was specified");
msg
}
}

View File

@ -1,23 +1,33 @@
use std::any::TypeId;
mod element;
mod field;
mod packed;
mod raw;
mod vtable;
pub use self::element::*;
pub use self::field::*;
pub use self::packed::Packed;
pub use self::vtable::{ContentVtable, FieldVtable};
#[doc(inline)]
pub use typst_macros::elem;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::hash::Hash;
use std::iter::{self, Sum};
use std::marker::PhantomData;
use std::ops::{Add, AddAssign, ControlFlow, Deref, DerefMut};
use std::sync::Arc;
use std::ops::{Add, AddAssign, ControlFlow};
use comemo::Tracked;
use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer};
use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
use typst_utils::singleton;
use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles,
Value,
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe,
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
};
use crate::introspection::Location;
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
@ -68,43 +78,14 @@ use crate::text::UnderlineElem;
/// elements the content is composed of and what fields they have.
/// Alternatively, you can inspect the output of the [`repr`] function.
#[ty(scope, cast)]
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Content {
/// The partially element-dependent inner data.
inner: Arc<Inner<dyn Bounds>>,
/// The element's source code location.
span: Span,
}
/// The inner representation behind the `Arc`.
#[derive(Hash)]
struct Inner<T: ?Sized + 'static> {
/// An optional label attached to the element.
label: Option<Label>,
/// The element's location which identifies it in the layouted output.
location: Option<Location>,
/// Manages the element during realization.
/// - If bit 0 is set, the element is prepared.
/// - If bit n is set, the element is guarded against the n-th show rule
/// recipe from the top of the style chain (counting from 1).
lifecycle: SmallBitSet,
/// The element's raw data.
elem: LazyHash<T>,
}
#[derive(Clone, PartialEq, Hash)]
#[repr(transparent)]
pub struct Content(raw::RawContent);
impl Content {
/// Creates a new content from an element.
pub fn new<T: NativeElement>(elem: T) -> Self {
Self {
inner: Arc::new(Inner {
label: None,
location: None,
lifecycle: SmallBitSet::new(),
elem: elem.into(),
}),
span: Span::detached(),
}
Self(raw::RawContent::new(elem))
}
/// Creates a empty sequence content.
@ -114,25 +95,25 @@ impl Content {
/// Get the element of this content.
pub fn elem(&self) -> Element {
self.inner.elem.dyn_elem()
self.0.elem()
}
/// Get the span of the content.
pub fn span(&self) -> Span {
self.span
self.0.span()
}
/// Set the span of the content.
pub fn spanned(mut self, span: Span) -> Self {
if self.span.is_detached() {
self.span = span;
if self.0.span().is_detached() {
*self.0.span_mut() = span;
}
self
}
/// Get the label of the content.
pub fn label(&self) -> Option<Label> {
self.inner.label
self.0.meta().label
}
/// Attach a label to the content.
@ -143,7 +124,7 @@ impl Content {
/// Set the label of the content.
pub fn set_label(&mut self, label: Label) {
self.make_mut().label = Some(label);
self.0.meta_mut().label = Some(label);
}
/// Assigns a location to the content.
@ -159,28 +140,28 @@ impl Content {
/// Set the location of the content.
pub fn set_location(&mut self, location: Location) {
self.make_mut().location = Some(location);
self.0.meta_mut().location = Some(location);
}
/// Check whether a show rule recipe is disabled.
pub fn is_guarded(&self, index: RecipeIndex) -> bool {
self.inner.lifecycle.contains(index.0)
self.0.meta().lifecycle.contains(index.0)
}
/// Disable a show rule recipe.
pub fn guarded(mut self, index: RecipeIndex) -> Self {
self.make_mut().lifecycle.insert(index.0);
self.0.meta_mut().lifecycle.insert(index.0);
self
}
/// Whether this content has already been prepared.
pub fn is_prepared(&self) -> bool {
self.inner.lifecycle.contains(0)
self.0.meta().lifecycle.contains(0)
}
/// Mark this content as prepared.
pub fn mark_prepared(&mut self) {
self.make_mut().lifecycle.insert(0);
self.0.meta_mut().lifecycle.insert(0);
}
/// Get a field by ID.
@ -198,9 +179,14 @@ impl Content {
return Ok(label.into_value());
}
}
match styles {
Some(styles) => self.inner.elem.field_with_styles(id, styles),
None => self.inner.elem.field(id),
match self.0.handle().field(id) {
Some(handle) => match styles {
Some(styles) => handle.get_with_styles(styles),
None => handle.get(),
}
.ok_or(FieldAccessError::Unset),
None => Err(FieldAccessError::Unknown),
}
}
@ -215,8 +201,11 @@ impl Content {
.map(|label| label.into_value())
.ok_or(FieldAccessError::Unknown);
}
let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?;
self.get(id, None)
match self.elem().field_id(name).and_then(|id| self.0.handle().field(id)) {
Some(handle) => handle.get().ok_or(FieldAccessError::Unset),
None => Err(FieldAccessError::Unknown),
}
}
/// Get a field by ID, returning a missing field error if it does not exist.
@ -240,7 +229,9 @@ impl Content {
/// Resolve all fields with the styles and save them in-place.
pub fn materialize(&mut self, styles: StyleChain) {
self.make_mut().elem.materialize(styles);
for id in 0..self.elem().vtable().fields.len() as u8 {
self.0.handle_mut().field(id).unwrap().materialize(styles);
}
}
/// Create a new sequence element from multiples elements.
@ -257,7 +248,7 @@ impl Content {
/// Whether the contained element is of type `T`.
pub fn is<T: NativeElement>(&self) -> bool {
self.inner.elem.dyn_type_id() == TypeId::of::<T>()
self.0.is::<T>()
}
/// Downcasts the element to a packed value.
@ -280,16 +271,6 @@ impl Content {
self.into_packed::<T>().map(Packed::unpack)
}
/// Makes sure the content is not shared and returns a mutable reference to
/// the inner data.
fn make_mut(&mut self) -> &mut Inner<dyn Bounds> {
let arc = &mut self.inner;
if Arc::strong_count(arc) > 1 || Arc::weak_count(arc) > 0 {
*self = arc.elem.dyn_clone(arc, self.span);
}
Arc::get_mut(&mut self.inner).unwrap()
}
/// Whether the contained element has the given capability.
pub fn can<C>(&self) -> bool
where
@ -304,13 +285,7 @@ impl Content {
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`.
// Since any `Packed<T>` is a repr(transparent) `Content`, we can also
// use a `*const Content` pointer.
let vtable = self.elem().vtable()(TypeId::of::<C>())?;
let data = self as *const Content as *const ();
Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) })
self.0.with::<C>()
}
/// Cast to a mutable trait object if the contained element has the given
@ -319,18 +294,7 @@ impl Content {
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`.
// Since any `Packed<T>` is a repr(transparent) `Content`, we can also
// use a `*const Content` pointer.
//
// The resulting trait object contains an `&mut Packed<T>`. We do _not_
// need to ensure that we hold the only reference to the `Arc` here
// because `Packed<T>`'s DerefMut impl will take care of that if
// mutable access is required.
let vtable = self.elem().vtable()(TypeId::of::<C>())?;
let data = self as *mut Content as *mut ();
Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) })
self.0.with_mut::<C>()
}
/// Whether the content is an empty sequence.
@ -372,6 +336,15 @@ impl Content {
Self::sequence(std::iter::repeat_with(|| self.clone()).take(count))
}
/// Sets a style property on the content.
pub fn set<E, const I: u8>(self, field: Field<E, I>, value: E::Type) -> Self
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
self.styled(Property::new(field, value))
}
/// Style this content with a style entry.
pub fn styled(mut self, style: impl Into<Style>) -> Self {
if let Some(style_elem) = self.to_packed_mut::<StyledElem>() {
@ -476,7 +449,7 @@ impl Content {
// Call f on the element itself before recursively iterating its fields.
f(self.clone())?;
for (_, value) in self.inner.elem.fields() {
for (_, value) in self.fields() {
walk_value(value, f)?;
}
ControlFlow::Continue(())
@ -504,12 +477,12 @@ impl Content {
/// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self {
self.styled(LinkElem::set_current(Some(dest)))
self.set(LinkElem::current, Some(dest))
}
/// Set alignments for this content.
pub fn aligned(self, align: Alignment) -> Self {
self.styled(AlignElem::set_alignment(align))
self.set(AlignElem::alignment, align)
}
/// Pad this content at the sides.
@ -562,7 +535,10 @@ impl Content {
return false;
};
self.inner.elem.has(id)
match self.0.handle().field(id) {
Some(field) => field.has(),
None => false,
}
}
/// Access the specified field on the content. Returns the default value if
@ -592,7 +568,12 @@ impl Content {
/// ```
#[func]
pub fn fields(&self) -> Dict {
let mut dict = self.inner.elem.fields();
let mut dict = Dict::new();
for field in self.0.handle().fields() {
if let Some(value) = field.get() {
dict.insert(field.name.into(), value);
}
}
if let Some(label) = self.label() {
dict.insert("label".into(), label.into_value());
}
@ -605,7 +586,7 @@ impl Content {
/// used with [counters]($counter), [state] and [queries]($query).
#[func]
pub fn location(&self) -> Option<Location> {
self.inner.location
self.0.meta().location
}
}
@ -617,7 +598,7 @@ impl Default for Content {
impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.inner.elem.fmt(f)
self.0.fmt(f)
}
}
@ -627,16 +608,22 @@ impl<T: NativeElement> From<T> for Content {
}
}
impl PartialEq for Content {
fn eq(&self, other: &Self) -> bool {
// Additional short circuit for different elements.
self.elem() == other.elem() && self.inner.elem.dyn_eq(other)
}
}
impl Repr for Content {
fn repr(&self) -> EcoString {
self.inner.elem.repr()
self.0.handle().repr().unwrap_or_else(|| {
let fields = self
.0
.handle()
.fields()
.filter_map(|field| field.get().map(|v| (field.name, v.repr())))
.map(|(name, value)| eco_format!("{name}: {value}"))
.collect::<Vec<_>>();
eco_format!(
"{}{}",
self.elem().name(),
repr::pretty_array_like(&fields, false),
)
})
}
}
@ -717,190 +704,8 @@ impl Serialize for Content {
}
}
/// The trait that combines all the other traits into a trait object.
trait Bounds: Debug + Repr + Fields + Send + Sync + 'static {
fn dyn_type_id(&self) -> TypeId;
fn dyn_elem(&self) -> Element;
fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content;
fn dyn_hash(&self, hasher: &mut dyn Hasher);
fn dyn_eq(&self, other: &Content) -> bool;
}
impl<T: NativeElement> Bounds for T {
fn dyn_type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn dyn_elem(&self) -> Element {
Self::elem()
}
fn dyn_clone(&self, inner: &Inner<dyn Bounds>, span: Span) -> Content {
Content {
inner: Arc::new(Inner {
label: inner.label,
location: inner.location,
lifecycle: inner.lifecycle.clone(),
elem: LazyHash::reuse(self.clone(), &inner.elem),
}),
span,
}
}
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
TypeId::of::<Self>().hash(&mut state);
self.hash(&mut state);
}
fn dyn_eq(&self, other: &Content) -> bool {
let Some(other) = other.to_packed::<Self>() else {
return false;
};
*self == **other
}
}
impl Hash for dyn Bounds {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dyn_hash(state);
}
}
/// A packed element of a static type.
#[derive(Clone, PartialEq, Hash)]
#[repr(transparent)]
pub struct Packed<T: NativeElement>(
/// Invariant: Must be of type `T`.
Content,
PhantomData<T>,
);
impl<T: NativeElement> Packed<T> {
/// Pack element while retaining its static type.
pub fn new(element: T) -> Self {
// Safety: The element is known to be of type `T`.
Packed(element.pack(), PhantomData)
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_ref(content: &Content) -> Option<&Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) });
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_mut(content: &mut Content) -> Option<&mut Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe {
std::mem::transmute::<&mut Content, &mut Packed<T>>(content)
});
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_owned(content: Content) -> Result<Self, Content> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) });
}
Err(content)
}
/// Pack back into content.
pub fn pack(self) -> Content {
self.0
}
/// Extract the raw underlying element.
pub fn unpack(self) -> T {
// This function doesn't yet need owned self, but might in the future.
(*self).clone()
}
/// The element's span.
pub fn span(&self) -> Span {
self.0.span()
}
/// Set the span of the element.
pub fn spanned(self, span: Span) -> Self {
Self(self.0.spanned(span), PhantomData)
}
/// Accesses the label of the element.
pub fn label(&self) -> Option<Label> {
self.0.label()
}
/// Accesses the location of the element.
pub fn location(&self) -> Option<Location> {
self.0.location()
}
/// Sets the location of the element.
pub fn set_location(&mut self, location: Location) {
self.0.set_location(location);
}
}
impl<T: NativeElement> AsRef<T> for Packed<T> {
fn as_ref(&self) -> &T {
self
}
}
impl<T: NativeElement> AsMut<T> for Packed<T> {
fn as_mut(&mut self) -> &mut T {
self
}
}
impl<T: NativeElement> Deref for Packed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// Safety:
// - Packed<T> guarantees that the content trait object wraps
// an element of type `T`.
// - This downcast works the same way as dyn Any's does. We can't reuse
// that one because we don't want to pay the cost for every deref.
let elem = &*self.0.inner.elem;
unsafe { &*(elem as *const dyn Bounds as *const T) }
}
}
impl<T: NativeElement> DerefMut for Packed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// Safety:
// - Packed<T> guarantees that the content trait object wraps
// an element of type `T`.
// - We have guaranteed unique access thanks to `make_mut`.
// - This downcast works the same way as dyn Any's does. We can't reuse
// that one because we don't want to pay the cost for every deref.
let elem = &mut *self.0.make_mut().elem;
unsafe { &mut *(elem as *mut dyn Bounds as *mut T) }
}
}
impl<T: NativeElement + Debug> Debug for Packed<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
/// A sequence of content.
#[elem(Debug, Repr, PartialEq)]
#[elem(Debug, Repr)]
pub struct SequenceElem {
/// The elements.
#[required]
@ -922,19 +727,13 @@ impl Default for SequenceElem {
}
}
impl PartialEq for SequenceElem {
fn eq(&self, other: &Self) -> bool {
self.children.iter().eq(other.children.iter())
}
}
impl Repr for SequenceElem {
fn repr(&self) -> EcoString {
if self.children.is_empty() {
"[]".into()
} else {
let elements = crate::foundations::repr::pretty_array_like(
&self.children.iter().map(|c| c.inner.elem.repr()).collect::<Vec<_>>(),
&self.children.iter().map(|c| c.repr()).collect::<Vec<_>>(),
false,
);
eco_format!("sequence{}", elements)
@ -974,49 +773,8 @@ impl Repr for StyledElem {
}
}
/// Tries to extract the plain-text representation of the element.
pub trait PlainText {
/// Write this element's plain text into the given buffer.
fn plain_text(&self, text: &mut EcoString);
}
/// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError {
Unknown,
Unset,
Internal,
}
impl FieldAccessError {
/// Formats the error message given the content and the field name.
#[cold]
pub fn message(self, content: &Content, field: &str) -> EcoString {
let elem_name = content.elem().name();
match self {
FieldAccessError::Unknown => {
eco_format!("{elem_name} does not have field {}", field.repr())
}
FieldAccessError::Unset => {
eco_format!(
"field {} in {elem_name} is not known at this point",
field.repr()
)
}
FieldAccessError::Internal => {
eco_format!(
"internal error when accessing field {} in {elem_name} this is a bug",
field.repr()
)
}
}
}
/// Formats the error message for an `at` calls without a default value.
#[cold]
pub fn message_no_default(self, content: &Content, field: &str) -> EcoString {
let mut msg = self.message(content, field);
msg.push_str(" and no default was specified");
msg
impl<T: NativeElement> IntoValue for T {
fn into_value(self) -> Value {
Value::Content(self.pack())
}
}

View File

@ -0,0 +1,147 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use typst_syntax::Span;
use crate::foundations::{Content, Label, NativeElement};
use crate::introspection::Location;
/// A packed element of a static type.
#[derive(Clone)]
#[repr(transparent)]
pub struct Packed<T: NativeElement>(
/// Invariant: Must be of type `T`.
Content,
PhantomData<T>,
);
impl<T: NativeElement> Packed<T> {
/// Pack element while retaining its static type.
pub fn new(element: T) -> Self {
// Safety: The element is known to be of type `T`.
Packed(element.pack(), PhantomData)
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_ref(content: &Content) -> Option<&Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe { std::mem::transmute::<&Content, &Packed<T>>(content) });
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_mut(content: &mut Content) -> Option<&mut Self> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Some(unsafe {
std::mem::transmute::<&mut Content, &mut Packed<T>>(content)
});
}
None
}
/// Try to cast type-erased content into a statically known packed element.
pub fn from_owned(content: Content) -> Result<Self, Content> {
if content.is::<T>() {
// Safety:
// - We have checked the type.
// - Packed<T> is repr(transparent).
return Ok(unsafe { std::mem::transmute::<Content, Packed<T>>(content) });
}
Err(content)
}
/// Pack back into content.
pub fn pack(self) -> Content {
self.0
}
/// Extract the raw underlying element.
pub fn unpack(self) -> T {
// This function doesn't yet need owned self, but might in the future.
(*self).clone()
}
/// The element's span.
pub fn span(&self) -> Span {
self.0.span()
}
/// Set the span of the element.
pub fn spanned(self, span: Span) -> Self {
Self(self.0.spanned(span), PhantomData)
}
/// Accesses the label of the element.
pub fn label(&self) -> Option<Label> {
self.0.label()
}
/// Accesses the location of the element.
pub fn location(&self) -> Option<Location> {
self.0.location()
}
/// Sets the location of the element.
pub fn set_location(&mut self, location: Location) {
self.0.set_location(location);
}
pub fn as_content(&self) -> &Content {
&self.0
}
}
impl<T: NativeElement> AsRef<T> for Packed<T> {
fn as_ref(&self) -> &T {
self
}
}
impl<T: NativeElement> AsMut<T> for Packed<T> {
fn as_mut(&mut self) -> &mut T {
self
}
}
impl<T: NativeElement> Deref for Packed<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
// Safety: Packed<T> guarantees that the content is of element type `T`.
unsafe { (self.0).0.data::<T>() }
}
}
impl<T: NativeElement> DerefMut for Packed<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
// Safety: Packed<T> guarantees that the content is of element type `T`.
unsafe { (self.0).0.data_mut::<T>() }
}
}
impl<T: NativeElement + Debug> Debug for Packed<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl<T: NativeElement> PartialEq for Packed<T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T: NativeElement> Hash for Packed<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}

View File

@ -0,0 +1,426 @@
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ptr::NonNull;
use std::sync::atomic::{self, AtomicUsize, Ordering};
use typst_syntax::Span;
use typst_utils::{fat, HashLock, SmallBitSet};
use super::vtable;
use crate::foundations::{Element, Label, NativeElement, Packed};
use crate::introspection::Location;
/// The raw, low-level implementation of content.
///
/// The `ptr` + `elem` fields implement a fat pointer setup similar to an
/// `Arc<Inner<dyn Trait>>`, but in a manual way, allowing us to have a custom
/// [vtable].
pub struct RawContent {
/// A type-erased pointer to an allocation containing two things:
/// - A header that is the same for all elements
/// - Element-specific `data` that holds the specific element
///
/// This pointer is valid for both a `Header` and an `Inner<E>` where
/// `E::ELEM == self.elem` and can be freely cast between both. This is
/// possible because
/// - `Inner<E>` is `repr(C)`
/// - The first field of `Inner<E>` is `Header`
/// - ISO/IEC 9899:TC2 C standard § 6.7.2.1 - 13 states that a pointer to a
/// structure "points to its initial member" with no padding at the start
ptr: NonNull<Header>,
/// Describes which kind of element this content holds. This is used for
///
/// - Direct comparisons, e.g. `is::<HeadingElem>()`
/// - Behavior: An `Element` is just a pointer to a `ContentVtable`
/// containing not just data, but also function pointers for various
/// element-specific operations that can be performed
///
/// It is absolutely crucial that `elem == <E as NativeElement>::ELEM` for
/// `Inner<E>` pointed to by `ptr`. Otherwise, things will go very wrong
/// since we'd be using the wrong vtable.
elem: Element,
/// The content's span.
span: Span,
}
/// The allocated part of an element's representation.
///
/// This is `repr(C)` to ensure that a pointer to the whole structure may be
/// cast to a pointer to its first field.
#[repr(C)]
struct Inner<E> {
/// It is crucial that this is the first field because we cast between
/// pointers to `Inner<E>` and pointers to `Header`. See the documentation
/// of `RawContent::ptr` for more details.
header: Header,
/// The element struct. E.g. `E = HeadingElem`.
data: E,
}
/// The header that is shared by all elements.
struct Header {
/// The element's reference count. This works just like for `Arc`.
/// Unfortunately, we have to reimplement reference counting because we
/// have a custom fat pointer and `Arc` wouldn't know how to drop its
/// contents. Something with `ManuallyDrop<Arc<_>>` might also work, but at
/// that point we're not gaining much and with the way it's implemented now
/// we can also skip the unnecessary weak reference count.
refs: AtomicUsize,
/// Metadata for the element.
meta: Meta,
/// A cell for memoizing the hash of just the `data` part of the content.
hash: HashLock,
}
/// Metadata that elements can hold.
#[derive(Clone, Hash)]
pub(super) struct Meta {
/// An optional label attached to the element.
pub label: Option<Label>,
/// The element's location which identifies it in the laid-out output.
pub location: Option<Location>,
/// Manages the element during realization.
/// - If bit 0 is set, the element is prepared.
/// - If bit n is set, the element is guarded against the n-th show rule
/// recipe from the top of the style chain (counting from 1).
pub lifecycle: SmallBitSet,
}
impl RawContent {
/// Creates raw content wrapping an element, with all metadata set to
/// default (including a detached span).
pub(super) fn new<E: NativeElement>(data: E) -> Self {
Self::create(
data,
Meta {
label: None,
location: None,
lifecycle: SmallBitSet::new(),
},
HashLock::new(),
Span::detached(),
)
}
/// Creates and allocates raw content.
fn create<E: NativeElement>(data: E, meta: Meta, hash: HashLock, span: Span) -> Self {
let raw = Box::into_raw(Box::<Inner<E>>::new(Inner {
header: Header { refs: AtomicUsize::new(1), meta, hash },
data,
}));
// Safety: `Box` always holds a non-null pointer. See also
// `Box::into_non_null` (which is unstable).
let non_null = unsafe { NonNull::new_unchecked(raw) };
// Safety: See `RawContent::ptr`.
let ptr = non_null.cast::<Header>();
Self { ptr, elem: E::ELEM, span }
}
/// Destroys raw content and deallocates.
///
/// # Safety
/// - The reference count must be zero.
/// - The raw content must be be of type `E`.
pub(super) unsafe fn drop_impl<E: NativeElement>(&mut self) {
debug_assert_eq!(self.header().refs.load(Ordering::Relaxed), 0);
// Safety:
// - The caller guarantees that the content is of type `E`.
// - Thus, `ptr` must have been created from `Box<Inner<E>>` (see
// `RawContent::ptr`).
// - And to clean it up, we can just reproduce our box.
unsafe {
let ptr = self.ptr.cast::<Inner<E>>();
drop(Box::<Inner<E>>::from_raw(ptr.as_ptr()));
}
}
/// Clones a packed element into new raw content.
pub(super) fn clone_impl<E: NativeElement>(elem: &Packed<E>) -> Self {
let raw = &elem.as_content().0;
let header = raw.header();
RawContent::create(
elem.as_ref().clone(),
header.meta.clone(),
header.hash.clone(),
raw.span,
)
}
/// Accesses the header part of the raw content.
fn header(&self) -> &Header {
// Safety: `self.ptr` is a valid pointer to a header structure.
unsafe { self.ptr.as_ref() }
}
/// Mutably accesses the header part of the raw content.
fn header_mut(&mut self) -> &mut Header {
self.make_unique();
// Safety:
// - `self.ptr` is a valid pointer to a header structure.
// - We have unique access to the backing allocation (just ensured).
unsafe { self.ptr.as_mut() }
}
/// Retrieves the contained element **without checking that the content is
/// of the correct type.**
///
/// # Safety
/// This must be preceded by a check to [`is`]. The safe API for this is
/// [`Content::to_packed`] and the [`Packed`] struct.
pub(super) unsafe fn data<E: NativeElement>(&self) -> &E {
debug_assert!(self.is::<E>());
// Safety:
// - The caller guarantees that the content is of type `E`.
// - `self.ptr` is a valid pointer to an `Inner<E>` (see
// `RawContent::ptr`).
unsafe { &self.ptr.cast::<Inner<E>>().as_ref().data }
}
/// Retrieves the contained element mutably **without checking that the
/// content is of the correct type.**
///
/// Ensures that the element's allocation is unique.
///
/// # Safety
/// This must be preceded by a check to [`is`]. The safe API for this is
/// [`Content::to_packed_mut`] and the [`Packed`] struct.
pub(super) unsafe fn data_mut<E: NativeElement>(&mut self) -> &mut E {
debug_assert!(self.is::<E>());
// Ensure that the memoized hash is reset because we may mutate the
// element.
self.header_mut().hash.reset();
// Safety:
// - The caller guarantees that the content is of type `E`.
// - `self.ptr` is a valid pointer to an `Inner<E>` (see
// `RawContent::ptr`).
// - We have unique access to the backing allocation (due to header_mut).
unsafe { &mut self.ptr.cast::<Inner<E>>().as_mut().data }
}
/// Ensures that we have unique access to the backing allocation by cloning
/// if the reference count exceeds 1. This is used before performing
/// mutable operations, implementing a clone-on-write scheme.
fn make_unique(&mut self) {
if self.header().refs.load(Ordering::Relaxed) > 1 {
*self = self.handle().clone();
}
}
/// Retrieves the element this content is for.
pub(super) fn elem(&self) -> Element {
self.elem
}
/// Whether this content holds an element of type `E`.
pub(super) fn is<E: NativeElement>(&self) -> bool {
self.elem == E::ELEM
}
/// Retrieves the content's span.
pub(super) fn span(&self) -> Span {
self.span
}
/// Retrieves the content's span mutably.
pub(super) fn span_mut(&mut self) -> &mut Span {
&mut self.span
}
/// Retrieves the content's metadata.
pub(super) fn meta(&self) -> &Meta {
&self.header().meta
}
/// Retrieves the content's metadata mutably.
pub(super) fn meta_mut(&mut self) -> &mut Meta {
&mut self.header_mut().meta
}
/// Casts into a trait object for a given trait if the packed element
/// implements said trait.
pub(super) fn with<C>(&self) -> Option<&C>
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`. Since
// any `Packed<T>` is repr(transparent) with `Content` and `RawContent`,
// we can also use a `*const RawContent` pointer.
let vtable = (self.elem.vtable().capability)(TypeId::of::<C>())?;
let data = self as *const Self as *const ();
Some(unsafe { &*fat::from_raw_parts(data, vtable.as_ptr()) })
}
/// Casts into a mutable trait object for a given trait if the packed
/// element implements said trait.
pub(super) fn with_mut<C>(&mut self) -> Option<&mut C>
where
C: ?Sized + 'static,
{
// Safety: The vtable comes from the `Capable` implementation which
// guarantees to return a matching vtable for `Packed<T>` and `C`. Since
// any `Packed<T>` is repr(transparent) with `Content` and `RawContent`,
// we can also use a `*const Content` pointer.
//
// The resulting trait object contains an `&mut Packed<T>`. We do _not_
// need to ensure that we hold the only reference to the `Arc` here
// because `Packed<T>`'s DerefMut impl will take care of that if mutable
// access is required.
let vtable = (self.elem.vtable().capability)(TypeId::of::<C>())?;
let data = self as *mut Self as *mut ();
Some(unsafe { &mut *fat::from_raw_parts_mut(data, vtable.as_ptr()) })
}
}
impl RawContent {
/// Retrieves the element's vtable.
pub(super) fn handle(&self) -> vtable::ContentHandle<&RawContent> {
// Safety `self.elem.vtable()` is a matching vtable for `self`.
unsafe { vtable::Handle::new(self, self.elem.vtable()) }
}
/// Retrieves the element's vtable.
pub(super) fn handle_mut(&mut self) -> vtable::ContentHandle<&mut RawContent> {
// Safety `self.elem.vtable()` is a matching vtable for `self`.
unsafe { vtable::Handle::new(self, self.elem.vtable()) }
}
/// Retrieves the element's vtable.
pub(super) fn handle_pair<'a, 'b>(
&'a self,
other: &'b RawContent,
) -> Option<vtable::ContentHandle<(&'a RawContent, &'b RawContent)>> {
(self.elem == other.elem).then(|| {
// Safety:
// - `self.elem.vtable()` is a matching vtable for `self`.
// - It's also matching for `other` because `self.elem == other.elem`.
unsafe { vtable::Handle::new((self, other), self.elem.vtable()) }
})
}
}
impl Debug for RawContent {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.handle().debug(f)
}
}
impl Clone for RawContent {
fn clone(&self) -> Self {
// See Arc's clone impl for details about memory ordering.
let prev = self.header().refs.fetch_add(1, Ordering::Relaxed);
// See Arc's clone impl details about guarding against incredibly
// degenerate programs.
if prev > isize::MAX as usize {
ref_count_overflow(self.ptr, self.elem, self.span);
}
Self { ptr: self.ptr, elem: self.elem, span: self.span }
}
}
impl Drop for RawContent {
fn drop(&mut self) {
// Drop our ref-count. If there was more than one content before
// (including this one), we shouldn't deallocate. See Arc's drop impl
// for details about memory ordering.
if self.header().refs.fetch_sub(1, Ordering::Release) != 1 {
return;
}
// See Arc's drop impl for details.
atomic::fence(Ordering::Acquire);
// Safety:
// No other content references the backing allocation (just checked)
unsafe {
self.handle_mut().drop();
}
}
}
impl PartialEq for RawContent {
fn eq(&self, other: &Self) -> bool {
let Some(handle) = self.handle_pair(other) else { return false };
handle
.eq()
.unwrap_or_else(|| handle.fields().all(|handle| handle.eq()))
}
}
impl Hash for RawContent {
fn hash<H: Hasher>(&self, state: &mut H) {
self.elem.hash(state);
let header = self.header();
header.meta.hash(state);
header.hash.get_or_insert_with(|| self.handle().hash()).hash(state);
self.span.hash(state);
}
}
// Safety:
// - Works like `Arc`.
// - `NativeElement` implies `Send` and `Sync`, see below.
unsafe impl Sync for RawContent {}
unsafe impl Send for RawContent {}
fn _ensure_send_sync<T: NativeElement>() {
fn needs_send_sync<T: Send + Sync>() {}
needs_send_sync::<T>();
}
#[cold]
fn ref_count_overflow(ptr: NonNull<Header>, elem: Element, span: Span) -> ! {
// Drop to decrement the ref count to counter the increment in `clone()`
drop(RawContent { ptr, elem, span });
panic!("reference count overflow");
}
#[cfg(test)]
mod tests {
use crate::foundations::{NativeElement, Repr, StyleChain, Value};
use crate::introspection::Location;
use crate::model::HeadingElem;
use crate::text::TextElem;
#[test]
fn test_miri() {
let styles = StyleChain::default();
let mut first = HeadingElem::new(TextElem::packed("Hi!")).with_offset(2).pack();
let hash1 = typst_utils::hash128(&first);
first.set_location(Location::new(10));
let _ = format!("{first:?}");
let _ = first.repr();
assert!(first.is::<HeadingElem>());
assert!(!first.is::<TextElem>());
assert_eq!(first.to_packed::<TextElem>(), None);
assert_eq!(first.location(), Some(Location::new(10)));
assert_eq!(first.field_by_name("offset"), Ok(Value::Int(2)));
assert!(!first.has("depth".into()));
let second = first.clone();
first.materialize(styles);
let first_packed = first.to_packed::<HeadingElem>().unwrap();
let second_packed = second.to_packed::<HeadingElem>().unwrap();
assert!(first.has("depth".into()));
assert!(!second.has("depth".into()));
assert!(first_packed.depth.is_set());
assert!(!second_packed.depth.is_set());
assert_ne!(first, second);
assert_ne!(hash1, typst_utils::hash128(&first));
}
}

View File

@ -0,0 +1,383 @@
//! A custom [vtable] implementation for content.
//!
//! This is similar to what is generated by the Rust compiler under the hood
//! when using trait objects. However, ours has two key advantages:
//!
//! - It can store a _slice_ of sub-vtables for field-specific operations.
//! - It can store not only methods, but also plain data, allowing us to access
//! that data without going through dynamic dispatch.
//!
//! Because our vtable pointers are backed by `static` variables, we can also
//! perform checks for element types by comparing raw vtable pointers giving us
//! `RawContent::is` without dynamic dispatch.
//!
//! Overall, the custom vtable gives us just a little more flexibility and
//! optimizability than using built-in trait objects.
//!
//! Note that all vtable methods receive elements of type `Packed<E>`, but some
//! only perform actions on the `E` itself, with the shared part kept outside of
//! the vtable (e.g. `hash`), while some perform the full action (e.g. `clone`
//! as it needs to return new, fully populated raw content). Which one it is, is
//! documented for each.
//!
//! # Safety
//! This module contains a lot of `unsafe` keywords, but almost all of it is the
//! same and quite straightfoward. All function pointers that operate on a
//! specific element type are marked as unsafe. In combination with `repr(C)`,
//! this grants us the ability to safely transmute a `ContentVtable<Packed<E>>`
//! into a `ContentVtable<RawContent>` (or just short `ContentVtable`). Callers
//! of functions marked as unsafe have to guarantee that the `ContentVtable` was
//! transmuted from the same `E` as the RawContent was constructed from. The
//! `Handle` struct provides a safe access layer, moving the guarantee that the
//! vtable is matching into a single spot.
//!
//! [vtable]: https://en.wikipedia.org/wiki/Virtual_method_table
use std::any::TypeId;
use std::fmt::{self, Debug, Formatter};
use std::ops::Deref;
use std::ptr::NonNull;
use ecow::EcoString;
use super::raw::RawContent;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
Args, CastInfo, Construct, Content, LazyElementStore, NativeElement, NativeScope,
Packed, Repr, Scope, Set, StyleChain, Styles, Value,
};
use crate::text::{Lang, LocalName, Region};
/// Encapsulates content and a vtable, granting safe access to vtable operations.
pub(super) struct Handle<T, V: 'static>(T, &'static V);
impl<T, V> Handle<T, V> {
/// Produces a new handle from content and a vtable.
///
/// # Safety
/// The content and vtable must be matching, i.e. `vtable` must be derived
/// from the content's vtable.
pub(super) unsafe fn new(content: T, vtable: &'static V) -> Self {
Self(content, vtable)
}
}
impl<T, V> Deref for Handle<T, V> {
type Target = V;
fn deref(&self) -> &Self::Target {
self.1
}
}
pub(super) type ContentHandle<T> = Handle<T, ContentVtable>;
pub(super) type FieldHandle<T> = Handle<T, FieldVtable>;
/// A vtable for performing element-specific actions on type-erased content.
/// Also contains general metadata for the specific element.
#[repr(C)]
pub struct ContentVtable<T: 'static = RawContent> {
/// The element's normal name, as in code.
pub(super) name: &'static str,
/// The element's title-cased name.
pub(super) title: &'static str,
/// The element's documentation (as Markdown).
pub(super) docs: &'static str,
/// Search keywords for the documentation.
pub(super) keywords: &'static [&'static str],
/// Subvtables for all fields of the element.
pub(super) fields: &'static [FieldVtable<T>],
/// Determines the ID for a field name. This is a separate function instead
/// of searching through `fields` so that Rust can generate optimized code
/// for the string matching.
pub(super) field_id: fn(name: &str) -> Option<u8>,
/// The constructor of the element.
pub(super) construct: fn(&mut Engine, &mut Args) -> SourceResult<Content>,
/// The set rule of the element.
pub(super) set: fn(&mut Engine, &mut Args) -> SourceResult<Styles>,
/// The element's local name in a specific lang-region pairing.
pub(super) local_name: Option<fn(Lang, Option<Region>) -> &'static str>,
/// Produces the associated [`Scope`] of the element.
pub(super) scope: fn() -> Scope,
/// If the `capability` function returns `Some(p)`, then `p` must be a valid
/// pointer to a native Rust vtable of `Packed<Self>` w.r.t to the trait `C`
/// where `capability` is `TypeId::of::<dyn C>()`.
pub(super) capability: fn(capability: TypeId) -> Option<NonNull<()>>,
/// The `Drop` impl (for the whole raw content). The content must have a
/// reference count of zero and may not be used anymore after `drop` was
/// called.
pub(super) drop: unsafe fn(&mut RawContent),
/// The `Clone` impl (for the whole raw content).
pub(super) clone: unsafe fn(&T) -> RawContent,
/// The `Hash` impl (for just the element).
pub(super) hash: unsafe fn(&T) -> u128,
/// The `Debug` impl (for just the element).
pub(super) debug: unsafe fn(&T, &mut Formatter) -> fmt::Result,
/// The `PartialEq` impl (for just the element). If this is `None`,
/// field-wise equality checks (via `FieldVtable`) should be performed.
pub(super) eq: Option<unsafe fn(&T, &T) -> bool>,
/// The `Repr` impl (for just the element). If this is `None`, a generic
/// name + fields representation should be produced.
pub(super) repr: Option<unsafe fn(&T) -> EcoString>,
/// Produces a reference to a `static` variable holding a `LazyElementStore`
/// that is unique for this element and can be populated with data that is
/// somewhat costly to initialize at runtime and shouldn't be initialized
/// over and over again. Must be a function rather than a direct reference
/// so that we can store the vtable in a `const` without Rust complaining
/// about the presence of interior mutability.
pub(super) store: fn() -> &'static LazyElementStore,
}
impl ContentVtable {
/// Creates the vtable for an element.
pub const fn new<E: NativeElement>(
name: &'static str,
title: &'static str,
docs: &'static str,
fields: &'static [FieldVtable<Packed<E>>],
field_id: fn(name: &str) -> Option<u8>,
capability: fn(TypeId) -> Option<NonNull<()>>,
store: fn() -> &'static LazyElementStore,
) -> ContentVtable<Packed<E>> {
ContentVtable {
name,
title,
docs,
keywords: &[],
fields,
field_id,
construct: <E as Construct>::construct,
set: <E as Set>::set,
local_name: None,
scope: || Scope::new(),
capability,
drop: RawContent::drop_impl::<E>,
clone: RawContent::clone_impl::<E>,
hash: |elem| typst_utils::hash128(elem.as_ref()),
debug: |elem, f| Debug::fmt(elem.as_ref(), f),
eq: None,
repr: None,
store,
}
}
/// Retrieves the vtable of the element with the given ID.
pub fn field(&self, id: u8) -> Option<&'static FieldVtable> {
self.fields.get(usize::from(id))
}
}
impl<E: NativeElement> ContentVtable<Packed<E>> {
/// Attaches search keywords for the documentation.
pub const fn with_keywords(mut self, keywords: &'static [&'static str]) -> Self {
self.keywords = keywords;
self
}
/// Takes a [`Repr`] impl into account.
pub const fn with_repr(mut self) -> Self
where
E: Repr,
{
self.repr = Some(|e| E::repr(&**e));
self
}
/// Takes a [`PartialEq`] impl into account.
pub const fn with_partial_eq(mut self) -> Self
where
E: PartialEq,
{
self.eq = Some(|a, b| E::eq(&**a, &**b));
self
}
/// Takes a [`LocalName`] impl into account.
pub const fn with_local_name(mut self) -> Self
where
Packed<E>: LocalName,
{
self.local_name = Some(<Packed<E> as LocalName>::local_name);
self
}
/// Takes a [`NativeScope`] impl into account.
pub const fn with_scope(mut self) -> Self
where
E: NativeScope,
{
self.scope = || E::scope();
self
}
/// Type-erases the data.
pub const fn erase(self) -> ContentVtable {
// Safety:
// - `ContentVtable` is `repr(C)`.
// - `ContentVtable` does not hold any `E`-specific data except for
// function pointers.
// - All functions pointers have the same memory layout.
// - All functions containing `E` are marked as unsafe and callers need
// to uphold the guarantee that they only call them with raw content
// that is of type `E`.
// - `Packed<E>` and `RawContent` have the exact same memory layout
// because of `repr(transparent)`.
unsafe {
std::mem::transmute::<ContentVtable<Packed<E>>, ContentVtable<RawContent>>(
self,
)
}
}
}
impl<T> ContentHandle<T> {
/// Provides safe access to operations for the field with the given `id`.
pub(super) fn field(self, id: u8) -> Option<FieldHandle<T>> {
self.fields.get(usize::from(id)).map(|vtable| {
// Safety: Field vtables are of same type as the content vtable.
unsafe { Handle::new(self.0, vtable) }
})
}
/// Provides safe access to all field operations.
pub(super) fn fields(self) -> impl Iterator<Item = FieldHandle<T>>
where
T: Copy,
{
self.fields.iter().map(move |vtable| {
// Safety: Field vtables are of same type as the content vtable.
unsafe { Handle::new(self.0, vtable) }
})
}
}
impl ContentHandle<&RawContent> {
/// See [`ContentVtable::debug`].
pub fn debug(&self, f: &mut Formatter) -> fmt::Result {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.debug)(self.0, f) }
}
/// See [`ContentVtable::repr`].
pub fn repr(&self) -> Option<EcoString> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { self.1.repr.map(|f| f(self.0)) }
}
/// See [`ContentVtable::clone`].
pub fn clone(&self) -> RawContent {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.clone)(self.0) }
}
/// See [`ContentVtable::hash`].
pub fn hash(&self) -> u128 {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.hash)(self.0) }
}
}
impl ContentHandle<&mut RawContent> {
/// See [`ContentVtable::drop`].
pub unsafe fn drop(&mut self) {
// Safety:
// - `Handle` has the invariant that the vtable is matching.
// - The caller satifies the requirements of `drop`
unsafe { (self.1.drop)(self.0) }
}
}
impl ContentHandle<(&RawContent, &RawContent)> {
/// See [`ContentVtable::eq`].
pub fn eq(&self) -> Option<bool> {
// Safety: `Handle` has the invariant that the vtable is matching.
let (a, b) = self.0;
unsafe { self.1.eq.map(|f| f(a, b)) }
}
}
/// A vtable for performing field-specific actions on type-erased
/// content. Also contains general metadata for the specific field.
#[repr(C)]
pub struct FieldVtable<T: 'static = RawContent> {
/// The field's name, as in code.
pub(super) name: &'static str,
/// The fields's documentation (as Markdown).
pub(super) docs: &'static str,
/// Whether the field's parameter is positional.
pub(super) positional: bool,
/// Whether the field's parameter is variadic.
pub(super) variadic: bool,
/// Whether the field's parameter is required.
pub(super) required: bool,
/// Whether the field can be set via a set rule.
pub(super) settable: bool,
/// Whether the field is synthesized (i.e. initially not present).
pub(super) synthesized: bool,
/// Reflects what types the field's parameter accepts.
pub(super) input: fn() -> CastInfo,
/// Produces the default value of the field, if any. This would e.g. be
/// `None` for a required parameter.
pub(super) default: Option<fn() -> Value>,
/// Whether the field is set on the given element. Always true for required
/// fields, but can be false for settable or synthesized fields.
pub(super) has: unsafe fn(elem: &T) -> bool,
/// Retrieves the field and [turns it into a
/// value](crate::foundations::IntoValue).
pub(super) get: unsafe fn(elem: &T) -> Option<Value>,
/// Retrieves the field given styles. The resulting value may come from the
/// element, the style chain, or a mix (if it's a
/// [`Fold`](crate::foundations::Fold) field).
pub(super) get_with_styles: unsafe fn(elem: &T, StyleChain) -> Option<Value>,
/// Retrieves the field just from the styles.
pub(super) get_from_styles: fn(StyleChain) -> Option<Value>,
/// Sets the field from the styles if it is currently unset. (Or merges
/// with the style data in case of a `Fold` field).
pub(super) materialize: unsafe fn(elem: &mut T, styles: StyleChain),
/// Compares the field for equality.
pub(super) eq: unsafe fn(a: &T, b: &T) -> bool,
}
impl FieldHandle<&RawContent> {
/// See [`FieldVtable::has`].
pub fn has(&self) -> bool {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.has)(self.0) }
}
/// See [`FieldVtable::get`].
pub fn get(&self) -> Option<Value> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.get)(self.0) }
}
/// See [`FieldVtable::get_with_styles`].
pub fn get_with_styles(&self, styles: StyleChain) -> Option<Value> {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.get_with_styles)(self.0, styles) }
}
}
impl FieldHandle<&mut RawContent> {
/// See [`FieldVtable::materialize`].
pub fn materialize(&mut self, styles: StyleChain) {
// Safety: `Handle` has the invariant that the vtable is matching.
unsafe { (self.1.materialize)(self.0, styles) }
}
}
impl FieldHandle<(&RawContent, &RawContent)> {
/// See [`FieldVtable::eq`].
pub fn eq(&self) -> bool {
// Safety: `Handle` has the invariant that the vtable is matching.
let (a, b) = self.0;
unsafe { (self.1.eq)(a, b) }
}
}

View File

@ -3,7 +3,7 @@ use comemo::Track;
use crate::diag::{bail, Hint, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value,
elem, Args, Construct, Content, Func, ShowFn, StyleChain, Value,
};
use crate::introspection::{Locatable, Location};
@ -61,7 +61,7 @@ fn require<T>(val: Option<T>) -> HintedStrResult<T> {
}
/// Executes a `context` block.
#[elem(Construct, Locatable, Show)]
#[elem(Construct, Locatable)]
pub struct ContextElem {
/// The function to call with the context.
#[required]
@ -75,11 +75,8 @@ impl Construct for ContextElem {
}
}
impl Show for Packed<ContextElem> {
#[typst_macros::time(name = "context", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let loc = self.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
Ok(self.func.call::<[Value; 0]>(engine, context.track(), [])?.display())
}
}
pub const CONTEXT_RULE: ShowFn<ContextElem> = |elem, engine, styles| {
let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
Ok(elem.func.call::<[Value; 0]>(engine, context.track(), [])?.display())
};

View File

@ -17,7 +17,6 @@ mod datetime;
mod decimal;
mod dict;
mod duration;
mod element;
mod fields;
mod float;
mod func;
@ -49,7 +48,6 @@ pub use self::datetime::*;
pub use self::decimal::*;
pub use self::dict::*;
pub use self::duration::*;
pub use self::element::*;
pub use self::fields::*;
pub use self::float::*;
pub use self::func::*;

View File

@ -8,8 +8,7 @@ use typst_syntax::Span;
use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{
Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
Type, Value,
Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Value,
};
use crate::{Category, Library};
@ -149,15 +148,15 @@ impl Scope {
/// Define a native type.
#[track_caller]
pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Type::from(data))
let ty = T::ty();
self.define(ty.short_name(), ty)
}
/// Define a native element.
#[track_caller]
pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
let data = T::data();
self.define(data.name, Element::from(data))
let elem = T::ELEM;
self.define(elem.name(), elem)
}
/// Define a built-in with compile-time known name and returns a mutable

View File

@ -21,12 +21,12 @@ macro_rules! __select_where {
let mut fields = ::smallvec::SmallVec::new();
$(
fields.push((
<$ty as $crate::foundations::Fields>::Enum::$field as u8,
<$ty>::$field.index(),
$crate::foundations::IntoValue::into_value($value),
));
)*
$crate::foundations::Selector::Elem(
<$ty as $crate::foundations::NativeElement>::elem(),
<$ty as $crate::foundations::NativeElement>::ELEM,
Some(fields),
)
}};

View File

@ -179,24 +179,40 @@ impl Str {
}
/// Extracts the first grapheme cluster of the string.
/// Fails with an error if the string is empty.
///
/// Returns the provided default value if the string is empty or fails with
/// an error if no default value was specified.
#[func]
pub fn first(&self) -> StrResult<Str> {
pub fn first(
&self,
/// A default value to return if the string is empty.
#[named]
default: Option<Str>,
) -> StrResult<Str> {
self.0
.graphemes(true)
.next()
.map(Into::into)
.or(default)
.ok_or_else(string_is_empty)
}
/// Extracts the last grapheme cluster of the string.
/// Fails with an error if the string is empty.
///
/// Returns the provided default value if the string is empty or fails with
/// an error if no default value was specified.
#[func]
pub fn last(&self) -> StrResult<Str> {
pub fn last(
&self,
/// A default value to return if the string is empty.
#[named]
default: Option<Str>,
) -> StrResult<Str> {
self.0
.graphemes(true)
.next_back()
.map(Into::into)
.or(default)
.ok_or_else(string_is_empty)
}

View File

@ -1,4 +1,5 @@
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::{mem, ptr};
@ -12,8 +13,8 @@ use typst_utils::LazyHash;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr,
Selector,
cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple,
Packed, RefableProperty, Repr, Selector, SettableProperty, Target,
};
use crate::text::{FontFamily, FontList, TextElem};
@ -48,7 +49,16 @@ impl Styles {
/// If the property needs folding and the value is already contained in the
/// style map, `self` contributes the outer values and `value` is the inner
/// one.
pub fn set(&mut self, style: impl Into<Style>) {
pub fn set<E, const I: u8>(&mut self, field: Field<E, I>, value: E::Type)
where
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
self.push(Property::new(field, value));
}
/// Add a new style to the list.
pub fn push(&mut self, style: impl Into<Style>) {
self.0.push(LazyHash::new(style.into()));
}
@ -101,22 +111,25 @@ impl Styles {
}
/// Whether there is a style for the given field of the given element.
pub fn has<T: NativeElement>(&self, field: u8) -> bool {
let elem = T::elem();
pub fn has<E: NativeElement, const I: u8>(&self, _: Field<E, I>) -> bool {
let elem = E::ELEM;
self.0
.iter()
.filter_map(|style| style.property())
.any(|property| property.is_of(elem) && property.id == field)
.any(|property| property.is_of(elem) && property.id == I)
}
/// Set a font family composed of a preferred family and existing families
/// from a style chain.
pub fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
self.set(TextElem::set_font(FontList(
std::iter::once(preferred)
.chain(TextElem::font_in(existing).into_iter().cloned())
.collect(),
)));
self.set(
TextElem::font,
FontList(
std::iter::once(preferred)
.chain(existing.get_ref(TextElem::font).into_iter().cloned())
.collect(),
),
);
}
}
@ -281,14 +294,14 @@ pub struct Property {
impl Property {
/// Create a new property from a key-value pair.
pub fn new<E, T>(id: u8, value: T) -> Self
pub fn new<E, const I: u8>(_: Field<E, I>, value: E::Type) -> Self
where
E: NativeElement,
T: Debug + Clone + Hash + Send + Sync + 'static,
E: SettableProperty<I>,
E::Type: Debug + Clone + Hash + Send + Sync + 'static,
{
Self {
elem: E::elem(),
id,
elem: E::ELEM,
id: I,
value: Block::new(value),
span: Span::detached(),
liftable: false,
@ -340,8 +353,11 @@ impl Block {
}
/// Downcasts the block to the specified type.
fn downcast<T: 'static>(&self) -> Option<&T> {
self.0.as_any().downcast_ref()
fn downcast<T: 'static>(&self, func: Element, id: u8) -> &T {
self.0
.as_any()
.downcast_ref()
.unwrap_or_else(|| block_wrong_type(func, id, self))
}
}
@ -528,6 +544,103 @@ impl<'a> StyleChain<'a> {
Self { head: &root.0, tail: None }
}
/// Retrieves the value of the given field from the style chain.
///
/// A `Field` value is a zero-sized value that specifies which field of an
/// element you want to retrieve on the type-system level. It also ensures
/// that Rust can infer the correct return type.
///
/// Should be preferred over [`get_cloned`](Self::get_cloned) or
/// [`get_ref`](Self::get_ref), but is only available for [`Copy`] types.
/// For other types an explicit decision needs to be made whether cloning is
/// necessary.
pub fn get<E, const I: u8>(self, field: Field<E, I>) -> E::Type
where
E: SettableProperty<I>,
E::Type: Copy,
{
self.get_cloned(field)
}
/// Retrieves and clones the value from the style chain.
///
/// Prefer [`get`](Self::get) if the type is `Copy` and
/// [`get_ref`](Self::get_ref) if a reference suffices.
pub fn get_cloned<E, const I: u8>(self, _: Field<E, I>) -> E::Type
where
E: SettableProperty<I>,
{
if let Some(fold) = E::FOLD {
self.get_folded::<E::Type>(E::ELEM, I, fold, E::default())
} else {
self.get_unfolded::<E::Type>(E::ELEM, I)
.cloned()
.unwrap_or_else(E::default)
}
}
/// Retrieves a reference to the value of the given field from the style
/// chain.
///
/// Not possible if the value needs folding.
pub fn get_ref<E, const I: u8>(self, _: Field<E, I>) -> &'a E::Type
where
E: RefableProperty<I>,
{
self.get_unfolded(E::ELEM, I).unwrap_or_else(|| E::default_ref())
}
/// Retrieves the value and then immediately [resolves](Resolve) it.
pub fn resolve<E, const I: u8>(
self,
field: Field<E, I>,
) -> <E::Type as Resolve>::Output
where
E: SettableProperty<I>,
E::Type: Resolve,
{
self.get_cloned(field).resolve(self)
}
/// Retrieves a reference to a field, also taking into account the
/// instance's value if any.
fn get_unfolded<T: 'static>(self, func: Element, id: u8) -> Option<&'a T> {
self.find(func, id).map(|block| block.downcast(func, id))
}
/// Retrieves a reference to a field, also taking into account the
/// instance's value if any.
fn get_folded<T: 'static + Clone>(
self,
func: Element,
id: u8,
fold: fn(T, T) -> T,
default: T,
) -> T {
let iter = self
.properties(func, id)
.map(|block| block.downcast::<T>(func, id).clone());
if let Some(folded) = iter.reduce(fold) {
fold(folded, default)
} else {
default
}
}
/// Iterate over all values for the given property in the chain.
fn find(self, func: Element, id: u8) -> Option<&'a Block> {
self.properties(func, id).next()
}
/// Iterate over all values for the given property in the chain.
fn properties(self, func: Element, id: u8) -> impl Iterator<Item = &'a Block> {
self.entries()
.filter_map(|style| style.property())
.filter(move |property| property.is(func, id))
.map(|property| &property.value)
}
/// Make the given chainable the first link of this chain.
///
/// The resulting style chain contains styles from `local` as well as
@ -540,80 +653,6 @@ impl<'a> StyleChain<'a> {
Chainable::chain(local, self)
}
/// Cast the first value for the given property in the chain.
pub fn get<T: Clone + 'static>(
self,
func: Element,
id: u8,
inherent: Option<&T>,
default: impl Fn() -> T,
) -> T {
self.properties::<T>(func, id, inherent)
.next()
.cloned()
.unwrap_or_else(default)
}
/// Cast the first value for the given property in the chain,
/// returning a borrowed value.
pub fn get_ref<T: 'static>(
self,
func: Element,
id: u8,
inherent: Option<&'a T>,
default: impl Fn() -> &'a T,
) -> &'a T {
self.properties::<T>(func, id, inherent)
.next()
.unwrap_or_else(default)
}
/// Cast the first value for the given property in the chain, taking
/// `Fold` implementations into account.
pub fn get_folded<T: Fold + Clone + 'static>(
self,
func: Element,
id: u8,
inherent: Option<&T>,
default: impl Fn() -> T,
) -> T {
fn next<T: Fold>(
mut values: impl Iterator<Item = T>,
default: &impl Fn() -> T,
) -> T {
values
.next()
.map(|value| value.fold(next(values, default)))
.unwrap_or_else(default)
}
next(self.properties::<T>(func, id, inherent).cloned(), &default)
}
/// Iterate over all values for the given property in the chain.
fn properties<T: 'static>(
self,
func: Element,
id: u8,
inherent: Option<&'a T>,
) -> impl Iterator<Item = &'a T> {
inherent.into_iter().chain(
self.entries()
.filter_map(|style| style.property())
.filter(move |property| property.is(func, id))
.map(|property| &property.value)
.map(move |value| {
value.downcast().unwrap_or_else(|| {
panic!(
"attempted to read a value of a different type than was written {}.{}: {:?}",
func.name(),
func.field_name(id).unwrap(),
value
)
})
}),
)
}
/// Iterate over the entries of the chain.
pub fn entries(self) -> Entries<'a> {
Entries { inner: [].as_slice().iter(), links: self.links() }
@ -804,6 +843,9 @@ impl<T: Resolve> Resolve for Option<T> {
/// #set rect(stroke: 4pt)
/// #rect()
/// ```
///
/// Note: Folding must be associative, i.e. any implementation must satisfy
/// `fold(fold(a, b), c) == fold(a, fold(b, c))`.
pub trait Fold {
/// Fold this inner value with an outer folded value.
fn fold(self, outer: Self) -> Self;
@ -847,6 +889,9 @@ impl<T> Fold for OneOrMultiple<T> {
}
}
/// A [folding](Fold) function.
pub type FoldFn<T> = fn(T, T) -> T;
/// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns
@ -884,3 +929,139 @@ impl Fold for Depth {
Self(outer.0 + self.0)
}
}
#[cold]
fn block_wrong_type(func: Element, id: u8, value: &Block) -> ! {
panic!(
"attempted to read a value of a different type than was written {}.{}: {:?}",
func.name(),
func.field_name(id).unwrap(),
value
)
}
/// Holds native show rules.
pub struct NativeRuleMap {
rules: HashMap<(Element, Target), NativeShowRule>,
}
/// The signature of a native show rule.
pub type ShowFn<T> = fn(
elem: &Packed<T>,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content>;
impl NativeRuleMap {
/// Creates a new rule map.
///
/// Should be populated with rules for all target-element combinations that
/// are supported.
///
/// Contains built-in rules for a few special elements.
pub fn new() -> Self {
let mut rules = Self { rules: HashMap::new() };
// ContextElem is as special as SequenceElem and StyledElem and could,
// in theory, also be special cased in realization.
rules.register_builtin(crate::foundations::CONTEXT_RULE);
// CounterDisplayElem only exists because the compiler can't currently
// express the equivalent of `context counter(..).display(..)` in native
// code (no native closures).
rules.register_builtin(crate::introspection::COUNTER_DISPLAY_RULE);
// These are all only for introspection and empty on all targets.
rules.register_empty::<crate::introspection::CounterUpdateElem>();
rules.register_empty::<crate::introspection::StateUpdateElem>();
rules.register_empty::<crate::introspection::MetadataElem>();
rules.register_empty::<crate::model::PrefixInfo>();
rules
}
/// Registers a rule for all targets.
fn register_empty<T: NativeElement>(&mut self) {
self.register_builtin::<T>(|_, _, _| Ok(Content::empty()));
}
/// Registers a rule for all targets.
fn register_builtin<T: NativeElement>(&mut self, f: ShowFn<T>) {
self.register(Target::Paged, f);
self.register(Target::Html, f);
}
/// Registers a rule for a target.
///
/// Panics if a rule already exists for this target-element combination.
pub fn register<T: NativeElement>(&mut self, target: Target, f: ShowFn<T>) {
let res = self.rules.insert((T::ELEM, target), NativeShowRule::new(f));
if res.is_some() {
panic!(
"duplicate native show rule for `{}` on {target:?} target",
T::ELEM.name()
)
}
}
/// Retrieves the rule that applies to the `content` on the current
/// `target`.
pub fn get(&self, target: Target, content: &Content) -> Option<NativeShowRule> {
self.rules.get(&(content.func(), target)).copied()
}
}
impl Default for NativeRuleMap {
fn default() -> Self {
Self::new()
}
}
pub use rule::NativeShowRule;
mod rule {
use super::*;
/// The show rule for a native element.
#[derive(Copy, Clone)]
pub struct NativeShowRule {
/// The element to which this rule applies.
elem: Element,
/// Must only be called with content of the appropriate type.
f: unsafe fn(
elem: &Content,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content>,
}
impl NativeShowRule {
/// Create a new type-erased show rule.
pub fn new<T: NativeElement>(f: ShowFn<T>) -> Self {
Self {
elem: T::ELEM,
// Safety: The two function pointer types only differ in the
// first argument, which changes from `&Packed<T>` to
// `&Content`. `Packed<T>` is a transparent wrapper around
// `Content`. The resulting function is unsafe to call because
// content of the correct type must be passed to it.
#[allow(clippy::missing_transmute_annotations)]
f: unsafe { std::mem::transmute(f) },
}
}
/// Applies the rule to content. Panics if the content is of the wrong
/// type.
pub fn apply(
&self,
content: &Content,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content> {
assert_eq!(content.elem(), self.elem);
// Safety: We just checked that the element is of the correct type.
unsafe { (self.f)(content, engine, styles) }
}
}
}

View File

@ -4,7 +4,7 @@ use crate::diag::HintedStrResult;
use crate::foundations::{elem, func, Cast, Context};
/// The export target.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)]
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Target {
/// The target that is used for paged, fully laid-out content.
#[default]
@ -73,5 +73,5 @@ pub struct TargetElem {
/// ```
#[func(contextual)]
pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?))
Ok(context.styles()?.get(TargetElem::target))
}

View File

@ -1,23 +1,12 @@
//! HTML output.
mod dom;
mod typed;
pub use self::dom::*;
use ecow::EcoString;
use crate::foundations::{elem, Content, Module, Scope};
/// Create a module with all HTML definitions.
pub fn module() -> Module {
let mut html = Scope::deduplicating();
html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
self::typed::define(&mut html);
Module::new("html", html)
}
use crate::foundations::{elem, Content};
/// An HTML element that can contain Typst content.
///
@ -47,21 +36,22 @@ pub struct HtmlElem {
pub tag: HtmlTag,
/// The element's HTML attributes.
#[borrowed]
pub attrs: HtmlAttrs,
/// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
impl HtmlElem {
/// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.get_or_insert_with(Default::default).push(attr, value);
self.attrs
.as_option_mut()
.get_or_insert_with(Default::default)
.push(attr, value);
self
}
}

View File

@ -12,7 +12,7 @@ use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
Selector, Show, Smart, Str, StyleChain, Value,
Selector, ShowFn, Smart, Str, StyleChain, Value,
};
use crate::introspection::{Introspector, Locatable, Location, Tag};
use crate::layout::{Frame, FrameItem, PageElem};
@ -338,7 +338,7 @@ impl Counter {
/// The selector relevant for this counter's updates.
fn selector(&self) -> Selector {
let mut selector = select_where!(CounterUpdateElem, Key => self.0.clone());
let mut selector = select_where!(CounterUpdateElem, key => self.0.clone());
if let CounterKey::Selector(key) = &self.0 {
selector = Selector::Or(eco_vec![selector, key.clone()]);
@ -367,16 +367,16 @@ impl Counter {
.or_else(|| {
let styles = styles?;
match self.0 {
CounterKey::Page => PageElem::numbering_in(styles).clone(),
CounterKey::Page => styles.get_cloned(PageElem::numbering),
CounterKey::Selector(Selector::Elem(func, _)) => {
if func == HeadingElem::elem() {
HeadingElem::numbering_in(styles).clone()
} else if func == FigureElem::elem() {
FigureElem::numbering_in(styles).clone()
} else if func == EquationElem::elem() {
EquationElem::numbering_in(styles).clone()
} else if func == FootnoteElem::elem() {
Some(FootnoteElem::numbering_in(styles).clone())
if func == HeadingElem::ELEM {
styles.get_cloned(HeadingElem::numbering)
} else if func == FigureElem::ELEM {
styles.get_cloned(FigureElem::numbering)
} else if func == EquationElem::ELEM {
styles.get_cloned(EquationElem::numbering)
} else if func == FootnoteElem::ELEM {
Some(styles.get_cloned(FootnoteElem::numbering))
} else {
None
}
@ -398,7 +398,7 @@ impl Counter {
/// Selects all state updates.
pub fn select_any() -> Selector {
CounterUpdateElem::elem().select()
CounterUpdateElem::ELEM.select()
}
}
@ -565,14 +565,14 @@ pub enum CounterKey {
cast! {
CounterKey,
self => match self {
Self::Page => PageElem::elem().into_value(),
Self::Page => PageElem::ELEM.into_value(),
Self::Selector(v) => v.into_value(),
Self::Str(v) => v.into_value(),
},
v: Str => Self::Str(v),
v: Label => Self::Selector(Selector::Label(v)),
v: Element => {
if v == PageElem::elem() {
if v == PageElem::ELEM {
Self::Page
} else {
Self::Selector(LocatableSelector::from_value(v.into_value())?.0)
@ -683,8 +683,8 @@ cast! {
}
/// Executes an update of a counter.
#[elem(Construct, Locatable, Show, Count)]
struct CounterUpdateElem {
#[elem(Construct, Locatable, Count)]
pub struct CounterUpdateElem {
/// The key that identifies the counter.
#[required]
key: CounterKey,
@ -701,12 +701,6 @@ impl Construct for CounterUpdateElem {
}
}
impl Show for Packed<CounterUpdateElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}
impl Count for Packed<CounterUpdateElem> {
fn update(&self) -> Option<CounterUpdate> {
Some(self.update.clone())
@ -714,7 +708,7 @@ impl Count for Packed<CounterUpdateElem> {
}
/// Executes a display of a counter.
#[elem(Construct, Locatable, Show)]
#[elem(Construct, Locatable)]
pub struct CounterDisplayElem {
/// The counter.
#[required]
@ -738,20 +732,18 @@ impl Construct for CounterDisplayElem {
}
}
impl Show for Packed<CounterDisplayElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self
.counter
.display_impl(
engine,
self.location().unwrap(),
self.numbering.clone(),
self.both,
Some(styles),
)?
.display())
}
}
pub const COUNTER_DISPLAY_RULE: ShowFn<CounterDisplayElem> = |elem, engine, styles| {
Ok(elem
.counter
.display_impl(
engine,
elem.location().unwrap(),
elem.numbering.clone(),
elem.both,
Some(styles),
)?
.display())
};
/// An specialized handler of the page counter that tracks both the physical
/// and the logical page counter.

View File

@ -1,6 +1,4 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value};
use crate::foundations::{elem, Value};
use crate::introspection::Locatable;
/// Exposes a value to the query system without producing visible content.
@ -24,15 +22,9 @@ use crate::introspection::Locatable;
/// query(<note>).first().value
/// }
/// ```
#[elem(Show, Locatable)]
#[elem(Locatable)]
pub struct MetadataElem {
/// The value to embed into the document.
#[required]
pub value: Value,
}
impl Show for Packed<MetadataElem> {
fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -6,8 +6,7 @@ use crate::diag::{bail, At, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain,
Value,
LocatableSelector, NativeElement, Repr, Selector, Str, Value,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::routines::Routines;
@ -259,12 +258,12 @@ impl State {
/// The selector for this state's updates.
fn selector(&self) -> Selector {
select_where!(StateUpdateElem, Key => self.key.clone())
select_where!(StateUpdateElem, key => self.key.clone())
}
/// Selects all state updates.
pub fn select_any() -> Selector {
StateUpdateElem::elem().select()
StateUpdateElem::ELEM.select()
}
}
@ -372,8 +371,8 @@ cast! {
}
/// Executes a display of a state.
#[elem(Construct, Locatable, Show)]
struct StateUpdateElem {
#[elem(Construct, Locatable)]
pub struct StateUpdateElem {
/// The key that identifies the state.
#[required]
key: Str,
@ -389,9 +388,3 @@ impl Construct for StateUpdateElem {
bail!(args.span, "cannot be constructed manually");
}
}
impl Show for Packed<StateUpdateElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -2,11 +2,10 @@ use std::ops::Add;
use ecow::{eco_format, EcoString};
use crate::diag::{bail, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{
cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed,
Reflect, Repr, Resolve, Show, StyleChain, Value,
cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Reflect,
Repr, Resolve, StyleChain, Value,
};
use crate::layout::{Abs, Axes, Axis, Dir, Side};
use crate::text::TextElem;
@ -73,7 +72,7 @@ use crate::text::TextElem;
/// ```example
/// Start #h(1fr) End
/// ```
#[elem(Show)]
#[elem]
pub struct AlignElem {
/// The [alignment] along both axes.
///
@ -97,13 +96,6 @@ pub struct AlignElem {
pub body: Content,
}
impl Show for Packed<AlignElem> {
#[typst_macros::time(name = "align", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().aligned(self.alignment(styles)))
}
}
/// Where to align something along an axis.
///
/// Possible values are:
@ -277,7 +269,7 @@ impl Resolve for Alignment {
type Output = Axes<FixedAlignment>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -378,7 +370,7 @@ impl Resolve for HAlignment {
type Output = FixedAlignment;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -414,7 +406,7 @@ impl Resolve for OuterHAlignment {
type Output = FixedAlignment;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}
@ -636,7 +628,7 @@ where
type Output = Axes<FixedAlignment>;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.fix(TextElem::dir_in(styles))
self.fix(styles.resolve(TextElem::dir))
}
}

View File

@ -1,9 +1,7 @@
use std::num::NonZeroUsize;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{BlockElem, Length, Ratio, Rel};
use crate::foundations::{elem, Content};
use crate::layout::{Length, Ratio, Rel};
/// Separates a region into multiple equally sized columns.
///
@ -41,7 +39,7 @@ use crate::layout::{BlockElem, Length, Ratio, Rel};
///
/// #lorem(40)
/// ```
#[elem(Show)]
#[elem]
pub struct ColumnsElem {
/// The number of columns.
#[positional]
@ -49,7 +47,6 @@ pub struct ColumnsElem {
pub count: NonZeroUsize,
/// The size of the gutter space between each column.
#[resolve]
#[default(Ratio::new(0.04).into())]
pub gutter: Rel<Length>,
@ -58,14 +55,6 @@ pub struct ColumnsElem {
pub body: Content,
}
impl Show for Packed<ColumnsElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns)
.pack()
.spanned(self.span()))
}
}
/// Forces a column break.
///
/// The function will behave like a [page break]($pagebreak) when used in a

View File

@ -51,7 +51,6 @@ pub struct BoxElem {
/// ```example
/// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
/// ```
#[resolve]
pub baseline: Rel<Length>,
/// The box's background color. See the
@ -60,13 +59,11 @@ pub struct BoxElem {
/// The box's border color. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Stroke>>>,
/// How much to round the box's corners. See the
/// [rectangle's documentation]($rect.radius) for more details.
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
@ -78,7 +75,6 @@ pub struct BoxElem {
/// ```example
/// #rect(inset: 0pt)[Tight]
/// ```
#[resolve]
#[fold]
pub inset: Sides<Option<Rel<Length>>>,
@ -97,7 +93,6 @@ pub struct BoxElem {
/// radius: 2pt,
/// )[rectangle].
/// ```
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
@ -119,7 +114,6 @@ pub struct BoxElem {
/// The contents of the box.
#[positional]
#[borrowed]
pub body: Option<Content>,
}
@ -262,25 +256,21 @@ pub struct BlockElem {
/// The block's border color. See the
/// [rectangle's documentation]($rect.stroke) for more details.
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Stroke>>>,
/// How much to round the block's corners. See the
/// [rectangle's documentation]($rect.radius) for more details.
#[resolve]
#[fold]
pub radius: Corners<Option<Rel<Length>>>,
/// How much to pad the block's content. See the
/// [box's documentation]($box.inset) for more details.
#[resolve]
#[fold]
pub inset: Sides<Option<Rel<Length>>>,
/// How much to expand the block's size without affecting the layout. See
/// the [box's documentation]($box.outset) for more details.
#[resolve]
#[fold]
pub outset: Sides<Option<Rel<Length>>>,
@ -358,7 +348,6 @@ pub struct BlockElem {
/// The contents of the block.
#[positional]
#[borrowed]
pub body: Option<BlockBody>,
}

View File

@ -167,7 +167,7 @@ impl Resolve for Em {
if self.is_zero() {
Abs::zero()
} else {
self.at(TextElem::size_in(styles))
self.at(styles.resolve(TextElem::size))
}
}
}

View File

@ -11,10 +11,10 @@ use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
IntoValue, Packed, Reflect, Resolve, Smart, StyleChain, Value,
};
use crate::layout::{
Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
Alignment, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
};
use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
use crate::visualize::{Paint, Stroke};
@ -136,7 +136,7 @@ use crate::visualize::{Paint, Stroke};
///
/// Furthermore, strokes of a repeated grid header or footer will take
/// precedence over regular cell strokes.
#[elem(scope, Show)]
#[elem(scope)]
pub struct GridElem {
/// The column sizes.
///
@ -144,14 +144,12 @@ pub struct GridElem {
/// with that many `{auto}`-sized columns. Note that opposed to rows and
/// gutters, providing a single track size will only ever create a single
/// column.
#[borrowed]
pub columns: TrackSizings,
/// The row sizes.
///
/// If there are more cells than fit the defined rows, the last row is
/// repeated until there are no more cells.
#[borrowed]
pub rows: TrackSizings,
/// The gaps between rows and columns.
@ -169,12 +167,10 @@ pub struct GridElem {
let gutter = args.named("gutter")?;
args.named("column-gutter")?.or_else(|| gutter.clone())
)]
#[borrowed]
pub column_gutter: TrackSizings,
/// The gaps between rows.
#[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
#[borrowed]
pub row_gutter: TrackSizings,
/// How to fill the cells.
@ -197,7 +193,6 @@ pub struct GridElem {
/// [O], [X], [O], [X],
/// )
/// ```
#[borrowed]
pub fill: Celled<Option<Paint>>,
/// How to align the cells' content.
@ -209,7 +204,6 @@ pub struct GridElem {
///
/// You can find an example for this argument at the
/// [`table.align`]($table.align) parameter.
#[borrowed]
pub align: Celled<Smart<Alignment>>,
/// How to [stroke]($stroke) the cells.
@ -289,7 +283,6 @@ pub struct GridElem {
/// ),
/// )
/// ```
#[resolve]
#[fold]
pub stroke: Celled<Sides<Option<Option<Arc<Stroke>>>>>,
@ -327,14 +320,6 @@ impl GridElem {
type GridFooter;
}
impl Show for Packed<GridElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid)
.pack()
.spanned(self.span()))
}
}
/// Track sizing definitions.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
@ -541,7 +526,6 @@ pub struct GridHLine {
///
/// Specifying `{none}` removes any lines previously placed across this
/// line's range, including hlines or per-cell stroke below it.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
@ -596,7 +580,6 @@ pub struct GridVLine {
///
/// Specifying `{none}` removes any lines previously placed across this
/// line's range, including vlines or per-cell stroke below it.
#[resolve]
#[fold]
#[default(Some(Arc::new(Stroke::default())))]
pub stroke: Option<Arc<Stroke>>,
@ -657,7 +640,7 @@ pub struct GridVLine {
/// which allows you, for example, to apply styles based on a cell's position.
/// Refer to the examples of the [`table.cell`]($table.cell) element to learn
/// more about this.
#[elem(name = "cell", title = "Grid Cell", Show)]
#[elem(name = "cell", title = "Grid Cell")]
pub struct GridCell {
/// The cell's body.
#[required]
@ -742,7 +725,6 @@ pub struct GridCell {
pub inset: Smart<Sides<Option<Rel<Length>>>>,
/// The cell's [stroke]($grid.stroke) override.
#[resolve]
#[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
@ -758,12 +740,6 @@ cast! {
v: Content => v.into(),
}
impl Show for Packed<GridCell> {
fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles))
}
}
impl Default for Packed<GridCell> {
fn default() -> Self {
Packed::new(
@ -784,28 +760,6 @@ impl From<Content> for GridCell {
}
}
/// Function with common code to display a grid cell or table cell.
pub(crate) fn show_grid_cell(
mut body: Content,
inset: Smart<Sides<Option<Rel<Length>>>>,
align: Smart<Alignment>,
) -> SourceResult<Content> {
let inset = inset.unwrap_or_default().map(Option::unwrap_or_default);
if inset != Sides::default() {
// Only pad if some inset is not 0pt.
// Avoids a bug where using .padded() in any way inside Show causes
// alignment in align(...) to break.
body = body.padded(inset);
}
if let Smart::Custom(alignment) = align {
body = body.aligned(alignment);
}
Ok(body)
}
/// A value that can be configured per cell.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Celled<T> {

View File

@ -31,14 +31,14 @@ pub fn grid_to_cellgrid<'a>(
locator: Locator<'a>,
styles: StyleChain,
) -> SourceResult<CellGrid<'a>> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let inset = elem.inset.get_cloned(styles);
let align = elem.align.get_ref(styles);
let columns = elem.columns.get_ref(styles);
let rows = elem.rows.get_ref(styles);
let column_gutter = elem.column_gutter.get_ref(styles);
let row_gutter = elem.row_gutter.get_ref(styles);
let fill = elem.fill.get_ref(styles);
let stroke = elem.stroke.resolve(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
@ -47,13 +47,13 @@ pub fn grid_to_cellgrid<'a>(
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
let children = elem.children.iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
level: header.level(styles),
repeat: header.repeat.get(styles),
level: header.level.get(styles),
span: header.span(),
items: header.children.iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
repeat: footer.repeat.get(styles),
span: footer.span(),
items: footer.children.iter().map(resolve_item),
},
@ -85,14 +85,14 @@ pub fn table_to_cellgrid<'a>(
locator: Locator<'a>,
styles: StyleChain,
) -> SourceResult<CellGrid<'a>> {
let inset = elem.inset(styles);
let align = elem.align(styles);
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let inset = elem.inset.get_cloned(styles);
let align = elem.align.get_ref(styles);
let columns = elem.columns.get_ref(styles);
let rows = elem.rows.get_ref(styles);
let column_gutter = elem.column_gutter.get_ref(styles);
let row_gutter = elem.row_gutter.get_ref(styles);
let fill = elem.fill.get_ref(styles);
let stroke = elem.stroke.resolve(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
@ -101,13 +101,13 @@ pub fn table_to_cellgrid<'a>(
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
let children = elem.children.iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
level: header.level(styles),
repeat: header.repeat.get(styles),
level: header.level.get(styles),
span: header.span(),
items: header.children.iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
repeat: footer.repeat.get(styles),
span: footer.span(),
items: footer.children.iter().map(resolve_item),
},
@ -137,27 +137,27 @@ fn grid_item_to_resolvable(
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
y: hline.y.get(styles),
start: hline.start.get(styles),
end: hline.end.get(styles),
stroke: hline.stroke.resolve(styles),
span: hline.span(),
position: match hline.position(styles) {
position: match hline.position.get(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
x: vline.x.get(styles),
start: vline.start.get(styles),
end: vline.end.get(styles),
stroke: vline.stroke.resolve(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
position: match vline.position.get(styles) {
OuterHAlignment::Left if styles.resolve(TextElem::dir) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
OuterHAlignment::Right if styles.resolve(TextElem::dir) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
@ -174,27 +174,27 @@ fn table_item_to_resolvable(
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
y: hline.y.get(styles),
start: hline.start.get(styles),
end: hline.end.get(styles),
stroke: hline.stroke.resolve(styles),
span: hline.span(),
position: match hline.position(styles) {
position: match hline.position.get(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
x: vline.x.get(styles),
start: vline.start.get(styles),
end: vline.end.get(styles),
stroke: vline.stroke.resolve(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
position: match vline.position.get(styles) {
OuterHAlignment::Left if styles.resolve(TextElem::dir) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
OuterHAlignment::Right if styles.resolve(TextElem::dir) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
@ -219,12 +219,12 @@ impl ResolvableCell for Packed<TableCell> {
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let colspan = cell.colspan.get(styles);
let rowspan = cell.rowspan.get(styles);
let breakable = cell.breakable.get(styles).unwrap_or(breakable);
let fill = cell.fill.get_cloned(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let cell_stroke = cell.stroke.resolve(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
@ -238,22 +238,22 @@ impl ResolvableCell for Packed<TableCell> {
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
cell.x.set(Smart::Custom(x));
cell.y.set(Smart::Custom(y));
cell.fill.set(Smart::Custom(fill.clone()));
cell.align.set(match align {
Smart::Custom(align) => Smart::Custom(
cell.align.get(styles).map_or(align, |inner| inner.fold(align)),
),
// Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
Smart::Auto => cell.align.get(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
cell.inset.set(Smart::Custom(
cell.inset.get(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
cell.stroke.set(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
@ -266,7 +266,7 @@ impl ResolvableCell for Packed<TableCell> {
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
cell.breakable.set(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
@ -280,19 +280,19 @@ impl ResolvableCell for Packed<TableCell> {
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
self.x.get(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
self.y.get(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
self.colspan.get(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
self.rowspan.get(styles)
}
fn span(&self) -> Span {
@ -314,12 +314,12 @@ impl ResolvableCell for Packed<GridCell> {
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let colspan = cell.colspan.get(styles);
let rowspan = cell.rowspan.get(styles);
let breakable = cell.breakable.get(styles).unwrap_or(breakable);
let fill = cell.fill.get_cloned(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let cell_stroke = cell.stroke.resolve(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
@ -333,22 +333,22 @@ impl ResolvableCell for Packed<GridCell> {
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
cell.x.set(Smart::Custom(x));
cell.y.set(Smart::Custom(y));
cell.fill.set(Smart::Custom(fill.clone()));
cell.align.set(match align {
Smart::Custom(align) => Smart::Custom(
cell.align.get(styles).map_or(align, |inner| inner.fold(align)),
),
// Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
Smart::Auto => cell.align.get(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
cell.inset.set(Smart::Custom(
cell.inset.get(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
cell.stroke.set(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
@ -361,7 +361,7 @@ impl ResolvableCell for Packed<GridCell> {
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
cell.breakable.set(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
@ -375,19 +375,19 @@ impl ResolvableCell for Packed<GridCell> {
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
self.x.get(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
self.y.get(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
self.colspan.get(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
self.rowspan.get(styles)
}
fn span(&self) -> Span {

View File

@ -1,6 +1,4 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
use crate::foundations::{elem, Content};
/// Hides content without affecting layout.
///
@ -14,7 +12,7 @@ use crate::foundations::{elem, Content, Packed, Show, StyleChain};
/// Hello Jane \
/// #hide[Hello] Joe
/// ```
#[elem(Show)]
#[elem]
pub struct HideElem {
/// The content to hide.
#[required]
@ -25,10 +23,3 @@ pub struct HideElem {
#[ghost]
pub hidden: bool,
}
impl Show for Packed<HideElem> {
#[typst_macros::time(name = "hide", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone().styled(HideElem::set_hidden(true)))
}
}

View File

@ -1,13 +1,7 @@
use comemo::Track;
use typst_syntax::Span;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain,
};
use crate::foundations::{elem, func, Content, Func, NativeElement};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Size};
/// Provides access to the current outer container's (or page's, if none)
/// dimensions (width and height).
@ -86,37 +80,9 @@ pub fn layout(
}
/// Executes a `layout` call.
#[elem(Locatable, Show)]
struct LayoutElem {
#[elem(Locatable)]
pub struct LayoutElem {
/// The function to call with the outer container's (or page's) size.
#[required]
func: Func,
}
impl Show for Packed<LayoutElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(
self.clone(),
|elem, engine, locator, styles, regions| {
// Gets the current region's base size, which will be the size of the
// outer container, or of the page if there is no such container.
let Size { x, y } = regions.base();
let loc = elem.location().unwrap();
let context = Context::new(Some(loc), Some(styles));
let result = elem
.func
.call(
engine,
context.track(),
[dict! { "width" => x, "height" => y }],
)?
.display();
(engine.routines.layout_fragment)(
engine, &result, locator, styles, regions,
)
},
)
.pack()
.spanned(self.span()))
}
pub func: Func,
}

View File

@ -1,7 +1,5 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{BlockElem, Length, Rel};
use crate::foundations::{elem, Content};
use crate::layout::{Length, Rel};
/// Adds spacing around content.
///
@ -16,7 +14,7 @@ use crate::layout::{BlockElem, Length, Rel};
/// _Typing speeds can be
/// measured in words per minute._
/// ```
#[elem(title = "Padding", Show)]
#[elem(title = "Padding")]
pub struct PadElem {
/// The padding at the left side.
#[parse(
@ -55,11 +53,3 @@ pub struct PadElem {
#[required]
pub body: Content,
}
impl Show for Packed<PadElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad)
.pack()
.spanned(self.span()))
}
}

View File

@ -60,7 +60,6 @@ pub struct PageElem {
/// box(square(width: 1cm))
/// }
/// ```
#[resolve]
#[parse(
let paper = args.named_or_find::<Paper>("paper")?;
args.named("width")?
@ -77,7 +76,6 @@ pub struct PageElem {
/// page set rule. Most examples throughout this documentation use `{auto}`
/// for the height of the page to dynamically grow and shrink to fit their
/// content.
#[resolve]
#[parse(
args.named("height")?
.or_else(|| paper.map(|paper| Smart::Custom(paper.height().into())))
@ -201,7 +199,6 @@ pub struct PageElem {
/// #set text(fill: rgb("fdfdfd"))
/// *Dark mode enabled.*
/// ```
#[borrowed]
#[ghost]
pub fill: Smart<Option<Paint>>,
@ -219,7 +216,6 @@ pub struct PageElem {
///
/// #lorem(48)
/// ```
#[borrowed]
#[ghost]
pub numbering: Option<Numbering>,
@ -276,12 +272,10 @@ pub struct PageElem {
///
/// #lorem(19)
/// ```
#[borrowed]
#[ghost]
pub header: Smart<Option<Content>>,
/// The amount the header is raised into the top margin.
#[resolve]
#[default(Ratio::new(0.3).into())]
#[ghost]
pub header_ascent: Rel<Length>,
@ -314,12 +308,10 @@ pub struct PageElem {
///
/// #lorem(48)
/// ```
#[borrowed]
#[ghost]
pub footer: Smart<Option<Content>>,
/// The amount the footer is lowered into the bottom margin.
#[resolve]
#[default(Ratio::new(0.3).into())]
#[ghost]
pub footer_descent: Rel<Length>,
@ -340,7 +332,6 @@ pub struct PageElem {
/// In the year 2023, we plan to take
/// over the world (of typesetting).
/// ```
#[borrowed]
#[ghost]
pub background: Option<Content>,
@ -355,7 +346,6 @@ pub struct PageElem {
/// "Weak Reject" because they did
/// not understand our approach...
/// ```
#[borrowed]
#[ghost]
pub foreground: Option<Content>,

View File

@ -134,7 +134,6 @@ pub struct PlaceElem {
///
/// Has no effect if `float` is `{false}`.
#[default(Em::new(1.5).into())]
#[resolve]
pub clearance: Length,
/// The horizontal displacement of the placed content.

View File

@ -1,7 +1,5 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{BlockElem, Length};
use crate::foundations::{elem, Content};
use crate::layout::Length;
/// Repeats content to the available space.
///
@ -24,7 +22,7 @@ use crate::layout::{BlockElem, Length};
/// Berlin, the 22nd of December, 2022
/// ]
/// ```
#[elem(Show)]
#[elem]
pub struct RepeatElem {
/// The content to repeat.
#[required]
@ -39,11 +37,3 @@ pub struct RepeatElem {
#[default(true)]
pub justify: bool,
}
impl Show for Packed<RepeatElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat)
.pack()
.spanned(self.span()))
}
}

View File

@ -1,9 +1,7 @@
use std::fmt::{self, Debug, Formatter};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{BlockElem, Dir, Spacing};
use crate::foundations::{cast, elem, Content};
use crate::layout::{Dir, Spacing};
/// Arranges content and spacing horizontally or vertically.
///
@ -19,7 +17,7 @@ use crate::layout::{BlockElem, Dir, Spacing};
/// rect(width: 90pt),
/// )
/// ```
#[elem(Show)]
#[elem]
pub struct StackElem {
/// The direction along which the items are stacked. Possible values are:
///
@ -47,14 +45,6 @@ pub struct StackElem {
pub children: Vec<StackChild>,
}
impl Show for Packed<StackElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack)
.pack()
.spanned(self.span()))
}
}
/// A child of a stack element.
#[derive(Clone, PartialEq, Hash)]
pub enum StackChild {

View File

@ -1,11 +1,5 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{
Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment,
};
use crate::foundations::{cast, elem, Content, Smart};
use crate::layout::{Abs, Alignment, Angle, HAlignment, Length, Ratio, Rel, VAlignment};
/// Moves content without affecting layout.
///
@ -25,7 +19,7 @@ use crate::layout::{
/// )
/// ))
/// ```
#[elem(Show)]
#[elem]
pub struct MoveElem {
/// The horizontal displacement of the content.
pub dx: Rel<Length>,
@ -38,14 +32,6 @@ pub struct MoveElem {
pub body: Content,
}
impl Show for Packed<MoveElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move)
.pack()
.spanned(self.span()))
}
}
/// Rotates content without affecting layout.
///
/// Rotates an element by a given angle. The layout will act as if the element
@ -60,7 +46,7 @@ impl Show for Packed<MoveElem> {
/// .map(i => rotate(24deg * i)[X]),
/// )
/// ```
#[elem(Show)]
#[elem]
pub struct RotateElem {
/// The amount of rotation.
///
@ -107,14 +93,6 @@ pub struct RotateElem {
pub body: Content,
}
impl Show for Packed<RotateElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate)
.pack()
.spanned(self.span()))
}
}
/// Scales content without affecting layout.
///
/// Lets you mirror content by specifying a negative scale on a single axis.
@ -125,7 +103,7 @@ impl Show for Packed<RotateElem> {
/// #scale(x: -100%)[This is mirrored.]
/// #scale(x: -100%, reflow: true)[This is mirrored.]
/// ```
#[elem(Show)]
#[elem]
pub struct ScaleElem {
/// The scaling factor for both axes, as a positional argument. This is just
/// an optional shorthand notation for setting `x` and `y` to the same
@ -179,14 +157,6 @@ pub struct ScaleElem {
pub body: Content,
}
impl Show for Packed<ScaleElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale)
.pack()
.spanned(self.span()))
}
}
/// To what size something shall be scaled.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ScaleAmount {
@ -215,7 +185,7 @@ cast! {
/// This is some fake italic text.
/// ]
/// ```
#[elem(Show)]
#[elem]
pub struct SkewElem {
/// The horizontal skewing angle.
///
@ -265,14 +235,6 @@ pub struct SkewElem {
pub body: Content,
}
impl Show for Packed<SkewElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew)
.pack()
.spanned(self.span()))
}
}
/// A scale-skew-translate transformation.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Transform {

View File

@ -36,6 +36,7 @@ use typst_utils::{LazyHash, SmallBitSet};
use crate::diag::FileResult;
use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles};
use crate::layout::{Alignment, Dir};
use crate::routines::Routines;
use crate::text::{Font, FontBook};
use crate::visualize::Color;
@ -139,6 +140,11 @@ impl<T: World + ?Sized> WorldExt for T {
}
/// Definition of Typst's standard library.
///
/// To create and configure the standard library, use the `LibraryExt` trait
/// and call
/// - `Library::default()` for a standard configuration
/// - `Library::builder().build()` if you want to customize the library
#[derive(Debug, Clone, Hash)]
pub struct Library {
/// The module that contains the definitions that are available everywhere.
@ -154,30 +160,27 @@ pub struct Library {
pub features: Features,
}
impl Library {
/// Create a new builder for a library.
pub fn builder() -> LibraryBuilder {
LibraryBuilder::default()
}
}
impl Default for Library {
/// Constructs the standard library with the default configuration.
fn default() -> Self {
Self::builder().build()
}
}
/// Configurable builder for the standard library.
///
/// This struct is created by [`Library::builder`].
#[derive(Debug, Clone, Default)]
/// Constructed via the `LibraryExt` trait.
#[derive(Debug, Clone)]
pub struct LibraryBuilder {
routines: &'static Routines,
inputs: Option<Dict>,
features: Features,
}
impl LibraryBuilder {
/// Creates a new builder.
#[doc(hidden)]
pub fn from_routines(routines: &'static Routines) -> Self {
Self {
routines,
inputs: None,
features: Features::default(),
}
}
/// Configure the inputs visible through `sys.inputs`.
pub fn with_inputs(mut self, inputs: Dict) -> Self {
self.inputs = Some(inputs);
@ -196,7 +199,7 @@ impl LibraryBuilder {
pub fn build(self) -> Library {
let math = math::module();
let inputs = self.inputs.unwrap_or_default();
let global = global(math.clone(), inputs, &self.features);
let global = global(self.routines, math.clone(), inputs, &self.features);
Library {
global: global.clone(),
math,
@ -278,7 +281,12 @@ impl Category {
}
/// Construct the module with global definitions.
fn global(math: Module, inputs: Dict, features: &Features) -> Module {
fn global(
routines: &Routines,
math: Module,
inputs: Dict,
features: &Features,
) -> Module {
let mut global = Scope::deduplicating();
self::foundations::define(&mut global, inputs, features);
@ -293,7 +301,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module {
global.define("math", math);
global.define("pdf", self::pdf::module());
if features.is_enabled(Feature::Html) {
global.define("html", self::html::module());
global.define("html", (routines.html_module)());
}
prelude(&mut global);

View File

@ -62,7 +62,6 @@ pub struct AccentElem {
/// ```example
/// $dash(A, size: #150%)$
/// ```
#[resolve]
#[default(Rel::one())]
pub size: Rel<Length>,

View File

@ -59,9 +59,9 @@ impl Packed<AttachElem> {
macro_rules! merge {
($content:ident) => {
if base.$content.is_none() && elem.$content.is_some() {
if !base.$content.is_set() && elem.$content.is_set() {
base.$content = elem.$content.clone();
elem.$content = None;
elem.$content.unset();
}
};
}
@ -152,7 +152,6 @@ pub struct StretchElem {
/// The size to stretch to, relative to the maximum size of the glyph and
/// its attachments.
#[resolve]
#[default(Rel::one())]
pub size: Rel<Length>,
}

View File

@ -29,7 +29,6 @@ pub struct CancelElem {
/// $ a + cancel(x, length: #200%)
/// - cancel(x, length: #200%) $
/// ```
#[resolve]
#[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))]
pub length: Rel<Length>,
@ -89,7 +88,6 @@ pub struct CancelElem {
/// ),
/// ) $
/// ```
#[resolve]
#[fold]
#[default(Stroke {
// Default stroke has 0.5pt for better visuals.

View File

@ -6,13 +6,11 @@ use unicode_math_class::MathClass;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
Synthesize,
elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize,
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{
AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment,
VAlignment,
AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment,
};
use crate::math::{MathSize, MathVariant};
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
@ -46,7 +44,7 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// least one space lifts it into a separate block that is centered
/// horizontally. For more details about math syntax, see the
/// [main math page]($category/math).
#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)]
#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)]
pub struct EquationElem {
/// Whether the equation is displayed as a separate block.
#[default(false)]
@ -63,7 +61,6 @@ pub struct EquationElem {
/// With @ratio, we get:
/// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
/// ```
#[borrowed]
pub numbering: Option<Numbering>,
/// The alignment of the equation numbering.
@ -152,7 +149,7 @@ impl Synthesize for Packed<EquationElem> {
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let supplement = match self.as_ref().supplement(styles) {
let supplement = match self.as_ref().supplement.get_ref(styles) {
Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(supplement)) => {
@ -160,50 +157,36 @@ impl Synthesize for Packed<EquationElem> {
}
};
self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
self.supplement
.set(Smart::Custom(Some(Supplement::Content(supplement))));
Ok(())
}
}
impl Show for Packed<EquationElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if self.block(styles) {
Ok(BlockElem::multi_layouter(
self.clone(),
engine.routines.layout_equation_block,
)
.pack()
.spanned(self.span()))
} else {
Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline)
.pack()
.spanned(self.span()))
}
}
}
impl ShowSet for Packed<EquationElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
if self.block(styles) {
out.set(AlignElem::set_alignment(Alignment::CENTER));
out.set(BlockElem::set_breakable(false));
out.set(ParLine::set_numbering(None));
out.set(EquationElem::set_size(MathSize::Display));
if self.block.get(styles) {
out.set(AlignElem::alignment, Alignment::CENTER);
out.set(AlignElem::alignment, Alignment::CENTER);
out.set(BlockElem::breakable, false);
out.set(ParLine::numbering, None);
out.set(EquationElem::size, MathSize::Display);
} else {
out.set(EquationElem::set_size(MathSize::Text));
out.set(EquationElem::size, MathSize::Text);
}
out.set(TextElem::set_weight(FontWeight::from_number(450)));
out.set(TextElem::set_font(FontList(vec![FontFamily::new(
"New Computer Modern Math",
)])));
out.set(TextElem::weight, FontWeight::from_number(450));
out.set(
TextElem::font,
FontList(vec![FontFamily::new("New Computer Modern Math")]),
);
out
}
}
impl Count for Packed<EquationElem> {
fn update(&self) -> Option<CounterUpdate> {
(self.block(StyleChain::default()) && self.numbering().is_some())
(self.block.get(StyleChain::default()) && self.numbering().is_some())
.then(|| CounterUpdate::Step(NonZeroUsize::ONE))
}
}
@ -215,24 +198,24 @@ impl LocalName for Packed<EquationElem> {
impl Refable for Packed<EquationElem> {
fn supplement(&self) -> Content {
// After synthesis, this should always be custom content.
match (**self).supplement(StyleChain::default()) {
match self.supplement.get_cloned(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
Counter::of(EquationElem::elem())
Counter::of(EquationElem::ELEM)
}
fn numbering(&self) -> Option<&Numbering> {
(**self).numbering(StyleChain::default()).as_ref()
self.numbering.get_ref(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<EquationElem> {
fn outlined(&self) -> bool {
self.block(StyleChain::default()) && self.numbering().is_some()
self.block.get(StyleChain::default()) && self.numbering().is_some()
}
fn prefix(&self, numbers: Content) -> Content {

View File

@ -9,7 +9,6 @@ use crate::math::Mathy;
#[elem(title = "Left/Right", Mathy)]
pub struct LrElem {
/// The size of the brackets, relative to the height of the wrapped content.
#[resolve]
#[default(Rel::one())]
pub size: Rel<Length>,
@ -130,7 +129,7 @@ fn delimited(
]));
// Push size only if size is provided
if let Some(size) = size {
elem.push_size(size);
elem.size.set(size);
}
elem.pack().spanned(span)
}

View File

@ -46,7 +46,6 @@ pub struct VecElem {
/// #set math.vec(align: right)
/// $ vec(-1, 1, -1) $
/// ```
#[resolve]
#[default(HAlignment::Center)]
pub align: HAlignment,
@ -56,7 +55,6 @@ pub struct VecElem {
/// #set math.vec(gap: 1em)
/// $ vec(1, 2) $
/// ```
#[resolve]
#[default(DEFAULT_ROW_GAP.into())]
pub gap: Rel<Length>,
@ -107,7 +105,6 @@ pub struct MatElem {
/// #set math.mat(align: right)
/// $ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $
/// ```
#[resolve]
#[default(HAlignment::Center)]
pub align: HAlignment,
@ -141,7 +138,6 @@ pub struct MatElem {
/// ```example
/// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $
/// ```
#[resolve]
#[fold]
pub augment: Option<Augment>,
@ -162,7 +158,6 @@ pub struct MatElem {
/// #set math.mat(row-gap: 1em)
/// $ mat(1, 2; 3, 4) $
/// ```
#[resolve]
#[parse(
let gap = args.named("gap")?;
args.named("row-gap")?.or(gap)
@ -176,7 +171,6 @@ pub struct MatElem {
/// #set math.mat(column-gap: 1em)
/// $ mat(1, 2; 3, 4) $
/// ```
#[resolve]
#[parse(args.named("column-gap")?.or(gap))]
#[default(DEFAULT_COL_GAP.into())]
pub column_gap: Rel<Length>,
@ -259,7 +253,6 @@ pub struct CasesElem {
/// #set math.cases(gap: 1em)
/// $ x = cases(1, 2) $
/// ```
#[resolve]
#[default(DEFAULT_ROW_GAP.into())]
pub gap: Rel<Length>,

View File

@ -11,7 +11,7 @@ pub fn bold(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_bold(true))
body.set(EquationElem::bold, true)
}
/// Upright (non-italic) font style in math.
@ -24,7 +24,7 @@ pub fn upright(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_italic(Smart::Custom(false)))
body.set(EquationElem::italic, Smart::Custom(false))
}
/// Italic font style in math.
@ -35,7 +35,7 @@ pub fn italic(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_italic(Smart::Custom(true)))
body.set(EquationElem::italic, Smart::Custom(true))
}
/// Serif (roman) font style in math.
@ -46,7 +46,7 @@ pub fn serif(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Serif))
body.set(EquationElem::variant, MathVariant::Serif)
}
/// Sans-serif font style in math.
@ -59,7 +59,7 @@ pub fn sans(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Sans))
body.set(EquationElem::variant, MathVariant::Sans)
}
/// Calligraphic font style in math.
@ -93,7 +93,7 @@ pub fn cal(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Cal))
body.set(EquationElem::variant, MathVariant::Cal)
}
/// Fraktur font style in math.
@ -106,7 +106,7 @@ pub fn frak(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Frak))
body.set(EquationElem::variant, MathVariant::Frak)
}
/// Monospace font style in math.
@ -119,7 +119,7 @@ pub fn mono(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Mono))
body.set(EquationElem::variant, MathVariant::Mono)
}
/// Blackboard bold (double-struck) font style in math.
@ -137,7 +137,7 @@ pub fn bb(
/// The content to style.
body: Content,
) -> Content {
body.styled(EquationElem::set_variant(MathVariant::Bb))
body.set(EquationElem::variant, MathVariant::Bb)
}
/// Forced display style in math.
@ -157,8 +157,8 @@ pub fn display(
#[default(false)]
cramped: bool,
) -> Content {
body.styled(EquationElem::set_size(MathSize::Display))
.styled(EquationElem::set_cramped(cramped))
body.set(EquationElem::size, MathSize::Display)
.set(EquationElem::cramped, cramped)
}
/// Forced inline (text) style in math.
@ -179,8 +179,8 @@ pub fn inline(
#[default(false)]
cramped: bool,
) -> Content {
body.styled(EquationElem::set_size(MathSize::Text))
.styled(EquationElem::set_cramped(cramped))
body.set(EquationElem::size, MathSize::Text)
.set(EquationElem::cramped, cramped)
}
/// Forced script style in math.
@ -200,8 +200,8 @@ pub fn script(
#[default(true)]
cramped: bool,
) -> Content {
body.styled(EquationElem::set_size(MathSize::Script))
.styled(EquationElem::set_cramped(cramped))
body.set(EquationElem::size, MathSize::Script)
.set(EquationElem::cramped, cramped)
}
/// Forced second script style in math.
@ -222,8 +222,8 @@ pub fn sscript(
#[default(true)]
cramped: bool,
) -> Content {
body.styled(EquationElem::set_size(MathSize::ScriptScript))
.styled(EquationElem::set_cramped(cramped))
body.set(EquationElem::size, MathSize::ScriptScript)
.set(EquationElem::cramped, cramped)
}
/// The size of elements in an equation.

View File

@ -2,7 +2,6 @@ use std::any::TypeId;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::{Arc, LazyLock};
@ -17,7 +16,7 @@ use hayagriva::{
use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec};
use typst_syntax::{Span, Spanned, SyntaxMode};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use typst_utils::{ManuallyHash, PicoStr};
use crate::diag::{
bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos,
@ -26,18 +25,17 @@ use crate::diag::{
use crate::engine::{Engine, Sink};
use crate::foundations::{
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles,
Synthesize, Value,
};
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sides, Sizing, TrackSizings,
Sizing, TrackSizings,
};
use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded};
use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
Url,
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, Url,
};
use crate::routines::Routines;
use crate::text::{
@ -88,7 +86,7 @@ use crate::World;
///
/// #bibliography("works.bib")
/// ```
#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
#[elem(Locatable, Synthesize, ShowSet, LocalName)]
pub struct BibliographyElem {
/// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or
/// BibLaTeX `.bib` files.
@ -158,7 +156,7 @@ pub struct BibliographyElem {
impl BibliographyElem {
/// Find the document's bibliography.
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Packed<Self>> {
let query = introspector.query(&Self::elem().select());
let query = introspector.query(&Self::ELEM.select());
let mut iter = query.iter();
let Some(elem) = iter.next() else {
bail!("the document does not contain a bibliography");
@ -175,7 +173,7 @@ impl BibliographyElem {
pub fn has(engine: &Engine, key: Label) -> bool {
engine
.introspector
.query(&Self::elem().select())
.query(&Self::ELEM.select())
.iter()
.any(|elem| elem.to_packed::<Self>().unwrap().sources.derived.has(key))
}
@ -183,7 +181,7 @@ impl BibliographyElem {
/// Find all bibliography keys.
pub fn keys(introspector: Tracked<Introspector>) -> Vec<(Label, Option<EcoString>)> {
let mut vec = vec![];
for elem in introspector.query(&Self::elem().select()).iter() {
for elem in introspector.query(&Self::ELEM.select()).iter() {
let this = elem.to_packed::<Self>().unwrap();
for (key, entry) in this.sources.derived.iter() {
let detail = entry.title().map(|title| title.value.to_str().into());
@ -197,96 +195,18 @@ impl BibliographyElem {
impl Synthesize for Packed<BibliographyElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.push_lang(TextElem::lang_in(styles));
elem.push_region(TextElem::region_in(styles));
elem.lang = Some(styles.get(TextElem::lang));
elem.region = Some(styles.get(TextElem::region));
Ok(())
}
}
impl Show for Packed<BibliographyElem> {
#[typst_macros::time(name = "bibliography", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
const COLUMN_GUTTER: Em = Em::new(0.65);
const INDENT: Em = Em::new(1.5);
let span = self.span();
let mut seq = vec![];
if let Some(title) = self.title(styles).unwrap_or_else(|| {
Some(TextElem::packed(Self::local_name_in(styles)).spanned(span))
}) {
seq.push(
HeadingElem::new(title)
.with_depth(NonZeroUsize::ONE)
.pack()
.spanned(span),
);
}
let works = Works::generate(engine).at(span)?;
let references = works
.references
.as_ref()
.ok_or_else(|| match self.style(styles).source {
CslSource::Named(style) => eco_format!(
"CSL style \"{}\" is not suitable for bibliographies",
style.display_name()
),
CslSource::Normal(..) => {
"CSL style is not suitable for bibliographies".into()
}
})
.at(span)?;
if references.iter().any(|(prefix, _)| prefix.is_some()) {
let row_gutter = ParElem::spacing_in(styles);
let mut cells = vec![];
for (prefix, reference) in references {
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(prefix.clone().unwrap_or_default()))
.spanned(span),
)));
cells.push(GridChild::Item(GridItem::Cell(
Packed::new(GridCell::new(reference.clone())).spanned(span),
)));
}
seq.push(
GridElem::new(cells)
.with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
.with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
.with_row_gutter(TrackSizings(smallvec![row_gutter.into()]))
.pack()
.spanned(span),
);
} else {
for (_, reference) in references {
let realized = reference.clone();
let block = if works.hanging_indent {
let body = HElem::new((-INDENT).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(INDENT.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
seq.push(block.pack().spanned(span));
}
}
Ok(Content::sequence(seq))
}
}
impl ShowSet for Packed<BibliographyElem> {
fn show_set(&self, _: StyleChain) -> Styles {
const INDENT: Em = Em::new(1.0);
let mut out = Styles::new();
out.set(HeadingElem::set_numbering(None));
out.set(PadElem::set_left(INDENT.into()));
out.set(HeadingElem::numbering, None);
out.set(PadElem::left, INDENT.into());
out
}
}
@ -564,7 +484,7 @@ impl IntoValue for CslSource {
/// memoization) for the whole document. This setup is necessary because
/// citation formatting is inherently stateful and we need access to all
/// citations to do it.
pub(super) struct Works {
pub struct Works {
/// Maps from the location of a citation group to its rendered content.
pub citations: HashMap<Location, SourceResult<Content>>,
/// Lists all references in the bibliography, with optional prefix, or
@ -643,7 +563,7 @@ impl<'a> Generator<'a> {
introspector: Tracked<Introspector>,
) -> StrResult<Self> {
let bibliography = BibliographyElem::find(introspector)?;
let groups = introspector.query(&CiteGroup::elem().select());
let groups = introspector.query(&CiteGroup::ELEM.select());
let infos = Vec::with_capacity(groups.len());
Ok(Self {
routines,
@ -661,7 +581,8 @@ impl<'a> Generator<'a> {
LazyLock::new(hayagriva::archive::locales);
let database = &self.bibliography.sources.derived;
let bibliography_style = &self.bibliography.style(StyleChain::default()).derived;
let bibliography_style =
&self.bibliography.style.get_ref(StyleChain::default()).derived;
// Process all citation groups.
let mut driver = BibliographyDriver::new();
@ -689,7 +610,7 @@ impl<'a> Generator<'a> {
continue;
};
let supplement = child.supplement(StyleChain::default());
let supplement = child.supplement.get_cloned(StyleChain::default());
let locator = supplement.as_ref().map(|_| {
SpecificLocator(
citationberg::taxonomy::Locator::Custom,
@ -698,7 +619,7 @@ impl<'a> Generator<'a> {
});
let mut hidden = false;
let special_form = match child.form(StyleChain::default()) {
let special_form = match child.form.get(StyleChain::default()) {
None => {
hidden = true;
None
@ -720,7 +641,7 @@ impl<'a> Generator<'a> {
continue;
}
let style = match first.style(StyleChain::default()) {
let style = match first.style.get_ref(StyleChain::default()) {
Smart::Auto => bibliography_style.get(),
Smart::Custom(style) => style.derived.get(),
};
@ -736,23 +657,20 @@ impl<'a> Generator<'a> {
driver.citation(CitationRequest::new(
items,
style,
Some(locale(
first.lang().copied().unwrap_or(Lang::ENGLISH),
first.region().copied().flatten(),
)),
Some(locale(first.lang.unwrap_or(Lang::ENGLISH), first.region.flatten())),
&LOCALES,
None,
));
}
let locale = locale(
self.bibliography.lang().copied().unwrap_or(Lang::ENGLISH),
self.bibliography.region().copied().flatten(),
self.bibliography.lang.unwrap_or(Lang::ENGLISH),
self.bibliography.region.flatten(),
);
// Add hidden items for everything if we should print the whole
// bibliography.
if self.bibliography.full(StyleChain::default()) {
if self.bibliography.full.get(StyleChain::default()) {
for (_, entry) in database.iter() {
driver.citation(CitationRequest::new(
vec![CitationItem::new(entry, None, None, true, None)],
@ -1071,25 +989,24 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con
match format.font_style {
citationberg::FontStyle::Normal => {}
citationberg::FontStyle::Italic => {
content = content.styled(TextElem::set_style(FontStyle::Italic));
content = content.set(TextElem::style, FontStyle::Italic);
}
}
match format.font_variant {
citationberg::FontVariant::Normal => {}
citationberg::FontVariant::SmallCaps => {
content =
content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules)));
content = content.set(TextElem::smallcaps, Some(Smallcaps::Minuscules));
}
}
match format.font_weight {
citationberg::FontWeight::Normal => {}
citationberg::FontWeight::Bold => {
content = content.styled(TextElem::set_delta(WeightDelta(300)));
content = content.set(TextElem::delta, WeightDelta(300));
}
citationberg::FontWeight::Light => {
content = content.styled(TextElem::set_delta(WeightDelta(-100)));
content = content.set(TextElem::delta, WeightDelta(-100));
}
}

View File

@ -3,8 +3,7 @@ use typst_syntax::Spanned;
use crate::diag::{error, At, HintedString, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain,
Synthesize,
cast, elem, Cast, Content, Derived, Label, Packed, Smart, StyleChain, Synthesize,
};
use crate::introspection::Locatable;
use crate::model::bibliography::Works;
@ -106,7 +105,6 @@ pub struct CiteElem {
Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto),
None => None,
})]
#[borrowed]
pub style: Smart<Derived<CslSource, CslStyle>>,
/// The text language setting where the citation is.
@ -123,8 +121,8 @@ pub struct CiteElem {
impl Synthesize for Packed<CiteElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.push_lang(TextElem::lang_in(styles));
elem.push_region(TextElem::region_in(styles));
elem.lang = Some(styles.get(TextElem::lang));
elem.region = Some(styles.get(TextElem::region));
Ok(())
}
}
@ -154,16 +152,15 @@ pub enum CitationForm {
///
/// This is automatically created from adjacent citations during show rule
/// application.
#[elem(Locatable, Show)]
#[elem(Locatable)]
pub struct CiteGroup {
/// The citations.
#[required]
pub children: Vec<Packed<CiteElem>>,
}
impl Show for Packed<CiteGroup> {
#[typst_macros::time(name = "cite", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
impl Packed<CiteGroup> {
pub fn realize(&self, engine: &mut Engine) -> SourceResult<Content> {
let location = self.location().unwrap();
let span = self.span();
Works::generate(engine)

View File

@ -3,7 +3,7 @@ use ecow::EcoString;
use crate::diag::{bail, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart,
cast, elem, Args, Array, Construct, Content, Datetime, OneOrMultiple, Smart,
StyleChain, Styles, Value,
};
@ -109,23 +109,26 @@ impl DocumentInfo {
/// Document set rules are a bit special, so we need to do this manually.
pub fn populate(&mut self, styles: &Styles) {
let chain = StyleChain::new(styles);
let has = |field| styles.has::<DocumentElem>(field as _);
if has(<DocumentElem as Fields>::Enum::Title) {
self.title =
DocumentElem::title_in(chain).map(|content| content.plain_text());
if styles.has(DocumentElem::title) {
self.title = chain
.get_ref(DocumentElem::title)
.as_ref()
.map(|content| content.plain_text());
}
if has(<DocumentElem as Fields>::Enum::Author) {
self.author = DocumentElem::author_in(chain).0;
if styles.has(DocumentElem::author) {
self.author = chain.get_cloned(DocumentElem::author).0;
}
if has(<DocumentElem as Fields>::Enum::Description) {
self.description =
DocumentElem::description_in(chain).map(|content| content.plain_text());
if styles.has(DocumentElem::description) {
self.description = chain
.get_ref(DocumentElem::description)
.as_ref()
.map(|content| content.plain_text());
}
if has(<DocumentElem as Fields>::Enum::Keywords) {
self.keywords = DocumentElem::keywords_in(chain).0;
if styles.has(DocumentElem::keywords) {
self.keywords = chain.get_cloned(DocumentElem::keywords).0;
}
if has(<DocumentElem as Fields>::Enum::Date) {
self.date = DocumentElem::date_in(chain);
if styles.has(DocumentElem::date) {
self.date = chain.get(DocumentElem::date);
}
}
}

View File

@ -1,10 +1,4 @@
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::text::{ItalicToggle, TextElem};
use crate::foundations::{elem, Content};
/// Emphasizes content by toggling italics.
///
@ -29,24 +23,9 @@ use crate::text::{ItalicToggle, TextElem};
/// This function also has dedicated syntax: To emphasize content, simply
/// enclose it in underscores (`_`). Note that this only works at word
/// boundaries. To emphasize part of a word, you have to use the function.
#[elem(title = "Emphasis", keywords = ["italic"], Show)]
#[elem(title = "Emphasis", keywords = ["italic"])]
pub struct EmphElem {
/// The content to emphasize.
#[required]
pub body: Content,
}
impl Show for Packed<EmphElem> {
#[typst_macros::time(name = "emph", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
Ok(if TargetElem::target_in(styles).is_html() {
HtmlElem::new(tag::em)
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
body.styled(TextElem::set_emph(ItalicToggle(true)))
})
}
}

View File

@ -1,19 +1,11 @@
use std::str::FromStr;
use ecow::eco_format;
use smallvec::SmallVec;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
};
use crate::diag::bail;
use crate::foundations::{cast, elem, scope, Array, Content, Packed, Smart, Styles};
use crate::layout::{Alignment, Em, HAlignment, Length, VAlignment};
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern};
/// A numbered list.
///
@ -71,7 +63,7 @@ use crate::model::{
/// Enumeration items can contain multiple paragraphs and other block-level
/// content. All content that is indented more than an item's marker becomes
/// part of that item.
#[elem(scope, title = "Numbered List", Show)]
#[elem(scope, title = "Numbered List")]
pub struct EnumElem {
/// Defines the default [spacing]($enum.spacing) of the enumeration. If it
/// is `{false}`, the items are spaced apart with
@ -117,7 +109,6 @@ pub struct EnumElem {
/// + Numbering!
/// ```
#[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
#[borrowed]
pub numbering: Numbering,
/// Which number to start the enumeration with.
@ -157,11 +148,9 @@ pub struct EnumElem {
pub reversed: bool,
/// The indentation of each item.
#[resolve]
pub indent: Length,
/// The space between the numbering and the body of each item.
#[resolve]
#[default(Em::new(0.5).into())]
pub body_indent: Length,
@ -226,50 +215,6 @@ impl EnumElem {
type EnumItem;
}
impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
let mut elem = HtmlElem::new(tag::ol);
if self.reversed(styles) {
elem = elem.with_attr(attr::reversed, "reversed");
}
if let Some(n) = self.start(styles).custom() {
elem = elem.with_attr(attr::start, eco_format!("{n}"));
}
let body = Content::sequence(self.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
li.with_body(Some(body)).pack().spanned(item.span())
}));
return Ok(elem.with_body(Some(body)).pack().spanned(self.span()));
}
let mut realized =
BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum)
.pack()
.spanned(self.span());
if tight {
let spacing = self
.spacing(styles)
.unwrap_or_else(|| ParElem::leading_in(styles).into());
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
}
}
/// An enumeration item.
#[elem(name = "item", title = "Numbered List Item")]
pub struct EnumItem {

View File

@ -9,19 +9,16 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector,
Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
ShowSet, Smart, StyleChain, Styles, Synthesize,
};
use crate::html::{tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
};
use crate::layout::{
AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{
Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement,
AlignElem, Alignment, BlockElem, Em, Length, OuterVAlignment, PlacementScope,
VAlignment,
};
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem};
use crate::visualize::ImageElem;
@ -104,7 +101,7 @@ use crate::visualize::ImageElem;
/// caption: [I'm up here],
/// )
/// ```
#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)]
#[elem(scope, Locatable, Synthesize, Count, ShowSet, Refable, Outlinable)]
pub struct FigureElem {
/// The content of the figure. Often, an [image].
#[required]
@ -161,7 +158,6 @@ pub struct FigureElem {
pub scope: PlacementScope,
/// The figure's caption.
#[borrowed]
pub caption: Option<Packed<FigureCaption>>,
/// The kind of figure this is.
@ -214,13 +210,11 @@ pub struct FigureElem {
/// kind: "foo",
/// )
/// ```
#[borrowed]
pub supplement: Smart<Option<Supplement>>,
/// How to number the figure. Accepts a
/// [numbering pattern or function]($numbering).
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
#[borrowed]
pub numbering: Option<Numbering>,
/// The vertical gap between the body and caption.
@ -259,25 +253,25 @@ impl Synthesize for Packed<FigureElem> {
let span = self.span();
let location = self.location();
let elem = self.as_mut();
let numbering = elem.numbering(styles);
let numbering = elem.numbering.get_ref(styles);
// Determine the figure's kind.
let kind = elem.kind(styles).unwrap_or_else(|| {
let kind = elem.kind.get_cloned(styles).unwrap_or_else(|| {
elem.body
.query_first(&Selector::can::<dyn Figurable>())
.map(|elem| FigureKind::Elem(elem.func()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
.unwrap_or_else(|| FigureKind::Elem(ImageElem::ELEM))
});
// Resolve the supplement.
let supplement = match elem.supplement(styles).as_ref() {
let supplement = match elem.supplement.get_ref(styles).as_ref() {
Smart::Auto => {
// Default to the local name for the kind, if available.
let name = match &kind {
FigureKind::Elem(func) => func
.local_name(
TextElem::lang_in(styles),
TextElem::region_in(styles),
styles.get(TextElem::lang),
styles.get(TextElem::region),
)
.map(TextElem::packed),
FigureKind::Name(_) => None,
@ -307,95 +301,37 @@ impl Synthesize for Packed<FigureElem> {
// Construct the figure's counter.
let counter = Counter::new(CounterKey::Selector(
select_where!(FigureElem, Kind => kind.clone()),
select_where!(FigureElem, kind => kind.clone()),
));
// Fill the figure's caption.
let mut caption = elem.caption(styles).clone();
let mut caption = elem.caption.get_cloned(styles);
if let Some(caption) = &mut caption {
caption.synthesize(engine, styles)?;
caption.push_kind(kind.clone());
caption.push_supplement(supplement.clone());
caption.push_numbering(numbering.clone());
caption.push_counter(Some(counter.clone()));
caption.push_figure_location(location);
caption.kind = Some(kind.clone());
caption.supplement = Some(supplement.clone());
caption.numbering = Some(numbering.clone());
caption.counter = Some(Some(counter.clone()));
caption.figure_location = Some(location);
}
elem.push_kind(Smart::Custom(kind));
elem.push_supplement(Smart::Custom(supplement.map(Supplement::Content)));
elem.push_counter(Some(counter));
elem.push_caption(caption);
elem.kind.set(Smart::Custom(kind));
elem.supplement
.set(Smart::Custom(supplement.map(Supplement::Content)));
elem.counter = Some(Some(counter));
elem.caption.set(caption);
Ok(())
}
}
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let target = TargetElem::target_in(styles);
let mut realized = self.body.clone();
// Build the caption, if any.
if let Some(caption) = self.caption(styles).clone() {
let (first, second) = match caption.position(styles) {
OuterVAlignment::Top => (caption.pack(), realized),
OuterVAlignment::Bottom => (realized, caption.pack()),
};
let mut seq = Vec::with_capacity(3);
seq.push(first);
if !target.is_html() {
let v = VElem::new(self.gap(styles).into()).with_weak(true);
seq.push(v.pack().spanned(span))
}
seq.push(second);
realized = Content::sequence(seq)
}
// Ensure that the body is considered a paragraph.
realized += ParbreakElem::shared().clone().spanned(span);
if target.is_html() {
return Ok(HtmlElem::new(tag::figure)
.with_body(Some(realized))
.pack()
.spanned(span));
}
// Wrap the contents in a block.
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(span);
// Wrap in a float.
if let Some(align) = self.placement(styles) {
realized = PlaceElem::new(realized)
.with_alignment(align.map(|align| HAlignment::Center + align))
.with_scope(self.scope(styles))
.with_float(true)
.pack()
.spanned(span);
} else if self.scope(styles) == PlacementScope::Parent {
bail!(
span,
"parent-scoped placement is only available for floating figures";
hint: "you can enable floating placement with `figure(placement: auto, ..)`"
);
}
Ok(realized)
}
}
impl ShowSet for Packed<FigureElem> {
fn show_set(&self, _: StyleChain) -> Styles {
// Still allows breakable figures with
// `show figure: set block(breakable: true)`.
let mut map = Styles::new();
map.set(BlockElem::set_breakable(false));
map.set(AlignElem::set_alignment(Alignment::CENTER));
map.set(BlockElem::breakable, false);
map.set(AlignElem::alignment, Alignment::CENTER);
map
}
}
@ -413,29 +349,28 @@ impl Count for Packed<FigureElem> {
impl Refable for Packed<FigureElem> {
fn supplement(&self) -> Content {
// After synthesis, this should always be custom content.
match (**self).supplement(StyleChain::default()).as_ref() {
Smart::Custom(Some(Supplement::Content(content))) => content.clone(),
match self.supplement.get_cloned(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
(**self)
.counter()
.cloned()
self.counter
.clone()
.flatten()
.unwrap_or_else(|| Counter::of(FigureElem::elem()))
.unwrap_or_else(|| Counter::of(FigureElem::ELEM))
}
fn numbering(&self) -> Option<&Numbering> {
(**self).numbering(StyleChain::default()).as_ref()
self.numbering.get_ref(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<FigureElem> {
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
&& (self.caption(StyleChain::default()).is_some()
self.outlined.get(StyleChain::default())
&& (self.caption.get_ref(StyleChain::default()).is_some()
|| self.numbering().is_some())
}
@ -449,7 +384,8 @@ impl Outlinable for Packed<FigureElem> {
}
fn body(&self) -> Content {
self.caption(StyleChain::default())
self.caption
.get_ref(StyleChain::default())
.as_ref()
.map(|caption| caption.body.clone())
.unwrap_or_default()
@ -473,7 +409,7 @@ impl Outlinable for Packed<FigureElem> {
/// caption: [A rectangle],
/// )
/// ```
#[elem(name = "caption", Synthesize, Show)]
#[elem(name = "caption", Synthesize)]
pub struct FigureCaption {
/// The caption's position in the figure. Either `{top}` or `{bottom}`.
///
@ -561,6 +497,35 @@ pub struct FigureCaption {
}
impl FigureCaption {
/// Realizes the textual caption content.
pub fn realize(
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content> {
let mut realized = self.body.clone();
if let (
Some(Some(mut supplement)),
Some(Some(numbering)),
Some(Some(counter)),
Some(Some(location)),
) = (
self.supplement.clone(),
&self.numbering,
&self.counter,
&self.figure_location,
) {
let numbers = counter.display_at_loc(engine, *location, styles, numbering)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
realized = supplement + numbers + self.get_separator(styles) + realized;
}
Ok(realized)
}
/// Gets the default separator in the given language and (optionally)
/// region.
fn local_separator(lang: Lang, _: Option<Region>) -> &'static str {
@ -573,10 +538,10 @@ impl FigureCaption {
}
fn get_separator(&self, styles: StyleChain) -> Content {
self.separator(styles).unwrap_or_else(|| {
self.separator.get_cloned(styles).unwrap_or_else(|| {
TextElem::packed(Self::local_separator(
TextElem::lang_in(styles),
TextElem::region_in(styles),
styles.get(TextElem::lang),
styles.get(TextElem::region),
))
})
}
@ -585,48 +550,11 @@ impl FigureCaption {
impl Synthesize for Packed<FigureCaption> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let elem = self.as_mut();
elem.push_separator(Smart::Custom(elem.get_separator(styles)));
elem.separator.set(Smart::Custom(elem.get_separator(styles)));
Ok(())
}
}
impl Show for Packed<FigureCaption> {
#[typst_macros::time(name = "figure.caption", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body.clone();
if let (
Some(Some(mut supplement)),
Some(Some(numbering)),
Some(Some(counter)),
Some(Some(location)),
) = (
self.supplement().cloned(),
self.numbering(),
self.counter(),
self.figure_location(),
) {
let numbers = counter.display_at_loc(engine, *location, styles, numbering)?;
if !supplement.is_empty() {
supplement += TextElem::packed('\u{a0}');
}
realized = supplement + numbers + self.get_separator(styles) + realized;
}
Ok(if TargetElem::target_in(styles).is_html() {
HtmlElem::new(tag::figcaption)
.with_body(Some(realized))
.pack()
.spanned(self.span())
} else {
BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span())
})
}
}
cast! {
FigureCaption,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),

View File

@ -3,16 +3,16 @@ use std::str::FromStr;
use typst_utils::NonZeroExt;
use crate::diag::{bail, At, SourceResult, StrResult};
use crate::diag::{bail, StrResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
cast, elem, scope, Content, Label, NativeElement, Packed, ShowSet, Smart, StyleChain,
Styles,
};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location};
use crate::layout::{Abs, Em, HElem, Length, Ratio};
use crate::model::{Destination, Numbering, NumberingPattern, ParElem};
use crate::text::{SuperElem, TextElem, TextSize};
use crate::introspection::{Count, CounterUpdate, Locatable, Location};
use crate::layout::{Abs, Em, Length, Ratio};
use crate::model::{Numbering, NumberingPattern, ParElem};
use crate::text::{TextElem, TextSize};
use crate::visualize::{LineElem, Stroke};
/// A footnote.
@ -51,7 +51,7 @@ use crate::visualize::{LineElem, Stroke};
/// apply to the footnote's content. See [here][issue] for more information.
///
/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
#[elem(scope, Locatable, Show, Count)]
#[elem(scope, Locatable, Count)]
pub struct FootnoteElem {
/// How to number footnotes.
///
@ -67,7 +67,6 @@ pub struct FootnoteElem {
/// #footnote[Star],
/// #footnote[Dagger]
/// ```
#[borrowed]
#[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
pub numbering: Numbering,
@ -136,21 +135,6 @@ impl Packed<FootnoteElem> {
}
}
impl Show for Packed<FootnoteElem> {
#[typst_macros::time(name = "footnote", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let loc = self.declaration_location(engine).at(span)?;
let numbering = self.numbering(styles);
let counter = Counter::of(FootnoteElem::elem());
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1);
// Add zero-width weak spacing to make the footnote "sticky".
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
}
}
impl Count for Packed<FootnoteElem> {
fn update(&self) -> Option<CounterUpdate> {
(!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
@ -192,7 +176,7 @@ cast! {
/// page run is a sequence of pages without an explicit pagebreak in between).
/// For this reason, set and show rules for footnote entries should be defined
/// before any page content, typically at the very start of the document.
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
#[elem(name = "entry", title = "Footnote Entry", ShowSet)]
pub struct FootnoteEntry {
/// The footnote for this entry. Its location can be used to determine
/// the footnote counter state.
@ -248,7 +232,6 @@ pub struct FootnoteEntry {
/// ]
/// ```
#[default(Em::new(1.0).into())]
#[resolve]
pub clearance: Length,
/// The gap between footnote entries.
@ -261,7 +244,6 @@ pub struct FootnoteEntry {
/// #footnote[Apart]
/// ```
#[default(Em::new(0.5).into())]
#[resolve]
pub gap: Length,
/// The indent of each footnote entry.
@ -277,42 +259,11 @@ pub struct FootnoteEntry {
pub indent: Length,
}
impl Show for Packed<FootnoteEntry> {
#[typst_macros::time(name = "footnote.entry", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let number_gap = Em::new(0.05);
let default = StyleChain::default();
let numbering = self.note.numbering(default);
let counter = Counter::of(FootnoteElem::elem());
let Some(loc) = self.note.location() else {
bail!(
span, "footnote entry must have a location";
hint: "try using a query or a show rule to customize the footnote instead"
);
};
let num = counter.display_at_loc(engine, loc, styles, numbering)?;
let sup = SuperElem::new(num)
.pack()
.spanned(span)
.linked(Destination::Location(loc))
.located(loc.variant(1));
Ok(Content::sequence([
HElem::new(self.indent(styles).into()).pack(),
sup,
HElem::new(number_gap.into()).with_weak(true).pack(),
self.note.body_content().unwrap().clone(),
]))
}
}
impl ShowSet for Packed<FootnoteEntry> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(ParElem::set_leading(Em::new(0.5).into()));
out.set(TextElem::set_size(TextSize(Em::new(0.85).into())));
out.set(ParElem::leading, Em::new(0.5).into());
out.set(TextElem::size, TextSize(Em::new(0.85).into()));
out
}
}

View File

@ -1,21 +1,16 @@
use std::num::NonZeroUsize;
use ecow::eco_format;
use typst_utils::{Get, NonZeroExt};
use typst_utils::NonZeroExt;
use crate::diag::{warning, SourceResult};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, TargetElem,
elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{BlockElem, Em, Length};
use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
use crate::text::{FontWeight, LocalName, TextElem, TextSize};
/// A section heading.
///
@ -49,7 +44,7 @@ use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
/// one or multiple equals signs, followed by a space. The number of equals
/// signs determines the heading's logical nesting depth. The `{offset}` field
/// can be set to configure the starting depth.
#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)]
#[elem(Locatable, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)]
pub struct HeadingElem {
/// The absolute nesting depth of the heading, starting from one. If set
/// to `{auto}`, it is computed from `{offset + depth}`.
@ -106,7 +101,6 @@ pub struct HeadingElem {
/// == A subsection
/// === A sub-subsection
/// ```
#[borrowed]
pub numbering: Option<Numbering>,
/// A supplement for the heading.
@ -187,8 +181,8 @@ pub struct HeadingElem {
impl HeadingElem {
pub fn resolve_level(&self, styles: StyleChain) -> NonZeroUsize {
self.level(styles).unwrap_or_else(|| {
NonZeroUsize::new(self.offset(styles) + self.depth(styles).get())
self.level.get(styles).unwrap_or_else(|| {
NonZeroUsize::new(self.offset.get(styles) + self.depth.get(styles).get())
.expect("overflow to 0 on NoneZeroUsize + usize")
})
}
@ -200,7 +194,7 @@ impl Synthesize for Packed<HeadingElem> {
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let supplement = match (**self).supplement(styles) {
let supplement = match self.supplement.get_ref(styles) {
Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
Smart::Custom(None) => Content::empty(),
Smart::Custom(Some(supplement)) => {
@ -209,105 +203,16 @@ impl Synthesize for Packed<HeadingElem> {
};
let elem = self.as_mut();
elem.push_level(Smart::Custom(elem.resolve_level(styles)));
elem.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
elem.level.set(Smart::Custom(elem.resolve_level(styles)));
elem.supplement
.set(Smart::Custom(Some(Supplement::Content(supplement))));
Ok(())
}
}
impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let html = TargetElem::target_in(styles).is_html();
const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = self.span();
let mut realized = self.body.clone();
let hanging_indent = self.hanging_indent(styles);
let mut indent = match hanging_indent {
Smart::Custom(length) => length.resolve(styles),
Smart::Auto => Abs::zero(),
};
if let Some(numbering) = (**self).numbering(styles).as_ref() {
let location = self.location().unwrap();
let numbering = Counter::of(HeadingElem::elem())
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
if hanging_indent.is_auto() && !html {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
// We don't have a locator for the numbering here, so we just
// use the measurement infrastructure for now.
let link = LocatorLink::measure(location);
let size = (engine.routines.layout_frame)(
engine,
&numbering,
Locator::link(&link),
styles,
pod,
)?
.size();
indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
}
let spacing = if html {
SpaceElem::shared().clone()
} else {
HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
};
realized = numbering + spacing + realized;
}
Ok(if html {
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
// reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`.
let level = self.resolve_level(styles).get();
if level >= 6 {
engine.sink.warn(warning!(span,
"heading of level {} was transformed to \
<div role=\"heading\" aria-level=\"{}\">, which is not \
supported by all assistive technology",
level, level + 1;
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
hint: "you may want to restructure your document so that \
it doesn't contain deep headings"));
HtmlElem::new(tag::div)
.with_body(Some(realized))
.with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack()
.spanned(span)
} else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(TextElem::dir_in(styles).start(), Some(indent.into()));
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.with_inset(inset)
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
block.pack().spanned(span)
})
}
}
impl ShowSet for Packed<HeadingElem> {
fn show_set(&self, styles: StyleChain) -> Styles {
let level = (**self).resolve_level(styles).get();
let level = self.resolve_level(styles).get();
let scale = match level {
1 => 1.4,
2 => 1.2,
@ -319,49 +224,49 @@ impl ShowSet for Packed<HeadingElem> {
let below = Em::new(0.75) / scale;
let mut out = Styles::new();
out.set(TextElem::set_size(TextSize(size.into())));
out.set(TextElem::set_weight(FontWeight::BOLD));
out.set(BlockElem::set_above(Smart::Custom(above.into())));
out.set(BlockElem::set_below(Smart::Custom(below.into())));
out.set(BlockElem::set_sticky(true));
out.set(TextElem::size, TextSize(size.into()));
out.set(TextElem::weight, FontWeight::BOLD);
out.set(BlockElem::above, Smart::Custom(above.into()));
out.set(BlockElem::below, Smart::Custom(below.into()));
out.set(BlockElem::sticky, true);
out
}
}
impl Count for Packed<HeadingElem> {
fn update(&self) -> Option<CounterUpdate> {
(**self)
.numbering(StyleChain::default())
self.numbering
.get_ref(StyleChain::default())
.is_some()
.then(|| CounterUpdate::Step((**self).resolve_level(StyleChain::default())))
.then(|| CounterUpdate::Step(self.resolve_level(StyleChain::default())))
}
}
impl Refable for Packed<HeadingElem> {
fn supplement(&self) -> Content {
// After synthesis, this should always be custom content.
match (**self).supplement(StyleChain::default()) {
match self.supplement.get_cloned(StyleChain::default()) {
Smart::Custom(Some(Supplement::Content(content))) => content,
_ => Content::empty(),
}
}
fn counter(&self) -> Counter {
Counter::of(HeadingElem::elem())
Counter::of(HeadingElem::ELEM)
}
fn numbering(&self) -> Option<&Numbering> {
(**self).numbering(StyleChain::default()).as_ref()
self.numbering.get_ref(StyleChain::default()).as_ref()
}
}
impl Outlinable for Packed<HeadingElem> {
fn outlined(&self) -> bool {
(**self).outlined(StyleChain::default())
self.outlined.get(StyleChain::default())
}
fn level(&self) -> NonZeroUsize {
(**self).resolve_level(StyleChain::default())
self.resolve_level(StyleChain::default())
}
fn prefix(&self, numbers: Content) -> Content {

View File

@ -2,13 +2,10 @@ use std::ops::Deref;
use ecow::{eco_format, EcoString};
use crate::diag::{bail, warning, At, SourceResult, StrResult};
use crate::engine::Engine;
use crate::diag::{bail, StrResult};
use crate::foundations::{
cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart,
StyleChain, Styles, TargetElem,
cast, elem, Content, Label, Packed, Repr, ShowSet, Smart, StyleChain, Styles,
};
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location;
use crate::layout::Position;
use crate::text::TextElem;
@ -38,7 +35,7 @@ use crate::text::TextElem;
/// # Syntax
/// This function also has dedicated syntax: Text that starts with `http://` or
/// `https://` is automatically turned into a link.
#[elem(Show)]
#[elem]
pub struct LinkElem {
/// The destination the link points to.
///
@ -103,42 +100,10 @@ impl LinkElem {
}
}
impl Show for Packed<LinkElem> {
#[typst_macros::time(name = "link", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
Ok(if TargetElem::target_in(styles).is_html() {
if let LinkTarget::Dest(Destination::Url(url)) = &self.dest {
HtmlElem::new(tag::a)
.with_attr(attr::href, url.clone().into_inner())
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
engine.sink.warn(warning!(
self.span(),
"non-URL links are not yet supported by HTML export"
));
body
}
} else {
match &self.dest {
LinkTarget::Dest(dest) => body.linked(dest.clone()),
LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(self.span())?;
let dest = Destination::Location(elem.location().unwrap());
body.clone().linked(dest)
}
}
})
}
}
impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out.set(TextElem::hyphenate, Smart::Custom(false));
out
}
}

View File

@ -3,12 +3,10 @@ use comemo::Track;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
Smart, StyleChain, Styles, TargetElem, Value,
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed,
Smart, StyleChain, Styles, Value,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::{ParElem, ParbreakElem};
use crate::layout::{Em, Length};
use crate::text::TextElem;
/// A bullet list.
@ -42,7 +40,7 @@ use crate::text::TextElem;
/// followed by a space to create a list item. A list item can contain multiple
/// paragraphs and other block-level content. All content that is indented
/// more than an item's marker becomes part of that item.
#[elem(scope, title = "Bullet List", Show)]
#[elem(scope, title = "Bullet List")]
pub struct ListElem {
/// Defines the default [spacing]($list.spacing) of the list. If it is
/// `{false}`, the items are spaced apart with
@ -86,7 +84,6 @@ pub struct ListElem {
/// - Items
/// - Items
/// ```
#[borrowed]
#[default(ListMarker::Content(vec![
// These are all available in the default font, vertically centered, and
// roughly of the same size (with the last one having slightly lower
@ -98,11 +95,9 @@ pub struct ListElem {
pub marker: ListMarker,
/// The indent of each item.
#[resolve]
pub indent: Length,
/// The spacing between the marker and the body of each item.
#[resolve]
#[default(Em::new(0.5).into())]
pub body_indent: Length,
@ -139,44 +134,6 @@ impl ListElem {
type ListItem;
}
impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight(styles);
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
.pack()
.spanned(self.span()));
}
let mut realized =
BlockElem::multi_layouter(self.clone(), engine.routines.layout_list)
.pack()
.spanned(self.span());
if tight {
let spacing = self
.spacing(styles)
.unwrap_or_else(|| ParElem::leading_in(styles).into());
let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
}
Ok(realized)
}
}
/// A bullet list item.
#[elem(name = "item", title = "Bullet List Item")]
pub struct ListItem {

Some files were not shown because too many files have changed in this diff Show More