<commit_before>/// <reference path="../localtypings/blockly.d.ts" />
/// <reference path="../built/pxtlib.d.ts" />
import Util = pxt.Util;

let lf = Util.lf;

namespace pxt.blocks {
    export const blockColors: Map<number | string> = {
        loops: '#107c10',
        logic: '#006970',
        math: '#712672',
        images: '#5C2D91',
        variables: '#A80000',
        text: '#996600',
        arrays: '#A94400',
        advanced: '#3c3c3c'
    }

    export enum CategoryMode {
        All,
        None,
        Basic
    }

    const typeDefaults: Map<{ field: string, block: string, defaultValue: string }> = {
        "string": {
            field: "TEXT",
            block: "text",
            defaultValue: ""
        },
        "number": {
            field: "NUM",
            block: "math_number",
            defaultValue: "0"
        },
        "boolean": {
            field: "BOOL",
            block: "logic_boolean",
            defaultValue: "false"
        },
        "Array": {
            field: "VAR",
            block: "variables_get",
            defaultValue: "list"
        }
    }

    let usedBlocks: Map<boolean> = {};
    let updateUsedBlocks = false;

    // list of built-in blocks, should be touched.
    const builtinBlocks: Map<{
        block: B.BlockDefinition;
        symbol?: pxtc.SymbolInfo;
    }> = {};
    Object.keys(Blockly.Blocks)
        .forEach(k => builtinBlocks[k] = { block: Blockly.Blocks[k] });
    export const buildinBlockStatements: Map<boolean> = {
        "controls_if": true,
        "controls_for": true,
        "controls_simple_for": true,
        "controls_repeat_ext": true,
        "variables_set": true,
        "variables_change": true,
        "device_while": true
    }

    // blocks cached
    interface CachedBlock {
        hash: string;
        fn: pxtc.SymbolInfo;
        block: Blockly.BlockDefinition;
    }
    let cachedBlocks: Map<CachedBlock> = {};
    let searchElementCache: Map<Node> = {};

    export function blockSymbol(type: string): pxtc.SymbolInfo {
        let b = cachedBlocks[type];
        return b ? b.fn : undefined;
    }

    function createShadowValue(name: string, type: string, v?: string, shadowType?: string): Element {
        if (v && v.slice(0, 1) == "\"")
            v = JSON.parse(v);
        if (type == "number" && shadowType == "value") {
            const field = document.createElement("field");
            field.setAttribute("name", name);
            field.appendChild(document.createTextNode("0"));
            return field;
        }

        const value = document.createElement("value");
        value.setAttribute("name", name);

        const shadow = document.createElement(shadowType == "variables_get" ? "block" : "shadow");
        value.appendChild(shadow);

        const typeInfo = typeDefaults[type];

        shadow.setAttribute("type", shadowType || typeInfo && typeInfo.block || type);

        if (typeInfo) {
            const field = document.createElement("field");
            shadow.appendChild(field);
            field.setAttribute("name", shadowType == "variables_get" ? "VAR" : typeInfo.field);

            let value: Text;
            if (type == "boolean") {
                value = document.createTextNode((v || typeInfo.defaultValue).toUpperCase())
            }
            else {
                value = document.createTextNode(v || typeInfo.defaultValue)
            }

            field.appendChild(value);
        }

        return value;
    }

    function createToolboxBlock(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>): HTMLElement {
        //
        // toolbox update
        //
        let block = document.createElement("block");
        block.setAttribute("type", fn.attributes.blockId);
        if (fn.attributes.blockGap)
            block.setAttribute("gap", fn.attributes.blockGap);
        if ((fn.kind == pxtc.SymbolKind.Method || fn.kind == pxtc.SymbolKind.Property)
            && attrNames["this"]) {
            let attr = attrNames["this"];
            block.appendChild(createShadowValue(attr.name, attr.type, attr.shadowValue || attr.name, attr.shadowType || "variables_get"));
        }
        if (fn.parameters) {
            fn.parameters.filter(pr => !!attrNames[pr.name].name &&
                (/^(string|number|boolean)$/.test(attrNames[pr.name].type)
                    || !!attrNames[pr.name].shadowType
                    || !!attrNames[pr.name].shadowValue))
                .forEach(pr => {
                    let attr = attrNames[pr.name];
                    let shadowValue: Element;
                    let container: HTMLElement;
                    if (pr.options && pr.options['min'] && pr.options['max']) {
                        shadowValue = createShadowValue(attr.name, attr.type, attr.shadowValue, 'math_number_minmax');
                        container = document.createElement('mutation');
                        container.setAttribute('min', pr.options['min'].value);
                        container.setAttribute('max', pr.options['max'].value);
                    } else {
                        shadowValue = createShadowValue(attr.name, attr.type, attr.shadowValue, attr.shadowType);
                    }
                    if (pr.options && pr.options['fieldEditorOptions']) {
                        if (!container) container = document.createElement('mutation');
                        container.setAttribute(`customfield`, JSON.stringify(pr.options['fieldEditorOptions'].value));
                    }
                    if (shadowValue && container)
                        shadowValue.firstChild.appendChild(container);
                    block.appendChild(shadowValue);
                })
        }
        searchElementCache[fn.attributes.blockId] = block.cloneNode(true);
        return block;
    }

    function createCategoryElement(name: string, nameid: string, weight: number, colour?: string, iconClass?: string): Element {
        const result = document.createElement("category");
        result.setAttribute("name", name);
        result.setAttribute("nameid", nameid.toLowerCase());
        result.setAttribute("weight", weight.toString());
        if (colour) {
            result.setAttribute("colour", colour);
        }
        if (iconClass) {
            result.setAttribute("iconclass", iconClass);
            result.setAttribute("expandedclass", iconClass);
        }
        return result;
    }

    function injectToolbox(tb: Element, info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, block: HTMLElement, showCategories = CategoryMode.Basic) {
        // identity function are just a trick to get an enum drop down in the block
        // while allowing the parameter to be a number
        if (fn.attributes.blockHidden)
            return;

        if (!fn.attributes.deprecated) {
            let ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0];
            let nsn = info.apis.byQName[ns];
            let isAdvanced = nsn && nsn.attributes.advanced;

            if (nsn) ns = nsn.attributes.block || ns;
            let catName = ts.pxtc.blocksCategory(fn);
            if (nsn && nsn.name)
                catName = Util.capitalize(nsn.name);

            let category = categoryElement(tb, catName);

            if (showCategories === CategoryMode.All || showCategories == CategoryMode.Basic && !isAdvanced) {
                if (!category) {
                    let categories = getChildCategories(tb)
                    let parentCategoryList = tb;

                    pxt.debug('toolbox: adding category ' + ns)

                    const nsWeight = (nsn ? nsn.attributes.weight : 50) || 50;
                    const locCatName = Util.capitalize((nsn ? nsn.attributes.block : "") || catName);
                    category = createCategoryElement(locCatName, catName, nsWeight);

                    if (nsn && nsn.attributes.color) {
                        category.setAttribute("colour", nsn.attributes.color);
                    }
                    else if (getNamespaceColor(ns)) {
                        category.setAttribute("colour", getNamespaceColor(ns));
                    }
                    if (nsn && nsn.attributes.icon) {
                        const nsnIconClassName = `blocklyTreeIcon${nsn.name.toLowerCase()}`.replace(/\s/g, '');
                        appendToolboxIconCss(nsnIconClassName, nsn.attributes.icon);
                        category.setAttribute("iconclass", nsnIconClassName);
                        category.setAttribute("expandedclass", nsnIconClassName);
                    } else {
                        category.setAttribute("iconclass", `blocklyTreeIconDefault`);
                        category.setAttribute("expandedclass", `blocklyTreeIconDefault`);
                    }

                    insertTopLevelCategory(category, tb, nsWeight, isAdvanced);
                }
                if (fn.attributes.advanced) {
                    category = getOrAddSubcategoryByWeight(category, lf("More"), "More", 1, category.getAttribute("colour"), 'blocklyTreeIconmore')
                }
                else if (fn.attributes.subcategory) {
                    const sub = fn.attributes.subcategory;
                    const all = nsn.attributes.subcategories;
                    if (all && all.indexOf(sub) !== -1) {
                        // Respect the weights given by the package
                        const weight = 10000 - all.indexOf(sub);
                        category = getOrAddSubcategoryByWeight(category, sub, sub, weight, category.getAttribute("colour"), 'blocklyTreeIconmore')
                    }
                    else {
                        // If no weight is specified, insert alphabetically after the weighted subcategories but above "More"
                        category = getOrAddSubcategoryByName(category, sub, sub, category.getAttribute("colour"), 'blocklyTreeIconmore')
                    }
                }
            }

            if (showCategories === CategoryMode.Basic && isAdvanced) {
                const type = block.getAttribute("type");
                usedBlocks[type] = true;
            }

            if (fn.attributes.mutateDefaults) {
                const mutationValues = fn.attributes.mutateDefaults.split(";");
                mutationValues.forEach(mutation => {
                    const mutatedBlock = block.cloneNode(true);
                    mutateToolboxBlock(mutatedBlock, fn.attributes.mutate, mutation);
                    if (showCategories !== CategoryMode.None) {
                        category.appendChild(mutatedBlock);
                    } else {
                        tb.appendChild(mutatedBlock);
                    }
                });
            }
            else {
                if (showCategories !== CategoryMode.None && !(showCategories === CategoryMode.Basic && isAdvanced)) {
                    category.appendChild(block);
                    injectToolboxIconCss();
                } else if (showCategories === CategoryMode.None) {
                    tb.appendChild(block);
                }
            }
        }
    }

    let toolboxStyle: HTMLStyleElement;
    let toolboxStyleBuffer: string = '';
    export function appendToolboxIconCss(className: string, i: string): void {
        if (toolboxStyleBuffer.indexOf(className) > -1) return;

        if (i.length === 1) {
            const icon = Util.unicodeToChar(i);
            toolboxStyleBuffer += `
                .blocklyTreeIcon.${className}::before {
                    content: "${icon}";
                }
            `;
        }
        else {
            toolboxStyleBuffer += `
                .blocklyTreeIcon.${className} {
                    display: inline-block !important;
                    background-image: url("${pxt.webConfig.commitCdnUrl + encodeURI(i)}")!important;
                    width: 1em;
                    height: 1em;
                    background-size: 1em!important;
                }
            `;
        }
    }

    export function injectToolboxIconCss(): void {
        if (!toolboxStyle) {
            toolboxStyle = document.createElement('style');
            toolboxStyle.id = "blocklyToolboxIcons";
            toolboxStyle.type = 'text/css';
            let head = document.head || document.getElementsByTagName('head')[0];
            head.appendChild(toolboxStyle);
        }

        if (toolboxStyle.sheet) {
            toolboxStyle.textContent = toolboxStyleBuffer;
        } else {
            toolboxStyle.appendChild(document.createTextNode(toolboxStyleBuffer));
        }
    }

    let iconCanvasCache: Map<string> = {};
    function iconToFieldImage(c: string): Blockly.FieldImage {
        let url = iconCanvasCache[c];
        if (!url) {
            if (c.length === 1) {
                let canvas = document.createElement('canvas');
                canvas.width = 64;
                canvas.height = 64;
                let ctx = canvas.getContext('2d');
                ctx.fillStyle = 'white';
                ctx.font = "56px Icons";
                ctx.textAlign = "center";
                ctx.fillText(c, canvas.width / 2, 56);
                url = iconCanvasCache[c] = canvas.toDataURL();
            }
            else {
                url = pxt.webConfig.commitCdnUrl + encodeURI(c);
            }
        }
        return new Blockly.FieldImage(url, 16, 16, '');
    }

    function getChildCategories(parent: Element) {
        const elements = parent.querySelectorAll("category");
        const result: Element[] = [];

        for (let i = 0; i < elements.length; i++) {
            if (elements[i].parentNode === parent) { // IE11: no parentElement
                result.push(elements[i])
            }
        }

        return result;
    }

    function insertTopLevelCategory(category: Element, tb: Element, nsWeight: number, isAdvanced: boolean) {
        let categories = getChildCategories(tb);
        if (isAdvanced) {
            category.setAttribute("advanced", "true");
        }

        // Insert the category based on weight
        let ci = 0;
        for (ci = 0; ci < categories.length; ++ci) {
            let cat = categories[ci];

            // Advanced categories always come last
            if (isAdvanced) {
                if (!cat.hasAttribute("advanced")) {
                    continue;
                }
            }
            else if (cat.hasAttribute("advanced")) {
                tb.insertBefore(category, cat);
                break;
            }

            if (parseInt(cat.getAttribute("weight") || "50") < nsWeight) {
                tb.insertBefore(category, cat);
                break;
            }
        }
        if (ci == categories.length)
            tb.appendChild(category);
    }

    function getOrAddSubcategoryByWeight(parent: Element, name: string, nameid: string, weight: number, colour?: string, iconClass?: string) {
        const existing = parent.querySelector(`category[nameid="${nameid.toLowerCase()}"]`);
        if (existing) {
            return existing;
        }

        const newCategory = createCategoryElement(name, nameid, weight, colour, iconClass);
        const siblings = parent.querySelectorAll("category");

        let ci = 0;
        for (ci = 0; ci < siblings.length; ++ci) {
            let cat = siblings[ci];
            if (parseInt(cat.getAttribute("weight") || "50") < weight) {
                parent.insertBefore(newCategory, cat);
                break;
            }
        }
        if (ci == siblings.length)
            parent.appendChild(newCategory);

        return newCategory;
    }

    function getOrAddSubcategoryByName(parent: Element, name: string, nameid: string, colour?: string, iconClass?: string) {
        const existing = parent.querySelector(`category[nameid="${nameid.toLowerCase()}"]`);
        if (existing) {
            return existing;
        }

        const newCategory = createCategoryElement(name, nameid, 100, colour, iconClass);

        const siblings = parent.querySelectorAll("category");
        const filtered: Element[] = [];

        let ci = 0;
        let inserted = false;
        let last: Element = undefined;
        for (ci = 0; ci < siblings.length; ++ci) {
            let cat = siblings[ci];
            const sibWeight = parseInt(cat.getAttribute("weight") || "50")

            if (sibWeight >= 1000) {
                continue;
            }
            else if (sibWeight === 1) {
                last = cat;
                break;
            }

            filtered.push(cat);

            if (!inserted && cat.getAttribute("name").localeCompare(name) >= 0) {
                parent.insertBefore(newCategory, cat);
                filtered.splice(filtered.length - 1, 0, newCategory);
                inserted = true;
            }
        }

        if (!inserted) {
            filtered.push(newCategory);

            if (last) {
                parent.insertBefore(newCategory, last);
            }
            else {
                parent.appendChild(newCategory);
            }
        }

        filtered.forEach((e, i) => {
            e.setAttribute("weight", (200 - i).toString());
        });

        return newCategory;
    }

    function injectBlockDefinition(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>, blockXml: HTMLElement): boolean {
        let id = fn.attributes.blockId;

        if (builtinBlocks[id]) {
            pxt.reportError("blocks", 'trying to override builtin block', { "details": id });
            return false;
        }

        let hash = JSON.stringify(fn);
        if (cachedBlocks[id] && cachedBlocks[id].hash == hash) {
            return true;
        }

        if (Blockly.Blocks[fn.attributes.blockId]) {
            console.error("duplicate block definition: " + id);
            return false;
        }

        let cachedBlock: CachedBlock = {
            hash: hash,
            fn: fn,
            block: {
                codeCard: mkCard(fn, blockXml),
                init: function () { initBlock(this, info, fn, attrNames) }
            }
        }

        cachedBlocks[id] = cachedBlock;
        Blockly.Blocks[id] = cachedBlock.block;

        return true;
    }

    function initField(i: any, ni: number, fn: pxtc.SymbolInfo, ns: pxtc.SymbolInfo, pre: string, right?: boolean, type?: string, nsinfo?: pxtc.SymbolInfo): any {
        if (ni == 0 && !pxt.appTarget.appTheme.disableBlockIcons) {
            const icon = ns && ns.attributes.icon ? ns.attributes.icon : null;
            if (icon)
                i.appendField(iconToFieldImage(icon));
        }
        if (pre)
            i.appendField(pre);
        if (right)
            i.setAlign(Blockly.ALIGN_RIGHT)
        // ignore generic types
        if (type && type != "T")
            i.setCheck(type);
        return i;
    }

    function cleanOuterHTML(el: HTMLElement): string {
        // remove IE11 junk
        return el.outerHTML.replace(/^<\?[^>]*>/, '');
    }

    function mkCard(fn: pxtc.SymbolInfo, blockXml: HTMLElement): pxt.CodeCard {
        return {
            name: fn.namespace + '.' + fn.name,
            shortName: fn.name,
            description: fn.attributes.jsDoc,
            url: fn.attributes.help ? 'reference/' + fn.attributes.help.replace(/^\//, '') : undefined,
            blocksXml: `<xml xmlns="http://www.w3.org/1999/xhtml">${cleanOuterHTML(blockXml)}</xml>`,
        }
    }

    function isSubtype(apis: pxtc.ApisInfo, specific: string, general: string) {
        if (specific == general) return true
        let inf = apis.byQName[specific]
        if (inf && inf.extendsTypes)
            return inf.extendsTypes.indexOf(general) >= 0
        return false
    }

    function initBlock(block: Blockly.Block, info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>) {
        const ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0];
        const instance = fn.kind == pxtc.SymbolKind.Method || fn.kind == pxtc.SymbolKind.Property;
        const nsinfo = info.apis.byQName[ns];
        const color =
            fn.attributes.color
            || (nsinfo ? nsinfo.attributes.color : undefined)
            || getNamespaceColor(ns.toLowerCase())
            || 255;

        if (fn.attributes.help)
            block.setHelpUrl("/reference/" + fn.attributes.help.replace(/^\//, ''));
        else if (fn.pkg && !pxt.appTarget.bundledpkgs[fn.pkg]) {// added package
            let anchor = fn.qName.toLowerCase().split('.');
            if (anchor[0] == fn.pkg) anchor.shift();
            block.setHelpUrl(`/pkg/${fn.pkg}#${encodeURIComponent(anchor.join('-'))}`)
        }

        block.setTooltip(fn.attributes.jsDoc);
        block.setColour(color);

        parseFields(fn.attributes.block).map(field => {
            let i: any;
            if (!field.p) {
                i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, field.n);
            } else {
                // find argument
                let pre = field.pre;
                let p = field.p;
                let n = Object.keys(attrNames).filter(k => attrNames[k].name == p)[0];
                if (!n) {
                    console.error("block " + fn.attributes.blockId + ": unkown parameter " + p);
                    return;
                }
                let pr = attrNames[n];
                let typeInfo = U.lookup(info.apis.byQName, pr.type)

                let isEnum = typeInfo && typeInfo.kind == pxtc.SymbolKind.Enum
                let isFixed = typeInfo && !!typeInfo.attributes.fixedInstances
                let customField = (fn.attributes.paramFieldEditor && fn.attributes.paramFieldEditor[p]);

                if (isEnum || isFixed) {
                    const syms = Util.values(info.apis.byQName)
                        .filter(e =>
                            isEnum ? e.namespace == pr.type
                                : (e.kind == pxtc.SymbolKind.Variable
                                    && e.attributes.fixedInstance
                                    && isSubtype(info.apis, e.retType, typeInfo.qName)))
                    if (syms.length == 0) {
                        console.error(`no instances of ${typeInfo.qName} found`)
                    }
                    const dd = syms.map(v => {
                        const k = v.attributes.block || v.attributes.blockId || v.name;
                        return [
                            v.attributes.blockImage ? {
                                src: pxt.webConfig.commitCdnUrl + `blocks/${v.namespace.toLowerCase()}/${v.name.toLowerCase()}.png`,
                                alt: k,
                                width: 32,
                                height: 32
                            } : k,
                            v.namespace + "." + v.name
                        ];
                    });
                    i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, pre, true);
                    // if a value is provided, move it first
                    if (pr.shadowValue)
                        dd.sort((v1, v2) => v1[1] == pr.shadowValue ? -1 : v2[1] == pr.shadowValue ? 1 : 0);

                    if (customField) {
                        let defl = fn.attributes.paramDefl[pr.name] || "";
                        const options = {
                            data: dd,
                            colour: color
                        } as Blockly.FieldCustomDropdownOptions;
                        Util.jsonMergeFrom(options, fn.attributes.paramFieldEditorOptions && fn.attributes.paramFieldEditorOptions[pr.name] || {});
                        i.appendField(createFieldEditor(customField, defl, options), attrNames[n].name);
                    }
                    else
                        i.appendField(new Blockly.FieldDropdown(dd), attrNames[n].name);

                } else if (customField) {
                    i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, pre, true);
                    const defl = fn.attributes.paramDefl[pr.name] || "";
                    const options = {<commit_msg>Fix logic for showing shadowvalue at the top of dropdown (#2096)<commit_after>/// <reference path="../localtypings/blockly.d.ts" />
/// <reference path="../built/pxtlib.d.ts" />
import Util = pxt.Util;

let lf = Util.lf;

namespace pxt.blocks {
    export const blockColors: Map<number | string> = {
        loops: '#107c10',
        logic: '#006970',
        math: '#712672',
        images: '#5C2D91',
        variables: '#A80000',
        text: '#996600',
        arrays: '#A94400',
        advanced: '#3c3c3c'
    }

    export enum CategoryMode {
        All,
        None,
        Basic
    }

    const typeDefaults: Map<{ field: string, block: string, defaultValue: string }> = {
        "string": {
            field: "TEXT",
            block: "text",
            defaultValue: ""
        },
        "number": {
            field: "NUM",
            block: "math_number",
            defaultValue: "0"
        },
        "boolean": {
            field: "BOOL",
            block: "logic_boolean",
            defaultValue: "false"
        },
        "Array": {
            field: "VAR",
            block: "variables_get",
            defaultValue: "list"
        }
    }

    let usedBlocks: Map<boolean> = {};
    let updateUsedBlocks = false;

    // list of built-in blocks, should be touched.
    const builtinBlocks: Map<{
        block: B.BlockDefinition;
        symbol?: pxtc.SymbolInfo;
    }> = {};
    Object.keys(Blockly.Blocks)
        .forEach(k => builtinBlocks[k] = { block: Blockly.Blocks[k] });
    export const buildinBlockStatements: Map<boolean> = {
        "controls_if": true,
        "controls_for": true,
        "controls_simple_for": true,
        "controls_repeat_ext": true,
        "variables_set": true,
        "variables_change": true,
        "device_while": true
    }

    // blocks cached
    interface CachedBlock {
        hash: string;
        fn: pxtc.SymbolInfo;
        block: Blockly.BlockDefinition;
    }
    let cachedBlocks: Map<CachedBlock> = {};
    let searchElementCache: Map<Node> = {};

    export function blockSymbol(type: string): pxtc.SymbolInfo {
        let b = cachedBlocks[type];
        return b ? b.fn : undefined;
    }

    function createShadowValue(name: string, type: string, v?: string, shadowType?: string): Element {
        if (v && v.slice(0, 1) == "\"")
            v = JSON.parse(v);
        if (type == "number" && shadowType == "value") {
            const field = document.createElement("field");
            field.setAttribute("name", name);
            field.appendChild(document.createTextNode("0"));
            return field;
        }

        const value = document.createElement("value");
        value.setAttribute("name", name);

        const shadow = document.createElement(shadowType == "variables_get" ? "block" : "shadow");
        value.appendChild(shadow);

        const typeInfo = typeDefaults[type];

        shadow.setAttribute("type", shadowType || typeInfo && typeInfo.block || type);

        if (typeInfo) {
            const field = document.createElement("field");
            shadow.appendChild(field);
            field.setAttribute("name", shadowType == "variables_get" ? "VAR" : typeInfo.field);

            let value: Text;
            if (type == "boolean") {
                value = document.createTextNode((v || typeInfo.defaultValue).toUpperCase())
            }
            else {
                value = document.createTextNode(v || typeInfo.defaultValue)
            }

            field.appendChild(value);
        }

        return value;
    }

    function createToolboxBlock(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>): HTMLElement {
        //
        // toolbox update
        //
        let block = document.createElement("block");
        block.setAttribute("type", fn.attributes.blockId);
        if (fn.attributes.blockGap)
            block.setAttribute("gap", fn.attributes.blockGap);
        if ((fn.kind == pxtc.SymbolKind.Method || fn.kind == pxtc.SymbolKind.Property)
            && attrNames["this"]) {
            let attr = attrNames["this"];
            block.appendChild(createShadowValue(attr.name, attr.type, attr.shadowValue || attr.name, attr.shadowType || "variables_get"));
        }
        if (fn.parameters) {
            fn.parameters.filter(pr => !!attrNames[pr.name].name &&
                (/^(string|number|boolean)$/.test(attrNames[pr.name].type)
                    || !!attrNames[pr.name].shadowType
                    || !!attrNames[pr.name].shadowValue))
                .forEach(pr => {
                    let attr = attrNames[pr.name];
                    let shadowValue: Element;
                    let container: HTMLElement;
                    if (pr.options && pr.options['min'] && pr.options['max']) {
                        shadowValue = createShadowValue(attr.name, attr.type, attr.shadowValue, 'math_number_minmax');
                        container = document.createElement('mutation');
                        container.setAttribute('min', pr.options['min'].value);
                        container.setAttribute('max', pr.options['max'].value);
                    } else {
                        shadowValue = createShadowValue(attr.name, attr.type, attr.shadowValue, attr.shadowType);
                    }
                    if (pr.options && pr.options['fieldEditorOptions']) {
                        if (!container) container = document.createElement('mutation');
                        container.setAttribute(`customfield`, JSON.stringify(pr.options['fieldEditorOptions'].value));
                    }
                    if (shadowValue && container)
                        shadowValue.firstChild.appendChild(container);
                    block.appendChild(shadowValue);
                })
        }
        searchElementCache[fn.attributes.blockId] = block.cloneNode(true);
        return block;
    }

    function createCategoryElement(name: string, nameid: string, weight: number, colour?: string, iconClass?: string): Element {
        const result = document.createElement("category");
        result.setAttribute("name", name);
        result.setAttribute("nameid", nameid.toLowerCase());
        result.setAttribute("weight", weight.toString());
        if (colour) {
            result.setAttribute("colour", colour);
        }
        if (iconClass) {
            result.setAttribute("iconclass", iconClass);
            result.setAttribute("expandedclass", iconClass);
        }
        return result;
    }

    function injectToolbox(tb: Element, info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, block: HTMLElement, showCategories = CategoryMode.Basic) {
        // identity function are just a trick to get an enum drop down in the block
        // while allowing the parameter to be a number
        if (fn.attributes.blockHidden)
            return;

        if (!fn.attributes.deprecated) {
            let ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0];
            let nsn = info.apis.byQName[ns];
            let isAdvanced = nsn && nsn.attributes.advanced;

            if (nsn) ns = nsn.attributes.block || ns;
            let catName = ts.pxtc.blocksCategory(fn);
            if (nsn && nsn.name)
                catName = Util.capitalize(nsn.name);

            let category = categoryElement(tb, catName);

            if (showCategories === CategoryMode.All || showCategories == CategoryMode.Basic && !isAdvanced) {
                if (!category) {
                    let categories = getChildCategories(tb)
                    let parentCategoryList = tb;

                    pxt.debug('toolbox: adding category ' + ns)

                    const nsWeight = (nsn ? nsn.attributes.weight : 50) || 50;
                    const locCatName = Util.capitalize((nsn ? nsn.attributes.block : "") || catName);
                    category = createCategoryElement(locCatName, catName, nsWeight);

                    if (nsn && nsn.attributes.color) {
                        category.setAttribute("colour", nsn.attributes.color);
                    }
                    else if (getNamespaceColor(ns)) {
                        category.setAttribute("colour", getNamespaceColor(ns));
                    }
                    if (nsn && nsn.attributes.icon) {
                        const nsnIconClassName = `blocklyTreeIcon${nsn.name.toLowerCase()}`.replace(/\s/g, '');
                        appendToolboxIconCss(nsnIconClassName, nsn.attributes.icon);
                        category.setAttribute("iconclass", nsnIconClassName);
                        category.setAttribute("expandedclass", nsnIconClassName);
                    } else {
                        category.setAttribute("iconclass", `blocklyTreeIconDefault`);
                        category.setAttribute("expandedclass", `blocklyTreeIconDefault`);
                    }

                    insertTopLevelCategory(category, tb, nsWeight, isAdvanced);
                }
                if (fn.attributes.advanced) {
                    category = getOrAddSubcategoryByWeight(category, lf("More"), "More", 1, category.getAttribute("colour"), 'blocklyTreeIconmore')
                }
                else if (fn.attributes.subcategory) {
                    const sub = fn.attributes.subcategory;
                    const all = nsn.attributes.subcategories;
                    if (all && all.indexOf(sub) !== -1) {
                        // Respect the weights given by the package
                        const weight = 10000 - all.indexOf(sub);
                        category = getOrAddSubcategoryByWeight(category, sub, sub, weight, category.getAttribute("colour"), 'blocklyTreeIconmore')
                    }
                    else {
                        // If no weight is specified, insert alphabetically after the weighted subcategories but above "More"
                        category = getOrAddSubcategoryByName(category, sub, sub, category.getAttribute("colour"), 'blocklyTreeIconmore')
                    }
                }
            }

            if (showCategories === CategoryMode.Basic && isAdvanced) {
                const type = block.getAttribute("type");
                usedBlocks[type] = true;
            }

            if (fn.attributes.mutateDefaults) {
                const mutationValues = fn.attributes.mutateDefaults.split(";");
                mutationValues.forEach(mutation => {
                    const mutatedBlock = block.cloneNode(true);
                    mutateToolboxBlock(mutatedBlock, fn.attributes.mutate, mutation);
                    if (showCategories !== CategoryMode.None) {
                        category.appendChild(mutatedBlock);
                    } else {
                        tb.appendChild(mutatedBlock);
                    }
                });
            }
            else {
                if (showCategories !== CategoryMode.None && !(showCategories === CategoryMode.Basic && isAdvanced)) {
                    category.appendChild(block);
                    injectToolboxIconCss();
                } else if (showCategories === CategoryMode.None) {
                    tb.appendChild(block);
                }
            }
        }
    }

    let toolboxStyle: HTMLStyleElement;
    let toolboxStyleBuffer: string = '';
    export function appendToolboxIconCss(className: string, i: string): void {
        if (toolboxStyleBuffer.indexOf(className) > -1) return;

        if (i.length === 1) {
            const icon = Util.unicodeToChar(i);
            toolboxStyleBuffer += `
                .blocklyTreeIcon.${className}::before {
                    content: "${icon}";
                }
            `;
        }
        else {
            toolboxStyleBuffer += `
                .blocklyTreeIcon.${className} {
                    display: inline-block !important;
                    background-image: url("${pxt.webConfig.commitCdnUrl + encodeURI(i)}")!important;
                    width: 1em;
                    height: 1em;
                    background-size: 1em!important;
                }
            `;
        }
    }

    export function injectToolboxIconCss(): void {
        if (!toolboxStyle) {
            toolboxStyle = document.createElement('style');
            toolboxStyle.id = "blocklyToolboxIcons";
            toolboxStyle.type = 'text/css';
            let head = document.head || document.getElementsByTagName('head')[0];
            head.appendChild(toolboxStyle);
        }

        if (toolboxStyle.sheet) {
            toolboxStyle.textContent = toolboxStyleBuffer;
        } else {
            toolboxStyle.appendChild(document.createTextNode(toolboxStyleBuffer));
        }
    }

    let iconCanvasCache: Map<string> = {};
    function iconToFieldImage(c: string): Blockly.FieldImage {
        let url = iconCanvasCache[c];
        if (!url) {
            if (c.length === 1) {
                let canvas = document.createElement('canvas');
                canvas.width = 64;
                canvas.height = 64;
                let ctx = canvas.getContext('2d');
                ctx.fillStyle = 'white';
                ctx.font = "56px Icons";
                ctx.textAlign = "center";
                ctx.fillText(c, canvas.width / 2, 56);
                url = iconCanvasCache[c] = canvas.toDataURL();
            }
            else {
                url = pxt.webConfig.commitCdnUrl + encodeURI(c);
            }
        }
        return new Blockly.FieldImage(url, 16, 16, '');
    }

    function getChildCategories(parent: Element) {
        const elements = parent.querySelectorAll("category");
        const result: Element[] = [];

        for (let i = 0; i < elements.length; i++) {
            if (elements[i].parentNode === parent) { // IE11: no parentElement
                result.push(elements[i])
            }
        }

        return result;
    }

    function insertTopLevelCategory(category: Element, tb: Element, nsWeight: number, isAdvanced: boolean) {
        let categories = getChildCategories(tb);
        if (isAdvanced) {
            category.setAttribute("advanced", "true");
        }

        // Insert the category based on weight
        let ci = 0;
        for (ci = 0; ci < categories.length; ++ci) {
            let cat = categories[ci];

            // Advanced categories always come last
            if (isAdvanced) {
                if (!cat.hasAttribute("advanced")) {
                    continue;
                }
            }
            else if (cat.hasAttribute("advanced")) {
                tb.insertBefore(category, cat);
                break;
            }

            if (parseInt(cat.getAttribute("weight") || "50") < nsWeight) {
                tb.insertBefore(category, cat);
                break;
            }
        }
        if (ci == categories.length)
            tb.appendChild(category);
    }

    function getOrAddSubcategoryByWeight(parent: Element, name: string, nameid: string, weight: number, colour?: string, iconClass?: string) {
        const existing = parent.querySelector(`category[nameid="${nameid.toLowerCase()}"]`);
        if (existing) {
            return existing;
        }

        const newCategory = createCategoryElement(name, nameid, weight, colour, iconClass);
        const siblings = parent.querySelectorAll("category");

        let ci = 0;
        for (ci = 0; ci < siblings.length; ++ci) {
            let cat = siblings[ci];
            if (parseInt(cat.getAttribute("weight") || "50") < weight) {
                parent.insertBefore(newCategory, cat);
                break;
            }
        }
        if (ci == siblings.length)
            parent.appendChild(newCategory);

        return newCategory;
    }

    function getOrAddSubcategoryByName(parent: Element, name: string, nameid: string, colour?: string, iconClass?: string) {
        const existing = parent.querySelector(`category[nameid="${nameid.toLowerCase()}"]`);
        if (existing) {
            return existing;
        }

        const newCategory = createCategoryElement(name, nameid, 100, colour, iconClass);

        const siblings = parent.querySelectorAll("category");
        const filtered: Element[] = [];

        let ci = 0;
        let inserted = false;
        let last: Element = undefined;
        for (ci = 0; ci < siblings.length; ++ci) {
            let cat = siblings[ci];
            const sibWeight = parseInt(cat.getAttribute("weight") || "50")

            if (sibWeight >= 1000) {
                continue;
            }
            else if (sibWeight === 1) {
                last = cat;
                break;
            }

            filtered.push(cat);

            if (!inserted && cat.getAttribute("name").localeCompare(name) >= 0) {
                parent.insertBefore(newCategory, cat);
                filtered.splice(filtered.length - 1, 0, newCategory);
                inserted = true;
            }
        }

        if (!inserted) {
            filtered.push(newCategory);

            if (last) {
                parent.insertBefore(newCategory, last);
            }
            else {
                parent.appendChild(newCategory);
            }
        }

        filtered.forEach((e, i) => {
            e.setAttribute("weight", (200 - i).toString());
        });

        return newCategory;
    }

    function injectBlockDefinition(info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>, blockXml: HTMLElement): boolean {
        let id = fn.attributes.blockId;

        if (builtinBlocks[id]) {
            pxt.reportError("blocks", 'trying to override builtin block', { "details": id });
            return false;
        }

        let hash = JSON.stringify(fn);
        if (cachedBlocks[id] && cachedBlocks[id].hash == hash) {
            return true;
        }

        if (Blockly.Blocks[fn.attributes.blockId]) {
            console.error("duplicate block definition: " + id);
            return false;
        }

        let cachedBlock: CachedBlock = {
            hash: hash,
            fn: fn,
            block: {
                codeCard: mkCard(fn, blockXml),
                init: function () { initBlock(this, info, fn, attrNames) }
            }
        }

        cachedBlocks[id] = cachedBlock;
        Blockly.Blocks[id] = cachedBlock.block;

        return true;
    }

    function initField(i: any, ni: number, fn: pxtc.SymbolInfo, ns: pxtc.SymbolInfo, pre: string, right?: boolean, type?: string, nsinfo?: pxtc.SymbolInfo): any {
        if (ni == 0 && !pxt.appTarget.appTheme.disableBlockIcons) {
            const icon = ns && ns.attributes.icon ? ns.attributes.icon : null;
            if (icon)
                i.appendField(iconToFieldImage(icon));
        }
        if (pre)
            i.appendField(pre);
        if (right)
            i.setAlign(Blockly.ALIGN_RIGHT)
        // ignore generic types
        if (type && type != "T")
            i.setCheck(type);
        return i;
    }

    function cleanOuterHTML(el: HTMLElement): string {
        // remove IE11 junk
        return el.outerHTML.replace(/^<\?[^>]*>/, '');
    }

    function mkCard(fn: pxtc.SymbolInfo, blockXml: HTMLElement): pxt.CodeCard {
        return {
            name: fn.namespace + '.' + fn.name,
            shortName: fn.name,
            description: fn.attributes.jsDoc,
            url: fn.attributes.help ? 'reference/' + fn.attributes.help.replace(/^\//, '') : undefined,
            blocksXml: `<xml xmlns="http://www.w3.org/1999/xhtml">${cleanOuterHTML(blockXml)}</xml>`,
        }
    }

    function isSubtype(apis: pxtc.ApisInfo, specific: string, general: string) {
        if (specific == general) return true
        let inf = apis.byQName[specific]
        if (inf && inf.extendsTypes)
            return inf.extendsTypes.indexOf(general) >= 0
        return false
    }

    function initBlock(block: Blockly.Block, info: pxtc.BlocksInfo, fn: pxtc.SymbolInfo, attrNames: Map<BlockParameter>) {
        const ns = (fn.attributes.blockNamespace || fn.namespace).split('.')[0];
        const instance = fn.kind == pxtc.SymbolKind.Method || fn.kind == pxtc.SymbolKind.Property;
        const nsinfo = info.apis.byQName[ns];
        const color =
            fn.attributes.color
            || (nsinfo ? nsinfo.attributes.color : undefined)
            || getNamespaceColor(ns.toLowerCase())
            || 255;

        if (fn.attributes.help)
            block.setHelpUrl("/reference/" + fn.attributes.help.replace(/^\//, ''));
        else if (fn.pkg && !pxt.appTarget.bundledpkgs[fn.pkg]) {// added package
            let anchor = fn.qName.toLowerCase().split('.');
            if (anchor[0] == fn.pkg) anchor.shift();
            block.setHelpUrl(`/pkg/${fn.pkg}#${encodeURIComponent(anchor.join('-'))}`)
        }

        block.setTooltip(fn.attributes.jsDoc);
        block.setColour(color);

        parseFields(fn.attributes.block).map(field => {
            let i: any;
            if (!field.p) {
                i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, field.n);
            } else {
                // find argument
                let pre = field.pre;
                let p = field.p;
                let n = Object.keys(attrNames).filter(k => attrNames[k].name == p)[0];
                if (!n) {
                    console.error("block " + fn.attributes.blockId + ": unkown parameter " + p);
                    return;
                }
                let pr = attrNames[n];
                let typeInfo = U.lookup(info.apis.byQName, pr.type)

                let isEnum = typeInfo && typeInfo.kind == pxtc.SymbolKind.Enum
                let isFixed = typeInfo && !!typeInfo.attributes.fixedInstances
                let customField = (fn.attributes.paramFieldEditor && fn.attributes.paramFieldEditor[p]);

                if (isEnum || isFixed) {
                    const syms = Util.values(info.apis.byQName)
                        .filter(e =>
                            isEnum ? e.namespace == pr.type
                                : (e.kind == pxtc.SymbolKind.Variable
                                    && e.attributes.fixedInstance
                                    && isSubtype(info.apis, e.retType, typeInfo.qName)))
                    if (syms.length == 0) {
                        console.error(`no instances of ${typeInfo.qName} found`)
                    }
                    const dd = syms.map(v => {
                        const k = v.attributes.block || v.attributes.blockId || v.name;
                        return [
                            v.attributes.blockImage ? {
                                src: pxt.webConfig.commitCdnUrl + `blocks/${v.namespace.toLowerCase()}/${v.name.toLowerCase()}.png`,
                                alt: k,
                                width: 32,
                                height: 32
                            } : k,
                            v.namespace + "." + v.name
                        ];
                    });
                    i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, pre, true);
                    // if a value is provided, move it first
                    if (pr.shadowValue) {
                        let shadowValueIndex = -1;
                        dd.some((v, i) => {
                            if (v[1] === pr.shadowValue) {
                                shadowValueIndex = i;
                                return true;
                            }
                            return false;
                        });
                        if (shadowValueIndex > -1) {
                            const shadowValue = dd.splice(shadowValueIndex, 1)[0];
                            dd.unshift(shadowValue);
                        }
                    }

                    if (customField) {
                        let defl = fn.attributes.paramDefl[pr.name] || "";
                        const options = {
                            data: dd,
                            colour: color
                        } as Blockly.FieldCustomDropdownOptions;
                        Util.jsonMergeFrom(options, fn.attributes.paramFieldEditorOptions && fn.attributes.paramFieldEditorOptions[pr.name] || {});
                        i.appendField(createFieldEditor(customField, defl, options), attrNames[n].name);
                    }
                    else
                        i.appendField(new Blockly.FieldDropdown(dd), attrNames[n].name);

                } else if (customField) {
                    i = initField(block.appendDummyInput(), field.ni, fn, nsinfo, pre, true);
                    const defl = fn.attributes.paramDefl[pr.name] || "";
                    const options = {
{'fraction_non_alphanumeric': 0.12017997475467242, 'fraction_numerical': 0.005496966488863553, 'mean_word_length': 1.6231775700934579, 'pattern_counts': {'":': 26, '<': 47, '<?xml version=': 0, '>': 66, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6582230', 'n_tokens_mistral': 13141, 'n_tokens_neox': 12535, 'n_words': 2639}