diff --git a/docs/userGuide/syntax/variables.mbdf b/docs/userGuide/syntax/variables.mbdf index d768635983..fcbcb4b27a 100644 --- a/docs/userGuide/syntax/variables.mbdf +++ b/docs/userGuide/syntax/variables.mbdf @@ -103,6 +103,72 @@ You can specify a default value for a variable, which is displayed when the vari Note: These variables will not be applied to [`` files]({{ baseUrl }}/userGuide/reusingContents.html#the-include-tag). Additionally, global variables (`_markbind/variables.md`) will take precedence over any page variables. *See also: [Specifying Variables in an ``]({{ baseUrl }}/userGuide/reusingContents.html#specifying-variables-in-an-include)*. +### Importing Variables + +**You can access [page variables](#page-variables) from another page by importing them.** + + +{{ icon_example }} Importing specific variables from `person.md` into `coverpage.md`: +`person.md`: +```html +123 Sun Avenue +Mark +123456789 +``` + +`coverpage.md`: + +```html + +``` + +will allow you to access the variables as per normal: {{address}}, {{name}}, {{phone}}. + +--- + +**When importing all variables, you should attach a _namespace_** to the imported variables using an `as` attributes. + + +{{ icon_example }}: +`coverpage.md`: +```html + +``` + +| Detail | How to access +| :------------- |:------------- +| address | {{details.address}} +| name | {{details.name}} +| phone | {{details.phone}} + +This way, ***all*** variables in `page.md` are accessible via {{details.<variable_name>}}. + +Note that in this case, `details` is treated as the variable name and so is subject to the same rules as other variables, such as global variables taking precedence, and multiple imports to the same namespace being impossible: + +```html + + +``` + +In this case, all the variables in `title.md` are not accessible, as they are overwritten with the variables from `index.md`. + + + +Note that global variables (`_markbind/variables.md`) and [page variables](#page-variables) will take precedence over any imported variables. + +While you can mix the two syntaxes for importing page variables, it may get confusing: +```html + +``` + +This may seem like it will import *only* `address` and `name` from `page.md` and storing them in the namespace `details`. + +However, this is a combination of *both* syntaxes above, and thus this will allow you to: + +- access `address` and `name` (but NOT `phone`) with {{address}} and {{name}} +- access `address`, `name`, and `phone` with {{details.address}}, {{details.name}}, and {{details.phone}} + + ### Variables: Tips and Tricks diff --git a/src/Site.js b/src/Site.js index 30eb5619d7..45d0790909 100644 --- a/src/Site.js +++ b/src/Site.js @@ -8,6 +8,7 @@ const path = require('path'); const Promise = require('bluebird'); const ProgressBar = require('progress'); const walkSync = require('walk-sync'); +const MarkBind = require('./lib/markbind/src/parser'); const _ = {}; _.difference = require('lodash/difference'); @@ -740,6 +741,7 @@ Site.prototype._rebuildAffectedSourceFiles = function (filePaths) { const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths]; const uniquePaths = _.uniq(filePathArray); logger.info('Rebuilding affected source files'); + MarkBind.resetVariables(); return new Promise((resolve, reject) => { this.regenerateAffectedPages(uniquePaths) .then(() => fs.removeAsync(this.tempPath)) diff --git a/src/lib/markbind/src/parser.js b/src/lib/markbind/src/parser.js index 27fd725657..72340f1c39 100644 --- a/src/lib/markbind/src/parser.js +++ b/src/lib/markbind/src/parser.js @@ -27,6 +27,14 @@ const ATTRIB_CWF = 'cwf'; const BOILERPLATE_FOLDER_NAME = '_markbind/boilerplates'; +/* Imported global variables will be assigned a namespace. + * A prefix is appended to reduce clashes with other variables in the page. + */ +const IMPORTED_VARIABLE_PREFIX = '$__MARKBIND__'; +const VARIABLE_LOOKUP = new Map(); +const FILE_ALIASES = new Map(); +const PROCESSED_INNER_VARIABLES = new Set(); + /* * Utils */ @@ -87,6 +95,12 @@ function Parser(options) { this.missingIncludeSrc = []; } +Parser.resetVariables = function () { + VARIABLE_LOOKUP.clear(); + FILE_ALIASES.clear(); + PROCESSED_INNER_VARIABLES.clear(); +}; + /** * Extract variables from an include element * @param includeElement include element to extract variables from @@ -123,6 +137,22 @@ function extractIncludeVariables(includeElement, contextVariables) { return includedVariables; } +/** + * Returns an object containing the imported variables for specified file + * @param file file name to get the imported variables for + */ +function getImportedVariableMap(file) { + const innerVariables = {}; + FILE_ALIASES.get(file).forEach((actualPath, alias) => { + innerVariables[alias] = {}; + const variables = VARIABLE_LOOKUP.get(actualPath); + variables.forEach((value, name) => { + innerVariables[alias][name] = value; + }); + }); + return innerVariables; +} + /** * Extract page variables from a page * @param filename for error printing @@ -133,6 +163,29 @@ function extractIncludeVariables(includeElement, contextVariables) { function extractPageVariables(fileName, data, userDefinedVariables, includedVariables) { const $ = cheerio.load(data); const pageVariables = { }; + VARIABLE_LOOKUP.set(fileName, new Map()); + /** + * ed variables have not been processed yet, we replace such variables with itself first. + */ + const importedVariables = {}; + $('import[from]').each((index, element) => { + const variableNames = Object.keys(element.attribs) + .filter(name => name !== 'from' && name !== 'as'); + // If no namespace is provided, we use the smallest name as one... + const largestName = variableNames.sort()[0]; + // ... and prepend it with $__MARKBIND__ to reduce collisions. + const generatedAlias = IMPORTED_VARIABLE_PREFIX + largestName; + const hasAlias = _.hasIn(element.attribs, 'as'); + const alias = hasAlias ? element.attribs.as : generatedAlias; + importedVariables[alias] = new Proxy({}, { + get(obj, prop) { + return `{{${alias}.${prop}}}`; + }, + }); + variableNames.forEach((name) => { + importedVariables[name] = `{{${alias}.${name}}}`; + }); + }); $('variable').each(function () { const variableElement = $(this); const variableName = variableElement.attr('name'); @@ -142,12 +195,18 @@ function extractPageVariables(fileName, data, userDefinedVariables, includedVari return; } if (!pageVariables[variableName]) { - pageVariables[variableName] - = nunjucks.renderString(md.renderInline(variableElement.html()), - { ...pageVariables, ...userDefinedVariables, ...includedVariables }); + const variableValue + = nunjucks.renderString( + md.renderInline(variableElement.html()), + { + ...importedVariables, ...pageVariables, ...userDefinedVariables, ...includedVariables, + }, + ); + pageVariables[variableName] = variableValue; + VARIABLE_LOOKUP.get(fileName).set(variableName, variableValue); } }); - return pageVariables; + return { ...importedVariables, ...pageVariables }; } Parser.prototype.getDynamicIncludeSrc = function () { @@ -181,6 +240,79 @@ Parser.prototype._preprocessThumbnails = function (element) { return element; }; +Parser.prototype._renderIncludeFile = function (filePath, element, context, config, asIfAt = filePath) { + try { + this._fileCache[filePath] = this._fileCache[filePath] + ? this._fileCache[filePath] : fs.readFileSync(filePath, 'utf8'); + } catch (e) { + // Read file fail + const missingReferenceErrorMessage = `Missing reference in: ${element.attribs[ATTRIB_CWF]}`; + e.message += `\n${missingReferenceErrorMessage}`; + this._onError(e); + return createErrorNode(element, e); + } + + const fileContent = this._fileCache[filePath]; // cache the file contents to save some I/O + const { parent, relative } + = calculateNewBaseUrls(asIfAt, config.rootPath, config.baseUrlMap); + const userDefinedVariables = config.userDefinedVariablesMap[path.resolve(parent, relative)]; + + // Extract included variables from the PARENT file + const includeVariables = extractIncludeVariables(element, context.variables); + + // Extract page variables from the CHILD file + const pageVariables = extractPageVariables(asIfAt, fileContent, + userDefinedVariables, includeVariables); + + const content = nunjucks.renderString(fileContent, + { ...pageVariables, ...includeVariables, ...userDefinedVariables }, + { path: filePath }); + + const childContext = _.cloneDeep(context); + childContext.cwf = asIfAt; + childContext.variables = includeVariables; + + return { content, childContext, userDefinedVariables }; +}; + +Parser.prototype._extractInnerVariables = function (content, context, config) { + const { cwf } = context; + const $ = cheerio.load(content, { + xmlMode: false, + decodeEntities: false, + }); + const aliases = new Map(); + FILE_ALIASES.set(cwf, aliases); + $('import[from]').each((index, element) => { + const filePath = path.resolve(path.dirname(cwf), element.attribs.from); + const variableNames = Object.keys(element.attribs) + .filter(name => name !== 'from' && name !== 'as'); + // If no namespace is provided, we use the smallest name as one + const largestName = variableNames.sort()[0]; + // ... and prepend it with $__MARKBIND__ to reduce collisions. + const generatedAlias = IMPORTED_VARIABLE_PREFIX + largestName; + const alias = _.hasIn(element.attribs, 'as') + ? element.attribs.as + : generatedAlias; + + aliases.set(alias, filePath); + this.staticIncludeSrc.push({ from: context.cwf, to: filePath }); + + // Render inner file content + const { content: renderedContent, childContext, userDefinedVariables } + = this._renderIncludeFile(filePath, element, context, config); + + if (!PROCESSED_INNER_VARIABLES.has(filePath)) { + PROCESSED_INNER_VARIABLES.add(filePath); + this._extractInnerVariables(renderedContent, childContext, config); + } + const innerVariables = getImportedVariableMap(filePath); + VARIABLE_LOOKUP.get(filePath).forEach((value, variableName, map) => { + map.set(variableName, nunjucks.renderString(value, { ...userDefinedVariables, ...innerVariables })); + }); + }); +}; + Parser.prototype._preprocess = function (node, context, config) { const element = node; const self = this; @@ -264,39 +396,23 @@ Parser.prototype._preprocess = function (node, context, config) { this.staticIncludeSrc.push({ from: context.cwf, to: actualFilePath }); - try { - self._fileCache[actualFilePath] = self._fileCache[actualFilePath] - ? self._fileCache[actualFilePath] : fs.readFileSync(actualFilePath, 'utf8'); - } catch (e) { - // Read file fail - const missingReferenceErrorMessage = `Missing reference in: ${element.attribs[ATTRIB_CWF]}`; - e.message += `\n${missingReferenceErrorMessage}`; - this._onError(e); - return createErrorNode(element, e); - } - const isIncludeSrcMd = utils.isMarkdownFileExt(utils.getExt(filePath)); if (isIncludeSrcMd && context.source === 'html') { // HTML include markdown, use special tag to indicate markdown code. element.name = 'markdown'; } + const { content, childContext, userDefinedVariables } + = this._renderIncludeFile(actualFilePath, element, context, config, filePath); + childContext.source = isIncludeSrcMd ? 'md' : 'html'; + childContext.callStack.push(context.cwf); - let fileContent = self._fileCache[actualFilePath]; // cache the file contents to save some I/O - const { parent, relative } = calculateNewBaseUrls(filePath, config.rootPath, config.baseUrlMap); - const userDefinedVariables = config.userDefinedVariablesMap[path.resolve(parent, relative)]; - - // Extract included variables from the PARENT file - const includeVariables = extractIncludeVariables(element, context.variables); - - // Extract page variables from the CHILD file - const pageVariables = extractPageVariables(element.attribs.src, fileContent, - userDefinedVariables, includeVariables); - - // Render inner file content - fileContent = nunjucks.renderString(fileContent, - { ...pageVariables, ...includeVariables, ...userDefinedVariables }, - { path: actualFilePath }); + if (!PROCESSED_INNER_VARIABLES.has(filePath)) { + PROCESSED_INNER_VARIABLES.add(filePath); + this._extractInnerVariables(content, childContext, config); + } + const innerVariables = getImportedVariableMap(filePath); + const fileContent = nunjucks.renderString(content, { ...userDefinedVariables, ...innerVariables }); // Delete variable attributes in include Object.keys(element.attribs).forEach((attribute) => { @@ -365,14 +481,6 @@ Parser.prototype._preprocess = function (node, context, config) { ); } - // The element's children are in the new context - // Process with new context - const childContext = _.cloneDeep(context); - childContext.cwf = filePath; - childContext.source = isIncludeSrcMd ? 'md' : 'html'; - childContext.callStack.push(context.cwf); - childContext.variables = includeVariables; - if (element.children && element.children.length > 0) { if (childContext.callStack.length > CyclicReferenceError.MAX_RECURSIVE_DEPTH) { const error = new CyclicReferenceError(childContext.callStack); @@ -388,7 +496,7 @@ Parser.prototype._preprocess = function (node, context, config) { element.attribs.src = filePath; this.dynamicIncludeSrc.push({ from: context.cwf, to: actualFilePath, asIfTo: filePath }); return element; - } else if (element.name === 'variable') { + } else if (element.name === 'variable' || element.name === 'import') { return createEmptyNode(); } else { if (element.name === 'body') { @@ -596,10 +704,14 @@ Parser.prototype.includeFile = function (file, config) { } const { parent, relative } = calculateNewBaseUrls(file, config.rootPath, config.baseUrlMap); const userDefinedVariables = config.userDefinedVariablesMap[path.resolve(parent, relative)]; - const pageVariables = extractPageVariables(path.basename(file), data, userDefinedVariables, {}); - const fileContent = nunjucks.renderString(data, - { ...pageVariables, ...userDefinedVariables }, - { path: actualFilePath }); + const pageVariables = extractPageVariables(file, data, userDefinedVariables, {}); + + let fileContent = nunjucks.renderString(data, + { ...pageVariables, ...userDefinedVariables }, + { path: actualFilePath }); + this._extractInnerVariables(fileContent, context, config); + const innerVariables = getImportedVariableMap(context.cwf); + fileContent = nunjucks.renderString(fileContent, { ...userDefinedVariables, ...innerVariables }); const fileExt = utils.getExt(file); if (utils.isMarkdownFileExt(fileExt)) { context.source = 'md'; diff --git a/test/functional/test_site/expected/siteData.json b/test/functional/test_site/expected/siteData.json index 260ac3336e..b0da98f1f1 100644 --- a/test/functional/test_site/expected/siteData.json +++ b/test/functional/test_site/expected/siteData.json @@ -299,6 +299,28 @@ "layout": "default", "globalOverrideProperty": "Overridden by global override", "globalAndFrontMatterOverrideProperty": "Overridden by global override" + }, + { + "headings": { + "trying-to-access-a-page-variable": "Trying to access a page variable:", + "trying-to-access-an-imported-variable-via-namespace": "Trying to access an imported variable via namespace:" + }, + "src": "testImportVariables.md", + "title": "Imported Variables Test", + "layout": "default", + "globalOverrideProperty": "Overridden by global override", + "globalAndFrontMatterOverrideProperty": "Overridden by global override" + }, + { + "headings": { + "below-panel-is-working": "Below panel is working", + "below-should-be-a-panel-but-is-now-an-error-uncomment-it-to-see-the-error": "Below should be a panel, but is now an error. Uncomment it to see the error." + }, + "src": "testPanelsWithImportedVariables.md", + "title": "Panels with Imported Variables Test", + "layout": "default", + "globalOverrideProperty": "Overridden by global override", + "globalAndFrontMatterOverrideProperty": "Overridden by global override" } ] } diff --git a/test/functional/test_site/expected/testImportVariables._include_.html b/test/functional/test_site/expected/testImportVariables._include_.html new file mode 100644 index 0000000000..3b8cff9686 --- /dev/null +++ b/test/functional/test_site/expected/testImportVariables._include_.html @@ -0,0 +1,13 @@ +
+

Making sure the issue here https://github.com/MarkBind/markbind/commit/48b57a18a8bfd68101b163908da4a0541756364a is fixed.

+
+

Test import variables from src specified via variable: This variable comes from variablesToImport.md

+

Test import variables that itself imports other variables:

+

Trying to access a page variable:

+

There should be something red below:

+
This is a page variable from variablestoimport.md
+Something should have appeared above in red. +

Trying to access an imported variable via namespace:

+

There should be something blue below:

+
This is a deeply imported variable
+Something should have appeared above in blue. \ No newline at end of file diff --git a/test/functional/test_site/expected/testImportVariables.html b/test/functional/test_site/expected/testImportVariables.html new file mode 100644 index 0000000000..cb11737013 --- /dev/null +++ b/test/functional/test_site/expected/testImportVariables.html @@ -0,0 +1,67 @@ + + + + + + + + + + Imported Variables Test + + + + + + + + + + + + + + + +
+
+
+
+

Making sure the issue here https://github.com/MarkBind/markbind/commit/48b57a18a8bfd68101b163908da4a0541756364a is fixed.

+
+

Test import variables from src specified via variable: This variable comes from variablesToImport.md

+

Test import variables that itself imports other variables:

+

Trying to access a page variable:

+

There should be something red below:

+
This is a page variable from variablestoimport.md
+ Something should have appeared above in red. +

Trying to access an imported variable via namespace:

+

There should be something blue below:

+
This is a deeply imported variable
+ Something should have appeared above in blue. +
+
+
+
+ Default footer +
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/functional/test_site/expected/testPanelsWithImportedVariables.html b/test/functional/test_site/expected/testPanelsWithImportedVariables.html new file mode 100644 index 0000000000..bca7b9f017 --- /dev/null +++ b/test/functional/test_site/expected/testPanelsWithImportedVariables.html @@ -0,0 +1,62 @@ + + + + + + + + + + Panels with Imported Variables Test + + + + + + + + + + + + + + + +
+
+
+

Refer to this comment: https://github.com/MarkBind/markbind/pull/751#issuecomment-469670640

+
+ Title +

Below panel is working

+

+ +

+

Below should be a panel, but is now an error. Uncomment it to see the error.

+
+
+
+
+ Default footer +
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/test/functional/test_site/moreVariablesToImport.md b/test/functional/test_site/moreVariablesToImport.md new file mode 100644 index 0000000000..30e3d815b0 --- /dev/null +++ b/test/functional/test_site/moreVariablesToImport.md @@ -0,0 +1,8 @@ +This page contains some variables that are being imported in other files. +There should be only VARIABLES START and VARIABLES END in red: + +
+VARIABLES START +This is a deeply imported variable +VARIABLES END +
\ No newline at end of file diff --git a/test/functional/test_site/panelSrcs.md b/test/functional/test_site/panelSrcs.md new file mode 100644 index 0000000000..f8507a582f --- /dev/null +++ b/test/functional/test_site/panelSrcs.md @@ -0,0 +1,10 @@ +This page contains some variables that are being imported in other files. +There should be only VARIABLES START and VARIABLES END in red: + +
+VARIABLES START +Title +testImportVariables.md +testImportVariables +VARIABLES END +
\ No newline at end of file diff --git a/test/functional/test_site/site.json b/test/functional/test_site/site.json index 91c2460049..6896ab6cd7 100644 --- a/test/functional/test_site/site.json +++ b/test/functional/test_site/site.json @@ -63,6 +63,14 @@ { "src": "testPlantUML.md", "title": "PlantUML Test" + }, + { + "src": "testImportVariables.md", + "title": "Imported Variables Test" + }, + { + "src": "testPanelsWithImportedVariables.md", + "title": "Panels with Imported Variables Test" } ], "ignore": [ diff --git a/test/functional/test_site/testImportVariables.md b/test/functional/test_site/testImportVariables.md new file mode 100644 index 0000000000..b8d8aa11eb --- /dev/null +++ b/test/functional/test_site/testImportVariables.md @@ -0,0 +1,11 @@ +variablesToImport + +Making sure the issue here https://github.com/MarkBind/markbind/commit/48b57a18a8bfd68101b163908da4a0541756364a is fixed. + + + +Test import variables from src specified via variable: +{{var}} + +Test import variables that itself imports other variables: +{{deepvar}} \ No newline at end of file diff --git a/test/functional/test_site/testPanelsWithImportedVariables.md b/test/functional/test_site/testPanelsWithImportedVariables.md new file mode 100644 index 0000000000..29b19acce0 --- /dev/null +++ b/test/functional/test_site/testPanelsWithImportedVariables.md @@ -0,0 +1,11 @@ +Refer to this comment: https://github.com/MarkBind/markbind/pull/751#issuecomment-469670640 + + +{{ child.title }} + + +## Below panel is working + + +## Below should be a panel, but is now an error. Uncomment it to see the error. + \ No newline at end of file diff --git a/test/functional/test_site/variablesToImport.md b/test/functional/test_site/variablesToImport.md new file mode 100644 index 0000000000..19c7466a39 --- /dev/null +++ b/test/functional/test_site/variablesToImport.md @@ -0,0 +1,26 @@ +This page contains some variables that are being imported in other files. +There should be only VARIABLES START and VARIABLES END in red: + +
+VARIABLES START + +This variable comes from variablesToImport.md + +This is a page variable from variablestoimport.md + + + + + +## Trying to access a page variable: +There should be something red below: +
{{pagevar}}
+Something should have appeared above in red. + +## Trying to access an imported variable via namespace: +There should be something blue below: +
{{namespace.variable}}
+Something should have appeared above in blue. +
+VARIABLES END +
\ No newline at end of file