mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-03 12:10:34 +00:00
Improve annotation with glossary terms
This commit is contained in:
+103
-9
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user