You are here

class XBBCodeFilter in Extensible BBCode 7

Same name and namespace in other branches
  1. 5 xbbcode-filter.php \XBBCodeFilter
  2. 6 xbbcode.filter.inc \XBBCodeFilter

@file The filtering class. This will be instanced for each filter, and then called to process a piece of text.

Hierarchy

Expanded class hierarchy of XBBCodeFilter

File

./xbbcode.filter.inc, line 8
The filtering class. This will be instanced for each filter, and then called to process a piece of text.

View source
class XBBCodeFilter {
  var $tags;

  /**
   * Construct a filter object from a bundle of tags, and the format ID.
   *
   * @param $tags
   *   Tag array.
   * @param $format
   *   Text format ID.
   */
  function __construct($tags, $filter, $format) {
    $this->tags = $tags;
    $this->format = $format;
    $this->filter = $filter;
    $this->autoclose_tags = $filter->settings['autoclose'];
  }

  /**
   * Execute the filter on a particular text.
   *
   * Note: This function makes use of substr() and strlen() instead of Drupal
   * wrappers. This is the correct approach as all offsets are calculated by
   * the PREG_OFFSET_CAPTURE setting of preg_match_all(), which returns
   * byte offsets rather than character offsets.
   *
   * @param $text
   *   The text to be filtered.
   *
   * @return
   *   HTML code.
   */
  function process($text) {

    // Find all opening and closing tags in the text.
    preg_match_all(XBBCODE_RE_TAG, $text, $tags, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
    if (!$tags) {
      return $text;
    }

    // Initialize the stack with a root tag, and the name tracker.
    $stack = array(
      new XBBCodeTagMatch(),
    );
    $open_by_name = array();
    foreach ($tags as $i => $tag) {
      $tag = $tags[$i] = new XBBCodeTagMatch($tag, $this);
      $open_by_name[$tag->name] = 0;
    }
    foreach ($tags as $tag) {

      // Case 1: The tag is opening, and known to the filter.
      if (!$tag->closing && isset($this->tags[$tag->name])) {

        // Add text before the new tag to the parent, then stack the new tag.
        end($stack)
          ->advance($text, $tag->start);

        // Stack the newly opened tag, or render it if it's selfclosing.
        if ($this->tags[$tag->name]->options->selfclosing) {
          $rendered = $this
            ->render_tag($tag);
          if ($rendered === NULL) {
            $rendered = $tag->element;
          }
          end($stack)
            ->append($rendered, $tag->end);
        }
        else {
          array_push($stack, $tag);
          $open_by_name[$tag->name]++;
        }
      }
      elseif ($tag->closing && !empty($open_by_name[$tag->name])) {

        // Find the last matching opening tag, breaking any unclosed tag since then.
        while (end($stack)->name != $tag->name) {
          $dangling = array_pop($stack);
          end($stack)
            ->break_tag($dangling);
          $open_by_name[$dangling->name]--;
        }
        end($stack)
          ->advance($text, $tag->start);
        $open_by_name[$tag->name]--;

        // If the tag forbids rendering its content, revert to the unrendered text.
        if ($this->tags[$tag->name]->options->nocode) {
          end($stack)
            ->revert($text);
        }
        if ($this->tags[$tag->name]->options->plain) {

          // We will double-encode entities only if non-encoded chars exist.
          if (end($stack)->content != htmlspecialchars(end($stack)->content, ENT_QUOTES, 'UTF-8', FALSE)) {
            end($stack)->content = check_plain(end($stack)->content);
          }
        }

        // Append the rendered HTML to the content of its parent tag.
        $current = array_pop($stack);
        $rendered = $this
          ->render_tag($current);
        if ($rendered === NULL) {
          $rendered = $current->element . $current->content . $tag->element;
        }
        end($stack)
          ->append($rendered, $tag->end);
      }
    }
    end($stack)->content .= substr($text, end($stack)->offset);
    if ($this->autoclose_tags) {
      while (count($stack) > 1) {

        // Render the unclosed tag and pop it off the stack
        $output = $this
          ->render_tag(array_pop($stack));
        end($stack)->content .= $output;
      }
    }
    else {
      while (count($stack) > 1) {
        $current = array_pop($stack);
        $content = $current->element . $current->content;
        end($stack)->content .= $content;
      }
    }
    return end($stack)->content;
  }

  /**
   * Render a single tag.
   *
   * @param $tag
   *   The complete match object, including its name, content and attributes.
   *
   * @return
   *   HTML code to insert in place of the tag and its content.
   */
  function render_tag(XBBCodeTagMatch $tag) {
    if ($callback = $this->tags[$tag->name]->callback) {
      return $callback($tag, $this);
    }
    else {
      $replace['{content}'] = $tag->content;
      $replace['{option}'] = $tag->option;
      foreach ($tag->attrs as $name => $value) {
        $replace['{' . $name . '}'] = $value;
      }
      $markup = str_replace(array_keys($replace), array_values($replace), $this->tags[$tag->name]->markup);

      // Make sure that unset placeholders are replaced with empty strings.
      $markup = preg_replace('/{\\w+}/', '', $markup);
      return $markup;
    }
  }

}

Members

Namesort descending Modifiers Type Description Overrides
XBBCodeFilter::$tags property
XBBCodeFilter::process function Execute the filter on a particular text.
XBBCodeFilter::render_tag function Render a single tag.
XBBCodeFilter::__construct function Construct a filter object from a bundle of tags, and the format ID.