@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.
@@ -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 - Converter & Validator</title>
6
+ <title>Regex Tools - filterRegex Builder &amp; 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.95);
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
- font-size: 2.5em;
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
- .header p {
44
- opacity: 0.9;
45
- font-size: 1.1em;
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: 20px;
61
+ padding: 18px;
57
62
  text-align: center;
58
63
  cursor: pointer;
59
64
  background: transparent;
60
65
  border: none;
61
- font-size: 1.1em;
66
+ font-size: 1.05em;
62
67
  font-weight: 600;
63
68
  color: #64748b;
64
- transition: all 0.3s ease;
69
+ transition: all 0.25s ease;
65
70
  position: relative;
66
71
  }
67
-
68
- .tab:hover {
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
- .tab-content {
94
- display: none;
95
- animation: fadeIn 0.3s ease;
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: 10px;
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: 15px;
98
+ padding: 13px;
118
99
  border: 2px solid #e2e8f0;
119
100
  border-radius: 10px;
120
- font-size: 1em;
101
+ font-size: 0.95em;
121
102
  font-family: 'Consolas', 'Monaco', monospace;
122
- transition: all 0.3s ease;
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.1);
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: 20px;
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: 15px 40px;
123
+ padding: 13px 36px;
161
124
  border-radius: 10px;
162
- font-size: 1.1em;
125
+ font-size: 1.05em;
163
126
  font-weight: 600;
164
127
  cursor: pointer;
165
- transition: all 0.3s ease;
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:hover {
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: 40px;
180
- padding: 25px;
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: 20px;
144
+ padding: 16px;
195
145
  border-radius: 8px;
196
146
  border: 1px solid #e2e8f0;
197
147
  font-family: 'Consolas', 'Monaco', monospace;
198
- margin-bottom: 20px;
148
+ white-space: pre-wrap;
199
149
  word-break: break-all;
200
- transition: all 0.3s ease;
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
- .success {
237
- background: #e6fffa;
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
- @keyframes fadeIn {
246
- from {
247
- opacity: 0;
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
- @keyframes fadeInDown {
257
- from {
258
- opacity: 0;
259
- transform: translateY(-20px);
260
- }
261
- to {
262
- opacity: 1;
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
- font-size: 1.8em;
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>?? Regex Tools</h1>
295
- <p>Convert and Validate Regular Expressions with Ease</p>
187
+ <h1>🔧 Regex Tools</h1>
188
+ <p>Build &amp; 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
- ?? Convert to JSON
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 &amp; 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="standardRegex">Enter Standard Regex Pattern:</label>
312
- <input type="text" id="standardRegex" placeholder="e.g., ^https?://.*\.example\.com/.*$">
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&#10;https://ads.example.com/track.js"></textarea>
313
212
  </div>
314
-
315
- <button class="button" id="convertBtn">Convert to JSON</button>
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> &rarr; <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> &rarr; <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="validate" class="tab-content">
322
- <div class="checkbox-group" style="background: #f0f9ff; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 2px solid #0ea5e9;">
323
- <input type="checkbox" id="useStandardRegex">
324
- <label for="useStandardRegex"><strong>?? Check this box to validate Standard Regex</strong> (uncheck for JSON format)</label>
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="regexToValidate">Enter Regex Pattern:</label>
329
- <textarea id="regexToValidate" placeholder='?? IMPORTANT: Check the box above if entering standard regex!&#10;&#10;For JSON (box unchecked): {"filterRegex": ["^https?:\\\\/\\\\/.*"]}&#10;For Standard (box checked): ^https?:\\/\\/.*'></textarea>
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
- <button class="button" id="validateBtn">Validate Regex</button>
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 id="validateOutput"></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&#10;https://example.com/index.html"></textarea>
260
+ </div>
261
+
262
+ <button class="button" id="testBtn" type="button">Validate &amp; Test</button>
263
+ <div id="testOutput"></div>
335
264
  </div>
336
265
  </div>
337
266
  </div>
338
267
 
339
268
  <script>
340
- // Use DOMContentLoaded to ensure all elements are loaded
341
- document.addEventListener('DOMContentLoaded', function() {
342
- // Tab switching with event delegation
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('validateBtn').addEventListener('click', validateRegex);
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
- // Update tabs
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
- function toggleRegexInput() {
384
- const checkbox = document.getElementById('useStandardRegex');
385
- const textarea = document.getElementById('regexToValidate');
386
-
387
- if (checkbox.checked) {
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
- function generateExamples(regex) {
396
- const examples = [];
397
-
398
- try {
399
- // Get the pattern string
400
- const pattern = regex.source || regex.pattern || regex.toString();
401
-
402
- // Helper function to generate random string of specific length
403
- function generateRandomString(minLen, maxLen) {
404
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-+=';
405
- const length = maxLen ? Math.floor(Math.random() * (maxLen - minLen + 1)) + minLen : minLen;
406
- let result = '';
407
- for (let i = 0; i < length; i++) {
408
- result += chars.charAt(Math.floor(Math.random() * chars.length));
409
- }
410
- return result;
411
- }
412
-
413
- if (pattern.includes('http')) {
414
- // Check for specific domain requirements
415
- if (pattern.includes('com|org')) {
416
- // Pattern requires .com or .org domains
417
- const protocols = pattern.includes('https?') ? ['https://', 'http://'] : ['https://'];
418
- const domains = ['example', 'test-site', 'web-app', 'api-server', 'my-service'];
419
- const extensions = ['com', 'org'];
420
-
421
- for (let i = 0; i < 5; i++) {
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 standardRegex = document.getElementById('standardRegex').value;
660
- const outputDiv = document.getElementById('convertOutput');
661
-
662
- if (!standardRegex) {
663
- outputDiv.innerHTML = '<div class="error">Please enter a regex pattern</div>';
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
- try {
668
- // Remove surrounding quotes if present
669
- let cleanPattern = standardRegex.trim();
670
- if ((cleanPattern.startsWith('"') && cleanPattern.endsWith('"')) ||
671
- (cleanPattern.startsWith("'") && cleanPattern.endsWith("'"))) {
672
- cleanPattern = cleanPattern.slice(1, -1);
673
- }
674
-
675
- // Test if the regex is valid first (using the cleaned pattern)
676
- const testRegex = new RegExp(cleanPattern);
677
-
678
- // For JSON string format: the input pattern stays as is
679
- // JSON.stringify will handle the escaping
680
- const jsonOutput = {
681
- filterRegex: [cleanPattern]
682
- };
683
-
684
- // Generate examples
685
- const examples = generateExamples(testRegex);
686
-
687
- outputDiv.innerHTML = `
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])} &rarr; ${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>?? JSON Output:</h3>
690
- <div class="output-box">${JSON.stringify(jsonOutput, null, 2)}</div>
691
-
692
- <h3>? Example Matches:</h3>
693
- <ul class="examples-list">
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
- function validateRegex() {
706
- const useStandard = document.getElementById('useStandardRegex').checked;
707
- const regexInput = document.getElementById('regexToValidate').value.trim();
708
- const outputDiv = document.getElementById('validateOutput');
709
-
710
- if (!regexInput) {
711
- outputDiv.innerHTML = '<div class="error">Please enter a regex pattern to validate</div>';
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
- let regex;
717
- let pattern;
718
-
719
- if (useStandard) {
720
- // Use standard regex directly
721
- pattern = regexInput;
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
- // JSON mode - handle multiple formats
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
- // Generate examples
760
- const examples = generateExamples(regex);
761
-
762
- outputDiv.innerHTML = `
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>?? Validation Result:</h3>
765
- <div class="output-box">
766
- Pattern: ${escapeHtml(pattern)}<br>
767
- Type: ${useStandard ? 'Standard Regex' : 'JSON Regex'}
768
- </div>
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 &mdash; 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
- function escapeHtml(text) {
788
- const div = document.createElement('div');
789
- div.textContent = text;
790
- return div.innerHTML;
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 &mdash; 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 &mdash; 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>