Repository: jamesramsay/hercule
Pull request #214 (2016-03-28 19:38:03)

New: refactoring resolve-stream

codecov-io: ## [Current coverage][1] is `98.66%`
> Merging **#214** into **master** will decrease coverage by **-0.45%** as of [`f9641aa`][3]


```diff
@@            master    #214   diff @@
======================================
  Files            7       7       
  Stmts          226     224     -2
  Branches        24      24       
  Methods          0       0       
======================================
- Hit            224     221     -3
- Partial          0       1     +1
  Missed           2       2       
```

> Review entire [Coverage Diff][4] as of [`f9641aa`][3]


[1]: https://codecov.io/github/jamesramsay/hercule?ref=f9641aa9b1392aa34b8fbc6211cac0104116fb55
[2]: https://codecov.io/github/jamesramsay/hercule/features/suggestions?ref=f9641aa9b1392aa34b8fbc6211cac0104116fb55
[3]: https://codecov.io/github/jamesramsay/hercule/commit/f9641aa9b1392aa34b8fbc6211cac0104116fb55
[4]: https://codecov.io/github/jamesramsay/hercule/compare/9bacfb5d3790d0c60344258906d44d4a9e878e49...f9641aa9b1392aa34b8fbc6211cac0104116fb55

> Powered by [Codecov](https://codecov.io). Updated on successful CI builds.

Patch:
diff --git a/src/inflate-stream.js b/src/inflate-stream.js
index a888f31..f6c754a 100644
--- a/src/inflate-stream.js
+++ b/src/inflate-stream.js
@@ -8,7 +8,6 @@ import regexpTokenizer from 'regexp-stream-tokenizer';
 
 import ResolveStream from './resolve-stream';
 import TrimStream from './trim-stream';
-import grammar from './transclude-parser';
 import { linkRegExp, defaultToken, defaultSeparator, WHITESPACE_GROUP, SUPPORTED_LINK_TYPES } from './config';
 
 /**
@@ -34,7 +33,7 @@ export default function InflateStream(opt, linkPaths) {
   const options = _.merge({}, DEFAULT_OPTIONS, opt);
 
   function inflateDuplex(chunk, link) {
-    const resolver = new ResolveStream(grammar, null, linkPaths);
+    const resolver = new ResolveStream(link.href, linkPaths);
     const inflater = new InflateStream();
     const trimmer = new TrimStream();
 
diff --git a/src/resolve-stream.js b/src/resolve-stream.js
index d998d15..63ee076 100644
--- a/src/resolve-stream.js
+++ b/src/resolve-stream.js
@@ -2,86 +2,92 @@ import through2 from 'through2';
 import path from 'path';
 import _ from 'lodash';
 
+import grammar from './transclude-parser';
+
 /**
 * Input stream: (object)
 * - link (object, required)
 *   - href (string, required)
-* - relativePath (string, optional)
-* - references (array, required) - Empty array permitted
+* - relativePath (string)
+* - references (array, required)
+*   - (object)
+*     - placeholder (string, required)
+*     - href (string, required)
+*     - hrefType (enum, required)
+*     - source (string)
+*     - original (object)
+*       - line (integer, required)
+*       - column (integer, required)
+* - parents (array, required)
 *
 * Output stream: (object)
 * - link (object, required)
 *   - href (string)
 *   - hrefType (enum)
 * - relativePath (string, optional)
+* - references (array, required) - References extended with any newly parsed references.
+*   - (object) - as above
+* - parents (array, required)
 *
 * Input and output properties can be altered by providing options
 */
 
-const DEFAULT_OPTIONS = {
-  input: 'link.href',
-  output: 'link',
-};
-
+function resolve(unresolvedLink, references, relativePath) {
+  const primary = unresolvedLink.primary;
+  const fallback = unresolvedLink.fallback;
+  const override = _.find(references, { placeholder: primary.href });
+  const link = _.pick(override || fallback || primary, ['href', 'hrefType']);
 
-export default function ResolveStream(grammar, opt, linkPaths = []) {
-  const options = _.merge({}, DEFAULT_OPTIONS, opt);
-
-
-  function resolve(unresolvedLink, references, relativePath) {
-    let link = unresolvedLink.primary;
-    const fallback = unresolvedLink.fallback;
-    const override = _.find(references, { placeholder: link.href });
+  if (!override && link.hrefType === 'file') {
+    link.href = path.join(relativePath, link.href);
+  }
 
-    if (override || fallback) {
-      link = _.pick(override || fallback, ['href', 'hrefType']);
-    }
+  return link;
+}
 
-    if (!override && link.hrefType === 'file') {
-      link.href = path.join(relativePath, link.href);
-    }
+function parse(rawLink, relativePath) {
+  // Parse link body using peg.js grammar
+  // This allows complex links with placeholders, fallbacks, and overrides
+  const parsedLink = grammar.parse(rawLink);
 
-    return link;
-  }
+  // Make references relative
+  const parsedReferences = _.map(parsedLink.references, ({ placeholder, href, hrefType }) => {
+    const relativeHref = (hrefType === 'file') ? path.join(relativePath, href) : href;
+    return { placeholder, hrefType, href: relativeHref };
+  });
 
+  return { parsedLink, parsedReferences };
+}
 
+export default function ResolveStream(sourceFile, sourcePaths = []) {
   function transform(chunk, encoding, cb) {
-    const rawLink = _.get(chunk, options.input);
+    const rawLink = _.get(chunk, ['link', 'href']);
     const relativePath = _.get(chunk, 'relativePath') || '';
     const parentRefs = _.get(chunk, 'references') || [];
-    let link;
-    let references;
+    let parsedLink;
+    let parsedReferences;
 
-    // No link to parse
+    // No link to parse, move along
     if (!rawLink) {
       this.push(chunk);
       return cb();
     }
 
     try {
-      link = grammar.parse(rawLink);
+      ({ parsedLink, parsedReferences } = parse(rawLink, relativePath));
     } catch (err) {
       this.push(chunk);
-      this.emit('error', {
-        msg: 'Link could not be parsed',
-        path: rawLink,
-        error: err,
-      });
+      this.emit('error', { msg: 'Link could not be parsed', path: rawLink, error: err });
       return cb();
     }
 
-    references = _.map(link.references, ({ placeholder, href, hrefType }) => {
-      let relativeHref = href;
-      if (hrefType === 'file') relativeHref = path.join(relativePath, href);
-      return { placeholder, hrefType, href: relativeHref };
-    });
-
-    references = _.uniq([...references, ...parentRefs]);
-    link = resolve(link, parentRefs, relativePath);
+    const references = _.uniq([...parsedReferences, ...parentRefs]);
+    const link = resolve(parsedLink, parentRefs, relativePath);
 
-    linkPaths.push(link.href);
+    // Add the resolved link path to the array of all source paths
+    sourcePaths.push(link.href);
 
-    this.push(_.assign(chunk, { [options.output]: link }, { references }));
+    this.push(_.assign(chunk, { link, references }));
     return cb();
   }
 
diff --git a/src/transclude-stream.js b/src/transclude-stream.js
index a4cea0f..dfecdfb 100644
--- a/src/transclude-stream.js
+++ b/src/transclude-stream.js
@@ -6,7 +6,6 @@ import regexpTokenizer from 'regexp-stream-tokenizer';
 import ResolveStream from './resolve-stream';
 import InflateStream from './inflate-stream';
 import IndentStream from './indent-stream';
-import grammar from './transclude-parser';
 import { linkRegExp, defaultToken, defaultSeparator, WHITESPACE_GROUP } from './config';
 
 /**
@@ -22,6 +21,7 @@ const DEFAULT_OPTIONS = {
 
 export default function Transcluder(opt, linkPaths) {
   const options = _.merge({}, DEFAULT_OPTIONS, opt);
+  const source = options.source;
 
   function token(match) {
     return defaultToken(match, options);
@@ -33,7 +33,7 @@ export default function Transcluder(opt, linkPaths) {
 
   const tokenizerOptions = { leaveBehind: `${WHITESPACE_GROUP}`, token, separator };
   const tokenizer = regexpTokenizer(tokenizerOptions, options.linkRegExp || linkRegExp);
-  const resolver = new ResolveStream(grammar, null, linkPaths);
+  const resolver = new ResolveStream(source, linkPaths);
   const inflater = new InflateStream({ linkRegExp: options.linkRegExp, linkMatch: options.linkMatch }, linkPaths);
   const indenter = new IndentStream();
   const stringify = get('content');


Test patch:
diff --git a/test/units/resolve-stream.js b/test/units/resolve-stream.js
index 697220d..7410471 100644
--- a/test/units/resolve-stream.js
+++ b/test/units/resolve-stream.js
@@ -1,10 +1,9 @@
 import test from 'ava';
-import grammar from '../../lib/transclude-parser';
 import ResolveStream from '../../lib/resolve-stream';
 
 
 test.cb('should handle no input', (t) => {
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -23,7 +22,7 @@ test.cb('should handle no input', (t) => {
 
 test.cb('should skip input without link', (t) => {
   const input = { content: 'The quick brown fox jumps over the lazy dog./n' };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -52,7 +51,7 @@ test.cb('should parse input simple link', (t) => {
     href: 'animal.md',
     hrefType: 'file',
   };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -99,7 +98,7 @@ test.cb('should parse input with overrides', (t) => {
       hrefType: 'string',
     },
   ];
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -140,7 +139,7 @@ test.cb('should parse input with overriding link', (t) => {
     href: 'fox.md',
     hrefType: 'file',
   };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -184,7 +183,7 @@ test.cb('should parse input with fallback link', (t) => {
       hrefType: 'string',
     },
   };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream
@@ -209,7 +208,7 @@ test.cb('should emit error on invalid transclusion link', (t) => {
       href: 'animal.md foo:bar:"exception!"',
     },
   };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(2);
   testStream
@@ -244,7 +243,7 @@ test.cb('should resolve link relative to file', (t) => {
     },
     relativePath: 'foo',
   };
-  const testStream = new ResolveStream(grammar);
+  const testStream = new ResolveStream();
 
   t.plan(1);
   testStream