Fix equations in RTL text (#4150)
@ -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.
|
||||
///
|
||||
@ -207,9 +214,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,
|
||||
}
|
||||
}
|
||||
@ -228,6 +238,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> {
|
||||
@ -254,6 +267,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,6 +278,7 @@ impl<'a> Item<'a> {
|
||||
Self::Absolute(v) => *v,
|
||||
Self::Frame(frame) => frame.width(),
|
||||
Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
|
||||
Self::Skip(_) => Abs::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -466,8 +481,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),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -480,7 +495,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>() {
|
||||
@ -535,7 +550,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();
|
||||
@ -608,6 +625,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)),
|
||||
@ -617,6 +635,7 @@ fn prepare<'a>(
|
||||
}
|
||||
}
|
||||
}
|
||||
items.push(Item::Skip(POP_ISOLATE));
|
||||
}
|
||||
Segment::Box(elem, _) => {
|
||||
if let Sizing::Fr(v) = elem.width(styles) {
|
||||
@ -1407,6 +1426,7 @@ fn commit(
|
||||
Item::Frame(frame) | Item::Meta(frame) => {
|
||||
push(&mut offset, frame.clone());
|
||||
}
|
||||
Item::Skip(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,6 +223,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 };
|
||||
|
||||
|
@ -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));
|
||||
|
BIN
tests/ref/issue-3696-equation-rtl.png
Normal file
After Width: | Height: | Size: 660 B |
BIN
tests/ref/math-at-line-end.png
Normal file
After Width: | Height: | Size: 479 B |
BIN
tests/ref/math-at-line-start.png
Normal file
After Width: | Height: | Size: 476 B |
BIN
tests/ref/math-at-par-end.png
Normal file
After Width: | Height: | Size: 450 B |
BIN
tests/ref/math-at-par-start.png
Normal file
After Width: | Height: | Size: 418 B |
BIN
tests/ref/math-consecutive.png
Normal file
After Width: | Height: | Size: 176 B |
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 664 B |
@ -224,3 +224,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]$
|
||||
|
@ -35,6 +35,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 => {
|
||||
|