@@ -34,6 +34,7 @@ import 'package:analyzer/src/generated/source_io.dart';
3434import 'package:analyzer/src/dart/element/member.dart'
3535 show ExecutableMember, Member, ParameterMember;
3636import 'package:analyzer/src/dart/analysis/driver.dart' ;
37+ import 'package:args/args.dart' ;
3738import 'package:collection/collection.dart' ;
3839import 'package:dartdoc/src/dartdoc_options.dart' ;
3940import '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];
0 commit comments