// Copyright 2016 The Chromium 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 sample} /// /// For example, the BSD license in this format could be encoded as follows: /// /// ```dart /// void initMyLibrary() { /// LicenseRegistry.addLicense(() async* { /// yield 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 /// [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 sync* { int lineStart = 0; int currentPosition = 0; int lastLineIndent = 0; int currentLineIndent = 0; int currentParagraphIndentation; _LicenseEntryWithLineBreaksParserState state = _LicenseEntryWithLineBreaksParserState.beforeParagraph; final List<String> lines = <String>[]; 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 '\n': case '\f': if (lines.isNotEmpty) { yield getParagraph(); } 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) { yield 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(); yield 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) { yield getParagraph(); } break; case _LicenseEntryWithLineBreaksParserState.inParagraph: addLine(); yield getParagraph(); break; } } } /// 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 { 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 async* { if (_collectors == null) return; for (LicenseEntryCollector collector in _collectors) yield* collector(); } /// Resets the internal state of [LicenseRegistry]. Intended for use in /// testing. @visibleForTesting static void reset() { _collectors = null; } }