laravel-workers 0.4.0 → 0.4.1
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/LICENSE +21 -21
- package/bin/laravel-workers.mjs +141 -141
- package/build.mjs +2599 -2599
- package/compiler/bin/controller.mjs +92 -92
- package/kernel/cache.mjs +405 -405
- package/kernel/db.mjs +703 -703
- package/kernel/encrypter.mjs +1 -1
- package/kernel/hash.mjs +113 -113
- package/kernel/multipart.mjs +161 -161
- package/kernel/router.mjs +276 -276
- package/kernel/scheduler.mjs +236 -236
- package/kernel/str.mjs +445 -445
- package/kernel/tests/cache.test.mjs +290 -290
- package/kernel/tests/db.test.mjs +1068 -1068
- package/kernel/tests/encrypter.test.mjs +1 -1
- package/kernel/tests/hash.test.mjs +149 -149
- package/kernel/tests/http.test.mjs +1182 -1182
- package/kernel/tests/php-json.test.mjs +263 -263
- package/kernel/tests/router.test.mjs +281 -281
- package/kernel/tests/services.test.mjs +474 -474
- package/kernel/tests/str.test.mjs +83 -83
- package/kernel/tests/validation.test.mjs +340 -340
- package/kernel/validation.mjs +123 -123
- package/package.json +69 -69
- package/tools/schema-dump.php +158 -158
package/kernel/str.mjs
CHANGED
|
@@ -1,445 +1,445 @@
|
|
|
1
|
-
// kernel/str.mjs - Illuminate\Support\Str (the subset that compiles at the edge).
|
|
2
|
-
//
|
|
3
|
-
// Every rule here is PHP-exact: the values are verified against the real
|
|
4
|
-
// Laravel (the oracle) in kernel/tests/str.test.mjs. The transliteration
|
|
5
|
-
// (Str::ascii) covers the Latin-1 supplement - the range Str::slug() needs.
|
|
6
|
-
//
|
|
7
|
-
// Not ported: Str::head (removed in Laravel 11+), the plural inflections, the
|
|
8
|
-
// random/ulid helpers and the `*Array` variants. A construct outside this
|
|
9
|
-
// surface fails the build (LW002), it never becomes a silent stub.
|
|
10
|
-
|
|
11
|
-
const ASCII_MAP = {
|
|
12
|
-
à: 'a', á: 'a', â: 'a', ã: 'a', ä: 'a', å: 'a', ā: 'a', ă: 'a', ą: 'a',
|
|
13
|
-
ç: 'c', ć: 'c', ĉ: 'c', ċ: 'c', č: 'c',
|
|
14
|
-
ď: 'd', đ: 'd',
|
|
15
|
-
è: 'e', é: 'e', ê: 'e', ë: 'e', ē: 'e', ĕ: 'e', ė: 'e', ę: 'e', ě: 'e',
|
|
16
|
-
ĝ: 'g', ğ: 'g', ġ: 'g', ģ: 'g',
|
|
17
|
-
ĥ: 'h', ħ: 'h',
|
|
18
|
-
ì: 'i', í: 'i', î: 'i', ï: 'i', ĩ: 'i', ī: 'i', ĭ: 'i', į: 'i', ı: 'i',
|
|
19
|
-
ĵ: 'j',
|
|
20
|
-
ķ: 'k',
|
|
21
|
-
ĺ: 'l', ļ: 'l', ľ: 'l', ł: 'l',
|
|
22
|
-
ñ: 'n', ń: 'n', ņ: 'n', ň: 'n', ŋ: 'n',
|
|
23
|
-
ò: 'o', ó: 'o', ô: 'o', õ: 'o', ö: 'o', ø: 'o', ō: 'o', ŏ: 'o', ő: 'o',
|
|
24
|
-
ŕ: 'r', ŗ: 'r', ř: 'r',
|
|
25
|
-
ś: 's', ŝ: 's', ş: 's', š: 's', ș: 's', ß: 'ss',
|
|
26
|
-
ţ: 't', ť: 't', ŧ: 't', ț: 't',
|
|
27
|
-
ù: 'u', ú: 'u', û: 'u', ü: 'u', ũ: 'u', ū: 'u', ŭ: 'u', ů: 'u', ű: 'u', Ų: 'u',
|
|
28
|
-
ŵ: 'w',
|
|
29
|
-
ý: 'y', ÿ: 'y', ŷ: 'y',
|
|
30
|
-
ź: 'z', ż: 'z', ž: 'z',
|
|
31
|
-
æ: 'ae', œ: 'oe', ð: 'd', þ: 'th',
|
|
32
|
-
'·': '', '×': 'x', '©': '(c)', '®': '(r)', '™': 'tm',
|
|
33
|
-
'\ufb00': 'ff', '\ufb01': 'fi', '\ufb02': 'fl', '\ufb03': 'ffi',
|
|
34
|
-
'\ufb04': 'ffl', '\ufb05': 'st', '\ufb06': 'st',
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function phpLower(value) {
|
|
38
|
-
return String(value == null ? '' : value).toLowerCase();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function phpUpper(value) {
|
|
42
|
-
return String(value == null ? '' : value).toUpperCase();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// PHP's mb_strtoupper: the full uppercase mapping (it expands, 'ß' -> 'SS').
|
|
46
|
-
function mbUpper(ch) {
|
|
47
|
-
return ch.toUpperCase();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// PHP's mb_lcfirst: the first CHARACTER is lowercased ('Été' -> 'été').
|
|
51
|
-
function mbLcfirst(value) {
|
|
52
|
-
const s = String(value == null ? '' : value);
|
|
53
|
-
const first = [...s][0];
|
|
54
|
-
if (first === undefined) return s;
|
|
55
|
-
return first.toLowerCase() + s.slice(first.length);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// PHP's global ucwords() (not Str::ucwords): the first ASCII letter of each
|
|
59
|
-
// word is uppercased and the rest lowercased; the other bytes are untouched
|
|
60
|
-
// ('ß' stays 'ß'). Laravel's snake() uses this one.
|
|
61
|
-
function phpUcwords(value) {
|
|
62
|
-
return String(value == null ? '' : value).replace(
|
|
63
|
-
/(^|\s)([a-z])/g,
|
|
64
|
-
(m, sep, ch) => sep + ch.toUpperCase(),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// PHP's ctype_lower(): true only for a non-empty string of lowercase ASCII
|
|
69
|
-
// letters (the C locale) - a space or a digit is not one.
|
|
70
|
-
function phpCtypeLower(value) {
|
|
71
|
-
return /^[a-z]+$/.test(String(value));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// The East Asian Wide/Fullwidth ranges: mb_strwidth() counts these as two
|
|
75
|
-
// columns (PHP's mbstring follows the East Asian Width property).
|
|
76
|
-
const WIDE_RANGES = [
|
|
77
|
-
[0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], [0x23e9, 0x23ec],
|
|
78
|
-
[0x23f0, 0x23f0], [0x23f3, 0x23f3], [0x25fd, 0x25fe], [0x2614, 0x2615],
|
|
79
|
-
[0x2648, 0x2653], [0x267f, 0x267f], [0x2693, 0x2693], [0x26a1, 0x26a1],
|
|
80
|
-
[0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], [0x26ce, 0x26ce],
|
|
81
|
-
[0x26d4, 0x26d4], [0x26ea, 0x26ea], [0x26f2, 0x26f3], [0x26f5, 0x26f5],
|
|
82
|
-
[0x26fa, 0x26fa], [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b],
|
|
83
|
-
[0x2728, 0x2728], [0x274c, 0x274c], [0x274e, 0x274e], [0x2753, 0x2755],
|
|
84
|
-
[0x2757, 0x2757], [0x2795, 0x2797], [0x27b0, 0x27b0], [0x27bf, 0x27bf],
|
|
85
|
-
[0x2b1b, 0x2b1c], [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0x303e],
|
|
86
|
-
[0x3041, 0x33ff], [0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf],
|
|
87
|
-
[0xa960, 0xa97f], [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe10, 0xfe19],
|
|
88
|
-
[0xfe30, 0xfe6f], [0xff00, 0xff60], [0xffe0, 0xffe6], [0x16fe0, 0x16fe4],
|
|
89
|
-
[0x17000, 0x18d08], [0x1b000, 0x1b152], [0x1b164, 0x1b167], [0x1b170, 0x1b2fb],
|
|
90
|
-
[0x1f004, 0x1f004], [0x1f0cf, 0x1f0cf], [0x1f18e, 0x1f18e], [0x1f191, 0x1f19a],
|
|
91
|
-
[0x1f200, 0x1f320], [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], [0x1f37e, 0x1f393],
|
|
92
|
-
[0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], [0x1f3f4, 0x1f3f4],
|
|
93
|
-
[0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d],
|
|
94
|
-
[0x1f54b, 0x1f54e], [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], [0x1f595, 0x1f596],
|
|
95
|
-
[0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], [0x1f6cc, 0x1f6cc],
|
|
96
|
-
[0x1f6d0, 0x1f6d2], [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6f9], [0x1f910, 0x1f9ff],
|
|
97
|
-
[0x20000, 0x2fffd], [0x30000, 0x3fffd],
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
// The width of one character (mbstring): the wide ones are two columns, the
|
|
101
|
-
// combining marks are none, the rest is one.
|
|
102
|
-
function charWidth(ch) {
|
|
103
|
-
const cp = ch.codePointAt(0);
|
|
104
|
-
for (const [lo, hi] of WIDE_RANGES) {
|
|
105
|
-
if (cp < lo) break;
|
|
106
|
-
if (cp <= hi) return 2;
|
|
107
|
-
}
|
|
108
|
-
if (/\p{Mn}|\p{Me}|\p{Cf}/u.test(ch)) return 0;
|
|
109
|
-
return 1;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// PHP's mb_strwidth($value, 'UTF-8').
|
|
113
|
-
function mbStrwidth(value) {
|
|
114
|
-
let w = 0;
|
|
115
|
-
for (const ch of String(value)) w += charWidth(ch);
|
|
116
|
-
return w;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// PHP's mb_strimwidth($value, $start, $width, '', 'UTF-8').
|
|
120
|
-
function mbStrimwidth(value, start, width) {
|
|
121
|
-
let out = '';
|
|
122
|
-
let w = 0;
|
|
123
|
-
let skipped = 0;
|
|
124
|
-
for (const ch of String(value)) {
|
|
125
|
-
const cw = charWidth(ch);
|
|
126
|
-
if (skipped < start) {
|
|
127
|
-
skipped++;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (w + cw > width) break;
|
|
131
|
-
out += ch;
|
|
132
|
-
w += cw;
|
|
133
|
-
}
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// PHP's mb_substr($value, $start, $length, 'UTF-8') for one character.
|
|
138
|
-
function mbSubstr(value, start, length) {
|
|
139
|
-
const chars = [...String(value)];
|
|
140
|
-
return chars.slice(start, start + length).join('');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// PHP's strip_tags(): the comments go, and a tag that is never closed takes
|
|
144
|
-
// the rest of the string with it ('a<B' -> 'a').
|
|
145
|
-
function stripTags(value) {
|
|
146
|
-
return String(value).replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>?/g, '');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Str::ascii() - the Latin-1 transliteration (the part Str::slug needs). */
|
|
150
|
-
export function ascii(value) {
|
|
151
|
-
let out = '';
|
|
152
|
-
for (const ch of String(value == null ? '' : value)) {
|
|
153
|
-
// The Turkish dotted capital I: PHP's transliteration maps it to 'I'
|
|
154
|
-
// (the JS lowercase would be 'i' + a combining dot).
|
|
155
|
-
if (ch === '\u0130') {
|
|
156
|
-
out += 'I';
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const lower = ch.toLowerCase();
|
|
160
|
-
if (ASCII_MAP[lower] !== undefined) {
|
|
161
|
-
// The uppercase form maps to the uppercase letter (Str::ascii('É') = 'E').
|
|
162
|
-
const mapped = ASCII_MAP[lower];
|
|
163
|
-
out += ch === lower ? mapped : mapped.toUpperCase();
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (ch.charCodeAt(0) < 128) {
|
|
167
|
-
out += ch;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
// Everything else is dropped (portable-ascii does the same).
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Str::slug() */
|
|
176
|
-
export function slug(title, separator = '-') {
|
|
177
|
-
let value = ascii(title);
|
|
178
|
-
value = value.replace(/[_\-]+/g, separator);
|
|
179
|
-
// Laravel's dictionary: '@' becomes 'at' (wrapped in the separator), so
|
|
180
|
-
// 'a@b' is 'a-at-b'.
|
|
181
|
-
value = value.split('@').join(separator + 'at' + separator);
|
|
182
|
-
value = phpLower(value);
|
|
183
|
-
value = value.replace(new RegExp('[^' + escapeRe(separator) + '\\p{L}\\p{N}\\s]+', 'gu'), '');
|
|
184
|
-
value = value.replace(new RegExp('[' + escapeRe(separator) + '\\s]+', 'gu'), separator);
|
|
185
|
-
return trimChars(value, separator);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** Str::limit() */
|
|
189
|
-
export function limit(value, limit = 100, end = '...', preserveWords = false) {
|
|
190
|
-
const str = String(value == null ? '' : value);
|
|
191
|
-
// Laravel: `if (mb_strwidth($value, 'UTF-8') <= $limit) return $value;` -
|
|
192
|
-
// the limit is a DISPLAY width (a CJK glyph is two columns), not a count of
|
|
193
|
-
// characters.
|
|
194
|
-
if (mbStrwidth(str) <= limit) return str;
|
|
195
|
-
if (!preserveWords) {
|
|
196
|
-
return trimEnd(mbStrimwidth(str, 0, limit)) + end;
|
|
197
|
-
}
|
|
198
|
-
// The preserve-words branch: the tags are stripped, the newlines become
|
|
199
|
-
// spaces and the cut lands on a word boundary.
|
|
200
|
-
const v = trimEnd(stripTags(str.replace(/[\n\r]+/g, ' ')).replace(/^[\s\u3000]+|[\s\u3000]+$/g, ''));
|
|
201
|
-
const trimmed = trimEnd(mbStrimwidth(v, 0, limit));
|
|
202
|
-
if (mbSubstr(v, limit, 1) === ' ') return trimmed + end;
|
|
203
|
-
const cut = trimmed.match(/^([\s\S]*)\s[\s\S]*$/);
|
|
204
|
-
return (cut ? cut[1] : trimmed) + end;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Str::studly() */
|
|
208
|
-
export function studly(value) {
|
|
209
|
-
// Laravel: the separators become spaces, the words are exploded, each is
|
|
210
|
-
// ucfirst'ed and they are joined *without* the spaces.
|
|
211
|
-
const words = String(value == null ? '' : value)
|
|
212
|
-
.replace(/[-_]+/g, ' ')
|
|
213
|
-
.split(' ')
|
|
214
|
-
.filter((w) => w !== '');
|
|
215
|
-
return words.map((w) => ucfirst(w)).join('');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Str::camel() */
|
|
219
|
-
export function camel(value) {
|
|
220
|
-
// Laravel: `static::lcfirst(static::studly($value))` and Str::lcfirst is
|
|
221
|
-
// `mb_lcfirst` (the first CHARACTER is lowercased).
|
|
222
|
-
return mbLcfirst(studly(value));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Str::snake() */
|
|
226
|
-
export function snake(value, delimiter = '_') {
|
|
227
|
-
let v = String(value == null ? '' : value);
|
|
228
|
-
// Laravel: `if (! ctype_lower($value))` - ctype_lower is true only for a
|
|
229
|
-
// non-empty string of lowercase ASCII letters, so 'hello world' is NOT
|
|
230
|
-
// lowercase-only and Laravel converts it ('hello_world').
|
|
231
|
-
if (!phpCtypeLower(v)) {
|
|
232
|
-
// Laravel uses PHP's global ucwords() here (not Str::ucwords): only the
|
|
233
|
-
// ASCII letters count.
|
|
234
|
-
v = phpUcwords(v).replace(/\s+/gu, '');
|
|
235
|
-
v = phpLower(v.replace(/(.)(?=[A-Z])/gu, '$1' + delimiter));
|
|
236
|
-
}
|
|
237
|
-
return v;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Str::kebab() */
|
|
241
|
-
export function kebab(value) {
|
|
242
|
-
return snake(value, '-');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Str::title() */
|
|
246
|
-
// The characters Unicode calls case-ignorable: they do not end a word, so
|
|
247
|
-
// 'o'brien' stays "O'brien" and 'a.b' stays 'A.b' (PHP's mb_convert_case with
|
|
248
|
-
// MB_CASE_TITLE - probed).
|
|
249
|
-
const CASE_IGNORABLE = /[\p{Mn}\p{Me}\p{Cf}\p{Lm}\p{Sk}\u0027\u002e\u003a\u00b7\u0387\u0559\u05f4\u2018\u2019\u2024\u2027\ufeff]/u;
|
|
250
|
-
|
|
251
|
-
// The titlecase mapping (the characters whose titlecase is not their
|
|
252
|
-
// uppercase).
|
|
253
|
-
const TITLE_MAP = {
|
|
254
|
-
'\u01c4': '\u01c5', '\u01c5': '\u01c5', '\u01c6': '\u01c5',
|
|
255
|
-
'\u01c8': '\u01c8', '\u01c9': '\u01c8',
|
|
256
|
-
'\u01cc': '\u01cb', '\u01cd': '\u01cd', '\u01ce': '\u01cb',
|
|
257
|
-
'\u01f2': '\u01f2', '\u01f3': '\u01f2',
|
|
258
|
-
'\ufb00': 'Ffl', '\ufb01': 'Fi', '\ufb02': 'Fl', '\ufb03': 'Ffi',
|
|
259
|
-
'\ufb04': 'Ffl', '\ufb05': 'St', '\ufb06': 'St',
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
function mbTitlecase(ch) {
|
|
263
|
-
if (TITLE_MAP[ch] !== undefined) return TITLE_MAP[ch];
|
|
264
|
-
// The Unicode's titlecase mapping of the sharp s is 'Ss' (the full
|
|
265
|
-
// mapping expands it).
|
|
266
|
-
if (ch === '\u00df') return 'Ss';
|
|
267
|
-
if (/\p{Lt}/u.test(ch)) return ch;
|
|
268
|
-
return mbUpper(ch);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function title(value) {
|
|
272
|
-
// Laravel: `mb_convert_case($value, MB_CASE_TITLE, 'UTF-8')`. PHP's rule
|
|
273
|
-
// (php_unicode_convert_case, PHP_UNICODE_CASE_TITLE_SIMPLE): a word is a
|
|
274
|
-
// run of CASED letters; a case-ignorable character does not end it ('a.b' ->
|
|
275
|
-
// 'A.b'), any other character does ('a1b2' -> 'A1B2', 'hello-world' ->
|
|
276
|
-
// 'Hello-World'). The first letter of a word is titlecased, the rest is
|
|
277
|
-
// lowercased.
|
|
278
|
-
const s = String(value == null ? '' : value);
|
|
279
|
-
let out = '';
|
|
280
|
-
let inWord = false;
|
|
281
|
-
for (const ch of s) {
|
|
282
|
-
if (/\p{Ll}|\p{Lu}|\p{Lt}/u.test(ch)) {
|
|
283
|
-
out += inWord ? ch.toLowerCase() : mbTitlecase(ch);
|
|
284
|
-
inWord = true;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
out += ch;
|
|
288
|
-
if (!CASE_IGNORABLE.test(ch)) inWord = false;
|
|
289
|
-
}
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Str::contains() */
|
|
294
|
-
export function contains(haystack, needles) {
|
|
295
|
-
const h = String(haystack == null ? '' : haystack);
|
|
296
|
-
const list = Array.isArray(needles) ? needles : [needles];
|
|
297
|
-
// Laravel: the needle must not be empty (`$needle !== ''`).
|
|
298
|
-
return list.some((n) => String(n) !== '' && h.includes(String(n)));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Str::startsWith() */
|
|
302
|
-
export function startsWith(haystack, needles) {
|
|
303
|
-
const h = String(haystack == null ? '' : haystack);
|
|
304
|
-
const list = Array.isArray(needles) ? needles : [needles];
|
|
305
|
-
// Laravel: the needle must not be empty (`(string) $needle !== ''`).
|
|
306
|
-
return list.some((n) => String(n) !== '' && h.startsWith(String(n)));
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Str::endsWith() */
|
|
310
|
-
export function endsWith(haystack, needles) {
|
|
311
|
-
const h = String(haystack == null ? '' : haystack);
|
|
312
|
-
const list = Array.isArray(needles) ? needles : [needles];
|
|
313
|
-
// Laravel: the needle must not be empty (`(string) $needle !== ''`).
|
|
314
|
-
return list.some((n) => String(n) !== '' && h.endsWith(String(n)));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/** Str::after() */
|
|
318
|
-
export function after(subject, search) {
|
|
319
|
-
const s = String(subject == null ? '' : subject);
|
|
320
|
-
const needle = String(search == null ? '' : search);
|
|
321
|
-
return needle === '' ? s : s.slice(s.indexOf(needle) + needle.length);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Str::before() */
|
|
325
|
-
export function before(subject, search) {
|
|
326
|
-
const s = String(subject == null ? '' : subject);
|
|
327
|
-
const needle = String(search == null ? '' : search);
|
|
328
|
-
if (needle === '') return s;
|
|
329
|
-
const at = s.indexOf(needle);
|
|
330
|
-
return at === -1 ? s : s.slice(0, at);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/** Str::length() */
|
|
334
|
-
export function length(value) {
|
|
335
|
-
return Array.from(String(value == null ? '' : value)).length;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Str::replace() */
|
|
339
|
-
export function replace(search, replace, subject) {
|
|
340
|
-
return String(subject == null ? '' : subject).split(String(search)).join(String(replace));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Str::replaceFirst() */
|
|
344
|
-
export function replaceFirst(search, replace, subject) {
|
|
345
|
-
const s = String(subject == null ? '' : subject);
|
|
346
|
-
const needle = String(search);
|
|
347
|
-
if (needle === '') return s;
|
|
348
|
-
const at = s.indexOf(needle);
|
|
349
|
-
if (at === -1) return s;
|
|
350
|
-
return s.slice(0, at) + String(replace) + s.slice(at + needle.length);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Str::replaceLast() */
|
|
354
|
-
export function replaceLast(search, replace, subject) {
|
|
355
|
-
const s = String(subject == null ? '' : subject);
|
|
356
|
-
const needle = String(search);
|
|
357
|
-
if (needle === '') return s;
|
|
358
|
-
const at = s.lastIndexOf(needle);
|
|
359
|
-
if (at === -1) return s;
|
|
360
|
-
return s.slice(0, at) + String(replace) + s.slice(at + needle.length);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Str::squish() */
|
|
364
|
-
export function squish(value) {
|
|
365
|
-
return String(value == null ? '' : value)
|
|
366
|
-
.replace(/^[\s\u3000]+|[\s\u3000]+$/g, '')
|
|
367
|
-
.replace(/(?:[\s\u3000])+/g, ' ');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Str::ucfirst() */
|
|
371
|
-
export function ucfirst(value) {
|
|
372
|
-
// Laravel: `mb_ucfirst($string, 'UTF-8')` - the first CHARACTER is
|
|
373
|
-
// titlecased ('dž' -> 'Dž', 'ß' -> 'Ss').
|
|
374
|
-
const s = String(value == null ? '' : value);
|
|
375
|
-
const first = [...s][0];
|
|
376
|
-
if (first === undefined) return s;
|
|
377
|
-
return mbTitlecase(first) + s.slice(first.length);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Str::ucwords() */
|
|
381
|
-
export function ucwords(value) {
|
|
382
|
-
// Laravel: `preg_replace_callback('/(^|['.preg_quote($separators).'])(\p{Ll})/u', ...)`
|
|
383
|
-
// with the separators " \t\r\n\f\v" - only a LOWERCASE letter right
|
|
384
|
-
// after a separator (or at the start) is uppercased ('foo_bar' -> 'Foo_bar',
|
|
385
|
-
// 'école' -> 'École', 'HELLO WORLD' stays).
|
|
386
|
-
return String(value == null ? '' : value).replace(
|
|
387
|
-
/(^|[ \t\r\n\f\v])(\p{Ll})/gu,
|
|
388
|
-
(m, sep, ch) => sep + mbUpper(ch),
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Str::upper() / Str::lower() */
|
|
393
|
-
export function upper(value) {
|
|
394
|
-
return phpUpper(value);
|
|
395
|
-
}
|
|
396
|
-
export function lower(value) {
|
|
397
|
-
return phpLower(value);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function escapeRe(value) {
|
|
401
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
402
|
-
}
|
|
403
|
-
function trimChars(value, chars) {
|
|
404
|
-
let out = String(value);
|
|
405
|
-
while (out.length && out.startsWith(chars)) out = out.slice(1);
|
|
406
|
-
while (out.length && out.endsWith(chars)) out = out.slice(0, -1);
|
|
407
|
-
return out;
|
|
408
|
-
}
|
|
409
|
-
function trimEnd(value) {
|
|
410
|
-
return String(value).replace(/\s+$/g, '');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// The surface the compiler allows (Str::x() -> rt.str.x()).
|
|
414
|
-
export const STR_METHODS = new Set([
|
|
415
|
-
'slug', 'limit', 'upper', 'lower', 'studly', 'camel', 'snake', 'kebab', 'title',
|
|
416
|
-
'contains', 'startsWith', 'endsWith', 'after', 'before', 'length',
|
|
417
|
-
'replace', 'replaceFirst', 'replaceLast', 'squish', 'ucfirst', 'ucwords', 'ascii',
|
|
418
|
-
]);
|
|
419
|
-
|
|
420
|
-
export const str = {
|
|
421
|
-
slug,
|
|
422
|
-
limit,
|
|
423
|
-
upper,
|
|
424
|
-
lower,
|
|
425
|
-
studly,
|
|
426
|
-
camel,
|
|
427
|
-
snake,
|
|
428
|
-
kebab,
|
|
429
|
-
title,
|
|
430
|
-
contains,
|
|
431
|
-
startsWith,
|
|
432
|
-
endsWith,
|
|
433
|
-
after,
|
|
434
|
-
before,
|
|
435
|
-
length,
|
|
436
|
-
replace,
|
|
437
|
-
replaceFirst,
|
|
438
|
-
replaceLast,
|
|
439
|
-
squish,
|
|
440
|
-
ucfirst,
|
|
441
|
-
ucwords,
|
|
442
|
-
ascii,
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
export default { str, STR_METHODS, ascii };
|
|
1
|
+
// kernel/str.mjs - Illuminate\Support\Str (the subset that compiles at the edge).
|
|
2
|
+
//
|
|
3
|
+
// Every rule here is PHP-exact: the values are verified against the real
|
|
4
|
+
// Laravel (the oracle) in kernel/tests/str.test.mjs. The transliteration
|
|
5
|
+
// (Str::ascii) covers the Latin-1 supplement - the range Str::slug() needs.
|
|
6
|
+
//
|
|
7
|
+
// Not ported: Str::head (removed in Laravel 11+), the plural inflections, the
|
|
8
|
+
// random/ulid helpers and the `*Array` variants. A construct outside this
|
|
9
|
+
// surface fails the build (LW002), it never becomes a silent stub.
|
|
10
|
+
|
|
11
|
+
const ASCII_MAP = {
|
|
12
|
+
à: 'a', á: 'a', â: 'a', ã: 'a', ä: 'a', å: 'a', ā: 'a', ă: 'a', ą: 'a',
|
|
13
|
+
ç: 'c', ć: 'c', ĉ: 'c', ċ: 'c', č: 'c',
|
|
14
|
+
ď: 'd', đ: 'd',
|
|
15
|
+
è: 'e', é: 'e', ê: 'e', ë: 'e', ē: 'e', ĕ: 'e', ė: 'e', ę: 'e', ě: 'e',
|
|
16
|
+
ĝ: 'g', ğ: 'g', ġ: 'g', ģ: 'g',
|
|
17
|
+
ĥ: 'h', ħ: 'h',
|
|
18
|
+
ì: 'i', í: 'i', î: 'i', ï: 'i', ĩ: 'i', ī: 'i', ĭ: 'i', į: 'i', ı: 'i',
|
|
19
|
+
ĵ: 'j',
|
|
20
|
+
ķ: 'k',
|
|
21
|
+
ĺ: 'l', ļ: 'l', ľ: 'l', ł: 'l',
|
|
22
|
+
ñ: 'n', ń: 'n', ņ: 'n', ň: 'n', ŋ: 'n',
|
|
23
|
+
ò: 'o', ó: 'o', ô: 'o', õ: 'o', ö: 'o', ø: 'o', ō: 'o', ŏ: 'o', ő: 'o',
|
|
24
|
+
ŕ: 'r', ŗ: 'r', ř: 'r',
|
|
25
|
+
ś: 's', ŝ: 's', ş: 's', š: 's', ș: 's', ß: 'ss',
|
|
26
|
+
ţ: 't', ť: 't', ŧ: 't', ț: 't',
|
|
27
|
+
ù: 'u', ú: 'u', û: 'u', ü: 'u', ũ: 'u', ū: 'u', ŭ: 'u', ů: 'u', ű: 'u', Ų: 'u',
|
|
28
|
+
ŵ: 'w',
|
|
29
|
+
ý: 'y', ÿ: 'y', ŷ: 'y',
|
|
30
|
+
ź: 'z', ż: 'z', ž: 'z',
|
|
31
|
+
æ: 'ae', œ: 'oe', ð: 'd', þ: 'th',
|
|
32
|
+
'·': '', '×': 'x', '©': '(c)', '®': '(r)', '™': 'tm',
|
|
33
|
+
'\ufb00': 'ff', '\ufb01': 'fi', '\ufb02': 'fl', '\ufb03': 'ffi',
|
|
34
|
+
'\ufb04': 'ffl', '\ufb05': 'st', '\ufb06': 'st',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function phpLower(value) {
|
|
38
|
+
return String(value == null ? '' : value).toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function phpUpper(value) {
|
|
42
|
+
return String(value == null ? '' : value).toUpperCase();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// PHP's mb_strtoupper: the full uppercase mapping (it expands, 'ß' -> 'SS').
|
|
46
|
+
function mbUpper(ch) {
|
|
47
|
+
return ch.toUpperCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// PHP's mb_lcfirst: the first CHARACTER is lowercased ('Été' -> 'été').
|
|
51
|
+
function mbLcfirst(value) {
|
|
52
|
+
const s = String(value == null ? '' : value);
|
|
53
|
+
const first = [...s][0];
|
|
54
|
+
if (first === undefined) return s;
|
|
55
|
+
return first.toLowerCase() + s.slice(first.length);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// PHP's global ucwords() (not Str::ucwords): the first ASCII letter of each
|
|
59
|
+
// word is uppercased and the rest lowercased; the other bytes are untouched
|
|
60
|
+
// ('ß' stays 'ß'). Laravel's snake() uses this one.
|
|
61
|
+
function phpUcwords(value) {
|
|
62
|
+
return String(value == null ? '' : value).replace(
|
|
63
|
+
/(^|\s)([a-z])/g,
|
|
64
|
+
(m, sep, ch) => sep + ch.toUpperCase(),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// PHP's ctype_lower(): true only for a non-empty string of lowercase ASCII
|
|
69
|
+
// letters (the C locale) - a space or a digit is not one.
|
|
70
|
+
function phpCtypeLower(value) {
|
|
71
|
+
return /^[a-z]+$/.test(String(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The East Asian Wide/Fullwidth ranges: mb_strwidth() counts these as two
|
|
75
|
+
// columns (PHP's mbstring follows the East Asian Width property).
|
|
76
|
+
const WIDE_RANGES = [
|
|
77
|
+
[0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], [0x23e9, 0x23ec],
|
|
78
|
+
[0x23f0, 0x23f0], [0x23f3, 0x23f3], [0x25fd, 0x25fe], [0x2614, 0x2615],
|
|
79
|
+
[0x2648, 0x2653], [0x267f, 0x267f], [0x2693, 0x2693], [0x26a1, 0x26a1],
|
|
80
|
+
[0x26aa, 0x26ab], [0x26bd, 0x26be], [0x26c4, 0x26c5], [0x26ce, 0x26ce],
|
|
81
|
+
[0x26d4, 0x26d4], [0x26ea, 0x26ea], [0x26f2, 0x26f3], [0x26f5, 0x26f5],
|
|
82
|
+
[0x26fa, 0x26fa], [0x26fd, 0x26fd], [0x2705, 0x2705], [0x270a, 0x270b],
|
|
83
|
+
[0x2728, 0x2728], [0x274c, 0x274c], [0x274e, 0x274e], [0x2753, 0x2755],
|
|
84
|
+
[0x2757, 0x2757], [0x2795, 0x2797], [0x27b0, 0x27b0], [0x27bf, 0x27bf],
|
|
85
|
+
[0x2b1b, 0x2b1c], [0x2b50, 0x2b50], [0x2b55, 0x2b55], [0x2e80, 0x303e],
|
|
86
|
+
[0x3041, 0x33ff], [0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf],
|
|
87
|
+
[0xa960, 0xa97f], [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe10, 0xfe19],
|
|
88
|
+
[0xfe30, 0xfe6f], [0xff00, 0xff60], [0xffe0, 0xffe6], [0x16fe0, 0x16fe4],
|
|
89
|
+
[0x17000, 0x18d08], [0x1b000, 0x1b152], [0x1b164, 0x1b167], [0x1b170, 0x1b2fb],
|
|
90
|
+
[0x1f004, 0x1f004], [0x1f0cf, 0x1f0cf], [0x1f18e, 0x1f18e], [0x1f191, 0x1f19a],
|
|
91
|
+
[0x1f200, 0x1f320], [0x1f32d, 0x1f335], [0x1f337, 0x1f37c], [0x1f37e, 0x1f393],
|
|
92
|
+
[0x1f3a0, 0x1f3ca], [0x1f3cf, 0x1f3d3], [0x1f3e0, 0x1f3f0], [0x1f3f4, 0x1f3f4],
|
|
93
|
+
[0x1f3f8, 0x1f43e], [0x1f440, 0x1f440], [0x1f442, 0x1f4fc], [0x1f4ff, 0x1f53d],
|
|
94
|
+
[0x1f54b, 0x1f54e], [0x1f550, 0x1f567], [0x1f57a, 0x1f57a], [0x1f595, 0x1f596],
|
|
95
|
+
[0x1f5a4, 0x1f5a4], [0x1f5fb, 0x1f64f], [0x1f680, 0x1f6c5], [0x1f6cc, 0x1f6cc],
|
|
96
|
+
[0x1f6d0, 0x1f6d2], [0x1f6eb, 0x1f6ec], [0x1f6f4, 0x1f6f9], [0x1f910, 0x1f9ff],
|
|
97
|
+
[0x20000, 0x2fffd], [0x30000, 0x3fffd],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// The width of one character (mbstring): the wide ones are two columns, the
|
|
101
|
+
// combining marks are none, the rest is one.
|
|
102
|
+
function charWidth(ch) {
|
|
103
|
+
const cp = ch.codePointAt(0);
|
|
104
|
+
for (const [lo, hi] of WIDE_RANGES) {
|
|
105
|
+
if (cp < lo) break;
|
|
106
|
+
if (cp <= hi) return 2;
|
|
107
|
+
}
|
|
108
|
+
if (/\p{Mn}|\p{Me}|\p{Cf}/u.test(ch)) return 0;
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// PHP's mb_strwidth($value, 'UTF-8').
|
|
113
|
+
function mbStrwidth(value) {
|
|
114
|
+
let w = 0;
|
|
115
|
+
for (const ch of String(value)) w += charWidth(ch);
|
|
116
|
+
return w;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// PHP's mb_strimwidth($value, $start, $width, '', 'UTF-8').
|
|
120
|
+
function mbStrimwidth(value, start, width) {
|
|
121
|
+
let out = '';
|
|
122
|
+
let w = 0;
|
|
123
|
+
let skipped = 0;
|
|
124
|
+
for (const ch of String(value)) {
|
|
125
|
+
const cw = charWidth(ch);
|
|
126
|
+
if (skipped < start) {
|
|
127
|
+
skipped++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (w + cw > width) break;
|
|
131
|
+
out += ch;
|
|
132
|
+
w += cw;
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// PHP's mb_substr($value, $start, $length, 'UTF-8') for one character.
|
|
138
|
+
function mbSubstr(value, start, length) {
|
|
139
|
+
const chars = [...String(value)];
|
|
140
|
+
return chars.slice(start, start + length).join('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// PHP's strip_tags(): the comments go, and a tag that is never closed takes
|
|
144
|
+
// the rest of the string with it ('a<B' -> 'a').
|
|
145
|
+
function stripTags(value) {
|
|
146
|
+
return String(value).replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>?/g, '');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Str::ascii() - the Latin-1 transliteration (the part Str::slug needs). */
|
|
150
|
+
export function ascii(value) {
|
|
151
|
+
let out = '';
|
|
152
|
+
for (const ch of String(value == null ? '' : value)) {
|
|
153
|
+
// The Turkish dotted capital I: PHP's transliteration maps it to 'I'
|
|
154
|
+
// (the JS lowercase would be 'i' + a combining dot).
|
|
155
|
+
if (ch === '\u0130') {
|
|
156
|
+
out += 'I';
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const lower = ch.toLowerCase();
|
|
160
|
+
if (ASCII_MAP[lower] !== undefined) {
|
|
161
|
+
// The uppercase form maps to the uppercase letter (Str::ascii('É') = 'E').
|
|
162
|
+
const mapped = ASCII_MAP[lower];
|
|
163
|
+
out += ch === lower ? mapped : mapped.toUpperCase();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch.charCodeAt(0) < 128) {
|
|
167
|
+
out += ch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
// Everything else is dropped (portable-ascii does the same).
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Str::slug() */
|
|
176
|
+
export function slug(title, separator = '-') {
|
|
177
|
+
let value = ascii(title);
|
|
178
|
+
value = value.replace(/[_\-]+/g, separator);
|
|
179
|
+
// Laravel's dictionary: '@' becomes 'at' (wrapped in the separator), so
|
|
180
|
+
// 'a@b' is 'a-at-b'.
|
|
181
|
+
value = value.split('@').join(separator + 'at' + separator);
|
|
182
|
+
value = phpLower(value);
|
|
183
|
+
value = value.replace(new RegExp('[^' + escapeRe(separator) + '\\p{L}\\p{N}\\s]+', 'gu'), '');
|
|
184
|
+
value = value.replace(new RegExp('[' + escapeRe(separator) + '\\s]+', 'gu'), separator);
|
|
185
|
+
return trimChars(value, separator);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Str::limit() */
|
|
189
|
+
export function limit(value, limit = 100, end = '...', preserveWords = false) {
|
|
190
|
+
const str = String(value == null ? '' : value);
|
|
191
|
+
// Laravel: `if (mb_strwidth($value, 'UTF-8') <= $limit) return $value;` -
|
|
192
|
+
// the limit is a DISPLAY width (a CJK glyph is two columns), not a count of
|
|
193
|
+
// characters.
|
|
194
|
+
if (mbStrwidth(str) <= limit) return str;
|
|
195
|
+
if (!preserveWords) {
|
|
196
|
+
return trimEnd(mbStrimwidth(str, 0, limit)) + end;
|
|
197
|
+
}
|
|
198
|
+
// The preserve-words branch: the tags are stripped, the newlines become
|
|
199
|
+
// spaces and the cut lands on a word boundary.
|
|
200
|
+
const v = trimEnd(stripTags(str.replace(/[\n\r]+/g, ' ')).replace(/^[\s\u3000]+|[\s\u3000]+$/g, ''));
|
|
201
|
+
const trimmed = trimEnd(mbStrimwidth(v, 0, limit));
|
|
202
|
+
if (mbSubstr(v, limit, 1) === ' ') return trimmed + end;
|
|
203
|
+
const cut = trimmed.match(/^([\s\S]*)\s[\s\S]*$/);
|
|
204
|
+
return (cut ? cut[1] : trimmed) + end;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Str::studly() */
|
|
208
|
+
export function studly(value) {
|
|
209
|
+
// Laravel: the separators become spaces, the words are exploded, each is
|
|
210
|
+
// ucfirst'ed and they are joined *without* the spaces.
|
|
211
|
+
const words = String(value == null ? '' : value)
|
|
212
|
+
.replace(/[-_]+/g, ' ')
|
|
213
|
+
.split(' ')
|
|
214
|
+
.filter((w) => w !== '');
|
|
215
|
+
return words.map((w) => ucfirst(w)).join('');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Str::camel() */
|
|
219
|
+
export function camel(value) {
|
|
220
|
+
// Laravel: `static::lcfirst(static::studly($value))` and Str::lcfirst is
|
|
221
|
+
// `mb_lcfirst` (the first CHARACTER is lowercased).
|
|
222
|
+
return mbLcfirst(studly(value));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Str::snake() */
|
|
226
|
+
export function snake(value, delimiter = '_') {
|
|
227
|
+
let v = String(value == null ? '' : value);
|
|
228
|
+
// Laravel: `if (! ctype_lower($value))` - ctype_lower is true only for a
|
|
229
|
+
// non-empty string of lowercase ASCII letters, so 'hello world' is NOT
|
|
230
|
+
// lowercase-only and Laravel converts it ('hello_world').
|
|
231
|
+
if (!phpCtypeLower(v)) {
|
|
232
|
+
// Laravel uses PHP's global ucwords() here (not Str::ucwords): only the
|
|
233
|
+
// ASCII letters count.
|
|
234
|
+
v = phpUcwords(v).replace(/\s+/gu, '');
|
|
235
|
+
v = phpLower(v.replace(/(.)(?=[A-Z])/gu, '$1' + delimiter));
|
|
236
|
+
}
|
|
237
|
+
return v;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Str::kebab() */
|
|
241
|
+
export function kebab(value) {
|
|
242
|
+
return snake(value, '-');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Str::title() */
|
|
246
|
+
// The characters Unicode calls case-ignorable: they do not end a word, so
|
|
247
|
+
// 'o'brien' stays "O'brien" and 'a.b' stays 'A.b' (PHP's mb_convert_case with
|
|
248
|
+
// MB_CASE_TITLE - probed).
|
|
249
|
+
const CASE_IGNORABLE = /[\p{Mn}\p{Me}\p{Cf}\p{Lm}\p{Sk}\u0027\u002e\u003a\u00b7\u0387\u0559\u05f4\u2018\u2019\u2024\u2027\ufeff]/u;
|
|
250
|
+
|
|
251
|
+
// The titlecase mapping (the characters whose titlecase is not their
|
|
252
|
+
// uppercase).
|
|
253
|
+
const TITLE_MAP = {
|
|
254
|
+
'\u01c4': '\u01c5', '\u01c5': '\u01c5', '\u01c6': '\u01c5',
|
|
255
|
+
'\u01c8': '\u01c8', '\u01c9': '\u01c8',
|
|
256
|
+
'\u01cc': '\u01cb', '\u01cd': '\u01cd', '\u01ce': '\u01cb',
|
|
257
|
+
'\u01f2': '\u01f2', '\u01f3': '\u01f2',
|
|
258
|
+
'\ufb00': 'Ffl', '\ufb01': 'Fi', '\ufb02': 'Fl', '\ufb03': 'Ffi',
|
|
259
|
+
'\ufb04': 'Ffl', '\ufb05': 'St', '\ufb06': 'St',
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
function mbTitlecase(ch) {
|
|
263
|
+
if (TITLE_MAP[ch] !== undefined) return TITLE_MAP[ch];
|
|
264
|
+
// The Unicode's titlecase mapping of the sharp s is 'Ss' (the full
|
|
265
|
+
// mapping expands it).
|
|
266
|
+
if (ch === '\u00df') return 'Ss';
|
|
267
|
+
if (/\p{Lt}/u.test(ch)) return ch;
|
|
268
|
+
return mbUpper(ch);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function title(value) {
|
|
272
|
+
// Laravel: `mb_convert_case($value, MB_CASE_TITLE, 'UTF-8')`. PHP's rule
|
|
273
|
+
// (php_unicode_convert_case, PHP_UNICODE_CASE_TITLE_SIMPLE): a word is a
|
|
274
|
+
// run of CASED letters; a case-ignorable character does not end it ('a.b' ->
|
|
275
|
+
// 'A.b'), any other character does ('a1b2' -> 'A1B2', 'hello-world' ->
|
|
276
|
+
// 'Hello-World'). The first letter of a word is titlecased, the rest is
|
|
277
|
+
// lowercased.
|
|
278
|
+
const s = String(value == null ? '' : value);
|
|
279
|
+
let out = '';
|
|
280
|
+
let inWord = false;
|
|
281
|
+
for (const ch of s) {
|
|
282
|
+
if (/\p{Ll}|\p{Lu}|\p{Lt}/u.test(ch)) {
|
|
283
|
+
out += inWord ? ch.toLowerCase() : mbTitlecase(ch);
|
|
284
|
+
inWord = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
out += ch;
|
|
288
|
+
if (!CASE_IGNORABLE.test(ch)) inWord = false;
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Str::contains() */
|
|
294
|
+
export function contains(haystack, needles) {
|
|
295
|
+
const h = String(haystack == null ? '' : haystack);
|
|
296
|
+
const list = Array.isArray(needles) ? needles : [needles];
|
|
297
|
+
// Laravel: the needle must not be empty (`$needle !== ''`).
|
|
298
|
+
return list.some((n) => String(n) !== '' && h.includes(String(n)));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Str::startsWith() */
|
|
302
|
+
export function startsWith(haystack, needles) {
|
|
303
|
+
const h = String(haystack == null ? '' : haystack);
|
|
304
|
+
const list = Array.isArray(needles) ? needles : [needles];
|
|
305
|
+
// Laravel: the needle must not be empty (`(string) $needle !== ''`).
|
|
306
|
+
return list.some((n) => String(n) !== '' && h.startsWith(String(n)));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Str::endsWith() */
|
|
310
|
+
export function endsWith(haystack, needles) {
|
|
311
|
+
const h = String(haystack == null ? '' : haystack);
|
|
312
|
+
const list = Array.isArray(needles) ? needles : [needles];
|
|
313
|
+
// Laravel: the needle must not be empty (`(string) $needle !== ''`).
|
|
314
|
+
return list.some((n) => String(n) !== '' && h.endsWith(String(n)));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Str::after() */
|
|
318
|
+
export function after(subject, search) {
|
|
319
|
+
const s = String(subject == null ? '' : subject);
|
|
320
|
+
const needle = String(search == null ? '' : search);
|
|
321
|
+
return needle === '' ? s : s.slice(s.indexOf(needle) + needle.length);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Str::before() */
|
|
325
|
+
export function before(subject, search) {
|
|
326
|
+
const s = String(subject == null ? '' : subject);
|
|
327
|
+
const needle = String(search == null ? '' : search);
|
|
328
|
+
if (needle === '') return s;
|
|
329
|
+
const at = s.indexOf(needle);
|
|
330
|
+
return at === -1 ? s : s.slice(0, at);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Str::length() */
|
|
334
|
+
export function length(value) {
|
|
335
|
+
return Array.from(String(value == null ? '' : value)).length;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Str::replace() */
|
|
339
|
+
export function replace(search, replace, subject) {
|
|
340
|
+
return String(subject == null ? '' : subject).split(String(search)).join(String(replace));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Str::replaceFirst() */
|
|
344
|
+
export function replaceFirst(search, replace, subject) {
|
|
345
|
+
const s = String(subject == null ? '' : subject);
|
|
346
|
+
const needle = String(search);
|
|
347
|
+
if (needle === '') return s;
|
|
348
|
+
const at = s.indexOf(needle);
|
|
349
|
+
if (at === -1) return s;
|
|
350
|
+
return s.slice(0, at) + String(replace) + s.slice(at + needle.length);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Str::replaceLast() */
|
|
354
|
+
export function replaceLast(search, replace, subject) {
|
|
355
|
+
const s = String(subject == null ? '' : subject);
|
|
356
|
+
const needle = String(search);
|
|
357
|
+
if (needle === '') return s;
|
|
358
|
+
const at = s.lastIndexOf(needle);
|
|
359
|
+
if (at === -1) return s;
|
|
360
|
+
return s.slice(0, at) + String(replace) + s.slice(at + needle.length);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Str::squish() */
|
|
364
|
+
export function squish(value) {
|
|
365
|
+
return String(value == null ? '' : value)
|
|
366
|
+
.replace(/^[\s\u3000]+|[\s\u3000]+$/g, '')
|
|
367
|
+
.replace(/(?:[\s\u3000])+/g, ' ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Str::ucfirst() */
|
|
371
|
+
export function ucfirst(value) {
|
|
372
|
+
// Laravel: `mb_ucfirst($string, 'UTF-8')` - the first CHARACTER is
|
|
373
|
+
// titlecased ('dž' -> 'Dž', 'ß' -> 'Ss').
|
|
374
|
+
const s = String(value == null ? '' : value);
|
|
375
|
+
const first = [...s][0];
|
|
376
|
+
if (first === undefined) return s;
|
|
377
|
+
return mbTitlecase(first) + s.slice(first.length);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Str::ucwords() */
|
|
381
|
+
export function ucwords(value) {
|
|
382
|
+
// Laravel: `preg_replace_callback('/(^|['.preg_quote($separators).'])(\p{Ll})/u', ...)`
|
|
383
|
+
// with the separators " \t\r\n\f\v" - only a LOWERCASE letter right
|
|
384
|
+
// after a separator (or at the start) is uppercased ('foo_bar' -> 'Foo_bar',
|
|
385
|
+
// 'école' -> 'École', 'HELLO WORLD' stays).
|
|
386
|
+
return String(value == null ? '' : value).replace(
|
|
387
|
+
/(^|[ \t\r\n\f\v])(\p{Ll})/gu,
|
|
388
|
+
(m, sep, ch) => sep + mbUpper(ch),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Str::upper() / Str::lower() */
|
|
393
|
+
export function upper(value) {
|
|
394
|
+
return phpUpper(value);
|
|
395
|
+
}
|
|
396
|
+
export function lower(value) {
|
|
397
|
+
return phpLower(value);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function escapeRe(value) {
|
|
401
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
402
|
+
}
|
|
403
|
+
function trimChars(value, chars) {
|
|
404
|
+
let out = String(value);
|
|
405
|
+
while (out.length && out.startsWith(chars)) out = out.slice(1);
|
|
406
|
+
while (out.length && out.endsWith(chars)) out = out.slice(0, -1);
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
function trimEnd(value) {
|
|
410
|
+
return String(value).replace(/\s+$/g, '');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The surface the compiler allows (Str::x() -> rt.str.x()).
|
|
414
|
+
export const STR_METHODS = new Set([
|
|
415
|
+
'slug', 'limit', 'upper', 'lower', 'studly', 'camel', 'snake', 'kebab', 'title',
|
|
416
|
+
'contains', 'startsWith', 'endsWith', 'after', 'before', 'length',
|
|
417
|
+
'replace', 'replaceFirst', 'replaceLast', 'squish', 'ucfirst', 'ucwords', 'ascii',
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
export const str = {
|
|
421
|
+
slug,
|
|
422
|
+
limit,
|
|
423
|
+
upper,
|
|
424
|
+
lower,
|
|
425
|
+
studly,
|
|
426
|
+
camel,
|
|
427
|
+
snake,
|
|
428
|
+
kebab,
|
|
429
|
+
title,
|
|
430
|
+
contains,
|
|
431
|
+
startsWith,
|
|
432
|
+
endsWith,
|
|
433
|
+
after,
|
|
434
|
+
before,
|
|
435
|
+
length,
|
|
436
|
+
replace,
|
|
437
|
+
replaceFirst,
|
|
438
|
+
replaceLast,
|
|
439
|
+
squish,
|
|
440
|
+
ucfirst,
|
|
441
|
+
ucwords,
|
|
442
|
+
ascii,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export default { str, STR_METHODS, ascii };
|