Separate out reusable stats code
diff --git a/tool/stats.dart b/tool/stats.dart
index d488dc5..8572c8d 100644
--- a/tool/stats.dart
+++ b/tool/stats.dart
@@ -2,19 +2,12 @@
 import 'dart:collection';
 import 'dart:convert';
 import 'dart:io';
-import 'dart:mirrors';
 
 import 'package:args/args.dart';
 import 'package:collection/collection.dart';
-import 'package:html/dom.dart';
-import 'package:html/parser.dart' show parseFragment;
-import 'package:markdown/markdown.dart' show markdownToHtml, ExtensionSet;
 import 'package:path/path.dart' as p;
 
-// Locate the "tool" directory. Use mirrors so that this works with the test
-// package, which loads this suite into an isolate.
-String get _currentDir => p
-    .dirname((reflect(main) as ClosureMirror).function.location.sourceUri.path);
+import 'stats_lib.dart';
 
 Future main(List<String> args) async {
   final parser = new ArgParser()
@@ -24,7 +17,7 @@
         defaultsTo: false, help: 'raw JSON format', negatable: false)
     ..addFlag('update-files',
         defaultsTo: false,
-        help: 'Update stats files in $_currentDir',
+        help: 'Update stats files in $toolDir',
         negatable: false)
     ..addFlag('verbose',
         defaultsTo: false,
@@ -35,7 +28,8 @@
         help: 'Print details for "loose" matches.',
         negatable: false)
     ..addOption('flavor',
-        allowed: ['common_mark', 'gfm'], defaultsTo: 'common_mark')
+        allowed: [Config.commonMarkConfig.prefix, Config.gfmConfig.prefix],
+        defaultsTo: Config.commonMarkConfig.prefix)
     ..addFlag('help', defaultsTo: false, negatable: false);
 
   ArgResults options;
@@ -69,14 +63,19 @@
 
   final testPrefix = options['flavor'] as String;
 
-  var baseUrl = 'http://spec.commonmark.org/0.28/';
-  ExtensionSet extensionSet;
-  if (testPrefix == 'gfm') {
-    extensionSet = ExtensionSet.gitHub;
-    baseUrl = 'https://github.github.com/gfm/';
+  Config config;
+  switch (testPrefix) {
+    case 'gfm':
+      config = Config.gfmConfig;
+      break;
+    case 'common_mark':
+      config = Config.commonMarkConfig;
+      break;
+    default:
+      throw new ArgumentError('Does not support `$testPrefix`.');
   }
 
-  var sections = _loadCommonMarkSections(testPrefix);
+  var sections = loadCommonMarkSections(testPrefix);
 
   var scores = new SplayTreeMap<String, SplayTreeMap<int, CompareLevel>>(
       compareAsciiLowerCaseNatural);
@@ -89,9 +88,8 @@
       var nestedMap = scores.putIfAbsent(
           section, () => new SplayTreeMap<int, CompareLevel>());
 
-      nestedMap[e.example] = _compareResult(
-          baseUrl, e, verbose, verboseLooseMatch,
-          extensionSet: extensionSet);
+      nestedMap[e.example] = compareResult(config, e,
+          verboseFail: verbose, verboseLooseMatch: verboseLooseMatch);
     }
   });
 
@@ -104,59 +102,6 @@
   }
 }
 
-CompareLevel _compareResult(String baseUrl, CommonMarkTestCase expected,
-    bool verboseFail, bool verboseLooseMatch,
-    {ExtensionSet extensionSet}) {
-  String output;
-  try {
-    output = markdownToHtml(expected.markdown, extensionSet: extensionSet);
-  } catch (err, stackTrace) {
-    if (verboseFail) {
-      printVerboseFailure(baseUrl, 'ERROR', expected, expected.html,
-          'Thrown: $err\n$stackTrace');
-    }
-
-    return CompareLevel.error;
-  }
-
-  if (expected.html == output) {
-    return CompareLevel.strict;
-  }
-
-  var expectedParsed = parseFragment(expected.html);
-  var actual = parseFragment(output);
-
-  var looseMatch = _compareHtml(expectedParsed.children, actual.children);
-
-  if (!looseMatch && verboseFail) {
-    printVerboseFailure(
-        baseUrl, 'FAIL', expected, expectedParsed.outerHtml, actual.outerHtml);
-  }
-
-  if (looseMatch && verboseLooseMatch) {
-    printVerboseFailure(baseUrl, 'LOOSE', expected, output, actual.outerHtml);
-  }
-
-  return looseMatch ? CompareLevel.loose : CompareLevel.fail;
-}
-
-String indent(String s) => s.splitMapJoin('\n', onNonMatch: (n) => '    $n');
-
-void printVerboseFailure(String baseUrl, String message,
-    CommonMarkTestCase test, String expected, String actual) {
-  print('$message: $baseUrl#example-${test.example} '
-      '@ ${test.section}');
-  print('input:');
-  print(indent(test.markdown));
-  print('expected:');
-  print(indent(expected));
-  print('actual:');
-  print(indent(actual));
-  print('-----------------------');
-}
-
-enum CompareLevel { strict, loose, fail, error }
-
 Object _convert(obj) {
   if (obj is CompareLevel) {
     switch (obj) {
@@ -186,9 +131,8 @@
 Future _printRaw(String testPrefix, Map scores, bool updateFiles) async {
   IOSink sink;
   if (updateFiles) {
-    var path = p.join(_currentDir, '${testPrefix}_stats.json');
-    print('Updating $path');
-    var file = new File(path);
+    var file = getStatsFile(testPrefix);
+    print('Updating ${file.path}');
     sink = file.openWrite();
   } else {
     sink = stdout;
@@ -218,7 +162,7 @@
 
   IOSink sink;
   if (updateFiles) {
-    var path = p.join(_currentDir, '${testPrefix}_stats.txt');
+    var path = p.join(toolDir, '${testPrefix}_stats.txt');
     print('Updating $path');
     var file = new File(path);
     sink = file.openWrite();
@@ -256,99 +200,3 @@
   await sink.flush();
   await sink.close();
 }
-
-/// Compare two DOM trees for equality.
-bool _compareHtml(
-    List<Element> expectedElements, List<Element> actualElements) {
-  if (expectedElements.length != actualElements.length) {
-    return false;
-  }
-
-  for (var childNum = 0; childNum < expectedElements.length; childNum++) {
-    var expected = expectedElements[childNum];
-    var actual = actualElements[childNum];
-
-    if (expected.runtimeType != actual.runtimeType) {
-      return false;
-    }
-
-    if (expected.localName != actual.localName) {
-      return false;
-    }
-
-    if (expected.attributes.length != actual.attributes.length) {
-      return false;
-    }
-
-    var expectedAttrKeys = expected.attributes.keys.toList();
-    expectedAttrKeys.sort();
-
-    var actualAttrKeys = actual.attributes.keys.toList();
-    actualAttrKeys.sort();
-
-    for (var attrNum = 0; attrNum < actualAttrKeys.length; attrNum++) {
-      var expectedAttrKey = expectedAttrKeys[attrNum];
-      var actualAttrKey = actualAttrKeys[attrNum];
-
-      if (expectedAttrKey != actualAttrKey) {
-        return false;
-      }
-
-      if (expected.attributes[expectedAttrKey] !=
-          actual.attributes[actualAttrKey]) {
-        return false;
-      }
-    }
-
-    var childrenEqual = _compareHtml(expected.children, actual.children);
-
-    if (!childrenEqual) {
-      return false;
-    }
-  }
-
-  return true;
-}
-
-Map<String, List<CommonMarkTestCase>> _loadCommonMarkSections(
-    String testPrefix) {
-  var testFile = new File(p.join(_currentDir, '${testPrefix}_tests.json'));
-  var testsJson = testFile.readAsStringSync();
-
-  var testArray = JSON.decode(testsJson) as List<Map<String, dynamic>>;
-
-  var sections = new Map<String, List<CommonMarkTestCase>>();
-
-  for (var exampleMap in testArray) {
-    var exampleTest = new CommonMarkTestCase.fromJson(exampleMap);
-
-    var sectionList =
-        sections.putIfAbsent(exampleTest.section, () => <CommonMarkTestCase>[]);
-
-    sectionList.add(exampleTest);
-  }
-
-  return sections;
-}
-
-class CommonMarkTestCase {
-  final String markdown;
-  final String section;
-  final int example;
-  final String html;
-  final int startLine;
-  final int endLine;
-
-  CommonMarkTestCase(this.example, this.section, this.startLine, this.endLine,
-      this.markdown, this.html);
-
-  factory CommonMarkTestCase.fromJson(Map<String, dynamic> json) {
-    return new CommonMarkTestCase(
-        json['example'] as int,
-        json['section'] as String,
-        json['start_line'] as int,
-        json['end_line'] as int,
-        json['markdown'] as String,
-        json['html'] as String);
-  }
-}
diff --git a/tool/stats_lib.dart b/tool/stats_lib.dart
new file mode 100644
index 0000000..8d06e79
--- /dev/null
+++ b/tool/stats_lib.dart
@@ -0,0 +1,189 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:mirrors';
+
+import 'package:path/path.dart' as p;
+
+import 'package:html/dom.dart';
+import 'package:html/parser.dart' show parseFragment;
+import 'package:markdown/markdown.dart' show markdownToHtml, ExtensionSet;
+
+// Locate the "tool" directory. Use mirrors so that this works with the test
+// package, which loads this suite into an isolate.
+String get toolDir =>
+    p.dirname((reflect(loadCommonMarkSections) as ClosureMirror)
+        .function
+        .location
+        .sourceUri
+        .path);
+
+File getStatsFile(String prefix) =>
+    new File(p.join(toolDir, '${prefix}_stats.json'));
+
+Map<String, List<CommonMarkTestCase>> loadCommonMarkSections(
+    String testPrefix) {
+  var testFile = new File(p.join(toolDir, '${testPrefix}_tests.json'));
+  var testsJson = testFile.readAsStringSync();
+
+  var testArray = JSON.decode(testsJson) as List<Map<String, dynamic>>;
+
+  var sections = new Map<String, List<CommonMarkTestCase>>();
+
+  for (var exampleMap in testArray) {
+    var exampleTest = new CommonMarkTestCase.fromJson(exampleMap);
+
+    var sectionList =
+        sections.putIfAbsent(exampleTest.section, () => <CommonMarkTestCase>[]);
+
+    sectionList.add(exampleTest);
+  }
+
+  return sections;
+}
+
+class Config {
+  static final Config commonMarkConfig =
+      new Config._('common_mark', 'http://spec.commonmark.org/0.28/', null);
+  static final Config gfmConfig = new Config._(
+      'gfm', 'https://github.github.com/gfm/', ExtensionSet.gitHub);
+
+  final String prefix;
+  final String baseUrl;
+  final ExtensionSet extensionSet;
+
+  Config._(this.prefix, this.baseUrl, this.extensionSet);
+}
+
+class CommonMarkTestCase {
+  final String markdown;
+  final String section;
+  final int example;
+  final String html;
+  final int startLine;
+  final int endLine;
+
+  CommonMarkTestCase(this.example, this.section, this.startLine, this.endLine,
+      this.markdown, this.html);
+
+  factory CommonMarkTestCase.fromJson(Map<String, dynamic> json) {
+    return new CommonMarkTestCase(
+        json['example'] as int,
+        json['section'] as String,
+        json['start_line'] as int,
+        json['end_line'] as int,
+        json['markdown'] as String,
+        json['html'] as String);
+  }
+}
+
+enum CompareLevel { strict, loose, fail, error }
+
+CompareLevel compareResult(Config config, CommonMarkTestCase expected,
+    {bool throwOnError: false,
+    bool verboseFail: false,
+    bool verboseLooseMatch: false}) {
+  String output;
+  try {
+    output =
+        markdownToHtml(expected.markdown, extensionSet: config.extensionSet);
+  } catch (err, stackTrace) {
+    if (throwOnError) {
+      rethrow;
+    }
+    if (verboseFail) {
+      _printVerboseFailure(config.baseUrl, 'ERROR', expected, expected.html,
+          'Thrown: $err\n$stackTrace');
+    }
+
+    return CompareLevel.error;
+  }
+
+  if (expected.html == output) {
+    return CompareLevel.strict;
+  }
+
+  var expectedParsed = parseFragment(expected.html);
+  var actual = parseFragment(output);
+
+  var looseMatch = _compareHtml(expectedParsed.children, actual.children);
+
+  if (!looseMatch && verboseFail) {
+    _printVerboseFailure(config.baseUrl, 'FAIL', expected,
+        expectedParsed.outerHtml, actual.outerHtml);
+  }
+
+  if (looseMatch && verboseLooseMatch) {
+    _printVerboseFailure(
+        config.baseUrl, 'LOOSE', expected, output, actual.outerHtml);
+  }
+
+  return looseMatch ? CompareLevel.loose : CompareLevel.fail;
+}
+
+String _indent(String s) => s.splitMapJoin('\n', onNonMatch: (n) => '    $n');
+
+void _printVerboseFailure(String baseUrl, String message,
+    CommonMarkTestCase test, String expected, String actual) {
+  print('$message: $baseUrl#example-${test.example} '
+      '@ ${test.section}');
+  print('input:');
+  print(_indent(test.markdown));
+  print('expected:');
+  print(_indent(expected));
+  print('actual:');
+  print(_indent(actual));
+  print('-----------------------');
+}
+
+/// Compare two DOM trees for equality.
+bool _compareHtml(
+    List<Element> expectedElements, List<Element> actualElements) {
+  if (expectedElements.length != actualElements.length) {
+    return false;
+  }
+
+  for (var childNum = 0; childNum < expectedElements.length; childNum++) {
+    var expected = expectedElements[childNum];
+    var actual = actualElements[childNum];
+
+    if (expected.runtimeType != actual.runtimeType) {
+      return false;
+    }
+
+    if (expected.localName != actual.localName) {
+      return false;
+    }
+
+    if (expected.attributes.length != actual.attributes.length) {
+      return false;
+    }
+
+    var expectedAttrKeys = expected.attributes.keys.toList();
+    expectedAttrKeys.sort();
+
+    var actualAttrKeys = actual.attributes.keys.toList();
+    actualAttrKeys.sort();
+
+    for (var attrNum = 0; attrNum < actualAttrKeys.length; attrNum++) {
+      var expectedAttrKey = expectedAttrKeys[attrNum];
+      var actualAttrKey = actualAttrKeys[attrNum];
+
+      if (expectedAttrKey != actualAttrKey) {
+        return false;
+      }
+
+      if (expected.attributes[expectedAttrKey] !=
+          actual.attributes[actualAttrKey]) {
+        return false;
+      }
+    }
+
+    var childrenEqual = _compareHtml(expected.children, actual.children);
+
+    if (!childrenEqual) {
+      return false;
+    }
+  }
+
+  return true;
+}