LCOV - code coverage report
Current view: top level - src - grammar.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 77.4 % 323 250
Test Date: 2026-08-24 04:55:06 Functions: - 0 0
Legend: Lines: hit not hit

            Line data    Source code
       1              : // Copyright 2026 The Authors. See the AUTHORS file for details.
       2              : //
       3              : // Licensed under the Apache License, Version 2.0 (the "License");
       4              : // you may not use this file except in compliance with the License.
       5              : // You may obtain a copy of the License at
       6              : //
       7              : //      https://www.apache.org/licenses/LICENSE-2.0
       8              : //
       9              : // Unless required by applicable law or agreed to in writing, software
      10              : // distributed under the License is distributed on an "AS IS" BASIS,
      11              : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12              : // See the License for the specific language governing permissions and
      13              : // limitations under the License.
      14              : 
      15              : import 'package:betto_common/collections.dart' show Stack;
      16              : import 'package:characters/characters.dart';
      17              : 
      18              : import 'core_rules.dart';
      19              : import 'parse.dart';
      20              : 
      21              : /// Visitor interface for traversing ABNF grammar [Element] nodes.
      22              : abstract class ElementVisitor<R> {
      23              :   R visitAlternativeLiterals(AlternativeLiterals element);
      24              :   R visitAlternatives(Alternatives element);
      25              :   R visitConcatenation(Concatenation element);
      26              :   R visitGroup(Group element);
      27              :   R visitLiteralElement(LiteralElement element);
      28              :   R visitOptionalSequence(OptionalSequence element);
      29              :   R visitRepetition(Repetition element);
      30              :   R visitSequence(Sequence element);
      31              :   R visitRule(Rule element);
      32              :   R visitGrammar(Grammar element);
      33              :   R visitValueRange(ValueRange element);
      34              :   R visitCharacterElement(CharacterElement element);
      35              :   R visitEmptyElement(EmptyElement element);
      36              :   R visitNegativeLookahead(NegativeLookahead element);
      37              : 
      38            1 :   R visitElement(Element element) => switch (element) {
      39            1 :     AlternativeLiterals _ => visitAlternativeLiterals(element),
      40            2 :     Alternatives _ => visitAlternatives(element),
      41            1 :     Concatenation _ => visitConcatenation(element),
      42            2 :     Group _ => visitGroup(element),
      43            2 :     LiteralElement _ => visitLiteralElement(element),
      44            2 :     CharacterElement _ => visitCharacterElement(element),
      45            2 :     OptionalSequence _ => visitOptionalSequence(element),
      46            2 :     Repetition _ => visitRepetition(element),
      47            1 :     Sequence _ => visitSequence(element),
      48            2 :     ValueRange _ => visitValueRange(element),
      49            2 :     Grammar _ => visitGrammar(element),
      50            2 :     Rule _ => visitRule(element),
      51            0 :     EmptyElement _ => visitEmptyElement(element),
      52            0 :     NegativeLookahead _ => visitNegativeLookahead(element),
      53              :   };
      54              : }
      55              : 
      56              : /// The base representation of an ABNF grammar element.
      57              : sealed class Element {
      58              :   /// Attempts to parse the [source] string matching this element.
      59              :   ///
      60              :   /// Returns a [ParseResult] containing the parsed lexeme and remaining string
      61              :   /// on success. On failure, returns a failed [ParseResult].
      62              :   ParseResult parse(String source);
      63              : 
      64              :   /// Accepts an [ElementVisitor] for traversing the grammar tree.
      65              :   void accept(ElementVisitor visitor);
      66              : 
      67              :   /// An optional human-readable description of this element.
      68              :   String? get description;
      69              : }
      70              : 
      71              : /// Represents a complete ABNF grammar starting from an entry rule.
      72              : final class Grammar implements Element {
      73              :   /// The unique name of this grammar.
      74              :   final String name;
      75              : 
      76              :   @override
      77              :   final String? description;
      78              : 
      79              :   /// The entry point [Rule] for parsing.
      80              :   final Rule entryRule;
      81              : 
      82            4 :   @Deprecated('Use grammar() factory instead.')
      83              :   Grammar(this.name, this.entryRule, {this.description});
      84              : 
      85            0 :   Rule get element => entryRule;
      86              : 
      87            4 :   @override
      88           16 :   String toString() => '$name = ${entryRule.name}';
      89              : 
      90              :   /// Alias for [entryRule].
      91            0 :   Rule get value => entryRule;
      92              : 
      93            4 :   @override
      94              :   ParseResult parse(String source) {
      95            8 :     final result = entryRule.parse(source);
      96            8 :     if (result.remaining.isNotEmpty) {
      97              :       // The full source was not consumed so the parsing has failed
      98            4 :       return ParseResult(
      99              :         false,
     100            4 :         result.remaining,
     101            4 :         lexeme: result.lexeme,
     102            4 :         ruleName: name,
     103            4 :         element: toString(),
     104            8 :         stack: Stack<ParseResult>()..push(result),
     105              :       );
     106              :     }
     107            4 :     return ParseResult(
     108            4 :       result.success,
     109            4 :       result.remaining,
     110            4 :       lexeme: result.lexeme,
     111            4 :       ruleName: name,
     112            4 :       element: toString(),
     113            8 :       stack: Stack<ParseResult>()..push(result),
     114              :     );
     115              :   }
     116              : 
     117            0 :   @override
     118            0 :   void accept(ElementVisitor visitor) => visitor.visitGrammar(this);
     119              : }
     120              : 
     121            4 : Grammar grammar(String name, Rule entryRule, {String? description}) =>
     122              :     // ignore: deprecated_member_use_from_same_package
     123            4 :     Grammar(name, entryRule, description: description);
     124              : 
     125              : /// A named grammar rule combining a name and an underlying [Element].
     126              : final class Rule implements Element {
     127              :   final String name;
     128              :   final Element element;
     129              :   @override
     130              :   final String? description;
     131              : 
     132            4 :   @Deprecated('Use rule() factory instead.')
     133              :   Rule(this.name, this.element, {this.description});
     134              : 
     135            4 :   @override
     136              :   String toString() =>
     137           24 :       '$name = ${element is Rule ? (element as Rule).name : element}';
     138              : 
     139            4 :   @override
     140              :   ParseResult parse(String source) {
     141            8 :     final result = element.parse(source);
     142              : 
     143            4 :     if (!result.success) {
     144            3 :       return ParseResult(
     145              :         false,
     146              :         source,
     147            3 :         ruleName: name,
     148            3 :         element: toString(),
     149            6 :         stack: Stack<ParseResult>()..push(result),
     150              :       );
     151              :     }
     152              : 
     153           20 :     final lexeme = source.substring(0, source.length - result.remaining.length);
     154              : 
     155            4 :     return ParseResult(
     156            4 :       result.success,
     157            4 :       result.remaining,
     158              :       lexeme: lexeme,
     159            4 :       ruleName: name,
     160            4 :       element: toString(),
     161            8 :       stack: Stack<ParseResult>()..push(result),
     162              :     );
     163              :   }
     164              : 
     165            3 :   static final abnfRuleName = rule(
     166              :     'rule',
     167            2 :     Concatenation([
     168            1 :       alpha,
     169            6 :       variableRepetition(alternatives([alpha, digit, literal('-')])),
     170              :     ]),
     171              :   );
     172              : 
     173            0 :   @override
     174            0 :   void accept(ElementVisitor visitor) => visitor.visitRule(this);
     175              : }
     176              : 
     177              : /// Creates a named [Rule] from a given grammar [element].
     178            4 : Rule rule(String name, Element element, {String? description}) =>
     179              :     // ignore: deprecated_member_use_from_same_package
     180            4 :     Rule(name, element, description: description);
     181              : 
     182              : /// Represents a single character (code point) element.
     183              : final class CharacterElement implements Element {
     184              :   /// The 32-bit Unicode code point value.
     185              :   final int value;
     186              :   @override
     187              :   final String? description;
     188              : 
     189            2 :   CharacterElement(this.value, {this.description});
     190              : 
     191            0 :   @override
     192            0 :   void accept(ElementVisitor visitor) => visitor.visitCharacterElement(this);
     193              : 
     194            2 :   @override
     195              :   ParseResult parse(String source) {
     196            2 :     if (source.isEmpty) {
     197            2 :       return ParseResult(false, source, element: toString());
     198              :     }
     199              : 
     200            4 :     final rune = source.runes.first;
     201            4 :     if (rune == value) {
     202              :       // Consume exactly one rune. String.fromCharCodes reconstructs the
     203              :       // string correctly even if the rune required a surrogate pair.
     204            2 :       final lexeme = String.fromCharCode(rune);
     205            2 :       return ParseResult(
     206              :         true,
     207            4 :         source.substring(lexeme.length),
     208              :         lexeme: lexeme,
     209            2 :         element: toString(),
     210              :       );
     211              :     }
     212            4 :     return ParseResult(false, source, element: toString());
     213              :   }
     214              : 
     215            2 :   @override
     216              :   String toString() =>
     217           10 :       '%x${value.toRadixString(16).padLeft(2, '0').toUpperCase()}';
     218              : }
     219              : 
     220            2 : CharacterElement character(int value, {String? description}) =>
     221            2 :     CharacterElement(value, description: description);
     222              : 
     223              : /// Represents a literal string (e.g., `"en"`).
     224              : ///
     225              : /// Per RFC 5234 §2.3, string literals in ABNF are case-**insensitive**
     226              : /// by default. Set to `true` to opt into case-sensitive matching
     227              : /// (equivalent to the `%s` prefix in RFC 7405).
     228              : final class LiteralElement implements Element {
     229              :   final String value;
     230              : 
     231              :   /// Whether matching is case-sensitive.
     232              :   ///
     233              :   /// Per RFC 5234 §2.3, string literals in ABNF are case-**insensitive**
     234              :   /// by default. Set to `true` to opt into case-sensitive matching
     235              :   /// (equivalent to the `%s` prefix in RFC 7405).
     236              :   final bool caseSensitive;
     237              : 
     238              :   @override
     239              :   final String? description;
     240              : 
     241            2 :   LiteralElement(this.value, {this.caseSensitive = false, this.description});
     242              : 
     243            2 :   @override
     244              :   // RFC 7405: prefix %s for case-sensitive, %i (or no prefix) for insensitive.
     245            8 :   String toString() => caseSensitive ? '%s"$value"' : '"$value"';
     246              : 
     247            2 :   @override
     248              :   ParseResult parse(String source) {
     249            2 :     final matched = caseSensitive
     250            2 :         ? source.startsWith(value)
     251            8 :         : source.toLowerCase().startsWith(value.toLowerCase());
     252              :     if (matched) {
     253            2 :       return ParseResult(
     254              :         true,
     255            6 :         source.substring(value.length),
     256            6 :         lexeme: source.substring(0, value.length),
     257            2 :         element: toString(),
     258              :       );
     259              :     }
     260            4 :     return ParseResult(false, source, element: toString());
     261              :   }
     262              : 
     263            0 :   @override
     264            0 :   void accept(ElementVisitor visitor) => visitor.visitLiteralElement(this);
     265              : }
     266              : 
     267              : /// Creates a case-insensitive literal element (the RFC 5234 default).
     268            2 : Element literal(String value, {String? description}) =>
     269            2 :     LiteralElement(value, description: description);
     270              : 
     271              : /// Creates a case-sensitive literal element (equivalent to RFC 7405 `%s`).
     272            1 : Element caseSensitiveLiteral(String value, {String? description}) =>
     273            1 :     LiteralElement(value, caseSensitive: true, description: description);
     274              : 
     275              : /// Represents a list of alternative literal strings (e.g., `"en" / "de"`).
     276              : final class AlternativeLiterals implements Element {
     277              :   /// The original (unmodified) values, used for display and for
     278              :   /// case-sensitive matching.
     279              :   final Set<String> values;
     280              : 
     281              :   /// Whether matching is case-sensitive.
     282              :   ///
     283              :   /// Per RFC 5234 §2.3, string literals in ABNF are case-**insensitive**
     284              :   /// by default. Set to `true` to opt into case-sensitive matching
     285              :   /// (equivalent to the `%s` prefix in RFC 7405).
     286              :   final bool caseSensitive;
     287              : 
     288              :   final bool jagged;
     289              :   final int minLength;
     290              :   @override
     291              :   final String? description;
     292              : 
     293              :   // Sort the strings by length - longest first.
     294              :   // This helps us be greedy when the literal list is jagged.
     295            3 :   static Set<String> _sortBySize(Iterable<String> literals) {
     296            3 :     final l = literals.toList(growable: false);
     297           15 :     l.sort((a, b) => b.length.compareTo(a.length));
     298            3 :     return Set<String>.of(l);
     299              :   }
     300              : 
     301            6 :   static bool _isJagged(Iterable<String> literals) => literals.any(
     302           21 :     (element) => element.characters.length != literals.first.characters.length,
     303              :   );
     304              : 
     305            6 :   static int _minLength(Iterable<String> literals) => literals.fold<int>(
     306            9 :     literals.first.characters.length,
     307           18 :     (min, e) => min < e.characters.length ? min : e.characters.length,
     308              :   );
     309              : 
     310            3 :   AlternativeLiterals(
     311              :     Iterable<String> literals, {
     312              :     this.caseSensitive = false,
     313              :     this.description,
     314            3 :   }) : values = _sortBySize(literals),
     315            3 :        jagged = _isJagged(literals),
     316            3 :        minLength = _minLength(literals);
     317              : 
     318            3 :   @override
     319              :   // RFC 7405: emit %s prefix for case-sensitive; bare quotes = case-insensitive.
     320              :   String toString() =>
     321            3 :       description ??
     322           18 :       values.map((e) => caseSensitive ? '%s"$e"' : '"$e"').join(' / ');
     323              : 
     324            1 :   ParseResult _parseJaggedSet(String source) {
     325            2 :     final sourceCmp = caseSensitive ? source : source.toLowerCase();
     326            2 :     for (final str in values) {
     327            2 :       final strCmp = caseSensitive ? str : str.toLowerCase();
     328            1 :       if (sourceCmp.startsWith(strCmp)) {
     329              :         // Return the slice from the original source to preserve input casing.
     330            1 :         return ParseResult(
     331              :           true,
     332            2 :           source.substring(str.length),
     333            2 :           lexeme: source.substring(0, str.length),
     334            1 :           element: toString(),
     335              :         );
     336              :       }
     337              :     }
     338            0 :     return ParseResult(false, source, element: toString());
     339              :   }
     340              : 
     341            3 :   ParseResult _parseUniformSet(String source) {
     342           12 :     if (source.characters.length < minLength) {
     343            2 :       return ParseResult(false, source, element: toString());
     344              :     }
     345           12 :     final slice = source.characters.take(minLength).toString();
     346            6 :     final sliceCmp = caseSensitive ? slice : slice.toLowerCase();
     347              :     // Build a normalised lookup set on demand (not stored, avoids extra field).
     348            6 :     final match = values.any(
     349           12 :       (v) => (caseSensitive ? v : v.toLowerCase()) == sliceCmp,
     350              :     );
     351              :     if (match) {
     352            3 :       return ParseResult(
     353              :         true,
     354            6 :         source.substring(slice.length),
     355              :         lexeme: slice,
     356            3 :         element: toString(),
     357              :       );
     358              :     }
     359            4 :     return ParseResult(false, source, element: toString());
     360              :   }
     361              : 
     362            3 :   @override
     363              :   ParseResult parse(String source) =>
     364            7 :       jagged ? _parseJaggedSet(source) : _parseUniformSet(source);
     365              : 
     366            0 :   @override
     367            0 :   void accept(ElementVisitor visitor) => visitor.visitAlternativeLiterals(this);
     368              : }
     369              : 
     370              : /// Creates a case-insensitive set of alternative literals (the RFC 5234 default).
     371            3 : AlternativeLiterals alternativeLiterals(
     372              :   Iterable<String> value, {
     373              :   String? description,
     374            3 : }) => AlternativeLiterals(value, description: description);
     375              : 
     376              : /// Creates a case-sensitive set of alternative literals (equivalent to RFC 7405 `%s`).
     377            0 : AlternativeLiterals caseSensitiveAlternativeLiterals(
     378              :   Iterable<String> value, {
     379              :   String? description,
     380            0 : }) => AlternativeLiterals(value, caseSensitive: true, description: description);
     381              : 
     382              : typedef ElementSequence = Iterable<Element>;
     383              : 
     384              : /// A sequence of grammar elements that must be matched in order.
     385              : final class Sequence implements Element {
     386              :   final ElementSequence elements;
     387              :   @override
     388              :   final String? description;
     389              : 
     390            1 :   Sequence(this.elements, {this.description});
     391              : 
     392            1 :   @override
     393              :   String toString() =>
     394            1 :       description ??
     395            6 :       elements.map((e) => e is Rule ? e.name : e.toString()).join(' ');
     396              : 
     397            1 :   @override
     398              :   ParseResult parse(String source) {
     399            1 :     final results = Stack<ParseResult>();
     400              :     var remaining = source;
     401              : 
     402            2 :     for (final element in elements) {
     403            1 :       final result = element.parse(remaining);
     404            1 :       results.push(result);
     405              : 
     406            1 :       if (!result.success) {
     407              :         // Backtrack: One part of the sequence failed, so the whole sequence fails.
     408              :         // We return the source as 'remaining' to allow the caller to retry other
     409              :         // paths if this sequence was part of an Alternatives.
     410            1 :         return ParseResult(
     411              :           false,
     412              :           remaining, // Original remaining before failure
     413            4 :           lexeme: source.substring(0, source.length - remaining.length),
     414            1 :           element: toString(),
     415              :           stack: results,
     416              :         );
     417              :       }
     418            1 :       remaining = result.remaining;
     419              :     }
     420            1 :     return ParseResult(
     421              :       true,
     422              :       remaining,
     423            4 :       lexeme: source.substring(0, source.length - remaining.length),
     424            1 :       element: toString(),
     425              :       stack: results,
     426              :     );
     427              :   }
     428              : 
     429            0 :   @override
     430            0 :   void accept(ElementVisitor visitor) => visitor.visitSequence(this);
     431              : }
     432              : 
     433              : /// Represents a concatenation of elements that must be matched in order (e.g., `foo bar`).
     434              : final class Concatenation implements Element {
     435              :   final ElementSequence sequence;
     436              :   @override
     437              :   final String? description;
     438              : 
     439            2 :   Concatenation(this.sequence, {this.description});
     440              : 
     441            2 :   @override
     442              :   String toString() =>
     443           13 :       sequence.map((e) => e is Rule ? e.name : e.toString()).join(' ');
     444              : 
     445            2 :   @override
     446              :   ParseResult parse(String source) {
     447              :     // Delegate to the shared sequence parsing logic
     448            6 :     return sequence._parseSequenceWithBacktracking(source, toString());
     449              :   }
     450              : 
     451            0 :   @override
     452            0 :   void accept(ElementVisitor visitor) => visitor.visitConcatenation(this);
     453              : }
     454              : 
     455            1 : Concatenation concatenation(ElementSequence elements) =>
     456            1 :     Concatenation(elements);
     457              : 
     458              : /// Represents a group of elements that must be matched together (e.g., `(foo bar)`).
     459              : final class Group implements Element {
     460              :   final Sequence sequence;
     461              :   @override
     462              :   final String? description;
     463              : 
     464            1 :   Group(ElementSequence elements, {this.description})
     465            1 :     : sequence = Sequence(elements);
     466              : 
     467            3 :   ElementSequence get elements => sequence.elements;
     468              : 
     469            1 :   @override
     470            2 :   String toString() => '($sequence)';
     471              : 
     472            1 :   @override
     473            2 :   ParseResult parse(String source) => sequence.parse(source);
     474              : 
     475            0 :   @override
     476            0 :   void accept(ElementVisitor visitor) => visitor.visitGroup(this);
     477              : }
     478              : 
     479            2 : Group group(ElementSequence elements) => Group(elements);
     480              : 
     481              : /// Represents an optional sequence of elements (e.g., `[foo bar]`).
     482              : final class OptionalSequence implements Element {
     483              :   final Sequence sequence;
     484              :   @override
     485              :   final String? description;
     486              : 
     487            1 :   OptionalSequence(ElementSequence elements, {this.description})
     488            1 :     : sequence = Sequence(elements);
     489              : 
     490            3 :   ElementSequence get elements => sequence.elements;
     491              : 
     492            1 :   @override
     493            2 :   String toString() => '[$sequence]';
     494              : 
     495            1 :   @override
     496              :   ParseResult parse(String source) {
     497            3 :     final result = sequence.elements._parseSequenceWithBacktracking(
     498              :       source,
     499            1 :       toString(),
     500              :     );
     501              : 
     502            1 :     if (result.success) {
     503              :       return result;
     504              :     } else {
     505              :       // If the sequence parse failed, the optional sequence still succeeds,
     506              :       // but consumes nothing and returns an empty result.
     507            1 :       return ParseResult(
     508              :         true, // Success is true because it's optional
     509              :         source, // No input consumed, remaining is the original source
     510            1 :         element: toString(),
     511              :         lexeme: '', // No lexeme produced
     512            1 :         stack: Stack<ParseResult>(), // Empty stack
     513              :       );
     514              :     }
     515              :   }
     516              : 
     517              :   /// Returns the alternatives for backtracking: the sequence and an empty match.
     518            0 :   Iterable<Element> get asAlternatives => [sequence, EmptyElement(toString())];
     519              : 
     520            0 :   @override
     521            0 :   void accept(ElementVisitor visitor) => visitor.visitOptionalSequence(this);
     522              : }
     523              : 
     524              : class EmptyElement implements Element {
     525              :   @override
     526              :   final String? description;
     527            0 :   EmptyElement(this.description);
     528            0 :   @override
     529            0 :   ParseResult parse(String source) => ParseResult(
     530              :     true,
     531              :     source,
     532            0 :     element: toString(),
     533              :     lexeme: '',
     534            0 :     stack: Stack<ParseResult>(),
     535              :   );
     536            0 :   @override
     537              :   void accept(ElementVisitor visitor) {}
     538            0 :   @override
     539              :   String toString() => '[]';
     540              : }
     541              : 
     542            0 : OptionalSequence optionalSequence(ElementSequence elements) =>
     543            0 :     OptionalSequence(elements);
     544              : 
     545            1 : OptionalSequence optional(ElementSequence elements) =>
     546            1 :     OptionalSequence(elements);
     547              : 
     548              : /// Represents a negative lookahead assertion. Matches if the inner element fails.
     549              : /// Consumes no input.
     550              : final class NegativeLookahead implements Element {
     551              :   final Element element;
     552              :   @override
     553              :   final String? description;
     554              : 
     555            0 :   NegativeLookahead(this.element, {this.description});
     556              : 
     557            0 :   @override
     558            0 :   String toString() => '(?!$element)';
     559              : 
     560            0 :   @override
     561              :   ParseResult parse(String source) {
     562            0 :     final result = element.parse(source);
     563            0 :     if (!result.success) {
     564            0 :       return ParseResult(
     565              :         true,
     566              :         source, // consumes no input
     567            0 :         element: toString(),
     568              :         lexeme: '',
     569              :       );
     570              :     }
     571            0 :     return ParseResult(false, source, element: toString());
     572              :   }
     573              : 
     574            0 :   @override
     575            0 :   void accept(ElementVisitor visitor) => visitor.visitNegativeLookahead(this);
     576              : }
     577              : 
     578            0 : NegativeLookahead negativeLookahead(Element element, {String? description}) =>
     579            0 :     NegativeLookahead(element, description: description);
     580              : 
     581              : /// Represents alternative rules that can be matched (e.g., `foo / bar` in RFC 5234).
     582              : final class Alternatives implements Element {
     583              :   final ElementSequence elements;
     584              :   @override
     585              :   final String? description;
     586              : 
     587            3 :   Alternatives(this.elements, {this.description});
     588              : 
     589            3 :   @override
     590              :   String toString() =>
     591           20 :       elements.map((e) => e is Rule ? e.name : e.toString()).join(' / ');
     592              : 
     593            2 :   @override
     594              :   ParseResult parse(String source) {
     595            4 :     for (final element in elements) {
     596            2 :       final result = element.parse(source);
     597            2 :       if (result.success) {
     598              :         // Return the first successful match found.
     599              :         // Construct a new result attributed to this Alternatives rule,
     600              :         // pushing the successful sub-element's result onto its stack.
     601            2 :         final lexeme = source.substring(
     602              :           0,
     603            8 :           source.length - result.remaining.length,
     604              :         );
     605            2 :         return ParseResult(
     606              :           true,
     607            2 :           result.remaining,
     608            2 :           element: toString(),
     609              :           lexeme: lexeme,
     610            4 :           stack: Stack<ParseResult>()..push(result),
     611              :         );
     612              :       }
     613              :       // If result.success is false, we continue to the next alternative (backtracking).
     614              :     }
     615              : 
     616              :     // If no alternatives succeeded, return failure.
     617            4 :     return ParseResult(false, source, element: toString());
     618              :   }
     619              : 
     620            0 :   @override
     621            0 :   void accept(ElementVisitor visitor) => visitor.visitAlternatives(this);
     622              : }
     623              : 
     624            6 : Alternatives alternatives(Iterable<Element> elements) => Alternatives(elements);
     625              : 
     626              : /// Represents variable repetition of an element (e.g., `*foo` or `1*3foo`).
     627              : final class Repetition implements Element {
     628              :   final Element element;
     629              :   final int min;
     630              : 
     631              :   @override
     632              :   final String? description;
     633              : 
     634              :   // If [max] is null, infinity is the maximum
     635              :   final int? max;
     636              : 
     637            4 :   Repetition(this.element, {this.min = 0, this.max, this.description});
     638              : 
     639            4 :   @override
     640              :   String toString() {
     641            4 :     final buffer = StringBuffer();
     642              : 
     643           12 :     if (min == max) {
     644            4 :       buffer.write(min);
     645              :     } else {
     646           13 :       buffer.write(min > 0 ? '$min*' : '*');
     647              : 
     648            3 :       if (max != null) {
     649            4 :         buffer.write(max!);
     650              :       }
     651              :     }
     652              : 
     653           24 :     buffer.write('(${element is Rule ? (element as Rule).name : element})');
     654            4 :     return buffer.toString();
     655              :   }
     656              : 
     657            3 :   @override
     658              :   ParseResult parse(String source) {
     659            9 :     if (min > source.length) {
     660            1 :       return ParseResult(false, source);
     661              :     }
     662              : 
     663            3 :     final results = Stack<ParseResult>();
     664              : 
     665              :     var remaining = source;
     666              :     var count = 0;
     667              : 
     668            3 :     final upper = max;
     669              : 
     670            5 :     while (remaining.isNotEmpty && (upper == null || count < upper)) {
     671            6 :       final result = element.parse(remaining);
     672            3 :       results.push(result);
     673            3 :       if (result.success) {
     674            3 :         count++;
     675            3 :         remaining = result.remaining;
     676              :       } else {
     677              :         // If the inner element fails but we've met the minimum requirement,
     678              :         // we stop repeating and return success.
     679            6 :         if (count >= min) {
     680              :           break;
     681              :         }
     682              :         // Otherwise, the entire repetition fails.
     683            2 :         return ParseResult(false, source, stack: results, element: toString());
     684              :       }
     685              :     }
     686              : 
     687            6 :     if (count < min) {
     688              :       // Didn't get the lower limit of repetitions
     689            0 :       return ParseResult(false, remaining, stack: results, element: toString());
     690              :     }
     691           12 :     final String lexeme = source.substring(0, source.length - remaining.length);
     692            3 :     return ParseResult(
     693              :       true,
     694              :       remaining,
     695              :       stack: results,
     696              :       lexeme: lexeme,
     697            3 :       element: toString(),
     698              :     );
     699              :   }
     700              : 
     701            0 :   @override
     702            0 :   void accept(ElementVisitor visitor) => visitor.visitRepetition(this);
     703              : }
     704              : 
     705            3 : Repetition variableRepetition(Element value, {int min = 0, int? max}) =>
     706            3 :     Repetition(value, min: min, max: max);
     707              : 
     708            2 : Repetition repetition(Element value, int n) =>
     709            2 :     Repetition(value, min: n, max: n);
     710              : 
     711              : /// Represents a range of values (e.g., `%x41-5A`).
     712              : final class ValueRange implements Element {
     713              :   final int start;
     714              :   final int end;
     715              :   @override
     716              :   final String? description;
     717              : 
     718            4 :   ValueRange(this.start, this.end, {this.description});
     719              : 
     720            4 :   @override
     721              :   String toString() =>
     722           28 :       '%x${start.toRadixString(16).toUpperCase()}-${end.toRadixString(16).toUpperCase()}';
     723              : 
     724            0 :   AlternativeLiterals toAlternativeLiteral() {
     725            0 :     final alts = <String>[];
     726              : 
     727            0 :     for (int i = start; i <= end; i++) {
     728            0 :       alts.add(String.fromCharCode(i));
     729              :     }
     730            0 :     return alternativeLiterals(alts);
     731              :   }
     732              : 
     733            4 :   @override
     734              :   ParseResult parse(String source) {
     735              :     // Check if the source string is empty.
     736            4 :     if (source.isEmpty) {
     737            2 :       return ParseResult(false, source, element: toString());
     738              :     }
     739              : 
     740            8 :     final rune = source.runes.first;
     741              : 
     742              :     // Check if the 32-bit code point falls within the range.
     743           16 :     if (rune < start || rune > end) {
     744            8 :       return ParseResult(false, source, element: toString());
     745              :     }
     746              : 
     747              :     // Yield exactly one rune.
     748            4 :     final lexeme = String.fromCharCode(rune);
     749            4 :     return ParseResult(
     750              :       true,
     751            8 :       source.substring(lexeme.length),
     752            4 :       element: toString(),
     753              :       lexeme: lexeme,
     754              :     );
     755              :   }
     756              : 
     757            0 :   @override
     758            0 :   void accept(ElementVisitor visitor) => visitor.visitValueRange(this);
     759              : }
     760              : 
     761            8 : ValueRange valueRange(int start, int end) => ValueRange(start, end);
     762              : 
     763              : extension _ParseSequenceExtension on ElementSequence {
     764              :   /// Parses a sequence of elements using backtracking to support alternative paths.
     765              :   ///
     766              :   /// Backtracking is required when a sequence contains [Alternatives] that might
     767              :   /// match different lengths of the input source. If one path fails later in the
     768              :   /// sequence, this will backtrack and attempt other alternative paths.
     769            3 :   ParseResult _parseSequenceWithBacktracking(
     770              :     String source,
     771              :     String ruleDescription,
     772              :   ) {
     773              :     // Helper function to parse a subsequence starting from a given index
     774            3 :     ParseResult parseSubsequence(String currentSource, int startIndex) {
     775            3 :       final Stack<ParseResult> successfulResults = Stack();
     776              :       String remainingSource = currentSource;
     777              : 
     778            9 :       for (int i = startIndex; i < length; i++) {
     779            3 :         final element = elementAt(i);
     780              : 
     781            6 :         if (element is Alternatives || element is OptionalSequence) {
     782            0 :           final alternatives = element is Alternatives
     783            0 :               ? element.elements
     784            0 :               : (element as OptionalSequence).asAlternatives;
     785              :           bool alternativeMatched = false;
     786            0 :           for (final alternativeElement in alternatives) {
     787            0 :             final altResult = alternativeElement.parse(remainingSource);
     788            0 :             if (altResult.success) {
     789              :               // Try parsing the rest of the sequence with this alternative's result
     790            0 :               final restResult = parseSubsequence(altResult.remaining, i + 1);
     791            0 :               if (restResult.success) {
     792              :                 // Found a valid path
     793              :                 // Combine altResult and restResult into the main stack
     794            0 :                 successfulResults.push(altResult);
     795            0 :                 successfulResults.pushAll(restResult.stack);
     796            0 :                 remainingSource = restResult.remaining;
     797              :                 alternativeMatched = true;
     798              :                 break; // Found a working alternative, stop trying others
     799              :               }
     800              :               // Backtrack: This alternative didn't work for the rest of the sequence
     801              :               // Continue to the next alternative
     802              :             }
     803              :           }
     804              :           if (!alternativeMatched) {
     805              :             // No alternative allowed the rest of the sequence to parse
     806            0 :             return ParseResult(
     807              :               false,
     808              :               source,
     809              :               element: ruleDescription,
     810            0 :               stack: Stack<ParseResult>()..pushAll(successfulResults),
     811              :             );
     812              :           }
     813              :         } else {
     814              :           // Regular element (not Alternatives)
     815            3 :           final result = element.parse(remainingSource);
     816            3 :           if (result.success) {
     817            3 :             successfulResults.push(result);
     818            3 :             remainingSource = result.remaining;
     819              :           } else {
     820              :             // If any non-alternative element fails, the concatenation fails
     821            3 :             return ParseResult(
     822              :               false,
     823              :               source,
     824              :               element: ruleDescription,
     825            6 :               stack: Stack<ParseResult>()..pushAll(successfulResults),
     826              :             );
     827              :           }
     828              :         }
     829              :       }
     830              : 
     831              :       // If we reached here, the subsequence (from startIndex) parsed successfully
     832            3 :       final String lexeme = source.substring(
     833              :         0,
     834            9 :         source.length - remainingSource.length,
     835              :       );
     836            3 :       return ParseResult(
     837              :         true,
     838              :         remainingSource,
     839              :         element: ruleDescription,
     840              :         lexeme: lexeme,
     841              :         stack: successfulResults,
     842              :       );
     843              :     }
     844              : 
     845              :     // Start parsing from the beginning of the sequence
     846            3 :     return parseSubsequence(source, 0);
     847              :   }
     848              : }
        

Generated by: LCOV version 2.0-1