wikimedia/mediawiki-extensions-VisualEditor

View on GitHub
modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js

Summary

Maintainability
A
3 hrs
Test Coverage
/*!
 * VisualEditor UserInterface MWWikitextStringTransferHandler class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Detect an attempt to paste wikitext, and convert it to proper
 * HTML.
 *
 * @class
 * @extends ve.ui.PlainTextStringTransferHandler
 *
 * @constructor
 * @param {ve.ui.Surface} surface
 * @param {ve.ui.DataTransferItem} item
 */
ve.ui.MWWikitextStringTransferHandler = function VeUiMWWikitextStringTransferHandler() {
    // Parent constructor
    ve.ui.MWWikitextStringTransferHandler.super.apply( this, arguments );

    // Properties
    this.parsoidRequest = null;
};

/* Inheritance */

OO.inheritClass( ve.ui.MWWikitextStringTransferHandler, ve.ui.PlainTextStringTransferHandler );

/* Static properties */

ve.ui.MWWikitextStringTransferHandler.static.name = 'wikitextString';

ve.ui.MWWikitextStringTransferHandler.static.types = [
    ...ve.ui.MWWikitextStringTransferHandler.super.static.types,
    'text/x-wiki'
];

ve.ui.MWWikitextStringTransferHandler.static.handlesPaste = true;

ve.ui.MWWikitextStringTransferHandler.static.matchFunction = function ( item ) {
    const text = item.getAsString(),
        registry = ve.ui.mwWikitextTransferRegistry;

    // If the mime type is explicitly wikitext (ie, not plain text),
    // always accept.
    if ( item.type === 'text/x-wiki' ) {
        return true;
    }

    // Detect autolink opportunities for magic words.
    // (The link should be the only contents of paste to match this heuristic)
    if ( ve.dm.MWMagicLinkNode.static.validateContent( text.trim() ) ) {
        return true;
    }

    // Use a heuristic regexp to find text likely to be wikitext.
    // This test could be made more sophisticated in the future.
    for ( const i in registry.registry ) {
        const rule = registry.registry[ i ];
        if ( rule instanceof RegExp ) {
            if ( registry.registry[ i ].test( text ) ) {
                return true;
            }
        } else if ( text.indexOf( rule ) !== -1 ) {
            return true;
        }
    }
    return false;
};

/**
 * Create a new document from HTML from Parsoid
 *
 * @param {string} html HTML from Parsoid
 * @param {ve.dm.Document} targetDoc DM document this will eventually be merged with
 * @return {ve.dm.Document} New document
 */
ve.ui.MWWikitextStringTransferHandler.static.createDocumentFromParsoidHtml = function ( html, targetDoc ) {
    const htmlDoc = ve.createDocumentFromHtml( html );

    // Strip RESTBase IDs
    mw.libs.ve.stripRestbaseIds( htmlDoc );

    // Strip legacy IDs, for example in section headings
    mw.libs.ve.stripParsoidFallbackIds( htmlDoc.body );

    // Pass an empty object for the second argument (importRules) so that clipboard mode is used
    // TODO: Fix that API
    const doc = targetDoc.newFromHtml( htmlDoc, {} );
    const data = doc.data.data;
    const surface = new ve.dm.Surface( doc );

    // Filter out auto-generated items, e.g. reference lists
    // This is done after conversion as the autoGenerated item may contain data
    // required by other non-autoGenerated items, e.g. reference contents
    for ( let i = data.length - 1; i >= 0; i-- ) {
        if ( ve.getProp( data[ i ], 'attributes', 'mw', 'autoGenerated' ) ) {
            surface.change(
                ve.dm.TransactionBuilder.static.newFromRemoval(
                    doc,
                    surface.getDocument().getDocumentNode().getNodeFromOffset( i + 1 ).getOuterRange()
                )
            );
        }
    }

    // Clone elements to avoid about attribute conflicts (T204007)
    doc.data.cloneElements( true );

    return doc;
};

/* Methods */

/**
 * @inheritdoc
 */
ve.ui.MWWikitextStringTransferHandler.prototype.process = function () {
    const wikitext = this.item.getAsString();

    // We already know how to handle wikitext magic links, no need for the API call
    if ( ve.dm.MWMagicLinkNode.static.validateContent( wikitext.trim() ) ) {
        this.resolve( [
            {
                type: 'link/mwMagic',
                attributes: {
                    content: wikitext.trim()
                }
            },
            {
                type: '/link/mwMagic'
            }
        ] );
        return;
    }

    const failure = () => {
        // There's no DTH fallback handling for failures, so just paste
        // the raw wikitext if things go wrong.
        this.resolve( wikitext );
    };

    // Convert wikitext to html using Parsoid.
    this.parsoidRequest = ve.init.target.parseWikitextFragment( wikitext, false, this.surface.getModel().getDocument() );

    // Don't immediately chain, as this.parsoidRequest must be abortable
    this.parsoidRequest.then( ( response ) => {
        if ( ve.getProp( response, 'visualeditor', 'result' ) !== 'success' ) {
            return failure();
        }

        const doc = this.constructor.static.createDocumentFromParsoidHtml(
            response.visualeditor.content,
            this.surface.getModel().getDocument()
        );

        if ( !doc.data.hasContent() ) {
            return failure();
        }

        this.resolve( doc );
    }, failure );

    this.createProgress( this.parsoidRequest, ve.msg( 'visualeditor-wikitext-progress' ) );
    // Indeterminate progress
    this.setProgress( null );
};

/**
 * @inheritdoc
 */
ve.ui.MWWikitextStringTransferHandler.prototype.abort = function () {
    // Parent method
    ve.ui.MWWikitextStringTransferHandler.super.prototype.abort.apply( this, arguments );

    if ( this.parsoidRequest ) {
        this.parsoidRequest.abort();
    }
};

/* Registration */

ve.ui.dataTransferHandlerFactory.register( ve.ui.MWWikitextStringTransferHandler );