number-flow-react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/lib/module/core/constants.js +21 -0
- package/lib/module/core/constants.js.map +1 -0
- package/lib/module/core/intlHelpers.js +310 -0
- package/lib/module/core/intlHelpers.js.map +1 -0
- package/lib/module/core/layout.js +71 -0
- package/lib/module/core/layout.js.map +1 -0
- package/lib/module/core/mask.js +50 -0
- package/lib/module/core/mask.js.map +1 -0
- package/lib/module/core/numerals/detection.js +105 -0
- package/lib/module/core/numerals/detection.js.map +1 -0
- package/lib/module/core/numerals/digits.js +128 -0
- package/lib/module/core/numerals/digits.js.map +1 -0
- package/lib/module/core/numerals/index.js +5 -0
- package/lib/module/core/numerals/index.js.map +1 -0
- package/lib/module/core/numerals/tables.js +114 -0
- package/lib/module/core/numerals/tables.js.map +1 -0
- package/lib/module/core/superscript.js +31 -0
- package/lib/module/core/superscript.js.map +1 -0
- package/lib/module/core/timeLayout.js +98 -0
- package/lib/module/core/timeLayout.js.map +1 -0
- package/lib/module/core/timeTypes.js +4 -0
- package/lib/module/core/timeTypes.js.map +1 -0
- package/lib/module/core/timing.js +45 -0
- package/lib/module/core/timing.js.map +1 -0
- package/lib/module/core/types.js +58 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/core/useAccessibilityAnnouncement.js +27 -0
- package/lib/module/core/useAccessibilityAnnouncement.js.map +1 -0
- package/lib/module/core/useAnimatedX.js +25 -0
- package/lib/module/core/useAnimatedX.js.map +1 -0
- package/lib/module/core/useAnimationLifecycle.js +37 -0
- package/lib/module/core/useAnimationLifecycle.js.map +1 -0
- package/lib/module/core/useCanAnimate.js +22 -0
- package/lib/module/core/useCanAnimate.js.map +1 -0
- package/lib/module/core/useContinuousSpin.js +89 -0
- package/lib/module/core/useContinuousSpin.js.map +1 -0
- package/lib/module/core/useDebouncedWidths.js +74 -0
- package/lib/module/core/useDebouncedWidths.js.map +1 -0
- package/lib/module/core/useDigitAnimation.js +138 -0
- package/lib/module/core/useDigitAnimation.js.map +1 -0
- package/lib/module/core/useFlowPipeline.js +85 -0
- package/lib/module/core/useFlowPipeline.js.map +1 -0
- package/lib/module/core/useFormattedValue.js +28 -0
- package/lib/module/core/useFormattedValue.js.map +1 -0
- package/lib/module/core/useLayoutDiff.js +59 -0
- package/lib/module/core/useLayoutDiff.js.map +1 -0
- package/lib/module/core/useNumberFormatting.js +158 -0
- package/lib/module/core/useNumberFormatting.js.map +1 -0
- package/lib/module/core/useSlotOpacity.js +53 -0
- package/lib/module/core/useSlotOpacity.js.map +1 -0
- package/lib/module/core/useTimeFormatting.js +74 -0
- package/lib/module/core/useTimeFormatting.js.map +1 -0
- package/lib/module/core/useTimingResolution.js +21 -0
- package/lib/module/core/useTimingResolution.js.map +1 -0
- package/lib/module/core/useWorkletFormatting.js +49 -0
- package/lib/module/core/useWorkletFormatting.js.map +1 -0
- package/lib/module/core/utils.js +132 -0
- package/lib/module/core/utils.js.map +1 -0
- package/lib/module/core/warnings.js +10 -0
- package/lib/module/core/warnings.js.map +1 -0
- package/lib/module/index.js +7 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/DigitSlot.js +163 -0
- package/lib/module/native/DigitSlot.js.map +1 -0
- package/lib/module/native/NumberFlow.js +244 -0
- package/lib/module/native/NumberFlow.js.map +1 -0
- package/lib/module/native/SymbolSlot.js +52 -0
- package/lib/module/native/SymbolSlot.js.map +1 -0
- package/lib/module/native/TimeFlow.js +270 -0
- package/lib/module/native/TimeFlow.js.map +1 -0
- package/lib/module/native/index.js +5 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/native/renderSlots.js +108 -0
- package/lib/module/native/renderSlots.js.map +1 -0
- package/lib/module/native/types.js +4 -0
- package/lib/module/native/types.js.map +1 -0
- package/lib/module/native/useMeasuredGlyphMetrics.js +156 -0
- package/lib/module/native/useMeasuredGlyphMetrics.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/skia/DigitSlot.js +171 -0
- package/lib/module/skia/DigitSlot.js.map +1 -0
- package/lib/module/skia/SkiaNumberFlow.js +430 -0
- package/lib/module/skia/SkiaNumberFlow.js.map +1 -0
- package/lib/module/skia/SkiaTimeFlow.js +226 -0
- package/lib/module/skia/SkiaTimeFlow.js.map +1 -0
- package/lib/module/skia/SymbolSlot.js +92 -0
- package/lib/module/skia/SymbolSlot.js.map +1 -0
- package/lib/module/skia/index.js +6 -0
- package/lib/module/skia/index.js.map +1 -0
- package/lib/module/skia/renderSlots.js +131 -0
- package/lib/module/skia/renderSlots.js.map +1 -0
- package/lib/module/skia/useGlyphMetrics.js +72 -0
- package/lib/module/skia/useGlyphMetrics.js.map +1 -0
- package/lib/module/skia/useScrubbing.js +165 -0
- package/lib/module/skia/useScrubbing.js.map +1 -0
- package/lib/module/skia/useSkiaFont.js +23 -0
- package/lib/module/skia/useSkiaFont.js.map +1 -0
- package/lib/typescript/core/constants.d.ts +15 -0
- package/lib/typescript/core/constants.d.ts.map +1 -0
- package/lib/typescript/core/intlHelpers.d.ts +10 -0
- package/lib/typescript/core/intlHelpers.d.ts.map +1 -0
- package/lib/typescript/core/layout.d.ts +22 -0
- package/lib/typescript/core/layout.d.ts.map +1 -0
- package/lib/typescript/core/mask.d.ts +18 -0
- package/lib/typescript/core/mask.d.ts.map +1 -0
- package/lib/typescript/core/numerals/detection.d.ts +17 -0
- package/lib/typescript/core/numerals/detection.d.ts.map +1 -0
- package/lib/typescript/core/numerals/digits.d.ts +43 -0
- package/lib/typescript/core/numerals/digits.d.ts.map +1 -0
- package/lib/typescript/core/numerals/index.d.ts +3 -0
- package/lib/typescript/core/numerals/index.d.ts.map +1 -0
- package/lib/typescript/core/numerals/tables.d.ts +32 -0
- package/lib/typescript/core/numerals/tables.d.ts.map +1 -0
- package/lib/typescript/core/superscript.d.ts +16 -0
- package/lib/typescript/core/superscript.d.ts.map +1 -0
- package/lib/typescript/core/timeLayout.d.ts +19 -0
- package/lib/typescript/core/timeLayout.d.ts.map +1 -0
- package/lib/typescript/core/timeTypes.d.ts +80 -0
- package/lib/typescript/core/timeTypes.d.ts.map +1 -0
- package/lib/typescript/core/timing.d.ts +6 -0
- package/lib/typescript/core/timing.d.ts.map +1 -0
- package/lib/typescript/core/types.d.ts +165 -0
- package/lib/typescript/core/types.d.ts.map +1 -0
- package/lib/typescript/core/useAccessibilityAnnouncement.d.ts +10 -0
- package/lib/typescript/core/useAccessibilityAnnouncement.d.ts.map +1 -0
- package/lib/typescript/core/useAnimatedX.d.ts +9 -0
- package/lib/typescript/core/useAnimatedX.d.ts.map +1 -0
- package/lib/typescript/core/useAnimationLifecycle.d.ts +14 -0
- package/lib/typescript/core/useAnimationLifecycle.d.ts.map +1 -0
- package/lib/typescript/core/useCanAnimate.d.ts +14 -0
- package/lib/typescript/core/useCanAnimate.d.ts.map +1 -0
- package/lib/typescript/core/useContinuousSpin.d.ts +23 -0
- package/lib/typescript/core/useContinuousSpin.d.ts.map +1 -0
- package/lib/typescript/core/useDebouncedWidths.d.ts +17 -0
- package/lib/typescript/core/useDebouncedWidths.d.ts.map +1 -0
- package/lib/typescript/core/useDigitAnimation.d.ts +38 -0
- package/lib/typescript/core/useDigitAnimation.d.ts.map +1 -0
- package/lib/typescript/core/useFlowPipeline.d.ts +46 -0
- package/lib/typescript/core/useFlowPipeline.d.ts.map +1 -0
- package/lib/typescript/core/useFormattedValue.d.ts +14 -0
- package/lib/typescript/core/useFormattedValue.d.ts.map +1 -0
- package/lib/typescript/core/useLayoutDiff.d.ts +18 -0
- package/lib/typescript/core/useLayoutDiff.d.ts.map +1 -0
- package/lib/typescript/core/useNumberFormatting.d.ts +18 -0
- package/lib/typescript/core/useNumberFormatting.d.ts.map +1 -0
- package/lib/typescript/core/useSlotOpacity.d.ts +18 -0
- package/lib/typescript/core/useSlotOpacity.d.ts.map +1 -0
- package/lib/typescript/core/useTimeFormatting.d.ts +22 -0
- package/lib/typescript/core/useTimeFormatting.d.ts.map +1 -0
- package/lib/typescript/core/useTimingResolution.d.ts +13 -0
- package/lib/typescript/core/useTimingResolution.d.ts.map +1 -0
- package/lib/typescript/core/useWorkletFormatting.d.ts +14 -0
- package/lib/typescript/core/useWorkletFormatting.d.ts.map +1 -0
- package/lib/typescript/core/utils.d.ts +44 -0
- package/lib/typescript/core/utils.d.ts.map +1 -0
- package/lib/typescript/core/warnings.d.ts +2 -0
- package/lib/typescript/core/warnings.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +8 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/native/DigitSlot.d.ts +27 -0
- package/lib/typescript/native/DigitSlot.d.ts.map +1 -0
- package/lib/typescript/native/NumberFlow.d.ts +3 -0
- package/lib/typescript/native/NumberFlow.d.ts.map +1 -0
- package/lib/typescript/native/SymbolSlot.d.ts +19 -0
- package/lib/typescript/native/SymbolSlot.d.ts.map +1 -0
- package/lib/typescript/native/TimeFlow.d.ts +3 -0
- package/lib/typescript/native/TimeFlow.d.ts.map +1 -0
- package/lib/typescript/native/index.d.ts +3 -0
- package/lib/typescript/native/index.d.ts.map +1 -0
- package/lib/typescript/native/renderSlots.d.ts +31 -0
- package/lib/typescript/native/renderSlots.d.ts.map +1 -0
- package/lib/typescript/native/types.d.ts +36 -0
- package/lib/typescript/native/types.d.ts.map +1 -0
- package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts +8 -0
- package/lib/typescript/native/useMeasuredGlyphMetrics.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/skia/DigitSlot.d.ts +35 -0
- package/lib/typescript/skia/DigitSlot.d.ts.map +1 -0
- package/lib/typescript/skia/SkiaNumberFlow.d.ts +3 -0
- package/lib/typescript/skia/SkiaNumberFlow.d.ts.map +1 -0
- package/lib/typescript/skia/SkiaTimeFlow.d.ts +3 -0
- package/lib/typescript/skia/SkiaTimeFlow.d.ts.map +1 -0
- package/lib/typescript/skia/SymbolSlot.d.ts +26 -0
- package/lib/typescript/skia/SymbolSlot.d.ts.map +1 -0
- package/lib/typescript/skia/index.d.ts +6 -0
- package/lib/typescript/skia/index.d.ts.map +1 -0
- package/lib/typescript/skia/renderSlots.d.ts +40 -0
- package/lib/typescript/skia/renderSlots.d.ts.map +1 -0
- package/lib/typescript/skia/useGlyphMetrics.d.ts +16 -0
- package/lib/typescript/skia/useGlyphMetrics.d.ts.map +1 -0
- package/lib/typescript/skia/useScrubbing.d.ts +59 -0
- package/lib/typescript/skia/useScrubbing.d.ts.map +1 -0
- package/lib/typescript/skia/useSkiaFont.d.ts +13 -0
- package/lib/typescript/skia/useSkiaFont.d.ts.map +1 -0
- package/package.json +104 -0
- package/src/core/constants.ts +20 -0
- package/src/core/intlHelpers.ts +351 -0
- package/src/core/layout.ts +108 -0
- package/src/core/mask.ts +72 -0
- package/src/core/numerals/detection.ts +112 -0
- package/src/core/numerals/digits.ts +102 -0
- package/src/core/numerals/index.ts +9 -0
- package/src/core/numerals/tables.ts +112 -0
- package/src/core/superscript.ts +27 -0
- package/src/core/timeLayout.ts +119 -0
- package/src/core/timeTypes.ts +88 -0
- package/src/core/timing.ts +60 -0
- package/src/core/types.ts +189 -0
- package/src/core/useAccessibilityAnnouncement.ts +27 -0
- package/src/core/useAnimatedX.ts +30 -0
- package/src/core/useAnimationLifecycle.ts +54 -0
- package/src/core/useCanAnimate.ts +21 -0
- package/src/core/useContinuousSpin.ts +112 -0
- package/src/core/useDebouncedWidths.ts +93 -0
- package/src/core/useDigitAnimation.ts +192 -0
- package/src/core/useFlowPipeline.ts +126 -0
- package/src/core/useFormattedValue.ts +32 -0
- package/src/core/useLayoutDiff.ts +71 -0
- package/src/core/useNumberFormatting.ts +164 -0
- package/src/core/useSlotOpacity.ts +66 -0
- package/src/core/useTimeFormatting.ts +95 -0
- package/src/core/useTimingResolution.ts +47 -0
- package/src/core/useWorkletFormatting.ts +59 -0
- package/src/core/utils.ts +149 -0
- package/src/core/warnings.ts +8 -0
- package/src/index.ts +15 -0
- package/src/native/DigitSlot.tsx +203 -0
- package/src/native/NumberFlow.tsx +287 -0
- package/src/native/SymbolSlot.tsx +68 -0
- package/src/native/TimeFlow.tsx +287 -0
- package/src/native/index.ts +2 -0
- package/src/native/renderSlots.tsx +150 -0
- package/src/native/types.ts +40 -0
- package/src/native/useMeasuredGlyphMetrics.tsx +205 -0
- package/src/skia/DigitSlot.tsx +221 -0
- package/src/skia/SkiaNumberFlow.tsx +506 -0
- package/src/skia/SkiaTimeFlow.tsx +257 -0
- package/src/skia/SymbolSlot.tsx +120 -0
- package/src/skia/index.ts +5 -0
- package/src/skia/renderSlots.tsx +180 -0
- package/src/skia/useGlyphMetrics.ts +79 -0
- package/src/skia/useScrubbing.ts +223 -0
- package/src/skia/useSkiaFont.ts +25 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { MEASURABLE_CHARS } from "./constants";
|
|
2
|
+
import {
|
|
3
|
+
detectNumberingSystem,
|
|
4
|
+
detectOutputZeroCodePoint,
|
|
5
|
+
getDigitStrings,
|
|
6
|
+
getZeroCodePoint,
|
|
7
|
+
isLocaleDigit,
|
|
8
|
+
} from "./numerals";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Avoids expensive new Intl.NumberFormat() on every render when callers pass
|
|
12
|
+
* inline format objects (which create new references each render).
|
|
13
|
+
*/
|
|
14
|
+
const formatterCache = new Map<string, Intl.NumberFormat>();
|
|
15
|
+
|
|
16
|
+
export function getOrCreateFormatter(
|
|
17
|
+
locales?: Intl.LocalesArgument,
|
|
18
|
+
format?: Intl.NumberFormatOptions,
|
|
19
|
+
): Intl.NumberFormat {
|
|
20
|
+
const key = JSON.stringify([locales, format]);
|
|
21
|
+
let cached = formatterCache.get(key);
|
|
22
|
+
if (!cached) {
|
|
23
|
+
cached = new Intl.NumberFormat(locales, format);
|
|
24
|
+
formatterCache.set(key, cached);
|
|
25
|
+
}
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Probes the formatter with a sample number that exercises group separators,
|
|
31
|
+
* decimal separator, and currency/percent symbols. Returns only characters NOT
|
|
32
|
+
* already in MEASURABLE_CHARS — this keeps the result stable across different
|
|
33
|
+
* prefix/suffix combinations and prevents native measurement cache invalidation.
|
|
34
|
+
*/
|
|
35
|
+
const formatCharsCache = new Map<string, string>();
|
|
36
|
+
|
|
37
|
+
export function getFormatCharacters(
|
|
38
|
+
locales?: Intl.LocalesArgument,
|
|
39
|
+
format?: Intl.NumberFormatOptions,
|
|
40
|
+
prefix = "",
|
|
41
|
+
suffix = "",
|
|
42
|
+
): string {
|
|
43
|
+
const key = JSON.stringify([locales, format, prefix, suffix]);
|
|
44
|
+
const cached = formatCharsCache.get(key);
|
|
45
|
+
if (cached !== undefined) return cached;
|
|
46
|
+
|
|
47
|
+
const formatter = getOrCreateFormatter(locales, format);
|
|
48
|
+
const numberingSystem = detectNumberingSystem(locales, format);
|
|
49
|
+
const zeroCP = getZeroCodePoint(numberingSystem);
|
|
50
|
+
|
|
51
|
+
// Sample that exercises: group separators, decimal, sign, large integers
|
|
52
|
+
const probes = [1234567.89, -1234567.89];
|
|
53
|
+
const chars = new Set<string>();
|
|
54
|
+
|
|
55
|
+
for (const probe of probes) {
|
|
56
|
+
for (const ch of formatter.format(probe)) {
|
|
57
|
+
const code = ch.charCodeAt(0);
|
|
58
|
+
// Skip digits in both Latin and the locale's numbering system
|
|
59
|
+
const isLatinDigit = code >= 48 && code <= 57;
|
|
60
|
+
const isLocale = isLocaleDigit(code, zeroCP);
|
|
61
|
+
if (!isLatinDigit && !isLocale) chars.add(ch);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Scientific/engineering notation replaces E with ×10 in our display
|
|
65
|
+
if (format?.notation === "scientific" || format?.notation === "engineering") {
|
|
66
|
+
chars.add("\u00D7"); // × (multiplication sign)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add locale digit strings so they get measured
|
|
70
|
+
const digitStrings = getDigitStrings(numberingSystem);
|
|
71
|
+
for (const ds of digitStrings) chars.add(ds);
|
|
72
|
+
|
|
73
|
+
// Also include prefix/suffix chars.
|
|
74
|
+
for (const ch of prefix) chars.add(ch);
|
|
75
|
+
for (const ch of suffix) chars.add(ch);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Only return chars NOT already in MEASURABLE_CHARS — this keeps the result
|
|
79
|
+
* stable across prefix/suffix changes and avoids native measurement cache misses.
|
|
80
|
+
*/
|
|
81
|
+
const result = Array.from(chars)
|
|
82
|
+
.filter((c) => !MEASURABLE_CHARS.includes(c))
|
|
83
|
+
.join("");
|
|
84
|
+
formatCharsCache.set(key, result);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const decimalSepCache = new Map<string, string>();
|
|
89
|
+
|
|
90
|
+
function detectDecimalSeparator(locales?: Intl.LocalesArgument): string {
|
|
91
|
+
const key = JSON.stringify(locales);
|
|
92
|
+
const cached = decimalSepCache.get(key);
|
|
93
|
+
if (cached) return cached;
|
|
94
|
+
|
|
95
|
+
let sep = ".";
|
|
96
|
+
try {
|
|
97
|
+
const fmt = new Intl.NumberFormat(locales, {
|
|
98
|
+
minimumFractionDigits: 1,
|
|
99
|
+
maximumFractionDigits: 1,
|
|
100
|
+
useGrouping: false,
|
|
101
|
+
});
|
|
102
|
+
const str = fmt.format(1.5);
|
|
103
|
+
const zeroCP = detectOutputZeroCodePoint(str);
|
|
104
|
+
for (const ch of str) {
|
|
105
|
+
const code = ch.charCodeAt(0);
|
|
106
|
+
if (!isLocaleDigit(code, zeroCP) && !(code >= 48 && code <= 57)) {
|
|
107
|
+
sep = ch;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
113
|
+
decimalSepCache.set(key, sep);
|
|
114
|
+
return sep;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parses the mantissa portion of a formatted number string into typed parts.
|
|
119
|
+
* Handles integer digits, fraction digits, decimal separators, signs, and group separators.
|
|
120
|
+
*/
|
|
121
|
+
function parseMantissa(
|
|
122
|
+
mantissa: string,
|
|
123
|
+
decimalSep: string,
|
|
124
|
+
parts: Intl.NumberFormatPart[],
|
|
125
|
+
zeroCodePoint = 48,
|
|
126
|
+
): void {
|
|
127
|
+
/**
|
|
128
|
+
* Search from right — in some locales the decimal sep char is also
|
|
129
|
+
* used as a group separator, so leftmost match could be wrong.
|
|
130
|
+
*/
|
|
131
|
+
let decimalPos = -1;
|
|
132
|
+
for (let i = mantissa.length - 1; i >= 0; i--) {
|
|
133
|
+
if (mantissa[i] === decimalSep) {
|
|
134
|
+
let hasDigitAfter = false;
|
|
135
|
+
for (let j = i + 1; j < mantissa.length; j++) {
|
|
136
|
+
if (isLocaleDigit(mantissa[j].charCodeAt(0), zeroCodePoint)) {
|
|
137
|
+
hasDigitAfter = true;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (hasDigitAfter) {
|
|
142
|
+
decimalPos = i;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let buf = "";
|
|
149
|
+
let inFraction = false;
|
|
150
|
+
|
|
151
|
+
const flush = () => {
|
|
152
|
+
if (buf) {
|
|
153
|
+
parts.push({ type: inFraction ? "fraction" : "integer", value: buf });
|
|
154
|
+
buf = "";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < mantissa.length; i++) {
|
|
159
|
+
const ch = mantissa[i];
|
|
160
|
+
|
|
161
|
+
if (i === decimalPos) {
|
|
162
|
+
flush();
|
|
163
|
+
parts.push({ type: "decimal", value: ch });
|
|
164
|
+
inFraction = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isLocaleDigit(ch.charCodeAt(0), zeroCodePoint)) {
|
|
169
|
+
buf += ch;
|
|
170
|
+
} else if (ch === "-") {
|
|
171
|
+
flush();
|
|
172
|
+
parts.push({ type: "minusSign", value: ch });
|
|
173
|
+
} else if (ch === "+") {
|
|
174
|
+
flush();
|
|
175
|
+
parts.push({ type: "plusSign", value: ch });
|
|
176
|
+
} else if (!inFraction) {
|
|
177
|
+
flush();
|
|
178
|
+
const prevDigit = i > 0 && isLocaleDigit(mantissa[i - 1].charCodeAt(0), zeroCodePoint);
|
|
179
|
+
const nextDigit =
|
|
180
|
+
i < mantissa.length - 1 && isLocaleDigit(mantissa[i + 1].charCodeAt(0), zeroCodePoint);
|
|
181
|
+
parts.push({
|
|
182
|
+
type: prevDigit && nextDigit ? "group" : "literal",
|
|
183
|
+
value: ch,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
flush();
|
|
187
|
+
parts.push({ type: "literal", value: ch });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
flush();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Parses a formatted number string (with optional E exponent) into typed parts.
|
|
196
|
+
* Separated from fallbackFormatToParts so it can be reused for polyfill strings.
|
|
197
|
+
*/
|
|
198
|
+
function parseNumberString(
|
|
199
|
+
formatted: string,
|
|
200
|
+
decimalSep: string,
|
|
201
|
+
zeroCodePoint = 48,
|
|
202
|
+
): Intl.NumberFormatPart[] {
|
|
203
|
+
// Detect exponent separator (E or e) — split into mantissa + exponent
|
|
204
|
+
let exponentPos = -1;
|
|
205
|
+
for (let i = 0; i < formatted.length; i++) {
|
|
206
|
+
if (formatted[i] === "E" || formatted[i] === "e") {
|
|
207
|
+
exponentPos = i;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const mantissa = exponentPos >= 0 ? formatted.slice(0, exponentPos) : formatted;
|
|
213
|
+
const parts: Intl.NumberFormatPart[] = [];
|
|
214
|
+
|
|
215
|
+
parseMantissa(mantissa, decimalSep, parts, zeroCodePoint);
|
|
216
|
+
|
|
217
|
+
if (exponentPos >= 0) {
|
|
218
|
+
parts.push({
|
|
219
|
+
type: "exponentSeparator" as string,
|
|
220
|
+
value: formatted[exponentPos],
|
|
221
|
+
} as Intl.NumberFormatPart);
|
|
222
|
+
|
|
223
|
+
let expBuf = "";
|
|
224
|
+
for (let i = exponentPos + 1; i < formatted.length; i++) {
|
|
225
|
+
const ch = formatted[i];
|
|
226
|
+
|
|
227
|
+
if (ch === "-") {
|
|
228
|
+
parts.push({ type: "exponentMinusSign" as string, value: ch } as Intl.NumberFormatPart);
|
|
229
|
+
} else if (ch === "+") {
|
|
230
|
+
parts.push({ type: "exponentPlusSign" as string, value: ch } as Intl.NumberFormatPart);
|
|
231
|
+
} else if (isLocaleDigit(ch.charCodeAt(0), zeroCodePoint)) {
|
|
232
|
+
expBuf += ch;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (expBuf) {
|
|
237
|
+
parts.push({ type: "exponentInteger" as string, value: expBuf } as Intl.NumberFormatPart);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return parts;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Hermes has Intl.NumberFormat but may lack formatToParts(). This fallback
|
|
246
|
+
* uses format() and parses the resulting string into typed parts.
|
|
247
|
+
*/
|
|
248
|
+
function fallbackFormatToParts(
|
|
249
|
+
formatter: Intl.NumberFormat,
|
|
250
|
+
value: number,
|
|
251
|
+
locales?: Intl.LocalesArgument,
|
|
252
|
+
): Intl.NumberFormatPart[] {
|
|
253
|
+
const formatted = formatter.format(value);
|
|
254
|
+
const zeroCp = detectOutputZeroCodePoint(formatted);
|
|
255
|
+
return parseNumberString(formatted, detectDecimalSeparator(locales), zeroCp);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Manually computes an engineering notation string for values where the
|
|
260
|
+
* platform's Intl.NumberFormat doesn't support notation: "engineering"
|
|
261
|
+
* (notably iOS Hermes, which uses NSNumberFormatter under the hood).
|
|
262
|
+
*
|
|
263
|
+
* Engineering notation: exponent is always a multiple of 3, mantissa has 1-3 integer digits.
|
|
264
|
+
*/
|
|
265
|
+
function computeEngineeringString(
|
|
266
|
+
value: number,
|
|
267
|
+
resolved: Intl.ResolvedNumberFormatOptions,
|
|
268
|
+
): string {
|
|
269
|
+
if (value === 0) return "0E0";
|
|
270
|
+
|
|
271
|
+
const negative = value < 0;
|
|
272
|
+
const abs = Math.abs(value);
|
|
273
|
+
const logFloor = Math.floor(Math.log10(abs));
|
|
274
|
+
const exp = 3 * Math.floor(logFloor / 3);
|
|
275
|
+
const mantissa = abs / 10 ** exp;
|
|
276
|
+
|
|
277
|
+
const maxFrac = resolved.maximumFractionDigits ?? 0;
|
|
278
|
+
const minFrac = resolved.minimumFractionDigits ?? 0;
|
|
279
|
+
|
|
280
|
+
let mantissaStr = mantissa.toFixed(maxFrac);
|
|
281
|
+
|
|
282
|
+
// Trim trailing zeros beyond minFrac
|
|
283
|
+
const dotIdx = mantissaStr.indexOf(".");
|
|
284
|
+
if (dotIdx >= 0) {
|
|
285
|
+
let end = mantissaStr.length;
|
|
286
|
+
const minEnd = dotIdx + 1 + minFrac;
|
|
287
|
+
|
|
288
|
+
while (end > minEnd && mantissaStr[end - 1] === "0") {
|
|
289
|
+
end--;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Remove the dot if no fraction digits remain
|
|
293
|
+
if (end <= dotIdx + 1 && minFrac === 0) {
|
|
294
|
+
end = dotIdx;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
mantissaStr = mantissaStr.slice(0, end);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const sign = negative ? "-" : "";
|
|
301
|
+
return `${sign}${mantissaStr}E${exp}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Safe wrapper around formatToParts that handles:
|
|
306
|
+
* 1. Missing formatToParts (Hermes fallback)
|
|
307
|
+
* 2. Broken formatToParts that returns all "literal" parts (non-Latin locales on Hermes)
|
|
308
|
+
* 3. Missing engineering notation support (iOS Hermes)
|
|
309
|
+
*/
|
|
310
|
+
export function safeFormatToParts(
|
|
311
|
+
formatter: Intl.NumberFormat,
|
|
312
|
+
value: number,
|
|
313
|
+
locales?: Intl.LocalesArgument,
|
|
314
|
+
): Intl.NumberFormatPart[] {
|
|
315
|
+
let parts: Intl.NumberFormatPart[];
|
|
316
|
+
|
|
317
|
+
if (typeof formatter.formatToParts === "function") {
|
|
318
|
+
parts = formatter.formatToParts(value);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Hermes may return formatToParts with all parts as "literal" for
|
|
322
|
+
* non-Latin locales, or misclassify digit characters. Validate that
|
|
323
|
+
* at least one "integer" or "fraction" part exists for non-zero values.
|
|
324
|
+
* If not, fall back to our manual parser which detects digits by codepoint.
|
|
325
|
+
*/
|
|
326
|
+
if (value !== 0) {
|
|
327
|
+
const hasDigitParts = parts.some((p) => p.type === "integer" || p.type === "fraction");
|
|
328
|
+
if (!hasDigitParts) {
|
|
329
|
+
parts = fallbackFormatToParts(formatter, value, locales);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
parts = fallbackFormatToParts(formatter, value, locales);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// iOS Hermes uses NSNumberFormatter which doesn't support engineering notation.
|
|
337
|
+
// It silently falls back to decimal formatting, producing no exponent parts.
|
|
338
|
+
// Detect this and manually compute the engineering representation.
|
|
339
|
+
const hasExponent = parts.some((p) => (p.type as string) === "exponentSeparator");
|
|
340
|
+
|
|
341
|
+
if (!hasExponent && Number.isFinite(value)) {
|
|
342
|
+
const notation = (formatter.resolvedOptions() as unknown as Record<string, unknown>).notation;
|
|
343
|
+
|
|
344
|
+
if (notation === "engineering") {
|
|
345
|
+
const str = computeEngineeringString(value, formatter.resolvedOptions());
|
|
346
|
+
return parseNumberString(str, ".");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return parts;
|
|
351
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { SUPERSCRIPT_SCALE } from "./constants";
|
|
2
|
+
import type { GlyphMetrics, KeyedPart, TextAlign } from "./types";
|
|
3
|
+
import { isDigitChar, localeDigitValue } from "./numerals";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Assigns x positions to each entry based on text alignment.
|
|
7
|
+
* Mutates the `x` field of each entry in place.
|
|
8
|
+
* Worklet-safe. When `precomputedContentWidth` is provided, skips the sum loop.
|
|
9
|
+
*/
|
|
10
|
+
export function assignXPositions(
|
|
11
|
+
chars: { x: number; width: number }[],
|
|
12
|
+
totalWidth: number,
|
|
13
|
+
textAlign: TextAlign,
|
|
14
|
+
precomputedContentWidth?: number,
|
|
15
|
+
): void {
|
|
16
|
+
"worklet";
|
|
17
|
+
let contentWidth = precomputedContentWidth ?? 0;
|
|
18
|
+
if (precomputedContentWidth === undefined) {
|
|
19
|
+
for (const entry of chars) contentWidth += entry.width;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let startX = 0;
|
|
23
|
+
if (textAlign === "right") startX = totalWidth - contentWidth;
|
|
24
|
+
else if (textAlign === "center") startX = (totalWidth - contentWidth) / 2;
|
|
25
|
+
|
|
26
|
+
let currentX = startX;
|
|
27
|
+
for (const entry of chars) {
|
|
28
|
+
entry.x = currentX;
|
|
29
|
+
currentX += entry.width;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CharLayout {
|
|
34
|
+
key: string;
|
|
35
|
+
char: string;
|
|
36
|
+
isDigit: boolean;
|
|
37
|
+
digitValue: number;
|
|
38
|
+
x: number;
|
|
39
|
+
width: number;
|
|
40
|
+
superscript?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function computeKeyedLayout(
|
|
44
|
+
parts: KeyedPart[],
|
|
45
|
+
metrics: GlyphMetrics,
|
|
46
|
+
totalWidth: number,
|
|
47
|
+
textAlign: TextAlign,
|
|
48
|
+
localeDigitStrings?: string[],
|
|
49
|
+
): CharLayout[] {
|
|
50
|
+
const chars: CharLayout[] = [];
|
|
51
|
+
|
|
52
|
+
for (const part of parts) {
|
|
53
|
+
const isSuperscript =
|
|
54
|
+
part.key.startsWith("exponentInteger:") || part.key.startsWith("exponentSign:");
|
|
55
|
+
|
|
56
|
+
// For digit parts, look up the width of the locale digit character
|
|
57
|
+
// (what DigitSlot actually renders) rather than the format output character.
|
|
58
|
+
// On Hermes, format() may output Latin "4" but DigitSlot renders e.g. "四".
|
|
59
|
+
let displayChar = part.char;
|
|
60
|
+
if (part.type === "digit" && localeDigitStrings && part.digitValue >= 0) {
|
|
61
|
+
displayChar = localeDigitStrings[part.digitValue];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rawWidth = metrics.charWidths[displayChar] ?? metrics.maxDigitWidth;
|
|
65
|
+
const width = isSuperscript ? rawWidth * SUPERSCRIPT_SCALE : rawWidth;
|
|
66
|
+
|
|
67
|
+
chars.push({
|
|
68
|
+
key: part.key,
|
|
69
|
+
char: part.char,
|
|
70
|
+
isDigit: part.type === "digit",
|
|
71
|
+
digitValue: part.digitValue,
|
|
72
|
+
x: 0,
|
|
73
|
+
width,
|
|
74
|
+
superscript: isSuperscript,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assignXPositions(chars, totalWidth, textAlign);
|
|
79
|
+
return chars;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function computeStringLayout(
|
|
83
|
+
text: string,
|
|
84
|
+
metrics: GlyphMetrics,
|
|
85
|
+
totalWidth: number,
|
|
86
|
+
textAlign: TextAlign,
|
|
87
|
+
zeroCodePoint = 48,
|
|
88
|
+
): CharLayout[] {
|
|
89
|
+
const chars: CharLayout[] = [];
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < text.length; i++) {
|
|
92
|
+
const char = text[i];
|
|
93
|
+
const digit = isDigitChar(char, zeroCodePoint);
|
|
94
|
+
const width = metrics.charWidths[char] ?? metrics.maxDigitWidth;
|
|
95
|
+
|
|
96
|
+
chars.push({
|
|
97
|
+
key: `pos:${i}`,
|
|
98
|
+
char,
|
|
99
|
+
isDigit: digit,
|
|
100
|
+
digitValue: digit ? localeDigitValue(char.charCodeAt(0), zeroCodePoint) : -1,
|
|
101
|
+
x: 0,
|
|
102
|
+
width,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
assignXPositions(chars, totalWidth, textAlign);
|
|
107
|
+
return chars;
|
|
108
|
+
}
|
package/src/core/mask.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CharLayout } from "./layout";
|
|
2
|
+
import type { GlyphMetrics } from "./types";
|
|
3
|
+
|
|
4
|
+
// Target gradient size as a ratio of lineHeight.
|
|
5
|
+
// The gradient will be at least this tall, expanding the container if needed.
|
|
6
|
+
const TARGET_GRADIENT_RATIO = 0.2;
|
|
7
|
+
|
|
8
|
+
// Small inset so the gradient doesn't start right at the glyph boundary.
|
|
9
|
+
const PADDING_PX = 2;
|
|
10
|
+
|
|
11
|
+
export interface MaskHeights {
|
|
12
|
+
// Total gradient height at the top edge
|
|
13
|
+
top: number;
|
|
14
|
+
// Total gradient height at the bottom edge
|
|
15
|
+
bottom: number;
|
|
16
|
+
// How much the container must expand beyond lineHeight at the top
|
|
17
|
+
expansionTop: number;
|
|
18
|
+
// How much the container must expand beyond lineHeight at the bottom
|
|
19
|
+
expansionBottom: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Computes adaptive vertical mask heights based on the actual characters
|
|
24
|
+
* currently displayed.
|
|
25
|
+
*
|
|
26
|
+
* The gradient starts at the edge of the visible glyph content and extends
|
|
27
|
+
* outward. If the "dead zone" inside lineHeight is smaller than the target
|
|
28
|
+
* gradient size, the container expands beyond lineHeight.
|
|
29
|
+
*/
|
|
30
|
+
export function computeAdaptiveMaskHeights(
|
|
31
|
+
layout: CharLayout[],
|
|
32
|
+
exitingEntries: Map<string, CharLayout>,
|
|
33
|
+
metrics: GlyphMetrics,
|
|
34
|
+
): MaskHeights {
|
|
35
|
+
let maxAscent = 0;
|
|
36
|
+
let maxDescent = 0;
|
|
37
|
+
|
|
38
|
+
const processChar = (char: string) => {
|
|
39
|
+
const bounds = metrics.charBounds[char];
|
|
40
|
+
if (!bounds) return;
|
|
41
|
+
|
|
42
|
+
if (bounds.top < maxAscent) maxAscent = bounds.top;
|
|
43
|
+
if (bounds.bottom > maxDescent) maxDescent = bounds.bottom;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const entry of layout) {
|
|
47
|
+
if (entry.superscript) continue;
|
|
48
|
+
processChar(entry.char);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const [, entry] of exitingEntries) {
|
|
52
|
+
if (entry.superscript) continue;
|
|
53
|
+
processChar(entry.char);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const targetGradient = TARGET_GRADIENT_RATIO * metrics.lineHeight;
|
|
57
|
+
|
|
58
|
+
// Dead zone: space within lineHeight not occupied by glyph content.
|
|
59
|
+
const deadZoneTop = Math.max(0, -metrics.ascent - -maxAscent - PADDING_PX);
|
|
60
|
+
const deadZoneBottom = Math.max(0, metrics.descent - maxDescent - PADDING_PX);
|
|
61
|
+
|
|
62
|
+
// If the dead zone is smaller than the target, expand the container.
|
|
63
|
+
const expansionTop = Math.max(0, targetGradient - deadZoneTop);
|
|
64
|
+
const expansionBottom = Math.max(0, targetGradient - deadZoneBottom);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
top: deadZoneTop + expansionTop,
|
|
68
|
+
bottom: deadZoneBottom + expansionBottom,
|
|
69
|
+
expansionTop,
|
|
70
|
+
expansionBottom,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { hanidecDigitValue } from "./digits";
|
|
2
|
+
import { CLDR_DEFAULT_NUMBERING, HANIDEC_ZERO, LATIN_ZERO, ZERO_CODEPOINTS } from "./tables";
|
|
3
|
+
|
|
4
|
+
const numberingSystemCache = new Map<string, string>();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Looks up the CLDR-expected numbering system for a locale.
|
|
8
|
+
* Checks explicit -u-nu- extension, then exact match (e.g. "ar-EG"),
|
|
9
|
+
* then language subtag (e.g. "ar").
|
|
10
|
+
* Returns undefined if the locale defaults to "latn".
|
|
11
|
+
*/
|
|
12
|
+
function getExpectedNumberingSystem(locales?: Intl.LocalesArgument): string | undefined {
|
|
13
|
+
if (!locales) return undefined;
|
|
14
|
+
|
|
15
|
+
const tag = String(locales);
|
|
16
|
+
|
|
17
|
+
// Parse explicit Unicode extension: "th-TH-u-nu-thai" → "thai"
|
|
18
|
+
const nuMatch = tag.match(/-u-nu-([a-z]+)/);
|
|
19
|
+
if (nuMatch) {
|
|
20
|
+
return nuMatch[1] === "latn" ? undefined : nuMatch[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const exact = CLDR_DEFAULT_NUMBERING[tag];
|
|
24
|
+
if (exact) return exact;
|
|
25
|
+
|
|
26
|
+
// Try language subtag only (strip region): "ar-EG" → "ar"
|
|
27
|
+
const dashIdx = tag.indexOf("-");
|
|
28
|
+
if (dashIdx > 0) {
|
|
29
|
+
return CLDR_DEFAULT_NUMBERING[tag.slice(0, dashIdx)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Scans a formatted number string and detects which numbering system's
|
|
37
|
+
* digits are actually present. Returns the zero codepoint of the detected
|
|
38
|
+
* system, or LATIN_ZERO if only Latin digits (or no digits) are found.
|
|
39
|
+
*
|
|
40
|
+
* Handles hanidec (non-contiguous ideographs) separately from contiguous systems.
|
|
41
|
+
*/
|
|
42
|
+
export function detectOutputZeroCodePoint(formattedStr: string): number {
|
|
43
|
+
for (let i = 0; i < formattedStr.length; i++) {
|
|
44
|
+
const code = formattedStr.charCodeAt(i);
|
|
45
|
+
|
|
46
|
+
// Skip Latin digits — we want to detect non-Latin systems
|
|
47
|
+
if (code >= LATIN_ZERO && code <= LATIN_ZERO + 9) continue;
|
|
48
|
+
|
|
49
|
+
// Check hanidec (non-contiguous codepoints)
|
|
50
|
+
if (hanidecDigitValue(code) >= 0) return HANIDEC_ZERO;
|
|
51
|
+
|
|
52
|
+
// Check all contiguous systems (skip latn and hanidec)
|
|
53
|
+
for (const system in ZERO_CODEPOINTS) {
|
|
54
|
+
if (system === "latn" || system === "hanidec") continue;
|
|
55
|
+
const zeroCp = ZERO_CODEPOINTS[system];
|
|
56
|
+
if (code >= zeroCp && code <= zeroCp + 9) return zeroCp;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return LATIN_ZERO;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Detects the numbering system for a locale/format combination.
|
|
65
|
+
*
|
|
66
|
+
* Strategy: query the platform first (resolvedOptions), then verify by
|
|
67
|
+
* formatting a probe. If the platform reports "latn" but the CLDR table
|
|
68
|
+
* says otherwise (common on Hermes), use the CLDR value.
|
|
69
|
+
*/
|
|
70
|
+
export function detectNumberingSystem(
|
|
71
|
+
locales?: Intl.LocalesArgument,
|
|
72
|
+
format?: Intl.NumberFormatOptions,
|
|
73
|
+
): string {
|
|
74
|
+
const key = JSON.stringify([locales, format]);
|
|
75
|
+
const cached = numberingSystemCache.get(key);
|
|
76
|
+
if (cached) return cached;
|
|
77
|
+
|
|
78
|
+
const formatter = new Intl.NumberFormat(locales ?? undefined, format);
|
|
79
|
+
const platformSystem = formatter.resolvedOptions().numberingSystem;
|
|
80
|
+
|
|
81
|
+
// If the platform reports a known non-latn system, trust it
|
|
82
|
+
if (platformSystem && platformSystem !== "latn") {
|
|
83
|
+
numberingSystemCache.set(key, platformSystem);
|
|
84
|
+
return platformSystem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Platform says "latn" or undefined — check if the locale expects something else
|
|
88
|
+
const expected = getExpectedNumberingSystem(locales);
|
|
89
|
+
if (!expected) {
|
|
90
|
+
numberingSystemCache.set(key, "latn");
|
|
91
|
+
return "latn";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Verify by checking the actual formatted output
|
|
95
|
+
const probe = formatter.format(1234567890);
|
|
96
|
+
const outputZeroCp = detectOutputZeroCodePoint(probe);
|
|
97
|
+
|
|
98
|
+
if (outputZeroCp !== LATIN_ZERO) {
|
|
99
|
+
// Platform does output non-Latin digits; find the matching system name
|
|
100
|
+
for (const system in ZERO_CODEPOINTS) {
|
|
101
|
+
if (ZERO_CODEPOINTS[system] === outputZeroCp) {
|
|
102
|
+
numberingSystemCache.set(key, system);
|
|
103
|
+
return system;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Platform truly outputs Latin digits — use the CLDR expected system
|
|
109
|
+
// so digitStrings render the correct locale characters on the wheel
|
|
110
|
+
numberingSystemCache.set(key, expected);
|
|
111
|
+
return expected;
|
|
112
|
+
}
|