User:Magnus Manske/author strings.js

From Wikidata
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// <nowiki>
/*
To add this gadget, put the following line on your common.js User subpage:

 importScript( 'User:Magnus_Manske/author_strings.js' );

*/

let author_strings = function () {

mw.loader.using(['vue', '@wikimedia/codex', 'jquery.ui', 'mediawiki.api'], function ( require ) {

	// Code to run on page load
	if ( mw.config.get('wgNamespaceNumber') != 0 ) return ;
	if ( mw.config.get('wgAction') != 'view' ) return ;
	if ( $('#P2093').length==0  // author name string, on papers
		&& $('#P496,#P1153,#P1960,#P2798,#P106 a[title="Q1650915"]').length==0 // author 
		&& $('#P31 a[title="Q5"]').length==0 // human (portlet link)
	) return ;
	
	const CdxButton = require( '@wikimedia/codex' ).CdxButton;
	const CdxCheckbox = require( '@wikimedia/codex' ).CdxCheckbox;
	const CdxTextInput = require( '@wikimedia/codex' ).CdxTextInput;

	const AuthorStringsApp = {
		template: `<div>
<div style='float: right; font-size: 8pt;'>
	<a href='/wiki/User:Magnus_Manske/Author_strings' target='_blank'>
		<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Breathe-help-browser.svg/16px-Breathe-help-browser.svg.png' border=0 />
	</a>
</div>
<div v-if='name_collisions.length>0' style='border-bottom:1px dotted #DDD; margin-bottom: 1rem;'>
	<h3>Author/author name string duplicates</h3>
	<div style='display: flex;'>
	<table>
	<thead>
	<tr>
		<th nowrap>Author #</th>
		<th>Author</th>
		<th>Author string name</th>
	</tr>
	</thead>
	<tbody>
	<tr v-for='nc in name_collisions'>
		<td>{{nc.serial}}</td>
		<td>
			<div v-for='entry in nc.p50'>
				<a :href='"/wiki/"+entry.q' target='_blank'>{{entry.name}}</a>
			</div>
		</td>
		<td>
			<div v-for='entry in nc.ans'>
				<div style='display: flex;'>
					<CdxCheckbox v-model='entry.checked'></CdxCheckbox>
					{{entry.name}}
				</div>
			</div>
		</td>
	</tr>
	</tbody></table>
	<div style='vertical-align: top;'>
		<cdx-button @click.prevent='removeANSStatements' aria-label='Remove selected statements'>Remove selected statements</cdx-button><br/>
		Make sure the authors are the same in each case!
	</div>
	</div>
</div>

<div v-if='loading'><i>Loading...</i></div>
<div v-else-if='papers.length==0'><i>No candidates.</i></div>
<table v-else-if='show="authors"' class='author_strings_authors' style='border-collapse: collapse;'>
<tbody>
<tr v-for='author_key in authors_order'>

<td style='vertical-align: top;'>
<table>
<tr v-for='o in authors[author_key].occurrences'>
<td style='width: 1rem;'>
	<CdxCheckbox v-if='typeof o.q=="undefined"' v-model='o.checked'></CdxCheckbox>
	<span v-else-if='typeof o.status!="undefined" && o.status=="done"' style='color: green;'>✓</span>
</td>
<td nowrap>
	<span v-if='typeof o.q=="undefined"'>{{o.name}}</span>
	<a v-else :href='"/wiki/"+o.q' target='_blank'>{{o.name}}</a>
</td>
<td>
<a :href='"/wiki/"+o.paper_q' target='_blank'>
	<small>{{paper_name[o.paper_q]}}</small>
</a>
<div v-if='typeof o.status!="undefined" && o.status!="done"' style='color: red;'><small>{{o.status}}</small></div>
</td>
</tr></table>
</td>

<td style='vertical-align: top; width: 15rem; min-width: 15rem;'>
<p style='text-align:center;'>
<a @click.prevent='onToggle(author_key,"on")'>All</a> |
<a @click.prevent='onToggle(author_key,"toggle")'>Toggle</a> |
<a @click.prevent='onToggle(author_key,"off")'>None</a>
</p>
<p>
	<a @click.prevent='searchForAuthor(author_key)' :title='author_key'>Search for author</a>
	<span v-if='typeof author_search_results[author_key]!="undefined"'>
		<span v-if='author_search_results[author_key].length==0' style='color:red'>
			[none found]
		</span>
		<span v-else-if='author_search_results[author_key].length==1' style='color:green'>
			[<a :href='"/wiki/"+author_search_results[author_key][0]' target='_blank'>
				one found
			</a>]
		</span>
		<span v-else-if='author_search_results[author_key].length>=10'>
			[{{author_search_results[author_key].length}}+ found]
		</span>
		<span v-else>
			[{{author_search_results[author_key].length}} found]
		</span>
	</span>
</p>
<p>
	<a @click.prevent='createAndLinkAuthor(author_key)'>Create new author</a>
	for the selected entries, and change the string author properties for authors (P50).
</p>
<p>
	<form style='display: flex;'>
		<cdx-text-input v-model="authors[author_key].tmp_q" placeholder='Qxxx' />
		<cdx-button :disabled="(authors[author_key].tmp_q||'')==''?'true':null" :action="(authors[author_key].tmp_q||'')==''?'':'progressive'" @click.prevent='assignQtoAuthor(author_key)' aria-label='Assign author item ID'>Assign</cdx-button>
	</form>
</p>
</td>

</tr>
</tbody>
</table>



<table v-else-if='show="papers"'>
<tr v-for='paper in papers'>
<td>
	<a :href='"/wiki/"+paper.q' target='_blank'>
		{{paper.label}}
	</a>
</td>
<td>{{paper.count}}</td>
</tr>
</table>
</div>`,
		data() { return {
			loading:true,
			name_collisions:[],
			max_authors_for_permutations:4,
			paper_count:{} ,
			papers:[],
			paper_name:{},
			authors:{},
			authors_order:[],
			author_search_results:{},
			show:'authors'
		} } ,
		created : function () {
			mw.util.addCSS( 'table.author_strings_authors > tbody > tr { border-top: 2px solid #DDD; padding-top: 0.5rem;}' );
			let self = this ;
			let string_names = [] ;
			if ( $('#P2093').length > 0 ) { // Paper
				$('#P2093 div.wikibase-statementview-mainsnak div.wikibase-snakview-value').each ( function ( dummy , name ) {
					string_names.push($(name).text());
				} ) ;
				string_names = string_names
					.map(self.simplifyName)
					.filter(function(name){return name!=''}) ;
				string_names = self.uArray(string_names);
				// Keep the first three authors, and the last one, because, you know...
				while ( string_names.length > self.max_authors_for_permutations ) string_names.splice(self.max_authors_for_permutations-1,1) ;
				self.permuteNamesAndQueryPapers(string_names,self.onPaperIDsLoaded) ;
				self.checkDoubleAuthors();
			} else if ( $('#P50').length == 0 ) { // Author
				let name = self.simplifyName($('span.wikibase-title-label').text()).toLowerCase();
				if ( name == '' ) return ; // Nothing useful to search for
				let query = name + ' haswbstatement:P31=Q13442814' ;
				self.searchForPaper ( query , function () {
					self.onPaperIDsLoaded(function(){
						Object.keys(self.authors).forEach((author_key)=>{
							if ( name == author_key ) self.authors[author_key].tmp_q = wdutil.q ;
						});
					});
				}) ;
			}
		} ,
		methods: {
			removeANSStatements : function () {
				let self = this ;
				let statement_ids = [] ;
				self.name_collisions.forEach((x)=>{x.ans.filter((y)=>y.checked).forEach((y)=>{
					statement_ids.push({id:y.statement_id,remove:''});
					y.checked = false ;
				})});
				let data = {claims:statement_ids};

				let payload = {
					action: 'wbeditentity',
					id: wdutil.q,
					data: JSON.stringify(data) ,
					summary: "Removing duplicate P2093s in favour of P50s (via author_strings gadget)"
				} ;

				let api = new mw.Api();
				api.postWithToken("csrf", payload ).done(function( new_entity ) {
					if ( new_entity.success==1 ) { // Update cache
						wdutil.entity_cache[wdutil.q] = new_entity.entity ;
					}
					statement_ids.forEach((x)=>{$('#'+$.escapeSelector(x.id)).remove()});
					self.checkDoubleAuthors();
				} ).fail( function(code, new_entity) {
					console.log(code,new_entity);
					alert("Something went wrong with the API");
				} );

			} ,
			changeAutorToP50Here : function(o,q) {
				let self = this;
				let param = {
					paper_q:mw.config.get('wgTitle'),
					name:o.name,
					status:'',
					q:q
				};
				if ( $("#P50").length == 0 ) wdutil.createPropertyContainerElement("P50") ;
				wdutil.loadEntities(["P50"],function(){
					self.changeOccurrenceStringNameToP50(param,function(){
						// Create fake P50 statement in interface
						if ( $('#'+q+q).length==0 ) {
							let value_html = wdutil.getValueHTML("P50",q);
							value_html = value_html.replace(/>[^<]+<\/a>/,'>'+o.name+'</a> ['+o.serial+']');
							wdutil.createStatementElement("P50",value_html,q+q,q+q);
							$('[id="'+o.statement_id+'"]').remove();
						}
					});
				})
			},
			addAuthorNameLinks: function(o) {
				let self = this;
				let div = document.createElement('span');
				div.style='font-size:8pt;';
				let a = document.createElement('a');
				a.style='margin-right:0.2rem;';
				a.innerHTML='💡';
				a.href='#';
				a.title='Create a new author and assign this one';
				a.onclick = function(){
					// let name = self.simplifyName(o.name);
					let aliases = [];
					// if ( name!=o.name ) aliases.push(o.name);
					aliases = aliases.map((name) => { return {language:'en',value:name} } );
					let payload = self.generateAuthorCreationPayload(o.name,aliases);
					let api = new mw.Api();
					api.postWithToken("csrf", payload ).done(function( data ) {
						if ( (data.success||0) != 1 ) {
							alert ( "Item creation failed" ) ;
							return ;
						}
						let q = data.entity.id ;
						self.changeAutorToP50Here(o,q);
					} ).fail( function(code, token_data) {
						console.log(code,token_data);
						alert("Error creating new author item: "+code);
					} );
				};
				div.append(a);

				a = document.createElement('a');
				a.style='margin-right:0.2rem;';
				a.innerHTML='⧉';
				a.href='#';
				a.title='Assign an existing author item';
				a.onclick = function(){
					let q = prompt("Item ID of the author");
					if ( q===null || q=='' ) return;
					q = 'Q'+q.replace(/\D/g,'');
					if ( q=='Q' ) return alert("Invalid Qid");
					self.changeAutorToP50Here(o,q.toUpperCase());
				};
				div.append(a);

				a = document.createElement('a');
				a.innerHTML='🔎';
				a.href='https://www.wikidata.org/w/index.php?search=&search='+o.name+' haswbstatement:P31=Q5&title=Special%3ASearch&ns0=1';
				a.title='Search for this name';
				a.target='_blank';
				div.append(a);


				$('[id="'+o.statement_id+'"] div.wikibase-statementview-mainsnak div.wikibase-snakview-body').each(function(dummy,svbody){
					svbody.append(div);
				});
			},
			checkDoubleAuthors : function () {
				let self = this ;
				let ans = self.getAuthorNamesFromInterfaceWithSerial('P2093') ;
				let p50 = self.getAuthorNamesFromInterfaceWithSerial('P50') ;
				let serials = {} ;
				self.name_collisions = [] ;

				ans.filter((author_serial)=>author_serial.serial!='').forEach((author_serial)=>{
					self.addAuthorNameLinks(author_serial);
				});

				ans.filter((author_serial)=>author_serial.serial!='').forEach((author_serial)=>{
					if ( typeof serials[author_serial.serial]=='undefined') serials[author_serial.serial] = {ans:[],p50:[]};
					serials[author_serial.serial].ans.push(author_serial);
				});
				p50.filter((author_serial)=>author_serial.serial!='').forEach((author_serial)=>{
					if ( typeof serials[author_serial.serial]=='undefined') serials[author_serial.serial] = {ans:[],p50:[]};
					serials[author_serial.serial].p50.push(author_serial);
				});
				self.name_collisions = Object
					.keys(serials)
					.filter((key)=>serials[key].ans.length>0 && serials[key].p50.length>0)
					.map((key)=>{return {serial:key,ans:serials[key].ans,p50:serials[key].p50}}) ;
				self.ans = ans;
			} ,
			getAuthorNamesFromInterfaceWithSerial : function ( property ) {
				let self = this ;
				let ret = [] ;
				let selector = 'div.wikibase-statementview-mainsnak div.wikibase-snakview-value' ;
				if ( property == 'P50' ) selector += ' a' ;
				$('#'+property+' div.wikibase-statementview').each ( function ( dummy , statement ) {
					let statement_id = $(statement).attr('id');
					let container = $(statement).find('div.wikibase-statementview-mainsnak-container').get(0);
					let name = $(container).find(selector).text().trim();
					let serial = '' ;
					$(container).find('div.wikibase-statementview-qualifiers div.wikibase-snaklistview').each(function(dummy,qualifier){
						if ( $(qualifier).find('a[title="Property:P1545"]').length == 0 ) return ;
						serial = $(qualifier).find('div.wikibase-snakview-value-container').text().trim();
					});
					let entry = {name:name,serial:serial,q:'',statement_id:statement_id,checked:true} ;
					if ( property == 'P50' ) entry.q = $(container).find(selector).attr('title').trim() ;
					ret.push(entry);
				} ) ;
				return ret ;
			} ,
			assignQtoAuthor : function ( author_key ) {
				let self = this ;
				let q = 'Q'+self.authors[author_key].tmp_q.replace(/\D/g,'') ;
				if ( q == 'Q' ) {
					alert ( "Invalid Qxxx" ) ;
					return ;
				}
				let occurrences = self.authors[author_key].occurrences.filter((o)=>o.checked);

				// chain will contain the "serialized" promises
				let chain = Promise.resolve();
				occurrences.forEach ( (o) => { 
					promise = new Promise((resolve) => {
						o.q = q ;
						self.changeOccurrenceStringNameToP50(o,function(){
							setTimeout(resolve,200); // Small delay to allow caches to update etc
						}) ;
					});
					chain = chain.then(() => promise);
				} ) ;

				// Start the "serialized" chain
				chain.then(() => { // chain is done
					self.authors[author_key].tmp_q = '' ;
				});
			} ,
			changeOccurrenceStringNameToP50 : function ( o , resolve ) { // o=occurrence {q,paper_q,name,checked}
				let self = this ;

				// Get the P2093 statement to replace
				let item = wdutil.entity_cache[o.paper_q] ;
				if ( typeof item=='undefined' ) return ;
				let p2093s = ((item.claims||[])["P2093"]||[]).filter((claim)=>((((claim||{}).mainsnak||{}).datavalue||{}).value||'')==o.name);
				if ( p2093s.length == 0 ) { o.status = "Author '"+o.name+"' in "+o.paper_q+" not found"; return resolve(); } ;
				if ( p2093s.length > 1 ) { o.status = "More than one author '"+o.name+"' in "+o.paper_q; return resolve(); } ;
				let p2093 = p2093s[0] ;

				// Make sure this author item is not already used as P50 in the paper
				let p50s = ((item.claims||[])["P50"]||[]).filter((claim)=>(((((claim||{}).mainsnak||{}).datavalue||{}).value||{}).id||'')==o.q);
				if ( p50s.length > 0 ) { o.status = "Author '"+o.name+"' already has a P50 in "+o.paper_q; return resolve(); } ;

				// Construct P50 creation and P2093 removal
				let p50_value = {
					"entity-type": "item",
                    "numeric-id": o.q.replace(/\D/g,''),
                    "id": o.q
				} ;
				let data = { claims:[
					{
						mainsnak:{
							snaktype: 'value',
							property: "P50",
							datavalue: {
								value: p50_value,
								type: "wikibase-entityid"
							},
						},
						type: 'statement',
						rank: 'normal'
					} ,
					{ id: p2093.id, remove: "" } // Remove P2093
				] } ;
				if ( typeof p2093.references!='undefined' ) {
					p2093.references.forEach((ref)=>{
						if ( typeof ref.hash!='undefined') delete ref['hash'] ;
						if ( typeof ref['snaks-order']!='undefined') delete ref['snaks-order'] ;
						Object.keys(ref.snaks).forEach((property)=>{
							ref.snaks[property].forEach((snak)=>{
								if ( typeof snak.hash!='undefined') delete snak['hash'] ;
							});
						});
					});
					data.claims[0].references = p2093.references ;
				}
				if ( typeof p2093.qualifiers!='undefined' ) {
					// Flatten and remove IDs
					let qualifiers = [] ;
					Object.keys(p2093.qualifiers).forEach((property) => {
						let subqs = p2093.qualifiers[property].map((subq)=>{
							if ( typeof subq.id!='undefined') delete subq['id'];
							if ( typeof subq.hash!='undefined') delete subq['hash'];
							return subq
						}) ;
						qualifiers = qualifiers.concat(subqs);
					})
					qualifiers.push({
						datatype: 'string',
						snaktype: 'value',
						property: 'P1932',
						datavalue: {
							value: o.name,
							type: "string"
						}
					});
					data.claims[0].qualifiers = qualifiers ;
				}
				let payload = {
					action: 'wbeditentity',
					id: o.paper_q,
					data: JSON.stringify(data) ,
					summary: "Changing author name '"+o.name+"' to P50:[["+o.q+"]] (via author_strings gadget)"
				} ;

				let api = new mw.Api();
				api.postWithToken("csrf", payload ).done(function( new_entity ) {
					if ( new_entity.success==1 ) { // Update cache
						wdutil.entity_cache[o.paper_q] = new_entity.entity ;
					}
					if ( typeof o.checked!='undefined') delete o['checked'] ;
					o.status = 'done' ;

					// Set cached Q ID
					let cache_key = self.getCacheAuthorKey(self.simplifyName(o.name)) ;
					let expiry = 60*60*24*7 ; // 7 days
					mw.storage.set ( cache_key , o.q , expiry ) ;

					// /* TODO UI fixme
					if ( o.q == wdutil.q ) {
						let statement_html = wdutil.getValueHTML('P50',p50_value) ;
						$('[id="'+p2093.id+'"]').remove();
						// wdutil.createStatementElement("P50",statement_html,'dummy1','dummy2'); // TODO UUIDs
					}
					// */
					resolve() ;
				} ).fail( function(code, new_entity) {
					o.status = code ;
					console.log(code,new_entity);
					resolve() ;
				} );
				resolve();
			} ,
			createAndLinkAuthor : function ( author_key ) {
				let self = this ;
				let occurrences = self.authors[author_key].occurrences.filter((o)=>o.checked);
				let author_name_count = {} ;
				occurrences.forEach(function(o){
					if ( typeof author_name_count[o.name]=='undefined' ) author_name_count[o.name] = 1 ;
					else author_name_count[o.name]++ ;
				});
				let anck = Object.keys(author_name_count);
				if ( anck.length == 0 ) {
					alert ( "Could not find any names" ) ;
					return ;
				}
				anck.sort((a,b)=>{
					if ( a.length==b.length ) return author_name_count[b]-author_name_count[a] ;
					else return a.length<b.length ;
				});

				// Prepare data
				let new_name = anck[0] ;
				let aliases = self.authors[author_key].occurrences
					.map((o)=>o.name) // Names
					.filter((val, ind, arr) => arr.indexOf(val) === ind) // Unique
					.filter((name) => name!=new_name)
					.map((name) => { return {language:'en',value:name} } );


				let payload = self.generateAuthorCreationPayload(new_name,aliases);
				let api = new mw.Api();
				api.postWithToken("csrf", payload ).done(function( data ) {
					if ( (data.success||0) != 1 ) {
						alert ( "Item creation failed" ) ;
						return ;
					}
					let q = data.entity.id ;
					self.authors[author_key].tmp_q = q ;
					self.assignQtoAuthor(author_key);
				} ).fail( function(code, token_data) {
					console.log(code,token_data);
					alert("Error creating new author item: "+code);
				} );

			} ,
			generateAuthorCreationPayload : function(new_name,aliases) {
				let new_data = {
					labels:{en:{language:"en",value:new_name}},
					descriptions:{en:{language:"en",value:"researcher"}},
					claims:[
						{
							mainsnak:{
								snaktype: 'value',
								property: 'P31',
								datavalue: {
									value: {
	                                    "entity-type": "item",
	                                    "numeric-id": 5,
	                                    "id": "Q5"
	                                },
									type: 'wikibase-entityid'
								},
							},
							type: 'statement',
							rank: 'normal'
						} ,
						{
							mainsnak:{
								snaktype: 'value',
								property: 'P106',
								datavalue: {
									value: {
	                                    "entity-type": "item",
	                                    "numeric-id": 1650915,
	                                    "id": "Q1650915"
	                                },
									type: 'wikibase-entityid'
								},
							},
							type: 'statement',
							rank: 'normal'
						}
					]
				} ;

				// Add aliases
				if ( aliases.length>0 ) new_data.aliases = aliases ;

				let payload = {
					action: 'wbeditentity',
					new: 'item',
					data: JSON.stringify(new_data) ,
					summary: "Creating new author item (via author_strings gadget)"
				} ;
				return payload;
			},
			onToggle : function (author_key,state) {
				let self = this ;
				self.authors[author_key].occurrences.forEach(function(o){
					o.checked = state=='on'?true:(state=='off'?false:!o.checked)
				}) ;
			} ,
			searchForAuthor : function (author_key) {
				let query = author_key+' haswbstatement:P31=Q5' ; //  haswbstatement:P496
				let url = '/w/index.php?search=&search='+encodeURIComponent(query)+'&title=Special%3ASearch&ns0=1'
				window.open(url, '_blank');
			} ,
			simplifyName : function ( name ) {
				let ret = name.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^\x00-\x7F]/g, '') // ASCII-fy
					.replace(/\S+\./g,' '); // Remove initials
				if ( !ret.match(/^ *\S{3,} *\S{3,} *$/) ) ret = ret.replace(/(\b(\w{1,2})\b(\s|$))/g,'');
				return ret.replace(/\s+/g,' ') // single spaces
					.trim()
			} ,
			onPaperIDsLoaded : function ( callback ) {
				let self = this ;
				let to_load = Object.keys(self.paper_count);
				to_load.sort((a,b)=> { // Most hits first
					if ( self.paper_count[b] == self.paper_count[a] ) return a.replace(/\D/g,'')*1 - b.replace(/\D/g,'')*1 ;
					else return self.paper_count[b] - self.paper_count[a]
				});
				to_load = to_load.slice(0, 45); // Max 45 papers so they will fit in a single query
				to_load.push(wdutil.q); // Load this item's data as well
				wdutil.loadEntities(to_load,function(){
					self.papers = to_load
						.filter((q)=>q!=wdutil.q)
						.map((q)=>{return {q:q,count:self.paper_count[q],label:wdutil.getEntityName(q)} }) ;
					to_load.forEach(function(q){self.paper_name[q]=wdutil.getEntityName(q)}) ;
					self.groupNames(to_load) ;
					self.loading = false ;
					if ( typeof callback!='undefined') callback();
				});
			} ,
			setTmpQ : function ( authors ) { // Uses mw.storage to retrieve recent name:Q mappings
				let self = this ;
				Object.keys(authors).forEach((author_key) => {
					let cache_key = self.getCacheAuthorKey(author_key) ;
					let recent_q = mw.storage.get ( cache_key ) ;
					if ( recent_q ) authors[author_key].tmp_q = recent_q ;
				}) ;
			} ,
			getCacheAuthorKey : function ( name ) {
				return "author_strings_cache_"+name.toLowerCase();
			} ,
			isValidShortName : function ( short_name ) {
				if ( short_name.length <= 4 ) return false ; // Do not use really short names
				return true ;
			} ,
			groupNames : function ( paper_ids ) {
				let self = this ;
				let authors = {} ;
				paper_ids.forEach ( function ( paper_q ) {
					let item = wdutil.entity_cache[paper_q] ;
					if ( typeof item == 'undefined' ) return ;
					let author_string_names = wdutil.getValuesForProperty(item,'P2093') ;
					author_string_names.forEach ( function ( name ) {
						let short_name = self.simplifyName(name).toLowerCase();
						if ( !self.isValidShortName(short_name) ) return ;
						if ( typeof authors[short_name]=='undefined' ) authors[short_name] = { short_name:short_name , occurrences:[] , tmp_q:'' } ;
						authors[short_name].occurrences.push({paper_q:paper_q,name:name,checked:true});
					} ) ;
				} ) ;

				self.authors_order = Object.keys(authors) ;
				self.setTmpQ(authors);
				self.sortAuthors(authors);
				self.authors = authors ;
				if ( self.authors_order.length < 500 ) { // Re-rendering will otherwise almost-crash machine...
					self.iterateAuthorSearch(JSON.parse(JSON.stringify(self.authors_order)));
				}
			} ,
			sortAuthors : function ( authors ) {
				let self = this ;
				self.authors_order.sort((a,b)=> {
					if ( authors[a].tmp_q=='' && authors[b].tmp_q!='' ) return 1 ;
					if ( authors[a].tmp_q!='' && authors[b].tmp_q=='' ) return -1 ;
					return authors[b].occurrences.length - authors[a].occurrences.length
				} ) ;
			} ,
			iterateAuthorSearch : function ( author_keys ) {
				let self = this ;
				if ( author_keys.length == 0 ) return ;
				let author_key = author_keys.shift();
				self.searchForAuthorItems(author_key,function(ids){
					self.author_search_results[author_key] = ids ;
					self.iterateAuthorSearch(author_keys);
				})
			} ,
			searchForAuthorItems : function ( author_key , resolve ) {
				let self = this ;
				if ( author_key.trim()=='' ) return Promise.resolve(author_key,[]);
				let query = author_key+' haswbstatement:P31=Q5' ;
				let params = {
					action:'query',
					list:'search',
					srnamespace:0,
					//srlimit:500,
					srsearch:query,
					format:'json'
				} ;
				let url = wdutil.api + '?' + new URLSearchParams(params) ;
				return fetch(url)
					.then(data => { return data.json() })
					.then(data => { return (((data||{}).query||{}).search||{}).map(function(result){return result.title}); })
					.then(ids => {resolve ( ids )})
			} ,
			permuteNamesAndQueryPapers : function ( names , callback ) {
				let self = this ;
				let promises = [] ;
				for (let i=0; i+1<names.length; i++) {
					for (let j=i+1; j<names.length; j++) {
						let query = self.buildSearchPapersForTwoAuthorsQuery(names[i],names[j]) ;
						promises.push(new Promise((resolve, reject) => {
							self.searchForPaper(query,resolve);
						}));
					}
				}
				Promise.all(promises).then(()=>{callback()});
			} ,
			searchForPaper : function ( query , resolve ) {
				let self = this ;
				$.get(wdutil.api,{
					action:'query',
					list:'search',
					srnamespace:0,
					srlimit:500,
					srsearch:query,
					format:'json'
				},function(d){
					let paper_ids = (((d||{}).query||{}).search||{})
						.map(function(result){return result.title})
						.filter(function(q){return q!=wdutil.q});
					paper_ids = self.uArray(paper_ids); // Paranoia
					paper_ids.forEach(function(q){
						if ( typeof self.paper_count[q]=='undefined' ) self.paper_count[q]=0 ;
						self.paper_count[q]++;
					});
					resolve();
				}) ;
			} ,
			buildSearchPapersForTwoAuthorsQuery : function ( name1 , name2 ) {
				let query = 'haswbstatement:P31=Q13442814 ' ;
				if (name1.indexOf(' ') == -1) query += name1 ;
				else query += '"'+name1+'"' ;
				query += ' ' ;
				if (name2.indexOf(' ') == -1) query += name2 ;
				else query += '"'+name2+'"' ;
				return query ;
			} ,
			// Makes an array unique while maintaining sort order
			uArray: function (array) {
			    let out = [];
			    for (let i=0, len=array.length; i<len; i++)
			        if (out.indexOf(array[i]) === -1)
			            out.push(array[i]);
			    return out;
			}
		},
		components: {
			CdxButton,
			CdxCheckbox,
			CdxTextInput,
		},
	};

	if ( $('#P496,#P1153,#P1960,#P2798,#P106 a[title="Q1650915"]').length==0 && $('#P31 a[title="Q5"]').length > 0 ) { // Human but not researcher
		let portletLink = mw.util.addPortletLink( 'p-tb', '#', 'Author strings','wikitext-wd_as');
		$(portletLink).click ( function () {
			wdutil_app.addTab({
				name: 'author_strings',
				label: 'Author strings',
			},function(id){
				const author_strings_app = Vue.createMwApp(AuthorStringsApp);
				author_strings_app.mount(id);
			});
			return false ;
		} ) ;
		return false ;
	}


	wdutil_app.addTab({
		name: 'author_strings',
		label: 'Author strings',
	},function(id){
		const author_strings_app = Vue.createMwApp(AuthorStringsApp);
		author_strings_app.mount(id);
	});
	return false ;
} ) ;

};

var wdutil_app ;
//mw.loader.load('https://wikidata-todo.toolforge.org/wdutils.js');
mw.loader.load('https://www.wikidata.org/w/index.php?title=User:Magnus_Manske/wdutil.js&action=raw&ctype=text/javascript');
if ( typeof wdutil!='undefined' ) wdutil.loadCallback(author_strings);
else {
	var wdutil_loaded_callbacks ;
	if ( typeof wdutil_loaded_callbacks=='undefined' ) wdutil_loaded_callbacks = [] ;
	wdutil_loaded_callbacks.push(author_strings);
}
// </nowiki>