diff --git a/src/editor/editor.ts b/src/editor/editor.ts index ae96ba73..76ff2e75 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -49,8 +49,8 @@ export class Editor { private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; private robotPath: string = ''; - private moduleContent: string = ''; - private robotContent: string = ''; + private moduleContentText: string = ''; + private robotContent: commonStorage.ModuleContent | null = null; private bindedOnChange: any = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; @@ -117,31 +117,31 @@ export class Editor { this.modulePath = ''; this.robotPath = ''; } - this.moduleContent = ''; - this.robotContent = ''; + this.moduleContentText = ''; + this.robotContent = null; this.clearBlocklyWorkspace(); if (currentModule) { // Fetch the content for the current module and the robot. // TODO: Also fetch the content for the mechanisms? const promises: { [key: string]: Promise } = {}; // key is module path, value is promise of module content. - promises[this.modulePath] = this.storage.fetchModuleContent(this.modulePath); + promises[this.modulePath] = this.storage.fetchModuleContentText(this.modulePath); if (this.robotPath !== this.modulePath) { // Also fetch the robot module content. It contains components, etc, that can be used in OpModes. - promises[this.robotPath] = this.storage.fetchModuleContent(this.robotPath) + promises[this.robotPath] = this.storage.fetchModuleContentText(this.robotPath) } - const moduleContents: { [key: string]: string } = {}; // key is module path, value is module content + const modulePathToContentText: { [key: string]: string } = {}; // key is module path, value is module content await Promise.all( Object.entries(promises).map(async ([modulePath, promise]) => { - moduleContents[modulePath] = await promise; + modulePathToContentText[modulePath] = await promise; }) ); - this.moduleContent = moduleContents[this.modulePath]; + this.moduleContentText = modulePathToContentText[this.modulePath]; if (this.robotPath === this.modulePath) { - this.robotContent = this.moduleContent; + this.robotContent = commonStorage.parseModuleContentText(this.moduleContentText); } else { - this.robotContent = moduleContents[this.robotPath]; + this.robotContent = commonStorage.parseModuleContentText(modulePathToContentText[this.robotPath]); } this.loadBlocksIntoBlocklyWorkspace(); } @@ -169,10 +169,8 @@ export class Editor { // Add the while-loading listener. this.bindedOnChange = this.onChangeWhileLoading.bind(this); this.blocklyWorkspace.addChangeListener(this.bindedOnChange); - const blocksContent = commonStorage.extractBlocksContent(this.moduleContent); - if (blocksContent) { - Blockly.serialization.workspaces.load(JSON.parse(blocksContent), this.blocklyWorkspace); - } + const moduleContent = commonStorage.parseModuleContentText(this.moduleContentText); + Blockly.serialization.workspaces.load(moduleContent.getBlocks(), this.blocklyWorkspace); } public updateToolbox(shownPythonToolboxCategories: Set): void { @@ -194,33 +192,33 @@ export class Editor { /* // This code is helpful for debugging issues where the editor says // 'Blocks have been modified!'. - if (this.getModuleContent() !== this.moduleContent) { + if (this.getModuleContentText() !== this.moduleContentText) { console.log('isModified will return true'); - console.log('this.getModuleContent() is ' + this.getModuleContent()); - console.log('this.moduleContent is ' + this.moduleContent); + console.log('this.getModuleContentText() is ' + this.getModuleContentText()); + console.log('this.moduleContentText is ' + this.moduleContentText); } */ - return this.getModuleContent() !== this.moduleContent; + return this.getModuleContentText() !== this.moduleContentText; } - private getModuleContent(): string { + private getModuleContentText(): string { if (!this.currentModule) { - throw new Error('getModuleContent: this.currentModule is null.'); + throw new Error('getModuleContentText: this.currentModule is null.'); } - const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode( - this.blocklyWorkspace, this.generatorContext); - const blocksContent = JSON.stringify( - Blockly.serialization.workspaces.save(this.blocklyWorkspace)); - const methodsContent = ( + + // Generate python because some parts of components, events, and methods are affected. + extendedPythonGenerator.mrcWorkspaceToCode(this.blocklyWorkspace, this.generatorContext); + + const blocks = Blockly.serialization.workspaces.save(this.blocklyWorkspace); + const components: commonStorage.Component[] = this.getComponentsFromWorkspace(); + const events: commonStorage.Event[] = this.getEventsFromWorkspace(); + const methods: commonStorage.Method[] = ( this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) - ? JSON.stringify(this.getMethodsForOutsideFromWorkspace()) - : '[]'; - const componentsContent = JSON.stringify(this.getComponentsFromWorkspace()); - const eventsContent = JSON.stringify(this.getEventsFromWorkspace()); - return commonStorage.makeModuleContent( - this.currentModule, pythonCode, blocksContent, - methodsContent, componentsContent, eventsContent); + ? this.getMethodsForOutsideFromWorkspace() + : []; + return commonStorage.makeModuleContentText( + this.currentModule, blocks, components, events, methods); } public getComponentsFromWorkspace(): commonStorage.Component[] { @@ -261,10 +259,10 @@ export class Editor { } public async saveBlocks() { - const moduleContent = this.getModuleContent(); + const moduleContentText = this.getModuleContentText(); try { - await this.storage.saveModule(this.modulePath, moduleContent); - this.moduleContent = moduleContent; + await this.storage.saveModule(this.modulePath, moduleContentText); + this.moduleContentText = moduleContentText; } catch (e) { throw e; } @@ -280,7 +278,7 @@ export class Editor { if (!this.robotContent) { throw new Error('getComponentsFromRobot: this.robotContent is null.'); } - return commonStorage.extractComponents(this.robotContent); + return this.robotContent.getComponents(); } /** @@ -293,7 +291,7 @@ export class Editor { if (!this.robotContent) { throw new Error('getEventsFromRobot: this.robotContent is null.'); } - return commonStorage.extractEvents(this.robotContent); + return this.robotContent.getEvents(); } /** @@ -306,7 +304,7 @@ export class Editor { if (!this.robotContent) { throw new Error('getMethodsFromRobot: this.robotContent is null.'); } - return commonStorage.extractMethods(this.robotContent); + return this.robotContent.getMethods(); } public static getEditorForBlocklyWorkspace(workspace: Blockly.Workspace): Editor | null { diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index ba1acd49..ea35f19f 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -117,7 +117,7 @@ class ClientSideStorage implements commonStorage.Storage { }; }); } - + async listProjects(): Promise { return new Promise((resolve, reject) => { const projects: {[key: string]: commonStorage.Project} = {}; // key is project name, value is Project @@ -220,7 +220,7 @@ class ClientSideStorage implements commonStorage.Storage { }); } - async fetchModuleContent(modulePath: string): Promise { + async fetchModuleContentText(modulePath: string): Promise { return new Promise((resolve, reject) => { const getRequest = this.db.transaction(['modules'], 'readonly') .objectStore('modules').get(modulePath); @@ -243,20 +243,20 @@ class ClientSideStorage implements commonStorage.Storage { async createProject(projectName: string, robotContent: string, opmodeContent : string): Promise { const modulePath = commonStorage.makeRobotPath(projectName); const opmodePath = commonStorage.makeModulePath(projectName, 'Teleop'); - + await this._saveModule(commonStorage.MODULE_TYPE_ROBOT, modulePath, robotContent); await this._saveModule(commonStorage.MODULE_TYPE_OPMODE, opmodePath, opmodeContent); } - async createModule(moduleType: string, modulePath: string, moduleContent: string): Promise { - return this._saveModule(moduleType, modulePath, moduleContent); + async createModule(moduleType: string, modulePath: string, moduleContentText: string): Promise { + return this._saveModule(moduleType, modulePath, moduleContentText); } - async saveModule(modulePath: string, moduleContent: string): Promise { - return this._saveModule('', modulePath, moduleContent); + async saveModule(modulePath: string, moduleContentText: string): Promise { + return this._saveModule('', modulePath, moduleContentText); } - private async _saveModule(moduleType: string, modulePath: string, moduleContent: string) + private async _saveModule(moduleType: string, modulePath: string, moduleContentText: string) : Promise { // When creating a new module, moduleType must be truthy. // When saving an existing module, the moduleType must be falsy. @@ -301,7 +301,7 @@ class ClientSideStorage implements commonStorage.Storage { } value = getRequest.result; } - value.content = moduleContent; + value.content = moduleContentText; value.dateModifiedMillis = Date.now(); const putRequest = modulesObjectStore.put(value); putRequest.onerror = () => { @@ -545,7 +545,7 @@ class ClientSideStorage implements commonStorage.Storage { async downloadProject(projectName: string): Promise { return new Promise((resolve, reject) => { // Collect all the modules in the project. - const moduleContents: {[key: string]: string} = {}; // key is module name, value is module content + const moduleNameToContentText: {[key: string]: string} = {}; // key is module name, value is module content const openCursorRequest = this.db.transaction(['modules'], 'readonly') .objectStore('modules') .openCursor(); @@ -560,13 +560,13 @@ class ClientSideStorage implements commonStorage.Storage { const value = cursor.value; if (commonStorage.getProjectName(value.path) === projectName) { const moduleName = commonStorage.getModuleName(value.path); - moduleContents[moduleName] = value.content; + moduleNameToContentText[moduleName] = value.content; } cursor.continue(); } else { // The cursor is done. We have finished collecting all the modules in the project. // Now create the blob for download. - const blobUrl = await commonStorage.produceDownloadProjectBlob(projectName, moduleContents); + const blobUrl = await commonStorage.produceDownloadProjectBlob(moduleNameToContentText); resolve(blobUrl); } }; @@ -576,17 +576,17 @@ class ClientSideStorage implements commonStorage.Storage { async uploadProject(projectName: string, blobUrl: string): Promise { return new Promise(async (resolve, reject) => { // Process the uploaded blob to get the module types and contents. - let moduleTypes: {[key: string]: string}; // key is module name, value is module content - let moduleContents: {[key: string]: string}; // key is module name, value is module content + let moduleNameToType: {[key: string]: string}; // key is module name, value is module content + let moduleNameToContentText: {[key: string]: string}; // key is module name, value is module content try { - [moduleTypes, moduleContents] = await commonStorage.processUploadedBlob( + [moduleNameToType, moduleNameToContentText] = await commonStorage.processUploadedBlob( projectName, blobUrl); } catch (e) { console.log('commonStorage.processUploadedBlob failed.'); reject(new Error('commonStorage.processUploadedBlob failed.')); return; } - + // Save each module. const transaction = this.db.transaction(['modules'], 'readwrite'); transaction.oncomplete = () => { @@ -597,10 +597,10 @@ class ClientSideStorage implements commonStorage.Storage { reject(new Error('IndexedDB transaction aborted.')); }; const modulesObjectStore = transaction.objectStore('modules'); - - for (const moduleName in moduleTypes) { - const moduleType = moduleTypes[moduleName]; - const moduleContent = moduleContents[moduleName]; + + for (const moduleName in moduleNameToType) { + const moduleType = moduleNameToType[moduleName]; + const moduleContentText = moduleNameToContentText[moduleName]; const modulePath = commonStorage.makeModulePath(projectName, moduleName); const getRequest = modulesObjectStore.get(modulePath); getRequest.onerror = () => { @@ -617,7 +617,7 @@ class ClientSideStorage implements commonStorage.Storage { const value = Object.create(null); value.path = modulePath; value.type = moduleType; - value.content = moduleContent; + value.content = moduleContentText; value.dateModifiedMillis = Date.now(); const putRequest = modulesObjectStore.put(value); putRequest.onerror = () => { diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 28d728d0..cbd5eff0 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -21,15 +21,10 @@ import JSZip from 'jszip'; -import * as Blockly from 'blockly/core'; - import startingOpModeBlocks from '../modules/opmode_start.json'; import startingMechanismBlocks from '../modules/mechanism_start.json'; import startingRobotBlocks from '../modules/robot_start.json'; -import { extendedPythonGenerator } from '../editor/extended_python_generator'; -import { createGeneratorContext } from '../editor/generator_context'; - // Types, constants, and functions related to modules, regardless of where the modules are stored. export type Module = { @@ -85,30 +80,16 @@ export const MODULE_TYPE_OPMODE = 'opmode'; const CLASS_NAME_ROBOT = 'Robot'; -const DELIMITER_PREFIX = 'BlocksContent'; -const MARKER_BLOCKS_CONTENT = 'blocksContent: '; -const MARKER_METHODS = 'methods: '; -const MARKER_MODULE_TYPE = 'moduleType: '; -const MARKER_COMPONENTS = 'components: '; -const MARKER_EVENTS = 'events: '; - -const PARTS_INDEX_BLOCKS_CONTENT = 0; -const PARTS_INDEX_METHODS = 1; -const PARTS_INDEX_MODULE_TYPE = 2; -const PARTS_INDEX_COMPONENTS = 3; -const PARTS_INDEX_EVENTS = 4; -const NUMBER_OF_PARTS = 5; - export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks'; export interface Storage { saveEntry(entryKey: string, entryValue: string): Promise; fetchEntry(entryKey: string, defaultValue: string): Promise; listProjects(): Promise; - fetchModuleContent(modulePath: string): Promise; + fetchModuleContentText(modulePath: string): Promise; createProject(projectName: string, robotContent: string, opmodeContent: string): Promise; - createModule(moduleType: string, modulePath: string, moduleContent: string): Promise; - saveModule(modulePath: string, moduleContent: string): Promise; + createModule(moduleType: string, modulePath: string, moduleContentText: string): Promise; + saveModule(modulePath: string, moduleContentText: string): Promise; renameProject(oldProjectName: string, newProjectName: string): Promise; copyProject(oldProjectName: string, newProjectName: string): Promise; renameModule(moduleType: string, projectName: string, oldModuleName: string, newModuleName: string): Promise; @@ -179,8 +160,8 @@ export async function deleteProject( */ export async function addModuleToProject( storage: Storage, project: Project, moduleType: string, newClassName: string): Promise { - let newModuleName = pascalCaseToSnakeCase(newClassName); - let newModulePath = makeModulePath(project.projectName, newModuleName); + const newModuleName = pascalCaseToSnakeCase(newClassName); + const newModulePath = makeModulePath(project.projectName, newModuleName); if (moduleType === MODULE_TYPE_MECHANISM) { const mechanismContent = newMechanismContent(project.projectName, newModuleName); @@ -466,7 +447,7 @@ export function isValidPythonModuleName(name: string): boolean { * Returns the module path for the given project and module names. */ export function makeModulePath(projectName: string, moduleName: string): string { - return projectName + '/' + moduleName + '.py'; + return projectName + '/' + moduleName + '.blocks'; } /** @@ -480,7 +461,7 @@ export function makeRobotPath(projectName: string): string { * Returns the project path for given module path. */ export function getProjectName(modulePath: string): string { - const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).py$'); + const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).blocks$'); const result = regex.exec(modulePath) if (!result) { throw new Error('Unable to extract the project name.'); @@ -492,7 +473,7 @@ export function getProjectName(modulePath: string): string { * Returns the module name for given module path. */ export function getModuleName(modulePath: string): string { - const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).py$'); + const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).blocks$'); const result = regex.exec(modulePath) if (!result) { throw new Error('Unable to extract the module name.'); @@ -500,30 +481,17 @@ export function getModuleName(modulePath: string): string { return result[2]; } -function startingBlocksToModuleContent( - module: Module, startingBlocks: { [key: string]: any }) { - // Create a headless blockly workspace. - const headlessBlocklyWorkspace = new Blockly.Workspace(); - headlessBlocklyWorkspace.options.oneBasedIndex = false; - Blockly.serialization.workspaces.load(startingBlocks, headlessBlocklyWorkspace); - - const generatorContext = createGeneratorContext(); - generatorContext.setModule(module); - - const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode( - headlessBlocklyWorkspace, generatorContext); - const blocksContent = JSON.stringify( - Blockly.serialization.workspaces.save(headlessBlocklyWorkspace)); - const methodsContent = '[]'; - const componentsContent = '[]'; - const eventsContent = '[]'; - return makeModuleContent( +function startingBlocksToModuleContentText( + module: Module, startingBlocks: { [key: string]: any }): string { + const components: Component[] = []; + const events: Event[] = []; + const methods: Method[] = []; + return makeModuleContentText( module, - pythonCode, - blocksContent, - methodsContent, - componentsContent, - eventsContent); + startingBlocks, + components, + events, + methods); } /** @@ -539,7 +507,7 @@ export function newRobotContent(projectName: string): string { className: CLASS_NAME_ROBOT, }; - return startingBlocksToModuleContent(module, startingRobotBlocks); + return startingBlocksToModuleContentText(module, startingRobotBlocks); } /** @@ -555,7 +523,7 @@ export function newMechanismContent(projectName: string, mechanismName: string): className: snakeCaseToPascalCase(mechanismName), }; - return startingBlocksToModuleContent(module, startingMechanismBlocks); + return startingBlocksToModuleContentText(module, startingMechanismBlocks); } /** @@ -571,169 +539,80 @@ export function newOpModeContent(projectName: string, opModeName: string): strin className: snakeCaseToPascalCase(opModeName), }; - return startingBlocksToModuleContent(module, startingOpModeBlocks); + return startingBlocksToModuleContentText(module, startingOpModeBlocks); } /** * Make the module content from the given python code and blocks content. */ -export function makeModuleContent( +export function makeModuleContentText( module: Module, - pythonCode: string, - blocksContent: string, - methodsContent: string, - componentsContent: string, - eventsContent: string): string { - let delimiter = DELIMITER_PREFIX; - while (module.moduleType.includes(delimiter) - || blocksContent.includes(delimiter) - || methodsContent.includes(delimiter) - || componentsContent.includes(delimiter) - || eventsContent.includes(delimiter)) { - delimiter += '.'; - } - return ( - '# This file was generated by the Blocks editor.\n\n' + - pythonCode + '\n\n\n' + - '"""\n' + - delimiter + '\n' + - MARKER_EVENTS + eventsContent + '\n' + - delimiter + '\n' + - MARKER_COMPONENTS + componentsContent + '\n' + - delimiter + '\n' + - MARKER_MODULE_TYPE + module.moduleType + '\n' + - delimiter + '\n' + - MARKER_METHODS + methodsContent + '\n' + - delimiter + '\n' + - MARKER_BLOCKS_CONTENT + blocksContent + '\n' + - delimiter + '\n' + - '"""\n'); -} - -function getParts(moduleContent: string): string[] { - // The last line is """. - const lastChars = '\n"""\n'; - if (!moduleContent.endsWith(lastChars) || moduleContent.length <= lastChars.length) { - throw new Error('Unable to parse the module content.'); - } - // The line before that is the delimiter. - const iEndOfDelimiter = moduleContent.length - lastChars.length; - const iPreviousNewLine = moduleContent.lastIndexOf('\n', iEndOfDelimiter - 1); - if (iPreviousNewLine === -1) { - throw new Error('Unable to parse the module content.'); - } - const iStartOfDelimiter = iPreviousNewLine + 1; - const delimiter = moduleContent.substring(iStartOfDelimiter, iEndOfDelimiter); - const split = moduleContent.split(delimiter); - split.pop(); // The last element is the """ that closes the python comment. - const parts = []; - // Pop the elements off of the split array and push them onto the parts array - // so they end up in the reverse order that they were in the module content. - // Ignore the first (index 0) element of the split array, which is the python - // code. - while (split.length > 1 && parts.length < NUMBER_OF_PARTS) { - const s = split.pop(); - if (s) { - parts.push(s.trim()); - } - } - if (parts.length <= PARTS_INDEX_METHODS) { - // This module was saved without methods. - parts.push(MARKER_METHODS + '[]'); - } else if (!parts[PARTS_INDEX_METHODS].startsWith(MARKER_METHODS)) { - // Older modules were saved with exported blocks instead of methods. - parts[PARTS_INDEX_METHODS] = MARKER_METHODS + '[]' - } - if (parts.length <= PARTS_INDEX_MODULE_TYPE) { - // This module was saved without the module type. - // This module is either a Project or an OpMode, but we don't know which from just the content. - parts.push(MODULE_TYPE_UNKNOWN); - } - if (parts.length <= PARTS_INDEX_COMPONENTS) { - // This module was saved without components. - parts.push(MARKER_COMPONENTS + '[]'); + blocks: { [key: string]: any }, + components: Component[], + events: Event[], + methods: Method[]): string { + const moduleContent = new ModuleContent( + module.moduleType, + blocks, + components, + events, + methods); + return moduleContent.getModuleContentText(); +} + +export function parseModuleContentText(moduleContentText: string): ModuleContent { + const parsedContent = JSON.parse(moduleContentText); + return new ModuleContent( + parsedContent.moduleType, + parsedContent.blocks, + parsedContent.components, + parsedContent.events, + parsedContent.methods); +} + +export class ModuleContent { + constructor( + private moduleType: string, + private blocks : { [key: string]: any }, + private components: Component[], + private events: Event[], + private methods: Method[]) { } - if (parts.length <= PARTS_INDEX_EVENTS) { - // This module was saved without events. - parts.push(MARKER_EVENTS + '[]'); + + getModuleContentText(): string { + return JSON.stringify(this); } - return parts; -} -/** - * Extract the blocks content from the given module content. - */ -export function extractBlocksContent(moduleContent: string): string { - const parts = getParts(moduleContent); - let blocksContent = parts[PARTS_INDEX_BLOCKS_CONTENT]; - if (blocksContent.startsWith(MARKER_BLOCKS_CONTENT)) { - blocksContent = blocksContent.substring(MARKER_BLOCKS_CONTENT.length); + getModuleType(): string { + return this.moduleType; } - return blocksContent; -} -/** - * Extract the methods from the given module content. - */ -export function extractMethods(moduleContent: string): Method[] { - const parts = getParts(moduleContent); - let methodsContent = parts[PARTS_INDEX_METHODS]; - if (methodsContent.startsWith(MARKER_METHODS)) { - methodsContent = methodsContent.substring(MARKER_METHODS.length); + getBlocks(): { [key: string]: any } { + return this.blocks; } - const methods: Method[] = JSON.parse(methodsContent); - return methods; -} -/** - * Extract the moduleType from the given module content. - */ -export function extractModuleType(moduleContent: string): string { - const parts = getParts(moduleContent); - let moduleType = parts[PARTS_INDEX_MODULE_TYPE]; - if (moduleType.startsWith(MARKER_MODULE_TYPE)) { - moduleType = moduleType.substring(MARKER_MODULE_TYPE.length); + getComponents(): Component[] { + return this.components; } - return moduleType; -} -/** - * Extract the components from the given module content. - */ -export function extractComponents(moduleContent: string): Component[] { - const parts = getParts(moduleContent); - let componentsContent = parts[PARTS_INDEX_COMPONENTS]; - if (componentsContent.startsWith(MARKER_COMPONENTS)) { - componentsContent = componentsContent.substring(MARKER_COMPONENTS.length); + getEvents(): Event[] { + return this.events; } - const components: Component[] = JSON.parse(componentsContent); - return components; -} -/** - * Extract the events from the given module content. - */ -export function extractEvents(moduleContent: string): Event[] { - const parts = getParts(moduleContent); - let eventsContent = parts[PARTS_INDEX_EVENTS]; - if (eventsContent.startsWith(MARKER_EVENTS)) { - eventsContent = eventsContent.substring(MARKER_EVENTS.length); + getMethods(): Method[] { + return this.methods; } - const events: Event[] = JSON.parse(eventsContent); - return events; } /** * Produce the blob for downloading a project. */ export async function produceDownloadProjectBlob( - projectName: string, moduleContents: { [key: string]: string }): Promise { + moduleNameToContentText: { [key: string]: string }): Promise { const zip = new JSZip(); - for (const moduleName in moduleContents) { - const moduleContent = moduleContents[moduleName]; - const moduleContentForDownload = _processModuleContentForDownload( - projectName, moduleName, moduleContent); - zip.file(moduleName, moduleContentForDownload); + for (const moduleName in moduleNameToContentText) { + const moduleContentText = moduleNameToContentText[moduleName]; + zip.file(moduleName, moduleContentText); } const content = await zip.generateAsync({ type: "blob" }); const blobUrl = URL.createObjectURL(content); @@ -746,53 +625,6 @@ export function getClassNameForModule(moduleType: string, moduleName: string) { : snakeCaseToPascalCase(moduleName); } -/** - * Process the module content so it can be downloaded. - */ -function _processModuleContentForDownload( - projectName: string, moduleName: string, moduleContent: string): string { - const parts = getParts(moduleContent); - let blocksContent = parts[PARTS_INDEX_BLOCKS_CONTENT]; - if (blocksContent.startsWith(MARKER_BLOCKS_CONTENT)) { - blocksContent = blocksContent.substring(MARKER_BLOCKS_CONTENT.length); - } - let moduleType = parts[PARTS_INDEX_MODULE_TYPE]; - if (moduleType.startsWith(MARKER_MODULE_TYPE)) { - moduleType = moduleType.substring(MARKER_MODULE_TYPE.length); - } - let methodsContent = parts[PARTS_INDEX_METHODS]; - if (methodsContent.startsWith(MARKER_METHODS)) { - methodsContent = methodsContent.substring(MARKER_METHODS.length); - } - let componentsContent = parts[PARTS_INDEX_COMPONENTS]; - if (componentsContent.startsWith(MARKER_COMPONENTS)) { - componentsContent = componentsContent.substring(MARKER_COMPONENTS.length); - } - let eventsContent = parts[PARTS_INDEX_EVENTS]; - if (eventsContent.startsWith(MARKER_EVENTS)) { - eventsContent = eventsContent.substring(MARKER_EVENTS.length); - } - - const module: Module = { - modulePath: makeModulePath(projectName, moduleName), - moduleType: moduleType, - projectName: projectName, - moduleName: moduleName, - dateModifiedMillis: 0, - className: getClassNameForModule(moduleType, moduleName), - }; - - // Clear out the python content. - const pythonCode = ''; - return makeModuleContent( - module, - pythonCode, - blocksContent, - methodsContent, - componentsContent, - eventsContent); -} - /** * Make a unique project name for an uploaded project. */ @@ -822,8 +654,9 @@ export function makeUploadProjectName( * Process the uploaded blob to get the module types and contents. */ export async function processUploadedBlob( - projectName: string, blobUrl: string) - : Promise<[{ [key: string]: string }, { [key: string]: string }]> { + projectName: string, blobUrl: string) + : Promise<[{ [key: string]: string }, { [key: string]: string }]> { + const prefix = 'data:application/octet-stream;base64,'; if (!blobUrl.startsWith(prefix)) { throw new Error('blobUrl does not have the expected prefix.'); @@ -845,79 +678,29 @@ export async function processUploadedBlob( ); // Process each module's content. - const moduleTypes: { [key: string]: string } = {}; // key is module name, value is module type - const moduleContents: { [key: string]: string } = {}; // key is module name, value is module content + const moduleNameToType: { [key: string]: string } = {}; // key is module name, value is module type + const moduleNameToContentText: { [key: string]: string } = {}; // key is module name, value is module content text for (const filename in files) { const uploadedContent = files[filename]; - let [moduleName, moduleType, moduleContent] = _processUploadedModule( - projectName, filename, uploadedContent); - - moduleTypes[moduleName] = moduleType; - moduleContents[moduleName] = moduleContent; + const [moduleName, moduleType, moduleContent] = _processUploadedModule( + projectName, filename, uploadedContent); + moduleNameToType[moduleName] = moduleType; + moduleNameToContentText[moduleName] = moduleContent; } - return [moduleTypes, moduleContents]; + return [moduleNameToType, moduleNameToContentText]; } /** - * Processes an uploaded module to get the module name, type, and content. + * Processes an uploaded module to get the module name, type, and content text. */ export function _processUploadedModule( - projectName: string, filename: string, uploadedContent: string) - : [string, string, string] { - const parts = getParts(uploadedContent); - let blocksContent = parts[PARTS_INDEX_BLOCKS_CONTENT]; - if (blocksContent.startsWith(MARKER_BLOCKS_CONTENT)) { - blocksContent = blocksContent.substring(MARKER_BLOCKS_CONTENT.length); - } - let moduleType = parts[PARTS_INDEX_MODULE_TYPE]; - if (moduleType.startsWith(MARKER_MODULE_TYPE)) { - moduleType = moduleType.substring(MARKER_MODULE_TYPE.length); - } - let methodsContent = parts[PARTS_INDEX_METHODS]; - if (methodsContent.startsWith(MARKER_METHODS)) { - methodsContent = methodsContent.substring(MARKER_METHODS.length); - } - let componentsContent = parts[PARTS_INDEX_COMPONENTS]; - if (componentsContent.startsWith(MARKER_COMPONENTS)) { - componentsContent = componentsContent.substring(MARKER_COMPONENTS.length); - } - let eventsContent = parts[PARTS_INDEX_EVENTS]; - if (eventsContent.startsWith(MARKER_EVENTS)) { - eventsContent = eventsContent.substring(MARKER_EVENTS.length); - } - - const moduleName = (moduleType === MODULE_TYPE_ROBOT) - ? projectName : filename; - - const module: Module = { - modulePath: makeModulePath(projectName, moduleName), - moduleType: moduleType, - projectName: projectName, - moduleName: moduleName, - dateModifiedMillis: 0, - className: snakeCaseToPascalCase(moduleName), - }; - - // Generate the python content. - // Create a headless blockly workspace. - const headlessBlocklyWorkspace = new Blockly.Workspace(); - headlessBlocklyWorkspace.options.oneBasedIndex = false; - Blockly.serialization.workspaces.load( - JSON.parse(blocksContent), headlessBlocklyWorkspace); - - const generatorContext = createGeneratorContext(); - generatorContext.setModule(module); - - const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode( - headlessBlocklyWorkspace, generatorContext); - - const moduleContent = makeModuleContent( - module, - pythonCode, - blocksContent, - methodsContent, - componentsContent, - eventsContent); - return [moduleName, moduleType, moduleContent]; + projectName: string, filename: string, uploadedContent: string) + : [string, string, string] { + + const moduleContent = parseModuleContentText(uploadedContent); + const moduleType = moduleContent.getModuleType(); + const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? projectName : filename; + const moduleContentText = moduleContent.getModuleContentText(); + return [moduleName, moduleType, moduleContentText]; }