Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Feature: {{reading_time}} theme helper (#9217)
closes #9200 - Registered new server helper `{{reading_time}}`. - Added new global util `word-count` based on the util in Ghost admin, which returns the number of words in an HTML string. - Based on the word count of the post html, the helper calculated the estimated reading time: - 275 words per minute - additional 12 seconds when post has feature image - Renders a string like 'x min red', unless reading time is less than a minute. In this case, the rendered string is '< 1 min read'.
- Loading branch information
Showing
6 changed files
with
178 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
// # Reading Time Helper | ||
// | ||
// Usage: `{{reading_time}}` | ||
// | ||
// Returns estimated reading time for post | ||
|
||
var proxy = require('./proxy'), | ||
schema = require('../data/schema').checks, | ||
SafeString = proxy.SafeString, | ||
wordCountUtil = require('../utils/word-count'); | ||
|
||
module.exports = function reading_time() {// eslint-disable-line camelcase | ||
var html, | ||
wordsPerMinute = 275, | ||
wordsPerSecond = wordsPerMinute / 60, | ||
wordCount, | ||
imageCount, | ||
readingTimeSeconds, | ||
readingTime; | ||
|
||
// only calculate reading time for posts | ||
if (!schema.isPost(this)) { | ||
return null; | ||
} | ||
|
||
html = this.html; | ||
imageCount = this.feature_image ? 1 : 0; | ||
wordCount = wordCountUtil(html); | ||
readingTimeSeconds = wordCount / wordsPerSecond; | ||
|
||
// add 12 seconds to reading time if feature image is present | ||
readingTimeSeconds = imageCount ? readingTimeSeconds + 12 : readingTimeSeconds; | ||
|
||
if (readingTimeSeconds < 60) { | ||
readingTime = '< 1 min read'; | ||
} else { | ||
readingTime = `${Math.round(readingTimeSeconds / 60)} min read`; | ||
} | ||
|
||
return new SafeString(readingTime); | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
var should = require('should'), // jshint ignore:line | ||
|
||
// Stuff we are testing | ||
helpers = require('../../../server/helpers'); | ||
|
||
describe('{{reading_time}} helper', function () { | ||
it('[success] renders reading time for less than one minute text correctly', function () { | ||
var data = { | ||
html: '<div class="kg-card-markdown"><p>This is a text example! Count me in ;)</p></div>', | ||
title: 'Test', | ||
slug: 'slug' | ||
}, | ||
result = helpers.reading_time.call(data); | ||
|
||
String(result).should.equal('< 1 min read'); | ||
}); | ||
|
||
it('[success] renders reading time for more than one minute text correctly', function () { | ||
var data = { | ||
html: '<div class="kg-card-markdown"><p>Ghost has a number of different user roles for your team</p>' + | ||
'<h3 id="authors">Authors</h3><p>The base user level in Ghost is an author. Authors can write posts,' + | ||
' edit their own posts, and publish their own posts. Authors are <strong>trusted</strong> users. If you ' + | ||
'don\'t trust users to be allowed to publish their own posts, you shouldn\'t invite them to Ghost admin.</p>' + | ||
'<h3 id="editors">Editors</h3><p>Editors are the 2nd user level in Ghost. Editors can do everything that an' + | ||
' Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new' + ' authors to the site.</p><h3 id="administrators">Administrators</h3><p>The top user level in Ghost is Administrator.' + | ||
' Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings ' + | ||
'and data, not just content. Additionally, administrators have full access to invite, manage or remove any other' + | ||
' user of the site.</p><h3 id="theowner">The Owner</h3><p>There is only ever one owner of a Ghost site. ' + | ||
'The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: ' + | ||
'The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings ' + | ||
'if applicable — for example, billing details, if using Ghost(Pro).</p><hr><p>It\'s a good idea to ask all of your' + | ||
' users to fill out their user profiles, including bio and social links. These will populate rich structured data ' + | ||
'for posts and generally create more opportunities for themes to fully populate their design.</p></div>', | ||
title: 'Test', | ||
slug: 'slug', | ||
feature_image: '/content/images/someimage.jpg' | ||
}, | ||
result = helpers.reading_time.call(data); | ||
|
||
String(result).should.equal('1 min read'); | ||
}); | ||
|
||
it('[success] adds time for feature image', function () { | ||
var data = { | ||
html: '<div class="kg-card-markdown"><p>Ghost has a number of different user roles for your team</p>' + | ||
'<h3 id="authors">Authors</h3><p>The base user level in Ghost is an author. Authors can write posts,' + | ||
' edit their own posts, and publish their own posts. Authors are <strong>trusted</strong> users. If you ' + | ||
'don\'t trust users to be allowed to publish their own posts, you shouldn\'t invite them to Ghost admin.</p>' + | ||
'<h3 id="editors">Editors</h3><p>Editors are the 2nd user level in Ghost. Editors can do everything that an' + | ||
' Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new' + ' authors to the site.</p><h3 id="administrators">Administrators</h3><p>The top user level in Ghost is Administrator.' + | ||
' Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings ' + | ||
'and data, not just content. Additionally, administrators have full access to invite, manage or remove any other' + | ||
' user of the site.</p><h3 id="theowner">The Owner</h3><p>There is only ever one owner of a Ghost site. ' + | ||
'The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: ' + | ||
'The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings ' + | ||
'if applicable — for example, billing details, if using Ghost(Pro).</p><hr><p>It\'s a good idea to ask all of your' + | ||
' users to fill out their user profiles, including bio and social links. These will populate rich structured data ', | ||
title: 'Test', | ||
slug: 'slug', | ||
feature_image: '/content/images/someimage.jpg' | ||
}, | ||
result = helpers.reading_time.call(data); | ||
|
||
// The reading time for this HTML snippet would be 63 seconds without the image | ||
// Adding the 12 additional seconds for the image results in a reading time > 1 minute | ||
String(result).should.equal('1 min read'); | ||
}); | ||
|
||
it('[failure] does not render reading time when not post', function () { | ||
var data = { | ||
author: { | ||
name: 'abc 123', | ||
slug: 'abc123' | ||
} | ||
}, | ||
result = helpers.reading_time.call(data); | ||
|
||
should.not.exist(result); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
var should = require('should'), // jshint ignore:line | ||
wordCount = require('../../../server/utils/word-count'); | ||
|
||
describe('wordCount', function () { | ||
it('[success] can count words', function () { | ||
var html = 'Some words here', | ||
result = wordCount(html); | ||
|
||
result.should.equal(3); | ||
}); | ||
|
||
it('[success] sanitized HTML tags', function () { | ||
var html = '<div class="kg-card-markdown"><p>This is a text example! Count me in ;)</p></div>', | ||
result = wordCount(html); | ||
|
||
result.should.equal(8); | ||
}); | ||
|
||
it('[success] sanitized non alpha-numeric characters', function () { | ||
var html = '<div class="kg-card-markdown"><p>This is a text example! I love Döner. Especially number 875.</p></div>', | ||
result = wordCount(html); | ||
|
||
result.should.equal(11); | ||
}); | ||
|
||
it('[success] sanitized white space correctly', function () { | ||
var html = ' <div class="kg-card-markdown"><p> This is a text example!\n Count me in ;)</p></div> ', | ||
result = wordCount(html); | ||
|
||
result.should.equal(8); | ||
}); | ||
}); |