@@ -339,14 +339,26 @@ mixin DocumentationComment implements Warnable, SourceCode {
339339 ///
340340 /// Syntax:
341341 ///
342- /// {@example <path> [lang=LANGUAGE] [indent=keep|strip]}
342+ /// {@example <path>[#<region>] [lang=LANGUAGE] [indent=keep|strip]}
343343 ///
344344 /// The `path` is resolved relative to the current file's path, using the
345345 /// package's root as the root path;
346346 /// see [resolveExamplePath] for details.
347347 ///
348348 /// The file content is processed as follows:
349349 /// - Line endings are normalized to `\n` .
350+ /// - If a `#<region>` is specified in the path, only the content within that
351+ /// region is extracted. A region is defined by `#region <region>` and
352+ /// `#endregion` . Lines containing region markers are stripped.
353+ /// - Lines containing a `#hide` marker are entirely omitted from the
354+ /// extracted output. This is useful for hiding setup, or assertions, or
355+ /// other code that is necessary for the example to compile but
356+ /// irrelevant to the documentation (e.g., `exit(0); // #hide` ).
357+ /// - Region extraction uses a best effort approach: if there are unmatched
358+ /// closing tags or unclosed regions, the code is extracted based on the
359+ /// stack ordering and emit a warning for the structural error.
360+ /// - If no region is specified, the entire file is included and no markers
361+ /// are stripped.
350362 /// - If `indent` is `strip` (the default), common indentation is removed from
351363 /// all lines. Whitespace-only lines are ignored for calculation and
352364 /// normalized to empty lines.
@@ -361,6 +373,7 @@ mixin DocumentationComment implements Warnable, SourceCode {
361373 /// Example:
362374 ///
363375 /// {@example /examples/my_example.dart}
376+ /// {@example /examples/my_example.dart#my_region}
364377 /// {@example ../subdir/utils.dart lang=dart indent=strip}
365378 /// {@example ../subdir/utils.dart indent=keep}
366379 String _injectExamples (String rawDocs) {
@@ -389,8 +402,15 @@ mixin DocumentationComment implements Warnable, SourceCode {
389402 'Ignoring extra arguments: ${args .rest .skip (1 ).join (" " )}' );
390403 }
391404
392- var filepath = args.rest.first;
393- // TODO(zarah): Add support for regions (#region, #endregion).
405+ var rawPath = args.rest.first;
406+ var parsedUri = Uri .tryParse (rawPath);
407+ if (parsedUri == null ) {
408+ warn (PackageWarning .invalidParameter,
409+ message: 'Invalid path for @example directive ($rawPath )' );
410+ return '' ;
411+ }
412+ var targetRegion = parsedUri.hasFragment ? parsedUri.fragment : null ;
413+ var filepath = parsedUri.removeFragment ().toString ();
394414
395415 var resolvedPath = resolveExamplePath (
396416 filepath,
@@ -418,14 +438,39 @@ mixin DocumentationComment implements Warnable, SourceCode {
418438 // Normalize line endings to \n.
419439 content = content.replaceAll (_lineEndingRegExp, '\n ' );
420440
441+ var lines = content.split ('\n ' );
442+
443+ // Extract the region and filter markers before touching the indentation
444+ if (targetRegion != null && targetRegion.isNotEmpty) {
445+ var extractedLines = _extractRegion (
446+ lines,
447+ targetRegion,
448+ filepath: filepath,
449+ warn: warn,
450+ );
451+
452+ if (extractedLines == null ) {
453+ warn (PackageWarning .missingExampleRegion,
454+ message: '$filepath #$targetRegion ' );
455+ return '' ;
456+ }
457+ lines = extractedLines;
458+ }
459+
460+ if (lines.isEmpty) {
461+ return '```$language \n ```' ;
462+ }
463+
464+ content = lines.join ('\n ' );
465+
421466 if (indent == 'strip' ) {
422467 content = _stripIndentation (content, warn);
423468 }
424469
425470 // Ensure the content ends with exactly one newline before the closing fence.
426471 var trailing = content.endsWith ('\n ' ) ? '' : '\n ' ;
427472
428- return '\n ```$language \n $content $trailing ```\n ' ;
473+ return '```$language \n $content $trailing ```' ;
429474 });
430475 }
431476
@@ -1146,4 +1191,99 @@ mixin DocumentationComment implements Warnable, SourceCode {
11461191
11471192 static final _canonicalForRegExp =
11481193 RegExp (r'[ ]*{@canonicalFor\s([^}]+)}[ ]*\n?' );
1194+
1195+ /// Extracts the lines for the specified [targetRegion] , retaining only the
1196+ /// content within the matching `#region` and `#endregion` boundaries.
1197+ ///
1198+ /// Any lines containing `#region` , `#endregion` , or `#hide` markers are
1199+ /// stripped and completely omitted from the result.
1200+ ///
1201+ /// The markers do not need to be within a code comment. Any line
1202+ /// containing `#region <name>` , `#endregion` , or `#hide` will be matched
1203+ /// and stripped.
1204+ ///
1205+ /// Validates that all regions are properly opened and closed.
1206+ ///
1207+ /// Uses a best effort stack-based approach: if there is a structural error
1208+ /// (like an extra `#endregion` or an unclosed region), it emits a warning
1209+ /// but still attempts to extract the code based on the innermost active
1210+ /// region.
1211+ ///
1212+ /// Returns `null` if the target region was never found.
1213+ static List <String >? _extractRegion (
1214+ List <String > lines,
1215+ String targetRegion, {
1216+ required String filepath,
1217+ required void Function (PackageWarning , {String ? message}) warn,
1218+ }) {
1219+ // Matches `#region` followed by spaces/tabs (no newlines), and captures
1220+ // the name (any characters up to the next whitespace or newline).
1221+ final regionStartPattern = RegExp (
1222+ r'#region[ \t]+(\S+)' ,
1223+ caseSensitive: false ,
1224+ );
1225+
1226+ // Matches `#endregion` anywhere in the line. Any name after it is ignored.
1227+ final regionEndPattern = RegExp (
1228+ r'#endregion\b' ,
1229+ caseSensitive: false ,
1230+ );
1231+
1232+ // Matches `#hide` anywhere in the line.
1233+ final hidePattern = RegExp (
1234+ r'#hide\b' ,
1235+ caseSensitive: false ,
1236+ );
1237+
1238+ final result = < String > [];
1239+ final regionStack = < String > [];
1240+ var regionFound = false ;
1241+
1242+ for (var i = 0 ; i < lines.length; i++ ) {
1243+ final line = lines[i];
1244+ final lineNumber = i + 1 ;
1245+
1246+ final startMatch = regionStartPattern.firstMatch (line);
1247+ if (startMatch != null ) {
1248+ final regionName = startMatch.group (1 )! ;
1249+ regionStack.add (regionName);
1250+
1251+ if (regionName == targetRegion) {
1252+ regionFound = true ;
1253+ }
1254+ continue ;
1255+ }
1256+
1257+ if (regionEndPattern.hasMatch (line)) {
1258+ if (regionStack.isEmpty) {
1259+ warn (
1260+ PackageWarning .invalidParameter,
1261+ message:
1262+ 'Found #endregion without a matching #region in $filepath at line $lineNumber .' ,
1263+ );
1264+ } else {
1265+ regionStack.removeLast ();
1266+ }
1267+ continue ;
1268+ }
1269+
1270+ if (hidePattern.hasMatch (line)) {
1271+ continue ;
1272+ }
1273+
1274+ if (regionStack.contains (targetRegion)) {
1275+ result.add (line);
1276+ }
1277+ }
1278+
1279+ if (regionStack.isNotEmpty) {
1280+ warn (
1281+ PackageWarning .invalidParameter,
1282+ message:
1283+ 'Unclosed #region(s) found in $filepath : ${regionStack .join (', ' )}' ,
1284+ );
1285+ }
1286+
1287+ return regionFound ? result : null ;
1288+ }
11491289}
0 commit comments