Initial commit of 001code-html Scratch frontend project.

Includes scratch-gui, scratch-vm, scratch-blocks, scratch-render, scratch-l10n, and deployment config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 15:37:45 +08:00
commit 6e0a1fbcbb
11350 changed files with 965674 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
const {test} = require('tap');
const compress = require('../../src/serialization/tw-compress-sb3');
const uid = require('../../src/util/uid');
test('handles type INPUT_DIFF_BLOCK_SHADOW (3) compressed inputs', t => {
const data = {
targets: [
{
isStage: true,
name: 'Stage',
variables: {},
lists: {},
broadcasts: {},
blocks: {
'CmRa^i]o}QL77;hk:54o': {
opcode: 'looks_switchbackdropto',
next: null,
parent: null,
inputs: {
BACKDROP: [
3,
'cq84G6uywD{m2R,E03Ci',
'E3/*4H*xk38{=*U;bVWm'
]
},
fields: {},
shadow: false,
topLevel: true,
x: 409,
y: 300
},
'cq84G6uywD{m2R,E03Ci': {
opcode: 'operator_not',
next: null,
parent: 'CmRa^i]o}QL77;hk:54o',
inputs: {},
fields: {},
shadow: false,
topLevel: false
},
'E3/*4H*xk38{=*U;bVWm': {
opcode: 'looks_backdrops',
next: null,
parent: 'CmRa^i]o}QL77;hk:54o',
inputs: {},
fields: {
BACKDROP: [
'backdrop1',
null
]
},
shadow: true,
topLevel: false
}
},
comments: {},
currentCostume: 0,
costumes: [],
sounds: [],
volume: 100,
layerOrder: 0,
tempo: 60,
videoTransparency: 50,
videoState: 'on',
textToSpeechLanguage: null
}
],
monitors: [],
extensions: [],
meta: {
semver: '3.0.0',
vm: '0.2.0',
agent: ''
}
};
compress(data);
const blocks = Object.entries(data.targets[0].blocks);
t.equal(blocks.length, 3);
const [parentId, parentBlock] = blocks.find(i => i[1].opcode === 'looks_switchbackdropto');
const [inputId, inputBlock] = blocks.find(i => i[1].opcode === 'operator_not');
const [shadowId, shadowBlock] = blocks.find(i => i[1].opcode === 'looks_backdrops');
t.equal(parentBlock.inputs.BACKDROP.length, 3);
t.equal(parentBlock.inputs.BACKDROP[0], 3);
t.equal(parentBlock.inputs.BACKDROP[1], inputId);
t.equal(parentBlock.inputs.BACKDROP[2], shadowId);
t.equal(inputBlock.parent, parentId);
t.equal(shadowBlock.parent, parentId);
t.end();
});
test('Compressed IDs will not collide with uncompressed IDs', t => {
const soup = 'abcdefghjijklmnopqstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789';
const items = [
[soup, '', ''],
['', soup, ''],
['', '', soup]
];
for (const [variableSoup, listSoup, broadcastSoup] of items) {
const data = {
targets: [
{
isStage: true,
name: 'Stage',
variables: Object.fromEntries(
variableSoup.split('').map(id => [id, [id, 0]])
),
lists: Object.fromEntries(
listSoup.split('').map(id => [id, [id, []]])
),
broadcasts: Object.fromEntries(
broadcastSoup.split('').map(id => [id, id])
),
blocks: {
'CmRa^i]o}QL77;hk:54o': {
opcode: 'looks_switchbackdropto',
next: null,
parent: null,
inputs: {
BACKDROP: [
3,
'cq84G6uywD{m2R,E03Ci',
'E3/*4H*xk38{=*U;bVWm'
]
},
fields: {},
shadow: false,
topLevel: true,
x: 409,
y: 300
},
'cq84G6uywD{m2R,E03Ci': {
opcode: 'operator_not',
next: null,
parent: 'CmRa^i]o}QL77;hk:54o',
inputs: {},
fields: {},
shadow: false,
topLevel: false
},
'E3/*4H*xk38{=*U;bVWm': {
opcode: 'looks_backdrops',
next: null,
parent: 'CmRa^i]o}QL77;hk:54o',
inputs: {},
fields: {
BACKDROP: [
'backdrop1',
null
]
},
shadow: true,
topLevel: false
}
},
comments: {
'ds{.EoY%0^6vO1WH0/9d': {
blockId: null,
x: 400,
y: 401,
width: 402,
height: 403,
minimized: false,
text: '4'
},
'blh[bsi@XtCkGh!-J5aa': {
blockId: null,
x: 500,
y: 501,
width: 502,
height: 503,
minimized: false,
text: '5'
},
'7#YgytOiJHs(Ne6,2i9(': {
blockId: null,
x: 600,
y: 601,
width: 602,
height: 603,
minimized: false,
text: '6'
}
},
currentCostume: 0,
costumes: [],
sounds: [],
volume: 100,
layerOrder: 0,
tempo: 60,
videoTransparency: 50,
videoState: 'on',
textToSpeechLanguage: null
}
],
monitors: [],
extensions: [],
meta: {
semver: '3.0.0',
vm: '0.2.0',
agent: ''
}
};
compress(data);
const uncompressedIDs = [
...Object.keys(data.targets[0].variables),
...Object.keys(data.targets[0].lists),
...Object.keys(data.targets[0].broadcasts)
];
const compressedIDs = [
...Object.keys(data.targets[0].blocks),
...Object.keys(data.targets[0].comments)
];
for (const compressedID of compressedIDs) {
t.notOk(uncompressedIDs.includes(compressedID), `${compressedID} does not collide`);
}
}
t.end();
});
test('Script execution order is preserved', t => {
const originalBlocks = {};
const blockIds = [];
for (let i = 0; i < 1000; i++) {
if (i === 339) {
blockIds.push('muffin');
} else if (i === 555) {
blockIds.push('555');
}
blockIds.push(uid());
}
blockIds.push('apple');
blockIds.push('-1');
blockIds.push('45');
for (const blockId of blockIds) {
originalBlocks[blockId] = {
opcode: 'event_whenbroadcastreceived',
next: null,
parent: null,
inputs: {},
fields: {
BROADCAST_OPTION: [
`broadcast-name-${blockId}`,
`broadcast-id-${blockId}`
]
},
shadow: false,
topLevel: true,
x: -10,
y: 420
};
}
const data = {
targets: [
{
isStage: true,
name: 'Stage',
variables: {},
lists: {},
broadcasts: {},
blocks: originalBlocks,
comments: {},
currentCostume: 0,
costumes: [],
sounds: [],
volume: 100,
layerOrder: 0,
tempo: 60,
videoTransparency: 50,
videoState: 'on',
textToSpeechLanguage: null
}
],
monitors: [],
extensions: [],
meta: {
semver: '3.0.0',
vm: '0.2.0',
agent: ''
}
};
compress(data);
// Sanity check: Make sure the new object is actually different
const newBlocks = data.targets[0].blocks;
t.not(originalBlocks, newBlocks);
t.notSame(Object.keys(originalBlocks), Object.keys(newBlocks));
// Check that the order has not changed
const newBlockValues = Object.values(newBlocks);
t.same(Object.values(originalBlocks), newBlockValues);
t.equal(newBlockValues[0].fields.BROADCAST_OPTION[0], 'broadcast-name-45');
t.equal(newBlockValues[1].fields.BROADCAST_OPTION[0], 'broadcast-name-555');
t.equal(newBlockValues[339 + 2].fields.BROADCAST_OPTION[0], 'broadcast-name-muffin');
t.equal(newBlockValues[newBlockValues.length - 2].fields.BROADCAST_OPTION[0], 'broadcast-name-apple');
t.equal(newBlockValues[newBlockValues.length - 1].fields.BROADCAST_OPTION[0], 'broadcast-name--1');
// Check that the new IDs do not look like array indexes as their enumeration
// order could cause unexpected behavior in other places.
for (const newBlockId of Object.keys(newBlocks)) {
// The actual definition of an array index is: https://tc39.es/ecma262/#array-index
// This approximation is currently good enough
if (!Number.isNaN(+newBlockId)) {
t.fail(`${newBlockId} might be treated as an array index`);
}
}
t.end();
});