Add chips to class pages for class modifiers (#3401)
* Add chips to class pages
* rebuild
* fullkind no longer shows abstract, that is intentional
diff --git a/lib/resources/styles.css b/lib/resources/styles.css
index 33c0244..09b85b5 100644
--- a/lib/resources/styles.css
+++ b/lib/resources/styles.css
@@ -550,7 +550,7 @@
vertical-align: middle;
}
-/* The badge under a declaration for things like "const", "read-only", etc. and for the badges inline like Null safety*/
+/* The badge under a declaration for things like "const", "read-only", etc. and for the badges inline like sealed or interface */
/* See https://github.com/dart-lang/dartdoc/blob/main/lib/src/model/feature.dart */
.feature {
display: inline-block;
@@ -570,6 +570,7 @@
h1 .feature {
vertical-align: middle;
+ margin: 0 -2px 0 0;
}
.source-link {
diff --git a/lib/src/generator/templates.runtime_renderers.dart b/lib/src/generator/templates.runtime_renderers.dart
index 8da62b4..cf3fa01 100644
--- a/lib/src/generator/templates.runtime_renderers.dart
+++ b/lib/src/generator/templates.runtime_renderers.dart
@@ -6867,6 +6867,19 @@
parent: r);
},
),
+ 'displayedLanguageFeatures': Property(
+ getValue: (CT_ c) => c.displayedLanguageFeatures,
+ renderVariable: (CT_ c, Property<CT_> self,
+ List<String> remainingNames) =>
+ self.renderSimpleVariable(
+ c, remainingNames, 'List<LanguageFeature>'),
+ renderIterable: (CT_ c, RendererBase<CT_> r,
+ List<MustachioNode> ast, StringSink sink) {
+ return c.displayedLanguageFeatures.map((e) =>
+ _render_LanguageFeature(e, ast, r.template, sink,
+ parent: r));
+ },
+ ),
'element': Property(
getValue: (CT_ c) => c.element,
renderVariable: (CT_ c, Property<CT_> self,
diff --git a/lib/src/model/container_modifiers.dart b/lib/src/model/container_modifiers.dart
index 94ebf08..6472054 100644
--- a/lib/src/model/container_modifiers.dart
+++ b/lib/src/model/container_modifiers.dart
@@ -2,7 +2,8 @@
// for details. 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:collection/collection.dart' show IterableExtension;
+import 'package:dartdoc/src/model/language_feature.dart';
+import 'package:dartdoc/src/render/language_feature_renderer.dart';
/// Represents a single modifier applicable to containers.
class ContainerModifier implements Comparable<ContainerModifier> {
@@ -15,6 +16,7 @@
/// The display order of this modifier.
final int order;
+
const ContainerModifier._(
this.name, {
required this.order,
@@ -41,10 +43,11 @@
static const ContainerModifier mixin = ContainerModifier._('mixin', order: 4);
}
-extension ContainerModifiers on Iterable<ContainerModifier> {
- /// Returns a string suitable for prefixing the class name in Dartdoc
- /// title bars based on given modifiers.
- String modifiersAsFullKindPrefix() => sorted((a, b) => a.compareTo(b))
- .where((m) => !m.hideIfPresent.any(contains))
- .join(' ');
+extension BuildLanguageFeatureSet on Iterable<ContainerModifier> {
+ /// Transforms [ContainerModifiers] into a series of [LanguageFeature] objects
+ /// suitable for rendering as chips. Assumes iterable is sorted.
+ Iterable<LanguageFeature> asLanguageFeatureSet(
+ LanguageFeatureRenderer languageFeatureRenderer) =>
+ where((m) => !m.hideIfPresent.any(contains))
+ .map((m) => LanguageFeature(m.name, languageFeatureRenderer));
}
diff --git a/lib/src/model/inheriting_container.dart b/lib/src/model/inheriting_container.dart
index 756d652..9b04906 100644
--- a/lib/src/model/inheriting_container.dart
+++ b/lib/src/model/inheriting_container.dart
@@ -9,6 +9,7 @@
import 'package:dartdoc/src/model/comment_referable.dart';
import 'package:dartdoc/src/model/container_modifiers.dart';
import 'package:dartdoc/src/model/extension_target.dart';
+import 'package:dartdoc/src/model/language_feature.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/model_utils.dart' as model_utils;
import 'package:meta/meta.dart';
@@ -87,14 +88,21 @@
/// Class modifiers from the Dart feature specification.
///
/// These apply to or have some meaning for [Class]es and [Mixin]s.
- late List<ContainerModifier> containerModifiers = [
+ late final List<ContainerModifier> containerModifiers = [
if (isAbstract) ContainerModifier.abstract,
if (isSealed) ContainerModifier.sealed,
if (isBase) ContainerModifier.base,
if (isInterface) ContainerModifier.interface,
if (isFinal) ContainerModifier.finalModifier,
if (isMixinClass) ContainerModifier.mixin,
- ];
+ ]..sort();
+
+ @override
+ late final List<LanguageFeature> displayedLanguageFeatures =
+ containerModifiers
+ .asLanguageFeatureSet(
+ packageGraph.rendererFactory.languageFeatureRenderer)
+ .toList();
late final List<ModelElement> _allModelElements = [
...super.allModelElements,
@@ -267,12 +275,7 @@
@override
Library get enclosingElement => library;
- String get fullkind {
- if (containerModifiers.isNotEmpty) {
- return '${containerModifiers.modifiersAsFullKindPrefix()} $kind';
- }
- return kind;
- }
+ String get fullkind => kind;
@override
bool get hasModifiers =>
diff --git a/lib/src/model/language_feature.dart b/lib/src/model/language_feature.dart
index 0813f2a..41df7e5 100644
--- a/lib/src/model/language_feature.dart
+++ b/lib/src/model/language_feature.dart
@@ -4,12 +4,33 @@
import 'package:dartdoc/src/render/language_feature_renderer.dart';
-const Map<String, String> _featureDescriptions = {};
+const Map<String, String> _featureDescriptions = {
+ 'sealed': 'All direct subtypes must be defined in the same library.',
+ 'abstract': 'This type can not be directly constructed.',
+ 'base': 'This type can only be extended (not implemented or mixed in).',
+ 'interface': 'This type can only be implemented (not extended or mixed in).',
+ 'final': 'This type can neither be extended, implemented, nor mixed in.',
+ 'mixin': 'This type can be used as a class and a mixin.',
+};
-const Map<String, String> _featureUrls = {};
+const Map<String, String> _featureUrls = {
+ // TODO(jcollins-g): link to dart.dev for all links once documentation is
+ // available.
+ 'sealed':
+ 'https://github.com/dart-lang/language/blob/main/accepted/future-releases/sealed-types/feature-specification.md#sealed-types',
+ 'abstract': 'https://dart.dev/language/classes#abstract-classes',
+ 'base':
+ 'https://github.com/dart-lang/language/blob/main/accepted/future-releases/class-modifiers/feature-specification.md#class-modifiers',
+ 'interface':
+ 'https://github.com/dart-lang/language/blob/main/accepted/future-releases/class-modifiers/feature-specification.md#class-modifiers',
+ 'final':
+ 'https://github.com/dart-lang/language/blob/main/accepted/future-releases/class-modifiers/feature-specification.md#class-modifiers',
+ 'mixin':
+ 'https://github.com/dart-lang/language/blob/main/accepted/future-releases/class-modifiers/feature-specification.md#class-modifiers',
+};
-/// An abstraction for a language feature; used to render tags to notify
-/// the user that the documentation should be specially interpreted.
+/// An abstraction for a language feature; used to render tags ('chips') to
+/// notify the user that the documentation should be specially interpreted.
class LanguageFeature {
/// The description of this language feature.
String? get featureDescription => _featureDescriptions[name];
diff --git a/test/class_modifiers_test.dart b/test/class_modifiers_test.dart
index e2c2dab..9d8bd8b 100644
--- a/test/class_modifiers_test.dart
+++ b/test/class_modifiers_test.dart
@@ -2,12 +2,18 @@
// for details. 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:dartdoc/src/model/model.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'dartdoc_test_base.dart';
import 'src/utils.dart';
+extension on InheritingContainer {
+ String languageFeatureChips() =>
+ displayedLanguageFeatures.map((l) => l.name).join(' ');
+}
+
void main() {
defineReflectiveSuite(() {
if (classModifiersAllowed) {
@@ -63,21 +69,21 @@
var Mclass = library.classes.named('M');
var Nmixin = library.mixins.named('N');
var Omixin = library.mixins.named('O');
- expect(Aclass.fullkind, equals('class'));
- expect(Bclass.fullkind, equals('base class'));
- expect(Cclass.fullkind, equals('interface class'));
- expect(Dclass.fullkind, equals('final class'));
- expect(Eclass.fullkind, equals('sealed class'));
- expect(Fclass.fullkind, equals('abstract class'));
- expect(Gclass.fullkind, equals('abstract base class'));
- expect(Hclass.fullkind, equals('abstract interface class'));
- expect(Iclass.fullkind, equals('abstract final class'));
- expect(Jclass.fullkind, equals('mixin class'));
- expect(Kclass.fullkind, equals('base mixin class'));
- expect(Lclass.fullkind, equals('abstract mixin class'));
- expect(Mclass.fullkind, equals('abstract base mixin class'));
- expect(Nmixin.fullkind, equals('mixin'));
- expect(Omixin.fullkind, equals('base mixin'));
+ expect(Aclass.languageFeatureChips(), equals(''));
+ expect(Bclass.languageFeatureChips(), equals('base'));
+ expect(Cclass.languageFeatureChips(), equals('interface'));
+ expect(Dclass.languageFeatureChips(), equals('final'));
+ expect(Eclass.languageFeatureChips(), equals('sealed'));
+ expect(Fclass.languageFeatureChips(), equals('abstract'));
+ expect(Gclass.languageFeatureChips(), equals('abstract base'));
+ expect(Hclass.languageFeatureChips(), equals('abstract interface'));
+ expect(Iclass.languageFeatureChips(), equals('abstract final'));
+ expect(Jclass.languageFeatureChips(), equals('mixin'));
+ expect(Kclass.languageFeatureChips(), equals('base mixin'));
+ expect(Lclass.languageFeatureChips(), equals('abstract mixin'));
+ expect(Mclass.languageFeatureChips(), equals('abstract base mixin'));
+ expect(Nmixin.languageFeatureChips(), equals(''));
+ expect(Omixin.languageFeatureChips(), equals('base'));
}
void test_abstractSealed() async {
@@ -86,7 +92,8 @@
sealed class B extends A {}
''');
var Bclass = library.classes.named('B');
- expect(Bclass.fullkind, equals('sealed class')); // *not* sealed abstract
+ expect(Bclass.languageFeatureChips(),
+ equals('sealed')); // *not* sealed abstract
}
void test_inferredModifiers() async {
@@ -116,13 +123,14 @@
var Iclass = library.classes.named('I');
var Lclass = library.classes.named('L');
var Mclass = library.classes.named('M');
- expect(Bclass.fullkind, equals('sealed class')); // *not* sealed base
- expect(Cclass.fullkind, equals('base class'));
- expect(Eclass.fullkind, equals('sealed class'));
- expect(Fclass.fullkind, equals('interface class'));
- expect(Hclass.fullkind, equals('sealed class'));
- expect(Iclass.fullkind, equals('final class'));
- expect(Lclass.fullkind, equals('sealed class'));
- expect(Mclass.fullkind, equals('base class'));
+ expect(
+ Bclass.languageFeatureChips(), equals('sealed')); // *not* sealed base
+ expect(Cclass.languageFeatureChips(), equals('base'));
+ expect(Eclass.languageFeatureChips(), equals('sealed'));
+ expect(Fclass.languageFeatureChips(), equals('interface'));
+ expect(Hclass.languageFeatureChips(), equals('sealed'));
+ expect(Iclass.languageFeatureChips(), equals('final'));
+ expect(Lclass.languageFeatureChips(), equals('sealed'));
+ expect(Mclass.languageFeatureChips(), equals('base'));
}
}
diff --git a/test/container_modifiers_test.dart b/test/container_modifiers_test.dart
index b1b77f9..a952a96 100644
--- a/test/container_modifiers_test.dart
+++ b/test/container_modifiers_test.dart
@@ -3,27 +3,41 @@
// BSD-style license that can be found in the LICENSE file.
import 'package:dartdoc/src/model/container_modifiers.dart';
+import 'package:dartdoc/src/model/language_feature.dart';
+import 'package:dartdoc/src/render/language_feature_renderer.dart';
import 'package:test/test.dart';
+class TestChipRenderer extends LanguageFeatureRenderer {
+ @override
+ String renderLanguageFeatureLabel(LanguageFeature l) => l.name;
+}
+
+extension TestChipsRenderer on Iterable<LanguageFeature> {
+ String asRenderedString() => map((l) => l.featureLabel).join(' ');
+}
+
void main() {
group('fullKind string tests', () {
test('basic', () {
- var l = {
+ var l = [
ContainerModifier.base,
ContainerModifier.interface,
ContainerModifier.abstract
- };
- expect(l.modifiersAsFullKindPrefix(), equals('abstract base interface'));
+ ]..sort();
+ expect(l.asLanguageFeatureSet(TestChipRenderer()).asRenderedString(),
+ equals('abstract base interface'));
});
test('hide abstract on sealed', () {
- var l = {ContainerModifier.abstract, ContainerModifier.sealed};
- expect(l.modifiersAsFullKindPrefix(), equals('sealed'));
+ var l = [ContainerModifier.abstract, ContainerModifier.sealed]..sort();
+ expect(l.asLanguageFeatureSet(TestChipRenderer()).asRenderedString(),
+ equals('sealed'));
});
test('empty', () {
- var l = <ContainerModifier>{};
- expect(l.modifiersAsFullKindPrefix(), equals(''));
+ var l = <ContainerModifier>[];
+ expect(l.asLanguageFeatureSet(TestChipRenderer()).asRenderedString(),
+ equals(''));
});
});
}
diff --git a/test/end2end/model_test.dart b/test/end2end/model_test.dart
index 94a73c9..2e27dd6 100644
--- a/test/end2end/model_test.dart
+++ b/test/end2end/model_test.dart
@@ -1991,10 +1991,6 @@
expect(interfaces[1].name, 'E');
});
- test('class title has abstract keyword', () {
- expect(Cat.fullkind, 'abstract class');
- });
-
test('class title has no abstract keyword', () {
expect(Dog.fullkind, 'class');
});