PetitParser는 Dart용 동적 파서 프레임워크로, 개발자가 최소한의 노력으로 효율적이고 재사용 가능한 문법과 파서를 구축할 수 있게 해줍니다. 파싱 규칙을 정의하기 위한 유창한 API를 지원하며 성능 최적화되어 있어 구성 파일, 도메인 전용 언어 또는 데이터 형식과 같은 구조화된 텍스트 처리에 이상적입니다. 이 프레임워크는 매우 모듈화되고 확장 가능하여 사용자 정의 파싱 로직을 쉽게 조합할 수 있습니다. 모든 주요 플랫폼에서 지원되므로, 강력한 텍스트 분석이 필요한 클라이언트 측 및 서버 측 애플리케이션 모두에 적합합니다.
사용 사례
구성 파일 파싱
도메인 전용 언어 구현
구조화된 데이터 형식 처리
코드 분석기 구축
사용자 정의 DSL 만들기
주요 기능
플루언트 문법 정의 API
고성능 파싱
모듈러 및 조합 가능한 규칙
크로스 플랫폼 호환성
재귀 문법 지원
확장 가능한 플러그인 시스템
적합한 대상
사용자 정의 파서를 구축하는 개발자
구조화된 텍스트 입력을 사용하는 팀
DSL 지원이 필요한 응용 프로그램
효율적인 텍스트 처리가 필요한 프로젝트
고려 사항
초보자를 위한 학습 곡선이 더 가파릅니다
구문 분석 개념에 대한 이해가 필요합니다
제한된 내장 오류 복구 지원
간단한 문자열 작업보다는 복잡한 구문 분석 작업에 적합
원본 README
연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.
PetitParser for Dart
PetitParser makes writing parsers in Dart fast, enjoyable, and fully type-safe.
Instead of writing fragile regular expressions or configuring complex external code generators, PetitParser lets you build parsers directly in plain Dart code. You assemble small, specialized parsers like LEGO bricks to model everything from simple configuration values to full programming languages.
✨ Why PetitParser?
🧩 Composable: Combine simple parsers into rich grammars using familiar operators and methods.
🛡️ Type-Safe: Take advantage of modern Dart records, generics, and pattern matching without casting dynamic types.
⚡ Fast & Efficient: Optimized character predicates and dedicated fast-path parsing keep execution blazing fast.
🔍 Debuggable: Step through parsing in your IDE, inspect tokens, and visualize execution trees with built-in tools.
🛠️ Extensible: Transform raw text directly into strongly typed models, syntax trees, or domain objects on the fly.
🌐 Open Source & Battle-Tested: Highly tested, well-documented, and trusted across production Dart and Flutter applications.
🚀 Quick Start
Add PetitParser to your Dart or Flutter project:
dart pub add petitparser
Import the package, assemble your parser, and parse key-value pairs:
import'package:petitparser/petitparser.dart';
voidmain() {
// 1. Match a key of one or more letters:final key = letter().plus().flatten();
// 2. Match a numeric value and convert it directly into an integer:final value = digit().plus().flatten().map(int.parse);
// 3. Combine them: match key, discard '=', and match value:final entry = key.trim().skip(after: char('=')).then(value.trim());
// 4. Transform the parsed pair into a typed Dart record:final parser = entry.map2((k, v) => (key: k, value: v));
// 5. Parse the input:final result = parser.parse('port = 8080');
print(result.value); // (key: port, value: 8080)
}
Notice what happened here:
letter().plus() matched consecutive letters, and .flatten() turned them into a String.
digit().plus() matched consecutive digits, and .map(int.parse) transformed them immediately into an int.
.skip(after: char('=')) consumed the separator without cluttering the result.
.then(...) combined both parts into a strongly typed record (String, int).
.map2(...) mapped both positional values into a readable named record.
🧩 Core Building Blocks
Every grammar in PetitParser is constructed from three fundamental concepts: matching text, combining parsers, and transforming results.
🔡 Matching Text
Primitive parsers recognize basic character patterns:
char('a') matches an exact single character:
char('a').parse('a').value; // 'a'
string('dart') matches an entire string:
string('dart').parse('dart').value; // 'dart'
digit() matches any digit from 0 to 9:
digit().parse('7').value; // '7'
letter() matches any uppercase or lowercase letter:
letter().parse('x').value; // 'x'
word() matches a letter, digit, or underscore:
word().parse('_').value; // '_'
whitespace() matches spaces, tabs, and newlines:
whitespace().parse(' ').value; // ' '
pattern('0-9a-fA-F') matches any character within a range:
pattern('0-9a-fA-F').parse('f').value; // 'f'
any() matches any character:
any().parse('!').value; // '!'
Most character parsers accept optional parameters such as ignoreCase: true for case-insensitive matching and unicode: true for full Unicode code-point support.
🔗 Combining Parsers
Snap individual parsers together to recognize structured input:
Sequences: Run parsers in order. letter().then(digit()) returns a typed Dart record with each result:
Calling parse on a string returns a Result. You can inspect the outcome using Dart pattern matching:
final parser = digit().plus().flatten().map(int.parse);
voidhandleResult(Result<int> result) {
switch (result) {
caseSuccess(value: final value):
print('Parsed $value');
caseFailure(message: final message, position: final position):
print('Error at $position: $message');
}
}
handleResult(parser.parse('123')); // Parsed 123handleResult(parser.parse('abc')); // Error at 0: digit expected
If you only need to know whether an input string conforms to your grammar, use accept:
if (parser.accept('123')) {
print('Valid input');
}
To extract all matching occurrences across a larger body of text, use allMatches:
final words = letter().plus().flatten();
final matches = words.allMatches('two words 123');
print(matches); // ('two', 'words')
🛠️ Practical Examples
📋 Delimited Lists
This example parses a comma-separated list of integers like '1, 2, 3' into a Dart List<int>:
final number = digit().plus().flatten().map(int.parse);
final numbers = number
.plusSeparated(char(',').trim())
.map((list) => list.elements);
final result = numbers.parse('1, 2, 3');
print(result.value); // [1, 2, 3]
list.elements gives you a typed List<int> containing only the parsed numbers, neatly dropping the comma characters.
🧮 Expression Builder
This example parses and evaluates mathematical expressions containing numbers, parentheses, negation, exponentiation, multiplication, division, addition, and subtraction:
This example parses nested JSON-style arrays of numbers like '[1, [2, 3], 4]' into a hierarchy of Dart lists:
final value = undefined<Object>();
// An array contains zero or more values separated by commas:final array = char('[')
.trim()
.then(value.starSeparated(char(',').trim()))
.then(char(']').trim())
.map3((open, elements, close) => elements.elements);
final number = digit().plus().flatten().map(int.parse);
// Wire the recursive definition into the placeholder:
value.set([number, array].toChoiceParser());
final parser = value.end();
print(parser.parse('[1, [2, 3], 4]').value); // [1, [2, 3], 4]
🏛️ Large Grammars
When a grammar grows to dozens or hundreds of productions, organizing rules as separate variables can become difficult to maintain. Subclassing GrammarDefinition lets you organize rules as methods on a class, resolving complex mutual recursions cleanly.
This example parses and evaluates arithmetic expressions using a modular, strongly typed grammar definition:
You can also build a parser starting from any specific production rule in the grammar using buildFrom:
final numberParser = definition.buildFrom(ref0(definition.number));
print(numberParser.parse('42').value); // 42
Organizing grammars this way makes them easy to navigate, refactor, and test production by production in your IDE.
🔬 Advanced Capabilities
📍 Source Positions and Tokens
If you are writing a compiler, linter, or syntax highlighter, you often need to know exactly where in the source file an element appeared. Wrap any parser with .token() to capture its start and stop offsets, line number, and column number:
final id = letter().plus().flatten().token();
final token = id.parse('hello').value;
print(token.value); // 'hello'print(token.start); // 0print(token.stop); // 5print(token.line); // 1print(token.column); // 1
🔭 Lookaheads and Semantic Guards
You can inspect the upcoming input stream without consuming characters:
parser.and() performs positive lookahead, succeeding only if parser matches next without advancing the input position.
parser.not() performs negative lookahead, succeeding only if parser fails to match next.
parser.where(predicate) rejects a match dynamically if a condition is not met.
For example, match a keyword like let only when it is not immediately followed by more word characters:
final keyword = string('let').skip(after: word().not());
print(keyword.parse('let').value); // 'let'print(keyword.accept('letter')); // false
⏳ Lazy Repetition
Standard repetition combinators like star() are possessive and consume as much input as possible. When matching delimited content like multi-line comments or quoted strings, use starLazy or plusLazy to stop as soon as a boundary parser succeeds:
Grammars can contain subtle issues such as unreachable branches, infinite loops, or accidental left-recursions. PetitParser provides a built-in linter in package:petitparser/reflection.dart that you can run in your tests:
For performance tuning, import package:petitparser/debug.dart and use profile(parser) to measure invocation counts and execution time per parser, or progress(parser) to inspect backtracking behavior.
📚 Resources
Website: Interactive web demos and ports to other languages.
Examples Repository: Full implementations for JSON, Lisp, Math, Smalltalk, and more.
📜 History
Grammars for programming languages are traditionally specified statically. They are hard to compose and reuse due to ambiguities that inevitably arise. PetitParser combines ideas from scannerless parsing, parser combinators, parsing expression grammars (PEG), and packrat parsers to model grammars and parsers as objects that can be reconfigured dynamically.
PetitParser was originally created in Smalltalk. Later on, as a way to learn and explore these platforms, I reimplemented PetitParser in Java and Dart. All implementations share a consistent architecture while adopting the idioms and modern best practices of each target language.