Improve annotation with glossary terms

This commit is contained in:
Samy Pesse
2016-02-17 21:03:00 +01:00
parent a68364d4d7
commit e75002a96d
3 changed files with 160 additions and 12 deletions
+103 -9
View File
@@ -6,14 +6,8 @@ var slug = require('github-slugid');
var Promise = require('../utils/promise');
var location = require('../utils/location');
// Render a cheerio DOM as html
function renderDOM($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
}
// Selector to ignore
var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script';
function HTMLPipeline(htmlString, opts) {
_.bindAll(this);
@@ -29,7 +23,13 @@ function HTMLPipeline(htmlString, opts) {
onCodeBlock: _.identity,
// Output a svg, if returns null the svg is kept inlined
onOutputSVG: _.constant(null)
onOutputSVG: _.constant(null),
// Words to annotate
annotations: [],
// When an annotation is applied
onAnnotation: function () { }
});
this.$ = cheerio.load(htmlString, {
@@ -146,6 +146,33 @@ HTMLPipeline.prototype.transformSvgs = function() {
});
};
// Annotate the content
HTMLPipeline.prototype.applyAnnotations = function() {
var that = this;
_.each(this.opts.annotations, function(annotation) {
var searchRegex = new RegExp( '\\b(' + pregQuote(annotation.name.toLowerCase()) + ')\\b' , 'gi' );
that.$('*').each(function() {
var $this = that.$(this);
if (
$this.is(ANNOTATION_IGNORE) ||
$this.parents(ANNOTATION_IGNORE).length > 0
) return;
replaceText(that.$, this, searchRegex, function(match) {
that.opts.onAnnotation(annotation);
return '<a href="'+that.opts.onRelativeLink(annotation.href) + '#' + annotation.id+'" '
+ 'class="glossary-term" title="'+_.escape(annotation.description)+'">'
+ match
+ '</a>';
});
});
});
};
// Write content to the pipeline
HTMLPipeline.prototype.output = function() {
var that = this;
@@ -156,9 +183,76 @@ HTMLPipeline.prototype.output = function() {
.then(this.transformHeadings)
.then(this.transformCodeBlocks)
.then(this.transformSvgs)
.then(this.applyAnnotations)
.then(function() {
return renderDOM(that.$);
});
};
// Render a cheerio DOM as html
function renderDOM($, dom, options) {
if (!dom && $._root && $._root.children) {
dom = $._root.children;
}
options = options|| dom.options || $._options;
return domSerializer(dom, options);
}
// Replace text in an element
function replaceText($, el, search, replace, text_only ) {
return $(el).each(function(){
var node = this.firstChild,
val,
new_val,
// Elements to be removed at the end.
remove = [];
// Only continue if firstChild exists.
if ( node ) {
// Loop over all childNodes.
while (node) {
// Only process text nodes.
if ( node.nodeType === 3 ) {
// The original node value.
val = node.nodeValue;
// The new value.
new_val = val.replace( search, replace );
// Only replace text if the new value is actually different!
if ( new_val !== val ) {
if ( !text_only && /</.test( new_val ) ) {
// The new value contains HTML, set it in a slower but far more
// robust way.
$(node).before( new_val );
// Don't remove the node yet, or the loop will lose its place.
remove.push( node );
} else {
// The new value contains no HTML, so it can be set in this
// very fast, simple way.
node.nodeValue = new_val;
}
}
}
node = node.nextSibling;
}
}
// Time to remove those elements!
if (remove.length) $(remove).remove();
});
}
function pregQuote( str ) {
return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
}
module.exports = HTMLPipeline;
+16 -2
View File
@@ -147,7 +147,11 @@ Page.prototype.toHTML = function(output) {
// Normalize HTML output
.then(function() {
var pipelineOpts = {
onRelativeLink: _.partial(output.onRelativeLink, that),
onRelativeLink: function (href) {
href = that.relative(href);
return output.onRelativeLink(that, href);
},
//_.partial(output.onRelativeLink, that),
onImage: _.partial(output.onOutputImage, that),
onOutputSVG: _.partial(output.onOutputSVG, that),
@@ -159,7 +163,17 @@ Page.prototype.toHTML = function(output) {
language: lang
}
});
}
},
// Convert glossary entries to annotations
annotations: _.map(that.book.glossary.entries, function(entry) {
return {
id: entry.id,
name: entry.name,
description: entry.description,
href: '/GLOSSARY.html'
};
})
};
var pipeline = new HTMLPipeline(that.content, pipelineOpts);
+41 -1
View File
@@ -9,6 +9,10 @@ describe('Page', function() {
'heading.md': '# Hello\n\n## World',
'links.md': '[link](hello.md) [link 2](variables/page/next.md) [readme](README.md)',
'annotations/simple.md': 'A magicien say abracadabra!',
'annotations/code.md': 'A magicien say `abracadabra`!',
'annotations/class.md': 'A magicien say <div class="no-glossary"><b>abracadabra</b>, right?</div>!',
'codes/simple.md': '```hello world```',
'codes/lang.md': '```js\nhello world\n```',
'codes/lang.adoc': '```js\nhello world\n```',
@@ -19,7 +23,9 @@ describe('Page', function() {
'variables/file/path.md': '{{ file.path }}',
'variables/page/title.md': '{{ page.title }}',
'variables/page/previous.md': '{{ page.previous.title }} {{ page.previous.path }}',
'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}'
'variables/page/next.md': '{{ page.next.title }} {{ page.next.path }}',
'GLOSSARY.md': '# Glossary\n\n\n## abracadabra\n\nthis is the description'
}, [
{
title: 'Test page.next',
@@ -186,4 +192,38 @@ describe('Page', function() {
.should.be.fulfilledWith('<p>Test Variables variables/page/title.md</p>\n');
});
});
describe('Annotations / Glossary', function() {
it('should replace glossary terms', function() {
return book.addPage('annotations/simple.md').toHTML(output)
.should.finally.be.html({
'.glossary-term': {
count: 1,
text: 'abracadabra',
attributes: {
title: 'this is the description',
href: '../GLOSSARY.html#abracadabra'
}
}
});
});
it('should not replace terms in code blocks', function() {
return book.addPage('annotations/code.md').toHTML(output)
.should.finally.be.html({
'.glossary-term': {
count: 0
}
});
});
it('should not replace terms in ".no-glossary"', function() {
return book.addPage('annotations/class.md').toHTML(output)
.should.finally.be.html({
'.glossary-term': {
count: 0
}
});
});
});
});