User:WillowW/Footnote.php

<?php if ( ! defined( 'MEDIAWIKI' ) ) die; /**#@+ * A parser extension that adds two tags, and for adding * explanatory footnotes (NB! *not* citations to references) to pages * * @addtogroup Extensions * * @link http://meta.wikimedia.org/wiki/Footnote/Footnote.php Documentation, based on http://meta.wikimedia.org/wiki/Cite/Cite.php Documentation * @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE definition in HTML * @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite definition in XHTML 2.0 * * * * @author WillowW , based on Cite.php by Ævar Arnfjörð Bjarmason  * @copyright Copyright © 2005, 2008 Ævar Arnfjörð Bjarmason, WillowW * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */

$wgExtensionFunctions[] = 'wfFootnote'; $wgExtensionCredits['parserhook'][] = array(	'name' => 'Footnote',	'version' => preg_replace('/^.* (\d\d\d\d-\d\d-\d\d) .*$/', '\1', '$LastChangedDate$'), #just the date of the last change	'author' => 'WillowW, based on Ævar Arnfjörð Bjarmason's work; thanks, big Æ!',	'description' => 'Adds  and tags, for explanatory footnotes', // kept for b/c	'descriptionmsg' => 'footnote_desc',	'url' => 'http://www.mediawiki.org/wiki/Extension:Footnote/Footnote.php' ); $wgParserTestFiles[] = dirname( __FILE__ ). "/footnoteParserTests.txt"; $wgExtensionMessagesFiles['Footnote'] = dirname( __FILE__ ). "/Footnote.i18n.php";

function wfFootnote { class Footnote { /**#@+		 * @access private */		/**		 * Datastructure representing input, in the format of: * 		 *		 * This works because: * * PHP's datastructures are guaranteed to be returned in the *  order that things are inserted into them (unless you mess		 *   with that) * * User supplied keys can't be integers, therefore avoiding *  conflict with anonymous keys *		 * @var array **/		var $mFootnotes = array; /**		 * Count for user displayed output (note[1], note[2], ...) *		 * @var int */		var $mOutCnt = 0;

/**		 * Internal counter for anonymous footnotes, separate from * $mOutCnt because anonymous footnotes won't increment it, * but will incremement $mOutCnt *		 * @var int */		var $mInCnt = 0;

/**		 * The backlinks, in order, to pass as $3 to		 * 'footnote_references_link_many_format', defined in		 * 'footnote_references_link_many_format_backlink_labels *		 * @var array */		var $mBacklinkLabels; /**		 * @var object */		var $mParser; /**		 * True when a or tag is being processed. * Used to avoid infinite recursion * 		 * @var boolean */		var $mInFootnote = false; /**#@-*/

/**		 * Constructor */		function Footnote { $this->setHooks; }

/**#@+ @access private */

/**		 * Callback function for *		 * @param string $str Input * @param array $argv Arguments * @return string */		function note( $str, $argv, $parser ) { wfLoadExtensionMessages( 'Footnote' ); if ( $this->mInFootnote ) { return htmlspecialchars( " $str " ); } else { $this->mInFootnote = true; $ret = $this->guardedNote( $str, $argv, $parser ); $this->mInFootnote = false; return $ret; }		}		function guardedNote( $str, $argv, $parser ) { $this->mParser = $parser; # The key here is the "name" attribute. $key = $this->noteArg( $argv ); if( $str === '' ) { # . This construct is always invalid: either # it's a contentful note, or it's a named duplicate and should # be . return $this->error( 'footnote_error_ref_no_input' ); }			if( $key === false ) { # TODO: Comment this case; what does this condition mean? return $this->error( 'footnote_error_ref_too_many_keys' ); }

if( $str === null and $key === null ) { # Something like ; this makes no sense. return $this->error( 'footnote_error_ref_no_key' ); }			if( preg_match( '/^[0-9]+$/', $key ) ) { # Numeric names mess up the resulting id's, potentially produ- # cing duplicate id's in the XHTML. The Right Thing To Do				# would be to mangle them, but it's not really high-priority # (and would produce weird id's anyway). return $this->error( 'footnote_error_ref_numeric_key' ); }			if( is_string( $key ) or is_string( $str ) ) { # We don't care about the content: if the key exists, the note # is presumptively valid. Either it stores a new note, or re- # fers to an existing one. If it refers to a nonexistent note, # we'll figure that out later. Likewise it's definitely valid # if there's any content, regardless of key. return $this->stack( $str, $key ); }

# Not clear how we could get here, but something is probably # wrong with the types. Let's fail fast. $this->croak( 'footnote_error_key_str_invalid', serialize( "$str; $key" ) ); }

/**		 * Parse the arguments to the tag *		 * @static *		 * @param array $argv The argument vector * @return mixed false on invalid input, a string on valid *              input and null on no input */		function noteArg( $argv ) { $cnt = count( $argv ); if ( $cnt > 1 ) // There should only be one key return false; else if ( $cnt == 1 ) if ( isset( $argv['name'] ) ) // Key given. return $this->validateName( array_shift( $argv ) ); else // Invalid key return false; else // No key return null; }		/**		 * Since the key name is used in an XHTML id attribute, it must * conform to the validity rules. The restriction to begin with * a letter is lifted since footnotes have their own prefix. *		 * @fixme merge this code with the various section name transformations * @fixme double-check for complete validity * @return string if valid, false if invalid */		function validateName( $name ) { if( preg_match( '/^[A-Za-z0-9:_.-]*$/i', $name ) ) { return $name; } else { // WARNING: CRAPPY CUT AND PASTE MAKES BABY JESUS CRY $text = urlencode( str_replace( ' ', '_', $name ) ); $replacearray = array(					'%3A' => ':',					'%' => '.'				); return str_replace(					array_keys( $replacearray ),					array_values( $replacearray ),					$text ); }		}

/**		 * Populate $this->mFootnotes based on input and arguments to 		 * * @param string $str Input from the tag * @param mixed $key Argument to the tag as returned by $this->noteArg * @return string */		function stack( $str, $key = null ) { if ( $key === null ) { // No key $this->mFootnotes[] = $str; return $this->linkNote( $this->mInCnt++ ); } else if ( is_string( $key ) ) // Valid key if ( ! isset( $this->mFootnotes[$key] ) || ! is_array( $this->mFootnotes[$key] ) ) { // First occurrence $this->mFootnotes[$key] = array(						'text' => $str,						'count' => 0,						'number' => ++$this->mOutCnt					); return $this->linkNote(							$key,							$this->mFootnotes[$key]['count'],							$this->mFootnotes[$key]['number']						); } else { // We've been here before if ( $this->mFootnotes[$key]['text'] === null && $str !== '' ) { // If no text found before, use this text $this->mFootnotes[$key]['text'] = $str; };					return $this->linkNote(							$key,							++$this->mFootnotes[$key]['count'],							$this->mFootnotes[$key]['number']						); } else $this->croak( 'footnote_error_stack_invalid_input', serialize( array( $key, $str ) ) ); }		/**		 * Callback function for *		 * @param string $str Input * @param array $argv Arguments * @return string */		function footnotes( $str, $argv, $parser ) { wfLoadExtensionMessages( 'Footnote' ); if ( $this->mInFootnote ) { if ( is_null( $str ) ) { return htmlspecialchars( " " ); } else { return htmlspecialchars( " $str " ); }			} else { $this->mInFootnote = true; $ret = $this->guardedFootnotes( $str, $argv, $parser ); $this->mInFootnote = false; return $ret; }		}		function guardedFootnotes( $str, $argv, $parser ) { $this->mParser = $parser; if ( $str !== null ) return $this->error( 'footnote_error_references_invalid_input' ); else if ( count( $argv ) ) return $this->error( 'footnote_error_references_invalid_parameters' ); else return $this->footnotesFormat; }

/**		 * Make output to be returned from the footnotes function *		 * @return string XHTML ready for output */		function footnotesFormat { if ( count( $this->mFootnotes ) == 0 ) return ''; wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ .'-entries' ); $ent = array; foreach ( $this->mFootnotes as $k => $v ) $ent[] = $this->footnotesFormatEntry( $k, $v ); $prefix = wfMsgForContentNoTrans( 'footnote_references_prefix' ); $suffix = wfMsgForContentNoTrans( 'footnote_references_suffix' ); $content = implode( "\n", $ent ); wfProfileOut( __METHOD__ .'-entries' ); wfProfileIn( __METHOD__ .'-parse' ); // Live hack: parse adds two newlines on WM, can't reproduce it locally -ævar $ret = rtrim( $this->parse( $prefix. $content. $suffix ), "\n" ); wfProfileOut( __METHOD__ .'-parse' ); wfProfileOut( __METHOD__ ); return $ret; }

/**		 * Format a single entry for the footnotesFormat function *		 * @param string $key The key of the note * @param mixed $val The value of the note, string for anonymous *                  notes, array for user-supplied * @return string Wikitext */		function footnotesFormatEntry( $key, $val ) { // Anonymous note if ( ! is_array( $val ) ) return wfMsgForContentNoTrans(						'footnote_references_link_one',						$this->footnotesKey( $key ),						$this->noteKey( $key ),						$val					); else if ($val['text']=='') return wfMsgForContentNoTrans(						'footnote_references_link_one',						$this->footnotesKey( $key ),						$this->noteKey( $key, $val['count'] ),						$this->error( 'footnote_error_references_no_text', $key )					); // Standalone named note, I want to format this like an // anonymous note because displaying "1. 1.1 Ref text" is			// overkill and users frequently use named footnotes when they // don't need them for convenience else if ( $val['count'] === 0 ) return wfMsgForContentNoTrans(						'footnote_references_link_one',						$this->footnotesKey( $key ),						$this->noteKey( $key, $val['count'] ),						( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) )					); // Named footnotes with >1 occurrences else { $links = array;

for ( $i = 0; $i <= $val['count']; ++$i ) { $links[] = wfMsgForContentNoTrans(							'footnote_references_link_many_format',							$this->noteKey( $key, $i ),							$this->footnotesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),							$this->footnotesFormatEntryAlternateBacklinkLabel( $i )					); }

$list = $this->listToText( $links );

return wfMsgForContentNoTrans( 'footnote_references_link_many',						$this->footnotesKey( $key ),						$list,						( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) )					); }		}

/**		 * Generate a numeric backlink given a base number and an * offset, e.g. $base = 1, $offset = 2; = 1.2 * Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100 *		 * @static *		 * @param int $base The base * @param int $offset The offset * @param int $max Maximum value expected. * @return string */		function footnotesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) { global $wgContLang; $scope = strlen( $max ); $ret = $wgContLang->formatNum( $offset ); return $ret; }

/**		 * Generate a custom format backlink given an offset, e.g.		 * $offset = 2; = c if $this->mBacklinkLabels = array( 'a',		 * 'b', 'c', ...). Return an error if the offset > the # of		 * array items *		 * @param int $offset The offset *		 * @return string */		function footnotesFormatEntryAlternateBacklinkLabel( $offset ) { if ( !isset( $this->mBacklinkLabels ) ) { $this->genBacklinkLabels; }			if ( isset( $this->mBacklinkLabels[$offset] ) ) { return $this->mBacklinkLabels[$offset]; } else { // Feed me! return $this->error( 'footnote_error_references_no_backlink_label' ); }		}

/**		 * Return an id for use in wikitext output based on a key and * optionally the # of it, used in, not * (since otherwise it would link to itself) *		 * @static *		 * @param string $key The key * @param int $num The number of the key * @return string A key for use in wikitext */		function noteKey( $key, $num = null ) { $prefix = wfMsgForContent( 'footnote_reference_link_prefix' ); $suffix = wfMsgForContent( 'footnote_reference_link_suffix' ); if ( isset( $num ) ) $key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num ); return $prefix. $key. $suffix; }

/**		 * Return an id for use in wikitext output based on a key and * optionally the # of it, used in, not * (since otherwise it would link to itself) *		 * @static *		 * @param string $key The key * @param int $num The number of the key * @return string A key for use in wikitext */		function footnotesKey( $key, $num = null ) { $prefix = wfMsgForContent( 'footnote_references_link_prefix' ); $suffix = wfMsgForContent( 'footnote_references_link_suffix' ); if ( isset( $num ) ) $key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num ); return $prefix. $key. $suffix; }

/**		 * Generate a link (<sup ...) for the element from a key * and return XHTML ready for output *		 * @param string $key The key for the link * @param int $count The # of the key, used for distinguishing *                  multiple occurrences of the same key * @param int $label The label to use for the link, I want to		 *                  use the same label for all occurrences of *                  the same named reference. * @return string */		function linkNote( $key, $count = null, $label = null ) { global $wgContLang;

return $this->parse(					wfMsgForContentNoTrans( 'footnote_reference_link', $this->noteKey( $key, $count ), $this->footnotesKey( $key ), $wgContLang->formatNum( is_null( $label ) ? $this->footnotesFormatEntryAlternateBacklinkLabel( ++$this->mOutCnt ) : $label ) )				);		}

/**		 * This does approximately the same thing as		 * Language::listToText but due to this being used for a		 * slightly different purpose (people might not want, as the		 * first separator and not 'and' as the second, and this has to		 * use messages from the content language) I'm rolling my own. *		 * @static *		 * @param array $arr The array to format * @return string */		function listToText( $arr ) { $cnt = count( $arr );

$sep = wfMsgForContentNoTrans( 'footnote_references_link_many_sep' ); $and = wfMsgForContentNoTrans( 'footnote_references_link_many_and' );

if ( $cnt == 1 ) // Enforce always returning a string return (string)$arr[0]; else { $t = array_slice( $arr, 0, $cnt - 1 ); return implode( $sep, $t ). $and. $arr[$cnt - 1]; }		}

/**		 * Parse a given fragment and fix up Tidy's trail of blood on * it... *		 * @param string $in The text to parse * @return string The parsed text */		function parse( $in ) { if ( method_exists( $this->mParser, 'recursiveTagParse' ) ) { // New fast method return $this->mParser->recursiveTagParse( $in ); } else { // Old method $ret = $this->mParser->parse(					$in,					$this->mParser->mTitle,					$this->mParser->mOptions,					// Avoid whitespace buildup					false,					// Important, otherwise $this->clearState					// would get run every time or					// is called, fucking the whole					// thing up.					false				); $text = $ret->getText; return $this->fixTidy( $text ); }		}

/**		 * Tidy treats all input as a block, it will e.g. wrap most * input in if it isn't already, fix that and return the fixed text *		 * @static *		 * @param string $text The text to fix * @return string The fixed text */		function fixTidy( $text ) { global $wgUseTidy;

if ( ! $wgUseTidy ) return $text; else { $text = preg_replace( '~^ \s*~', '', $text ); $text = preg_replace( '~\s* \s*~', '', $text ); $text = preg_replace( '~\n$~', '', $text ); return $text; }		}

/**		 * Generate the labels to pass to the * 'footnote_references_link_many_format' message, the format is an		 * arbitary number of tokens separated by [\t\n ] */		function genBacklinkLabels { wfProfileIn( __METHOD__ ); $text = wfMsgForContentNoTrans( 'footnote_references_link_many_format_backlink_labels' ); $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text ); wfProfileOut( __METHOD__ ); }

/**		 * Gets run when Parser::clearState gets run, since we don't		 * want the counts to transcend pages and other instances */		function clearState { $this->mOutCnt = $this->mInCnt = 0; $this->mFootnotes = array;

return true; }

/**		 * Initialize the parser hooks */		function setHooks { global $wgParser, $wgHooks; $wgParser->setHook( 'note', array( &$this, 'note' ) ); $wgParser->setHook( 'notes', array( &$this, 'footnotes' ) );

$wgHooks['ParserClearState'][] = array( &$this, 'clearState' ); }

/**		 * Return an error message based on an error ID		 * * @param string $key  Message name for the error * @param string $param Parameter to pass to the message * @return string XHTML ready for output */		function error( $key, $param=null ) { # We rely on the fact that PHP is okay with passing unused argu- # ments to functions. If $1 is not used in the message, wfMsg will # just ignore the extra parameter. return $this->parse(					' ' .					wfMsg( 'footnote_error', wfMsg( $key, $param ) ) .					' '				); }

/**		 * Die with a backtrace if something happens in the code which * shouldn't have *		 * @param int $error ID for the error * @param string $data Serialized error data */		function croak( $error, $data ) { wfDebugDieBacktrace( wfMsgForContent( 'footnote_croak', $this->error( $error ), $data ) ); }

/**#@-*/	}

new Footnote; }

/**#@-*/