From: Phlip Date: 2007-08-01T11:27:08+09:00 Subject: Tweak RDoc to transclude a function into another function's documentation Ruboids: A recent post here, by Robert Dober, showed how to tweak RDoc to add a raw HTML directive, %html. Below my sig is a monkey-patch, for Ruby 1.8's RDoc, which includes this tweak and adds another; the ability to insert a method's contents into another method's documentation. This allows us to do "literate programming". We can compete with the likes of Knuth and Sedgewick by inserting automatically tested source code into our verbiage. To use the monkey patch, simply save it to a file, say rdoc_patch.rb and then add require 'rdoc_patch' inside your Rakefile. If your Rakefile is normal, it will run RDoc in-process, so that will get the patch. I use the %html and %transclude tags like this: # %html # Example: # # %transclude AssertJavaScriptTest#test_assert_json # def assert_json(...) ... end The contents of test_assert_json will explain to us how to use assert_json. And the %html gives us a direct, external link to the method: http://assertxpath.rubyforge.org/classes/AssertJavaScript.html#assert_json I'm putting the code here because I don't think it's good enough as a direct patch to RDoc yet. The main reasons: - the directives escape with %, where :x: is the RDoc standard. - both these tweaks gleefully ignore the other documentation flavors - future directives are no easier. The ideal system would help users easily plug new directives in, without stitching them into each element of the Visitor Pattern. -- Phlip http://www.oreilly.com/catalog/9780596510657/ "Test Driven Ajax (on Rails)" assert_xpath, assert_javascript, & assert_ajax ################################################################## ## eek eek ook ook patch to add directives to RDocage require 'rdoc/rdoc' require 'rdoc/markup/simple_markup' require 'rdoc/markup/simple_markup/lines' require 'rdoc/markup/simple_markup/fragments' require 'rdoc/markup/simple_markup/to_html' def find_method_contents(file_info, modool, method) file_info.each do |top| if mod = top.find_module_named(modool) and symbol = mod.find_local_symbol(method) return symbol.token_stream end end return nil end module SM class Line PURE_HTML = :PURE_HTML TRANSCLUDE = :TRANSCLUDE end class PureHTML < Fragment type_name Line::PURE_HTML end class Transclude < Fragment type_name Line::TRANSCLUDE end class ToHtml def accept_transclude(am, fragment) if found = find_method_contents(@context.context.parent.in_files, *fragment.txt.split('#')) @res << '
'
        #  to do: hilite syntax; make method name clickable
        found.each_with_index do |tok, index|
          next if 0 == index and tok.text =~ /^\#/
          next if 1 == index and tok.text == "\n"
          @res << tok.text
        end
        @res << '
' else @res << fragment.txt end end def accept_pure_html(am, fragment) @res << fragment.txt end end class LineCollection def accept(am, visitor) visitor.start_accepting @fragments.each do |fragment| case fragment when Verbatim visitor.accept_verbatim(am, fragment) when PureHTML visitor.accept_pure_html(am, fragment) when Transclude visitor.accept_transclude(am, fragment) when Rule visitor.accept_rule(am, fragment) when ListStart visitor.accept_list_start(am, fragment) when ListEnd visitor.accept_list_end(am, fragment) when ListItem visitor.accept_list_item(am, fragment) when BlankLine visitor.accept_blank_line(am, fragment) when Heading visitor.accept_heading(am, fragment) when Paragraph visitor.accept_paragraph(am, fragment) end end visitor.end_accepting end end class SimpleMarkup private def assign_types_to_lines(margin = 0, level = 0) while line = @lines.next if /^\s*%html/ === line.text then line.text.sub!("%html","") line.stamp( Line::PURE_HTML, level ) next end if /^\s*%transclude/ === line.text then line.text.sub!("%transclude","") line.stamp( Line::TRANSCLUDE, level ) next end if line.isBlank? line.stamp(Line::BLANK, level) next end # if a line contains non-blanks before the margin, then it must belong # to an outer level text = line.text for i in 0...margin if text[i] != SPACE @lines.unget return end end active_line = text[margin..-1] # Rules (horizontal lines) look like # # --- (three or more hyphens) # # The more hyphens, the thicker the rule # if /^(---+)\s*$/ =~ active_line line.stamp(Line::RULE, level, $1.length-2) next end # Then look for list entries. First the ones that have to have # text following them (* xxx, - xxx, and dd. xxx) if SIMPLE_LIST_RE =~ active_line offset = margin + $1.length prefix = $2 prefix_length = prefix.length flag = case prefix when "*","-" then ListBase::BULLET when /^\d/ then ListBase::NUMBER when /^[A-Z]/ then ListBase::UPPERALPHA when /^[a-z]/ then ListBase::LOWERALPHA else raise "Invalid List Type: #{self.inspect}" end line.stamp(Line::LIST, level+1, prefix, flag) text[margin, prefix_length] = " " * prefix_length assign_types_to_lines(offset, level + 1) next end if LABEL_LIST_RE =~ active_line offset = margin + $1.length prefix = $2 prefix_length = prefix.length next if handled_labeled_list(line, level, margin, offset, prefix) end # Headings look like # = Main heading # == Second level # === Third # # Headings reset the level to 0 if active_line[0] == ?= and active_line =~ /^(=+)\s*(.*)/ prefix_length = $1.length prefix_length = 6 if prefix_length > 6 line.stamp(Line::HEADING, 0, prefix_length) line.strip_leading(margin + prefix_length) next end # If the character's a space, then we have verbatim text, # otherwise if active_line[0] == SPACE line.strip_leading(margin) if margin > 0 line.stamp(Line::VERBATIM, level) else line.stamp(Line::PARAGRAPH, level) end end end end end ## eek eek ook ook patch to add directives to RDocage ##################################################################