Commit 98646d6a authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Extract DocScanner base class from DocParser.

The purpose of the base class is to clearly separate out methods that
can be called from @tag implementations.
parent 35c138c0
Loading
Loading
Loading
Loading
+4 −210
Original line number Diff line number Diff line
require 'strscan'
require 'jsduck/doc_comment'
require 'jsduck/doc_scanner'
require 'jsduck/builtins_registry'
require 'jsduck/meta_tag_registry'
require 'jsduck/logger'
@@ -25,10 +26,9 @@ module JsDuck
  # @example, {@img}, {@link} and {@video} are parsed separately in
  # JsDuck::DocFormatter.
  #
  class DocParser
  class DocParser < DocScanner
    def initialize
      @ident_pattern = /[$\w-]+/
      @ident_chain_pattern = /[$\w-]+(\.[$\w-]+)*/
      super
      @builtins = BuiltinsRegistry
      @meta_tags = MetaTagRegistry.instance
    end
@@ -78,7 +78,7 @@ module JsDuck

    # Processes anything beginning with @-sign.
    #
    # - When @ is not followed by any word chards, do nothing.
    # - 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 one of the meta-tags, process it as such.
    # - When it's something else, print a warning.
@@ -149,212 +149,6 @@ module JsDuck
    def indented_as_code?
      @current_tag[:doc] =~ /^ {4,}[^\n]*\Z/
    end

    #
    # Parsing helpers ...
    #

    # Provides access to the tag that's currently being parsed
    attr_reader :current_tag

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

    # Forgets the previously parsed tag
    def remove_last_tag
      @tags.pop
      @current_tag = @tags.last
    end

    # matches {type} if possible and sets it on @current_tag
    # Also checks for {optionality=} in type definition.
    def maybe_type
      skip_horiz_white
      if look(/\{/)
        tdf = typedef
        @current_tag[:type] = tdf[:type]
        @current_tag[:optional] = true if tdf[:optional]
      end
    end

    # matches: <ident-chain> | "[" <ident-chain> [ "=" <default-value> ] "]"
    def maybe_name_with_default
      skip_horiz_white
      if look(/\[/)
        match(/\[/)
        maybe_ident_chain(:name)
        skip_horiz_white
        if look(/=/)
          match(/=/)
          skip_horiz_white
          @current_tag[:default] = default_value
        end
        skip_horiz_white
        match(/\]/)
        @current_tag[:optional] = true
      else
        maybe_ident_chain(:name)
      end
    end

    # matches: "(optional)"
    def maybe_optional
      skip_horiz_white
      if look(/\(optional\)/i)
        match(/\(optional\)/i)
        @current_tag[:optional] = true
      end
    end

    # matches: "(required)"
    def maybe_required
      skip_horiz_white
      if look(/\(required\)/i)
        match(/\(required\)/i)
        @current_tag[:optional] = false
      end
    end

    # matches identifier name if possible and sets it on @current_tag
    def maybe_name
      skip_horiz_white
      if look(@ident_pattern)
        @current_tag[:name] = match(@ident_pattern)
      end
    end

    # matches ident.chain if possible and sets it on @current_tag
    def maybe_ident_chain(propname)
      skip_horiz_white
      if look(@ident_chain_pattern)
        @current_tag[propname] = ident_chain
      end
    end

    # matches a member reference: "#" <static> "-" <type> "-" <member>
    # setting the corresponding properties on @current_tag
    def maybe_member_reference
      if look(/#\w/)
        match(/#/)
        if look(/static-/)
          @current_tag[:static] = true
          match(/static-/)
        end
        if look(/(cfg|property|method|event|css_var|css_mixin)-/)
          @current_tag[:type] = ident.to_sym
          match(/-/)
        end
        @current_tag[:member] = ident
      end
    end

    # Attempts to allow balanced braces in default value.
    # When the nested parsing doesn't finish at closing "]",
    # roll back to beginning and simply grab anything up to closing "]".
    def default_value
      start_pos = @input.pos
      value = parse_balanced(/\[/, /\]/, /[^\[\]'"]*/)
      if look(/\]/)
        value
      else
        @input.pos = start_pos
        match(/[^\]]*/)
      end
    end

    # matches {...=} and returns text inside brackets
    def typedef
      match(/\{/)

      name = parse_balanced(/\{/, /\}/, /[^{}'"]*/)

      if name =~ /=$/
        name = name.chop
        optional = true
      else
        optional = nil
      end

      match(/\}/)

      return {:type => name, :optional => optional}
    end

    # Helper method to parse a string up to a closing brace,
    # balancing opening-closing braces in between.
    #
    # @param re_open  The beginning brace regex
    # @param re_close The closing brace regex
    # @param re_rest  Regex to match text without any braces and strings
    def parse_balanced(re_open, re_close, re_rest)
      result = parse_with_strings(re_rest)
      while look(re_open)
        result += match(re_open)
        result += parse_balanced(re_open, re_close, re_rest)
        result += match(re_close)
        result += parse_with_strings(re_rest)
      end
      result
    end

    # Helper for parse_balanced to parse rest of the text between
    # braces, taking account the strings which might occur there.
    def parse_with_strings(re_rest)
      result = match(re_rest)
      while look(/['"]/)
        result += parse_string('"') if look(/"/)
        result += parse_string("'") if look(/'/)
        result += match(re_rest)
      end
      result
    end

    # Parses "..." or '...' including the escape sequence \' or '\"
    def parse_string(quote)
      re_quote = Regexp.new(quote)
      re_rest = Regexp.new("(?:[^"+quote+"\\\\]|\\\\.)*")
      match(re_quote) + match(re_rest) + (match(re_quote) || "")
    end

    # matches <ident_chain> <ident_chain> ... until line end
    def classname_list(propname)
      skip_horiz_white
      classes = []
      while look(@ident_chain_pattern)
        classes << ident_chain
        skip_horiz_white
      end
      @current_tag[propname] = classes
    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(/\w+/)
    end

    def look(re)
      @input.check(re)
    end

    def match(re)
      @input.scan(re)
    end

    def skip_white
      @input.scan(/\s+/)
    end

    # skips horizontal whitespace (tabs and spaces)
    def skip_horiz_white
      @input.scan(/[ \t]+/)
    end
  end

end
+222 −0
Original line number Diff line number Diff line
module JsDuck

  # 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 DocParser, only methods of DocScanner should be called
  # by them.
  #
  class DocScanner
    def initialize
      @ident_pattern = /[$\w-]+/
      @ident_chain_pattern = /[$\w-]+(\.[$\w-]+)*/

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

    # Provides access to the tag that's currently being parsed
    attr_reader :current_tag

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

    # Forgets the previously parsed tag
    def remove_last_tag
      @tags.pop
      @current_tag = @tags.last
    end

    # matches {type} if possible and sets it on @current_tag
    # Also checks for {optionality=} in type definition.
    def maybe_type
      skip_horiz_white
      if look(/\{/)
        tdf = typedef
        @current_tag[:type] = tdf[:type]
        @current_tag[:optional] = true if tdf[:optional]
      end
    end

    # matches: <ident-chain> | "[" <ident-chain> [ "=" <default-value> ] "]"
    def maybe_name_with_default
      skip_horiz_white
      if look(/\[/)
        match(/\[/)
        maybe_ident_chain(:name)
        skip_horiz_white
        if look(/=/)
          match(/=/)
          skip_horiz_white
          @current_tag[:default] = default_value
        end
        skip_horiz_white
        match(/\]/)
        @current_tag[:optional] = true
      else
        maybe_ident_chain(:name)
      end
    end

    # matches: "(optional)"
    def maybe_optional
      skip_horiz_white
      if look(/\(optional\)/i)
        match(/\(optional\)/i)
        @current_tag[:optional] = true
      end
    end

    # matches: "(required)"
    def maybe_required
      skip_horiz_white
      if look(/\(required\)/i)
        match(/\(required\)/i)
        @current_tag[:optional] = false
      end
    end

    # matches identifier name if possible and sets it on @current_tag
    def maybe_name
      skip_horiz_white
      if look(@ident_pattern)
        @current_tag[:name] = match(@ident_pattern)
      end
    end

    # matches ident.chain if possible and sets it on @current_tag
    def maybe_ident_chain(propname)
      skip_horiz_white
      if look(@ident_chain_pattern)
        @current_tag[propname] = ident_chain
      end
    end

    # matches a member reference: "#" <static> "-" <type> "-" <member>
    # setting the corresponding properties on @current_tag
    def maybe_member_reference
      if look(/#\w/)
        match(/#/)
        if look(/static-/)
          @current_tag[:static] = true
          match(/static-/)
        end
        if look(/(cfg|property|method|event|css_var|css_mixin)-/)
          @current_tag[:type] = ident.to_sym
          match(/-/)
        end
        @current_tag[:member] = ident
      end
    end

    # Attempts to allow balanced braces in default value.
    # When the nested parsing doesn't finish at closing "]",
    # roll back to beginning and simply grab anything up to closing "]".
    def default_value
      start_pos = @input.pos
      value = parse_balanced(/\[/, /\]/, /[^\[\]'"]*/)
      if look(/\]/)
        value
      else
        @input.pos = start_pos
        match(/[^\]]*/)
      end
    end

    # matches {...=} and returns text inside brackets
    def typedef
      match(/\{/)

      name = parse_balanced(/\{/, /\}/, /[^{}'"]*/)

      if name =~ /=$/
        name = name.chop
        optional = true
      else
        optional = nil
      end

      match(/\}/)

      return {:type => name, :optional => optional}
    end

    # Helper method to parse a string up to a closing brace,
    # balancing opening-closing braces in between.
    #
    # @param re_open  The beginning brace regex
    # @param re_close The closing brace regex
    # @param re_rest  Regex to match text without any braces and strings
    def parse_balanced(re_open, re_close, re_rest)
      result = parse_with_strings(re_rest)
      while look(re_open)
        result += match(re_open)
        result += parse_balanced(re_open, re_close, re_rest)
        result += match(re_close)
        result += parse_with_strings(re_rest)
      end
      result
    end

    # Helper for parse_balanced to parse rest of the text between
    # braces, taking account the strings which might occur there.
    def parse_with_strings(re_rest)
      result = match(re_rest)
      while look(/['"]/)
        result += parse_string('"') if look(/"/)
        result += parse_string("'") if look(/'/)
        result += match(re_rest)
      end
      result
    end

    # Parses "..." or '...' including the escape sequence \' or '\"
    def parse_string(quote)
      re_quote = Regexp.new(quote)
      re_rest = Regexp.new("(?:[^"+quote+"\\\\]|\\\\.)*")
      match(re_quote) + match(re_rest) + (match(re_quote) || "")
    end

    # matches <ident_chain> <ident_chain> ... until line end
    def classname_list(propname)
      skip_horiz_white
      classes = []
      while look(@ident_chain_pattern)
        classes << ident_chain
        skip_horiz_white
      end
      @current_tag[propname] = classes
    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(/\w+/)
    end

    def look(re)
      @input.check(re)
    end

    def match(re)
      @input.scan(re)
    end

    def skip_white
      @input.scan(/\s+/)
    end

    # skips horizontal whitespace (tabs and spaces)
    def skip_horiz_white
      @input.scan(/[ \t]+/)
    end
  end

end