Skip to content

Commit 7dfb7d9

Browse files
committed
Add plugins functionality
1 parent 8d29884 commit 7dfb7d9

File tree

14 files changed

+212
-4
lines changed

14 files changed

+212
-4
lines changed

docs/_markbind/navigation/userGuideSections.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [Including Contents]({{baseUrl}}/userGuide/includingContents.html)
88
* [Using Components]({{baseUrl}}/userGuide/usingComponents.html)
99
* [Using Variables]({{baseUrl}}/userGuide/usingVariables.html)
10+
* [Using Plugins]({{baseUrl}}/userGuide/usingPlugins.html)
1011
* [Page Layout]({{baseUrl}}/userGuide/pageLayout.html)
1112
* [Known Problems]({{baseUrl}}/userGuide/knownProblems.html)
1213
* Deploying a Site

docs/common/header.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<li><a href="{{baseUrl}}/userGuide/includingContents.html" class="dropdown-item">◦&nbsp; Including Contents</a></li>
1313
<li><a href="{{baseUrl}}/userGuide/usingComponents.html" class="dropdown-item">◦&nbsp; Using Components</a></li>
1414
<li><a href="{{baseUrl}}/userGuide/usingVariables.html" class="dropdown-item">◦&nbsp; Using Variables</a></li>
15+
<li><a href="{{baseUrl}}/userGuide/usingPlugins.html" class="dropdown-item">◦&nbsp; Using Plugins</a></li>
1516
<li><a href="{{baseUrl}}/userGuide/pageLayout.html" class="dropdown-item">◦&nbsp; Page Layout</a></li>
1617
<li><a href="{{baseUrl}}/userGuide/knownProblems.html" class="dropdown-item">◦&nbsp; Known Problems</a></li>
1718
<li role="separator" class="dropdown-divider"></li>

docs/images/rendering.png

16.7 KB
Loading

docs/site.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
"src": "userGuide/usingVariables.md",
3838
"title": "MarkBind: User Guide - Using Variables"
3939
},
40+
{
41+
"src": "userGuide/usingPlugins.md",
42+
"title": "MarkBind: User Guide - Using Plugins"
43+
},
4044
{
4145
"src": "userGuide/pageLayout.md",
4246
"title": "MarkBind: User Guide - Page Layout"

docs/userGuide/usingPlugins.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<frontmatter>
2+
footer: userGuideFooter.md
3+
siteNav: userGuideSections.md
4+
</frontmatter>
5+
6+
<include src="../common/header.md" />
7+
8+
<div class="website-content">
9+
10+
# Using Plugins
11+
12+
A plugin is a user defined extension that can add custom features to MarkBind. Markbind plugins are `js` scripts that are loaded and run during the page generation.
13+
14+
### Adding Plugins
15+
16+
Plugins are stored in the `_markbind/plugins` folder which is generated on `init`. To use a plugin, place the `js` file in the `_markbind/plugins` folder and add the following options to the site configuration:
17+
18+
- `plugins`: An array of plugin names to use
19+
- `context`: An object mapping plugin name to data provided to the plugin. Each plugin can have its own custom data.
20+
21+
For example:
22+
23+
```
24+
{
25+
...
26+
"plugins" : [
27+
"plugin1",
28+
"plugin2",
29+
],
30+
"context" : {
31+
"plugin1" : "Input Data",
32+
"plugin2" : ["Input", "Data"]
33+
}
34+
}
35+
```
36+
37+
### Writing Plugins
38+
39+
MarkBind plugins allow the user to mutate the page data (`html`) during the page compilation process.
40+
41+
![Markbind Rendering]({{baseUrl}}/images/rendering.png)
42+
43+
MarkBind provides two entry points for modifying the page:
44+
45+
- Prerender: Called before any markbind rendering is done on the site. Markbind rendering refers to the conversion of markdown to valid HTML.
46+
- Postrender: Called after all markbind rendering is done on the site.
47+
48+
These are controlled by specifying the `preRender` and `postRender` functions in the plugin `js`. Each function takes in two parameters:
49+
50+
- `content`: The intermediate content of the page data represented as a string.
51+
- For `preRender`, `content` will be the raw markdown of a `.md` file.
52+
- For `postRender`, `content` will be the compiled `.html` file.
53+
- `siteContext`: Any other data to be provided to the plugin. This can be specified in the `site.json`.
54+
55+
Markbind will call these two functions with the respective content, and retrieve a string data that is used for the next step of the compilation process.
56+
57+
A plugin will typically follow a format similar to this one:
58+
59+
```js
60+
// myPlugin.js
61+
62+
const cheerio = module.parent.require('cheerio');
63+
64+
module.exports = {
65+
postRender: (content, siteContext) => {
66+
const $ = cheerio.load(content, { xmlMode: false });
67+
// Modify the page...
68+
$('#my-div').append(siteContext["post"]);
69+
return $.html();
70+
},
71+
};
72+
```
73+
74+
```js
75+
// site.json
76+
77+
{
78+
...
79+
"plugins" : [
80+
"myPlugin"
81+
],
82+
"context" : {
83+
"plugin1" : {
84+
"post" : "<p>Hello</p>"
85+
}
86+
}
87+
}
88+
```
89+
90+
```md
91+
// index.md
92+
93+
...
94+
<div id="my-div"></div>
95+
```
96+
97+
The above plugin appends a paragraph of text to a div in the `index.md`.
98+
99+
</div>

lib/Page.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ function Page(pageConfig) {
5151
this.baseUrl = pageConfig.baseUrl;
5252
this.baseUrlMap = pageConfig.baseUrlMap;
5353
this.content = pageConfig.content || '';
54+
this.context = pageConfig.context;
5455
this.faviconUrl = pageConfig.faviconUrl;
5556
this.layout = pageConfig.layout;
5657
this.layoutsAssetPath = pageConfig.layoutsAssetPath;
5758
this.rootPath = pageConfig.rootPath;
59+
this.plugins = pageConfig.plugins;
5860
this.searchable = pageConfig.searchable;
5961
this.src = pageConfig.src;
6062
this.template = pageConfig.pageTemplate;
@@ -555,13 +557,15 @@ Page.prototype.generate = function (builtFiles) {
555557
return this.removeFrontMatter(result);
556558
})
557559
.then(result => addContentWrapper(result))
560+
.then(result => this.preRender(result))
558561
.then(result => this.insertSiteNav((result)))
559562
.then(result => this.insertFooter(result)) // Footer has to be inserted last to ensure proper formatting
560563
.then(result => formatFooter(result))
561564
.then(result => markbinder.resolveBaseUrl(result, fileConfig))
562565
.then(result => fs.outputFileAsync(this.tempPath, result))
563566
.then(() => markbinder.renderFile(this.tempPath, fileConfig))
564567
.then(result => this.addAnchors(result))
568+
.then(result => this.postRender(result))
565569
.then((result) => {
566570
this.content = htmlBeautify(result, { indent_size: 2 });
567571

@@ -594,6 +598,32 @@ Page.prototype.generate = function (builtFiles) {
594598
});
595599
};
596600

601+
/**
602+
* Entry point for plugin pre render
603+
*/
604+
Page.prototype.preRender = function (content) {
605+
let preRenderedContent = content;
606+
Object.keys(this.plugins).forEach((plugin) => {
607+
if (this.plugins[plugin].preRender) {
608+
preRenderedContent = this.plugins[plugin].preRender(preRenderedContent, this.context[plugin]);
609+
}
610+
});
611+
return preRenderedContent;
612+
};
613+
614+
/**
615+
* Entry point for plugin post render
616+
*/
617+
Page.prototype.postRender = function (content) {
618+
let postRenderedContent = content;
619+
Object.keys(this.plugins).forEach((plugin) => {
620+
if (this.plugins[plugin].postRender) {
621+
postRenderedContent = this.plugins[plugin].postRender(postRenderedContent, this.context[plugin]);
622+
}
623+
});
624+
return postRenderedContent;
625+
};
626+
597627
/**
598628
* Adds linked layout files to page assets
599629
*/

lib/Site.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const GLYPHICONS_PATH = 'asset/glyphicons.csv';
3939
const HEAD_FOLDER_PATH = '_markbind/head';
4040
const INDEX_MARKDOWN_FILE = 'index.md';
4141
const PAGE_TEMPLATE_NAME = 'page.ejs';
42+
const PLUGIN_FOLDER_PATH = '_markbind/plugins';
4243
const SITE_CONFIG_NAME = 'site.json';
4344
const SITE_DATA_NAME = 'siteData.json';
4445
const SITE_NAV_PATH = '_markbind/navigation/site-nav.md';
@@ -123,6 +124,7 @@ function Site(rootPath, outputPath, forceReload = false, siteConfigPath = SITE_C
123124
this.addressablePages = [];
124125
this.baseUrlMap = {};
125126
this.forceReload = forceReload;
127+
this.loadedPlugins = {};
126128
this.siteConfig = {};
127129
this.siteConfigPath = siteConfigPath;
128130
this.userDefinedVariablesMap = {};
@@ -184,6 +186,7 @@ Site.initSite = function (rootPath) {
184186
const siteNavPath = path.join(rootPath, SITE_NAV_PATH);
185187
const siteLayoutPath = path.join(rootPath, LAYOUT_FOLDER_PATH);
186188
const siteLayoutDefaultPath = path.join(siteLayoutPath, LAYOUT_DEFAULT_NAME);
189+
const sitePluginPath = path.join(rootPath, PLUGIN_FOLDER_PATH);
187190
const userDefinedVariablesPath = path.join(rootPath, USER_VARIABLES_PATH);
188191
// TODO: log the generate info
189192
return new Promise((resolve, reject) => {
@@ -261,6 +264,13 @@ Site.initSite = function (rootPath) {
261264
});
262265
});
263266
})
267+
.then(() => fs.accessAsync(sitePluginPath))
268+
.catch(() => {
269+
if (fs.existsSync(sitePluginPath)) {
270+
return Promise.resolve();
271+
}
272+
return fs.mkdirp(sitePluginPath);
273+
})
264274
.then(resolve)
265275
.catch(reject);
266276
});
@@ -302,8 +312,10 @@ Site.prototype.createPage = function (config) {
302312
baseUrl: this.siteConfig.baseUrl,
303313
baseUrlMap: this.baseUrlMap,
304314
content: '',
315+
context: this.siteConfig.context || {},
305316
faviconUrl: config.faviconUrl,
306317
pageTemplate: this.pageTemplate,
318+
plugins: this.loadedPlugins || {},
307319
rootPath: this.rootPath,
308320
searchable: config.searchable,
309321
src: config.pageSrc,
@@ -449,6 +461,7 @@ Site.prototype.generate = function (baseUrl) {
449461
.then(() => this.collectAddressablePages())
450462
.then(() => this.collectBaseUrl())
451463
.then(() => this.collectUserDefinedVariablesMap())
464+
.then(() => this.collectPlugins())
452465
.then(() => this.buildAssets())
453466
.then(() => this.buildSourceFiles())
454467
.then(() => this.copyMarkBindAsset())
@@ -564,6 +577,19 @@ Site.prototype.buildAssets = function () {
564577
});
565578
};
566579

580+
/**
581+
* Load all plugins of the site
582+
*/
583+
Site.prototype.collectPlugins = function () {
584+
if (!this.siteConfig.plugins) {
585+
return;
586+
}
587+
this.siteConfig.plugins.forEach((plugin) => {
588+
// eslint-disable-next-line global-require, import/no-dynamic-require
589+
this.loadedPlugins[plugin] = require(path.join(this.rootPath, PLUGIN_FOLDER_PATH, `${plugin}.js`));
590+
});
591+
};
592+
567593
/**
568594
* Renders all pages specified in site configuration file to the output folder
569595
*/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const cheerio = module.parent.require('cheerio');
2+
3+
module.exports = {
4+
preRender: (content, siteContext) => content.replace('Prerender', `${siteContext.pre}`),
5+
postRender: (content, siteContext) => {
6+
const $ = cheerio.load(content, { xmlMode: false });
7+
$('#test-plugins').append(`${siteContext.post}`);
8+
return $.html();
9+
},
10+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const cheerio = module.parent.require('cheerio');
2+
3+
module.exports = {
4+
preRender: (content, siteContext) => content.replace('Prerender', `${siteContext.pre}`),
5+
postRender: (content, siteContext) => {
6+
const $ = cheerio.load(content, { xmlMode: false });
7+
$('#test-plugins').append(`${siteContext.post}`);
8+
return $.html();
9+
},
10+
};

test/test_site/expected/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,11 @@ <h2 id="panel-content-of-unexpanded-panel-should-not-appear-in-search-data">Pane
295295
<h2 id="panel-content-inside-unexpanded-panel-should-not-appear-in-search-data">Panel content inside unexpanded panel should not appear in search data<a class="fa fa-anchor" href="#panel-content-inside-unexpanded-panel-should-not-appear-in-search-data"></a></h2>
296296
</panel>
297297
</panel>
298+
<h1 id="div-for-plugins">Div for plugins<a class="fa fa-anchor" href="#div-for-plugins"></a></h1>
299+
<div id="test-plugins">
300+
<p>Prerender</p>
301+
<p>Postrender</p>
302+
</div>
298303
</div>
299304
</div>
300305
</div>

0 commit comments

Comments
 (0)