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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +64 -1
- data/README.md +121 -59
- data/lib/tty-markdown.rb +1 -1
- data/lib/tty/markdown.rb +127 -72
- data/lib/tty/markdown/converter.rb +821 -0
- data/lib/tty/markdown/kramdown_ext.rb +23 -0
- data/lib/tty/markdown/syntax_highlighter.rb +20 -16
- data/lib/tty/markdown/version.rb +3 -1
- metadata +44 -66
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.travis.yml +0 -23
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -16
- data/Rakefile +0 -8
- data/appveyor.yml +0 -23
- data/assets/headers.png +0 -0
- data/assets/hr.png +0 -0
- data/assets/link.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 +0 -14
- data/bin/setup +0 -8
- data/examples/example.md +0 -49
- data/examples/man.md +0 -17
- data/examples/man.rb +0 -6
- data/examples/marked.rb +0 -6
- data/lib/tty/markdown/parser.rb +0 -467
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
- data/tty-markdown.gemspec +0 -35
@@ -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
|