Skip to content

Commit ea80a75

Browse files
committed
Remove name parameter from animation directive.
1 parent 7430e1c commit ea80a75

Some content is hidden

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

51 files changed

+1969
-119
lines changed

lib/src/model.dart

Lines changed: 160 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import 'package:analyzer/src/generated/source_io.dart';
3434
import 'package:analyzer/src/dart/element/member.dart'
3535
show ExecutableMember, Member, ParameterMember;
3636
import 'package:analyzer/src/dart/analysis/driver.dart';
37+
import 'package:args/args.dart';
3738
import 'package:collection/collection.dart';
3839
import 'package:dartdoc/src/dartdoc_options.dart';
3940
import 'package:dartdoc/src/element_type.dart';
@@ -3487,12 +3488,9 @@ abstract class ModelElement extends Canonicalization
34873488
///
34883489
/// {@example PATH [region=NAME] [lang=NAME]}
34893490
///
3490-
/// where PATH and NAME are tokens _without_ whitespace; NAME can optionally be
3491-
/// quoted (use of quotes is for backwards compatibility and discouraged).
3492-
///
34933491
/// If PATH is `dir/file.ext` and region is `r` then we'll look for the file
3494-
/// named `dir/file-r.ext.md`, relative to the project root directory (of the
3495-
/// project for which the docs are being generated).
3492+
/// named `dir/file-r.ext.md`, relative to the project root directory of the
3493+
/// project for which the docs are being generated.
34963494
///
34973495
/// Examples: (escaped in this comment to show literal values in dartdoc's
34983496
/// dartdoc)
@@ -3505,6 +3503,10 @@ abstract class ModelElement extends Canonicalization
35053503
RegExp exampleRE = new RegExp(r'{@example\s+([^}]+)}');
35063504
return rawdocs.replaceAllMapped(exampleRE, (match) {
35073505
var args = _getExampleArgs(match[1]);
3506+
if (args == null) {
3507+
// Already warned about an invalid parameter if this happens.
3508+
return '';
3509+
}
35083510
var lang =
35093511
args['lang'] ?? pathLib.extension(args['src']).replaceFirst('.', '');
35103512

@@ -3533,17 +3535,19 @@ abstract class ModelElement extends Canonicalization
35333535
///
35343536
/// Syntax:
35353537
///
3536-
/// {@animation NAME WIDTH HEIGHT URL}
3538+
/// {@animation WIDTH HEIGHT URL [id=ID]}
35373539
///
35383540
/// Example:
35393541
///
3540-
/// {@animation my_video 300 300 https://example.com/path/to/video.mp4}
3542+
/// {@animation 300 300 https://example.com/path/to/video.mp4 id="my_video"}
35413543
///
35423544
/// Which will render the HTML necessary for embedding a simple click-to-play
3543-
/// HTML5 video player with no controls.
3545+
/// HTML5 video player with no controls that has an HTML id of "my_video".
35443546
///
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+
/// The optional ID should be a unique id that is a valid JavaScript
3548+
/// identifier, and will be used as the id for the video tag. If no ID is
3549+
/// supplied, then a unique identifier (starting with "animation_") will be
3550+
/// generated.
35473551
///
35483552
/// The width and height must be integers specifying the dimensions of the
35493553
/// video file in pixels.
@@ -3554,87 +3558,114 @@ abstract class ModelElement extends Canonicalization
35543558
final RegExp basicAnimationRegExp =
35553559
new RegExp(r'''{@animation\s+([^}]+)}''');
35563560

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-
35643561
// 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>();
3562+
final RegExp validIdRegExp = new RegExp(r'^[a-zA-Z_]\w*$');
3563+
3564+
final Set<String> uniqueIds = new Set<String>();
3565+
String getUniqueId(String base) {
3566+
int count = 1;
3567+
String id = '$base$count';
3568+
while (uniqueIds.contains(id)) {
3569+
count++;
3570+
id = '$base$count';
3571+
}
3572+
return id;
3573+
}
35693574

35703575
return rawDocs.replaceAllMapped(basicAnimationRegExp, (basicMatch) {
3571-
final Match match = animationRegExp.firstMatch(basicMatch[0]);
3572-
if (match == null) {
3576+
final ArgParser parser = new ArgParser();
3577+
parser.addOption('id');
3578+
final ArgResults args = _parseArgs(basicMatch[1], parser, 'animation');
3579+
if (args == null) {
3580+
// Already warned about an invalid parameter if this happens.
3581+
return '';
3582+
}
3583+
final List<String> positionalArgs = args.rest.sublist(0);
3584+
String uniqueId;
3585+
bool wasDeprecated = false;
3586+
if (positionalArgs.length == 4) {
3587+
// Supports the original form of the animation tag for backward
3588+
// compatibility.
3589+
uniqueId = positionalArgs.removeAt(0);
3590+
wasDeprecated = true;
3591+
} else if (positionalArgs.length == 3) {
3592+
uniqueId = args['id'] ?? getUniqueId('animation_');
3593+
} else {
35733594
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}');
3595+
message: 'Invalid @animation directive, "${basicMatch[0]}"\n'
3596+
'Animation directives must be of the form "{@animation WIDTH '
3597+
'HEIGHT URL [id=ID]}"');
35773598
return '';
35783599
}
3579-
String name = match[1];
3580-
if (!validNameRegExp.hasMatch(name)) {
3600+
3601+
if (!validIdRegExp.hasMatch(uniqueId)) {
35813602
warn(PackageWarning.invalidParameter,
3582-
message: 'An animation has an invalid name: $name. The name can '
3583-
'only contain letters, numbers and underscores.');
3603+
message: 'An animation has an invalid identifier, "$uniqueId". The '
3604+
'identifier can only contain letters, numbers and underscores, '
3605+
'and must not begin with a number.');
3606+
return '';
3607+
}
3608+
if (uniqueIds.contains(uniqueId)) {
3609+
warn(PackageWarning.invalidParameter,
3610+
message: 'An animation has a non-unique identifier, "$uniqueId". '
3611+
'Animation identifiers must be unique.');
35843612
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);
35943613
}
3614+
uniqueIds.add(uniqueId);
3615+
35953616
int width;
35963617
try {
3597-
width = int.parse(match[2]);
3618+
width = int.parse(positionalArgs[0]);
35983619
} on FormatException {
35993620
warn(PackageWarning.invalidParameter,
3600-
message:
3601-
'An animation has an invalid width ($name): ${match[2]}. The '
3602-
'width must be an integer.');
3621+
message: 'An animation has an invalid width ($uniqueId), '
3622+
'"${positionalArgs[0]}". The width must be an integer.');
36033623
return '';
36043624
}
3625+
36053626
int height;
36063627
try {
3607-
height = int.parse(match[3]);
3628+
height = int.parse(positionalArgs[1]);
36083629
} on FormatException {
36093630
warn(PackageWarning.invalidParameter,
3610-
message:
3611-
'An animation has an invalid height ($name): ${match[3]}. The '
3612-
'height must be an integer.');
3631+
message: 'An animation has an invalid height ($uniqueId), '
3632+
'"${positionalArgs[1]}". The height must be an integer.');
36133633
return '';
36143634
}
3635+
36153636
Uri movieUrl;
36163637
try {
3617-
movieUrl = Uri.parse(match[4]);
3638+
movieUrl = Uri.parse(positionalArgs[2]);
36183639
} on FormatException catch (e) {
36193640
warn(PackageWarning.invalidParameter,
3620-
message:
3621-
'An animation URL could not be parsed ($name): ${match[4]}\n$e');
3641+
message: 'An animation URL could not be parsed ($uniqueId): '
3642+
'${positionalArgs[2]}\n$e');
36223643
return '';
36233644
}
3624-
final String overlayName = '${name}_play_button_';
3645+
final String overlayId = '${uniqueId}_play_button_';
3646+
3647+
// Only warn about deprecation if some other warning didn't occur.
3648+
if (wasDeprecated) {
3649+
warn(PackageWarning.deprecated,
3650+
message:
3651+
'Deprecated form of @animation directive, "${basicMatch[0]}"\n'
3652+
'Animation directives are now of the form "{@animation '
3653+
'WIDTH HEIGHT URL [id=ID]}" (id is an optional '
3654+
'parameter)');
3655+
}
36253656

36263657
// Blank lines before and after, and no indenting at the beginning and end
36273658
// is needed so that Markdown doesn't confuse this with code, so be
36283659
// careful of whitespace here.
36293660
return '''
36303661
36313662
<div style="position: relative;">
3632-
<div id="${overlayName}"
3633-
onclick="if ($name.paused) {
3634-
$name.play();
3663+
<div id="${overlayId}"
3664+
onclick="if ($uniqueId.paused) {
3665+
$uniqueId.play();
36353666
this.style.display = 'none';
36363667
} else {
3637-
$name.pause();
3668+
$uniqueId.pause();
36383669
this.style.display = 'block';
36393670
}"
36403671
style="position:absolute;
@@ -3645,14 +3676,14 @@ abstract class ModelElement extends Canonicalization
36453676
background-repeat: no-repeat;
36463677
background-image: url(static-assets/play_button.svg);">
36473678
</div>
3648-
<video id="$name"
3679+
<video id="$uniqueId"
36493680
style="width:${width}px; height:${height}px;"
36503681
onclick="if (this.paused) {
36513682
this.play();
3652-
$overlayName.style.display = 'none';
3683+
$overlayId.style.display = 'none';
36533684
} else {
36543685
this.pause();
3655-
$overlayName.style.display = 'block';
3686+
$overlayId.style.display = 'block';
36563687
}" loop>
36573688
<source src="$movieUrl" type="video/mp4"/>
36583689
</video>
@@ -3718,29 +3749,80 @@ abstract class ModelElement extends Canonicalization
37183749
});
37193750
}
37203751

3752+
/// Helper to process arguments given as a (possibly quoted) string.
3753+
///
3754+
/// First, this will split the given [argsAsString] into separate arguments,
3755+
/// taking any quoting (either ' or " are accepted) into account, including
3756+
/// handling backslash-escaped quotes.
3757+
///
3758+
/// Then, it will prepend "--" to any args that start with an identifier
3759+
/// followed by an equals sign, allowing the argument parser to treat any
3760+
/// "foo=bar" argument as "--foo=bar". It does handle quoted args like
3761+
/// "foo='bar baz'" too, returning just bar (without quotes) for the foo
3762+
/// value.
3763+
///
3764+
/// It then parses the resulting argument list normally with [argParser] and
3765+
/// returns the result.
3766+
ArgResults _parseArgs(
3767+
String argsAsString, ArgParser argParser, String directiveName) {
3768+
// Regexp to take care of splitting arguments, and handling the quotes
3769+
// around arguments, if any.
3770+
//
3771+
// Match group 1 is the "foo=" part of the option, if any.
3772+
// Match group 2 contains the quote character used (which is discarded).
3773+
// Match group 3 is a quoted arg, if any, without the quotes.
3774+
// Match group 4 is the unquoted arg, if any.
3775+
final RegExp argMatcher = new RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
3776+
r'(?:' // Start a new non-capture group for the two possibilities.
3777+
r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
3778+
r'([^ ]+))'); // without quotes.
3779+
final Iterable<Match> matches = argMatcher.allMatches(argsAsString);
3780+
3781+
// Remove quotes around args, and for any args that look like assignments
3782+
// (start with valid option names followed by an equals sign), add a "--" in front
3783+
// so that they parse as options.
3784+
final Iterable<String> args = matches.map<String>((Match match) {
3785+
String option = '';
3786+
if (match[1] != null) {
3787+
option = '--${match[1]}';
3788+
}
3789+
return option + (match[3] ?? '') + (match[4] ?? '');
3790+
});
3791+
3792+
try {
3793+
return argParser.parse(args);
3794+
} on ArgParserException catch (e) {
3795+
warn(PackageWarning.invalidParameter,
3796+
message: 'The {@$directiveName ...} directive was called with '
3797+
'invalid parameters. $e');
3798+
return null;
3799+
}
3800+
}
3801+
37213802
/// Helper for _injectExamples used to process @example arguments.
37223803
/// Returns a map of arguments. The first unnamed argument will have key 'src'.
37233804
/// The computed file path, constructed from 'src' and 'region' will have key
37243805
/// 'file'.
37253806
Map<String, String> _getExampleArgs(String argsAsString) {
3726-
// Extract PATH and return is under key 'src'
3727-
var endOfSrc = argsAsString.indexOf(' ');
3728-
if (endOfSrc < 0) endOfSrc = argsAsString.length;
3729-
var src = argsAsString.substring(0, endOfSrc);
3730-
src = src.replaceAll('/', Platform.pathSeparator);
3731-
final args = {'src': src};
3732-
3733-
// Process remaining named arguments
3734-
var namedArgs = argsAsString.substring(endOfSrc);
3735-
// Arg value: allow optional quotes; warning: we still don't support whitespace.
3736-
RegExp keyValueRE = new RegExp('(\\w+)=[\'"]?(\\S*)[\'"]?');
3737-
Iterable<Match> matches = keyValueRE.allMatches(namedArgs);
3738-
matches.forEach((match) {
3739-
// Ignore optional quotes
3740-
args[match[1]] = match[2].replaceAll(new RegExp('[\'"]'), '');
3741-
});
3807+
ArgParser parser = new ArgParser();
3808+
parser.addOption('lang');
3809+
parser.addOption('region');
3810+
ArgResults results = _parseArgs(argsAsString, parser, 'example');
3811+
if (results == null) {
3812+
return null;
3813+
}
37423814

3743-
// Compute 'file'
3815+
// Extract PATH and fix the path separators.
3816+
final String src = results.rest.isEmpty
3817+
? ''
3818+
: results.rest.first.replaceAll('/', Platform.pathSeparator);
3819+
final Map<String, String> args = <String, String>{
3820+
'src': src,
3821+
'lang': results['lang'],
3822+
'region': results['region'] ?? '',
3823+
};
3824+
3825+
// Compute 'file' from region and src.
37443826
final fragExtension = '.md';
37453827
var file = src + fragExtension;
37463828
var region = args['region'] ?? '';
@@ -4233,6 +4315,9 @@ class PackageGraph {
42334315
case PackageWarning.invalidParameter:
42344316
warningMessage = 'invalid parameter to dartdoc directive: ${message}';
42354317
break;
4318+
case PackageWarning.deprecated:
4319+
warningMessage = 'deprecated dartdoc usage: ${message}';
4320+
break;
42364321
}
42374322

42384323
List<String> messageParts = [warningMessage];

lib/src/warnings.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ final Map<PackageWarning, PackageWarningHelpText> packageWarningText = const {
9090
PackageWarning.invalidParameter,
9191
"invalidParameter",
9292
"A parameter given to a dartdoc directive was invalid."),
93+
PackageWarning.deprecated: const PackageWarningHelpText(
94+
PackageWarning.deprecated,
95+
"deprecated",
96+
"A dartdoc directive has a deprecated format."),
9397
};
9498

9599
/// Something that package warnings can be called on. Optionally associated
@@ -130,6 +134,7 @@ enum PackageWarning {
130134
missingFromSearchIndex,
131135
typeAsHtml,
132136
invalidParameter,
137+
deprecated,
133138
}
134139

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

0 commit comments

Comments
 (0)