mediawiki-extensions-Visual.../modules/ve-mw/dm/nodes/ve.dm.MWMagicLinkNode.js
James D. Forrester b518e55ef9 docs: Replace JSDuck with JSDoc (and pull-through VE with said change)
This is not great, but it's a start (and unblocks other pull-throughs).

New changes:
c401efc98 build: Replace jsduck with jsdoc for documentation
16ba162a0 JSDoc: @mixins -> @mixes
9e0a1f53b JSDoc: Fix complex return types
449b6cc0f Prefer arrow function callbacks
1539af2c8 Remove 'this' bindings in arrow functions
b760f3b14 Use arrow functions in OO.ui.Process steps
57c24109e Use arrow functions in jQuery callbacks
9622ccef9 Convert some remaining functions callbacks to arrow functions
f6c885021 Remove useless local variable
1cd800020 Clear branch node cache when rebuilding tree

Bug: T250843
Bug: T363329
Change-Id: I0f4878ca84b95e3f388b358b943f105637e455f9
2024-04-29 16:16:50 +01:00

418 lines
11 KiB
JavaScript

/*!
* VisualEditor DataModel MWMagicLinkNode class.
*
* @copyright See AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* DataModel MediaWiki magic link node.
*
* @class
* @extends ve.dm.LeafNode
* @mixins ve.dm.FocusableNode
*
* @constructor
* @param {Object} [element] Reference to element in linear model
*/
ve.dm.MWMagicLinkNode = function VeDmMWMagicLinkNode() {
// Parent constructor
ve.dm.MWMagicLinkNode.super.apply( this, arguments );
// Mixin constructors
ve.dm.FocusableNode.call( this );
};
/* Inheritance */
OO.inheritClass( ve.dm.MWMagicLinkNode, ve.dm.LeafNode );
OO.mixinClass( ve.dm.MWMagicLinkNode, ve.dm.FocusableNode );
/* Static Properties */
ve.dm.MWMagicLinkNode.static.name = 'link/mwMagic';
ve.dm.MWMagicLinkNode.static.isContent = true;
ve.dm.MWMagicLinkNode.static.matchTagNames = [ 'a' ];
ve.dm.MWMagicLinkNode.static.matchRdfaTypes = [ 'mw:WikiLink', 'mw:ExtLink' ];
// Allow additional 'rel' values in Parsoid output (T321437)
ve.dm.MWMagicLinkNode.static.allowedRdfaTypes = [ 'nofollow', 'noreferrer', 'noopener' ];
ve.dm.MWMagicLinkNode.static.disallowedAnnotationTypes = [ 'link' ];
/**
* Determine whether the given `element` is a magic link.
*
* @param {HTMLElement} element
* @return {boolean} True if the element is a magic link
*/
ve.dm.MWMagicLinkNode.static.matchFunction = function ( element ) {
var children = element.childNodes,
href = element.getAttribute( 'href' );
// All children must be text nodes, or a <span> representing an entity.
for ( var i = 0; i < children.length; i++ ) {
if ( children[ i ].nodeType === Node.TEXT_NODE ) {
continue;
}
// <span typeof='mw:Entity'>...</span> (for &nbsp;)
if ( children[ i ].nodeType === Node.ELEMENT_NODE &&
children[ i ].tagName === 'SPAN' &&
children[ i ].getAttribute( 'typeof' ) === 'mw:Entity' ) {
continue;
}
return false;
}
// Check that text content matches one of the magic link types and that
// the href matches that expected for the magic link type.
return ve.dm.MWMagicLinkNode.static.validateHref( element.textContent, href );
};
/**
* Test that a proposed content string is valid for a magic link.
* If `optType` is given, additionally verify that the content string is
* valid for the particular type of magic link (RFC/ISBN/PMID).
*
* @param {string} content
* The content string to test.
* @param {string} [optType]
* The desired type of magic link, one of "RFC", "ISBN", or "PMID".
* If not supplied, returns true if the content is valid for any one
* of these.
* @return {boolean}
* True if the content string is valid for a magic link of the appropriate
* type (or any type).
*/
ve.dm.MWMagicLinkNode.static.validateContent = function ( content, optType ) {
var type = ve.dm.MWMagicLinkType.static.fromContent( content );
if ( type === null || ( optType !== undefined && type.type !== optType ) ) {
// Not a valid magic link, or a magic link of the wrong type.
return false;
}
return true;
};
/**
* Test that a proposed content string and href is valid for a magic link.
*
* @param {string} content
* The content string to test.
* @param {string} href
* The URL target of the magic link.
* @return {boolean}
* True if the content string and href are valid for a magic link.
*/
ve.dm.MWMagicLinkNode.static.validateHref = function ( content, href ) {
var type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type && type.matchHref( href );
};
/**
* Return a link annotation appropriate for converting a magic link
* with the given content into a simple link, or `null` if the given
* content is not a valid magic link.
*
* @param {string} content
* @return {ve.dm.MWExternalLinkAnnotation|ve.dm.MWInternalLinkAnnotation|null}
*/
ve.dm.MWMagicLinkNode.static.annotationFromContent = function ( content ) {
var type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type !== null ? type.getAnnotation() : null;
};
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkNode.static.toDataElement = function ( domElements ) {
var textContent = domElements[ 0 ].textContent,
htmlContent = domElements[ 0 ].innerHTML;
return {
type: this.name,
attributes: {
content: textContent,
// These next two attributes allow lossless round-tripping
// if the original wikitext contained html entities like
// &nbsp;
origText: textContent,
origHtml: htmlContent
}
};
};
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkNode.static.toDomElements = function ( dataElement, doc ) {
var content = dataElement.attributes.content,
type = ve.dm.MWMagicLinkType.static.fromContent( content ),
href = type.getHref(),
domElement = doc.createElement( 'a' );
domElement.setAttribute( 'href', href );
domElement.setAttribute( 'rel', type.rel );
if ( content === dataElement.attributes.origText ) {
// Preserve <span typeof="mw:Entity"> elements from the original.
domElement.innerHTML = dataElement.attributes.origHtml;
} else {
domElement.textContent = content;
}
return [ domElement ];
};
/* Methods */
/**
* Return the link target appropriate for this magic link node.
*
* @return {string} Link href
*/
ve.dm.MWMagicLinkNode.prototype.getHref = function () {
var content = this.element.attributes.content,
type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type.getHref();
};
/**
* Return the rel attribute appropriate for this magic link node.
*
* @return {string} Either "mw:ExtLink" or "mw:WikiLink"
*/
ve.dm.MWMagicLinkNode.prototype.getRel = function () {
var content = this.element.attributes.content,
type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type.rel;
};
/**
* Return the type of this magic link node: one of "RFC", "PMID", or "ISBN".
*
* @return {string} Magic link type
*/
ve.dm.MWMagicLinkNode.prototype.getMagicType = function () {
var content = this.element.attributes.content,
type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type.type;
};
/**
* Return the numeric code associated with this magic link node.
*
* @return {string}
*/
ve.dm.MWMagicLinkNode.prototype.getCode = function () {
var content = this.element.attributes.content,
type = ve.dm.MWMagicLinkType.static.fromContent( content );
return type.num;
};
/**
* Encapsulation of a particular magic link type.
*
* @class
* @private
*
* @constructor
* @param {string} type
* The type of magic link; one of `'ISBN'`, `'PMID'`, or `'RFC'`.
* @param {string} rel
* The value of the link's "rel" attribute.
* Either `'mw:ExtLink'` or `'mw:WikiLink'`.
* @param {string} content
* The content of the magic link.
*/
ve.dm.MWMagicLinkType = function VeDmMWMagicLinkType( type, rel, content ) {
this.type = type;
this.rel = rel;
this.content = content;
// Make the code available as a property; this is also used for
// validity checking.
this.code = this.getCode();
};
OO.initClass( ve.dm.MWMagicLinkType );
/**
* @inheritdoc ve.dm.MWMagicLinkNode#annotationFromContent
*/
ve.dm.MWMagicLinkType.prototype.getAnnotation = function () {
return new ve.dm.MWExternalLinkAnnotation( {
type: 'link/mwExternal',
attributes: { href: this.getHref() }
} );
};
/**
* @inheritdoc ve.dm.MWMagicLinkNode#getCode
* @protected
*/
ve.dm.MWMagicLinkType.prototype.getCode = function () {
var m = /^([A-Z]+)[\t \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+(\d+)$/.exec( this.content );
if ( !m || m[ 1 ] !== this.type ) {
return null;
}
return m[ 2 ];
};
/**
* @method ve.dm.MWMagicLinkNode.getHref
* @inheritdoc ve.dm.MWMagicLinkNode#getHref
*/
/**
* Return true if the given href is appropriate for this magic link.
*
* @param {string} href
* @return {boolean}
*/
ve.dm.MWMagicLinkType.prototype.matchHref = function ( href ) {
return href.replace( /^https?:/i, '' ) === this.getHref().replace( /^https?:/i, '' );
};
/**
* Return the subclass of {@link ve.dm.MWMagicLinkType}
* appropriate for the given content, or `null` if the content
* is not appropriate for a magic link.
*
* @param {string} content
* @return {ve.dm.MWMagicLinkType|null}
*/
ve.dm.MWMagicLinkType.static.fromContent = function ( content ) {
var m = /^(ISBN|PMID|RFC)/.exec( content ),
typeStr = m && m[ 1 ],
type = null;
if ( typeStr === 'ISBN' ) {
type = new ve.dm.MWMagicLinkIsbnType( content );
} else if ( typeStr === 'PMID' ) {
type = new ve.dm.MWMagicLinkPmidType( content );
} else if ( typeStr === 'RFC' ) {
type = new ve.dm.MWMagicLinkRfcType( content );
}
// validate parsed number
return type && type.code !== null ? type : null;
};
/**
* An ISBN magic link.
*
* @class
* @extends ve.dm.MWMagicLinkType
* @private
*
* @constructor
* @param {string} content
*/
ve.dm.MWMagicLinkIsbnType = function VeDmMWMagicLinkIsbnType( content ) {
// Parent constructor
ve.dm.MWMagicLinkIsbnType.super.call( this, 'ISBN', 'mw:WikiLink', content );
};
OO.inheritClass( ve.dm.MWMagicLinkIsbnType, ve.dm.MWMagicLinkType );
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkIsbnType.prototype.getAnnotation = function () {
var conf = mw.config.get( 'wgVisualEditorConfig' ),
title = mw.Title.newFromText( conf.specialBooksources + '/' + this.code );
return ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title );
};
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkIsbnType.prototype.getCode = function () {
var content = this.content;
if ( !/^ISBN[^-0-9][\s\S]+[0-9Xx]$/.test( content ) ) {
return null;
}
// Remove unicode whitespace and dashes
var spaceOrDash = /[-\t \u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]+/g;
var isbnCode = content.replace( spaceOrDash, '' ).replace( /^ISBN/, '' );
// Verify format of ISBN
if ( !/^(97[89])?\d{9}[0-9Xx]$/.test( isbnCode ) ) {
return null;
}
return isbnCode.toUpperCase();
};
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkIsbnType.prototype.getHref = function () {
var conf = mw.config.get( 'wgVisualEditorConfig' );
return './' + conf.specialBooksources + '/' + this.code;
};
/**
* @inheritdoc
*/
ve.dm.MWMagicLinkIsbnType.prototype.matchHref = function ( href ) {
var conf = mw.config.get( 'wgVisualEditorConfig' ),
matches = /^(?:[.]+\/)*([^/]+)\/(\d+[Xx]?)$/.exec( href );
if ( !matches ) {
return false;
}
// conf.specialBooksources has localized name for Special:Booksources
var normalized = mw.libs.ve.decodeURIComponentIntoArticleTitle( matches[ 1 ], true ).replace( ' ', '_' );
if ( normalized !== 'Special:BookSources' && normalized !== conf.specialBooksources ) {
return false;
}
if ( matches[ 2 ] !== this.code ) {
return false;
}
return true;
};
/**
* A PMID magic link.
*
* @class
* @extends ve.dm.MWMagicLinkType
* @private
*
* @constructor
* @param {string} content
*/
ve.dm.MWMagicLinkPmidType = function VeDmMWMagicLinkPmidType( content ) {
// Parent constructor
ve.dm.MWMagicLinkPmidType.super.call( this, 'PMID', 'mw:ExtLink', content );
};
OO.inheritClass( ve.dm.MWMagicLinkPmidType, ve.dm.MWMagicLinkType );
ve.dm.MWMagicLinkPmidType.prototype.getHref = function () {
return mw.msg( 'pubmedurl', this.code );
};
/**
* An RFC magic link.
*
* @class
* @extends ve.dm.MWMagicLinkType
* @private
*
* @constructor
* @param {string} content
*/
ve.dm.MWMagicLinkRfcType = function VeDmMWMagicLinkRfcType( content ) {
// Parent constructor
ve.dm.MWMagicLinkRfcType.super.call( this, 'RFC', 'mw:ExtLink', content );
};
OO.inheritClass( ve.dm.MWMagicLinkRfcType, ve.dm.MWMagicLinkType );
ve.dm.MWMagicLinkRfcType.prototype.getHref = function () {
return mw.msg( 'rfcurl', this.code );
};
/* Registration */
ve.dm.modelRegistry.register( ve.dm.MWMagicLinkNode );