mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
adbffc0734
* chore: clear mechanical lint warnings Drops 44 oxlint warnings (559 → 515) by fixing easy mechanical rules across the codebase: no-useless-escape, no-duplicate-type-constituents, no-redundant-type-constituents, no-unused-expressions, no-meaningless-void-operator, require-array-sort-compare, await-thenable. * chore: drop callback parameter from useCallback deps The `open` argument is a parameter of the callback, not a closed-over variable, so it doesn't belong in the deps array. * chore: promote cleared lint rules to errors Promotes the rules cleared in this PR from warn to error so future violations fail the lint: - no-unused-expressions - typescript/await-thenable - typescript/no-duplicate-type-constituents - typescript/no-meaningless-void-operator - typescript/require-array-sort-compare Removes the override that suppressed no-useless-escape on source files (the global rule is already error) and fixes the 21 escape violations that this exposed in regex character classes and template literals. * chore: address PR review feedback - usePinnedDocuments: simplify UrlId to plain string instead of the intersection trick. - PlantUML embed: move - to end of character class so it's a literal hyphen rather than a range operator. - checkboxes: type token params as Token | undefined to match the actual call sites that pass tokens[index - 2] etc.
179 lines
5.4 KiB
TypeScript
179 lines
5.4 KiB
TypeScript
/**
|
|
* Common currency symbols used around the world.
|
|
*/
|
|
const currencySymbols = [
|
|
"$", // Dollar (USD, CAD, AUD, etc.)
|
|
"€", // Euro
|
|
"£", // Pound
|
|
"¥", // Yen/Yuan
|
|
"₹", // Indian Rupee
|
|
"₽", // Russian Ruble
|
|
"₿", // Bitcoin
|
|
"₩", // Korean Won
|
|
"₪", // Israeli Shekel
|
|
"₺", // Turkish Lira
|
|
"₴", // Ukrainian Hryvnia
|
|
"₱", // Philippine Peso
|
|
"฿", // Thai Baht
|
|
"₫", // Vietnamese Dong
|
|
"₦", // Nigerian Naira
|
|
"₵", // Ghanaian Cedi
|
|
"₡", // Costa Rican Colón
|
|
"₲", // Paraguayan Guaraní
|
|
"₸", // Kazakhstani Tenge
|
|
"₼", // Azerbaijani Manat
|
|
"₾", // Georgian Lari
|
|
"৳", // Bangladeshi Taka
|
|
"₠", // European Currency Unit
|
|
"R$", // Brazilian Real
|
|
"kr", // Scandinavian Krona/Krone
|
|
"zł", // Polish Zloty
|
|
"Kč", // Czech Koruna
|
|
"Ft", // Hungarian Forint
|
|
"CHF", // Swiss Franc
|
|
"лв", // Bulgarian Lev
|
|
"lei", // Romanian Leu
|
|
"ден", // Macedonian Denar
|
|
"дин", // Serbian Dinar
|
|
"ر.س", // Saudi Riyal
|
|
"د.إ", // UAE Dirham
|
|
"ر.ع", // Omani Rial
|
|
"د.ك", // Kuwaiti Dinar
|
|
"د.ب", // Bahraini Dinar
|
|
"ر.ق", // Qatari Riyal
|
|
];
|
|
|
|
/**
|
|
* Checks if a string appears to be a currency value.
|
|
* Matches formats like: $1,234.56, €50, £1.000,50, -$500, ($500), 1234¥
|
|
*
|
|
* @param value - the string to check.
|
|
* @returns true if the string appears to be a currency value.
|
|
*/
|
|
export function isCurrency(value: string): boolean {
|
|
if (!value || value.trim().length === 0) {
|
|
return false;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
|
|
// Must contain at least one currency symbol
|
|
const hasCurrencySymbol = currencySymbols.some((symbol) =>
|
|
trimmed.includes(symbol)
|
|
);
|
|
if (!hasCurrencySymbol) {
|
|
return false;
|
|
}
|
|
|
|
// Must contain at least one digit
|
|
if (!/\d/.test(trimmed)) {
|
|
return false;
|
|
}
|
|
|
|
// Remove all valid currency characters and check if anything unexpected remains
|
|
let remaining = trimmed;
|
|
|
|
// Remove currency symbols (longest first to handle multi-char symbols like R$)
|
|
const sortedSymbols = [...currencySymbols].sort(
|
|
(a, b) => b.length - a.length
|
|
);
|
|
for (const symbol of sortedSymbols) {
|
|
remaining = remaining.split(symbol).join("");
|
|
}
|
|
|
|
// Remove digits, separators, whitespace, and negative indicators
|
|
remaining = remaining.replace(/[\d.,\s()-]/g, "");
|
|
|
|
// If anything remains, it's not a valid currency
|
|
return remaining.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Parses a currency string and returns its numeric value.
|
|
* Handles various formats including:
|
|
* - US/UK style: $1,234.56
|
|
* - European style: €1.234,56
|
|
* - Negative values: -$500, ($500), -500€
|
|
* - Currency symbol before or after the number
|
|
*
|
|
* @param value - the currency string to parse.
|
|
* @returns the numeric value, or null if parsing fails.
|
|
*/
|
|
export function parseCurrency(value: string): number | null {
|
|
if (!value || value.trim().length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let trimmed = value.trim();
|
|
|
|
// Detect negative values: parentheses indicate negative in accounting
|
|
const isNegative =
|
|
trimmed.startsWith("(") ||
|
|
trimmed.startsWith("-") ||
|
|
trimmed.includes(")-") ||
|
|
(trimmed.endsWith(")") && trimmed.includes("("));
|
|
|
|
// Remove currency symbols, parentheses, and whitespace
|
|
// Sort symbols by length descending so multi-character symbols (like R$) are removed first
|
|
let cleaned = trimmed;
|
|
const sortedSymbols = [...currencySymbols].sort(
|
|
(a, b) => b.length - a.length
|
|
);
|
|
for (const symbol of sortedSymbols) {
|
|
cleaned = cleaned.split(symbol).join("");
|
|
}
|
|
cleaned = cleaned
|
|
.replace(/[()]/g, "")
|
|
.replace(/\s/g, "")
|
|
.replace(/^-|-$/g, "");
|
|
|
|
// Determine the decimal separator by looking at the last separator
|
|
// European format uses comma as decimal: 1.234,56
|
|
// US/UK format uses period as decimal: 1,234.56
|
|
const lastComma = cleaned.lastIndexOf(",");
|
|
const lastPeriod = cleaned.lastIndexOf(".");
|
|
const hasComma = lastComma !== -1;
|
|
const hasPeriod = lastPeriod !== -1;
|
|
|
|
if (hasComma && hasPeriod) {
|
|
// Both separators present - the one that appears last is the decimal
|
|
if (lastComma > lastPeriod) {
|
|
// European format: comma is the decimal separator
|
|
// Remove periods (thousands separator) and replace comma with period
|
|
cleaned = cleaned.replace(/\./g, "").replace(",", ".");
|
|
} else {
|
|
// US/UK format: period is the decimal separator
|
|
// Remove commas (thousands separator)
|
|
cleaned = cleaned.replace(/,/g, "");
|
|
}
|
|
} else if (hasComma) {
|
|
// Only commas present - could be thousands separator or decimal
|
|
// If there's exactly one comma and 1-2 digits after it, treat as decimal
|
|
const parts = cleaned.split(",");
|
|
if (parts.length === 2 && parts[1].length <= 2) {
|
|
cleaned = cleaned.replace(",", ".");
|
|
} else {
|
|
// Multiple commas or 3+ digits after comma = thousands separator
|
|
cleaned = cleaned.replace(/,/g, "");
|
|
}
|
|
} else if (hasPeriod) {
|
|
// Only periods present - could be thousands separator or decimal
|
|
// If there's exactly one period and 1-2 digits after it, treat as decimal
|
|
const parts = cleaned.split(".");
|
|
if (parts.length === 2 && parts[1].length <= 2) {
|
|
// Already in correct format
|
|
} else {
|
|
// Multiple periods or 3+ digits after period = thousands separator
|
|
cleaned = cleaned.replace(/\./g, "");
|
|
}
|
|
}
|
|
|
|
const numericValue = parseFloat(cleaned);
|
|
|
|
if (isNaN(numericValue)) {
|
|
return null;
|
|
}
|
|
|
|
return isNegative ? -Math.abs(numericValue) : numericValue;
|
|
}
|