Skip to content

Commit

Permalink
Merge pull request #88 from megadoc/prismjs
Browse files Browse the repository at this point in the history
Perform syntax-highlighting at compile-time
  • Loading branch information
amireh committed May 19, 2016
2 parents 394ed44 + 4ee4cfe commit 5b7f809
Show file tree
Hide file tree
Showing 21 changed files with 251 additions and 186 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ selection.json
# runtime junk
npm-debug.log
/examples/*/public
/megadoc.sublime-workspace
5 changes: 3 additions & 2 deletions doc/usage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ not be presenting anything as we haven't fed it any input yet.
Hopefully, the command ran successfully and you see some files written to
`public/docs`:

```shell
```bash
$ ls public/docs
. .. 404.html config.js index.html styles.css megadoc.js megadoc__vendor.js
. .. 404.html config.js
index.html styles.css megadoc.js megadoc__vendor.js
```

Okay, beef time!
Expand Down
154 changes: 49 additions & 105 deletions lib/Renderer.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
var marked = require('marked');
var _ = require('lodash');
var RendererUtils = require('./RendererUtils');
var multiline = require('multiline-slash');
var renderHeading = require('./Renderer__renderHeading');
var CodeRenderer = require('./Renderer__renderCode');
var LinkRenderer = require('./Renderer__renderLink');
var assign = _.assign;

var AnchorableHeadingTmpl = _.template(
multiline(function() {;
// <h<%- level %> class="anchorable-heading">
// <a name="<%- id %>" class="anchorable-heading__anchor"></a>
// <a href="#<%- id %>" class="anchorable-heading__link icon icon-link"></a><span class="anchorable-heading__text"><%= text %></span>
// </h<%- level %>>
})
);

var HeadingTmpl = _.template(
multiline(function() {;
// <h<%- level %>>
// <%= text %>
// </h<%- level %>>
})
);

var RE_INTERNAL_LINK = Object.freeze(/^mega:\/\//);

function createRenderer(config) {
function Renderer(config) {
var renderer = new marked.Renderer();
var markedOptions = Object.freeze({
renderer: renderer,
Expand All @@ -35,118 +18,89 @@ function createRenderer(config) {
pedantic: false,
});

var state;
var runOptions;
var runState, runOptions;
var inSinglePageMode = config.layoutOptions.singlePageMode;
var codeBlockRenderers = {};
var originalCodeRenderer = renderer.code;
var renderCode = CodeRenderer(config.syntaxHighlighting);
var renderLink = LinkRenderer(config);

function createState(baseURL) {
return { baseURL: baseURL || '', toc: [] };
function createRunState(baseURL) {
return { baseURL: inSinglePageMode ? (baseURL || '') : null, toc: [] };
}

function createRunOptions(options) {
options = options || {};

return { anchorableHeadings: options.anchorableHeadings !== false };
return { anchorableHeadings: !options || options.anchorableHeadings !== false };
}

// this could be heavily optimized, but meh for now
renderer.heading = function(text, level) {
var scopedId = RendererUtils.normalizeHeading(
RendererUtils.htmlToText(text.split('\n')[0])
);

var id = inSinglePageMode ? joinBySlash(state.baseURL, scopedId) : scopedId;

state.toc.push({
id: id,
scopedId: scopedId,
level: level,
html: text,
text: RendererUtils.markdownToText(text).trim()
});

if (runOptions.anchorableHeadings && id && id.length) {
return AnchorableHeadingTmpl({
// we need to strip any leading # because in SinglePageMode all baseURL
// values will have that
id: stripLeadingHash(id),
level: level,
text: text,
});
}
else {
return HeadingTmpl({ level: level, text: text });
}
return renderHeading(text, level, runState, runOptions);
};

renderer.link = function(srcHref, title, text) {
var href = srcHref.replace(RE_INTERNAL_LINK, '');
var tagString = '<a href="' + href + '"';

if (title) {
tagString += ' title="' + _.escape(title) + '"';
}

if (href === srcHref && href[0] !== '#' && config.launchExternalLinksInNewTabs) {
tagString += ' target="_blank"';
}

return tagString + '>' + text + '</a>';
};
renderer.link = renderLink;

renderer.code = function(code, language) {
if (codeBlockRenderers[language]) {
return codeBlockRenderers[language](code);
}
else {
return originalCodeRenderer.apply(renderer, arguments);
return renderCode(code, language);
}
};

/**
* Render Markdown to HTML.
*
* @param {String} text
* Markdown text.
*
* @param {Object} options
* @param {String} [options.baseURL=null]
* The url to prefix the anchor links with. This is necessary only in
* the Single Page Mode because all anchors must be absolute then. In
* the regular mode, the anchors are shortened to be more friendly
* since they assume there won't be conflicts (because each "page" is
* expected to represent a single document.)
*
* Pass the documentNode's @meta.href value here if you're compiling
* from a corpus node.
*
* @param {Boolean} [options.anchorableHeadings=true]
* Turn this off if you do not want the headings to have anchors -
* the [name] attribute and the .anchorable-heading stuff.
*
* @param {Boolean} [options.withTOC=false]
* Turn this on if you want the ToC meta-data. The return value will
* be an object of the shape: `{ html: String, toc: Array.<Object> }`.
*
* @return {String|Object}
* The HTML.
*/
var exports = function renderMarkdown(text, options) {
var html;
var html, toc;

options = options || {};
state = createState(options.baseURL);
runState = createRunState(options.baseURL);
runOptions = createRunOptions(options);

html = marked(text, assign({}, markedOptions, {
sanitize: options.sanitize !== false
}));

toc = runState.toc;

if (options && options.trimHTML) {
html = RendererUtils.trimHTML(html);
}

state.baseURL = '';
runState = createRunState();
runOptions = createRunOptions();

return html;
return options.withTOC ? { html: html, toc: toc } : html;
};

exports.withTOC = function(text, options) {
var html, toc;

options = options || {};
state = createState(options.baseURL);
runOptions = createRunOptions(options);

html = marked(text, assign({}, markedOptions, {
sanitize: options.sanitize !== false
}));

toc = state.toc;

if (options && options.trimHTML) {
html = RendererUtils.trimHTML(html);
}

state = createState();
runOptions = createRunOptions();

return { html: html, toc: toc };
return exports(text, assign({ withTOC: true }, options));
};

/**
Expand All @@ -172,14 +126,4 @@ function createRenderer(config) {
return exports;
}

module.exports = createRenderer;

function stripLeadingHash(x) {
return x[0] === '#' ? x.slice(1) : x;
}

function joinBySlash(a, b) {
var shouldDelimit = a && a[a.length-1] !== '/';

return a + (shouldDelimit ? '/' : '') + b;
}
module.exports = Renderer;
38 changes: 38 additions & 0 deletions lib/Renderer__renderCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
var Prism = require('prismjs');
var assign = require('lodash').assign;

function CodeRenderer(userConfig) {
var config = assign({
languages: [],
aliases: {}
}, userConfig);

config.languages.forEach(function(name) {
try {
require('prismjs/components/prism-' + name);
}
catch(e) {
console.warn("SyntaxHighlighter: definition for language '%s' could not be found",
name
);
}
});

return function renderCode(code, language) {
var languageCode = config.aliases[language] || language;
var grammar = Prism.languages[languageCode];
var html = code;

if (grammar) {
html = Prism.highlight(code, grammar);
}

return (
'<pre class="language-' + (grammar ? languageCode : 'none') + '"><code>' +
html +
'</code></pre>'
);
};
}

module.exports = CodeRenderer;
62 changes: 62 additions & 0 deletions lib/Renderer__renderHeading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
var _ = require('lodash');
var RendererUtils = require('./RendererUtils');
var multiline = require('multiline-slash');

var AnchorableHeadingTmpl = _.template(
multiline(function() {;
// <h<%- level %> class="anchorable-heading">
// <a name="<%- id %>" class="anchorable-heading__anchor"></a>
// <a href="#<%- id %>" class="anchorable-heading__link icon icon-link"></a><span class="anchorable-heading__text"><%= text %></span>
// </h<%- level %>>
})
);

var HeadingTmpl = _.template(
multiline(function() {;
// <h<%- level %>>
// <%= text %>
// </h<%- level %>>
})
);

function renderHeading(text, level, state, runOptions) {
var scopedId = RendererUtils.normalizeHeading(
RendererUtils.htmlToText(text.split('\n')[0])
);

var id = state.baseURL ? joinBySlash(state.baseURL, scopedId) : scopedId;

// TODO: gief a markdown analyzer, really...
state.toc.push({
id: id,
scopedId: scopedId,
level: level,
html: text,
text: RendererUtils.markdownToText(text).trim()
});

if (runOptions.anchorableHeadings && id && id.length) {
return AnchorableHeadingTmpl({
// we need to strip any leading # because in SinglePageMode all baseURL
// values will have that
id: stripLeadingHash(id),
level: level,
text: text,
});
}
else {
return HeadingTmpl({ level: level, text: text });
}
};

function stripLeadingHash(x) {
return x[0] === '#' ? x.slice(1) : x;
}

function joinBySlash(a, b) {
var shouldDelimit = a && a[a.length-1] !== '/';

return a + (shouldDelimit ? '/' : '') + b;
}

module.exports = renderHeading;
21 changes: 21 additions & 0 deletions lib/Renderer__renderLink.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
var escapeHTML = require('lodash').escape;
var RE_INTERNAL_LINK = Object.freeze(/^mega:\/\//);

function LinkRenderer(config) {
return function renderLink(srcHref, title, text) {
var href = srcHref.replace(RE_INTERNAL_LINK, '');
var tagString = '<a href="' + href + '"';

if (title) {
tagString += ' title="' + escapeHTML(title) + '"';
}

if (href === srcHref && href[0] !== '#' && config.launchExternalLinksInNewTabs) {
tagString += ' target="_blank"';
}

return tagString + '>' + text + '</a>';
};
}

module.exports = LinkRenderer;
17 changes: 17 additions & 0 deletions lib/__tests__/Renderer.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var Renderer = require('../Renderer');
var assert = require('chai').assert;
var TestUtils = require('../TestUtils');
var multiline = require('multiline-slash');

describe('Renderer', function() {
var subject, spmSubject;
Expand Down Expand Up @@ -101,6 +102,22 @@ describe('Renderer', function() {
})
});

describe('rendering code blocks', function() {
it('performs syntax highlighting', function() {
var text = subject(multiline(function() {;
// # Hello
//
// ```javascript
// var foo = 'bar';
// ```
}, true));

assert.include(text.trim(),
'token keyword'
);
});
});

describe('#withTOC', function() {
var compiled;

Expand Down
Loading

0 comments on commit 5b7f809

Please sign in to comment.