Skip to content

Commit efe10f5

Browse files
authored
Merge branch 'TurboWarp:develop' into develop
2 parents 2fcaf7c + af3b751 commit efe10f5

23 files changed

+716
-65
lines changed

.github/workflows/deploy.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ jobs:
1313
build:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
17+
with:
18+
persist-credentials: false
1719
- name: Install Node.js
18-
uses: actions/setup-node@v4
20+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
1921
with:
2022
node-version: 20
2123
cache: npm
@@ -29,7 +31,7 @@ jobs:
2931
# It will still generate what it can, so it's safe to ignore the error
3032
continue-on-error: true
3133
- name: Upload artifact
32-
uses: actions/upload-pages-artifact@v3
34+
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
3335
with:
3436
path: ./playground/
3537

@@ -45,4 +47,4 @@ jobs:
4547
steps:
4648
- name: Deploy to GitHub Pages
4749
id: deployment
48-
uses: actions/deploy-pages@v4
50+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e

.github/workflows/node.js.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ jobs:
88
build:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v4
11+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
12+
with:
13+
persist-credentials: false
1214
- name: Install Node.js
13-
uses: actions/setup-node@v4
15+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
1416
with:
1517
node-version: 20
1618
cache: npm

src/compiler/irgen.js

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,12 @@ const LIST_TYPE = 'list';
1919
*/
2020

2121
/**
22-
* Create a variable codegen object.
23-
* @param {'target'|'stage'} scope The scope of this variable -- which object owns it.
24-
* @param {import('../engine/variable.js')} varObj The Scratch Variable
25-
* @returns {*} A variable codegen object.
22+
* @typedef DescendedVariable
23+
* @property {'target'|'stage'} scope
24+
* @property {string} id
25+
* @property {string} name
26+
* @property {boolean} isCloud
2627
*/
27-
const createVariableData = (scope, varObj) => ({
28-
scope,
29-
id: varObj.id,
30-
name: varObj.name,
31-
isCloud: varObj.isCloud
32-
});
3328

3429
/**
3530
* @param {string} code
@@ -1420,35 +1415,49 @@ class ScriptTreeGenerator {
14201415
const variable = block.fields[fieldName];
14211416
const id = variable.id;
14221417

1423-
if (Object.prototype.hasOwnProperty.call(this.variableCache, id)) {
1418+
if (id && Object.prototype.hasOwnProperty.call(this.variableCache, id)) {
14241419
return this.variableCache[id];
14251420
}
14261421

14271422
const data = this._descendVariable(id, variable.value, type);
1428-
this.variableCache[id] = data;
1423+
// If variable ID was null, this might do some unnecessary updates, but that is a rare
1424+
// edge case and it won't have any adverse effects anyways.
1425+
this.variableCache[data.id] = data;
14291426
return data;
14301427
}
14311428

14321429
/**
1433-
* @param {string} id The ID of the variable.
1430+
* @param {string|null} id The ID of the variable.
14341431
* @param {string} name The name of the variable.
14351432
* @param {''|'list'} type The variable type.
14361433
* @private
1437-
* @returns {*} A parsed variable object.
1434+
* @returns {DescendedVariable} A parsed variable object.
14381435
*/
14391436
_descendVariable (id, name, type) {
14401437
const target = this.target;
14411438
const stage = this.stage;
14421439

14431440
// Look for by ID in target...
14441441
if (Object.prototype.hasOwnProperty.call(target.variables, id)) {
1445-
return createVariableData('target', target.variables[id]);
1442+
const currVar = target.variables[id];
1443+
return {
1444+
scope: 'target',
1445+
id: currVar.id,
1446+
name: currVar.name,
1447+
isCloud: currVar.isCloud
1448+
};
14461449
}
14471450

14481451
// Look for by ID in stage...
14491452
if (!target.isStage) {
14501453
if (stage && Object.prototype.hasOwnProperty.call(stage.variables, id)) {
1451-
return createVariableData('stage', stage.variables[id]);
1454+
const currVar = stage.variables[id];
1455+
return {
1456+
scope: 'stage',
1457+
id: currVar.id,
1458+
name: currVar.name,
1459+
isCloud: currVar.isCloud
1460+
};
14521461
}
14531462
}
14541463

@@ -1457,7 +1466,12 @@ class ScriptTreeGenerator {
14571466
if (Object.prototype.hasOwnProperty.call(target.variables, varId)) {
14581467
const currVar = target.variables[varId];
14591468
if (currVar.name === name && currVar.type === type) {
1460-
return createVariableData('target', currVar);
1469+
return {
1470+
scope: 'target',
1471+
id: currVar.id,
1472+
name: currVar.name,
1473+
isCloud: currVar.isCloud
1474+
};
14611475
}
14621476
}
14631477
}
@@ -1468,14 +1482,22 @@ class ScriptTreeGenerator {
14681482
if (Object.prototype.hasOwnProperty.call(stage.variables, varId)) {
14691483
const currVar = stage.variables[varId];
14701484
if (currVar.name === name && currVar.type === type) {
1471-
return createVariableData('stage', currVar);
1485+
return {
1486+
scope: 'stage',
1487+
id: currVar.id,
1488+
name: currVar.name,
1489+
isCloud: currVar.isCloud
1490+
};
14721491
}
14731492
}
14741493
}
14751494
}
14761495

14771496
// Create it locally...
14781497
const newVariable = new Variable(id, name, type, false);
1498+
1499+
// Intentionally not using newVariable.id so that this matches vanilla Scratch quirks regarding
1500+
// handling of null variable IDs.
14791501
target.variables[id] = newVariable;
14801502

14811503
if (target.sprite) {
@@ -1489,7 +1511,14 @@ class ScriptTreeGenerator {
14891511
}
14901512
}
14911513

1492-
return createVariableData('target', newVariable);
1514+
return {
1515+
scope: 'target',
1516+
// If the given ID was null, this won't match the .id property of the Variable object.
1517+
// This is intentional to match vanilla Scratch quirks.
1518+
id,
1519+
name: newVariable.name,
1520+
isCloud: newVariable.isCloud
1521+
};
14931522
}
14941523

14951524
descendProcedure (block) {

src/engine/thread.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ class Thread {
451451
let callCount = 5; // Max number of enclosing procedure calls to examine.
452452
const sp = this.stackFrames.length - 1;
453453
for (let i = sp - 1; i >= 0; i--) {
454-
const block = this.target.blocks.getBlock(this.stackFrames[i].op.id);
454+
const block = this.target.blocks.getBlock(this.stackFrames[i].op.id) ||
455+
this.target.runtime.flyoutBlocks.getBlock(this.stackFrames[i].op.id);
455456
if (block.opcode === 'procedures_call' &&
456457
block.mutation.proccode === procedureCode) {
457458
return true;

src/engine/tw-font-manager.js

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ const AssetUtil = require('../util/tw-asset-util');
33
const StringUtil = require('../util/string-util');
44
const log = require('../util/log');
55

6+
/*
7+
* In general in this file, note that font names in browsers are case-insensitive
8+
* but are whitespace-sensitive.
9+
*/
10+
611
/**
712
* @typedef InternalFont
813
* @property {boolean} system True if the font is built in to the system
@@ -11,40 +16,121 @@ const log = require('../util/log');
1116
* @property {Asset} [asset] scratch-storage asset if system: false
1217
*/
1318

19+
/**
20+
* @param {string} font
21+
* @returns {string}
22+
*/
23+
const removeInvalidCharacters = font => font.replace(/[^-\w ]/g, '');
24+
25+
/**
26+
* @param {InternalFont[]} fonts Modified in-place
27+
* @param {InternalFont} newFont
28+
* @returns {InternalFont|null}
29+
*/
30+
const addOrUpdateFont = (fonts, newFont) => {
31+
let oldFont;
32+
const oldIndex = fonts.findIndex(i => i.family.toLowerCase() === newFont.family.toLowerCase());
33+
if (oldIndex !== -1) {
34+
oldFont = fonts[oldIndex];
35+
fonts.splice(oldIndex, 1);
36+
}
37+
fonts.push(newFont);
38+
return oldFont;
39+
};
40+
1441
class FontManager extends EventEmitter {
1542
/**
1643
* @param {Runtime} runtime
1744
*/
1845
constructor (runtime) {
1946
super();
47+
48+
/** @type {Runtime} */
2049
this.runtime = runtime;
50+
2151
/** @type {Array<InternalFont>} */
2252
this.fonts = [];
53+
54+
/**
55+
* All entries should be lowercase.
56+
* @type {Set<string>}
57+
*/
58+
this.restrictedFonts = new Set();
2359
}
2460

2561
/**
26-
* @param {string} family An unknown font family
27-
* @returns {boolean} true if the family is valid
62+
* Prevents a family from being overridden by a custom font. The project may still use it as a system font.
63+
* @param {string} family
2864
*/
29-
isValidFamily (family) {
65+
restrictFont (family) {
66+
if (!this.isValidSystemFont(family)) {
67+
throw new Error('Invalid font');
68+
}
69+
70+
this.restrictedFonts.add(family.toLowerCase());
71+
72+
const oldLength = this.fonts.length;
73+
this.fonts = this.fonts.filter(font => font.system || this.isValidCustomFont(font.family));
74+
if (this.fonts.length !== oldLength) {
75+
this.updateRenderer();
76+
this.changed();
77+
}
78+
}
79+
80+
/**
81+
* @param {string} family Untrusted font name input
82+
* @returns {boolean} true if the family is valid for a system font
83+
*/
84+
isValidSystemFont (family) {
3085
return /^[-\w ]+$/.test(family);
3186
}
3287

3388
/**
34-
* @param {string} family
35-
* @returns {boolean}
89+
* @param {string} family Untrusted font name input
90+
* @returns {boolean} true if the family is valid for a custom font
3691
*/
37-
hasFont (family) {
38-
return !!this.fonts.find(i => i.family === family);
92+
isValidCustomFont (family) {
93+
return /^[-\w ]+$/.test(family) && !this.restrictedFonts.has(family.toLowerCase());
94+
}
95+
96+
/**
97+
* @deprecated only exists for extension compatibility, use isValidSystemFont or isValidCustomFont instead
98+
*/
99+
isValidFamily (family) {
100+
return this.isValidSystemFont(family) && this.isValidCustomFont(family);
101+
}
102+
103+
/**
104+
* @param {string} family Untrusted font name input
105+
* @returns {string}
106+
*/
107+
getUnusedSystemFont (family) {
108+
return StringUtil.caseInsensitiveUnusedName(
109+
removeInvalidCharacters(family),
110+
this.fonts.map(i => i.family)
111+
);
112+
}
113+
114+
/**
115+
* @param {string} family Untrusted font name input
116+
* @returns {string}
117+
*/
118+
getUnusedCustomFont (family) {
119+
return StringUtil.caseInsensitiveUnusedName(
120+
removeInvalidCharacters(family),
121+
[
122+
...this.fonts.map(i => i.family),
123+
...this.restrictedFonts
124+
]
125+
);
39126
}
40127

41128
/**
42129
* @param {string} family
43130
* @returns {boolean}
44131
*/
45-
getSafeName (family) {
46-
family = family.replace(/[^-\w ]/g, '');
47-
return StringUtil.unusedName(family, this.fonts.map(i => i.family));
132+
hasFont (family) {
133+
return !!this.fonts.find(i => i.family.toLowerCase() === family.toLowerCase());
48134
}
49135

50136
changed () {
@@ -56,14 +142,17 @@ class FontManager extends EventEmitter {
56142
* @param {string} fallback
57143
*/
58144
addSystemFont (family, fallback) {
59-
if (!this.isValidFamily(family)) {
60-
throw new Error('Invalid family');
145+
if (!this.isValidSystemFont(family)) {
146+
throw new Error('Invalid system font family');
61147
}
62-
this.fonts.push({
148+
const oldFont = addOrUpdateFont(this.fonts, {
63149
system: true,
64150
family,
65151
fallback
66152
});
153+
if (oldFont && !oldFont.system) {
154+
this.updateRenderer();
155+
}
67156
this.changed();
68157
}
69158

@@ -73,17 +162,15 @@ class FontManager extends EventEmitter {
73162
* @param {Asset} asset scratch-storage asset
74163
*/
75164
addCustomFont (family, fallback, asset) {
76-
if (!this.isValidFamily(family)) {
77-
throw new Error('Invalid family');
165+
if (!this.isValidCustomFont(family)) {
166+
throw new Error('Invalid custom font family');
78167
}
79-
80-
this.fonts.push({
168+
addOrUpdateFont(this.fonts, {
81169
system: false,
82170
family,
83171
fallback,
84172
asset
85173
});
86-
87174
this.updateRenderer();
88175
this.changed();
89176
}

0 commit comments

Comments
 (0)