--- Cite.php    (revision 14725)
+++ Cite.php    (working copy)
@@ -35,13 +35,15 @@
		'CITE_ERROR_STR_INVALID',
		'CITE_ERROR_KEY_INVALID_1',
		'CITE_ERROR_KEY_INVALID_2',
+		'CITE_ERROR_GROUP_INVALID_1',
		'CITE_ERROR_STACK_INVALID_INPUT'
	),
	'user' => array(
		'CITE_ERROR_REF_NUMERIC_KEY',
		'CITE_ERROR_REF_NO_KEY',
-		'CITE_ERROR_REF_TOO_MANY_KEYS',
+		'CITE_ERROR_REF_BAD_ARGS',
		'CITE_ERROR_REF_NO_INPUT',
+		'CITE_ERROR_REF_BAD_GROUP',
		'CITE_ERROR_REFERENCES_INVALID_INPUT',
		'CITE_ERROR_REFERENCES_INVALID_PARAMETERS',
		'CITE_ERROR_REFERENCES_NO_BACKLINK_LABEL'
@@ -63,45 +65,47 @@
			/*
			   Debug & errors
			*/

			// Internal errors
			'cite_croak' => 'Cite croaked; $1: $2',

			'cite_error_' . CITE_ERROR_STR_INVALID => 'Internal error; invalid $str',
			'cite_error_' . CITE_ERROR_KEY_INVALID_1 => 'Internal error; invalid key',
			'cite_error_' . CITE_ERROR_KEY_INVALID_2 => 'Internal error; invalid key',
+			'cite_error_' . CITE_ERROR_GROUP_INVALID_1 => 'Internal error; invalid group',
			'cite_error_' . CITE_ERROR_STACK_INVALID_INPUT => 'Internal error; invalid stack key',

			// User errors
			'cite_error' => 'Cite error $1; $2',

-			'cite_error_' . CITE_ERROR_REF_NUMERIC_KEY => 'Invalid call; expecting a non-integer key',
+			'cite_error_' . CITE_ERROR_REF_NUMERIC_KEY => 'Invalid key defined for &lt;ref&gt; (keys cannot be integers)',
-			'cite_error_' . CITE_ERROR_REF_NO_KEY => 'Invalid call; no key specified',
+			'cite_error_' . CITE_ERROR_REF_NO_KEY => 'No key defined for &lt;ref&gt;',
-			'cite_error_' . CITE_ERROR_REF_TOO_MANY_KEYS => 'Invalid call; invalid keys, e.g. too many or wrong key specified',
+			'cite_error_' . CITE_ERROR_REF_BAD_ARGS => 'Invalid arguments defined for &lt;ref&gt;: exactly one name and/or one group are allowed',
-			'cite_error_' . CITE_ERROR_REF_NO_INPUT => 'Invalid call; no input specified',
+			'cite_error_' . CITE_ERROR_REF_NO_INPUT => 'No input specified for &lt;ref&gt;',
+			'cite_error_' . CITE_ERROR_REF_BAD_GROUP => "Invalid group label defined for &lt;ref&gt;: 'ungrouped' is reserved",
-			'cite_error_' . CITE_ERROR_REFERENCES_INVALID_INPUT => 'Invalid input; expecting none',
+			'cite_error_' . CITE_ERROR_REFERENCES_INVALID_INPUT => 'Invalid input to &lt;references&gt;: expecting none, use &lt;references /&gt;',
-			'cite_error_' . CITE_ERROR_REFERENCES_INVALID_PARAMETERS => 'Invalid parameters; expecting none',
+			'cite_error_' . CITE_ERROR_REFERENCES_INVALID_PARAMETERS => 'Invalid parameters defined for &lt;references&gt;: only "group" is valid',
			'cite_error_' . CITE_ERROR_REFERENCES_NO_BACKLINK_LABEL => "Ran out of custom backlink labels, define more in the \"''cite_references_link_many_format_backlink_labels''\" message",

			/*
			   Output formatting
			*/
			'cite_reference_link_key_with_num' => '$1_$2',
			// Ids produced by <ref>
-			'cite_reference_link_prefix' => '_ref-',
+			'cite_ref_link_prefix' => '_ref-',
-			'cite_reference_link_suffix' => '',
+			'cite_ref_link_suffix' => '',
			// Ids produced by <references>
			'cite_references_link_prefix' => '_note-',
			'cite_references_link_suffix' => '',

-			'cite_reference_link' => '<sup id="$1" class="reference">[[#$2|<nowiki>[</nowiki>$3<nowiki>]</nowiki>]]</sup>',
+			'cite_ref_link' => '<sup id="$1" class="reference">[[#$2|<nowiki>[</nowiki>$3<nowiki>]</nowiki>]]</sup>',
			'cite_references_link_one' => '<li id="$1">[[#$2|Gåæ]] $3</li>',
			'cite_references_link_many' => '<li id="$1">Gåæ $2 $3</li>',
			'cite_references_link_many_format' => '[[#$1|<sup>$2</sup>]]',
			// An item from this set is passed as $3 in the message above
			'cite_references_link_many_format_backlink_labels' => 'a b c d e f g h i j k l m n o p q r s t u v w x y z',
			'cite_references_link_many_sep' => "\xc2\xa0", // &nbsp;
-			'cite_references_link_many_and' => "\xc2\xa0", // &nbps;
+			'cite_references_link_many_and' => "\xc2\xa0", // &nbsp;


			// Although I could just use # instead of <li> above and nothing here that
			// will break on input that contains linebreaks
@@ -109,36 +113,43 @@
			'cite_references_suffix' => '</ol>',
		)
	);

	class Cite {
		/**#@+
		 * @access private
		 */

+
+
		/**
		 * Datastructure representing <ref> input, in the format of:
		 * <code>
		 * array(
+		 *	'ungrouped' => array(
-		 *	'user supplied' => array(
+		 *		'user supplied' => array(
-		 *		'text' => 'user supplied reference & key',
+		 *			'text' => 'user supplied reference & key',
-		 *		'count' => 1, // occurs twice
+		 *			'count' => 1,	// occurs twice
-		 *		'number' => 1, // The first reference, we want
+		 *			'number' => 1,	// The first reference, we want
-		 *		               // all occourances of it to
+		 *					// all occurrences of it to
-		 *		               // use the same number
+		 *					// all occurrences of it to
-		 *	),
+		 *			),
-		 *	0 => 'Anonymous reference',
+		 *			0 => 'Anonymous reference',
-		 *	1 => 'Another anonymous reference',
+		 *			1 => 'Another anonymous reference',
-		 *	'some key' => array(
+		 *			'some key' => array(
-		 *		'text' => 'this one occurs once'
+		 *				'text' => 'this one occurs once'
-		 *		'count' => 0,
+		 *				'count' => 0,
-		 *		'number' => 4
+		 *				'number' => 4
-		 *	),
+		 *			),
-		 *	3 => 'more stuff'
+		 *			3 => 'more stuff'
+		 *		),
+		 *	'custom_group' => array(
+		 *		...
+		 *	)
		 * );
		 * </code>
		 *
		 * This works because:
-		 * * PHP's datastructures are guarenteed to be returned in the
+		 * * 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
@@ -147,22 +158,23 @@
		 * @var array
		 **/
		 var $mRefs = array();

		/**
		 * Count for user displayed output (ref[1], ref[2], ...)
+		 * Broken up into pieces for different groups, default is 'ungrouped'
		 *
		 * @var int
		 */
-		 var $mOutCnt = 0;
+		 var $mOutCnt = array();

		/**
-		 * Internal counter for anonymous references, seperate from
+		 * Internal counter for anonymous references, separate from
		 * $mOutCnt because anonymous references won't increment it,
		 * but will incremement $mOutCnt
		 *
		 * @var int
		 */
-		var $mInCnt = 0;
+		var $mInCnt = array();

		/**
		 * The backlinks, in order, to pass as $3 to
@@ -215,42 +227,48 @@
				return $ret;
			}
		}

		function guardedRef( $str, $argv, $parser ) {
-			$this->mParser = $parser;
+			$this->mParser = $parser;
-			$key = $this->refArg( $argv );
+			$args = $this->refArg( $argv );
+			$key = $args['name'];
+			$group = $args['group'];

-			if ( $str !== null ) {
+			if ( $str !== null ) { // <ref>s have some content--not <ref ... />
-				if ( $str === '' )
+				if ( $str === '' ) // <ref ...></ref>
					return $this->error( CITE_ERROR_REF_NO_INPUT );
-				if ( is_string( $key ) )
+				elseif ( is_string( $key ) || $key === null )
					// I don't want keys in the form of /^[0-9]+$/ because they would
					// conflict with the php datastructure I'm using, besides, why specify
					// a manual key if it's just going to be any old integer?
					if ( sprintf( '%d', $key ) === (string)$key )
						return $this->error( CITE_ERROR_REF_NUMERIC_KEY );
-					else
+					elseif ($group === false) // $group === 'ungrouped'
+						$this->error( CITE_ERROR_REF_BAD_GROUP );
+					elseif ( !is_string( $group ) && $group !== null )
+						$this->croak( CITE_ERROR_GROUP_INVALID_1, serialize( $group ) );
+					else // $key is valid, $group is valid: hopefully we end up here
-						return $this->stack( $str, $key);
+						return $this->stack( $str, $key, $group );
-				else if ( $key === null )
-					return $this->stack( $str );
-				else if ( $key === false )
+				elseif ( $key === false ) // refArg determined $key is bad
-					return $this->error( CITE_ERROR_REF_TOO_MANY_KEYS );
+					return $this->error( CITE_ERROR_REF_BAD_ARGS );
				else
					$this->croak( CITE_ERROR_KEY_INVALID_1,serialize( $key ) );
-			} else if ( $str === null ) {
+			} elseif ( $str === null ) { // <ref ... />
				if ( is_string( $key ) )
					if ( sprintf( '%d', $key ) === (string)$key )
						return $this->error( CITE_ERROR_REF_NUMERIC_KEY );
-					else
+					elseif ($group === false) // $group === 'ungrouped'
+						$this->error( CITE_ERROR_REF_BAD_GROUP );
+					else // $key is valid, $group is valid: hopefully we end up here
-						return $this->stack( $str, $key);
+						return $this->stack( $str, $key, $group );
-				else if ( $key === false )
+				elseif ( $key === false )
-					return $this->error( CITE_ERROR_REF_TOO_MANY_KEYS );
+					return $this->error( CITE_ERROR_REF_BAD_ARGS );
-				else if ( $key === null )
+				elseif ( $key === null )
					return $this->error( CITE_ERROR_REF_NO_KEY );
				else
					$this->croak( CITE_ERROR_KEY_INVALID_2,serialize( $key ) );
-			} else
+			} else // If we reach here, something is seriously wrong.
				$this->croak( CITE_ERROR_STR_INVALID, serialize( $str ) );
		}

@@ -260,28 +278,41 @@
		 * @static
		 *
		 * @param array $argv The argument vector
-		 * @return mixed false on invalid input, a string on valid
-		 *               input and null on no input
+		 * @return array: each element is false on invalid input, a string on
+		 *               valid input and null on no input (or 'ungrouped' for
+		 *               groups, since that's less confusing as an array key)
		 */
		function refArg( $argv ) {

			$cnt = count( $argv );

-			if ( $cnt > 1 )
+			if ( $cnt > 2 )
-				// There should only be one key
+				// There should never be more than two arguments
-				return false;
+				return array('name' => false, 'group' => false);
-			else if ( $cnt == 1 )
+			elseif ( $cnt == 1 )
				if ( isset( $argv['name'] ) )
					// Key given.
-					return $this->validateName( array_shift( $argv ) );
+					return array('name' => $this->validateName($argv['name']),
+						    'group' => 'ungrouped' );
+				elseif ( isset( $argv['group'] ) )
+					// Group given.
+					return array('name' => null,
+						    'group' => $this->validateGroup($argv['group']) );
				else
-					// Invalid key
+					// Invalid arg
-					return false;
+					return array('name' => false, 'group' => false);
+			elseif ( $cnt == 2 )
+				if ( isset( $argv['name'] ) && isset( $argv['group'] ) )
+					// Key and group given.
+					return array('name' => $this->validateName($argv['name']),
+						    'group' => $this->validateGroup($argv['group']) );
+				else
+					// Invalid arg(s)
+					return array('name' => false, 'group' => false);
			else
-				// No key
+				// No key or group
-				return null;
+				return array('name' => null, 'group' => 'ungrouped');
		}

		/**
		 * Since the key name is used in an XHTML id attribute, it must
		 * conform to the validity rules. The restriction to begin with
@@ -292,7 +323,7 @@
		 * @return string if valid, false if invalid
		 */
		function validateName( $name ) {
-			if( preg_match( '/^[A-Za-z0-9:_.-]*$/i', $name ) ) {
+			if( preg_match( '/^[A-Za-z0-9:_.-]*$/', $name ) ) {
				return $name;
			} else {
				// WARNING: CRAPPY CUT AND PASTE MAKES BABY JESUS CRY
@@ -309,44 +340,67 @@
		}
+
+		/**
+		 * Since the group name is used as an array key, make sure it's not
+		 * screwy.  NUL, \, ', ", and $ are all probably Bad.
+		 *
+		 * @return string
+		 */
+		function validateGroup( $group ) {
+			if( $group === 'ungrouped' )
+				return false;
+			elseif( !get_magic_quotes_gpc() )
+				return addcslashes($group,'\0\\\'"$');
+			else
+				return $group;
+		}

		/**
		 * Populate $this->mRefs based on input and arguments to <ref>
		 *
		 * @param string $str Input from the <ref> tag
-		 * @param mixed $key Argument to the <ref> tag as returned by $this->refArg()
+		 * @param mixed $key Key for the <ref> tag as returned by $this->refArg()
+		 *                                      (either a string or null)
+		 * @param mixed $group Which grouping of references this is part of
-		 * @return string
+		 * @return string
		 */
-		function stack( $str, $key = null ) {
+		function stack( $str, $key = null, $group = 'ungrouped' ) {
+			if ($this->mInCnt[$group] === null)
+				$this->mInCnt[$group] = 0;
+			if ($this->mOutCnt[$group] === null)
+				$this->mOutCnt[$group] = 0;
			if ( $key === null ) {
				// No key
-				$this->mRefs[] = $str;
+				$this->mRefs[$group][] = $str;
-				return $this->linkRef( $this->mInCnt++ );
+				return $this->linkRef( $this->mInCnt[$group]++,$group );
-			} else if ( is_string( $key ) )
+			} elseif ( is_string( $key ) ) {
				// Valid key
-				if ( ! @is_array( $this->mRefs[$key] ) ) {
+				if ( ! @is_array( $this->mRefs[$group][$key] ) ) {
-					// First occourance
+					// First occurrence
-					$this->mRefs[$key] = array(
+					$this->mRefs[$group][$key] = array(
						'text' => $str,
						'count' => 0,
-						'number' => ++$this->mOutCnt
+						'number' => ++$this->mOutCnt[$group]
					);
					return
						$this->linkRef(
							$key,
+							$group,
-							$this->mRefs[$key]['count'],
+							$this->mRefs[$group][$key]['count'],
-							$this->mRefs[$key]['number']
+							$this->mRefs[$group][$key]['number']
						);
				} else
					// We've been here before
					return
						$this->linkRef(
							$key,
+							$group,
-							++$this->mRefs[$key]['count'],
+							++$this->mRefs[$group][$key]['count'],
-							$this->mRefs[$key]['number']
+							$this->mRefs[$group][$key]['number']
						);
-			else
+			} else
				$this->croak( CITE_ERROR_STACK_INVALID_INPUT, serialize( array( $key, $str ) ) );
		}

		/**
		 * Callback function for <references>
		 *
@@ -368,64 +422,99 @@
				return $ret;
			}
		}

		function guardedReferences( $str, $argv, $parser ) {
			$this->mParser = $parser;
+			$group = $this->referencesArg( $argv );
+
			if ( $str !== null )
				return $this->error( CITE_ERROR_REFERENCES_INVALID_INPUT );
-			else if ( count( $argv ) )
+			elseif ( $group === false )
				return $this->error( CITE_ERROR_REFERENCES_INVALID_PARAMETERS );
			else
-				return $this->referencesFormat();
+				return $this->referencesFormat( $group );
		}
+
+		/**
+		 * Parse the arguments to the <references> tag
+		 *
+		 * @static
+		 *
+		 * @param array $argv The argument vector
+		 * @return mixed false on invalid input, a string on valid
+		 *               or no input
+		 */
+		function referencesArg( $argv ) {
+			$cnt = count( $argv );
+
+			if ( $cnt > 1 )
+				// There should never be more than one argument
+				return false;
+			elseif ( $cnt == 1 ) {
+				if ( isset( $argv['group'] ) )
+					// Group given.
+					return $this->validateGroup( $argv['group'] );
+				else
+					// Not a group
+					return false;
+			} else
+				// No group
+				return 'ungrouped';
+		}

		/**
		 * Make output to be returned from the references() function
		 *
+		 * @param string $group The group of <references />
		 * @return string XHTML ready for output
		 */
-		function referencesFormat() {
+		function referencesFormat( $group = 'ungrouped' ) {
			$ent = array();

-			foreach ( $this->mRefs as $k => $v )
+			foreach ( $this->mRefs[$group] as $k => $v )
+				// $k: name of a named ref, number of an anon
+				// $v: contents of the ref
-				$ent[] = $this->referencesFormatEntry( $k, $v );
+				$ent[] = $this->referencesFormatEntry( $k, $v, $group );


			$prefix = wfMsgForContentNoTrans( 'cite_references_prefix' );
			$suffix = wfMsgForContentNoTrans( 'cite_references_suffix' );
			$content = implode( "\n", $ent );

			// Live hack: parse() adds two newlines on WM, can't reproduce it locally -+ªvar
			return rtrim( $this->parse( $prefix . $content . $suffix ), "\n" );
		}

		/**
		 * Format a single entry for the referencesFormat() function
		 *
-		 * @param string $key The key of the reference
+		 * @param string $key The key (name/number) of the reference
-		 * @param mixed $val The value of the reference, string for anonymous
+		 * @param mixed $val The value of the reference (string for anonymous
-		 *                   references, array for user-suppplied
+		 *                   references, array for user-supplied)
+		 * @param string $group The group that this whole <references> belongs
+		 *                                      to
		 * @return string Wikitext
		 */
-		function referencesFormatEntry( $key, $val ) {
+		function referencesFormatEntry( $key, $val, $group = 'ungrouped' ) {
			// Anonymous reference
			if ( ! is_array( $val ) )
				return
					wfMsgForContentNoTrans(
						'cite_references_link_one',
-						$this->referencesKey( $key ),
+						$this->referencesKey( $key, $group ),
-						$this->refKey( $key ),
+						$this->refKey( $key, $group ),
						$val
					);
			// 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
-			else if ( $val['count'] === 0 )
+			elseif ( $val['count'] === 0 )
				return
					wfMsgForContentNoTrans(
						'cite_references_link_one',
-						$this->referencesKey( $key ),
+						$this->referencesKey( $key, $group ),
-						$this->refKey( $key, $val['count'] ),
+						$this->refKey( $key, $group, $val['count'] ),
						$val['text']
					);
			// Named references with >1 occurrences
@@ -435,7 +524,7 @@
				for ( $i = 0; $i <= $val['count']; ++$i ) {
					$links[] = wfMsgForContentNoTrans(
							'cite_references_link_many_format',
-						        $this->refKey( $key, $i),
+							$this->refKey( $key, $group, $i ),
							$this->referencesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),
							$this->referencesFormatEntryAlternateBacklinkLabel( $i )
					);
@@ -445,7 +534,7 @@

				return
					wfMsgForContentNoTrans( 'cite_references_link_many',
-						$this->referencesKey( $key ),
+						$this->referencesKey( $key, $group ),
						$list,
						$val['text']
					);
@@ -500,14 +589,17 @@
		 *
		 * @param string $key The key
		 * @param int $num The number of the key
+		 * @param string $group The group
		 * @return string A key for use in wikitext
		 */
-		function refKey( $key, $num = null ) {
+		function refKey( $key, $group = 'ungrouped', $num = null ) {
-			$prefix = wfMsgForContent( 'cite_reference_link_prefix');
+			$prefix = wfMsgForContent( 'cite_ref_link_prefix' );
-			$suffix = wfMsgForContent( 'cite_reference_link_suffix');
+			$suffix = wfMsgForContent( 'cite_ref_link_suffix' );
+			if ( $group !== 'ungrouped' )
+				$key .= '-'.$group;
			if ( isset( $num ) )
				$key = wfMsgForContentNoTrans( 'cite_reference_link_key_with_num', $key, $num );

			return $prefix . $key . $suffix;
		}

@@ -522,12 +614,14 @@
		 * @param int $num The number of the key
		 * @return string A key for use in wikitext
		 */
-		function referencesKey( $key, $num = null ) {
+		function referencesKey( $key, $group = 'ungrouped', $num = null ) {
			$prefix = wfMsgForContent( 'cite_references_link_prefix' );
			$suffix = wfMsgForContent( 'cite_references_link_suffix' );
+			if ( $group !== 'ungrouped' )
+				$key .= '-'.$group;
			if ( isset( $num ) )
				$key = wfMsgForContentNoTrans( 'cite_reference_link_key_with_num', $key, $num );

			return $prefix . $key . $suffix;
		}

@@ -537,22 +631,24 @@
		 *
		 * @param string $key The key for the link
		 * @param int $count The # of the key, used for distinguishing
-		 *                   multiple occourances of the same key
+		 *                   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 occourances of
+		 *                   use the same label for all occurrences of
		 *                   the same named reference.
+		 * @param string $group The link's group (affects numbering and
+		 *                                      link name)
		 * @return string
		 */
-		function linkRef( $key, $count = null, $label = null ) {
+		function linkRef( $key, $group = 'ungrouped', $count = null, $label = null ) {
			global $wgContLang;

			return
				$this->parse(
					wfMsgForContentNoTrans(
-						'cite_reference_link',
+						'cite_ref_link',
-						$this->refKey( $key, $count ),
+						$this->refKey( $key, $group, $count ),
-						$this->referencesKey( $key ),
+						$this->referencesKey( $key, $group ),
-						$wgContLang->formatNum( is_null( $label ) ? ++$this->mOutCnt : $label )
+						$wgContLang->formatNum( is_null( $label ) ? ++$this->mOutCnt[$group] : $label )
					)
				);
		}
@@ -647,7 +743,7 @@
		 * want the counts to transcend pages and other instances
		 */
		function clearState() {
-			$this->mOutCnt = $this->mInCnt = 0;
+			$this->mOutCnt = $this->mInCnt = array();
			$this->mRefs = array();

			return true;
id, wfMsgForContent( "cite_error_$id" ) ) .
						'</strong>'
					);
-			else if ( $id < 0 )
+			elseif ( $id < 0 )
				return wfMsgforContent( 'cite_error', $id );
		}
