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