diff --git a/.oxlintrc.json b/.oxlintrc.json index d807af3b95..086ee7f601 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -73,9 +73,13 @@ "eqeqeq": "error", "curly": "error", "no-console": "error", + "no-unused-expressions": "error", "arrow-body-style": ["error", "as-needed"], - "no-useless-escape": "off", "react/react-in-jsx-scope": "off", + "typescript/await-thenable": "error", + "typescript/no-duplicate-type-constituents": "error", + "typescript/no-meaningless-void-operator": "error", + "typescript/require-array-sort-compare": "error", "react/self-closing-comp": [ "error", { diff --git a/app/components/Menu/ContextMenu.tsx b/app/components/Menu/ContextMenu.tsx index 933f5f2f7e..a690991689 100644 --- a/app/components/Menu/ContextMenu.tsx +++ b/app/components/Menu/ContextMenu.tsx @@ -47,7 +47,7 @@ export const ContextMenu = observer( onClose?.(); } }, - [open, onOpen, onClose] + [onOpen, onClose] ); const enablePointerEvents = React.useCallback(() => { diff --git a/app/components/Sharing/components/Suggestions.tsx b/app/components/Sharing/components/Suggestions.tsx index e4ea4b7865..4f2b9a6b23 100644 --- a/app/components/Sharing/components/Suggestions.tsx +++ b/app/components/Sharing/components/Suggestions.tsx @@ -143,7 +143,7 @@ export const Suggestions = observer( ); React.useEffect(() => { - void fetchUsersByQuery(query); + fetchUsersByQuery(query); }, [query, fetchUsersByQuery]); function getListItemProps(suggestion: User | Group) { diff --git a/app/components/primitives/Menu/index.tsx b/app/components/primitives/Menu/index.tsx index 56317587fa..7677eea644 100644 --- a/app/components/primitives/Menu/index.tsx +++ b/app/components/primitives/Menu/index.tsx @@ -72,8 +72,7 @@ type ContentProps = React.ComponentPropsWithoutRef< React.ComponentPropsWithoutRef; const MenuContent = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, ContentProps >((props, ref) => { const { variant } = useMenuContext(); @@ -120,8 +119,7 @@ type SubMenuTriggerProps = BaseItemProps & React.ComponentPropsWithoutRef; const SubMenuTrigger = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, SubMenuTriggerProps >((props, ref) => { const { variant } = useMenuContext(); @@ -150,8 +148,7 @@ type SubMenuContentProps = React.ComponentPropsWithoutRef< React.ComponentPropsWithoutRef; const SubMenuContent = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, SubMenuContentProps >((props, ref) => { const { variant } = useMenuContext(); @@ -203,8 +200,7 @@ type MenuGroupProps = { >; const MenuGroup = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuGroupProps >((props, ref) => { const { variant } = useMenuContext(); @@ -275,8 +271,7 @@ type MenuButtonProps = BaseItemProps & { >; const MenuButton = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuButtonProps >((props, ref) => { const { variant } = useMenuContext(); @@ -338,8 +333,7 @@ type MenuInternalLinkProps = BaseItemProps & { >; const MenuInternalLink = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuInternalLinkProps >((props, ref) => { const { variant } = useMenuContext(); @@ -375,8 +369,7 @@ type MenuExternalLinkProps = BaseItemProps & { >; const MenuExternalLink = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuExternalLinkProps >((props, ref) => { const { variant } = useMenuContext(); @@ -409,8 +402,7 @@ type MenuSeparatorProps = React.ComponentPropsWithoutRef< React.ComponentPropsWithoutRef; const MenuSeparator = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuSeparatorProps >((props, ref) => { const { variant } = useMenuContext(); @@ -434,8 +426,7 @@ type MenuLabelProps = React.ComponentPropsWithoutRef< React.ComponentPropsWithoutRef; const MenuLabel = React.forwardRef< - | React.ElementRef - | React.ElementRef, + React.ElementRef, MenuLabelProps >((props, ref) => { const { variant } = useMenuContext(); diff --git a/app/editor/components/LinkEditor.tsx b/app/editor/components/LinkEditor.tsx index d6dd4b0c8b..906a39c806 100644 --- a/app/editor/components/LinkEditor.tsx +++ b/app/editor/components/LinkEditor.tsx @@ -144,7 +144,11 @@ const LinkEditor: React.FC = ({ if (selectedIndex >= 0 && results[selectedIndex]) { const selectedDoc = results[selectedIndex]; - !mark ? addLink(selectedDoc.url) : updateLink(selectedDoc.url); + if (!mark) { + addLink(selectedDoc.url); + } else { + updateLink(selectedDoc.url); + } } else if (!trimmedQuery) { removeLink(); } else if (!mark) { @@ -238,7 +242,11 @@ const LinkEditor: React.FC = ({ {results.map((doc, index) => ( { - !mark ? addLink(doc.path) : updateLink(doc.path); + if (!mark) { + addLink(doc.path); + } else { + updateLink(doc.path); + } }} onPointerMove={() => setSelectedIndex(index)} selected={index === selectedIndex} diff --git a/app/editor/extensions/SmartText.ts b/app/editor/extensions/SmartText.ts index 6a34831efb..1b31b1ca97 100644 --- a/app/editor/extensions/SmartText.ts +++ b/app/editor/extensions/SmartText.ts @@ -3,7 +3,7 @@ import { InputRule } from "@shared/editor/lib/InputRule"; const rightArrow = new InputRule(/->$/, "→"); // Note that the suppression of pipe here prevents conflict with table creation rule. -const emdash = new InputRule(/(?:^|[^\|])(--\s)$/, "— "); +const emdash = new InputRule(/(?:^|[^|])(--\s)$/, "— "); const oneHalf = new InputRule(/(?:^|\s)(1\/2)$/, "½"); const threeQuarters = new InputRule(/(?:^|\s)(3\/4)$/, "¾"); const copyright = new InputRule(/\(c\)$/, "©️"); @@ -12,17 +12,11 @@ const trademarked = new InputRule(/\(tm\)$/, "™️"); const ellipsis = new InputRule(/\.\.\.$/, "…"); // Double quotes -const openDoubleQuote = new InputRule( - /(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/, - "“" -); +const openDoubleQuote = new InputRule(/(?:^|[\s{[(<'"\u2018\u201C])(")$/, "“"); const closeDoubleQuote = new InputRule(/^(?!.*`)[\s\S]*(")$/, "”"); // Single quotes -const openSingleQuote = new InputRule( - /(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/, - "‘" -); +const openSingleQuote = new InputRule(/(?:^|[\s{[(<'"\u2018\u201C])(')$/, "‘"); const closeSingleQuote = new InputRule(/^(?!.*`)[\s\S]*(')$/, "’"); export default class SmartText extends Extension { diff --git a/app/editor/extensions/Suggestion.ts b/app/editor/extensions/Suggestion.ts index ceb1e4394a..0bb8342e5f 100644 --- a/app/editor/extensions/Suggestion.ts +++ b/app/editor/extensions/Suggestion.ts @@ -27,7 +27,7 @@ export default class Suggestion extends Extension { : `(?:${triggers.map(escapeRegExp).join("|")})`; this.openRegex = new RegExp( - `(?:^|\\s|\\(|[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}])${triggerPattern}(${`[\\p{L}\/\\p{M}\\d${ + `(?:^|\\s|\\(|[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}])${triggerPattern}(${`[\\p{L}/\\p{M}\\d${ this.options.allowSpaces ? "\\s{1}" : "" }\\.\\-–_]+`})${this.options.requireSearchTerm ? "" : "?"}$`, "u" diff --git a/app/hooks/usePaginatedRequest.ts b/app/hooks/usePaginatedRequest.ts index 82f9fa1fd1..4f24e1a397 100644 --- a/app/hooks/usePaginatedRequest.ts +++ b/app/hooks/usePaginatedRequest.ts @@ -31,7 +31,7 @@ const DEFAULT_LIMIT = 10; * @returns */ export default function usePaginatedRequest( - requestFn: (params?: PaginationParams | undefined) => Promise, + requestFn: (params?: PaginationParams) => Promise, params: PaginationParams = {} ): RequestResponse { const [data, setData] = useState(); diff --git a/app/hooks/usePinnedDocuments.ts b/app/hooks/usePinnedDocuments.ts index ed1d5b3c2a..90a8ccf2dd 100644 --- a/app/hooks/usePinnedDocuments.ts +++ b/app/hooks/usePinnedDocuments.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import usePersistedState from "~/hooks/usePersistedState"; import useStores from "./useStores"; -type UrlId = "home" | string; +type UrlId = string; export const pinsCacheKey = (urlId: UrlId) => `pins-${urlId}`; diff --git a/app/scenes/Document/hooks/useDocumentSave.ts b/app/scenes/Document/hooks/useDocumentSave.ts index 64b41bd82f..a674330568 100644 --- a/app/scenes/Document/hooks/useDocumentSave.ts +++ b/app/scenes/Document/hooks/useDocumentSave.ts @@ -282,7 +282,7 @@ export function useDocumentSave({ titleRef.current = value; document.title = value; updateIsDirtyRef.current(); - void autosave(); + autosave(); }, [document, autosave] ); diff --git a/app/scenes/Login/OAuthScopeHelper.ts b/app/scenes/Login/OAuthScopeHelper.ts index ebbdfc850b..ac301e4fb6 100644 --- a/app/scenes/Login/OAuthScopeHelper.ts +++ b/app/scenes/Login/OAuthScopeHelper.ts @@ -42,7 +42,7 @@ export class OAuthScopeHelper { return t("Write all data"); } - const [namespace, method] = scope.replace("/api/", "").split(/[:\.]/g); + const [namespace, method] = scope.replace("/api/", "").split(/[:.]/g); const readableMethod = methodToReadable[method as keyof typeof methodToReadable] ?? method; if (!readableMethod) { diff --git a/app/scenes/Settings/Template.tsx b/app/scenes/Settings/Template.tsx index a5fe777410..beb9d814b7 100644 --- a/app/scenes/Settings/Template.tsx +++ b/app/scenes/Settings/Template.tsx @@ -43,7 +43,9 @@ const LoadingState = observer(function LoadingState() { ui.addActiveModel(template); } return () => { - template && ui.removeActiveModel(template); + if (template) { + ui.removeActiveModel(template); + } }; }, [template, ui]); diff --git a/app/stores/PinsStore.ts b/app/stores/PinsStore.ts index 659e665c39..5cedad4087 100644 --- a/app/stores/PinsStore.ts +++ b/app/stores/PinsStore.ts @@ -53,7 +53,7 @@ export default class PinsStore extends Store { } @action - fetchPage = async (params?: FetchParams | undefined): Promise => { + fetchPage = async (params?: FetchParams): Promise => { this.isFetching = true; try { diff --git a/app/stores/StarsStore.ts b/app/stores/StarsStore.ts index 8d2906db2a..e83a43adc5 100644 --- a/app/stores/StarsStore.ts +++ b/app/stores/StarsStore.ts @@ -12,9 +12,7 @@ export default class StarsStore extends Store { } @action - fetchPage = async ( - params?: PaginationParams | undefined - ): Promise => { + fetchPage = async (params?: PaginationParams): Promise => { this.isFetching = true; try { diff --git a/app/stores/UserMembershipsStore.ts b/app/stores/UserMembershipsStore.ts index dffca1987d..19c632cf32 100644 --- a/app/stores/UserMembershipsStore.ts +++ b/app/stores/UserMembershipsStore.ts @@ -19,9 +19,7 @@ export default class UserMembershipsStore extends Store { } @action - fetchPage = async ( - params?: PaginationParams | undefined - ): Promise => { + fetchPage = async (params?: PaginationParams): Promise => { this.isFetching = true; try { diff --git a/app/stores/base/Store.ts b/app/stores/base/Store.ts index f2be44f926..09834eb285 100644 --- a/app/stores/base/Store.ts +++ b/app/stores/base/Store.ts @@ -401,7 +401,7 @@ export default abstract class Store { @action fetchPage = async ( - params?: FetchPageParams | undefined + params?: FetchPageParams ): Promise> => { if (!this.actions.includes(RPCAction.List)) { throw new Error(`Cannot list ${this.modelName}`); diff --git a/app/utils/ApiClient.ts b/app/utils/ApiClient.ts index 45e1b4fb35..4d316f7be8 100644 --- a/app/utils/ApiClient.ts +++ b/app/utils/ApiClient.ts @@ -285,7 +285,7 @@ class ApiClient { post = ( path: string, - data?: JSONObject | FormData | undefined, + data?: JSONObject | FormData, options?: FetchOptions ): Promise => { if (data instanceof FormData) { diff --git a/server/models/ApiKey.ts b/server/models/ApiKey.ts index cb3a7d5770..4116aa2a20 100644 --- a/server/models/ApiKey.ts +++ b/server/models/ApiKey.ts @@ -54,7 +54,7 @@ class ApiKey extends ParanoidModel< name: string; /** A list of scopes that this API key has access to */ - @Matches(/[\/\.\w\s]*/, { + @Matches(/[/.\w\s]*/, { each: true, }) @Column(DataType.ARRAY(DataType.STRING)) diff --git a/server/models/oauth/OAuthAuthentication.ts b/server/models/oauth/OAuthAuthentication.ts index 43936a615b..f75a355aca 100644 --- a/server/models/oauth/OAuthAuthentication.ts +++ b/server/models/oauth/OAuthAuthentication.ts @@ -83,7 +83,7 @@ class OAuthAuthentication extends ParanoidModel< grantId: string | null; /** A list of scopes that this authentication has access to */ - @Matches(/[\/\.\w\s]*/, { + @Matches(/[/.\w\s]*/, { each: true, }) @Column(DataType.ARRAY(DataType.STRING)) diff --git a/server/models/oauth/OAuthAuthorizationCode.ts b/server/models/oauth/OAuthAuthorizationCode.ts index c0e4835ca7..37767effe9 100644 --- a/server/models/oauth/OAuthAuthorizationCode.ts +++ b/server/models/oauth/OAuthAuthorizationCode.ts @@ -57,7 +57,7 @@ class OAuthAuthorizationCode extends IdModel< grantId: string | null; /** A list of scopes that this authorization code has access to */ - @Matches(/[\/\.\w\s]*/, { + @Matches(/[/.\w\s]*/, { each: true, }) @Column(DataType.ARRAY(DataType.STRING)) diff --git a/server/queues/tasks/ImportJSONTask.ts b/server/queues/tasks/ImportJSONTask.ts index 4d7251ba36..824db125eb 100644 --- a/server/queues/tasks/ImportJSONTask.ts +++ b/server/queues/tasks/ImportJSONTask.ts @@ -155,7 +155,7 @@ export default class ImportJSONTask extends ImportTask { } if (Object.values(item.attachments).length) { - await mapAttachments(item.attachments); + mapAttachments(item.attachments); } } diff --git a/server/queues/tasks/ImportMarkdownZipTask.ts b/server/queues/tasks/ImportMarkdownZipTask.ts index d1e99bf415..03114b39ac 100644 --- a/server/queues/tasks/ImportMarkdownZipTask.ts +++ b/server/queues/tasks/ImportMarkdownZipTask.ts @@ -228,11 +228,11 @@ export default class ImportMarkdownZipTask extends ImportTask { document.text = document.text .replace(new RegExp(escapeRegExp(encodedPath), "g"), reference) .replace( - new RegExp(`\\\.?/?${escapeRegExp(normalizedAttachmentPath)}`, "g"), + new RegExp(`\\.?/?${escapeRegExp(normalizedAttachmentPath)}`, "g"), reference ) .replace( - new RegExp(`\\\.?/?${escapeRegExp(genericNormalizedPath)}`, "g"), + new RegExp(`\\.?/?${escapeRegExp(genericNormalizedPath)}`, "g"), reference ); } diff --git a/server/routes/api/comments/comments.test.ts b/server/routes/api/comments/comments.test.ts index e5b82231e0..18082c80a0 100644 --- a/server/routes/api/comments/comments.test.ts +++ b/server/routes/api/comments/comments.test.ts @@ -397,9 +397,9 @@ describe("#comments.list", () => { expect(res.status).toEqual(200); expect(body.data.length).toEqual(2); - expect([body.data[0].id, body.data[1].id].sort()).toEqual( - [comment1.id, comment2.id].sort() - ); + expect( + [body.data[0].id, body.data[1].id].sort((a, b) => a.localeCompare(b)) + ).toEqual([comment1.id, comment2.id].sort((a, b) => a.localeCompare(b))); expect(body.policies.length).toEqual(2); expect(body.policies[0].abilities.read).toBeTruthy(); expect(body.policies[1].abilities.read).toBeTruthy(); diff --git a/server/validation.test.ts b/server/validation.test.ts index eba2d83123..10a282184a 100644 --- a/server/validation.test.ts +++ b/server/validation.test.ts @@ -48,7 +48,7 @@ describe("#ValidateKey.sanitize", () => { const uuid1 = randomUUID(); const uuid2 = randomUUID(); expect( - ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~\.\u0000\malicious_key`) + ValidateKey.sanitize(`public/${uuid1}/${uuid2}/~.\u0000malicious_key`) ).toEqual(`public/${uuid1}/${uuid2}/~.malicious_key`); }); diff --git a/shared/editor/embeds/index.tsx b/shared/editor/embeds/index.tsx index 102713fb37..b84d8e3e93 100644 --- a/shared/editor/embeds/index.tsx +++ b/shared/editor/embeds/index.tsx @@ -183,9 +183,7 @@ const embeds: EmbedDescriptor[] = [ id: "canva", title: "Canva", keywords: "design", - regexMatch: [ - /^https:\/\/(?:www\.)?canva\.com\/design\/([\/a-zA-Z0-9_\-]*)$/, - ], + regexMatch: [/^https:\/\/(?:www\.)?canva\.com\/design\/([/a-zA-Z0-9_-]*)$/], transformMatch: (matches: RegExpMatchArray) => { const input = matches.input ?? matches[0]; @@ -634,7 +632,7 @@ const embeds: EmbedDescriptor[] = [ id: "tella", title: "Tella", keywords: "video", - regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^\/]+)(?:.*)?$/], + regexMatch: [/^https?:\/\/(?:www\.)?tella\.tv\/video\/([^/]+)(?:.*)?$/], transformMatch: (matches: RegExpMatchArray) => `https://www.tella.tv/video/${matches[1]}/embed?b=0&title=1&a=0&loop=0&t=0&muted=0&wt=1`, icon: Tella, @@ -719,7 +717,7 @@ const embeds: EmbedDescriptor[] = [ title: "YouTube", keywords: "google video", regexMatch: [ - /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([\&\?](.*))?$/i, + /(?:https?:\/\/)?(?:www\.)?youtu\.?be(?:\.com)?\/?.*(?:watch|embed)?(?:.*v=|v\/|\/)([a-zA-Z0-9_-]{11})([&?](.*))?$/i, ], icon: YouTube, component: YouTube, @@ -729,7 +727,7 @@ const embeds: EmbedDescriptor[] = [ title: "Plant UML", keywords: "plant plantuml uml", regexMatch: [ - /(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9\-_]+)([\&\?].*)?$/i, + /(?:https?:\/\/)?(?:www\.)?editor\.plantuml\.com\/uml\/([a-zA-Z0-9_-]+)([&?].*)?$/i, ], icon: PlantUml, component: PlantUmlDiagrams, diff --git a/shared/editor/lib/headingToSlug.ts b/shared/editor/lib/headingToSlug.ts index 5235ca4245..6f32c50421 100644 --- a/shared/editor/lib/headingToSlug.ts +++ b/shared/editor/lib/headingToSlug.ts @@ -14,7 +14,7 @@ function safeSlugify(text: string) { const slug = `h-${escape( slugify(text, { - remove: /[!"#$%&'\.()*+,\/:;<=>?@\[\]\\^_`{|}~]/g, + remove: /[!"#$%&'.()*+,/:;<=>?@[\]\\^_`{|}~]/g, lower: true, }) )}`; diff --git a/shared/editor/lib/isMarkdown.test.ts b/shared/editor/lib/isMarkdown.test.ts index 5633c6fa8d..74b05070e6 100644 --- a/shared/editor/lib/isMarkdown.test.ts +++ b/shared/editor/lib/isMarkdown.test.ts @@ -25,11 +25,11 @@ this is code }); test("returns true for latex fence", () => { - expect(isMarkdown(`\$i\$`)).toBe(true); + expect(isMarkdown(`$i$`)).toBe(true); expect( - isMarkdown(`\$0.00 + isMarkdown(`$0.00 random content -\$1.00`) +$1.00`) ).toBe(false); }); diff --git a/shared/editor/rules/checkboxes.ts b/shared/editor/rules/checkboxes.ts index 153a1fefdd..8efa10934f 100644 --- a/shared/editor/rules/checkboxes.ts +++ b/shared/editor/rules/checkboxes.ts @@ -3,19 +3,19 @@ import type MarkdownIt from "markdown-it"; const CHECKBOX_REGEX = /\[(X|\s|_|-)\]\s(.*)?/i; -function matches(token: Token | void) { +function matches(token: Token | undefined) { return token && token.content.match(CHECKBOX_REGEX); } -function isInline(token: Token | void): boolean { +function isInline(token: Token | undefined): boolean { return !!token && token.type === "inline"; } -function isParagraph(token: Token | void): boolean { +function isParagraph(token: Token | undefined): boolean { return !!token && token.type === "paragraph_open"; } -function isListItem(token: Token | void): boolean { +function isListItem(token: Token | undefined): boolean { // Only match list_item_open, not checkbox_item_open - items that are already // checkbox_item_open have been processed (e.g., by the tables rule for // checkboxes in table cells) and should not be processed again. diff --git a/shared/editor/rules/emoji.ts b/shared/editor/rules/emoji.ts index fc8cc9bfc5..2ced32e859 100644 --- a/shared/editor/rules/emoji.ts +++ b/shared/editor/rules/emoji.ts @@ -4,7 +4,7 @@ import { full as emojiPlugin } from "markdown-it-emoji"; import { isUUID } from "validator"; import { nameToEmoji } from "../lib/emoji"; -type Options = MarkdownIt.Options & { +type Options = { emoji: boolean; }; diff --git a/shared/helpers/AuthenticationHelper.ts b/shared/helpers/AuthenticationHelper.ts index 277e446df8..9241fc3136 100644 --- a/shared/helpers/AuthenticationHelper.ts +++ b/shared/helpers/AuthenticationHelper.ts @@ -46,8 +46,8 @@ export default class AuthenticationHelper { const [namespace, method] = resource.split("."); return scopes.some((scope) => { - const [scopeNamespace, scopeMethod] = scope.match(/[:\.]/g) - ? scope.replace("/api/", "").split(/[:\.]/g) + const [scopeNamespace, scopeMethod] = scope.match(/[:.]/g) + ? scope.replace("/api/", "").split(/[:.]/g) : ["*", scope]; const isRouteScope = scope.startsWith("/api/"); diff --git a/shared/utils/currency.ts b/shared/utils/currency.ts index d16f1df348..cb83e4048f 100644 --- a/shared/utils/currency.ts +++ b/shared/utils/currency.ts @@ -82,7 +82,7 @@ export function isCurrency(value: string): boolean { } // Remove digits, separators, whitespace, and negative indicators - remaining = remaining.replace(/[\d.,\s()\-]/g, ""); + remaining = remaining.replace(/[\d.,\s()-]/g, ""); // If anything remains, it's not a valid currency return remaining.length === 0;