Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Code highlighting feature #304

Closed
wants to merge 13 commits into from
19 changes: 17 additions & 2 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ All theme’s features are demonstrated in the [index.html](index.html) file. Us
- [Columns](#columns)
- [Tables](#tables)
- [Code](#code)
- [Highlight](#highlight)
- [Elements](#elements)
- [Cover](#cover)
- [Shout](#shout)
Expand Down Expand Up @@ -305,14 +306,28 @@ If you want to add lines numbers use next construction:
<code>}<code>
</pre>

When neccessary emphasize that code is commented, you need to use span element with `comment` class;
If you want to color part of code, wrap this part with `mark` to add yellow background and `mark` with `important` class to add red background;
When neccessary emphasize that code is commented, you need to use span element with `comment` class.

If you want to color part of code, wrap this part with `mark` to add yellow background and `mark` with `important` class to add red background.

<pre><code>function <mark>action()</mark> {
<span class="comment">// TODO<span>
return <mark class="important">true</mark>;
}</code></pre>

#### Highlight

You can automatically highlight code samples using [highlight.js](https://highlightjs.org/).

Add language definition to all `<pre>` code blocks you want to highlight.

<pre class="javascript"><code>function action() {
// TODO
return true;
}</code></pre>

Check all available languages and styles in [highlight.js demo](https://highlightjs.org/static/demo/).

### Elements

Expand Down
43 changes: 40 additions & 3 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const replace = require('gulp-replace');
const zip = require('gulp-zip');
const pages = require('gh-pages');
const sync = require('browser-sync').create();
const highlightCode = require('./highlightCode');

gulp.task('prepare', () => {

Expand All @@ -18,17 +19,23 @@ gulp.task('prepare', () => {
'!LICENSE.md',
'!README.md',
'!gulpfile.js',
'!highlightCode.js',
'!package.json',
'!package-lock.json'
])
.pipe(replace(
/(<link rel="stylesheet" href=")(node_modules\/shower-)([^\/]*)\/(.*\.css">)/g,
'$1shower/themes/$3/$4', { skipBinary: true }
))
.pipe(replace(
/(<link rel="stylesheet" href=")(node_modules\/highlight\.js\/styles\/)(.*\.css">)/g,
'$1shower/highlight.js/styles/$3', { skipBinary: true }
))
.pipe(replace(
/(<script src=")(node_modules\/shower-core\/)(shower.min.js"><\/script>)/g,
'$1shower/$3', { skipBinary: true }
));
))
.pipe(highlightCode());

const core = gulp.src([
'shower.min.js'
Expand All @@ -46,7 +53,7 @@ gulp.task('prepare', () => {
})
.pipe(rename( (path) => {
path.dirname = 'shower/themes/material/' + path.dirname;
}))
}));

const ribbon = gulp.src([
'**', '!package.json'
Expand All @@ -63,11 +70,41 @@ gulp.task('prepare', () => {
'$1../../$3', { skipBinary: true }
));

return merge(shower, core, themes)
const highlight = gulp.src([
'**', '!package.json'
], {
cwd: 'node_modules/highlight.js/styles'
})
.pipe(rename( (path) => {
path.dirname = 'shower/highlight.js/styles/' + path.dirname;
}));

return merge(shower, core, themes, highlight)
.pipe(gulp.dest('prepared'));

});

gulp.task('highlight', () => {
const STYLES_SRC_PATH = 'node_modules/highlight.js/styles';
const STYLES_DEST_PATH = 'shower/highlight.js/styles/';

const copyStyles = gulp.src(['**', '!package.json'], {cwd: STYLES_SRC_PATH})
.pipe(rename(path =>
path.dirname = STYLES_DEST_PATH + path.dirname
))
.pipe(gulp.dest('prepared'));

const highlightFiles = gulp.src(['prepared/*.html'])
.pipe(replace(
/(<link rel="stylesheet" href=")(node_modules\/highlight\.js\/styles)\/(.*\.css">)/g,
`$1${STYLES_DEST_PATH}$3`, { skipBinary: true }
))
.pipe(highlightCode())
.pipe(gulp.dest('.'));

return merge(copyStyles, highlightFiles);
});

gulp.task('clean', () => {
return del('prepared/**');
});
Expand Down
100 changes: 100 additions & 0 deletions highlightCode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const highlight = require('highlight.js');
const cheerio = require('cherio');
const Transform = require('stream').Transform;
const Vinyl = require('vinyl');

const AVAILABLE_LANGUAGES = highlight.listLanguages();

let $ = null;

function getCompiledLine(codeNode, highlightFunction) {
const nodeContents = $(codeNode).contents();

const lineParts = Array.from(nodeContents).map(subNode => {
if (subNode.type === 'text') {
const code = $(subNode).text();

if (!(code || '').trim()) {
return code;
}

return highlightFunction(code);
}

if (subNode.type === 'tag') {
return $.html(subNode);
}

return '';
});

return lineParts.join('');
}

function getCompiledCodeLines(language, preNode) {
const codeNodes = preNode.find('code');
const compiledLines = [];
let lastStack = null;

codeNodes.each((i, codeNode) => {
const className = $(codeNode).attr('class');
const code = getCompiledLine(codeNode, (code) => {
const {value, top} = highlight.highlight(language, code, false, lastStack);
lastStack = top;

return value;
});

compiledLines.push({code, className});
});

return compiledLines;
}

function getCompiledHtml() {
const preNodes = $('.slide pre[class]:has(code)');

preNodes.each((i, preNode) => {
const $node = $(preNode);
const langClass = ($node.attr('class') || '').trim().toLowerCase();

if (!AVAILABLE_LANGUAGES.includes(langClass)) {
return;
}

$node.removeAttr('class');

const compiledLines = getCompiledCodeLines(langClass, $node);

$node.html(compiledLines.map(({code, className}) => {
return `<code ${className ? 'class="' + className + '"' : ''}>${code}</code>`;
}).join(''));
});

return $.html();
}

module.exports = function() {
const transformStream = new Transform({objectMode: true});

transformStream._transform = (file, encoding, callback) => {
let error = null;

if (file.isBuffer() && file.path.endsWith('.html')) {
const content = String(file.contents);

$ = cheerio.load(content);

const compiledFile = new Vinyl({
path: file.path,
contents: Buffer.from(getCompiledHtml())
});

return callback(error, compiledFile);
}

return callback(null, file);
};

return transformStream;
};
14 changes: 14 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<title>Shower Presentation Engine</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="node_modules/highlight.js/styles/github.css">
<link rel="stylesheet" href="node_modules/shower-ribbon/styles/styles.css">
<style>
.shower {
Expand Down Expand Up @@ -113,6 +114,19 @@ <h2>Code samples</h2>
</pre>
</section>

<section class="slide">
<h2>Highlighted code samples</h2>
<pre class="javascript">
<code>if (subNode.type === 'text') {</code>
<code> const code = $(subNode).text();</code>
<code> if (!(code || '').trim()) {</code>
<code> return code;</code>
<code> }</code>
<code> highlightFunction(code);</code>
<code>}</code>
</pre>
</section>

<section class="slide">
<h2>Even tables</h2>
<table>
Expand Down