1# frozen_string_literal: true 2require 'cgi' 3 4## 5# Outputs RDoc markup as HTML. 6 7class RDoc::Markup::ToHtml < RDoc::Markup::Formatter 8 9 include RDoc::Text 10 11 # :section: Utilities 12 13 ## 14 # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags 15 16 LIST_TYPE_TO_HTML = { 17 :BULLET => ['<ul>', '</ul>'], 18 :LABEL => ['<dl class="rdoc-list label-list">', '</dl>'], 19 :LALPHA => ['<ol style="list-style-type: lower-alpha">', '</ol>'], 20 :NOTE => ['<dl class="rdoc-list note-list">', '</dl>'], 21 :NUMBER => ['<ol>', '</ol>'], 22 :UALPHA => ['<ol style="list-style-type: upper-alpha">', '</ol>'], 23 } 24 25 attr_reader :res # :nodoc: 26 attr_reader :in_list_entry # :nodoc: 27 attr_reader :list # :nodoc: 28 29 ## 30 # The RDoc::CodeObject HTML is being generated for. This is used to 31 # generate namespaced URI fragments 32 33 attr_accessor :code_object 34 35 ## 36 # Path to this document for relative links 37 38 attr_accessor :from_path 39 40 # :section: 41 42 ## 43 # Creates a new formatter that will output HTML 44 45 def initialize options, markup = nil 46 super 47 48 @code_object = nil 49 @from_path = '' 50 @in_list_entry = nil 51 @list = nil 52 @th = nil 53 @hard_break = "<br>\n" 54 55 # external links 56 @markup.add_regexp_handling(/(?:link:|https?:|mailto:|ftp:|irc:|www\.)\S+\w/, 57 :HYPERLINK) 58 59 add_regexp_handling_RDOCLINK 60 add_regexp_handling_TIDYLINK 61 62 init_tags 63 end 64 65 # :section: Regexp Handling 66 # 67 # These methods are used by regexp handling markup added by RDoc::Markup#add_regexp_handling. 68 69 def handle_RDOCLINK url # :nodoc: 70 case url 71 when /^rdoc-ref:/ 72 $' 73 when /^rdoc-label:/ 74 text = $' 75 76 text = case text 77 when /\Alabel-/ then $' 78 when /\Afootmark-/ then $' 79 when /\Afoottext-/ then $' 80 else text 81 end 82 83 gen_url url, text 84 when /^rdoc-image:/ 85 "<img src=\"#{$'}\">" 86 else 87 url =~ /\Ardoc-[a-z]+:/ 88 89 $' 90 end 91 end 92 93 ## 94 # +target+ is a <code><br></code> 95 96 def handle_regexp_HARD_BREAK target 97 '<br>' 98 end 99 100 ## 101 # +target+ is a potential link. The following schemes are handled: 102 # 103 # <tt>mailto:</tt>:: 104 # Inserted as-is. 105 # <tt>http:</tt>:: 106 # Links are checked to see if they reference an image. If so, that image 107 # gets inserted using an <tt><img></tt> tag. Otherwise a conventional 108 # <tt><a href></tt> is used. 109 # <tt>link:</tt>:: 110 # Reference to a local file relative to the output directory. 111 112 def handle_regexp_HYPERLINK(target) 113 url = target.text 114 115 gen_url url, url 116 end 117 118 ## 119 # +target+ is an rdoc-schemed link that will be converted into a hyperlink. 120 # 121 # For the +rdoc-ref+ scheme the named reference will be returned without 122 # creating a link. 123 # 124 # For the +rdoc-label+ scheme the footnote and label prefixes are stripped 125 # when creating a link. All other contents will be linked verbatim. 126 127 def handle_regexp_RDOCLINK target 128 handle_RDOCLINK target.text 129 end 130 131 ## 132 # This +target+ is a link where the label is different from the URL 133 # <tt>label[url]</tt> or <tt>{long label}[url]</tt> 134 135 def handle_regexp_TIDYLINK(target) 136 text = target.text 137 138 return text unless 139 text =~ /^\{(.*)\}\[(.*?)\]$/ or text =~ /^(\S+)\[(.*?)\]$/ 140 141 label = $1 142 url = $2 143 144 label = handle_RDOCLINK label if /^rdoc-image:/ =~ label 145 146 gen_url url, label 147 end 148 149 # :section: Visitor 150 # 151 # These methods implement the HTML visitor. 152 153 ## 154 # Prepares the visitor for HTML generation 155 156 def start_accepting 157 @res = [] 158 @in_list_entry = [] 159 @list = [] 160 end 161 162 ## 163 # Returns the generated output 164 165 def end_accepting 166 @res.join 167 end 168 169 ## 170 # Adds +block_quote+ to the output 171 172 def accept_block_quote block_quote 173 @res << "\n<blockquote>" 174 175 block_quote.parts.each do |part| 176 part.accept self 177 end 178 179 @res << "</blockquote>\n" 180 end 181 182 ## 183 # Adds +paragraph+ to the output 184 185 def accept_paragraph paragraph 186 @res << "\n<p>" 187 text = paragraph.text @hard_break 188 text = text.gsub(/\r?\n/, ' ') 189 @res << to_html(text) 190 @res << "</p>\n" 191 end 192 193 ## 194 # Adds +verbatim+ to the output 195 196 def accept_verbatim verbatim 197 text = verbatim.text.rstrip 198 199 klass = nil 200 201 content = if verbatim.ruby? or parseable? text then 202 begin 203 tokens = RDoc::Parser::RipperStateLex.parse text 204 klass = ' class="ruby"' 205 206 result = RDoc::TokenStream.to_html tokens 207 result = result + "\n" unless "\n" == result[-1] 208 result 209 rescue 210 CGI.escapeHTML text 211 end 212 else 213 CGI.escapeHTML text 214 end 215 216 if @options.pipe then 217 @res << "\n<pre><code>#{CGI.escapeHTML text}\n</code></pre>\n" 218 else 219 @res << "\n<pre#{klass}>#{content}</pre>\n" 220 end 221 end 222 223 ## 224 # Adds +rule+ to the output 225 226 def accept_rule rule 227 @res << "<hr>\n" 228 end 229 230 ## 231 # Prepares the visitor for consuming +list+ 232 233 def accept_list_start(list) 234 @list << list.type 235 @res << html_list_name(list.type, true) 236 @in_list_entry.push false 237 end 238 239 ## 240 # Finishes consumption of +list+ 241 242 def accept_list_end(list) 243 @list.pop 244 if tag = @in_list_entry.pop 245 @res << tag 246 end 247 @res << html_list_name(list.type, false) << "\n" 248 end 249 250 ## 251 # Prepares the visitor for consuming +list_item+ 252 253 def accept_list_item_start(list_item) 254 if tag = @in_list_entry.last 255 @res << tag 256 end 257 258 @res << list_item_start(list_item, @list.last) 259 end 260 261 ## 262 # Finishes consumption of +list_item+ 263 264 def accept_list_item_end(list_item) 265 @in_list_entry[-1] = list_end_for(@list.last) 266 end 267 268 ## 269 # Adds +blank_line+ to the output 270 271 def accept_blank_line(blank_line) 272 # @res << annotate("<p />") << "\n" 273 end 274 275 ## 276 # Adds +heading+ to the output. The headings greater than 6 are trimmed to 277 # level 6. 278 279 def accept_heading heading 280 level = [6, heading.level].min 281 282 label = heading.label @code_object 283 284 @res << if @options.output_decoration 285 "\n<h#{level} id=\"#{label}\">" 286 else 287 "\n<h#{level}>" 288 end 289 @res << to_html(heading.text) 290 unless @options.pipe then 291 @res << "<span><a href=\"##{label}\">¶</a>" 292 @res << " <a href=\"#top\">↑</a></span>" 293 end 294 @res << "</h#{level}>\n" 295 end 296 297 ## 298 # Adds +raw+ to the output 299 300 def accept_raw raw 301 @res << raw.parts.join("\n") 302 end 303 304 # :section: Utilities 305 306 ## 307 # CGI-escapes +text+ 308 309 def convert_string(text) 310 CGI.escapeHTML text 311 end 312 313 ## 314 # Generate a link to +url+ with content +text+. Handles the special cases 315 # for img: and link: described under handle_regexp_HYPERLINK 316 317 def gen_url url, text 318 scheme, url, id = parse_url url 319 320 if %w[http https link].include?(scheme) and 321 url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then 322 "<img src=\"#{url}\" />" 323 else 324 text = text.sub %r%^#{scheme}:/*%i, '' 325 text = text.sub %r%^[*\^](\d+)$%, '\1' 326 327 link = "<a#{id} href=\"#{url}\">#{text}</a>" 328 329 link = "<sup>#{link}</sup>" if /"foot/ =~ id 330 331 link 332 end 333 end 334 335 ## 336 # Determines the HTML list element for +list_type+ and +open_tag+ 337 338 def html_list_name(list_type, open_tag) 339 tags = LIST_TYPE_TO_HTML[list_type] 340 raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags 341 tags[open_tag ? 0 : 1] 342 end 343 344 ## 345 # Maps attributes to HTML tags 346 347 def init_tags 348 add_tag :BOLD, "<strong>", "</strong>" 349 add_tag :TT, "<code>", "</code>" 350 add_tag :EM, "<em>", "</em>" 351 end 352 353 ## 354 # Returns the HTML tag for +list_type+, possible using a label from 355 # +list_item+ 356 357 def list_item_start(list_item, list_type) 358 case list_type 359 when :BULLET, :LALPHA, :NUMBER, :UALPHA then 360 "<li>" 361 when :LABEL, :NOTE then 362 Array(list_item.label).map do |label| 363 "<dt>#{to_html label}\n" 364 end.join << "<dd>" 365 else 366 raise RDoc::Error, "Invalid list type: #{list_type.inspect}" 367 end 368 end 369 370 ## 371 # Returns the HTML end-tag for +list_type+ 372 373 def list_end_for(list_type) 374 case list_type 375 when :BULLET, :LALPHA, :NUMBER, :UALPHA then 376 "</li>" 377 when :LABEL, :NOTE then 378 "</dd>" 379 else 380 raise RDoc::Error, "Invalid list type: #{list_type.inspect}" 381 end 382 end 383 384 ## 385 # Returns true if text is valid ruby syntax 386 387 def parseable? text 388 verbose, $VERBOSE = $VERBOSE, nil 389 eval("BEGIN {return true}\n#{text}") 390 rescue SyntaxError 391 false 392 ensure 393 $VERBOSE = verbose 394 end 395 396 ## 397 # Converts +item+ to HTML using RDoc::Text#to_html 398 399 def to_html item 400 super convert_flow @am.flow item 401 end 402 403end 404 405