mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Fix duplicate RTL text and alignment + fr bugs
This commit is contained in:
parent
db820ae9aa
commit
a302105b9f
@ -199,25 +199,48 @@ struct Preparation<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Preparation<'a> {
|
impl<'a> Preparation<'a> {
|
||||||
/// Find the item which is at the `text_offset`.
|
/// Find the item that contains the given `text_offset`.
|
||||||
fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
|
fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
|
||||||
self.find_idx_and_offset(text_offset).map(|(idx, _)| &self.items[idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the index and text offset of the item which is at the
|
|
||||||
/// `text_offset`.
|
|
||||||
fn find_idx_and_offset(&self, text_offset: usize) -> Option<(usize, usize)> {
|
|
||||||
let mut cursor = 0;
|
let mut cursor = 0;
|
||||||
for (idx, item) in self.items.iter().enumerate() {
|
for item in &self.items {
|
||||||
let end = cursor + item.len();
|
let end = cursor + item.len();
|
||||||
if (cursor .. end).contains(&text_offset) {
|
if (cursor .. end).contains(&text_offset) {
|
||||||
return Some((idx, cursor));
|
return Some(item);
|
||||||
}
|
}
|
||||||
cursor = end;
|
cursor = end;
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the items that intersect the given `text_range`.
|
||||||
|
///
|
||||||
|
/// Returns the expanded range around the items and the items.
|
||||||
|
fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) {
|
||||||
|
let mut cursor = 0;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut end = 0;
|
||||||
|
let mut expanded = text_range.clone();
|
||||||
|
|
||||||
|
for (i, item) in self.items.iter().enumerate() {
|
||||||
|
if cursor <= text_range.start {
|
||||||
|
start = i;
|
||||||
|
expanded.start = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = item.len();
|
||||||
|
if cursor < text_range.end || cursor + len <= text_range.end {
|
||||||
|
end = i + 1;
|
||||||
|
expanded.end = cursor + len;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
(expanded, &self.items[start .. end])
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a style property, but only if it is the same for all children of the
|
/// Get a style property, but only if it is the same for all children of the
|
||||||
/// paragraph.
|
/// paragraph.
|
||||||
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
|
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
|
||||||
@ -307,12 +330,14 @@ impl<'a> Item<'a> {
|
|||||||
struct Line<'a> {
|
struct Line<'a> {
|
||||||
/// Bidi information about the paragraph.
|
/// Bidi information about the paragraph.
|
||||||
bidi: &'a BidiInfo<'a>,
|
bidi: &'a BidiInfo<'a>,
|
||||||
/// The (untrimmed) range the line spans in the paragraph.
|
/// The trimmed range the line spans in the paragraph.
|
||||||
range: Range,
|
trimmed: Range,
|
||||||
|
/// The untrimmed end where the line ends.
|
||||||
|
end: usize,
|
||||||
/// A reshaped text item if the line sliced up a text item at the start.
|
/// A reshaped text item if the line sliced up a text item at the start.
|
||||||
first: Option<Item<'a>>,
|
first: Option<Item<'a>>,
|
||||||
/// Middle items which don't need to be reprocessed.
|
/// Inner items which don't need to be reprocessed.
|
||||||
items: &'a [Item<'a>],
|
inner: &'a [Item<'a>],
|
||||||
/// A reshaped text item if the line sliced up a text item at the end. If
|
/// A reshaped text item if the line sliced up a text item at the end. If
|
||||||
/// there is only one text item, this takes precedence over `first`.
|
/// there is only one text item, this takes precedence over `first`.
|
||||||
last: Option<Item<'a>>,
|
last: Option<Item<'a>>,
|
||||||
@ -328,29 +353,31 @@ struct Line<'a> {
|
|||||||
impl<'a> Line<'a> {
|
impl<'a> Line<'a> {
|
||||||
/// Iterate over the line's items.
|
/// Iterate over the line's items.
|
||||||
fn items(&self) -> impl Iterator<Item = &Item<'a>> {
|
fn items(&self) -> impl Iterator<Item = &Item<'a>> {
|
||||||
self.first.iter().chain(self.items).chain(&self.last)
|
self.first.iter().chain(self.inner).chain(&self.last)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the item at the index.
|
/// Return items that intersect the given `text_range`.
|
||||||
fn get(&self, index: usize) -> Option<&Item<'a>> {
|
fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
|
||||||
self.items().nth(index)
|
let mut cursor = self.trimmed.start;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut end = 0;
|
||||||
|
|
||||||
|
for (i, item) in self.items().enumerate() {
|
||||||
|
if cursor <= text_range.start {
|
||||||
|
start = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the index of the item whose range contains the `text_offset`.
|
let len = item.len();
|
||||||
fn find(&self, text_offset: usize) -> usize {
|
if cursor < text_range.end || cursor + len <= text_range.end {
|
||||||
let mut idx = 0;
|
end = i + 1;
|
||||||
let mut cursor = self.range.start;
|
} else {
|
||||||
|
break;
|
||||||
for item in self.items() {
|
|
||||||
let end = cursor + item.len();
|
|
||||||
if (cursor .. end).contains(&text_offset) {
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
cursor = end;
|
|
||||||
idx += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
idx.saturating_sub(1)
|
cursor += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.items().skip(start).take(end - start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many justifiable glyphs the line contains.
|
// How many justifiable glyphs the line contains.
|
||||||
@ -664,7 +691,7 @@ fn linebreak_optimized<'a>(
|
|||||||
// Find the optimal predecessor.
|
// Find the optimal predecessor.
|
||||||
for (i, pred) in table.iter_mut().enumerate().skip(active) {
|
for (i, pred) in table.iter_mut().enumerate().skip(active) {
|
||||||
// Layout the line.
|
// Layout the line.
|
||||||
let start = pred.line.range.end;
|
let start = pred.line.end;
|
||||||
let attempt = line(p, fonts, start .. end, mandatory, hyphen);
|
let attempt = line(p, fonts, start .. end, mandatory, hyphen);
|
||||||
|
|
||||||
// Determine how much the line's spaces would need to be stretched
|
// Determine how much the line's spaces would need to be stretched
|
||||||
@ -848,16 +875,17 @@ impl Breakpoints<'_> {
|
|||||||
fn line<'a>(
|
fn line<'a>(
|
||||||
p: &'a Preparation,
|
p: &'a Preparation,
|
||||||
fonts: &mut FontStore,
|
fonts: &mut FontStore,
|
||||||
range: Range,
|
mut range: Range,
|
||||||
mandatory: bool,
|
mandatory: bool,
|
||||||
hyphen: bool,
|
hyphen: bool,
|
||||||
) -> Line<'a> {
|
) -> Line<'a> {
|
||||||
if range.is_empty() {
|
if range.is_empty() {
|
||||||
return Line {
|
return Line {
|
||||||
bidi: &p.bidi,
|
bidi: &p.bidi,
|
||||||
range,
|
end: range.end,
|
||||||
|
trimmed: range,
|
||||||
first: None,
|
first: None,
|
||||||
items: &[],
|
inner: &[],
|
||||||
last: None,
|
last: None,
|
||||||
width: Length::zero(),
|
width: Length::zero(),
|
||||||
justify: !mandatory,
|
justify: !mandatory,
|
||||||
@ -865,33 +893,25 @@ fn line<'a>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the last item.
|
|
||||||
let (last_idx, last_offset) =
|
|
||||||
p.find_idx_and_offset(range.end.saturating_sub(1)).unwrap();
|
|
||||||
|
|
||||||
// Find the first item.
|
|
||||||
let (first_idx, first_offset) = if range.is_empty() {
|
|
||||||
(last_idx, last_offset)
|
|
||||||
} else {
|
|
||||||
p.find_idx_and_offset(range.start).unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Slice out the relevant items.
|
// Slice out the relevant items.
|
||||||
let mut items = &p.items[first_idx ..= last_idx];
|
let end = range.end;
|
||||||
|
let (expanded, mut inner) = p.slice(range.clone());
|
||||||
let mut width = Length::zero();
|
let mut width = Length::zero();
|
||||||
|
|
||||||
// Reshape the last item if it's split in half.
|
// Reshape the last item if it's split in half or hyphenated.
|
||||||
let mut last = None;
|
let mut last = None;
|
||||||
let mut dash = false;
|
let mut dash = false;
|
||||||
let mut justify = !mandatory;
|
let mut justify = !mandatory;
|
||||||
if let Some((Item::Text(shaped), before)) = items.split_last() {
|
if let Some((Item::Text(shaped), before)) = inner.split_last() {
|
||||||
// Compute the range we want to shape, trimming whitespace at the
|
// Compute the range we want to shape, trimming whitespace at the
|
||||||
// end of the line.
|
// end of the line.
|
||||||
let base = last_offset;
|
let base = expanded.end - shaped.text.len();
|
||||||
let start = range.start.max(last_offset);
|
let start = range.start.max(base);
|
||||||
let end = range.end;
|
let text = &p.bidi.text[start .. range.end];
|
||||||
let text = &p.bidi.text[start .. end];
|
|
||||||
let trimmed = text.trim_end();
|
let trimmed = text.trim_end();
|
||||||
|
range.end = start + trimmed.len();
|
||||||
|
|
||||||
|
// Deal with hyphens, dashes and justification.
|
||||||
let shy = trimmed.ends_with('\u{ad}');
|
let shy = trimmed.ends_with('\u{ad}');
|
||||||
dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
|
dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
|
||||||
justify |= text.ends_with('\u{2028}');
|
justify |= text.ends_with('\u{2028}');
|
||||||
@ -905,10 +925,9 @@ fn line<'a>(
|
|||||||
// need the shaped empty string to make the line the appropriate
|
// need the shaped empty string to make the line the appropriate
|
||||||
// height. That is the case exactly if the string is empty and there
|
// height. That is the case exactly if the string is empty and there
|
||||||
// are no other items in the line.
|
// are no other items in the line.
|
||||||
if hyphen || trimmed.len() < shaped.text.len() {
|
if hyphen || start + shaped.text.len() > range.end {
|
||||||
if hyphen || !trimmed.is_empty() || before.is_empty() {
|
if hyphen || start < range.end || before.is_empty() {
|
||||||
let end = start + trimmed.len();
|
let shifted = start - base .. range.end - base;
|
||||||
let shifted = start - base .. end - base;
|
|
||||||
let mut reshaped = shaped.reshape(fonts, shifted);
|
let mut reshaped = shaped.reshape(fonts, shifted);
|
||||||
if hyphen || shy {
|
if hyphen || shy {
|
||||||
reshaped.push_hyphen(fonts);
|
reshaped.push_hyphen(fonts);
|
||||||
@ -917,41 +936,41 @@ fn line<'a>(
|
|||||||
last = Some(Item::Text(reshaped));
|
last = Some(Item::Text(reshaped));
|
||||||
}
|
}
|
||||||
|
|
||||||
items = before;
|
inner = before;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reshape the start item if it's split in half.
|
// Reshape the start item if it's split in half.
|
||||||
let mut first = None;
|
let mut first = None;
|
||||||
if let Some((Item::Text(shaped), after)) = items.split_first() {
|
if let Some((Item::Text(shaped), after)) = inner.split_first() {
|
||||||
// Compute the range we want to shape.
|
// Compute the range we want to shape.
|
||||||
let base = first_offset;
|
let base = expanded.start;
|
||||||
let start = range.start;
|
let end = range.end.min(base + shaped.text.len());
|
||||||
let end = range.end.min(first_offset + shaped.text.len());
|
|
||||||
|
|
||||||
// Reshape if necessary.
|
// Reshape if necessary.
|
||||||
if end - start < shaped.text.len() {
|
if range.start + shaped.text.len() > end {
|
||||||
if start < end {
|
if range.start < end {
|
||||||
let shifted = start - base .. end - base;
|
let shifted = range.start - base .. end - base;
|
||||||
let reshaped = shaped.reshape(fonts, shifted);
|
let reshaped = shaped.reshape(fonts, shifted);
|
||||||
width += reshaped.width;
|
width += reshaped.width;
|
||||||
first = Some(Item::Text(reshaped));
|
first = Some(Item::Text(reshaped));
|
||||||
}
|
}
|
||||||
|
|
||||||
items = after;
|
inner = after;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure the inner items.
|
// Measure the inner items.
|
||||||
for item in items {
|
for item in inner {
|
||||||
width += item.width();
|
width += item.width();
|
||||||
}
|
}
|
||||||
|
|
||||||
Line {
|
Line {
|
||||||
bidi: &p.bidi,
|
bidi: &p.bidi,
|
||||||
range,
|
trimmed: range,
|
||||||
|
end,
|
||||||
first,
|
first,
|
||||||
items,
|
inner,
|
||||||
last,
|
last,
|
||||||
width,
|
width,
|
||||||
justify,
|
justify,
|
||||||
@ -1058,10 +1077,7 @@ fn commit(
|
|||||||
let fr = line.fr();
|
let fr = line.fr();
|
||||||
let mut justification = Length::zero();
|
let mut justification = Length::zero();
|
||||||
if remaining < Length::zero()
|
if remaining < Length::zero()
|
||||||
|| (justify
|
|| (justify && line.justify && line.end < line.bidi.text.len() && fr.is_zero())
|
||||||
&& line.justify
|
|
||||||
&& line.range.end < line.bidi.text.len()
|
|
||||||
&& fr.is_zero())
|
|
||||||
{
|
{
|
||||||
let justifiables = line.justifiables();
|
let justifiables = line.justifiables();
|
||||||
if justifiables > 0 {
|
if justifiables > 0 {
|
||||||
@ -1104,7 +1120,11 @@ fn commit(
|
|||||||
let pod = Regions::one(size, regions.base, Spec::new(false, false));
|
let pod = Regions::one(size, regions.base, Spec::new(false, false));
|
||||||
let frame = node.layout(ctx, &pod, styles)?.remove(0);
|
let frame = node.layout(ctx, &pod, styles)?.remove(0);
|
||||||
let count = (width / frame.size.x).floor();
|
let count = (width / frame.size.x).floor();
|
||||||
let apart = (width % frame.size.x) / (count - 1.0);
|
let remaining = width % frame.size.x;
|
||||||
|
let apart = remaining / (count - 1.0);
|
||||||
|
if count == 1.0 {
|
||||||
|
offset += align.position(remaining);
|
||||||
|
}
|
||||||
if frame.size.x > Length::zero() {
|
if frame.size.x > Length::zero() {
|
||||||
for _ in 0 .. (count as usize).min(1000) {
|
for _ in 0 .. (count as usize).min(1000) {
|
||||||
push(&mut offset, frame.as_ref().clone());
|
push(&mut offset, frame.as_ref().clone());
|
||||||
@ -1116,6 +1136,11 @@ fn commit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remaining space is distributed now.
|
||||||
|
if !fr.is_zero() {
|
||||||
|
remaining = Length::zero();
|
||||||
|
}
|
||||||
|
|
||||||
let size = Size::new(width, top + bottom);
|
let size = Size::new(width, top + bottom);
|
||||||
let mut output = Frame::new(size);
|
let mut output = Frame::new(size);
|
||||||
output.baseline = Some(top);
|
output.baseline = Some(top);
|
||||||
@ -1131,12 +1156,12 @@ fn commit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return a line's items in visual order.
|
/// Return a line's items in visual order.
|
||||||
fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> {
|
fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> {
|
||||||
let mut reordered = vec![];
|
let mut reordered = vec![];
|
||||||
|
|
||||||
// The bidi crate doesn't like empty lines.
|
// The bidi crate doesn't like empty lines.
|
||||||
if line.range.is_empty() {
|
if line.trimmed.is_empty() {
|
||||||
return reordered;
|
return line.slice(line.trimmed.clone()).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the paragraph that contains the line.
|
// Find the paragraph that contains the line.
|
||||||
@ -1144,24 +1169,25 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> {
|
|||||||
.bidi
|
.bidi
|
||||||
.paragraphs
|
.paragraphs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|para| para.range.contains(&line.range.start))
|
.find(|para| para.range.contains(&line.trimmed.start))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Compute the reordered ranges in visual order (left to right).
|
// Compute the reordered ranges in visual order (left to right).
|
||||||
let (levels, runs) = line.bidi.visual_runs(para, line.range.clone());
|
let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
|
||||||
|
|
||||||
// Collect the reordered items.
|
// Collect the reordered items.
|
||||||
for run in runs {
|
for run in runs {
|
||||||
let first_idx = line.find(run.start);
|
// Skip reset L1 runs because handling them would require reshaping
|
||||||
let last_idx = line.find(run.end - 1);
|
// again in some cases.
|
||||||
let range = first_idx ..= last_idx;
|
if line.bidi.levels[run.start] != levels[run.start] {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Provide the items forwards or backwards depending on the run's
|
let prev = reordered.len();
|
||||||
// direction.
|
reordered.extend(line.slice(run.clone()));
|
||||||
if levels[run.start].is_ltr() {
|
|
||||||
reordered.extend(range.filter_map(|i| line.get(i)));
|
if levels[run.start].is_rtl() {
|
||||||
} else {
|
reordered[prev ..].reverse();
|
||||||
reordered.extend(range.rev().filter_map(|i| line.get(i)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 9.1 KiB |
@ -21,7 +21,14 @@ Add #h(10pt) #h(10pt) up
|
|||||||
// Test spacing collapsing before spacing.
|
// Test spacing collapsing before spacing.
|
||||||
#set par(align: right)
|
#set par(align: right)
|
||||||
A #h(0pt) B #h(0pt) \
|
A #h(0pt) B #h(0pt) \
|
||||||
A B
|
A B \
|
||||||
|
A #h(-1fr) B
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test RTL spacing.
|
||||||
|
#set text(dir: rtl)
|
||||||
|
A #h(10pt) B \
|
||||||
|
A #h(1fr) B
|
||||||
|
|
||||||
---
|
---
|
||||||
// Missing spacing.
|
// Missing spacing.
|
||||||
|
@ -45,6 +45,10 @@ Lריווח #h(1cm) R
|
|||||||
#set text(lang: "he", "IBM Plex Serif")
|
#set text(lang: "he", "IBM Plex Serif")
|
||||||
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
|
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test whether L1 whitespace resetting destroys stuff.
|
||||||
|
الغالب #h(70pt) ن{" "}ة
|
||||||
|
|
||||||
---
|
---
|
||||||
// Test setting a vertical direction.
|
// Test setting a vertical direction.
|
||||||
// Ref: false
|
// Ref: false
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Test the `repeat` function.
|
// Test the `repeat` function.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
// Test multiple repeats.
|
||||||
#let sections = (
|
#let sections = (
|
||||||
("Introduction", 1),
|
("Introduction", 1),
|
||||||
("Approach", 1),
|
("Approach", 1),
|
||||||
@ -13,3 +14,26 @@
|
|||||||
#for section in sections [
|
#for section in sections [
|
||||||
#section(0) #repeat[.] #section(1) \
|
#section(0) #repeat[.] #section(1) \
|
||||||
]
|
]
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test dots with RTL.
|
||||||
|
#set text(lang: "ar")
|
||||||
|
مقدمة #repeat[.] 15
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test empty repeat.
|
||||||
|
A #repeat[] B
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test spaceless repeat.
|
||||||
|
A#repeat(rect(width: 2.5em, height: 1em))B
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test single repeat in both directions.
|
||||||
|
A#repeat(rect(width: 6em, height: 0.7em))B
|
||||||
|
|
||||||
|
#set par(align: center)
|
||||||
|
A#repeat(rect(width: 6em, height: 0.7em))B
|
||||||
|
|
||||||
|
#set text(dir: rtl)
|
||||||
|
ريجين#repeat(rect(width: 4em, height: 0.7em))سون
|
||||||
|
Loading…
x
Reference in New Issue
Block a user