Support the upcoming mw:File typeof

The "mediaClass" property now only serves to capture the original class
found on the media so that it can be roundtripped without causing dirty
diffs.  In the 2.4.0 version of Parsoid's output, that will still be
the usual Image/Audio/Video.  As of 2.5.0, it will always be File and
the mediaClass property can be dropped.

Parsoid is currently forward compatible with serializing mw:File, so
edited or new media can use that type already.

The contextmenu item for media has been updated to make use of the
"mediaTag" instead of mediaClass to continue distinguishing media types.
That was the only place a grep of mediaClass turned up any use.

Bug: T273505
Change-Id: If5dc6b794dacd6973d3b2093e6b385591b91d539
This commit is contained in:
Arlo Breault 2022-05-30 19:17:46 -04:00
parent 03a2523501
commit 0533f49fd5
7 changed files with 79 additions and 47 deletions

View file

@ -130,7 +130,8 @@ ve.dm.MWImageModel.static.createImageNode = function ( attributes, imageType ) {
.thumbLimits[ mw.user.options.get( 'thumbsize' ) ];
var attrs = ve.extendObject( {
mediaClass: 'Image',
mediaClass: 'File',
mediaTag: 'img',
type: 'thumb',
align: 'default',
width: defaultThumbSize,
@ -562,7 +563,8 @@ ve.dm.MWImageModel.prototype.getUpdatedAttributes = function () {
}
var attrs = {
mediaClass: this.getMediaClass(),
mediaClass: 'File',
mediaTag: this.getMediaTag(),
type: this.getType(),
width: currentDimensions.width,
height: currentDimensions.height,
@ -798,20 +800,20 @@ ve.dm.MWImageModel.prototype.getMediaType = function () {
};
/**
* Get Parsoid media class: Image, Video or Audio
* Get media tag: img, video or audio
*
* @return {string} Media class
* @return {string} Tag name
*/
ve.dm.MWImageModel.prototype.getMediaClass = function () {
ve.dm.MWImageModel.prototype.getMediaTag = function () {
var mediaType = this.getMediaType();
if ( mediaType === 'VIDEO' ) {
return 'Video';
return 'video';
}
if ( mediaType === 'AUDIO' ) {
return 'Audio';
return 'audio';
}
return 'Image';
return 'img';
};
/**

View file

@ -95,6 +95,7 @@ ve.dm.MWBlockImageNode.static.toDataElement = function ( domElements, converter
var attributes = {
mediaClass: types.mediaClass,
mediaTag: img.nodeName.toLowerCase(),
type: types.frameType,
src: img.getAttribute( 'src' ) || img.getAttribute( 'poster' ),
href: href,
@ -162,16 +163,15 @@ ve.dm.MWBlockImageNode.static.toDataElement = function ( domElements, converter
ve.dm.MWBlockImageNode.static.toDomElements = function ( data, doc, converter ) {
var dataElement = data[ 0 ],
attributes = dataElement.attributes,
mediaClass = attributes.mediaClass,
figure = doc.createElement( 'figure' ),
imgWrapper = doc.createElement( attributes.href ? 'a' : 'span' ),
img = doc.createElement( attributes.isError ? 'span' : this.typesToTags[ mediaClass ] ),
img = doc.createElement( attributes.isError ? 'span' : attributes.mediaTag ),
wrapper = doc.createElement( 'div' ),
classAttr = this.getClassAttrFromAttributes( attributes ),
captionData = data.slice( 1, -1 );
// RDFa type
figure.setAttribute( 'typeof', this.getRdfa( mediaClass, attributes.type, attributes.isError ) );
figure.setAttribute( 'typeof', this.getRdfa( attributes.mediaClass, attributes.type, attributes.isError ) );
if ( !ve.isEmptyObject( attributes.mw ) ) {
figure.setAttribute( 'data-mw', JSON.stringify( attributes.mw ) );
}
@ -209,7 +209,7 @@ ve.dm.MWBlockImageNode.static.toDomElements = function ( data, doc, converter )
}
}
var srcAttr = this.typesToSrcAttrs[ mediaClass ];
var srcAttr = this.tagsToSrcAttrs[ img.nodeName.toLowerCase() ];
if ( srcAttr && !attributes.isError ) {
img.setAttribute( srcAttr, attributes.src );
}

View file

@ -75,9 +75,17 @@ ve.dm.MWGalleryImageNode.static.toDataElement = function ( domElements, converte
var width = img.getAttribute( isError ? 'data-width' : 'width' );
var height = img.getAttribute( isError ? 'data-height' : 'height' );
if ( isError ) {
typeofAttrs.splice( errorIndex, 1 );
}
var types = ve.dm.MWImageNode.static.rdfaToTypes[ typeofAttrs[ 0 ] ];
var dataElement = {
type: this.name,
attributes: {
mediaClass: types.mediaClass,
mediaTag: img.nodeName.toLowerCase(),
resource: './' + mw.libs.ve.normalizeParsoidResourceName( img.getAttribute( 'resource' ) ),
altText: img.getAttribute( 'alt' ),
// 'src' for images, 'poster' for video/audio
@ -106,12 +114,16 @@ ve.dm.MWGalleryImageNode.static.toDomElements = function ( data, doc ) {
thumbDiv = doc.createElement( 'div' ),
container = doc.createElement( 'span' ),
a = doc.createElement( 'a' ),
img = doc.createElement( attributes.isError ? 'span' : 'img' ),
img = doc.createElement( attributes.isError ? 'span' : ( attributes.mediaTag || 'img' ) ),
alt = attributes.altText;
// FIXME: attributes.mediaTag and attributes.mediaClass aren't set after edit
li.classList.add( 'gallerybox' );
thumbDiv.classList.add( 'thumb' );
container.setAttribute( 'typeof', 'mw:Image' );
container.setAttribute( 'typeof', ve.dm.MWImageNode.static.getRdfa(
( attributes.mediaClass || 'File' ), 'none', attributes.isError
) );
// TODO: Support editing the link
// FIXME: Dropping the href causes Parsoid to mark the node as wrapper modified,
@ -126,7 +138,8 @@ ve.dm.MWGalleryImageNode.static.toDomElements = function ( data, doc ) {
var filename = mw.libs.ve.normalizeParsoidResourceName( attributes.resource || '' );
img.appendChild( doc.createTextNode( filename ) );
} else {
img.setAttribute( 'src', attributes.src );
var srcAttr = ve.dm.MWImageNode.static.tagsToSrcAttrs[ img.nodeName.toLowerCase() ];
img.setAttribute( srcAttr, attributes.src );
}
img.setAttribute( attributes.isError ? 'data-width' : 'width', attributes.width );
img.setAttribute( attributes.isError ? 'data-height' : 'height', attributes.height );

View file

@ -55,7 +55,7 @@ OO.mixinClass( ve.dm.MWImageNode, ve.dm.ResizableNode );
ve.dm.MWImageNode.static.rdfaToTypes = ( function () {
var rdfaToType = {};
[ 'Image', 'Video', 'Audio' ].forEach( function ( mediaClass ) {
[ 'File', 'Image', 'Video', 'Audio' ].forEach( function ( mediaClass ) {
rdfaToType[ 'mw:' + mediaClass ] = { mediaClass: mediaClass, frameType: 'none' };
rdfaToType[ 'mw:' + mediaClass + '/Frameless' ] = { mediaClass: mediaClass, frameType: 'frameless' };
// Block image only:
@ -70,7 +70,7 @@ ve.dm.MWImageNode.static.rdfaToTypes = ( function () {
* Get RDFa type
*
* @static
* @param {string} mediaClass Media class, one of 'Image', 'Video' or 'Audio'
* @param {string} mediaClass Media class, one of 'File', 'Image', 'Video' or 'Audio'
* @param {string} frameType Frame type, one of 'none', 'frameless', 'thumb' or 'frame'
* @param {boolean} isError Whether the included media file is missing
* @return {string} RDFa type
@ -86,25 +86,15 @@ ve.dm.MWImageNode.static.getRdfa = function ( mediaClass, frameType, isError ) {
};
/**
* Map media types to tag names
* Map media tags to source attributes
*
* @type {Object}
*/
ve.dm.MWImageNode.static.typesToTags = {
Image: 'img',
Audio: 'audio',
Video: 'video'
};
/**
* Map media types to source attributes
*
* @type {Object}
*/
ve.dm.MWImageNode.static.typesToSrcAttrs = {
Image: 'src',
Audio: null,
Video: 'poster'
ve.dm.MWImageNode.static.tagsToSrcAttrs = {
img: 'src',
audio: null,
video: 'poster',
span: null
};
/**

View file

@ -54,7 +54,7 @@ ve.dm.MWInlineImageNode.static.toDataElement = function ( domElements, converter
// Malformed figure, alienate (T267282)
return null;
}
var img = imgWrapper.children[ 0 ]; // <img>, <video> or <audio>
var img = imgWrapper.children[ 0 ]; // <img>, <video>, <audio>, or <span> if mw:Error
var typeofAttrs = ( container.getAttribute( 'typeof' ) || '' ).trim().split( /\s+/ );
var mwDataJSON = container.getAttribute( 'data-mw' );
var mwData = mwDataJSON ? JSON.parse( mwDataJSON ) : {};
@ -83,6 +83,7 @@ ve.dm.MWInlineImageNode.static.toDataElement = function ( domElements, converter
var attributes = {
mediaClass: types.mediaClass,
mediaTag: img.nodeName.toLowerCase(),
type: types.frameType,
src: img.getAttribute( 'src' ) || img.getAttribute( 'poster' ),
href: href,
@ -142,9 +143,8 @@ ve.dm.MWInlineImageNode.static.toDataElement = function ( domElements, converter
ve.dm.MWInlineImageNode.static.toDomElements = function ( dataElement, doc, converter ) {
var attributes = dataElement.attributes,
mediaClass = attributes.mediaClass,
container = doc.createElement( 'span' ),
img = doc.createElement( attributes.isError ? 'span' : this.typesToTags[ mediaClass ] ),
img = doc.createElement( attributes.isError ? 'span' : attributes.mediaTag ),
classes = [],
originalClasses = attributes.originalClasses;
@ -158,7 +158,7 @@ ve.dm.MWInlineImageNode.static.toDomElements = function ( dataElement, doc, conv
img.setAttribute( attributes.isError ? 'data-width' : 'height', height );
}
var srcAttr = this.typesToSrcAttrs[ mediaClass ];
var srcAttr = this.tagsToSrcAttrs[ img.nodeName.toLowerCase() ];
if ( srcAttr && !attributes.isError ) {
img.setAttribute( srcAttr, attributes.src );
}
@ -169,7 +169,7 @@ ve.dm.MWInlineImageNode.static.toDomElements = function ( dataElement, doc, conv
}
// RDFa type
container.setAttribute( 'typeof', this.getRdfa( mediaClass, attributes.type, attributes.isError ) );
container.setAttribute( 'typeof', this.getRdfa( attributes.mediaClass, attributes.type, attributes.isError ) );
if ( !ve.isEmptyObject( attributes.mw ) ) {
container.setAttribute( 'data-mw', JSON.stringify( attributes.mw ) );
}

View file

@ -283,6 +283,7 @@ ve.dm.mwExample.MWBlockImage = {
imageClassAttr: null,
imgWrapperClassAttr: null,
mediaClass: 'Image',
mediaTag: 'img',
src: ve.ce.minImgDataUri,
width: 1,
height: 2,
@ -321,6 +322,7 @@ ve.dm.mwExample.MWInlineImage = {
imageClassAttr: null,
imgWrapperClassAttr: null,
mediaClass: 'Image',
mediaTag: 'img',
width: 135,
height: 155,
alt: 'alt text',
@ -353,6 +355,7 @@ ve.dm.mwExample.MWInlineImageWithWrapperClass = {
imageClassAttr: null,
imgWrapperClassAttr: 'mw-file-description',
mediaClass: 'Image',
mediaTag: 'img',
width: 135,
height: 155,
alt: 'alt text',
@ -876,6 +879,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'img',
altText: null,
width: 120,
height: 120,
@ -922,6 +927,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'span',
altText: null,
width: 120,
height: 120,
@ -944,8 +951,8 @@ ve.dm.mwExample.domToDataCases = {
{ type: 'internalList' },
{ type: '/internalList' }
],
normalizedBody: '<ul class="gallery mw-gallery-packed-hover" typeof="mw:Extension/gallery" data-mw=\'{"attrs":{"mode":"packed-hover"},"body":{"extsrc":""},"name":"gallery"}\'><li class="gallerybox" style="width: 122px;"><div class="thumb"><span typeof="mw:Image"><a><span class="mw-broken-media" resource="./Foo" data-height="120" data-width="120">Foo</span></a></span></div><div class="gallerytext"></div></li></ul>',
fromDataBody: '<ul typeof="mw:Extension/gallery" data-mw=\'{"attrs":{"mode":"packed-hover"},"body":{"extsrc":""},"name":"gallery"}\'><li class="gallerybox"><div class="thumb"><span typeof="mw:Image"><a><span class="mw-broken-media" resource="./Foo" data-height="120" data-width="120">Foo</span></a></span></div><div class="gallerytext"></div></li></ul>'
normalizedBody: '<ul class="gallery mw-gallery-packed-hover" typeof="mw:Extension/gallery" data-mw=\'{"attrs":{"mode":"packed-hover"},"body":{"extsrc":""},"name":"gallery"}\'><li class="gallerybox" style="width: 122px;"><div class="thumb"><span typeof="mw:Error mw:Image"><a><span class="mw-broken-media" resource="./Foo" data-height="120" data-width="120">Foo</span></a></span></div><div class="gallerytext"></div></li></ul>',
fromDataBody: '<ul typeof="mw:Extension/gallery" data-mw=\'{"attrs":{"mode":"packed-hover"},"body":{"extsrc":""},"name":"gallery"}\'><li class="gallerybox"><div class="thumb"><span typeof="mw:Error mw:Image"><a><span class="mw-broken-media" resource="./Foo" data-height="120" data-width="120">Foo</span></a></span></div><div class="gallerytext"></div></li></ul>'
},
'mwGalleryImage (empty caption in DOM)': {
body: '<ul class="gallery mw-gallery-packed" typeof="mw:Extension/gallery" data-mw=\'{"attrs":{"mode":"packed"},"body":{"extsrc":""},"name":"gallery"}\'><li class="gallerybox" style="width: 122px;"><div class="thumb" style="width: 120px;"><span typeof="mw:Image"><a href="./Foo"><img resource="./Foo" src="' + ve.ce.minImgDataUri + '" height="120" width="120"/></a></span></div><div class="gallerytext"></div></li></ul>',
@ -968,6 +975,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'img',
altText: null,
width: 120,
height: 120,
@ -1014,6 +1023,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'img',
altText: null,
width: 120,
height: 120,
@ -1060,6 +1071,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'img',
altText: null,
width: 120,
height: 120,
@ -1095,6 +1108,8 @@ ve.dm.mwExample.domToDataCases = {
{
type: 'mwGalleryImage',
attributes: {
mediaClass: 'Image',
mediaTag: 'img',
altText: null,
width: 120,
height: 120,
@ -1126,6 +1141,7 @@ ve.dm.mwExample.domToDataCases = {
imgWrapperClassAttr: null,
isError: false,
mediaClass: 'Image',
mediaTag: 'img',
mw: {},
resource: './Foo',
src: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
@ -1162,6 +1178,7 @@ ve.dm.mwExample.domToDataCases = {
imgWrapperClassAttr: null,
isError: false,
mediaClass: 'Image',
mediaTag: 'img',
mw: {},
resource: './Foo',
src: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
@ -1197,6 +1214,7 @@ ve.dm.mwExample.domToDataCases = {
imgWrapperClassAttr: null,
isError: false,
mediaClass: 'Image',
mediaTag: 'img',
mw: {},
resource: './Foo',
src: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
@ -1232,6 +1250,7 @@ ve.dm.mwExample.domToDataCases = {
imgWrapperClassAttr: null,
isError: false,
mediaClass: 'Image',
mediaTag: 'img',
mw: {},
resource: './Foo',
src: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
@ -1258,6 +1277,7 @@ ve.dm.mwExample.domToDataCases = {
imgWrapperClassAttr: null,
isError: false,
mediaClass: 'Image',
mediaTag: 'img',
mw: {},
resource: './Foo',
src: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
@ -2015,6 +2035,7 @@ ve.dm.mwExample.domToDataCases = {
imageClassAttr: 'mw-broken-media',
imgWrapperClassAttr: null,
mediaClass: 'Image',
mediaTag: 'span',
src: null,
defaultSize: true,
width: 220,
@ -2070,6 +2091,7 @@ ve.dm.mwExample.domToDataCases = {
imageClassAttr: 'mw-broken-media',
imgWrapperClassAttr: null,
mediaClass: 'Image',
mediaTag: 'span',
src: null,
width: 200,
height: null,
@ -2131,6 +2153,7 @@ ve.dm.mwExample.domToDataCases = {
imageClassAttr: null,
imgWrapperClassAttr: null,
mediaClass: 'Image',
mediaTag: 'img',
src: ve.ce.minImgDataUri,
width: 1,
height: 2,

View file

@ -22,19 +22,23 @@ ve.ui.MWMediaContextItem = function VeUiMWMediaContextItem( context, model ) {
// Initialization
this.$element.addClass( 've-ui-mwMediaContextItem' );
var mediaClass = model.getAttribute( 'mediaClass' ) || 'Image';
var mediaTag = model.getAttribute( 'mediaTag' ) || 'img';
this.setIcon( model.getAttribute( 'isError' ) ? 'imageBroken' : {
Image: 'image',
this.setIcon( {
img: 'image',
span: 'imageBroken',
// TODO: Better icons for audio/video
Audio: 'play',
Video: 'play'
}[ mediaClass ] );
audio: 'play',
video: 'play'
}[ mediaTag ] );
var messagePostfix = ( mediaTag === 'audio' || mediaTag === 'video' ) ? mediaTag : 'image';
// The following messages are used here:
// * visualeditor-media-title-audio
// * visualeditor-media-title-image
// * visualeditor-media-title-video
this.setLabel( ve.msg( 'visualeditor-media-title-' + mediaClass.toLowerCase() ) );
this.setLabel( ve.msg( 'visualeditor-media-title-' + messagePostfix ) );
};
/* Inheritance */