scratch-blocks 2.1.12 → 2.1.14

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.
@@ -30,6 +30,7 @@ type ConnectionMap = Record<string, {
30
30
  /**
31
31
  * Possible types for procedure arguments.
32
32
  */
33
+ type ArgumentDefault = string | number | boolean | null;
33
34
  declare enum ArgumentType {
34
35
  STRING = "s",
35
36
  NUMBER = "n",
@@ -50,7 +51,7 @@ interface ProcedureBlock extends Blockly.BlockSvg {
50
51
  }
51
52
  export interface ProcedureDeclarationBlock extends ProcedureBlock {
52
53
  displayNames_: string[];
53
- argumentDefaults_: string[];
54
+ argumentDefaults_: ArgumentDefault[];
54
55
  removeFieldCallback: (field: Blockly.Field) => void;
55
56
  createArgumentEditor_: (argumentType: ArgumentType, displayName: string) => Blockly.BlockSvg;
56
57
  focusLastEditor_: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"procedures.d.ts","sourceRoot":"","sources":["../../../../src/blocks/procedures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH;;GAEG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAIvC;;GAEG;AACH,KAAK,aAAa,GAAG,MAAM,CACzB,MAAM,EACN;IACE,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;CAC/B,GAAG,IAAI,CACT,CAAA;AAED;;GAEG;AACH,aAAK,YAAY;IACf,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,OAAO,MAAM;CACd;AA8vCD,UAAU,cAAe,SAAQ,OAAO,CAAC,QAAQ;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,oBAAoB,EAAE,MAAM,aAAa,CAAA;IACzC,sBAAsB,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,CAAA;IAC9D,gBAAgB,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,CAAA;IACxD,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,iBAAiB,EAAE,CACjB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,aAAa,EAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,KAAK,KACjB,IAAI,CAAA;IACT,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC3C;AAED,MAAM,WAAW,yBAA0B,SAAQ,cAAc;IAC/D,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,KAAK,IAAI,CAAA;IACnD,qBAAqB,EAAE,CAAC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAA;IAC5F,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,uBAAuB,EAAE,MAAM,IAAI,CAAA;IACnC,UAAU,EAAE,MAAM,IAAI,CAAA;CACvB"}
1
+ {"version":3,"file":"procedures.d.ts","sourceRoot":"","sources":["../../../../src/blocks/procedures.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH;;GAEG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAIvC;;GAEG;AACH,KAAK,aAAa,GAAG,MAAM,CACzB,MAAM,EACN;IACE,MAAM,EAAE,OAAO,GAAG,SAAS,CAAA;IAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAA;CAC/B,GAAG,IAAI,CACT,CAAA;AAED;;GAEG;AACH,KAAK,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AAEvD,aAAK,YAAY;IACf,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,OAAO,MAAM;CACd;AAgwCD,UAAU,cAAe,SAAQ,OAAO,CAAC,QAAQ;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,KAAK,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,MAAM,MAAM,CAAA;IACzB,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,oBAAoB,EAAE,MAAM,aAAa,CAAA;IACzC,sBAAsB,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,CAAA;IAC9D,gBAAgB,EAAE,CAAC,aAAa,EAAE,aAAa,KAAK,IAAI,CAAA;IACxD,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,iBAAiB,EAAE,CACjB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,aAAa,EAC5B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,KAAK,KACjB,IAAI,CAAA;IACT,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC3C;AAED,MAAM,WAAW,yBAA0B,SAAQ,cAAc;IAC/D,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,iBAAiB,EAAE,eAAe,EAAE,CAAA;IACpC,mBAAmB,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,KAAK,IAAI,CAAA;IACnD,qBAAqB,EAAE,CAAC,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAA;IAC5F,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IAChC,gBAAgB,EAAE,MAAM,IAAI,CAAA;IAC5B,kBAAkB,EAAE,MAAM,IAAI,CAAA;IAC9B,uBAAuB,EAAE,MAAM,IAAI,CAAA;IACnC,UAAU,EAAE,MAAM,IAAI,CAAA;CACvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"scratch_field_dropdown.d.ts","sourceRoot":"","sources":["../../../../src/fields/scratch_field_dropdown.ts"],"names":[],"mappings":"AAqCA;;GAEG;AACH,wBAAgB,4BAA4B,SAG3C"}
1
+ {"version":3,"file":"scratch_field_dropdown.d.ts","sourceRoot":"","sources":["../../../../src/fields/scratch_field_dropdown.ts"],"names":[],"mappings":"AAiFA;;GAEG;AACH,wBAAgB,4BAA4B,SAG3C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scratch-blocks",
3
- "version": "2.1.12",
3
+ "version": "2.1.14",
4
4
  "description": "Scratch Blocks is a library for building creative computing interfaces.",
5
5
  "author": "Massachusetts Institute of Technology",
6
6
  "license": "Apache-2.0",
@@ -41,7 +41,7 @@
41
41
  "@vitest/browser": "4.1.4",
42
42
  "@vitest/browser-playwright": "4.1.4",
43
43
  "eslint": "9.39.4",
44
- "eslint-config-scratch": "14.1.7",
44
+ "eslint-config-scratch": "14.1.8",
45
45
  "husky": "9.1.7",
46
46
  "playwright": "1.59.1",
47
47
  "prettier": "3.8.2",
@@ -37,6 +37,8 @@ type ConnectionMap = Record<
37
37
  /**
38
38
  * Possible types for procedure arguments.
39
39
  */
40
+ type ArgumentDefault = string | number | boolean | null
41
+
40
42
  enum ArgumentType {
41
43
  STRING = 's',
42
44
  NUMBER = 'n',
@@ -75,39 +77,17 @@ function getRequiredMutationAttribute(xmlElement: Element, name: string): string
75
77
  return value
76
78
  }
77
79
 
78
- /**
79
- * Parse a required mutation attribute as JSON, then validate its type.
80
- * @param xmlElement The mutation element that should contain required attributes.
81
- * @param name The specific mutation attribute to retrieve and parse.
82
- * @param parse Validates and narrows the parsed JSON value.
83
- * @returns Parsed and validated mutation attribute value.
84
- */
85
- function parseRequiredMutationJson<T>(
86
- xmlElement: Element,
87
- name: string,
88
- parse: (value: unknown, name: string) => T,
89
- ): T {
90
- const rawValue = getRequiredMutationAttribute(xmlElement, name)
91
- let parsedValue: unknown
92
- try {
93
- parsedValue = JSON.parse(rawValue)
94
- } catch {
95
- throw new Error(`Invalid JSON in mutation attribute: ${name}`)
96
- }
97
- return parse(parsedValue, name)
98
- }
99
-
100
80
  /**
101
81
  * Parse an optional mutation attribute as JSON, returning a fallback when the
102
- * attribute is absent. Use this only for attributes that can be safely
103
- * defaulted in isolation, without invalidating structural invariants that
104
- * relate them to other attributes on the same mutation. A present-but-malformed
105
- * attribute still throws, since that indicates corruption rather than an older
106
- * schema.
82
+ * attribute is absent or malformed. Older saved projects may omit attributes
83
+ * that current serialization always writes, and some projects in the wild
84
+ * contain attributes whose values are not valid for the expected type.
85
+ * Returning the fallback in both cases lets the project load rather than
86
+ * aborting the entire workspace parse.
107
87
  * @param xmlElement The mutation element that may contain the attribute.
108
88
  * @param name The specific mutation attribute to retrieve and parse.
109
89
  * @param parse Validates and narrows the parsed JSON value.
110
- * @param fallback Value to return when the attribute is absent.
90
+ * @param fallback Value to return when the attribute is absent or malformed.
111
91
  * @returns Parsed and validated mutation attribute value, or `fallback`.
112
92
  */
113
93
  function parseOptionalMutationJson<T>(
@@ -120,13 +100,12 @@ function parseOptionalMutationJson<T>(
120
100
  if (rawValue === null) {
121
101
  return fallback
122
102
  }
123
- let parsedValue: unknown
124
103
  try {
125
- parsedValue = JSON.parse(rawValue)
104
+ return parse(JSON.parse(rawValue), name)
126
105
  } catch {
127
- throw new Error(`Invalid JSON in mutation attribute: ${name}`)
106
+ console.warn(`Invalid or unexpected mutation attribute "${name}", using default. Raw value: ${rawValue}`)
107
+ return fallback
128
108
  }
129
- return parse(parsedValue, name)
130
109
  }
131
110
 
132
111
  /**
@@ -155,6 +134,26 @@ function parseStringArrayMutationValue(value: unknown, name: string): string[] {
155
134
  return value
156
135
  }
157
136
 
137
+ /**
138
+ * Validate a parsed mutation value as an array of primitives. Argument
139
+ * defaults from older projects (especially Scratch 2.0 conversions) contain
140
+ * mixed types — strings for text arguments, numbers for numeric arguments,
141
+ * and booleans for boolean arguments (e.g. `["",1,1,1,1]`). The old fork
142
+ * stored these via untyped `JSON.parse` with no validation, and the VM
143
+ * passes them through as-is.
144
+ * @param value Parsed mutation value.
145
+ * @param name Attribute name used in error messages.
146
+ * @returns Validated array value.
147
+ */
148
+ function parsePrimitiveArrayMutationValue(value: unknown, name: string): ArgumentDefault[] {
149
+ const isArgumentDefault = (entry: unknown): entry is ArgumentDefault =>
150
+ entry === null || typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean'
151
+ if (!Array.isArray(value) || !value.every(isArgumentDefault)) {
152
+ throw new Error(`Expected ArgumentDefault[] JSON value in mutation attribute: ${name}`)
153
+ }
154
+ return value
155
+ }
156
+
158
157
  /**
159
158
  * A drag strategy for the procedures_prototype block that delegates all drag
160
159
  * operations to its parent (the procedures_definition block). This lets the
@@ -308,7 +307,7 @@ function callerDomToMutation(this: ProcedureCallBlock, xmlElement: Element) {
308
307
  this.procCode_ = getRequiredMutationAttribute(xmlElement, 'proccode')
309
308
  const generateshadows = xmlElement.getAttribute('generateshadows')
310
309
  this.generateShadows_ = generateshadows !== null ? JSON.parse(generateshadows) === true : false
311
- this.argumentIds_ = parseRequiredMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue)
310
+ this.argumentIds_ = parseOptionalMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue, [])
312
311
  this.warp_ = parseOptionalMutationJson(xmlElement, 'warp', parseBooleanMutationValue, false)
313
312
  this.updateDisplay_()
314
313
  }
@@ -349,9 +348,14 @@ function definitionDomToMutation(this: ProcedurePrototypeBlock | ProcedureDeclar
349
348
  const prevArgIds = this.argumentIds_
350
349
  const prevDisplayNames = this.displayNames_
351
350
 
352
- this.argumentIds_ = parseRequiredMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue)
353
- this.displayNames_ = parseRequiredMutationJson(xmlElement, 'argumentnames', parseStringArrayMutationValue)
354
- this.argumentDefaults_ = parseRequiredMutationJson(xmlElement, 'argumentdefaults', parseStringArrayMutationValue)
351
+ this.argumentIds_ = parseOptionalMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue, [])
352
+ this.displayNames_ = parseOptionalMutationJson(xmlElement, 'argumentnames', parseStringArrayMutationValue, [])
353
+ this.argumentDefaults_ = parseOptionalMutationJson(
354
+ xmlElement,
355
+ 'argumentdefaults',
356
+ parsePrimitiveArrayMutationValue,
357
+ [],
358
+ )
355
359
 
356
360
  // During full XML deserialization (Blockly.Xml.domToWorkspace), the mutation element
357
361
  // is part of the parsed XML tree and its parent element also contains <value> children
@@ -1341,7 +1345,7 @@ interface ProcedureBlock extends Blockly.BlockSvg {
1341
1345
 
1342
1346
  export interface ProcedureDeclarationBlock extends ProcedureBlock {
1343
1347
  displayNames_: string[]
1344
- argumentDefaults_: string[]
1348
+ argumentDefaults_: ArgumentDefault[]
1345
1349
  removeFieldCallback: (field: Blockly.Field) => void
1346
1350
  createArgumentEditor_: (argumentType: ArgumentType, displayName: string) => Blockly.BlockSvg
1347
1351
  focusLastEditor_: () => void
@@ -1361,7 +1365,7 @@ interface ProcedureCallBlock extends ProcedureBlock {
1361
1365
 
1362
1366
  interface ProcedurePrototypeBlock extends ProcedureBlock {
1363
1367
  displayNames_: string[]
1364
- argumentDefaults_: string[]
1368
+ argumentDefaults_: ArgumentDefault[]
1365
1369
  skipArgumentReporters_: boolean
1366
1370
  createArgumentReporter_: (argumentType: ArgumentType, displayName: string) => Blockly.BlockSvg
1367
1371
  updateArgumentReporterNames_: (prevArgIds: string[], prevDisplayNames: string[]) => void
@@ -7,6 +7,50 @@ import * as Blockly from 'blockly/core'
7
7
  class ScratchFieldDropdown extends Blockly.FieldDropdown {
8
8
  private originalStyle!: string
9
9
 
10
+ /**
11
+ * Accept string values even when they are not in the current options list.
12
+ * Scratch populates some dropdowns from dynamic data (e.g. the sprite list
13
+ * for motion_goto_menu, motion_pointtowards_menu, sensing_touchingobject_menu)
14
+ * and deliberately excludes the currently-editing sprite. A block moved into
15
+ * the sprite it now targets carries a stored value that is therefore absent
16
+ * from the option list for this editing context, but is still valid — the
17
+ * runtime reads the stored value directly. Preserve that value so the UI
18
+ * shows what the block actually does, rather than silently reverting to the
19
+ * first option.
20
+ * @param newValue The value to validate.
21
+ * @returns The value unchanged if it is a string; otherwise null to reject
22
+ * the update.
23
+ */
24
+ protected override doClassValidation_(newValue?: string): string | null {
25
+ if (typeof newValue !== 'string') {
26
+ return null
27
+ }
28
+ return newValue
29
+ }
30
+
31
+ /**
32
+ * When the stored value is not in the current options list, display the raw
33
+ * value. Blockly's base implementation reads from the last-matched option,
34
+ * which stays stale when the incoming value does not appear in the list, so
35
+ * the default would otherwise render as the first option's text.
36
+ * @returns Text to display for the currently-selected value.
37
+ */
38
+ protected override getText_(): string | null {
39
+ const value = this.getValue()
40
+ if (value === null) {
41
+ return super.getText_()
42
+ }
43
+ for (const option of this.getOptions(true)) {
44
+ if (option === 'separator') continue
45
+ if (option[1] === value) {
46
+ // Delegate to the base for the matched case so image/HTMLElement
47
+ // option types render correctly.
48
+ return super.getText_()
49
+ }
50
+ }
51
+ return value
52
+ }
53
+
10
54
  showEditor_(event: PointerEvent) {
11
55
  super.showEditor_(event)
12
56
  const sourceBlock = this.getSourceBlock() as Blockly.BlockSvg