and for adding * citations to pages * * @ingroup Extensions * * @link http://www.mediawiki.org/wiki/Extension: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 * * @bug 4579 * * @author Ævar Arnfjörð Bjarmason * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ class Cite { /**#@+ * @access private */ /** * Datastructure representing input, in the format of: * * array( * 'user supplied' => array( * 'text' => 'user supplied reference & key', * 'count' => 1, // occurs twice * 'number' => 1, // The first reference, we want * // all occourances of it to * // use the same number * ), * 0 => 'Anonymous reference', * 1 => 'Another anonymous reference', * 'some key' => array( * 'text' => 'this one occurs once' * 'count' => 0, * 'number' => 4 * ), * 3 => 'more stuff' * ); * * * 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 $mRefs = array(); /** * Count for user displayed output (ref[1], ref[2], ...) * * @var int */ var $mOutCnt = 0; var $mGroupCnt = array(); /** * Counter to track the total number of (useful) calls to either the * ref or references tag hook */ var $mCallCnt = 0; /** * The backlinks, in order, to pass as $3 to * 'cite_references_link_many_format', defined in * 'cite_references_link_many_format_backlink_labels * * @var array */ var $mBacklinkLabels; /** * The links to use per group, in order. * * @var array */ var $mLinkLabels = array(); /** * @var Parser */ var $mParser; /** * True when the ParserAfterParse hook has been called. * Used to avoid doing anything in ParserBeforeTidy. * * @var boolean */ var $mHaveAfterParse = false; /** * True when a tag is being processed. * Used to avoid infinite recursion * * @var boolean */ var $mInCite = false; /** * True when a tag is being processed. * Used to detect the use of to define refs * * @var boolean */ var $mInReferences = false; /** * Error stack used when defining refs in * * @var array */ var $mReferencesErrors = array(); /** * Group used when in block * * @var string */ var $mReferencesGroup = ''; /** * call stack * Used to cleanup out of sequence ref calls created by #tag * See description of function rollbackRef. * * @var array */ var $mRefCallStack = array(); /** * Did we install us into $wgHooks yet? * @var Boolean */ static protected $hooksInstalled = false; /**#@+ @access private */ /** * Callback function for * * @param $str string Input * @param $argv array Arguments * @param $parser Parser * * @return string */ function ref( $str, $argv, $parser ) { global $wgCiteEnablePopups; if ( $this->mInCite ) { return htmlspecialchars( "$str" ); } else { $this->mCallCnt++; $this->mInCite = true; $ret = $this->guardedRef( $str, $argv, $parser ); $this->mInCite = false; $parserOutput = $parser->getOutput(); $parserOutput->addModules( 'ext.cite' ); if ( $wgCiteEnablePopups ) { $parserOutput->addModules( 'ext.cite.popups' ); } $parserOutput->addModuleStyles( 'ext.rtlcite' ); return $ret; } } /** * @param $str string Input * @param $argv array Arguments * @param $parser Parser * @param $default_group string * @return string */ function guardedRef( $str, $argv, $parser, $default_group = CITE_DEFAULT_GROUP ) { $this->mParser = $parser; # The key here is the "name" attribute. list( $key, $group, $follow ) = $this->refArg( $argv ); # Split these into groups. if ( $group === null ) { if ( $this->mInReferences ) { $group = $this->mReferencesGroup; } else { $group = $default_group; } } # This section deals with constructions of the form # # # BAR # # if ( $this->mInReferences ) { if ( $group != $this->mReferencesGroup ) { # and have conflicting group attributes. $this->mReferencesErrors[] = $this->error( 'cite_error_references_group_mismatch', htmlspecialchars( $group ) ); } elseif ( $str !== '' ) { if ( !isset( $this->mRefs[$group] ) ) { # Called with group attribute not defined in text. $this->mReferencesErrors[] = $this->error( 'cite_error_references_missing_group', htmlspecialchars( $group ) ); } elseif ( $key === null || $key === '' ) { # calls inside must be named $this->mReferencesErrors[] = $this->error( 'cite_error_references_no_key' ); } elseif ( !isset( $this->mRefs[$group][$key] ) ) { # Called with name attribute not defined in text. $this->mReferencesErrors[] = $this->error( 'cite_error_references_missing_key', $key ); } else { # Assign the text to corresponding ref $this->mRefs[$group][$key]['text'] = $str; } } else { # called in has no content. $this->mReferencesErrors[] = $this->error( 'cite_error_empty_references_define', $key ); } return ''; } if ( $str === '' ) { # . This construct is invalid if # it's a contentful ref, but OK if it's a named duplicate and should # be equivalent , for compatability with #tag. if ( $key == false ) { $this->mRefCallStack[] = false; return $this->error( 'cite_error_ref_no_input' ); } else { $str = null; } } if ( $key === false ) { # TODO: Comment this case; what does this condition mean? $this->mRefCallStack[] = false; return $this->error( 'cite_error_ref_too_many_keys' ); } if ( $str === null && $key === null ) { # Something like ; this makes no sense. $this->mRefCallStack[] = false; return $this->error( 'cite_error_ref_no_key' ); } if ( preg_match( '/^[0-9]+$/', $key ) || preg_match( '/^[0-9]+$/', $follow ) ) { # 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). $this->mRefCallStack[] = false; return $this->error( 'cite_error_ref_numeric_key' ); } if ( preg_match( '//', preg_replace( '#<([^ ]+?).*?>.*?|#', '', $str ) ) ) { # (bug 6199) This most likely implies that someone left off the # closing tag, which will cause the entire article to be # eaten up until the next . So we bail out early instead. # The fancy regex above first tries chopping out anything that # looks like a comment or SGML tag, which is a crude way to avoid # false alarms for ,
, etc.
			#
			# Possible improvement: print the warning, followed by the contents
			# of the  tag.  This way no part of the article will be eaten
			# even temporarily.

			$this->mRefCallStack[] = false;
			return $this->error( 'cite_error_included_ref' );
		}

		if ( is_string( $key ) || is_string( $str ) ) {
			# We don't care about the content: if the key exists, the ref
			# is presumptively valid.  Either it stores a new ref, or re-
			# fers to an existing one.  If it refers to a nonexistent ref,
			# we'll figure that out later.  Likewise it's definitely valid
			# if there's any content, regardless of key.

			return $this->stack( $str, $key, $group, $follow, $argv );
		}

		# Not clear how we could get here, but something is probably
		# wrong with the types.  Let's fail fast.
		throw new MWException( 'Invalid $str and/or $key: ' . serialize( array( $str, $key ) ) );
	}

	/**
	 * Parse the arguments to the  tag
	 *
	 *  "name" : Key of the reference.
	 *  "group" : Group to which it belongs. Needs to be passed to  too.
	 *  "follow" : If the current reference is the continuation of another, key of that reference.
	 *
	 *
	 * @param $argv array The argument vector
	 * @return mixed false on invalid input, a string on valid
	 *               input and null on no input
	 */
	function refArg( $argv ) {
		global $wgAllowCiteGroups;
		$cnt = count( $argv );
		$group = null;
		$key = null;
		$follow = null;

		if ( $cnt > 2 ) {
			// There should only be one key or follow parameter, and one group parameter
			// FIXME : this looks inconsistent, it should probably return a tuple
			return false;
		} elseif ( $cnt >= 1 ) {
			if ( isset( $argv['name'] ) && isset( $argv['follow'] ) ) {
				return array( false, false, false );
			}
			if ( isset( $argv['name'] ) ) {
				// Key given.
				$key = Sanitizer::escapeId( $argv['name'], 'noninitial' );
				unset( $argv['name'] );
				--$cnt;
			}
			if ( isset( $argv['follow'] ) ) {
				// Follow given.
				$follow = Sanitizer::escapeId( $argv['follow'], 'noninitial' );
				unset( $argv['follow'] );
				--$cnt;
			}
			if ( isset( $argv['group'] ) ) {
				if ( !$wgAllowCiteGroups ) {
					// remove when groups are fully tested.
					return array( false );
				}
				// Group given.
				$group = $argv['group'];
				unset( $argv['group'] );
				--$cnt;
			}

			if ( $cnt == 0 ) {
				return array ( $key, $group, $follow );
			} else {
				// Invalid key
				return array( false, false, false );
			}
		} else {
			// No key
			return array( null, $group, false );
		}
	}

	/**
	 * Populate $this->mRefs based on input and arguments to 
	 *
	 * @param $str string Input from the  tag
	 * @param $key mixed Argument to the  tag as returned by $this->refArg()
	 * @param $group
	 * @param $follow
	 * @param $call
	 *
	 * @return string
	 */
	function stack( $str, $key = null, $group, $follow, $call ) {
		if ( !isset( $this->mRefs[$group] ) ) {
			$this->mRefs[$group] = array();
		}
		if ( !isset( $this->mGroupCnt[$group] ) ) {
			$this->mGroupCnt[$group] = 0;
		}

		if ( $follow != null ) {
			if ( isset( $this->mRefs[$group][$follow] ) && is_array( $this->mRefs[$group][$follow] ) ) {
				// add text to the note that is being followed
				$this->mRefs[$group][$follow]['text'] = $this->mRefs[$group][$follow]['text'] . ' ' . $str;
			} else {
				// insert part of note at the beginning of the group
				for ( $k = 0 ; $k < count( $this->mRefs[$group] ) ; $k++ ) {
					if ( $this->mRefs[$group][$k]['follow'] == null ) {
						break;
					}
				}
				array_splice( $this->mRefs[$group], $k, 0,
						   array( array( 'count' => - 1,
							  'text' => $str,
							  'key' => ++$this->mOutCnt ,
							  'follow' => $follow ) ) );
				array_splice( $this->mRefCallStack, $k, 0,
						   array( array( 'new', $call, $str, $key, $group, $this->mOutCnt ) ) );
			}
			// return an empty string : this is not a reference
			return '';
		}
		if ( $key === null ) {
			// No key
			// $this->mRefs[$group][] = $str;
			$this->mRefs[$group][] = array( 'count' => - 1, 'text' => $str, 'key' => ++$this->mOutCnt );
			$this->mRefCallStack[] = array( 'new', $call, $str, $key, $group, $this->mOutCnt );

			return $this->linkRef( $group, $this->mOutCnt );
		} elseif ( is_string( $key ) ) {
			// Valid key
			if ( !isset( $this->mRefs[$group][$key] ) || !is_array( $this->mRefs[$group][$key] ) ) {
				// First occurrence
				$this->mRefs[$group][$key] = array(
					'text' => $str,
					'count' => 0,
					'key' => ++$this->mOutCnt,
					'number' => ++$this->mGroupCnt[$group]
				);
				$this->mRefCallStack[] = array( 'new', $call, $str, $key, $group, $this->mOutCnt );

				return
					$this->linkRef(
						$group,
						$key,
						$this->mRefs[$group][$key]['key'] . "-" . $this->mRefs[$group][$key]['count'],
						$this->mRefs[$group][$key]['number'],
						"-" . $this->mRefs[$group][$key]['key']
					);
			} else {
				// We've been here before
				if ( $this->mRefs[$group][$key]['text'] === null && $str !== '' ) {
					// If no text found before, use this text
					$this->mRefs[$group][$key]['text'] = $str;
					$this->mRefCallStack[] = array( 'assign', $call, $str, $key, $group,
						$this->mRefs[$group][$key]['key'] );
				} else {
					$this->mRefCallStack[] = array( 'increment', $call, $str, $key, $group,
						$this->mRefs[$group][$key]['key'] );
				}
				return
					$this->linkRef(
						$group,
						$key,
						$this->mRefs[$group][$key]['key'] . "-" . ++$this->mRefs[$group][$key]['count'],
						$this->mRefs[$group][$key]['number'],
						"-" . $this->mRefs[$group][$key]['key']
					);
			}
		} else {
			throw new MWException( 'Invalid stack key: ' . serialize( $key ) );
		}
	}

	/**
	 * Partially undoes the effect of calls to stack()
	 *
	 * Called by guardedReferences()
	 *
	 * The option to define  within  makes the
	 * behavior of  context dependent.  This is normally fine
	 * but certain operations (especially #tag) lead to out-of-order
	 * parser evaluation with the  tags being processed before
	 * their containing  element is read.  This leads to
	 * stack corruption that this function works to fix.
	 *
	 * This function is not a total rollback since some internal
	 * counters remain incremented.  Doing so prevents accidentally
	 * corrupting certain links.
	 *
	 * @param $type
	 * @param $key
	 * @param $group
	 * @param $index
	 */
	function rollbackRef( $type, $key, $group, $index ) {
		if ( !isset( $this->mRefs[$group] ) ) {
			return;
		}

		if ( $key === null ) {
			foreach ( $this->mRefs[$group] as $k => $v ) {
				if ( $this->mRefs[$group][$k]['key'] === $index ) {
					$key = $k;
					break;
				}
			}
		}

		# Sanity checks that specified element exists.
		if ( $key === null ) {
			return;
		}
		if ( !isset( $this->mRefs[$group][$key] ) ) {
			return;
		}
		if ( $this->mRefs[$group][$key]['key'] != $index ) {
			return;
		}

		switch ( $type ) {
		case 'new':
			# Rollback the addition of new elements to the stack.
			unset( $this->mRefs[$group][$key] );
			if ( count( $this->mRefs[$group] ) == 0 ) {
				unset( $this->mRefs[$group] );
				unset( $this->mGroupCnt[$group] );
			}
			break;
		case 'assign':
			# Rollback assignment of text to pre-existing elements.
			$this->mRefs[$group][$key]['text'] = null;
			# continue without break
		case 'increment':
			# Rollback increase in named ref occurrences.
			$this->mRefs[$group][$key]['count']--;
			break;
		}
	}

	/**
	 * Callback function for 
	 *
	 * @param $str string Input
	 * @param $argv array Arguments
	 * @param $parser Parser
	 *
	 * @return string
	 */
	function references( $str, $argv, $parser ) {
		if ( $this->mInCite || $this->mInReferences ) {
			if ( is_null( $str ) ) {
				return htmlspecialchars( "" );
			} else {
				return htmlspecialchars( "$str" );
			}
		} else {
			$this->mCallCnt++;
			$this->mInReferences = true;
			$ret = $this->guardedReferences( $str, $argv, $parser );
			$this->mInReferences = false;
			return $ret;
		}
	}

	/**
	 * @param $str string
	 * @param $argv array
	 * @param $parser Parser
	 * @param $group string
	 * @return string
	 */
	function guardedReferences( $str, $argv, $parser, $group = CITE_DEFAULT_GROUP ) {
		global $wgAllowCiteGroups;

		$this->mParser = $parser;

		if ( isset( $argv['group'] ) && $wgAllowCiteGroups ) {
			$group = $argv['group'];
			unset ( $argv['group'] );
		}

		if ( strval( $str ) !== '' ) {
			$this->mReferencesGroup = $group;

			# Detect whether we were sent already rendered s
			# Mostly a side effect of using #tag to call references
			$count = substr_count( $str, $parser->uniqPrefix() . "-ref-" );
			for ( $i = 1; $i <= $count; $i++ ) {
				if ( count( $this->mRefCallStack ) < 1 ) {
					break;
				}

				# The following assumes that the parsed s sent within
				# the  block were the most recent calls to
				# .  This assumption is true for all known use cases,
				# but not strictly enforced by the parser.  It is possible
				# that some unusual combination of #tag,  and
				# conditional parser functions could be created that would
				# lead to malformed references here.
				$call = array_pop( $this->mRefCallStack );
				if ( $call !== false ) {
					list( $type, $ref_argv, $ref_str,
						$ref_key, $ref_group, $ref_index ) = $call;

					# Undo effects of calling  while unaware of containing 
					$this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );

					# Rerun  call now that mInReferences is set.
					$this->guardedRef( $ref_str, $ref_argv, $parser );
				}
			}

			# Parse $str to process any unparsed  tags.
			$parser->recursiveTagParse( $str );

			# Reset call stack
			$this->mRefCallStack = array();
		}

		if ( count( $argv ) && $wgAllowCiteGroups ) {
			return $this->error( 'cite_error_references_invalid_parameters_group' );
		} elseif ( count( $argv ) ) {
			return $this->error( 'cite_error_references_invalid_parameters' );
		} else {
			$s = $this->referencesFormat( $group );
			if ( $parser->getOptions()->getIsSectionPreview() ) {
				return $s;
			}

			# Append errors generated while processing 
			if ( count( $this->mReferencesErrors ) > 0 ) {
				$s .= "\n" . implode( "
\n", $this->mReferencesErrors ); $this->mReferencesErrors = array(); } return $s; } } /** * Make output to be returned from the references() function * * @param $group * * @return string XHTML ready for output */ function referencesFormat( $group ) { if ( ( count( $this->mRefs ) == 0 ) || ( empty( $this->mRefs[$group] ) ) ) { return ''; } wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ . '-entries' ); $ent = array(); foreach ( $this->mRefs[$group] as $k => $v ) { $ent[] = $this->referencesFormatEntry( $k, $v ); } $prefix = wfMessage( 'cite_references_prefix' )->inContentLanguage()->plain(); $suffix = wfMessage( 'cite_references_suffix' )->inContentLanguage()->plain(); $content = implode( "\n", $ent ); // Prepare the parser input. We add new lines between the pieces to avoid a confused tidy (bug 13073) $parserInput = $prefix . "\n" . $content . "\n" . $suffix; // Let's try to cache it. global $wgMemc; $cacheKey = wfMemcKey( 'citeref', md5( $parserInput ), $this->mParser->Title()->getArticleID() ); wfProfileOut( __METHOD__ . '-entries' ); global $wgCiteCacheReferences; $data = false; if ( $wgCiteCacheReferences ) { wfProfileIn( __METHOD__ . '-cache-get' ); $data = $wgMemc->get( $cacheKey ); wfProfileOut( __METHOD__ . '-cache-get' ); } if ( !$data || !$this->mParser->isValidHalfParsedText( $data ) ) { wfProfileIn( __METHOD__ . '-parse' ); // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar $ret = rtrim( $this->parse( $parserInput ), "\n" ); if ( $wgCiteCacheReferences ) { $serData = $this->mParser->serializeHalfParsedText( $ret ); $wgMemc->set( $cacheKey, $serData, 86400 ); } wfProfileOut( __METHOD__ . '-parse' ); } else { $ret = $this->mParser->unserializeHalfParsedText( $data ); } wfProfileOut( __METHOD__ ); // done, clean up so we can reuse the group unset( $this->mRefs[$group] ); unset( $this->mGroupCnt[$group] ); return $ret; } /** * Format a single entry for the referencesFormat() function * * @param string $key The key of the reference * @param mixed $val The value of the reference, string for anonymous * references, array for user-suppplied * @return string Wikitext */ function referencesFormatEntry( $key, $val ) { // Anonymous reference if ( !is_array( $val ) ) { return wfMessage( 'cite_references_link_one', $this->referencesKey( $key ), $this->refKey( $key ), $this->referenceText( $key, $val ) )->inContentLanguage()->plain(); } $text = $this->referenceText( $key, $val['text'] ); if ( isset( $val['follow'] ) ) { return wfMessage( 'cite_references_no_link', $this->referencesKey( $val['follow'] ), $text )->inContentLanguage()->plain(); } elseif ( $val['text'] == '' ) { return wfMessage( 'cite_references_link_one', $this->referencesKey( $key ), $this->refKey( $key, $val['count'] ), $text )->inContentLanguage()->plain(); } if ( $val['count'] < 0 ) { return wfMessage( 'cite_references_link_one', $this->referencesKey( $val['key'] ), # $this->refKey( $val['key'], $val['count'] ), $this->refKey( $val['key'] ), $text )->inContentLanguage()->plain(); // Standalone named reference, I want to format this like an // anonymous reference because displaying "1. 1.1 Ref text" is // overkill and users frequently use named references when they // don't need them for convenience } elseif ( $val['count'] === 0 ) { return wfMessage( 'cite_references_link_one', $this->referencesKey( $key . "-" . $val['key'] ), # $this->refKey( $key, $val['count'] ), $this->refKey( $key, $val['key'] . "-" . $val['count'] ), $text )->inContentLanguage()->plain(); // Named references with >1 occurrences } else { $links = array(); // for group handling, we have an extra key here. for ( $i = 0; $i <= $val['count']; ++$i ) { $links[] = wfMessage( 'cite_references_link_many_format', $this->refKey( $key, $val['key'] . "-$i" ), $this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ), $this->referencesFormatEntryAlternateBacklinkLabel( $i ) )->inContentLanguage()->plain(); } $list = $this->listToText( $links ); return wfMessage( 'cite_references_link_many', $this->referencesKey( $key . "-" . $val['key'] ), $list, $text )->inContentLanguage()->plain(); } } /** * Returns formatted reference text * @param String $key * @param String $text * @return String */ function referenceText( $key, $text ) { if ( $text == '' ) { return $this->error( 'cite_error_references_no_text', $key, 'noparse' ); } return '' . rtrim( $text, "\n" ) . "\n"; } /** * 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 referencesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) { global $wgContLang; $scope = strlen( $max ); $ret = $wgContLang->formatNum( sprintf( "%s.%0{$scope}s", $base, $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 referencesFormatEntryAlternateBacklinkLabel( $offset ) { if ( !isset( $this->mBacklinkLabels ) ) { $this->genBacklinkLabels(); } if ( isset( $this->mBacklinkLabels[$offset] ) ) { return $this->mBacklinkLabels[$offset]; } else { // Feed me! return $this->error( 'cite_error_references_no_backlink_label', null, 'noparse' ); } } /** * Generate a custom format link for a group given an offset, e.g. * the second is b if $this->mLinkLabels["foo"] = * array( 'a', 'b', 'c', ...). * Return an error if the offset > the # of array items * * @param int $offset The offset * @param string $group The group name * @param string $label The text to use if there's no message for them. * * @return string */ function getLinkLabel( $offset, $group, $label ) { $message = "cite_link_label_group-$group"; if ( !isset( $this->mLinkLabels[$group] ) ) { $this->genLinkLabels( $group, $message ); } if ( $this->mLinkLabels[$group] === false ) { // Use normal representation, ie. "$group 1", "$group 2"... return $label; } if ( isset( $this->mLinkLabels[$group][$offset - 1] ) ) { return $this->mLinkLabels[$group][$offset - 1]; } else { // Feed me! return $this->error( 'cite_error_no_link_label_group', array( $group, $message ), 'noparse' ); } } /** * Return an id for use in wikitext output based on a key and * optionally the number 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 refKey( $key, $num = null ) { $prefix = wfMessage( 'cite_reference_link_prefix' )->inContentLanguage()->text(); $suffix = wfMessage( 'cite_reference_link_suffix' )->inContentLanguage()->text(); if ( isset( $num ) ) { $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num ) ->inContentLanguage()->plain(); } return "$prefix$key$suffix"; } /** * Return an id for use in wikitext output based on a key and * optionally the number 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 referencesKey( $key, $num = null ) { $prefix = wfMessage( 'cite_references_link_prefix' )->inContentLanguage()->text(); $suffix = wfMessage( 'cite_references_link_suffix' )->inContentLanguage()->text(); if ( isset( $num ) ) { $key = wfMessage( 'cite_reference_link_key_with_num', $key, $num ) ->inContentLanguage()->plain(); } return "$prefix$key$suffix"; } /** * Generate a link ( element from a key * and return XHTML ready for output * * @param $group * @param $key string The key for the link * @param $count int The index of the key, used for distinguishing * multiple occurrences of the same key * @param $label int The label to use for the link, I want to * use the same label for all occourances of * the same named reference. * @param $subkey string * * @return string */ function linkRef( $group, $key, $count = null, $label = null, $subkey = '' ) { global $wgContLang; $label = is_null( $label ) ? ++$this->mGroupCnt[$group] : $label; return $this->parse( wfMessage( 'cite_reference_link', $this->refKey( $key, $count ), $this->referencesKey( $key . $subkey ), $this->getLinkLabel( $label, $group, ( ( $group == CITE_DEFAULT_GROUP ) ? '' : "$group " ) . $wgContLang->formatNum( $label ) ) )->inContentLanguage()->plain() ); } /** * 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 = wfMessage( 'cite_references_link_many_sep' )->inContentLanguage()->plain(); $and = wfMessage( 'cite_references_link_many_and' )->inContentLanguage()->plain(); 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 * 'cite_references_link_many_format' message, the format is an * arbitrary number of tokens separated by [\t\n ] */ function genBacklinkLabels() { wfProfileIn( __METHOD__ ); $text = wfMessage( 'cite_references_link_many_format_backlink_labels' ) ->inContentLanguage()->plain(); $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text ); wfProfileOut( __METHOD__ ); } /** * Generate the labels to pass to the * 'cite_reference_link' message instead of numbers, the format is an * arbitrary number of tokens separated by [\t\n ] * * @param $group * @param $message */ function genLinkLabels( $group, $message ) { wfProfileIn( __METHOD__ ); $text = false; $msg = wfMessage( $message )->inContentLanguage(); if ( $msg->exists() ) { $text = $msg->plain(); } $this->mLinkLabels[$group] = ( $text == '' ) ? false : 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 * * @param $parser Parser * * @return bool */ function clearState( &$parser ) { if ( $parser->extCite !== $this ) { return $parser->extCite->clearState( $parser ); } # Don't clear state when we're in the middle of parsing # a tag if ( $this->mInCite || $this->mInReferences ) { return true; } $this->mGroupCnt = array(); $this->mOutCnt = 0; $this->mCallCnt = 0; $this->mRefs = array(); $this->mReferencesErrors = array(); $this->mRefCallStack = array(); return true; } /** * Gets run when the parser is cloned. * * @param $parser Parser * * @return bool */ function cloneState( $parser ) { if ( $parser->extCite !== $this ) { return $parser->extCite->cloneState( $parser ); } $parser->extCite = clone $this; $parser->setHook( 'ref' , array( $parser->extCite, 'ref' ) ); $parser->setHook( 'references' , array( $parser->extCite, 'references' ) ); // Clear the state, making sure it will actually work. $parser->extCite->mInCite = false; $parser->extCite->mInReferences = false; $parser->extCite->clearState( $parser ); return true; } /** * Called at the end of page processing to append an error if refs were * used without a references tag. * * @param $afterParse bool true if called from the ParserAfterParse hook * @param $parser Parser * @param $text string * * @return bool */ function checkRefsNoReferences( $afterParse, &$parser, &$text ) { if ( $parser->extCite !== $this ) { return $parser->extCite->checkRefsNoReferences( $afterParse, $parser, $text ); } if ( $afterParse ) { $this->mHaveAfterParse = true; } elseif ( $this->mHaveAfterParse ) { return true; } if ( $parser->getOptions()->getIsSectionPreview() ) { return true; } foreach ( $this->mRefs as $group => $refs ) { if ( count( $refs ) == 0 ) { continue; } $text .= "\n
"; if ( $group == CITE_DEFAULT_GROUP ) { $text .= $this->error( 'cite_error_refs_without_references' ); } else { $text .= $this->error( 'cite_error_group_refs_without_references', htmlspecialchars( $group ) ); } } return true; } /** * Hook for the InlineEditor extension. If any ref or reference reference tag is in the text, the entire * page should be reparsed, so we return false in that case. * * @param $output * * @return bool */ function checkAnyCalls( &$output ) { global $wgParser; /* InlineEditor always uses $wgParser */ return ( $wgParser->extCite->mCallCnt <= 0 ); } /** * Initialize the parser hooks * * @param $parser Parser * * @return bool */ static function setHooks( $parser ) { global $wgHooks; $parser->extCite = new self(); if ( !Cite::$hooksInstalled ) { $wgHooks['ParserClearState'][] = array( $parser->extCite, 'clearState' ); $wgHooks['ParserCloned'][] = array( $parser->extCite, 'cloneState' ); $wgHooks['ParserAfterParse'][] = array( $parser->extCite, 'checkRefsNoReferences', true ); $wgHooks['ParserBeforeTidy'][] = array( $parser->extCite, 'checkRefsNoReferences', false ); $wgHooks['InlineEditorPartialAfterParse'][] = array( $parser->extCite, 'checkAnyCalls' ); Cite::$hooksInstalled = true; } $parser->setHook( 'ref' , array( $parser->extCite, 'ref' ) ); $parser->setHook( 'references' , array( $parser->extCite, 'references' ) ); return true; } /** * 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 * @param string $parse Whether to parse the message ('parse') or not ('noparse') * @return string XHTML or wikitext ready for output */ function error( $key, $param = null, $parse = 'parse' ) { # We rely on the fact that PHP is okay with passing unused argu- # ments to functions. If $1 is not used in the message, wfMessage will # just ignore the extra parameter. $ret = '' . wfMessage( 'cite_error', wfMessage( $key, $param )->inContentLanguage()->plain() )->inContentLanguage()->plain() . ''; if ( $parse == 'parse' ) { $ret = $this->parse( $ret ); } return $ret; } /**#@-*/ }