Source: widget/help/XmlHelper.js

(function() {
    /**
     * XML Helper class.
     */
    Help4.widget.help.XmlHelper = class {
        /**
         * @param {Document} entityXml
         * @param {string} projectId
         * @returns {Help4.widget.help.project.SEN.EntityTxt|null}
         */
        static toEntityTxt(entityXml, projectId) {
            /** @type {Element|null} */
            const project = _getElements('project', entityXml, true);
            if (!project) return null;

            /** @type {Help4.widget.help.project.SEN.EntityTxt} */
            const entityTxt = _getAttributes(project);
            entityTxt.assets = [];
            entityTxt.tclass = 'project';
            entityTxt.uid = projectId;
            entityTxt.hidden = Help4.clampBoolean(entityTxt.hidden);
            return entityTxt;
        }

        /**
         * @param {Document} projectDpr
         * @param {Help4.widget.help.project.SEN.EntityTxt} entityTxt
         * @returns {Help4.widget.help.project.SEN.LessonJs}
         */
        static toLessonJs(projectDpr, entityTxt) {
            /** @type {Element|null} */
            const audioFormat = _getElements('AudioFormat', projectDpr, true);
            let videoFormat = '.wav', isMP3 = false, sps = 0;
            if (audioFormat) {
                videoFormat = audioFormat.getAttribute('Encoding') === 'Linear' ? '.wav' : '.mp3';
                isMP3 = videoFormat === '.mp3';
                sps = Number(audioFormat.getAttribute('SamplesPerSecond')) || 0;
            }

            /** @type {Element|null} */
            const author = _getElements('author', projectDpr, true);

            /** @type {Element|null} */
            const version = _getElements('modified_version', projectDpr, true) ||
                _getElements('version', projectDpr, true);

            /** @type {Element|null} */
            const globalParams = _getElements('GlobalParams', projectDpr, true);

            /** @type {Help4.widget.help.project.SEN.LessonJsTourstop[]} */
            const tourstops = [];

            /** @type {Help4.widget.help.project.SEN.LessonJs} */
            const json = {
                user_header: {
                    audio_ext: videoFormat,
                    author: author ? _getElementText(author) : '',
                    comment: entityTxt.description,
                    language: entityTxt.language,
                    mastery_score_percent: entityTxt.mastery_score_percent,
                    max_score_percent: entityTxt.max_score_percent,
                    shelftype: entityTxt.macroset,
                    title: entityTxt.caption,
                    version: version ? _getElementText(version) : ''
                },
                global_params: globalParams ? _getAttributes(globalParams) : {},
                tourstops
            };

            /** @type {NodeList} */
            const takes = _getElements('Take', projectDpr);
            for (const [index, /** @type {Element} */ take] of Help4.arrayEntries(takes)) {
                /** @type {Element|null} */
                const macroTrack = _getElements('MacroTrack', take, true);
                if (!macroTrack) continue;

                /** @type {Element|null} */
                let data = _getElements('data', take, true);
                if (!data) continue;

                /** @type {Object} */
                data = _getAttributes(data);
                const {audio_duration_ms, callable, jump, caption: title, name: uid, show} = data;

                /** @type {number} */
                const smp = Number(macroTrack.getAttribute('samples')) || 0;
                /** @type {number} */
                const adm = Number(audio_duration_ms) || 0;

                /** @type {Help4.widget.help.project.SEN.LessonJsMacroArray} */
                const macros = _getMacros(macroTrack, sps);

                /** @type {Help4.widget.help.project.SEN.LessonJsTourstop} */
                const tourstop = {
                    audio: '',
                    audio_duration: isMP3 ? (adm + 100) : adm,
                    callable: callable === '1',
                    duration: ~~(smp * 1000 / sps),
                    index,
                    jumpable: jump === '1',
                    macros,
                    title,
                    uid,
                    visible: show === '1'
                };

                if (tourstop.audio_duration) tourstop.audio = uid;
                tourstops.push(tourstop);
            }

            return json;
        }
    }

    /**
     * @memberof Help4.widget.help.XmlHelper
     * @private
     * @param {Element} macroTrack
     * @param {number} sps
     * @returns {Help4.widget.help.project.SEN.LessonJsMacroArray}
     */
    function _getMacros(macroTrack, sps) {
        /** @type {Help4.widget.help.project.SEN.LessonJsMacroArray} */
        const macros = [];

        const {/** @type {Help4.widget.help.HeadersXml} */ HeadersXml} = Help4.widget.help;

        /** @type {Element} */
        for (const macro of macroTrack.children) {
            const first = macro.nodeType === 1 && macro.firstElementChild;  // Init or MacroTemplate
            if (!first) continue;

            const name = first.getAttribute('template_name');
            /** @type {Help4.widget.help.HeadersXmlMacroDefinition} */
            const paramset = HeadersXml[name];
            /** @type {Help4.widget.help.project.SEN.LessonJsMacro} */
            const attributes = _getAttributes(first, {paramset, includeSubTree: true});
            const pos = Number(macro.getAttribute('pos')) || 0;
            attributes.time = Math.floor(pos  * 1000 / sps);
            attributes.duration = paramset.Duration;

            attributes.macro_template = attributes.template_name;
            delete attributes.template_name;

            macros.push(attributes);
        }

        return macros;
    }

    /**
     * @memberof Help4.widget.help.XmlHelper
     * @private
     * @param {Element} element
     * @returns {string}
     */
    function _getElementText(element) {
        const text = element.innerHTML || element.nodeTypedValue || '';
        if (!text) {
            // needed for Edge browser
            const child = element.firstChild;
            return child?.wholeText?.trim() || '';
        }
        return text;
    }

    /**
     * @memberof Help4.widget.help.XmlHelper
     * @private
     * @param {Element} element
     * @param {Object} [options = {}]
     * @param {Help4.widget.help.HeadersXmlMacroDefinition} [options.paramset]
     * @param {boolean} [options.includeSubTree = false]
     * @returns {Object}
     */
    function _getAttributes(element, {paramset, includeSubTree = false} = {}) {
        const result = {};

        /**
         * @param {string} nodeName
         * @param {*} nodeValue
         */
        const handleResult = (nodeName, nodeValue) => {
            if (paramset) {
                const {NoExport, ParamSet} = paramset;
                const {type} = ParamSet[nodeName] || {};
                if (NoExport.indexOf(nodeName) < 0 && type) {
                    result[nodeName] = _clampTypedValue(type, nodeValue);
                }
            } else {
                result[nodeName] = nodeValue;
            }
        }

        /** @type {Attr} */
        for (const attribute of element.attributes) {
            const {nodeName, nodeValue} = attribute;
            handleResult(nodeName, nodeValue);
        }

        if (includeSubTree) {
            /** @type {Element} */
            for (const child of element.children) {
                const attributes = _getAttributes(child);
                handleResult(child.nodeName, attributes);
            }
        }

        return result;
    }

    /**
     * @memberof Help4.widget.help.XmlHelper
     * @private
     * @param {string} type
     * @param {*} value
     * @returns {*}
     */
    function _clampTypedValue(type, value) {
        switch (type) {
            case 'POSITION': {
                const left = Number(value?.x) || 0;
                const top = Number(value?.y) || 0;
                return {left, top};
            }
            case 'POSSIZE': {
                const left = Number(value?.x) || 0;
                const top = Number(value?.y) || 0;
                const width = Number(value?.cx) || 100;
                const height = Number(value?.cy) || 100;
                return {left, top, width, height};
            }
            case 'SIZE':
                // type mismatch in Headers.xml; SIZE is sometimes string and sometimes object
                if (typeof value === 'string') {
                    let [width, height] = value?.split(',') || [];
                    width = Number(width) || 0;
                    height = Number(height) || 0;
                    return {width, height};
                } else {
                    const [x, y] = value?.hasOwnProperty('x')
                        ? ['x', 'y']
                        : ['width', 'height'];
                    const width = Number(value?.[x]) || 0;
                    const height = Number(value?.[y]) || 0;
                    return {width, height};
                }
            case 'BOOL_0_1':
            case 'BOOL_YES_NO':
            case 'BOOL_TRUE_FALSE':
                // not always accurate within project.dpr
                return value == 1 || value === 'yes' || value === 'true';
        }
        return value;
    }

    /**
     * @memberof Help4.widget.help.XmlHelper
     * @private
     * @param {string} nodeName
     * @param {Element|Document} xml
     * @param {boolean} [first = false]
     * @returns {Element|NodeList|null}
     */
    function _getElements(nodeName, xml, first = false) {
        return first
            ? xml.querySelector(nodeName)
            : xml.querySelectorAll(nodeName);
    }
})();