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
66 changes: 66 additions & 0 deletions docs/userGuide/syntax/variables.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -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 [`<include>` 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 `<include>`]({{ 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
<variable name="address">123 Sun Avenue</variable>
<variable name="name">Mark</variable>
<variable name="phone">123456789</variable>
```

`coverpage.md`:

```html
<import address name from="person.md"/>
```

will allow you to access the variables as per normal: <code>{<span></span>{address}}</code>, <code>{<span></span>{name}}</code>, <code>{<span></span>{phone}}</code>.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could collapse this by directly using it in coverpage.md, saves some words.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I think i'll keep it as it is, the change doesn't look very nice.


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider this structure (to be consistent with the rest of the documentation) i.e., abstract explanation followed by concrete example:

You can access page variables from another page by importing them.

{{ icon_example }} Importing specific variables from person.md into coverpage.md:
person.md:

<variable name="address">123 Sun Avenue</variable>
<variable name="name">Mark</variable>

coverpage.md:

<import address name phone from="page.md"/>
Address: {{ address }} 
Name: {{ name }}

When importing all variables, you should attach a namespace to the imported variables using an as attribute.

{{ icon_example }}:
person.md:

<variable name="address">123 Sun Avenue</variable>
<variable name="name">Mark</variable>

coverpage.md:

<import  from="person.md" as="persondata" />
Address: {{ persondata.address }} 
Name: {{ persondata.name }}

---

**When importing all variables, you should attach a _namespace_** to the imported variables using an `as` attributes.


{{ icon_example }}:
`coverpage.md`:
```html
<import from="page.md" as="details"/>
```

| Detail | How to access
| :------------- |:-------------
| address | <code>{<span></span>{details.address}}</code>
| name | <code>{<span></span>{details.<span></span>name}}</code>
| phone | <code>{<span></span>{details.phone}}</code>

This way, ***all*** variables in `page.md` are accessible via <code>{<span></span>{details.&lt;variable_name&gt;}}</code>.

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
<import from="title.md" as="book"/>
<import from="index.md" as="book"/>
```

In this case, all the variables in `title.md` are not accessible, as they are overwritten with the variables from `index.md`.

<box type="important">

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
<import address name from="page.md" as="details"/>
```

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 <code>{<span></span>{address}}</code> and <code>{<span></span>{name}}</code>
- access `address`, `name`, and `phone` with <code>{<span></span>{details.address}}</code>, <code>{<span></span>{details.<span></span>name}}</code>, and <code>{<span></span>{details.phone}}</code>

</box>

### Variables: Tips and Tricks

Expand Down
2 changes: 2 additions & 0 deletions src/Site.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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))
Expand Down
198 changes: 155 additions & 43 deletions src/lib/markbind/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Comment thread
openorclose marked this conversation as resolved.
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
Expand All @@ -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());
/**
* <import>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');
Expand All @@ -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 () {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
Expand All @@ -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') {
Expand Down Expand Up @@ -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';
Expand Down
22 changes: 22 additions & 0 deletions test/functional/test_site/expected/siteData.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
Loading