Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rdoc / markup / fragments.rb
blob1765861ad01d746f56a947ba0045f852b9cb6cb5
1 require 'rdoc/markup'
2 require 'rdoc/markup/lines'
4 class RDoc::Markup
6   ##
7   # A Fragment is a chunk of text, subclassed as a paragraph, a list
8   # entry, or verbatim text.
10   class Fragment
11     attr_reader   :level, :param, :txt
12     attr_accessor :type
14     ######
15     # This is a simple factory system that lets us associate fragement
16     # types (a string) with a subclass of fragment
18     TYPE_MAP = {}
20     def self.type_name(name)
21       TYPE_MAP[name] = self
22     end
24     def self.for(line)
25       klass =  TYPE_MAP[line.type] ||
26         raise("Unknown line type: '#{line.type.inspect}:' '#{line.text}'")
27       return klass.new(line.level, line.param, line.flag, line.text)
28     end
30     def initialize(level, param, type, txt)
31       @level = level
32       @param = param
33       @type  = type
34       @txt   = ""
35       add_text(txt) if txt
36     end
38     def add_text(txt)
39       @txt << " " if @txt.length > 0
40       @txt << txt.tr_s("\n ", "  ").strip
41     end
43     def to_s
44       "L#@level: #{self.class.name.split('::')[-1]}\n#@txt"
45     end
47   end
49   ##
50   # A paragraph is a fragment which gets wrapped to fit. We remove all
51   # newlines when we're created, and have them put back on output.
53   class Paragraph < Fragment
54     type_name :PARAGRAPH
55   end
57   class BlankLine < Paragraph
58     type_name :BLANK
59   end
61   class Heading < Paragraph
62     type_name :HEADING
64     def head_level
65       @param.to_i
66     end
67   end
69   ##
70   # A List is a fragment with some kind of label
72   class ListBase < Paragraph
73     LIST_TYPES = [
74       :BULLET,
75       :NUMBER,
76       :UPPERALPHA,
77       :LOWERALPHA,
78       :LABELED,
79       :NOTE,
80     ]
81   end
83   class ListItem < ListBase
84     type_name :LIST
86     def to_s
87       text = if [:NOTE, :LABELED].include? type then
88                "#{@param}: #{@txt}"
89              else
90                @txt
91              end
93       "L#@level: #{type} #{self.class.name.split('::')[-1]}\n#{text}"
94     end
96   end
98   class ListStart < ListBase
99     def initialize(level, param, type)
100       super(level, param, type, nil)
101     end
102   end
104   class ListEnd < ListBase
105     def initialize(level, type)
106       super(level, "", type, nil)
107     end
108   end
110   ##
111   # Verbatim code contains lines that don't get wrapped.
113   class Verbatim < Fragment
114     type_name  :VERBATIM
116     def add_text(txt)
117       @txt << txt.chomp << "\n"
118     end
120   end
122   ##
123   # A horizontal rule
125   class Rule < Fragment
126     type_name :RULE
127   end
129   ##
130   # Collect groups of lines together. Each group will end up containing a flow
131   # of text.
133   class LineCollection
135     def initialize
136       @fragments = []
137     end
139     def add(fragment)
140       @fragments << fragment
141     end
143     def each(&b)
144       @fragments.each(&b)
145     end
147     def to_a # :nodoc:
148       @fragments.map {|fragment| fragment.to_s}
149     end
151     ##
152     # Factory for different fragment types
154     def fragment_for(*args)
155       Fragment.for(*args)
156     end
158     ##
159     # Tidy up at the end
161     def normalize
162       change_verbatim_blank_lines
163       add_list_start_and_ends
164       add_list_breaks
165       tidy_blank_lines
166     end
168     def to_s
169       @fragments.join("\n----\n")
170     end
172     def accept(am, visitor)
173       visitor.start_accepting
175       @fragments.each do |fragment|
176         case fragment
177         when Verbatim
178           visitor.accept_verbatim(am, fragment)
179         when Rule
180           visitor.accept_rule(am, fragment)
181         when ListStart
182           visitor.accept_list_start(am, fragment)
183         when ListEnd
184           visitor.accept_list_end(am, fragment)
185         when ListItem
186           visitor.accept_list_item(am, fragment)
187         when BlankLine
188           visitor.accept_blank_line(am, fragment)
189         when Heading
190           visitor.accept_heading(am, fragment)
191         when Paragraph
192           visitor.accept_paragraph(am, fragment)
193         end
194       end
196       visitor.end_accepting
197     end
199     private
201     # If you have:
202     #
203     #    normal paragraph text.
204     #
205     #       this is code
206     #   
207     #       and more code
208     #
209     # You'll end up with the fragments Paragraph, BlankLine, Verbatim,
210     # BlankLine, Verbatim, BlankLine, etc.
211     #
212     # The BlankLine in the middle of the verbatim chunk needs to be changed to
213     # a real verbatim newline, and the two verbatim blocks merged
215     def change_verbatim_blank_lines
216       frag_block = nil
217       blank_count = 0
218       @fragments.each_with_index do |frag, i|
219         if frag_block.nil?
220           frag_block = frag if Verbatim === frag
221         else
222           case frag
223           when Verbatim
224             blank_count.times { frag_block.add_text("\n") }
225             blank_count = 0
226             frag_block.add_text(frag.txt)
227             @fragments[i] = nil    # remove out current fragment
228           when BlankLine
229             if frag_block
230               blank_count += 1
231               @fragments[i] = nil
232             end
233           else
234             frag_block = nil
235             blank_count = 0
236           end
237         end
238       end
239       @fragments.compact!
240     end
242     ##
243     # List nesting is implicit given the level of indentation. Make it
244     # explicit, just to make life a tad easier for the output processors
246     def add_list_start_and_ends
247       level = 0
248       res = []
249       type_stack = []
251       @fragments.each do |fragment|
252         # $stderr.puts "#{level} : #{fragment.class.name} : #{fragment.level}"
253         new_level = fragment.level
254         while (level < new_level)
255           level += 1
256           type = fragment.type
257           res << ListStart.new(level, fragment.param, type) if type
258           type_stack.push type
259           # $stderr.puts "Start: #{level}"
260         end
262         while level > new_level
263           type = type_stack.pop
264           res << ListEnd.new(level, type) if type
265           level -= 1
266           # $stderr.puts "End: #{level}, #{type}"
267         end
269         res << fragment
270         level = fragment.level
271       end
272       level.downto(1) do |i|
273         type = type_stack.pop
274         res << ListEnd.new(i, type) if type
275       end
277       @fragments = res
278     end
280     ##
281     # Inserts start/ends between list entries at the same level that have
282     # different element types
284     def add_list_breaks
285       res = @fragments
287       @fragments = []
288       list_stack = []
290       res.each do |fragment|
291         case fragment
292         when ListStart
293           list_stack.push fragment
294         when ListEnd
295           start = list_stack.pop
296           fragment.type = start.type
297         when ListItem
298           l = list_stack.last
299           if fragment.type != l.type
300             @fragments << ListEnd.new(l.level, l.type)
301             start = ListStart.new(l.level, fragment.param, fragment.type)
302             @fragments << start
303             list_stack.pop
304             list_stack.push start
305           end
306         else
307           ;
308         end
309         @fragments << fragment
310       end
311     end
313     ##
314     # Tidy up the blank lines:
315     # * change Blank/ListEnd into ListEnd/Blank
316     # * remove blank lines at the front
318     def tidy_blank_lines
319       (@fragments.size - 1).times do |i|
320         if BlankLine === @fragments[i] and ListEnd === @fragments[i+1] then
321           @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i]
322         end
323       end
325       # remove leading blanks
326       @fragments.each_with_index do |f, i|
327         break unless f.kind_of? BlankLine
328         @fragments[i] = nil
329       end
331       @fragments.compact!
332     end
334   end