mediawiki-extensions-Visual.../modules/ve-mw/dm/nodes/ve.dm.MWReferenceNode.js

324 lines
9.9 KiB
JavaScript
Raw Normal View History

/*!
* VisualEditor DataModel MWReferenceNode class.
*
* @copyright 2011-2014 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* DataModel MediaWiki reference node.
*
* @class
* @extends ve.dm.LeafNode
* @constructor
* @param {number} [length] Length of content data in document; ignored and overridden to 0
* @param {Object} [element] Reference to element in linear model
*/
ve.dm.MWReferenceNode = function VeDmMWReferenceNode( length, element ) {
// Parent constructor
ve.dm.LeafNode.call( this, 0, element );
// Event handlers
this.connect( this, {
'root': 'onRoot',
'unroot': 'onUnroot'
} );
};
/* Inheritance */
OO.inheritClass( ve.dm.MWReferenceNode, ve.dm.LeafNode );
/* Static members */
ve.dm.MWReferenceNode.static.name = 'mwReference';
ve.dm.MWReferenceNode.static.matchTagNames = null;
ve.dm.MWReferenceNode.static.matchRdfaTypes = [ 'mw:Extension/ref' ];
ve.dm.MWReferenceNode.static.isContent = true;
ve.dm.MWReferenceNode.static.blacklistedAnnotationTypes = [ 'link' ];
/**
* Regular expression for parsing the listKey attribute
* @static
* @property {RegExp}
* @inheritable
*/
ve.dm.MWReferenceNode.static.listKeyRegex = /^(auto|literal)\/(.*)$/;
ve.dm.MWReferenceNode.static.toDataElement = function ( domElements, converter ) {
var dataElement,
mwDataJSON = domElements[0].getAttribute( 'data-mw' ),
mwData = mwDataJSON ? JSON.parse( mwDataJSON ) : {},
body = mwData.body ? mwData.body.html : '',
refGroup = mwData.attrs && mwData.attrs.group || '',
listGroup = this.name + '/' + refGroup,
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
autoKeyed = !mwData.attrs || mwData.attrs.name === undefined,
listKey = autoKeyed ? 'auto/' + converter.internalList.getNextUniqueNumber() : 'literal/' + mwData.attrs.name,
queueResult = converter.internalList.queueItemHtml( listGroup, listKey, body ),
listIndex = queueResult.index,
contentsUsed = ( body !== '' && queueResult.isNew );
dataElement = {
'type': this.name,
'attributes': {
'mw': mwData,
'originalMw': mwDataJSON,
'childDomElements': ve.copy( Array.prototype.slice.apply( domElements[0].childNodes ) ),
'listIndex': listIndex,
'listGroup': listGroup,
'listKey': listKey,
'refGroup': refGroup,
'contentsUsed': contentsUsed
}
};
return dataElement;
};
ve.dm.MWReferenceNode.static.toDomElements = function ( dataElement, doc, converter ) {
var itemNodeHtml, originalHtml, mwData, i, iLen, keyedNodes, setContents, contentsAlreadySet,
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
originalMw, childDomElements, listKeyParts, name,
el = doc.createElement( 'span' ),
itemNodeWrapper = doc.createElement( 'div' ),
itemNode = converter.internalList.getItemNode( dataElement.attributes.listIndex ),
itemNodeRange = itemNode.getRange();
el.setAttribute( 'typeof', 'mw:Extension/ref' );
mwData = dataElement.attributes.mw ? ve.copy( dataElement.attributes.mw ) : {};
mwData.name = 'ref';
setContents = dataElement.attributes.contentsUsed;
keyedNodes = converter.internalList
.getNodeGroup( dataElement.attributes.listGroup )
.keyedNodes[dataElement.attributes.listKey];
Rich paste Allow pasting of rich (HTML) content. ve.ce.Surface * Use a sliced document clone for converting to DM HTML (copy) * Add full context to pasteTarget before copying * Add ve-pasteProtect class to spans to prevent them being dropped * Implement external paste by converting HTML to data and inserting with newFromDocumentInsertion * Remove clipboard key placeholder after read so they aren't picked up by rich paste. Hash no longer includes the placeholder. * Detect the corruption of important spans and fallback to clipboard data HTML if available. ve.dm.LinearData * Add clone method for copy ve.dm.ElementLinearData * Add compareUnannotated for use by context diffing. * Add sanitize method for cleaning data according to a set of rules. ve.dm.Transaction * Add range parameter for inserting a range of a document only, e.g. stripping the paste context. ve.dm.Document * Implement sliced document clone creation so that DM HTML is generated correctly in onCopy ve.dm.DocumentSlice * Replaces LinearDataSlice. Now has two ranges for balanced data and data with a full context. ve.init.Target.js * Define default, loose, paste rules (just remove aliens). ve.init.mw.ViewPageTarget * Define strict MW paste rules: + no links, spans, underlines + no images, divs, aliens + strip extra HTML attribues ve.init.sa.Target, ve.init.mw.ViewPageTarget, ve.ui.Surface * Pass through and store paste rules. Bug: 41193 Bug: 48170 Bug: 50128 Bug: 53828 Change-Id: I38d63e31ee3e3ee11707e3fffed5174e1d633b42
2013-09-30 13:26:33 +00:00
if ( setContents ) {
// Check if a previous node has already set the content. If so, we don't overwrite this
// node's contents.
contentsAlreadySet = false;
if ( keyedNodes ) {
for ( i = 0, iLen = keyedNodes.length; i < iLen; i++ ) {
if ( keyedNodes[i].element === dataElement ) {
break;
}
if ( keyedNodes[i].element.attributes.contentsUsed ) {
contentsAlreadySet = true;
break;
}
}
}
} else {
// Check if any other nodes with this key provided content. If not
// then we attach the contents to the first reference with this key
Rich paste Allow pasting of rich (HTML) content. ve.ce.Surface * Use a sliced document clone for converting to DM HTML (copy) * Add full context to pasteTarget before copying * Add ve-pasteProtect class to spans to prevent them being dropped * Implement external paste by converting HTML to data and inserting with newFromDocumentInsertion * Remove clipboard key placeholder after read so they aren't picked up by rich paste. Hash no longer includes the placeholder. * Detect the corruption of important spans and fallback to clipboard data HTML if available. ve.dm.LinearData * Add clone method for copy ve.dm.ElementLinearData * Add compareUnannotated for use by context diffing. * Add sanitize method for cleaning data according to a set of rules. ve.dm.Transaction * Add range parameter for inserting a range of a document only, e.g. stripping the paste context. ve.dm.Document * Implement sliced document clone creation so that DM HTML is generated correctly in onCopy ve.dm.DocumentSlice * Replaces LinearDataSlice. Now has two ranges for balanced data and data with a full context. ve.init.Target.js * Define default, loose, paste rules (just remove aliens). ve.init.mw.ViewPageTarget * Define strict MW paste rules: + no links, spans, underlines + no images, divs, aliens + strip extra HTML attribues ve.init.sa.Target, ve.init.mw.ViewPageTarget, ve.ui.Surface * Pass through and store paste rules. Bug: 41193 Bug: 48170 Bug: 50128 Bug: 53828 Change-Id: I38d63e31ee3e3ee11707e3fffed5174e1d633b42
2013-09-30 13:26:33 +00:00
// Check that this is the first reference with its key
if ( keyedNodes && dataElement === keyedNodes[0].element ) {
setContents = true;
// Check no other reference originally defined the contents
// As this is keyedNodes[0] we can start at 1
for ( i = 1, iLen = keyedNodes.length; i < iLen; i++ ) {
if ( keyedNodes[i].element.attributes.contentsUsed ) {
setContents = false;
break;
}
}
}
}
if ( setContents && !contentsAlreadySet ) {
converter.getDomSubtreeFromData(
itemNode.getDocument().getFullData( new ve.Range( itemNodeRange.start, itemNodeRange.end ), true ),
itemNodeWrapper
);
itemNodeHtml = $( itemNodeWrapper ).html(); // Returns '' if itemNodeWrapper is empty
originalHtml = ve.getProp( mwData, 'body', 'html' ) || '';
// Only set body.html if itemNodeHtml and originalHtml are actually different
if ( !$( '<div>' ).html( originalHtml ).get( 0 ).isEqualNode( itemNodeWrapper ) ) {
ve.setProp( mwData, 'body', 'html', itemNodeHtml );
}
}
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
// Generate name
listKeyParts = dataElement.attributes.listKey.match( this.listKeyRegex );
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
if ( listKeyParts[1] === 'auto' ) {
// Only render a name if this key was reused
if ( keyedNodes.length > 1 ) {
// Allocate a unique list key, then strip the 'literal/'' prefix
name = converter.internalList.getUniqueListKey(
dataElement.attributes.listGroup,
dataElement.attributes.listKey,
// Generate a name starting with ':' to distinguish it from normal names
'literal/:'
).substr( 'literal/'.length );
} else {
name = undefined;
}
} else {
// Use literal name
name = listKeyParts[2];
}
Prevent naming collisions when generating unique reference names Simply generating ':3' as the "unique" name for the 4th reference doesn't work. Even if getUniqueListKey() had been used, that only checks for conflicts with names that have already been encountered (i.e. occur in <ref> tags that precede the current one), not for conflicts with names that first occur further down in the document. The solution is to generate names at serialization time, when we have full knowledge of which names are in use. Internally, we use 'literal/<name>' for names that literally appeared in the source, and 'auto/<number>' for unnamed references. Then at serialization time, we translate 'auto/<number>' to 'literal/:<number>' if needed (i.e. if the reference was reused). ve.dm.MWReferenceNode.js: * toDataElement() ** Prefix listKey with literal/ or auto/ as appropriate * toDomElements() ** Map auto/ listKeys to unique names ** Don't try to unset the name if not present (was unsetting a property that didn't exist anyway) ve.dm.InternalList.js: * Remove now-unused isUniqueListKey() * Rewrite getUniqueListKey() ** Make prefix configurable ** Take previously generated unique keys into account ** Map the same old key (auto/N) to the same generated key (literal/:M) * Add getNextUniqueNumber() as a source for auto/N numbers: previously used the length of the itemHtmlQueue, but that only works during conversion, not from the UI dialog ve.ui.MWReferenceDialog.js: * For new references or conflicting names, generate an auto/N key and let toDomElements() deal with actually mapping that to name ve.dm.InternalList.test.js: * Rename listKeys to new style * Split the test case into two groups so we can test multi-group cases * Add tests for getUniqueListKey() ve.dm.mwExample.js: * Rename things to new style * Modify the test case so it attempts to trigger bug 54341 Bug: 54341 Change-Id: I726fb83e6fb66ffec643d996768a854ec9474b3d
2013-09-19 22:57:08 +00:00
// Set name
if ( name !== undefined ) {
ve.setProp( mwData, 'attrs', 'name', name );
}
// Set or clear group
if ( dataElement.attributes.refGroup !== '' ) {
ve.setProp( mwData, 'attrs', 'group', dataElement.attributes.refGroup );
} else if ( mwData.attrs ) {
delete mwData.attrs.refGroup;
}
// If mwAttr and originalMw are the same, use originalMw to prevent reserialization.
// Reserialization has the potential to reorder keys and so change the DOM unnecessarily
originalMw = dataElement.attributes.originalMw;
if ( originalMw && ve.compare( mwData, JSON.parse( originalMw ) ) ) {
el.setAttribute( 'data-mw', originalMw );
// Restore the span's childNodes to prevent unnecessary DOM diffs
childDomElements = ve.copyDomElements( dataElement.attributes.childDomElements, doc );
for ( i = 0, iLen = childDomElements.length; i < iLen; i++ ) {
el.appendChild( childDomElements[i] );
}
} else {
el.setAttribute( 'data-mw', JSON.stringify( mwData ) );
// HTML for the external clipboard, it will be ignored by the converter
$( el ).append(
$( '<sup>', doc ).text( this.getIndexLabel( dataElement, converter.internalList ) )
);
}
return [ el ];
};
ve.dm.MWReferenceNode.static.remapInternalListIndexes = function ( dataElement, mapping, internalList ) {
var listKeyParts;
// Remap listIndex
dataElement.attributes.listIndex = mapping[dataElement.attributes.listIndex];
// Remap listKey if it was automatically generated
listKeyParts = dataElement.attributes.listKey.match( this.listKeyRegex );
if ( listKeyParts[1] === 'auto' ) {
dataElement.attributes.listKey = 'auto/' + internalList.getNextUniqueNumber();
}
};
Rich paste Allow pasting of rich (HTML) content. ve.ce.Surface * Use a sliced document clone for converting to DM HTML (copy) * Add full context to pasteTarget before copying * Add ve-pasteProtect class to spans to prevent them being dropped * Implement external paste by converting HTML to data and inserting with newFromDocumentInsertion * Remove clipboard key placeholder after read so they aren't picked up by rich paste. Hash no longer includes the placeholder. * Detect the corruption of important spans and fallback to clipboard data HTML if available. ve.dm.LinearData * Add clone method for copy ve.dm.ElementLinearData * Add compareUnannotated for use by context diffing. * Add sanitize method for cleaning data according to a set of rules. ve.dm.Transaction * Add range parameter for inserting a range of a document only, e.g. stripping the paste context. ve.dm.Document * Implement sliced document clone creation so that DM HTML is generated correctly in onCopy ve.dm.DocumentSlice * Replaces LinearDataSlice. Now has two ranges for balanced data and data with a full context. ve.init.Target.js * Define default, loose, paste rules (just remove aliens). ve.init.mw.ViewPageTarget * Define strict MW paste rules: + no links, spans, underlines + no images, divs, aliens + strip extra HTML attribues ve.init.sa.Target, ve.init.mw.ViewPageTarget, ve.ui.Surface * Pass through and store paste rules. Bug: 41193 Bug: 48170 Bug: 50128 Bug: 53828 Change-Id: I38d63e31ee3e3ee11707e3fffed5174e1d633b42
2013-09-30 13:26:33 +00:00
ve.dm.MWReferenceNode.static.remapInternalListKeys = function ( dataElement, internalList ) {
var suffix = '';
// Try name, name2, name3, ... until unique
while ( ve.indexOf( dataElement.attributes.listKey + suffix, internalList.keys ) !== -1 ) {
suffix = suffix ? suffix + 1 : 2;
}
if ( suffix ) {
dataElement.attributes.listKey = dataElement.attributes.listKey + suffix;
}
};
/**
* Gets the index label for the reference
* @static
* @param {Object} dataElement Element data
* @param {ve.dm.InternalList} internalList Internal list
* @returns {string} Reference label
*/
ve.dm.MWReferenceNode.static.getIndexLabel = function ( dataElement, internalList ) {
var listIndex = dataElement.attributes.listIndex,
listGroup = dataElement.attributes.listGroup,
refGroup = dataElement.attributes.refGroup,
position = internalList.getIndexPosition( listGroup, listIndex );
return '[' + ( refGroup ? refGroup + ' ' : '' ) + ( position + 1 ) + ']';
};
/* Methods */
/**
* Don't allow reference nodes to be inspected if we can't find their contents.
*
* @inheritdoc
*/
ve.dm.MWReferenceNode.prototype.isInspectable = function () {
var internalItem = this.getInternalItem();
return internalItem && internalItem.getLength() > 0;
};
/**
* Gets the internal item node associated with this node
* @method
* @returns {ve.dm.InternalItemNode} Item node
*/
ve.dm.MWReferenceNode.prototype.getInternalItem = function () {
return this.getDocument().getInternalList().getItemNode( this.getAttribute( 'listIndex' ) );
};
/**
* Gets the index label for the reference
* @method
* @returns {string} Reference label
*/
ve.dm.MWReferenceNode.prototype.getIndexLabel = function () {
return this.constructor.static.getIndexLabel( this.element, this.getDocument().getInternalList() );
};
/**
* Handle the node being attached to the root
* @method
*/
ve.dm.MWReferenceNode.prototype.onRoot = function () {
this.addToInternalList();
};
/**
* Handle the node being detatched from the root
* @method
*/
ve.dm.MWReferenceNode.prototype.onUnroot = function () {
this.removeFromInternalList();
};
/**
* Register the node with the internal list
* @method
*/
ve.dm.MWReferenceNode.prototype.addToInternalList = function () {
if ( this.getRoot() === this.getDocument().getDocumentNode() ) {
this.getDocument().getInternalList().addNode(
this.element.attributes.listGroup,
this.element.attributes.listKey,
this.element.attributes.listIndex,
this
);
}
};
/**
* Unregister the node from the internal list
* @method
*/
ve.dm.MWReferenceNode.prototype.removeFromInternalList = function () {
this.getDocument().getInternalList().removeNode(
this.element.attributes.listGroup,
this.element.attributes.listKey,
this.element.attributes.listIndex,
this
);
};
/** */
ve.dm.MWReferenceNode.prototype.getClonedElement = function () {
var clone = ve.dm.LeafNode.prototype.getClonedElement.call( this );
delete clone.attributes.contentsUsed;
delete clone.attributes.mw;
delete clone.attributes.originalMw;
return clone;
};
/* Registration */
ve.dm.modelRegistry.register( ve.dm.MWReferenceNode );