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'; |
8 | library; |
9 | |
10 | import 'dart:async'; |
11 | |
12 | import 'package:meta/meta.dart' show visibleForTesting; |
13 | |
14 | /// Signature for callbacks passed to [LicenseRegistry.addLicense]. |
15 | typedef LicenseEntryCollector = Stream<LicenseEntry> Function(); |
16 | |
17 | /// A string that represents one paragraph in a [LicenseEntry]. |
18 | /// |
19 | /// See [LicenseEntry.paragraphs]. |
20 | class 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]. |
49 | abstract 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 | |
63 | enum _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]. |
125 | class 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]. |
290 | abstract 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 | |