Fix equations in RTL text (#4150)

This commit is contained in:
Laurenz 2024-05-17 10:36:07 +02:00
parent c2eddff485
commit f56408a73c
12 changed files with 61 additions and 12 deletions

View File

@ -102,6 +102,13 @@ type Range = std::ops::Range<usize>;
const SPACING_REPLACE: char = ' '; // Space
const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
// Unicode BiDi control characters.
const LTR_EMBEDDING: char = '\u{202A}';
const RTL_EMBEDDING: char = '\u{202B}';
const POP_EMBEDDING: char = '\u{202C}';
const LTR_ISOLATE: char = '\u{2066}';
const POP_ISOLATE: char = '\u{2069}';
/// A paragraph representation in which children are already layouted and text
/// is already preshaped.
///
@ -206,9 +213,12 @@ impl Segment<'_> {
Self::Box(_, frac) => {
(if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8()
}
Self::Equation(ref par_items) => {
par_items.iter().map(MathParItem::text).map(char::len_utf8).sum()
}
Self::Equation(ref par_items) => par_items
.iter()
.map(MathParItem::text)
.chain([LTR_ISOLATE, POP_ISOLATE])
.map(char::len_utf8)
.sum(),
Self::Meta => 0,
}
}
@ -227,6 +237,9 @@ enum Item<'a> {
Frame(Frame),
/// Metadata.
Meta(Frame),
/// An item that is invisible and needs to be skipped, e.g. a Unicode
/// isolate.
Skip(char),
}
impl<'a> Item<'a> {
@ -253,6 +266,7 @@ impl<'a> Item<'a> {
Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) => OBJ_REPLACE.len_utf8(),
Self::Meta(_) => 0,
Self::Skip(c) => c.len_utf8(),
}
}
@ -263,6 +277,7 @@ impl<'a> Item<'a> {
Self::Absolute(v) => *v,
Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
Self::Skip(_) => Abs::zero(),
}
}
}
@ -452,8 +467,8 @@ fn collect<'a>(
if dir != outer_dir {
// Insert "Explicit Directional Embedding".
match dir {
Dir::LTR => full.push('\u{202A}'),
Dir::RTL => full.push('\u{202B}'),
Dir::LTR => full.push(LTR_EMBEDDING),
Dir::RTL => full.push(RTL_EMBEDDING),
_ => {}
}
}
@ -466,7 +481,7 @@ fn collect<'a>(
if dir != outer_dir {
// Insert "Pop Directional Formatting".
full.push('\u{202C}');
full.push(POP_EMBEDDING);
}
Segment::Text(full.len() - prev)
} else if let Some(elem) = child.to_packed::<HElem>() {
@ -521,7 +536,9 @@ fn collect<'a>(
let MathParItem::Frame(frame) = item else { continue };
frame.meta(styles, false);
}
full.push(LTR_ISOLATE);
full.extend(items.iter().map(MathParItem::text));
full.push(POP_ISOLATE);
Segment::Equation(items)
} else if let Some(elem) = child.to_packed::<BoxElem>() {
let frac = elem.width(styles).is_fractional();
@ -594,6 +611,7 @@ fn prepare<'a>(
}
},
Segment::Equation(par_items) => {
items.push(Item::Skip(LTR_ISOLATE));
for item in par_items {
match item {
MathParItem::Space(s) => items.push(Item::Absolute(s)),
@ -603,6 +621,7 @@ fn prepare<'a>(
}
}
}
items.push(Item::Skip(POP_ISOLATE));
}
Segment::Box(elem, _) => {
if let Sizing::Fr(v) = elem.width(styles) {
@ -1353,6 +1372,7 @@ fn commit(
Item::Frame(frame) | Item::Meta(frame) => {
push(&mut offset, frame.clone());
}
Item::Skip(_) => {}
}
}

View File

@ -224,6 +224,12 @@ impl Packed<EquationElem> {
vec![MathParItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
};
// An empty equation should have a height, so we still create a frame
// (which is then resized in the loop).
if items.is_empty() {
items.push(MathParItem::Frame(Frame::soft(Size::zero())));
}
for item in &mut items {
let MathParItem::Frame(frame) = item else { continue };

View File

@ -3,7 +3,7 @@ use std::iter::once;
use unicode_math_class::MathClass;
use crate::foundations::{Resolve, StyleChain};
use crate::layout::{Abs, AlignElem, Em, Frame, FrameKind, Point, Size};
use crate::layout::{Abs, AlignElem, Em, Frame, Point, Size};
use crate::math::{
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
MathFragment, MathParItem, MathSize,
@ -257,7 +257,7 @@ impl MathRun {
let mut x = Abs::zero();
let mut ascent = Abs::zero();
let mut descent = Abs::zero();
let mut frame = Frame::new(Size::zero(), FrameKind::Soft);
let mut frame = Frame::soft(Size::zero());
let mut empty = true;
let finalize_frame = |frame: &mut Frame, x, ascent, descent| {
@ -301,10 +301,8 @@ impl MathRun {
|| (class == MathClass::Relation
&& !iter.peek().map(is_relation).unwrap_or_default())
{
let mut frame_prev = std::mem::replace(
&mut frame,
Frame::new(Size::zero(), FrameKind::Soft),
);
let mut frame_prev =
std::mem::replace(&mut frame, Frame::soft(Size::zero()));
finalize_frame(&mut frame_prev, x, ascent, descent);
items.push(MathParItem::Frame(frame_prev));

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 664 B

View File

@ -210,3 +210,8 @@ $ <quadratic>
// Error: 14-24 cannot reference equation without numbering
// Hint: 14-24 you can enable equation numbering with `#set math.equation(numbering: "1.")`
Looks at the @quadratic formula.
--- issue-3696-equation-rtl ---
#set page(width: 150pt)
#set text(lang: "he")
תהא סדרה $a_n$: $[a_n: 1, 1/2, 1/3, dots]$

View File

@ -34,6 +34,26 @@ $#here[f] := #here[Hi there]$.
// Test boxes with a baseline are respected
#box(stroke: 0.2pt, $a #box(baseline:0.5em, stroke: 0.2pt, $a$)$)
--- math-at-par-start ---
// Test that equation at start of paragraph works fine.
$x$ is a variable.
--- math-at-par-end ---
// Test that equation at end of paragraph works fine.
One number is $1$
--- math-at-line-start ---
// Test math at the natural end of a line.
#h(60pt) Number $1$ exists.
--- math-at-line-end ---
// Test math at the natural end of a line.
#h(50pt) Number $1$ exists.
--- math-consecutive ---
// Test immediately consecutive equations.
$x$$y$
--- issue-2821-missing-fields ---
// Issue #2821: Setting a figure's supplement to none removes the field
#show figure.caption: it => {