From d03d2d8d20d2e647e1a78aefe5b6df3a860d3e23 Mon Sep 17 00:00:00 2001 From: Adam Wight Date: Fri, 28 Jun 2024 11:54:20 +0200 Subject: [PATCH] [refactor] switch reflist rendering source of truth Pure refactor which shouldn't change output in production. Switches to interfacing with MWDocumentReferences to get refs in index order. Temporarily suppresses any subrefs, we only show top-level refs. Bug: T247921 Change-Id: I9c8347b064173027f436722c87e15e0381c958bd --- extension.json | 1 + jsdoc.json | 1 + modules/ve-cite/ve.ce.MWReferencesListNode.js | 24 ++++--- modules/ve-cite/ve.dm.MWDocumentReferences.js | 68 +++++++++++++++++++ 4 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 modules/ve-cite/ve.dm.MWDocumentReferences.js diff --git a/extension.json b/extension.json index ed4c8c5b7..43f074536 100644 --- a/extension.json +++ b/extension.json @@ -71,6 +71,7 @@ "localBasePath": "modules/ve-cite", "remoteExtPath": "Cite/modules/ve-cite", "scripts": [ + "ve.dm.MWDocumentReferences.js", "ve.dm.MWReferenceModel.js", "ve.dm.MWReferencesListNode.js", "ve.dm.MWReferenceNode.js", diff --git a/jsdoc.json b/jsdoc.json index 7160f61e1..ca541eaf6 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -29,6 +29,7 @@ "mw.": "https://doc.wikimedia.org/mediawiki-core/master/js/{type}.html" }, "prefixMapIgnore": [ + "ve.dm.MWDocumentReferences", "ve.dm.MWReference", "ve.ce.MWReference", "ve.ui.MWReference" diff --git a/modules/ve-cite/ve.ce.MWReferencesListNode.js b/modules/ve-cite/ve.ce.MWReferencesListNode.js index a80e745d2..b3df0c0d8 100644 --- a/modules/ve-cite/ve.ce.MWReferencesListNode.js +++ b/modules/ve-cite/ve.ce.MWReferencesListNode.js @@ -27,6 +27,7 @@ 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' ); @@ -174,6 +175,7 @@ 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' ); @@ -238,9 +240,11 @@ ve.ce.MWReferencesListNode.prototype.update = function () { this.$refmsg.text( emptyText ); this.$element.append( this.$refmsg ); } else { + const groupedByParent = this.docRefs.getGroupRefsByParents( listGroup ); + const topLevelNodes = groupedByParent[ '' ] || []; this.$reflist.append( - nodes.indexOrder.map( ( index ) => this.renderListItem( - nodes, internalList, refGroup, index + topLevelNodes.map( ( node ) => this.renderListItem( + nodes, internalList, refGroup, node ) ) ); @@ -256,11 +260,12 @@ ve.ce.MWReferencesListNode.prototype.update = function () { * @param {Object} nodes Node group object, containing nodes and key order array * @param {ve.dm.InternalList} internalList Internal list * @param {string} refGroup Reference group - * @param {number} index Item index - * @return {jQuery} List item + * @param {ve.dm.MWReferenceNode} node Reference node to render as a footnote body + * @return {jQuery} Rendered list item */ -ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internalList, refGroup, index ) { - const key = internalList.keys[ index ]; +ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internalList, refGroup, node ) { + const listIndex = node.getAttribute( 'listIndex' ); + const key = internalList.keys[ listIndex ]; const keyedNodes = ( nodes.keyedNodes[ key ] || [] ) .filter( // Exclude placeholders and references defined inside the references list node @@ -271,7 +276,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal .append( this.renderBacklinks( keyedNodes, refGroup ), ' ' ); // Generate reference HTML from first item in key - const modelNode = internalList.getItemNode( index ); + const modelNode = internalList.getItemNode( listIndex ); if ( modelNode && modelNode.length ) { const refPreview = new ve.ui.MWPreviewElement( modelNode, { useView: true } ); $li.append( @@ -285,8 +290,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal // TODO: attach to the singleton click handler on the surface $li.on( 'mousedown', ( e ) => { if ( ve.isUnmodifiedLeftClick( e ) && modelNode && modelNode.length ) { - const firstNode = nodes.firstNodes[ index ]; - const items = ve.ui.contextItemFactory.getRelatedItems( [ firstNode ] ) + const items = ve.ui.contextItemFactory.getRelatedItems( [ node ] ) .filter( ( item ) => item.name !== 'mobileActions' ); if ( items.length ) { const contextItem = ve.ui.contextItemFactory.lookup( items[ 0 ].name ); @@ -296,7 +300,7 @@ ve.ce.MWReferencesListNode.prototype.renderListItem = function ( nodes, internal if ( command ) { const fragmentArgs = { fragment: surface.getModel() - .getLinearFragment( firstNode.getOuterRange(), true ), + .getLinearFragment( node.getOuterRange(), true ), selectFragmentOnClose: false }; const newArgs = ve.copy( command.args ); diff --git a/modules/ve-cite/ve.dm.MWDocumentReferences.js b/modules/ve-cite/ve.dm.MWDocumentReferences.js new file mode 100644 index 000000000..db62bcaa5 --- /dev/null +++ b/modules/ve-cite/ve.dm.MWDocumentReferences.js @@ -0,0 +1,68 @@ +'use strict'; + +/*! + * @copyright 2024 VisualEditor Team's Cite sub-team and others; see AUTHORS.txt + * @license MIT + */ + +/** + * A facade providing a simplified and safe interface to Cite `ref` and + * `references` tags in a document. + * + * @constructor + * @mixes OO.EventEmitter + * @param {ve.dm.Document} doc The document that reference tags will be embedded in. + */ +ve.dm.MWDocumentReferences = function VeDmMWDocumentReferences( doc ) { + // Mixin constructors + OO.EventEmitter.call( this ); + + // Properties + this.doc = doc; +}; + +/* Inheritance */ + +OO.mixinClass( ve.dm.MWDocumentReferences, OO.EventEmitter ); + +/* Methods */ + +/** + * Singleton MWDocumentReferences for a document. + * + * @param {ve.dm.Document} doc Source document associated with the singleton + * @return {ve.dm.MWDocumentReferences} Singleton docRefs + */ +ve.dm.MWDocumentReferences.static.refsForDoc = function ( doc ) { + let docRefs = doc.getStorage( 'document-references-store' ); + if ( docRefs === undefined ) { + docRefs = new ve.dm.MWDocumentReferences( doc ); + doc.setStorage( 'document-references-store', docRefs ); + } + return docRefs; +}; + +/** + * Get all refs for a group, organized by parent ref + * + * This is appropriate when rendering a reflist organized hierarchically by + * subrefs using the `extends` feature. + * + * @param {string} groupName Filter by this group. + * @return {Object.} Mapping from parent ref + * name to a list of its subrefs. Note that the top-level refs are under the + * `null` value. + */ +ve.dm.MWDocumentReferences.prototype.getGroupRefsByParents = function ( groupName ) { + const nodeGroup = this.doc.getInternalList().getNodeGroup( groupName ); + return ( nodeGroup ? nodeGroup.indexOrder : [] ) + .reduce( ( acc, index ) => { + const node = nodeGroup.firstNodes[ index ]; + const extendsRef = node.element.attributes.extendsRef || ''; + if ( acc[ extendsRef ] === undefined ) { + acc[ extendsRef ] = []; + } + acc[ extendsRef ].push( node ); + return acc; + }, {} ); +};