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