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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 0.2.0
- Complete package rewrite that is now implemented as a wrapper around the native `Uri` class.
- Automatic Punycode conversion for hostnames in `toUri()`.
- Unicode-aware component accessors (host, path, query, fragment, userInfo).
- Support for standard constructors: `Iri()`, `Iri.http()`, `Iri.https()`, `Iri.file()`.
- Static methods `Iri.parse()` and `Iri.tryParse()`.
- Immutable design with `@immutable` and equality support.

## 0.1.1
- Minor fixes in documentation

Expand Down
107 changes: 45 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +1,68 @@
[![pub package](https://img.shields.io/pub/v/iri.svg)](https://pub.dev/packages/iri)
[![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%iri)
# IRI - Internationalized Resource Identifiers
A Dart library for parsing, validating, manipulating, and converting
Internationalized Resource Identifiers (IRIs) based on [RFC 3987](https://www.rfc-editor.org/rfc/rfc3987).
# IRI (Internationalized Resource Identifiers)

## Overview
A pure Dart implementation of Internationalized Resource Identifiers (IRI) as defined in [RFC 3987](https://datatracker.ietf.org/doc/html/rfc3987).

Internationalized Resource Identifiers (IRIs) extend the syntax of Uniform
Resource Identifiers (URIs) to support a wider range of characters from the
Universal Character Set (Unicode/ISO 10646). This is essential for representing
resource identifiers in languages that use characters outside the US-ASCII range.
This package provides an `Iri` class that acts as a Unicode-aware wrapper around Dart's native `Uri` class. It handles the mapping between IRIs and URIs, including automatic Punycode conversion for hostnames and UTF-8 percent-encoding for other components.

This package provides the `IRI` class to work with IRIs in Dart applications.
It handles the necessary conversions between IRIs and standard URIs, including:
## Features

* **Punycode encoding/decoding** for internationalized domain names (IDNs) within the host component.
* **Percent encoding/decoding** for other non-ASCII characters in various IRI components, using UTF-8 as required by the standard.
- **Standard Compliant**: Implements the IRI-to-URI mapping and URI-to-IRI conversion rules from RFC 3987.
- **Punycode Support**: Automatically converts non-ASCII hostnames to Punycode.
- **Unicode-Aware**: Access components (path, query, fragment, etc.) in their original Unicode form.
- **Normalization**: Automatically applies **NFKC** (Normalization Form KC) to all inputs as recommended by RFC 3987 to prevent comparison false-negatives.
- **IDNA Separators**: Supports international domain separators (`。`, `.`, `。`) during parsing and conversion.
- **Mailto Support**: Special handling for `mailto:` IRIs, ensuring email domain parts are correctly Punycode-encoded.
- **Familiar API**: Mirrors the Dart `Uri` class API, including `resolve`, `resolveIri`, and `replace`.
- **Immutable**: The `Iri` class is immutable and supports equality checks.

## Features
## RFC 3987 Compliance & Limitations

* **Parse IRI strings:** Create `IRI` objects from strings.
* **Validate IRIs:** Check if strings conform to RFC 3987 syntax.
* **Access Components:** Easily get decoded IRI components like `scheme`, `host`, `path`, `query`, `fragment`, `userInfo`, `port`.
* **IRI-to-URI Conversion:** Convert an `IRI` object to a standard Dart `Uri` object, applying Punycode and percent-encoding according to RFC 3987 rules.
* **URI-to-IRI Conversion:** Convert a standard `Uri` back into an `IRI`, decoding percent-encoded sequences where appropriate.
* **Normalization:** Applies syntax-based normalization including:
* Case normalization (scheme, host).
* Percent-encoding normalization (uses uppercase hex, decodes unreserved characters where possible in IRI representation).
* Path segment normalization (removes `.` and `..` segments).
* **Comparison:** Compare `IRI` objects based on their code point sequence (simple string comparison).
While this package aims for high compatibility with RFC 3987, there are known areas where the current implementation deviates from the strict specification:

## Getting Started
1. **Robust URI-to-IRI Decoding (RFC 3987 Section 3.2)**: When converting from a `Uri` to an `Iri`, the package currently uses standard UTF-8 decoding. If a percent-encoded sequence is invalid UTF-8 (e.g., `%FC`), the implementation may throw a `FormatException` instead of preserving the percent-encoding as required by the RFC.
2. **Prohibited Characters (RFC 3987 Section 4.1)**: Certain Unicode characters (like bidirectional control characters `U+202E`) are prohibited from appearing directly in an IRI. Currently, these characters are decoded if present in a URI, whereas they should remain percent-encoded.
3. **Bidi Validation (RFC 3987 Section 4.2)**: The package does not currently perform structural validation of bidirectional IRIs (e.g., ensuring RTL components don't mix directions incorrectly).

Add the package to your `pubspec.yaml`:
These areas **may** be a part of future updates. For most common use cases involving standard Unicode text in paths and hosts, the package provides a robust experience.

```yaml
dependencies:
iri: ^0.1.0
```
## Getting started

Then, import the library in your Dart code:
Add `iri` to your `pubspec.yaml` dependencies:

```dart
import 'package:iri/iri.dart';
```yaml
dependencies:
iri: ^0.2.0
```

## Usage
Here's a basic example demonstrating how to create an `IRI` and convert it to a `Uri`:

### Basic Parsing and Conversion

```dart
import 'package:iri/iri.dart';

void main() {
// 1. Create an IRI from a string containing non-ASCII characters.
// 例子 means "example" in Chinese.
// The path contains 'ȧ' (U+0227 LATIN SMALL LETTER A WITH DOT ABOVE).
final iri = IRI('https://例子.com/pȧth?q=1');

// 2. Print the original IRI string representation.
print('Original IRI: $iri');
// Output: Original IRI: https://例子.com/pȧth?q=1

// 3. Convert the IRI to its standard URI representation.
// - The host (例子.com) is converted to Punycode (xn--fsqu00a.com).
// - The non-ASCII path character 'ȧ' (UTF-8 bytes C8 A7) is percent-encoded (%C8%A7).
// Parse an IRI with Unicode characters
final iri = Iri.parse('http://résumé.example.org/résumé');

print('IRI host: ${iri.host}'); // résumé.example.org
print('IRI path: ${iri.path}'); // /résumé

// Convert to a standard URI for network operations
final uri = iri.toUri();
print('Converted URI: $uri');
// Output: Converted URI: https://xn--fsqu00a.com/p%C8%A7th?q=1
print('URI host: ${uri.host}'); // xn--rsum-bpad.example.org
print('URI string: $uri'); // http://xn--rsum-bpad.example.org/r%C3%A9sum%C3%A9
}
```

// 4. Access components (values are decoded for IRI representation).
print('Scheme: ${iri.scheme}'); // Output: Scheme: https
print('Host: ${iri.host}'); // Output: Host: 例子.com
print('Path: ${iri.path}'); // Output: Path: /pȧth
print('Query: ${iri.query}'); // Output: Query: q=1
### Creating IRIs from Components

// 5. Compare IRIs
final iri2 = IRI('https://例子.com/pȧth?q=1');
print('IRIs equal: ${iri == iri2}'); // Output: IRIs equal: true
```dart
final iri = Iri(
scheme: 'https',
host: 'münchen.test',
path: '/city',
);

final iri3 = IRI('https://example.com/');
print('IRIs equal: ${iri == iri3}'); // Output: IRIs equal: false
}
```
print(iri.toUri()); // https://xn--mnchen-3ya.test/city
```
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
70 changes: 40 additions & 30 deletions doc/api/__404error.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,41 @@
<form class="search navbar-right" role="search">
<input type="text" id="search-box" autocomplete="off" disabled class="form-control typeahead" placeholder="Loading search...">
</form>
<div class="toggle" id="theme-button" title="Toggle brightness">
<label for="theme">
<input type="checkbox" id="theme" value="light-theme">
<span id="dark-theme-button" class="material-symbols-outlined">
dark_mode
</span>
<span id="light-theme-button" class="material-symbols-outlined">
light_mode
</span>
</label>
</div>
<button class="toggle" id="theme-button" title="Toggle between light and dark mode" aria-label="Light and dark mode toggle">
<span id="dark-theme-button" class="material-symbols-outlined" aria-hidden="true">
dark_mode
</span>
<span id="light-theme-button" class="material-symbols-outlined" aria-hidden="true">
light_mode
</span>
</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 @@ -66,20 +74,22 @@ <h1>404: Something's gone wrong :-(</h1>
<li class="self-crumb">iri package</li>
</ol>

<h5><span class="package-name">iri</span> <span class="package-kind">package</span></h5>
<ol>
<h5>
<span class="package-name">iri</span>
<span class="package-kind">package</span>
</h5>
<ol>
<li class="section-title">Libraries</li>
<li><a href="iri">iri</a></li>
<li><a href="iri/">iri</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">
iri
0.1.1
0.2.0
</span>

</footer>
Expand Down
Loading