cb(), $subject, $flags ); } /** * More or less "markup-safe" explode() * Ignores any instances of the separator inside <...> * @param string $separator * @param string $text * @return array */ static function explodeMarkup( $separator, $text ) { $placeholder = "\x00"; // Remove placeholder instances $text = str_replace( $placeholder, '', $text ); // Replace instances of the separator inside HTML-like tags with the placeholder $replacer = new DoubleReplacer( $separator, $placeholder ); $cleaned = StringUtils::delimiterReplaceCallback( '<', '>', $replacer->cb(), $text ); // Explode, then put the replaced separators back in $items = explode( $separator, $cleaned ); foreach ( $items as $i => $str ) { $items[$i] = str_replace( $placeholder, $separator, $str ); } return $items; } /** * Escape a string to make it suitable for inclusion in a preg_replace() * replacement parameter. * * @param string $string * @return string */ static function escapeRegexReplacement( $string ) { $string = str_replace( '\\', '\\\\', $string ); $string = str_replace( '$', '\\$', $string ); return $string; } /** * Workalike for explode() with limited memory usage. * Returns an Iterator * @param string $separator * @param string $subject * @return ArrayIterator|ExplodeIterator */ static function explode( $separator, $subject ) { if ( substr_count( $subject, $separator ) > 1000 ) { return new ExplodeIterator( $separator, $subject ); } else { return new ArrayIterator( explode( $separator, $subject ) ); } } } /** * Base class for "replacers", objects used in preg_replace_callback() and * StringUtils::delimiterReplaceCallback() */ class Replacer { /** * @return array */ function cb() { return array( &$this, 'replace' ); } } /** * Class to replace regex matches with a string similar to that used in preg_replace() */ class RegexlikeReplacer extends Replacer { private $r; /** * @param string $r */ function __construct( $r ) { $this->r = $r; } /** * @param array $matches * @return string */ function replace( $matches ) { $pairs = array(); foreach ( $matches as $i => $match ) { $pairs["\$$i"] = $match; } return strtr( $this->r, $pairs ); } } /** * Class to perform secondary replacement within each replacement string */ class DoubleReplacer extends Replacer { /** * @param mixed $from * @param mixed $to * @param int $index */ function __construct( $from, $to, $index = 0 ) { $this->from = $from; $this->to = $to; $this->index = $index; } /** * @param array $matches * @return mixed */ function replace( $matches ) { return str_replace( $this->from, $this->to, $matches[$this->index] ); } } /** * Class to perform replacement based on a simple hashtable lookup */ class HashtableReplacer extends Replacer { private $table, $index; /** * @param array $table * @param int $index */ function __construct( $table, $index = 0 ) { $this->table = $table; $this->index = $index; } /** * @param array $matches * @return mixed */ function replace( $matches ) { return $this->table[$matches[$this->index]]; } } /** * Replacement array for FSS with fallback to strtr() * Supports lazy initialisation of FSS resource */ class ReplacementArray { private $data = false; private $fss = false; /** * Create an object with the specified replacement array * The array should have the same form as the replacement array for strtr() * @param array $data */ function __construct( $data = array() ) { $this->data = $data; } /** * @return array */ function __sleep() { return array( 'data' ); } function __wakeup() { $this->fss = false; } /** * Set the whole replacement array at once * @param array $data */ function setArray( $data ) { $this->data = $data; $this->fss = false; } /** * @return array|bool */ function getArray() { return $this->data; } /** * Set an element of the replacement array * @param string $from * @param string $to */ function setPair( $from, $to ) { $this->data[$from] = $to; $this->fss = false; } /** * @param array $data */ function mergeArray( $data ) { $this->data = array_merge( $this->data, $data ); $this->fss = false; } /** * @param ReplacementArray $other */ function merge( $other ) { $this->data = array_merge( $this->data, $other->data ); $this->fss = false; } /** * @param string $from */ function removePair( $from ) { unset( $this->data[$from] ); $this->fss = false; } /** * @param array $data */ function removeArray( $data ) { foreach ( $data as $from => $to ) { $this->removePair( $from ); } $this->fss = false; } /** * @param string $subject * @return string */ function replace( $subject ) { if ( function_exists( 'fss_prep_replace' ) ) { wfProfileIn( __METHOD__ . '-fss' ); if ( $this->fss === false ) { $this->fss = fss_prep_replace( $this->data ); } $result = fss_exec_replace( $this->fss, $subject ); wfProfileOut( __METHOD__ . '-fss' ); } else { wfProfileIn( __METHOD__ . '-strtr' ); $result = strtr( $subject, $this->data ); wfProfileOut( __METHOD__ . '-strtr' ); } return $result; } } /** * An iterator which works exactly like: * * foreach ( explode( $delim, $s ) as $element ) { * ... * } * * Except it doesn't use 193 byte per element */ class ExplodeIterator implements Iterator { // The subject string private $subject, $subjectLength; // The delimiter private $delim, $delimLength; // The position of the start of the line private $curPos; // The position after the end of the next delimiter private $endPos; // The current token private $current; /** * Construct a DelimIterator * @param string $delim * @param string $subject */ function __construct( $delim, $subject ) { $this->subject = $subject; $this->delim = $delim; // Micro-optimisation (theoretical) $this->subjectLength = strlen( $subject ); $this->delimLength = strlen( $delim ); $this->rewind(); } function rewind() { $this->curPos = 0; $this->endPos = strpos( $this->subject, $this->delim ); $this->refreshCurrent(); } function refreshCurrent() { if ( $this->curPos === false ) { $this->current = false; } elseif ( $this->curPos >= $this->subjectLength ) { $this->current = ''; } elseif ( $this->endPos === false ) { $this->current = substr( $this->subject, $this->curPos ); } else { $this->current = substr( $this->subject, $this->curPos, $this->endPos - $this->curPos ); } } function current() { return $this->current; } /** * @return int|bool Current position or boolean false if invalid */ function key() { return $this->curPos; } /** * @return string */ function next() { if ( $this->endPos === false ) { $this->curPos = false; } else { $this->curPos = $this->endPos + $this->delimLength; if ( $this->curPos >= $this->subjectLength ) { $this->endPos = false; } else { $this->endPos = strpos( $this->subject, $this->delim, $this->curPos ); } } $this->refreshCurrent(); return $this->current; } /** * @return bool */ function valid() { return $this->curPos !== false; } }