Compare commits

..

1 Commits

98 changed files with 2440 additions and 2198 deletions

9
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,13 @@
//! Typst's HTML exporter. //! Typst's HTML exporter.
mod css;
mod encode; mod encode;
mod rules;
mod typed;
pub use self::encode::html; pub use self::encode::html;
pub use self::rules::register;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, warning, At, SourceResult}; use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{ use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
Content, Module, Scope, StyleChain, Target, TargetElem,
};
use typst_library::html::{ use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
}; };
@ -24,19 +18,9 @@ use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Si
use typst_library::model::{DocumentInfo, ParElem}; use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::{Category, World}; use typst_library::World;
use typst_syntax::Span; 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. /// Produce an HTML document from content.
/// ///
/// This first performs root-level realization and then turns the resulting /// This first performs root-level realization and then turns the resulting

View File

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

View File

@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{singleton, LazyHash};
use typst::{Feature, Library, LibraryExt, World}; use typst::{Feature, Library, World};
use crate::IdeWorld; use crate::IdeWorld;

View File

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

View File

@ -10,11 +10,21 @@ mod modifiers;
mod pad; mod pad;
mod pages; mod pages;
mod repeat; mod repeat;
mod rules;
mod shapes; mod shapes;
mod stack; mod stack;
mod transforms; mod transforms;
pub use self::flow::{layout_fragment, layout_frame}; 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::pages::layout_document; pub use self::pages::layout_document;
pub use self::rules::register; 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};

View File

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

View File

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

View File

@ -246,6 +246,12 @@ pub trait Synthesize {
-> SourceResult<()>; -> 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. /// Defines built-in show set rules for an element.
/// ///
/// This is a bit more powerful than a user-defined show-set because it can /// This is a bit more powerful than a user-defined show-set because it can

View File

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

View File

@ -179,40 +179,24 @@ impl Str {
} }
/// Extracts the first grapheme cluster of the string. /// 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] #[func]
pub fn first( pub fn first(&self) -> StrResult<Str> {
&self,
/// A default value to return if the string is empty.
#[named]
default: Option<Str>,
) -> StrResult<Str> {
self.0 self.0
.graphemes(true) .graphemes(true)
.next() .next()
.map(Into::into) .map(Into::into)
.or(default)
.ok_or_else(string_is_empty) .ok_or_else(string_is_empty)
} }
/// Extracts the last grapheme cluster of the string. /// 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] #[func]
pub fn last( pub fn last(&self) -> StrResult<Str> {
&self,
/// A default value to return if the string is empty.
#[named]
default: Option<Str>,
) -> StrResult<Str> {
self.0 self.0
.graphemes(true) .graphemes(true)
.next_back() .next_back()
.map(Into::into) .map(Into::into)
.or(default)
.ok_or_else(string_is_empty) .ok_or_else(string_is_empty)
} }

View File

@ -1,5 +1,4 @@
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::{mem, ptr}; use std::{mem, ptr};
@ -14,7 +13,7 @@ use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple, cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple,
Packed, RefableProperty, Repr, Selector, SettableProperty, Target, RefableProperty, Repr, Selector, SettableProperty,
}; };
use crate::text::{FontFamily, FontList, TextElem}; use crate::text::{FontFamily, FontList, TextElem};
@ -939,129 +938,3 @@ fn block_wrong_type(func: Element, id: u8, value: &Block) -> ! {
value value
) )
} }
/// Holds native show rules.
pub struct NativeRuleMap {
rules: HashMap<(Element, Target), NativeShowRule>,
}
/// The signature of a native show rule.
pub type ShowFn<T> = fn(
elem: &Packed<T>,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content>;
impl NativeRuleMap {
/// Creates a new rule map.
///
/// Should be populated with rules for all target-element combinations that
/// are supported.
///
/// Contains built-in rules for a few special elements.
pub fn new() -> Self {
let mut rules = Self { rules: HashMap::new() };
// ContextElem is as special as SequenceElem and StyledElem and could,
// in theory, also be special cased in realization.
rules.register_builtin(crate::foundations::CONTEXT_RULE);
// CounterDisplayElem only exists because the compiler can't currently
// express the equivalent of `context counter(..).display(..)` in native
// code (no native closures).
rules.register_builtin(crate::introspection::COUNTER_DISPLAY_RULE);
// These are all only for introspection and empty on all targets.
rules.register_empty::<crate::introspection::CounterUpdateElem>();
rules.register_empty::<crate::introspection::StateUpdateElem>();
rules.register_empty::<crate::introspection::MetadataElem>();
rules.register_empty::<crate::model::PrefixInfo>();
rules
}
/// Registers a rule for all targets.
fn register_empty<T: NativeElement>(&mut self) {
self.register_builtin::<T>(|_, _, _| Ok(Content::empty()));
}
/// Registers a rule for all targets.
fn register_builtin<T: NativeElement>(&mut self, f: ShowFn<T>) {
self.register(Target::Paged, f);
self.register(Target::Html, f);
}
/// Registers a rule for a target.
///
/// Panics if a rule already exists for this target-element combination.
pub fn register<T: NativeElement>(&mut self, target: Target, f: ShowFn<T>) {
let res = self.rules.insert((T::ELEM, target), NativeShowRule::new(f));
if res.is_some() {
panic!(
"duplicate native show rule for `{}` on {target:?} target",
T::ELEM.name()
)
}
}
/// Retrieves the rule that applies to the `content` on the current
/// `target`.
pub fn get(&self, target: Target, content: &Content) -> Option<NativeShowRule> {
self.rules.get(&(content.func(), target)).copied()
}
}
impl Default for NativeRuleMap {
fn default() -> Self {
Self::new()
}
}
pub use rule::NativeShowRule;
mod rule {
use super::*;
/// The show rule for a native element.
#[derive(Copy, Clone)]
pub struct NativeShowRule {
/// The element to which this rule applies.
elem: Element,
/// Must only be called with content of the appropriate type.
f: unsafe fn(
elem: &Content,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content>,
}
impl NativeShowRule {
/// Create a new type-erased show rule.
pub fn new<T: NativeElement>(f: ShowFn<T>) -> Self {
Self {
elem: T::ELEM,
// Safety: The two function pointer types only differ in the
// first argument, which changes from `&Packed<T>` to
// `&Content`. `Packed<T>` is a transparent wrapper around
// `Content`. The resulting function is unsafe to call because
// content of the correct type must be passed to it.
#[allow(clippy::missing_transmute_annotations)]
f: unsafe { std::mem::transmute(f) },
}
}
/// Applies the rule to content. Panics if the content is of the wrong
/// type.
pub fn apply(
&self,
content: &Content,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content> {
assert_eq!(content.elem(), self.elem);
// Safety: We just checked that the element is of the correct type.
unsafe { (self.f)(content, engine, styles) }
}
}
}

View File

@ -4,7 +4,7 @@ use crate::diag::HintedStrResult;
use crate::foundations::{elem, func, Cast, Context}; use crate::foundations::{elem, func, Cast, Context};
/// The export target. /// The export target.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)]
pub enum Target { pub enum Target {
/// The target that is used for paged, fully laid-out content. /// The target that is used for paged, fully laid-out content.
#[default] #[default]

View File

@ -1,12 +1,23 @@
//! HTML output. //! HTML output.
mod dom; mod dom;
mod typed;
pub use self::dom::*; pub use self::dom::*;
use ecow::EcoString; use ecow::EcoString;
use crate::foundations::{elem, Content}; 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)
}
/// An HTML element that can contain Typst content. /// An HTML element that can contain Typst content.
/// ///

View File

@ -11,20 +11,19 @@ use bumpalo::Bump;
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, eco_vec, EcoString}; use ecow::{eco_format, eco_vec, EcoString};
use typst_assets::html as data; use typst_assets::html as data;
use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult}; use typst_macros::cast;
use typst_library::engine::Engine;
use typst_library::foundations::{ use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration, Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo, FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value, PositiveF64, Reflect, Scope, Str, Type, Value,
}; };
use typst_library::html::tag; use crate::html::tag;
use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use typst_library::layout::{Axes, Axis, Dir, Length}; use crate::layout::{Axes, Axis, Dir, Length};
use typst_library::visualize::Color; use crate::visualize::Color;
use typst_macros::cast;
use crate::css;
/// Hook up all typed HTML definitions. /// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) { pub(super) fn define(html: &mut Scope) {
@ -706,6 +705,153 @@ 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -12,7 +12,7 @@ use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr,
Selector, ShowFn, Smart, Str, StyleChain, Value, Selector, Show, Smart, Str, StyleChain, Value,
}; };
use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::introspection::{Introspector, Locatable, Location, Tag};
use crate::layout::{Frame, FrameItem, PageElem}; use crate::layout::{Frame, FrameItem, PageElem};
@ -683,8 +683,8 @@ cast! {
} }
/// Executes an update of a counter. /// Executes an update of a counter.
#[elem(Construct, Locatable, Count)] #[elem(Construct, Locatable, Show, Count)]
pub struct CounterUpdateElem { struct CounterUpdateElem {
/// The key that identifies the counter. /// The key that identifies the counter.
#[required] #[required]
key: CounterKey, key: CounterKey,
@ -701,6 +701,12 @@ 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> { impl Count for Packed<CounterUpdateElem> {
fn update(&self) -> Option<CounterUpdate> { fn update(&self) -> Option<CounterUpdate> {
Some(self.update.clone()) Some(self.update.clone())
@ -708,7 +714,7 @@ impl Count for Packed<CounterUpdateElem> {
} }
/// Executes a display of a counter. /// Executes a display of a counter.
#[elem(Construct, Locatable)] #[elem(Construct, Locatable, Show)]
pub struct CounterDisplayElem { pub struct CounterDisplayElem {
/// The counter. /// The counter.
#[required] #[required]
@ -732,18 +738,20 @@ impl Construct for CounterDisplayElem {
} }
} }
pub const COUNTER_DISPLAY_RULE: ShowFn<CounterDisplayElem> = |elem, engine, styles| { impl Show for Packed<CounterDisplayElem> {
Ok(elem fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
.counter Ok(self
.display_impl( .counter
engine, .display_impl(
elem.location().unwrap(), engine,
elem.numbering.clone(), self.location().unwrap(),
elem.both, self.numbering.clone(),
Some(styles), self.both,
)? Some(styles),
.display()) )?
}; .display())
}
}
/// An specialized handler of the page counter that tracks both the physical /// An specialized handler of the page counter that tracks both the physical
/// and the logical page counter. /// and the logical page counter.

View File

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

View File

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

View File

@ -2,10 +2,11 @@ use std::ops::Add;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Reflect, cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed,
Repr, Resolve, StyleChain, Value, Reflect, Repr, Resolve, Show, StyleChain, Value,
}; };
use crate::layout::{Abs, Axes, Axis, Dir, Side}; use crate::layout::{Abs, Axes, Axis, Dir, Side};
use crate::text::TextElem; use crate::text::TextElem;
@ -72,7 +73,7 @@ use crate::text::TextElem;
/// ```example /// ```example
/// Start #h(1fr) End /// Start #h(1fr) End
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct AlignElem { pub struct AlignElem {
/// The [alignment] along both axes. /// The [alignment] along both axes.
/// ///
@ -96,6 +97,13 @@ pub struct AlignElem {
pub body: Content, 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.get(styles)))
}
}
/// Where to align something along an axis. /// Where to align something along an axis.
/// ///
/// Possible values are: /// Possible values are:

View File

@ -1,7 +1,9 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use crate::foundations::{elem, Content}; use crate::diag::SourceResult;
use crate::layout::{Length, Ratio, Rel}; use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{BlockElem, Length, Ratio, Rel};
/// Separates a region into multiple equally sized columns. /// Separates a region into multiple equally sized columns.
/// ///
@ -39,7 +41,7 @@ use crate::layout::{Length, Ratio, Rel};
/// ///
/// #lorem(40) /// #lorem(40)
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct ColumnsElem { pub struct ColumnsElem {
/// The number of columns. /// The number of columns.
#[positional] #[positional]
@ -55,6 +57,14 @@ pub struct ColumnsElem {
pub body: Content, 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. /// Forces a column break.
/// ///
/// The function will behave like a [page break]($pagebreak) when used in a /// The function will behave like a [page break]($pagebreak) when used in a

View File

@ -11,10 +11,10 @@ use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
IntoValue, Packed, Reflect, Resolve, Smart, StyleChain, Value, IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
}; };
use crate::layout::{ use crate::layout::{
Alignment, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
}; };
use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine};
use crate::visualize::{Paint, Stroke}; 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 /// Furthermore, strokes of a repeated grid header or footer will take
/// precedence over regular cell strokes. /// precedence over regular cell strokes.
#[elem(scope)] #[elem(scope, Show)]
pub struct GridElem { pub struct GridElem {
/// The column sizes. /// The column sizes.
/// ///
@ -320,6 +320,14 @@ impl GridElem {
type GridFooter; 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. /// Track sizing definitions.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
@ -640,7 +648,7 @@ pub struct GridVLine {
/// which allows you, for example, to apply styles based on a cell's position. /// 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 /// Refer to the examples of the [`table.cell`]($table.cell) element to learn
/// more about this. /// more about this.
#[elem(name = "cell", title = "Grid Cell")] #[elem(name = "cell", title = "Grid Cell", Show)]
pub struct GridCell { pub struct GridCell {
/// The cell's body. /// The cell's body.
#[required] #[required]
@ -740,6 +748,12 @@ cast! {
v: Content => v.into(), 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.get(styles), self.align.get(styles))
}
}
impl Default for Packed<GridCell> { impl Default for Packed<GridCell> {
fn default() -> Self { fn default() -> Self {
Packed::new( Packed::new(
@ -760,6 +774,28 @@ 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. /// A value that can be configured per cell.
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum Celled<T> { pub enum Celled<T> {

View File

@ -1,4 +1,6 @@
use crate::foundations::{elem, Content}; use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
/// Hides content without affecting layout. /// Hides content without affecting layout.
/// ///
@ -12,7 +14,7 @@ use crate::foundations::{elem, Content};
/// Hello Jane \ /// Hello Jane \
/// #hide[Hello] Joe /// #hide[Hello] Joe
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct HideElem { pub struct HideElem {
/// The content to hide. /// The content to hide.
#[required] #[required]
@ -23,3 +25,10 @@ pub struct HideElem {
#[ghost] #[ghost]
pub hidden: bool, 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().set(HideElem::hidden, true))
}
}

View File

@ -1,7 +1,13 @@
use comemo::Track;
use typst_syntax::Span; use typst_syntax::Span;
use crate::foundations::{elem, func, Content, Func, NativeElement}; use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain,
};
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::layout::{BlockElem, Size};
/// Provides access to the current outer container's (or page's, if none) /// Provides access to the current outer container's (or page's, if none)
/// dimensions (width and height). /// dimensions (width and height).
@ -80,9 +86,37 @@ pub fn layout(
} }
/// Executes a `layout` call. /// Executes a `layout` call.
#[elem(Locatable)] #[elem(Locatable, Show)]
pub struct LayoutElem { struct LayoutElem {
/// The function to call with the outer container's (or page's) size. /// The function to call with the outer container's (or page's) size.
#[required] #[required]
pub func: Func, 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()))
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use codex::styling::MathVariant;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
Synthesize,
}; };
use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::introspection::{Count, Counter, CounterUpdate, Locatable};
use crate::layout::{ use crate::layout::{
AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment,
VAlignment,
}; };
use crate::math::MathSize; use crate::math::{MathSize, MathVariant};
use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement};
use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
@ -45,7 +46,7 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem};
/// least one space lifts it into a separate block that is centered /// least one space lifts it into a separate block that is centered
/// horizontally. For more details about math syntax, see the /// horizontally. For more details about math syntax, see the
/// [main math page]($category/math). /// [main math page]($category/math).
#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)] #[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)]
pub struct EquationElem { pub struct EquationElem {
/// Whether the equation is displayed as a separate block. /// Whether the equation is displayed as a separate block.
#[default(false)] #[default(false)]
@ -112,7 +113,7 @@ pub struct EquationElem {
/// The style variant to select. /// The style variant to select.
#[internal] #[internal]
#[ghost] #[ghost]
pub variant: Option<MathVariant>, pub variant: MathVariant,
/// Affects the height of exponents. /// Affects the height of exponents.
#[internal] #[internal]
@ -129,7 +130,7 @@ pub struct EquationElem {
/// Whether to use italic glyphs. /// Whether to use italic glyphs.
#[internal] #[internal]
#[ghost] #[ghost]
pub italic: Option<bool>, pub italic: Smart<bool>,
/// A forced class to use for all fragment. /// A forced class to use for all fragment.
#[internal] #[internal]
@ -164,6 +165,23 @@ impl Synthesize for Packed<EquationElem> {
} }
} }
impl Show for Packed<EquationElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if self.block.get(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> { impl ShowSet for Packed<EquationElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();

View File

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

View File

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

View File

@ -2,6 +2,7 @@ use std::any::TypeId;
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
@ -16,7 +17,7 @@ use hayagriva::{
use indexmap::IndexMap; use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use typst_syntax::{Span, Spanned, SyntaxMode}; use typst_syntax::{Span, Spanned, SyntaxMode};
use typst_utils::{ManuallyHash, PicoStr}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{ use crate::diag::{
bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos,
@ -25,17 +26,18 @@ use crate::diag::{
use crate::engine::{Engine, Sink}; use crate::engine::{Engine, Sink};
use crate::foundations::{ use crate::foundations::{
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles, OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
Synthesize, Value, Synthesize, Value,
}; };
use crate::introspection::{Introspector, Locatable, Location}; use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::{ use crate::layout::{
BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem,
Sizing, TrackSizings, Sides, Sizing, TrackSizings,
}; };
use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded}; use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded};
use crate::model::{ use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, Url, CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
Url,
}; };
use crate::routines::Routines; use crate::routines::Routines;
use crate::text::{ use crate::text::{
@ -86,7 +88,7 @@ use crate::World;
/// ///
/// #bibliography("works.bib") /// #bibliography("works.bib")
/// ``` /// ```
#[elem(Locatable, Synthesize, ShowSet, LocalName)] #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
pub struct BibliographyElem { pub struct BibliographyElem {
/// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or
/// BibLaTeX `.bib` files. /// BibLaTeX `.bib` files.
@ -201,6 +203,84 @@ impl Synthesize for Packed<BibliographyElem> {
} }
} }
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.get_ref(styles).clone().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.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))
}
}
impl ShowSet for Packed<BibliographyElem> { impl ShowSet for Packed<BibliographyElem> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
const INDENT: Em = Em::new(1.0); const INDENT: Em = Em::new(1.0);
@ -484,7 +564,7 @@ impl IntoValue for CslSource {
/// memoization) for the whole document. This setup is necessary because /// memoization) for the whole document. This setup is necessary because
/// citation formatting is inherently stateful and we need access to all /// citation formatting is inherently stateful and we need access to all
/// citations to do it. /// citations to do it.
pub struct Works { pub(super) struct Works {
/// Maps from the location of a citation group to its rendered content. /// Maps from the location of a citation group to its rendered content.
pub citations: HashMap<Location, SourceResult<Content>>, pub citations: HashMap<Location, SourceResult<Content>>,
/// Lists all references in the bibliography, with optional prefix, or /// Lists all references in the bibliography, with optional prefix, or

View File

@ -3,7 +3,8 @@ use typst_syntax::Spanned;
use crate::diag::{error, At, HintedString, SourceResult}; use crate::diag::{error, At, HintedString, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Cast, Content, Derived, Label, Packed, Smart, StyleChain, Synthesize, cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain,
Synthesize,
}; };
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::model::bibliography::Works; use crate::model::bibliography::Works;
@ -152,15 +153,16 @@ pub enum CitationForm {
/// ///
/// This is automatically created from adjacent citations during show rule /// This is automatically created from adjacent citations during show rule
/// application. /// application.
#[elem(Locatable)] #[elem(Locatable, Show)]
pub struct CiteGroup { pub struct CiteGroup {
/// The citations. /// The citations.
#[required] #[required]
pub children: Vec<Packed<CiteElem>>, pub children: Vec<Packed<CiteElem>>,
} }
impl Packed<CiteGroup> { impl Show for Packed<CiteGroup> {
pub fn realize(&self, engine: &mut Engine) -> SourceResult<Content> { #[typst_macros::time(name = "cite", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
let location = self.location().unwrap(); let location = self.location().unwrap();
let span = self.span(); let span = self.span();
Works::generate(engine) Works::generate(engine)

View File

@ -1,4 +1,10 @@
use crate::foundations::{elem, Content}; 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};
/// Emphasizes content by toggling italics. /// Emphasizes content by toggling italics.
/// ///
@ -23,9 +29,24 @@ use crate::foundations::{elem, Content};
/// This function also has dedicated syntax: To emphasize content, simply /// This function also has dedicated syntax: To emphasize content, simply
/// enclose it in underscores (`_`). Note that this only works at word /// enclose it in underscores (`_`). Note that this only works at word
/// boundaries. To emphasize part of a word, you have to use the function. /// boundaries. To emphasize part of a word, you have to use the function.
#[elem(title = "Emphasis", keywords = ["italic"])] #[elem(title = "Emphasis", keywords = ["italic"], Show)]
pub struct EmphElem { pub struct EmphElem {
/// The content to emphasize. /// The content to emphasize.
#[required] #[required]
pub body: Content, 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 styles.get(TargetElem::target).is_html() {
HtmlElem::new(tag::em)
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
body.set(TextElem::emph, ItalicToggle(true))
})
}
}

View File

@ -1,11 +1,19 @@
use std::str::FromStr; use std::str::FromStr;
use ecow::eco_format;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::diag::bail; use crate::diag::{bail, SourceResult};
use crate::foundations::{cast, elem, scope, Array, Content, Packed, Smart, Styles}; use crate::engine::Engine;
use crate::layout::{Alignment, Em, HAlignment, Length, VAlignment}; use crate::foundations::{
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern}; 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,
};
/// A numbered list. /// A numbered list.
/// ///
@ -63,7 +71,7 @@ use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern};
/// Enumeration items can contain multiple paragraphs and other block-level /// Enumeration items can contain multiple paragraphs and other block-level
/// content. All content that is indented more than an item's marker becomes /// content. All content that is indented more than an item's marker becomes
/// part of that item. /// part of that item.
#[elem(scope, title = "Numbered List")] #[elem(scope, title = "Numbered List", Show)]
pub struct EnumElem { pub struct EnumElem {
/// Defines the default [spacing]($enum.spacing) of the enumeration. If it /// Defines the default [spacing]($enum.spacing) of the enumeration. If it
/// is `{false}`, the items are spaced apart with /// is `{false}`, the items are spaced apart with
@ -215,6 +223,51 @@ impl EnumElem {
type EnumItem; type EnumItem;
} }
impl Show for Packed<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight.get(styles);
if styles.get(TargetElem::target).is_html() {
let mut elem = HtmlElem::new(tag::ol);
if self.reversed.get(styles) {
elem = elem.with_attr(attr::reversed, "reversed");
}
if let Some(n) = self.start.get(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.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 !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
.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)
}
}
/// An enumeration item. /// An enumeration item.
#[elem(name = "item", title = "Numbered List Item")] #[elem(name = "item", title = "Numbered List Item")]
pub struct EnumItem { pub struct EnumItem {

View File

@ -9,16 +9,19 @@ use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector,
ShowSet, Smart, StyleChain, Styles, Synthesize, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
}; };
use crate::html::{tag, HtmlElem};
use crate::introspection::{ use crate::introspection::{
Count, Counter, CounterKey, CounterUpdate, Locatable, Location, Count, Counter, CounterKey, CounterUpdate, Locatable, Location,
}; };
use crate::layout::{ use crate::layout::{
AlignElem, Alignment, BlockElem, Em, Length, OuterVAlignment, PlacementScope, AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment,
VAlignment, PlaceElem, PlacementScope, VAlignment, VElem,
};
use crate::model::{
Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement,
}; };
use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement};
use crate::text::{Lang, Region, TextElem}; use crate::text::{Lang, Region, TextElem};
use crate::visualize::ImageElem; use crate::visualize::ImageElem;
@ -101,7 +104,7 @@ use crate::visualize::ImageElem;
/// caption: [I'm up here], /// caption: [I'm up here],
/// ) /// )
/// ``` /// ```
#[elem(scope, Locatable, Synthesize, Count, ShowSet, Refable, Outlinable)] #[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)]
pub struct FigureElem { pub struct FigureElem {
/// The content of the figure. Often, an [image]. /// The content of the figure. Often, an [image].
#[required] #[required]
@ -325,6 +328,65 @@ impl Synthesize for Packed<FigureElem> {
} }
} }
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 = styles.get(TargetElem::target);
let mut realized = self.body.clone();
// Build the caption, if any.
if let Some(caption) = self.caption.get_cloned(styles) {
let (first, second) = match caption.position.get(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.get(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.get(styles) {
realized = PlaceElem::new(realized)
.with_alignment(align.map(|align| HAlignment::Center + align))
.with_scope(self.scope.get(styles))
.with_float(true)
.pack()
.spanned(span);
} else if self.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)
}
}
impl ShowSet for Packed<FigureElem> { impl ShowSet for Packed<FigureElem> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
// Still allows breakable figures with // Still allows breakable figures with
@ -409,7 +471,7 @@ impl Outlinable for Packed<FigureElem> {
/// caption: [A rectangle], /// caption: [A rectangle],
/// ) /// )
/// ``` /// ```
#[elem(name = "caption", Synthesize)] #[elem(name = "caption", Synthesize, Show)]
pub struct FigureCaption { pub struct FigureCaption {
/// The caption's position in the figure. Either `{top}` or `{bottom}`. /// The caption's position in the figure. Either `{top}` or `{bottom}`.
/// ///
@ -497,35 +559,6 @@ pub struct FigureCaption {
} }
impl 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) /// Gets the default separator in the given language and (optionally)
/// region. /// region.
fn local_separator(lang: Lang, _: Option<Region>) -> &'static str { fn local_separator(lang: Lang, _: Option<Region>) -> &'static str {
@ -555,6 +588,43 @@ impl Synthesize for Packed<FigureCaption> {
} }
} }
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.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(if styles.get(TargetElem::target).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! { cast! {
FigureCaption, FigureCaption,
v: Content => v.unpack::<Self>().unwrap_or_else(Self::new), v: Content => v.unpack::<Self>().unwrap_or_else(Self::new),

View File

@ -3,16 +3,16 @@ use std::str::FromStr;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Content, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart,
Styles, StyleChain, Styles,
}; };
use crate::introspection::{Count, CounterUpdate, Locatable, Location}; use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location};
use crate::layout::{Abs, Em, Length, Ratio}; use crate::layout::{Abs, Em, HElem, Length, Ratio};
use crate::model::{Numbering, NumberingPattern, ParElem}; use crate::model::{Destination, Numbering, NumberingPattern, ParElem};
use crate::text::{TextElem, TextSize}; use crate::text::{SuperElem, TextElem, TextSize};
use crate::visualize::{LineElem, Stroke}; use crate::visualize::{LineElem, Stroke};
/// A footnote. /// A footnote.
@ -51,7 +51,7 @@ use crate::visualize::{LineElem, Stroke};
/// apply to the footnote's content. See [here][issue] for more information. /// apply to the footnote's content. See [here][issue] for more information.
/// ///
/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 /// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
#[elem(scope, Locatable, Count)] #[elem(scope, Locatable, Show, Count)]
pub struct FootnoteElem { pub struct FootnoteElem {
/// How to number footnotes. /// How to number footnotes.
/// ///
@ -135,6 +135,21 @@ 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.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)))
}
}
impl Count for Packed<FootnoteElem> { impl Count for Packed<FootnoteElem> {
fn update(&self) -> Option<CounterUpdate> { fn update(&self) -> Option<CounterUpdate> {
(!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
@ -176,7 +191,7 @@ cast! {
/// page run is a sequence of pages without an explicit pagebreak in between). /// 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 /// 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. /// before any page content, typically at the very start of the document.
#[elem(name = "entry", title = "Footnote Entry", ShowSet)] #[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
pub struct FootnoteEntry { pub struct FootnoteEntry {
/// The footnote for this entry. Its location can be used to determine /// The footnote for this entry. Its location can be used to determine
/// the footnote counter state. /// the footnote counter state.
@ -259,6 +274,37 @@ pub struct FootnoteEntry {
pub indent: Length, 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.get_ref(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.get(styles).into()).pack(),
sup,
HElem::new(number_gap.into()).with_weak(true).pack(),
self.note.body_content().unwrap().clone(),
]))
}
}
impl ShowSet for Packed<FootnoteEntry> { impl ShowSet for Packed<FootnoteEntry> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();

View File

@ -1,16 +1,21 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use typst_utils::NonZeroExt; use ecow::eco_format;
use typst_utils::{Get, NonZeroExt};
use crate::diag::SourceResult; use crate::diag::{warning, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Synthesize, TargetElem,
}; };
use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::html::{attr, tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length}; use crate::introspection::{
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
};
use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides};
use crate::model::{Numbering, Outlinable, Refable, Supplement}; use crate::model::{Numbering, Outlinable, Refable, Supplement};
use crate::text::{FontWeight, LocalName, TextElem, TextSize}; use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize};
/// A section heading. /// A section heading.
/// ///
@ -44,7 +49,7 @@ use crate::text::{FontWeight, LocalName, TextElem, TextSize};
/// one or multiple equals signs, followed by a space. The number of equals /// one or multiple equals signs, followed by a space. The number of equals
/// signs determines the heading's logical nesting depth. The `{offset}` field /// signs determines the heading's logical nesting depth. The `{offset}` field
/// can be set to configure the starting depth. /// can be set to configure the starting depth.
#[elem(Locatable, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)] #[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)]
pub struct HeadingElem { pub struct HeadingElem {
/// The absolute nesting depth of the heading, starting from one. If set /// The absolute nesting depth of the heading, starting from one. If set
/// to `{auto}`, it is computed from `{offset + depth}`. /// to `{auto}`, it is computed from `{offset + depth}`.
@ -210,6 +215,96 @@ impl Synthesize for Packed<HeadingElem> {
} }
} }
impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let html = styles.get(TargetElem::target).is_html();
const SPACING_TO_NUMBERING: Em = Em::new(0.3);
let span = self.span();
let mut realized = self.body.clone();
let hanging_indent = self.hanging_indent.get(styles);
let mut indent = match hanging_indent {
Smart::Custom(length) => length.resolve(styles),
Smart::Auto => Abs::zero(),
};
if let Some(numbering) = self.numbering.get_ref(styles).as_ref() {
let location = self.location().unwrap();
let numbering = Counter::of(HeadingElem::ELEM)
.display_at_loc(engine, location, styles, numbering)?
.spanned(span);
if hanging_indent.is_auto() && !html {
let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false));
// We don't have a locator for the numbering here, so we just
// use the measurement infrastructure for now.
let link = LocatorLink::measure(location);
let size = (engine.routines.layout_frame)(
engine,
&numbering,
Locator::link(&link),
styles,
pod,
)?
.size();
indent = size.x + SPACING_TO_NUMBERING.resolve(styles);
}
let spacing = if html {
SpaceElem::shared().clone()
} else {
HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack()
};
realized = numbering + spacing + realized;
}
Ok(if html {
// HTML's h1 is closer to a title element. There should only be one.
// Meanwhile, a level 1 Typst heading is a section heading. For this
// reason, levels are offset by one: A Typst level 1 heading becomes
// a `<h2>`.
let level = self.resolve_level(styles).get();
if level >= 6 {
engine.sink.warn(warning!(span,
"heading of level {} was transformed to \
<div role=\"heading\" aria-level=\"{}\">, which is not \
supported by all assistive technology",
level, level + 1;
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
hint: "you may want to restructure your document so that \
it doesn't contain deep headings"));
HtmlElem::new(tag::div)
.with_body(Some(realized))
.with_attr(attr::role, "heading")
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
.pack()
.spanned(span)
} else {
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
}
} else {
let block = if indent != Abs::zero() {
let body = HElem::new((-indent).into()).pack() + realized;
let inset = Sides::default()
.with(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)))
};
block.pack().spanned(span)
})
}
}
impl ShowSet for Packed<HeadingElem> { impl ShowSet for Packed<HeadingElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let level = self.resolve_level(styles).get(); let level = self.resolve_level(styles).get();

View File

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

View File

@ -3,10 +3,12 @@ use comemo::Track;
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show,
Smart, StyleChain, Styles, Value, Smart, StyleChain, Styles, TargetElem, Value,
}; };
use crate::layout::{Em, Length}; use crate::html::{tag, HtmlElem};
use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem; use crate::text::TextElem;
/// A bullet list. /// A bullet list.
@ -40,7 +42,7 @@ use crate::text::TextElem;
/// followed by a space to create a list item. A list item can contain multiple /// followed by a space to create a list item. A list item can contain multiple
/// paragraphs and other block-level content. All content that is indented /// paragraphs and other block-level content. All content that is indented
/// more than an item's marker becomes part of that item. /// more than an item's marker becomes part of that item.
#[elem(scope, title = "Bullet List")] #[elem(scope, title = "Bullet List", Show)]
pub struct ListElem { pub struct ListElem {
/// Defines the default [spacing]($list.spacing) of the list. If it is /// Defines the default [spacing]($list.spacing) of the list. If it is
/// `{false}`, the items are spaced apart with /// `{false}`, the items are spaced apart with
@ -134,6 +136,45 @@ impl ListElem {
type ListItem; type ListItem;
} }
impl Show for Packed<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let tight = self.tight.get(styles);
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::ul)
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
HtmlElem::new(tag::li)
.with_body(Some(body))
.pack()
.spanned(item.span())
}))))
.pack()
.spanned(self.span()));
}
let mut realized =
BlockElem::multi_layouter(self.clone(), engine.routines.layout_list)
.pack()
.spanned(self.span());
if tight {
let spacing = self
.spacing
.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)
}
}
/// A bullet list item. /// A bullet list item.
#[elem(name = "item", title = "Bullet List Item")] #[elem(name = "item", title = "Bullet List Item")]
pub struct ListItem { pub struct ListItem {

View File

@ -46,23 +46,23 @@ use crate::foundations::Scope;
pub fn define(global: &mut Scope) { pub fn define(global: &mut Scope) {
global.start_category(crate::Category::Model); global.start_category(crate::Category::Model);
global.define_elem::<DocumentElem>(); global.define_elem::<DocumentElem>();
global.define_elem::<ParElem>(); global.define_elem::<RefElem>();
global.define_elem::<ParbreakElem>();
global.define_elem::<StrongElem>();
global.define_elem::<EmphElem>();
global.define_elem::<ListElem>();
global.define_elem::<EnumElem>();
global.define_elem::<TermsElem>();
global.define_elem::<LinkElem>(); global.define_elem::<LinkElem>();
global.define_elem::<OutlineElem>();
global.define_elem::<HeadingElem>(); global.define_elem::<HeadingElem>();
global.define_elem::<FigureElem>(); global.define_elem::<FigureElem>();
global.define_elem::<QuoteElem>();
global.define_elem::<FootnoteElem>(); global.define_elem::<FootnoteElem>();
global.define_elem::<OutlineElem>(); global.define_elem::<QuoteElem>();
global.define_elem::<RefElem>();
global.define_elem::<CiteElem>(); global.define_elem::<CiteElem>();
global.define_elem::<BibliographyElem>(); global.define_elem::<BibliographyElem>();
global.define_elem::<EnumElem>();
global.define_elem::<ListElem>();
global.define_elem::<ParbreakElem>();
global.define_elem::<ParElem>();
global.define_elem::<TableElem>(); global.define_elem::<TableElem>();
global.define_elem::<TermsElem>();
global.define_elem::<EmphElem>();
global.define_elem::<StrongElem>();
global.define_func::<numbering>(); global.define_func::<numbering>();
global.reset_category(); global.reset_category();
} }

View File

@ -18,7 +18,7 @@ use crate::foundations::{cast, func, Context, Func, Str, Value};
/// ///
/// A numbering pattern consists of counting symbols, for which the actual /// A numbering pattern consists of counting symbols, for which the actual
/// number is substituted, their prefixes, and one suffix. The prefixes and the /// number is substituted, their prefixes, and one suffix. The prefixes and the
/// suffix are displayed as-is. /// suffix are repeated as-is.
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -66,10 +66,10 @@ pub fn numbering(
/// items, the number is represented using repeated symbols. /// items, the number is represented using repeated symbols.
/// ///
/// **Suffixes** are all characters after the last counting symbol. They are /// **Suffixes** are all characters after the last counting symbol. They are
/// displayed as-is at the end of any rendered number. /// repeated as-is at the end of any rendered number.
/// ///
/// **Prefixes** are all characters that are neither counting symbols nor /// **Prefixes** are all characters that are neither counting symbols nor
/// suffixes. They are displayed as-is at in front of their rendered /// suffixes. They are repeated as-is at in front of their rendered
/// equivalent of their counting symbol. /// equivalent of their counting symbol.
/// ///
/// This parameter can also be an arbitrary function that gets each number /// This parameter can also be an arbitrary function that gets each number

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::str::FromStr; use std::str::FromStr;
use comemo::Tracked; use comemo::{Track, Tracked};
use smallvec::SmallVec; use smallvec::SmallVec;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, NonZeroExt}; use typst_utils::{Get, NonZeroExt};
@ -10,7 +10,7 @@ use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func,
LocatableSelector, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
Styles, Styles,
}; };
use crate::introspection::{ use crate::introspection::{
@ -20,7 +20,8 @@ use crate::layout::{
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
RepeatElem, Sides, RepeatElem, Sides,
}; };
use crate::model::{HeadingElem, NumberingPattern, ParElem, Refable}; use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
use crate::text::{LocalName, SpaceElem, TextElem}; use crate::text::{LocalName, SpaceElem, TextElem};
/// A table of contents, figures, or other elements. /// A table of contents, figures, or other elements.
@ -146,7 +147,7 @@ use crate::text::{LocalName, SpaceElem, TextElem};
/// ///
/// [^1]: The outline of equations is the exception to this rule as it does not /// [^1]: The outline of equations is the exception to this rule as it does not
/// have a body and thus does not use indented layout. /// have a body and thus does not use indented layout.
#[elem(scope, keywords = ["Table of Contents", "toc"], ShowSet, LocalName, Locatable)] #[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)]
pub struct OutlineElem { pub struct OutlineElem {
/// The title of the outline. /// The title of the outline.
/// ///
@ -248,6 +249,44 @@ impl OutlineElem {
type OutlineEntry; type OutlineEntry;
} }
impl Show for Packed<OutlineElem> {
#[typst_macros::time(name = "outline", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
// Build the outline title.
let mut seq = vec![];
if let Some(title) = self.title.get_cloned(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 elems = engine.introspector.query(&self.target.get_ref(styles).0);
let depth = self.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))
}
}
impl ShowSet for Packed<OutlineElem> { impl ShowSet for Packed<OutlineElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();
@ -324,7 +363,7 @@ pub trait Outlinable: Refable {
/// With show-set and show rules on outline entries, you can richly customize /// With show-set and show rules on outline entries, you can richly customize
/// the outline's appearance. See the /// the outline's appearance. See the
/// [section on styling the outline]($outline/#styling-the-outline) for details. /// [section on styling the outline]($outline/#styling-the-outline) for details.
#[elem(scope, name = "entry", title = "Outline Entry")] #[elem(scope, name = "entry", title = "Outline Entry", Show)]
pub struct OutlineEntry { pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level /// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries. /// entries.
@ -369,6 +408,30 @@ pub struct OutlineEntry {
pub parent: Option<Packed<OutlineElem>>, pub parent: Option<Packed<OutlineElem>>,
} }
impl Show for Packed<OutlineEntry> {
#[typst_macros::time(name = "outline.entry", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let context = Context::new(None, Some(styles));
let context = context.track();
let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?;
let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner;
BlockElem::new()
.with_body(Some(BlockBody::Content(body)))
.pack()
.spanned(span)
} else {
self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())?
};
let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc)))
}
}
#[scope] #[scope]
impl OutlineEntry { impl OutlineEntry {
/// A helper function for producing an indented entry layout: Lays out a /// A helper function for producing an indented entry layout: Lays out a
@ -591,8 +654,7 @@ impl OutlineEntry {
.ok_or_else(|| error!("cannot outline {}", self.element.func().name())) .ok_or_else(|| error!("cannot outline {}", self.element.func().name()))
} }
/// Returns the location of the outlined element. fn element_location(&self) -> HintedStrResult<Location> {
pub fn element_location(&self) -> HintedStrResult<Location> {
let elem = &self.element; let elem = &self.element;
elem.location().ok_or_else(|| { elem.location().ok_or_else(|| {
if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() { if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
@ -668,8 +730,8 @@ fn query_prefix_widths(
} }
/// Helper type for introspection-based prefix alignment. /// Helper type for introspection-based prefix alignment.
#[elem(Construct, Locatable)] #[elem(Construct, Locatable, Show)]
pub(crate) struct PrefixInfo { struct PrefixInfo {
/// The location of the outline this prefix is part of. This is used to /// The location of the outline this prefix is part of. This is used to
/// scope prefix computations to a specific outline. /// scope prefix computations to a specific outline.
#[required] #[required]
@ -691,3 +753,9 @@ impl Construct for PrefixInfo {
bail!(args.span, "cannot be constructed manually"); bail!(args.span, "cannot be constructed manually");
} }
} }
impl Show for Packed<PrefixInfo> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(Content::empty())
}
}

View File

@ -1,13 +1,16 @@
use typst_syntax::Span; use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
Styles, StyleChain, Styles, TargetElem,
}; };
use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::layout::{BlockElem, Em, PadElem}; use crate::layout::{
use crate::model::{CitationForm, CiteElem}; Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
use crate::text::{SmartQuotes, SpaceElem, TextElem}; };
use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
/// Displays a quote alongside an optional attribution. /// Displays a quote alongside an optional attribution.
/// ///
@ -41,7 +44,7 @@ use crate::text::{SmartQuotes, SpaceElem, TextElem};
/// flame of Udûn. Go back to the Shadow! You cannot pass. /// flame of Udûn. Go back to the Shadow! You cannot pass.
/// ] /// ]
/// ``` /// ```
#[elem(Locatable, ShowSet)] #[elem(Locatable, ShowSet, Show)]
pub struct QuoteElem { pub struct QuoteElem {
/// Whether this is a block quote. /// Whether this is a block quote.
/// ///
@ -59,7 +62,7 @@ pub struct QuoteElem {
/// Ich bin ein Berliner. /// Ich bin ein Berliner.
/// ] /// ]
/// ``` /// ```
pub block: bool, block: bool,
/// Whether double quotes should be added around this quote. /// Whether double quotes should be added around this quote.
/// ///
@ -85,7 +88,7 @@ pub struct QuoteElem {
/// translate the quote: /// translate the quote:
/// #quote[I am a Berliner.] /// #quote[I am a Berliner.]
/// ``` /// ```
pub quotes: Smart<bool>, quotes: Smart<bool>,
/// The attribution of this quote, usually the author or source. Can be a /// The attribution of this quote, usually the author or source. Can be a
/// label pointing to a bibliography entry or any content. By default only /// label pointing to a bibliography entry or any content. By default only
@ -102,7 +105,7 @@ pub struct QuoteElem {
/// } /// }
/// ///
/// #quote( /// #quote(
/// attribution: link("https://typst.app/home")[typst.app] /// attribution: link("https://typst.app/home")[typst.com]
/// )[ /// )[
/// Compose papers faster /// Compose papers faster
/// ] /// ]
@ -120,36 +123,17 @@ pub struct QuoteElem {
/// ///
/// #bibliography("works.bib", style: "apa") /// #bibliography("works.bib", style: "apa")
/// ``` /// ```
pub attribution: Option<Attribution>, attribution: Option<Attribution>,
/// The quote. /// The quote.
#[required] #[required]
pub body: Content, body: Content,
/// The nesting depth. /// The nesting depth.
#[internal] #[internal]
#[fold] #[fold]
#[ghost] #[ghost]
pub depth: Depth, depth: Depth,
}
impl QuoteElem {
/// Quotes the body content with the appropriate quotes based on the current
/// styles and surroundings.
pub fn quoted(body: Content, styles: StyleChain<'_>) -> Content {
let quotes = SmartQuotes::get_in(styles);
// Alternate between single and double quotes.
let Depth(depth) = styles.get(QuoteElem::depth);
let double = depth % 2 == 0;
Content::sequence([
TextElem::packed(quotes.open(double)),
body,
TextElem::packed(quotes.close(double)),
])
.set(QuoteElem::depth, Depth(1))
}
} }
/// Attribution for a [quote](QuoteElem). /// Attribution for a [quote](QuoteElem).
@ -159,23 +143,6 @@ pub enum Attribution {
Label(Label), Label(Label),
} }
impl Attribution {
/// Realize as an em dash followed by text or a citation.
pub fn realize(&self, span: Span) -> Content {
Content::sequence([
TextElem::packed('—'),
SpaceElem::shared().clone(),
match self {
Attribution::Content(content) => content.clone(),
Attribution::Label(label) => CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(span),
},
])
}
}
cast! { cast! {
Attribution, Attribution,
self => match self { self => match self {
@ -186,6 +153,96 @@ cast! {
label: Label => Self::Label(label), label: Label => Self::Label(label),
} }
impl Show for Packed<QuoteElem> {
#[typst_macros::time(name = "quote", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body.clone();
let block = self.block.get(styles);
let html = styles.get(TargetElem::target).is_html();
if self.quotes.get(styles).unwrap_or(!block) {
let quotes = SmartQuotes::get(
styles.get_ref(SmartQuoteElem::quotes),
styles.get(TextElem::lang),
styles.get(TextElem::region),
styles.get(SmartQuoteElem::alternative),
);
// Alternate between single and double quotes.
let Depth(depth) = styles.get(QuoteElem::depth);
let double = depth % 2 == 0;
if !html {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
realized = Content::sequence([hole.clone(), realized, hole]);
}
realized = Content::sequence([
TextElem::packed(quotes.open(double)),
realized,
TextElem::packed(quotes.close(double)),
])
.set(QuoteElem::depth, Depth(1));
}
let attribution = self.attribution.get_ref(styles);
if block {
realized = if html {
let mut elem = 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 {
elem = elem.with_attr(attr::cite, url.clone().into_inner());
}
}
}
elem.pack()
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack()
}
.spanned(self.span());
if let Some(attribution) = attribution {
let attribution = match attribution {
Attribution::Content(content) => content.clone(),
Attribution::Label(label) => CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
};
let attribution = Content::sequence([
TextElem::packed('—'),
SpaceElem::shared().clone(),
attribution,
]);
if html {
realized += attribution;
} else {
// 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)))
.pack()
.aligned(Alignment::END);
}
}
if !html {
realized = PadElem::new(realized).pack();
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone()
+ CiteElem::new(*label).pack().spanned(self.span());
}
Ok(realized)
}
}
impl ShowSet for Packed<QuoteElem> { impl ShowSet for Packed<QuoteElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();

View File

@ -5,7 +5,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed, cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed,
Repr, Smart, StyleChain, Synthesize, Repr, Show, Smart, StyleChain, Synthesize,
}; };
use crate::introspection::{Counter, CounterKey, Locatable}; use crate::introspection::{Counter, CounterKey, Locatable};
use crate::math::EquationElem; use crate::math::EquationElem;
@ -134,7 +134,7 @@ use crate::text::TextElem;
/// In @beginning we prove @pythagoras. /// In @beginning we prove @pythagoras.
/// $ a^2 + b^2 = c^2 $ <pythagoras> /// $ a^2 + b^2 = c^2 $ <pythagoras>
/// ``` /// ```
#[elem(title = "Reference", Synthesize, Locatable)] #[elem(title = "Reference", Synthesize, Locatable, Show)]
pub struct RefElem { pub struct RefElem {
/// The target label that should be referenced. /// The target label that should be referenced.
/// ///
@ -220,13 +220,9 @@ impl Synthesize for Packed<RefElem> {
} }
} }
impl Packed<RefElem> { impl Show for Packed<RefElem> {
/// Realize as a linked, textual reference. #[typst_macros::time(name = "ref", span = self.span())]
pub fn realize( fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
&self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<Content> {
let elem = engine.introspector.query_label(self.target); let elem = engine.introspector.query_label(self.target);
let span = self.span(); let span = self.span();
@ -246,7 +242,7 @@ impl Packed<RefElem> {
.at(span)?; .at(span)?;
let supplement = engine.introspector.page_supplement(loc); let supplement = engine.introspector.page_supplement(loc);
return realize_reference( return show_reference(
self, self,
engine, engine,
styles, styles,
@ -310,7 +306,7 @@ impl Packed<RefElem> {
)) ))
.at(span)?; .at(span)?;
realize_reference( show_reference(
self, self,
engine, engine,
styles, styles,
@ -323,7 +319,7 @@ impl Packed<RefElem> {
} }
/// Show a reference. /// Show a reference.
fn realize_reference( fn show_reference(
reference: &Packed<RefElem>, reference: &Packed<RefElem>,
engine: &mut Engine, engine: &mut Engine,
styles: StyleChain, styles: StyleChain,

View File

@ -1,4 +1,10 @@
use crate::foundations::{elem, Content}; 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::{TextElem, WeightDelta};
/// Strongly emphasizes content by increasing the font weight. /// Strongly emphasizes content by increasing the font weight.
/// ///
@ -18,7 +24,7 @@ use crate::foundations::{elem, Content};
/// simply enclose it in stars/asterisks (`*`). Note that this only works at /// simply enclose it in stars/asterisks (`*`). Note that this only works at
/// word boundaries. To strongly emphasize part of a word, you have to use the /// word boundaries. To strongly emphasize part of a word, you have to use the
/// function. /// function.
#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"])] #[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Show)]
pub struct StrongElem { pub struct StrongElem {
/// The delta to apply on the font weight. /// The delta to apply on the font weight.
/// ///
@ -33,3 +39,18 @@ pub struct StrongElem {
#[required] #[required]
pub body: Content, pub body: Content,
} }
impl Show for Packed<StrongElem> {
#[typst_macros::time(name = "strong", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
Ok(if styles.get(TargetElem::target).is_html() {
HtmlElem::new(tag::strong)
.with_body(Some(body))
.pack()
.spanned(self.span())
} else {
body.set(TextElem::delta, WeightDelta(self.delta.get(styles)))
})
}
}

View File

@ -3,11 +3,19 @@ use std::sync::Arc;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use crate::diag::{bail, HintedStrResult, HintedString}; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
use crate::foundations::{cast, elem, scope, Content, Packed, Smart}; use crate::engine::Engine;
use crate::foundations::{
cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
TargetElem,
};
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator;
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use crate::layout::{ use crate::layout::{
Abs, Alignment, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
Length, OuterHAlignment, OuterVAlignment, Rel, Sides, TrackSizings, GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides,
TrackSizings,
}; };
use crate::model::Figurable; use crate::model::Figurable;
use crate::text::LocalName; use crate::text::LocalName;
@ -113,7 +121,7 @@ use crate::visualize::{Paint, Stroke};
/// [Robert], b, a, b, /// [Robert], b, a, b,
/// ) /// )
/// ``` /// ```
#[elem(scope, LocalName, Figurable)] #[elem(scope, Show, LocalName, Figurable)]
pub struct TableElem { pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more /// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing. /// information on track sizing.
@ -247,6 +255,113 @@ impl TableElem {
type TableFooter; type TableFooter;
} }
fn show_cell_html(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())
}
fn show_cellgrid_html(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_html(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))
}
impl Show for Packed<TableElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(if styles.get(TargetElem::target).is_html() {
// TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
// How can we find out whether locator is actually used?
let locator = Locator::root();
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
} else {
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
}
.spanned(self.span()))
}
}
impl LocalName for Packed<TableElem> { impl LocalName for Packed<TableElem> {
const KEY: &'static str = "table"; const KEY: &'static str = "table";
} }
@ -646,7 +761,7 @@ pub struct TableVLine {
/// [Vikram], [49], [Perseverance], /// [Vikram], [49], [Perseverance],
/// ) /// )
/// ``` /// ```
#[elem(name = "cell", title = "Table Cell")] #[elem(name = "cell", title = "Table Cell", Show)]
pub struct TableCell { pub struct TableCell {
/// The cell's body. /// The cell's body.
#[required] #[required]
@ -693,6 +808,12 @@ cast! {
v: Content => v.into(), v: Content => v.into(),
} }
impl Show for Packed<TableCell> {
fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
show_grid_cell(self.body.clone(), self.inset.get(styles), self.align.get(styles))
}
}
impl Default for Packed<TableCell> { impl Default for Packed<TableCell> {
fn default() -> Self { fn default() -> Self {
Packed::new( Packed::new(

View File

@ -1,9 +1,15 @@
use crate::diag::bail; use typst_utils::{Get, Numeric};
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Array, Content, NativeElement, Packed, Smart, Styles, cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
Styles, TargetElem,
}; };
use crate::layout::{Em, HElem, Length}; use crate::html::{tag, HtmlElem};
use crate::model::{ListItemLike, ListLike}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem;
/// A list of terms and their descriptions. /// A list of terms and their descriptions.
/// ///
@ -21,7 +27,7 @@ use crate::model::{ListItemLike, ListLike};
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Starting a line with a slash, /// This function also has dedicated syntax: Starting a line with a slash,
/// followed by a term, a colon and a description creates a term list item. /// followed by a term, a colon and a description creates a term list item.
#[elem(scope, title = "Term List")] #[elem(scope, title = "Term List", Show)]
pub struct TermsElem { pub struct TermsElem {
/// Defines the default [spacing]($terms.spacing) of the term list. If it is /// Defines the default [spacing]($terms.spacing) of the term list. If it is
/// `{false}`, the items are spaced apart with /// `{false}`, the items are spaced apart with
@ -111,6 +117,94 @@ impl TermsElem {
type TermItem; type TermItem;
} }
impl Show for Packed<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let span = self.span();
let tight = self.tight.get(styles);
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::dl)
.with_body(Some(Content::sequence(self.children.iter().flat_map(
|item| {
// Text in wide term lists shall always turn into paragraphs.
let mut description = item.description.clone();
if !tight {
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());
}
let separator = self.separator.get_ref(styles);
let indent = self.indent.get(styles);
let hanging_indent = self.hanging_indent.get(styles);
let gutter = self.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 self.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 = self
.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)
}
}
/// A term list item. /// A term list item.
#[elem(name = "item", title = "Term List Item")] #[elem(name = "item", title = "Term List Item")]
pub struct TermItem { pub struct TermItem {

View File

@ -1,8 +1,12 @@
use ecow::EcoString; use ecow::EcoString;
use typst_library::foundations::Target;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::At; use crate::diag::{warning, At, SourceResult};
use crate::foundations::{elem, Bytes, Cast, Derived}; use crate::engine::Engine;
use crate::foundations::{
elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
};
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::World; use crate::World;
@ -29,7 +33,7 @@ use crate::World;
/// - This element is ignored if exporting to a format other than PDF. /// - This element is ignored if exporting to a format other than PDF.
/// - File embeddings are not currently supported for PDF/A-2, even if the /// - File embeddings are not currently supported for PDF/A-2, even if the
/// embedded file conforms to PDF/A-1 or PDF/A-2. /// embedded file conforms to PDF/A-1 or PDF/A-2.
#[elem(Locatable)] #[elem(Show, Locatable)]
pub struct EmbedElem { pub struct EmbedElem {
/// The [path]($syntax/#paths) of the file to be embedded. /// The [path]($syntax/#paths) of the file to be embedded.
/// ///
@ -73,6 +77,17 @@ pub struct EmbedElem {
pub description: Option<EcoString>, pub description: Option<EcoString>,
} }
impl Show for Packed<EmbedElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if styles.get(TargetElem::target) == Target::Html {
engine
.sink
.warn(warning!(self.span(), "embed was ignored during HTML export"));
}
Ok(Content::empty())
}
}
/// The relationship of an embedded file with the document. /// The relationship of an embedded file with the document.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum EmbeddedFileRelationship { pub enum EmbeddedFileRelationship {

View File

@ -1,5 +1,7 @@
use std::fmt::{self, Debug, Formatter}; #![allow(unused)]
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut}; use comemo::{Tracked, TrackedMut};
use typst_syntax::{Span, SyntaxMode}; use typst_syntax::{Span, SyntaxMode};
@ -8,12 +10,20 @@ use typst_utils::LazyHash;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::{Engine, Route, Sink, Traced}; use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{ use crate::foundations::{
Args, Closure, Content, Context, Func, Module, NativeRuleMap, Scope, StyleChain, Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value,
Styles, Value,
}; };
use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::introspection::{Introspector, Locator, SplitLocator};
use crate::layout::{Frame, Region}; use crate::layout::{
use crate::model::DocumentInfo; Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem,
PagedDocument, Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size,
SkewElem, StackElem,
};
use crate::math::EquationElem;
use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem};
use crate::visualize::{
CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem,
RectElem, SquareElem,
};
use crate::World; use crate::World;
/// Defines the `Routines` struct. /// Defines the `Routines` struct.
@ -28,8 +38,6 @@ macro_rules! routines {
/// This is essentially dynamic linking and done to allow for crate /// This is essentially dynamic linking and done to allow for crate
/// splitting. /// splitting.
pub struct Routines { pub struct Routines {
/// Native show rules.
pub rules: NativeRuleMap,
$( $(
$(#[$attr])* $(#[$attr])*
pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret
@ -39,12 +47,6 @@ macro_rules! routines {
impl Hash for Routines { impl Hash for Routines {
fn hash<H: Hasher>(&self, _: &mut H) {} fn hash<H: Hasher>(&self, _: &mut H) {}
} }
impl Debug for Routines {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Routines(..)")
}
}
}; };
} }
@ -84,6 +86,15 @@ routines! {
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Vec<Pair<'a>>> ) -> SourceResult<Vec<Pair<'a>>>
/// Lays out content into multiple regions.
fn layout_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out content into a single region, producing a single frame. /// Lays out content into a single region, producing a single frame.
fn layout_frame( fn layout_frame(
engine: &mut Engine, engine: &mut Engine,
@ -93,8 +104,212 @@ routines! {
region: Region, region: Region,
) -> SourceResult<Frame> ) -> SourceResult<Frame>
/// Constructs the `html` module. /// Lays out a [`ListElem`].
fn html_module() -> Module fn layout_list(
elem: &Packed<ListElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out an [`EnumElem`].
fn layout_enum(
elem: &Packed<EnumElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`GridElem`].
fn layout_grid(
elem: &Packed<GridElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`TableElem`].
fn layout_table(
elem: &Packed<TableElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`StackElem`].
fn layout_stack(
elem: &Packed<StackElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`ColumnsElem`].
fn layout_columns(
elem: &Packed<ColumnsElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`MoveElem`].
fn layout_move(
elem: &Packed<MoveElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`RotateElem`].
fn layout_rotate(
elem: &Packed<RotateElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`ScaleElem`].
fn layout_scale(
elem: &Packed<ScaleElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`SkewElem`].
fn layout_skew(
elem: &Packed<SkewElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`RepeatElem`].
fn layout_repeat(
elem: &Packed<RepeatElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`PadElem`].
fn layout_pad(
elem: &Packed<PadElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
/// Lays out a [`LineElem`].
fn layout_line(
elem: &Packed<LineElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`CurveElem`].
fn layout_curve(
elem: &Packed<CurveElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`PathElem`].
fn layout_path(
elem: &Packed<PathElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`PolygonElem`].
fn layout_polygon(
elem: &Packed<PolygonElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`RectElem`].
fn layout_rect(
elem: &Packed<RectElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`SquareElem`].
fn layout_square(
elem: &Packed<SquareElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`EllipseElem`].
fn layout_ellipse(
elem: &Packed<EllipseElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out a [`CircleElem`].
fn layout_circle(
elem: &Packed<CircleElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out an [`ImageElem`].
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame>
/// Lays out an [`EquationElem`] in a paragraph.
fn layout_equation_inline(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Size,
) -> SourceResult<Vec<InlineItem>>
/// Lays out an [`EquationElem`] in a flow.
fn layout_equation_block(
elem: &Packed<EquationElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment>
} }
/// Defines what kind of realization we are performing. /// Defines what kind of realization we are performing.

View File

@ -1,6 +1,13 @@
use crate::foundations::{elem, Content, Smart}; use smallvec::smallvec;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Abs, Corners, Length, Rel, Sides}; use crate::layout::{Abs, Corners, Length, Rel, Sides};
use crate::text::{BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::visualize::{Color, FixedStroke, Paint, Stroke}; use crate::visualize::{Color, FixedStroke, Paint, Stroke};
/// Underlines text. /// Underlines text.
@ -9,7 +16,7 @@ use crate::visualize::{Color, FixedStroke, Paint, Stroke};
/// ```example /// ```example
/// This is #underline[important]. /// This is #underline[important].
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct UnderlineElem { pub struct UnderlineElem {
/// How to [stroke] the line. /// How to [stroke] the line.
/// ///
@ -71,13 +78,41 @@ pub struct UnderlineElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<UnderlineElem> {
#[typst_macros::time(name = "underline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if styles.get(TargetElem::target).is_html() {
// Note: In modern HTML, `<u>` is not the underline element, but
// rather an "Unarticulated Annotation" element (see HTML spec
// 4.5.22). Using `text-decoration` instead is recommended by MDN.
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: underline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Underline {
stroke: self.stroke.resolve(styles).unwrap_or_default(),
offset: self.offset.resolve(styles),
evade: self.evade.get(styles),
background: self.background.get(styles),
},
extent: self.extent.resolve(styles),
}],
))
}
}
/// Adds a line over text. /// Adds a line over text.
/// ///
/// # Example /// # Example
/// ```example /// ```example
/// #overline[A line over text.] /// #overline[A line over text.]
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct OverlineElem { pub struct OverlineElem {
/// How to [stroke] the line. /// How to [stroke] the line.
/// ///
@ -145,13 +180,38 @@ pub struct OverlineElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<OverlineElem> {
#[typst_macros::time(name = "overline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: overline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Overline {
stroke: self.stroke.resolve(styles).unwrap_or_default(),
offset: self.offset.resolve(styles),
evade: self.evade.get(styles),
background: self.background.get(styles),
},
extent: self.extent.resolve(styles),
}],
))
}
}
/// Strikes through text. /// Strikes through text.
/// ///
/// # Example /// # Example
/// ```example /// ```example
/// This is #strike[not] relevant. /// This is #strike[not] relevant.
/// ``` /// ```
#[elem(title = "Strikethrough")] #[elem(title = "Strikethrough", Show)]
pub struct StrikeElem { pub struct StrikeElem {
/// How to [stroke] the line. /// How to [stroke] the line.
/// ///
@ -204,13 +264,35 @@ pub struct StrikeElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<StrikeElem> {
#[typst_macros::time(name = "strike", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack());
}
Ok(self.body.clone().set(
TextElem::deco,
smallvec![Decoration {
// Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough {
stroke: self.stroke.resolve(styles).unwrap_or_default(),
offset: self.offset.resolve(styles),
background: self.background.get(styles),
},
extent: self.extent.resolve(styles),
}],
))
}
}
/// Highlights text with a background color. /// Highlights text with a background color.
/// ///
/// # Example /// # Example
/// ```example /// ```example
/// This is #highlight[important]. /// This is #highlight[important].
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct HighlightElem { pub struct HighlightElem {
/// The color to highlight the text with. /// The color to highlight the text with.
/// ///
@ -281,6 +363,35 @@ pub struct HighlightElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<HighlightElem> {
#[typst_macros::time(name = "highlight", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::mark)
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().set(
TextElem::deco,
smallvec![Decoration {
line: DecoLine::Highlight {
fill: self.fill.get_cloned(styles),
stroke: self
.stroke
.resolve(styles)
.unwrap_or_default()
.map(|stroke| stroke.map(Stroke::unwrap_or_default)),
top_edge: self.top_edge.get(styles),
bottom_edge: self.bottom_edge.get(styles),
radius: self.radius.resolve(styles).unwrap_or_default(),
},
extent: self.extent.resolve(styles),
}],
))
}
}
/// A text decoration. /// A text decoration.
/// ///
/// Can be positioned over, under, or on top of text, or highlight the text with /// Can be positioned over, under, or on top of text, or highlight the text with

View File

@ -16,13 +16,14 @@ use crate::diag::{
}; };
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, scope, Bytes, Content, Derived, OneOrMultiple, Packed, PlainText, cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed,
ShowSet, Smart, StyleChain, Styles, Synthesize, PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
}; };
use crate::layout::{Em, HAlignment}; use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load}; use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
use crate::text::{FontFamily, FontList, LocalName, TextElem, TextSize}; use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
use crate::visualize::Color; use crate::visualize::Color;
use crate::World; use crate::World;
@ -77,6 +78,7 @@ use crate::World;
scope, scope,
title = "Raw Text / Code", title = "Raw Text / Code",
Synthesize, Synthesize,
Show,
ShowSet, ShowSet,
LocalName, LocalName,
Figurable, Figurable,
@ -427,6 +429,46 @@ impl Packed<RawElem> {
} }
} }
impl Show for Packed<RawElem> {
#[typst_macros::time(name = "raw", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let lines = self.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 styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(if self.block.get(styles) {
tag::pre
} else {
tag::code
})
.with_body(Some(realized))
.pack()
.spanned(self.span()));
}
if self.block.get(styles) {
// Align the text before inserting it into the block.
realized = realized.aligned(self.align.get(styles).into());
realized = BlockElem::new()
.with_body(Some(BlockBody::Content(realized)))
.pack()
.spanned(self.span());
}
Ok(realized)
}
}
impl ShowSet for Packed<RawElem> { impl ShowSet for Packed<RawElem> {
fn show_set(&self, styles: StyleChain) -> Styles { fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();
@ -456,11 +498,7 @@ impl PlainText for Packed<RawElem> {
} }
/// The content of the raw text. /// The content of the raw text.
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash, PartialEq)]
#[allow(
clippy::derived_hash_with_manual_eq,
reason = "https://github.com/typst/typst/pull/6560#issuecomment-3045393640"
)]
pub enum RawContent { pub enum RawContent {
/// From a string. /// From a string.
Text(EcoString), Text(EcoString),
@ -485,22 +523,6 @@ impl RawContent {
} }
} }
impl PartialEq for RawContent {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(RawContent::Text(a), RawContent::Text(b)) => a == b,
(lines @ RawContent::Lines(_), RawContent::Text(text))
| (RawContent::Text(text), lines @ RawContent::Lines(_)) => {
*text == lines.get()
}
(RawContent::Lines(a), RawContent::Lines(b)) => Iterator::eq(
a.iter().map(|(line, _)| line),
b.iter().map(|(line, _)| line),
),
}
}
}
cast! { cast! {
RawContent, RawContent,
self => self.get().into_value(), self => self.get().into_value(),
@ -612,7 +634,7 @@ fn format_theme_error(error: syntect::LoadingError) -> LoadError {
/// It allows you to access various properties of the line, such as the line /// It allows you to access various properties of the line, such as the line
/// number, the raw non-highlighted text, the highlighted text, and whether it /// number, the raw non-highlighted text, the highlighted text, and whether it
/// is the first or last line of the raw block. /// is the first or last line of the raw block.
#[elem(name = "line", title = "Raw Text / Code Line", PlainText)] #[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)]
pub struct RawLine { pub struct RawLine {
/// The line number of the raw line inside of the raw block, starts at 1. /// The line number of the raw line inside of the raw block, starts at 1.
#[required] #[required]
@ -631,6 +653,13 @@ pub struct RawLine {
pub body: Content, pub body: Content,
} }
impl Show for Packed<RawLine> {
#[typst_macros::time(name = "raw.line", span = self.span())]
fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
impl PlainText for Packed<RawLine> { impl PlainText for Packed<RawLine> {
fn plain_text(&self, text: &mut EcoString) { fn plain_text(&self, text: &mut EcoString) {
text.push_str(&self.text); text.push_str(&self.text);

View File

@ -1,8 +1,13 @@
use ttf_parser::Tag; use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Smart}; use crate::foundations::{
elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{Em, Length}; use crate::layout::{Em, Length};
use crate::text::{FontMetrics, ScriptMetrics, TextSize}; use crate::text::{FontMetrics, TextElem, TextSize};
use ttf_parser::Tag;
use typst_library::text::ScriptMetrics;
/// Renders text in subscript. /// Renders text in subscript.
/// ///
@ -12,7 +17,7 @@ use crate::text::{FontMetrics, ScriptMetrics, TextSize};
/// ```example /// ```example
/// Revenue#sub[yearly] /// Revenue#sub[yearly]
/// ``` /// ```
#[elem(title = "Subscript")] #[elem(title = "Subscript", Show)]
pub struct SubElem { pub struct SubElem {
/// Whether to create artificial subscripts by lowering and scaling down /// Whether to create artificial subscripts by lowering and scaling down
/// regular glyphs. /// regular glyphs.
@ -59,6 +64,29 @@ pub struct SubElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<SubElem> {
#[typst_macros::time(name = "sub", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::sub)
.with_body(Some(body))
.pack()
.spanned(self.span()));
}
show_script(
styles,
body,
self.typographic.get(styles),
self.baseline.get(styles),
self.size.get(styles),
ScriptKind::Sub,
)
}
}
/// Renders text in superscript. /// Renders text in superscript.
/// ///
/// The text is rendered smaller and its baseline is raised. /// The text is rendered smaller and its baseline is raised.
@ -67,7 +95,7 @@ pub struct SubElem {
/// ```example /// ```example
/// 1#super[st] try! /// 1#super[st] try!
/// ``` /// ```
#[elem(title = "Superscript")] #[elem(title = "Superscript", Show)]
pub struct SuperElem { pub struct SuperElem {
/// Whether to create artificial superscripts by raising and scaling down /// Whether to create artificial superscripts by raising and scaling down
/// regular glyphs. /// regular glyphs.
@ -118,6 +146,49 @@ pub struct SuperElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<SuperElem> {
#[typst_macros::time(name = "super", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
if styles.get(TargetElem::target).is_html() {
return Ok(HtmlElem::new(tag::sup)
.with_body(Some(body))
.pack()
.spanned(self.span()));
}
show_script(
styles,
body,
self.typographic.get(styles),
self.baseline.get(styles),
self.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,
}),
))
}
/// Configuration values for sub- or superscript text. /// Configuration values for sub- or superscript text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ShiftSettings { pub struct ShiftSettings {

View File

@ -1,4 +1,7 @@
use crate::foundations::{elem, Content}; use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
use crate::text::TextElem;
/// Displays text in small capitals. /// Displays text in small capitals.
/// ///
@ -40,7 +43,7 @@ use crate::foundations::{elem, Content};
/// = Introduction /// = Introduction
/// #lorem(40) /// #lorem(40)
/// ``` /// ```
#[elem(title = "Small Capitals")] #[elem(title = "Small Capitals", Show)]
pub struct SmallcapsElem { pub struct SmallcapsElem {
/// Whether to turn uppercase letters into small capitals as well. /// Whether to turn uppercase letters into small capitals as well.
/// ///
@ -58,6 +61,15 @@ pub struct SmallcapsElem {
pub body: Content, pub body: Content,
} }
impl Show for Packed<SmallcapsElem> {
#[typst_macros::time(name = "smallcaps", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let sc =
if self.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules };
Ok(self.body.clone().set(TextElem::smallcaps, Some(sc)))
}
}
/// What becomes small capitals. /// What becomes small capitals.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Smallcaps { pub enum Smallcaps {

View File

@ -5,10 +5,9 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{ use crate::foundations::{
array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str,
StyleChain,
}; };
use crate::layout::Dir; use crate::layout::Dir;
use crate::text::{Lang, Region, TextElem}; use crate::text::{Lang, Region};
/// A language-aware quote that reacts to its context. /// A language-aware quote that reacts to its context.
/// ///
@ -201,16 +200,6 @@ pub struct SmartQuotes<'s> {
} }
impl<'s> SmartQuotes<'s> { impl<'s> SmartQuotes<'s> {
/// Retrieve the smart quotes as configured by the current styles.
pub fn get_in(styles: StyleChain<'s>) -> Self {
Self::get(
styles.get_ref(SmartQuoteElem::quotes),
styles.get(TextElem::lang),
styles.get(TextElem::region),
styles.get(SmartQuoteElem::alternative),
)
}
/// Create a new `Quotes` struct with the given quotes, optionally falling /// Create a new `Quotes` struct with the given quotes, optionally falling
/// back to the defaults for a language and region. /// back to the defaults for a language and region.
/// ///

View File

@ -2,9 +2,12 @@ use kurbo::ParamCurveExtrema;
use typst_macros::{scope, Cast}; use typst_macros::{scope, Cast};
use typst_utils::Numeric; use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, HintedString}; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
use crate::foundations::{cast, elem, Content, Packed, Smart}; use crate::engine::Engine;
use crate::layout::{Abs, Axes, Length, Point, Rel, Size}; use crate::foundations::{
cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
use super::FixedStroke; use super::FixedStroke;
@ -39,7 +42,7 @@ use super::FixedStroke;
/// curve.close(), /// curve.close(),
/// ) /// )
/// ``` /// ```
#[elem(scope)] #[elem(scope, Show)]
pub struct CurveElem { pub struct CurveElem {
/// How to fill the curve. /// How to fill the curve.
/// ///
@ -92,6 +95,14 @@ pub struct CurveElem {
pub components: Vec<CurveComponent>, pub components: Vec<CurveComponent>,
} }
impl Show for Packed<CurveElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_curve)
.pack()
.spanned(self.span()))
}
}
#[scope] #[scope]
impl CurveElem { impl CurveElem {
#[elem] #[elem]

View File

@ -15,11 +15,13 @@ use ecow::EcoString;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash; use typst_utils::LazyHash;
use crate::diag::StrResult; use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show,
Smart, StyleChain,
}; };
use crate::layout::{Length, Rel, Sizing}; use crate::layout::{BlockElem, Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable; use crate::model::Figurable;
use crate::text::LocalName; use crate::text::LocalName;
@ -42,7 +44,7 @@ use crate::text::LocalName;
/// ], /// ],
/// ) /// )
/// ``` /// ```
#[elem(scope, LocalName, Figurable)] #[elem(scope, Show, LocalName, Figurable)]
pub struct ImageElem { pub struct ImageElem {
/// A [path]($syntax/#paths) to an image file or raw bytes making up an /// A [path]($syntax/#paths) to an image file or raw bytes making up an
/// image in one of the supported [formats]($image.format). /// image in one of the supported [formats]($image.format).
@ -217,6 +219,16 @@ impl ImageElem {
} }
} }
impl Show for Packed<ImageElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image)
.with_width(self.width.get(styles))
.with_height(self.height.get(styles))
.pack()
.spanned(self.span()))
}
}
impl LocalName for Packed<ImageElem> { impl LocalName for Packed<ImageElem> {
const KEY: &'static str = "figure"; const KEY: &'static str = "figure";
} }

View File

@ -1,5 +1,7 @@
use crate::foundations::elem; use crate::diag::SourceResult;
use crate::layout::{Abs, Angle, Axes, Length, Rel}; use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::layout::{Abs, Angle, Axes, BlockElem, Length, Rel};
use crate::visualize::Stroke; use crate::visualize::Stroke;
/// A line from one point to another. /// A line from one point to another.
@ -15,7 +17,7 @@ use crate::visualize::Stroke;
/// stroke: 2pt + maroon, /// stroke: 2pt + maroon,
/// ) /// )
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct LineElem { pub struct LineElem {
/// The start point of the line. /// The start point of the line.
/// ///
@ -48,3 +50,11 @@ pub struct LineElem {
#[fold] #[fold]
pub stroke: Stroke, pub stroke: Stroke,
} }
impl Show for Packed<LineElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_line)
.pack()
.spanned(self.span()))
}
}

View File

@ -1,7 +1,11 @@
use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
use crate::diag::bail; use crate::diag::{bail, SourceResult};
use crate::foundations::{array, cast, elem, Array, Reflect, Smart}; use crate::engine::Engine;
use crate::layout::{Axes, Length, Rel}; use crate::foundations::{
array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart,
StyleChain,
};
use crate::layout::{Axes, BlockElem, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A path through a list of points, connected by Bézier curves. /// A path through a list of points, connected by Bézier curves.
@ -17,7 +21,7 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// ((50%, 0pt), (40pt, 0pt)), /// ((50%, 0pt), (40pt, 0pt)),
/// ) /// )
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct PathElem { pub struct PathElem {
/// How to fill the path. /// How to fill the path.
/// ///
@ -79,6 +83,14 @@ pub struct PathElem {
pub vertices: Vec<PathVertex>, pub vertices: Vec<PathVertex>,
} }
impl Show for Packed<PathElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_path)
.pack()
.spanned(self.span()))
}
}
/// A component used for path creation. /// A component used for path creation.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PathVertex { pub enum PathVertex {

View File

@ -2,8 +2,12 @@ use std::f64::consts::PI;
use typst_syntax::Span; use typst_syntax::Span;
use crate::foundations::{elem, func, scope, Content, NativeElement, Smart}; use crate::diag::SourceResult;
use crate::layout::{Axes, Em, Length, Rel}; use crate::engine::Engine;
use crate::foundations::{
elem, func, scope, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{Axes, BlockElem, Em, Length, Rel};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A closed polygon. /// A closed polygon.
@ -21,7 +25,7 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// (0%, 2cm), /// (0%, 2cm),
/// ) /// )
/// ``` /// ```
#[elem(scope)] #[elem(scope, Show)]
pub struct PolygonElem { pub struct PolygonElem {
/// How to fill the polygon. /// How to fill the polygon.
/// ///
@ -120,3 +124,11 @@ impl PolygonElem {
elem.pack().spanned(span) elem.pack().spanned(span)
} }
} }
impl Show for Packed<PolygonElem> {
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_polygon)
.pack()
.spanned(self.span()))
}
}

View File

@ -1,5 +1,9 @@
use crate::foundations::{elem, Cast, Content, Smart}; use crate::diag::SourceResult;
use crate::layout::{Abs, Corners, Length, Point, Rel, Sides, Size, Sizing}; use crate::engine::Engine;
use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing};
use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
/// A rectangle with optional content. /// A rectangle with optional content.
@ -15,7 +19,7 @@ use crate::visualize::{Curve, FixedStroke, Paint, Stroke};
/// to fit the content. /// to fit the content.
/// ] /// ]
/// ``` /// ```
#[elem(title = "Rectangle")] #[elem(title = "Rectangle", Show)]
pub struct RectElem { pub struct RectElem {
/// The rectangle's width, relative to its parent container. /// The rectangle's width, relative to its parent container.
pub width: Smart<Rel<Length>>, pub width: Smart<Rel<Length>>,
@ -118,6 +122,16 @@ pub struct RectElem {
pub body: Option<Content>, pub body: Option<Content>,
} }
impl Show for Packed<RectElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect)
.with_width(self.width.get(styles))
.with_height(self.height.get(styles))
.pack()
.spanned(self.span()))
}
}
/// A square with optional content. /// A square with optional content.
/// ///
/// # Example /// # Example
@ -131,7 +145,7 @@ pub struct RectElem {
/// sized to fit. /// sized to fit.
/// ] /// ]
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct SquareElem { pub struct SquareElem {
/// The square's side length. This is mutually exclusive with `width` and /// The square's side length. This is mutually exclusive with `width` and
/// `height`. /// `height`.
@ -195,6 +209,16 @@ pub struct SquareElem {
pub body: Option<Content>, pub body: Option<Content>,
} }
impl Show for Packed<SquareElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square)
.with_width(self.width.get(styles))
.with_height(self.height.get(styles))
.pack()
.spanned(self.span()))
}
}
/// An ellipse with optional content. /// An ellipse with optional content.
/// ///
/// # Example /// # Example
@ -209,7 +233,7 @@ pub struct SquareElem {
/// to fit the content. /// to fit the content.
/// ] /// ]
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct EllipseElem { pub struct EllipseElem {
/// The ellipse's width, relative to its parent container. /// The ellipse's width, relative to its parent container.
pub width: Smart<Rel<Length>>, pub width: Smart<Rel<Length>>,
@ -245,6 +269,16 @@ pub struct EllipseElem {
pub body: Option<Content>, pub body: Option<Content>,
} }
impl Show for Packed<EllipseElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse)
.with_width(self.width.get(styles))
.with_height(self.height.get(styles))
.pack()
.spanned(self.span()))
}
}
/// A circle with optional content. /// A circle with optional content.
/// ///
/// # Example /// # Example
@ -259,7 +293,7 @@ pub struct EllipseElem {
/// sized to fit. /// sized to fit.
/// ] /// ]
/// ``` /// ```
#[elem] #[elem(Show)]
pub struct CircleElem { pub struct CircleElem {
/// The circle's radius. This is mutually exclusive with `width` and /// The circle's radius. This is mutually exclusive with `width` and
/// `height`. /// `height`.
@ -320,6 +354,16 @@ pub struct CircleElem {
pub body: Option<Content>, pub body: Option<Content>,
} }
impl Show for Packed<CircleElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle)
.with_width(self.width.get(styles))
.with_height(self.height.get(styles))
.pack()
.spanned(self.span()))
}
}
/// A geometric shape with optional fill and stroke. /// A geometric shape with optional fill and stroke.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Shape { pub struct Shape {

View File

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

View File

@ -14,9 +14,9 @@ use ecow::EcoString;
use typst_library::diag::{bail, At, SourceResult}; use typst_library::diag::{bail, At, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{ use typst_library::foundations::{
Content, Context, ContextElem, Element, NativeElement, NativeShowRule, Recipe, Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector,
RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles, SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem,
SymbolElem, Synthesize, TargetElem, Transformation, Synthesize, Transformation,
}; };
use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::html::{tag, FrameElem, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
@ -160,7 +160,7 @@ enum ShowStep<'a> {
/// A user-defined transformational show rule. /// A user-defined transformational show rule.
Recipe(&'a Recipe, RecipeIndex), Recipe(&'a Recipe, RecipeIndex),
/// The built-in show rule. /// The built-in show rule.
Builtin(NativeShowRule), Builtin,
} }
/// A match of a regex show rule. /// A match of a regex show rule.
@ -382,7 +382,9 @@ fn visit_show_rules<'a>(
} }
// Apply a built-in show rule. // Apply a built-in show rule.
ShowStep::Builtin(rule) => rule.apply(&output, s.engine, chained), ShowStep::Builtin => {
output.with::<dyn Show>().unwrap().show(s.engine, chained)
}
}; };
// Errors in show rules don't terminate compilation immediately. We just // Errors in show rules don't terminate compilation immediately. We just
@ -424,14 +426,14 @@ fn visit_show_rules<'a>(
Ok(true) Ok(true)
} }
/// Inspects an element and the current styles and determines how to proceed /// Inspects a target element and the current styles and determines how to
/// with the styling. /// proceed with the styling.
fn verdict<'a>( fn verdict<'a>(
engine: &mut Engine, engine: &mut Engine,
elem: &'a Content, target: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> Option<Verdict<'a>> { ) -> Option<Verdict<'a>> {
let prepared = elem.is_prepared(); let prepared = target.is_prepared();
let mut map = Styles::new(); let mut map = Styles::new();
let mut step = None; let mut step = None;
@ -439,20 +441,20 @@ fn verdict<'a>(
// fields before real synthesis runs (during preparation). It's really // fields before real synthesis runs (during preparation). It's really
// unfortunate that we have to do this, but otherwise // unfortunate that we have to do this, but otherwise
// `show figure.where(kind: table)` won't work :( // `show figure.where(kind: table)` won't work :(
let mut elem = elem; let mut target = target;
let mut slot; let mut slot;
if !prepared && elem.can::<dyn Synthesize>() { if !prepared && target.can::<dyn Synthesize>() {
slot = elem.clone(); slot = target.clone();
slot.with_mut::<dyn Synthesize>() slot.with_mut::<dyn Synthesize>()
.unwrap() .unwrap()
.synthesize(engine, styles) .synthesize(engine, styles)
.ok(); .ok();
elem = &slot; target = &slot;
} }
// Lazily computes the total number of recipes in the style chain. We need // Lazily computes the total number of recipes in the style chain. We need
// it to determine whether a particular show rule was already applied to the // it to determine whether a particular show rule was already applied to the
// `elem` previously. For this purpose, show rules are indexed from the // `target` previously. For this purpose, show rules are indexed from the
// top of the chain as the chain might grow to the bottom. // top of the chain as the chain might grow to the bottom.
let depth = LazyCell::new(|| styles.recipes().count()); let depth = LazyCell::new(|| styles.recipes().count());
@ -460,7 +462,7 @@ fn verdict<'a>(
// We're not interested in recipes that don't match. // We're not interested in recipes that don't match.
if !recipe if !recipe
.selector() .selector()
.is_some_and(|selector| selector.matches(elem, Some(styles))) .is_some_and(|selector| selector.matches(target, Some(styles)))
{ {
continue; continue;
} }
@ -478,9 +480,9 @@ fn verdict<'a>(
continue; continue;
} }
// Check whether this show rule was already applied to the element. // Check whether this show rule was already applied to the target.
let index = RecipeIndex(*depth - r); let index = RecipeIndex(*depth - r);
if elem.is_guarded(index) { if target.is_guarded(index) {
continue; continue;
} }
@ -496,22 +498,19 @@ fn verdict<'a>(
} }
// If we found no user-defined rule, also consider the built-in show rule. // If we found no user-defined rule, also consider the built-in show rule.
if step.is_none() { if step.is_none() && target.can::<dyn Show>() {
let target = styles.get(TargetElem::target); step = Some(ShowStep::Builtin);
if let Some(rule) = engine.routines.rules.get(target, elem) {
step = Some(ShowStep::Builtin(rule));
}
} }
// If there's no nothing to do, there is also no verdict. // If there's no nothing to do, there is also no verdict.
if step.is_none() if step.is_none()
&& map.is_empty() && map.is_empty()
&& (prepared || { && (prepared || {
elem.label().is_none() target.label().is_none()
&& elem.location().is_none() && target.location().is_none()
&& !elem.can::<dyn ShowSet>() && !target.can::<dyn ShowSet>()
&& !elem.can::<dyn Locatable>() && !target.can::<dyn Locatable>()
&& !elem.can::<dyn Synthesize>() && !target.can::<dyn Synthesize>()
}) })
{ {
return None; return None;
@ -524,7 +523,7 @@ fn verdict<'a>(
fn prepare( fn prepare(
engine: &mut Engine, engine: &mut Engine,
locator: &mut SplitLocator, locator: &mut SplitLocator,
elem: &mut Content, target: &mut Content,
map: &mut Styles, map: &mut Styles,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Option<(Tag, Tag)>> { ) -> SourceResult<Option<(Tag, Tag)>> {
@ -534,43 +533,43 @@ fn prepare(
// //
// The element could already have a location even if it is not prepared // The element could already have a location even if it is not prepared
// when it stems from a query. // when it stems from a query.
let key = typst_utils::hash128(&elem); let key = typst_utils::hash128(&target);
if elem.location().is_none() if target.location().is_none()
&& (elem.can::<dyn Locatable>() || elem.label().is_some()) && (target.can::<dyn Locatable>() || target.label().is_some())
{ {
let loc = locator.next_location(engine.introspector, key); let loc = locator.next_location(engine.introspector, key);
elem.set_location(loc); target.set_location(loc);
} }
// Apply built-in show-set rules. User-defined show-set rules are already // Apply built-in show-set rules. User-defined show-set rules are already
// considered in the map built while determining the verdict. // considered in the map built while determining the verdict.
if let Some(show_settable) = elem.with::<dyn ShowSet>() { if let Some(show_settable) = target.with::<dyn ShowSet>() {
map.apply(show_settable.show_set(styles)); map.apply(show_settable.show_set(styles));
} }
// If necessary, generated "synthesized" fields (which are derived from // If necessary, generated "synthesized" fields (which are derived from
// other fields or queries). Do this after show-set so that show-set styles // other fields or queries). Do this after show-set so that show-set styles
// are respected. // are respected.
if let Some(synthesizable) = elem.with_mut::<dyn Synthesize>() { if let Some(synthesizable) = target.with_mut::<dyn Synthesize>() {
synthesizable.synthesize(engine, styles.chain(map))?; synthesizable.synthesize(engine, styles.chain(map))?;
} }
// Copy style chain fields into the element itself, so that they are // Copy style chain fields into the element itself, so that they are
// available in rules. // available in rules.
elem.materialize(styles.chain(map)); target.materialize(styles.chain(map));
// If the element is locatable, create start and end tags to be able to find // If the element is locatable, create start and end tags to be able to find
// the element in the frames after layout. Do this after synthesis and // the element in the frames after layout. Do this after synthesis and
// materialization, so that it includes the synthesized fields. Do it before // materialization, so that it includes the synthesized fields. Do it before
// marking as prepared so that show-set rules will apply to this element // marking as prepared so that show-set rules will apply to this element
// when queried. // when queried.
let tags = elem let tags = target
.location() .location()
.map(|loc| (Tag::Start(elem.clone()), Tag::End(loc, key))); .map(|loc| (Tag::Start(target.clone()), Tag::End(loc, key)));
// Ensure that this preparation only runs once by marking the element as // Ensure that this preparation only runs once by marking the element as
// prepared. // prepared.
elem.mark_prepared(); target.mark_prepared();
Ok(tags) Ok(tags)
} }

View File

@ -202,7 +202,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group
mask.intersect_path( mask.intersect_path(
&path, &path,
sk::FillRule::default(), sk::FillRule::default(),
true, false,
sk::Transform::default(), sk::Transform::default(),
); );
storage = mask; storage = mask;
@ -218,7 +218,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group
mask.fill_path( mask.fill_path(
&path, &path,
sk::FillRule::default(), sk::FillRule::default(),
true, false,
sk::Transform::default(), sk::Transform::default(),
); );
storage = mask; storage = mask;

View File

@ -39,7 +39,6 @@ pub use typst_syntax as syntax;
pub use typst_utils as utils; pub use typst_utils as utils;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::LazyLock;
use comemo::{Track, Tracked, Validate}; use comemo::{Track, Tracked, Validate};
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
@ -47,7 +46,7 @@ use typst_library::diag::{
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned, bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
}; };
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value}; use typst_library::foundations::{StyleChain, Styles, Value};
use typst_library::html::HtmlDocument; use typst_library::html::HtmlDocument;
use typst_library::introspection::Introspector; use typst_library::introspection::Introspector;
use typst_library::layout::PagedDocument; use typst_library::layout::PagedDocument;
@ -323,39 +322,37 @@ mod sealed {
} }
} }
/// Provides ways to construct a [`Library`].
pub trait LibraryExt {
/// Creates the default library.
fn default() -> Library;
/// Creates a builder for configuring a library.
fn builder() -> LibraryBuilder;
}
impl LibraryExt for Library {
fn default() -> Library {
Self::builder().build()
}
fn builder() -> LibraryBuilder {
LibraryBuilder::from_routines(&ROUTINES)
}
}
/// Defines implementation of various Typst compiler routines as a table of /// Defines implementation of various Typst compiler routines as a table of
/// function pointers. /// function pointers.
/// ///
/// This is essentially dynamic linking and done to allow for crate splitting. /// This is essentially dynamic linking and done to allow for crate splitting.
pub static ROUTINES: LazyLock<Routines> = LazyLock::new(|| Routines { pub static ROUTINES: Routines = Routines {
rules: {
let mut rules = NativeRuleMap::new();
typst_layout::register(&mut rules);
typst_html::register(&mut rules);
rules
},
eval_string: typst_eval::eval_string, eval_string: typst_eval::eval_string,
eval_closure: typst_eval::eval_closure, eval_closure: typst_eval::eval_closure,
realize: typst_realize::realize, realize: typst_realize::realize,
layout_fragment: typst_layout::layout_fragment,
layout_frame: typst_layout::layout_frame, layout_frame: typst_layout::layout_frame,
html_module: typst_html::module, layout_list: typst_layout::layout_list,
}); layout_enum: typst_layout::layout_enum,
layout_grid: typst_layout::layout_grid,
layout_table: typst_layout::layout_table,
layout_stack: typst_layout::layout_stack,
layout_columns: typst_layout::layout_columns,
layout_move: typst_layout::layout_move,
layout_rotate: typst_layout::layout_rotate,
layout_scale: typst_layout::layout_scale,
layout_skew: typst_layout::layout_skew,
layout_repeat: typst_layout::layout_repeat,
layout_pad: typst_layout::layout_pad,
layout_line: typst_layout::layout_line,
layout_curve: typst_layout::layout_curve,
layout_path: typst_layout::layout_path,
layout_polygon: typst_layout::layout_polygon,
layout_rect: typst_layout::layout_rect,
layout_square: typst_layout::layout_square,
layout_ellipse: typst_layout::layout_ellipse,
layout_circle: typst_layout::layout_circle,
layout_image: typst_layout::layout_image,
layout_equation_block: typst_layout::layout_equation_block,
layout_equation_inline: typst_layout::layout_equation_inline,
};

View File

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

View File

@ -24,7 +24,7 @@ use typst::foundations::{
use typst::layout::{Abs, Margin, PageElem, PagedDocument}; use typst::layout::{Abs, Margin, PageElem, PagedDocument};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use typst::{Category, Feature, Library, LibraryExt}; use typst::{Category, Feature, Library, LibraryBuilder};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
macro_rules! load { macro_rules! load {
@ -51,7 +51,7 @@ static GROUPS: LazyLock<Vec<GroupData>> = LazyLock::new(|| {
}); });
static LIBRARY: LazyLock<LazyHash<Library>> = LazyLock::new(|| { static LIBRARY: LazyLock<LazyHash<Library>> = LazyLock::new(|| {
let mut lib = Library::builder() let mut lib = LibraryBuilder::default()
.with_features([Feature::Html].into_iter().collect()) .with_features([Feature::Html].into_iter().collect())
.build(); .build();
let scope = lib.global.scope_mut(); let scope = lib.global.scope_mut();

View File

@ -7,7 +7,7 @@ use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source}; use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World}; use typst::{Library, World};
struct FuzzWorld { struct FuzzWorld {
library: LazyHash<Library>, library: LazyHash<Library>,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 965 B

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 834 B

After

Width:  |  Height:  |  Size: 828 B

View File

@ -19,7 +19,7 @@ use typst::syntax::{FileId, Source, Span};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{singleton, LazyHash};
use typst::visualize::Color; use typst::visualize::Color;
use typst::{Feature, Library, LibraryExt, World}; use typst::{Feature, Library, World};
use typst_syntax::Lines; use typst_syntax::Lines;
/// A world that provides access to the tests environment. /// A world that provides access to the tests environment.

View File

@ -103,10 +103,6 @@
#test("Hello".last(), "o") #test("Hello".last(), "o")
#test("🏳🌈A🏳".first(), "🏳️‍🌈") #test("🏳🌈A🏳".first(), "🏳️‍🌈")
#test("🏳🌈A🏳".last(), "🏳️‍⚧️") #test("🏳🌈A🏳".last(), "🏳️‍⚧️")
#test("hey".first(default: "d"), "h")
#test("".first(default: "d"), "d")
#test("hey".last(default: "d"), "y")
#test("".last(default: "d"), "d")
--- string-first-empty --- --- string-first-empty ---
// Error: 2-12 string is empty // Error: 2-12 string is empty

View File

@ -325,10 +325,3 @@ b
a a
#block(height: -25pt)[b] #block(height: -25pt)[b]
c c
--- issue-6267-clip-anti-alias ---
#block(
clip: true,
radius: 100%,
rect(fill: gray, height: 1cm, width: 1cm),
)

View File

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

View File

@ -687,11 +687,6 @@ a b c --------------------
#let hi = "你好world" #let hi = "你好world"
``` ```
--- issue-6559-equality-between-raws ---
#test(`foo`, `foo`)
#assert.ne(`foo`, `bar`)
--- raw-theme-set-to-auto --- --- raw-theme-set-to-auto ---
```typ ```typ
#let hi = "Hello World" #let hi = "Hello World"