@tolgee/core 4.7.0 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/tolgee.cjs.js +2 -4
  2. package/dist/tolgee.cjs.js.map +1 -1
  3. package/dist/tolgee.cjs.min.js +1 -1
  4. package/dist/tolgee.cjs.min.js.map +1 -1
  5. package/dist/{tolgee.esm.js → tolgee.esm.min.mjs} +2 -2
  6. package/dist/tolgee.esm.min.mjs.map +1 -0
  7. package/dist/tolgee.esm.mjs +5690 -0
  8. package/dist/tolgee.esm.mjs.map +1 -0
  9. package/dist/tolgee.umd.js +2 -4
  10. package/dist/tolgee.umd.js.map +1 -1
  11. package/dist/tolgee.umd.min.js +1 -1
  12. package/dist/tolgee.umd.min.js.map +1 -1
  13. package/package.json +10 -9
  14. package/src/Constants/Global.ts +9 -0
  15. package/src/Constants/ModifierKey.ts +6 -0
  16. package/src/Errors/ApiHttpError.ts +8 -0
  17. package/src/Observer.test.ts +119 -0
  18. package/src/Observer.ts +68 -0
  19. package/src/Properties.test.ts +150 -0
  20. package/src/Properties.ts +112 -0
  21. package/src/Tolgee.test.ts +473 -0
  22. package/src/Tolgee.ts +335 -0
  23. package/src/TolgeeConfig.test.ts +21 -0
  24. package/src/TolgeeConfig.ts +134 -0
  25. package/src/__integration/FormatterIcu.test.ts +80 -0
  26. package/src/__integration/FormatterMissing.ts +54 -0
  27. package/src/__integration/Tolgee.test.ts +90 -0
  28. package/src/__integration/TolgeeInvisible.test.ts +145 -0
  29. package/src/__integration/mockTranslations.ts +6 -0
  30. package/src/__integration/testConfig.ts +16 -0
  31. package/src/__testFixtures/classMock.ts +11 -0
  32. package/src/__testFixtures/createElement.ts +43 -0
  33. package/src/__testFixtures/createTestDom.ts +25 -0
  34. package/src/__testFixtures/mocked.ts +25 -0
  35. package/src/__testFixtures/setupAfterEnv.ts +34 -0
  36. package/src/helpers/NodeHelper.ts +90 -0
  37. package/src/helpers/TextHelper.test.ts +62 -0
  38. package/src/helpers/TextHelper.ts +58 -0
  39. package/src/helpers/commonTypes.ts +8 -0
  40. package/src/helpers/encoderPolyfill.ts +96 -0
  41. package/src/helpers/secret.test.ts +61 -0
  42. package/src/helpers/secret.ts +68 -0
  43. package/src/helpers/sleep.ts +2 -0
  44. package/src/highlighter/HighlightFunctionsInitializer.test.ts +40 -0
  45. package/src/highlighter/HighlightFunctionsInitializer.ts +61 -0
  46. package/src/highlighter/MouseEventHandler.test.ts +151 -0
  47. package/src/highlighter/MouseEventHandler.ts +191 -0
  48. package/src/highlighter/TranslationHighlighter.test.ts +177 -0
  49. package/src/highlighter/TranslationHighlighter.ts +113 -0
  50. package/src/index.ts +10 -0
  51. package/src/internal.ts +2 -0
  52. package/src/modules/IcuFormatter.ts +17 -0
  53. package/src/modules/index.ts +1 -0
  54. package/src/services/ApiHttpService.ts +85 -0
  55. package/src/services/CoreService.test.ts +142 -0
  56. package/src/services/CoreService.ts +76 -0
  57. package/src/services/DependencyService.test.ts +51 -0
  58. package/src/services/DependencyService.ts +116 -0
  59. package/src/services/ElementRegistrar.test.ts +131 -0
  60. package/src/services/ElementRegistrar.ts +108 -0
  61. package/src/services/EventEmitter.ts +52 -0
  62. package/src/services/EventService.ts +14 -0
  63. package/src/services/ModuleService.ts +14 -0
  64. package/src/services/ScreenshotService.ts +31 -0
  65. package/src/services/Subscription.ts +7 -0
  66. package/src/services/TextService.test.ts +88 -0
  67. package/src/services/TextService.ts +82 -0
  68. package/src/services/TranslationService.test.ts +358 -0
  69. package/src/services/TranslationService.ts +417 -0
  70. package/src/services/__mocks__/CoreService.ts +17 -0
  71. package/src/toolsManager/Messages.test.ts +79 -0
  72. package/src/toolsManager/Messages.ts +60 -0
  73. package/src/toolsManager/PluginManager.test.ts +108 -0
  74. package/src/toolsManager/PluginManager.ts +129 -0
  75. package/src/types/DTOs.ts +25 -0
  76. package/src/types/apiSchema.generated.ts +6208 -0
  77. package/src/types.ts +146 -0
  78. package/src/wrappers/AbstractWrapper.ts +14 -0
  79. package/src/wrappers/NodeHandler.ts +143 -0
  80. package/src/wrappers/WrappedHandler.ts +28 -0
  81. package/src/wrappers/invisible/AttributeHandler.ts +23 -0
  82. package/src/wrappers/invisible/Coder.ts +65 -0
  83. package/src/wrappers/invisible/ContentHandler.ts +15 -0
  84. package/src/wrappers/invisible/CoreHandler.ts +17 -0
  85. package/src/wrappers/invisible/InvisibleWrapper.ts +59 -0
  86. package/src/wrappers/invisible/ValueMemory.test.ts +25 -0
  87. package/src/wrappers/invisible/ValueMemory.ts +16 -0
  88. package/src/wrappers/text/AttributeHandler.test.ts +117 -0
  89. package/src/wrappers/text/AttributeHandler.ts +25 -0
  90. package/src/wrappers/text/Coder.test.ts +298 -0
  91. package/src/wrappers/text/Coder.ts +202 -0
  92. package/src/wrappers/text/ContentHandler.test.ts +185 -0
  93. package/src/wrappers/text/ContentHandler.ts +21 -0
  94. package/src/wrappers/text/CoreHandler.test.ts +106 -0
  95. package/src/wrappers/text/CoreHandler.ts +45 -0
  96. package/src/wrappers/text/TextWrapper.ts +69 -0
  97. package/dist/tolgee.esm.js.map +0 -1
@@ -0,0 +1,90 @@
1
+ jest.autoMockOff();
2
+
3
+ import { waitFor } from '@testing-library/dom';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Tolgee } from '..';
6
+ import mockTranslations from './mockTranslations';
7
+ import fetchMock from 'jest-fetch-mock';
8
+ import { testConfig } from './testConfig';
9
+
10
+ const API_URL = 'http://localhost';
11
+ const API_KEY = 'dummyApiKey';
12
+
13
+ const fetch = fetchMock.mockResponse(async (req) => {
14
+ if (req.url.includes('/v2/api-keys/current')) {
15
+ return JSON.stringify(testConfig);
16
+ } else if (req.url.includes('/v2/projects/translations/en')) {
17
+ return JSON.stringify(mockTranslations);
18
+ }
19
+
20
+ throw new Error('Invalid request');
21
+ });
22
+
23
+ describe('Tolgee invisble mode', () => {
24
+ let tolgee: Tolgee;
25
+ beforeEach(async () => {
26
+ fetch.enableMocks();
27
+ tolgee = Tolgee.init({
28
+ targetElement: document.body,
29
+ apiKey: API_KEY,
30
+ apiUrl: API_URL,
31
+ inputPrefix: '{{',
32
+ inputSuffix: '}}',
33
+ });
34
+ document.body.innerHTML = '';
35
+ await tolgee.run();
36
+ });
37
+
38
+ afterEach(() => {
39
+ tolgee.stop();
40
+ });
41
+
42
+ it('it translate text in body', async () => {
43
+ document.body.innerHTML = '{{hello_world}}';
44
+
45
+ await waitFor(() => {
46
+ const el = document
47
+ .evaluate(
48
+ "descendant-or-self::*[contains(text(), 'Hello world!')]",
49
+ document.body,
50
+ null,
51
+ 0
52
+ )
53
+ .iterateNext();
54
+
55
+ expect(el).toBeInTheDocument();
56
+ });
57
+ });
58
+
59
+ it('returns default when provided', async () => {
60
+ expect(
61
+ await tolgee.translate({
62
+ key: 'nonexistant',
63
+ noWrap: true,
64
+ defaultValue: 'This is default',
65
+ })
66
+ ).toEqual('This is default');
67
+ });
68
+
69
+ test('returns empty value if orEmpty is true', async () => {
70
+ expect(
71
+ await tolgee.translate({
72
+ key: 'nonexistant',
73
+ noWrap: true,
74
+ orEmpty: true,
75
+ })
76
+ ).toEqual('');
77
+ });
78
+
79
+ test('will return key when no translation found', async () => {
80
+ expect(
81
+ await tolgee.translate({ key: 'test\\.key.this\\.is\\.it', noWrap: true })
82
+ ).toEqual('test\\.key.this\\.is\\.it');
83
+ });
84
+
85
+ test('will return proper text without any dot', async () => {
86
+ expect(
87
+ await tolgee.translate({ key: 'text without any dot', noWrap: true })
88
+ ).toEqual('text without any dot');
89
+ });
90
+ });
@@ -0,0 +1,145 @@
1
+ jest.autoMockOff();
2
+
3
+ import { waitFor } from '@testing-library/dom';
4
+ import '@testing-library/jest-dom/extend-expect';
5
+ import { Tolgee } from '..';
6
+ import mockTranslations from './mockTranslations';
7
+ import fetchMock from 'jest-fetch-mock';
8
+ import { testConfig } from './testConfig';
9
+ import { decodeFromText } from '../helpers/secret';
10
+
11
+ const API_URL = 'http://localhost';
12
+ const API_KEY = 'dummyApiKey';
13
+
14
+ const fetch = fetchMock.mockResponse(async (req) => {
15
+ if (req.url.includes('/v2/api-keys/current')) {
16
+ return JSON.stringify(testConfig);
17
+ } else if (req.url.includes('/v2/projects/translations/en')) {
18
+ return JSON.stringify(mockTranslations);
19
+ }
20
+
21
+ throw new Error('Invalid request');
22
+ });
23
+
24
+ describe('Tolgee invisble mode', () => {
25
+ let tolgee: Tolgee;
26
+ beforeEach(async () => {
27
+ fetch.enableMocks();
28
+ tolgee = Tolgee.init({
29
+ targetElement: document.body,
30
+ apiKey: API_KEY,
31
+ apiUrl: API_URL,
32
+ wrapperMode: 'invisible',
33
+ });
34
+ document.body.innerHTML = '';
35
+ await tolgee.run();
36
+ });
37
+
38
+ afterEach(() => {
39
+ tolgee.stop();
40
+ });
41
+
42
+ it('handles translation in text', async () => {
43
+ const translation = await tolgee.translate('hello_world');
44
+
45
+ document.body.innerHTML = `<div id="test">${translation}</div>`;
46
+
47
+ await waitFor(() => {
48
+ return expect(
49
+ // @ts-ignore
50
+ document.getElementById('test')._tolgee
51
+ ).not.toBeUndefined();
52
+ });
53
+
54
+ expect(
55
+ decodeFromText(document.getElementById('test').textContent).length
56
+ ).toEqual(0);
57
+ expect(document.getElementById('test').textContent).toEqual('Hello world!');
58
+ });
59
+
60
+ it('handles missing translation in text', async () => {
61
+ const translation = await tolgee.translate('nonexistant');
62
+
63
+ document.body.innerHTML = `<div id="test">${translation}</div>`;
64
+
65
+ await waitFor(() => {
66
+ return expect(
67
+ // @ts-ignore
68
+ document.getElementById('test')._tolgee
69
+ ).not.toBeUndefined();
70
+ });
71
+
72
+ expect(
73
+ decodeFromText(document.getElementById('test').textContent).length
74
+ ).toEqual(0);
75
+ expect(document.getElementById('test').textContent).toEqual('nonexistant');
76
+ });
77
+
78
+ it('handles default value in text', async () => {
79
+ const translation = await tolgee.translate({
80
+ key: 'nonexistant',
81
+ defaultValue: 'Default value!',
82
+ });
83
+
84
+ document.body.innerHTML = `<div id="test">${translation}</div>`;
85
+
86
+ await waitFor(() => {
87
+ return expect(
88
+ // @ts-ignore
89
+ document.getElementById('test')._tolgee
90
+ ).not.toBeUndefined();
91
+ });
92
+
93
+ expect(
94
+ decodeFromText(document.getElementById('test').textContent).length
95
+ ).toEqual(0);
96
+ expect(document.getElementById('test').textContent).toEqual(
97
+ 'Default value!'
98
+ );
99
+ });
100
+
101
+ it('handles translation in attribute', async () => {
102
+ const translation = await tolgee.translate('hello_world');
103
+
104
+ document.body.innerHTML = `<div id="test" title="${translation}"></div>`;
105
+
106
+ await waitFor(() => {
107
+ return expect(
108
+ // @ts-ignore
109
+ document.getElementById('test')._tolgee
110
+ ).not.toBeUndefined();
111
+ });
112
+
113
+ expect(
114
+ decodeFromText(document.getElementById('test').getAttribute('title'))
115
+ .length
116
+ ).toEqual(0);
117
+ expect(document.getElementById('test').getAttribute('title')).toEqual(
118
+ 'Hello world!'
119
+ );
120
+ });
121
+
122
+ it('handles translation on select', async () => {
123
+ const translation = await tolgee.translate('hello_world');
124
+
125
+ document.body.innerHTML = `
126
+ <select id="select">
127
+ <option id="option">${translation}</option>
128
+ </select>
129
+ `;
130
+
131
+ await waitFor(() => {
132
+ return expect(
133
+ // @ts-ignore
134
+ document.getElementById('select')._tolgee
135
+ ).not.toBeUndefined();
136
+ });
137
+
138
+ expect(
139
+ decodeFromText(document.getElementById('option').textContent).length
140
+ ).toEqual(0);
141
+ expect(document.getElementById('option').textContent).toEqual(
142
+ 'Hello world!'
143
+ );
144
+ });
145
+ });
@@ -0,0 +1,6 @@
1
+ export default {
2
+ en: {
3
+ peter_dogs: 'Peter has {dogsCount} dogs.',
4
+ hello_world: 'Hello world!',
5
+ },
6
+ };
@@ -0,0 +1,16 @@
1
+ export const testConfig = {
2
+ id: 42,
3
+ key: 'asdfasdsfasdfasdfasdf',
4
+ username: 'john@toe@tolgee.com',
5
+ userFullName: 'John Doe',
6
+ projectId: 15,
7
+ projectName: 'Tolgee test',
8
+ scopes: [
9
+ 'screenshots.view',
10
+ 'screenshots.upload',
11
+ 'screenshots.delete',
12
+ 'keys.edit',
13
+ 'translations.edit',
14
+ 'translations.view',
15
+ ],
16
+ };
@@ -0,0 +1,11 @@
1
+ export default <T>(
2
+ implementationFn: () => Partial<T>,
3
+ baseConstructor?: new (...args) => any
4
+ ) => {
5
+ function theMock() {
6
+ Object.assign(this, new baseConstructor());
7
+ Object.assign(this, implementationFn());
8
+ }
9
+
10
+ return jest.fn().mockImplementation(theMock);
11
+ };
@@ -0,0 +1,43 @@
1
+ import { ElementWithMeta, NodeMeta, NodeWithMeta } from '../types';
2
+ import { TOLGEE_ATTRIBUTE_NAME } from '../Constants/Global';
3
+
4
+ export const createElement = (
5
+ nodesCount: number,
6
+ keysCount: number,
7
+ sameKeys = false
8
+ ) => {
9
+ const mockedElement = document.createElement(
10
+ 'div'
11
+ ) as Element as ElementWithMeta;
12
+
13
+ let keyNum = 0;
14
+
15
+ const cn = (text) => {
16
+ const node = document.createTextNode(text) as Node as NodeWithMeta;
17
+ const keys = [];
18
+
19
+ for (let i = 0; i < keysCount; i++) {
20
+ keys.push({
21
+ key: `key${sameKeys ? `` : ` ${keyNum++}`}`,
22
+ params: { a: 'aaa' },
23
+ defaultValue: 'default value',
24
+ });
25
+ }
26
+ node._tolgee = {
27
+ oldTextContent: `"${text}" before translation.`,
28
+ keys,
29
+ } as NodeMeta;
30
+ return node;
31
+ };
32
+
33
+ const nodes = [];
34
+ for (let i = 0; i < nodesCount; i++) {
35
+ nodes.push(cn(`text ${i}`));
36
+ }
37
+ mockedElement._tolgee = {
38
+ nodes: new Set(nodes),
39
+ };
40
+ mockedElement.append(...nodes);
41
+ mockedElement.setAttribute(TOLGEE_ATTRIBUTE_NAME, '');
42
+ return mockedElement;
43
+ };
@@ -0,0 +1,25 @@
1
+ export const createTestDom = (document: Document) => {
2
+ const c = {
3
+ keyInRoot: 'key_in_root',
4
+ keyInRootDiv: 'aaa_translate_Key.aaa',
5
+ hereKey: 'here',
6
+ hereTooKey: 'here_too',
7
+ appendedKey: 'appended_key',
8
+ ariaLabelKey: 'aria_label_key',
9
+ optionKey: 'option_key',
10
+ };
11
+
12
+ document.body = document.createElement('body');
13
+ document.body.innerHTML = `{{${c.keyInRoot}}}' +
14
+ '<div id="rootDiv">Some trash... {{${c.keyInRootDiv}}}' +
15
+ ' <div>Some other text to translate <span>{{${c.hereKey}}} and {{${c.hereTooKey}}}</span> text continues.'</div> +
16
+ ' <div id="multipleTextNodes">Before text</div>' +
17
+ ' <div aria-label="this is {{${c.ariaLabelKey}}} label"></div>' +
18
+ ' <select><option>{{${c.optionKey}}}</option></select>'+
19
+ '<div>`;
20
+ document.getElementById('multipleTextNodes').append('some text node');
21
+ document.getElementById('multipleTextNodes').append(`{{${c.appendedKey}}}`);
22
+ document.getElementById('multipleTextNodes').append(` after text`);
23
+
24
+ return c;
25
+ };
@@ -0,0 +1,25 @@
1
+ import { TolgeeConfig } from '../TolgeeConfig';
2
+ import { Properties } from '../Properties';
3
+ import { CoreService } from '../services/CoreService';
4
+ import { Observer } from '../Observer';
5
+ import { mocked } from 'ts-jest/utils';
6
+ import { TranslationService } from '../services/TranslationService';
7
+ import { TextService } from '../services/TextService';
8
+ import { EventService } from '../services/EventService';
9
+ import { EventEmitterImpl } from '../services/EventEmitter';
10
+ import { ContentHandler } from '../wrappers/text/ContentHandler';
11
+
12
+ export const configMock = mocked(TolgeeConfig);
13
+ export const propertiesMock = mocked(Properties);
14
+ export const coreServiceMock = mocked(CoreService);
15
+ export const observerMock = mocked(Observer);
16
+ export const translationServiceMock = mocked(TranslationService);
17
+ export const textServiceMock = mocked(TextService);
18
+ export const eventServiceMock = mocked(EventService);
19
+ export const eventEmitterMock = mocked(EventEmitterImpl);
20
+ export const tolgeeConfigMock = mocked(TolgeeConfig);
21
+ export const textHandlerMock = mocked(ContentHandler);
22
+
23
+ export const getMockedInstance = <T>(constructor: new (...args) => T) => {
24
+ return mocked(constructor).mock.instances[0];
25
+ };
@@ -0,0 +1,34 @@
1
+ jest.dontMock('../helpers/NodeHelper.ts');
2
+
3
+ import { NodeHelper } from '../helpers/NodeHelper';
4
+ import { prettyDOM } from '@testing-library/dom';
5
+
6
+ expect.extend({
7
+ toBeFoundIn(xpath, contextNode) {
8
+ const result = NodeHelper.evaluate(xpath, contextNode);
9
+ if (result.length > 0 && document.contains(result[0])) {
10
+ return {
11
+ message: () =>
12
+ `Xpath ${xpath} should not be found in ${contextNode}\n\n${prettyDOM(
13
+ contextNode
14
+ )}`,
15
+ pass: true,
16
+ };
17
+ }
18
+ return {
19
+ message: () =>
20
+ `Xpath ${xpath} should be found in ${contextNode}\n\n${prettyDOM(
21
+ contextNode
22
+ )}`,
23
+ pass: false,
24
+ };
25
+ },
26
+ } as any);
27
+
28
+ declare global {
29
+ namespace jest {
30
+ interface Matchers<R> {
31
+ toBeFoundIn(contextNode: Node): R;
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,90 @@
1
+ import { ArgumentTypes } from './commonTypes';
2
+ import { TOLGEE_TARGET_ATTRIBUTE } from '../Constants/Global';
3
+
4
+ export class NodeHelper {
5
+ static evaluate<T extends Node>(
6
+ ...args: ArgumentTypes<typeof NodeHelper.evaluateGenerator>
7
+ ): T[] {
8
+ return Array.from(this.evaluateGenerator(...args)) as T[];
9
+ }
10
+
11
+ static evaluateToSingle<T extends Node>(
12
+ ...args: ArgumentTypes<typeof NodeHelper.evaluateGenerator>
13
+ ): T {
14
+ const arr = this.evaluate<T>(...args);
15
+ if (arr.length === 1) {
16
+ return arr[0];
17
+ }
18
+ if (arr.length < 1) {
19
+ throw new Error('No element found');
20
+ }
21
+ throw new Error('Multiple elements found');
22
+ }
23
+
24
+ public static closestElement(node: Element | Text) {
25
+ if (node instanceof Text) {
26
+ return node.parentElement;
27
+ }
28
+ return node;
29
+ }
30
+
31
+ static getParentElement(node: Node): Element | undefined {
32
+ if (node.parentElement) {
33
+ return node.parentElement;
34
+ }
35
+ if ((node as Attr).ownerElement) {
36
+ return (node as Attr).ownerElement;
37
+ }
38
+ }
39
+
40
+ static isElementTargetElement(element: Element): boolean {
41
+ return element.hasAttribute(TOLGEE_TARGET_ATTRIBUTE);
42
+ }
43
+
44
+ static markElementAsTargetElement(element: Element): void {
45
+ element.setAttribute(TOLGEE_TARGET_ATTRIBUTE, '');
46
+ }
47
+
48
+ static unmarkElementAsTargetElement(element: Element): void {
49
+ element.removeAttribute(TOLGEE_TARGET_ATTRIBUTE);
50
+ }
51
+
52
+ static nodeContains(descendant: Node, node: Node) {
53
+ if (descendant.contains(node)) {
54
+ return true;
55
+ }
56
+ if (node instanceof Attr) {
57
+ const ownerContainsAttr =
58
+ node.ownerElement &&
59
+ Object.values(node.ownerElement.attributes).indexOf(node) > -1;
60
+ if (descendant.contains(node.ownerElement) && ownerContainsAttr) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ private static *evaluateGenerator<T extends Node>(
68
+ expression: string,
69
+ targetNode: Node
70
+ ): Generator<T> {
71
+ let node: Node;
72
+ const evaluated = document.evaluate(
73
+ expression,
74
+ targetNode,
75
+ undefined,
76
+ XPathResult.ANY_TYPE
77
+ );
78
+ while ((node = evaluated.iterateNext()) !== null) {
79
+ yield node as T;
80
+ }
81
+ }
82
+
83
+ static getNodeText(node: Node) {
84
+ return node.textContent;
85
+ }
86
+
87
+ static setNodeText(node: Node, text: string) {
88
+ node.textContent = text;
89
+ }
90
+ }
@@ -0,0 +1,62 @@
1
+ jest.dontMock('./TextHelper');
2
+
3
+ import { TextHelper } from './TextHelper';
4
+
5
+ describe('TextHelper', () => {
6
+ describe('will split', () => {
7
+ test('on non escaped properly', () => {
8
+ const strings = TextHelper.splitOnNonEscapedDelimiter(
9
+ 'text.to.split',
10
+ '.'
11
+ );
12
+ expect(strings).toEqual(['text', 'to', 'split']);
13
+ });
14
+
15
+ test('on escaped and non escaped properly', () => {
16
+ const strings = TextHelper.splitOnNonEscapedDelimiter(
17
+ 'text\\.to\\.split',
18
+ '.'
19
+ );
20
+ expect(strings).toEqual(['text.to.split']);
21
+ });
22
+
23
+ test('empty string properly', () => {
24
+ const strings = TextHelper.splitOnNonEscapedDelimiter('', '.');
25
+ expect(strings).toEqual(['']);
26
+ });
27
+
28
+ test('correctly when it begins with escape', async () => {
29
+ const strings = TextHelper.splitOnNonEscapedDelimiter('\\.aa', '.');
30
+ expect(strings).toEqual(['.aa']);
31
+ });
32
+
33
+ test('correctly when it ends with escape', async () => {
34
+ const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\.', '.');
35
+ expect(strings).toEqual(['aa.']);
36
+ });
37
+
38
+ test('correctly when it contains escaped escape character', async () => {
39
+ const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\\\.', '.');
40
+ expect(strings).toEqual(['aa\\', '']);
41
+ });
42
+
43
+ test('correctly when it contains escaped escape character and delimiter is escaped', async () => {
44
+ const strings = TextHelper.splitOnNonEscapedDelimiter('aa\\\\\\.', '.');
45
+ expect(strings).toEqual(['aa\\.']);
46
+ });
47
+ });
48
+
49
+ describe('It remove escapes', () => {
50
+ test('basically', async () => {
51
+ expect(TextHelper.removeEscapes('t\\t')).toEqual('tt');
52
+ });
53
+
54
+ test('not if escape character is escaped', async () => {
55
+ expect(TextHelper.removeEscapes('\\\\')).toEqual('\\');
56
+ });
57
+
58
+ test('if there is escaped escaped character', async () => {
59
+ expect(TextHelper.removeEscapes('\\\\\\')).toEqual('\\');
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,58 @@
1
+ export class TextHelper {
2
+ public static splitOnNonEscapedDelimiter(
3
+ string: string,
4
+ delimiter: string
5
+ ): string[] {
6
+ const result = [];
7
+ let actual = '';
8
+ let escaped = false;
9
+ for (let i = 0; i < string.length; i++) {
10
+ const char = string[i];
11
+ if (char === '\\' && !escaped) {
12
+ escaped = true;
13
+ continue;
14
+ }
15
+ if (escaped) {
16
+ escaped = false;
17
+ actual += char;
18
+ continue;
19
+ }
20
+ if (char === delimiter) {
21
+ result.push(actual);
22
+ actual = '';
23
+ continue;
24
+ }
25
+ actual += char;
26
+ }
27
+ result.push(actual);
28
+ return result;
29
+ }
30
+
31
+ public static isCharEscaped(position: number, fullString: string) {
32
+ let escapeCharsCount = 0;
33
+ while (position > -1 && fullString[position - 1] === '\\') {
34
+ escapeCharsCount++;
35
+ position--;
36
+ }
37
+ return escapeCharsCount % 2 == 1;
38
+ }
39
+
40
+ public static removeEscapes(string: string) {
41
+ let result = '';
42
+ let escaped = false;
43
+ for (let i = 0; i < string.length; i++) {
44
+ const char = string[i];
45
+ if (char === '\\' && !escaped) {
46
+ escaped = true;
47
+ continue;
48
+ }
49
+ if (escaped) {
50
+ escaped = false;
51
+ result += char;
52
+ continue;
53
+ }
54
+ result += char;
55
+ }
56
+ return result;
57
+ }
58
+ }
@@ -0,0 +1,8 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-types
2
+ export type ArgumentTypes<F extends Function> = F extends (
3
+ ...args: infer A
4
+ ) => any
5
+ ? A
6
+ : never;
7
+
8
+ export type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;