wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/dm/nodes/ve.dm.MWLanguageVariantNode.js

Summary

Maintainability
C
1 day
Test Coverage
/*!
 * VisualEditor DataModel MWLanguageVariantNode class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * DataModel MediaWiki language variant node, used to represent
 * LanguageConverter markup.
 *
 * @class
 * @abstract
 * @extends ve.dm.LeafNode
 * @mixes ve.dm.FocusableNode
 *
 * @constructor
 */
ve.dm.MWLanguageVariantNode = function VeDmMWLanguageVariantNode() {
    // Parent constructor
    ve.dm.MWLanguageVariantNode.super.apply( this, arguments );

    // Mixin constructors
    ve.dm.FocusableNode.call( this );
};

/* Inheritance */

OO.inheritClass( ve.dm.MWLanguageVariantNode, ve.dm.LeafNode );

OO.mixinClass( ve.dm.MWLanguageVariantNode, ve.dm.FocusableNode );

/* Static members */

ve.dm.MWLanguageVariantNode.static.name = 'mwLanguageVariant';

ve.dm.MWLanguageVariantNode.static.matchTagNames = null;

ve.dm.MWLanguageVariantNode.static.matchRdfaTypes = [ 'mw:LanguageVariant' ];

ve.dm.MWLanguageVariantNode.static.getHashObject = function ( dataElement ) {
    return {
        type: dataElement.type,
        variantInfo: dataElement.attributes.variantInfo
    };
};

/**
 * Node type to use when the contents are inline
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.MWLanguageVariantNode.static.inlineType = 'mwLanguageVariantInline';

/**
 * Node type to use when the contents are a block
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.MWLanguageVariantNode.static.blockType = 'mwLanguageVariantBlock';

/**
 * Node type to use when the contents are hidden
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.MWLanguageVariantNode.static.hiddenType = 'mwLanguageVariantHidden';

/**
 * Migrate field names from old Parsoid spec to new field names.
 * This method will go away after the next Parsoid flag day.
 *
 * @static
 * @private
 * @param {Object} dataMwv
 * @return {Object}
 */
ve.dm.MWLanguageVariantNode.static.migrateFieldNames = function ( dataMwv ) {
    // Field name migration: `bidir`=>`twoway`; `unidir`=>`oneway`
    // This will go away eventually.
    if ( dataMwv.bidir ) {
        dataMwv.twoway = dataMwv.bidir;
        delete dataMwv.bidir;
    }
    if ( dataMwv.unidir ) {
        dataMwv.oneway = dataMwv.unidir;
        delete dataMwv.unidir;
    }
    return dataMwv;
};

/**
 * @inheritdoc
 */
ve.dm.MWLanguageVariantNode.static.toDataElement = function ( domElements, converter ) {
    const firstElement = domElements[ 0 ],
        dataMwvJSON = firstElement.getAttribute( 'data-mw-variant' ),
        dataMwv = dataMwvJSON ? JSON.parse( dataMwvJSON ) : {};

    this.migrateFieldNames( dataMwv );

    const dataElement = {
        attributes: {
            variantInfo: dataMwv,
            originalVariantInfo: dataMwvJSON
        }
    };

    if ( firstElement.tagName === 'META' ) {
        dataElement.type = this.hiddenType;
        return dataElement;
    }

    const isInline = this.isHybridInline( domElements, converter );
    dataElement.type = isInline ? this.inlineType : this.blockType;
    return dataElement;
};

/**
 * @inheritdoc
 */
ve.dm.MWLanguageVariantNode.static.toDomElements = function ( dataElement, doc, converter ) {
    const variantInfo = dataElement.attributes.variantInfo,
        tagName = this.matchTagNames[ 0 ],
        rdfaType = this.matchRdfaTypes[ 0 ],
        domElement = doc.createElement( tagName );

    let dataMwvJSON = JSON.stringify( variantInfo );
    // Preserve exact equality of this attribute for selser.
    if ( dataElement.attributes.originalVariantInfo ) {
        if ( OO.compare(
            this.migrateFieldNames(
                JSON.parse( dataElement.attributes.originalVariantInfo )
            ),
            variantInfo
        ) ) {
            dataMwvJSON = dataElement.attributes.originalVariantInfo;
        }
    }

    domElement.setAttribute( 'typeof', rdfaType );
    domElement.setAttribute( 'data-mw-variant', dataMwvJSON );
    if ( converter.doesModeNeedRendering() && tagName !== 'META' ) {
        // Fill in contents of span for diff/cut-and-paste/etc.
        this.insertPreviewElements( domElement, variantInfo );
    }
    return [ domElement ];
};

/**
 * Add previews for language variant markup inside their <span> nodes.
 * This ensures that template expansion, cut-and-paste, etc have reasonable
 * renderings.
 *
 * @static
 * @param {HTMLElement} container Container element to process
 * @param {Object|null} opts Preview options
 * @param {boolean} [opts.describeAll=false] Treat all rules as if the
 *   "describe" flag was set. This displays every language and its associated
 *   text, not just the one appropriate for the current user.
 */
ve.dm.MWLanguageVariantNode.static.processVariants = function ( container, opts ) {
    Array.prototype.forEach.call( container.querySelectorAll( '[typeof="mw:LanguageVariant"]' ), ( element ) => {
        const dataMwvJSON = element.getAttribute( 'data-mw-variant' );
        if ( dataMwvJSON && element.tagName !== 'META' ) {
            this.insertPreviewElements(
                element, JSON.parse( dataMwvJSON ), opts
            );
        }
    } );
};

/**
 * Insert language variant preview for specified element.
 *
 * @static
 * @param {HTMLElement} element Element to insert preview inside of.
 * @param {Object} variantInfo Language variant information object.
 * @param {Object|null} opts Preview options
 * @param {boolean} [opts.describeAll=false] Treat all rules as if the
 *   "describe" flag was set. This displays every language and its associated
 *   text, not just the one appropriate for the current user.
 * @return {HTMLElement} el
 */
ve.dm.MWLanguageVariantNode.static.insertPreviewElements = function ( element, variantInfo, opts ) {
    // Note that `element` can't be a <meta> (or other void tag)
    element.innerHTML = this.getPreviewHtml( variantInfo, opts );
    // This recurses into the children added by the `html` clause to ensure
    // that nested variants are expanded.
    this.processVariants( element, opts );
    return element;
};

/**
 * Helper method to return an appropriate HTML preview string for a
 * language converter node, based on the language variant information
 * object and the user's currently preferred variant.
 *
 * @static
 * @private
 * @param {Object} variantInfo Language variant information object.
 * @param {Object|null} opts Preview options
 * @param {boolean} [opts.describeAll=false] Treat all rules as if the
 *   "describe" flag was set. This displays every language and its associated
 *   text, not just the one appropriate for the current user.
 * @return {string} HTML string
 */
ve.dm.MWLanguageVariantNode.static.getPreviewHtml = function ( variantInfo, opts ) {
    if ( variantInfo.disabled ) {
        return variantInfo.disabled.t;
    } else if ( variantInfo.name ) {
        return ve.init.platform.getLanguageName( variantInfo.name.t.toLowerCase() );
    } else if ( variantInfo.filter ) {
        return variantInfo.filter.t;
    } else if ( variantInfo.describe || ( opts && opts.describeAll ) ) {
        let html = '';
        if ( variantInfo.twoway && variantInfo.twoway.length ) {
            variantInfo.twoway.forEach( ( item ) => {
                html += ve.init.platform.getLanguageName( item.l.toLowerCase() ) + ':' +
                    item.t + ';';
            } );
        } else if ( variantInfo.oneway && variantInfo.oneway.length ) {
            variantInfo.oneway.forEach( ( item ) => {
                html += item.f + '⇒' +
                    ve.init.platform.getLanguageName( item.l.toLowerCase() ) + ':' +
                    item.t + ';';
            } );
        }
        return html;
    } else {
        let languageIndex;
        if ( variantInfo.twoway && variantInfo.twoway.length ) {
            languageIndex = this.matchLanguage( variantInfo.twoway );
            return variantInfo.twoway[ languageIndex ].t;
        } else if ( variantInfo.oneway && variantInfo.oneway.length ) {
            languageIndex = this.matchLanguage( variantInfo.oneway );
            return variantInfo.oneway[ languageIndex ].t;
        }
    }
    return '';
};

/**
 * @inheritdoc
 */
ve.dm.MWLanguageVariantNode.static.describeChanges = function () {
    // TODO: Provide a more detailed description of markup changes
    return ve.msg( 'visualeditor-changedesc-mwlanguagevariant' );
};

/**
 * @inheritdoc ve.dm.Node
 */
ve.dm.MWLanguageVariantNode.static.cloneElement = function () {
    // Parent method
    const clone = ve.dm.MWLanguageVariantNode.super.static.cloneElement.apply( this, arguments );
    delete clone.attributes.originalVariantInfo;
    return clone;
};

/**
 * Match the currently-selected language variant against the most appropriate
 * among a provided list of language codes.
 *
 * @static
 * @param {Object[]} [items] An array of objects, each of which have a field
 *  named `l` equal to a language code.
 * @return {number} The index in `items` with the most appropriate language
 *  code.
 */
ve.dm.MWLanguageVariantNode.static.matchLanguage = function ( items ) {
    const userVariant = mw.config.get( 'wgUserVariant' ),
        fallbacks = mw.config.get( 'wgVisualEditor' ).pageVariantFallbacks,
        languageCodes =
            ( userVariant ? [ userVariant ] : [] ).concat( fallbacks || [] );
    for ( let j = 0; j < languageCodes.length; j++ ) {
        const code = languageCodes[ j ].toLowerCase();
        for ( let i = 0; i < items.length; i++ ) {
            if (
                items[ i ].l === '*' ||
                items[ i ].l.toLowerCase() === code
            ) {
                return i;
            }
        }
    }
    // Bail: just show the first item.
    return 0;
};

/* Methods */

/**
 * Helper function to get the description object for this markup node.
 *
 * @return {Object}
 */
ve.dm.MWLanguageVariantNode.prototype.getVariantInfo = function () {
    return this.element.attributes.variantInfo;
};

/**
 * Helper function to discriminate between hidden and shown rules.
 *
 * @return {boolean} True if this node represents a conversion rule
 *  with no shown output
 */
ve.dm.MWLanguageVariantNode.prototype.isHidden = function () {
    return false;
};

/**
 * Helper function to discriminate between various types of language
 * converter markup.
 *
 * @return {string}
 */
ve.dm.MWLanguageVariantNode.prototype.getRuleType = function () {
    return this.constructor.static.getRuleType( this.getVariantInfo() );
};

/**
 * Helper function to discriminate between various types of language
 * converter markup.
 *
 * @static
 * @param {Object} variantInfo Language variant information object.
 * @return {string}
 */
ve.dm.MWLanguageVariantNode.static.getRuleType = function ( variantInfo ) {
    if ( variantInfo.disabled ) {
        return 'disabled';
    }
    if ( variantInfo.filter ) {
        return 'filter';
    }
    if ( variantInfo.name ) {
        return 'name';
    }
    if ( variantInfo.twoway ) {
        return 'twoway';
    }
    if ( variantInfo.oneway ) {
        return 'oneway';
    }
    return 'unknown'; // should never happen
};