# frozen_string_literal: true
# ANSI color library
#
# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
module Gitlab
module Ci
module Ansi2html
# keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
COLOR = {
0 => 'black', # not that this is gray in the intense color table
1 => 'red',
2 => 'green',
3 => 'yellow',
4 => 'blue',
5 => 'magenta',
6 => 'cyan',
7 => 'white' # not that this is gray in the dark (aka default) color table
}.freeze
STYLE_SWITCHES = {
bold: 0x01,
italic: 0x02,
underline: 0x04,
conceal: 0x08,
cross: 0x10
}.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
end
Result = Struct.new(:html, :state, :append, :truncated, :offset, :size, :total, keyword_init: true) # rubocop:disable Lint/StructNewOverride
class Converter
def on_0(_)
reset
end
def on_1(_)
enable(STYLE_SWITCHES[:bold])
end
def on_3(_)
enable(STYLE_SWITCHES[:italic])
end
def on_4(_)
enable(STYLE_SWITCHES[:underline])
end
def on_8(_)
enable(STYLE_SWITCHES[:conceal])
end
def on_9(_)
enable(STYLE_SWITCHES[:cross])
end
def on_21(_)
disable(STYLE_SWITCHES[:bold])
end
def on_22(_)
disable(STYLE_SWITCHES[:bold])
end
def on_23(_)
disable(STYLE_SWITCHES[:italic])
end
def on_24(_)
disable(STYLE_SWITCHES[:underline])
end
def on_28(_)
disable(STYLE_SWITCHES[:conceal])
end
def on_29(_)
disable(STYLE_SWITCHES[:cross])
end
def on_30(_)
set_fg_color(0)
end
def on_31(_)
set_fg_color(1)
end
def on_32(_)
set_fg_color(2)
end
def on_33(_)
set_fg_color(3)
end
def on_34(_)
set_fg_color(4)
end
def on_35(_)
set_fg_color(5)
end
def on_36(_)
set_fg_color(6)
end
def on_37(_)
set_fg_color(7)
end
def on_38(stack)
set_fg_color_256(stack)
end
def on_39(_)
set_fg_color(9)
end
def on_40(_)
set_bg_color(0)
end
def on_41(_)
set_bg_color(1)
end
def on_42(_)
set_bg_color(2)
end
def on_43(_)
set_bg_color(3)
end
def on_44(_)
set_bg_color(4)
end
def on_45(_)
set_bg_color(5)
end
def on_46(_)
set_bg_color(6)
end
def on_47(_)
set_bg_color(7)
end
def on_48(stack)
set_bg_color_256(stack)
end
def on_49(_)
set_bg_color(9)
end
def on_90(_)
set_fg_color(0, 'l')
end
def on_91(_)
set_fg_color(1, 'l')
end
def on_92(_)
set_fg_color(2, 'l')
end
def on_93(_)
set_fg_color(3, 'l')
end
def on_94(_)
set_fg_color(4, 'l')
end
def on_95(_)
set_fg_color(5, 'l')
end
def on_96(_)
set_fg_color(6, 'l')
end
def on_97(_)
set_fg_color(7, 'l')
end
def on_99(_)
set_fg_color(9, 'l')
end
def on_100(_)
set_bg_color(0, 'l')
end
def on_101(_)
set_bg_color(1, 'l')
end
def on_102(_)
set_bg_color(2, 'l')
end
def on_103(_)
set_bg_color(3, 'l')
end
def on_104(_)
set_bg_color(4, 'l')
end
def on_105(_)
set_bg_color(5, 'l')
end
def on_106(_)
set_bg_color(6, 'l')
end
def on_107(_)
set_bg_color(7, 'l')
end
def on_109(_)
set_bg_color(9, 'l')
end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section].freeze
def convert(stream, new_state)
reset_state
restore_state(new_state, stream) if new_state.present?
append = false
truncated = false
cur_offset = stream.tell
if cur_offset > @offset
@offset = cur_offset
truncated = true
else
stream.seek(@offset)
append = @offset > 0
end
start_offset = @offset
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
if s.scan(Gitlab::Regex.build_trace_section_regex)
handle_section(s)
elsif s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif s.scan(/)
write_in_tag '<'
elsif s.scan(/\r?\n/)
handle_new_line
else
write_in_tag s.scan(/./m)
end
@offset += s.matched_size
end
end
close_open_tags
Ansi2html::Result.new(
html: @out.force_encoding(Encoding.default_external),
state: state,
append: append,
truncated: truncated,
offset: start_offset,
size: stream.tell - start_offset,
total: stream.size
)
end
def section_to_class_name(section)
section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
end
def handle_new_line
write_in_tag %{
}
close_open_tags if @sections.any? && @lineno_in_section == 0
@lineno_in_section += 1
end
def handle_section(scanner)
action = scanner[1]
timestamp = scanner[2]
section = scanner[3]
normalized_section = section_to_class_name(section)
if action == "start"
handle_section_start(normalized_section, timestamp)
elsif action == "end"
handle_section_end(normalized_section, timestamp)
end
end
def handle_section_start(section, timestamp)
return if @sections.include?(section)
@sections << section
write_raw %{