mirror of
https://github.com/TryGhost/Ghost.git
synced 2023-12-13 21:00:40 +01:00
Merge pull request #4270 from mispy/footnotes2
Footnotes extension for Showdown
This commit is contained in:
commit
13757041d9
6 changed files with 225 additions and 5 deletions
|
@ -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'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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'),
|
||||
|
|
111
core/shared/lib/showdown/extensions/ghostfootnotes.js
Normal file
111
core/shared/lib/showdown/extensions/ghostfootnotes.js
Normal file
|
@ -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 = '<sup id="fnref:' + n + '">' +
|
||||
'<a href="#fn:' + n + '" rel="footnote">' + n + '</a>' +
|
||||
'</sup>';
|
||||
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, '<br>');
|
||||
|
||||
var s = '<li class="footnote" id="fn:' + n + '">' +
|
||||
'<p>' + content + ' <a href="#fnref:' + n +
|
||||
'" title="return to article">↩</a>' +
|
||||
'</p>' +
|
||||
'</li>';
|
||||
|
||||
if (i === 0) {
|
||||
s = '<div class="footnotes"><ol>' + s;
|
||||
}
|
||||
|
||||
if (i === total - 1) {
|
||||
s = s + '</ol></div>';
|
||||
}
|
||||
|
||||
i += 1;
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
(function () {
|
||||
var footnotes = function () {
|
||||
return [
|
||||
{
|
||||
type: 'lang',
|
||||
filter: function (text) {
|
||||
var preExtractions = {},
|
||||
hashID = 0;
|
||||
|
||||
function hashId() {
|
||||
return hashID += 1;
|
||||
}
|
||||
|
||||
// Extract pre blocks
|
||||
text = text.replace(/```[\s\S]*?\n```/gim, function (x) {
|
||||
var hash = hashId();
|
||||
preExtractions[hash] = x;
|
||||
return '{gfm-js-extract-pre-' + hash + '}';
|
||||
}, 'm');
|
||||
|
||||
console.log(text);
|
||||
text = replaceInlineFootnotes(text);
|
||||
text = replaceEndFootnotes(text);
|
||||
|
||||
// replace extractions
|
||||
text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
|
||||
return preExtractions[y];
|
||||
});
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
|
||||
window.Showdown.extensions.footnotes = footnotes;
|
||||
}
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') {
|
||||
module.exports = footnotes;
|
||||
}
|
||||
}());
|
|
@ -12,8 +12,9 @@ var should = require('should'),
|
|||
Showdown = require('showdown-ghost'),
|
||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
||||
ghostimagepreview = require('../../shared/lib/showdown/extensions/ghostimagepreview'),
|
||||
ghostfootnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
|
||||
|
||||
converter = new Showdown.converter({extensions: [ghostimagepreview, ghostgfm]});
|
||||
converter = new Showdown.converter({extensions: [ghostimagepreview, ghostgfm, ghostfootnotes]});
|
||||
|
||||
// To stop jshint complaining
|
||||
should.equal(true, true);
|
||||
|
@ -512,6 +513,25 @@ describe('Showdown client side converter', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should treat ![^n] as footnote unless it occurs on a new line', function () {
|
||||
var testPhrases = [
|
||||
{
|
||||
input: 'Foo![^1](bar)',
|
||||
output: '<p>Foo!<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>(bar)</p>'
|
||||
},
|
||||
|
||||
{
|
||||
input: '![^1](bar)',
|
||||
output: '<section id="image_upload_undefined" class="js-drop-zone image-uploader"><img class="js-upload-target" src="bar"/><div class="description">Add image of <strong>^1</strong></div><input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage"></section>'
|
||||
}
|
||||
];
|
||||
|
||||
testPhrases.forEach(function (testPhrase) {
|
||||
var processedMarkup = converter.makeHtml(testPhrase.input);
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
});
|
||||
|
||||
// Waiting for showdown typography to be updated
|
||||
// it('should correctly convert quotes to curly quotes', function () {
|
||||
// var testPhrases = [
|
||||
|
|
86
core/test/unit/showdown_footnotes_spec.js
Normal file
86
core/test/unit/showdown_footnotes_spec.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Tests the footnotes extension for showdown
|
||||
*
|
||||
*/
|
||||
|
||||
/*globals describe, it */
|
||||
/*jshint expr:true*/
|
||||
var should = require('should'),
|
||||
|
||||
// Stuff we are testing
|
||||
ghostfootnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes');
|
||||
|
||||
// To stop jshint complaining
|
||||
should.equal(true, true);
|
||||
|
||||
function _ExecuteExtension(ext, text) {
|
||||
if (ext.regex) {
|
||||
var re = new RegExp(ext.regex, 'g');
|
||||
return text.replace(re, ext.replace);
|
||||
} else if (ext.filter) {
|
||||
return ext.filter(text);
|
||||
}
|
||||
}
|
||||
|
||||
function _ConvertPhrase(testPhrase) {
|
||||
return ghostfootnotes().reduce(function (text, ext) {
|
||||
return _ExecuteExtension(ext, text);
|
||||
}, testPhrase);
|
||||
}
|
||||
|
||||
describe('Ghost footnotes showdown extension', function () {
|
||||
/*jslint regexp: true */
|
||||
|
||||
it('should export an array of methods for processing', function () {
|
||||
ghostfootnotes.should.be.a.function;
|
||||
ghostfootnotes().should.be.an.Array;
|
||||
|
||||
ghostfootnotes().forEach(function (processor) {
|
||||
processor.should.be.an.Object;
|
||||
processor.should.have.property('type');
|
||||
processor.type.should.be.a.String;
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace inline footnotes with the right html', function () {
|
||||
var testPhrase = {
|
||||
input: 'foo_bar[^1]',
|
||||
output: /<sup id="fnref:1"><a href="#fn:1" rel="footnote">1<\/a><\/sup>/
|
||||
}, processedMarkup = _ConvertPhrase(testPhrase.input);
|
||||
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
|
||||
it('should replace end footnotes with the right html', function () {
|
||||
var testPhrase = {
|
||||
input: '[^1]: foo bar',
|
||||
output: /<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>foo bar <a href="#fnref:1" title="return to article">↩<\/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: /foo<sup id="fnref:1"><a href="#fn:1" rel="footnote">1<\/a><\/sup> bar<sup id="fnref:2"><a href="#fn:2" rel="footnote">2<\/a><\/sup> etc<sup id="fnref:2"><a href="#fn:2" rel="footnote">2<\/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 bar<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup> is a very<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup> foo bar<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>\n' +
|
||||
'<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>a metasyntactic variable <a href="#fnref:1" title="return to article">↩</a></p></li>\n' +
|
||||
'<li class="footnote" id="fn:2"><p>this is hard to measure <a href="#fnref:2" title="return to article">↩</a></p></li></ol></div>'
|
||||
}, processedMarkup = _ConvertPhrase(testPhrase.input);
|
||||
|
||||
processedMarkup.should.match(testPhrase.output);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue