mdless 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f01cf923665ca168bdbe40383e7182173c9b3427
4
+ data.tar.gz: 9d9e98c876836336fa9f789997db7c939b931948
5
+ SHA512:
6
+ metadata.gz: 26539f402a4c058de79aa8539e8872fc12a8c564380f58231a5ec4f8df637740c3f48e336c71bdc88c1cb6f289424c7fc05cad5d33cea284896525d83082885c
7
+ data.tar.gz: 885ca66c4586a75d3aabef7a7cd50c6f692864e298f8041be6abf3270746e92916ddfb29012dc0cf2d2f180f206651a5e6252cea91758d2beb5cae876c19066b
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+
2
+ # mdless
3
+
4
+
5
+ `mdless` is a utility that provides a formatted and highlighted view of Markdown files in Terminal.
6
+
7
+ I often use iTerm2 in visor mode, so `qlmanage -p` is annoying. I still wanted a way to view Markdown files quickly and without cruft.
8
+
9
+ ## Features
10
+
11
+ - Built in pager functionality with pipe capability, `less` replacement for Markdown files
12
+ - Format tables
13
+ - Colorize Markdown syntax for most elements
14
+ - Normalize spacing and link formatting
15
+ - Display footnotes after each paragraph
16
+ - Inline image display (local, optionally remote) if using iTerm2 2.9+
17
+ - Syntax highlighting when `pygmentize` is available
18
+ - List headlines in document
19
+ - Display section of the document via headline search
20
+
21
+ ## Installation
22
+
23
+ gem install mdless
24
+
25
+ ## Usage
26
+
27
+ `mdless [options] path` or `cat [path] | mdless`
28
+
29
+ The pager used is determined by system configuration in this order of preference:
30
+
31
+ * `$GIT_PAGER`
32
+ * `$PAGER`
33
+ * `git config --get-all core.pager`
34
+ * `less`
35
+ * `more`
36
+ * `cat`
37
+ * `pager`
38
+
39
+ ### Options
40
+
41
+ -s, --section=TITLE Output only a headline-based section of
42
+ the input
43
+ -w, --width=COLUMNS Column width to format for (default 80)
44
+ -p, --[no-]pager Formatted output to pager (default on)
45
+ -P Disable pager (same as --no-pager)
46
+ -c, --[no-]color Colorize output (default on)
47
+ -l, --list List headers in document and exit
48
+ -i, --images=TYPE Include [local|remote (both)] images in
49
+ output (requires imgcat and iTerm2,
50
+ default NONE)
51
+ -I, --all-images Include local and remote images in output
52
+ -h, --help Display this screen
53
+ -v, --version Display version number
54
+
55
+
56
+
data/bin/mdless ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+ require 'mdless'
4
+
5
+ def class_exists?(class_name)
6
+ klass = Module.const_get(class_name)
7
+ return klass.is_a?(Class)
8
+ rescue NameError
9
+ return false
10
+ end
11
+
12
+ if class_exists? 'Encoding'
13
+ Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external')
14
+ Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal')
15
+ end
16
+
17
+ CLIMarkdown::Converter.new(ARGV)
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/python
2
+
3
+ import sys
4
+ import re
5
+
6
+ def just(string, type, n):
7
+ "Justify a string to length n according to type."
8
+
9
+ if type == '::':
10
+ return string.center(n)
11
+ elif type == '-:':
12
+ return string.rjust(n)
13
+ elif type == ':-':
14
+ return string.ljust(n)
15
+ else:
16
+ return string
17
+
18
+
19
+ def normtable(text):
20
+ "Aligns the vertical bars in a text table."
21
+
22
+ # Start by turning the text into a list of lines.
23
+ lines = text.splitlines()
24
+ rows = len(lines)
25
+
26
+ # Figure out the cell formatting.
27
+ # First, find the separator line.
28
+ for i in range(rows):
29
+ if set(lines[i]).issubset('|:.-'):
30
+ formatline = lines[i]
31
+ formatrow = i
32
+ break
33
+
34
+ # Delete the separator line from the content.
35
+ del lines[formatrow]
36
+
37
+ # Determine how each column is to be justified.
38
+ formatline = formatline.strip(' ')
39
+ if formatline[0] == '|': formatline = formatline[1:]
40
+ if formatline[-1] == '|': formatline = formatline[:-1]
41
+ fstrings = formatline.split('|')
42
+ justify = []
43
+ for cell in fstrings:
44
+ ends = cell[0] + cell[-1]
45
+ if ends == '::':
46
+ justify.append('::')
47
+ elif ends == '-:':
48
+ justify.append('-:')
49
+ else:
50
+ justify.append(':-')
51
+
52
+ # Assume the number of columns in the separator line is the number
53
+ # for the entire table.
54
+ columns = len(justify)
55
+
56
+ # Extract the content into a matrix.
57
+ content = []
58
+ for line in lines:
59
+ line = line.strip(' ')
60
+ if line[0] == '|': line = line[1:]
61
+ if line[-1] == '|': line = line[:-1]
62
+ cells = line.split('|')
63
+ # Put exactly one space at each end as "bumpers."
64
+ linecontent = [ ' ' + x.strip() + ' ' for x in cells ]
65
+ content.append(linecontent)
66
+
67
+ # Append cells to rows that don't have enough.
68
+ rows = len(content)
69
+ for i in range(rows):
70
+ while len(content[i]) < columns:
71
+ content[i].append('')
72
+
73
+ # Get the width of the content in each column. The minimum width will
74
+ # be 2, because that's the shortest length of a formatting string and
75
+ # because that matches an empty column with "bumper" spaces.
76
+ widths = [2] * columns
77
+ for row in content:
78
+ for i in range(columns):
79
+ widths[i] = max(len(row[i]), widths[i])
80
+
81
+ # Add whitespace to make all the columns the same width and
82
+ formatted = []
83
+ for row in content:
84
+ formatted.append('|' + '|'.join([ just(s, t, n) for (s, t, n) in zip(row, justify, widths) ]) + '|')
85
+
86
+ # Recreate the format line with the appropriate column widths.
87
+ formatline = '|' + '|'.join([ s[0] + '-'*(n-2) + s[-1] for (s, n) in zip(justify, widths) ]) + '|'
88
+
89
+ # Insert the formatline back into the table.
90
+ formatted.insert(formatrow, formatline)
91
+
92
+ # Return the formatted table.
93
+ return '\n'.join(formatted)
94
+
95
+ space_cleaner = r'''(?s)[ \t]*(?P<value>[\|:])[ \t]*'''
96
+
97
+ def whitespaceProcess(group_object):
98
+ return group_object.group('value')
99
+
100
+
101
+ # Read the input, process, and print.
102
+ unformatted = sys.stdin.read()
103
+ unformatted = re.sub(space_cleaner, whitespaceProcess, unformatted, flags=re.DOTALL)
104
+
105
+ print normtable(unformatted)
@@ -0,0 +1,124 @@
1
+ module CLIMarkdown
2
+ module Colors
3
+
4
+ def uncolor
5
+ self.gsub(/\e\[[\d;]+m/,'')
6
+ end
7
+
8
+ def uncolor!
9
+ self.replace self.uncolor
10
+ end
11
+
12
+ def size_clean
13
+ self.uncolor.size
14
+ end
15
+
16
+ def wrap(width=78)
17
+
18
+ if self.uncolor =~ /(^([%~] |\s*>)| +[=\-]{5,})/
19
+ return self
20
+ end
21
+
22
+ visible_width = 0
23
+ lines = []
24
+ line = ''
25
+ last_ansi = ''
26
+
27
+ line += self.match(/^\s*/)[0].gsub(/\t/,' ')
28
+ input = self.dup # .gsub(/(\w-)(\w)/,'\1 \2')
29
+ input.split(/\s+/).each do |word|
30
+ last_ansi = line.scan(/\e\[[\d;]+m/)[-1] || ''
31
+ if visible_width + word.size_clean >= width
32
+ lines << line + xc
33
+ visible_width = word.size_clean
34
+ line = last_ansi + word
35
+ elsif line.empty?
36
+ visible_width = word.size_clean
37
+ line = last_ansi + word
38
+ else
39
+ visible_width += word.size_clean + 1
40
+ line << " " << last_ansi + word
41
+ end
42
+ end
43
+ lines << line + self.match(/\s*$/)[0] + xc if line
44
+ return lines.join("\n") # .gsub(/\- (\S)/,'-\1')
45
+ end
46
+
47
+ def xc(count=0)
48
+ c([:x,:white])
49
+ end
50
+
51
+ def c(args)
52
+
53
+ colors = {
54
+ :reset => 0, # synonym for :clear
55
+ :x => 0,
56
+ :bold => 1,
57
+ :b => 1,
58
+ :dark => 2,
59
+ :d => 2,
60
+ :italic => 3, # not widely implemented
61
+ :i => 3,
62
+ :underline => 4,
63
+ :underscore => 4, # synonym for :underline
64
+ :u => 4,
65
+ :blink => 5,
66
+ :rapid_blink => 6, # not widely implemented
67
+ :negative => 7, # no reverse because of String#reverse
68
+ :r => 7,
69
+ :concealed => 8,
70
+ :strikethrough => 9, # not widely implemented
71
+ :black => 30,
72
+ :red => 31,
73
+ :green => 32,
74
+ :yellow => 33,
75
+ :blue => 34,
76
+ :magenta => 35,
77
+ :cyan => 36,
78
+ :white => 37,
79
+ :on_black => 40,
80
+ :on_red => 41,
81
+ :on_green => 42,
82
+ :on_yellow => 43,
83
+ :on_blue => 44,
84
+ :on_magenta => 45,
85
+ :on_cyan => 46,
86
+ :on_white => 47,
87
+ :intense_black => 90, # High intensity, aixterm (works in OS X)
88
+ :intense_red => 91,
89
+ :intense_green => 92,
90
+ :intense_yellow => 93,
91
+ :intense_blue => 94,
92
+ :intense_magenta => 95,
93
+ :intense_cyan => 96,
94
+ :intense_white => 97,
95
+ :on_intense_black => 100, # High intensity background, aixterm (works in OS X)
96
+ :on_intense_red => 101,
97
+ :on_intense_green => 102,
98
+ :on_intense_yellow => 103,
99
+ :on_intense_blue => 104,
100
+ :on_intense_magenta => 105,
101
+ :on_intense_cyan => 106,
102
+ :on_intense_white => 107
103
+ }
104
+
105
+ out = []
106
+
107
+ args.each {|arg|
108
+ if colors.key? arg
109
+ out << colors[arg]
110
+ end
111
+ }
112
+
113
+ if out.size > 0
114
+ "\e[#{out.sort.join(';')}m"
115
+ else
116
+ ''
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ class String
123
+ include CLIMarkdown::Colors
124
+ end
@@ -0,0 +1,781 @@
1
+ module CLIMarkdown
2
+ class Converter
3
+ include Colors
4
+
5
+ attr_reader :helpers, :log
6
+
7
+ def version
8
+ "#{CLIMarkdown::EXECUTABLE_NAME} #{CLIMarkdown::VERSION}"
9
+ end
10
+
11
+ def initialize(args)
12
+ @log = Logger.new(STDERR)
13
+ @log.level = Logger::FATAL
14
+
15
+ @options = {}
16
+ optparse = OptionParser.new do |opts|
17
+ opts.banner = "#{version} by Brett Terpstra\n\n> Usage: #{CLIMarkdown::EXECUTABLE_NAME} [options] path\n\n"
18
+
19
+ @options[:section] = nil
20
+ opts.on( '-s', '--section=TITLE', 'Output only a headline-based section of the input' ) do |section|
21
+ @options[:section] = section
22
+ end
23
+
24
+ @options[:width] = 80
25
+ opts.on( '-w', '--width=COLUMNS', 'Column width to format for (default 80)' ) do |columns|
26
+ @options[:width] = columns.to_i
27
+ if @options[:width] = 0
28
+ @options[:width] = %x{tput cols}.strip.to_i
29
+ end
30
+ end
31
+
32
+ @options[:pager] = true
33
+ opts.on( '-p', '--[no-]pager', 'Formatted output to pager (default on)' ) do |p|
34
+ @options[:pager] = p
35
+ end
36
+
37
+ opts.on( '-P', 'Disable pager (same as --no-pager)' ) do
38
+ @options[:pager] = false
39
+ end
40
+
41
+ @options[:color] = true
42
+ opts.on( '-c', '--[no-]color', 'Colorize output (default on)' ) do |c|
43
+ @options[:color] = c
44
+ end
45
+
46
+ @options[:links] = :inline
47
+ opts.on( '--links=FORMAT', 'Link style ([inline, reference], default inline)' ) do |format|
48
+ if format =~ /^r/i
49
+ @options[:links] = :reference
50
+ end
51
+ end
52
+
53
+ @options[:list] = false
54
+ opts.on( '-l', '--list', 'List headers in document and exit' ) do
55
+ @options[:list] = true
56
+ end
57
+
58
+ @options[:local_images] = false
59
+ @options[:remote_images] = false
60
+
61
+ if exec_available('imgcat') && ENV['TERM_PROGRAM'] == 'iTerm.app'
62
+ opts.on('-i', '--images=TYPE', 'Include [local|remote (both)] images in output (requires imgcat and iTerm2, default NONE)' ) do |type|
63
+ if type =~ /^(r|b|a)/i
64
+ @options[:local_images] = true
65
+ @options[:remote_images] = true
66
+ elsif type =~ /^l/i
67
+ @options[:local_images] = true
68
+ end
69
+ end
70
+ opts.on('-I', '--all-images', 'Include local and remote images in output (requires imgcat and iTerm2)' ) do
71
+ @options[:local_images] = true
72
+ @options[:remote_images] = true
73
+ end
74
+ end
75
+
76
+
77
+ opts.on( '-d', '--debug LEVEL', 'Level of debug messages to output' ) do |level|
78
+ if level.to_i > 0 && level.to_i < 5
79
+ @log.level = 5 - level.to_i
80
+ else
81
+ $stderr.puts "Log level out of range (1-4)"
82
+ Process.exit 1
83
+ end
84
+ end
85
+
86
+ opts.on( '-h', '--help', 'Display this screen' ) do
87
+ puts opts
88
+ exit
89
+ end
90
+
91
+ opts.on( '-v', '--version', 'Display version number' ) do
92
+ puts version
93
+ exit
94
+ end
95
+ end
96
+
97
+ optparse.parse!
98
+
99
+ @cols = @options[:width] || %x{tput cols}.strip.to_i * 0.9
100
+ @output = ''
101
+
102
+ input = ''
103
+ @ref_links = {}
104
+ @footnotes = {}
105
+
106
+ if args.length > 0
107
+ files = args.delete_if { |f| !File.exists?(f) }
108
+ files.each {|file|
109
+ @log.info(%Q{Processing "#{file}"})
110
+ @file = file
111
+ begin
112
+ input = IO.read(file).force_encoding('utf-8')
113
+ rescue
114
+ input = IO.read(file)
115
+ end
116
+ if @options[:list]
117
+ list_headers(input)
118
+ else
119
+ convert_markdown(input)
120
+ end
121
+ }
122
+ printout
123
+ elsif STDIN.stat.size > 0
124
+ @file = nil
125
+ begin
126
+ input = STDIN.read.force_encoding('utf-8')
127
+ rescue
128
+ input = STDIN.read
129
+ end
130
+ if @options[:list]
131
+ list_headers(input)
132
+ else
133
+ convert_markdown(input)
134
+ end
135
+ printout
136
+ else
137
+ $stderr.puts "No input"
138
+ Process.exit 1
139
+ end
140
+ end
141
+
142
+ def list_headers(input)
143
+ h_adjust = highest_header(input) - 1
144
+ input.gsub!(/^(#+)/) do |m|
145
+ match = Regexp.last_match
146
+ new_level = match[1].length - h_adjust
147
+ if new_level > 0
148
+ "#" * new_level
149
+ else
150
+ ''
151
+ end
152
+ end
153
+
154
+ headers = []
155
+ last_level = 0
156
+ input.split(/\n/).each do |line|
157
+ if line =~ /^(#+)\s*(.*?)( #+)?\s*$/
158
+ level = $1.size - 1
159
+ title = $2
160
+
161
+ if level - 1 > last_level
162
+ level = last_level + 1
163
+ end
164
+ last_level = level
165
+
166
+ subdoc = case level
167
+ when 0
168
+ ' '
169
+ when 1
170
+ '- '
171
+ when 2
172
+ '+ '
173
+ when 3
174
+ '* '
175
+ else
176
+ ' '
177
+ end
178
+ headers.push((" "*level) + (c([:x, :yellow]) + subdoc + title.strip + xc))
179
+ end
180
+ end
181
+
182
+ @output += headers.join("\n")
183
+ end
184
+
185
+ def highest_header(input)
186
+ headers = input.scan(/^(#+)/)
187
+ top = 6
188
+ headers.each {|h|
189
+ top = h[0].length if h[0].length < top
190
+ }
191
+ top
192
+ end
193
+
194
+ def color_table(input)
195
+ first = true
196
+ input.split(/\n/).map{|line|
197
+ if first
198
+ first = false
199
+ line.gsub!(/\|/, "#{c([:d,:black])}|#{c([:x,:yellow])}")
200
+ elsif line.strip =~ /^[|:\- ]+$/
201
+ line.gsub!(/^(.*)$/, "#{c([:d,:black])}\\1#{c([:x,:white])}")
202
+ line.gsub!(/([:\-]+)/,"#{c([:b,:black])}\\1#{c([:d,:black])}")
203
+ else
204
+ line.gsub!(/\|/, "#{c([:d,:black])}|#{c([:x,:white])}")
205
+ end
206
+ }.join("\n")
207
+ end
208
+
209
+ def table_cleanup(input)
210
+ in_table = false
211
+ header_row = false
212
+ all_content = []
213
+ this_table = []
214
+ orig_table = []
215
+ input.split(/\n/).each {|line|
216
+ if line =~ /(\|.*?)+/ && line !~ /^\s*~/
217
+ in_table = true
218
+ table_line = line.to_s.uncolor.strip.sub(/^\|?\s*/,'|').gsub(/\s*([\|:])\s*/,'\1')
219
+
220
+ if table_line.strip.gsub(/[\|:\- ]/,'') == ''
221
+ header_row = true
222
+ end
223
+ this_table.push(table_line)
224
+ orig_table.push(line)
225
+ else
226
+ if in_table
227
+ if this_table.length > 3
228
+ # if there's no header row, add one, cleanup requires it
229
+ unless header_row
230
+ cells = this_table[0].sub(/^\|/,'').scan(/.*?\|/).length
231
+ cell_row = '|' + ':-----|'*cells
232
+ this_table.insert(1, cell_row)
233
+ end
234
+
235
+ table = this_table.join("\n")
236
+ begin
237
+ res = clean_table(table)
238
+ res = color_table(res)
239
+ rescue
240
+ res = orig_table.join("\n")
241
+ end
242
+ all_content.push("\n" + res)
243
+ else
244
+ all_content.push(orig_table.join("\n"))
245
+ end
246
+ this_table = []
247
+ orig_table = []
248
+ end
249
+ in_table = false
250
+ header_row = false
251
+ all_content.push(line)
252
+ end
253
+ }
254
+ all_content.join("\n")
255
+ end
256
+
257
+ def clean_table(input)
258
+ dir = File.dirname(__FILE__)
259
+ lib = File.expand_path(dir + '/../../lib')
260
+ script = File.join(lib, 'helpers/formattables.py')
261
+
262
+ if File.exists?(script) and File.executable?(script)
263
+ begin
264
+
265
+ res, s = Open3.capture2(script, :stdin_data=>input.strip)
266
+
267
+ if s.success?
268
+ res
269
+ else
270
+ input
271
+ end
272
+ rescue => e
273
+ @log.error(e)
274
+ input
275
+ end
276
+ else
277
+ input
278
+ end
279
+ end
280
+
281
+ def clean_markers(input)
282
+ input.gsub!(/^(\e\[[\d;]+m)?[%~] ?/,'\1')
283
+ input
284
+ end
285
+
286
+ def update_inline_links(input)
287
+ links = {}
288
+ counter = 1
289
+ input.gsub!(/(?<=\])\((.*?)\)/) do |m|
290
+ links[counter] = $1.uncolor
291
+ "[#{counter}]"
292
+ end
293
+ end
294
+
295
+ def find_color(line,nullable=false)
296
+ return line if line.nil?
297
+ colors = line.scan(/\e\[[\d;]+m/)
298
+ if colors && colors.size > 0
299
+ colors[-1]
300
+ else
301
+ nullable ? nil : xc
302
+ end
303
+ end
304
+
305
+ def color_link(line, text, url)
306
+ out = c([:b,:black])
307
+ out += "[#{c([:u,:blue])}#{text}"
308
+ out += c([:b,:black])
309
+ out += "]("
310
+ out += c([:x,:cyan])
311
+ out += url
312
+ out += c([:b,:black])
313
+ out += ")"
314
+ out += find_color(line)
315
+ out
316
+ end
317
+
318
+ def color_image(line, text, url)
319
+ text.gsub!(/\e\[0m/,c([:x,:cyan]))
320
+
321
+ "#{c([:x,:red])}!#{c([:b,:black])}[#{c([:x,:cyan])}#{text}#{c([:b,:black])}](#{c([:u,:yellow])}#{url}#{c([:b,:black])})" + find_color(line)
322
+ end
323
+
324
+ def convert_markdown(input)
325
+
326
+ # yaml/MMD headers
327
+ in_yaml = false
328
+ if input.split("\n")[0] =~ /(?i-m)^---[ \t]*?(\n|$)/
329
+ @log.info("Found YAML")
330
+ # YAML
331
+ in_yaml = true
332
+ input.sub!(/(?i-m)^---[ \t]*\n([\s\S]*?)\n[\-.]{3}[ \t]*\n/) do |yaml|
333
+ m = Regexp.last_match
334
+
335
+ @log.warn("Processing YAML Header")
336
+ m[0].split(/\n/).map {|line|
337
+ if line =~ /^[\-.]{3}\s*$/
338
+ line = c([:d,:black,:on_black]) + "% " + c([:d,:black,:on_black]) + line
339
+ else
340
+ line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
341
+ line = c([:d,:black,:on_black]) + "% " + c([:d,:white]) + line
342
+ end
343
+ if @cols - line.uncolor.size > 0
344
+ line += " "*(@cols-line.uncolor.size)
345
+ end
346
+ }.join("\n") + "#{xc}\n"
347
+ end
348
+ end
349
+
350
+ if !in_yaml && input.gsub(/\n/,' ') =~ /(?i-m)^\w.+:\s+\S+ /
351
+ @log.info("Found MMD Headers")
352
+ input.sub!(/(?i-m)^([\S ]+:[\s\S]*?)+(?=\n\n)/) do |mmd|
353
+ puts mmd
354
+ mmd.split(/\n/).map {|line|
355
+ line.sub!(/^(.*?:)[ \t]+(\S)/, '\1 \2')
356
+ line = c([:d,:black,:on_black]) + "% " + c([:d,:white,:on_black]) + line
357
+ if @cols - line.uncolor.size > 0
358
+ line += " "*(@cols - line.uncolor.size)
359
+ end
360
+ }.join("\n") + " "*@cols + "#{xc}\n"
361
+ end
362
+
363
+ end
364
+
365
+
366
+ # Gather reference links
367
+ input.gsub!(/^\s{,3}(?<![\e*])\[\b(.+)\b\]: +(.+)/) do |m|
368
+ match = Regexp.last_match
369
+ @ref_links[match[1]] = match[2]
370
+ ''
371
+ end
372
+
373
+ # Gather footnotes (non-inline)
374
+ input.gsub!(/^ {,3}(?<!\*)(?:\e\[[\d;]+m)*\[(?:\e\[[\d;]+m)*\^(?:\e\[[\d;]+m)*\b(.+)\b(?:\e\[[\d;]+m)*\]: *(.*?)\n/) do |m|
375
+ match = Regexp.last_match
376
+ @footnotes[match[1].uncolor] = match[2].uncolor
377
+ ''
378
+ end
379
+
380
+ if @options[:section]
381
+ in_section = false
382
+ top_level = 1
383
+ new_content = []
384
+
385
+ input.split(/\n/).each {|graf|
386
+ if graf =~ /^(#+) *(.*?)( *#+)?$/
387
+ level = $1.length
388
+ title = $2
389
+
390
+ if in_section
391
+ if level > top_level
392
+ new_content.push(graf)
393
+ else
394
+ break
395
+ end
396
+ elsif title.downcase =~ /#{@options[:section]}/i
397
+ in_section = true
398
+ top_level = level
399
+ new_content.push(graf)
400
+ else
401
+ next
402
+ end
403
+ elsif in_section
404
+ new_content.push(graf)
405
+ end
406
+ }
407
+
408
+ input = new_content.join("\n")
409
+ end
410
+
411
+ h_adjust = highest_header(input) - 1
412
+ input.gsub!(/^(#+)/) do |m|
413
+ match = Regexp.last_match
414
+ "#" * (match[1].length - h_adjust)
415
+ end
416
+
417
+ # TODO: Probably easiest to just collect these with line indexes, remove until other highlighting is finished
418
+ input.gsub!(/(?i-m)([`~]{3,})([\s\S]*?)\n([\s\S]*?)\1/ ) do |cb|
419
+ m = Regexp.last_match
420
+ leader = m[2] ? m[2].upcase + ":" : 'CODE:'
421
+ leader += xc
422
+
423
+ if exec_available('pygmentize')
424
+ lexer = m[2].nil? ? '-g' : "-l #{m[2]}"
425
+ begin
426
+ hilite, s = Open3.capture2(%Q{pygmentize #{lexer} 2> /dev/null}, :stdin_data=>m[3])
427
+
428
+ if s.success?
429
+ hilite = hilite.split(/\n/).map{|l| "#{c([:x,:black])}~ #{xc}" + l}.join("\n")
430
+ end
431
+ rescue => e
432
+ @log.error(e)
433
+ hilite = m[0]
434
+ end
435
+
436
+ else
437
+
438
+ hilite = m[3].split(/\n/).map{|l|
439
+ new_code_line = l.gsub(/\t/,' ')
440
+ orig_length = new_code_line.size + 3
441
+ new_code_line.gsub!(/ /,"#{c([:x,:white,:on_black])} ")
442
+ "#{c([:x,:black])}~ #{c([:x,:white,:on_black])} " + new_code_line + c([:x,:white,:on_black]) + " "*(@cols - orig_length) + xc
443
+ }.join("\n")
444
+ end
445
+ "#{c([:x,:magenta])}#{leader}\n#{hilite}#{xc}"
446
+ end
447
+
448
+ # remove empty links
449
+ input.gsub!(/\[(.*?)\]\(\s*?\)/, '\1')
450
+ input.gsub!(/\[(.*?)\]\[\]/, '[\1][\1]')
451
+
452
+ lines = input.split(/\n/)
453
+
454
+ # previous_indent = 0
455
+
456
+ lines.map!.with_index do |aLine, i|
457
+ line = aLine.dup
458
+ clean_line = line.dup.uncolor
459
+
460
+
461
+ if clean_line.uncolor =~ /(^[%~])/ # || clean_line.uncolor =~ /^( {4,}|\t+)/
462
+ ## TODO: find indented code blocks and prevent highlighting
463
+ ## Needs to miss block indented 1 level in lists
464
+ ## Needs to catch lists in code
465
+ ## Needs to avoid within fenced code blocks
466
+ # if line =~ /^([ \t]+)([^*-+]+)/
467
+ # indent = $1.gsub(/\t/, " ").size
468
+ # if indent >= previous_indent
469
+ # line = "~" + line
470
+ # end
471
+ # p [indent, previous_indent]
472
+ # previous_indent = indent
473
+ # end
474
+ else
475
+ # Headlines
476
+ line.gsub!(/^(#+) *(.*?)(\s*#+)?\s*$/) do |match|
477
+ m = Regexp.last_match
478
+ pad = ""
479
+ ansi = ''
480
+ case m[1].length
481
+ when 1
482
+ ansi = c([:b, :black, :on_intense_white])
483
+ pad = c([:b,:white])
484
+ pad += m[2].length + 2 > @cols ? "*"*m[2].length : "*"*(@cols - (m[2].length + 2))
485
+ when 2
486
+ ansi = c([:b, :green, :on_black])
487
+ pad = c([:b,:black])
488
+ pad += m[2].length + 2 > @cols ? "-"*m[2].length : "-"*(@cols - (m[2].length + 2))
489
+ when 3
490
+ ansi = c([:u, :b, :yellow])
491
+ when 4
492
+ ansi = c([:x, :u, :yellow])
493
+ else
494
+ ansi = c([:b, :white])
495
+ end
496
+
497
+ "\n#{xc}#{ansi}#{m[2]} #{pad}#{xc}\n"
498
+ end
499
+
500
+ # place footnotes under paragraphs that reference them
501
+ if line =~ /\[(?:\e\[[\d;]+m)*\^(?:\e\[[\d;]+m)*(\S+)(?:\e\[[\d;]+m)*\]/
502
+ key = $1.uncolor
503
+ if @footnotes.key? key
504
+ line += "\n\n#{c([:b,:black,:on_black])}[#{c([:b,:cyan,:on_black])}^#{c([:x,:yellow,:on_black])}#{key}#{c([:b,:black,:on_black])}]: #{c([:u,:white,:on_black])}#{@footnotes[key]}#{xc}"
505
+ @footnotes.delete(key)
506
+ end
507
+ end
508
+
509
+ # color footnote references
510
+ line.gsub!(/\[\^(\S+)\]/) do |m|
511
+ match = Regexp.last_match
512
+ last = find_color(match.pre_match, true)
513
+ counter = i
514
+ while last.nil? && counter > 0
515
+ counter -= 1
516
+ find_color(lines[counter])
517
+ end
518
+ "#{c([:b,:black])}[#{c([:b,:yellow])}^#{c([:x,:yellow])}#{match[1]}#{c([:b,:black])}]" + (last ? last : xc)
519
+ end
520
+
521
+ # blockquotes
522
+ line.gsub!(/^(\s*>)+( .*?)?$/) do |m|
523
+ match = Regexp.last_match
524
+ last = find_color(match.pre_match, true)
525
+ counter = i
526
+ while last.nil? && counter > 0
527
+ counter -= 1
528
+ find_color(lines[counter])
529
+ end
530
+ "#{c([:b,:black])}#{match[1]}#{c([:x,:magenta])} #{match[2]}" + (last ? last : xc)
531
+ end
532
+
533
+ # make reference links inline
534
+ line.gsub!(/(?<![\e*])\[(\b.*?\b)?\]\[(\b.+?\b)?\]/) do |m|
535
+ match = Regexp.last_match
536
+ title = match[2] || ''
537
+ text = match[1] || ''
538
+ if match[2] && @ref_links.key?(title.downcase)
539
+ "[#{text}](#{@ref_links[title]})"
540
+ elsif match[1] && @ref_links.key?(text.downcase)
541
+ "[#{text}](#{@ref_links[text]})"
542
+ else
543
+ if input.match(/^#+\s*#{Regexp.escape(text)}/i)
544
+ "[#{text}](##{text})"
545
+ else
546
+ match[1]
547
+ end
548
+ end
549
+ end
550
+
551
+ # color inline links
552
+ line.gsub!(/(?<![\e*!])\[(\S.*?\S)\]\((\S.+?\S)\)/) do |m|
553
+ match = Regexp.last_match
554
+ color_link(match.pre_match, match[1], match[2])
555
+ end
556
+
557
+
558
+
559
+ # inline code
560
+ line.gsub!(/`(.*?)`/) do |m|
561
+ match = Regexp.last_match
562
+ last = find_color(match.pre_match, true)
563
+ "#{c([:b,:black])}`#{c([:b,:white])}#{match[1]}#{c([:b,:black])}`" + (last ? last : xc)
564
+ end
565
+
566
+ # horizontal rules
567
+ line.gsub!(/^ {,3}([\-*] ?){3,}$/) do |m|
568
+ c([:x,:black]) + '_'*@cols + xc
569
+ end
570
+
571
+ # bold, bold/italic
572
+ line.gsub!(/(^|\s)[\*_]{2,3}([^\*_\s][^\*_]+?[^\*_\s])[\*_]{2,3}/) do |m|
573
+ match = Regexp.last_match
574
+ last = find_color(match.pre_match, true)
575
+ counter = i
576
+ while last.nil? && counter > 0
577
+ counter -= 1
578
+ find_color(lines[counter])
579
+ end
580
+ "#{match[1]}#{c([:b])}#{match[2]}" + (last ? last : xc)
581
+ end
582
+
583
+ # italic
584
+ line.gsub!(/(^|\s)[\*_]([^\*_\s][^\*_]+?[^\*_\s])[\*_]/) do |m|
585
+ match = Regexp.last_match
586
+ last = find_color(match.pre_match, true)
587
+ counter = i
588
+ while last.nil? && counter > 0
589
+ counter -= 1
590
+ find_color(lines[counter])
591
+ end
592
+ "#{match[1]}#{c([:u])}#{match[2]}" + (last ? last : xc)
593
+ end
594
+
595
+ # equations
596
+ line.gsub!(/((\\\\\[)(.*?)(\\\\\])|(\\\\\()(.*?)(\\\\\)))/) do |m|
597
+ match = Regexp.last_match
598
+ last = find_color(match.pre_match)
599
+ if match[2]
600
+ brackets = [match[2], match[4]]
601
+ equat = match[3]
602
+ else
603
+ brackets = [match[5], match[7]]
604
+ equat = match[6]
605
+ end
606
+ "#{c([:b, :black])}#{brackets[0]}#{xc}#{c([:b,:blue])}#{equat}#{c([:b, :black])}#{brackets[1]}" + (last ? last : xc)
607
+ end
608
+
609
+ # list items
610
+ line.gsub!(/^(\s*)([*\-+]|\d\.) /) do |m|
611
+ match = Regexp.last_match
612
+ last = find_color(match.pre_match)
613
+ indent = match[1] || ''
614
+ "#{indent}#{c([:d, :red])}#{match[2]} " + (last ? last : xc)
615
+ end
616
+
617
+ # definition lists
618
+ line.gsub!(/^(:\s*)(.*?)/) do |m|
619
+ match = Regexp.last_match
620
+ "#{c([:d, :red])}#{match[1]} #{c([:b, :white])}#{match[2]}#{xc}"
621
+ end
622
+
623
+ # misc html
624
+ line.gsub!(/<br\/?>/, "\n")
625
+ line.gsub!(/(?i-m)((<\/?)(\w+[\s\S]*?)(>))/) do |tag|
626
+ match = Regexp.last_match
627
+ last = find_color(match.pre_match)
628
+ "#{c([:d,:black])}#{match[2]}#{c([:b,:black])}#{match[3]}#{c([:d,:black])}#{match[4]}" + (last ? last : xc)
629
+ end
630
+ end
631
+
632
+ line
633
+ end
634
+
635
+ input = lines.join("\n")
636
+
637
+ # images
638
+ input.gsub!(/^(.*?)!\[(.*)?\]\((.*?\.(?:png|gif|jpg))( +.*)?\)/) do |m|
639
+ match = Regexp.last_match
640
+ if match[1].uncolor =~ /^( {4,}|\t)+/
641
+ match[0]
642
+ else
643
+ tail = match[4].nil? ? '' : " "+match[4].strip
644
+ result = nil
645
+ if exec_available('imgcat') && @options[:local_images]
646
+ if match[3]
647
+ img_path = match[3]
648
+ if img_path =~ /^http/ && @options[:remote_images]
649
+ begin
650
+ res, s = Open3.capture2(%Q{curl -sS "#{img_path}" 2> /dev/null | imgcat})
651
+
652
+ if s.success?
653
+ pre = match[2].size > 0 ? " #{c([:d,:blue])}[#{match[2].strip}]\n" : ''
654
+ post = tail.size > 0 ? "\n #{c([:b,:blue])}-- #{tail} --" : ''
655
+ result = pre + res + post
656
+ end
657
+ rescue => e
658
+ @log.error(e)
659
+ end
660
+ else
661
+ if img_path =~ /^[~\/]/
662
+ img_path = File.expand_path(img_path)
663
+ elsif @file
664
+ base = File.expand_path(File.dirname(@file))
665
+ img_path = File.join(base,img_path)
666
+ end
667
+ if File.exists?(img_path)
668
+ pre = match[2].size > 0 ? " #{c([:d,:blue])}[#{match[2].strip}]\n" : ''
669
+ post = tail.size > 0 ? "\n #{c([:b,:blue])}-- #{tail} --" : ''
670
+ img = %x{imgcat "#{img_path}"}
671
+ result = pre + img + post
672
+ end
673
+ end
674
+ end
675
+ end
676
+ if result.nil?
677
+ match[1] + color_image(match.pre_match, match[2], match[3] + tail) + xc
678
+ else
679
+ match[1] + result + xc
680
+ end
681
+ end
682
+ end
683
+
684
+ @footnotes.each {|t, v|
685
+ input += "\n\n#{c([:b,:black,:on_black])}[#{c([:b,:yellow,:on_black])}^#{c([:x,:yellow,:on_black])}#{t}#{c([:b,:black,:on_black])}]: #{c([:u,:white,:on_black])}#{v}#{xc}"
686
+ }
687
+
688
+ @output += input
689
+
690
+ end
691
+
692
+ def exec_available(cli)
693
+ if File.exists?(File.expand_path(cli))
694
+ File.executable?(File.expand_path(cli))
695
+ else
696
+ system "which #{cli} &> /dev/null"
697
+ end
698
+ end
699
+
700
+ def page(text, &callback)
701
+ read_io, write_io = IO.pipe
702
+
703
+ input = $stdin
704
+
705
+ pid = Kernel.fork do
706
+ write_io.close
707
+ input.reopen(read_io)
708
+ read_io.close
709
+
710
+ # Wait until we have input before we start the pager
711
+ IO.select [input]
712
+
713
+ pager = which_pager
714
+ begin
715
+ exec(pager.join(' '))
716
+ rescue SystemCallError => e
717
+ @log.error(e)
718
+ exit 1
719
+ end
720
+ end
721
+
722
+ read_io.close
723
+ write_io.write(text)
724
+ write_io.close
725
+
726
+ _, status = Process.waitpid2(pid)
727
+ status.success?
728
+ end
729
+
730
+ def printout
731
+ out = @output.strip.split(/\n/).map {|p|
732
+ p.wrap(@cols)
733
+ }.join("\n")
734
+
735
+
736
+ unless out && out.size > 0
737
+ $stderr.puts "No results"
738
+ Process.exit
739
+ end
740
+
741
+ out = table_cleanup(out)
742
+ out = clean_markers(out)
743
+ out = out.gsub(/\n+{2,}/m,"\n\n") + "\n#{xc}\n\n"
744
+
745
+ unless @options[:color]
746
+ out.uncolor!
747
+ end
748
+
749
+ if @options[:pager]
750
+ page("\n\n" + out)
751
+ else
752
+ $stdout.puts ("\n\n" + out)
753
+ end
754
+ end
755
+
756
+ def which_pager
757
+ pagers = [ENV['GIT_PAGER'], ENV['PAGER'],
758
+ `git config --get-all core.pager`.split.first,
759
+ 'less', 'more', 'cat', 'pager']
760
+ pagers.select! do |f|
761
+ if f
762
+ system "which #{f} &> /dev/null"
763
+ else
764
+ false
765
+ end
766
+ end
767
+
768
+ pg = pagers.first
769
+ args = case pg
770
+ when 'more'
771
+ ' -r'
772
+ when 'less'
773
+ ' -r'
774
+ else
775
+ ''
776
+ end
777
+
778
+ [pg, args]
779
+ end
780
+ end
781
+ end
@@ -0,0 +1,3 @@
1
+ module CLIMarkdown
2
+ VERSION = '0.0.5'
3
+ end
data/lib/mdless.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'optparse'
2
+ require 'shellwords'
3
+ require 'open3'
4
+ require 'fileutils'
5
+ require 'logger'
6
+ require 'mdless/version.rb'
7
+ require 'mdless/colors'
8
+ require 'mdless/converter'
9
+
10
+ module CLIMarkdown
11
+ EXECUTABLE_NAME = 'mdless'
12
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mdless
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Brett Terpstra
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rdoc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '4.1'
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: 4.1.1
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '4.1'
44
+ - - '>='
45
+ - !ruby/object:Gem::Version
46
+ version: 4.1.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: aruba
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ description: A CLI that provides a formatted and highlighted view of Markdown files
62
+ in a terminal
63
+ email: me@brettterpstra.com
64
+ executables:
65
+ - mdless
66
+ extensions: []
67
+ extra_rdoc_files:
68
+ - README.md
69
+ files:
70
+ - README.md
71
+ - bin/mdless
72
+ - lib/helpers/formattables.py
73
+ - lib/mdless.rb
74
+ - lib/mdless/colors.rb
75
+ - lib/mdless/converter.rb
76
+ - lib/mdless/version.rb
77
+ homepage: http://brettterpstra.com/project/mdless/
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options:
83
+ - --title
84
+ - mdless
85
+ - --main
86
+ - README.md
87
+ - --markup
88
+ - markdown
89
+ - -ri
90
+ require_paths:
91
+ - lib
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 2.4.6
106
+ signing_key:
107
+ specification_version: 4
108
+ summary: A pager like less, but for Markdown files
109
+ test_files: []