├── .npmignore ├── .gitignore ├── pre-content ├── pdf │ ├── test │ │ ├── integration │ │ │ ├── index.pdf │ │ │ ├── extras │ │ │ │ ├── 1-copyright.pdf │ │ │ │ ├── 2-propagandas.pdf │ │ │ │ └── 3-copy right e propagandas.pdf │ │ │ ├── calibre-test.js │ │ │ ├── pdftk-test.js │ │ │ └── toc.html │ │ └── unit │ │ │ ├── pdftk-join-test.js │ │ │ ├── gs-pageInfo-test.js │ │ │ └── pdftk-bookmarkInfo-test.js │ ├── toc.js │ └── index.js └── index.js ├── package.json ├── cmd ├── pdftk │ ├── index.js │ ├── multistamp.js │ ├── join.js │ ├── extractNumberOfPages.js │ ├── updateBookmarkInfo.js │ └── extractTOC.js ├── pdfToText │ └── index.js ├── calibre │ └── index.js └── gs │ └── index.js ├── renderers ├── html │ └── index.js └── md │ └── index.js ├── helpers ├── imageHelper │ └── index.js └── fileHelper │ └── index.js ├── index.js ├── renderHooks └── index.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npmignore 3 | pre-content/test/ 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | node_modules 3 | pre-content/test/integration/debug 4 | pre-content/test/integration/toc.pdf 5 | -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/index.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casadocodigo/gitbook-plugin-cdc/HEAD/pre-content/pdf/test/integration/index.pdf -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/extras/1-copyright.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casadocodigo/gitbook-plugin-cdc/HEAD/pre-content/pdf/test/integration/extras/1-copyright.pdf -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/extras/2-propagandas.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casadocodigo/gitbook-plugin-cdc/HEAD/pre-content/pdf/test/integration/extras/2-propagandas.pdf -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/extras/3-copy right e propagandas.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casadocodigo/gitbook-plugin-cdc/HEAD/pre-content/pdf/test/integration/extras/3-copy right e propagandas.pdf -------------------------------------------------------------------------------- /pre-content/index.js: -------------------------------------------------------------------------------- 1 | var fileHelper = require('./../helpers/fileHelper'); 2 | var pdf = require('./pdf'); 3 | 4 | function addPreContent() { 5 | var extension = fileHelper.obtainExtension(this.options); 6 | if (extension !== 'pdf') { 7 | return; 8 | } 9 | return pdf.addPreContent.call(this); 10 | } 11 | 12 | module.exports = { 13 | 'addPreContent': addPreContent 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitbook-plugin-cdc", 3 | "version": "0.0.1", 4 | "description": "Plugins da Casa do Codigo", 5 | "main": "index.js", 6 | "private": true, 7 | "engines": { 8 | "gitbook": ">=1.5.0" 9 | }, 10 | "dependencies": { 11 | "cheerio": "^0.15.0", 12 | "q": "1.0.1", 13 | "swig": "1.3.2", 14 | "fs-extra": "0.10.0", 15 | "kramed": "0.4.3", 16 | "xml2js": "0.4.10", 17 | "he": "0.5.0" 18 | }, 19 | "devDependencies": { 20 | "mocha": "1.18.2", 21 | "proxyquire": "1.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd/pdftk/index.js: -------------------------------------------------------------------------------- 1 | var multistamp = require("./multistamp.js"); 2 | var extractTOC = require("./extractTOC.js"); 3 | var extractNumberOfPages = require("./extractNumberOfPages.js"); 4 | var join = require("./join.js"); 5 | var updateBookmarkInfo = require("./updateBookmarkInfo.js"); 6 | 7 | /* 8 | References: 9 | https://www.pdflabs.com/docs/pdftk-man-page/ 10 | https://www.pdflabs.com/docs/pdftk-cli-examples/ 11 | 12 | */ 13 | 14 | module.exports = { 15 | multistamp: multistamp, 16 | extractTOC: extractTOC, 17 | extractNumberOfPagesFromFiles: extractNumberOfPages, 18 | join: join, 19 | updateBookmarkInfo: updateBookmarkInfo 20 | }; -------------------------------------------------------------------------------- /cmd/pdftk/multistamp.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var Q = require('q'); 4 | 5 | function multistamp(stamp, input, output) { 6 | 7 | console.log('pdftk - Preparing to multistamp...'); 8 | 9 | var d = Q.defer(); 10 | 11 | return Q().then(function () { 12 | 13 | //pdftk in.pdf multistamp stamp.pdf output out.pdf 14 | 15 | 16 | var pdftkCall = 'pdftk ' + input + ' multistamp ' + stamp + ' output ' + output; 17 | 18 | console.log('pdftk - Calling pdftk...'); 19 | console.log(pdftkCall); 20 | 21 | exec(pdftkCall, function (error, stdout, stderr) { 22 | if (error) { 23 | console.log('pdftk - Error while multistamping. :/'); 24 | return d.reject(error); 25 | } 26 | console.log('pdftk - Mutistamp done! :)'); 27 | return d.resolve(); 28 | }); 29 | 30 | return d.promise; 31 | 32 | }); 33 | } 34 | 35 | module.exports = multistamp; 36 | -------------------------------------------------------------------------------- /cmd/pdfToText/index.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var Q = require('q'); 3 | 4 | function extractTextPositions(pdfFile) { 5 | console.log('pdftotext - Preparing to extract text positions from pdf...'); 6 | var d = Q.defer(); 7 | return Q().then(function () { 8 | var pdfToTextCall = 'pdftotext -bbox ' + pdfFile + ' -'; 9 | console.log('pdftotext - Calling pdftotext...'); 10 | console.log(pdfToTextCall); 11 | exec(pdfToTextCall, function (error, stdout, stderr) { 12 | if (error) { 13 | console.log('pdftotext - Error while extraction text positions from pdf. :/'); 14 | return d.reject(error); 15 | } 16 | console.log('pdftotext - Extracted text positions from pdf! :)'); 17 | var xml = stdout; 18 | return d.resolve(xml); 19 | }); 20 | return d.promise; 21 | }); 22 | } 23 | 24 | module.exports = { 25 | extractTextPositions: extractTextPositions 26 | }; -------------------------------------------------------------------------------- /renderers/html/index.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var swig = require('swig'); 3 | 4 | function render(params, templateLocation) { 5 | 6 | //console.log('htmlRenderer - Preparing to render html...'); 7 | 8 | var d = Q.defer(); 9 | return Q() 10 | .then(function () { 11 | 12 | //console.log('htmlRenderer - Rendering html...'); 13 | swig.setDefaults({ 14 | locals: { 15 | version: function () { 16 | return params.options.book.version; 17 | } 18 | } 19 | }); 20 | 21 | swig.compileFile(templateLocation, { 22 | autoescape: false 23 | }, function (error, template) { 24 | if (error) { 25 | console.log('htmlRenderer - Error rendering html. :/'); 26 | return d.reject(error); 27 | } 28 | var output = template(params); 29 | //console.log('htmlRenderer - html rendered! :)'); 30 | return d.resolve(output); 31 | 32 | }); 33 | return d.promise; 34 | }); 35 | } 36 | 37 | module.exports = { 38 | render: render 39 | }; -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/calibre-test.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var calibre = require("./../../calibre.js"); 3 | 4 | var inputFilename = path.resolve(__dirname , "./toc.html"); 5 | var outputFilename = path.resolve(__dirname , "./toc.pdf"); 6 | 7 | var options = { 8 | "-d debug": true, 9 | "--pdf-page-numbers": null, 10 | "--disable-font-rescaling": true, 11 | "--paper-size": null, 12 | "--custom-size": "155x230", 13 | "--unit": "millimeter", 14 | "--pdf-default-font-size": "11", 15 | "--pdf-mono-font-size": "11", 16 | "--margin-left":"62", 17 | "--margin-right":"62", 18 | "--margin-top":"62", 19 | "--margin-bottom":"62", 20 | "--pdf-header-template": "
Casa do CódigoSumárioSumárioCasa do Código
", 21 | "--chapter": "/", 22 | "--page-breaks-before": "/" 23 | }; 24 | 25 | calibre.generate(inputFilename, outputFilename, options) 26 | .fail(function(error){ 27 | console.log(error); 28 | }); -------------------------------------------------------------------------------- /helpers/imageHelper/index.js: -------------------------------------------------------------------------------- 1 | var WIDTH_REGEX = /\{w=(\d+)%?\}$/; 2 | var DESKTOP_WIDTH = 1000; 3 | 4 | function adjustImageWidth(img, extension) { 5 | if (!img.length) { 6 | return; 7 | } 8 | //ajusta width da imagem 9 | var text = img.attr('alt').trim(); 10 | var regexMatch = text.match(WIDTH_REGEX); 11 | if (regexMatch && regexMatch[1]) { 12 | text = text.replace(WIDTH_REGEX, ''); 13 | var width = regexMatch[1]; 14 | if (extension === 'pdf') { 15 | img.css('width', width + '%'); 16 | } else { 17 | var maxWidth = parseInt(width, 10) / 100 * DESKTOP_WIDTH; 18 | img.css('max-width', maxWidth + 'px'); 19 | } 20 | img.attr('alt', text); 21 | } 22 | } 23 | 24 | function insertImageCaption($, img, captionPrefix) { 25 | //insere div com caption 26 | var text = img.attr('alt').trim(); 27 | var parent = img.parent(); 28 | img.remove(); 29 | var figure = $(''); 33 | caption.text(captionPrefix + ': ' + text); 34 | figure.append(caption); 35 | } 36 | } 37 | 38 | module.exports = { 39 | 'adjustImageWidth': adjustImageWidth, 40 | 'insertImageCaption': insertImageCaption 41 | }; -------------------------------------------------------------------------------- /cmd/calibre/index.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var Q = require('q'); 4 | 5 | function _calibreOptions(options) { 6 | var calibreOptions = ''; 7 | var option; 8 | for (option in options) { 9 | if (options.hasOwnProperty(option)) { 10 | var value = options[option]; 11 | if (value) { 12 | if (typeof value === 'boolean') { 13 | calibreOptions += option + ' '; 14 | } else { 15 | calibreOptions += option + '="' + options[option] + '" '; 16 | } 17 | } 18 | } 19 | } 20 | return calibreOptions; 21 | } 22 | 23 | function generate(inputFilename, outputFilename, options) { 24 | var d = Q.defer(); 25 | 26 | console.log('calibre - Preparing to call calibre...'); 27 | 28 | return Q().then(function () { 29 | var calibreCall = 'ebook-convert ' + inputFilename + ' ' + outputFilename + ' ' + _calibreOptions(options); 30 | 31 | console.log('calibre - Calling calibre...'); 32 | console.log(calibreCall); 33 | 34 | exec(calibreCall, function (error, stdout, stderr) { 35 | if (error) { 36 | console.log('calibre - Error calling calibre. :/'); 37 | return d.reject(error.message + ' ' + stdout); 38 | } 39 | console.log('calibre - done! :)'); 40 | return d.resolve(); 41 | }); 42 | 43 | return d.promise; 44 | }); 45 | } 46 | 47 | module.exports = { 48 | generate: generate 49 | }; -------------------------------------------------------------------------------- /helpers/fileHelper/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | var Q = require('q'); 5 | 6 | function obtainExtension(options) { 7 | var extension = options.extension || path.extname(options.output).replace('.', ''); 8 | if (!extension && options.format === 'ebook') { 9 | extension = 'pdf'; 10 | } 11 | return extension; 12 | } 13 | 14 | function outputPath(options) { 15 | var output = options.output; 16 | if (output.indexOf('/') !== 0) { 17 | var currentPath = path.resolve('.'); 18 | return path.join(currentPath, output); 19 | } 20 | return output; 21 | } 22 | 23 | function listFilesByExtension(dir, extension) { 24 | extension = extension || '.pdf'; 25 | var d = Q.defer(); 26 | return Q().then(function () { 27 | fs.readdir(dir, function (error, files) { 28 | if (error) { 29 | return d.resolve([]); 30 | } 31 | var filtered = files.filter(function (file) { 32 | return path.extname(file) === extension; 33 | }); 34 | var sortedByName = filtered.sort(function (a, b) { 35 | return a.localeCompare(b); 36 | }); 37 | var resolvedFiles = sortedByName.map(function (file) { 38 | return path.resolve(dir, file); 39 | }); 40 | return d.resolve(resolvedFiles); 41 | }); 42 | return d.promise; 43 | }); 44 | } 45 | 46 | module.exports = { 47 | 'obtainExtension': obtainExtension, 48 | 'outputPath': outputPath, 49 | 'listFilesByExtension': listFilesByExtension 50 | }; 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var renderHooks = require('./renderHooks'); 2 | var preContent = require('./pre-content'); 3 | 4 | /* 5 | Hooks do Gitbook disponíveis, chamados nessa ordem: 6 | 1. init 7 | 2. summary:before 8 | @param summary 9 | @return summary 10 | 3. summary:after 11 | @param summary 12 | @return summary 13 | 4. glossary:before 14 | @param glossary 15 | @return glossary 16 | 5. glossary:after 17 | @param glossary 18 | @return glossary 19 | 6. page:before 20 | @param page 21 | @return page 22 | 7. page 23 | @param page 24 | @return page 25 | 8. page:after 26 | @param page 27 | @return page 28 | 9. ebook:before 29 | @param options 30 | @return options 31 | 10. ebook:after 32 | 11. finish:before 33 | 12. finish 34 | */ 35 | 36 | module.exports = { 37 | hooks: { 38 | 'summary:after': function (summary) { 39 | return renderHooks.handleSummaryAfter.call(this, summary); 40 | }, 41 | 'page:before': function (page) { 42 | return renderHooks.handlePageBefore.call(this, page); 43 | }, 44 | 'page': function (page) { 45 | return renderHooks.handlePage.call(this, page); 46 | }, 47 | 'page:after': function (page) { 48 | return renderHooks.handlePageAfter.call(this, page); 49 | }, 50 | 'ebook:before': function (options) { 51 | return renderHooks.handleEbookBefore.call(this, options); 52 | }, 53 | 'finish': function () { 54 | return preContent.addPreContent.call(this); 55 | } 56 | } 57 | }; -------------------------------------------------------------------------------- /cmd/pdftk/join.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var Q = require('q'); 4 | 5 | function _generatePdftkJoinCall(pdfFile, files, outputFile) { 6 | function filesRanges(files, action) { 7 | var ranges = ''; 8 | var i, letter; 9 | for (i = 0, letter = 'B'.charCodeAt(0); i < files.length; i++, letter++) { 10 | ranges += action(letter, i); 11 | } 12 | return ranges; 13 | } 14 | 15 | function onlyLetter(letter, i) { 16 | return ' ' + String.fromCharCode(letter); 17 | } 18 | 19 | function letterAndFile(letter, i) { 20 | return onlyLetter(letter, i) + '="' + files[i] + '"'; 21 | } 22 | 23 | //O toc original, gerado pelo gitbook/calibre, tem sempre apenas uma pagina. 24 | //isso é garantido pq o conteudo do toc original nao é visivel (display:none). 25 | 26 | var pdftkCall = 'pdftk A="' + pdfFile + '"'; 27 | pdftkCall += filesRanges(files, letterAndFile); 28 | pdftkCall += ' cat A1'; 29 | pdftkCall += filesRanges(files, onlyLetter); 30 | pdftkCall += ' A3-end output ' + outputFile; 31 | return pdftkCall; 32 | } 33 | 34 | function join(pdfFile, files, outputFile) { 35 | console.log('pdftk - Preparing to join pre content to pdf...'); 36 | var d = Q.defer(); 37 | return Q().then(function () { 38 | var pdftkCall = _generatePdftkJoinCall(pdfFile, files, outputFile); 39 | console.log('pdftk - Calling pdftk...'); 40 | console.log(pdftkCall); 41 | exec(pdftkCall, function (error, stdout, stderr) { 42 | if (error) { 43 | console.log('pdftk - Error while joining pre content. :/'); 44 | return d.reject(error); 45 | } 46 | console.log('pdftk - Joined pre content! :)'); 47 | return d.resolve(); 48 | }); 49 | return d.promise; 50 | }); 51 | } 52 | 53 | module.exports = join; 54 | -------------------------------------------------------------------------------- /cmd/pdftk/extractNumberOfPages.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var Q = require('q'); 4 | 5 | function _extractNumberOfPages(pdfFile) { 6 | console.log('pdftk - Preparing to extract number of pages...'); 7 | 8 | var d = Q.defer(); 9 | 10 | return Q().then(function () { 11 | 12 | var pdftkCall = 'pdftk "' + pdfFile + '" dump_data'; 13 | 14 | console.log('pdftk - Calling pdftk...'); 15 | console.log(pdftkCall); 16 | 17 | exec(pdftkCall, function (error, stdout, stderr) { 18 | if (error) { 19 | console.log('pdftk - Error while extracting number of pages. :/'); 20 | return d.reject(error); 21 | } 22 | 23 | var numberOfPages = 24 | stdout 25 | .split('\n') 26 | .filter(function (line) { 27 | return line.indexOf('NumberOfPages') === 0; 28 | })[0].split(':')[1]; 29 | console.log('pdftk - Extracted number of pages! :)'); 30 | 31 | return d.resolve(parseInt(numberOfPages, 10)); 32 | }); 33 | 34 | return d.promise; 35 | 36 | }); 37 | } 38 | 39 | function extractNumberOfPagesFromFiles(files) { 40 | if (!files.length) { 41 | return 0; 42 | } 43 | 44 | var d = Q.defer(); 45 | 46 | return Q().then(function () { 47 | 48 | var promises = []; 49 | files.forEach(function (file) { 50 | promises.push(_extractNumberOfPages(file)); 51 | }); 52 | 53 | Q.all(promises) 54 | .spread(function () { 55 | var sum = []. 56 | reduce 57 | .call(arguments, function (a, b) { 58 | return a + b; 59 | }); 60 | return d.resolve(sum); 61 | }, function (error) { 62 | return d.reject(error); 63 | }); 64 | 65 | return d.promise; 66 | 67 | }); 68 | } 69 | 70 | module.exports = extractNumberOfPagesFromFiles; 71 | -------------------------------------------------------------------------------- /pre-content/pdf/test/unit/pdftk-join-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var proxyquire = require("proxyquire"); 3 | 4 | var childProcessStub = {}; 5 | var fsStub = {}; 6 | 7 | var pdftk = proxyquire("./../../pdftk.js", {"child_process": childProcessStub, "fs": fsStub}); 8 | 9 | describe('pdftk', function(){ 10 | 11 | it('should generate pdftk join call without any extras', function(){ 12 | 13 | var pdfFile = "index.pdf"; 14 | var files = ["toc.pdf"]; 15 | var outputFile = "index-with-toc.pdf"; 16 | 17 | childProcessStub.exec = function(pdftkCall, fn) { 18 | assert.equal('pdftk A="index.pdf" B="toc.pdf" cat A1 B A3-end output index-with-toc.pdf', pdftkCall); 19 | fn(); 20 | }; 21 | 22 | pdftk.join(pdfFile, files, outputFile).done(); 23 | }); 24 | 25 | it('should generate pdftk join call with extras', function(){ 26 | 27 | var pdfFile = "index.pdf"; 28 | var files = ["copyright.pdf", "ads.pdf", "toc.pdf"]; 29 | var outputFile = "index-with-extras-and-toc.pdf"; 30 | 31 | childProcessStub.exec = function(pdftkCall, fn) { 32 | assert.equal('pdftk A="index.pdf" B="copyright.pdf" C="ads.pdf" D="toc.pdf" cat A1 B C D A3-end output index-with-extras-and-toc.pdf', pdftkCall); 33 | fn(); 34 | }; 35 | 36 | pdftk.join(pdfFile, files, outputFile).done(); 37 | 38 | }); 39 | 40 | 41 | it('should generate pdftk join call considering spaces in filenames', function(){ 42 | 43 | var pdfFile = "index.pdf"; 44 | var files = ["copyright and ads.pdf"]; 45 | var outputFile = "index-with-extras.pdf"; 46 | 47 | childProcessStub.exec = function(pdftkCall, fn) { 48 | assert.equal('pdftk A="index.pdf" B="copyright and ads.pdf" cat A1 B A3-end output index-with-extras.pdf', pdftkCall); 49 | fn(); 50 | }; 51 | 52 | pdftk.join(pdfFile, files, outputFile).done(); 53 | 54 | }); 55 | 56 | }); -------------------------------------------------------------------------------- /pre-content/pdf/test/unit/gs-pageInfo-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var proxyquire = require("proxyquire"); 3 | 4 | var childProcessStub = {}; 5 | var fsStub = {}; 6 | 7 | var gs = proxyquire("./../../gs.js", {"child_process": childProcessStub, "fs": fsStub}); 8 | 9 | describe('gs', function(){ 10 | 11 | it('should generate page info', function(){ 12 | 13 | var info = { 14 | "book": { 15 | "title": "Git e GitHub", 16 | "author": "Alexandre", 17 | "publisher": "Caelum" 18 | }, 19 | "preContent": { "numberOfPages": 9 } 20 | }; 21 | 22 | fsStub.writeFile = function(infoFile, pageInfo) { 23 | assert.equal("info.txt", infoFile); 24 | 25 | assert.equal("string", typeof pageInfo); 26 | assert(pageInfo.length > 0); 27 | var lines = pageInfo.split("\n"); 28 | assert.equal(11, lines.length); 29 | assert.equal("[ /Title (Git e GitHub)", lines[0]); 30 | assert.equal("/Author (Alexandre)", lines[1]); 31 | assert.equal("/Creator (Caelum)", lines[2]); 32 | assert.equal("/Producer (Caelum)", lines[3]); 33 | assert.equal("/DOCINFO pdfmark", lines[4]); 34 | assert.equal("[/_objdef {pl} /type /dict /OBJ pdfmark", lines[5]); 35 | assert.equal("[{pl} <> ", lines[7]); 37 | assert.equal("10 << /S /D /St 1 >> ", lines[8]); 38 | assert.equal("]>> /PUT pdfmark", lines[9]); 39 | assert.equal("[{Catalog} <> /PUT pdfmark", lines[10]); 40 | }; 41 | 42 | childProcessStub.exec = function(pdftkCall, fn){ 43 | assert.equal('gs -q -o output.pdf -sDEVICE=pdfwrite input.pdf info.txt', pdftkCall); 44 | fn(); 45 | }; 46 | 47 | gs.updatePageNumberInfo('input.pdf', info, 'info.txt', 'output.pdf').done(); 48 | 49 | }); 50 | 51 | }); -------------------------------------------------------------------------------- /pre-content/pdf/test/integration/pdftk-test.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var fs = require("fs"); 3 | var assert = require("assert"); 4 | 5 | var pdftk = require("./../../pdftk.js"); 6 | 7 | describe("pdftk", function(){ 8 | 9 | it('should extract toc', function(done){ 10 | pdftk 11 | .extractTOC(path.resolve('./index.pdf')) 12 | .then(function(toc){ 13 | assert.equal("object", typeof toc); 14 | assert.equal(11, toc.length); 15 | 16 | var chapter1 = toc[0]; 17 | assert.equal("1 Introdução", chapter1.title); 18 | assert.equal(3, chapter1.pageNumber); 19 | assert.equal(6, chapter1.sections.length); 20 | 21 | var section1_1 = chapter1.sections[0]; 22 | assert.equal("1.1 Mantendo o histórico do código", section1_1.title); 23 | assert.equal(3, section1_1.pageNumber); 24 | 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should get number of pages', function(done){ 30 | pdftk 31 | .extractNumberOfPagesFromFiles([path.resolve('./index.pdf')]) 32 | .then(function(pN){ 33 | assert.equal(181, pN); 34 | done(); 35 | }); 36 | }); 37 | 38 | 39 | it('should extract added number of pages of multiple files', function(done){ 40 | 41 | var index = path.resolve('./index.pdf'); 42 | var copyright = path.resolve('./extras/1-copyright.pdf'); 43 | var propagandas = path.resolve('./extras/2-propagandas.pdf'); 44 | 45 | var files = [index, copyright, propagandas]; 46 | 47 | pdftk.extractNumberOfPagesFromFiles(files) 48 | .then(function(pN){ 49 | assert.equal(184, pN); 50 | done(); 51 | }).done(); 52 | 53 | }); 54 | 55 | it('should work with spaces in the filename', function(done){ 56 | 57 | var owasp = path.resolve('./extras/3-copy right e propagandas.pdf'); 58 | 59 | var files = [owasp]; 60 | 61 | pdftk.extractNumberOfPagesFromFiles(files) 62 | .then(function(pN){ 63 | assert.equal(3, pN); 64 | done(); 65 | }).done(); 66 | 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /cmd/pdftk/updateBookmarkInfo.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var fs = require('fs'); 3 | 4 | var Q = require('q'); 5 | 6 | function _bookmarkInfo(info) { 7 | var pdfInfo = ''; 8 | 9 | //somando 1 para considerar a capa 10 | var pageNumberOffset = info.preContent.extras.numberOfPages + info.preContent.intro.numberOfPages + info.preContent.toc.numberOfPages + 1; 11 | 12 | if (info.toc) { 13 | info.toc.forEach(function (chapter) { 14 | pdfInfo += 'BookmarkBegin\n'; 15 | pdfInfo += 'BookmarkTitle: ' + chapter.title + '\n'; 16 | pdfInfo += 'BookmarkLevel: 1\n'; 17 | pdfInfo += 'BookmarkPageNumber: ' + (chapter.pageNumber + pageNumberOffset) + '\n'; 18 | 19 | if (chapter.sections) { 20 | chapter.sections.forEach(function (section) { 21 | pdfInfo += 'BookmarkBegin\n'; 22 | pdfInfo += 'BookmarkTitle: ' + section.title + '\n'; 23 | pdfInfo += 'BookmarkLevel: 2\n'; 24 | pdfInfo += 'BookmarkPageNumber: ' + (section.pageNumber + pageNumberOffset) + '\n'; 25 | 26 | if (section.subSections) { 27 | section.subSections.forEach(function (subSection) { 28 | pdfInfo += 'BookmarkBegin\n'; 29 | pdfInfo += 'BookmarkTitle: ' + subSection.title + '\n'; 30 | pdfInfo += 'BookmarkLevel: 3\n'; 31 | pdfInfo += 'BookmarkPageNumber: ' + (subSection.pageNumber + pageNumberOffset) + '\n'; 32 | }); 33 | } 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | return pdfInfo; 40 | } 41 | 42 | function updateBookmarkInfo(inputFile, info, infoFile, outputFile) { 43 | console.log('pdftk - Preparing to update bookmark info...'); 44 | var d = Q.defer(); 45 | return Q() 46 | .then(function (output) { 47 | return Q.nfcall(fs.writeFile, infoFile, _bookmarkInfo(info)); 48 | }).then(function () { 49 | var pdftkCall = 'pdftk ' + inputFile + ' update_info ' + infoFile + ' output ' + outputFile; 50 | 51 | console.log('pdftk - Calling pdftk...'); 52 | console.log(pdftkCall); 53 | 54 | exec(pdftkCall, function (error, stdout, stderr) { 55 | if (error) { 56 | console.log('pdftk - Error while updating bookmark info. :/'); 57 | return d.reject(error); 58 | } 59 | console.log('pdftk - Updated bookmark info! :)'); 60 | return d.resolve(); 61 | }); 62 | return d.promise; 63 | }); 64 | } 65 | 66 | module.exports = updateBookmarkInfo; 67 | 68 | -------------------------------------------------------------------------------- /cmd/pdftk/extractTOC.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | var Q = require('q'); 4 | 5 | function _buildTocFromDumpedData(pdftkData) { 6 | var bookmarkInfo = 7 | pdftkData 8 | .split('\n') 9 | .filter(function (line) { 10 | return line.indexOf('Bookmark') === 0 && 11 | (line.indexOf('Level') > 0 || line.indexOf('Title') > 0 || line.indexOf('PageNumber') > 0); 12 | }); 13 | 14 | var tocInfo = { 15 | 'BookmarkTitle': [], 16 | 'BookmarkLevel': [], 17 | 'BookmarkPageNumber': [] 18 | }; 19 | bookmarkInfo.forEach(function (el) { 20 | var index = el.indexOf(':'); 21 | var key = el.substring(0, index); 22 | var value = el.substring(index + 1); 23 | tocInfo[key].push(value); 24 | }); 25 | 26 | var flatToc = []; 27 | tocInfo.BookmarkTitle.forEach(function (el, i) { 28 | var item = { 29 | title: tocInfo.BookmarkTitle[i].trim(), 30 | level: parseInt(tocInfo.BookmarkLevel[i], 10), 31 | pageNumber: parseInt(tocInfo.BookmarkPageNumber[i], 10) 32 | }; 33 | flatToc.push(item); 34 | }); 35 | 36 | var toc = []; 37 | var chapter; 38 | var section; 39 | flatToc.forEach(function (item) { 40 | if (item.level === 1) { 41 | chapter = { 42 | title: item.title, 43 | pageNumber: item.pageNumber, 44 | sections: [] 45 | }; 46 | toc.push(chapter); 47 | } else if (item.level === 2) { 48 | section = { 49 | title: item.title, 50 | pageNumber: item.pageNumber, 51 | subSections: [] 52 | }; 53 | chapter.sections.push(section); 54 | } else if (item.level === 3) { 55 | var subSection = { 56 | title: item.title, 57 | pageNumber: item.pageNumber 58 | }; 59 | section.subSections.push(subSection); 60 | } 61 | }); 62 | return toc; 63 | } 64 | 65 | function extractTOC(pdfFile) { 66 | 67 | console.log('pdftk - Preparing to extract toc...'); 68 | 69 | var d = Q.defer(); 70 | 71 | return Q().then(function () { 72 | 73 | var pdftkCall = 'pdftk "' + pdfFile + '" dump_data'; 74 | 75 | console.log('pdftk - Calling pdftk...'); 76 | console.log(pdftkCall); 77 | 78 | exec(pdftkCall, function (error, stdout, stderr) { 79 | if (error) { 80 | console.log('pdftk - Error while extracting TOC. :/'); 81 | return d.reject(error); 82 | } 83 | 84 | var toc = _buildTocFromDumpedData(stdout); 85 | 86 | console.log('pdftk - Extracted TOC! :)'); 87 | return d.resolve(toc); 88 | }); 89 | 90 | return d.promise; 91 | 92 | }); 93 | } 94 | 95 | module.exports = extractTOC; 96 | -------------------------------------------------------------------------------- /renderers/md/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var Q = require('q'); 4 | var cheerio = require('cheerio'); 5 | var kramed = require('kramed'); 6 | 7 | var htmlRenderer = require('./../html'); 8 | var calibre = require('./../../cmd/calibre'); 9 | var imageHelper = require('./../../helpers/imageHelper'); 10 | var fileHelper = require('./../../helpers/fileHelper'); 11 | 12 | function _renderPdf(mdFile, htmlFile, pdfFile, template, pdfInfo) { 13 | return Q().then(function () { 14 | return Q.nfcall(fs.readFile, mdFile); 15 | }).then(function (mdData) { 16 | var extension = fileHelper.obtainExtension(pdfInfo.options); 17 | var htmlSnippet = kramed(mdData.toString()); 18 | var $ = cheerio.load(htmlSnippet); 19 | $('img').each(function(i){ 20 | var img = $(this); 21 | imageHelper.adjustImageWidth(img, extension); 22 | var captionPrefix = 'Figura ' + (i + 1); 23 | imageHelper.insertImageCaption($, img, captionPrefix); 24 | }); 25 | htmlSnippet = $.html(); 26 | return htmlSnippet; 27 | }).then(function (htmlSnippet) { 28 | if (template) { 29 | return htmlRenderer.render({ 30 | content: htmlSnippet, 31 | options: pdfInfo 32 | }, template); 33 | } 34 | return htmlSnippet; 35 | }).then(function (html) { 36 | return Q.nfcall(fs.writeFile, htmlFile, html); 37 | }).then(function () { 38 | var pdfOptions = { 39 | '--pdf-page-numbers': null, 40 | '--disable-font-rescaling': true, 41 | '--paper-size': null, 42 | '--unit': 'millimeter', 43 | '--chapter': '/', 44 | '--page-breaks-before': '/', 45 | '--custom-size': pdfInfo.options.pdf.customSize, 46 | '--margin-left': pdfInfo.options.pdf.margin.left, 47 | '--margin-right': pdfInfo.options.pdf.margin.right, 48 | '--margin-top': pdfInfo.options.pdf.margin.top, 49 | '--margin-bottom': pdfInfo.options.pdf.margin.bottom, 50 | '--pdf-default-font-size': pdfInfo.options.pdf.fontSize, 51 | '--pdf-mono-font-size': pdfInfo.options.pdf.fontSize, 52 | '--pdf-header-template': null, 53 | '--pdf-footer-template': null 54 | }; 55 | return calibre.generate(htmlFile, pdfFile, pdfOptions); 56 | }); 57 | } 58 | 59 | function renderPdfs(files, template, pdfInfo) { 60 | return Q() 61 | .then(function () { 62 | var promises = []; 63 | files.forEach(function (mdFile) { 64 | var htmlFile = mdFile.replace('.md', '.html'); 65 | var pdfFile = htmlFile.replace('.html', '.pdf'); 66 | promises.push(_renderPdf(mdFile, htmlFile, pdfFile, template, pdfInfo)); 67 | }); 68 | return Q.all(promises); 69 | }); 70 | } 71 | 72 | module.exports = { 73 | renderPdfs: renderPdfs 74 | }; 75 | -------------------------------------------------------------------------------- /cmd/gs/index.js: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | var fs = require('fs'); 3 | 4 | var Q = require('q'); 5 | 6 | function _pageNumberInfo(info) { 7 | var firstChapterPageNumber = info.preContent.extras.numberOfPages + info.preContent.intro.numberOfPages + info.preContent.toc.numberOfPages + 1; 8 | 9 | var pdfMarks = ''; 10 | pdfMarks += '[ /Title (' + info.book.title + ')\n'; 11 | pdfMarks += '/Author (' + info.book.author + ')\n'; 12 | pdfMarks += '/Creator (' + info.book.publisher + ')\n'; 13 | pdfMarks += '/Producer (' + info.book.publisher + ')\n'; 14 | pdfMarks += '/DOCINFO pdfmark\n'; 15 | pdfMarks += '[/_objdef {pl} /type /dict /OBJ pdfmark\n'; 16 | pdfMarks += '[{pl} <> \n'; //capa e sumário em números romanos 18 | pdfMarks += firstChapterPageNumber + ' << /S /D /St 1 >> \n'; 19 | pdfMarks += ']>> /PUT pdfmark\n'; 20 | pdfMarks += '[{Catalog} <> /PUT pdfmark\n\n'; 21 | 22 | info.positions.pages.forEach(function (page, i) { 23 | page.links.forEach(function (link) { 24 | pdfMarks += '[\n'; 25 | pdfMarks += '/Rect [ ' + link.xMin.toFixed() + ' ' + link.yMin.toFixed() + ' ' + link.xMax.toFixed() + ' ' + link.yMax.toFixed() + ' ]\n'; 26 | //pdfMarks += '/Border [ 0 0 1 ]\n'; 27 | //pdfMarks += '/Color [ 0 0 1 ]\n'; 28 | pdfMarks += '/Page ' + (firstChapterPageNumber + link.page) + '\n'; 29 | pdfMarks += '/SrcPg ' + (i + 2 + info.preContent.extras.numberOfPages + info.preContent.intro.numberOfPages) + '\n'; 30 | pdfMarks += '/Subtype /Link\n'; 31 | pdfMarks += '/ANN pdfmark\n\n'; 32 | }); 33 | }); 34 | 35 | return pdfMarks; 36 | } 37 | 38 | function updatePageNumberInfo(inputFile, pageInfo, pageInfoFile, outputFile) { 39 | console.log('gs - Preparing to update page number info...'); 40 | var d = Q.defer(); 41 | return Q(). 42 | then(function (output) { 43 | return Q.nfcall(fs.writeFile, pageInfoFile, _pageNumberInfo(pageInfo), { 44 | encoding: 'ascii' 45 | }); 46 | }). 47 | then(function () { 48 | var pdfSettings = pageInfo.options.pdfImageQuality || 'prepress'; 49 | var gsCall = 'gs -q -dPDFSETTINGS=/' + pdfSettings + ' -o ' + outputFile + ' -sDEVICE=pdfwrite ' + inputFile + ' ' + pageInfoFile; 50 | 51 | console.log('gs - Calling gs...'); 52 | console.log(gsCall); 53 | 54 | exec(gsCall, function (error, stdout, stderr) { 55 | if (error) { 56 | console.log('gs - Error while updating page number info. :/'); 57 | return d.reject(error); 58 | } 59 | console.log('gs - Updated page number info! :)'); 60 | return d.resolve(); 61 | }); 62 | return d.promise; 63 | }); 64 | } 65 | 66 | /* 67 | References: 68 | 69 | http://askubuntu.com/questions/32048/renumber-pages-of-a-pdf 70 | http://superuser.com/questions/232553/how-to-change-internal-page-numbers-in-the-meta-data-of-a-pdf 71 | http://www.ghostscript.com/doc/current/Use.htm 72 | */ 73 | 74 | module.exports = { 75 | updatePageNumberInfo: updatePageNumberInfo 76 | }; 77 | -------------------------------------------------------------------------------- /pre-content/pdf/test/unit/pdftk-bookmarkInfo-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var proxyquire = require("proxyquire"); 3 | 4 | var childProcessStub = {}; 5 | var fsStub = {}; 6 | 7 | var pdftk = proxyquire("./../../pdftk.js", {"child_process": childProcessStub, "fs": fsStub}); 8 | 9 | describe('pdftk', function(){ 10 | it('should generate one level bookmark info', function(){ 11 | 12 | var info = { 13 | "toc":[ 14 | { 15 | "title":"1 Introdução", 16 | "pageNumber":3 17 | } 18 | ], 19 | "preContent": { "numberOfPages": 9 } 20 | }; 21 | 22 | fsStub.writeFile = function(infoFile, bookmarkInfo) { 23 | assert.equal("info.txt", infoFile); 24 | 25 | assert.equal("string", typeof bookmarkInfo); 26 | assert(bookmarkInfo.length > 0); 27 | var lines = bookmarkInfo.split("\n"); 28 | assert.equal(5, lines.length); 29 | assert.equal("BookmarkBegin", lines[0]); 30 | assert.equal("BookmarkTitle: 1 Introdução", lines[1]); 31 | assert.equal("BookmarkLevel: 1", lines[2]); 32 | assert.equal("BookmarkPageNumber: 13", lines[3]); 33 | assert.equal("", lines[4]); 34 | }; 35 | 36 | childProcessStub.exec = function(pdftkCall, fn){ 37 | assert.equal('pdftk input.pdf update_info info.txt output output.pdf', pdftkCall); 38 | fn(); 39 | }; 40 | 41 | pdftk.updateBookmarkInfo('input.pdf', info, 'info.txt', 'output.pdf').done(); 42 | 43 | }); 44 | 45 | it('should generate two level bookmark info', function(){ 46 | 47 | var info = { 48 | "toc":[ 49 | { 50 | "title":"1 Introdução", 51 | "pageNumber":3, 52 | "sections":[ 53 | { 54 | "title":"1.1 Mantendo o histórico do código", 55 | "pageNumber":3 56 | } 57 | ] 58 | } 59 | ], 60 | "preContent": { "numberOfPages": 9 } 61 | }; 62 | 63 | fsStub.writeFile = function(infoFile, bookmarkInfo) { 64 | assert.equal("info.txt", infoFile); 65 | 66 | assert.equal("string", typeof bookmarkInfo); 67 | assert(bookmarkInfo.length > 0); 68 | var lines = bookmarkInfo.split("\n"); 69 | assert.equal(9, lines.length); 70 | assert.equal("BookmarkBegin", lines[0]); 71 | assert.equal("BookmarkTitle: 1 Introdução", lines[1]); 72 | assert.equal("BookmarkLevel: 1", lines[2]); 73 | assert.equal("BookmarkPageNumber: 13", lines[3]); 74 | assert.equal("BookmarkBegin", lines[4]); 75 | assert.equal("BookmarkTitle: 1.1 Mantendo o histórico do código", lines[5]); 76 | assert.equal("BookmarkLevel: 2", lines[6]); 77 | assert.equal("BookmarkPageNumber: 13", lines[7]); 78 | assert.equal("", lines[8]); 79 | }; 80 | 81 | childProcessStub.exec = function(pdftkCall, fn){ 82 | assert.equal('pdftk input.pdf update_info info.txt output output.pdf', pdftkCall); 83 | fn(); 84 | }; 85 | 86 | pdftk.updateBookmarkInfo('input.pdf', info, 'info.txt', 'output.pdf').done(); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /pre-content/pdf/toc.js: -------------------------------------------------------------------------------- 1 | var Q = require('q'); 2 | var xml2js = require('xml2js'); 3 | var he = require('he'); 4 | var cheerio = require('cheerio'); 5 | 6 | var pdfToText = require('./../../cmd/pdfToText'); 7 | 8 | function _findLinkPageNumber(words, i, headers) { 9 | while (headers.indexOf(words[i]._) !== -1) { 10 | i++; 11 | } 12 | do { 13 | var linkPage = Number(words[i]._); 14 | if (!isNaN(linkPage)) { 15 | return linkPage; 16 | } 17 | i++; 18 | } while (i < words.length); 19 | return; 20 | } 21 | 22 | function update(toc, pdfInfo) { 23 | //Atualiza numero de paginas do toc, 24 | //para o primeiro capitulo comecar na pagina 1 25 | var chapterNum = 1; 26 | 27 | function _chapterPrefix() { 28 | //quando tiver partes, nao insere numero no nivel de chapter 29 | if (pdfInfo.options.partHeaders && pdfInfo.options.partHeaders.length) { 30 | return ''; 31 | } 32 | return chapterNum++ + ' '; 33 | } 34 | 35 | function _sectionPrefix() { 36 | //quando tiver partes, insere numero no nivel de section 37 | if (pdfInfo.options.partHeaders && pdfInfo.options.partHeaders.length) { 38 | return chapterNum++ + ' '; 39 | } 40 | return ''; 41 | } 42 | 43 | function _updateSubSection(subSection) { 44 | var updatedSubSection = { 45 | title: subSection.title, 46 | pageNumber: subSection.pageNumber - pdfInfo.content.pageNumberOffset 47 | }; 48 | return updatedSubSection; 49 | } 50 | 51 | function _updateSection(section) { 52 | var updatedSection = { 53 | title: _sectionPrefix() + section.title, 54 | pageNumber: section.pageNumber - pdfInfo.content.pageNumberOffset 55 | }; 56 | var updatedSubSections = []; 57 | section.subSections.forEach(function (subSection) { 58 | updatedSubSections.push(_updateSubSection(subSection)); 59 | }); 60 | updatedSection.subSections = updatedSubSections; 61 | return updatedSection; 62 | } 63 | 64 | function updateChapter(chapter) { 65 | var updatedChapter = { 66 | title: _chapterPrefix() + chapter.title, 67 | pageNumber: chapter.pageNumber - pdfInfo.content.pageNumberOffset 68 | }; 69 | var updatedSections = []; 70 | chapter.sections.forEach(function (section) { 71 | updatedSections.push(_updateSection(section)); 72 | }); 73 | updatedChapter.sections = updatedSections; 74 | return updatedChapter; 75 | } 76 | 77 | var updatedToc = []; 78 | toc.forEach(function (chapter) { 79 | updatedToc.push(updateChapter(chapter)); 80 | }); 81 | pdfInfo.toc = updatedToc; 82 | return updatedToc; 83 | 84 | } 85 | 86 | function _getLink(words, page, word, i, title, positionTitle, headers) { 87 | var decodedSpacelessTitle = he.decode(title.replace(/\s/g, '')); 88 | if (decodedSpacelessTitle === positionTitle) { 89 | var link = { 90 | xMin: Number(word.$.xMin), 91 | xMax: Number(word.$.xMax), 92 | yMin: page.$.height - Number(word.$.yMin), 93 | yMax: page.$.height - Number(word.$.yMax) 94 | }; 95 | var linkPage = _findLinkPageNumber(words, i, headers); 96 | if (linkPage) { 97 | link.page = linkPage; 98 | return link; 99 | } 100 | return; 101 | } 102 | } 103 | 104 | function _headerText(pdfInfo) { 105 | //skips header text 106 | var $ = cheerio.load(pdfInfo.options.pdf.summary.headerTemplate); 107 | var headers = {}; 108 | $('*').each(function () { 109 | headers[$(this).text()] = true; 110 | }); 111 | return Object.keys(headers); 112 | } 113 | 114 | function _positionXmlToJs(xml) { 115 | var d = Q.defer(); 116 | return Q().then(function () { 117 | xml2js.parseString(xml, function (err, result) { 118 | if (err) { 119 | console.log('Error transforming position xml to js... :/'); 120 | return d.reject(err); 121 | } 122 | console.log('Transformed position xml to js! :)'); 123 | return d.resolve(result); 124 | }); 125 | return d.promise; 126 | }); 127 | } 128 | 129 | function findLinkPositions(tocPdf, pdfInfo) { 130 | return Q() 131 | .then(function () { 132 | return pdfToText.extractTextPositions(tocPdf); 133 | }) 134 | .then(_positionXmlToJs) 135 | .then(function (positions) { 136 | console.log('Building pdf links...'); 137 | var headers = _headerText(pdfInfo); 138 | 139 | var pages = positions.html.body[0].doc[0].page; 140 | pages.forEach(function (page, i) { 141 | var words = page.word; 142 | var pageInfo = { 143 | links: [] 144 | }; 145 | words.forEach(function (word, i) { 146 | var positionTitle = word._.replace(/\s/g, ''); 147 | pdfInfo.toc.forEach(function (chapter) { 148 | var link = _getLink(words, page, word, i, chapter.title, positionTitle, headers); 149 | if (link) { 150 | pageInfo.links.push(link); 151 | } else { 152 | chapter.sections.forEach(function (section) { 153 | var link = _getLink(words, page, word, i, section.title, positionTitle, headers); 154 | if (link) { 155 | pageInfo.links.push(link); 156 | } else { 157 | section.subSections.forEach(function (subSection) { 158 | var link = _getLink(words, page, word, i, subSection.title, positionTitle, headers); 159 | if (link) { 160 | pageInfo.links.push(link); 161 | } 162 | }); 163 | } 164 | }); 165 | } 166 | }); 167 | }); 168 | pdfInfo.positions.pages.push(pageInfo); 169 | }); 170 | console.log('Built pdf links...'); 171 | }) 172 | .then(function () { 173 | return tocPdf; 174 | }); 175 | } 176 | 177 | function getTocItemsByPageNumber(pdfInfo) { 178 | var tocItemsByPageNumber = {}; 179 | 180 | function addTocItem(pageNumber, tocItem) { 181 | if (!tocItemsByPageNumber[pageNumber]) { 182 | tocItemsByPageNumber[pageNumber] = [tocItem]; 183 | } else { 184 | tocItemsByPageNumber[pageNumber].push(tocItem); 185 | } 186 | } 187 | 188 | //fazer objeto pageNum -> title 189 | pdfInfo.toc.forEach(function (chapter) { 190 | var chapterTocItem = { 191 | type: 'chapter', 192 | title: he.decode(chapter.title) 193 | }; 194 | addTocItem(chapter.pageNumber, chapterTocItem); 195 | chapter.sections.forEach(function (section) { 196 | var sectionTocItem = { 197 | type: 'section', 198 | title: he.decode(section.title), 199 | chapter: chapterTocItem 200 | }; 201 | addTocItem(section.pageNumber, sectionTocItem); 202 | section.subSections.forEach(function (subSection) { 203 | var subSectionTocItem = { 204 | type: 'subSection', 205 | title: he.decode(subSection.title), 206 | section: sectionTocItem 207 | }; 208 | addTocItem(subSection.pageNumber, subSectionTocItem); 209 | }); 210 | }); 211 | }); 212 | 213 | //expandir pageNums 214 | var tocItemsByPageNumberExpanded = {}; 215 | var previousPageNum; 216 | Object.keys(tocItemsByPageNumber).forEach(function (pageNumber) { 217 | pageNumber = parseInt(pageNumber, 10); 218 | if (previousPageNum && pageNumber - previousPageNum > 1) { 219 | var i; 220 | for (i = previousPageNum + 1; i < pageNumber; i++) { 221 | tocItemsByPageNumberExpanded[i] = tocItemsByPageNumber[previousPageNum].slice(-1)[0]; 222 | } 223 | } 224 | tocItemsByPageNumberExpanded[pageNumber] = tocItemsByPageNumber[pageNumber][0]; 225 | previousPageNum = pageNumber; 226 | }); 227 | if (pdfInfo.content.numberOfPages - previousPageNum > 0) { 228 | var i; 229 | for (i = previousPageNum + 1; i <= pdfInfo.content.numberOfPages; i++) { 230 | tocItemsByPageNumberExpanded[i] = tocItemsByPageNumber[previousPageNum].slice(-1)[0]; 231 | } 232 | } 233 | return tocItemsByPageNumberExpanded; 234 | } 235 | 236 | module.exports = { 237 | update: update, 238 | findLinkPositions: findLinkPositions, 239 | tocItemsByPageNumber: getTocItemsByPageNumber 240 | }; -------------------------------------------------------------------------------- /pre-content/pdf/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | var fs = require('fs-extra'); 4 | var Q = require('q'); 5 | 6 | var tocHandler = require('./toc.js'); 7 | var fileHelper = require('./../../helpers/fileHelper'); 8 | var imageHelper = require('./../../helpers/imageHelper'); 9 | var pdftk = require('./../../cmd/pdftk'); 10 | var gs = require('./../../cmd/gs'); 11 | var calibre = require('./../../cmd/calibre'); 12 | var htmlRenderer = require('./../../renderers/html'); 13 | var mdRenderer = require('./../../renderers/md'); 14 | 15 | function _handlePreContent(inputDir, outputDir, tocPDF, pdfInfo) { 16 | var extrasDir = process.env.EXTRAS_DIR || ''; 17 | var inBookExtrasDir = path.join(inputDir, 'extras'); 18 | var introDir = path.join(outputDir, 'intro'); 19 | 20 | var extraFiles = []; 21 | var introFiles = []; 22 | 23 | var preContentFiles = []; 24 | 25 | return Q().then(function () { 26 | return fileHelper.listFilesByExtension(extrasDir, '.pdf'); 27 | }).then(function (extras) { 28 | if (extras.length) { 29 | console.log('Extra files from ' + extrasDir + ': ' + extras.join(',')); 30 | } 31 | extraFiles = extras; 32 | }).then(function () { 33 | return fileHelper.listFilesByExtension(inBookExtrasDir, '.pdf'); 34 | }).then(function (extras) { 35 | if (extras.length) { 36 | console.log('Extra files from ' + inBookExtrasDir + ': ' + extras.join(',')); 37 | } 38 | extras.forEach(function (file) { 39 | extraFiles.push(file); 40 | }); 41 | }).then(function () { 42 | return fileHelper.listFilesByExtension(introDir, '.md'); 43 | }).then(function (introMDs) { 44 | if (introMDs.length) { 45 | console.log('Intro files from ' + introDir + ': ' + introMDs.join(',')); 46 | } 47 | introMDs.forEach(function (file) { 48 | var pdfFile = file.replace('.md', '.pdf'); 49 | introFiles.push(pdfFile); 50 | }); 51 | return introMDs; 52 | }).then(function (introMDs) { 53 | return mdRenderer.renderPdfs(introMDs, pdfInfo.options.pdf.introTemplate, pdfInfo); 54 | }).then(function () { 55 | preContentFiles = extraFiles.concat(introFiles); 56 | preContentFiles.push(tocPDF); 57 | }).then(function () { 58 | return pdftk.extractNumberOfPagesFromFiles(extraFiles); 59 | }).then(function (numberOfPages) { 60 | pdfInfo.preContent.extras.numberOfPages = numberOfPages; 61 | }).then(function () { 62 | return pdftk.extractNumberOfPagesFromFiles(introFiles); 63 | }).then(function (numberOfPages) { 64 | pdfInfo.preContent.intro.numberOfPages = numberOfPages; 65 | }).then(function () { 66 | return pdftk.extractNumberOfPagesFromFiles([tocPDF]); 67 | }).then(function (numberOfPages) { 68 | pdfInfo.preContent.toc.numberOfPages = numberOfPages; 69 | }).then(function () { 70 | pdfInfo.preContent.files = preContentFiles; 71 | }); 72 | } 73 | 74 | function _renderTocPDF(outputDir, pdfInfo) { 75 | pdfInfo.tocHTML = path.join(outputDir, 'toc.html'); 76 | return Q().then(function () { 77 | return pdftk.extractTOC(pdfInfo.originalPDF); 78 | }).then(function (toc) { 79 | return tocHandler.update(toc, pdfInfo); 80 | }).then(function () { 81 | var tocOptions = { 82 | chapters: pdfInfo.toc, 83 | options: pdfInfo 84 | }; 85 | return htmlRenderer.render(tocOptions, pdfInfo.options.pdf.tocTemplate); 86 | }).then(function (html) { 87 | return Q.nfcall(fs.writeFile, pdfInfo.tocHTML, html); 88 | }).then(function () { 89 | var pdfOptions = { 90 | '--pdf-page-numbers': null, 91 | '--disable-font-rescaling': true, 92 | '--paper-size': null, 93 | '--unit': 'millimeter', 94 | '--chapter': '/', 95 | '--page-breaks-before': '/', 96 | '--custom-size': pdfInfo.options.pdf.customSize, 97 | '--margin-left': pdfInfo.options.pdf.margin.left, 98 | '--margin-right': pdfInfo.options.pdf.margin.right, 99 | '--margin-top': pdfInfo.options.pdf.margin.top, 100 | '--margin-bottom': pdfInfo.options.pdf.margin.bottom, 101 | '--pdf-default-font-size': pdfInfo.options.pdf.fontSize, 102 | '--pdf-mono-font-size': pdfInfo.options.pdf.fontSize, 103 | '--pdf-header-template': pdfInfo.options.pdf.summary.headerTemplate, 104 | '--pdf-footer-template': pdfInfo.options.pdf.summary.footerTemplate 105 | }; 106 | pdfInfo.tocPDF = path.join(outputDir, './toc.pdf'); 107 | return calibre.generate(pdfInfo.tocHTML, pdfInfo.tocPDF, pdfOptions); 108 | }).then(function () { 109 | return pdfInfo.tocPDF; 110 | }); 111 | } 112 | 113 | function _headerAndFooter(pdfInfo) { 114 | return Q().then(function () { 115 | pdfInfo.headerFooterDir = path.join(pdfInfo.options.output, './header-footer'); 116 | return Q.nfcall(fs.mkdir, pdfInfo.headerFooterDir); 117 | }).then(function () { 118 | var tocItemsByPageNumber = tocHandler.tocItemsByPageNumber(pdfInfo); 119 | //renderizar pdf com cabeçalho e rodapé (inclusive imagens) 120 | var promises = []; 121 | var pageNumbers = Object.keys(tocItemsByPageNumber); 122 | var lastPageNumber = pageNumbers.length; 123 | pageNumbers.forEach(function (pageNumber) { 124 | pageNumber = parseInt(pageNumber, 10); 125 | var next; 126 | if (pageNumber + 1 <= lastPageNumber) { 127 | next = pageNumber + 1; 128 | } 129 | var tocItem = tocItemsByPageNumber[pageNumber]; 130 | var promise = Q().then(function () { 131 | var headerFooterOptions = { 132 | tocItem: tocItem, 133 | pageNumber: pageNumber, 134 | next: next, 135 | options: pdfInfo 136 | }; 137 | return htmlRenderer.render(headerFooterOptions, pdfInfo.options.pdf.headerFooterTemplate); 138 | }).then(function (html) { 139 | var headerFooterPath = path.join(pdfInfo.headerFooterDir, './' + pageNumber + '.html'); 140 | return Q.nfcall(fs.writeFile, headerFooterPath, html); 141 | }); 142 | promises.push(promise); 143 | }); 144 | return Q.all(promises); 145 | }).then(function () { 146 | return Q() 147 | .then(function () { 148 | var headerFooterOptions = { 149 | tocItem: null, 150 | pageNumber: null, 151 | next: 'blank-toc', 152 | options: pdfInfo 153 | }; 154 | return htmlRenderer.render(headerFooterOptions, pdfInfo.options.pdf.headerFooterTemplate); 155 | }) 156 | .then(function (html) { 157 | var headerFooterPath = path.join(pdfInfo.headerFooterDir, './blank-cover.html'); 158 | return Q.nfcall(fs.writeFile, headerFooterPath, html); 159 | }) 160 | .then(function () { 161 | var headerFooterOptions = { 162 | tocItem: null, 163 | pageNumber: null, 164 | next: '1', 165 | options: pdfInfo 166 | }; 167 | return htmlRenderer.render(headerFooterOptions, pdfInfo.options.pdf.headerFooterTemplate); 168 | }) 169 | .then(function (html) { 170 | var headerFooterPath = path.join(pdfInfo.headerFooterDir, './blank-toc.html'); 171 | return Q.nfcall(fs.writeFile, headerFooterPath, html); 172 | }); 173 | }).then(function () { 174 | var pdfOptions = { 175 | '--pdf-page-numbers': null, 176 | '--disable-font-rescaling': true, 177 | '--paper-size': null, 178 | '--unit': 'millimeter', 179 | '--chapter': '/', 180 | '--page-breaks-before': '/', 181 | '--custom-size': pdfInfo.options.pdf.customSize, 182 | '--margin-left': pdfInfo.options.pdf.margin.left, 183 | '--margin-right': pdfInfo.options.pdf.margin.right, 184 | '--margin-top': pdfInfo.options.pdf.margin.top, 185 | '--margin-bottom': pdfInfo.options.pdf.margin.bottom, 186 | '--pdf-default-font-size': pdfInfo.options.pdf.fontSize, 187 | '--pdf-mono-font-size': pdfInfo.options.pdf.fontSize, 188 | '--pdf-header-template': null, 189 | '--pdf-footer-template': null, 190 | '--max-levels': pdfInfo.content.originalNumberOfPages, 191 | '--breadth-first': true 192 | }; 193 | var firstHeaderFooterHtmlPath = path.join(pdfInfo.headerFooterDir, './blank-cover.html'); 194 | pdfInfo.headerFooterPath = path.join(pdfInfo.headerFooterDir, './header-footer.pdf'); 195 | return calibre.generate(firstHeaderFooterHtmlPath, pdfInfo.headerFooterPath, pdfOptions); 196 | }).then(function () { 197 | pdfInfo.pdfWithHeaderAndFooter = path.join(pdfInfo.options.output, './index-with-header-and-footer.pdf'); 198 | return pdftk.multistamp(pdfInfo.headerFooterPath, pdfInfo.originalPDF, pdfInfo.pdfWithHeaderAndFooter); 199 | }); 200 | } 201 | 202 | function addPreContent() { 203 | var command = this.options._name; 204 | 205 | var inputDir = this.options.input; 206 | var outputDir = this.options.output; 207 | 208 | var pdfWithPreContent = path.join(outputDir, './index-with-pre-content.pdf'); 209 | 210 | var pdfWithBookmarkInfo = path.join(outputDir, './index-with-pre-content-and-bookmarks.pdf'); 211 | var pdfWithPageNumberInfo = path.join(outputDir, './index-with-pre-content-bookmarks-and-page-numbers.pdf'); 212 | 213 | var pdfInfo = { 214 | book: { 215 | author: this.options.author, 216 | publisher: this.options.publisher, 217 | title: this.options.title, 218 | version: this.options.version 219 | }, 220 | content: { 221 | //O toc original, gerado pelo gitbook/calibre, tem sempre apenas uma pagina. 222 | //isso é garantido pq o conteudo do toc original nao é visivel (display:none). 223 | //descontando 1 pagina para a capa + uma pagina para o toc original 224 | pageNumberOffset: 2, 225 | originalNumberOfPages: 0, 226 | numberOfPages: 0 227 | }, 228 | preContent: { 229 | extras: { 230 | numberOfPages: 0 231 | }, 232 | intro: { 233 | numberOfPages: 0 234 | }, 235 | toc: { 236 | numberOfPages: 0 237 | } 238 | }, 239 | positions: { 240 | pages: [] 241 | }, 242 | parts: { 243 | status: {}, 244 | mds: [], 245 | pdfs: [] 246 | }, 247 | options: this.options, 248 | originalPDF: path.join(outputDir, './index.pdf'), 249 | hasParts: this.options.partHeaders && this.options.partHeaders.length > 0, 250 | css: this.plugins.resources.css, 251 | cssPath: path.join(fileHelper.outputPath(this.options), './gitbook') 252 | }; 253 | 254 | return Q() 255 | .then(function () { 256 | return pdftk.extractNumberOfPagesFromFiles([pdfInfo.originalPDF]); 257 | }).then(function (numberOfPages) { 258 | pdfInfo.content.originalNumberOfPages = numberOfPages; 259 | pdfInfo.content.numberOfPages = numberOfPages - pdfInfo.content.pageNumberOffset; 260 | }).then(function () { 261 | return _renderTocPDF(outputDir, pdfInfo); 262 | }).then(function (tocPDF) { 263 | return tocHandler.findLinkPositions(tocPDF, pdfInfo); 264 | }).then(function (tocPDF) { 265 | return _handlePreContent(inputDir, outputDir, tocPDF, pdfInfo); 266 | }).then(function () { 267 | return _headerAndFooter(pdfInfo); 268 | }).then(function () { 269 | return pdftk.join(pdfInfo.pdfWithHeaderAndFooter, pdfInfo.preContent.files, pdfWithPreContent); 270 | }).then(function () { 271 | var pdfBookmarkInfoFile = path.join(outputDir, './bookmark-info.txt'); 272 | return pdftk.updateBookmarkInfo(pdfWithPreContent, pdfInfo, pdfBookmarkInfoFile, pdfWithBookmarkInfo); 273 | }).then(function () { 274 | var pdfPageNumberInfoFile = path.join(outputDir, './page-number-info.txt'); 275 | return gs.updatePageNumberInfo(pdfWithBookmarkInfo, pdfInfo, pdfPageNumberInfoFile, pdfWithPageNumberInfo); 276 | }).then(function () { 277 | if (command === 'pdf') { 278 | return Q.nfcall(fs.copy, pdfWithPageNumberInfo, pdfInfo.originalPDF); 279 | } 280 | return Q(); 281 | }); 282 | } 283 | 284 | module.exports = { 285 | 'addPreContent': addPreContent 286 | }; 287 | -------------------------------------------------------------------------------- /renderHooks/index.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | var cheerio = require('cheerio'); 5 | var kramed = require('kramed'); 6 | 7 | var fileHelper = require('./../helpers/fileHelper'); 8 | var imageHelper = require('./../helpers/imageHelper'); 9 | 10 | var CHAPTER_HEADER_TITLE = 'Capítulo '; 11 | var CAPTION_PREFIX = 'Figura '; 12 | var TOC_TITLE = 'Sumário'; 13 | 14 | function handleSummaryAfter(summary) { 15 | var options = this.options; 16 | 17 | _renderIntro(options); 18 | 19 | _renderParts(summary, options); 20 | 21 | return summary; 22 | } 23 | 24 | function handlePageBefore(page) { 25 | var maxLength = this.options.maxLineLength || 80; 26 | var filename = page.path == 'README.md' ? this.options.firstChapter + '.md' : page.path; 27 | var dentroDeCode = false; 28 | 29 | //nao foi utilizada regex para manter numero de linha (i) 30 | page.content.split('\n').forEach(function (line, i) { 31 | if (!dentroDeCode && line.trim().indexOf('```') == 0) { 32 | dentroDeCode = true; //comecando um novo code 33 | if (line.trim() != '```' && line.trim().lastIndexOf('```') == line.trim().length - 3) { 34 | dentroDeCode = false; //code de uma linha só 35 | } 36 | } else if (dentroDeCode && line.trim().indexOf('```') == 0) { 37 | dentroDeCode = false; //terminando um code anterior 38 | } 39 | 40 | if (dentroDeCode && line.length > maxLength) { 41 | console.log('warning: code "' + line.trim().substring(0, 30) + '"... too long. was ' + line.length + ' (max ' + maxLength + ') in ' + filename + ':' + (i + 1)); 42 | } 43 | }); 44 | return page; 45 | } 46 | 47 | function handlePage(page) { 48 | var options = this.options; 49 | 50 | var chapter = page.progress.current; 51 | _verifyChapterTitle(chapter); 52 | 53 | page.sections.forEach(function (section) { 54 | if (section.type === 'normal') { 55 | var $ = cheerio.load(section.content); 56 | _addSectionNumbers($, chapter, section, options); 57 | _adjustImages($, chapter, section, options); 58 | _removeComments($, section); 59 | } 60 | }); 61 | 62 | return page; 63 | } 64 | 65 | var partsAddedToPages = {}; 66 | function handlePageAfter(page) { 67 | var options = this.options; 68 | 69 | var chapter = page.progress.current; 70 | 71 | if (options.partsByPathOfFirstChapter) { 72 | var part = options.partsByPathOfFirstChapter[chapter.path]; 73 | if (part && !partsAddedToPages[part.title]) { 74 | var partHeaderHtml = part.partHeaderHtml; 75 | var partHeader = '
_SECTION_
", //String com um template html para o cabeçalho de cada página 120 | "footerTemplate": "_PAGENUM_
", //String com um template html para o rodapé de cada página 121 | "summary": { 122 | "headerTemplate": "Sumário
", //String com template do cabeçalho do sumário 123 | "footerTemplate": "Casa do Código
" //String com template do rodapé do sumário 124 | } 125 | } 126 | ``` 127 | 128 | Tanto para o `headerTemplate` como para o `footerTemplate`, podem ser usadas as seguintes variáveis: 129 | * `_PAGENUM_`, que contém o número da página atual 130 | * `_SECTION`, que contém o nome da seção atual 131 | * `_TITLE_`, que contém o título do livro 132 | * `_AUTHOR_`, que contém o nome do autor 133 | 134 | Os valores padrão das propriedades são os seguintes: 135 | * `pdf.fontSize`: tamanho do texto. O padrão, definido pelo Gitbook, é 12. 136 | * `pdf.margin.left`: define a margem esquerda do pdf em pts. O padrão, definido pelo Gitbook, é 62. 137 | * `pdf.margin.right`: define a margem direita do pdf em pts. O padrão, definido pelo Gitbook, é 62. 138 | * `pdf.margin.top`: define a margem de cima do pdf em pts. O padrão, definido pelo Gitbook, é 36. 139 | * `pdf.margin.bottom`: define a margem de baixo do pdf em pts. O padrão, definido pelo Gitbook, é 36. 140 | 141 | ## Inserindo conteúdo antes do sumário 142 | 143 | Existem alguns conteúdos que não fazem parte do texto do livro em si. 144 | 145 | Alguns são conteúdos estáticos como copyright e propagandas. 146 | 147 | Outros são conteúdos dinâmicos como prefácio, apresentação dos autores e agradecimentos. Estes conteúdos são parte do livro, mas vem antes do sumário. 148 | 149 | ### Conteúdo estático antes do sumário 150 | 151 | Conteúdos estáticos como copyright e propagandas, que são sempre iguais para todos os livros, devem ser colocados em arquivos `.pdf` no diretório configurado pela variável de ambiente `EXTRAS_DIR`. 152 | 153 | Os `.pdf` de `EXTRAS_DIR` serão ordenados por nome e inseridos logo depois da capa do livro. 154 | 155 | Também é possível definir arquivos `.pdf` dentro do diretório do livro, no sub-diretório `extras`. Se o `EXTRAS_DIR` já estiver configurado, os `.pdf` de `extras` virão logo depois. 156 | 157 | ### Conteúdo dinâmico antes do sumário 158 | 159 | Conteúdos do livro como prefácio, apresentação dos autores e agradecimentos devem ser colocados em arquivos `.md` dentro do diretório `intro`. 160 | 161 | Ao gerar um `pdf`, cada arquivo `.md` do diretório `intro` será transformado em um `.pdf` e inserido logo antes do sumário (mas depois dos extras). 162 | 163 | Os `.md` serão ordenados pelo nome do arquivo antes de serem renderizados e inseridos. 164 | 165 | ## Código 166 | 167 | Este plugin está organizado em dois sub-módulos: 168 | * **ebook**: manipula conteúdo do livro como nomes das seções, legenda de imagens, etc... 169 | * **pre-content**: insere conteúdo _no início_ do livro como copyright, propagandas, apresentação dos autores, etc... 170 | 171 | ### [index.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/index.js) 172 | 173 | Configura hooks do gitbook. 174 | 175 | Os hooks configurados são: 176 | * `page`: disparado logo depois ter renderizado o .md de cada página em um .html, chama a função _handlePage_ do sub-módulo _ebook_ 177 | * `page:after`: disparado depois do gitbook manipular o .html de cada página, chama a função _handlePageAfter_ do sub-módulo _ebook_ 178 | * `ebook:before`: hook customizado, criado a partir [de um patch](https://raw.githubusercontent.com/alexandreaquiles/gitbook/32c941569e547045a13bd6c2835737b1cd2a6a8c/lib/generate/ebook/index.js). É disparado logo antes da chamada do calibre para a geração do ebook. Chama a função _handleEbookBefore_ do sub-módulo _ebook_ 179 | * `finish`: disparado ao fim da geração do ebook ou site, chama a função _finish_ do sub-módulo _pre-content_ 180 | 181 | As funções acima são chamadas com código do tipo `funcao.call(this, argumento)`, para que o `this` do gitbook seja propagado para cada função. 182 | 183 | ___ 184 | 185 | ### [util.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/util.js) 186 | Contém as funções comuns: 187 | 188 | #### obtainExtension 189 | Função que obtém a extensão do livro a ser gerado. 190 | 191 | Parâmetros: 192 | * options - `Object` com as configurações do `book.json` do gitbook. 193 | 194 | Retorno: 195 | * uma `String` com a extensão do livro a ser gerado (pdf, mobi ou epub). 196 | 197 | Se não houver como descobrir a extensão do livro e o formato for _ebook_, é considerado o "pdf". 198 | 199 | Se o formato for _site_, o retorno é "", uma String vazia. 200 | 201 | ___ 202 | 203 | ### [ebook/index.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/ebook/index.js) 204 | 205 | Manipula conteúdo do livro. 206 | 207 | Dependências externas: 208 | * [cheerio](https://github.com/cheeriojs/cheerio), uma implementação enxuta do jQuery para Node.js. 209 | 210 | #### handlePage 211 | Função que manipula conteúdo da página renderizada. 212 | 213 | Parâmetros: 214 | * page - `Object` com as informações sobre a página que será renderizada. 215 | 216 | Retorno: 217 | * um `Object`com a page com conteúdo manipulado. 218 | 219 | Se o título capítulo começar com números, será lançada uma exceção. Essa limitação tem a ver com a maneira com que é feito o cabeçalho das páginas no calibre (`pdf.headerTemplate` do `book.json`). 220 | 221 | O gitbook/calibre não coloca números antes das seções (p. ex.: _**1.2** Integração com tecnologias do JavaEE_). Por isso, para cada `h2` de cada seção da página, é inserido o número do capítulo seguido por um número sequencial para cada seção. 222 | 223 | Para cada `img` da página, é extraído do `alt` configurações de largura da imagem (ex.: `{w=60%}`). Se a extensão do livro for _pdf_ e houver largura configurada, é colocado um `width` no `img`. Além disso, é inserido um `div` com um `p` que serve como legenda para a imagem. 224 | 225 | Todos os comentários html são removidos do livro. 226 | 227 | #### handlePageAfter 228 | Função que manipula conteúdo da página renderizada. 229 | 230 | Parâmetros: 231 | * page - `Object` com as informações sobre a página que será renderizada. 232 | 233 | Retorno: 234 | * um `Object`com a page com conteúdo manipulado. 235 | 236 | Antes de cada `h1` que contém o título do capítulo, é inserido um `div` com o número do capítulo (ex.: _Capítulo 1_). 237 | 238 | #### handleEbookBefore 239 | Função que altera opções do [ebook-convert do calibre](http://manual.calibre-ebook.com/cli/ebook-convert.html) 240 | 241 | Parâmetros: 242 | * options - `Object` com configurações que vão ser passadas para o calibre. 243 | 244 | Retorno: 245 | * um `Object`com as opções do calibre alteradas. 246 | 247 | Essa função é chamada no hook `ebook:before`, que não é padrão do gitbook. Esse hook foi criado a partir [de um patch](https://raw.githubusercontent.com/alexandreaquiles/gitbook/32c941569e547045a13bd6c2835737b1cd2a6a8c/lib/generate/ebook/index.js). 248 | 249 | É disparado logo antes da chamada do calibre para a geração do ebook. 250 | 251 | Não é chamado na geração de _site_. 252 | 253 | São alteradas [configurações do ebook-convert](http://manual.calibre-ebook.com/cli/ebook-convert.html) do calibre que não são inseridas pelo gitbook como: 254 | * `--publisher`: nome da editora, obtido a partir da propriedade `publisher` do `book.json` 255 | * `--chapter-mark`: colocado para `none`, para que não sejam colocadas quebras de página no início de cada capítulo. As quebras de página devem ser controladas por css. 256 | * `--level2-toc`: configura a detecção de seções para considerar `h2`. Utilizado na geração do sumário pelo calibre. 257 | * `--level2-toc`: setado para `null`, de maneira a desconsiderar títulos (na verdade, `h3`) na geração do sumário pelo calibre. 258 | 259 | Se a extensão do livro a ser gerado for `mobi`, são configuradas as seguintes opções: 260 | * `--mobi-keep-original-images`: setada para `true`, fazendo com que o calibre não comprima as imagens 261 | * `--toc-title`: modificado para _Sumário_ 262 | 263 | Se a extensão do livro a ser gerado for `pdf`, são configuradas também as seguintes opções: 264 | * `--pdf-page-numbers`: setado para _null_ 265 | * `--disable-font-rescaling`: setado para _true_ 266 | * `--paper-size`: setado para _null_ 267 | * `--custom-size`: utilizado o valor da propriedade `pdf.customSize` do `book.json` 268 | * `--unit`: setada para _millimeter_ 269 | 270 | 271 | ___ 272 | 273 | ### [pre-content/index.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/index.js) 274 | 275 | Insere conteúdo no início do livro. 276 | 277 | Dependências externas: 278 | * [fs-extra](https://github.com/jprichardson/node-fs-extra), métodos extras para o módulo de sistemas de arquivo do Node.js. 279 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 280 | 281 | #### finish 282 | 283 | Função associada ao hook de `finish` do gitbook, que é chamada no fim da geração de um site ou ebook pelo gitbook. 284 | 285 | Se a extensão do livro a ser gerado não for pdf, não faz nada. 286 | 287 | Agora, se for pdf, faz os seguintes passos: 288 | 289 | 1. **cria sumário** utilizando a função `renderTocPDF` 290 | 1. **adiciona conteúdo antes do sumário** através da função `handlePreContent` 291 | 1. **junta conteúdos em um pdf só** utilizando a função `join` do módulo `pdftk`, gerando o arquivo `index-with-pre-content.pdf` 292 | 1. **atualiza índice** do pdf usando a função `updateBookmarkInfo` do módulo `pdftk`, gerando o arquivo `index-with-pre-content-and-bookmarks.pdf` 293 | 1. **atualiza informações de número das páginas** do pdf com a função `updatePageNumberInfo` do módulo `gs`, gerando o arquivo `index-with-pre-content-bookmarks-and-page-numbers.pdf` 294 | 1. há um passo final, se o comando executado for `gitbook pdf`. Os arquivos intermediários são gerados no diretório `/tmp` e logo em seguida apagados. Por isso, copiamos o `/tmp/index-with-pre-content-bookmarks-and-page-numbers.pdf` para `/tmp/index.pdf`. O gitbook se encarrega de transformá-lo no arquivo, chamado `book.pdf`. 295 | 296 | #### renderTocPDF 297 | Função privada de `pre-content/index.js` que é responsável por gerar um pdf com o sumário, a partir do pdf original do gitbook/calibre. 298 | 299 | Parâmetros: 300 | * outputDir - `String` com o caminho do diretório de saída 301 | * originalPDF - `String` com o caminho do pdf gerado pelo gitbook/calibre 302 | * pdfInfo - `Object` com informações do livro como autor, editora e título, além das opções do gitbook. 303 | 304 | Retorno: 305 | * `String` com o caminho do pdf gerado que contém o sumário 306 | 307 | Os passos para gerar o pdf com o sumário são os seguintes: 308 | 309 | 1. extrair do índice do pdf original, através do função `extractTOC` do módulo `pdftk.js`, um `Object` com capítulos e seções com suas respectivas páginas. 310 | 2. atualizar as páginas do `Object` obtida no passo anterior, utilizando a função `update` do módulo `toc.js`, para que o primeiro capítulo comece na página 1. Nas informações extraídas pelo _pdftk_, o primeiro capítulo começa na página 3, porque é considerada a capa e uma página com o sumário original (e incompleto) gerado pelo gitbook. 311 | 3. com o `Object` com as páginas atualizadas, é renderizado um html através da função `render` do módulo `htmlRenderer.js`. Para isso, é passado o template `book/templates/toc.tpl.html`. 312 | 4. a `String` com o html renderizado no passo anterior é salva em um arquivo 313 | 5. para gerar um pdf com o sumário é chamada a função `generate` do módulo `calibre` passando o caminho do arquivo html, o caminho onde o arquivo pdf deve ser gerado e opções para geração do pdf. As opções do calibre vem do objeto `pdfInfo`, que são obtidas do `book.json`. São definidas as seguintes opções: 314 | * `--pdf-page-numbers` fica como `null` porque será usado o `--pdf-footer-template` 315 | * `--disable-font-rescaling` fica como `true` para desabilitar mudança nas fontes 316 | * `--paper-size` fica com `null` porque será usado o `--custom-size` 317 | * `--unit` fica com `millimeter` 318 | * `--chapter` fica com `/` para desligar a detecção de capítulos 319 | * `--page-breaks-before` fica com `/` para desabilitar quebras de página. 320 | * `--custom-size` é obtido de `pdf.customSize` do `book.json` 321 | * `--margin-left` é obtido de `pdf.margin.left` do `book.json` 322 | * `--margin-right` é obtido de `pdf.margin.right` do `book.json` 323 | * `--margin-top` é obtido de `pdf.margin.top` do `book.json` 324 | * `--margin-bottom` é obtido de `pdf.margin.bottom` do `book.json` 325 | * `--pdf-default-font-size` é obtido de `pdf.fontSize` do `book.json` 326 | * `--pdf-mono-font-size` é obtido de `pdf.fontSize` do `book.json` 327 | * `--pdf-header-template` é obtido de `pdf.summary.headerTemplate` do `book.json` 328 | * `--pdf-footer-template` é obtido de `pdf.summary.footerTemplate` do `book.json` 329 | 330 | No fim desses passos, temos um pdf com o sumário do livro com capítulos e seções e as respectivas páginas. 331 | 332 | #### handlePreContent 333 | Função privada de `pre-content/index.js` responsável por gerar pdfs com todo o conteúdo que precede o primeiro capítulo: conteúdo fixo como pdfs com copyright e propagandas, conteúdo .md que precisa ser renderizado e o pdf do sumário. 334 | 335 | Parâmetros: 336 | * inputDir - `String` com o caminho do diretório de entrada 337 | * outputDir - `String` com o caminho do diretório de saída 338 | * tocPDF - `String` com o caminho do pdf do sumário 339 | * pdfInfo - `Object` com informações do livro como autor, editora e título, além das opções do gitbook. 340 | 341 | Retorno: 342 | * `Array` com todos os caminhos de pdfs que devem ser inseridos antes do primeiro capítulo 343 | 344 | Os passos para gerar o pdfs são os seguintes: 345 | 346 | 1. obter todos os arquivos `.pdf` ordenados por nome do diretório apontado pela variável de ambiente `EXTRAS_DIR`, se presente 347 | 2. verificar se existe um diretório `extras` no livro e obter todos os arquivos `.pdf` desse diretório ordenados por nome 348 | 3. obter todos os arquivos `.md`, ordenados por nome, do diretório `intro` 349 | 4. renderizar os `.md`, transformado-os em `.pdf` 350 | 5. extrair a soma dos número de páginas de todos os `.pdf` encontrados no `EXTRAS_DIR`, no `extras` ou gerados a partir do `.md` de `intro` 351 | 6. atualizar o objeto `pdfInfo` com um objeto `preContent` que tem a propriedade `numberOfPages` com a soma do número de páginas obtida anteriormente 352 | 353 | Após a execução da função, é retornado um array com os caminhos dos `.pdf` de `EXTRAS_DIR`, `extras` e `intro` (renderizados). Além disso, o objeto `pdfInfo` terá o número de páginas desses `.pdf`. 354 | 355 | ### [pre-content/pdftk.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/pdftk.js) 356 | 357 | Contém código de invocação da ferramenta [pdftk](https://www.pdflabs.com/docs/pdftk-man-page/). 358 | 359 | Dependências externas: 360 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 361 | 362 | #### extractTOC 363 | Função que usa o pdftk para extrair o índice de um pdf. 364 | 365 | Parâmetros: 366 | * pdfFile - `String` com o caminho de um pdf 367 | 368 | Retorno: 369 | * `Array` de `Object`s com informações dos capítulos (`title`, `pageNumber` e `sections`). 370 | 371 | É executado o comando `pdftk arquivo.pdf dump_data`. 372 | 373 | Cada linha da resposta do comando anterior é lida, buscando a página através do `BookmarkPageNumber`, se é seção ou capítulo através do `BookmarkLevel` e o título através do `BookmarkTitle` . 374 | 375 | Então, é retornado um `Array` que contém objetos com as informações de cada capítulo. O retorno será algo como: 376 | ``` js 377 | [ 378 | { title: "Capítulo 1", 379 | pageNumber: 1, 380 | sections: [ 381 | { title: "Seção 1.1", pageNumber: 1}, 382 | { title: "Seção 1.2", pageNumber: 3} 383 | ] 384 | }, 385 | { title: "Capítulo 2", 386 | pageNumber: 5, 387 | sections: [ 388 | { title: "Seção 2.1", pageNumber: 6} 389 | ] 390 | } 391 | ] 392 | ``` 393 | 394 | Em [`pre-content/test/integration/pdftk-test.js`](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/test/integration/pdftk-test.js), há um teste de que executa a extração do índice em um pdf de exemplo. 395 | 396 | #### extractNumberOfPagesFromFiles 397 | Função que retorna a soma do número de páginas dos arquivos pdf passados como parâmetro, usando o pdftk. 398 | 399 | Parâmetros: 400 | * files - `Array` com o caminho de arquivos pdf 401 | 402 | Retorno: 403 | * `Number` com a soma do número de páginas dos pdfs. 404 | 405 | Para extrair o número de páginas, é executado o comando `pdftk arquivo.pdf dump_data` e lida a informação `NumberOfPages`. 406 | 407 | Como o comando `pdftk` é executado várias vezes, é criado um array de _promises_ e são utilizadas as funções [`all`](https://github.com/kriskowal/q/wiki/API-Reference#promiseall) e [`spread`](https://github.com/kriskowal/q/wiki/API-Reference#promisespreadonfulfilled-onrejected) da biblioteca `Q`, para gerenciar a execução das promises. 408 | 409 | Em [`pre-content/test/integration/pdftk-test.js`](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/test/integration/pdftk-test.js), há um teste de que executa a extração do número de páginas de pdfs de exemplo. 410 | 411 | #### join 412 | 413 | Função que mescla vários arquivos pdf usando o pdftk. 414 | 415 | Parâmetros: 416 | * pdfFile - `String` com o caminho de um pdf principal 417 | * files - `Array` com o caminho de arquivos pdf a serem inseridos no começo do pdf principal 418 | * outputFile - `String` com caminho do pdf de saída 419 | 420 | Os arquivos pdf no array `files` são colocados logo depois da página 1 do arquivo principal (que contém a capa do livro). Depois de todos os arquivos pdf extras, é inserido o conteúdo do pdf principal. 421 | 422 | _Obs.: Na verdade, é retirada a página 2 do pdf principal, porque essa página contém um sumário incompleto que é gerado pelo gitbook._ 423 | 424 | É utilizada o comando `cat` do pdftk. Um exemplo de chamada é o seguinte: 425 | ``` 426 | pdftk A="input.pdf" B="copyright.pdf" C="ads.pdf" D="toc.pdf" cat A1 B C D A3-end output out.pdf 427 | ``` 428 | 429 | Em [`pre-content/test/unit/pdftk-join-test.js`](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/test/unit/pdftk-join-test.js), há um teste de unidade com exemplos. 430 | 431 | 432 | #### updateBookmarkInfo 433 | 434 | Função que, dado um pdf de entrada e um objeto com as páginas iniciais e títulos dos capítulos e seções, gera um pdf de saída com as informações atualizadas. 435 | 436 | Parâmetros: 437 | * inputFile - `String` com o caminho do pdf de entrada 438 | * info - `Object` com informações atualizadas das páginas e títulos 439 | * infoFile - `String` com o caminho onde deve ser gravado o arquivo com informações do novo índice no formato do pdftk 440 | * outputFile - `String` com caminho do pdf de saída 441 | 442 | Vamos supor que invocamos essa função com o objeto `info`, conforme a seguir: 443 | ``` js 444 | { 445 | "toc":[ 446 | { 447 | "title":"1 Introdução", 448 | "pageNumber":3, 449 | "sections":[ 450 | { 451 | "title":"1.1 Mantendo o histórico do código", 452 | "pageNumber":3 453 | } 454 | ] 455 | } 456 | ], 457 | "preContent": { "numberOfPages": 9 } 458 | }; 459 | ``` 460 | 461 | Se o parâmetro `infoFile` for `info.txt`, esse arquivo será gerado com o seguinte conteúdo: 462 | ``` 463 | BookmarkBegin 464 | BookmarkTitle: 1 Introdução 465 | BookmarkLevel: 1 466 | BookmarkPageNumber: 13 467 | BookmarkBegin 468 | BookmarkTitle: 1.1 Mantendo o histórico do código 469 | BookmarkLevel: 2 470 | BookmarkPageNumber: 13 471 | ``` 472 | 473 | Então, é invocada a opção `update_info` do comando pdftk da seguinte maneira: 474 | ``` 475 | pdftk input.pdf update_info info.txt output output.pdf 476 | ``` 477 | 478 | Depois dessa invocação, o índice do `output.pdf` estará com as páginas atualizadas de acordo com as informações de `info`. 479 | 480 | Em [`pre-content/test/unit/pdftk-bookmarkInfo-test.js`](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/test/unit/pdftk-bookmarkInfo-test.js), há um teste de unidade com exemplos dos parâmetros e do tipo de arquivo que é gerado. 481 | 482 | ### [pre-content/toc.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/toc.js) 483 | 484 | Atualiza páginas do sumário. 485 | 486 | #### update 487 | Função que atualiza a informação do número da página dos capítulo e seções, de maneira que o primeiro capítulo comece na página 1. Para isso, são descontadas 2 páginas: 1 para a capa e outra para o sumário original do gitbook. 488 | 489 | Parâmetros: 490 | * toc - `Object` com informações dos capítulos (`title`, `pageNumber` e `sections`). 491 | 492 | Retorno: 493 | * `Object` com número das páginas dos capítulos e seções atualizados 494 | 495 | ### [pre-content/htmlRenderer.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/htmlRenderer.js) 496 | 497 | Renderiza um html. 498 | 499 | Dependências externas: 500 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 501 | * [swig](https://github.com/paularmstrong/swig/), uma engine de templates para Node.js. 502 | 503 | 504 | #### render 505 | Função que renderiza um html utilizando a _template engine_ `swig`, dados um conteúdo e um template. 506 | 507 | Parâmetros: 508 | * content - `Object` com o conteúdo a ser renderizado. 509 | * templateLocation - `String` com o caminho de um template compatível com o `swig` 510 | 511 | Retorno: 512 | * uma `String` que contém o html renderizado 513 | 514 | ### [pre-content/calibre.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/calibre.js) 515 | 516 | Renderiza um pdf utilizando o comando [`ebook-convert`](http://manual.calibre-ebook.com/cli/ebook-convert.html) do calibre. 517 | 518 | Dependências externas: 519 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 520 | 521 | #### generate 522 | Função que gera um pdf a partir de um html, utilizando o `ebook-convert` do calibre. 523 | 524 | Parâmetros: 525 | * inputFilename - `String` com o caminho do html de entrada 526 | * outputFilename - `String` com o caminho do pdf de saída 527 | * options - `Object` com opções do `ebook-convert` 528 | 529 | É gerada um chamada do tipo: 530 | ``` 531 | ebook-convert input.html output.pdf --disable-font-rescaling --chapter="/" --page-breaks-before="/" 532 | ``` 533 | 534 | O comando é executado através da função `exec` do módulo `child_process` do Node.js. 535 | 536 | ### [pre-content/dir.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/dir.js) 537 | 538 | Lista os arquivos de um diretório. 539 | 540 | Dependências externas: 541 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 542 | 543 | #### listFilesByName 544 | Função que retorna os caminhos de todos os arquivos de uma determinada extensão de um diretório, ordenados pelo nome do arquivo. 545 | 546 | Parâmetros: 547 | * dir - `String` com o caminho de um diretório. 548 | * extension - `String` com uma extensão de arquivos 549 | 550 | Retorno: 551 | * `Array` com os caminhos dos arquivos da extensão, ordenados por nome. 552 | 553 | ### [pre-content/mdRenderer.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/mdRenderer.js) 554 | 555 | Renderiza arquivos md em pdf. 556 | 557 | Dependências externas: 558 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 559 | * [kramed](https://github.com/GitbookIO/kramed), parser de Markdown para Node.js. 560 | 561 | #### renderPdfs 562 | Função que renderiza uma lista de arquivos md em pdf. 563 | 564 | Parâmetros: 565 | * files - `Array` com caminhos de arquivos .md 566 | * template - `String` com o caminho de um template compatível com `swig` 567 | * pdfOptions - `Object` com opções do `ebook-convert` 568 | 569 | Para cada arquivo .md do parâmetro `files`, são feitos os seguintes passos: 570 | 571 | 1. renderizar um html a partir do md utilizando a biblioteca `kramed` 572 | 2. é utilizada a função [`render`](#render) do módulo `htmlRenderer.js` passando o parâmetro `template`, para melhorar o html gerado no passo anterior. 573 | 3. é criado um arquivo com o conteúdo html com o mesmo nome do md, só que com extensão `.html` 574 | 4. é utilizada a função [`generate`](#generate) do módulo `calibre.js` para gerar um pdf a partir do arquivo html. São utilizadas as seguintes opções: 575 | * `--pdf-page-numbers` fica como `null` porque será usado o `--pdf-footer-template` 576 | * `--disable-font-rescaling` fica como `true` para desabilitar mudança nas fontes 577 | * `--paper-size` fica com `null` porque será usado o `--custom-size` 578 | * `--unit` fica com `millimeter` 579 | * `--chapter` fica com `/` para desligar a detecção de capítulos 580 | * `--page-breaks-before` fica com `/` para desabilitar quebras de página. 581 | * `--custom-size` é obtido de `pdf.customSize` do `book.json` 582 | * `--margin-left` é obtido de `pdf.margin.left` do `book.json` 583 | * `--margin-right` é obtido de `pdf.margin.right` do `book.json` 584 | * `--margin-top` é obtido de `pdf.margin.top` do `book.json` 585 | * `--margin-bottom` é obtido de `pdf.margin.bottom` do `book.json` 586 | * `--pdf-default-font-size` é obtido de `pdf.fontSize` do `book.json` 587 | * `--pdf-mono-font-size` é obtido de `pdf.fontSize` do `book.json` 588 | * `--pdf-header-template` fica como _null_, para não ter cabeçalho 589 | * `--pdf-footer-template` fica como _null_, para não ter rodapé 590 | 591 | 592 | ### [pre-content/gs.js](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/gs.js) 593 | 594 | Código de chamada do [ghostscript](http://www.ghostscript.com/doc/current/Use.htm). 595 | 596 | Dependências externas: 597 | * [Q](https://github.com/kriskowal/q), biblioteca de promises para Node.js. 598 | 599 | #### updatePageNumberInfo 600 | Função que configura o pdf para usar números romanos (i, ii, iii, iv, ...) até antes do capítulo 1 e algarismos arábicos (1, 2, 3, 4, ...) do capítulo 1 em diante. 601 | 602 | Parâmetros: 603 | * inputFile - `String` com o caminho do pdf de entrada 604 | * pageInfo - `Object` com informações do livro como título, autores, editora e número de páginas de pré-conteúdo 605 | * pageInfoFile - `String` com o caminho onde gravar o arquivo que será usado o `gs` 606 | * outputFile - `String` com o caminho do pdf de saída 607 | 608 | 609 | Suponha que passamos o objeto `pageInfo`, conforme abaixo: 610 | ``` js 611 | { 612 | "book": { 613 | "title": "Git e GitHub", 614 | "author": "Alexandre", 615 | "publisher": "Casa do Código" 616 | }, 617 | "preContent": { "numberOfPages": 9 } 618 | }; 619 | ``` 620 | 621 | O arquivo gerado teria o seguinte conteúdo: 622 | ``` 623 | [ /Title (Git e GitHub) 624 | /Author (Alexandre) 625 | /Creator (Casa do Código) 626 | /Producer (Casa do Código) 627 | /DOCINFO pdfmark 628 | [/_objdef {pl} /type /dict /OBJ pdfmark 629 | [{pl} <> 631 | 10 << /S /D /St 1 >> 632 | ]>> /PUT pdfmark 633 | [{Catalog} <> /PUT pdfmark 634 | ``` 635 | 636 | O conteúdo do arquivo anterior informa para o Ghostscript que da página 0 a 9 devem ser utilizados números romanos e da página 10 em diante devem ser usados algarismos arábicos. 637 | 638 | Então, será chamado o Ghostscript de maneira parecida com: 639 | ``` 640 | gs -q -o output.pdf -sDEVICE=pdfwrite input.pdf info.txt 641 | ``` 642 | 643 | Em [`pre-content/test/unit/gs-pageInfo-test.js`](https://github.com/casadocodigo/gitbook-plugin-cdc/blob/master/pre-content/test/unit/gs-pageInfo-test.js), há um teste de unidade. 644 | --------------------------------------------------------------------------------