@nejs/basic-extensions 2.2.1 → 2.4.0
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.
- package/README.md +186 -65
- package/dist/@nejs/{basic-extensions.bundle.2.2.0.js → basic-extensions.bundle.2.3.0.js} +5 -5
- package/dist/@nejs/basic-extensions.bundle.2.3.0.js.map +7 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/newClasses/deferred.d.ts +20 -0
- package/dist/cjs/newClasses/deferred.js +60 -3
- package/dist/cjs/newClasses/deferred.js.map +1 -1
- package/dist/cjs/stringextensions.d.ts +13 -0
- package/dist/cjs/stringextensions.js +129 -1
- package/dist/cjs/stringextensions.js.map +1 -1
- package/dist/mjs/index.js +2 -1
- package/dist/mjs/index.js.map +1 -1
- package/dist/mjs/newClasses/deferred.d.ts +20 -0
- package/dist/mjs/newClasses/deferred.js +57 -0
- package/dist/mjs/newClasses/deferred.js.map +1 -1
- package/dist/mjs/stringextensions.d.ts +13 -0
- package/dist/mjs/stringextensions.js +128 -0
- package/dist/mjs/stringextensions.js.map +1 -1
- package/docs/index.html +918 -201
- package/package.json +3 -3
- package/repl.bootstrap.js +248 -0
- package/src/index.js +6 -5
- package/src/newClasses/deferred.js +67 -0
- package/src/stringextensions.js +139 -0
- package/tests/arrayextensions.test.js +54 -0
- package/tests/stringextensions.test.js +60 -0
- package/dist/@nejs/basic-extensions.bundle.2.2.0.js.map +0 -7
package/package.json
CHANGED
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"test": "jest"
|
|
59
59
|
},
|
|
60
60
|
"type": "module",
|
|
61
|
-
"version": "2.
|
|
61
|
+
"version": "2.4.0",
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@nejs/extension": "^2.7.
|
|
63
|
+
"@nejs/extension": "^2.7.2"
|
|
64
64
|
},
|
|
65
|
-
"browser": "dist/@nejs/basic-extensions.bundle.2.
|
|
65
|
+
"browser": "dist/@nejs/basic-extensions.bundle.2.3.0.js"
|
|
66
66
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node --no-warnings --no-deprecations
|
|
2
|
+
|
|
3
|
+
// Import everything for playtesting.
|
|
4
|
+
(await import('./dist/mjs/index.js')).Controls.enableAll();
|
|
5
|
+
|
|
6
|
+
const nejsExtension = await import('@nejs/extension');
|
|
7
|
+
|
|
8
|
+
const repl = await import('node:repl');
|
|
9
|
+
const fs = await import('node:fs');
|
|
10
|
+
const project = JSON.parse(String(fs.readFileSync('./package.json')));
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
useGlobal: true,
|
|
14
|
+
prompt: '\x1b[32mλ\x1b[1;39m\x1b[22m '
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let allowInvocation = true;
|
|
18
|
+
|
|
19
|
+
Object.assign(global, { Patch: nejsExtension.Patch, Extension: nejsExtension.Extension });
|
|
20
|
+
global.replServer = new repl.REPLServer(options);
|
|
21
|
+
|
|
22
|
+
function about() {
|
|
23
|
+
console.log(`\x1b[32m${project.name}\x1b[39m v\x1b[1m${project.version}\x1b[22m`);
|
|
24
|
+
console.log(`\x1b[3m${project.description}\x1b[23m`);
|
|
25
|
+
console.log(`Written by \x1b[34m${project.author ?? 'Jane Doe'}\x1b[39m.\n`);
|
|
26
|
+
this?.displayPrompt() ?? replServer.displayPrompt();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const clear = () => {
|
|
30
|
+
if (allowInvocation) {
|
|
31
|
+
process.stdout.write('\x1b[0;0H\x1b[J');
|
|
32
|
+
return about();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clear();
|
|
37
|
+
|
|
38
|
+
replServer.defineCommand('cls', {
|
|
39
|
+
action: clear,
|
|
40
|
+
help: 'Clears the screen.'
|
|
41
|
+
});
|
|
42
|
+
replServer.defineCommand('clear', {
|
|
43
|
+
action: clear,
|
|
44
|
+
help: 'Clears the screen.'
|
|
45
|
+
});
|
|
46
|
+
replServer.defineCommand('about', {
|
|
47
|
+
action: about,
|
|
48
|
+
help: 'Shows info about this project.'
|
|
49
|
+
});
|
|
50
|
+
replServer.defineCommand('state', {
|
|
51
|
+
help: 'Shows stats about this REPL\'s state.',
|
|
52
|
+
action() {
|
|
53
|
+
const state = generateState();
|
|
54
|
+
const i = (s) => `\x1b[3m${s}\x1b[23m`;
|
|
55
|
+
const j = ', ';
|
|
56
|
+
|
|
57
|
+
state.classes = [...Object.keys(state.classes)].map(k => String(k));
|
|
58
|
+
state.functions = [...Object.keys(state.functions)].map(k => String(k));
|
|
59
|
+
state.properties = [...Object.keys(state.properties)].map(k => String(k));
|
|
60
|
+
state.descriptors.accessors = [...Object.keys(state.descriptors.accessors)].map(k => String(k));
|
|
61
|
+
state.descriptors.data = [...Object.keys(state.descriptors.data)].map(k => String(k));
|
|
62
|
+
|
|
63
|
+
console.log(`\x1b[1mClasses\x1b[22m\n${wrapContent(state.classes, i, j)}`);
|
|
64
|
+
console.log(`\x1b[1mFunctions\x1b[22m\n${wrapContent(state.functions, i, j)}`);
|
|
65
|
+
console.log(`\x1b[1mProperties\x1b[22m\n${wrapContent(state.properties, i, j)}`);
|
|
66
|
+
console.log(`\x1b[1mAccessor Descriptors\x1b[22m\n${wrapContent(state.descriptors.accessors, i, j)}`);
|
|
67
|
+
console.log(`\x1b[1mData Descriptors\x1b[22m\n${wrapContent(state.descriptors.data, i, j)}`);
|
|
68
|
+
|
|
69
|
+
replServer.displayPrompt();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
overridableGlobal('clear', clear);
|
|
74
|
+
overridableGlobal('cls', clear);
|
|
75
|
+
overridableGlobal('state', generateState);
|
|
76
|
+
|
|
77
|
+
Object.defineProperty(replServer, '_initialPrompt', {
|
|
78
|
+
get() {
|
|
79
|
+
const isRed = !globalThis?._;
|
|
80
|
+
const prompt = isRed
|
|
81
|
+
? '\x1b[31mλ\x1b[1;39m\x1b[22m '
|
|
82
|
+
: '\x1b[32mλ\x1b[1;39m\x1b[22m ';
|
|
83
|
+
|
|
84
|
+
return prompt;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function overridableGlobal(
|
|
89
|
+
property,
|
|
90
|
+
action,
|
|
91
|
+
changeText = 'Expression assignment to "@X", previous function now disabled.'
|
|
92
|
+
) {
|
|
93
|
+
const message = changeText.replaceAll(/\@X/g, property);
|
|
94
|
+
let changed = false;
|
|
95
|
+
let storage = undefined;
|
|
96
|
+
|
|
97
|
+
const makeDescriptor = () => ({
|
|
98
|
+
get() {
|
|
99
|
+
if (changed === false) {
|
|
100
|
+
return action();
|
|
101
|
+
}
|
|
102
|
+
return storage;
|
|
103
|
+
},
|
|
104
|
+
set(value) {
|
|
105
|
+
if (changed === false) {
|
|
106
|
+
console.log(message);
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
storage = value;
|
|
110
|
+
},
|
|
111
|
+
configurable: true,
|
|
112
|
+
get enumerable() { return changed }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
replServer.defineCommand(
|
|
116
|
+
`restore${property.charAt(0).toUpperCase()}${property.substring(1,property.length)}`,
|
|
117
|
+
{
|
|
118
|
+
action() {
|
|
119
|
+
changed = false;
|
|
120
|
+
storage = undefined;
|
|
121
|
+
Object.defineProperty(globalThis, property, makeDescriptor());
|
|
122
|
+
console.log(this.help);
|
|
123
|
+
},
|
|
124
|
+
help: `Restores ${property} to default REPL custom state.`,
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
Object.defineProperty(globalThis, property, makeDescriptor());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function generateState() {
|
|
132
|
+
const replState = {
|
|
133
|
+
classes: {},
|
|
134
|
+
functions: {},
|
|
135
|
+
properties: {},
|
|
136
|
+
descriptors: {
|
|
137
|
+
accessors: {},
|
|
138
|
+
data: {},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (!allowInvocation) {
|
|
143
|
+
return replState;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let skipped = [];
|
|
147
|
+
|
|
148
|
+
allowInvocation = false;
|
|
149
|
+
Reflect.ownKeys(globalThis).forEach(key => {
|
|
150
|
+
try {
|
|
151
|
+
const value = globalThis[key];
|
|
152
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, key);
|
|
153
|
+
|
|
154
|
+
if (String(value).startsWith('class')) {
|
|
155
|
+
replState.classes[key] = {key, value, descriptor};
|
|
156
|
+
}
|
|
157
|
+
else if (typeof value === 'function') {
|
|
158
|
+
replState.functions[key] = {key, value, descriptor};
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
replState.properties[key] = {key, value, descriptor};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (Reflect.has(descriptor, 'get') || Reflect.has(descriptor, 'set')) {
|
|
165
|
+
replState.descriptors.accessors[key] = { key, descriptor };
|
|
166
|
+
}
|
|
167
|
+
else if (Reflect.has(descriptor, 'value')) {
|
|
168
|
+
replState.descriptors.data[key] = { key, descriptor };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (ignored) {
|
|
172
|
+
skipped.push(String(key));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
allowInvocation = true;
|
|
176
|
+
|
|
177
|
+
return replState;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Formats a string or array of values into lines with specified indentation and line width.
|
|
182
|
+
* @param {string|array} input - The input string or array of strings to be formatted.
|
|
183
|
+
* @param {number} nCols - The maximum number of columns per line (default 80).
|
|
184
|
+
* @param {number} nSpaceIndents - The number of spaces for indentation (default 2).
|
|
185
|
+
* @returns {string} The formatted string.
|
|
186
|
+
*/
|
|
187
|
+
function formatValues(input, transform, nCols = 80, nSpaceIndents = 2) {
|
|
188
|
+
// Split the string into an array if input is a string
|
|
189
|
+
const values = typeof input === 'string' ? input.split(', ') : input;
|
|
190
|
+
let line = ''.padStart(nSpaceIndents, ' ');
|
|
191
|
+
let result = [];
|
|
192
|
+
|
|
193
|
+
values.forEach((value, index) => {
|
|
194
|
+
// Transform value if a transform function is supplied.
|
|
195
|
+
if (transform && typeof transform === 'function') {
|
|
196
|
+
value = transform(value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if adding the next value exceeds the column limit
|
|
200
|
+
if (line.length + value.length + 2 > nCols && line.trim().length > 0) {
|
|
201
|
+
// If it does, push the line to the result and start a new line
|
|
202
|
+
result.push(line);
|
|
203
|
+
line = ''.padStart(nSpaceIndents, ' ');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add the value to the line, followed by ", " if it's not the last value
|
|
207
|
+
line += value + (index < values.length - 1 ? ', ' : '');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Add the last line if it's not empty
|
|
211
|
+
if (line.trim().length > 0) {
|
|
212
|
+
result.push(line);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return result.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function wrapContent(longString, transform, joinOn = ' ', indent = 2, wrapAt = 80) {
|
|
219
|
+
let asArray = Array.isArray(longString)
|
|
220
|
+
? longString
|
|
221
|
+
: String(longString).replaceAll(/\r\n/g, '\n').split('\n');
|
|
222
|
+
|
|
223
|
+
asArray = asArray.map(element => String(element).trim());
|
|
224
|
+
|
|
225
|
+
let lines = [];
|
|
226
|
+
let maxLen = wrapAt - indent;
|
|
227
|
+
let curLine = [];
|
|
228
|
+
let sgrLength = (s) => s.replaceAll(/\x1b\[?\d+(;\d+)*[a-zA-Z]/g, '').length;
|
|
229
|
+
|
|
230
|
+
for (let element of asArray) {
|
|
231
|
+
if (typeof transform === 'function') {
|
|
232
|
+
element = String(transform(element)).trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let curLength = sgrLength(curLine.join(joinOn));
|
|
236
|
+
let elementLength = sgrLength(String(element) + joinOn);
|
|
237
|
+
|
|
238
|
+
if (curLength + elementLength > maxLen) {
|
|
239
|
+
let leading = indent > 0 ? ' '.repeat(indent) : '';
|
|
240
|
+
lines.push(`${leading}${curLine.join(joinOn)}`);
|
|
241
|
+
curLine = [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
curLine.push(String(element));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ObjectExtensions, ObjectPrototypeExtensions } from './objectextensions.
|
|
|
3
3
|
import { MapPrototypeExtensions } from './mapextensions.js'
|
|
4
4
|
import { SetPrototypeExtensions } from './setextensions.js'
|
|
5
5
|
import { ReflectExtensions } from './reflectextensions.js'
|
|
6
|
-
import { StringExtensions } from './stringextensions.js'
|
|
6
|
+
import { StringExtensions, StringPrototypeExtensions } from './stringextensions.js'
|
|
7
7
|
import { SymbolExtensions } from './symbolextensions.js'
|
|
8
8
|
import { ArrayPrototypeExtensions } from './arrayextensions.js'
|
|
9
9
|
import { DescriptorExtensions, Descriptor } from './newClasses/descriptor.js'
|
|
@@ -32,6 +32,7 @@ const StaticPatches = [
|
|
|
32
32
|
|
|
33
33
|
const InstancePatches = [
|
|
34
34
|
[Object.prototype, ObjectPrototypeExtensions, Object.name],
|
|
35
|
+
[String.prototype, StringPrototypeExtensions, String.name],
|
|
35
36
|
[Function.prototype, FunctionPrototypeExtensions, Function.name],
|
|
36
37
|
[Array.prototype, ArrayPrototypeExtensions, Array.name],
|
|
37
38
|
[Map.prototype, MapPrototypeExtensions, Map.name],
|
|
@@ -137,9 +138,9 @@ export const all = (() => {
|
|
|
137
138
|
const instancePatchReducer = (accumulator, [_, patch, ownerName]) => {
|
|
138
139
|
if (!accumulator?.[ownerName])
|
|
139
140
|
accumulator[ownerName] = {};
|
|
140
|
-
|
|
141
|
+
|
|
141
142
|
if (!accumulator[ownerName]?.prototype)
|
|
142
|
-
accumulator[ownerName].prototype = {};
|
|
143
|
+
accumulator[ownerName].prototype = {};
|
|
143
144
|
|
|
144
145
|
[...patch].reduce(entriesReducer, accumulator[ownerName].prototype)
|
|
145
146
|
return accumulator
|
|
@@ -151,13 +152,13 @@ export const all = (() => {
|
|
|
151
152
|
.flatMap(extension => [...extension])
|
|
152
153
|
.reduce(entriesReducer, dest.classes)
|
|
153
154
|
)
|
|
154
|
-
|
|
155
|
+
|
|
155
156
|
for (const [key, entry] of GlobalFunctionsAndProps) {
|
|
156
157
|
const descriptor = new Descriptor(entry.descriptor, entry.owner)
|
|
157
158
|
Object.defineProperty(dest.global, key, descriptor.toObject(true))
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
return dest
|
|
161
|
+
return dest
|
|
161
162
|
})()
|
|
162
163
|
|
|
163
164
|
const results = {
|
|
@@ -49,6 +49,10 @@ export class Deferred extends Promise {
|
|
|
49
49
|
*/
|
|
50
50
|
#resolve = null
|
|
51
51
|
|
|
52
|
+
#rejected = false
|
|
53
|
+
|
|
54
|
+
#resolved = false
|
|
55
|
+
|
|
52
56
|
/**
|
|
53
57
|
* When the Deferred is settled with {@link Deferred.resolve}, the `value`
|
|
54
58
|
* passed to that function will be set here as well.
|
|
@@ -145,6 +149,10 @@ export class Deferred extends Promise {
|
|
|
145
149
|
}
|
|
146
150
|
// Mark the Deferred instance as settled
|
|
147
151
|
this.#settled = true
|
|
152
|
+
|
|
153
|
+
// Mark the Deferred instance as resolved
|
|
154
|
+
this.#resolved = true
|
|
155
|
+
|
|
148
156
|
// Resolve the promise with the provided value
|
|
149
157
|
return _resolve(value)
|
|
150
158
|
}
|
|
@@ -157,6 +165,10 @@ export class Deferred extends Promise {
|
|
|
157
165
|
}
|
|
158
166
|
// Mark the Deferred instance as settled
|
|
159
167
|
this.#settled = true
|
|
168
|
+
|
|
169
|
+
// Mark the Deferred as being rejected.
|
|
170
|
+
this.#rejected = true
|
|
171
|
+
|
|
160
172
|
// Reject the promise with the provided reason
|
|
161
173
|
return _reject(reason)
|
|
162
174
|
}
|
|
@@ -184,6 +196,32 @@ export class Deferred extends Promise {
|
|
|
184
196
|
return this.#settled
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
/**
|
|
200
|
+
* A getter that returns a boolean indicating whether the Deferred instance
|
|
201
|
+
* was rejected. This property can be used to check if the Deferred has been
|
|
202
|
+
* settled with a rejection. It is particularly useful in scenarios where
|
|
203
|
+
* the resolution status of the Deferred needs to be checked without
|
|
204
|
+
* accessing the rejection reason or invoking any additional logic.
|
|
205
|
+
*
|
|
206
|
+
* @returns {boolean} `true` if the Deferred was rejected, otherwise `false`.
|
|
207
|
+
*/
|
|
208
|
+
get wasRejected() {
|
|
209
|
+
return this.#rejected
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A getter that returns a boolean indicating whether the Deferred instance
|
|
214
|
+
* was resolved. This property is useful for checking if the Deferred has been
|
|
215
|
+
* settled with a resolution, allowing for checks on the Deferred's status
|
|
216
|
+
* without needing to access the resolved value or trigger any additional
|
|
217
|
+
* logic.
|
|
218
|
+
*
|
|
219
|
+
* @returns {boolean} `true` if the Deferred was resolved, otherwise `false`.
|
|
220
|
+
*/
|
|
221
|
+
get wasResolved() {
|
|
222
|
+
return this.#resolved
|
|
223
|
+
}
|
|
224
|
+
|
|
187
225
|
/**
|
|
188
226
|
* Accessor for the promise managed by this Deferred instance.
|
|
189
227
|
*
|
|
@@ -223,6 +261,35 @@ export class Deferred extends Promise {
|
|
|
223
261
|
return this.#reject(reason)
|
|
224
262
|
}
|
|
225
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Customizes the output of `util.inspect` on instances of Deferred when
|
|
266
|
+
* used in Node.js. This method is invoked by Node.js's `util.inspect`
|
|
267
|
+
* utility to format the inspection output of a Deferred instance.
|
|
268
|
+
*
|
|
269
|
+
* The output includes the state of the Deferred (resolved, rejected, or
|
|
270
|
+
* unsettled) along with the resolved value or rejection reason, if
|
|
271
|
+
* applicable. This provides a quick, readable status of the Deferred
|
|
272
|
+
* instance directly in the console or debugging tools.
|
|
273
|
+
*
|
|
274
|
+
* @param {number} depth The depth to which `util.inspect` will recurse.
|
|
275
|
+
* @param {object} options Formatting options provided by `util.inspect`.
|
|
276
|
+
* @param {function} inspect Reference to the `util.inspect` function.
|
|
277
|
+
* @returns {string} A formatted string representing the Deferred instance.
|
|
278
|
+
*/
|
|
279
|
+
[Symbol.for('nodejs.util.inspect.custom')](depth, options, inspect) {
|
|
280
|
+
return [
|
|
281
|
+
'\x1b[1mDeferred [\x1b[22;3mPromise\x1b[23;1m]\x1b[22m ',
|
|
282
|
+
'{ ',
|
|
283
|
+
(this.settled
|
|
284
|
+
? (this.wasResolved
|
|
285
|
+
? `resolved with \x1b[32m${this.value}\x1b[39m`
|
|
286
|
+
: `rejected with \x1b[31m${this.reason?.message ?? this.reason}\x1b[39m`)
|
|
287
|
+
: '\x1b[33munsettled valued or reason\x1b[39m'
|
|
288
|
+
),
|
|
289
|
+
' }'
|
|
290
|
+
].join('')
|
|
291
|
+
}
|
|
292
|
+
|
|
226
293
|
/**
|
|
227
294
|
* A getter for the species symbol which returns a custom DeferredPromise
|
|
228
295
|
* class. This class extends from Deferred and is used to ensure that the
|
package/src/stringextensions.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Patch } from '@nejs/extension';
|
|
2
2
|
|
|
3
|
+
const parenthesisPair = ['(', ')'];
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* `StringExtensions` is a patch for the JavaScript built-in `String` class. It
|
|
5
7
|
* adds utility methods to the `String` class without modifying the global namespace
|
|
@@ -22,4 +24,141 @@ export const StringExtensions = new Patch(String, {
|
|
|
22
24
|
}
|
|
23
25
|
return false
|
|
24
26
|
},
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A getter property that returns a pair of parentheses as an array.
|
|
30
|
+
* This property can be used when operations require a clear distinction
|
|
31
|
+
* between the opening and closing parentheses, such as parsing or
|
|
32
|
+
* matching balanced expressions in strings.
|
|
33
|
+
*
|
|
34
|
+
* @returns {[string, string]} An array containing a pair of strings: the
|
|
35
|
+
* opening parenthesis '(' as the first element, and the closing parenthesis
|
|
36
|
+
* ')' as the second element.
|
|
37
|
+
*/
|
|
38
|
+
get parenthesisPair() {
|
|
39
|
+
return ['(', ')'];
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A getter property that returns a pair of square brackets as an array.
|
|
44
|
+
* This property is particularly useful for operations that require a clear
|
|
45
|
+
* distinction between the opening and closing square brackets, such as
|
|
46
|
+
* parsing arrays in strings or matching balanced expressions within
|
|
47
|
+
* square brackets.
|
|
48
|
+
*
|
|
49
|
+
* @returns {[string, string]} An array containing a pair of strings: the
|
|
50
|
+
* opening square bracket '[' as the first element, and the closing square
|
|
51
|
+
* bracket ']' as the second element.
|
|
52
|
+
*/
|
|
53
|
+
get squareBracketsPair() {
|
|
54
|
+
return ['[', ']'];
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A getter property that returns a pair of curly brackets as an array.
|
|
59
|
+
* This property is particularly useful for operations that require a clear
|
|
60
|
+
* distinction between the opening and closing curly brackets, such as
|
|
61
|
+
* parsing objects in strings or matching balanced expressions within
|
|
62
|
+
* curly brackets. The returned array consists of the opening curly bracket
|
|
63
|
+
* '{' as the first element, and the closing curly bracket '}' as the
|
|
64
|
+
* second element.
|
|
65
|
+
*
|
|
66
|
+
* @returns {[string, string]} An array containing a pair of strings: the
|
|
67
|
+
* opening curly bracket '{' as the first element, and the closing curly
|
|
68
|
+
* bracket '}' as the second element.
|
|
69
|
+
*/
|
|
70
|
+
get curlyBracketsPair() {
|
|
71
|
+
return ['{', '}'];
|
|
72
|
+
},
|
|
25
73
|
});
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `StringPrototypeExtensions` provides a set of utility methods that are
|
|
77
|
+
* added to the `String` prototype. This allows all string instances to
|
|
78
|
+
* access new functionality directly, enhancing their capabilities beyond
|
|
79
|
+
* the standard `String` class methods. These extensions are applied using
|
|
80
|
+
* the `Patch` class from '@nejs/extension', ensuring that they do not
|
|
81
|
+
* interfere with the global namespace or existing properties.
|
|
82
|
+
*
|
|
83
|
+
* The extensions include methods for extracting substrings based on
|
|
84
|
+
* specific tokens, checking the presence of certain patterns, and more,
|
|
85
|
+
* making string manipulation tasks more convenient and expressive.
|
|
86
|
+
*/
|
|
87
|
+
export const StringPrototypeExtensions = new Patch(String.prototype, {
|
|
88
|
+
/**
|
|
89
|
+
* Extracts a substring from the current string, starting at a given offset
|
|
90
|
+
* and bounded by specified opening and closing tokens. This method is
|
|
91
|
+
* particularly useful for parsing nested structures or quoted strings,
|
|
92
|
+
* where the level of nesting or the presence of escape characters must
|
|
93
|
+
* be considered.
|
|
94
|
+
*
|
|
95
|
+
* @param {number} offset The position in the string from which to start the
|
|
96
|
+
* search for the substring.
|
|
97
|
+
* @param {[string, string]} tokens An array containing two strings: the
|
|
98
|
+
* opening and closing tokens that define the boundaries of the substring
|
|
99
|
+
* to be extracted.
|
|
100
|
+
* @returns {Object} An object with two properties: `extracted`, the
|
|
101
|
+
* extracted substring, and `newOffset`, the position in the original
|
|
102
|
+
* string immediately after the end of the extracted substring. If no
|
|
103
|
+
* substring is found, `extracted` is `null` and `newOffset` is the same
|
|
104
|
+
* as the input offset.
|
|
105
|
+
*/
|
|
106
|
+
extractSubstring(offset = 0, tokens = parenthesisPair) {
|
|
107
|
+
let [openToken, closeToken] = tokens;
|
|
108
|
+
let depth = 0;
|
|
109
|
+
let start = -1;
|
|
110
|
+
let end = -1;
|
|
111
|
+
let leadingToken = '';
|
|
112
|
+
let firstToken = 0;
|
|
113
|
+
|
|
114
|
+
for (let i = offset; i < this.length; i++) {
|
|
115
|
+
const char = this[i];
|
|
116
|
+
|
|
117
|
+
if (char === openToken) {
|
|
118
|
+
depth++;
|
|
119
|
+
if (start === -1)
|
|
120
|
+
start = i;
|
|
121
|
+
}
|
|
122
|
+
else if (char === closeToken) {
|
|
123
|
+
depth--;
|
|
124
|
+
if (depth === 0) {
|
|
125
|
+
end = i;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let lRange = [
|
|
132
|
+
Math.max(0, start - 100),
|
|
133
|
+
start
|
|
134
|
+
];
|
|
135
|
+
let leading = [...this.substring(lRange[0], lRange[1])].reverse().join('')
|
|
136
|
+
let reversedLeadingToken;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
reversedLeadingToken = /([^ \,\"\'\`]+)/.exec(leading)[1] ?? '';
|
|
140
|
+
leadingToken = [...reversedLeadingToken].reverse().join('');
|
|
141
|
+
}
|
|
142
|
+
catch(ignored) { }
|
|
143
|
+
|
|
144
|
+
if (start !== -1 && end !== -1) {
|
|
145
|
+
const sliceRange = [start, end + 1];
|
|
146
|
+
const extracted = this.slice(sliceRange[0], sliceRange[1]);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
extracted,
|
|
150
|
+
range: [start, end],
|
|
151
|
+
newOffset: end + 1,
|
|
152
|
+
leadingToken,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return {
|
|
157
|
+
extracted: null,
|
|
158
|
+
range: [start, end],
|
|
159
|
+
newOffset: offset,
|
|
160
|
+
leadingToken,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { Patches } = require('../dist/cjs/index.js')
|
|
2
|
+
const ArrayPrototypeExtensions = Patches.get(Array.prototype)
|
|
3
|
+
|
|
4
|
+
ArrayPrototypeExtensions.apply();
|
|
5
|
+
|
|
6
|
+
describe('ArrayPrototypeExtensions', () => {
|
|
7
|
+
describe('contains method', () => {
|
|
8
|
+
it('should return true if the array contains the specified element', () => {
|
|
9
|
+
const arr = [1, 2, 3];
|
|
10
|
+
expect(arr.contains(2)).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return false if the array does not contain the specified element', () => {
|
|
14
|
+
const arr = [1, 2, 3];
|
|
15
|
+
expect(arr.contains(4)).toBeFalsy();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('findEntry method', () => {
|
|
20
|
+
it('should return the first matching [index, value] entry', () => {
|
|
21
|
+
const arr = ['a', 'b', 'c'];
|
|
22
|
+
expect(arr.findEntry(x => x === 'b')).toEqual([1, 'b']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return undefined if no match is found', () => {
|
|
26
|
+
const arr = ['a', 'b', 'c'];
|
|
27
|
+
expect(arr.findEntry(x => x === 'd')).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('first getter', () => {
|
|
32
|
+
it('should return the first element of the array', () => {
|
|
33
|
+
const arr = [1, 2, 3];
|
|
34
|
+
expect(arr.first).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return undefined if the array is empty', () => {
|
|
38
|
+
const arr = [];
|
|
39
|
+
expect(arr.first).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('last getter', () => {
|
|
44
|
+
it('should return the last element of the array', () => {
|
|
45
|
+
const arr = [1, 2, 3];
|
|
46
|
+
expect(arr.last).toBe(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return undefined if the array is empty', () => {
|
|
50
|
+
const arr = [];
|
|
51
|
+
expect(arr.last).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { Patches } = require('../dist/cjs/index.js')
|
|
2
|
+
const StringExtensions = Patches.get(String)
|
|
3
|
+
const StringPrototypeExtensions = Patches.get(String.prototype)
|
|
4
|
+
|
|
5
|
+
describe('StringExtensions', () => {
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
// Apply the StringExtensions patch
|
|
8
|
+
StringExtensions.apply();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('isString should correctly identify strings', () => {
|
|
12
|
+
expect(String.isString('hello')).toBe(true);
|
|
13
|
+
expect(String.isString(new String('hello'))).toBe(true);
|
|
14
|
+
expect(String.isString(123)).toBe(false);
|
|
15
|
+
expect(String.isString(null)).toBe(false);
|
|
16
|
+
expect(String.isString(undefined)).toBe(false);
|
|
17
|
+
expect(String.isString({})).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('parenthesisPair should return correct pair', () => {
|
|
21
|
+
expect(String.parenthesisPair).toEqual(['(', ')']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('squareBracketsPair should return correct pair', () => {
|
|
25
|
+
expect(String.squareBracketsPair).toEqual(['[', ']']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('curlyBracketsPair should return correct pair', () => {
|
|
29
|
+
expect(String.curlyBracketsPair).toEqual(['{', '}']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('StringPrototypeExtensions', () => {
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
// Apply the StringPrototypeExtensions patch
|
|
36
|
+
StringPrototypeExtensions.apply();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('extractSubstring should correctly extract substrings', () => {
|
|
40
|
+
const testString = "This is a test (with a substring) and some more text.";
|
|
41
|
+
const result = testString.extractSubstring(0, ['(', ')']);
|
|
42
|
+
expect(result).toEqual({
|
|
43
|
+
extracted: "(with a substring)",
|
|
44
|
+
range: [15, 32],
|
|
45
|
+
newOffset: 33,
|
|
46
|
+
leadingToken: 'test',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('extractSubstring with no matching tokens should return null', () => {
|
|
51
|
+
const testString = "No parentheses here.";
|
|
52
|
+
const result = testString.extractSubstring();
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
extracted: null,
|
|
55
|
+
range: [-1, -1],
|
|
56
|
+
newOffset: 0,
|
|
57
|
+
leadingToken: '',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|