diff --git a/jsdoc.json b/jsdoc.json
index d933418b5..ca0d97301 100644
--- a/jsdoc.json
+++ b/jsdoc.json
@@ -25,6 +25,7 @@
"ve.ui.MW": "https://doc.wikimedia.org/VisualEditor/master/js/{type}.html",
"mw.": "https://doc.wikimedia.org/mediawiki-core/master/js/{type}.html",
"ve.dm.MWDocumentReferences": true,
+ "ve.dm.MWGroupReferences": true,
"ve.dm.MWReference": true,
"ve.ce.MWReference": true,
"ve.ui.MWReference": true
diff --git a/modules/ve-cite/ve.ce.MWReferencesListNode.js b/modules/ve-cite/ve.ce.MWReferencesListNode.js
index 029f25b44..269b77679 100644
--- a/modules/ve-cite/ve.ce.MWReferencesListNode.js
+++ b/modules/ve-cite/ve.ce.MWReferencesListNode.js
@@ -27,7 +27,6 @@ ve.ce.MWReferencesListNode = function VeCeMWReferencesListNode() {
this.internalList = null;
this.listNode = null;
this.modified = false;
- this.docRefs = null;
// DOM changes
this.$element.addClass( 've-ce-mwReferencesListNode' );
@@ -175,12 +174,11 @@ ve.ce.MWReferencesListNode.prototype.update = function () {
return;
}
- this.docRefs = ve.dm.MWDocumentReferences.static.refsForDoc( model.getDocument() );
- const internalList = model.getDocument().internalList;
const refGroup = model.getAttribute( 'refGroup' );
- const listGroup = model.getAttribute( 'listGroup' );
- const nodes = internalList.getNodeGroup( listGroup );
- const hasModelReferences = !!( nodes && nodes.indexOrder.length );
+
+ const docRefs = ve.dm.MWDocumentReferences.static.refsForDoc( model.getDocument() );
+ const groupRefs = docRefs.getGroupRefs( refGroup );
+ const hasModelReferences = !groupRefs.isEmpty();
let emptyText;
if ( refGroup !== '' ) {
@@ -240,14 +238,11 @@ ve.ce.MWReferencesListNode.prototype.update = function () {
this.$refmsg.text( emptyText );
this.$element.append( this.$refmsg );
} else {
- const groupRefs = this.docRefs.getGroupRefs( listGroup );
+ // Render all at once.
this.$reflist.append(
- // FIXME: Clean up access functions.
- Object.keys( groupRefs.footnoteNumberLookup )
- .filter( ( listKey ) => groupRefs.footnoteNumberLookup[ listKey ][ 1 ] === -1 )
- .sort( ( aKey, bKey ) => groupRefs.footnoteNumberLookup[ aKey ][ 0 ] - groupRefs.footnoteNumberLookup[ bKey ][ 0 ] )
+ groupRefs.getTopLevelKeysInReflistOrder()
.map( ( listKey ) => this.renderListItem(
- nodes, internalList, groupRefs, refGroup, listKey
+ groupRefs, refGroup, listKey
) )
);
@@ -260,31 +255,22 @@ ve.ce.MWReferencesListNode.prototype.update = function () {
* Render a reference list item
*
* @private
- * @param {Object} nodes Node group object, containing nodes and key order array
- * @param {ve.dm.InternalList} internalList Internal list
* @param {ve.dm.MWGroupReferences} groupRefs object holding calculated information about all group refs
* @param {string} refGroup Reference group
* @param {string} key top-level reference key, doesn't necessarily exist
* @return {jQuery} Rendered list item
*/
-ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internalList, groupRefs, refGroup, key ) {
- const keyedNodes = nodes.keyedNodes[ key ] || [];
- const node = keyedNodes ? keyedNodes[ 0 ] : null;
- const listIndex = node ? node.getAttribute( 'listIndex' ) : null;
- const backlinkNodes = keyedNodes.filter(
- // Exclude placeholders and references defined inside the references list node
- ( backRefNode ) => !backRefNode.getAttribute( 'placeholder' ) && !backRefNode.findParent( ve.dm.MWReferencesListNode )
- );
- const subrefs = groupRefs.subRefsByParent[ key ] || [];
+ve.ce.MWReferencesListNode.prototype.renderListItem = function ( groupRefs, refGroup, key ) {
+ const ref = groupRefs.getInternalModelNode( key );
+ const backlinkNodes = groupRefs.getRefUsages( key );
+ const subrefs = groupRefs.getSubrefs( key );
const $li = $( '
' )
.css( '--footnote-number', `"${ groupRefs.getIndexLabel( key ) }."` )
.append( this.renderBacklinks( backlinkNodes, refGroup ), ' ' );
- // Generate reference HTML from first item in key
- const modelNode = internalList.getItemNode( listIndex );
- if ( modelNode && modelNode.length ) {
- const refPreview = new ve.ui.MWPreviewElement( modelNode, { useView: true } );
+ if ( ref && ref.length ) {
+ const refPreview = new ve.ui.MWPreviewElement( ref, { useView: true } );
$li.append(
$( '' )
.addClass( 'reference-text' )
@@ -295,7 +281,8 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal
const surface = this.getRoot().getSurface().getSurface();
// TODO: attach to the singleton click handler on the surface
$li.on( 'mousedown', ( e ) => {
- if ( ve.isUnmodifiedLeftClick( e ) && modelNode && modelNode.length ) {
+ if ( ve.isUnmodifiedLeftClick( e ) ) {
+ const node = groupRefs.getRefNode( key );
const items = ve.ui.contextItemFactory.getRelatedItems( [ node ] )
.filter( ( item ) => item.name !== 'mobileActions' );
if ( items.length ) {
@@ -336,7 +323,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal
$li.append(
$( '' ).append(
subrefs.map( ( subNode ) => this.renderListItem(
- nodes, internalList, groupRefs, refGroup, subNode.getAttribute( 'listKey' )
+ groupRefs, refGroup, subNode.getAttribute( 'listKey' )
) )
)
);
diff --git a/modules/ve-cite/ve.dm.MWDocumentReferences.js b/modules/ve-cite/ve.dm.MWDocumentReferences.js
index 6a0269b52..5d1ececc4 100644
--- a/modules/ve-cite/ve.dm.MWDocumentReferences.js
+++ b/modules/ve-cite/ve.dm.MWDocumentReferences.js
@@ -85,7 +85,8 @@ ve.dm.MWDocumentReferences.prototype.updateGroup = function ( groupName ) {
* @return {ve.dm.MWGroupReferences}
*/
ve.dm.MWDocumentReferences.prototype.getGroupRefs = function ( groupName ) {
- return this.cachedByGroup[ groupName.startsWith( 'mwReference/' ) ? groupName : 'mwReference/' + groupName ];
+ return this.cachedByGroup[ groupName.startsWith( 'mwReference/' ) ? groupName : 'mwReference/' + groupName ] ||
+ new ve.dm.MWGroupReferences();
};
ve.dm.MWDocumentReferences.prototype.getAllGroupNames = function () {
diff --git a/modules/ve-cite/ve.dm.MWGroupReferences.js b/modules/ve-cite/ve.dm.MWGroupReferences.js
index 198ff4258..c4c8ee3c3 100644
--- a/modules/ve-cite/ve.dm.MWGroupReferences.js
+++ b/modules/ve-cite/ve.dm.MWGroupReferences.js
@@ -11,7 +11,6 @@
* 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.
*
- * @private
* @constructor
*/
ve.dm.MWGroupReferences = function VeDmMWGroupReferences() {
@@ -19,13 +18,33 @@ ve.dm.MWGroupReferences = function VeDmMWGroupReferences() {
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.}
+ */
this.footnoteNumberLookup = {};
- // FIXME: push labeling to presentation code and drop from here.
+ /**
+ * 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.}
+ */
this.footnoteLabelLookup = {};
+ /**
+ * Lookup from parent listKey to subrefs.
+ *
+ * @member {Object.}
+ */
this.subRefsByParent = {};
/** @private */
this.topLevelCounter = 1;
+ /** @private */
this.nodeGroup = null;
};
@@ -100,6 +119,20 @@ ve.dm.MWGroupReferences.prototype.addSubref = function ( parentKey, listKey, sub
};
/**
+ * 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 () {
@@ -110,6 +143,64 @@ ve.dm.MWGroupReferences.prototype.getAllRefsInDocumentOrder = function () {
.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.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 `` 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.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