@@ -34,6 +34,7 @@ import 'package:analyzer/src/generated/source_io.dart';
34
34
import 'package:analyzer/src/dart/element/member.dart'
35
35
show ExecutableMember, Member, ParameterMember;
36
36
import 'package:analyzer/src/dart/analysis/driver.dart' ;
37
+ import 'package:args/args.dart' ;
37
38
import 'package:collection/collection.dart' ;
38
39
import 'package:dartdoc/src/dartdoc_options.dart' ;
39
40
import 'package:dartdoc/src/element_type.dart' ;
@@ -3487,12 +3488,9 @@ abstract class ModelElement extends Canonicalization
3487
3488
///
3488
3489
/// {@example PATH [region=NAME] [lang=NAME]}
3489
3490
///
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
- ///
3493
3491
/// 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.
3496
3494
///
3497
3495
/// Examples: (escaped in this comment to show literal values in dartdoc's
3498
3496
/// dartdoc)
@@ -3505,6 +3503,10 @@ abstract class ModelElement extends Canonicalization
3505
3503
RegExp exampleRE = new RegExp (r'{@example\s+([^}]+)}' );
3506
3504
return rawdocs.replaceAllMapped (exampleRE, (match) {
3507
3505
var args = _getExampleArgs (match[1 ]);
3506
+ if (args == null ) {
3507
+ // Already warned about an invalid parameter if this happens.
3508
+ return '' ;
3509
+ }
3508
3510
var lang =
3509
3511
args['lang' ] ?? pathLib.extension (args['src' ]).replaceFirst ('.' , '' );
3510
3512
@@ -3533,17 +3535,19 @@ abstract class ModelElement extends Canonicalization
3533
3535
///
3534
3536
/// Syntax:
3535
3537
///
3536
- /// {@animation NAME WIDTH HEIGHT URL}
3538
+ /// {@animation WIDTH HEIGHT URL [id=ID] }
3537
3539
///
3538
3540
/// Example:
3539
3541
///
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" }
3541
3543
///
3542
3544
/// 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" .
3544
3546
///
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.
3547
3551
///
3548
3552
/// The width and height must be integers specifying the dimensions of the
3549
3553
/// video file in pixels.
@@ -3554,87 +3558,114 @@ abstract class ModelElement extends Canonicalization
3554
3558
final RegExp basicAnimationRegExp =
3555
3559
new RegExp (r'''{@animation\s+([^}]+)}''' );
3556
3560
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
3561
// 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
+ }
3569
3574
3570
3575
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 {
3573
3594
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]}" ' );
3577
3598
return '' ;
3578
3599
}
3579
- String name = match[ 1 ];
3580
- if (! validNameRegExp .hasMatch (name )) {
3600
+
3601
+ if (! validIdRegExp .hasMatch (uniqueId )) {
3581
3602
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.' );
3584
3612
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
3613
}
3614
+ uniqueIds.add (uniqueId);
3615
+
3595
3616
int width;
3596
3617
try {
3597
- width = int .parse (match[ 2 ]);
3618
+ width = int .parse (positionalArgs[ 0 ]);
3598
3619
} on FormatException {
3599
3620
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.' );
3603
3623
return '' ;
3604
3624
}
3625
+
3605
3626
int height;
3606
3627
try {
3607
- height = int .parse (match[ 3 ]);
3628
+ height = int .parse (positionalArgs[ 1 ]);
3608
3629
} on FormatException {
3609
3630
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.' );
3613
3633
return '' ;
3614
3634
}
3635
+
3615
3636
Uri movieUrl;
3616
3637
try {
3617
- movieUrl = Uri .parse (match[ 4 ]);
3638
+ movieUrl = Uri .parse (positionalArgs[ 2 ]);
3618
3639
} on FormatException catch (e) {
3619
3640
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 ' );
3622
3643
return '' ;
3623
3644
}
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
+ }
3625
3656
3626
3657
// Blank lines before and after, and no indenting at the beginning and end
3627
3658
// is needed so that Markdown doesn't confuse this with code, so be
3628
3659
// careful of whitespace here.
3629
3660
return '''
3630
3661
3631
3662
<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();
3635
3666
this.style.display = 'none';
3636
3667
} else {
3637
- $name .pause();
3668
+ $uniqueId .pause();
3638
3669
this.style.display = 'block';
3639
3670
}"
3640
3671
style="position:absolute;
@@ -3645,14 +3676,14 @@ abstract class ModelElement extends Canonicalization
3645
3676
background-repeat: no-repeat;
3646
3677
background-image: url(static-assets/play_button.svg);">
3647
3678
</div>
3648
- <video id="$name "
3679
+ <video id="$uniqueId "
3649
3680
style="width:${width }px; height:${height }px;"
3650
3681
onclick="if (this.paused) {
3651
3682
this.play();
3652
- $overlayName .style.display = 'none';
3683
+ $overlayId .style.display = 'none';
3653
3684
} else {
3654
3685
this.pause();
3655
- $overlayName .style.display = 'block';
3686
+ $overlayId .style.display = 'block';
3656
3687
}" loop>
3657
3688
<source src="$movieUrl " type="video/mp4"/>
3658
3689
</video>
@@ -3718,29 +3749,80 @@ abstract class ModelElement extends Canonicalization
3718
3749
});
3719
3750
}
3720
3751
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
+
3721
3802
/// Helper for _injectExamples used to process @example arguments.
3722
3803
/// Returns a map of arguments. The first unnamed argument will have key 'src'.
3723
3804
/// The computed file path, constructed from 'src' and 'region' will have key
3724
3805
/// 'file'.
3725
3806
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
+ }
3742
3814
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.
3744
3826
final fragExtension = '.md' ;
3745
3827
var file = src + fragExtension;
3746
3828
var region = args['region' ] ?? '' ;
@@ -4233,6 +4315,9 @@ class PackageGraph {
4233
4315
case PackageWarning .invalidParameter:
4234
4316
warningMessage = 'invalid parameter to dartdoc directive: ${message }' ;
4235
4317
break ;
4318
+ case PackageWarning .deprecated:
4319
+ warningMessage = 'deprecated dartdoc usage: ${message }' ;
4320
+ break ;
4236
4321
}
4237
4322
4238
4323
List <String > messageParts = [warningMessage];
0 commit comments