Compare commits

...

8 Commits

Author SHA1 Message Date
cAttte
46d57b00b5
Warn when using variable fonts (#6425) 2025-06-11 14:42:57 +00:00
Laurenz
ada90fdd87 Fix untidy Cargo.lock 2025-06-11 16:44:13 +02:00
Y.D.X.
19325d5027
Warning when watching stdin (#6381)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-11 14:21:05 +00:00
Lachlan Kermode
1f5846ce24
Render #super as <sup>, #sub as <sub> in HTML (#6422) 2025-06-11 14:07:25 +00:00
T0mstone
d7e0c52dd5
Use codex::ModifierSet (#6159)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-11 13:28:03 +00:00
Tobias Schmitz
d1c7757da8
Fix panic when test source is not found in world (#6428) 2025-06-11 10:19:41 +00:00
Andrew Voynov
71032c8349
List both YAML file extensions in bibliography docs (#6426) 2025-06-11 10:04:10 +00:00
Malo
3a6d5fd6b2
Do not force math.mid elements to have the Large math class (#5980) 2025-06-11 08:29:38 +00:00
21 changed files with 184 additions and 126 deletions

4
Cargo.lock generated
View File

@ -413,8 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe"
checksum = "724d27a0ee38b700e5e164350e79aba601a0db673ac47fce1cb74c3e38864036"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -2921,6 +2920,7 @@ name = "typst-docs"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"clap", "clap",
"codex",
"ecow", "ecow",
"heck", "heck",
"pulldown-cmark", "pulldown-cmark",

View File

@ -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 = "0.1.1" codex = { git = "https://github.com/typst/codex", rev = "56eb217" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"

View File

@ -10,11 +10,12 @@ use codespan_reporting::term::{self, termcolor};
use ecow::eco_format; use ecow::eco_format;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _}; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _};
use same_file::is_same_file; use same_file::is_same_file;
use typst::diag::{bail, StrResult}; use typst::diag::{bail, warning, StrResult};
use typst::syntax::Span;
use typst::utils::format_duration; use typst::utils::format_duration;
use crate::args::{Input, Output, WatchCommand}; use crate::args::{Input, Output, WatchCommand};
use crate::compile::{compile_once, CompileConfig}; use crate::compile::{compile_once, print_diagnostics, CompileConfig};
use crate::timings::Timer; use crate::timings::Timer;
use crate::world::{SystemWorld, WorldCreationError}; use crate::world::{SystemWorld, WorldCreationError};
use crate::{print_error, terminal}; use crate::{print_error, terminal};
@ -55,6 +56,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation. // Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??; timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Print warning when trying to watch stdin.
if matches!(&config.input, Input::Stdin) {
warn_watching_std(&world, &config)?;
}
// Recompile whenever something relevant happens. // Recompile whenever something relevant happens.
loop { loop {
// Watch all dependencies of the most recent compilation. // Watch all dependencies of the most recent compilation.
@ -332,3 +338,15 @@ impl Status {
} }
} }
} }
/// Emits a warning when trying to watch stdin.
fn warn_watching_std(world: &SystemWorld, config: &CompileConfig) -> StrResult<()> {
let warning = warning!(
Span::detached(),
"cannot watch changes for stdin";
hint: "to recompile on changes, watch a regular file instead";
hint: "to compile once and exit, please use `typst compile` instead"
);
print_diagnostics(world, &[], &[warning], config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))
}

View File

@ -45,12 +45,12 @@ pub fn layout_lr(
// Scale up fragments at both ends. // Scale up fragments at both ends.
match inner_fragments { match inner_fragments {
[one] => scale(ctx, one, relative_to, height, None), [one] => scale_if_delimiter(ctx, one, relative_to, height, None),
[first, .., last] => { [first, .., last] => {
scale(ctx, first, relative_to, height, Some(MathClass::Opening)); scale_if_delimiter(ctx, first, relative_to, height, Some(MathClass::Opening));
scale(ctx, last, relative_to, height, Some(MathClass::Closing)); scale_if_delimiter(ctx, last, relative_to, height, Some(MathClass::Closing));
} }
_ => {} [] => {}
} }
// Handle MathFragment::Glyph fragments that should be scaled up. // Handle MathFragment::Glyph fragments that should be scaled up.
@ -58,7 +58,7 @@ pub fn layout_lr(
if let MathFragment::Glyph(ref mut glyph) = fragment { if let MathFragment::Glyph(ref mut glyph) = fragment {
if glyph.mid_stretched == Some(false) { if glyph.mid_stretched == Some(false) {
glyph.mid_stretched = Some(true); glyph.mid_stretched = Some(true);
scale(ctx, fragment, relative_to, height, Some(MathClass::Large)); scale(ctx, fragment, relative_to, height);
} }
} }
} }
@ -97,7 +97,7 @@ pub fn layout_mid(
for fragment in &mut fragments { for fragment in &mut fragments {
if let MathFragment::Glyph(ref mut glyph) = fragment { if let MathFragment::Glyph(ref mut glyph) = fragment {
glyph.mid_stretched = Some(false); glyph.mid_stretched = Some(false);
glyph.class = MathClass::Fence; glyph.class = MathClass::Relation;
} }
} }
@ -105,8 +105,12 @@ pub fn layout_mid(
Ok(()) Ok(())
} }
/// Scale a math fragment to a height. /// Scales a math fragment to a height if it has the class Opening, Closing, or
fn scale( /// Fence.
///
/// In case `apply` is `Some(class)`, `class` will be applied to the fragment if
/// it is a delimiter, in a way that cannot be overridden by the user.
fn scale_if_delimiter(
ctx: &mut MathContext, ctx: &mut MathContext,
fragment: &mut MathFragment, fragment: &mut MathFragment,
relative_to: Abs, relative_to: Abs,
@ -117,20 +121,23 @@ fn scale(
fragment.class(), fragment.class(),
MathClass::Opening | MathClass::Closing | MathClass::Fence MathClass::Opening | MathClass::Closing | MathClass::Fence
) { ) {
// This unwrap doesn't really matter. If it is None, then the fragment scale(ctx, fragment, relative_to, height);
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(
ctx,
fragment,
Some(Axis::Y),
Some(relative_to),
height,
short_fall,
);
if let Some(class) = apply { if let Some(class) = apply {
fragment.set_class(class); fragment.set_class(class);
} }
} }
} }
/// Scales a math fragment to a height.
fn scale(
ctx: &mut MathContext,
fragment: &mut MathFragment,
relative_to: Abs,
height: Rel<Abs>,
) {
// This unwrap doesn't really matter. If it is None, then the fragment
// won't be stretchable anyways.
let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default());
stretch_fragment(ctx, fragment, Some(Axis::Y), Some(relative_to), height, short_fall);
}

View File

@ -1,8 +1,8 @@
use std::cmp::Reverse;
use std::collections::{BTreeSet, HashMap}; use std::collections::{BTreeSet, HashMap};
use std::fmt::{self, Debug, Display, Formatter, Write}; use std::fmt::{self, Debug, Display, Formatter, Write};
use std::sync::Arc; use std::sync::Arc;
use codex::ModifierSet;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::{is_ident, Span, Spanned}; use typst_syntax::{is_ident, Span, Spanned};
@ -54,18 +54,18 @@ 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 [(&'static str, char)]), Complex(&'static [(ModifierSet<&'static str>, char)]),
/// 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, EcoString)>), Modified(Arc<(List, ModifierSet<EcoString>)>),
} }
/// A collection of symbols. /// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum List { enum List {
Static(&'static [(&'static str, char)]), Static(&'static [(ModifierSet<&'static str>, char)]),
Runtime(Box<[(EcoString, char)]>), Runtime(Box<[(ModifierSet<EcoString>, char)]>),
} }
impl Symbol { impl Symbol {
@ -76,24 +76,26 @@ 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 [(&'static str, char)]) -> Self { pub const fn list(list: &'static [(ModifierSet<&'static str>, char)]) -> 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<[(EcoString, char)]>) -> Self { pub fn runtime(list: Box<[(ModifierSet<EcoString>, char)]>) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Modified(Arc::new((List::Runtime(list), EcoString::new())))) Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
} }
/// Get the symbol's character. /// Get the symbol's character.
pub fn get(&self) -> char { pub fn get(&self) -> char {
match &self.0 { match &self.0 {
Repr::Single(c) => *c, Repr::Single(c) => *c,
Repr::Complex(_) => find(self.variants(), "").unwrap(), Repr::Complex(_) => ModifierSet::<&'static str>::default()
Repr::Modified(arc) => find(self.variants(), &arc.1).unwrap(), .best_match_in(self.variants())
.unwrap(),
Repr::Modified(arc) => arc.1.best_match_in(self.variants()).unwrap(),
} }
} }
@ -128,16 +130,14 @@ 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, modifier: &str) -> StrResult<Self> {
if let Repr::Complex(list) = self.0 { if let Repr::Complex(list) = self.0 {
self.0 = Repr::Modified(Arc::new((List::Static(list), EcoString::new()))); self.0 =
Repr::Modified(Arc::new((List::Static(list), ModifierSet::default())));
} }
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);
if !modifiers.is_empty() { modifiers.insert_raw(modifier);
modifiers.push('.'); if modifiers.best_match_in(list.variants()).is_some() {
}
modifiers.push_str(modifier);
if find(list.variants(), modifiers).is_some() {
return Ok(self); return Ok(self);
} }
} }
@ -146,7 +146,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 = (&str, char)> { pub fn variants(&self) -> impl Iterator<Item = (ModifierSet<&str>, char)> {
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()),
@ -156,17 +156,15 @@ impl Symbol {
/// Possible modifiers. /// Possible modifiers.
pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ { pub fn modifiers(&self) -> impl Iterator<Item = &str> + '_ {
let mut set = BTreeSet::new();
let modifiers = match &self.0 { let modifiers = match &self.0 {
Repr::Modified(arc) => arc.1.as_str(), Repr::Modified(arc) => arc.1.as_deref(),
_ => "", _ => ModifierSet::default(),
}; };
for modifier in self.variants().flat_map(|(name, _)| name.split('.')) { self.variants()
if !modifier.is_empty() && !contained(modifiers, modifier) { .flat_map(|(m, _)| m)
set.insert(modifier); .filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier))
} .collect::<BTreeSet<_>>()
} .into_iter()
set.into_iter()
} }
} }
@ -256,7 +254,10 @@ impl Symbol {
seen.insert(hash, i); seen.insert(hash, i);
} }
let list = variants.into_iter().map(|s| (s.v.0, s.v.1)).collect(); let list = variants
.into_iter()
.map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1))
.collect();
Ok(Symbol::runtime(list)) Ok(Symbol::runtime(list))
} }
} }
@ -291,14 +292,23 @@ impl crate::foundations::Repr for Symbol {
match &self.0 { match &self.0 {
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c), Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
Repr::Complex(variants) => { Repr::Complex(variants) => {
eco_format!("symbol{}", repr_variants(variants.iter().copied(), "")) eco_format!(
"symbol{}",
repr_variants(variants.iter().copied(), ModifierSet::default())
)
} }
Repr::Modified(arc) => { Repr::Modified(arc) => {
let (list, modifiers) = arc.as_ref(); let (list, modifiers) = arc.as_ref();
if modifiers.is_empty() { if modifiers.is_empty() {
eco_format!("symbol{}", repr_variants(list.variants(), "")) eco_format!(
"symbol{}",
repr_variants(list.variants(), ModifierSet::default())
)
} else { } else {
eco_format!("symbol{}", repr_variants(list.variants(), modifiers)) eco_format!(
"symbol{}",
repr_variants(list.variants(), modifiers.as_deref())
)
} }
} }
} }
@ -306,24 +316,24 @@ impl crate::foundations::Repr for Symbol {
} }
fn repr_variants<'a>( fn repr_variants<'a>(
variants: impl Iterator<Item = (&'a str, char)>, variants: impl Iterator<Item = (ModifierSet<&'a str>, char)>,
applied_modifiers: &str, applied_modifiers: ModifierSet<&str>,
) -> String { ) -> String {
crate::foundations::repr::pretty_array_like( crate::foundations::repr::pretty_array_like(
&variants &variants
.filter(|(variant, _)| { .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.
parts(applied_modifiers).all(|am| variant.split('.').any(|m| m == am)) applied_modifiers.iter().all(|am| modifiers.contains(am))
}) })
.map(|(variant, c)| { .map(|(modifiers, c)| {
let trimmed_variant = variant let trimmed_modifiers =
.split('.') modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
.filter(|&m| parts(applied_modifiers).all(|am| m != am)); if trimmed_modifiers.clone().all(|m| m.is_empty()) {
if trimmed_variant.clone().all(|m| m.is_empty()) {
eco_format!("\"{c}\"") eco_format!("\"{c}\"")
} else { } else {
let trimmed_modifiers = trimmed_variant.collect::<Vec<_>>().join("."); let trimmed_modifiers =
trimmed_modifiers.collect::<Vec<_>>().join(".");
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c) eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
} }
}) })
@ -369,67 +379,22 @@ 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, (&'static str, char)>), Static(std::slice::Iter<'static, (ModifierSet<&'static str>, char)>),
Runtime(std::slice::Iter<'a, (EcoString, char)>), Runtime(std::slice::Iter<'a, (ModifierSet<EcoString>, char)>),
} }
impl<'a> Iterator for Variants<'a> { impl<'a> Iterator for Variants<'a> {
type Item = (&'a str, char); type Item = (ModifierSet<&'a str>, char);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
Self::Single(iter) => Some(("", iter.next()?)), Self::Single(iter) => Some((ModifierSet::default(), iter.next()?)),
Self::Static(list) => list.next().copied(), Self::Static(list) => list.next().copied(),
Self::Runtime(list) => list.next().map(|(s, c)| (s.as_str(), *c)), Self::Runtime(list) => list.next().map(|(m, c)| (m.as_deref(), *c)),
} }
} }
} }
/// Find the best symbol from the list.
fn find<'a>(
variants: impl Iterator<Item = (&'a str, char)>,
modifiers: &str,
) -> Option<char> {
let mut best = None;
let mut best_score = None;
// Find the best table entry with this name.
'outer: for candidate in variants {
for modifier in parts(modifiers) {
if !contained(candidate.0, modifier) {
continue 'outer;
}
}
let mut matching = 0;
let mut total = 0;
for modifier in parts(candidate.0) {
if contained(modifiers, modifier) {
matching += 1;
}
total += 1;
}
let score = (matching, Reverse(total));
if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1);
best_score = Some(score);
}
}
best
}
/// Split a modifier list into its parts.
fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
modifiers.split('.').filter(|s| !s.is_empty())
}
/// Whether the modifier string contains the modifier `m`.
fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m)
}
/// A single character. /// A single character.
#[elem(Repr, PlainText)] #[elem(Repr, PlainText)]
pub struct SymbolElem { pub struct SymbolElem {

View File

@ -51,8 +51,8 @@ use crate::World;
/// You can create a new bibliography by calling this function with a path /// You can create a new bibliography by calling this function with a path
/// to a bibliography file in either one of two formats: /// to a bibliography file in either one of two formats:
/// ///
/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format /// - A Hayagriva `.yaml`/`.yml` file. Hayagriva is a new bibliography
/// designed for use with Typst. Visit its /// file format designed for use with Typst. Visit its
/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md) /// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)
/// for more details. /// for more details.
/// - A BibLaTeX `.bib` file. /// - A BibLaTeX `.bib` file.

View File

@ -196,6 +196,8 @@ bitflags::bitflags! {
const SERIF = 1 << 1; const SERIF = 1 << 1;
/// Font face has a MATH table /// Font face has a MATH table
const MATH = 1 << 2; const MATH = 1 << 2;
/// Font face has an fvar table
const VARIABLE = 1 << 3;
} }
} }
@ -275,6 +277,7 @@ impl FontInfo {
let mut flags = FontFlags::empty(); let mut flags = FontFlags::empty();
flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); flags.set(FontFlags::MONOSPACE, ttf.is_monospaced());
flags.set(FontFlags::MATH, ttf.tables().math.is_some()); flags.set(FontFlags::MATH, ttf.tables().math.is_some());
flags.set(FontFlags::VARIABLE, ttf.is_variable());
// Determine whether this is a serif or sans-serif font. // Determine whether this is a serif or sans-serif font.
if let Some(panose) = ttf if let Some(panose) = ttf

View File

@ -1412,12 +1412,24 @@ pub fn is_default_ignorable(c: char) -> bool {
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) { fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
let book = engine.world.book(); let book = engine.world.book();
for family in &list.v { for family in &list.v {
if !book.contains_family(family.as_str()) { match book.select_family(family.as_str()).next() {
engine.sink.warn(warning!( Some(index) => {
if book
.info(index)
.is_some_and(|x| x.flags.contains(FontFlags::VARIABLE))
{
engine.sink.warn(warning!(
list.span,
"variable fonts are not currently supported and may render incorrectly";
hint: "try installing a static version of \"{}\" instead", family.as_str()
))
}
}
None => engine.sink.warn(warning!(
list.span, list.span,
"unknown font family: {}", "unknown font family: {}",
family.as_str(), family.as_str(),
)); )),
} }
} }
} }

View File

@ -2,7 +2,10 @@ use ecow::EcoString;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain}; use crate::foundations::{
elem, Content, NativeElement, Packed, SequenceElem, Show, StyleChain, TargetElem,
};
use crate::html::{tag, HtmlElem};
use crate::layout::{Em, Length}; use crate::layout::{Em, Length};
use crate::text::{variant, SpaceElem, TextElem, TextSize}; use crate::text::{variant, SpaceElem, TextElem, TextSize};
use crate::World; use crate::World;
@ -52,6 +55,13 @@ impl Show for Packed<SubElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone(); let body = self.body.clone();
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::sub)
.with_body(Some(body))
.pack()
.spanned(self.span()));
}
if self.typographic(styles) { if self.typographic(styles) {
if let Some(text) = convert_script(&body, true) { if let Some(text) = convert_script(&body, true) {
if is_shapable(engine, &text, styles) { if is_shapable(engine, &text, styles) {
@ -111,6 +121,13 @@ impl Show for Packed<SuperElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone(); let body = self.body.clone();
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::sup)
.with_body(Some(body))
.pack()
.spanned(self.span()));
}
if self.typographic(styles) { if self.typographic(styles) {
if let Some(text) = convert_script(&body, false) { if let Some(text) = convert_script(&body, false) {
if is_shapable(engine, &text, styles) { if is_shapable(engine, &text, styles) {

View File

@ -22,6 +22,7 @@ typst-utils = { workspace = true }
typst-assets = { workspace = true, features = ["fonts"] } typst-assets = { workspace = true, features = ["fonts"] }
typst-dev-assets = { workspace = true } typst-dev-assets = { workspace = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
codex = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
heck = { workspace = true } heck = { workspace = true }
pulldown-cmark = { workspace = true } pulldown-cmark = { workspace = true }

View File

@ -712,11 +712,11 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
let mut list = vec![]; let mut list = vec![];
for (name, binding) in group.module().scope().iter() { for (name, binding) in group.module().scope().iter() {
let Value::Symbol(symbol) = binding.read() else { continue }; let Value::Symbol(symbol) = binding.read() else { continue };
let complete = |variant: &str| { let complete = |variant: codex::ModifierSet<&str>| {
if variant.is_empty() { if variant.is_empty() {
name.clone() name.clone()
} else { } else {
eco_format!("{}.{}", name, variant) eco_format!("{}.{}", name, variant.as_str())
} }
}; };

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>1<sup>st</sup>, 2<sup>nd</sup>, 3<sup>rd</sup>.</p>
<p>log<sub>2</sub>, log<sub>3</sub>, log<sub>variable</sub>.</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 B

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -10,7 +10,7 @@ use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
use typst::visualize::Color; use typst::visualize::Color;
use typst::{Document, WorldExt}; use typst::{Document, WorldExt};
use typst_pdf::PdfOptions; use typst_pdf::PdfOptions;
use typst_syntax::FileId; use typst_syntax::{FileId, Lines};
use crate::collect::{Attr, FileSize, NoteKind, Test}; use crate::collect::{Attr, FileSize, NoteKind, Test};
use crate::logger::TestResult; use crate::logger::TestResult;
@ -292,7 +292,7 @@ impl<'a> Runner<'a> {
return "(empty)".into(); return "(empty)".into();
} }
let lines = self.world.lookup(file); let lines = self.lookup(file);
lines.text()[range.clone()].replace('\n', "\\n").replace('\r', "\\r") lines.text()[range.clone()].replace('\n', "\\n").replace('\r', "\\r")
} }
@ -318,7 +318,7 @@ impl<'a> Runner<'a> {
/// Display a position as a line:column pair. /// Display a position as a line:column pair.
fn format_pos(&self, file: FileId, pos: usize) -> String { fn format_pos(&self, file: FileId, pos: usize) -> String {
let lines = self.world.lookup(file); let lines = self.lookup(file);
let res = lines.byte_to_line_column(pos).map(|(line, col)| (line + 1, col + 1)); let res = lines.byte_to_line_column(pos).map(|(line, col)| (line + 1, col + 1));
let Some((line, col)) = res else { let Some((line, col)) = res else {
@ -331,6 +331,15 @@ impl<'a> Runner<'a> {
format!("{line}:{col}") format!("{line}:{col}")
} }
} }
#[track_caller]
fn lookup(&self, file: FileId) -> Lines<String> {
if self.test.source.id() == file {
self.test.source.lines().clone()
} else {
self.world.lookup(file)
}
}
} }
/// An output type we can test. /// An output type we can test.

View File

@ -88,7 +88,7 @@ impl TestWorld {
/// Lookup line metadata for a file by id. /// Lookup line metadata for a file by id.
#[track_caller] #[track_caller]
pub fn lookup(&self, id: FileId) -> Lines<String> { pub(crate) fn lookup(&self, id: FileId) -> Lines<String> {
self.slot(id, |slot| { self.slot(id, |slot| {
if let Some(source) = slot.source.get() { if let Some(source) = slot.source.get() {
let source = source.as_ref().expect("file is not valid"); let source = source.as_ref().expect("file is not valid");

View File

@ -77,6 +77,14 @@ $ lr(body) quad
lr(size: #1em, body) quad lr(size: #1em, body) quad
lr(size: #(1em+20%), body) $ lr(size: #(1em+20%), body) $
--- math-lr-mid-class ---
// Test that `mid` creates a Relation, but that can be overridden.
$ (a | b) $
$ (a mid(|) b) $
$ (a class("unary", |) b) $
$ (a class("unary", mid(|)) b) $
$ (a mid(class("unary", |)) b) $
--- math-lr-unbalanced --- --- math-lr-unbalanced ---
// Test unbalanced delimiters. // Test unbalanced delimiters.
$ 1/(2 (x) $ $ 1/(2 (x) $

View File

@ -104,9 +104,11 @@
("long", "⟹"), ("long", "⟹"),
("long.bar", "⟾"), ("long.bar", "⟾"),
("not", "⇏"), ("not", "⇏"),
("struck", "⤃"),
("l", "⇔"), ("l", "⇔"),
("l.long", "⟺"), ("l.long", "⟺"),
("l.not", "⇎"), ("l.not", "⇎"),
("l.struck", "⤄"),
) )
```.text, ```.text,
) )

View File

@ -17,3 +17,8 @@ n#super[1], n#sub[2], ... n#super[N]
#underline[The claim#super[\[4\]]] has been disputed. \ #underline[The claim#super[\[4\]]] has been disputed. \
The claim#super[#underline[\[4\]]] has been disputed. \ The claim#super[#underline[\[4\]]] has been disputed. \
It really has been#super(box(text(baseline: 0pt, underline[\[4\]]))) \ It really has been#super(box(text(baseline: 0pt, underline[\[4\]]))) \
--- basic-sup-sub html ---
1#super[st], 2#super[nd], 3#super[rd].
log#sub[2], log#sub[3], log#sub[variable].