tty-markdown 0.6.0 → 0.7.1

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.
@@ -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