在Docker中构建GitBook并整合GitLab Runner的使用经验分享一文中,自己在Docker环境下基于GitLab Runner实现了GitBook的自动构建,同时基于实际使用需求添加了很多GitBook插件,在此过程中积累了一些GitBook插件的开发与使用经验。

本文简要介绍自己在GitBook插件定制化修改过程中如何实现从多个不同的文件夹下加载jscss文件的实现过程。

问题背景

由于GitBook官方默认的代码高亮插件highlight使用的是highlightjs,其功能没有prismjs完善,故最初将GitBook的代码高亮采用prismjs实现,对应的插件为gitbook-plugin-prism,其使用方式也很简单,只需要在book.json中添加如下配置:

1
2
3
{
  "plugins": ["prism", "-highlight"]
}

该插件使用起来一切正常,由于部门GitBook的一大用途是记录设计方案与实现的代码,经常会有多个功能相似的对比代码进行展示,占用了大量的空间篇幅且阅读也不是很方便,需要添加一个tab效果对其进行分组归类。

在网上搜索相关的GitBook插件后,主要有gitbook-plugin-codegroup以及gitbook-plugin-codetabs,它们的对比如下:

gitbook-plugin-codegroup gitbook-plugin-codetabs
代码高亮实现 highlight prism
代码写入方式 进行markdown中代码块原生语法,typora展示更方便 GitBook代码块,typora展示不友好
插件改造难度

综合对比后,决定采用gitbook-plugin-codetabs结合gitbook-plugin-prism两者结合,同时代码块的输入还是采用类似 ``` 包裹的方式以便与markdowntypora的使用方式保持一致。

确定好方案后,接下来就需要将这两个插件组合到一起,查看它们的源码,发现都各自引入了静态文件

gitbook插件中都分别引入了静态文件

自己首先想到的是添加多个assets然后在jscss使用完整路径,类似如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
module.exports = {
    book: {
        assets: ['./assets1','./assets2'],
        css: [
            'codetabs1.css','codetabs2.css'
        ],
        js: [
            'codetabs1.js','codetabs2.js'
        ]
    },

但此种方式明显不可行,通过gitbook serve启动之后,终端控制台提示类似如下错误

1
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an instance of List

很明显assets的值只能为单个字符串,不能为数组。

接下来尝试将assets 配置去掉,改为类似如下代码

1
2
3
4
5
6
7
8
book: {
   css: [
       './assets1/codetabs1.css','./assets2/codetabs2.css'
   ],
   js: [
       './assets1/codetabs1.js','./assets2/codetabs2.js'
   ]
},

此时通过gitbook serve启动时,终端控制台提示一切正常,但访问对应的页面时,显示效果不正常,通过Chrome调试工具可发现上述的jscss都显示为404,都未能正常加载,导致页面显示不正常。

看来assets属性不能省略,且不能以数组方式配置,此时很容易想到的是将这些静态资源文件都放置到一个文件夹下,不就解决了此问题?

理论上确实可行,但是理想很丰满,现实很骨感,此种方式并不能满足自己的实际需求!

原因为gitbook-plugin-prism插件的index.js中有如下代码,通过此代码可知gitbook-plugin-prism中是通过去npm仓库中获取prismjs的源文件,而不是直接将primsjs下载到该插件的资源文件目录下,通过这种方式可实现gitbook-plugin-prismprismjs的解耦,且能快速的升级prismjs,是一个合理且优秀的设计!

作为一个对代码编写要求很高的人,当前要将别人优秀的习惯保持下来,这样的话就不能将代码分组的静态文件与prismjs代码高亮相关的文件混合在一起,问题产生!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getAssets() {

  var cssFiles = getConfig(this, 'pluginsConfig.prism.css', []);
  var cssFolder = null;
  var cssNames = [];
  var cssName = null;

  if (cssFiles.length === 0) {
    cssFiles.push('prismjs/themes/prism.css');
  }

  cssFiles.forEach(function(cssFile) {
    var cssPath = require.resolve(cssFile);
    cssFolder = path.dirname(cssPath);
    cssName = path.basename(cssPath);
    cssNames.push(cssName);
  });

  return {
    assets: cssFolder,
    css: cssNames
  };
}

解决方案

既不能同时支持多个assets属性,自己又不想破坏prismjs本身的结构,怎么办?

一开始自己通过GoogleGitHub去查找相关的资料,没有找到自己想要的东西,后来用ChatGPT连续切换了好几次提示词,结果答案都是错的.

PS:个人鄙视在编程中遇到问题无脑使用ChatGPT,个人观点是要明白其底层原理,找到合适的使用方法。ChatGPT是基于训练的,网络上相关资料越丰富,ChatGPT训练的材料也就越多,其给出的答案也就越准确,反之,对于新出现的技术,网上资料很少或者ChatGPT来不及训练,此时就不能给出准确的答案了。

通过前述方式没有找到自己想要的答案,没办法只能在Stackoverflow上用英文提问,希望能有人给回复,遗憾的提出问题之后一周也没人回复,只能另想它法了。

分析GitBook中插件的生成结果,发现要想某个静态资源文件能够被访问到,其必须在_book目录下对应的插件中存在,之前的去掉assetsgitbook serve不报错,但是jscss 加载出现404即是这个原因。

gitbook中可访问文件路径

找到问题原因后,接下来自己尝试手工把相关的jscss文件加到_book下对应的插件目录中,此时可通过浏览器地址直接访问,不会出现404问题,但是相关的静态资源文件仍然没有加载,在生成的html源码中也没有看见。

gitbook资源文件没有加载

问题原因为我们没有在下述代码中引入对应的jscss文件。

1
2
3
4
5
6
7
8
9
book: {
    assets: './assets',
        css: [
            'codetabs.css'
        ],
        js: [
            'codetabs.js'
        ]
},

要想文件被加载,则必须要引入相关文件,要想文件能被正常访问,在_book下对应的插件目录中必须要有对应的文件。

至此,问题的解决方案找到!

引入文件很容易实现,直接在数组中添加对应的文件即可,但如何引入相关文件呢?经过多处查看代码后,自己在index.js中找到了答案

1
2
3
4
5
6
try {
    fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
} catch (e) {
    console.warn('Failed to write prism-ebook.css. See https://git.io/v1LHY for side effects.');
    console.warn(e);
}

利用相关的JavaScript API手工将文件写入_book下对应插件的目录中!之后将assets的值设置为对应插件的名称,既能同时访问多个文件!

经过多次测试,最终捣鼓出可用的代码,在不破坏prismjs完整性的同时还能加载插件自身的jscss文件,完整代码参见gitbook-plugin-prism-codetab-fox

核心的index.js代码如下,主要是通过syncFile函数进行文件写入:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
var Prism = require('prismjs');
var languages = require('prismjs').languages;
var path = require('path');
var fs = require('fs');
var cheerio = require('cheerio');
var mkdirp = require('mkdirp');
var codeBlocks = require('gfm-code-blocks');
var trim = require('lodash/trim');
const includes = require('lodash/includes');
const get = require('lodash/get');

var DEFAULT_LANGUAGE = 'markup';
var DEFAULT_CODE_TAB_SEPERATOR = '::';
var MAP_LANGUAGES = {
    'py': 'python',
    'js': 'javascript',
    'rb': 'ruby',
    'cs': 'csharp',
    'sh': 'bash',
    'html': 'markup'
};

// Base languages syntaxes (as of prism@1.6.0), extended by other syntaxes.
// They need to be required before the others.
var PRELUDE = [
    'markup-templating', 'clike', 'javascript', 'markup', 'c', 'ruby', 'css',
    // The following depends on previous ones
    'java', 'php'
];
PRELUDE.map(requireSyntax);

/**
 * Load the syntax definition for a language id
 */
function requireSyntax(lang) {
    require('prismjs/components/prism-' + lang + '.js');
}

function getConfig(context, property, defaultValue) {
    var config = context.config ? /* 3.x */ context.config : /* 2.x */ context.book.config;
    return config.get(property, defaultValue);
}

function isEbook(book) {
    // 2.x
    if (book.options && book.options.generator) {
        return book.options.generator === 'ebook';
    }

    // 3.x
    return book.output.name === 'ebook';
}

function getAssets() {

    var cssFiles = getConfig(this, 'pluginsConfig.prism.css', []);
    var cssFolder = null;
    var cssNames = [];
    var cssName = null;

    if (cssFiles.length === 0) {
        cssFiles.push('prismjs/themes/prism.css');
    }

    cssFiles.forEach(function(cssFile) {
        var cssPath = require.resolve(cssFile);
        cssFolder = path.dirname(cssPath);
        cssName = path.basename(cssPath);
        cssNames.push(cssName);
    });

    cssNames.push('codetab/codetab.css');
    return {
        assets: cssFolder,
        css: cssNames,
        js: ['codetab/codetab.js']
    };
}

function syncFile(book, outputDirectory, outputFile, inputFile) {
    outputDirectory = path.join(book.output.root(), '/gitbook/gitbook-plugin-prism-codetab-fox/' + outputDirectory);
    outputFile = path.resolve(outputDirectory, outputFile);
    inputFile = path.resolve(__dirname, inputFile);
    mkdirp.sync(outputDirectory);

    try {
        fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
    } catch (e) {
        console.warn('Failed to write ' + inputFile);
        console.warn(e);
    }

}

module.exports = {
    book: getAssets,
    ebook: function() {

        // Adding prism-ebook.css to the CSS collection forces Gitbook
        // reference to it in the html markup that is converted into a PDF.
        var assets = getAssets.call(this);
        assets.css.push('prism-ebook.css');
        return assets;
    },
    blocks: {
        code: // xxxx
        codetab: // xxxx
    },
    hooks: {

        // Manually copy prism-ebook.css into the temporary directory that Gitbook uses for inlining
        // styles from this plugin. The getAssets() (above) function can't be leveraged because
        // ebook-prism.css lives outside the folder referenced by this plugin's config.
        //
        // @Inspiration https://github.com/GitbookIO/plugin-styles-less/blob/master/index.js#L8
        init: function() {

            var book = this;

            syncFile(book, 'codetab', 'codetab.js', './codetab/codetab.js');
            syncFile(book, 'codetab', 'codetab.css', './codetab/codetab.css');

            // If failed to write prism-ebook.css. See https://git.io/v1LHY for side effects.
            if (isEbook(book)) {
                syncFile(book, '', 'prism-ebook.css', './prism-ebook.css');
            }

        },
        page: // xxx
    }
};

自己改造后的插件名为gitbook-plugin-prism-codetab-fox,将该插件在book.json中启用并运行gitbook install && gitbook serve命令后,可以看见在对应的GitBook工程下分别将prismjscodetab相关的插件都安装到node modules模块下。

gitbook node modules列表

分别检查这两个js模块,可发现其内容都正常

gitbook node modules文件列表

检查_book目录,发现生成的插件同时包含prismjs以及 codetab相关的文件,此时该插件已经将相关的内容合并了。

gitbook生成的插件

查看GitBook中生成的HTML页面源码,可发现相关的静态文件均正常加载,至此问题解决!

gitbook加载多个文件