Module: Kward::ANSI

Defined in:
lib/kward/ansi.rb

Defined Under Namespace

Classes: MarkdownStream

Constant Summary collapse

ESCAPE_PATTERN =
/\e\[[0-9;?]*[ -\/]*[@-~]/.freeze
SGR_PATTERN =
/\e\[[0-9;:]*m/.freeze
OSC_PATTERN =
/\e\][^\a]*(?:\a|\e\\)/m.freeze
STRING_ESCAPE_PATTERN =
/\e[P_X^][\s\S]*?\e\\/m.freeze
STYLES =
{
  reset: 0,
  bold: 1,
  dim: 2,
  italic: 3,
  strikethrough: 9,
  red: 31,
  green: 32,
  yellow: 33,
  blue: 34,
  magenta: 35,
  cyan: 36,
  gray: 90,
  grey: 90,
  primary_green: "38;2;138;160;106",
  bright_accent_green: "38;2;155;255;0",
  augen: "38;2;155;255;0",
  dark_forest_green: "38;2;78;88;53",
  stone: "38;2;196;192;178",
  metal_dark: "38;2;42;42;42",
  background: "38;2;22;24;22"
}.freeze

Class Method Summary collapse

Class Method Details

.blockquote(text, enabled: enabled?) ) ⇒ Object



208
209
210
# File 'lib/kward/ansi.rb', line 208

def blockquote(text, enabled: enabled?)
  "#{colorize("", :gray, enabled: enabled)} #{inline_markdown(text, enabled: enabled)}"
end

.colorize(text, *styles, enabled: enabled?) ) ⇒ Object



42
43
44
45
46
47
48
49
50
# File 'lib/kward/ansi.rb', line 42

def colorize(text, *styles, enabled: enabled?)
  string = text.to_s
  return string unless enabled

  codes = styles.flatten.map { |style| STYLES.fetch(style, style) }.compact
  return string if codes.empty?

  "\e[#{codes.join(";")}m#{string}\e[0m"
end

.disabled_color?(env) ⇒ Boolean

Returns:

  • (Boolean)


269
270
271
272
273
274
# File 'lib/kward/ansi.rb', line 269

def disabled_color?(env)
  return true if env.key?("NO_COLOR") && !env["NO_COLOR"].to_s.empty?
  return true if env["CLICOLOR"] == "0"

  env["TERM"] == "dumb"
end

.enabled?(output = $stdout, env: ENV) ⇒ Boolean

Returns:

  • (Boolean)


32
33
34
35
36
37
38
39
40
# File 'lib/kward/ansi.rb', line 32

def enabled?(output = $stdout, env: ENV)
  setting = env["KWARD_COLOR"].to_s.downcase
  return true if %w[always force forced true yes 1].include?(setting)
  return false if %w[never false no 0].include?(setting)
  return true if forced_color?(env)
  return false if disabled_color?(env)

  output.respond_to?(:tty?) && output.tty?
end

.forced_color?(env) ⇒ Boolean

Returns:

  • (Boolean)


263
264
265
266
267
# File 'lib/kward/ansi.rb', line 263

def forced_color?(env)
  force_color = env["FORCE_COLOR"]
  clicolor_force = env["CLICOLOR_FORCE"]
  (force_color && force_color != "0") || (clicolor_force && clicolor_force != "0")
end

.inline_bold(text, enabled: enabled?) ) ⇒ Object



238
239
240
241
242
# File 'lib/kward/ansi.rb', line 238

def inline_bold(text, enabled: enabled?)
  text.gsub(/\*\*([^\n]+?)\*\*/) do
    colorize(Regexp.last_match(1), :bold, enabled: enabled)
  end
end

.inline_code(line, enabled: enabled?) ) ⇒ Object



259
260
261
# File 'lib/kward/ansi.rb', line 259

def inline_code(line, enabled: enabled?)
  inline_markdown(line, enabled: enabled)
end

.inline_emphasis(text, enabled: enabled?) ) ⇒ Object



232
233
234
235
236
# File 'lib/kward/ansi.rb', line 232

def inline_emphasis(text, enabled: enabled?)
  rendered = inline_bold(text, enabled: enabled)
  rendered = inline_strikethrough(rendered, enabled: enabled)
  inline_italic(rendered, enabled: enabled)
end

.inline_italic(text, enabled: enabled?) ) ⇒ Object



250
251
252
253
254
255
256
257
# File 'lib/kward/ansi.rb', line 250

def inline_italic(text, enabled: enabled?)
  rendered = text.gsub(/(^|[\s\(\[{])\*([^*\n]+?)\*(?=$|[\s\)\]},.!?:;])/) do
    "#{Regexp.last_match(1)}#{colorize(Regexp.last_match(2), :italic, enabled: enabled)}"
  end
  rendered.gsub(/(^|[\s\(\[{])_([^_\n]+?)_(?=$|[\s\)\]},.!?:;])/) do
    "#{Regexp.last_match(1)}#{colorize(Regexp.last_match(2), :italic, enabled: enabled)}"
  end
end


222
223
224
225
226
227
228
229
230
# File 'lib/kward/ansi.rb', line 222

def inline_links(text, enabled: enabled?)
  text.split(/(\[[^\]\n]+\]\([^)\s\n]+\))/).map do |part|
    if (match = part.match(/\A\[([^\]\n]+)\]\(([^)\s\n]+)\)\z/))
      "#{colorize(match[1], :cyan, enabled: enabled)} (#{colorize(match[2], :dim, enabled: enabled)})"
    else
      inline_emphasis(part, enabled: enabled)
    end
  end.join
end

.inline_markdown(line, enabled: enabled?) ) ⇒ Object



212
213
214
215
216
217
218
219
220
# File 'lib/kward/ansi.rb', line 212

def inline_markdown(line, enabled: enabled?)
  line.to_s.split(/(`[^`\n]+`)/).map do |part|
    if part.start_with?("`") && part.end_with?("`") && part.length > 1
      "`#{colorize(part[1...-1], :dim, enabled: enabled)}`"
    else
      inline_links(part, enabled: enabled)
    end
  end.join
end

.inline_strikethrough(text, enabled: enabled?) ) ⇒ Object



244
245
246
247
248
# File 'lib/kward/ansi.rb', line 244

def inline_strikethrough(text, enabled: enabled?)
  text.gsub(/~~([^\n]+?)~~/) do
    colorize(Regexp.last_match(1), :strikethrough, enabled: enabled)
  end
end

.markdown(text, enabled: enabled?) ) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/kward/ansi.rb', line 97

def markdown(text, enabled: enabled?)
  string = text.to_s
  lines = string.lines(chomp: true)
  rendered = []
  in_fence = false

  lines.each do |line|
    if (match = line.match(/\A\s*```([^`]*)\s*\z/))
      if in_fence
        rendered << colorize("" + "" * 39, :gray, enabled: enabled)
        in_fence = false
      else
        language = match[1].to_s.strip
        label = language.empty? ? "code" : "code #{language}"
        rendered << colorize("┌─ #{label}", :gray, enabled: enabled)
        in_fence = true
      end
      next
    end

    if in_fence
      rendered << colorize("#{line}", :dim, enabled: enabled)
    else
      rendered << markdown_line(line, enabled: enabled)
    end
  end

  rendered << colorize("" + "" * 39, :gray, enabled: enabled) if in_fence
  rendered.join("\n") + (string.end_with?("\n") ? "\n" : "")
end

.markdown_heading(marker, text, enabled: enabled?) ) ⇒ Object



198
199
200
# File 'lib/kward/ansi.rb', line 198

def markdown_heading(marker, text, enabled: enabled?)
  "#{marker}#{colorize(text, :bold, enabled: enabled)}"
end

.markdown_line(line, enabled: enabled?) ) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
# File 'lib/kward/ansi.rb', line 186

def markdown_line(line, enabled: enabled?)
  if (match = line.match(/\A(\#{1,6}\s+)(.+)\z/))
    markdown_heading(match[1], match[2], enabled: enabled)
  elsif (match = line.match(/\A(\s*)[-*]\s+\[([ xX])\]\s+(.+)\z/))
    task_list_item(match[1], match[2], match[3], enabled: enabled)
  elsif (match = line.match(/\A>\s?(.*)\z/))
    blockquote(match[1], enabled: enabled)
  else
    inline_markdown(line, enabled: enabled)
  end
end

.sanitize_transcript(text) ⇒ Object



56
57
58
59
60
61
# File 'lib/kward/ansi.rb', line 56

def sanitize_transcript(text)
  string = text.to_s.gsub(OSC_PATTERN, "").gsub(STRING_ESCAPE_PATTERN, "")
  string.gsub(/\e(?:\[[0-9;:?]*[ -\/]*[@-~]|.)/m) do |sequence|
    sequence.match?(SGR_PATTERN) ? sequence : ""
  end
end

.strip(text) ⇒ Object



52
53
54
# File 'lib/kward/ansi.rb', line 52

def strip(text)
  text.to_s.gsub(ESCAPE_PATTERN, "")
end

.task_list_item(indent, marker, text, enabled: enabled?) ) ⇒ Object



202
203
204
205
206
# File 'lib/kward/ansi.rb', line 202

def task_list_item(indent, marker, text, enabled: enabled?)
  checked = marker.downcase == "x"
  box = checked ? colorize("", :green, enabled: enabled) : colorize("", :gray, enabled: enabled)
  "#{indent}#{box} #{inline_markdown(text, enabled: enabled)}"
end

.wrap_visible(text, width) ⇒ Object



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
# File 'lib/kward/ansi.rb', line 63

def wrap_visible(text, width)
  line_width = [width.to_i, 1].max
  rows = []
  current = +""
  visible_width = 0
  string = text.to_s
  index = 0

  while index < string.length
    if string[index] == "\e" && (match = string[index..].match(/\A\e\[[0-9;:]*m/))
      if current.empty? && rows.any?
        rows[-1] << match[0]
      else
        current << match[0]
      end
      index += match[0].length
      next
    end

    char = string[index]
    current << char
    visible_width += 1
    index += 1
    if visible_width >= line_width
      rows << current
      current = +""
      visible_width = 0
    end
  end

  rows << current unless current.empty?
  rows
end