User:Mikevoir/SourceTemplateData.js

(function {   'use strict';

// Double-load protection. if (window.dev.sourceTemplateData) { return; }

// Initialize window namespace and store it to `settings` variable var settings = (window.dev = window.dev || {}).sourceTemplateData = {};

// Load dependencies importArticles({       type: 'script',        articles: [            'u:dev:MediaWiki:I18n-js/code.js'        ]    });

// Load styling importArticle({       type: 'style',        article: 'u:community:User:Mikevoir/betterCodeMirror.css' // CSS IF PORTED TO DEV: 'u:dev:MediaWiki:SourceTemplateData.css'    });

// Main object var sourceTemplateData = { // Cached mw.config values config: mw.config.get(['wgAction']),

// CodeMirror's textarea selector query textbox: '#wpTextbox1',

// API instance api: new mw.Api,

// Blank variables to store cached data templateData: null, templateCall: null,

// Attempt to initialize the script init: function {

// Check we're in source editing screen, VE editor logs as "view" if (this.config.wgAction && ['edit', 'submit'].includes(this.config.wgAction)) {

// Wait until CodeMirror's textarea is loaded to start the script this.waitForElement(this.textbox, this.initEvents.bind(sourceTemplateData));

}

},

// Initalize permanent events initEvents: function { sourceTemplateData.textbox = $(sourceTemplateData.textbox); // Change apply event document.addEventListener('mouseup', (function(event){ if (event.target.classList.contains('td-popup-head-apply-button')) { sourceTemplateData.applyChanges.bind(sourceTemplateData); }			}).bind(this));

// Script start event // Run on: left click when (alt key + ctrl + shift) document.addEventListener('keydown', (function(event){ if (event.ctrlKey && event.altKey && !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));

// Close popup event ['mouseup', 'keyup'].forEach(function(type){				document.addEventListener(type, function(event) { var el = document.querySelector('.td-popup'); if ( el && ( (							event.target.classList.contains('td-popup-head-close-button') &&							(event.type == 'mouseup')						)|| (							event.type == 'keyup' &&							event.key == 'Escape'						) )){el.remove;} });			});

// 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;					console.log('inputFilter: ', inputFilter);					console.log('existFilter: ', existFilter);					document.querySelectorAll('.td-popup-param .td-popup-param-toggle-button').forEach(function(el) { var filterOut = true; var param = el.id.replace(/^param-toggle-/, ''); var paramBlock = document.querySelector('.td-popup-param#' + param); // Check if param starts with if (inputFilter.length>0 && param.indexOf(inputFilter) == 0) {filterOut = false;} else if (existFilter && el.checked) {filterOut = false;} else if (!existFilter && inputFilter.length==0) {filterOut = false;} if (filterOut) { paramBlock.classList.add('td-popup-param-filterout'); } else { paramBlock.classList.remove('td-popup-param-filterout'); } });				}			});

},

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

// Get template's templatedata getTD: function(template) {

// Valid API response handler var apiValid = function(data) { var tdRegex = / ([\s\n\S]+)<\/templatedata>/;

if (					data &&					data.parse &&					data.parse.wikitext &&					data.parse.wikitext['*'] &&					tdRegex.test(data.parse.wikitext['*'])				){ var raw_templateData = tdRegex.exec(data.parse.wikitext['*']); if (raw_templateData && raw_templateData[0]) { var _templateData = JSON.parse(raw_templateData[1]); _templateData.TEMPLATE = template; this.proofTD.bind(this)(_templateData); } else { alert('"Template:' + template + '" does not have templatedata on "Template:' + template + '/doc".'); }				}			};

// Invalid API response handler var apiInvalid = function { alert('"Template:' + template + '/doc" is not a valid target.'); };

// API call this.api.get({				action: 'parse',				page: 'Template:' + template + '/doc',				prop: 'wikitext'			}).then(apiValid.bind(sourceTemplateData), apiInvalid);

},

// Default templatedata variables proofTD: function(data) { if (!data.paramOrder && data.params) { data.paramOrder = []; Object.keys(data.params).forEach(function(key) {					data.paramOrder.push(key);				}); }			if (!data.format) {data.format='inline';} this.renderTD.bind(this)(data); },

// Render popup with templatadata and current template call's data renderTD: function(data) { document.querySelector('.CodeMirror').blur; // Remove focus to avoid unwanted input if (!data.NORECORD) {this.templateData = data;} var frame = this.nestElements(document.querySelector('body'),					[						{classNames: [ 'td-popup' ]},						{classNames: [ 'td-popup-wrapper' ]}					]				);

// Head container this.addElement({				addTo: frame,				classNames: [ 'td-popup-head-wrapper' ],				content:					this.listElements(null, [							{								node: 'div', classNames: [ 'td-popup-head-label' ], content: 'Template Data: ' + data.TEMPLATE },							{								node: 'a', classNames: [ 'td-popup-head-close-button' ], attributes: { role: 'button', tabindex: '0', rel: 'nofollow', title: 'Close popup' },								content: 'X'							}, {								node: 'span', classNames: [ 'td-popup-head-apply-button', 'wds-button' ], attributes: { role: 'button', tabindex: '0', rel: 'nofollow', title: 'Apply changes' },								content: 'Apply changes' }						]					)			});

// Window body var body = this.addElement({ addTo: frame, classNames: [ 'td-popup-body-wrapper' ] });

// Body title var body_title = this.addElement({				addTo: body,				classNames: [ 'td-popup-body-header' ],				content:					this.listElements( null, [							{								classNames: [ 'td-popup-template' ], content: data.TEMPLATE, },							{								classNames: [ 'td-popup-template-description' ], content: data.description || 'No template description.' },							{								classNames: [ 'td-popup-template-format' ], content: this.listElements(null,										[											{classNames: ['td-popup-format-text'], content:'Format: ' + (data.format.replace('\n', '\\n') || 'inline')},											{node: 'input', classNames: ['td-popup-checkbox', 'td-popup-format-button'], attributes: {type: 'checkbox', id: 'template-format'}},											{content:'Apply format', node: 'label', classNames: ['td-popup-checkbox', 'td-popup-format-button-label'], attributes: {type: 'checkbox', 'for': 'template-format'}},										]									) },							{								classNames: [ 'td-popup-template-filter' ], content: this.listElements(										null,										[											{												classNames: [ 'td-popup-template-filter-label' ],												node: 'label',												content: 'Filter params: ',											},											{												classNames: [ 'td-popup-template-filter-input' ],												attributes: {id: 'td-popup-template-filter'},												node: 'input'											},										]									) },							{								classNames: [ 'td-popup-template-filter' ], content: this.listElements(										null,										[											{												node: 'input',												classNames: ['td-popup-checkbox', 'td-popup-template-filter-input'],												attributes: {type: 'checkbox', id: 'template-filter-notincall'}											},											{												content:'Toggle parameters not in call',												node: 'label',												classNames: ['td-popup-checkbox', 'td-popup-template-filter-label'],												attributes: {type: 'checkbox', 'for': 'template-filter-notincall'}											},										]									) },

]					)			});			var i = 0; var waitForData = function { if (sourceTemplateData.templateCall) { var alreadyBuilt = {}; data.paramOrder.forEach(function(param){						sourceTemplateData.addElement({ addTo: body, classNames: [ 'td-popup-param' ], attributes: {id: param}, content: sourceTemplateData.parseParam.bind(sourceTemplateData)(data.params[param], param) });						alreadyBuilt[param] = true;					}); // Add params not in templatedata at the end Object.keys(sourceTemplateData.templateCall.params).forEach(function(param){						if (!alreadyBuilt[param]) {							sourceTemplateData.addElement({ addTo: body, classNames: [ 'td-popup-param' ], attributes: {id: param}, content: sourceTemplateData.parseParam.bind(sourceTemplateData)({}, param) });							alreadyBuilt[param] = true;						}					}); }				else { i++; setTimeout(waitForData, 1000); } };			waitForData; // Start the loop },		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; },		parseParam: function(data, param) { var checkbox_attr = { type: 'checkbox', id: 'param-toggle-'+param };			if (this.templateCall.params[param] && this.templateCall.params[param].value) {checkbox_attr.checked = 'true';} var all = [ {					node: 'input', classNames: ['td-popup-checkbox', 'td-popup-param-toggle-button'], attributes: checkbox_attr },				{					content: data.label || param, 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 (data.aliases && data.aliases.length>0) { data.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: this.listElements.bind(this)(null,param_names) }); if (data.description) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({					content: data.description,					node: 'p'				}); }			if (data.default) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({					content: 'Default: ' + data.default,					classNames: ['vetd-popup-param-default'],					node: 'p'				});} if (data.example) { if (!hr){param_desc.push({node: 'hr'}); hr=true;} param_desc.push({					content: 'Example: ' + data.example,					classNames: ['td-popup-param-example'],					node: 'p'				}); }			all.push({ classNames: [ 'td-popup-param-description' ], content: this.listElements.bind(this)(null,param_desc) }); all.push({				classNames: [ 'td-popup-param-value' ],				node: 'textarea',				content: this.templateCall.params[param] && this.templateCall.params[param].value || null,				attributes: {id: 'ParamVal'+param}			}); return this.listElements.bind(this)(null, all); },		parseTemplate: function(str, FROM) { var unnamedCount = 0; 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, };						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)); return template;

// 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*=\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 order: order, // original param order };					// Terminate into parsed template if (sectioned && sectioned[1]) { param.prefix = sectioned[1]+sectioned[2]; param.value = sectioned[3]; param.name = sectioned[1].trim; } else { unnamedCount++; param.prefix = ''; param.value = tempParam.value; param.name = unnamedCount; }					template.params[param.name] = param; // Initialize next order++; 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 TDref = this.templateData; var format = this.handleFormat(TDref.format); var params = this.templateCall.params; var alreadyBuilt = {}; var newT = [''; this.textbox.textSelection('setSelection', {start: this.templateCall.FROM, end: this.templateCall.FROM+this.templateCall.STR.length}); this.textbox.textSelection('replaceSelection', newT); document.querySelector('.td-popup').remove;

// Build param with data function buildParam(param, data) { var param_str = '|'; var textarea = document.querySelector('#ParamVal'+param); if (textarea.value) { if (updateFormat) { if (isNaN(param)) { param_str = param_str + param + ' '.repeat(Math.max(0, (format.maxlen - param.length))) + ' = ';						}					} else { param_str = param_str + (data ? data.prefix : ''); }					param_str = param_str + textarea.value.replace(/\s*$/, '');

newT.push(param_str); } else {} alreadyBuilt[param] = true; }		},

// Parse templatedata's `format` param handleFormat: function(format){ var sett = { preall: '', maxlen: 0, afterall: '' };			if (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; },

// 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 {		$(sourceTemplateData.init.bind(sourceTemplateData));	});

// JS end });