From 35e8af7d401a998ea3110ea7e48688fbd0a92ce5 Mon Sep 17 00:00:00 2001 From: Adam Wight Date: Fri, 23 Aug 2024 16:25:51 +0200 Subject: [PATCH] Clean up reflist usage of MWGroupReferences Switch this usage entirely to the new interface, moving much of the junk out. Bug: T372871 Change-Id: I445af469a823144b0b6fa5e6f4f23a858939e9e6 --- jsdoc.json | 1 + modules/ve-cite/ve.ce.MWReferencesListNode.js | 45 ++++----- modules/ve-cite/ve.dm.MWDocumentReferences.js | 3 +- modules/ve-cite/ve.dm.MWGroupReferences.js | 95 ++++++++++++++++++- 4 files changed, 112 insertions(+), 32 deletions(-) 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