/**
* @module gateway/reference
*/
const { TYPE_REFERENCE } = require( './constants.js' );
/**
* @return {Gateway}
*/
module.exports = function createReferenceGateway() {
/**
* @param {string} id
* @return {HTMLElement|null}
*/
function findReferenceTextElement( id ) {
const idSelector = `#${ CSS.escape( id ) }`;
/**
* Same alternative selectors with and without mw-… as in the RESTbased endpoint.
*
* @see https://phabricator.wikimedia.org/diffusion/GMOA/browse/master/lib/transformations/references/structureReferenceListContent.js$138
*/
return document.querySelector( `${ idSelector } .mw-reference-text, ${ idSelector } .reference-text` );
}
/**
* @param {HTMLElement} el
* @return {HTMLElement|null}
*/
function findParentReferenceTextElement( el ) {
// This finds either the inner
, or the outer
//
const ol = el.closest( 'ol' );
return ol && ol.classList.contains( 'mw-extended-references' ) ?
ol.parentElement.querySelector( '.mw-reference-text, .reference-text' ) :
null;
}
/**
* @param {HTMLElement} referenceElement
* @param {(HTMLElement|null)} parentElement
* @return {string}
*/
function scrapeReferenceText( referenceElement, parentElement ) {
if ( !parentElement ) {
return referenceElement.innerHTML;
}
return `
${ parentElement.innerHTML }
${ referenceElement.innerHTML }
`;
}
/**
* Attempts to find a single reference type identifier, limited to a list of known types.
* - When a `class="…"` attribute mentions multiple known types, the last one is used, following
* CSS semantics.
* - When there are multiple tags, the first with a known type is used.
*
* @param {HTMLElement} referenceElement
* @return {string|null}
*/
function scrapeReferenceType( referenceElement ) {
const KNOWN_TYPES = [ 'book', 'journal', 'news', 'note', 'web' ];
let type = null;
const citeTags = referenceElement.querySelectorAll( 'cite[class]' );
Array.prototype.forEach.call( citeTags, ( element ) => {
// don't need to keep scanning if one is found.
if ( type ) {
return;
}
const classNames = element.className.split( /\s+/ );
for ( let i = classNames.length; i--; ) {
if ( KNOWN_TYPES.indexOf( classNames[ i ] ) !== -1 ) {
type = classNames[ i ];
return false;
}
}
} );
return type;
}
/**
* @param {mw.Title} title
* @param {HTMLAnchorElement} el
* @return {Promise}
*/
function fetchPreviewForTitle( title, el ) {
// Need to encode the fragment again as mw.Title returns it as decoded text
const id = title.getFragment().replace( / /g, '_' );
const referenceTextElement = findReferenceTextElement( id );
if ( !referenceTextElement ||
// Skip references that don't contain anything but whitespace, e.g. a single
( !referenceTextElement.textContent.trim() && !referenceTextElement.children.length )
) {
return Promise.reject(
// Required to set showNullPreview to false and not open an error popup
{ textStatus: 'abort', textContext: 'Footnote not found or empty', xhr: { readyState: 0 } }
);
}
const referenceParentTextElement = findParentReferenceTextElement( referenceTextElement );
const model = {
url: `#${ id }`,
extract: scrapeReferenceText( referenceTextElement, referenceParentTextElement ),
type: TYPE_REFERENCE,
referenceType: scrapeReferenceType( referenceParentTextElement || referenceTextElement ),
// Note: Even the top-most HTMLHtmlElement is guaranteed to have a parent.
sourceElementId: el.parentNode.id
};
// Make promise abortable.
const promise = Promise.resolve( model );
promise.abort = () => {};
return promise;
}
return {
fetchPreviewForTitle
};
};