Merge ce7636b5bd8bfa5aff2490a973ff925f984e3a22 into 9b09146a6b5e936966ed7ee73bce9dd2df3810ae

This commit is contained in:
Malo 2025-05-06 17:44:20 +03:00 committed by GitHub
commit 05da6f3797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 335 additions and 163 deletions

View File

@ -206,7 +206,7 @@ pub fn collect<'a>(
}
InlineItem::Frame(mut frame) => {
frame.modify(&FrameModifiers::get_in(styles));
apply_baseline_shift(&mut frame, styles);
apply_shift(&engine.world, &mut frame, styles);
collector.push_item(Item::Frame(frame));
}
}
@ -221,7 +221,7 @@ pub fn collect<'a>(
let mut frame = layout_and_modify(styles, |styles| {
layout_box(elem, engine, loc, styles, region)
})?;
apply_baseline_shift(&mut frame, styles);
apply_shift(&engine.world, &mut frame, styles);
collector.push_item(Item::Frame(frame));
}
} else if let Some(elem) = child.to_packed::<TagElem>() {

View File

@ -5,7 +5,7 @@ use typst_library::engine::Engine;
use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem};
use typst_library::text::{variant, Lang, TextElem};
use typst_utils::Numeric;
use super::*;
@ -412,9 +412,30 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
}
}
/// Apply the current baseline shift to a frame.
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
/// Apply the current baseline shift and italic compensation to a frame.
pub fn apply_shift<'a>(
world: &Tracked<'a, dyn World + 'a>,
frame: &mut Frame,
styles: StyleChain,
) {
let mut baseline = TextElem::baseline_in(styles);
let mut compensation = Abs::zero();
if let Some(scripts) = TextElem::subperscript_in(styles) {
let font_metrics = TextElem::font_in(styles)
.into_iter()
.find_map(|family| {
world
.book()
.select(family.as_str(), variant(styles))
.and_then(|id| world.font(id))
})
.map_or(scripts.kind.default_metrics(), |f| {
scripts.kind.read_metrics(f.metrics())
});
baseline -= scripts.shift.unwrap_or(font_metrics.vertical_offset).resolve(styles);
compensation += font_metrics.horizontal_offset.resolve(styles);
}
frame.translate(Point::new(compensation, baseline));
}
/// Commit to a line and build its frame.
@ -519,7 +540,7 @@ pub fn commit(
let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), styles, region)
})?;
apply_baseline_shift(&mut frame, *styles);
apply_shift(&engine.world, &mut frame, *styles);
push(&mut offset, frame);
} else {
offset += amount;

View File

@ -28,7 +28,7 @@ use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate;
use self::finalize::finalize;
use self::line::{apply_baseline_shift, commit, line, Line};
use self::line::{apply_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation};
use self::shaping::{

View File

@ -5,14 +5,15 @@ use std::sync::Arc;
use az::SaturatingAs;
use ecow::EcoString;
use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer};
use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer};
use ttf_parser::gsub::SubstitutionSubtable;
use ttf_parser::Tag;
use typst_library::engine::Engine;
use typst_library::foundations::{Smart, StyleChain};
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
use typst_library::text::{
families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant,
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
Glyph, Lang, Region, ScriptSettings, TextEdgeBounds, TextElem, TextItem,
};
use typst_library::World;
use typst_utils::SliceExt;
@ -64,6 +65,8 @@ pub struct ShapedGlyph {
pub x_offset: Em,
/// The vertical offset of the glyph.
pub y_offset: Em,
/// How much to scale the glyph compared to its normal size.
pub scale: Em,
/// The adjustability of the glyph.
pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full inline layout. A
@ -230,8 +233,10 @@ impl<'a> ShapedText<'a> {
let stroke = TextElem::stroke_in(self.styles);
let span_offset = TextElem::span_offset_in(self.styles);
for ((font, y_offset), group) in
self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
for ((font, y_offset, scale), group) in self
.glyphs
.as_ref()
.group_by_key(|g| (g.font.clone(), g.y_offset, g.scale))
{
let mut range = group[0].range.clone();
for glyph in group {
@ -304,7 +309,7 @@ impl<'a> ShapedText<'a> {
let item = TextItem {
font,
size: self.size,
size: scale.at(self.size),
lang: self.lang,
region: self.region,
fill: fill.clone(),
@ -420,7 +425,7 @@ impl<'a> ShapedText<'a> {
styles: self.styles,
size: self.size,
variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
width: glyphs_width(glyphs).at(self.size),
glyphs: Cow::Borrowed(glyphs),
}
} else {
@ -491,6 +496,7 @@ impl<'a> ShapedText<'a> {
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
scale: Em::one(),
adjustability: Adjustability::default(),
range,
safe_to_break: true,
@ -666,6 +672,7 @@ fn shape<'a>(
region: Option<Region>,
) -> ShapedText<'a> {
let size = TextElem::size_in(styles);
let script_settings = TextElem::subperscript_in(styles);
let mut ctx = ShapingContext {
engine,
size,
@ -679,7 +686,7 @@ fn shape<'a>(
};
if !text.is_empty() {
shape_segment(&mut ctx, base, text, families(styles));
shape_segment(&mut ctx, base, text, families(styles), script_settings);
}
track_and_space(&mut ctx);
@ -699,11 +706,17 @@ fn shape<'a>(
styles,
variant: ctx.variant,
size,
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
width: glyphs_width(&ctx.glyphs).at(size),
glyphs: Cow::Owned(ctx.glyphs),
}
}
/// Computes the width of a run of glyphs relative to the font size, accounting
/// for their individual scaling factors and other font metrics.
fn glyphs_width(glyphs: &[ShapedGlyph]) -> Em {
glyphs.iter().map(|g| g.x_advance * g.scale.get()).sum()
}
/// Holds shaping results and metadata common to all shaped segments.
struct ShapingContext<'a, 'v> {
engine: &'a Engine<'v>,
@ -723,6 +736,7 @@ fn shape_segment<'a>(
base: usize,
text: &str,
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
script_settings: Option<ScriptSettings>,
) {
// Don't try shaping newlines, tabs, or default ignorables.
if text
@ -789,6 +803,52 @@ fn shape_segment<'a>(
// text extraction.
buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES);
// Those values determine how the rendered text should be transformed to
// display sub-/super-scripts properly. If the text is not scripted, or if
// the OpenType feature can be used, the rendered text should not be
// transformed in any way, and so those values are neutral (`(0, 0, 1)`). If
// scripts should be synthesized, those values determine how to transform
// the rendered text to display scripts as expected.
let (script_shift, script_compensation, scale) = match script_settings {
None => (Em::zero(), Em::zero(), Em::one()),
Some(settings) => settings
.typographic
.then(|| {
// If typographic scripts are enabled (i.e., we want to use the
// OpenType feature instead of synthesizing if possible), we add
// "subs"/"sups" to the feature list if supported by the font.
// In case of a problem, we just early exit
let gsub = font.rusty().tables().gsub?;
let subtable_index =
gsub.features.find(settings.kind.feature())?.lookup_indices.get(0)?;
let coverage = gsub
.lookups
.get(subtable_index)?
.subtables
.get::<SubstitutionSubtable>(0)?
.coverage();
text.chars()
.all(|c| {
font.rusty().glyph_index(c).is_some_and(|i| coverage.contains(i))
})
.then(|| {
ctx.features.push(Feature::new(settings.kind.feature(), 1, ..));
(Em::zero(), Em::zero(), Em::one())
})
})
// Reunite the cases where `typographic` is `false` or where using
// the OpenType feature would not work.
.flatten()
.unwrap_or_else(|| {
let script_metrics = settings.kind.read_metrics(font.metrics());
(
settings.shift.unwrap_or(script_metrics.vertical_offset),
script_metrics.horizontal_offset,
settings.size.unwrap_or(script_metrics.height),
)
}),
};
// Prepare the shape plan. This plan depends on direction, script, language,
// and features, but is independent from the text and can thus be memoized.
let plan = create_shape_plan(
@ -869,8 +929,9 @@ fn shape_segment<'a>(
glyph_id: info.glyph_id as u16,
// TODO: Don't ignore y_advance.
x_advance,
x_offset: font.to_em(pos[i].x_offset),
y_offset: font.to_em(pos[i].y_offset),
x_offset: font.to_em(pos[i].x_offset) + script_compensation,
y_offset: font.to_em(pos[i].y_offset) + script_shift,
scale,
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: !info.unsafe_to_break(),
@ -923,7 +984,13 @@ fn shape_segment<'a>(
}
// Recursively shape the tofu sequence with the next family.
shape_segment(ctx, base + start, &text[start..end], families.clone());
shape_segment(
ctx,
base + start,
&text[start..end],
families.clone(),
script_settings,
);
}
i += 1;
@ -963,6 +1030,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
x_advance,
x_offset: Em::zero(),
y_offset: Em::zero(),
scale: Em::one(),
adjustability: Adjustability::default(),
range: start..end,
safe_to_break: true,

View File

@ -91,6 +91,10 @@ impl Length {
hint: "or use `length.abs.{unit}()` instead to ignore its em component"
)
}
pub fn to_em(&self, text_size: Abs) -> Em {
self.em + Em::new(self.abs / text_size)
}
}
#[scope]

View File

@ -215,6 +215,10 @@ pub struct FontMetrics {
pub underline: LineMetrics,
/// Recommended metrics for an overline.
pub overline: LineMetrics,
/// Metrics for subscripts.
pub subscript: ScriptMetrics,
/// Metrics for superscripts.
pub superscript: ScriptMetrics,
}
impl FontMetrics {
@ -227,6 +231,7 @@ impl FontMetrics {
let cap_height = ttf.capital_height().filter(|&h| h > 0).map_or(ascender, to_em);
let x_height = ttf.x_height().filter(|&h| h > 0).map_or(ascender, to_em);
let descender = to_em(ttf.typographic_descender().unwrap_or(ttf.descender()));
let strikeout = ttf.strikeout_metrics();
let underline = ttf.underline_metrics();
@ -249,6 +254,26 @@ impl FontMetrics {
thickness: underline.thickness,
};
let subscript = match ttf.subscript_metrics() {
None => ScriptMetrics::default_subscript(),
Some(metrics) => ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: -to_em(metrics.y_offset),
},
};
let superscript = match ttf.superscript_metrics() {
None => ScriptMetrics::default_superscript(),
Some(metrics) => ScriptMetrics {
width: to_em(metrics.x_size),
height: to_em(metrics.y_size),
horizontal_offset: to_em(metrics.x_offset),
vertical_offset: to_em(metrics.y_offset),
},
};
Self {
units_per_em,
ascender,
@ -258,6 +283,8 @@ impl FontMetrics {
strikethrough,
underline,
overline,
superscript,
subscript,
}
}
@ -283,6 +310,34 @@ pub struct LineMetrics {
pub thickness: Em,
}
/// Metrics for subscripts or superscripts.
#[derive(Debug, Copy, Clone)]
pub struct ScriptMetrics {
pub width: Em,
pub height: Em,
pub horizontal_offset: Em,
pub vertical_offset: Em,
}
impl ScriptMetrics {
pub const fn default_with_vertical_offset(offset: Em) -> Self {
Self {
width: Em::new(0.6),
height: Em::new(0.6),
horizontal_offset: Em::zero(),
vertical_offset: offset,
}
}
pub const fn default_subscript() -> Self {
Self::default_with_vertical_offset(Em::new(-0.2))
}
pub const fn default_superscript() -> Self {
Self::default_with_vertical_offset(Em::new(0.5))
}
}
/// Identifies a vertical metric of a font.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum VerticalFontMetric {

View File

@ -752,6 +752,11 @@ pub struct TextElem {
#[internal]
#[ghost]
pub smallcaps: Option<Smallcaps>,
/// Whether subscripts or superscripts are enabled.
#[internal]
#[ghost]
pub subperscript: Option<ScriptSettings>,
}
impl TextElem {

View File

@ -1,11 +1,10 @@
use ecow::EcoString;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
use crate::layout::{Em, Length};
use crate::text::{variant, SpaceElem, TextElem, TextSize};
use crate::World;
use crate::text::{FontMetrics, TextElem, TextSize};
use ttf_parser::Tag;
use typst_library::text::ScriptMetrics;
/// Renders text in subscript.
///
@ -17,11 +16,17 @@ use crate::World;
/// ```
#[elem(title = "Subscript", Show)]
pub struct SubElem {
/// Whether to prefer the dedicated subscript characters of the font.
/// Whether to create artificial subscripts by shifting and scaling down
/// regular glyphs.
///
/// If this is enabled, Typst first tries to transform the text to subscript
/// codepoints. If that fails, it falls back to rendering lowered and shrunk
/// normal letters.
/// Ideally, subscripts glyphs should be provided by the font (using the
/// `subs` OpenType feature). Otherwise, Typst is able to synthesize
/// subscripts as explained above.
///
/// When this is set to `{false}`, synthesized glyphs will be used
/// regardless of whether the font provides dedicated subscript glyphs. When
/// `{true}`, synthesized glyphs may still be used in case the font does not
/// provide the necessary subscript glyphs.
///
/// ```example
/// N#sub(typographic: true)[1]
@ -30,17 +35,19 @@ pub struct SubElem {
#[default(true)]
pub typographic: bool,
/// The baseline shift for synthetic subscripts. Does not apply if
/// `typographic` is true and the font has subscript codepoints for the
/// given `body`.
#[default(Em::new(0.2).into())]
pub baseline: Length,
/// The baseline shift for synthesized subscripts.
///
/// This only applies to synthesized subscripts. In other words, this has no
/// effect if `typographic` is `{true}` and the font provides the necessary
/// subscript glyphs.
pub baseline: Smart<Length>,
/// The font size for synthetic subscripts. Does not apply if
/// `typographic` is true and the font has subscript codepoints for the
/// given `body`.
#[default(TextSize(Em::new(0.6).into()))]
pub size: TextSize,
/// The font size for synthesized subscripts.
///
/// This only applies to synthesized subscripts. In other words, this has no
/// effect if `typographic` is `{true}` and the font provides the necessary
/// subscript glyphs.
pub size: Smart<TextSize>,
/// The text to display in subscript.
#[required]
@ -49,20 +56,17 @@ pub struct SubElem {
impl Show for Packed<SubElem> {
#[typst_macros::time(name = "sub", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
if self.typographic(styles) {
if let Some(text) = convert_script(&body, true) {
if is_shapable(engine, &text, styles) {
return Ok(TextElem::packed(text));
}
}
};
Ok(body
.styled(TextElem::set_baseline(self.baseline(styles)))
.styled(TextElem::set_size(self.size(styles))))
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let outer_text_size = TextElem::size_in(styles);
Ok(self
.body
.clone()
.styled(TextElem::set_subperscript(Some(ScriptSettings {
typographic: self.typographic(styles),
shift: self.baseline(styles).map(|l| -l.to_em(outer_text_size)),
size: self.size(styles).map(|t| t.0.to_em(outer_text_size)),
kind: ScriptKind::Sub,
}))))
}
}
@ -76,11 +80,17 @@ impl Show for Packed<SubElem> {
/// ```
#[elem(title = "Superscript", Show)]
pub struct SuperElem {
/// Whether to prefer the dedicated superscript characters of the font.
/// Whether to create artificial superscripts by shifting and scaling down
/// regular glyphs.
///
/// If this is enabled, Typst first tries to transform the text to
/// superscript codepoints. If that fails, it falls back to rendering
/// raised and shrunk normal letters.
/// Ideally, superscripts glyphs should be provided by the font (using the
/// `subs` OpenType feature). Otherwise, Typst is able to synthesize
/// superscripts as explained above.
///
/// When this is set to `{false}`, synthesized glyphs will be used
/// regardless of whether the font provides dedicated superscript glyphs.
/// When `{true}`, synthesized glyphs may still be used in case the font
/// does not provide the necessary superscript glyphs.
///
/// ```example
/// N#super(typographic: true)[1]
@ -89,17 +99,19 @@ pub struct SuperElem {
#[default(true)]
pub typographic: bool,
/// The baseline shift for synthetic superscripts. Does not apply if
/// `typographic` is true and the font has superscript codepoints for the
/// given `body`.
#[default(Em::new(-0.5).into())]
pub baseline: Length,
/// The baseline shift for synthesized superscripts.
///
/// This only applies to synthesized superscripts. In other words, this has
/// no effect if `typographic` is `{true}` and the font provides the
/// necessary superscript glyphs.
pub baseline: Smart<Length>,
/// The font size for synthetic superscripts. Does not apply if
/// `typographic` is true and the font has superscript codepoints for the
/// given `body`.
#[default(TextSize(Em::new(0.6).into()))]
pub size: TextSize,
/// The font size for synthesized superscripts.
///
/// This only applies to synthesized superscripts. In other words, this has
/// no effect if `typographic` is `{true}` and the font provides the
/// necessary superscript glyphs.
pub size: Smart<TextSize>,
/// The text to display in superscript.
#[required]
@ -108,107 +120,55 @@ pub struct SuperElem {
impl Show for Packed<SuperElem> {
#[typst_macros::time(name = "super", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let body = self.body.clone();
if self.typographic(styles) {
if let Some(text) = convert_script(&body, false) {
if is_shapable(engine, &text, styles) {
return Ok(TextElem::packed(text));
}
}
};
Ok(body
.styled(TextElem::set_baseline(self.baseline(styles)))
.styled(TextElem::set_size(self.size(styles))))
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let outer_text_size = TextElem::size_in(styles);
Ok(self
.body
.clone()
.styled(TextElem::set_subperscript(Some(ScriptSettings {
typographic: self.typographic(styles),
shift: self.baseline(styles).map(|l| -l.to_em(outer_text_size)),
size: self.size(styles).map(|t| t.0.to_em(outer_text_size)),
kind: ScriptKind::Super,
}))))
}
}
/// Find and transform the text contained in `content` to the given script kind
/// if and only if it only consists of `Text`, `Space`, and `Empty` leaves.
fn convert_script(content: &Content, sub: bool) -> Option<EcoString> {
if content.is::<SpaceElem>() {
Some(' '.into())
} else if let Some(elem) = content.to_packed::<TextElem>() {
if sub {
elem.text.chars().map(to_subscript_codepoint).collect()
} else {
elem.text.chars().map(to_superscript_codepoint).collect()
}
} else if let Some(sequence) = content.to_packed::<SequenceElem>() {
sequence
.children
.iter()
.map(|item| convert_script(item, sub))
.collect()
} else {
None
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ScriptKind {
Sub,
Super,
}
/// Checks whether the first retrievable family contains all code points of the
/// given string.
fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
let world = engine.world;
for family in TextElem::font_in(styles) {
if let Some(font) = world
.book()
.select(family.as_str(), variant(styles))
.and_then(|id| world.font(id))
{
let covers = family.covers();
return text.chars().all(|c| {
covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
&& font.ttf().glyph_index(c).is_some()
});
impl ScriptKind {
pub const fn default_metrics(self) -> ScriptMetrics {
match self {
Self::Sub => ScriptMetrics::default_subscript(),
Self::Super => ScriptMetrics::default_superscript(),
}
}
false
}
pub const fn read_metrics(self, font_metrics: &FontMetrics) -> ScriptMetrics {
match self {
Self::Sub => font_metrics.subscript,
Self::Super => font_metrics.superscript,
}
}
/// Convert a character to its corresponding Unicode superscript.
fn to_superscript_codepoint(c: char) -> Option<char> {
match c {
'1' => Some('¹'),
'2' => Some('²'),
'3' => Some('³'),
'0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32),
'+' => Some('⁺'),
'' => Some('⁻'),
'=' => Some('⁼'),
'(' => Some('⁽'),
')' => Some('⁾'),
'n' => Some('ⁿ'),
'i' => Some('ⁱ'),
' ' => Some(' '),
_ => None,
/// The corresponding OpenType feature.
pub const fn feature(self) -> Tag {
match self {
Self::Sub => Tag::from_bytes(b"subs"),
Self::Super => Tag::from_bytes(b"sups"),
}
}
}
/// Convert a character to its corresponding Unicode subscript.
fn to_subscript_codepoint(c: char) -> Option<char> {
match c {
'0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32),
'+' => Some('₊'),
'' => Some('₋'),
'=' => Some('₌'),
'(' => Some('₍'),
')' => Some('₎'),
'a' => Some('ₐ'),
'e' => Some('ₑ'),
'o' => Some('ₒ'),
'x' => Some('ₓ'),
'h' => Some('ₕ'),
'k' => Some('ₖ'),
'l' => Some('ₗ'),
'm' => Some('ₘ'),
'n' => Some('ₙ'),
'p' => Some('ₚ'),
's' => Some('ₛ'),
't' => Some('ₜ'),
' ' => Some(' '),
_ => None,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ScriptSettings {
/// Whether the OpenType feature should be used if possible.
pub typographic: bool,
pub shift: Smart<Em>,
pub size: Smart<Em>,
pub kind: ScriptKind,
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 815 B

BIN
tests/ref/long-scripts.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
tests/ref/super-1em.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,19 +1,78 @@
// Test sub- and superscript shifts.
--- sub-super ---
#let sq = box(square(size: 4pt))
#table(
columns: 3,
[Typo.], [Fallb.], [Synth],
[x#super[1]], [x#super[5n]], [x#super[2 #box(square(size: 6pt))]],
[x#sub[1]], [x#sub[5n]], [x#sub[2 #box(square(size: 6pt))]],
[Typo.], [Fallb.], [Synth.],
[x#super[1 #sq]], [x#super[5: #sq]], [x#super(typographic: false)[2 #sq]],
[x#sub[1 #sq]], [x#sub[5: #sq]], [x#sub(typographic: false)[2 #sq]],
)
--- sub-super-typographic ---
#set text(size: 20pt)
// Libertinus Serif supports "subs" and "sups" for `typo`, but not for `synth`.
#let synth = [1,2,3]
#let typo = [123]
#let sq = [1#box(square(size: 4pt))2]
x#super(synth) x#super(typo) x#super(sq) \
x#sub(synth) x#sub(typo) x#sub(sq)
--- sub-super-italic-compensation ---
#set text(size: 20pt, style: "italic")
// Libertinus Serif supports "subs" and "sups" for `typo`, but not for `synth`.
#let synth = [1,2,3]
#let typo = [123]
#let sq = [1#box(square(size: 4pt))2]
x#super(synth) x#super(typo) x#super(sq) \
x#sub(synth) x#sub(typo) x#sub(sq)
--- sub-super-non-typographic ---
#set super(typographic: false, baseline: -0.25em, size: 0.7em)
n#super[1], n#sub[2], ... n#super[N]
--- super-underline ---
#set underline(stroke: 0.5pt, offset: 0.15em)
#underline[The claim#super[\[4\]]] has been disputed. \
The claim#super[#underline[\[4\]]] has been disputed. \
It really has been#super(box(text(baseline: 0pt, underline[\[4\]]))) \
#set super(typographic: false)
#underline[The claim#super[4]] has been disputed. \
The claim#super[#underline[4]] has been disputed. \
The claim #underline(super[4]) has been disputed. \
#set super(typographic: true)
#underline[The claim#super[4]] has been disputed. \
The claim#super[#underline[4]] has been disputed. \
The claim #underline(super[4]) has been disputed.
--- super-highlight ---
#set super(typographic: false)
#highlight[The claim#super[4]] has been disputed. \
The claim#super[#highlight[4]] has been disputed. \
It really has been#super(highlight[4]) \
#set super(typographic: true)
#highlight[The claim#super[4]] has been disputed. \
The claim#super[#highlight[4]] has been disputed. \
It really has been#super(highlight[4])
--- super-1em ---
Test#super[#box(rect(height: 1em))]#box(rect(height: 1em))
--- long-scripts ---
|longscript| \
|#super(typographic: true)[longscript]| \
|#super(typographic: false)[longscript]| \
|#sub(typographic: true)[longscript]| \
|#sub(typographic: false)[longscript]|
--- scripts-with-bundeled-fonts ---
#let test(font, weights, styles) = {
for weight in weights {
for style in styles {
text(font: font, weight: weight, style: style)[Xx#super[Xx]#sub[Xx]]
linebreak()
}
}
}
#test("DejaVu Sans Mono", ("regular", "bold"), ("normal", "oblique"))
#test("Libertinus Serif", ("regular", "semibold", "bold"), ("normal", "italic"))
#test("New Computer Modern", ("regular", "bold"), ("normal", "italic"))
#test("New Computer Modern Math", (400, 450, "bold"), ("normal",))