class RubyBBCode::TagSifter

TagSifter is in charge of building up the BBTree with nodes as it parses through the supplied text such as

"[b]I'm bold and the next word is [i]ITALIC[/i][b]"

Attributes

bbtree[R]
errors[R]

Public Class Methods

new(text_to_parse, dictionary, escape_html = true) click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 10
def initialize(text_to_parse, dictionary, escape_html = true)
  @text = escape_html ? text_to_parse.gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;') : text_to_parse

  @dictionary = dictionary # dictionary containing all allowed/defined tags
  @bbtree = BBTree.new(nodes: TagCollection.new)
  @ti = nil
  @errors = []
end

Public Instance Methods

process_text() click to toggle source

BBTree#process_text is responsible for parsing the actual BBCode text and converting it into a 'syntax tree' of nodes, each node represeting either a tag type or content for a tag once this tree is built, the to_html method can be invoked where the tree is finally converted into HTML syntax.

# File lib/ruby-bbcode/tag_sifter.rb, line 27
def process_text
  @text.scan(TagInfo::REGEX) do |tag_info|
    @ti = TagInfo.new(tag_info, @dictionary)

    validate_element

    case @ti.type
    when :opening_tag
      element = { is_tag: true, tag: @ti[:tag], definition: @ti.definition, opening_whitespace: @ti.whitespace, errors: @ti[:errors], nodes: TagCollection.new }
      element[:invalid_quick_param] = true if @ti.invalid_quick_param?
      element[:params] = get_formatted_element_params

      if self_closing_tag_reached_a_closer?
        transfer_whitespace_to_closing_tag
        @bbtree.retrogress_bbtree
      end

      @bbtree.build_up_new_tag(element)

      @bbtree.escalate_bbtree(element)
    when :text
      tag_def = @bbtree.current_node.definition
      if tag_def && (tag_def[:multi_tag] == true)
        set_multi_tag_to_actual_tag
        tag_def = @bbtree.current_node.definition
      end

      if within_open_tag? && tag_def[:require_between]
        between = get_formatted_between
        @bbtree.current_node[:between] = between
        if use_text_as_parameter?
          value_array = tag_def[:quick_param_format].nil? ? true : between.scan(tag_def[:quick_param_format])[0]
          if value_array.nil?
            if @ti[:invalid_quick_param].nil?
              # Add text element (with error(s))
              add_element = true

              # ...and clear between, as this would result in two 'between' texts
              @bbtree.current_node[:between] = ''
            end
          else
            # Between text can be used as (first) parameter
            @bbtree.current_node[:params][tag_def[:param_tokens][0][:token]] = between
          end
        end
        # Don't add this text node, as it is used as between (and might be used as first param)
        next unless add_element
      end

      create_text_element
    when :closing_tag
      @bbtree.current_node[:closing_whitespace] = @ti.whitespace
      if @ti[:wrong_closing]
        # Convert into text, so it
        @ti.handle_tag_as_text
        create_text_element
      else
        if parent_of_self_closing_tag? && within_open_tag?
          transfer_whitespace_to_closing_tag
          @bbtree.retrogress_bbtree
        end
        @bbtree.retrogress_bbtree
      end
    end
  end

  validate_all_tags_closed_off
  validate_stack_level_too_deep_potential
end
valid?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 19
def valid?
  @errors.empty?
end

Private Instance Methods

add_tag_error(message, tag = @ti) click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 377
def add_tag_error(message, tag = @ti)
  @errors << message
  tag[:errors] << message
end
create_text_element() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 145
def create_text_element
  element = { is_tag: false, text: @ti.text, errors: @ti[:errors] }
  @bbtree.build_up_new_tag(element)
end
expecting_a_closing_tag?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 354
def expecting_a_closing_tag?
  @bbtree.expecting_a_closing_tag?
end
get_actual_tag() click to toggle source

The media tag support multiple other tags, this method checks the tag url param to find actual tag type (to use)

# File lib/ruby-bbcode/tag_sifter.rb, line 132
def get_actual_tag
  supported_tags = @bbtree.current_node[:definition][:supported_tags]

  supported_tags.each do |tag|
    regex_list = @dictionary[tag][:url_matches]

    regex_list.each do |regex|
      return tag if regex =~ @ti.text
    end
  end
  :tag_not_found
end
get_formatted_between() click to toggle source

Get 'between tag' for tag

# File lib/ruby-bbcode/tag_sifter.rb, line 161
def get_formatted_between
  between = @ti[:text]
  # perform special formatting for certain tags
  between = match_url_id(between, @bbtree.current_node.definition[:url_matches]) if @bbtree.current_node.definition[:url_matches]
  between
end
get_formatted_element_params() click to toggle source

Gets the params, and format them if needed…

# File lib/ruby-bbcode/tag_sifter.rb, line 151
def get_formatted_element_params
  params = @ti[:params]
  if @ti.definition[:url_matches]
    # perform special formatting for certain tags
    params[:url] = match_url_id(params[:url], @ti.definition[:url_matches])
  end
  params
end
has_no_text_node?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 350
def has_no_text_node?
  @bbtree.current_node[:nodes].blank? || @bbtree.current_node[:nodes][0][:text].nil?
end
last_tag_fit_in_this_tag?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 276
def last_tag_fit_in_this_tag?
  @ti.definition[:only_allow]&.each do |tag|
    return true if tag == @bbtree.current_node[:tag]
  end
  false
end
match_url_id(url, regex_matches) click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 168
def match_url_id(url, regex_matches)
  regex_matches.each do |regex|
    if url =~ regex
      id = Regexp.last_match(1)
      return id
    end
  end

  url # if we couldn't find a match, then just return the url, hopefully it's a valid ID...
end
node_last_text(node) click to toggle source

Return the node that holds the last piece of text for the given node (self or child)

# File lib/ruby-bbcode/tag_sifter.rb, line 110
def node_last_text(node)
  return node if node.type == :text
  return node_last_text(node.children[-1]) unless node.children.empty?

  # node does not hold text
  nil
end
parent_has_constraints_on_children?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 371
def parent_has_constraints_on_children?
  @bbtree.parent_has_constraints_on_children?
end
parent_of_self_closing_tag?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 270
def parent_of_self_closing_tag?
  was_last_tag_self_closable = @bbtree.current_node[:definition][:self_closable] unless @bbtree.current_node[:definition].nil?

  was_last_tag_self_closable && last_tag_fit_in_this_tag?
end
parent_tag() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 367
def parent_tag
  @bbtree.parent_tag
end
self_closing_tag_reached_a_closer?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 223
def self_closing_tag_reached_a_closer?
  @ti.definition[:self_closable] && (@bbtree.current_node[:tag] == @ti[:tag])
end
set_multi_tag_to_actual_tag() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 118
def set_multi_tag_to_actual_tag
  # Try to find the actual tag
  tag = get_actual_tag
  if tag == :tag_not_found
    # Add error
    add_tag_error "Unknown multi-tag type for [#{@bbtree.current_node[:tag]}]", @bbtree.current_node
  else
    # Update current_node with found information, so it behaves as the actual tag
    @bbtree.current_node[:definition] = @dictionary[tag]
    @bbtree.current_node[:tag] = tag
  end
end
throw_child_requires_specific_parent_error() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 321
def throw_child_requires_specific_parent_error
  err = "[#{@ti[:tag]}] can only be used in [#{@ti.definition[:only_in].to_sentence(to_sentence_bbcode_tags)}]"
  err += ", so using it in a [#{parent_tag[:tag]}] tag is not allowed" if expecting_a_closing_tag?
  add_tag_error err
end
throw_invalid_quick_param_error(tag) click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 327
def throw_invalid_quick_param_error(tag)
  add_tag_error tag.definition[:quick_param_format_description].gsub('%param%', tag[:invalid_quick_param]), tag
end
throw_parent_prohibits_this_child_error() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 331
def throw_parent_prohibits_this_child_error
  allowed_tags = parent_tag[:definition][:only_allow]
  err = "[#{parent_tag[:tag]}] can only contain [#{allowed_tags.to_sentence(to_sentence_bbcode_tags)}] tags, so "
  err += "[#{@ti[:tag]}]" if @ti.element_is_tag?
  err += "\"#{@ti[:text]}\"" unless @ti.element_is_tag?
  err += ' is not allowed'
  add_tag_error err
end
throw_stack_level_will_be_too_deep_error() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 340
def throw_stack_level_will_be_too_deep_error
  @errors << 'Stack level would go too deep.  You must be trying to process a text containing thousands of BBTree nodes at once.  (limit around 2300 tags containing 2,300 strings).  Check RubyBBCode::TagCollection#to_html to see why this validation is needed.'
end
to_sentence_bbcode_tags() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 344
def to_sentence_bbcode_tags
  { words_connector: '], [',
    two_words_connector: '] and [',
    last_word_connector: '] and [' }
end
transfer_whitespace_to_closing_tag() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 99
def transfer_whitespace_to_closing_tag
  last_text_node = node_last_text(parent_tag)
  unless last_text_node.nil?
    last_text_node[:text].scan(/(\s+)$/) do |result|
      parent_tag[:closing_whitespace] = result[0]
      last_text_node[:text] = last_text_node[:text][0..-result[0].length - 1]
    end
  end
end
use_text_as_parameter?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 362
def use_text_as_parameter?
  tag = @bbtree.current_node
  tag.definition[:allow_between_as_param] && tag.params_not_set? && !tag.invalid_quick_param?
end
valid_closing_element?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 241
def valid_closing_element?
  if @ti.element_is_closing_tag?

    if parent_tag.nil?
      add_tag_error "Closing tag [/#{@ti[:tag]}] doesn't match an opening tag"
      @ti[:wrong_closing] = true
      return false
    end

    if (parent_tag[:tag] != @ti[:tag]) && !parent_of_self_closing_tag?
      # Make an exception for 'supported tags'
      if @ti.definition[:supported_tags].nil? || !@ti.definition[:supported_tags].include?(parent_tag[:tag])
        add_tag_error "Closing tag [/#{@ti[:tag]}] doesn't match [#{parent_tag[:tag]}]"
        @ti[:wrong_closing] = true
        return false
      end
    end

    tag_def = @bbtree.current_node.definition
    if tag_def[:require_between] && @bbtree.current_node[:between].nil? && @bbtree.current_node[:nodes].empty?
      err = "No text between [#{@ti[:tag]}] and [/#{@ti[:tag]}] tags."
      err = "Cannot determine multi-tag type: #{err}" if tag_def[:multi_tag]
      add_tag_error err, @bbtree.current_node
      return false
    end
  end
  true
end
valid_constraints_on_child?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 227
def valid_constraints_on_child?
  if within_open_tag? && parent_has_constraints_on_children?
    # Check if the found tag is allowed
    last_tag_def = parent_tag[:definition]
    allowed_tags = last_tag_def[:only_allow]
    if (!@ti.element_is_tag? && (last_tag_def[:require_between] != true) && (@ti[:text].lstrip != '')) || (@ti.element_is_tag? && (allowed_tags.include?(@ti[:tag]) == false)) # TODO: refactor this, it's just too long
      # Last opened tag does not allow tag
      throw_parent_prohibits_this_child_error
      return false
    end
  end
  true
end
valid_opening_tag?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 194
def valid_opening_tag?
  if @ti.element_is_opening_tag?
    if @ti.only_allowed_in_parent_tags? && (!within_open_tag? || !@ti.allowed_in?(parent_tag[:tag])) && !self_closing_tag_reached_a_closer?
      # Tag doesn't belong in the last opened tag
      throw_child_requires_specific_parent_error
      return false
    end

    if @ti.invalid_quick_param?
      throw_invalid_quick_param_error @ti
      return false
    end

    # Note that if allow_between_as_param is true, other checks already test the (correctness of the) 'between parameter'
    unless @ti.definition[:param_tokens].nil? || (@ti.definition[:allow_between_as_param] == true)
      # Check if all required parameters are added
      @ti.definition[:param_tokens].each do |token|
        add_tag_error "Tag [#{@ti[:tag]}] must have '#{token[:token]}' parameter" if @ti[:params][token[:token]].nil? && token[:optional].nil?
      end

      # Check if no 'custom parameters' are added
      @ti[:params].keys.each do |token|
        add_tag_error "Tag [#{@ti[:tag]}] doesn't have a '#{token}' parameter" if @ti.definition[:param_tokens].find { |param_token| param_token[:token] == token }.nil?
      end
    end
  end
  true
end
valid_param_supplied_as_text?() click to toggle source

This validation is for text elements with between text that might be construed as a param. The validation code checks if the params match constraints imposed by the node/tag/parent.

# File lib/ruby-bbcode/tag_sifter.rb, line 287
def valid_param_supplied_as_text?
  tag_def = @bbtree.current_node.definition

  if within_open_tag? && use_text_as_parameter? && @ti.element_is_tag? && has_no_text_node?
    add_tag_error 'between parameter must be plain text'
    return false
  end

  # this conditional ensures whether the validation is apropriate to this tag type
  if @ti.element_is_text? && within_open_tag? && tag_def[:require_between] && use_text_as_parameter? && !tag_def[:quick_param_format].nil?

    # check if valid
    if @ti[:text].match(tag_def[:quick_param_format]).nil?
      add_tag_error tag_def[:quick_param_format_description].gsub('%param%', @ti[:text])
      return false
    end
  end
  true
end
valid_text_or_opening_element?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 186
def valid_text_or_opening_element?
  if @ti.element_is_text? || @ti.element_is_opening_tag?
    return false unless valid_opening_tag?
    return false unless valid_constraints_on_child?
  end
  true
end
validate_all_tags_closed_off() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 307
def validate_all_tags_closed_off
  return unless expecting_a_closing_tag?

  # if we're still expecting a closing tag and we've come to the end of the string... throw error(s)
  @bbtree.tags_list.each do |tag|
    add_tag_error "[#{tag[:tag]}] not closed", tag
    tag[:closed] = false
  end
end
validate_element() click to toggle source

Validates the element

# File lib/ruby-bbcode/tag_sifter.rb, line 180
def validate_element
  return unless valid_text_or_opening_element?
  return unless valid_closing_element?
  return unless valid_param_supplied_as_text?
end
validate_stack_level_too_deep_potential() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 317
def validate_stack_level_too_deep_potential
  throw_stack_level_will_be_too_deep_error if @bbtree.nodes.count > 2200
end
within_open_tag?() click to toggle source
# File lib/ruby-bbcode/tag_sifter.rb, line 358
def within_open_tag?
  @bbtree.within_open_tag?
end