<commit_before>require 'rexml/document'

#
# Prawn::Svg::Parser is responsible for parsing an SVG file and converting it into a tree of
# prawn-compatible method calls.
#
# You probably do not want to use this class directly.  Instead, use Prawn::Svg to draw
# SVG data to your Prawn::Document object.
#
# This class is not passed the prawn object, so knows nothing about
# prawn specifically - this might be useful if you want to take this code and use it to convert
# SVG to another format.
#
class Prawn::Svg::Parser
  CONTAINER_TAGS = %w(g svg symbol defs clipPath)
  COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/

  #
  # Construct a Parser object.
  #
  # The +data+ argument is SVG data.
  #
  # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
  #
  # +options+ can optionally contain
  # the key :width or :height.  If both are specified, only :width will be used.
  #
  def initialize(document)
    @document = document
  end

  #
  # Parse the SVG data and return a call tree.  The returned +Array+ is in the format:
  #
  #   [
  #     ['prawn_method_name', ['argument1', 'argument2'], []],
  #     ['method_that_takes_a_block', ['argument1', 'argument2'], [
  #       ['method_called_inside_block', ['argument'], []]
  #     ]
  #   ]
  #
  def parse
    @document.warnings.clear

    calls = [['fill_color', '000000', []]]

    calls << [
      'transformation_matrix',
      [@document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0],
      []
    ]

    calls << [
      'transformation_matrix',
      [1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset],
      []
    ]

    root_element = Prawn::Svg::Element.new(@document, @document.root, calls, :ids => {}, :fill => true)

    parse_element(root_element)
    calls
  end


  private
  REQUIRED_ATTRIBUTES = {
    "polyline"  => %w(points),
    "polygon"   => %w(points),
    "circle"    => %w(r),
    "ellipse"   => %w(rx ry),
    "rect"      => %w(width height),
    "path"      => %w(d),
    "image"     => %w(width height)
  }

  USE_NEW_CIRCLE_CALL = Prawn::Document.new.respond_to?(:circle)
  USE_NEW_ELLIPSE_CALL = Prawn::Document.new.respond_to?(:ellipse)

  def parse_element(element)
    attrs = element.attributes

    if required_attributes = REQUIRED_ATTRIBUTES[element.name]
      return unless check_attrs_present(element, required_attributes)
    end

    case element.name
    when *CONTAINER_TAGS
      do_not_append_calls = %w(symbol defs clipPath).include?(element.name)
      element.state[:disable_drawing] = true if element.name == "clipPath"

      element.each_child_element do |child|
        element.add_call "save"
        parse_element(child)
        element.add_call "restore"
      end

    when 'style'
      load_css_styles(element)

    when 'text'
      @svg_text ||= Text.new
      @svg_text.parse(element)

    when 'line'
      element.add_call 'line', x(attrs['x1'] || '0'), y(attrs['y1'] || '0'), x(attrs['x2'] || '0'), y(attrs['y2'] || '0')

    when 'polyline'
      points = parse_points(attrs['points'])
      return unless points.length > 0
      x, y = points.shift
      element.add_call 'move_to', x(x), y(y)
      element.add_call_and_enter 'stroke'
      points.each do |x, y|
        element.add_call "line_to", x(x), y(y)
      end

    when 'polygon'
      points = parse_points(attrs['points']).collect do |x, y|
        [x(x), y(y)]
      end
      element.add_call "polygon", *points

    when 'circle'
      xy, r = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])

      return if zero_argument?(r)

      if USE_NEW_CIRCLE_CALL
        element.add_call "circle", xy, r
      else
        element.add_call "circle_at", xy, :radius => r
      end

    when 'ellipse'
      xy, rx, ry = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx'], :x), distance(attrs['ry'], :y)

      return if zero_argument?(rx, ry)

      element.add_call USE_NEW_ELLIPSE_CALL ? "ellipse" : "ellipse_at", xy, rx, ry

    when 'rect'
      xy            = [x(attrs['x'] || '0'), y(attrs['y'] || '0')]
      width, height = distance(attrs['width'], :x), distance(attrs['height'], :y)
      radius        = distance(attrs['rx'] || attrs['ry'])

      return if zero_argument?(width, height)

      if radius
        # n.b. does not support both rx and ry being specified with different values
        element.add_call "rounded_rectangle", xy, width, height, radius
      else
        element.add_call "rectangle", xy, width, height
      end

    when 'path'
      parse_path(element)

    when 'use'
      parse_use(element)

    when 'title', 'desc', 'metadata'
      # ignore
      do_not_append_calls = true

    when 'font-face'
      # not supported
      do_not_append_calls = true

    when 'image'
      @svg_image ||= Image.new(@document)
      @svg_image.parse(element)

    else
      @document.warnings << "Unknown tag '#{element.name}'; ignoring"
    end

    element.append_calls_to_parent unless do_not_append_calls
  end


  def parse_path(element)
    @svg_path ||= Path.new

    begin
      commands = @svg_path.parse(element.attributes['d'])
    rescue Prawn::Svg::Parser::Path::InvalidError => e
      commands = []
      @document.warnings << e.message
    end

    element.add_call 'join_style', :bevel

    commands.collect do |command, args|
      if args && args.length > 0
        point_to = [x(args[0]), y(args[1])]
        if command == 'curve_to'
          opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
        end
        element.add_call command, point_to, opts
      else
        element.add_call command
      end
    end
  end

  def parse_use(element)
    if href = element.attributes['xlink:href']
      if href[0..0] == '#'
        id = href[1..-1]
        if definition_element = @document.elements_by_id[id]
          x = element.attributes['x']
          y = element.attributes['y']
          if x || y
            element.add_call_and_enter "translate", distance(x || 0, :x), -distance(y || 0, :y)
          end
          element.add_calls_from_element definition_element
        else
          @document.warnings << "no tag with ID '#{id}' was found, referenced by use tag"
        end
      else
        @document.warnings << "use tag has an href that is not a reference to an id; this is not supported"
      end
    else
      @document.warnings << "no xlink:href specified on use tag"
    end
  end

  ####################################################################################################################

  def load_css_styles(element)
    if @document.css_parser
      data = if element.element.cdatas.any?
        element.element.cdatas.collect {|d| d.to_s}.join
      else
        element.element.text
      end

      @document.css_parser.add_block!(data)
    end
  end<commit_msg>Remove stray "*/" in <style> CDATA, if found<commit_after>require 'rexml/document'

#
# Prawn::Svg::Parser is responsible for parsing an SVG file and converting it into a tree of
# prawn-compatible method calls.
#
# You probably do not want to use this class directly.  Instead, use Prawn::Svg to draw
# SVG data to your Prawn::Document object.
#
# This class is not passed the prawn object, so knows nothing about
# prawn specifically - this might be useful if you want to take this code and use it to convert
# SVG to another format.
#
class Prawn::Svg::Parser
  CONTAINER_TAGS = %w(g svg symbol defs clipPath)
  COMMA_WSP_REGEXP = /(?:\s+,?\s*|,\s*)/

  #
  # Construct a Parser object.
  #
  # The +data+ argument is SVG data.
  #
  # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points.
  #
  # +options+ can optionally contain
  # the key :width or :height.  If both are specified, only :width will be used.
  #
  def initialize(document)
    @document = document
  end

  #
  # Parse the SVG data and return a call tree.  The returned +Array+ is in the format:
  #
  #   [
  #     ['prawn_method_name', ['argument1', 'argument2'], []],
  #     ['method_that_takes_a_block', ['argument1', 'argument2'], [
  #       ['method_called_inside_block', ['argument'], []]
  #     ]
  #   ]
  #
  def parse
    @document.warnings.clear

    calls = [['fill_color', '000000', []]]

    calls << [
      'transformation_matrix',
      [@document.sizing.x_scale, 0, 0, @document.sizing.y_scale, 0, 0],
      []
    ]

    calls << [
      'transformation_matrix',
      [1, 0, 0, 1, @document.sizing.x_offset, @document.sizing.y_offset],
      []
    ]

    root_element = Prawn::Svg::Element.new(@document, @document.root, calls, :ids => {}, :fill => true)

    parse_element(root_element)
    calls
  end


  private
  REQUIRED_ATTRIBUTES = {
    "polyline"  => %w(points),
    "polygon"   => %w(points),
    "circle"    => %w(r),
    "ellipse"   => %w(rx ry),
    "rect"      => %w(width height),
    "path"      => %w(d),
    "image"     => %w(width height)
  }

  USE_NEW_CIRCLE_CALL = Prawn::Document.new.respond_to?(:circle)
  USE_NEW_ELLIPSE_CALL = Prawn::Document.new.respond_to?(:ellipse)

  def parse_element(element)
    attrs = element.attributes

    if required_attributes = REQUIRED_ATTRIBUTES[element.name]
      return unless check_attrs_present(element, required_attributes)
    end

    case element.name
    when *CONTAINER_TAGS
      do_not_append_calls = %w(symbol defs clipPath).include?(element.name)
      element.state[:disable_drawing] = true if element.name == "clipPath"

      element.each_child_element do |child|
        element.add_call "save"
        parse_element(child)
        element.add_call "restore"
      end

    when 'style'
      load_css_styles(element)

    when 'text'
      @svg_text ||= Text.new
      @svg_text.parse(element)

    when 'line'
      element.add_call 'line', x(attrs['x1'] || '0'), y(attrs['y1'] || '0'), x(attrs['x2'] || '0'), y(attrs['y2'] || '0')

    when 'polyline'
      points = parse_points(attrs['points'])
      return unless points.length > 0
      x, y = points.shift
      element.add_call 'move_to', x(x), y(y)
      element.add_call_and_enter 'stroke'
      points.each do |x, y|
        element.add_call "line_to", x(x), y(y)
      end

    when 'polygon'
      points = parse_points(attrs['points']).collect do |x, y|
        [x(x), y(y)]
      end
      element.add_call "polygon", *points

    when 'circle'
      xy, r = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['r'])

      return if zero_argument?(r)

      if USE_NEW_CIRCLE_CALL
        element.add_call "circle", xy, r
      else
        element.add_call "circle_at", xy, :radius => r
      end

    when 'ellipse'
      xy, rx, ry = [x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx'], :x), distance(attrs['ry'], :y)

      return if zero_argument?(rx, ry)

      element.add_call USE_NEW_ELLIPSE_CALL ? "ellipse" : "ellipse_at", xy, rx, ry

    when 'rect'
      xy            = [x(attrs['x'] || '0'), y(attrs['y'] || '0')]
      width, height = distance(attrs['width'], :x), distance(attrs['height'], :y)
      radius        = distance(attrs['rx'] || attrs['ry'])

      return if zero_argument?(width, height)

      if radius
        # n.b. does not support both rx and ry being specified with different values
        element.add_call "rounded_rectangle", xy, width, height, radius
      else
        element.add_call "rectangle", xy, width, height
      end

    when 'path'
      parse_path(element)

    when 'use'
      parse_use(element)

    when 'title', 'desc', 'metadata'
      # ignore
      do_not_append_calls = true

    when 'font-face'
      # not supported
      do_not_append_calls = true

    when 'image'
      @svg_image ||= Image.new(@document)
      @svg_image.parse(element)

    else
      @document.warnings << "Unknown tag '#{element.name}'; ignoring"
    end

    element.append_calls_to_parent unless do_not_append_calls
  end


  def parse_path(element)
    @svg_path ||= Path.new

    begin
      commands = @svg_path.parse(element.attributes['d'])
    rescue Prawn::Svg::Parser::Path::InvalidError => e
      commands = []
      @document.warnings << e.message
    end

    element.add_call 'join_style', :bevel

    commands.collect do |command, args|
      if args && args.length > 0
        point_to = [x(args[0]), y(args[1])]
        if command == 'curve_to'
          opts = {:bounds => [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]}
        end
        element.add_call command, point_to, opts
      else
        element.add_call command
      end
    end
  end

  def parse_use(element)
    if href = element.attributes['xlink:href']
      if href[0..0] == '#'
        id = href[1..-1]
        if definition_element = @document.elements_by_id[id]
          x = element.attributes['x']
          y = element.attributes['y']
          if x || y
            element.add_call_and_enter "translate", distance(x || 0, :x), -distance(y || 0, :y)
          end
          element.add_calls_from_element definition_element
        else
          @document.warnings << "no tag with ID '#{id}' was found, referenced by use tag"
        end
      else
        @document.warnings << "use tag has an href that is not a reference to an id; this is not supported"
      end
    else
      @document.warnings << "no xlink:href specified on use tag"
    end
  end

  ####################################################################################################################

  def load_css_styles(element)
    if @document.css_parser
      data = if element.element.cdatas.any?
        # Some people start "<style>/* <![CDATA[ */" and that confuses the CSS parser.
        # Remove any "*/" at the start of the cdata section.
        element.element.cdatas.collect {|d| d.to_s}.join.gsub(%r(\A\s*\*/), '')
      else
        element.element.text
      end

      @document.css_parser.add_block!(data)
    end
  end
{'fraction_non_alphanumeric': 0.15363649404676516, 'fraction_numerical': 0.006598766317601492, 'mean_word_length': 2.7340653454740225, 'pattern_counts': {'":': 0, '<': 34, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2847492', 'n_tokens_mistral': 4722, 'n_tokens_neox': 4589, 'n_words': 1177}