User:RPI2026F1/PropertyCreator.js

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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>
/* jshint maxerr: 999 */

// Script to simplify creation of new properties.

// After creating, when marking proposal as done, for easy of copying
// I created this in part using a new script to simplify property creation, please let me know if something went wrong --~~~~

// notes:
// convert "instance of" to dropdown? Always use Q19847637? ("Wikidata property for an identifier")
// regex to convert examples from the page: `Example.*?\((.*?)\) → ` -> `$1|`
$(() => {
const PropertyCreator = {};
window.PropertyCreator = PropertyCreator;

PropertyCreator.config = {
	version: '1.2',
	author: 'DannyS712',
	createSummary: 'Creating new property',
	attribution: ' using [[User:DannyS712/PropertyCreator.js]]',
	// show a warning to users that the script is currently being worked on and
	// may not be stable
	currentDev: false,
	
	// CONFIG for api calls
	
	// "property proposal discussion", P3254 in prod, P94264 in test
	propertyProposalDiscussionProp: 'P3254',
	
	// "Wikidata property example", P1855 in prod, P48077 in test
	propertyExampleProp: 'P1855',
	
	// Expected completeness. New properties are almost always created
	// as "always incomplete" or "eventually complete"
	// "expected completeness", P2429 in prod, P95238 in test
	// "always incomplete", Q21873886 in prod, Q211837 in test
	// "eventually complete", Q21873974 in prod, Q213515 in test
	completenessProp: "P2429",
	completenessAlwaysIncomplete: "Q21873886",
	completenessEventuallyComplete: "Q21873974",
	
	// Stability of property values
	// "stability of property value", P2668 in prod
	// "never changes", Q23611288 in prod
	// "sometimes changes", Q24025284 in prod
	// "values can be added", Q23611840 in prod
	// "continuously changes", Q23611587 in prod
	stabilityProp: "P2668",
	stabilityNeverChanges: "Q23611288",
	stabilitySometimesChanges: "Q24025284",
	stabilityValuesCanBeAdded: "Q23611840",
	stabilityContinuouslyChanges: "Q23611587",
	
	// "subject item of this property", P1629 in prod, P94574 in test
	subjectItemProp: 'P1629',
	// Inverse, used to link back
	// "Wikidata property", P1687 in prod, P678 in test
	wikidataPropertyProp: 'P1687',
	
	// "formatter URL", P1630 in prod, P368 in test
	formatterURLProp: 'P1630',
	
	// "format as a regular expression", P1793 in prod, P51065 in test
	regexFormatProperty: 'P1793',
	
	// CONSTRAINTS
	// Overall property, "property constraint", P2302 in prod, P51064 in test
	constraintPropertyProp: 'P2302',
	// regex "format constraint", Q21502404 in prod, Q100086 in test
	regexFormatConstraint: 'Q21502404',
	
	// "Wikimedia import URL", P4656 in prod, P77057 in test
	wikimediaImportURLProp: 'P4656',
	
	// For now only works with main scope
	// "property scope constraint", Q53869507 in prod, Q187953 in test
	// qualifier for the constraint, "property scope", P5314 in prod, P84130 in test
	// scope: main value, "as main value", Q54828448 in prod, Q187967 in test
	propertyScopeConstraint: 'Q53869507',
	propertyScopeConstraintProp: 'P5314',
	propertyScopeAsMain: 'Q54828448',
	
	// For now only works with items
	// "allowed entity types constraint", Q52004125 in prod, Q187951 in test
	// "item of property constraint", P2305 in prod, P76946 in test
	// "Wikibase item", Q29934200 in prod, Q187962 in test
	// "class" (for classes in property constraints), P2308 in prod
	// "relation" (for classes in property constraints), P2309 in prod
	// currently only instance of is used:
	// "instance of" (for relations), Q21503252 in prod
	allowedEntityTypesConstraint: 'Q52004125',
	itemOfPropertyConstraint: 'P2305',
	entityTypeItem: 'Q29934200',
	classForPropertyConstraint: 'P2308',
	relationOfPropertyConstraint: 'P2309',
	instanceOfRelation: 'Q21503252',
	
	// "instance of", P31 in prod, P82 in test
	instanceOfProp: 'P31',
	
	// "distinct values constraint", Q21502410 in prod, ??? in test
	distinctValuesConstraint: 'Q21502410',
	
	// "single value constraint", Q19474404 in prod, ??? in test
	singleValueConstraint: 'Q19474404',
	
	// "source website for the property", P1896 in prod, P94562 in test
	sourceWebsiteProp: 'P1896',
	
	// "subject type constraint", Q21503250 in prod
	subjectTypeConstraint: 'Q21503250',
	
	// "language of work or name", P407 in prod
	languageOfWorkProp: 'P407'
};

PropertyCreator.config.attribution += (" (version " + PropertyCreator.config.version + ")");
PropertyCreator.config.createSummary += PropertyCreator.config.attribution;


PropertyCreator.checkDomain = function () {
	// Use different configuration on test.wikidata.org
	if ( mw.config.get( 'wgDBname') !== 'testwikidatawiki' ) {
		return;
	}
	var testWikidataConfig = {
		propertyProposalDiscussionProp: 'P94264',
		propertyExampleProp: 'P48077',
		completenessProp: "P95238",
		completenessAlwaysIncomplete: "Q211837",
		completenessEventuallyComplete: "Q213515",
		subjectItemProp: 'P94574',
		wikidataPropertyProp: 'P678',
		formatterURLProp: 'P368',
		regexFormatProperty: 'P51065',
		constraintPropertyProp: 'P51064',
		regexFormatConstraint: 'Q100086',
		wikimediaImportURLProp: 'P77057',
		propertyScopeConstraint: 'Q187953',
		propertyScopeConstraintProp: 'P84130',
		propertyScopeAsMain: 'Q187967',
		allowedEntityTypesConstraint: 'Q187951',
		itemOfPropertyConstraint: 'P76946',
		entityTypeItem: 'Q187962',
		instanceOfProp: 'P82',
		sourceWebsiteProp: 'P94562',
		stabilityProp: 'P97210',
		stabilityNeverChanges: 'Q227415',
		stabilitySometimesChanges: 'Q227416',
		stabilityValuesCanBeAdded: 'Q227417',
		stabilityContinuouslyChanges: 'Q227418',
		relationOfPropertyConstraint: 'P97211',
		instanceOfRelation: 'Q227419',
		distinctValuesConstraint: 'Q227420',
		singleValueConstraint: 'Q227421',
		subjectTypeConstraint: 'Q227422',
		languageOfWorkProp: 'P97212'

	};
	$.extend( PropertyCreator.config, testWikidataConfig );
	
	var e = PropertyCreator.elements;
	// Only for testing/development; label needs to be changed (eg add a number at the end)
	e.pLabel.setValue( 'ProperlyLabel' );
	e.pDescription.setValue( 'PropertyDescription' );
	e.instanceOf.setValue( 'Q213516|Q227413' );
	e.proposalDiscussion.setValue( 'https://www.wikidata.org/wiki/Wikidata:Property_proposal/Supreme_Court_docket_number' );
	e.subjectItem.setValue( 'Q34734' );
	e.subjectType.setValue( 'Q227413|Q227414' )
	e.sourceWebsite.setValue( 'https://www.wikidata.org/wiki/Property:P7063' );
	e.sourceWebsiteLang.setValue( 'Q227413' )
	e.examplesText.setValue( "Q199546|foo\nQ57403|bar" );
	e.formatterURL.setValue( 'https://caselaw.findlaw.com/search.html?search_type=docket&court=us-supreme-court&text=$1' );
	e.formatterURLLang.setValue( 'Q227413' );
	e.valueRegex.setValue( '\\d+' );
};

PropertyCreator.onErrHandler = function () {
	// Shared error handler
	alert( 'Something went wrong' );
	console.log( arguments );
};

PropertyCreator.init = function () {
	window.document.title = 'PropertyCreator script';
	PropertyCreator.createElements();
	PropertyCreator.checkDomain();
	PropertyCreator.maybeAddDevWarning();
	
	var preloadFieldSet = new OO.ui.FieldsetLayout( {
		label: 'Form preloader'
	} );
	var e = PropertyCreator.elements;
	preloadFieldSet.addItems( [
		e.proposalDiscussionPageLayout,
		e.preloadButtonLayout
	] );
	var preloadTitle = mw.util.getParamValue( 'forDiscussion' );
	if ( preloadTitle !== null ) {
		e.proposalDiscussionPage.setValue( preloadTitle );
	}

	var formFieldSet = new OO.ui.FieldsetLayout( { 
		label: 'Helper to easily create new properties'
	} );
	formFieldSet.addItems( [
		e.pLabelLayout,
		e.pDescriptionLayout,
		e.pDataTypesLayout,
		e.proposalDiscussionLayout,
		e.instanceOfLayout,
		e.allowedEntityTypeLayout,
		e.subjectItemLayout,
		e.subjectTypeLayout,
		e.sourceWebsiteLayout,
		e.sourceWebsiteLangLayout,
		e.propertyScopeLayout,
		e.stabilityLayout,
		e.expectedCompletenessLayout,
		e.examplesTextLayout,
		e.formatterURLLayout,
		e.formatterURLLangLayout,
		e.valueRegexLayout,
		e.distinctValuesConstraintLayout,
		e.distinctValuesConstraintRefLayout,
		e.singleValueConstraintLayout,
		e.singleValueConstraintRefLayout,
		e.submitButtonLayout
	] );
	
	$('#mw-content-text').empty().append(
		preloadFieldSet.$element,
		$( '<hr>' ),
		formFieldSet.$element
	);
	
	e.preloadButton.on(
		'click',
		PropertyCreator.onPreload
	);
	e.submitButton.on(
		'click',
		PropertyCreator.onSubmit
	);
};

PropertyCreator.maybeAddDevWarning = function () {
	if ( !PropertyCreator.config.currentDev ) {
		return;
	}
	OO.ui.alert(
		'PropertyCreator is currently undergoing active development and is not stable!'
	);
};

PropertyCreator.done = function ( propId ) {
	console.log( 'DONE' );
	console.log( 'Property created at: ' + propId );
};

PropertyCreator.onPreload = function () {
	console.log( 'Preloading!' );
	var e = PropertyCreator.elements;
	e.submitButton.setDisabled( true );
	var subpageTitle = e.proposalDiscussionPage.getValue();
	PropertyCreator.preloadFrom( subpageTitle ).then(
		function () {
			console.log( 'Preloaded!' );
			e.submitButton.setDisabled( false );
		},
		PropertyCreator.onErrHandler
	);
};

PropertyCreator.preloadFrom = function ( subpageTitle ) {
	var fullTitle = 'Wikidata:Property proposal/' + subpageTitle;
	return new Promise( function ( resolve ) {
		new mw.Api().postWithEditToken( {
			action: 'parse',
			page: fullTitle,
			prop: 'wikitext',
			format: 'json',
			formatversion: 2,
			assert: 'user',
			assertuser: mw.config.get( 'wgUserName' )
		} ).then(
			function ( response ) {
				console.log( response ); // Good result
				var wikitext = response.parse.wikitext;
				console.log( wikitext );
				var extracted = PropertyCreator.extractFromDiscussionPage( fullTitle, wikitext );
				console.log( extracted );
				PropertyCreator.applyPreloads( extracted );
				resolve();
			},
			PropertyCreator.onErrHandler
		);
	} );
};

PropertyCreator.extractFromDiscussionPage = function ( fullTitle, wikitext ) {
	var fields = {};
	fields.proposalDiscussion = 'https://www.wikidata.org/wiki/' + ( fullTitle.replaceAll( ' ', '_' ) );
	// strip comments
	wikitext = wikitext.replace( /<!--[\s\S]*?-->/g, '' );
	// strip motivation and discussion, everything after first ====
	wikitext = wikitext.slice( 0, wikitext.indexOf( '====' ) );

	// only keep English from TranslateThis templates
	wikitext = wikitext.replace( /{{TranslateThis[\s\S]*?\|\s*en\s*=\s*([\s\S]*?)(?:\|[\s\S]*?}}|}})/g, '$1' );
	console.log( wikitext );
	// Split - get the heading and the content
	var splits = wikitext.split( '===' );
	var sectionHeading = splits[ 1 ];
	wikitext = splits[ 2 ];
	console.log( sectionHeading, wikitext );
	fields.pLabel = sectionHeading.trim();
	wikitext = wikitext.replace( /\s*?{{Property proposal\s*/, '' );
	wikitext = wikitext.replace( /\n?}}\s*$/, '' );
	wikitext = wikitext.replace( /[ \t]+/g, ' ' );
	wikitext = wikitext.replace( /\n\n/g, '\n' );
	var params = wikitext.split( /(?:^|[^Q])\|/g );
	console.log( params );
	var rawFields = {};
	params.forEach(
		function ( paramLine ) {
			var match = paramLine.match( /\s*(\S[^=]+?)\s*=\s*(.*)/ );
			if ( match !== null ) {
				var matchValue = ( match[ 2 ] ).trim();
				matchValue = matchValue.replace( /{{\s*[qQ]\s*\|\s*[qQ]?(\d+)\s*}}/g, 'Q$1' );
				if ( matchValue !== '' ) {
					rawFields[ match[1] ] = matchValue;
				}
			}
		}
	);
	console.log( rawFields );
	fields[ 'pDescription' ] = rawFields[ 'description' ];
	fields.subjectItem = rawFields[ 'subject item' ];
	fields.subjectType = rawFields[ 'domain' ];
	fields.sourceWebsite = rawFields[ 'source' ];
	fields.formatterURL = rawFields[ 'formatter URL' ];
	fields.valueRegex = rawFields[ 'allowed values' ];
	fields.distinctValuesConstraint = rawFields[ 'distinct values constraint' ] === 'yes';
	fields.distinctValuesConstraintRef = rawFields[ 'distinct values constraint' ] === 'yes';
	fields.singleValueConstraint = rawFields[ 'single value constraint' ] === 'yes';
	fields.singleValueConstraintRef = rawFields[ 'single value constraint' ] === 'yes';
	// examples
	fields.examplesText = '';
	Object.keys( rawFields ).filter(
		function ( key ) { return key.startsWith( 'example' ); }
	)
		.forEach(
			function ( key ) {
				var valMatch = rawFields[ key ].match( /^(Q\d+)[^\[]+\[\S+\s+(.*?)\]/ );
				if ( valMatch !== null ) {
					console.log( valMatch );
					fields.examplesText += valMatch [ 1 ] + '|' + valMatch[ 2 ] + '\n';
				}
			}
		);
	fields.examplesText = fields.examplesText.trim();
	return fields;
};

PropertyCreator.applyPreloads = function ( fields ) {
	var e = PropertyCreator.elements;
	Object.keys( fields ).forEach(
		function ( key ) {
			if ( fields[ key ] !== undefined && e[ key ] !== undefined ) {
				if ( typeof ( fields[ key ] ) === 'string' ) {
					e[ key ].setValue( fields[ key ] );
				} else {
					// must be boolean
					e[ key ].setSelected( fields[ key ] );
				}
			}
		}
	);
};

PropertyCreator.onSubmit = function () {
	console.log( 'Submitted!' );
	
	var e = PropertyCreator.elements;
	e.submitButton.setDisabled( true );
	PropertyCreator.createProperty(
		e.pLabel.getValue(),
		e.pDescription.getValue(),
		e.pDataTypes.getValue()
	);
};

PropertyCreator.util = {};

PropertyCreator.util.uuidv4 = function () {
	// Generates a random UUID v4
	// Source: https://stackoverflow.com/a/2117523/12248328
	// *Should* be compatible with IE11
	return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
	  (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
	);
  }

PropertyCreator.util.makeSnak = function ( property, value ) {
	const snak = {
		"snaktype": "value",
		"property": property,
	}
	if (value.match(/^[QP][1-9]\d*$/i)){
		// This is an entity ID
		snak.datavalue = {
            "type": "wikibase-entityid",
            "value": {
                "id": value
            }
        }
	} else {
		snak.datavalue = {
			"type": "string",
			"value": value
		}
	}
	return snak;
}

PropertyCreator.util.serializeClaim = function ( claimInfo ) {
	// Serializes a claim for batch editing.
	const claim = {
		"type": "statement",
		"mainsnak": {
			"snaktype": "value",
		},
		"rank": "normal"
	}
	claim.mainsnak = PropertyCreator.util.makeSnak(claimInfo.claimProperty, claimInfo.claimValue);
	if (claimInfo.claimEntity){
		claim.id = claimInfo.claimEntity + "$" + PropertyCreator.util.uuidv4()
	}
	let qualifiers = [];
	if ( claimInfo.claimQualifier && claimInfo.claimQualifier !== false ){
		qualifiers = [claimInfo.claimQualifier]
	} else if (claimInfo.claimQualifiers && claimInfo.claimQualifiers !== false) {
		qualifiers = claimInfo.claimQualifiers;
	}
	if (qualifiers.length > 0){
		claim.qualifiers = {};
		claim["qualifiers-order"] = [];
		qualifiers.forEach((qualifier) => {
			if (!claim.qualifiers.hasOwnProperty(qualifier.prop)){
				claim.qualifiers[qualifier.prop] = [];
			}
			if (!claim["qualifiers-order"].includes(qualifier.prop)){
				claim["qualifiers-order"].push(qualifier.prop);
			}
			claim.qualifiers[qualifier.prop].push(PropertyCreator.util.makeSnak(qualifier.prop, qualifier.value));
		})
	}
	if (claimInfo.addRef && PropertyCreator.elements.proposalDiscussion.getValue()){
		const refProp = PropertyCreator.config.wikimediaImportURLProp
		claim.references = [{
			"snaks": {
				
			},
			"snaks-order": [refProp]
		}];
		claim.references[0].snaks[refProp] = [PropertyCreator.util.makeSnak(refProp, PropertyCreator.elements.proposalDiscussion.getValue())]
	}
	return claim
}
  

PropertyCreator.util.addClaim = function ( claimInfo ) {
	// claimInfo is an object with `claimEntity`, `claimProperty`, and `claimValue` all set
	// as well as `addRef` for whether the discussion should be reference
	// claimQualifier can be false, or an object with a `prop` and `value`,
	// or claimQualifiers can be an array of those objects
	console.log( 'Converting claim value to snak, started as ' + claimInfo.claimValue );
	console.log(
		'Adding claim to ' + claimInfo.claimEntity +
		': property ' + claimInfo.claimProperty +
		' has value ' + claimInfo.claimValue
	);
	return new Promise( function ( resolve ) {
		new mw.Api().postWithEditToken( {
			action: 'wbsetclaim',
			claim: JSON.stringify(PropertyCreator.util.serializeClaim(claimInfo)),
			assert: 'user',
			assertuser: mw.config.get( 'wgUserName' )
		} ).then(resolve).catch(PropertyCreator.onErrHandler);
	} );
};

PropertyCreator.createProperty = function ( labelValue, descriptionValue, dataType ) {
	// Instance of data

	var propertyData = {
		'labels': { 'en': { 'language': 'en', 'value': labelValue } },
		'descriptions': { 'en': { 'language': 'en', 'value': descriptionValue } },
		'datatype': dataType,
		'claims': [
			...PropertyCreator.getInstanceOfData(),
			PropertyCreator.documentSubjectItem(),
			PropertyCreator.documentDiscussion(),
			PropertyCreator.documentSourceWebsite(),
			PropertyCreator.documentExpectedCompleteness(),
			PropertyCreator.documentStability(),
			PropertyCreator.documentFormatterURL(),
			PropertyCreator.addRegex(),
			PropertyCreator.addRegexConstraint(),
			PropertyCreator.addScopeConstraint(),
			PropertyCreator.addEntityTypeConstraint(),
			PropertyCreator.maybeAddDistinctValuesConstraint(),
			PropertyCreator.maybeAddSingleValueConstraint(),
			PropertyCreator.maybeAddSubjectTypeConstraint()
		].filter(item => item !== null)
	};
	var propertyDataStr = JSON.stringify( propertyData );
	console.log( propertyData, propertyDataStr );
	
	new mw.Api().postWithEditToken( {
		'action': 'wbeditentity',
		'new': 'property',
		'summary': PropertyCreator.config.createSummary,
		'data': propertyDataStr,
		'assert': 'user',
		'assertuser': mw.config.get( 'wgUserName' )
	} ).then(
		function ( response ) {
			console.log( response ); // Good result
			var propertyId = response.entity.id;
			
			// No need to wait for this to be done
			PropertyCreator.createPropertyTalk( propertyId );
			PropertyCreator.addPropertyExamplesInit( propertyId )
			
		},
		PropertyCreator.onErrHandler
	);
};

PropertyCreator.createPropertyTalk = function ( propId ) {
	new mw.Api().postWithEditToken( {
		action: 'edit',
		title: 'Property talk:' + propId,
		text: '{{Property documentation}}',
		summary: 'Create with {{Property documentation}}' + PropertyCreator.config.attribution,
		assert: 'user',
		assertuser: mw.config.get( 'wgUserName' )
	} ).then(
		function ( response ) {
			console.log( response ); // Good result
		},
		PropertyCreator.onErrHandler
	);
};

PropertyCreator.getInstanceOfData = function () {
	var instanceOfValues = PropertyCreator.elements.instanceOf.getValue().split("|");
	return instanceOfValues.map( instanceOfValue => PropertyCreator.util.serializeClaim({
		claimProperty: PropertyCreator.config.instanceOfProp,
		claimValue: instanceOfValue,
		addRef: true
	}))
};

PropertyCreator.addPropertyExamplesInit = function ( propId ) {
	console.log( 'Adding property examples for new property with id ' + propId );
	var examplesStr = PropertyCreator.elements.examplesText.getValue();
	var examples = examplesStr.split("\n");
	PropertyCreator.addPropertyExamplesRecursive( propId, examples );
};

PropertyCreator.addPropertyExamplesRecursive = function ( propertyId, examples ) {
	var currentExample = examples.pop();
	var exampleParams = currentExample.split('|');
	var exampleItem = exampleParams[0];
	var exampleValue = exampleParams[1];
	PropertyCreator.addPropertyExample( exampleItem, propertyId, exampleValue ).then( function () {
		PropertyCreator.documentPropertyExample( exampleItem, propertyId, exampleValue ).then( function () {
			if ( examples.length !== 0 ) {
				PropertyCreator.addPropertyExamplesRecursive( propertyId, examples );
			} else {
				// Continue to the next step
				PropertyCreator.documentSubjectProperty( propertyId )
			}
		} );
	} );
};

PropertyCreator.addPropertyExample = function ( item, propId, value ) {
	console.log( 'Setting examples: item ' + item + ' has property value ' + value );
	// THIS ONLY WORKS FOR EXTERNAL LINKS FOR NOW (probably)
	return PropertyCreator.util.addClaim( {
		claimEntity: item,
		claimProperty: propId,
		claimValue: value,
		addRef: true
	} );
};
PropertyCreator.documentPropertyExample = function ( item, propId, value ) {
	console.log( 'Document examples: item ' + item + ' has property value ' + value );
	// THIS ONLY WORKS FOR EXTERNAL LINKS FOR NOW (probably)
	return PropertyCreator.util.addClaim( {
		claimEntity: propId,
		claimProperty: PropertyCreator.config.propertyExampleProp,
		claimValue: item,
		claimQualifier: {
			prop: propId,
			value: value
		},
		addRef: true
	} );
};

PropertyCreator.documentSubjectItem = function ( ) {
	// Link property to subject item
	subjectItemValue = PropertyCreator.elements.subjectItem.getValue();
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.subjectItemProp,
		claimValue: subjectItemValue,
		addRef: true
	} )
};

PropertyCreator.documentSubjectProperty = function ( propId ) {
	// Link subject item back to property
	subjectItemValue = PropertyCreator.elements.subjectItem.getValue();
	PropertyCreator.util.addClaim( {
		claimEntity: subjectItemValue,
		claimProperty: PropertyCreator.config.wikidataPropertyProp,
		claimValue: propId,
		addRef: true
	} ).then(
		(propId) => PropertyCreator.done(propId),
		PropertyCreator.onErrHandler
	);
};

PropertyCreator.documentDiscussion = function () {
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.propertyProposalDiscussionProp,
		claimValue: PropertyCreator.elements.proposalDiscussion.getValue(),
		addRef: false
	} )
};

PropertyCreator.documentSourceWebsite = function ( ) {
	var sourceWebsite = PropertyCreator.elements.sourceWebsite.getValue();
	if ( sourceWebsite === '' ) {
		// No source website set, move on
		console.log( 'Skipping source website handling, no source website set set' );
		return null;
	}
	var sourceWebsiteLang = PropertyCreator.elements.sourceWebsiteLang.getValue();
	var langQualifier = {
		prop: PropertyCreator.config.languageOfWorkProp,
		value: sourceWebsiteLang
	};
	// empty value = omit qualifier, util.addClaim() ignores false value
	if ( sourceWebsiteLang === "" ) {
		langQualifier = false;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.sourceWebsiteProp,
		claimValue: sourceWebsite,
		claimQualifier: langQualifier,
		addRef: true
	} )
};

PropertyCreator.documentExpectedCompleteness = function () {
	if ( PropertyCreator.elements.expectedCompleteness.getValue() === 'unknown' ) {
		console.log( 'Skipping expected completeness, not listed' );
		return null;
	}
	var expectedCompletenessValue;
	if ( PropertyCreator.elements.expectedCompleteness.getValue() === 'alwaysIncomplete' ) {
		expectedCompletenessValue = PropertyCreator.config.completenessAlwaysIncomplete;
	} else {
		expectedCompletenessValue = PropertyCreator.config.completenessEventuallyComplete;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.completenessProp,
		claimValue: expectedCompletenessValue,
		addRef: true
	} )
};

PropertyCreator.getStabilityValue = function ( input ) {
	switch ( input ) {
		case 'neverChanges':
			return PropertyCreator.config.stabilityNeverChanges;
		case 'sometimesChanges':
			return PropertyCreator.config.stabilitySometimesChanges;
		case 'valuesCanBeAdded':
			return PropertyCreator.config.stabilityValuesCanBeAdded;
		case 'continuouslyChanges':
			return PropertyCreator.config.stabilityContinuouslyChanges;
		case 'unknown':
			return false;
		default:
			console.log( 'Invalid expected stability: ' + input );
			return false;
	}
};
PropertyCreator.documentStability = function ( ) {
	var stabilityValue = PropertyCreator.getStabilityValue(
		PropertyCreator.elements.stability.getValue()
	);
	if ( stabilityValue === false ) {
		console.log( 'Skipping value stability, not listed' );
		return null;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.stabilityProp,
		claimValue: stabilityValue,
		addRef: true
	} )
};

PropertyCreator.documentFormatterURL = function ( ) {
	var formatterURL = PropertyCreator.elements.formatterURL.getValue();
	var formatterURLLang = PropertyCreator.elements.formatterURLLang.getValue();
	var langQualifier = {
		prop: PropertyCreator.config.languageOfWorkProp,
		value: formatterURLLang
	};
	// empty value = omit qualifier, util.addClaim() ignores false value
	if ( formatterURLLang === "" ) {
		langQualifier = false;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.formatterURLProp,
		claimValue: formatterURL,
		claimQualifier: langQualifier,
		addRef: true
	} )
};

PropertyCreator.addRegex = function ( ) {
	// Adds "format as a regular expression" and calls addRegexConstraint
	var propertyRegex = PropertyCreator.elements.valueRegex.getValue();
	if ( propertyRegex === '' ) {
		// No regex set, move on
		console.log( 'Skipping regex handling, no regex set' );
		return null;
	}
	console.log( 'Adding regex: ' + propertyRegex ); 
	
	return PropertyCreator.util.serializeClaim( {
		claimProperty: PropertyCreator.config.regexFormatProperty,
		claimValue: propertyRegex,
		addRef: true
	} )
};

PropertyCreator.addRegexConstraint = function ( ) {
	var propertyRegex = PropertyCreator.elements.valueRegex.getValue();
	if ( propertyRegex === '' ) {
		// No regex set, move on
		return null;
	}
	var cfg = PropertyCreator.config;
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.regexFormatConstraint,
		claimQualifier: {
			prop: cfg.regexFormatProperty,
			value: propertyRegex
		},
		addRef: true
	} )
};

PropertyCreator.addScopeConstraint = function (  ) {
	var cfg = PropertyCreator.config;
	var scopeConstraintValue;
	if ( PropertyCreator.elements.propertyScope.getValue() === 'asMain' ) {
		// Only option for now
		scopeConstraintValue = cfg.propertyScopeAsMain;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.propertyScopeConstraint,
		claimQualifier: {
			prop: cfg.propertyScopeConstraintProp,
			value: scopeConstraintValue
		},
		addRef: true
	} )
};

PropertyCreator.addEntityTypeConstraint = function ( ) {
	var cfg = PropertyCreator.config;
	var typeConstraintValue;
	if ( PropertyCreator.elements.allowedEntityType.getValue() === 'typeItem' ) {
		// Only option for now
		typeConstraintValue = cfg.entityTypeItem;
	}
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.allowedEntityTypesConstraint,
		claimQualifier: {
			prop: cfg.itemOfPropertyConstraint,
			value: typeConstraintValue
		},
		addRef: true
	} )
};

PropertyCreator.maybeAddDistinctValuesConstraint = function ( ) {
	if ( PropertyCreator.elements.distinctValuesConstraint.isSelected() === false ) {
		// Skip
		return null;
	}
	var cfg = PropertyCreator.config;
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.distinctValuesConstraint,
		addRef: PropertyCreator.elements.distinctValuesConstraintRef.isSelected()
	} )
};

PropertyCreator.maybeAddSingleValueConstraint = function ( ) {
	if ( PropertyCreator.elements.singleValueConstraint.isSelected() === false ) {
		// Skip
		return null;
	}
	var cfg = PropertyCreator.config;
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.singleValueConstraint,
		addRef: PropertyCreator.elements.singleValueConstraintRef.isSelected()
	} )
	// TODO "domain" / "value type constraint"
};

PropertyCreator.maybeAddSubjectTypeConstraint = function ( ) {
	var subjectTypeString = PropertyCreator.elements.subjectType.getValue();
	if ( subjectTypeString === '' ) {
		// No subject type set, move on
		console.log( 'Skipping subject type handling, no subject set' );
		return null;
	}
	var subjectTypes = subjectTypeString.split("|");
	
	var cfg = PropertyCreator.config;
	return PropertyCreator.util.serializeClaim( {
		claimProperty: cfg.constraintPropertyProp,
		claimValue: cfg.subjectTypeConstraint,
		claimQualifiers: [
			...subjectTypes.map((subjectType) => { return { prop: cfg.classForPropertyConstraint, value: subjectType } }),
			// instance of:
			{ prop: cfg.relationOfPropertyConstraint, value: cfg.instanceOfRelation }
		],
		addRef: true
	} );
};

PropertyCreator.addDiscussionReference = function ( claimNeedingReference ) {
	console.log( 'Documenting the discussion as the reference for a claim' );
	var reference = {
		referenceProp: [ {
			'snaktype': 'value',
			'property': PropertyCreator.config.wikimediaImportURLProp,
			'datavalue': {
				'type': 'string',
				'value': PropertyCreator.elements.proposalDiscussion.getValue()
			}
		} ]
	};
	var referenceStr = JSON.stringify( reference );
	console.log( 'Documenting the discussion as a reference for the claim', claimNeedingReference, reference, referenceStr );
	return new mw.Api().postWithEditToken( {
		action: 'wbsetreference',
		statement: claimNeedingReference,
		snaks: referenceStr,
		assert: 'user',
		assertuser: mw.config.get( 'wgUserName' )
	} );
};

PropertyCreator.elements = {};
PropertyCreator.createElements = function () {
	var e = {};
	
	// WIP shortcut: loading from the wikitext of a proposal discussion
	e.proposalDiscussionPage = new OO.ui.TextInputWidget();
	e.proposalDiscussionPageLayout = new OO.ui.FieldLayout(
		e.proposalDiscussionPage,
		{ label: 'Proposal discussion page (just the subpage title)' }
	);
	e.preloadButton = new OO.ui.ButtonInputWidget( { 
		label: 'Preload',
		flags: [
			'primary',
			'progressive'
		]
	} );
	e.preloadButtonLayout = new OO.ui.FieldLayout( e.preloadButton );
	
	e.pLabel = new OO.ui.TextInputWidget();
	e.pLabelLayout = new OO.ui.FieldLayout( e.pLabel, { label: 'Property label' } );
	e.pDescription = new OO.ui.TextInputWidget();
	e.pDescriptionLayout = new OO.ui.FieldLayout( e.pDescription, { label: 'Property description' } );
	
	var validDataTypes = [ 'external-id' ];
	var dataTypeOptions = validDataTypes.map(
		function ( type ) {
			return { data: type, label: type };
		}
	);
	e.pDataTypes = new OO.ui.DropdownInputWidget( { options: dataTypeOptions } );
	e.pDataTypesLayout = new OO.ui.FieldLayout( e.pDataTypes, { label: 'Data type' } );
	
	e.proposalDiscussion = new OO.ui.TextInputWidget();
	e.proposalDiscussionLayout = new OO.ui.FieldLayout( e.proposalDiscussion, { label: 'Proposal discussion' } );
	
	e.instanceOf = new OO.ui.TextInputWidget();
	e.instanceOfLayout = new OO.ui.FieldLayout( e.instanceOf, { label: 'Instance of (Q19847637 = Wikidata property for an identifier)' } );
	
	e.allowedEntityType = new OO.ui.RadioSelectInputWidget( {
		options: [
			{ data: 'typeItem', label: 'Wikibase item' }
		]
	} );
	e.allowedEntityTypeLayout = new OO.ui.FieldLayout( e.allowedEntityType, { label: 'Allowed entity type' } );
	
	e.subjectItem = new OO.ui.TextInputWidget();
	e.subjectItemLayout = new OO.ui.FieldLayout( e.subjectItem, { label: 'Subject item for this property' } );
	
	e.subjectType = new OO.ui.TextInputWidget();
	e.subjectTypeLayout = new OO.ui.FieldLayout( e.subjectType, { label: 'Subject type (domain) of this property' } );
	
	e.sourceWebsite = new OO.ui.TextInputWidget();
	e.sourceWebsiteLayout = new OO.ui.FieldLayout( e.sourceWebsite, { label: 'Source website for this property' } );
	
	e.sourceWebsiteLang = new OO.ui.TextInputWidget();
	e.sourceWebsiteLangLayout = new OO.ui.FieldLayout( e.sourceWebsiteLang, { label: 'Source website language (Q1860 = English, omit = skip)' } );
	
	e.propertyScope = new OO.ui.RadioSelectInputWidget( {
		options: [
			{ data: 'asMain', label: 'As main value' }
		]
	} );
	e.propertyScopeLayout = new OO.ui.FieldLayout( e.propertyScope, { label: 'Property scope' } );
	
	e.expectedCompleteness = new OO.ui.RadioSelectInputWidget( {
		options: [
			{ data: 'alwaysIncomplete', label: 'Always incomplete' },
        	{ data: 'eventuallyComplete', label: 'Eventually complete' },
        	{ data: 'unknown', label: 'Unknown (skip)' }
		]
	} );
	e.expectedCompletenessLayout = new OO.ui.FieldLayout( e.expectedCompleteness, { label: 'Expected completeness' } );

	e.stability = new OO.ui.RadioSelectInputWidget( {
		options: [
			{ data: 'neverChanges', label: 'Never changes' },
        	{ data: 'sometimesChanges', label: 'Sometimes changes' },
        	{ data: 'valuesCanBeAdded', label: 'Values can be added' },
        	{ data: 'continuouslyChanges', label: 'Continuously changes' },
        	{ data: 'unknown', label: 'Unknown (skip)' }
		]
	} );
	// default to unknown
	e.stability.setValue( 'unknown' );
	e.stabilityLayout = new OO.ui.FieldLayout( e.stability, { label: 'Stability of property values' } );

	e.examplesText = new OO.ui.MultilineTextInputWidget( { rows: 7 } );
	e.examplesTextLayout = new OO.ui.FieldLayout(
		e.examplesText,
		{ label: 'Examples (one per line, separate item and value with |)' }
	);
	
	e.formatterURL = new OO.ui.TextInputWidget();
	e.formatterURLLayout = new OO.ui.FieldLayout( e.formatterURL, { label: 'Formatter URL' } );
	
	e.formatterURLLang = new OO.ui.TextInputWidget();
	e.formatterURLLangLayout = new OO.ui.FieldLayout( e.formatterURLLang, { label: 'Formatter URL language (Q1860 = English, omit = skip)' } );
	
	e.valueRegex = new OO.ui.TextInputWidget();
	e.valueRegexLayout = new OO.ui.FieldLayout( e.valueRegex, { label: 'Regex for values' } );
	
	e.distinctValuesConstraint = new OO.ui.CheckboxInputWidget();
	e.distinctValuesConstraintLayout = new OO.ui.FieldLayout( e.distinctValuesConstraint, { label: 'Distinct values constraint' } );
	
	e.distinctValuesConstraintRef = new OO.ui.CheckboxInputWidget();
	e.distinctValuesConstraintRefLayout = new OO.ui.FieldLayout( e.distinctValuesConstraintRef, { label: 'Distinct values constraint - reference' } );
	
	e.singleValueConstraint = new OO.ui.CheckboxInputWidget();
	e.singleValueConstraintLayout = new OO.ui.FieldLayout( e.singleValueConstraint, { label: 'Single value constraint' } );
	
	e.singleValueConstraintRef = new OO.ui.CheckboxInputWidget();
	e.singleValueConstraintRefLayout = new OO.ui.FieldLayout( e.singleValueConstraintRef, { label: 'Single value constraint - reference' } );
	
	e.submitButton = new OO.ui.ButtonInputWidget( { 
		label: 'Create',
		flags: [
			'primary',
			'progressive'
		]
	} );
	e.submitButtonLayout = new OO.ui.FieldLayout( e.submitButton );
	
	PropertyCreator.elements = e;
};

});

mw.loader.using(
	[ 'mediawiki.util', 'mediawiki.api', 'oojs-ui-widgets', 'oojs-ui-windows' ],
	function () {
		$( document ).ready( () => {
			if (
				mw.config.get( 'wgNamespaceNumber' ) === -1 &&
				mw.config.get( 'wgTitle' ) === 'BlankPage/PropertyCreator'
			) {
				window.PropertyCreator.init();
			} else {
				var urlParams = '';
				if ( mw.config.get( 'wgNamespaceNumber' ) === 4 &&
					mw.config.get( 'wgTitle').startsWith( 'Property proposal/' )
				) {
					urlParams += '?forDiscussion=';
					urlParams += mw.util.rawurlencode(
						mw.config.get( 'wgTitle').replace( 'Property proposal/', '' )
					);
				}
				mw.util.addPortletLink(
					'p-tb',
					'/wiki/Special:BlankPage/PropertyCreator' + urlParams,
					'PropertyCreator helper form'
				);
			}
		} );
	}
);
// </nowiki>