pxt-core
Version:
Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors
1,149 lines (1,148 loc) • 60.7 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;
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 = () => {
if (typeof marked !== "undefined")
return marked;
if (typeof require === "undefined")
return undefined;
return require("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) {
renderer.image = function (href, title, text) {
var _a, _b;
const endpointName = "makecodeprodmediaeastus-usea";
if (href.startsWith("youtube:")) {
let out = '<div class="tutorial-video-embed"><iframe class="yt-embed" src="https://www.youtube.com/embed/' + href.split(":").pop()
+ '" title="' + text + '" frameborder="0" ' + 'allowFullScreen ' + 'allow="autoplay; picture-in-picture"></iframe></div>';
return out;
}
else if (href.startsWith("azuremedia:")) {
let videoID = href.split(":")[1];
const flagsSplit = videoID.split("?");
let startTime;
let endTime;
if (flagsSplit[1]) {
videoID = flagsSplit[0];
const passedParameters = flagsSplit[1];
startTime = (_a = /start(?:time)?=(\d+)/i.exec(passedParameters)) === null || _a === void 0 ? void 0 : _a[1];
endTime = (_b = /end(?:time)?=(\d+)/i.exec(passedParameters)) === null || _b === void 0 ? void 0 : _b[1];
}
const url = new URL(`https://${endpointName}.streaming.media.azure.net/${videoID}/manifest(format=mpd-time-csf).mpd`);
if (startTime) {
url.hash = `t=${startTime}`;
url.searchParams.append("startTime", startTime);
}
if (endTime) {
url.searchParams.append("endTime", endTime);
}
let out = `<div class="tutorial-video-embed"><video class="ams-embed" controls src="${url.toString()}" /></div>`;
return out;
}
else {
let out = '<img class="ui image" src="' + href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
out += ' loading="lazy"';
out += this.options.xhtml ? '/>' : '>';
return out;
}
};
renderer.listitem = function (text) {
const m = /^\s*\[( |x)\]/i.exec(text);
if (m)
return `<li class="${m[1] == ' ' ? 'unchecked' : 'checked'}">` + text.slice(m[0].length) + '</li>\n';
return '<li>' + text + '</li>\n';
};
renderer.heading = function (text, level, raw) {
let m = /(.*)#([\w\-]+)\s*$/.exec(text);
let id = "";
if (m) {
text = m[1];
id = m[2];
}
// remove tutorial macros
if (text)
text = text.replace(/@(fullscreen|unplugged|showdialog|showhint)/gi, '');
// remove brackets for hiding step title
if (text.match(/\{([\s\S]+)\}/))
text = text.match(/\{([\s\S]+)\}/)[1].trim();
if (id === "") {
id = text.toLowerCase().replace(/[^\w]+/g, '-');
}
return `<h${level} id="${this.options.headerPrefix}${id}">${text}</h${level}>`;
};
}
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();
}
// We have to re-create the renderer every time to avoid the link() function's closure capturing the opts
let renderer = new markedInstance.Renderer();
setupRenderer(renderer);
const linkRenderer = renderer.link;
renderer.link = function (href, title, text) {
const relative = new RegExp('^[/#]').test(href);
const target = !relative ? '_blank' : '';
if (relative && d.versionPath)
href = `/${d.versionPath}${href}`;
const html = linkRenderer.call(renderer, href, title, text);
return html.replace(/^<a /, `<a ${target ? `target="${target}"` : ''} rel="nofollow noopener" `);
};
let sanitizer = docs.requireDOMSanitizer();
markedInstance.setOptions({
renderer: renderer,
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
sanitizer: sanitizer,
smartLists: true,
smartypants: true
});
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(markdown);
// 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, (f, 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, (f, lvl, tp, body) => {
let m = /^(\w+)\s+(.*)/.exec(body);
let cmd = m ? m[1] : body;
let args = m ? m[2] : "";
let rawArgs = args;
args = html2Quote(args);
cmd = html2Quote(cmd);
lvl = parseInt(lvl);
if (!tp) {
return appendEndBox(lvl, endBox, f);
}
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(lvl, 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(lvl, endBox, parts[0].replace("@ARGS@", args));
endBox = parts[1];
let attrs = box.match(/data-[^>\s]+/ig);
if (attrs && attrs.indexOf('data-inferred') >= 0) {
boxSize = lvl;
}
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, (f, nam, cont) => {
let s = U.lookup(registers, nam);
registers[nam] = (s || "") + cont;
return "<!-- aside -->";
});
// fix up spourious newlines at the end of code blocks
html = html.replace(/\n<\/code>/g, "</code>");
registers["main"] = html;
let injectBody = (tmpl, body) => injectHtml(d.boxes[tmpl] || "@BODY@", { BODY: body }, ["BODY"]);
html = "";
for (let k of Object.keys(registers)) {
html += injectBody(k + "-container", registers[k]);
}
pubinfo["body"] = html;
// don't mangle target name in title, it is already in the sitename
pubinfo["name"] = pubinfo["title"] || "";
for (let k of Object.keys(opts.theme)) {
let v = opts.theme[k];
if (typeof v == "string")
pubinfo["theme_" + k] = v;
}
return d.finish();
}
docs.renderMarkdown = renderMarkdown;
function injectHtml(template, vars, quoted = []) {
if (!template)
return '';
return template.replace(/@(\w+)@/g, (f, key) => {
let res = U.lookup(vars, key) || "";
res += ""; // make sure it's a string
if (quoted.indexOf(key) < 0) {
res = html2Quote(res);
}
return res;
});
}
function embedUrl(rootUrl, tag, id, height) {
const url = `${rootUrl}#${tag}:${id}`;
let padding = '70%';
return `<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}" frameborder="0" sandbox="allow-popups allow-forms allow-scripts allow-same-origin"></iframe></div>`;
}
docs.embedUrl = embedUrl;
function runUrl(url, padding, id) {
let embed = `<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}?id=${encodeURIComponent(id)}" allowfullscreen="allowfullscreen" sandbox="allow-popups allow-forms allow-scripts allow-same-origin" frameborder="0"></iframe></div>`;
return embed;
}
docs.runUrl = runUrl;
function codeEmbedUrl(rootUrl, id, height) {
const docurl = `${rootUrl}---codeembed#pub:${id}`;
height = Math.ceil(height || 300);
return `<div style="position:relative;height:calc(${height}px + 5em);width:100%;overflow:hidden;"><iframe style="position:absolute;top:0;left:0;width:100%;height:100%;" src="${docurl}" allowfullscreen="allowfullscreen" frameborder="0" sandbox="allow-scripts allow-same-origin"></iframe></div>`;
}
docs.codeEmbedUrl = codeEmbedUrl;
const inlineTags = {
b: 1,
strong: 1,
em: 1,
};