User:WillowW/Footnote.php

if ( ! defined( 'MEDIAWIKI' ) )

die();

/**#@+

* A parser extension that adds two tags, and for adding

* explanatory footnotes (NB! *not* citations to references) to pages

*

* @addtogroup Extensions

*

* @link http://meta.wikimedia.org/wiki/Footnote/Footnote.php Documentation, based on http://meta.wikimedia.org/wiki/Cite/Cite.php Documentation

* @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE definition in HTML

* @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite definition in XHTML 2.0

*

*

*

* @author WillowW , based on Cite.php by Ævar Arnfjörð Bjarmason

* @copyright Copyright © 2005, 2008 Ævar Arnfjörð Bjarmason, WillowW

* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later

*/

$wgExtensionFunctions[] = 'wfFootnote';

$wgExtensionCredits['parserhook'][] = array(

'name' => 'Footnote',

'version' => preg_replace('/^.* (\d\d\d\d-\d\d-\d\d) .*$/', '\1', '$LastChangedDate$'), #just the date of the last change

'author' => 'WillowW, based on Ævar Arnfjörð Bjarmason's work; thanks, big Æ!',

'description' => 'Adds and tags, for explanatory footnotes', // kept for b/c

'descriptionmsg' => 'footnote_desc',

'url' => 'http://www.mediawiki.org/wiki/Extension:Footnote/Footnote.php'

);

$wgParserTestFiles[] = dirname( __FILE__ ) . "/footnoteParserTests.txt";

$wgExtensionMessagesFiles['Footnote'] = dirname( __FILE__ ) . "/Footnote.i18n.php";

function wfFootnote() {

class Footnote {

/**#@+

* @access private

*/

/**

* Datastructure representing input, in the format of:

*

* 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'

* );

*

*

* This works because:

* * PHP's datastructures are guaranteed to be returned in the

* order that things are inserted into them (unless you mess

* with that)

* * User supplied keys can't be integers, therefore avoiding

* conflict with anonymous keys

*

* @var array

**/

var $mFootnotes = array();

/**

* Count for user displayed output (note[1], note[2], ...)

*

* @var int

*/

var $mOutCnt = 0;

/**

* Internal counter for anonymous footnotes, separate from

* $mOutCnt because anonymous footnotes won't increment it,

* but will incremement $mOutCnt

*

* @var int

*/

var $mInCnt = 0;

/**

* The backlinks, in order, to pass as $3 to

* 'footnote_references_link_many_format', defined in

* 'footnote_references_link_many_format_backlink_labels

*

* @var array

*/

var $mBacklinkLabels;

/**

* @var object

*/

var $mParser;

/**

* True when a or tag is being processed.

* Used to avoid infinite recursion

*

* @var boolean

*/

var $mInFootnote = false;

/**#@-*/

/**

* Constructor

*/

function Footnote() {

$this->setHooks();

}

/**#@+ @access private */

/**

* Callback function for

*

* @param string $str Input

* @param array $argv Arguments

* @return string

*/

function note( $str, $argv, $parser ) {

wfLoadExtensionMessages( 'Footnote' );

if ( $this->mInFootnote ) {

return htmlspecialchars( "$str" );

} else {

$this->mInFootnote = true;

$ret = $this->guardedNote( $str, $argv, $parser );

$this->mInFootnote = false;

return $ret;

}

}

function guardedNote( $str, $argv, $parser ) {

$this->mParser = $parser;

# The key here is the "name" attribute.

$key = $this->noteArg( $argv );

if( $str === '' ) {

# . This construct is always invalid: either

# it's a contentful note, or it's a named duplicate and should

# be .

return $this->error( 'footnote_error_ref_no_input' );

}

if( $key === false ) {

# TODO: Comment this case; what does this condition mean?

return $this->error( 'footnote_error_ref_too_many_keys' );

}

if( $str === null and $key === null ) {

# Something like ; this makes no sense.

return $this->error( 'footnote_error_ref_no_key' );

}

if( preg_match( '/^[0-9]+$/', $key ) ) {

# Numeric names mess up the resulting id's, potentially produ-

# cing duplicate id's in the XHTML. The Right Thing To Do

# would be to mangle them, but it's not really high-priority

# (and would produce weird id's anyway).

return $this->error( 'footnote_error_ref_numeric_key' );

}

if( is_string( $key ) or is_string( $str ) ) {

# We don't care about the content: if the key exists, the note

# is presumptively valid. Either it stores a new note, or re-

# fers to an existing one. If it refers to a nonexistent note,

# we'll figure that out later. Likewise it's definitely valid

# if there's any content, regardless of key.

return $this->stack( $str, $key );

}

# Not clear how we could get here, but something is probably

# wrong with the types. Let's fail fast.

$this->croak( 'footnote_error_key_str_invalid', serialize( "$str; $key" ) );

}

/**

* Parse the arguments to the tag

*

* @static

*

* @param array $argv The argument vector

* @return mixed false on invalid input, a string on valid

* input and null on no input

*/

function noteArg( $argv ) {

$cnt = count( $argv );

if ( $cnt > 1 )

// There should only be one key

return false;

else if ( $cnt == 1 )

if ( isset( $argv['name'] ) )

// Key given.

return $this->validateName( array_shift( $argv ) );

else

// Invalid key

return false;

else

// No key

return null;

}

/**

* Since the key name is used in an XHTML id attribute, it must

* conform to the validity rules. The restriction to begin with

* a letter is lifted since footnotes have their own prefix.

*

* @fixme merge this code with the various section name transformations

* @fixme double-check for complete validity

* @return string if valid, false if invalid

*/

function validateName( $name ) {

if( preg_match( '/^[A-Za-z0-9:_.-]*$/i', $name ) ) {

return $name;

} else {

// WARNING: CRAPPY CUT AND PASTE MAKES BABY JESUS CRY

$text = urlencode( str_replace( ' ', '_', $name ) );

$replacearray = array(

'%3A' => ':',

'%' => '.'

);

return str_replace(

array_keys( $replacearray ),

array_values( $replacearray ),

$text );

}

}

/**

* Populate $this->mFootnotes based on input and arguments to

*

* @param string $str Input from the tag

* @param mixed $key Argument to the tag as returned by $this->noteArg()

* @return string

*/

function stack( $str, $key = null ) {

if ( $key === null ) {

// No key

$this->mFootnotes[] = $str;

return $this->linkNote( $this->mInCnt++ );

} else if ( is_string( $key ) )

// Valid key

if ( ! isset( $this->mFootnotes[$key] ) || ! is_array( $this->mFootnotes[$key] ) ) {

// First occurrence

$this->mFootnotes[$key] = array(

'text' => $str,

'count' => 0,

'number' => ++$this->mOutCnt

);

return

$this->linkNote(

$key,

$this->mFootnotes[$key]['count'],

$this->mFootnotes[$key]['number']

);

} else {

// We've been here before

if ( $this->mFootnotes[$key]['text'] === null && $str !== '' ) {

// If no text found before, use this text

$this->mFootnotes[$key]['text'] = $str;

};

return

$this->linkNote(

$key,

++$this->mFootnotes[$key]['count'],

$this->mFootnotes[$key]['number']

); }

else

$this->croak( 'footnote_error_stack_invalid_input', serialize( array( $key, $str ) ) );

}

/**

* Callback function for

*

* @param string $str Input

* @param array $argv Arguments

* @return string

*/

function footnotes( $str, $argv, $parser ) {

wfLoadExtensionMessages( 'Footnote' );

if ( $this->mInFootnote ) {

if ( is_null( $str ) ) {

return htmlspecialchars( "" );

} else {

return htmlspecialchars( "$str" );

}

} else {

$this->mInFootnote = true;

$ret = $this->guardedFootnotes( $str, $argv, $parser );

$this->mInFootnote = false;

return $ret;

}

}

function guardedFootnotes( $str, $argv, $parser ) {

$this->mParser = $parser;

if ( $str !== null )

return $this->error( 'footnote_error_references_invalid_input' );

else if ( count( $argv ) )

return $this->error( 'footnote_error_references_invalid_parameters' );

else

return $this->footnotesFormat();

}

/**

* Make output to be returned from the footnotes() function

*

* @return string XHTML ready for output

*/

function footnotesFormat() {

if ( count( $this->mFootnotes ) == 0 )

return '';

wfProfileIn( __METHOD__ );

wfProfileIn( __METHOD__ .'-entries' );

$ent = array();

foreach ( $this->mFootnotes as $k => $v )

$ent[] = $this->footnotesFormatEntry( $k, $v );

$prefix = wfMsgForContentNoTrans( 'footnote_references_prefix' );

$suffix = wfMsgForContentNoTrans( 'footnote_references_suffix' );

$content = implode( "\n", $ent );

wfProfileOut( __METHOD__ .'-entries' );

wfProfileIn( __METHOD__ .'-parse' );

// Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar

$ret = rtrim( $this->parse( $prefix . $content . $suffix ), "\n" );

wfProfileOut( __METHOD__ .'-parse' );

wfProfileOut( __METHOD__ );

return $ret;

}

/**

* Format a single entry for the footnotesFormat() function

*

* @param string $key The key of the note

* @param mixed $val The value of the note, string for anonymous

* notes, array for user-supplied

* @return string Wikitext

*/

function footnotesFormatEntry( $key, $val ) {

// Anonymous note

if ( ! is_array( $val ) )

return

wfMsgForContentNoTrans(

'footnote_references_link_one',

$this->footnotesKey( $key ),

$this->noteKey( $key ),

$val

);

else if ($val['text']=='') return

wfMsgForContentNoTrans(

'footnote_references_link_one',

$this->footnotesKey( $key ),

$this->noteKey( $key, $val['count'] ),

$this->error( 'footnote_error_references_no_text', $key )

);

// Standalone named note, I want to format this like an

// anonymous note because displaying "1. 1.1 Ref text" is

// overkill and users frequently use named footnotes when they

// don't need them for convenience

else if ( $val['count'] === 0 )

return

wfMsgForContentNoTrans(

'footnote_references_link_one',

$this->footnotesKey( $key ),

$this->noteKey( $key, $val['count'] ),

( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) )

);

// Named footnotes with >1 occurrences

else {

$links = array();

for ( $i = 0; $i <= $val['count']; ++$i ) {

$links[] = wfMsgForContentNoTrans(

'footnote_references_link_many_format',

$this->noteKey( $key, $i ),

$this->footnotesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ),

$this->footnotesFormatEntryAlternateBacklinkLabel( $i )

);

}

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

return

wfMsgForContentNoTrans( 'footnote_references_link_many',

$this->footnotesKey( $key ),

$list,

( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) )

);

}

}

/**

* Generate a numeric backlink given a base number and an

* offset, e.g. $base = 1, $offset = 2; = 1.2

* Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100

*

* @static

*

* @param int $base The base

* @param int $offset The offset

* @param int $max Maximum value expected.

* @return string

*/

function footnotesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) {

global $wgContLang;

$scope = strlen( $max );

$ret = $wgContLang->formatNum( $offset );

return $ret;

}

/**

* Generate a custom format backlink given an offset, e.g.

* $offset = 2; = c if $this->mBacklinkLabels = array( 'a',

* 'b', 'c', ...). Return an error if the offset > the # of

* array items

*

* @param int $offset The offset

*

* @return string

*/

function footnotesFormatEntryAlternateBacklinkLabel( $offset ) {

if ( !isset( $this->mBacklinkLabels ) ) {

$this->genBacklinkLabels();

}

if ( isset( $this->mBacklinkLabels[$offset] ) ) {

return $this->mBacklinkLabels[$offset];

} else {

// Feed me!

return $this->error( 'footnote_error_references_no_backlink_label' );

}

}

/**

* Return an id for use in wikitext output based on a key and

* optionally the # of it, used in , not

* (since otherwise it would link to itself)

*

* @static

*

* @param string $key The key

* @param int $num The number of the key

* @return string A key for use in wikitext

*/

function noteKey( $key, $num = null ) {

$prefix = wfMsgForContent( 'footnote_reference_link_prefix' );

$suffix = wfMsgForContent( 'footnote_reference_link_suffix' );

if ( isset( $num ) )

$key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num );

return $prefix . $key . $suffix;

}

/**

* Return an id for use in wikitext output based on a key and

* optionally the # of it, used in , not

* (since otherwise it would link to itself)

*

* @static

*

* @param string $key The key

* @param int $num The number of the key

* @return string A key for use in wikitext

*/

function footnotesKey( $key, $num = null ) {

$prefix = wfMsgForContent( 'footnote_references_link_prefix' );

$suffix = wfMsgForContent( 'footnote_references_link_suffix' );

if ( isset( $num ) )

$key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num );

return $prefix . $key . $suffix;

}

/**

* Generate a link ( element from a key

* and return XHTML ready for output

*

* @param string $key The key for the link

* @param int $count The # of the key, used for distinguishing

* multiple occurrences of the same key

* @param int $label The label to use for the link, I want to

* use the same label for all occurrences of

* the same named reference.

* @return string

*/

function linkNote( $key, $count = null, $label = null ) {

global $wgContLang;

return

$this->parse(

wfMsgForContentNoTrans(

'footnote_reference_link',

$this->noteKey( $key, $count ),

$this->footnotesKey( $key ),

$wgContLang->formatNum( is_null( $label ) ? $this->footnotesFormatEntryAlternateBacklinkLabel( ++$this->mOutCnt ) : $label )

)

);

}

/**

* This does approximately the same thing as

* Language::listToText() but due to this being used for a

* slightly different purpose (people might not want , as the

* first separator and not 'and' as the second, and this has to

* use messages from the content language) I'm rolling my own.

*

* @static

*

* @param array $arr The array to format

* @return string

*/

function listToText( $arr ) {

$cnt = count( $arr );

$sep = wfMsgForContentNoTrans( 'footnote_references_link_many_sep' );

$and = wfMsgForContentNoTrans( 'footnote_references_link_many_and' );

if ( $cnt == 1 )

// Enforce always returning a string

return (string)$arr[0];

else {

$t = array_slice( $arr, 0, $cnt - 1 );

return implode( $sep, $t ) . $and . $arr[$cnt - 1];

}

}

/**

* Parse a given fragment and fix up Tidy's trail of blood on

* it...

*

* @param string $in The text to parse

* @return string The parsed text

*/

function parse( $in ) {

if ( method_exists( $this->mParser, 'recursiveTagParse' ) ) {

// New fast method

return $this->mParser->recursiveTagParse( $in );

} else {

// Old method

$ret = $this->mParser->parse(

$in,

$this->mParser->mTitle,

$this->mParser->mOptions,

// Avoid whitespace buildup

false,

// Important, otherwise $this->clearState()

// would get run every time or

// is called, fucking the whole

// thing up.

false

);

$text = $ret->getText();

return $this->fixTidy( $text );

}

}

/**

* Tidy treats all input as a block, it will e.g. wrap most

* input in

if it isn't already, fix that and return the fixed text

*

* @static

*

* @param string $text The text to fix

* @return string The fixed text

*/

function fixTidy( $text ) {

global $wgUseTidy;

if ( ! $wgUseTidy )

return $text;

else {

$text = preg_replace( '~^

\s*~', '', $text );

$text = preg_replace( '~\s*

\s*~', '', $text );

$text = preg_replace( '~\n$~', '', $text );

return $text;

}

}

/**

* Generate the labels to pass to the

* 'footnote_references_link_many_format' message, the format is an

* arbitary number of tokens separated by [\t\n ]

*/

function genBacklinkLabels() {

wfProfileIn( __METHOD__ );

$text = wfMsgForContentNoTrans( 'footnote_references_link_many_format_backlink_labels' );

$this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text );

wfProfileOut( __METHOD__ );

}

/**

* Gets run when Parser::clearState() gets run, since we don't

* want the counts to transcend pages and other instances

*/

function clearState() {

$this->mOutCnt = $this->mInCnt = 0;

$this->mFootnotes = array();

return true;

}

/**

* Initialize the parser hooks

*/

function setHooks() {

global $wgParser, $wgHooks;

$wgParser->setHook( 'note' , array( &$this, 'note' ) );

$wgParser->setHook( 'notes' , array( &$this, 'footnotes' ) );

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

}

/**

* Return an error message based on an error ID

*

* @param string $key Message name for the error

* @param string $param Parameter to pass to the message

* @return string XHTML ready for output

*/

function error( $key, $param=null ) {

# We rely on the fact that PHP is okay with passing unused argu-

# ments to functions. If $1 is not used in the message, wfMsg will

# just ignore the extra parameter.

return

$this->parse(

'' .

wfMsg( 'footnote_error', wfMsg( $key, $param ) ) .

''

);

}

/**

* Die with a backtrace if something happens in the code which

* shouldn't have

*

* @param int $error ID for the error

* @param string $data Serialized error data

*/

function croak( $error, $data ) {

wfDebugDieBacktrace( wfMsgForContent( 'footnote_croak', $this->error( $error ), $data ) );

}

/**#@-*/

}

new Footnote;

}

/**#@-*/