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
3 changes: 1 addition & 2 deletions Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"regexp-v-flag",
"source-phase-imports",
"tail-call-optimization",
"Temporal",
"u180e"
"Temporal"
],
"ExcludedFlags": [
"CanBlockIsFalse"
Expand Down
122 changes: 120 additions & 2 deletions Jint/Native/String/StringPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,132 @@ private JsValue ToLocaleLowerCase(JsValue thisObject, JsCallArguments arguments)
{
TypeConverter.RequireObjectCoercible(_engine, thisObject);
var s = TypeConverter.ToString(thisObject);
return new JsString(s.ToLower(CultureInfo.InvariantCulture));
return ToLowerCaseWithSpecialCasing(s, CultureInfo.InvariantCulture);
}

private JsValue ToLowerCase(JsValue thisObject, JsCallArguments arguments)
{
TypeConverter.RequireObjectCoercible(_engine, thisObject);
var s = TypeConverter.ToString(thisObject);
return s.ToLowerInvariant();
return ToLowerCaseWithSpecialCasing(s, CultureInfo.InvariantCulture);
}

/// <summary>
/// Converts string to lowercase with Unicode special casing rules.
/// Handles Final_Sigma context for Greek capital sigma (U+03A3).
/// https://unicode.org/reports/tr21/tr21-5.html#SpecialCasing
/// </summary>
private static string ToLowerCaseWithSpecialCasing(string s, CultureInfo culture)
{
const char GreekCapitalSigma = '\u03A3';

// Fast path: if no Greek capital sigma, use standard lowercase
if (s.IndexOf(GreekCapitalSigma) < 0)
{
return s.ToLower(culture);
}

// Need to handle Final_Sigma context
var result = new char[s.Length];
for (var i = 0; i < s.Length; i++)
{
var c = s[i];
if (c == GreekCapitalSigma)
{
// Check if this is a Final_Sigma context
// Final_Sigma: preceded by cased letter (skipping Case_Ignorable), not followed by cased letter (skipping Case_Ignorable)
result[i] = IsFinalSigmaContext(s, i) ? '\u03C2' : '\u03C3';
}
else
{
result[i] = char.ToLower(c, culture);
}
}

return new string(result);
}

/// <summary>
/// Determines if the character at the given position is in a Final_Sigma context.
/// Final_Sigma: C is preceded by a sequence consisting of a cased letter and then zero or more Case_Ignorable characters,
/// and C is NOT followed by a sequence consisting of zero or more Case_Ignorable characters and then a cased letter.
/// https://unicode.org/reports/tr21/tr21-5.html#Context
/// </summary>
private static bool IsFinalSigmaContext(string s, int index)
{
// Check backward: must find a cased letter (skipping Case_Ignorable)
var foundCasedBefore = false;
for (var i = index - 1; i >= 0; i--)
{
var c = s[i];
if (IsCased(c))
{
foundCasedBefore = true;
break;
}
if (!IsCaseIgnorable(c))
{
break;
}
}

if (!foundCasedBefore)
{
return false;
}

// Check forward: must NOT find a cased letter (skipping Case_Ignorable)
for (var i = index + 1; i < s.Length; i++)
{
var c = s[i];
if (IsCased(c))
{
return false; // Found cased letter after, so NOT Final_Sigma
}
if (!IsCaseIgnorable(c))
{
break;
}
}

return true;
}

/// <summary>
/// Checks if a character is "cased" (has uppercase or lowercase property).
/// A character is cased if it has the Lowercase or Uppercase property, or has General_Category=Titlecase_Letter.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCased(char c)
{
// Cased = Lowercase OR Uppercase OR General_Category=Lt
return char.IsLetter(c) && (char.IsLower(c) || char.IsUpper(c) || CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.TitlecaseLetter);
}

/// <summary>
/// Checks if a character is Case_Ignorable.
/// Case_Ignorable characters include: Mn (Nonspacing_Mark), Me (Enclosing_Mark), Cf (Format),
/// Lm (Modifier_Letter), Sk (Modifier_Symbol), and characters with Word_Break property MidLetter, MidNumLet, or Single_Quote.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsCaseIgnorable(char c)
{
var category = CharUnicodeInfo.GetUnicodeCategory(c);
return category == UnicodeCategory.NonSpacingMark || // Mn
category == UnicodeCategory.EnclosingMark || // Me
category == UnicodeCategory.Format || // Cf (includes U+180E Mongolian Vowel Separator)
category == UnicodeCategory.ModifierLetter || // Lm
category == UnicodeCategory.ModifierSymbol || // Sk
c == '\u0027' || // APOSTROPHE (Word_Break=Single_Quote)
c == '\u00B7' || // MIDDLE DOT (Word_Break=MidLetter)
c == '\u0387' || // GREEK ANO TELEIA (Word_Break=MidLetter)
c == '\u05F4' || // HEBREW PUNCTUATION GERSHAYIM (Word_Break=MidLetter)
c == '\u2019' || // RIGHT SINGLE QUOTATION MARK (Word_Break=Single_Quote)
c == '\u2027' || // HYPHENATION POINT (Word_Break=MidLetter)
c == '\uFE13' || // PRESENTATION FORM FOR VERTICAL COLON (Word_Break=MidLetter)
c == '\uFE55' || // SMALL COLON (Word_Break=MidLetter)
c == '\uFF07' || // FULLWIDTH APOSTROPHE (Word_Break=MidNumLet)
c == '\uFF1A'; // FULLWIDTH COLON (Word_Break=MidLetter)
}

private static int ToIntegerSupportInfinity(JsValue numberVal)
Expand Down