Add support for multiple extensions (#69)

Closes #13
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0804f02..7564f60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.7.0
+
+* Add support for multiple extension in `context.extension()`.
+
 ## 1.6.4
 
 * Fixed a number of lints that affect the package health score.
diff --git a/lib/path.dart b/lib/path.dart
index a501e0c..1cecf66 100644
--- a/lib/path.dart
+++ b/lib/path.dart
@@ -194,7 +194,18 @@
 ///
 ///     p.extension('~/.bashrc');    // -> ''
 ///     p.extension('~/.notes.txt'); // -> '.txt'
-String extension(String path) => context.extension(path);
+///
+/// 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 full extension is returned. The value of `level` must
+/// be greater than 0, else `RangeError` is thrown.
+///
+///     p.extension('foo.bar.dart.js', 2);   // -> '.dart.js
+///     p.extension('foo.bar.dart.js', 3);   // -> '.bar.dart.js'
+///     p.extension('foo.bar.dart.js', 10);  // -> '.bar.dart.js'
+///     p.extension('path/to/foo.bar.dart.js', 2);  // -> '.dart.js'
+String extension(String path, [int level = 1]) =>
+    context.extension(path, level);
 
 // TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
 /// Returns the root of [path], if it's absolute, or the empty string if it's
diff --git a/lib/src/context.dart b/lib/src/context.dart
index 9e58e4a..3a99804 100644
--- a/lib/src/context.dart
+++ b/lib/src/context.dart
@@ -145,7 +145,18 @@
   ///
   ///     context.extension('~/.bashrc');    // -> ''
   ///     context.extension('~/.notes.txt'); // -> '.txt'
-  String extension(String path) => _parse(path).extension;
+  ///
+  /// 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 full extension is returned. The value of `level` must
+  /// be greater than 0, else `RangeError` is thrown.
+  ///
+  ///     context.extension('foo.bar.dart.js', 2);   // -> '.dart.js
+  ///     context.extension('foo.bar.dart.js', 3);   // -> '.bar.dart.js'
+  ///     context.extension('foo.bar.dart.js', 10);  // -> '.bar.dart.js'
+  ///     context.extension('path/to/foo.bar.dart.js', 2);  // -> '.dart.js'
+  String extension(String path, [int level = 1]) =>
+      _parse(path).extension(level);
 
   // TODO(nweiz): add a UNC example for Windows once issue 7323 is fixed.
   /// Returns the root of [path] if it's absolute, or an empty string if it's
diff --git a/lib/src/parsed_path.dart b/lib/src/parsed_path.dart
index 9efd529..a9d443a 100644
--- a/lib/src/parsed_path.dart
+++ b/lib/src/parsed_path.dart
@@ -33,7 +33,7 @@
 
   /// The file extension of the last non-empty part, or "" if it doesn't have
   /// one.
-  String get extension => _splitExtension()[1];
+  String extension([int level]) => _splitExtension(level)[1];
 
   /// `true` if this is an absolute path.
   bool get isAbsolute => root != null;
@@ -161,18 +161,47 @@
     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() {
+  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 = file.lastIndexOf('.');
+    final lastDot = _kthLastIndexOf(file, '.', level);
 
     // If there is no dot, or it's the first character, like '.bashrc', it
     // doesn't count.
diff --git a/test/posix_test.dart b/test/posix_test.dart
index bd6c3e0..5138086 100644
--- a/test/posix_test.dart
+++ b/test/posix_test.dart
@@ -26,6 +26,14 @@
     expect(context.extension(r'a.b\c'), r'.b\c');
     expect(context.extension('foo.dart/'), '.dart');
     expect(context.extension('foo.dart//'), '.dart');
+    expect(context.extension('foo.bar.dart.js', 2), '.dart.js');
+    expect(context.extension(r'foo.bar.dart.js', 3), '.bar.dart.js');
+    expect(context.extension(r'foo.bar.dart.js', 10), '.bar.dart.js');
+    expect(context.extension('a.b/c.d', 2), '.d');
+    expect(() => context.extension(r'foo.bar.dart.js', 0), throwsRangeError);
+    expect(() => context.extension(r'foo.bar.dart.js', -1), throwsRangeError);
+    expect(
+        () => context.extension(r'foo.bar.dart.js', null), throwsArgumentError);
   });
 
   test('rootPrefix', () {
diff --git a/test/windows_test.dart b/test/windows_test.dart
index 2d6b90f..5710f06 100644
--- a/test/windows_test.dart
+++ b/test/windows_test.dart
@@ -29,6 +29,15 @@
     expect(context.extension(r'a.b/c'), r'');
     expect(context.extension(r'foo.dart\'), '.dart');
     expect(context.extension(r'foo.dart\\'), '.dart');
+    expect(context.extension('a.b/..', 2), '');
+    expect(context.extension('foo.bar.dart.js', 2), '.dart.js');
+    expect(context.extension(r'foo.bar.dart.js', 3), '.bar.dart.js');
+    expect(context.extension(r'foo.bar.dart.js', 10), '.bar.dart.js');
+    expect(context.extension('a.b/c.d', 2), '.d');
+    expect(() => context.extension(r'foo.bar.dart.js', 0), throwsRangeError);
+    expect(() => context.extension(r'foo.bar.dart.js', -1), throwsRangeError);
+    expect(
+        () => context.extension(r'foo.bar.dart.js', null), throwsArgumentError);
   });
 
   test('rootPrefix', () {