mdless 0.0.5

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.
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: []