1##
2# Outputs RDoc markup as RDoc markup! (mostly)
3
4class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter
5
6  ##
7  # Current indent amount for output in characters
8
9  attr_accessor :indent
10
11  ##
12  # Output width in characters
13
14  attr_accessor :width
15
16  ##
17  # Stack of current list indexes for alphabetic and numeric lists
18
19  attr_reader :list_index
20
21  ##
22  # Stack of list types
23
24  attr_reader :list_type
25
26  ##
27  # Stack of list widths for indentation
28
29  attr_reader :list_width
30
31  ##
32  # Prefix for the next list item.  See #use_prefix
33
34  attr_reader :prefix
35
36  ##
37  # Output accumulator
38
39  attr_reader :res
40
41  ##
42  # Creates a new formatter that will output (mostly) \RDoc markup
43
44  def initialize markup = nil
45    super nil, markup
46
47    @markup.add_special(/\\\S/, :SUPPRESSED_CROSSREF)
48    @width = 78
49    init_tags
50
51    @headings = {}
52    @headings.default = []
53
54    @headings[1] = ['= ',      '']
55    @headings[2] = ['== ',     '']
56    @headings[3] = ['=== ',    '']
57    @headings[4] = ['==== ',   '']
58    @headings[5] = ['===== ',  '']
59    @headings[6] = ['====== ', '']
60
61    @hard_break = "\n"
62  end
63
64  ##
65  # Maps attributes to HTML sequences
66
67  def init_tags
68    add_tag :BOLD, "<b>", "</b>"
69    add_tag :TT,   "<tt>", "</tt>"
70    add_tag :EM,   "<em>", "</em>"
71  end
72
73  ##
74  # Adds +blank_line+ to the output
75
76  def accept_blank_line blank_line
77    @res << "\n"
78  end
79
80  ##
81  # Adds +paragraph+ to the output
82
83  def accept_block_quote block_quote
84    @indent += 2
85
86    block_quote.parts.each do |part|
87      @prefix = '> '
88
89      part.accept self
90    end
91
92    @indent -= 2
93  end
94
95  ##
96  # Adds +heading+ to the output
97
98  def accept_heading heading
99    use_prefix or @res << ' ' * @indent
100    @res << @headings[heading.level][0]
101    @res << attributes(heading.text)
102    @res << @headings[heading.level][1]
103    @res << "\n"
104  end
105
106  ##
107  # Finishes consumption of +list+
108
109  def accept_list_end list
110    @list_index.pop
111    @list_type.pop
112    @list_width.pop
113  end
114
115  ##
116  # Finishes consumption of +list_item+
117
118  def accept_list_item_end list_item
119    width = case @list_type.last
120            when :BULLET then
121              2
122            when :NOTE, :LABEL then
123              if @prefix then
124                @res << @prefix.strip
125                @prefix = nil
126              end
127
128              @res << "\n"
129              2
130            else
131              bullet = @list_index.last.to_s
132              @list_index[-1] = @list_index.last.succ
133              bullet.length + 2
134            end
135
136    @indent -= width
137  end
138
139  ##
140  # Prepares the visitor for consuming +list_item+
141
142  def accept_list_item_start list_item
143    type = @list_type.last
144
145    case type
146    when :NOTE, :LABEL then
147      bullets = Array(list_item.label).map do |label|
148        attributes(label).strip
149      end.join "\n"
150
151      bullets << ":\n" unless bullets.empty?
152
153      @prefix = ' ' * @indent
154      @indent += 2
155      @prefix << bullets + (' ' * @indent)
156    else
157      bullet = type == :BULLET ? '*' :  @list_index.last.to_s + '.'
158      @prefix = (' ' * @indent) + bullet.ljust(bullet.length + 1)
159      width = bullet.length + 1
160      @indent += width
161    end
162  end
163
164  ##
165  # Prepares the visitor for consuming +list+
166
167  def accept_list_start list
168    case list.type
169    when :BULLET then
170      @list_index << nil
171      @list_width << 1
172    when :LABEL, :NOTE then
173      @list_index << nil
174      @list_width << 2
175    when :LALPHA then
176      @list_index << 'a'
177      @list_width << list.items.length.to_s.length
178    when :NUMBER then
179      @list_index << 1
180      @list_width << list.items.length.to_s.length
181    when :UALPHA then
182      @list_index << 'A'
183      @list_width << list.items.length.to_s.length
184    else
185      raise RDoc::Error, "invalid list type #{list.type}"
186    end
187
188    @list_type << list.type
189  end
190
191  ##
192  # Adds +paragraph+ to the output
193
194  def accept_paragraph paragraph
195    text = paragraph.text @hard_break
196    wrap attributes text
197  end
198
199  ##
200  # Adds +paragraph+ to the output
201
202  def accept_indented_paragraph paragraph
203    @indent += paragraph.indent
204    text = paragraph.text @hard_break
205    wrap attributes text
206    @indent -= paragraph.indent
207  end
208
209  ##
210  # Adds +raw+ to the output
211
212  def accept_raw raw
213    @res << raw.parts.join("\n")
214  end
215
216  ##
217  # Adds +rule+ to the output
218
219  def accept_rule rule
220    use_prefix or @res << ' ' * @indent
221    @res << '-' * (@width - @indent)
222    @res << "\n"
223  end
224
225  ##
226  # Outputs +verbatim+ indented 2 columns
227
228  def accept_verbatim verbatim
229    indent = ' ' * (@indent + 2)
230
231    verbatim.parts.each do |part|
232      @res << indent unless part == "\n"
233      @res << part
234    end
235
236    @res << "\n" unless @res =~ /\n\z/
237  end
238
239  ##
240  # Applies attribute-specific markup to +text+ using RDoc::AttributeManager
241
242  def attributes text
243    flow = @am.flow text.dup
244    convert_flow flow
245  end
246
247  ##
248  # Returns the generated output
249
250  def end_accepting
251    @res.join
252  end
253
254  ##
255  # Removes preceding \\ from the suppressed crossref +special+
256
257  def handle_special_SUPPRESSED_CROSSREF special
258    text = special.text
259    text = text.sub('\\', '') unless in_tt?
260    text
261  end
262
263  ##
264  # Adds a newline to the output
265
266  def handle_special_HARD_BREAK special
267    "\n"
268  end
269
270  ##
271  # Prepares the visitor for text generation
272
273  def start_accepting
274    @res = [""]
275    @indent = 0
276    @prefix = nil
277
278    @list_index = []
279    @list_type  = []
280    @list_width = []
281  end
282
283  ##
284  # Adds the stored #prefix to the output and clears it.  Lists generate a
285  # prefix for later consumption.
286
287  def use_prefix
288    prefix, @prefix = @prefix, nil
289    @res << prefix if prefix
290
291    prefix
292  end
293
294  ##
295  # Wraps +text+ to #width
296
297  def wrap text
298    return unless text && !text.empty?
299
300    text_len = @width - @indent
301
302    text_len = 20 if text_len < 20
303
304    re = /^(.{0,#{text_len}})[ \n]/
305    next_prefix = ' ' * @indent
306
307    prefix = @prefix || next_prefix
308    @prefix = nil
309
310    @res << prefix
311
312    while text.length > text_len
313      if text =~ re then
314        @res << $1
315        text.slice!(0, $&.length)
316      else
317        @res << text.slice!(0, text_len)
318      end
319
320      @res << "\n" << next_prefix
321    end
322
323    if text.empty? then
324      @res.pop
325      @res.pop
326    else
327      @res << text
328      @res << "\n"
329    end
330  end
331
332end
333
334