scratch-blocks 2.1.13 → 2.1.15

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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scratch-blocks",
3
- "version": "2.1.13",
3
+ "version": "2.1.15",
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,17 +41,17 @@
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.11",
45
45
  "husky": "9.1.7",
46
46
  "playwright": "1.59.1",
47
- "prettier": "3.8.2",
47
+ "prettier": "3.8.3",
48
48
  "scratch-semantic-release-config": "4.0.1",
49
49
  "semantic-release": "25.0.3",
50
50
  "source-map-loader": "5.0.0",
51
51
  "ts-loader": "9.5.7",
52
52
  "typescript": "5.9.3",
53
53
  "vitest": "4.1.4",
54
- "webpack": "5.106.1",
54
+ "webpack": "5.106.2",
55
55
  "webpack-cli": "6.0.1",
56
56
  "webpack-dev-server": "5.2.3"
57
57
  },
@@ -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
@@ -81,9 +81,10 @@ Reflect.set(
81
81
  // Restore a real block from a statement input to nextConnection so that
82
82
  // unplug(true) can heal the stack correctly. Conditions:
83
83
  // - marker is mid-stack (has a predecessor)
84
- // - marker.nextConnection is empty (displaced block is NOT already there)
84
+ // - marker.nextConnection is either empty or absent (cap blocks do not
85
+ // have a bottom connector, so the displaced block is NOT already there)
85
86
  // - a non-marker block is sitting in a statement input
86
- if (markerPreviousConnection?.isConnected() && markerNextConnection && !markerNextConnection.isConnected()) {
87
+ if (markerPreviousConnection?.isConnected() && !markerNextConnection?.isConnected()) {
87
88
  for (const input of marker.inputList) {
88
89
  const conn = input.connection
89
90
  const connType = conn?.type as Blockly.ConnectionType | undefined
@@ -97,7 +98,19 @@ Reflect.set(
97
98
  continue
98
99
  }
99
100
  prev.disconnect()
100
- markerNextConnection.connect(prev)
101
+ if (markerNextConnection) {
102
+ markerNextConnection.connect(prev)
103
+ } else {
104
+ // Blocks without a bottom connector (e.g. forever) have no nextConnection. Reconnect
105
+ // the displaced block directly to the connection the marker's
106
+ // previousConnection is plugged into, then detach the marker
107
+ // so unplug() has nothing left to heal.
108
+ const aboveConn = markerPreviousConnection.targetConnection
109
+ if (aboveConn) {
110
+ markerPreviousConnection.disconnect()
111
+ aboveConn.connect(prev)
112
+ }
113
+ }
101
114
  break
102
115
  }
103
116
  }
@@ -44,6 +44,32 @@ class ScratchConnectionChecker extends Blockly.ConnectionChecker {
44
44
  return false
45
45
  }
46
46
 
47
+ // Blockly's base doDragChecks rejects inserting a block with no
48
+ // nextConnection into the middle of a stack (NEXT_STATEMENT case) because
49
+ // it assumes the displaced blocks have nowhere to go. Our
50
+ // getConnectionForOrphanedConnection patch routes displaced blocks into
51
+ // statement inputs, so we allow the connection when a suitable statement
52
+ // input exists on the dragging block.
53
+ if (
54
+ (b.type as Blockly.ConnectionType) === Blockly.ConnectionType.NEXT_STATEMENT &&
55
+ b.isConnected() &&
56
+ !(a.getSourceBlock() as Blockly.Block).nextConnection
57
+ ) {
58
+ const orphan = b.targetBlock()
59
+ const orphanPrev = (orphan as Blockly.Block | null)?.previousConnection
60
+ if (orphan && !orphan.isShadow() && orphanPrev) {
61
+ const canWrap = !!Blockly.Connection.getConnectionForOrphanedConnection(a.getSourceBlock(), orphanPrev)
62
+ if (canWrap) {
63
+ // Skip the base class NEXT_STATEMENT check (which would reject
64
+ // this) but still apply the other generic guards it uses.
65
+ if ('distanceFrom' in a && a.distanceFrom(b) > distance) return false
66
+ if (b.getSourceBlock().isInsertionMarker()) return false
67
+ if (Blockly.common.draggingConnections.includes(b)) return false
68
+ return true
69
+ }
70
+ }
71
+ }
72
+
47
73
  return super.doDragChecks(a, b, distance)
48
74
  }
49
75
  }