function createPlayground(
  container,
  createWorkspace = Blockly.inject,
  defaultOptions = {
    toolbox: toolboxCategories,
  },
  config = {},
  vsEditorPath,
) {
  const {
    blocklyDiv,
    minimizeButton,
    monacoDiv,
    guiContainer,
    playgroundDiv,
    tabButtons,
    tabsDiv,
  } = renderPlayground(container);

  const monacoOptions = {
    model: null,
    language: 'xml',
    minimap: {
      enabled: false,
    },
    theme: 'vs-dark',
    scrollBeyondLastLine: false,
    automaticLayout: true,
  };

  // Load the code editor.
  return addCodeEditor(monacoDiv, monacoOptions, vsEditorPath).then(
    (editor) => {
      let workspace;

      // Create a model for displaying errors.
      const errorModel = window.monaco.editor.createModel('');
      const editorXmlContextKey = editor.createContextKey('isEditorXml', true);
      const editorJsonContextKey = editor.createContextKey(
        'isEditorJson',
        true,
      );

      // Load / Save playground state.
      // setting active tab as JSON
      const playgroundState = new LocalStorageState(`playgroundState_${id}`, {
        activeTab: 'JSON',
        playgroundOpen: true,
        autoGenerate: config && config.auto != undefined ? config.auto : true,
        workspaceJson: '',
      });
      playgroundState.load();

      /**
       * Register a generator and create a new code tab for it.
       * @param {string} name The generator label.
       * @param {string} language The monaco language to use.
       * @param {function(Blockly.WorkspaceSvg):string} generator
       *     The Blockly generator.
       * @param {boolean=} isReadOnly Whether the editor should be set to
       *     read-only mode.
       * @returns {!PlaygroundTab} An object that represents the newly created
       *     tab.
       */
      function registerGenerator(name, language, generator, isReadOnly) {
        const tabElement = renderCodeTab(name);
        tabElement.setAttribute('data-tab', name);
        tabsDiv.appendChild(tabElement);

        // Create a monaco editor model for each tab.
        const model = window.monaco.editor.createModel('', language);
        const state = {
          name,
          model,
          language,
          viewState: undefined,
        };

        /**
         * Call the generator, displaying an error message if it fails.
         */
        function generate() {
          let text;
          let generateModel = model;
          try {
            text = generator(workspace);
          } catch (e) {
            console.error(e);
            text = e.message;
            generateModel = errorModel;
            editor.updateOptions({
              wordWrap: true,
            });
          }
          if (
            generateModel.getValue() === text &&
            editor.getModel() === generateModel
          ) {
            return;
          }
          generateModel.pushEditOperations(
            [],
            [{range: generateModel.getFullModelRange(), text}],
            () => null,
          );
          editor.setModel(generateModel);
          editor.setSelection(new window.monaco.Range(0, 0, 0, 0));
        }

        const tab = {
          generate,
          state,
          tabElement,
        };
        return tab;
      }

      /**
       * Set the active tab.
       * @param {!PlaygroundTab} tab The new tab.
       */
      const setActiveTab = (tab) => {
        currentTab = tab;
        currentGenerate = tab.generate;
        const isXml = tab.state.name == 'XML';
        const isJson = tab.state.name == 'JSON';
        editor.setModel(currentTab.state.model);
        editor.updateOptions({
          readOnly: !isXml && !isJson,
          wordWrap: false,
        });

        // Update tab UI.
        Object.values(tabs).forEach(
          (t) =>
            (t.tabElement.style.background =
              t.tabElement == tab.tabElement ? '#1E1E1E' : '#2D2D2D'),
        );
        // Update editor state.
        editorXmlContextKey.set(isXml);
        editorJsonContextKey.set(isJson);
        updateImportButtonDisplay();
        playgroundState.set('activeTab', tab.state.name);
        playgroundState.save();
      };

      /**
       * Call the current generate method if we are in 'auto' mode. In
       * addition, persist the current workspace xml regardless of which tab
       * we are in.
       */
      const updateEditor = () => {
        if (playgroundState.get('autoGenerate')) {
          if (initialWorkspaceJson && isFirstLoad) {
            isFirstLoad = false;
            try {
              Blockly.serialization.workspaces.load(
                JSON.parse(initialWorkspaceJson),
                workspace,
              );
            } catch (e) {
              console.warn('Failed to auto import.', e);
            }
          }

          if (currentGenerate) {
            currentGenerate();
          }

          let code = '';
          try {
            code = JSON.stringify(
              Blockly.serialization.workspaces.save(workspace),
              null,
              2,
            );
          } catch (e) {
            console.warn('Failed to auto save.', e);
          }
          playgroundState.set('workspaceJson', code);
          playgroundState.save();
        }
      };

      // Register default tabs.
      const tabs = {
        JSON: registerGenerator('JSON', 'json', (ws) => {
          return JSON.stringify(
            Blockly.serialization.workspaces.save(ws),
            null,
            2,
          );
        }),
        XML: registerGenerator('XML', 'xml', (ws) => {
          return Blockly.Xml.domToPrettyText(Blockly.Xml.workspaceToDom(ws));
        }),
        JavaScript: registerGenerator(
          'JavaScript',
          'javascript',
          (ws) =>
            (javascriptGenerator || Blockly.JavaScript).workspaceToCode(ws),
          true,
        ),
        Python: registerGenerator(
          'Python',
          'python',
          (ws) => (pythonGenerator || Blockly.Python).workspaceToCode(ws),
          true,
        ),
        Dart: registerGenerator(
          'Dart',
          'dart',
          (ws) => (dartGenerator || Blockly.Dart).workspaceToCode(ws),
          true,
        ),
        Lua: registerGenerator(
          'Lua',
          'lua',
          (ws) => (luaGenerator || Blockly.Lua).workspaceToCode(ws),
          true,
        ),
        PHP: registerGenerator(
          'PHP',
          'php',
          (ws) => (phpGenerator || Blockly.PHP).workspaceToCode(ws),
          true,
        ),
      };

      // Handle tab click.
      tabsDiv.addEventListener('click', (e) => {
        const target = /** @type {HTMLElement} */ (e.target);
        const tabName = target.getAttribute('data-tab');
        if (!tabName) {
          // Not a tab.
          return;
        }
        const tab = tabs[tabName];

        // Save current tab state (eg: scroll position).
        currentTab.state.viewState = editor.saveViewState();

        setActiveTab(tab);
        updateEditor();

        // Restore tab state (eg: scroll position).
        editor.restoreViewState(currentTab.state.viewState);
        editor.focus();
      });

      // Initialized saved JSON and bind change listener.
      const initialWorkspaceJson = playgroundState.get('workspaceJson') || '';
      const jsonTab = tabs['JSON'];
      const jsonModel = jsonTab.state.model;
      let isFirstLoad = true;
      jsonModel.setValue(initialWorkspaceJson);
      jsonModel.onDidChangeContent(() => {
        playgroundState.set('workspaceJson', jsonModel.getValue());
        playgroundState.save();
      });

      // Create a button to import state from the editor tab to the workspace.
      const importButton = renderButton('Import');
      tabButtons.appendChild(importButton);
      importButton.addEventListener('click', (e) => {
        if (editorXmlContextKey.get()) {
          editor.getAction('import-xml').run();
        }
        if (editorJsonContextKey.get()) {
          editor.getAction('import-json').run();
        }
      });
      const updateImportButtonDisplay = function () {
        // The import button is only relevant for the XML and JSON tabs.
        if (editorXmlContextKey.get() || editorJsonContextKey.get()) {
          importButton.style.display = '';
        } else {
          importButton.style.display = 'none';
        }
      };

      // Set the initial tab as active.
      const activeTab = playgroundState.get('activeTab');
      let currentTab = tabs[activeTab];
      let currentGenerate;
      if (currentTab) {
        setActiveTab(currentTab);
      }

      // Load the GUI controls.
      const gui = addGUIControls(
        (options) => {
          workspace = createWorkspace(blocklyDiv, options);

          // Initialize the test toolbox.
          toolboxTestBlocksInit(
            /** @type {!Blockly.WorkspaceSvg} */ (workspace),
          );

          // Add download screenshot option.
          const prevConfigureContextMenu = workspace.configureContextMenu;
          workspace.configureContextMenu = (menuOptions, e) => {
            prevConfigureContextMenu &&
              prevConfigureContextMenu.call(null, menuOptions, e);

            const screenshotOption = {
              text: 'Download Screenshot',
              enabled: workspace.getTopBlocks().length,
              callback: function () {
                downloadWorkspaceScreenshot(workspace);
              },
            };
            menuOptions.push(screenshotOption);
          };

          updateEditor();
          workspace.addChangeListener((e) => {
            if (e.type !== 'ui' && e.type !== 'viewport_change') {
              updateEditor();
            }
          });
          return workspace;
        },
        defaultOptions,
        {
          disableResize: true,
          toolboxes: config.toolboxes || {
            'categories': toolboxCategories,
            'simple': toolboxSimple,
            'test blocks': toolboxTestBlocks,
          },
        },
      );

      // Move the GUI Element to the gui container.
      const guiElement = gui.domElement;
      guiElement.removeChild(guiElement.firstChild);
      guiElement.style.position = 'relative';
      guiElement.style.minWidth = '100%';
      guiContainer.appendChild(guiElement);

      // Click handler to toggle the playground.
      const togglePlayground = (e) => {
        const shouldOpen = playgroundDiv.style.display === 'none';
        if (shouldOpen) {
          playgroundDiv.style.display = 'flex';
          minimizeButton.textContent = 'Collapse';
        } else {
          playgroundDiv.style.display = 'none';
          minimizeButton.textContent = 'Expand';
        }
        playgroundState.set('playgroundOpen', shouldOpen);
        playgroundState.save();
        Blockly.svgResize(workspace);
      };
      minimizeButton.addEventListener('click', togglePlayground);

      // Start minimized if the playground was previously closed.
      if (playgroundState.get('playgroundOpen') === false) {
        togglePlayground();
      }

      // Playground API.

      /**
       * Get the current GUI controls.
       * @returns {!dat.GUI} The GUI controls.
       */
      const getGUI = function () {
        return gui;
      };

      /**
       * Get the current workspace.
       * @returns {!Blockly.WorkspaceSvg} The Blockly workspace.
       */
      const getWorkspace = function () {
        return workspace;
      };

      /**
       * Get the current tab.
       * @returns {!PlaygroundTab} The current tab.
       */
      const getCurrentTab = function () {
        return currentTab;
      };

      /**
       * Add a generator tab.
       * @param {string} label The label of the generator tab.
       * @param {Blockly.Generator} generator The Blockly generator.
       * @param {string=} language Optional editor language, defaults to
       *     'javascript'.
       */
      const addGenerator = function (label, generator, language) {
        if (!label || !generator) {
          throw Error('usage: addGenerator(label, generator, language?);');
        }
        tabs[label] = registerGenerator(
          label,
          language || 'javascript',
          (ws) => generator.workspaceToCode(ws),
          true,
        );
        if (activeTab === label) {
          // Set the new generator as the current tab if it is currently
          // active. This occurs when a dynamically added generator is active
          // and the page is reloaded.
          setActiveTab(tabs[label]);
        }
      };

      /**
       * Removes a generator tab.
       * @param {string} label The label of the generator tab to remove.
       */
      const removeGenerator = function (label) {
        if (!label) {
          throw Error('usage: removeGenerator(label);');
        }
        if (!(label in tabs)) {
          throw Error('removeGenerator called on invalid label: ' + label);
        }
        const tab = tabs[label];
        tabsDiv.removeChild(tab.tabElement);
        delete tabs[label];
        const tabsKeys = Object.keys(tabs);
        if (activeTab === label && tabsKeys.length) {
          // Set the active tab to another tab if the active tab corresponded
          // to a removed generator.
          setActiveTab(tabs[tabsKeys[0]]);
        }
      };

      const playground = {
        state: playgroundState,
        addAction: /** @type {?} */ (gui).addAction,
        addCheckboxAction: /** @type {?} */ (gui).addCheckboxAction,
        addGenerator,
        getCurrentTab,
        getGUI,
        getWorkspace,
        removeGenerator,
      };

      // Add tab buttons.
      registerTabButtons(editor, playground, tabButtons, updateEditor);

      // Register editor commands.
      registerEditorCommands(editor, playground);

      return playground;
    },
  );
}
Create the Blockly playground.
@param {!HTMLElement} container Container element.
@param {CreateWorkspaceFn=} createWorkspace A workspace creation method
    called every time the toolbox is re-configured.
@param {Blockly.BlocklyOptions=} defaultOptions The default workspace options
    to use.
@param {PlaygroundConfig=} config Optional Playground config.
@param {string=} vsEditorPath Optional editor path.
@returns {Promise<PlaygroundAPI>} A promise to the playground API.