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