@fanboynz/network-scanner 3.0.3 → 3.1.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.
- package/CHANGELOG.md +53 -0
- package/lib/adblock-rust.js +17 -4
- package/lib/adblock.js +92 -15
- package/lib/browserhealth.js +41 -100
- package/lib/cdp.js +68 -34
- package/lib/clear_sitedata.js +68 -20
- package/lib/compress.js +26 -58
- package/lib/curl.js +44 -22
- package/lib/domain-cache.js +8 -57
- package/lib/dry-run.js +9 -4
- package/lib/fingerprint.js +599 -129
- package/lib/fingerprint.md +94 -0
- package/lib/interaction.js +262 -26
- package/lib/nettools.js +47 -76
- package/lib/openvpn_vpn.js +116 -35
- package/lib/proxy.js +6 -2
- package/lib/searchstring.js +15 -237
- package/lib/smart-cache.js +9 -1
- package/lib/socks-relay.js +14 -9
- package/lib/validate_rules.js +285 -3
- package/lib/wireguard_vpn.js +64 -12
- package/nwss.js +557 -220
- package/package.json +1 -1
- package/regex-tool/index.html +321 -628
package/regex-tool/index.html
CHANGED
|
@@ -3,13 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Regex Tools -
|
|
6
|
+
<title>Regex Tools - filterRegex Builder & Tester</title>
|
|
7
7
|
<style>
|
|
8
|
-
* {
|
|
9
|
-
margin: 0;
|
|
10
|
-
padding: 0;
|
|
11
|
-
box-sizing: border-box;
|
|
12
|
-
}
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
13
9
|
|
|
14
10
|
body {
|
|
15
11
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
@@ -21,7 +17,7 @@
|
|
|
21
17
|
.container {
|
|
22
18
|
max-width: 1000px;
|
|
23
19
|
margin: 0 auto;
|
|
24
|
-
background: rgba(255, 255, 255, 0.
|
|
20
|
+
background: rgba(255, 255, 255, 0.97);
|
|
25
21
|
border-radius: 20px;
|
|
26
22
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
27
23
|
overflow: hidden;
|
|
@@ -30,19 +26,28 @@
|
|
|
30
26
|
.header {
|
|
31
27
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
32
28
|
color: white;
|
|
33
|
-
padding: 30px;
|
|
29
|
+
padding: 28px 30px;
|
|
34
30
|
text-align: center;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
.header h1 {
|
|
38
|
-
|
|
39
|
-
margin-bottom: 10px;
|
|
40
|
-
animation: fadeInDown 0.5s ease;
|
|
41
|
-
}
|
|
33
|
+
.header h1 { font-size: 2.2em; margin-bottom: 8px; }
|
|
34
|
+
.header p { opacity: 0.9; font-size: 1.05em; }
|
|
42
35
|
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
.note {
|
|
37
|
+
background: #f0f9ff;
|
|
38
|
+
border: 1px solid #bae6fd;
|
|
39
|
+
color: #075985;
|
|
40
|
+
padding: 12px 16px;
|
|
41
|
+
margin: 18px 40px 0;
|
|
42
|
+
border-radius: 8px;
|
|
43
|
+
font-size: 0.9em;
|
|
44
|
+
line-height: 1.5;
|
|
45
|
+
}
|
|
46
|
+
.note code {
|
|
47
|
+
background: #e0f2fe;
|
|
48
|
+
padding: 1px 5px;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
font-family: 'Consolas', 'Monaco', monospace;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
.tabs {
|
|
@@ -53,742 +58,430 @@
|
|
|
53
58
|
|
|
54
59
|
.tab {
|
|
55
60
|
flex: 1;
|
|
56
|
-
padding:
|
|
61
|
+
padding: 18px;
|
|
57
62
|
text-align: center;
|
|
58
63
|
cursor: pointer;
|
|
59
64
|
background: transparent;
|
|
60
65
|
border: none;
|
|
61
|
-
font-size: 1.
|
|
66
|
+
font-size: 1.05em;
|
|
62
67
|
font-weight: 600;
|
|
63
68
|
color: #64748b;
|
|
64
|
-
transition: all 0.
|
|
69
|
+
transition: all 0.25s ease;
|
|
65
70
|
position: relative;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
.tab:
|
|
69
|
-
background: rgba(102, 126, 234, 0.1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.tab.active {
|
|
73
|
-
color: #667eea;
|
|
74
|
-
background: white;
|
|
75
|
-
}
|
|
76
|
-
|
|
72
|
+
.tab:hover { background: rgba(102, 126, 234, 0.1); }
|
|
73
|
+
.tab.active { color: #667eea; background: white; }
|
|
77
74
|
.tab.active::after {
|
|
78
75
|
content: '';
|
|
79
76
|
position: absolute;
|
|
80
|
-
bottom: -2px;
|
|
81
|
-
left: 0;
|
|
82
|
-
right: 0;
|
|
77
|
+
bottom: -2px; left: 0; right: 0;
|
|
83
78
|
height: 3px;
|
|
84
79
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
|
85
|
-
animation: slideIn 0.3s ease;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.content {
|
|
89
|
-
padding: 40px;
|
|
90
|
-
min-height: 500px;
|
|
91
80
|
}
|
|
92
81
|
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.tab-content.active {
|
|
99
|
-
display: block;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.input-group {
|
|
103
|
-
margin-bottom: 30px;
|
|
104
|
-
}
|
|
82
|
+
.content { padding: 30px 40px 40px; min-height: 420px; }
|
|
83
|
+
.tab-content { display: none; }
|
|
84
|
+
.tab-content.active { display: block; }
|
|
105
85
|
|
|
86
|
+
.input-group { margin-bottom: 22px; }
|
|
106
87
|
.input-group label {
|
|
107
88
|
display: block;
|
|
108
|
-
margin-bottom:
|
|
89
|
+
margin-bottom: 8px;
|
|
109
90
|
font-weight: 600;
|
|
110
91
|
color: #334155;
|
|
111
|
-
font-size: 1.1em;
|
|
112
92
|
}
|
|
93
|
+
.input-group .hint { font-weight: 400; color: #64748b; font-size: 0.85em; }
|
|
113
94
|
|
|
114
95
|
.input-group input[type="text"],
|
|
115
96
|
.input-group textarea {
|
|
116
97
|
width: 100%;
|
|
117
|
-
padding:
|
|
98
|
+
padding: 13px;
|
|
118
99
|
border: 2px solid #e2e8f0;
|
|
119
100
|
border-radius: 10px;
|
|
120
|
-
font-size:
|
|
101
|
+
font-size: 0.95em;
|
|
121
102
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
122
|
-
transition:
|
|
103
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
123
104
|
}
|
|
124
|
-
|
|
125
105
|
.input-group input[type="text"]:focus,
|
|
126
106
|
.input-group textarea:focus {
|
|
127
107
|
outline: none;
|
|
128
108
|
border-color: #667eea;
|
|
129
|
-
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.input-group textarea {
|
|
133
|
-
resize: vertical;
|
|
134
|
-
min-height: 100px;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.checkbox-group {
|
|
138
|
-
display: flex;
|
|
139
|
-
align-items: center;
|
|
140
|
-
margin-bottom: 20px;
|
|
109
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
|
|
141
110
|
}
|
|
111
|
+
.input-group textarea { resize: vertical; min-height: 90px; }
|
|
142
112
|
|
|
113
|
+
.checkbox-group { display: flex; align-items: center; margin-bottom: 18px; }
|
|
143
114
|
.checkbox-group input[type="checkbox"] {
|
|
144
|
-
width:
|
|
145
|
-
height: 20px;
|
|
146
|
-
margin-right: 10px;
|
|
147
|
-
cursor: pointer;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.checkbox-group label {
|
|
151
|
-
cursor: pointer;
|
|
152
|
-
font-weight: 500;
|
|
153
|
-
color: #475569;
|
|
115
|
+
width: 18px; height: 18px; margin-right: 10px; cursor: pointer;
|
|
154
116
|
}
|
|
117
|
+
.checkbox-group label { cursor: pointer; font-weight: 500; color: #475569; }
|
|
155
118
|
|
|
156
119
|
.button {
|
|
157
120
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
158
121
|
color: white;
|
|
159
122
|
border: none;
|
|
160
|
-
padding:
|
|
123
|
+
padding: 13px 36px;
|
|
161
124
|
border-radius: 10px;
|
|
162
|
-
font-size: 1.
|
|
125
|
+
font-size: 1.05em;
|
|
163
126
|
font-weight: 600;
|
|
164
127
|
cursor: pointer;
|
|
165
|
-
transition:
|
|
128
|
+
transition: transform 0.15s ease, box-shadow 0.2s ease;
|
|
166
129
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
167
130
|
}
|
|
168
|
-
|
|
169
|
-
.button:
|
|
170
|
-
transform: translateY(-2px);
|
|
171
|
-
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.button:active {
|
|
175
|
-
transform: translateY(0);
|
|
176
|
-
}
|
|
131
|
+
.button:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); }
|
|
132
|
+
.button:active { transform: translateY(0); }
|
|
177
133
|
|
|
178
134
|
.output-section {
|
|
179
|
-
margin-top:
|
|
180
|
-
padding:
|
|
135
|
+
margin-top: 28px;
|
|
136
|
+
padding: 22px;
|
|
181
137
|
background: #f8fafc;
|
|
182
138
|
border-radius: 10px;
|
|
183
139
|
border: 2px solid #e2e8f0;
|
|
184
140
|
}
|
|
185
|
-
|
|
186
|
-
.output-section h3 {
|
|
187
|
-
margin-bottom: 15px;
|
|
188
|
-
color: #334155;
|
|
189
|
-
font-size: 1.3em;
|
|
190
|
-
}
|
|
191
|
-
|
|
141
|
+
.output-section h3 { margin-bottom: 12px; color: #334155; font-size: 1.15em; }
|
|
192
142
|
.output-box {
|
|
193
143
|
background: white;
|
|
194
|
-
padding:
|
|
144
|
+
padding: 16px;
|
|
195
145
|
border-radius: 8px;
|
|
196
146
|
border: 1px solid #e2e8f0;
|
|
197
147
|
font-family: 'Consolas', 'Monaco', monospace;
|
|
198
|
-
|
|
148
|
+
white-space: pre-wrap;
|
|
199
149
|
word-break: break-all;
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.output-box:hover {
|
|
204
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.examples-list {
|
|
208
|
-
list-style: none;
|
|
209
|
-
padding: 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.examples-list li {
|
|
213
|
-
background: white;
|
|
214
|
-
padding: 15px;
|
|
215
|
-
margin-bottom: 10px;
|
|
216
|
-
border-radius: 8px;
|
|
217
|
-
border-left: 4px solid #667eea;
|
|
218
|
-
font-family: 'Consolas', 'Monaco', monospace;
|
|
219
|
-
transition: all 0.3s ease;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.examples-list li:hover {
|
|
223
|
-
transform: translateX(5px);
|
|
224
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.error {
|
|
228
|
-
background: #fee;
|
|
229
|
-
color: #c00;
|
|
230
|
-
padding: 15px;
|
|
231
|
-
border-radius: 8px;
|
|
232
|
-
border: 1px solid #fcc;
|
|
233
|
-
margin-top: 10px;
|
|
150
|
+
margin-bottom: 18px;
|
|
234
151
|
}
|
|
235
152
|
|
|
236
|
-
.
|
|
237
|
-
|
|
238
|
-
color: #047857;
|
|
239
|
-
padding: 15px;
|
|
240
|
-
border-radius: 8px;
|
|
241
|
-
border: 1px solid #6ee7b7;
|
|
242
|
-
margin-top: 10px;
|
|
243
|
-
}
|
|
153
|
+
.error { background: #fef2f2; color: #b91c1c; padding: 14px; border-radius: 8px; border: 1px solid #fecaca; margin-top: 10px; }
|
|
154
|
+
.success { background: #ecfdf5; color: #047857; padding: 14px; border-radius: 8px; border: 1px solid #6ee7b7; margin-top: 10px; }
|
|
244
155
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
transform: translateY(10px);
|
|
249
|
-
}
|
|
250
|
-
to {
|
|
251
|
-
opacity: 1;
|
|
252
|
-
transform: translateY(0);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
156
|
+
.pattern-status { font-family: 'Consolas','Monaco',monospace; font-size: 0.9em; margin-bottom: 6px; }
|
|
157
|
+
.pattern-status.ok { color: #047857; }
|
|
158
|
+
.pattern-status.bad { color: #b91c1c; }
|
|
255
159
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
transform: translateY(0);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
@keyframes slideIn {
|
|
268
|
-
from {
|
|
269
|
-
transform: scaleX(0);
|
|
270
|
-
}
|
|
271
|
-
to {
|
|
272
|
-
transform: scaleX(1);
|
|
273
|
-
}
|
|
160
|
+
table.results { width: 100%; border-collapse: collapse; font-family: 'Consolas','Monaco',monospace; font-size: 0.88em; }
|
|
161
|
+
table.results th, table.results td {
|
|
162
|
+
text-align: left;
|
|
163
|
+
padding: 9px 10px;
|
|
164
|
+
border-bottom: 1px solid #e2e8f0;
|
|
165
|
+
vertical-align: top;
|
|
166
|
+
word-break: break-all;
|
|
274
167
|
}
|
|
168
|
+
table.results th { background: #f1f5f9; color: #334155; font-weight: 600; }
|
|
169
|
+
tr.match td { background: #ecfdf5; }
|
|
170
|
+
tr.nomatch td { background: #fef2f2; }
|
|
171
|
+
.badge { display: inline-block; padding: 2px 9px; border-radius: 999px; font-weight: 700; font-size: 0.8em; }
|
|
172
|
+
.badge.match { background: #047857; color: white; }
|
|
173
|
+
.badge.nomatch { background: #b91c1c; color: white; }
|
|
174
|
+
.summary { margin: 4px 0 14px; font-weight: 600; color: #334155; }
|
|
275
175
|
|
|
276
176
|
@media (max-width: 768px) {
|
|
277
|
-
.header h1 {
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
.content {
|
|
282
|
-
padding: 20px;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
.button {
|
|
286
|
-
width: 100%;
|
|
287
|
-
}
|
|
177
|
+
.header h1 { font-size: 1.6em; }
|
|
178
|
+
.content { padding: 20px; }
|
|
179
|
+
.note { margin: 14px 20px 0; }
|
|
180
|
+
.button { width: 100%; }
|
|
288
181
|
}
|
|
289
182
|
</style>
|
|
290
183
|
</head>
|
|
291
184
|
<body>
|
|
292
185
|
<div class="container">
|
|
293
186
|
<div class="header">
|
|
294
|
-
<h1
|
|
295
|
-
<p>
|
|
187
|
+
<h1>🔧 Regex Tools</h1>
|
|
188
|
+
<p>Build & test <code>filterRegex</code> patterns for network-scanner</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="note">
|
|
192
|
+
Matching here mirrors the scanner exactly: patterns are compiled
|
|
193
|
+
<strong>case-sensitive, with no flags</strong>, a surrounding
|
|
194
|
+
<code>/.../</code> is stripped, and a URL counts as a hit when
|
|
195
|
+
<strong>any</strong> pattern matches (OR) — or <strong>all</strong>
|
|
196
|
+
of them match when <code>regex_and</code> is enabled.
|
|
296
197
|
</div>
|
|
297
198
|
|
|
298
199
|
<div class="tabs">
|
|
299
|
-
<button class="tab active" data-tab="convert">
|
|
300
|
-
|
|
301
|
-
</button>
|
|
302
|
-
<button class="tab" data-tab="validate">
|
|
303
|
-
? Validate Regex
|
|
304
|
-
</button>
|
|
200
|
+
<button class="tab active" data-tab="convert" type="button">Convert to JSON</button>
|
|
201
|
+
<button class="tab" data-tab="test" type="button">Validate & Test</button>
|
|
305
202
|
</div>
|
|
306
203
|
|
|
307
204
|
<div class="content">
|
|
308
205
|
<!-- Convert Tab -->
|
|
309
206
|
<div id="convert" class="tab-content active">
|
|
310
207
|
<div class="input-group">
|
|
311
|
-
<label for="
|
|
312
|
-
|
|
208
|
+
<label for="convertPatterns">Pattern(s) / text
|
|
209
|
+
<span class="hint">— one per line; each becomes an entry in <code>filterRegex</code></span>
|
|
210
|
+
</label>
|
|
211
|
+
<textarea id="convertPatterns" placeholder="/wrr/test.js https://ads.example.com/track.js"></textarea>
|
|
313
212
|
</div>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
213
|
+
<div style="background:#f0f9ff; padding:12px 14px; border-radius:8px; border:1px solid #bae6fd; margin-bottom:18px;">
|
|
214
|
+
<div style="font-weight:600; color:#334155; margin-bottom:8px;">Input is:</div>
|
|
215
|
+
<label style="display:block; margin-bottom:6px; cursor:pointer; font-weight:500; color:#475569;">
|
|
216
|
+
<input type="radio" name="convertMode" value="literal" checked style="margin-right:8px;">
|
|
217
|
+
Literal text — escape it so the regex matches exactly
|
|
218
|
+
(<code>/wrr/test.js</code> → <code>"\\/wrr\\/test\\.js"</code>)
|
|
219
|
+
</label>
|
|
220
|
+
<label style="display:block; cursor:pointer; font-weight:500; color:#475569;">
|
|
221
|
+
<input type="radio" name="convertMode" value="regex" style="margin-right:8px;">
|
|
222
|
+
Standard regex — already a pattern, just JSON-encode it
|
|
223
|
+
(<code>\.js$</code> → <code>"\\.js$"</code>)
|
|
224
|
+
</label>
|
|
225
|
+
</div>
|
|
226
|
+
<div class="checkbox-group">
|
|
227
|
+
<input type="checkbox" id="convertRegexAnd">
|
|
228
|
+
<label for="convertRegexAnd">Add <code>"regex_and": true</code> (all patterns must match — default is OR)</label>
|
|
229
|
+
</div>
|
|
230
|
+
<button class="button" id="convertBtn" type="button">Generate JSON</button>
|
|
317
231
|
<div id="convertOutput"></div>
|
|
318
232
|
</div>
|
|
319
233
|
|
|
320
|
-
<!-- Validate Tab -->
|
|
321
|
-
<div id="
|
|
322
|
-
<div class="checkbox-group" style="background
|
|
323
|
-
<input type="checkbox" id="
|
|
324
|
-
<label for="
|
|
234
|
+
<!-- Validate & Test Tab -->
|
|
235
|
+
<div id="test" class="tab-content">
|
|
236
|
+
<div class="checkbox-group" style="background:#f0f9ff; padding:12px 14px; border-radius:8px; border:1px solid #bae6fd;">
|
|
237
|
+
<input type="checkbox" id="inputIsJson">
|
|
238
|
+
<label for="inputIsJson">Pattern input is a JSON config snippet
|
|
239
|
+
<span class="hint">(<code>{"filterRegex": [...], "regex_and": true}</code>)</span>
|
|
240
|
+
</label>
|
|
325
241
|
</div>
|
|
326
242
|
|
|
327
243
|
<div class="input-group">
|
|
328
|
-
<label for="
|
|
329
|
-
|
|
244
|
+
<label for="testPatterns">Pattern(s)
|
|
245
|
+
<span class="hint">— one regex per line, or a JSON snippet if the box above is checked</span>
|
|
246
|
+
</label>
|
|
247
|
+
<textarea id="testPatterns" placeholder="^https?://[a-z]{8,15}\.com/.*\.(?:css|js)$"></textarea>
|
|
330
248
|
</div>
|
|
331
249
|
|
|
332
|
-
<
|
|
250
|
+
<div class="checkbox-group">
|
|
251
|
+
<input type="checkbox" id="testRegexAnd">
|
|
252
|
+
<label for="testRegexAnd">Use AND logic (<code>regex_and</code>) — ignored when JSON input supplies its own</label>
|
|
253
|
+
</div>
|
|
333
254
|
|
|
334
|
-
<div
|
|
255
|
+
<div class="input-group">
|
|
256
|
+
<label for="testUrls">Test URLs
|
|
257
|
+
<span class="hint">— one per line; paste real request URLs from your debug logs</span>
|
|
258
|
+
</label>
|
|
259
|
+
<textarea id="testUrls" placeholder="https://ads.example.com/track.js https://example.com/index.html"></textarea>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<button class="button" id="testBtn" type="button">Validate & Test</button>
|
|
263
|
+
<div id="testOutput"></div>
|
|
335
264
|
</div>
|
|
336
265
|
</div>
|
|
337
266
|
</div>
|
|
338
267
|
|
|
339
268
|
<script>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const tabs = document.querySelectorAll('.tab');
|
|
344
|
-
tabs.forEach(tab => {
|
|
345
|
-
tab.addEventListener('click', function() {
|
|
346
|
-
const tabName = this.getAttribute('data-tab');
|
|
347
|
-
switchTab(tabName, this);
|
|
348
|
-
});
|
|
269
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
270
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
271
|
+
tab.addEventListener('click', function () { switchTab(this.getAttribute('data-tab'), this); });
|
|
349
272
|
});
|
|
350
|
-
|
|
351
|
-
// Button event listeners
|
|
352
273
|
document.getElementById('convertBtn').addEventListener('click', convertToJSON);
|
|
353
|
-
document.getElementById('
|
|
354
|
-
document.getElementById('useStandardRegex').addEventListener('change', toggleRegexInput);
|
|
355
|
-
|
|
356
|
-
// Enter key support
|
|
357
|
-
document.getElementById('standardRegex').addEventListener('keypress', function(e) {
|
|
358
|
-
if (e.key === 'Enter') convertToJSON();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
document.getElementById('regexToValidate').addEventListener('keypress', function(e) {
|
|
362
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
363
|
-
e.preventDefault();
|
|
364
|
-
validateRegex();
|
|
365
|
-
}
|
|
366
|
-
});
|
|
274
|
+
document.getElementById('testBtn').addEventListener('click', validateAndTest);
|
|
367
275
|
});
|
|
368
276
|
|
|
369
277
|
function switchTab(tabName, clickedTab) {
|
|
370
|
-
|
|
371
|
-
document.querySelectorAll('.tab').forEach(tab => {
|
|
372
|
-
tab.classList.remove('active');
|
|
373
|
-
});
|
|
278
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
374
279
|
clickedTab.classList.add('active');
|
|
375
|
-
|
|
376
|
-
// Update content
|
|
377
|
-
document.querySelectorAll('.tab-content').forEach(content => {
|
|
378
|
-
content.classList.remove('active');
|
|
379
|
-
});
|
|
280
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
380
281
|
document.getElementById(tabName).classList.add('active');
|
|
381
282
|
}
|
|
382
283
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
textarea.placeholder = 'Enter standard regex pattern, e.g., ^https?://.*';
|
|
389
|
-
} else {
|
|
390
|
-
textarea.placeholder = 'Enter JSON regex, e.g., {"pattern": "^https?://.*", "flags": "gi"}';
|
|
391
|
-
}
|
|
392
|
-
textarea.value = '';
|
|
284
|
+
// Mirror nwss getCompiledRegex(): strip a surrounding /.../ then compile
|
|
285
|
+
// with NO flags (case-sensitive). Throws on an invalid pattern.
|
|
286
|
+
function compilePattern(pattern) {
|
|
287
|
+
const stripped = pattern.replace(/^\/(.*)\/$/, '$1');
|
|
288
|
+
return new RegExp(stripped);
|
|
393
289
|
}
|
|
394
290
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const protocol = protocols[i % protocols.length];
|
|
423
|
-
const domain = domains[i % domains.length];
|
|
424
|
-
const ext = extensions[i % extensions.length];
|
|
425
|
-
|
|
426
|
-
// Build the path part
|
|
427
|
-
let path = '';
|
|
428
|
-
|
|
429
|
-
// Check for specific length requirements in path
|
|
430
|
-
const pathMatch = pattern.match(/\[A-Za-z0-9[^\]]*\]\{(\d+),?(\d*)\}/);
|
|
431
|
-
if (pathMatch) {
|
|
432
|
-
const minLen = parseInt(pathMatch[1]);
|
|
433
|
-
const maxLen = pathMatch[2] ? parseInt(pathMatch[2]) : minLen + 20;
|
|
434
|
-
path = generateRandomString(minLen, maxLen);
|
|
435
|
-
} else {
|
|
436
|
-
path = 'api/v1/users/profile/data';
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
let url = `${protocol}${domain}.${ext}/${path}`;
|
|
440
|
-
|
|
441
|
-
// Add query params if pattern requires them
|
|
442
|
-
if (pattern.includes('\\?')) {
|
|
443
|
-
// Check for minimum length requirements in query
|
|
444
|
-
const queryMatch = pattern.match(/\\\?\.\{(\d+),?(\d*)\}/);
|
|
445
|
-
if (queryMatch) {
|
|
446
|
-
const minLen = parseInt(queryMatch[1]);
|
|
447
|
-
const maxLen = queryMatch[2] ? parseInt(queryMatch[2]) : minLen + 10;
|
|
448
|
-
url += '?' + generateRandomString(minLen, maxLen);
|
|
449
|
-
} else if (pattern.match(/\?\.\{(\d+),/)) {
|
|
450
|
-
// Handle any {n,} pattern after ?
|
|
451
|
-
const match = pattern.match(/\?\.\{(\d+),/);
|
|
452
|
-
const minLen = parseInt(match[1]);
|
|
453
|
-
url += '?param=' + generateRandomString(Math.max(minLen - 6, 1), minLen + 15);
|
|
454
|
-
} else {
|
|
455
|
-
url += '?param=value&key=data';
|
|
456
|
-
}
|
|
457
|
-
} else if (pattern.includes('&.{')) {
|
|
458
|
-
// Handle &.{n,} case
|
|
459
|
-
const ampMatch = pattern.match(/&\.\{(\d+),/);
|
|
460
|
-
if (ampMatch) {
|
|
461
|
-
const minLen = parseInt(ampMatch[1]);
|
|
462
|
-
url += '&' + generateRandomString(minLen, minLen + 15);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
examples.push(url);
|
|
467
|
-
}
|
|
468
|
-
} else {
|
|
469
|
-
// General URL patterns
|
|
470
|
-
const protocols = pattern.includes('https?') ? ['http://', 'https://'] : ['https://'];
|
|
471
|
-
const urls = [
|
|
472
|
-
'www.example.com/path/to/resource',
|
|
473
|
-
'subdomain.test.com/api/endpoint',
|
|
474
|
-
'example.org/page/section/item',
|
|
475
|
-
'demo.net/products/category',
|
|
476
|
-
'sample.io/user/profile/settings'
|
|
477
|
-
];
|
|
478
|
-
|
|
479
|
-
urls.forEach((url, i) => {
|
|
480
|
-
if (i < 5) {
|
|
481
|
-
let fullUrl = protocols[i % protocols.length] + url;
|
|
482
|
-
|
|
483
|
-
// Check for length requirements
|
|
484
|
-
const lengthMatch = pattern.match(/\{(\d+),?(\d*)\}/);
|
|
485
|
-
if (lengthMatch && !pattern.includes('\\?')) {
|
|
486
|
-
const minLen = parseInt(lengthMatch[1]);
|
|
487
|
-
const maxLen = lengthMatch[2] ? parseInt(lengthMatch[2]) : minLen + 10;
|
|
488
|
-
fullUrl += '/' + generateRandomString(minLen, maxLen);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
examples.push(fullUrl);
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Test all generated examples against the regex
|
|
497
|
-
const validExamples = [];
|
|
498
|
-
for (const example of examples) {
|
|
499
|
-
try {
|
|
500
|
-
// Reset regex lastIndex in case of global flag
|
|
501
|
-
regex.lastIndex = 0;
|
|
502
|
-
if (regex.test(example)) {
|
|
503
|
-
validExamples.push(example);
|
|
504
|
-
}
|
|
505
|
-
} catch (e) {
|
|
506
|
-
console.error('Error testing example:', e);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// If we don't have enough valid examples, try more specific ones with proper lengths
|
|
511
|
-
if (validExamples.length < 5) {
|
|
512
|
-
const moreSpecific = [];
|
|
513
|
-
for (let i = 0; i < 10; i++) {
|
|
514
|
-
const protocol = i % 2 === 0 ? 'https://' : 'http://';
|
|
515
|
-
const domain = ['test-api', 'web-server', 'my-app', 'demo-site', 'api-gateway'][i % 5];
|
|
516
|
-
const ext = i % 2 === 0 ? 'com' : 'org';
|
|
517
|
-
|
|
518
|
-
// Generate path with proper length if specified
|
|
519
|
-
let path = 'Path_';
|
|
520
|
-
const pathMatch = pattern.match(/\[A-Za-z0-9[^\]]*\]\{(\d+),?(\d*)\}/);
|
|
521
|
-
if (pathMatch) {
|
|
522
|
-
const minLen = parseInt(pathMatch[1]);
|
|
523
|
-
const maxLen = pathMatch[2] ? parseInt(pathMatch[2]) : minLen + 20;
|
|
524
|
-
path = generateRandomString(minLen, maxLen);
|
|
525
|
-
} else {
|
|
526
|
-
path += generateRandomString(10, 30);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
let url = `${protocol}${domain}.${ext}/${path}`;
|
|
530
|
-
|
|
531
|
-
// Add query with proper length if needed
|
|
532
|
-
if (pattern.includes('\\?')) {
|
|
533
|
-
const queryMatch = pattern.match(/\\\?\.\{(\d+),?(\d*)\}/);
|
|
534
|
-
if (queryMatch) {
|
|
535
|
-
const minLen = parseInt(queryMatch[1]);
|
|
536
|
-
url += '?q=' + generateRandomString(Math.max(minLen - 2, 1), minLen + 10);
|
|
537
|
-
} else if (pattern.match(/\?\.\{(\d+),/)) {
|
|
538
|
-
const match = pattern.match(/\?\.\{(\d+),/);
|
|
539
|
-
const minLen = parseInt(match[1]);
|
|
540
|
-
url += '?' + generateRandomString(minLen, minLen + 15);
|
|
541
|
-
} else {
|
|
542
|
-
url += '?param=' + generateRandomString(10, 20);
|
|
543
|
-
}
|
|
544
|
-
} else if (pattern.includes('&.{')) {
|
|
545
|
-
const ampMatch = pattern.match(/&\.\{(\d+),/);
|
|
546
|
-
if (ampMatch) {
|
|
547
|
-
const minLen = parseInt(ampMatch[1]);
|
|
548
|
-
url += '&' + generateRandomString(minLen, minLen + 15);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
moreSpecific.push(url);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
for (const url of moreSpecific) {
|
|
556
|
-
regex.lastIndex = 0;
|
|
557
|
-
if (regex.test(url) && validExamples.length < 5) {
|
|
558
|
-
validExamples.push(url);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
return validExamples.length > 0 ? validExamples.slice(0, 5) : [
|
|
564
|
-
'Pattern is very specific - generating custom examples...',
|
|
565
|
-
'Try: https://example.com/Page_Name/path?query=value12345',
|
|
566
|
-
'Try: http://test.org/API_endpoint/data?param=longvalue123',
|
|
567
|
-
'Manual testing recommended for this pattern',
|
|
568
|
-
'Validation successful'
|
|
569
|
-
];
|
|
570
|
-
} else if (pattern.includes('@')) {
|
|
571
|
-
examples.push(
|
|
572
|
-
'user@example.com',
|
|
573
|
-
'john.doe@company.org',
|
|
574
|
-
'admin+tag@subdomain.example.com',
|
|
575
|
-
'contact@business.co.uk',
|
|
576
|
-
'support123@service.io'
|
|
577
|
-
);
|
|
578
|
-
} else if (pattern.includes('\\d')) {
|
|
579
|
-
// Check for length requirements
|
|
580
|
-
const lengthMatch = pattern.match(/\{(\d+),?(\d*)\}/);
|
|
581
|
-
if (lengthMatch) {
|
|
582
|
-
const minLen = parseInt(lengthMatch[1]);
|
|
583
|
-
const maxLen = lengthMatch[2] ? parseInt(lengthMatch[2]) : minLen + 10;
|
|
584
|
-
for (let i = 0; i < 5; i++) {
|
|
585
|
-
examples.push(generateRandomString(minLen, maxLen));
|
|
586
|
-
}
|
|
587
|
-
} else {
|
|
588
|
-
examples.push(
|
|
589
|
-
'12345',
|
|
590
|
-
'2024-01-15',
|
|
591
|
-
'ID-9876-XY',
|
|
592
|
-
'Version 3.14.159',
|
|
593
|
-
'Order #00425'
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
} else if (pattern.includes('\\w')) {
|
|
597
|
-
// Check for length requirements
|
|
598
|
-
const lengthMatch = pattern.match(/\{(\d+),?(\d*)\}/);
|
|
599
|
-
if (lengthMatch) {
|
|
600
|
-
const minLen = parseInt(lengthMatch[1]);
|
|
601
|
-
const maxLen = lengthMatch[2] ? parseInt(lengthMatch[2]) : minLen + 10;
|
|
602
|
-
for (let i = 0; i < 5; i++) {
|
|
603
|
-
examples.push(generateRandomString(minLen, maxLen));
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
examples.push(
|
|
607
|
-
'HelloWorld',
|
|
608
|
-
'user_name_123',
|
|
609
|
-
'CamelCaseExample',
|
|
610
|
-
'snake_case_text',
|
|
611
|
-
'MixedContent2024'
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
} else {
|
|
615
|
-
// Check for any length requirements in the pattern
|
|
616
|
-
const lengthMatch = pattern.match(/\{(\d+),?(\d*)\}/);
|
|
617
|
-
if (lengthMatch) {
|
|
618
|
-
const minLen = parseInt(lengthMatch[1]);
|
|
619
|
-
const maxLen = lengthMatch[2] ? parseInt(lengthMatch[2]) : minLen + 15;
|
|
620
|
-
for (let i = 0; i < 5; i++) {
|
|
621
|
-
examples.push(generateRandomString(minLen, maxLen));
|
|
622
|
-
}
|
|
623
|
-
} else {
|
|
624
|
-
// Generic examples
|
|
625
|
-
examples.push(
|
|
626
|
-
'example text',
|
|
627
|
-
'Sample String 123',
|
|
628
|
-
'test-data-here',
|
|
629
|
-
'Another_Example',
|
|
630
|
-
'final.test.value'
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Filter examples that actually match the regex
|
|
636
|
-
const validExamples = examples.filter(ex => {
|
|
637
|
-
try {
|
|
638
|
-
regex.lastIndex = 0;
|
|
639
|
-
return regex.test(ex);
|
|
640
|
-
} catch (e) {
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
return validExamples.length > 0 ? validExamples.slice(0, 5) : [
|
|
646
|
-
'No automatic examples could be generated.',
|
|
647
|
-
'The pattern may be too specific.',
|
|
648
|
-
'Try testing with your own strings.',
|
|
649
|
-
'Enter test strings manually.',
|
|
650
|
-
'Pattern validation successful.'
|
|
651
|
-
];
|
|
652
|
-
} catch (error) {
|
|
653
|
-
console.error('Error in generateExamples:', error);
|
|
654
|
-
return ['Error generating examples'];
|
|
291
|
+
// Escape every regex metacharacter so the input matches literally.
|
|
292
|
+
// Forward slash isn't special in a RegExp built from a string, but we
|
|
293
|
+
// escape it too (\/) to match adblock convention and what the scanner
|
|
294
|
+
// configs use. Example: /wrr/test.js -> \/wrr\/test\.js , which
|
|
295
|
+
// JSON.stringify then renders as "\\/wrr\\/test\\.js".
|
|
296
|
+
function escapeRegexLiteral(text) {
|
|
297
|
+
return text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Split a textarea into non-empty, trimmed lines.
|
|
301
|
+
function toLines(text) {
|
|
302
|
+
return text.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function escapeHtml(text) {
|
|
306
|
+
const div = document.createElement('div');
|
|
307
|
+
div.textContent = String(text);
|
|
308
|
+
return div.innerHTML;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Compile a list of pattern strings, returning { compiled, errors }.
|
|
312
|
+
// compiled is [{ source, re }]; errors is [{ source, message }].
|
|
313
|
+
function compileAll(patterns) {
|
|
314
|
+
const compiled = [], errors = [];
|
|
315
|
+
for (const p of patterns) {
|
|
316
|
+
try { compiled.push({ source: p, re: compilePattern(p) }); }
|
|
317
|
+
catch (e) { errors.push({ source: p, message: e.message }); }
|
|
655
318
|
}
|
|
319
|
+
return { compiled, errors };
|
|
656
320
|
}
|
|
657
321
|
|
|
658
322
|
function convertToJSON() {
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
323
|
+
const out = document.getElementById('convertOutput');
|
|
324
|
+
const rawPatterns = toLines(document.getElementById('convertPatterns').value);
|
|
325
|
+
const useAnd = document.getElementById('convertRegexAnd').checked;
|
|
326
|
+
const escapeLiteral = document.querySelector('input[name="convertMode"]:checked').value === 'literal';
|
|
327
|
+
|
|
328
|
+
if (rawPatterns.length === 0) {
|
|
329
|
+
out.innerHTML = '<div class="error">Please enter at least one line of text or a regex pattern.</div>';
|
|
664
330
|
return;
|
|
665
331
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
332
|
+
|
|
333
|
+
// In literal mode, escape each line into a regex that matches it
|
|
334
|
+
// exactly; otherwise treat each line as an already-written regex.
|
|
335
|
+
const patterns = escapeLiteral ? rawPatterns.map(escapeRegexLiteral) : rawPatterns;
|
|
336
|
+
|
|
337
|
+
const { compiled, errors } = compileAll(patterns);
|
|
338
|
+
|
|
339
|
+
// Per-pattern report. Show the JSON-string form (JSON.stringify ->
|
|
340
|
+
// doubled backslashes, with quotes) since that's what actually goes
|
|
341
|
+
// in config.json — never the single-backslash "engine" form, which
|
|
342
|
+
// would be under-escaped if pasted into JSON. In literal mode also
|
|
343
|
+
// show the raw input -> JSON form so the transformation is clear.
|
|
344
|
+
let statusHtml = '';
|
|
345
|
+
patterns.forEach((p, i) => {
|
|
346
|
+
const bad = errors.find(e => e.source === p);
|
|
347
|
+
const jsonForm = JSON.stringify(p); // e.g. "\\/wrr\\/test\\.js"
|
|
348
|
+
const shown = escapeLiteral
|
|
349
|
+
? `${escapeHtml(rawPatterns[i])} → ${escapeHtml(jsonForm)}`
|
|
350
|
+
: escapeHtml(jsonForm);
|
|
351
|
+
statusHtml += bad
|
|
352
|
+
? `<div class="pattern-status bad">✗ ${shown} — ${escapeHtml(bad.message)}</div>`
|
|
353
|
+
: `<div class="pattern-status ok">✓ ${shown}</div>`;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (errors.length > 0) {
|
|
357
|
+
out.innerHTML = `
|
|
688
358
|
<div class="output-section">
|
|
689
|
-
<h3
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
${examples.map(ex => `<li>${escapeHtml(ex)}</li>`).join('')}
|
|
695
|
-
</ul>
|
|
696
|
-
|
|
697
|
-
<div class="success">? Regex successfully converted to JSON format!</div>
|
|
698
|
-
</div>
|
|
699
|
-
`;
|
|
700
|
-
} catch (error) {
|
|
701
|
-
outputDiv.innerHTML = `<div class="error">? Invalid regex pattern: ${escapeHtml(error.message)}</div>`;
|
|
359
|
+
<h3>Pattern check</h3>
|
|
360
|
+
${statusHtml}
|
|
361
|
+
<div class="error">Fix the invalid pattern(s) above before generating JSON.</div>
|
|
362
|
+
</div>`;
|
|
363
|
+
return;
|
|
702
364
|
}
|
|
703
|
-
}
|
|
704
365
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
366
|
+
// Build the config object. JSON.stringify performs the escaping that
|
|
367
|
+
// trips people up (e.g. \\/ , \\? ) — this is the canonical form.
|
|
368
|
+
const config = { filterRegex: patterns };
|
|
369
|
+
if (useAnd) config.regex_and = true;
|
|
370
|
+
const json = JSON.stringify(config, null, 2);
|
|
371
|
+
|
|
372
|
+
out.innerHTML = `
|
|
373
|
+
<div class="output-section">
|
|
374
|
+
<h3>Pattern check</h3>
|
|
375
|
+
${statusHtml}
|
|
376
|
+
<h3>JSON for your site config</h3>
|
|
377
|
+
<div class="output-box">${escapeHtml(json)}</div>
|
|
378
|
+
<div class="success">✓ ${patterns.length} pattern(s) compiled successfully. Paste the keys above into your site entry.</div>
|
|
379
|
+
</div>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function validateAndTest() {
|
|
383
|
+
const out = document.getElementById('testOutput');
|
|
384
|
+
const isJson = document.getElementById('inputIsJson').checked;
|
|
385
|
+
const raw = document.getElementById('testPatterns').value.trim();
|
|
386
|
+
let useAnd = document.getElementById('testRegexAnd').checked;
|
|
387
|
+
|
|
388
|
+
if (!raw) {
|
|
389
|
+
out.innerHTML = '<div class="error">Please enter pattern(s) to validate.</div>';
|
|
712
390
|
return;
|
|
713
391
|
}
|
|
714
|
-
|
|
392
|
+
|
|
393
|
+
// Resolve the pattern list (+ regex_and from JSON if provided).
|
|
394
|
+
let patterns;
|
|
715
395
|
try {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
regex = new RegExp(pattern);
|
|
396
|
+
if (isJson) {
|
|
397
|
+
const parsed = JSON.parse(raw);
|
|
398
|
+
const fr = parsed.filterRegex;
|
|
399
|
+
if (fr === undefined) throw new Error('JSON has no "filterRegex" key.');
|
|
400
|
+
patterns = Array.isArray(fr) ? fr.slice() : [fr];
|
|
401
|
+
if (typeof parsed.regex_and === 'boolean') useAnd = parsed.regex_and;
|
|
723
402
|
} else {
|
|
724
|
-
|
|
725
|
-
if (regexInput.startsWith('{')) {
|
|
726
|
-
// Full JSON format
|
|
727
|
-
const jsonRegex = JSON.parse(regexInput);
|
|
728
|
-
|
|
729
|
-
if (jsonRegex.filterRegex && Array.isArray(jsonRegex.filterRegex)) {
|
|
730
|
-
pattern = jsonRegex.filterRegex[0];
|
|
731
|
-
} else if (jsonRegex.pattern) {
|
|
732
|
-
pattern = jsonRegex.pattern;
|
|
733
|
-
} else {
|
|
734
|
-
throw new Error('Invalid JSON format. Expected {"filterRegex": ["pattern"]}');
|
|
735
|
-
}
|
|
736
|
-
} else if (regexInput.startsWith('"') && regexInput.endsWith('"')) {
|
|
737
|
-
// Just a quoted string - parse as JSON string
|
|
738
|
-
pattern = JSON.parse(regexInput);
|
|
739
|
-
} else {
|
|
740
|
-
// Raw pattern without quotes or JSON wrapper
|
|
741
|
-
outputDiv.innerHTML = `
|
|
742
|
-
<div class="error">
|
|
743
|
-
? Invalid format for JSON mode.<br><br>
|
|
744
|
-
You can use one of these formats:<br>
|
|
745
|
-
<div class="output-box" style="margin-top: 10px; background: #fef3c7;">
|
|
746
|
-
1. Full JSON: {"filterRegex": ["${escapeHtml(regexInput)}"]}<br><br>
|
|
747
|
-
2. Quoted string: "${escapeHtml(regexInput)}"
|
|
748
|
-
</div>
|
|
749
|
-
<br>
|
|
750
|
-
?? <strong>Tip:</strong> Check the "Use Standard Regex" box above to validate regex patterns directly without quotes.
|
|
751
|
-
</div>
|
|
752
|
-
`;
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
regex = new RegExp(pattern);
|
|
403
|
+
patterns = toLines(raw);
|
|
757
404
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
405
|
+
} catch (e) {
|
|
406
|
+
out.innerHTML = `<div class="error">Invalid JSON: ${escapeHtml(e.message)}<br><br>Expected: <code>{"filterRegex": ["pattern"], "regex_and": false}</code></div>`;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (patterns.length === 0) {
|
|
411
|
+
out.innerHTML = '<div class="error">No patterns found in the input.</div>';
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const { compiled, errors } = compileAll(patterns);
|
|
416
|
+
|
|
417
|
+
let statusHtml = '';
|
|
418
|
+
for (const p of patterns) {
|
|
419
|
+
const bad = errors.find(e => e.source === p);
|
|
420
|
+
statusHtml += bad
|
|
421
|
+
? `<div class="pattern-status bad">✗ ${escapeHtml(p)} — ${escapeHtml(bad.message)}</div>`
|
|
422
|
+
: `<div class="pattern-status ok">✓ ${escapeHtml(p)}</div>`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const logicLabel = useAnd ? 'AND (all patterns must match)' : 'OR (any pattern matches)';
|
|
426
|
+
|
|
427
|
+
if (errors.length > 0) {
|
|
428
|
+
out.innerHTML = `
|
|
763
429
|
<div class="output-section">
|
|
764
|
-
<h3
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
<h3>? Example Matches:</h3>
|
|
771
|
-
<ul class="examples-list">
|
|
772
|
-
${examples.map(ex => `<li>${escapeHtml(ex)}</li>`).join('')}
|
|
773
|
-
</ul>
|
|
774
|
-
|
|
775
|
-
<div class="success">? Regex is valid!</div>
|
|
776
|
-
</div>
|
|
777
|
-
`;
|
|
778
|
-
} catch (error) {
|
|
779
|
-
if (error instanceof SyntaxError && !useStandard) {
|
|
780
|
-
outputDiv.innerHTML = `<div class="error">? Invalid JSON syntax: ${escapeHtml(error.message)}<br><br>Expected format: {"filterRegex": ["your-regex-pattern"]} or "your-regex-pattern"</div>`;
|
|
781
|
-
} else {
|
|
782
|
-
outputDiv.innerHTML = `<div class="error">? Invalid regex: ${escapeHtml(error.message)}</div>`;
|
|
783
|
-
}
|
|
430
|
+
<h3>Pattern check — logic: ${logicLabel}</h3>
|
|
431
|
+
${statusHtml}
|
|
432
|
+
<div class="error">Fix the invalid pattern(s) above before testing.</div>
|
|
433
|
+
</div>`;
|
|
434
|
+
return;
|
|
784
435
|
}
|
|
785
|
-
}
|
|
786
436
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
437
|
+
const urls = toLines(document.getElementById('testUrls').value);
|
|
438
|
+
|
|
439
|
+
// No test URLs: report validity only.
|
|
440
|
+
if (urls.length === 0) {
|
|
441
|
+
out.innerHTML = `
|
|
442
|
+
<div class="output-section">
|
|
443
|
+
<h3>Pattern check — logic: ${logicLabel}</h3>
|
|
444
|
+
${statusHtml}
|
|
445
|
+
<div class="success">✓ ${patterns.length} pattern(s) valid. Add test URLs above to see what they match.</div>
|
|
446
|
+
</div>`;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Evaluate each URL exactly as nwss would.
|
|
451
|
+
let matchCount = 0;
|
|
452
|
+
const rows = urls.map(url => {
|
|
453
|
+
const hitIdx = [];
|
|
454
|
+
compiled.forEach((c, i) => {
|
|
455
|
+
c.re.lastIndex = 0;
|
|
456
|
+
if (c.re.test(url)) hitIdx.push(i);
|
|
457
|
+
});
|
|
458
|
+
const matched = useAnd ? hitIdx.length === compiled.length : hitIdx.length > 0;
|
|
459
|
+
if (matched) matchCount++;
|
|
460
|
+
const which = hitIdx.length
|
|
461
|
+
? hitIdx.map(i => '#' + (i + 1)).join(', ')
|
|
462
|
+
: '—';
|
|
463
|
+
return `
|
|
464
|
+
<tr class="${matched ? 'match' : 'nomatch'}">
|
|
465
|
+
<td>${escapeHtml(url)}</td>
|
|
466
|
+
<td><span class="badge ${matched ? 'match' : 'nomatch'}">${matched ? 'MATCH' : 'NO MATCH'}</span></td>
|
|
467
|
+
<td>${which}</td>
|
|
468
|
+
</tr>`;
|
|
469
|
+
}).join('');
|
|
470
|
+
|
|
471
|
+
// Numbered legend so the "matched pattern #" column is readable.
|
|
472
|
+
const legend = compiled.map((c, i) => `<div class="pattern-status ok">#${i + 1}: ${escapeHtml(c.source)}</div>`).join('');
|
|
473
|
+
|
|
474
|
+
out.innerHTML = `
|
|
475
|
+
<div class="output-section">
|
|
476
|
+
<h3>Pattern check — logic: ${logicLabel}</h3>
|
|
477
|
+
${legend}
|
|
478
|
+
<div class="summary">${matchCount} of ${urls.length} URL(s) matched.</div>
|
|
479
|
+
<table class="results">
|
|
480
|
+
<thead><tr><th>URL</th><th>Result</th><th>Matched pattern</th></tr></thead>
|
|
481
|
+
<tbody>${rows}</tbody>
|
|
482
|
+
</table>
|
|
483
|
+
</div>`;
|
|
791
484
|
}
|
|
792
485
|
</script>
|
|
793
486
|
</body>
|
|
794
|
-
</html>
|
|
487
|
+
</html>
|