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