UNPKG

@finos/legend-shared

Version:
246 lines (227 loc) 8.08 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { type Replacer, stringify as losslessStringify, parse as losslessParse, isSafeNumber as lossIsSafeNumber, } from 'lossless-json'; import CSVParser, { type LocalFile, type ParseLocalConfig, type UnparseConfig, } from 'papaparse'; import { assertNonNullable } from '../error/AssertionUtils.js'; export const capitalize = (value: string): string => value.length >= 1 ? `${(value[0] as string).toUpperCase()}${value.substring(1, value.length)}` : value; export const toSentenceCase = (value: string | undefined): string => (value ?? '').trim().replace(/^(?:\w+)\b/u, (val) => capitalize(val)); export const TITLE_CASE_EXCEPTION_WORDS = [ // We roughly follow AP style for simplicity // See https://en.wikipedia.org/wiki/Title_case 'a', 'an', 'the', 'but', 'by', 'nor', 'and', 'or', 'so', 'as', 'yet', 'for', 'in', 'nor', 'of', 'on', 'at', 'out', 'to', 'up', ]; export const toTitleCase = (value: string | undefined): string => (value ?? '') .trim() .replace(/\b(?:\w+)\b/gu, (val) => { // lowercase minor words (typically articles, short prepositions, and some conjunctions) if (TITLE_CASE_EXCEPTION_WORDS.includes(val.toLowerCase())) { return val.toLowerCase(); } return capitalize(val); }) // always capitalize first and last word .replace(/^(?:\w+)\b/u, (val) => capitalize(val)) .replace(/\b(?:\w+)$/u, (val) => capitalize(val)); export const prettyCONSTName = (value: string | undefined): string => { if (!value) { return ''; } // This handles our constant naming convention, e.g. SOME_VALUE, __PRIVATE_VALUE__, etc. if (value.trim().match(/^[A-Z_]+$/)) { return toTitleCase(value.trim().replace(/_+/gu, ' ').toLowerCase()); } return ( capitalize(value.trim()) // NOTE: here we must use capturing group as we also need to capture the breakpoint/separator as chunks .split(/(?<chunk>[A-Z][a-z]+|[0-9]+)/) .map((chunk) => chunk.toUpperCase() === chunk ? chunk : chunk.charAt(0).toUpperCase() + chunk.slice(1).toLowerCase(), ) .filter(Boolean) .join(' ') .replace(/_+/gu, ' ') .replace(/\s+/gu, ' ') ); }; export const tryToFormatJSONString = (value: string, tabSize = 2): string => { try { return JSON.stringify(JSON.parse(value), undefined, tabSize); } catch { return value; } }; export const tryToMinifyJSONString = (value: string): string => { try { return JSON.stringify(JSON.parse(value)); } catch { return value.replace(/\n\s*/g, ''); } }; /** * NOTE: this splits a string value into an array of strings by using a * delimiter of a comma if the string is only one line. However, if the * string has multiple lines, the delimiter will not be applied. This is so * that for an example input of multiple lines like * One, Comma in One * Two * Three * will still equal 3 elements (['One, Comma in One', 'Two', 'Three']) rather than 4 */ export const parseCSVString = (value: string): string[] | undefined => { let parseData; if (value.includes('\n')) { parseData = value.trim().split(/\r?\n|\r|\n/g); return parseData; } else { const parseResult = CSVParser.parse<string[]>(value.trim(), { delimiter: ',', }); parseData = parseResult.data.flat(); if (parseResult.errors.length) { // if ( // parseResult.errors[0] && // parseResult.errors[0].code === 'UndetectableDelimiter' && // parseResult.errors[0].type === 'Delimiter' && // parseResult.data.length === 1 // ) { // // NOTE: this happens when the user only put one item in the value input // // we can go the other way by ensure the input has a comma but this is arguably neater // // as it tinkers with the parser // } else { // // there were some parsing error, escape // // NOTE: ideally, we could show a warning here // } return undefined; } else { return parseData; } } }; export const parseCSVFile = ( file: LocalFile, config: ParseLocalConfig<unknown[], LocalFile>, ): void => CSVParser.parse(file, config); export const csvStringify = ( value: unknown[], config?: UnparseConfig, ): string => CSVParser.unparse(value, config); /** * One very common use case is that we get the JSON as response from the server than we will convert this to a string and persist * in the protocol. As such, we have to make sure this string is safe in PURE grammar format, i.e. it will escape single quotes * properly since PURE strings are wrapped in a pair of single quotes. The following pair of escape/unescape method does the simple job of * converting a JSON string to/from PURE grammar string * * NOTE: this is slightly different than escaping/unescaping JS string or normal JSON string, this is conversion is actually simplier since * the escaping is naturally handling by string conversion in Javascript, we just need to have special handling for the escaping of single-quotes * See https://stackoverflow.com/questions/3020094/how-should-i-escape-strings-in-json * See https://github.com/joliss/js-string-escape/blob/master/index.js */ export const fromGrammarString = (value: string): string => value.replace(/\\'/gu, "'"); export const toGrammarString = (value: string): string => value.replace(/'/gu, "\\'"); /** * These are the sets of methods that helps with lossless conversion of JSON to/from text. * * The implementation of `JSON.parse` and `JSON.stringify` in Javascript is not lossless, e.g. values like 1.0 (a double) is automatically converted to 1 (an integer). * This pairs of method will convert the JSON losslessly. One caveat is that numeric values are stored as LosslessNumber, a data type which stores the numeric value as a string. * * NOTE: One can perform regular operations with a LosslessNumber, and it will throw an error when this would result in losing information. * But use this with discretion since it it does not result in the same object as `JSON.parse` */ export { losslessParse as parseLosslessJSON }; export { lossIsSafeNumber as isLossSafeNumber }; export const stringifyLosslessJSON = ( val: unknown, replacer?: Replacer, space?: number | string, ): string => { const result = losslessStringify(val, replacer, space); assertNonNullable(result, `Can't stringify lossless JSON value`); return result; }; export const tryToFormatLosslessJSONString = ( value: string, tabSize = 2, ): string => { try { return stringifyLosslessJSON(losslessParse(value), undefined, tabSize); } catch { return value; } }; export const tryToMinifyLosslessJSONString = (value: string): string => { try { return tryToFormatLosslessJSONString(value, 0); } catch { return value.replace(/\n\s*/g, ''); } }; export const indent = (value: string, indentText: string): string => value .split('\n') .map((line) => `${indentText}${line}`) .join('\n'); export const quantify = ( value: number, label: string, pluralForm?: string | undefined, ): string => value <= 0 ? `no ${pluralForm ?? `${label}s`}` : value > 1 ? `${value} ${pluralForm ?? `${label}s`}` : `1 ${label}`; export const quantifyList = ( val: Array<unknown>, label: string, pluralForm?: string | undefined, ): string => quantify(val.length, label, pluralForm);