mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Cite
synced 2024-11-24 06:54:00 +00:00
5f888c98fa
In a document without references, the default group doesn't exist. Make sure that we don't assume any group's existence. Bug: T373661 Change-Id: I1bfd41b0b61938f32219d61142cd576c8ca97ace
225 lines
6.9 KiB
JavaScript
225 lines
6.9 KiB
JavaScript
'use strict';
|
|
|
|
/*!
|
|
* @copyright 2024 VisualEditor Team's Cite sub-team and others; see AUTHORS.txt
|
|
* @license MIT
|
|
*/
|
|
|
|
/**
|
|
* Holds information about the refs from a single Cite group.
|
|
*
|
|
* This structure is persisted in memory until a document change affects a ref
|
|
* tag from this group, at which point it will be fully recalculated.
|
|
*
|
|
* @constructor
|
|
*/
|
|
ve.dm.MWGroupReferences = function VeDmMWGroupReferences() {
|
|
// Mixin constructors
|
|
OO.EventEmitter.call( this );
|
|
|
|
// Properties
|
|
/**
|
|
* Lookup from listKey to a pair of integers which are the [major, minor] footnote numbers
|
|
* that will be rendered on the ref in some digit system. Note that top-level refs always
|
|
* have minor number `-1`.
|
|
*
|
|
* @member {Object.<string, number[]>}
|
|
*/
|
|
this.footnoteNumberLookup = {};
|
|
/**
|
|
* Lookup from listKey to a rendered footnote number or subref number like "1.2", in the
|
|
* local content language.
|
|
*
|
|
* FIXME: push labeling to presentation code and drop from here.
|
|
*
|
|
* @member {Object.<string, string>}
|
|
*/
|
|
this.footnoteLabelLookup = {};
|
|
/**
|
|
* Lookup from parent listKey to subrefs.
|
|
*
|
|
* @member {Object.<string, ve.dm.MWReferenceNode[]>}
|
|
*/
|
|
this.subRefsByParent = {};
|
|
|
|
/** @private */
|
|
this.topLevelCounter = 1;
|
|
/**
|
|
* InternalList node group, or null if no such group exists.
|
|
*
|
|
* @member {Object|null}
|
|
* @private
|
|
*/
|
|
this.nodeGroup = null;
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.initClass( ve.dm.MWGroupReferences );
|
|
|
|
/* Static Methods */
|
|
|
|
/**
|
|
* Rebuild information about this group of references.
|
|
*
|
|
* @param {Object} nodeGroup InternalList group object containing refs.
|
|
* @return {ve.dm.MWGroupReferences}
|
|
*/
|
|
ve.dm.MWGroupReferences.static.makeGroupRefs = function ( nodeGroup ) {
|
|
const result = new ve.dm.MWGroupReferences();
|
|
result.nodeGroup = nodeGroup;
|
|
|
|
( nodeGroup ? nodeGroup.indexOrder : [] )
|
|
.map( ( index ) => nodeGroup.firstNodes[ index ] )
|
|
// FIXME: debug null nodes
|
|
.filter( ( node ) => node && !node.getAttribute( 'placeholder' ) )
|
|
.forEach( ( node ) => {
|
|
const listKey = node.getAttribute( 'listKey' );
|
|
const extendsRef = node.getAttribute( 'extendsRef' );
|
|
|
|
if ( !extendsRef ) {
|
|
result.getOrAllocateTopLevelIndex( listKey );
|
|
} else {
|
|
result.addSubref( extendsRef, listKey, node );
|
|
}
|
|
} );
|
|
|
|
return result;
|
|
};
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* @private
|
|
* @param {string} listKey Full key for the top-level ref
|
|
* @return {number[]} Allocated topLevelIndex
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getOrAllocateTopLevelIndex = function ( listKey ) {
|
|
if ( this.footnoteNumberLookup[ listKey ] === undefined ) {
|
|
const number = this.topLevelCounter++;
|
|
this.footnoteNumberLookup[ listKey ] = [ number, -1 ];
|
|
this.footnoteLabelLookup[ listKey ] = ve.dm.MWDocumentReferences.static.contentLangDigits( number );
|
|
}
|
|
return this.footnoteNumberLookup[ listKey ][ 0 ];
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* @param {string} parentKey Full key of the parent reference
|
|
* @param {string} listKey Full key of the subreference
|
|
* @param {ve.dm.MWReferenceNode} subrefNode Subref to add to internal tracking
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.addSubref = function ( parentKey, listKey, subrefNode ) {
|
|
if ( this.subRefsByParent[ parentKey ] === undefined ) {
|
|
this.subRefsByParent[ parentKey ] = [];
|
|
}
|
|
this.subRefsByParent[ parentKey ].push( subrefNode );
|
|
const subrefIndex = this.subRefsByParent[ parentKey ].length;
|
|
|
|
const topLevelIndex = this.getOrAllocateTopLevelIndex( parentKey );
|
|
this.footnoteNumberLookup[ listKey ] = [ topLevelIndex, subrefIndex ];
|
|
this.footnoteLabelLookup[ listKey ] = ve.dm.MWDocumentReferences.static.contentLangDigits( topLevelIndex ) +
|
|
// FIXME: RTL, and customization of the separator like with mw:referencedBy
|
|
'.' + ve.dm.MWDocumentReferences.static.contentLangDigits( subrefIndex );
|
|
};
|
|
|
|
/**
|
|
* Check whether the group has any references.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.isEmpty = function () {
|
|
// Use an internal shortcut, otherwise we could do something like
|
|
// !!nodes.indexOrder.length
|
|
return this.topLevelCounter === 1;
|
|
};
|
|
|
|
/**
|
|
* List all document references in the order they first appear, ignoring reuses
|
|
* and placeholders.
|
|
*
|
|
* @return {ve.dm.MWReferenceNode[]}
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getAllRefsInDocumentOrder = function () {
|
|
return Object.keys( this.footnoteNumberLookup )
|
|
.sort( ( aKey, bKey ) => this.footnoteNumberLookup[ aKey ][ 0 ] - this.footnoteNumberLookup[ bKey ][ 0 ] )
|
|
.map( ( listKey ) => this.nodeGroup.keyedNodes[ listKey ] )
|
|
.filter( ( nodes ) => !!nodes )
|
|
.map( ( nodes ) => nodes[ 0 ] );
|
|
};
|
|
|
|
/**
|
|
* List all reference listKeys in the order they appear in the reflist including
|
|
* named refs, unnamed refs, and those that don't resolve
|
|
*
|
|
* @return {string[]} Reference listKeys
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getTopLevelKeysInReflistOrder = function () {
|
|
return Object.keys( this.footnoteNumberLookup )
|
|
.sort( ( aKey, bKey ) => this.footnoteNumberLookup[ aKey ][ 0 ] - this.footnoteNumberLookup[ bKey ][ 0 ] )
|
|
// TODO: Function could be split here, if a use case is found for a list of
|
|
// all numbers including subrefs.
|
|
.filter( ( listKey ) => this.footnoteNumberLookup[ listKey ][ 1 ] === -1 );
|
|
};
|
|
|
|
/**
|
|
* Return the defining reference node for this key
|
|
*
|
|
* @see #getInternalModelNode
|
|
*
|
|
* @param {string} key in listKey format
|
|
* @return {ve.dm.MWReferenceNode|undefined}
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getRefNode = function ( key ) {
|
|
const keyedNodes = this.nodeGroup && this.nodeGroup.keyedNodes[ key ];
|
|
return keyedNodes && keyedNodes[ 0 ];
|
|
};
|
|
|
|
/**
|
|
* Return the internalList internal item if it exists.
|
|
*
|
|
* @see #getRefNode
|
|
*
|
|
* @param {string} key in listKey format
|
|
* @return {ve.dm.InternalItemNode|undefined}
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getInternalModelNode = function ( key ) {
|
|
const ref = this.getRefNode( key );
|
|
return ref && ref.getInternalItem();
|
|
};
|
|
|
|
/**
|
|
* Return document nodes for each usage of a ref key. This excludes usages
|
|
* under the `<references>` section, so note that nested references won't behave
|
|
* as expected. The reflist item for a ref is not counted as a reference,
|
|
* either.
|
|
*
|
|
* FIXME: Implement backlinks from within a nested ref within the footnote body.
|
|
*
|
|
* @param {string} key in listKey format
|
|
* @return {ve.dm.MWReferenceNode[]}
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getRefUsages = function ( key ) {
|
|
return ( this.nodeGroup && this.nodeGroup.keyedNodes[ key ] || [] )
|
|
.filter( ( node ) => !node.getAttribute( 'placeholder' ) &&
|
|
!node.findParent( ve.dm.MWReferencesListNode )
|
|
);
|
|
};
|
|
|
|
/**
|
|
* @param {string} parentKey parent ref key
|
|
* @return {ve.dm.MWReferenceNode[]} List of subrefs for this parent
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getSubrefs = function ( parentKey ) {
|
|
return this.subRefsByParent[ parentKey ] || [];
|
|
};
|
|
|
|
/**
|
|
* @deprecated TODO: push to presentation
|
|
* @param {string} listKey full ref key
|
|
* @return {string} rendered number label
|
|
*/
|
|
ve.dm.MWGroupReferences.prototype.getIndexLabel = function ( listKey ) {
|
|
return this.footnoteLabelLookup[ listKey ];
|
|
};
|