require 'jsduck/meta_tag_registry' require 'jsduck/html' module JsDuck # Ruby-side implementation of class docs Renderer. # Uses PhantomJS to run Docs.Renderer JavaScript. class Renderer def initialize(opts) @opts = opts end def render(cls) @cls = cls return [ "<div>", render_sidebar, "<div class='doc-contents'>", render_meta_data(@cls[:html_meta], :top), render_private_class_notice, @cls[:doc], render_enum_class_notice, render_meta_data(@cls[:html_meta], :bottom), "</div>", "<div class='members'>", render_all_sections, "</div>", "</div>", ].flatten.compact.join end def render_private_class_notice return if !@cls[:private] return [ "<p class='private'><strong>NOTE</strong> ", "This is a private utility class for internal use by the framework. ", "Don't rely on its existence.</p>", ] end def render_enum_class_notice return if !@cls[:enum] if @cls[:enum][:type] == "String" first = @cls[:members][:property][0] || {:name => 'foo', :default => '"foo"'} [ "<p class='enum'><strong>ENUM:</strong> ", "This enumeration defines a set of String values. ", "It exists primarily for documentation purposes - ", "in code use the actual string values like #{first[:default]}, ", "don't reference them through this class like #{@cls[:name]}.#{first[:name]}.</p>", ] else [ "<p class='enum'><strong>ENUM:</strong> ", "This enumeration defines a set of #{@cls[:enum][:type]} values.</p>", ] end end def render_meta_data(meta_data, position) return if meta_data.size == 0 MetaTagRegistry.instance.tags(position).map {|tag| meta_data[tag.key] } end def render_sidebar items = [ render_alternate_class_names, render_tree, render_dependencies(:mixins, "Mixins"), render_dependencies(:parentMixins, "Inherited mixins"), render_dependencies(:requires, "Requires"), render_dependencies(:subclasses, "Subclasses"), render_dependencies(:mixedInto, "Mixed into"), render_dependencies(:uses, "Uses"), @opts.source ? render_files : nil, ] if items.compact.length > 0 return ['<pre class="hierarchy">', items, '</pre>'] else return nil end end def render_alternate_class_names return if @cls[:alternateClassNames].length == 0 return [ "<h4>Alternate names</h4>", @cls[:alternateClassNames].sort.map {|name| "<div class='alternate-class-name'>#{name}</div>" }, ] end def render_dependencies(type, title) return if !@cls[type] || @cls[type].length == 0 return [ "<h4>#{title}</h4>", @cls[type].sort.map {|name| "<div class='dependency'>#{name.exists? ? render_link(name) : name}</div>" }, ] end def render_files return if @cls[:files].length == 0 || @cls[:files][0][:filename] == "" return [ "<h4>Files</h4>", @cls[:files].map do |file| url = "source/" + file[:href] title = File.basename(file[:filename]) "<div class='dependency'><a href='#{url}' target='_blank'>#{title}</a></div>" end ] end # Take care of the special case where class has parent for which we have no docs. # In that case the "extends" property exists but "superclasses" is empty. # We still create the tree, but without links in it. def render_tree return if !@cls[:extends] || @cls[:extends] == "Object" return [ "<h4>Hierarchy</h4>", render_class_tree(@cls[:superclasses] + [@cls[:name]]) ] end def render_class_tree(classes, i=0) return "" if classes.length <= i name = classes[i] return [ "<div class='subclass #{i == 0 ? 'first-child' : ''}'>", classes.length-1 == i ? "<strong>#{name}</strong>" : (name.exists? ? render_link(name) : name), render_class_tree(classes, i+1), "</div>", ] end def render_link(cls_name, member=nil) id = member ? cls_name + "-" + member[:id] : cls_name label = member ? cls_name + "." + member[:name] : cls_name return "<a href='#!/api/#{id}' rel='#{id}' class='docClass'>#{label}</a>" end def render_all_sections sections = [ {:type => :property, :title => "Properties"}, {:type => :method, :title => "Methods"}, {:type => :event, :title => "Events"}, {:type => :css_var, :title => "CSS Variables"}, {:type => :css_mixin, :title => "CSS Mixins"}, ] render_configs_section + sections.map {|sec| render_section(sec) } end def render_configs_section configs = @cls[:members][:cfg] + @cls[:statics][:cfg] if configs.length > 0 required, optional = configs.partition {|c| c[:meta][:required] } return [ "<div class='members-section'>", required.length == 0 ? "<div class='definedBy'>Defined By</div>" : "", "<h3 class='members-title icon-cfg'>Config options</h3>", render_subsection(required, "Required Config options"), render_subsection(optional, required.length > 0 ? "Optional Config options" : nil), "</div>", ] else return [] end end def render_section(sec) members = @cls[:members][sec[:type]] statics = @cls[:statics][sec[:type]] # Skip rendering empty sections if members.length > 0 || statics.length > 0 return [ "<div class='members-section'>", statics.length == 0 ? "<div class='definedBy'>Defined By</div>" : "", "<h3 class='members-title icon-#{sec[:type]}'>#{sec[:title]}</h3>", render_subsection(members, statics.length > 0 ? "Instance #{sec[:title]}" : nil), render_subsection(statics, "Static #{sec[:title]}"), "</div>", ] else return [] end end def render_subsection(members, title) return if members.length == 0 index = 0 return [ "<div class='subsection'>", title ? "<div class='definedBy'>Defined By</div><h4 class='members-subtitle'>#{title}</h3>" : "", members.map {|m| index += 1; render_member(m, index == 1) }, "</div>", ] end def render_member(m, is_first) # use classname "first-child" when it's first member in its category first_child = is_first ? "first-child" : "" # shorthand to owner class owner = m[:owner] # is this method inherited from parent? inherited = (owner != @cls[:name]) return [ "<div id='#{m[:id]}' class='member #{first_child} #{inherited ? 'inherited' : 'not-inherited'}'>", # leftmost column: expand button "<a href='#' class='side expandable'>", "<span> </span>", "</a>", # member name and type + link to owner class and source "<div class='title'>", "<div class='meta'>", inherited ? "<a href='#!/api/#{owner}' rel='#{owner}' class='defined-in docClass'>#{owner}</a>" : "<span class='defined-in' rel='#{owner}'>#{owner}</span>", "<br/>", @opts.source ? "<a href='source/#{m[:files][0][:href]}' target='_blank' class='view-source'>view source</a>" : "", "</div>", # method params signature or property type signature render_signature(m), "</div>", # short and long descriptions "<div class='description'>", "<div class='short'>", m[:shortDoc] ? m[:shortDoc] : m[:doc], "</div>", "<div class='long'>", render_long_doc(m), "</div>", "</div>", "</div>", ] end def render_signature(m) expandable = m[:shortDoc] ? "expandable" : "not-expandable" name = m[:name] before = "" if m[:tagname] == :method && m[:name] == "constructor" before = "<strong class='new-keyword'>new</strong>" name = @cls[:name] end if m[:tagname] == :cfg || m[:tagname] == :property || m[:tagname] == :css_var params = "<span> : #{m[:html_type]}</span>" else ps = m[:params].map {|p| render_short_param(p) }.join(", ") params = "( <span class='pre'>#{ps}</span> )" if m[:tagname] == :method && m[:return][:type] != "undefined" params += " : " + m[:return][:html_type] end end after = "" MetaTagRegistry.instance.signatures.each do |s| after += "<strong class='#{s[:key]} signature'>#{s[:long]}</strong>" if m[:meta][s[:key]] end uri = "#!/api/#{m[:owner]}-#{m[:id]}" return [ before, "<a href='#{uri}' class='name #{expandable}'>#{name}</a>", params, after ] end def render_short_param(param) p = param[:html_type] + " " + param[:name] return param[:optional] ? "["+p+"]" : p end def render_long_doc(m) doc = [] doc << render_meta_data(m[:html_meta], :top) doc << m[:doc] if m[:default] && m[:default] != "undefined" doc << "<p>Defaults to: <code>" + HTML.escape(m[:default]) + "</code></p>" end doc << render_meta_data(m[:html_meta], :bottom) doc << render_params_and_return(m) if m[:overrides] overrides = m[:overrides].map {|o| render_link(o[:owner], o) }.join(", ") doc << "<p>Overrides: #{overrides}</p>" end doc end # Handles both rendering of method parameters and return value. # Plus the rendering of object properties, which could also be # functions in which case they too will be rendered with # parameters and return value. def render_params_and_return(item) doc = [] if item[:params] && item[:params].length > 0 params = item[:params] elsif item[:properties] && item[:properties].length > 0 params = item[:properties] # If the name of last property is "return" # remove it from params list and handle it separately afterwards if params.last[:name] == "return" ret = params.last params = params.slice(0, params.length-1) end end if params if item[:type] == "Function" || item[:tagname] == :method || item[:tagname] == :event doc << '<h3 class="pa">Parameters</h3>' end doc << [ "<ul>", params.map {|p| render_long_param(p) }, "</ul>", ] end if item[:return] doc << render_return(item[:return]) elsif ret doc << render_return(ret) end if item[:throws] doc << render_throws(item[:throws]) end doc end def render_long_param(p) return [ "<li>", "<span class='pre'>#{p[:name]}</span> : ", p[:html_type], p[:optional] ? " (optional)" : "", "<div class='sub-desc'>", p[:doc], p[:default] ? "<p>Defaults to: <code>#{HTML.escape(p[:default])}</code></p>" : "", p[:properties] && p[:properties].length > 0 ? render_params_and_return(p) : "", "</div>", "</li>", ] end def render_return(ret) return if ret[:type] == "undefined" return [ "<h3 class='pa'>Returns</h3>", "<ul>", "<li>", "<span class='pre'>#{ret[:html_type]}</span>", "<div class='sub-desc'>", ret[:doc], ret[:properties] && ret[:properties].length > 0 ? render_params_and_return(ret) : "", "</div>", "</li>", "</ul>", ] end def render_throws(throws) return [ "<h3 class='pa'>Throws</h3>", "<ul>", throws.map do |thr| [ "<li>", "<span class='pre'>#{thr[:html_type]}</span>", "<div class='sub-desc'>#{thr[:doc]}</div>", "</li>", ] end, "</ul>", ] end end end