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