mirror of
https://github.com/typst/typst
synced 2025-08-19 01:18:32 +08:00
Compare commits
13 Commits
524c1eccff
...
708eea9015
Author | SHA1 | Date | |
---|---|---|---|
|
708eea9015 | ||
|
eed3407051 | ||
|
1bbb58c43f | ||
|
1dc4c248d1 | ||
|
9e6adb6f45 | ||
|
4534167656 | ||
|
9ad1879e9d | ||
|
e5e813219e | ||
|
52a708b988 | ||
|
e71674f6b3 | ||
|
e5e1dcd9c0 | ||
|
0a3c6939dd | ||
|
ced5091edb |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -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
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -29,6 +29,7 @@ typst-svg = { workspace = true }
|
||||
typst-timing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
clap_complete = { workspace = true }
|
||||
codespan-reporting = { workspace = true }
|
||||
color-print = { workspace = true }
|
||||
comemo = { workspace = true }
|
||||
|
@ -7,6 +7,7 @@ use std::str::FromStr;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::builder::{TypedValueParser, ValueParser};
|
||||
use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint};
|
||||
use clap_complete::Shell;
|
||||
use semver::Version;
|
||||
|
||||
/// The character typically used to separate path components
|
||||
@ -81,6 +82,9 @@ pub enum Command {
|
||||
/// Self update the Typst CLI.
|
||||
#[cfg_attr(not(feature = "self-update"), clap(hide = true))]
|
||||
Update(UpdateCommand),
|
||||
|
||||
/// Generates shell completion scripts.
|
||||
Completions(CompletionsCommand),
|
||||
}
|
||||
|
||||
/// Compiles an input file into a supported output format.
|
||||
@ -198,6 +202,14 @@ pub struct UpdateCommand {
|
||||
pub backup_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Generates shell completion scripts.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct CompletionsCommand {
|
||||
/// The shell to generate completions for.
|
||||
#[arg(value_enum)]
|
||||
pub shell: Shell,
|
||||
}
|
||||
|
||||
/// Arguments for compilation and watching.
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CompileArgs {
|
||||
|
13
crates/typst-cli/src/completions.rs
Normal file
13
crates/typst-cli/src/completions.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use std::io::stdout;
|
||||
|
||||
use clap::CommandFactory;
|
||||
use clap_complete::generate;
|
||||
|
||||
use crate::args::{CliArguments, CompletionsCommand};
|
||||
|
||||
/// Execute the completions command.
|
||||
pub fn completions(command: &CompletionsCommand) {
|
||||
let mut cmd = CliArguments::command();
|
||||
let bin_name = cmd.get_name().to_string();
|
||||
generate(command.shell, &mut cmd, bin_name, &mut stdout());
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
mod args;
|
||||
mod compile;
|
||||
mod completions;
|
||||
mod download;
|
||||
mod fonts;
|
||||
mod greet;
|
||||
@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> {
|
||||
Command::Query(command) => crate::query::query(command)?,
|
||||
Command::Fonts(command) => crate::fonts::fonts(command),
|
||||
Command::Update(command) => crate::update::update(command)?,
|
||||
Command::Completions(command) => crate::completions::completions(command),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
135
crates/typst-html/src/css.rs
Normal file
135
crates/typst-html/src/css.rs
Normal 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
|
||||
}
|
@ -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!(
|
||||
|
411
crates/typst-html/src/rules.rs
Normal file
411
crates/typst-html/src/rules.rs
Normal 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());
|
@ -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::*;
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,18 +189,19 @@ 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(_)));
|
||||
|
@ -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,
|
||||
@ -631,7 +633,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| {
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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| {
|
||||
|
@ -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);
|
||||
|
@ -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()?;
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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() {
|
||||
|
@ -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.
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}),
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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![];
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
890
crates/typst-layout/src/rules.rs
Normal file
890
crates/typst-layout/src/rules.rs
Normal 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());
|
@ -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(),
|
||||
)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
564
crates/typst-library/src/foundations/content/field.rs
Normal file
564
crates/typst-library/src/foundations/content/field.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
147
crates/typst-library/src/foundations/content/packed.rs
Normal file
147
crates/typst-library/src/foundations/content/packed.rs
Normal 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);
|
||||
}
|
||||
}
|
426
crates/typst-library/src/foundations/content/raw.rs
Normal file
426
crates/typst-library/src/foundations/content/raw.rs
Normal 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));
|
||||
}
|
||||
}
|
383
crates/typst-library/src/foundations/content/vtable.rs
Normal file
383
crates/typst-library/src/foundations/content/vtable.rs
Normal 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) }
|
||||
}
|
||||
}
|
@ -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())
|
||||
};
|
||||
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
}};
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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>,
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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 {
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -62,7 +62,6 @@ pub struct AccentElem {
|
||||
/// ```example
|
||||
/// $dash(A, size: #150%)$
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[default(Rel::one())]
|
||||
pub size: Rel<Length>,
|
||||
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>,
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user