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 | import 'dart:async'; |
6 | |
7 | import 'package:meta/meta.dart' show visibleForTesting; |
8 | |
9 | /// Signature for callbacks passed to [LicenseRegistry.addLicense]. |
10 | typedef LicenseEntryCollector = Stream<LicenseEntry> Function(); |
11 | |
12 | /// A string that represents one paragraph in a [LicenseEntry]. |
13 | /// |
14 | /// See [LicenseEntry.paragraphs]. |
15 | class 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]. |
44 | abstract 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 | |
58 | enum _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]. |
120 | class 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]. |
285 | abstract 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 | |