utils.js

/**
 * @module
 */
import Payload from "./Payload.js"

/**
 * Load uglify from unpkg.com, required by Wrapper_JS.minify()
 * @returns {Function} the minify function from uglifiyjs
 */
export async function loadUglify() {
    // Uglify is not really made to run in the browser
    // We need to load each file in the right order or it wont work

    if (window.minify) {
        // already loaded
        return
    }
    const loadScript = (src) => {
        return new Promise(resolve => {
            const script = document.createElement("script")
            script.async = false
            script.defer = true
            script.src = src
            script.onload = resolve
            document.body.appendChild(script)

        })
    }

    const files = [
        "minify.js",
        "utils.js",
        "ast.js",
        "parse.js",
        "transform.js",
        "scope.js",
        "output.js",
        "compress.js",
        "propmangle.js",
    ]


    await Promise.all(files.map(filename => loadScript(`https://unpkg.com/uglify-js@3.13.0/lib/${filename}`)))
    return window.minify
}

// poor man jquery
export const $ = (s) => document.querySelector(s);
export const $$ = (s) => Array.from(document.querySelectorAll(s));

/**
 * Create an html element with given attributes and children element
 * @param {String} tag Tag name for the element 
 * @param {Object} [attr=null] Attributes to add to the elements
 * @param {Array} [children=[]] Array of HTMLElement to add as children
 * @returns {HTMLElement} The created html element
 */
export function createElement(tag, attr = null, children = []) {
    const el = document.createElement(tag)
    if (attr !== null) {
        if ("style" in attr) {
            Object.assign(el.style, attr.style)
            delete attr.style
        }
        Object.assign(el, attr)
    }
    for (const child of children) {
        el.appendChild(child)
    }
    return el
}

export function isCallable(f) {
    return typeof f === "function" || f instanceof Payload
}

export function repr(s) {
    if (isCallable(s)) {
        return s.toString()
    }
    if (Array.isArray(s) && s.every(f => isCallable(f))) {
        return `[${s.map(f => f.toString()).join(",")}]`
    }
    // Escape / because </script> can break syntax if some cases
    return JSON.stringify(s).replace("/", "\\/")
}

/**
 * Create an url with get parameters
 *  
 * @param {String} url Base url
 * @param {Object} urlParams object representing parameters
 * @param {String} [hash] Hash to add  
 * @returns {String} The full url with parameters and hash
 */
export function buildUrl(url, urlParams={}, hash = null) {
    const searchParams = new URLSearchParams(urlParams).toString()
    const separator = (url.includes("?") ? "&" : "?")
    const fullHash = hash ? "#" + hash : ""
    return url + separator + searchParams + fullHash
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
export function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// sleep in ms
export const sleep = (ms) => new Promise((r) => window.setTimeout(r, ms));

/** 
 * @alias SandboxAttributes
 * @typedef {Object} SandboxAttributes
 * @property {Boolean} [allow-downloads-without-user-activation=true],
 * @property {Boolean} [allow-downloads=true],
 * @property {Boolean} [allow-forms=true],
 * @property {Boolean} [allow-modals=true],
 * @property {Boolean} [allow-orientation-lock=true],
 * @property {Boolean} [allow-pointer-lock=true],
 * @property {Boolean} [allow-popups=true],
 * @property {Boolean} [allow-popups-to-escape-sandbox=true],
 * @property {Boolean} [allow-presentation=true],
 * @property {Boolean} [allow-same-origin=true],
 * @property {Boolean} [allow-scripts=true],
 * @property {Boolean} [allow-storage-access-by-user-activation=true],
 * @property {Boolean} [allow-top-navigation=true],
 * @property {Boolean} [allow-top-navigation-by-user-activation=true],
 */



/**
 * Create a new iframe element
 *
 * @param {string} url Url for the frame
 * @param {string} [name=null] name of the frame
 * @param {SandboxAttributes} sandbox reverse sandbox option
 * @returns {HTMLElement} The iframe created
 */
export function createFrame(url, name = null, sandbox = {}) {
    const allowAll = {
        "allow-downloads-without-user-activation": true,
        "allow-downloads": true,
        "allow-forms": true,
        "allow-modals": true,
        "allow-orientation-lock": true,
        "allow-pointer-lock": true,
        "allow-popups": true,
        "allow-popups-to-escape-sandbox": true,
        "allow-presentation": true,
        "allow-same-origin": true,
        "allow-scripts": true,
        "allow-storage-access-by-user-activation": true,
        "allow-top-navigation": true,
        "allow-top-navigation-by-user-activation": true,
    };
    const sandboxAttr = Object.entries({ ...allowAll, ...sandbox })
        .filter(([k, v]) => v)
        .map(([k, v]) => k)
        .join(" ");

    return createElement("iframe", {
        sandbox: sandboxAttr,
        name,
        src: url,
    });
}

/**
 * Return a random integer between min and max
 * @param {number} min lower bound
 * @param {number} max upper bound
 * @returns {number}
 */
export function randInt(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}