1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:meta/meta.dart' show visibleForTesting;
/// Signature for callbacks passed to [LicenseRegistry.addLicense].
typedef LicenseEntryCollector = Stream<LicenseEntry> Function();
/// A string that represents one paragraph in a [LicenseEntry].
///
/// See [LicenseEntry.paragraphs].
class LicenseParagraph {
/// Creates a string for a license entry paragraph.
const LicenseParagraph(this.text, this.indent);
/// The text of the paragraph. Should not have any leading or trailing whitespace.
final String text;
/// How many steps of indentation the paragraph has.
///
/// * 0 means the paragraph is not indented.
/// * 1 means the paragraph is indented one unit of indentation.
/// * 2 means the paragraph is indented two units of indentation.
///
/// ...and so forth.
///
/// In addition, the special value [centeredIndent] can be used to indicate
/// that rather than being indented, the paragraph is centered.
final int indent; // can be set to centeredIndent
/// A constant that represents "centered" alignment for [indent].
static const int centeredIndent = -1;
}
/// A license that covers part of the application's software or assets, to show
/// in an interface such as the [LicensePage].
///
/// For optimal performance, [LicenseEntry] objects should only be created on
/// demand in [LicenseEntryCollector] callbacks passed to
/// [LicenseRegistry.addLicense].
abstract class LicenseEntry {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const LicenseEntry();
/// The names of the packages that this license entry applies to.
Iterable<String> get packages;
/// The paragraphs of the license, each as a [LicenseParagraph] consisting of
/// a string and some formatting information. Paragraphs can include newline
/// characters, but this is discouraged as it results in ugliness.
Iterable<LicenseParagraph> get paragraphs;
}
enum _LicenseEntryWithLineBreaksParserState {
beforeParagraph,
inParagraph,
}
/// Variant of [LicenseEntry] for licenses that separate paragraphs with blank
/// lines and that hard-wrap text within paragraphs. Lines that begin with one
/// or more space characters are also assumed to introduce new paragraphs,
/// unless they start with the same number of spaces as the previous line, in
/// which case it's assumed they are a continuation of an indented paragraph.
///
/// {@tool snippet}
///
/// For example, the BSD license in this format could be encoded as follows:
///
/// ```dart
/// void initMyLibrary() {
/// LicenseRegistry.addLicense(() => Stream<LicenseEntry>.value(
/// const LicenseEntryWithLineBreaks(<String>['my_library'], '''
/// Copyright 2016 The Sample Authors. All rights reserved.
///
/// Redistribution and use in source and binary forms, with or without
/// modification, are permitted provided that the following conditions are
/// met:
///
/// * Redistributions of source code must retain the above copyright
/// notice, this list of conditions and the following disclaimer.
/// * Redistributions in binary form must reproduce the above
/// copyright notice, this list of conditions and the following disclaimer
/// in the documentation and/or other materials provided with the
/// distribution.
/// * Neither the name of Example Inc. nor the names of its
/// contributors may be used to endorse or promote products derived from
/// this software without specific prior written permission.
///
/// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
/// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
/// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
/// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
/// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
/// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
/// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
/// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
/// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
/// ),
/// ));
/// }
/// ```
/// {@end-tool}
///
/// This would result in a license with six [paragraphs], the third, fourth, and
/// fifth being indented one level.
///
/// ## Performance considerations
///
/// Computing the paragraphs is relatively expensive. Doing the work for one
/// license per frame is reasonable; doing more at the same time is ill-advised.
/// Consider doing all the work at once using [compute] to move the work to
/// another thread, or spreading the work across multiple frames using
/// [SchedulerBinding.scheduleTask].
class LicenseEntryWithLineBreaks extends LicenseEntry {
/// Create a license entry for a license whose text is hard-wrapped within
/// paragraphs and has paragraph breaks denoted by blank lines or with
/// indented text.
const LicenseEntryWithLineBreaks(this.packages, this.text);
@override
final List<String> packages;
/// The text of the license.
///
/// The text will be split into paragraphs according to the following
/// conventions:
///
/// * Lines starting with a different number of space characters than the
/// previous line start a new paragraph, with those spaces removed.
/// * Blank lines start a new paragraph.
/// * Other line breaks are replaced by a single space character.
/// * Leading spaces on a line are removed.
///
/// For each paragraph, the algorithm attempts (using some rough heuristics)
/// to identify how indented the paragraph is, or whether it is centered.
final String text;
@override
Iterable<LicenseParagraph> get paragraphs {
int lineStart = 0;
int currentPosition = 0;
int lastLineIndent = 0;
int currentLineIndent = 0;
int? currentParagraphIndentation;
_LicenseEntryWithLineBreaksParserState state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
final List<String> lines = <String>[];
final List<LicenseParagraph> result = <LicenseParagraph>[];
void addLine() {
assert(lineStart < currentPosition);
lines.add(text.substring(lineStart, currentPosition));
}
LicenseParagraph getParagraph() {
assert(lines.isNotEmpty);
assert(currentParagraphIndentation != null);
final LicenseParagraph result = LicenseParagraph(lines.join(' '), currentParagraphIndentation!);
assert(result.text.trimLeft() == result.text);
assert(result.text.isNotEmpty);
lines.clear();
return result;
}
while (currentPosition < text.length) {
switch (state) {
case _LicenseEntryWithLineBreaksParserState.beforeParagraph:
assert(lineStart == currentPosition);
switch (text[currentPosition]) {
case ' ':
lineStart = currentPosition + 1;
currentLineIndent += 1;
state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
break;
case '\t':
lineStart = currentPosition + 1;
currentLineIndent += 8;
state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
break;
case '\r':
case '\n':
case '\f':
if (lines.isNotEmpty) {
result.add(getParagraph());
}
if (text[currentPosition] == '\r' && currentPosition < text.length - 1
&& text[currentPosition + 1] == '\n') {
currentPosition += 1;
}
lastLineIndent = 0;
currentLineIndent = 0;
currentParagraphIndentation = null;
lineStart = currentPosition + 1;
state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
break;
case '[':
// This is a bit of a hack for the LGPL 2.1, which does something like this:
//
// [this is a
// single paragraph]
//
// ...near the top.
currentLineIndent += 1;
continue startParagraph;
startParagraph:
default:
if (lines.isNotEmpty && currentLineIndent > lastLineIndent) {
result.add(getParagraph());
currentParagraphIndentation = null;
}
// The following is a wild heuristic for guessing the indentation level.
// It happens to work for common variants of the BSD and LGPL licenses.
if (currentParagraphIndentation == null) {
if (currentLineIndent > 10) {
currentParagraphIndentation = LicenseParagraph.centeredIndent;
} else {
currentParagraphIndentation = currentLineIndent ~/ 3;
}
}
state = _LicenseEntryWithLineBreaksParserState.inParagraph;
}
break;
case _LicenseEntryWithLineBreaksParserState.inParagraph:
switch (text[currentPosition]) {
case '\n':
addLine();
lastLineIndent = currentLineIndent;
currentLineIndent = 0;
lineStart = currentPosition + 1;
state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
break;
case '\f':
addLine();
result.add(getParagraph());
lastLineIndent = 0;
currentLineIndent = 0;
currentParagraphIndentation = null;
lineStart = currentPosition + 1;
state = _LicenseEntryWithLineBreaksParserState.beforeParagraph;
break;
default:
state = _LicenseEntryWithLineBreaksParserState.inParagraph;
}
break;
}
currentPosition += 1;
}
switch (state) {
case _LicenseEntryWithLineBreaksParserState.beforeParagraph:
if (lines.isNotEmpty) {
result.add(getParagraph());
}
break;
case _LicenseEntryWithLineBreaksParserState.inParagraph:
addLine();
result.add(getParagraph());
break;
}
return result;
}
}
/// A registry for packages to add licenses to, so that they can be displayed
/// together in an interface such as the [LicensePage].
///
/// Packages can register their licenses using [addLicense]. User interfaces
/// that wish to show all the licenses can obtain them by calling [licenses].
///
/// The flutter tool will automatically collect the contents of all the LICENSE
/// files found at the root of each package into a single LICENSE file in the
/// default asset bundle. Each license in that file is separated from the next
/// by a line of eighty hyphens (`-`), and begins with a list of package names
/// that the license applies to, one to a line, separated from the next by a
/// blank line. The `services` package registers a license collector that splits
/// that file and adds each entry to the registry.
///
/// The LICENSE files in each package can either consist of a single license, or
/// can be in the format described above. In the latter case, each component
/// license and list of package names is merged independently.
///
/// See also:
///
/// * [showAboutDialog], which shows a Material-style dialog with information
/// about the application, including a button that shows a [LicensePage] that
/// uses this API to select licenses to show.
/// * [AboutListTile], which is a widget that can be added to a [Drawer]. When
/// tapped it calls [showAboutDialog].
class LicenseRegistry {
// This class is not meant to be instantiated or extended; this constructor
// prevents instantiation and extension.
LicenseRegistry._();
static List<LicenseEntryCollector>? _collectors;
/// Adds licenses to the registry.
///
/// To avoid actually manipulating the licenses unless strictly necessary,
/// licenses are added by adding a closure that returns a list of
/// [LicenseEntry] objects. The closure is only called if [licenses] is itself
/// called; in normal operation, if the user does not request to see the
/// licenses, the closure will not be called.
static void addLicense(LicenseEntryCollector collector) {
_collectors ??= <LicenseEntryCollector>[];
_collectors!.add(collector);
}
/// Returns the licenses that have been registered.
///
/// Generating the list of licenses is expensive.
static Stream<LicenseEntry> get licenses {
if (_collectors == null) {
return const Stream<LicenseEntry>.empty();
}
late final StreamController<LicenseEntry> controller;
controller = StreamController<LicenseEntry>(
onListen: () async {
for (final LicenseEntryCollector collector in _collectors!) {
await controller.addStream(collector());
}
await controller.close();
},
);
return controller.stream;
}
/// Resets the internal state of [LicenseRegistry]. Intended for use in
/// testing.
@visibleForTesting
static void reset() {
_collectors = null;
}
}