webtalekit-alpha 0.2.11 → 0.2.14
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/README.md +231 -87
- package/package.json +12 -2
- package/parser/checker.js +184 -0
- package/parser/checker.test.ts +491 -0
- package/parser/cli.js +34 -2
- package/parser/parser.js +25 -18
- package/src/core/defaultUIHandler.js +309 -0
- package/src/core/defaultUIHandler.js.map +1 -0
- package/src/core/drawer.js +65 -49
- package/src/core/drawer.js.map +1 -1
- package/src/core/index.js +418 -179
- package/src/core/scenarioManager.js +33 -12
- package/src/core/scenarioManager.js.map +1 -1
- package/src/resource/soundObject.js +4 -2
- package/src/resource/soundObject.js.map +1 -1
- package/src/utils/eventBus.js +88 -0
- package/src/utils/eventBus.js.map +1 -0
- package/src/utils/fallbackTemplate.js +13 -0
- package/src/utils/fallbackTemplate.js.map +1 -0
- package/src/utils/logger.js +45 -1
- package/src/utils/logger.js.map +1 -1
- package/src/utils/store.js +5 -0
- package/src/utils/store.js.map +1 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { check } from '../parser/checker'
|
|
2
|
+
|
|
3
|
+
describe('checker', () => {
|
|
4
|
+
describe('valid scenarios', () => {
|
|
5
|
+
test('empty scenario passes without errors', () => {
|
|
6
|
+
expect(check([])).toEqual([])
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test('top-level commands pass without errors', () => {
|
|
10
|
+
const scenario = [
|
|
11
|
+
{ type: 'text', content: ['Hello'] },
|
|
12
|
+
{ type: 'say', name: 'Narrator', content: ['World'] },
|
|
13
|
+
{ type: 'show', src: './bg.jpg', content: [] },
|
|
14
|
+
{ type: 'route', to: 'title', content: [] },
|
|
15
|
+
]
|
|
16
|
+
expect(check(scenario)).toEqual([])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('item inside choice passes without errors', () => {
|
|
20
|
+
const scenario = [
|
|
21
|
+
{
|
|
22
|
+
type: 'choice',
|
|
23
|
+
prompt: 'Choose',
|
|
24
|
+
content: [
|
|
25
|
+
{ type: 'item', label: 'A', content: [] },
|
|
26
|
+
{ type: 'item', label: 'B', content: [] },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
expect(check(scenario)).toEqual([])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('action inside actions inside dialog passes without errors', () => {
|
|
34
|
+
const scenario = [
|
|
35
|
+
{
|
|
36
|
+
type: 'dialog',
|
|
37
|
+
name: 'test',
|
|
38
|
+
content: [
|
|
39
|
+
{ type: 'prompt', content: ['Question?'] },
|
|
40
|
+
{
|
|
41
|
+
type: 'actions',
|
|
42
|
+
content: [
|
|
43
|
+
{ type: 'action', value: 'yes', label: 'Yes', content: [] },
|
|
44
|
+
{ type: 'action', value: 'no', label: 'No', content: [] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
expect(check(scenario)).toEqual([])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('then and else inside if passes without errors', () => {
|
|
54
|
+
const scenario = [
|
|
55
|
+
{
|
|
56
|
+
type: 'if',
|
|
57
|
+
condition: 'x === 1',
|
|
58
|
+
content: [
|
|
59
|
+
{ type: 'then', content: [{ type: 'text', content: ['yes'] }] },
|
|
60
|
+
{ type: 'else', content: [{ type: 'text', content: ['no'] }] },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
expect(check(scenario)).toEqual([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('nested choice inside item passes without errors', () => {
|
|
68
|
+
const scenario = [
|
|
69
|
+
{
|
|
70
|
+
type: 'choice',
|
|
71
|
+
prompt: 'Outer',
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'item',
|
|
75
|
+
label: 'Pick',
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'choice',
|
|
79
|
+
prompt: 'Inner',
|
|
80
|
+
content: [{ type: 'item', label: 'Sub', content: [] }],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
expect(check(scenario)).toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('inline text decoration tags inside text pass without errors', () => {
|
|
91
|
+
const scenario = [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
content: [
|
|
95
|
+
{ type: 'color', value: 'red', content: ['Red text'] },
|
|
96
|
+
{ type: 'ruby', text: 'ルビ', content: ['漢字'] },
|
|
97
|
+
{ type: 'b', content: ['Bold'] },
|
|
98
|
+
{ type: 'i', content: ['Italic'] },
|
|
99
|
+
{ type: 'br', content: [] },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
expect(check(scenario)).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('inline text decoration tags inside say pass without errors', () => {
|
|
107
|
+
const scenario = [
|
|
108
|
+
{
|
|
109
|
+
type: 'say',
|
|
110
|
+
name: 'Narrator',
|
|
111
|
+
content: [
|
|
112
|
+
{ type: 'color', value: 'blue', content: ['Blue'] },
|
|
113
|
+
{ type: 'b', content: ['Bold'] },
|
|
114
|
+
{ type: 'br', content: [] },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
]
|
|
118
|
+
expect(check(scenario)).toEqual([])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('then inside text (HTTP response) passes without errors', () => {
|
|
122
|
+
const scenario = [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
get: 'https://api.example.com/data',
|
|
126
|
+
content: [
|
|
127
|
+
{ type: 'progress', content: ['Loading...'] },
|
|
128
|
+
{ type: 'header', content: [] },
|
|
129
|
+
{ type: 'data', content: [] },
|
|
130
|
+
{ type: 'then', content: ['Success'] },
|
|
131
|
+
{ type: 'error', content: ['Error'] },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
expect(check(scenario)).toEqual([])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('HTTP sub-tags inside call pass without errors', () => {
|
|
139
|
+
const scenario = [
|
|
140
|
+
{
|
|
141
|
+
type: 'call',
|
|
142
|
+
post: 'https://api.example.com/action',
|
|
143
|
+
content: [
|
|
144
|
+
{ type: 'header', content: [] },
|
|
145
|
+
{ type: 'data', content: [] },
|
|
146
|
+
{ type: 'then', content: ['Done'] },
|
|
147
|
+
{ type: 'error', content: ['Failed'] },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
expect(check(scenario)).toEqual([])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('all known show attributes pass without warnings', () => {
|
|
155
|
+
const scenario = [
|
|
156
|
+
{
|
|
157
|
+
type: 'show',
|
|
158
|
+
src: './chara.png',
|
|
159
|
+
name: 'hero',
|
|
160
|
+
mode: 'chara',
|
|
161
|
+
x: 100,
|
|
162
|
+
y: 200,
|
|
163
|
+
width: 300,
|
|
164
|
+
height: 400,
|
|
165
|
+
pos: 'center:middle',
|
|
166
|
+
look: false,
|
|
167
|
+
entry: { time: 1, wait: false },
|
|
168
|
+
sepia: 0.5,
|
|
169
|
+
mono: false,
|
|
170
|
+
blur: 0,
|
|
171
|
+
opacity: 1,
|
|
172
|
+
transition: 'fade',
|
|
173
|
+
duration: 1000,
|
|
174
|
+
content: [],
|
|
175
|
+
},
|
|
176
|
+
]
|
|
177
|
+
expect(check(scenario)).toEqual([])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('all known sound attributes pass without warnings', () => {
|
|
181
|
+
const scenario = [
|
|
182
|
+
{
|
|
183
|
+
type: 'sound',
|
|
184
|
+
src: './bgm.mp3',
|
|
185
|
+
name: 'main',
|
|
186
|
+
mode: 'bgm',
|
|
187
|
+
play: true,
|
|
188
|
+
loop: true,
|
|
189
|
+
content: [],
|
|
190
|
+
},
|
|
191
|
+
]
|
|
192
|
+
expect(check(scenario)).toEqual([])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('save and load with known attributes pass without warnings', () => {
|
|
196
|
+
const scenario = [
|
|
197
|
+
{ type: 'save', slot: '1', name: 'chapter1', message: true, content: [] },
|
|
198
|
+
{ type: 'load', slot: '1', message: false, content: [] },
|
|
199
|
+
]
|
|
200
|
+
expect(check(scenario)).toEqual([])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('global attributes (if, get, post, put, delete) do not produce warnings', () => {
|
|
204
|
+
const scenario = [
|
|
205
|
+
{ type: 'text', if: 'flag', get: 'https://api.example.com', content: ['ok'] },
|
|
206
|
+
{ type: 'call', if: 'x > 0', method: 'doSomething()', content: [] },
|
|
207
|
+
]
|
|
208
|
+
expect(check(scenario)).toEqual([])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('item with all known attributes passes without warnings', () => {
|
|
212
|
+
const scenario = [
|
|
213
|
+
{
|
|
214
|
+
type: 'choice',
|
|
215
|
+
content: [
|
|
216
|
+
{
|
|
217
|
+
type: 'item',
|
|
218
|
+
label: 'Option A',
|
|
219
|
+
id: 1,
|
|
220
|
+
default: './btn.png',
|
|
221
|
+
hover: './btn_hover.png',
|
|
222
|
+
select: './btn_select.png',
|
|
223
|
+
color: { default: 'black', hover: 'white', select: 'red' },
|
|
224
|
+
position: { x: 100, y: 200 },
|
|
225
|
+
content: [],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
expect(check(scenario)).toEqual([])
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('invalid parent-child relationships', () => {
|
|
235
|
+
test('item at scenario root produces an error', () => {
|
|
236
|
+
const scenario = [{ type: 'item', label: 'Option', content: [] }]
|
|
237
|
+
const errors = check(scenario)
|
|
238
|
+
expect(errors).toHaveLength(1)
|
|
239
|
+
expect(errors[0].type).toBe('invalid_parent')
|
|
240
|
+
expect(errors[0].node).toBe('item')
|
|
241
|
+
expect(errors[0].parent).toBeNull()
|
|
242
|
+
expect(errors[0].message).toContain('<item>')
|
|
243
|
+
expect(errors[0].message).toContain('<choice>')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('item inside if (not choice) produces an error', () => {
|
|
247
|
+
const scenario = [
|
|
248
|
+
{
|
|
249
|
+
type: 'if',
|
|
250
|
+
condition: 'x',
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: 'then',
|
|
254
|
+
content: [{ type: 'item', label: 'Option', content: [] }],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
const errors = check(scenario)
|
|
260
|
+
expect(errors.some((e) => e.node === 'item')).toBe(true)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('action at scenario root produces an error', () => {
|
|
264
|
+
const scenario = [{ type: 'action', value: 'yes', label: 'Yes', content: [] }]
|
|
265
|
+
const errors = check(scenario)
|
|
266
|
+
expect(errors).toHaveLength(1)
|
|
267
|
+
expect(errors[0].type).toBe('invalid_parent')
|
|
268
|
+
expect(errors[0].node).toBe('action')
|
|
269
|
+
expect(errors[0].message).toContain('<actions>')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('then at scenario root produces an error', () => {
|
|
273
|
+
const scenario = [{ type: 'then', content: [] }]
|
|
274
|
+
const errors = check(scenario)
|
|
275
|
+
expect(errors).toHaveLength(1)
|
|
276
|
+
expect(errors[0].node).toBe('then')
|
|
277
|
+
expect(errors[0].message).toContain('<if>')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('else at scenario root produces an error', () => {
|
|
281
|
+
const scenario = [{ type: 'else', content: [] }]
|
|
282
|
+
const errors = check(scenario)
|
|
283
|
+
expect(errors).toHaveLength(1)
|
|
284
|
+
expect(errors[0].node).toBe('else')
|
|
285
|
+
expect(errors[0].message).toContain('<if>')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('prompt at scenario root produces an error', () => {
|
|
289
|
+
const scenario = [{ type: 'prompt', content: ['Question?'] }]
|
|
290
|
+
const errors = check(scenario)
|
|
291
|
+
expect(errors).toHaveLength(1)
|
|
292
|
+
expect(errors[0].node).toBe('prompt')
|
|
293
|
+
expect(errors[0].message).toContain('<dialog>')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('actions at scenario root produces an error', () => {
|
|
297
|
+
const scenario = [{ type: 'actions', content: [] }]
|
|
298
|
+
const errors = check(scenario)
|
|
299
|
+
expect(errors).toHaveLength(1)
|
|
300
|
+
expect(errors[0].node).toBe('actions')
|
|
301
|
+
expect(errors[0].message).toContain('<dialog>')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('multiple errors are all reported', () => {
|
|
305
|
+
const scenario = [
|
|
306
|
+
{ type: 'item', label: 'A', content: [] },
|
|
307
|
+
{ type: 'item', label: 'B', content: [] },
|
|
308
|
+
{ type: 'action', value: 'x', label: 'X', content: [] },
|
|
309
|
+
]
|
|
310
|
+
const errors = check(scenario)
|
|
311
|
+
expect(errors).toHaveLength(3)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('color at scenario root produces an error', () => {
|
|
315
|
+
const scenario = [{ type: 'color', value: 'red', content: ['Red'] }]
|
|
316
|
+
const errors = check(scenario)
|
|
317
|
+
expect(errors).toHaveLength(1)
|
|
318
|
+
expect(errors[0].node).toBe('color')
|
|
319
|
+
expect(errors[0].message).toContain('<text>')
|
|
320
|
+
expect(errors[0].message).toContain('<say>')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test('ruby at scenario root produces an error', () => {
|
|
324
|
+
const scenario = [{ type: 'ruby', text: 'ルビ', content: ['漢字'] }]
|
|
325
|
+
const errors = check(scenario)
|
|
326
|
+
expect(errors).toHaveLength(1)
|
|
327
|
+
expect(errors[0].node).toBe('ruby')
|
|
328
|
+
expect(errors[0].message).toContain('<text>')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('b and i at scenario root produce errors', () => {
|
|
332
|
+
const scenario = [
|
|
333
|
+
{ type: 'b', content: ['Bold'] },
|
|
334
|
+
{ type: 'i', content: ['Italic'] },
|
|
335
|
+
]
|
|
336
|
+
const errors = check(scenario)
|
|
337
|
+
expect(errors).toHaveLength(2)
|
|
338
|
+
expect(errors[0].node).toBe('b')
|
|
339
|
+
expect(errors[1].node).toBe('i')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
test('br at scenario root produces an error', () => {
|
|
343
|
+
const scenario = [{ type: 'br', content: [] }]
|
|
344
|
+
const errors = check(scenario)
|
|
345
|
+
expect(errors).toHaveLength(1)
|
|
346
|
+
expect(errors[0].node).toBe('br')
|
|
347
|
+
expect(errors[0].message).toContain('<text>')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('color inside choice (not text/say) produces an error', () => {
|
|
351
|
+
const scenario = [
|
|
352
|
+
{
|
|
353
|
+
type: 'choice',
|
|
354
|
+
content: [{ type: 'color', value: 'red', content: ['Red'] }],
|
|
355
|
+
},
|
|
356
|
+
]
|
|
357
|
+
const errors = check(scenario)
|
|
358
|
+
expect(errors.some((e) => e.node === 'color')).toBe(true)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('HTTP sub-tags at scenario root produce errors', () => {
|
|
362
|
+
const scenario = [
|
|
363
|
+
{ type: 'header', content: [] },
|
|
364
|
+
{ type: 'data', content: [] },
|
|
365
|
+
{ type: 'error', content: ['Error'] },
|
|
366
|
+
{ type: 'progress', content: ['Loading'] },
|
|
367
|
+
]
|
|
368
|
+
const errors = check(scenario)
|
|
369
|
+
expect(errors).toHaveLength(4)
|
|
370
|
+
expect(errors.map((e) => e.node)).toEqual(['header', 'data', 'error', 'progress'])
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('unknown attribute warnings', () => {
|
|
375
|
+
test('unknown attribute on text produces a warning', () => {
|
|
376
|
+
const scenario = [{ type: 'text', content: ['Hello'], unknownAttr: 'value' }]
|
|
377
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
378
|
+
expect(warnings).toHaveLength(1)
|
|
379
|
+
expect(warnings[0].node).toBe('text')
|
|
380
|
+
expect(warnings[0].attribute).toBe('unknownAttr')
|
|
381
|
+
expect(warnings[0].message).toContain('"unknownAttr"')
|
|
382
|
+
expect(warnings[0].message).toContain('will be ignored')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('unknown attribute on show produces a warning', () => {
|
|
386
|
+
const scenario = [{ type: 'show', src: './bg.jpg', content: [], typo: 'yes' }]
|
|
387
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
388
|
+
expect(warnings).toHaveLength(1)
|
|
389
|
+
expect(warnings[0].node).toBe('show')
|
|
390
|
+
expect(warnings[0].attribute).toBe('typo')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('unknown attribute on jump produces a warning', () => {
|
|
394
|
+
const scenario = [{ type: 'jump', index: 5, content: [], badAttr: true }]
|
|
395
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
396
|
+
expect(warnings).toHaveLength(1)
|
|
397
|
+
expect(warnings[0].node).toBe('jump')
|
|
398
|
+
expect(warnings[0].attribute).toBe('badAttr')
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('any attribute on newpage produces a warning', () => {
|
|
402
|
+
const scenario = [{ type: 'newpage', content: [], extra: 'ignored' }]
|
|
403
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
404
|
+
expect(warnings).toHaveLength(1)
|
|
405
|
+
expect(warnings[0].node).toBe('newpage')
|
|
406
|
+
expect(warnings[0].attribute).toBe('extra')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('unknown attribute on item produces a warning', () => {
|
|
410
|
+
const scenario = [
|
|
411
|
+
{
|
|
412
|
+
type: 'choice',
|
|
413
|
+
content: [{ type: 'item', label: 'A', content: [], ghost: true }],
|
|
414
|
+
},
|
|
415
|
+
]
|
|
416
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
417
|
+
expect(warnings).toHaveLength(1)
|
|
418
|
+
expect(warnings[0].node).toBe('item')
|
|
419
|
+
expect(warnings[0].attribute).toBe('ghost')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test('multiple unknown attributes on one node produce multiple warnings', () => {
|
|
423
|
+
const scenario = [{ type: 'route', to: 'next', content: [], foo: 1, bar: 2 }]
|
|
424
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
425
|
+
expect(warnings).toHaveLength(2)
|
|
426
|
+
expect(warnings.map((w) => w.attribute).sort()).toEqual(['bar', 'foo'])
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
test('unknown attribute on inline color tag produces a warning', () => {
|
|
430
|
+
const scenario = [
|
|
431
|
+
{
|
|
432
|
+
type: 'text',
|
|
433
|
+
content: [{ type: 'color', value: 'red', content: ['Red'], extra: true }],
|
|
434
|
+
},
|
|
435
|
+
]
|
|
436
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
437
|
+
expect(warnings).toHaveLength(1)
|
|
438
|
+
expect(warnings[0].node).toBe('color')
|
|
439
|
+
expect(warnings[0].attribute).toBe('extra')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('unknown attribute on HTTP sub-tag produces a warning', () => {
|
|
443
|
+
const scenario = [
|
|
444
|
+
{
|
|
445
|
+
type: 'text',
|
|
446
|
+
get: 'https://api.example.com',
|
|
447
|
+
content: [{ type: 'error', content: ['Fail'], retries: 3 }],
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
451
|
+
expect(warnings).toHaveLength(1)
|
|
452
|
+
expect(warnings[0].node).toBe('error')
|
|
453
|
+
expect(warnings[0].attribute).toBe('retries')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('global attributes never produce warnings', () => {
|
|
457
|
+
const scenario = [
|
|
458
|
+
{
|
|
459
|
+
type: 'say',
|
|
460
|
+
name: 'Alice',
|
|
461
|
+
if: 'flag',
|
|
462
|
+
get: 'https://api.example.com',
|
|
463
|
+
post: 'https://api.example.com',
|
|
464
|
+
put: 'https://api.example.com',
|
|
465
|
+
delete: 'https://api.example.com',
|
|
466
|
+
content: ['Hello'],
|
|
467
|
+
},
|
|
468
|
+
]
|
|
469
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
470
|
+
expect(warnings).toHaveLength(0)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test('unknown node types do not produce attribute warnings', () => {
|
|
474
|
+
// Custom/unknown tag types should not be flagged
|
|
475
|
+
const scenario = [{ type: 'custom-tag', anything: 'value', content: [] }]
|
|
476
|
+
const warnings = check(scenario).filter((r) => r.type === 'unknown_attribute')
|
|
477
|
+
expect(warnings).toHaveLength(0)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('warnings and errors can occur together', () => {
|
|
481
|
+
// item at root (error) with an unknown attribute (warning)
|
|
482
|
+
const scenario = [{ type: 'item', label: 'A', content: [], badAttr: true }]
|
|
483
|
+
const results = check(scenario)
|
|
484
|
+
const errors = results.filter((r) => r.type === 'invalid_parent')
|
|
485
|
+
const warnings = results.filter((r) => r.type === 'unknown_attribute')
|
|
486
|
+
expect(errors).toHaveLength(1)
|
|
487
|
+
expect(warnings).toHaveLength(1)
|
|
488
|
+
expect(warnings[0].attribute).toBe('badAttr')
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
})
|
package/parser/cli.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
const { HTMLToJSON } = require('html-to-json-parser')
|
|
3
|
+
const { minify } = require('html-minifier')
|
|
2
4
|
const parse = require('./parser.js')
|
|
3
5
|
const fs = require('fs')
|
|
4
6
|
const path = require('path')
|
|
7
|
+
|
|
8
|
+
const minifyOptions = {
|
|
9
|
+
removeTagWhitespace: true,
|
|
10
|
+
collapseWhitespace: true,
|
|
11
|
+
removeComments: true,
|
|
12
|
+
minifyJS: true,
|
|
13
|
+
minifyCSS: true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Node.js 向け HTMLParserAdapter(minify + HTMLToJSON)
|
|
18
|
+
* @param {string} data
|
|
19
|
+
* @returns {Promise<object>}
|
|
20
|
+
*/
|
|
21
|
+
const nodeHtmlParser = async (data) => {
|
|
22
|
+
const html = minify(data, minifyOptions)
|
|
23
|
+
return HTMLToJSON(html)
|
|
24
|
+
}
|
|
5
25
|
/**
|
|
6
|
-
* WebTaleScript
|
|
26
|
+
* WebTaleScript パーサー CLI
|
|
7
27
|
*/
|
|
8
28
|
|
|
9
29
|
const exec = (targetScript) => {
|
|
@@ -24,7 +44,19 @@ const exec = (targetScript) => {
|
|
|
24
44
|
return
|
|
25
45
|
}
|
|
26
46
|
// パーサーを呼び出す。
|
|
27
|
-
const { scenario, script, lang } = await parse(data)
|
|
47
|
+
const { scenario, script, lang, errors } = await parse(data, nodeHtmlParser)
|
|
48
|
+
// 構文エラーと属性警告を分ける
|
|
49
|
+
const syntaxErrors = errors ? errors.filter((e) => e.type !== 'unknown_attribute') : []
|
|
50
|
+
const attrWarnings = errors ? errors.filter((e) => e.type === 'unknown_attribute') : []
|
|
51
|
+
// 属性警告を標準エラー出力へ
|
|
52
|
+
if (attrWarnings.length > 0) {
|
|
53
|
+
attrWarnings.forEach((w) => console.warn(`Attribute Warning in ${targetScript}: ${w.message}`))
|
|
54
|
+
}
|
|
55
|
+
// 構文エラーがある場合、エラーを出力して終了する
|
|
56
|
+
if (syntaxErrors.length > 0) {
|
|
57
|
+
syntaxErrors.forEach((err) => console.error(`Syntax Error in ${targetScript}: ${err.message}`))
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
28
60
|
// jsディレクトリがない場合、作成する
|
|
29
61
|
if (!fs.existsSync(outputPath)) {
|
|
30
62
|
fs.mkdirSync(outputPath)
|
package/parser/parser.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { minify } = require('html-minifier')
|
|
1
|
+
const { check } = require('./checker')
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* WebTaleScript パーサー(環境非依存)
|
|
5
|
+
* @param {string} data - WTSファイルの文字列
|
|
6
|
+
* @param {Function} htmlParser - HTMLをParsedNode形式に変換する関数
|
|
7
|
+
* @returns {Promise<{scenario: Array, script: Array, lang: string, errors: Array}>}
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async (data, htmlParser) => {
|
|
5
10
|
let scenario = []
|
|
6
11
|
let script = []
|
|
7
12
|
let lang = 'js'
|
|
13
|
+
const errors = []
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
16
|
* 渡されたオブジェクトを展開する
|
|
@@ -27,27 +33,28 @@ module.exports = async (data) => {
|
|
|
27
33
|
return rest
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/
|
|
34
|
-
/** HTMLを読み込む */
|
|
35
|
-
const html = minify(data, {
|
|
36
|
-
removeTagWhitespace: true,
|
|
37
|
-
collapseWhitespace: true,
|
|
38
|
-
removeComments: true,
|
|
39
|
-
minifyJS: true,
|
|
40
|
-
minifyCSS: true,
|
|
41
|
-
})
|
|
42
|
-
// HTMLをJSONに変換
|
|
43
|
-
const parseJson = await HTMLToJSON(html)
|
|
36
|
+
// 外から注入されたパーサーを使用
|
|
37
|
+
const parseJson = await htmlParser(data)
|
|
38
|
+
let scenarioCount = 0
|
|
44
39
|
parseJson.content.forEach((element) => {
|
|
45
40
|
if (element.type === 'scenario') {
|
|
41
|
+
scenarioCount++
|
|
42
|
+
if (scenarioCount > 1) {
|
|
43
|
+
errors.push({
|
|
44
|
+
type: 'duplicate_scenario',
|
|
45
|
+
message: 'Multiple <scenario> sections found. Only one <scenario> is allowed per scene file.',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
46
48
|
scenario = flattenAttributes(element.content)
|
|
47
49
|
} else {
|
|
48
50
|
script = element.content
|
|
49
51
|
lang = element.attributes?.type
|
|
50
52
|
}
|
|
51
53
|
})
|
|
52
|
-
|
|
54
|
+
|
|
55
|
+
// パース済みシナリオに対して構文チェッカーを実行する
|
|
56
|
+
const checkerErrors = check(scenario)
|
|
57
|
+
errors.push(...checkerErrors)
|
|
58
|
+
|
|
59
|
+
return { scenario, script, lang, errors }
|
|
53
60
|
}
|