1 | module RI
|
---|
2 | class TextFormatter
|
---|
3 |
|
---|
4 | attr_reader :indent
|
---|
5 |
|
---|
6 | def initialize(options, indent)
|
---|
7 | @options = options
|
---|
8 | @width = options.width
|
---|
9 | @indent = indent
|
---|
10 | end
|
---|
11 |
|
---|
12 |
|
---|
13 | ######################################################################
|
---|
14 |
|
---|
15 | def draw_line(label=nil)
|
---|
16 | len = @width
|
---|
17 | len -= (label.size+1) if label
|
---|
18 | print "-"*len
|
---|
19 | if label
|
---|
20 | print(" ")
|
---|
21 | bold_print(label)
|
---|
22 | end
|
---|
23 | puts
|
---|
24 | end
|
---|
25 |
|
---|
26 | ######################################################################
|
---|
27 |
|
---|
28 | def wrap(txt, prefix=@indent, linelen=@width)
|
---|
29 | return unless txt && !txt.empty?
|
---|
30 | work = conv_markup(txt)
|
---|
31 | textLen = linelen - prefix.length
|
---|
32 | patt = Regexp.new("^(.{0,#{textLen}})[ \n]")
|
---|
33 | next_prefix = prefix.tr("^ ", " ")
|
---|
34 |
|
---|
35 | res = []
|
---|
36 |
|
---|
37 | while work.length > textLen
|
---|
38 | if work =~ patt
|
---|
39 | res << $1
|
---|
40 | work.slice!(0, $&.length)
|
---|
41 | else
|
---|
42 | res << work.slice!(0, textLen)
|
---|
43 | end
|
---|
44 | end
|
---|
45 | res << work if work.length.nonzero?
|
---|
46 | puts(prefix + res.join("\n" + next_prefix))
|
---|
47 | end
|
---|
48 |
|
---|
49 | ######################################################################
|
---|
50 |
|
---|
51 | def blankline
|
---|
52 | puts
|
---|
53 | end
|
---|
54 |
|
---|
55 | ######################################################################
|
---|
56 |
|
---|
57 | # called when we want to ensure a nbew 'wrap' starts on a newline
|
---|
58 | # Only needed for HtmlFormatter, because the rest do their
|
---|
59 | # own line breaking
|
---|
60 |
|
---|
61 | def break_to_newline
|
---|
62 | end
|
---|
63 |
|
---|
64 | ######################################################################
|
---|
65 |
|
---|
66 | def bold_print(txt)
|
---|
67 | print txt
|
---|
68 | end
|
---|
69 |
|
---|
70 | ######################################################################
|
---|
71 |
|
---|
72 | def raw_print_line(txt)
|
---|
73 | puts txt
|
---|
74 | end
|
---|
75 |
|
---|
76 | ######################################################################
|
---|
77 |
|
---|
78 | # convert HTML entities back to ASCII
|
---|
79 | def conv_html(txt)
|
---|
80 | txt.
|
---|
81 | gsub(/>/, '>').
|
---|
82 | gsub(/</, '<').
|
---|
83 | gsub(/"/, '"').
|
---|
84 | gsub(/&/, '&')
|
---|
85 |
|
---|
86 | end
|
---|
87 |
|
---|
88 | # convert markup into display form
|
---|
89 | def conv_markup(txt)
|
---|
90 | txt.
|
---|
91 | gsub(%r{<tt>(.*?)</tt>}) { "+#$1+" } .
|
---|
92 | gsub(%r{<code>(.*?)</code>}) { "+#$1+" } .
|
---|
93 | gsub(%r{<b>(.*?)</b>}) { "*#$1*" } .
|
---|
94 | gsub(%r{<em>(.*?)</em>}) { "_#$1_" }
|
---|
95 | end
|
---|
96 |
|
---|
97 | ######################################################################
|
---|
98 |
|
---|
99 | def display_list(list)
|
---|
100 | case list.type
|
---|
101 |
|
---|
102 | when SM::ListBase::BULLET
|
---|
103 | prefixer = proc { |ignored| @indent + "* " }
|
---|
104 |
|
---|
105 | when SM::ListBase::NUMBER,
|
---|
106 | SM::ListBase::UPPERALPHA,
|
---|
107 | SM::ListBase::LOWERALPHA
|
---|
108 |
|
---|
109 | start = case list.type
|
---|
110 | when SM::ListBase::NUMBER then 1
|
---|
111 | when SM::ListBase::UPPERALPHA then 'A'
|
---|
112 | when SM::ListBase::LOWERALPHA then 'a'
|
---|
113 | end
|
---|
114 | prefixer = proc do |ignored|
|
---|
115 | res = @indent + "#{start}.".ljust(4)
|
---|
116 | start = start.succ
|
---|
117 | res
|
---|
118 | end
|
---|
119 |
|
---|
120 | when SM::ListBase::LABELED
|
---|
121 | prefixer = proc do |li|
|
---|
122 | li.label
|
---|
123 | end
|
---|
124 |
|
---|
125 | when SM::ListBase::NOTE
|
---|
126 | longest = 0
|
---|
127 | list.contents.each do |item|
|
---|
128 | if item.kind_of?(SM::Flow::LI) && item.label.length > longest
|
---|
129 | longest = item.label.length
|
---|
130 | end
|
---|
131 | end
|
---|
132 |
|
---|
133 | prefixer = proc do |li|
|
---|
134 | @indent + li.label.ljust(longest+1)
|
---|
135 | end
|
---|
136 |
|
---|
137 | else
|
---|
138 | fail "unknown list type"
|
---|
139 |
|
---|
140 | end
|
---|
141 |
|
---|
142 | list.contents.each do |item|
|
---|
143 | if item.kind_of? SM::Flow::LI
|
---|
144 | prefix = prefixer.call(item)
|
---|
145 | display_flow_item(item, prefix)
|
---|
146 | else
|
---|
147 | display_flow_item(item)
|
---|
148 | end
|
---|
149 | end
|
---|
150 | end
|
---|
151 |
|
---|
152 | ######################################################################
|
---|
153 |
|
---|
154 | def display_flow_item(item, prefix=@indent)
|
---|
155 | case item
|
---|
156 | when SM::Flow::P, SM::Flow::LI
|
---|
157 | wrap(conv_html(item.body), prefix)
|
---|
158 | blankline
|
---|
159 |
|
---|
160 | when SM::Flow::LIST
|
---|
161 | display_list(item)
|
---|
162 |
|
---|
163 | when SM::Flow::VERB
|
---|
164 | display_verbatim_flow_item(item, @indent)
|
---|
165 |
|
---|
166 | when SM::Flow::H
|
---|
167 | display_heading(conv_html(item.text), item.level, @indent)
|
---|
168 |
|
---|
169 | when SM::Flow::RULE
|
---|
170 | draw_line
|
---|
171 |
|
---|
172 | else
|
---|
173 | fail "Unknown flow element: #{item.class}"
|
---|
174 | end
|
---|
175 | end
|
---|
176 |
|
---|
177 | ######################################################################
|
---|
178 |
|
---|
179 | def display_verbatim_flow_item(item, prefix=@indent)
|
---|
180 | item.body.split(/\n/).each do |line|
|
---|
181 | print @indent, conv_html(line), "\n"
|
---|
182 | end
|
---|
183 | blankline
|
---|
184 | end
|
---|
185 |
|
---|
186 | ######################################################################
|
---|
187 |
|
---|
188 | def display_heading(text, level, indent)
|
---|
189 | text = strip_attributes(text)
|
---|
190 | case level
|
---|
191 | when 1
|
---|
192 | ul = "=" * text.length
|
---|
193 | puts
|
---|
194 | puts text.upcase
|
---|
195 | puts ul
|
---|
196 | # puts
|
---|
197 |
|
---|
198 | when 2
|
---|
199 | ul = "-" * text.length
|
---|
200 | puts
|
---|
201 | puts text
|
---|
202 | puts ul
|
---|
203 | # puts
|
---|
204 | else
|
---|
205 | print indent, text, "\n"
|
---|
206 | end
|
---|
207 | end
|
---|
208 |
|
---|
209 |
|
---|
210 | def display_flow(flow)
|
---|
211 | flow.each do |f|
|
---|
212 | display_flow_item(f)
|
---|
213 | end
|
---|
214 | end
|
---|
215 |
|
---|
216 | def strip_attributes(txt)
|
---|
217 | tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
|
---|
218 | text = []
|
---|
219 | attributes = 0
|
---|
220 | tokens.each do |tok|
|
---|
221 | case tok
|
---|
222 | when %r{^</(\w+)>$}, %r{^<(\w+)>$}
|
---|
223 | ;
|
---|
224 | else
|
---|
225 | text << tok
|
---|
226 | end
|
---|
227 | end
|
---|
228 | text.join
|
---|
229 | end
|
---|
230 |
|
---|
231 |
|
---|
232 | end
|
---|
233 |
|
---|
234 |
|
---|
235 | ######################################################################
|
---|
236 | # Handle text with attributes. We're a base class: there are
|
---|
237 | # different presentation classes (one, for example, uses overstrikes
|
---|
238 | # to handle bold and underlining, while another using ANSI escape
|
---|
239 | # sequences
|
---|
240 |
|
---|
241 | class AttributeFormatter < TextFormatter
|
---|
242 |
|
---|
243 | BOLD = 1
|
---|
244 | ITALIC = 2
|
---|
245 | CODE = 4
|
---|
246 |
|
---|
247 | ATTR_MAP = {
|
---|
248 | "b" => BOLD,
|
---|
249 | "code" => CODE,
|
---|
250 | "em" => ITALIC,
|
---|
251 | "i" => ITALIC,
|
---|
252 | "tt" => CODE
|
---|
253 | }
|
---|
254 |
|
---|
255 | # TODO: struct?
|
---|
256 | class AttrChar
|
---|
257 | attr_reader :char
|
---|
258 | attr_reader :attr
|
---|
259 |
|
---|
260 | def initialize(char, attr)
|
---|
261 | @char = char
|
---|
262 | @attr = attr
|
---|
263 | end
|
---|
264 | end
|
---|
265 |
|
---|
266 |
|
---|
267 | class AttributeString
|
---|
268 | attr_reader :txt
|
---|
269 |
|
---|
270 | def initialize
|
---|
271 | @txt = []
|
---|
272 | @optr = 0
|
---|
273 | end
|
---|
274 |
|
---|
275 | def <<(char)
|
---|
276 | @txt << char
|
---|
277 | end
|
---|
278 |
|
---|
279 | def empty?
|
---|
280 | @optr >= @txt.length
|
---|
281 | end
|
---|
282 |
|
---|
283 | # accept non space, then all following spaces
|
---|
284 | def next_word
|
---|
285 | start = @optr
|
---|
286 | len = @txt.length
|
---|
287 |
|
---|
288 | while @optr < len && @txt[@optr].char != " "
|
---|
289 | @optr += 1
|
---|
290 | end
|
---|
291 |
|
---|
292 | while @optr < len && @txt[@optr].char == " "
|
---|
293 | @optr += 1
|
---|
294 | end
|
---|
295 |
|
---|
296 | @txt[start...@optr]
|
---|
297 | end
|
---|
298 | end
|
---|
299 |
|
---|
300 | ######################################################################
|
---|
301 | # overrides base class. Looks for <tt>...</tt> etc sequences
|
---|
302 | # and generates an array of AttrChars. This array is then used
|
---|
303 | # as the basis for the split
|
---|
304 |
|
---|
305 | def wrap(txt, prefix=@indent, linelen=@width)
|
---|
306 | return unless txt && !txt.empty?
|
---|
307 |
|
---|
308 | txt = add_attributes_to(txt)
|
---|
309 | next_prefix = prefix.tr("^ ", " ")
|
---|
310 | linelen -= prefix.size
|
---|
311 |
|
---|
312 | line = []
|
---|
313 |
|
---|
314 | until txt.empty?
|
---|
315 | word = txt.next_word
|
---|
316 | if word.size + line.size > linelen
|
---|
317 | write_attribute_text(prefix, line)
|
---|
318 | prefix = next_prefix
|
---|
319 | line = []
|
---|
320 | end
|
---|
321 | line.concat(word)
|
---|
322 | end
|
---|
323 |
|
---|
324 | write_attribute_text(prefix, line) if line.length > 0
|
---|
325 | end
|
---|
326 |
|
---|
327 | protected
|
---|
328 |
|
---|
329 | # overridden in specific formatters
|
---|
330 |
|
---|
331 | def write_attribute_text(prefix, line)
|
---|
332 | print prefix
|
---|
333 | line.each do |achar|
|
---|
334 | print achar.char
|
---|
335 | end
|
---|
336 | puts
|
---|
337 | end
|
---|
338 |
|
---|
339 | # again, overridden
|
---|
340 |
|
---|
341 | def bold_print(txt)
|
---|
342 | print txt
|
---|
343 | end
|
---|
344 |
|
---|
345 | private
|
---|
346 |
|
---|
347 | def add_attributes_to(txt)
|
---|
348 | tokens = txt.split(%r{(</?(?:b|code|em|i|tt)>)})
|
---|
349 | text = AttributeString.new
|
---|
350 | attributes = 0
|
---|
351 | tokens.each do |tok|
|
---|
352 | case tok
|
---|
353 | when %r{^</(\w+)>$} then attributes &= ~(ATTR_MAP[$1]||0)
|
---|
354 | when %r{^<(\w+)>$} then attributes |= (ATTR_MAP[$1]||0)
|
---|
355 | else
|
---|
356 | tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)}
|
---|
357 | end
|
---|
358 | end
|
---|
359 | text
|
---|
360 | end
|
---|
361 |
|
---|
362 | end
|
---|
363 |
|
---|
364 |
|
---|
365 | ##################################################
|
---|
366 |
|
---|
367 | # This formatter generates overstrike-style formatting, which
|
---|
368 | # works with pagers such as man and less.
|
---|
369 |
|
---|
370 | class OverstrikeFormatter < AttributeFormatter
|
---|
371 |
|
---|
372 | BS = "\C-h"
|
---|
373 |
|
---|
374 | def write_attribute_text(prefix, line)
|
---|
375 | print prefix
|
---|
376 | line.each do |achar|
|
---|
377 | attr = achar.attr
|
---|
378 | if (attr & (ITALIC+CODE)) != 0
|
---|
379 | print "_", BS
|
---|
380 | end
|
---|
381 | if (attr & BOLD) != 0
|
---|
382 | print achar.char, BS
|
---|
383 | end
|
---|
384 | print achar.char
|
---|
385 | end
|
---|
386 | puts
|
---|
387 | end
|
---|
388 |
|
---|
389 | # draw a string in bold
|
---|
390 | def bold_print(text)
|
---|
391 | text.split(//).each do |ch|
|
---|
392 | print ch, BS, ch
|
---|
393 | end
|
---|
394 | end
|
---|
395 | end
|
---|
396 |
|
---|
397 | ##################################################
|
---|
398 |
|
---|
399 | # This formatter uses ANSI escape sequences
|
---|
400 | # to colorize stuff
|
---|
401 | # works with pages such as man and less.
|
---|
402 |
|
---|
403 | class AnsiFormatter < AttributeFormatter
|
---|
404 |
|
---|
405 | def initialize(*args)
|
---|
406 | print "\033[0m"
|
---|
407 | super
|
---|
408 | end
|
---|
409 |
|
---|
410 | def write_attribute_text(prefix, line)
|
---|
411 | print prefix
|
---|
412 | curr_attr = 0
|
---|
413 | line.each do |achar|
|
---|
414 | attr = achar.attr
|
---|
415 | if achar.attr != curr_attr
|
---|
416 | update_attributes(achar.attr)
|
---|
417 | curr_attr = achar.attr
|
---|
418 | end
|
---|
419 | print achar.char
|
---|
420 | end
|
---|
421 | update_attributes(0) unless curr_attr.zero?
|
---|
422 | puts
|
---|
423 | end
|
---|
424 |
|
---|
425 |
|
---|
426 | def bold_print(txt)
|
---|
427 | print "\033[1m#{txt}\033[m"
|
---|
428 | end
|
---|
429 |
|
---|
430 | HEADINGS = {
|
---|
431 | 1 => [ "\033[1;32m", "\033[m" ] ,
|
---|
432 | 2 => ["\033[4;32m", "\033[m" ],
|
---|
433 | 3 => ["\033[32m", "\033[m" ]
|
---|
434 | }
|
---|
435 |
|
---|
436 | def display_heading(text, level, indent)
|
---|
437 | level = 3 if level > 3
|
---|
438 | heading = HEADINGS[level]
|
---|
439 | print indent
|
---|
440 | print heading[0]
|
---|
441 | print strip_attributes(text)
|
---|
442 | puts heading[1]
|
---|
443 | end
|
---|
444 |
|
---|
445 | private
|
---|
446 |
|
---|
447 | ATTR_MAP = {
|
---|
448 | BOLD => "1",
|
---|
449 | ITALIC => "33",
|
---|
450 | CODE => "36"
|
---|
451 | }
|
---|
452 |
|
---|
453 | def update_attributes(attr)
|
---|
454 | str = "\033["
|
---|
455 | for quality in [ BOLD, ITALIC, CODE]
|
---|
456 | unless (attr & quality).zero?
|
---|
457 | str << ATTR_MAP[quality]
|
---|
458 | end
|
---|
459 | end
|
---|
460 | print str, "m"
|
---|
461 | end
|
---|
462 | end
|
---|
463 |
|
---|
464 | ##################################################
|
---|
465 |
|
---|
466 | # This formatter uses HTML.
|
---|
467 |
|
---|
468 | class HtmlFormatter < AttributeFormatter
|
---|
469 |
|
---|
470 | def initialize(*args)
|
---|
471 | super
|
---|
472 | end
|
---|
473 |
|
---|
474 | def write_attribute_text(prefix, line)
|
---|
475 | curr_attr = 0
|
---|
476 | line.each do |achar|
|
---|
477 | attr = achar.attr
|
---|
478 | if achar.attr != curr_attr
|
---|
479 | update_attributes(curr_attr, achar.attr)
|
---|
480 | curr_attr = achar.attr
|
---|
481 | end
|
---|
482 | print(escape(achar.char))
|
---|
483 | end
|
---|
484 | update_attributes(curr_attr, 0) unless curr_attr.zero?
|
---|
485 | end
|
---|
486 |
|
---|
487 | def draw_line(label=nil)
|
---|
488 | if label != nil
|
---|
489 | bold_print(label)
|
---|
490 | end
|
---|
491 | puts("<hr>")
|
---|
492 | end
|
---|
493 |
|
---|
494 | def bold_print(txt)
|
---|
495 | tag("b") { txt }
|
---|
496 | end
|
---|
497 |
|
---|
498 | def blankline()
|
---|
499 | puts("<p>")
|
---|
500 | end
|
---|
501 |
|
---|
502 | def break_to_newline
|
---|
503 | puts("<br>")
|
---|
504 | end
|
---|
505 |
|
---|
506 | def display_heading(text, level, indent)
|
---|
507 | level = 4 if level > 4
|
---|
508 | tag("h#{level}") { text }
|
---|
509 | puts
|
---|
510 | end
|
---|
511 |
|
---|
512 | ######################################################################
|
---|
513 |
|
---|
514 | def display_list(list)
|
---|
515 |
|
---|
516 | case list.type
|
---|
517 | when SM::ListBase::BULLET
|
---|
518 | list_type = "ul"
|
---|
519 | prefixer = proc { |ignored| "<li>" }
|
---|
520 |
|
---|
521 | when SM::ListBase::NUMBER,
|
---|
522 | SM::ListBase::UPPERALPHA,
|
---|
523 | SM::ListBase::LOWERALPHA
|
---|
524 | list_type = "ol"
|
---|
525 | prefixer = proc { |ignored| "<li>" }
|
---|
526 |
|
---|
527 | when SM::ListBase::LABELED
|
---|
528 | list_type = "dl"
|
---|
529 | prefixer = proc do |li|
|
---|
530 | "<dt><b>" + escape(li.label) + "</b><dd>"
|
---|
531 | end
|
---|
532 |
|
---|
533 | when SM::ListBase::NOTE
|
---|
534 | list_type = "table"
|
---|
535 | prefixer = proc do |li|
|
---|
536 | %{<tr valign="top"><td>#{li.label.gsub(/ /, ' ')}</td><td>}
|
---|
537 | end
|
---|
538 | else
|
---|
539 | fail "unknown list type"
|
---|
540 | end
|
---|
541 |
|
---|
542 | print "<#{list_type}>"
|
---|
543 | list.contents.each do |item|
|
---|
544 | if item.kind_of? SM::Flow::LI
|
---|
545 | prefix = prefixer.call(item)
|
---|
546 | print prefix
|
---|
547 | display_flow_item(item, prefix)
|
---|
548 | else
|
---|
549 | display_flow_item(item)
|
---|
550 | end
|
---|
551 | end
|
---|
552 | print "</#{list_type}>"
|
---|
553 | end
|
---|
554 |
|
---|
555 | def display_verbatim_flow_item(item, prefix=@indent)
|
---|
556 | print("<pre>")
|
---|
557 | puts item.body
|
---|
558 | puts("</pre>")
|
---|
559 | end
|
---|
560 |
|
---|
561 | private
|
---|
562 |
|
---|
563 | ATTR_MAP = {
|
---|
564 | BOLD => "b>",
|
---|
565 | ITALIC => "i>",
|
---|
566 | CODE => "tt>"
|
---|
567 | }
|
---|
568 |
|
---|
569 | def update_attributes(current, wanted)
|
---|
570 | str = ""
|
---|
571 | # first turn off unwanted ones
|
---|
572 | off = current & ~wanted
|
---|
573 | for quality in [ BOLD, ITALIC, CODE]
|
---|
574 | if (off & quality) > 0
|
---|
575 | str << "</" + ATTR_MAP[quality]
|
---|
576 | end
|
---|
577 | end
|
---|
578 |
|
---|
579 | # now turn on wanted
|
---|
580 | for quality in [ BOLD, ITALIC, CODE]
|
---|
581 | unless (wanted & quality).zero?
|
---|
582 | str << "<" << ATTR_MAP[quality]
|
---|
583 | end
|
---|
584 | end
|
---|
585 | print str
|
---|
586 | end
|
---|
587 |
|
---|
588 | def tag(code)
|
---|
589 | print("<#{code}>")
|
---|
590 | print(yield)
|
---|
591 | print("</#{code}>")
|
---|
592 | end
|
---|
593 |
|
---|
594 | def escape(str)
|
---|
595 | str.
|
---|
596 | gsub(/&/n, '&').
|
---|
597 | gsub(/\"/n, '"').
|
---|
598 | gsub(/>/n, '>').
|
---|
599 | gsub(/</n, '<')
|
---|
600 | end
|
---|
601 |
|
---|
602 | end
|
---|
603 |
|
---|
604 | ##################################################
|
---|
605 |
|
---|
606 | # This formatter reduces extra lines for a simpler output.
|
---|
607 | # It improves way output looks for tools like IRC bots.
|
---|
608 |
|
---|
609 | class SimpleFormatter < TextFormatter
|
---|
610 |
|
---|
611 | ######################################################################
|
---|
612 |
|
---|
613 | # No extra blank lines
|
---|
614 |
|
---|
615 | def blankline
|
---|
616 | end
|
---|
617 |
|
---|
618 | ######################################################################
|
---|
619 |
|
---|
620 | # Display labels only, no lines
|
---|
621 |
|
---|
622 | def draw_line(label=nil)
|
---|
623 | unless label.nil? then
|
---|
624 | bold_print(label)
|
---|
625 | puts
|
---|
626 | end
|
---|
627 | end
|
---|
628 |
|
---|
629 | ######################################################################
|
---|
630 |
|
---|
631 | # Place heading level indicators inline with heading.
|
---|
632 |
|
---|
633 | def display_heading(text, level, indent)
|
---|
634 | text = strip_attributes(text)
|
---|
635 | case level
|
---|
636 | when 1
|
---|
637 | puts "= " + text.upcase
|
---|
638 | when 2
|
---|
639 | puts "-- " + text
|
---|
640 | else
|
---|
641 | print indent, text, "\n"
|
---|
642 | end
|
---|
643 | end
|
---|
644 |
|
---|
645 | end
|
---|
646 |
|
---|
647 |
|
---|
648 | # Finally, fill in the list of known formatters
|
---|
649 |
|
---|
650 | class TextFormatter
|
---|
651 |
|
---|
652 | FORMATTERS = {
|
---|
653 | "ansi" => AnsiFormatter,
|
---|
654 | "bs" => OverstrikeFormatter,
|
---|
655 | "html" => HtmlFormatter,
|
---|
656 | "plain" => TextFormatter,
|
---|
657 | "simple" => SimpleFormatter,
|
---|
658 | }
|
---|
659 |
|
---|
660 | def TextFormatter.list
|
---|
661 | FORMATTERS.keys.sort.join(", ")
|
---|
662 | end
|
---|
663 |
|
---|
664 | def TextFormatter.for(name)
|
---|
665 | FORMATTERS[name.downcase]
|
---|
666 | end
|
---|
667 |
|
---|
668 | end
|
---|
669 |
|
---|
670 | end
|
---|
671 |
|
---|
672 |
|
---|