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 [
"
",
render_sidebar,
"
",
render_meta_data(@cls[:html_meta], :top),
render_private_class_notice,
@cls[:doc],
render_enum_class_notice,
render_meta_data(@cls[:html_meta], :bottom),
"
",
"
",
render_all_sections,
"
",
"
",
].flatten.compact.join
end
def render_private_class_notice
return if !@cls[:private]
return [
"NOTE ",
"This is a private utility class for internal use by the framework. ",
"Don't rely on its existence.
",
]
end
def render_enum_class_notice
return if !@cls[:enum]
if @cls[:enum][:type] == "String"
first = @cls[:members][:property][0] || {:name => 'foo', :default => '"foo"'}
[
"ENUM: ",
"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]}.
",
]
else
[
"ENUM: ",
"This enumeration defines a set of #{@cls[:enum][:type]} values.
",
]
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 ['', items, '
']
else
return nil
end
end
def render_alternate_class_names
return if @cls[:alternateClassNames].length == 0
return [
"Alternate names
",
@cls[:alternateClassNames].sort.map {|name| "#{name}
" },
]
end
def render_dependencies(type, title)
return if !@cls[type] || @cls[type].length == 0
return [
"#{title}
",
@cls[type].sort.map {|name| "#{name.exists? ? render_link(name) : name}
" },
]
end
def render_files
return if @cls[:files].length == 0 || @cls[:files][0][:filename] == ""
return [
"Files
",
@cls[:files].map do |file|
url = "source/" + file[:href]
title = File.basename(file[:filename])
""
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 [
"Hierarchy
",
render_class_tree(@cls[:superclasses] + [@cls[:name]])
]
end
def render_class_tree(classes, i=0)
return "" if classes.length <= i
name = classes[i]
return [
"",
classes.length-1 == i ? "#{name}" : (name.exists? ? render_link(name) : name),
render_class_tree(classes, i+1),
"
",
]
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 "#{label}"
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 [
"",
required.length == 0 ? "
Defined By
" : "",
"
Config options
",
render_subsection(required, "Required Config options"),
render_subsection(optional, required.length > 0 ? "Optional Config options" : nil),
"
",
]
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 [
"",
statics.length == 0 ? "
Defined By
" : "",
"
#{sec[:title]}
",
render_subsection(members, statics.length > 0 ? "Instance #{sec[:title]}" : nil),
render_subsection(statics, "Static #{sec[:title]}"),
"
",
]
else
return []
end
end
def render_subsection(members, title)
return if members.length == 0
index = 0
return [
"",
title ? "
Defined By
#{title}" : "",
members.map {|m| index += 1; render_member(m, index == 1) },
"
",
]
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 [
"",
# leftmost column: expand button
"
",
" ",
"",
# member name and type + link to owner class and source
"
",
"
",
# method params signature or property type signature
render_signature(m),
"
",
# short and long descriptions
"
",
"
",
m[:shortDoc] ? m[:shortDoc] : m[:doc],
"
",
"
",
render_long_doc(m),
"
",
"
",
"
",
]
end
def render_signature(m)
expandable = m[:shortDoc] ? "expandable" : "not-expandable"
name = m[:name]
before = ""
if m[:tagname] == :method && m[:name] == "constructor"
before = "new"
name = @cls[:name]
end
if m[:tagname] == :cfg || m[:tagname] == :property || m[:tagname] == :css_var
params = " : #{m[:html_type]}"
else
ps = m[:params].map {|p| render_short_param(p) }.join(", ")
params = "( #{ps} )"
if m[:tagname] == :method && m[:return][:type] != "undefined"
params += " : " + m[:return][:html_type]
end
end
after = ""
MetaTagRegistry.instance.signatures.each do |s|
after += "#{s[:long]}" if m[:meta][s[:key]]
end
uri = "#!/api/#{m[:owner]}-#{m[:id]}"
return [
before,
"#{name}",
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 << "Defaults to: " + HTML.escape(m[:default]) + "
"
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 << "Overrides: #{overrides}
"
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 << 'Parameters
'
end
doc << [
"",
params.map {|p| render_long_param(p) },
"
",
]
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 [
"",
"#{p[:name]} : ",
p[:html_type],
p[:optional] ? " (optional)" : "",
"",
p[:doc],
p[:default] ? "
Defaults to: #{HTML.escape(p[:default])}
" : "",
p[:properties] && p[:properties].length > 0 ? render_params_and_return(p) : "",
"
",
"",
]
end
def render_return(ret)
return if ret[:type] == "undefined"
return [
"Returns
",
"",
"- ",
"#{ret[:html_type]}",
"
",
ret[:doc],
ret[:properties] && ret[:properties].length > 0 ? render_params_and_return(ret) : "",
"
",
" ",
"
",
]
end
def render_throws(throws)
return [
"Throws
",
"",
throws.map do |thr|
[
"- ",
"#{thr[:html_type]}",
"
#{thr[:doc]}
",
" ",
]
end,
"
",
]
end
end
end