module Sass # A class representing the stack when compiling a Sass file. class Stack # TODO: use this to generate stack information for Sass::SyntaxErrors. # A single stack frame. class Frame # The filename of the file in which this stack frame was created. # # @return [String] attr_reader :filename # The line number on which this stack frame was created. # # @return [String] attr_reader :line # The type of this stack frame. This can be `:import`, `:mixin`, or # `:base`. # # `:base` indicates that this is the bottom-most frame, meaning that it # represents a single line of code rather than a nested context. The stack # will only ever have one base frame, and it will always be the most # deeply-nested frame. # # @return [Symbol?] attr_reader :type # The name of the stack frame. For mixin frames, this is the mixin name; # otherwise, it's `nil`. # # @return [String?] attr_reader :name def initialize(filename, line, type, name = nil) @filename = filename @line = line @type = type @name = name end # Whether this frame represents an import. # # @return [Boolean] def is_import? type == :import end # Whether this frame represents a mixin. # # @return [Boolean] def is_mixin? type == :mixin end # Whether this is the base frame. # # @return [Boolean] def is_base? type == :base end end # The stack frames. The last frame is the most deeply-nested. # # @return [Array] attr_reader :frames def initialize @frames = [] end # Pushes a base frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @yield [] A block in which the new frame is on the stack. def with_base(filename, line) with_frame(filename, line, :base) {yield} end # Pushes an import frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @yield [] A block in which the new frame is on the stack. def with_import(filename, line) with_frame(filename, line, :import) {yield} end # Pushes a mixin frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_mixin(filename, line, name) with_frame(filename, line, :mixin, name) {yield} end # Pushes a function frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_function(filename, line, name) with_frame(filename, line, :function, name) {yield} end # Pushes a function frame onto the stack. # # @param filename [String] See \{Frame#filename}. # @param line [String] See \{Frame#line}. # @param name [String] See \{Frame#name}. # @yield [] A block in which the new frame is on the stack. def with_directive(filename, line, name) with_frame(filename, line, :directive, name) {yield} end def to_s (frames.reverse + [nil]).each_cons(2).each_with_index. map do |(frame, caller), i| "#{i == 0 ? 'on' : 'from'} line #{frame.line}" + " of #{frame.filename || 'an unknown file'}" + (caller && caller.name ? ", in `#{caller.name}'" : "") end.join("\n") end private def with_frame(filename, line, type, name = nil) @frames.pop if @frames.last && @frames.last.type == :base @frames.push(Frame.new(filename, line, type, name)) yield ensure @frames.pop unless type == :base && @frames.last && @frames.last.type != :base end end end