/**************************************************

== Français ==
Gadget pour éditer les balises « templatedata » de l’extension « TemplateData » de MediaWiki sans avoir à manipuler le JSON.

Un lien « TDE » est ajouté dans la boîte à outils d’édition.
Il ouvre une fenêtre de modification permettant toutes les modifications autorisées.

* Auteurs : Ltrlg (TDE) & Salix alba (TDS)
* Dernière mise à jour : 19 août 2013

== English ==
Gadget to edit “templatedata” tags for the MediaWiki extension TemplateData without having to edit JSON.

A “TDE” link is added in the edition toolbox when editing a template.
It opens a window allowing all modifications to the template data.

* Authors: Ltrlg (TDE) & Salix alba (TDS)
* Last update: 2013-08-19

== Limitation ==
MediaWiki _allows_ using parameters like '{' (yes, really!)

But here, you can’t use it:
* TDS does not see these parameters
* The default name for a parameter matches /\{+\}/ and TDE does not save if if there is any parameter containing '{'.

Of course, I don’t think anybody uses a '{' parameter. So this is not blocking in many cases.

== Translations ==
* el: Xaris333, Geraki
* en: NicoV
* it: Jacopo Werther
* gl: Elisardojm
* ja: Shirayuki
* ko: Kwj2772
* nl: Wolf Lambert

== Source ==

<syntaxhighlight lang="javascript">

 **************************************************/

function TemplateDataEditor($) {

	var
		/* global objects */
		ui, // The interface (instance of Interface)
		td, // The current (or last) editor (instance of TemplateData)
		
		/* unique identifiers (see trait UniqueElement) */
		uniq = 0,
		
		/* traits */
		UniqueElement, DataForm,
		
		/* tde regexps */
		regExpTwoTags = /<templatedata*>(*)<\/templatedata>/,
		regExpOneTag = /<templatedata*\/>/,
		matchType = 0,
		
		/* indntation */
		defaultIndent = '\t',
		indent = defaultIndent,
		
		/* languages */
		userLanguage = mw.config.get('wgUserLanguage'),
		contentLanguage = mw.config.get('wgContentLanguage'),
		
		/* translations */
		messages = {
			"el": {
				"apply": "Εφαρμογή",
				"cancel": "Ακύρωση",
				"close": "Κλείσιμο",
				"collapse": "Κατάρρευση",
				"colon": ": ",
				"description": "Περιγραφή προτύπου",
				"description-placeholder": "Τοποθετείστε μια περιγραφή του προτύπου",
				"error-description": "Συνέβη κάποιο σφάλμα$2:\n\n$1",
				"error-it-lang-inexistent": "Αυτή η γλώσσα ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να την εντοπίσει", // $1 είναι ο κωδικός της γλώσσας
				"error-name-already-used": "Δεν μπορείτε να μετονομάσετε το στοιχείο επειδή το νέο όνομα χρησιμοποιείται ήδη",
				"error-name-inexistent": "Αυτό το στοιχείο ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να το εντοπίσει.", // $1 είναι η ονομασία
				"error-report": " (Αναφορά σφάλματος)",
				"error-set-inexistent": "Αυτό το σύνολο ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να το εντοπίσει", // $1 είναι το id του συνόλου
				"error-tds-not-loaded": "Η σελίδα δεν έχει φορτώσει",
				"expand": "Ανάπτυξη",
				"invalid-name": "“$1” δεν είναι κατάλληλο κλειδί",
				"it-add": "Προσθέστε μια γλώσσα",
				"it-otherlanguages-show": "Δείξτε τις γλώσσες ($1)", // $1 είναι η ονομασία της ξένης γλώσσας
				"it-otherlanguages-hide": "Αποκρύψτε τις γλώσσες", // $1 είναι ο αριθμός των ξένων γλωσσών
				"it-remove": "Αφαίρεση γλώσσας",
				"param-add": "Προσθήκη παραμέτρου",
				"param-aliases": "Άλλες ονομασίες",
				"param-default": "Προεπιλεγμένη τιμή: ",
				"param-deprecated": "Καταργημένη",
				"param-deprecated-tooltip": "Λεπτομέρειες",
				"param-description": "Περιγραφή",
				"param-inherits": "Μεταβιβάζει",
				"param-label": "Εμφανιζόμενο όνομα",
				"param-name": "Πραγματικό όνομα",
				"param-remove": "Αφαίρεση αυτής της παραμέτρου",
				"param-required": "Απαιτείται",
				"param-type": "Είδος: ",
				"param-type-number": "Αριθμός",
				"param-type-string": "Κείμενο",
				"param-type-string/line": "Κείμενο (μία γραμμή)",
				"param-type-string/wiki-page-name": "Τίτλος σελίδας",
				"param-type-string/wiki-user-name": "Όνομα χρήστη",
				"param-type-unknown": "Άγνωστο",
				"parse-error": "Το δεδομένο δεν μπορεί να αναλυθεί. Αυτό προκαλείται σε γενικές γραμμές από ένα συντακτικό σφάλμα στην JSON ή από την παρουσία των δύο συνόλων δεδομένων.",
				"preload-data": "Προσυμπληρώστε τα δεδομένα",
				"preload-load": "Εκτέλεση",
				"preload-none": "Μην προσυμπληρώσετε",
				"preload-running": "Εκτελείται…",
				"preload-select": "Προσυμπληρώστε από",
				"no-data": "Δεν έχουν βρεθεί δεδομένα. Παρακαλώ προσθέστε μια ετικέτα <templatedata /> ώστε να μπορείτε να την επεξεργαστείτε.",
				"section-description": "Περιγραφή",
				"section-params": "Παράμετροι",
				"section-sets": "Σύνολα",
				"set-add": "Πρόσθεση συνόλου",
				"set-label": "Όνομα: ",
				"set-params": "Παράμετροι",
				"set-remove": "Αφαίρεση αυτού του συνόλου",
				"title": "Τροποποίηση δεδομένων προτύπου",
				"title-documentation": "Τεκμηρίωση",
				"start-tde": "Επεξεργασία δεδομένων προτύπου",
				"use-pipes": " (να διαχωρίζονται με “|”)"
			},
			"en": {
				"apply": "Apply",
				"cancel": "Cancel",
				"close": "Close",
				"collapse": "Collapse",
				"colon": ": ",
				"description": "Description of this template",
				"description-placeholder": "Enter a description of this template here",
				"error-description": "An error happened$2:\n\n$1", // $2 is message(error-report) or '' ; $1 is the error
				"error-it-lang-inexistent": "This language ($1) can’t be removed because TDE can’t find it anymore", // $1 is the language code
				"error-name-already-used": "You can’t rename this element because the new name is already used",
				"error-name-inexistent": "This element ($1) can’t be removed because TDE can’t find it anymore", // $1 is the name
				"error-report": " (report it)",
				"error-set-inexistent": "This set ($1) can’t be removed because TDE can’t find it anymore", // $1 is the id of the set
				"error-tds-not-loaded": "The page hasn’t been loaded",
				"expand": "Expand",
				"invalid-name": "“$1” is not a valid key",
				"it-add": "Add a language",
				"it-otherlanguages-show": "Show the languages ($1)", // $1 is the number of foreign languages
				"it-otherlanguages-hide": "Hide the languages", // $1 is the number of foreign languages
				"it-remove": "Remove this language",
				"param-add": "Add a parameter",
				"param-aliases": "Other names",
				"param-default": "Default value",
				"param-deprecated": "Deprecated",
				"param-description": "Description",
				"param-description-placeholder": "Insert a description of this parameter here",
				"param-inherits": "Documentation inherits from",
				"param-label": "Displayed name",
				"param-label-placeholder": "Enter name here",
				"param-name": "Real name",
				"param-remove": "Remove this parameter",
				"param-required": "Required",
				"param-type": "Type",
				"param-type-number": "Number",
				"param-type-string": "Text",
				"param-type-string/line": "Text (one line)",
				"param-type-string/wiki-page-name": "Page title",
				"param-type-string/wiki-user-name": "User name",
				"param-type-unknown": "Unknown",
				"parse-error": "The data can’t be parsed. This is caused in general by a syntax error in the JSON or by the presence of two datasets.",
				"preload-data": "Prefill the data",
				"preload-load": "Run",
				"preload-none": "Do not prefill",
				"preload-running": "Running…",
				"preload-select": "Prefill from",
				"no-data": "No data has been found. Please, add a <templatedata /> tag to be able to edit it.",
				"section-description": "Description",
				"section-params": "Parameters",
				"section-sets": "Sets",
				"set-add": "Add a set",
				"set-label": "Name",
				"set-params": "Parameters",
				"set-remove": "Remove this set",
				"start-tde": "Modify template data",
				"title": "Modify template data",
				"title-documentation": "documentation",
				"use-pipes": " (separated by pipes “|”)"
			},
			"fr": {
				"apply": "Appliquer",
				"cancel": "Annuler",
				"close": "Fermer",
				"collapse": "Fermer",
				"colon": "\xA0: ",
				"description": "Description de ce modèle",
				"description-placeholder": "Entrez une description de ce modèle",
				"error-description": "Une erreur est survenue$2\xA0:\n\n$1",
				"error-invalid-name": "L’identifiant «\xA0$1\xA0» n’est pas valide",
				"error-it-lang-inexistent": "Cette traduction ($1) ne peut être supprimée car TDE ne la trouve plus",
				"error-name-already-used": "Vous ne pouvez pas renommer cet élément car le nom donné est déjà utilisé",
				"error-name-inexistent": "Cet élément ($1) ne peut être supprimé car TDE ne le trouve plus",
				"error-report": " (signalez-la)",
				"error-set-inexistent": "Cet ensemble ($1) ne peut être supprimé car TDE ne le trouve plus",
				"error-tds-not-loaded": "La page n’a pas pu être chargée",
				"expand": "Ouvrir",
				"it-add": "Ajouter une langue",
				"it-no-language": "Pas de code de langue ($1)",
				"it-otherlanguages-show": "Afficher les traductions ($1)",
				"it-otherlanguages-hide": "Cacher les traductions",
				"it-remove": "Retirer cette langue",
				"param-add": "Ajouter un paramètre",
				"param-aliases": "Autre noms",
				"param-default": "Valeur par défaut",
				"param-deprecated": "Obsolète",
				"param-description": "Description",
				"param-description-placeholder": "Entrez une description de ce paramètre",
				"param-inherits": "Documentation héritée de",
				"param-label": "Nom affiché",
				"param-label-placeholder": "Ajoutez un nom",
				"param-name": "Nom réel",
				"param-remove": "Retirer ce paramètre",
				"param-required": "Obligatoire",
				"param-type": "Type",
				"param-type-number": "Nombre",
				"param-type-string": "Texte",
				"param-type-string/line": "Texte (une ligne)",
				"param-type-string/wiki-page-name": "Titre de page",
				"param-type-string/wiki-user-name": "Nom d’utilisateur",
				"param-type-unknown": "Inconnu",
				"parse-error": "Les données ne peuvent pas être interprétées. Cela arrive généralement lorsqu’il y a une erreur de syntaxe dans le JSON ou lorsqu’il y a deux ensembles de données dans la page.",
				"preload-data": "Pré-remplir les données du modèle",
				"preload-load": "Exécuter",
				"preload-none": "Ne pas pré-remplir",
				"preload-running": "Chargement en cours…",
				"preload-select": "Pré-remplir depuis",
				"no-data": "Aucune donnée n’a été trouvée. Veuillez ajouter une balise <templatedata /> pour pouvoir l’éditer.",
				"section-description": "Description",
				"section-params": "Paramètres",
				"section-sets": "Ensembles",
				"set-add": "Ajouter un ensemble",
				"set-label": "Nom",
				"set-label-placeholder": "Ajoutez un nom",
				"set-params": "Paramètres",
				"set-remove": "Retirer cet ensemble",
				"start-tde": "Modifier les données du modèle",
				"title": "Modifier les données du modèle",
				"title-documentation": "documentation",
				"use-pipes": " (séparés par des tubes «\xA0|\xA0»)"
			},
			"gl": {
				"apply": "Aplicar",
				"cancel": "Cancelar",
				"close": "Pechar",
				"collapse": "Pregar",
				"colon": ": ",
				"description": "Descrición deste modelo",
				"description-placeholder": "Indique aquí unha descrición deste modelo",
				"error-description": "Ocorreu un erro$2:\n\n$1", // $2 é a mensaxe(erro-aviso) ou '' ; $1 é o erro
				"error-it-lang-inexistent": "Este idioma ($1) non pode eliminarse porque TDE non a pode atopar", // $1 é o código do idioma
				"error-name-already-used": "Non pode renomear este elemento porque o novo nome xa se está usando",
				"error-name-inexistent": "Este elemento ($1) non pode eliminarse porque TDE non o pode atopar", // $1 é o nome
				"error-report": " (informar)",
				"error-set-inexistent": "Este conxunto ($1) non pode eliminarse porque TDE non o pode atopar", // $1 é o id do conxunto
				"error-tds-not-loaded": "A páxina non foi cargada",
				"expand": "Expandir",
				"invalid-name": "“$1” non é unha chave válida",
				"it-add": "Engadir un idioma",
				"it-otherlanguages-show": "Mostrar os idiomas ($1)", // $1 é o número de idiomas estranxeiros
				"it-otherlanguages-hide": "Ocultar os idiomas", 
				"it-remove": "Eliminar este idioma",
				"param-add": "Engadir un parámetro",
				"param-aliases": "Outros nomes",
				"param-default": "Valor por defecto",
				"param-deprecated": "Desprezar",
				"param-description": "Descrición",
				"param-description-placeholder": "Inserir aquí a descrición para este parámetro",
				"param-inherits": "Documentación herdada de",
				"param-label": "Nome visualizado",
				"param-label-placeholder": "Engada o nome aquí",
				"param-name": "Nome real",
				"param-remove": "Eliminar este parámetro",
				"param-required": "Requirido",
				"param-type": "Tipo",
				"param-type-number": "Número",
				"param-type-string": "Texto",
				"param-type-string/line": "Texto (unha liña)",
				"param-type-string/wiki-page-name": "Título de páxina",
				"param-type-string/wiki-user-name": "Nome de usuario",
				"param-type-unknown": "Descoñecido",
				"parse-error": "Os datos non poden ser analizados. Normalmente isto ocorre por un erro de sintaxe no JSON ou pola presenza de dous bloques de datos.",
				"preload-data": "Cubra os datos",
				"preload-load": "Executar",
				"preload-none": "Non cubrir",
				"preload-running": "Executando…",
				"preload-select": "Cubrir dende",
				"no-data": "Non se atoparon datos. Por favor, engada unha marca <templatedata /> para poder editala.",
				"section-description": "Descrición",
				"section-params": "Parámetros",
				"section-sets": "Conxuntos",
				"set-add": "Engadir un conxunto",
				"set-label": "Nome",
				"set-params": "Parámetros",
				"set-remove": "Eliminar este conxunto",
				"start-tde": "Modificar datos do modelo",
				"title": "Modificar datos do modelo",
				"title-documentation": "documentación",
				"use-pipes": " (separados por barras “|”)"
			},
			"ja": {
				"apply": "適用",
				"close": "閉じる",
				"colon": ": ",
				"error-description": "エラーが発生しました$2:\n\n$1",
				"param-add": "引数を追加",
				"param-aliases": "その他の名前",
				"param-default": "既定値",
				"param-deprecated": "廃止予定",
				"param-deprecated-tooltip": "詳細: ",
				"param-description": "説明",
				"param-inherits": "継承",
				"param-label": "表示名",
				"param-name": "名前",
				"param-remove": "この引数を除去",
				"param-required": "必須",
				"param-type": "型: ",
				"param-type-number": "数値",
				"param-type-string": "文字列",
				"param-type-string/line": "文字列 (1 行)",
				"param-type-string/wiki-page-name": "ページ名",
				"param-type-string/wiki-user-name": "利用者名",
				"param-type-unknown": "不明",
				"no-data": "データが見つかりませんでした。編集できるようにするには、<templatedata /> タグを追加してください。",
				"section-description": "説明",
				"section-params": "引数",
				"section-sets": "集合",
				"set-add": "集合を追加",
				"set-label": "名前: ",
				"set-params": "引数",
				"set-remove": "この集合を除去",
				"title": "TemplateData の変更",
				"title-documentation": "説明文書",
				"start-tde": "TemplateData の編集",
				"use-pipe": " (パイプ記号「|」で区切る)"
			},
			"ko": {
				"apply": "적용",
				"close": "닫기",
				"colon": ": ",
				"error-description": "오류 발생$2:\n\n$1",
				"param-add": "변수 추가하기",
				"param-aliases": "다른 이름",
				"param-default": "기본값",
				"param-deprecated": "사용 중지",
				"param-deprecated-tooltip": "자세한 정보",
				"param-description": "설명",
				"param-inherits": "상속받을 변수",
				"param-label": "표시될 이름",
				"param-name": "실제 이름",
				"param-remove": "이 변수 제거",
				"param-required": "필수",
				"param-type": "Type: ",
				"param-type-number": "숫자",
				"param-type-string": "문자열",
				"param-type-string/wiki-page-name": "문서 이름",
				"param-type-string/wiki-user-name": "사용자 이름",
				"param-type-unknown": "알 수 없음",
				"no-data": "데이터가 없습니다. 편집을 가능하게 하려면 <templatedata /> 태그를 추가하십시오.",
				"section-description": "설명",
				"section-params": "변수",
				"section-sets": "집합",
				"set-add": "집합 추가하기",
				"set-label": "이름: ",
				"set-params": "변수",
				"set-remove": "이 집합 제거하기",
				"title": "틀 데이터 수정하기",
				"title-documentation": "설명 문서",
				"start-tde": "틀 데이터 편집하기",
				"use-pipes": " (파이프 “|”로 구분)"
			},
			"it": {
				"apply": "Applica (a tuo rischio e pericolo, ogni abuso sarà segnalato)",
				"colon": "\xA0: ",
				"param-add": "Aggiungi un parametro",
				"param-aliases": "Altri nomi",
				"param-default": "Valore di default",
				"param-deprecated": "Deprecato",
				"param-deprecated-tooltip": "Dettagli",
				"param-description": "Descrizione",
				"param-inherits": "Eredità",
				"param-label": "Nome visualizzato",
				"param-name": "Nome effettivo",
				"param-remove": "Rimuovi questo parametro",
				"param-required": "Richiesto",
				"no-data": "Spiacente. Nessun dato è stato trovato. Aggiungere un tag <templatedata /> per poter modificare il template data.",
				"section-description": "Descrizione",
				"section-params": "Parametri",
				"set-add": "Aggiungi un set",
				"title": "Benvenuto in Modifica TemplateData",
				"title-documentation": "tutorial per negati",
				"use-pipes": " (separati da pipes « | »)"
			},
			"nl": {
				"apply": "Oké",
				"cancel": "Annuleren",
				"close": "Sluiten",
				"collapse": "Inklappen",
				"colon": ": ",
				"description": "Beschrijving van dit sjabloon",
				"description-placeholder": "Voer hier een beschrijving van dit sjabloon in",
				"error-description": "Er deed zich een fout voor$2:\n\n$1", // $2 is message(error-report) or '' ; $1 is the error
				"error-it-lang-inexistent": "De taal $1 kan niet worden verwijderd, omdat TDE hem niet meer kan vinden.", // $1 is the language code
				"error-name-already-used": "Dit element kan niet van naam veranderd worden omdat de naam al in gebruik is.",
				"error-name-inexistent": "Het element \"$1\" kan niet worden verwijderd, omdat TDE het niet meer kan vinden.", // $1 is the name
				"error-report": " (rapporteer het)",
				"error-set-inexistent": "De set \"$1\" kan niet worden verwijderd, omdat TDE hem niet meer kan vinden.", // $1 is the id of the set
				"error-tds-not-loaded": "Deze pagina is niet geladen.",
				"expand": "Uitklappen",
				"invalid-name": "\"$1\" is geen valide sleutel",
				"it-add": "Taal toevoegen",
				"it-otherlanguages-show": "Toon de talen ($1)", // $1 is the number of foreign languages
				"it-otherlanguages-hide": "Verberg de talen", // $1 is the number of foreign languages
				"it-remove": "Verwijder deze taal",
				"param-add": "Parameter toevoegen",
				"param-aliases": "Andere namen",
				"param-default": "Standaardwaarde",
				"param-deprecated": "Verouderd",
				"param-description": "Beschrijving",
				"param-description-placeholder": "Voer hier een beschrijving van deze parameter in",
				"param-inherits": "Documentatie wordt overgenomen van",
				"param-label": "Getoonde naam",
				"param-label-placeholder": "Voer hier een naam in",
				"param-name": "Echte naam",
				"param-remove": "Verwijder deze parameter",
				"param-required": "Verplicht",
				"param-type": "Type",
				"param-type-number": "Nummer",
				"param-type-string": "Tekst",
				"param-type-string/line": "Tekst (één lijn)",
				"param-type-string/wiki-page-name": "Paginatitel",
				"param-type-string/wiki-user-name": "Gebruikersnaam",
				"param-type-unknown": "Onbekend",
				"parse-error": "De data kan niet geparsed worden. Dit is meestal de oorzaak van een fout in de JSON-syntax, of wanneer er twee datasets aanwezig zijn.",
				"preload-data": "Vul de data vooraf in",
				"preload-load": "Start",
				"preload-none": "Vul de data niet vooraf in",
				"preload-running": "Bezig…",
				"preload-select": "Vul vooraf in van",
				"no-data": "Er staat nog geen templatedata op dit sjabloon; plaats <templatedata /> onderaan het sjabloon om dit sjabloon te bewerken.",
				"section-description": "Beschrijving",
				"section-params": "Parameters",
				"section-sets": "Sets",
				"set-add": "Voeg een set toe",
				"set-label": "Naam",
				"set-params": "Parameters",
				"set-remove": "Verwijder deze set",
				"start-tde": "Pas de templatedata aan",
				"title": "Templatedata aanpassen",
				"title-documentation": "documentatie",
				"use-pipes": " (gescheiden door pipes \"|\")"
			}
		},
		documentations = { // Local pages (but full link for default)
			"default": '//en.wikipedia.orghttps://wikines.com/fr/User:NicoV/TemplateDataEditor',
			"enwiki": 'User:NicoV/TemplateDataEditor',
			"frwiki": 'Utilisateur:Ltrlg/TemplateDataEditor',
			"itwiki": 'Wikipedia:VisualEditor/TemplateData'
		};
	
	////////// Translation //////////
	
	function messageLang(name, lang) {
		var
			res,
			i,
			T = ;
		
		if( name == '' ) {
			return '';
		}
		
		if( lang == 'qqx' ) {
			res = '(-tde-' + name;
			if( arguments.length > 2 ) {
				res += ': ';
				for(i=2; i<arguments.length; i++) {
					T.push(arguments);
				}
				res += T.join(', ');
			}
			return res + ')';
		} else {
			if( messages && messages ) {
				res = messages;
			} else if( messages.en ) {
				res = messages.en;
			} else {
				arguments = 'qqx';
				return messageLang.apply(null, arguments);
			}
		
			// Replace vars
			for(i=arguments.length-2; i>0; i--) {
				res = res.replace(new RegExp('\\$'+i, 'g'), arguments);
			}
			
			return res;
		}
	}
	
	function message(name) {
		var args = Array.prototype.slice.call(arguments);
		args.shift();
		args.unshift(name, userLanguage);
		return messageLang.apply(null, args);
	}
	
	function documentationLink() {
		var wiki = mw.config.get('wgWikiID');
		if( documentations.hasOwnProperty( wiki ) ) {
			return mw.util.getUrl( documentations );
		} else {
			return documentations;
		}
	}
	
	////////// Getting & setting text (compatibility with WikEd) //////////
	
	function getText() {
		if( window.wikEd && window.wikEd.useWikEd ) {
			WikEdUpdateTextarea();
		}
		return $('#wpTextbox1').val();
	}
	
	function setText(value) {
		$('#wpTextbox1').val(value);
		if( window.wikEd && window.wikEd.useWikEd ) {
			WikEdUpdateFrame();
		}
	}
	
	////////// Usefull functions //////////
	
	function userError() {
		var e = new Error( message.apply(null, arguments) );
		e.userError = true;
		return e;
	}
	
	function scriptError() {
		var e = new Error( message.apply(null, arguments) );
		e.userError = false;
		return e;
	}
	
	function alertError(e) {
		alert(message(
			'error-description',
			e.message,
			e.userError ? '' : message('error-report')
		));
		console.error('“' + e.message + '”\nError thrown by ' + e.fileName + ' on line ' + e.lineNumber);
	}
	
	function ucfirst(str) {
		return str.toUpperCase() + str.substring(1, str.length);
	}
	
	function norm(str) {
		return ucfirst( str.replace('_', ' ') );
	}
		
	function trim(str) {
		return str.replace(/^\s*(\S.*\S|\S)\s*$/, '$1');
	}

	function trimArray(Arr) {
		var i = 0;
		for(; i<Arr.length; i++) {
			Arr = trim(Arr);
		}
		return Arr;
	}
	
	function strToArr(str) {
		return trimArray( str.split('|') );
	}
	
	function arrToStr(arr) {
		return arr.join(' | ');
	}
	
	function $clear() {
		return $('<div>').addClass('tde-clear');
	}
	
	function selectValue(select) { // Select is a jQuery object $('<select>') or a DOM select node
		var res;
		$(select).children('option').each(function(){
			if( $(this).prop('selected') ) {
				res = $(this).val();
				return false;
			}
		});
		return res;
	}
	
	function getIndent(text) { // Should work fine with any well-indented JSON
		var
			lines = text.split('\n'),
			i,
			maxLength = Infinity,
			indent = defaultIndent,
			localIndent;
		
		for(i=0; i<lines.length; i++) {
			try {
				localIndent = /^(\s*)\S/.exec(lines);
				if( localIndent.length < maxLength && localIndent.length != 0 ) {
					indent = localIndent;
					maxLength = localIndent.length;
				}
			} catch(e) {
				// Nothing to do, just a line without \S
			}
		}
		return indent;
	}
	
	function noCurlyBraceKey(object) {
		for(var i in object) {
			if( //.test(i) ) {
				throw userError('error-invalid-name', i);
			}
		}
	}
	
	function arrayRemoveElement(array, i) {
		var
			newLength = array.length-1,
			j;
		
		for(j=i; j<array.length-1; j++) {
			array = array;
		}
		
		array.length = newLength;
	}
	
	////////// TemplateDataSkeleton (partial, adapted) //////////
	
	function TemplateDataSkeletonFromText(text) {
		
		var
			pat = /\{\{\{(+)(.)/g,  // '{{{' then any char other than {|}\n<
			matches, name, newReq, oldReq,
			params = {};
		
		while( (matches=pat.exec(text)) != null ) {
			name = trim(matches);
			newReq = ( matches== '}' );
			oldReq = ( params == null || params.required ); 
			
			params = {
				required: newReq && oldReq,
				label: norm(name)
			};
			
			pat.lastIndex--; // need to backtrack one character
		}
		
		return { params: params };
	}
	
	function TemplateDataSkeleton(page, cb) {
		
		function error() {
			alertError( scriptError('tds-not-loaded') );
			cb({});
		}
		
		$.ajax({
			url: mw.util.wikiScript('api'),
			data: {
				action: 'query',
				prop: 'revisions',
				titles: page,
				rvprop: 'content',
				format: 'json'
			},
			dataType: 'json',
			error: error,
			success: function( data ) {
				try {
					var pageId = Object.keys(data.query.pages);
					cb( TemplateDataSkeletonFromText(data.query.pages.revisions || '') ); // '' if missing page
				} catch(e) {
					error();
				}
			}
		});
		
	}
	
	////////// Actions //////////
	
	function action(_options) {
		
		var
			options = Object.assign({
				type: null,
				desc: '',
				fn: function(){},
				aClass: '',
				aId: ''
			}, _options),
			img = action.images;
		
		return $('<a>')
			.attr({
				title: options.desc,
				href: '#',
				'class': options.aClass,
				id: options.aId
			})
			.click(function(){
				options.fn.call(this);
				return false;
			})
			.append($('<img>')
				.attr({
					alt: options.desc,
					src: '//upload.wikimedia.org/wikipedia/commons/thumb/'
						+ img.hashStart + '/'
						+ img.hashStart + '/'
						+ img.name + '/'
						+ '24px-' + img.name + '.png'
				})
			);
	}
	
	action.images = {
		add: {
			hashStart: '8b',
			name: 'VisualEditor_-_Icon_-_Add-item.svg'
		},
		remove:  {
			hashStart: '0e',
			name: 'VisualEditor_-_Icon_-_Remove-item.svg'
		},
		close:  {
			hashStart: '8d',
			name: 'VisualEditor_-_Icon_-_Close.svg'
		},
		expand:  {
			hashStart: '2f',
			name: 'VisualEditor_-_Icon_-_Expand.svg'
		},
		collapse:  {
			hashStart: '32',
			name: 'VisualEditor_-_Icon_-_Collapse.svg'
		},
		expand_inline:  {
			hashStart: 'd3',
			name: 'VisualEditor_-_Icon_-_Move-ltr.svg'
		},
		collapse_inline:  {
			hashStart: '4e',
			name: 'VisualEditor_-_Icon_-_Move-rtl.svg'
		}
	};
	
	////////// class Interface //////////
	
	function Interface() {
		
		var that = this;
		
		this.$title = $('<h2>')
			.text( message('title') )
			.append(document.createTextNode(' ('))
			.append($('<a>')
				.attr('href', documentationLink() )
				.text( message('title-documentation') )
			)
			.append(document.createTextNode(')'));
		
		this.$body = $('<div>').attr('id', 'tde-body');
		
		this.$buttonContainer = $('<div>').addClass('tde-buttons');
		
		this.$cont = $('<div>')
			.attr('id', 'tde')
			.append($('<div>').attr('id', 'tde-mask'))
			.append($('<div>')
			.attr('id', 'tde-dialog')
			.append( this.$title )
			.append( action({type: 'close', desc: message('close'), fn: function() { that.close(); }, aId: 'tde-close'}) )
			.append( this.$body )
			.append( this.$buttonContainer )
			)
			.hide();
		
		$(document.body).append(this.$cont);
	}
	
	Interface.prototype = {
		clear: function() {
			this.$body.children().remove();
			this.deleteButtons();
		},
		
		close: function() {
			this.$cont.fadeOut();
		},
		
		open: function() {
			this.$cont.fadeIn();
		},
		
		addCancelButton: function() {
			var that = this;
			this.addButton('cancel', function(){
				that.close();
			});
		},
		
		addButton: function(msg, fn) {
			this.$buttonContainer.append($('<input>')
				.attr('type', 'button')
				.val( message(msg) )
				.click(fn)
			);
		},
		
		deleteButtons: function() {
			this.$buttonContainer.children().remove();
		},
		
		replaceButton: function(fn) {
			this.deleteButtons();
			this.$buttonContainer.append($('<input>')
				.attr({
					id: 'tde-apply',
					type: 'button'
				})
				.val( message('apply') )
				.click(fn)
			);
		}
	};
	
	ui = new Interface;
	
	////////// class Renamer //////////
	
	function Renamer(id, name, parent, readonly) {
		this.name = name;
		this.parent = parent;
		this.readonly = !! readonly;
		
		var that = this;
		
		this.$input = $('<input>')
			.addClass('tde-renamer-name-input')
			.attr({
				type: 'text',
				id: id
			})
			.prop('readonly', this.readonly)
			.blur(function(){
				that.exec();
			})
			.val(name);
	}
	
	Renamer.prototype = {
		getNode: function() {
			return this.$input;
		},
		
		exec: function() {
			var newName = this.$input.val();
			try {
				this.parent.renameElement(this.name, newName);
				this.name = newName;
			} catch(e) {
				alertError( e );
			}
			this.$input.val(this.name);
		}
	};
	
	////////// trait UniqueElement //////////
	
	UniqueElement = {
	
		defineUniq: function() {
			this.uniq = uniq;
			uniq++;
		}
	
	};
	
	////////// abstract class InterfaceText use UniqueElement //////////
	
	function InterfaceText() { /* Call to this.construct from subclasses */ }
	
	InterfaceText.prototype = Object.assign({}, UniqueElement, {
		construct: function(values, labelText, placeholderMessage, $cont) {
			this.defineUniq();
			if( $.type(values) != 'object' ) {
				this.data = {};
				this.data = values;
			} else {
				this.data = values;
				if( typeof this.data == 'undefined' ) {
					this.data = '';
				}
			}
			this.numberOtherLanguages = Object.keys( this.data ).length - 1;
			this.label = labelText + (this.useColon ? message('colon') : '');
			this.placeholder = placeholderMessage;
			this.$cont = $cont.addClass('tde-it');;
			this.createContent();
			this.createInputs();
			this.hideOther();
		},
		
		getClasses: function(lang) {
			return 'tde-it-lang tde-it-lang-' + (lang || contentLanguage);
		},
		
		createInputs: function() {
			var i;
			for( i in this.data ) {
				this.createInput(i, i == contentLanguage);
			}
		},
		
		onChange: function(domInput) {
			this.data[
				/(^|\s)tde-it-lang-(\S+)(\s|$)/.exec( $(domInput).closest('.tde-it-lang').attr('class') )
			] = domInput.value;
		},
		
		input: function($input, lang) {
			var that = this;
			return $input
				.val( this.data )
				.addClass('tde-it-input')
				.attr('placeholder', messageLang(this.placeholder, lang))
				.change(function(){
					that.onChange(this);
				});
		},
		
		getPlaceholder: function(lang) {
			return messageLang(this.placeholder, lang);
		},
		
		hideOther: function() {
			this.$expand.show();
			this.$collapse.hide();
			this.$cont.find('.tde-it-lang').each(function(){
				if(
					! $(this).hasClass('tde-it-lang-'+contentLanguage)
					&& ! $(this).hasClass('tde-it-lang-'+userLanguage)
				) {
					$(this).hide();
				}
			});
			this.$add.hide();
		},
		
		showOther: function() {
			this.$expand.hide();
			this.$collapse.show();
			this.$cont.find('.tde-it-lang').css('display', ''); // .show() does .css('display', 'inline'), but here we need inline-block, as declared in the stylesheet
			this.$add.show();
		},
		
		updateNumber: function() {
			this.$expand.attr('title', message('it-otherlanguages-show', this.numberOtherLanguages));
			this.$expand.find('img').attr('alt', message('it-otherlanguages-show', this.numberOtherLanguages));
			this.$collapse.attr('title', message('it-otherlanguages-hide', this.numberOtherLanguages));
			this.$collapse.find('img').attr('alt', message('it-otherlanguages-hide', this.numberOtherLanguages));
		},
		
		actionSuffix: '',
		useColon: true,
		
		createToggleLinks: function(){
			var that = this;
			
			this.$expand = action({
				type: 'expand' + this.actionSuffix,
				aClass: 'tde-it-expand',
				fn: function(){
					that.showOther();
					return false;
				}
			});
			
			this.$collapse = action({
				type: 'collapse' + this.actionSuffix,
				aClass: 'tde-it-collapse',
				fn: function(){
					that.hideOther();
					return false;
				}
			});
			
			this.updateNumber();
			return $().add(this.$collapse).add(this.$expand);
		},
		
		createAddLink: function() {
			var that = this;
			this.$add = action({
				type: 'add',
				desc: message('it-add'),
				fn: function(){
					that.addInput();
				},
				aClass: 'tde-add-language'
			});
			return this.$add;
		},
		
		getLangDiv: function(lang) {
			var res;
			this.$cont.find('.tde-it-lang').each(function(){
				if( $(this).hasClass('tde-it-lang-'+lang) ) {
					res = $(this);
					return false;
				}
			});
			return res;
		},
		
		getValueInput: function(lang) {
			return this.getLangDiv(lang).find('.tde-it-input');
		},
		
		renameElement: function(from, to) {
			if( this.data.hasOwnProperty(to) ) {
				if( to != from ) {
					throw userError('error-name-already-used');
				}
			} else if( this.data.hasOwnProperty(from) ) {
				this.getLangDiv(from).attr('class', this.getClasses(to));
				this.getValueInput(to).attr('placeholder', this.getPlaceholder(to)); // "this.getValueInput(to)": "to" because the class have been modified by the line before
				this.data = this.data;
				delete this.data;
			} else {
				throw scriptError('error-it-lang-inexistent', from);
			}
		},
		
		removeElement: function(lang) {
			if( this.data.hasOwnProperty(lang) ) {
				delete this.data;
				this.getLangDiv(lang).remove();
			} else {
				throw scriptError('error-it-lang-inexistent', lang);
			}
		},
		
		$removeLang: function(readonly) {
			var that = this;
			if( readonly ) {
				return null;
			} else {
				return action({
					type: 'remove',
					desc: message('it-remove'),
					fn: function(){
						var $lang = $(this).closest('.tde-it-lang');
						try {
							that.removeElement( $lang.find('.tde-renamer-name-input').val() );
							$lang.remove();
						} catch(e) {
							alertError(e);
						}
					}
				});
			}
		},
		
		untitledId: 0,
		
		addInput: function() {
			this.untitledId++;
			var lang = '{' + this.untitledId + '}';
			this.data = '';
			this.createInput(lang, false);
		},
		
		getData: function() {
			noCurlyBraceKey( this.data );
			if( Object.keys(this.data).length == 1 ) {
				return this.data || undefined;
			} else {
				// TODO delete empty strings
				return this.data;
			}
		},
		
		/* abstract */ createContent: function() { },
		/* abstract */ createInput: function(lang, readonly) { }
	});
	
	////////// class InterfaceTextBlock extends InterfaceText //////////
	
	function InterfaceTextBlock() {
		this.construct.apply(this, arguments);
		this.$cont.addClass('tde-it-block');
	}
	
	InterfaceTextBlock.prototype = Object.assign(new InterfaceText(), {
		createContent: function() {
			
			this.$tbody = $('<tbody>');
			
			var $caption = $('<caption>')
				.text(this.label)
				.append(this.createToggleLinks());
			
			this.$cont
				.append($('<table>')
					.append( $caption )
					.append( this.$tbody )
				)
				.append( this.createAddLink() )
				.append( $clear() );
			
		},
		
		useColon: false,
		
		createInput: function(lang, readonly) {
			this.$tbody.append($('<tr>')
				.attr('class', this.getClasses(lang))
				.append($('<th>')
					.attr('scope', 'row')
					.append( new Renamer(null, lang, this, readonly).getNode() )
				)
				.append($('<td>')
					.append( this.input($('<textarea>'), lang) )
				)
				.append($('<td>')
					.append( this.$removeLang(readonly) )
				)
			);
		}
	});
	
	////////// class InterfaceTextInline extends InterfaceText //////////
	
	function InterfaceTextInline() {
		this.construct.apply(this, arguments);
		this.$cont.addClass('tde-it-inline');
	}
	
	InterfaceTextInline.prototype = Object.assign(new InterfaceText(), {
		createContent: function() {
			
			this.$span = $('<span>');
			
			this.$cont
				.text( this.label )
				.append( this.$span )
				.append( this.createAddLink() )
				.append( this.createToggleLinks() );
			
		},
		
		actionSuffix: '_inline',
		
		createInput: function(lang, readonly) {
			this.$span.append($('<span>')
				.attr('class', this.getClasses(lang))
				.append( new Renamer(null, lang, this, readonly).getNode() )
				.append( document.createTextNode( message('colon') ) )
				.append( this.input($('<input>').attr('type', 'text'), lang) )
				.append( this.$removeLang(readonly) )
			);
		}
	});
	
	////////// trait DataForm //////////
	
	DataForm = {
		
		getCont: function( name ) {
			return this;
		},
		
		newline: function(cont) {
			this.getCont(cont).append('<br>');
			return this;
		},
		
		addInput: function(cont, type, key, label) {
			var
				$cont = this.getCont(cont),
				that = this,
				inputClass = 'tde-' + this.type + '-' + key,
				labelClass = inputClass + '-label',
				id = inputClass + '-' + this.uniq,
				$input;
			
			function addLabel(colon) {
				$cont
					.append($('<label>')
						.addClass(labelClass)
						.attr('for', id)
						.text(
							label
							+ ( type == 'array' ? message('use-pipes') : '' )
							+ ( colon ? message('colon') : '' ) )
					);
			}
			
			function addInput() {
				$cont
					.append( $input );
			}
			
			switch(type) {
				case 'text':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'text',
							id: id
						})
						.val( this.data || '' )
						.change(function(){ that.change(key, this.value); });
					addLabel(true);
					addInput();
					break;
				case 'array':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'text',
							id: id
						})
						.val( arrToStr( this.data ||  ) )
						.change(function(){ that.changeArray(key, this.value); });
					addLabel(true);
					addInput();
					break;
				case 'checkbox':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'checkbox',
							id: id
						})
						.prop('checked', this.data)
						.change(function(){ that.change(key, this.checked); });
					addInput();
					addLabel(false);
					break;
				case 'typeSelect':
					$input = $('<select>')
						.addClass(inputClass)
						.attr('type', 'text')
						.attr('id', id)
						.append( Param.type('unknown', this.data.type) )
						.append( Param.type('number', this.data.type) )
						.append( Param.type('string', this.data.type) )
						.append( Param.type('string/line', this.data.type) )
						.append( Param.type('string/wiki-user-name', this.data.type) )
						.append( Param.type('string/wiki-page-name', this.data.type) )
						.change(function(){ that.changeType(this); });
					addLabel(true);
					addInput();
					break;
			}
			this = $input;
			return this;
		},
		
		addDescription: function(cont) {
			var $div = $('<div>');
			
			this.getCont(cont).append($div);
			
			this.description = new InterfaceTextBlock(
				this.data.description || '',
				message(this.type + '-description'),
				this.type + '-description-placeholder',
				$div
			);
			
			return this;
		},
		
		addLabel: function(cont) {
			var $span = $('<span>');
			
			this.getCont(cont).append($span);
			
			this.label = new InterfaceTextInline(
				this.data.label || '',
				message(this.type + '-label'),
				this.type + '-label-placeholder',
				$span
			);
			
			return this;
		},
		
		change: function(key, newValue) {
			if( key == 'deprecated' ) {
				this.data = newValue || false;
			} else {
				this.data = newValue;
			}
		},
		
		changeArray: function(key, str) {
			this.change(key, strToArr(str));
		},
		
		changeType: function(domSelect) {
			this.change('type', selectValue(domSelect));
		}
		
	};
	
	////////// class Param uses UniqueElement, DataForm //////////
	
	function Param(params, name, $list, data) {
		this.defineUniq();
		
		var
			that = this;
		
		this.params = params;
		this.name = name;
		this.$cont = $('<li>');
		this.data = data;
		
		this.$head = $('<div>');
		this.$body = $('<div>');
		
		this.$expand = action({type: 'expand', desc: message('expand'), fn: function() { that.expand(); }, aClass: 'tde-param-expand'});
		this.$collapse = action({type: 'collapse', desc: message('collapse'), fn: function() { that.collapse(); }, aClass: 'tde-param-collapse'});
		
		if(
			( data.required || data.description == '' )
			&& ! data.inherits
		) {
			this.expand();
		} else {
			this.collapse();
		}
		
		this.$cont
			.append( action({type: 'remove', desc: message('param-remove'), fn: function() { that.remove(); }, aClass: 'tde-remove-line'}) )
			.append( this.$expand )
			.append( this.$collapse )
			.append( this.$head )
			.append( this.$body )
			.append( $clear() );
		
		this.$head
			.append($('<label>')
				.attr('for', 'tde-paramName-'+this.uniq)
				.text( message('param-name') + message('colon') )
			)
			.append( (new Renamer('tde-paramName-'+this.uniq, name, params)).getNode() );
		
		this
			.addInput('head', 'checkbox', 'required', message('param-required'))
			.addInput('head', 'text', 'inherits', message('param-inherits'))
			.addLabel('body')
			.newline('body')
			.addInput('body', 'typeSelect', 'type', message('param-type'))
			.addInput('body', 'text', 'default', message('param-default'))
			.newline('body')
			.addInput('body', 'text', 'deprecated', message('param-deprecated'))
			.newline('body')
			.addInput('body', 'array', 'aliases', message('param-aliases'))
			.addDescription('body');
		
		$list.append( this.$cont );
	}
	
	Param.prototype = Object.assign({}, UniqueElement, DataForm, {
		
		type: 'param',
		
		expand: function() {
			this.$collapse.show();
			this.$expand.hide();
			this.$body.show();
		},
		
		collapse: function() {
			this.$collapse.hide();
			this.$expand.show();
			this.$body.hide();
		},
		
		remove: function() {
			try {
				this.params.removeElement(this.name);
				this.$cont.remove();
			} catch(e) {
				alertError( e );
			}
		},
		
		getCont: function( name ) {
			return this;
		},
		
		getData: function() {
			this.data.label = this.label.getData();
			this.data.description = this.description.getData();
			return this.data;
		}
	});
	
	Param.type = function(type, currentType) {
		return $('<option>')
			.val(type)
			.text( message('param-type-' + type) )
			.prop('selected', currentType == type);
	};
	
	////////// class Params //////////
	
	function Params($list, data) {
		this.$list = $list;
		this.data = data;
		this.params = {};
		
		var i, that = this;
		
		for( i in data ) {
			this.createLi(i);
		}
		
		this.$list.first().after(
			action({type: 'add', fn: function(){that.addItem();return false;}, desc: message('param-add'), aClass: 'tde-add-line'})
		);
	}
	
	Params.prototype = {
		
		untitledId: 0,
		
		addItem: function() {
			this.untitledId++;
			var name = '{' + this.untitledId + '}';
			this.data = {};
			this.createLi(name);
		},
		
		createLi: function(name) {
			this.params = new Param(this, name, this.$list, this.data);
		},
		
		renameElement: function(from, to) {
			if( this.data.hasOwnProperty(to) ) {
				if( to != from ) {
					throw userError('error-name-already-used');
				}
			} else if( this.data.hasOwnProperty(from) ) {
				this.data = this.data;
				this.params = this.params;
				delete this.data;
				delete this.params;
			} else {
				throw scriptError('error-name-inexistent', from);
			}
		},
		
		removeElement: function(name) {
			if( this.data.hasOwnProperty(name) ) {
				delete this.data;
				delete this.params;
			} else {
				throw scriptError('error-name-inexistent', name);
			}
		},
		
		getData: function() {
			for( var i in this.data ) {
				this.data = this.params.getData();
			}
			noCurlyBraceKey( this.data );
			return this.data;
		}
	};
	
	////////// class Set uses UniqueElement, DataForm //////////
	
	function Set(sets, $list, data) {
		this.defineUniq();
		
		var
			that = this;
		
		this.sets = sets;
		this.$cont = $('<li>');
		this.data = data;
		
		this.$body = $('<div>');
		
		this.$cont
			.append( action({type: 'remove', desc: message('set-remove'), fn: function() { that.remove(); }, aClass: 'tde-remove-line'}) )
			.append( this.$body )
			.append( $clear() );
		
		this
			.addLabel('body')
			.newline('body')
			.addInput('body', 'array', 'params', message('set-params'));
		
		$list.append( this.$cont );
	}
	
	Set.prototype = Object.assign({}, UniqueElement, DataForm, {
		
		type: 'set',
		
		remove: function() {
			try {
				this.sets.removeElement(this);
				this.$cont.remove();
			} catch(e) {
				alertError( e );
			}
		},
		
		getData: function() {
			this.data.label = this.label.getData();
			return this.data;
		}
	});
	
	Param.type = function(type, currentType) {
		return $('<option>')
			.val(type)
			.text( message('param-type-' + type) )
			.prop('selected', currentType == type);
	};
	
	////////// class Sets //////////
	
	function Sets($list, data) {
		this.$list = $list;
		this.data = data;
		this.sets = ;
		
		var i, that = this;
		
		for(i = 0; i<data.length; i++ ) {
			this.createLi(i);
		}
		
		this.$list.first().after(
			action({type: 'add', fn: function(){that.addItem();return false;}, desc: message('set-add'), aClass: 'tde-add-line'})
		);
	}
	
	Sets.prototype = {
		
		addItem: function() {
			this.data.push({});
			this.createLi(this.data.length-1);
		},
		
		createLi: function(i) {
			this.sets = new Set(this, this.$list, this.data);
		},
		
		removeElement: function(set) {
			var i = this.sets.indexOf(set);
			if( i >= 0 ) {
				arrayRemoveElement(this.data, i);
				arrayRemoveElement(this.sets, i);
			} else {
				throw scriptError('error-set-inexistent', name);
			}
		},
		
		getData: function() {
			for(var i=0; i<this.data.length; i++) {
				this.data = this.sets.getData();
			}
			return this.data;
		}
	};
	
	////////// class TemplateData //////////
	
	function TemplateData(data) {
		
		this.data = data;
		this.cleanData();
		
		ui.clear();
		
		ui.$title
			.text( message('title') )
			.append(document.createTextNode(' ('))
			.append($('<a>')
				.attr('href', documentationLink() )
				.text( message('title-documentation') )
			)
			.append(document.createTextNode(')'));
		
		this.dataToUi();
		
		ui.open();
	}
	
	TemplateData.regexpDouble = /<templatedata*>(*)<\/templatedata>/;
	TemplateData.regexpSimple = /<templatedata*\/>/;
	
	TemplateData.prototype = {
		
		cleanData: function() {
			if( typeof this.data.description == 'undefined' ) this.data.description = '';
			if( $.type(this.data.params) != 'object' ) this.data.params = {};
			if( $.type(this.data.sets) != 'array' ) this.data.sets = ;
		},
		
		dataToUi: function() {
			this.descriptionToUi();
			this.paramsToUi();
			this.setsToUi();
		},
		
		descriptionToUi: function() {
			var $div = $('<div>').attr('id', 'tde-desc-cont');
			
			ui.$body
				.append($('<h3>')
					.text( message('section-description') )
				)
				.append($div);
			
			this.description = new InterfaceTextBlock(this.data.description, message('description'), 'description-placeholder', $div);
		},
		
		paramsToUi: function() {
			var $list = $('<ul>');
			
			ui.$body.append($('<h3>')
					.text( message('section-params') )
				)
				.append($list);
			
			this.params = new Params($list, this.data.params);
		},
		
		setsToUi: function() {
			var $list = $('<ul>');
			
			ui.$body.append($('<h3>')
					.text( message('section-sets') )
				)
				.append($list);
			
			this.sets = new Sets($list, this.data.sets);
		},
		
		getData: function() {
			this.data.description = this.description.getData();
			this.data.params = this.params.getData();
			this.data.sets = this.sets.getData();
			if( this.data.sets.length == 0 ) {
				delete this.data.sets;
			}
			return this.data;
		}
	};
	
	///////// Starting /////////
	
	function write() {
		try {
			var
				newContent = '<templatedata>\n' + JSON.stringify(td.getData(), null, indent) + '\n</templatedata>',
				text = getText();
		
			switch(matchType) {
				case 1:
					text = text.replace(regExpOneTag, newContent);
					break;
				case 2:
					text = text.replace(regExpTwoTags, newContent);
					break;
			}
			
			setText(text);
			ui.close();
		} catch(e) {
			alertError(e);
		}
	}
	
	function startWithData(data) {
		td = new TemplateData(data);
		ui.addCancelButton();
		ui.addButton('apply', write);
	}
	
	function startWithTds(text) {
		var
			T = mw.config.get('wgPageName').replace('_', ' ').split('/'),
			i,
			page = '',
			$preload = $('<div>').attr('id', 'tde-preload'),
			$select = $('<select>').attr('id', 'tde-preload-select');
		
		function $option(val, _label) {
			var label = _label || val;
			return $('<option>')
				.val( val )
				.text( label );
		}
		
		for(i=0; i<T.length; i++) {
			page += (page ? '/' : '') + T;
			T = page;
		}
		
		$select.append( $option('', message('preload-none')) );
		
		for(i=T.length-1; i>-1; i-- ) {
			$select.append( $option( T ) );
		}
		
		ui.clear();
		
		ui.$title.text( message('preload-data') );
		
		$preload
			.append( $('<label>')
				.attr('for', 'tde-preload-select')
				.text( message('preload-select') + message('colon') )
			)
			.append($select);
		
		ui.$body.html( $preload );
		
		ui.addCancelButton();
		ui.addButton('preload-load', function(){
			
			var template = selectValue($select);
			
			if( ! template ) {
				startWithData({});
			} else if( template == T ) { // The current template
				startWithData(
					TemplateDataSkeletonFromText(text)
				);
			} else {
				$preload
					.text( message('preload-running') )
					.addClass('tde-preload-loading');
			
				ui.deleteButtons();
			
				TemplateDataSkeleton(template, startWithData);
			}
			
		});
		
		ui.open();
	}
	
	function startTDE() {
		var
			text = getText(),
			content,
			data = null;
		
		if( regExpTwoTags.test(text) ) {
			matchType = 2;
			content = regExpTwoTags.exec( text );
			indent = getIndent(content);
			
			if( /^\s*(\{\s*\})?\s*$/.test(content) ) {
				startWithTds(text);
			} else {
				try {
					data = JSON.parse( content );
				} catch(e) {
					data = null;
				}
			
				if( data != null ) {
					startWithData(data);
				} else {
					alertError(userError('parse-error'));
				}
			}
			
		} else if( regExpOneTag.test(text) ) {
			matchType = 1;
			indent = defaultIndent;
			startWithTds(text);
		} else {
			matchType = 0;
			alertError(userError('no-data'));
		}
		
	}
	
	////////// Add a link to start TDE //////////
	
	function addTdeLink() {
		var
			$img = $('<img>').attr('alt', message('start-tde')),
			$link = $('<a>')
				.attr({
					href: '#',
					title: message('start-tde')
				})
				.append($img)
				.click(function(){
					startTDE();
					return false;
				});
		
		if( mw.user.options.get('usebetatoolbar') ) {
			$img.attr('src', '//upload.wikimedia.org/wikipedia/commons/d/d8/TemplateData_-_Icon_-_Beta_toolbar.png');
			mw.loader.using('ext.wikiEditor', function(){
				$('#wikiEditor-ui-toolbar .section-main .group-insert').before($('<div>')
					.addClass('group group-tde')
					.append($link)
				);
			});
		} else {
			$img.attr('src', '//upload.wikimedia.org/wikipedia/commons/6/63/TemplateData_-_Icon_-_Old_toolbar.png');
			$('#toolbar').append($link);
		}
	}
	
	addTdeLink();
	
	/*
	$(
		mw.util.addPortletLink('p-tb', '#', 'TemplateData', 'tde-toolbox', message('toolbox-label'))
	).click(function(){
		startTDE();
		return false;
	});
	*/
}

if( .indexOf( mw.config.get('wgNamespaceNumber') ) !== -1 && .indexOf( mw.config.get('wgAction') ) !== -1 ) {
	mw.loader.load(
		'//fr.wikipedia.org/w/index.php?title=Utilisateur:Ltrlg/styles/TemplateDataEditor.css&action=raw&ctype=text/css',
		'text/css'
	);
	mw.loader.using('mediawiki.util', function () {
		$(document).ready(TemplateDataEditor);
	});
}

// </syntaxhighlight> {{catégorisation JS|TemplateDataEditor}}