UNPKG

pxt-core

Version:

Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors

1,043 lines (1,042 loc) • 120 kB
var pxt; (function (pxt) { var runner; (function (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 */ function startDebuggerAsync(container) { const debugRunner = new DebugRunner(container); debugRunner.start(); } runner.startDebuggerAsync = startDebuggerAsync; /** * 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) */ class DebugRunner { constructor(container) { this.container = container; this.pkgLoaded = false; this.intervalRunning = false; } 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); } this.session = new pxsim.SimDebugSession(this.container); this.session.start(this); } initializeWebsocket() { if (!pxt.BrowserUtils.isLocalHost() || !pxt.Cloud.localToken) return; pxt.debug('initializing debug pipe'); this.ws = new WebSocket('ws://localhost:3234/' + pxt.Cloud.localToken + '/simdebug'); this.ws.onopen = ev => { pxt.debug('debug: socket opened'); }; this.ws.onclose = ev => { pxt.debug('debug: socket closed'); if (this.closeListener) { this.closeListener(); } this.session.stopSimulator(); this.ws = undefined; }; this.ws.onerror = ev => { pxt.debug('debug: socket closed due to error'); if (this.errorListener) { this.errorListener(ev.type); } this.session.stopSimulator(); this.ws = undefined; }; this.ws.onmessage = ev => { let message; 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); } else { // Intercept the launch configuration and notify the server-side debug runner if (message.type === "request" && message.command === "launch") { this.sendRunnerMessage("configure", { projectDir: message.arguments.projectDir }); } this.dataListener(message); } } }; } send(msg) { this.ws.send(msg); } onData(cb) { this.dataListener = cb; } onError(cb) { this.errorListener = cb; } onClose(cb) { this.closeListener = cb; } close() { if (this.session) { this.session.stopSimulator(true); } if (this.intervalRunning) { clearInterval(this.intervalId); this.intervalId = undefined; } if (this.ws) { this.ws.close(); } } handleRunnerMessage(msg) { switch (msg.subtype) { case "ready": this.sendRunnerMessage("ready"); break; case "runcode": this.runCode(msg); break; } } runCode(msg) { const breakpoints = []; // 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); } sendRunnerMessage(subtype, msg = {}) { msg["subtype"] = subtype; msg["type"] = "runner"; this.send(JSON.stringify(msg)); } } DebugRunner.RETRY_MS = 2500; runner.DebugRunner = DebugRunner; })(runner = pxt.runner || (pxt.runner = {})); })(pxt || (pxt = {})); var pxt; (function (pxt) { var runner; (function (runner) { const JS_ICON = "icon xicon js"; const PY_ICON = "icon xicon python"; const BLOCKS_ICON = "icon xicon blocks"; function defaultClientRenderOptions() { const renderOptions = { 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; } runner.defaultClientRenderOptions = defaultClientRenderOptions; function highlight($js) { if (typeof hljs !== "undefined") { if ($js.hasClass("highlight")) { hljs.highlightBlock($js[0]); } else { $js.find('code.highlight').each(function (i, block) { hljs.highlightBlock(block); }); } highlightLine($js); } } function highlightLine($js) { // apply line highlighting $js.find("span.hljs-comment:contains(@highlight)") .each((i, el) => { try { highlightLineElement(el); } catch (e) { pxt.reportException(e); } }); } function highlightLineElement(el) { 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.textContent; let inewline = text.indexOf('\n'); if (inewline < 0) return; // there should have been a new line here // split the next node next.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.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.textContent = text.substring(inewline + 1); break; } } else { span.appendChild(next); next = nextnext; } } // insert back $(span).insertAfter($el); // remove line entry $el.remove(); } function appendBlocks($parent, $svg) { $parent.append($(`<div class="ui content blocks"/>`).append($svg)); } function appendJs($parent, $js, woptions) { $parent.append($(`<div class="ui content js"><div class="subheading"><i class="ui icon xicon js"></i>JavaScript</div></div>`).append($js)); highlight($js); } function appendPy($parent, $py, woptions) { $parent.append($(`<div class="ui content py"><div class="subheading"><i class="ui icon xicon python"></i>Python</div></div>`).append($py)); highlight($py); } function snippetBtn(label, icon) { const $btn = $(`<a class="item" role="button" tabindex="0"><i role="presentation" aria-hidden="true"></i><span class="ui desktop only"></span></a>`); $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) { 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, $container, $js, $py, $svg, decompileResult, woptions = {}) { let $h = $('<div class="ui bottom attached tabular icon small compact menu hideprint">' + ' <div class="right icon menu"></div></div>'); let $c = $('<div class="ui top attached segment codewidget"></div>'); 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); } 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]); } } 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); } 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 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 = $(`<div class="ui card sim"><div class="ui content"><div style="position:relative;height:0;padding-bottom:${padding};overflow:hidden;"><iframe style="position:absolute;top:0;left:0;width:100%;height:100%;" src="${url}" data-code="${data}" ${assets} allowfullscreen="allowfullscreen" sandbox="allow-popups allow-forms allow-scripts allow-same-origin" frameborder="0"></iframe></div></div></div>`); $c.append($embed); scrollJQueryIntoView($embed); } }); $menu.append($runBtn); } 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); } let r = $(`<div class=codesnippet></div>`); // 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 { if ($js) appendBlocks($js.parent(), $svg); else appendBlocks($c, $svg); scrollJQueryIntoView($svg); } }); $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); } } function scrollJQueryIntoView($toScrollTo) { var _a; (_a = $toScrollTo[0]) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth", block: "center" }); } } let renderQueue = []; function consumeRenderQueueAsync() { const existingFilters = {}; return consumeNext() .then(() => { Blockly.Workspace.getAll().forEach(el => el.dispose()); pxt.blocks.cleanRenderingWorkspace(); }); function consumeNext() { const job = renderQueue.shift(); if (!job) return Promise.resolve(); // done 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; } }); render(el, r); }, e => { pxt.reportException(e); el.append($('<div/>').addClass("ui segment warning").text(e.message)); }).finally(() => { el.removeClass("lang-shadow"); return consumeNext(); }); } } function renderNextSnippetAsync(cls, render, options) { 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) { 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 = $('<div class="ui segment codewidget"/>').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) : undefined; const p = r.compilePython && r.compilePython.success && r.compilePython.outfiles[pxt.MAIN_PY]; const js = $('<code class="lang-typescript highlight"/>').text(c.text().trim()); const py = p ? $('<code class="lang-python highlight"/>').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) { if (!stmt || stmt.kind != ts.SyntaxKind.ExpressionStatement) return null; let estmt = stmt; if (!estmt.expression || estmt.expression.kind != ts.SyntaxKind.CallExpression) return null; let call = estmt.expression; let info = pxtc.pxtInfo(call).callInfo; return info; } function renderSignaturesAsync(options) { return renderNextSnippetAsync(options.signatureClass, (c, r) => { var _a, _b, _c, _d; 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 = ((_a = block === null || block === void 0 ? void 0 : block.codeCard) === null || _a === void 0 ? void 0 : _a.blocksXml) || undefined; const blocksHtml = xml ? pxt.blocks.render(xml) : ((_b = r.compileBlocks) === null || _b === void 0 ? void 0 : _b.success) ? r.blocksSvg : undefined; const s = blocksHtml ? $(blocksHtml) : undefined; let jsSig = ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ false, r.apiInfo) .split("\n")[1] + ";"; const js = $('<code class="lang-typescript highlight"/>').text(jsSig); const pySig = ((_d = (_c = pxt.appTarget) === null || _c === void 0 ? void 0 : _c.appTheme) === null || _d === void 0 ? void 0 : _d.python) && ts.pxtc.service.displayStringForSymbol(symbolInfo, /** python **/ true, r.apiInfo).split("\n")[1]; const py = pySig && $('<code class="lang-python highlight"/>').text(pySig); if (options.snippetReplaceParent) c = c.parent(); // add an html widge that allows to translate the block if (pxt.Util.isTranslationMode()) { const trs = $('<div class="ui segment" />'); trs.append($(`<div class="ui header"><i class="ui xicon globe"></i></div>`)); if (symbolInfo.attributes.translationId) trs.append($('<div class="ui message">').text(symbolInfo.attributes.translationId)); if (symbolInfo.attributes.jsDoc) trs.append($('<div class="ui message">').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 }); } function renderBlocksAsync(options) { return renderNextSnippetAsync(options.blocksClass, (c, r) => { const s = r.blocksSvg; if (options.snippetReplaceParent) c = c.parent(); const segment = $('<div class="ui segment codewidget"/>').append(s); c.replaceWith(segment); }, { package: options.package, snippetMode: true, aspectRatio: options.blocksAspectRatio, assets: options.assetJSON }); } function renderStaticPythonAsync(options) { // Highlight python snippets if the snippet has compile python const woptions = { showEdit: !!options.showEdit, run: !!options.simulator }; 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) { if (!opts.blocksXmlClass) return Promise.resolve(); const cls = opts.blocksXmlClass; function renderNextXmlAsync(cls, render, options) { 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($('<div/>').addClass("ui segment warning").text(e.message)); } $el.removeClass(cls); 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 = $('<div class="ui segment codewidget"/>').append(s); c.replaceWith(segment); }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); } function renderDiffBlocksXmlAsync(opts) { if (!opts.diffBlocksXmlClass) return Promise.resolve(); const cls = opts.diffBlocksXmlClass; function renderNextXmlAsync(cls, render, options) { 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($('<div/>').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 = $('<div class="ui segment codewidget"/>').append(s); c.replaceWith(segment); }, { package: opts.package, snippetMode: true, aspectRatio: opts.blocksAspectRatio, assets: opts.assetJSON }); } function renderDiffAsync(opts) { if (!opts.diffClass) return Promise.resolve(); const cls = opts.diffClass; function renderNextDiffAsync(cls) { let $el = $("." + cls).first(); if (!$el[0]) return Promise.resolve(); const { fileA: oldSrc, fileB: newSrc } = pxt.diff.split($el.text()); 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 = $('<div class="ui segment codewidget"/>').append(diffEl); $el.removeClass(cls); $el.replaceWith(segment); } catch (e) { pxt.reportException(e); $el.append($('<div/>').addClass("ui segment warning").text(e.message)); } return pxt.U.delay(1, renderNextDiffAsync(cls)); } return renderNextDiffAsync(cls); } function renderDiffBlocksAsync(opts) { if (!opts.diffBlocksClass) return Promise.resolve(); const cls = opts.diffBlocksClass; function renderNextDiffAsync(cls) { 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 => 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, { hideLineNumbers: true, hideMarkerLine: true, hideMarker: true, hideRemoved: true, update: true, ignoreWhitespace: true }); let diffPy; 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), undefined, { showEdit: false, run: false, hexname: undefined, hex: undefined }); } catch (e) { pxt.reportException(e); $el.append($('<div/>').addClass("ui segment warning").text(e.message)); } return pxt.U.delay(1, renderNextDiffAsync(cls)); }); } return renderNextDiffAsync(cls); } let decompileApiPromise; function decompileApiAsync(options) { if (!decompileApiPromise) decompileApiPromise = pxt.runner.decompileSnippetAsync('', options); return decompileApiPromise; } function renderNamespaces(options) { if (pxt.appTarget.id == "core") return Promise.resolve(); return decompileApiAsync(options) .then((r) => { let res = {}; 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 += ` 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) { options = pxt.Util.clone(options); options.emPixels = 18; options.snippetMode = true; const $els = $(`:not(pre) > code`); let i = 0; function renderNextAsync() { 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($(`<span class="${lev}"/>`).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 = $('<span class="block"/>').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 = $(`<a class="ui link"/>`).attr("href", `/reference/${symbolInfo.attributes.help}`).append($newel); } } $el.replaceWith($newel); } return pxt.U.delay(1, renderNextAsync()); }); } return renderNextAsync(); } function renderProjectAsync(options) { if (!options.projectClass) return Promise.resolve(); function render() { let $el = $("." + options.projectClass).first(); let e = $el[0]; if (!e) return Promise.resolve(); $el.removeClass(options.projectClass); 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; } return pxt.runner.renderProjectAsync(e, id) .then(() => render()); } else return render(); } return render(); } function renderApisAsync(options, replaceParent) { 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 = $('<div />').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, card) { 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, symbol, cardStyle) { const attributes = symbol.attributes; const block = !attributes.blockHidden && Blockly.Blocks[attributes.blockId]; const card = block === null || block === void 0 ? void 0 : block.codeCard; if (card) { const ccard = pxt.U.clone(block.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 }); } } function renderLinksAsync(options, cls, replaceParent, ns) { 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 = $('<div />').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 ? `<xml xmlns="http://www.w3.org/1999/xhtml"><block type="${attributes.blockId}"></block></xml>` : undefined }); } else { addSymbolCardItem(ul, symbol); } } else switch (kind) { case ts.SyntaxKind.ExpressionStatement: { const es = stmt; 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: '<xml xmlns="http://www.w3.org/1999/xhtml"><block type="logic_boolean"><field name="BOOL">TRUE</field></block></xml>' }); 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: '<xml xmlns="http://www.w3.org/1999/xhtml"><block type="controls_if"></block></xml>' }); break;