docsify(五):为PDF文档注入css脚本

318 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

相关工程部署可参考

docsify(一)

docsify(二)

docsify(三)

docsify(四)

先了解下docsify生成pdf的原理,前面文章也有提到过:

docsify现将md碎片进行合并生成一个完整的md文档,再将完整的md交给docsify-server,生成在线的html网页,再由puppeteer启动一个虚拟的chrome浏览器,根据我们给pdf设置的尺寸进行一张一张截取,从而生成pdf。

要想为pdf注入css脚本,其实就是在这个虚拟浏览器对网页进行截取前,为网页注入css脚本,这就必然要使用到浏览器爬虫puppeteer的api。

原理和思路都很简单,直接上代码了:

  1. style.css    新增文件  被注入的css脚本;

  2. render.js   源文件  做了部分调整,已添加中文注释;

style.css

html {
  counter-reset: chapter;
}

ul {
  margin: 0 0 !important;
  margin-top: -10px !important;
}

li>ul {
  margin-top: 0px !important;
}

blockquote {
  margin: 0 0 !important;
  margin-top: -5px !important;
  padding: 5px !important;
}

li blockquote {
  margin-top: 10px !important;
  padding-top: 10px !important;
}

a {
  color: #20435C !important;
}

p,
li {
  line-height: 20px !important;
  margin-top: 5px !important;
}

h1 {
  page-break-before: always;
  margin-bottom: 50px !important;
}

h1:before {
  counter-increment: chapter;
  content: "Chapter "counter(chapter)"\A\A";
  white-space: pre;
  color: #20435C;
}

h2,
h3,
h4,
h5,
h6 {
  margin: 10px 0 !important;
  border-bottom: 0 !important;
  padding: 0px !important;
}

#mulu {
  font-size: 40px !important;
  margin-bottom: 30px !important;
  display: inline-block !important;
}

render.js

const path = require("path");
const fs = require("fs");
const puppeteer = require("puppeteer");
const logger = require("./logger.js");
const runSandboxScript = require("./run-sandbox-script.js");
const merge = require("easy-pdf-merge");

const renderPdf = async ({
  mainMdFilename,
  pathToStatic,
  pathToPublic,
  pdfOptions,
  docsifyRendererPort,
  emulateMedia,
  facebookName,
  facebook,
}) => {
  const browser = await puppeteer.launch({
    defaultViewport: {
      width: 1200,
      height: 1000,
    },
  });
  try {
    const mainMdFilenameWithoutExt = path.parse(mainMdFilename).name;
    const docsifyUrl = `http://localhost:${docsifyRendererPort}/#/${pathToStatic}/${mainMdFilenameWithoutExt}`;
    let pdfUrls = []
    const page = await browser.newPage();
    // 生成封面pdf
    if (facebook) {
      await page.goto(`http://localhost:${docsifyRendererPort}/${facebookName}`, { waitUntil: "networkidle0" });
      await page.emulateMedia(emulateMedia);
      await page.pdf({
        ...{
          ...pdfOptions,
          margin: {
            top: '0px',
            right: '0px',
            bottom: '0px',
            left: '0px'
          },
        },
        path: path.resolve(pathToStatic + "/facebook.pdf"),
      });
      pdfUrls.push(pathToStatic + '/facebook.pdf')
    }

    // 生成内容pdf
    await page.goto(docsifyUrl, { waitUntil: "networkidle0" });
    await page.addStyleTag({ path: 'style.css' })
    const renderProcessingErrors = await runSandboxScript(page, {
      mainMdFilenameWithoutExt,
      pathToStatic,
    });
    if (renderProcessingErrors.length)
      logger.warn("anchors processing errors", renderProcessingErrors);
    await page.emulateMedia(emulateMedia);

    //隐藏下载btn
    await page.$eval('.download', function (el, value) {
      return el.setAttribute('style', value)
    }, 'display: none')


    await page.pdf({
      ...pdfOptions,
      path: path.resolve(pathToStatic + '/' + (facebook ? 'content.pdf' : pathToPublic)),
    });
    pdfUrls.push(pathToStatic + '/' + (facebook ? 'content.pdf' : pathToPublic))


    await browser.close();
    if (facebook) {
      await mergeMultiplePDF(pdfUrls, pathToStatic + '/' + pathToPublic);
      pdfUrls.map(url => fs.unlinkSync(url))
    }
  } catch (e) {
    await browser.close();
    throw e;
  }
};

const mergeMultiplePDF = (pdfFiles, pdfPath) => {
  return new Promise((resolve, reject) => {
    merge(pdfFiles, pdfPath, async (err) => {
      if (err) {
        console.log(err);
        reject(err)
      }
      console.log('PDF Merge Success!');
      resolve()
    });
  });
};

const htmlToPdf = ({
  mainMdFilename,
  pathToStatic,
  pathToPublic,
  pdfOptions,
  removeTemp,
  docsifyRendererPort,
  emulateMedia,
  facebookName,
  facebook,
}) => async () => {
  const { closeProcess } = require("./utils.js")({ pathToStatic, removeTemp });
  try {
    return await renderPdf({
      mainMdFilename,
      pathToStatic,
      pathToPublic,
      pdfOptions,
      docsifyRendererPort,
      emulateMedia,
      facebookName,
      facebook,
    });
  } catch (err) {
    logger.err("puppeteer renderer error:", err);
    await closeProcess(1);
  }
};

module.exports = config => ({
  htmlToPdf: htmlToPdf(config),
});

不到之处,烦请指正~