tty-markdown 0.6.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/examples/man.rb DELETED
@@ -1,6 +0,0 @@
1
- require_relative '../lib/tty-markdown'
2
-
3
- path = File.join(__dir__, 'man.md')
4
- out = TTY::Markdown.parse_file(path, colors: 256)
5
-
6
- puts out
data/examples/marked.rb DELETED
@@ -1,6 +0,0 @@
1
- require_relative '../lib/tty-markdown'
2
-
3
- path = File.join(__dir__, 'example.md')
4
- out = TTY::Markdown.parse_file(path, colors: 256, width: 80)
5
-
6
- puts out
@@ -1,482 +0,0 @@
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
-
68
- if opts[:parent].type != :blockquote
69
- opts[:result] << indent
70
- end
71
-
72
- opts[:indent] = @current_indent
73
- opts[:strip] = false
74
-
75
- case opts[:parent].type
76
- when :li
77
- bullet = TTY::Markdown.symbols[:bullet]
78
- index = @stack.last[1][:index] + 1
79
- symbol = opts[:ordered] ? "#{index}." : bullet
80
- styles = Array(@theme[:list])
81
- opts[:result] << @pastel.decorate(symbol, *styles) + ' '
82
- opts[:indent] += @indent
83
- opts[:strip] = true
84
- when :blockquote
85
- opts[:indent] = 0
86
- end
87
-
88
- inner(el, opts)
89
-
90
- if opts[:parent].type == :blockquote
91
- format_blockquote(result_before, opts[:result])
92
- end
93
-
94
- unless opts[:result].last.end_with?("\n")
95
- opts[:result] << "\n"
96
- end
97
- end
98
-
99
- # Format current element by inserting prefix for each
100
- # quoted line within the allowed screen size.
101
- #
102
- # @param [Array[String]] result_before
103
- # @param [Array[String]] result
104
- #
105
- # @return [nil]
106
- #
107
- # @api private
108
- def format_blockquote(result_before, result)
109
- indent = ' ' * @current_indent
110
- start_index = result_before.size
111
- max_index = result.size - 1
112
- bar_symbol = TTY::Markdown.symbols[:bar]
113
- styles = Array(@theme[:quote])
114
- prefix = "#{indent}#{@pastel.decorate(bar_symbol, *styles)} "
115
-
116
- result.map!.with_index do |str, i|
117
- if i == start_index
118
- str.insert(0, prefix)
119
- end
120
-
121
- # only modify blockquote element
122
- if i >= start_index && str.to_s.include?("\n") # multiline string found
123
- str.lines.map! do |line|
124
- if (line != str.lines.last || i < max_index)
125
- line.insert(-1, line.end_with?("\n") ? prefix : "\n" + prefix)
126
- else
127
- line
128
- end
129
- end.join
130
- else
131
- str
132
- end
133
- end
134
- end
135
-
136
- def convert_text(el, opts)
137
- text = Strings.wrap(el.value, @width)
138
- text = text.chomp if opts[:strip]
139
- indent = ' ' * opts[:indent]
140
- text = text.gsub(/\n/, "\n#{indent}")
141
- opts[:result] << text
142
- end
143
-
144
- def convert_strong(el, opts)
145
- styles = Array(@theme[:strong])
146
- opts[:result] << @pastel.lookup(*styles)
147
- inner(el, opts)
148
- opts[:result] << @pastel.lookup(:reset)
149
- end
150
-
151
- def convert_em(el, opts)
152
- styles = Array(@theme[:em])
153
- opts[:result] << @pastel.lookup(*styles)
154
- inner(el, opts)
155
- opts[:result] << @pastel.lookup(:reset)
156
- end
157
-
158
- def convert_blank(el, opts)
159
- opts[:result] << "\n"
160
- end
161
-
162
- def convert_smart_quote(el, opts)
163
- opts[:result] << TTY::Markdown.symbols[el.value]
164
- end
165
-
166
- def convert_codespan(el, opts)
167
- raw_code = Strings.wrap(el.value, @width)
168
- highlighted = SyntaxHighliter.highlight(raw_code, @color_opts.merge(opts))
169
- code = highlighted.split("\n").map.with_index do |line, i|
170
- if i.zero? # first line
171
- line
172
- else
173
- line.insert(0, ' ' * @current_indent)
174
- end
175
- end
176
- opts[:result] << code.join("\n")
177
- end
178
-
179
- def convert_codeblock(el, opts)
180
- opts[:fenced] = false
181
- convert_codespan(el, opts)
182
- end
183
-
184
- def convert_blockquote(el, opts)
185
- inner(el, opts)
186
- end
187
-
188
- def convert_ul(el, opts)
189
- @current_indent += @indent unless opts[:parent].type == :root
190
- inner(el, opts)
191
- @current_indent -= @indent unless opts[:parent].type == :root
192
- end
193
- alias convert_ol convert_ul
194
-
195
- def convert_li(el, opts)
196
- if opts[:parent].type == :ol
197
- opts[:ordered] = true
198
- end
199
- inner(el, opts)
200
- end
201
-
202
- def convert_table(el, opts)
203
- opts[:alignment] = el.options[:alignment]
204
-
205
- result = opts[:result]
206
- opts[:result] = []
207
- data = []
208
-
209
- el.children.each do |container|
210
- container.children.each do |row|
211
- data_row = []
212
- data << data_row
213
- row.children.each do |cell|
214
- opts[:result] = []
215
- cell_data = inner(cell, opts)
216
- data_row << cell_data[1][:result]
217
- end
218
- end
219
- end
220
-
221
- opts[:result] = result
222
- opts[:table_data] = data
223
-
224
- inner(el, opts)
225
- end
226
-
227
- def convert_thead(el, opts)
228
- indent = ' ' * @current_indent
229
- table_data = opts[:table_data]
230
-
231
- opts[:result] << indent
232
- opts[:result] << border(table_data, :top)
233
- opts[:result] << "\n"
234
- inner(el, opts)
235
- end
236
-
237
- # Render horizontal border line
238
- #
239
- # @param [Array[Array[String]]] table_data
240
- # table rows and cells
241
- # @param [Symbol] location
242
- # location out of :top, :mid, :bottom
243
- #
244
- # @return [String]
245
- #
246
- # @api private
247
- def border(table_data, location)
248
- symbols = TTY::Markdown.symbols
249
- result = []
250
- result << symbols[:"#{location}_left"]
251
- distribute_widths(max_widths(table_data)).each.with_index do |width, i|
252
- result << symbols[:"#{location}_center"] if i != 0
253
- result << (symbols[:line] * (width + 2))
254
- end
255
- result << symbols[:"#{location}_right"]
256
- styles = Array(@theme[:table])
257
- @pastel.decorate(result.join, *styles)
258
- end
259
-
260
- def convert_tbody(el, opts)
261
- indent = ' ' * @current_indent
262
- table_data = opts[:table_data]
263
-
264
- opts[:result] << indent
265
- if opts[:prev] && opts[:prev].type == :thead
266
- opts[:result] << border(table_data, :mid)
267
- else
268
- opts[:result] << border(table_data, :top)
269
- end
270
- opts[:result] << "\n"
271
-
272
- inner(el, opts)
273
-
274
- opts[:result] << indent
275
- opts[:result] << border(table_data, :bottom)
276
- opts[:result] << "\n"
277
- end
278
-
279
- def convert_tfoot(el, opts)
280
- inner(el, opts)
281
- end
282
-
283
- def convert_tr(el, opts)
284
- indent = ' ' * @current_indent
285
- table_data = opts[:table_data]
286
-
287
- if opts[:prev] && opts[:prev].type == :tr
288
- opts[:result] << indent
289
- opts[:result] << border(table_data, :mid)
290
- opts[:result] << "\n"
291
- end
292
-
293
- opts[:cells] = []
294
-
295
- inner(el, opts)
296
-
297
- columns = table_data.first.count
298
-
299
- row = opts[:cells].each_with_index.reduce([]) do |acc, (cell, i)|
300
- if cell.size > 1 # multiline
301
- cell.each_with_index do |c, j| # zip columns
302
- acc[j] = [] if acc[j].nil?
303
- acc[j] << c.chomp
304
- acc[j] << "\n" if i == (columns - 1)
305
- end
306
- else
307
- acc << cell
308
- acc << "\n" if i == (columns - 1)
309
- end
310
- acc
311
- end.join
312
-
313
- opts[:result] << row
314
- end
315
-
316
- def convert_td(el, opts)
317
- indent = ' ' * @current_indent
318
- pipe = TTY::Markdown.symbols[:pipe]
319
- styles = Array(@theme[:table])
320
- table_data = opts[:table_data]
321
- result = opts[:cells]
322
- suffix = " #{@pastel.decorate(pipe, *styles)} "
323
- opts[:result] = []
324
-
325
- inner(el, opts)
326
-
327
- row, column = *find_row_column(table_data, opts[:result])
328
- cell_widths = distribute_widths(max_widths(table_data))
329
- cell_width = cell_widths[column]
330
- cell_height = max_height(table_data, row, cell_widths)
331
- alignment = opts[:alignment][column]
332
- align_opts = alignment == :default ? {} : { direction: alignment }
333
-
334
- wrapped = Strings.wrap(opts[:result].join, cell_width)
335
- aligned = Strings.align(wrapped, cell_width, align_opts)
336
- padded = if aligned.lines.size < cell_height
337
- Strings.pad(aligned, [0, 0, cell_height - aligned.lines.size, 0])
338
- else
339
- aligned.dup
340
- end
341
-
342
- result << padded.lines.map do |line|
343
- # add pipe to first column
344
- (column.zero? ? indent + @pastel.decorate("#{pipe} ", *styles) : '') +
345
- (line.end_with?("\n") ? line.insert(-2, suffix) : line << suffix)
346
- end
347
- end
348
-
349
- # Find row and column indexes
350
- #
351
- # @return [Array[Integer, Integer]]
352
- #
353
- # @api private
354
- def find_row_column(table_data, cell)
355
- table_data.each_with_index do |row, row_no|
356
- row.size.times do |col|
357
- return [row_no, col] if row[col] == cell
358
- end
359
- end
360
- end
361
-
362
- # Calculate maximum cell width for a given column
363
- #
364
- # @return [Integer]
365
- #
366
- # @api private
367
- def max_width(table_data, col)
368
- table_data.map do |row|
369
- Strings.sanitize(row[col].join).lines.map(&:length).max
370
- end.max
371
- end
372
-
373
- # Calculate maximum cell height for a given row
374
- #
375
- # @return [Integer]
376
- #
377
- # @api private
378
- def max_height(table_data, row, cell_widths)
379
- table_data[row].map.with_index do |col, i|
380
- Strings.wrap(col.join, cell_widths[i]).lines.size
381
- end.max
382
- end
383
-
384
- def max_widths(table_data)
385
- table_data.first.each_with_index.reduce([]) do |acc, (*, col)|
386
- acc << max_width(table_data, col)
387
- acc
388
- end
389
- end
390
-
391
- def distribute_widths(widths)
392
- indent = ' ' * @current_indent
393
- total_width = widths.reduce(&:+)
394
- screen_width = @width - (indent.length + 1) * 2 - (widths.size + 1)
395
- return widths if total_width <= screen_width
396
-
397
- extra_width = total_width - screen_width
398
-
399
- widths.map do |w|
400
- ratio = w / total_width.to_f
401
- w - (extra_width * ratio).floor
402
- end
403
- end
404
-
405
- def convert_hr(el, opts)
406
- indent = ' ' * @current_indent
407
- symbols = TTY::Markdown.symbols
408
- width = @width - (indent.length + 1) * 2
409
- styles = Array(@theme[:hr])
410
- line = symbols[:diamond] + symbols[:line] * width + symbols[:diamond]
411
-
412
- opts[:result] << indent
413
- opts[:result] << @pastel.decorate(line, *styles)
414
- opts[:result] << "\n"
415
- end
416
-
417
- def convert_a(el, opts)
418
- symbols = TTY::Markdown.symbols
419
- styles = Array(@theme[:link])
420
- if el.children.size == 1 && el.children[0].type == :text
421
- opts[:result] << @pastel.decorate(el.attr['href'], *styles)
422
- else
423
- if el.attr['title']
424
- opts[:result] << el.attr['title']
425
- else
426
- inner(el, opts)
427
- end
428
- opts[:result] << " #{symbols[:arrow]} "
429
- opts[:result] << @pastel.decorate(el.attr['href'], *styles)
430
- opts[:result] << "\n"
431
- end
432
- end
433
-
434
- def convert_math(el, opts)
435
- if opts[:prev] && opts[:prev].type == :blank
436
- indent = ' ' * @current_indent
437
- opts[:result] << indent
438
- end
439
- convert_codespan(el, opts)
440
- opts[:result] << "\n"
441
- end
442
-
443
- def convert_abbreviation(el, opts)
444
- opts[:result] << el.value
445
- end
446
-
447
- def convert_typographic_sym(el, opts)
448
- opts[:result] << TTY::Markdown.symbols[el.value]
449
- end
450
-
451
- def convert_entity(el, opts)
452
- opts[:result] << unicode_char(el.value.code_point)
453
- end
454
-
455
- # Convert codepoint to UTF-8 representation
456
- def unicode_char(codepoint)
457
- [codepoint].pack('U*')
458
- end
459
-
460
- def convert_footnote(*)
461
- warning("Footnotes are not supported")
462
- end
463
-
464
- def convert_raw(*)
465
- warning("Raw content is not supported")
466
- end
467
-
468
- def convert_img(*)
469
- warning("Images are not supported")
470
- end
471
-
472
- def convert_html_element(*)
473
- warning("HTML elements are not supported")
474
- end
475
-
476
- def convert_xml_comment(el, opts)
477
- opts[:result] << el.value << "\n"
478
- end
479
- alias convert_comment convert_xml_comment
480
- end # Parser
481
- end # Markdown
482
- end # TTY
data/spec/spec_helper.rb DELETED
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- if ENV['COVERAGE'] || ENV['TRAVIS']
4
- require 'simplecov'
5
- require 'coveralls'
6
-
7
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
- SimpleCov::Formatter::HTMLFormatter,
9
- Coveralls::SimpleCov::Formatter
10
- ]
11
-
12
- SimpleCov.start do
13
- command_name 'spec'
14
- add_filter 'spec'
15
- end
16
- end
17
-
18
- require "bundler/setup"
19
- require "tty/markdown"
20
-
21
- RSpec.configure do |config|
22
- # Enable flags like --only-failures and --next-failure
23
- config.example_status_persistence_file_path = ".rspec_status"
24
-
25
- # Disable RSpec exposing methods globally on `Module` and `main`
26
- config.disable_monkey_patching!
27
-
28
- config.expect_with :rspec do |c|
29
- c.syntax = :expect
30
- end
31
- end
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe TTY::Markdown, 'abbrev' do
4
- it "abbreviates markdown" do
5
- markdown =<<-TEXT
6
- *[HTML]: Hyper Text Markup Language
7
- test HTML
8
- TEXT
9
- parsed = TTY::Markdown.parse(markdown)
10
- expect(parsed).to eq([
11
- "test HTML\n"
12
- ].join("\n"))
13
- end
14
-
15
- it "indents abbreviations correctly" do
16
- markdown =<<-TEXT
17
- ### header
18
- *[HTML]: Hyper Text Markup Language
19
- test HTML
20
- TEXT
21
- parsed = TTY::Markdown.parse(markdown)
22
- expect(parsed).to eq([
23
- " \e[36;1mheader\e[0m",
24
- " test HTML\n"
25
- ].join("\n"))
26
- end
27
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe TTY::Markdown, 'blockquote' do
4
- let(:bar) { TTY::Markdown.symbols[:bar] }
5
- let(:apos) { TTY::Markdown.symbols[:rsquo] }
6
-
7
- it "converts single blockquote" do
8
- markdown =<<-TEXT
9
- > Oh, you can *put* **Markdown** into a blockquote.
10
- TEXT
11
- parsed = TTY::Markdown.parse(markdown)
12
- expect(parsed).to eq([
13
- "\e[33m#{bar}\e[0m Oh, you can \e[33mput\e[0m \e[33;1mMarkdown\e[0m into a blockquote.\n"
14
- ].join)
15
- end
16
-
17
- it "indents blockquote within header" do
18
- markdown =<<-TEXT
19
- ### Quote
20
- > Oh, you can *put* **Markdown** into a blockquote.
21
- TEXT
22
- parsed = TTY::Markdown.parse(markdown)
23
- expect(parsed).to eq([
24
- " \e[36;1mQuote\e[0m",
25
- " \e[33m#{bar}\e[0m Oh, you can \e[33mput\e[0m \e[33;1mMarkdown\e[0m into a blockquote.\n"
26
- ].join("\n"))
27
- end
28
-
29
- it "converts multiple blockquotes without header" do
30
- markdown =<<-TEXT
31
- > one
32
- > two
33
- > three
34
- TEXT
35
- parsed = TTY::Markdown.parse(markdown)
36
- expected_output =
37
- "\e[33m#{bar}\e[0m one\n" +
38
- "\e[33m#{bar}\e[0m two\n" +
39
- "\e[33m#{bar}\e[0m three\n"
40
-
41
- expect(parsed).to eq(expected_output)
42
- end
43
-
44
- it "converts multiple blockquote" do
45
- markdown =<<-TEXT
46
- ### Quote
47
- > Blockquotes are very handy in email to emulate reply text.
48
- > This line is part of the same quote.
49
- > *Oh*, you can put **Markdown** into a blockquote.
50
- TEXT
51
- parsed = TTY::Markdown.parse(markdown)
52
- expect(parsed).to eq([
53
- " \e[36;1mQuote\e[0m\n",
54
- " \e[33m#{bar}\e[0m Blockquotes are very handy in email to emulate reply text.\n",
55
- " \e[33m#{bar}\e[0m This line is part of the same quote.\n",
56
- " \e[33m#{bar}\e[0m \e[33mOh\e[0m, you can put \e[33;1mMarkdown\e[0m into a blockquote.\n"
57
- ].join)
58
- end
59
-
60
- it "converts blockquote into lines" do
61
- markdown =<<-TEXT
62
- > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
63
- > Last line to ensure all is fine.
64
- TEXT
65
-
66
- parsed = TTY::Markdown.parse(markdown, width: 50)
67
- expected_output =
68
- "\e[33m#{bar}\e[0m This is a very long line that will still be \n" +
69
- "\e[33m#{bar}\e[0m quoted properly when it wraps. Oh boy let\n" +
70
- "\e[33m#{bar}\e[0m #{apos}s keep writing to make sure this is long enough \n" +
71
- "\e[33m#{bar}\e[0m to actually wrap for everyone. Oh, you can \n" +
72
- "\e[33m#{bar}\e[0m \e[33mput\e[0m \e[33;1mMarkdown\e[0m into a blockquote.\n" +
73
- "\e[33m#{bar}\e[0m Last line to ensure all is fine.\n"
74
-
75
- expect(parsed).to eq(expected_output)
76
- end
77
- end