diff options
Diffstat (limited to 'packages/gitbook/lib/output/modifiers')
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; |