From 01ec640bdb88cf07167bfd43f3dbd202381153f5 Mon Sep 17 00:00:00 2001 From: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:16:54 -0800 Subject: [PATCH] Chore: Refactor string-utils to TypeScript (#9869) --- .eslintignore | 2 + .gitignore | 2 + packages/lib/StringUtils.test.js | 86 ------------------- packages/lib/string-utils-common.js | 4 + packages/lib/string-utils.test.ts | 65 ++++++++++++++ .../lib/{string-utils.js => string-utils.ts} | 50 ++++++----- 6 files changed, 103 insertions(+), 106 deletions(-) delete mode 100644 packages/lib/StringUtils.test.js create mode 100644 packages/lib/string-utils.test.ts rename packages/lib/{string-utils.js => string-utils.ts} (89%) diff --git a/.eslintignore b/.eslintignore index e9834718c..60a314c6c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -984,6 +984,8 @@ packages/lib/services/synchronizer/utils/syncDeleteStep.js packages/lib/services/synchronizer/utils/types.js packages/lib/shim-init-node.js packages/lib/shim.js +packages/lib/string-utils.test.js +packages/lib/string-utils.js packages/lib/testing/syncTargetUtils.js packages/lib/testing/test-utils-synchronizer.js packages/lib/testing/test-utils.js diff --git a/.gitignore b/.gitignore index abaa3b592..6f4b4500b 100644 --- a/.gitignore +++ b/.gitignore @@ -964,6 +964,8 @@ packages/lib/services/synchronizer/utils/syncDeleteStep.js packages/lib/services/synchronizer/utils/types.js packages/lib/shim-init-node.js packages/lib/shim.js +packages/lib/string-utils.test.js +packages/lib/string-utils.js packages/lib/testing/syncTargetUtils.js packages/lib/testing/test-utils-synchronizer.js packages/lib/testing/test-utils.js diff --git a/packages/lib/StringUtils.test.js b/packages/lib/StringUtils.test.js deleted file mode 100644 index 6751a33e8..000000000 --- a/packages/lib/StringUtils.test.js +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable no-unused-vars */ - -const { splitCommandBatch } = require('./string-utils'); -const StringUtils = require('./string-utils'); - -describe('StringUtils', () => { - - - - it('should surround keywords with strings', (async () => { - const testCases = [ - [[], 'test', 'a', 'b', null, 'test'], - [['test'], 'test', 'a', 'b', null, 'atestb'], - [['test'], 'Test', 'a', 'b', null, 'aTestb'], - [['te[]st'], 'Te[]st', 'a', 'b', null, 'aTe[]stb'], - // [['test1', 'test2'], 'bla test1 blabla test1 bla test2 not this one - test22', 'a', 'b', 'bla atest1b blabla atest1b bla atest2b not this one - test22'], - [['test1', 'test2'], 'bla test1 test1 bla test2', '', '', null, 'bla test1 test1 bla test2'], - // [[{ type:'regex', value:'test.*?'}], 'bla test1 test1 bla test2 test tttest', 'a', 'b', 'bla atest1b atest1b bla atest2b atestb tttest'], - [['test'], 'testTest', 'a', 'b', { escapeHtml: true }, 'atestbaTestb'], - [['test'], 'test test Test', 'a', 'b', { escapeHtml: true }, 'atestb atestb aTestb'], - [['d'], 'dfasdf', '[', ']', { escapeHtml: true }, '[d]fas[d]f'], - [[{ scriptType: 'en', type: 'regex', value: 'd*', valueRegex: 'd[^ \t\n\r,\\.,\\+\\-\\*\\?\\!\\=\\{\\}\\<\\>\\|\\:"\'\\(\\)\\[\\]]*?' }], 'dfasdf', '[', ']', { escapeHtml: true }, '[d]fas[d]f'], - [['zzz'], 'zzz', 'a', 'b', { escapeHtml: true }, 'azzzb<img src=q onerror=eval("require('child_process').exec('mate-calc');");>'], - ]; - - for (let i = 0; i < testCases.length; i++) { - const t = testCases[i]; - - const keywords = t[0]; - const input = t[1]; - const prefix = t[2]; - const suffix = t[3]; - const options = t[4]; - const expected = t[5]; - - const actual = StringUtils.surroundKeywords(keywords, input, prefix, suffix, options); - - expect(actual).toBe(expected, `Test case ${i}`); - } - })); - - it('should find the next whitespace character', (async () => { - const testCases = [ - ['', [[0, 0]]], - ['Joplin', [[0, 6], [3, 6], [6, 6]]], - ['Joplin is a free, open source\n note taking and *to-do* application', [[0, 6], [12, 17], [23, 29], [48, 54]]], - ]; - - // eslint-disable-next-line github/array-foreach -- Old code before rule was applied - testCases.forEach((t, i) => { - const str = t[0]; - // eslint-disable-next-line github/array-foreach -- Old code before rule was applied - t[1].forEach((pair, j) => { - const begin = pair[0]; - const expected = pair[1]; - - const actual = StringUtils.nextWhitespaceIndex(str, begin); - expect(actual).toBe(expected, `Test string ${i} - case ${j}`); - }); - }); - })); - - it('should split the command batch by newlines not inside quotes', (async () => { - const eol = '\n'; - const testCases = [ - ['', - ['']], - ['command1', - ['command1']], - ['command1 arg1 arg2 arg3', - ['command1 arg1 arg2 arg3']], - [`command1 arg1 'arg2${eol}continue' arg3`, - [`command1 arg1 'arg2${eol}continue' arg3`]], - [`command1 arg1 'arg2${eol}continue'${eol}command2${eol}command3 'arg1${eol}continue${eol}continue' arg2 arg3`, - [`command1 arg1 'arg2${eol}continue'`, 'command2', `command3 'arg1${eol}continue${eol}continue' arg2 arg3`]], - [`command1 arg\\1 'arg2${eol}continue\\'continue' arg3`, - [`command1 arg\\1 'arg2${eol}continue\\'continue' arg3`]], - ]; - - // eslint-disable-next-line github/array-foreach -- Old code before rule was applied - testCases.forEach((t) => { - expect(splitCommandBatch(t[0])).toEqual(t[1]); - }); - })); - -}); diff --git a/packages/lib/string-utils-common.js b/packages/lib/string-utils-common.js index cff1117d4..48319de6e 100644 --- a/packages/lib/string-utils-common.js +++ b/packages/lib/string-utils-common.js @@ -1,3 +1,7 @@ +// Leave this file as JavaScript -- our current TypeScript configuration +// generates code that tries to access modules/exports, which is incompatible +// with browser environments. + function pregQuote(str, delimiter = '') { return (`${str}`).replace(new RegExp(`[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\${delimiter || ''}-]`, 'g'), '\\$&'); } diff --git a/packages/lib/string-utils.test.ts b/packages/lib/string-utils.test.ts new file mode 100644 index 000000000..ddded4ad7 --- /dev/null +++ b/packages/lib/string-utils.test.ts @@ -0,0 +1,65 @@ +import { splitCommandBatch } from './string-utils'; +import * as StringUtils from './string-utils'; + +describe('string-utils', () => { + + test.each([ + [[], 'test', 'a', 'b', null, 'test'], + [['test'], 'test', 'a', 'b', null, 'atestb'], + [['test'], 'Test', 'a', 'b', null, 'aTestb'], + [['te[]st'], 'Te[]st', 'a', 'b', null, 'aTe[]stb'], + // [['test1', 'test2'], 'bla test1 blabla test1 bla test2 not this one - test22', 'a', 'b', 'bla atest1b blabla atest1b bla atest2b not this one - test22'], + [['test1', 'test2'], 'bla test1 test1 bla test2', '', '', null, 'bla test1 test1 bla test2'], + // [[{ type:'regex', value:'test.*?'}], 'bla test1 test1 bla test2 test tttest', 'a', 'b', 'bla atest1b atest1b bla atest2b atestb tttest'], + [['test'], 'testTest', 'a', 'b', { escapeHtml: true }, 'atestbaTestb'], + [['test'], 'test test Test', 'a', 'b', { escapeHtml: true }, 'atestb atestb aTestb'], + [['d'], 'dfasdf', '[', ']', { escapeHtml: true }, '[d]fas[d]f'], + [ + [{ + scriptType: 'en', + type: 'regex', + value: 'd*', + valueRegex: 'd[^ \t\n\r,\\.,\\+\\-\\*\\?\\!\\=\\{\\}\\<\\>\\|\\:"\'\\(\\)\\[\\]]*?', + } as StringUtils.KeywordObjectType], + 'dfasdf', '[', ']', { escapeHtml: true }, '[d]fas[d]f', + ], + [['zzz'], 'zzz', 'a', 'b', { escapeHtml: true }, 'azzzb<img src=q onerror=eval("require('child_process').exec('mate-calc');");>'], + ])('should surround keywords with strings (case %#)', (async (keywords, input, prefix, suffix, options, expected) => { + const actual = StringUtils.surroundKeywords(keywords, input, prefix, suffix, options); + + expect(actual).toBe(expected); + })); + + test.each([ + ['', [[0, 0]]], + ['Joplin', [[0, 6], [3, 6], [6, 6]]], + ['Joplin is a free, open source\n note taking and *to-do* application', [[0, 6], [12, 17], [23, 29], [48, 54]]], + ])('should find the next whitespace character in string %s', (async (str, testCases) => { + for (const range of testCases) { + const begin = range[0]; + const expected = range[1]; + + const actual = StringUtils.nextWhitespaceIndex(str, begin); + expect(actual).toBe(expected); + } + })); + + const eol = '\n'; + test.each([ + ['', + ['']], + ['command1', + ['command1']], + ['command1 arg1 arg2 arg3', + ['command1 arg1 arg2 arg3']], + [`command1 arg1 'arg2${eol}continue' arg3`, + [`command1 arg1 'arg2${eol}continue' arg3`]], + [`command1 arg1 'arg2${eol}continue'${eol}command2${eol}command3 'arg1${eol}continue${eol}continue' arg2 arg3`, + [`command1 arg1 'arg2${eol}continue'`, 'command2', `command3 'arg1${eol}continue${eol}continue' arg2 arg3`]], + [`command1 arg\\1 'arg2${eol}continue\\'continue' arg3`, + [`command1 arg\\1 'arg2${eol}continue\\'continue' arg3`]], + ])('should split the command batch by newlines not inside quotes (case %#)', (async (batch, expected) => { + expect(splitCommandBatch(batch)).toEqual(expected); + })); + +}); diff --git a/packages/lib/string-utils.js b/packages/lib/string-utils.ts similarity index 89% rename from packages/lib/string-utils.js rename to packages/lib/string-utils.ts index 2f3877eca..e480d19fa 100644 --- a/packages/lib/string-utils.js +++ b/packages/lib/string-utils.ts @@ -2,6 +2,9 @@ const Entities = require('html-entities').AllHtmlEntities; const htmlentities = new Entities().encode; const stringUtilsCommon = require('./string-utils-common.js'); +export const pregQuote = stringUtilsCommon.pregQuote; +export const replaceRegexDiacritics = stringUtilsCommon.replaceRegexDiacritics; + const defaultDiacriticsRemovalMap = [ { base: 'A', letters: /[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g }, { base: 'AA', letters: /[\uA732]/g }, @@ -89,7 +92,7 @@ const defaultDiacriticsRemovalMap = [ { base: 'z', letters: /[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g }, ]; -function removeDiacritics(str) { +export function removeDiacritics(str: string) { for (let i = 0; i < defaultDiacriticsRemovalMap.length; i++) { str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base); } @@ -97,7 +100,7 @@ function removeDiacritics(str) { return str; } -function escapeFilename(s, maxLength = 32) { +export function escapeFilename(s: string, maxLength = 32) { let output = removeDiacritics(s); output = output.replace('\n\r', ' '); output = output.replace('\r\n', ' '); @@ -116,7 +119,7 @@ function escapeFilename(s, maxLength = 32) { return output.substr(0, maxLength); } -function wrap(text, indent, width) { +export function wrap(text: string, indent: string, width: number) { const wrap_ = require('word-wrap'); return wrap_(text, { @@ -125,7 +128,7 @@ function wrap(text, indent, width) { }); } -function commandArgumentsToString(args) { +export function commandArgumentsToString(args: string[]) { const output = []; for (let i = 0; i < args.length; i++) { let arg = args[i]; @@ -138,7 +141,7 @@ function commandArgumentsToString(args) { return output.join(' '); } -function splitCommandBatch(commandBatch) { +export function splitCommandBatch(commandBatch: string) { const commandLines = []; const eol = '\n'; @@ -191,7 +194,7 @@ function splitCommandBatch(commandBatch) { return commandLines; } -function padLeft(string, length, padString) { +export function padLeft(string: string, length: number, padString: string) { if (!string) return ''; while (string.length < length) { @@ -201,16 +204,16 @@ function padLeft(string, length, padString) { return string; } -function toTitleCase(string) { +export function toTitleCase(string: string) { if (!string) return string; return string.charAt(0).toUpperCase() + string.slice(1); } -function urlDecode(string) { +export function urlDecode(string: string) { return decodeURIComponent((`${string}`).replace(/\+/g, '%20')); } -function escapeHtml(s) { +export function escapeHtml(s: string) { return s .replace(/&/g, '&') .replace(/ { - if (k.type === 'regex') { - return stringUtilsCommon.replaceRegexDiacritics(k.valueRegex); + let regex; + if (typeof k === 'string' || k.type === 'string') { + regex = stringUtilsCommon.pregQuote(typeof k === 'string' ? k : k.value); } else { - const value = typeof k === 'string' ? k : k.value; - return stringUtilsCommon.replaceRegexDiacritics(stringUtilsCommon.pregQuote(value)); + regex = k.valueRegex; } + return stringUtilsCommon.replaceRegexDiacritics(regex); }) .join('|'); regexString = `(${regexString})`; @@ -244,18 +255,18 @@ function surroundKeywords(keywords, text, prefix, suffix, options = null) { return text.replace(re, `${prefix}$1${suffix}`); } -function substrWithEllipsis(s, start, length) { +export function substrWithEllipsis(s: string, start: number, length: number) { if (s.length <= length) return s; return `${s.substr(start, length - 3)}...`; } -function nextWhitespaceIndex(s, begin) { +export function nextWhitespaceIndex(s: string, begin: number) { // returns index of the next whitespace character const i = s.slice(begin).search(/\s/); return i < 0 ? s.length : begin + i; } -function camelCaseToDash(s) { +export function camelCaseToDash(s: string) { const output = []; for (let i = 0; i < s.length; i++) { const c = s[i]; @@ -270,7 +281,7 @@ function camelCaseToDash(s) { return output.join(''); } -function formatCssSize(v) { +export function formatCssSize(v: string) { if (typeof v === 'string') { if (v.includes('px') || v.includes('em') || v.includes('%')) return v; } @@ -282,7 +293,7 @@ const REGEX_CHINESE = /[\u4e00-\u9fff]|[\u3400-\u4dbf]|[\u{20000}-\u{2a6df}]|[\u const REGEX_KOREAN = /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/; const REGEX_THAI = /[\u0e00-\u0e7f]/; -function scriptType(s) { +export function scriptType(s: string) { // A string entirely with Chinese character will be detected as Japanese too // so Chinese detection must go first. if (REGEX_CHINESE.test(s)) return 'zh'; @@ -292,4 +303,3 @@ function scriptType(s) { return 'en'; } -module.exports = { formatCssSize, camelCaseToDash, removeDiacritics, substrWithEllipsis, nextWhitespaceIndex, escapeFilename, wrap, splitCommandBatch, padLeft, toTitleCase, urlDecode, escapeHtml, surroundKeywords, scriptType, commandArgumentsToString, ...stringUtilsCommon };