@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,959 @@
1
+ import {
2
+ Label,
3
+ Icon,
4
+ Input,
5
+ Newline,
6
+ Block,
7
+ Comment,
8
+ Glow,
9
+ Script,
10
+ Document,
11
+ extensions,
12
+ aliasExtensions,
13
+ } from "../syntax/index.js"
14
+
15
+ import SVG from "./draw.js"
16
+ import style from "./style.js"
17
+ const {
18
+ defaultFont,
19
+ commentFont,
20
+ makeStyle,
21
+ makeOriginalIcons,
22
+ makeHighContrastIcons,
23
+ iconName,
24
+ } = style
25
+
26
+ export class LabelView {
27
+ constructor(label) {
28
+ Object.assign(this, label)
29
+
30
+ this.el = null
31
+ this.height = 12
32
+ this.metrics = null
33
+ this.x = 0
34
+ }
35
+
36
+ get isLabel() {
37
+ return true
38
+ }
39
+
40
+ draw(_iconStyle) {
41
+ return this.el
42
+ }
43
+
44
+ get width() {
45
+ return this.metrics.width
46
+ }
47
+
48
+ measure() {
49
+ const value = this.value
50
+ const cls = `sb3-${this.cls}`
51
+ this.el = SVG.text(0, 13, value, {
52
+ class: `sb3-label ${cls}`,
53
+ })
54
+
55
+ let cache = LabelView.metricsCache[cls]
56
+ if (!cache) {
57
+ cache = LabelView.metricsCache[cls] = Object.create(null)
58
+ }
59
+
60
+ if (Object.hasOwnProperty.call(cache, value)) {
61
+ this.metrics = cache[value]
62
+ } else {
63
+ const font = /comment-label/.test(this.cls) ? commentFont : defaultFont
64
+ this.metrics = cache[value] = LabelView.measure(value, font)
65
+ // TODO: word-spacing? (fortunately it seems to have no effect!)
66
+ }
67
+ }
68
+
69
+ static measure(value, font) {
70
+ const context = LabelView.measuring
71
+ context.font = font
72
+ const textMetrics = context.measureText(value)
73
+ const width = (textMetrics.width + 0.5) | 0
74
+ return { width: width }
75
+ }
76
+ }
77
+
78
+ LabelView.metricsCache = {}
79
+ LabelView.toMeasure = []
80
+
81
+ export class IconView {
82
+ constructor(icon) {
83
+ Object.assign(this, icon)
84
+
85
+ const info = IconView.icons[this.name]
86
+ if (!info) {
87
+ throw new Error(`no info for icon: ${this.name}`)
88
+ }
89
+ Object.assign(this, info)
90
+ }
91
+
92
+ get isIcon() {
93
+ return true
94
+ }
95
+
96
+ draw(iconStyle) {
97
+ const symbol = SVG.symbol(`#sb3-${iconName(this.name, iconStyle)}`, {
98
+ width: this.width,
99
+ height: this.height,
100
+ })
101
+ if (this.name === "addInput" || this.name === "delInput") {
102
+ const scaled = SVG.group([symbol])
103
+ SVG.setProps(scaled, {
104
+ transform: "scale(1.6)",
105
+ })
106
+ return SVG.group([scaled])
107
+ }
108
+ return symbol
109
+ }
110
+
111
+ static get icons() {
112
+ return {
113
+ greenFlag: { width: 20, height: 21, dy: -2 },
114
+ stopSign: { width: 20, height: 20 },
115
+ turnLeft: { width: 24, height: 24 },
116
+ turnRight: { width: 24, height: 24 },
117
+ loopArrow: { width: 24, height: 24 },
118
+ addInput: { width: 16, height: 21 },
119
+ delInput: { width: 16, height: 21 },
120
+ list: { width: 15, height: 18 },
121
+ musicBlock: { width: 40, height: 40 },
122
+ penBlock: { width: 40, height: 40 },
123
+ videoBlock: { width: 40, height: 40, dy: 10 },
124
+ ttsBlock: { width: 40, height: 40 },
125
+ translateBlock: { width: 40, height: 40 },
126
+ wedoBlock: { width: 40, height: 40 },
127
+ ev3Block: { width: 40, height: 40 },
128
+ microbitBlock: { width: 40, height: 40 },
129
+ makeymakeyBlock: { width: 40, height: 40 },
130
+ gdxforBlock: { width: 40, height: 40 },
131
+ boostBlock: { width: 40, height: 40 },
132
+ }
133
+ }
134
+ }
135
+
136
+ export class LineView {
137
+ constructor() {
138
+ this.width = 1
139
+ this.height = 40
140
+ this.x = 0
141
+ }
142
+
143
+ get isLine() {
144
+ return true
145
+ }
146
+
147
+ measure() {}
148
+
149
+ draw(_iconStyle, parent) {
150
+ const category = parent.info.category
151
+ return SVG.el("line", {
152
+ class: `sb3-${category}`,
153
+ "stroke-linecap": "round",
154
+ x1: 0,
155
+ y1: 0,
156
+ x2: 0,
157
+ y2: 40,
158
+ })
159
+ }
160
+ }
161
+
162
+ export class InputView {
163
+ constructor(input) {
164
+ Object.assign(this, input)
165
+ if (input.label) {
166
+ this.label = newView(input.label)
167
+ }
168
+ this.isBoolean = this.shape === "boolean"
169
+ this.isObject = this.shape === "object"
170
+ this.isArray = this.shape === "array"
171
+ this.isDropdown = this.shape === "dropdown"
172
+ this.isRound = !(this.isBoolean || this.isDropdown)
173
+
174
+ this.x = 0
175
+ }
176
+
177
+ get isInput() {
178
+ return true
179
+ }
180
+
181
+ measure() {
182
+ if (this.hasLabel) {
183
+ this.label.measure()
184
+ }
185
+ }
186
+
187
+ static get shapes() {
188
+ return {
189
+ string: SVG.pillRect,
190
+ number: SVG.pillRect,
191
+ "number-dropdown": SVG.pillRect,
192
+ color: SVG.pillRect,
193
+ dropdown: SVG.roundRect,
194
+
195
+ boolean: SVG.pointedRect,
196
+ object: SVG.objectRect,
197
+ array: SVG.roundRect,
198
+ stack: SVG.stackRect,
199
+ reporter: SVG.pillRect,
200
+ }
201
+ }
202
+
203
+ draw(iconStyle, parent) {
204
+ let w
205
+ let label
206
+ if (this.isBoolean) {
207
+ w = 48
208
+ } else if (this.isColor) {
209
+ w = 40
210
+ } else if (this.hasLabel) {
211
+ label = this.label.draw(iconStyle)
212
+ // Minimum padding of 11
213
+ // Minimum width of 40, at which point we center the label
214
+ const px = this.label.width >= 18 ? 11 : (40 - this.label.width) / 2
215
+ w = this.label.width + 2 * px
216
+ label = SVG.move(px, 9, label)
217
+ } else {
218
+ w = this.isObject || this.isArray ? 48 : this.isInset ? 30 : null
219
+ }
220
+ if (this.hasArrow) {
221
+ w += 20
222
+ }
223
+ this.width = w
224
+
225
+ const h = (this.height = 32)
226
+
227
+ const el = InputView.shapes[this.shape](w, h)
228
+ SVG.setProps(el, {
229
+ class: `${
230
+ this.isColor ? "" : `sb3-${parent.info.category}`
231
+ } sb3-input sb3-input-${this.shape}`,
232
+ })
233
+
234
+ if (this.isColor) {
235
+ SVG.setProps(el, {
236
+ fill: this.value,
237
+ })
238
+ } else if (this.shape === "dropdown") {
239
+ // custom colors
240
+ if (parent.info.color) {
241
+ SVG.setProps(el, {
242
+ fill: parent.info.color,
243
+ stroke: "rgba(0, 0, 0, 0.2)",
244
+ })
245
+ }
246
+ } else if (this.shape === "number-dropdown") {
247
+ el.classList.add(`sb3-${parent.info.category}-alt`)
248
+
249
+ // custom colors
250
+ if (parent.info.color) {
251
+ SVG.setProps(el, {
252
+ fill: "rgba(0, 0, 0, 0.1)",
253
+ stroke: "rgba(0, 0, 0, 0.15)", // combines with fill...
254
+ })
255
+ }
256
+ } else if (
257
+ this.shape === "boolean" ||
258
+ this.shape === "object" ||
259
+ this.shape === "array"
260
+ ) {
261
+ el.classList.remove(`sb3-${parent.info.category}`)
262
+ el.classList.add(`sb3-${parent.info.category}-dark`)
263
+
264
+ // custom colors
265
+ if (parent.info.color) {
266
+ SVG.setProps(el, {
267
+ fill: "rgba(0, 0, 0, 0.15)",
268
+ })
269
+ }
270
+ }
271
+
272
+ const result = SVG.group([el])
273
+ if (this.hasLabel) {
274
+ result.appendChild(label)
275
+ }
276
+ if (this.hasArrow) {
277
+ result.appendChild(
278
+ SVG.move(
279
+ w - 24,
280
+ 13,
281
+ SVG.symbol(
282
+ iconStyle === "high-contrast"
283
+ ? "#sb3-dropdownArrow-high-contrast"
284
+ : "#sb3-dropdownArrow",
285
+ {},
286
+ ),
287
+ ),
288
+ )
289
+ }
290
+ return result
291
+ }
292
+ }
293
+
294
+ export class NewlineView {
295
+ get isNewline() {
296
+ return true
297
+ }
298
+
299
+ measure() {}
300
+
301
+ draw() {
302
+ this.width = 0
303
+ this.height = 0
304
+ return SVG.group([])
305
+ }
306
+ }
307
+
308
+ class BlockView {
309
+ constructor(block) {
310
+ Object.assign(this, block)
311
+ this.children = block.children.map(newView)
312
+ this.comment = this.comment ? newView(this.comment) : null
313
+ this.isRound = this.info.shape === "reporter"
314
+
315
+ // Avoid accidental mutation
316
+ this.info = { ...block.info }
317
+ if (
318
+ Object.prototype.hasOwnProperty.call(aliasExtensions, this.info.category)
319
+ ) {
320
+ this.info.category = aliasExtensions[this.info.category]
321
+ }
322
+ if (Object.prototype.hasOwnProperty.call(extensions, this.info.category)) {
323
+ this.children.unshift(new LineView())
324
+ this.children.unshift(
325
+ new IconView({ name: this.info.category + "Block" }),
326
+ )
327
+ this.info.category = "extension"
328
+ }
329
+
330
+ this.x = 0
331
+ this.width = null
332
+ this.height = null
333
+ this.firstLine = null
334
+ this.innerWidth = null
335
+ }
336
+
337
+ get isBlock() {
338
+ return true
339
+ }
340
+
341
+ measure() {
342
+ for (const child of this.children) {
343
+ if (child.measure) {
344
+ child.measure()
345
+ }
346
+ }
347
+ if (this.comment) {
348
+ this.comment.measure()
349
+ }
350
+ }
351
+
352
+ static get shapes() {
353
+ return {
354
+ stack: SVG.stackRect,
355
+ "c-block": SVG.stackRect,
356
+ "if-block": SVG.stackRect,
357
+ celse: SVG.stackRect,
358
+ cend: SVG.stackRect,
359
+
360
+ cap: SVG.capRect,
361
+ reporter: SVG.pillRect,
362
+ object: SVG.objectRect,
363
+ array: SVG.roundRect,
364
+ boolean: SVG.pointedRect,
365
+ hat: SVG.hatRect,
366
+ cat: SVG.catHat,
367
+ "define-hat": SVG.procHatRect,
368
+ ring: SVG.pillRect,
369
+ }
370
+ }
371
+
372
+ drawSelf(iconStyle, w, h, lines) {
373
+ // mouths
374
+ if (lines.some(line => line.isScript)) {
375
+ return SVG.mouthRect(w, h, this.isFinal, lines, {
376
+ class: `sb3-${this.info.category}`,
377
+ })
378
+ }
379
+
380
+ // outlines
381
+ if (this.info.shape === "outline") {
382
+ return SVG.setProps(SVG.stackRect(w, h), {
383
+ class: `sb3-${this.info.category} sb3-${this.info.category}-alt`,
384
+ })
385
+ }
386
+
387
+ // rings
388
+ if (this.isRing) {
389
+ const child = this.children[0]
390
+ if (child && (child.isInput || child.isBlock || child.isScript)) {
391
+ return SVG.roundRect(w, h, {
392
+ class: `sb3-${this.info.category}`,
393
+ })
394
+ }
395
+ }
396
+
397
+ const func = BlockView.shapes[this.info.shape]
398
+ if (!func) {
399
+ throw new Error(`no shape func: ${this.info.shape}`)
400
+ }
401
+ return func(w, h, {
402
+ class: `sb3-${this.info.category}`,
403
+ })
404
+ }
405
+
406
+ static get padding() {
407
+ return {
408
+ hat: [24, 8],
409
+ cat: [24, 8],
410
+ "define-hat": [20, 16],
411
+ null: [4, 4],
412
+ }
413
+ }
414
+
415
+ horizontalPadding(child) {
416
+ if (this.isObject) {
417
+ return child.isLabel || child.isIcon ? 16 : 8
418
+ }
419
+ if (this.isRound) {
420
+ if (child.isIcon) {
421
+ return 16
422
+ } else if (child.isLabel) {
423
+ return 12 // text in circle: 3 units
424
+ } else if (child.isDropdown) {
425
+ return 12 // square in circle: 3 units
426
+ } else if (child.isBoolean) {
427
+ return 12 // hexagon in circle: 3 units
428
+ } else if (child.isRound) {
429
+ return 4 // circle in circle: 1 unit
430
+ }
431
+ } else if (this.isBoolean) {
432
+ if (child.isIcon) {
433
+ return 24 // icon in hexagon: ???
434
+ } else if (child.isLabel) {
435
+ return 20 // text in hexagon: 5 units
436
+ } else if (child.isDropdown) {
437
+ return 20 // square in hexagon: 5 units
438
+ } else if (child.isRound && child.isBlock) {
439
+ return 24 // circle in hexagon: 5 + 1 units
440
+ } else if (child.isRound) {
441
+ return 20 // circle in hexagon: 5 units
442
+ } else if (child.isBoolean) {
443
+ return 8 // hexagon in hexagon: 2 units
444
+ }
445
+ }
446
+ return 8 // default: 2 units
447
+ }
448
+
449
+ marginBetween(a, b) {
450
+ // Consecutive labels should be rendered as a single text element.
451
+ // For now, approximate the size of one space
452
+ if (a.isLabel && b.isLabel) {
453
+ return 5
454
+ }
455
+
456
+ return 8 // default: 2 units
457
+ }
458
+
459
+ draw(iconStyle) {
460
+ const isDefine = this.info.shape === "define-hat"
461
+ let children = this.children
462
+ const isCommand = this.isCommand
463
+
464
+ const padding = BlockView.padding[this.info.shape] || BlockView.padding.null
465
+ const visualOutputPadding =
466
+ !isCommand && children.some(child => child.isNewline) ? 4 : 0
467
+ const pt = padding[0] + visualOutputPadding,
468
+ pb = padding[1] + visualOutputPadding
469
+
470
+ let y = this.info.shape === "cat" ? 16 : 0
471
+ const Line = function (y) {
472
+ this.y = y
473
+ this.width = 0
474
+ this.height = isCommand ? 40 : 32
475
+ this.children = []
476
+ }
477
+
478
+ let innerWidth = 0
479
+ let scriptWidth = 0
480
+ let line = new Line(y)
481
+ const pushLine = isLast => {
482
+ if (lines.length === 0) {
483
+ line.height += pt + pb
484
+ } else if (line.isVisualRow && isLast) {
485
+ line.height += pb + 4
486
+ } else if (!line.isVisualRow) {
487
+ line.height -= 11
488
+ line.y -= 2
489
+ }
490
+ y += line.height
491
+ lines.push(line)
492
+ }
493
+
494
+ if (this.info.isRTL) {
495
+ let start = 0
496
+ const flip = () => {
497
+ children = children
498
+ .slice(0, start)
499
+ .concat(children.slice(start, i).reverse())
500
+ .concat(children.slice(i))
501
+ }
502
+ let i
503
+ for (i = 0; i < children.length; i++) {
504
+ if (children[i].isScript) {
505
+ flip()
506
+ start = i + 1
507
+ }
508
+ }
509
+ if (start < i) {
510
+ flip()
511
+ }
512
+ }
513
+
514
+ const lines = []
515
+ let previousChild
516
+ let lastChild
517
+ for (let i = 0; i < children.length; i++) {
518
+ const child = children[i]
519
+ child.el = child.draw(iconStyle, this)
520
+
521
+ if (child.isNewline) {
522
+ pushLine()
523
+ line = new Line(y)
524
+ line.isVisualRow = true
525
+ previousChild = null
526
+ } else if (child.isScript && this.hasScript) {
527
+ this.hasScript = true
528
+ pushLine()
529
+ child.y = y - 1
530
+ lines.push(child)
531
+ scriptWidth = Math.max(scriptWidth, Math.max(1, child.width))
532
+ child.height = Math.max(29, child.height + 3) - 2
533
+ y += child.height
534
+ line = new Line(y)
535
+ previousChild = null
536
+ } else if (child.isArrow) {
537
+ line.children.push(child)
538
+ previousChild = child
539
+ } else {
540
+ // Remember the last child on the first line
541
+ if (!lines.length) {
542
+ lastChild = child
543
+ }
544
+
545
+ // Leave space between inputs
546
+ if (previousChild) {
547
+ line.width += this.marginBetween(previousChild, child)
548
+ }
549
+
550
+ // Align first input with right of notch
551
+ if (children[0] != null) {
552
+ const cmw = 48 - this.horizontalPadding(children[0])
553
+ if (
554
+ (this.isCommand || this.isOutline) &&
555
+ !child.isLabel &&
556
+ !child.isIcon &&
557
+ line.width < cmw
558
+ ) {
559
+ line.width = cmw
560
+ }
561
+ }
562
+
563
+ // Align extension category icons below notch
564
+ if (child.isIcon && i === 0 && this.isCommand) {
565
+ line.height = Math.max(line.height, child.height + 8)
566
+ }
567
+
568
+ child.x = line.width
569
+ line.width += child.width
570
+ innerWidth = Math.max(innerWidth, line.width)
571
+ if (!child.isLabel) {
572
+ line.height = Math.max(line.height, child.height)
573
+ }
574
+ line.children.push(child)
575
+ previousChild = child
576
+ }
577
+ }
578
+ pushLine(true)
579
+
580
+ let padLeft = children.length ? this.horizontalPadding(children[0]) : 0
581
+ const padRight = children.length ? this.horizontalPadding(lastChild) : 0
582
+ innerWidth += padLeft + padRight
583
+
584
+ // Commands have a minimum width.
585
+ // Outline min-width is deliberately higher (because Scratch 3 looks silly).
586
+ const originalInnerWidth = innerWidth
587
+ innerWidth = Math.max(
588
+ this.hasScript
589
+ ? 160
590
+ : this.isHat
591
+ ? 100 // Correct for Scratch 3.0.
592
+ : this.isCommand || this.isOutline
593
+ ? 64
594
+ : this.isReporter
595
+ ? 48
596
+ : 0,
597
+ innerWidth,
598
+ )
599
+
600
+ // Center the label text inside small reporters.
601
+ if (this.isReporter) {
602
+ padLeft += (innerWidth - originalInnerWidth) / 2
603
+ }
604
+
605
+ this.height = y
606
+
607
+ this.width = scriptWidth
608
+ ? Math.max(innerWidth, 15 + scriptWidth)
609
+ : innerWidth
610
+ this.firstLine = lines[0]
611
+ this.innerWidth = innerWidth
612
+
613
+ const objects = []
614
+
615
+ for (let i = 0; i < lines.length; i++) {
616
+ const line = lines[i]
617
+ if (line.isScript) {
618
+ objects.push(SVG.move(16, line.y, line.el))
619
+ continue
620
+ }
621
+
622
+ const h = line.height
623
+
624
+ for (let j = 0; j < line.children.length; j++) {
625
+ const child = line.children[j]
626
+ if (child.isArrow) {
627
+ objects.push(SVG.move(innerWidth - 32, this.height - 28, child.el))
628
+ continue
629
+ }
630
+
631
+ let y = pt + (h - child.height - pt - pb) / 2
632
+ if (child.isLabel && i === 0) {
633
+ // We only do this for the first line so that the `else` label is
634
+ // correctly aligned
635
+ y -= 1
636
+ } else if (isDefine && child.isLabel) {
637
+ y += 3
638
+ } else if (child.isIcon) {
639
+ y += child.dy | 0
640
+ if (this.isCommand && i === 0 && j === 0) {
641
+ y += 4
642
+ }
643
+ }
644
+
645
+ let x = padLeft + child.x
646
+ if (child.dx) {
647
+ x += child.dx
648
+ }
649
+
650
+ objects.push(SVG.move(x, (line.y + y) | 0, child.el))
651
+ }
652
+ }
653
+
654
+ const el = this.drawSelf(iconStyle, innerWidth, this.height, lines)
655
+ objects.splice(0, 0, el)
656
+ if (this.info.color) {
657
+ SVG.setProps(el, {
658
+ fill: this.info.color,
659
+ stroke: "rgba(0, 0, 0, 0.2)",
660
+ })
661
+ }
662
+
663
+ return SVG.group(objects)
664
+ }
665
+ }
666
+
667
+ export class CommentView {
668
+ constructor(comment) {
669
+ Object.assign(this, comment)
670
+ this.label = newView(comment.label)
671
+
672
+ this.width = null
673
+ }
674
+
675
+ get isComment() {
676
+ return true
677
+ }
678
+
679
+ static get lineLength() {
680
+ return 12
681
+ }
682
+
683
+ get height() {
684
+ return 20
685
+ }
686
+
687
+ measure() {
688
+ this.label.measure()
689
+ }
690
+
691
+ draw(iconStyle) {
692
+ const labelEl = this.label.draw(iconStyle)
693
+
694
+ this.width = this.label.width + 16
695
+ return SVG.group([
696
+ SVG.commentLine(this.hasBlock ? CommentView.lineLength : 0, 6),
697
+ SVG.commentRect(this.width, this.height, {
698
+ class: "sb3-comment",
699
+ }),
700
+ SVG.move(8, 4, labelEl),
701
+ ])
702
+ }
703
+ }
704
+
705
+ class GlowView {
706
+ constructor(glow) {
707
+ Object.assign(this, glow)
708
+ this.child = newView(glow.child)
709
+
710
+ this.width = null
711
+ this.height = null
712
+ this.y = 0
713
+ }
714
+
715
+ get isGlow() {
716
+ return true
717
+ }
718
+
719
+ measure() {
720
+ this.child.measure()
721
+ }
722
+
723
+ drawSelf(iconStyle) {
724
+ const c = this.child
725
+ let el
726
+ const w = this.width
727
+ const h = this.height - 1
728
+ if (c.isScript) {
729
+ if (!c.isEmpty && c.blocks[0].isHat) {
730
+ el = SVG.hatRect(w, h)
731
+ } else if (c.isFinal) {
732
+ el = SVG.capRect(w, h)
733
+ } else {
734
+ el = SVG.stackRect(w, h)
735
+ }
736
+ } else {
737
+ el = c.drawSelf(iconStyle, w, h, [])
738
+ }
739
+ return SVG.setProps(el, {
740
+ class: "sb3-diff sb3-diff-ins",
741
+ })
742
+ }
743
+ // TODO how can we always raise Glows above their parents?
744
+
745
+ draw(iconStyle) {
746
+ const c = this.child
747
+ const el = c.isScript ? c.draw(iconStyle, true) : c.draw(iconStyle)
748
+
749
+ this.width = c.width
750
+ this.height = (c.isBlock && c.firstLine.height) || c.height
751
+
752
+ // encircle
753
+ return SVG.group([el, this.drawSelf(iconStyle)])
754
+ }
755
+ }
756
+
757
+ class ScriptView {
758
+ constructor(script) {
759
+ Object.assign(this, script)
760
+ this.blocks = script.blocks.map(newView)
761
+
762
+ this.y = 0
763
+ }
764
+
765
+ get isScript() {
766
+ return true
767
+ }
768
+
769
+ measure() {
770
+ for (const block of this.blocks) {
771
+ block.measure()
772
+ }
773
+ }
774
+
775
+ draw(iconStyle, inside) {
776
+ const children = []
777
+ let y = 1
778
+ this.width = 0
779
+ for (const block of this.blocks) {
780
+ const x = inside ? 0 : 2
781
+ const child = block.draw(iconStyle)
782
+ children.push(SVG.move(x, y, child))
783
+ this.width = Math.max(this.width, block.width)
784
+
785
+ const diff = block.diff
786
+ if (diff === "-") {
787
+ const dw = block.width
788
+ const dh = block.firstLine.height || block.height
789
+ children.push(SVG.move(x, y + dh / 2 + 1, SVG.strikethroughLine(dw)))
790
+ this.width = Math.max(this.width, block.width)
791
+ }
792
+
793
+ y += block.height
794
+
795
+ const comment = block.comment
796
+ if (comment) {
797
+ const line = block.firstLine
798
+ const cx = block.innerWidth + 2 + CommentView.lineLength
799
+ const cy = y - block.height + line.height / 2
800
+ const el = comment.draw(iconStyle)
801
+ children.push(SVG.move(cx, cy - comment.height / 2, el))
802
+ this.width = Math.max(this.width, cx + comment.width)
803
+ }
804
+ }
805
+ const lastBlock = this.blocks[this.blocks.length - 1]
806
+ this.height = y + 1
807
+ if (!inside && !this.isFinal) {
808
+ this.height += lastBlock.hasPuzzle ? 8 : 0
809
+ }
810
+ if (!inside && lastBlock.isGlow) {
811
+ this.height += 7 // TODO unbreak this
812
+ }
813
+ return SVG.group(children)
814
+ }
815
+ }
816
+
817
+ class DocumentView {
818
+ constructor(doc, options) {
819
+ Object.assign(this, doc)
820
+ this.scripts = doc.scripts.map(newView)
821
+
822
+ this.width = null
823
+ this.height = null
824
+ this.el = null
825
+ this.defs = null
826
+ this.scale = options.scale
827
+ this.iconStyle = options.style.replace("scratch3-", "")
828
+ }
829
+
830
+ measure() {
831
+ this.scripts.forEach(script => {
832
+ script.measure()
833
+ })
834
+ }
835
+
836
+ render(cb) {
837
+ if (typeof cb === "function") {
838
+ throw new Error("render() no longer takes a callback")
839
+ }
840
+
841
+ // measure strings
842
+ this.measure()
843
+
844
+ // TODO: separate layout + render steps.
845
+ // render each script
846
+ let width = 0
847
+ let height = 0
848
+ const elements = []
849
+ for (let i = 0; i < this.scripts.length; i++) {
850
+ const script = this.scripts[i]
851
+ if (height) {
852
+ height += 10
853
+ }
854
+ script.y = height
855
+ elements.push(SVG.move(0, height, script.draw(this.iconStyle)))
856
+ height += script.height
857
+ if (i !== this.scripts.length - 1) {
858
+ height += 36
859
+ }
860
+ width = Math.max(width, script.width + 4)
861
+ }
862
+ this.width = width
863
+ this.height = height
864
+
865
+ // return SVG
866
+ const svg = SVG.newSVG(width, height, this.scale)
867
+ const icons =
868
+ this.iconStyle === "high-contrast"
869
+ ? makeHighContrastIcons()
870
+ : makeOriginalIcons()
871
+ svg.appendChild((this.defs = SVG.withChildren(SVG.el("defs"), icons)))
872
+
873
+ svg.appendChild(
874
+ SVG.setProps(SVG.group(elements), {
875
+ style: `transform: scale(${this.scale})`,
876
+ }),
877
+ )
878
+ this.el = svg
879
+ return svg
880
+ }
881
+
882
+ /* Export SVG image as XML string */
883
+ exportSVGString() {
884
+ if (this.el == null) {
885
+ throw new Error("call draw() first")
886
+ }
887
+
888
+ const style = makeStyle()
889
+ this.defs.appendChild(style)
890
+ const xml = new SVG.XMLSerializer().serializeToString(this.el)
891
+ this.defs.removeChild(style)
892
+ return xml
893
+ }
894
+
895
+ /* Export SVG image as data URI */
896
+ exportSVG() {
897
+ const xml = this.exportSVGString()
898
+ return `data:image/svg+xml;utf8,${xml.replace(/[#]/g, encodeURIComponent)}`
899
+ }
900
+
901
+ toCanvas(cb, exportScale) {
902
+ exportScale = exportScale || 1.0
903
+
904
+ const canvas = SVG.makeCanvas()
905
+ canvas.width = Math.max(1, this.width * exportScale * this.scale)
906
+ canvas.height = Math.max(1, this.height * exportScale * this.scale)
907
+ const context = canvas.getContext("2d")
908
+
909
+ const image = new Image()
910
+ image.src = this.exportSVG()
911
+ image.onload = () => {
912
+ context.save()
913
+ context.scale(exportScale, exportScale)
914
+ context.drawImage(image, 0, 0)
915
+ context.restore()
916
+
917
+ cb(canvas)
918
+ }
919
+ }
920
+
921
+ exportPNG(cb, scale) {
922
+ this.toCanvas(canvas => {
923
+ if (URL && URL.createObjectURL && Blob && canvas.toBlob) {
924
+ canvas.toBlob(blob => {
925
+ cb(URL.createObjectURL(blob))
926
+ }, "image/png")
927
+ } else {
928
+ cb(canvas.toDataURL("image/png"))
929
+ }
930
+ }, scale)
931
+ }
932
+ }
933
+
934
+ const viewFor = node => {
935
+ switch (node.constructor) {
936
+ case Label:
937
+ return LabelView
938
+ case Icon:
939
+ return IconView
940
+ case Input:
941
+ return InputView
942
+ case Newline:
943
+ return NewlineView
944
+ case Block:
945
+ return BlockView
946
+ case Comment:
947
+ return CommentView
948
+ case Glow:
949
+ return GlowView
950
+ case Script:
951
+ return ScriptView
952
+ case Document:
953
+ return DocumentView
954
+ default:
955
+ throw new Error(`no view for ${node.constructor.name}`)
956
+ }
957
+ }
958
+
959
+ export const newView = (node, options) => new (viewFor(node))(node, options)