Skip to content

Commit 1a443e4

Browse files
jamos-tayyamgent
authored andcommitted
Add plugin functionality (#474)
1 parent 093bed2 commit 1a443e4

File tree

13 files changed

+367
-60
lines changed

13 files changed

+367
-60
lines changed

docs/_markbind/navigation/userGuideSections.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Formatting Contents]({{baseUrl}}/userGuide/formattingContents.html)
1010
* [Using Components]({{baseUrl}}/userGuide/usingComponents.html)
1111
* [Using HTML, JavaScript, CSS]({{baseUrl}}/userGuide/usingHtmlJavaScriptCss.html)
12+
* [Using Plugins]({{baseUrl}}/userGuide/usingPlugins.html)
1213
* [Tweaking the Page Structure]({{baseUrl}}/userGuide/tweakingThePageStructure.html)
1314
* [Reusing Contents]({{baseUrl}}/userGuide/reusingContents.html)
1415
* [Making the Site Searchable]({{baseUrl}}/userGuide/makingTheSiteSearchable.html)

docs/images/rendering.png

16.7 KB
Loading

docs/userGuide/usingPlugins.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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. MarkBind allows plugins to modify a page's content during the page generation process.
13+
14+
<tip-box type="warning">
15+
16+
**WARNING:** Plugins are executable programs that can be written by anyone. This means that they might contain malicious code that may damage your computer.
17+
18+
Only run plugins from sources that you trust. Do not run the plugin if the source/origin of the plugin cannot be ascertained.
19+
</tip-box>
20+
21+
### Adding Plugins
22+
23+
Plugins are stored in the `_markbind/plugins` folder which is generated on `init`. To use a plugin, place the `js` source of the plugin in the `_markbind/plugins` folder and add the following options to `site.json`:
24+
25+
- `plugins`: An array of plugin names to use.
26+
- `pluginsContext`: A mapping of plugin names to parameters passed to each individual plugin. It is recommended to use key-value pairs for consistency.
27+
28+
For example:
29+
30+
```js
31+
{
32+
...
33+
"plugins": [
34+
"plugin1",
35+
"plugin2",
36+
],
37+
"pluginsContext": {
38+
"plugin1": {
39+
"input": "Input for Plugin 1"
40+
},
41+
"plugin2": {
42+
"data": "Data for Plugin 2"
43+
},
44+
}
45+
}
46+
```
47+
48+
### Writing Plugins
49+
50+
![MarkBind Rendering]({{baseUrl}}/images/rendering.png)
51+
52+
MarkBind provides two entry points for modifying the page, pre-render and post-render. These are controlled by implementing the `preRender()` and `postRender()` functions in the plugin:
53+
54+
- `preRender(content, pluginContext, frontMatter)`: Called before MarkBind renders the source from Markdown to HTML.
55+
- `content`: The raw Markdown of any Markdown file (`.md`, `.mbd`, etc.).
56+
- `pluginContext`: User provided parameters for the plugin. This can be specified in the `site.json`.
57+
- `frontMatter`: The frontMatter of the page being processed, in case any frontMatter data is required.
58+
- `postRender(content, pluginContext, frontMatter)`: Called after the HTML is rendered, before writing it to a file.
59+
- `content`: The rendered HTML.
60+
- `pluginContext`: User provided parameters for the plugin. This can be specified in the `site.json`.
61+
- `frontMatter`: The frontMatter of the page being processed, in case any frontMatter data is required.
62+
63+
MarkBind will call these functions with the respective content, and retrieve a string data that is used for the next step of the page generation process.
64+
65+
An example of a plugin is shown below. The plugin shows two ways of appending a paragraph of text to a specific `div` in the Markdown files:
66+
67+
```js
68+
// myPlugin.js
69+
70+
const cheerio = module.parent.require('cheerio');
71+
72+
module.exports = {
73+
preRender: (content, pluginContext, frontMatter) => content.replace('[Pre-render Placeholder]', `${pluginContext.pre}`),
74+
postRender: (content, pluginContext, frontMatter) => {
75+
const $ = cheerio.load(content, { xmlMode: false });
76+
// Modify the page...
77+
$('#my-div').append(pluginContext.post);
78+
return $.html();
79+
},
80+
};
81+
```
82+
83+
```js
84+
// site.json
85+
86+
{
87+
...
88+
"plugins": [
89+
"myPlugin"
90+
],
91+
"pluginsContext": {
92+
"myPlugin": {
93+
"pre": "<p>Hello</p>",
94+
"post": "<p>Goodbye</p>"
95+
}
96+
}
97+
}
98+
```
99+
100+
```md
101+
// index.md
102+
103+
...
104+
<div id="my-div">
105+
[Pre-render Placeholder]
106+
</div>
107+
```
108+
109+
### Built-in plugins
110+
111+
MarkBind has a set of built-in plugins that can be used immediately without installation.
112+
113+
#### `filterTags`: Toggling alternative contents in a page
114+
115+
You can use tags to selectively filter HTML elements when building a site.
116+
117+
Tags are specified by the `tags` attribute, **and can be attached to any HTML element**. During rendering, only elements that match tags specified in the `site.json` files will be rendered.
118+
119+
<div class="indented">
120+
121+
{{ icon_example }} Attaching tags to elements:
122+
```html
123+
# Print 'Hello world'
124+
125+
<p tags="language--java">System.out.println("Hello world");</p>
126+
<p tags="language--C#">Console.WriteLine("Hello world");</p>
127+
<p tags="language--python">print("Hello world")</p>
128+
```
129+
130+
You need to specify the tags to include in the `pluginsContext`, under `tags`:
131+
132+
```json
133+
{
134+
...
135+
"plugins" : [
136+
"filterTags"
137+
],
138+
"pluginsContext" : {
139+
"filterTags" : {
140+
"tags": ["language--java"]
141+
}
142+
}
143+
}
144+
```
145+
146+
All other tagged elements will be filtered out. In this case, only the element with the `language--java` tag will be rendered. This is helpful when creating multiple versions of a page without having to maintain separate copies.
147+
148+
</div>
149+
150+
If the `filterTags` plugin is not enabled in `site.json`, all tagged elements will be rendered.
151+
152+
**You can also use multiple tags in a single HTML element. Specify each tag in the `tags` attribute** separated by a space. An element will be rendered if **any of the tags** matches the one in `site.json`.
153+
154+
<div class="indented">
155+
156+
{{ icon_example }} Attaching multiple tags to an element:
157+
```html
158+
# For loops
159+
160+
<p tags="language--java language--C#">for (int i = 0; i < 5; i++) { ... }</p>
161+
```
162+
163+
As long as the `language--java` or `language--C#` tag is specified, the code snippet will be rendered.
164+
165+
</div>
166+
167+
Alternatively, you can specify tags to render for a page in the front matter.
168+
169+
<div class="indented">
170+
171+
{{ icon_example }} Specifying tags in front matter:
172+
```html
173+
<frontmatter>
174+
title: "Hello World"
175+
tags: ["language--java"]
176+
</frontmatter>
177+
```
178+
</div>
179+
180+
Tags in `site.json` will take precedence over the ones in the front matter.
181+
182+
</div>

src/Page.js

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ function Page(pageConfig) {
6666
this.layoutsAssetPath = pageConfig.layoutsAssetPath;
6767
this.rootPath = pageConfig.rootPath;
6868
this.enableSearch = pageConfig.enableSearch;
69+
this.plugins = pageConfig.plugins;
70+
this.pluginsContext = pageConfig.pluginsContext;
6971
this.searchable = pageConfig.searchable;
7072
this.src = pageConfig.src;
71-
this.tags = pageConfig.tags;
7273
this.template = pageConfig.pageTemplate;
7374
this.title = pageConfig.title || '';
7475
this.titlePrefix = pageConfig.titlePrefix;
@@ -451,29 +452,6 @@ Page.prototype.concatenateHeadingsAndKeywords = function () {
451452
});
452453
};
453454

454-
/**
455-
* Filters out elements on the page based on config tags
456-
* @param tags to filter
457-
* @param content of the page
458-
*/
459-
Page.prototype.filterTags = function (tags, content) {
460-
if (tags === undefined) {
461-
return content;
462-
}
463-
const tagsArray = Array.isArray(tags) ? tags : [tags];
464-
const $ = cheerio.load(content, { xmlMode: false });
465-
$('[tags]').each((i, element) => {
466-
$(element).attr('hidden', true);
467-
});
468-
tagsArray.forEach((tag) => {
469-
$(`[tags~="${tag}"]`).each((i, element) => {
470-
$(element).removeAttr('hidden');
471-
});
472-
});
473-
$('[hidden]').remove();
474-
return $.html();
475-
};
476-
477455
/**
478456
* Adds anchor links to headings in the page
479457
* @param content of the page
@@ -530,8 +508,6 @@ Page.prototype.collectFrontMatter = function (includedPage) {
530508
this.frontMatter.title = (this.title || this.frontMatter.title || '');
531509
// Layout specified in site.json will override layout specified in the front matter
532510
this.frontMatter.layout = (this.layout || this.frontMatter.layout || LAYOUT_DEFAULT_NAME);
533-
// Included tags specified in site.json will override included tags specified in front matter
534-
this.frontMatter.tags = (this.tags || this.frontMatter.tags);
535511
} else {
536512
// Page is addressable but no front matter specified
537513
this.frontMatter = {
@@ -801,6 +777,7 @@ Page.prototype.generate = function (builtFiles) {
801777
return this.removeFrontMatter(result);
802778
})
803779
.then(result => addContentWrapper(result))
780+
.then(result => this.preRender(result))
804781
.then(result => this.insertPageNavWrapper(result))
805782
.then(result => this.insertSiteNav((result)))
806783
.then(result => this.insertTemporaryStyles(result))
@@ -809,8 +786,8 @@ Page.prototype.generate = function (builtFiles) {
809786
.then(result => markbinder.resolveBaseUrl(result, fileConfig))
810787
.then(result => fs.outputFileAsync(this.tempPath, result))
811788
.then(() => markbinder.renderFile(this.tempPath, fileConfig))
812-
.then(result => this.filterTags(this.frontMatter.tags, result))
813789
.then(result => this.addAnchors(result))
790+
.then(result => this.postRender(result))
814791
.then((result) => {
815792
this.content = htmlBeautify(result, { indent_size: 2 });
816793

@@ -844,6 +821,34 @@ Page.prototype.generate = function (builtFiles) {
844821
});
845822
};
846823

824+
/**
825+
* Entry point for plugin pre-render
826+
*/
827+
Page.prototype.preRender = function (content) {
828+
let preRenderedContent = content;
829+
Object.entries(this.plugins).forEach(([pluginName, plugin]) => {
830+
if (plugin.preRender) {
831+
preRenderedContent
832+
= plugin.preRender(preRenderedContent, this.pluginsContext[pluginName], this.frontMatter);
833+
}
834+
});
835+
return preRenderedContent;
836+
};
837+
838+
/**
839+
* Entry point for plugin post-render
840+
*/
841+
Page.prototype.postRender = function (content) {
842+
let postRenderedContent = content;
843+
Object.entries(this.plugins).forEach(([pluginName, plugin]) => {
844+
if (plugin.postRender) {
845+
postRenderedContent
846+
= plugin.postRender(postRenderedContent, this.pluginsContext[pluginName], this.frontMatter);
847+
}
848+
});
849+
return postRenderedContent;
850+
};
851+
847852
/**
848853
* Adds linked layout files to page assets
849854
*/

0 commit comments

Comments
 (0)