mockaton 12.5.3 → 12.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client/app-store.js +49 -83
- package/src/client/app-store.test.js +1 -1
- package/src/client/app.js +4 -208
- package/src/client/dom-utils.js +4 -0
- package/src/client/payload-viewer.js +210 -0
- package/src/server/utils/UrlParsers.js +1 -0
- /package/src/client/{styles.css → app.css} +0 -0
package/package.json
CHANGED
package/src/client/app-store.js
CHANGED
|
@@ -20,23 +20,34 @@ export const store = {
|
|
|
20
20
|
|
|
21
21
|
collectProxied: false,
|
|
22
22
|
proxyFallback: '',
|
|
23
|
+
showProxyField: null,
|
|
23
24
|
get canProxy() {
|
|
24
25
|
return Boolean(store.proxyFallback)
|
|
25
26
|
},
|
|
26
27
|
|
|
27
28
|
getSyncVersion: api.getSyncVersion,
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const response = await
|
|
30
|
+
_action(action, onSuccess) {
|
|
31
|
+
Promise.try(async () => {
|
|
32
|
+
const response = await action()
|
|
32
33
|
if (!response.ok) throw response
|
|
34
|
+
return response
|
|
35
|
+
})
|
|
36
|
+
.then(onSuccess)
|
|
37
|
+
.catch(store.onError)
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async fetchState() {
|
|
41
|
+
store._action(api.getState, async response => {
|
|
33
42
|
Object.assign(store, await response.json())
|
|
43
|
+
|
|
44
|
+
if (store.showProxyField === null) // isFirstCall
|
|
45
|
+
store.showProxyField = Boolean(store.proxyFallback)
|
|
46
|
+
|
|
34
47
|
store.render()
|
|
35
|
-
}
|
|
36
|
-
catch (error) { store.onError(error) }
|
|
48
|
+
})
|
|
37
49
|
},
|
|
38
50
|
|
|
39
|
-
|
|
40
51
|
groupByMethod: initPreference('groupByMethod'),
|
|
41
52
|
toggleGroupByMethod() {
|
|
42
53
|
store.groupByMethod = !store.groupByMethod
|
|
@@ -53,71 +64,47 @@ export const store = {
|
|
|
53
64
|
return store.chosenLink.method && store.chosenLink.urlMask
|
|
54
65
|
},
|
|
55
66
|
|
|
56
|
-
|
|
57
67
|
async reset() {
|
|
58
|
-
|
|
59
|
-
const response = await api.reset()
|
|
60
|
-
if (!response.ok) throw response
|
|
68
|
+
store._action(api.reset, () => {
|
|
61
69
|
store.setChosenLink('', '')
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
catch (error) { store.onError(error) }
|
|
70
|
+
store.fetchState()
|
|
71
|
+
})
|
|
65
72
|
},
|
|
66
73
|
|
|
67
74
|
async bulkSelectByComment(value) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!response.ok) throw response
|
|
71
|
-
await store.fetchState()
|
|
72
|
-
}
|
|
73
|
-
catch (error) { store.onError(error) }
|
|
75
|
+
store._action(() => api.bulkSelectByComment(value),
|
|
76
|
+
store.fetchState)
|
|
74
77
|
},
|
|
75
78
|
|
|
76
|
-
|
|
77
79
|
async setGlobalDelay(value) {
|
|
78
|
-
|
|
79
|
-
const response = await api.setGlobalDelay(value)
|
|
80
|
-
if (!response.ok) throw response
|
|
80
|
+
store._action(() => api.setGlobalDelay(value), () => {
|
|
81
81
|
store.delay = value
|
|
82
|
-
}
|
|
83
|
-
catch (error) { store.onError(error) }
|
|
82
|
+
})
|
|
84
83
|
},
|
|
85
84
|
|
|
86
85
|
async setGlobalDelayJitter(value) {
|
|
87
|
-
|
|
88
|
-
const response = await api.setGlobalDelayJitter(value)
|
|
89
|
-
if (!response.ok) throw response
|
|
86
|
+
store._action(() => api.setGlobalDelayJitter(value), () => {
|
|
90
87
|
store.delayJitter = value
|
|
91
|
-
}
|
|
92
|
-
catch (error) { store.onError(error) }
|
|
88
|
+
})
|
|
93
89
|
},
|
|
94
90
|
|
|
95
91
|
async selectCookie(name) {
|
|
96
|
-
|
|
97
|
-
const response = await api.selectCookie(name)
|
|
98
|
-
if (!response.ok) throw response
|
|
92
|
+
store._action(() => api.selectCookie(name), async response => {
|
|
99
93
|
store.cookies = await response.json()
|
|
100
|
-
}
|
|
101
|
-
catch (error) { store.onError(error) }
|
|
94
|
+
})
|
|
102
95
|
},
|
|
103
96
|
|
|
104
97
|
async setProxyFallback(value) {
|
|
105
|
-
|
|
106
|
-
const response = await api.setProxyFallback(value)
|
|
107
|
-
if (!response.ok) throw response
|
|
98
|
+
store._action(() => api.setProxyFallback(value), () => {
|
|
108
99
|
store.proxyFallback = value
|
|
109
100
|
store.render()
|
|
110
|
-
}
|
|
111
|
-
catch (error) { store.onError(error) }
|
|
101
|
+
})
|
|
112
102
|
},
|
|
113
103
|
|
|
114
104
|
async setCollectProxied(checked) {
|
|
115
|
-
|
|
116
|
-
const response = await api.setCollectProxied(checked)
|
|
117
|
-
if (!response.ok) throw response
|
|
105
|
+
store._action(() => api.setCollectProxied(checked), () => {
|
|
118
106
|
store.collectProxied = checked
|
|
119
|
-
}
|
|
120
|
-
catch (error) { store.onError(error) }
|
|
107
|
+
})
|
|
121
108
|
},
|
|
122
109
|
|
|
123
110
|
|
|
@@ -180,65 +167,46 @@ export const store = {
|
|
|
180
167
|
},
|
|
181
168
|
|
|
182
169
|
async selectFile(file) {
|
|
183
|
-
|
|
184
|
-
const response = await api.select(file)
|
|
185
|
-
if (!response.ok) throw response
|
|
170
|
+
store._action(() => api.select(file), async response => {
|
|
186
171
|
const { method, urlMask } = parseFilename(file)
|
|
187
172
|
store.setBroker(await response.json())
|
|
188
173
|
store.setChosenLink(method, urlMask)
|
|
189
174
|
store.renderRow(method, urlMask)
|
|
190
|
-
}
|
|
191
|
-
catch (error) { store.onError(error) }
|
|
175
|
+
})
|
|
192
176
|
},
|
|
193
177
|
|
|
194
178
|
async toggle500(method, urlMask) {
|
|
195
|
-
|
|
196
|
-
const response = await api.toggle500(method, urlMask)
|
|
197
|
-
if (!response.ok) throw response
|
|
179
|
+
store._action(() => api.toggle500(method, urlMask), async response => {
|
|
198
180
|
store.setBroker(await response.json())
|
|
199
181
|
store.setChosenLink(method, urlMask)
|
|
200
182
|
store.renderRow(method, urlMask)
|
|
201
|
-
}
|
|
202
|
-
catch (error) { store.onError(error) }
|
|
183
|
+
})
|
|
203
184
|
},
|
|
204
185
|
|
|
205
186
|
async setProxied(method, urlMask, checked) {
|
|
206
|
-
|
|
207
|
-
const response = await api.setRouteIsProxied(method, urlMask, checked)
|
|
208
|
-
if (!response.ok) throw response
|
|
187
|
+
store._action(() => api.setRouteIsProxied(method, urlMask, checked), async response => {
|
|
209
188
|
store.setBroker(await response.json())
|
|
210
189
|
store.setChosenLink(method, urlMask)
|
|
211
190
|
store.renderRow(method, urlMask)
|
|
212
|
-
}
|
|
213
|
-
catch (error) { store.onError(error) }
|
|
191
|
+
})
|
|
214
192
|
},
|
|
215
193
|
|
|
216
194
|
async setDelayed(method, urlMask, checked) {
|
|
217
|
-
|
|
218
|
-
const response = await api.setRouteIsDelayed(method, urlMask, checked)
|
|
219
|
-
if (!response.ok) throw response
|
|
195
|
+
store._action(() => api.setRouteIsDelayed(method, urlMask, checked), async response => {
|
|
220
196
|
store.setBroker(await response.json())
|
|
221
|
-
}
|
|
222
|
-
catch (error) { store.onError(error) }
|
|
197
|
+
})
|
|
223
198
|
},
|
|
224
199
|
|
|
225
|
-
|
|
226
200
|
async setDelayedStatic(route, checked) {
|
|
227
|
-
|
|
228
|
-
const response = await api.setStaticRouteIsDelayed(route, checked)
|
|
229
|
-
if (!response.ok) throw response
|
|
201
|
+
store._action(() => api.setStaticRouteIsDelayed(route, checked), () => {
|
|
230
202
|
store.staticBrokers[route].delayed = checked
|
|
231
|
-
}
|
|
232
|
-
catch (error) { store.onError(error) }
|
|
203
|
+
})
|
|
233
204
|
},
|
|
234
205
|
|
|
235
206
|
async setStaticRouteStatus(route, status) {
|
|
236
|
-
|
|
237
|
-
const response = await api.setStaticRouteStatus(route, status)
|
|
238
|
-
if (!response.ok) throw response
|
|
207
|
+
store._action(() => api.setStaticRouteStatus(route, status), () => {
|
|
239
208
|
store.staticBrokers[route].status = status
|
|
240
|
-
}
|
|
241
|
-
catch (error) { store.onError(error) }
|
|
209
|
+
})
|
|
242
210
|
}
|
|
243
211
|
}
|
|
244
212
|
|
|
@@ -286,20 +254,18 @@ export function dittoSplitPaths(paths) {
|
|
|
286
254
|
const pathsInParts = paths.map(p => p.split('/').filter(Boolean))
|
|
287
255
|
|
|
288
256
|
for (let i = 1; i < paths.length; i++) {
|
|
289
|
-
const
|
|
290
|
-
const
|
|
257
|
+
const prev = pathsInParts[i - 1]
|
|
258
|
+
const curr = pathsInParts[i]
|
|
291
259
|
|
|
260
|
+
const min = Math.min(curr.length, prev.length)
|
|
292
261
|
let j = 0
|
|
293
|
-
while (
|
|
294
|
-
j < currParts.length &&
|
|
295
|
-
j < prevParts.length &&
|
|
296
|
-
currParts[j] === prevParts[j])
|
|
262
|
+
while (j < min && curr[j] === prev[j])
|
|
297
263
|
j++
|
|
298
264
|
|
|
299
265
|
if (!j) // no common dirs
|
|
300
266
|
result.push(['', paths[i]])
|
|
301
267
|
else {
|
|
302
|
-
const ditto = '/' +
|
|
268
|
+
const ditto = '/' + curr.slice(0, j).join('/') + '/'
|
|
303
269
|
result.push([ditto, paths[i].slice(ditto.length)])
|
|
304
270
|
}
|
|
305
271
|
}
|
package/src/client/app.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createElement as r,
|
|
3
3
|
createSvgElement as s,
|
|
4
|
-
classNames, restoreFocus, Fragment, adoptCSS
|
|
4
|
+
t, classNames, restoreFocus, Fragment, adoptCSS
|
|
5
5
|
} from './dom-utils.js'
|
|
6
|
-
|
|
7
6
|
import { store } from './app-store.js'
|
|
8
|
-
import {
|
|
9
|
-
import { HEADER_502 } from './ApiConstants.js'
|
|
7
|
+
import { PayloadViewer, previewMock } from './payload-viewer.js'
|
|
10
8
|
|
|
11
|
-
import CSS from './
|
|
9
|
+
import CSS from './app.css' with { type: 'css' }
|
|
12
10
|
adoptCSS(CSS)
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
const t = translation => translation[0]
|
|
16
|
-
|
|
17
13
|
store.onError = onError
|
|
18
14
|
store.render = render
|
|
19
15
|
store.renderRow = renderRow
|
|
@@ -69,7 +65,7 @@ function Header() {
|
|
|
69
65
|
GlobalDelayField(),
|
|
70
66
|
GlobalDelayJitterField()),
|
|
71
67
|
CookieSelector(),
|
|
72
|
-
ProxyFallbackField(),
|
|
68
|
+
store.showProxyField && ProxyFallbackField(),
|
|
73
69
|
ResetButton(),
|
|
74
70
|
HelpLink()
|
|
75
71
|
)))
|
|
@@ -556,115 +552,6 @@ function Resizer(ref) {
|
|
|
556
552
|
}
|
|
557
553
|
|
|
558
554
|
|
|
559
|
-
/** # Payload Preview */
|
|
560
|
-
|
|
561
|
-
const payloadViewerTitleRef = {}
|
|
562
|
-
const payloadViewerCodeRef = {}
|
|
563
|
-
|
|
564
|
-
function PayloadViewer() {
|
|
565
|
-
return (
|
|
566
|
-
r('div', classNames(CSS.PayloadViewer),
|
|
567
|
-
RightToolbar(),
|
|
568
|
-
r('pre', null,
|
|
569
|
-
r('code', { ref: payloadViewerCodeRef },
|
|
570
|
-
!store.hasChosenLink && t`Click a link to preview it`))))
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function RightToolbar() {
|
|
574
|
-
return (
|
|
575
|
-
r('div', classNames(CSS.SubToolbar),
|
|
576
|
-
r('h2', { ref: payloadViewerTitleRef },
|
|
577
|
-
!store.hasChosenLink && t`Preview`)))
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
function PayloadViewerTitle(file, statusText) {
|
|
582
|
-
const { method, status, ext } = parseFilename(file)
|
|
583
|
-
const fileNameWithComments = file.split('.').slice(0, -3).join('.')
|
|
584
|
-
return (
|
|
585
|
-
r('span', null,
|
|
586
|
-
fileNameWithComments + '.' + method + '.',
|
|
587
|
-
r('abbr', { title: statusText }, status),
|
|
588
|
-
'.' + ext))
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function PayloadViewerTitleWhenProxied(response) {
|
|
592
|
-
const mime = response.headers.get('content-type') || ''
|
|
593
|
-
const badGateway = response.headers.get(HEADER_502)
|
|
594
|
-
return (
|
|
595
|
-
r('span', null,
|
|
596
|
-
badGateway
|
|
597
|
-
? r('span', null, t`⛔ Fallback Backend Error` + ' ')
|
|
598
|
-
: r('span', null, t`Got` + ' '),
|
|
599
|
-
r('abbr', { title: response.statusText }, response.status),
|
|
600
|
-
' ' + mime))
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const SPINNER_DELAY = 80
|
|
604
|
-
function PayloadViewerProgressBar() {
|
|
605
|
-
return (
|
|
606
|
-
r('div', classNames(CSS.ProgressBar),
|
|
607
|
-
r('div', {
|
|
608
|
-
style: {
|
|
609
|
-
animationDuration: store.delay - SPINNER_DELAY + 'ms'
|
|
610
|
-
}
|
|
611
|
-
})))
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
async function previewMock() {
|
|
615
|
-
const { method, urlMask } = store.chosenLink
|
|
616
|
-
|
|
617
|
-
previewMock.controller?.abort()
|
|
618
|
-
previewMock.controller = new AbortController
|
|
619
|
-
|
|
620
|
-
const spinnerTimer = setTimeout(() => {
|
|
621
|
-
payloadViewerTitleRef.elem.replaceChildren(t`Fetching…`)
|
|
622
|
-
payloadViewerCodeRef.elem.replaceChildren(PayloadViewerProgressBar())
|
|
623
|
-
}, SPINNER_DELAY)
|
|
624
|
-
|
|
625
|
-
try {
|
|
626
|
-
const response = await fetch(urlMask, {
|
|
627
|
-
method,
|
|
628
|
-
signal: previewMock.controller.signal
|
|
629
|
-
})
|
|
630
|
-
clearTimeout(spinnerTimer)
|
|
631
|
-
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
632
|
-
if (proxied || file)
|
|
633
|
-
await updatePayloadViewer(proxied, file, response)
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
payloadViewerCodeRef.elem.replaceChildren()
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async function updatePayloadViewer(proxied, file, response) {
|
|
641
|
-
const mime = response.headers.get('content-type') || ''
|
|
642
|
-
|
|
643
|
-
payloadViewerTitleRef.elem.replaceChildren(proxied
|
|
644
|
-
? PayloadViewerTitleWhenProxied(response)
|
|
645
|
-
: PayloadViewerTitle(file, response.statusText))
|
|
646
|
-
|
|
647
|
-
if (mime.startsWith('image/')) // Naively assumes GET 200
|
|
648
|
-
payloadViewerCodeRef.elem.replaceChildren(r('img', {
|
|
649
|
-
src: URL.createObjectURL(await response.blob())
|
|
650
|
-
}))
|
|
651
|
-
else {
|
|
652
|
-
const body = await response.text() || t`/* Empty Response Body */`
|
|
653
|
-
if (mime === 'application/json')
|
|
654
|
-
payloadViewerCodeRef.elem.replaceChildren(SyntaxJSON(body))
|
|
655
|
-
else if (isXML(mime))
|
|
656
|
-
payloadViewerCodeRef.elem.replaceChildren(SyntaxXML(body))
|
|
657
|
-
else
|
|
658
|
-
payloadViewerCodeRef.elem.textContent = body
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function isXML(mime) {
|
|
663
|
-
return ['text/html', 'text/xml', 'application/xml'].some(m => mime.includes(m))
|
|
664
|
-
|| /application\/.*\+xml/.test(mime)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
555
|
async function onError(error) {
|
|
669
556
|
if (error?.name === 'AbortError')
|
|
670
557
|
return
|
|
@@ -827,94 +714,3 @@ function initKeyboardNavigation() {
|
|
|
827
714
|
}
|
|
828
715
|
}
|
|
829
716
|
|
|
830
|
-
|
|
831
|
-
function SyntaxJSON(json) {
|
|
832
|
-
// Capture groups: [string, optional colon, punc]
|
|
833
|
-
const regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
|
|
834
|
-
|
|
835
|
-
const MAX_NODES = 50_000
|
|
836
|
-
let nNodes = 0
|
|
837
|
-
|
|
838
|
-
const frag = new DocumentFragment()
|
|
839
|
-
|
|
840
|
-
function span(className, textContent) {
|
|
841
|
-
nNodes++
|
|
842
|
-
const s = document.createElement('span')
|
|
843
|
-
s.className = className
|
|
844
|
-
s.textContent = textContent
|
|
845
|
-
frag.appendChild(s)
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function text(t) {
|
|
849
|
-
nNodes++
|
|
850
|
-
frag.appendChild(document.createTextNode(t))
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
let match
|
|
854
|
-
let lastIndex = 0
|
|
855
|
-
while ((match = regex.exec(json)) !== null) {
|
|
856
|
-
if (nNodes > MAX_NODES)
|
|
857
|
-
break
|
|
858
|
-
|
|
859
|
-
if (match.index > lastIndex)
|
|
860
|
-
text(json.slice(lastIndex, match.index))
|
|
861
|
-
|
|
862
|
-
const [full, str, colon, punc] = match
|
|
863
|
-
lastIndex = match.index + full.length
|
|
864
|
-
|
|
865
|
-
if (str && colon) {
|
|
866
|
-
span(CSS.syntaxKey, str)
|
|
867
|
-
text(colon)
|
|
868
|
-
}
|
|
869
|
-
else if (punc) text(punc)
|
|
870
|
-
else if (str) span(CSS.syntaxStr, str)
|
|
871
|
-
else span(CSS.syntaxVal, full)
|
|
872
|
-
}
|
|
873
|
-
frag.normalize()
|
|
874
|
-
text(json.slice(lastIndex))
|
|
875
|
-
return frag
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
function SyntaxXML(xml) {
|
|
880
|
-
// Capture groups: [tagPunc, tagName, attrName, attrVal]
|
|
881
|
-
const regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
|
882
|
-
|
|
883
|
-
const MAX_NODES = 50_000
|
|
884
|
-
let nNodes = 0
|
|
885
|
-
|
|
886
|
-
const frag = new DocumentFragment()
|
|
887
|
-
|
|
888
|
-
function span(className, textContent) {
|
|
889
|
-
nNodes++
|
|
890
|
-
const s = document.createElement('span')
|
|
891
|
-
s.className = className
|
|
892
|
-
s.textContent = textContent
|
|
893
|
-
frag.appendChild(s)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function text(t) {
|
|
897
|
-
nNodes++
|
|
898
|
-
frag.appendChild(document.createTextNode(t))
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
let match
|
|
902
|
-
let lastIndex = 0
|
|
903
|
-
while ((match = regex.exec(xml)) !== null) {
|
|
904
|
-
if (nNodes > MAX_NODES)
|
|
905
|
-
break
|
|
906
|
-
|
|
907
|
-
if (match.index > lastIndex)
|
|
908
|
-
text(xml.slice(lastIndex, match.index))
|
|
909
|
-
|
|
910
|
-
lastIndex = match.index + match[0].length
|
|
911
|
-
|
|
912
|
-
if (match[1]) span(CSS.syntaxPunc, match[1])
|
|
913
|
-
else if (match[2]) span(CSS.syntaxTag, match[2])
|
|
914
|
-
else if (match[3]) span(CSS.syntaxAttr, match[3])
|
|
915
|
-
else if (match[4]) span(CSS.syntaxAttrVal, match[4])
|
|
916
|
-
}
|
|
917
|
-
text(xml.slice(lastIndex))
|
|
918
|
-
frag.normalize()
|
|
919
|
-
return frag
|
|
920
|
-
}
|
package/src/client/dom-utils.js
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement as r,
|
|
3
|
+
t, classNames, adoptCSS
|
|
4
|
+
} from './dom-utils.js'
|
|
5
|
+
import { parseFilename } from './Filename.js'
|
|
6
|
+
import { HEADER_502 } from './ApiConstants.js'
|
|
7
|
+
import { store } from './app-store.js'
|
|
8
|
+
|
|
9
|
+
import CSS from './app.css' with { type: 'css' }
|
|
10
|
+
adoptCSS(CSS)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const payloadViewerTitleRef = {}
|
|
14
|
+
const payloadViewerCodeRef = {}
|
|
15
|
+
|
|
16
|
+
export function PayloadViewer() {
|
|
17
|
+
return (
|
|
18
|
+
r('div', classNames(CSS.PayloadViewer),
|
|
19
|
+
RightToolbar(),
|
|
20
|
+
r('pre', null,
|
|
21
|
+
r('code', { ref: payloadViewerCodeRef },
|
|
22
|
+
!store.hasChosenLink && t`Click a link to preview it`))))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function RightToolbar() {
|
|
26
|
+
return (
|
|
27
|
+
r('div', classNames(CSS.SubToolbar),
|
|
28
|
+
r('h2', { ref: payloadViewerTitleRef },
|
|
29
|
+
!store.hasChosenLink && t`Preview`)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function PayloadViewerTitle(file, statusText) {
|
|
34
|
+
const { method, status, ext } = parseFilename(file)
|
|
35
|
+
const fileNameWithComments = file.split('.').slice(0, -3).join('.')
|
|
36
|
+
return (
|
|
37
|
+
r('span', null,
|
|
38
|
+
fileNameWithComments + '.' + method + '.',
|
|
39
|
+
r('abbr', { title: statusText }, status),
|
|
40
|
+
'.' + ext))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function PayloadViewerTitleWhenProxied(response) {
|
|
44
|
+
const mime = response.headers.get('content-type') || ''
|
|
45
|
+
const badGateway = response.headers.get(HEADER_502)
|
|
46
|
+
return (
|
|
47
|
+
r('span', null,
|
|
48
|
+
badGateway
|
|
49
|
+
? r('span', null, t`⛔ Fallback Backend Error` + ' ')
|
|
50
|
+
: r('span', null, t`Got` + ' '),
|
|
51
|
+
r('abbr', { title: response.statusText }, response.status),
|
|
52
|
+
' ' + mime))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SPINNER_DELAY = 80
|
|
56
|
+
function PayloadViewerProgressBar() {
|
|
57
|
+
return (
|
|
58
|
+
r('div', classNames(CSS.ProgressBar),
|
|
59
|
+
r('div', {
|
|
60
|
+
style: {
|
|
61
|
+
animationDuration: store.delay - SPINNER_DELAY + 'ms'
|
|
62
|
+
}
|
|
63
|
+
})))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function previewMock() {
|
|
67
|
+
const { method, urlMask } = store.chosenLink
|
|
68
|
+
|
|
69
|
+
previewMock.controller?.abort()
|
|
70
|
+
previewMock.controller = new AbortController
|
|
71
|
+
|
|
72
|
+
const spinnerTimer = setTimeout(() => {
|
|
73
|
+
payloadViewerTitleRef.elem.replaceChildren(t`Fetching…`)
|
|
74
|
+
payloadViewerCodeRef.elem.replaceChildren(PayloadViewerProgressBar())
|
|
75
|
+
}, SPINNER_DELAY)
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(urlMask, {
|
|
79
|
+
method,
|
|
80
|
+
signal: previewMock.controller.signal
|
|
81
|
+
})
|
|
82
|
+
clearTimeout(spinnerTimer)
|
|
83
|
+
const { proxied, file } = store.brokerFor(method, urlMask)
|
|
84
|
+
if (proxied || file)
|
|
85
|
+
await updatePayloadViewer(proxied, file, response)
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
payloadViewerCodeRef.elem.replaceChildren()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function updatePayloadViewer(proxied, file, response) {
|
|
93
|
+
const mime = response.headers.get('content-type') || ''
|
|
94
|
+
|
|
95
|
+
payloadViewerTitleRef.elem.replaceChildren(proxied
|
|
96
|
+
? PayloadViewerTitleWhenProxied(response)
|
|
97
|
+
: PayloadViewerTitle(file, response.statusText))
|
|
98
|
+
|
|
99
|
+
if (mime.startsWith('image/')) // Naively assumes GET 200
|
|
100
|
+
payloadViewerCodeRef.elem.replaceChildren(r('img', {
|
|
101
|
+
src: URL.createObjectURL(await response.blob())
|
|
102
|
+
}))
|
|
103
|
+
else {
|
|
104
|
+
const body = await response.text() || t`/* Empty Response Body */`
|
|
105
|
+
if (mime === 'application/json')
|
|
106
|
+
payloadViewerCodeRef.elem.replaceChildren(SyntaxJSON(body))
|
|
107
|
+
else if (isXML(mime))
|
|
108
|
+
payloadViewerCodeRef.elem.replaceChildren(SyntaxXML(body))
|
|
109
|
+
else
|
|
110
|
+
payloadViewerCodeRef.elem.textContent = body
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isXML(mime) {
|
|
115
|
+
return ['text/html', 'text/xml', 'application/xml'].some(m => mime.includes(m))
|
|
116
|
+
|| /application\/.*\+xml/.test(mime)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
function SyntaxJSON(json) {
|
|
122
|
+
// Capture groups: [string, optional colon, punc]
|
|
123
|
+
const regex = /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|([{}\[\],:\s]+)|\S+/g
|
|
124
|
+
|
|
125
|
+
const MAX_NODES = 50_000
|
|
126
|
+
let nNodes = 0
|
|
127
|
+
|
|
128
|
+
const frag = new DocumentFragment()
|
|
129
|
+
|
|
130
|
+
function span(className, textContent) {
|
|
131
|
+
nNodes++
|
|
132
|
+
const s = document.createElement('span')
|
|
133
|
+
s.className = className
|
|
134
|
+
s.textContent = textContent
|
|
135
|
+
frag.appendChild(s)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function text(t) {
|
|
139
|
+
nNodes++
|
|
140
|
+
frag.appendChild(document.createTextNode(t))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let match
|
|
144
|
+
let lastIndex = 0
|
|
145
|
+
while ((match = regex.exec(json)) !== null) {
|
|
146
|
+
if (nNodes > MAX_NODES)
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if (match.index > lastIndex)
|
|
150
|
+
text(json.slice(lastIndex, match.index))
|
|
151
|
+
|
|
152
|
+
const [full, str, colon, punc] = match
|
|
153
|
+
lastIndex = match.index + full.length
|
|
154
|
+
|
|
155
|
+
if (str && colon) {
|
|
156
|
+
span(CSS.syntaxKey, str)
|
|
157
|
+
text(colon)
|
|
158
|
+
}
|
|
159
|
+
else if (punc) text(punc)
|
|
160
|
+
else if (str) span(CSS.syntaxStr, str)
|
|
161
|
+
else span(CSS.syntaxVal, full)
|
|
162
|
+
}
|
|
163
|
+
frag.normalize()
|
|
164
|
+
text(json.slice(lastIndex))
|
|
165
|
+
return frag
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
function SyntaxXML(xml) {
|
|
170
|
+
// Capture groups: [tagPunc, tagName, attrName, attrVal]
|
|
171
|
+
const regex = /(<\/?|\/?>|\?>)|(?<=<\??\/?)([A-Za-z_:][\w:.-]*)|([A-Za-z_:][\w:.-]*)(?==)|("(?:[^"\\]|\\.)*")/g
|
|
172
|
+
|
|
173
|
+
const MAX_NODES = 50_000
|
|
174
|
+
let nNodes = 0
|
|
175
|
+
|
|
176
|
+
const frag = new DocumentFragment()
|
|
177
|
+
|
|
178
|
+
function span(className, textContent) {
|
|
179
|
+
nNodes++
|
|
180
|
+
const s = document.createElement('span')
|
|
181
|
+
s.className = className
|
|
182
|
+
s.textContent = textContent
|
|
183
|
+
frag.appendChild(s)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function text(t) {
|
|
187
|
+
nNodes++
|
|
188
|
+
frag.appendChild(document.createTextNode(t))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let match
|
|
192
|
+
let lastIndex = 0
|
|
193
|
+
while ((match = regex.exec(xml)) !== null) {
|
|
194
|
+
if (nNodes > MAX_NODES)
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if (match.index > lastIndex)
|
|
198
|
+
text(xml.slice(lastIndex, match.index))
|
|
199
|
+
|
|
200
|
+
lastIndex = match.index + match[0].length
|
|
201
|
+
|
|
202
|
+
if (match[1]) span(CSS.syntaxPunc, match[1])
|
|
203
|
+
else if (match[2]) span(CSS.syntaxTag, match[2])
|
|
204
|
+
else if (match[3]) span(CSS.syntaxAttr, match[3])
|
|
205
|
+
else if (match[4]) span(CSS.syntaxAttrVal, match[4])
|
|
206
|
+
}
|
|
207
|
+
text(xml.slice(lastIndex))
|
|
208
|
+
frag.normalize()
|
|
209
|
+
return frag
|
|
210
|
+
}
|
|
File without changes
|