| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'internal_style.dart'; |
| import 'style.dart'; |
| |
| class ParsedPath { |
| /// The [InternalStyle] that was used to parse this path. |
| InternalStyle style; |
| |
| /// The absolute root portion of the path, or `null` if the path is relative. |
| /// On POSIX systems, this will be `null` or "/". On Windows, it can be |
| /// `null`, "//" for a UNC path, or something like "C:\" for paths with drive |
| /// letters. |
| String root; |
| |
| /// Whether this path is root-relative. |
| /// |
| /// See [Context.isRootRelative]. |
| bool isRootRelative; |
| |
| /// The path-separated parts of the path. All but the last will be |
| /// directories. |
| List<String> parts; |
| |
| /// The path separators preceding each part. |
| /// |
| /// The first one will be an empty string unless the root requires a separator |
| /// between it and the path. The last one will be an empty string unless the |
| /// path ends with a trailing separator. |
| List<String> separators; |
| |
| /// The file extension of the last non-empty part, or "" if it doesn't have |
| /// one. |
| String extension([int level]) => _splitExtension(level)[1]; |
| |
| /// `true` if this is an absolute path. |
| bool get isAbsolute => root != null; |
| |
| factory ParsedPath.parse(String path, InternalStyle style) { |
| // Remove the root prefix, if any. |
| final root = style.getRoot(path); |
| final isRootRelative = style.isRootRelative(path); |
| if (root != null) path = path.substring(root.length); |
| |
| // Split the parts on path separators. |
| final parts = <String>[]; |
| final separators = <String>[]; |
| |
| var start = 0; |
| |
| if (path.isNotEmpty && style.isSeparator(path.codeUnitAt(0))) { |
| separators.add(path[0]); |
| start = 1; |
| } else { |
| separators.add(''); |
| } |
| |
| for (var i = start; i < path.length; i++) { |
| if (style.isSeparator(path.codeUnitAt(i))) { |
| parts.add(path.substring(start, i)); |
| separators.add(path[i]); |
| start = i + 1; |
| } |
| } |
| |
| // Add the final part, if any. |
| if (start < path.length) { |
| parts.add(path.substring(start)); |
| separators.add(''); |
| } |
| |
| return ParsedPath._(style, root, isRootRelative, parts, separators); |
| } |
| |
| ParsedPath._( |
| this.style, this.root, this.isRootRelative, this.parts, this.separators); |
| |
| String get basename { |
| final copy = clone(); |
| copy.removeTrailingSeparators(); |
| if (copy.parts.isEmpty) return root ?? ''; |
| return copy.parts.last; |
| } |
| |
| String get basenameWithoutExtension => _splitExtension()[0]; |
| |
| bool get hasTrailingSeparator => |
| parts.isNotEmpty && (parts.last == '' || separators.last != ''); |
| |
| void removeTrailingSeparators() { |
| while (parts.isNotEmpty && parts.last == '') { |
| parts.removeLast(); |
| separators.removeLast(); |
| } |
| if (separators.isNotEmpty) separators[separators.length - 1] = ''; |
| } |
| |
| void normalize({bool canonicalize = false}) { |
| // Handle '.', '..', and empty parts. |
| var leadingDoubles = 0; |
| final newParts = <String>[]; |
| for (var part in parts) { |
| if (part == '.' || part == '') { |
| // Do nothing. Ignore it. |
| } else if (part == '..') { |
| // Pop the last part off. |
| if (newParts.isNotEmpty) { |
| newParts.removeLast(); |
| } else { |
| // Backed out past the beginning, so preserve the "..". |
| leadingDoubles++; |
| } |
| } else { |
| newParts.add(canonicalize ? style.canonicalizePart(part) : part); |
| } |
| } |
| |
| // A relative path can back out from the start directory. |
| if (!isAbsolute) { |
| newParts.insertAll(0, List.filled(leadingDoubles, '..')); |
| } |
| |
| // If we collapsed down to nothing, do ".". |
| if (newParts.isEmpty && !isAbsolute) { |
| newParts.add('.'); |
| } |
| |
| // Canonicalize separators. |
| final newSeparators = List<String>.generate( |
| newParts.length, (_) => style.separator, |
| growable: true); |
| newSeparators.insert( |
| 0, |
| isAbsolute && newParts.isNotEmpty && style.needsSeparator(root) |
| ? style.separator |
| : ''); |
| |
| parts = newParts; |
| separators = newSeparators; |
| |
| // Normalize the Windows root if needed. |
| if (root != null && style == Style.windows) { |
| if (canonicalize) root = root.toLowerCase(); |
| root = root.replaceAll('/', '\\'); |
| } |
| removeTrailingSeparators(); |
| } |
| |
| @override |
| String toString() { |
| final builder = StringBuffer(); |
| if (root != null) builder.write(root); |
| for (var i = 0; i < parts.length; i++) { |
| builder.write(separators[i]); |
| builder.write(parts[i]); |
| } |
| builder.write(separators.last); |
| |
| return builder.toString(); |
| } |
| |
| /// Returns k-th last index of the `character` in the `path`. |
| /// |
| /// If `k` exceeds the count of `character`s in `path`, the left most index |
| /// of the `character` is returned. |
| int _kthLastIndexOf(String path, String character, int k) { |
| var count = 0, leftMostIndexedCharacter = 0; |
| for (var index = path.length - 1; index >= 0; --index) { |
| if (path[index] == character) { |
| leftMostIndexedCharacter = index; |
| ++count; |
| if (count == k) { |
| return index; |
| } |
| } |
| } |
| return leftMostIndexedCharacter; |
| } |
| |
| /// Splits the last non-empty part of the path into a `[basename, extension`] |
| /// pair. |
| /// |
| /// Takes an optional parameter `level` which makes possible to return |
| /// multiple extensions having `level` number of dots. If `level` exceeds the |
| /// number of dots, the path is splitted into the left most dot. The value of |
| /// `level` must be greater than 0, else `RangeError` is thrown. |
| /// |
| /// Returns a two-element list. The first is the name of the file without any |
| /// extension. The second is the extension or "" if it has none. |
| List<String> _splitExtension([int level = 1]) { |
| if (level == null) throw ArgumentError.notNull('level'); |
| if (level <= 0) { |
| throw RangeError.value( |
| level, 'level', "level's value must be greater than 0"); |
| } |
| |
| final file = parts.lastWhere((p) => p != '', orElse: () => null); |
| |
| if (file == null) return ['', '']; |
| if (file == '..') return ['..', '']; |
| |
| final lastDot = _kthLastIndexOf(file, '.', level); |
| |
| // If there is no dot, or it's the first character, like '.bashrc', it |
| // doesn't count. |
| if (lastDot <= 0) return [file, '']; |
| |
| return [file.substring(0, lastDot), file.substring(lastDot)]; |
| } |
| |
| ParsedPath clone() => ParsedPath._( |
| style, root, isRootRelative, List.from(parts), List.from(separators)); |
| } |