header: lf()

in webapp/src/app.tsx [1680:2079]


                                header: lf("Extensions cannot be used together"),
                                body: lf("Extensions '{0}' and '{1}' cannot be used together, because they use incompatible settings ({2}).",
                                    confl.pkg1.id, confl.pkg0.id, confl.settingName)
                            })
                        }
                    });

                // load side docs
                const editorForFile = this.pickEditorFor(file);
                const documentation = pkg?.mainPkg?.config?.documentation;
                if (documentation)
                    this.setSideDoc(documentation, editorForFile == this.blocksEditor);
                else {
                    const readme = main.lookupFile("this/README.md");
                    const readmeContent = readme?.content?.trim();
                    // no auto-popup when editing packages locally
                    // ### @autoOpen false
                    if (!h.githubId && readmeContent && !/#{2,}\s+@autoOpen\s+false\s*/i.test(readmeContent))
                        this.setSideMarkdown(readme.content);
                }

                // update recentUse on the header
                return workspace.saveAsync(h)
            }).then(() => this.loadTutorialFiltersAsync())
            .finally(() => {
                // Editor is loaded
                pxt.BrowserUtils.changeHash("#editor", true);
                document.getElementById("root").focus(); // Clear the focus.
                cmds.maybeReconnectAsync(false, true);
                this.editorLoaded();
            })
    }

    private loadTutorialFiltersAsync(): Promise<void> {
        const header = pkg.mainEditorPkg().header;
        if (!header || !header.tutorial || !header.tutorial.tutorialMd)
            return Promise.resolve();

        pxt.perf.measureStart("loadTutorial loadBlockly")

        const t = header.tutorial;

        if (typeof t.tutorialCode === "string") {
            t.tutorialCode = [t.tutorialCode];
        }

        return this.loadBlocklyAsync()
            .then(() => tutorial.getUsedBlocksAsync(t.tutorialCode, t.tutorial, t.language))
            .then((tutorialBlocks) => {
                let editorState: pxt.editor.EditorState = {
                    searchBar: false
                }

                if (tutorialBlocks?.usedBlocks && Object.keys(tutorialBlocks.usedBlocks).length > 0) {
                    editorState.filters = {
                        blocks: tutorialBlocks.usedBlocks,
                        defaultState: pxt.editor.FilterState.Hidden
                    }
                    editorState.hasCategories = !(header.tutorial.metadata && header.tutorial.metadata.flyoutOnly);
                }
                this.setState({ editorState: editorState });
                this.editor.filterToolbox(true);
                const stepInfo = t.tutorialStepInfo;
                const showHint = stepInfo[header.tutorial.tutorialStep].showHint;
                if (showHint) this.showTutorialHint();
                //else this.showLightbox();
            })
            .catch(e => {
                // Failed to decompile
                pxt.tickEvent('tutorial.faileddecompile', { tutorial: t.tutorial });
                this.setState({ editorState: { searchBar: false, filters: undefined } });
                core.warningNotification(lf("Could not filter tutorial blocks, displaying full toolbox."))
            })
            .finally(() => {
                pxt.perf.measureEnd("loadTutorial loadBlockly")
            });
    }

    private async loadTutorialTemplateCodeAsync(): Promise<void> {
        const header = pkg.mainEditorPkg().header;
        if (!header || !header.tutorial || !header.tutorial.templateCode)
            return;

        const template = header.tutorial.templateCode;

        // Delete from the header so that we don't overwrite the user code if the tutorial is
        // re-opened
        delete header.tutorial.templateCode;
        let currentText = await workspace.getTextAsync(header.id);

        // If we're starting in the asset editor, always load into TS
        const preferredEditor = header.tutorial.metadata?.preferredEditor;
        if (preferredEditor && filenameForEditor(preferredEditor) === pxt.ASSETS_FILE) {
            currentText[pxt.MAIN_TS] = template;
        }

        const projectname = projectNameForEditor(preferredEditor || header.editor);


        if (projectname === pxt.JAVASCRIPT_PROJECT_NAME) {
            currentText[pxt.MAIN_TS] = template;
        }
        else if (projectname === pxt.PYTHON_PROJECT_NAME) {
            const pyCode = await compiler.decompilePythonSnippetAsync(template)
            if (pyCode) {
                currentText[pxt.MAIN_PY] = pyCode;
            }
        }
        else {
            const resp = await compiler.decompileBlocksSnippetAsync(template)
            const blockXML = resp.outfiles[pxt.MAIN_BLOCKS];
            if (blockXML) {
                currentText[pxt.MAIN_BLOCKS] = blockXML
            }
        }

        let newText = currentText;
        if (header.tutorial.mergeHeaderId) {
            const previousHeader = workspace.getHeader(header.tutorial.mergeHeaderId);
            if (previousHeader) {
                const previousText = await workspace.getTextAsync(previousHeader.id);
                newText = mergeProjectCode(previousText, currentText, !!header.tutorial.mergeCarryoverCode);
            }
        }

        for (const file of Object.keys(newText)) {
            if (newText[file] !== undefined) {
                pkg.mainEditorPkg().setFile(file, newText[file]);
            }
        }

        await workspace.saveAsync(header);

        let parsedTilemap: any;
        let parsedImage: any;

        try {
            parsedTilemap = newText[pxt.TILEMAP_JRES] ? JSON.parse(newText[pxt.TILEMAP_JRES]) : null;
            parsedImage = newText[pxt.IMAGES_JRES] ? JSON.parse(newText[pxt.IMAGES_JRES]) : null;
        }
        catch (e) {
            return Promise.reject(e);
        }

        const project = pxt.react.getTilemapProject();
        if (parsedTilemap) {
            project.loadTilemapJRes(pxt.inflateJRes(parsedTilemap), true);
        }
        if (parsedImage) {
            project.loadAssetsJRes(pxt.inflateJRes(parsedImage));
        }

        await pkg.mainEditorPkg().buildAssetsAsync();
        await pkg.mainEditorPkg().saveFilesAsync();

    }

    private loadTutorialJresCodeAsync(): Promise<void> {
        const header = pkg.mainEditorPkg().header;
        if (!header || !header.tutorial || !(header.tutorial.jres || header.tutorial.assetFiles))
            return Promise.resolve();

        const tilemapJRes = header.tutorial.jres || header.tutorial.assetFiles?.[pxt.TILEMAP_JRES];
        const imageJRes = header.tutorial.assetFiles?.[pxt.IMAGES_JRES];

        // Delete from the header so that we don't double-load the tiles into
        // the project if the project gets reloaded
        delete header.tutorial.jres;
        delete header.tutorial.assetFiles;

        let parsedTilemap: any;
        let parsedImage: any;

        try {
            parsedTilemap = tilemapJRes ? JSON.parse(tilemapJRes) : null;
            parsedImage = imageJRes ? JSON.parse(imageJRes) : null;
        }
        catch (e) {
            return Promise.reject(e);
        }

        const project = pxt.react.getTilemapProject();
        if (parsedTilemap) {
            project.loadTilemapJRes(pxt.inflateJRes(parsedTilemap), true);
        }
        if (parsedImage) {
            project.loadAssetsJRes(pxt.inflateJRes(parsedImage));
        }

        return pkg.mainEditorPkg().buildAssetsAsync();
    }

    private async loadTutorialCustomTsAsync(): Promise<void> {
        const mainPkg = pkg.mainEditorPkg();
        const header = mainPkg.header;
        if (!header || !header.tutorial || !header.tutorial.customTs)
            return Promise.resolve();

        const customTs = header.tutorial.customTs;
        await mainPkg.setContentAsync(pxt.TUTORIAL_CUSTOM_TS, customTs);
        await mainPkg.saveFilesAsync();
        return Promise.resolve();
    }

    removeProject() {
        if (!pkg.mainEditorPkg().header) return;

        core.confirmDelete(pkg.mainEditorPkg().header.name, () => {
            let curr = pkg.mainEditorPkg().header
            curr.isDeleted = true
            return workspace.forceSaveAsync(curr, {})
                .then(() => this.openHome());
        })
    }

    ///////////////////////////////////////////////////////////
    ////////////             Import               /////////////
    ///////////////////////////////////////////////////////////

    hexFileImporters: pxt.editor.IHexFileImporter[] = [{
        id: "default",
        canImport: data => data.meta.cloudId == "ks/" + pxt.appTarget.id || data.meta.cloudId == pxt.CLOUD_ID + pxt.appTarget.id // match on targetid
            || (Util.startsWith(data.meta.cloudId, pxt.CLOUD_ID + pxt.appTarget.id)) // trying to load white-label file into main target
        ,
        importAsync: (project, data) => {
            let h: pxt.workspace.InstallHeader = {
                target: pxt.appTarget.id,
                targetVersion: data.meta.targetVersions ? data.meta.targetVersions.target : undefined,
                editor: data.meta.editor,
                name: data.meta.name,
                meta: {},
                pubId: "",
                pubCurrent: false
            };
            const files = JSON.parse(data.source) as pxt.Map<string>;
            // we cannot load the workspace until we've loaded the project
            return workspace.installAsync(h, files)
                .then(hd => this.loadHeaderAsync(hd, null));
        }
    }];

    resourceImporters: pxt.editor.IResourceImporter[] = [
        new serial.ResourceImporter()
    ];


    isHexFile(filename: string): boolean {
        return /\.(hex|uf2)$/i.test(filename)
    }

    isBlocksFile(filename: string): boolean {
        return /\.blocks$/i.test(filename)
    }

    isTypescriptFile(filename: string): boolean {
        return /\.ts$/i.test(filename);
    }

    isPythonFile(filename: string): boolean {
        return /\.py$/i.test(filename);
    }

    isProjectFile(filename: string): boolean {
        return /\.(pxt|mkcd|mkcd-\w+)$/i.test(filename)
    }

    isPNGFile(filename: string): boolean {
        return pxt.appTarget.compile.saveAsPNG && /\.png$/i.test(filename);
    }

    isAssetFile(filename: string): boolean {
        let exts = pxt.appTarget.runtime ? pxt.appTarget.runtime.assetExtensions : null
        if (exts) {
            let ext = filename.replace(/.*\./, "").toLowerCase()
            return exts.indexOf(ext) >= 0
        }
        return false
    }

    importProjectCoreAsync(buf: Uint8Array, options?: pxt.editor.ImportFileOptions) {
        return (buf[0] == '{'.charCodeAt(0) ?
            Promise.resolve(pxt.U.uint8ArrayToString(buf)) :
            pxt.lzmaDecompressAsync(buf))
            .then(contents => {
                let parsedContents = JSON.parse(contents);
                if (parsedContents.target && parsedContents.target == pxt.appTarget.id) {
                    let blockSnippet = parsedContents as pxt.blocks.BlockSnippet;
                    blockSnippet.xml.forEach(xml => {
                        let text = pxt.Util.htmlUnescape(xml.replace(/^"|"$/g, ""));
                        pxt.blocks.loadBlocksXml(this.blocksEditor.editor, text)
                    })
                } else {
                    let data = parsedContents as pxt.cpp.HexFile;
                    this.importHex(data, options);
                }
            }).catch(e => {
                core.warningNotification(lf("Sorry, we could not import this project or block snippet."))
                this.openHome();
            });
    }

    importHexFile(file: File, options?: pxt.editor.ImportFileOptions) {
        if (!file) return;
        pxt.cpp.unpackSourceFromHexFileAsync(file)
            .then(data => this.importHex(data, options))
            .catch(e => {
                pxt.reportException(e);
                core.warningNotification(lf("Sorry, we could not recognize this file."))
            });
    }

    importBlocksFiles(file: File, options?: pxt.editor.ImportFileOptions) {
        if (!file) return;
        ts.pxtc.Util.fileReadAsTextAsync(file)
            .then(contents => {
                this.newProject({
                    filesOverride: { [pxt.MAIN_BLOCKS]: contents, [pxt.MAIN_TS]: "  " },
                    name: file.name.replace(/\.blocks$/i, '') || lf("Untitled")
                })
            })
    }

    importTypescriptFile(file: File, options?: pxt.editor.ImportFileOptions) {
        if (!file) return;
        ts.pxtc.Util.fileReadAsTextAsync(file)
            .then(contents => {
                this.newProject({
                    filesOverride: { [pxt.MAIN_BLOCKS]: '', [pxt.MAIN_TS]: contents || "  " },
                    name: file.name.replace(/\.ts$/i, '') || lf("Untitled")
                })
            })
    }

    importProjectFile(file: File, options?: pxt.editor.ImportFileOptions) {
        if (!file) return;
        ts.pxtc.Util.fileReadAsBufferAsync(file)
            .then(buf => this.importProjectCoreAsync(buf, options))
    }

    importPNGFile(file: File, options?: pxt.editor.ImportFileOptions) {
        if (!file) return;
        ts.pxtc.Util.fileReadAsBufferAsync(file)
            .then(buf => pxt.Util.decodeBlobAsync("data:image/png;base64," +
                btoa(pxt.Util.uint8ArrayToString(buf))))
            .then(buf => this.importProjectCoreAsync(buf, options))
    }

    importPNGBuffer(buf: ArrayBuffer) {
        pxt.Util.decodeBlobAsync("data:image/png;base64," +
            btoa(pxt.Util.uint8ArrayToString(new Uint8Array(buf))))
            .then(buf => this.importProjectCoreAsync(buf));
    }

    importAssetFile(file: File) {
        ts.pxtc.Util.fileReadAsBufferAsync(file)
            .then(buf => {
                let basename = file.name.replace(/.*[\/\\]/, "")
                return pkg.mainEditorPkg().saveAssetAsync(basename, buf)
            });
    }

    importHex(data: pxt.cpp.HexFile, options?: pxt.editor.ImportFileOptions) {
        if (!data || !data.meta) {
            if (data && (data as any)[pxt.CONFIG_NAME]) {
                data = cloudsync.reconstructMeta(data as any)
            } else {
                core.warningNotification(lf("Sorry, we could not recognize this file."))
                if (options && options.openHomeIfFailed) this.openHome();
                return;
            }
        }

        if (typeof data.source == "object") {
            (data as any).source = JSON.stringify(data.source)
        }

        // intercept newer files early
        if (this.hexFileImporters.some(fi => fi.id == "default" && fi.canImport(data))) {
            const checkAsync = this.tryCheckTargetVersionAsync(data.meta.targetVersions && data.meta.targetVersions.target);
            if (checkAsync) {
                checkAsync.then(() => {
                    if (options && options.openHomeIfFailed) this.newProject();
                });
                return;
            }
        }

        if (options && options.extension) {
            pxt.tickEvent("import.extension");
            const files = ts.pxtc.Util.jsonTryParse(data.source);
            if (files) {
                const n = data.meta.name;
                const fn = `${data.meta.name}.json`;
                // insert file into package
                const mpkg = pkg.mainEditorPkg();
                if (mpkg) {
                    pxt.debug(`adding ${fn} to package`);
                    // save file
                    mpkg.setContentAsync(fn, data.source)
                        .then(() => mpkg.updateConfigAsync(cfg => {