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[aria-label="Wikitext source editor"]',

// 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 {

// Change apply event document.addEventListener('mouseup', (function(event){ if (event.target.classList.contains('td-popup-head-apply-button')) { sourceTemplateData.applyChanges.bind(this); }			}).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(this)(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 == 'td-popup-template-filter') {					var filter = document.getElementById('td-popup-template-filter').value;					document.querySelectorAll('.td-popup-body-wrapper .td-popup-param').forEach(function(el) { var match = el.id.search(new RegExp('^'+filter, 'i')); //check if param starts with if (match < 0) { el.classList.add('td-popup-param-filterout'); } else { el.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;			if (uptoCaret.length==0){				uptoCaret = $(this.$textbox).textSelection('getContents').substring(0, $(this.$textbox).textSelection('getCaretPosition'));				customSelect = false;			}			if (customSelect || (uptoCaret && checkTemplate.test(uptoCaret))) {				var template = (customSelect && uptoCaret) || uptoCaret.replace(checkTemplate, '$1').trim;				var FROM = $(this.$textbox).textSelection('getCaretPosition') -					( (customSelect && (uptoCaret.length + 2)) || (uptoCaret.replace(checkTemplate, '$1$2').length + 2) );				var toCheck = this.getEncased({ str: $(this.$textbox).textSelection('getContents').substring(FROM), startC: '{', endC: '}' });

this.templateData = null; this.templateCall = null; if (toCheck.length>0){ var waitForCache = function { if (sourceTemplateData.templateData && sourceTemplateData.templateData.TEMPLATE == template) { sourceTemplateData.templateCall = sourceTemplateData.parseTemplate.bind(sourceTemplateData)(toCheck, FROM); } else { setTimeout(waitForCache, 1000); } };					waitForCache; // Start the loop }				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 + '" 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' ],												content: 'Filter params: ',											},											{												classNames: [ 'td-popup-template-filter-input' ],												attributes: {id: 'td-popup-template-filter'},												node: 'input',												content: data.description || 'No template description.'											},										]									) },

]					)			});

var waitForData = function { if (sourceTemplateData.templateCall) { 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) });					});				}				else { 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||this.templateCall.params[param].NEW)) {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 = { braceR: '}', braceL: '{', bracketR: '[', bracketL: ']', 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: {}, };						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 { handleNest(str.substring(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 tags check: (first == S.tag && (/^<[a-z][^>/]*>/).test(newS)), startR: /<\w+/, endR: /<\/\w+>/ },					{	// Handle HTML tag short calls check: (first == S.tag && (/^<[a-z][^>/]*\/>/).test(newS)), startR: /<\\w+/, endR: /\/>/ }				];

// Handle specified types for (var h = 0; h0) { tempParam.value = tempParam.value + ret; i = i + ret.length; } else { tempParam.value = tempParam.value + char; i++; }			}

// Terminate param and initialize next function closeParam(end) { var sectioned = tempParam.value.match(/^([^=]*)(\s*=\s*)([\s\S]*)$/); console.log('parsed param:', sectioned); var param = { raw: tempParam.value, // entire param string order: order, // original param order 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 { 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)) || sub.indexOf(options.startC) == 0) { console.log('start "'+char+'": ', startR.exec(sub)); nest = nest || 0; sub = options.startR ? options.startR.exec(sub)[0] : char; nest++; console.log('new sub:', sub); }				else if ((options.endR && options.startR.test(sub)) || sub.indexOf(options.endC) == 0) { console.log('end "'+char+'": ', endR.exec(sub)); nest = nest || 0; sub = options.REGEX ? options.endR.exec(sub)[0] : char; nest--; console.log('new sub:', sub); }				else { sub = char; }				newStr = newStr + sub; caret = caret + sub.length; if (nest < 1) { break; }			}

return newStr; },		applyChanges: function { var TDref = this.templateData; var format = this.handleFormat(TDref.format); var param_echo = JSON.parse(JSON.stringify(this.templateCall.params)); 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 var buildParam = function(param) { var param_str = '|'; var value = document.querySelector('#ParamVal'+name) && document.querySelector('#ParamVal'+name).value || null; if (value) { if (updateFormat) { if (isNaN(param)) { param_str = param_str + param + ' '.repeat(Math.max(0, (format.maxlen - param.length))) + ' = ';						}					} else { param_str = param_str + prefix; }					param_str = param_str + value;

newT.push(param_str); } else {} param_echo[param] = null; };		},

// 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 });