Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/grapheme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,12 @@ impl GraphemeCursor {
use crate::tables::grapheme as gr;
assert!(chunk_start.saturating_add(chunk.len()) == self.pre_context_offset.unwrap());
self.pre_context_offset = None;
if self.is_extended && chunk_start + chunk.len() == self.offset {
// GB4 and GB5 come before GB9b, so a Control, CR or LF after the
// Prepend still breaks; only apply the GB9b shortcut when the
// following category cannot be decided by an earlier rule.
let after_breaks_first =
matches!(self.cat_after, Some(gr::GC_Control | gr::GC_CR | gr::GC_LF));
if self.is_extended && !after_breaks_first && chunk_start + chunk.len() == self.offset {
let ch = chunk.chars().next_back().unwrap();
if self.grapheme_category(ch) == gr::GC_Prepend {
self.decide(false); // GB9b
Expand Down Expand Up @@ -918,6 +923,29 @@ fn test_grapheme_cursor_chunk_start_require_precontext() {
assert_eq!(c.is_boundary(&s[1..], 1), Ok(false));
}

#[test]
fn test_grapheme_cursor_prepend_before_control_on_chunk_start() {
// GB4 and GB5 take precedence over GB9b, so `Prepend` does not glue a
// following `Control`, `CR` or `LF` to itself. GraphemeBreakTest.txt:
// ÷ 06DD ÷ 000D ÷ # ÷ [0.2] ARABIC END OF AYAH (Prepend) ÷ [5.0] <CR> (CR) ÷ [0.3]
for s in ["\u{06dd}\r", "\u{06dd}\n", "\u{06dd}\0"] {
let prepend_len = "\u{06dd}".len();
// The whole-string answer, which the `Graphemes` iterator already gets right.
assert_eq!(
GraphemeCursor::new(prepend_len, s.len(), true).is_boundary(s, 0),
Ok(true)
);
// The same query answered from separate chunks must agree.
let mut c = GraphemeCursor::new(prepend_len, s.len(), true);
assert_eq!(
c.is_boundary(&s[prepend_len..], prepend_len),
Err(GraphemeIncomplete::PreContext(prepend_len))
);
c.provide_context(&s[..prepend_len], 0);
assert_eq!(c.is_boundary(&s[prepend_len..], prepend_len), Ok(true));
}
}

#[test]
fn test_grapheme_cursor_prev_boundary() {
let s = "abcd";
Expand Down
56 changes: 56 additions & 0 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ fn test_graphemes() {
"\u{1F938}\u{1F3FE}\u{1F3FE}",
&["\u{1F938}\u{1F3FE}\u{1F3FE}"],
),
// GB4 and GB5 come before GB9b, so a Prepend does not join to a
// following Control, CR or LF. ARABIC END OF AYAH is Prepend.
("\u{06DD}\r", &["\u{06DD}", "\r"]),
("\u{06DD}\n", &["\u{06DD}", "\n"]),
("\u{06DD}\u{0000}", &["\u{06DD}", "\u{0000}"]),
// ARABIC NUMBER SIGN, the other Prepend used in these tests
("\u{0600}\r", &["\u{0600}", "\r"]),
];

for &(s, g) in TEST_SAME.iter().chain(EXTRA_SAME) {
Expand Down Expand Up @@ -107,6 +114,55 @@ fn test_graphemes() {
assert_eq!(gr, b);
}

// The `graphemes` iterator sees the whole string at once, so it always had these
// right. `GraphemeCursor` answers the same question one chunk at a time, and
// `provide_context` applied GB9b directly on a trailing Prepend without checking
// whether GB4 or GB5 had already decided the boundary. Feeding the input one
// codepoint at a time is the smallest way to reach that path.
#[test]
fn test_grapheme_cursor_chunked_matches_iterator() {
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};

const CASES: &[&str] = &[
"\u{06DD}\r",
"\u{06DD}\n",
"\u{06DD}\u{0000}",
"\u{0600}\r",
"\u{0600}a",
"a\u{06DD}\r",
];

for &s in CASES {
for is_extended in [true, false] {
// Prepend is only joined in extended mode, so the expected
// boundaries have to come from the matching iterator.
let expected: Vec<usize> = std::iter::once(0)
.chain(
UnicodeSegmentation::grapheme_indices(s, is_extended).map(|(i, g)| i + g.len()),
)
.collect();

for (offset, _) in s.char_indices().skip(1) {
let mut cursor = GraphemeCursor::new(offset, s.len(), is_extended);
let boundary = loop {
match cursor.is_boundary(&s[offset..], offset) {
Ok(b) => break b,
Err(GraphemeIncomplete::PreContext(n)) => {
cursor.provide_context(&s[..n], 0);
}
Err(e) => panic!("{:?} at {}: unexpected {:?}", s, offset, e),
}
};
assert_eq!(
boundary,
expected.contains(&offset),
"{s:?} extended={is_extended} offset={offset}"
);
}
}
}
}

#[test]
fn test_words() {
use crate::testdata::TEST_WORD;
Expand Down
Loading