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.
- package/LICENSE +19 -0
- package/README.md +193 -0
- package/browser.es.js +8 -0
- package/browser.js +8 -0
- package/build/scratchblocks-plus.min.es.js +12 -0
- package/build/scratchblocks-plus.min.es.js.map +1 -0
- package/build/scratchblocks-plus.min.js +12 -0
- package/build/scratchblocks-plus.min.js.map +1 -0
- package/build/translations-all-es.js +11 -0
- package/build/translations-all-es.js.map +1 -0
- package/build/translations-all.js +11 -0
- package/build/translations-all.js.map +1 -0
- package/build/translations-es.js +11 -0
- package/build/translations-es.js.map +1 -0
- package/build/translations.js +11 -0
- package/build/translations.js.map +1 -0
- package/index.d.ts +297 -0
- package/index.js +229 -0
- package/locales/ab.json +1630 -0
- package/locales/af.json +1630 -0
- package/locales/all.d.ts +108 -0
- package/locales/all.js +161 -0
- package/locales/am.json +1925 -0
- package/locales/an.json +1630 -0
- package/locales/ar.json +1924 -0
- package/locales/ast.json +1630 -0
- package/locales/az.json +1925 -0
- package/locales/be.json +1630 -0
- package/locales/bg.json +1924 -0
- package/locales/bn.json +1630 -0
- package/locales/ca.json +1930 -0
- package/locales/ckb.json +1630 -0
- package/locales/cs.json +1930 -0
- package/locales/cy.json +1929 -0
- package/locales/da.json +1924 -0
- package/locales/de.json +1929 -0
- package/locales/el.json +1931 -0
- package/locales/eo.json +1630 -0
- package/locales/es-419.json +1924 -0
- package/locales/es.json +1929 -0
- package/locales/et.json +1924 -0
- package/locales/eu.json +1924 -0
- package/locales/fa.json +1929 -0
- package/locales/fi.json +1924 -0
- package/locales/fil.json +1631 -0
- package/locales/forums.js +37 -0
- package/locales/fr.json +1929 -0
- package/locales/fy.json +1630 -0
- package/locales/ga.json +1924 -0
- package/locales/gd.json +1929 -0
- package/locales/gl.json +1924 -0
- package/locales/ha.json +1630 -0
- package/locales/he.json +1929 -0
- package/locales/hi.json +1635 -0
- package/locales/hr.json +1929 -0
- package/locales/ht.json +1630 -0
- package/locales/hu.json +1930 -0
- package/locales/hy.json +1630 -0
- package/locales/id.json +1929 -0
- package/locales/is.json +1924 -0
- package/locales/it.json +1929 -0
- package/locales/ja-Hira.json +1637 -0
- package/locales/ja.json +1931 -0
- package/locales/ka.json +1630 -0
- package/locales/kk.json +1632 -0
- package/locales/km.json +1630 -0
- package/locales/ko.json +1924 -0
- package/locales/ku.json +1632 -0
- package/locales/lt.json +1924 -0
- package/locales/lv.json +1924 -0
- package/locales/mi.json +1924 -0
- package/locales/mn.json +1631 -0
- package/locales/nb.json +1929 -0
- package/locales/nl.json +1929 -0
- package/locales/nn.json +1630 -0
- package/locales/nso.json +1630 -0
- package/locales/oc.json +1630 -0
- package/locales/or.json +1631 -0
- package/locales/pl.json +1929 -0
- package/locales/pt-br.json +1924 -0
- package/locales/pt.json +1929 -0
- package/locales/qu.json +1630 -0
- package/locales/rap.json +1632 -0
- package/locales/ro.json +1929 -0
- package/locales/ru.json +1929 -0
- package/locales/sk.json +1924 -0
- package/locales/sl.json +1929 -0
- package/locales/sr.json +1924 -0
- package/locales/sv.json +1924 -0
- package/locales/sw.json +1630 -0
- package/locales/th.json +1924 -0
- package/locales/tn.json +1630 -0
- package/locales/tr.json +1932 -0
- package/locales/uk.json +1924 -0
- package/locales/uz.json +1631 -0
- package/locales/vi.json +1925 -0
- package/locales/xh.json +1630 -0
- package/locales/zh-cn.json +1930 -0
- package/locales/zh-tw.json +1930 -0
- package/locales/zu.json +1918 -0
- package/package.json +81 -0
- package/scratch2/blocks.js +1000 -0
- package/scratch2/draw.js +452 -0
- package/scratch2/filter.js +78 -0
- package/scratch2/index.js +12 -0
- package/scratch2/style.css.js +148 -0
- package/scratch2/style.js +214 -0
- package/scratch3/blocks.js +1134 -0
- package/scratch3/draw.js +334 -0
- package/scratch3/index.js +12 -0
- package/scratch3/style.css.js +280 -0
- package/scratch3/style.js +877 -0
- package/syntax/blocks.js +921 -0
- package/syntax/commands.js +1755 -0
- package/syntax/dropdowns.js +688 -0
- package/syntax/extensions.js +34 -0
- package/syntax/index.js +17 -0
- package/syntax/model.js +566 -0
- package/syntax/syntax.js +1091 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
When a new extension is added:
|
|
3
|
+
1) Add it to extensions object
|
|
4
|
+
2) Add its blocks to commands.js
|
|
5
|
+
3) Add icon width/height to scratch3/blocks.js IconView
|
|
6
|
+
4) Add icon to scratch3/style.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Moved extensions: key is scratch3, value is scratch2
|
|
10
|
+
export const movedExtensions = {
|
|
11
|
+
pen: "pen",
|
|
12
|
+
video: "sensing",
|
|
13
|
+
music: "sound",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const extensions = {
|
|
17
|
+
...movedExtensions,
|
|
18
|
+
faceSensing: "faceSensing",
|
|
19
|
+
tts: "tts",
|
|
20
|
+
translate: "translate",
|
|
21
|
+
microbit: "microbit",
|
|
22
|
+
gdxfor: "gdxfor",
|
|
23
|
+
wedo: "wedo",
|
|
24
|
+
makeymakey: "makeymakey",
|
|
25
|
+
ev3: "ev3",
|
|
26
|
+
boost: "boost",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Alias extensions: unlike movedExtensions, this is handled for both scratch2 and scratch3.
|
|
30
|
+
// Key is alias, value is real extension name
|
|
31
|
+
export const aliasExtensions = {
|
|
32
|
+
wedo2: "wedo",
|
|
33
|
+
text2speech: "tts",
|
|
34
|
+
}
|
package/syntax/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { parse } from "./syntax.js"
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
Label,
|
|
5
|
+
Icon,
|
|
6
|
+
Input,
|
|
7
|
+
Matrix,
|
|
8
|
+
Block,
|
|
9
|
+
Comment,
|
|
10
|
+
Glow,
|
|
11
|
+
Script,
|
|
12
|
+
Document,
|
|
13
|
+
} from "./model.js"
|
|
14
|
+
|
|
15
|
+
export { allLanguages, loadLanguages } from "./blocks.js"
|
|
16
|
+
|
|
17
|
+
export { extensions, movedExtensions, aliasExtensions } from "./extensions.js"
|
package/syntax/model.js
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
function assert(bool, message) {
|
|
2
|
+
if (!bool) {
|
|
3
|
+
throw new Error(`Assertion failed! ${message || ""}`)
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function indent(text) {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map(line => {
|
|
11
|
+
return ` ${line}`
|
|
12
|
+
})
|
|
13
|
+
.join("\n")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Compute display width of a string, counting common CJK / fullwidth
|
|
17
|
+
// characters as width 2 so alignment in mixed-language text looks correct.
|
|
18
|
+
function displayWidth(str) {
|
|
19
|
+
if (!str) {
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
str = String(str)
|
|
23
|
+
let w = 0
|
|
24
|
+
// Rough classification: common CJK ranges and fullwidth block
|
|
25
|
+
const wideRe =
|
|
26
|
+
/[\u2E80-\u2EFF\u2F00-\u2FDF\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF00-\uFFEF]/u
|
|
27
|
+
for (const ch of str) {
|
|
28
|
+
w += wideRe.test(ch) ? 2 : 1
|
|
29
|
+
}
|
|
30
|
+
return w
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
parseSpec,
|
|
35
|
+
inputPat,
|
|
36
|
+
parseInputNumber,
|
|
37
|
+
iconPat,
|
|
38
|
+
rtlLanguages,
|
|
39
|
+
unicodeIcons,
|
|
40
|
+
hexColorPat,
|
|
41
|
+
} from "./blocks.js"
|
|
42
|
+
|
|
43
|
+
export class Label {
|
|
44
|
+
constructor(value, cls) {
|
|
45
|
+
this.value = value
|
|
46
|
+
this.cls = cls || ""
|
|
47
|
+
this.el = null
|
|
48
|
+
this.height = 12
|
|
49
|
+
this.metrics = null
|
|
50
|
+
this.x = 0
|
|
51
|
+
}
|
|
52
|
+
get isLabel() {
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
stringify() {
|
|
57
|
+
if (this.value === "<" || this.value === ">") {
|
|
58
|
+
return this.value
|
|
59
|
+
}
|
|
60
|
+
return this.value
|
|
61
|
+
.replace(/([<>[\](){}\\])/g, "\\$1")
|
|
62
|
+
.replace(/:{2,}/g, m => ":" + "\\:".repeat(m.length - 1))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class Icon {
|
|
67
|
+
constructor(name) {
|
|
68
|
+
this.name = name
|
|
69
|
+
this.isArrow = name === "loopArrow"
|
|
70
|
+
|
|
71
|
+
assert(Icon.icons[name], `no info for icon ${name}`)
|
|
72
|
+
}
|
|
73
|
+
get isIcon() {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static get icons() {
|
|
78
|
+
return {
|
|
79
|
+
greenFlag: true,
|
|
80
|
+
stopSign: true,
|
|
81
|
+
turnLeft: true,
|
|
82
|
+
turnRight: true,
|
|
83
|
+
loopArrow: true,
|
|
84
|
+
addInput: true,
|
|
85
|
+
delInput: true,
|
|
86
|
+
list: true,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
stringify() {
|
|
91
|
+
return unicodeIcons[`@${this.name}`] || ""
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class Matrix {
|
|
96
|
+
constructor(rows) {
|
|
97
|
+
// rows should already be 2D array of booleans from parsing
|
|
98
|
+
const inputRows = Array.isArray(rows) ? rows : []
|
|
99
|
+
// Make shallow copies and coerce non-array entries into arrays
|
|
100
|
+
this.rows = inputRows.map(row => (Array.isArray(row) ? row.slice() : [row]))
|
|
101
|
+
|
|
102
|
+
// Determine maximum row length
|
|
103
|
+
const maxLen = this.rows.reduce((m, r) => Math.max(m, r.length), 0)
|
|
104
|
+
|
|
105
|
+
// Pad short rows with false to make all rows the same length
|
|
106
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
107
|
+
const r = this.rows[i]
|
|
108
|
+
while (r.length < maxLen) {
|
|
109
|
+
r.push(false)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get isMatrix() {
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
stringify() {
|
|
119
|
+
// Format as {row1,row2,row3,...} where each cell is 0 or 1
|
|
120
|
+
const rowStrings = this.rows.map(row =>
|
|
121
|
+
row.map(cell => (cell ? "1" : "0")).join(""),
|
|
122
|
+
)
|
|
123
|
+
return `{${rowStrings.join(",")}}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
translate() {
|
|
127
|
+
// Matrix doesn't need translation
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class Input {
|
|
132
|
+
constructor(shape, value) {
|
|
133
|
+
this.shape = shape
|
|
134
|
+
this.value = value
|
|
135
|
+
|
|
136
|
+
this.isRound = shape === "number" || shape === "number-dropdown"
|
|
137
|
+
this.isBoolean = shape === "boolean"
|
|
138
|
+
this.isStack = shape === "stack"
|
|
139
|
+
this.isInset =
|
|
140
|
+
shape === "boolean" || shape === "stack" || shape === "reporter"
|
|
141
|
+
this.isColor = shape === "color"
|
|
142
|
+
this.isMatrix = shape === "matrix"
|
|
143
|
+
this.hasArrow = shape === "dropdown" || shape === "number-dropdown"
|
|
144
|
+
this.isDarker =
|
|
145
|
+
shape === "boolean" || shape === "stack" || shape === "dropdown"
|
|
146
|
+
this.isSquare =
|
|
147
|
+
shape === "string" || shape === "color" || shape === "dropdown"
|
|
148
|
+
|
|
149
|
+
// Check if value is a Matrix object
|
|
150
|
+
const isMatrixValue = value && typeof value === "object" && value.isMatrix
|
|
151
|
+
|
|
152
|
+
this.hasLabel = !(
|
|
153
|
+
this.isColor ||
|
|
154
|
+
this.isInset ||
|
|
155
|
+
this.isMatrix ||
|
|
156
|
+
isMatrixValue
|
|
157
|
+
)
|
|
158
|
+
this.label = this.hasLabel
|
|
159
|
+
? new Label(value, `literal-${this.shape}`)
|
|
160
|
+
: null
|
|
161
|
+
this.x = 0
|
|
162
|
+
}
|
|
163
|
+
get isInput() {
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setMenu(value) {
|
|
168
|
+
this.menu = value
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setMenu(value) {
|
|
172
|
+
this.menu = value
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
stringify(parentPrefix = "") {
|
|
176
|
+
if (this.isColor) {
|
|
177
|
+
assert(this.value[0] === "#")
|
|
178
|
+
return `[${this.value}]`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Handle Matrix values
|
|
182
|
+
if (this.value && typeof this.value === "object" && this.value.isMatrix) {
|
|
183
|
+
const matrixStr = this.value.stringify()
|
|
184
|
+
// Calculate the indentation needed for alignment
|
|
185
|
+
// parentPrefix is the text before this input on the same line
|
|
186
|
+
// We need to account for: parentPrefix + "(" + "{"
|
|
187
|
+
const indentSpaces = Math.max(0, displayWidth(parentPrefix) + 2)
|
|
188
|
+
const indent = " ".repeat(indentSpaces)
|
|
189
|
+
|
|
190
|
+
// Split matrix string and add indentation to each line after the first
|
|
191
|
+
const parts = matrixStr.slice(1, -1).split(",") // Remove outer braces and split by comma
|
|
192
|
+
const formattedMatrix = parts
|
|
193
|
+
.map((part, index) => {
|
|
194
|
+
if (index === 0) {
|
|
195
|
+
return part
|
|
196
|
+
}
|
|
197
|
+
return `\n${indent}${part}`
|
|
198
|
+
})
|
|
199
|
+
.join(",")
|
|
200
|
+
|
|
201
|
+
return `({${formattedMatrix}} v)`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Order sensitive; see #439
|
|
205
|
+
let text = (this.value ? String(this.value) : "")
|
|
206
|
+
.replace(/([\]\\])/g, "\\$1")
|
|
207
|
+
.replace(this.isRound ? /([<>])/g : /$^/, "\\$1")
|
|
208
|
+
.replace(/ v$/, " \\v")
|
|
209
|
+
if (this.hasArrow) {
|
|
210
|
+
text += " v"
|
|
211
|
+
} else if (hexColorPat.test(text)) {
|
|
212
|
+
text = "\\" + text // Escape hex colors
|
|
213
|
+
}
|
|
214
|
+
return this.isRound
|
|
215
|
+
? `(${text})`
|
|
216
|
+
: this.isSquare
|
|
217
|
+
? `[${text}]`
|
|
218
|
+
: this.isBoolean
|
|
219
|
+
? "<>"
|
|
220
|
+
: this.isStack
|
|
221
|
+
? "{}"
|
|
222
|
+
: text
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
translate(lang) {
|
|
226
|
+
if (this.hasArrow) {
|
|
227
|
+
// Don't create label for Matrix values
|
|
228
|
+
if (this.value && typeof this.value === "object" && this.value.isMatrix) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
if (this.menu) {
|
|
232
|
+
this.value = lang.dropdowns[this.menu].value
|
|
233
|
+
}
|
|
234
|
+
this.label = new Label(this.value, `literal-${this.shape}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export class Block {
|
|
240
|
+
constructor(info, children, comment) {
|
|
241
|
+
assert(info)
|
|
242
|
+
this.info = { ...info }
|
|
243
|
+
this.children = children
|
|
244
|
+
this.comment = comment || null
|
|
245
|
+
this.diff = null
|
|
246
|
+
|
|
247
|
+
// Block path for identification (e.g., "1.2.1" = Script 1, Block 2, Child 1)
|
|
248
|
+
this.blockPath = null
|
|
249
|
+
|
|
250
|
+
const shape = this.info.shape
|
|
251
|
+
this.isHat = shape === "hat" || shape === "cat" || shape === "define-hat"
|
|
252
|
+
this.hasPuzzle =
|
|
253
|
+
shape === "stack" ||
|
|
254
|
+
shape === "hat" ||
|
|
255
|
+
shape === "cat" ||
|
|
256
|
+
shape === "c-block" ||
|
|
257
|
+
shape === "define-hat"
|
|
258
|
+
this.isFinal = /cap/.test(shape)
|
|
259
|
+
this.isCommand = shape === "stack" || shape === "cap" || /block/.test(shape)
|
|
260
|
+
this.isOutline = shape === "outline"
|
|
261
|
+
this.isReporter = shape === "reporter"
|
|
262
|
+
this.isBoolean = shape === "boolean"
|
|
263
|
+
|
|
264
|
+
this.isRing = shape === "ring"
|
|
265
|
+
this.hasScript = /block/.test(shape)
|
|
266
|
+
this.isElse = shape === "celse"
|
|
267
|
+
this.isEnd = shape === "cend"
|
|
268
|
+
}
|
|
269
|
+
get isBlock() {
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
stringify(extras) {
|
|
274
|
+
let firstInput = null
|
|
275
|
+
let checkAlias = false
|
|
276
|
+
let currentLinePrefix = ""
|
|
277
|
+
const parts = []
|
|
278
|
+
|
|
279
|
+
for (const child of this.children) {
|
|
280
|
+
if (child.isIcon) {
|
|
281
|
+
checkAlias = true
|
|
282
|
+
}
|
|
283
|
+
if (!firstInput && !(child.isLabel || child.isIcon)) {
|
|
284
|
+
firstInput = child
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (child.isScript) {
|
|
288
|
+
parts.push(`\n${indent(child.stringify())}\n`)
|
|
289
|
+
currentLinePrefix = ""
|
|
290
|
+
} else {
|
|
291
|
+
// Pass the current line prefix to child's stringify for alignment
|
|
292
|
+
const childStr = child.isInput
|
|
293
|
+
? child.stringify(currentLinePrefix)
|
|
294
|
+
: child.stringify()
|
|
295
|
+
|
|
296
|
+
const trimmed = childStr.trim()
|
|
297
|
+
parts.push(trimmed)
|
|
298
|
+
|
|
299
|
+
// Update prefix for next child
|
|
300
|
+
if (!childStr.includes("\n")) {
|
|
301
|
+
currentLinePrefix += trimmed + " "
|
|
302
|
+
} else {
|
|
303
|
+
// If there's a newline, extract the last line as new prefix
|
|
304
|
+
const lines = childStr.split("\n")
|
|
305
|
+
currentLinePrefix = lines[lines.length - 1].trim() + " "
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let text = parts.join(" ").trim().replace(/ +\n/g, "\n")
|
|
311
|
+
|
|
312
|
+
if (this.info.shape === "reporter" && hexColorPat.test(text)) {
|
|
313
|
+
return `(\\${text})`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const lang = this.info.language
|
|
317
|
+
if (checkAlias && lang && this.info.selector) {
|
|
318
|
+
const aliases = lang.nativeAliases[this.info.id]
|
|
319
|
+
if (aliases && aliases.length) {
|
|
320
|
+
let alias = aliases[0]
|
|
321
|
+
// TODO make translate() not in-place, and use that
|
|
322
|
+
if (inputPat.test(alias) && firstInput) {
|
|
323
|
+
alias = alias.replace(inputPat, firstInput.stringify())
|
|
324
|
+
}
|
|
325
|
+
return alias
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let overrides = extras || ""
|
|
330
|
+
if (
|
|
331
|
+
this.info.categoryIsDefault === false ||
|
|
332
|
+
(this.info.category === "custom-arg" &&
|
|
333
|
+
(this.isReporter || this.isBoolean)) ||
|
|
334
|
+
(this.info.category === "custom" && this.info.shape === "stack")
|
|
335
|
+
) {
|
|
336
|
+
if (overrides) {
|
|
337
|
+
overrides += " "
|
|
338
|
+
}
|
|
339
|
+
if (this.info.isReset && this.info.category === "obsolete") {
|
|
340
|
+
overrides += "reset"
|
|
341
|
+
} else {
|
|
342
|
+
overrides += this.info.category
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (this.info.shapeIsDefault === false) {
|
|
346
|
+
if (overrides) {
|
|
347
|
+
overrides += " "
|
|
348
|
+
}
|
|
349
|
+
overrides += this.info.shape
|
|
350
|
+
}
|
|
351
|
+
if (overrides) {
|
|
352
|
+
text += ` :: ${overrides}`
|
|
353
|
+
}
|
|
354
|
+
return this.hasScript
|
|
355
|
+
? text +
|
|
356
|
+
"\n" +
|
|
357
|
+
(Object.keys(lang.aliases).find(
|
|
358
|
+
key => lang.aliases[key] === "scratchblocks:end",
|
|
359
|
+
) || "end")
|
|
360
|
+
: this.info.shape === "reporter"
|
|
361
|
+
? `(${text})`
|
|
362
|
+
: this.info.shape === "boolean"
|
|
363
|
+
? `<${text}>`
|
|
364
|
+
: text
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
translate(lang, isShallow) {
|
|
368
|
+
if (!lang) {
|
|
369
|
+
throw new Error("Missing language")
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const id = this.info.id
|
|
373
|
+
if (!id) {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (id === "PROCEDURES_DEFINITION") {
|
|
378
|
+
// Find the first 'outline' child (there should be exactly one).
|
|
379
|
+
const outline = this.children.find(child => child.isOutline)
|
|
380
|
+
|
|
381
|
+
this.children = []
|
|
382
|
+
for (const word of lang.definePrefix) {
|
|
383
|
+
this.children.push(new Label(word))
|
|
384
|
+
}
|
|
385
|
+
this.children.push(outline)
|
|
386
|
+
for (const word of lang.defineSuffix) {
|
|
387
|
+
this.children.push(new Label(word))
|
|
388
|
+
}
|
|
389
|
+
return
|
|
390
|
+
} else if (id === "PROCEDURES_CALL") {
|
|
391
|
+
this.children.forEach(child => {
|
|
392
|
+
if (!child.isLabel && !child.isIcon) {
|
|
393
|
+
child.translate(lang)
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const oldSpec = this.info.language.commands[id]
|
|
400
|
+
|
|
401
|
+
const nativeSpec = lang.commands[id]
|
|
402
|
+
if (!nativeSpec) {
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
const nativeInfo = parseSpec(nativeSpec)
|
|
406
|
+
|
|
407
|
+
const rawArgs = this.children.filter(
|
|
408
|
+
child => !child.isLabel && !child.isIcon,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if (!isShallow) {
|
|
412
|
+
rawArgs.forEach(child => child.translate(lang))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Work out indexes of existing children
|
|
416
|
+
const oldParts = parseSpec(oldSpec).parts
|
|
417
|
+
const oldInputOrder = oldParts
|
|
418
|
+
.map(part => parseInputNumber(part))
|
|
419
|
+
.filter(x => x)
|
|
420
|
+
|
|
421
|
+
let highestNumber = 0
|
|
422
|
+
const args = oldInputOrder.map(number => {
|
|
423
|
+
highestNumber = Math.max(highestNumber, number)
|
|
424
|
+
return rawArgs[number - 1]
|
|
425
|
+
})
|
|
426
|
+
const remainingArgs = rawArgs.slice(highestNumber)
|
|
427
|
+
|
|
428
|
+
// Get new children by index
|
|
429
|
+
this.children = nativeInfo.parts
|
|
430
|
+
.map(part => {
|
|
431
|
+
part = part.trim()
|
|
432
|
+
if (!part) {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
const number = parseInputNumber(part)
|
|
436
|
+
if (number) {
|
|
437
|
+
return args[number - 1]
|
|
438
|
+
}
|
|
439
|
+
return iconPat.test(part) ? new Icon(part.slice(1)) : new Label(part)
|
|
440
|
+
})
|
|
441
|
+
.filter(x => x)
|
|
442
|
+
|
|
443
|
+
// Push any remaining children, so we pick up C block bodies
|
|
444
|
+
remainingArgs.forEach((arg, index) => {
|
|
445
|
+
if (index === 1 && this.info.id === "CONTROL_IF") {
|
|
446
|
+
this.children.push(new Label(lang.commands.CONTROL_ELSE))
|
|
447
|
+
}
|
|
448
|
+
this.children.push(arg)
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
this.info.language = lang
|
|
452
|
+
this.info.isRTL = rtlLanguages.includes(lang.code)
|
|
453
|
+
this.info.categoryIsDefault = true
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export class Comment {
|
|
458
|
+
constructor(value, hasBlock) {
|
|
459
|
+
this.label = new Label(value, "comment-label")
|
|
460
|
+
this.width = null
|
|
461
|
+
this.hasBlock = hasBlock
|
|
462
|
+
}
|
|
463
|
+
get isComment() {
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
stringify() {
|
|
468
|
+
return `// ${this.label.value.trim()}`
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export class Glow {
|
|
473
|
+
constructor(child) {
|
|
474
|
+
assert(child)
|
|
475
|
+
this.child = child
|
|
476
|
+
if (child.isBlock) {
|
|
477
|
+
this.shape = child.info.shape
|
|
478
|
+
this.info = child.info
|
|
479
|
+
} else {
|
|
480
|
+
this.shape = "stack"
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
get isGlow() {
|
|
484
|
+
return true
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
stringify() {
|
|
488
|
+
if (this.child.isBlock) {
|
|
489
|
+
return this.child.stringify("+")
|
|
490
|
+
}
|
|
491
|
+
const lines = this.child.stringify().split("\n")
|
|
492
|
+
return lines.map(line => `+ ${line}`).join("\n")
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
translate(lang) {
|
|
496
|
+
this.child.translate(lang)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export class Script {
|
|
501
|
+
constructor(blocks) {
|
|
502
|
+
this.blocks = blocks
|
|
503
|
+
this.isEmpty = !blocks.length
|
|
504
|
+
this.isFinal = !this.isEmpty && blocks[blocks.length - 1].isFinal
|
|
505
|
+
// Script index for block path generation
|
|
506
|
+
this.scriptIndex = null
|
|
507
|
+
}
|
|
508
|
+
get isScript() {
|
|
509
|
+
return true
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
stringify() {
|
|
513
|
+
return this.blocks
|
|
514
|
+
.map(block => {
|
|
515
|
+
let line = block.stringify()
|
|
516
|
+
if (block.comment) {
|
|
517
|
+
// If this block contains a script (multi-line), insert the
|
|
518
|
+
// comment on the first line (the opening line) instead of
|
|
519
|
+
// appending it after the whole multi-line block (which would
|
|
520
|
+
// place it after the trailing "end").
|
|
521
|
+
if (block.isBlock && block.hasScript) {
|
|
522
|
+
const commentText = ` ${block.comment.stringify()}`
|
|
523
|
+
const nl = line.indexOf("\n")
|
|
524
|
+
if (nl !== -1) {
|
|
525
|
+
line = line.slice(0, nl) + commentText + line.slice(nl)
|
|
526
|
+
} else {
|
|
527
|
+
line += commentText
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
line += ` ${block.comment.stringify()}`
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return line
|
|
534
|
+
})
|
|
535
|
+
.join("\n")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
translate(lang) {
|
|
539
|
+
this.blocks.forEach(block => block.translate && block.translate(lang))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export class Document {
|
|
544
|
+
constructor(scripts) {
|
|
545
|
+
this.scripts = scripts
|
|
546
|
+
// Map of blockPath -> Block for quick lookup
|
|
547
|
+
this.blockMap = new Map()
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
stringify() {
|
|
551
|
+
return this.scripts.map(script => script.stringify()).join("\n\n")
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
translate(lang) {
|
|
555
|
+
this.scripts.forEach(script => script.translate(lang))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get a block by its path (e.g., "1.2.1")
|
|
560
|
+
* @param {string} path - The block path
|
|
561
|
+
* @returns {Block|null}
|
|
562
|
+
*/
|
|
563
|
+
getBlockByPath(path) {
|
|
564
|
+
return this.blockMap.get(path) || null
|
|
565
|
+
}
|
|
566
|
+
}
|