Commit e5fb2fb0 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

An API for tracking software licenses. (#4711)

parent bbf31cd3
......@@ -13,5 +13,6 @@ export 'src/foundation/assertions.dart';
export 'src/foundation/basic_types.dart';
export 'src/foundation/binding.dart';
export 'src/foundation/change_notifier.dart';
export 'src/foundation/licenses.dart';
export 'src/foundation/print.dart';
export 'src/foundation/synchronous_future.dart';
......@@ -11,6 +11,7 @@ import 'package:meta/meta.dart';
import 'assertions.dart';
import 'basic_types.dart';
import 'licenses.dart';
/// Signature for service extensions.
///
......@@ -74,9 +75,14 @@ abstract class BindingBase {
/// `initInstances()`.
void initInstances() {
assert(!_debugInitialized);
LicenseRegistry.addLicense(_addLicenses);
assert(() { _debugInitialized = true; return true; });
}
Iterable<LicenseEntry> _addLicenses() sync* {
// TODO(ianh): Populate the license registry.
}
/// Called when the binding is initialized, to register service
/// extensions.
///
......
// 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.
/// Signature for callbacks passed to [LicenseRegistry.addLicense].
typedef Iterable<LicenseEntry> LicenseEntryCollector();
/// 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();
/// Returns each paragraph of the license as a [LicenseParagraph], which
/// consists 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.
///
/// For example, the BSD license in this format could be encoded as follows:
///
/// ```dart
/// LicenseRegistry.addLicense(() {
/// yield new LicenseEntryWithLineBreaks('''
/// 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.''');
/// }
/// ```
///
/// This would result in a license with six [paragraphs], the third, fourth, and
/// fifth being indented one level.
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.text);
/// 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;
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 = new 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 '\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 should register their licenses using [addLicense]. User interfaces
/// that wish to show all the licenses can obtain them by calling [licenses].
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.
///
/// Each time the iterable returned by this function is called, the callbacks
/// registered with [addLicense] are called again, which is relatively
/// expensive. For this reason, consider immediately converting the results to
/// a list with [Iterable.toList].
static Iterable<LicenseEntry> get licenses sync* {
if (_collectors == null)
return;
for (LicenseEntryCollector collector in _collectors)
yield* collector();
}
}
\ No newline at end of file
......@@ -301,7 +301,7 @@ class AboutDialog extends StatelessWidget {
///
/// To show a [LicensePage], use [showLicensePage].
// TODO(ianh): Mention the API for registering more licenses once it exists.
class LicensePage extends StatelessWidget {
class LicensePage extends StatefulWidget {
/// Creates a page that shows licenses for software used by the application.
///
/// The arguments are all optional. The application name, if omitted, will be
......@@ -337,27 +337,73 @@ class LicensePage extends StatelessWidget {
/// Defaults to the empty string.
final String applicationLegalese;
@override
_LicensePageState createState() => new _LicensePageState();
}
class _LicensePageState extends State<LicensePage> {
List<Widget> _licenses = _initLicenses();
static List<Widget> _initLicenses() {
List<Widget> result = <Widget>[];
for (LicenseEntry license in LicenseRegistry.licenses) {
bool haveMargin = true;
result.add(new Padding(
padding: new EdgeInsets.symmetric(vertical: 18.0),
child: new Text(
'🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere.
textAlign: TextAlign.center
)
));
for (LicenseParagraph paragraph in license.paragraphs) {
if (paragraph.indent == LicenseParagraph.centeredIndent) {
result.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 16.0),
child: new Text(
paragraph.text,
style: new TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center
)
));
} else {
assert(paragraph.indent >= 0);
result.add(new Padding(
padding: new EdgeInsets.only(top: haveMargin ? 0.0 : 8.0, left: 16.0 * paragraph.indent),
child: new Text(paragraph.text)
));
}
haveMargin = false;
}
}
return result;
}
@override
Widget build(BuildContext context) {
final String name = applicationName ?? _defaultApplicationName(context);
final String version = applicationVersion ?? _defaultApplicationVersion(context);
return new Scaffold(
appBar: new AppBar(
title: new Text('Licenses')
),
body: new Block(
padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0),
children: <Widget>[
final String name = config.applicationName ?? _defaultApplicationName(context);
final String version = config.applicationVersion ?? _defaultApplicationVersion(context);
final List<Widget> contents = <Widget>[
new Text(name, style: Theme.of(context).textTheme.headline, textAlign: TextAlign.center),
new Text(version, style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center),
new Container(height: 18.0),
new Text(applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center),
new Text(config.applicationLegalese ?? '', style: Theme.of(context).textTheme.caption, textAlign: TextAlign.center),
new Container(height: 18.0),
new Text('Powered by Flutter', style: Theme.of(context).textTheme.body1, textAlign: TextAlign.center),
new Container(height: 24.0),
// TODO(ianh): Fill in the licenses from the API for registering more licenses once it exists.
new Text('<licenses will be automatically included here>', style: Theme.of(context).textTheme.caption)
]
];
contents.addAll(_licenses);
return new Scaffold(
appBar: new AppBar(
title: new Text('Licenses')
),
body: new DefaultTextStyle(
style: Theme.of(context).textTheme.caption,
child: new LazyBlock(
padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0),
delegate: new LazyBlockChildren(
children: contents
)
)
)
);
}
......
// 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 'package:flutter/foundation.dart';
import 'package:test/test.dart';
void main() {
test('LicenseEntryWithLineBreaks - most cases', () {
// There's some trailing spaces in this string.
// To avoid IDEs stripping them, I've escaped them as \u0020.
List<LicenseParagraph> paragraphs = new LicenseEntryWithLineBreaks('''
A
A
A
B
B
B
C
C
C
D
D
D
E
E
F
G
G
G
[H
H
H]
\u0020\u0020
I J
K
K
L
L L
L L
L L
L L
L L
M
M\u0020\u0020\u0020
M\u0020\u0020\u0020\u0020
N
O
O
P
QQQ
RR RRR RRRR RRRRR
R
S
T
U
V
W
X
\u0020\u0020\u0020\u0020\u0020\u0020
Y''').paragraphs.toList();
int index = 0;
expect(paragraphs[index].text, 'A A A');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'B B B');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'C C C');
expect(paragraphs[index].indent, 1);
index += 1;
expect(paragraphs[index].text, 'D D D');
expect(paragraphs[index].indent, LicenseParagraph.centeredIndent);
index += 1;
expect(paragraphs[index].text, 'E E');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'F');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'G G G');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, '[H H H]');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'I');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'J');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'K K');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'L L L L L L L L L L L');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'M M M ');
expect(paragraphs[index].indent, 1);
index += 1;
expect(paragraphs[index].text, 'N');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'O O');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'P');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'QQQ');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'RR RRR RRRR RRRRR R');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'S');
expect(paragraphs[index].indent, 0);
index += 1;
expect(paragraphs[index].text, 'T');
expect(paragraphs[index].indent, 1);
index += 1;
expect(paragraphs[index].text, 'U');
expect(paragraphs[index].indent, 2);
index += 1;
expect(paragraphs[index].text, 'V');
expect(paragraphs[index].indent, 3);
index += 1;
expect(paragraphs[index].text, 'W');
expect(paragraphs[index].indent, 2);
index += 1;
expect(paragraphs[index].text, 'X');
expect(paragraphs[index].indent, 2);
index += 1;
expect(paragraphs[index].text, 'Y');
expect(paragraphs[index].indent, 2);
index += 1;
expect(paragraphs, hasLength(index));
});
test('LicenseEntryWithLineBreaks - leading and trailing whitespace', () {
expect(new LicenseEntryWithLineBreaks(' \n\n ').paragraphs.toList(), isEmpty);
List<LicenseParagraph> paragraphs;
paragraphs = new LicenseEntryWithLineBreaks(' \nA\n ').paragraphs.toList();
expect(paragraphs[0].text, 'A');
expect(paragraphs[0].indent, 0);
expect(paragraphs, hasLength(1));
paragraphs = new LicenseEntryWithLineBreaks('\n\n\nA\n\n\n').paragraphs.toList();
expect(paragraphs[0].text, 'A');
expect(paragraphs[0].indent, 0);
expect(paragraphs, hasLength(1));
});
test('LicenseRegistry', () {
expect(LicenseRegistry.licenses, isEmpty);
LicenseRegistry.addLicense(() {
return <LicenseEntry>[
new LicenseEntryWithLineBreaks('A'),
new LicenseEntryWithLineBreaks('B'),
];
});
LicenseRegistry.addLicense(() {
return <LicenseEntry>[
new LicenseEntryWithLineBreaks('C'),
new LicenseEntryWithLineBreaks('D'),
];
});
expect(LicenseRegistry.licenses, hasLength(4));
List<LicenseEntry> licenses = LicenseRegistry.licenses.toList();
expect(licenses, hasLength(4));
expect(licenses[0].paragraphs.single.text, 'A');
expect(licenses[1].paragraphs.single.text, 'B');
expect(licenses[2].paragraphs.single.text, 'C');
expect(licenses[3].paragraphs.single.text, 'D');
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment