learningtapestry/lcms-engine

View on GitHub
lib/doc_template/document.rb

Summary

Maintainability
B
4 hrs
Test Coverage
F
59%
# frozen_string_literal: true

module DocTemplate
  class Document
    MAX_PARSE_ITERATIONS = 300

    # Contains the list of tags for which no parts should be created
    TAGS_WITHOUT_PARTS = [
      Tags::DefaultTag::TAG_NAME,
      Tags::GlsTag::TAG_NAME,
      Tags::MaterialsTag::TAG_NAME,
      '#'
    ].freeze

    ELA_TG_TEMPLATE = Lcms::Engine::Engine.root.join 'lib', 'doc_template', 'templates', 'ela-teacher-guidance.html.erb'

    attr_accessor :parts

    def self.parse(nodes, opts = {})
      new.parse(nodes, opts)
    end

    def parse(nodes, opts = {})
      @nodes = nodes
      @opts = opts
      @parts = @opts[:parts] || []

      # find all tags except ones which were marked as parsed first and nested levels
      xpath = [%(*[not(contains(@data-parsed, "true"))]/#{::DocTemplate::STARTTAG_XPATH}),
               %(*//*[not(contains(@data-parsed, "true"))]/#{::DocTemplate::STARTTAG_XPATH})]
      while (node = @nodes.at_xpath(*xpath))
        # identify the tag, take the siblings or enclosing and send it to the
        # relative tag class to render it
        next unless (tag_node = node.parent)

        handle_invalid_tag tag_node
        parse_node tag_node
      end

      add_custom_nodes unless @opts.key?(:level) || @opts.key?(:material)

      self
    end

    def render
      @nodes.to_html
    end

    private

    def add_custom_nodes
      return unless @opts[:metadata].try(:subject).to_s.casecmp('ela').zero?
      return unless ela_teacher_guidance_allowed?

      ::DocTemplate.sanitizer.strip_content(@nodes)
      @nodes.prepend_child ela_teacher_guidance(@opts[:metadata], @opts[:context_type])
    end

    #
    # Check if we're getting the same tag again
    #
    def check_loop_tag(name, value)
      if @opts.dig(:last_tag, :name) == name && @opts.dig(:last_tag, :value) == value &&
         @opts.dig(:last_tag, :iteration) > DocTemplate::Document::MAX_PARSE_ITERATIONS
        raise ::Lcms::Engine::DocumentError, "Loop detected for tag #{name} with value #{value}"
      end
    end

    def ela_teacher_guidance(metadata, _context_type)
      @data = metadata
      @data.preparation = ::DocTemplate.sanitizer.strip_html_element(@data.preparation)
      template = File.read ELA_TG_TEMPLATE
      ERB.new(template).result(binding)
    end

    # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
    def ela_teacher_guidance_allowed?
      # only for G6 and G2
      # As stated on issue #240 and here https://github.com/learningtapestry/unbounded/pull/267#issuecomment-307870881
      g2 = @opts[:metadata]['grade'] == '2'
      g6 = @opts[:metadata]['grade'] == '6'
      return false unless g2 || g6

      # Additional filter for lessons
      # https://github.com/learningtapestry/unbounded/issues/311
      # https://github.com/learningtapestry/unbounded/issues/240

      # G2 Unit 1 apart from for Lessons: 6,10,11,12
      g2_u1 = g2 && @opts[:metadata]['unit'] == '1'
      return false if g2_u1 && %w(6 10 11 12).include?(@opts[:metadata]['lesson'])

      # G2 Unit 2 apart from for Lessons: 8,16,17,18
      g2_u2 = g2 && @opts[:metadata]['unit'] == '2'
      return false if g2_u2 && %w(8 16 17 18).include?(@opts[:metadata]['lesson'])

      # G2 Unit 3 apart from for Lessons: 8,14,15,16
      g2_u3 = g2 && @opts[:metadata]['unit'] == '3'
      return false if g2_u3 && %w(8 14 15 16).include?(@opts[:metadata]['lesson'])

      # G2 Unit 4 apart from for Lessons: 8,13,14,15
      g2_u4 = g2 && @opts[:metadata]['unit'] == '4'
      return false if g2_u4 && %w(8 13 14 15).include?(@opts[:metadata]['lesson'])

      # G2 Unit 5 apart from for Lessons: 5,10,11,12
      g2_u5 = g2 && @opts[:metadata]['unit'] == '5'
      return false if g2_u5 && %w(5 10 11 12).include?(@opts[:metadata]['lesson'])

      # G2 Unit 6 apart from for Lessons: 6,11,12,13
      g2_u6 = g2 && @opts[:metadata]['unit'] == '6'
      return false if g2_u6 && %w(6 11 12 13).include?(@opts[:metadata]['lesson'])

      # G2 Unit 7 apart from for Lessons: 6,11,12,13
      g2_u7 = g2 && @opts[:metadata]['unit'] == '7'
      return false if g2_u7 && %w(6 11 12 13).include?(@opts[:metadata]['lesson'])

      # G2 Unit 8 apart from for Lessons: 5,12,11,10
      g2_u8 = g2 && @opts[:metadata]['unit'] == '8'
      return false if g2_u8 && %w(5 12 11 10).include?(@opts[:metadata]['lesson'])

      # G2 Unit 9 apart from for Lessons: 15,14,13,6
      g2_u9 = g2 && @opts[:metadata]['unit'] == '9'
      return false if g2_u9 && %w(15 14 13 6).include?(@opts[:metadata]['lesson'])

      # G2 Unit 10 apart from for Lessons: 11,13,5,12
      g2_u10 = g2 && @opts[:metadata]['unit'] == '10'
      return false if g2_u10 && %w(11 13 5 12).include?(@opts[:metadata]['lesson'])

      # G2 Unit 11 apart from for Lessons: 14,12,7,13
      g2_u11 = g2 && @opts[:metadata]['unit'] == '11'
      return false if g2_u11 && %w(14 12 7 13).include?(@opts[:metadata]['lesson'])

      # G2 Unit 12 apart from for Lessons: 12,13,6,11
      g2_u12 = g2 && @opts[:metadata]['unit'] == '12'
      return false if g2_u12 && %w(12 13 6 11).include?(@opts[:metadata]['lesson'])

      true
    end
    # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize

    def find_tag(name, value = '')
      key = registered_tags.keys.detect do |k|
        if k.is_a?(Regexp)
          name =~ k
        else
          k == name or k == [name, value].join(' ')
        end
      end
      registered_tags[key]
    end

    #
    # catch invalid tags and report about them
    #
    def handle_invalid_tag(node)
      return if ::DocTemplate::FULL_TAG.match(node.text).present?

      raise Lcms::Engine::DocumentError, "No closing bracket for node:<br>#{node.to_html}"
    end

    def parse_node(node)
      matches = FULL_TAG.match(node.text)
      return if matches.nil?

      tag_name, tag_value = matches.captures
      return unless (tag = find_tag tag_name.downcase, tag_value.downcase)

      # Did we get the same tag as previous?
      check_loop_tag tag_name, tag_value

      parsed_tag = tag.parse(node, @opts.merge(parent_document: self, value: tag_value))
      store_last_tag tag_name, tag_value

      parsed_content = parsed_tag.content.presence || parsed_tag.render.to_s
      sanitized_content = ::DocTemplate.sanitizer.post_processing(parsed_content, @opts)

      return if TAGS_WITHOUT_PARTS.include?(tag::TAG_NAME)

      parts << {
        anchor: parsed_tag.anchor.to_s,
        content: parsed_tag.try(:without_squish?) ? sanitized_content : sanitized_content.squish,
        context_type: @opts[:context_type],
        data: parsed_tag.tag_data,
        materials: parsed_tag.materials,
        optional: (parsed_tag.try(:optional?) || false),
        placeholder: parsed_tag.placeholder,
        part_type: tag_name.underscore
      }
    end

    def registered_tags
      Template.tags
    end

    #
    # Save info about the latest parsed tag
    #
    def store_last_tag(name, value)
      iteration =
        if @opts.dig(:last_tag, :name) != name && @opts.dig(:last_tag, :value) != value
          0
        else
          @opts.dig(:last_tag, :iteration).to_i + 1
        end

      @opts[:last_tag] = {
        iteration: iteration,
        name: name,
        value: value
      }
    end
  end
end