From f04407f2f4fe0775bb81f6c4ddb060ee0965e43b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Tainon?= Date: Wed, 5 Jun 2024 18:12:55 +0200 Subject: [PATCH] Make it possible to reload answer headless (without Blockly loaded in HTML) --- frontend/stepper/js/index.ts | 82 +++++++++++++++++++--------------- frontend/stepper/platforms.ts | 2 +- frontend/task/blocks/blocks.ts | 2 +- frontend/task/utils.ts | 4 +- 4 files changed, 50 insertions(+), 40 deletions(-) diff --git a/frontend/stepper/js/index.ts b/frontend/stepper/js/index.ts index 550dbdcee..8b283d71f 100644 --- a/frontend/stepper/js/index.ts +++ b/frontend/stepper/js/index.ts @@ -20,10 +20,9 @@ let originalFireNow; let originalSetBackgroundPathVertical_; export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { - let blocklyHelper; - - if (!context) { return; } - + if (!context) { + return; + } if (context && context.blocklyHelper && !context.blocklyHelper.fake) { context.blocklyHelper.unloadLevel(); } @@ -73,7 +72,32 @@ export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { }; } - blocklyHelper = window.getBlocklyHelper(context.infos.maxInstructions, context); + context.blocklyHelper = createBlocklyHelper(context); + context.onChange = () => {}; + + // There is a setTimeout delay in Blockly lib between blockly program loading and Blockly firing events. + // We overload this function to catch the Blockly firing event instant so that we know when the program + // is successfully reloaded and that the events won't trigger an editor content update which would trigger + // a stepper.exit + if ('main' === state.environment) { + window.Blockly.Events.fireNow_ = () => { + originalFireNow(); + context.blocklyHelper.reloading = false; + }; + } + + const groupsCategory = !!(context && context.infos && context.infos.includeBlocks && context.infos.includeBlocks.groupByCategory); + if (groupsCategory && 'tralalere' === options.app) { + overrideBlocklyFlyoutForCategories(isMobile); + } else if (originalSetBackgroundPathVertical_) { + window.Blockly.Flyout.prototype.setBackgroundPathVertical_ = originalSetBackgroundPathVertical_; + } + + yield* put(taskIncreaseContextId()); +} + +export function createBlocklyHelper(context: QuickAlgoLibrary) { + const blocklyHelper = window.getBlocklyHelper(context.infos.maxInstructions, context); log.getLogger('blockly_runner').debug('[blockly.editor] load blockly helper', context, blocklyHelper); // Override this function to keep handling the display, and avoiding a call to un-highlight the current block // during loadPrograms at the start of the program execution @@ -101,36 +125,15 @@ export function* loadBlocklyHelperSaga(context: QuickAlgoLibrary) { blocklyHelper.startingBlock = false; } - context.blocklyHelper = blocklyHelper; - context.onChange = () => {}; - if (!originalFireNow) { originalFireNow = window.Blockly.Events.fireNow_; } - // There is a setTimeout delay in Blockly lib between blockly program loading and Blockly firing events. - // We overload this function to catch the Blockly firing event instant so that we know when the program - // is successfully reloaded and that the events won't trigger an editor content update which would trigger - // a stepper.exit - if ('main' === state.environment) { - window.Blockly.Events.fireNow_ = () => { - originalFireNow(); - blocklyHelper.reloading = false; - }; - } - log.getLogger('blockly_runner').debug('[blockly.editor] load context into blockly editor'); blocklyHelper.loadContext(context); blocklyHelper.setIncludeBlocks(context.infos.includeBlocks); - const groupsCategory = !!(context && context.infos && context.infos.includeBlocks && context.infos.includeBlocks.groupByCategory); - if (groupsCategory && 'tralalere' === options.app) { - overrideBlocklyFlyoutForCategories(isMobile); - } else if (originalSetBackgroundPathVertical_) { - window.Blockly.Flyout.prototype.setBackgroundPathVertical_ = originalSetBackgroundPathVertical_; - } - - yield* put(taskIncreaseContextId()); + return blocklyHelper; } export const overrideBlocklyFlyoutForCategories = (isMobile: boolean) => { @@ -192,7 +195,7 @@ export const checkBlocklyCode = function (answer: Document, context: QuickAlgoLi let blocks; try { // This method can fail if Blockly is not loaded in the DOM. In this case it's ok we don't make the check - blocks = getBlocksFromXml(blockly); + blocks = getBlocksFromXml(state, context, blockly); } catch (e) { console.error(e); return; @@ -268,7 +271,7 @@ const getBlockCount = function (block, context: QuickAlgoLibrary) { return 1; } -export const getBlocklyBlocksUsage = function (answer: Document, context: QuickAlgoLibrary) { +export const getBlocklyBlocksUsage = function (answer: Document, context: QuickAlgoLibrary, state: AppStore) { // We cannot evaluate blocks as long as the answer has not been loaded into Blockly // Thus we wait that context.blocklyHelper.programs is filled (by BlocklyEditor) const blockly = (answer as unknown as BlockDocument)?.content?.blockly; @@ -284,7 +287,7 @@ export const getBlocklyBlocksUsage = function (answer: Document, context: QuickA let blocks; try { // This method can fail if Blockly is not loaded in the DOM. In this case it's ok we don't make the check - blocks = getBlocksFromXml(blockly); + blocks = getBlocksFromXml(state, context, blockly); } catch (e) { console.error(e); return { @@ -320,13 +323,20 @@ export function blocklyCount(blocks: any[], context: QuickAlgoLibrary): number { return blocksUsed; } -const getBlocksFromXml = function (xmlText) { - const xml = window.Blockly.Xml.textToDom(xmlText) - const tmpOptions = new window.Blockly.Options({}); - const tmpWorkspace = new window.Blockly.Workspace(tmpOptions); - window.Blockly.Xml.domToWorkspace(xml, tmpWorkspace); +const getBlocksFromXml = function (state: AppStore, context: QuickAlgoLibrary, xmlText: string) { + const xml = window.Blockly.Xml.textToDom(xmlText); + + const blocklyHelper = createBlocklyHelper(context); + const language = state.options.language.split('-')[0]; + blocklyHelper.load(language, false, 1, {}); + + if (!window.Blockly.mainWorkspace) { + window.Blockly.mainWorkspace = blocklyHelper.workspace; + } + + window.Blockly.Xml.domToWorkspace(xml, blocklyHelper.workspace); - return tmpWorkspace.getAllBlocks(); + return blocklyHelper.workspace.getAllBlocks(); }; export const blocklyFindLimited = (blocks, limitedUses, context) => { diff --git a/frontend/stepper/platforms.ts b/frontend/stepper/platforms.ts index 2e0e5237f..085945b00 100644 --- a/frontend/stepper/platforms.ts +++ b/frontend/stepper/platforms.ts @@ -24,7 +24,7 @@ export interface PlatformData { getSpecificBlocks?: (notionsBag: NotionsBag, includeBlocks?: QuickalgoTaskIncludeBlocks) => Block[], runner?: typeof AbstractRunner, checkCode?: (document: Document, context: QuickAlgoLibrary, state: AppStore, disabledValidations: string[]) => void, - getBlocksUsage?: (document: Document, context: QuickAlgoLibrary) => BlocksUsage, + getBlocksUsage?: (document: Document, context: QuickAlgoLibrary, state?: AppStore) => BlocksUsage, } const platformBundles = { diff --git a/frontend/task/blocks/blocks.ts b/frontend/task/blocks/blocks.ts index c3690d7de..c07c24634 100644 --- a/frontend/task/blocks/blocks.ts +++ b/frontend/task/blocks/blocks.ts @@ -238,7 +238,7 @@ function* checkSourceSaga() { blocksUsage.error = e.toString(); } - const currentUsage = getBlocksUsage(answer); + const currentUsage = getBlocksUsage(answer, state); if (currentUsage) { const maxInstructions = context.infos.maxInstructions ? context.infos.maxInstructions : Infinity; diff --git a/frontend/task/utils.ts b/frontend/task/utils.ts index ca3a1e303..02dddb61a 100644 --- a/frontend/task/utils.ts +++ b/frontend/task/utils.ts @@ -152,7 +152,7 @@ export function checkCompilingCode(answer: TaskAnswer|null, state: AppStore, dis } } -export function getBlocksUsage(answer: TaskAnswer|null) { +export function getBlocksUsage(answer: TaskAnswer|null, state: AppStore) { const context = quickAlgoLibraries.getContext(null, 'main'); if (!context || !answer) { return null; @@ -161,7 +161,7 @@ export function getBlocksUsage(answer: TaskAnswer|null) { const {document, platform} = answer; if (platformsList[platform].getBlocksUsage) { - return platformsList[platform].getBlocksUsage(document, context); + return platformsList[platform].getBlocksUsage(document, context, state); } return null;