Refactors Liquid syntax highlighters to add line highlights.

Usage (ranges are space separated):

{% highlight js 1,4-6 %}
One range
Adds `highlight-line-active` to lines 1,4,5,6

{% highlight js 3-4 -1 %}
Two ranges (add/remove), remove is N/A
Adds `highlight-line-add` to lines 3,4

{% highlight js -1 3-4 %}
Two ranges (add/remove), add is N/A
Adds `highlight-line-remove` to lines 3,4

{% highlight js 3-4 5,8-10 %}
Two ranges, both are used
Adds `highlight-line-add` to lines 3-4
Adds `highlight-line-remove` to lines 5,8,9,10
master
Zach Leatherman 2018-01-26 22:12:46 -06:00
parent e408e2fe50
commit 963b5d46e6
8 changed files with 164 additions and 72 deletions

View File

@ -1,5 +1,5 @@
const { DateTime } = require("luxon");
const liquidjsSyntaxHighlighter = require("./_src/eleventy-liquidjs-tag-highlight-prismjs");
const highlighters = require("./_src/eleventy-liquidjs-tag-highlight");
function dateToISO(dateObj) {
return DateTime.fromJSDate(dateObj).toISO({ includeOffset: true, suppressMilliseconds: true });
@ -25,7 +25,7 @@ module.exports = function(eleventyConfig) {
});
// compatibility with existing {% highlight js %} and others
eleventyConfig.addLiquidTag("highlight", liquidjsSyntaxHighlighter);
eleventyConfig.addLiquidTag("highlight", highlighters.prismjs);
// only content in the `posts/` directory
eleventyConfig.addCollection("posts", function(collection) {

33
_src/HighlightLines.js Normal file
View File

@ -0,0 +1,33 @@
class HighlightLines {
constructor(rangeStr) {
this.highlights = this.convertRangeToHash(rangeStr);
}
convertRangeToHash(rangeStr) {
let hash = {};
if( !rangeStr ) {
return hash;
}
let ranges = rangeStr.split(",").map(function(range) {
return range.trim();
});
for(let range of ranges) {
let startFinish = range.split('-');
let start = parseInt(startFinish[0], 10);
let end = parseInt(startFinish[1] || start, 10);
for( let j = start, k = end; j<=k; j++ ) {
hash[j] = true;
}
}
return hash;
}
isHighlighted(lineNumber) {
return !!this.highlights[lineNumber]
}
}
module.exports = HighlightLines;

83
_src/LiquidHighlight.js Normal file
View File

@ -0,0 +1,83 @@
const HighlightLines = require('./HighlightLines');
class LiquidHighlight {
constructor(liquidEngine) {
this.liquidEngine = liquidEngine;
this.hooks = [];
this.classHooks = [];
}
addHook(hookFunction) {
this.hooks.push(hookFunction);
}
addClassHook(hookFunction) {
this.classHooks.push(hookFunction);
}
getObject() {
let ret = function(highlighter) {
return {
parse: function(tagToken, remainTokens) {
let split = tagToken.args.split(" ");
this.language = split[0];
this.highlights = new HighlightLines(split.length === 2 ? split[1] : "");
this.highlightsAdd = new HighlightLines(split.length === 3 ? split[1] : "");
this.highlightsRemove = new HighlightLines(split.length === 3 ? split[2] : "");
this.tokens = [];
var stream = highlighter.liquidEngine.parser.parseStream(remainTokens);
stream
.on('token', token => {
if (token.name === 'endhighlight') {
stream.stop();
} else {
this.tokens.push(token);
}
})
.on('end', x => {
throw new Error("tag highlight not closed");
});
stream.start();
},
render: function(scope, hash) {
let tokens = this.tokens.map(token => token.raw);
let tokenStr = tokens.join('').trim();
for( let hook of highlighter.hooks ) {
tokenStr = hook.call(this, this.language, tokenStr);
}
let lines = tokenStr.split("\n").map(function(line, j) {
let classHookClasses = [];
for( let classHook of highlighter.classHooks ) {
let ret = classHook(this.language, line, j);
if( ret ) {
classHookClasses.push(ret);
}
}
return '<div class="highlight-line' +
(this.highlights.isHighlighted(j) ? ' highlight-line-active' : '') +
(this.highlightsAdd.isHighlighted(j) ? ' highlight-line-add' : '') +
(this.highlightsRemove.isHighlighted(j) ? ' highlight-line-remove' : '') +
(classHookClasses.length ? " " + classHookClasses.join(" ") : "") +
'">' +
line +
'</div>';
}.bind(this));
return Promise.resolve(`<pre class="language-${this.language}"><code class="language-${this.language}">` + lines.join("") + "</code></pre>");
}
};
};
return ret(this);
}
}
module.exports = LiquidHighlight;

View File

@ -1,32 +0,0 @@
module.exports = function(liquidEngine) {
return {
parse: function(tagToken, remainTokens) {
this.language = tagToken.args;
this.tokens = [];
var stream = liquidEngine.parser.parseStream(remainTokens);
stream
.on('token', token => {
if (token.name === 'endhighlight') {
stream.stop();
} else {
this.tokens.push(token);
}
})
.on('end', x => {
throw new Error("tag highlight not closed");
});
stream.start();
},
render: function(scope, hash) {
var tokens = this.tokens.map(token => {
return token.raw.trim();
}).join('').trim();
return Promise.resolve(`<pre class="language-${this.language}"><code class="language-${this.language}">\n` + tokens + "\n</code></pre>");
}
}
};

View File

@ -1,37 +0,0 @@
const Prism = require('prismjs');
module.exports = function(liquidEngine) {
let langMap = {
"css": "css",
"html": "markup",
"js": "javascript"
};
return {
parse: function(tagToken, remainTokens) {
this.language = langMap[ tagToken.args ] || tagToken.args;
this.tokens = [];
var stream = liquidEngine.parser.parseStream(remainTokens);
stream
.on('token', token => {
if (token.name === 'endhighlight') {
stream.stop();
} else {
this.tokens.push(token);
}
})
.on('end', x => {
throw new Error("tag highlight not closed");
});
stream.start()
},
render: function(scope, hash) {
var tokens = this.tokens.map(token => token.raw).join('').trim();
var html = Prism.highlight(tokens, Prism.languages[ this.language ]);
return Promise.resolve(`<pre class="language-${this.language}"><code class="language-${this.language}">` + html + "</code></pre>");
}
}
};

View File

@ -0,0 +1,28 @@
const Prism = require('prismjs');
const LiquidHighlight = require( "./LiquidHighlight" );
module.exports = {
plain: function(liquidEngine) {
let highlight = new LiquidHighlight(liquidEngine);
highlight.addClassHook(function(language, line) {
if( language === "dir" ) {
// has trailing slash
if( line.match(/\/$/) !== null ) {
return "highlight-line-isdir";
}
}
});
return highlight.getObject();
},
prismjs: function(liquidEngine) {
let highlight = new LiquidHighlight(liquidEngine);
highlight.addHook(function(language, htmlStr, lines) {
return Prism.highlight(htmlStr, Prism.languages[ language ]);
});
return highlight.getObject();
}
};

View File

@ -73,6 +73,23 @@ pre {
margin: .5em 0;
background-color: #f6f6f6;
}
.highlight-line {
padding: 0.125em 1em; /* 2px 16px /16 */
}
.highlight-line-isdir {
color: #b0b0b0;
background-color: #222;
}
.highlight-line-active {
background-color: #444;
background-color: hsla(0, 0%, 27%, .8);
}
.highlight-line-add {
background-color: #45844b;
}
.highlight-line-remove {
background-color: #902f2f;
}
/* Header */
.home {

View File

@ -17,7 +17,7 @@ code[class*="language-"], pre[class*="language-"] {
color: #f8f8f2;
}
pre[class*="language-"] {
padding: 1em;
padding: 1.5em 0;
margin: .5em 0;
overflow: auto;
}