diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 6328e2faeb..7702bd09be 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -117,11 +117,8 @@ // Many of these tests require non-English locales (Spanish, Polish, German, Chinese, etc.) or // advanced features like formatRange, calendar-specific formatting, or complex locale canonicalization. - // Collator - German phonebook collation and locale-specific collation data + // Collator - German phonebook collation "intl402/Collator/prototype/compare/non-normative-phonebook.js", - "intl402/Collator/prototype/resolvedOptions/resolved-collation-unicode-extensions-and-options.js", - "intl402/Collator/unicode-ext-seq-in-private-tag.js", - "intl402/Collator/unicode-ext-value-collation.js", "intl402/Collator/usage-de.js", // DateTimeFormat - hanidec numbering system has different AM/PM behavior in ICU/CLDR @@ -132,22 +129,10 @@ "intl402/DateTimeFormat/prototype/formatRangeToParts/dangi-calendar-dates.js", "intl402/DateTimeFormat/prototype/formatToParts/chinese-calendar-dates.js", - // DisplayNames - locale-specific display names and Symbol edge cases - "intl402/DisplayNames/locales-symbol-length.js", - - // DurationFormat - formatting with specific styles and locales - "intl402/DurationFormat/prototype/format/duration-out-of-range-3.js", - "intl402/DurationFormat/prototype/format/duration-out-of-range-4.js", + // DurationFormat - precision formatting requires exact sub-second arithmetic + "intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js", // Requires Spanish unit patterns (ICU4N doesn't include unit data, only core locale data) "intl402/DurationFormat/prototype/format/mixed-non-numeric-styles-es.js", - "intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js", - "intl402/DurationFormat/prototype/resolvedOptions/resolved-numbering-system-unicode-extensions-and-options.js", - - // Locale canonicalization - variant/region aliasing requires CLDR data - "intl402/Intl/getCanonicalLocales/complex-region-subtag-replacement.js", - "intl402/Intl/getCanonicalLocales/non-iana-canon.js", - "intl402/Intl/getCanonicalLocales/preferred-variant.js", - "intl402/Locale/reject-duplicate-variants-in-tlang.js", // NumberFormat - locale-specific formatting and formatRange "intl402/NumberFormat/prototype/format/format-significant-digits.js", @@ -163,28 +148,15 @@ "intl402/NumberFormat/prototype/formatToParts/unit-ko-KR.js", "intl402/NumberFormat/prototype/formatToParts/unit-zh-TW.js", - // PluralRules - plural categories require full CLDR data, notation requires compact support - "intl402/PluralRules/prototype/resolvedOptions/plural-categories-order.js", + // PluralRules - notation requires compact support "intl402/PluralRules/prototype/select/notation.js", - // String - Turkish/Lithuanian/Azeri locale-specific case conversions - "intl402/String/prototype/toLocaleLowerCase/capital_I_with_dot.js", - "intl402/String/prototype/toLocaleLowerCase/special_casing_Azeri.js", - "intl402/String/prototype/toLocaleLowerCase/special_casing_Lithuanian.js", - "intl402/String/prototype/toLocaleLowerCase/special_casing_Turkish.js", - "intl402/String/prototype/toLocaleLowerCase/validates-all-locale-identifiers.js", - "intl402/String/prototype/toLocaleUpperCase/validates-all-locale-identifiers.js", - - // supportedValuesOf - currencies accepted by DisplayNames check - "intl402/Intl/supportedValuesOf/currencies-accepted-by-DisplayNames.js", - // Misc intl402 tests requiring full locale support "intl402/fallback-locales-are-supported.js", "intl402/supportedLocalesOf-consistent-with-resolvedOptions.js", // misc features that require investigation "intl402/NumberFormat/prototype/format/numbering-systems.js", - "intl402/Intl/supportedValuesOf/numberingSystems-with-simple-digit-mappings.js", // Temporal "intl402/Temporal/Duration/**/*.js", diff --git a/Jint/Native/BigInt/BigIntConstructor.cs b/Jint/Native/BigInt/BigIntConstructor.cs index fde3975575..9d7b4ffcfb 100644 --- a/Jint/Native/BigInt/BigIntConstructor.cs +++ b/Jint/Native/BigInt/BigIntConstructor.cs @@ -90,7 +90,7 @@ private JsBigInt NumberToBigInt(JsNumber value) { if (TypeConverter.IsIntegralNumber(value._value)) { - return JsBigInt.Create((long) value._value); + return JsBigInt.Create(new BigInteger(value._value)); } Throw.RangeError(_realm, "The number " + value + " cannot be converted to a BigInt because it is not an integer"); diff --git a/Jint/Native/Intl/CollatorConstructor.cs b/Jint/Native/Intl/CollatorConstructor.cs index 3b0edff6df..b9486acea4 100644 --- a/Jint/Native/Intl/CollatorConstructor.cs +++ b/Jint/Native/Intl/CollatorConstructor.cs @@ -26,6 +26,24 @@ internal sealed class CollatorConstructor : Constructor "searchjl", "stroke", "trad", "unihan", "zhuyin", "default" ], StringComparison.Ordinal); + // Locale-specific supported collation types (from CLDR) + // Only locales with non-default collation support are listed + private static readonly Dictionary> LocaleCollationSupport = new(StringComparer.OrdinalIgnoreCase) + { + ["ar"] = new(StringComparer.Ordinal) { "default", "compat", "eor" }, + ["da"] = new(StringComparer.Ordinal) { "default", "eor" }, + ["de"] = new(StringComparer.Ordinal) { "default", "phonebk", "eor" }, + ["en"] = new(StringComparer.Ordinal) { "default", "ducet", "emoji", "eor" }, + ["es"] = new(StringComparer.Ordinal) { "default", "trad", "eor" }, + ["hi"] = new(StringComparer.Ordinal) { "default", "direct", "eor" }, + ["ja"] = new(StringComparer.Ordinal) { "default", "unihan", "eor" }, + ["ko"] = new(StringComparer.Ordinal) { "default", "searchjl", "unihan", "eor" }, + ["ln"] = new(StringComparer.Ordinal) { "default", "phonetic", "eor" }, + ["si"] = new(StringComparer.Ordinal) { "default", "dict", "eor" }, + ["sv"] = new(StringComparer.Ordinal) { "default", "reformed", "eor" }, + ["zh"] = new(StringComparer.Ordinal) { "default", "big5han", "gb2312", "pinyin", "stroke", "unihan", "zhuyin", "eor" }, + }; + public CollatorConstructor( Engine engine, Realm realm, @@ -90,7 +108,7 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT // Get options (options override unicode extensions) var usage = GetStringOption(optionsObj, "usage", UsageValues, "sort"); var sensitivity = GetSensitivity(optionsObj); - var collation = GetCollationOption(optionsObj, uCollation); + var collation = GetCollationOption(optionsObj, uCollation, resolvedLocale); var numeric = GetNumericOption(optionsObj, uNumeric); var caseFirst = GetCaseFirstOption(optionsObj, uCaseFirst); @@ -224,13 +242,18 @@ private static void ParseUnicodeExtensions(string locale, out string? collation, numeric = null; caseFirst = null; - var uIndex = locale.IndexOf("-u-", StringComparison.Ordinal); + // Only search for -u- before the private-use section (-x-) + var xIndex = locale.IndexOf("-x-", StringComparison.OrdinalIgnoreCase); + var searchRange = xIndex >= 0 ? locale.Substring(0, xIndex) : locale; + var uIndex = searchRange.IndexOf("-u-", StringComparison.Ordinal); if (uIndex < 0) { return; } - var parts = locale.Substring(uIndex + 3).Split('-'); + // Extract the -u- extension content (up to private-use or next singleton) + var extensionContent = (xIndex >= 0 ? locale.Substring(uIndex + 3, xIndex - uIndex - 3) : locale.Substring(uIndex + 3)); + var parts = extensionContent.Split('-'); for (var i = 0; i < parts.Length; i++) { var key = parts[i]; @@ -270,35 +293,41 @@ private static void ParseUnicodeExtensions(string locale, out string? collation, } } - private static string GetCollationOption(ObjectInstance options, string? unicodeExtension) + private static string GetCollationOption(ObjectInstance options, string? unicodeExtension, string resolvedLocale) { + // Get the language code for locale-specific collation support + var langCode = resolvedLocale; + var dashIdx = resolvedLocale.IndexOf('-'); + if (dashIdx > 0) + { + langCode = resolvedLocale.Substring(0, dashIdx); + } + var value = options.Get("collation"); if (!value.IsUndefined()) { var collation = TypeConverter.ToString(value); // Per ECMA-402: "standard" and "search" collations are explicitly disallowed - // They should fall back to "default" if (string.Equals(collation, "standard", StringComparison.Ordinal) || string.Equals(collation, "search", StringComparison.Ordinal)) { - return "default"; + // Fall through to check unicode extension } - - // Validate against known collation types - ignore invalid values - if (!ValidCollationTypes.Contains(collation)) + // Validate against known collation types AND locale-specific support + else if (ValidCollationTypes.Contains(collation) && IsCollationSupportedForLocale(langCode, collation)) { - return "default"; + return collation; } - - return collation; + // Options value is not supported - fall through to check unicode extension } // Check unicode extension, but disallow "standard", "search", and invalid values if (unicodeExtension != null && !string.Equals(unicodeExtension, "standard", StringComparison.Ordinal) && !string.Equals(unicodeExtension, "search", StringComparison.Ordinal) && - ValidCollationTypes.Contains(unicodeExtension)) + ValidCollationTypes.Contains(unicodeExtension) && + IsCollationSupportedForLocale(langCode, unicodeExtension)) { return unicodeExtension; } @@ -306,6 +335,23 @@ private static string GetCollationOption(ObjectInstance options, string? unicode return "default"; } + private static bool IsCollationSupportedForLocale(string language, string collation) + { + if (string.Equals(collation, "default", StringComparison.Ordinal)) + { + return true; + } + + // If we have explicit locale data, check against it + if (LocaleCollationSupport.TryGetValue(language, out var supported)) + { + return supported.Contains(collation); + } + + // For unlisted locales, only "default" and "eor" are universally supported + return string.Equals(collation, "eor", StringComparison.Ordinal); + } + private static bool GetNumericOption(ObjectInstance options, bool? unicodeExtension) { var value = options.Get("numeric"); diff --git a/Jint/Native/Intl/Data/LocaleData.cs b/Jint/Native/Intl/Data/LocaleData.cs index 21b6d1ab4c..dddff84d9a 100644 --- a/Jint/Native/Intl/Data/LocaleData.cs +++ b/Jint/Native/Intl/Data/LocaleData.cs @@ -238,14 +238,20 @@ private static void ParseComplexLanguageMapping(string key, string value) private static void ParseVariantMapping(string key, string value) { - // Format: type,replacement - var commaIndex = value.IndexOf(','); - if (commaIndex > 0) + // Format: type,replacement[,prefix:variant] + var parts = value.Split(','); + if (parts.Length >= 2) { - _variantMappings![key] = new VariantMapping( - value.Substring(0, commaIndex), - value.Substring(commaIndex + 1) - ); + string? prefix = null; + for (var i = 2; i < parts.Length; i++) + { + if (parts[i].StartsWith("prefix:", StringComparison.Ordinal)) + { + prefix = parts[i].Substring(7); + } + } + + _variantMappings![key] = new VariantMapping(parts[0], parts[1], prefix); } } @@ -284,13 +290,19 @@ public ComplexLanguageMapping(string language, string? script, string? region) internal readonly struct VariantMapping { - public VariantMapping(string type, string replacement) + public VariantMapping(string type, string replacement, string? prefix = null) { Type = type; Replacement = replacement; + Prefix = prefix; } public string Type { get; } public string Replacement { get; } + + /// + /// Optional prefix variant that should be removed when this alias is applied. + /// + public string? Prefix { get; } } } diff --git a/Jint/Native/Intl/Data/LocaleData.txt b/Jint/Native/Intl/Data/LocaleData.txt index d560939efd..4f6a7e24ec 100644 --- a/Jint/Native/Intl/Data/LocaleData.txt +++ b/Jint/Native/Intl/Data/LocaleData.txt @@ -694,12 +694,16 @@ WK=UM YD=YE YU=RS ZR=CD +810=RU +NT=SA +SU=RU +AN=CW [VARIANT_MAPPINGS] aaland=region,AX arevela=language,hy arevmda=language,hyw -heploc=variant,alalc97 +heploc=variant,alalc97,prefix:hepburn polytoni=variant,polyton [LANGUAGE_VARIANT_MAPPINGS] @@ -715,6 +719,13 @@ Cyrl+SU=RU Latn+SU=RU Geor+SU=GE Arab+SU=UZ +Armn+810=AM +Cyrl+810=RU +Latn+810=RU +Geor+810=GE +Arab+810=UZ +Cyrl+CS=RS +Latn+CS=RS [UNICODE_MAPPINGS] ca:ethiopic-amete-alem=ethioaa diff --git a/Jint/Native/Intl/Data/NumberingSystemData.cs b/Jint/Native/Intl/Data/NumberingSystemData.cs index 961fe16579..0a64fd5264 100644 --- a/Jint/Native/Intl/Data/NumberingSystemData.cs +++ b/Jint/Native/Intl/Data/NumberingSystemData.cs @@ -24,9 +24,11 @@ internal static class NumberingSystemData ["deva"] = "\u0966\u0967\u0968\u0969\u096A\u096B\u096C\u096D\u096E\u096F", ["diak"] = "\U00011950\U00011951\U00011952\U00011953\U00011954\U00011955\U00011956\U00011957\U00011958\U00011959", ["fullwide"] = "\uFF10\uFF11\uFF12\uFF13\uFF14\uFF15\uFF16\uFF17\uFF18\uFF19", + ["gara"] = "\U00010D40\U00010D41\U00010D42\U00010D43\U00010D44\U00010D45\U00010D46\U00010D47\U00010D48\U00010D49", ["gong"] = "\U00011DA0\U00011DA1\U00011DA2\U00011DA3\U00011DA4\U00011DA5\U00011DA6\U00011DA7\U00011DA8\U00011DA9", ["gonm"] = "\U00011D50\U00011D51\U00011D52\U00011D53\U00011D54\U00011D55\U00011D56\U00011D57\U00011D58\U00011D59", ["gujr"] = "\u0AE6\u0AE7\u0AE8\u0AE9\u0AEA\u0AEB\u0AEC\u0AED\u0AEE\u0AEF", + ["gukh"] = "\U00016130\U00016131\U00016132\U00016133\U00016134\U00016135\U00016136\U00016137\U00016138\U00016139", ["guru"] = "\u0A66\u0A67\u0A68\u0A69\u0A6A\u0A6B\u0A6C\u0A6D\u0A6E\u0A6F", ["hanidec"] = "\u3007\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D", ["hmng"] = "\U00016B50\U00016B51\U00016B52\U00016B53\U00016B54\U00016B55\U00016B56\U00016B57\U00016B58\U00016B59", @@ -36,6 +38,7 @@ internal static class NumberingSystemData ["kawi"] = "\U00011F50\U00011F51\U00011F52\U00011F53\U00011F54\U00011F55\U00011F56\U00011F57\U00011F58\U00011F59", ["khmr"] = "\u17E0\u17E1\u17E2\u17E3\u17E4\u17E5\u17E6\u17E7\u17E8\u17E9", ["knda"] = "\u0CE6\u0CE7\u0CE8\u0CE9\u0CEA\u0CEB\u0CEC\u0CED\u0CEE\u0CEF", + ["krai"] = "\U00016D70\U00016D71\U00016D72\U00016D73\U00016D74\U00016D75\U00016D76\U00016D77\U00016D78\U00016D79", ["lana"] = "\u1A80\u1A81\u1A82\u1A83\u1A84\u1A85\u1A86\u1A87\u1A88\u1A89", ["lanatham"] = "\u1A90\u1A91\u1A92\u1A93\u1A94\u1A95\u1A96\u1A97\u1A98\u1A99", ["laoo"] = "\u0ED0\u0ED1\u0ED2\u0ED3\u0ED4\u0ED5\u0ED6\u0ED7\u0ED8\u0ED9", @@ -53,14 +56,18 @@ internal static class NumberingSystemData ["mroo"] = "\U00016A60\U00016A61\U00016A62\U00016A63\U00016A64\U00016A65\U00016A66\U00016A67\U00016A68\U00016A69", ["mtei"] = "\uABF0\uABF1\uABF2\uABF3\uABF4\uABF5\uABF6\uABF7\uABF8\uABF9", ["mymr"] = "\u1040\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049", + ["mymrepka"] = "\U000116DA\U000116DB\U000116DC\U000116DD\U000116DE\U000116DF\U000116E0\U000116E1\U000116E2\U000116E3", + ["mymrpao"] = "\U000116D0\U000116D1\U000116D2\U000116D3\U000116D4\U000116D5\U000116D6\U000116D7\U000116D8\U000116D9", ["mymrshan"] = "\u1090\u1091\u1092\u1093\u1094\u1095\u1096\u1097\u1098\u1099", ["mymrtlng"] = "\uA9F0\uA9F1\uA9F2\uA9F3\uA9F4\uA9F5\uA9F6\uA9F7\uA9F8\uA9F9", ["nagm"] = "\U0001E4F0\U0001E4F1\U0001E4F2\U0001E4F3\U0001E4F4\U0001E4F5\U0001E4F6\U0001E4F7\U0001E4F8\U0001E4F9", ["newa"] = "\U00011450\U00011451\U00011452\U00011453\U00011454\U00011455\U00011456\U00011457\U00011458\U00011459", ["nkoo"] = "\u07C0\u07C1\u07C2\u07C3\u07C4\u07C5\u07C6\u07C7\u07C8\u07C9", ["olck"] = "\u1C50\u1C51\u1C52\u1C53\u1C54\u1C55\u1C56\u1C57\u1C58\u1C59", + ["onao"] = "\U0001E5F1\U0001E5F2\U0001E5F3\U0001E5F4\U0001E5F5\U0001E5F6\U0001E5F7\U0001E5F8\U0001E5F9\U0001E5FA", ["orya"] = "\u0B66\u0B67\u0B68\u0B69\u0B6A\u0B6B\u0B6C\u0B6D\u0B6E\u0B6F", ["osma"] = "\U000104A0\U000104A1\U000104A2\U000104A3\U000104A4\U000104A5\U000104A6\U000104A7\U000104A8\U000104A9", + ["outlined"] = "\U0001CCF0\U0001CCF1\U0001CCF2\U0001CCF3\U0001CCF4\U0001CCF5\U0001CCF6\U0001CCF7\U0001CCF8\U0001CCF9", ["rohg"] = "\U00010D30\U00010D31\U00010D32\U00010D33\U00010D34\U00010D35\U00010D36\U00010D37\U00010D38\U00010D39", ["saur"] = "\uA8D0\uA8D1\uA8D2\uA8D3\uA8D4\uA8D5\uA8D6\uA8D7\uA8D8\uA8D9", ["segment"] = "\U0001FBF0\U0001FBF1\U0001FBF2\U0001FBF3\U0001FBF4\U0001FBF5\U0001FBF6\U0001FBF7\U0001FBF8\U0001FBF9", @@ -69,6 +76,7 @@ internal static class NumberingSystemData ["sinh"] = "\u0DE6\u0DE7\u0DE8\u0DE9\u0DEA\u0DEB\u0DEC\u0DED\u0DEE\u0DEF", ["sora"] = "\U000110F0\U000110F1\U000110F2\U000110F3\U000110F4\U000110F5\U000110F6\U000110F7\U000110F8\U000110F9", ["sund"] = "\u1BB0\u1BB1\u1BB2\u1BB3\u1BB4\u1BB5\u1BB6\u1BB7\u1BB8\u1BB9", + ["sunu"] = "\U00011BF0\U00011BF1\U00011BF2\U00011BF3\U00011BF4\U00011BF5\U00011BF6\U00011BF7\U00011BF8\U00011BF9", ["takr"] = "\U000116C0\U000116C1\U000116C2\U000116C3\U000116C4\U000116C5\U000116C6\U000116C7\U000116C8\U000116C9", ["talu"] = "\u19D0\u19D1\u19D2\u19D3\u19D4\u19D5\u19D6\u19D7\u19D8\u19D9", ["tamldec"] = "\u0BE6\u0BE7\u0BE8\u0BE9\u0BEA\u0BEB\u0BEC\u0BED\u0BEE\u0BEF", @@ -77,6 +85,7 @@ internal static class NumberingSystemData ["tibt"] = "\u0F20\u0F21\u0F22\u0F23\u0F24\u0F25\u0F26\u0F27\u0F28\u0F29", ["tirh"] = "\U000114D0\U000114D1\U000114D2\U000114D3\U000114D4\U000114D5\U000114D6\U000114D7\U000114D8\U000114D9", ["tnsa"] = "\U00016AC0\U00016AC1\U00016AC2\U00016AC3\U00016AC4\U00016AC5\U00016AC6\U00016AC7\U00016AC8\U00016AC9", + ["tols"] = "\U00011DE0\U00011DE1\U00011DE2\U00011DE3\U00011DE4\U00011DE5\U00011DE6\U00011DE7\U00011DE8\U00011DE9", ["vaii"] = "\uA620\uA621\uA622\uA623\uA624\uA625\uA626\uA627\uA628\uA629", ["wara"] = "\U000118E0\U000118E1\U000118E2\U000118E3\U000118E4\U000118E5\U000118E6\U000118E7\U000118E8\U000118E9", ["wcho"] = "\U0001E2F0\U0001E2F1\U0001E2F2\U0001E2F3\U0001E2F4\U0001E2F5\U0001E2F6\U0001E2F7\U0001E2F8\U0001E2F9", diff --git a/Jint/Native/Intl/DurationFormatConstructor.cs b/Jint/Native/Intl/DurationFormatConstructor.cs index 5b7112fe86..d9a0139da3 100644 --- a/Jint/Native/Intl/DurationFormatConstructor.cs +++ b/Jint/Native/Intl/DurationFormatConstructor.cs @@ -1,5 +1,6 @@ using System.Globalization; using Jint.Native.Function; +using Jint.Native.Intl.Data; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Descriptors; @@ -68,8 +69,8 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT // Step 5: localeMatcher var localeMatcher = GetStringOption(optionsObj, "localeMatcher", LocaleMatcherValues, "best fit"); - // Step 6: numberingSystem (must be read before style) - var numberingSystem = GetNumberingSystemOption(optionsObj); + // Step 6: numberingSystem option (must be read before style) + var numberingSystemOption = GetNumberingSystemOption(optionsObj); // Step 13: style (must be read after localeMatcher and numberingSystem, before unit options) var style = GetStringOption(optionsObj, "style", StyleValues, "short"); @@ -79,6 +80,23 @@ public override ObjectInstance Construct(JsCallArguments arguments, JsValue newT var availableLocales = IntlUtilities.GetAvailableLocales(); var resolved = IntlUtilities.ResolveLocale(_engine, availableLocales, requestedLocales, localeMatcher, []); + // Parse -u-nu- extension from the first requested locale + string? extensionNu = null; + if (requestedLocales.Count > 0) + { + extensionNu = ParseNumberingSystemExtension(requestedLocales[0]); + } + + // Resolve numbering system: option overrides extension; validate both + var numberingSystem = ResolveNumberingSystem(numberingSystemOption, extensionNu); + + // Build resolved locale: include -u-nu- extension when resolved value matches extension value + var resolvedLocale = resolved.Locale; + if (extensionNu != null && string.Equals(numberingSystem, extensionNu, StringComparison.Ordinal)) + { + resolvedLocale = resolved.Locale + "-u-nu-" + numberingSystem; + } + // Determine base style based on overall style (for date units) var baseStyle = style switch { @@ -197,7 +215,7 @@ bool IsNumericLikeOrFractional(string s) => return new JsDurationFormat( _engine, proto, - resolved.Locale, + resolvedLocale, style, numberingSystem, culture, @@ -255,12 +273,12 @@ private string GetStringOption(ObjectInstance options, string property, string[] return stringValue; } - private string GetNumberingSystemOption(ObjectInstance options) + private string? GetNumberingSystemOption(ObjectInstance options) { var value = options.Get("numberingSystem"); if (value.IsUndefined()) { - return "latn"; // Default + return null; } var stringValue = TypeConverter.ToString(value); @@ -275,6 +293,48 @@ private string GetNumberingSystemOption(ObjectInstance options) return stringValue; } + private static string? ParseNumberingSystemExtension(string locale) + { + // Only search for -u- before the private-use section (-x-) + var xIndex = locale.IndexOf("-x-", StringComparison.OrdinalIgnoreCase); + var searchRange = xIndex >= 0 ? locale.Substring(0, xIndex) : locale; + var uIndex = searchRange.IndexOf("-u-", StringComparison.Ordinal); + if (uIndex < 0) + { + return null; + } + + var extensionContent = xIndex >= 0 ? locale.Substring(uIndex + 3, xIndex - uIndex - 3) : locale.Substring(uIndex + 3); + var parts = extensionContent.Split('-'); + for (var i = 0; i < parts.Length; i++) + { + if (string.Equals(parts[i], "nu", StringComparison.Ordinal) && i + 1 < parts.Length && parts[i + 1].Length >= 3) + { + return parts[i + 1]; + } + } + + return null; + } + + private static string ResolveNumberingSystem(string? optionValue, string? extensionValue) + { + // Options override extension + if (optionValue != null && NumberingSystemData.Digits.ContainsKey(optionValue)) + { + return optionValue; + } + + // Extension fallback + if (extensionValue != null && NumberingSystemData.Digits.ContainsKey(extensionValue)) + { + return extensionValue; + } + + // Default + return "latn"; + } + /// /// https://tc39.es/proposal-intl-duration-format/#sec-intl.durationformat.supportedlocalesof /// diff --git a/Jint/Native/Intl/DurationFormatPrototype.cs b/Jint/Native/Intl/DurationFormatPrototype.cs index 96d346c3fa..6ba1659d18 100644 --- a/Jint/Native/Intl/DurationFormatPrototype.cs +++ b/Jint/Native/Intl/DurationFormatPrototype.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Jint.Native.Object; using Jint.Native.Symbol; using Jint.Runtime; @@ -184,7 +185,6 @@ private JsDurationFormat.DurationRecord ToDurationRecord(JsValue value) private void ValidateDurationRecord(JsDurationFormat.DurationRecord record) { const double MaxYearsMonthsWeeks = 4294967296.0; // 2^32 - const double MaxSafeInteger = 9007199254740992.0; // 2^53 // Check if years, months, weeks are in valid range if (System.Math.Abs(record.Years) >= MaxYearsMonthsWeeks) @@ -203,16 +203,21 @@ private void ValidateDurationRecord(JsDurationFormat.DurationRecord record) // Per spec: normalizedSeconds = days × 86400 + hours × 3600 + minutes × 60 + seconds + // milliseconds × 10^-3 + microseconds × 10^-6 + nanoseconds × 10^-9 // If abs(normalizedSeconds) >= 2^53, throw RangeError - var normalizedSeconds = - record.Days * 86400 + - record.Hours * 3600 + - record.Minutes * 60 + - record.Seconds + - record.Milliseconds * 1e-3 + - record.Microseconds * 1e-6 + - record.Nanoseconds * 1e-9; - - if (System.Math.Abs(normalizedSeconds) >= MaxSafeInteger) + // Use BigInteger arithmetic to avoid double precision loss. + // Compute totalNanoseconds = normalizedSeconds × 10^9 (exact integer arithmetic) + var totalNanoseconds = + new BigInteger(record.Days) * 86_400_000_000_000 + + new BigInteger(record.Hours) * 3_600_000_000_000 + + new BigInteger(record.Minutes) * 60_000_000_000 + + new BigInteger(record.Seconds) * 1_000_000_000 + + new BigInteger(record.Milliseconds) * 1_000_000 + + new BigInteger(record.Microseconds) * 1_000 + + new BigInteger(record.Nanoseconds); + + // maxTimeDuration = 2^53 × 10^9 - 1 (the maximum valid total nanoseconds) + // abs(totalNanoseconds) >= 2^53 × 10^9 means normalizedSeconds >= 2^53 + BigInteger maxTimeDuration = ((BigInteger) 1 << 53) * 1_000_000_000; + if (BigInteger.Abs(totalNanoseconds) >= maxTimeDuration) { Throw.RangeError(_realm, "Duration time values out of range"); } diff --git a/Jint/Native/Intl/IntlUtilities.cs b/Jint/Native/Intl/IntlUtilities.cs index adfbfa647f..618186cc70 100644 --- a/Jint/Native/Intl/IntlUtilities.cs +++ b/Jint/Native/Intl/IntlUtilities.cs @@ -186,6 +186,35 @@ internal static class IntlUtilities { "FX", "FR" }, // Metropolitan France -> France }; + /// + /// Likely script for common languages (from CLDR likelySubtags). + /// Used for complex region subtag replacement when no explicit script is present. + /// + private static readonly Dictionary LikelyScripts = new(StringComparer.OrdinalIgnoreCase) + { + { "aa", "Latn" }, { "ab", "Cyrl" }, { "af", "Latn" }, { "am", "Ethi" }, + { "ar", "Arab" }, { "as", "Beng" }, { "az", "Latn" }, { "be", "Cyrl" }, + { "bg", "Cyrl" }, { "bn", "Beng" }, { "bs", "Latn" }, { "ca", "Latn" }, + { "cs", "Latn" }, { "cy", "Latn" }, { "da", "Latn" }, { "de", "Latn" }, + { "el", "Grek" }, { "en", "Latn" }, { "es", "Latn" }, { "et", "Latn" }, + { "eu", "Latn" }, { "fa", "Arab" }, { "fi", "Latn" }, { "fr", "Latn" }, + { "ga", "Latn" }, { "gl", "Latn" }, { "gu", "Gujr" }, { "he", "Hebr" }, + { "hi", "Deva" }, { "hr", "Latn" }, { "hu", "Latn" }, { "hy", "Armn" }, + { "id", "Latn" }, { "is", "Latn" }, { "it", "Latn" }, { "ja", "Jpan" }, + { "ka", "Geor" }, { "kk", "Cyrl" }, { "km", "Khmr" }, { "kn", "Knda" }, + { "ko", "Kore" }, { "ky", "Cyrl" }, { "lo", "Laoo" }, { "lt", "Latn" }, + { "lv", "Latn" }, { "mk", "Cyrl" }, { "ml", "Mlym" }, { "mn", "Cyrl" }, + { "mr", "Deva" }, { "ms", "Latn" }, { "my", "Mymr" }, { "nb", "Latn" }, + { "ne", "Deva" }, { "nl", "Latn" }, { "nn", "Latn" }, { "no", "Latn" }, + { "or", "Orya" }, { "pa", "Guru" }, { "pl", "Latn" }, { "ps", "Arab" }, + { "pt", "Latn" }, { "ro", "Latn" }, { "ru", "Cyrl" }, { "si", "Sinh" }, + { "sk", "Latn" }, { "sl", "Latn" }, { "sq", "Latn" }, { "sr", "Cyrl" }, + { "sv", "Latn" }, { "sw", "Latn" }, { "ta", "Taml" }, { "te", "Telu" }, + { "tg", "Cyrl" }, { "th", "Thai" }, { "tk", "Latn" }, { "tr", "Latn" }, + { "uk", "Cyrl" }, { "und", "Latn" }, { "ur", "Arab" }, { "uz", "Latn" }, + { "vi", "Latn" }, { "zh", "Hans" }, + }; + /// /// T extension value aliases for deprecated values. /// From CLDR supplemental/alias.xml (tvalueAlias). @@ -607,6 +636,7 @@ private static bool ValidateTransformedExtension(string locale) var tlangHasScript = false; var tlangHasRegion = false; var currentTKeyHasValue = true; // Start true since we don't have a key yet + HashSet? tlangSeenVariants = null; while (index < parts.Length) { @@ -647,6 +677,11 @@ private static bool ValidateTransformedExtension(string locale) { // 4-char variant starting with digit (e.g., "1994") // Must come after script/region checks + tlangSeenVariants ??= new HashSet(StringComparer.OrdinalIgnoreCase); + if (!tlangSeenVariants.Add(part)) + { + return false; // Duplicate variant in tlang + } } else if (!tlangHasRegion && ((part.Length == 2 && IsAllLetters(part)) || (part.Length == 3 && IsAllDigits(part)))) { @@ -656,7 +691,11 @@ private static bool ValidateTransformedExtension(string locale) else if (IsValidVariant(part)) { // Variant subtag (5-8 alphanum or 4 starting with digit) - // OK + tlangSeenVariants ??= new HashSet(StringComparer.OrdinalIgnoreCase); + if (!tlangSeenVariants.Add(part)) + { + return false; // Duplicate variant in tlang + } } else { @@ -841,10 +880,33 @@ private static string CanonicalizeUnicodeLocaleId(string locale) } } - // 4. Apply region aliasing (LocaleData first, then fallback) + // 4. Apply region aliasing with script-aware replacement for multi-territory regions if (parsed.Region != null) { - if (LocaleData.RegionMappings.TryGetValue(parsed.Region, out var regionReplacement)) + // First try script-aware replacement (for deprecated regions with multiple replacements) + var script = parsed.Script; + if (script == null && parsed.Language != null) + { + LikelyScripts.TryGetValue(parsed.Language, out script); + } + + if (script != null) + { + var scriptRegionKey = script + "+" + parsed.Region; + if (LocaleData.ScriptRegionMappings.TryGetValue(scriptRegionKey, out var scriptRegionReplacement)) + { + parsed.Region = scriptRegionReplacement; + } + else if (LocaleData.RegionMappings.TryGetValue(parsed.Region, out var regionReplacement)) + { + parsed.Region = regionReplacement; + } + else if (RegionAliases.TryGetValue(parsed.Region, out regionReplacement)) + { + parsed.Region = regionReplacement; + } + } + else if (LocaleData.RegionMappings.TryGetValue(parsed.Region, out var regionReplacement)) { parsed.Region = regionReplacement; } @@ -857,11 +919,57 @@ private static string CanonicalizeUnicodeLocaleId(string locale) // 5. Apply variant aliasing from CLDR data if (parsed.Variants != null && parsed.Variants.Count > 0) { - for (var i = 0; i < parsed.Variants.Count; i++) + for (var i = parsed.Variants.Count - 1; i >= 0; i--) { if (LocaleData.VariantMappings.TryGetValue(parsed.Variants[i], out var variantMapping)) { - parsed.Variants[i] = variantMapping.Replacement; + if (string.Equals(variantMapping.Type, "language", StringComparison.Ordinal)) + { + // Variant maps to a language replacement - remove variant and update language + parsed.Language = variantMapping.Replacement; + parsed.Variants.RemoveAt(i); + } + else if (string.Equals(variantMapping.Type, "region", StringComparison.Ordinal)) + { + // Variant maps to a region - remove variant and set region + if (parsed.Region == null) + { + parsed.Region = variantMapping.Replacement; + } + parsed.Variants.RemoveAt(i); + } + else + { + // Type is "variant" - simple variant replacement + parsed.Variants[i] = variantMapping.Replacement; + + // Remove prefix variants if specified + if (variantMapping.Prefix != null) + { + for (var j = parsed.Variants.Count - 1; j >= 0; j--) + { + if (j != i && string.Equals(parsed.Variants[j], variantMapping.Prefix, StringComparison.OrdinalIgnoreCase)) + { + parsed.Variants.RemoveAt(j); + if (j < i) i--; + } + } + } + } + } + } + } + + // 5b. Apply language+variant mappings (e.g., "art+lojban" → "jbo") + if (parsed.Language != null && parsed.Variants != null && parsed.Variants.Count > 0) + { + for (var i = parsed.Variants.Count - 1; i >= 0; i--) + { + var key = parsed.Language + "+" + parsed.Variants[i].ToLowerInvariant(); + if (LocaleData.LanguageVariantMappings.TryGetValue(key, out var newLanguage)) + { + parsed.Language = newLanguage; + parsed.Variants.RemoveAt(i); } } } diff --git a/Jint/Native/Intl/JsPluralRules.cs b/Jint/Native/Intl/JsPluralRules.cs index 8f81b45f93..438007a52a 100644 --- a/Jint/Native/Intl/JsPluralRules.cs +++ b/Jint/Native/Intl/JsPluralRules.cs @@ -151,13 +151,19 @@ private string SelectCardinal(double n) // French - "one" for 0 and 1, "many" for multiples of 1 million with no fraction "fr" => SelectFrenchCardinal(i, v), - // Portuguese - "one" for 0 and 1 - "pt" => (i == 0 || i == 1) ? "one" : "other", + // Portuguese, Persian - "one" for 0 and 1 + "pt" or "fa" => (i == 0 || i == 1) ? "one" : "other", // Spanish, Italian - "one" for 1 "es" or "it" => (i == 1 && v == 0) ? "one" : "other", + // Manx (gv) + "gv" => SelectManxCardinal(i, v), + + // Slovenian (sl) + "sl" => SelectSlovenianCardinal(i, v), + // Russian, Ukrainian, Polish - complex rules "ru" or "uk" => SelectSlavicCardinal(i, v), @@ -286,6 +292,71 @@ private static string SelectPolishCardinal(long i, int v) return "other"; } + /// + /// Manx (gv) cardinal plural rules from CLDR. + /// one: v = 0 and i % 10 = 1 + /// two: v = 0 and i % 10 = 2 + /// few: v = 0 and i % 20 = 0 + /// many: v != 0 + /// other: everything else + /// + private static string SelectManxCardinal(long i, int v) + { + if (v != 0) + { + return "many"; + } + + var mod10 = i % 10; + var mod20 = i % 20; + + if (mod10 == 1) + { + return "one"; + } + + if (mod10 == 2) + { + return "two"; + } + + if (mod20 == 0) + { + return "few"; + } + + return "other"; + } + + /// + /// Slovenian (sl) cardinal plural rules from CLDR. + /// one: v = 0 and i % 100 = 1 + /// two: v = 0 and i % 100 = 2 + /// few: v = 0 and i % 100 = 3..4 OR v != 0 + /// other: everything else + /// + private static string SelectSlovenianCardinal(long i, int v) + { + var mod100 = i % 100; + + if (v == 0 && mod100 == 1) + { + return "one"; + } + + if (v == 0 && mod100 == 2) + { + return "two"; + } + + if ((v == 0 && mod100 >= 3 && mod100 <= 4) || v != 0) + { + return "few"; + } + + return "other"; + } + private static string SelectArabicCardinal(long i) { if (i == 0) @@ -375,7 +446,9 @@ internal string[] GetPluralCategories() return lang switch { "ar" => new[] { "zero", "one", "two", "few", "many", "other" }, + "gv" => new[] { "one", "two", "few", "many", "other" }, "ru" or "uk" or "pl" => new[] { "one", "few", "many", "other" }, + "sl" => new[] { "one", "two", "few", "other" }, // French and Portuguese have "many" for compact notation (large numbers) "fr" or "pt" => new[] { "one", "many", "other" }, "zh" or "ja" or "ko" or "vi" => new[] { "other" }, diff --git a/Jint/Native/Intl/LocaleConstructor.cs b/Jint/Native/Intl/LocaleConstructor.cs index 310caa464c..e42226283d 100644 --- a/Jint/Native/Intl/LocaleConstructor.cs +++ b/Jint/Native/Intl/LocaleConstructor.cs @@ -611,22 +611,24 @@ private static ParsedLocale ParseLanguageTag(string tag) { var region = parts[index].ToUpperInvariant(); - // Apply region aliasing (e.g., numeric codes like 554 → NZ) - if (Data.LocaleData.RegionMappings.TryGetValue(region, out var regionReplacement)) - { - region = regionReplacement; - } - - // Apply script-sensitive region aliasing (e.g., Armn + SU → AM) + // Apply script-sensitive region aliasing first (e.g., Armn + SU → AM) + var scriptRegionResolved = false; if (result.Script != null) { var scriptRegionKey = result.Script + "+" + region; if (Data.LocaleData.ScriptRegionMappings.TryGetValue(scriptRegionKey, out var scriptRegionReplacement)) { region = scriptRegionReplacement; + scriptRegionResolved = true; } } + // Fall back to simple region aliasing (e.g., numeric codes like 554 → NZ) + if (!scriptRegionResolved && Data.LocaleData.RegionMappings.TryGetValue(region, out var regionReplacement)) + { + region = regionReplacement; + } + result.Region = region; index++; } diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index 33f327f81c..5a526f7edb 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -236,19 +236,16 @@ private JsValue ToLocaleUpperCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); + + // https://tc39.es/ecma402/#sup-string.prototype.tolocaleuppercase + // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). + var requestedLocales = IntlUtilities.CanonicalizeLocaleList(_engine, arguments.At(0)); var culture = CultureInfo.InvariantCulture; - if (arguments.Length > 0 && arguments[0].IsString()) + if (requestedLocales.Count > 0) { - try - { - var cultureArgument = arguments[0].ToString(); - culture = CultureInfo.GetCultureInfo(cultureArgument); - } - catch (CultureNotFoundException) - { - Throw.RangeError(_realm, "Incorrect culture information provided"); - } + culture = IntlUtilities.GetCultureInfo(requestedLocales[0]) ?? CultureInfo.InvariantCulture; } + if (string.Equals("lt", culture.Name, StringComparison.OrdinalIgnoreCase)) { s = StringInlHelper.LithuanianStringProcessor(s); @@ -275,7 +272,17 @@ private JsValue ToLocaleLowerCase(JsValue thisObject, JsCallArguments arguments) { TypeConverter.RequireObjectCoercible(_engine, thisObject); var s = TypeConverter.ToString(thisObject); - return ToLowerCaseWithSpecialCasing(s, CultureInfo.InvariantCulture); + + // https://tc39.es/ecma402/#sup-string.prototype.tolocalelowercase + // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). + var requestedLocales = IntlUtilities.CanonicalizeLocaleList(_engine, arguments.At(0)); + var culture = CultureInfo.InvariantCulture; + if (requestedLocales.Count > 0) + { + culture = IntlUtilities.GetCultureInfo(requestedLocales[0]) ?? CultureInfo.InvariantCulture; + } + + return ToLowerCaseWithSpecialCasing(s, culture); } private JsValue ToLowerCase(JsValue thisObject, JsCallArguments arguments) @@ -287,37 +294,299 @@ private JsValue ToLowerCase(JsValue thisObject, JsCallArguments arguments) /// /// Converts string to lowercase with Unicode special casing rules. - /// Handles Final_Sigma context for Greek capital sigma (U+03A3). + /// Handles Final_Sigma, Turkish/Azeri I-dot, Lithuanian soft-dotted, and İ decomposition. /// https://unicode.org/reports/tr21/tr21-5.html#SpecialCasing /// private static string ToLowerCaseWithSpecialCasing(string s, CultureInfo culture) { - const char GreekCapitalSigma = '\u03A3'; + var langName = culture.TwoLetterISOLanguageName; + var isTurkishOrAzeri = string.Equals(langName, "tr", StringComparison.Ordinal) || + string.Equals(langName, "az", StringComparison.Ordinal); + var isLithuanian = string.Equals(langName, "lt", StringComparison.Ordinal); - // Fast path: if no Greek capital sigma, use standard lowercase - if (s.IndexOf(GreekCapitalSigma) < 0) + // Fast path: if no special characters, use standard lowercase (but not for Turkish/Azeri/Lithuanian) + if (!isTurkishOrAzeri && !isLithuanian && !NeedsSpecialCasing(s)) { return s.ToLower(culture); } - // Need to handle Final_Sigma context - var result = new char[s.Length]; + var sb = new System.Text.StringBuilder(s.Length + 4); + for (var i = 0; i < s.Length; i++) { var c = s[i]; - if (c == GreekCapitalSigma) + + // Greek Final_Sigma (all locales) + if (c == '\u03A3') + { + sb.Append(IsFinalSigmaContext(s, i) ? '\u03C2' : '\u03C3'); + continue; + } + + if (isTurkishOrAzeri) + { + // Turkish/Azeri: İ (U+0130) → i + if (c == '\u0130') + { + sb.Append('i'); + continue; + } + + // Turkish/Azeri: I + combining dot above (with only cc<230 in between) → i (remove dot above) + if (c == 'I') + { + if (FollowedByDotAbove(s, i)) + { + sb.Append('i'); + // Skip intervening cc<230 chars and the dot above + i++; + while (i < s.Length && s[i] != '\u0307') + { + sb.Append(char.ToLower(s[i], culture)); + i++; + } + // i now points at U+0307, skip it + continue; + } + + // Turkish/Azeri: I (not followed by dot above) → ı (dotless i) + sb.Append('\u0131'); + continue; + } + } + else if (isLithuanian) + { + // Lithuanian: Ì (U+00CC) → i + ̇ + ̀ + if (c == '\u00CC') + { + sb.Append('i'); + sb.Append('\u0307'); + sb.Append('\u0300'); + continue; + } + + // Lithuanian: Í (U+00CD) → i + ̇ + ́ + if (c == '\u00CD') + { + sb.Append('i'); + sb.Append('\u0307'); + sb.Append('\u0301'); + continue; + } + + // Lithuanian: Ĩ (U+0128) → i + ̇ + ̃ + if (c == '\u0128') + { + sb.Append('i'); + sb.Append('\u0307'); + sb.Append('\u0303'); + continue; + } + + // Lithuanian: I, J, Į followed by combining class 230 mark → add U+0307 after lowercase + if (c == 'I' || c == 'J' || c == '\u012E') + { + if (FollowedByCombiningClass230(s, i)) + { + sb.Append(char.ToLower(c, culture)); + sb.Append('\u0307'); + continue; + } + } + } + else + { + // Default locale: İ (U+0130) → i + ̇ (U+0069 + U+0307) + if (c == '\u0130') + { + sb.Append('i'); + sb.Append('\u0307'); + continue; + } + } + + // Handle surrogate pairs + if (char.IsHighSurrogate(c) && i + 1 < s.Length && char.IsLowSurrogate(s[i + 1])) + { + sb.Append(c); + sb.Append(s[i + 1]); + i++; + continue; + } + + sb.Append(char.ToLower(c, culture)); + } + + return sb.ToString(); + } + + /// + /// Checks if the string needs special casing beyond standard ToLower. + /// + private static bool NeedsSpecialCasing(string s) + { + foreach (var c in s) + { + if (c == '\u03A3' || c == '\u0130') + { + return true; + } + } + return false; + } + + /// + /// Checks if character at index i is followed by U+0307 (COMBINING DOT ABOVE) + /// with only characters of combining class less than 230 in between. + /// Used for Turkish/Azeri I + dot above handling. + /// + private static bool FollowedByDotAbove(string s, int i) + { + for (var j = i + 1; j < s.Length; j++) + { + // Handle surrogate pairs + if (char.IsHighSurrogate(s[j]) && j + 1 < s.Length && char.IsLowSurrogate(s[j + 1])) + { + var cp = char.ConvertToUtf32(s[j], s[j + 1]); + var cc = GetCombiningClass(cp); + if (cc == 0 || cc >= 230) + { + return false; + } + j++; + continue; + } + + var ch = s[j]; + if (ch == '\u0307') + { + return true; + } + + var charCc = GetCombiningClass(ch); + if (charCc == 0 || charCc >= 230) { - // 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'; + return false; + } + } + return false; + } + + /// + /// Checks if character at index i is followed by a combining mark with combining class 230. + /// Used for Lithuanian soft-dotted character handling. + /// + private static bool FollowedByCombiningClass230(string s, int i) + { + for (var j = i + 1; j < s.Length; j++) + { + int cp; + if (char.IsHighSurrogate(s[j]) && j + 1 < s.Length && char.IsLowSurrogate(s[j + 1])) + { + cp = char.ConvertToUtf32(s[j], s[j + 1]); + j++; } else { - result[i] = char.ToLower(c, culture); + cp = s[j]; + } + + var cc = GetCombiningClass(cp); + if (cc == 0) + { + return false; + } + + if (cc == 230) + { + return true; } } + return false; + } + + /// + /// Returns the Unicode combining class for a code point. + /// Covers the Combining Diacritical Marks block and common supplementary combining marks. + /// + private static int GetCombiningClass(int codePoint) + { + // Most characters have combining class 0 + if (codePoint < 0x0300) + { + return 0; + } + + // Combining Diacritical Marks (U+0300-U+036F) + return codePoint switch + { + // Class 230 (Above) + >= 0x0300 and <= 0x0314 => 230, + 0x033D or 0x033E or 0x033F => 230, + 0x0340 or 0x0341 or 0x0342 or 0x0343 or 0x0344 or 0x0346 => 230, + 0x034A or 0x034B or 0x034C => 230, + 0x0350 or 0x0351 or 0x0352 => 230, + 0x0357 => 230, + 0x035B => 230, + >= 0x0363 and <= 0x036F => 230, + + // Class 232 (Double Below) + 0x035C or 0x035F => 233, + + // Class 1 (Overlay) + 0x0334 or 0x0335 or 0x0336 or 0x0337 or 0x0338 => 1, + + // Class 220 (Below) + >= 0x0316 and <= 0x0319 => 220, + >= 0x031C and <= 0x0320 => 220, + >= 0x0323 and <= 0x0326 => 220, + >= 0x0329 and <= 0x0333 => 220, + 0x0339 or 0x033A or 0x033B or 0x033C => 220, + 0x0345 => 240, // Iota subscript + 0x0347 or 0x0348 or 0x0349 => 220, + 0x034D or 0x034E => 220, + 0x0353 or 0x0354 or 0x0355 or 0x0356 => 220, + 0x0359 or 0x035A => 220, + + // Class 202 (Attached Below Right) + 0x031A => 232, + + // Class 216 (Attached Above Right) + 0x0315 => 232, + + // Class 226 (Above Right) + 0x0358 => 232, + + // Combining marks outside the main block + >= 0x0590 and <= 0x05CF => GetHebrewCombiningClass(codePoint), + + // Common supplementary combining marks + 0x101FD => 220, // PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE + >= 0x1D165 and <= 0x1D169 => 216, // MUSICAL SYMBOL COMBINING STEM etc. (attached) + >= 0x1D16D and <= 0x1D172 => 216, // MUSICAL SYMBOL COMBINING AUGMENTATION DOT etc. + >= 0x1D17B and <= 0x1D182 => 220, // MUSICAL SYMBOL COMBINING ACCENT etc. + >= 0x1D185 and <= 0x1D189 => 230, // MUSICAL SYMBOL COMBINING DOIT etc. + >= 0x1D18A and <= 0x1D18B => 220, // MUSICAL SYMBOL COMBINING DOWN BOW etc. + + _ => codePoint <= 0xFFFF && CharUnicodeInfo.GetUnicodeCategory((char) codePoint) == UnicodeCategory.NonSpacingMark ? 230 : 0 + }; + } - return new string(result); + private static int GetHebrewCombiningClass(int cp) + { + // Simplified: most Hebrew combining marks are below (220) or above (230) + return cp switch + { + >= 0x0591 and <= 0x05AF => 220, // Hebrew accents (various, simplified) + >= 0x05B0 and <= 0x05BD => 220, // Hebrew points + 0x05BF => 230, + 0x05C1 => 230, + 0x05C2 => 220, + 0x05C4 => 230, + 0x05C5 => 220, + 0x05C7 => 220, + _ => 0 + }; } /// diff --git a/Jint/Native/Symbol/SymbolPrototype.cs b/Jint/Native/Symbol/SymbolPrototype.cs index bd3eafbf3c..e715708a28 100644 --- a/Jint/Native/Symbol/SymbolPrototype.cs +++ b/Jint/Native/Symbol/SymbolPrototype.cs @@ -29,9 +29,8 @@ protected override void Initialize() { const PropertyFlag lengthFlags = PropertyFlag.Configurable; const PropertyFlag propertyFlags = PropertyFlag.Configurable; - SetProperties(new PropertyDictionary(5, checkExistingKeys: false) + SetProperties(new PropertyDictionary(4, checkExistingKeys: false) { - ["length"] = new PropertyDescriptor(JsNumber.PositiveZero, propertyFlags), ["constructor"] = new PropertyDescriptor(_constructor, PropertyFlag.Configurable | PropertyFlag.Writable), ["description"] = new GetSetPropertyDescriptor(new ClrFunction(Engine, "description", Description, 0, lengthFlags), Undefined, propertyFlags), ["toString"] = new PropertyDescriptor(new ClrFunction(Engine, "toString", ToSymbolString, 0, lengthFlags), PropertyFlag.Configurable | PropertyFlag.Writable),