mediawiki-extensions-Cite/modules/ve-cite/ve.dm.MWGroupReferences.js
Adam Wight 5f888c98fa Finish safety fallback for missing groups
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
2024-09-03 13:37:31 +02:00

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 ];
};