@base-ui-components/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
214 lines (201 loc) • 8.74 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.UNICODE_PLUS_SIGNS = exports.UNICODE_MINUS_SIGNS = exports.SPACE_SEPARATOR_RE = exports.PLUS_SIGNS_WITH_ASCII = exports.PERSIAN_RE = exports.PERSIAN_NUMERALS = exports.PERSIAN_DETECT_RE = exports.PERMILLE_RE = exports.PERMILLE = exports.PERCENT_RE = exports.PERCENTAGES = exports.MINUS_SIGNS_WITH_ASCII = exports.HAN_RE = exports.HAN_NUMERAL_TO_DIGIT = exports.HAN_NUMERALS = exports.HAN_DETECT_RE = exports.FULLWIDTH_RE = exports.FULLWIDTH_NUMERALS = exports.FULLWIDTH_GROUP = exports.FULLWIDTH_DETECT_RE = exports.FULLWIDTH_DECIMAL = exports.BASE_NON_NUMERIC_SYMBOLS = exports.ARABIC_RE = exports.ARABIC_NUMERALS = exports.ARABIC_DETECT_RE = exports.ANY_PLUS_RE = exports.ANY_PLUS_DETECT_RE = exports.ANY_MINUS_RE = exports.ANY_MINUS_DETECT_RE = void 0;
exports.getNumberLocaleDetails = getNumberLocaleDetails;
exports.parseNumber = parseNumber;
var _formatNumber = require("../../utils/formatNumber");
const HAN_NUMERALS = exports.HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
// Map Han numeral characters to ASCII digits. Includes both forms of zero.
const HAN_NUMERAL_TO_DIGIT = exports.HAN_NUMERAL_TO_DIGIT = {
零: '0',
〇: '0',
一: '1',
二: '2',
三: '3',
四: '4',
五: '5',
六: '6',
七: '7',
八: '8',
九: '9'
};
const ARABIC_NUMERALS = exports.ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
const PERSIAN_NUMERALS = exports.PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
const FULLWIDTH_NUMERALS = exports.FULLWIDTH_NUMERALS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const PERCENTAGES = exports.PERCENTAGES = ['%', '٪', '%', '﹪'];
const PERMILLE = exports.PERMILLE = ['‰', '؉'];
const UNICODE_MINUS_SIGNS = exports.UNICODE_MINUS_SIGNS = ['−', '-', '‒', '–', '—', '﹣'];
const UNICODE_PLUS_SIGNS = exports.UNICODE_PLUS_SIGNS = ['+', '﹢'];
// Fullwidth punctuation common in CJK inputs
const FULLWIDTH_DECIMAL = exports.FULLWIDTH_DECIMAL = '.'; // U+FF0E
const FULLWIDTH_GROUP = exports.FULLWIDTH_GROUP = ','; // U+FF0C
const ARABIC_RE = exports.ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
const PERSIAN_RE = exports.PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
const FULLWIDTH_RE = exports.FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
const HAN_RE = exports.HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
const PERCENT_RE = exports.PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
const PERMILLE_RE = exports.PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
// Detection regexes (non-global to avoid lastIndex side effects)
const ARABIC_DETECT_RE = exports.ARABIC_DETECT_RE = /[٠١٢٣٤٥٦٧٨٩]/;
const PERSIAN_DETECT_RE = exports.PERSIAN_DETECT_RE = /[۰۱۲۳۴۵۶۷۸۹]/;
const HAN_DETECT_RE = exports.HAN_DETECT_RE = /[零〇一二三四五六七八九]/;
const FULLWIDTH_DETECT_RE = exports.FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);
const BASE_NON_NUMERIC_SYMBOLS = exports.BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
const SPACE_SEPARATOR_RE = exports.SPACE_SEPARATOR_RE = /\p{Zs}/u;
const PLUS_SIGNS_WITH_ASCII = exports.PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
const MINUS_SIGNS_WITH_ASCII = exports.MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]
const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
const ANY_MINUS_RE = exports.ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
const ANY_PLUS_RE = exports.ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
const ANY_MINUS_DETECT_RE = exports.ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
const ANY_PLUS_DETECT_RE = exports.ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
function getNumberLocaleDetails(locale, options) {
const parts = (0, _formatNumber.getFormatter)(locale, options).formatToParts(11111.1);
const result = {};
parts.forEach(part => {
result[part.type] = part.value;
});
// The formatting options may result in not returning a decimal.
(0, _formatNumber.getFormatter)(locale).formatToParts(0.1).forEach(part => {
if (part.type === 'decimal') {
result[part.type] = part.value;
}
});
return result;
}
function parseNumber(formattedNumber, locale, options) {
if (formattedNumber == null) {
return null;
}
// Normalize control characters and whitespace; remove bidi/format controls
let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();
// Normalize unicode minus/plus to ASCII, handle leading/trailing signs
input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
let isNegative = false;
// Trailing sign, e.g. "1234-" / "1234+"
const trailing = input.match(/([+-])\s*$/);
if (trailing) {
if (trailing[1] === '-') {
isNegative = true;
}
input = input.replace(/([+-])\s*$/, '');
}
// Leading sign
const leading = input.match(/^\s*([+-])/);
if (leading) {
if (leading[1] === '-') {
isNegative = true;
}
input = input.replace(/^\s*[+-]/, '');
}
// Heuristic locale detection
let computedLocale = locale;
if (computedLocale === undefined) {
if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
computedLocale = 'ar';
} else if (HAN_DETECT_RE.test(input)) {
computedLocale = 'zh';
}
}
const {
group,
decimal,
currency
} = getNumberLocaleDetails(computedLocale, options);
// Build robust unit regex from all unit parts (such as "km/h")
const unitParts = (0, _formatNumber.getFormatter)(computedLocale, options).formatToParts(1).filter(p => p.type === 'unit').map(p => escapeRegExp(p.value));
const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null;
let groupRegex = null;
if (group) {
// Check if the group separator is a space-like character.
// If so, we'll replace all such characters with an empty string.
groupRegex = /\p{Zs}/u.test(group) ? /\p{Zs}/gu : new RegExp(escapeRegExp(group), 'g');
}
const replacements = [{
regex: group ? groupRegex : null,
replacement: ''
}, {
regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
replacement: '.'
},
// Fullwidth punctuation
{
regex: /./g,
replacement: '.'
},
// FULLWIDTH_DECIMAL
{
regex: /,/g,
replacement: ''
},
// FULLWIDTH_GROUP
// Arabic punctuation
{
regex: /٫/g,
replacement: '.'
},
// ARABIC DECIMAL SEPARATOR (U+066B)
{
regex: /٬/g,
replacement: ''
},
// ARABIC THOUSANDS SEPARATOR (U+066C)
// Currency & unit labels
{
regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
replacement: ''
}, {
regex: unitRegex,
replacement: ''
},
// Numeral systems to ASCII digits
{
regex: ARABIC_RE,
replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
}, {
regex: PERSIAN_RE,
replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
}, {
regex: FULLWIDTH_RE,
replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
}, {
regex: HAN_RE,
replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
}];
let unformatted = replacements.reduce((acc, {
regex,
replacement
}) => {
return regex ? acc.replace(regex, replacement) : acc;
}, input);
// Mixed-locale safety: keep only the last '.' as decimal
const lastDot = unformatted.lastIndexOf('.');
if (lastDot !== -1) {
unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`;
}
// Guard against Infinity inputs (ASCII and symbol)
if (/^[-+]?Infinity$/i.test(input) || /[∞]/.test(input)) {
return null;
}
const parseTarget = (isNegative ? '-' : '') + unformatted;
let num = parseFloat(parseTarget);
const style = options?.style;
const isUnitPercent = style === 'unit' && options?.unit === 'percent';
const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent';
const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber);
if (hasPermilleSymbol) {
num /= 1000;
} else if (!isUnitPercent && hasPercentSymbol) {
num /= 100;
}
if (Number.isNaN(num)) {
return null;
}
return num;
}