about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--config/default.toml2
-rw-r--r--package.json3
-rw-r--r--src/actions.js22
-rw-r--r--src/app.js50
-rw-r--r--src/domain/posts.js42
-rw-r--r--src/index.js14
-rw-r--r--src/modules/markdown.js24
-rw-r--r--src/templates/layout.html1
-rw-r--r--test/app.test.js22
-rw-r--r--test/domain/posts.test.js28
-rw-r--r--test/snapshots/app.test.js.md46
-rw-r--r--test/snapshots/app.test.js.snapbin1236 -> 1550 bytes
-rw-r--r--test/testsite/posts/testfile.md11
-rw-r--r--yarn.lock122
14 files changed, 312 insertions, 75 deletions
diff --git a/config/default.toml b/config/default.toml
index 3b6ceb6..bb07508 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -18,3 +18,5 @@ folder = "./posts"
 tag = "tags"
 category = "categories"
 
+[posts.code]
+theme = "default"
\ No newline at end of file
diff --git a/package.json b/package.json
index d324fdf..517687f 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,8 @@
     "case": "^1.5.2",
     "configly": "^4.1.0",
     "gray-matter": "^2.1.1",
+    "highland": "^2.11.0",
+    "highlight.js": "^9.12.0",
     "hyperfast": "^2.1.0",
     "indent-string": "^3.1.0",
     "koa": "^2.2.0",
@@ -55,6 +57,7 @@
     "koa-router": "^7.2.1",
     "koa-send": "^4.1.0",
     "markdown-it": "^8.3.1",
+    "predentation": "alanpearce/predentation#fix-code-class",
     "toml": "^2.3.2"
   }
 }
diff --git a/src/actions.js b/src/actions.js
index 8a04671..7c7482f 100644
--- a/src/actions.js
+++ b/src/actions.js
@@ -1,5 +1,7 @@
 "use strict";
 
+const fs = require("fs");
+const path = require("path");
 const send = require("koa-send");
 const responders = require("./responders");
 
@@ -10,6 +12,25 @@ function home(config, posts) {
   };
 }
 
+function highlightTheme(config) {
+  const theme = config.posts.code.theme;
+  const themeFile = path.resolve(
+    __dirname,
+    `../node_modules/highlight.js/styles/${theme}.css`
+  );
+
+  if (!fs.existsSync(themeFile)) {
+    throw new Error(`Couldn't find highlight theme ${theme}`);
+  }
+
+  const css = fs.readFileSync(themeFile, "utf-8");
+
+  return async function(ctx, next) {
+    ctx.type = "css";
+    ctx.body = css;
+  };
+}
+
 function post(config, posts) {
   return async function(ctx, next) {
     ctx.assert(posts.has(ctx.params.filename), 404, "Post not found");
@@ -41,6 +62,7 @@ async function serveFiles(ctx) {
 
 module.exports = {
   home,
+  highlightTheme,
   post,
   taxonGenerator,
   serveFiles
diff --git a/src/app.js b/src/app.js
index 1a0c5cb..6be44bd 100644
--- a/src/app.js
+++ b/src/app.js
@@ -14,32 +14,40 @@ const router = new Router();
 
 app.context.getURL = router.url.bind(router);
 
-const Posts = require("./domain/posts.js")(config.posts, basename =>
-  router.url("post", basename)
-);
-
-router.get("home", "/", actions.home(config, Posts.posts));
+module.exports = async function() {
+  const Posts = await require("./domain/posts.js")(config.posts, basename =>
+    router.url("post", basename)
+  );
 
-router.get("post", "/post/:filename", actions.post(config, Posts.posts));
+  router.get("home", "/", actions.home(config, Posts.posts));
 
-for (let [term, items] of Posts.taxonomies) {
   router.get(
-    `taxon-${term}`,
-    `/${term}/:value`,
-    actions.taxonGenerator(config, term, items)
+    "highlight-theme",
+    "/css/code.css",
+    actions.highlightTheme(config)
   );
-}
 
-app.use(
-  helmet({
-    hsts: {
-      setIf: ctx => ctx.secure
-    }
-  })
-);
+  router.get("post", "/post/:filename", actions.post(config, Posts.posts));
+
+  for (let [term, items] of Posts.taxonomies) {
+    router.get(
+      `taxon-${term}`,
+      `/${term}/:value`,
+      actions.taxonGenerator(config, term, items)
+    );
+  }
+
+  app.use(
+    helmet({
+      hsts: {
+        setIf: ctx => ctx.secure
+      }
+    })
+  );
 
-app.use(router.routes()).use(router.allowedMethods());
+  app.use(router.routes()).use(router.allowedMethods());
 
-app.use(actions.serveFiles);
+  app.use(actions.serveFiles);
 
-module.exports = app;
+  return app;
+};
diff --git a/src/domain/posts.js b/src/domain/posts.js
index fd4fb3d..98488ba 100644
--- a/src/domain/posts.js
+++ b/src/domain/posts.js
@@ -1,9 +1,12 @@
 "use strict";
 
+const h = require("highland");
 const fs = require("fs");
 const path = require("path");
+const { promisify } = require("util");
 const matter = require("gray-matter");
 const markdown = require("../modules/markdown.js");
+const predentation = require("predentation");
 const { indentForTemplate, postIndentLevel } = require("../responders.js");
 
 const grayMatterOptions = {
@@ -30,26 +33,35 @@ function getTitle(file) {
   return path.basename(file.path, path.extname(file.path));
 }
 
-function get(getURL, filename) {
+async function indentBody(content) {
+  return await h(
+    h
+      .of(content)
+      .map(markdown)
+      .map(html => indentForTemplate(html, postIndentLevel))
+      .pipe(predentation())
+  )
+    .invoke("toString", ["utf-8"])
+    .toPromise(Promise);
+}
+
+async function get(getURL, filename) {
   const fileMatter = matter.read(filename, grayMatterOptions);
   fileMatter.basename = getTitle(fileMatter);
   delete fileMatter.orig;
-  fileMatter.body = indentForTemplate(
-    markdown(fileMatter.content),
-    postIndentLevel
-  );
+  fileMatter.body = await indentBody(fileMatter.content);
   fileMatter.url = getURL(fileMatter.basename);
-  return canonicaliseMetadata(fileMatter);
+  return Promise.resolve(canonicaliseMetadata(fileMatter));
 }
 
-function getFolder(folder, getURL) {
-  return new Map(
-    fs
-      .readdirSync(folder)
-      .map(f => path.resolve(folder, f))
-      .map(get.bind(this, getURL))
-      .map(f => [getTitle(f), f])
+async function getFolder(folder, getURL) {
+  const files = (await promisify(fs.readdir)(folder)).map(f =>
+    path.resolve(folder, f)
   );
+
+  const posts = await Promise.all(files.map(get.bind(this, getURL)));
+
+  return new Map(posts.map(f => [getTitle(f), f]));
 }
 
 function taxonomise(taxonomies, posts) {
@@ -71,8 +83,8 @@ function taxonomise(taxonomies, posts) {
   return taxons;
 }
 
-module.exports = function(config, getURL) {
-  const posts = getFolder(config.folder, getURL);
+module.exports = async function(config, getURL) {
+  const posts = await getFolder(config.folder, getURL);
   const taxonomies = taxonomise(config.taxonomies, posts);
   return {
     posts,
diff --git a/src/index.js b/src/index.js
index 2c75554..b7bc1a3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -11,8 +11,12 @@ if (targetDir) {
 
 const app = require("./app.js");
 
-module.exports = app;
-
-app.listen(PORT, () => {
-  console.log(`App listening on port ${PORT}`);
-});
+(async function() {
+  try {
+    (await app()).listen(PORT, () => {
+      console.log(`App listening on port ${PORT}`);
+    });
+  } catch (error) {
+    console.error("App startup error", error);
+  }
+})();
diff --git a/src/modules/markdown.js b/src/modules/markdown.js
index 9f0af45..6a0ec9d 100644
--- a/src/modules/markdown.js
+++ b/src/modules/markdown.js
@@ -1,12 +1,24 @@
-'use strict'
+"use strict";
 
-const Markdown = require('markdown-it')
+const highlight = require("highlight.js");
+const Markdown = require("markdown-it");
 
 const markdownOptions = {
   html: true,
-  typographer: true
-}
+  typographer: true,
+  highlight: function(str, lang) {
+    if (lang && highlight.getLanguage(lang)) {
+      try {
+        return `
+${highlight.highlight(lang, str).value}`;
+      } catch (error) {
+        console.error("highlighting failed", error);
+      }
+    }
+    return "";
+  }
+};
 
-const markdown = new Markdown(markdownOptions)
+const markdown = new Markdown(markdownOptions);
 
-module.exports = markdown.render.bind(markdown)
+module.exports = markdown.render.bind(markdown);
diff --git a/src/templates/layout.html b/src/templates/layout.html
index 65fdede..86eec59 100644
--- a/src/templates/layout.html
+++ b/src/templates/layout.html
@@ -2,6 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8"/>
+    <link href="/css/code.css" rel="stylesheet" />
     <title></title>
   </head>
   <body>
diff --git a/test/app.test.js b/test/app.test.js
index 2445493..ac0ce2d 100644
--- a/test/app.test.js
+++ b/test/app.test.js
@@ -9,7 +9,9 @@ const mf = require("microformat-node");
 process.chdir(path.resolve(__dirname, "./testsite/"));
 const config = require(path.resolve(__dirname, "../src/modules/config.js"));
 
-const app = require("../src/app.js");
+const App = require("../src/app.js");
+let app;
+test.before(async t => (app = await App()));
 
 const parseResponse = res =>
   cheerio.load(res.text, {
@@ -52,7 +54,8 @@ test("homepage", async function(t) {
   t.deepEqual(count, {
     "h-card": 1,
     "h-feed": 1,
-    "h-entry": 1
+    "h-entry": 1,
+    rels: 1
   });
 
   const data = await mf.getAsync(options);
@@ -82,7 +85,8 @@ test("post", async function(t) {
 
   t.deepEqual(count, {
     "h-card": 1,
-    "h-entry": 1
+    "h-entry": 1,
+    rels: 1
   });
 
   const data = await mf.getAsync(options);
@@ -111,7 +115,8 @@ test("tags", async function(t) {
   t.deepEqual(count, {
     "h-card": 1,
     "h-feed": 1,
-    "h-entry": 1
+    "h-entry": 1,
+    rels: 1
   });
 
   const data = await mf.getAsync(options);
@@ -119,5 +124,14 @@ test("tags", async function(t) {
   t.snapshot(data, "should contain relevant microformats data");
 });
 
+test("highlight css", async function(t) {
+  const res = await request(app.listen()).get("/css/code.css");
+
+  t.is(res.statusCode, 200);
+  t.is(res.type, "text/css");
+  t.is(res.charset, "utf-8");
+  t.regex(res.text, /^\.hljs/m);
+});
+
 test(notFound, "/post/non-existent", /Post not found/);
 test(notFound, "/tag/non-existent", /tag non-existent not found/);
diff --git a/test/domain/posts.test.js b/test/domain/posts.test.js
index 8b27698..bb97361 100644
--- a/test/domain/posts.test.js
+++ b/test/domain/posts.test.js
@@ -1,18 +1,22 @@
 const test = require("ava");
 const path = require("path");
 
-const Posts = require("../../src/domain/posts.js")(
-  {
-    folder: path.resolve(__dirname, "../testsite/posts/"),
-    taxonomies: {
-      tag: "tags",
-      category: "categories"
-    }
-  },
-  basename => basename
-);
+const Posts = require("../../src/domain/posts.js");
 
-test("get", t => {
+test.beforeEach(async t => {
+  t.context = await Posts(
+    {
+      folder: path.resolve(__dirname, "../testsite/posts/"),
+      taxonomies: {
+        tag: "tags",
+        category: "categories"
+      }
+    },
+    basename => basename
+  );
+});
+
+test("get", async t => {
   const expected = new Map(
     Object.entries({
       title: "This is a test",
@@ -21,7 +25,7 @@ test("get", t => {
       tags: ["a", "b"]
     })
   );
-  const post = Posts.get(
+  const post = await t.context.get(
     basename => basename,
     path.resolve(__dirname, "../testsite/posts/testfile.md")
   );
diff --git a/test/snapshots/app.test.js.md b/test/snapshots/app.test.js.md
index 99811f9..a595585 100644
--- a/test/snapshots/app.test.js.md
+++ b/test/snapshots/app.test.js.md
@@ -56,8 +56,18 @@ Generated by [AVA](https://ava.li).
           ],
         },
       ],
-      'rel-urls': {},
-      rels: {},
+      'rel-urls': {
+        '/css/code.css': {
+          rels: [
+            'stylesheet',
+          ],
+        },
+      },
+      rels: {
+        stylesheet: [
+          '/css/code.css',
+        ],
+      },
     }
 
 ## post
@@ -86,8 +96,8 @@ Generated by [AVA](https://ava.li).
           properties: {
             content: [
               {
-                html: '<p>Ut enim blandit volutpat maecenas? Volutpat blandit aliquam etiam erat velit, scelerisque in dictum non, consectetur a erat nam at lectus urna duis convallis convallis tellus, id interdum velit laoreet!</p>',
-                value: 'Ut enim blandit volutpat maecenas? Volutpat blandit aliquam etiam erat velit, scelerisque in dictum non, consectetur a erat nam at lectus urna duis convallis convallis tellus, id interdum velit laoreet!',
+                html: '<p>Ut enim blandit volutpat maecenas? Volutpat blandit aliquam etiam erat velit, scelerisque in dictum non, consectetur a erat nam at lectus urna duis convallis convallis tellus, id interdum velit laoreet!</p> <pre><code class="language-sh"> <span class="hljs-meta">#!/usr/bin/env zsh </span> <span class="hljs-built_in">echo</span> this is some shell code <span class="hljs-keyword">if</span> [[ -n <span class="hljs-variable">$test</span> ]] <span class="hljs-keyword">then</span> <span class="hljs-built_in">echo</span> <span class="hljs-built_in">test</span> passed <span class="hljs-keyword">fi</span> </code></pre>',
+                value: 'Ut enim blandit volutpat maecenas? Volutpat blandit aliquam etiam erat velit, scelerisque in dictum non, consectetur a erat nam at lectus urna duis convallis convallis tellus, id interdum velit laoreet! #!/usr/bin/env zsh echo this is some shell code if [[ -n $test ]] then echo test passed fi',
               },
             ],
             name: [
@@ -102,8 +112,18 @@ Generated by [AVA](https://ava.li).
           ],
         },
       ],
-      'rel-urls': {},
-      rels: {},
+      'rel-urls': {
+        '/css/code.css': {
+          rels: [
+            'stylesheet',
+          ],
+        },
+      },
+      rels: {
+        stylesheet: [
+          '/css/code.css',
+        ],
+      },
     }
 
 ## tags
@@ -158,6 +178,16 @@ Generated by [AVA](https://ava.li).
           ],
         },
       ],
-      'rel-urls': {},
-      rels: {},
+      'rel-urls': {
+        '/css/code.css': {
+          rels: [
+            'stylesheet',
+          ],
+        },
+      },
+      rels: {
+        stylesheet: [
+          '/css/code.css',
+        ],
+      },
     }
diff --git a/test/snapshots/app.test.js.snap b/test/snapshots/app.test.js.snap
index bbc5c61..e356c77 100644
--- a/test/snapshots/app.test.js.snap
+++ b/test/snapshots/app.test.js.snap
Binary files differdiff --git a/test/testsite/posts/testfile.md b/test/testsite/posts/testfile.md
index 84d8ed1..d6a4292 100644
--- a/test/testsite/posts/testfile.md
+++ b/test/testsite/posts/testfile.md
@@ -7,3 +7,14 @@ Tags = ["a", "b"]
 Ut enim blandit volutpat maecenas? Volutpat blandit aliquam etiam erat
 velit, scelerisque in dictum non, consectetur a erat nam at lectus
 urna duis convallis convallis tellus, id interdum velit laoreet!
+
+```sh
+#!/usr/bin/env zsh
+
+echo this is some shell code
+
+if [[ -n $test ]]
+then
+    echo test passed
+fi
+```
diff --git a/yarn.lock b/yarn.lock
index a43ee0a..60f4071 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -48,6 +48,17 @@
   version "6.0.79"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.79.tgz#5efe7d4a6d8c453c7e9eaf55d931f4a22fac5169"
 
+CSSselect@~0.4.0:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/CSSselect/-/CSSselect-0.4.1.tgz#f8ab7e1f8418ce63cda6eb7bd778a85d7ec492b2"
+  dependencies:
+    CSSwhat "0.4"
+    domutils "1.4"
+
+CSSwhat@0.4:
+  version "0.4.7"
+  resolved "https://registry.yarnpkg.com/CSSwhat/-/CSSwhat-0.4.7.tgz#867da0ff39f778613242c44cfea83f0aa4ebdf9b"
+
 abbrev@1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
@@ -225,6 +236,10 @@ async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
 
+async@^1.2.1:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
 async@^2.0.1:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
@@ -852,6 +867,16 @@ cheerio@0.22.x:
     lodash.reject "^4.4.0"
     lodash.some "^4.4.0"
 
+cheerio@^0.17.0:
+  version "0.17.0"
+  resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.17.0.tgz#fa5ae42cc60121133d296d0b46d983215f7268ea"
+  dependencies:
+    CSSselect "~0.4.0"
+    dom-serializer "~0.0.0"
+    entities "~1.1.1"
+    htmlparser2 "~3.7.2"
+    lodash "~2.4.1"
+
 cheerio@^1.0.0-rc.1:
   version "1.0.0-rc.1"
   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.1.tgz#2af37339eab713ef6b72cde98cefa672b87641fe"
@@ -1009,7 +1034,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
-commander@^2.9.0:
+commander@^2.8.1, commander@^2.9.0:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
   dependencies:
@@ -1413,6 +1438,13 @@ dom-serializer@0, dom-serializer@~0.1.0:
     domelementtype "~1.1.1"
     entities "~1.1.1"
 
+dom-serializer@~0.0.0:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.0.1.tgz#9589827f1e32d22c37c829adabd59b3247af8eaf"
+  dependencies:
+    domelementtype "~1.1.1"
+    entities "~1.1.1"
+
 domelementtype@1, domelementtype@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
@@ -1421,13 +1453,25 @@ domelementtype@~1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
 
+domhandler@2.2:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.2.1.tgz#59df9dcd227e808b365ae73e1f6684ac3d946fc2"
+  dependencies:
+    domelementtype "1"
+
 domhandler@^2.3.0:
   version "2.4.1"
   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
   dependencies:
     domelementtype "1"
 
-domutils@1.5.1:
+domutils@1.4:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.4.3.tgz#0865513796c6b306031850e175516baf80b72a6f"
+  dependencies:
+    domelementtype "1"
+
+domutils@1.5, domutils@1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
   dependencies:
@@ -1502,6 +1546,10 @@ ent@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
+entities@1.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
+
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
@@ -2200,6 +2248,16 @@ glob@7.1.0:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@^5.0.3:
+  version "5.0.15"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+  dependencies:
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "2 || 3"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@@ -2226,6 +2284,15 @@ globals@^9.0.0, globals@^9.14.0, globals@^9.17.0:
   version "9.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
 
+globby@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-2.1.0.tgz#9e9192bcd33f4ab6a4f894e5e7ea8b713213c482"
+  dependencies:
+    array-union "^1.0.1"
+    async "^1.2.1"
+    glob "^5.0.3"
+    object-assign "^3.0.0"
+
 globby@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
@@ -2398,6 +2465,16 @@ hide-powered-by@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
 
+highland@^2.11.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/highland/-/highland-2.11.0.tgz#4d156709c5f10bc31cab6a97c7feb6c373e2466d"
+  dependencies:
+    util-deprecate "^1.0.2"
+
+highlight.js@^9.12.0:
+  version "9.12.0"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e"
+
 hoek@2.x.x:
   version "2.16.3"
   resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
@@ -2434,6 +2511,16 @@ htmlparser2@^3.8.2, htmlparser2@^3.9.1:
     inherits "^2.0.1"
     readable-stream "^2.0.2"
 
+htmlparser2@~3.7.2:
+  version "3.7.3"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.7.3.tgz#6a64c77637c08c6f30ec2a8157a53333be7cb05e"
+  dependencies:
+    domelementtype "1"
+    domhandler "2.2"
+    domutils "1.5"
+    entities "1.0"
+    readable-stream "1.1"
+
 http-assert@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.3.0.tgz#a31a5cf88c873ecbb5796907d4d6f132e8c01e4a"
@@ -3285,6 +3372,10 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, l
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
+lodash@~2.4.1:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e"
+
 log-symbols@1.0.2, log-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@@ -3485,7 +3576,7 @@ mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
 
-minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
   dependencies:
@@ -3700,6 +3791,10 @@ oauth-sign@~0.8.1:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
+object-assign@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
+
 object-assign@^4.0.1, object-assign@^4.1.0:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -4049,6 +4144,16 @@ precise-typeof@^1.0.2:
   dependencies:
     is-buffer "^1.1.1"
 
+predentation@alanpearce/predentation#fix-code-class:
+  version "0.1.1"
+  resolved "https://codeload.github.com/alanpearce/predentation/tar.gz/cb370272545eb85917aa7df0de6aa771cf7cbf63"
+  dependencies:
+    chalk "^1.0.0"
+    cheerio "^0.17.0"
+    commander "^2.8.1"
+    globby "^2.1.0"
+    through2 "^2.0.0"
+
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -4206,6 +4311,15 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
+readable-stream@1.1:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
 readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2:
   version "2.2.11"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72"
@@ -5041,7 +5155,7 @@ user-home@^2.0.0:
   dependencies:
     os-homedir "^1.0.0"
 
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"