tty-markdown 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -2
- data/README.md +117 -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 +1 -1
- metadata +38 -65
- data/Rakefile +0 -8
- 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/man.rb +0 -6
- data/examples/marked.rb +0 -6
- data/lib/tty/markdown/parser.rb +0 -482
- data/spec/spec_helper.rb +0 -31
- data/spec/unit/parse/abbrev_spec.rb +0 -27
- data/spec/unit/parse/blockquote_spec.rb +0 -77
- data/spec/unit/parse/codeblock_spec.rb +0 -130
- data/spec/unit/parse/comment_spec.rb +0 -19
- data/spec/unit/parse/emphasis_spec.rb +0 -35
- data/spec/unit/parse/entity_spec.rb +0 -11
- data/spec/unit/parse/header_spec.rb +0 -35
- data/spec/unit/parse/hr_spec.rb +0 -25
- data/spec/unit/parse/link_spec.rb +0 -25
- data/spec/unit/parse/list_spec.rb +0 -103
- data/spec/unit/parse/math_spec.rb +0 -37
- data/spec/unit/parse/paragraph_spec.rb +0 -38
- data/spec/unit/parse/table_spec.rb +0 -164
- data/spec/unit/parse/typography_spec.rb +0 -20
- data/tasks/console.rake +0 -11
- data/tasks/coverage.rake +0 -11
- data/tasks/spec.rake +0 -29
@@ -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
|