@nitro-bolt/scratchblocks 1.1.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.
Files changed (106) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +205 -0
  3. package/browser.es.js +8 -0
  4. package/browser.js +8 -0
  5. package/build/scratchblocks.min.es.js +11 -0
  6. package/build/scratchblocks.min.es.js.map +1 -0
  7. package/build/scratchblocks.min.js +11 -0
  8. package/build/scratchblocks.min.js.map +1 -0
  9. package/build/translations-all-es.js +10 -0
  10. package/build/translations-all.js +10 -0
  11. package/build/translations-es.js +10 -0
  12. package/build/translations.js +10 -0
  13. package/index.js +163 -0
  14. package/locales/ab.json +261 -0
  15. package/locales/af.json +261 -0
  16. package/locales/all.js +161 -0
  17. package/locales/am.json +262 -0
  18. package/locales/an.json +261 -0
  19. package/locales/ar.json +261 -0
  20. package/locales/ast.json +261 -0
  21. package/locales/az.json +262 -0
  22. package/locales/be.json +261 -0
  23. package/locales/bg.json +261 -0
  24. package/locales/bn.json +261 -0
  25. package/locales/ca.json +267 -0
  26. package/locales/ckb.json +261 -0
  27. package/locales/cs.json +267 -0
  28. package/locales/cy.json +266 -0
  29. package/locales/da.json +261 -0
  30. package/locales/de.json +266 -0
  31. package/locales/el.json +268 -0
  32. package/locales/eo.json +261 -0
  33. package/locales/es-419.json +261 -0
  34. package/locales/es.json +266 -0
  35. package/locales/et.json +261 -0
  36. package/locales/eu.json +261 -0
  37. package/locales/fa.json +266 -0
  38. package/locales/fi.json +261 -0
  39. package/locales/fil.json +262 -0
  40. package/locales/forums.js +37 -0
  41. package/locales/fr.json +266 -0
  42. package/locales/fy.json +261 -0
  43. package/locales/ga.json +261 -0
  44. package/locales/gd.json +266 -0
  45. package/locales/gl.json +261 -0
  46. package/locales/ha.json +261 -0
  47. package/locales/he.json +266 -0
  48. package/locales/hi.json +266 -0
  49. package/locales/hr.json +266 -0
  50. package/locales/ht.json +261 -0
  51. package/locales/hu.json +267 -0
  52. package/locales/hy.json +261 -0
  53. package/locales/id.json +266 -0
  54. package/locales/is.json +261 -0
  55. package/locales/it.json +266 -0
  56. package/locales/ja-Hira.json +268 -0
  57. package/locales/ja.json +268 -0
  58. package/locales/ka.json +261 -0
  59. package/locales/kk.json +263 -0
  60. package/locales/km.json +261 -0
  61. package/locales/ko.json +261 -0
  62. package/locales/ku.json +263 -0
  63. package/locales/lt.json +261 -0
  64. package/locales/lv.json +261 -0
  65. package/locales/mi.json +261 -0
  66. package/locales/mn.json +262 -0
  67. package/locales/nb.json +266 -0
  68. package/locales/nl.json +266 -0
  69. package/locales/nn.json +261 -0
  70. package/locales/nso.json +261 -0
  71. package/locales/oc.json +261 -0
  72. package/locales/or.json +262 -0
  73. package/locales/pl.json +266 -0
  74. package/locales/pt-br.json +261 -0
  75. package/locales/pt.json +266 -0
  76. package/locales/qu.json +261 -0
  77. package/locales/rap.json +263 -0
  78. package/locales/ro.json +266 -0
  79. package/locales/ru.json +266 -0
  80. package/locales/sk.json +261 -0
  81. package/locales/sl.json +266 -0
  82. package/locales/sr.json +261 -0
  83. package/locales/sv.json +261 -0
  84. package/locales/sw.json +261 -0
  85. package/locales/th.json +261 -0
  86. package/locales/tn.json +261 -0
  87. package/locales/tr.json +269 -0
  88. package/locales/uk.json +261 -0
  89. package/locales/uz.json +262 -0
  90. package/locales/vi.json +262 -0
  91. package/locales/xh.json +261 -0
  92. package/locales/zh-cn.json +266 -0
  93. package/locales/zh-tw.json +266 -0
  94. package/locales/zu.json +261 -0
  95. package/package.json +77 -0
  96. package/scratch3/blocks.js +959 -0
  97. package/scratch3/draw.js +404 -0
  98. package/scratch3/index.js +12 -0
  99. package/scratch3/style.css.js +226 -0
  100. package/scratch3/style.js +807 -0
  101. package/syntax/blocks.js +467 -0
  102. package/syntax/commands.js +1692 -0
  103. package/syntax/extensions.js +33 -0
  104. package/syntax/index.js +17 -0
  105. package/syntax/model.js +427 -0
  106. package/syntax/syntax.js +941 -0
@@ -0,0 +1,941 @@
1
+ function assert(bool, message) {
2
+ if (!bool) {
3
+ throw new Error(`Assertion failed! ${message || ""}`)
4
+ }
5
+ }
6
+
7
+ import {
8
+ Label,
9
+ Icon,
10
+ Input,
11
+ Newline,
12
+ Block,
13
+ Comment,
14
+ Glow,
15
+ Script,
16
+ Document,
17
+ } from "./model.js"
18
+
19
+ import {
20
+ allLanguages,
21
+ lookupDropdown,
22
+ hexColorPat,
23
+ minifyHash,
24
+ lookupHash,
25
+ hashSpec,
26
+ applyOverrides,
27
+ rtlLanguages,
28
+ iconPat,
29
+ blockName,
30
+ } from "./blocks.js"
31
+
32
+ function paintBlock(info, children, languages) {
33
+ let overrides = []
34
+ if (Array.isArray(children[children.length - 1])) {
35
+ overrides = children.pop()
36
+ }
37
+
38
+ // build hash
39
+ const words = []
40
+ for (const child of children) {
41
+ if (child.isLabel) {
42
+ words.push(child.value)
43
+ } else if (child.isIcon) {
44
+ words.push(`@${child.name}`)
45
+ } else {
46
+ words.push("_")
47
+ }
48
+ }
49
+ const string = words.join(" ")
50
+ const shortHash = (info.hash = minifyHash(string))
51
+
52
+ // paint
53
+ const o = lookupHash(shortHash, info, children, languages)
54
+ let lang
55
+ let type
56
+ if (o) {
57
+ lang = o.lang
58
+ type = o.type
59
+ info.language = lang
60
+ info.isRTL = rtlLanguages.includes(lang.code)
61
+
62
+ if (
63
+ type.shape === "ring" ? info.shape === "reporter" : info.shape === "stack"
64
+ ) {
65
+ info.shape = type.shape
66
+ }
67
+ info.category = type.category
68
+ info.categoryIsDefault = true
69
+ // store selector, used for translation among other things
70
+ if (type.selector) {
71
+ info.selector = type.selector
72
+ }
73
+ if (type.id) {
74
+ info.id = type.id
75
+ }
76
+ info.hasLoopArrow = type.hasLoopArrow
77
+
78
+ // ellipsis block
79
+ if (type.spec === ". . .") {
80
+ children = [new Label(". . .")]
81
+ }
82
+ } else {
83
+ // The block was not recognised, so we check if it's a define block.
84
+ //
85
+ // We check for built-in blocks first to avoid ambiguity, e.g. the
86
+ // `defina o tamanho como (100) %` block in pt_BR.
87
+ for (const lang of languages) {
88
+ if (!isDefineBlock(children, lang)) {
89
+ continue
90
+ }
91
+
92
+ // Setting the shape also triggers some logic in recogniseStuff.
93
+ info.shape = "define-hat"
94
+ info.category = "custom"
95
+
96
+ // Move the children of the define block into an "outline", transforming
97
+ // () and [] shapes as we go.
98
+ const outlineChildren = children
99
+ .splice(
100
+ lang.definePrefix.length,
101
+ children.length - lang.defineSuffix.length,
102
+ )
103
+ .map(child => {
104
+ if (child.isInput && child.isBoolean) {
105
+ // Convert empty boolean slot to empty boolean argument.
106
+ child = paintBlock(
107
+ {
108
+ shape: "boolean",
109
+ argument: "boolean",
110
+ category: "custom-arg",
111
+ },
112
+ [new Label("")],
113
+ languages,
114
+ )
115
+ } else if (
116
+ child.isInput &&
117
+ (child.shape === "string" || child.shape === "number")
118
+ ) {
119
+ // Convert string inputs to string arguments, number inputs to number arguments.
120
+ const labels = child.value.split(/ +/g).map(word => new Label(word))
121
+ child = paintBlock(
122
+ {
123
+ shape: "reporter",
124
+ argument: child.shape === "string" ? "string" : "number",
125
+ category: "custom-arg",
126
+ },
127
+ labels,
128
+ languages,
129
+ )
130
+ } else if (child.isReporter || child.isBoolean) {
131
+ // Convert variables to number arguments, predicates to boolean arguments.
132
+ if (child.info.categoryIsDefault) {
133
+ child.info.category = "custom-arg"
134
+ child.info.argument = child.isBoolean ? "boolean" : "number"
135
+ }
136
+ }
137
+ return child
138
+ })
139
+
140
+ const outlineInfo = {
141
+ shape: "outline",
142
+ category: "custom",
143
+ categoryIsDefault: true,
144
+ hasLoopArrow: false,
145
+ }
146
+ const outline = new Block(outlineInfo, outlineChildren)
147
+ children.splice(lang.definePrefix.length, 0, outline)
148
+ break
149
+ }
150
+ }
151
+
152
+ // Apply overrides.
153
+ applyOverrides(info, overrides)
154
+
155
+ // loop arrows
156
+ if (info.hasLoopArrow) {
157
+ children.push(new Icon("loopArrow"))
158
+ }
159
+
160
+ const block = new Block(info, children)
161
+
162
+ // image replacement
163
+ if (type && iconPat.test(type.spec)) {
164
+ block.translate(lang, true)
165
+ }
166
+
167
+ // diffs
168
+ if (info.diff === "+") {
169
+ return new Glow(block)
170
+ }
171
+ block.diff = info.diff
172
+
173
+ return block
174
+ }
175
+
176
+ function isDefineBlock(children, lang) {
177
+ if (children.length < lang.definePrefix.length) {
178
+ return false
179
+ }
180
+ if (children.length < lang.defineSuffix.length) {
181
+ return false
182
+ }
183
+
184
+ for (let i = 0; i < lang.definePrefix.length; i++) {
185
+ const defineWord = lang.definePrefix[i]
186
+ const child = children[i]
187
+ if (!child.isLabel || minifyHash(child.value) !== minifyHash(defineWord)) {
188
+ return false
189
+ }
190
+ }
191
+
192
+ for (let i = 1; i <= lang.defineSuffix.length; i++) {
193
+ const defineWord = lang.defineSuffix[lang.defineSuffix.length - i]
194
+ const child = children[children.length - i]
195
+ if (!child.isLabel || minifyHash(child.value) !== minifyHash(defineWord)) {
196
+ return false
197
+ }
198
+ }
199
+
200
+ return true
201
+ }
202
+
203
+ function parseLines(code, languages) {
204
+ let tok = code[0]
205
+ let index = 0
206
+ function next() {
207
+ tok = code[++index]
208
+ }
209
+ function peek() {
210
+ return code[index + 1]
211
+ }
212
+ function peekNonWs() {
213
+ for (let i = index + 1; i < code.length; i++) {
214
+ if (code[i] !== " ") {
215
+ return code[i]
216
+ }
217
+ }
218
+ }
219
+ let sawNL
220
+
221
+ let define = []
222
+ languages.map(lang => {
223
+ define = define.concat(lang.define)
224
+ })
225
+
226
+ function makeBlock(shape, children) {
227
+ const hasInputs = children.filter(x => !x.isLabel).length
228
+
229
+ const info = {
230
+ shape: shape,
231
+ category: shape === "reporter" && !hasInputs ? "variables" : "obsolete",
232
+ categoryIsDefault: true,
233
+ hasLoopArrow: false,
234
+ }
235
+
236
+ return paintBlock(info, children, languages)
237
+ }
238
+
239
+ function makeMenu(shape, value) {
240
+ const menu = lookupDropdown(value, languages) || value
241
+ return new Input(shape, value, menu)
242
+ }
243
+
244
+ function pParts(end) {
245
+ const children = []
246
+ let label
247
+ while (tok && tok !== "\n") {
248
+ // So that comparison operators `<()<()>` and `<()>()>` don't need the
249
+ // central <> escaped, we interpret it as a label if particular
250
+ // conditions are met.
251
+ if (
252
+ (tok === "<" || tok === ">") &&
253
+ end === ">" && // We're parsing a predicate.
254
+ children.length === 1 && // There's exactly one AST node behind us.
255
+ !children[children.length - 1].isLabel // That node is not a label.
256
+ ) {
257
+ const c = peekNonWs()
258
+ // The next token starts some kind of input.
259
+ if (c === "[" || c === "(" || c === "<" || c === "{") {
260
+ label = null
261
+ children.push(new Label(tok))
262
+ next()
263
+ continue
264
+ }
265
+ }
266
+ if (tok === end) {
267
+ break
268
+ }
269
+ if (tok === "/" && peek() === "/" && !end) {
270
+ break
271
+ }
272
+
273
+ switch (tok) {
274
+ case "[":
275
+ label = null
276
+ children.push(peek() === "]" ? pEmptyTypedInput("array") : pString())
277
+ break
278
+ case "(":
279
+ label = null
280
+ children.push(pReporter())
281
+ break
282
+ case "<":
283
+ label = null
284
+ children.push(pPredicate())
285
+ break
286
+ case "{":
287
+ label = null
288
+ children.push(
289
+ peek() === "}" ? pEmptyTypedInput("object") : pEmbedded(),
290
+ )
291
+ break
292
+ case " ":
293
+ case "\t":
294
+ next() // Skip over whitespace.
295
+ label = null
296
+ break
297
+ case "◂":
298
+ case "▸":
299
+ children.push(pIcon())
300
+ label = null
301
+ break
302
+ case "@": {
303
+ next()
304
+ let name = ""
305
+ while (tok && /[a-zA-Z]/.test(tok)) {
306
+ name += tok
307
+ next()
308
+ }
309
+ if (name === "cloud") {
310
+ children.push(new Label("☁"))
311
+ } else {
312
+ children.push(
313
+ Object.prototype.hasOwnProperty.call(Icon.icons, name)
314
+ ? new Icon(name)
315
+ : new Label(`@${name}`),
316
+ )
317
+ }
318
+ label = null
319
+ break
320
+ }
321
+ case "\\":
322
+ if (peek() === "n") {
323
+ next()
324
+ next()
325
+ children.push(new Newline())
326
+ label = null
327
+ break
328
+ }
329
+ next() // escape character
330
+ // fallthrough
331
+ case ":":
332
+ if (tok === ":" && peek() === ":") {
333
+ children.push(pOverrides(end))
334
+ return children
335
+ }
336
+ // fallthrough
337
+ default:
338
+ if (!label) {
339
+ children.push((label = new Label("")))
340
+ }
341
+ label.value += tok
342
+ next()
343
+ }
344
+ }
345
+ return children
346
+ }
347
+
348
+ function pString() {
349
+ next() // '['
350
+ let s = ""
351
+ let escapeV = false
352
+ while (tok && tok !== "]" && tok !== "\n") {
353
+ if (tok === "\\") {
354
+ next()
355
+ if (tok === "v") {
356
+ escapeV = true
357
+ }
358
+ if (!tok) {
359
+ break
360
+ }
361
+ } else {
362
+ escapeV = false
363
+ }
364
+ s += tok
365
+ next()
366
+ }
367
+ if (tok === "]") {
368
+ next()
369
+ }
370
+ if (hexColorPat.test(s)) {
371
+ return new Input("color", s)
372
+ }
373
+ return !escapeV && / v$/.test(s)
374
+ ? makeMenu("dropdown", s.slice(0, s.length - 2))
375
+ : new Input("string", s)
376
+ }
377
+
378
+ function pEmptyTypedInput(shape) {
379
+ next()
380
+ next()
381
+ return new Input(shape, "")
382
+ }
383
+
384
+ function pBlock(end) {
385
+ if (!end && tok === "{" && isWrappedBlock("object")) {
386
+ return pWrappedBlock("object")
387
+ }
388
+ if (!end && tok === "[" && isWrappedBlock("array")) {
389
+ return pWrappedBlock("array")
390
+ }
391
+ const children = pParts(end)
392
+ if (tok && tok === "\n") {
393
+ sawNL = true
394
+ next()
395
+ }
396
+ if (children.length === 0) {
397
+ return
398
+ }
399
+
400
+ // standalone reporters
401
+ if (children.length === 1) {
402
+ const child = children[0]
403
+ if (
404
+ child.isBlock &&
405
+ (child.isReporter || child.isBoolean || child.isRing)
406
+ ) {
407
+ return child
408
+ }
409
+ }
410
+
411
+ return makeBlock("stack", children)
412
+ }
413
+
414
+ function isWrappedBlock(shape) {
415
+ const lineEnd = code.indexOf("\n", index)
416
+ const line = code.slice(index, lineEnd === -1 ? code.length : lineEnd)
417
+ const close = shape === "object" ? "}" : "]"
418
+ if (!line.endsWith(close)) {
419
+ return false
420
+ }
421
+ if (shape === "array" && / v\]$/.test(line) && !line.includes("::")) {
422
+ return false
423
+ }
424
+ return true
425
+ }
426
+
427
+ function pWrappedBlock(shape) {
428
+ const close = shape === "object" ? "}" : "]"
429
+ next()
430
+ const children = pParts(close)
431
+ if (tok === close) {
432
+ next()
433
+ }
434
+ if (tok === "\n") {
435
+ sawNL = true
436
+ next()
437
+ }
438
+ return makeBlock(shape, children)
439
+ }
440
+
441
+ function pReporter() {
442
+ next() // '('
443
+
444
+ // empty number-dropdown
445
+ if (tok === " ") {
446
+ next()
447
+ if (tok === "v" && peek() === ")") {
448
+ next()
449
+ next()
450
+ return new Input("number-dropdown", "")
451
+ }
452
+ }
453
+
454
+ const children = pParts(")")
455
+ if (tok && tok === ")") {
456
+ next()
457
+ }
458
+
459
+ // empty numbers
460
+ if (children.length === 0) {
461
+ return new Input("number", "")
462
+ }
463
+
464
+ // number
465
+ if (children.length === 1 && children[0].isLabel) {
466
+ const value = children[0].value
467
+ if (/^[0-9e.-]*$/.test(value)) {
468
+ return new Input("number", value)
469
+ }
470
+ if (hexColorPat.test(value)) {
471
+ return new Input("color", value)
472
+ }
473
+ }
474
+
475
+ // number-dropdown
476
+ if (children.length > 1 && children.every(child => child.isLabel)) {
477
+ const last = children[children.length - 1]
478
+ if (last.value === "v") {
479
+ children.pop()
480
+ const value = children.map(l => l.value).join(" ")
481
+ return makeMenu("number-dropdown", value)
482
+ }
483
+ }
484
+
485
+ const block = makeBlock("reporter", children)
486
+
487
+ // rings
488
+ if (block.info && block.info.shape === "ring") {
489
+ const first = block.children[0]
490
+ if (
491
+ first &&
492
+ first.isInput &&
493
+ first.shape === "number" &&
494
+ first.value === ""
495
+ ) {
496
+ block.children[0] = new Input("reporter")
497
+ } else if (
498
+ (first && first.isScript && first.isEmpty) ||
499
+ (first && first.isBlock && !first.children.length)
500
+ ) {
501
+ block.children[0] = new Input("stack")
502
+ }
503
+ }
504
+
505
+ return block
506
+ }
507
+
508
+ function pPredicate() {
509
+ next() // '<'
510
+ const children = pParts(">")
511
+ if (tok && tok === ">") {
512
+ next()
513
+ }
514
+ if (children.length === 0) {
515
+ return new Input("boolean")
516
+ }
517
+ return makeBlock("boolean", children)
518
+ }
519
+
520
+ function pEmbedded() {
521
+ next() // '{'
522
+
523
+ sawNL = false
524
+ const f = function () {
525
+ while (tok && tok !== "}") {
526
+ const block = pBlock("}")
527
+ if (block) {
528
+ return block
529
+ }
530
+ }
531
+ }
532
+ const scripts = parseScripts(f)
533
+ let blocks = []
534
+ scripts.forEach(script => {
535
+ blocks = blocks.concat(script.blocks)
536
+ })
537
+
538
+ if (tok === "}") {
539
+ next()
540
+ }
541
+ if (!sawNL) {
542
+ assert(blocks.length <= 1)
543
+ return blocks.length ? blocks[0] : makeBlock("stack", [])
544
+ }
545
+ return new Script(blocks)
546
+ }
547
+
548
+ function pIcon() {
549
+ const c = tok
550
+ next()
551
+ switch (c) {
552
+ case "▸":
553
+ return new Icon("addInput")
554
+ case "◂":
555
+ return new Icon("delInput")
556
+ default:
557
+ return
558
+ }
559
+ }
560
+
561
+ function pOverrides(end) {
562
+ next()
563
+ next()
564
+ const overrides = []
565
+ let override = ""
566
+ while (tok && tok !== "\n" && tok !== end) {
567
+ if (tok === " ") {
568
+ if (override) {
569
+ overrides.push(override)
570
+ override = ""
571
+ }
572
+ } else if (tok === "/" && peek() === "/") {
573
+ break
574
+ } else {
575
+ override += tok
576
+ }
577
+ next()
578
+ }
579
+ if (override) {
580
+ overrides.push(override)
581
+ }
582
+ return overrides
583
+ }
584
+
585
+ function pComment(end) {
586
+ next()
587
+ next()
588
+ let comment = ""
589
+ while (tok && tok !== "\n" && tok !== end) {
590
+ comment += tok
591
+ next()
592
+ }
593
+ if (tok && tok === "\n") {
594
+ next()
595
+ }
596
+ return new Comment(comment, true)
597
+ }
598
+
599
+ function pLine() {
600
+ let diff
601
+ if (tok === "+" || tok === "-") {
602
+ diff = tok
603
+ next()
604
+ }
605
+ const block = pBlock()
606
+ if (tok === "/" && peek() === "/") {
607
+ const comment = pComment()
608
+ comment.hasBlock = block && block.children.length
609
+ if (!comment.hasBlock) {
610
+ return comment
611
+ }
612
+ block.comment = comment
613
+ }
614
+ if (block) {
615
+ block.diff = diff
616
+ }
617
+ return block
618
+ }
619
+
620
+ return () => {
621
+ if (!tok) {
622
+ return undefined
623
+ }
624
+ const line = pLine()
625
+ return line || "NL"
626
+ }
627
+ }
628
+
629
+ /* * */
630
+
631
+ function parseScripts(getLine) {
632
+ let line = getLine()
633
+ function next() {
634
+ line = getLine()
635
+ }
636
+
637
+ function pFile() {
638
+ while (line === "NL") {
639
+ next()
640
+ }
641
+ const scripts = []
642
+ while (line) {
643
+ let blocks = []
644
+ while (line && line !== "NL") {
645
+ let b = pLine()
646
+ const isGlow = b.diff === "+"
647
+ if (isGlow) {
648
+ b.diff = null
649
+ }
650
+
651
+ if (b.isElse || b.isEnd) {
652
+ b = new Block({ ...b.info, shape: "stack" }, b.children)
653
+ }
654
+
655
+ if (isGlow) {
656
+ const last = blocks[blocks.length - 1]
657
+ let children = []
658
+ if (last && last.isGlow) {
659
+ blocks.pop()
660
+ children = last.child.isScript ? last.child.blocks : [last.child]
661
+ }
662
+ children.push(b)
663
+ blocks.push(new Glow(new Script(children)))
664
+ } else if (b.isHat) {
665
+ if (blocks.length) {
666
+ scripts.push(new Script(blocks))
667
+ }
668
+ blocks = [b]
669
+ } else if (b.isFinal) {
670
+ blocks.push(b)
671
+ break
672
+ } else if (b.isCommand) {
673
+ blocks.push(b)
674
+ } else {
675
+ // reporter or predicate
676
+ if (blocks.length) {
677
+ scripts.push(new Script(blocks))
678
+ }
679
+ scripts.push(new Script([b]))
680
+ blocks = []
681
+ break
682
+ }
683
+ }
684
+ if (blocks.length) {
685
+ scripts.push(new Script(blocks))
686
+ }
687
+ while (line === "NL") {
688
+ next()
689
+ }
690
+ }
691
+ return scripts
692
+ }
693
+
694
+ function pLine() {
695
+ const b = line
696
+ next()
697
+
698
+ if (b.hasScript) {
699
+ while (true) {
700
+ const blocks = pMouth()
701
+ b.children.push(new Script(blocks))
702
+ if (line && line.isElse) {
703
+ for (const child of line.children) {
704
+ b.children.push(child)
705
+ }
706
+ next()
707
+ continue
708
+ }
709
+ if (line && line.isEnd) {
710
+ next()
711
+ }
712
+ break
713
+ }
714
+ }
715
+ return b
716
+ }
717
+
718
+ function pMouth() {
719
+ const blocks = []
720
+ while (line) {
721
+ if (line === "NL") {
722
+ next()
723
+ continue
724
+ }
725
+ if (!line.isCommand) {
726
+ return blocks
727
+ }
728
+
729
+ const b = pLine()
730
+ const isGlow = b.diff === "+"
731
+ if (isGlow) {
732
+ b.diff = null
733
+ }
734
+
735
+ if (isGlow) {
736
+ const last = blocks[blocks.length - 1]
737
+ let children = []
738
+ if (last && last.isGlow) {
739
+ blocks.pop()
740
+ children = last.child.isScript ? last.child.blocks : [last.child]
741
+ }
742
+ children.push(b)
743
+ blocks.push(new Glow(new Script(children)))
744
+ } else {
745
+ blocks.push(b)
746
+ }
747
+ }
748
+ return blocks
749
+ }
750
+
751
+ return pFile()
752
+ }
753
+
754
+ /* * */
755
+
756
+ function eachBlock(x, cb) {
757
+ if (x.isScript) {
758
+ x.blocks = x.blocks.map(block => {
759
+ eachBlock(block, cb)
760
+ return cb(block) || block
761
+ })
762
+ } else if (x.isBlock) {
763
+ x.children = x.children.map(child => {
764
+ eachBlock(child, cb)
765
+ return cb(child) || child
766
+ })
767
+ } else if (x.isGlow) {
768
+ eachBlock(x.child, cb)
769
+ }
770
+ }
771
+
772
+ const listBlocks = {
773
+ "append:toList:": 1,
774
+ "deleteLine:ofList:": 1,
775
+ "insert:at:ofList:": 2,
776
+ "setLine:ofList:to:": 1,
777
+ "showList:": 0,
778
+ "hideList:": 0,
779
+ }
780
+
781
+ function recogniseStuff(scripts) {
782
+ const customBlocksByHash = Object.create(null)
783
+ const listNames = new Set()
784
+
785
+ scripts.forEach(script => {
786
+ const customArgs = new Set()
787
+
788
+ eachBlock(script, block => {
789
+ if (!block.isBlock) {
790
+ return
791
+ }
792
+
793
+ // custom blocks
794
+ if (block.info.shape === "define-hat") {
795
+ // There should be exactly one `outline` child, added in paintBlock.
796
+ const outline = block.children.find(child => child.isOutline)
797
+ if (!outline) {
798
+ return
799
+ }
800
+
801
+ const names = []
802
+ const parts = []
803
+ for (const child of outline.children) {
804
+ if (child.isLabel) {
805
+ parts.push(child.value)
806
+ } else if (child.isBlock) {
807
+ if (!child.info.argument) {
808
+ return
809
+ }
810
+ parts.push(
811
+ {
812
+ number: "%n",
813
+ string: "%s",
814
+ boolean: "%b",
815
+ }[child.info.argument],
816
+ )
817
+
818
+ const name = blockName(child)
819
+ names.push(name)
820
+ customArgs.add(name)
821
+ }
822
+ }
823
+ const spec = parts.join(" ")
824
+ const hash = hashSpec(spec)
825
+
826
+ const info = {
827
+ spec: spec,
828
+ names: names,
829
+ }
830
+ if (!customBlocksByHash[hash]) {
831
+ customBlocksByHash[hash] = info
832
+ }
833
+ block.info.id = "PROCEDURES_DEFINITION"
834
+ block.info.selector = "procDef"
835
+ block.info.call = info.spec
836
+ block.info.names = info.names
837
+ block.info.category = "custom"
838
+
839
+ // custom arguments
840
+ } else if (
841
+ block.info.categoryIsDefault &&
842
+ (block.isReporter || block.isBoolean)
843
+ ) {
844
+ const name = blockName(block)
845
+ if (customArgs.has(name)) {
846
+ block.info.category = "custom-arg"
847
+ block.info.categoryIsDefault = false
848
+ block.info.selector = "getParam"
849
+ }
850
+
851
+ // list names
852
+ } else if (
853
+ Object.prototype.hasOwnProperty.call(listBlocks, block.info.selector)
854
+ ) {
855
+ const argIndex = listBlocks[block.info.selector]
856
+ const inputs = block.children.filter(child => !child.isLabel)
857
+ const input = inputs[argIndex]
858
+ if (input && input.isInput) {
859
+ listNames.add(input.value)
860
+ }
861
+ }
862
+ })
863
+ })
864
+
865
+ scripts.forEach(script => {
866
+ eachBlock(script, block => {
867
+ if (
868
+ block.info &&
869
+ block.info.categoryIsDefault &&
870
+ block.info.category === "obsolete"
871
+ ) {
872
+ // custom blocks
873
+ const info = customBlocksByHash[block.info.hash]
874
+ if (info) {
875
+ block.info.selector = "call"
876
+ block.info.call = info.spec
877
+ block.info.names = info.names
878
+ block.info.category = "custom"
879
+ }
880
+ return
881
+ }
882
+
883
+ let name, info
884
+ if (
885
+ block.isReporter &&
886
+ block.info.category === "variables" &&
887
+ block.info.categoryIsDefault
888
+ ) {
889
+ // We set the selector here for some reason
890
+ block.info.selector = "readVariable"
891
+ name = blockName(block)
892
+ info = block.info
893
+ }
894
+ if (!name) {
895
+ return
896
+ }
897
+
898
+ // list reporters
899
+ if (listNames.has(name)) {
900
+ info.category = "list"
901
+ info.categoryIsDefault = false
902
+ info.selector = "contentsOfList:"
903
+ }
904
+
905
+ return // already done
906
+ })
907
+ })
908
+ }
909
+
910
+ export function parse(code, options) {
911
+ options = {
912
+ inline: false,
913
+ languages: ["en"],
914
+ ...options,
915
+ }
916
+
917
+ if (options.dialect) {
918
+ throw new Error("Option 'dialect' no longer supported")
919
+ }
920
+
921
+ code = code.replace(/&lt;/g, "<")
922
+ code = code.replace(/&gt;/g, ">")
923
+ if (options.inline) {
924
+ code = code.replace(/\n/g, " ")
925
+ }
926
+
927
+ const languages = options.languages.map(code => {
928
+ const lang = allLanguages[code]
929
+ if (!lang) {
930
+ throw new Error(`Unknown language: '${code}'`)
931
+ }
932
+ return lang
933
+ })
934
+
935
+ /* * */
936
+
937
+ const f = parseLines(code, languages)
938
+ const scripts = parseScripts(f)
939
+ recogniseStuff(scripts)
940
+ return new Document(scripts)
941
+ }