pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
1,043 lines (1,042 loc) • 120 kB
JavaScript
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;