js/utils.js

/**
 * Various utilities.
 *
 * @module utils
 */

let cookies;

/**
 * Parses an Expression to extract all function calls and their argument arrays.
 *
 * @static
 * @param {string} expr - The expression to search
 * @param {string} func - The function name to search for
 * @return {Array<Array<string, any>>} The result array, where each result is an array containing the function call and array of arguments.
 */
function parseFunctionFromExpression( expr, func ) {
    let result;
    const findFunc = new RegExp( `${func}\\s*\\(`, 'g' );
    const results = [];

    if ( !expr || !func ) {
        return results;
    }

    while ( ( result = findFunc.exec( expr ) ) !== null ) {
        const args = [];
        let openBrackets = 1;
        let start = result.index;
        let argStart = findFunc.lastIndex;
        let index = argStart - 1;
        while ( openBrackets !== 0 && index < expr.length ) {
            index++;
            if ( expr[ index ] === '(' ) {
                openBrackets++;
            } else if ( expr[ index ] === ')' ) {
                openBrackets--;
            } else if ( expr[ index ] === ',' && openBrackets === 1 ) {
                args.push( expr.substring( argStart, index ).trim() );
                argStart = index + 1;
            }
        }
        // add last argument
        if ( argStart < index ) {
            args.push( expr.substring( argStart, index ).trim() );
        }

        // add [ 'function( a ,b)', ['a','b'] ] to result array
        results.push( [ expr.substring( start, index + 1 ), args ] );
    }

    return results;
}

/**
 * @static
 * @param {string} str - original string
 * @return {string} stripped string
 */
function stripQuotes( str ) {
    if ( /^".+"$/.test( str ) || /^'.+'$/.test( str ) ) {
        return str.substring( 1, str.length - 1 );
    }

    return str;
}

// Because iOS gives any camera-provided file the same filename, we need to a
// unique-ified filename.
//
// See https://github.com/kobotoolbox/enketo-express/issues/374
/**
 * @static
 * @param {object} file - File instance
 * @param {string} postfix - postfix for filename
 * @return {string} new filename
 */
function getFilename( file, postfix ) {
    if ( typeof file === 'object' && file !== null && file.name ) {
        postfix = postfix || '';
        const filenameParts = file.name.split( '.' );
        if ( filenameParts.length > 1 ) {
            filenameParts[ filenameParts.length - 2 ] += postfix;
        } else if ( filenameParts.length === 1 ) {
            filenameParts[ 0 ] += postfix;
        }

        return filenameParts.join( '.' );
    }

    return '';
}

/**
 * @static
 * @param {*} n - value
 * @return {boolean} whether it is a number value
 */
function isNumber( n ) {
    return !isNaN( parseFloat( n ) ) && isFinite( n );
}

/**
 * @static
 * @param {string} name - a cookie to look for
 * @return {string|undefined} the value of the cookie
 */
function readCookie( name ) {
    if ( cookies ) {
        return cookies[ name ];
    }

    // In enketo-validate and perhaps other contexts, enketo-core is used in an empty page in a headless browser
    // In such a context document.cookie throws an 'Access is denied for this document' error.
    try {
        const parts = document.cookie.split( '; ' );
        cookies = {};

        for ( let i = parts.length - 1; i >= 0; i-- ) {
            const ck = parts[ i ].split( '=' );
            // decode URI
            ck[ 1 ] = decodeURIComponent( ck[ 1 ] );
            // if cookie is signed (using expressjs/cookie-parser/), extract value
            if ( ck[ 1 ].substr( 0, 2 ) === 's:' ) {
                ck[ 1 ] = ck[ 1 ].slice( 2 );
                ck[ 1 ] = ck[ 1 ].slice( 0, ck[ 1 ].lastIndexOf( '.' ) );
            }
            cookies[ ck[ 0 ] ] = decodeURIComponent( ck[ 1 ] );
        }

        return cookies[ name ];

    } catch( e ){
        console.error( 'Cookie error', e );

        return null;
    }
}

/**
 * @static
 * @param {string} dataURI - dataURI
 * @return {Blob} dataURI converted to a Blob
 */
function dataUriToBlobSync( dataURI ) {
    // convert base64 to raw binary data held in a string
    // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
    const byteString = atob( dataURI.split( ',' )[ 1 ] );
    // separate out the mime component
    const mimeString = dataURI.split( ',' )[ 0 ].split( ':' )[ 1 ].split( ';' )[ 0 ];

    // write the bytes of the string to an ArrayBuffer
    const buffer = new ArrayBuffer( byteString.length );
    const array = new Uint8Array( buffer );

    for ( let i = 0; i < byteString.length; i++ ) {
        array[ i ] = byteString.charCodeAt( i );
    }

    // write the ArrayBuffer to a blob
    return new Blob( [ array ], {
        type: mimeString
    } );
}

/**
 * @static
 * @param {Event} event - a paste event
 * @return {string|null} clipboard data text value contained in event or null
 */
function getPasteData( event ) {
    const clipboardData = event.clipboardData || window.clipboardData; // modern || IE11

    return ( clipboardData ) ? clipboardData.getData( 'text' ) : null;
}

/**
 * @static
 * @param {File} file - Image file to be resized
 * @param {number} maxPixels - Maximum pixels of resized image
 * @return {Promise<Blob>} Promise of resized image blob
 */
function resizeImage( file, maxPixels ) {
    return new Promise( ( resolve, reject ) => {
        const image = new Image();
        image.src = URL.createObjectURL( file );
        image.onload = () => {
            const width = image.width;
            const height = image.height;

            if ( width <= maxPixels && height <= maxPixels ) {
                resolve( file );
            }

            let newWidth;
            let newHeight;

            if ( width > height ) {
                newHeight = height * ( maxPixels / width );
                newWidth = maxPixels;
            } else {
                newWidth = width * ( maxPixels / height );
                newHeight = maxPixels;
            }

            const canvas = document.createElement( 'canvas' );
            canvas.width = newWidth;
            canvas.height = newHeight;

            const context = canvas.getContext( '2d' );

            context.drawImage( image, 0, 0, newWidth, newHeight );

            canvas.toBlob( resolve, file.type );
        };
        image.onerror = reject;
    } );
}

/**
 * Copied from: https://gist.github.com/creationix/7435851
 * Joins path segments.  Preserves initial "/" and resolves ".." and "."
 * Does not support using ".." to go above/outside the root.
 * This means that join("foo", "../../bar") will not resolve to "../bar"
 */
function joinPath( /* path segments */ ) {
    // Split the inputs into a list of path commands.
    let parts = [];
    for ( var i = 0, l = arguments.length; i < l; i++ ) {
        parts = parts.concat( arguments[i].split( '/' ) );
    }
    // Interpret the path commands to get the new resolved path.
    let newParts = [];
    for ( i = 0, l = parts.length; i < l; i++ ) {
        var part = parts[i];
        // Remove leading and trailing slashes
        // Also remove "." segments
        if ( !part || part === '.' ) continue;
        // Interpret ".." to pop the last segment
        if ( part === '..' ) newParts.pop();
        // Push new path segments.
        else newParts.push( part );
    }
    // Preserve the initial slash if there was one.
    if ( parts[0] === '' ) newParts.unshift( '' );

    // Turn back into a single string path.
    return newParts.join( '/' ) || ( newParts.length ? '/' : '.' );
}


function getScript( url ) {
    const scriptTag = document.createElement( 'script' );
    const firstScriptTag = document.getElementsByTagName( 'script' )[0];
    scriptTag.src = url;
    firstScriptTag.parentNode.insertBefore( scriptTag, firstScriptTag );
}

function encodeHtmlEntities( text ){
    return text
        .replace( /&/g, '&amp;' )
        .replace( /</g, '&lt;' )
        .replace( />/g, '&gt;' )
        .replace( /"/g, '&quot;' );
}

export {
    parseFunctionFromExpression,
    stripQuotes,
    getFilename,
    isNumber,
    readCookie,
    dataUriToBlobSync,
    getPasteData,
    resizeImage,
    joinPath,
    getScript,
    encodeHtmlEntities
};