summaryrefslogtreecommitdiffstats
path: root/lib/plugins/plugin.js
blob: 9f9cc35deabe6c76a4521b1d91759e4d5dae99ae (plain)
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
132
133
134
135
136
137
var _ = require('lodash');
var path = require('path');
var resolve = require('resolve');

var Promise = require('../utils/promise');
var error = require('../utils/error');
var gitbook = require('../gitbook');
var registry = require('./registry');

var HOOKS = [
    'init', 'finish', 'finish:before', 'config', 'page', 'page:before'
];

// Return true if an error is a "module not found"
// Wait on https://github.com/substack/node-resolve/pull/81 to be merged
function isModuleNotFound(err) {
    return err.message.indexOf('Cannot find module') >= 0;
}

function BookPlugin(book, pluginId) {
    this.book = book;
    this.log = this.book.log;

    this.id = pluginId;
    this.npmId = registry.npmId(pluginId);
    this.root;

    this.packageInfos = undefined;
    this.content = undefined;

    _.bindAll(this);
}

// Return true if plugin has been loaded correctly
BookPlugin.prototype.isLoaded = function() {
    return Boolean(this.packageInfos && this.content);
};

// Load this plugin
BookPlugin.prototype.load = function() {
    var that = this;

    if (this.isLoaded()) {
        return Promise.reject(new Error('Plugin "' + this.id + '" is already loaded'));
    }

    // Try loading plugins from different location
    var p = Promise.some([
        this.book.resolve('node_modules'),
        __dirname
    ], function(baseDir) {
        // Locate plugin and load pacjage.json
        try {
            var res = resolve.sync(that.npmId + '/package.json', { basedir: baseDir });

            that.root = path.dirname(res);
            that.packageInfos = require(res);
        } catch (err) {
            if (!isModuleNotFound(err)) throw err;

            that.packageInfos = undefined;
            that.content = undefined;

            return false;
        }

        // Load plugin JS content
        try {
            that.content = require(resolve.sync(that.npmId, { basedir: baseDir }));
        } catch(err) {
            // It's no big deal if the plugin doesn't have an "index.js"
            // (For example: themes)
            if (isModuleNotFound(err)) {
                that.content = {};
            } else {
                throw new error.PluginError(err, {
                    plugin: that.id
                });
            }
        }

        return true;
    })

    .then(that.validate);

    this.log.info('Loading plugin "' + this.id + '" ...');
    return this.log.info.promise(p);
};


// Verify the definition of a plugin
// Also verify that the plugin accepts the current gitbook version
// This method throws erros if plugin is invalid
BookPlugin.prototype.validate = function() {
    var isValid = (
        this.packageInfos &&
        this.packageInfos.name &&
        this.packageInfos.engines &&
        this.packageInfos.engines.gitbook
    );

    if (!this.isLoaded()) {
        throw new Error('Couldn\'t locate plugin "' + this.id + '", Run \'gitbook install\' to install plugins from registry.');
    }

    if (!isValid) {
        throw new Error('Invalid plugin "' + this.id + '"');
    }

    if (!gitbook.satisfies(this.packageInfos.engines.gitbook)) {
        throw new Error('GitBook doesn\'t satisfy the requirements of this plugin: '+this.packageInfos.engines.gitbook);
    }
};

// Call a hook and returns its result
BookPlugin.prototype.hook = function(name, input) {
    // Our book will be the context to apply
    var context = this.book;

    var hookFunc = this.content.hooks? this.content.hooks[name] : null;
    input = input || {};

    if (!hookFunc) return Promise(input);

    this.book.log.debug.ln('call hook "' + name + '" for plugin "' + this.id + '"');
    if (!_.contains(HOOKS, name)) {
        this.book.log.warn.ln('hook "'+name+'" used by plugin "'+this.name+'" is deprecated, and will be removed in the coming versions');
    }

    return Promise()
    .then(function() {
        return hookFunc.apply(context, [input]);
    });
};

module.exports = BookPlugin;