Commit 4669fc76 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Redesign StandardTagParser.

Add :default and :optional config options and only return these
fields when these configs present.  With this we now also allow
the syntax for default values without the optionality-braces:

    @mytag {Type} name=<some funky default value>

Added a separate testsuite and updated all calls to
Scanner#standard_tag().
parent 23309cc1
Loading
Loading
Loading
Loading
+105 −0
Original line number Diff line number Diff line
module JsDuck
  module Doc

    # Helper in parsing the default values and type definitions where
    # we take into account correctly nested parenthesis and strings.
    # But at the same time we don't care much about the actual
    # contents.
    class DelimitedParser
      # Initialized with Doc::Scanner instance
      def initialize(doc_scanner)
        @ds = doc_scanner
      end

      # Parses until a closing "}".
      def parse_until_close_curly
        parse_until_close_paren(/\}/, /[^\}]*/)
      end

      # Parses until a closing "]".
      def parse_until_close_square
        parse_until_close_paren(/\]/, /[^\]]*/)
      end

      # Parses until a closing parenthesis or space.
      def parse_until_space
        begin
          parse_while(/[^\[\]\{\}\(\)'"\s]/)
        rescue
          nil
        end
      end

      private

      # Parses until a given closing parenthesis.
      # When unsuccessful, try simple parsing.
      def parse_until_close_paren(re_paren, re_simple)
        begin
          start_pos = @ds.input.pos
          result = parse_while(/[^\[\]\{\}\(\)'"]/)
          if look(re_paren)
            result
          else
            throw "Closing brace expected"
          end
        rescue
          @ds.input.pos = start_pos
          match(re_simple)
        end
      end

      def parse_while(regex)
        result = ""
        while r = parse_compound || match(regex)
          result += r
        end
        result
      end

      def parse_compound
        if look(/\[/)
          parse_balanced(/\[/, /\]/)
        elsif look(/\{/)
          parse_balanced(/\{/, /\}/)
        elsif look(/\(/)
          parse_balanced(/\(/, /\)/)
        elsif look(/"/)
          parse_string('"')
        elsif look(/'/)
          parse_string("'")
        end
      end

      def parse_balanced(re_open, re_close)
        result = match(re_open)

        result += parse_while(/[^\[\]\{\}\(\)'"]/)

        if r = match(re_close)
          result + r
        else
          throw "Unbalanced parenthesis"
        end
      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 Doc::Scanner

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

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

  end
end
+2 −1
Original line number Diff line number Diff line
require 'jsduck/doc/standard_tag_parser'
require 'jsduck/logger'

module JsDuck
  module Doc
@@ -20,7 +21,7 @@ module JsDuck
      end

      # Provides access to StringScanner
      attr_reader :input
      attr_accessor :input

      # Parses standard pattern common in several builtin tags, which
      # goes like this:
+37 −71
Original line number Diff line number Diff line
require 'jsduck/doc/delimited_parser'

module JsDuck
  module Doc

@@ -10,6 +12,7 @@ module JsDuck
      # Initialized with Doc::Scanner instance
      def initialize(doc_scanner)
        @ds = doc_scanner
        @delimited_parser = Doc::DelimitedParser.new(doc_scanner)
      end

      # Parses the standard tag pattern.
@@ -19,21 +22,25 @@ module JsDuck
      #
      # - :tagname => The :tagname of the hash to return.
      #
      # - :type => True to parse {Type} section.
      #            Produces :type and :optional keys.
      # - :type => True to parse `{Type}` section.
      #
      # - :name => True to parse `some.name` section.
      #
      # - :name => Trye to parse [some.name=default] section.
      #            Produces :name, :default and :optional keys.
      # - :default => True to parse `=<default-value>` after name.
      #
      # Returns tag definition hash containing the given :tagname and a
      # set of other fields depending on whether :type and :name configs
      # were specified and how their matching succeeded.
      # - :optional => True to allow placing name and default value
      #       inside [ and ] brackets to denote optionality.
      #       Also returns :optional=>true when {SomType=} syntax used.
      #
      # Returns tag definition hash containing the fields specified by
      # config.
      #
      def parse(cfg)
        @tagname = cfg[:tagname]
        tag = {:tagname => cfg[:tagname]}
        add_type(tag) if cfg[:type]
        add_name_with_default(tag) if cfg[:name]
        tag = {}
        tag[:tagname] = cfg[:tagname] if cfg[:tagname]
        add_type(tag, cfg) if cfg[:type]
        add_name_with_default(tag, cfg) if cfg[:name]
        tag
      end

@@ -41,11 +48,11 @@ module JsDuck

      # matches {type} if possible and sets it on given tag hash.
      # Also checks for {optionality=} in type definition.
      def add_type(tag)
      def add_type(tag, cfg)
        if hw.look(/\{/)
          tdf = typedef
          tag[:type] = tdf[:type]
          tag[:optional] = true if tdf[:optional]
          tag[:optional] = true if tdf[:optional] && cfg[:optional]
        end
      end

@@ -53,7 +60,11 @@ module JsDuck
      def typedef
        match(/\{/)

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

        unless match(/\}/)
          warn("@#{@tagname} tag syntax: '}' expected")
        end

        if name =~ /=$/
          name = name.chop
@@ -62,74 +73,29 @@ module JsDuck
          optional = nil
        end

        match(/\}/) or warn("@#{@tagname} tag syntax: '}' expected")

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

      # matches: <ident-chain> | "[" <ident-chain> [ "=" <default-value> ] "]"
      def add_name_with_default(tag)
        if hw.match(/\[/)
      # matches:   <ident-chain>
      #          | <ident-chain> [ "=" <default-value>
      #          | "[" <ident-chain> [ "=" <default-value> ] "]"
      def add_name_with_default(tag, cfg)
        if hw.look(/\[/) && cfg[:optional]
          match(/\[/)
          tag[:name] = hw.ident_chain
          if hw.match(/=/)
            hw
            tag[:default] = default_value
            tag[:default] = @delimited_parser.parse_until_close_square
          end
          hw.match(/\]/) or warn("@#{@tagname} tag syntax: ']' expected")
          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(/[^\]]*/)
        elsif name = ident_chain
          tag[:name] = name
          if cfg[:default] && hw.match(/=/)
            hw
            tag[:default] = @delimited_parser.parse_until_space
          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 Doc::Scanner
+7 −1
Original line number Diff line number Diff line
@@ -23,7 +23,13 @@ module JsDuck::Tag

    # @cfg {Type} [name=default] (required) ...
    def parse_doc(p, pos)
      tag = p.standard_tag({:tagname => :cfg, :type => true, :name => true})
      tag = p.standard_tag({
          :tagname => :cfg,
          :type => true,
          :name => true,
          :default => true,
          :optional => true
        })
      tag[:optional] = false if parse_required(p)
      tag[:doc] = :multiline
      tag
+7 −1
Original line number Diff line number Diff line
@@ -17,7 +17,13 @@ module JsDuck::Tag

    # @var {Type} [name=default] ...
    def parse_doc(p, pos)
      p.standard_tag({:tagname => :css_var, :type => true, :name => true})
      p.standard_tag({
          :tagname => :css_var,
          :type => true,
          :name => true,
          :default => true,
          :optional => true
        })
    end

    def process_doc(h, tags, pos)
Loading