diff --git a/Gruntfile.js b/Gruntfile.js
index 3090606d4a..298d813a38 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -589,7 +589,8 @@ var _ = require('lodash'),
'bower_components/nanoscroller/bin/javascripts/jquery.nanoscroller.js',
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
- 'core/shared/lib/showdown/extensions/ghostgfm.js'
+ 'core/shared/lib/showdown/extensions/ghostgfm.js',
+ 'core/shared/lib/showdown/extensions/ghostfootnotes.js'
]
},
@@ -624,7 +625,8 @@ var _ = require('lodash'),
'bower_components/nanoscroller/bin/javascripts/jquery.nanoscroller.js',
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
- 'core/shared/lib/showdown/extensions/ghostgfm.js'
+ 'core/shared/lib/showdown/extensions/ghostgfm.js',
+ 'core/shared/lib/showdown/extensions/ghostfootnotes.js'
]
}
},
diff --git a/core/client/helpers/gh-format-markdown.js b/core/client/helpers/gh-format-markdown.js
index 29419b798e..69be7fc13d 100644
--- a/core/client/helpers/gh-format-markdown.js
+++ b/core/client/helpers/gh-format-markdown.js
@@ -4,7 +4,7 @@ import cajaSanitizers from 'ghost/utils/caja-sanitizers';
var showdown,
formatMarkdown;
-showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm']});
+showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes']});
formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) {
var escapedhtml = '';
diff --git a/core/server/models/post.js b/core/server/models/post.js
index 77a3ba6eed..8a1483b142 100644
--- a/core/server/models/post.js
+++ b/core/server/models/post.js
@@ -5,7 +5,8 @@ var _ = require('lodash'),
errors = require('../errors'),
Showdown = require('showdown-ghost'),
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
- converter = new Showdown.converter({extensions: [ghostgfm]}),
+ ghostfootnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
+ converter = new Showdown.converter({extensions: [ghostgfm, ghostfootnotes]}),
ghostBookshelf = require('./base'),
xmlrpc = require('../xmlrpc'),
sitemap = require('../data/sitemap'),
diff --git a/core/shared/lib/showdown/extensions/ghostfootnotes.js b/core/shared/lib/showdown/extensions/ghostfootnotes.js
new file mode 100644
index 0000000000..dcb56f3c36
--- /dev/null
+++ b/core/shared/lib/showdown/extensions/ghostfootnotes.js
@@ -0,0 +1,111 @@
+/* jshint node:true, browser:true */
+
+// Adds footnote syntax as per Markdown Extra:
+//
+// https://michelf.ca/projects/php-markdown/extra/#footnotes
+//
+// That's some text with a footnote.[^1]
+//
+// [^1]: And that's the footnote.
+//
+// That's the second paragraph.
+//
+// Also supports [^n] if you don't want to worry about preserving
+// the footnote order yourself.
+
+function replaceInlineFootnotes(text) {
+ // Inline footnotes e.g. "foo[^1]"
+ var inlineRegex = /(?!^)\[\^(\d|n)\]/gim,
+ i = 0;
+
+ return text.replace(inlineRegex, function (match, n) {
+ // We allow both automatic and manual footnote numbering
+ if (n === 'n') {
+ n = i + 1;
+ }
+
+ var s = '' +
+ '' + n + '' +
+ '';
+ i += 1;
+ return s;
+ });
+}
+
+function replaceEndFootnotes(text) {
+ // Expanded footnotes at the end e.g. "[^1]: cool stuff"
+ var endRegex = /\[\^(\d|n)\]: ([\s\S]*?)$(?! )/gim,
+ m = text.match(endRegex),
+ total = m ? m.length : 0,
+ i = 0;
+
+ return text.replace(endRegex, function (match, n, content) {
+ if (n === 'n') {
+ n = i + 1;
+ }
+
+ content = content.replace(/\n /g, '
');
+
+ var s = '
' + content + ' ↩' + + '
' + + 'Foo!1(bar)
' + }, + + { + input: '![^1](bar)', + output: 'foo bar ↩<\/a><\/p><\/li><\/ol><\/div>/ + }, processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it('should number multiple footnotes correctly', function () { + var testPhrase = { + input: 'foo[^1] bar[^n] etc[^2]', + output: /foo1<\/a><\/sup> bar2<\/a><\/sup> etc2<\/a><\/sup>/ + }, processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it('should put everything together', function () { + // Tests for some interaction bugs between components e.g. + // confusing the end form and the inline form + var testPhrase = { + input: 'foo bar[^1] is a very[^n] foo bar[^1]\n' + + '[^n]: a metasyntactic variable\n' + + '[^n]: this is hard to measure', + output: 'foo bar1 is a very2 foo bar1\n' + + '' + }, processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); +});