+ /**
+ * parse any parentheses in format ((title|part|part))
+ * and call callbacks to get a replacement text for any found piece
+ *
+ * @param string $text The text to parse
+ * @param array $callbacks rules in form:
+ * '{' => array( # opening parentheses
+ * 'end' => '}', # closing parentheses
+ * 'cb' => array(2 => callback, # replacement callback to call if {{..}} is found
+ * 4 => callback # replacement callback to call if {{{{..}}}} is found
+ * )
+ * )
+ * @access private
+ */
+ function replace_callback ($text, $callbacks) {
+ $openingBraceStack = array(); # this array will hold a stack of parentheses which are not closed yet
+ $lastOpeningBrace = -1; # last not closed parentheses
+
+ for ($i = 0; $i < strlen($text); $i++) {
+ # check for any opening brace
+ $rule = null;
+ $nextPos = -1;
+ foreach ($callbacks as $key => $value) {
+ $pos = strpos ($text, $key, $i);
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)) {
+ $rule = $value;
+ $nextPos = $pos;
+ }
+ }
+
+ if ($lastOpeningBrace >= 0) {
+ $pos = strpos ($text, $openingBraceStack[$lastOpeningBrace]['braceEnd'], $i);
+
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
+ $rule = null;
+ $nextPos = $pos;
+ }
+
+ $pos = strpos ($text, '|', $i);
+
+ if (false !== $pos && (-1 == $nextPos || $pos < $nextPos)){
+ $rule = null;
+ $nextPos = $pos;
+ }
+ }
+
+ if ($nextPos == -1)
+ break;
+
+ $i = $nextPos;
+
+ # found openning brace, lets add it to parentheses stack
+ if (null != $rule) {
+ $piece = array('brace' => $text[$i],
+ 'braceEnd' => $rule['end'],
+ 'count' => 1,
+ 'title' => '',
+ 'parts' => null);
+
+ # count openning brace characters
+ while ($i+1 < strlen($text) && $text[$i+1] == $piece['brace']) {
+ $piece['count']++;
+ $i++;
+ }
+
+ $piece['startAt'] = $i+1;
+ $piece['partStart'] = $i+1;
+
+ # we need to add to stack only if openning brace count is enough for any given rule
+ foreach ($rule['cb'] as $cnt => $fn) {
+ if ($piece['count'] >= $cnt) {
+ $lastOpeningBrace ++;
+ $openingBraceStack[$lastOpeningBrace] = $piece;
+ break;
+ }
+ }
+
+ continue;
+ }
+ else if ($lastOpeningBrace >= 0) {
+ # first check if it is a closing brace
+ if ($openingBraceStack[$lastOpeningBrace]['braceEnd'] == $text[$i]) {
+ # lets check if it is enough characters for closing brace
+ $count = 1;
+ while ($i+$count < strlen($text) && $text[$i+$count] == $text[$i])
+ $count++;
+
+ # if there are more closing parentheses than opening ones, we parse less
+ if ($openingBraceStack[$lastOpeningBrace]['count'] < $count)
+ $count = $openingBraceStack[$lastOpeningBrace]['count'];
+
+ # check for maximum matching characters (if there are 5 closing characters, we will probably need only 3 - depending on the rules)
+ $matchingCount = 0;
+ $matchingCallback = null;
+ foreach ($callbacks[$openingBraceStack[$lastOpeningBrace]['brace']]['cb'] as $cnt => $fn) {
+ if ($count >= $cnt && $matchingCount < $cnt) {
+ $matchingCount = $cnt;
+ $matchingCallback = $fn;
+ }
+ }
+
+ if ($matchingCount == 0) {
+ $i += $count - 1;
+ continue;
+ }
+
+ # lets set a title or last part (if '|' was found)
+ if (null === $openingBraceStack[$lastOpeningBrace]['parts'])
+ $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+ else
+ $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+
+ $pieceStart = $openingBraceStack[$lastOpeningBrace]['startAt'] - $matchingCount;
+ $pieceEnd = $i + $matchingCount;
+
+ if( is_callable( $matchingCallback ) ) {
+ $cbArgs = array (
+ 'text' => substr($text, $pieceStart, $pieceEnd - $pieceStart),
+ 'title' => trim($openingBraceStack[$lastOpeningBrace]['title']),
+ 'parts' => $openingBraceStack[$lastOpeningBrace]['parts'],
+ 'lineStart' => (($pieceStart > 0) && ($text[$pieceStart-1] == '\n')),
+ );
+ # finally we can call a user callback and replace piece of text
+ $replaceWith = call_user_func( $matchingCallback, $cbArgs );
+ $text = substr($text, 0, $pieceStart) . $replaceWith . substr($text, $pieceEnd);
+ $i = $pieceStart + strlen($replaceWith) - 1;
+ }
+ else {
+ # null value for callback means that parentheses should be parsed, but not replaced
+ $i += $matchingCount - 1;
+ }
+
+ # reset last openning parentheses, but keep it in case there are unused characters
+ $piece = array('brace' => $openingBraceStack[$lastOpeningBrace]['brace'],
+ 'braceEnd' => $openingBraceStack[$lastOpeningBrace]['braceEnd'],
+ 'count' => $openingBraceStack[$lastOpeningBrace]['count'],
+ 'title' => '',
+ 'parts' => null,
+ 'startAt' => $openingBraceStack[$lastOpeningBrace]['startAt']);
+ $openingBraceStack[$lastOpeningBrace--] = null;
+
+ if ($matchingCount < $piece['count']) {
+ $piece['count'] -= $matchingCount;
+ $piece['startAt'] -= $matchingCount;
+ $piece['partStart'] = $piece['startAt'];
+ # do we still qualify for any callback with remaining count?
+ foreach ($callbacks[$piece['brace']]['cb'] as $cnt => $fn) {
+ if ($piece['count'] >= $cnt) {
+ $lastOpeningBrace ++;
+ $openingBraceStack[$lastOpeningBrace] = $piece;
+ break;
+ }
+ }
+ }
+ continue;
+ }
+
+ # lets set a title if it is a first separator, or next part otherwise
+ if ($text[$i] == '|') {
+ if (null === $openingBraceStack[$lastOpeningBrace]['parts']) {
+ $openingBraceStack[$lastOpeningBrace]['title'] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+ $openingBraceStack[$lastOpeningBrace]['parts'] = array();
+ }
+ else
+ $openingBraceStack[$lastOpeningBrace]['parts'][] = substr($text, $openingBraceStack[$lastOpeningBrace]['partStart'], $i - $openingBraceStack[$lastOpeningBrace]['partStart']);
+
+ $openingBraceStack[$lastOpeningBrace]['partStart'] = $i + 1;
+ }
+ }
+ }
+
+ return $text;
+ }
+