1##
2# Outputs RDoc markup as paragraphs with inline markup only.
3
4class RDoc::Markup::ToHtmlSnippet < RDoc::Markup::ToHtml
5
6  ##
7  # After this many characters the input will be cut off.
8
9  attr_reader :character_limit
10
11  ##
12  # The number of characters seen so far.
13
14  attr_reader :characters # :nodoc:
15
16  ##
17  # The attribute bitmask
18
19  attr_reader :mask
20
21  ##
22  # After this many paragraphs the input will be cut off.
23
24  attr_reader :paragraph_limit
25
26  ##
27  # Count of paragraphs found
28
29  attr_reader :paragraphs
30
31  ##
32  # Creates a new ToHtmlSnippet formatter that will cut off the input on the
33  # next word boundary after the given number of +characters+ or +paragraphs+
34  # of text have been encountered.
35
36  def initialize options, characters = 100, paragraphs = 3, markup = nil
37    super options, markup
38
39    @character_limit = characters
40    @paragraph_limit = paragraphs
41
42    @characters = 0
43    @mask       = 0
44    @paragraphs = 0
45
46    @markup.add_special RDoc::CrossReference::CROSSREF_REGEXP, :CROSSREF
47  end
48
49  ##
50  # Adds +heading+ to the output as a paragraph
51
52  def accept_heading heading
53    @res << "<p>#{to_html heading.text}\n"
54
55    add_paragraph
56  end
57
58  ##
59  # Raw sections are untrusted and ignored
60
61  alias accept_raw ignore
62
63  ##
64  # Rules are ignored
65
66  alias accept_rule ignore
67
68  def accept_paragraph paragraph
69    para = @in_list_entry.last || "<p>"
70
71    text = paragraph.text @hard_break
72
73    @res << "#{para}#{wrap to_html text}\n"
74
75    add_paragraph
76  end
77
78  ##
79  # Finishes consumption of +list_item+
80
81  def accept_list_item_end list_item
82  end
83
84  ##
85  # Prepares the visitor for consuming +list_item+
86
87  def accept_list_item_start list_item
88    @res << list_item_start(list_item, @list.last)
89  end
90
91  ##
92  # Prepares the visitor for consuming +list+
93
94  def accept_list_start list
95    @list << list.type
96    @res << html_list_name(list.type, true)
97    @in_list_entry.push ''
98  end
99
100  ##
101  # Adds +verbatim+ to the output
102
103  def accept_verbatim verbatim
104    throw :done if @characters >= @character_limit
105    input = verbatim.text.rstrip
106
107    text = truncate input
108    text << ' ...' unless text == input
109
110    super RDoc::Markup::Verbatim.new text
111
112    add_paragraph
113  end
114
115  ##
116  # Prepares the visitor for HTML snippet generation
117
118  def start_accepting
119    super
120
121    @characters = 0
122  end
123
124  ##
125  # Removes escaping from the cross-references in +special+
126
127  def handle_special_CROSSREF special
128    special.text.sub(/\A\\/, '')
129  end
130
131  ##
132  # +special+ is a <code><br></code>
133
134  def handle_special_HARD_BREAK special
135    @characters -= 4
136    '<br>'
137  end
138
139  ##
140  # Lists are paragraphs, but notes and labels have a separator
141
142  def list_item_start list_item, list_type
143    throw :done if @characters >= @character_limit
144
145    case list_type
146    when :BULLET, :LALPHA, :NUMBER, :UALPHA then
147      "<p>"
148    when :LABEL, :NOTE then
149      labels = Array(list_item.label).map do |label|
150        to_html label
151      end.join ', '
152
153      labels << " &mdash; " unless labels.empty?
154
155      start = "<p>#{labels}"
156      @characters += 1 # try to include the label
157      start
158    else
159      raise RDoc::Error, "Invalid list type: #{list_type.inspect}"
160    end
161  end
162
163  ##
164  # Returns just the text of +link+, +url+ is only used to determine the link
165  # type.
166
167  def gen_url url, text
168    if url =~ /^rdoc-label:([^:]*)(?::(.*))?/ then
169      type = "link"
170    elsif url =~ /([A-Za-z]+):(.*)/ then
171      type = $1
172    else
173      type = "http"
174    end
175
176    if (type == "http" or type == "https" or type == "link") and
177       url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then
178      ''
179    else
180      text.sub(%r%^#{type}:/*%, '')
181    end
182  end
183
184  ##
185  # In snippets, there are no lists
186
187  def html_list_name list_type, open_tag
188    ''
189  end
190
191  ##
192  # Throws +:done+ when paragraph_limit paragraphs have been encountered
193
194  def add_paragraph
195    @paragraphs += 1
196
197    throw :done if @paragraphs >= @paragraph_limit
198  end
199
200  ##
201  # Marks up +content+
202
203  def convert content
204    catch :done do
205      return super
206    end
207
208    end_accepting
209  end
210
211  ##
212  # Converts flow items +flow+
213
214  def convert_flow flow
215    throw :done if @characters >= @character_limit
216
217    res = []
218    @mask = 0
219
220    flow.each do |item|
221      case item
222      when RDoc::Markup::AttrChanger then
223        off_tags res, item
224        on_tags  res, item
225      when String then
226        text = convert_string item
227        res << truncate(text)
228      when RDoc::Markup::Special then
229        text = convert_special item
230        res << truncate(text)
231      else
232        raise "Unknown flow element: #{item.inspect}"
233      end
234
235      if @characters >= @character_limit then
236        off_tags res, RDoc::Markup::AttrChanger.new(0, @mask)
237        break
238      end
239    end
240
241    res << ' ...' if @characters >= @character_limit
242
243    res.join
244  end
245
246  ##
247  # Maintains a bitmask to allow HTML elements to be closed properly.  See
248  # RDoc::Markup::Formatter.
249
250  def on_tags res, item
251    @mask ^= item.turn_on
252
253    super
254  end
255
256  ##
257  # Maintains a bitmask to allow HTML elements to be closed properly.  See
258  # RDoc::Markup::Formatter.
259
260  def off_tags res, item
261    @mask ^= item.turn_off
262
263    super
264  end
265
266  ##
267  # Truncates +text+ at the end of the first word after the character_limit.
268
269  def truncate text
270    length = text.length
271    characters = @characters
272    @characters += length
273
274    return text if @characters < @character_limit
275
276    remaining = @character_limit - characters
277
278    text =~ /\A(.{#{remaining},}?)(\s|$)/m # TODO word-break instead of \s?
279
280    $1
281  end
282
283end
284
285