From 723841f9347a901081349fc4a6895c01445194ac Mon Sep 17 00:00:00 2001 From: Adam Kowalski Date: Fri, 16 Jan 2026 21:35:53 -0800 Subject: [PATCH] fix: improved dynamic ligature probing to handle contextual alternates --- src/shape.rs | 24 ++++++++++++++++++++++-- tests/shaping_and_rendering.rs | 14 +++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/shape.rs b/src/shape.rs index 339a14a..9767e61 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -863,10 +863,30 @@ impl ShapeSpan { false, ); - // If we get fewer glyphs than characters, it's likely a ligature. - if glyphs.len() == 1 { + // 1. If we have fewer glyphs than chars, it's definitely a ligature (e.g. -> becoming 1 arrow). + if glyphs.len() < probe_text.chars().count() { continue; } + + // 2. If we have the same number of glyphs, they might be contextual alternates (e.g. |> becoming 2 special glyphs). + // Check if the glyphs match the standard "cmap" (character to glyph) mapping. + // If they differ, the shaper substituted them, so we should keep them together. + if glyphs.len() == probe_text.chars().count() { + let charmap = font.as_swash().charmap(); + let mut is_modified = false; + for (i, c) in probe_text.chars().enumerate() { + let std_id = charmap.map(c); + if glyphs[i].glyph_id != std_id { + is_modified = true; + break; + } + } + + if is_modified { + // Ligature/Contextual Alternate detected! + continue; + } + } } } } diff --git a/tests/shaping_and_rendering.rs b/tests/shaping_and_rendering.rs index 1166b6e..6ef28da 100644 --- a/tests/shaping_and_rendering.rs +++ b/tests/shaping_and_rendering.rs @@ -94,12 +94,12 @@ fn test_ligature_segmentation() { let shape = line.shape_opt().expect("ShapeLine not found"); let span = &shape.spans[0]; - // Inter-Regular does NOT have a ligature for |>, so we expect it to be split. - // This confirms that we didn't break valid wrapping for non-ligatures. + // Inter-Regular HAS a contextual alternate for |> (changing the glyph ID), + // so our probe detects it and keeps them together. assert_eq!( span.words.len(), - 2, - "Expected '|>' to be 2 words (no ligature in Inter), but found {} words.", + 1, + "Expected '|>' to be 1 word (contextual alternate in Inter), but found {} words.", span.words.len() ); @@ -121,11 +121,11 @@ fn test_ligature_segmentation() { buffer.shape_until_scroll(false); let line = &buffer.lines[0]; let shape = line.shape_opt().expect("ShapeLine not found"); - // Inter-Regular does not have a != ligature. + // Inter has a contextual alternate for != too. assert_eq!( shape.spans[0].words.len(), - 2, - "Expected '!=' to be 2 words (no ligature), but found {} words.", + 1, + "Expected '!=' to be 1 word (contextual alternate), but found {} words.", shape.spans[0].words.len() );