Spaces:
Build error
Build error
File size: 5,638 Bytes
30c32c8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
// We don't generate new IDs using numbers at this time because their enumeration
// order can affect script execution order as they always come first.
// https://tc39.es/ecma262/#sec-ordinaryownpropertykeys
const SOUP = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#%()*+,-./:;=?@[]^_`{|}~';
const generateId = i => {
let str = '';
while (i >= 0) {
str = SOUP[i % SOUP.length] + str;
i = Math.floor(i / SOUP.length) - 1;
}
return str;
};
class Pool {
constructor () {
this.generatedIds = new Map();
this.references = new Map();
this.skippedIds = new Set();
// IDs in Object.keys(vm.runtime.monitorBlocks._blocks) already have meaning, so make sure to skip those
// We don't bother listing many here because most would take more than ten million items to be used
this.skippedIds.add('of');
}
skip (id) {
this.skippedIds.add(id);
}
addReference (id) {
const currentCount = this.references.get(id) || 0;
this.references.set(id, currentCount + 1);
}
generateNewIds () {
const entries = Array.from(this.references.entries());
// The most used original IDs should get the shortest new IDs.
entries.sort((a, b) => b[1] - a[1]);
let i = 0;
for (const entry of entries) {
const oldId = entry[0];
let newId = generateId(i);
while (this.skippedIds.has(newId)) {
i++;
newId = generateId(i);
}
this.generatedIds.set(oldId, newId);
i++;
}
}
getNewId (originalId) {
if (this.generatedIds.has(originalId)) {
return this.generatedIds.get(originalId);
}
return originalId;
}
}
const compress = projectData => {
// projectData is modified in-place
// The optimization here is not optimal. This is intentional.
// We only compress block and comment IDs because we want to maintain 100% (not 99.99%; 100%) compatibility and be
// truly lossless. Optimizing things like variable IDs will cause things such as the editor's backpack feature
// to misbehave.
// We use the same variable pool for all objects to avoid any possible issues if IDs are ever treated as unique
// within a given project.
const pool = new Pool();
for (const target of projectData.targets) {
// While we don't compress these IDs, we need to make sure that our compressed IDs
// do not intersect, which could happen if the project was compressed with a
// different tool.
for (const variableId of Object.keys(target.variables)) {
pool.skip(variableId);
}
for (const listId of Object.keys(target.lists)) {
pool.skip(listId);
}
for (const broadcastId of Object.keys(target.broadcasts)) {
pool.skip(broadcastId);
}
for (const blockId of Object.keys(target.blocks)) {
const block = target.blocks[blockId];
pool.addReference(blockId);
if (Array.isArray(block)) {
// Compressed native
continue;
}
if (block.parent) {
pool.addReference(block.parent);
}
if (block.next) {
pool.addReference(block.next);
}
if (block.comment) {
pool.addReference(block.comment);
}
for (const input of Object.values(block.inputs)) {
for (let i = 1; i < input.length; i++) {
const inputValue = input[i];
if (typeof inputValue === 'string') {
pool.addReference(inputValue);
}
}
}
}
for (const commentId of Object.keys(target.comments)) {
const comment = target.comments[commentId];
pool.addReference(commentId);
if (comment.blockId) {
pool.addReference(comment.blockId);
}
}
}
pool.generateNewIds();
for (const target of projectData.targets) {
const newBlocks = {};
const newComments = {};
for (const blockId of Object.keys(target.blocks)) {
const block = target.blocks[blockId];
newBlocks[pool.getNewId(blockId)] = block;
if (Array.isArray(block)) {
// Compressed native
continue;
}
if (block.parent) {
block.parent = pool.getNewId(block.parent);
}
if (block.next) {
block.next = pool.getNewId(block.next);
}
if (block.comment) {
block.comment = pool.getNewId(block.comment);
}
for (const input of Object.values(block.inputs)) {
for (let i = 1; i < input.length; i++) {
const inputValue = input[i];
if (typeof inputValue === 'string') {
input[i] = pool.getNewId(inputValue);
}
}
}
}
for (const commentId of Object.keys(target.comments)) {
const comment = target.comments[commentId];
newComments[pool.getNewId(commentId)] = comment;
if (comment.blockId) {
comment.blockId = pool.getNewId(comment.blockId);
}
}
target.blocks = newBlocks;
target.comments = newComments;
}
};
module.exports = compress;
|