diff --git a/docs/userGuide/usingPlugins.md b/docs/userGuide/usingPlugins.md index 28f9a3b6c9..b88dbe06da 100644 --- a/docs/userGuide/usingPlugins.md +++ b/docs/userGuide/usingPlugins.md @@ -152,22 +152,28 @@ During the `preRender` and `postRender` stages however, plugins may do custom pr source file types, as parsed from the raw Markdown, typically requiring rebuilding the site. Hence, to add custom source files to watch, you can implement the `getSources()` method. -- `getSources(content, pluginContext, frontMatter)`: Returns an array of source file paths to watch. Called **before** a Markdown file's `preRender` function is called. - - `content`: The raw Markdown of the current 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. -Example usage of `getSources` from the PlantUML plugin: +`getSources(content, pluginContext, frontMatter)`: Called _before_ a Markdown file's `preRender` function is called. +- `content`: The raw Markdown of the current 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. + +It should return an object, consisting of _at least one of the following fields_: +- `tagMap`: An array consisting of `['tag name', 'source attribute name']` key value pairs. + - MarkBind will automatically search for matching tags with the source attributes, and watch them. + - For relative file paths, _if the tag is part of some included content_ ( eg. `` tags ), it will be resolved against the included page. Otherwise, it is resolved against the page being processed. +- `sources`: An array of source file paths to watch, where relative file paths are resolved only against the page being processed. +- You can also directly return an array of source file paths to watch. ( ie. the `sources` field ) ___(deprecated)___ + +Example usage of `getSources` from the PlantUML plugin, which allows insertion of PlantUML diagrams using `` tags. +This allows files specified by the `src` attributes of `` tags to be watched: ```js { ... - getSources: (content) => { - // Add all src attributes in tags to watch list - const $ = cheerio.load(content, { xmlMode: true }); - - return $('puml').map((i, tag) => tag.attribs.src).get(); - }, + getSources: () => ({ + tagMap: [['puml', 'src']], + }) } ``` diff --git a/src/Page.js b/src/Page.js index 6e9d974911..8ee0af33f3 100644 --- a/src/Page.js +++ b/src/Page.js @@ -9,6 +9,8 @@ const Promise = require('bluebird'); const _ = {}; _.isString = require('lodash/isString'); +_.isObject = require('lodash/isObject'); +_.isArray = require('lodash/isArray'); const CyclicReferenceError = require('./lib/markbind/src/handlers/cyclicReferenceError.js'); const { ensurePosix } = require('./lib/markbind/src/utils'); @@ -16,6 +18,7 @@ const FsUtil = require('./util/fsUtil'); const logger = require('./util/logger'); const MarkBind = require('./lib/markbind/src/parser'); const md = require('./lib/markbind/src/lib/markdown-it'); +const utils = require('./lib/markbind/src/utils'); const CLI_VERSION = require('../package.json').version; @@ -864,14 +867,81 @@ Page.prototype.getPluginConfig = function () { }; /** - * Collects file sources provided by plugins for the page + * Collects file sources provided by plugins for the page for live reloading */ Page.prototype.collectPluginSources = function (content) { - Object.entries(this.plugins).forEach(([pluginName, plugin]) => { - if (plugin.getSources) { - const sources = plugin.getSources(content, this.pluginsContext[pluginName] || {}, - this.frontMatter, this.getPluginConfig()); - sources.forEach(src => this.pluginSourceFiles.add(path.resolve(ensurePosix(src)))); + const self = this; + + Object.entries(self.plugins).forEach(([pluginName, plugin]) => { + if (!plugin.getSources) { + return; + } + + const result = plugin.getSources(content, self.pluginsContext[pluginName] || {}, + self.frontMatter, self.getPluginConfig()); + + let pageContextSources; + let domTagSourcesMap; + + if (_.isArray(result)) { + pageContextSources = result; + } else if (_.isObject(result)) { + pageContextSources = result.sources; + domTagSourcesMap = result.tagMap; + } else { + logger.warn(`${pluginName} returned unsupported type for ${self.sourcePath}`); + return; + } + + if (pageContextSources) { + pageContextSources.forEach((src) => { + if (src === undefined || src === '' || utils.isUrl(src)) { + return; + } else if (utils.isAbsolutePath(src)) { + self.pluginSourceFiles.add(path.resolve(src)); + return; + } + + // Resolve relative paths from the current page source + const originalSrcFolder = path.dirname(self.sourcePath); + const resolvedResourcePath = path.resolve(originalSrcFolder, src); + + self.pluginSourceFiles.add(resolvedResourcePath); + }); + } + + if (domTagSourcesMap) { + const $ = cheerio.load(content, { xmlMode: true }); + + domTagSourcesMap.forEach(([tagName, attrName]) => { + if (!_.isString(tagName) || !_.isString(attrName)) { + logger.warn(`Invalid tag or attribute provided in tagMap by ${pluginName} plugin.`); + return; + } + + const selector = `${tagName}[${attrName}]`; + $(selector).each((i, el) => { + const elem = $(el); + + let src = elem.attr(attrName); + + src = ensurePosix(src); + if (src === '' || utils.isUrl(src)) { + return; + } else if (utils.isAbsolutePath(src)) { + self.pluginSourceFiles.add(path.resolve(src)); + return; + } + + // Resolve relative paths from the include page source, or current page source otherwise + const firstParent = elem.closest('div[data-included-from], span[data-included-from]'); + const originalSrc = firstParent.attr('data-included-from') || self.sourcePath; + const originalSrcFolder = path.dirname(originalSrc); + const resolvedResourcePath = path.resolve(originalSrcFolder, src); + + self.pluginSourceFiles.add(resolvedResourcePath); + }); + }); } }); diff --git a/src/plugins/default/markbind-plugin-plantuml.js b/src/plugins/default/markbind-plugin-plantuml.js index 67a1ea61bf..a7d87b7a1f 100644 --- a/src/plugins/default/markbind-plugin-plantuml.js +++ b/src/plugins/default/markbind-plugin-plantuml.js @@ -110,10 +110,7 @@ module.exports = { return $.html(); }, - getSources: (content) => { - // Add all src attributes in tags to watch list - const $ = cheerio.load(content, { xmlMode: true }); - - return $('puml').map((i, tag) => tag.attribs.src).get(); - }, + getSources: () => ({ + tagMap: [['puml', 'src']], + }), }; diff --git a/test/unit/Page.test.js b/test/unit/Page.test.js index 8404c454b3..6838187d9a 100644 --- a/test/unit/Page.test.js +++ b/test/unit/Page.test.js @@ -1,3 +1,8 @@ +const path = require('path'); +const { + COLLECT_PLUGIN_SOURCES, + COLLECT_PLUGIN_TEST_PLUGIN, +} = require('./utils/pageData'); const Page = require('../../src/Page'); test('Page#collectIncludedFiles collects included files from 1 dependency object', () => { @@ -20,3 +25,26 @@ test('Page#collectIncludedFiles collects nothing', () => { expect(page.includedFiles).toEqual(new Set()); }); + +test('Page#collectPluginSources collects correct sources', () => { + const page = new Page({ + sourcePath: path.resolve('/root/index.md'), + plugins: { testPlugin: COLLECT_PLUGIN_TEST_PLUGIN }, + pluginsContext: { testPlugin: {} }, + }); + page.collectPluginSources(COLLECT_PLUGIN_SOURCES); + + const EXPECTED_SOURCE_FILES = new Set([ + // source files from { sources: [...] } + path.resolve('/root/paths/here/should/be/resolved/relative/to/processed/page.cpp'), + path.resolve('/except/absolute/sources.c'), + // source files found from provided { tagMap: [[tag, srcAttr], ...] } + path.resolve('/root/images/sample1.png'), + path.resolve('/root/subdir/images/sample2.png'), + path.resolve('/root/subdir2/sample3.png'), + path.resolve('/root/images/sample4.png'), + path.resolve('/absolute/paths/should/not/be/rewritten.png'), + ]); + + expect(page.pluginSourceFiles).toEqual(EXPECTED_SOURCE_FILES); +}); diff --git a/test/unit/utils/pageData.js b/test/unit/utils/pageData.js new file mode 100644 index 0000000000..305e579ece --- /dev/null +++ b/test/unit/utils/pageData.js @@ -0,0 +1,37 @@ +module.exports.COLLECT_PLUGIN_SOURCES = ` + + Lorem ipsum dolor sit amet, Ut enim ad minim veniam, + + consectetur adipiscing elit, + + + sed do eiusmod tempor incididunt ut labore + + + et dolore + + + magna aliqua. + + + + + + quis nostrud exercitation ut + Lorem ipsum + + ullamco laboris nisi + + aliquip ex ea commodo consequat. + +`; + +module.exports.COLLECT_PLUGIN_TEST_PLUGIN = { + getSources: () => ({ + tagMap: [['custom-tag-one', 'src'], ['custom-tag-two', 'srcattr']], + sources: [ + 'paths/here/should/be/resolved/relative/to/processed/page.cpp', + '/except/absolute/sources.c', + ], + }), +};