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

Add StandardTagParser for parsing tags with Type and name.

For now, it parses the following pattern:

    @tag {Type} [some.name=default]
parent ff7dd0bd
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
require 'jsduck/tag_registry'
require 'jsduck/standard_tag_parser'

module JsDuck

@@ -21,6 +21,9 @@ module JsDuck
    # Provides access to the tag that's currently being parsed
    attr_reader :current_tag

    # Provides access to StringScanner
    attr_reader :input

    # Appends new @tag to parsed tags list
    def add_tag(tag)
      if tag.is_a?(Hash)
@@ -32,6 +35,14 @@ module JsDuck
      @current_tag[:doc] = "" unless @current_tag.has_key?(:doc)
    end

    # Parses standard pattern governing several builtin tags.
    #
    # @tag {Type} [some.name=default]
    #
    def standard_tag(tagdef)
      StandardTagParser.new(self).parse(tagdef)
    end

    # matches {type} if possible and sets it on @current_tag
    # Also checks for {optionality=} in type definition.
    def maybe_type
+134 −0
Original line number Diff line number Diff line
module JsDuck

  # Helper in parsing the standard tag pattern with type definition
  # followed by name and default value:
  #
  # @tag {Type} [some.name=default]
  #
  class StandardTagParser
    # Initialized with DocScanner instance
    def initialize(doc_scanner)
      @ds = doc_scanner
    end

    # Parses our tag.
    def parse(tagdef)
      tag = tagdef
      add_type(tag)
      add_name_with_default(tag)
      tag
    end

    # matches {type} if possible and sets it on @current_tag
    # Also checks for {optionality=} in type definition.
    def add_type(tag)
      if hw.look(/\{/)
        tdf = typedef
        tag[:type] = tdf[:type]
        tag[:optional] = true if tdf[:optional]
      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

    # matches: <ident-chain> | "[" <ident-chain> [ "=" <default-value> ] "]"
    def add_name_with_default(tag)
      if hw.match(/\[/)
        tag[:name] = hw.ident_chain
        if hw.match(/=/)
          tag[:default] = hw.default_value
        end
        hw.match(/\]/)
        tag[:optional] = true
      else
        tag[:name] = hw.ident_chain
      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 = @ds.input.pos
      value = parse_balanced(/\[/, /\]/, /[^\[\]'"]*/)
      if look(/\]/)
        value
      else
        @ds.input.pos = start_pos
        match(/[^\]]*/)
      end
    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

    ### Forward these calls to DocScanner

    def ident_chain
      @ds.ident_chain
    end

    def look(re)
      @ds.look(re)
    end

    def match(re)
      @ds.match(re)
    end

    def hw
      @ds.hw
      self
    end
  end

end
+3 −4
Original line number Diff line number Diff line
@@ -9,10 +9,9 @@ module JsDuck::Tag

    # @cfg {Type} [name=default] (required) ...
    def parse(p)
      p.add_tag(:cfg)
      p.maybe_type
      p.maybe_name_with_default
      p.current_tag[:optional] = false if parse_required(p)
      tag = p.standard_tag({:tagname => :cfg})
      tag[:optional] = false if parse_required(p)
      tag
    end

    def parse_required(p)
+1 −3
Original line number Diff line number Diff line
@@ -9,9 +9,7 @@ module JsDuck::Tag

    # @var {Type} [name=default] ...
    def parse(p)
      p.add_tag(:css_var)
      p.maybe_type
      p.maybe_name_with_default
      p.standard_tag({:tagname => :css_var})
    end
  end
end
+3 −4
Original line number Diff line number Diff line
@@ -9,10 +9,9 @@ module JsDuck::Tag
    # @enum {Type} [name=default] ...
    def parse(p)
      # @enum is a special case of class
      p.add_tag(:class)
      p.current_tag[:enum] = true
      p.maybe_type
      p.maybe_name_with_default
      tag = p.standard_tag({:tagname => :class})
      tag[:enum] = true
      tag
    end

  end
Loading