From bb02b941e3a5ca482fda7c82a5b90da3482bf4cf Mon Sep 17 00:00:00 2001 From: jamos-tay Date: Sun, 10 Feb 2019 17:02:06 +0800 Subject: [PATCH] Add plugin functionality --- .../_markbind/navigation/userGuideSections.md | 1 + docs/images/rendering.png | Bin 0 -> 17065 bytes docs/userGuide/usingPlugins.md | 182 ++++++++++++++++++ src/Page.js | 59 +++--- src/Site.js | 55 +++++- src/plugins/filterTags.js | 32 +++ .../_markbind/plugins/testMarkbindPlugin.js | 11 ++ .../_markbind/plugins/testMarkbindPlugin.js | 11 ++ test/functional/test_site/expected/index.html | 7 + .../test_site/expected/siteData.json | 37 +--- test/functional/test_site/index.md | 6 + test/functional/test_site/site.json | 16 +- test/unit/Site.test.js | 10 +- 13 files changed, 367 insertions(+), 60 deletions(-) create mode 100644 docs/images/rendering.png create mode 100644 docs/userGuide/usingPlugins.md create mode 100644 src/plugins/filterTags.js create mode 100644 test/functional/test_site/_markbind/plugins/testMarkbindPlugin.js create mode 100644 test/functional/test_site/expected/_markbind/plugins/testMarkbindPlugin.js diff --git a/docs/_markbind/navigation/userGuideSections.md b/docs/_markbind/navigation/userGuideSections.md index f0f1415be0..9f805e0c78 100644 --- a/docs/_markbind/navigation/userGuideSections.md +++ b/docs/_markbind/navigation/userGuideSections.md @@ -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) diff --git a/docs/images/rendering.png b/docs/images/rendering.png new file mode 100644 index 0000000000000000000000000000000000000000..33a9e08165cd4b88ca79e4b4fc7ee763204e6b3f GIT binary patch literal 17065 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≧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 + footer: userGuideFooter.md + siteNav: userGuideSections.md + + + + +
+ +# 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. + + + +**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. + + +### 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": "

Hello

", + "post": "

Goodbye

" + } + } +} +``` + +```md +// index.md + +... +
+[Pre-render Placeholder] +
+``` + +### 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. + +
+ +{{ icon_example }} Attaching tags to elements: +```html +# Print 'Hello world' + +

System.out.println("Hello world");

+

Console.WriteLine("Hello world");

+

print("Hello world")

+``` + +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. + +
+ +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`. + +
+ +{{ icon_example }} Attaching multiple tags to an element: +```html +# For loops + +

for (int i = 0; i < 5; i++) { ... }

+``` + +As long as the `language--java` or `language--C#` tag is specified, the code snippet will be rendered. + +
+ +Alternatively, you can specify tags to render for a page in the front matter. + +
+ +{{ icon_example }} Specifying tags in front matter: +```html + + title: "Hello World" + tags: ["language--java"] + +``` +
+ +Tags in `site.json` will take precedence over the ones in the front matter. + +
diff --git a/src/Page.js b/src/Page.js index aa24ba63dc..0026298b23 100644 --- a/src/Page.js +++ b/src/Page.js @@ -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; @@ -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 @@ -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 = { @@ -782,6 +758,7 @@ 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 @@ -789,8 +766,8 @@ Page.prototype.generate = function (builtFiles) { .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 }); @@ -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 */ diff --git a/src/Site.js b/src/Site.js index b2f619935c..f9d8f906b4 100644 --- a/src/Site.js +++ b/src/Site.js @@ -34,6 +34,7 @@ const TEMP_FOLDER_NAME = '.temp'; const TEMPLATE_ROOT_FOLDER_NAME = 'template'; const TEMPLATE_SITE_ASSET_FOLDER_NAME = 'markbind'; +const BUILT_IN_PLUGIN_FOLDER_NAME = 'plugins'; const FAVICON_DEFAULT_PATH = 'favicon.ico'; const FONT_AWESOME_PATH = 'asset/font-awesome.csv'; const FOOTER_PATH = '_markbind/footers/footer.md'; @@ -41,6 +42,7 @@ const GLYPHICONS_PATH = 'asset/glyphicons.csv'; const HEAD_FOLDER_PATH = '_markbind/head'; const INDEX_MARKDOWN_FILE = 'index.md'; const PAGE_TEMPLATE_NAME = 'page.ejs'; +const PROJECT_PLUGIN_FOLDER_NAME = '_markbind/plugins'; const SITE_CONFIG_NAME = 'site.json'; const SITE_DATA_NAME = 'siteData.json'; const SITE_NAV_PATH = '_markbind/navigation/site-nav.md'; @@ -135,6 +137,7 @@ function Site(rootPath, outputPath, onePagePath, forceReload = false, siteConfig this.baseUrlMap = {}; this.forceReload = forceReload; this.onePagePath = onePagePath; + this.plugins = {}; this.siteConfig = {}; this.siteConfigPath = siteConfigPath; this.userDefinedVariablesMap = {}; @@ -197,6 +200,7 @@ Site.initSite = function (rootPath) { const siteLayoutPath = path.join(rootPath, LAYOUT_FOLDER_PATH); const siteLayoutDefaultPath = path.join(siteLayoutPath, LAYOUT_DEFAULT_NAME); const siteDefaultLayoutScriptsPath = path.join(siteLayoutDefaultPath, LAYOUT_SCRIPTS_PATH); + const sitePluginPath = path.join(rootPath, PROJECT_PLUGIN_FOLDER_NAME); const userDefinedVariablesPath = path.join(rootPath, USER_VARIABLES_PATH); // TODO: log the generate info return new Promise((resolve, reject) => { @@ -281,6 +285,13 @@ Site.initSite = function (rootPath) { } return fs.outputFileAsync(siteDefaultLayoutScriptsPath, LAYOUT_SCRIPTS_DEFAULT); }) + .then(() => fs.accessAsync(sitePluginPath)) + .catch(() => { + if (fs.existsSync(sitePluginPath)) { + return Promise.resolve(); + } + return fs.mkdirp(sitePluginPath); + }) .then(resolve) .catch(reject); }); @@ -323,9 +334,10 @@ Site.prototype.createPage = function (config) { baseUrl: this.siteConfig.baseUrl, baseUrlMap: this.baseUrlMap, content: '', + pluginsContext: this.siteConfig.pluginsContext || {}, faviconUrl: config.faviconUrl, - tags: this.siteConfig.tags, pageTemplate: this.pageTemplate, + plugins: this.plugins || {}, rootPath: this.rootPath, enableSearch: this.siteConfig.enableSearch, searchable: this.siteConfig.enableSearch && config.searchable, @@ -505,6 +517,7 @@ Site.prototype.generate = function (baseUrl) { .then(() => this.collectAddressablePages()) .then(() => this.collectBaseUrl()) .then(() => this.collectUserDefinedVariablesMap()) + .then(() => this.collectPlugins()) .then(() => this.buildAssets()) .then(() => this.buildSourceFiles()) .then(() => this.copyMarkBindAsset()) @@ -642,6 +655,46 @@ Site.prototype.buildAssets = function () { }); }; +/** + * Retrieves the correct plugin path for a plugin name, if not in node_modules + * @param pluginName name of the plugin + */ +function getPluginPath(rootPath, plugin) { + // Check in project folder + const pluginPath = path.join(rootPath, PROJECT_PLUGIN_FOLDER_NAME, `${plugin}.js`); + if (fs.existsSync(pluginPath)) { + return pluginPath; + } + + // Check in src folder + const defaultPath = path.join(__dirname, BUILT_IN_PLUGIN_FOLDER_NAME, `${plugin}.js`); + if (fs.existsSync(defaultPath)) { + return defaultPath; + } + + return ''; +} + +/** + * Load all plugins of the site + */ +Site.prototype.collectPlugins = function () { + if (!this.siteConfig.plugins) { + return; + } + module.paths.push(path.join(this.rootPath, 'node_modules')); + this.siteConfig.plugins.forEach((plugin) => { + try { + const pluginPath = getPluginPath(this.rootPath, plugin); + + // eslint-disable-next-line global-require, import/no-dynamic-require + this.plugins[plugin] = require(pluginPath || plugin); + } catch (e) { + logger.warn(`Unable to load plugin ${plugin}, skipping`); + } + }); +}; + /** * Renders all pages specified in site configuration file to the output folder */ diff --git a/src/plugins/filterTags.js b/src/plugins/filterTags.js new file mode 100644 index 0000000000..b60c912286 --- /dev/null +++ b/src/plugins/filterTags.js @@ -0,0 +1,32 @@ +const cheerio = module.parent.require('cheerio'); + +/** + * Filters out elements on the page based on config tags + * @param tags to filter + * @param content of the page + */ +function filterTags(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(); +} + +module.exports = { + postRender: (content, pluginContext, frontMatter) => { + // Included tags specified in site.json will override included tags specified in front matter + const tags = (pluginContext.tags || frontMatter.tags); + return filterTags(tags, content); + }, +}; diff --git a/test/functional/test_site/_markbind/plugins/testMarkbindPlugin.js b/test/functional/test_site/_markbind/plugins/testMarkbindPlugin.js new file mode 100644 index 0000000000..de0d5020b3 --- /dev/null +++ b/test/functional/test_site/_markbind/plugins/testMarkbindPlugin.js @@ -0,0 +1,11 @@ +const cheerio = module.parent.require('cheerio'); + +module.exports = { + preRender: (content, pluginContext) => + content.replace('Markbind Plugin Pre-render Placeholder', `${pluginContext.pre}`), + postRender: (content, pluginContext) => { + const $ = cheerio.load(content, { xmlMode: false }); + $('#test-markbind-plugin').append(`${pluginContext.post}`); + return $.html(); + }, +}; diff --git a/test/functional/test_site/expected/_markbind/plugins/testMarkbindPlugin.js b/test/functional/test_site/expected/_markbind/plugins/testMarkbindPlugin.js new file mode 100644 index 0000000000..de0d5020b3 --- /dev/null +++ b/test/functional/test_site/expected/_markbind/plugins/testMarkbindPlugin.js @@ -0,0 +1,11 @@ +const cheerio = module.parent.require('cheerio'); + +module.exports = { + preRender: (content, pluginContext) => + content.replace('Markbind Plugin Pre-render Placeholder', `${pluginContext.pre}`), + postRender: (content, pluginContext) => { + const $ = cheerio.load(content, { xmlMode: false }); + $('#test-markbind-plugin').append(`${pluginContext.post}`); + return $.html(); + }, +}; diff --git a/test/functional/test_site/expected/index.html b/test/functional/test_site/expected/index.html index 7f7ea81beb..9cddda4daa 100644 --- a/test/functional/test_site/expected/index.html +++ b/test/functional/test_site/expected/index.html @@ -145,6 +145,8 @@ + Test plugin in markbind/plugins‎ + Markbind Plugin Pre-render‎ @@ -505,6 +507,11 @@

Pane

Panel content inside unexpanded panel should not appear in search data

+

Test plugin in markbind/plugins

+
+

Markbind Plugin Pre-render

+

Node Modules Plugin Post-render

+
diff --git a/test/functional/test_site/expected/siteData.json b/test/functional/test_site/expected/siteData.json index e125636b7d..bad586e925 100644 --- a/test/functional/test_site/expected/siteData.json +++ b/test/functional/test_site/expected/siteData.json @@ -9,11 +9,7 @@ }, "title": "Open Bugs", "src": "bugs/index.md", - "layout": "default", - "tags": [ - "tag--shown", - "tag--shown-2" - ] + "layout": "default" }, { "headings": { @@ -99,7 +95,9 @@ "nested-panel-without-src": "Nested panel without src", "panel-with-src-from-another-markbind-site": "Panel with src from another Markbind site", "modal-with-panel-inside": "Modal with panel inside", - "unexpanded-panel": "Unexpanded panel" + "unexpanded-panel": "Unexpanded panel", + "test-plugin-in-markbind-plugins": "Test plugin in markbind/plugins", + "markbind-plugin-pre-render": "Markbind Plugin Pre-render" }, "title": "Hello World", "footer": "footer.md", @@ -108,11 +106,7 @@ "pageNavTitle": "Testing Page Navigation", "head": "myCustomHead.md, myCustomHead2.md", "src": "index.md", - "layout": "default", - "tags": [ - "tag--shown", - "tag--shown-2" - ] + "layout": "default" }, { "headings": { @@ -134,11 +128,7 @@ "headings": {}, "layout": "testAfterSetup", "src": "testAfterSetup.md", - "title": "Hello World", - "tags": [ - "tag--shown", - "tag--shown-2" - ] + "title": "Hello World" }, { "headings": {}, @@ -159,11 +149,7 @@ "title": "Hello World", "head": "overwriteLayoutHead.md", "layout": "testLayout", - "src": "testLayouts.md", - "tags": [ - "tag--shown", - "tag--shown-2" - ] + "src": "testLayouts.md" }, { "headings": { @@ -172,11 +158,7 @@ "title": "Hello World", "head": "overwriteLayoutHead.md", "layout": "testLayout", - "src": "testLayoutsOverride.md", - "tags": [ - "tag--shown", - "tag--shown-2" - ] + "src": "testLayoutsOverride.md" }, { "headings": { @@ -189,8 +171,7 @@ }, "title": "Hello World", "tags": [ - "tag--shown", - "tag--shown-2" + "tag--should-be-overridden" ], "src": "testTags.md", "layout": "default" diff --git a/test/functional/test_site/index.md b/test/functional/test_site/index.md index 5dd5bc3a84..08bc173b9a 100644 --- a/test/functional/test_site/index.md +++ b/test/functional/test_site/index.md @@ -209,3 +209,9 @@ head: myCustomHead.md, myCustomHead2.md ## Panel content inside unexpanded panel should not appear in search data + +# Test plugin in markbind/plugins + +
+ Markbind Plugin Pre-render Placeholder +
diff --git a/test/functional/test_site/site.json b/test/functional/test_site/site.json index 6b2ac10771..65e1584f3f 100644 --- a/test/functional/test_site/site.json +++ b/test/functional/test_site/site.json @@ -59,6 +59,18 @@ "deploy": { "message": "Site Update." }, - "tags" : ["tag--shown", "tag--shown-2"], - "headingIndexingLevel": 4 + "headingIndexingLevel": 4, + "plugins" : [ + "testMarkbindPlugin", + "filterTags" + ], + "pluginsContext" : { + "testMarkbindPlugin" : { + "pre": "\n\n# Markbind Plugin Pre-render", + "post": "

Node Modules Plugin Post-render

" + }, + "filterTags" : { + "tags" : ["tag--shown", "tag--shown-2"] + } + } } diff --git a/test/unit/Site.test.js b/test/unit/Site.test.js index cfc6308f40..e97cee8c2a 100644 --- a/test/unit/Site.test.js +++ b/test/unit/Site.test.js @@ -94,7 +94,7 @@ test('Site Init in existing directory generates correct assets', async () => { await Site.initSite(''); const paths = Object.keys(fs.vol.toJSON()); const originalNumFiles = Object.keys(json).length; - const expectedNumBuilt = 12; + const expectedNumBuilt = 13; expect(paths.length).toEqual(originalNumFiles + expectedNumBuilt); // _boilerplates @@ -124,6 +124,9 @@ test('Site Init in existing directory generates correct assets', async () => { expect(fs.readFileSync(path.resolve(`_markbind/layouts/default/${layoutFile}`), 'utf8')).toEqual('')); expect(fs.readFileSync(path.resolve('_markbind/layouts/default/scripts.js'), 'utf8')) .toEqual(LAYOUT_SCRIPTS_DEFAULT); + + // plugins folder + expect(fs.existsSync(path.resolve('_markbind/plugins'), 'utf8')).toEqual(true); }); test('Site Init in directory which does not exist generates correct assets', async () => { @@ -135,7 +138,7 @@ test('Site Init in directory which does not exist generates correct assets', asy await Site.initSite('newDir'); const paths = Object.keys(fs.vol.toJSON()); const originalNumFiles = Object.keys(json).length; - const expectedNumBuilt = 12; + const expectedNumBuilt = 13; expect(paths.length).toEqual(originalNumFiles + expectedNumBuilt); @@ -168,6 +171,9 @@ test('Site Init in directory which does not exist generates correct assets', asy .toEqual('')); expect(fs.readFileSync(path.resolve('newDir/_markbind/layouts/default/scripts.js'), 'utf8')) .toEqual(LAYOUT_SCRIPTS_DEFAULT); + + // plugins folder + expect(fs.existsSync(path.resolve('newDir/_markbind/plugins'), 'utf8')).toEqual(true); }); test('Site baseurls are correct for sub nested subsites', async () => {