| module Sass::Source |
| class Map |
| # A mapping from one source range to another. Indicates that `input` was |
| # compiled to `output`. |
| # |
| # @!attribute input |
| # @return [Sass::Source::Range] The source range in the input document. |
| # |
| # @!attribute output |
| # @return [Sass::Source::Range] The source range in the output document. |
| class Mapping < Struct.new(:input, :output) |
| # @return [String] A string representation of the mapping. |
| def inspect |
| "#{input.inspect} => #{output.inspect}" |
| end |
| end |
| |
| # The mapping data ordered by the location in the target. |
| # |
| # @return [Array<Mapping>] |
| attr_reader :data |
| |
| def initialize |
| @data = [] |
| end |
| |
| # Adds a new mapping from one source range to another. Multiple invocations |
| # of this method should have each `output` range come after all previous ranges. |
| # |
| # @param input [Sass::Source::Range] |
| # The source range in the input document. |
| # @param output [Sass::Source::Range] |
| # The source range in the output document. |
| def add(input, output) |
| @data.push(Mapping.new(input, output)) |
| end |
| |
| # Shifts all output source ranges forward one or more lines. |
| # |
| # @param delta [Integer] The number of lines to shift the ranges forward. |
| def shift_output_lines(delta) |
| return if delta == 0 |
| @data.each do |m| |
| m.output.start_pos.line += delta |
| m.output.end_pos.line += delta |
| end |
| end |
| |
| # Shifts any output source ranges that lie on the first line forward one or |
| # more characters on that line. |
| # |
| # @param delta [Integer] The number of characters to shift the ranges |
| # forward. |
| def shift_output_offsets(delta) |
| return if delta == 0 |
| @data.each do |m| |
| break if m.output.start_pos.line > 1 |
| m.output.start_pos.offset += delta |
| m.output.end_pos.offset += delta if m.output.end_pos.line > 1 |
| end |
| end |
| |
| # Returns the standard JSON representation of the source map. |
| # |
| # If the `:css_uri` option isn't specified, the `:css_path` and |
| # `:sourcemap_path` options must both be specified. Any options may also be |
| # specified alongside the `:css_uri` option. If `:css_uri` isn't specified, |
| # it will be inferred from `:css_path` and `:sourcemap_path` using the |
| # assumption that the local file system has the same layout as the server. |
| # |
| # Regardless of which options are passed to this method, source stylesheets |
| # that are imported using a non-default importer will only be linked to in |
| # the source map if their importers implement |
| # \{Sass::Importers::Base#public\_url\}. |
| # |
| # @option options :css_uri [String] |
| # The publicly-visible URI of the CSS output file. |
| # @option options :css_path [String] |
| # The local path of the CSS output file. |
| # @option options :sourcemap_path [String] |
| # The (eventual) local path of the sourcemap file. |
| # @option options :type [Symbol] |
| # `:auto` (default), `:file`, or `:inline`. |
| # @return [String] The JSON string. |
| # @raise [ArgumentError] If neither `:css_uri` nor `:css_path` and |
| # `:sourcemap_path` are specified. |
| # @comment |
| # rubocop:disable MethodLength |
| def to_json(options) |
| css_uri, css_path, sourcemap_path = |
| options[:css_uri], options[:css_path], options[:sourcemap_path] |
| unless css_uri || (css_path && sourcemap_path) |
| raise ArgumentError.new("Sass::Source::Map#to_json requires either " \ |
| "the :css_uri option or both the :css_path and :soucemap_path options.") |
| end |
| css_path &&= Sass::Util.pathname(File.absolute_path(css_path)) |
| sourcemap_path &&= Sass::Util.pathname(File.absolute_path(sourcemap_path)) |
| css_uri ||= Sass::Util.file_uri_from_path( |
| Sass::Util.relative_path_from(css_path, sourcemap_path.dirname)) |
| |
| result = "{\n" |
| write_json_field(result, "version", 3, true) |
| |
| source_uri_to_id = {} |
| id_to_source_uri = {} |
| id_to_contents = {} if options[:type] == :inline |
| next_source_id = 0 |
| line_data = [] |
| segment_data_for_line = [] |
| |
| # These track data necessary for the delta coding. |
| previous_target_line = nil |
| previous_target_offset = 1 |
| previous_source_line = 1 |
| previous_source_offset = 1 |
| previous_source_id = 0 |
| |
| @data.each do |m| |
| file, importer = m.input.file, m.input.importer |
| |
| next unless importer |
| |
| if options[:type] == :inline |
| source_uri = file |
| else |
| sourcemap_dir = sourcemap_path && sourcemap_path.dirname.to_s |
| sourcemap_dir = nil if options[:type] == :file |
| source_uri = importer.public_url(file, sourcemap_dir) |
| next unless source_uri |
| end |
| |
| current_source_id = source_uri_to_id[source_uri] |
| unless current_source_id |
| current_source_id = next_source_id |
| next_source_id += 1 |
| |
| source_uri_to_id[source_uri] = current_source_id |
| id_to_source_uri[current_source_id] = source_uri |
| |
| if options[:type] == :inline |
| id_to_contents[current_source_id] = |
| importer.find(file, {}).instance_variable_get('@template') |
| end |
| end |
| |
| [ |
| [m.input.start_pos, m.output.start_pos], |
| [m.input.end_pos, m.output.end_pos] |
| ].each do |source_pos, target_pos| |
| if previous_target_line != target_pos.line |
| line_data.push(segment_data_for_line.join(",")) unless segment_data_for_line.empty? |
| (target_pos.line - 1 - (previous_target_line || 0)).times {line_data.push("")} |
| previous_target_line = target_pos.line |
| previous_target_offset = 1 |
| segment_data_for_line = [] |
| end |
| |
| # `segment` is a data chunk for a single position mapping. |
| segment = "" |
| |
| # Field 1: zero-based starting offset. |
| segment << Sass::Util.encode_vlq(target_pos.offset - previous_target_offset) |
| previous_target_offset = target_pos.offset |
| |
| # Field 2: zero-based index into the "sources" list. |
| segment << Sass::Util.encode_vlq(current_source_id - previous_source_id) |
| previous_source_id = current_source_id |
| |
| # Field 3: zero-based starting line in the original source. |
| segment << Sass::Util.encode_vlq(source_pos.line - previous_source_line) |
| previous_source_line = source_pos.line |
| |
| # Field 4: zero-based starting offset in the original source. |
| segment << Sass::Util.encode_vlq(source_pos.offset - previous_source_offset) |
| previous_source_offset = source_pos.offset |
| |
| segment_data_for_line.push(segment) |
| |
| previous_target_line = target_pos.line |
| end |
| end |
| line_data.push(segment_data_for_line.join(",")) |
| write_json_field(result, "mappings", line_data.join(";")) |
| |
| source_names = [] |
| (0...next_source_id).each {|id| source_names.push(id_to_source_uri[id].to_s)} |
| write_json_field(result, "sources", source_names) |
| |
| if options[:type] == :inline |
| write_json_field(result, "sourcesContent", |
| (0...next_source_id).map {|id| id_to_contents[id]}) |
| end |
| |
| write_json_field(result, "names", []) |
| write_json_field(result, "file", css_uri) |
| |
| result << "\n}" |
| result |
| end |
| # @comment |
| # rubocop:enable MethodLength |
| |
| private |
| |
| def write_json_field(out, name, value, is_first = false) |
| out << (is_first ? "" : ",\n") << |
| "\"" << |
| Sass::Util.json_escape_string(name) << |
| "\": " << |
| Sass::Util.json_value_of(value) |
| end |
| end |
| end |