tty-markdown 0.1.0
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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.travis.yml +22 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +8 -0
- data/appveyor.yml +21 -0
- data/assets/headers.png +0 -0
- data/assets/list.png +0 -0
- data/assets/quote.png +0 -0
- data/assets/syntax_highlight.png +0 -0
- data/assets/table.png +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/examples/example.md +49 -0
- data/examples/marked.rb +6 -0
- data/lib/tty-markdown.rb +1 -0
- data/lib/tty/markdown.rb +92 -0
- data/lib/tty/markdown/parser.rb +353 -0
- data/lib/tty/markdown/syntax_highlighter.rb +65 -0
- data/lib/tty/markdown/version.rb +5 -0
- data/tasks/console.rake +11 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-markdown.gemspec +33 -0
- metadata +197 -0
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "tty/markdown"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/examples/example.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
TTY::Markdown
|
3
|
+
=============
|
4
|
+
|
5
|
+
**tty-markdown** converts markdown document into a terminal friendly output.
|
6
|
+
|
7
|
+
## Examples
|
8
|
+
|
9
|
+
### Nested list items
|
10
|
+
|
11
|
+
- Item 1
|
12
|
+
- Item 2
|
13
|
+
- Item 3
|
14
|
+
- Item 4
|
15
|
+
- Item 5
|
16
|
+
|
17
|
+
### Quote
|
18
|
+
|
19
|
+
> Blockquotes are very handy in email to emulate reply text.
|
20
|
+
> This line is part of the same quote.
|
21
|
+
> *Oh*, you can put **Markdown** into a blockquote.
|
22
|
+
|
23
|
+
### Codeblock
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
class Greeter
|
27
|
+
def hello(name)
|
28
|
+
puts "Hello #{name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
### Table
|
34
|
+
|
35
|
+
| Tables | Are | Cool |
|
36
|
+
|----------|:-------------:|------:|
|
37
|
+
| col 1 is | left-aligned | $1600 |
|
38
|
+
| col 2 is | centered | $12 |
|
39
|
+
| col 3 is | right-aligned | $1 |
|
40
|
+
|
41
|
+
### Horizontal line
|
42
|
+
|
43
|
+
***
|
44
|
+
|
45
|
+
### Link
|
46
|
+
|
47
|
+
[I'm an inline-style link](https://www.google.com)
|
48
|
+
|
49
|
+
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
data/examples/marked.rb
ADDED
data/lib/tty-markdown.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'tty/markdown'
|
data/lib/tty/markdown.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kramdown'
|
4
|
+
|
5
|
+
require_relative 'markdown/parser'
|
6
|
+
require_relative 'markdown/version'
|
7
|
+
|
8
|
+
module TTY
|
9
|
+
module Markdown
|
10
|
+
SYMBOLS = {
|
11
|
+
arrow: '»',
|
12
|
+
bullet: '●',
|
13
|
+
bar: '┃',
|
14
|
+
diamond: '◈',
|
15
|
+
pipe: '│',
|
16
|
+
line: '─',
|
17
|
+
top_left: '┌',
|
18
|
+
top_right: '┐',
|
19
|
+
top_center: '┬',
|
20
|
+
mid_left: '├',
|
21
|
+
mid_right: '┤',
|
22
|
+
mid_center: '┼',
|
23
|
+
bottom_right: '┘',
|
24
|
+
bottom_left: '└',
|
25
|
+
bottom_center: '┴'
|
26
|
+
}
|
27
|
+
|
28
|
+
WIN_SYMBOLS = {
|
29
|
+
arrow: '->',
|
30
|
+
bullet: '*',
|
31
|
+
diamond: '*',
|
32
|
+
bar: '│',
|
33
|
+
pipe: '|',
|
34
|
+
line: '─',
|
35
|
+
top_left: '+',
|
36
|
+
top_right: '+',
|
37
|
+
top_center: '+',
|
38
|
+
mid_left: '+',
|
39
|
+
mid_right: '+',
|
40
|
+
mid_center: '+',
|
41
|
+
bottom_right: '+',
|
42
|
+
bottom_left: '+',
|
43
|
+
bottom_center: '+'
|
44
|
+
}
|
45
|
+
|
46
|
+
THEME = {
|
47
|
+
em: :italic,
|
48
|
+
header: [:cyan, :bold],
|
49
|
+
hr: :yellow,
|
50
|
+
link: [:blue, :underline],
|
51
|
+
list: :yellow,
|
52
|
+
strong: [:yellow, :bold],
|
53
|
+
table: :blue,
|
54
|
+
quote: :yellow,
|
55
|
+
}
|
56
|
+
|
57
|
+
# Parse a markdown string
|
58
|
+
#
|
59
|
+
# @param [Hash] options
|
60
|
+
# @option options [String] :colors
|
61
|
+
# a number of colors supported
|
62
|
+
# @option options [String] :width
|
63
|
+
#
|
64
|
+
# @param [String] source
|
65
|
+
# the source with markdown
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def parse(source, **options)
|
69
|
+
doc = Kramdown::Document.new(source, options)
|
70
|
+
Parser.convert(doc.root, doc.options).join
|
71
|
+
end
|
72
|
+
module_function :parse
|
73
|
+
|
74
|
+
# Pase a markdown document
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def parse_file(path, **options)
|
78
|
+
parse(::File.read(path), options)
|
79
|
+
end
|
80
|
+
module_function :parse_file
|
81
|
+
|
82
|
+
def symbols
|
83
|
+
@symbols ||= windows? ? WIN_SYMBOLS : SYMBOLS
|
84
|
+
end
|
85
|
+
module_function :symbols
|
86
|
+
|
87
|
+
def windows?
|
88
|
+
::File::ALT_SEPARATOR == "\\"
|
89
|
+
end
|
90
|
+
module_function :windows?
|
91
|
+
end # Markdown
|
92
|
+
end # TTY
|
@@ -0,0 +1,353 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kramdown/converter'
|
4
|
+
require 'pastel'
|
5
|
+
require 'strings'
|
6
|
+
require 'tty-screen'
|
7
|
+
|
8
|
+
require_relative 'syntax_highlighter'
|
9
|
+
|
10
|
+
module TTY
|
11
|
+
module Markdown
|
12
|
+
# Converts a Kramdown::Document tree to a terminal friendly output
|
13
|
+
class Parser < Kramdown::Converter::Base
|
14
|
+
|
15
|
+
def initialize(root, **options)
|
16
|
+
super
|
17
|
+
@stack = []
|
18
|
+
@current_indent = 0
|
19
|
+
@indent = options.fetch(:indent, 2)
|
20
|
+
@pastel = Pastel.new
|
21
|
+
@color_opts = {mode: options[:colors]}
|
22
|
+
@width = options.fetch(:width) { TTY::Screen.width }
|
23
|
+
@theme = options.fetch(:theme) { TTY::Markdown::THEME }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Invoke an element conversion
|
27
|
+
#
|
28
|
+
# @api public
|
29
|
+
def convert(el, opts = {indent: 0, result: []})
|
30
|
+
send("convert_#{el.type}", el, opts)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Process children of this element
|
36
|
+
def inner(el, opts)
|
37
|
+
@stack << [el, opts]
|
38
|
+
el.children.each_with_index do |inner_el, i|
|
39
|
+
options = opts.dup
|
40
|
+
options[:parent] = el
|
41
|
+
options[:prev] = (i == 0 ? nil : el.children[i - 1])
|
42
|
+
options[:index] = i
|
43
|
+
convert(inner_el, options)
|
44
|
+
end
|
45
|
+
@stack.pop
|
46
|
+
end
|
47
|
+
|
48
|
+
def convert_root(el, opts)
|
49
|
+
inner(el, opts)
|
50
|
+
opts[:result]
|
51
|
+
end
|
52
|
+
|
53
|
+
def convert_header(el, opts)
|
54
|
+
level = el.options[:level]
|
55
|
+
@current_indent = (level - 1) * @indent # Header determines indentation
|
56
|
+
indent = ' ' * (level - 1) * @indent
|
57
|
+
styles = Array(@theme[:header]).dup
|
58
|
+
styles << :underline if level == 1
|
59
|
+
opts[:result] << indent + @pastel.lookup(*styles)
|
60
|
+
inner(el, opts)
|
61
|
+
opts[:result] << @pastel.lookup(:reset) + "\n"
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert_p(el, opts)
|
65
|
+
result_before = @stack.last[1][:result].dup
|
66
|
+
indent = ' ' * @current_indent
|
67
|
+
if opts[:parent].type != :blockquote
|
68
|
+
opts[:result] << indent
|
69
|
+
end
|
70
|
+
|
71
|
+
case opts[:parent].type
|
72
|
+
when :li
|
73
|
+
bullet = TTY::Markdown.symbols[:bullet]
|
74
|
+
index = @stack.last[1][:index] + 1
|
75
|
+
symbol = opts[:ordered] ? "#{index}." : bullet
|
76
|
+
styles = Array(@theme[:list])
|
77
|
+
opts[:result] << @pastel.decorate(symbol, *styles) + ' '
|
78
|
+
end
|
79
|
+
|
80
|
+
inner(el, opts)
|
81
|
+
|
82
|
+
if opts[:parent].type == :blockquote
|
83
|
+
format_blockquote(result_before, opts[:result])
|
84
|
+
end
|
85
|
+
|
86
|
+
unless opts[:result].last.end_with?("\n")
|
87
|
+
opts[:result] << "\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_blockquote(result_before, result)
|
92
|
+
indent = ' ' * @current_indent
|
93
|
+
start_index = result_before.size
|
94
|
+
max_index = result.size - 1
|
95
|
+
bar_symbol = TTY::Markdown.symbols[:bar]
|
96
|
+
styles = Array(@theme[:quote])
|
97
|
+
prefix = "#{indent}#{@pastel.decorate(bar_symbol, *styles)} "
|
98
|
+
|
99
|
+
result.map!.with_index do |str, i|
|
100
|
+
if i == start_index
|
101
|
+
str.insert(0, prefix)
|
102
|
+
end
|
103
|
+
|
104
|
+
if i >= start_index && str.include?("\n")
|
105
|
+
str.lines.map! do |line|
|
106
|
+
if line != str.lines.last || i < max_index
|
107
|
+
line.insert(-1, prefix)
|
108
|
+
else
|
109
|
+
line
|
110
|
+
end
|
111
|
+
end
|
112
|
+
else
|
113
|
+
str
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def convert_text(el, opts)
|
119
|
+
text = el.value
|
120
|
+
opts[:result] << text
|
121
|
+
end
|
122
|
+
|
123
|
+
def convert_strong(el, opts)
|
124
|
+
styles = Array(@theme[:strong])
|
125
|
+
opts[:result] << @pastel.lookup(*styles)
|
126
|
+
inner(el, opts)
|
127
|
+
opts[:result] << @pastel.lookup(:reset)
|
128
|
+
end
|
129
|
+
|
130
|
+
def convert_em(el, opts)
|
131
|
+
styles = Array(@theme[:em])
|
132
|
+
opts[:result] << @pastel.lookup(*styles)
|
133
|
+
inner(el, opts)
|
134
|
+
opts[:result] << @pastel.lookup(:reset)
|
135
|
+
end
|
136
|
+
|
137
|
+
def convert_blank(el, opts)
|
138
|
+
opts[:result] << "\n"
|
139
|
+
end
|
140
|
+
|
141
|
+
def convert_smart_quote(el, opts)
|
142
|
+
inner(el, opts)
|
143
|
+
end
|
144
|
+
|
145
|
+
def convert_codespan(el, opts)
|
146
|
+
raw_code = el.value
|
147
|
+
highlighted = SyntaxHighliter.highlight(raw_code, @color_opts)
|
148
|
+
code = highlighted.split("\n").map.with_index do |line, i|
|
149
|
+
if i == 0 # first line
|
150
|
+
line
|
151
|
+
else
|
152
|
+
line.insert(0, ' ' * @current_indent)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
opts[:result] << code.join("\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
def convert_blockquote(el, opts)
|
159
|
+
inner(el, opts)
|
160
|
+
end
|
161
|
+
|
162
|
+
def convert_ul(el, opts)
|
163
|
+
@current_indent += @indent unless opts[:parent].type == :root
|
164
|
+
inner(el, opts)
|
165
|
+
@current_indent -= @indent unless opts[:parent].type == :root
|
166
|
+
end
|
167
|
+
alias convert_ol convert_ul
|
168
|
+
|
169
|
+
def convert_li(el, opts)
|
170
|
+
if opts[:parent].type == :ol
|
171
|
+
opts[:ordered] = true
|
172
|
+
end
|
173
|
+
inner(el, opts)
|
174
|
+
end
|
175
|
+
|
176
|
+
def convert_table(el, opts)
|
177
|
+
opts[:alignment] = el.options[:alignment]
|
178
|
+
|
179
|
+
result = opts[:result]
|
180
|
+
opts[:result] = []
|
181
|
+
data = []
|
182
|
+
|
183
|
+
el.children.each do |container|
|
184
|
+
container.children.each do |row|
|
185
|
+
data_row = []
|
186
|
+
data << data_row
|
187
|
+
row.children.each do |cell|
|
188
|
+
opts[:result] = []
|
189
|
+
cell_data = inner(cell, opts)
|
190
|
+
data_row << cell_data[1][:result]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
opts[:result] = result
|
196
|
+
opts[:table_data] = data
|
197
|
+
|
198
|
+
inner(el, opts)
|
199
|
+
end
|
200
|
+
|
201
|
+
def convert_thead(el, opts)
|
202
|
+
indent = ' ' * @current_indent
|
203
|
+
table_data = opts[:table_data]
|
204
|
+
|
205
|
+
opts[:result] << indent
|
206
|
+
opts[:result] << border(table_data, :top)
|
207
|
+
opts[:result] << "\n"
|
208
|
+
inner(el, opts)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Render horizontal border line
|
212
|
+
#
|
213
|
+
# @param [Array[Array[String]]] table_data
|
214
|
+
# table rows and cells
|
215
|
+
# @param [Symbol] location
|
216
|
+
# location out of :top, :mid, :bottom
|
217
|
+
#
|
218
|
+
# @return [String]
|
219
|
+
#
|
220
|
+
# @api private
|
221
|
+
def border(table_data, location)
|
222
|
+
symbols = TTY::Markdown.symbols
|
223
|
+
result = []
|
224
|
+
result << symbols[:"#{location}_left"]
|
225
|
+
max_widths(table_data).each.with_index do |width, i|
|
226
|
+
result << symbols[:"#{location}_center"] if i != 0
|
227
|
+
result << (symbols[:line] * (width + 2))
|
228
|
+
end
|
229
|
+
result << symbols[:"#{location}_right"]
|
230
|
+
styles = Array(@theme[:table])
|
231
|
+
@pastel.decorate(result.join, *styles)
|
232
|
+
end
|
233
|
+
|
234
|
+
def convert_tbody(el, opts)
|
235
|
+
indent = ' ' * @current_indent
|
236
|
+
table_data = opts[:table_data]
|
237
|
+
|
238
|
+
opts[:result] << indent
|
239
|
+
if opts[:prev] && opts[:prev].type == :thead
|
240
|
+
opts[:result] << border(table_data, :mid)
|
241
|
+
else
|
242
|
+
opts[:result] << border(table_data, :top)
|
243
|
+
end
|
244
|
+
opts[:result] << "\n"
|
245
|
+
|
246
|
+
inner(el, opts)
|
247
|
+
|
248
|
+
opts[:result] << indent
|
249
|
+
opts[:result] << border(table_data, :bottom)
|
250
|
+
opts[:result] << "\n"
|
251
|
+
end
|
252
|
+
|
253
|
+
def convert_tfoot(el, opts)
|
254
|
+
innert(el, opts)
|
255
|
+
end
|
256
|
+
|
257
|
+
def convert_tr(el, opts)
|
258
|
+
indent = ' ' * @current_indent
|
259
|
+
pipe = TTY::Markdown.symbols[:pipe]
|
260
|
+
styles = Array(@theme[:table])
|
261
|
+
table_data = opts[:table_data]
|
262
|
+
|
263
|
+
if opts[:prev] && opts[:prev].type == :tr
|
264
|
+
opts[:result] << indent
|
265
|
+
opts[:result] << border(table_data, :mid)
|
266
|
+
opts[:result] << "\n"
|
267
|
+
end
|
268
|
+
|
269
|
+
opts[:result] << indent + @pastel.decorate("#{pipe} ", *styles)
|
270
|
+
inner(el, opts)
|
271
|
+
opts[:result] << "\n"
|
272
|
+
end
|
273
|
+
|
274
|
+
def convert_td(el, opts)
|
275
|
+
pipe = TTY::Markdown.symbols[:pipe]
|
276
|
+
styles = Array(@theme[:table])
|
277
|
+
table_data = opts[:table_data]
|
278
|
+
result = opts[:result]
|
279
|
+
opts[:result] = []
|
280
|
+
|
281
|
+
inner(el, opts)
|
282
|
+
|
283
|
+
column = find_column(table_data, opts[:result])
|
284
|
+
width = max_width(table_data, column)
|
285
|
+
alignment = opts[:alignment][column]
|
286
|
+
align_opts = alignment == :default ? {} : {direction: alignment}
|
287
|
+
|
288
|
+
result << Strings.align(opts[:result].join, width, align_opts) <<
|
289
|
+
" #{@pastel.decorate(pipe, *styles)} "
|
290
|
+
end
|
291
|
+
|
292
|
+
def find_column(table_data, cell)
|
293
|
+
table_data.each do |row|
|
294
|
+
row.size.times do |col|
|
295
|
+
return col if row[col] == cell
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def max_width(table_data, col)
|
301
|
+
table_data.map { |row| Strings.sanitize(row[col].join).length }.max
|
302
|
+
end
|
303
|
+
|
304
|
+
def max_widths(table_data)
|
305
|
+
table_data.first.each_with_index.reduce([]) do |acc, (*, col)|
|
306
|
+
acc << max_width(table_data, col)
|
307
|
+
acc
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def convert_hr(el, opts)
|
312
|
+
indent = ' ' * @current_indent
|
313
|
+
symbols = TTY::Markdown.symbols
|
314
|
+
width = @width - (indent.length+1) * 2
|
315
|
+
styles = Array(@theme[:hr])
|
316
|
+
line = symbols[:diamond] + symbols[:line] * width + symbols[:diamond]
|
317
|
+
|
318
|
+
opts[:result] << indent
|
319
|
+
opts[:result] << @pastel.decorate(line, *styles)
|
320
|
+
opts[:result] << "\n"
|
321
|
+
end
|
322
|
+
|
323
|
+
def convert_a(el, opts)
|
324
|
+
symbols = TTY::Markdown.symbols
|
325
|
+
styles = Array(@theme[:link])
|
326
|
+
if el.children.size == 1 && el.children[0].type == :text
|
327
|
+
opts[:result] << @pastel.decorate(el.attr['href'], *styles)
|
328
|
+
else
|
329
|
+
if el.attr['title']
|
330
|
+
opts[:result] << el.attr['title']
|
331
|
+
else
|
332
|
+
inner(el, opts)
|
333
|
+
end
|
334
|
+
opts[:result] << " #{symbols[:arrow]} "
|
335
|
+
opts[:result] << @pastel.decorate(el.attr['href'], *styles)
|
336
|
+
opts[:result] << "\n"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def convert_footnote(*)
|
341
|
+
warning("Footnotes are not supported")
|
342
|
+
end
|
343
|
+
|
344
|
+
def convert_raw(*)
|
345
|
+
warning("Raw content is not supported")
|
346
|
+
end
|
347
|
+
|
348
|
+
def convert_img(*)
|
349
|
+
warning("Images are not supported")
|
350
|
+
end
|
351
|
+
end # Parser
|
352
|
+
end # Markdown
|
353
|
+
end # TTY
|