relative method

String relative(
  1. String path, {
  2. String? from,
})

Attempts to convert path to an equivalent relative path relative to current.

var context = Context(current: '/root/path');
context.relative('/root/path/a/b.dart'); // -> 'a/b.dart'
context.relative('/root/other.dart'); // -> '../other.dart'

If the from argument is passed, path is made relative to that instead.

context.relative('/root/path/a/b.dart',
    from: '/root/path'); // -> 'a/b.dart'
context.relative('/root/other.dart',
    from: '/root/path'); // -> '../other.dart'

If path and/or from are relative paths, they are assumed to be relative to current.

Since there is no relative path from one drive letter to another on Windows, this will return an absolute path in that case.

context.relative(r'D:\other', from: r'C:\other'); // -> 'D:\other'

This will also return an absolute path if an absolute path is passed to a context with a relative path for current.

var context = Context(r'some/relative/path');
context.relative(r'/absolute/path'); // -> '/absolute/path'

If current is relative, it may be impossible to determine a path from from to path. For example, if current and path are "." and from is "/", no path can be determined. In this case, a PathException will be thrown.

Implementation

String relative(String path, {String? from}) {
  // Avoid expensive computation if the path is already relative.
  if (from == null && isRelative(path)) return normalize(path);

  from = from == null ? current : absolute(from);

  // We can't determine the path from a relative path to an absolute path.
  if (isRelative(from) && isAbsolute(path)) {
    return normalize(path);
  }

  // If the given path is relative, resolve it relative to the context's
  // current directory.
  if (isRelative(path) || isRootRelative(path)) {
    path = absolute(path);
  }

  // If the path is still relative and `from` is absolute, we're unable to
  // find a path from `from` to `path`.
  if (isRelative(path) && isAbsolute(from)) {
    throw PathException('Unable to find a path to "$path" from "$from".');
  }

  final fromParsed = _parse(from)..normalize();
  final pathParsed = _parse(path)..normalize();

  if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '.') {
    return pathParsed.toString();
  }

  // If the root prefixes don't match (for example, different drive letters
  // on Windows), then there is no relative path, so just return the absolute
  // one. In Windows, drive letters are case-insenstive and we allow
  // calculation of relative paths, even if a path has not been normalized.
  if (fromParsed.root != pathParsed.root &&
      ((fromParsed.root == null || pathParsed.root == null) ||
          !style.pathsEqual(fromParsed.root!, pathParsed.root!))) {
    return pathParsed.toString();
  }

  // Strip off their common prefix.
  while (fromParsed.parts.isNotEmpty &&
      pathParsed.parts.isNotEmpty &&
      style.pathsEqual(fromParsed.parts[0], pathParsed.parts[0])) {
    fromParsed.parts.removeAt(0);
    fromParsed.separators.removeAt(1);
    pathParsed.parts.removeAt(0);
    pathParsed.separators.removeAt(1);
  }

  // If there are any directories left in the from path, we need to walk up
  // out of them. If a directory left in the from path is '..', it cannot
  // be cancelled by adding a '..'.
  if (fromParsed.parts.isNotEmpty && fromParsed.parts[0] == '..') {
    throw PathException('Unable to find a path to "$path" from "$from".');
  }
  pathParsed.parts.insertAll(0, List.filled(fromParsed.parts.length, '..'));
  pathParsed.separators[0] = '';
  pathParsed.separators
      .insertAll(1, List.filled(fromParsed.parts.length, style.separator));

  // Corner case: the paths completely collapsed.
  if (pathParsed.parts.isEmpty) return '.';

  // Corner case: path was '.' and some '..' directories were added in front.
  // Don't add a final '/.' in that case.
  if (pathParsed.parts.length > 1 && pathParsed.parts.last == '.') {
    pathParsed.parts.removeLast();
    pathParsed.separators
      ..removeLast()
      ..removeLast()
      ..add('');
  }

  // Make it relative.
  pathParsed.root = '';
  pathParsed.removeTrailingSeparators();

  return pathParsed.toString();
}