Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / rdoc / markup / attribute_manager.rb
blob72f70dadd7747808274627f7b7928274285ca4d4
1 require 'rdoc/markup/inline'
3 class RDoc::Markup::AttributeManager
5   NULL = "\000".freeze
7   ##
8   # We work by substituting non-printing characters in to the text. For now
9   # I'm assuming that I can substitute a character in the range 0..8 for a 7
10   # bit character without damaging the encoded string, but this might be
11   # optimistic
13   A_PROTECT  = 004
14   PROTECT_ATTR  = A_PROTECT.chr
16   ##
17   # This maps delimiters that occur around words (such as *bold* or +tt+)
18   # where the start and end delimiters and the same. This lets us optimize
19   # the regexp
21   MATCHING_WORD_PAIRS = {}
23   ##
24   # And this is used when the delimiters aren't the same. In this case the
25   # hash maps a pattern to the attribute character
27   WORD_PAIR_MAP = {}
29   ##
30   # This maps HTML tags to the corresponding attribute char
32   HTML_TAGS = {}
34   ##
35   # And this maps _special_ sequences to a name. A special sequence is
36   # something like a WikiWord
38   SPECIAL = {}
40   ##
41   # Return an attribute object with the given turn_on and turn_off bits set
43   def attribute(turn_on, turn_off)
44     RDoc::Markup::AttrChanger.new turn_on, turn_off
45   end
47   def change_attribute(current, new)
48     diff = current ^ new
49     attribute(new & diff, current & diff)
50   end
52   def changed_attribute_by_name(current_set, new_set)
53     current = new = 0
54     current_set.each do |name|
55       current |= RDoc::Markup::Attribute.bitmap_for(name)
56     end
58     new_set.each do |name|
59       new |= RDoc::Markup::Attribute.bitmap_for(name)
60     end
62     change_attribute(current, new)
63   end
65   def copy_string(start_pos, end_pos)
66     res = @str[start_pos...end_pos]
67     res.gsub!(/\000/, '')
68     res
69   end
71   ##
72   # Map attributes like <b>text</b>to the sequence
73   # \001\002<char>\001\003<char>, where <char> is a per-attribute specific
74   # character
76   def convert_attrs(str, attrs)
77     # first do matching ones
78     tags = MATCHING_WORD_PAIRS.keys.join("")
80     re = /(^|\W)([#{tags}])([#:\\]?[\w.\/-]+?\S?)\2(\W|$)/
82     1 while str.gsub!(re) do
83       attr = MATCHING_WORD_PAIRS[$2]
84       attrs.set_attrs($`.length + $1.length + $2.length, $3.length, attr)
85       $1 + NULL * $2.length + $3 + NULL * $2.length + $4
86     end
88     # then non-matching
89     unless WORD_PAIR_MAP.empty? then
90       WORD_PAIR_MAP.each do |regexp, attr|
91         str.gsub!(regexp) {
92           attrs.set_attrs($`.length + $1.length, $2.length, attr)
93           NULL * $1.length + $2 + NULL * $3.length
94         }
95       end
96     end
97   end
99   def convert_html(str, attrs)
100     tags = HTML_TAGS.keys.join '|'
102     1 while str.gsub!(/<(#{tags})>(.*?)<\/\1>/i) {
103       attr = HTML_TAGS[$1.downcase]
104       html_length = $1.length + 2
105       seq = NULL * html_length
106       attrs.set_attrs($`.length + html_length, $2.length, attr)
107       seq + $2 + seq + NULL
108     }
109   end
111   def convert_specials(str, attrs)
112     unless SPECIAL.empty?
113       SPECIAL.each do |regexp, attr|
114         str.scan(regexp) do
115           attrs.set_attrs($`.length, $&.length,
116                           attr | RDoc::Markup::Attribute::SPECIAL)
117         end
118       end
119     end
120   end
122   ##
123   # A \ in front of a character that would normally be processed turns off
124   # processing. We do this by turning \< into <#{PROTECT}
126   PROTECTABLE = %w[<\\]
128   def mask_protected_sequences
129     protect_pattern = Regexp.new("\\\\([#{Regexp.escape(PROTECTABLE.join(''))}])")
130     @str.gsub!(protect_pattern, "\\1#{PROTECT_ATTR}")
131   end
133   def unmask_protected_sequences
134     @str.gsub!(/(.)#{PROTECT_ATTR}/, "\\1\000")
135   end
137   def initialize
138     add_word_pair("*", "*", :BOLD)
139     add_word_pair("_", "_", :EM)
140     add_word_pair("+", "+", :TT)
142     add_html("em", :EM)
143     add_html("i",  :EM)
144     add_html("b",  :BOLD)
145     add_html("tt",   :TT)
146     add_html("code", :TT)
148     add_special(/<!--(.*?)-->/, :COMMENT)
149   end
151   def add_word_pair(start, stop, name)
152     raise ArgumentError, "Word flags may not start with '<'" if
153       start[0,1] == '<'
155     bitmap = RDoc::Markup::Attribute.bitmap_for name
157     if start == stop then
158       MATCHING_WORD_PAIRS[start] = bitmap
159     else
160       pattern = /(#{Regexp.escape start})(\S+)(#{Regexp.escape stop})/
161       WORD_PAIR_MAP[pattern] = bitmap
162     end
164     PROTECTABLE << start[0,1]
165     PROTECTABLE.uniq!
166   end
168   def add_html(tag, name)
169     HTML_TAGS[tag.downcase] = RDoc::Markup::Attribute.bitmap_for name
170   end
172   def add_special(pattern, name)
173     SPECIAL[pattern] = RDoc::Markup::Attribute.bitmap_for name
174   end
176   def flow(str)
177     @str = str
179     puts("Before flow, str='#{@str.dump}'") if $DEBUG_RDOC
180     mask_protected_sequences
182     @attrs = RDoc::Markup::AttrSpan.new @str.length
184     puts("After protecting, str='#{@str.dump}'") if $DEBUG_RDOC
186     convert_attrs(@str, @attrs)
187     convert_html(@str, @attrs)
188     convert_specials(str, @attrs)
190     unmask_protected_sequences
192     puts("After flow, str='#{@str.dump}'") if $DEBUG_RDOC
194     return split_into_flow
195   end
197   def display_attributes
198     puts
199     puts @str.tr(NULL, "!")
200     bit = 1
201     16.times do |bno|
202       line = ""
203       @str.length.times do |i|
204         if (@attrs[i] & bit) == 0
205           line << " "
206         else
207           if bno.zero?
208             line << "S"
209           else
210             line << ("%d" % (bno+1))
211           end
212         end
213       end
214       puts(line) unless line =~ /^ *$/
215       bit <<= 1
216     end
217   end
219   def split_into_flow
220     display_attributes if $DEBUG_RDOC
222     res = []
223     current_attr = 0
224     str = ""
226     str_len = @str.length
228     # skip leading invisible text
229     i = 0
230     i += 1 while i < str_len and @str[i].chr == "\0"
231     start_pos = i
233     # then scan the string, chunking it on attribute changes
234     while i < str_len
235       new_attr = @attrs[i]
236       if new_attr != current_attr
237         if i > start_pos
238           res << copy_string(start_pos, i)
239           start_pos = i
240         end
242         res << change_attribute(current_attr, new_attr)
243         current_attr = new_attr
245         if (current_attr & RDoc::Markup::Attribute::SPECIAL) != 0 then
246           i += 1 while
247             i < str_len and (@attrs[i] & RDoc::Markup::Attribute::SPECIAL) != 0
249           res << RDoc::Markup::Special.new(current_attr,
250                                            copy_string(start_pos, i))
251           start_pos = i
252           next
253         end
254       end
256       # move on, skipping any invisible characters
257       begin
258         i += 1
259       end while i < str_len and @str[i].chr == "\0"
260     end
262     # tidy up trailing text
263     if start_pos < str_len
264       res << copy_string(start_pos, str_len)
265     end
267     # and reset to all attributes off
268     res << change_attribute(current_attr, 0) if current_attr != 0
270     return res
271   end