summaryrefslogtreecommitdiffstats
path: root/packages/gitbook/lib/output/modifiers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/gitbook/lib/output/modifiers')
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/addHeadingId.js26
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/annotateText.js46
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/fetchRemoteImages.js40
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/highlightCode.js60
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/inlinePng.js25
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/resolveLinks.js104
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/svgToImg.js25
-rw-r--r--packages/gitbook/lib/output/modifiers/__tests__/svgToPng.js33
-rw-r--r--packages/gitbook/lib/output/modifiers/addHeadingId.js23
-rw-r--r--packages/gitbook/lib/output/modifiers/annotateText.js94
-rw-r--r--packages/gitbook/lib/output/modifiers/editHTMLElement.js15
-rw-r--r--packages/gitbook/lib/output/modifiers/fetchRemoteImages.js44
-rw-r--r--packages/gitbook/lib/output/modifiers/highlightCode.js58
-rw-r--r--packages/gitbook/lib/output/modifiers/index.js15
-rw-r--r--packages/gitbook/lib/output/modifiers/inlineAssets.js29
-rw-r--r--packages/gitbook/lib/output/modifiers/inlinePng.js47
-rw-r--r--packages/gitbook/lib/output/modifiers/modifyHTML.js25
-rw-r--r--packages/gitbook/lib/output/modifiers/resolveImages.js33
-rw-r--r--packages/gitbook/lib/output/modifiers/resolveLinks.js53
-rw-r--r--packages/gitbook/lib/output/modifiers/svgToImg.js56
-rw-r--r--packages/gitbook/lib/output/modifiers/svgToPng.js53
21 files changed, 904 insertions, 0 deletions
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/addHeadingId.js b/packages/gitbook/lib/output/modifiers/__tests__/addHeadingId.js
new file mode 100644
index 0000000..a3b1d81
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/addHeadingId.js
@@ -0,0 +1,26 @@
+var cheerio = require('cheerio');
+var addHeadingId = require('../addHeadingId');
+
+describe('addHeadingId', function() {
+ it('should add an ID if none', function() {
+ var $ = cheerio.load('<h1>Hello World</h1><h2>Cool !!</h2>');
+
+ return addHeadingId($)
+ .then(function() {
+ var html = $.html();
+ expect(html).toBe('<h1 id="hello-world">Hello World</h1><h2 id="cool-">Cool !!</h2>');
+ });
+ });
+
+ it('should not change existing IDs', function() {
+ var $ = cheerio.load('<h1 id="awesome">Hello World</h1>');
+
+ return addHeadingId($)
+ .then(function() {
+ var html = $.html();
+ expect(html).toBe('<h1 id="awesome">Hello World</h1>');
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/annotateText.js b/packages/gitbook/lib/output/modifiers/__tests__/annotateText.js
new file mode 100644
index 0000000..67e7a10
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/annotateText.js
@@ -0,0 +1,46 @@
+var Immutable = require('immutable');
+var cheerio = require('cheerio');
+var GlossaryEntry = require('../../../models/glossaryEntry');
+var annotateText = require('../annotateText');
+
+describe('annotateText', function() {
+ var entries = Immutable.List([
+ GlossaryEntry({ name: 'Word' }),
+ GlossaryEntry({ name: 'Multiple Words' })
+ ]);
+
+ it('should annotate text', function() {
+ var $ = cheerio.load('<p>This is a word, and multiple words</p>');
+
+ annotateText(entries, 'GLOSSARY.md', $);
+
+ var links = $('a');
+ expect(links.length).toBe(2);
+
+ var word = $(links.get(0));
+ expect(word.attr('href')).toBe('/GLOSSARY.md#word');
+ expect(word.text()).toBe('word');
+ expect(word.hasClass('glossary-term')).toBeTruthy();
+
+ var words = $(links.get(1));
+ expect(words.attr('href')).toBe('/GLOSSARY.md#multiple-words');
+ expect(words.text()).toBe('multiple words');
+ expect(words.hasClass('glossary-term')).toBeTruthy();
+ });
+
+ it('should not annotate scripts', function() {
+ var $ = cheerio.load('<script>This is a word, and multiple words</script>');
+
+ annotateText(entries, 'GLOSSARY.md', $);
+ expect($('a').length).toBe(0);
+ });
+
+ it('should not annotate when has class "no-glossary"', function() {
+ var $ = cheerio.load('<p class="no-glossary">This is a word, and multiple words</p>');
+
+ annotateText(entries, 'GLOSSARY.md', $);
+ expect($('a').length).toBe(0);
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/fetchRemoteImages.js b/packages/gitbook/lib/output/modifiers/__tests__/fetchRemoteImages.js
new file mode 100644
index 0000000..bc1704d
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/fetchRemoteImages.js
@@ -0,0 +1,40 @@
+var cheerio = require('cheerio');
+var tmp = require('tmp');
+var path = require('path');
+
+var URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png';
+
+describe('fetchRemoteImages', function() {
+ var dir;
+ var fetchRemoteImages = require('../fetchRemoteImages');
+
+ beforeEach(function() {
+ dir = tmp.dirSync();
+ });
+
+ it('should download image file', function() {
+ var $ = cheerio.load('<img src="' + URL + '" />');
+
+ return fetchRemoteImages(dir.name, 'index.html', $)
+ .then(function() {
+ var $img = $('img');
+ var src = $img.attr('src');
+
+ expect(dir.name).toHaveFile(src);
+ });
+ });
+
+ it('should download image file and replace with relative path', function() {
+ var $ = cheerio.load('<img src="' + URL + '" />');
+
+ return fetchRemoteImages(dir.name, 'test/index.html', $)
+ .then(function() {
+ var $img = $('img');
+ var src = $img.attr('src');
+
+ expect(dir.name).toHaveFile(path.join('test', src));
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/highlightCode.js b/packages/gitbook/lib/output/modifiers/__tests__/highlightCode.js
new file mode 100644
index 0000000..75d9902
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/highlightCode.js
@@ -0,0 +1,60 @@
+var cheerio = require('cheerio');
+var Promise = require('../../../utils/promise');
+var highlightCode = require('../highlightCode');
+
+describe('highlightCode', function() {
+ function doHighlight(lang, code) {
+ return {
+ text: '' + (lang || '') + '$' + code
+ };
+ }
+
+ function doHighlightAsync(lang, code) {
+ return Promise()
+ .then(function() {
+ return doHighlight(lang, code);
+ });
+ }
+
+ it('should call it for normal code element', function() {
+ var $ = cheerio.load('<p>This is a <code>test</code></p>');
+
+ return highlightCode(doHighlight, $)
+ .then(function() {
+ var $code = $('code');
+ expect($code.text()).toBe('$test');
+ });
+ });
+
+ it('should call it for markdown code block', function() {
+ var $ = cheerio.load('<pre><code class="lang-js">test</code></pre>');
+
+ return highlightCode(doHighlight, $)
+ .then(function() {
+ var $code = $('code');
+ expect($code.text()).toBe('js$test');
+ });
+ });
+
+ it('should call it for asciidoc code block', function() {
+ var $ = cheerio.load('<pre><code class="language-python">test</code></pre>');
+
+ return highlightCode(doHighlight, $)
+ .then(function() {
+ var $code = $('code');
+ expect($code.text()).toBe('python$test');
+ });
+ });
+
+ it('should accept async highlighter', function() {
+ var $ = cheerio.load('<pre><code class="language-python">test</code></pre>');
+
+ return highlightCode(doHighlightAsync, $)
+ .then(function() {
+ var $code = $('code');
+ expect($code.text()).toBe('python$test');
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/inlinePng.js b/packages/gitbook/lib/output/modifiers/__tests__/inlinePng.js
new file mode 100644
index 0000000..0073cff
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/inlinePng.js
@@ -0,0 +1,25 @@
+var cheerio = require('cheerio');
+var tmp = require('tmp');
+var inlinePng = require('../inlinePng');
+
+describe('inlinePng', function() {
+ var dir;
+
+ beforeEach(function() {
+ dir = tmp.dirSync();
+ });
+
+ it('should write an inline PNG using data URI as a file', function() {
+ var $ = cheerio.load('<img alt="GitBook Logo 20x20" src="data:image/png;base64,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"/>');
+
+ return inlinePng(dir.name, 'index.html', $)
+ .then(function() {
+ var $img = $('img');
+ var src = $img.attr('src');
+
+ expect(dir.name).toHaveFile(src);
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/resolveLinks.js b/packages/gitbook/lib/output/modifiers/__tests__/resolveLinks.js
new file mode 100644
index 0000000..8904c11
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/resolveLinks.js
@@ -0,0 +1,104 @@
+var path = require('path');
+var cheerio = require('cheerio');
+var resolveLinks = require('../resolveLinks');
+
+describe('resolveLinks', function() {
+ function resolveFileBasic(href) {
+ return 'fakeDir/' + href;
+ }
+
+ function resolveFileCustom(href) {
+ if (path.extname(href) == '.md') {
+ return href.slice(0, -3) + '.html';
+ }
+
+ return href;
+ }
+
+ describe('Absolute path', function() {
+ var TEST = '<p>This is a <a href="/test/cool.md"></a></p>';
+
+ it('should resolve path starting by "/" in root directory', function() {
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('hello.md', resolveFileBasic, $)
+ .then(function() {
+ var link = $('a');
+ expect(link.attr('href')).toBe('fakeDir/test/cool.md');
+ });
+ });
+
+ it('should resolve path starting by "/" in child directory', function() {
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('afolder/hello.md', resolveFileBasic, $)
+ .then(function() {
+ var link = $('a');
+ expect(link.attr('href')).toBe('../fakeDir/test/cool.md');
+ });
+ });
+ });
+
+ describe('Anchor', function() {
+ it('should prevent anchors in resolution', function() {
+ var TEST = '<p>This is a <a href="test/cool.md#an-anchor"></a></p>';
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('hello.md', resolveFileCustom, $)
+ .then(function() {
+ var link = $('a');
+ expect(link.attr('href')).toBe('test/cool.html#an-anchor');
+ });
+ });
+
+ it('should ignore pure anchor links', function() {
+ var TEST = '<p>This is a <a href="#an-anchor"></a></p>';
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('hello.md', resolveFileCustom, $)
+ .then(function() {
+ var link = $('a');
+ expect(link.attr('href')).toBe('#an-anchor');
+ });
+ });
+ });
+
+ describe('Custom Resolver', function() {
+ var TEST = '<p>This is a <a href="/test/cool.md"></a> <a href="afile.png"></a></p>';
+
+ it('should resolve path correctly for absolute path', function() {
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('hello.md', resolveFileCustom, $)
+ .then(function() {
+ var link = $('a').first();
+ expect(link.attr('href')).toBe('test/cool.html');
+ });
+ });
+
+ it('should resolve path correctly for absolute path (2)', function() {
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('afodler/hello.md', resolveFileCustom, $)
+ .then(function() {
+ var link = $('a').first();
+ expect(link.attr('href')).toBe('../test/cool.html');
+ });
+ });
+ });
+
+ describe('External link', function() {
+ var TEST = '<p>This is a <a href="http://www.github.com">external link</a></p>';
+
+ it('should have target="_blank" attribute', function() {
+ var $ = cheerio.load(TEST);
+
+ return resolveLinks('hello.md', resolveFileBasic, $)
+ .then(function() {
+ var link = $('a');
+ expect(link.attr('target')).toBe('_blank');
+ });
+ });
+ });
+
+});
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/svgToImg.js b/packages/gitbook/lib/output/modifiers/__tests__/svgToImg.js
new file mode 100644
index 0000000..5fe9796
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/svgToImg.js
@@ -0,0 +1,25 @@
+var cheerio = require('cheerio');
+var tmp = require('tmp');
+
+describe('svgToImg', function() {
+ var dir;
+ var svgToImg = require('../svgToImg');
+
+ beforeEach(function() {
+ dir = tmp.dirSync();
+ });
+
+ it('should write svg as a file', function() {
+ var $ = cheerio.load('<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" version="1.1"><rect width="200" height="100" stroke="black" stroke-width="6" fill="green"/></svg>');
+
+ return svgToImg(dir.name, 'index.html', $)
+ .then(function() {
+ var $img = $('img');
+ var src = $img.attr('src');
+
+ expect(dir.name).toHaveFile(src);
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/__tests__/svgToPng.js b/packages/gitbook/lib/output/modifiers/__tests__/svgToPng.js
new file mode 100644
index 0000000..dbb3502
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/__tests__/svgToPng.js
@@ -0,0 +1,33 @@
+var cheerio = require('cheerio');
+var tmp = require('tmp');
+var path = require('path');
+
+var svgToImg = require('../svgToImg');
+var svgToPng = require('../svgToPng');
+
+describe('svgToPng', function() {
+ var dir;
+
+ beforeEach(function() {
+ dir = tmp.dirSync();
+ });
+
+ it('should write svg as png file', function() {
+ var $ = cheerio.load('<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100" version="1.1"><rect width="200" height="100" stroke="black" stroke-width="6" fill="green"/></svg>');
+ var fileName = 'index.html';
+
+ return svgToImg(dir.name, fileName, $)
+ .then(function() {
+ return svgToPng(dir.name, fileName, $);
+ })
+ .then(function() {
+ var $img = $('img');
+ var src = $img.attr('src');
+
+ expect(dir.name).toHaveFile(src);
+ expect(path.extname(src)).toBe('.png');
+ });
+ });
+});
+
+
diff --git a/packages/gitbook/lib/output/modifiers/addHeadingId.js b/packages/gitbook/lib/output/modifiers/addHeadingId.js
new file mode 100644
index 0000000..e2e2720
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/addHeadingId.js
@@ -0,0 +1,23 @@
+var slug = require('github-slugid');
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Add ID to an heading
+
+ @param {HTMLElement} heading
+*/
+function addId(heading) {
+ if (heading.attr('id')) return;
+ heading.attr('id', slug(heading.text()));
+}
+
+/**
+ Add ID to all headings
+
+ @param {HTMLDom} $
+*/
+function addHeadingId($) {
+ return editHTMLElement($, 'h1,h2,h3,h4,h5,h6', addId);
+}
+
+module.exports = addHeadingId;
diff --git a/packages/gitbook/lib/output/modifiers/annotateText.js b/packages/gitbook/lib/output/modifiers/annotateText.js
new file mode 100644
index 0000000..490c228
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/annotateText.js
@@ -0,0 +1,94 @@
+var escape = require('escape-html');
+
+// Selector to ignore
+var ANNOTATION_IGNORE = '.no-glossary,code,pre,a,script,h1,h2,h3,h4,h5,h6';
+
+function pregQuote( str ) {
+ return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
+}
+
+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();
+ });
+}
+
+/**
+ * Annotate text using a list of GlossaryEntry
+ *
+ * @param {List<GlossaryEntry>}
+ * @param {String} glossaryFilePath
+ * @param {HTMLDom} $
+ */
+function annotateText(entries, glossaryFilePath, $) {
+ entries.forEach(function(entry) {
+ var entryId = entry.getID();
+ var name = entry.getName();
+ var description = entry.getDescription();
+ var searchRegex = new RegExp( '\\b(' + pregQuote(name.toLowerCase()) + ')\\b' , 'gi' );
+
+ $('*').each(function() {
+ var $this = $(this);
+
+ if (
+ $this.is(ANNOTATION_IGNORE) ||
+ $this.parents(ANNOTATION_IGNORE).length > 0
+ ) return;
+
+ replaceText($, this, searchRegex, function(match) {
+ return '<a href="/' + glossaryFilePath + '#' + entryId + '" '
+ + 'class="glossary-term" title="' + escape(description) + '">'
+ + match
+ + '</a>';
+ });
+ });
+
+ });
+}
+
+module.exports = annotateText;
diff --git a/packages/gitbook/lib/output/modifiers/editHTMLElement.js b/packages/gitbook/lib/output/modifiers/editHTMLElement.js
new file mode 100644
index 0000000..755598e
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/editHTMLElement.js
@@ -0,0 +1,15 @@
+var Promise = require('../../utils/promise');
+
+/**
+ Edit all elements matching a selector
+*/
+function editHTMLElement($, selector, fn) {
+ var $elements = $(selector);
+
+ return Promise.forEach($elements, function(el) {
+ var $el = $(el);
+ return fn($el);
+ });
+}
+
+module.exports = editHTMLElement;
diff --git a/packages/gitbook/lib/output/modifiers/fetchRemoteImages.js b/packages/gitbook/lib/output/modifiers/fetchRemoteImages.js
new file mode 100644
index 0000000..ef868b9
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/fetchRemoteImages.js
@@ -0,0 +1,44 @@
+var path = require('path');
+var crc = require('crc');
+
+var editHTMLElement = require('./editHTMLElement');
+var fs = require('../../utils/fs');
+var LocationUtils = require('../../utils/location');
+
+/**
+ Fetch all remote images
+
+ @param {String} rootFolder
+ @param {String} currentFile
+ @param {HTMLDom} $
+ @return {Promise}
+*/
+function fetchRemoteImages(rootFolder, currentFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'img', function($img) {
+ var src = $img.attr('src');
+ var extension = path.extname(src);
+
+ if (!LocationUtils.isExternal(src)) {
+ return;
+ }
+
+ // We avoid generating twice the same PNG
+ var hash = crc.crc32(src).toString(16);
+ var fileName = hash + extension;
+ var filePath = path.join(rootFolder, fileName);
+
+ return fs.assertFile(filePath, function() {
+ return fs.download(src, filePath);
+ })
+ .then(function() {
+ // Convert to relative
+ src = LocationUtils.relative(currentDirectory, fileName);
+
+ $img.replaceWith('<img src="' + src + '" />');
+ });
+ });
+}
+
+module.exports = fetchRemoteImages;
diff --git a/packages/gitbook/lib/output/modifiers/highlightCode.js b/packages/gitbook/lib/output/modifiers/highlightCode.js
new file mode 100644
index 0000000..5d397bb
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/highlightCode.js
@@ -0,0 +1,58 @@
+var is = require('is');
+var Immutable = require('immutable');
+
+var Promise = require('../../utils/promise');
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Return language for a code blocks from a list of class names
+
+ @param {Array<String>}
+ @return {String}
+*/
+function getLanguageForClass(classNames) {
+ return Immutable.List(classNames)
+ .map(function(cl) {
+ // Markdown
+ if (cl.search('lang-') === 0) {
+ return cl.slice('lang-'.length);
+ }
+
+ // Asciidoc
+ if (cl.search('language-') === 0) {
+ return cl.slice('language-'.length);
+ }
+
+ return null;
+ })
+ .find(function(cl) {
+ return Boolean(cl);
+ });
+}
+
+
+/**
+ Highlight all code elements
+
+ @param {Function(lang, body) -> String} highlight
+ @param {HTMLDom} $
+ @return {Promise}
+*/
+function highlightCode(highlight, $) {
+ return editHTMLElement($, 'code', function($code) {
+ var classNames = ($code.attr('class') || '').split(' ');
+ var lang = getLanguageForClass(classNames);
+ var source = $code.text();
+
+ return Promise(highlight(lang, source))
+ .then(function(r) {
+ if (is.string(r.html)) {
+ $code.html(r.html);
+ } else {
+ $code.text(r.text);
+ }
+ });
+ });
+}
+
+module.exports = highlightCode;
diff --git a/packages/gitbook/lib/output/modifiers/index.js b/packages/gitbook/lib/output/modifiers/index.js
new file mode 100644
index 0000000..f1daa2b
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/index.js
@@ -0,0 +1,15 @@
+
+module.exports = {
+ modifyHTML: require('./modifyHTML'),
+ inlineAssets: require('./inlineAssets'),
+
+ // HTML transformations
+ addHeadingId: require('./addHeadingId'),
+ svgToImg: require('./svgToImg'),
+ fetchRemoteImages: require('./fetchRemoteImages'),
+ svgToPng: require('./svgToPng'),
+ resolveLinks: require('./resolveLinks'),
+ resolveImages: require('./resolveImages'),
+ annotateText: require('./annotateText'),
+ highlightCode: require('./highlightCode')
+};
diff --git a/packages/gitbook/lib/output/modifiers/inlineAssets.js b/packages/gitbook/lib/output/modifiers/inlineAssets.js
new file mode 100644
index 0000000..7cd874b
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/inlineAssets.js
@@ -0,0 +1,29 @@
+var svgToImg = require('./svgToImg');
+var svgToPng = require('./svgToPng');
+var inlinePng = require('./inlinePng');
+var resolveImages = require('./resolveImages');
+var fetchRemoteImages = require('./fetchRemoteImages');
+
+var Promise = require('../../utils/promise');
+
+/**
+ Inline all assets in a page
+
+ @param {String} rootFolder
+*/
+function inlineAssets(rootFolder, currentFile) {
+ return function($) {
+ return Promise()
+
+ // Resolving images and fetching external images should be
+ // done before svg conversion
+ .then(resolveImages.bind(null, currentFile, $))
+ .then(fetchRemoteImages.bind(null, rootFolder, currentFile, $))
+
+ .then(svgToImg.bind(null, rootFolder, currentFile, $))
+ .then(svgToPng.bind(null, rootFolder, currentFile, $))
+ .then(inlinePng.bind(null, rootFolder, currentFile, $));
+ };
+}
+
+module.exports = inlineAssets;
diff --git a/packages/gitbook/lib/output/modifiers/inlinePng.js b/packages/gitbook/lib/output/modifiers/inlinePng.js
new file mode 100644
index 0000000..161f164
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/inlinePng.js
@@ -0,0 +1,47 @@
+var crc = require('crc');
+var path = require('path');
+
+var imagesUtil = require('../../utils/images');
+var fs = require('../../utils/fs');
+var LocationUtils = require('../../utils/location');
+
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Convert all inline PNG images to PNG file
+
+ @param {String} rootFolder
+ @param {HTMLDom} $
+ @return {Promise}
+*/
+function inlinePng(rootFolder, currentFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'img', function($img) {
+ var src = $img.attr('src');
+ if (!LocationUtils.isDataURI(src)) {
+ return;
+ }
+
+ // We avoid generating twice the same PNG
+ var hash = crc.crc32(src).toString(16);
+ var fileName = hash + '.png';
+
+ // Result file path
+ var filePath = path.join(rootFolder, fileName);
+
+ return fs.assertFile(filePath, function() {
+ return imagesUtil.convertInlinePNG(src, filePath);
+ })
+ .then(function() {
+ // Convert filename to a relative filename
+ fileName = LocationUtils.relative(currentDirectory, fileName);
+
+ // Replace src
+ $img.attr('src', fileName);
+ });
+ });
+}
+
+
+module.exports = inlinePng;
diff --git a/packages/gitbook/lib/output/modifiers/modifyHTML.js b/packages/gitbook/lib/output/modifiers/modifyHTML.js
new file mode 100644
index 0000000..cd3d6e5
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/modifyHTML.js
@@ -0,0 +1,25 @@
+var cheerio = require('cheerio');
+var Promise = require('../../utils/promise');
+
+/**
+ Apply a list of operations to a page and
+ output the new page.
+
+ @param {Page}
+ @param {List|Array<Transformation>}
+ @return {Promise<Page>}
+*/
+function modifyHTML(page, operations) {
+ var html = page.getContent();
+ var $ = cheerio.load(html);
+
+ return Promise.forEach(operations, function(op) {
+ return op($);
+ })
+ .then(function() {
+ var resultHTML = $.html();
+ return page.set('content', resultHTML);
+ });
+}
+
+module.exports = modifyHTML;
diff --git a/packages/gitbook/lib/output/modifiers/resolveImages.js b/packages/gitbook/lib/output/modifiers/resolveImages.js
new file mode 100644
index 0000000..cc25cfa
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/resolveImages.js
@@ -0,0 +1,33 @@
+var path = require('path');
+
+var LocationUtils = require('../../utils/location');
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Resolve all HTML images:
+ - /test.png in hello -> ../test.html
+
+ @param {String} currentFile
+ @param {HTMLDom} $
+*/
+function resolveImages(currentFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'img', function($img) {
+ var src = $img.attr('src');
+
+ if (LocationUtils.isExternal(src) || LocationUtils.isDataURI(src)) {
+ return;
+ }
+
+ // Calcul absolute path for this
+ src = LocationUtils.toAbsolute(src, currentDirectory, '.');
+
+ // Convert back to relative
+ src = LocationUtils.relative(currentDirectory, src);
+
+ $img.attr('src', src);
+ });
+}
+
+module.exports = resolveImages;
diff --git a/packages/gitbook/lib/output/modifiers/resolveLinks.js b/packages/gitbook/lib/output/modifiers/resolveLinks.js
new file mode 100644
index 0000000..9d15e5e
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/resolveLinks.js
@@ -0,0 +1,53 @@
+var path = require('path');
+var url = require('url');
+
+var LocationUtils = require('../../utils/location');
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Resolve all HTML links:
+ - /test.md in hello -> ../test.html
+
+ @param {String} currentFile
+ @param {Function(String) -> String} resolveFile
+ @param {HTMLDom} $
+*/
+function resolveLinks(currentFile, resolveFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'a', function($a) {
+ var href = $a.attr('href');
+
+ // Don't change a tag without href
+ if (!href) {
+ return;
+ }
+
+ if (LocationUtils.isExternal(href)) {
+ $a.attr('target', '_blank');
+ return;
+ }
+
+ // Split anchor
+ var parsed = url.parse(href);
+ href = parsed.pathname || '';
+
+ if (href) {
+ // Calcul absolute path for this
+ href = LocationUtils.toAbsolute(href, currentDirectory, '.');
+
+ // Resolve file
+ href = resolveFile(href);
+
+ // Convert back to relative
+ href = LocationUtils.relative(currentDirectory, href);
+ }
+
+ // Add back anchor
+ href = href + (parsed.hash || '');
+
+ $a.attr('href', href);
+ });
+}
+
+module.exports = resolveLinks;
diff --git a/packages/gitbook/lib/output/modifiers/svgToImg.js b/packages/gitbook/lib/output/modifiers/svgToImg.js
new file mode 100644
index 0000000..f31b06d
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/svgToImg.js
@@ -0,0 +1,56 @@
+var path = require('path');
+var crc = require('crc');
+var domSerializer = require('dom-serializer');
+
+var editHTMLElement = require('./editHTMLElement');
+var fs = require('../../utils/fs');
+var LocationUtils = require('../../utils/location');
+
+/**
+ Render a cheerio DOM as html
+
+ @param {HTMLDom} $
+ @param {HTMLElement} dom
+ @param {Object}
+ @return {String}
+*/
+function renderDOM($, dom, options) {
+ if (!dom && $._root && $._root.children) {
+ dom = $._root.children;
+ }
+ options = options|| dom.options || $._options;
+ return domSerializer(dom, options);
+}
+
+/**
+ Replace SVG tag by IMG
+
+ @param {String} baseFolder
+ @param {HTMLDom} $
+*/
+function svgToImg(baseFolder, currentFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'svg', function($svg) {
+ var content = '<?xml version="1.0" encoding="UTF-8"?>' +
+ renderDOM($, $svg);
+
+ // We avoid generating twice the same PNG
+ var hash = crc.crc32(content).toString(16);
+ var fileName = hash + '.svg';
+ var filePath = path.join(baseFolder, fileName);
+
+ // Write the svg to the file
+ return fs.assertFile(filePath, function() {
+ return fs.writeFile(filePath, content, 'utf8');
+ })
+
+ // Return as image
+ .then(function() {
+ var src = LocationUtils.relative(currentDirectory, fileName);
+ $svg.replaceWith('<img src="' + src + '" />');
+ });
+ });
+}
+
+module.exports = svgToImg;
diff --git a/packages/gitbook/lib/output/modifiers/svgToPng.js b/packages/gitbook/lib/output/modifiers/svgToPng.js
new file mode 100644
index 0000000..1093106
--- /dev/null
+++ b/packages/gitbook/lib/output/modifiers/svgToPng.js
@@ -0,0 +1,53 @@
+var crc = require('crc');
+var path = require('path');
+
+var imagesUtil = require('../../utils/images');
+var fs = require('../../utils/fs');
+var LocationUtils = require('../../utils/location');
+
+var editHTMLElement = require('./editHTMLElement');
+
+/**
+ Convert all SVG images to PNG
+
+ @param {String} rootFolder
+ @param {HTMLDom} $
+ @return {Promise}
+*/
+function svgToPng(rootFolder, currentFile, $) {
+ var currentDirectory = path.dirname(currentFile);
+
+ return editHTMLElement($, 'img', function($img) {
+ var src = $img.attr('src');
+ if (path.extname(src) !== '.svg') {
+ return;
+ }
+
+ // Calcul absolute path for this
+ src = LocationUtils.toAbsolute(src, currentDirectory, '.');
+
+ // We avoid generating twice the same PNG
+ var hash = crc.crc32(src).toString(16);
+ var fileName = hash + '.png';
+
+ // Input file path
+ var inputPath = path.join(rootFolder, src);
+
+ // Result file path
+ var filePath = path.join(rootFolder, fileName);
+
+ return fs.assertFile(filePath, function() {
+ return imagesUtil.convertSVGToPNG(inputPath, filePath);
+ })
+ .then(function() {
+ // Convert filename to a relative filename
+ fileName = LocationUtils.relative(currentDirectory, fileName);
+
+ // Replace src
+ $img.attr('src', fileName);
+ });
+ });
+}
+
+
+module.exports = svgToPng;