diff --git a/gulpfile.js b/gulpfile.js index 1d6919171599..86a816851e72 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,7 +39,7 @@ const pxtcompiler = () => compileTsProject("pxtcompiler"); const pxtpy = () => compileTsProject("pxtpy"); const pxtsim = () => compileTsProject("pxtsim"); const pxtblocks = () => compileTsProject("pxtblocks"); -const pxtrunner = () => compileTsProject("pxtrunner"); +const pxtrunner = () => compileTsProject("pxtrunner", "built", true); const pxteditor = () => compileTsProject("pxteditor"); const pxtweb = () => compileTsProject("docfiles/pxtweb", "built/web"); const backendutils = () => compileTsProject("backendutils") @@ -90,7 +90,7 @@ const pxtembed = () => gulp.src([ "built/pxtblockly.js", "built/pxteditor.js", "built/pxtsim.js", - "built/pxtrunner.js" + "built/web/runnerembed.js" ]) .pipe(concat("pxtembed.js")) .pipe(gulp.dest("built/web")); @@ -126,7 +126,7 @@ function initWatch() { gulp.parallel(pxtpy, gulp.series(copyBlockly, pxtblocks, pxtblockly)), pxteditor, gulp.parallel(pxtrunner, cli, pxtcommon), - updatestrings, + gulp.parallel(updatestrings, browserifyEmbed), gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), targetjs, reactCommon, @@ -393,7 +393,6 @@ const copyWebapp = () => "built/pxtblocks.js", "built/pxtblockly.js", "built/pxtsim.js", - "built/pxtrunner.js", "built/pxteditor.js", "built/webapp/src/worker.js", "built/webapp/src/serviceworker.js", @@ -413,6 +412,10 @@ const browserifyAssetEditor = () => process.env.PXT_ENV == 'production' ? exec('node node_modules/browserify/bin/cmd ./built/webapp/src/assetEditor.js -g [ envify --NODE_ENV production ] -g uglifyify -o ./built/web/pxtasseteditor.js') : exec('node node_modules/browserify/bin/cmd built/webapp/src/assetEditor.js -o built/web/pxtasseteditor.js --debug') +const browserifyEmbed = () => process.env.PXT_ENV == 'production' ? + exec('node node_modules/browserify/bin/cmd ./built/pxtrunner/embed.js -g [ envify --NODE_ENV production ] -g uglifyify -o ./built/web/runnerembed.js') : + exec('node node_modules/browserify/bin/cmd built/pxtrunner/embed.js -o built/web/runnerembed.js --debug') + const buildSVGIcons = () => { let webfontsGenerator = require('@vusion/webfonts-generator') let name = "xicon" @@ -844,6 +847,7 @@ const buildAll = gulp.series( gulp.parallel(pxtpy, gulp.series(copyBlockly, pxtblocks, pxtblockly)), pxteditor, gulp.parallel(pxtrunner, cli, pxtcommon), + browserifyEmbed, gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), targetjs, reactCommon, diff --git a/pxtrunner/debugRunner.ts b/pxtrunner/debugRunner.ts index 663e5ac972b9..9b14cc48c59f 100644 --- a/pxtrunner/debugRunner.ts +++ b/pxtrunner/debugRunner.ts @@ -1,219 +1,217 @@ -namespace pxt.runner { - /** - * Starts the simulator and injects it into the provided container. - * the simulator will attempt to establish a websocket connection - * to the debugger's user interface on port 3234. - * - * @param container The container to inject the simulator into - */ - export function startDebuggerAsync(container: HTMLElement) { - const debugRunner = new DebugRunner(container); - debugRunner.start(); - } - - /** - * Runner messages are specific to the debugger host and not part - * of the debug protocol. They contain file system requests and other - * information for the server-side runner. - */ - interface RunnerMessage extends DebugProtocol.ProtocolMessage { - type: "runner"; - subtype: string; - } +/** + * Starts the simulator and injects it into the provided container. + * the simulator will attempt to establish a websocket connection + * to the debugger's user interface on port 3234. + * + * @param container The container to inject the simulator into + */ +export function startDebuggerAsync(container: HTMLElement) { + const debugRunner = new DebugRunner(container); + debugRunner.start(); +} + +/** + * Runner messages are specific to the debugger host and not part + * of the debug protocol. They contain file system requests and other + * information for the server-side runner. + */ +interface RunnerMessage extends DebugProtocol.ProtocolMessage { + type: "runner"; + subtype: string; +} + +/** + * Message that indicates that the simulator is ready to run code. + */ +interface ReadyMessage extends RunnerMessage { + subtype: "ready"; +} + + +/** + * Message containing code and debug information for simulator + */ +interface RunCodeMessage extends RunnerMessage { + subtype: "runcode"; + code: string; + usedParts: string[]; + usedArguments: pxt.Map; + breakpoints: pxtc.Breakpoint[]; +} + +/** + * Runner for the debugger that handles communication with the user + * interface. Also talks to the server for anything to do with + * the filesystem (like reading code) + */ +export class DebugRunner implements pxsim.protocol.DebugSessionHost { + private static RETRY_MS = 2500; + + private session: pxsim.SimDebugSession; + private ws: WebSocket; + private pkgLoaded = false; + + private dataListener: (msg: DebugProtocol.ProtocolMessage) => void; + private errorListener: (msg: string) => void; + private closeListener: () => void; + + private intervalId: number; + private intervalRunning = false; + + constructor(private container: HTMLElement) { } + + public start() { + + this.initializeWebsocket(); + + if (!this.intervalRunning) { + this.intervalRunning = true; + this.intervalId = setInterval(() => { + if (!this.ws) { + try { + this.initializeWebsocket(); + } + catch (e) { + console.warn(`Connection to server failed, retrying in ${DebugRunner.RETRY_MS} ms`); + } + } + }, DebugRunner.RETRY_MS); + } - /** - * Message that indicates that the simulator is ready to run code. - */ - interface ReadyMessage extends RunnerMessage { - subtype: "ready"; + this.session = new pxsim.SimDebugSession(this.container); + this.session.start(this); } + private initializeWebsocket() { + if (!pxt.BrowserUtils.isLocalHost() || !pxt.Cloud.localToken) + return; - /** - * Message containing code and debug information for simulator - */ - interface RunCodeMessage extends RunnerMessage { - subtype: "runcode"; - code: string; - usedParts: string[]; - usedArguments: Map; - breakpoints: pxtc.Breakpoint[]; - } - - /** - * Runner for the debugger that handles communication with the user - * interface. Also talks to the server for anything to do with - * the filesystem (like reading code) - */ - export class DebugRunner implements pxsim.protocol.DebugSessionHost { - private static RETRY_MS = 2500; - - private session: pxsim.SimDebugSession; - private ws: WebSocket; - private pkgLoaded = false; - - private dataListener: (msg: DebugProtocol.ProtocolMessage) => void; - private errorListener: (msg: string) => void; - private closeListener: () => void; - - private intervalId: number; - private intervalRunning = false; - - constructor(private container: HTMLElement) {} - - public start() { - - this.initializeWebsocket(); - - if (!this.intervalRunning) { - this.intervalRunning = true; - this.intervalId = setInterval(() => { - if (!this.ws) { - try { - this.initializeWebsocket(); - } - catch (e) { - console.warn(`Connection to server failed, retrying in ${DebugRunner.RETRY_MS} ms`); - } - } - }, DebugRunner.RETRY_MS); - } + pxt.debug('initializing debug pipe'); + this.ws = new WebSocket('ws://localhost:3234/' + pxt.Cloud.localToken + '/simdebug'); - this.session = new pxsim.SimDebugSession(this.container); - this.session.start(this); + this.ws.onopen = ev => { + pxt.debug('debug: socket opened'); } - private initializeWebsocket() { - if (!pxt.BrowserUtils.isLocalHost() || !Cloud.localToken) - return; - - pxt.debug('initializing debug pipe'); - this.ws = new WebSocket('ws://localhost:3234/' + Cloud.localToken + '/simdebug'); + this.ws.onclose = ev => { + pxt.debug('debug: socket closed') - this.ws.onopen = ev => { - pxt.debug('debug: socket opened'); + if (this.closeListener) { + this.closeListener(); } - this.ws.onclose = ev => { - pxt.debug('debug: socket closed') + this.session.stopSimulator(); - if (this.closeListener) { - this.closeListener(); - } + this.ws = undefined; + } - this.session.stopSimulator(); + this.ws.onerror = ev => { + pxt.debug('debug: socket closed due to error') - this.ws = undefined; + if (this.errorListener) { + this.errorListener(ev.type); } - this.ws.onerror = ev => { - pxt.debug('debug: socket closed due to error') + this.session.stopSimulator(); - if (this.errorListener) { - this.errorListener(ev.type); - } + this.ws = undefined; + } - this.session.stopSimulator(); + this.ws.onmessage = ev => { + let message: DebugProtocol.ProtocolMessage; - this.ws = undefined; + try { + message = JSON.parse(ev.data); + } catch (e) { + pxt.debug('debug: could not parse message') } - this.ws.onmessage = ev => { - let message: DebugProtocol.ProtocolMessage; - - try { - message = JSON.parse(ev.data); - } catch (e) { - pxt.debug('debug: could not parse message') + if (message) { + // FIXME: ideally, we should just open two websockets instead of adding to the + // debug protocol. One for the debugger, one for meta-information and file + // system requests + if (message.type === 'runner') { + this.handleRunnerMessage(message as RunnerMessage); } - - if (message) { - // FIXME: ideally, we should just open two websockets instead of adding to the - // debug protocol. One for the debugger, one for meta-information and file - // system requests - if (message.type === 'runner') { - this.handleRunnerMessage(message as RunnerMessage); - } - else { - // Intercept the launch configuration and notify the server-side debug runner - if (message.type === "request" && (message as DebugProtocol.Request).command === "launch") { - this.sendRunnerMessage("configure", { - projectDir: (message as any).arguments.projectDir - }); - } - this.dataListener(message); + else { + // Intercept the launch configuration and notify the server-side debug runner + if (message.type === "request" && (message as DebugProtocol.Request).command === "launch") { + this.sendRunnerMessage("configure", { + projectDir: (message as any).arguments.projectDir + }); } + this.dataListener(message); } } } + } - public send(msg: string): void { - this.ws.send(msg); - } + public send(msg: string): void { + this.ws.send(msg); + } - public onData(cb: (msg: DebugProtocol.ProtocolMessage) => void): void { - this.dataListener = cb; - } + public onData(cb: (msg: DebugProtocol.ProtocolMessage) => void): void { + this.dataListener = cb; + } - public onError(cb: (e?: any) => void): void { - this.errorListener = cb; - } + public onError(cb: (e?: any) => void): void { + this.errorListener = cb; + } - public onClose(cb: () => void): void { - this.closeListener = cb; - } + public onClose(cb: () => void): void { + this.closeListener = cb; + } - public close(): void { - if (this.session) { - this.session.stopSimulator(true); - } + public close(): void { + if (this.session) { + this.session.stopSimulator(true); + } - if (this.intervalRunning) { - clearInterval(this.intervalId); - this.intervalId = undefined; - } + if (this.intervalRunning) { + clearInterval(this.intervalId); + this.intervalId = undefined; + } - if (this.ws) { - this.ws.close(); - } + if (this.ws) { + this.ws.close(); } + } - private handleRunnerMessage(msg: RunnerMessage) { - switch (msg.subtype) { - case "ready": - this.sendRunnerMessage("ready"); - break; - case "runcode": - this.runCode(msg as RunCodeMessage); - break; - } + private handleRunnerMessage(msg: RunnerMessage) { + switch (msg.subtype) { + case "ready": + this.sendRunnerMessage("ready"); + break; + case "runcode": + this.runCode(msg as RunCodeMessage); + break; } + } - private runCode(msg: RunCodeMessage) { - const breakpoints: [number, DebugProtocol.Breakpoint][] = []; - - // The breakpoints are in the format returned by the compiler - // and need to be converted to the format used by the DebugProtocol - msg.breakpoints.forEach(bp => { - breakpoints.push([bp.id, { - verified: true, - line: bp.line, - column: bp.column, - endLine: bp.endLine, - endColumn: bp.endColumn, - source: { - path: bp.fileName - } - }]); - }); + private runCode(msg: RunCodeMessage) { + const breakpoints: [number, DebugProtocol.Breakpoint][] = []; + + // The breakpoints are in the format returned by the compiler + // and need to be converted to the format used by the DebugProtocol + msg.breakpoints.forEach(bp => { + breakpoints.push([bp.id, { + verified: true, + line: bp.line, + column: bp.column, + endLine: bp.endLine, + endColumn: bp.endColumn, + source: { + path: bp.fileName + } + }]); + }); - this.session.runCode(msg.code, msg.usedParts, msg.usedArguments, new pxsim.BreakpointMap(breakpoints), pxt.appTarget.simulator.boardDefinition); - } + this.session.runCode(msg.code, msg.usedParts, msg.usedArguments, new pxsim.BreakpointMap(breakpoints), pxt.appTarget.simulator.boardDefinition); + } - private sendRunnerMessage(subtype: string, msg: Map = {}) { - msg["subtype"] = subtype; - msg["type"] = "runner"; - this.send(JSON.stringify(msg)); - } + private sendRunnerMessage(subtype: string, msg: pxt.Map = {}) { + msg["subtype"] = subtype; + msg["type"] = "runner"; + this.send(JSON.stringify(msg)); } } \ No newline at end of file diff --git a/pxtrunner/embed.ts b/pxtrunner/embed.ts new file mode 100644 index 000000000000..43bd735c4ead --- /dev/null +++ b/pxtrunner/embed.ts @@ -0,0 +1,26 @@ +import * as runner from "./runner"; +import * as renderer from "./renderer"; + +/** + * This file serves as the browserify entry point for compiling + * pxtrunner. You probably don't want to import this file since + * it just pollutes the global namespace. The browserified code + * gets appended to pxtembed.js which is used in --docs, --embed, + * --run, etc. + */ + +if (!window.pxt) { + (window as any).pxt = {}; +} + +(window as any).pxt.runner = { + ...runner, + ...renderer +} + +function windowLoad() { + let f = (window as any).ksRunnerWhenLoaded + if (f) f(); +} + +windowLoad(); \ No newline at end of file diff --git a/pxtrunner/renderer.ts b/pxtrunner/renderer.ts index 49bfddf3a73f..ecc1e2165e77 100644 --- a/pxtrunner/renderer.ts +++ b/pxtrunner/renderer.ts @@ -1,662 +1,679 @@ -namespace pxt.runner { - const JS_ICON = "icon xicon js"; - const PY_ICON = "icon xicon python"; - const BLOCKS_ICON = "icon xicon blocks"; - - export interface ClientRenderOptions { - snippetClass?: string; - signatureClass?: string; - blocksClass?: string; - blocksXmlClass?: string; - diffBlocksXmlClass?: string; - diffBlocksClass?: string; - diffClass?: string; - staticPythonClass?: string; // typescript to be converted to static python - diffStaticPythonClass?: string; // diff between two spy snippets - projectClass?: string; - blocksAspectRatio?: number; - simulatorClass?: string; - linksClass?: string; - namespacesClass?: string; - apisClass?: string; - codeCardClass?: string; - tutorial?: boolean; - snippetReplaceParent?: boolean; - simulator?: boolean; - hex?: boolean; - hexName?: string; - pxtUrl?: string; - packageClass?: string; - package?: string; - jresClass?: string; - assetJSONClass?: string; - assetJSON?: Map; - showEdit?: boolean; - showJavaScript?: boolean; // default is to show blocks first - split?: boolean; // split in multiple divs if too big +import { DecompileResult, compileBlocksAsync, decompileSnippetAsync, renderProjectAsync } from "./runner"; + +const JS_ICON = "icon xicon js"; +const PY_ICON = "icon xicon python"; +const BLOCKS_ICON = "icon xicon blocks"; + +export interface ClientRenderOptions { + snippetClass?: string; + signatureClass?: string; + blocksClass?: string; + blocksXmlClass?: string; + diffBlocksXmlClass?: string; + diffBlocksClass?: string; + diffClass?: string; + staticPythonClass?: string; // typescript to be converted to static python + diffStaticPythonClass?: string; // diff between two spy snippets + projectClass?: string; + blocksAspectRatio?: number; + simulatorClass?: string; + linksClass?: string; + namespacesClass?: string; + apisClass?: string; + codeCardClass?: string; + tutorial?: boolean; + snippetReplaceParent?: boolean; + simulator?: boolean; + hex?: boolean; + hexName?: string; + pxtUrl?: string; + packageClass?: string; + package?: string; + jresClass?: string; + assetJSONClass?: string; + assetJSON?: pxt.Map; + showEdit?: boolean; + showJavaScript?: boolean; // default is to show blocks first + split?: boolean; // split in multiple divs if too big +} + +export function defaultClientRenderOptions() { + const renderOptions: ClientRenderOptions = { + blocksAspectRatio: window.innerHeight < window.innerWidth ? 1.62 : 1 / 1.62, + snippetClass: 'lang-blocks', + signatureClass: 'lang-sig', + blocksClass: 'lang-block', + blocksXmlClass: 'lang-blocksxml', + diffBlocksXmlClass: 'lang-diffblocksxml', + diffClass: 'lang-diff', + diffStaticPythonClass: 'lang-diffspy', + diffBlocksClass: 'lang-diffblocks', + staticPythonClass: 'lang-spy', + simulatorClass: 'lang-sim', + linksClass: 'lang-cards', + namespacesClass: 'lang-namespaces', + apisClass: 'lang-apis', + codeCardClass: 'lang-codecard', + packageClass: 'lang-package', + jresClass: 'lang-jres', + assetJSONClass: 'lang-assetsjson', + projectClass: 'lang-project', + snippetReplaceParent: true, + simulator: true, + showEdit: true, + hex: true, + tutorial: false, + showJavaScript: false, + hexName: pxt.appTarget.id } - - export function defaultClientRenderOptions() { - const renderOptions: ClientRenderOptions = { - blocksAspectRatio: window.innerHeight < window.innerWidth ? 1.62 : 1 / 1.62, - snippetClass: 'lang-blocks', - signatureClass: 'lang-sig', - blocksClass: 'lang-block', - blocksXmlClass: 'lang-blocksxml', - diffBlocksXmlClass: 'lang-diffblocksxml', - diffClass: 'lang-diff', - diffStaticPythonClass: 'lang-diffspy', - diffBlocksClass: 'lang-diffblocks', - staticPythonClass: 'lang-spy', - simulatorClass: 'lang-sim', - linksClass: 'lang-cards', - namespacesClass: 'lang-namespaces', - apisClass: 'lang-apis', - codeCardClass: 'lang-codecard', - packageClass: 'lang-package', - jresClass: 'lang-jres', - assetJSONClass: 'lang-assetsjson', - projectClass: 'lang-project', - snippetReplaceParent: true, - simulator: true, - showEdit: true, - hex: true, - tutorial: false, - showJavaScript: false, - hexName: pxt.appTarget.id + return renderOptions; +} + +export interface WidgetOptions { + showEdit?: boolean; + showJs?: boolean; + showPy?: boolean; + hideGutter?: boolean; + run?: boolean; + hexname?: string; + hex?: string; +} + +declare const hljs: any; + +function highlight($js: JQuery) { + if (typeof hljs !== "undefined") { + if ($js.hasClass("highlight")) { + hljs.highlightBlock($js[0]); } - return renderOptions; - } - - export interface WidgetOptions { - showEdit?: boolean; - showJs?: boolean; - showPy?: boolean; - hideGutter?: boolean; - run?: boolean; - hexname?: string; - hex?: string; + else { + $js.find('code.highlight').each(function (i, block) { + hljs.highlightBlock(block); + }); + } + highlightLine($js); } +} - function highlight($js: JQuery) { - if (typeof hljs !== "undefined") { - if ($js.hasClass("highlight")) { - hljs.highlightBlock($js[0]); +function highlightLine($js: JQuery) { + // apply line highlighting + $js.find("span.hljs-comment:contains(@highlight)") + .each((i, el) => { + try { + highlightLineElement(el); + } catch (e) { + pxt.reportException(e); } - else { - $js.find('code.highlight').each(function (i, block) { - hljs.highlightBlock(block); - }); + }) +} + +function highlightLineElement(el: Element) { + const $el = $(el); + const span = document.createElement("span"); + span.className = "highlight-line" + + // find new line and split text node + let next = el.nextSibling; + if (!next || next.nodeType != Node.TEXT_NODE) return; // end of snippet? + let text = (next as Text).textContent; + let inewline = text.indexOf('\n'); + if (inewline < 0) + return; // there should have been a new line here + + // split the next node + (next as Text).textContent = text.substring(0, inewline + 1); + $(document.createTextNode(text.substring(inewline + 1).replace(/^\s+/, ''))).insertAfter($(next)); + + // process and highlight new line + next = next.nextSibling; + while (next) { + let nextnext = next.nextSibling; // before we hoist it from the tree + if (next.nodeType == Node.TEXT_NODE) { + text = (next as Text).textContent; + const inewline = text.indexOf('\n'); + if (inewline < 0) { + span.appendChild(next); + next = nextnext; + } else { + // we've hit the end of the line... split node in two + span.appendChild(document.createTextNode(text.substring(0, inewline))); + (next as Text).textContent = text.substring(inewline + 1); + break; } - highlightLine($js); + } else { + span.appendChild(next); + next = nextnext; } } - function highlightLine($js: JQuery) { - // apply line highlighting - $js.find("span.hljs-comment:contains(@highlight)") - .each((i, el) => { - try { - highlightLineElement(el); - } catch (e) { - pxt.reportException(e); - } - }) - } + // insert back + $(span).insertAfter($el); + // remove line entry + $el.remove(); +} + +function appendBlocks($parent: JQuery, $svg: JQuery) { + $parent.append($(`
`).append($svg)); +} + +function appendJs($parent: JQuery, $js: JQuery, woptions: WidgetOptions) { + $parent.append($(`
JavaScript
`).append($js)); + highlight($js); +} + +function appendPy($parent: JQuery, $py: JQuery, woptions: WidgetOptions) { + $parent.append($(`
Python
`).append($py)); + highlight($py); +} + +function snippetBtn(label: string, icon: string): JQuery { + const $btn = $(``); + $btn.attr("aria-label", label); + $btn.attr("title", label); + $btn.find('i').attr("class", icon); + $btn.find('span').text(label); + + addFireClickOnEnter($btn); + return $btn; +} + +function addFireClickOnEnter(el: JQuery) { + el.keypress(e => { + const charCode = (typeof e.which == "number") ? e.which : e.keyCode; + if (charCode === 13 /* enter */ || charCode === 32 /* space */) { + e.preventDefault(); + e.currentTarget.click(); + } + }); +} + +function fillWithWidget( + options: ClientRenderOptions, + $container: JQuery, + $js: JQuery, + $py: JQuery, + $svg: JQuery, + decompileResult: DecompileResult, + woptions: WidgetOptions = {} +) { + let $h = $(''); + let $c = $('
'); + let $menu = $h.find('.right.menu'); + + const theme = pxt.appTarget.appTheme || {}; + if (woptions.showEdit && !theme.hideDocsEdit && decompileResult) { // edit button + const $editBtn = snippetBtn(lf("Edit"), "edit icon"); + + const { package: pkg, compileBlocks, compilePython } = decompileResult; + const host = pkg.host(); + + if ($svg && compileBlocks) { + pkg.setPreferredEditor(pxt.BLOCKS_PROJECT_NAME); + host.writeFile(pkg, pxt.MAIN_BLOCKS, compileBlocks.outfiles[pxt.MAIN_BLOCKS]); + } else if ($py && compilePython) { + pkg.setPreferredEditor(pxt.PYTHON_PROJECT_NAME); + host.writeFile(pkg, pxt.MAIN_PY, compileBlocks.outfiles[pxt.MAIN_PY]); + } else { + pkg.setPreferredEditor(pxt.JAVASCRIPT_PROJECT_NAME); + } - function highlightLineElement(el: Element) { - const $el = $(el); - const span = document.createElement("span"); - span.className = "highlight-line" - - // find new line and split text node - let next = el.nextSibling; - if (!next || next.nodeType != Node.TEXT_NODE) return; // end of snippet? - let text = (next as Text).textContent; - let inewline = text.indexOf('\n'); - if (inewline < 0) - return; // there should have been a new line here - - // split the next node - (next as Text).textContent = text.substring(0, inewline + 1); - $(document.createTextNode(text.substring(inewline + 1).replace(/^\s+/, ''))).insertAfter($(next)); - - // process and highlight new line - next = next.nextSibling; - while (next) { - let nextnext = next.nextSibling; // before we hoist it from the tree - if (next.nodeType == Node.TEXT_NODE) { - text = (next as Text).textContent; - const inewline = text.indexOf('\n'); - if (inewline < 0) { - span.appendChild(next); - next = nextnext; - } else { - // we've hit the end of the line... split node in two - span.appendChild(document.createTextNode(text.substring(0, inewline))); - (next as Text).textContent = text.substring(inewline + 1); - break; + if (options.assetJSON) { + for (const key of Object.keys(options.assetJSON)) { + if (pkg.config.files.indexOf(key) < 0) { + pkg.config.files.push(key); } - } else { - span.appendChild(next); - next = nextnext; + host.writeFile(pkg, key, options.assetJSON[key]); } } - // insert back - $(span).insertAfter($el); - // remove line entry - $el.remove(); - } - - function appendBlocks($parent: JQuery, $svg: JQuery) { - $parent.append($(`
`).append($svg)); - } - - function appendJs($parent: JQuery, $js: JQuery, woptions: WidgetOptions) { - $parent.append($(`
JavaScript
`).append($js)); - highlight($js); + const compressed = pkg.compressToFileAsync(); + $editBtn.click(() => { + pxt.tickEvent("docs.btn", { button: "edit" }); + compressed.then(buf => { + window.open(`${getEditUrl(options)}/#project:${ts.pxtc.encodeBase64(pxt.Util.uint8ArrayToString(buf))}`, 'pxt'); + }); + }); + $menu.append($editBtn); } - function appendPy($parent: JQuery, $py: JQuery, woptions: WidgetOptions) { - $parent.append($(`
Python
`).append($py)); - highlight($py); + if (options.showJavaScript || (!$svg && !$py)) { + // js + $c.append($js); + appendBlocksButton(); + appendPyButton(); + } else if ($svg) { + // blocks + $c.append($svg); + appendJsButton(); + appendPyButton(); + } else if ($py) { + $c.append($py); + appendBlocksButton(); + appendJsButton(); } - function snippetBtn(label: string, icon: string): JQuery { - const $btn = $(``); - $btn.attr("aria-label", label); - $btn.attr("title", label); - $btn.find('i').attr("class", icon); - $btn.find('span').text(label); - - addFireClickOnEnter($btn); - return $btn; + // runner menu + if (woptions.run && !theme.hideDocsSimulator) { + let $runBtn = snippetBtn(lf("Run"), "play icon").click(() => { + pxt.tickEvent("docs.btn", { button: "sim" }); + if ($c.find('.sim')[0]) { + $c.find('.sim').remove(); // remove previous simulators + scrollJQueryIntoView($c); + } else { + let padding = '81.97%'; + if (pxt.appTarget.simulator) padding = (100 / pxt.appTarget.simulator.aspectRatio) + '%'; + const deps = options.package ? "&deps=" + encodeURIComponent(options.package) : ""; + const url = getRunUrl(options) + "#nofooter=1" + deps; + const assets = options.assetJSON ? `data-assets="${encodeURIComponent(JSON.stringify(options.assetJSON))}"` : ""; + const data = encodeURIComponent($js.text()); + let $embed = $(`
`); + $c.append($embed); + + scrollJQueryIntoView($embed); + } + }) + $menu.append($runBtn); } - function addFireClickOnEnter(el: JQuery) { - el.keypress(e => { - const charCode = (typeof e.which == "number") ? e.which : e.keyCode; - if (charCode === 13 /* enter */ || charCode === 32 /* space */) { - e.preventDefault(); - e.currentTarget.click(); - } - }); + if (woptions.hexname && woptions.hex) { + let $hexBtn = snippetBtn(lf("Download"), "download icon").click(() => { + pxt.tickEvent("docs.btn", { button: "hex" }); + pxt.BrowserUtils.browserDownloadBinText(woptions.hex, woptions.hexname, { contentType: pxt.appTarget.compile.hexMimeType }); + }) + $menu.append($hexBtn); } - function fillWithWidget( - options: ClientRenderOptions, - $container: JQuery, - $js: JQuery, - $py: JQuery, - $svg: JQuery, - decompileResult: DecompileResult, - woptions: WidgetOptions = {} - ) { - let $h = $(''); - let $c = $('
'); - let $menu = $h.find('.right.menu'); - - const theme = pxt.appTarget.appTheme || {}; - if (woptions.showEdit && !theme.hideDocsEdit && decompileResult) { // edit button - const $editBtn = snippetBtn(lf("Edit"), "edit icon"); - - const { package: pkg, compileBlocks, compilePython } = decompileResult; - const host = pkg.host(); - - if ($svg && compileBlocks) { - pkg.setPreferredEditor(pxt.BLOCKS_PROJECT_NAME); - host.writeFile(pkg, pxt.MAIN_BLOCKS, compileBlocks.outfiles[pxt.MAIN_BLOCKS]); - } else if ($py && compilePython) { - pkg.setPreferredEditor(pxt.PYTHON_PROJECT_NAME); - host.writeFile(pkg, pxt.MAIN_PY, compileBlocks.outfiles[pxt.MAIN_PY]); + let r = $(`
`); + // don't add menu if empty + if ($menu.children().length) + r.append($h); + r.append($c); + + // inject container + $container.replaceWith(r); + + function appendBlocksButton() { + if (!$svg) return; + const $svgBtn = snippetBtn(lf("Blocks"), BLOCKS_ICON).click(() => { + pxt.tickEvent("docs.btn", { button: "blocks" }); + if ($c.find('.blocks')[0]) { + $c.find('.blocks').remove(); + scrollJQueryIntoView($c); } else { - pkg.setPreferredEditor(pxt.JAVASCRIPT_PROJECT_NAME); - } + if ($js) appendBlocks($js.parent(), $svg); + else appendBlocks($c, $svg); - if (options.assetJSON) { - for (const key of Object.keys(options.assetJSON)) { - if (pkg.config.files.indexOf(key) < 0) { - pkg.config.files.push(key); - } - host.writeFile(pkg, key, options.assetJSON[key]); - } + scrollJQueryIntoView($svg); } + }) + $menu.append($svgBtn); + } - const compressed = pkg.compressToFileAsync(); - $editBtn.click(() => { - pxt.tickEvent("docs.btn", { button: "edit" }); - compressed.then(buf => { - window.open(`${getEditUrl(options)}/#project:${ts.pxtc.encodeBase64(Util.uint8ArrayToString(buf))}`, 'pxt'); - }); - }); - $menu.append($editBtn); - } - - if (options.showJavaScript || (!$svg && !$py)) { - // js - $c.append($js); - appendBlocksButton(); - appendPyButton(); - } else if ($svg) { - // blocks - $c.append($svg); - appendJsButton(); - appendPyButton(); - } else if ($py) { - $c.append($py); - appendBlocksButton(); - appendJsButton(); - } - - // runner menu - if (woptions.run && !theme.hideDocsSimulator) { - let $runBtn = snippetBtn(lf("Run"), "play icon").click(() => { - pxt.tickEvent("docs.btn", { button: "sim" }); - if ($c.find('.sim')[0]) { - $c.find('.sim').remove(); // remove previous simulators + function appendJsButton() { + if (!$js) return; + if (woptions.showJs) + appendJs($c, $js, woptions); + else { + const $jsBtn = snippetBtn("JavaScript", JS_ICON).click(() => { + pxt.tickEvent("docs.btn", { button: "js" }); + if ($c.find('.js')[0]) { + $c.find('.js').remove(); scrollJQueryIntoView($c); } else { - let padding = '81.97%'; - if (pxt.appTarget.simulator) padding = (100 / pxt.appTarget.simulator.aspectRatio) + '%'; - const deps = options.package ? "&deps=" + encodeURIComponent(options.package) : ""; - const url = getRunUrl(options) + "#nofooter=1" + deps; - const assets = options.assetJSON ? `data-assets="${encodeURIComponent(JSON.stringify(options.assetJSON))}"` : ""; - const data = encodeURIComponent($js.text()); - let $embed = $(`
`); - $c.append($embed); - - scrollJQueryIntoView($embed); - } - }) - $menu.append($runBtn); - } + if ($svg) appendJs($svg.parent(), $js, woptions); + else appendJs($c, $js, woptions); - if (woptions.hexname && woptions.hex) { - let $hexBtn = snippetBtn(lf("Download"), "download icon").click(() => { - pxt.tickEvent("docs.btn", { button: "hex" }); - BrowserUtils.browserDownloadBinText(woptions.hex, woptions.hexname, { contentType: pxt.appTarget.compile.hexMimeType }); + scrollJQueryIntoView($js); + } }) - $menu.append($hexBtn); + $menu.append($jsBtn); } + } - let r = $(`
`); - // don't add menu if empty - if ($menu.children().length) - r.append($h); - r.append($c); - - // inject container - $container.replaceWith(r); - - function appendBlocksButton() { - if (!$svg) return; - const $svgBtn = snippetBtn(lf("Blocks"), BLOCKS_ICON).click(() => { - pxt.tickEvent("docs.btn", { button: "blocks" }); - if ($c.find('.blocks')[0]) { - $c.find('.blocks').remove(); + function appendPyButton() { + if (!$py) return; + if (woptions.showPy) { + appendPy($c, $py, woptions); + } else { + const $pyBtn = snippetBtn("Python", PY_ICON).click(() => { + pxt.tickEvent("docs.btn", { button: "py" }); + if ($c.find('.py')[0]) { + $c.find('.py').remove(); scrollJQueryIntoView($c); } else { - if ($js) appendBlocks($js.parent(), $svg); - else appendBlocks($c, $svg); + if ($svg) appendPy($svg.parent(), $py, woptions); + else appendPy($c, $py, woptions); - scrollJQueryIntoView($svg); + scrollJQueryIntoView($py); } }) - $menu.append($svgBtn); - } - - function appendJsButton() { - if (!$js) return; - if (woptions.showJs) - appendJs($c, $js, woptions); - else { - const $jsBtn = snippetBtn("JavaScript", JS_ICON).click(() => { - pxt.tickEvent("docs.btn", { button: "js" }); - if ($c.find('.js')[0]) { - $c.find('.js').remove(); - scrollJQueryIntoView($c); - } else { - if ($svg) appendJs($svg.parent(), $js, woptions); - else appendJs($c, $js, woptions); - - scrollJQueryIntoView($js); - } - }) - $menu.append($jsBtn); - } - } - - function appendPyButton() { - if (!$py) return; - if (woptions.showPy) { - appendPy($c, $py, woptions); - } else { - const $pyBtn = snippetBtn("Python", PY_ICON).click(() => { - pxt.tickEvent("docs.btn", { button: "py" }); - if ($c.find('.py')[0]) { - $c.find('.py').remove(); - scrollJQueryIntoView($c); - } else { - if ($svg) appendPy($svg.parent(), $py, woptions); - else appendPy($c, $py, woptions); - - scrollJQueryIntoView($py); - } - }) - $menu.append($pyBtn); - } + $menu.append($pyBtn); } + } - function scrollJQueryIntoView($toScrollTo: JQuery) { - $toScrollTo[0]?.scrollIntoView({ - behavior: "smooth", - block: "center" - }); - } + function scrollJQueryIntoView($toScrollTo: JQuery) { + $toScrollTo[0]?.scrollIntoView({ + behavior: "smooth", + block: "center" + }); } +} + +let renderQueue: { + el: JQuery; + source: string; + options: pxt.blocks.BlocksRenderOptions; + render: (container: JQuery, r: DecompileResult) => void; +}[] = []; +function consumeRenderQueueAsync(): Promise { + const existingFilters: pxt.Map = {}; + return consumeNext() + .then(() => { + Blockly.Workspace.getAll().forEach(el => el.dispose()); + pxt.blocks.cleanRenderingWorkspace(); + }); - let renderQueue: { - el: JQuery; - source: string; - options: blocks.BlocksRenderOptions; - render: (container: JQuery, r: pxt.runner.DecompileResult) => void; - }[] = []; - function consumeRenderQueueAsync(): Promise { - const existingFilters: Map = {}; - return consumeNext() - .then(() => { - Blockly.Workspace.getAll().forEach(el => el.dispose()); - pxt.blocks.cleanRenderingWorkspace(); - }); + function consumeNext(): Promise { + const job = renderQueue.shift(); + if (!job) return Promise.resolve(); // done - function consumeNext(): Promise { - const job = renderQueue.shift(); - if (!job) return Promise.resolve(); // done + const { el, options, render } = job; + return decompileSnippetAsync(el.text(), options) + .then(r => { + const errors = r.compileJS && r.compileJS.diagnostics && r.compileJS.diagnostics.filter(d => d.category == pxtc.DiagnosticCategory.Error); + if (errors && errors.length) { + errors.forEach(diag => pxt.reportError("docs.decompile", "" + diag.messageText, { "code": diag.code + "" })); + } - const { el, options, render } = job; - return pxt.runner.decompileSnippetAsync(el.text(), options) - .then(r => { - const errors = r.compileJS && r.compileJS.diagnostics && r.compileJS.diagnostics.filter(d => d.category == pxtc.DiagnosticCategory.Error); - if (errors && errors.length) { - errors.forEach(diag => pxt.reportError("docs.decompile", "" + diag.messageText, { "code": diag.code + "" })); + // filter out any blockly definitions from the svg that would be duplicates on the page + r.blocksSvg.querySelectorAll("defs *").forEach(el => { + if (existingFilters[el.id]) { + el.remove(); + } else { + existingFilters[el.id] = true; } - - // filter out any blockly definitions from the svg that would be duplicates on the page - r.blocksSvg.querySelectorAll("defs *").forEach(el => { - if (existingFilters[el.id]) { - el.remove(); - } else { - existingFilters[el.id] = true; - } - }); - render(el, r); - }, e => { - pxt.reportException(e); - el.append($('
').addClass("ui segment warning").text(e.message)); - }).finally(() => { - el.removeClass("lang-shadow"); - return consumeNext(); }); - } - } - - function renderNextSnippetAsync(cls: string, - render: (container: JQuery, r: pxt.runner.DecompileResult) => void, - options?: pxt.blocks.BlocksRenderOptions): Promise { - if (!cls) return Promise.resolve(); - - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); - - if (!options.emPixels) options.emPixels = 18; - if (!options.layout) options.layout = pxt.blocks.BlockLayout.Align; - options.splitSvg = true; - - renderQueue.push({ el: $el, source: $el.text(), options, render }); - $el.addClass("lang-shadow"); - $el.removeClass(cls); - return renderNextSnippetAsync(cls, render, options); - } - - function renderSnippetsAsync(options: ClientRenderOptions): Promise { - if (options.tutorial) { - // don't render chrome for tutorials - return renderNextSnippetAsync(options.snippetClass, (c, r) => { - const s = r.blocksSvg; - if (options.snippetReplaceParent) c = c.parent(); - const segment = $('
').append(s); - c.replaceWith(segment); - }, { package: options.package, snippetMode: false, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); - } - - let snippetCount = 0; - return renderNextSnippetAsync(options.snippetClass, (c, r) => { - const s = r.compileBlocks && r.compileBlocks.success ? $(r.blocksSvg as HTMLElement) : undefined; - const p = r.compilePython && r.compilePython.success && r.compilePython.outfiles[pxt.MAIN_PY]; - const js = $('').text(c.text().trim()); - const py = p ? $('').text(p.trim()) : undefined; - if (options.snippetReplaceParent) c = c.parent(); - const compiled = r.compileJS && r.compileJS.success; - // TODO should this use pxt.outputName() and not pxtc.BINARY_HEX - const hex = options.hex && compiled && r.compileJS.outfiles[pxtc.BINARY_HEX] - ? r.compileJS.outfiles[pxtc.BINARY_HEX] : undefined; - const hexname = `${appTarget.nickname || appTarget.id}-${options.hexName || ''}-${snippetCount++}.hex`; - fillWithWidget(options, c, js, py, s, r, { - showEdit: options.showEdit, - run: options.simulator, - hexname: hexname, - hex: hex, + render(el, r); + }, e => { + pxt.reportException(e); + el.append($('
').addClass("ui segment warning").text(e.message)); + }).finally(() => { + el.removeClass("lang-shadow"); + return consumeNext(); }); - }, { package: options.package, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); } +} - function decompileCallInfo(stmt: ts.Statement): pxtc.CallInfo { - if (!stmt || stmt.kind != ts.SyntaxKind.ExpressionStatement) - return null; +function renderNextSnippetAsync(cls: string, + render: (container: JQuery, r: DecompileResult) => void, + options?: pxt.blocks.BlocksRenderOptions): Promise { + if (!cls) return Promise.resolve(); - let estmt = stmt as ts.ExpressionStatement; - if (!estmt.expression || estmt.expression.kind != ts.SyntaxKind.CallExpression) - return null; + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); - let call = estmt.expression as ts.CallExpression; - let info = pxtc.pxtInfo(call).callInfo; + if (!options.emPixels) options.emPixels = 18; + if (!options.layout) options.layout = pxt.blocks.BlockLayout.Align; + options.splitSvg = true; - return info; - } - - function renderSignaturesAsync(options: ClientRenderOptions): Promise { - return renderNextSnippetAsync(options.signatureClass, (c, r) => { - let cjs = r.compileProgram; - if (!cjs) return; - let file = cjs.getSourceFile(pxt.MAIN_TS); - let info = decompileCallInfo(file.statements[0]); - if (!info || !r.apiInfo) return; - const symbolInfo = r.apiInfo.byQName[info.qName]; - if (!symbolInfo) return; - let block = Blockly.Blocks[symbolInfo.attributes.blockId]; - let xml = block?.codeCard?.blocksXml || undefined; - - const blocksHtml = xml ? pxt.blocks.render(xml) : r.compileBlocks?.success ? r.blocksSvg : undefined; - const s = blocksHtml ? $(blocksHtml as HTMLElement) : undefined - let jsSig = ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ false, r.apiInfo) - .split("\n")[1] + ";"; - const js = $('').text(jsSig); - - const pySig = pxt.appTarget?.appTheme?.python && ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ true, r.apiInfo).split("\n")[1]; - const py: JQuery = pySig && $('').text(pySig); - if (options.snippetReplaceParent) c = c.parent(); - // add an html widge that allows to translate the block - if (pxt.Util.isTranslationMode()) { - const trs = $('
'); - trs.append($(`
`)); - if (symbolInfo.attributes.translationId) - trs.append($('
').text(symbolInfo.attributes.translationId)); - if (symbolInfo.attributes.jsDoc) - trs.append($('
').text(symbolInfo.attributes.jsDoc)); - trs.insertAfter(c); - } - fillWithWidget(options, c, js, py, s, r, { showJs: true, showPy: true, hideGutter: true }); - }, { package: options.package, snippetMode: true, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); - } + renderQueue.push({ el: $el, source: $el.text(), options, render }); + $el.addClass("lang-shadow"); + $el.removeClass(cls); + return renderNextSnippetAsync(cls, render, options); +} - function renderBlocksAsync(options: ClientRenderOptions): Promise { - return renderNextSnippetAsync(options.blocksClass, (c, r) => { +function renderSnippetsAsync(options: ClientRenderOptions): Promise { + if (options.tutorial) { + // don't render chrome for tutorials + return renderNextSnippetAsync(options.snippetClass, (c, r) => { const s = r.blocksSvg; if (options.snippetReplaceParent) c = c.parent(); const segment = $('
').append(s); c.replaceWith(segment); - }, { package: options.package, snippetMode: true, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); + }, { package: options.package, snippetMode: false, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); } - function renderStaticPythonAsync(options: ClientRenderOptions): Promise { - // Highlight python snippets if the snippet has compile python - const woptions: WidgetOptions = { - showEdit: !!options.showEdit, - run: !!options.simulator + let snippetCount = 0; + return renderNextSnippetAsync(options.snippetClass, (c, r) => { + const s = r.compileBlocks && r.compileBlocks.success ? $(r.blocksSvg as HTMLElement) : undefined; + const p = r.compilePython && r.compilePython.success && r.compilePython.outfiles[pxt.MAIN_PY]; + const js = $('').text(c.text().trim()); + const py = p ? $('').text(p.trim()) : undefined; + if (options.snippetReplaceParent) c = c.parent(); + const compiled = r.compileJS && r.compileJS.success; + // TODO should this use pxt.outputName() and not pxtc.BINARY_HEX + const hex = options.hex && compiled && r.compileJS.outfiles[pxtc.BINARY_HEX] + ? r.compileJS.outfiles[pxtc.BINARY_HEX] : undefined; + const hexname = `${pxt.appTarget.nickname || pxt.appTarget.id}-${options.hexName || ''}-${snippetCount++}.hex`; + fillWithWidget(options, c, js, py, s, r, { + showEdit: options.showEdit, + run: options.simulator, + hexname: hexname, + hex: hex, + }); + }, { package: options.package, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); +} + +function decompileCallInfo(stmt: ts.Statement): pxtc.CallInfo { + if (!stmt || stmt.kind != ts.SyntaxKind.ExpressionStatement) + return null; + + let estmt = stmt as ts.ExpressionStatement; + if (!estmt.expression || estmt.expression.kind != ts.SyntaxKind.CallExpression) + return null; + + let call = estmt.expression as ts.CallExpression; + let info = pxtc.pxtInfo(call).callInfo; + + return info; +} + +function renderSignaturesAsync(options: ClientRenderOptions): Promise { + return renderNextSnippetAsync(options.signatureClass, (c, r) => { + let cjs = r.compileProgram; + if (!cjs) return; + let file = cjs.getSourceFile(pxt.MAIN_TS); + let info = decompileCallInfo(file.statements[0]); + if (!info || !r.apiInfo) return; + const symbolInfo = r.apiInfo.byQName[info.qName]; + if (!symbolInfo) return; + let block = Blockly.Blocks[symbolInfo.attributes.blockId]; + let xml = block?.codeCard?.blocksXml || undefined; + + const blocksHtml = xml ? pxt.blocks.render(xml) : r.compileBlocks?.success ? r.blocksSvg : undefined; + const s = blocksHtml ? $(blocksHtml as HTMLElement) : undefined + let jsSig = ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ false, r.apiInfo) + .split("\n")[1] + ";"; + const js = $('').text(jsSig); + + const pySig = pxt.appTarget?.appTheme?.python && ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ true, r.apiInfo).split("\n")[1]; + const py: JQuery = pySig && $('').text(pySig); + if (options.snippetReplaceParent) c = c.parent(); + // add an html widge that allows to translate the block + if (pxt.Util.isTranslationMode()) { + const trs = $('
'); + trs.append($(`
`)); + if (symbolInfo.attributes.translationId) + trs.append($('
').text(symbolInfo.attributes.translationId)); + if (symbolInfo.attributes.jsDoc) + trs.append($('
').text(symbolInfo.attributes.jsDoc)); + trs.insertAfter(c); } - return renderNextSnippetAsync(options.staticPythonClass, (c, r) => { - const s = r.compilePython; - if (s && s.success) { - const $js = c.clone().removeClass('lang-shadow').addClass('highlight'); - const $py = $js.clone().addClass('lang-python').text(s.outfiles[pxt.MAIN_PY]); - $js.addClass('lang-typescript'); - highlight($py); - fillWithWidget(options, c.parent(), /* js */ $js, /* py */ $py, /* svg */ undefined, r, woptions); - } - }, { package: options.package, snippetMode: true, assets: options.assetJSON }); + fillWithWidget(options, c, js, py, s, r, { showJs: true, showPy: true, hideGutter: true }); + }, { package: options.package, snippetMode: true, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); +} + +function renderBlocksAsync(options: ClientRenderOptions): Promise { + return renderNextSnippetAsync(options.blocksClass, (c, r) => { + const s = r.blocksSvg; + if (options.snippetReplaceParent) c = c.parent(); + const segment = $('
').append(s); + c.replaceWith(segment); + }, { package: options.package, snippetMode: true, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); +} + +function renderStaticPythonAsync(options: ClientRenderOptions): Promise { + // Highlight python snippets if the snippet has compile python + const woptions: WidgetOptions = { + showEdit: !!options.showEdit, + run: !!options.simulator } - - function renderBlocksXmlAsync(opts: ClientRenderOptions): Promise { - if (!opts.blocksXmlClass) return Promise.resolve(); - const cls = opts.blocksXmlClass; - function renderNextXmlAsync(cls: string, - render: (container: JQuery, r: pxt.runner.DecompileResult) => void, - options?: pxt.blocks.BlocksRenderOptions): Promise { - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); - - if (!options.emPixels) options.emPixels = 18; - options.splitSvg = true; - return pxt.runner.compileBlocksAsync($el.text(), options) - .then((r) => { - try { - render($el, r); - } catch (e) { - pxt.reportException(e) - $el.append($('
').addClass("ui segment warning").text(e.message)); - } - $el.removeClass(cls); - return U.delay(1, renderNextXmlAsync(cls, render, options)); - }) + return renderNextSnippetAsync(options.staticPythonClass, (c, r) => { + const s = r.compilePython; + if (s && s.success) { + const $js = c.clone().removeClass('lang-shadow').addClass('highlight'); + const $py = $js.clone().addClass('lang-python').text(s.outfiles[pxt.MAIN_PY]); + $js.addClass('lang-typescript'); + highlight($py); + fillWithWidget(options, c.parent(), /* js */ $js, /* py */ $py, /* svg */ undefined, r, woptions); } + }, { package: options.package, snippetMode: true, assets: options.assetJSON }); +} + +function renderBlocksXmlAsync(opts: ClientRenderOptions): Promise { + if (!opts.blocksXmlClass) return Promise.resolve(); + const cls = opts.blocksXmlClass; + function renderNextXmlAsync(cls: string, + render: (container: JQuery, r: DecompileResult) => void, + options?: pxt.blocks.BlocksRenderOptions): Promise { + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); - return renderNextXmlAsync(cls, (c, r) => { - const s = r.blocksSvg; - if (opts.snippetReplaceParent) c = c.parent(); - const segment = $('
').append(s); - c.replaceWith(segment); - }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); + if (!options.emPixels) options.emPixels = 18; + options.splitSvg = true; + return compileBlocksAsync($el.text(), options) + .then((r) => { + try { + render($el, r); + } catch (e) { + pxt.reportException(e) + $el.append($('
').addClass("ui segment warning").text(e.message)); + } + $el.removeClass(cls); + return pxt.U.delay(1, renderNextXmlAsync(cls, render, options)); + }) } - function renderDiffBlocksXmlAsync(opts: ClientRenderOptions): Promise { - if (!opts.diffBlocksXmlClass) return Promise.resolve(); - const cls = opts.diffBlocksXmlClass; - function renderNextXmlAsync(cls: string, - render: (container: JQuery, r: pxt.runner.DecompileResult) => void, - options?: pxt.blocks.BlocksRenderOptions): Promise { - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); - - if (!options.emPixels) options.emPixels = 18; - options.splitSvg = true; - - const xml = $el.text().split(/-{10,}/); - const oldXml = xml[0]; - const newXml = xml[1]; - - return pxt.runner.compileBlocksAsync("", options) // force loading blocks - .then(r => { - $el.removeClass(cls); - try { - const diff = pxt.blocks.diffXml(oldXml, newXml); - if (!diff) - $el.text("no changes"); - else { - r.blocksSvg = diff.svg; - render($el, r); - } - } catch (e) { - pxt.reportException(e) - $el.append($('
').addClass("ui segment warning").text(e.message)); - } - return U.delay(1, renderNextXmlAsync(cls, render, options)); - }) - } + return renderNextXmlAsync(cls, (c, r) => { + const s = r.blocksSvg; + if (opts.snippetReplaceParent) c = c.parent(); + const segment = $('
').append(s); + c.replaceWith(segment); + }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); +} + +function renderDiffBlocksXmlAsync(opts: ClientRenderOptions): Promise { + if (!opts.diffBlocksXmlClass) return Promise.resolve(); + const cls = opts.diffBlocksXmlClass; + function renderNextXmlAsync(cls: string, + render: (container: JQuery, r: DecompileResult) => void, + options?: pxt.blocks.BlocksRenderOptions): Promise { + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); - return renderNextXmlAsync(cls, (c, r) => { - const s = r.blocksSvg; - if (opts.snippetReplaceParent) c = c.parent(); - const segment = $('
').append(s); - c.replaceWith(segment); - }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); + if (!options.emPixels) options.emPixels = 18; + options.splitSvg = true; + + const xml = $el.text().split(/-{10,}/); + const oldXml = xml[0]; + const newXml = xml[1]; + + return compileBlocksAsync("", options) // force loading blocks + .then(r => { + $el.removeClass(cls); + try { + const diff = pxt.blocks.diffXml(oldXml, newXml); + if (!diff) + $el.text("no changes"); + else { + r.blocksSvg = diff.svg; + render($el, r); + } + } catch (e) { + pxt.reportException(e) + $el.append($('
').addClass("ui segment warning").text(e.message)); + } + return pxt.U.delay(1, renderNextXmlAsync(cls, render, options)); + }) } + return renderNextXmlAsync(cls, (c, r) => { + const s = r.blocksSvg; + if (opts.snippetReplaceParent) c = c.parent(); + const segment = $('
').append(s); + c.replaceWith(segment); + }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); +} - function renderDiffAsync(opts: ClientRenderOptions): Promise { - if (!opts.diffClass) return Promise.resolve(); - const cls = opts.diffClass; - function renderNextDiffAsync(cls: string): Promise { - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); - const { fileA: oldSrc, fileB: newSrc } = pxt.diff.split($el.text()); +function renderDiffAsync(opts: ClientRenderOptions): Promise { + if (!opts.diffClass) return Promise.resolve(); + const cls = opts.diffClass; + function renderNextDiffAsync(cls: string): Promise { + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); - try { - const diffEl = pxt.diff.render(oldSrc, newSrc, { - hideLineNumbers: true, - hideMarkerLine: true, - hideMarker: true, - hideRemoved: true, - update: true, - ignoreWhitespace: true, - }); - if (opts.snippetReplaceParent) $el = $el.parent(); - const segment = $('
').append(diffEl); - $el.removeClass(cls); - $el.replaceWith(segment); - } catch (e) { - pxt.reportException(e) - $el.append($('
').addClass("ui segment warning").text(e.message)); - } - return U.delay(1, renderNextDiffAsync(cls)); - } + const { fileA: oldSrc, fileB: newSrc } = pxt.diff.split($el.text()); - return renderNextDiffAsync(cls); + try { + const diffEl = pxt.diff.render(oldSrc, newSrc, { + hideLineNumbers: true, + hideMarkerLine: true, + hideMarker: true, + hideRemoved: true, + update: true, + ignoreWhitespace: true, + }); + if (opts.snippetReplaceParent) $el = $el.parent(); + const segment = $('
').append(diffEl); + $el.removeClass(cls); + $el.replaceWith(segment); + } catch (e) { + pxt.reportException(e) + $el.append($('
').addClass("ui segment warning").text(e.message)); + } + return pxt.U.delay(1, renderNextDiffAsync(cls)); } - function renderDiffBlocksAsync(opts: ClientRenderOptions): Promise { - if (!opts.diffBlocksClass) return Promise.resolve(); - const cls = opts.diffBlocksClass; - function renderNextDiffAsync(cls: string): Promise { - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); + return renderNextDiffAsync(cls); +} - const { fileA: oldSrc, fileB: newSrc } = pxt.diff.split($el.text(), { - removeTrailingSemiColumns: true - }); - return U.promiseMapAllSeries([oldSrc, newSrc], src => pxt.runner.decompileSnippetAsync(src, { - generateSourceMap: true - })) - .then(resps => { - try { - const diffBlocks = pxt.blocks.decompiledDiffAsync( - oldSrc, resps[0].compileBlocks, newSrc, resps[1].compileBlocks, { - hideDeletedTopBlocks: true, - hideDeletedBlocks: true - }); - const diffJs = pxt.diff.render(oldSrc, newSrc, { +function renderDiffBlocksAsync(opts: ClientRenderOptions): Promise { + if (!opts.diffBlocksClass) return Promise.resolve(); + const cls = opts.diffBlocksClass; + function renderNextDiffAsync(cls: string): Promise { + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); + + const { fileA: oldSrc, fileB: newSrc } = pxt.diff.split($el.text(), { + removeTrailingSemiColumns: true + }); + return pxt.U.promiseMapAllSeries([oldSrc, newSrc], src => decompileSnippetAsync(src, { + generateSourceMap: true + })) + .then(resps => { + try { + const diffBlocks = pxt.blocks.decompiledDiffAsync( + oldSrc, resps[0].compileBlocks, newSrc, resps[1].compileBlocks, { + hideDeletedTopBlocks: true, + hideDeletedBlocks: true + }); + const diffJs = pxt.diff.render(oldSrc, newSrc, { + hideLineNumbers: true, + hideMarkerLine: true, + hideMarker: true, + hideRemoved: true, + update: true, + ignoreWhitespace: true + }) + let diffPy: HTMLElement; + const [oldPy, newPy] = resps.map(resp => + resp.compilePython + && resp.compilePython.outfiles + && resp.compilePython.outfiles[pxt.MAIN_PY]); + if (oldPy && newPy) { + diffPy = pxt.diff.render(oldPy, newPy, { hideLineNumbers: true, hideMarkerLine: true, hideMarker: true, @@ -664,664 +681,649 @@ namespace pxt.runner { update: true, ignoreWhitespace: true }) - let diffPy: HTMLElement; - const [oldPy, newPy] = resps.map(resp => - resp.compilePython - && resp.compilePython.outfiles - && resp.compilePython.outfiles[pxt.MAIN_PY]); - if (oldPy && newPy) { - diffPy = pxt.diff.render(oldPy, newPy, { - hideLineNumbers: true, - hideMarkerLine: true, - hideMarker: true, - hideRemoved: true, - update: true, - ignoreWhitespace: true - }) - } - fillWithWidget(opts, $el.parent(), $(diffJs), diffPy && $(diffPy), $(diffBlocks.svg as HTMLElement), undefined, { - showEdit: false, - run: false, - hexname: undefined, - hex: undefined - }); - } catch (e) { - pxt.reportException(e) - $el.append($('
').addClass("ui segment warning").text(e.message)); } - return U.delay(1, renderNextDiffAsync(cls)); - }) - } - - return renderNextDiffAsync(cls); - } - - let decompileApiPromise: Promise; - function decompileApiAsync(options: ClientRenderOptions): Promise { - if (!decompileApiPromise) - decompileApiPromise = pxt.runner.decompileSnippetAsync('', options); - return decompileApiPromise; + fillWithWidget(opts, $el.parent(), $(diffJs), diffPy && $(diffPy), $(diffBlocks.svg as HTMLElement), undefined, { + showEdit: false, + run: false, + hexname: undefined, + hex: undefined + }); + } catch (e) { + pxt.reportException(e) + $el.append($('
').addClass("ui segment warning").text(e.message)); + } + return pxt.U.delay(1, renderNextDiffAsync(cls)); + }) } - function renderNamespaces(options: ClientRenderOptions): Promise { - if (pxt.appTarget.id == "core") return Promise.resolve(); - - return decompileApiAsync(options) - .then((r) => { - let res: pxt.Map = {}; - const info = r.compileBlocks.blocksInfo; - info.blocks.forEach(fn => { - const ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0]; - if (!res[ns]) { - const nsn = info.apis.byQName[ns]; - if (nsn && nsn.attributes.color) - res[ns] = nsn.attributes.color; - } - }); - let nsStyleBuffer = ''; - Object.keys(res).forEach(ns => { - const color = res[ns] || '#dddddd'; - nsStyleBuffer += ` + return renderNextDiffAsync(cls); +} + +let decompileApiPromise: Promise; +function decompileApiAsync(options: ClientRenderOptions): Promise { + if (!decompileApiPromise) + decompileApiPromise = decompileSnippetAsync('', options); + return decompileApiPromise; +} + +function renderNamespaces(options: ClientRenderOptions): Promise { + if (pxt.appTarget.id == "core") return Promise.resolve(); + + return decompileApiAsync(options) + .then((r) => { + let res: pxt.Map = {}; + const info = r.compileBlocks.blocksInfo; + info.blocks.forEach(fn => { + const ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0]; + if (!res[ns]) { + const nsn = info.apis.byQName[ns]; + if (nsn && nsn.attributes.color) + res[ns] = nsn.attributes.color; + } + }); + let nsStyleBuffer = ''; + Object.keys(res).forEach(ns => { + const color = res[ns] || '#dddddd'; + nsStyleBuffer += ` span.docs.${ns.toLowerCase()} { background-color: ${color} !important; border-color: ${pxt.toolbox.fadeColor(color, 0.1, false)} !important; } `; - }) - return nsStyleBuffer; }) - .then((nsStyleBuffer) => { - Object.keys(pxt.toolbox.blockColors).forEach((ns) => { - const color = pxt.toolbox.getNamespaceColor(ns); - nsStyleBuffer += ` + return nsStyleBuffer; + }) + .then((nsStyleBuffer) => { + Object.keys(pxt.toolbox.blockColors).forEach((ns) => { + const color = pxt.toolbox.getNamespaceColor(ns); + nsStyleBuffer += ` span.docs.${ns.toLowerCase()} { background-color: ${color} !important; border-color: ${pxt.toolbox.fadeColor(color, 0.1, false)} !important; } `; - }) - return nsStyleBuffer; }) - .then((nsStyleBuffer) => { - // Inject css - let nsStyle = document.createElement('style'); - nsStyle.id = "namespaceColors"; - nsStyle.type = 'text/css'; - let head = document.head || document.getElementsByTagName('head')[0]; - head.appendChild(nsStyle); - nsStyle.appendChild(document.createTextNode(nsStyleBuffer)); - }); - } - - function renderInlineBlocksAsync(options: pxt.blocks.BlocksRenderOptions): Promise { - options = Util.clone(options); - options.emPixels = 18; - options.snippetMode = true; - - const $els = $(`:not(pre) > code`); - let i = 0; - function renderNextAsync(): Promise { - if (i >= $els.length) return Promise.resolve(); - const $el = $($els[i++]); - const text = $el.text(); - const mbtn = /^(\|+)([^\|]+)\|+$/.exec(text); - if (mbtn) { - const mtxt = /^(([^\:\.]*?)[\:\.])?(.*)$/.exec(mbtn[2]); - const ns = mtxt[2] ? mtxt[2].trim().toLowerCase() : ''; - const lev = mbtn[1].length == 1 ? `docs inlinebutton ${ns}` : `docs inlineblock ${ns}`; - const txt = mtxt[3].trim(); - $el.replaceWith($(``).text(U.rlf(txt))); - return renderNextAsync(); - } + return nsStyleBuffer; + }) + .then((nsStyleBuffer) => { + // Inject css + let nsStyle = document.createElement('style'); + nsStyle.id = "namespaceColors"; + nsStyle.type = 'text/css'; + let head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(nsStyle); + nsStyle.appendChild(document.createTextNode(nsStyleBuffer)); + }); +} + +function renderInlineBlocksAsync(options: pxt.blocks.BlocksRenderOptions): Promise { + options = pxt.Util.clone(options); + options.emPixels = 18; + options.snippetMode = true; + + const $els = $(`:not(pre) > code`); + let i = 0; + function renderNextAsync(): Promise { + if (i >= $els.length) return Promise.resolve(); + const $el = $($els[i++]); + const text = $el.text(); + const mbtn = /^(\|+)([^\|]+)\|+$/.exec(text); + if (mbtn) { + const mtxt = /^(([^\:\.]*?)[\:\.])?(.*)$/.exec(mbtn[2]); + const ns = mtxt[2] ? mtxt[2].trim().toLowerCase() : ''; + const lev = mbtn[1].length == 1 ? `docs inlinebutton ${ns}` : `docs inlineblock ${ns}`; + const txt = mtxt[3].trim(); + $el.replaceWith($(``).text(pxt.U.rlf(txt))); + return renderNextAsync(); + } - const m = /^\[(.+)\]$/.exec(text); - if (!m) return renderNextAsync(); - - const code = m[1]; - return pxt.runner.decompileSnippetAsync(code, options) - .then(r => { - if (r.blocksSvg) { - let $newel = $('').append(r.blocksSvg); - const file = r.compileProgram.getSourceFile(pxt.MAIN_TS); - const stmt = file.statements[0]; - const info = decompileCallInfo(stmt); - if (info && r.apiInfo) { - const symbolInfo = r.apiInfo.byQName[info.qName]; - if (symbolInfo && symbolInfo.attributes.help) { - $newel = $(``).attr("href", `/reference/${symbolInfo.attributes.help}`).append($newel); - } + const m = /^\[(.+)\]$/.exec(text); + if (!m) return renderNextAsync(); + + const code = m[1]; + return decompileSnippetAsync(code, options) + .then(r => { + if (r.blocksSvg) { + let $newel = $('').append(r.blocksSvg); + const file = r.compileProgram.getSourceFile(pxt.MAIN_TS); + const stmt = file.statements[0]; + const info = decompileCallInfo(stmt); + if (info && r.apiInfo) { + const symbolInfo = r.apiInfo.byQName[info.qName]; + if (symbolInfo && symbolInfo.attributes.help) { + $newel = $(``).attr("href", `/reference/${symbolInfo.attributes.help}`).append($newel); } - $el.replaceWith($newel); } - return U.delay(1, renderNextAsync()); - }); - } - - return renderNextAsync(); + $el.replaceWith($newel); + } + return pxt.U.delay(1, renderNextAsync()); + }); } - function renderProjectAsync(options: ClientRenderOptions): Promise { - if (!options.projectClass) return Promise.resolve(); + return renderNextAsync(); +} - function render(): Promise { - let $el = $("." + options.projectClass).first(); - let e = $el[0]; - if (!e) return Promise.resolve(); +function doRenderProjectAsync(options: ClientRenderOptions): Promise { + if (!options.projectClass) return Promise.resolve(); - $el.removeClass(options.projectClass); + function render(): Promise { + let $el = $("." + options.projectClass).first(); + let e = $el[0]; + if (!e) return Promise.resolve(); - let id = pxt.Cloud.parseScriptId(e.innerText); - if (id) { - if (options.snippetReplaceParent) { - e = e.parentElement; - // create a new div to host the rendered code - let d = document.createElement("div"); - e.parentElement.insertBefore(d, e); - e.parentElement.removeChild(e); + $el.removeClass(options.projectClass); - e = d; - } - return pxt.runner.renderProjectAsync(e, id) - .then(() => render()); + let id = pxt.Cloud.parseScriptId(e.innerText); + if (id) { + if (options.snippetReplaceParent) { + e = e.parentElement; + // create a new div to host the rendered code + let d = document.createElement("div"); + e.parentElement.insertBefore(d, e); + e.parentElement.removeChild(e); + + e = d; } - else return render(); + return renderProjectAsync(e, id) + .then(() => render()); } - - return render(); + else return render(); } - function renderApisAsync(options: ClientRenderOptions, replaceParent: boolean): Promise { - const cls = options.apisClass; - if (!cls) return Promise.resolve(); - - const apisEl = $('.' + cls); - if (!apisEl.length) return Promise.resolve(); - - return decompileApiAsync(options) - .then((r) => { - const info = r.compileBlocks.blocksInfo; - const symbols = pxt.Util.values(info.apis.byQName) - .filter(symbol => !symbol.attributes.hidden - && !symbol.attributes.deprecated - && !symbol.attributes.blockAliasFor - && !!symbol.attributes.jsDoc - && !!symbol.attributes.block - && !/^__/.test(symbol.name) - ); - apisEl.each((i, e) => { - let c = $(e); - const namespaces = pxt.Util.toDictionary(c.text().split('\n'), n => n); // list of namespace to list apis for. - - const csymbols = symbols.filter(symbol => !!namespaces[symbol.attributes.blockNamespace || symbol.namespace]) - if (!csymbols.length) return; - - csymbols.sort((l, r) => { - // render cards first - const lcard = !l.attributes.blockHidden && Blockly.Blocks[l.attributes.blockId]; - const rcard = !r.attributes.blockHidden && Blockly.Blocks[r.attributes.blockId] - if (!!lcard != !!rcard) return -(lcard ? 1 : 0) + (rcard ? 1 : 0); - - // sort alphabetically - return l.name.localeCompare(r.name); - }) - - const ul = $('
').addClass('ui divided items'); - ul.attr("role", "listbox"); - csymbols.forEach(symbol => addSymbolCardItem(ul, symbol, "item")); - if (replaceParent) c = c.parent(); - c.replaceWith(ul) + return render(); +} + +function renderApisAsync(options: ClientRenderOptions, replaceParent: boolean): Promise { + const cls = options.apisClass; + if (!cls) return Promise.resolve(); + + const apisEl = $('.' + cls); + if (!apisEl.length) return Promise.resolve(); + + return decompileApiAsync(options) + .then((r) => { + const info = r.compileBlocks.blocksInfo; + const symbols = pxt.Util.values(info.apis.byQName) + .filter(symbol => !symbol.attributes.hidden + && !symbol.attributes.deprecated + && !symbol.attributes.blockAliasFor + && !!symbol.attributes.jsDoc + && !!symbol.attributes.block + && !/^__/.test(symbol.name) + ); + apisEl.each((i, e) => { + let c = $(e); + const namespaces = pxt.Util.toDictionary(c.text().split('\n'), n => n); // list of namespace to list apis for. + + const csymbols = symbols.filter(symbol => !!namespaces[symbol.attributes.blockNamespace || symbol.namespace]) + if (!csymbols.length) return; + + csymbols.sort((l, r) => { + // render cards first + const lcard = !l.attributes.blockHidden && Blockly.Blocks[l.attributes.blockId]; + const rcard = !r.attributes.blockHidden && Blockly.Blocks[r.attributes.blockId] + if (!!lcard != !!rcard) return -(lcard ? 1 : 0) + (rcard ? 1 : 0); + + // sort alphabetically + return l.name.localeCompare(r.name); }) - }); - } - - function addCardItem(ul: JQuery, card: pxt.CodeCard) { - if (!card) return; - const mC = /^\/(v\d+)/.exec(card.url); - const mP = /^\/(v\d+)/.exec(window.location.pathname); - const inEditor = /#doc/i.test(window.location.href); - if (card.url && !mC && mP && !inEditor) card.url = `/${mP[1]}/${card.url}`; - ul.append(pxt.docs.codeCard.render(card, { hideHeader: true, shortName: true })); - } - function addSymbolCardItem(ul: JQuery, symbol: pxtc.SymbolInfo, cardStyle?: string) { - const attributes = symbol.attributes; - const block = !attributes.blockHidden && Blockly.Blocks[attributes.blockId]; - const card = block?.codeCard; - if (card) { - const ccard = U.clone(block.codeCard) as pxt.CodeCard; - if (cardStyle) ccard.style = cardStyle; - addCardItem(ul, ccard); - } - else { - // default to text - // no block available here - addCardItem(ul, { - name: symbol.qName, - description: attributes.jsDoc, - url: attributes.help || undefined, - style: cardStyle + const ul = $('
').addClass('ui divided items'); + ul.attr("role", "listbox"); + csymbols.forEach(symbol => addSymbolCardItem(ul, symbol, "item")); + if (replaceParent) c = c.parent(); + c.replaceWith(ul) }) - } + }); +} + +function addCardItem(ul: JQuery, card: pxt.CodeCard) { + if (!card) return; + const mC = /^\/(v\d+)/.exec(card.url); + const mP = /^\/(v\d+)/.exec(window.location.pathname); + const inEditor = /#doc/i.test(window.location.href); + if (card.url && !mC && mP && !inEditor) card.url = `/${mP[1]}/${card.url}`; + ul.append(pxt.docs.codeCard.render(card, { hideHeader: true, shortName: true })); +} + +function addSymbolCardItem(ul: JQuery, symbol: pxtc.SymbolInfo, cardStyle?: string) { + const attributes = symbol.attributes; + const block = !attributes.blockHidden && Blockly.Blocks[attributes.blockId]; + const card = block?.codeCard; + if (card) { + const ccard = pxt.U.clone(block.codeCard) as pxt.CodeCard; + if (cardStyle) ccard.style = cardStyle; + addCardItem(ul, ccard); } - - function renderLinksAsync(options: ClientRenderOptions, cls: string, replaceParent: boolean, ns: boolean): Promise { - return renderNextSnippetAsync(cls, (c, r) => { - const cjs = r.compileProgram; - if (!cjs) return; - const file = cjs.getSourceFile(pxt.MAIN_TS); - const stmts = file.statements.slice(0); - const ul = $('
').addClass('ui cards'); - ul.attr("role", "listbox"); - stmts.forEach(stmt => { - const kind = stmt.kind; - const info = decompileCallInfo(stmt); - if (info && r.apiInfo && r.apiInfo.byQName[info.qName]) { - const symbol = r.apiInfo.byQName[info.qName]; - const attributes = symbol.attributes; - const block = Blockly.Blocks[attributes.blockId]; - if (ns) { - const ii = symbol; - const nsi = r.compileBlocks.blocksInfo.apis.byQName[ii.namespace]; - addCardItem(ul, { - name: nsi.attributes.blockNamespace || nsi.name, - url: nsi.attributes.help || ("reference/" + (nsi.attributes.blockNamespace || nsi.name).toLowerCase()), - description: nsi.attributes.jsDoc, - blocksXml: block && block.codeCard - ? block.codeCard.blocksXml - : attributes.blockId - ? `` - : undefined - }) - } else { - addSymbolCardItem(ul, symbol); + else { + // default to text + // no block available here + addCardItem(ul, { + name: symbol.qName, + description: attributes.jsDoc, + url: attributes.help || undefined, + style: cardStyle + }) + } +} + +function renderLinksAsync(options: ClientRenderOptions, cls: string, replaceParent: boolean, ns: boolean): Promise { + return renderNextSnippetAsync(cls, (c, r) => { + const cjs = r.compileProgram; + if (!cjs) return; + const file = cjs.getSourceFile(pxt.MAIN_TS); + const stmts = file.statements.slice(0); + const ul = $('
').addClass('ui cards'); + ul.attr("role", "listbox"); + stmts.forEach(stmt => { + const kind = stmt.kind; + const info = decompileCallInfo(stmt); + if (info && r.apiInfo && r.apiInfo.byQName[info.qName]) { + const symbol = r.apiInfo.byQName[info.qName]; + const attributes = symbol.attributes; + const block = Blockly.Blocks[attributes.blockId]; + if (ns) { + const ii = symbol; + const nsi = r.compileBlocks.blocksInfo.apis.byQName[ii.namespace]; + addCardItem(ul, { + name: nsi.attributes.blockNamespace || nsi.name, + url: nsi.attributes.help || ("reference/" + (nsi.attributes.blockNamespace || nsi.name).toLowerCase()), + description: nsi.attributes.jsDoc, + blocksXml: block && block.codeCard + ? block.codeCard.blocksXml + : attributes.blockId + ? `` + : undefined + }) + } else { + addSymbolCardItem(ul, symbol); + } + } else + switch (kind) { + case ts.SyntaxKind.ExpressionStatement: { + const es = stmt as ts.ExpressionStatement; + switch (es.expression.kind) { + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + addCardItem(ul, { + name: "Boolean", + url: "blocks/logic/boolean", + description: lf("True or false values"), + blocksXml: 'TRUE' + }); + break; + default: + pxt.debug(`card expr kind: ${es.expression.kind}`); + break; + } + break; } - } else - switch (kind) { - case ts.SyntaxKind.ExpressionStatement: { - const es = stmt as ts.ExpressionStatement; - switch (es.expression.kind) { - case ts.SyntaxKind.TrueKeyword: - case ts.SyntaxKind.FalseKeyword: - addCardItem(ul, { - name: "Boolean", - url: "blocks/logic/boolean", - description: lf("True or false values"), - blocksXml: 'TRUE' - }); - break; - default: - pxt.debug(`card expr kind: ${es.expression.kind}`); - break; - } - break; + case ts.SyntaxKind.IfStatement: + addCardItem(ul, { + name: ns ? "Logic" : "if", + url: "blocks/logic" + (ns ? "" : "/if"), + description: ns ? lf("Logic operators and constants") : lf("Conditional statement"), + blocksXml: '' + }); + break; + case ts.SyntaxKind.WhileStatement: + addCardItem(ul, { + name: ns ? "Loops" : "while", + url: "blocks/loops" + (ns ? "" : "/while"), + description: ns ? lf("Loops and repetition") : lf("Repeat code while a condition is true."), + blocksXml: '' + }); + break; + case ts.SyntaxKind.ForOfStatement: + addCardItem(ul, { + name: ns ? "Loops" : "for of", + url: "blocks/loops" + (ns ? "" : "/for-of"), + description: ns ? lf("Loops and repetition") : lf("Repeat code for each item in a list."), + blocksXml: '' + }); + break; + case ts.SyntaxKind.BreakStatement: + addCardItem(ul, { + name: ns ? "Loops" : "break", + url: "blocks/loops" + (ns ? "" : "/break"), + description: ns ? lf("Loops and repetition") : lf("Break out of the current loop."), + blocksXml: '' + }); + break; + case ts.SyntaxKind.ContinueStatement: + addCardItem(ul, { + name: ns ? "Loops" : "continue", + url: "blocks/loops" + (ns ? "" : "/continue"), + description: ns ? lf("Loops and repetition") : lf("Skip iteration and continue the current loop."), + blocksXml: '' + }); + break; + case ts.SyntaxKind.ForStatement: { + let fs = stmt as ts.ForStatement; + // look for the 'repeat' loop style signature in the condition expression, explicitly: (let i = 0; i < X; i++) + // for loops will have the '<=' conditional. + let forloop = true; + if (fs.condition.getChildCount() == 3) { + forloop = !(fs.condition.getChildAt(0).getText() == "0" || + fs.condition.getChildAt(1).kind == ts.SyntaxKind.LessThanToken); } - case ts.SyntaxKind.IfStatement: - addCardItem(ul, { - name: ns ? "Logic" : "if", - url: "blocks/logic" + (ns ? "" : "/if"), - description: ns ? lf("Logic operators and constants") : lf("Conditional statement"), - blocksXml: '' - }); - break; - case ts.SyntaxKind.WhileStatement: - addCardItem(ul, { - name: ns ? "Loops" : "while", - url: "blocks/loops" + (ns ? "" : "/while"), - description: ns ? lf("Loops and repetition") : lf("Repeat code while a condition is true."), - blocksXml: '' - }); - break; - case ts.SyntaxKind.ForOfStatement: + if (forloop) { addCardItem(ul, { - name: ns ? "Loops" : "for of", - url: "blocks/loops" + (ns ? "" : "/for-of"), - description: ns ? lf("Loops and repetition") : lf("Repeat code for each item in a list."), - blocksXml: '' + name: ns ? "Loops" : "for", + url: "blocks/loops" + (ns ? "" : "/for"), + description: ns ? lf("Loops and repetition") : lf("Repeat code for a given number of times using an index."), + blocksXml: '' }); - break; - case ts.SyntaxKind.BreakStatement: - addCardItem(ul, { - name: ns ? "Loops" : "break", - url: "blocks/loops" + (ns ? "" : "/break"), - description: ns ? lf("Loops and repetition") : lf("Break out of the current loop."), - blocksXml: '' - }); - break; - case ts.SyntaxKind.ContinueStatement: + } else { addCardItem(ul, { - name: ns ? "Loops" : "continue", - url: "blocks/loops" + (ns ? "" : "/continue"), - description: ns ? lf("Loops and repetition") : lf("Skip iteration and continue the current loop."), - blocksXml: '' + name: ns ? "Loops" : "repeat", + url: "blocks/loops" + (ns ? "" : "/repeat"), + description: ns ? lf("Loops and repetition") : lf("Repeat code for a given number of times."), + blocksXml: '' }); - break; - case ts.SyntaxKind.ForStatement: { - let fs = stmt as ts.ForStatement; - // look for the 'repeat' loop style signature in the condition expression, explicitly: (let i = 0; i < X; i++) - // for loops will have the '<=' conditional. - let forloop = true; - if (fs.condition.getChildCount() == 3) { - forloop = !(fs.condition.getChildAt(0).getText() == "0" || - fs.condition.getChildAt(1).kind == ts.SyntaxKind.LessThanToken); - } - if (forloop) { - addCardItem(ul, { - name: ns ? "Loops" : "for", - url: "blocks/loops" + (ns ? "" : "/for"), - description: ns ? lf("Loops and repetition") : lf("Repeat code for a given number of times using an index."), - blocksXml: '' - }); - } else { - addCardItem(ul, { - name: ns ? "Loops" : "repeat", - url: "blocks/loops" + (ns ? "" : "/repeat"), - description: ns ? lf("Loops and repetition") : lf("Repeat code for a given number of times."), - blocksXml: '' - }); - } - break; } - case ts.SyntaxKind.VariableStatement: - addCardItem(ul, { - name: ns ? "Variables" : "variable declaration", - url: "blocks/variables" + (ns ? "" : "/assign"), - description: ns ? lf("Variables") : lf("Assign a value to a named variable."), - blocksXml: '' - }); - break; - default: - pxt.debug(`card kind: ${kind}`) - } - }) - - if (replaceParent) c = c.parent(); - c.replaceWith(ul) - }, { package: options.package, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }) - } - - function fillCodeCardAsync(c: JQuery, cards: pxt.CodeCard[], options: pxt.docs.codeCard.CodeCardRenderOptions): Promise { - if (!cards || cards.length == 0) return Promise.resolve(); - - if (cards.length == 0) { - let cc = pxt.docs.codeCard.render(cards[0], options) - c.replaceWith(cc); - } else { - let cd = document.createElement("div") - cd.className = "ui cards"; - cd.setAttribute("role", "listbox") - cards.forEach(card => { - // patch card url with version if necessary, we don't do this in the editor because that goes through the backend and passes the targetVersion then - const mC = /^\/(v\d+)/.exec(card.url); - const mP = /^\/(v\d+)/.exec(window.location.pathname); - const inEditor = /#doc/i.test(window.location.href); - if (card.url && !mC && mP && !inEditor) card.url = `/${mP[1]}${card.url}`; - const cardEl = pxt.docs.codeCard.render(card, options); - cd.appendChild(cardEl) - // automitcally display package icon for approved packages - if (card.cardType == "package") { - const repoId = pxt.github.parseRepoId((card.url || "").replace(/^\/pkg\//, '')); - if (repoId) { - pxt.packagesConfigAsync() - .then(pkgConfig => { - const status = pxt.github.repoStatus(repoId, pkgConfig); - switch (status) { - case pxt.github.GitRepoStatus.Banned: - cardEl.remove(); break; - case pxt.github.GitRepoStatus.Approved: - // update card info - card.imageUrl = pxt.github.mkRepoIconUrl(repoId); - // inject - cd.insertBefore(pxt.docs.codeCard.render(card, options), cardEl); - cardEl.remove(); - break; - } - }) - .catch(e => { - // swallow - pxt.reportException(e); - pxt.debug(`failed to load repo ${card.url}`) - }) + break; } + case ts.SyntaxKind.VariableStatement: + addCardItem(ul, { + name: ns ? "Variables" : "variable declaration", + url: "blocks/variables" + (ns ? "" : "/assign"), + description: ns ? lf("Variables") : lf("Assign a value to a named variable."), + blocksXml: '' + }); + break; + default: + pxt.debug(`card kind: ${kind}`) } - }); - c.replaceWith(cd); - } + }) - return Promise.resolve(); + if (replaceParent) c = c.parent(); + c.replaceWith(ul) + }, { package: options.package, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }) +} + +function fillCodeCardAsync(c: JQuery, cards: pxt.CodeCard[], options: pxt.docs.codeCard.CodeCardRenderOptions): Promise { + if (!cards || cards.length == 0) return Promise.resolve(); + + if (cards.length == 0) { + let cc = pxt.docs.codeCard.render(cards[0], options) + c.replaceWith(cc); + } else { + let cd = document.createElement("div") + cd.className = "ui cards"; + cd.setAttribute("role", "listbox") + cards.forEach(card => { + // patch card url with version if necessary, we don't do this in the editor because that goes through the backend and passes the targetVersion then + const mC = /^\/(v\d+)/.exec(card.url); + const mP = /^\/(v\d+)/.exec(window.location.pathname); + const inEditor = /#doc/i.test(window.location.href); + if (card.url && !mC && mP && !inEditor) card.url = `/${mP[1]}${card.url}`; + const cardEl = pxt.docs.codeCard.render(card, options); + cd.appendChild(cardEl) + // automitcally display package icon for approved packages + if (card.cardType == "package") { + const repoId = pxt.github.parseRepoId((card.url || "").replace(/^\/pkg\//, '')); + if (repoId) { + pxt.packagesConfigAsync() + .then(pkgConfig => { + const status = pxt.github.repoStatus(repoId, pkgConfig); + switch (status) { + case pxt.github.GitRepoStatus.Banned: + cardEl.remove(); break; + case pxt.github.GitRepoStatus.Approved: + // update card info + card.imageUrl = pxt.github.mkRepoIconUrl(repoId); + // inject + cd.insertBefore(pxt.docs.codeCard.render(card, options), cardEl); + cardEl.remove(); + break; + } + }) + .catch(e => { + // swallow + pxt.reportException(e); + pxt.debug(`failed to load repo ${card.url}`) + }) + } + } + }); + c.replaceWith(cd); } - function renderNextCodeCardAsync(cls: string, options: ClientRenderOptions): Promise { - if (!cls) return Promise.resolve(); - - let $el = $("." + cls).first(); - if (!$el[0]) return Promise.resolve(); + return Promise.resolve(); +} - $el.removeClass(cls); - // try parsing the card as json - const cards = pxt.gallery.parseCodeCardsHtml($el[0]); - if (!cards) { - $el.append($('
').addClass("ui segment warning").text("invalid codecard format")); - } +function renderNextCodeCardAsync(cls: string, options: ClientRenderOptions): Promise { + if (!cls) return Promise.resolve(); - if (options.snippetReplaceParent) $el = $el.parent(); - return fillCodeCardAsync($el, cards, { hideHeader: true }) - .then(() => U.delay(1, renderNextCodeCardAsync(cls, options))); - } + let $el = $("." + cls).first(); + if (!$el[0]) return Promise.resolve(); - function getRunUrl(options: ClientRenderOptions): string { - return options.pxtUrl ? options.pxtUrl + '/--run' : pxt.webConfig && pxt.webConfig.runUrl ? pxt.webConfig.runUrl : '/--run'; + $el.removeClass(cls); + // try parsing the card as json + const cards = pxt.gallery.parseCodeCardsHtml($el[0]); + if (!cards) { + $el.append($('
').addClass("ui segment warning").text("invalid codecard format")); } - function getEditUrl(options: ClientRenderOptions): string { - const url = options.pxtUrl || pxt.appTarget.appTheme.homeUrl; - return (url || "").replace(/\/$/, ''); + if (options.snippetReplaceParent) $el = $el.parent(); + return fillCodeCardAsync($el, cards, { hideHeader: true }) + .then(() => pxt.U.delay(1, renderNextCodeCardAsync(cls, options))); +} + +function getRunUrl(options: ClientRenderOptions): string { + return options.pxtUrl ? options.pxtUrl + '/--run' : pxt.webConfig && pxt.webConfig.runUrl ? pxt.webConfig.runUrl : '/--run'; +} + +function getEditUrl(options: ClientRenderOptions): string { + const url = options.pxtUrl || pxt.appTarget.appTheme.homeUrl; + return (url || "").replace(/\/$/, ''); +} + +function mergeConfig(options: ClientRenderOptions) { + // additional config options + if (!options.packageClass) return; + $('.' + options.packageClass).each((i, c) => { + let $c = $(c); + let name = $c.text().split('\n').map(s => s.replace(/\s*/g, '')).filter(s => !!s).join(','); + options.package = options.package ? `${options.package},${name}` : name; + if (options.snippetReplaceParent) $c = $c.parent(); + $c.remove(); + }); + $('.lang-config').each((i, c) => { + let $c = $(c); + if (options.snippetReplaceParent) $c = $c.parent(); + $c.remove(); + }) +} + +function readAssetJson(options: ClientRenderOptions) { + let assetJson: string; + let tilemapJres: string; + if (options.jresClass) { + $(`.${options.jresClass}`).each((i, c) => { + const $c = $(c); + tilemapJres = $c.text(); + c.parentElement.remove(); + }); } - - function mergeConfig(options: ClientRenderOptions) { - // additional config options - if (!options.packageClass) return; - $('.' + options.packageClass).each((i, c) => { - let $c = $(c); - let name = $c.text().split('\n').map(s => s.replace(/\s*/g, '')).filter(s => !!s).join(','); - options.package = options.package ? `${options.package},${name}` : name; - if (options.snippetReplaceParent) $c = $c.parent(); - $c.remove(); + if (options.assetJSONClass) { + $(`.${options.assetJSONClass}`).each((i, c) => { + const $c = $(c); + assetJson = $c.text(); + c.parentElement.remove(); }); - $('.lang-config').each((i, c) => { - let $c = $(c); - if (options.snippetReplaceParent) $c = $c.parent(); - $c.remove(); - }) } - function readAssetJson(options: ClientRenderOptions) { - let assetJson: string; - let tilemapJres: string; - if (options.jresClass) { - $(`.${options.jresClass}`).each((i, c) => { - const $c = $(c); - tilemapJres = $c.text(); - c.parentElement.remove(); - }); - } - if (options.assetJSONClass) { - $(`.${options.assetJSONClass}`).each((i, c) => { - const $c = $(c); - assetJson = $c.text(); - c.parentElement.remove(); - }); - } - - options.assetJSON = mergeAssetJson(assetJson, tilemapJres); + options.assetJSON = mergeAssetJson(assetJson, tilemapJres); - function mergeAssetJson(assetJSON: string, tilemapJres: string) { - if (!assetJSON && !tilemapJres) return undefined; - const mergedJson = pxt.tutorial.parseAssetJson(assetJSON) || {}; - if (tilemapJres) { - const parsedTmapJres = JSON.parse(tilemapJres); - mergedJson[pxt.TILEMAP_JRES] = JSON.stringify(parsedTmapJres); - mergedJson[pxt.TILEMAP_CODE] = pxt.emitTilemapsFromJRes(parsedTmapJres); - } - return mergedJson; + function mergeAssetJson(assetJSON: string, tilemapJres: string) { + if (!assetJSON && !tilemapJres) return undefined; + const mergedJson = pxt.tutorial.parseAssetJson(assetJSON) || {}; + if (tilemapJres) { + const parsedTmapJres = JSON.parse(tilemapJres); + mergedJson[pxt.TILEMAP_JRES] = JSON.stringify(parsedTmapJres); + mergedJson[pxt.TILEMAP_CODE] = pxt.emitTilemapsFromJRes(parsedTmapJres); } + return mergedJson; } - - function renderDirectPython(options?: ClientRenderOptions) { - // Highlight python snippets written with the ```python - // language tag (as opposed to the ```spy tag, see renderStaticPythonAsync for that) - const woptions: WidgetOptions = { - showEdit: !!options.showEdit, - run: !!options.simulator - } - - function render(e: HTMLElement, ignored: boolean) { - if (typeof hljs !== "undefined") { - $(e).text($(e).text().replace(/^\s*\r?\n/, '')) - hljs.highlightBlock(e) - highlightLine($(e)); - } - const opts = pxt.U.clone(woptions); - if (ignored) { - opts.run = false; - opts.showEdit = false; - } - fillWithWidget(options, $(e).parent(), $(e), /* py */ undefined, /* JQuery */ undefined, /* decompileResult */ undefined, opts); - } - - $('code.lang-python').each((i, e) => { - render(e, false); - $(e).removeClass('lang-python'); - }); +} + +function renderDirectPython(options?: ClientRenderOptions) { + // Highlight python snippets written with the ```python + // language tag (as opposed to the ```spy tag, see renderStaticPythonAsync for that) + const woptions: WidgetOptions = { + showEdit: !!options.showEdit, + run: !!options.simulator } - function renderTypeScript(options?: ClientRenderOptions) { - const woptions: WidgetOptions = { - showEdit: !!options.showEdit, - run: !!options.simulator + function render(e: HTMLElement, ignored: boolean) { + if (typeof hljs !== "undefined") { + $(e).text($(e).text().replace(/^\s*\r?\n/, '')) + hljs.highlightBlock(e) + highlightLine($(e)); } - - function render(e: HTMLElement, ignored: boolean) { - if (typeof hljs !== "undefined") { - $(e).text($(e).text().replace(/^\s*\r?\n/, '')) - hljs.highlightBlock(e) - highlightLine($(e)); - } - const opts = pxt.U.clone(woptions); - if (ignored) { - opts.run = false; - opts.showEdit = false; - } - fillWithWidget(options, $(e).parent(), $(e), /* py */ undefined, /* JQuery */ undefined, /* decompileResult */ undefined, opts); + const opts = pxt.U.clone(woptions); + if (ignored) { + opts.run = false; + opts.showEdit = false; } - - $('code.lang-typescript').each((i, e) => { - render(e, false); - $(e).removeClass('lang-typescript'); - }); - $('code.lang-typescript-ignore').each((i, e) => { - $(e).removeClass('lang-typescript-ignore'); - $(e).addClass('lang-typescript'); - render(e, true); - $(e).removeClass('lang-typescript'); - }); - $('code.lang-typescript-invalid').each((i, e) => { - $(e).removeClass('lang-typescript-invalid'); - $(e).addClass('lang-typescript'); - render(e, true); - $(e).removeClass('lang-typescript'); - $(e).parent('div').addClass('invalid'); - $(e).parent('div').prepend($("", { "class": "icon ban" })); - $(e).addClass('invalid'); - }); - $('code.lang-typescript-valid').each((i, e) => { - $(e).removeClass('lang-typescript-valid'); - $(e).addClass('lang-typescript'); - render(e, true); - $(e).removeClass('lang-typescript'); - $(e).parent('div').addClass('valid'); - $(e).parent('div').prepend($("", { "class": "icon check" })); - $(e).addClass('valid'); - }); + fillWithWidget(options, $(e).parent(), $(e), /* py */ undefined, /* JQuery */ undefined, /* decompileResult */ undefined, opts); } - function renderGhost(options: ClientRenderOptions) { - let c = $('code.lang-ghost'); - if (options.snippetReplaceParent) - c = c.parent(); - c.remove(); + $('code.lang-python').each((i, e) => { + render(e, false); + $(e).removeClass('lang-python'); + }); +} + +function renderTypeScript(options?: ClientRenderOptions) { + const woptions: WidgetOptions = { + showEdit: !!options.showEdit, + run: !!options.simulator } - function renderBlockConfig(options: ClientRenderOptions) { - function render(scope: "local" | "global") { - $(`code.lang-blockconfig.${scope}`).each((i, c) => { - let $c = $(c); - if (options.snippetReplaceParent) - $c = $c.parent(); - $c.remove(); - }); + function render(e: HTMLElement, ignored: boolean) { + if (typeof hljs !== "undefined") { + $(e).text($(e).text().replace(/^\s*\r?\n/, '')) + hljs.highlightBlock(e) + highlightLine($(e)); } - render("local"); - render("global"); + const opts = pxt.U.clone(woptions); + if (ignored) { + opts.run = false; + opts.showEdit = false; + } + fillWithWidget(options, $(e).parent(), $(e), /* py */ undefined, /* JQuery */ undefined, /* decompileResult */ undefined, opts); } - function renderSims(options: ClientRenderOptions) { - if (!options.simulatorClass) return; - // simulators - $('.' + options.simulatorClass).each((i, c) => { + $('code.lang-typescript').each((i, e) => { + render(e, false); + $(e).removeClass('lang-typescript'); + }); + $('code.lang-typescript-ignore').each((i, e) => { + $(e).removeClass('lang-typescript-ignore'); + $(e).addClass('lang-typescript'); + render(e, true); + $(e).removeClass('lang-typescript'); + }); + $('code.lang-typescript-invalid').each((i, e) => { + $(e).removeClass('lang-typescript-invalid'); + $(e).addClass('lang-typescript'); + render(e, true); + $(e).removeClass('lang-typescript'); + $(e).parent('div').addClass('invalid'); + $(e).parent('div').prepend($("", { "class": "icon ban" })); + $(e).addClass('invalid'); + }); + $('code.lang-typescript-valid').each((i, e) => { + $(e).removeClass('lang-typescript-valid'); + $(e).addClass('lang-typescript'); + render(e, true); + $(e).removeClass('lang-typescript'); + $(e).parent('div').addClass('valid'); + $(e).parent('div').prepend($("", { "class": "icon check" })); + $(e).addClass('valid'); + }); +} + +function renderGhost(options: ClientRenderOptions) { + let c = $('code.lang-ghost'); + if (options.snippetReplaceParent) + c = c.parent(); + c.remove(); +} + +function renderBlockConfig(options: ClientRenderOptions) { + function render(scope: "local" | "global") { + $(`code.lang-blockconfig.${scope}`).each((i, c) => { let $c = $(c); - let padding = '81.97%'; - if (pxt.appTarget.simulator) padding = (100 / pxt.appTarget.simulator.aspectRatio) + '%'; - let $sim = $(`
+ if (options.snippetReplaceParent) + $c = $c.parent(); + $c.remove(); + }); + } + render("local"); + render("global"); +} + +function renderSims(options: ClientRenderOptions) { + if (!options.simulatorClass) return; + // simulators + $('.' + options.simulatorClass).each((i, c) => { + let $c = $(c); + let padding = '81.97%'; + if (pxt.appTarget.simulator) padding = (100 / pxt.appTarget.simulator.aspectRatio) + '%'; + let $sim = $(`
`) - const deps = options.package ? "&deps=" + encodeURIComponent(options.package) : ""; - - const url = getRunUrl(options) + "#nofooter=1" + deps; - const data = encodeURIComponent($c.text().trim()); - const $simIFrame = $sim.find("iframe"); - $simIFrame.attr("src", url); - $simIFrame.attr("data-code", data); - if (options.assetJSON) { - $simIFrame.attr("data-assets", JSON.stringify(options.assetJSON)); - } - if (options.snippetReplaceParent) $c = $c.parent(); - $c.replaceWith($sim); - }); - } - - export function renderAsync(options?: ClientRenderOptions): Promise { - pxt.analytics.enable(pxt.Util.userLanguage()); - if (!options) options = defaultClientRenderOptions(); - if (options.pxtUrl) options.pxtUrl = options.pxtUrl.replace(/\/$/, ''); - if (options.showEdit) options.showEdit = !pxt.BrowserUtils.isIFrame(); - - mergeConfig(options); - readAssetJson(options); - - renderQueue = []; - renderGhost(options); - renderBlockConfig(options); - renderSims(options); - renderTypeScript(options); - renderDirectPython(options); - return Promise.resolve() - .then(() => renderNextCodeCardAsync(options.codeCardClass, options)) - .then(() => renderNamespaces(options)) - .then(() => renderInlineBlocksAsync(options)) - .then(() => renderLinksAsync(options, options.linksClass, options.snippetReplaceParent, false)) - .then(() => renderLinksAsync(options, options.namespacesClass, options.snippetReplaceParent, true)) - .then(() => renderApisAsync(options, options.snippetReplaceParent)) - .then(() => renderSignaturesAsync(options)) - .then(() => renderSnippetsAsync(options)) - .then(() => renderBlocksAsync(options)) - .then(() => renderBlocksXmlAsync(options)) - .then(() => renderDiffBlocksXmlAsync(options)) - .then(() => renderDiffBlocksAsync(options)) - .then(() => renderDiffAsync(options)) - .then(() => renderStaticPythonAsync(options)) - .then(() => renderProjectAsync(options)) - .then(() => consumeRenderQueueAsync()) - } + const deps = options.package ? "&deps=" + encodeURIComponent(options.package) : ""; + + const url = getRunUrl(options) + "#nofooter=1" + deps; + const data = encodeURIComponent($c.text().trim()); + const $simIFrame = $sim.find("iframe"); + $simIFrame.attr("src", url); + $simIFrame.attr("data-code", data); + if (options.assetJSON) { + $simIFrame.attr("data-assets", JSON.stringify(options.assetJSON)); + } + if (options.snippetReplaceParent) $c = $c.parent(); + $c.replaceWith($sim); + }); +} + +export function renderAsync(options?: ClientRenderOptions): Promise { + pxt.analytics.enable(pxt.Util.userLanguage()); + if (!options) options = defaultClientRenderOptions(); + if (options.pxtUrl) options.pxtUrl = options.pxtUrl.replace(/\/$/, ''); + if (options.showEdit) options.showEdit = !pxt.BrowserUtils.isIFrame(); + + mergeConfig(options); + readAssetJson(options); + + renderQueue = []; + renderGhost(options); + renderBlockConfig(options); + renderSims(options); + renderTypeScript(options); + renderDirectPython(options); + return Promise.resolve() + .then(() => renderNextCodeCardAsync(options.codeCardClass, options)) + .then(() => renderNamespaces(options)) + .then(() => renderInlineBlocksAsync(options)) + .then(() => renderLinksAsync(options, options.linksClass, options.snippetReplaceParent, false)) + .then(() => renderLinksAsync(options, options.namespacesClass, options.snippetReplaceParent, true)) + .then(() => renderApisAsync(options, options.snippetReplaceParent)) + .then(() => renderSignaturesAsync(options)) + .then(() => renderSnippetsAsync(options)) + .then(() => renderBlocksAsync(options)) + .then(() => renderBlocksXmlAsync(options)) + .then(() => renderDiffBlocksXmlAsync(options)) + .then(() => renderDiffBlocksAsync(options)) + .then(() => renderDiffAsync(options)) + .then(() => renderStaticPythonAsync(options)) + .then(() => doRenderProjectAsync(options)) + .then(() => consumeRenderQueueAsync()) } \ No newline at end of file diff --git a/pxtrunner/runner.ts b/pxtrunner/runner.ts index 273c711cd109..cc5b7083313d 100644 --- a/pxtrunner/runner.ts +++ b/pxtrunner/runner.ts @@ -6,911 +6,908 @@ /// /// -namespace pxt.runner { - export interface SimulateOptions { - embedId?: string; - id?: string; - code?: string; - assets?: string; - highContrast?: boolean; - light?: boolean; - fullScreen?: boolean; - dependencies?: string[]; - builtJsInfo?: pxtc.BuiltSimJsInfo; - // single simulator frame, no message simulators - single?: boolean; - mute?: boolean; - hideSimButtons?: boolean; - autofocus?: boolean; - additionalQueryParameters?: string; - debug?: boolean; - mpRole?: "server" | "client"; - } +import { defaultClientRenderOptions, renderAsync } from "./renderer"; + +export interface SimulateOptions { + embedId?: string; + id?: string; + code?: string; + assets?: string; + highContrast?: boolean; + light?: boolean; + fullScreen?: boolean; + dependencies?: string[]; + builtJsInfo?: pxtc.BuiltSimJsInfo; + // single simulator frame, no message simulators + single?: boolean; + mute?: boolean; + hideSimButtons?: boolean; + autofocus?: boolean; + additionalQueryParameters?: string; + debug?: boolean; + mpRole?: "server" | "client"; +} - class EditorPackage { - files: Map = {}; - id: string; +class EditorPackage { + files: pxt.Map = {}; + id: string; - constructor(private ksPkg: pxt.Package, public topPkg: EditorPackage) { - } + constructor(private ksPkg: pxt.Package, public topPkg: EditorPackage) { + } - getKsPkg() { - return this.ksPkg; - } + getKsPkg() { + return this.ksPkg; + } - getPkgId() { - return this.ksPkg ? this.ksPkg.id : this.id; - } + getPkgId() { + return this.ksPkg ? this.ksPkg.id : this.id; + } - isTopLevel() { - return this.ksPkg && this.ksPkg.level == 0; - } + isTopLevel() { + return this.ksPkg && this.ksPkg.level == 0; + } - setFiles(files: Map) { - this.files = files; - } + setFiles(files: pxt.Map) { + this.files = files; + } - getAllFiles() { - return Util.mapMap(this.files, (k, f) => f) - } + getAllFiles() { + return pxt.Util.mapMap(this.files, (k, f) => f) } +} - class Host - implements pxt.Host { +class Host + implements pxt.Host { - readFile(module: pxt.Package, filename: string): string { - let epkg = getEditorPkg(module) - return U.lookup(epkg.files, filename) - } + readFile(module: pxt.Package, filename: string): string { + let epkg = getEditorPkg(module) + return pxt.U.lookup(epkg.files, filename) + } - writeFile(module: pxt.Package, filename: string, contents: string): void { - const epkg = getEditorPkg(module); - epkg.files[filename] = contents; - } + writeFile(module: pxt.Package, filename: string, contents: string): void { + const epkg = getEditorPkg(module); + epkg.files[filename] = contents; + } - getHexInfoAsync(extInfo: pxtc.ExtensionInfo): Promise { - return pxt.hexloader.getHexInfoAsync(this, extInfo) - } + getHexInfoAsync(extInfo: pxtc.ExtensionInfo): Promise { + return pxt.hexloader.getHexInfoAsync(this, extInfo) + } - cacheStoreAsync(id: string, val: string): Promise { - return Promise.resolve() - } + cacheStoreAsync(id: string, val: string): Promise { + return Promise.resolve() + } - cacheGetAsync(id: string): Promise { - return Promise.resolve(null as string) - } + cacheGetAsync(id: string): Promise { + return Promise.resolve(null as string) + } - patchDependencies(cfg: pxt.PackageConfig, name: string, repoId: string): boolean { - if (!repoId) return false; - // check that the same package hasn't been added yet - const repo = pxt.github.parseRepoId(repoId); - if (!repo) return false; - - for (const k of Object.keys(cfg.dependencies)) { - const v = cfg.dependencies[k]; - const kv = pxt.github.parseRepoId(v); - if (kv && repo.fullName == kv.fullName) { - if (pxt.semver.strcmp(repo.tag, kv.tag) < 0) { - // we have a later tag, use this one - cfg.dependencies[k] = repoId; - } - return true; + patchDependencies(cfg: pxt.PackageConfig, name: string, repoId: string): boolean { + if (!repoId) return false; + // check that the same package hasn't been added yet + const repo = pxt.github.parseRepoId(repoId); + if (!repo) return false; + + for (const k of Object.keys(cfg.dependencies)) { + const v = cfg.dependencies[k]; + const kv = pxt.github.parseRepoId(v); + if (kv && repo.fullName == kv.fullName) { + if (pxt.semver.strcmp(repo.tag, kv.tag) < 0) { + // we have a later tag, use this one + cfg.dependencies[k] = repoId; } + return true; } - - return false; } - private githubPackageCache: pxt.Map> = {}; - downloadPackageAsync(pkg: pxt.Package, dependencies?: string[]) { - let proto = pkg.verProtocol() - let cached: pxt.Map = undefined; - // cache resolve github packages - if (proto == "github") - cached = this.githubPackageCache[pkg._verspec]; - let epkg = getEditorPkg(pkg) - - return (cached ? Promise.resolve(cached) : pkg.commonDownloadAsync()) - .then(resp => { - if (resp) { - if (proto == "github" && !cached) - this.githubPackageCache[pkg._verspec] = Util.clone(resp); - epkg.setFiles(resp) - return Promise.resolve() + return false; + } + + private githubPackageCache: pxt.Map> = {}; + downloadPackageAsync(pkg: pxt.Package, dependencies?: string[]) { + let proto = pkg.verProtocol() + let cached: pxt.Map = undefined; + // cache resolve github packages + if (proto == "github") + cached = this.githubPackageCache[pkg._verspec]; + let epkg = getEditorPkg(pkg) + + return (cached ? Promise.resolve(cached) : pkg.commonDownloadAsync()) + .then(resp => { + if (resp) { + if (proto == "github" && !cached) + this.githubPackageCache[pkg._verspec] = pxt.Util.clone(resp); + epkg.setFiles(resp) + return Promise.resolve() + } + if (proto == "empty") { + if (Object.keys(epkg.files).length == 0) { + epkg.setFiles(emptyPrjFiles()) } - if (proto == "empty") { - if (Object.keys(epkg.files).length == 0) { - epkg.setFiles(emptyPrjFiles()) - } - if (dependencies && dependencies.length) { - const files = getEditorPkg(pkg).files; - const cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; - dependencies.forEach((d: string) => { - addPackageToConfig(cfg, d); - }); - files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(cfg); - } - return Promise.resolve() - } else if (proto == "docs") { - let files = emptyPrjFiles(); - let cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; - // load all dependencies - pkg.verArgument().split(',').forEach(d => { - if (!addPackageToConfig(cfg, d)) { - return; - } + if (dependencies && dependencies.length) { + const files = getEditorPkg(pkg).files; + const cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; + dependencies.forEach((d: string) => { + addPackageToConfig(cfg, d); }); - - if (!cfg.yotta) cfg.yotta = {}; - cfg.yotta.ignoreConflicts = true; files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(cfg); - epkg.setFiles(files); - return Promise.resolve(); - } else if (proto == "invalid") { - pxt.log(`skipping invalid pkg ${pkg.id}`); - return Promise.resolve(); - } else { - return Promise.reject(`Cannot download ${pkg.version()}; unknown protocol`) } - }) - } + return Promise.resolve() + } else if (proto == "docs") { + let files = emptyPrjFiles(); + let cfg = JSON.parse(files[pxt.CONFIG_NAME]) as pxt.PackageConfig; + // load all dependencies + pkg.verArgument().split(',').forEach(d => { + if (!addPackageToConfig(cfg, d)) { + return; + } + }); + + if (!cfg.yotta) cfg.yotta = {}; + cfg.yotta.ignoreConflicts = true; + files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(cfg); + epkg.setFiles(files); + return Promise.resolve(); + } else if (proto == "invalid") { + pxt.log(`skipping invalid pkg ${pkg.id}`); + return Promise.resolve(); + } else { + return Promise.reject(`Cannot download ${pkg.version()}; unknown protocol`) + } + }) } +} - export let mainPkg: pxt.MainPackage; - let tilemapProject: TilemapProject; +export let mainPkg: pxt.MainPackage; +let tilemapProject: pxt.TilemapProject; - if (!pxt.react.getTilemapProject) { - pxt.react.getTilemapProject = () => { - if (!tilemapProject) { - tilemapProject = new TilemapProject(); - tilemapProject.loadPackage(mainPkg); - } - - return tilemapProject; +if (!pxt.react.getTilemapProject) { + pxt.react.getTilemapProject = () => { + if (!tilemapProject) { + tilemapProject = new pxt.TilemapProject(); + tilemapProject.loadPackage(mainPkg); } + + return tilemapProject; } +} - function addPackageToConfig(cfg: pxt.PackageConfig, dep: string) { - let m = /^([a-zA-Z0-9_-]+)(=(.+))?$/.exec(dep); - if (m) { - // TODO this line seems bad, patchdependencies is on host not this? - // looks like this should be a method in host - if (m[3] && this && this.patchDependencies(cfg, m[1], m[3])) - return false; - cfg.dependencies[m[1]] = m[3] || "*" - } else - console.warn(`unknown package syntax ${dep}`) - return true; - } +function addPackageToConfig(cfg: pxt.PackageConfig, dep: string) { + let m = /^([a-zA-Z0-9_-]+)(=(.+))?$/.exec(dep); + if (m) { + cfg.dependencies[m[1]] = m[3] || "*" + } else + console.warn(`unknown package syntax ${dep}`) + return true; +} - function getEditorPkg(p: pxt.Package) { - let r: EditorPackage = (p as any)._editorPkg - if (r) return r - let top: EditorPackage = null - if (p != mainPkg) - top = getEditorPkg(mainPkg) - let newOne = new EditorPackage(p, top) - if (p == mainPkg) - newOne.topPkg = newOne; - (p as any)._editorPkg = newOne - return newOne - } +function getEditorPkg(p: pxt.Package) { + let r: EditorPackage = (p as any)._editorPkg + if (r) return r + let top: EditorPackage = null + if (p != mainPkg) + top = getEditorPkg(mainPkg) + let newOne = new EditorPackage(p, top) + if (p == mainPkg) + newOne.topPkg = newOne; + (p as any)._editorPkg = newOne + return newOne +} - function emptyPrjFiles() { - let p = appTarget.tsprj - let files = U.clone(p.files) - files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(p.config); - files[pxt.MAIN_BLOCKS] = ""; - return files - } +function emptyPrjFiles() { + let p = pxt.appTarget.tsprj + let files = pxt.U.clone(p.files) + files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(p.config); + files[pxt.MAIN_BLOCKS] = ""; + return files +} - function patchSemantic() { - if ($ && $.fn && ($.fn as any).embed && ($.fn as any).embed.settings && ($.fn as any).embed.settings.sources && ($.fn as any).embed.settings.sources.youtube) { - ($.fn as any).embed.settings.sources.youtube.url = '//www.youtube.com/embed/{id}?rel=0' - } +function patchSemantic() { + if ($ && $.fn && ($.fn as any).embed && ($.fn as any).embed.settings && ($.fn as any).embed.settings.sources && ($.fn as any).embed.settings.sources.youtube) { + ($.fn as any).embed.settings.sources.youtube.url = '//www.youtube.com/embed/{id}?rel=0' } +} - function initInnerAsync() { - pxt.setAppTarget((window as any).pxtTargetBundle) - pxt.analytics.enable(pxt.Util.userLanguage()); - Util.assert(!!pxt.appTarget); - - const href = window.location.href; - let force = false; - let lang: string = undefined; - if (/[&?]translate=1/.test(href) && !pxt.BrowserUtils.isIE()) { - lang = ts.pxtc.Util.TRANSLATION_LOCALE; - force = true; +function initInnerAsync() { + pxt.setAppTarget((window as any).pxtTargetBundle) + pxt.analytics.enable(pxt.Util.userLanguage()); + pxt.Util.assert(!!pxt.appTarget); + + const href = window.location.href; + let force = false; + let lang: string = undefined; + if (/[&?]translate=1/.test(href) && !pxt.BrowserUtils.isIE()) { + lang = ts.pxtc.Util.TRANSLATION_LOCALE; + force = true; + pxt.Util.enableLiveLocalizationUpdates(); + } else { + const cookieValue = /PXT_LANG=(.*?)(?:;|$)/.exec(document.cookie); + const mlang = /(live)?(force)?lang=([a-z]{2,}(-[A-Z]+)?)/i.exec(href); + lang = mlang ? mlang[3] : (cookieValue && cookieValue[1] || pxt.appTarget.appTheme.defaultLocale || (navigator as any).userLanguage || navigator.language); + + const defLocale = pxt.appTarget.appTheme.defaultLocale; + const langLowerCase = lang?.toLocaleLowerCase(); + const localDevServe = pxt.BrowserUtils.isLocalHostDev() + && (!langLowerCase || (defLocale + ? defLocale.toLocaleLowerCase() === langLowerCase + : "en" === langLowerCase || "en-us" === langLowerCase)); + const serveLocal = pxt.BrowserUtils.isPxtElectron() || localDevServe; + const liveTranslationsDisabled = serveLocal || pxt.appTarget.appTheme.disableLiveTranslations; + if (!liveTranslationsDisabled || !!mlang?.[1]) { pxt.Util.enableLiveLocalizationUpdates(); - } else { - const cookieValue = /PXT_LANG=(.*?)(?:;|$)/.exec(document.cookie); - const mlang = /(live)?(force)?lang=([a-z]{2,}(-[A-Z]+)?)/i.exec(href); - lang = mlang ? mlang[3] : (cookieValue && cookieValue[1] || pxt.appTarget.appTheme.defaultLocale || (navigator as any).userLanguage || navigator.language); - - const defLocale = pxt.appTarget.appTheme.defaultLocale; - const langLowerCase = lang?.toLocaleLowerCase(); - const localDevServe = pxt.BrowserUtils.isLocalHostDev() - && (!langLowerCase || (defLocale - ? defLocale.toLocaleLowerCase() === langLowerCase - : "en" === langLowerCase || "en-us" === langLowerCase)); - const serveLocal = pxt.BrowserUtils.isPxtElectron() || localDevServe; - const liveTranslationsDisabled = serveLocal || pxt.appTarget.appTheme.disableLiveTranslations; - if (!liveTranslationsDisabled || !!mlang?.[1]) { - pxt.Util.enableLiveLocalizationUpdates(); - } - force = !!mlang && !!mlang[2]; } - const versions = pxt.appTarget.versions; - - patchSemantic(); - const cfg = pxt.webConfig - return Util.updateLocalizationAsync({ - targetId: pxt.appTarget.id, - baseUrl: cfg.commitCdnUrl, - code: lang, - pxtBranch: versions ? versions.pxtCrowdinBranch : "", - targetBranch: versions ? versions.targetCrowdinBranch : "", - force: force, - }) - .then(() => initHost()) + force = !!mlang && !!mlang[2]; } + const versions = pxt.appTarget.versions; + + patchSemantic(); + const cfg = pxt.webConfig + return pxt.Util.updateLocalizationAsync({ + targetId: pxt.appTarget.id, + baseUrl: cfg.commitCdnUrl, + code: lang, + pxtBranch: versions ? versions.pxtCrowdinBranch : "", + targetBranch: versions ? versions.targetCrowdinBranch : "", + force: force, + }) + .then(() => initHost()) +} - export function initHost() { - mainPkg = new pxt.MainPackage(new Host()); - } +export function initHost() { + mainPkg = new pxt.MainPackage(new Host()); +} - export function initFooter(footer: HTMLElement, shareId?: string) { - if (!footer) return; - - let theme = pxt.appTarget.appTheme; - let body = $('body'); - let $footer = $(footer) - let footera = $('').attr('href', theme.homeUrl) - .attr('target', '_blank'); - $footer.append(footera); - if (theme.organizationLogo) - footera.append($('').attr('src', Util.toDataUri(theme.organizationLogo))); - else footera.append(lf("powered by {0}", theme.title)); - - body.mouseenter(ev => $footer.fadeOut()); - body.mouseleave(ev => $footer.fadeIn()); - } +export function initFooter(footer: HTMLElement, shareId?: string) { + if (!footer) return; + + let theme = pxt.appTarget.appTheme; + let body = $('body'); + let $footer = $(footer) + let footera = $('').attr('href', theme.homeUrl) + .attr('target', '_blank'); + $footer.append(footera); + if (theme.organizationLogo) + footera.append($('').attr('src', pxt.Util.toDataUri(theme.organizationLogo))); + else footera.append(lf("powered by {0}", theme.title)); + + body.mouseenter(ev => $footer.fadeOut()); + body.mouseleave(ev => $footer.fadeIn()); +} - export function showError(msg: string) { - console.error(msg) - } +export function showError(msg: string) { + console.error(msg) +} - let previousMainPackage: pxt.MainPackage = undefined; - function loadPackageAsync(id: string, code?: string, dependencies?: string[]) { - const verspec = id ? /\w+:\w+/.test(id) ? id : "pub:" + id : "empty:tsprj"; - let host: pxt.Host; - let downloadPackagePromise: Promise; - let installPromise: Promise; - if (previousMainPackage && previousMainPackage._verspec == verspec) { - mainPkg = previousMainPackage; - host = mainPkg.host(); - downloadPackagePromise = Promise.resolve(); - installPromise = Promise.resolve(); - } else { - host = mainPkg.host(); - mainPkg = new pxt.MainPackage(host) - mainPkg._verspec = id ? /\w+:\w+/.test(id) ? id : "pub:" + id : "empty:tsprj" - downloadPackagePromise = host.downloadPackageAsync(mainPkg, dependencies); - installPromise = mainPkg.installAllAsync() - // cache previous package - previousMainPackage = mainPkg; - } +let previousMainPackage: pxt.MainPackage = undefined; +function loadPackageAsync(id: string, code?: string, dependencies?: string[]) { + const verspec = id ? /\w+:\w+/.test(id) ? id : "pub:" + id : "empty:tsprj"; + let host: pxt.Host; + let downloadPackagePromise: Promise; + let installPromise: Promise; + if (previousMainPackage && previousMainPackage._verspec == verspec) { + mainPkg = previousMainPackage; + host = mainPkg.host(); + downloadPackagePromise = Promise.resolve(); + installPromise = Promise.resolve(); + } else { + host = mainPkg.host(); + mainPkg = new pxt.MainPackage(host) + mainPkg._verspec = id ? /\w+:\w+/.test(id) ? id : "pub:" + id : "empty:tsprj" + downloadPackagePromise = host.downloadPackageAsync(mainPkg, dependencies); + installPromise = mainPkg.installAllAsync() + // cache previous package + previousMainPackage = mainPkg; + } - return downloadPackagePromise - .then(() => host.readFile(mainPkg, pxt.CONFIG_NAME)) - .then(str => { - if (!str) return Promise.resolve() - return installPromise.then(() => { - if (code) { - //Set the custom code if provided for docs. - let epkg = getEditorPkg(mainPkg); - epkg.files[pxt.MAIN_TS] = code; - //set the custom doc name from the URL. - let cfg = JSON.parse(epkg.files[pxt.CONFIG_NAME]) as pxt.PackageConfig; - cfg.name = window.location.href.split('/').pop().split(/[?#]/)[0];; - epkg.files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(cfg); - - //Propgate the change to main package - mainPkg.config.name = cfg.name; - if (mainPkg.config.files.indexOf(pxt.MAIN_BLOCKS) == -1) { - mainPkg.config.files.push(pxt.MAIN_BLOCKS); - } + return downloadPackagePromise + .then(() => host.readFile(mainPkg, pxt.CONFIG_NAME)) + .then(str => { + if (!str) return Promise.resolve() + return installPromise.then(() => { + if (code) { + //Set the custom code if provided for docs. + let epkg = getEditorPkg(mainPkg); + epkg.files[pxt.MAIN_TS] = code; + //set the custom doc name from the URL. + let cfg = JSON.parse(epkg.files[pxt.CONFIG_NAME]) as pxt.PackageConfig; + cfg.name = window.location.href.split('/').pop().split(/[?#]/)[0];; + epkg.files[pxt.CONFIG_NAME] = pxt.Package.stringifyConfig(cfg); + + //Propgate the change to main package + mainPkg.config.name = cfg.name; + if (mainPkg.config.files.indexOf(pxt.MAIN_BLOCKS) == -1) { + mainPkg.config.files.push(pxt.MAIN_BLOCKS); } - }).catch(e => { - showError(lf("Cannot load extension: {0}", e.message)) + } + }).catch(e => { + showError(lf("Cannot load extension: {0}", e.message)) + }) + }); +} + +function getCompileOptionsAsync(hex?: boolean) { + let trg = mainPkg.getTargetOptions() + trg.isNative = !!hex + trg.hasHex = !!hex + return mainPkg.getCompileOptionsAsync(trg) +} + +function compileAsync(hex: boolean, updateOptions?: (ops: pxtc.CompileOptions) => void) { + return getCompileOptionsAsync(hex) + .then(opts => { + if (updateOptions) updateOptions(opts); + let resp = pxtc.compile(opts) + if (resp.diagnostics && resp.diagnostics.length > 0) { + resp.diagnostics.forEach(diag => { + console.error(diag.messageText) }) - }); - } + } + return resp + }) +} - function getCompileOptionsAsync(hex?: boolean) { - let trg = mainPkg.getTargetOptions() - trg.isNative = !!hex - trg.hasHex = !!hex - return mainPkg.getCompileOptionsAsync(trg) - } +export function generateHexFileAsync(options: SimulateOptions): Promise { + return loadPackageAsync(options.id) + .then(() => compileAsync(true, opts => { + if (options.code) opts.fileSystem[pxt.MAIN_TS] = options.code; + })) + .then(resp => { + if (resp.diagnostics && resp.diagnostics.length > 0) { + console.error("Diagnostics", resp.diagnostics) + } + return resp.outfiles[pxtc.BINARY_HEX]; + }); +} - function compileAsync(hex: boolean, updateOptions?: (ops: pxtc.CompileOptions) => void) { - return getCompileOptionsAsync(hex) - .then(opts => { - if (updateOptions) updateOptions(opts); - let resp = pxtc.compile(opts) - if (resp.diagnostics && resp.diagnostics.length > 0) { - resp.diagnostics.forEach(diag => { - console.error(diag.messageText) - }) - } - return resp - }) - } +export function generateVMFileAsync(options: SimulateOptions): Promise { + pxt.setHwVariant("vm") + return loadPackageAsync(options.id) + .then(() => compileAsync(true, opts => { + if (options.code) opts.fileSystem[pxt.MAIN_TS] = options.code; + })) + .then(resp => { + console.log(resp) + return resp + }) +} - export function generateHexFileAsync(options: SimulateOptions): Promise { - return loadPackageAsync(options.id) - .then(() => compileAsync(true, opts => { - if (options.code) opts.fileSystem[pxt.MAIN_TS] = options.code; - })) - .then(resp => { - if (resp.diagnostics && resp.diagnostics.length > 0) { - console.error("Diagnostics", resp.diagnostics) - } - return resp.outfiles[pxtc.BINARY_HEX]; - }); - } +export async function simulateAsync(container: HTMLElement, simOptions: SimulateOptions): Promise { + const builtSimJS = simOptions.builtJsInfo || await fetchSimJsInfo(simOptions) || await buildSimJsInfo(simOptions); + const { js } = builtSimJS; - export function generateVMFileAsync(options: SimulateOptions): Promise { - pxt.setHwVariant("vm") - return loadPackageAsync(options.id) - .then(() => compileAsync(true, opts => { - if (options.code) opts.fileSystem[pxt.MAIN_TS] = options.code; - })) - .then(resp => { - console.log(resp) - return resp - }) + if (!js) { + console.error("Program failed to compile"); + return undefined; } - export async function simulateAsync(container: HTMLElement, simOptions: SimulateOptions): Promise { - const builtSimJS = simOptions.builtJsInfo || await fetchSimJsInfo(simOptions) || await buildSimJsInfo(simOptions); - const { js } = builtSimJS; - - if (!js) { - console.error("Program failed to compile"); - return undefined; + const runOptions = initDriverAndOptions(container, simOptions, builtSimJS); + simDriver.options.messageSimulators = pxt.appTarget?.simulator?.messageSimulators; + simDriver.options.onSimulatorCommand = msg => { + if (msg.command === "restart") { + runOptions.storedState = getStoredState(simOptions.id) + simDriver.run(js, runOptions); } - - const runOptions = initDriverAndOptions(container, simOptions, builtSimJS); - simDriver.options.messageSimulators = pxt.appTarget?.simulator?.messageSimulators; - simDriver.options.onSimulatorCommand = msg => { - if (msg.command === "restart") { - runOptions.storedState = getStoredState(simOptions.id) - simDriver.run(js, runOptions); - } - if (msg.command == "setstate") { - if (msg.stateKey) { - setStoredState(simOptions.id, msg.stateKey, msg.stateValue) - } + if (msg.command == "setstate") { + if (msg.stateKey) { + setStoredState(simOptions.id, msg.stateKey, msg.stateValue) } - }; - if (builtSimJS.breakpoints && simOptions.debug) { - simDriver.setBreakpoints(builtSimJS.breakpoints); } - simDriver.run(js, runOptions); - return builtSimJS; + }; + if (builtSimJS.breakpoints && simOptions.debug) { + simDriver.setBreakpoints(builtSimJS.breakpoints); } + simDriver.run(js, runOptions); + return builtSimJS; +} - let simDriver: pxsim.SimulatorDriver; - // iff matches and truthy, reuse existing simdriver - let currDriverId: string; - function initDriverAndOptions( - container: HTMLElement, - simOptions: SimulateOptions, - compileInfo?: pxtc.BuiltSimJsInfo - ): pxsim.SimulatorRunOptions { - if (!simDriver || !simOptions.embedId || currDriverId !== simOptions.embedId) { - simDriver = new pxsim.SimulatorDriver(container); - currDriverId = simOptions.embedId; - } else { - simDriver.container = container; - } - const { - fnArgs, - parts, - usedBuiltinParts, - } = compileInfo || {}; - let board = pxt.appTarget.simulator.boardDefinition; - let storedState: Map = getStoredState(simOptions.id) - let runOptions: pxsim.SimulatorRunOptions = { - debug: simOptions.debug, - mute: simOptions.mute, - boardDefinition: board, - parts: parts, - builtinParts: usedBuiltinParts, - fnArgs: fnArgs, - cdnUrl: pxt.webConfig.commitCdnUrl, - localizedStrings: Util.getLocalizedStrings(), - highContrast: simOptions.highContrast, - storedState: storedState, - light: simOptions.light, - single: simOptions.single, - hideSimButtons: simOptions.hideSimButtons, - autofocus: simOptions.autofocus, - queryParameters: simOptions.additionalQueryParameters, - mpRole: simOptions.mpRole, - theme: mainPkg.config?.theme, - }; - if (pxt.appTarget.simulator && !simOptions.fullScreen) - runOptions.aspectRatio = parts.length && pxt.appTarget.simulator.partsAspectRatio - ? pxt.appTarget.simulator.partsAspectRatio - : pxt.appTarget.simulator.aspectRatio; - simDriver.setRunOptions(runOptions); - return runOptions; +let simDriver: pxsim.SimulatorDriver; +// iff matches and truthy, reuse existing simdriver +let currDriverId: string; +function initDriverAndOptions( + container: HTMLElement, + simOptions: SimulateOptions, + compileInfo?: pxtc.BuiltSimJsInfo +): pxsim.SimulatorRunOptions { + if (!simDriver || !simOptions.embedId || currDriverId !== simOptions.embedId) { + simDriver = new pxsim.SimulatorDriver(container); + currDriverId = simOptions.embedId; + } else { + simDriver.container = container; } + const { + fnArgs, + parts, + usedBuiltinParts, + } = compileInfo || {}; + let board = pxt.appTarget.simulator.boardDefinition; + let storedState: pxt.Map = getStoredState(simOptions.id) + let runOptions: pxsim.SimulatorRunOptions = { + debug: simOptions.debug, + mute: simOptions.mute, + boardDefinition: board, + parts: parts, + builtinParts: usedBuiltinParts, + fnArgs: fnArgs, + cdnUrl: pxt.webConfig.commitCdnUrl, + localizedStrings: pxt.Util.getLocalizedStrings(), + highContrast: simOptions.highContrast, + storedState: storedState, + light: simOptions.light, + single: simOptions.single, + hideSimButtons: simOptions.hideSimButtons, + autofocus: simOptions.autofocus, + queryParameters: simOptions.additionalQueryParameters, + mpRole: simOptions.mpRole, + theme: mainPkg.config?.theme, + }; + if (pxt.appTarget.simulator && !simOptions.fullScreen) + runOptions.aspectRatio = parts.length && pxt.appTarget.simulator.partsAspectRatio + ? pxt.appTarget.simulator.partsAspectRatio + : pxt.appTarget.simulator.aspectRatio; + simDriver.setRunOptions(runOptions); + return runOptions; +} - export function preloadSim(container: HTMLElement, simOpts: SimulateOptions) { - initDriverAndOptions(container, simOpts); - simDriver.preload( - pxt.appTarget?.simulator?.aspectRatio || 1, - true /** no auto run **/ - ); - } +export function preloadSim(container: HTMLElement, simOpts: SimulateOptions) { + initDriverAndOptions(container, simOpts); + simDriver.preload( + pxt.appTarget?.simulator?.aspectRatio || 1, + true /** no auto run **/ + ); +} - export function currentDriver() { - return simDriver; - } - export function postSimMessage(msg: pxsim.SimulatorMessage) { - simDriver?.postMessage(msg); - } +export function currentDriver() { + return simDriver; +} +export function postSimMessage(msg: pxsim.SimulatorMessage) { + simDriver?.postMessage(msg); +} - export async function fetchSimJsInfo(simOptions: SimulateOptions): Promise { - try { - const start = Date.now(); - const result = await pxt.Cloud.downloadBuiltSimJsInfoAsync(simOptions.id); - pxt.tickEvent("perfMeasurement", { - durationMs: Date.now() - start, - operation: "fetchSimJsInfo", - }); - return result; - } catch (e) { - // This exception will happen in the majority of cases, so we don't want to log it unless for debugging. - pxt.debug(e.toString()); - return undefined; - } +export async function fetchSimJsInfo(simOptions: SimulateOptions): Promise { + try { + const start = Date.now(); + const result = await pxt.Cloud.downloadBuiltSimJsInfoAsync(simOptions.id); + pxt.tickEvent("perfMeasurement", { + durationMs: Date.now() - start, + operation: "fetchSimJsInfo", + }); + return result; + } catch (e) { + // This exception will happen in the majority of cases, so we don't want to log it unless for debugging. + pxt.debug(e.toString()); + return undefined; } +} - export async function buildSimJsInfo(simOptions: SimulateOptions): Promise { - const start = Date.now(); - await loadPackageAsync(simOptions.id, simOptions.code, simOptions.dependencies); - - let didUpgrade = false; - const currentTargetVersion = pxt.appTarget.versions.target; - let compileResult = await compileAsync(false, opts => { - opts.computeUsedParts = true; - - if (simOptions.debug) - opts.breakpoints = true; - if (simOptions.assets) { - const parsedAssets = JSON.parse(simOptions.assets); - for (const key of Object.keys(parsedAssets)) { - const el = parsedAssets[key]; - opts.fileSystem[key] = el; - if (opts.sourceFiles.indexOf(key) < 0) { - opts.sourceFiles.push(key); - } - if (/\.jres$/.test(key)) { - const parsedJres = JSON.parse(el) - opts.jres = pxt.inflateJRes(parsedJres, opts.jres); - } +export async function buildSimJsInfo(simOptions: SimulateOptions): Promise { + const start = Date.now(); + await loadPackageAsync(simOptions.id, simOptions.code, simOptions.dependencies); + + let didUpgrade = false; + const currentTargetVersion = pxt.appTarget.versions.target; + let compileResult = await compileAsync(false, opts => { + opts.computeUsedParts = true; + + if (simOptions.debug) + opts.breakpoints = true; + if (simOptions.assets) { + const parsedAssets = JSON.parse(simOptions.assets); + for (const key of Object.keys(parsedAssets)) { + const el = parsedAssets[key]; + opts.fileSystem[key] = el; + if (opts.sourceFiles.indexOf(key) < 0) { + opts.sourceFiles.push(key); + } + if (/\.jres$/.test(key)) { + const parsedJres = JSON.parse(el) + opts.jres = pxt.inflateJRes(parsedJres, opts.jres); } } - if (simOptions.code) opts.fileSystem[pxt.MAIN_TS] = simOptions.code; - - // Api info needed for py2ts conversion, if project is shared in Python - if (opts.target.preferredEditor === pxt.PYTHON_PROJECT_NAME) { - opts.target.preferredEditor = pxt.JAVASCRIPT_PROJECT_NAME; - opts.ast = true; - const resp = pxtc.compile(opts); - const apis = getApiInfo(resp.ast, opts); - opts.apisInfo = apis; - opts.target.preferredEditor = pxt.PYTHON_PROJECT_NAME; - } + } + if (simOptions.code) opts.fileSystem[pxt.MAIN_TS] = simOptions.code; + + // Api info needed for py2ts conversion, if project is shared in Python + if (opts.target.preferredEditor === pxt.PYTHON_PROJECT_NAME) { + opts.target.preferredEditor = pxt.JAVASCRIPT_PROJECT_NAME; + opts.ast = true; + const resp = pxtc.compile(opts); + const apis = getApiInfo(resp.ast, opts); + opts.apisInfo = apis; + opts.target.preferredEditor = pxt.PYTHON_PROJECT_NAME; + } - // Apply upgrade rules if necessary - const sharedTargetVersion = mainPkg.config.targetVersions?.target; + // Apply upgrade rules if necessary + const sharedTargetVersion = mainPkg.config.targetVersions?.target; - if (sharedTargetVersion && currentTargetVersion && - pxt.semver.cmp(pxt.semver.parse(sharedTargetVersion), pxt.semver.parse(currentTargetVersion)) < 0) { - for (const fileName of Object.keys(opts.fileSystem)) { - if (!pxt.Util.startsWith(fileName, "pxt_modules") && pxt.Util.endsWith(fileName, ".ts")) { - didUpgrade = true; - opts.fileSystem[fileName] = pxt.patching.patchJavaScript(sharedTargetVersion, opts.fileSystem[fileName]); - } + if (sharedTargetVersion && currentTargetVersion && + pxt.semver.cmp(pxt.semver.parse(sharedTargetVersion), pxt.semver.parse(currentTargetVersion)) < 0) { + for (const fileName of Object.keys(opts.fileSystem)) { + if (!pxt.Util.startsWith(fileName, "pxt_modules") && pxt.Util.endsWith(fileName, ".ts")) { + didUpgrade = true; + opts.fileSystem[fileName] = pxt.patching.patchJavaScript(sharedTargetVersion, opts.fileSystem[fileName]); } } - }); - - if (compileResult.diagnostics?.length > 0 && didUpgrade) { - pxt.log("Compile with upgrade rules failed, trying again with original code"); - compileResult = await compileAsync(false, opts => { - if (simOptions.code) opts.fileSystem[pxt.MAIN_TS] = simOptions.code; - }); } + }); - if (compileResult.diagnostics && compileResult.diagnostics.length > 0) { - console.error("Diagnostics", compileResult.diagnostics); - } - - const res = pxtc.buildSimJsInfo(compileResult); - res.parts = compileResult.usedParts; - pxt.tickEvent("perfMeasurement", { - durationMs: Date.now() - start, - operation: "buildSimJsInfo", + if (compileResult.diagnostics?.length > 0 && didUpgrade) { + pxt.log("Compile with upgrade rules failed, trying again with original code"); + compileResult = await compileAsync(false, opts => { + if (simOptions.code) opts.fileSystem[pxt.MAIN_TS] = simOptions.code; }); - return res; } - function getStoredState(id: string) { - let storedState: Map = {} - try { - let projectStorage = window.localStorage.getItem(id) - if (projectStorage) { - storedState = JSON.parse(projectStorage) - } - } catch (e) { } - return storedState; + if (compileResult.diagnostics && compileResult.diagnostics.length > 0) { + console.error("Diagnostics", compileResult.diagnostics); } - function setStoredState(id: string, key: string, value: any) { - let storedState: Map = getStoredState(id); - if (!id) { - return - } + const res = pxtc.buildSimJsInfo(compileResult); + res.parts = compileResult.usedParts; + pxt.tickEvent("perfMeasurement", { + durationMs: Date.now() - start, + operation: "buildSimJsInfo", + }); + return res; +} - if (value != null) - storedState[key] = value - else - delete storedState[key] +function getStoredState(id: string) { + let storedState: pxt.Map = {} + try { + let projectStorage = window.localStorage.getItem(id) + if (projectStorage) { + storedState = JSON.parse(projectStorage) + } + } catch (e) { } + return storedState; +} - try { - window.localStorage.setItem(id, JSON.stringify(storedState)) - } catch (e) { } +function setStoredState(id: string, key: string, value: any) { + let storedState: pxt.Map = getStoredState(id); + if (!id) { + return } - export enum LanguageMode { - Blocks, - TypeScript, - Python - } + if (value != null) + storedState[key] = value + else + delete storedState[key] - export let editorLanguageMode = LanguageMode.Blocks; + try { + window.localStorage.setItem(id, JSON.stringify(storedState)) + } catch (e) { } +} - export function setEditorContextAsync(mode: LanguageMode, localeInfo: string) { - editorLanguageMode = mode; - if (localeInfo != pxt.Util.localeInfo()) { - const localeLiveRx = /^live-/; - const fetchLive = localeLiveRx.test(localeInfo); - if (fetchLive) { - pxt.Util.enableLiveLocalizationUpdates(); - } +export enum LanguageMode { + Blocks, + TypeScript, + Python +} - return pxt.Util.updateLocalizationAsync({ - targetId: pxt.appTarget.id, - baseUrl: pxt.webConfig.commitCdnUrl, - code: localeInfo.replace(localeLiveRx, ''), - pxtBranch: pxt.appTarget.versions.pxtCrowdinBranch, - targetBranch: pxt.appTarget.versions.targetCrowdinBranch, - }); +export let editorLanguageMode = LanguageMode.Blocks; + +export function setEditorContextAsync(mode: LanguageMode, localeInfo: string) { + editorLanguageMode = mode; + if (localeInfo != pxt.Util.localeInfo()) { + const localeLiveRx = /^live-/; + const fetchLive = localeLiveRx.test(localeInfo); + if (fetchLive) { + pxt.Util.enableLiveLocalizationUpdates(); } - return Promise.resolve(); + return pxt.Util.updateLocalizationAsync({ + targetId: pxt.appTarget.id, + baseUrl: pxt.webConfig.commitCdnUrl, + code: localeInfo.replace(localeLiveRx, ''), + pxtBranch: pxt.appTarget.versions.pxtCrowdinBranch, + targetBranch: pxt.appTarget.versions.targetCrowdinBranch, + }); } - function receiveDocMessage(e: MessageEvent) { - let m = e.data as pxsim.SimulatorMessage; - if (!m) return; - switch (m.type) { - case "fileloaded": - let fm = m as pxsim.SimulatorFileLoadedMessage; - let name = fm.name; - let mode = LanguageMode.Blocks; - if (/\.ts$/i.test(name)) { - mode = LanguageMode.TypeScript; - } - else if (/\.py$/i.test(name)) { - mode = LanguageMode.Python; - } + return Promise.resolve(); +} - setEditorContextAsync(mode, fm.locale); - break; - case "popout": - let mp = /((\/v[0-9+])\/)?[^\/]*#(doc|md):([^&?:]+)/i.exec(window.location.href); - if (mp) { - const docsUrl = pxt.webConfig.docsUrl || '/--docs'; - let verPrefix = mp[2] || ''; - let url = mp[3] == "doc" ? (pxt.webConfig.isStatic ? `/docs${mp[4]}.html` : `${mp[4]}`) : `${docsUrl}?md=${mp[4]}`; - // notify parent iframe that we have completed the popout - if (window.parent) - window.parent.postMessage({ - type: "opendoc", - url: BrowserUtils.urlJoin(verPrefix, url) - }, "*"); - } - break; - case "localtoken": - let dm = m as pxsim.SimulatorDocMessage; - if (dm && dm.localToken) { - Cloud.localToken = dm.localToken; - pendingLocalToken.forEach(p => p()); - pendingLocalToken = []; - } - break; - } +function receiveDocMessage(e: MessageEvent) { + let m = e.data as pxsim.SimulatorMessage; + if (!m) return; + switch (m.type) { + case "fileloaded": + let fm = m as pxsim.SimulatorFileLoadedMessage; + let name = fm.name; + let mode = LanguageMode.Blocks; + if (/\.ts$/i.test(name)) { + mode = LanguageMode.TypeScript; + } + else if (/\.py$/i.test(name)) { + mode = LanguageMode.Python; + } + + setEditorContextAsync(mode, fm.locale); + break; + case "popout": + let mp = /((\/v[0-9+])\/)?[^\/]*#(doc|md):([^&?:]+)/i.exec(window.location.href); + if (mp) { + const docsUrl = pxt.webConfig.docsUrl || '/--docs'; + let verPrefix = mp[2] || ''; + let url = mp[3] == "doc" ? (pxt.webConfig.isStatic ? `/docs${mp[4]}.html` : `${mp[4]}`) : `${docsUrl}?md=${mp[4]}`; + // notify parent iframe that we have completed the popout + if (window.parent) + window.parent.postMessage({ + type: "opendoc", + url: pxt.BrowserUtils.urlJoin(verPrefix, url) + }, "*"); + } + break; + case "localtoken": + let dm = m as pxsim.SimulatorDocMessage; + if (dm && dm.localToken) { + pxt.Cloud.localToken = dm.localToken; + pendingLocalToken.forEach(p => p()); + pendingLocalToken = []; + } + break; } +} - export function startRenderServer() { - pxt.tickEvent("renderer.ready"); - - const jobQueue: pxsim.RenderBlocksRequestMessage[] = []; - let jobPromise: Promise = undefined; - - function consumeQueue() { - if (jobPromise) return; // other worker already in action - const msg = jobQueue.shift(); - if (!msg) return; // no more work - - const options = (msg.options || {}) as pxt.blocks.BlocksRenderOptions; - options.splitSvg = false; // don't split when requesting rendered images - pxt.tickEvent("renderer.job") - const isXml = /^\s* { - await pxt.BrowserUtils.loadBlocklyAsync(); - const result = isXml - ? await pxt.runner.compileBlocksAsync(msg.code, options) - : await runner.decompileSnippetAsync(msg.code, msg.options); - const blocksSvg = result.blocksSvg as SVGSVGElement; - const width = blocksSvg.viewBox.baseVal.width; - const height = blocksSvg.viewBox.baseVal.height; - const res = blocksSvg - ? await pxt.blocks.layout.blocklyToSvgAsync(blocksSvg, 0, 0, width, height) +export function startRenderServer() { + pxt.tickEvent("renderer.ready"); + + const jobQueue: pxsim.RenderBlocksRequestMessage[] = []; + let jobPromise: Promise = undefined; + + function consumeQueue() { + if (jobPromise) return; // other worker already in action + const msg = jobQueue.shift(); + if (!msg) return; // no more work + + const options = (msg.options || {}) as pxt.blocks.BlocksRenderOptions; + options.splitSvg = false; // don't split when requesting rendered images + pxt.tickEvent("renderer.job") + const isXml = /^\s* { + await pxt.BrowserUtils.loadBlocklyAsync(); + const result = isXml + ? await compileBlocksAsync(msg.code, options) + : await decompileSnippetAsync(msg.code, msg.options); + const blocksSvg = result.blocksSvg as SVGSVGElement; + const width = blocksSvg.viewBox.baseVal.width; + const height = blocksSvg.viewBox.baseVal.height; + const res = blocksSvg + ? await pxt.blocks.layout.blocklyToSvgAsync(blocksSvg, 0, 0, width, height) + : undefined; + // try to render to png + let png: string; + try { + png = res + ? await pxt.BrowserUtils.encodeToPngAsync(res.xml, { width, height }) : undefined; - // try to render to png - let png: string; - try { - png = res - ? await pxt.BrowserUtils.encodeToPngAsync(res.xml, { width, height }) - : undefined; - } catch (e) { - console.warn(e); - } - window.parent.postMessage({ - source: "makecode", - type: "renderblocks", - id: msg.id, - width: res?.width, - height: res?.height, - svg: res?.svg, - uri: png || res?.xml, - css: res?.css - }, "*"); + } catch (e) { + console.warn(e); } - - jobPromise = doWork() - .catch(e => { - window.parent.postMessage({ - source: "makecode", - type: "renderblocks", - id: msg.id, - error: e.message - }, "*"); - }) - .finally(() => { - jobPromise = undefined; - consumeQueue(); - }) + window.parent.postMessage({ + source: "makecode", + type: "renderblocks", + id: msg.id, + width: res?.width, + height: res?.height, + svg: res?.svg, + uri: png || res?.xml, + css: res?.css + }, "*"); } - pxt.editor.initEditorExtensionsAsync() - .then(() => { - // notify parent that render engine is loaded - window.addEventListener("message", function (ev) { - const msg = ev.data as pxsim.RenderBlocksRequestMessage; - if (msg.type == "renderblocks") { - jobQueue.push(msg); - consumeQueue(); - } - }, false); - window.parent.postMessage({ + jobPromise = doWork() + .catch(e => { + window.parent.postMessage({ source: "makecode", - type: "renderready", - versions: pxt.appTarget.versions + type: "renderblocks", + id: msg.id, + error: e.message }, "*"); }) + .finally(() => { + jobPromise = undefined; + consumeQueue(); + }) } - export function startDocsServer(loading: HTMLElement, content: HTMLElement, backButton?: HTMLElement) { - pxt.tickEvent("docrenderer.ready"); + pxt.editor.initEditorExtensionsAsync() + .then(() => { + // notify parent that render engine is loaded + window.addEventListener("message", function (ev) { + const msg = ev.data as pxsim.RenderBlocksRequestMessage; + if (msg.type == "renderblocks") { + jobQueue.push(msg); + consumeQueue(); + } + }, false); + window.parent.postMessage({ + source: "makecode", + type: "renderready", + versions: pxt.appTarget.versions + }, "*"); + }) +} - const history: string[] = []; +export function startDocsServer(loading: HTMLElement, content: HTMLElement, backButton?: HTMLElement) { + pxt.tickEvent("docrenderer.ready"); - if (backButton) { - backButton.addEventListener("click", () => { - goBack(); - }); - setElementDisabled(backButton, true); - } + const history: string[] = []; - function render(doctype: string, src: string) { - pxt.debug(`rendering ${doctype}`); - if (backButton) $(backButton).hide() - $(content).hide() - $(loading).show() + if (backButton) { + backButton.addEventListener("click", () => { + goBack(); + }); + setElementDisabled(backButton, true); + } - U.delay(100) // allow UI to update - .then(() => { - switch (doctype) { - case "print": - const data = window.localStorage["printjob"]; - delete window.localStorage["printjob"]; - return renderProjectFilesAsync(content, JSON.parse(data), undefined, true) - .then(() => pxsim.print(1000)); - case "project": - return renderProjectFilesAsync(content, JSON.parse(src)) - .then(() => pxsim.print(1000)); - case "projectid": - return renderProjectAsync(content, JSON.parse(src)) - .then(() => pxsim.print(1000)); - case "doc": - return renderDocAsync(content, src); - case "book": - return renderBookAsync(content, src); - default: - return renderMarkdownAsync(content, src); - } - }) - .catch(e => { - $(content).html(` - -

${lf("Oops")}

-

${lf("We could not load the documentation, please check your internet connection.")}

- `); - $(content).find('#tryagain').click(() => { - render(doctype, src); - }) - // notify parent iframe that docs weren't loaded - if (window.parent) - window.parent.postMessage({ - type: "docfailed", - docType: doctype, - src: src - }, "*"); - }).finally(() => { - $(loading).hide() - if (backButton) $(backButton).show() - $(content).show() + function render(doctype: string, src: string) { + pxt.debug(`rendering ${doctype}`); + if (backButton) $(backButton).hide() + $(content).hide() + $(loading).show() + + pxt.U.delay(100) // allow UI to update + .then(() => { + switch (doctype) { + case "print": + const data = window.localStorage["printjob"]; + delete window.localStorage["printjob"]; + return renderProjectFilesAsync(content, JSON.parse(data), undefined, true) + .then(() => pxsim.print(1000)); + case "project": + return renderProjectFilesAsync(content, JSON.parse(src)) + .then(() => pxsim.print(1000)); + case "projectid": + return renderProjectAsync(content, JSON.parse(src)) + .then(() => pxsim.print(1000)); + case "doc": + return renderDocAsync(content, src); + case "book": + return renderBookAsync(content, src); + default: + return renderMarkdownAsync(content, src); + } + }) + .catch(e => { + $(content).html(` + +

${lf("Oops")}

+

${lf("We could not load the documentation, please check your internet connection.")}

+ `); + $(content).find('#tryagain').click(() => { + render(doctype, src); }) - .then(() => { }); - } + // notify parent iframe that docs weren't loaded + if (window.parent) + window.parent.postMessage({ + type: "docfailed", + docType: doctype, + src: src + }, "*"); + }).finally(() => { + $(loading).hide() + if (backButton) $(backButton).show() + $(content).show() + }) + .then(() => { }); + } - function pushHistory() { - if (!backButton) return; + function pushHistory() { + if (!backButton) return; - history.push(window.location.hash); - if (history.length > 10) { - history.shift(); - } + history.push(window.location.hash); + if (history.length > 10) { + history.shift(); + } - if (history.length > 1) { - setElementDisabled(backButton, false); - } + if (history.length > 1) { + setElementDisabled(backButton, false); } + } - function goBack() { - if (!backButton) return; - if (history.length > 1) { - // Top is current page - history.pop(); - window.location.hash = history.pop(); - } + function goBack() { + if (!backButton) return; + if (history.length > 1) { + // Top is current page + history.pop(); + window.location.hash = history.pop(); + } - if (history.length <= 1) { - setElementDisabled(backButton, true); - } + if (history.length <= 1) { + setElementDisabled(backButton, true); } + } - function setElementDisabled(el: HTMLElement, disabled: boolean) { - if (disabled) { - pxsim.U.addClass(el, "disabled"); - el.setAttribute("aria-disabled", "true"); - } else { - pxsim.U.removeClass(el, "disabled"); - el.setAttribute("aria-disabled", "false"); - } + function setElementDisabled(el: HTMLElement, disabled: boolean) { + if (disabled) { + pxsim.U.addClass(el, "disabled"); + el.setAttribute("aria-disabled", "true"); + } else { + pxsim.U.removeClass(el, "disabled"); + el.setAttribute("aria-disabled", "false"); } + } - async function renderHashAsync() { - let m = /^#(doc|md|tutorial|book|project|projectid|print):([^&?:]+)(:([^&?:]+):([^&?:]+))?/i.exec(window.location.hash); - if (m) { - pushHistory(); + async function renderHashAsync() { + let m = /^#(doc|md|tutorial|book|project|projectid|print):([^&?:]+)(:([^&?:]+):([^&?:]+))?/i.exec(window.location.hash); + if (m) { + pushHistory(); - if (m[4]) { - let mode = LanguageMode.TypeScript; - if (/^blocks$/i.test(m[4])) { - mode = LanguageMode.Blocks; - } - else if (/^python$/i.test(m[4])) { - mode = LanguageMode.Python; - } - await setEditorContextAsync(mode, m[5]); + if (m[4]) { + let mode = LanguageMode.TypeScript; + if (/^blocks$/i.test(m[4])) { + mode = LanguageMode.Blocks; } - - // navigation occured - render(m[1], decodeURIComponent(m[2])); + else if (/^python$/i.test(m[4])) { + mode = LanguageMode.Python; + } + await setEditorContextAsync(mode, m[5]); } - } - let promise = pxt.editor.initEditorExtensionsAsync(); - promise.then(() => { - window.addEventListener("message", receiveDocMessage, false); - window.addEventListener("hashchange", () => { - renderHashAsync(); - }, false); - - parent.postMessage({ type: "sidedocready" }, "*"); - // delay load doc page to allow simulator to load first - setTimeout(() => renderHashAsync(), 1); - }) + // navigation occured + render(m[1], decodeURIComponent(m[2])); + } } + let promise = pxt.editor.initEditorExtensionsAsync(); + promise.then(() => { + window.addEventListener("message", receiveDocMessage, false); + window.addEventListener("hashchange", () => { + renderHashAsync(); + }, false); + + parent.postMessage({ type: "sidedocready" }, "*"); + + // delay load doc page to allow simulator to load first + setTimeout(() => renderHashAsync(), 1); + }) +} - export function renderProjectAsync(content: HTMLElement, projectid: string): Promise { - return Cloud.privateGetTextAsync(projectid + "/text") - .then(txt => JSON.parse(txt)) - .then(files => renderProjectFilesAsync(content, files, projectid)); - } +export function renderProjectAsync(content: HTMLElement, projectid: string): Promise { + return pxt.Cloud.privateGetTextAsync(projectid + "/text") + .then(txt => JSON.parse(txt)) + .then(files => renderProjectFilesAsync(content, files, projectid)); +} - export function renderProjectFilesAsync(content: HTMLElement, files: Map, projectid: string = null, escapeLinks = false): Promise { - const cfg = (JSON.parse(files[pxt.CONFIG_NAME]) || {}) as PackageConfig; +export function renderProjectFilesAsync(content: HTMLElement, files: pxt.Map, projectid: string = null, escapeLinks = false): Promise { + const cfg = (JSON.parse(files[pxt.CONFIG_NAME]) || {}) as pxt.PackageConfig; - let md = `# ${cfg.name} ${cfg.version ? cfg.version : ''} + let md = `# ${cfg.name} ${cfg.version ? cfg.version : ''} `; - const readme = "README.md"; - if (files[readme]) - md += files[readme].replace(/^#+/, "$0#") + '\n'; // bump all headers down 1 - - cfg.files.filter(f => f != pxt.CONFIG_NAME && f != readme) - .filter(f => matchesLanguageMode(f, editorLanguageMode)) - .forEach(f => { - if (!/^main\.(ts|blocks)$/.test(f)) - md += ` + const readme = "README.md"; + if (files[readme]) + md += files[readme].replace(/^#+/, "$0#") + '\n'; // bump all headers down 1 + + cfg.files.filter(f => f != pxt.CONFIG_NAME && f != readme) + .filter(f => matchesLanguageMode(f, editorLanguageMode)) + .forEach(f => { + if (!/^main\.(ts|blocks)$/.test(f)) + md += ` ## ${f} `; - if (/\.ts$/.test(f)) { - md += `\`\`\`typescript + if (/\.ts$/.test(f)) { + md += `\`\`\`typescript ${files[f]} \`\`\` `; - } else if (/\.blocks?$/.test(f)) { - md += `\`\`\`blocksxml + } else if (/\.blocks?$/.test(f)) { + md += `\`\`\`blocksxml ${files[f]} \`\`\` `; - } else { - md += `\`\`\`${f.substr(f.indexOf('.'))} + } else { + md += `\`\`\`${f.substr(f.indexOf('.'))} ${files[f]} \`\`\` `; - } - }); + } + }); - const deps = cfg && cfg.dependencies && Object.keys(cfg.dependencies).filter(k => k != pxt.appTarget.corepkg); - if (deps && deps.length) { - md += ` + const deps = cfg && cfg.dependencies && Object.keys(cfg.dependencies).filter(k => k != pxt.appTarget.corepkg); + if (deps && deps.length) { + md += ` ## ${lf("Extensions")} #extensions ${deps.map(k => `* ${k}, ${cfg.dependencies[k]}`).join('\n')} @@ -919,424 +916,421 @@ ${deps.map(k => `* ${k}, ${cfg.dependencies[k]}`).join('\n')} ${deps.map(k => `${k}=${cfg.dependencies[k]}`).join('\n')} \`\`\` `; - } + } - if (projectid) { - let linkString = (pxt.appTarget.appTheme.shareUrl || "https://makecode.com/") + projectid; - if (escapeLinks) { - // If printing the link will show up twice if it's an actual link - linkString = "`" + linkString + "`"; - } - md += ` + if (projectid) { + let linkString = (pxt.appTarget.appTheme.shareUrl || "https://makecode.com/") + projectid; + if (escapeLinks) { + // If printing the link will show up twice if it's an actual link + linkString = "`" + linkString + "`"; + } + md += ` ${linkString} `; - } - console.debug(`print md: ${md}`); - const options: RenderMarkdownOptions = { - print: true - } - return renderMarkdownAsync(content, md, options); } - - function matchesLanguageMode(filename: string, mode: LanguageMode) { - switch (mode) { - case LanguageMode.Blocks: - return /\.blocks?$/.test(filename) - case LanguageMode.TypeScript: - return /\.ts?$/.test(filename) - case LanguageMode.Python: - return /\.py?$/.test(filename) - } + console.debug(`print md: ${md}`); + const options: RenderMarkdownOptions = { + print: true } + return renderMarkdownAsync(content, md, options); +} - async function renderDocAsync(content: HTMLElement, docid: string): Promise { - docid = docid.replace(/^\//, ""); - // if it fails on requesting, propagate failed promise - const md = await pxt.Cloud.markdownAsync(docid, undefined, true /** don't suppress exception **/); - try { - // just log exceptions that occur during rendering, - // similar to how normal docs handle them. - await renderMarkdownAsync(content, md, { path: docid }); - } catch (e) { - console.warn(e); - } +function matchesLanguageMode(filename: string, mode: LanguageMode) { + switch (mode) { + case LanguageMode.Blocks: + return /\.blocks?$/.test(filename) + case LanguageMode.TypeScript: + return /\.ts?$/.test(filename) + case LanguageMode.Python: + return /\.py?$/.test(filename) } +} - function renderBookAsync(content: HTMLElement, summaryid: string): Promise { - summaryid = summaryid.replace(/^\//, ""); - pxt.tickEvent('book', { id: summaryid }); - pxt.log(`rendering book from ${summaryid}`) - - // display loader - const $loader = $("#loading").find(".loader"); - $loader.addClass("text").text(lf("Compiling your book (this may take a minute)")); - - // start the work - let toc: TOCMenuEntry[]; - return U.delay(100) - .then(() => pxt.Cloud.markdownAsync(summaryid, undefined, true)) - .then(summary => { - toc = pxt.docs.buildTOC(summary); - pxt.log(`TOC: ${JSON.stringify(toc, null, 2)}`) - const tocsp: TOCMenuEntry[] = []; - pxt.docs.visitTOC(toc, entry => { - if (/^\//.test(entry.path) && !/^\/pkg\//.test(entry.path)) - tocsp.push(entry); - }); - - return U.promisePoolAsync(4, tocsp, async entry => { - try { - const md = await pxt.Cloud.markdownAsync(entry.path, undefined, true); - entry.markdown = md; - } catch (e) { - entry.markdown = `_${entry.path} failed to load._`; - } - }); - }) - .then(pages => { - let md = toc[0].name; - pxt.docs.visitTOC(toc, entry => { - if (entry.markdown) - md += '\n\n' + entry.markdown - }); - return renderMarkdownAsync(content, md); - }) +async function renderDocAsync(content: HTMLElement, docid: string): Promise { + docid = docid.replace(/^\//, ""); + // if it fails on requesting, propagate failed promise + const md = await pxt.Cloud.markdownAsync(docid, undefined, true /** don't suppress exception **/); + try { + // just log exceptions that occur during rendering, + // similar to how normal docs handle them. + await renderMarkdownAsync(content, md, { path: docid }); + } catch (e) { + console.warn(e); } +} + +function renderBookAsync(content: HTMLElement, summaryid: string): Promise { + summaryid = summaryid.replace(/^\//, ""); + pxt.tickEvent('book', { id: summaryid }); + pxt.log(`rendering book from ${summaryid}`) + + // display loader + const $loader = $("#loading").find(".loader"); + $loader.addClass("text").text(lf("Compiling your book (this may take a minute)")); + + // start the work + let toc: pxt.TOCMenuEntry[]; + return pxt.U.delay(100) + .then(() => pxt.Cloud.markdownAsync(summaryid, undefined, true)) + .then(summary => { + toc = pxt.docs.buildTOC(summary); + pxt.log(`TOC: ${JSON.stringify(toc, null, 2)}`) + const tocsp: pxt.TOCMenuEntry[] = []; + pxt.docs.visitTOC(toc, entry => { + if (/^\//.test(entry.path) && !/^\/pkg\//.test(entry.path)) + tocsp.push(entry); + }); + + return pxt.U.promisePoolAsync(4, tocsp, async entry => { + try { + const md = await pxt.Cloud.markdownAsync(entry.path, undefined, true); + entry.markdown = md; + } catch (e) { + entry.markdown = `_${entry.path} failed to load._`; + } + }); + }) + .then(pages => { + let md = toc[0].name; + pxt.docs.visitTOC(toc, entry => { + if (entry.markdown) + md += '\n\n' + entry.markdown + }); + return renderMarkdownAsync(content, md); + }) +} - const template = ` +const template = `
@breadcrumb@ @body@`; - export interface RenderMarkdownOptions { - path?: string; - tutorial?: boolean; - blocksAspectRatio?: number; - print?: boolean; // render for print - } +export interface RenderMarkdownOptions { + path?: string; + tutorial?: boolean; + blocksAspectRatio?: number; + print?: boolean; // render for print +} - export function renderMarkdownAsync(content: HTMLElement, md: string, options: RenderMarkdownOptions = {}): Promise { - const html = pxt.docs.renderMarkdown({ - template: template, - markdown: md, - theme: pxt.appTarget.appTheme - }); - let blocksAspectRatio = options.blocksAspectRatio - || window.innerHeight < window.innerWidth ? 1.62 : 1 / 1.62; - $(content).html(html); - $(content).find('a').attr('target', '_blank'); - const renderOptions = pxt.runner.defaultClientRenderOptions(); - renderOptions.tutorial = !!options.tutorial; - renderOptions.blocksAspectRatio = blocksAspectRatio || renderOptions.blocksAspectRatio; - renderOptions.showJavaScript = editorLanguageMode == LanguageMode.TypeScript; - if (options.print) { - renderOptions.showEdit = false; - renderOptions.simulator = false; - } +export function renderMarkdownAsync(content: HTMLElement, md: string, options: RenderMarkdownOptions = {}): Promise { + const html = pxt.docs.renderMarkdown({ + template: template, + markdown: md, + theme: pxt.appTarget.appTheme + }); + let blocksAspectRatio = options.blocksAspectRatio + || window.innerHeight < window.innerWidth ? 1.62 : 1 / 1.62; + $(content).html(html); + $(content).find('a').attr('target', '_blank'); + const renderOptions = defaultClientRenderOptions(); + renderOptions.tutorial = !!options.tutorial; + renderOptions.blocksAspectRatio = blocksAspectRatio || renderOptions.blocksAspectRatio; + renderOptions.showJavaScript = editorLanguageMode == LanguageMode.TypeScript; + if (options.print) { + renderOptions.showEdit = false; + renderOptions.simulator = false; + } - return pxt.runner.renderAsync(renderOptions).then(() => { - // patch a elements - $(content).find('a[href^="/"]').removeAttr('target').each((i, a) => { - $(a).attr('href', '#doc:' + $(a).attr('href').replace(/^\//, '')); - }); - // enable embeds - ($(content).find('.ui.embed') as any).embed(); + return renderAsync(renderOptions).then(() => { + // patch a elements + $(content).find('a[href^="/"]').removeAttr('target').each((i, a) => { + $(a).attr('href', '#doc:' + $(a).attr('href').replace(/^\//, '')); }); - } + // enable embeds + ($(content).find('.ui.embed') as any).embed(); + }); +} - export interface DecompileResult { - package: pxt.MainPackage; - compileProgram?: ts.Program; - compileJS?: pxtc.CompileResult; - compileBlocks?: pxtc.CompileResult; - compilePython?: pxtc.transpile.TranspileResult; - apiInfo?: pxtc.ApisInfo; - blocksSvg?: Element; - } +export interface DecompileResult { + package: pxt.MainPackage; + compileProgram?: ts.Program; + compileJS?: pxtc.CompileResult; + compileBlocks?: pxtc.CompileResult; + compilePython?: pxtc.transpile.TranspileResult; + apiInfo?: pxtc.ApisInfo; + blocksSvg?: Element; +} - let programCache: ts.Program; - let apiCache: pxt.Map; - - export function decompileSnippetAsync(code: string, options?: blocks.BlocksRenderOptions): Promise { - const { assets, forceCompilation, snippetMode, generateSourceMap } = options || {}; - - // code may be undefined or empty!!! - const packageid = options && options.packageId ? "pub:" + options.packageId : - options && options.package ? "docs:" + options.package - : null; - return loadPackageAsync(packageid, code) - .then(() => getCompileOptionsAsync(appTarget.compile ? appTarget.compile.hasHex : false)) - .then(opts => { - // compile - if (code) - opts.fileSystem[pxt.MAIN_TS] = code; - opts.ast = true - - if (assets) { - for (const key of Object.keys(assets)) { - if (opts.sourceFiles.indexOf(key) < 0) { - opts.sourceFiles.push(key); - } - opts.fileSystem[key] = assets[key]; +let programCache: ts.Program; +let apiCache: pxt.Map; + +export function decompileSnippetAsync(code: string, options?: pxt.blocks.BlocksRenderOptions): Promise { + const { assets, forceCompilation, snippetMode, generateSourceMap } = options || {}; + + // code may be undefined or empty!!! + const packageid = options && options.packageId ? "pub:" + options.packageId : + options && options.package ? "docs:" + options.package + : null; + return loadPackageAsync(packageid, code) + .then(() => getCompileOptionsAsync(pxt.appTarget.compile ? pxt.appTarget.compile.hasHex : false)) + .then(opts => { + // compile + if (code) + opts.fileSystem[pxt.MAIN_TS] = code; + opts.ast = true + + if (assets) { + for (const key of Object.keys(assets)) { + if (opts.sourceFiles.indexOf(key) < 0) { + opts.sourceFiles.push(key); } + opts.fileSystem[key] = assets[key]; } + } - let compileJS: pxtc.CompileResult = undefined; - let program: ts.Program; - if (forceCompilation) { - compileJS = pxtc.compile(opts); - program = compileJS && compileJS.ast; - } else { - program = pxtc.getTSProgram(opts, programCache); - } - programCache = program; - - // decompile to python - let compilePython: pxtc.transpile.TranspileResult = undefined; - if (pxt.appTarget.appTheme.python) { - compilePython = ts.pxtc.transpile.tsToPy(program, pxt.MAIN_TS); - } + let compileJS: pxtc.CompileResult = undefined; + let program: ts.Program; + if (forceCompilation) { + compileJS = pxtc.compile(opts); + program = compileJS && compileJS.ast; + } else { + program = pxtc.getTSProgram(opts, programCache); + } + programCache = program; - // decompile to blocks - let apis = getApiInfo(program, opts); - return ts.pxtc.localizeApisAsync(apis, mainPkg) - .then(() => { - let blocksInfo = pxtc.getBlocksInfo(apis); - pxt.blocks.initializeAndInject(blocksInfo); - const tilemapJres = assets?.[pxt.TILEMAP_JRES]; - const assetsJres = assets?.[pxt.IMAGES_JRES]; - if (tilemapJres || assetsJres) { - tilemapProject = new TilemapProject(); - tilemapProject.loadPackage(mainPkg); - if (tilemapJres) - tilemapProject.loadTilemapJRes(JSON.parse(tilemapJres), true); - if (assetsJres) - tilemapProject.loadAssetsJRes(JSON.parse(assetsJres)) - } - let bresp = pxtc.decompiler.decompileToBlocks( - blocksInfo, - program.getSourceFile(pxt.MAIN_TS), - { - snippetMode, - generateSourceMap - }); - if (bresp.diagnostics && bresp.diagnostics.length > 0) - bresp.diagnostics.forEach(diag => console.error(diag.messageText)); - if (!bresp.success) - return { - package: mainPkg, - compileProgram: program, - compileJS, - compileBlocks: bresp, - apiInfo: apis - }; - pxt.debug(bresp.outfiles[pxt.MAIN_BLOCKS]) - - const blocksSvg = pxt.blocks.render(bresp.outfiles[pxt.MAIN_BLOCKS], options); - - if (tilemapJres || assetsJres) { - tilemapProject = null; - } + // decompile to python + let compilePython: pxtc.transpile.TranspileResult = undefined; + if (pxt.appTarget.appTheme.python) { + compilePython = ts.pxtc.transpile.tsToPy(program, pxt.MAIN_TS); + } + // decompile to blocks + let apis = getApiInfo(program, opts); + return ts.pxtc.localizeApisAsync(apis, mainPkg) + .then(() => { + let blocksInfo = pxtc.getBlocksInfo(apis); + pxt.blocks.initializeAndInject(blocksInfo); + const tilemapJres = assets?.[pxt.TILEMAP_JRES]; + const assetsJres = assets?.[pxt.IMAGES_JRES]; + if (tilemapJres || assetsJres) { + tilemapProject = new pxt.TilemapProject(); + tilemapProject.loadPackage(mainPkg); + if (tilemapJres) + tilemapProject.loadTilemapJRes(JSON.parse(tilemapJres), true); + if (assetsJres) + tilemapProject.loadAssetsJRes(JSON.parse(assetsJres)) + } + let bresp = pxtc.decompiler.decompileToBlocks( + blocksInfo, + program.getSourceFile(pxt.MAIN_TS), + { + snippetMode, + generateSourceMap + }); + if (bresp.diagnostics && bresp.diagnostics.length > 0) + bresp.diagnostics.forEach(diag => console.error(diag.messageText)); + if (!bresp.success) return { package: mainPkg, compileProgram: program, compileJS, compileBlocks: bresp, - compilePython, - apiInfo: apis, - blocksSvg + apiInfo: apis }; - }) - }); - } + pxt.debug(bresp.outfiles[pxt.MAIN_BLOCKS]) - function getApiInfo(program: ts.Program, opts: pxtc.CompileOptions) { - if (!apiCache) apiCache = {}; + const blocksSvg = pxt.blocks.render(bresp.outfiles[pxt.MAIN_BLOCKS], options); - const key = Object.keys(opts.fileSystem).sort().join(";"); + if (tilemapJres || assetsJres) { + tilemapProject = null; + } - if (!apiCache[key]) apiCache[key] = pxtc.getApiInfo(program, opts.jres); + return { + package: mainPkg, + compileProgram: program, + compileJS, + compileBlocks: bresp, + compilePython, + apiInfo: apis, + blocksSvg + }; + }) + }); +} - return apiCache[key]; - } +function getApiInfo(program: ts.Program, opts: pxtc.CompileOptions) { + if (!apiCache) apiCache = {}; - export function compileBlocksAsync(code: string, options?: blocks.BlocksRenderOptions): Promise { - const { assets } = options || {}; - - const packageid = options && options.packageId ? "pub:" + options.packageId : - options && options.package ? "docs:" + options.package - : null; - return loadPackageAsync(packageid, "") - .then(() => getCompileOptionsAsync(appTarget.compile ? appTarget.compile.hasHex : false)) - .then(opts => { - opts.ast = true - if (assets) { - for (const key of Object.keys(assets)) { - if (opts.sourceFiles.indexOf(key) < 0) { - opts.sourceFiles.push(key); - } - opts.fileSystem[key] = assets[key]; - } - } - const resp = pxtc.compile(opts) - const apis = getApiInfo(resp.ast, opts); - return ts.pxtc.localizeApisAsync(apis, mainPkg) - .then(() => { - const blocksInfo = pxtc.getBlocksInfo(apis); - pxt.blocks.initializeAndInject(blocksInfo); - - const tilemapJres = assets?.[pxt.TILEMAP_JRES]; - const assetsJres = assets?.[pxt.IMAGES_JRES]; - if (tilemapJres || assetsJres) { - tilemapProject = new TilemapProject(); - tilemapProject.loadPackage(mainPkg); - if (tilemapJres) - tilemapProject.loadTilemapJRes(JSON.parse(tilemapJres), true); - if (assetsJres) - tilemapProject.loadAssetsJRes(JSON.parse(assetsJres)) - } - const blockSvg = pxt.blocks.render(code, options); + const key = Object.keys(opts.fileSystem).sort().join(";"); - if (tilemapJres || assetsJres) { - tilemapProject = null; - } + if (!apiCache[key]) apiCache[key] = pxtc.getApiInfo(program, opts.jres); - return { - package: mainPkg, - blocksSvg: blockSvg, - apiInfo: apis - }; - }) - }); - } + return apiCache[key]; +} - let pendingLocalToken: (() => void)[] = []; +export function compileBlocksAsync(code: string, options?: pxt.blocks.BlocksRenderOptions): Promise { + const { assets } = options || {}; + + const packageid = options && options.packageId ? "pub:" + options.packageId : + options && options.package ? "docs:" + options.package + : null; + return loadPackageAsync(packageid, "") + .then(() => getCompileOptionsAsync(pxt.appTarget.compile ? pxt.appTarget.compile.hasHex : false)) + .then(opts => { + opts.ast = true + if (assets) { + for (const key of Object.keys(assets)) { + if (opts.sourceFiles.indexOf(key) < 0) { + opts.sourceFiles.push(key); + } + opts.fileSystem[key] = assets[key]; + } + } + const resp = pxtc.compile(opts) + const apis = getApiInfo(resp.ast, opts); + return ts.pxtc.localizeApisAsync(apis, mainPkg) + .then(() => { + const blocksInfo = pxtc.getBlocksInfo(apis); + pxt.blocks.initializeAndInject(blocksInfo); + + const tilemapJres = assets?.[pxt.TILEMAP_JRES]; + const assetsJres = assets?.[pxt.IMAGES_JRES]; + if (tilemapJres || assetsJres) { + tilemapProject = new pxt.TilemapProject(); + tilemapProject.loadPackage(mainPkg); + if (tilemapJres) + tilemapProject.loadTilemapJRes(JSON.parse(tilemapJres), true); + if (assetsJres) + tilemapProject.loadAssetsJRes(JSON.parse(assetsJres)) + } + const blockSvg = pxt.blocks.render(code, options); - function waitForLocalTokenAsync() { - if (pxt.Cloud.localToken) { - return Promise.resolve(); - } - return new Promise((resolve, reject) => { - pendingLocalToken.push(resolve); + if (tilemapJres || assetsJres) { + tilemapProject = null; + } + + return { + package: mainPkg, + blocksSvg: blockSvg, + apiInfo: apis + }; + }) }); - } +} - export let initCallbacks: (() => void)[] = []; - export function init() { - initInnerAsync() - .then(() => { - for (let i = 0; i < initCallbacks.length; ++i) { - initCallbacks[i](); - } - }) - } +let pendingLocalToken: (() => void)[] = []; - function windowLoad() { - let f = (window as any).ksRunnerWhenLoaded - if (f) f(); +function waitForLocalTokenAsync() { + if (pxt.Cloud.localToken) { + return Promise.resolve(); } + return new Promise((resolve, reject) => { + pendingLocalToken.push(resolve); + }); +} + +let initCallbacks: (() => void)[] = []; - windowLoad(); +export function setInitCallbacks(callbacks: (() => void)[]) { + initCallbacks = callbacks; } + +export function init() { + initInnerAsync() + .then(() => { + for (let i = 0; i < initCallbacks.length; ++i) { + initCallbacks[i](); + } + }) +} \ No newline at end of file diff --git a/pxtrunner/tsconfig.json b/pxtrunner/tsconfig.json index 211f99a4c4bb..f040b892878b 100644 --- a/pxtrunner/tsconfig.json +++ b/pxtrunner/tsconfig.json @@ -3,10 +3,18 @@ "target": "es2017", "noImplicitAny": true, "noImplicitReturns": true, - "declaration": true, - "outFile": "../built/pxtrunner.js", + "noImplicitThis": true, + "module": "commonjs", + "outDir": "../built/pxtrunner", + "rootDir": "..", "newLine": "LF", "sourceMap": false, + "moduleResolution": "node", + "isolatedModules": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": false, + "preserveConstEnums": true, "lib": [ "dom", "dom.iterable", @@ -15,9 +23,13 @@ "ES2018.Promise" ], "types": [ - "jquery", - "highlight.js" + "resize-observer-browser", + "jquery" ], - "incremental": true - } -} \ No newline at end of file + "incremental": false, + "skipLibCheck": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/webapp/public/embed.js b/webapp/public/embed.js index c1b19654bf0f..b130da08aa47 100644 --- a/webapp/public/embed.js +++ b/webapp/public/embed.js @@ -27,7 +27,7 @@ window.ksRunnerWhenLoaded = function() { pxt.docs.requireHighlightJs = function() { return hljs; } pxt.setupWebConfig(pxtConfig || window.pxtWebConfig) - pxt.runner.initCallbacks = pxtCallbacks + pxt.runner.setInitCallbacks(pxtCallbacks) pxtCallbacks.push(function() { pxtCallbacks = null })