Merge branch 'main' into opentype-text-scripts-t3

# Conflicts:
#	crates/typst-layout/src/inline/line.rs
This commit is contained in:
Malo 2025-06-29 14:46:32 +01:00
commit 05f276c933
47 changed files with 327 additions and 130 deletions

4
Cargo.lock generated
View File

@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -2911,7 +2911,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -219,7 +219,7 @@ fn collect_items<'a>(
// Add fallback text to expand the line height, if necessary. // Add fallback text to expand the line height, if necessary.
if !items.iter().any(|item| matches!(item, Item::Text(_))) { if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback { if let Some(fallback) = fallback {
items.push(fallback); items.push(fallback, usize::MAX);
} }
} }
@ -270,10 +270,10 @@ fn collect_range<'a>(
items: &mut Items<'a>, items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>, fallback: &mut Option<ItemEntry<'a>>,
) { ) {
for (subrange, item) in p.slice(range.clone()) { for (idx, (subrange, item)) in p.slice(range.clone()).enumerate() {
// All non-text items are just kept, they can't be split. // All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else { let Item::Text(shaped) = item else {
items.push(item); items.push(item, idx);
continue; continue;
}; };
@ -293,10 +293,10 @@ fn collect_range<'a>(
} else if split { } else if split {
// When the item is split in half, reshape it. // When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced); let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped)); items.push(Item::Text(reshaped), idx);
} else { } else {
// When the item is fully contained, just keep it. // When the item is fully contained, just keep it.
items.push(item); items.push(item, idx);
} }
} }
} }
@ -520,16 +520,16 @@ pub fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for item in line.items.iter() { for &(idx, ref item) in line.items.indexed_iter() {
let mut push = |offset: &mut Abs, frame: Frame| { let mut push = |offset: &mut Abs, frame: Frame, idx: usize| {
let width = frame.width(); let width = frame.width();
top.set_max(frame.baseline()); top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline()); bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame)); frames.push((*offset, frame, idx));
*offset += width; *offset += width;
}; };
match item { match &**item {
Item::Absolute(v, _) => { Item::Absolute(v, _) => {
offset += *v; offset += *v;
} }
@ -541,7 +541,7 @@ pub fn commit(
layout_box(elem, engine, loc.relayout(), styles, region) layout_box(elem, engine, loc.relayout(), styles, region)
})?; })?;
apply_shift(&engine.world, &mut frame, *styles); apply_shift(&engine.world, &mut frame, *styles);
push(&mut offset, frame); push(&mut offset, frame, idx);
} else { } else {
offset += amount; offset += amount;
} }
@ -553,15 +553,15 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame); push(&mut offset, frame, idx);
} }
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone()); push(&mut offset, frame.clone(), idx);
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame)); frames.push((offset, frame, idx));
} }
Item::Skip(_) => {} Item::Skip(_) => {}
} }
@ -580,8 +580,13 @@ pub fn commit(
add_par_line_marker(&mut output, marker, engine, locator, top); add_par_line_marker(&mut output, marker, engine, locator, top);
} }
// Ensure that the final frame's items are in logical order rather than in
// visual order. This is important because it affects the order of elements
// during introspection and thus things like counters.
frames.sort_unstable_by_key(|(_, _, idx)| *idx);
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame, _) in frames {
let x = offset + p.config.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
@ -648,7 +653,7 @@ fn overhang(c: char) -> f64 {
} }
/// A collection of owned or borrowed inline items. /// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<ItemEntry<'a>>); pub struct Items<'a>(Vec<(usize, ItemEntry<'a>)>);
impl<'a> Items<'a> { impl<'a> Items<'a> {
/// Create empty items. /// Create empty items.
@ -657,33 +662,38 @@ impl<'a> Items<'a> {
} }
/// Push a new item. /// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) { pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) {
self.0.push(entry.into()); self.0.push((idx, entry.into()));
} }
/// Iterate over the items /// Iterate over the items.
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> { pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|item| &**item) self.0.iter().map(|(_, item)| &**item)
}
/// Iterate over the items with indices
pub fn indexed_iter(&self) -> impl Iterator<Item = &(usize, ItemEntry<'a>)> {
self.0.iter()
} }
/// Access the first item. /// Access the first item.
pub fn first(&self) -> Option<&Item<'a>> { pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item) self.0.first().map(|(_, item)| &**item)
} }
/// Access the last item. /// Access the last item.
pub fn last(&self) -> Option<&Item<'a>> { pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|item| &**item) self.0.last().map(|(_, item)| &**item)
} }
/// Access the first item mutably, if it is text. /// Access the first item mutably, if it is text.
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.text_mut() self.0.first_mut()?.1.text_mut()
} }
/// Access the last item mutably, if it is text. /// Access the last item mutably, if it is text.
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.text_mut() self.0.last_mut()?.1.text_mut()
} }
/// Reorder the items starting at the given index to RTL. /// Reorder the items starting at the given index to RTL.
@ -694,12 +704,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> { impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().collect()) Self(iter.into_iter().enumerate().collect())
} }
} }
impl<'a> Deref for Items<'a> { impl<'a> Deref for Items<'a> {
type Target = Vec<ItemEntry<'a>>; type Target = Vec<(usize, ItemEntry<'a>)>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -719,6 +729,10 @@ impl Debug for Items<'_> {
} }
/// A reference to or a boxed item. /// A reference to or a boxed item.
///
/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow],
/// but we box owned items since an [`Item`] is much bigger than
/// a box.
pub enum ItemEntry<'a> { pub enum ItemEntry<'a> {
Ref(&'a Item<'a>), Ref(&'a Item<'a>),
Box(Box<Item<'a>>), Box(Box<Item<'a>>),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -321,7 +321,11 @@ impl Bibliography {
for d in data.iter() { for d in data.iter() {
let library = decode_library(d)?; let library = decode_library(d)?;
for entry in library { for entry in library {
match map.entry(Label::new(PicoStr::intern(entry.key()))) { let label = Label::new(PicoStr::intern(entry.key()))
.ok_or("bibliography contains entry with empty key")
.at(d.source.span)?;
match map.entry(label) {
indexmap::map::Entry::Vacant(vacant) => { indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry); vacant.insert(entry);
} }
@ -592,7 +596,7 @@ impl Works {
/// Context for generating the bibliography. /// Context for generating the bibliography.
struct Generator<'a> { struct Generator<'a> {
/// The routines that is used to evaluate mathematical material in citations. /// The routines that are used to evaluate mathematical material in citations.
routines: &'a Routines, routines: &'a Routines,
/// The world that is used to evaluate mathematical material in citations. /// The world that is used to evaluate mathematical material in citations.
world: Tracked<'a, dyn World + 'a>, world: Tracked<'a, dyn World + 'a>,
@ -609,7 +613,7 @@ struct Generator<'a> {
/// Details about a group of merged citations. All citations are put into groups /// Details about a group of merged citations. All citations are put into groups
/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
/// Even single citations will be put into groups of length ones. /// Even single citations will be put into groups of length one.
struct GroupInfo { struct GroupInfo {
/// The group's location. /// The group's location.
location: Location, location: Location,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -206,7 +206,6 @@ label exists on the current page:
```typ ```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) >>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
#set page(header: context { #set page(header: context {
let page-counter =
let matches = query(<big-table>) let matches = query(<big-table>)
let current = counter(page).get() let current = counter(page).get()
let has-table = matches.any(m => let has-table = matches.any(m =>
@ -218,7 +217,7 @@ label exists on the current page:
#h(1fr) #h(1fr)
National Academy of Sciences National Academy of Sciences
] ]
})) })
#lorem(100) #lorem(100)
#pagebreak() #pagebreak()

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 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>
<p><s>Struck</s> <mark>Highlighted</mark> <span style="text-decoration: underline">Underlined</span> <span style="text-decoration: overline">Overlined</span></p>
<p><span style="text-decoration: overline"><span style="text-decoration: underline"><mark><s>Mixed</s></mark></span></span></p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -121,8 +121,8 @@ $a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.lo
--- math-attach-integral --- --- math-attach-integral ---
// Test default of scripts attachments on integrals at display size. // Test default of scripts attachments on integrals at display size.
$ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ $ integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $
$integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ $integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$
--- math-attach-large-operator --- --- math-attach-large-operator ---
// Test default of limit attachments on large operators at display size only. // Test default of limit attachments on large operators at display size only.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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