Compare commits

...

9 Commits

62 changed files with 1379 additions and 465 deletions

19
Cargo.lock generated
View File

@ -748,9 +748,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"libz-rs-sys",
@ -1469,9 +1469,9 @@ dependencies = [
[[package]]
name = "libz-rs-sys"
version = "0.4.2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d"
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
dependencies = [
"zlib-rs",
]
@ -2911,7 +2911,7 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
source = "git+https://github.com/typst/typst-dev-assets?rev=64f8c71#64f8c7108db88323a9d3476e9750562de753f24e"
[[package]]
name = "typst-docs"
@ -3932,13 +3932,12 @@ dependencies = [
[[package]]
name = "zip"
version = "2.5.0"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"flate2",
"indexmap 2.7.1",
"memchr",
@ -3947,9 +3946,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
version = "0.4.2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
[[package]]
name = "zopfli"

View File

@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" }
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 = "64f8c71" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -143,7 +143,7 @@ xmlparser = "0.13.5"
xmlwriter = "0.1.0"
xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1"
zip = { version = "2.5", default-features = false, features = ["deflate"] }
zip = { version = "4.3", default-features = false, features = ["deflate"] }
[profile.dev.package."*"]
opt-level = 2

View File

@ -251,7 +251,7 @@ impl Eval for ast::EnumItem<'_> {
let body = self.body().eval(vm)?;
let mut elem = EnumItem::new(body);
if let Some(number) = self.number() {
elem.number.set(Some(number));
elem.number.set(Smart::Custom(number));
}
Ok(elem.pack())
}

View File

@ -1,10 +1,13 @@
use std::collections::HashSet;
use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut};
use typst_library::diag::{bail, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator};
use typst_library::introspection::{
Introspector, IntrospectorBuilder, Location, Locator,
};
use typst_library::layout::{Point, Position, Transform};
use typst_library::model::DocumentInfo;
use typst_library::routines::{Arenas, RealizationKind, Routines};
@ -83,42 +86,56 @@ fn html_document_impl(
&mut locator,
children.iter().copied(),
)?;
let introspector = introspect_html(&output);
let root = root_element(output, &info)?;
let mut link_targets = HashSet::new();
let mut introspector = introspect_html(&output, &mut link_targets);
let mut root = root_element(output, &info)?;
crate::link::identify_link_targets(&mut root, &mut introspector, link_targets);
Ok(HtmlDocument { info, root, introspector })
}
/// Introspects HTML nodes.
#[typst_macros::time(name = "introspect html")]
fn introspect_html(output: &[HtmlNode]) -> Introspector {
fn introspect_html(
output: &[HtmlNode],
link_targets: &mut HashSet<Location>,
) -> Introspector {
fn discover(
builder: &mut IntrospectorBuilder,
sink: &mut Vec<(Content, Position)>,
link_targets: &mut HashSet<Location>,
nodes: &[HtmlNode],
) {
for node in nodes {
match node {
HtmlNode::Tag(tag) => builder.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
HtmlNode::Tag(tag) => {
builder.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
);
}
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => discover(builder, sink, &elem.children),
HtmlNode::Frame(frame) => builder.discover_in_frame(
sink,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
),
HtmlNode::Element(elem) => {
discover(builder, sink, link_targets, &elem.children)
}
HtmlNode::Frame(frame) => {
builder.discover_in_frame(
sink,
&frame.inner,
NonZeroUsize::ONE,
Transform::identity(),
);
crate::link::introspect_frame_links(&frame.inner, link_targets);
}
}
}
}
let mut elems = Vec::new();
let mut builder = IntrospectorBuilder::new();
discover(&mut builder, &mut elems, output);
discover(&mut builder, &mut elems, link_targets, output);
builder.finalize(elems)
}

View File

@ -4,7 +4,7 @@ use ecow::{EcoString, EcoVec};
use typst_library::diag::{bail, HintedStrResult, StrResult};
use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain};
use typst_library::introspection::{Introspector, Tag};
use typst_library::layout::{Abs, Frame};
use typst_library::layout::{Abs, Frame, Point};
use typst_library::model::DocumentInfo;
use typst_library::text::TextElem;
use typst_syntax::Span;
@ -172,10 +172,20 @@ impl HtmlAttrs {
Self::default()
}
/// Add an attribute.
/// Adds an attribute.
pub fn push(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.push((attr, value.into()));
}
/// Adds an attribute to the start of the list.
pub fn push_front(&mut self, attr: HtmlAttr, value: impl Into<EcoString>) {
self.0.insert(0, (attr, value.into()));
}
/// Finds an attribute value.
pub fn get(&self, attr: HtmlAttr) -> Option<&EcoString> {
self.0.iter().find(|&&(k, _)| k == attr).map(|(_, v)| v)
}
}
cast! {
@ -279,11 +289,20 @@ pub struct HtmlFrame {
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
/// An ID to assign to the SVG itself.
pub id: Option<EcoString>,
/// IDs to assign to destination jump points within the SVG.
pub link_points: Vec<(Point, EcoString)>,
}
impl HtmlFrame {
/// Wraps a laid-out frame.
pub fn new(inner: Frame, styles: StyleChain) -> Self {
Self { inner, text_size: styles.resolve(TextElem::size) }
Self {
inner,
text_size: styles.resolve(TextElem::size),
id: None,
link_points: vec![],
}
}
}

View File

@ -2,6 +2,7 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::introspection::Introspector;
use typst_syntax::Span;
use crate::{
@ -10,7 +11,7 @@ use crate::{
/// Encodes an HTML document into a string.
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
let mut w = Writer { pretty: true, ..Writer::default() };
let mut w = Writer::new(&document.introspector, true);
w.buf.push_str("<!DOCTYPE html>");
write_indent(&mut w);
write_element(&mut w, &document.root)?;
@ -20,16 +21,25 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
Ok(w.buf)
}
#[derive(Default)]
struct Writer {
/// Encodes HTML.
struct Writer<'a> {
/// The output buffer.
buf: String,
/// The current indentation level
level: usize,
/// The document's introspector.
introspector: &'a Introspector,
/// Whether pretty printing is enabled.
pretty: bool,
}
impl<'a> Writer<'a> {
/// Creates a new writer.
fn new(introspector: &'a Introspector, pretty: bool) -> Self {
Self { buf: String::new(), level: 0, introspector, pretty }
}
}
/// Writes a newline and indent, if pretty printing is enabled.
fn write_indent(w: &mut Writer) {
if w.pretty {
@ -306,6 +316,12 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
/// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
let svg = typst_svg::svg_html_frame(&frame.inner, frame.text_size);
let svg = typst_svg::svg_html_frame(
&frame.inner,
frame.text_size,
frame.id.as_deref(),
&frame.link_points,
w.introspector,
);
w.buf.push_str(&svg);
}

View File

@ -8,6 +8,7 @@ mod document;
mod dom;
mod encode;
mod fragment;
mod link;
mod rules;
mod tag;
mod typed;
@ -79,6 +80,19 @@ impl HtmlElem {
self
}
/// Adds the attribute to the element if value is not `None`.
pub fn with_optional_attr(
self,
attr: HtmlAttr,
value: Option<impl Into<EcoString>>,
) -> Self {
if let Some(value) = value {
self.with_attr(attr, value)
} else {
self
}
}
/// Adds CSS styles to an element.
fn with_styles(self, properties: css::Properties) -> Self {
if let Some(value) = properties.into_inline_styles() {

View File

@ -0,0 +1,290 @@
use std::collections::{HashMap, HashSet, VecDeque};
use comemo::Track;
use ecow::{eco_format, EcoString};
use typst_library::foundations::{Label, NativeElement};
use typst_library::introspection::{Introspector, Location, Tag};
use typst_library::layout::{Frame, FrameItem, Point};
use typst_library::model::{Destination, LinkElem};
use typst_utils::PicoStr;
use crate::{attr, tag, HtmlElement, HtmlNode};
/// Searches for links within a frame.
///
/// If all links are created via `LinkElem` in the future, this can be removed
/// in favor of the query in `identify_link_targets`. For the time being, some
/// links are created without existence of a `LinkElem`, so this is
/// unfortunately necessary.
pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet<Location>) {
for (_, item) in frame.items() {
match item {
FrameItem::Link(Destination::Location(loc), _) => {
targets.insert(*loc);
}
FrameItem::Group(group) => introspect_frame_links(&group.frame, targets),
_ => {}
}
}
}
/// Attaches IDs to nodes produced by link targets to make them linkable.
///
/// May produce `<span>`s for link targets that turned into text nodes or no
/// nodes at all. See the [`LinkElem`] documentation for more details.
pub fn identify_link_targets(
root: &mut HtmlElement,
introspector: &mut Introspector,
mut targets: HashSet<Location>,
) {
// Query for all links with an intra-doc (i.e. `Location`) destination to
// know what needs IDs.
targets.extend(
introspector
.query(&LinkElem::ELEM.select())
.iter()
.map(|elem| elem.to_packed::<LinkElem>().unwrap())
.filter_map(|elem| match elem.dest.resolve(introspector.track()) {
Ok(Destination::Location(loc)) => Some(loc),
_ => None,
}),
);
if targets.is_empty() {
// Nothing to do.
return;
}
// Assign IDs to all link targets.
let mut work = Work::new();
traverse(
&mut work,
&targets,
&mut Identificator::new(introspector),
&mut root.children,
);
// Add the mapping from locations to IDs to the introspector to make it
// available to links in the next iteration.
introspector.set_html_ids(work.ids);
}
/// Traverses a list of nodes.
fn traverse(
work: &mut Work,
targets: &HashSet<Location>,
identificator: &mut Identificator<'_>,
nodes: &mut Vec<HtmlNode>,
) {
let mut i = 0;
while i < nodes.len() {
let node = &mut nodes[i];
match node {
// When visiting a start tag, we check whether the element needs an
// ID and if so, add it to the queue, so that its first child node
// receives an ID.
HtmlNode::Tag(Tag::Start(elem)) => {
let loc = elem.location().unwrap();
if targets.contains(&loc) {
work.enqueue(loc, elem.label());
}
}
// When we reach an end tag, we check whether it closes an element
// that is still in our queue. If so, that means the element
// produced no nodes and we need to insert an empty span.
HtmlNode::Tag(Tag::End(loc, _)) => {
work.remove(*loc, |label| {
let mut element = HtmlElement::new(tag::span);
let id = identificator.assign(&mut element, label);
nodes.insert(i + 1, HtmlNode::Element(element));
id
});
}
// When visiting an element and the queue is non-empty, we assign an
// ID. Then, we traverse its children.
HtmlNode::Element(element) => {
work.drain(|label| identificator.assign(element, label));
traverse(work, targets, identificator, &mut element.children);
}
// When visiting text and the queue is non-empty, we generate a span
// and assign an ID.
HtmlNode::Text(..) => {
work.drain(|label| {
let mut element =
HtmlElement::new(tag::span).with_children(vec![node.clone()]);
let id = identificator.assign(&mut element, label);
*node = HtmlNode::Element(element);
id
});
}
// When visiting a frame and the queue is non-empty, we assign an
// ID to it (will be added to the resulting SVG element).
HtmlNode::Frame(frame) => {
work.drain(|label| {
frame.id.get_or_insert_with(|| identificator.identify(label)).clone()
});
traverse_frame(
work,
targets,
identificator,
&frame.inner,
&mut frame.link_points,
);
}
}
i += 1;
}
}
/// Traverses a frame embedded in HTML.
fn traverse_frame(
work: &mut Work,
targets: &HashSet<Location>,
identificator: &mut Identificator<'_>,
frame: &Frame,
link_points: &mut Vec<(Point, EcoString)>,
) {
for (_, item) in frame.items() {
match item {
FrameItem::Tag(Tag::Start(elem)) => {
let loc = elem.location().unwrap();
if targets.contains(&loc) {
let pos = identificator.introspector.position(loc).point;
let id = identificator.identify(elem.label());
work.ids.insert(loc, id.clone());
link_points.push((pos, id));
}
}
FrameItem::Group(group) => {
traverse_frame(work, targets, identificator, &group.frame, link_points);
}
_ => {}
}
}
}
/// Keeps track of the work to be done during ID generation.
struct Work {
/// The locations and labels of elements we need to assign an ID to right
/// now.
queue: VecDeque<(Location, Option<Label>)>,
/// The resulting mapping from element location's to HTML IDs.
ids: HashMap<Location, EcoString>,
}
impl Work {
/// Sets up.
fn new() -> Self {
Self { queue: VecDeque::new(), ids: HashMap::new() }
}
/// Marks the element with the given location and label as in need of an
/// ID. A subsequent call to `drain` will call `f`.
fn enqueue(&mut self, loc: Location, label: Option<Label>) {
self.queue.push_back((loc, label))
}
/// If one or multiple elements are in need of an ID, calls `f` to generate
/// an ID and apply it to the current node with `f`, and then establishes a
/// mapping from the elements' locations to that ID.
fn drain(&mut self, f: impl FnOnce(Option<Label>) -> EcoString) {
if let Some(&(_, label)) = self.queue.front() {
let id = f(label);
for (loc, _) in self.queue.drain(..) {
self.ids.insert(loc, id.clone());
}
}
}
/// Similar to `drain`, but only for a specific given location.
fn remove(&mut self, loc: Location, f: impl FnOnce(Option<Label>) -> EcoString) {
if let Some(i) = self.queue.iter().position(|&(l, _)| l == loc) {
let (_, label) = self.queue.remove(i).unwrap();
let id = f(label);
self.ids.insert(loc, id.clone());
}
}
}
/// Creates unique IDs for elements.
struct Identificator<'a> {
introspector: &'a Introspector,
loc_counter: usize,
label_counter: HashMap<Label, usize>,
}
impl<'a> Identificator<'a> {
/// Creates a new identificator.
fn new(introspector: &'a Introspector) -> Self {
Self {
introspector,
loc_counter: 0,
label_counter: HashMap::new(),
}
}
/// Assigns an ID to an element or reuses an existing ID.
fn assign(&mut self, element: &mut HtmlElement, label: Option<Label>) -> EcoString {
element.attrs.get(attr::id).cloned().unwrap_or_else(|| {
let id = self.identify(label);
element.attrs.push_front(attr::id, id.clone());
id
})
}
/// Generates an ID, potentially based on a label.
fn identify(&mut self, label: Option<Label>) -> EcoString {
if let Some(label) = label {
let resolved = label.resolve();
let text = resolved.as_str();
if can_use_label_as_id(text) {
if self.introspector.label_count(label) == 1 {
return text.into();
}
let counter = self.label_counter.entry(label).or_insert(0);
*counter += 1;
return disambiguate(self.introspector, text, counter);
}
}
self.loc_counter += 1;
disambiguate(self.introspector, "loc", &mut self.loc_counter)
}
}
/// Whether the label is both a valid CSS identifier and a valid URL fragment
/// for linking.
///
/// This is slightly more restrictive than HTML and CSS, but easier to
/// understand and explain.
fn can_use_label_as_id(label: &str) -> bool {
!label.is_empty()
&& label.chars().all(|c| c.is_alphanumeric() || matches!(c, '-' | '_'))
&& !label.starts_with(|c: char| c.is_numeric() || c == '-')
}
/// Disambiguates `text` with the suffix `-{counter}`, while ensuring that this
/// does not result in a collision with an existing label.
fn disambiguate(
introspector: &Introspector,
text: &str,
counter: &mut usize,
) -> EcoString {
loop {
let disambiguated = eco_format!("{text}-{counter}");
if PicoStr::get(&disambiguated)
.and_then(Label::new)
.is_some_and(|label| introspector.label_count(label) > 0)
{
*counter += 1;
} else {
break disambiguated;
}
}
}

View File

@ -1,7 +1,7 @@
use std::num::NonZeroUsize;
use ecow::{eco_format, EcoVec};
use typst_library::diag::warning;
use typst_library::diag::{warning, At};
use typst_library::foundations::{
Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
};
@ -59,19 +59,11 @@ pub fn register(rules: &mut NativeRuleMap) {
rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
}
const STRONG_RULE: ShowFn<StrongElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::strong)
.with_body(Some(elem.body.clone()))
.pack()
.spanned(elem.span()))
};
const STRONG_RULE: ShowFn<StrongElem> =
|elem, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
const EMPH_RULE: ShowFn<EmphElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::em)
.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());
const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
Ok(HtmlElem::new(tag::ul)
@ -86,8 +78,7 @@ const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
.pack()
.spanned(item.span())
}))))
.pack()
.spanned(elem.span()))
.pack())
};
const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
@ -103,7 +94,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
let body = Content::sequence(elem.children.iter().map(|item| {
let mut li = HtmlElem::new(tag::li);
if let Some(nr) = item.number.get(styles) {
if let Smart::Custom(nr) = item.number.get(styles) {
li = li.with_attr(attr::value, eco_format!("{nr}"));
}
// Text in wide enums shall always turn into paragraphs.
@ -114,7 +105,7 @@ const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
li.with_body(Some(body)).pack().spanned(item.span())
}));
Ok(ol.with_body(Some(body)).pack().spanned(elem.span()))
Ok(ol.with_body(Some(body)).pack())
};
const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
@ -141,20 +132,32 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
};
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
})
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
let href = match dest {
Destination::Url(url) => Some(url.clone().into_inner()),
Destination::Location(location) => {
let id = engine
.introspector
.html_id(location)
.cloned()
.ok_or("failed to determine link anchor")
.at(elem.span())?;
Some(eco_format!("#{id}"))
}
Destination::Position(_) => {
engine.sink.warn(warning!(
elem.span(),
"positional link was ignored during HTML export"
));
None
}
};
Ok(HtmlElem::new(tag::a)
.with_optional_attr(attr::href, href)
.with_body(Some(elem.body.clone()))
.pack())
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
@ -190,10 +193,9 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
.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)
HtmlElem::new(t).with_body(Some(realized)).pack()
})
};
@ -212,17 +214,13 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
// 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))
Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
};
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()))
.pack())
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
@ -363,19 +361,11 @@ fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
.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 SUB_RULE: ShowFn<SubElem> =
|elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
const SUPER_RULE: ShowFn<SuperElem> = |elem, _, _| {
Ok(HtmlElem::new(tag::sup)
.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());
const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
// Note: In modern HTML, `<u>` is not the underline element, but
@ -414,8 +404,7 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code })
.with_body(Some(Content::sequence(seq)))
.pack()
.spanned(elem.span()))
.pack())
};
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());

View File

@ -719,6 +719,10 @@ fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs {
struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>,
glyphs: Vec<ShapedGlyph>,
/// Font families that have been used with unlimited coverage.
///
/// These font families are considered exhausted and will not be used again,
/// even if they are declared again (e.g., during fallback after normal selection).
used: Vec<Font>,
styles: StyleChain<'a>,
size: Abs,
@ -777,7 +781,10 @@ fn shape_segment<'a>(
return;
};
ctx.used.push(font.clone());
// This font has been exhausted and will not be used again.
if covers.is_none() {
ctx.used.push(font.clone());
}
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();

View File

@ -1,11 +1,10 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::AccentElem;
use super::{
style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext,
MathFragment,
style_cramped, style_dtls, style_flac, FrameFragment, MathContext, MathFragment,
};
/// How much the accent can be shorter than the base.
@ -27,14 +26,17 @@ pub fn layout_accent(
if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles };
let cramped = style_cramped();
let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?;
let base_styles = base_styles.chain(&cramped);
let base = ctx.layout_into_fragment(&elem.base, base_styles)?;
let (font, size) = base.font(ctx, base_styles, elem.base.span())?;
// Preserve class to preserve automatic spacing.
let base_class = base.class();
let base_attach = base.accent_attach();
// Try to replace the accent glyph with its flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
let flattened_base_height = value!(font, flattened_accent_base_height).at(size);
let flac = style_flac();
let accent_styles = if top_accent && base.ascent() > flattened_base_height {
styles.chain(&flac)
@ -42,23 +44,25 @@ pub fn layout_accent(
styles
};
let mut glyph =
GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?;
let mut accent = ctx.layout_into_fragment(
&SymbolElem::packed(accent.0).spanned(elem.span()),
accent_styles,
)?;
// Forcing the accent to be at least as large as the base makes it too wide
// in many cases.
let width = elem.size.resolve(styles).relative_to(base.width());
let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size);
glyph.stretch_horizontal(ctx, width - short_fall);
let accent_attach = glyph.accent_attach.0;
let accent = glyph.into_frame();
let short_fall = ACCENT_SHORT_FALL.at(size);
accent.stretch_horizontal(ctx, width - short_fall);
let accent_attach = accent.accent_attach().0;
let accent = accent.into_frame();
let (gap, accent_pos, base_pos) = if top_accent {
// Descent is negative because the accent's ink bottom is above the
// baseline. Therefore, the default gap is the accent's negated descent
// minus the accent base height. Only if the base is very small, we
// need a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height);
let accent_base_height = value!(font, accent_base_height).at(size);
let gap = -accent.descent() - base.ascent().min(accent_base_height);
let accent_pos = Point::with_x(base_attach.0 - accent_attach);
let base_pos = Point::with_y(accent.height() + gap);

View File

@ -4,6 +4,8 @@ use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
};
use typst_library::text::Font;
use typst_syntax::Span;
use typst_utils::OptionExt;
use super::{
@ -83,7 +85,7 @@ pub fn layout_attach(
layout!(br, sub_style_chain)?,
];
layout_attachments(ctx, styles, base, fragments)
layout_attachments(ctx, styles, base, elem.base.span(), fragments)
}
/// Lays out a [`PrimeElem`].
@ -102,13 +104,19 @@ pub fn layout_primes(
4 => '⁗',
_ => unreachable!(),
};
let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
let f = ctx.layout_into_fragment(
&SymbolElem::packed(c).spanned(elem.span()),
styles,
)?;
ctx.push(f);
}
count => {
// Custom amount of primes
let prime = ctx
.layout_into_fragment(&SymbolElem::packed(''), styles)?
.layout_into_fragment(
&SymbolElem::packed('').spanned(elem.span()),
styles,
)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height()));
@ -170,22 +178,25 @@ fn layout_attachments(
ctx: &mut MathContext,
styles: StyleChain,
base: MathFragment,
span: Span,
[tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
) -> SourceResult<()> {
let base_class = base.class();
let class = base.class();
let (font, size) = base.font(ctx, styles, span)?;
let cramped = styles.get(EquationElem::cramped);
// Calculate the distance from the base's baseline to the superscripts' and
// subscripts' baseline.
let (tx_shift, bx_shift) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) {
(Abs::zero(), Abs::zero())
} else {
compute_script_shifts(ctx, styles, &base, [&tl, &tr, &bl, &br])
compute_script_shifts(&font, size, cramped, &base, [&tl, &tr, &bl, &br])
};
// Calculate the distance from the base's baseline to the top attachment's
// and bottom attachment's baseline.
let (t_shift, b_shift) =
compute_limit_shifts(ctx, styles, &base, [t.as_ref(), b.as_ref()]);
compute_limit_shifts(&font, size, &base, [t.as_ref(), b.as_ref()]);
// Calculate the final frame height.
let ascent = base
@ -215,7 +226,7 @@ fn layout_attachments(
// `space_after_script` is extra spacing that is at the start before each
// pre-script, and at the end after each post-script (see the MathConstants
// table in the OpenType MATH spec).
let space_after_script = scaled!(ctx, styles, space_after_script);
let space_after_script = value!(font, space_after_script).at(size);
// Calculate the distance each pre-script extends to the left of the base's
// width.
@ -272,7 +283,7 @@ fn layout_attachments(
layout!(b, b_x, b_y); // lower-limit
// Done! Note that we retain the class of the base.
ctx.push(FrameFragment::new(styles, frame).with_class(base_class));
ctx.push(FrameFragment::new(styles, frame).with_class(class));
Ok(())
}
@ -364,8 +375,8 @@ fn compute_limit_widths(
/// Returns two lengths, the first being the distance to the upper-limit's
/// baseline and the second being the distance to the lower-limit's baseline.
fn compute_limit_shifts(
ctx: &MathContext,
styles: StyleChain,
font: &Font,
font_size: Abs,
base: &MathFragment,
[t, b]: [Option<&MathFragment>; 2],
) -> (Abs, Abs) {
@ -373,16 +384,15 @@ fn compute_limit_shifts(
// ascender of the limits respectively, whereas `upper_rise_min` and
// `lower_drop_min` give gaps to each limit's baseline (see the
// MathConstants table in the OpenType MATH spec).
let t_shift = t.map_or_default(|t| {
let upper_gap_min = scaled!(ctx, styles, upper_limit_gap_min);
let upper_rise_min = scaled!(ctx, styles, upper_limit_baseline_rise_min);
let upper_gap_min = value!(font, upper_limit_gap_min).at(font_size);
let upper_rise_min = value!(font, upper_limit_baseline_rise_min).at(font_size);
base.ascent() + upper_rise_min.max(upper_gap_min + t.descent())
});
let b_shift = b.map_or_default(|b| {
let lower_gap_min = scaled!(ctx, styles, lower_limit_gap_min);
let lower_drop_min = scaled!(ctx, styles, lower_limit_baseline_drop_min);
let lower_gap_min = value!(font, lower_limit_gap_min).at(font_size);
let lower_drop_min = value!(font, lower_limit_baseline_drop_min).at(font_size);
base.descent() + lower_drop_min.max(lower_gap_min + b.ascent())
});
@ -393,25 +403,27 @@ fn compute_limit_shifts(
/// Returns two lengths, the first being the distance to the superscripts'
/// baseline and the second being the distance to the subscripts' baseline.
fn compute_script_shifts(
ctx: &MathContext,
styles: StyleChain,
font: &Font,
font_size: Abs,
cramped: bool,
base: &MathFragment,
[tl, tr, bl, br]: [&Option<MathFragment>; 4],
) -> (Abs, Abs) {
let sup_shift_up = if styles.get(EquationElem::cramped) {
scaled!(ctx, styles, superscript_shift_up_cramped)
let sup_shift_up = (if cramped {
value!(font, superscript_shift_up_cramped)
} else {
scaled!(ctx, styles, superscript_shift_up)
};
value!(font, superscript_shift_up)
})
.at(font_size);
let sup_bottom_min = scaled!(ctx, styles, superscript_bottom_min);
let sup_bottom_min = value!(font, superscript_bottom_min).at(font_size);
let sup_bottom_max_with_sub =
scaled!(ctx, styles, superscript_bottom_max_with_subscript);
let sup_drop_max = scaled!(ctx, styles, superscript_baseline_drop_max);
let gap_min = scaled!(ctx, styles, sub_superscript_gap_min);
let sub_shift_down = scaled!(ctx, styles, subscript_shift_down);
let sub_top_max = scaled!(ctx, styles, subscript_top_max);
let sub_drop_min = scaled!(ctx, styles, subscript_baseline_drop_min);
value!(font, superscript_bottom_max_with_subscript).at(font_size);
let sup_drop_max = value!(font, superscript_baseline_drop_max).at(font_size);
let gap_min = value!(font, sub_superscript_gap_min).at(font_size);
let sub_shift_down = value!(font, subscript_shift_down).at(font_size);
let sub_top_max = value!(font, subscript_top_max).at(font_size);
let sub_drop_min = value!(font, subscript_baseline_drop_min).at(font_size);
let mut shift_up = Abs::zero();
let mut shift_down = Abs::zero();

View File

@ -7,7 +7,7 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span;
use super::{
style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
find_math_font, style_for_denominator, style_for_numerator, FrameFragment,
MathContext, DELIM_SHORT_FALL,
};
@ -49,29 +49,33 @@ fn layout_frac_like(
binom: bool,
span: Span,
) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height);
let thickness = scaled!(ctx, styles, fraction_rule_thickness);
let shift_up = scaled!(
ctx, styles,
let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
let thickness = value!(font, fraction_rule_thickness).resolve(styles);
let shift_up = value!(
font, styles,
text: fraction_numerator_shift_up,
display: fraction_numerator_display_style_shift_up,
);
let shift_down = scaled!(
ctx, styles,
)
.resolve(styles);
let shift_down = value!(
font, styles,
text: fraction_denominator_shift_down,
display: fraction_denominator_display_style_shift_down,
);
let num_min = scaled!(
ctx, styles,
)
.resolve(styles);
let num_min = value!(
font, styles,
text: fraction_numerator_gap_min,
display: fraction_num_display_style_gap_min,
);
let denom_min = scaled!(
ctx, styles,
)
.resolve(styles);
let denom_min = value!(
font, styles,
text: fraction_denominator_gap_min,
display: fraction_denom_display_style_gap_min,
);
)
.resolve(styles);
let num_style = style_for_numerator(styles);
let num = ctx.layout_into_frame(num, styles.chain(&num_style))?;
@ -82,7 +86,7 @@ fn layout_frac_like(
// Add a comma between each element.
denom
.iter()
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
.flat_map(|a| [SymbolElem::packed(',').spanned(span), a.clone()])
.skip(1),
),
styles.chain(&denom_style),
@ -109,12 +113,18 @@ fn layout_frac_like(
frame.push_frame(denom_pos, denom);
if binom {
let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?;
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let mut left =
ctx.layout_into_fragment(&SymbolElem::packed('(').spanned(span), styles)?;
left.stretch_vertical(ctx, height - short_fall);
left.center_on_axis();
ctx.push(left);
ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?;
let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(')').spanned(span), styles)?;
right.stretch_vertical(ctx, height - short_fall);
right.center_on_axis();
ctx.push(right);

View File

@ -11,12 +11,16 @@ use typst_library::layout::{
Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem};
use typst_library::text::{
families, features, language, variant, Font, Glyph, TextElem, TextItem,
};
use typst_library::visualize::Paint;
use typst_library::World;
use typst_syntax::Span;
use typst_utils::{default_math_class, Get};
use unicode_math_class::MathClass;
use super::MathContext;
use super::{find_math_font, MathContext};
use crate::inline::create_shape_plan;
use crate::modifiers::{FrameModifiers, FrameModify};
@ -108,6 +112,21 @@ impl MathFragment {
}
}
pub fn font(
&self,
ctx: &MathContext,
styles: StyleChain,
span: Span,
) -> SourceResult<(Font, Abs)> {
Ok((
match self {
Self::Glyph(glyph) => glyph.item.font.clone(),
_ => find_math_font(ctx.engine.world, styles, span)?,
},
self.font_size().unwrap_or_else(|| styles.resolve(TextElem::size)),
))
}
pub fn font_size(&self) -> Option<Abs> {
match self {
Self::Glyph(glyph) => Some(glyph.item.size),
@ -192,6 +211,31 @@ impl MathFragment {
}
}
pub fn fill(&self) -> Option<Paint> {
match self {
Self::Glyph(glyph) => Some(glyph.item.fill.clone()),
_ => None,
}
}
pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_vertical(ctx, height)
}
}
pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) {
if let Self::Glyph(glyph) = self {
glyph.stretch_horizontal(ctx, width)
}
}
pub fn center_on_axis(&mut self) {
if let Self::Glyph(glyph) = self {
glyph.center_on_axis()
}
}
/// If no kern table is provided for a corner, a kerning amount of zero is
/// assumed.
pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs {
@ -261,23 +305,70 @@ pub struct GlyphFragment {
impl GlyphFragment {
/// Calls `new` with the given character.
pub fn new_char(
font: &Font,
ctx: &MathContext,
styles: StyleChain,
c: char,
span: Span,
) -> SourceResult<Self> {
Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span)
) -> SourceResult<Option<Self>> {
Self::new(ctx, styles, c.encode_utf8(&mut [0; 4]), span)
}
/// Selects a font to use and then shapes text.
pub fn new(
ctx: &MathContext,
styles: StyleChain,
text: &str,
span: Span,
) -> SourceResult<Option<Self>> {
let families = families(styles);
let variant = variant(styles);
let fallback = styles.get(TextElem::fallback);
let end = text.char_indices().nth(1).map(|(i, _)| i).unwrap_or(text.len());
// Find the next available family.
let world = ctx.engine.world;
let book = world.book();
let mut selection = None;
for family in families {
selection = book
.select(family.as_str(), variant)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
})
.filter(|_| family.covers().is_none_or(|cov| cov.is_match(&text[..end])));
if selection.is_some() {
break;
}
}
// Do font fallback if the families are exhausted and fallback is enabled.
if selection.is_none() && fallback {
selection = book
.select_fallback(None, variant, text)
.and_then(|id| world.font(id))
.filter(|font| {
font.ttf().tables().math.and_then(|math| math.constants).is_some()
});
}
// Error out if no math font could be found at all.
let Some(font) = selection else {
bail!(span, "current font does not support math");
};
Self::shape(&font, styles, text, span)
}
/// Try to create a new glyph out of the given string. Will bail if the
/// result from shaping the string is not a single glyph or is a tofu.
/// result from shaping the string is more than a single glyph.
#[comemo::memoize]
pub fn new(
pub fn shape(
font: &Font,
styles: StyleChain,
text: &str,
span: Span,
) -> SourceResult<GlyphFragment> {
) -> SourceResult<Option<GlyphFragment>> {
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_language(language(styles));
@ -300,18 +391,15 @@ impl GlyphFragment {
);
let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer);
if buffer.len() != 1 {
bail!(span, "did not get a single glyph after shaping {}", text);
match buffer.len() {
0 => return Ok(None),
1 => {}
_ => bail!(span, "did not get a single glyph after shaping {}", text),
}
let info = buffer.glyph_infos()[0];
let pos = buffer.glyph_positions()[0];
// TODO: add support for coverage and fallback, like in normal text shaping.
if info.glyph_id == 0 {
bail!(span, "current font is missing a glyph for {}", text);
}
let cluster = info.cluster as usize;
let c = text[cluster..].chars().next().unwrap();
let limits = Limits::for_char(c);
@ -361,7 +449,7 @@ impl GlyphFragment {
modifiers: FrameModifiers::get_in(styles),
};
fragment.update_glyph();
Ok(fragment)
Ok(Some(fragment))
}
/// Sets element id and boxes in appropriate way without changing other

View File

@ -33,12 +33,13 @@ pub fn layout_lr(
let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height);
let max_extent = inner_fragments
.iter()
.map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
.max()
.unwrap_or_default();
let mut max_extent = Abs::zero();
for fragment in inner_fragments.iter() {
let (font, size) = fragment.font(ctx, styles, elem.span())?;
let axis = value!(font, axis_height).at(size);
let extent = (fragment.ascent() - axis).max(fragment.descent() + axis);
max_extent = max_extent.max(extent);
}
let relative_to = 2.0 * max_extent;
let height = elem.size.resolve(styles);

View File

@ -1,5 +1,5 @@
use typst_library::diag::{bail, warning, SourceResult};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
};
@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span;
use super::{
alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment,
LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
alignments, find_math_font, style_for_denominator, AlignmentResult, FrameFragment,
GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
};
const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -186,12 +186,10 @@ fn layout_body(
// We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are
// way too big.
let paren = GlyphFragment::new_char(
ctx.font,
styles.chain(&denom_style),
'(',
Span::detached(),
)?;
// This will never panic as a paren will never shape into nothing.
let paren =
GlyphFragment::new_char(ctx, styles.chain(&denom_style), '(', Span::detached())?
.unwrap();
for (column, col) in columns.iter().zip(&mut cols) {
for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
@ -314,13 +312,15 @@ fn layout_delimiters(
span: Span,
) -> SourceResult<()> {
let short_fall = DELIM_SHORT_FALL.resolve(styles);
let axis = scaled!(ctx, styles, axis_height);
let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
let height = frame.height();
let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis);
if let Some(left_c) = left {
let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?;
let mut left =
ctx.layout_into_fragment(&SymbolElem::packed(left_c).spanned(span), styles)?;
left.stretch_vertical(ctx, target - short_fall);
left.center_on_axis();
ctx.push(left);
@ -329,7 +329,8 @@ fn layout_delimiters(
ctx.push(FrameFragment::new(styles, frame));
if let Some(right_c) = right {
let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?;
let mut right =
ctx.layout_into_fragment(&SymbolElem::packed(right_c).spanned(span), styles)?;
right.stretch_vertical(ctx, target - short_fall);
right.center_on_axis();
ctx.push(right);

View File

@ -13,7 +13,7 @@ mod stretch;
mod text;
mod underover;
use typst_library::diag::{bail, SourceResult};
use typst_library::diag::SourceResult;
use typst_library::engine::Engine;
use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
@ -27,11 +27,7 @@ use typst_library::layout::{
use typst_library::math::*;
use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{
families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
};
use typst_library::World;
use typst_syntax::Span;
use typst_library::text::{LinebreakElem, RawElem, SpaceElem, TextEdgeBounds, TextElem};
use typst_utils::Numeric;
use unicode_math_class::MathClass;
@ -53,12 +49,11 @@ pub fn layout_equation_inline(
) -> SourceResult<Vec<InlineItem>> {
assert!(!elem.block.get(styles));
let font = find_math_font(engine, styles, elem.span())?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, region, &font);
let mut ctx = MathContext::new(engine, &mut locator, region);
let scale_style = style_for_script_scale(&ctx);
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let run = ctx.layout_into_run(&elem.body, styles)?;
@ -108,12 +103,12 @@ pub fn layout_equation_block(
assert!(elem.block.get(styles));
let span = elem.span();
let font = find_math_font(engine, styles, span)?;
let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font);
let mut ctx = MathContext::new(engine, &mut locator, regions.base());
let scale_style = style_for_script_scale(&ctx);
let font = find_math_font(ctx.engine.world, styles, elem.span())?;
let scale_style = style_for_script_scale(&font);
let styles = styles.chain(&scale_style);
let full_equation_builder = ctx
@ -234,24 +229,6 @@ pub fn layout_equation_block(
Ok(Fragment::frames(frames))
}
fn find_math_font(
engine: &mut Engine<'_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let world = engine.world;
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
Some(font)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
fn add_equation_number(
equation_builder: MathRunFrameBuilder,
number: Frame,
@ -370,9 +347,6 @@ struct MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
region: Region,
// Font-related.
font: &'a Font,
constants: ttf_parser::math::Constants<'a>,
// Mutable.
fragments: Vec<MathFragment>,
}
@ -383,19 +357,11 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
engine: &'v mut Engine<'e>,
locator: &'v mut SplitLocator<'a>,
base: Size,
font: &'a Font,
) -> Self {
// These unwraps are safe as the font given is one returned by the
// find_math_font function, which only returns fonts that have a math
// constants table.
let constants = font.ttf().tables().math.unwrap().constants.unwrap();
Self {
engine,
locator,
region: Region::new(base, Axes::splat(false)),
font,
constants,
fragments: vec![],
}
}
@ -469,17 +435,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
styles,
)?;
let outer = styles;
for (elem, styles) in pairs {
// Hack because the font is fixed in math.
if styles != outer
&& styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font)
{
let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue;
}
layout_realized(elem, self, styles)?;
}
@ -496,7 +452,10 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() {
let space_width = ctx.font.space_width().unwrap_or(THICK);
let space_width = find_math_font(ctx.engine.world, styles, elem.span())
.ok()
.and_then(|font| font.space_width())
.unwrap_or(THICK);
ctx.push(MathFragment::Space(space_width.resolve(styles)));
} else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak);
@ -566,9 +525,11 @@ fn layout_realized(
self::underover::layout_overshell(elem, ctx, styles)?
} else {
let mut frame = layout_external(elem, ctx, styles)?;
if !frame.has_baseline() {
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
if !frame.has_baseline() && !elem.is::<RawElem>() {
if let Ok(font) = find_math_font(ctx.engine.world, styles, elem.span()) {
let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
}
}
ctx.push(
FrameFragment::new(styles, frame)

View File

@ -1,11 +1,11 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain};
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Frame, FrameItem, Point, Size};
use typst_library::math::{EquationElem, MathSize, RootElem};
use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry};
use super::{style_cramped, FrameFragment, GlyphFragment, MathContext};
use super::{style_cramped, FrameFragment, MathContext};
/// Lays out a [`RootElem`].
///
@ -17,45 +17,62 @@ pub fn layout_root(
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let index = elem.index.get_ref(styles);
let span = elem.span();
let gap = scaled!(
ctx, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
);
let thickness = scaled!(ctx, styles, radical_rule_thickness);
let extra_ascender = scaled!(ctx, styles, radical_extra_ascender);
let kern_before = scaled!(ctx, styles, radical_kern_before_degree);
let kern_after = scaled!(ctx, styles, radical_kern_after_degree);
let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
// Layout radicand.
let radicand = {
let cramped = style_cramped();
let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline();
let mut radicand = run.into_fragment(styles).into_frame();
let radicand = run.into_fragment(styles);
if multiline {
// Align the frame center line with the math axis.
radicand.set_baseline(
radicand.height() / 2.0 + scaled!(ctx, styles, axis_height),
);
let (font, size) = radicand.font(ctx, styles, elem.radicand.span())?;
let axis = value!(font, axis_height).at(size);
let mut radicand = radicand.into_frame();
radicand.set_baseline(radicand.height() / 2.0 + axis);
radicand
} else {
radicand.into_frame()
}
radicand
};
// Layout root symbol.
let mut sqrt =
ctx.layout_into_fragment(&SymbolElem::packed('√').spanned(span), styles)?;
let (font, size) = sqrt.font(ctx, styles, span)?;
let thickness = value!(font, radical_rule_thickness).at(size);
let extra_ascender = value!(font, radical_extra_ascender).at(size);
let kern_before = value!(font, radical_kern_before_degree).at(size);
let kern_after = value!(font, radical_kern_after_degree).at(size);
let raise_factor = percent!(font, radical_degree_bottom_raise_percent);
let gap = value!(
font, styles,
text: radical_vertical_gap,
display: radical_display_style_vertical_gap,
)
.at(size);
let line = FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke::from_pair(
sqrt.fill()
.unwrap_or_else(|| styles.get_ref(TextElem::fill).as_decoration()),
thickness,
)),
span,
);
let target = radicand.height() + thickness + gap;
let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?;
sqrt.stretch_vertical(ctx, target);
let sqrt = sqrt.into_frame();
// Layout the index.
let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap();
let index = index
let index = elem
.index
.get_ref(styles)
.as_ref()
.map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript)))
.transpose()?;
@ -107,19 +124,7 @@ pub fn layout_root(
}
frame.push_frame(sqrt_pos, sqrt);
frame.push(
line_pos,
FrameItem::Shape(
Geometry::Line(Point::with_x(radicand.width())).stroked(
FixedStroke::from_pair(
styles.get_ref(TextElem::fill).as_decoration(),
thickness,
),
),
span,
),
);
frame.push(line_pos, line);
frame.push_frame(radicand_pos, radicand);
ctx.push(FrameFragment::new(styles, frame));

View File

@ -1,32 +1,47 @@
use comemo::Tracked;
use ttf_parser::math::MathValue;
use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{FontFeatures, TextElem};
use typst_library::text::{families, variant, Font, FontFeatures, TextElem};
use typst_library::World;
use typst_syntax::Span;
use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
use super::{LeftRightAlternator, MathFragment, MathRun};
macro_rules! scaled {
($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
macro_rules! value {
($font:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => {
match $styles.get(typst_library::math::EquationElem::size) {
typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display),
_ => scaled!($ctx, $styles, $text),
typst_library::math::MathSize::Display => value!($font, $display),
_ => value!($font, $text),
}
};
($ctx:expr, $styles:expr, $name:ident) => {
$crate::math::Scaled::scaled(
$ctx.constants.$name(),
$ctx,
$styles.resolve(typst_library::text::TextElem::size),
)
($font:expr, $name:ident) => {
$font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| {
crate::math::shared::Scaled::scaled(constants.$name(), &$font)
})
.unwrap()
};
}
macro_rules! percent {
($ctx:expr, $name:ident) => {
$ctx.constants.$name() as f64 / 100.0
($font:expr, $name:ident) => {
$font
.ttf()
.tables()
.math
.and_then(|math| math.constants)
.map(|constants| constants.$name())
.unwrap() as f64
/ 100.0
};
}
@ -35,27 +50,47 @@ pub const DELIM_SHORT_FALL: Em = Em::new(0.1);
/// Converts some unit to an absolute length with the current font & font size.
pub trait Scaled {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs;
fn scaled(self, font: &Font) -> Em;
}
impl Scaled for i16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
fn scaled(self, font: &Font) -> Em {
font.to_em(self)
}
}
impl Scaled for u16 {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
ctx.font.to_em(self).at(font_size)
fn scaled(self, font: &Font) -> Em {
font.to_em(self)
}
}
impl Scaled for MathValue<'_> {
fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs {
self.value.scaled(ctx, font_size)
fn scaled(self, font: &Font) -> Em {
self.value.scaled(font)
}
}
/// Get the current math font.
#[comemo::memoize]
pub fn find_math_font(
world: Tracked<dyn World + '_>,
styles: StyleChain,
span: Span,
) -> SourceResult<Font> {
let variant = variant(styles);
let Some(font) = families(styles).find_map(|family| {
let id = world.book().select(family.as_str(), variant)?;
let font = world.font(id)?;
let _ = font.ttf().tables().math?.constants?;
// Take the base font as the "main" math font.
family.covers().map_or(Some(font), |_| None)
}) else {
bail!(span, "current font does not support math");
};
Ok(font)
}
/// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> {
EquationElem::cramped.set(true).wrap()
@ -107,11 +142,12 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
}
/// Styles to add font constants to the style chain.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
pub fn style_for_script_scale(font: &Font) -> LazyHash<Style> {
let constants = font.ttf().tables().math.and_then(|math| math.constants).unwrap();
EquationElem::script_scale
.set((
ctx.constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(),
constants.script_percent_scale_down(),
constants.script_script_percent_scale_down(),
))
.wrap()
}

View File

@ -3,7 +3,7 @@ use std::f64::consts::SQRT_2;
use codex::styling::{to_style, MathStyle};
use ecow::EcoString;
use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::foundations::{Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize};
use typst_library::text::{
@ -14,8 +14,8 @@ use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation;
use super::{
has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext, MathFragment,
MathRun,
find_math_font, has_dtls_feat, style_dtls, FrameFragment, GlyphFragment, MathContext,
MathFragment, MathRun,
};
/// Lays out a [`TextElem`].
@ -52,7 +52,8 @@ fn layout_text_lines<'a>(
}
}
let mut frame = MathRun::new(fragments).into_frame(styles);
let axis = scaled!(ctx, styles, axis_height);
let font = find_math_font(ctx.engine.world, styles, span)?;
let axis = value!(font, axis_height).resolve(styles);
frame.set_baseline(frame.height() / 2.0 + axis);
Ok(FrameFragment::new(styles, frame))
}
@ -80,7 +81,9 @@ fn layout_inline_text(
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)?;
// This won't panic as ASCII digits and '.' will never end up as
// nothing after shaping.
let glyph = GlyphFragment::new_char(ctx, styles, c, span)?.unwrap();
fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
@ -132,8 +135,11 @@ pub fn layout_symbol(
// Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass.
let dtls = style_dtls();
let (unstyled_c, symbol_styles) = match try_dotless(elem.text) {
Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)),
let (unstyled_c, symbol_styles) = match (
try_dotless(elem.text),
find_math_font(ctx.engine.world, styles, elem.span()),
) {
(Some(c), Ok(font)) if has_dtls_feat(&font) => (c, styles.chain(&dtls)),
_ => (elem.text, styles),
};
@ -144,39 +150,22 @@ pub fn layout_symbol(
let style = MathStyle::select(unstyled_c, variant, bold, italic);
let text: EcoString = to_style(unstyled_c, style).collect();
let fragment: MathFragment =
match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) {
Ok(mut glyph) => {
adjust_glyph_layout(&mut glyph, ctx, styles);
glyph.into()
}
Err(_) => {
// Not in the math font, fallback to normal inline text layout.
// TODO: Should replace this with proper fallback in [`GlyphFragment::new`].
layout_inline_text(&text, elem.span(), ctx, styles)?.into()
}
};
ctx.push(fragment);
Ok(())
}
/// Centers large glyphs vertically on the axis, scaling them if in display
/// style.
fn adjust_glyph_layout(
glyph: &mut GlyphFragment,
ctx: &mut MathContext,
styles: StyleChain,
) {
if glyph.class == MathClass::Large {
if styles.get(EquationElem::size) == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
glyph.center_on_axis();
if let Some(mut glyph) = GlyphFragment::new(ctx, symbol_styles, &text, elem.span())? {
if glyph.class == MathClass::Large {
if styles.get(EquationElem::size) == MathSize::Display {
let height = value!(glyph.item.font, display_operator_min_height)
.at(glyph.item.size)
.max(SQRT_2 * glyph.size.y);
glyph.stretch_vertical(ctx, height);
};
// TeXbook p 155. Large operators are always vertically centered on
// the axis.
glyph.center_on_axis();
}
ctx.push(glyph);
}
Ok(())
}
/// The non-dotless version of a dotless character that can be used with the

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
@ -11,7 +11,7 @@ use typst_syntax::Span;
use super::{
stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
GlyphFragment, LeftRightAlternator, MathContext, MathRun,
LeftRightAlternator, MathContext, MathRun,
};
const BRACE_GAP: Em = Em::new(0.25);
@ -208,26 +208,29 @@ fn layout_underoverline(
let (extra_height, content, line_pos, content_pos, baseline, bar_height, line_adjust);
match position {
Position::Under => {
let sep = scaled!(ctx, styles, underbar_extra_descender);
bar_height = scaled!(ctx, styles, underbar_rule_thickness);
let gap = scaled!(ctx, styles, underbar_vertical_gap);
extra_height = sep + bar_height + gap;
content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, underbar_extra_descender).at(size);
bar_height = value!(font, underbar_rule_thickness).at(size);
let gap = value!(font, underbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
content_pos = Point::zero();
baseline = content.ascent();
line_adjust = -content.italics_correction();
}
Position::Over => {
let sep = scaled!(ctx, styles, overbar_extra_ascender);
bar_height = scaled!(ctx, styles, overbar_rule_thickness);
let gap = scaled!(ctx, styles, overbar_vertical_gap);
extra_height = sep + bar_height + gap;
let cramped = style_cramped();
content = ctx.layout_into_fragment(body, styles.chain(&cramped))?;
let styles = styles.chain(&cramped);
content = ctx.layout_into_fragment(body, styles)?;
let (font, size) = content.font(ctx, styles, span)?;
let sep = value!(font, overbar_extra_ascender).at(size);
bar_height = value!(font, overbar_rule_thickness).at(size);
let gap = value!(font, overbar_vertical_gap).at(size);
extra_height = sep + bar_height + gap;
line_pos = Point::with_y(sep + bar_height / 2.0);
content_pos = Point::with_y(extra_height);
@ -285,7 +288,8 @@ fn layout_underoverspreader(
let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class();
let body = body.into_fragment(styles);
let mut glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?;
let mut glyph =
ctx.layout_into_fragment(&SymbolElem::packed(c).spanned(span), styles)?;
glyph.stretch_horizontal(ctx, body.width());
let mut rows = vec![];

View File

@ -20,8 +20,8 @@ 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,
LinkElem, ListElem, Outlinable, OutlineElem, OutlineEntry, ParElem, ParbreakElem,
QuoteElem, RefElem, StrongElem, TableCell, TableElem, TermsElem, Works,
};
use typst_library::pdf::EmbedElem;
use typst_library::text::{
@ -216,14 +216,8 @@ const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
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)
}
})
let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
Ok(body.linked(dest))
};
const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
@ -278,7 +272,7 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
BlockElem::new().with_body(Some(BlockBody::Content(realized)))
};
Ok(block.pack().spanned(span))
Ok(block.pack())
};
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
@ -332,8 +326,7 @@ const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
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()))
.pack())
};
const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
@ -556,9 +549,7 @@ const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
};
const TABLE_RULE: ShowFn<TableElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table)
.pack()
.spanned(elem.span()))
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table).pack())
};
const TABLE_CELL_RULE: ShowFn<TableCell> = |elem, _, styles| {
@ -709,27 +700,19 @@ 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()))
Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad).pack())
};
const COLUMNS_RULE: ShowFn<ColumnsElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns)
.pack()
.spanned(elem.span()))
Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns).pack())
};
const STACK_RULE: ShowFn<StackElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack)
.pack()
.spanned(elem.span()))
Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack).pack())
};
const GRID_RULE: ShowFn<GridElem> = |elem, _, _| {
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid)
.pack()
.spanned(elem.span()))
Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid).pack())
};
const GRID_CELL_RULE: ShowFn<GridCell> = |elem, _, styles| {
@ -759,33 +742,23 @@ fn show_cell(
}
const MOVE_RULE: ShowFn<MoveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move).pack())
};
const SCALE_RULE: ShowFn<ScaleElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale).pack())
};
const ROTATE_RULE: ShowFn<RotateElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate).pack())
};
const SKEW_RULE: ShowFn<SkewElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew).pack())
};
const REPEAT_RULE: ShowFn<RepeatElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat).pack())
};
const HIDE_RULE: ShowFn<HideElem> =
@ -807,83 +780,66 @@ const LAYOUT_RULE: ShowFn<LayoutElem> = |elem, _, _| {
crate::flow::layout_fragment(engine, &result, locator, styles, regions)
},
)
.pack()
.spanned(elem.span()))
.pack())
};
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()))
.pack())
};
const LINE_RULE: ShowFn<LineElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line).pack())
};
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()))
.pack())
};
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()))
.pack())
};
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()))
.pack())
};
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()))
.pack())
};
const POLYGON_RULE: ShowFn<PolygonElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon).pack())
};
const CURVE_RULE: ShowFn<CurveElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve).pack())
};
const PATH_RULE: ShowFn<PathElem> = |elem, _, _| {
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path)
.pack()
.spanned(elem.span()))
Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path).pack())
};
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()))
.pack())
} else {
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline)
.pack()
.spanned(elem.span()))
Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline).pack())
}
};

View File

@ -37,13 +37,12 @@ pub use crate::__select_where as select_where;
/// A filter for selecting elements within the document.
///
/// You can construct a selector in the following ways:
/// - you can use an element [function]
/// - you can filter for an element function with
/// [specific fields]($function.where)
/// - you can use a [string]($str) or [regular expression]($regex)
/// - you can use a [`{<label>}`]($label)
/// - you can use a [`location`]
/// To construct a selector you can:
/// - use an element [function]
/// - filter for an element function with [specific fields]($function.where)
/// - use a [string]($str) or [regular expression]($regex)
/// - use a [`{<label>}`]($label)
/// - use a [`location`]
/// - call the [`selector`] constructor to convert any of the above types into a
/// selector value and use the methods below to refine it
///
@ -148,7 +147,9 @@ impl Selector {
impl Selector {
/// Turns a value into a selector. The following values are accepted:
/// - An element function like a `heading` or `figure`.
/// - A [string]($str) or [regular expression]($regex).
/// - A `{<label>}`.
/// - A [`location`].
/// - A more complex selector like `{heading.where(level: 1)}`.
#[func(constructor)]
pub fn construct(

View File

@ -4,7 +4,7 @@ use std::hash::Hash;
use std::num::NonZeroUsize;
use std::sync::RwLock;
use ecow::EcoVec;
use ecow::{EcoString, EcoVec};
use smallvec::SmallVec;
use typst_utils::NonZeroExt;
@ -35,6 +35,11 @@ pub struct Introspector {
/// Accelerates lookup of elements by label.
labels: MultiMap<Label, usize>,
/// Maps from element locations to assigned HTML IDs. This used to support
/// intra-doc links in HTML export. In paged export, is is simply left
/// empty and [`Self::html_id`] is not used.
html_ids: HashMap<Location, EcoString>,
/// Caches queries done on the introspector. This is important because
/// even if all top-level queries are distinct, they often have shared
/// subqueries. Example: Individual counter queries with `before` that
@ -51,6 +56,17 @@ impl Introspector {
self.elems.iter().map(|(c, _)| c)
}
/// Checks how many times a label exists.
pub fn label_count(&self, label: Label) -> usize {
self.labels.get(&label).len()
}
/// Enriches an existing introspector with HTML IDs, which were assigned
/// to the DOM in a post-processing step.
pub fn set_html_ids(&mut self, html_ids: HashMap<Location, EcoString>) {
self.html_ids = html_ids;
}
/// Retrieves the element with the given index.
#[track_caller]
fn get_by_idx(&self, idx: usize) -> &Content {
@ -268,6 +284,11 @@ impl Introspector {
self.page_supplements.get(page.get() - 1).cloned().unwrap_or_default()
}
/// Retrieves the ID to link to for this location in HTML export.
pub fn html_id(&self, location: Location) -> Option<&EcoString> {
self.html_ids.get(&location)
}
/// Try to find a location for an element with the given `key` hash
/// that is closest after the `anchor`.
///
@ -343,6 +364,7 @@ pub struct IntrospectorBuilder {
pub pages: usize,
pub page_numberings: Vec<Option<Numbering>>,
pub page_supplements: Vec<Content>,
pub html_ids: HashMap<Location, EcoString>,
seen: HashSet<Location>,
insertions: MultiMap<Location, Vec<Pair>>,
keys: MultiMap<u128, Location>,
@ -426,6 +448,7 @@ impl IntrospectorBuilder {
pages: self.pages,
page_numberings: self.page_numberings,
page_supplements: self.page_supplements,
html_ids: self.html_ids,
elems,
keys: self.keys,
locations: self.locations,

View File

@ -98,14 +98,11 @@ pub fn cal(
/// ```example
/// #let scr(it) = text(
/// features: ("ss01",),
/// box($cal(it)$),
/// $cal(it)$,
/// )
///
/// We establish $cal(P) != scr(P)$.
/// ```
///
/// (The box is not conceptually necessary, but unfortunately currently needed
/// due to limitations in Typst's text style handling in math.)
#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])]
pub fn scr(
/// The content to style.

View File

@ -220,7 +220,7 @@ impl EnumElem {
pub struct EnumItem {
/// The item's number.
#[positional]
pub number: Option<u64>,
pub number: Smart<u64>,
/// The item's body.
#[required]

View File

@ -1,12 +1,13 @@
use std::ops::Deref;
use comemo::Tracked;
use ecow::{eco_format, EcoString};
use crate::diag::{bail, StrResult};
use crate::foundations::{
cast, elem, Content, Label, Packed, Repr, ShowSet, Smart, StyleChain, Styles,
};
use crate::introspection::Location;
use crate::introspection::{Introspector, Locatable, Location};
use crate::layout::Position;
use crate::text::TextElem;
@ -27,15 +28,62 @@ use crate::text::TextElem;
/// ]
/// ```
///
/// # Syntax
/// This function also has dedicated syntax: Text that starts with `http://` or
/// `https://` is automatically turned into a link.
///
/// # Hyphenation
/// If you enable hyphenation or justification, by default, it will not apply to
/// links to prevent unwanted hyphenation in URLs. You can opt out of this
/// default via `{show link: set text(hyphenate: true)}`.
///
/// # Syntax
/// This function also has dedicated syntax: Text that starts with `http://` or
/// `https://` is automatically turned into a link.
#[elem]
/// # Links in HTML export
/// In HTML export, a link to a [label] or [location] will be turned into a
/// fragment link to a named anchor point. To support this, targets without an
/// existing ID will automatically receive an ID in the DOM. How this works
/// varies by which kind of HTML node(s) the link target turned into:
///
/// - If the link target turned into a single HTML element, that element will
/// receive the ID. This is, for instance, typically the case when linking to
/// a top-level heading (which turns into a single `<h2>` element).
///
/// - If the link target turned into a single text node, the node will be
/// wrapped in a `<span>`, which will then receive the ID.
///
/// - If the link target turned into multiple nodes, the first node will receive
/// the ID.
///
/// - If the link target turned into no nodes at all, an empty span will be
/// generated to serve as a link target.
///
/// If you rely on a specific DOM structure, you should ensure that the link
/// target turns into one or multiple elements, as the compiler makes no
/// guarantees on the precise segmentation of text into text nodes.
///
/// If present, the automatic ID generation tries to reuse the link target's
/// label to create a human-readable ID. A label can be reused if:
///
/// - All characters are alphabetic or numeric according to Unicode, or a
/// hyphen, or an underscore.
///
/// - The label does not start with a digit or hyphen.
///
/// These rules ensure that the label is both a valid CSS identifier and a valid
/// URL fragment for linking.
///
/// As IDs must be unique in the DOM, duplicate labels might need disambiguation
/// when reusing them as IDs. The precise rules for this are as follows:
///
/// - If a label can be reused and is unique in the document, it will directly
/// be used as the ID.
///
/// - If it's reusable, but not unique, a suffix consisting of a hyphen and an
/// integer will be added. For instance, if the label `<mylabel>` exists
/// twice, it would turn into `mylabel-1` and `mylabel-2`.
///
/// - Otherwise, a unique ID of the form `loc-` followed by an integer will be
/// generated.
#[elem(Locatable)]
pub struct LinkElem {
/// The destination the link points to.
///
@ -124,6 +172,19 @@ pub enum LinkTarget {
Label(Label),
}
impl LinkTarget {
/// Resolves the destination.
pub fn resolve(&self, introspector: Tracked<Introspector>) -> StrResult<Destination> {
Ok(match self {
LinkTarget::Dest(dest) => dest.clone(),
LinkTarget::Label(label) => {
let elem = introspector.query_label(*label)?;
Destination::Location(elem.location().unwrap())
}
})
}
}
cast! {
LinkTarget,
self => match self {

View File

@ -5,12 +5,13 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed,
Repr, Smart, StyleChain, Synthesize,
Repr, Smart, StyleChain, Synthesize, TargetElem,
};
use crate::introspection::{Counter, CounterKey, Locatable};
use crate::math::EquationElem;
use crate::model::{
BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, Numbering,
BibliographyElem, CiteElem, Destination, Figurable, FootnoteElem, LinkElem,
LinkTarget, Numbering,
};
use crate::text::TextElem;
@ -346,7 +347,14 @@ fn realize_reference(
content = supplement + TextElem::packed("\u{a0}") + content;
}
Ok(content.linked(Destination::Location(loc)))
Ok(if styles.get(TargetElem::target).is_html() {
LinkElem::new(LinkTarget::Dest(Destination::Location(loc)), content).pack()
} else {
// TODO: We should probably also use `LinkElem` in the paged target, but
// it's a bit breaking and it becomes hard to style links without
// affecting references, so this change should be well-considered.
content.linked(Destination::Location(loc))
})
}
/// Turn a reference into a citation.

View File

@ -937,6 +937,7 @@ pub fn families(styles: StyleChain<'_>) -> impl Iterator<Item = &'_ FontFamily>
"noto color emoji",
"apple color emoji",
"segoe ui emoji",
"new computer modern math",
]
.into_iter()
.map(FontFamily::new)

View File

@ -28,7 +28,7 @@ use typst_library::model::{
ParElem, ParbreakElem, TermsElem,
};
use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::text::{LinebreakElem, RawElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_syntax::Span;
use typst_utils::{SliceExt, SmallBitSet};
@ -286,6 +286,13 @@ fn visit_kind_rules<'a>(
styles: StyleChain<'a>,
) -> SourceResult<bool> {
if let RealizationKind::Math = s.kind {
// Deal with Raw later when it gets laid out externally, so that it
// renders correctly in math.
if content.is::<RawElem>() {
s.sink.push((content, styles));
return Ok(true);
}
// Transparently recurse into equations nested in math, so that things
// like this work:
// ```
@ -374,7 +381,11 @@ fn visit_show_rules<'a>(
}
// Apply a built-in show rule.
ShowStep::Builtin(rule) => rule.apply(&output, s.engine, chained),
ShowStep::Builtin(rule) => {
let _scope = typst_timing::TimingScope::new(output.elem().name());
rule.apply(&output, s.engine, chained)
.map(|content| content.spanned(output.span()))
}
};
// Errors in show rules don't terminate compilation immediately. We just

View File

@ -9,7 +9,7 @@ use typst_library::visualize::{
use crate::SVGRenderer;
impl SVGRenderer {
impl SVGRenderer<'_> {
/// Render an image element.
pub(super) fn render_image(&mut self, image: &Image, size: &Axes<Abs>) {
let url = convert_image_to_base64_url(image);

View File

@ -6,6 +6,8 @@ mod shape;
mod text;
pub use image::{convert_image_scaling, convert_image_to_base64_url};
use typst_library::introspection::Introspector;
use typst_library::model::Destination;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter, Write};
@ -47,12 +49,24 @@ pub fn svg_frame(frame: &Frame) -> String {
/// Export a frame into an SVG suitable for embedding into HTML.
#[typst_macros::time(name = "svg html frame")]
pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String {
let mut renderer = SVGRenderer::with_options(xmlwriter::Options {
indent: xmlwriter::Indent::None,
..Default::default()
});
pub fn svg_html_frame(
frame: &Frame,
text_size: Abs,
id: Option<&str>,
link_points: &[(Point, EcoString)],
introspector: &Introspector,
) -> String {
let mut renderer = SVGRenderer::with_options(
xmlwriter::Options {
indent: xmlwriter::Indent::None,
..Default::default()
},
Some(introspector),
);
renderer.write_header_with_custom_attrs(frame.size(), |xml| {
if let Some(id) = id {
xml.write_attribute("id", id);
}
xml.write_attribute("class", "typst-frame");
xml.write_attribute_fmt(
"style",
@ -66,6 +80,11 @@ pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String {
let state = State::new(frame.size(), Transform::identity());
renderer.render_frame(state, Transform::identity(), frame);
for (pos, id) in link_points {
renderer.render_link_point(*pos, id);
}
renderer.finalize()
}
@ -102,9 +121,11 @@ pub fn svg_merged(document: &PagedDocument, padding: Abs) -> String {
}
/// Renders one or multiple frames to an SVG file.
struct SVGRenderer {
struct SVGRenderer<'a> {
/// The internal XML writer.
xml: XmlWriter,
/// The document's introspector, if we're writing an HTML frame.
introspector: Option<&'a Introspector>,
/// Prepared glyphs.
glyphs: Deduplicator<RenderedGlyph>,
/// Clip paths are used to clip a group. A clip path is a path that defines
@ -179,16 +200,20 @@ impl State {
}
}
impl SVGRenderer {
impl<'a> SVGRenderer<'a> {
/// Create a new SVG renderer with empty glyph and clip path.
fn new() -> Self {
Self::with_options(Default::default())
Self::with_options(Default::default(), None)
}
/// Create a new SVG renderer with the given configuration.
fn with_options(options: xmlwriter::Options) -> Self {
fn with_options(
options: xmlwriter::Options,
introspector: Option<&'a Introspector>,
) -> Self {
SVGRenderer {
xml: XmlWriter::new(options),
introspector,
glyphs: Deduplicator::new('g'),
clip_paths: Deduplicator::new('c'),
gradient_refs: Deduplicator::new('g'),
@ -248,8 +273,7 @@ impl SVGRenderer {
for (pos, item) in frame.items() {
// File size optimization.
// TODO: SVGs could contain links, couldn't they?
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
if matches!(item, FrameItem::Tag(_)) {
continue;
}
@ -270,7 +294,7 @@ impl SVGRenderer {
self.render_shape(state.pre_translate(*pos), shape)
}
FrameItem::Image(image, size, _) => self.render_image(image, size),
FrameItem::Link(_, _) => unreachable!(),
FrameItem::Link(dest, size) => self.render_link(dest, *size),
FrameItem::Tag(_) => unreachable!(),
};
@ -308,6 +332,53 @@ impl SVGRenderer {
self.xml.end_element();
}
/// Render a link element.
fn render_link(&mut self, dest: &Destination, size: Size) {
self.xml.start_element("a");
match dest {
Destination::Location(loc) => {
// TODO: Location links on the same page could also be supported
// outside of HTML.
if let Some(introspector) = self.introspector {
if let Some(id) = introspector.html_id(*loc) {
self.xml.write_attribute_fmt("href", format_args!("#{id}"));
self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}"));
}
}
}
Destination::Position(_) => {
// TODO: Links on the same page could be supported.
}
Destination::Url(url) => {
self.xml.write_attribute("href", url.as_str());
self.xml.write_attribute("xlink:href", url.as_str());
}
}
self.xml.start_element("rect");
self.xml
.write_attribute_fmt("width", format_args!("{}", size.x.to_pt()));
self.xml
.write_attribute_fmt("height", format_args!("{}", size.y.to_pt()));
self.xml.write_attribute("fill", "transparent");
self.xml.write_attribute("stroke", "none");
self.xml.end_element();
self.xml.end_element();
}
/// Renders a linkable point that can be used to link into an HTML frame.
fn render_link_point(&mut self, pos: Point, id: &str) {
self.xml.start_element("g");
self.xml.write_attribute("id", id);
self.xml.write_attribute_fmt(
"transform",
format_args!("translate({} {})", pos.x.to_pt(), pos.y.to_pt()),
);
self.xml.end_element();
}
/// Finalize the SVG file. This must be called after all rendering is done.
fn finalize(mut self) -> String {
self.write_glyph_defs();

View File

@ -15,7 +15,7 @@ use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
/// Smaller values could be interesting for optimization.
const CONIC_SEGMENT: usize = 360;
impl SVGRenderer {
impl SVGRenderer<'_> {
/// Render a frame to a string.
pub(super) fn render_tiling_frame(
&mut self,

View File

@ -8,7 +8,7 @@ use typst_library::visualize::{
use crate::paint::ColorEncode;
use crate::{SVGRenderer, State, SvgPathBuilder};
impl SVGRenderer {
impl SVGRenderer<'_> {
/// Render a shape element.
pub(super) fn render_shape(&mut self, state: State, shape: &Shape) {
self.xml.start_element("path");

View File

@ -13,7 +13,7 @@ use typst_utils::hash128;
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
impl SVGRenderer {
impl SVGRenderer<'_> {
/// Render a text item. The text is rendered as a group of glyphs. We will
/// try to render the text as SVG first, then bitmap, then outline. If none
/// of them works, we will skip the text.

View File

@ -65,6 +65,23 @@ impl PicoStr {
id
}
/// Try to create a `PicoStr`, but don't intern it if it does not exist yet.
///
/// This is useful to try to compare against one or multiple `PicoStr`
/// without interning needlessly.
///
/// Will always return `Some(_)` if the string can be represented inline.
pub fn get(string: &str) -> Option<PicoStr> {
// Try to use bitcode or exception representations.
if let Ok(value) = PicoStr::try_constant(string) {
return Some(value);
}
// Try to find an existing entry that we can reuse.
let interner = INTERNER.read().unwrap();
interner.seen.get(string).copied()
}
/// Creates a compile-time constant `PicoStr`.
///
/// Should only be used in const contexts because it can panic.

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<svg class="typst-frame" style="overflow: visible; width: 4.5em; height: 3em;" viewBox="0 0 45 30" width="45pt" height="30pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 30 L 45 30 L 45 0 Z "/></g><g transform="translate(0 0)"><a href="#intro" xlink:href="#intro"><rect width="45" height="30" fill="transparent" stroke="none"/></a></g></g></svg>
<h2 id="intro">1 Introduction</h2>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h2>Frame 1</h2>
<svg class="typst-frame" style="overflow: visible; width: 20em; height: 50em;" viewBox="0 0 200 500" width="200pt" height="500pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(0 0)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 500 L 200 500 L 200 0 Z "/></g><g transform="translate(75 100)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="#39cccc" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(0 0)"><a href="#f1" xlink:href="#f1"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g><g transform="translate(20 0)"><path class="typst-shape" fill="#000000" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(20 0)"><a href="#text" xlink:href="#text"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g><g transform="translate(40 0)"><path class="typst-shape" fill="#ffdc00" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g><g transform="translate(40 0)"><a href="#f2" xlink:href="#f2"><rect width="10" height="10" fill="transparent" stroke="none"/></a></g></g></g></g><g transform="translate(95 200)"><path class="typst-shape" fill="#39cccc" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g></g></g></g></g><g id="f1" transform="translate(95 200)"/></svg>
<h2 id="text">Text</h2>
<p><a href="#f1">Go to teal square</a></p>
<h2>Frame 2</h2>
<svg class="typst-frame" style="overflow: visible; width: 20em; height: 50em;" viewBox="0 0 200 500" width="200pt" height="500pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"><g><g transform="translate(0 0)"><g class="typst-group"><g><g transform="translate(-0 -0)"><path class="typst-shape" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" d="M 0 0 L 0 500 L 200 500 L 200 0 Z "/></g><g transform="translate(95 100)"><path class="typst-shape" fill="#ffdc00" fill-rule="nonzero" d="M 0 0 L 0 10 L 10 10 L 10 0 Z "/></g></g></g></g></g><g id="f2" transform="translate(95 100)"/></svg>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p id="loc-1"><a href="#loc-1">Go</a></p>
</body>
</html>

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<ul>
<li><a href="#t1">Go</a></li>
<li><a href="#t2">Go</a></li>
<li><a href="#t3">Go</a></li>
<li><a href="#t4">Go</a></li>
<li><a href="#t5">Go</a></li>
<li><a href="#t6">Go</a></li>
<li><a href="#t7">Go</a></li>
<li><a href="#t8">Go</a></li>
</ul>
<p id="t1">Hi</p>
<p id="t2">Hi there</p>
<p>See <span id="t4">it</span></p>
<p>See <span id="t5">it</span> here</p>
<p>See <span id="t6">a</span> <strong>b</strong></p>
<p>See <strong id="t3">a <em>b</em></strong></p>
<p>See</p>
<span id="t7"></span>
<p>See</p>
<span id="t8"></span>
</body>
</html>

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div><span id="this">This</span></div>
<p><a href="#this">Go</a></p>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h2 id="loc-1">A</h2>
<h2 id="loc-3">B</h2>
<h2 id="loc">C</h2>
<h2 id="loc-2">D</h2>
<h2 id="lib-1">E</h2>
<h2 id="lib-3">F</h2>
<h2 id="lib-2">G</h2>
<h2 id="hi">H</h2>
<h2 id="hi-2-1">I</h2>
<h2 id="hi-2-2">J</h2>
<ul>
<li><a href="#loc-1">A</a></li>
<li><a href="#loc-3">B</a></li>
<li><a href="#loc">C</a></li>
<li><a href="#loc-2">D</a></li>
<li><a href="#lib-1">E</a></li>
<li><a href="#lib-3">F</a></li>
<li><a href="#lib-2">G</a></li>
<li><a href="#hi">H</a></li>
<li><a href="#hi-2-1">I</a></li>
<li><a href="#hi-2-2">J</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<span id="a"></span><span id="b"></span>
<p>Hi</p>
<p><a href="#a">A</a> <a href="#b">B</a> <a href="#a">C</a></p>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h2 id="intro">1. Introduction</h2>
<p>See <a href="#setup">Section 1.1</a>.</p>
<h3 id="setup">1.1. Setup</h3>
<p>As seen in <a href="#intro">Section 1</a>, we proceed.</p>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 B

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
tests/ref/math-op-font.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -28,3 +28,19 @@ $ bold(op("bold", limits: #true))_x y $
--- math-non-math-content ---
// With non-text content
$ op(#underline[ul]) a $
--- math-op-font ---
// Test with different font.
#let colim = math.op(
text(font: "IBM Plex Sans", weight: "regular", size: 0.8em)[colim],
limits: true,
)
$ colim_(x -> 0) inline(colim_(x -> 0)) $
--- math-op-set-font ---
// Test setting font.
#let lig = math.op("fi")
#let test = $sin(x) lim_(x -> oo) lig_1(X)$
#test
#show math.op: set text(font: "Libertinus Serif")
#test

View File

@ -2,7 +2,7 @@
--- math-font-fallback ---
// Test font fallback.
$ and 🏳🌈 $
$ and "よ" and 🏳🌈 $
--- math-text-color ---
// Test text properties.
@ -17,6 +17,21 @@ $ nothing $
$ "hi ∅ hey" $
$ sum_(i in NN) 1 + i $
--- math-font-features-switch ---
#let scr(it) = text(features: ("ss01",), $cal(it)$)
$cal(P)_i != scr(P)_i$, $cal(bold(I))_l != bold(scr(I))_l$
$ product.co_(B in scr(B))^(B in scr(bold(B))) cal(B)(X) $
--- math-font-covers ---
#show math.equation: set text(
font: (
// Ignore that this regex actually misses some of the script glyphs...
(name: "XITS Math", covers: regex("[\u{1D49C}-\u{1D503}]")),
),
features: ("ss01",),
)
$ cal(P)_i (X) * cal(C)_1 $
--- math-optical-size-nested-scripts ---
// Test transition from script to scriptscript.
#[

View File

@ -56,6 +56,10 @@ a + 0.
enum.item(5)[Fifth]
)
--- enum-item-number-optional ---
#enum.item[First]
#enum.item[Second]
--- enum-numbering-pattern ---
// Test numbering pattern.
#set enum(numbering: "(1.a.*)")
@ -217,7 +221,7 @@ a + 0.
--- issue-2530-enum-item-panic ---
// Enum item (pre-emptive)
#enum.item(none)[Hello]
#enum.item(auto)[Hello]
#enum.item(17)[Hello]
--- issue-5503-enum-in-align ---

View File

@ -66,6 +66,117 @@ My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
Text <hey>
#link(<hey>)[Go to text.]
--- link-html-id-attach html ---
// Tests how IDs and, if necessary, spans, are added to the DOM to support
// links.
#for i in range(1, 9) {
list.item(link(label("t" + str(i)), [Go]))
}
// Text at start of paragraph
Hi <t1>
// Text at start of paragraph + more text
Hi <t2> there
// Text in the middle of paragraph
See #[it <t4>]
// Text in the middle of paragraph + more text
See #[it <t5>] here
// Text + more elements
See #[a *b*] <t6>
// Element
See *a _b_* <t3>
// Nothing
See #[] <t7>
// Nothing 2
See #metadata(none) <t8>
--- link-html-label-disambiguation html ---
// Tests automatic ID generation for labelled elements.
#[= A] #label("%") // not reusable => loc-1
= B <1> // not reusable => loc-3 (loc-2 exists)
= C <loc> // reusable, unique => loc
= D <loc-2> // reusable, unique => loc-2
= E <lib> // reusable, not unique => lib-1
= F <lib> // reusable, not unique => lib-3 (lib-2 exists)
= G <lib-2> // reusable, unique => lib-2
= H <hi> // reusable, unique => hi
= I <hi-2> // reusable, not unique => hi-2-1
= J <hi-2> // reusable, not unique => hi-2-2
#context for it in query(heading) {
list.item(link(it.location(), it.body))
}
--- link-html-id-existing html ---
// Test that linking reuses the existing ID, if any.
#html.div[
#html.span(id: "this")[This] <other>
]
#link(<other>)[Go]
--- link-html-here html ---
#context link(here())[Go]
--- link-html-nested-empty html ---
#[#metadata(none) <a> #metadata(none) <b> Hi] <c>
#link(<a>)[A] // creates empty span
#link(<b>)[B] // creates second empty span
#link(<c>)[C] // links to #a because the generated span is contained in it
--- link-html-frame html ---
= Frame 1
#html.frame(block(
stroke: 1pt,
width: 200pt,
height: 500pt,
)[
#place(center, dy: 100pt, stack(
dir: ltr,
spacing: 10pt,
link(<f1>, square(size: 10pt, fill: teal)),
link(<text>, square(size: 10pt, fill: black)),
link(<f2>, square(size: 10pt, fill: yellow)),
))
#place(center, dy: 200pt)[
#square(size: 10pt, fill: teal) <f1>
]
])
= Text <text>
#link(<f1>)[Go to teal square]
= Frame 2
#html.frame(block(
stroke: 1pt,
width: 200pt,
height: 500pt,
)[
#place(center, dy: 100pt)[
#square(size: 10pt, fill: yellow) <f2>
]
])
--- link-html-frame-ref html ---
// Test that reference links work in `html.frame`. Currently, references (and a
// few other elements) do not internally use `LinkElem`s, so they trigger a
// slightly different code path; see `typst-html/src/link.rs`. The text show
// rule is only there to keep the output small.
#set heading(numbering: "1")
#show "Section" + sym.space.nobreak + "1": rect()
#html.frame[@intro]
= Introduction <intro>
--- link-to-label-missing ---
// Error: 2-20 label `<hey>` does not exist in the document
#link(<hey>)[Nope.]

View File

@ -1,6 +1,6 @@
// Test references.
--- ref-basic ---
--- ref-basic render html ---
#set heading(numbering: "1.")
= Introduction <intro>

View File

@ -135,6 +135,34 @@ I
The number 123.
--- text-font-covers-repeat ---
// Repeatedly use the same font.
#set text(font: (
(name: "Libertinus Serif", covers: regex("[0-9]")),
"Libertinus Serif"
))
The number 123.
--- text-font-covers-riffle ---
// Repeatedly use two fonts alternately.
#set text(font: (
(name: "Noto Color Emoji", covers: regex("[🔗⛓‍💥]")),
(name: "Twitter Color Emoji", covers: regex("[^🖥️]")),
"Noto Color Emoji",
))
🔗⛓‍💥🖥️🔑
// The above should be the same as:
#{
text(font: "Noto Color Emoji", "🔗⛓‍💥🖥️")
text(font: "Twitter Color Emoji", "🔑")
}
// but not:
#text(font: "Twitter Color Emoji", "🔗⛓‍💥🖥️🔑")
--- text-font-covers-bad-1 ---
// Error: 17-59 coverage regex may only use dot, letters, and character classes
// Hint: 17-59 the regex is applied to each letter individually