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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "mockaton",
3
3
  "description": "HTTP Mock Server",
4
4
  "type": "module",
5
- "version": "12.5.3",
5
+ "version": "12.6.0",
6
6
  "exports": {
7
7
  ".": {
8
8
  "import": "./index.js",
@@ -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
- async fetchState() {
30
- try {
31
- const response = await api.getState()
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
- try {
59
- const response = await api.reset()
60
- if (!response.ok) throw response
68
+ store._action(api.reset, () => {
61
69
  store.setChosenLink('', '')
62
- await store.fetchState()
63
- }
64
- catch (error) { store.onError(error) }
70
+ store.fetchState()
71
+ })
65
72
  },
66
73
 
67
74
  async bulkSelectByComment(value) {
68
- try {
69
- const response = await api.bulkSelectByComment(value)
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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
- try {
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 prevParts = pathsInParts[i - 1]
290
- const currParts = pathsInParts[i]
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 = '/' + currParts.slice(0, j).join('/') + '/'
268
+ const ditto = '/' + curr.slice(0, j).join('/') + '/'
303
269
  result.push([ditto, paths[i].slice(ditto.length)])
304
270
  }
305
271
  }
@@ -1,5 +1,5 @@
1
1
  import { test } from 'node:test'
2
- import { deepEqual } from 'node:assert'
2
+ import { deepEqual } from 'node:assert/strict'
3
3
 
4
4
  import { dittoSplitPaths, BrokerRowModel, t } from './app-store.js'
5
5
 
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 { parseFilename } from './Filename.js'
9
- import { HEADER_502 } from './ApiConstants.js'
7
+ import { PayloadViewer, previewMock } from './payload-viewer.js'
10
8
 
11
- import CSS from './styles.css' with { type: 'css' }
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
- }
@@ -1,3 +1,7 @@
1
+ export function t(translation) {
2
+ return translation[0]
3
+ }
4
+
1
5
  export function classNames(...args) {
2
6
  return {
3
7
  className: args.filter(Boolean).join(' ')
@@ -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
+ }
@@ -8,6 +8,7 @@ export function parseQueryParams(url) {
8
8
  return new URL(url, 'http://_').searchParams
9
9
  }
10
10
 
11
+
11
12
  export function parseSplats(url, filename) {
12
13
  const { urlMask } = parseFilename(relative(config.mocksDir, filename))
13
14
 
File without changes