Repository: jamesramsay/hercule
Pull request #41 (2015-07-21 00:26:51)

Support for default values
Based on #40 

Adds new syntax for default values:

```markdown
:[Placeholder link example](placeholder || "default if placeholder isn't provided")

:[Missing file example](example.md || "default if example.md can't be found")
```

This is useful for handling optional overrides or dynamically generated files that might not exist.

/cc @MichaelHirn 


Patch:
diff --git a/src/grammar.coffee b/src/grammar.coffee
index a871873..3c70dfc 100644
--- a/src/grammar.coffee
+++ b/src/grammar.coffee
@@ -1,23 +1,28 @@
 # Parses transclusion links
 #
-# :[example transclude link](file.md common:file.md)
+# :[example transclude link](file.md || "default value" common:file.md)
 #
 # {
-#  link: file.md
-#  type: file
+#  href: file.md
+#  hrefType: file
 #  references: [
 #     placeholder: common
 #     type: file
 #     value: file.md
 #   ]
+#  default: {
+#   type: string
+#   value: default value
+#  }
 # }
 
 transcludeGrammar = """
-start = l:link? ' '? o:reference* {
+start = l:link? ' || '? d:default? ' '? o:reference* {
   return {
     "href": l.href,
     "hrefType": l.hrefType,
-    "references": o
+    "references": o,
+    "default": d
   };
 }
 
@@ -33,6 +38,8 @@ placeholder = p:[a-zA-Z0-9]+ {
   return p.join("");
 }
 
+default = emptyString / string
+
 link = httpLink / fileLink / string / reset
 
 fileLink = f:[^ ()\"]+ {
@@ -49,6 +56,14 @@ httpLink = left:("http://" / "https://") right:[^ ()]+ {
   };
 }
 
+emptyString = '\"' '\"' {
+  return {
+    "hrefType": "string",
+    "href": ""
+  };
+}
+
+
 string = '\"' s:([^\"]+) '\"' {
   return {
     "hrefType": "string",
diff --git a/src/hercule.coffee b/src/hercule.coffee
index 2c244e6..02055b0 100644
--- a/src/hercule.coffee
+++ b/src/hercule.coffee
@@ -38,6 +38,8 @@ transclude = (input, relativePath, parents, parentRefs, logger, cb) ->
 
     utils.inflate href, hrefType, (content) ->
       logger "Transcluding: #{href} (#{hrefType}) into #{parents[-1..][0]}"
+      if content is null && link.default
+        content = link.default.href
       transclude content, dir, parents, references, logger, (output) ->
         if output?
           # Preserve leading whitespace and trim excess new lines at EOF


Test patch:
diff --git a/test/fixtures/test-default/attribute-file.md b/test/fixtures/test-default/attribute-file.md
new file mode 100644
index 0000000..cf9d277
--- /dev/null
+++ b/test/fixtures/test-default/attribute-file.md
@@ -0,0 +1,1 @@
+:[Placeholder Attribute](attribute || "imagined")
diff --git a/test/fixtures/test-default/invalid-link.md b/test/fixtures/test-default/invalid-link.md
new file mode 100644
index 0000000..307bcd8
--- /dev/null
+++ b/test/fixtures/test-default/invalid-link.md
@@ -0,0 +1,1 @@
+Jackdaws love my :[test link](non-existend-file.md || "imagined") sphinx of quartz.
diff --git a/test/fixtures/test-default/undefined-placeholder.md b/test/fixtures/test-default/undefined-placeholder.md
new file mode 100644
index 0000000..faebfeb
--- /dev/null
+++ b/test/fixtures/test-default/undefined-placeholder.md
@@ -0,0 +1,1 @@
+Jackdaws love my :[Attribute](attribute-file.md) sphinx of quartz.
diff --git a/test/hercule.coffee b/test/hercule.coffee
index 8226af2..340919a 100644
--- a/test/hercule.coffee
+++ b/test/hercule.coffee
@@ -69,6 +69,25 @@ describe 'hercule', ->
         assert.equal output, 'Jackdaws love my big sphinx of quartz.\n'
         done()
 
+    it 'should transclude strings with invalid links but a default', (done) ->
+      file = __dirname + "/fixtures/test-default/invalid-link.md"
+      input = (fs.readFileSync file).toString()
+      dir = path.dirname file
+
+      hercule.transcludeString input, null, {relativePath: dir}, (output) ->
+        assert.equal output, 'Jackdaws love my imagined sphinx of quartz.\n'
+        done()
+
+    it 'should transclude strings with undefined placeholders but a default', (done) ->
+      file = __dirname + "/fixtures/test-default/undefined-placeholder.md"
+      input = (fs.readFileSync file).toString()
+      dir = path.dirname file
+
+      hercule.transcludeString input, null, {relativePath: dir}, (output) ->
+        assert.equal output, 'Jackdaws love my imagined sphinx of quartz.\n'
+        done()
+
+
   describe 'transcludeFile', ->
 
     beforeEach ->
@@ -137,3 +156,18 @@ describe 'hercule', ->
       hercule.transcludeFile inputFile, (output) ->
         assert.equal output, "The quick brown fox jumps over the lazy dog.\n"
         done()
+
+    it 'should transclude files with invalid links but a default', (done) ->
+      inputFile = __dirname + "/fixtures/test-default/invalid-link.md"
+
+      hercule.transcludeFile inputFile, (output) ->
+        assert.equal output, 'Jackdaws love my imagined sphinx of quartz.\n'
+        done()
+
+    it 'should transclude files with undefined placeholder but a default', (done) ->
+      inputFile = __dirname + "/fixtures/test-default/undefined-placeholder.md"
+
+      hercule.transcludeFile inputFile, (output) ->
+        assert.equal output, 'Jackdaws love my imagined sphinx of quartz.\n'
+        done()
+
diff --git a/test/utils.coffee b/test/utils.coffee
index 8820495..02670f0 100644
--- a/test/utils.coffee
+++ b/test/utils.coffee
@@ -64,6 +64,7 @@ describe 'utils', ->
         hrefType: "file"
         placeholder: link.placeholder
         references: []
+        default: null
         relativePath: ""
       }
 
@@ -83,6 +84,7 @@ describe 'utils', ->
         hrefType: "http"
         placeholder: link.placeholder
         references: []
+        default: null
         relativePath: ""
       }
 
@@ -118,11 +120,98 @@ describe 'utils', ->
           hrefType:"string"
           href:"Copyright 2014 (c)"
         ]
+        default: null
         relativePath: "customer/farmers-market"
       }
 
       done()
 
+    it 'should parse links with default', (done) ->
+      href = "file-which-does-not-exist.md || \"default value\""
+      link =
+        href: href
+        placeholder: ":[simple](#{href})"
+        relativePath: ""
+
+      parsedLink = utils.parse link
+
+      assert.deepEqual parsedLink, {
+        href: "file-which-does-not-exist.md"
+        hrefType: "file"
+        placeholder: link.placeholder
+        references: []
+        default: {
+          hrefType: "string"
+          href: "default value"
+        }
+        relativePath: ""
+      }
+
+      done()
+
+    it 'should parse complex links with default', (done) ->
+      mixedLink = "file.md || \"Nope\" fruit:apple.md header: footer:../common/footer.md copyright:\"Copyright 2014 (c)\""
+      link =
+        href: mixedLink
+        placeholder: ":[](#{mixedLink})"
+        relativePath: "customer/farmers-market"
+
+      parsedLink = utils.parse link
+
+      assert.deepEqual parsedLink, {
+        href: "file.md"
+        hrefType: "file"
+        placeholder: link.placeholder
+        references: [
+          placeholder: "fruit"
+          hrefType: "file"
+          href: "customer/farmers-market/apple.md"
+        ,
+          placeholder: "header"
+          hrefType: "string"
+          href: ""
+        ,
+          placeholder: "footer"
+          hrefType: "file"
+          href: "customer/common/footer.md"
+        ,
+          placeholder:"copyright"
+          hrefType:"string"
+          href:"Copyright 2014 (c)"
+        ]
+        default: {
+          hrefType: "string"
+          href: "Nope"
+        }
+        relativePath: "customer/farmers-market"
+      }
+
+      done()
+
+    it 'should parse links with an empty default', (done) ->
+      href = "file-which-does-not-exist.md || \"\""
+      link =
+        href: href
+        placeholder: ":[simple](#{href})"
+        relativePath: ""
+
+      parsedLink = utils.parse link
+
+      assert.deepEqual parsedLink, {
+        href: "file-which-does-not-exist.md"
+        hrefType: "file"
+        placeholder: link.placeholder
+        references: []
+        default: {
+          hrefType: "string"
+          href: ""
+        }
+        relativePath: ""
+      }
+
+      done()
+
+
 
   describe 'readFile', ->
     it 'should return null for missing files', (done) ->