Commit 7a8f60e3 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Merge branch 'sass'

parents f35f5b2d 7094af1a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -11,3 +11,4 @@ install:
  - gem install rspec
  - gem install rake
  - gem install dimensions
  - gem install sass

lib/jsduck/css/lexer.rb

deleted100644 → 0
+0 −203
Original line number Diff line number Diff line
require 'strscan'

module JsDuck
  module Css

    # Tokenizes CSS or SCSS code into lexical tokens.
    #
    # Each token has a type and value.
    # Types and possible values for them are as follows:
    #
    # - :number      -- "25.8"
    # - :percentage  -- "25%"
    # - :dimension   -- "2em"
    # - :string      -- '"Hello world"'
    # - :ident       -- "foo-bar"
    # - :at_keyword  -- "@mixin"
    # - :hash        -- "#00FF66"
    # - :delim       -- "{"
    # - :doc_comment -- "/** My comment */"
    #
    # Notice that doc-comments are recognized as tokens while normal
    # comments are ignored just as whitespace.
    #
    class Lexer
      # Initializes lexer with input string.
      def initialize(input)
        @input = StringScanner.new(input)
        @buffer = []
      end

      # Tests if given pattern matches the tokens that follow at current
      # position.
      #
      # Takes list of strings and symbols.  Symbols are compared to
      # token type, while strings to token value.  For example:
      #
      #     look(:ident, ":", :dimension)
      #
      def look(*tokens)
        buffer_tokens(tokens.length)
        i = 0
        tokens.all? do |t|
          tok = @buffer[i]
          i += 1
          if !tok
            false
          elsif t.instance_of?(Symbol)
            tok[:type] == t
          else
            tok[:value] == t
          end
        end
      end

      # Returns the value of next token, moving the current token cursor
      # also to next token.
      #
      # When full=true, returns full token as hash like so:
      #
      #     {:type => :ident, :value => "foo"}
      #
      # For doc-comments the full token also contains the field :linenr,
      # pointing to the line where the doc-comment began.
      #
      def next(full=false)
        buffer_tokens(1)
        tok = @buffer.shift
        # advance the scanpointer to the position after this token
        @input.pos = tok[:pos]
        full ? tok : tok[:value]
      end

      # True when no more tokens.
      def empty?
        buffer_tokens(1)
        return !@buffer.first
      end

      # Ensures next n tokens are read in buffer
      #
      # At the end of buffering the initial position scanpointer is
      # restored.  Only the #next method will advance the scanpointer in
      # a way that's visible outside this class.
      def buffer_tokens(n)
        prev_pos = @input.pos
        @input.pos = @buffer.last[:pos] if @buffer.last
        (n - @buffer.length).times do
          @previous_token = tok = next_token
          if tok
            # remember scanpointer position after each token
            tok[:pos] = @input.pos
            @buffer << tok
          end
        end
        @input.pos = prev_pos
      end

      # Parses out next token from input stream.
      def next_token
        while !@input.eos? do
          skip_white
          if @input.check(IDENT)
            return {
              :type => :ident,
              :value => @input.scan(IDENT)
            }
          elsif @input.check(/'/)
            return {
              :type => :string,
              :value => @input.scan(/'([^'\\]|\\.)*('|\z)/m)
            }
          elsif @input.check(/"/)
            return {
              :type => :string,
              :value => @input.scan(/"([^"\\]|\\.)*("|\z)/m)
            }
          elsif @input.check(/\//)
            # Several things begin with dash:
            # - comments, regexes, division-operators
            if @input.check(/\/\*\*[^\/]/)
              return {
                :type => :doc_comment,
                # Calculate current line number, starting with 1
                :linenr => @input.string[0...@input.pos].count("\n") + 1,
                :value => @input.scan_until(/\*\/|\z/).sub(/\A\/\*\*/, "").sub(/\*\/\z/, "")
              }
            elsif @input.check(/\/\*/)
              # skip multiline comment
              @input.scan_until(/\*\/|\z/)
            elsif @input.check(/\/\//)
              # skip line comment
              @input.scan_until(/\n|\z/)
            else
              return {
                :type => :operator,
                :value => @input.scan(/\//)
              }
            end
          elsif @input.check(NUM)
            nr = @input.scan(NUM)
            if @input.check(/%/)
              return {
                :type => :percentage,
                :value => nr + @input.scan(/%/)
              }
            elsif @input.check(IDENT)
              return {
                :type => :dimension,
                :value => nr + @input.scan(IDENT)
              }
            else
              return {
                :type => :number,
                :value => nr
              }
            end
          elsif @input.check(/@/)
            return maybe(:at_keyword, /@/, IDENT)
          elsif @input.check(/#/)
            return maybe(:hash, /#/, NAME)
          elsif @input.check(/\$/)
            return maybe(:var, /\$/, IDENT)
          elsif @input.check(/./)
            return {
              :type => :delim,
              :value => @input.scan(/./)
            }
          end
        end
      end

      # Returns token of given type when both regexes match.
      # Otherwise returns :delim token with value of first regex match.
      # First regex must always match.
      def maybe(token_type, before_re, after_re)
        before = @input.scan(before_re)
        if @input.check(after_re)
          return {
            :type => token_type,
            :value => before + @input.scan(after_re)
          }
        else
          return {
            :type => :delim,
            :value => before
          }
        end
      end

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

      # Simplified token syntax based on:
      # http://www.w3.org/TR/CSS21/syndata.html
      IDENT = /-?[_a-z][_a-z0-9-]*/i
      NAME = /[_a-z0-9-]+/i
      NUM = /[0-9]*\.[0-9]+|[0-9]+/

    end

  end
end
+59 −90
Original line number Diff line number Diff line
require 'jsduck/css/lexer'
require 'sass'
require 'jsduck/css/type'

module JsDuck
  module Css

    # Parses SCSS using the official SASS parser.
    class Parser
      TYPE = Css::Type.new

      def initialize(input, options = {})
        @lex = Css::Lexer.new(input)
        @input = input
        @docs = []
      end

      # Parses the whole CSS block and returns same kind of structure
      # that JavaScript parser does.
      # Returns an array of docsets like the Js::Parser does.
      def parse
        while !@lex.empty? do
          if look(:doc_comment)
            comment = @lex.next(true)
            @docs << {
              :comment => comment[:value],
              :linenr => comment[:linenr],
              :code => code_block,
              :type => :doc_comment,
            }
          else
            @lex.next
        root = Sass::Engine.new(@input, :syntax => :scss).to_tree
        find_doc_comments(root.children)
        @docs
      end

      private

      def find_doc_comments(nodes)
        prev_comment = nil

        nodes.each do |node|
          if prev_comment
            @docs << make_docset(prev_comment, node)
            prev_comment = nil
          end

          if node.class == Sass::Tree::CommentNode
            if node.type == :normal && node.value[0] =~ /\A\/\*\*/
              prev_comment = node
            end
        @docs
          end

      # <code-block> := <mixin-declaration> | <var-declaration> | <property>
      def code_block
        if look("@mixin")
          mixin_declaration
        elsif look(:var, ":")
          var_declaration
        else
          # Default to property like in Js::Parser.
          {:tagname => :property}
          find_doc_comments(node.children)
        end

        if prev_comment
          @docs << make_docset(prev_comment)
        end
      end

      # <mixin-declaration> := "@mixin" <ident>
      def mixin_declaration
        match("@mixin")
      def make_docset(prev_comment, node=nil)
        return {
          :tagname => :css_mixin,
          :name => look(:ident) ? match(:ident) : nil,
          :comment => prev_comment.value[0].sub(/\A\/\*\*/, "").sub(/\*\/\z/, ""),
          :linenr => prev_comment.line,
          :code => analyze_code(node),
          :type => :doc_comment,
        }
      end

      # <var-declaration> := <var> ":" <css-value>
      def var_declaration
        name = match(:var)
        match(":")
        value_list = css_value
      def analyze_code(node)
        if node.class == Sass::Tree::VariableNode
          return {
            :tagname => :css_var,
          :name => name,
          :default => value_list.map {|v| v[:value] }.join(" "),
          :type => value_type(value_list),
            :name => "$" + node.name,
            :default => node.expr.to_sass,
            :type => TYPE.detect(node.expr),
          }
      end

      # <css-value> := ...anything up to... [ ";" | "}" | "!default" ]
      def css_value
        val = []
        while !look(";") && !look("}") && !look("!", "default")
          val << @lex.next(true)
        end
        val
      end

      # Determines type of CSS value
      def value_type(val)
        case val[0][:type]
        when :number
          "number"
        when :dimension
          "length"
        when :percentage
          "percentage"
        when :string
          "string"
        when :hash
          "color"
        when :ident
          case val[0][:value]
          when "true", "false"
            return "boolean"
          when "rgb", "rgba", "hsl", "hsla"
            return "color"
          when "black", "silver", "gray", "white", "maroon",
            "red", "purple", "fuchsia", "green", "lime", "olive",
            "yellow", "navy", "blue", "teal", "aqua", "orange"
            return "color"
          when "transparent"
            return "color"
          end
        elsif node.class == Sass::Tree::MixinDefNode
          return {
            :tagname => :css_mixin,
            :name => node.name,
            :params => build_params(node.args),
          }
        else
          # Default to property like in Js::Parser.
          return {:tagname => :property}
        end
      end

      # Matches all arguments, returns the value of last match
      # When the whole sequence doesn't match, throws exception
      def match(*args)
        if look(*args)
          last = nil
          args.length.times { last = @lex.next }
          last
        else
          throw "Expected: " + args.join(", ")
      def build_params(mixin_args)
        mixin_args.map do |arg|
          {
            :name => "$" + arg[0].name,
            :default => arg[1] ? arg[1].to_s : nil,
            :type => arg[1] ? TYPE.detect(arg[1]) : nil,
          }
        end
      end

      def look(*args)
        @lex.look(*args)
      end
    end

  end

lib/jsduck/css/type.rb

0 → 100644
+55 −0
Original line number Diff line number Diff line
require 'sass'

module JsDuck
  module Css

    class Type
      # Given SASS expression node, determines its type.
      # When unknown, return nil.
      def detect(node)
        if LITERAL_TYPES[node.class]
          LITERAL_TYPES[node.class]
        elsif node.class == Sass::Script::Funcall && COLOR_FUNCTIONS[node.name]
          "color"
        else
          nil
        end
      end

      LITERAL_TYPES = {
        Sass::Script::Number => "number",
        Sass::Script::String => "string",
        Sass::Script::Color => "color",
        Sass::Script::Bool => "boolean",
        Sass::Script::List => "list",
      }

      COLOR_FUNCTIONS = {
        # CSS3 builtins
        "rgb" => true,
        "rgba" => true,
        "hsl" => true,
        "hsla" => true,
        # SASS builtins
        "mix" => true,
        "adjust-hue" => true,
        "lighten" => true,
        "darken" => true,
        "saturate" => true,
        "desaturate" => true,
        "grayscale" => true,
        "complement" => true,
        "invert" => true,
        "opacify" => true,
        "fade-in" => true,
        "transparentize" => true,
        "fade-out" => true,
        "adjust-color" => true,
        "scale-color" => true,
        "change-color" => true,
      }

    end

  end
end
+2 −7
Original line number Diff line number Diff line
@@ -151,13 +151,8 @@ module JsDuck

      # Detects item in object literal either as method or property
      def detect_method_or_property(cls, key, value, pair)
        if value.function?
          m = make_method(key, value)
          cls[:members] << m if apply_autodetected(m, pair)
        else
          p = make_property(key, value)
          cls[:members] << p if apply_autodetected(p, pair)
        end
        member = value.function? ? make_method(key, value) : make_property(key, value)
        cls[:members] << member if apply_autodetected(member, pair)
      end

      def make_configs(ast, defaults={})
Loading