@eclipse-scout/core
Version:
Eclipse Scout runtime
462 lines (423 loc) • 15.4 kB
text/typescript
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {HtmlEncoder, objects, PlainTextEncoder, PlainTextEncoderOptions, scout} from '../index';
let htmlEncoder: HtmlEncoder = null;
let plainTextEncoder: PlainTextEncoder = null;
let lineFeedRegex = /\n/g;
let carriageReturnRegex = /\r/g;
let whitespaceRegex = /^\s*$/;
export const strings = {
/**
* @param [encodeHtml] defaults to true
*/
nl2br(text: string, encodeHtml?: boolean): string {
if (!text) {
return text;
}
text = strings.asString(text);
encodeHtml = scout.nvl(encodeHtml, true);
if (encodeHtml) {
text = strings.encode(text);
}
return text.replace(lineFeedRegex, '<br>').replace(carriageReturnRegex, '');
},
insertAt(text: string, insertText: string, position: number): string {
if (!text) {
return text;
}
text = strings.asString(text);
insertText = strings.asString(insertText);
// @ts-expect-error
if (insertText && (typeof position === 'number' || position instanceof Number) && position >= 0) {
return text.substring(0, position) + insertText + text.substring(position);
}
return text;
},
/**
* @returns true if the given string contains any non-space characters
*/
hasText(text: string): boolean {
if (text === undefined || text === null) {
return false;
}
text = strings.asString(text);
if (typeof text !== 'string' || text.length === 0) {
return false;
}
return !whitespaceRegex.test(text);
},
/**
* Inverse operation of hasText(string). Used because empty(s) is more readable than !hasText(s).
* @returns true if the given string is not set or contains only white-space characters.
*/
empty(text: string): boolean {
return !strings.hasText(text);
},
repeat(pattern: string, count: number): string {
if (pattern === undefined || pattern === null) {
return pattern;
}
if (typeof count !== 'number' || count < 1) {
return '';
}
let result = '';
for (let i = 0; i < count; i++) {
result += pattern;
}
return result;
},
padZeroLeft(string: string | number, padding: number): string {
let s = strings.asString(string);
if (s === undefined || s === null || typeof padding !== 'number' || padding < 1 || s.length >= padding) {
return s;
}
let z = strings.repeat('0', padding) + s;
return z.slice(-padding);
},
contains(string: string, searchFor: string): boolean {
if (!string) {
return false;
}
return string.indexOf(searchFor) > -1;
},
startsWith(fullString: string, startString: string): boolean {
if (objects.isNullOrUndefined(fullString) || objects.isNullOrUndefined(startString)) {
return false;
}
fullString = strings.asString(fullString);
startString = strings.asString(startString);
return fullString.startsWith(startString);
},
endsWith(fullString: string, endString: string): boolean {
if (objects.isNullOrUndefined(fullString) || objects.isNullOrUndefined(endString)) {
return false;
}
fullString = strings.asString(fullString);
endString = strings.asString(endString);
return fullString.endsWith(endString);
},
/**
* Returns the number of occurrences of 'separator' in 'string'
*/
count(string: string, separator: string): number {
if (!string || separator === undefined || separator === null) {
return 0;
}
string = strings.asString(string);
separator = strings.asString(separator);
return string.split(separator).length - 1;
},
/**
* Returns the HTML encoded text. Example: 'Foo<br>Bar' returns 'Foo&lt;br&gt;Bar'.
* If the argument is or undefined, the same value is returned.
* @param text plain text to encode
* @returns HTML encoded text
*/
encode(text: string): string {
if (!htmlEncoder) { // lazy instantiation to avoid cyclic dependency errors during webpack bootstrap
htmlEncoder = new HtmlEncoder();
}
return htmlEncoder.encode(text);
},
/**
* Returns the plain text of the given html string using simple tag replacement.<p>
* Tries to preserve the new lines. Since it does not consider the style, it won't be right in any cases.
* A div for example always generates a new line, even if display style is not set to block.
*/
plainText(text: string, options?: PlainTextEncoderOptions): string {
if (!plainTextEncoder) { // lazy instantiation to avoid cyclic dependency errors during webpack bootstrap
plainTextEncoder = new PlainTextEncoder();
}
return plainTextEncoder.encode(text, options);
},
/**
* Joins a list of strings to a single string using the given separator. Elements that are
* not defined or have zero length are ignored. The default return value is the empty string.
*
* @param separator String to use as separator
* @param args list of strings to join. Can be an array or individual arguments
*/
join(separator: string, ...args: string[]): string {
let stringsToJoin = args;
if (args[0] && objects.isArray(args[0])) {
stringsToJoin = (args[0] as unknown) as string[];
}
separator = strings.asString(separator);
let s = '';
for (let i = 0; i < stringsToJoin.length; i++) {
let arg = strings.asString(stringsToJoin[i]);
if (arg) {
if (s && separator) {
s += separator;
}
s += arg;
}
}
return s;
},
/**
* If the given 'string' has text, it is returned with the 'prefix' and 'suffix'
* prepended and appended, respectively. Otherwise, the empty string is returned.
*/
box(prefix: string, string: string, suffix?: string): string {
prefix = strings.asString(prefix);
string = strings.asString(string);
suffix = strings.asString(suffix);
let s = '';
if (strings.hasText(string)) {
if (prefix) {
s += prefix;
}
s += string;
if (suffix) {
s += suffix;
}
}
return s;
},
/**
* Quotes a string for use in a regular expression, i.e. escapes all characters with special meaning.
*/
quote(string: string): string {
if (string === undefined || string === null) {
return string;
}
string = strings.asString(string);
// see "escapeRegExp()" from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Using_special_characters
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& = last match
},
/**
* If the given input is not of type string, it is converted to a string (using the standard
* JavaScript "String()" function). Inputs 'null' and 'undefined' are returned as they are.
*/
asString(input: any): string {
if (input === undefined || input === null) {
return input;
}
if (typeof input === 'string' || input instanceof String) {
return input as string;
}
return String(input);
},
/**
* This is a shortcut for <code>scout.nvl(string, '')</code>.
* @param string String to check
* @returns Empty string '' when given string is null or undefined.
*/
nvl(string: string): string {
if (arguments.length > 1) {
throw new Error('strings.nvl only accepts one argument. Use scout.nvl if you need to handle multiple arguments');
}
return scout.nvl(string, '');
},
/**
* Null-safe version of <code>String.prototype.length</code>.
* If the argument is null or undefined, 0 will be returned.
* A non-string argument will be converted to a string.
*/
length(string: string): number {
string = strings.asString(string);
return (string ? string.length : 0);
},
/**
* Null-safe version of <code>String.prototype.trim</code>.
* If the argument is null or undefined, the same value will be returned.
* A non-string argument will be converted to a string.
*/
trim(string: string): string {
string = strings.asString(string);
return (string ? string.trim() : string);
},
/**
* Null-safe version of <code>String.prototype.toUpperCase</code>.
* If the argument is null or undefined, the same value will be returned.
* A non-string argument will be converted to a string.
*/
toUpperCase(string: string): string {
string = strings.asString(string);
return (string ? string.toUpperCase() : string);
},
/**
* Null-safe version of <code>String.prototype.toLowerCase</code>.
* If the argument is null or undefined, the same value will be returned.
* A non-string argument will be converted to a string.
*/
toLowerCase(string: string): string {
string = strings.asString(string);
return (string ? string.toLowerCase() : string);
},
/**
* Returns the given string, with the first character converted to upper case and the remainder unchanged.
* If the argument is null or undefined, the same value will be returned.
* A non-string argument will be converted to a string.
*/
toUpperCaseFirstLetter(string: string): string {
string = strings.asString(string);
if (!string) {
return string;
}
return string.substring(0, 1).toUpperCase() + string.substring(1);
},
/**
* Returns the given string, with the first character converted to lower case and the remainder unchanged.
* If the argument is null or undefined, the same value will be returned.
* A non-string argument will be converted to a string.
*/
toLowerCaseFirstLetter(string: string): string {
string = strings.asString(string);
if (!string) {
return string;
}
return string.substring(0, 1).toLowerCase() + string.substring(1);
},
/**
* Returns the number of unicode characters in the given string.
* As opposed to the string.length property, astral symbols are
* counted as one single character.
*
* Example: <code>'\uD83D\uDC4D'.length</code> returns 2, whereas
* <code>countCodePoints('\uD83D\uDC4D')</code> returns 1.
*
* (\uD83D\uDC4D = unicode character U+1F44D 'THUMBS UP SIGN')
*/
countCodePoints(string: string): number {
return string
// Replace every surrogate pair with a BMP symbol.
.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_')
// and then get the length.
.length;
},
/**
* Splits the given 'string' at 'separator' while returning at most 'limit' elements.
* Unlike String.prototype.split(), this function does not discard elements if more than
* 'limit' elements are found. Instead, the surplus elements are joined with the last element.
*
* Example:
* <ul>
* <li>'a-b-c'.split('-', 2) ==> ['a', 'b']
* <li>splitMax('a-b-c', '-', 2) ==> ['a', 'b-c']
* </ul>
*/
splitMax(string: string, separator: string, limit: number): string[] {
if (string === null || string === undefined) {
return [];
}
string = strings.asString(string);
separator = strings.asString(separator);
limit = Number(limit);
let array = string.split(separator);
if (isNaN(limit) || limit <= 0 || limit >= array.length) {
return array;
}
let arrayShort = array.slice(0, limit - 1);
let last = array.slice(limit - 1).join(separator); // combine the rest
arrayShort.push(last);
return arrayShort;
},
nullIfEmpty(string: string): string {
return strings.empty(string) ? null : string;
},
/**
* Null safe case-sensitive comparison of two strings.
*
* @param [ignoreCase] optional flag to perform case-insensitive comparison
*/
equals(a: string, b: string, ignoreCase?: boolean): boolean {
a = strings.nullIfEmpty(a);
b = strings.nullIfEmpty(b);
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
if (ignoreCase) {
return a.toLowerCase() === b.toLowerCase();
}
return a === b;
},
equalsIgnoreCase(a: string, b: string): boolean {
return strings.equals(a, b, true);
},
/**
* Adds the given prefix to the start of the given string and returns the result.
* If either of the given arguments is null or empty, the original string is returned unchanged, i.e. without prefix.
*/
addPrefix(string: string, prefix: string): string {
return string && prefix ? prefix + string : string;
},
/**
* Adds the given suffix to the end of the given string and returns the result.
* If either of the given arguments is null or empty, the original string is returned unchanged, i.e. without suffix.
*/
addSuffix(string: string, suffix: string): string {
return string && suffix ? string + suffix : string;
},
/**
* If the given string starts with the given prefix, the prefix is removed and the remaining string is returned.
* Otherwise, the string is returned unchanged. This method is case-sensitive and null-safe.
*/
removePrefix(string: string, prefix: string): string {
return strings.startsWith(string, prefix) ? string.substring(prefix.length) : string;
},
/**
* If the given string ends with the given suffix, the suffix is removed and the remaining string is returned.
* Otherwise, the string is returned unchanged. This method is case-sensitive and null-safe.
*/
removeSuffix(string: string, suffix: string): string {
return strings.endsWith(string, suffix) ? string.substring(0, string.length - suffix.length) : string;
},
/**
* Truncates the given text and appends '...' so it fits into the given horizontal space.
*
* @param text the text to be truncated
* @param horizontalSpace the horizontal space the text needs to fit into
* @param measureText a function that measures the span of a text, it needs to return an object containing a 'width' property.
* If not provided, the width corresponds to the number of characters.
* @returns the truncated text
*/
truncateText(text: string, horizontalSpace: number, measureText?: (text: string) => { width: number }): string {
if (!text || !horizontalSpace || horizontalSpace <= 0) {
return text;
}
if (!measureText) {
measureText = text => ({width: (text || '').length});
}
text = strings.asString(text).trim();
let textWidth = measureText(text).width;
if (textWidth <= horizontalSpace) {
return text;
}
let upperBound = text.length; // exclusive
let lowerBound = 0; // inclusive
while (lowerBound + 1 < upperBound) {
let textLength = Math.round((upperBound + lowerBound) / 2);
if (measureText(text.slice(0, textLength) + '...').width > horizontalSpace) {
upperBound = textLength;
} else {
lowerBound = textLength;
}
}
return text.slice(0, lowerBound).trim() + '...';
},
/**
* @returns `true` or `false` if the given string is either `'true'` or `'false'` (ignoring case). Otherwise, `undefined` is returned.
*/
parseBoolean(value: string): boolean {
value = value?.toLowerCase();
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
return undefined;
}
};