pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
1,151 lines • 65.6 kB
JavaScript
// Needs to be in its own file to avoid a circular dependency: util.ts -> main.ts -> util.ts
var pxt;
(function (pxt) {
/**
* Track an event.
*/
pxt.tickEvent = function (id) { };
})(pxt || (pxt = {}));
var pxt;
(function (pxt) {
})(pxt || (pxt = {}));
var pxt;
(function (pxt) {
let LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["Debug"] = 0] = "Debug";
LogLevel[LogLevel["Info"] = 1] = "Info";
LogLevel[LogLevel["Log"] = 1] = "Log";
LogLevel[LogLevel["Warning"] = 2] = "Warning";
LogLevel[LogLevel["Error"] = 3] = "Error";
})(LogLevel = pxt.LogLevel || (pxt.LogLevel = {}));
class ConsoleLogger {
constructor() {
this.setLogLevel(LogLevel.Info);
}
setLogLevel(level) {
this.logLevel = level;
}
getLogLevel() {
return this.logLevel;
}
info(...args) {
if (!this.shouldLog(LogLevel.Info))
return;
if (console === null || console === void 0 ? void 0 : console.info) {
console.info.call(null, ...args);
}
}
log(...args) {
if (!this.shouldLog(LogLevel.Log))
return;
if (console === null || console === void 0 ? void 0 : console.log) {
console.log.call(null, ...args);
}
}
debug(...args) {
if (!this.shouldLog(LogLevel.Debug))
return;
if (console === null || console === void 0 ? void 0 : console.debug) {
console.debug.call(null, ...args);
}
}
error(...args) {
if (!this.shouldLog(LogLevel.Error))
return;
if (console === null || console === void 0 ? void 0 : console.error) {
console.error.call(null, ...args);
}
}
warn(...args) {
if (!this.shouldLog(LogLevel.Warning))
return;
if (console === null || console === void 0 ? void 0 : console.warn) {
console.warn.call(null, ...args);
}
}
shouldLog(level) {
return level >= this.logLevel;
}
}
pxt.ConsoleLogger = ConsoleLogger;
let logger = new ConsoleLogger();
function info(...args) {
logger.info(...args);
}
pxt.info = info;
function log(...args) {
logger.log(...args);
}
pxt.log = log;
function debug(...args) {
logger.debug(...args);
}
pxt.debug = debug;
function error(...args) {
logger.error(...args);
}
pxt.error = error;
function warn(...args) {
logger.warn(...args);
}
pxt.warn = warn;
function setLogger(impl) {
const level = logger === null || logger === void 0 ? void 0 : logger.getLogLevel();
logger = impl;
if (level !== undefined) {
logger.setLogLevel(level);
}
}
pxt.setLogger = setLogger;
function setLogLevel(level) {
logger.setLogLevel(level);
}
pxt.setLogLevel = setLogLevel;
})(pxt || (pxt = {}));
/// <reference path="./tickEvent.ts" />
/// <reference path="./apptarget.ts" />
/// <reference path="./logger.ts" />
var ts;
(function (ts) {
var pxtc;
(function (pxtc) {
pxtc.__dummy = 42;
})(pxtc = ts.pxtc || (ts.pxtc = {}));
})(ts || (ts = {}));
var pxtc = ts.pxtc;
(function (ts) {
var pxtc;
(function (pxtc) {
var Util;
(function (Util) {
function assert(cond, msg = "Assertion failed") {
if (!cond) {
debugger;
throw new Error(msg);
}
}
Util.assert = assert;
function flatClone(obj) {
if (obj == null)
return null;
let r = {};
Object.keys(obj).forEach((k) => { r[k] = obj[k]; });
return r;
}
Util.flatClone = flatClone;
function clone(v) {
if (v == null)
return null;
return JSON.parse(JSON.stringify(v));
}
Util.clone = clone;
function htmlEscape(_input) {
if (!_input)
return _input; // null, undefined, empty string test
return _input.replace(/([^\w .!?\-$])/ug, c => "&#" + c.codePointAt(0) + ";");
}
Util.htmlEscape = htmlEscape;
function htmlUnescape(_input) {
if (!_input)
return _input; // null, undefined, empty string test
return _input.replace(/(&#\d+;)/g, c => String.fromCodePoint(Number(c.substr(2, c.length - 3))));
}
Util.htmlUnescape = htmlUnescape;
function jsStringQuote(s) {
return s.replace(/[^\w .!?\-$]/g, (c) => {
let h = c.charCodeAt(0).toString(16);
return "\\u" + "0000".substr(0, 4 - h.length) + h;
});
}
Util.jsStringQuote = jsStringQuote;
function jsStringLiteral(s) {
return "\"" + jsStringQuote(s) + "\"";
}
Util.jsStringLiteral = jsStringLiteral;
function initials(username) {
if (/^\w+@/.test(username)) {
// Looks like an email address. Return first two characters.
const initials = username.match(/^\w\w/);
return initials.shift().toUpperCase();
}
else {
// Parse the user name for user initials
const initials = username.match(/\b\w/g) || [];
return ((initials.shift() || '') + (initials.pop() || '')).toUpperCase();
}
}
Util.initials = initials;
// Localization functions. Please port any modifications over to pxtsim/localization.ts
let _localizeLang = "en";
let _localizeStrings = {};
let _translationsCache = {};
//let _didSetlocalizations = false;
//let _didReportLocalizationsNotSet = false;
let localizeLive = false;
function enableLiveLocalizationUpdates() {
localizeLive = true;
}
Util.enableLiveLocalizationUpdates = enableLiveLocalizationUpdates;
function liveLocalizationEnabled() {
return localizeLive;
}
Util.liveLocalizationEnabled = liveLocalizationEnabled;
/**
* Returns the current user language, prepended by "live-" if in live mode
*/
function localeInfo() {
return `${localizeLive ? "live-" : ""}${userLanguage()}`;
}
Util.localeInfo = localeInfo;
/**
* Returns current user language iSO-code. Default is `en`.
*/
function userLanguage() {
return _localizeLang;
}
Util.userLanguage = userLanguage;
// This function returns normalized language code
// For example: zh-CN this returns ["zh-CN", "zh", "zh-cn"]
// First two are valid crowdin\makecode locale code,
// Last all lowercase one is just for the backup when reading user defined extensions & tutorials.
function normalizeLanguageCode(code) {
const langParts = /^(\w{2,3})-(\w{2,4}$)/i.exec(code);
if (langParts && langParts[1] && langParts[2]) {
return [`${langParts[1].toLowerCase()}-${langParts[2].toUpperCase()}`, langParts[1].toLowerCase(),
`${langParts[1].toLowerCase()}-${langParts[2].toLowerCase()}`];
}
else {
return [(code || "en").toLowerCase()];
}
}
Util.normalizeLanguageCode = normalizeLanguageCode;
function setUserLanguage(localizeLang) {
_localizeLang = normalizeLanguageCode(localizeLang)[0];
}
Util.setUserLanguage = setUserLanguage;
function isUserLanguageRtl() {
return /^ar|dv|fa|ha|he|ks|ku|ps|ur|yi/i.test(_localizeLang);
}
Util.isUserLanguageRtl = isUserLanguageRtl;
Util.TRANSLATION_LOCALE = "pxt";
function isTranslationMode() {
return userLanguage() == Util.TRANSLATION_LOCALE;
}
Util.isTranslationMode = isTranslationMode;
function _localize(s) {
// Needs to be test in localhost / CLI
/*if (!_didSetlocalizations && !_didReportLocalizationsNotSet) {
_didReportLocalizationsNotSet = true;
pxt.tickEvent("locale.localizationsnotset");
// pxt.reportError can't be used here because of order of file imports
// Just use pxt.error instead, and use an Error so stacktrace is reported
pxt.error(new Error("Attempted to translate a string before localizations were set"));
}*/
return _localizeStrings[s] || s;
}
Util._localize = _localize;
function getLocalizedStrings() {
return _localizeStrings;
}
Util.getLocalizedStrings = getLocalizedStrings;
function setLocalizedStrings(strs) {
//_didSetlocalizations = true;
_localizeStrings = strs;
}
Util.setLocalizedStrings = setLocalizedStrings;
function translationsCache() {
return _translationsCache;
}
Util.translationsCache = translationsCache;
function fmt_va(f, args) {
if (args.length == 0)
return f;
return f.replace(/\{([0-9]+)(\:[^\}]+)?\}/g, function (s, n, spec) {
let v = args[parseInt(n)];
let r = "";
let fmtMatch = /^:f(\d*)\.(\d+)/.exec(spec);
if (fmtMatch) {
let precision = parseInt(fmtMatch[2]);
let len = parseInt(fmtMatch[1]) || 0;
let fillChar = /^0/.test(fmtMatch[1]) ? "0" : " ";
let num = v.toFixed(precision);
if (len > 0 && precision > 0)
len += precision + 1;
if (len > 0) {
while (num.length < len) {
num = fillChar + num;
}
}
r = num;
}
else if (spec == ":x") {
r = "0x" + v.toString(16);
}
else if (v === undefined)
r = "(undef)";
else if (v === null)
r = "(null)";
else if (v.toString)
r = v.toString();
else
r = v + "";
if (spec == ":a") {
if (/^\s*[euioah]/.test(r.toLowerCase()))
r = "an " + r;
else if (/^\s*[bcdfgjklmnpqrstvwxz]/.test(r.toLowerCase()))
r = "a " + r;
}
else if (spec == ":s") {
if (v == 1)
r = "";
else
r = "s";
}
else if (spec == ":q") {
r = Util.htmlEscape(r);
}
else if (spec == ":jq") {
r = Util.jsStringQuote(r);
}
else if (spec == ":uri") {
r = encodeURIComponent(r).replace(/'/g, "%27").replace(/"/g, "%22");
}
else if (spec == ":url") {
r = encodeURI(r).replace(/'/g, "%27").replace(/"/g, "%22");
}
else if (spec == ":%") {
r = (v * 100).toFixed(1).toString() + '%';
}
return r;
});
}
Util.fmt_va = fmt_va;
function fmt(f, ...args) { return fmt_va(f, args); }
Util.fmt = fmt;
const locStats = {};
function dumpLocStats() {
const r = {};
Object.keys(locStats).sort((a, b) => locStats[b] - locStats[a])
.forEach(k => r[k] = k);
pxt.log('prioritized list of strings:');
pxt.log(JSON.stringify(r, null, 2));
}
Util.dumpLocStats = dumpLocStats;
let sForPlural = true;
function lf_va(format, args) {
if (!format)
return format;
locStats[format] = (locStats[format] || 0) + 1;
let lfmt = Util._localize(format);
if (!sForPlural && lfmt != format && /\d:s\}/.test(lfmt)) {
lfmt = lfmt.replace(/\{\d+:s\}/g, "");
}
lfmt = lfmt.replace(/^\{(id|loc):[^\}]+\}/g, '');
return fmt_va(lfmt, args);
}
Util.lf_va = lf_va;
function lf(format, ...args) {
return lf_va(format, args); // @ignorelf@
}
Util.lf = lf;
/**
* Similar to lf but the string do not get extracted into the loc file.
*/
function rlf(format, ...args) {
return lf_va(format, args); // @ignorelf@
}
Util.rlf = rlf;
/**
* Same as lf except the strings are not replaced in translation mode. This is used
* exclusively for blockly JSON block definitions as the crowdin in-context translation
* script doesn't handle the SVG text fields. Instead, they are translated via a context
* menu item on the block.
*/
function blf(format) {
if (isTranslationMode()) {
return format;
}
return lf_va(format, []); // @ignorelf@
}
Util.blf = blf;
function lookup(m, key) {
if (m.hasOwnProperty(key))
return m[key];
return null;
}
Util.lookup = lookup;
function isoTime(time) {
let d = new Date(time * 1000);
return Util.fmt("{0}-{1:f02.0}-{2:f02.0} {3:f02.0}:{4:f02.0}:{5:f02.0}", d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds());
}
Util.isoTime = isoTime;
function userError(msg) {
let e = new Error(msg);
e.isUserError = true;
throw e;
}
Util.userError = userError;
// small deep equals for primitives, objects, arrays. returns error message
function deq(a, b) {
if (a === b)
return null;
if (!a || !b)
return "Null value";
if (typeof a == 'object' && typeof b == 'object') {
if (Array.isArray(a)) {
if (!Array.isArray(b)) {
return "Expected array";
}
if (a.length != b.length) {
return "Expected array of length " + a.length + ", got " + b.length;
}
for (let i = 0; i < a.length; i++) {
if (deq(a[i], b[i]) != null) {
return "Expected array value " + a[i] + " got " + b[i];
}
}
return null;
}
let ak = Object.keys(a);
let bk = Object.keys(a);
if (ak.length != bk.length) {
return "Expected " + ak.length + " keys, got " + bk.length;
}
for (let i = 0; i < ak.length; i++) {
if (!Object.prototype.hasOwnProperty.call(b, ak[i])) {
return "Missing key " + ak[i];
}
else if (deq(a[ak[i]], b[ak[i]]) != null) {
return "Expected value of " + ak[i] + " to be " + a[ak[i]] + ", got " + b[ak[i]];
}
}
return null;
}
return "Unable to compare " + a + ", " + b;
}
Util.deq = deq;
function deepEqual(a, b) {
if (a === b) {
return true;
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
const arrA = Array.isArray(a);
const arrB = Array.isArray(b);
if (arrA && arrB) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; ++i) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
if (arrA !== arrB) {
return false;
}
const keysA = Object.keys(a);
if (keysA.length !== Object.keys(b).length) {
return false;
}
for (const key of keysA) {
if (!b.hasOwnProperty(key)) {
return false;
}
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// True if both are NaN, false otherwise
return a !== a && b !== b;
}
Util.deepEqual = deepEqual;
;
})(Util = pxtc.Util || (pxtc.Util = {}));
})(pxtc = ts.pxtc || (ts.pxtc = {}));
})(ts || (ts = {}));
const lf = ts.pxtc.Util.lf;
/// <reference path='../localtypings/pxtarget.d.ts' />
/// <reference path='../localtypings/dompurify.d.ts' />
/// <reference path="commonutil.ts"/>
/// <reference path="./logger.ts" />
var pxt;
(function (pxt) {
var docs;
(function (docs) {
var U = pxtc.Util;
let markedInstance;
let stdboxes = {};
let stdmacros = {};
const stdSetting = "<!-- @CMD@ @ARGS@ -->";
let stdsettings = {
"parent": stdSetting,
"short": stdSetting,
"description": "<!-- desc -->",
"activities": "<!-- activities -->",
"explicitHints": "<!-- hints -->",
"flyoutOnly": "<!-- flyout -->",
"hideToolbox": "<!-- hideToolbox -->",
"hideIteration": "<!-- iter -->",
"codeStart": "<!-- start -->",
"codeStop": "<!-- stop -->",
"autoOpen": "<!-- autoOpen -->",
"autoexpandOff": "<!-- autoexpandOff -->",
"preferredEditor": "<!-- preferredEditor -->"
};
function replaceAll(replIn, x, y) {
return replIn.split(x).join(y);
}
function htmlQuote(s) {
s = replaceAll(s, "&", "&");
s = replaceAll(s, "<", "<");
s = replaceAll(s, ">", ">");
s = replaceAll(s, "\"", """);
s = replaceAll(s, "\'", "'");
return s;
}
docs.htmlQuote = htmlQuote;
// the input already should be HTML-quoted but we want to make sure, and also quote quotes
function html2Quote(s) {
if (!s)
return s;
return htmlQuote(s.replace(/\&([#a-z0-9A-Z]+);/g, (f, ent) => {
switch (ent) {
case "amp": return "&";
case "lt": return "<";
case "gt": return ">";
case "quot": return "\"";
default:
if (ent[0] == "#")
return String.fromCharCode(parseInt(ent.slice(1)));
else
return f;
}
}));
}
docs.html2Quote = html2Quote;
//The extra YouTube macros are in case there is a timestamp on the YouTube URL.
//TODO: Add equivalent support for youtu.be links
const links = [
{
rx: /^vimeo\.com\/(\d+)/i,
cmd: "### @vimeo $1"
},
{
rx: /^(www\.youtube\.com\/watch\?v=|youtu\.be\/)([\w\-]+(\#t=([0-9]+m[0-9]+s|[0-9]+m|[0-9]+s))?)/i,
cmd: "### @youtube $2"
}
];
docs.requireMarked = () => {
const globalMarked = globalThis === null || globalThis === void 0 ? void 0 : globalThis.marked;
if (globalMarked)
return globalMarked;
if (typeof require === "undefined")
return undefined;
const tryRequire = (id) => {
var _a;
try {
const mod = require(id);
if (mod === null || mod === void 0 ? void 0 : mod.marked)
return mod.marked;
if ((_a = mod === null || mod === void 0 ? void 0 : mod.default) === null || _a === void 0 ? void 0 : _a.marked)
return mod.default.marked;
if (mod === null || mod === void 0 ? void 0 : mod.default)
return mod.default;
return mod;
}
catch (e) {
return undefined;
}
};
return tryRequire("marked/lib/marked.umd.js")
|| tryRequire("marked");
};
docs.requireDOMSanitizer = () => {
if (typeof DOMPurify !== "undefined")
return DOMPurify.sanitize;
if (typeof require === "undefined")
return undefined;
return require("dompurify").sanitize;
};
function parseHtmlAttrs(s) {
let attrs = {};
while (s.trim()) {
let m = /\s*([^=\s]+)=("([^"]*)"|'([^']*)'|(\S*))/.exec(s);
if (m) {
let v = m[3] || m[4] || m[5] || "";
attrs[m[1].toLowerCase()] = v;
}
else {
m = /^\s*(\S+)/.exec(s);
attrs[m[1]] = "true";
}
s = s.slice(m[0].length);
}
return attrs;
}
const error = (s) => `<div class='ui negative message'>${htmlQuote(s)}</div>`;
function prepTemplate(d) {
let boxes = U.clone(stdboxes);
let macros = U.clone(stdmacros);
let settings = U.clone(stdsettings);
let menus = {};
let toc = {};
let params = d.params;
let theme = d.theme;
d.boxes = boxes;
d.macros = macros;
d.settings = settings;
d.html = d.html.replace(/<aside\s+([^<>]+)>([^]*?)<\/aside>/g, (full, attrsStr, body) => {
let attrs = parseHtmlAttrs(attrsStr);
let name = attrs["data-name"] || attrs["id"];
if (!name)
return error("id or data-name missing on macro");
if (/box/.test(attrs["class"])) {
boxes[name] = body;
}
else if (/aside/.test(attrs["class"])) {
boxes[name] = `<!-- BEGIN-ASIDE ${name} -->${body}<!-- END-ASIDE -->`;
}
else if (/setting/.test(attrs["class"])) {
settings[name] = body;
}
else if (/menu/.test(attrs["class"])) {
menus[name] = body;
}
else if (/toc/.test(attrs["class"])) {
toc[name] = body;
}
else {
macros[name] = body;
}
return `<!-- macro ${name} -->`;
});
let recMenu = (m, lev) => {
let templ = menus["item"];
let mparams = {
NAME: m.name,
};
if (m.subitems) {
if (!!menus["toc-dropdown"]) {
templ = menus["toc-dropdown"];
}
else {
/** TODO: when all targets bumped to include https://github.com/microsoft/pxt/pull/6058,
* swap templ assignments below with the commented out version, and remove
* top-dropdown, top-dropdown-noheading, inner-dropdown, and nested-dropdown from
* docfiles/macros.html **/
if (lev == 0)
templ = menus["top-dropdown"];
else
templ = menus["inner-dropdown"];
}
mparams["ITEMS"] = m.subitems.map(e => recMenu(e, lev + 1)).join("\n");
}
else {
if (/^-+$/.test(m.name)) {
templ = menus["divider"];
}
if (m.path && !/^(https?:|\/)/.test(m.path))
return error("Invalid link: " + m.path);
mparams["LINK"] = m.path;
}
return injectHtml(templ, mparams, ["ITEMS"]);
};
let breadcrumb = [{
name: lf("Docs"),
href: "/docs"
}];
const TOC = d.TOC || theme.TOC || [];
let tocPath = [];
let isCurrentTOC = (m) => {
for (let c of m.subitems || []) {
if (isCurrentTOC(c)) {
tocPath.push(m);
return true;
}
}
if (d.filepath && !!m.path && d.filepath == m.path) {
tocPath.push(m);
return true;
}
return false;
};
TOC.forEach(isCurrentTOC);
let recTOC = (m, lev) => {
let templ = toc["item"];
let mparams = {
NAME: m.name,
};
if (m.path && !/^(https?:|\/)/.test(m.path))
return error("Invalid link: " + m.path);
if (/^\//.test(m.path) && d.versionPath)
m.path = `/${d.versionPath}${m.path}`;
mparams["LINK"] = m.path;
if (tocPath.indexOf(m) >= 0) {
mparams["ACTIVE"] = 'active';
mparams["EXPANDED"] = 'true';
breadcrumb.push({
name: m.name,
href: m.path
});
}
else {
mparams["EXPANDED"] = 'false';
}
if (m.subitems && m.subitems.length > 0) {
if (!!toc["toc-dropdown"]) {
// if macros support "toc-*", use them
if (m.name !== "") {
templ = toc["toc-dropdown"];
}
else {
templ = toc["toc-dropdown-noLink"];
}
}
else {
// if macros don't support "toc-*"
/** TODO: when all targets bumped to include https://github.com/microsoft/pxt/pull/6058,
* delete this else branch, and remove
* top-dropdown, top-dropdown-noheading, inner-dropdown, and nested-dropdown from
* docfiles/macros.html **/
if (lev == 0) {
if (m.name !== "") {
templ = toc["top-dropdown"];
}
else {
templ = toc["top-dropdown-noHeading"];
}
}
else if (lev == 1)
templ = toc["inner-dropdown"];
else
templ = toc["nested-dropdown"];
}
mparams["ITEMS"] = m.subitems.map(e => recTOC(e, lev + 1)).join("\n");
}
else {
if (/^-+$/.test(m.name)) {
templ = toc["divider"];
}
}
return injectHtml(templ, mparams, ["ITEMS"]);
};
params["menu"] = (theme.docMenu || []).map(e => recMenu(e, 0)).join("\n");
params["TOC"] = TOC.map(e => recTOC(e, 0)).join("\n");
if (theme.appStoreID)
params["appstoremeta"] = `<meta name="apple-itunes-app" content="app-id=${U.htmlEscape(theme.appStoreID)}"/>`;
let breadcrumbHtml = '';
if (breadcrumb.length > 1) {
breadcrumbHtml = `
<nav class="ui breadcrumb" aria-label="${lf("Breadcrumb")}">
${breadcrumb.map((b, i) => `<a class="${i == breadcrumb.length - 1 ? "active" : ""} section"
href="${html2Quote(b.href)}" aria-current="${i == breadcrumb.length - 1 ? "page" : ""}">${html2Quote(b.name)}</a>`)
.join('<i class="right chevron icon divider"></i>')}
</nav>`;
}
params["breadcrumb"] = breadcrumbHtml;
if (theme.boardName)
params["boardname"] = html2Quote(theme.boardName);
if (theme.boardNickname)
params["boardnickname"] = html2Quote(theme.boardNickname);
if (theme.driveDisplayName)
params["drivename"] = html2Quote(theme.driveDisplayName);
if (theme.homeUrl)
params["homeurl"] = html2Quote(theme.homeUrl);
params["targetid"] = theme.id || "???";
params["targetname"] = theme.name || "Microsoft MakeCode";
params["docsheader"] = theme.docsHeader || "Documentation";
params["orgtitle"] = "MakeCode";
const docsLogo = theme.docsLogo && U.htmlEscape(theme.docsLogo);
const orgLogo = (theme.organizationWideLogo || theme.organizationLogo) && U.htmlEscape(theme.organizationWideLogo || theme.organizationLogo);
const orglogomobile = theme.organizationLogo && U.htmlEscape(theme.organizationLogo);
params["targetlogo"] = docsLogo ? `<img aria-hidden="true" role="presentation" class="ui ${theme.logoWide ? "small" : "mini"} image" src="${docsLogo}" />` : "";
params["orglogo"] = orgLogo ? `<img aria-hidden="true" role="presentation" class="ui image" src="${orgLogo}" />` : "";
params["orglogomobile"] = orglogomobile ? `<img aria-hidden="true" role="presentation" class="ui image" src="${orglogomobile}" />` : "";
let ghURLs = d.ghEditURLs || [];
if (ghURLs.length) {
let ghText = `<p style="margin-top:1em">\n`;
let linkLabel = lf("Edit this page on GitHub");
for (let u of ghURLs) {
ghText += `<a href="${u}"><i class="write icon"></i>${linkLabel}</a><br>\n`;
linkLabel = lf("Edit template of this page on GitHub");
}
ghText += `</p>\n`;
params["github"] = ghText;
}
else {
params["github"] = "";
}
// Add accessiblity menu
const accMenuHtml = `
<a href="#maincontent" class="ui item link" tabindex="0" role="menuitem">${lf("Skip to main content")}</a>
`;
params['accMenu'] = accMenuHtml;
const printButtonTitleText = lf("Print this page");
// Add print button
const printBtnHtml = `
<button id="printbtn" class="circular ui icon right floated button hideprint" title="${printButtonTitleText}" aria-label="${printButtonTitleText}">
<i class="icon print"></i>
</button>
`;
params['printBtn'] = printBtnHtml;
// Add sidebar toggle
const sidebarToggleHtml = `
<a id="togglesidebar" class="launch icon item" tabindex="0" title="Side menu" aria-label="${lf("Side menu")}" role="menuitem" aria-expanded="false">
<i class="content icon"></i>
</a>
`;
params['sidebarToggle'] = sidebarToggleHtml;
// Add search bars
const searchBarIds = ['tocsearch1', 'tocsearch2'];
const searchBarsHtml = searchBarIds.map((searchBarId) => {
return `
<input type="search" name="q" placeholder="${lf("Search...")}" aria-label="${lf("Search Documentation")}">
<i onclick="document.getElementById('${searchBarId}').submit();" tabindex="0" class="search link icon" aria-label="${lf("Search")}" role="button"></i>
`;
});
params["searchBar1"] = searchBarsHtml[0];
params["searchBar2"] = searchBarsHtml[1];
let style = '';
if (theme.accentColor)
style += `
.ui.accent { color: ${theme.accentColor}; }
.ui.inverted.accent { background: ${theme.accentColor}; }
`;
params["targetstyle"] = style;
params["tocclass"] = theme.lightToc ? "lighttoc" : "inverted";
for (let k of Object.keys(theme)) {
let v = theme[k];
if (params[k] === undefined && typeof v == "string")
params[k] = v;
}
d.finish = () => injectHtml(d.html, params, [
"body",
"menu",
"accMenu",
"TOC",
"prev",
"next",
"printBtn",
"breadcrumb",
"targetlogo",
"orglogo",
"orglogomobile",
"github",
"JSON",
"appstoremeta",
"sidebarToggle",
"searchBar1",
"searchBar2"
]);
// Normalize any path URL with any version path in the current URL
function normalizeUrl(href) {
if (!href)
return href;
const relative = href.indexOf('/') == 0;
if (relative && d.versionPath)
href = `/${d.versionPath}${href}`;
return href;
}
}
docs.prepTemplate = prepTemplate;
function setupRenderer(renderer, context = {}) {
renderer.image = function (token) {
var _a;
const href = token.href || "";
const title = token.title || "";
const text = token.text || "";
if (href.startsWith("youtube:")) {
const videoId = href.split(":").pop();
return `<div class="tutorial-video-embed"><iframe class="yt-embed" src="https://www.youtube.com/embed/${videoId}" title="${text}" frameborder="0" allowFullScreen allow="autoplay; picture-in-picture"></iframe></div>`;
}
else {
let out = `<img class="ui image" src="${href}" alt="${text}"`;
if (title) {
out += ` title="${title}"`;
}
out += ' loading="lazy"';
out += ((_a = this.options) === null || _a === void 0 ? void 0 : _a.xhtml) ? '/>' : '>';
return out;
}
};
renderer.listitem = function (item) {
const inner = this.parser.parse(item.tokens, !!item.loose);
if (item.task) {
return `<li class="${item.checked ? "checked" : "unchecked"}">${inner}</li>\n`;
}
return `<li>${inner}</li>\n`;
};
renderer.heading = function (token) {
var _a;
const parser = this.parser;
let html = parser.parseInline(token.tokens).trim();
// Extract custom id, if provided
let id = "";
const idMatch = /(.*)#([\w\-]+)\s*$/.exec(token.text || "");
if (idMatch) {
id = idMatch[2];
html = html.replace(/#([\w\-]+)\s*$/, "").trim();
}
// remove tutorial macros
html = html.replace(/@(fullscreen|unplugged|showdialog|showhint)/gi, '').trim();
// remove brackets for hiding step title
const braceMatch = html.match(/\{([\s\S]+)\}/);
if (braceMatch) {
html = braceMatch[1].trim();
}
if (!id) {
const stripHtmlTags = (input) => {
let prev;
do {
prev = input;
input = input.replace(/<[^>]+>/g, '');
} while (input !== prev);
return input;
};
const plain = stripHtmlTags(html).toLowerCase();
id = plain.replace(/[^\w]+/g, '-');
}
const headerPrefix = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.headerPrefix) || "";
return `<h${token.depth} id="${headerPrefix}${id}">${html}</h${token.depth}>`;
};
renderer.code = function (token) {
const rawLang = typeof (token === null || token === void 0 ? void 0 : token.lang) === "string" ? token.lang.trim() : "";
const normalizedLang = htmlQuote(rawLang.toLowerCase());
const text = typeof (token === null || token === void 0 ? void 0 : token.text) === "string" ? token.text : "";
const escaped = (token === null || token === void 0 ? void 0 : token.escaped) !== false;
const code = escaped ? text : htmlQuote(text);
const classAttr = normalizedLang ? ` class="lang-${normalizedLang}"` : "";
return `<pre><code${classAttr}>${code}</code></pre>`;
};
const versionPath = context === null || context === void 0 ? void 0 : context.versionPath;
const linkRenderer = renderer.link ? renderer.link.bind(renderer) : undefined;
renderer.link = function (token) {
const originalHref = token.href || "";
const isRelative = /^[/#]/.test(originalHref);
const adjustedHref = isRelative && versionPath ? `/${versionPath}${originalHref}` : originalHref;
const adjustedToken = Object.assign(Object.assign({}, token), { href: adjustedHref });
const html = linkRenderer
? linkRenderer(adjustedToken)
: `<a href="${adjustedToken.href}">${this.parser.parseInline(adjustedToken.tokens || [])}</a>`;
const attrs = [];
if (!isRelative) {
attrs.push('target="_blank"');
}
attrs.push('rel="nofollow noopener"');
const attrString = attrs.join(' ');
return html.replace(/^<a /, `<a ${attrString} `);
};
}
docs.setupRenderer = setupRenderer;
function renderConditionalMacros(template, pubinfo) {
return template
.replace(/<!--\s*@(ifn?def)\s+(\w+)\s*-->([^]*?)<!--\s*@endif\s*-->/g, (full, cond, sym, inner) => {
if ((cond == "ifdef" && pubinfo[sym]) || (cond == "ifndef" && !pubinfo[sym]))
return `<!-- ${cond} ${sym} -->${inner}<!-- endif -->`;
else
return `<!-- ${cond} ${sym} endif -->`;
});
}
docs.renderConditionalMacros = renderConditionalMacros;
function renderMarkdown(opts) {
let hasPubInfo = true;
if (!opts.pubinfo) {
hasPubInfo = false;
opts.pubinfo = {};
}
let pubinfo = opts.pubinfo;
if (!opts.theme)
opts.theme = {};
delete opts.pubinfo["private"]; // just in case
if (pubinfo["time"]) {
let tm = parseInt(pubinfo["time"]);
if (!pubinfo["timems"])
pubinfo["timems"] = 1000 * tm + "";
if (!pubinfo["humantime"])
pubinfo["humantime"] = U.isoTime(tm);
}
if (pubinfo["name"]) {
pubinfo["dirname"] = pubinfo["name"].replace(/[^A-Za-z0-9_]/g, "-");
pubinfo["title"] = pubinfo["name"];
}
if (hasPubInfo) {
pubinfo["JSON"] = JSON.stringify(pubinfo, null, 4).replace(/</g, "\\u003c");
}
let template = opts.template;
template = template
.replace(/<!--\s*@include\s+(\S+)\s*-->/g, (full, fn) => {
let cont = (opts.theme.htmlDocIncludes || {})[fn] || "";
return "<!-- include " + fn + " -->\n" + cont + "\n<!-- end include -->\n";
});
template = renderConditionalMacros(template, pubinfo);
if (opts.locale)
template = translate(template, opts.locale).text;
let d = {
html: template,
theme: opts.theme,
filepath: opts.filepath,
versionPath: opts.versionPath,
ghEditURLs: opts.ghEditURLs,
params: pubinfo,
TOC: opts.TOC
};
prepTemplate(d);
if (!markedInstance) {
markedInstance = docs.requireMarked();
}
if (!markedInstance)
return d.finish ? d.finish() : d.html;
// We have to re-create the renderer every time to avoid the link() function's closure capturing the opts
const renderer = new markedInstance.Renderer();
setupRenderer(renderer, { versionPath: d.versionPath });
let sanitizer = docs.requireDOMSanitizer();
markedInstance.setOptions({
renderer: renderer,
async: false,
gfm: true,
tables: true,
breaks: false,
pedantic: false
});
let markdown = opts.markdown;
// append repo info if any
if (opts.repo)
markdown += `
\`\`\`package
${opts.repo.name.replace(/^pxt-/, '')}=github:${opts.repo.fullName}#${opts.repo.tag || "master"}
\`\`\`
`;
//Uses the CmdLink definitions to replace links to YouTube and Vimeo (limited at the moment)
markdown = markdown.replace(/^\s*https?:\/\/(\S+)\s*$/mg, (f, lnk) => {
for (let ent of links) {
let m = ent.rx.exec(lnk);
if (m) {
return ent.cmd.replace(/\$(\d+)/g, (f, k) => {
return m[parseInt(k)] || "";
}) + "\n";
}
}
return f;
});
// replace pre-template in markdown
markdown = markdown.replace(/@([a-z]+)@/ig, (m, param) => {
let macro = pubinfo[param];
if (!macro && opts.throwOnError)
U.userError(`unknown macro ${param}`);
return macro || 'unknown macro';
});
let html = markedInstance.parse(markdown);
const coerceToString = (value, fallback = "") => {
if (typeof value === "string")
return value;
if (value && typeof value.toString === "function") {
return value.toString();
}
return fallback;
};
let sanitizedHtml = html;
if (sanitizer) {
sanitizedHtml = sanitizer(html);
}
html = coerceToString(sanitizedHtml, coerceToString(html, ""));
// support for breaks which somehow don't work out of the box
html = html.replace(/<br\s*\/>/ig, "<br/>");
// github will render images if referenced as 
// we require /static/foo.png
html = html.replace(/(<img [^>]* src=")\/docs\/static\/([^">]+)"/g, (full, pref, addr) => pref + '/static/' + addr + '"');
let endBox = "";
let boxSize = 0;
function appendEndBox(size, box, html) {
let r = html;
if (size <= boxSize) {
r = endBox + r;
endBox = "";
boxSize = 0;
}
return r;
}
html = html.replace(/<h(\d)[^>]+>\s*([~@])?\s*(.*?)<\/h\d>/g, (full, lvl, tp, body) => {
let m = /^(\w+)\s+(.*)/.exec(body);
let cmd = m ? m[1] : body;
let args = m ? m[2] : "";
args = html2Quote(args);
cmd = html2Quote(cmd);
const level = parseInt(lvl, 10);
if (!tp) {
return appendEndBox(level, endBox, full);
}
else if (tp == "@") {
let expansion = U.lookup(d.settings, cmd);
if (expansion != null) {
pubinfo[cmd] = args;
}
else {
expansion = U.lookup(d.macros, cmd);
if (expansion == null) {
if (opts.throwOnError)
U.userError(`Unknown command: @${cmd}`);
return error(`Unknown command: @${cmd}`);
}
}
let ivars = {
ARGS: args,
CMD: cmd
};
return appendEndBox(level, endBox, injectHtml(expansion, ivars, ["ARGS", "CMD"]));
}
else {
if (!cmd) {
let r = endBox;
endBox = "";
return r;
}
let box = U.lookup(d.boxes, cmd);
if (box) {
let parts = box.split("@BODY@");
let r = appendEndBox(level, endBox, parts[0].replace("@ARGS@", args));
endBox = parts[1];
let attrs = box.match(/data-[^>\s]+/ig);
if (attrs && attrs.indexOf('data-inferred') >= 0) {
boxSize = level;
}
return r;
}
else {
if (opts.throwOnError)
U.userError(`Unknown box: ~ ${cmd}`);
return error(`Unknown box: ~ ${cmd}`);
}
}
});
if (endBox)
html = html + endBox;
if (!pubinfo["title"]) {
let titleM = /<h1[^<>]*>([^<>]+)<\/h1>/.exec(html);
if (titleM)
pubinfo["title"] = html2Quote(titleM[1]);
}
if (!pubinfo["description"]) {
let descM = /<p>([^]+?)<\/p>/.exec(html);
if (descM)
pubinfo["description"] = html2Quote(descM[1]);
}
// try getting a better custom image for twitter
const imgM = /<div class="ui embed mdvid"[^<>]+?data-placeholder="([^"]+)"[^>]*\/?>/i.exec(html)
|| /<img class="ui [^"]*image" src="([^"]+)"[^>]*\/?>/i.exec(html);
if (imgM)
pubinfo["cardLogo"] = html2Quote(imgM[1]);
pubinfo["twitter"] = html2Quote(opts.theme.twitter || "@msmakecode");
let registers = {};
registers["main"] = ""; // first
html = html.replace(/<!-- BEGIN-ASIDE (\S+) -->([^]*?)<!-- END-ASIDE -->/g, (full, nam, cont) => {
let s = U.lookup(registers, nam);
registers[nam] = (s || "") + cont;
return "<!-- aside -->";
});
// fix up