719 lines
24 KiB
Dart
719 lines
24 KiB
Dart
|
|
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
|||
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|||
|
|
// BSD-style license that can be found in the LICENSE file.
|
|||
|
|
|
|||
|
|
/// Contains the top-level function to parse source maps version 3.
|
|||
|
|
library source_maps.parser;
|
|||
|
|
|
|||
|
|
import 'dart:convert';
|
|||
|
|
|
|||
|
|
import 'package:source_span/source_span.dart';
|
|||
|
|
|
|||
|
|
import 'builder.dart' as builder;
|
|||
|
|
import 'src/source_map_span.dart';
|
|||
|
|
import 'src/utils.dart';
|
|||
|
|
import 'src/vlq.dart';
|
|||
|
|
|
|||
|
|
/// Parses a source map directly from a json string.
|
|||
|
|
///
|
|||
|
|
/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
|
|||
|
|
/// the source map file itself. If it's passed, any URLs in the source
|
|||
|
|
/// map will be interpreted as relative to this URL when generating spans.
|
|||
|
|
// TODO(sigmund): evaluate whether other maps should have the json parsed, or
|
|||
|
|
// the string represenation.
|
|||
|
|
// TODO(tjblasi): Ignore the first line of [jsonMap] if the JSON safety string
|
|||
|
|
// `)]}'` begins the string representation of the map.
|
|||
|
|
Mapping parse(String jsonMap,
|
|||
|
|
{Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
|
|||
|
|
parseJson(jsonDecode(jsonMap) as Map, otherMaps: otherMaps, mapUrl: mapUrl);
|
|||
|
|
|
|||
|
|
/// Parses a source map or source map bundle directly from a json string.
|
|||
|
|
///
|
|||
|
|
/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
|
|||
|
|
/// the source map file itself. If it's passed, any URLs in the source
|
|||
|
|
/// map will be interpreted as relative to this URL when generating spans.
|
|||
|
|
Mapping parseExtended(String jsonMap,
|
|||
|
|
{Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) =>
|
|||
|
|
parseJsonExtended(jsonDecode(jsonMap),
|
|||
|
|
otherMaps: otherMaps, mapUrl: mapUrl);
|
|||
|
|
|
|||
|
|
/// Parses a source map or source map bundle.
|
|||
|
|
///
|
|||
|
|
/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
|
|||
|
|
/// the source map file itself. If it's passed, any URLs in the source
|
|||
|
|
/// map will be interpreted as relative to this URL when generating spans.
|
|||
|
|
Mapping parseJsonExtended(/*List|Map*/ Object? json,
|
|||
|
|
{Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
|
|||
|
|
if (json is List) {
|
|||
|
|
return MappingBundle.fromJson(json, mapUrl: mapUrl);
|
|||
|
|
}
|
|||
|
|
return parseJson(json as Map);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Parses a source map.
|
|||
|
|
///
|
|||
|
|
/// [mapUrl], which may be either a [String] or a [Uri], indicates the URL of
|
|||
|
|
/// the source map file itself. If it's passed, any URLs in the source
|
|||
|
|
/// map will be interpreted as relative to this URL when generating spans.
|
|||
|
|
Mapping parseJson(Map map,
|
|||
|
|
{Map<String, Map>? otherMaps, /*String|Uri*/ Object? mapUrl}) {
|
|||
|
|
if (map['version'] != 3) {
|
|||
|
|
throw ArgumentError('unexpected source map version: ${map["version"]}. '
|
|||
|
|
'Only version 3 is supported.');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (map.containsKey('sections')) {
|
|||
|
|
if (map.containsKey('mappings') ||
|
|||
|
|
map.containsKey('sources') ||
|
|||
|
|
map.containsKey('names')) {
|
|||
|
|
throw const FormatException('map containing "sections" '
|
|||
|
|
'cannot contain "mappings", "sources", or "names".');
|
|||
|
|
}
|
|||
|
|
return MultiSectionMapping.fromJson(map['sections'] as List, otherMaps,
|
|||
|
|
mapUrl: mapUrl);
|
|||
|
|
}
|
|||
|
|
return SingleMapping.fromJson(map.cast<String, dynamic>(), mapUrl: mapUrl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A mapping parsed out of a source map.
|
|||
|
|
abstract class Mapping {
|
|||
|
|
/// Returns the span associated with [line] and [column].
|
|||
|
|
///
|
|||
|
|
/// [uri] is the optional location of the output file to find the span for
|
|||
|
|
/// to disambiguate cases where a mapping may have different mappings for
|
|||
|
|
/// different output files.
|
|||
|
|
SourceMapSpan? spanFor(int line, int column,
|
|||
|
|
{Map<String, SourceFile>? files, String? uri});
|
|||
|
|
|
|||
|
|
/// Returns the span associated with [location].
|
|||
|
|
SourceMapSpan? spanForLocation(SourceLocation location,
|
|||
|
|
{Map<String, SourceFile>? files}) {
|
|||
|
|
return spanFor(location.line, location.column,
|
|||
|
|
uri: location.sourceUrl?.toString(), files: files);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A meta-level map containing sections.
|
|||
|
|
class MultiSectionMapping extends Mapping {
|
|||
|
|
/// For each section, the start line offset.
|
|||
|
|
final List<int> _lineStart = <int>[];
|
|||
|
|
|
|||
|
|
/// For each section, the start column offset.
|
|||
|
|
final List<int> _columnStart = <int>[];
|
|||
|
|
|
|||
|
|
/// For each section, the actual source map information, which is not adjusted
|
|||
|
|
/// for offsets.
|
|||
|
|
final List<Mapping> _maps = <Mapping>[];
|
|||
|
|
|
|||
|
|
/// Creates a section mapping from json.
|
|||
|
|
MultiSectionMapping.fromJson(List sections, Map<String, Map>? otherMaps,
|
|||
|
|
{/*String|Uri*/ Object? mapUrl}) {
|
|||
|
|
for (var section in sections.cast<Map>()) {
|
|||
|
|
var offset = section['offset'] as Map?;
|
|||
|
|
if (offset == null) throw const FormatException('section missing offset');
|
|||
|
|
|
|||
|
|
var line = offset['line'] as int?;
|
|||
|
|
if (line == null) throw const FormatException('offset missing line');
|
|||
|
|
|
|||
|
|
var column = offset['column'] as int?;
|
|||
|
|
if (column == null) throw const FormatException('offset missing column');
|
|||
|
|
|
|||
|
|
_lineStart.add(line);
|
|||
|
|
_columnStart.add(column);
|
|||
|
|
|
|||
|
|
var url = section['url'] as String?;
|
|||
|
|
var map = section['map'] as Map?;
|
|||
|
|
|
|||
|
|
if (url != null && map != null) {
|
|||
|
|
throw const FormatException(
|
|||
|
|
"section can't use both url and map entries");
|
|||
|
|
} else if (url != null) {
|
|||
|
|
var other = otherMaps?[url];
|
|||
|
|
if (otherMaps == null || other == null) {
|
|||
|
|
throw FormatException(
|
|||
|
|
'section contains refers to $url, but no map was '
|
|||
|
|
'given for it. Make sure a map is passed in "otherMaps"');
|
|||
|
|
}
|
|||
|
|
_maps.add(parseJson(other, otherMaps: otherMaps, mapUrl: url));
|
|||
|
|
} else if (map != null) {
|
|||
|
|
_maps.add(parseJson(map, otherMaps: otherMaps, mapUrl: mapUrl));
|
|||
|
|
} else {
|
|||
|
|
throw const FormatException('section missing url or map');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (_lineStart.isEmpty) {
|
|||
|
|
throw const FormatException('expected at least one section');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int _indexFor(int line, int column) {
|
|||
|
|
for (var i = 0; i < _lineStart.length; i++) {
|
|||
|
|
if (line < _lineStart[i]) return i - 1;
|
|||
|
|
if (line == _lineStart[i] && column < _columnStart[i]) return i - 1;
|
|||
|
|
}
|
|||
|
|
return _lineStart.length - 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
SourceMapSpan? spanFor(int line, int column,
|
|||
|
|
{Map<String, SourceFile>? files, String? uri}) {
|
|||
|
|
// TODO(jacobr): perhaps verify that targetUrl matches the actual uri
|
|||
|
|
// or at least ends in the same file name.
|
|||
|
|
var index = _indexFor(line, column);
|
|||
|
|
return _maps[index].spanFor(
|
|||
|
|
line - _lineStart[index], column - _columnStart[index],
|
|||
|
|
files: files);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String toString() {
|
|||
|
|
var buff = StringBuffer('$runtimeType : [');
|
|||
|
|
for (var i = 0; i < _lineStart.length; i++) {
|
|||
|
|
buff
|
|||
|
|
..write('(')
|
|||
|
|
..write(_lineStart[i])
|
|||
|
|
..write(',')
|
|||
|
|
..write(_columnStart[i])
|
|||
|
|
..write(':')
|
|||
|
|
..write(_maps[i])
|
|||
|
|
..write(')');
|
|||
|
|
}
|
|||
|
|
buff.write(']');
|
|||
|
|
return buff.toString();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class MappingBundle extends Mapping {
|
|||
|
|
final Map<String, SingleMapping> _mappings = {};
|
|||
|
|
|
|||
|
|
MappingBundle();
|
|||
|
|
|
|||
|
|
MappingBundle.fromJson(List json, {/*String|Uri*/ Object? mapUrl}) {
|
|||
|
|
for (var map in json) {
|
|||
|
|
addMapping(parseJson(map as Map, mapUrl: mapUrl) as SingleMapping);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void addMapping(SingleMapping mapping) {
|
|||
|
|
// TODO(jacobr): verify that targetUrl is valid uri instead of a windows
|
|||
|
|
// path.
|
|||
|
|
// TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
|
|||
|
|
var targetUrl = ArgumentError.checkNotNull<String>(
|
|||
|
|
mapping.targetUrl, 'mapping.targetUrl');
|
|||
|
|
_mappings[targetUrl] = mapping;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Encodes the Mapping mappings as a json map.
|
|||
|
|
List toJson() => _mappings.values.map((v) => v.toJson()).toList();
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String toString() {
|
|||
|
|
var buff = StringBuffer();
|
|||
|
|
for (var map in _mappings.values) {
|
|||
|
|
buff.write(map.toString());
|
|||
|
|
}
|
|||
|
|
return buff.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bool containsMapping(String url) => _mappings.containsKey(url);
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
SourceMapSpan? spanFor(int line, int column,
|
|||
|
|
{Map<String, SourceFile>? files, String? uri}) {
|
|||
|
|
// TODO: Remove type arg https://github.com/dart-lang/sdk/issues/42227
|
|||
|
|
uri = ArgumentError.checkNotNull<String>(uri, 'uri');
|
|||
|
|
|
|||
|
|
// Find the longest suffix of the uri that matches the sourcemap
|
|||
|
|
// where the suffix starts after a path segment boundary.
|
|||
|
|
// We consider ":" and "/" as path segment boundaries so that
|
|||
|
|
// "package:" uris can be handled with minimal special casing. Having a
|
|||
|
|
// few false positive path segment boundaries is not a significant issue
|
|||
|
|
// as we prefer the longest matching prefix.
|
|||
|
|
// Using package:path `path.split` to find path segment boundaries would
|
|||
|
|
// not generate all of the path segment boundaries we want for "package:"
|
|||
|
|
// urls as "package:package_name" would be one path segment when we want
|
|||
|
|
// "package" and "package_name" to be sepearate path segments.
|
|||
|
|
|
|||
|
|
var onBoundary = true;
|
|||
|
|
var separatorCodeUnits = ['/'.codeUnitAt(0), ':'.codeUnitAt(0)];
|
|||
|
|
for (var i = 0; i < uri.length; ++i) {
|
|||
|
|
if (onBoundary) {
|
|||
|
|
var candidate = uri.substring(i);
|
|||
|
|
var candidateMapping = _mappings[candidate];
|
|||
|
|
if (candidateMapping != null) {
|
|||
|
|
return candidateMapping.spanFor(line, column,
|
|||
|
|
files: files, uri: candidate);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
onBoundary = separatorCodeUnits.contains(uri.codeUnitAt(i));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Note: when there is no source map for an uri, this behaves like an
|
|||
|
|
// identity function, returning the requested location as the result.
|
|||
|
|
|
|||
|
|
// Create a mock offset for the output location. We compute it in terms
|
|||
|
|
// of the input line and column to minimize the chances that two different
|
|||
|
|
// line and column locations are mapped to the same offset.
|
|||
|
|
var offset = line * 1000000 + column;
|
|||
|
|
var location = SourceLocation(offset,
|
|||
|
|
line: line, column: column, sourceUrl: Uri.parse(uri));
|
|||
|
|
return SourceMapSpan(location, location, '');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A map containing direct source mappings.
|
|||
|
|
class SingleMapping extends Mapping {
|
|||
|
|
/// Source urls used in the mapping, indexed by id.
|
|||
|
|
final List<String> urls;
|
|||
|
|
|
|||
|
|
/// Source names used in the mapping, indexed by id.
|
|||
|
|
final List<String> names;
|
|||
|
|
|
|||
|
|
/// The [SourceFile]s to which the entries in [lines] refer.
|
|||
|
|
///
|
|||
|
|
/// This is in the same order as [urls]. If this was constructed using
|
|||
|
|
/// [SingleMapping.fromEntries], this contains files from any [FileLocation]s
|
|||
|
|
/// used to build the mapping. If it was parsed from JSON, it contains files
|
|||
|
|
/// for any sources whose contents were provided via the `"sourcesContent"`
|
|||
|
|
/// field.
|
|||
|
|
///
|
|||
|
|
/// Files whose contents aren't available are `null`.
|
|||
|
|
final List<SourceFile?> files;
|
|||
|
|
|
|||
|
|
/// Entries indicating the beginning of each span.
|
|||
|
|
final List<TargetLineEntry> lines;
|
|||
|
|
|
|||
|
|
/// Url of the target file.
|
|||
|
|
String? targetUrl;
|
|||
|
|
|
|||
|
|
/// Source root prepended to all entries in [urls].
|
|||
|
|
String? sourceRoot;
|
|||
|
|
|
|||
|
|
final Uri? _mapUrl;
|
|||
|
|
|
|||
|
|
final Map<String, dynamic> extensions;
|
|||
|
|
|
|||
|
|
SingleMapping._(this.targetUrl, this.files, this.urls, this.names, this.lines)
|
|||
|
|
: _mapUrl = null,
|
|||
|
|
extensions = {};
|
|||
|
|
|
|||
|
|
factory SingleMapping.fromEntries(Iterable<builder.Entry> entries,
|
|||
|
|
[String? fileUrl]) {
|
|||
|
|
// The entries needs to be sorted by the target offsets.
|
|||
|
|
var sourceEntries = entries.toList()..sort();
|
|||
|
|
var lines = <TargetLineEntry>[];
|
|||
|
|
|
|||
|
|
// Indices associated with file urls that will be part of the source map. We
|
|||
|
|
// rely on map order so that `urls.keys[urls[u]] == u`
|
|||
|
|
var urls = <String, int>{};
|
|||
|
|
|
|||
|
|
// Indices associated with identifiers that will be part of the source map.
|
|||
|
|
// We rely on map order so that `names.keys[names[n]] == n`
|
|||
|
|
var names = <String, int>{};
|
|||
|
|
|
|||
|
|
/// The file for each URL, indexed by [urls]' values.
|
|||
|
|
var files = <int, SourceFile>{};
|
|||
|
|
|
|||
|
|
int? lineNum;
|
|||
|
|
late List<TargetEntry> targetEntries;
|
|||
|
|
for (var sourceEntry in sourceEntries) {
|
|||
|
|
if (lineNum == null || sourceEntry.target.line > lineNum) {
|
|||
|
|
lineNum = sourceEntry.target.line;
|
|||
|
|
targetEntries = <TargetEntry>[];
|
|||
|
|
lines.add(TargetLineEntry(lineNum, targetEntries));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sourceUrl = sourceEntry.source.sourceUrl;
|
|||
|
|
var urlId = urls.putIfAbsent(
|
|||
|
|
sourceUrl == null ? '' : sourceUrl.toString(), () => urls.length);
|
|||
|
|
|
|||
|
|
if (sourceEntry.source is FileLocation) {
|
|||
|
|
files.putIfAbsent(
|
|||
|
|
urlId, () => (sourceEntry.source as FileLocation).file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sourceEntryIdentifierName = sourceEntry.identifierName;
|
|||
|
|
var srcNameId = sourceEntryIdentifierName == null
|
|||
|
|
? null
|
|||
|
|
: names.putIfAbsent(sourceEntryIdentifierName, () => names.length);
|
|||
|
|
targetEntries.add(TargetEntry(sourceEntry.target.column, urlId,
|
|||
|
|
sourceEntry.source.line, sourceEntry.source.column, srcNameId));
|
|||
|
|
}
|
|||
|
|
return SingleMapping._(fileUrl, urls.values.map((i) => files[i]).toList(),
|
|||
|
|
urls.keys.toList(), names.keys.toList(), lines);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
SingleMapping.fromJson(Map<String, dynamic> map, {Object? mapUrl})
|
|||
|
|
: targetUrl = map['file'] as String?,
|
|||
|
|
urls = List<String>.from(map['sources'] as List),
|
|||
|
|
names = List<String>.from((map['names'] as List?) ?? []),
|
|||
|
|
files = List.filled((map['sources'] as List).length, null),
|
|||
|
|
sourceRoot = map['sourceRoot'] as String?,
|
|||
|
|
lines = <TargetLineEntry>[],
|
|||
|
|
_mapUrl = mapUrl is String ? Uri.parse(mapUrl) : (mapUrl as Uri?),
|
|||
|
|
extensions = {} {
|
|||
|
|
var sourcesContent = map['sourcesContent'] == null
|
|||
|
|
? const <String?>[]
|
|||
|
|
: List<String?>.from(map['sourcesContent'] as List);
|
|||
|
|
for (var i = 0; i < urls.length && i < sourcesContent.length; i++) {
|
|||
|
|
var source = sourcesContent[i];
|
|||
|
|
if (source == null) continue;
|
|||
|
|
files[i] = SourceFile.fromString(source, url: urls[i]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var line = 0;
|
|||
|
|
var column = 0;
|
|||
|
|
var srcUrlId = 0;
|
|||
|
|
var srcLine = 0;
|
|||
|
|
var srcColumn = 0;
|
|||
|
|
var srcNameId = 0;
|
|||
|
|
var tokenizer = _MappingTokenizer(map['mappings'] as String);
|
|||
|
|
var entries = <TargetEntry>[];
|
|||
|
|
|
|||
|
|
while (tokenizer.hasTokens) {
|
|||
|
|
if (tokenizer.nextKind.isNewLine) {
|
|||
|
|
if (entries.isNotEmpty) {
|
|||
|
|
lines.add(TargetLineEntry(line, entries));
|
|||
|
|
entries = <TargetEntry>[];
|
|||
|
|
}
|
|||
|
|
line++;
|
|||
|
|
column = 0;
|
|||
|
|
tokenizer._consumeNewLine();
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Decode the next entry, using the previous encountered values to
|
|||
|
|
// decode the relative values.
|
|||
|
|
//
|
|||
|
|
// We expect 1, 4, or 5 values. If present, values are expected in the
|
|||
|
|
// following order:
|
|||
|
|
// 0: the starting column in the current line of the generated file
|
|||
|
|
// 1: the id of the original source file
|
|||
|
|
// 2: the starting line in the original source
|
|||
|
|
// 3: the starting column in the original source
|
|||
|
|
// 4: the id of the original symbol name
|
|||
|
|
// The values are relative to the previous encountered values.
|
|||
|
|
if (tokenizer.nextKind.isNewSegment) throw _segmentError(0, line);
|
|||
|
|
column += tokenizer._consumeValue();
|
|||
|
|
if (!tokenizer.nextKind.isValue) {
|
|||
|
|
entries.add(TargetEntry(column));
|
|||
|
|
} else {
|
|||
|
|
srcUrlId += tokenizer._consumeValue();
|
|||
|
|
if (srcUrlId >= urls.length) {
|
|||
|
|
throw StateError(
|
|||
|
|
'Invalid source url id. $targetUrl, $line, $srcUrlId');
|
|||
|
|
}
|
|||
|
|
if (!tokenizer.nextKind.isValue) throw _segmentError(2, line);
|
|||
|
|
srcLine += tokenizer._consumeValue();
|
|||
|
|
if (!tokenizer.nextKind.isValue) throw _segmentError(3, line);
|
|||
|
|
srcColumn += tokenizer._consumeValue();
|
|||
|
|
if (!tokenizer.nextKind.isValue) {
|
|||
|
|
entries.add(TargetEntry(column, srcUrlId, srcLine, srcColumn));
|
|||
|
|
} else {
|
|||
|
|
srcNameId += tokenizer._consumeValue();
|
|||
|
|
if (srcNameId >= names.length) {
|
|||
|
|
throw StateError('Invalid name id: $targetUrl, $line, $srcNameId');
|
|||
|
|
}
|
|||
|
|
entries.add(
|
|||
|
|
TargetEntry(column, srcUrlId, srcLine, srcColumn, srcNameId));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (tokenizer.nextKind.isNewSegment) tokenizer._consumeNewSegment();
|
|||
|
|
}
|
|||
|
|
if (entries.isNotEmpty) {
|
|||
|
|
lines.add(TargetLineEntry(line, entries));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
map.forEach((name, value) {
|
|||
|
|
if (name.startsWith('x_')) extensions[name] = value;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Encodes the Mapping mappings as a json map.
|
|||
|
|
///
|
|||
|
|
/// If [includeSourceContents] is `true`, this includes the source file
|
|||
|
|
/// contents from [files] in the map if possible.
|
|||
|
|
Map<String, dynamic> toJson({bool includeSourceContents = false}) {
|
|||
|
|
var buff = StringBuffer();
|
|||
|
|
var line = 0;
|
|||
|
|
var column = 0;
|
|||
|
|
var srcLine = 0;
|
|||
|
|
var srcColumn = 0;
|
|||
|
|
var srcUrlId = 0;
|
|||
|
|
var srcNameId = 0;
|
|||
|
|
var first = true;
|
|||
|
|
|
|||
|
|
for (var entry in lines) {
|
|||
|
|
var nextLine = entry.line;
|
|||
|
|
if (nextLine > line) {
|
|||
|
|
for (var i = line; i < nextLine; ++i) {
|
|||
|
|
buff.write(';');
|
|||
|
|
}
|
|||
|
|
line = nextLine;
|
|||
|
|
column = 0;
|
|||
|
|
first = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (var segment in entry.entries) {
|
|||
|
|
if (!first) buff.write(',');
|
|||
|
|
first = false;
|
|||
|
|
column = _append(buff, column, segment.column);
|
|||
|
|
|
|||
|
|
// Encoding can be just the column offset if there is no source
|
|||
|
|
// information.
|
|||
|
|
var newUrlId = segment.sourceUrlId;
|
|||
|
|
if (newUrlId == null) continue;
|
|||
|
|
srcUrlId = _append(buff, srcUrlId, newUrlId);
|
|||
|
|
srcLine = _append(buff, srcLine, segment.sourceLine!);
|
|||
|
|
srcColumn = _append(buff, srcColumn, segment.sourceColumn!);
|
|||
|
|
|
|||
|
|
if (segment.sourceNameId == null) continue;
|
|||
|
|
srcNameId = _append(buff, srcNameId, segment.sourceNameId!);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var result = <String, dynamic>{
|
|||
|
|
'version': 3,
|
|||
|
|
'sourceRoot': sourceRoot ?? '',
|
|||
|
|
'sources': urls,
|
|||
|
|
'names': names,
|
|||
|
|
'mappings': buff.toString(),
|
|||
|
|
};
|
|||
|
|
if (targetUrl != null) result['file'] = targetUrl!;
|
|||
|
|
|
|||
|
|
if (includeSourceContents) {
|
|||
|
|
result['sourcesContent'] = files.map((file) => file?.getText(0)).toList();
|
|||
|
|
}
|
|||
|
|
extensions.forEach((name, value) => result[name] = value);
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Appends to [buff] a VLQ encoding of [newValue] using the difference
|
|||
|
|
/// between [oldValue] and [newValue]
|
|||
|
|
static int _append(StringBuffer buff, int oldValue, int newValue) {
|
|||
|
|
buff.writeAll(encodeVlq(newValue - oldValue));
|
|||
|
|
return newValue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
StateError _segmentError(int seen, int line) =>
|
|||
|
|
StateError('Invalid entry in sourcemap, expected 1, 4, or 5'
|
|||
|
|
' values, but got $seen.\ntargeturl: $targetUrl, line: $line');
|
|||
|
|
|
|||
|
|
/// Returns [TargetLineEntry] which includes the location in the target [line]
|
|||
|
|
/// number. In particular, the resulting entry is the last entry whose line
|
|||
|
|
/// number is lower or equal to [line].
|
|||
|
|
TargetLineEntry? _findLine(int line) {
|
|||
|
|
var index = binarySearch(lines, (e) => e.line > line);
|
|||
|
|
return (index <= 0) ? null : lines[index - 1];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Returns [TargetEntry] which includes the location denoted by
|
|||
|
|
/// [line], [column]. If [lineEntry] corresponds to [line], then this will be
|
|||
|
|
/// the last entry whose column is lower or equal than [column]. If
|
|||
|
|
/// [lineEntry] corresponds to a line prior to [line], then the result will be
|
|||
|
|
/// the very last entry on that line.
|
|||
|
|
TargetEntry? _findColumn(int line, int column, TargetLineEntry? lineEntry) {
|
|||
|
|
if (lineEntry == null || lineEntry.entries.isEmpty) return null;
|
|||
|
|
if (lineEntry.line != line) return lineEntry.entries.last;
|
|||
|
|
var entries = lineEntry.entries;
|
|||
|
|
var index = binarySearch(entries, (e) => e.column > column);
|
|||
|
|
return (index <= 0) ? null : entries[index - 1];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
SourceMapSpan? spanFor(int line, int column,
|
|||
|
|
{Map<String, SourceFile>? files, String? uri}) {
|
|||
|
|
var entry = _findColumn(line, column, _findLine(line));
|
|||
|
|
if (entry == null) return null;
|
|||
|
|
|
|||
|
|
var sourceUrlId = entry.sourceUrlId;
|
|||
|
|
if (sourceUrlId == null) return null;
|
|||
|
|
|
|||
|
|
var url = urls[sourceUrlId];
|
|||
|
|
if (sourceRoot != null) {
|
|||
|
|
url = '$sourceRoot$url';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sourceNameId = entry.sourceNameId;
|
|||
|
|
var file = files?[url];
|
|||
|
|
if (file != null) {
|
|||
|
|
var start = file.getOffset(entry.sourceLine!, entry.sourceColumn);
|
|||
|
|
if (sourceNameId != null) {
|
|||
|
|
var text = names[sourceNameId];
|
|||
|
|
return SourceMapFileSpan(file.span(start, start + text.length),
|
|||
|
|
isIdentifier: true);
|
|||
|
|
} else {
|
|||
|
|
return SourceMapFileSpan(file.location(start).pointSpan());
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
var start = SourceLocation(0,
|
|||
|
|
sourceUrl: _mapUrl?.resolve(url) ?? url,
|
|||
|
|
line: entry.sourceLine,
|
|||
|
|
column: entry.sourceColumn);
|
|||
|
|
|
|||
|
|
// Offset and other context is not available.
|
|||
|
|
if (sourceNameId != null) {
|
|||
|
|
return SourceMapSpan.identifier(start, names[sourceNameId]);
|
|||
|
|
} else {
|
|||
|
|
return SourceMapSpan(start, start, '');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String toString() {
|
|||
|
|
return (StringBuffer('$runtimeType : [')
|
|||
|
|
..write('targetUrl: ')
|
|||
|
|
..write(targetUrl)
|
|||
|
|
..write(', sourceRoot: ')
|
|||
|
|
..write(sourceRoot)
|
|||
|
|
..write(', urls: ')
|
|||
|
|
..write(urls)
|
|||
|
|
..write(', names: ')
|
|||
|
|
..write(names)
|
|||
|
|
..write(', lines: ')
|
|||
|
|
..write(lines)
|
|||
|
|
..write(']'))
|
|||
|
|
.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String get debugString {
|
|||
|
|
var buff = StringBuffer();
|
|||
|
|
for (var lineEntry in lines) {
|
|||
|
|
var line = lineEntry.line;
|
|||
|
|
for (var entry in lineEntry.entries) {
|
|||
|
|
buff
|
|||
|
|
..write(targetUrl)
|
|||
|
|
..write(': ')
|
|||
|
|
..write(line)
|
|||
|
|
..write(':')
|
|||
|
|
..write(entry.column);
|
|||
|
|
var sourceUrlId = entry.sourceUrlId;
|
|||
|
|
if (sourceUrlId != null) {
|
|||
|
|
buff
|
|||
|
|
..write(' --> ')
|
|||
|
|
..write(sourceRoot)
|
|||
|
|
..write(urls[sourceUrlId])
|
|||
|
|
..write(': ')
|
|||
|
|
..write(entry.sourceLine)
|
|||
|
|
..write(':')
|
|||
|
|
..write(entry.sourceColumn);
|
|||
|
|
}
|
|||
|
|
var sourceNameId = entry.sourceNameId;
|
|||
|
|
if (sourceNameId != null) {
|
|||
|
|
buff
|
|||
|
|
..write(' (')
|
|||
|
|
..write(names[sourceNameId])
|
|||
|
|
..write(')');
|
|||
|
|
}
|
|||
|
|
buff.write('\n');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return buff.toString();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A line entry read from a source map.
|
|||
|
|
class TargetLineEntry {
|
|||
|
|
final int line;
|
|||
|
|
List<TargetEntry> entries;
|
|||
|
|
TargetLineEntry(this.line, this.entries);
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String toString() => '$runtimeType: $line $entries';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A target segment entry read from a source map
|
|||
|
|
class TargetEntry {
|
|||
|
|
final int column;
|
|||
|
|
final int? sourceUrlId;
|
|||
|
|
final int? sourceLine;
|
|||
|
|
final int? sourceColumn;
|
|||
|
|
final int? sourceNameId;
|
|||
|
|
|
|||
|
|
TargetEntry(this.column,
|
|||
|
|
[this.sourceUrlId,
|
|||
|
|
this.sourceLine,
|
|||
|
|
this.sourceColumn,
|
|||
|
|
this.sourceNameId]);
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String toString() => '$runtimeType: '
|
|||
|
|
'($column, $sourceUrlId, $sourceLine, $sourceColumn, $sourceNameId)';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// A character iterator over a string that can peek one character ahead.
|
|||
|
|
class _MappingTokenizer implements Iterator<String> {
|
|||
|
|
final String _internal;
|
|||
|
|
final int _length;
|
|||
|
|
int index = -1;
|
|||
|
|
_MappingTokenizer(String internal)
|
|||
|
|
: _internal = internal,
|
|||
|
|
_length = internal.length;
|
|||
|
|
|
|||
|
|
// Iterator API is used by decodeVlq to consume VLQ entries.
|
|||
|
|
@override
|
|||
|
|
bool moveNext() => ++index < _length;
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
String get current => (index >= 0 && index < _length)
|
|||
|
|
? _internal[index]
|
|||
|
|
: throw RangeError.index(index, _internal);
|
|||
|
|
|
|||
|
|
bool get hasTokens => index < _length - 1 && _length > 0;
|
|||
|
|
|
|||
|
|
_TokenKind get nextKind {
|
|||
|
|
if (!hasTokens) return _TokenKind.eof;
|
|||
|
|
var next = _internal[index + 1];
|
|||
|
|
if (next == ';') return _TokenKind.line;
|
|||
|
|
if (next == ',') return _TokenKind.segment;
|
|||
|
|
return _TokenKind.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int _consumeValue() => decodeVlq(this);
|
|||
|
|
void _consumeNewLine() {
|
|||
|
|
++index;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _consumeNewSegment() {
|
|||
|
|
++index;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Print the state of the iterator, with colors indicating the current
|
|||
|
|
// position.
|
|||
|
|
@override
|
|||
|
|
String toString() {
|
|||
|
|
var buff = StringBuffer();
|
|||
|
|
for (var i = 0; i < index; i++) {
|
|||
|
|
buff.write(_internal[i]);
|
|||
|
|
}
|
|||
|
|
buff.write('[31m');
|
|||
|
|
try {
|
|||
|
|
buff.write(current);
|
|||
|
|
// TODO: Determine whether this try / catch can be removed.
|
|||
|
|
// ignore: avoid_catching_errors
|
|||
|
|
} on RangeError catch (_) {}
|
|||
|
|
buff.write('[0m');
|
|||
|
|
for (var i = index + 1; i < _internal.length; i++) {
|
|||
|
|
buff.write(_internal[i]);
|
|||
|
|
}
|
|||
|
|
buff.write(' ($index)');
|
|||
|
|
return buff.toString();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _TokenKind {
|
|||
|
|
static const _TokenKind line = _TokenKind(isNewLine: true);
|
|||
|
|
static const _TokenKind segment = _TokenKind(isNewSegment: true);
|
|||
|
|
static const _TokenKind eof = _TokenKind(isEof: true);
|
|||
|
|
static const _TokenKind value = _TokenKind();
|
|||
|
|
final bool isNewLine;
|
|||
|
|
final bool isNewSegment;
|
|||
|
|
final bool isEof;
|
|||
|
|
bool get isValue => !isNewLine && !isNewSegment && !isEof;
|
|||
|
|
|
|||
|
|
const _TokenKind(
|
|||
|
|
{this.isNewLine = false, this.isNewSegment = false, this.isEof = false});
|
|||
|
|
}
|