Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_markbind/navigation/userGuideSections.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [Formatting Contents]({{baseUrl}}/userGuide/formattingContents.html)
* [Using Components]({{baseUrl}}/userGuide/usingComponents.html)
* [Using HTML, JavaScript, CSS]({{baseUrl}}/userGuide/usingHtmlJavaScriptCss.html)
* [Using Plugins]({{baseUrl}}/userGuide/usingPlugins.html)
* [Tweaking the Page Structure]({{baseUrl}}/userGuide/tweakingThePageStructure.html)
* [Reusing Contents]({{baseUrl}}/userGuide/reusingContents.html)
* [Making the Site Searchable]({{baseUrl}}/userGuide/makingTheSiteSearchable.html)
Expand Down
Binary file added docs/images/rendering.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
182 changes: 182 additions & 0 deletions docs/userGuide/usingPlugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<frontmatter>
footer: userGuideFooter.md
siteNav: userGuideSections.md
</frontmatter>

<include src="../common/header.md" />

<div class="website-content">

# Using Plugins

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.

<tip-box type="warning">

**WARNING:** Plugins are executable programs that can be written by anyone. This means that they might contain malicious code that may damage your computer.

Only run plugins from sources that you trust. Do not run the plugin if the source/origin of the plugin cannot be ascertained.
</tip-box>

### Adding Plugins

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`:

- `plugins`: An array of plugin names to use.
- `pluginsContext`: A mapping of plugin names to parameters passed to each individual plugin. It is recommended to use key-value pairs for consistency.

For example:

```js
{
...
"plugins": [
"plugin1",
"plugin2",
],
"pluginsContext": {
"plugin1": {
"input": "Input for Plugin 1"
},
"plugin2": {
"data": "Data for Plugin 2"
},
}
}
```

### Writing Plugins

![MarkBind Rendering]({{baseUrl}}/images/rendering.png)

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:

- `preRender(content, pluginContext, frontMatter)`: Called before MarkBind renders the source from Markdown to HTML.
- `content`: The raw Markdown of any Markdown file (`.md`, `.mbd`, etc.).
- `pluginContext`: User provided parameters for the plugin. This can be specified in the `site.json`.
- `frontMatter`: The frontMatter of the page being processed, in case any frontMatter data is required.
- `postRender(content, pluginContext, frontMatter)`: Called after the HTML is rendered, before writing it to a file.
- `content`: The rendered HTML.
- `pluginContext`: User provided parameters for the plugin. This can be specified in the `site.json`.
- `frontMatter`: The frontMatter of the page being processed, in case any frontMatter data is required.

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.

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:

```js
// myPlugin.js

const cheerio = module.parent.require('cheerio');

module.exports = {
preRender: (content, pluginContext, frontMatter) => content.replace('[Pre-render Placeholder]', `${pluginContext.pre}`),
postRender: (content, pluginContext, frontMatter) => {
const $ = cheerio.load(content, { xmlMode: false });
// Modify the page...
$('#my-div').append(pluginContext.post);
return $.html();
},
};
```

```js
// site.json

{
...
"plugins": [
"myPlugin"
],
"pluginsContext": {
"myPlugin": {
"pre": "<p>Hello</p>",
"post": "<p>Goodbye</p>"
}
}
}
```

```md
// index.md

...
<div id="my-div">
[Pre-render Placeholder]
</div>
```

### Built-in plugins

MarkBind has a set of built-in plugins that can be used immediately without installation.

#### `filterTags`: Toggling alternative contents in a page

You can use tags to selectively filter HTML elements when building a site.

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.

<div class="indented">

{{ icon_example }} Attaching tags to elements:
```html
# Print 'Hello world'

<p tags="language--java">System.out.println("Hello world");</p>
<p tags="language--C#">Console.WriteLine("Hello world");</p>
<p tags="language--python">print("Hello world")</p>
```

You need to specify the tags to include in the `pluginsContext`, under `tags`:

```json
{
...
"plugins" : [
"filterTags"
],
"pluginsContext" : {
"filterTags" : {
"tags": ["language--java"]
}
}
}
```

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.

</div>

If the `filterTags` plugin is not enabled in `site.json`, all tagged elements will be rendered.

**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`.

<div class="indented">

{{ icon_example }} Attaching multiple tags to an element:
```html
# For loops

<p tags="language--java language--C#">for (int i = 0; i < 5; i++) { ... }</p>
```

As long as the `language--java` or `language--C#` tag is specified, the code snippet will be rendered.

</div>

Alternatively, you can specify tags to render for a page in the front matter.

<div class="indented">

{{ icon_example }} Specifying tags in front matter:
```html
<frontmatter>
title: "Hello World"
tags: ["language--java"]
</frontmatter>
```
</div>

Tags in `site.json` will take precedence over the ones in the front matter.

</div>
59 changes: 32 additions & 27 deletions src/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ function Page(pageConfig) {
this.layoutsAssetPath = pageConfig.layoutsAssetPath;
this.rootPath = pageConfig.rootPath;
this.enableSearch = pageConfig.enableSearch;
this.plugins = pageConfig.plugins;
this.pluginsContext = pageConfig.pluginsContext;
this.searchable = pageConfig.searchable;
this.src = pageConfig.src;
this.tags = pageConfig.tags;
this.template = pageConfig.pageTemplate;
this.title = pageConfig.title || '';
this.titlePrefix = pageConfig.titlePrefix;
Expand Down Expand Up @@ -447,29 +448,6 @@ Page.prototype.concatenateHeadingsAndKeywords = function () {
});
};

/**
* Filters out elements on the page based on config tags
* @param tags to filter
* @param content of the page
*/
Page.prototype.filterTags = function (tags, content) {
if (tags === undefined) {
return content;
}
const tagsArray = Array.isArray(tags) ? tags : [tags];
const $ = cheerio.load(content, { xmlMode: false });
$('[tags]').each((i, element) => {
$(element).attr('hidden', true);
});
tagsArray.forEach((tag) => {
$(`[tags~="${tag}"]`).each((i, element) => {
$(element).removeAttr('hidden');
});
});
$('[hidden]').remove();
return $.html();
};

/**
* Adds anchor links to headings in the page
* @param content of the page
Expand Down Expand Up @@ -526,8 +504,6 @@ Page.prototype.collectFrontMatter = function (includedPage) {
this.frontMatter.title = (this.title || this.frontMatter.title || '');
// Layout specified in site.json will override layout specified in the front matter
this.frontMatter.layout = (this.layout || this.frontMatter.layout || LAYOUT_DEFAULT_NAME);
// Included tags specified in site.json will override included tags specified in front matter
this.frontMatter.tags = (this.tags || this.frontMatter.tags);
} else {
// Page is addressable but no front matter specified
this.frontMatter = {
Expand Down Expand Up @@ -782,15 +758,16 @@ Page.prototype.generate = function (builtFiles) {
return this.removeFrontMatter(result);
})
.then(result => addContentWrapper(result))
.then(result => this.preRender(result))
.then(result => this.insertPageNavWrapper(result))
.then(result => this.insertSiteNav((result)))
.then(result => this.insertFooter(result)) // Footer has to be inserted last to ensure proper formatting
.then(result => formatFooter(result))
.then(result => markbinder.resolveBaseUrl(result, fileConfig))
.then(result => fs.outputFileAsync(this.tempPath, result))
.then(() => markbinder.renderFile(this.tempPath, fileConfig))
.then(result => this.filterTags(this.frontMatter.tags, result))
.then(result => this.addAnchors(result))
.then(result => this.postRender(result))
.then((result) => {
this.content = htmlBeautify(result, { indent_size: 2 });

Expand Down Expand Up @@ -824,6 +801,34 @@ Page.prototype.generate = function (builtFiles) {
});
};

/**
* Entry point for plugin pre-render
*/
Page.prototype.preRender = function (content) {
let preRenderedContent = content;
Object.entries(this.plugins).forEach(([pluginName, plugin]) => {
if (plugin.preRender) {
preRenderedContent
= plugin.preRender(preRenderedContent, this.pluginsContext[pluginName], this.frontMatter);
}
});
return preRenderedContent;
};

/**
* Entry point for plugin post-render
*/
Page.prototype.postRender = function (content) {
let postRenderedContent = content;
Object.entries(this.plugins).forEach(([pluginName, plugin]) => {
if (plugin.postRender) {
postRenderedContent
= plugin.postRender(postRenderedContent, this.pluginsContext[pluginName], this.frontMatter);
}
});
return postRenderedContent;
};

/**
* Adds linked layout files to page assets
*/
Expand Down
Loading