class ComfortableMexicanSofa::Content::Renderer

Processing content follows these stages:

string        - Text with tags. like this: "some {{cms:fragment content}} text"
tokenization  - Splits string into a list of strings and hashes that define tags
                Example: ["some ", {tag_class: "fragment", tag_params: ""}, " text"]
nodefying     - Initializes Tag instances from tag hashes and returns list
                like this: ["some ", (FragmentTagInstance), " text"]
rendering     - Recursively iterates through nodes. Tag instances get their
                `render` method called. Result of that is tokenized, nodefied
                and rendered once again until there are no tags to expand.
                Resulting list is flattened and joined into a final rendered string.

Constants

MAX_DEPTH
TAG_REGEX

tags are in this format: {{ cms:tag_class params }}

Public Class Methods

new(context) click to toggle source

@param [Comfy::Cms::WithFragments, nil] context

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 43
def initialize(context)
  @context = context
  @depth   = 0
end
register_tag(name, klass) click to toggle source

@param [String] name @param [Class<ComfortableMexicanSofa::Content::Tag>] klass

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 36
def register_tag(name, klass)
  tags[name.to_s] = klass
end
tags() click to toggle source

@return [Hash<String, Class<ComfortableMexicanSofa::Content::Tag>>]

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 30
def tags
  @tags ||= {}
end

Public Instance Methods

nodes(tokens) click to toggle source

Constructing node tree for content. It's a list of strings and tags with their own `nodes` method that has array of strings and tags with their own `nodes` method that… you get the idea. @param [Array<String, {Symbol => String}>] tokens @return [Array<String, ComfortableMexicanSofa::Content::Tag>]

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 101
def nodes(tokens)
  nodes = [[]]
  tokens.each do |token|
    case token

    # tag signature
    when Hash
      case tag_class = token[:tag_class]

      # This handles {{cms:end}} tag. Stopping collecting block nodes.
      when "end"
        if nodes.count == 1
          raise SyntaxError, "closing unopened block"
        end
        nodes.pop

      else
        # @type [Class<ComfortableMexicanSofa::Content::Tag>]
        klass = self.class.tags[tag_class] ||
          raise(SyntaxError, "Unrecognized tag: #{token[:source]}")

        # @type [ComfortableMexicanSofa::Content::Tag]
        tag = klass.new(
          context:  @context,
          params:   ComfortableMexicanSofa::Content::ParamsParser.new(token[:tag_params]).params,
          source:   token[:source]
        )
        nodes.last << tag

        # If it's a block tag we start collecting nodes into it
        if tag.is_a?(ComfortableMexicanSofa::Content::Block)
          nodes << tag.nodes
        end
      end

    # text
    else
      nodes.last << token
    end
  end

  if nodes.count > 1
    raise SyntaxError, "unclosed block detected"
  end

  nodes.flatten
end
render(nodes, allow_erb = ComfortableMexicanSofa.config.allow_erb) click to toggle source

This is how we render content out. Takes context (cms page) and content nodes @param [Array<String, ComfortableMexicanSofa::Content::Tag>] @param [Boolean] allow_erb

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 52
def render(nodes, allow_erb = ComfortableMexicanSofa.config.allow_erb)
  if (@depth += 1) > MAX_DEPTH
    raise Error, "Deep tag nesting or recursive nesting detected"
  end

  nodes.map do |node|
    case node
    when String
      sanitize_erb(node, allow_erb)
    else
      tokens  = tokenize(node.render)
      nodes   = nodes(tokens)
      render(nodes, allow_erb || node.allow_erb?)
    end
  end.flatten.join
end
sanitize_erb(string, allow_erb) click to toggle source
# File lib/comfortable_mexican_sofa/content/renderer.rb, line 69
def sanitize_erb(string, allow_erb)
  if allow_erb
    string.to_s
  else
    string.to_s.gsub("<%", "&lt;%").gsub("%>", "%&gt;")
  end
end
tokenize(string) click to toggle source

Splitting text with tags into tokens we can process down the line @return [Array<String, {Symbol => String}>]

# File lib/comfortable_mexican_sofa/content/renderer.rb, line 79
def tokenize(string)
  tokens = []
  ss = StringScanner.new(string.to_s)
  while (string = ss.scan_until(TAG_REGEX))
    text = string.sub(ss[0], "")
    tokens << text unless text.empty?
    tokens << {
      tag_class:  ss[:class],
      tag_params: ss[:params].strip,
      source:     ss[0]
    }
  end
  text = ss.rest
  tokens << text if text.present?
  tokens
end