MediaWiki:Gadget-Accessibility.js

Une page de Wikipédia, l'encyclopédie libre.
Note : après avoir enregistré la page, vous devrez forcer le rechargement complet du cache de votre navigateur pour voir les changements.

Mozilla / Firefox / Konqueror / Safari : maintenez la touche Majuscule (Shift) en cliquant sur le bouton Actualiser (Reload) ou pressez Maj-Ctrl-R (Cmd-R sur Apple Mac) ;

Chrome / Internet Explorer / Opera : maintenez la touche Ctrl en cliquant sur le bouton Actualiser ou pressez Ctrl-F5.
/**
 * Outils de vérification de l'accessibilité des pages.
 * Ajoute une palette de liens, classiquement à gauche.
 * Auteurs : Lgd, Ltrlg
 * {{Projet:JavaScript/Script|Accessibility}}
 */
/* eslint-env browser */
/* jshint esversion: 6 */
/* global $, mw, OO */

( function () {

var
	supportedSkins = [ 'vector', 'monobook', 'timeless', 'vector-2022' ],
	portletId = 'p-accessibility',
	storageId = 'gadget-accessibility',
	messages;

// Literal non-breaking space, for situations where HTML entities can't be used
const NBSP = '\xA0';

/* ----- Translations ----- */

messages = {
	fr: {
		'accessibility-all-disable': 'Tout désactiver',
		'accessibility-alt-empty': 'ALT VIDE',
		'accessibility-alt-flag': 'L’alternative devrait commencer par « Drapeau… » ou « Pavillon… »',
		'accessibility-alt-gallery': 'Indiquez l’alternative de chaque image de la galerie.',
		'accessibility-alt-label': 'Alternatives',
		'accessibility-alt-link': 'Vérifiez si le lien sur l’image peut être désactivé (LINK vide) (ou si l’image peut être transformée en thumb)',
		'accessibility-alt-long': 'Vérifiez si cette alternative de $1 caractères pourrait être plus concise (environ 120' + NBSP + 'caractères au plus) et si l’alternative actuelle pourrait être transférée dans la section description de la page de l’image.',
		'accessibility-alt-missing': 'ALT MANQUANT',
		'accessibility-alt-title': 'Vérifier la présence des alternatives textuelles',
		'accessibility-alt-usemap': 'Indiquez l’alternative de l’image.',
		'accessibility-disclaimer': 'Soyez prudent dans vos modifications et n’hésitez pas à <a href="//fr.wikipedia.org/wiki/Discussion_Wikip%C3%A9dia:Atelier_accessibilit%C3%A9">demander conseil</a>.',
		'accessibility-good-practice-label': 'Bonnes pratiques',
		'accessibility-good-practice-page': 'Wikipédia:Atelier accessibilité/Bonnes pratiques',
		'accessibility-good-practice-title': 'Bonnes pratiques d’accessibilité (nouvelle fenêtre)',
		'accessibility-headers-dl': 'Possible élément de liste de définition utilisé à la place d’un titre de section',
		'accessibility-headers-h1': 'H1',
		'accessibility-headers-h2': 'H2',
		'accessibility-headers-h3': 'H3',
		'accessibility-headers-h4': 'H4',
		'accessibility-headers-h5': 'H5',
		'accessibility-headers-h6': 'H6',
		'accessibility-headers-label': 'Titres',
		'accessibility-headers-title': 'Vérifier l’accessibilité des titres de section',
		'accessibility-help-label': 'Aide gadget',
		'accessibility-help-page': 'Wikipédia:Atelier accessibilité/Aide gadget',
		'accessibility-help-title': 'Aide pour l’utilisation du gadget (nouvelle fenêtre)',
		'accessibility-lang-dk': 'Code de langue erroné' + NBSP + ': utilisez da (danois)',
		'accessibility-lang-empty': 'Code de langue erroné' + NBSP + ': lang ne doit pas être vide',
		'accessibility-lang-foreign-latn': 'Vérifiez que ce qui suit est bien une transcription en caractères latins (et non une traduction)' + NBSP + ':',
		'accessibility-lang-foreign-missing': 'Code de langue manquant pour une citation en langue étrangère',
		'accessibility-lang-gr': 'Code de langue erroné' + NBSP + ': utilisez el (grec moderne) ou grc (grec ancien avant 1453)',
		'accessibility-lang-jp': 'Code de langue erroné' + NBSP + ': utilisez ja (japonais)',
		'accessibility-lang-label': 'Langues',
		'accessibility-lang-lang': 'Lang' + NBSP + ': $1',
		'accessibility-lang-lat': 'Code de langue erroné' + NBSP + ': utilisez la (latin)',
		'accessibility-lang-lu': 'Code de langue erroné' + NBSP + ': utilisez lb (luxembourgeois)',
		'accessibility-lang-mul': 'Code de langue erroné' + NBSP + ': ne pas utiliser le code mul (multiple)',
		'accessibility-lang-oci': 'Code de langue erroné' + NBSP + ': utilisez oc (occitan)',
		'accessibility-lang-title': 'Vérifier la présence des changements de langue',
		'accessibility-lang-und': 'Code de langue erroné' + NBSP + ': ne pas utiliser le code und (undefined)',
		'accessibility-lists-dd': 'Définition' + NBSP + ': ',
		'accessibility-lists-dl': 'Vérifiez le bon usage et la structure de cette liste de définition.',
		'accessibility-lists-dt': 'Terme défini' + NBSP + ': ',
		'accessibility-lists-header': 'Possible élément de liste de définition utilisé à la place d’un titre de section',
		'accessibility-lists-indent': 'Possible élément de liste de définition utilisé uniquement pour indenter le texte',
		'accessibility-lists-label': 'Listes',
		'accessibility-lists-orphean': 'Élément de liste potentiellement orphelin' + NBSP + ': vérifier l’absence de sauts de ligne entre les *',
		'accessibility-lists-title': 'Vérifier l’accessibilité des listes de définition',
		'accessibility-portlet-collapse-title': 'Réduire',
		'accessibility-portlet-expand-title': 'Étendre',
		'accessibility-portlet-fix-title': 'Fixer le gadget',
		'accessibility-portlet-title': 'Accessibilité',
		'accessibility-portlet-unfix-title': 'Détacher',
		'accessibility-quotes-label': 'Citations',
		'accessibility-quotes-ok': 'Citation bien balisée' + NBSP + ': ',
		'accessibility-quotes-title': 'Vérifier l’accessibilité des citations',
		'accessibility-tables-caption-error': 'Ce tableau ne devrait pas avoir de titre caption (code wiki' + NBSP + ': |+)</caption>',
		'accessibility-tables-caption-missing': 'Ce tableau devrait avoir un titre caption (code wiki' + NBSP + ': |+)</caption>',
		'accessibility-tables-imbrication': 'Limitez autant que possible l’imbrication de tableaux.',
		'accessibility-tables-label': 'Tableaux',
		'accessibility-tables-summary': '@summary=$1',
		'accessibility-tables-td-headers': 'HEADERS: $1',
		'accessibility-tables-td-scope': 'ERREUR TD SCOPE: $1',
		'accessibility-tables-th-id': 'ID: $1',
		'accessibility-tables-th-missing-scope': 'Vérifiez s’il faut ajouter un SCOPE ou transformer en TD (code wiki' + NBSP + ': |)',
		'accessibility-tables-th-scope': 'SCOPE: $1',
		'accessibility-tables-th-scope-headers': 'SCOPE: $1 HEADERS: $2',
		'accessibility-tables-th-scope-headers-id': 'SCOPE: $1 HEADERS: $2 ID: $3',
		'accessibility-tables-th-scope-id': 'SCOPE: $1 ID: $2',
		'accessibility-tables-title': 'Vérifier l’accessibilité des tableaux'
	}
};

function selectApplicableMessages() {
	var
		defaultLang = 'fr',
		currentLang = mw.config.get( 'wgUserLanguage' );

	mw.messages.set( messages[ defaultLang ] );

	if ( currentLang !== defaultLang && currentLang in messages ) {
		mw.messages.set( messages[ currentLang ] );
	}
}

/* ----- Storage ----- */

function loadOptions() {
	var res = mw.storage.getObject( storageId );

	return $.isPlainObject( res ) ? res : {};
}

function storeOptions( options ) {
	mw.storage.setObject( storageId, options );
}

function storeBooleanState( name, val ) {
	var options = loadOptions();
	options[ name ] = val;
	storeOptions( options );
}

function loadBooleanState( name ) {
	var options = loadOptions();
	return Object.prototype.hasOwnProperty.call( options, name ) ?
		options[ name ] :
		null;
}

/* ----- Fixed or not ----- */

/** Add a state control to a portlet.
 *
 * @param {object} description Options bag with the following properties:
 *   - `$portlet`: jQuery handler for the portlet;
 *   - `$controls`: jQuery handler for the controls bar;
 *   - `id`: storage id, partial state class id;
 *   - `enableIcon': OOUI icon for enabling;
 *   - `enableTitle`: hover text for enabling;
 *   - `disableIcon': OOUI icon for disabling;
 *   - `disableTitle`: hover text for disabling;
 */
function addStateControlToPortlet( description ) {
	var
		disabledData = {
			title: description.enableTitle,
			icon: description.enableIcon
		},
		enabledData = {
			title: description.disableTitle,
			icon: description.disableIcon
		},

		enabled = loadBooleanState( description.id ),
		currentData = enabled ? enabledData : disabledData,

		button = new OO.ui.ButtonWidget( {
			framed: false,
			icon: currentData.icon,
			title: currentData.title
		} ),

		className = 'acc_' + description.id;

	if ( enabled ) {
		$( '#' + portletId ).addClass( className );
	}

	button.on( 'click', function () {
		var currentData;
		description.$portlet.toggleClass( className );
		enabled = !enabled;
		currentData = enabled ? enabledData : disabledData;
		storeBooleanState( description.id, enabled );

		button.setTitle( currentData.title );
		button.setIcon( currentData.icon );
	} );

	description.$controls.append( button.$element );
}

function addControlsToPortlet( $portlet ) {
	var $controls = $( '<span>' ).attr( 'id', 'acc_controls' );

	addStateControlToPortlet( {
		$portlet: $portlet,
		$controls: $controls,
		id: 'collapsed',
		enableTitle: mw.msg( 'accessibility-portlet-collapse-title' ),
		enableIcon: 'collapse',
		disableTitle: mw.msg( 'accessibility-portlet-expand-title' ),
		disableIcon: 'expand'
	} );

	addStateControlToPortlet( {
		$portlet: $portlet,
		$controls: $controls,
		id: 'fixed',
		enableTitle: mw.msg( 'accessibility-portlet-fix-title' ),
		enableIcon: 'window',
		disableTitle: mw.msg( 'accessibility-portlet-unfix-title' ),
		disableIcon: 'close'
	} );

	if ( ( /vector/ ).test( mw.config.get( 'skin' ) ) === true ) {
		$portlet.find( '.vector-menu-heading' ).before( $controls );
	} else {
		$portlet.find( 'h3' ).before( $controls );
	}
}

/* ----- Portlet creation ----- */

function addDisclaimerToPortlet( $portlet ) {
	var $disc = $( '<p>' )
		.attr( 'id', 'acc_disclaimer' )
		.html( mw.msg( 'accessibility-disclaimer' ) );
	$portlet.find( 'ul' ).after( $disc );
}

function createPortlet( id, title ) {
	var $portlet;
	switch ( mw.config.get( 'skin' ) ) {
		case 'vector-2022':
		case 'vector':
			$portlet = $( '<div>' )
				.attr( { id: id, 'class': 'vector-menu mw-portlet mw-portlet-accessibility vector-menu-portal portal' } )
				.append( $( '<div>' )
					.attr( 'class', 'vector-menu-heading' )
					.append( $( '<span>' )
						.attr( 'class', 'vector-menu-heading-label' )
						.text( title )
					)
				)
				.append( $( '<div>' )
					.attr( 'class', 'vector-menu-content body' )
					.append( $( '<ul>' )
						.attr( 'class', 'vector-menu-content-list' )
					)
				);
			$( '#p-navigation' ).after( $portlet );
			break;


		case 'timeless':
			$portlet = $( '<div>' )
				.attr( { id: id, 'class': 'mw-portlet', role: 'navigation', 'aria-labelledby': id + '-label' } )
				.append( $( '<h3>' )
					.attr( { id: id + '-label' } )
					.text( title )
				)
				.append( $( '<div>' )
					.attr( 'class', 'mw-portlet-body' )
					.append( $( '<ul>' ) )
				);
			$( '#p-navigation' ).after( $portlet );
			break;

		case 'monobook':
			$portlet = $( '<div>' )
				.attr( { id: id, 'class': 'portlet' } )
				.append( $( '<h3>' ).text( title ) )
				.append( $( '<div>' ).attr( 'class', 'pBody' )
					.append( $( '<ul>' ) )
				);
			$( '#p-search' ).after( $portlet );
			break;

		default:
			throw Error( 'Unsupported skin' );
	}
	return $portlet;
}

/* ----- CSS manipulation ----- */
/* We want to keep the code in CSS for now, but make strings translatable…
 * Let’s build a tiny CSS superset for that…
 * We will replace `«msg-name, args…»` (because french quotes are so easy for
 * french people) with `"msg with " arg "."`
 */

function replacesMessagesinCss( extendedCss ) {
	return extendedCss.replace( /«([^»]+)»/g, function ( _, cont ) {
		var
			parts = cont.split( /,\s*/ ),
			msgName = parts[ 0 ],
			msg = mw.msg( msgName ),
			// Obviously, we may need escaping at some point…
			wrappedMessage = '"' + msg + '"',
			finalString = wrappedMessage.replace(
				/\$(\d)/g,
				function ( _, sIndex ) {
					// Exit the string, then add our value.
					return '"' + parts[ Number( sIndex ) ] + '"';
				}
			);
		return finalString;
	} );
}

/* ----- Individual options ----- */

/** Create a option link in the portlet.
 *
 * @param {object} description is an option bag with the following properties:
 *    - [req] `id`: the storage identifier & link identifier;
 *    - [req] `title`: title for the portlet link;
 *    - [req] `label`: label for the portlet link;
 *    - [opt] `activate`: function executed for activating the option;
 *    - [opt] `deactivate`: function executed for deactivating the option.
 *    - [opt] `styles`: extended CSS used by this option (see
 *      `replacesMessagesinCss`).
 *
 *  @return {function} a function to change the state.
 *  Assuming you ran `var setActive = createOptionLink(…)`, you would:
 *    - run `setActive(true)` to activate the option;
 *    - run `setActive(false)` to deactivate the option.
 */
function createOptionLink( description ) {
	var
		active = loadBooleanState( description.id ),

		$portlet = $( mw.util.addPortletLink(
			/*   portlet */ portletId,
			/*      href */ '#',
			/*      text */ description.label,
			/*        id */ 'acc_' + description.id,
			/*   tooltip */ description.title,
			/* accesskey */ undefined,
			/*  nextnode */ undefined
		) );

	if ( description.styles ) {
		$( mw.loader.addStyleTag( replacesMessagesinCss( description.styles ) ) )
			// People should be able to see were styles come from
			.attr( 'id', 'accessibility-option-' + description.id );
	}

	function getPageContentElement( $content = null ) {
		if ( $content ) {
			if ( $content.is( '.mw-parser-output' ) ) {
				// $content est déjà l'élément .mw-parser-output, [[phab:T349298]]
				return $content;
			} else {
				// ça peut ne pas trouver d'élément, notamment si c'est
				// une page de modification n'ayant pas encore de prévisualisation
				return $content.find( '.mw-parser-output' );
			}
		} else {
			// ça peut ne pas trouver d'élément, notamment si c'est
			// une page de modification n'ayant pas encore de prévisualisation
			return $( '#mw-content-text' ).find( '.mw-parser-output' );
		}
	}

	function runCallback( cb, $pageContent ) {
		if ( cb ) {
			cb( $pageContent );
		}
	}

	function activate( $pageContent ) {
		if ( $pageContent.length ) {
			$pageContent.addClass( 'acc_' + description.id );
			runCallback( description.activate, $pageContent );
		}
	}

	function deactivate( $pageContent ) {
		if ( $pageContent.length ) {
			$pageContent.removeClass( 'acc_' + description.id );
			runCallback( description.deactivate, $pageContent );
		}
	}

	function setActive( newState ) {
		if ( newState !== active ) {
			active = newState;
			storeBooleanState( description.id, active );
			var $pageContent = getPageContentElement();
			if ( active ) {
				$portlet.addClass( 'acc_on' );
				activate( $pageContent );
			} else {
				$portlet.removeClass( 'acc_on' );
				deactivate( $pageContent );
			}
		}
	}

	mw.hook( 'wikipage.content' ).add( function ( $content ) {
		if ( active ) {
			var $pageContent = getPageContentElement( $content );
			activate( $pageContent );
		}
	} );

	$portlet.click( function ( e ) {
		e.preventDefault();
		setActive( !active );
	} );

	return setActive;
}

function createTableOption() {
	return createOptionLink( {
		id: 'table',
		title: mw.msg( 'accessibility-tables-title' ),
		label: mw.msg( 'accessibility-tables-label' ),

		activate: function ( $pageContent ) {
			$pageContent.find( 'table:has(th[scope])' )
				.not( ':has(caption)' )
				.prepend( $( '<caption>' )
					.attr( 'class', 'error acc_caption_error' )
					.text( mw.msg( 'accessibility-tables-caption-missing' ) )
				);

			$pageContent.find( 'table:has(caption)' )
				.not( ':has(th[scope])' )
				.prepend( $( '<caption>' )
					.attr( 'class', 'error acc_caption_error' )
					.text( mw.msg( 'accessibility-tables-caption-error' ) )
				);
		},

		deactivate: function ( $pageContent ) {
			$pageContent.find( 'caption.acc_caption_error' ).remove();
		},

		styles: `
			.acc_table table {
				border: 2px solid #FE7707 !important;
			}

			.acc_table table caption {
				display: table-caption !important;
				visibility: visible !important;
				position: static !important;
				height: auto !important;
				width: auto !important;
				font-size: 1em !important;
				line-height: 1em !important;
				text-indent: 0 !important;
				background-color: lightgreen !important;
			}
			.acc_table table caption.error {
				background: transparent !important;
				font-weight: bold !important;
			}
			.acc_table table[summary]:before {
				background-color:#FFFF99 !important;
				border: 1px solid #FFCC66 !important;
				color:#000000 !important;
				opacity:0.9 !important;
				font-size: 10px !important;
				line-height: 12px !important;
				padding: 2px 5px !important;
				content: «accessibility-tables-summary»;
			}
			.acc_table table th {
				background-color: red !important;
			}
			.acc_table table th:before {
				content: «accessibility-tables-th-missing-scope»;
				display: block;
				color: red !important;
				background: #fff !important
			}
			.acc_table table th[scope]:before,
			.acc_table table th[id]:before {
				display: none;
			}
			.acc_table table th[id] {
				background-color: lime !important;
			}
			.acc_table table th[scope] {
				background-color: lime !important;
			}
			.acc_table table th[scope]:after {
				content: "SCOPE " attr(scope);
				color: darkgreen;
				display: block;
			}
			.acc_table table th[id]:after {
				content: «accessibility-tables-th-id, attr(id)»;
				display: block;
				color: darkgreen;
			}
			.acc_table table th[scope][id]:after {
				content: «accessibility-tables-th-scope-id,
					attr(scope),
					attr(id)»;
				display: block;
				color: darkgreen;
			}
			.acc_table table th[scope][headers]:after {
				content: «accessibility-tables-th-scope-headers,
					attr(scope),
					attr(HEADERS)»;
				display: block;
				color: darkgreen;
			}
			.acc_table table th[scope][headers][id]:after {
				content: «accessibility-tables-th-scope-headers-id,
					attr(scope),
					attr(HEADERS),
					attr(ID)»;
				display: block;
				color: darkgreen;
			}

			.acc_table table td[headers] {
				background-color: yellow !important;
			}
			.acc_table table td[headers]:after {
				content: «accessibility-tables-td-headers, attr(HEADERS)»;
				display: block;
				color: darkgreen;
			}

			.acc_table table td[scope] {
				background-color: red !important;
			}
			.acc_table table td[scope]:after {
				content: «accessibility-tables-td-scope, attr(scope)»;
				display: block;
			}

			.acc_table table table {
				border: 5px solid red !important;
			}
			.acc_table table table:before {
				content: «accessibility-tables-imbrication»;
				font-weight: bold;
				color: red;
				background: #fff;
				display: block;
				padding: 5px;
			}

			.acc_table table col {
				width: auto !important;
			}
		`
	} );
}

function createHeaderOption() {
	return createOptionLink( {
		id: 'header',
		title: mw.msg( 'accessibility-headers-title' ),
		label: mw.msg( 'accessibility-headers-label' ),

		styles: `
			/* Headers*/
			.acc_header h1,
			.acc_header h2,
			.acc_header h3,
			.acc_header h4,
			.acc_header h5,
			.acc_header h6 {
				border: 3px solid green !important;
				padding: 3px !important;
				color: #000 !important;
			}
			.acc_header h1:before {
				content: «accessibility-headers-h1» " ";
			}
			.acc_header h2 {
				margin-left: 15px !important;
			}
			.acc_header h2:before {
				content: «accessibility-headers-h2» " ";
			}
			.acc_header h3 {
				margin-left: 30px !important;
			}
			.acc_header h3:before {
				content: «accessibility-headers-h3» " ";
			}
			.acc_header h4 {
				margin-left: 45px !important;
			}
			.acc_header h4:before {
				content: «accessibility-headers-h4» " ";
			}
			.acc_header h5 {
				margin-left: 60px !important;
			}
			.acc_header h5:before {
				content: «accessibility-headers-h5» " ";
			}
			.acc_header h6 {
				margin-left: 75px !important;
			}
			.acc_header h6:before {
				content: «accessibility-headers-h6» " ";
			}

			.acc_header dl dt:only-child:after {
				content: «accessibility-headers-dl»;
				color: red;
				margin: 5px;
				padding: 0 5px;
				border: 1px solid red;
			}
		`
	} );
}

function createListOption() {
	return createOptionLink( {
		id: 'list',
		title: mw.msg( 'accessibility-lists-title' ),
		label: mw.msg( 'accessibility-lists-label' ),

		styles: `
			/* listes */

			.acc_list dl,
			.acc_list dl dt {
				border: 1px solid #FE7707;
			}
			.acc_list dt:before {
				content: «accessibility-lists-dt»;
				color: #FE7707;
			}
			.acc_list dd:before {
				content: «accessibility-lists-dd»;
				color: #FE7707;
			}
			.acc_list dl:before {
				content: «accessibility-lists-dl»;
				color: #FE7707;
				font-weight: bold;
				display: block;
				margin: 5px;
			}
			.acc_list dl dt:only-child:after {
				content: «accessibility-lists-header»;
				color: red !important;
				margin: 5px;
				padding: 0 5px;
				border: 1px solid red;
			}
			.acc_list dl dd:only-child:before {
				content: «accessibility-lists-indent»;
				color: red !important;
				margin: 5px;
				padding: 0 5px;
				border: 1px solid red;
				font-weight: bold;
			}

			.acc_list ul + ul li:only-child {
				border: 1px solid red;
			}
			.acc_list ul + ul li:only-child:before {
				background-color: #fff !important;
				border: 1px solid red !important;
				color: red !important;
				padding: 2px 5px !important;
				content: «accessibility-lists-orphean»;
				display: block;
			}
			`
	} );
}

function createQuoteOption() {
	return createOptionLink( {
		id: 'quote',
		title: mw.msg( 'accessibility-quotes-title' ),
		label: mw.msg( 'accessibility-quotes-label' ),

		styles: `
			/* Quotes */
			.acc_quote .citation,
			.acc_quote blockquote {
				border: 1px solid green;
				background: lightgreen !important;
			}
			.acc_quote .citation:before,
			.acc_quote blockquote:before {
				content: «accessibility-quotes-ok»;
				color: Darkgreen ! important;
				font-weight: bold;
				padding: 5px;
			}
			`
	} );
}

function createLangOption() {
	return createOptionLink( {
		id: 'lang',
		title: mw.msg( 'accessibility-lang-title' ),
		label: mw.msg( 'accessibility-lang-label' ),

		styles: `
			.acc_lang *[lang] {
				border: 1px solid darkgreen;
			}
			.acc_lang *[lang]:after {
				content: «accessibility-lang-lang, attr(lang)» " ";
				border: 1px solid darkgreen;
				padding: 2px 5px;
				margin: 0 5px;
				background: yellow;
			}
			.acc_lang *[lang]:before {
				color: red;
			}

			.acc_lang *[lang=lat]:before {
				content: «accessibility-lang-lat» " ";
			}
			*[lang=gr]:before,
			*[lang=gr-Latn]:before,
			*[lang=gr-latn]:before {
				content: «accessibility-lang-gr» " ";
			}
			.acc_lang *[lang=oci]:before {
				content: «accessibility-lang-oci» " ";
			}
			.acc_lang *[lang=lu]:before {
				content: «accessibility-lang-lu» " ";
			}
			.acc_lang *[lang=jp]:before {
				content: «accessibility-lang-jp» " ";
			}
			.acc_lang *[lang=dk]:before {
				content: «accessibility-lang-dk» " ";
			}
			.acc_lang *[lang=mul]:before {
				content: «accessibility-lang-mul» " ";
			}
			.acc_lang *[lang=und]:before {
				content: «accessibility-lang-und» " ";
			}
			.acc_lang *[lang=""]:before {
				content: «accessibility-lang-empty» " ";
			}

			/* Modèle Citation étrangère */
			.acc_lang .not_fr_quote {
				border: 1px solid darkgreen;
			}
			.acc_lang .not_fr_quote:before {
				color: red;
				content: «accessibility-lang-foreign-missing» " ";
			}
			.acc_lang .not_fr_quote[lang]:not([lang=""]):before {
				content: "";
			}
			.acc_lang .transcription:before {
				content: «accessibility-lang-foreign-latn» " ";
			}
		`
	} );
}

/* ----- Alternatives (a bit longer…) ----- */

function missingAltMessage() {
	return $( '<span>' )
		.attr( 'class', 'error' )
		.text( mw.msg( 'accessibility-alt-missing' ) );
}

function $text( text ) {
	return $( document.createTextNode( text ) );
}

function joinJqArray( arr ) {
	var res, i;
	switch ( arr.length ) {
		case 0:
			return $( [] );
		case 1:
			return arr[ 0 ];
		default:
			res = arr[ 0 ];
			for ( i = 1; i < arr.length; ++i ) {
				res = res
					.add( $( '<br>' ) )
					.add( arr[ i ] );
			}
			return res;
	}
}

function getMessageForImage( $img, $parent, $grandparent, $great_grandparent ) {
	var
		alt = $img.attr( 'alt' ) || '',
		parts = [];

	if ( alt === '' ) {
		if ( !$great_grandparent.is( '.bandeau-icone' ) ) {
			if ( $parent.is( 'a' ) ) {
				parts.push( missingAltMessage() );
			} else {
				parts.push( $text( mw.msg( 'accessibility-alt-empty' ) ) );
			}
		}
	} else {
		parts.push( $text( alt ) );
		if ( alt.length > 120 ) {
			parts.push( $( '<span>' )
				.attr( 'class', 'error' )
				.text( mw.msg( 'accessibility-alt-long', alt.length ) )
			);
		}
	}

	if ( !$grandparent.is( 'figure[typeof~="mw:File/Thumb"]' ) ) {
		if ( /\.(png|svg|jpg|gif)$/i.test( alt ) ) {
			return missingAltMessage();
		}
		if ( $parent.is( '.mw-file-description' ) && !$great_grandparent.is( '.smiley' ) ) {
			if ( $great_grandparent.is( '.flagicon' ) ) {
				if ( !/^(Drapeau|Pavillon)/i.test( alt ) ) {
					parts.push( $( '<span>' )
						.attr( 'class', 'error' )
						.text( mw.msg( 'accessibility-alt-flag' ) )
					);
				}

			} else {
				parts.push( $( '<span>' )
					.attr( 'class', 'error' )
					.text( mw.msg( 'accessibility-alt-link' ) )
				);
			}
		}
	}

	return joinJqArray( parts );
}

function processSingleImageForAlternative( $img ) {
	var
		$parent = $img.parent(),
		$grandparent = $parent.parent(),
		$great_grandparent = $grandparent.parent(),
		$msg, $wrapped;

	$msg = getMessageForImage( $img, $parent, $grandparent, $great_grandparent );

	if ( $msg.length < 1 ) {
		return;
	}

	$wrapped = $( '<span>' )
		.attr( 'class', 'acc_attr_show' )
		.append( $msg );

	// override du "line-height: 0" hérité de l'élément figure
	if ( $grandparent.is( 'figure[typeof~="mw:File/Thumb"]' ) ) {
		$wrapped.css( 'line-height', '1.4em' );
	}

	if ( $parent.is( 'a' ) ) {
		$parent.before( $wrapped );
	} else {
		$img.before( $wrapped );
	}
}

function insertBeforeElements( $targets, $element ) {
	var i;

	try {
		$targets.eq( 0 ).before( $element );
		for ( i = 1; i < $targets.length; ++i ) {
			$targets.eq( i ).before( $element.clone );
		}
	} catch (e) {
		// avoid TypeError: this.map is not a function (seen in VisualEditor)
	}
}

function createAltOption() {
	return createOptionLink( {
		id: 'alt',
		title: mw.msg( 'accessibility-alt-title' ),
		label: mw.msg( 'accessibility-alt-label' ),

		activate: function ( $pageContent ) {
			$pageContent.find( 'img' ).each( function () {
				processSingleImageForAlternative( $( this ) );
			} );

			// TODO Are these double spans necessary?

			insertBeforeElements(
				$pageContent.find( 'ul.gallery' ),
				$( '<span>' )
					.attr( 'class', 'acc_attr_show' )
					.append( $( '<span>' )
						.attr( 'class', 'error' )
						.text( mw.msg( 'accessibility-alt-gallery' ) )
					)
			);

			insertBeforeElements(
				$pageContent.find( 'img[usemap]' ),
				$( '<span>' )
					.attr( 'class', 'acc_attr_show' )
					.append( $( '<span>' )
						.attr( 'class', 'error' )
						.text( mw.msg( 'accessibility-alt-usemap' ) )
					)
			);
		},

		deactivate: function ( $pageContent ) {
			$pageContent.find( 'span.acc_attr_show' ).remove();
		}
	} );
}

/* ----- Global links ----- */

function createGlobalDisabler( optionsActivators ) {
	var portlet = mw.util.addPortletLink(
		/*   portlet */ portletId,
		/*      href */ '#',
		/*      text */ mw.msg( 'accessibility-all-disable' ),
		/*        id */ 'acc_all',
		/*   tooltip */ undefined,
		/* accesskey */ undefined,
		/*  nextnode */ undefined
	);

	$( portlet ).click( function ( e ) {
		e.preventDefault();
		optionsActivators.forEach( function ( setActive ) {
			setActive( false );
		} );
	} );
}

function createNewWindowPortletLink( description ) {
	var portlet = mw.util.addPortletLink(
		/*   portlet */ portletId,
		/*      href */ mw.util.getUrl( description.page ),
		/*      text */ description.label,
		/*        id */ description.id,
		/*   tooltip */ description.title,
		/* accesskey */ undefined,
		/*  nextnode */ undefined
	);
	$( portlet ).find( 'a' ).attr( 'target', '_blank' );
}

/* ----- Startup ----- */

function runAccessibilityGadget( $ ) { // eslint-disable-line no-unused-vars
	var $portlet, optionsActivators;

	if ( !supportedSkins.includes( mw.config.get( 'skin' ) ) ) { return; }

	mw.loader.load(
		'//fr.wikipedia.org/w/index.php?title=MediaWiki:Gadget-Accessibility.css&action=raw&ctype=text/css',
		'text/css'
	);

	selectApplicableMessages();

	$portlet = createPortlet(
		portletId,
		mw.msg( 'accessibility-portlet-title' )
	);
	addControlsToPortlet( $portlet );
	addDisclaimerToPortlet( $portlet );

	optionsActivators = [
		createTableOption(),
		createHeaderOption(),
		createListOption(),
		createQuoteOption(),
		createLangOption(),
		createAltOption()
	];

	createGlobalDisabler( optionsActivators );

	createNewWindowPortletLink( {
		id: 'acc_bp',
		page: mw.msg( 'accessibility-good-practice-page' ),
		title: mw.msg( 'accessibility-good-practice-title' ),
		label: mw.msg( 'accessibility-good-practice-label' )
	} );

	createNewWindowPortletLink( {
		id: 'acc_help',
		page: mw.msg( 'accessibility-help-page' ),
		title: mw.msg( 'accessibility-help-title' ),
		label: mw.msg( 'accessibility-help-label' )
	} );
}

if ( mw.config.get( 'wgNamespaceNumber' ) >= 0 ) {
	mw.loader.using( [ 'mediawiki.storage', 'oojs-ui-core' ], function () {
		$( runAccessibilityGadget );
	} );
}

} )();