mdless 2.0.17 → 2.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,33 +4,33 @@ require 'yaml'
4
4
  module CLIMarkdown
5
5
  class Converter
6
6
  include Colors
7
- include Theme
8
-
9
- attr_reader :helpers, :log
10
7
 
11
8
  def version
12
9
  "#{CLIMarkdown::EXECUTABLE_NAME} #{CLIMarkdown::VERSION}"
13
10
  end
14
11
 
12
+ def default(option, default)
13
+ MDLess.options[option] = default if MDLess.options[option].nil?
14
+ end
15
+
15
16
  def initialize(args)
16
- @log = Logger.new($stderr)
17
- @log.level = Logger::WARN
17
+ MDLess.log.level = Logger::WARN
18
18
 
19
- @options = {}
19
+ MDLess.options = {}
20
20
  config = File.expand_path('~/.config/mdless/config.yml')
21
- @options = YAML.load(IO.read(config)) if File.exist?(config)
21
+ MDLess.options = YAML.load(IO.read(config)) if File.exist?(config)
22
22
 
23
23
  optparse = OptionParser.new do |opts|
24
24
  opts.banner = "#{version} by Brett Terpstra\n\n> Usage: #{CLIMarkdown::EXECUTABLE_NAME} [options] [path]\n\n"
25
25
 
26
- @options[:color] ||= true
26
+ default(:color, true)
27
27
  opts.on('-c', '--[no-]color', 'Colorize output (default on)') do |c|
28
- @options[:color] = c
28
+ MDLess.options[:color] = c
29
29
  end
30
30
 
31
31
  opts.on('-d', '--debug LEVEL', 'Level of debug messages to output (1-4, 4 to see all messages)') do |level|
32
32
  if level.to_i.positive? && level.to_i < 5
33
- @log.level = 5 - level.to_i
33
+ MDLess.log.level = 5 - level.to_i
34
34
  else
35
35
  puts 'Error: Debug level out of range (1-4)'
36
36
  Process.exit 1
@@ -42,64 +42,71 @@ module CLIMarkdown
42
42
  exit
43
43
  end
44
44
 
45
- @options[:local_images] ||= false
46
- @options[:remote_images] ||= false
45
+ default(:local_images, false)
46
+ default(:remote_images, false)
47
47
  opts.on('-i', '--images=TYPE',
48
48
  'Include [local|remote (both)|none] images in output (requires chafa or imgcat, default none).') do |type|
49
49
  if exec_available('imgcat') || exec_available('chafa')
50
50
  case type
51
51
  when /^(r|b|a)/i
52
- @options[:local_images] = true
53
- @options[:remote_images] = true
52
+ MDLess.options[:local_images] = true
53
+ MDLess.options[:remote_images] = true
54
54
  when /^l/i
55
- @options[:local_images] = true
55
+ MDLess.options[:local_images] = true
56
56
  when /^n/
57
- @options[:local_images] = false
58
- @options[:remote_images] = false
57
+ MDLess.options[:local_images] = false
58
+ MDLess.options[:remote_images] = false
59
59
  end
60
60
  else
61
- @log.warn('images turned on but imgcat/chafa not found')
61
+ MDLess.log.warn('images turned on but imgcat/chafa not found')
62
62
  end
63
63
  end
64
64
 
65
65
  opts.on('-I', '--all-images', 'Include local and remote images in output (requires imgcat or chafa)') do
66
66
  if exec_available('imgcat') || exec_available('chafa') # && ENV['TERM_PROGRAM'] == 'iTerm.app'
67
- @options[:local_images] = true
68
- @options[:remote_images] = true
67
+ MDLess.options[:local_images] = true
68
+ MDLess.options[:remote_images] = true
69
69
  else
70
- @log.warn('images turned on but imgcat/chafa not found')
70
+ MDLess.log.warn('images turned on but imgcat/chafa not found')
71
71
  end
72
72
  end
73
73
 
74
- @options[:list] ||= false
74
+ default(:list, false)
75
75
  opts.on('-l', '--list', 'List headers in document and exit') do
76
- @options[:list] = true
76
+ MDLess.options[:list] = true
77
77
  end
78
78
 
79
- @options[:pager] ||= true
79
+ default(:pager, true)
80
80
  opts.on('-p', '--[no-]pager', 'Formatted output to pager (default on)') do |p|
81
- @options[:pager] = p
81
+ MDLess.options[:pager] = p
82
82
  end
83
83
 
84
- @options[:pager] ||= true
84
+ default(:pager, true)
85
85
  opts.on('-P', 'Disable pager (same as --no-pager)') do
86
- @options[:pager] = false
86
+ MDLess.options[:pager] = false
87
87
  end
88
88
 
89
- @options[:section] ||= nil
89
+ default(:section, nil)
90
90
  opts.on('-s', '--section=NUMBER[,NUMBER]',
91
91
  'Output only a headline-based section of the input (numeric from --list)') do |section|
92
- @options[:section] = section.split(/ *, */).map(&:strip).map(&:to_i)
92
+ sections = section.split(/ *, */).map(&:strip)
93
+ MDLess.options[:section] = sections.map do |sect|
94
+ if sect =~ /^\d+$/
95
+ sect.to_i
96
+ else
97
+ sect
98
+ end
99
+ end
93
100
  end
94
101
 
95
- @options[:theme] ||= 'default'
102
+ default(:theme, 'default')
96
103
  opts.on('-t', '--theme=THEME_NAME', 'Specify an alternate color theme to load') do |theme|
97
- @options[:theme] = theme
104
+ MDLess.options[:theme] = theme
98
105
  end
99
106
 
100
- @options[:at_tags] ||= false
107
+ default(:at_tags, false)
101
108
  opts.on('-@', '--at_tags', 'Highlight @tags and values in the document') do
102
- @options[:at_tags] = true
109
+ MDLess.options[:at_tags] = true
103
110
  end
104
111
 
105
112
  opts.on('-v', '--version', 'Display version number') do
@@ -107,32 +114,39 @@ module CLIMarkdown
107
114
  exit
108
115
  end
109
116
 
110
- @options[:width] = `tput cols`.strip.to_i
117
+ default(:width, TTY::Screen.cols)
111
118
  opts.on('-w', '--width=COLUMNS', 'Column width to format for (default: terminal width)') do |columns|
112
- @options[:width] = columns.to_i
119
+ MDLess.options[:width] = columns.to_i
120
+ end
121
+ cols = TTY::Screen.cols
122
+ MDLess.options[:width] = cols if MDLess.options[:width] > cols
123
+
124
+ default(:autolink, true)
125
+ opts.on('--[no-]autolink', 'Convert bare URLs and emails to <links>') do |p|
126
+ MDLess.options[:autolink] = p
113
127
  end
114
128
 
115
- @options[:inline_footnotes] ||= false
129
+ default(:inline_footnotes, false)
116
130
  opts.on('--[no-]inline_footnotes',
117
131
  'Display footnotes immediately after the paragraph that references them') do |p|
118
- @options[:inline_footnotes] = p
132
+ MDLess.options[:inline_footnotes] = p
119
133
  end
120
134
 
121
- @options[:intra_emphasis] ||= true
135
+ default(:intra_emphasis, true)
122
136
  opts.on('--[no-]intra-emphasis', 'Parse emphasis inside of words (e.g. Mark_down_)') do |opt|
123
- @options[:intra_emphasis] = opt
137
+ MDLess.options[:intra_emphasis] = opt
124
138
  end
125
139
 
126
- @options[:lax_spacing] ||= true
140
+ default(:lax_spacing, true)
127
141
  opts.on('--[no-]lax-spacing', 'Allow lax spacing') do |opt|
128
- @options[:lax_spacing] = opt
142
+ MDLess.options[:lax_spacing] = opt
129
143
  end
130
144
 
131
- @options[:links] ||= :inline
145
+ default(:links, :inline)
132
146
  opts.on('--links=FORMAT',
133
147
  'Link style ([inline, reference, paragraph], default inline,
134
148
  "paragraph" will position reference links after each paragraph)') do |fmt|
135
- @options[:links] = case fmt
149
+ MDLess.options[:links] = case fmt
136
150
  when /^:?r/i
137
151
  :reference
138
152
  when /^:?p/i
@@ -142,13 +156,13 @@ module CLIMarkdown
142
156
  end
143
157
  end
144
158
 
145
- @options[:syntax_higlight] ||= false
159
+ default(:syntax_higlight, false)
146
160
  opts.on('--[no-]syntax', 'Syntax highlight code blocks') do |p|
147
- @options[:syntax_higlight] = p
161
+ MDLess.options[:syntax_higlight] = p
148
162
  end
149
163
 
150
- @options[:taskpaper] = if @options[:taskpaper]
151
- case @options[:taskpaper].to_s
164
+ MDLess.options[:taskpaper] = if MDLess.options[:taskpaper]
165
+ case MDLess.options[:taskpaper].to_s
152
166
  when /^[ty1]/
153
167
  true
154
168
  when /^a/
@@ -160,7 +174,7 @@ module CLIMarkdown
160
174
  false
161
175
  end
162
176
  opts.on('--taskpaper=OPTION', 'Highlight TaskPaper format (true|false|auto)') do |tp|
163
- @options[:taskpaper] = case tp
177
+ MDLess.options[:taskpaper] = case tp
164
178
  when /^[ty1]/
165
179
  true
166
180
  when /^a/
@@ -170,14 +184,14 @@ module CLIMarkdown
170
184
  end
171
185
  end
172
186
 
173
- @options[:update_config] ||= false
187
+ default(:update_config, false)
174
188
  opts.on('--update_config', 'Update the configuration file with new keys and current command line options') do
175
- @options[:update_config] = true
189
+ MDLess.options[:update_config] = true
176
190
  end
177
191
 
178
- @options[:wiki_links] ||= false
192
+ default(:wiki_links, false)
179
193
  opts.on('--[no-]wiki-links', 'Highlight [[wiki links]]') do |opt|
180
- @options[:wiki_links] = opt
194
+ MDLess.options[:wiki_links] = opt
181
195
  end
182
196
  end
183
197
 
@@ -188,20 +202,21 @@ module CLIMarkdown
188
202
  exit 1
189
203
  end
190
204
 
191
- if !File.exist?(config) || @options[:update_config]
205
+ if !File.exist?(config) || MDLess.options[:update_config]
192
206
  FileUtils.mkdir_p(File.dirname(config))
193
207
  File.open(config, 'w') do |f|
194
- opts = @options.dup
208
+ opts = MDLess.options.dup
195
209
  opts.delete(:list)
196
210
  opts.delete(:section)
197
211
  opts.delete(:update_config)
212
+ opts = opts.keys.map(&:to_s).sort.map { |k| [k.to_sym, opts[k.to_sym]] }.to_h
198
213
  f.puts YAML.dump(opts)
199
214
  warn "Config file saved to #{config}"
200
215
  end
201
216
  end
202
217
 
203
- @theme = load_theme(@options[:theme])
204
- @cols = @options[:width] - 2
218
+ MDLess.cols = MDLess.options[:width] - 2
219
+
205
220
  @output = ''
206
221
  @headers = []
207
222
  @setheaders = []
@@ -211,19 +226,15 @@ module CLIMarkdown
211
226
  @footnotes = {}
212
227
 
213
228
  renderer = Redcarpet::Render::Console.new
214
- renderer.theme = @theme
215
- renderer.cols = @cols
216
- renderer.log = @log
217
- renderer.options = @options
218
229
 
219
230
  markdown = Redcarpet::Markdown.new(renderer,
220
- no_intra_emphasis: !@options[:intra_emphasis],
221
- autolink: true,
231
+ no_intra_emphasis: !MDLess.options[:intra_emphasis],
232
+ autolink: MDLess.options[:autolink],
222
233
  fenced_code_blocks: true,
223
234
  footnotes: true,
224
235
  hard_wrap: false,
225
236
  highlight: true,
226
- lax_spacing: @options[:lax_spacing],
237
+ lax_spacing: MDLess.options[:lax_spacing],
227
238
  quote: false,
228
239
  space_after_headers: false,
229
240
  strikethrough: true,
@@ -234,9 +245,9 @@ module CLIMarkdown
234
245
  if !args.empty?
235
246
  files = args.delete_if { |f| !File.exist?(f) }
236
247
  files.each do |file|
237
- @log.info(%(Processing "#{file}"))
238
- @file = file
239
- renderer.file = @file
248
+ MDLess.log.info(%(Processing "#{file}"))
249
+ MDLess.file = file
250
+
240
251
  begin
241
252
  input = IO.read(file).force_encoding('utf-8')
242
253
  rescue StandardError
@@ -247,25 +258,27 @@ module CLIMarkdown
247
258
  input.scrub!
248
259
  input.gsub!(/\r?\n/, "\n")
249
260
 
250
- if @options[:list]
251
- puts list_headers(input)
261
+ if MDLess.options[:taskpaper] == :auto
262
+ MDLess.options[:taskpaper] = if CLIMarkdown::TaskPaper.is_taskpaper?(input)
263
+ MDLess.log.info('TaskPaper detected')
264
+ true
265
+ else
266
+ false
267
+ end
268
+ end
269
+
270
+ if MDLess.options[:list]
271
+ if MDLess.options[:taskpaper]
272
+ puts CLIMarkdown::TaskPaper.list_projects(input)
273
+ else
274
+ puts list_headers(input)
275
+ end
252
276
  Process.exit 0
253
277
  else
254
- if @options[:taskpaper] == :auto
255
- @options[:taskpaper] = if file =~ /\.taskpaper/
256
- @log.info('TaskPaper extension detected')
257
- true
258
- elsif CLIMarkdown::TaskPaper.is_taskpaper?(input)
259
- @log.info('TaskPaper document detected')
260
- true
261
- else
262
- false
263
- end
264
- end
265
-
266
- if @options[:taskpaper]
267
- input = CLIMarkdown::TaskPaper.highlight(input, @theme)
268
- @output = input.highlight_tags(@theme, @log)
278
+ if MDLess.options[:taskpaper]
279
+ input = input.color_meta(MDLess.cols)
280
+ input = CLIMarkdown::TaskPaper.highlight(input)
281
+ @output = input.highlight_tags
269
282
  else
270
283
  @output = markdown.render(input)
271
284
  end
@@ -273,14 +286,34 @@ module CLIMarkdown
273
286
  end
274
287
  printout
275
288
  elsif !$stdin.isatty
276
- @file = nil
289
+ MDLess.file = nil
277
290
  input = $stdin.read.scrub
278
291
  input.gsub!(/\r?\n/, "\n")
279
- if @options[:list]
280
- puts list_headers(input)
292
+
293
+ if MDLess.options[:taskpaper] == :auto
294
+ MDLess.options[:taskpaper] = if CLIMarkdown::TaskPaper.is_taskpaper?(input)
295
+ MDLess.log.info('TaskPaper detected')
296
+ true
297
+ else
298
+ false
299
+ end
300
+ end
301
+
302
+ if MDLess.options[:list]
303
+ if MDLess.options[:taskpaper]
304
+ puts CLIMarkdown::TaskPaper.list_projects(input)
305
+ else
306
+ puts list_headers(input)
307
+ end
281
308
  Process.exit 0
282
309
  else
283
- @output = markdown.render(input)
310
+ if MDLess.options[:taskpaper]
311
+ input = input.color_meta(MDLess.cols)
312
+ input = CLIMarkdown::TaskPaper.highlight(input)
313
+ @output = input.highlight_tags
314
+ else
315
+ @output = markdown.render(input)
316
+ end
284
317
  end
285
318
  printout
286
319
  else
@@ -292,17 +325,17 @@ module CLIMarkdown
292
325
  def color(key)
293
326
  val = nil
294
327
  keys = key.split(/[ ,>]/)
295
- if @theme.key?(keys[0])
296
- val = @theme[keys.shift]
328
+ if MDLess.theme.key?(keys[0])
329
+ val = MDLess.theme[keys.shift]
297
330
  else
298
- @log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/
331
+ MDLess.log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/
299
332
  return c([:reset])
300
333
  end
301
334
  keys.each do |k|
302
335
  if val.key?(k)
303
336
  val = val[k]
304
337
  else
305
- @log.error("Invalid theme key: #{k}")
338
+ MDLess.log.error("Invalid theme key: #{k}")
306
339
  return c([:reset])
307
340
  end
308
341
  end
@@ -394,8 +427,8 @@ module CLIMarkdown
394
427
 
395
428
  def clean_markers(input)
396
429
  input.gsub!(/^(\e\[[\d;]+m)?[%~] ?/, '\1')
397
- input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?( +)/, ' \3\1\2')
398
- input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?/, '\1\2')
430
+ # input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?( +)/, ' \3\1\2')
431
+ # input.gsub!(/^(\e\[[\d;]+m)*>(\e\[[\d;]+m)?/, '\1\2')
399
432
  input.gsub!(/(\e\[[\d;]+m)?@@@(\e\[[\d;]+m)?$/, '')
400
433
  input
401
434
  end
@@ -424,7 +457,7 @@ module CLIMarkdown
424
457
  block.split(/\n/).map do |l|
425
458
  new_code_line = l.gsub(/\t/, ' ')
426
459
  orig_length = new_code_line.size + 8 + eol.size
427
- pad_count = [@cols - orig_length, 0].max
460
+ pad_count = [MDLess.cols - orig_length, 0].max
428
461
 
429
462
  [
430
463
  new_code_line,
@@ -448,11 +481,11 @@ module CLIMarkdown
448
481
  IO.select [input]
449
482
 
450
483
  pager = which_pager
451
- @log.info("Using #{pager} as pager")
484
+ MDLess.log.info("Using #{pager} as pager")
452
485
  begin
453
486
  exec(pager.join(' '))
454
487
  rescue SystemCallError => e
455
- @log.error(e)
488
+ MDLess.log.error(e)
456
489
  exit 1
457
490
  end
458
491
  end
@@ -470,21 +503,25 @@ module CLIMarkdown
470
503
  end
471
504
 
472
505
  def printout
473
- out = @output.rstrip.split(/\n/).map do |p|
474
- p.wrap(@cols, color('text'))
475
- end.join("\n")
506
+ if MDLess.options[:taskpaper]
507
+ out = @output
508
+ else
509
+ out = @output.rstrip.split(/\n/).map do |p|
510
+ p.wrap(MDLess.cols, color('text'))
511
+ end.join("\n")
512
+ end
476
513
 
477
514
  unless out.size&.positive?
478
- @log.warn 'No results'
515
+ MDLess.log.warn 'No results'
479
516
  Process.exit
480
517
  end
481
518
 
482
519
  out = clean_markers(out)
483
520
  out = "#{out.gsub(/\n{2,}/m, "\n\n")}#{xc}"
484
521
 
485
- out.uncolor! unless @options[:color]
522
+ out.uncolor! unless MDLess.options[:color]
486
523
 
487
- if @options[:pager]
524
+ if MDLess.options[:pager]
488
525
  page(out)
489
526
  else
490
527
  $stdout.print out.rstrip
@@ -507,7 +544,7 @@ module CLIMarkdown
507
544
  if f.strip =~ /[ |]/
508
545
  f
509
546
  elsif f == 'most'
510
- @log.warn('most not allowed as pager')
547
+ MDLess.log.warn('most not allowed as pager')
511
548
  false
512
549
  else
513
550
  system "which #{f}", out: File::NULL, err: File::NULL
data/lib/mdless/string.rb CHANGED
@@ -4,20 +4,28 @@
4
4
  class ::String
5
5
  include CLIMarkdown::Colors
6
6
 
7
- def color(key, theme, log)
7
+ def clean_empty_lines
8
+ gsub(/^[ \t]+$/, '')
9
+ end
10
+
11
+ def clean_empty_lines!
12
+ replace clean_empty_lines
13
+ end
14
+
15
+ def color(key)
8
16
  val = nil
9
17
  keys = key.split(/[ ,>]/)
10
- if theme.key?(keys[0])
11
- val = theme[keys.shift]
18
+ if MDLess.theme.key?(keys[0])
19
+ val = MDLess.theme[keys.shift]
12
20
  else
13
- log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/
21
+ MDLess.log.error("Invalid theme key: #{key}") unless keys[0] =~ /^text/
14
22
  return c([:reset])
15
23
  end
16
24
  keys.each do |k|
17
25
  if val.key?(k)
18
26
  val = val[k]
19
27
  else
20
- log.error("Invalid theme key: #{k}")
28
+ MDLess.log.error("Invalid theme key: #{k}")
21
29
  return c([:reset])
22
30
  end
23
31
  end
@@ -30,9 +38,75 @@ class ::String
30
38
  end
31
39
  end
32
40
 
33
- def highlight_tags(theme, log)
34
- tag_color = color('at_tags tag', theme, log)
35
- value_color = color('at_tags value', theme, log)
41
+ def to_rx(distance: 2, string_start: false)
42
+ chars = downcase.split(//)
43
+ pre = string_start ? '^' : '^.*?'
44
+ /#{pre}#{chars.join(".{,#{distance}}")}.*?$/
45
+ end
46
+
47
+ def clean_header_ids!
48
+ replace clean_header_ids
49
+ end
50
+
51
+ def clean_header_ids
52
+ gsub(/ +\[.*?\] *$/, '').gsub(/ *\{#.*?\} *$/, '').strip
53
+ end
54
+
55
+ def color_meta(cols)
56
+ @cols = cols
57
+ input = dup
58
+ input.clean_empty_lines!
59
+
60
+ in_yaml = false
61
+ first_line = input.split("\n").first
62
+ if first_line =~ /(?i-m)^---[ \t]*?$/
63
+ MDLess.log.info('Found YAML')
64
+ # YAML
65
+ in_yaml = true
66
+ input.sub!(/(?i-m)^---[ \t]*\n([\s\S]*?)\n[-.]{3}[ \t]*\n/m) do
67
+ m = Regexp.last_match
68
+ MDLess.log.info('Processing YAML Header')
69
+ lines = m[0].split(/\n/)
70
+ longest = lines.inject { |memo, word| memo.length > word.length ? memo : word }.length
71
+ longest = longest < @cols ? longest + 1 : @cols
72
+ lines.map do |line|
73
+ if line =~ /^[-.]{3}\s*$/
74
+ line = "#{color('metadata marker')}#{'%' * longest}"
75
+ else
76
+ line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
77
+ line = "#{color('metadata color')}#{line}"
78
+ end
79
+
80
+ line += "\u00A0" * (longest - line.uncolor.strip.length) + xc
81
+ line
82
+ end.join("\n") + "#{xc}\n"
83
+ end
84
+ end
85
+
86
+ if !in_yaml && first_line =~ /(?i-m)^[\w ]+:\s+\S+/
87
+ MDLess.log.info('Found MMD Headers')
88
+ input.sub!(/(?i-m)^([\S ]+:[\s\S]*?)+(?=\n\n)/) do |mmd|
89
+ lines = mmd.split(/\n/)
90
+ return mmd if lines.count > 20
91
+
92
+ longest = lines.inject { |memo, word| memo.length > word.length ? memo : word }.length
93
+ longest = longest < @cols ? longest + 1 : @cols
94
+ lines.map do |line|
95
+ line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
96
+ line = "#{color('metadata color')}#{line}"
97
+ line += "\u00A0" * (longest - line.uncolor.strip.length)
98
+ line + xc
99
+ end.join("\n") + "#{"\u00A0" * longest}#{xc}\n"
100
+ end
101
+ end
102
+
103
+ input
104
+ end
105
+
106
+ def highlight_tags
107
+ log = MDLess.log
108
+ tag_color = color('at_tags tag')
109
+ value_color = color('at_tags value')
36
110
  gsub(/(?<pre>\s|m)(?<tag>@[^ \].?!,("']+)(?:(?<lparen>\()(?<value>.*?)(?<rparen>\)))?/) do
37
111
  m = Regexp.last_match
38
112
  last_color = m.pre_match.last_color_code
@@ -58,4 +132,16 @@ class ::String
58
132
  def scrub!
59
133
  replace scrub
60
134
  end
135
+
136
+ def valid_pygments_theme?
137
+ return false unless TTY::Which.exist?('pygmentize')
138
+
139
+ MDLess.pygments_styles.include?(self)
140
+ end
141
+
142
+ def valid_lexer?
143
+ return false unless TTY::Which.exist?('pygmentize')
144
+
145
+ MDLess.pygments_lexers.include?(self.downcase)
146
+ end
61
147
  end