Участник:Putnik/wikidataInfoboxExport.js: различия между версиями

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску
Содержимое удалено Содержимое добавлено
+chv language (Q33348)
unique monolingual values
Строка 21: Строка 21:
languages.unshift( contentLanguage );
languages.unshift( contentLanguage );
languages.unshift( userLanguage );
languages.unshift( userLanguage );
languages = $.unique( languages );
languages = $.uniqueSort( languages );


// Main config
// Main config
Строка 444: Строка 444:
} );
} );
languages = $.merge( languages, _this.config.languages );
languages = $.merge( languages, _this.config.languages );
languages = $.unique( languages );
languages = $.uniqueSort( languages );
var sites = titles.map( function ( item ) {
var sites = titles.map( function ( item ) {
Строка 654: Строка 654:
}
}
}
}
titles = $.unique( titles );
titles = $.uniqueSort( titles );
}
}
if ( redirects.length ) {
if ( redirects.length ) {
Строка 1039: Строка 1039:
}
}
for ( var idx = 0; idx < $.unique( units ).length; idx += 50) {
for ( var idx = 0; idx < $.uniqueSort( units ).length; idx += 50) {
_this.wdApi.get( {
_this.wdApi.get( {
action: 'wbgetentities',
action: 'wbgetentities',
languages: _this.config.languages,
languages: _this.config.languages,
props: [ 'labels', 'descriptions', 'aliases', 'claims' ],
props: [ 'labels', 'descriptions', 'aliases', 'claims' ],
ids: $.unique( units ).slice( idx, idx + 50 )
ids: $.uniqueSort( units ).slice( idx, idx + 50 )
} ).done( function ( unitData ) {
} ).done( function ( unitData ) {
if ( !unitData.success ) {
if ( !unitData.success ) {
Строка 1094: Строка 1094:
}
}
}
}
_this.config.units[ unitId ].search = $.unique( unitSearch );
_this.config.units[ unitId ].search = $.uniqueSort( unitSearch );
_this.saveConfig();
_this.saveConfig();
Строка 1377: Строка 1377:
}
}
}
}
var valueLanguages = [];
for ( var i in values ) {
for ( var i in values ) {
var valueLanguage = values[ i ].wd.value.language;
if ( valueLanguages.indexOf( valueLanguage ) > -1 ) {
continue;
}
valueLanguages.push( valueLanguage );
values[ i ].label = $( '<span>' )
values[ i ].label = $( '<span>' )
.append( $( '<span>' ).css( 'color', '#666' ).text( '(' + values[ i ].wd.value.language + ') ' ) )
.append( $( '<span>' ).css( 'color', '#666' ).text( '(' + valueLanguage + ') ' ) )
.append( $( '<strong>' ).text( values[ i ].wd.value.text ) );
.append( $( '<strong>' ).text( values[ i ].wd.value.text ) );
}
}
values = values.filter( function( item ) {
return item.label !== undefined;
} );
break;
break;
Строка 1527: Строка 1536:
}
}
values = $.unique( values );
values = $.uniqueSort( values );
_this.dialog( $field, propertyId, values, _this.getReference( $field ) );
_this.dialog( $field, propertyId, values, _this.getReference( $field ) );
};
};

Версия от 21:16, 18 июня 2019

/**
 * Quick Wikidata Infobox Export.
 * 
 * Gadget for export information from infoboxes to Wikidata.
 * The export window is shown by double clicking.
 */
( function ( mw, $ ) {
	var wikidataInfoboxExport = function() {
		var _this = this;

		this.months = [
			'january', 'february', 'march', 'april', 'may', 'june',
			'july', 'august', 'september', 'october', 'november', 'december'
		];
		this.monthsGen = months;

		// Site and user language setup 
		var contentLanguage = mw.config.get( 'wgContentLanguage' );
		var userLanguage = mw.user.options.get( 'language' ) || contentLanguage;
		var languages = [ 'en' ];
		languages.unshift( contentLanguage );
		languages.unshift( userLanguage );
		languages = $.uniqueSort( languages );

		// Main config
		this.config = Object.assign( {
			version: '2.0.0',
			project: mw.config.get( 'wgDBname' ),
			language: userLanguage,
			userLanguage: userLanguage,
			contentLanguage: contentLanguage,
			languages: languages,
			storageKey: 'infoboxExportConfig',
			references: {},
			units: {},
			fixedValues: [],
			misLang: {
				ady: 'Q27776',
				ain: 'Q20968488',
				alt: 'Q1991779',
				atv: 'Q2640863',
				chm: 'Q973685',
				chv: 'Q33348',
				ckt: 'Q33170',
				enf: 'Q29942',
				evn: 'Q30004',
				inh: 'Q33509',
				izh: 'Q33559',
				jdt: 'Q56495',
				kjh: 'Q33575',
				krl: 'Q33557',
				kum: 'Q36209',
				orv: 'Q35228',
				otk: 'Q34988',
				pdt: 'Q1751432',
				sjd: 'Q33656',
				sjt: 'Q36656',
				sga: 'Q35308',
				sma: 'Q13293',
				vot: 'Q32858',
				yrk: 'Q36452'
			},
			centuries: [ 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII',
				'VIII', 'IX', 'X', 'XI', 'XII', 'XIII', 'XIV', 'XV',
				'XVI', 'XVII', 'XVIII', 'XIX', 'XX', 'XXI', 'XXII' ],
		}, window.wieConfig || {} );

		this.i18n = {};

		this.api = null;
		this.wdApi = null;
		this.commonsApi = null;
	
		this.baseRevId;
		this.propertyIds = [ 'P2076', 'P2077' ]; // Temperature and pressure for qualifiers
		this.typesMapping = {
			'commonsMedia': 'string',
			'external-id': 'string',
			'url': 'string',
			'wikibase-item': 'wikibase-entityid'
		};
		this.alreadyExistingItems = {};
		this.windowManager;
	
		/**
		 * Compares the values of the infobox and Wikidata
		 */
		this.canExportValue = function ( $field, claims, callbackIfCan ) {
			if ( !claims || !( claims.length ) ) {
				// Can't export only if it is large local image
				if ( !( $field.find( '.image img[src*="/wikipedia/' + _this.config.contentLanguage + '/"]' ).width() >= 80 ) ) {
					callbackIfCan();
				}
				return;
			}
	
			switch ( claims[ 0 ].mainsnak.datatype ) {
				case 'quantity':
					for ( var i = 0; i < claims.length; i++ ) {
						var parsedTime = _this.createTimeSnak( ( $field.text().match( /\(([^)]*\d\d\d\d)[,)\s]/ ) || [] )[ 1 ] );
						if ( parsedTime && ( claims[ i ].qualifiers || {} ).P585 ) {
							var claimPrecision = claims[ i ].qualifiers.P585[ 0 ].datavalue.value.precision;
							if ( parsedTime.precision < claimPrecision ) {
								claims[ i ].qualifiers.P585[ 0 ].datavalue.value.precision = parsedTime.precision;
							} else if ( parsedTime.precision > claimPrecision ) { // FIXME: Specify the date in Wikidata later
								parsedTime.precision = claimPrecision;
							}
							var p585 = parsedTime ? _this.formatDataValue( {
								type: 'time',
								value: parsedTime
							} )[ 0 ].innerText : '';
	
							if ( _this.formatDataValue( claims[ i ].qualifiers.P585[ 0 ].datavalue )[ 0 ].innerText !== p585 ) {
								claims[ i ].qualifiers.P585[ 0 ].datavalue.value.precision = claimPrecision;
								continue;
							}
						}
						return;
					}
					callbackIfCan( true );
					break;
	
				case 'wikibase-item':
					value = _this.parseItems( $field, $field, function ( values ) {
						var duplicates = [];
						for ( var i = 0; i < values.length; i++ ) {
							for ( var j = 0; j < claims.length; j++ ) {
								if ( values[ i ].wd.value.id === claims[ j ].mainsnak.datavalue.value.id ) {
									duplicates.push( values[ i ].wd.value.id );
								}
							}
						}
						if ( duplicates.length < values.length ) {
							if ( duplicates.length > 0 ) {
								var propertyId = claims[ 0 ].mainsnak.property;
								_this.alreadyExistingItems[ propertyId ] = duplicates;
								if ( propertyId === 'P166' && values.length === claims.length )
									return;
							}
							if ( claims.length > 0 ) {
								if ( claims[ 0 ].mainsnak.property === 'P19' || claims[ 0 ].mainsnak.property === 'P20' )
									return;
							}
							callbackIfCan( true );
						}
					} );
			}
			// By default we can't export if there are claims already
		};
	
		/**
		 * Claim's GUID generation
		 */
		this.claimGuid = function ( entityId ) {
			var getRandomHex = function ( min, max ) {
				return ( Math.floor( Math.random() * ( max - min + 1 ) ) + min ).toString( 16 );
			};
	
			var template = 'xx-x-x-x-xxx';
			var guid = '';
			for ( var i = 0; i < template.length; i++ ) {
				if ( template.charAt( i ) === '-' ) {
					guid += '-';
					continue;
				}
	
				var hex;
				if ( i === 3 ) {
					hex = getRandomHex( 16384, 20479 );
				} else if ( i === 4 ) {
					hex = getRandomHex( 32768, 49151 );
				} else {
					hex = getRandomHex( 0, 65535 );
				}
	
				while ( hex.length < 4 ) {
					hex = '0' + hex;
				}
	
				guid += hex;
			}
	
			return entityId + '$' + guid;
		};
	
		/**
		 * Formate dates as datavalue for Wikidata
		 */
		this.createTimeSnak = function ( timestamp, forceJulian ) {
			if ( !timestamp ) {
				return;
			}
			var result = { timezone: 0, before: 0, after: 0 };
			var isoDate;
			var dateParts;
	
			if ( timestamp.match ( /\s\([^\)]*\)\s/ ) ) {
				forceJulian = true;
			}
			timestamp = timestamp.replace( /\([^\)]*\)/, '' ).trim();
	
			var isBce = false;
			var bceMatch = timestamp.match( _this.config.reBce );
			if ( bceMatch ) {
				isBce = true;
				timestamp = timestamp.replace( bceMatch[ 0 ], '' ).trim();
			} else {
				var ceMatch = timestamp.match( _this.config.reCe );
				if ( ceMatch ) {
					timestamp = timestamp.replace( ceMatch[ 0 ], '' ).trim();
				}
			}
	
			if ( dateParts = timestamp.match( _this.config.reCentury ) ) {
				isoDate = new Date( 0 );
				isoDate.setFullYear( _this.config.centuries.indexOf( dateParts[ 1 ].toUpperCase() ) * 100 + 1 );
				result.precision = 7;
			} else if ( dateParts = timestamp.match( _this.config.reMonthYear ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 2 ], _this.months.indexOf( dateParts[ 1 ] ) ) );
				result.precision = 10;
			} else if ( dateParts = timestamp.match( _this.config.reTextDate ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 3 ], _this.monthsGen.indexOf( dateParts[ 2 ] ), dateParts[ 1 ] ) );
				result.precision = 11;
			} else if ( dateParts = timestamp.match( _this.config.reDotDate ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 3 ] < 100 ? 1900 + parseInt( dateParts[ 3 ] ) : dateParts[ 3 ], dateParts[ 2 ] - 1, dateParts[ 1 ] ) );
				result.precision = 11;
			} else if ( dateParts = timestamp.match( _this.config.reIsoDate ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 1 ] < 100 ? 1900 + parseInt( dateParts[ 1 ] ) : dateParts[ 1 ], dateParts[ 2 ] - 1, dateParts[ 3 ] ) );
				result.precision = 11;
			} else if ( dateParts = timestamp.match( _this.config.reDecade ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 1 ], 0 ) );
				result.precision = 8;
			} else if ( dateParts = timestamp.match( _this.config.reYear ) ) {
				isoDate = new Date( Date.UTC( dateParts[ 1 ], 0 ) );
				result.precision = 9;
			} else if ( timestamp.match( _this.config.rePresent ) ) {
				return 'novalue';
			} else if ( timestamp.match( _this.config.reUnknown ) ) {
				return 'somevalue';
			} else {
				return;
			}

			try {
				isoDate.setUTCHours( 0 );
				isoDate.setUTCMinutes( 0 );
				isoDate.setUTCSeconds( 0 );
	
				result.time = ( isBce ? '-' : '+' ) + isoDate.toISOString().replace( /\.000Z/, 'Z' );
			} catch ( e ) {
				return;
			}
			if ( result.precision < 11 ) {
				result.time = result.time.replace( /-\d\dT/, '-00T' );
			}
			if ( result.precision < 10 ) {
				result.time = result.time.replace( /-\d\d-/, '-00-' );
			}
	
			result.calendarmodel = 'http://www.wikidata.org/entity/Q' +
				( forceJulian || isoDate < new Date( Date.UTC( 1582, 9, 15 ) ) ? '1985786' : '1985727' );
			return result;
		};
	
		/**
		 * Error display
		 */
		this.errorDialog = function ( title, message ) {
			var errorDialog = new OO.ui.MessageDialog();
			_this.windowManager.addWindows( [ errorDialog ] );
			_this.windowManager.openWindow( errorDialog, {
				title: title,
				message: message
			} );
		};
	
		/**
		 * Extract reference URL
		 */
		this.getReference = function ( $field ) {
			var references = [];
			var $notes = $field.find( 'sup.reference a' );
			for ( var i = 0; i < $notes.length; i++ ) {
				var $externalLinks = $( decodeURIComponent($notes[ i ].hash).replace( /[!"$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, '\\$&' ) + ' a[rel="nofollow"]' );
				for ( var j = 0; j < $externalLinks.length; j++ ) {
					var $externalLink = $( $externalLinks.get( j ) );
					if ( !$externalLink.attr( 'href' ).match( /(wikipedia.org|webcitation.org|archive.is)/ ) ) {
						var source = {
							snaks: {
								P854: [ {
									property: 'P854',
									datatype: 'url',
									snaktype: 'value',
									datavalue: {
										type: 'string',
										value: $externalLink.attr( 'href' ).replace( /^\/\//, 'https://' )
									}
								} ]
							}
						};
	
						// P813
						if ( _this.config.markChecked !== '' ) {
							var $accessed = $externalLinks.parent().find( 'small:contains("' + _this.config.markChecked + '")' );
							if ( $accessed.length ) {
								var accessDate = _this.createTimeSnak( $accessed.first().text() );
								if ( accessDate ) {
									source.snaks.P813 = [ {
										property: 'P813',
										datatype: 'time',
										snaktype: 'value',
										datavalue: {
											type: 'time',
											value: accessDate
										}
									} ];
								}
							}
						}
	
						// P1065 + P2960
						if ( _this.config.markArchived !== '' ) {
							var $archiveLinks = $externalLinks.filter( 'a:contains("' + _this.config.markArchived + '")' );
							if ( $archiveLinks.length ) {
								var $archiveLink = $archiveLinks.first();
								source.snaks.P1065 = [ {
									property: 'P1065',
									datatype: 'url',
									snaktype: 'value',
									datavalue: {
										type: 'string',
										value: $archiveLink.attr( 'href' ).replace( /^\/\//, 'https://' )
									}
								} ];
		
								var archiveDate = _this.createTimeSnak( $archiveLink.parent().text().replace( _this.config.markArchived, '' ).trim() );
								if ( archiveDate ) {
									source.snaks.P2960 = [ {
										property: 'P2960',
										datatype: 'time',
										snaktype: 'value',
										datavalue: {
											type: 'time',
											value: archiveDate
										}
									} ];
								}
							}
						}
	
						references.push( source );
						break;
					}
				}
			}
			references.push( { snaks: _this.config.references } );
			return references;
		};
	
		/**
		 * Format sources for display
		 */
		this.formatDomains = function ( references ) {
			var $result = $( '<sup>' );
			for ( var i = 0; i < references.length; i++ ) {
				var p854 = references[ i ].snaks.P854;
				if ( p854 ) {
					var domain = p854[ 0 ].datavalue.value.replace( 'http://', '' ).replace( 'https://', '' ).replace( 'www.', '' );
					if ( domain.indexOf( '/' ) > 0 ) {
						domain = domain.substr( 0, domain.indexOf( '/' ) );
					}
					$result.append( $( '<a>' ).attr( 'href', p854[ 0 ].datavalue.value ).text( '[' + domain + ']' ) );
				}
			}
			return $result;
		};
	
		/**
		 * Formatting wikidata values for display to the user
		 */
		this.formatDataValue = function ( datavalue ) {
			var $label = $( '<span>' );
			switch ( datavalue.type ) {
				case 'time':
					var bceMark = ( datavalue.value.time.charAt( 0 ) === '-' ? _this.i18n.bcePostfix : '' );
	
					if ( datavalue.value.precision === 7 ) {
						$label.text( _this.config.centuries[ Math.floor( (datavalue.value.time.substr( 1, 4 ) - 1) / 100 ) ] + _this.i18n.agePostfix + bceMark );
						break;
					}
					var options = {};
					if ( datavalue.value.precision > 7 ) {
						options.year = 'numeric';
					}
					if ( datavalue.value.precision > 9 ) {
						options.month = 'long';
					}
					if ( datavalue.value.precision > 10 ) {
						options.day = 'numeric';
					}
					var parsedDate = new Date( Date.parse( datavalue.value.time.substring( 1 ).replace( /-00/g, '-01' ) ) );
					$label.text( parsedDate.toLocaleString( _this.config.userLanguage, options ) + ( datavalue.value.precision === 8 ? _this.i18n.decadePostfix : '' ) + bceMark );
					break;
	
				case 'quantity':
					$label.append( $( '<strong>' ).text( datavalue.value.amount ) );
					if ( datavalue.value.bound ) {
						$label.append( $( '<span>' ).text( ' ± ' + datavalue.value.bound ) );
					}
					if ( datavalue.value.unit !== '1' ) {
						var unitId = datavalue.value.unit.substr( datavalue.value.unit.indexOf( 'Q' ) );
						var name = ( ( _this.config.units[ unitId ] || {} ).label || {} ).value || unitId;
						var description = ( ( _this.config.units[ unitId ] || {} ).description || {} ).value || _this.i18n.noDescription;
						$label.append( '&nbsp;' ).append( $( '<abbr>' ).attr( 'title', description ).text( name ) );
					}
					break;
	
				case 'wikibase-entityid':
					$label.append( $( '<strong>' ).text( datavalue.value.label ? datavalue.value.label : datavalue.value.id ) )
						.append( datavalue.value.description ? ' — ' + datavalue.value.description : '' );
					break;
			}
	
			for ( var propertyId in datavalue.qualifiers ) {
				if ( !datavalue.qualifiers.hasOwnProperty( propertyId ) ) {
					continue;
				}
				if ( propertyId === 'P1480' && datavalue.qualifiers[ propertyId ][ 0 ].datavalue.value.id === 'Q5727902' ) {
					$label.prepend( $( '<abbr>' ).attr( 'title', _this.i18n.circaTitle ).text( _this.i18n.circaPrefix ), ' ' );
				} else {
					var formatted = _this.formatDataValue( datavalue.qualifiers[ propertyId ][ 0 ].datavalue );
					if ( formatted && $( '<span>' ).append( formatted ).text() ) {
						$label.append( $( '<span>' ).text( ' (' ).append( formatted ).append( ')' ) );
					}
				}
			}
	
			return $label;
		};
	
		this.getWikidataIds = function ( titles, callback ) {
			var languages = titles.map( function ( item ) {
				return item.language;
			} );
			languages = $.merge( languages, _this.config.languages );
			languages = $.uniqueSort( languages );
	
			var sites = titles.map( function ( item ) {
				return item.project;
			} );
	
			_this.wdApi.get( {
				action: 'wbgetentities',
				sites: sites,
				languages: languages,
				props: [ 'labels', 'descriptions', 'claims' ],
				titles: titles.map( function ( item ) {
					return item.label;
				} )
			} ).done( function ( data ) {
				if ( data.success ) {
					var valuesObj = {};
					var value;
	
					for ( var entityId in data.entities ) {
						if ( !data.entities.hasOwnProperty( entityId ) || !entityId.match( /^Q/ ) ) {
							continue;
						}
	
						var entity = data.entities[ entityId ];
						var label = entity.labels[ _this.config.userLanguage ] || entity.labels.en || entity.labels[ Object.keys( entity.labels )[ 0 ] ] || '';
						var description = entity.descriptions[ _this.config.userLanguage ] || entity.descriptions.en || entity.descriptions[ Object.keys( entity.descriptions )[ 0 ] ] || '';
	
						if ( ( ( ( ( ( ( ( entity || {} ).claims || {} ).P31 || [] )[ 0 ] || {} ).mainsnak || {} ).datavalue || {} ).value || {} ).id === 'Q4167410' ) {
							continue; // skip disambigs
						}
	
						var subclassFound = false;
						var subclassEntity = null;
						for ( var candidateId in data.entities ) {
							if ( !data.entities.hasOwnProperty( candidateId ) || !candidateId.match( /^Q/ ) || entityId === candidateId ) {
								continue;
							}
	
							subclassFound = [ 'P17', 'P31', 'P131', 'P279', 'P361' ].find( function ( propertyId ) {
								var values = ( ( ( data.entities[ candidateId ] || {} ).claims || {} )[ propertyId ] || [] );
								return values.find( function ( statement ) {
									var result = ( ( ( statement.mainsnak || {} ).datavalue || {} ).value || {} ).id === entityId;
									if ( result ) {
										subclassEntity = data.entities[ candidateId ];
									}
									return result;
								} );
							} );
	
							if ( subclassFound ) {
								break;
							}
						}
	
						if ( subclassFound ) {
							if ( subclassEntity ) {
								var subclassLabel = subclassEntity.labels[ _this.config.userLanguage ] || subclassEntity.labels.en || subclassEntity.labels[ Object.keys( subclassEntity.labels )[ 0 ] ];
								var text = _this.i18n.morePreciseValue
									.replace( '$1', label.value )
									.replace( '$2', subclassLabel.value );
								mw.notify(text , {
									type: 'warn',
									tag: 'wikidataInfoboxExport-warn-precise'
								} );
							}
							continue; // skip values for which there are more accurate values
						}
	
						value = {
							wd: {
								type: 'wikibase-entityid',
								value: {
									id: entityId,
									label: label ? label.value : label,
									description: description ? description.value : description
								}
							}
						};
						if ( label ) {
							var results = titles.filter( function ( item ) {
								return item.label.toLowerCase() === label.value.toLowerCase();
							} );
							if ( results.length === 1 ) {
								value.wd.qualifiers = results[ 0 ].qualifiers;
							}
						}
						value.label = _this.formatDataValue( value.wd );
						delete value.wd.value.label;
						delete value.wd.value.description;
						valuesObj[ entityId ] = value;
					}
	
					callback( valuesObj );
				}
			} );
		};
	
		this.parseItems = function ( $content, $wrapper, callback ) {
			var processWbGetItems = function ( valuesObj ) {
				var values = $.map( valuesObj, function( value, index ) {
					return [ value ];
				} );
				if ( values.length === 1 ) {
					value = values.pop();
					_this.addQualifiers( $wrapper, value.wd, value.label, function( value ) {
						callback( [ value ] );
					} );
				} else if ( callback ) {
					callback( values );
				}
			};

			var titles = [];
	
			for ( var k = 0; k < _this.config.fixedValues.length; k++ ) {
				var fixedValue = _this.config.fixedValues[ k ];
				if ( $content.attr( 'data-wikidata-property-id' ) === fixedValue.property &&
					$content.text().match( fixedValue.regexp )
				) {
					var result = { success: true, entities: {} };
					result.entities[ fixedValue.item ] = {
						labels: { ru: { value: fixedValue.label } },
						descriptions: {}
					};
					processWbGetItems( result );
					return;
				}
			}
	
			var $links = $content.find( 'a[title][class!=image][class!=new]' );
			var redirects = [];
	
			if ( $links.length ) {
				for ( var j = 0; j < $links.length; j++ ) {
					var $link = $( $links[ j ] );
					if ( $link.parents( '[data-wikidata-qualifier-id]' ).length ) {
						continue;
					}
					var extractedUrl = decodeURIComponent( $link.attr( 'href' ) ).replace( /^.*\/wiki\//, '' );
					if ( extractedUrl ) {
						extractedUrl = extractedUrl.replace( /_/g, ' ' ).trim();
						var value = {
							label: extractedUrl.charAt( 0 ).toUpperCase() + extractedUrl.substr( 1, extractedUrl.length - 1 ),
							language: _this.config.contentLanguage,
							project: _this.config.project,
							qualifiers: {}
						};
						var match = $links[ j ].innerHTML.match( _this.config.reSinceYear );
						if ( !match ) {
							match = $links[ j ].innerHTML.match( _this.config.reUntilYear );
						}
						var extractedYear = match ? _this.createTimeSnak( match[ 1 ] ) : _this.createTimeSnak( ( $links[ j ].nextSibling || {} ).textContent );
						if ( extractedYear ) {
							value.qualifiers.P585 = [ {
								property: 'P585',
								datatype: 'time',
								snaktype: 'value',
								datavalue: {
									type: 'time',
									value: extractedYear
								}
							} ];
						}
						if ( $link.hasClass( 'extiw' ) ) {
							var m = $links[ j ].getAttribute( 'href' ).match( /^https:\/\/([a-z\-]+)\.(wik[^\.]+)\./ );
							if ( m && m[ 2 ] !== 'wikimedia' ) {
								value.language = m[ 1 ];
								value.project = m[ 1 ] + m[ 2 ].replace( 'wikipedia', 'wiki' );
							}
						}
						if ( $link.hasClass( 'mw-redirect' ) ) {
							redirects.push( extractedUrl );
						}
						titles.push( value );
						if ( $( $links[ j ] ).find( 'img' ) ) {
							redirects.push( extractedUrl );
						}
					}
				}
			} else if ( $content.text().trim() ) {
				// If no links found try to search for articles by text value
				var parts = $content.text().split( /[\n,;]+/ );
				for ( var i in parts ) {
					var year = '';
					var articleTitle = parts[ i ].replace( /\([^)]*\)/, function ( match ) {
						year = match.replace( /\(\)/, '' );
						return '';
					} ).trim();
					if ( articleTitle ) {
						var value = {
							label: articleTitle.charAt( 0 ).toUpperCase() + articleTitle.substr( 1, articleTitle.length - 1 ),
							language: _this.config.language,
							project: _this.config.project,
							qualifiers: {}
						};
						if ( _this.createTimeSnak( year ) ) {
							value.qualifiers.P585 = [ {
								property: 'P585',
								datatype: 'time',
								snaktype: 'value',
								datavalue: {
									type: 'time',
									value: _this.createTimeSnak( year )
								}
							} ];
						}
						titles.push( value );
					}
				}
				titles = $.uniqueSort( titles );
			}
			if ( redirects.length ) {
				_this.api.get( {
					action: 'query',
					redirects: 1,
					titles: redirects
				} ).done( function ( data ) {
					if ( data.query && data.query.redirects ) {
						for ( var i = 0; i < data.query.redirects.length; i++ ) {
							for ( var j = 0; j < titles.length; j++ ) {
								var lcTitle = titles[ j ].label.substr( 0, 1 ).toLowerCase() + titles[ j ].label.substr( 1 );
								var lcRedirect = data.query.redirects[ i ].from.substr( 0, 1 ).toLowerCase() + data.query.redirects[ i ].from.substr( 1 );
								if ( lcTitle === lcRedirect ) {
									titles.splice( j + 1, 0, {
										label: data.query.redirects[ i ].to,
										language: _this.config.contentLanguage,
										project: _this.config.project,
										year: titles[ j ].year
									} );
									j++;
								}
							}
						}
					}
	
					_this.getWikidataIds( titles, processWbGetItems );
				} );
			} else {
				_this.getWikidataIds( titles, processWbGetItems );
			}
		};
	
		/**
		 * Parsing the number and (optionally) the accuracy
		 */
		this.parseQuantity = function ( text, forceInteger ) {
			var out = {
				value: {},
			};
			text = text.replace( /,/g, '.' ).replace( /[−–—]/g, '-' ).trim();
	
			// Sourcing circumstances (P1480) = circa (Q5727902)
			var circaMatch = text.match( _this.config.reCirca );
			if ( circaMatch ) {
				out.qualifiers = {
					P1480: [ {
						property: 'P1480',
						snaktype: 'value',
						datavalue: {
							type: 'wikibase-entityid',
							value: { id: 'Q5727902' }
						}
					} ],
				};
				text = text.replace( circaMatch[ 0 ], '' );
			}
	
			var magnitude = 0;
			if ( text.match( _this.config.re10_3 ) ) {
				magnitude += 3;
			} else if ( text.match( _this.config.re10_6 ) ) {
				magnitude += 6;
			} else if ( text.match( _this.config.re10_9 ) ) {
				magnitude += 9;
			} else if ( text.match( _this.config.re10_12 ) ) {
				magnitude += 12;
			} else {
				var match = text.match( /[\*|·]10(-?\d+)/ );
				if ( match ) {
					text = text.replace( /[\*|·]10(-?\d+)/, '' );
					magnitude += parseInt( match[ 1 ] );
				}
			}
			var decimals = text.split( '±' );
			if ( magnitude === 0 && forceInteger ) {
				decimals[ 0 ] = decimals[ 0 ].replace( /\./g, '' ).trim();
			}
	
			var amount;
			var bound;
			var interval = decimals[ 0 ].split( '-' );
			if ( magnitude === 0 &&
				decimals.length === 1 &&
				interval.length === 2 &&
				interval[ 0 ].length !== 0 &&
				interval[ 1 ].length !== 0
			) {
				out.value.lowerBound = interval[ 0 ].replace( /[^0-9.+-]/g, '' );
				out.value.upperBound = interval[ 1 ].replace( /[^0-9.+-]/g, '' );
				parts = out.value.lowerBound.match( /(\d+)\.(\d+)/ );
				fractional = parts ? parts[ 2 ].length : 0;
				out.value.amount = ( ( parseFloat( out.value.upperBound ) + 
									parseFloat( out.value.lowerBound ) )/2 )
									.toFixed( fractional + 1 );
				out.value.bound = ( ( parseFloat( out.value.upperBound ) - 
									parseFloat( out.value.lowerBound ) )/2 )
									.toFixed( fractional + 1 );
				return out;
			} else {
				amount = parseFloat( decimals[ 0 ].replace( /[^0-9.+-]/g, '' ) );
			}
	
			if ( isNaN( amount ) ) {
				return;
			}
	
			var parts = amount.toString().match( /(\d+)\.(\d+)/ );
			var integral = parts ? parts[ 1 ].length : amount.toString().length;
			var fractional = parts ? parts[ 2 ].length : 0;
			if ( magnitude >= 0 ) {
				if ( magnitude <= fractional ) {
					out.value.amount = ( ( '1e' + magnitude ) * amount ).toFixed( fractional - magnitude );
				} else {
					out.value.amount = ( ( '1e' + fractional ) * amount ).toFixed( 0 ).replace( /$/, new Array( magnitude - fractional + 1 ).join( '0' ) );
				}
			} else {
				if ( magnitude >= -integral ) {
					out.value.amount = ( ( '1e' + magnitude ) * amount ).toFixed( fractional - magnitude );
				} else {
					out.value.amount = ( ( '1e-' + integral ) * amount ).toFixed( integral + fractional ).replace( /0\./, '0.' + new Array( -magnitude - integral + 1 ).join( '0' ) );
				}
			}
	
			if ( decimals.length > 1 ) {
				bound = parseFloat( decimals[ 1 ].replace( /[^0-9.+-]/g, '' ) );
			}
	
			if ( !isNaN( bound ) ) {
				if ( decimals.length > 1 && decimals[ 1 ].indexOf( '%' ) > 0 ) {
					bound = amount * bound / 100;
				} else {
					parts = bound.toString().match( /(\d+)\.(\d+)/ );
					integral = parts ? parts[ 1 ].length : amount.toString().length;
					fractional = parts ? parts[ 2 ].length : 0;
				}
				if ( magnitude >= 0 ) {
					if ( magnitude <= fractional ) {
						out.value.lowerBound = ( ( '1e' + magnitude ) * ( amount - bound ) ).toFixed( fractional - magnitude );
						out.value.upperBound = ( ( '1e' + magnitude ) * ( amount + bound ) ).toFixed( fractional - magnitude );
						out.value.bound = ( ( '1e' + magnitude ) * bound ).toFixed( fractional - magnitude ); // need to show it to user
					} else {
						out.value.lowerBound = ( ( '1e' + fractional) * ( amount - bound ) ).toFixed( 0 ).replace( /$/, new Array( magnitude - fractional + 1 ).join( '0' ) );
						out.value.upperBound = ( ( '1e' + fractional) * ( amount + bound ) ).toFixed( 0 ).replace( /$/, new Array( magnitude - fractional + 1 ).join( '0' ) );
						out.value.bound = ( ( '1e' + fractional ) * bound ).toFixed( 0 ).replace( /$/, new Array( magnitude - fractional + 1 ).join( '0' ) );
					}
				} else {
					if ( magnitude >= -integral ) {
						out.value.lowerBound = ( ( '1e' + magnitude ) * ( amount - bound ) ).toFixed( fractional - magnitude );
						out.value.upperBound = ( ( '1e' + magnitude ) * ( amount + bound ) ).toFixed( fractional - magnitude );
						out.value.bound = ( ( '1e' + magnitude ) * bound ).toFixed( fractional - magnitude );
					} else {
						out.value.lowerBound = ( ( '1e-' + integral ) * ( amount - bound ) ).toFixed( integral + fractional ).replace( /0\./, '0.' + new Array( -magnitude - integral + 1 ).join( '0' ) );
						out.value.upperBound = ( ( '1e-' + integral ) * ( amount + bound ) ).toFixed( integral + fractional ).replace( /0\./, '0.' + new Array( -magnitude - integral + 1 ).join( '0' ) );
						out.value.bound = ( ( '1e-' + integral ) * bound ).toFixed( integral + fractional ).replace( /0\./, '0.' + new Array( -magnitude - integral + 1 ).join( '0' ) );
					}
				}
			}
			return out;
		};
	
		/**
		 * Recognition of units of measurement in the infobox parameter and its label
		 */
		this.recognizeUnits = function ( text, units, label ) {
			if ( Array.isArray( units ) && units.length === 0 ) {
				return [ '1' ];
			}
			var result = [];
			for ( var idx in units ) {
				if ( !units.hasOwnProperty( idx ) ) {
					continue;
				}
				var item = parseInt( idx ) >= 0 ? units[ idx ] : idx;
				var search = _this.config.units[ item ].search;
				for ( var j = 0; j < search.length; j++ ) {
					var expr = search[ j ];
					if ( search[ j ].charAt( 0 ) !== '^' ) {
						expr = '[\\d\\s\\.]' + expr;
						if ( search[ j ].length < 5 ) {
							expr = expr + '\\.?$';
						}
					}
					if ( text.match( new RegExp( expr ) ) ) {
						result.push( item );
						break;
					} else if ( search[ j ].charAt( 0 ) !== '^' && label && label.match( new RegExp( '\\s' + search[ j ] + ':?$' ) ) ) {
						result.push( item );
						break;
					}
				}
			}
			return result;
		};
	
		/**
		 * Create all statements in Wikidata and mark properties exported
		 */
		this.createClaims = function ( propertyId, values, refUrl, revIds ) {
			var value = values.shift();
			revIds = revIds || [];
			if ( !value ) {
				// All statements are added - go to the tagging
				_this.addTags( propertyId, revIds );
				return;
			} else {
				value = JSON.parse( value );
			}
			if ( _this.config.properties[ propertyId ] === undefined ) {
				mw.notify( _this.i18n.noPropertyData.replace( '$1', propertyId ), {
					type: 'error',
					tag: 'wikidataInfoboxExport-property-error'
				} );
				return;
			}
			var datatype = _this.config.properties[ propertyId ].datatype;
			var mainsnak = value.value.toString().match( /^(novalue|somevalue)$/ ) ? {
				snaktype: value.value,
				property: propertyId
			} : {
				snaktype: 'value',
				property: propertyId,
				datavalue: {
					type: _this.typesMapping[ datatype ] ? _this.typesMapping[ datatype ] : datatype,
					value: value.value
				}
			};
			var claim = {
				type: 'statement',
				mainsnak: mainsnak,
				id: _this.claimGuid( mw.config.get( 'wgWikibaseItemId' ) ),
				references: refUrl,
				rank: 'normal'
			};
			if ( value.qualifiers ) {
				claim.qualifiers = value.qualifiers;
			}
	
			_this.wdApi.postWithToken( 'csrf', {
				action: 'wbsetclaim',
				claim: JSON.stringify( claim ),
				baserevid: _this.baseRevId
			} ).done( function ( claimData ) {
				if ( claimData.success ) {
					var valuesLeftStr = values.length ? _this.i18n.valuesLeft.replace( '$1', values.length ) : '';
					mw.notify( _this.i18n.valueSaved.replace( '$1', propertyId ) + valuesLeftStr, {
						tag: 'wikidataInfoboxExport-success'
					} );
	
					_this.baseRevId = claimData.pageinfo.lastrevid;
					revIds.push( _this.baseRevId );
					_this.createClaims( propertyId, values, refUrl, revIds );
				} else {
					_this.errorDialog( _this.i18n.saveFailed, JSON.stringify( claimData ) );
				}
			} );
		};
	
		/**
		 * Setting gadget tags for edits
		 * FIXME: Add tags directly for edit when [[phab:T155109]] will be fixed
		 */
		this.addTags = function ( propertyId, revIds ) {
			_this.wdApi.postWithToken( 'csrf', {
				action: 'tag',
				add: 'InfoboxExport gadget',
				revid: revIds
			} ).done( function ( data ) {
				var success = false;
				if ( data.tag ) {
					success = true;
					for ( var i = 0; i < data.tag.length; i++ ) {
						if ( data.tag[ i ].status !== 'success' ) {
							success = false;
							break;
						}
					}
				}
				if ( success ) {
					mw.notify( _this.i18n.tagsSaved, {
						tag: 'wikidataInfoboxExport-tags-success'
					} );
	
					$( '.no-wikidata[data-wikidata-property-id=' + propertyId + ']' )
						.removeClass( 'no-wikidata' )
						.off( 'dblclick', _this.clickEvent );
				} else {
					mw.notify( _this.i18n.tagsFailed, {
						type: 'warn',
						tag: 'wikidataInfoboxExport-tags-error'
					} );
				}
			} );
		};
	
		/**
		 * Wrapper for property preloading that excludes already loaded properties
		 */
		this.loadProperties = function ( propertyIds ) {
			if ( !propertyIds || !propertyIds.length ) {
				return;
			}
	
			var realPropertyIds = [];
			for ( var i in propertyIds ) {
				var propertyId = propertyIds[ i ];
				if ( propertyId && _this.config.properties[ propertyId ] === undefined ) {
					realPropertyIds.push( propertyId );
				}
			}
	
			if ( realPropertyIds.length ) {
				_this.realLoadProperties( realPropertyIds );
			}
		};
	
		/**
		 * Preload information on all properties
		 */
		this.realLoadProperties = function ( propertyIds ) {
			if ( !propertyIds || !propertyIds.length ) {
				return;
			}
	
			var units = [];
			_this.wdApi.get( {
				action: 'wbgetentities',
				languages: _this.config.languages,
				props: [ 'labels', 'datatype', 'claims' ],
				ids: propertyIds
			} ).done( function ( data ) {
				if ( !data.success ) {
					return;
				}
	
				for ( var propertyId in data.entities ) {
					if ( !data.entities.hasOwnProperty( propertyId ) ) {
						continue;
					}
					var entity = data.entities[ propertyId ];
					var label = entity.labels[ _this.config.language ] ? entity.labels[ _this.config.language ].value : entity.labels.en.value;
					_this.config.properties[ propertyId ] = {
						datatype: entity.datatype,
						label: label.charAt( 0 ).toUpperCase() + label.slice( 1 ),
						constraints: { qualifier: [] },
						units: []
					};
					if ( propertyId === 'P1128' || propertyId === 'P2196' ) {
						_this.config.properties[ propertyId ].constraints.integer = 1;
					}
					if ( entity.claims ) {
						// Property restrictions
						if ( entity.claims.P2302 ) {
							for ( var i in entity.claims.P2302 ) {
								var type = ( ( ( ( entity.claims.P2302[ i ] || {} ).mainsnak || {} ).datavalue || {} ).value || {} ).id;
								switch ( type ) {
									case 'Q19474404':
									case 'Q21502410':
										_this.config.properties[ propertyId ].constraints.unique = 1;
										break;
									case 'Q21510856': // Required
										qualifiers = ( ( ( entity.claims.P2302[ i ] || {} ).qualifiers || {} ).P2306 || [] );
										for ( var idx = 0; idx < qualifiers.length; idx++) {
											var qualifierId = ( ( ( qualifiers[ idx ] || {}).datavalue || {} ).value || {} ).id;
											if ( qualifierId ) {
												_this.config.properties[ propertyId ].constraints.qualifier.push( qualifierId.toString() );
											}
										}
										break;
									case 'Q21514353': // Units
										qualifiers = ( ( ( entity.claims.P2302[ i ] || {} ).qualifiers || {} ).P2305 || [] );
										for ( var idx = 0; idx < qualifiers.length; idx++) {
											var unitId = ( ( ( qualifiers[ idx ] || {}).datavalue || {} ).value || {} ).id;
											if ( unitId ) {
												_this.config.properties[ propertyId ].units.push( unitId );
												units.push( unitId );
											}
										}
										break;
								}
							}
						}
					}
				}
	
				for ( var idx = 0; idx < $.uniqueSort( units ).length; idx += 50) {
					_this.wdApi.get( {
						action: 'wbgetentities',
						languages: _this.config.languages,
						props: [ 'labels', 'descriptions', 'aliases', 'claims' ],
						ids: $.uniqueSort( units ).slice( idx, idx + 50 )
					} ).done( function ( unitData ) {
						if ( !unitData.success ) {
							return;
						}
		
						for ( var unitId in unitData.entities ) {
							var unit = unitData.entities[ unitId ];
							var unitSearch = _this.config.units[ unitId ] ? _this.config.units[ unitId ].search : [];
							if ( !_this.config.units[ unitId ] ) {
								_this.config.units[ unitId ] = {};
							}
		
							// Label
							if ( unit.labels ) {
								_this.config.units[ unitId ].label = unit.labels[ _this.config.userLanguage ] ||
									unit.labels.en ||
									unit.labels[ Object.keys( unit.labels )[ 0 ] ];
		
								if ( unit.labels[ _this.config.userLanguage ] ) {
									unitSearch.push( unit.labels[ _this.config.userLanguage ].value.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ) );
								}
							}
		
							// Description
							if ( unit.descriptions ) {
								_this.config.units[ unitId ].description = unit.descriptions[ _this.config.userLanguage ] ||
									unit.descriptions.en ||
									unit.descriptions[ Object.keys( unit.labels )[ 0 ] ];
							}
		
							// Aliases
							if ( unit.aliases && unit.aliases[ _this.config.userLanguage ] ) {
								for ( var i in unit.aliases[ _this.config.userLanguage ] ) {
									unitSearch.push( unit.aliases[ _this.config.userLanguage ][ i ].value.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ) );
								}
							}
		
							// Units (P558)
							if ( unit.claims && unit.claims.P558 ) {
								for ( var i in unit.claims.P558 ) {
									var claim = unit.claims.P558[ i ];
									if ( claim.mainsnak &&
										claim.mainsnak.datavalue &&
										claim.mainsnak.datavalue.value
									) {
										unitSearch.push( claim.mainsnak.datavalue.value.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&' ) );
									}
								}
							}
							_this.config.units[ unitId ].search = $.uniqueSort( unitSearch );
		
							_this.saveConfig();
						}
					} );
				}
			} );
		};
	
		this.checkForMisLang = function ( wd ) {
			var lang = wd.value.language;
			if ( 'misLang' in _this.config && lang in _this.config.misLang ) {
				wd.value.language = 'mis';
				if ( !( 'qualifiers' in wd ) ) {
					wd.qualifiers = {};
				}
				wd.qualifiers.P585 = [ {
					property: 'P407',
					snaktype: 'value',
					datavalue: {
						type: 'wikibase-entityid',
						value: { id: _this.config.misLang[ lang ] }
					}
				} ];
			}
	
			return wd;
		};
	
		this.addQualifiers = function ( $field, value, $label, callback ) {
			var $qualifiers = $field.find( '[data-wikidata-qualifier-id]' );
			if ( $qualifiers.length ) {
				$label = $( '<div>' ).append( $label );
			}
	
			var addQualifierValue = function ( qualifierId, qualifierValue, qualifierLabel ) {
				if ( value.qualifiers === undefined ) {
					value.qualifiers = {};
				}
				if ( value.qualifiers[ qualifierId ] === undefined ) {
					value.qualifiers[ qualifierId ] = [];
				}
				var datatype = _this.config.properties[ qualifierId ].datatype;
				value.qualifiers[ qualifierId ].push( {
					snaktype: 'value',
					property: qualifierId,
					datavalue: {
						type: _this.typesMapping[ datatype ] ? _this.typesMapping[ datatype ] : datatype,
						value: qualifierValue
					}
				} );
				$label.append( $( '<p>' )
					.append( $( '<a>' )
						.attr( 'href', '//www.wikidata.org/wiki/Property:' + qualifierId )
						.text( _this.config.properties[ qualifierId ].label )
					)
					.append( $( '<span>' ).text( ': ' ) )
					.append( qualifierLabel )
				);
			};
	
			var qualifierTitles = {};
			for ( var q = 0; q < $qualifiers.length; q++ ) {
				var $qualifier = $( $qualifiers[ q ] );
				var qualifierId = $qualifier.data( 'wikidata-qualifier-id' );
				var qualifierValue = $qualifier.text().replace( '\n', ' ' ).trim();
				switch ( _this.config.properties[ qualifierId ].datatype ) {
					case 'monolingualtext':
						qualifierValue = {
							text: $qualifier.text().replace( '\n', ' ' ).trim(),
							language: $qualifier.attr( 'lang' ) || _this.config.language
						};
						addQualifierValue( qualifierId, qualifierValue, $qualifier.text() );
						break;
	
					case 'string':
						qualifierValue = $qualifier.text().replace( '\n', ' ' ).trim();
						addQualifierValue( qualifierId, qualifierValue, $qualifier.text() );
						break;
	
					case 'time':
						qualifierValue = _this.createTimeSnak( qualifierValue );
						addQualifierValue( qualifierId, qualifierValue, $qualifier.text() );
						break;
	
					case 'wikibase-item':
						if ( qualifierTitles[ qualifierId ] === undefined ) {
							qualifierTitles[ qualifierId ] = [];
						}
	
						var $links = $qualifier.find( 'a[title][class!=image][class!=new]' );
						if ( $links.length ) {
							for ( var l = 0; l < $links.length; l++ ) {
								var $link = $( $links[ l ] );
								var extractedUrl = decodeURIComponent( $link.attr( 'href' ) ).replace( /^.*\/wiki\//, '' );
								if ( extractedUrl ) {
									extractedUrl = extractedUrl.replace( /_/g, ' ' ).trim();
									var title = {
										label: extractedUrl.charAt( 0 ).toUpperCase() + extractedUrl.substr( 1, extractedUrl.length - 1 ),
										language: _this.config.contentLanguage,
										project: _this.config.project,
										qualifiers: {}
									};
									if ( $link.hasClass( 'extiw' ) ) {
										var m = $link.attr( 'href' ).match( /^https:\/\/([a-z\-]+)\.(wik[^\.]+)\./ );
										if ( m && m[ 2 ] !== 'wikimedia' ) {
											title.language = m[ 1 ];
											title.project = m[ 1 ] + m[ 2 ].replace( 'wikipedia', 'wiki' );
										}
									}
									qualifierTitles[ qualifierId ].push( title );
								}
							}
						} else {
							qualifierTitles[ qualifierId ].push( {
								label: qualifierValue.charAt( 0 ).toUpperCase() + qualifierValue.substr( 1, qualifierValue.length - 1 ),
								language: _this.config.contentLanguage,
								project: _this.config.project,
								qualifiers: {}
							} );
						}
						break;
				}
			}
	
			var processItemTitles = function ( itemTitles, callback ) {
				if ( Object.keys( itemTitles ).length ) {
					var qualifierId = Object.keys( itemTitles ).shift();
					var qualifierItemTitles = itemTitles[ qualifierId ];
					delete itemTitles[ qualifierId ];
					_this.getWikidataIds( qualifierItemTitles, function ( valuesObj ) {
						for ( var entityId in valuesObj ) {
							var valueObj = valuesObj[ entityId ];
							addQualifierValue( qualifierId, valueObj.wd.value, valueObj.label );
						}
						processItemTitles( itemTitles, callback );
					} );
				} else {
					callback( {
						wd: value,
						label: $label
					} );
				}
			};
			processItemTitles( qualifierTitles, callback );
		};
	
		/**
		 * Parsing values from parameters before displaying a dialog
		 */
		this.prepareDialog = function ( $field, propertyId ) {
			var values = [];
			var datatype = _this.config.properties[ propertyId ].datatype;
	
			var $content = $field.clone();
			$content.find( 'sup.reference' ).remove();
			$content.find( '[style*="display:none"]' ).remove();
	
			var $wrapper = $content;
			var $row = $field.closest( 'tr' );
			if ( $row.length === 1 && $row.find( '[data-wikidata-property-id]' ).length === 1 ) {
				$wrapper = $row.clone();
			}
	
			switch ( datatype ) {
				case 'commonsMedia':
					var $imgs = $content.find( 'img' );
					$imgs.each( function () {
						var $img = $( this );
						var src = $img.attr( 'src' );
						if ( !src.match( /upload.wikimedia.org\/wikipedia\/commons/ ) ) {
							return;
						}
						var srcParts = src.split( '/' );
						var fileName = srcParts.pop();
						if ( fileName.match( /(?:^|-)\d+px-/ ) ) {
							fileName = srcParts.pop();
						}
						fileName = decodeURIComponent( fileName );
						fileName = fileName.replace( /_/g, ' ' );
						var value = { value: fileName };
						var $label = $img.clone()
							.attr( 'title', fileName )
							.css( 'border', '1px dashed #a2a9b1' );
	
						_this.addQualifiers( $wrapper, value, $label, function( valueObj ) {
							values.push( valueObj );
						} );
					} );
					break;
	
				case 'external-id':
					var externalId = $content.data( 'wikidata-external-id' ) || $content.text();
					if ( propertyId === 'P345' ) { // IMDB
						externalId = $content.find( 'a' ).first().attr( 'href' );
						externalId = externalId.substr( externalId.lastIndexOf( '/', externalId.length - 2 ) ).replace( /\//g, '' );
					} else {
						externalId = externalId.toString().replace( /^ID\s/, '' ).replace( /\s/g, '' );
					}
					var sparql = 'SELECT * WHERE { ?item wdt:' + propertyId + ' "' + externalId + '" }';
	
					$.ajax( {
						url: 'https://query.wikidata.org/sparql?format=json&query=' + sparql,
						success: function ( data ) {
							var $label = $( '<code>' ).text( externalId );
							if ( data.results.bindings.length ) {
								var url = data.results.bindings[ 0 ].item.value;
								$label = $( '<span>' ).append( $( '<code>' ).text( externalId ) )
									.append( $( '<strong>' ).css( { 'color': 'red' } ).text( _this.i18n.alreadyUsedIn ) )
									.append( $( '<a>' ).attr( 'href', url ).attr( 'target', '_blank' ).text( url.replace( /[^Q]*Q/, 'Q' ) ) );
							}
							_this.dialog( $field, propertyId, [ {
								wd: { value: externalId.toString() },
								label: $label
							} ], _this.getReference( $content ) );
						}
					} );
					return;
	
				case 'string':
					var text = $content.data( 'wikidata-external-id' );
					if ( !text ) {
						text = $content.text();
					}
					var strings = text.toString().trim().split( /[\n,;]+/ );
	
					// Commons category
					if ( propertyId === 'P373' ) {
						var $link = $content.find( 'a[class="extiw"]' ).first();
						if ( $link.length ) {
							var url = $link.attr( 'href' );
							var value = url.substr( url.lastIndexOf( '/' ) + 1 )
								.replace( /_/g, ' ' )
								.replace( /^[Cc]ategory:/, '' )
								.replace( /\?.*$/, '' );
							value = decodeURIComponent( value );
							strings = [ value ];
						}
					}
	
					for ( var i in strings ) {
						var s = strings[ i ].replace( /\n/g, ' ' ).trim();
						if ( s ) {
							values.push( {
								wd: { value: s },
								label: $( '<code>' + s + '</code>' )
							} );
						}
					}
					break;
	
				case 'monolingualtext':
					var $items = $content.find( 'span[lang]' );
					$items.each( function () {
						var $item = $( this );
						values.push( {
							wd: _this.checkForMisLang( {
								value: {
									text: $item.text().trim(),
									language: $item.attr( 'lang' ).trim()
								}
							} )
						} );
					} );
					if ( !values.length ) {
						var text = $content.text().trim();
						if ( text ) {
							var $items = mw.util.$content.find( 'span[lang]' );
							$items.each( function () {
								$item = $( this );
								if ( $item.text().trim().startsWith( text ) ) {
									values.push( {
										wd: {
											value: {
												text: text,
												language: $item.attr( 'lang' ).trim()
											}
										}
									} );
								}
							} );
						}
					}
					var valueLanguages = [];
					for ( var i in values ) {
						var valueLanguage = values[ i ].wd.value.language;
						if ( valueLanguages.indexOf( valueLanguage ) > -1 ) {
							continue;
						}
						valueLanguages.push( valueLanguage );
						values[ i ].label = $( '<span>' )
							.append( $( '<span>' ).css( 'color', '#666' ).text( '(' + valueLanguage + ') ' ) )
							.append( $( '<strong>' ).text( values[ i ].wd.value.text ) );
					}
					values = values.filter( function( item ) {
						return item.label !== undefined;
					} );
					break;
	
				case 'quantity':
					var text = $content.text()
						.replace( /[\u00a0\u25bc\u25b2]/g, ' ' )
						.replace( /\s*\(([^)]*\))/g, '' )
						.trim();
	
					// Hack for time in formats "hh:mm:ss" and "00m 00s""
					var match = text.replace( _this.config.reMinSec, '$1:$2' )
						.match( /^(?:(\d+):)?(\d+):(\d+)$/ );
					if ( match ) {
						var amount = 0;
						for ( var i = 1; i < match.length; i++ ) {
							if ( match[ i ] !== undefined ) {
								amount = amount * 60 + parseInt( match[ i ], 10 );
							}
						}
	
						text = amount + _this.i18n.unitSec;
					}
	
					var result = { wd: _this.parseQuantity( text, _this.config.properties[ propertyId ].constraints.integer ) };
					if ( !result.wd || !result.wd.value ) {
						break;
					}
	
					_this.addQualifiers( $wrapper, result.wd, _this.formatDataValue( result.wd ), function( valueObj ) {
						result = valueObj;
					} );
	
					if ( _this.config.properties[ propertyId ].constraints.qualifier.indexOf( 'P585' ) !== -1 ) {
						var yearMatch = $content.text().match( /\(([^)]*[12]\s?\d\d\d)[,)\s]/ );
						if ( !yearMatch ) {
							yearMatch = $field.closest( 'tr' ).find( 'th' ).first().text().match( /\(([^)]*[12]\s?\d\d\d)[,)\s]/ );
						}
						if ( yearMatch ) {
							if ( extractedDate = _this.createTimeSnak( yearMatch[ 1 ].replace ( /(\d)\s(\d)/, '$1$2' ) ) ) {
								result.wd.qualifiers = {
									P585: [ {
										snaktype: 'value',
										property: 'P585',
										datavalue: {
											type: 'time',
											value: extractedDate
										}
									} ]
								};
							}
						}
					}
	
					var qualMatch = $content.text().match( /\(([^\)]*)/ );
					if ( qualMatch ) {
						qualQuantity = _this.parseQuantity( qualMatch[ 1 ] );
						if ( qualQuantity ) {
							var supportedProperties = [ 'P2076', 'P2077' ];
							for ( var j = 0; j < supportedProperties.length; j++ ) {
								var units = _this.recognizeUnits( qualMatch[ 1 ], _this.config.properties[ supportedProperties[ j ] ].units );
								if ( units.length === 1 ) {
									qualQuantity.value.unit = 'http://www.wikidata.org/entity/' + units[ 0 ];
									if ( !result.wd.qualifiers ) {
										result.wd.qualifiers = {};
									}
									result.wd.qualifiers[ supportedProperties[ j ] ] = [ {
										snaktype: 'value',
										property: supportedProperties[ j ],
										datavalue: {
											type: 'quantity',
											value: qualQuantity.value
										}
									} ];
								}
							}
						}
					}
	
					var founded = _this.recognizeUnits( text, _this.config.properties[ propertyId ].units, $field.closest( 'tr' ).find( 'th' ).first().text() );
					for ( var u = 0; u < founded.length; u++ ) {
						result.wd.value.unit = '1';
						if ( founded[ u ] !== '1' ) {
							result.wd.value.unit = 'http://www.wikidata.org/entity/' + founded[ u ];
							var item = _this.config.units[ founded[ u ] ];
						}
						result.wd.type = 'quantity';
						result.label = _this.formatDataValue( result.wd );
						values.push( result );
					}
					break;
	
				case 'time':
					var value = _this.createTimeSnak( $content.text().toLowerCase().trim().replace( _this.config.reYearPostfix, '' ),
						$content[ 0 ].outerHTML.includes( _this.config.markJulian ) );
					if ( value ) {
						if ( value.toString().match( /^(novalue|somevalue)$/ ) ) {
							var $label = $( '<span>' );
							if ( wueI18n.valuePrefix !== '' ) {
								$label.append( $( '<span>' ).css( 'color', '#666' ).text( wueI18n.valuePrefix ) );
							}
							$label.append( $( '<strong>' ).text( value.toString() === 'novalue' ? wueI18n.noValue : wueI18n.unknownValue ) );
	
							values.push( {
								wd: { value: value },
								label: $label
							} );
						} else {
							values.push( {
								wd: { value: value },
								label: $( '<span>' )
									.append( $( '<strong>' ).append( _this.formatDataValue( {
										type: 'time',
										value: value
									} ) ) )
									.append( $( '<span>' ).css( 'color', '#666' ).text( ' (' +
										( value.calendarmodel.includes( '1985727' ) ? _this.i18n.grigorianCalendar : _this.i18n.julianCalendar ) + ') ' ) )
							} );
						}
					}
					break;
	
				case 'wikibase-item':
					value = _this.parseItems( $content, $wrapper, function ( values ) {
						_this.dialog( $field, propertyId, values, _this.getReference( $content ) );
					} );
					return;
	
				case 'url':
					var $links = $content.find( 'a' );
					$links.each( function () {
						var $link = $( this );
						var url = $link.attr( 'href' ).replace( /^\/\//, 'https://' );
						values.push( {
							wd: { value: url },
							label: $( '<code>' + url + '</code>' )
						} );
					} );
					break;
	
				default:
					mw.notify( _this.i18n.unknownDatatype.replace( '$1', datatype ), {
						type: 'error',
						tag: 'wikidataInfoboxExport-error'
					} );
			}
	
			values = $.uniqueSort( values );
			_this.dialog( $field, propertyId, values, _this.getReference( $field ) );
		};
	
		/**
		 * Double-click event on the infobox field
		 */
		this.clickEvent = function ( e ) {
			var $field = $( this );
			var propertyId = $field.attr( 'data-wikidata-property-id' );
			_this.prepareDialog( $field, propertyId );
		};
	
		/**
		 * Display a dialog to confirm export
		 */
		this.dialog = function ( $field, propertyId, values, refUrl ) {
			var fieldset;
	
			if ( !values || !values.length ) {
				mw.notify( _this.i18n.parsingError, {
					type: 'error',
					tag: 'wikidataInfoboxExport-error'
				} );
				return;
			}
	
			// Create a dialog
			function ProcessDialog( config ) {
				ProcessDialog.super.call( this, config );
			}
	
			OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
			ProcessDialog.static.name = _this.i18n.windowHeader;
			ProcessDialog.static.title = $( '<span>' )
				.attr( 'title', _this.i18n.versionString.replace( '$1', _this.config.version ) )
				.text( ProcessDialog.static.name );
			ProcessDialog.static.actions = [
				{ action: 'export', label: _this.i18n.exportButtonLabel, flags: [ 'primary', 'progressive' ] },
				{ label: _this.i18n.cancelButtonLabel, flags: [ 'safe' ] }
			];
			ProcessDialog.prototype.initialize = function () {
				ProcessDialog.super.prototype.initialize.apply( this, arguments );
				this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
	
				fieldset = new OO.ui.FieldsetLayout();
				var firstSelected = false;
				for ( var i = 0; i < values.length; i++ ) {
					var alreadyInWikidata = ( _this.alreadyExistingItems[ propertyId ] || [] ).includes( ( ( values[ i ].wd || {} ).value || {} ).id );
					var checkbox = new OO.ui.CheckboxInputWidget( {
						value: JSON.stringify( values[ i ].wd ),
						selected: alreadyInWikidata,
						disabled: alreadyInWikidata
					} );
					if ( !checkbox.isDisabled() ) {
						if ( !firstSelected || !_this.config.properties[ propertyId ].constraints.unique ) {
							firstSelected = true;
							checkbox.setSelected( true );
						}
	
						if ( values[ i ].label[ 0 ].innerText.match( new RegExp( _this.i18n.alreadyUsedIn ) ) &&
							_this.config.properties[ propertyId ].constraints.unique &&
							_this.config.properties[ propertyId ].datatype === 'external-id' ) {
							checkbox.setSelected( false );
						}
					}
					if ( refUrl ) {
						values[ i ].label.append( _this.formatDomains( refUrl ) );
					}
					fieldset.addItems( [
						new OO.ui.FieldLayout( checkbox, {
							label: values[ i ].label,
							align: 'inline'
						} )
					] );
				}
	
				this.content.$element
					.append( $( '<p>' ).append( $( '<strong>' )
						.append( $( '<a>' ).attr( 'href', 'https://wikidata.org/wiki/Property:' + propertyId ).attr( 'target', '_blank' ).text( _this.config.properties[ propertyId ].label ) )
						.append( $( '<span>' ).text( ':' ) )
					) )
					.append( fieldset.$element )
					.append( $( '<hr>' ).css( 'margin-top', '1.5em' ) )
					.append( $( '<p>' ).text( _this.i18n.exportConfirmation ) )
					.append( $( '<p>' ).css( 'font-size', 'smaller' ).html( _this.i18n.licenseCc0  ) );
	
				this.$body.append( this.content.$element );
			};
			ProcessDialog.prototype.getActionProcess = function ( action ) {
				var dialog = this;
				if ( action === 'export' ) {
					return new OO.ui.Process( function () {
						var values = [];
						var fields = fieldset.getItems();
						for ( var i in fields ) {
							var checkbox = fields[ i ].getField();
							if ( checkbox.isSelected() && !checkbox.isDisabled() ) {
								values.push( checkbox.getValue() );
							}
						}
	
						_this.createClaims( propertyId, values, refUrl );
						dialog.close( { action: action } );
					}, this );
				}
				return ProcessDialog.super.prototype.getActionProcess.call( this, action );
			};
			var windowManager = new OO.ui.WindowManager();
			$( 'body' ).append( windowManager.$element );
			var processDialog = new ProcessDialog();
			windowManager.addWindows( [ processDialog ] );
			windowManager.openWindow( processDialog );
		};
	
		/**
		 * Initializing the gadget
		 */
		this.init = function () {
			if ( mw.config.get( 'wgWikibaseItemId' ) === null ||
				mw.config.get( 'wgAction' ) !== 'view' ||
				mw.util.getParamValue( 'veaction' ) !== null ||
				( window.ve && window.ve.init ) ||
				mw.config.get( 'wgNamespaceNumber' )
			) {
				return;
			}

			_this.loadConfig();
	
			var sparql = 'SELECT ?wiki WHERE { ?wiki wdt:P31/wdt:P279* wd:Q33120876 . ?wiki wdt:P856 ?site . FILTER REGEX(STR(?site), "https://' + location.host + '/") }';
			$.ajax( {
				url: 'https://query.wikidata.org/sparql?format=json&query=' + sparql,
				success: function ( data ) {
					if ( 0 === data.results.bindings.length ) {
						return;
					}
					// Add current wiki project as "imported from Wikimedia project"
					var projectId = data.results.bindings[ 0 ].wiki.value.replace( 'http://www.wikidata.org/entity/', '' );
					_this.config.references.P143 = [ {
						property: 'P143',
						snaktype: 'value',
						datavalue: {
							type: 'wikibase-entityid',
							value: { id: projectId }
						}
					} ];
					
					_this.initContinue();
				}
			} );
		};
	
		/**
		 * Save config to localStorage
		 */
		this.saveConfig = function () {
			var config = _this.config;
			for ( var key in config ) {
				var value = config[ key ];
				if ( value instanceof RegExp ) {
					config[ key ] = value.source;
				}
			}
			
			localStorage.setItem( config.storageKey, JSON.stringify( config ) );
		};
	
		/**
		 * Load config from localStorage
		 */
		this.loadConfig = function () {
			var config;
			try {
				config = JSON.parse( localStorage.getItem( _this.config.storageKey ) );
			} catch ( e ) {}

			for ( var key in config ) {
				if ( key.match( /^re[A-Z1]/ ) && typeof config[ key ] === 'string' ) {
					config[ key ] = new RegExp( config[ key ] );
				}
			}

			if ( config &&
				config.version === _this.config.version &&
				config.userLanguage === _this.config.userLanguage
			) {
				_this.config = config;
			}

			if ( _this.config.properties === undefined ) {
				_this.config.properties = {};
			}
		};

		/**
		 * Load config from localStorage
		 */
		this.loadCommonsConfig = function () {
			_this.commonsApi.get( {
				action: 'jsondata',
				formatversion: 2,
				title: 'I18n/WikidataInfoboxExportConfig.tab',
				uselang: _this.config.contentLanguage
			} ).done( function ( data ) {
				if ( !data.jsondata || !data.jsondata.data ) {
					return;
				}

				for ( var i in data.jsondata.data ) {
					var row = data.jsondata.data[ i ];
					var key = row[ 0 ].replace(/-([a-z1])/g, function ( g ) {
						return g[ 1 ].toUpperCase();
					});
					var value = row[ 1 ];
					if ( key.match( /^re[A-Z1]/ ) ) {
						value = value.replace( '%months%', _this.months.join( '|' ) );
						value = value.replace( '%months-gen%', _this.monthsGen.join( '|' ) );
						if ( value === '' ) {
							value = '^@{999}$'; // impossible regexp
						}
						value = new RegExp( value );
					}
					_this.config[ key ] = value;
				}

				_this.saveConfig();
			} );

			var config;
			try {
				config = JSON.parse( localStorage.getItem( _this.config.storageKey ) );
			} catch ( e ) {}

			for ( var key in config ) {
				if ( key.match( /^re[A-Z1]/ ) && typeof config[ key ] === 'string' ) {
					config[ key ] = new RegExp( config[ key ] );
				}
			}

			if ( config && config.version == _this.config.version ) {
				_this.config = config;
			}

			if ( _this.config.properties === undefined ) {
				_this.config.properties = {};
			}
		};

		/**
		 * Load internationalization data from Commons
		 */
		this.loadI18n = function () {
			_this.commonsApi.get( {
				action: 'jsondata',
				formatversion: 2,
				title: 'I18n/WikidataInfoboxExport.tab',
				uselang: _this.config.userLanguage
			} ).done( function ( data ) {
				if ( !data.jsondata || !data.jsondata.data ) {
					return;
				}
				for ( var i in data.jsondata.data ) {
					var row = data.jsondata.data[ i ];
					var key = row[ 0 ].replace(/-([a-z1])/g, function ( g ) {
						return g[ 1 ].toUpperCase();
					});
					_this.i18n[ key ] = row[ 1 ];
				}
				_this.i18n.licenseCc0 = _this.i18n.licenseCc0
					.replace( '$button', this.i18n.exportButtonLabel )
					.replace( '$terms', 'href="https://foundation.wikimedia.org/wiki/Terms_of_Use" class="extiw" title="wikimedia:Terms of Use"' )
					.replace( '$license', 'rel="nofollow" class="external text" href="https://creativecommons.org/publicdomain/zero/1.0/"' );
			} );
		};
		
		/**
		 * Load local month names from messages API
		 */
		this.loadMonths = function () {
			var messageKeys = [];
			for ( var i in _this.months ) {
				messageKeys.push( _this.months[ i ] );
				messageKeys.push( _this.months[ i ] + '-gen' );
			}
			_this.api.getMessages( messageKeys, { amlang: _this.config.contentLanguage } )
			.then( function ( messages ) {
				var monthLocal = [];
				var monthLocalGen = [];
				for ( var pos in _this.months ) {
					var key = _this.months[ pos ];
					monthLocal.push( messages[ key ] );
					monthLocalGen.push( messages[ key + '-gen' ] );
				}
				_this.months = monthLocal;
				_this.monthsGen = monthLocalGen;
			} );
		};

		/**
		 * Continue gadget initializing
		 */
		this.initContinue = function () {
			// Add a link to the current version of the page as "Wikimedia import URL"
			_this.config.references.P4656 = [ {
				property: 'P4656',
				datatype: 'url',
				snaktype: 'value',
				datavalue: {
					type: 'string',
					value: 'https://' + location.host + '/?oldid=' + mw.config.get( 'wgRevisionId' )
				}
			} ];

			_this.saveConfig();

			// API initialization
			_this.api = new mw.Api();
			_this.wdApi = new mw.ForeignApi( '//www.wikidata.org/w/api.php' );
			_this.commonsApi = new mw.ForeignApi( '//commons.wikimedia.org/w/api.php' );
	
			// Dialogs initialization
			_this.windowManager = new OO.ui.WindowManager();
			$( 'body' ).append( _this.windowManager.$element );
	
			_this.loadI18n();
			_this.loadMonths();
			_this.loadCommonsConfig();

			// Item data request
			_this.wdApi.get( {
				action: 'wbgetentities',
				props: [ 'info', 'claims' ],
				ids: mw.config.get( 'wgWikibaseItemId' )
			} ).done( function ( data ) {
				if ( data.success ) {
					var claims;
					for ( var i in data.entities ) {
						if ( i == -1 ) {
							return;
						}
	
						claims = data.entities[ i ].claims;
						_this.baseRevId = data.entities[ i ].lastrevid;
						break;
					}
					if ( !claims ) {
						return;
					}
	
					var $fields = $( '.infobox .no-wikidata' );
					$fields.each( function () {
						var $field = $( this );
						var propertyId = $field.attr( 'data-wikidata-property-id' );
	
						$field
							.removeClass( 'no-wikidata' )
							.off( 'dblclick' );
						_this.propertyIds.push( propertyId );
						_this.canExportValue( $field, claims[ propertyId ], function ( hasClaims ) {
							$field.addClass( 'no-wikidata' );
							if ( hasClaims === true ) {
								$field.addClass( 'partial-wikidata' );
							}
							$field.on( 'dblclick', _this.clickEvent );
						} );
	
						var $fieldQualifiers = $field.closest( 'tr' ).find( '[data-wikidata-qualifier-id]' );
						$fieldQualifiers.each( function () {
							_this.propertyIds.push( $( this ).data( 'wikidata-qualifier-id' ) );
						} );
					} );
					mw.util.addCSS( '\
						.infobox .no-wikidata {\
							display: block !important;\
							background: #fdc;\
							padding: 5px 0;\
						}\
						.infobox .no-wikidata.partial-wikidata {\
							background: #eeb;\
						}\
						.infobox .no-wikidata .no-wikidata {\
							margin: -5px 0;\
						}\
					' );
	
					// TODO: Do not load properties until the window is opened for the first time
					_this.loadProperties( _this.propertyIds );
				}
			} );
		};
		
		return this;
	};

	$.when(
		$.ready,
		mw.loader.using( [
			'mediawiki.api',
			'mediawiki.ForeignApi',
			'mediawiki.util',
			'oojs-ui-core',
			'oojs-ui-widgets',
			'oojs-ui-windows'
		] )
	)
	.done( wikidataInfoboxExport().init );
}( mediaWiki, jQuery ) );