Skip to content
Merged
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 0.3.0

This release is a complete, ground-up rewrite of the package. It provides a fully compliant, strictly validated, and idiomatic Dart implementation of the Punycode standard.

### 🚨 Breaking Changes
* **Separation of IDNA and Raw Punycode:** The default `PunycodeCodec` no longer attempts to automatically parse and encode/decode full domains or email addresses. It now acts purely on raw strings.
* **Removed `PunycodeCodec.simple()`:** Because the default codec now handles raw strings exclusively, the `.simple()` constructor has been removed. A new global `punycode` instance is provided for convenience.
* **Extracted Domain & Email Handling:** Domain and email conversions are now handled by dedicated top-level functions (`domainToAscii`, `domainToUnicode`, `emailToAscii`, and `emailToUnicode`).
* **Strict (but Configurable) IDNA Validation:** IDNA processing now strictly enforces formatting rules by default. Inputs with labels exceeding 63 characters, domains exceeding 253 characters, or invalid hyphen placements will actively throw a `FormatException`. You can bypass this by passing `validate: false`.

### ✨ New Features & Improvements
* **Full RFC 3492 Compliance:** Rigorous implementation of the complete Punycode specification.
* **Mixed-Case Support (RFC 3429 Appendix A):** Added support for Appendix A mixed-case annotations, properly preserving casing during encoding and decoding.
* **Official Errata Fixes:** Implemented technical corrections for RFC 3492 Errata IDs 265 and 3026.
* **IDNA2003 Separator Support:** `domainToAscii` now recognizes and properly splits labels using standard IDNA2003 separators (`.`, `\u3002`, `\uFF0E`, `\uFF61`).
* **Platform Parity:** Added a comprehensive suite of unit and integration tests to guarantee identical behavior across both Dart VM and Web platforms.

## 0.2.2

- Fix code formatting and ensure README is up to date.
Expand Down
117 changes: 65 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,26 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
[![Open in Firebase Studio](https://cdn.firebasestudio.dev/btn/open_light_20.svg)](https://studio.firebase.google.com/import?url=https%3A%2F%2Fgithub.com%2Fdropbear-software%2Fpunycoder)
# Punycoder
A Dart implementation of the Punycode (RFC 3492) encoding algorithm used for
Internationalized Domain Names in Applications (IDNA).

## Overview
A pure Dart implementation of Punycode ([RFC 3492](https://tools.ietf.org/html/rfc3492)) with support for mixed-case annotation and technical errata.

This package provides a robust and efficient way to convert Unicode strings,
such as internationalized domain names or email addresses, into their ASCII-Compatible
Encoding (ACE) representation according to the Punycode specification (RFC 3492),
and back again.

Punycode allows representing Unicode characters using only the limited ASCII
character subset (letters, digits, and hyphens) allowed in components of
domain names. This is essential for the IDNA standard, enabling the use of
international characters in domain names while maintaining compatibility with
the existing DNS infrastructure. ACE labels generated by IDNA typically start
with the prefix `xn--`.
Punycoder provides an idiomatic and high-performance way to encode and decode Punycode strings, which are essential for Internationalized Domain Names in Applications (IDNA).

## Features

* **RFC 3492 Compliant:** Implements the Punycode encoding and decoding
algorithms as specified in the standard.
* **IDNA Friendly:** Provides convenient ways of converting full domain names or
email addresses, automatically handling the `xn--` prefix and processing
only the necessary parts of the string.
* **Idiomatic Dart:** This package implements the `Converter<S, T>` interface
defined in `dart:convert` to make working with Punycode feel easy and familiar.
* **Efficient and Tested:** Based on a port of the well-regarded Punycode.js
library, including tests based on official RFC examples.
- **Standard Compliant**: Faithful implementation of the Bootstring algorithm specifically for Punycode.
- **Mixed-Case Annotation**: Full support for Appendix A, preserving original character casing during the encoding process.
- **Cross-Platform**: Fully compatible with both the Dart VM and Web (transpiled via dart2js or ddc).
- **Native Performance**: Uses `StringBuffer` and Unicode-aware `Runes` for efficient processing.
- **Idiomatic API**: Implements `Codec<String, String>` for seamless integration with `dart:convert`.

## Getting Started
## Getting started

Add the package to your `pubspec.yaml`:
Add `punycoder` to your `pubspec.yaml` dependencies:

```yaml
dependencies:
punycoder: ^0.2.0
punycoder: ^0.3.0
```

Then, import the library in your Dart code:
Expand All @@ -48,31 +32,60 @@ import 'package:punycoder/punycoder.dart';

## Usage

### Basic Encoding and Decoding

```dart
import 'package:punycoder/punycoder.dart';
// Encode a Unicode string to Punycode
final encoded = punycode.encode('münchen'); // mnchen-3ya

// Decode a Punycode string back to Unicode
final decoded = punycode.decode('mnchen-3ya'); // münchen
```

### IDNA Helpers (Domains and Emails)

Punycoder provides high-level helpers for handling Internationalized Domain Names (IDN) and email addresses.

```dart
// Convert a Unicode domain to ASCII (Punycode)
final domainAscii = domainToAscii('mañana.com'); // xn--maana-pta.com

// Convert back to Unicode
final domainUnicode = domainToUnicode('xn--maana-pta.com'); // mañana.com

// Supports IDNA2003 separators (。 . 。)
final alternative = domainToAscii('mañana\u3002com'); // xn--maana-pta.com

// Convert an email address
final emailAscii = emailToAscii('джумла@джpумлатест.bрфa');
// джумла@xn--p-8sbkgc5ag7bhce.xn--ba-lmcq
```

By default, `domainToAscii` and `emailToAscii` perform validation (label length, domain length, invalid characters). You can disable this if needed:

```dart
final raw = domainToAscii('ab--c.com', validate: false);
```

### Preserving Mixed Case

By default, Punycoder uses Appendix A annotations to preserve casing:

```dart
final encoded = punycode.encode('MÜnchen'); // Mnchen-3yA
final decoded = punycode.decode('Mnchen-3yA'); // MÜnchen
```

## Additional information

### Contributions

Contributions are welcome! Please feel free to open issues or submit pull requests on the [GitHub repository](https://github.com/dropbear-software/punycoder).

### Reporting Issues

If you encounter any bugs or have feature requests, please file them through the [issue tracker](https://github.com/dropbear-software/punycoder/issues).

### License

void main() {
// Designed to be used with domains and emails which have special rules
const domainCodec = PunycodeCodec();
// Designed to work with simple strings
const simpleCodec = PunycodeCodec.simple();

final encodedString = simpleCodec.encode('münchen');
final encodedDomain = domainCodec.encode('münchen.com');
final encodedEmail = domainCodec.encode('münchen@münchen.com');

stdout.writeln(encodedString); // Output: mnchen-3ya
// Uses the correct prefix for the domain
stdout.writeln(encodedDomain); // Output: xn--mnchen-3ya.com
// Only the domain should be encoded
stdout.writeln(encodedEmail); // Output: münchen@xn--mnchen-3ya.com

final decodedString = simpleCodec.decode('mnchen-3ya');
final decodecDomain = domainCodec.decode('xn--mnchen-3ya.com');
final decodedEmail = domainCodec.decode('münchen@xn--mnchen-3ya.com');

stdout.writeln(decodedString); // Output: münchen
stdout.writeln(decodecDomain); // Output: münchen.com
stdout.writeln(decodedEmail); // Output: münchen@münchen.com
}
```
This project is licensed under the MIT License - see the LICENSE file for details.
108 changes: 80 additions & 28 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,82 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml

include: package:oath/strict.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types

# analyzer:
# exclude:
# - path/to/excluded/files/**

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options
linter:
rules:
- always_put_required_named_parameters_first
- annotate_redeclares
- avoid_annotating_with_dynamic
- avoid_bool_literals_in_conditional_expressions
- avoid_catches_without_on_clauses
- avoid_catching_errors
- avoid_classes_with_only_static_members
- avoid_double_and_int_checks
- avoid_dynamic_calls
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_final_parameters
- avoid_futureor_void
- avoid_js_rounded_ints
- avoid_multiple_declarations_per_line
- avoid_positional_boolean_parameters
- avoid_print
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_returning_this
- avoid_setters_without_getters
- avoid_unused_constructor_parameters
- avoid_void_async
- combinators_ordering
- comment_references
- conditional_uri_does_not_exist
- deprecated_consistency
- directives_ordering
- discarded_futures
- do_not_use_environment
- document_ignores
- implicit_reopen
- invalid_runtime_check_with_js_interop_types
- leading_newlines_in_multiline_strings
- matching_super_parameters
- missing_code_block_language_in_doc_comment
- no_literal_bool_comparisons
- noop_primitive_operations
- omit_local_variable_types
- only_throw_errors
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_constructors_over_static_methods
- prefer_final_in_for_each
- prefer_final_locals
- prefer_if_elements_to_conditional_expressions
- prefer_single_quotes
- public_member_api_docs
- sort_pub_dependencies
- strict_top_level_inference
- throw_in_finally
- unawaited_futures
- unintended_html_in_doc_comment
- unnecessary_async
- unnecessary_await_in_return
- unnecessary_breaks
- unnecessary_ignore
- unnecessary_lambdas
- unnecessary_library_directive
- unnecessary_library_name
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_statements
- unnecessary_underscores
- unreachable_from_main
- unsafe_variance
- use_is_even_rather_than_modulo
- use_named_constants
- use_null_aware_elements
- use_raw_strings
- use_truncating_division
49 changes: 31 additions & 18 deletions doc/api/__404error.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,31 @@
</button>
</header>
<main>
<div id="dartdoc-main-content" class="main-content">
<h1>404: Something's gone wrong :-(</h1>
<section class="desc">
<p>You've tried to visit a page that doesn't exist. Luckily this site
has other <a href="index.html">pages</a>.</p>
<p>If you were looking for something specific, try searching:
<div id="dartdoc-main-content" class="main-content">
<h1>404: Something's gone wrong :-(</h1>
<section class="desc">
<p>
You've tried to visit a page that doesn't exist. Luckily this site has
other <a href="index.html">pages</a>.
</p>
<div>
If you were looking for something specific, try searching:
<form class="search-body" role="search">
<input type="text" id="search-body" autocomplete="off" disabled class="form-control typeahead" placeholder="Loading search...">
<input
type="text"
id="search-body"
autocomplete="off"
disabled
class="form-control typeahead"
placeholder="Loading search..."
/>
</form>
</p>
</section>
</div> <!-- /.main-content -->
<div id="dartdoc-sidebar-left" class="sidebar sidebar-offcanvas-left">
<!-- The search input and breadcrumbs below are only responsively visible at low resolutions. -->
</div>
</section>
</div>
<!-- /.main-content -->
<div id="dartdoc-sidebar-left" class="sidebar sidebar-offcanvas-left">
<!-- The search input and breadcrumbs below are only responsively visible at low resolutions. -->
<header id="header-search-sidebar" class="hidden-l">
<form class="search-sidebar" role="search">
<input type="text" id="search-sidebar" autocomplete="off" disabled class="form-control typeahead" placeholder="Loading search...">
Expand All @@ -63,20 +74,22 @@ <h1>404: Something's gone wrong :-(</h1>
<li class="self-crumb">punycoder package</li>
</ol>

<h5><span class="package-name">punycoder</span> <span class="package-kind">package</span></h5>
<ol>
<h5>
<span class="package-name">punycoder</span>
<span class="package-kind">package</span>
</h5>
<ol>
<li class="section-title">Libraries</li>
<li><a href="punycoder/">punycoder</a></li>
</ol>

</div>
<div id="dartdoc-sidebar-right" class="sidebar sidebar-offcanvas-right">
</div>
</div>
<div id="dartdoc-sidebar-right" class="sidebar sidebar-offcanvas-right"></div>
</main>
<footer>
<span class="no-break">
punycoder
0.1.0
0.3.0
</span>

</footer>
Expand Down
Loading