diff --git a/lib/jsduck/doc_formatter.rb b/lib/jsduck/doc_formatter.rb index 7677d6bd95ee550d6cb6ea8f49d5a4b390ed8fcb..bfa165822ae7d5000010b5885c929b51ba79325b 100644 --- a/lib/jsduck/doc_formatter.rb +++ b/lib/jsduck/doc_formatter.rb @@ -3,6 +3,7 @@ require 'rubygems' require 'strscan' require 'rdiscount' require 'jsduck/logger' +require 'jsduck/inline/link' require 'jsduck/inline/img' require 'jsduck/inline/video' require 'jsduck/util/html' @@ -11,50 +12,18 @@ module JsDuck # Formats doc-comments class DocFormatter - # Template HTML that replaces {@link Class#member anchor text}. - # Can contain placeholders: - # - # %c - full class name (e.g. "Ext.Panel") - # %m - class member name prefixed with member type (e.g. "method-urlEncode") - # %# - inserts "#" if member name present - # %- - inserts "-" if member name present - # %a - anchor text for link - # - # Default value: '%a' - attr_accessor :link_tpl - - # Sets up instance to work in context of particular class, so - # that when {@link #blah} is encountered it knows that - # Context#blah is meant. - attr_accessor :class_context - - # Sets up instance to work in context of particular doc object. - # Used for error reporting. - attr_accessor :doc_context - # Maximum length for text that doesn't get shortened, defaults to 120 attr_accessor :max_length - # JsDuck::Relations for looking up class names. - # - # When auto-creating class links from CamelCased names found from - # text, we check the relations object to see if a class with that - # name actually exists. - attr_accessor :relations - def initialize(relations={}, opts={}) - @class_context = "" - @doc_context = {} @max_length = 120 - @relations = relations @images = [] + @inline_link = Inline::Link.new(opts) + @inline_link.relations = relations @inline_img = Inline::Img.new(opts) @inline_video = Inline::Video.new(opts) - @link_tpl = opts[:link_tpl] || '%a' - @link_re = /\{@link\s+(\S*?)(?:\s+(.+?))?\}/m - @example_annotation_re = /
\s*@example( +[^\n]*)?\s+/m
end
@@ -68,6 +37,34 @@ module JsDuck
@inline_img.images
end
+ # Sets up instance to work in context of particular class, so
+ # that when {@link #blah} is encountered it knows that
+ # Context#blah is meant.
+ def class_context=(cls)
+ @inline_link.class_context = cls
+ end
+
+ # Sets up instance to work in context of particular doc object.
+ # Used for error reporting.
+ def doc_context=(doc)
+ @inline_video.doc_context = doc
+ @inline_link.doc_context = doc
+ end
+
+ # Returns the current documentation context
+ def doc_context
+ @inline_link.doc_context
+ end
+
+ # JsDuck::Relations for looking up class names.
+ #
+ # When auto-creating class links from CamelCased names found from
+ # text, we check the relations object to see if a class with that
+ # name actually exists.
+ def relations=(relations)
+ @inline_link.relations = relations
+ end
+
# Replaces {@link} and {@img} tags, auto-generates links for
# recognized classnames.
#
@@ -91,11 +88,11 @@ module JsDuck
open_a_tags = 0
while !s.eos? do
- if s.check(@link_re)
- out += replace_link_tag(s.scan(@link_re))
+ if substitute = @inline_link.replace(s)
+ out += substitute
elsif substitute = @inline_img.replace(s)
out += substitute
- elsif substitute = @inline_video.replace(s, @doc_context)
+ elsif substitute = @inline_video.replace(s)
out += substitute
elsif s.check(/[{]/)
# There might still be "{" that doesn't begin {@link} or {@img} - ignore it
@@ -121,178 +118,15 @@ module JsDuck
# Replace class names in the following text up to next "<" or "{"
# but only when we're not inside ...
text = s.scan(/[^{<]+/)
- out += open_a_tags > 0 ? text : create_magic_links(text)
+ out += open_a_tags > 0 ? text : @inline_link.create_magic_links(text)
end
end
out
end
- def replace_link_tag(input)
- input.sub(@link_re) do
- target = $1
- text = $2
- if target =~ /^(.*)#(static-)?(?:(cfg|property|method|event|css_var|css_mixin)-)?(.*)$/
- cls = $1.empty? ? @class_context : $1
- static = $2 ? true : nil
- type = $3 ? $3.intern : nil
- member = $4
- else
- cls = target
- static = nil
- type = false
- member = false
- end
-
- # Construct link text
- if text
- text = text
- elsif member
- text = (cls == @class_context) ? member : (cls + "." + member)
- else
- text = cls
- end
-
- file = @doc_context[:filename]
- line = @doc_context[:linenr]
- if !@relations[cls]
- Logger.warn(:link, "#{input} links to non-existing class", file, line)
- return text
- elsif member
- ms = find_members(cls, {:name => member, :tagname => type, :static => static})
- if ms.length == 0
- Logger.warn(:link, "#{input} links to non-existing member", file, line)
- return text
- end
-
- if ms.length > 1
- # When multiple public members, see if there remains just
- # one when we ignore the static members. If there's more,
- # report ambiguity. If there's only static members, also
- # report ambiguity.
- instance_ms = ms.find_all {|m| !m[:meta][:static] }
- if instance_ms.length > 1
- alternatives = instance_ms.map {|m| "#{m[:tagname]} in #{m[:owner]}" }.join(", ")
- Logger.warn(:link_ambiguous, "#{input} is ambiguous: "+alternatives, file, line)
- elsif instance_ms.length == 0
- static_ms = ms.find_all {|m| m[:meta][:static] }
- alternatives = static_ms.map {|m| "static " + m[:tagname].to_s }.join(", ")
- Logger.warn(:link_ambiguous, "#{input} is ambiguous: "+alternatives, file, line)
- end
- end
-
- return link(cls, member, text, type, static)
- else
- return link(cls, false, text)
- end
- end
- end
-
- # Looks input text for patterns like:
- #
- # My.ClassName
- # MyClass#method
- # #someProperty
- #
- # and converts them to links, as if they were surrounded with
- # {@link} tag. One notable exception is that Foo is not created to
- # link, even when Foo class exists, but Foo.Bar is. This is to
- # avoid turning normal words into links. For example:
- #
- # Math involves a lot of numbers. Ext JS is a JavaScript framework.
- #
- # In these sentences we don't want to link "Math" and "Ext" to the
- # corresponding JS classes. And that's why we auto-link only
- # class names containing a dot "."
- #
- def create_magic_links(input)
- cls_re = "([A-Z][A-Za-z0-9.]*[A-Za-z0-9])"
- member_re = "(?:#([A-Za-z0-9]+))"
-
- input.gsub(/\b#{cls_re}#{member_re}?\b|#{member_re}\b/m) do
- replace_magic_link($1, $2 || $3)
- end
- end
-
- def replace_magic_link(cls, member)
- if cls && member
- if @relations[cls] && get_matching_member(cls, {:name => member})
- return link(cls, member, cls+"."+member)
- else
- warn_magic_link("#{cls}##{member} links to non-existing " + (@relations[cls] ? "member" : "class"))
- end
- elsif cls && cls =~ /\./
- if @relations[cls]
- return link(cls, nil, cls)
- else
- cls2, member2 = split_to_cls_and_member(cls)
- if @relations[cls2] && get_matching_member(cls2, {:name => member2})
- return link(cls2, member2, cls2+"."+member2)
- elsif cls =~ /\.(js|css|html|php)\Z/
- # Ignore common filenames
- else
- warn_magic_link("#{cls} links to non-existing class")
- end
- end
- elsif !cls && member
- if get_matching_member(@class_context, {:name => member})
- return link(@class_context, member, member)
- elsif member =~ /\A([A-F0-9]{3}|[A-F0-9]{6})\Z/i || member =~ /\A[0-9]/
- # Ignore HEX color codes and
- # member names beginning with number
- else
- warn_magic_link("##{member} links to non-existing member")
- end
- end
-
- return "#{cls}#{member ? '#' : ''}#{member}"
- end
-
- def split_to_cls_and_member(str)
- parts = str.split(/\./)
- return [parts.slice(0, parts.length-1).join("."), parts.last]
- end
-
- def warn_magic_link(msg)
- Logger.warn(:link_auto, msg, @doc_context[:filename], @doc_context[:linenr])
- end
-
- # applies the link template
+ # Creates a link based on the link template.
def link(cls, member, anchor_text, type=nil, static=nil)
- # Use the canonical class name for link (not some alternateClassName)
- cls = @relations[cls][:name]
- # prepend type name to member name
- member = member && get_matching_member(cls, {:name => member, :tagname => type, :static => static})
-
- @link_tpl.gsub(/(%[\w#-])/) do
- case $1
- when '%c'
- cls
- when '%m'
- member ? member[:id] : ""
- when '%#'
- member ? "#" : ""
- when '%-'
- member ? "-" : ""
- when '%a'
- Util::HTML.escape(anchor_text||"")
- else
- $1
- end
- end
- end
-
- def get_matching_member(cls, query)
- ms = find_members(cls, query).find_all {|m| !m[:private] }
- if ms.length > 1
- instance_ms = ms.find_all {|m| !m[:meta][:static] }
- instance_ms.length > 0 ? instance_ms[0] : ms.find_all {|m| m[:meta][:static] }[0]
- else
- ms[0]
- end
- end
-
- def find_members(cls, query)
- @relations[cls] ? @relations[cls].find_members(query) : []
+ @inline_link.link(cls, member, anchor_text, type, static)
end
# Formats doc-comment for placement into HTML.
diff --git a/lib/jsduck/inline/link.rb b/lib/jsduck/inline/link.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8c0d4fb9f591aa08c972b803bdaec98c3f0b0368
--- /dev/null
+++ b/lib/jsduck/inline/link.rb
@@ -0,0 +1,227 @@
+require 'jsduck/util/html'
+require 'jsduck/logger'
+
+module JsDuck
+ module Inline
+
+ # Implementation of inline tag {@link}
+ #
+ # It also takes care of the auto-detection of links in text
+ # through the #create_magic_links method.
+ class Link
+ # Sets up instance to work in context of particular class, so
+ # that when {@link #blah} is encountered it knows that
+ # Context#blah is meant.
+ attr_accessor :class_context
+
+ # Sets up instance to work in context of particular doc object.
+ # Used for error reporting.
+ attr_accessor :doc_context
+
+ # JsDuck::Relations for looking up class names.
+ #
+ # When auto-creating class links from CamelCased names found from
+ # text, we check the relations object to see if a class with that
+ # name actually exists.
+ attr_accessor :relations
+
+ def initialize(opts={})
+ @class_context = ""
+ @doc_context = {}
+ @relations = {}
+
+ # Template HTML that replaces {@link Class#member anchor text}.
+ # Can contain placeholders:
+ #
+ # %c - full class name (e.g. "Ext.Panel")
+ # %m - class member name prefixed with member type (e.g. "method-urlEncode")
+ # %# - inserts "#" if member name present
+ # %- - inserts "-" if member name present
+ # %a - anchor text for link
+ @tpl = opts[:link_tpl] || '%a'
+
+ @re = /\{@link\s+(\S*?)(?:\s+(.+?))?\}/m
+ end
+
+ # Takes StringScanner instance.
+ #
+ # Looks for inline tag at the current scan pointer position, when
+ # found, moves scan pointer forward and performs the apporpriate
+ # replacement.
+ def replace(input)
+ if input.check(@re)
+ input.scan(@re).sub(@re) { apply_tpl($1, $2, $0) }
+ else
+ false
+ end
+ end
+
+ # applies the link template
+ def apply_tpl(target, text, full_link)
+ if target =~ /^(.*)#(static-)?(?:(cfg|property|method|event|css_var|css_mixin)-)?(.*)$/
+ cls = $1.empty? ? @class_context : $1
+ static = $2 ? true : nil
+ type = $3 ? $3.intern : nil
+ member = $4
+ else
+ cls = target
+ static = nil
+ type = false
+ member = false
+ end
+
+ # Construct link text
+ if text
+ text = text
+ elsif member
+ text = (cls == @class_context) ? member : (cls + "." + member)
+ else
+ text = cls
+ end
+
+ file = @doc_context[:filename]
+ line = @doc_context[:linenr]
+ if !@relations[cls]
+ Logger.warn(:link, "#{full_link} links to non-existing class", file, line)
+ return text
+ elsif member
+ ms = find_members(cls, {:name => member, :tagname => type, :static => static})
+ if ms.length == 0
+ Logger.warn(:link, "#{full_link} links to non-existing member", file, line)
+ return text
+ end
+
+ if ms.length > 1
+ # When multiple public members, see if there remains just
+ # one when we ignore the static members. If there's more,
+ # report ambiguity. If there's only static members, also
+ # report ambiguity.
+ instance_ms = ms.find_all {|m| !m[:meta][:static] }
+ if instance_ms.length > 1
+ alternatives = instance_ms.map {|m| "#{m[:tagname]} in #{m[:owner]}" }.join(", ")
+ Logger.warn(:link_ambiguous, "#{full_link} is ambiguous: "+alternatives, file, line)
+ elsif instance_ms.length == 0
+ static_ms = ms.find_all {|m| m[:meta][:static] }
+ alternatives = static_ms.map {|m| "static " + m[:tagname].to_s }.join(", ")
+ Logger.warn(:link_ambiguous, "#{full_link} is ambiguous: "+alternatives, file, line)
+ end
+ end
+
+ return link(cls, member, text, type, static)
+ else
+ return link(cls, false, text)
+ end
+ end
+
+ # Looks input text for patterns like:
+ #
+ # My.ClassName
+ # MyClass#method
+ # #someProperty
+ #
+ # and converts them to links, as if they were surrounded with
+ # {@link} tag. One notable exception is that Foo is not created to
+ # link, even when Foo class exists, but Foo.Bar is. This is to
+ # avoid turning normal words into links. For example:
+ #
+ # Math involves a lot of numbers. Ext JS is a JavaScript framework.
+ #
+ # In these sentences we don't want to link "Math" and "Ext" to the
+ # corresponding JS classes. And that's why we auto-link only
+ # class names containing a dot "."
+ #
+ def create_magic_links(input)
+ cls_re = "([A-Z][A-Za-z0-9.]*[A-Za-z0-9])"
+ member_re = "(?:#([A-Za-z0-9]+))"
+
+ input.gsub(/\b#{cls_re}#{member_re}?\b|#{member_re}\b/m) do
+ replace_magic_link($1, $2 || $3)
+ end
+ end
+
+ def replace_magic_link(cls, member)
+ if cls && member
+ if @relations[cls] && get_matching_member(cls, {:name => member})
+ return link(cls, member, cls+"."+member)
+ else
+ warn_magic_link("#{cls}##{member} links to non-existing " + (@relations[cls] ? "member" : "class"))
+ end
+ elsif cls && cls =~ /\./
+ if @relations[cls]
+ return link(cls, nil, cls)
+ else
+ cls2, member2 = split_to_cls_and_member(cls)
+ if @relations[cls2] && get_matching_member(cls2, {:name => member2})
+ return link(cls2, member2, cls2+"."+member2)
+ elsif cls =~ /\.(js|css|html|php)\Z/
+ # Ignore common filenames
+ else
+ warn_magic_link("#{cls} links to non-existing class")
+ end
+ end
+ elsif !cls && member
+ if get_matching_member(@class_context, {:name => member})
+ return link(@class_context, member, member)
+ elsif member =~ /\A([A-F0-9]{3}|[A-F0-9]{6})\Z/i || member =~ /\A[0-9]/
+ # Ignore HEX color codes and
+ # member names beginning with number
+ else
+ warn_magic_link("##{member} links to non-existing member")
+ end
+ end
+
+ return "#{cls}#{member ? '#' : ''}#{member}"
+ end
+
+ def split_to_cls_and_member(str)
+ parts = str.split(/\./)
+ return [parts.slice(0, parts.length-1).join("."), parts.last]
+ end
+
+ def warn_magic_link(msg)
+ Logger.warn(:link_auto, msg, @doc_context[:filename], @doc_context[:linenr])
+ end
+
+ # applies the link template
+ def link(cls, member, anchor_text, type=nil, static=nil)
+ # Use the canonical class name for link (not some alternateClassName)
+ cls = @relations[cls][:name]
+ # prepend type name to member name
+ member = member && get_matching_member(cls, {:name => member, :tagname => type, :static => static})
+
+ @tpl.gsub(/(%[\w#-])/) do
+ case $1
+ when '%c'
+ cls
+ when '%m'
+ member ? member[:id] : ""
+ when '%#'
+ member ? "#" : ""
+ when '%-'
+ member ? "-" : ""
+ when '%a'
+ Util::HTML.escape(anchor_text||"")
+ else
+ $1
+ end
+ end
+ end
+
+ def get_matching_member(cls, query)
+ ms = find_members(cls, query).find_all {|m| !m[:private] }
+ if ms.length > 1
+ instance_ms = ms.find_all {|m| !m[:meta][:static] }
+ instance_ms.length > 0 ? instance_ms[0] : ms.find_all {|m| m[:meta][:static] }[0]
+ else
+ ms[0]
+ end
+ end
+
+ def find_members(cls, query)
+ @relations[cls] ? @relations[cls].find_members(query) : []
+ end
+
+ end
+
+ end
+end
diff --git a/lib/jsduck/inline/video.rb b/lib/jsduck/inline/video.rb
index 75dc162e0165fd9b77ad1188b288f6dd61ea775c..a9ebdf23c0a9788031ca2b95bdcd78f30349ec0d 100644
--- a/lib/jsduck/inline/video.rb
+++ b/lib/jsduck/inline/video.rb
@@ -6,7 +6,13 @@ module JsDuck
# Implementation of inline tag {@video}
class Video
+ # Sets up instance to work in context of particular doc object.
+ # Used for error reporting.
+ attr_accessor :doc_context
+
def initialize(opts={})
+ @doc_context = {}
+
@templates = {
"html5" => '',
"vimeo" => [
@@ -29,17 +35,18 @@ module JsDuck
# Looks for inline tag at the current scan pointer position, when
# found, moves scan pointer forward and performs the apporpriate
# replacement.
- def replace(input, doc_context)
+ def replace(input)
if input.check(@re)
- input.scan(@re).sub(@re) { apply_tpl($1, $2, $3, doc_context) }
+ input.scan(@re).sub(@re) { apply_tpl($1, $2, $3) }
else
false
end
end
# applies the video template of the specified type
- def apply_tpl(type, url, alt_text, ctx)
+ def apply_tpl(type, url, alt_text)
unless @templates.has_key?(type)
+ ctx = @doc_context
Logger.warn(nil, "Unknown video type #{type}", ctx[:filename], ctx[:linenr])
end