5
5
import 'package:meta/meta.dart' ;
6
6
import 'package:source_span/source_span.dart' ;
7
7
8
- import '../../interpolation_buffer.dart' ;
9
8
import 'expression.dart' ;
10
9
import 'node.dart' ;
11
10
@@ -19,6 +18,15 @@ final class Interpolation implements SassNode {
19
18
/// [String] s.
20
19
final List <Object /* String | Expression */ > contents;
21
20
21
+ /// The source spans for each [Expression] in [contents] .
22
+ ///
23
+ /// Unlike [Expression.span] , which just covers the expresssion itself, this
24
+ /// should go from `#{` through `}` .
25
+ ///
26
+ /// @nodoc
27
+ @internal
28
+ final List <FileSpan ?> spans;
29
+
22
30
final FileSpan span;
23
31
24
32
/// Returns whether this contains no interpolated expressions.
@@ -37,42 +45,62 @@ final class Interpolation implements SassNode {
37
45
String get initialPlain =>
38
46
switch (contents) { [String first, ...] => first, _ => '' };
39
47
40
- /// Creates a new [Interpolation] by concatenating a sequence of [String] s,
41
- /// [Expression] s, or nested [Interpolation] s .
42
- static Interpolation concat (
43
- Iterable < Object /* String | Expression | Interpolation */ > contents,
44
- FileSpan span) {
45
- var buffer = InterpolationBuffer ();
46
- for ( var element in contents) {
47
- switch (element) {
48
- case String () :
49
- buffer. write (element);
50
- case Expression () :
51
- buffer. add (element);
52
- case Interpolation () :
53
- buffer. addInterpolation (element);
54
- case _ :
55
- throw ArgumentError . value (contents, "contents" ,
56
- "May only contains Strings, Expressions, or Interpolations." );
57
- }
58
- }
48
+ /// Returns the [FileSpan] covering the element of the interpolation at
49
+ /// [index] .
50
+ ///
51
+ /// Unlike `contents[index].span` , which only covers the text of the
52
+ /// expression itself, this typically covers the entire `#{}` that surrounds
53
+ /// the expression. However, this is not a strong guarantee—there are cases
54
+ /// where interpolations are constructed when the source uses Sass expressions
55
+ /// directly where this may return the same value as `contents[index].span` .
56
+ ///
57
+ /// For string elements, this is the span that covers the entire text of the
58
+ /// string, including the quote for text at the beginning or end of quoted
59
+ /// strings. Note that the quote is *never* included for expressions.
60
+ FileSpan spanForElement ( int index) => switch (contents[index]) {
61
+ String () => span.file. span (
62
+ (index == 0 ? span.start : spans[index - 1 ] ! .end).offset,
63
+ (index == spans.length ? span.end : spans[index + 1 ] ! .start)
64
+ .offset),
65
+ _ => spans[index] !
66
+ };
59
67
60
- return buffer.interpolation (span);
61
- }
68
+ Interpolation .plain (String text, this .span)
69
+ : contents = List .unmodifiable ([text]),
70
+ spans = const [null ];
71
+
72
+ /// Creates a new [Interpolation] with the given [contents] .
73
+ ///
74
+ /// The [spans] must include a [FileSpan] for each [Expression] in [contents] .
75
+ /// These spans should generally cover the entire `#{}` surrounding the
76
+ /// expression.
77
+ ///
78
+ /// The single [span] must cover the entire interpolation.
79
+ Interpolation (Iterable <Object /* String | Expression */ > contents,
80
+ Iterable <FileSpan ?> spans, this .span)
81
+ : contents = List .unmodifiable (contents),
82
+ spans = List .unmodifiable (spans) {
83
+ if (spans.length != contents.length) {
84
+ throw ArgumentError .value (
85
+ this .spans, "spans" , "Must be the same length as contents." );
86
+ }
62
87
63
- Interpolation (Iterable <Object /* String | Expression */ > contents, this .span)
64
- : contents = List .unmodifiable (contents) {
65
88
for (var i = 0 ; i < this .contents.length; i++ ) {
66
- if (this .contents[i] is ! String && this .contents[i] is ! Expression ) {
89
+ var isString = this .contents[i] is String ;
90
+ if (! isString && this .contents[i] is ! Expression ) {
67
91
throw ArgumentError .value (this .contents, "contents" ,
68
- "May only contains Strings or Expressions." );
69
- }
70
-
71
- if (i != 0 &&
72
- this .contents[i - 1 ] is String &&
73
- this .contents[i] is String ) {
74
- throw ArgumentError .value (
75
- this .contents, "contents" , "May not contain adjacent Strings." );
92
+ "May only contain Strings or Expressions." );
93
+ } else if (isString) {
94
+ if (i != 0 && this .contents[i - 1 ] is String ) {
95
+ throw ArgumentError .value (
96
+ this .contents, "contents" , "May not contain adjacent Strings." );
97
+ } else if (i < spans.length && this .spans[i] != null ) {
98
+ throw ArgumentError .value (this .spans, "spans" ,
99
+ "May not have a value for string elements (at index $i )." );
100
+ }
101
+ } else if (i >= spans.length || this .spans[i] == null ) {
102
+ throw ArgumentError .value (this .spans, "spans" ,
103
+ "Must not have a value for expression elements (at index $i )." );
76
104
}
77
105
}
78
106
}
0 commit comments