Source: widget/help/project/UACP.js

(function() {
    const ATTRIBUTES_MAPPING = {
        appUrl: 'screen',
        locale: 'language'
    }

    const TRANSFORM = {
        /**
         * @param {string} text
         * @param {Object} params
         * @param {string} params.mediaUrl
         * @returns {string}
         */
        processText: (text, {mediaUrl}) => {
            if (typeof text !== 'string') return '';

            // process images
            text = text.replace(/<img.*?>/gi, (match) => {
                const s = match.match(/src=(["'])(https?:\/\/.*?)\1/);
                return '<img src="' + (s ? s[2] : match.replace(/.*?src=(["'])(.*?)\1.*?>/, mediaUrl + '$2')) + '">';
            });

            // process links
            text = text.replace(/<a[^>]*?href=(["'])(.*?)\1.*?>/g, (match, p1, p2) => {
                if (match.search(/class=(["'])xref\1/) >= 0) {  // KM link
                    return match.search(/target=(["'])_blank\1/) >= 0
                        ? '<a rel="noopener nofollow noreferrer" target="_blank" href="' + p2 + '">'  // external KM link; XRAY-873
                        : '<a rel="noopener nofollow noreferrer" target="_blank" href="' + mediaUrl + p2 + '">';  // internal KM link
                } else {  // other
                    return match.replace(/target=(["']).*?\1/, '')  // remove all targets...
                    .replace(/rel=(["']).*?\1/, '')  // ... and remove all rel...
                    .replace(/>$/, ' rel="noopener nofollow noreferrer" target="_blank">')  // ...force them to be _blank
                    .replace(/class=(["']).*?\1/, '');  // XRAY-1946
                }
            });

            // see there for explanation
            return Help4.control.input.HtmlEditor.transformFromUACP(text);
        },

        /**
         * @param {string} isMachineTranslated
         * @returns {boolean}
         */
        processIsMachineTranslated: isMachineTranslated => isMachineTranslated === 'yes'
    }

    /**
     * @typedef {Object} Help4.widget.help.project.UACP.ProjectTileHotspotAssignment
     * @property {string} displayProperties
     * @property {string} hotspotAnchor
     */

    /**
     * @typedef {Object} Help4.widget.help.project.UACP.ProjectTile
     * @property {string} content
     * @property {Help4.widget.help.project.UACP.ProjectTileHotspotAssignment[]} hotspotAssignments
     * @property {string} id
     * @property {string} lastModifiedDate
     * @property {string} pageUrl
     * @property {string} summaryText
     * @property {number} tileOrder
     * @property {string} title
     * @property {string} tourAppUrl
     * @property {string} tourProduct
     * @property {string} tourVersion
     * @property {boolean} isMachineTranslated
     */

    /**
     * @typedef {Object} Help4.widget.help.project.UACP.Project
     * @property {string} alias
     * @property {string} appUrl
     * @property {Help4.widget.help.ContextTypes} contextType
     * @property {string} editable
     * @property {string} environment
     * @property {string} id
     * @property {string} lastModifiedDate
     * @property {string} locale
     * @property {string} loio
     * @property {string|Object} otherMetadata
     * @property {string} product
     * @property {string} shortDescription
     * @property {string} state
     * @property {string} system
     * @property {Help4.widget.help.project.UACP.ProjectTile[]} tiles
     * @property {string} title
     * @property {string} version
     * @property {Object} [conditions]
     */

    /**
     * this backend connector is able to handle UACP project content for a RO model configuration
     */
    Help4.widget.help.project.UACP = class {
        /**
         * @param {string} projectId - project ID
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @param {Help4.widget.help.Data} data
         * @param {Help4.widget.help.CatalogueTypes} [catalogueType = 'UACP']
         * @returns {Promise<Help4.widget.help.Projects|null>}
         */
        static async load(projectId, config, data, catalogueType = 'UACP') {
            const {roModel, serviceUrl} = config.help;
            if (!serviceUrl || roModel !== Help4.SERVICE_LAYER.uacp) return null;

            const {featureProfileUACP} = config.help;
            const profile = featureProfileUACP ? '&profile=' + encodeURIComponent(featureProfileUACP) : '';
            const url = serviceUrl + '/context?id=' + projectId + profile;

            /** @type {Help4.widget.companionCore.UACP.ProjectResponse} */
            const serverResponse = await Help4.widget.companionCore.UACP.doGetRequest(url);
            const {status, data: response} = serverResponse || {};

            if (status === 'OK' && response) {
                /** @type {Help4.widget.help.Project} */
                const pub = this._parse(response, config, catalogueType);
                return {pub};
            }

            return null;
        }

        /**
         * @protected
         * @param {Help4.widget.help.project.UACP.Project} data
         * @param {Help4.typedef.SystemConfiguration} config
         * @param {Help4.widget.help.CatalogueTypes} catalogueType
         * @returns {Help4.widget.help.Project}
         */
        static _parse(data, config, catalogueType) {
            /** @param {Help4.widget.help.project.UACP.Project} data */
            const setDefaults = data => {
                for (const [key, {f, uacp}] of Object.entries(Help4.PROJECT_DEFAULTS)) {
                    /** @type {string[]} */
                    const path = (uacp || key).split('.');

                    let value = data;
                    while (path[0] && value) value = value[path.shift()];

                    data[key] = value || f;  // attention: only works for json type!!!
                }
            }

            /**
             * @param {Help4.widget.help.project.UACP.Project} data
             * @param {string} mediaUrl
             * @param {Help4.widget.help.Project} project
             * @returns {Help4.widget.help.ProjectTile[]}
             */
            const parseTiles = (data, mediaUrl, project) => {
                /** @type {Array<Help4.widget.help.ProjectTile>} */
                const tiles = [];

                const {id, _dataType, _catalogueType, language} = project;
                const type = data.contextType.toLowerCase();
                const isTour = type === 'tour';

                for (const uacpTile of data.tiles) {
                    /** @type {Help4.widget.help.ProjectTile} */
                    const tile = parseTile(uacpTile, mediaUrl);
                    tile.type ||= type;
                    tile.language = language;
                    tile._catalogueType = _catalogueType;
                    tile._dataType = _dataType;
                    tile._projectId = id;

                    if (isTour && !tile.hotspotAnchor && !tile.hotspotCentered) {
                        // fix for old content, that could have no hotspot but also is not centered
                        // this constellation is no longer possible
                        tile.hotspotCentered = true;
                    }

                    tiles.push(tile);
                }

                return tiles;
            }

            /**
             * @param {Help4.widget.help.project.UACP.ProjectTile} tile
             * @param {string} mediaUrl
             * @returns {Help4.widget.help.ProjectTile}
             */
            const parseTile = (tile, mediaUrl) => {
                /** @type {Help4.widget.help.ProjectTile} */
                const parsed = {};

                // parse tile hotspot:
                // - create hotspot id
                // - convert displayProperties to JSON
                const hotspot = tile.hotspotAssignments?.[0];
                if (hotspot) {
                    hotspot.id = `${tile.id}-0`;

                    const {displayProperties} = hotspot;
                    if (displayProperties) {
                        try {
                            hotspot.displayProperties = Help4.JSON.parse(displayProperties);
                        } catch (e) {
                            delete hotspot.displayProperties;
                        }
                    }
                }
                tile.hotspot = hotspot;

                for (const [key, {t, f, /** @type {string} */ uacp, /** @type {string} */ uacpGet}] of Object.entries(Help4.DEFAULTS)) {
                    /** @type {string[]} */
                    const path = (uacp || key).split('.');

                    let value = tile;
                    while (path[0] && value) value = value[path.shift()];

                    const transform = TRANSFORM[uacpGet];
                    if (transform) value = transform(value, {mediaUrl});

                    value = Help4.clampValue(value, t, f);

                    if (value !== undefined) parsed[key] = value;
                }

                parsed._standalone = true;

                const wta = whatsThisAppTile(parsed.content);
                if (wta) {
                    // identify whether a saved data for showAsButton prop is available from server {tile}
                    // if not available (undefined) out.showAsButton will be false because Help4.DEFAULTS will be applied
                    // hence make it true if undefined
                    const path = Help4.DEFAULTS.showAsButton.uacp.split('.');
                    const showAsButton = path.reduce((prev, curr) => prev?.[curr], tile);
                    if (showAsButton === undefined) parsed.showAsButton = true;

                    parsed._special = 'wta';
                    parsed.type = 'link';
                    parsed.linkTo = wta;
                    parsed.hotspotAnchor = null;  // link tiles do not support hotspots
                }

                return parsed;
            }

            /**
             * @param {string} text
             * @returns {string|null}
             */
            const whatsThisAppTile = text => {
                // XRAY-1138: "What's this app" support
                // if a tile contains only a link we auto-convert it into a link tile
                const t1 = text.replace(/[\r\n]/g, '');
                const t2 = t1.replace(/^<p>\s*<a[^>]*?href=(["'])(.*?)\1.*?>.*?<\/a>\s*<\/p>$/, '$2');
                return t1 !== t2
                    ? t2.replace(/javascript:ctx\.cfg_show\((["'])(.*?)\1.*$/, '$2')
                    : null;
            }

            try {
                data.otherMetadata = Help4.JSON.parse(data.otherMetadata);
            } catch(e) {
                data.otherMetadata = {};
            }
            setDefaults(data);

            const {ACCEPTED_ATTRIBUTES} = Help4.widget.help.Data;
            /** @type {Help4.widget.help.Project} */
            const project = {};
            for (const [key, value] of Object.entries(data)) {
                const name = ATTRIBUTES_MAPPING[key] || key;
                if (ACCEPTED_ATTRIBUTES.includes(name)) {
                    project[name] = value;
                }
            }

            project._catalogueType = catalogueType;
            project._dataType = 'UACP';

            const {loio, version, locale} = data;
            const baseUrl = config.help.mediaUrl;
            const mediaUrl = [baseUrl, loio, encodeURIComponent(version), encodeURIComponent(locale), ''].join('/');
            project.tiles = parseTiles(data, mediaUrl, project);

            return project;
        }
    }
})();