Source: utils.js

const PLATFORM_IS_MAC_OS = /mac/i.test(window.navigator.platform);
const PLATFORM_IS_WINDOWS = /win/i.test(window.navigator.platform);
const PLATFORM_IS_LINUX = /linux/i.test(window.navigator.platform);

//BROWSER BUG: used to work around Firefox bugs.
const BROWSER_ENGINE_IS_GECKO = /Gecko\/\S+/.test(window.navigator.userAgent);

/*
   ----------------------------
   Why is Safari not supported?
   ----------------------------
   Tested against 
   - Safari 17.1 AppleWebKit/605.1.15 on November 16, 2023.
   - Safari 16.6 AppleWebKit/605.1.15 on August 17, 2023.
   - Safari 16.5.1 AppleWebKit/605.1.15 on July 2, 2023.
   Same issues.
   ----------------------------
   Tested against Safari 16.4.1 AppleWebKit/605.1.15 on April 12, 2023.

   Works normally^1 except that, in practice, Safari's WebSocket client 
   implementation is unusable.

   - With Develop|Experimental Features|NSUrlSession WebSocket ON (the default):
     * When opening a remote document containing images, Safari closes the
       WebSocket with code 1006.
       See https://bugs.webkit.org/show_bug.cgi?id=228296
     * A self-signed certificate works after being sanctioned by the user.

   - With Develop|Experimental Features|NSUrlSession WebSocket OFF:
     * Opening a remote document containing images works.
     * Quitting XMLEditor makes Safari closes the WebSocket in a unclean way
       (Jetty always reports: XXEWebSocket.onError [client=null]: 
        java.nio.channels.ClosedChannelException)
     * A self-signed certificate causes the wss:// connection to fail 
       even if this certificate is sanctioned by the user.

   ---
   ^1 Very minor issues:
   * Resizing XMLEditor by dragging is bottom/right corner works but there is
     no resize icon there.
   * A multi-line outline around focused text is rendered using a solid style
     and not a dotted style.
*/

function browserEngineIsSupported() {
    // See https://developer.mozilla.org/en-US/docs/Web/HTTP/
    //     Browser_detection_using_the_user_agent
    //
    // Also note that Apple forces every web browser on iOS to use Apple's
    // WebKit engine.

    // Remove/add trailing "000" below to allow/deny using Safari.
    const appleWebKitMinVersion = 605000;
    
    let supported = false;
    if (window.navigator) {
        const userAgent = window.navigator.userAgent;
        if (userAgent) {
            let match = /Chrome\/([0-9.]+)/.exec(userAgent);
            if (match !== null) {
                // Something like "87.0.4280.141" parses as 87.
                let version = parseInt(match[1]);
                if (!isNaN(version) && version >= 107) {
                    supported = true;
                }
            } else {
                let match = /AppleWebKit\/([0-9.]+)/.exec(userAgent);
                if (match !== null) {
                    // Something like "605.1.15" parses as 605.
                    let version = parseInt(match[1]);
                    if (!isNaN(version) && version >= appleWebKitMinVersion) {
                        supported = true;
                    }
                } else if (/Gecko\/\S+/.test(userAgent)) {
                    let match = /rv:([0-9.]+)/.exec(userAgent);
                    if (match !== null) {
                        let version = parseInt(match[1]);
                        if (!isNaN(version) && version >= 102) {
                            supported = true;
                        }
                    }
                }
            }
        }
        
        if (supported && ("ontouchstart" in window)) {
            // In practice, XMLEditor is unusable on a touch-capable device
            // like a tablet or a smartphone.
            supported = false;
        }
    }
    
    return supported;
}

/**
 * Unlike <code>console.assert</code>, this function throws an error when
 * the assertion fails.
 */
function assertOrError(assertion, message=null) {
    if (!assertion) {
        if (!message) {
            message = "ASSERTION FAILED!";
        }
        let error = new Error(message);
        
        console.error(`assertOrError
---
${error.stack}
---`);
        
        throw error;
    }
}

/**
 * Unlike <code>String.split</code> which,  when the string is empty, 
 * returns an array containing one empty string, this function returns 
 * an empty array.
 *
 * @param {string} s - string to be first <em>trimmed</em> then split. 
 * May be <code>null</code>.
 * @param {separ} separ - where each split should occur; 
 * can be a string or a regular expression
 * @return {array} an array of strings, split at each point where 
 * the separator occurs.
 */
function splitTrimString(s, separ) {
    return (s === null || (s = s.trim()).length === 0)? [] : s.split(separ);
}