Fix equations in RTL text (#4150)
@ -102,6 +102,13 @@ type Range = std::ops::Range<usize>;
|
|||||||
const SPACING_REPLACE: char = ' '; // Space
|
const SPACING_REPLACE: char = ' '; // Space
|
||||||
const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
|
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
|
/// A paragraph representation in which children are already layouted and text
|
||||||
/// is already preshaped.
|
/// is already preshaped.
|
||||||
///
|
///
|
||||||
@ -206,9 +213,12 @@ impl Segment<'_> {
|
|||||||
Self::Box(_, frac) => {
|
Self::Box(_, frac) => {
|
||||||
(if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8()
|
(if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8()
|
||||||
}
|
}
|
||||||
Self::Equation(ref par_items) => {
|
Self::Equation(ref par_items) => par_items
|
||||||
par_items.iter().map(MathParItem::text).map(char::len_utf8).sum()
|
.iter()
|
||||||
}
|
.map(MathParItem::text)
|
||||||
|
.chain([LTR_ISOLATE, POP_ISOLATE])
|
||||||
|
.map(char::len_utf8)
|
||||||
|
.sum(),
|
||||||
Self::Meta => 0,
|
Self::Meta => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,6 +237,9 @@ enum Item<'a> {
|
|||||||
Frame(Frame),
|
Frame(Frame),
|
||||||
/// Metadata.
|
/// Metadata.
|
||||||
Meta(Frame),
|
Meta(Frame),
|
||||||
|
/// An item that is invisible and needs to be skipped, e.g. a Unicode
|
||||||
|
/// isolate.
|
||||||
|
Skip(char),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Item<'a> {
|
impl<'a> Item<'a> {
|
||||||
@ -253,6 +266,7 @@ impl<'a> Item<'a> {
|
|||||||
Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
|
Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
|
||||||
Self::Frame(_) => OBJ_REPLACE.len_utf8(),
|
Self::Frame(_) => OBJ_REPLACE.len_utf8(),
|
||||||
Self::Meta(_) => 0,
|
Self::Meta(_) => 0,
|
||||||
|
Self::Skip(c) => c.len_utf8(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +277,7 @@ impl<'a> Item<'a> {
|
|||||||
Self::Absolute(v) => *v,
|
Self::Absolute(v) => *v,
|
||||||
Self::Frame(frame) => frame.width(),
|
Self::Frame(frame) => frame.width(),
|
||||||
Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
|
Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
|
||||||
|
Self::Skip(_) => Abs::zero(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -452,8 +467,8 @@ fn collect<'a>(
|
|||||||
if dir != outer_dir {
|
if dir != outer_dir {
|
||||||
// Insert "Explicit Directional Embedding".
|
// Insert "Explicit Directional Embedding".
|
||||||
match dir {
|
match dir {
|
||||||
Dir::LTR => full.push('\u{202A}'),
|
Dir::LTR => full.push(LTR_EMBEDDING),
|
||||||
Dir::RTL => full.push('\u{202B}'),
|
Dir::RTL => full.push(RTL_EMBEDDING),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -466,7 +481,7 @@ fn collect<'a>(
|
|||||||
|
|
||||||
if dir != outer_dir {
|
if dir != outer_dir {
|
||||||
// Insert "Pop Directional Formatting".
|
// Insert "Pop Directional Formatting".
|
||||||
full.push('\u{202C}');
|
full.push(POP_EMBEDDING);
|
||||||
}
|
}
|
||||||
Segment::Text(full.len() - prev)
|
Segment::Text(full.len() - prev)
|
||||||
} else if let Some(elem) = child.to_packed::<HElem>() {
|
} else if let Some(elem) = child.to_packed::<HElem>() {
|
||||||
@ -521,7 +536,9 @@ fn collect<'a>(
|
|||||||
let MathParItem::Frame(frame) = item else { continue };
|
let MathParItem::Frame(frame) = item else { continue };
|
||||||
frame.meta(styles, false);
|
frame.meta(styles, false);
|
||||||
}
|
}
|
||||||
|
full.push(LTR_ISOLATE);
|
||||||
full.extend(items.iter().map(MathParItem::text));
|
full.extend(items.iter().map(MathParItem::text));
|
||||||
|
full.push(POP_ISOLATE);
|
||||||
Segment::Equation(items)
|
Segment::Equation(items)
|
||||||
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
} else if let Some(elem) = child.to_packed::<BoxElem>() {
|
||||||
let frac = elem.width(styles).is_fractional();
|
let frac = elem.width(styles).is_fractional();
|
||||||
@ -594,6 +611,7 @@ fn prepare<'a>(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
Segment::Equation(par_items) => {
|
Segment::Equation(par_items) => {
|
||||||
|
items.push(Item::Skip(LTR_ISOLATE));
|
||||||
for item in par_items {
|
for item in par_items {
|
||||||
match item {
|
match item {
|
||||||
MathParItem::Space(s) => items.push(Item::Absolute(s)),
|
MathParItem::Space(s) => items.push(Item::Absolute(s)),
|
||||||
@ -603,6 +621,7 @@ fn prepare<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.push(Item::Skip(POP_ISOLATE));
|
||||||
}
|
}
|
||||||
Segment::Box(elem, _) => {
|
Segment::Box(elem, _) => {
|
||||||
if let Sizing::Fr(v) = elem.width(styles) {
|
if let Sizing::Fr(v) = elem.width(styles) {
|
||||||
@ -1353,6 +1372,7 @@ fn commit(
|
|||||||
Item::Frame(frame) | Item::Meta(frame) => {
|
Item::Frame(frame) | Item::Meta(frame) => {
|
||||||
push(&mut offset, frame.clone());
|
push(&mut offset, frame.clone());
|
||||||
}
|
}
|
||||||
|
Item::Skip(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +224,12 @@ impl Packed<EquationElem> {
|
|||||||
vec![MathParItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
|
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 {
|
for item in &mut items {
|
||||||
let MathParItem::Frame(frame) = item else { continue };
|
let MathParItem::Frame(frame) = item else { continue };
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ use std::iter::once;
|
|||||||
use unicode_math_class::MathClass;
|
use unicode_math_class::MathClass;
|
||||||
|
|
||||||
use crate::foundations::{Resolve, StyleChain};
|
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::{
|
use crate::math::{
|
||||||
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
|
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
|
||||||
MathFragment, MathParItem, MathSize,
|
MathFragment, MathParItem, MathSize,
|
||||||
@ -257,7 +257,7 @@ impl MathRun {
|
|||||||
let mut x = Abs::zero();
|
let mut x = Abs::zero();
|
||||||
let mut ascent = Abs::zero();
|
let mut ascent = Abs::zero();
|
||||||
let mut descent = 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 mut empty = true;
|
||||||
|
|
||||||
let finalize_frame = |frame: &mut Frame, x, ascent, descent| {
|
let finalize_frame = |frame: &mut Frame, x, ascent, descent| {
|
||||||
@ -301,10 +301,8 @@ impl MathRun {
|
|||||||
|| (class == MathClass::Relation
|
|| (class == MathClass::Relation
|
||||||
&& !iter.peek().map(is_relation).unwrap_or_default())
|
&& !iter.peek().map(is_relation).unwrap_or_default())
|
||||||
{
|
{
|
||||||
let mut frame_prev = std::mem::replace(
|
let mut frame_prev =
|
||||||
&mut frame,
|
std::mem::replace(&mut frame, Frame::soft(Size::zero()));
|
||||||
Frame::new(Size::zero(), FrameKind::Soft),
|
|
||||||
);
|
|
||||||
|
|
||||||
finalize_frame(&mut frame_prev, x, ascent, descent);
|
finalize_frame(&mut frame_prev, x, ascent, descent);
|
||||||
items.push(MathParItem::Frame(frame_prev));
|
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 |
@ -210,3 +210,8 @@ $ <quadratic>
|
|||||||
// Error: 14-24 cannot reference equation without numbering
|
// Error: 14-24 cannot reference equation without numbering
|
||||||
// Hint: 14-24 you can enable equation numbering with `#set math.equation(numbering: "1.")`
|
// Hint: 14-24 you can enable equation numbering with `#set math.equation(numbering: "1.")`
|
||||||
Looks at the @quadratic formula.
|
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]$
|
||||||
|
@ -34,6 +34,26 @@ $#here[f] := #here[Hi there]$.
|
|||||||
// Test boxes with a baseline are respected
|
// Test boxes with a baseline are respected
|
||||||
#box(stroke: 0.2pt, $a #box(baseline:0.5em, stroke: 0.2pt, $a$)$)
|
#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-missing-fields ---
|
||||||
// Issue #2821: Setting a figure's supplement to none removes the field
|
// Issue #2821: Setting a figure's supplement to none removes the field
|
||||||
#show figure.caption: it => {
|
#show figure.caption: it => {
|
||||||
|