Commit fc468f61 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Move doc-comments parsing classes to doc/ subdir.

Another cleanup.
parent 791bdbbc
Loading
Loading
Loading
Loading

lib/jsduck/doc/ast.rb

0 → 100644
+194 −0
Original line number Diff line number Diff line
require 'jsduck/tag_registry'
require 'jsduck/doc/subproperties'
require 'jsduck/logger'

module JsDuck
  module Doc

    # Detects docs info directly from comment.
    class Ast
      # Allow passing in filename and line for error reporting
      attr_accessor :filename
      attr_accessor :linenr

      def initialize
        @filename = ""
        @linenr = 0
      end

      # Given tagname and array of tags from DocParser, produces docs
      # of the type determined by tagname.
      def detect(tagname, docs, doc_map)
        case tagname
        when :class
          create_class(docs, doc_map)
        when :event
          create_event(docs, doc_map)
        when :method
          create_method(docs, doc_map)
        when :cfg
          create_cfg(docs, doc_map)
        when :property
          create_property(docs, doc_map)
        when :css_var
          create_css_var(docs, doc_map)
        when :css_mixin
          create_css_mixin(docs, doc_map)
        end
      end

      private

      def create_class(docs, doc_map)
        return add_shared({
            :tagname => :class,
            :name => detect_name(:class, doc_map),
            :doc => detect_doc(docs),
          }, doc_map)
      end

      def create_method(docs, doc_map)
        return add_shared({
            :tagname => :method,
            :name => detect_name(:method, doc_map),
            :doc => detect_doc(docs),
            :params => detect_params(doc_map),
            :return => detect_return(doc_map),
          }, doc_map)
      end

      def create_event(docs, doc_map)
        return add_shared({
            :tagname => :event,
            :name => detect_name(:event, doc_map),
            :doc => detect_doc(docs),
            :params => detect_params(doc_map),
          }, doc_map)
      end

      def create_cfg(docs, doc_map)
        return add_shared({
            :tagname => :cfg,
            :name => detect_name(:cfg, doc_map),
            :type => detect_type(:cfg, doc_map),
            :doc => detect_doc(docs),
            :default => detect_default(:cfg, doc_map),
            :properties => detect_subproperties(:cfg, docs),
          }, doc_map)
      end

      def create_property(docs, doc_map)
        return add_shared({
            :tagname => :property,
            :name => detect_name(:property, doc_map),
            :type => detect_type(:property, doc_map),
            :doc => detect_doc(docs),
            :default => detect_default(:property, doc_map),
            :properties => detect_subproperties(:property, docs),
          }, doc_map)
      end

      def create_css_var(docs, doc_map)
        return add_shared({
            :tagname => :css_var,
            :name => detect_name(:css_var, doc_map),
            :type => detect_type(:css_var, doc_map),
            :default => detect_default(:css_var, doc_map),
            :doc => detect_doc(docs),
          }, doc_map)
      end

      def create_css_mixin(docs, doc_map)
        return add_shared({
            :tagname => :css_mixin,
            :name => detect_name(:css_mixin, doc_map),
            :doc => detect_doc(docs),
            :params => detect_params(doc_map),
          }, doc_map)
      end

      # Detects properties common for each doc-object and adds them
      def add_shared(hash, doc_map)
        doc_map.each_pair do |key, value|
          if tag = TagRegistry.get_by_key(key)
            hash[key] = tag.process_doc(value)
          end
        end

        hash[:required] = true if detect_required(doc_map)

        return hash
      end

      def detect_name(tagname, doc_map)
        name = extract(doc_map, tagname, :name)
        if name
          name
        else
          doc_map[:constructor] ? "constructor" : nil
        end
      end

      def extract(doc_map, tagname, propname = nil)
        tag = doc_map[tagname] ? doc_map[tagname].first : nil
        if tag && propname
          tag[propname]
        else
          tag
        end
      end

      def detect_type(tagname, doc_map)
        extract(doc_map, tagname, :type) || extract(doc_map, :type, :type)
      end

      def detect_default(tagname, doc_map)
        extract(doc_map, tagname, :default)
      end

      def detect_required(doc_map)
        doc_map[:cfg] && doc_map[:cfg].first[:optional] == false
      end

      def detect_params(doc_map)
        nest_properties(doc_map[:param] || [])
      end

      def detect_subproperties(tagname, docs)
        prop_docs = docs.find_all {|tag| tag[:tagname] == tagname}
        prop_docs.length > 0 ? nest_properties(prop_docs)[0][:properties] : []
      end

      def nest_properties(raw_items)
        items, warnings = Doc::Subproperties.nest(raw_items)
        warnings.each {|msg| Logger.warn(:subproperty, msg, @filename, @linenr) }
        items
      end

      def detect_return(doc_map)
        has_return_tag = !!extract(doc_map, :return)
        ret = extract(doc_map, :return) || {}
        return {
          :type => ret[:type] || (has_return_tag ? "Object" : "undefined"),
          :name => ret[:name] || "return",
          :doc => ret[:doc] || "",
          :properties => doc_map[:return] ? detect_subproperties(:return, doc_map[:return]) : []
        }
      end

      # Combines :doc-s of most tags
      # Ignores tags that have doc comment themselves and subproperty tags
      def detect_doc(docs)
        ignore_tags = [:param, :return] + TagRegistry.multiliners.map {|t| t.key }
        doc_tags = docs.find_all { |tag| !ignore_tags.include?(tag[:tagname]) && !subproperty?(tag) }
        doc_tags.map { |tag| tag[:doc] }.compact.join(" ")
      end

      def subproperty?(tag)
        (tag[:tagname] == :cfg || tag[:tagname] == :property) && tag[:name] =~ /\./
      end

    end

  end
end
+40 −0
Original line number Diff line number Diff line
module JsDuck
  module Doc

    # A simple helper to extract doc comment contents.
    class Comment

      # Extracts content inside /** ... */
      def self.purify(input)
        result = []

        # We can have two types of lines:
        # - those beginning with *
        # - and those without it
        indent = nil
        input.each_line do |line|
          line.chomp!
          if line =~ /\A\s*\*\s?(.*)\Z/
            # When comment contains *-lines, switch indent-trimming off
            indent = 0
            result << $1
          elsif line =~ /\A\s*\Z/
            # pass-through empty lines
            result << line
          elsif indent == nil && line =~ /\A(\s*)(.*?\Z)/
            # When indent not measured, measure it and remember
            indent = $1.length
            result << $2
          else
            # Trim away indent if available
            result << line.sub(/\A\s{0,#{indent||0}}/, "")
          end
        end

        result.join("\n")
      end

    end

  end
end

lib/jsduck/doc/map.rb

0 → 100644
+23 −0
Original line number Diff line number Diff line
module JsDuck
  module Doc

    # Helper for building at-tags lookup table.
    class Map

      # Builds map of at-tags for quick lookup
      def self.build(docs)
        map = {}
        docs.each do |tag|
          if map[tag[:tagname]]
            map[tag[:tagname]] << tag
          else
            map[tag[:tagname]] = [tag]
          end
        end
        map
      end

    end

  end
end
+127 −0
Original line number Diff line number Diff line
require 'strscan'
require 'jsduck/doc/comment'
require 'jsduck/doc/scanner'
require 'jsduck/tag_registry'
require 'jsduck/logger'

module JsDuck
  module Doc

    # Parses doc-comment into array of @tags
    #
    # For each @tag it produces Hash like the following:
    #
    #     {
    #       :tagname => :cfg/:property/:type/:extends/...,
    #       :doc => "Some documentation for this tag",
    #       ...@tag specific stuff like :name, :type, and so on...
    #     }
    #
    # When doc-comment begins with comment, not preceded by @tag, then
    # the comment will be placed into Hash with :tagname => :default.
    #
    # Unrecognized @tags are left as is into documentation as if they
    # were normal text.
    #
    # @example, {@img}, {@link} and {@video} are parsed separately in
    # JsDuck::DocFormatter.
    #
    class Parser < Doc::Scanner
      def parse(input, filename="", linenr=0)
        @filename = filename
        @linenr = linenr
        @tags = []
        @input = StringScanner.new(Doc::Comment.purify(input))

        parse_loop

        clean_empty_docs
        clean_empty_default_tag
        @tags
      end

      # The parsing process can leave whitespace at the ends of
      # doc-strings, here we get rid of it.
      # Additionally null all empty docs.
      def clean_empty_docs
        @tags.each do |tag|
          tag[:doc].strip!
          tag[:doc] = nil if tag[:doc] == ""
        end
      end

      # Gets rid of empty default tag
      def clean_empty_default_tag
        if @tags.first && @tags.first[:tagname] == :default && !@tags.first[:doc]
          @tags.shift
        end
      end

      # The main loop of the DocParser
      def parse_loop
        add_tag({:tagname => :default, :doc => ""})

        while !@input.eos? do
          if look(/@/)
            parse_at_tag
          elsif look(/[^@]/)
            skip_to_next_at_tag
          end
        end
      end

      # Processes anything beginning with @-sign.
      #
      # - When @ is not followed by any word chars, do nothing.
      # - When it's one of the builtin tags, process it as such.
      # - When it's something else, print a warning.
      #
      def parse_at_tag
        match(/@/)
        name = look(/\w+/)

        if !name
          # ignore
        elsif tag = TagRegistry.get_by_pattern(name)
          match(/\w+/)

          tags = tag.parse(self)
          if tags.is_a?(Hash)
            add_tag(tags)
          elsif tags.is_a?(Array)
            tags.each {|t| add_tag(t) }
          end

          skip_white
        else
          Logger.warn(:tag, "Unsupported tag: @#{name}", @filename, @linenr)
          @current_tag[:doc] += "@"
        end
      end

      # Skips until the beginning of next @tag.
      #
      # There must be space before the next @tag - this ensures that we
      # don't detect tags inside "foo@example.com" or "{@link}".
      #
      # Also check that the @tag is not part of an indented code block -
      # in which case we also ignore the tag.
      def skip_to_next_at_tag
        @current_tag[:doc] += match(/[^@]+/)

        while look(/@/) && (!prev_char_is_whitespace? || indented_as_code?)
          @current_tag[:doc] += match(/@+[^@]+/)
        end
      end

      def prev_char_is_whitespace?
        @current_tag[:doc][-1,1] =~ /\s/
      end

      def indented_as_code?
        @current_tag[:doc] =~ /^ {4,}[^\n]*\Z/
      end
    end

  end
end
+87 −0
Original line number Diff line number Diff line
require 'jsduck/doc/standard_tag_parser'

module JsDuck
  module Doc

    # Abstract base class for parsing doc-comments.
    #
    # The methods of this class are to be called from implementations
    # of concrete @tags.  Although the @tag classes will get passed an
    # instance of Doc::Parser, only methods of Doc::Scanner should be
    # called by them.
    #
    class Scanner
      def initialize
        @ident_pattern = /[$\w-]+/
        @ident_chain_pattern = /[$\w-]+(\.[$\w-]+)*/

        @tags = []
        @input = nil # set to StringScanner in subclass
      end

      # Provides access to StringScanner
      attr_reader :input

      # Appends new @tag to parsed tags list
      def add_tag(tag)
        if tag.is_a?(Hash)
          @tags << @current_tag = tag
        else
          @tags << @current_tag = {:tagname => tag, :doc => ""}
        end

        @current_tag[:doc] = "" unless @current_tag.has_key?(:doc)
      end

      # Parses standard pattern common in several builtin tags, which
      # goes like this:
      #
      #     @tag {Type} [some.name=default]
      #
      # See StandardTagParser#parse for details.
      #
      def standard_tag(cfg)
        Doc::StandardTagParser.new(self).parse(cfg)
      end

      # matches chained.identifier.name and returns it
      def ident_chain
        @input.scan(@ident_chain_pattern)
      end

      # matches identifier and returns its name
      def ident
        @input.scan(@ident_pattern)
      end

      # Looks for the existance of pattern.  Returns true on success.
      # Doesn't advance the scan pointer.
      def look(re)
        @input.check(re)
      end

      # Matches the given pattern and advances the scan pointer
      # returning the string that matched.  When the pattern doesn't
      # match, nil is returned.
      def match(re)
        @input.scan(re)
      end

      # Skips all whitespace.  Moves scan pointer to next non-whitespace
      # character.
      def skip_white
        @input.scan(/\s+/)
      end

      # Skips horizontal whitespace (tabs and spaces).  Moves scan
      # pointer to next non-whitespace character or to the end of line.
      # Returns self to allow chaining.
      def hw
        @input.scan(/[ \t]+/)
        self
      end

    end

  end
end
Loading