resolveIri function

String resolveIri(
  1. String iri,
  2. String? baseIri
)

Converts a relative IRI to absolute form using a base IRI.

Takes a potentially relative iri and resolves it against baseIri to produce an absolute IRI. This is the inverse operation of relativizeIri.

If iri is already absolute (contains a scheme like 'http:'), it's returned unchanged regardless of baseIri.

Throws BaseIriRequiredException if iri is relative but baseIri is null or empty.

Examples:

resolveIri('file.txt', 'http://example.org/path/')
// Returns: 'http://example.org/path/file.txt'

resolveIri('#section', 'http://example.org/document')
// Returns: 'http://example.org/document#section'

resolveIri('http://other.org/file', 'http://example.org/')
// Returns: 'http://other.org/file' (unchanged - already absolute)

The function uses Dart's built-in Uri.resolveUri when possible, falling back to manual resolution for edge cases.

Implementation

String resolveIri(String iri, String? baseIri) {
  if (_isAbsoluteUri(iri)) {
    return iri;
  }

  if (baseIri == null || baseIri.isEmpty) {
    throw BaseIriRequiredException(relativeUri: iri);
  }

  // For query/fragment-only and empty references, RFC 3986 keeps the base path
  // as-is (no dot-segment normalization).
  if (iri.isEmpty || iri.startsWith('?') || iri.startsWith('#')) {
    return _resolveSimpleReferencePreservingBasePath(iri, baseIri);
  }

  if (_baseEndsWithDotSegment(baseIri) && _isRelativePathReference(iri)) {
    return _resolveRelativePathReferenceAgainstRawBase(iri, baseIri);
  }

  // Reject IRIs with empty scheme (e.g. "://invalid") — these are invalid
  // per RFC 3986 as both absolute URIs and relative references.
  if (iri.startsWith('://')) {
    throw FormatException('Invalid empty scheme', iri, 0);
  }

  try {
    final base = Uri.parse(baseIri);
    // Pre-process: Dart's Uri.parse treats `.?q` and `.#f` as a single path
    // segment instead of recognizing `?`/`#` as delimiters after `.`.
    // Fix by resolving `.` separately and reattaching query/fragment.
    final dotRef = _extractDotReference(iri);
    if (dotRef != null) {
      final resolvedDot = base.resolveUri(Uri.parse(dotRef.path));
      return '$resolvedDot${dotRef.suffix}';
    }
    final resolved = base.resolveUri(Uri.parse(iri));
    var result = resolved.toString();
    // Post-process: Dart adds a trailing `/` to authority-only references
    // (`//auth`) for file: scheme. RFC 3986 says the path should be empty.
    if (iri.startsWith('//') && !iri.contains('/./') && !iri.endsWith('/')) {
      final afterAuth = iri.substring(2);
      // Pure authority reference: no path component after authority
      if (!afterAuth.contains('/')) {
        result = result.replaceFirst(RegExp(r'/$'), '');
      }
    }
    return result;
  } catch (e) {
    // Fall back to manual resolution if URI parsing fails
    return _manualResolveUri(iri, baseIri);
  }
}