@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.
- package/LICENSE +18 -0
- package/README.md +205 -0
- package/browser.es.js +8 -0
- package/browser.js +8 -0
- package/build/scratchblocks.min.es.js +11 -0
- package/build/scratchblocks.min.es.js.map +1 -0
- package/build/scratchblocks.min.js +11 -0
- package/build/scratchblocks.min.js.map +1 -0
- package/build/translations-all-es.js +10 -0
- package/build/translations-all.js +10 -0
- package/build/translations-es.js +10 -0
- package/build/translations.js +10 -0
- package/index.js +163 -0
- package/locales/ab.json +261 -0
- package/locales/af.json +261 -0
- package/locales/all.js +161 -0
- package/locales/am.json +262 -0
- package/locales/an.json +261 -0
- package/locales/ar.json +261 -0
- package/locales/ast.json +261 -0
- package/locales/az.json +262 -0
- package/locales/be.json +261 -0
- package/locales/bg.json +261 -0
- package/locales/bn.json +261 -0
- package/locales/ca.json +267 -0
- package/locales/ckb.json +261 -0
- package/locales/cs.json +267 -0
- package/locales/cy.json +266 -0
- package/locales/da.json +261 -0
- package/locales/de.json +266 -0
- package/locales/el.json +268 -0
- package/locales/eo.json +261 -0
- package/locales/es-419.json +261 -0
- package/locales/es.json +266 -0
- package/locales/et.json +261 -0
- package/locales/eu.json +261 -0
- package/locales/fa.json +266 -0
- package/locales/fi.json +261 -0
- package/locales/fil.json +262 -0
- package/locales/forums.js +37 -0
- package/locales/fr.json +266 -0
- package/locales/fy.json +261 -0
- package/locales/ga.json +261 -0
- package/locales/gd.json +266 -0
- package/locales/gl.json +261 -0
- package/locales/ha.json +261 -0
- package/locales/he.json +266 -0
- package/locales/hi.json +266 -0
- package/locales/hr.json +266 -0
- package/locales/ht.json +261 -0
- package/locales/hu.json +267 -0
- package/locales/hy.json +261 -0
- package/locales/id.json +266 -0
- package/locales/is.json +261 -0
- package/locales/it.json +266 -0
- package/locales/ja-Hira.json +268 -0
- package/locales/ja.json +268 -0
- package/locales/ka.json +261 -0
- package/locales/kk.json +263 -0
- package/locales/km.json +261 -0
- package/locales/ko.json +261 -0
- package/locales/ku.json +263 -0
- package/locales/lt.json +261 -0
- package/locales/lv.json +261 -0
- package/locales/mi.json +261 -0
- package/locales/mn.json +262 -0
- package/locales/nb.json +266 -0
- package/locales/nl.json +266 -0
- package/locales/nn.json +261 -0
- package/locales/nso.json +261 -0
- package/locales/oc.json +261 -0
- package/locales/or.json +262 -0
- package/locales/pl.json +266 -0
- package/locales/pt-br.json +261 -0
- package/locales/pt.json +266 -0
- package/locales/qu.json +261 -0
- package/locales/rap.json +263 -0
- package/locales/ro.json +266 -0
- package/locales/ru.json +266 -0
- package/locales/sk.json +261 -0
- package/locales/sl.json +266 -0
- package/locales/sr.json +261 -0
- package/locales/sv.json +261 -0
- package/locales/sw.json +261 -0
- package/locales/th.json +261 -0
- package/locales/tn.json +261 -0
- package/locales/tr.json +269 -0
- package/locales/uk.json +261 -0
- package/locales/uz.json +262 -0
- package/locales/vi.json +262 -0
- package/locales/xh.json +261 -0
- package/locales/zh-cn.json +266 -0
- package/locales/zh-tw.json +266 -0
- package/locales/zu.json +261 -0
- package/package.json +77 -0
- package/scratch3/blocks.js +959 -0
- package/scratch3/draw.js +404 -0
- package/scratch3/index.js +12 -0
- package/scratch3/style.css.js +226 -0
- package/scratch3/style.js +807 -0
- package/syntax/blocks.js +467 -0
- package/syntax/commands.js +1692 -0
- package/syntax/extensions.js +33 -0
- package/syntax/index.js +17 -0
- package/syntax/model.js +427 -0
- package/syntax/syntax.js +941 -0
package/syntax/syntax.js
ADDED
|
@@ -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(/</g, "<")
|
|
922
|
+
code = code.replace(/>/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
|
+
}
|