/*
* Copyright (c) 2010, 2026 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, '
').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. If the text is falsy, the input value is returned.
*
* Example: 'Foo<br>Bar' returns 'Foo<br>Bar'.
*
* @param text 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.
* 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 scout.nvl(string, '').
* @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 String.prototype.length.
* 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 String.prototype.trim.
* 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 String.prototype.toUpperCase.
* 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 String.prototype.toLowerCase.
* 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: '\uD83D\uDC4D'.length returns 2, whereas
* countCodePoints('\uD83D\uDC4D') 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:
*