diff --git a/tgui/packages/common/string.babel-plugin.cjs b/tgui/packages/common/string.babel-plugin.cjs
deleted file mode 100644
index 97ca67c6ea4c..000000000000
--- a/tgui/packages/common/string.babel-plugin.cjs
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * This plugin saves overall about 10KB on the final bundle size, so it's
- * sort of worth it.
- *
- * We are using a .cjs extension because:
- *
- * 1. Webpack CLI only supports CommonJS modules;
- * 2. tgui-dev-server supports both, but we still need to signal NodeJS
- * to import it as a CommonJS module, hence .cjs extension.
- *
- * We need to copy-paste the whole "multiline" function because we can't
- * synchronously import an ES module from a CommonJS module.
- *
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-/**
- * Removes excess whitespace and indentation from the string.
- */
-const multiline = (str) => {
- const lines = str.split('\n');
- // Determine base indentation
- let minIndent;
- for (let line of lines) {
- for (let indent = 0; indent < line.length; indent++) {
- const char = line[indent];
- if (char !== ' ') {
- if (minIndent === undefined || indent < minIndent) {
- minIndent = indent;
- }
- break;
- }
- }
- }
- if (!minIndent) {
- minIndent = 0;
- }
- // Remove this base indentation and trim the resulting string
- // from both ends.
- return lines
- .map((line) => line.substr(minIndent).trimRight())
- .join('\n')
- .trim();
-};
-
-const StringPlugin = (ref) => {
- return {
- visitor: {
- TaggedTemplateExpression: (path) => {
- if (path.node.tag.name === 'multiline') {
- const { quasi } = path.node;
- if (quasi.expressions.length > 0) {
- throw new Error('Multiline tag does not support expressions!');
- }
- if (quasi.quasis.length > 1) {
- throw new Error('Quasis is longer than 1');
- }
- const { value } = quasi.quasis[0];
- value.raw = multiline(value.raw);
- value.cooked = multiline(value.cooked);
- path.replaceWith(quasi);
- }
- },
- },
- };
-};
-
-module.exports = {
- __esModule: true,
- default: StringPlugin,
-};
diff --git a/tgui/packages/common/string.js b/tgui/packages/common/string.js
deleted file mode 100644
index 5906d83cb84f..000000000000
--- a/tgui/packages/common/string.js
+++ /dev/null
@@ -1,196 +0,0 @@
-/**
- * @file
- * @copyright 2020 Aleksej Komarov
- * @license MIT
- */
-
-/**
- * Removes excess whitespace and indentation from the string.
- */
-export const multiline = (str) => {
- if (Array.isArray(str)) {
- // Small stub to allow usage as a template tag
- return multiline(str.join(''));
- }
- const lines = str.split('\n');
- // Determine base indentation
- let minIndent;
- for (let line of lines) {
- for (let indent = 0; indent < line.length; indent++) {
- const char = line[indent];
- if (char !== ' ') {
- if (minIndent === undefined || indent < minIndent) {
- minIndent = indent;
- }
- break;
- }
- }
- }
- if (!minIndent) {
- minIndent = 0;
- }
- // Remove this base indentation and trim the resulting string
- // from both ends.
- return lines
- .map((line) => line.substr(minIndent).trimRight())
- .join('\n')
- .trim();
-};
-
-/**
- * Creates a glob pattern matcher.
- *
- * Matches strings with wildcards.
- *
- * Example: createGlobPattern('*@domain')('user@domain') === true
- */
-export const createGlobPattern = (pattern) => {
- const escapeString = (str) => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
- // prettier-ignore
- const regex = new RegExp('^'
- + pattern.split(/\*+/).map(escapeString).join('.*')
- + '$');
- return (str) => regex.test(str);
-};
-
-/**
- * Creates a search terms matcher.
- *
- * Returns true if given string matches the search text.
- *
- * @template T
- * @param {string} searchText
- * @param {(obj: T) => string} stringifier
- * @returns {(obj: T) => boolean}
- */
-export const createSearch = (searchText, stringifier) => {
- const preparedSearchText = searchText.toLowerCase().trim();
- return (obj) => {
- if (!preparedSearchText) {
- return true;
- }
- const str = stringifier ? stringifier(obj) : obj;
- if (!str) {
- return false;
- }
- return str.toLowerCase().includes(preparedSearchText);
- };
-};
-
-/**
- * Capitalizes a word and lowercases the rest.
- * @param {string} str
- * @returns {string} capitalized string
- *
- * @example capitalize('heLLo') === 'Hello'
- */
-export const capitalize = (str) => {
- // Handle array
- if (Array.isArray(str)) {
- return str.map(capitalize);
- }
- // Handle string
- return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
-};
-
-/**
- * Similar to capitalize, this takes a string and replaces all first letters
- * of any words.
- *
- * @param {string} str
- * @return {string} The string with the first letters capitalized.
- *
- * @example capitalizeAll('heLLo woRLd') === 'HeLLo WoRLd'
- */
-export const capitalizeAll = (str) => {
- return str.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
-};
-
-/**
- * Capitalizes only the first letter of the str.
- *
- * @param {string} str
- * @return {string} capitalized string
- *
- * @example capitalizeFirst('heLLo woRLd') === 'HeLLo woRLd'
- */
-export const capitalizeFirst = (str) => {
- return str.replace(/^\w/, (letter) => letter.toUpperCase());
-};
-
-export const toTitleCase = (str) => {
- // Handle array
- if (Array.isArray(str)) {
- return str.map(toTitleCase);
- }
- // Pass non-string
- if (typeof str !== 'string') {
- return str;
- }
- // Handle string
- const WORDS_UPPER = ['Id', 'Tv'];
- // prettier-ignore
- const WORDS_LOWER = [
- 'A', 'An', 'And', 'As', 'At', 'But', 'By', 'For', 'For', 'From', 'In',
- 'Into', 'Near', 'Nor', 'Of', 'On', 'Onto', 'Or', 'The', 'To', 'With',
- ];
- let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, (str) => {
- return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
- });
- for (let word of WORDS_LOWER) {
- const regex = new RegExp('\\s' + word + '\\s', 'g');
- currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
- }
- for (let word of WORDS_UPPER) {
- const regex = new RegExp('\\b' + word + '\\b', 'g');
- currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
- }
- return currentStr;
-};
-
-/**
- * Decodes HTML entities, and removes unnecessary HTML tags.
- *
- * @param {String} str Encoded HTML string
- * @return {String} Decoded HTML string
- */
-export const decodeHtmlEntities = (str) => {
- if (!str) {
- return str;
- }
- const translate_re = /&(nbsp|amp|quot|lt|gt|apos);/g;
- const translate = {
- nbsp: ' ',
- amp: '&',
- quot: '"',
- lt: '<',
- gt: '>',
- apos: "'",
- };
- // prettier-ignore
- return str
- // Newline tags
- .replace(/
/gi, '\n')
- .replace(/<\/?[a-z0-9-_]+[^>]*>/gi, '')
- // Basic entities
- .replace(translate_re, (match, entity) => translate[entity])
- // Decimal entities
- .replace(/?([0-9]+);/gi, (match, numStr) => {
- const num = parseInt(numStr, 10);
- return String.fromCharCode(num);
- })
- // Hex entities
- .replace(/?([0-9a-f]+);/gi, (match, numStr) => {
- const num = parseInt(numStr, 16);
- return String.fromCharCode(num);
- });
-};
-
-/**
- * Converts an object into a query string,
- */
-// prettier-ignore
-export const buildQueryString = obj => Object.keys(obj)
- .map(key => encodeURIComponent(key)
- + '=' + encodeURIComponent(obj[key]))
- .join('&');
diff --git a/tgui/packages/common/string.test.ts b/tgui/packages/common/string.test.ts
new file mode 100644
index 000000000000..06b24da80136
--- /dev/null
+++ b/tgui/packages/common/string.test.ts
@@ -0,0 +1,35 @@
+import { createSearch, decodeHtmlEntities, toTitleCase } from './string';
+
+describe('createSearch', () => {
+ it('matches search terms correctly', () => {
+ const search = createSearch('test', (obj: { value: string }) => obj.value);
+
+ const obj1 = { value: 'This is a test string.' };
+ const obj2 = { value: 'This is a different string.' };
+ const obj3 = { value: 'This is a test string.' };
+
+ const objects = [obj1, obj2, obj3];
+
+ expect(objects.filter(search)).toEqual([obj1, obj3]);
+ });
+});
+
+describe('toTitleCase', () => {
+ it('converts strings to title case correctly', () => {
+ expect(toTitleCase('hello world')).toBe('Hello World');
+ expect(toTitleCase('HELLO WORLD')).toBe('Hello World');
+ expect(toTitleCase('HeLLo wORLd')).toBe('Hello World');
+ expect(toTitleCase('a tale of two cities')).toBe('A Tale of Two Cities');
+ expect(toTitleCase('war and peace')).toBe('War and Peace');
+ });
+});
+
+describe('decodeHtmlEntities', () => {
+ it('decodes HTML entities and removes unnecessary HTML tags correctly', () => {
+ expect(decodeHtmlEntities('
')).toBe('\n');
+ expect(decodeHtmlEntities('
Hello World
')).toBe('Hello World'); + expect(decodeHtmlEntities('&')).toBe('&'); + expect(decodeHtmlEntities('&')).toBe('&'); + expect(decodeHtmlEntities('&')).toBe('&'); + }); +}); diff --git a/tgui/packages/common/string.ts b/tgui/packages/common/string.ts new file mode 100644 index 000000000000..4c93d155e662 --- /dev/null +++ b/tgui/packages/common/string.ts @@ -0,0 +1,174 @@ +/* eslint-disable func-style */ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +/** + * Creates a search terms matcher. Returns true if given string matches the search text. + * + * @example + * ```tsx + * type Thing = { id: string; name: string }; + * + * const objects = [ + * { id: '123', name: 'Test' }, + * { id: '456', name: 'Test' }, + * ]; + * + * const search = createSearch('123', (obj: Thing) => obj.id); + * + * objects.filter(search); // returns [{ id: '123', name: 'Test' }] + * ``` + */ +export function createSearch