summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/ansi2json/line.rb
blob: 466706384c09d312e1735b5ea39d20264d46c2fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# frozen_string_literal: true

module Gitlab
  module Ci
    module Ansi2json
      # Line class is responsible for keeping the internal state of
      # a log line and to finally serialize it as Hash.
      class Line
        # Line::Segment is a portion of a line that has its own style
        # and text. Multiple segments make the line content.
        class Segment
          attr_accessor :text, :style

          def initialize(style:)
            @text = +''
            @style = style
          end

          def empty?
            text.empty?
          end

          def to_h
            # Without force encoding to UTF-8 we could get an error
            # when serializing the Hash to JSON.
            # Encoding::UndefinedConversionError:
            #   "\xE2" from ASCII-8BIT to UTF-8
            { text: text.force_encoding('UTF-8') }.tap do |result|
              result[:style] = style.to_s if style.set?
            end
          end
        end

        attr_reader :offset, :sections, :segments, :current_segment,
                    :section_header, :section_duration, :section_options

        def initialize(offset:, style:, sections: [])
          @offset = offset
          @segments = []
          @sections = sections
          @section_header = false
          @duration = nil
          @current_segment = Segment.new(style: style)
        end

        def <<(data)
          @current_segment.text << data
        end

        def clear!
          @segments.clear
          @current_segment = Segment.new(style: style)
        end

        def style
          @current_segment.style
        end

        def empty?
          @segments.empty? && @current_segment.empty? && @section_duration.nil?
        end

        def update_style(ansi_commands)
          @current_segment.style.update(ansi_commands)
        end

        def add_section(section)
          @sections << section
        end

        def set_section_options(options)
          @section_options = options
        end

        def set_as_section_header
          @section_header = true
        end

        def set_section_duration(duration)
          @section_duration = Time.at(duration.to_i).utc.strftime('%M:%S')
        end

        def flush_current_segment!
          return if @current_segment.empty?

          @segments << @current_segment.to_h
          @current_segment = Segment.new(style: @current_segment.style)
        end

        def to_h
          flush_current_segment!

          { offset: offset, content: @segments }.tap do |result|
            result[:section] = sections.last if sections.any?
            result[:section_header] = true if @section_header
            result[:section_duration] = @section_duration if @section_duration
            result[:section_options] = @section_options if @section_options
          end
        end
      end
    end
  end
end