User:WillowW/Footnote.php
Appearance
<?php if ( ! defined( 'MEDIAWIKI' ) ) die(); /**#@+ * A parser extension that adds two tags, <note> and <notes/> for adding * explanatory footnotes (NB! *not* citations to references) to pages * * @addtogroup Extensions * * @link http://meta.wikimedia.org/wiki/Footnote/Footnote.php Documentation, based on http://meta.wikimedia.org/wiki/Cite/Cite.php Documentation * @link http://www.w3.org/TR/html4/struct/text.html#edef-CITE <cite> definition in HTML * @link http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-text.html#edef_text_cite <cite> definition in XHTML 2.0 * * * * @author WillowW <theknittingcat@yahoo.com>, based on Cite.php by Ævar Arnfjörð Bjarmason <avarab@gmail.com> * @copyright Copyright © 2005, 2008 Ævar Arnfjörð Bjarmason, WillowW * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later */ $wgExtensionFunctions[] = 'wfFootnote'; $wgExtensionCredits['parserhook'][] = array( 'name' => 'Footnote', 'version' => preg_replace('/^.* (\d\d\d\d-\d\d-\d\d) .*$/', '\1', '$LastChangedDate$'), #just the date of the last change 'author' => 'WillowW, based on Ævar Arnfjörð Bjarmason's work; thanks, big Æ!', 'description' => 'Adds <note[ name=id]> and <notes/> tags, for explanatory footnotes', // kept for b/c 'descriptionmsg' => 'footnote_desc', 'url' => 'http://www.mediawiki.org/wiki/Extension:Footnote/Footnote.php' ); $wgParserTestFiles[] = dirname( __FILE__ ) . "/footnoteParserTests.txt"; $wgExtensionMessagesFiles['Footnote'] = dirname( __FILE__ ) . "/Footnote.i18n.php"; function wfFootnote() { class Footnote { /**#@+ * @access private */ /** * Datastructure representing <note> input, in the format of: * <code> * array( * 'user supplied' => array( * 'text' => 'user supplied footnote & key', * 'count' => 1, // occurs twice * 'number' => 1, // The first footnote, we want * // all occurrences of it to * // use the same number * ), * 0 => 'Anonymous footnote', * 1 => 'Another anonymous footnote', * 'some key' => array( * 'text' => 'this one occurs once' * 'count' => 0, * 'number' => 4 * ), * 3 => 'more stuff' * ); * </code> * * This works because: * * PHP's datastructures are guaranteed to be returned in the * order that things are inserted into them (unless you mess * with that) * * User supplied keys can't be integers, therefore avoiding * conflict with anonymous keys * * @var array **/ var $mFootnotes = array(); /** * Count for user displayed output (note[1], note[2], ...) * * @var int */ var $mOutCnt = 0; /** * Internal counter for anonymous footnotes, separate from * $mOutCnt because anonymous footnotes won't increment it, * but will incremement $mOutCnt * * @var int */ var $mInCnt = 0; /** * The backlinks, in order, to pass as $3 to * 'footnote_references_link_many_format', defined in * 'footnote_references_link_many_format_backlink_labels * * @var array */ var $mBacklinkLabels; /** * @var object */ var $mParser; /** * True when a <note> or <notes> tag is being processed. * Used to avoid infinite recursion * * @var boolean */ var $mInFootnote = false; /**#@-*/ /** * Constructor */ function Footnote() { $this->setHooks(); } /**#@+ @access private */ /** * Callback function for <note> * * @param string $str Input * @param array $argv Arguments * @return string */ function note( $str, $argv, $parser ) { wfLoadExtensionMessages( 'Footnote' ); if ( $this->mInFootnote ) { return htmlspecialchars( "<note>$str</note>" ); } else { $this->mInFootnote = true; $ret = $this->guardedNote( $str, $argv, $parser ); $this->mInFootnote = false; return $ret; } } function guardedNote( $str, $argv, $parser ) { $this->mParser = $parser; # The key here is the "name" attribute. $key = $this->noteArg( $argv ); if( $str === '' ) { # <note ...></note>. This construct is always invalid: either # it's a contentful note, or it's a named duplicate and should # be <note ... />. return $this->error( 'footnote_error_ref_no_input' ); } if( $key === false ) { # TODO: Comment this case; what does this condition mean? return $this->error( 'footnote_error_ref_too_many_keys' ); } if( $str === null and $key === null ) { # Something like <note />; this makes no sense. return $this->error( 'footnote_error_ref_no_key' ); } if( preg_match( '/^[0-9]+$/', $key ) ) { # Numeric names mess up the resulting id's, potentially produ- # cing duplicate id's in the XHTML. The Right Thing To Do # would be to mangle them, but it's not really high-priority # (and would produce weird id's anyway). return $this->error( 'footnote_error_ref_numeric_key' ); } if( is_string( $key ) or is_string( $str ) ) { # We don't care about the content: if the key exists, the note # is presumptively valid. Either it stores a new note, or re- # fers to an existing one. If it refers to a nonexistent note, # we'll figure that out later. Likewise it's definitely valid # if there's any content, regardless of key. return $this->stack( $str, $key ); } # Not clear how we could get here, but something is probably # wrong with the types. Let's fail fast. $this->croak( 'footnote_error_key_str_invalid', serialize( "$str; $key" ) ); } /** * Parse the arguments to the <note> tag * * @static * * @param array $argv The argument vector * @return mixed false on invalid input, a string on valid * input and null on no input */ function noteArg( $argv ) { $cnt = count( $argv ); if ( $cnt > 1 ) // There should only be one key return false; else if ( $cnt == 1 ) if ( isset( $argv['name'] ) ) // Key given. return $this->validateName( array_shift( $argv ) ); else // Invalid key return false; else // No key return null; } /** * Since the key name is used in an XHTML id attribute, it must * conform to the validity rules. The restriction to begin with * a letter is lifted since footnotes have their own prefix. * * @fixme merge this code with the various section name transformations * @fixme double-check for complete validity * @return string if valid, false if invalid */ function validateName( $name ) { if( preg_match( '/^[A-Za-z0-9:_.-]*$/i', $name ) ) { return $name; } else { // WARNING: CRAPPY CUT AND PASTE MAKES BABY JESUS CRY $text = urlencode( str_replace( ' ', '_', $name ) ); $replacearray = array( '%3A' => ':', '%' => '.' ); return str_replace( array_keys( $replacearray ), array_values( $replacearray ), $text ); } } /** * Populate $this->mFootnotes based on input and arguments to <note> * * @param string $str Input from the <note> tag * @param mixed $key Argument to the <note> tag as returned by $this->noteArg() * @return string */ function stack( $str, $key = null ) { if ( $key === null ) { // No key $this->mFootnotes[] = $str; return $this->linkNote( $this->mInCnt++ ); } else if ( is_string( $key ) ) // Valid key if ( ! isset( $this->mFootnotes[$key] ) || ! is_array( $this->mFootnotes[$key] ) ) { // First occurrence $this->mFootnotes[$key] = array( 'text' => $str, 'count' => 0, 'number' => ++$this->mOutCnt ); return $this->linkNote( $key, $this->mFootnotes[$key]['count'], $this->mFootnotes[$key]['number'] ); } else { // We've been here before if ( $this->mFootnotes[$key]['text'] === null && $str !== '' ) { // If no text found before, use this text $this->mFootnotes[$key]['text'] = $str; }; return $this->linkNote( $key, ++$this->mFootnotes[$key]['count'], $this->mFootnotes[$key]['number'] ); } else $this->croak( 'footnote_error_stack_invalid_input', serialize( array( $key, $str ) ) ); } /** * Callback function for <notes> * * @param string $str Input * @param array $argv Arguments * @return string */ function footnotes( $str, $argv, $parser ) { wfLoadExtensionMessages( 'Footnote' ); if ( $this->mInFootnote ) { if ( is_null( $str ) ) { return htmlspecialchars( "<notes/>" ); } else { return htmlspecialchars( "<notes>$str</notes>" ); } } else { $this->mInFootnote = true; $ret = $this->guardedFootnotes( $str, $argv, $parser ); $this->mInFootnote = false; return $ret; } } function guardedFootnotes( $str, $argv, $parser ) { $this->mParser = $parser; if ( $str !== null ) return $this->error( 'footnote_error_references_invalid_input' ); else if ( count( $argv ) ) return $this->error( 'footnote_error_references_invalid_parameters' ); else return $this->footnotesFormat(); } /** * Make output to be returned from the footnotes() function * * @return string XHTML ready for output */ function footnotesFormat() { if ( count( $this->mFootnotes ) == 0 ) return ''; wfProfileIn( __METHOD__ ); wfProfileIn( __METHOD__ .'-entries' ); $ent = array(); foreach ( $this->mFootnotes as $k => $v ) $ent[] = $this->footnotesFormatEntry( $k, $v ); $prefix = wfMsgForContentNoTrans( 'footnote_references_prefix' ); $suffix = wfMsgForContentNoTrans( 'footnote_references_suffix' ); $content = implode( "\n", $ent ); wfProfileOut( __METHOD__ .'-entries' ); wfProfileIn( __METHOD__ .'-parse' ); // Live hack: parse() adds two newlines on WM, can't reproduce it locally -ævar $ret = rtrim( $this->parse( $prefix . $content . $suffix ), "\n" ); wfProfileOut( __METHOD__ .'-parse' ); wfProfileOut( __METHOD__ ); return $ret; } /** * Format a single entry for the footnotesFormat() function * * @param string $key The key of the note * @param mixed $val The value of the note, string for anonymous * notes, array for user-supplied * @return string Wikitext */ function footnotesFormatEntry( $key, $val ) { // Anonymous note if ( ! is_array( $val ) ) return wfMsgForContentNoTrans( 'footnote_references_link_one', $this->footnotesKey( $key ), $this->noteKey( $key ), $val ); else if ($val['text']=='') return wfMsgForContentNoTrans( 'footnote_references_link_one', $this->footnotesKey( $key ), $this->noteKey( $key, $val['count'] ), $this->error( 'footnote_error_references_no_text', $key ) ); // Standalone named note, I want to format this like an // anonymous note because displaying "1. 1.1 Ref text" is // overkill and users frequently use named footnotes when they // don't need them for convenience else if ( $val['count'] === 0 ) return wfMsgForContentNoTrans( 'footnote_references_link_one', $this->footnotesKey( $key ), $this->noteKey( $key, $val['count'] ), ( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) ) ); // Named footnotes with >1 occurrences else { $links = array(); for ( $i = 0; $i <= $val['count']; ++$i ) { $links[] = wfMsgForContentNoTrans( 'footnote_references_link_many_format', $this->noteKey( $key, $i ), $this->footnotesFormatEntryNumericBacklinkLabel( $val['number'], $i, $val['count'] ), $this->footnotesFormatEntryAlternateBacklinkLabel( $i ) ); } $list = $this->listToText( $links ); return wfMsgForContentNoTrans( 'footnote_references_link_many', $this->footnotesKey( $key ), $list, ( $val['text'] != '' ? $val['text'] : $this->error( 'footnote_error_references_no_text', $key ) ) ); } } /** * Generate a numeric backlink given a base number and an * offset, e.g. $base = 1, $offset = 2; = 1.2 * Since bug #5525, it correctly does 1.9 -> 1.10 as well as 1.099 -> 1.100 * * @static * * @param int $base The base * @param int $offset The offset * @param int $max Maximum value expected. * @return string */ function footnotesFormatEntryNumericBacklinkLabel( $base, $offset, $max ) { global $wgContLang; $scope = strlen( $max ); $ret = $wgContLang->formatNum( $offset ); return $ret; } /** * Generate a custom format backlink given an offset, e.g. * $offset = 2; = c if $this->mBacklinkLabels = array( 'a', * 'b', 'c', ...). Return an error if the offset > the # of * array items * * @param int $offset The offset * * @return string */ function footnotesFormatEntryAlternateBacklinkLabel( $offset ) { if ( !isset( $this->mBacklinkLabels ) ) { $this->genBacklinkLabels(); } if ( isset( $this->mBacklinkLabels[$offset] ) ) { return $this->mBacklinkLabels[$offset]; } else { // Feed me! return $this->error( 'footnote_error_references_no_backlink_label' ); } } /** * Return an id for use in wikitext output based on a key and * optionally the # of it, used in <notes>, not <note> * (since otherwise it would link to itself) * * @static * * @param string $key The key * @param int $num The number of the key * @return string A key for use in wikitext */ function noteKey( $key, $num = null ) { $prefix = wfMsgForContent( 'footnote_reference_link_prefix' ); $suffix = wfMsgForContent( 'footnote_reference_link_suffix' ); if ( isset( $num ) ) $key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num ); return $prefix . $key . $suffix; } /** * Return an id for use in wikitext output based on a key and * optionally the # of it, used in <note>, not <notes> * (since otherwise it would link to itself) * * @static * * @param string $key The key * @param int $num The number of the key * @return string A key for use in wikitext */ function footnotesKey( $key, $num = null ) { $prefix = wfMsgForContent( 'footnote_references_link_prefix' ); $suffix = wfMsgForContent( 'footnote_references_link_suffix' ); if ( isset( $num ) ) $key = wfMsgForContentNoTrans( 'footnote_reference_link_key_with_num', $key, $num ); return $prefix . $key . $suffix; } /** * Generate a link (<sup ...) for the <note> element from a key * and return XHTML ready for output * * @param string $key The key for the link * @param int $count The # of the key, used for distinguishing * multiple occurrences of the same key * @param int $label The label to use for the link, I want to * use the same label for all occurrences of * the same named reference. * @return string */ function linkNote( $key, $count = null, $label = null ) { global $wgContLang; return $this->parse( wfMsgForContentNoTrans( 'footnote_reference_link', $this->noteKey( $key, $count ), $this->footnotesKey( $key ), $wgContLang->formatNum( is_null( $label ) ? $this->footnotesFormatEntryAlternateBacklinkLabel( ++$this->mOutCnt ) : $label ) ) ); } /** * This does approximately the same thing as * Language::listToText() but due to this being used for a * slightly different purpose (people might not want , as the * first separator and not 'and' as the second, and this has to * use messages from the content language) I'm rolling my own. * * @static * * @param array $arr The array to format * @return string */ function listToText( $arr ) { $cnt = count( $arr ); $sep = wfMsgForContentNoTrans( 'footnote_references_link_many_sep' ); $and = wfMsgForContentNoTrans( 'footnote_references_link_many_and' ); if ( $cnt == 1 ) // Enforce always returning a string return (string)$arr[0]; else { $t = array_slice( $arr, 0, $cnt - 1 ); return implode( $sep, $t ) . $and . $arr[$cnt - 1]; } } /** * Parse a given fragment and fix up Tidy's trail of blood on * it... * * @param string $in The text to parse * @return string The parsed text */ function parse( $in ) { if ( method_exists( $this->mParser, 'recursiveTagParse' ) ) { // New fast method return $this->mParser->recursiveTagParse( $in ); } else { // Old method $ret = $this->mParser->parse( $in, $this->mParser->mTitle, $this->mParser->mOptions, // Avoid whitespace buildup false, // Important, otherwise $this->clearState() // would get run every time <note> or // <notes> is called, fucking the whole // thing up. false ); $text = $ret->getText(); return $this->fixTidy( $text ); } } /** * Tidy treats all input as a block, it will e.g. wrap most * input in <p> if it isn't already, fix that and return the fixed text * * @static * * @param string $text The text to fix * @return string The fixed text */ function fixTidy( $text ) { global $wgUseTidy; if ( ! $wgUseTidy ) return $text; else { $text = preg_replace( '~^<p>\s*~', '', $text ); $text = preg_replace( '~\s*</p>\s*~', '', $text ); $text = preg_replace( '~\n$~', '', $text ); return $text; } } /** * Generate the labels to pass to the * 'footnote_references_link_many_format' message, the format is an * arbitary number of tokens separated by [\t\n ] */ function genBacklinkLabels() { wfProfileIn( __METHOD__ ); $text = wfMsgForContentNoTrans( 'footnote_references_link_many_format_backlink_labels' ); $this->mBacklinkLabels = preg_split( '#[\n\t ]#', $text ); wfProfileOut( __METHOD__ ); } /** * Gets run when Parser::clearState() gets run, since we don't * want the counts to transcend pages and other instances */ function clearState() { $this->mOutCnt = $this->mInCnt = 0; $this->mFootnotes = array(); return true; } /** * Initialize the parser hooks */ function setHooks() { global $wgParser, $wgHooks; $wgParser->setHook( 'note' , array( &$this, 'note' ) ); $wgParser->setHook( 'notes' , array( &$this, 'footnotes' ) ); $wgHooks['ParserClearState'][] = array( &$this, 'clearState' ); } /** * Return an error message based on an error ID * * @param string $key Message name for the error * @param string $param Parameter to pass to the message * @return string XHTML ready for output */ function error( $key, $param=null ) { # We rely on the fact that PHP is okay with passing unused argu- # ments to functions. If $1 is not used in the message, wfMsg will # just ignore the extra parameter. return $this->parse( '<strong class="error">' . wfMsg( 'footnote_error', wfMsg( $key, $param ) ) . '</strong>' ); } /** * Die with a backtrace if something happens in the code which * shouldn't have * * @param int $error ID for the error * @param string $data Serialized error data */ function croak( $error, $data ) { wfDebugDieBacktrace( wfMsgForContent( 'footnote_croak', $this->error( $error ), $data ) ); } /**#@-*/ } new Footnote; } /**#@-*/