Skip to content

Commit d834d1a

Browse files
gspencergoogjcollins-g
authored andcommitted
Adding animation directive for injecting animated diagrams (#1698)
* Adding animation directive for injecting animated diagrams * Updated Testing Goldens * Adding two more tests, updating goldens again
1 parent 37273d8 commit d834d1a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1766
-11
lines changed

lib/resources/play_button.svg

Lines changed: 1 addition & 0 deletions
Loading

lib/src/model.dart

Lines changed: 152 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,7 @@ abstract class ModelElement extends Canonicalization
28782878
_rawDocs = computeDocumentationComment ?? '';
28792879
_rawDocs = stripComments(_rawDocs) ?? '';
28802880
_rawDocs = _injectExamples(_rawDocs);
2881+
_rawDocs = _injectAnimations(_rawDocs);
28812882
_rawDocs = _stripMacroTemplatesAndAddToIndex(_rawDocs);
28822883
}
28832884
return _rawDocs;
@@ -3527,24 +3528,156 @@ abstract class ModelElement extends Canonicalization
35273528
});
35283529
}
35293530

3530-
/// Replace {@macro ...} in API comments with the contents of the macro
3531+
/// Replace {@animation ...} in API comments with some HTML to manage an
3532+
/// MPEG 4 video as an animation.
35313533
///
35323534
/// Syntax:
35333535
///
3534-
/// {@macro NAME}
3536+
/// {@animation NAME WIDTH HEIGHT URL}
3537+
///
3538+
/// Example:
3539+
///
3540+
/// {@animation my_video 300 300 https://example.com/path/to/video.mp4}
3541+
///
3542+
/// Which will render the HTML necessary for embedding a simple click-to-play
3543+
/// HTML5 video player with no controls.
3544+
///
3545+
/// The NAME should be a unique name that is a valid javascript identifier,
3546+
/// and will be used as the id for the video tag.
3547+
///
3548+
/// The width and height must be integers specifying the dimensions of the
3549+
/// video file in pixels.
3550+
String _injectAnimations(String rawDocs) {
3551+
// Matches all animation directives (even some invalid ones). This is so
3552+
// we can give good error messages if the directive is malformed, instead of
3553+
// just silently emitting it as-is.
3554+
final RegExp basicAnimationRegExp =
3555+
new RegExp(r'''{@animation\s+([^}]+)}''');
3556+
3557+
// Animations have four parameters, and the last one can be surrounded by
3558+
// quotes (which are ignored). This RegExp is used to validate the directive
3559+
// for the correct number of parameters.
3560+
final RegExp animationRegExp =
3561+
new RegExp(r'''{@animation\s+([^}\s]+)\s+([^}\s]+)\s+([^}\s]+)'''
3562+
r'''\s+['"]?([^}]+)['"]?}''');
3563+
3564+
// Matches valid javascript identifiers.
3565+
final RegExp validNameRegExp = new RegExp(r'^[a-zA-Z_][a-zA-Z0-9_]*$');
3566+
3567+
// Keeps names unique.
3568+
final Set<String> uniqueNames = new Set<String>();
3569+
3570+
return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) {
3571+
final Match match = animationRegExp.firstMatch(basicMatch[0]);
3572+
if (match == null) {
3573+
warn(PackageWarning.invalidParameter,
3574+
message: 'Invalid @animation directive: ${basicMatch[0]}\n'
3575+
'Animation directives must be of the form: {@animation NAME '
3576+
'WIDTH HEIGHT URL}');
3577+
return '';
3578+
}
3579+
String name = match[1];
3580+
if (!validNameRegExp.hasMatch(name)) {
3581+
warn(PackageWarning.invalidParameter,
3582+
message: 'An animation has an invalid name: $name. The name can '
3583+
'only contain letters, numbers and underscores.');
3584+
return '';
3585+
} else {
3586+
if (uniqueNames.contains(name)) {
3587+
warn(PackageWarning.invalidParameter,
3588+
message:
3589+
'An animation has a non-unique name: $name. Animation names '
3590+
'must be unique.');
3591+
return '';
3592+
}
3593+
uniqueNames.add(name);
3594+
}
3595+
int width;
3596+
try {
3597+
width = int.parse(match[2]);
3598+
} on FormatException {
3599+
warn(PackageWarning.invalidParameter,
3600+
message: 'An animation has an invalid width ($name): ${match[2]}. The '
3601+
'width must be an integer.');
3602+
return '';
3603+
}
3604+
int height;
3605+
try {
3606+
height = int.parse(match[3]);
3607+
} on FormatException {
3608+
warn(PackageWarning.invalidParameter,
3609+
message: 'An animation has an invalid height ($name): ${match[3]}. The '
3610+
'height must be an integer.');
3611+
return '';
3612+
}
3613+
Uri movieUrl;
3614+
try {
3615+
movieUrl = Uri.parse(match[4]);
3616+
} on FormatException catch (e) {
3617+
warn(PackageWarning.invalidParameter,
3618+
message: 'An animation URL could not be parsed ($name): ${match[4]}\n$e');
3619+
return '';
3620+
}
3621+
final String overlayName = '${name}_play_button_';
3622+
3623+
// Blank lines before and after, and no indenting at the beginning and end
3624+
// is needed so that Markdown doesn't confuse this with code, so be
3625+
// careful of whitespace here.
3626+
return '''
3627+
3628+
<div style="position: relative;">
3629+
<div id="${overlayName}"
3630+
onclick="if ($name.paused) {
3631+
$name.play();
3632+
this.style.display = 'none';
3633+
} else {
3634+
$name.pause();
3635+
this.style.display = 'block';
3636+
}"
3637+
style="position:absolute;
3638+
width:${width}px;
3639+
height:${height}px;
3640+
z-index:100000;
3641+
background-position: center;
3642+
background-repeat: no-repeat;
3643+
background-image: url(static-assets/play_button.svg);">
3644+
</div>
3645+
<video id="$name"
3646+
style="width:${width}px; height:${height}px;"
3647+
onclick="if (this.paused) {
3648+
this.play();
3649+
$overlayName.style.display = 'none';
3650+
} else {
3651+
this.pause();
3652+
$overlayName.style.display = 'block';
3653+
}" loop>
3654+
<source src="$movieUrl" type="video/mp4"/>
3655+
</video>
3656+
</div>
3657+
3658+
'''; // String must end at beginning of line, or following inline text will be
3659+
// indented.
3660+
});
3661+
}
3662+
3663+
/// Replace &#123;@macro ...&#125; in API comments with the contents of the macro
3664+
///
3665+
/// Syntax:
3666+
///
3667+
/// &#123;@macro NAME&#125;
35353668
///
35363669
/// Example:
35373670
///
35383671
/// You define the template in any comment for a documentable entity like:
35393672
///
3540-
/// {@template foo}
3673+
/// &#123;@template foo&#125;
35413674
/// Foo contents!
3542-
/// {@endtemplate}
3675+
/// &#123;@endtemplate&#125;
35433676
///
35443677
/// and them somewhere use it like this:
35453678
///
35463679
/// Some comments
3547-
/// {@macro foo}
3680+
/// &#123;@macro foo&#125;
35483681
/// More comments
35493682
///
35503683
/// Which will render
@@ -3564,13 +3697,13 @@ abstract class ModelElement extends Canonicalization
35643697
});
35653698
}
35663699

3567-
/// Parse {@template ...} in API comments and store them in the index on the package.
3700+
/// Parse &#123;@template ...&#125; in API comments and store them in the index on the package.
35683701
///
35693702
/// Syntax:
35703703
///
3571-
/// {@template NAME}
3704+
/// &#123;@template NAME&#125;
35723705
/// The contents of the macro
3573-
/// {@endtemplate}
3706+
/// &#123;@endtemplate&#125;
35743707
///
35753708
String _stripMacroTemplatesAndAddToIndex(String rawDocs) {
35763709
final templateRegExp = new RegExp(
@@ -4133,6 +4266,9 @@ class PackageGraph extends Canonicalization
41334266
// so bracket with a triple quote for defense.
41344267
warningMessage = 'generic type handled as HTML: """${message}"""';
41354268
break;
4269+
case PackageWarning.invalidParameter:
4270+
warningMessage = 'invalid parameter to dartdoc directive: ${message}';
4271+
break;
41364272
}
41374273

41384274
List<String> messageParts = [warningMessage];
@@ -5434,7 +5570,14 @@ class PackageBuilder {
54345570
// TODO(jcollins-g): explode this into detailed command line options.
54355571
if (config.showWarnings) {
54365572
for (PackageWarning kind in PackageWarning.values) {
5437-
warningOptions.warn(kind);
5573+
switch (kind) {
5574+
case PackageWarning.invalidParameter:
5575+
warningOptions.error(kind);
5576+
break;
5577+
default:
5578+
warningOptions.warn(kind);
5579+
break;
5580+
}
54385581
}
54395582
}
54405583
return warningOptions;

lib/src/warnings.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ final Map<PackageWarning, PackageWarningHelpText> packageWarningText = const {
8686
PackageWarning.typeAsHtml,
8787
"typeAsHtml",
8888
"Use of <> in a comment for type parameters is being treated as HTML by markdown"),
89+
PackageWarning.invalidParameter: const PackageWarningHelpText(
90+
PackageWarning.invalidParameter,
91+
"invalidParameter",
92+
"A parameter given to a dartdoc directive was invalid."),
8993
};
9094

9195
/// Something that package warnings can be called on. Optionally associated
@@ -125,6 +129,7 @@ enum PackageWarning {
125129
unknownFile,
126130
missingFromSearchIndex,
127131
typeAsHtml,
132+
invalidParameter,
128133
}
129134

130135
/// Warnings it is OK to skip if we can determine the warnable isn't documented.

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,4 +394,4 @@ packages:
394394
source: hosted
395395
version: "2.1.13"
396396
sdks:
397-
dart: ">=2.0.0-dev.54.0 <=2.0.0-dev.55.0"
397+
dart: ">=2.0.0-dev.54.0 <=2.0.0-dev.58.0"

test/model_test.dart

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,86 @@ void main() {
496496
});
497497
});
498498

499+
group('Animation', () {
500+
Class dog;
501+
Method withAnimation;
502+
Method withAnimationNonUnique;
503+
Method withAnimationWrongParams;
504+
Method withAnimationBadWidth;
505+
Method withAnimationBadHeight;
506+
Method withAnimationInOneLineDoc;
507+
Method withAnimationInline;
508+
509+
setUp(() {
510+
dog = exLibrary.classes.firstWhere((c) => c.name == 'Dog');
511+
withAnimation =
512+
dog.allInstanceMethods.firstWhere((m) => m.name == 'withAnimation');
513+
withAnimationNonUnique = dog.allInstanceMethods
514+
.firstWhere((m) => m.name == 'withAnimationNonUnique');
515+
withAnimationWrongParams = dog.allInstanceMethods
516+
.firstWhere((m) => m.name == 'withAnimationWrongParams');
517+
withAnimationBadWidth = dog.allInstanceMethods
518+
.firstWhere((m) => m.name == 'withAnimationBadWidth');
519+
withAnimationBadHeight = dog.allInstanceMethods
520+
.firstWhere((m) => m.name == 'withAnimationBadHeight');
521+
withAnimationInOneLineDoc = dog.allInstanceMethods
522+
.firstWhere((m) => m.name == 'withAnimationInOneLineDoc');
523+
withAnimationInline = dog.allInstanceMethods
524+
.firstWhere((m) => m.name == 'withAnimationInline');
525+
packageGraph.allLocalModelElements.forEach((m) => m.documentation);
526+
});
527+
528+
test("renders an animation within the method documentation", () {
529+
expect(
530+
withAnimation.documentation, contains('<video id="methodAnimation"'));
531+
});
532+
test("warns on a non-unique animation within a method", () {
533+
expect(
534+
packageGraph.packageWarningCounter.hasWarning(
535+
withAnimationNonUnique,
536+
PackageWarning.invalidParameter,
537+
'An animation has a non-unique name: fooHerderAnimation. Animation names '
538+
'must be unique.'),
539+
isTrue);
540+
});
541+
test("warns on animation with missing parameters", () {
542+
expect(
543+
packageGraph.packageWarningCounter.hasWarning(
544+
withAnimationWrongParams,
545+
PackageWarning.invalidParameter,
546+
'Invalid @animation directive: {@animation http://host/path/to/video.mp4}\n'
547+
'Animation directives must be of the form: {@animation NAME '
548+
'WIDTH HEIGHT URL}'),
549+
isTrue);
550+
});
551+
test("warns on animation with non-integer width", () {
552+
expect(
553+
packageGraph.packageWarningCounter.hasWarning(
554+
withAnimationBadWidth,
555+
PackageWarning.invalidParameter,
556+
'An animation has an invalid width (badWidthAnimation): 100px. The width must be an integer.'),
557+
isTrue);
558+
});
559+
test("warns on animation with non-integer height", () {
560+
expect(
561+
packageGraph.packageWarningCounter.hasWarning(
562+
withAnimationBadHeight,
563+
PackageWarning.invalidParameter,
564+
'An animation has an invalid height (badHeightAnimation): 100px. The height must be an integer.'),
565+
isTrue);
566+
});
567+
test("Doesn't place animations in one line doc", () {
568+
expect(
569+
withAnimationInline.oneLineDoc, isNot(contains('<video')));
570+
expect(
571+
withAnimationInline.documentation, contains('<video'));
572+
});
573+
test("Handles animations inline properly", () {
574+
expect(
575+
withAnimationInline.documentation, isNot(contains(' works')));
576+
});
577+
});
578+
499579
group('MultiplyInheritedExecutableElement handling', () {
500580
Class BaseThingy, BaseThingy2, ImplementingThingy2;
501581
Method aImplementingThingyMethod;
@@ -1019,7 +1099,7 @@ void main() {
10191099
});
10201100

10211101
test('get methods', () {
1022-
expect(Dog.publicInstanceMethods, hasLength(12));
1102+
expect(Dog.publicInstanceMethods, hasLength(19));
10231103
});
10241104

10251105
test('get operators', () {
@@ -1084,6 +1164,13 @@ void main() {
10841164
'testGenericMethod',
10851165
'testMethod',
10861166
'toString',
1167+
'withAnimation',
1168+
'withAnimationBadHeight',
1169+
'withAnimationBadWidth',
1170+
'withAnimationInline',
1171+
'withAnimationInOneLineDoc',
1172+
'withAnimationNonUnique',
1173+
'withAnimationWrongParams',
10871174
'withMacro',
10881175
'withMacro2',
10891176
'withPrivateMacro',

testing/test_package/lib/example.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,48 @@ class Dog implements Cat, E {
355355
/// Don't define this: {@macro ThatDoesNotExist}
356356
void withUndefinedMacro() {}
357357

358+
/// Animation method
359+
///
360+
/// {@animation methodAnimation 100 100 http://host/path/to/video.mp4}
361+
/// More docs
362+
void withAnimation() {}
363+
364+
/// Non-Unique Animation method (between methods)
365+
///
366+
/// {@animation fooHerderAnimation 100 100 http://host/path/to/video.mp4}
367+
/// {@animation fooHerderAnimation 100 100 http://host/path/to/video.mp4}
368+
/// More docs
369+
void withAnimationNonUnique() {}
370+
371+
/// Malformed Animation method with wrong parameters
372+
///
373+
/// {@animation http://host/path/to/video.mp4}
374+
/// More docs
375+
void withAnimationWrongParams() {}
376+
377+
/// Malformed Animation method with non-integer width
378+
///
379+
/// {@animation badWidthAnimation 100px 100 http://host/path/to/video.mp4}
380+
/// More docs
381+
void withAnimationBadWidth() {}
382+
383+
/// Malformed Animation method with non-integer height
384+
///
385+
/// {@animation badHeightAnimation 100 100px http://host/path/to/video.mp4}
386+
/// More docs
387+
void withAnimationBadHeight() {}
388+
389+
/// Animation in one line doc {@animation oneLine 100 100 http://host/path/to/video.mp4}
390+
///
391+
/// This tests to see that we do the right thing if the animation is in
392+
/// the one line doc above.
393+
void withAnimationInOneLineDoc() {}
394+
395+
/// Animation inline in text.
396+
///
397+
/// Tests to see that an inline {@animation inline 100 100 http://host/path/to/video.mp4} works as expected.
398+
void withAnimationInline() {}
399+
358400
void testGeneric(Map<String, dynamic> args) {}
359401

360402
void testMethod(Iterable it) {}

0 commit comments

Comments
 (0)