Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Dart CI

on:
pull_request:
paths:
- 'lib/**'
- 'bin/**'
- 'test/**'
- 'pubspec.yaml'
- 'pubspec.lock'
- 'analysis_options.yaml'
- '.github/workflows/test.yml'
merge_group:
types: [checks_requested]
push:
branches: [main]
paths:
- 'lib/**'
- 'bin/**'
- 'test/**'
- 'pubspec.yaml'
- 'pubspec.lock'
- 'analysis_options.yaml'
- '.github/workflows/test.yml'

jobs:
test:
name: Dart Format, Analyze, and Test
runs-on: ubuntu-latest
permissions:
contents: read

steps:
Comment thread
zanderso marked this conversation as resolved.
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
persist-credentials: false

- name: Setup Dart
uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d # v1.8.1

- name: Install Dependencies
run: dart pub get

- name: Verify Formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze Code
run: dart analyze --fatal-infos

- name: Run Tests
run: dart test
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Created by https://www.toptal.com/developers/gitignore/api/dart
# Edit at https://www.toptal.com/developers/gitignore?templates=dart

### Dart ###
# Don't commit the following directories created by pub.
.dart_tool/
.packages
build/
# If you're building an executable, sub-directories will be created in
# .dart_tool/pub/bin/ that contain the compiled executables. Do not
# commit these.
.dart_tool/pub/bin/
pubspec.lock
4 changes: 4 additions & 0 deletions .markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
default: true # Enable all standard markdownlint rules by default
MD013: false # Do not enforce line lengths (diff churning, table lengths, diagrams etc)
MD033: false # Allow inline HTML (badges, centered logos/images, details/summary folds)
MD041: true # Enforce top-level heading (# RFC AAA.NNNN: Title) after frontmatter
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"cSpell.words": [
"Basenames",
"frontmatter",
"octocat",
"Slugified"
]
}
7 changes: 7 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include: package:lints/recommended.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
120 changes: 120 additions & 0 deletions lib/src/models/rfc_author.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2026 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/// Represents an author attribution in RFC frontmatter.
sealed class RfcAuthor {
/// The raw author string representation.
final String raw;

const RfcAuthor({required this.raw});

/// Regex pattern for GitHub user profile URLs:
/// `https://github.com/<username>`
static final RegExp githubUrlPattern = RegExp(
r'^https:\/\/github\.com\/([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})\/?$',
caseSensitive: false,
);

/// Regex pattern for RFC 5322 mailbox format:
/// `"Display Name" <user@example.com>` or `'Display Name' <user@example.com>` or `Display Name <user@example.com>`
static final RegExp mailboxPattern = RegExp(
r'^(?:"((?:[^"\\]|\\.)+)"|'
"'"
r"((?:[^'\\]|\\.)+)'"
r'|([^<]+))\s+<([^@\s>]+@[^@\s>]+\.[^@\s>]+)>$',
);

/// Attempts to parse an author string into a [GitHubAuthor] or [EmailAuthor].
///
/// Returns `null` if the format is unrecognized or invalid.
static RfcAuthor? tryParse(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty) return null;

final ghMatch = githubUrlPattern.firstMatch(trimmed);
if (ghMatch != null) {
return GitHubAuthor(username: ghMatch.group(1)!, raw: trimmed);
}

final mbMatch = mailboxPattern.firstMatch(trimmed);
if (mbMatch != null) {
var name = (mbMatch.group(1) ?? mbMatch.group(2) ?? mbMatch.group(3))
?.trim();
if (name != null) {
name = name.replaceAll(r'\"', '"').replaceAll(r"\'", "'");
}
final email = mbMatch.group(4)?.trim();
if (name != null &&
name.isNotEmpty &&
email != null &&
email.isNotEmpty) {
return EmailAuthor(name: name, email: email, raw: trimmed);
}
}

return null;
}

/// Parses an author string into a [GitHubAuthor] or [EmailAuthor].
///
/// Throws [FormatException] if the author string cannot be parsed.
factory RfcAuthor.parse(String value) {
final author = tryParse(value);
if (author == null) {
throw FormatException(
'Author "$value" must be a GitHub profile URL ("https://github.com/<username>") '
'or RFC 5322 mailbox (\'"Display Name" <user@example.com>\').',
);
}
return author;
}
}

/// An author represented by a GitHub profile.
final class GitHubAuthor extends RfcAuthor {
/// The GitHub username.
final String username;

const GitHubAuthor({required this.username, String? raw})
: super(raw: raw ?? 'https://github.com/$username');

/// Canonical GitHub user profile URL.
String get url => 'https://github.com/$username';

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GitHubAuthor && username == other.username;

@override
int get hashCode => username.hashCode;

@override
String toString() => 'GitHubAuthor(username: $username)';
}

/// An author represented by an RFC 5322 mailbox.
final class EmailAuthor extends RfcAuthor {
/// The display name of the author (e.g. "John McDole").
final String name;

/// The email address of the author (e.g. "codefu@google.com").
final String email;

const EmailAuthor({required this.name, required this.email, String? raw})
: super(raw: raw ?? '"$name" <$email>');

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is EmailAuthor &&
name == other.name &&
email.toLowerCase() == other.email.toLowerCase();

@override
int get hashCode => Object.hash(name, email.toLowerCase());

@override
String toString() => 'EmailAuthor(name: $name, email: $email)';
}
18 changes: 18 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: rfc_tools
description: Tooling, validation, and linting for Flutter RFC repository.
version: 0.1.0
publish_to: 'none'

environment:
sdk: '>=3.12.0 <4.0.0'

dependencies:
args: ^2.5.0
clock: ^1.1.2
file: ^7.0.0
path: ^1.9.0
yaml: ^3.1.2

dev_dependencies:
lints: ^6.1.0
test: ^1.25.0
Loading
Loading