diff --git a/lib/markdown.js b/lib/markdown.js index cdb6ee0..9649f86 100644 --- a/lib/markdown.js +++ b/lib/markdown.js @@ -1,7 +1,7 @@ 'use strict'; const hljs = require('highlight.js'); const utils = require('handlebars-utils'); -const Remarkable = require('remarkable'); +const {Remarkable} = require('remarkable'); /** * Expose markdown `helpers` (for performance we're using getters so @@ -55,7 +55,7 @@ Object.defineProperty(helpers, 'markdown', { * @api public */ -helpers.md = require('helper-md'); +helpers.md = require('./md.js'); helpers.helpersForMarkdown = function(config) { diff --git a/lib/md.js b/lib/md.js new file mode 100644 index 0000000..d01b87c --- /dev/null +++ b/lib/md.js @@ -0,0 +1,119 @@ +/*! + * helper-markdown + * + * Copyright (c) 2014 Jon Schlinkert, contributors. + * Licensed under the MIT license. + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); +const { Remarkable } = require('remarkable'); +var extend = require('extend-shallow'); +var exists = require('fs-exists-sync'); +var ent = require('ent'); + +/** + * Expose `md` helper + */ + +var helper = module.exports = function(name, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (typeof cb !== 'function') { + return helper.sync.apply(this, arguments); + } + + /* c8 ignore next 3 */ + if (typeof this === 'undefined' || typeof this.app === 'undefined') { + throw new Error('md async helper expects `app` to be exposed on the context'); + } + + var opts = extend({cwd: process.cwd()}, this.options, options); + opts = extend({}, opts, opts.hash); + var md = markdown(opts); + + var filepath = path.resolve(opts.cwd, name); + var view; + var str = ''; + + if (exists(filepath)) { + // create a collection to ensure middleware is consistent + this.app.create('mdfiles'); + str = fs.readFileSync(filepath, 'utf8'); + view = this.app.mdfile(filepath, {path: filepath, content: str}); + } else { + view = this.app.find(name); + } + /* c8 ignore next 4 */ + if (typeof view === 'undefined') { + cb(null, ''); + return; + } + + view.content = ent.decode(md.render(view.content)); + this.app.render(view, this.context, function(err, res) { + if (err) return cb(err); + cb(null, res.content); + }); +}; + +helper.sync = function(name, options) { + var ctx = this || {}; + var app = ctx.app || {}; + + var opts = extend({cwd: process.cwd()}, ctx.options, options); + opts = extend({}, opts, opts.hash); + var md = markdown(opts); + + var filepath = path.resolve(opts.cwd, name); + var view; + var html = ''; + var str = ''; + + if (exists(filepath)) { + str = fs.readFileSync(filepath, 'utf8'); + html = ent.decode(md.render(str)); + } else if (app.views) { + view = app.find(name); + if (view) { + html = view.content = ent.decode(md.render(view.content)); + } + } + + if (view && typeof view.compile === 'function') { + view.compile(opts); + var data = ctx.cache ? ctx.cache.data : {}; + ctx = extend({}, data, view.data); + return view.fn(ctx); + } + + if (typeof this.compile === 'function') { + var fn = this.compile(html); + return fn(this); + } + return html; +}; + +/** + * Shared settings for remarkable + * + * @param {Object} `options` + * @return {Object} + * @api private + */ + +function markdown(options) { + let optsConfig = options || {}; + optsConfig.breaks = true; + optsConfig.html = true; + optsConfig.langPrefix = 'lang-'; + optsConfig.typographer = false; + optsConfig.xhtmlOut = false; + const md = new Remarkable(optsConfig); + return md; +} \ No newline at end of file diff --git a/package.json b/package.json index c59055f..016272f 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "array-sort": "^1.0.0", "create-frame": "^1.0.0", "define-property": "^2.0.2", + "ent": "^2.2.0", "falsey": "^1.0.0", "for-in": "^1.0.2", "for-own": "^1.0.0", @@ -127,7 +128,6 @@ "handlebars-utils": "^1.0.6", "has-value": "^2.0.2", "helper-date": "^1.0.1", - "helper-md": "^0.2.2", "highlight.js": "^11.9.0", "html-tag": "^2.0.0", "is-even": "^1.0.0", @@ -138,6 +138,7 @@ "logging-helpers": "^1.0.0", "micromatch": "^4.0.5", "relative": "^3.0.2", + "remarkable": "^2.0.1", "striptags": "^3.2.0", "to-gfm-code-block": "^0.1.1", "year": "^0.2.1" @@ -145,6 +146,7 @@ "devDependencies": { "c8": "^9.1.0", "chai": "^4.3.10", + "lodash": "^4.17.21", "mocha": "^10.4.0", "rimraf": "^5.0.5", "template-helpers": "^1.0.1", diff --git a/test/fixtures/a.md b/test/fixtures/a.md new file mode 100644 index 0000000..54d773f --- /dev/null +++ b/test/fixtures/a.md @@ -0,0 +1,3 @@ +# AAA + +> this is aaa \ No newline at end of file diff --git a/test/fixtures/b.md b/test/fixtures/b.md new file mode 100644 index 0000000..6d1df08 --- /dev/null +++ b/test/fixtures/b.md @@ -0,0 +1,3 @@ +# BBB + +> this is bbb \ No newline at end of file diff --git a/test/fixtures/c.md b/test/fixtures/c.md new file mode 100644 index 0000000..ad10bc3 --- /dev/null +++ b/test/fixtures/c.md @@ -0,0 +1,3 @@ +# CCC + +This is {{name}} \ No newline at end of file diff --git a/test/fixtures/d.md b/test/fixtures/d.md new file mode 100644 index 0000000..e0cf766 --- /dev/null +++ b/test/fixtures/d.md @@ -0,0 +1,3 @@ +# DDD + +This is <%= name %> \ No newline at end of file diff --git a/test/fixtures/e.md b/test/fixtures/e.md new file mode 100644 index 0000000..eaf856b --- /dev/null +++ b/test/fixtures/e.md @@ -0,0 +1,6 @@ +# EEE + +``` +var message = 'This is an alert'; +alert(message); +``` \ No newline at end of file diff --git a/test/fs.js b/test/fs.js index caee99d..c11cf40 100644 --- a/test/fs.js +++ b/test/fs.js @@ -59,6 +59,7 @@ describe('fs', function() { path.join('lib', 'markdown.js'), path.join('lib', 'match.js'), path.join('lib', 'math.js'), + path.join('lib', 'md.js'), path.join('lib', 'misc.js'), path.join('lib', 'number.js'), path.join('lib', 'object.js'), @@ -86,6 +87,7 @@ describe('fs', function() { path.join('lib', 'markdown.js'), path.join('lib', 'match.js'), path.join('lib', 'math.js'), + path.join('lib', 'md.js'), path.join('lib', 'misc.js'), path.join('lib', 'number.js'), path.join('lib', 'object.js'), diff --git a/test/md.js b/test/md.js new file mode 100644 index 0000000..c50d3c0 --- /dev/null +++ b/test/md.js @@ -0,0 +1,147 @@ +/*! +* helper-md +* +* Copyright (c) 2014 Jon Schlinkert, contributors. +* Licensed under the MIT License +*/ + +'use strict'; + +require('mocha'); +var assert = require('assert'); +var handlebars = require('handlebars'); +var Templates = require('templates'); +var hljs = require('highlight.js'); +var md = require('../lib/md.js'); +var _ = require('lodash'); +var app; + +describe('sync', function() { + beforeEach(function() { + app = new Templates(); + + app.helper('md', md.sync); + app.engine('md', require('engine-base')); + app.option('engine', 'md'); + + app.create('page'); + app.create('partial', {viewType: ['partial']}); + app.create('include', {viewType: ['partial']}); + + app.include('one', {content: '# heading <%= name %>', data: {name: 'one'}}); + app.partial('two', {content: '# heading <%= name %>', data: {name: 'two'}}); + }); + + it('should convert markdown on the `content` property of a template to HTML:', function(cb) { + app.page('home.md', {content: '<%= md("one") %>'}); + + app.render('home.md', function(err, view) { + if (err) return cb(err); + assert.equal(view.content, '

heading one

\n'); + cb(); + }); + }); + + it('should support rendering markdown from a file:', function() { + assert.equal(md.sync('test/fixtures/a.md'), '

AAA

\n
\n

this is aaa

\n
\n'); + }); + + describe('handlebars:', function() { + it('should support rendering markdown from a file:', function() { + handlebars.registerHelper('md', md.sync); + assert.equal(handlebars.compile('{{{md "test/fixtures/a.md"}}}')(), '

AAA

\n
\n

this is aaa

\n
\n'); + }); + + it('should use the `render` function passed on the locals to render templates in partials :', function() { + handlebars.registerHelper('md', md.sync); + var locals = {name: 'CCC', compile: handlebars.compile}; + assert.equal(handlebars.compile('{{{md "test/fixtures/c.md"}}}')(locals), '

CCC

\n

This is CCC

\n'); + }); + }); +}); + +describe('async', function() { + beforeEach(function() { + app = new Templates(); + + app.asyncHelper('md', md); + app.engine('md', require('engine-base')); + app.option('engine', 'md'); + + app.create('page'); + app.create('partial', {viewType: ['partial']}); + app.create('include', {viewType: ['partial']}); + + app.include('one', {content: '# heading <%= name %>', data: {name: 'one'}}); + app.partial('two', {content: '# heading <%= name %>', data: {name: 'two'}}); + }); + + it('should convert markdown on the `content` property of a template to HTML:', function(cb) { + app.page('home.md', {content: '<%= md("one") %>'}); + + app.render('home.md', function(err, view) { + if (err) return cb(err); + assert.equal(view.content, '

heading one

\n'); + cb(); + }); + }); + + it('should support rendering from a file', function(cb) { + app.page('home.md', {content: '<%= md("test/fixtures/d.md") %>'}); + + app.render('home.md', {name: 'DDD'}, function(err, view) { + if (err) return cb(err); + assert.equal(view.content, '

DDD

\n

This is DDD

\n'); + cb(); + }); + }); + + it('should use sync helper when a callback is not passed:', function(cb) { + app.helper('md2', md); + app.page('home.md', {content: '<%= md2("one") %>'}); + + app.render('home.md', function(err, view) { + if (err) return cb(err); + assert.equal(view.content, '

heading one

\n'); + cb(); + }); + }); +}); + +describe('lodash:', function() { + it('should work as a lodash mixin:', function() { + _.mixin({md: md.sync}); + assert.equal(_.template('<%= _.md("test/fixtures/a.md") %>', {})(), '

AAA

\n
\n

this is aaa

\n
\n'); + }); + + it('should work when passed to lodash on the locals:', function() { + assert.equal(_.template('<%= _.md("test/fixtures/a.md") %>')({md: md.sync}), '

AAA

\n
\n

this is aaa

\n
\n'); + }); + + it('should work as a lodash import:', function() { + var settings = {imports: {md: md.sync}}; + assert.equal(_.template('<%= _.md("test/fixtures/a.md") %>', {}, settings)(), '

AAA

\n
\n

this is aaa

\n
\n'); + }); +}); + +describe('highlight:', function(argument) { + it('should support syntax highlighting', function() { + var actual = md.sync('test/fixtures/e.md', { + highlight: function(code, lang) { + try { + try { + return hljs.highlight(lang, code).value; + } catch (err) { + if (!/Unknown language/i.test(err.message)) { + throw err; + } + return hljs.highlightAuto(code).value; + } + } catch (err) { + return code; + } + } + }); + assert.equal(actual, '

EEE

\n
var message = \'This is an alert\';\nalert(message);\n
\n'); + }); +}); \ No newline at end of file