summaryrefslogtreecommitdiff
path: root/extensions/Cite/Cite_body.php
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/Cite/Cite_body.php')
-rw-r--r--extensions/Cite/Cite_body.php1197
1 files changed, 1197 insertions, 0 deletions
diff --git a/extensions/Cite/Cite_body.php b/extensions/Cite/Cite_body.php
new file mode 100644
index 00000000..488bacbb
--- /dev/null
+++ b/extensions/Cite/Cite_body.php
@@ -0,0 +1,1197 @@
+<?php
+
+/**#@+
+ * A parser extension that adds two tags, <ref> and <references> 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 <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
+ *
+ * @bug 4579
+ *
+ * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
+ * @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 <ref> input, in the format of:
+ * <code>
+ * 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'
+ * );
+ * </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 $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 <ref> tag is being processed.
+ * Used to avoid infinite recursion
+ *
+ * @var boolean
+ */
+ var $mInCite = false;
+
+ /**
+ * True when a <references> tag is being processed.
+ * Used to detect the use of <references> to define refs
+ *
+ * @var boolean
+ */
+ var $mInReferences = false;
+
+ /**
+ * Error stack used when defining refs in <references>
+ *
+ * @var array
+ */
+ var $mReferencesErrors = array();
+
+ /**
+ * Group used when in <references> block
+ *
+ * @var string
+ */
+ var $mReferencesGroup = '';
+
+ /**
+ * <ref> 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 <ref>
+ *
+ * @param $str string Input
+ * @param $argv array Arguments
+ * @param $parser Parser
+ *
+ * @return string
+ */
+ function ref( $str, $argv, $parser ) {
+ if ( $this->mInCite ) {
+ return htmlspecialchars( "<ref>$str</ref>" );
+ } else {
+ $this->mCallCnt++;
+ $this->mInCite = true;
+ $ret = $this->guardedRef( $str, $argv, $parser );
+ $this->mInCite = false;
+ 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
+ #
+ # <references>
+ # <ref name="foo"> BAR </ref>
+ # </references>
+ #
+ if ( $this->mInReferences ) {
+ if ( $group != $this->mReferencesGroup ) {
+ # <ref> and <references> 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 === '' ) {
+ # <ref> calls inside <references> 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 {
+ # <ref> called in <references> has no content.
+ $this->mReferencesErrors[] =
+ $this->error( 'cite_error_empty_references_define', $key );
+ }
+ return '';
+ }
+
+ if ( $str === '' ) {
+ # <ref ...></ref>. This construct is invalid if
+ # it's a contentful ref, but OK if it's a named duplicate and should
+ # be equivalent <ref ... />, 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 <ref />; 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(
+ '/<ref\b[^<]*?>/',
+ preg_replace( '#<([^ ]+?).*?>.*?</\\1 *>|<!--.*?-->#', '', $str )
+ ) ) {
+ # (bug 6199) This most likely implies that someone left off the
+ # closing </ref> tag, which will cause the entire article to be
+ # eaten up until the next <ref>. 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 <nowiki>, <pre>, etc.
+ #
+ # Possible improvement: print the warning, followed by the contents
+ # of the <ref> 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.
+ $this->croak( 'cite_error_key_str_invalid', serialize( "$str; $key" ) );
+ }
+
+ /**
+ * Parse the arguments to the <ref> tag
+ *
+ * "name" : Key of the reference.
+ * "group" : Group to which it belongs. Needs to be passed to <references /> 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 <ref>
+ *
+ * @param $str string Input from the <ref> tag
+ * @param $key mixed Argument to the <ref> 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 {
+ $this->croak( 'cite_error_stack_invalid_input', serialize( array( $key, $str ) ) );
+ }
+ }
+
+ /**
+ * Partially undoes the effect of calls to stack()
+ *
+ * Called by guardedReferences()
+ *
+ * The option to define <ref> within <references> makes the
+ * behavior of <ref> context dependent. This is normally fine
+ * but certain operations (especially #tag) lead to out-of-order
+ * parser evaluation with the <ref> tags being processed before
+ * their containing <reference> 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 <references>
+ *
+ * @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( "<references/>" );
+ } else {
+ return htmlspecialchars( "<references>$str</references>" );
+ }
+ } 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 <ref>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 <ref>s sent within
+ # the <references> block were the most recent calls to
+ # <ref>. 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, <references> 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 <ref> while unaware of containing <references>
+ $this->rollbackRef( $type, $ref_key, $ref_group, $ref_index );
+
+ # Rerun <ref> call now that mInReferences is set.
+ $this->guardedRef( $ref_str, $ref_argv, $parser );
+ }
+ }
+
+ # Parse $str to process any unparsed <ref> 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 <references>
+ if ( count( $this->mReferencesErrors ) > 0 ) {
+ $s .= "\n" . implode( "<br />\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 '<span class="reference-text">' . rtrim( $text, "\n" ) . "</span>\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 <ref group="foo"> 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 <references>, not <ref>
+ * (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 <ref>, not <references>
+ * (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 (<sup ...) for the <ref> 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 <ref> or
+ // <references> 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
+ * '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 <ref> 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<br />";
+ 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 = '<strong class="error">' .
+ wfMessage( 'cite_error', wfMessage( $key, $param )->plain() )->plain() .
+ '</strong>';
+ if ( $parse == 'parse' ) {
+ $ret = $this->parse( $ret );
+ }
+ return $ret;
+ }
+
+ /**
+ * 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( wfMessage( 'cite_croak', $this->error( $error ), $data )
+ ->inContentLanguage()->text() );
+ }
+
+ /**#@-*/
+}