API Docs for: 0.8.0
Show:

File: include/service/entities/content/article_renderer.js

  1. /*
  2. Copyright (C) 2016 PencilBlue, LLC
  3.  
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8.  
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13.  
  14. You should have received a copy of the GNU General Public License
  15. along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17.  
  18. //dependencies
  19. var util = require('../../../util.js');
  20. var async = require('async');
  21.  
  22. module.exports = function(pb) {
  23.  
  24. //pb dependencies
  25. var DAO = pb.DAO;
  26.  
  27. /**
  28. * Retrieves the necessary data as well as prepares the layout so a view
  29. * loader can complete the render of content
  30. * @class ArticleRenderer
  31. * @constructor
  32. * @param {Object} context
  33. * @param {String} context.hostname
  34. * @param {String} context.site
  35. * @param {Boolean} context.onlyThisSite
  36. * @param {UserService} [context.userService]
  37. */
  38. function ArticleRenderer(context) {
  39. if (context) {
  40.  
  41. /**
  42. * @property commentService
  43. * @type {CommentService}
  44. */
  45. this.commentService = new pb.CommentService(context);
  46.  
  47. /**
  48. * @property hostname
  49. * @type {string}
  50. */
  51. this.hostname = context.hostname;
  52. }
  53.  
  54. /**
  55. * @property site
  56. * @type {string}
  57. */
  58. this.site = pb.SiteService.getCurrentSite(context.site);
  59.  
  60. /**
  61. * @property onlyThisSite
  62. * @type {boolean}
  63. */
  64. this.onlyThisSite = context.onlyThisSite;
  65.  
  66. /**
  67. * Instance of user service. No context is provided and therefore can only be used
  68. * @property userService
  69. * @type {UserService}
  70. */
  71. this.userService = context.userService || new pb.UserService(context);
  72. }
  73.  
  74. /**
  75. * @private
  76. * @static
  77. * @readonly
  78. * @property READ_MORE_FLAG
  79. * @type {String}
  80. */
  81. var READ_MORE_FLAG = '^read_more^';
  82.  
  83. /**
  84. * @private
  85. * @static
  86. * @readonly
  87. * @property ANONYMOUS_COMMENTER
  88. * @type {String}
  89. */
  90. var ANONYMOUS_COMMENTER = 'Anonymous';
  91.  
  92. /**
  93. * @method render
  94. * @param {Object} content
  95. * @param {Object} context
  96. * @param {Object} [context.authors] A hash of user objects representing the
  97. * authors of the content to be rendered
  98. * @param {Object} context.contentSettings The content settings
  99. * @param {Integer} context.contentCount An integer representing the total
  100. * number of content objects that will be processed for this request
  101. * @param {Boolean} [context.renderBylines=true]
  102. * @param {Boolean} [context.renderTimestamp=true]
  103. * @param {Boolean} [context.renderComments=true]
  104. * @param {Boolean} [context.readMore=false]
  105. * @param {Function} cb
  106. */
  107. ArticleRenderer.prototype.render = function(content, context, cb) {
  108. if (!util.isObject(content)) {
  109. return cb(new Error('content parameter must be a valid object'));
  110. }
  111. else if (!util.isObject(context)) {
  112. return cb(new Error('context parameter must be a valid object'));
  113. }
  114. else if (!util.isObject(context.authors) && context.renderBylines !== false) {
  115. return cb(new Error('context.authors parameter must be a valid hash of users when the bylines will be rendered'));
  116. }
  117. else if (!util.isObject(context.contentSettings)) {
  118. return cb(new Error('context.contentSettings parameter must be a valid hash of content settings'));
  119. }
  120. else if (isNaN(context.contentCount)) {
  121. return cb(new Error('context.contentCount parameter must be a valid integer greater than 0'));
  122. }
  123.  
  124. if (context.renderBylines !== false) {
  125. this.formatBylines(content, context);
  126. }
  127. if (context.renderTimestamp !== false) {
  128. this.formatTimestamp(content, context);
  129. }
  130. this.formatLayout(content, context);
  131.  
  132. //build out task list
  133. var tasks = [
  134. util.wrapTask(this, this.formatMediaReferences, [content, context])
  135. ];
  136.  
  137. //render comments unless explicitly asked not too
  138. if (context.renderComments !== false) {
  139. tasks.push(util.wrapTask(this, this.formatComments, [content, context]));
  140. }
  141.  
  142. //run the tasks
  143. async.parallel(tasks, function(err/*, results*/) {
  144. cb(err, content);
  145. });
  146. };
  147.  
  148. /**
  149. * @method formatBylines
  150. * @param {Object} content
  151. * @param {Object} context
  152. */
  153. ArticleRenderer.prototype.formatBylines = function(content, context) {
  154.  
  155. var author = context.authors[content.author];
  156. if (util.isNullOrUndefined(author)) {
  157. pb.log.warn('ArticleRenderer: Failed to find author [%s] for content [%s]', content.author, content[DAO.getIdField()]);
  158. return;
  159. }
  160.  
  161. var contentSettings = context.contentSettings;
  162. if(author.photo && contentSettings.display_author_photo) {
  163. content.author_photo = author.photo;
  164. content.media_body_style = '';
  165. }
  166.  
  167. content.author_name = this.userService.getFormattedName(author);
  168. content.author_position = '';
  169. if (author.position && contentSettings.display_author_position) {
  170. content.author_position = author.position;
  171. }
  172. };
  173.  
  174. /**
  175. * @method formatTimestamp
  176. * @param {Object} content
  177. * @param {Object} context
  178. */
  179. ArticleRenderer.prototype.formatTimestamp = function(content, context) {
  180. if(context.contentSettings.display_timestamp ) {
  181. content.timestamp = pb.ContentService.getTimestampTextFromSettings(
  182. content.publish_date,
  183. context.contentSettings
  184. );
  185. }
  186. };
  187.  
  188. /**
  189. * @method formatLayout
  190. * @param {Object} content
  191. * @param {Object} context
  192. */
  193. ArticleRenderer.prototype.formatLayout = function(content, context) {
  194. var contentSettings = context.contentSettings;
  195.  
  196. if(this.containsReadMoreFlag(content)) {
  197. this.formatLayoutForReadMore(content, context);
  198. }
  199. else if(context.readMore && contentSettings.auto_break_articles) {
  200. this.formatAutoBreaks(content, context);
  201. }
  202. };
  203.  
  204. /**
  205. * @method formatMediaReferences
  206. * @param {Object} content
  207. * @param {Object} context
  208. * @param {Function} cb
  209. */
  210. ArticleRenderer.prototype.formatMediaReferences = function(content, context, cb) {
  211. var self = this;
  212.  
  213. content.layout = this.getLayout(content);
  214. var mediaLoader = new pb.MediaLoader({site: self.site, onlyThisSite: self.onlyThisSite});
  215. mediaLoader.start(content.layout, function(err, newLayout) {
  216. content.layout = newLayout;
  217. self.setLayout(content, undefined);
  218. cb(err);
  219. });
  220. };
  221.  
  222. /**
  223. * @method formatComments
  224. * @param {Object} content
  225. * @param {Object} context
  226. * @param {Function} cb
  227. */
  228. ArticleRenderer.prototype.formatComments = function(content, context, cb) {
  229. var self = this;
  230. if (!pb.ArticleService.allowComments(context.contentSettings, content)) {
  231. return cb(null);
  232. }
  233.  
  234. var opts = {
  235. where: {
  236. article: content[pb.DAO.getIdField()] + ''
  237. },
  238. order: [['created', pb.DAO.ASC]]
  239. };
  240. this.commentService.getAll(opts, function(err, comments) {
  241. if(util.isError(err) || comments.length === 0) {
  242. return cb(err);
  243. }
  244.  
  245. self.getCommenters(comments, context.contentSettings, function(err, commentsWithCommenters) {
  246. content.comments = commentsWithCommenters;
  247. cb(null, null);
  248. });
  249. });
  250. };
  251.  
  252. /**
  253. * Retrieves the commenters for an array of comments
  254. *
  255. * @method getCommenters
  256. * @param {Array} comments Array of comment objects
  257. * @param {Object} contentSettings Content settings to use for processing
  258. * @param {Function} cb Callback function
  259. */
  260. ArticleRenderer.prototype.getCommenters = function(comments, contentSettings, cb) {
  261. var self = this;
  262.  
  263. //callback for iteration to handle setting the commenter attributes
  264. var processComment = function(comment, commenter) {
  265. comment.commenter_name = ANONYMOUS_COMMENTER;
  266. comment.timestamp = pb.ContentService.getTimestampTextFromSettings(comment.created, contentSettings);
  267.  
  268. if (commenter) {
  269. comment.commenter_name = self.userService.getFormattedName(commenter);
  270. if(commenter.photo) {
  271. comment.commenter_photo = commenter.photo;
  272. }
  273. if(commenter.position) {
  274. comment.commenter_position = commenter.position;
  275. }
  276. }
  277. };
  278.  
  279. var processedComments = [];
  280. var users = {};
  281. var tasks = util.getTasks(comments, function(comments, i) {
  282. return function(callback) {
  283.  
  284. var comment = comments[i];
  285. if (!comment.commenter || users[comment.commenter]) {
  286.  
  287. //user already commented so pull locally
  288. processComment(comment, users[comment.commenter]);
  289. processedComments.push(comment);
  290. callback(null, true);
  291. return;
  292. }
  293.  
  294. //user has not already commented so load
  295. var dao = new pb.DAO();
  296. dao.loadById(comment.commenter, 'user', function(err, commenter) {
  297. if(util.isError(err) || commenter === null) {
  298. callback(null, false);
  299. return;
  300. }
  301.  
  302. //process the comment
  303. users[commenter[pb.DAO.getIdField()].toString()] = commenter;
  304. processComment(comment, commenter);
  305. processedComments.push(comment);
  306.  
  307. callback(null, true);
  308. });
  309. };
  310. });
  311. async.series(tasks, function(err, result) {
  312. cb(err, processedComments);
  313. });
  314. };
  315.  
  316. /**
  317. * @method formatAutoBreak
  318. * @param {Object} content
  319. * @param {Object} context
  320. */
  321. ArticleRenderer.prototype.formatAutoBreaks = function(content, context) {
  322. var contentSettings = context.contentSettings;
  323. var breakString = '<br>';
  324. var tempLayout;
  325. var layout = this.getLayout(content);
  326.  
  327. // Firefox uses br and Chrome uses div in content editables.
  328. // We need to see which one is being used
  329. var brIndex = layout.indexOf('<br>');
  330. if(brIndex === -1) {
  331. brIndex = layout.indexOf('<br />');
  332. breakString = '<br />';
  333. }
  334. var divIndex = layout.indexOf('</div>');
  335.  
  336. // Temporarily replace double breaks with a directive so we don't mess up the count
  337. if(divIndex === -1 || (brIndex > -1 && divIndex > -1 && brIndex < divIndex)) {
  338. tempLayout = layout.split(breakString + breakString).join(breakString + '^dbl_pgf_break^');
  339. }
  340. else {
  341. breakString = '</div>';
  342. tempLayout = layout.split('<div><br></div>').join(breakString + '^dbl_pgf_break^')
  343. .split('<div><br /></div>').join(breakString + '^dbl_pgf_break^');
  344. }
  345.  
  346. // Split the layout by paragraphs and remove any empty indices
  347. var tempLayoutArray = tempLayout.split(breakString);
  348. for(var i = 0; i < tempLayoutArray.length; i++) {
  349. if(!tempLayoutArray[i].length) {
  350. tempLayoutArray.splice(i, 1);
  351. i--;
  352. }
  353. }
  354.  
  355. // Only continue if we have more than 1 paragraph
  356. if(tempLayoutArray.length > 1) {
  357. var newLayout = '';
  358.  
  359. // Cutoff the content at the right number of paragraphs
  360. for(i = 0; i < tempLayoutArray.length && i < contentSettings.auto_break_articles; i++) {
  361. if(i === contentSettings.auto_break_articles - 1 && i !== tempLayoutArray.length - 1) {
  362.  
  363. newLayout += tempLayoutArray[i] + this.getReadMoreSpan(content, contentSettings.read_more_text) + breakString;
  364. continue;
  365. }
  366. newLayout += tempLayoutArray[i] + breakString;
  367. }
  368.  
  369. if(breakString === '</div>') {
  370. breakString = '<div><br /></div>';
  371. }
  372.  
  373. // Replace the double breaks
  374. newLayout = newLayout.split('^dbl_pgf_break^').join(breakString);
  375.  
  376. this.setLayout(content, newLayout);
  377. }
  378. };
  379.  
  380. /**
  381. * @method formatLayoutForReadMore
  382. * @param {Object} content
  383. * @param {Object} context
  384. * @param {boolean} context.readMore
  385. */
  386. ArticleRenderer.prototype.formatLayoutForReadMore = function(content, context) {
  387. var layout = this.getLayout(content);
  388.  
  389. if(context.readMore) {
  390. var beforeReadMore = layout.substr(0, layout.indexOf(READ_MORE_FLAG));
  391. layout = beforeReadMore + this.getReadMoreSpan(content, context.contentSettings.read_more_text);
  392. }
  393. else {
  394. layout = layout.split(READ_MORE_FLAG).join('');
  395. }
  396. this.setLayout(content, layout);
  397. };
  398.  
  399. /**
  400. *
  401. * @method getReadMoreSpan
  402. * @param {Object} content
  403. * @param {String} anchorContent
  404. * @return {String}
  405. */
  406. ArticleRenderer.prototype.getReadMoreSpan = function(content, anchorContent) {
  407. return '&nbsp;<span class="read_more">' + this.getReadMoreLink(content, anchorContent) + '</span>';
  408. };
  409.  
  410. /**
  411. * @method getReadMoreLink
  412. * @param {Object} content
  413. * @param {String} anchorContent
  414. * @return {String}
  415. */
  416. ArticleRenderer.prototype.getReadMoreLink = function(content, anchorContent) {
  417.  
  418. var path = pb.UrlService.urlJoin(this.getContentLinkPrefix() + content.url);
  419. return '<a href="' + pb.UrlService.createSystemUrl(path, { hostname: this.hostname }) + '">' + anchorContent + '</a>';
  420. };
  421.  
  422. /**
  423. * @method getContentLinkPrefix
  424. * @return {String}
  425. */
  426. ArticleRenderer.prototype.getContentLinkPrefix = function() {
  427. return '/article/';
  428. };
  429.  
  430. /**
  431. * Retrieves the layout from the content object. Provides a mechanism to
  432. * allow for layout parameter to have any name.
  433. * @method getLayout
  434. * @param {Object} content
  435. * @return {String}
  436. */
  437. ArticleRenderer.prototype.getLayout = function(content) {
  438. return content.article_layout;
  439. };
  440.  
  441. /**
  442. * A workaround to allow this prototype to operate on articles and pages.
  443. * The layout parameter is not the same. Until we introduce breaking
  444. * changes this will have to do.
  445. * @method setLayout
  446. * @param {Object} content
  447. * @param {String} layout
  448. */
  449. ArticleRenderer.prototype.setLayout = function(content, layout) {
  450. content.article_layout = layout;
  451. };
  452.  
  453. /**
  454. * @method containsReadMoreFlag
  455. * @param {Object} content
  456. * @return {Boolean}
  457. */
  458. ArticleRenderer.prototype.containsReadMoreFlag = function(content) {
  459. if (!util.isObject(content)) {
  460. throw new Error('The content parameter must be an object');
  461. }
  462. return this.getLayout(content).indexOf(READ_MORE_FLAG) > -1;
  463. };
  464.  
  465. return ArticleRenderer;
  466. };
  467.