1##
2# Base class for RDoc markup formatters
3#
4# Formatters are a visitor that converts an RDoc::Markup tree (from a comment)
5# into some kind of output.  RDoc ships with formatters for converting back to
6# rdoc, ANSI text, HTML, a Table of Contents and other formats.
7#
8# If you'd like to write your own Formatter use
9# RDoc::Markup::FormatterTestCase.  If you're writing a text-output formatter
10# use RDoc::Markup::TextFormatterTestCase which provides extra test cases.
11
12class RDoc::Markup::Formatter
13
14  ##
15  # Tag for inline markup containing a +bit+ for the bitmask and the +on+ and
16  # +off+ triggers.
17
18  InlineTag = Struct.new(:bit, :on, :off)
19
20  ##
21  # Converts a target url to one that is relative to a given path
22
23  def self.gen_relative_url path, target
24    from        = File.dirname path
25    to, to_file = File.split target
26
27    from = from.split "/"
28    to   = to.split "/"
29
30    from.delete '.'
31    to.delete '.'
32
33    while from.size > 0 and to.size > 0 and from[0] == to[0] do
34      from.shift
35      to.shift
36    end
37
38    from.fill ".."
39    from.concat to
40    from << to_file
41    File.join(*from)
42  end
43
44  ##
45  # Creates a new Formatter
46
47  def initialize options, markup = nil
48    @options = options
49
50    @markup = markup || RDoc::Markup.new
51    @am     = @markup.attribute_manager
52    @am.add_special(/<br>/, :HARD_BREAK)
53
54    @attributes = @am.attributes
55
56    @attr_tags = []
57
58    @in_tt = 0
59    @tt_bit = @attributes.bitmap_for :TT
60
61    @hard_break = ''
62    @from_path = '.'
63  end
64
65  ##
66  # Adds +document+ to the output
67
68  def accept_document document
69    document.parts.each do |item|
70      case item
71      when RDoc::Markup::Document then # HACK
72        accept_document item
73      else
74        item.accept self
75      end
76    end
77  end
78
79  ##
80  # Adds a special for links of the form rdoc-...:
81
82  def add_special_RDOCLINK
83    @markup.add_special(/rdoc-[a-z]+:\S+/, :RDOCLINK)
84  end
85
86  ##
87  # Adds a special for links of the form {<text>}[<url>] and <word>[<url>]
88
89  def add_special_TIDYLINK
90    @markup.add_special(/(?:
91                          \{.*?\} |   # multi-word label
92                          \b[^\s{}]+? # single-word label
93                         )
94
95                         \[\S+?\]     # link target
96                        /x, :TIDYLINK)
97  end
98
99  ##
100  # Add a new set of tags for an attribute. We allow separate start and end
101  # tags for flexibility
102
103  def add_tag(name, start, stop)
104    attr = @attributes.bitmap_for name
105    @attr_tags << InlineTag.new(attr, start, stop)
106  end
107
108  ##
109  # Allows +tag+ to be decorated with additional information.
110
111  def annotate(tag)
112    tag
113  end
114
115  ##
116  # Marks up +content+
117
118  def convert content
119    @markup.convert content, self
120  end
121
122  ##
123  # Converts flow items +flow+
124
125  def convert_flow(flow)
126    res = []
127
128    flow.each do |item|
129      case item
130      when String then
131        res << convert_string(item)
132      when RDoc::Markup::AttrChanger then
133        off_tags res, item
134        on_tags res, item
135      when RDoc::Markup::Special then
136        res << convert_special(item)
137      else
138        raise "Unknown flow element: #{item.inspect}"
139      end
140    end
141
142    res.join
143  end
144
145  ##
146  # Converts added specials.  See RDoc::Markup#add_special
147
148  def convert_special special
149    return special.text if in_tt?
150
151    handled = false
152
153    @attributes.each_name_of special.type do |name|
154      method_name = "handle_special_#{name}"
155
156      if respond_to? method_name then
157        special.text = send method_name, special
158        handled = true
159      end
160    end
161
162    unless handled then
163      special_name = @attributes.as_string special.type
164
165      raise RDoc::Error, "Unhandled special #{special_name}: #{special}"
166    end
167
168    special.text
169  end
170
171  ##
172  # Converts a string to be fancier if desired
173
174  def convert_string string
175    string
176  end
177
178  ##
179  # Use ignore in your subclass to ignore the content of a node.
180  #
181  #   ##
182  #   # We don't support raw nodes in ToNoRaw
183  #
184  #   alias accept_raw ignore
185
186  def ignore *node
187  end
188
189  ##
190  # Are we currently inside tt tags?
191
192  def in_tt?
193    @in_tt > 0
194  end
195
196  ##
197  # Turns on tags for +item+ on +res+
198
199  def on_tags res, item
200    attr_mask = item.turn_on
201    return if attr_mask.zero?
202
203    @attr_tags.each do |tag|
204      if attr_mask & tag.bit != 0 then
205        res << annotate(tag.on)
206        @in_tt += 1 if tt? tag
207      end
208    end
209  end
210
211  ##
212  # Turns off tags for +item+ on +res+
213
214  def off_tags res, item
215    attr_mask = item.turn_off
216    return if attr_mask.zero?
217
218    @attr_tags.reverse_each do |tag|
219      if attr_mask & tag.bit != 0 then
220        @in_tt -= 1 if tt? tag
221        res << annotate(tag.off)
222      end
223    end
224  end
225
226  ##
227  # Extracts and a scheme, url and an anchor id from +url+ and returns them.
228
229  def parse_url url
230    case url
231    when /^rdoc-label:([^:]*)(?::(.*))?/ then
232      scheme = 'link'
233      path   = "##{$1}"
234      id     = " id=\"#{$2}\"" if $2
235    when /([A-Za-z]+):(.*)/ then
236      scheme = $1.downcase
237      path   = $2
238    when /^#/ then
239    else
240      scheme = 'http'
241      path   = url
242      url    = "http://#{url}"
243    end
244
245    if scheme == 'link' then
246      url = if path[0, 1] == '#' then # is this meaningful?
247              path
248            else
249              self.class.gen_relative_url @from_path, path
250            end
251    end
252
253    [scheme, url, id]
254  end
255
256  ##
257  # Is +tag+ a tt tag?
258
259  def tt? tag
260    tag.bit == @tt_bit
261  end
262
263end
264
265