User:Mikevoir/SourceTemplateData.js

(function {   'use strict';

// Double-load protection. if (window.dev && window.dev.sourceTemplateData && window.dev.sourceTemplateData.__LOADED) { console.log('SourceTemplateData.js has attempted to load more than once!'); return; }   window.dev = window.dev || {}; window.dev.sourceTemplateData = window.dev.sourceTemplateData || {};

// Load dependencies importArticles({       type: 'script',        articles: [            'u:dev:MediaWiki:I18n-js/code.js',            'u:dev:MediaWiki:Modal.js'        ]    }); importArticle({       type: 'style',        article: 'u:community:User:Mikevoir/SourceTemplateData.css' // CSS IF PORTED TO DEV: 'u:dev:MediaWiki:SourceTemplateData.css'    }); var config = mw.config.get(['wgAction']); var api, textbox, popup, newParam;

// Main object var sourceTemplateData = { // Blank variables to store cached data templateData: null, templateCall: null, paramOrder: [],

// Initialize the script init: function { api = new mw.Api; textbox = $('#wpTextbox1'); window.dev.sourceTemplateData.__LOADED = true; // Script start event // Run on: ctrl + alt document.addEventListener('keydown', (function(event){ var keyCombo = window.dev.sourceTemplateData.activate || ['ctrl', 'alt']; var attempt = true; keyCombo.forEach(function(key){					if (['shift', 'ctrl', 'alt'].includes(key)) {						if (!event[key+'Key']){attempt = false;}					} else if (event.key !== key) {attempt = false;}				}); if (attempt && !document.querySelector('.td-popup') && document.querySelector('.CodeMirror-focused')){ event.preventDefault; // Avoid any possible unwanted input by disabling default this.validateSelection.bind(sourceTemplateData)(event); // Attempt to validate }			}).bind(this)); // Param filter event document.addEventListener('change', function(event) {				if (event.target.id && (					event.target.id == 'td-popup-template-filter' ||					event.target.id == 'template-filter-notincall' ||					event.target.id.indexOf('param-toggle-') == 0				)){					var inputFilter = document.querySelector('#td-popup-template-filter').value;					var existFilter = document.querySelector('#template-filter-notincall').checked;					document.querySelectorAll('.td-popup-param .td-popup-param-toggle-button').forEach(function(el) { var filterOut = 0; var param = el.id.replace(/^param-toggle-/, ''); var paramBlock = el.parentElement; // Input filter as main filter if (inputFilter.length>0) { if (param.indexOf(inputFilter) == 0) { if (existFilter && el.checked) {filterOut--;} else if (!existFilter) {filterOut--;} }					   }					    else if (existFilter && el.checked) {filterOut--;} else if (!existFilter && inputFilter.length==0) {filterOut--;} if (filterOut==0) { paramBlock.classList.add('td-popup-param-filterout'); } else { paramBlock.classList.remove('td-popup-param-filterout'); } });				} else if (event.target.classList.contains('td-popup-param-value')) {					var param = event.target.id.replace(/^ParamVal/, );					sourceTemplateData.templateCall.params[param] = sourceTemplateData.templateCall.params[param] || {name: param, value: };					sourceTemplateData.templateCall.params[param].value = event.target.value.trim || '';				}			}); },

// Check if caret is at start of template to run code validateSelection: function(event) { if (textbox.textSelection('getSelection').length>0 || window.dev.sourceTemplateData.running) {return;} var checkTemplate = /^[\s\S]*)?$/; // check for template start with name			var uptoCaret = textbox.textSelection('getContents').substring(0, textbox.textSelection('getCaretPosition'));			var FROM;			var template;			if (uptoCaret && checkTemplate.test(uptoCaret)) {				var matches = checkTemplate.exec(uptoCaret);				template = matches[1].trim;				FROM = textbox.textSelection('getCaretPosition') - ( ( matches[1] + (matches[2]||'') ).length + 2);				if (FROM<0) {FROM=0;}			}			if (FROM !== null && template) {				var render = this.getEncased({ str: textbox.textSelection('getContents').substring(FROM), startC: '{', endC: '}' });				this.templateData = null;				this.templateCall = null;				this.paramOrder = [];				sourceTemplateData.parseTemplate.bind(sourceTemplateData)(render, FROM);			}		},

// Get template's templatedata getTD: function(template) { // API response handler var apiResult = function(data) { var id = Object.keys(data.pages)[0]; var Tdata = data.pages[id]; window.dev.sourceTemplateData.running = false; if (id == '-1'){ alert('"Template:' + template + '" does not exist.'); sourceTemplateData.render({title:template}); } else if (Tdata.notemplatedata) { alert('"Template:' + template + '" does not have templatedata.'); sourceTemplateData.render({title:template}); } else { if (!Tdata.paramOrder && Tdata.params) { Tdata.paramOrder = []; Object.keys(Tdata.params).forEach(function(key) {							Tdata.paramOrder.push(key);						}); }					if (Tdata.paramOrder && Tdata.paramOrder.length>0) {sourceTemplateData.paramOrder = Tdata.paramOrder.concat(sourceTemplateData.paramOrder);} if (!Tdata.format) {Tdata.format='inline';} sourceTemplateData.render(Tdata); }			};

// API call api.get({				action: 'templatedata',				titles: 'Template:' + template,				includeMissingTitles: 1,				format: 'json'			}).then(apiResult);

},

// Render popup with templatadata and current template call's data render: function(data) { sourceTemplateData.templateData = data; if (popup) { popup.setTitle(data.title.replace(/^Template:\s*/, 'Template: ')); popup.setContent(					' '+						' '+(data.description ? data.description.en : 'No template description available.')+' '+						' '+							' Format: '+(data.format ? data.format.replace('\n', '\\n') : 'inline')+' '+							''+							'Apply format '+						' '+						' '+							'Filter params: '+							''+						' '+						' '+							''+							'Toggle parameters not in call '+						' '+					' '+					' '+						sourceTemplateData.parseParams+					' '				); popup.show; } else { mw.hook('dev.modal').add(function(Modal) {					popup = 					new Modal.Modal({ title: data.title.replace(/^Template:\s*/, 'Template: '), id: 'std-popup', size: 'large', buttons: [ {								text:'New Parameter', title:'New Parameter', id:'std-NewParameter', event: 'NewParameter' },							{								text:'Apply Changes', title:'Apply Changes', id:'std-ApplyChanges', primary: true, event: 'ApplyChanges' }						],						events: { ApplyChanges: function { sourceTemplateData.applyChanges.bind(sourceTemplateData); },							NewParameter: function { var content = ' '+											'Name: '+ ''+ ' '+										' '+											' Value: '+ ' '+ ' ';								if (newParam) { newParam.setContent(content); popup.hide; newParam.show; } else{ newParam = new Modal.Modal({										title: 'Add New Parameter',										id: 'std-popup-newparam',										content: content,										buttons: [											{												text:'Add Param',												title:'Add Param',												id: 'newParam-add',												event: 'addParam'											}										],										events: {											addParam: function {												sourceTemplateData.addParam;											}										},										close: function {											newParam.hide;											popup.show;										}									}); newParam.create; popup.hide; newParam.show; }							}						},						content: ' '+   						' '+(data.description ? data.description.en : 'No template description available.')+' '+ ' '+       						' Format: '+(data.format ? data.format.replace('\n', '\\n') : 'inline')+' '+ ''+ 'Apply format '+ ' '+   						' '+    							'Filter params: '+ ''+ ' '+   						' '+    							''+ 'Toggle parameters not in call '+ ' '+						' '+						' '+							sourceTemplateData.parseParams+ ' '					});					popup.create;					popup.show;				}); }		},		addElement: function(options) { if (!options.node){options.node = 'div';} var newNode; if (options.node !== 'text') { newNode = document.createElement(options.node); if (options.classNames) { options.classNames.forEach(function(className){						if ('string' == typeof className && className.length > 0) {							newNode.classList.add(className);						}					}); }				if (options.styles) { Object.keys(options.styles).forEach(function(key) {						newNode.style[key] = options.styles[key];					}); }				if (options.attributes) { Object.keys(options.attributes).forEach(function(key) {						newNode.setAttribute(key, options.attributes[key]);					}); }				if (options.content) { if (Array.isArray(options.content) && options.content.length>0) { options.content.forEach(function(content){							if ('string' == typeof content) {								newNode.append(document.createTextNode(content));							}else if ('object' == typeof content && content.ownerDocument) {								newNode.append(content);							}						}); } else if ('string' == typeof options.content || (options.content) instanceof String) { newNode.append(document.createTextNode(options.content)); }else if ('object' == typeof options.content && options.content.ownerDocument) { newNode.append(options.content); }				}			} else { newNode = document.createTextNode(options.content); }

if (options.addTo) { options.addTo.append(newNode); }			if (!options.noreturn) { return newNode; }		},		nestElements: function(startNode, optionsArray) { var oldNode = startNode; optionsArray.forEach(function(options){				options.addTo = oldNode;				oldNode = sourceTemplateData.addElement(options);			}); return oldNode; },		listElements: function(parentNode, optionsArray) { var newNodes = []; optionsArray.forEach(function(options){				if (parentNode) {options.addTo = parentNode;} // Append to parent node if given				var newNode = sourceTemplateData.addElement(options);				if (!parentNode && newNode) {newNodes.push(newNode);} // Include to return list if no parent node			}); return parentNode || newNodes; },		parseParams: function(_params) { var ret = ''; var built = {}; var order = _params; if (!order) {order = sourceTemplateData.paramOrder;} else if ('string' == typeof order) {order = [order];} order.forEach(function(param){				if (built[param]) {return;}				var checkbox_attr = {					type: 'checkbox',					id: 'param-toggle-'+param				};				if (sourceTemplateData.templateCall.params[param]) {checkbox_attr.checked = 'true';}				var Tdata = sourceTemplateData.templateData.params && sourceTemplateData.templateData.params[param] || {};				var all = [					{						node: 'input',						classNames: ['td-popup-checkbox', 'td-popup-param-toggle-button'],						attributes: checkbox_attr					},					{						content: Tdata.label ? Tdata.label.en : param.toString,						node: 'label',						classNames: ['td-popup-checkbox', 'td-popup-param-toggle-button-label', 'td-popup-param-label'],						attributes: {type: 'checkbox', 'for': 'param-toggle-'+param}					}				];				var param_names = [{ content: param, node: 'code', classNames: ['td-popup-param-list-item'] }];				var param_desc  = [];				var hr = false; if (Tdata.aliases && Tdata.aliases.length>0) { Tdata.aliases.forEach(function(alias){						param_names.push({ content: alias, node: 'code', classNames: ['td-popup-param-list-item'] });					});				}				all.push({ classNames: [ 'td-popup-param-list' ], content: sourceTemplateData.listElements(null,param_names) }); if (Tdata.description && Tdata.description.en) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({						content: Tdata.description.en,						node: 'p'					}); }				if (Tdata.default && Tdata.default.en) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({						content: 'Default: ' + Tdata.default.en,						classNames: ['vetd-popup-param-default'],						node: 'p'					});} if (Tdata.example && Tdata.example.en) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({						content: 'Example: ' + Tdata.example.en,						classNames: ['td-popup-param-example'],						node: 'p'					}); }				all.push({ classNames: [ 'td-popup-param-description' ], content: sourceTemplateData.listElements(null,param_desc) }); all.push({					classNames: [ 'td-popup-param-value' ],					node: 'textarea',					content: sourceTemplateData.templateCall.params[param] && sourceTemplateData.templateCall.params[param].value || '',					attributes: {id: 'ParamVal'+param}				}); ret = ret + sourceTemplateData.addElement({								classNames: [ 'td-popup-param' ],								attributes: {id: param},								content: sourceTemplateData.listElements(null, all)							}).outerHTML; built[param] = true; });			return ret;		},		parseTemplate: function(str, FROM) {			var order = 0;			var template;			var tempParam = {				value: '',				start: 0			};			var S = {				braceL: '{',				braceR: '}',				bracketL: '[',				bracketR: ']',				pipe: '|',				tag: '<',				excl: '!',			};

// Start parsing var i = 0; while (i < str.length) { var char = str.charAt(i); var Nchar = str.charAt(i+1); if (!template) { if (char == S.braceL && Nchar == S.braceL) { template = { name: null, params: null, paramOrder: [], unnamedCount: 0 };						i++; i++; }				} else { // Get template's name if (!template.name) { if (![S.braceR, S.braceL, S.pipe].includes(char) && ![S.braceR, S.braceL, S.pipe].includes(Nchar)) { tempParam.value = tempParam.value + char; i++; } else if (![S.braceR, S.braceL, S.pipe].includes(char) && [S.braceR, S.braceL, S.pipe].includes(Nchar)) { template.name = (tempParam.value + char).trim; if (!template.name || template.name.length == 0) {return null;} // Null return if template name is blank tempParam.value = ''; i++; }

// Get template's parameters if any and finalize } else {

// End template if (char == S.braceR && Nchar == S.braceR) { closeParam(i); template.FROM = FROM; template.STR = str; template._echo = JSON.parse(JSON.stringify(template)); sourceTemplateData.templateCall = template; sourceTemplateData.getTD(template.name); break;

// Template param start } else if (char == S.pipe) { tempParam.start = i;							closeParam(i); i++;

// Default check } else { var nest = handleNest(str.substring(i)); if (nest && nest.length>0) { tempParam.value = tempParam.value + nest; i = i + nest.length; } else { tempParam.value = tempParam.value + char; i++; }						}					}				}			} // End of loop // Test if start of nest and handle it			function handleNest(newS) { var first = newS.charAt(0); var second = newS.charAt(1); var ret = ''; // Types to handle in array for automatic parsing in loop var handles = [ {  // Handle nested template check: (first == S.braceL && second == S.braceL), startC: S.braceL, endC: S.braceR },					{  // Handle link check: (first == S.bracketL), startC: S.bracketL, endC: S.bracketR },					{	// Handle HTML comments check: (first == S.tag && second == S.excl), startC: '' },					{	// Handle HTML tag short calls check: (first == S.tag && (/^<[a-z][^>/]*\/>/).test(newS)), startR: /^<\\w+/, endR: /^\/>/ },					{	// Handle HTML tags check: (first == S.tag && (/^<[a-z][^>/]*>[^<]*<\/[a-z][^>/]*>/).test(newS)), startR: /^<\w+/, endR: /^<\/\w+>/ },				];

// Handle specified types for (var h = 0; h<handles.length; h++) { var handle = handles[h]; if (handle.check) { var encasedSettings = { str: newS, startC: handle.startC || null, endC: handle.endC || null, startR: handle.startR || null, endR: handle.endR || null, };						ret = sourceTemplateData.getEncased(encasedSettings); break; }				}				return ret; }

// Terminate param and initialize next function closeParam(end) { // Ignore template name if (template.params == null) {template.params = {};} else { var sectioned = (/^([^=]*)(\s*=[^\n\S]*)([\s\S]*)$/).exec(tempParam.value); var param = { raw: tempParam.value, // entire param string start: tempParam.start, // param string start point end: end, // param string end point };					// Terminate into parsed template if (sectioned && sectioned[1]) { param.prefix = sectioned[1]+sectioned[2]; param.value = sectioned[3]; param.name = sectioned[1].trim; } else { template.unnamedCount++; param.prefix = ''; param.value = tempParam.value; param.name = template.unnamedCount.toString; }					sourceTemplateData.paramOrder.push(param.name); template.params[param.name] = param; // Initialize next tempParam = { name: '', value: '', start: 0, raw: '' };				}

}

// Blank return if invalid return null; },		getEncased: function(options) { var nest = null; var newStr = ''; var caret = 0; if ( (!options.endR && !options.endC) || (!options.startR && !options.startC) ) { return ''; } while (caret < options.str.length) { var sub = options.str.substring(caret); var char = options.str.charAt(caret); if (options.startR && options.startR.test(sub)) { nest = nest || 0; sub = options.startR.exec(sub)[0]; nest++; }				else if (options.endR && options.endR.test(sub)) { nest = nest || 0; sub = options.endR.exec(sub)[0]; nest--; }				else if (options.startC && sub.indexOf(options.startC) == 0) { nest = nest || 0; sub = options.startC; nest++; }				else if (options.endC && sub.indexOf(options.endC) == 0) { nest = nest || 0; sub = options.endC; nest--; }				else { sub = char; }				newStr = newStr + sub; caret = caret + sub.length; if (nest < 1) { break; }			}			if (nest < 1) { return newStr; } else {return '';} },		applyChanges: function { var format = this.handleFormat(this.templateData ? this.templateData.format : null); var params = this.templateCall.params; var buildOrder = this.templateOrder; var newT = [''; textbox.textSelection('setSelection', {start: this.templateCall.FROM, end: this.templateCall.FROM+this.templateCall.STR.length}); textbox.textSelection('replaceSelection', newT); popup.hide;

// Build param with data function buildParam(param, data) { if (data) { var param_str = '|'; if (updateFormat || !data.prefix) { if (isNaN(param)) { param_str = param_str + param + ' '.repeat(Math.max(0, (format.maxlen - param.length))) + ' = ';						}					} else { param_str = param_str + data.prefix; }					param_str = param_str + params[param].value.replace(/[\s\n]*$/, ''); newT.push(param_str); }			}		},

// Parse templatedata's `format` param handleFormat: function(format){ var sett = { preall: '', maxlen: 0, afterall: '' };			if (!format || format == 'inline') {} else if (format == 'block') { sett.preall = '\n'; } else { var touse = /^[\s\S]*?[\s\S]*$/.exec(format.trim); if (touse) { sett = { preall:  touse[1], maxlen:  touse[2].length, afterall: touse[3] };				}else{alert('Template with non-standard "format," defaulting to inline.');} }			return sett; },		templateOrder: function { var paramOrder = []; // Params not specified in TD's paramOrder go to the end sourceTemplateData.paramOrder.forEach(function(param){				var checkBox = document.querySelector('#param-toggle-'+param);				if (paramOrder.indexOf(param) == -1 && ((checkBox && checkBox.checked) || !checkBox)) { paramOrder.push(param); }			}); return paramOrder; },		addParam: function { var name = document.querySelector('#td-popup-newparam-name').value.trim || ''; var value = document.querySelector('#td-popup-newparam-value').value.trim || ''; if (name.length > 0 && sourceTemplateData.paramOrder.indexOf(name) == -1) { sourceTemplateData.templateCall.params[name] = { value: value, name: name };				sourceTemplateData.paramOrder.push(name); var container = document.querySelector('.td-popup-params'); container.innerHTML = container.innerHTML + sourceTemplateData.parseParams(name); newParam.hide; popup.show; } else { alert('Empty name or already existing parameter!'); } },

// Delay execution until element matching query exists, or return immediately if it already exists waitForElement: function(query, callback, extraDelay) { if (callback instanceof Function && (typeof query === 'string' || query instanceof String)) { extraDelay = extraDelay || 0; if (document.querySelector(query)) { setTimeout(callback, extraDelay); } else { // set up the mutation observer var observer = new MutationObserver(function (mutations, me) {						// mutations is an array of mutations that occurred						// me is the MutationObserver instance						var targetNode = document.querySelector(query);						if (targetNode) {							setTimeout(callback, extraDelay);							me.disconnect; // stop observing							return;						}					});

// start observing observer.observe(document, {					 childList: true,					  subtree: true					}); }			}

// Debug return in case of invalid input else { console.debug('Invalid input of `query` or `callback` to `sourceTemplateData.waitForElement`'); } },

// Escape string literal to be used in RexExp's search // KNOWN ISSUES: does not work with character classes (e.g., `\w`) //	* Case 1: escapeSearch(`\w`) === `w` -- not a character class //	* Case 2: escapeSearch(`\\w`) === `\\\\w` -- not a character class (in regex literal: /\\w/ which equals to /w/) escapeSearch: function(string) { return string.replace(/[.*+?^${}|[\]\\]/g, '\\$&'); // $& means the whole matched string },

// Escape string literal to be used in RexExp's replace escapeReplace: function(string) { return string.replace(/\$/g, '$$$$'); }

};

// Attempt to load after API is ready and page loads mw.loader.using('mediawiki.api').then(function {		// Check we're in source editing screen, VE editor logs as "view"		if (['edit', 'submit'].includes(config.wgAction)) {			// Wait until CodeMirror's textarea is loaded to start the script			sourceTemplateData.waitForElement('#wpTextbox1', sourceTemplateData.init.bind(sourceTemplateData));		}	});

// JS end });