tty-markdown 0.3.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,821 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown/converter"
4
+ require "kramdown/element"
5
+ require "pastel"
6
+ require "strings"
7
+ require "uri"
8
+
9
+ require_relative "syntax_highlighter"
10
+
11
+ module TTY
12
+ module Markdown
13
+ # Converts a Kramdown::Document tree to a terminal friendly output
14
+ class Converter < ::Kramdown::Converter::Base
15
+ NEWLINE = "\n"
16
+ SPACE = " "
17
+
18
+ def initialize(root, options = {})
19
+ super
20
+ @current_indent = 0
21
+ @indent = options[:indent]
22
+ @pastel = Pastel.new(enabled: options[:enabled])
23
+ @color_opts = { mode: options[:mode],
24
+ color: @pastel.yellow.detach,
25
+ enabled: options[:enabled] }
26
+ @width = options[:width]
27
+ @theme = options[:theme].each_with_object({}) do |(key, val), acc|
28
+ acc[key] = Array(val)
29
+ end
30
+ @symbols = options[:symbols]
31
+ @footnote_no = 1
32
+ @footnotes = {}
33
+ end
34
+
35
+ # Invoke an element conversion
36
+ #
37
+ # @api public
38
+ def convert(el, opts = { indent: 0 })
39
+ send("convert_#{el.type}", el, opts)
40
+ end
41
+
42
+ private
43
+
44
+ # Process children of this element
45
+ #
46
+ # @param [Kramdown::Element] el
47
+ # the element with child elements
48
+ #
49
+ # @api private
50
+ def inner(el, opts)
51
+ result = []
52
+ el.children.each_with_index do |inner_el, i|
53
+ options = opts.dup
54
+ options[:parent] = el
55
+ options[:prev] = (i.zero? ? nil : el.children[i - 1])
56
+ options[:next] = (i == el.children.length - 1 ? nil : el.children[i + 1])
57
+ options[:index] = i
58
+ result << convert(inner_el, options)
59
+ end
60
+ result
61
+ end
62
+
63
+ # Convert root element
64
+ #
65
+ # @param [Kramdown::Element] el
66
+ # the `kd:root` element
67
+ # @param [Hash] opts
68
+ # the element options
69
+ #
70
+ # @api private
71
+ def convert_root(el, opts)
72
+ content = inner(el, opts)
73
+ return content.join if @footnotes.empty?
74
+
75
+ content.join + footnotes_list(root, opts)
76
+ end
77
+
78
+ # Create an ordered list of footnotes
79
+ #
80
+ # @param [Kramdown::Element] root
81
+ # the `kd:root` element
82
+ # @param [Hash] opts
83
+ # the root element options
84
+ #
85
+ # @api private
86
+ def footnotes_list(root, opts)
87
+ ol = Kramdown::Element.new(:ol)
88
+ @footnotes.values.each do |footnote|
89
+ value, index = *footnote
90
+ options = { index: index, parent: ol }
91
+ li = Kramdown::Element.new(:li, nil, {}, options.merge(opts))
92
+ li.children = Marshal.load(Marshal.dump(value.children))
93
+ ol.children << li
94
+ end
95
+ convert_ol(ol, { parent: root }.merge(opts))
96
+ end
97
+
98
+ # Convert header element
99
+ #
100
+ # @param [Kramdown::Element] el
101
+ # the `kd:header` element
102
+ # @param [Hash] opts
103
+ # the element options
104
+ #
105
+ # @api private
106
+ def convert_header(el, opts)
107
+ level = el.options[:level]
108
+ if opts[:parent] && opts[:parent].type == :root
109
+ # Header determines indentation only at top level
110
+ @current_indent = (level - 1) * @indent
111
+ indent = SPACE * (level - 1) * @indent
112
+ else
113
+ indent = SPACE * @current_indent
114
+ end
115
+ styles = @theme[:header].dup
116
+ styles << :underline if level == 1
117
+
118
+ content = inner(el, opts)
119
+
120
+ content.join.lines.map do |line|
121
+ indent + @pastel.decorate(line.chomp, *styles) + NEWLINE
122
+ end
123
+ end
124
+
125
+ # Convert paragraph element
126
+ #
127
+ # @param [Kramdown::Element] el
128
+ # the `kd:p` element
129
+ # @param [Hash] opts
130
+ # the element options
131
+ #
132
+ # @api private
133
+ def convert_p(el, opts)
134
+ indent = SPACE * @current_indent
135
+ result = []
136
+
137
+ if ![:blockquote, :li].include?(opts[:parent].type)
138
+ result << indent
139
+ end
140
+
141
+ opts[:indent] = @current_indent
142
+ if opts[:parent].type == :blockquote
143
+ opts[:indent] = 0
144
+ end
145
+
146
+ content = inner(el, opts)
147
+
148
+ result << content.join
149
+ unless result.last.to_s.end_with?(NEWLINE)
150
+ result << NEWLINE
151
+ end
152
+ result
153
+ end
154
+
155
+ # Convert text element
156
+ #
157
+ # @param [Kramdown::Element] element
158
+ # the `kd:text` element
159
+ # @param [Hash] opts
160
+ # the element options
161
+ #
162
+ # @api private
163
+ def convert_text(el, opts)
164
+ text = Strings.wrap(el.value, @width - @current_indent)
165
+ text = text.chomp if opts[:strip]
166
+ indent = SPACE * opts[:indent]
167
+ text.gsub(/\n/, "#{NEWLINE}#{indent}")
168
+ end
169
+
170
+ # Convert strong element
171
+ #
172
+ # @param [Kramdown::Element] element
173
+ # the `kd:strong` element
174
+ # @param [Hash] opts
175
+ # the element options
176
+ #
177
+ # @api private
178
+ def convert_strong(el, opts)
179
+ content = inner(el, opts)
180
+
181
+ content.join.lines.map do |line|
182
+ @pastel.decorate(line.chomp, *@theme[:strong])
183
+ end.join(NEWLINE)
184
+ end
185
+
186
+ # Convert em element
187
+ #
188
+ # @param [Kramdown::Element] el
189
+ # the `kd:em` element
190
+ # @param [Hash] opts
191
+ # the element options
192
+ #
193
+ # @api private
194
+ def convert_em(el, opts)
195
+ content = inner(el, opts)
196
+
197
+ content.join.lines.map do |line|
198
+ @pastel.decorate(line.chomp, *@theme[:em])
199
+ end.join(NEWLINE)
200
+ end
201
+
202
+ # Convert new line element
203
+ #
204
+ # @param [Kramdown::Element] el
205
+ # the `kd:blank` element
206
+ # @param [Hash] opts
207
+ # the element options
208
+ #
209
+ # @api private
210
+ def convert_blank(*)
211
+ NEWLINE
212
+ end
213
+
214
+ # Convert smart quote element
215
+ #
216
+ # @param [Kramdown::Element] el
217
+ # the `kd:smart_quote` element
218
+ # @param [Hash] opts
219
+ # the element options
220
+ #
221
+ # @api private
222
+ def convert_smart_quote(el, opts)
223
+ @symbols[el.value]
224
+ end
225
+
226
+ # Convert codespan element
227
+ #
228
+ # @param [Kramdown::Element] el
229
+ # the `kd:codespan` element
230
+ # @param [Hash] opts
231
+ # the element options
232
+ #
233
+ # @api private
234
+ def convert_codespan(el, opts)
235
+ indent = SPACE * @current_indent
236
+ syntax_opts = @color_opts.merge(lang: el.options[:lang])
237
+ raw_code = Strings.wrap(el.value, @width - @current_indent)
238
+ highlighted = SyntaxHighliter.highlight(raw_code, **syntax_opts)
239
+
240
+ highlighted.lines.map.with_index do |line, i|
241
+ i.zero? ? line.chomp : indent + line.chomp
242
+ end.join(NEWLINE)
243
+ end
244
+
245
+ # Convert codeblock element
246
+ #
247
+ # @param [Kramdown::Element] el
248
+ # the `kd:codeblock` element
249
+ # @param [Hash] opts
250
+ # the element options
251
+ #
252
+ # @api private
253
+ def convert_codeblock(el, opts)
254
+ indent = SPACE * @current_indent
255
+ indent + convert_codespan(el, opts)
256
+ end
257
+
258
+ # Convert blockquote element
259
+ #
260
+ # @param [Kramdown::Element] el
261
+ # the `kd:blockquote` element
262
+ # @param [Hash] opts
263
+ # the element options
264
+ #
265
+ # @api private
266
+ def convert_blockquote(el, opts)
267
+ indent = SPACE * @current_indent
268
+ bar_symbol = @symbols[:bar]
269
+ prefix = "#{indent}#{@pastel.decorate(bar_symbol, *@theme[:quote])} "
270
+
271
+ content = inner(el, opts)
272
+
273
+ content.join.lines.map do |line|
274
+ prefix + line
275
+ end
276
+ end
277
+
278
+ # Convert ordered and unordered list element
279
+ #
280
+ # @param [Kramdown::Element] el
281
+ # the `kd:ul` or `kd:ol` element
282
+ # @param [Hash] opts
283
+ # the element options
284
+ #
285
+ # @api private
286
+ def convert_ul(el, opts)
287
+ @current_indent += @indent unless opts[:parent].type == :root
288
+ content = inner(el, opts)
289
+ @current_indent -= @indent unless opts[:parent].type == :root
290
+ content.join
291
+ end
292
+ alias convert_ol convert_ul
293
+ alias convert_dl convert_ul
294
+
295
+ # Convert list element
296
+ #
297
+ # @param [Kramdown::Element] el
298
+ # the `kd:li` element
299
+ # @param [Hash] opts
300
+ # the element options
301
+ #
302
+ # @api private
303
+ def convert_li(el, opts)
304
+ index = opts[:index] + 1
305
+ indent = SPACE * @current_indent
306
+ prefix_type = opts[:parent].type == :ol ? "#{index}." : @symbols[:bullet]
307
+ prefix = @pastel.decorate(prefix_type, *@theme[:list]) + SPACE
308
+ opts[:strip] = true
309
+
310
+ content = inner(el, opts)
311
+
312
+ indent + prefix + content.join
313
+ end
314
+
315
+ # Convert dt element
316
+ #
317
+ # @param [Kramdown::Element] el
318
+ # the `kd:dt` element
319
+ # @param [Hash] opts
320
+ # the element options
321
+ #
322
+ # @api private
323
+ def convert_dt(el, opts)
324
+ indent = SPACE * @current_indent
325
+ content = inner(el, opts)
326
+ indent + content.join + NEWLINE
327
+ end
328
+
329
+ # Convert dd element
330
+ #
331
+ # @param [Kramdown::Element] el
332
+ # the `kd:dd` element
333
+ # @param [Hash] opts
334
+ # the element options
335
+ #
336
+ # @api private
337
+ def convert_dd(el, opts)
338
+ result = []
339
+ @current_indent += @indent unless opts[:parent].type == :root
340
+ content = inner(el, opts)
341
+ @current_indent -= @indent unless opts[:parent].type == :root
342
+ result << content.join
343
+ result << NEWLINE if opts[:next] && opts[:next].type == :dt
344
+ result
345
+ end
346
+
347
+ # Convert table element
348
+ #
349
+ # @param [Kramdown::Element] el
350
+ # the `kd:table` element
351
+ # @param [Hash] opts
352
+ # the element options
353
+ #
354
+ # @api private
355
+ def convert_table(el, opts)
356
+ @row = 0
357
+ @column = 0
358
+ opts[:alignment] = el.options[:alignment]
359
+ opts[:table_data] = extract_table_data(el, opts)
360
+ opts[:column_widths] = distribute_widths(max_widths(opts[:table_data]))
361
+ opts[:row_heights] = max_row_heights(opts[:table_data], opts[:column_widths])
362
+
363
+ inner(el, opts).join
364
+ end
365
+
366
+ # Extract table data
367
+ #
368
+ # @param [Kramdown::Element] el
369
+ # the `kd:table` element
370
+ #
371
+ # @api private
372
+ def extract_table_data(el, opts)
373
+ el.children.each_with_object([]) do |container, data|
374
+ container.children.each do |row|
375
+ data_row = []
376
+ row.children.each do |cell|
377
+ data_row << inner(cell, opts)
378
+ end
379
+ data << data_row
380
+ end
381
+ end
382
+ end
383
+
384
+ # Distribute column widths inside total width
385
+ #
386
+ # @return [Array<Integer>]
387
+ #
388
+ # @api private
389
+ def distribute_widths(widths)
390
+ indent = SPACE * @current_indent
391
+ total_width = widths.reduce(&:+)
392
+ screen_width = @width - (indent.length + 1) * 2 - (widths.size + 1)
393
+ return widths if total_width <= screen_width
394
+
395
+ extra_width = total_width - screen_width
396
+
397
+ widths.map do |w|
398
+ ratio = w / total_width.to_f
399
+ w - (extra_width * ratio).floor
400
+ end
401
+ end
402
+
403
+ # Calculate maximum widths for each column
404
+ #
405
+ # @return [Array<Integer>]
406
+ #
407
+ # @api private
408
+ def max_widths(table_data)
409
+ table_data.first.each_with_index.reduce([]) do |acc, (*, col)|
410
+ acc << max_width(table_data, col)
411
+ end
412
+ end
413
+
414
+ # Calculate maximum cell width for a given column
415
+ #
416
+ # @return [Integer]
417
+ #
418
+ # @api private
419
+ def max_width(table_data, col)
420
+ table_data.map do |row|
421
+ Strings.sanitize(row[col].join).lines.map(&:length).max || 0
422
+ end.max
423
+ end
424
+
425
+ # Calculate maximum heights for each row
426
+ #
427
+ # @return [Array<Integer>]
428
+ #
429
+ # @api private
430
+ def max_row_heights(table_data, column_widths)
431
+ table_data.reduce([]) do |acc, row|
432
+ acc << max_row_height(row, column_widths)
433
+ end
434
+ end
435
+
436
+ # Calculate maximum cell height for a given row
437
+ #
438
+ # @return [Integer]
439
+ #
440
+ # @api private
441
+ def max_row_height(row, column_widths)
442
+ row.map.with_index do |column, col_index|
443
+ Strings.wrap(column.join, column_widths[col_index]).lines.size
444
+ end.max
445
+ end
446
+
447
+ # Convert thead element
448
+ #
449
+ # @param [Kramdown::Element] el
450
+ # the `kd:thead` element
451
+ # @param [Hash] opts
452
+ # the element options
453
+ #
454
+ # @api private
455
+ def convert_thead(el, opts)
456
+ indent = SPACE * @current_indent
457
+ result = []
458
+
459
+ result << indent
460
+ result << border(opts[:column_widths], :top)
461
+ result << NEWLINE
462
+
463
+ content = inner(el, opts)
464
+
465
+ result << content.join
466
+ result.join
467
+ end
468
+
469
+ # Render horizontal border line
470
+ #
471
+ # @param [Array<Integer>] column_widths
472
+ # the table column widths
473
+ # @param [Symbol] location
474
+ # location out of :top, :mid, :bottom
475
+ #
476
+ # @return [String]
477
+ #
478
+ # @api private
479
+ def border(column_widths, location)
480
+ result = []
481
+ result << @symbols[:"#{location}_left"]
482
+ column_widths.each.with_index do |width, i|
483
+ result << @symbols[:"#{location}_center"] if i != 0
484
+ result << (@symbols[:line] * (width + 2))
485
+ end
486
+ result << @symbols[:"#{location}_right"]
487
+ @pastel.decorate(result.join, *@theme[:table])
488
+ end
489
+
490
+ # Convert tbody element
491
+ #
492
+ # @param [Kramdown::Element] el
493
+ # the `kd:tbody` element
494
+ # @param [Hash] opts
495
+ # the element options
496
+ #
497
+ # @api private
498
+ def convert_tbody(el, opts)
499
+ indent = SPACE * @current_indent
500
+ result = []
501
+
502
+ result << indent
503
+ if opts[:prev] && opts[:prev].type == :thead
504
+ result << border(opts[:column_widths], :mid)
505
+ else
506
+ result << border(opts[:column_widths], :top)
507
+ end
508
+ result << "\n"
509
+
510
+ content = inner(el, opts)
511
+
512
+ result << content.join
513
+ result << indent
514
+ if opts[:next] && opts[:next].type == :tfoot
515
+ result << border(opts[:column_widths], :mid)
516
+ else
517
+ result << border(opts[:column_widths], :bottom)
518
+ end
519
+ result << NEWLINE
520
+ result.join
521
+ end
522
+
523
+ # Convert tfoot element
524
+ #
525
+ # @param [Kramdown::Element] el
526
+ # the `kd:tfoot` element
527
+ # @param [Hash] opts
528
+ # the element options
529
+ #
530
+ # @api private
531
+ def convert_tfoot(el, opts)
532
+ indent = SPACE * @current_indent
533
+
534
+ inner(el, opts).join + indent +
535
+ border(opts[:column_widths], :bottom) +
536
+ NEWLINE
537
+ end
538
+
539
+ # Convert td element
540
+ #
541
+ # @param [Kramdown::Element] el
542
+ # the `kd:td` element
543
+ # @param [Hash] opts
544
+ # the element options
545
+ #
546
+ # @api private
547
+ def convert_tr(el, opts)
548
+ indent = SPACE * @current_indent
549
+ result = []
550
+
551
+ if opts[:prev] && opts[:prev].type == :tr
552
+ result << indent
553
+ result << border(opts[:column_widths], :mid)
554
+ result << NEWLINE
555
+ end
556
+
557
+ content = inner(el, opts)
558
+
559
+ columns = content.count
560
+
561
+ row = content.each_with_index.reduce([]) do |acc, (cell, i)|
562
+ if cell.size > 1 # multiline
563
+ cell.each_with_index do |c, j| # zip columns
564
+ acc[j] = [] if acc[j].nil?
565
+ acc[j] << c.chomp
566
+ acc[j] << "\n" if i == (columns - 1)
567
+ end
568
+ else
569
+ acc << cell
570
+ acc << "\n" if i == (columns - 1)
571
+ end
572
+ acc
573
+ end.join
574
+
575
+ result << row
576
+ @row += 1
577
+ result.join
578
+ end
579
+
580
+ # Convert td element
581
+ #
582
+ # @param [Kramdown::Element] el
583
+ # the `kd:td` element
584
+ # @param [Hash] opts
585
+ # the element options
586
+ #
587
+ # @api private
588
+ def convert_td(el, opts)
589
+ indent = SPACE * @current_indent
590
+ pipe_char = @symbols[:pipe]
591
+ pipe = @pastel.decorate(pipe_char, *@theme[:table])
592
+ suffix = " #{pipe} "
593
+
594
+ cell_content = inner(el, opts)
595
+ cell_width = opts[:column_widths][@column]
596
+ cell_height = opts[:row_heights][@row]
597
+ alignment = opts[:alignment][@column]
598
+ align_opts = alignment == :default ? {} : { direction: alignment }
599
+
600
+ wrapped = Strings.wrap(cell_content.join, cell_width)
601
+ aligned = Strings.align(wrapped, cell_width, **align_opts)
602
+ padded = if aligned.lines.size < cell_height
603
+ Strings.pad(aligned, [0, 0, cell_height - aligned.lines.size, 0])
604
+ else
605
+ aligned.dup
606
+ end
607
+
608
+ content = padded.lines.map do |line|
609
+ # add pipe to first column
610
+ (@column.zero? ? "#{indent}#{pipe} " : "") +
611
+ (line.end_with?("\n") ? line.insert(-2, suffix) : line << suffix)
612
+ end
613
+ @column = (@column + 1) % opts[:column_widths].size
614
+ content
615
+ end
616
+
617
+ def convert_br(el, opts)
618
+ NEWLINE
619
+ end
620
+
621
+ # Convert hr element
622
+ #
623
+ # @param [Kramdown::Element] el
624
+ # the `kd:hr` element
625
+ # @param [Hash] opts
626
+ # the element options
627
+ #
628
+ # @api private
629
+ def convert_hr(el, opts)
630
+ width = @width - @symbols[:diamond].length * 2
631
+ line = @symbols[:diamond] + @symbols[:line] * width + @symbols[:diamond]
632
+ @pastel.decorate(line, *@theme[:hr]) + NEWLINE
633
+ end
634
+
635
+ # Convert a element
636
+ #
637
+ # @param [Kramdown::Element] el
638
+ # the `kd:a` element
639
+ # @param [Hash] opts
640
+ # the element options
641
+ #
642
+ # @api private
643
+ def convert_a(el, opts)
644
+ result = []
645
+
646
+ if URI.parse(el.attr["href"]).class == URI::MailTo
647
+ el.attr["href"] = URI.parse(el.attr["href"]).to
648
+ end
649
+
650
+ if el.children.size == 1 && el.children[0].type == :text &&
651
+ el.children[0].value == el.attr["href"]
652
+
653
+ if !el.attr["title"].nil? && !el.attr["title"].strip.empty?
654
+ result << "(#{el.attr["title"]}) "
655
+ end
656
+ result << @pastel.decorate(el.attr["href"], *@theme[:link])
657
+
658
+ elsif el.children.size > 0 &&
659
+ (el.children[0].type != :text || !el.children[0].value.strip.empty?)
660
+
661
+ content = inner(el, opts)
662
+
663
+ result << content.join
664
+ result << " #{@symbols[:arrow]} "
665
+ if el.attr["title"]
666
+ result << "(#{el.attr["title"]}) "
667
+ end
668
+ result << @pastel.decorate(el.attr["href"], *@theme[:link])
669
+ end
670
+ result
671
+ end
672
+
673
+ # Convert math element
674
+ #
675
+ # @param [Kramdown::Element] el
676
+ # the `kd:math` element
677
+ # @param [Hash] opts
678
+ # the element options
679
+ #
680
+ # @api private
681
+ def convert_math(el, opts)
682
+ if el.options[:category] == :block
683
+ convert_codeblock(el, opts) + NEWLINE
684
+ else
685
+ convert_codespan(el, opts)
686
+ end
687
+ end
688
+
689
+ # Convert abbreviation element
690
+ #
691
+ # @param [Kramdown::Element] el
692
+ # the `kd:abbreviation` element
693
+ # @param [Hash] opts
694
+ # the element options
695
+ #
696
+ # @api private
697
+ def convert_abbreviation(el, opts)
698
+ title = @root.options[:abbrev_defs][el.value]
699
+ if title.to_s.empty?
700
+ el.value
701
+ else
702
+ "#{el.value}(#{title})"
703
+ end
704
+ end
705
+
706
+ def convert_typographic_sym(el, opts)
707
+ @symbols[el.value]
708
+ end
709
+
710
+ def convert_entity(el, opts)
711
+ unicode_char(el.value.code_point)
712
+ end
713
+
714
+ # Convert codepoint to UTF-8 representation
715
+ def unicode_char(codepoint)
716
+ [codepoint].pack("U*")
717
+ end
718
+
719
+ # Convert image element
720
+ #
721
+ # @param [Kramdown::Element] element
722
+ # the `kd:footnote` element
723
+ # @param [Hash] opts
724
+ # the element options
725
+ #
726
+ # @api private
727
+ def convert_footnote(el, opts)
728
+ name = el.options[:name]
729
+ if footnote = @footnotes[name]
730
+ number = footnote.last
731
+ else
732
+ number = @footnote_no
733
+ @footnote_no += 1
734
+ @footnotes[name] = [el.value, number]
735
+ end
736
+
737
+ content = "#{@symbols[:bracket_left]}#{number}#{@symbols[:bracket_right]}"
738
+ @pastel.decorate(content, *@theme[:note])
739
+ end
740
+
741
+ def convert_raw(*)
742
+ warning("Raw content is not supported")
743
+ end
744
+
745
+ # Convert image element
746
+ #
747
+ # @param [Kramdown::Element] element
748
+ # the `kd:img` element
749
+ # @param [Hash] opts
750
+ # the element options
751
+ #
752
+ # @api private
753
+ def convert_img(el, opts)
754
+ src = el.attr["src"]
755
+ alt = el.attr["alt"]
756
+ link = [@symbols[:paren_left]]
757
+ unless alt.to_s.empty?
758
+ link << "#{alt} #{@symbols[:ndash]} "
759
+ end
760
+ link << "#{src}#{@symbols[:paren_right]}"
761
+ @pastel.decorate(link.join, *@theme[:image])
762
+ end
763
+
764
+ # Convert html element
765
+ #
766
+ # @param [Kramdown::Element] element
767
+ # the `kd:html_element` element
768
+ # @param [Hash] opts
769
+ # the element options
770
+ #
771
+ # @api private
772
+ def convert_html_element(el, opts)
773
+ if el.value == "div"
774
+ inner(el, opts)
775
+ elsif %w[i em].include?(el.value)
776
+ convert_em(el, opts)
777
+ elsif %w[b strong].include?(el.value)
778
+ convert_strong(el, opts)
779
+ elsif el.value == "img"
780
+ convert_img(el, opts)
781
+ elsif el.value == "a"
782
+ convert_a(el, opts)
783
+ elsif el.value == "del"
784
+ inner(el, opts).join.chars.to_a.map do |char|
785
+ char + @symbols[:delete]
786
+ end
787
+ elsif el.value == "br"
788
+ NEWLINE
789
+ elsif !el.children.empty?
790
+ inner(el, opts)
791
+ else
792
+ warning("HTML element '#{el.value.inspect}' not supported")
793
+ ""
794
+ end
795
+ end
796
+
797
+ # Convert xml comment element
798
+ #
799
+ # @param [Kramdown::Element] element
800
+ # the `kd:xml_comment` element
801
+ # @param [Hash] opts
802
+ # the element options
803
+ #
804
+ # @api private
805
+ def convert_xml_comment(el, opts)
806
+ block = el.options[:category] == :block
807
+ indent = SPACE * @current_indent
808
+ content = el.value
809
+ content.gsub!(/^<!-{2,}\s*/, "") if content.start_with?("<!--")
810
+ content.gsub!(/-{2,}>$/, "") if content.end_with?("-->")
811
+ result = content.lines.map.with_index do |line, i|
812
+ (i.zero? && !block ? "" : indent) +
813
+ @pastel.decorate("#{@symbols[:hash]} " + line.chomp,
814
+ *@theme[:comment])
815
+ end.join(NEWLINE)
816
+ block ? result + NEWLINE : result
817
+ end
818
+ alias convert_comment convert_xml_comment
819
+ end # Parser
820
+ end # Markdown
821
+ end # TTY