User:WillowW/Footnote.php

<?php
if ( ! defined( 'MEDIAWIKI' ) )
	die();
/**#@+
 * A parser extension that adds two tags, <note> and <notes/> 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 <cite> definition in HTML
 * @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite <cite> definition in XHTML 2.0
 *
 *
 *
 * @author WillowW <[email protected]>, based on Cite.php by Ævar Arnfjörð Bjarmason <[email protected]>
 * @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 <note[ name=id]> and <notes/> 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 <note> input, in the format of:
		 * <code>
		 * array(
		 * 	'user supplied' => array(
		 *		'text' => 'user supplied footnote & key',
		 *		'count' => 1, // occurs twice
		 * 		'number' => 1, // The first footnote, we want
		 * 		               // all occurrences of it to
		 * 		               // use the same number
		 *	),
		 *	0 => 'Anonymous footnote',
		 *	1 => 'Another anonymous footnote',
		 *	'some key' => array(
		 *		'text' => 'this one occurs once'
		 *		'count' => 0,
		 * 		'number' => 4
		 *	),
		 *	3 => 'more stuff'
		 * );
		 * </code>
		 *
		 * 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 <note> or <notes> tag is being processed.
		 * Used to avoid infinite recursion
		 * 
		 * @var boolean
		 */
		var $mInFootnote = false;
		
		/**#@-*/

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

		/**#@+ @access private */

		/**
		 * Callback function for <note>
		 *
		 * @param string $str Input
		 * @param array $argv Arguments
		 * @return string
		 */
		function note( $str, $argv, $parser ) {
			wfLoadExtensionMessages( 'Footnote' );
			if ( $this->mInFootnote ) {
				return htmlspecialchars( "<note>$str</note>" );
			} 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 === '' ) {
				# <note ...></note>.  This construct is always invalid: either
				# it's a contentful note, or it's a named duplicate and should
				# be <note ... />.
				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 <note />; 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 <note> 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 <note>
		 *
		 * @param string $str Input from the <note> tag
		 * @param mixed $key Argument to the <note> 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 <notes>
		 *
		 * @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( "<notes/>" );
				} else {
					return htmlspecialchars( "<notes>$str</notes>" );
				}
			} 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 <notes>, not <note>
		 * (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 <note>, not <notes>
		 * (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 <note> 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 <note> or
					// <notes> 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 <p> 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( '~^<p>\s*~', '', $text );
				$text = preg_replace( '~\s*</p>\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(
					'<strong class="error">' .
					wfMsg( 'footnote_error', wfMsg( $key, $param ) ) .
					'</strong>'
				);
		}

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

/**#@-*/

Content Disclaimer

Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.

  1. The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
  2. There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
  3. It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
  4. Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
  5. Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.