# -*- coding: utf-8 -*- require 'rubygems' require 'strscan' require 'jsduck/markdown' require 'jsduck/logger' require 'jsduck/inline_img' require 'jsduck/inline_video' require 'jsduck/html' 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_img = InlineImg.new(opts) @inline_video = InlineVideo.new(opts) @link_tpl = opts[:link_tpl] || '%a' @link_re = /\{@link\s+(\S*?)(?:\s+(.+?))?\}/m @example_annotation_re = /
\s*@example( +[^\n]*)?\s+/m
    end

    # Sets base path to prefix images from {@img} tags.
    def img_path=(path)
      @inline_img.base_path = path
    end

    # Returns list of all image paths gathered from {@img} tags.
    def images
      @inline_img.images
    end

    # Replaces {@link} and {@img} tags, auto-generates links for
    # recognized classnames.
    #
    # Replaces {@link Class#member link text} in given string with
    # HTML from @link_tpl.
    #
    # Replaces {@img path/to/image.jpg Alt text} with HTML from @img_tpl.
    #
    # Adds 'inline-example' class to code examples beginning with @example.
    #
    # Additionally replaces strings recognized as ClassNames or
    # #members with links to these classes or members.  So one doesn't
    # even need to use the @link tag to create a link.
    def replace(input)
      s = StringScanner.new(input)
      out = ""

      # Keep track of the nesting level of  tags. We're not
      # auto-detecting class names when inside . Normally links
      # shouldn't be nested, but just to be extra safe.
      open_a_tags = 0

      while !s.eos? do
        if s.check(@link_re)
          out += replace_link_tag(s.scan(@link_re))
        elsif substitute = @inline_img.replace(s)
          out += substitute
        elsif substitute = @inline_video.replace(s)
          out += substitute
        elsif s.check(/[{]/)
          # There might still be "{" that doesn't begin {@link} or {@img} - ignore it
          out += s.scan(/[{]/)
        elsif s.check(@example_annotation_re)
          # Match possible classnames following @example and add them
          # as CSS classes inside 
 element.
          s.scan(@example_annotation_re) =~ @example_annotation_re
          css_classes = ($1 || "").strip
          out += "
"
        elsif s.check(/ tags.
          open_a_tags += 1
          out += s.scan_until(/>|\Z/)
        elsif s.check(/<\/a>/)
          #  closed, auto-detection may continue when no more  tags open.
          open_a_tags -= 1
          out += s.scan(/<\/a>/)
        elsif s.check(/|\Z/)
        else
          # 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)
        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.instance.warn(:link, "#{input} links to non-existing class", file, line)
          return text
        elsif member
          ms = get_members(cls, member, type, static)
          if ms.length == 0
            Logger.instance.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].to_s }.join(", ")
              Logger.instance.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.instance.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, 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, 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, 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.instance.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].full_name
      # prepend type name to member name
      member = member && get_matching_member(cls, member, type, static)

      @link_tpl.gsub(/(%[\w#-])/) do
        case $1
        when '%c'
          cls
        when '%m'
          member ? member[:id] : ""
        when '%#'
          member ? "#" : ""
        when '%-'
          member ? "-" : ""
        when '%a'
          HTML.escape(anchor_text||"")
        else
          $1
        end
      end
    end

    def get_matching_member(cls, member, type=nil, static=nil)
      ms = get_members(cls, member, type, static).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 get_members(cls, member, type=nil, static=nil)
      @relations[cls] ? @relations[cls].get_members(member, type, static) : []
    end

    # Formats doc-comment for placement into HTML.
    # Renders it with Markdown-formatter and replaces @link-s.
    def format(input)
      # In ExtJS source "
" is often at the end of paragraph, not
      # on its own line.  But in that case RDiscount doesn't recognize
      # it as the beginning of 
-block and goes on parsing it as
      # normal Markdown, which often causes nested 
-blocks.
      #
      # To prevent this, we always add extra newline before 
.
      input.gsub!(/([^\n])
/, "\\1\n
")

      # But we remove trailing newline after 
 to prevent
      # code-blocks beginning with empty line.
      input.gsub!(/
()?\n?/, "
\\1")

      replace(JsDuck::Markdown.to_html(input))
    end

    # Shortens text
    #
    # 116 chars is also where ext-doc makes its cut, but unlike
    # ext-doc we only make the cut when there's more than 120 chars.
    #
    # This way we don't get stupid expansions like:
    #
    #   Blah blah blah some text...
    #
    # expanding to:
    #
    #   Blah blah blah some text.
    #
    def shorten(input)
      sent = first_sentence(HTML.strip_tags(input).strip)
      # Use u-modifier to correctly count multi-byte characters
      chars = sent.scan(/./mu)
      if chars.length > @max_length
        chars[0..(@max_length-4)].join + "..."
      else
        sent + " ..."
      end
    end

    def first_sentence(str)
      str.sub(/\A(.+?(\.|。))\s.*\Z/mu, "\\1")
    end

    # Returns true when input should get shortened.
    def too_long?(input)
      stripped = HTML.strip_tags(input).strip
      # for sentence v/s full - compare byte length
      # for full v/s max - compare char length
      first_sentence(stripped).length < stripped.length || stripped.scan(/./mu).length > @max_length
    end

  end

end