Source rapidcontext/text.mjs

1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2026 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15/**
16 * Provides functions for basic text processing.
17 * @namespace RapidContext.Text
18 */
19
20/**
21 * Converts a value to a string, similar to `String()`, but handles `null`
22 * and `undefined` values.
23 *
24 * @param {*} val the value to convert
25 * @return {string} the string representation
26 * @name str
27 * @memberof RapidContext.Text
28 *
29 * @example
30 * str(null) //==> ''
31 * str(undefined) //==> ''
32 * str(123) //==> '123'
33 */
34export function str(val) {
35    return (val === null || val === undefined) ? '' : String(val);
36}
37
38/**
39 * Converts a string to lower case, similar to `String.toLowerCase()`, but
40 * handles `null` and `undefined` values.
41 *
42 * @param {*} val the value to convert
43 * @return {string} the lower case string
44 * @name lower
45 * @memberof RapidContext.Text
46 *
47 * @example
48 * lower('AbC') //==> 'abc'
49 */
50export function lower(val) {
51    return str(val).toLowerCase();
52}
53
54/**
55 * Converts a string to upper case, similar to `String.toUpperCase()`, but
56 * handles `null` and `undefined` values.
57 *
58 * @param {*} val the value to convert
59 * @return {string} the upper case string
60 * @name upper
61 * @memberof RapidContext.Text
62 *
63 * @example
64 * upper('Abc') //==> 'ABC'
65 */
66export function upper(val) {
67    return str(val).toUpperCase();
68}
69
70/**
71 * Converts the first character of a string to lower case.
72 *
73 * @param {*} val the value to convert
74 * @return {string} the converted string
75 * @name lowerFirst
76 * @memberof RapidContext.Text
77 *
78 * @example
79 * lowerFirst('ABC') //==> 'aBC'
80 */
81export function lowerFirst(val) {
82    const s = str(val);
83    return s.charAt(0).toLowerCase() + s.slice(1);
84}
85
86/**
87 * Converts the first character of a string to upper case.
88 *
89 * @param {*} val the value to convert
90 * @return {string} the converted string
91 * @name upperFirst
92 * @memberof RapidContext.Text
93 *
94 * @example
95 * upperFirst('abc') //==> 'Abc'
96 */
97export function upperFirst(val) {
98    const s = str(val);
99    return s.charAt(0).toUpperCase() + s.slice(1);
100}
101
102/**
103 * Capitalizes the first character of a string and converts the rest to lower
104 * case.
105 *
106 * @param {*} val the value to convert
107 * @return {string} the capitalized string
108 * @name capitalize
109 * @memberof RapidContext.Text
110 *
111 * @example
112 * capitalize('aBC') //==> 'Abc'
113 */
114export function capitalize(val) {
115    const s = str(val);
116    return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
117}
118
119/**
120 * Splits a string into an array of words. It handles whitespace, punctuation,
121 * and CamelCase boundaries.
122 *
123 * @param {*} val the value to split
124 * @return {Array} the array of words
125 * @name words
126 * @memberof RapidContext.Text
127 *
128 * @example
129 * words('hello world') //==> ['hello', 'world']
130 * words('helloWorld') //==> ['hello', 'World']
131 * words('XMLHttpRequest') //==> ['XML', 'Http', 'Request']
132 */
133export function words(val) {
134    return str(val)
135        .trim()
136        .replaceAll(/(\p{Ll})(\p{Lu})/gu, '$1 $2')
137        .replaceAll(/(\p{Lu})(\p{Lu}\p{Ll})/gu, '$1 $2')
138        .split(/[\s\p{P}]+/gu)
139        .filter(Boolean);
140}
141
142/**
143 * Converts a string to space separated lower case words.
144 *
145 * @param {*} val the value to convert
146 * @return {string} the lower case string
147 * @name lowerCase
148 * @memberof RapidContext.Text
149 *
150 * @example
151 * lowerCase('foo bar') //==> 'foo bar'
152 * lowerCase('--foo-bar--') //==> 'foo bar'
153 * lowerCase('__foo_bar__') //==> 'foo bar'
154 */
155export function lowerCase(val) {
156    return words(val).map(lower).join(' ');
157}
158
159/**
160 * Converts a string to space separated upper case words.
161 *
162 * @param {*} val the value to convert
163 * @return {string} the upper case string
164 * @name upperCase
165 * @memberof RapidContext.Text
166 *
167 * @example
168 * upperCase('foo bar') //==> 'FOO BAR'
169 * upperCase('--foo-bar--') //==> 'FOO BAR'
170 * upperCase('__foo_bar__') //==> 'FOO BAR'
171 */
172export function upperCase(val) {
173    return words(val).map(upper).join(' ');
174}
175
176/**
177 * Converts a string to camel-case, i.e. each word (except the first) is
178 * capitalized and connected without spaces.
179 *
180 * @param {*} val the value to convert
181 * @return {string} the camel-case string
182 * @name camelCase
183 * @memberof RapidContext.Text
184 *
185 * @example
186 * camelCase('Foo Bar') //==> 'fooBar'
187 * camelCase('--foo-bar--') //==> 'fooBar'
188 * camelCase('__FOO_BAR__') //==> 'fooBar'
189 */
190export function camelCase(val) {
191    return words(val).map((s, idx) => (idx === 0) ? lower(s) : capitalize(s)).join('');
192}
193
194/**
195 * Converts a string to kebab-case, i.e. each word is connected with a
196 * hyphen.
197 *
198 * @param {*} val the value to convert
199 * @return {string} the kebab-case string
200 * @name kebabCase
201 * @memberof RapidContext.Text
202 *
203 * @example
204 * kebabCase('foo bar') //==> 'foo-bar'
205 * kebabCase('--foo-bar--') //==> 'foo-bar'
206 * kebabCase('__foo_bar__') //==> 'foo-bar'
207 */
208export function kebabCase(val) {
209    return words(val).map(lower).join('-');
210}
211
212/**
213 * Converts a string to snake_case, i.e. each word is connected with an
214 * underscore.
215 *
216 * @param {*} val the value to convert
217 * @return {string} the snake_case string
218 * @name snakeCase
219 * @memberof RapidContext.Text
220 *
221 * @example
222 * snakeCase('foo bar') //==> 'foo_bar'
223 * snakeCase('--foo-bar--') //==> 'foo_bar'
224 * snakeCase('__foo_bar__') //==> 'foo_bar'
225 */
226export function snakeCase(val) {
227    return words(val).map(lower).join('_');
228}
229
230/**
231 * Converts a string to Start Case, i.e. each word is capitalized and
232 * connected with a single space.
233 *
234 * @param {*} val the value to convert
235 * @return {string} the Start Case string
236 * @name startCase
237 * @memberof RapidContext.Text
238 *
239 * @example
240 * startCase('foo bar') //==> 'Foo Bar'
241 * startCase('--foo-bar--') //==> 'Foo Bar'
242 * startCase('__foo_bar__') //==> 'Foo Bar'
243 */
244export function startCase(val) {
245    return words(val).map(capitalize).join(' ');
246}
247
248const ESCAPE_MAP = {
249    '&': '&amp;',
250    '<': '&lt;',
251    '>': '&gt;',
252    '"': '&quot;',
253    '\'': '&apos;'
254};
255
256const UNESCAPE_MAP = {
257    '&amp;': '&',
258    '&lt;': '<',
259    '&gt;': '>',
260    '&quot;': '"',
261    '&apos;': '\''
262};
263
264/**
265 * Escapes HTML/XML special characters to their entities. Only encodes the
266 * '&', '<', '>', '"' and "'" characters.
267 *
268 * @param {*} val the value to escape
269 * @return {string} the escaped string
270 * @name escape
271 * @memberof RapidContext.Text
272 *
273 * @example
274 * escape('foo & bar') //==> 'foo &amp; bar'
275 */
276export function escape(val) {
277    const s = (val == null) ? '' : [].concat(val).join('');
278    return s.replaceAll(/[&<>"']/g, (m) => ESCAPE_MAP[m]);
279}
280
281/**
282 * Unescapes some HTML/XML entities to their original characters. Only decodes
283 * the '&', '<', '>', '"' and "'" characters, as well as numerical entities.
284 *
285 * @param {*} val the value to unescape
286 * @return {string} the unescaped string
287 * @name unescape
288 * @memberof RapidContext.Text
289 *
290 * @example
291 * unescape('foo &amp; bar') //==> 'foo & bar'
292 * unescape('&#39;') //==> "'"
293 */
294export function unescape(val) {
295    return str(val).replaceAll(/&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, (m) => {
296        if (m.startsWith('&#x')) {
297            return String.fromCharCode(parseInt(m.substring(3, m.length - 1), 16));
298        } else if (m.startsWith('&#')) {
299            return String.fromCharCode(parseInt(m.substring(2, m.length - 1), 10));
300        } else {
301            return UNESCAPE_MAP[m];
302        }
303    });
304}
305
306export default {
307    str, lower, upper, lowerFirst, upperFirst, capitalize, words, lowerCase,
308    upperCase, camelCase, kebabCase, snakeCase, startCase, escape, unescape
309};
310