diff --git a/src/Page.js b/src/Page.js index a8275e5b09..2d27226d89 100644 --- a/src/Page.js +++ b/src/Page.js @@ -10,6 +10,7 @@ const _ = {}; _.isString = require('lodash/isString'); _.isObject = require('lodash/isObject'); _.isArray = require('lodash/isArray'); +_.isFunction = require('lodash/isFunction'); const CyclicReferenceError = require('./lib/markbind/src/handlers/cyclicReferenceError.js'); const { ensurePosix } = require('./lib/markbind/src/utils'); @@ -141,10 +142,10 @@ class Page { */ this.globalOverride = pageConfig.globalOverride; /** - * Array of plugins used in this page. - * @type {Array} + * Plugin names and the corresponding plugins used in this page. + * @type {Object} */ - this.plugins = pageConfig.plugins; + this.plugins = pageConfig.plugins || {}; /** * @type {Object>} */ @@ -521,6 +522,7 @@ class Page { collectFrontMatter(includedPage) { const $ = cheerio.load(includedPage); const frontMatter = $('frontmatter'); + Object.keys(this.frontMatter).forEach(k => delete this.frontMatter[k]); if (frontMatter.text().trim()) { // Retrieves the front matter from either the first frontmatter element // or from a frontmatter element that includes from another file @@ -529,15 +531,16 @@ class Page { ? frontMatter.find('div')[0].children[0].data : frontMatter[0].children[0].data; const frontMatterWrapped = `${FRONT_MATTER_FENCE}\n${frontMatterData}\n${FRONT_MATTER_FENCE}`; + // Parse front matter data const parsedData = fm(frontMatterWrapped); - this.frontMatter = { ...parsedData.attributes }; - this.frontMatter.src = this.src; + parsedData.attributes.src = this.src; // Title specified in site.json will override title specified in front matter - this.frontMatter.title = (this.title || this.frontMatter.title || ''); + parsedData.attributes.title = (this.title || parsedData.attributes.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); - this.frontMatter = { ...this.frontMatter, ...this.globalOverride, ...this.frontmatterOverride }; + parsedData.attributes.layout = (this.layout || parsedData.attributes.layout || LAYOUT_DEFAULT_NAME); + + Object.assign(this.frontMatter, parsedData.attributes, this.globalOverride, this.frontmatterOverride); } else { // Page is addressable but no front matter specified const defaultAttributes = { @@ -545,11 +548,7 @@ class Page { title: this.title || '', layout: LAYOUT_DEFAULT_NAME, }; - this.frontMatter = { - ...defaultAttributes, - ...this.globalOverride, - ...this.frontmatterOverride, - }; + Object.assign(this.frontMatter, defaultAttributes, this.globalOverride, this.frontmatterOverride); } this.title = this.frontMatter.title; } @@ -942,8 +941,12 @@ class Page { generate(builtFiles) { this.includedFiles = new Set([this.sourcePath]); this.headerIdMap = {}; // Reset for live reload + + const pluginConfig = this.getPluginConfig(); + const markbinder = new MarkBind({ variablePreprocessor: this.variablePreprocessor, + ...this.preparePluginHooks(pluginConfig), }); /** * @type {FileConfig} @@ -963,15 +966,15 @@ class Page { .then(result => this.generateExpressiveLayout(result, fileConfig, markbinder)) .then(result => Page.removePageHeaderAndFooter(result)) .then(result => Page.addContentWrapper(result)) - .then(result => this.collectPluginSources(result)) - .then(result => this.preRender(result)) + .then(result => this.collectPluginSources(result, pluginConfig)) + .then(result => this.preRender(result, pluginConfig)) .then(result => this.insertSiteNav((result))) .then(result => this.insertHeaderFile(result, fileConfig)) .then(result => this.insertFooterFile(result)) .then(result => Page.insertTemporaryStyles(result)) .then(result => markbinder.resolveBaseUrl(result, fileConfig)) .then(result => markbinder.render(result, this.sourcePath, fileConfig)) - .then(result => this.postRender(result)) + .then(result => this.postRender(result, pluginConfig)) .then(result => this.collectPluginsAssets(result)) .then(result => markbinder.processDynamicResources(this.sourcePath, result)) .then(result => MarkBind.unwrapIncludeSrc(result)) @@ -1033,7 +1036,7 @@ class Page { * Retrieves page config for plugins * @return {PluginConfig} pluginConfig */ - getPluginConfig() { + getPluginConfig(override = {}) { return { headingIndexingLevel: this.headingIndexingLevel, enableSearch: this.enableSearch, @@ -1042,13 +1045,27 @@ class Page { sourcePath: this.sourcePath, includedFiles: this.includedFiles, resultPath: this.resultPath, + ...override, }; } + preparePluginHooks(pluginConfig) { + // Prepare curried plugin node hooks + const preRenderNodeHooks = Object.entries(this.plugins) + .filter(([, plugin]) => _.isFunction(plugin.preRenderNode)) + .map(([name, plugin]) => node => plugin.preRenderNode(node, this.pluginsContext[name] || {}, + this.frontMatter, pluginConfig)); + const postRenderNodeHooks = Object.entries(this.plugins) + .filter(([, plugin]) => _.isFunction(plugin.postRenderNode)) + .map(([name, plugin]) => node => plugin.postRenderNode(node, this.pluginsContext[name] || {}, + this.frontMatter, pluginConfig)); + return { preRenderNodeHooks, postRenderNodeHooks }; + } + /** * Collects file sources provided by plugins for the page for live reloading */ - collectPluginSources(content) { + collectPluginSources(content, pluginConfig) { const self = this; Object.entries(self.plugins) @@ -1058,7 +1075,7 @@ class Page { } const result = plugin.getSources(content, self.pluginsContext[pluginName] || {}, - self.frontMatter, self.getPluginConfig()); + self.frontMatter, pluginConfig); let pageContextSources; let domTagSourcesMap; @@ -1132,12 +1149,12 @@ class Page { /** * Entry point for plugin pre-render */ - preRender(content) { + preRender(content, pluginConfig) { let preRenderedContent = content; Object.entries(this.plugins).forEach(([pluginName, plugin]) => { if (plugin.preRender) { preRenderedContent = plugin.preRender(preRenderedContent, this.pluginsContext[pluginName] || {}, - this.frontMatter, this.getPluginConfig()); + this.frontMatter, pluginConfig); } }); return preRenderedContent; @@ -1146,12 +1163,12 @@ class Page { /** * Entry point for plugin post-render */ - postRender(content) { + postRender(content, pluginConfig) { let postRenderedContent = content; Object.entries(this.plugins).forEach(([pluginName, plugin]) => { if (plugin.postRender) { postRenderedContent = plugin.postRender(postRenderedContent, this.pluginsContext[pluginName] || {}, - this.frontMatter, this.getPluginConfig()); + this.frontMatter, pluginConfig); } }); return postRenderedContent; @@ -1253,12 +1270,19 @@ class Page { return resolve(); } builtFiles.add(resultPath); + + const pluginConfig = this.getPluginConfig({ + sourcePath: dependency.asIfTo, + resultPath, + }); + /* * We create a local instance of Markbind for an empty dynamicIncludeSrc * so that we only recursively rebuild the file's included content */ const markbinder = new MarkBind({ variablePreprocessor: this.variablePreprocessor, + ...this.preparePluginHooks(pluginConfig), }); return fs.readFileAsync(dependency.to, 'utf-8') .then(result => markbinder.includeFile(dependency.to, result, { @@ -1267,8 +1291,8 @@ class Page { cwf: file, })) .then(result => Page.removeFrontMatter(result)) - .then(result => this.collectPluginSources(result)) - .then(result => this.preRender(result)) + .then(result => this.collectPluginSources(result, pluginConfig)) + .then(result => this.preRender(result, pluginConfig)) .then(result => markbinder.resolveBaseUrl(result, { baseUrlMap: this.baseUrlMap, rootPath: this.rootPath, @@ -1278,7 +1302,7 @@ class Page { rootPath: this.rootPath, headerIdMap: {}, })) - .then(result => this.postRender(result)) + .then(result => this.postRender(result, pluginConfig)) .then(result => this.collectPluginsAssets(result)) .then(result => markbinder.processDynamicResources(file, result)) .then(result => MarkBind.unwrapIncludeSrc(result)) diff --git a/src/Site.js b/src/Site.js index c39a87dcf2..4edf614819 100644 --- a/src/Site.js +++ b/src/Site.js @@ -262,7 +262,7 @@ class Site { disableHtmlBeautify: this.siteConfig.disableHtmlBeautify, globalOverride: this.siteConfig.globalOverride, pageTemplate: this.pageTemplate, - plugins: this.plugins || {}, + plugins: this.plugins, rootPath: this.rootPath, enableSearch: this.siteConfig.enableSearch, searchable: this.siteConfig.enableSearch && config.searchable, @@ -270,7 +270,7 @@ class Site { layoutsAssetPath: path.relative(path.dirname(resultPath), path.join(this.siteAssetsDestPath, LAYOUT_SITE_FOLDER_NAME)), layout: config.layout, - title: config.title || '', + title: config.title, titlePrefix: this.siteConfig.titlePrefix, headingIndexingLevel: this.siteConfig.headingIndexingLevel, variablePreprocessor: this.variablePreprocessor, diff --git a/src/lib/markbind/src/parser.js b/src/lib/markbind/src/parser.js index 30a83b3303..97edc0c164 100644 --- a/src/lib/markbind/src/parser.js +++ b/src/lib/markbind/src/parser.js @@ -29,11 +29,11 @@ const { } = require('./constants'); class Parser { - constructor(config) { - this.variablePreprocessor = config.variablePreprocessor; - this.dynamicIncludeSrc = []; - this.staticIncludeSrc = []; - this.missingIncludeSrc = []; + constructor(options) { + this.variablePreprocessor = options.variablePreprocessor; + this.preRenderNodeHooks = options.preRenderNodeHooks || []; + this.postRenderNodeHooks = options.postRenderNodeHooks || []; + this.resetIncludeSrces(); } getDynamicIncludeSrc() { @@ -48,6 +48,12 @@ class Parser { return _.clone(this.missingIncludeSrc); } + resetIncludeSrces() { + this.dynamicIncludeSrc = []; + this.staticIncludeSrc = []; + this.missingIncludeSrc = []; + } + processDynamicResources(context, html) { const $ = cheerio.load(html, { xmlMode: false, @@ -177,14 +183,12 @@ class Parser { }); } - componentParser.postParseComponents(node); - // If a fixed header is applied to this page, generate dummy spans as anchor points if (config.fixedHeader && isHeadingTag && node.attribs.id) { cheerio(node).append(cheerio.parseHTML(``)); } - return node; + return componentParser.postParseComponents(node, this.postRenderNodeHooks); } _trimNodes(node) { diff --git a/src/lib/markbind/src/parsers/componentParser.js b/src/lib/markbind/src/parsers/componentParser.js index e4665b59f3..7e03f65f68 100644 --- a/src/lib/markbind/src/parsers/componentParser.js +++ b/src/lib/markbind/src/parsers/componentParser.js @@ -4,6 +4,7 @@ const _ = {}; _.has = require('lodash/has'); const md = require('../lib/markdown-it'); +const utils = require('../utils'); const logger = require('../../../../util/logger'); cheerio.prototype.options.xmlMode = true; // Enable xml mode for self-closing tag @@ -471,17 +472,26 @@ function parseComponents(node) { } } -function postParseComponents(node) { +function postParseComponents(node, postRenderNodeHooks) { try { - switch (node.name) { + let element = node; + + switch (element.name) { case 'panel': - _assignPanelId(node); + _assignPanelId(element); break; default: break; } + + postRenderNodeHooks.forEach((hook) => { + element = hook(element); + }); + + return element; } catch (error) { logger.error(error); + return utils.createErrorNode(node, error); } } diff --git a/src/lib/markbind/src/preprocessors/componentPreprocessor.js b/src/lib/markbind/src/preprocessors/componentPreprocessor.js index a4b7a749fc..47fdfb9b33 100644 --- a/src/lib/markbind/src/preprocessors/componentPreprocessor.js +++ b/src/lib/markbind/src/preprocessors/componentPreprocessor.js @@ -343,19 +343,21 @@ function _preprocessBody(node) { function preProcessComponent(node, context, config, parser) { - const element = node; + let element = node; _preProcessAllComponents(element, context); switch (element.name) { case 'panel': - return _preProcessPanel(element, context, config, parser); + element = _preProcessPanel(element, context, config, parser); + break; case 'variable': return _preprocessVariables(); case 'import': return _preprocessImports(node, parser); case 'include': - return _preprocessInclude(element, context, config, parser); + element = _preprocessInclude(element, context, config, parser); + break; case 'body': _preprocessBody(element); // eslint-disable-next-line no-fallthrough @@ -364,8 +366,13 @@ function preProcessComponent(node, context, config, parser) { if (element.children && element.children.length > 0) { element.children = element.children.map(e => preProcessComponent(e, context, config, parser)); } - return element; } + + parser.preRenderNodeHooks.forEach((hook) => { + element = hook(element); + }); + + return element; } diff --git a/src/lib/markbind/src/utils/index.js b/src/lib/markbind/src/utils/index.js index b46bebf05f..c166e5f341 100644 --- a/src/lib/markbind/src/utils/index.js +++ b/src/lib/markbind/src/utils/index.js @@ -87,7 +87,7 @@ module.exports = { getTextContent(element) { const elements = element.children; if (!elements || !elements.length) { - return undefined; + return ''; } const elementStack = elements.slice(); @@ -103,6 +103,6 @@ module.exports = { } } - return text.join('').trim(); + return text.join(''); }, }; diff --git a/src/plugins/codeBlockCopyButtons.js b/src/plugins/codeBlockCopyButtons.js index 7c11b169d1..4173794835 100644 --- a/src/plugins/codeBlockCopyButtons.js +++ b/src/plugins/codeBlockCopyButtons.js @@ -9,13 +9,13 @@ const COPY_ICON = ' + const buttonHtml = `
${COPY_ICON} ${COPY_TO_CLIPBOARD}
`; - return html; + return cheerio.parseHTML(buttonHtml)[0]; } @@ -44,13 +44,10 @@ const copyCodeBlockScript = `