Skip to content

Commit

Permalink
Add live preview support for nunjucks dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
ang-zeyu committed Sep 24, 2020
1 parent 78075a0 commit 38856e5
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 62 deletions.
9 changes: 6 additions & 3 deletions docs/userGuide/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
layout: userGuide
</frontmatter>

#### Live Preview
#### Live Preview <span style="font-size: 0.8em;">:fas-sync:</span>

<span id="live-preview">
<md>**_Live preview_ is the regenerating the site upon any change to source files and reloading the updated site in the Browser**.</md>

{{ icon_info }} Live preview works for the following file types only: `css`, `.html`, `.md`, <tooltip content="MarkBind file">`.mbd`</tooltip>, <tooltip content="MarkBind fragment">`.mbdf`</tooltip>. Changes to `css` might may not be visible in the site until you do a manual refresh of the page.
**_Live preview_** is:
- Regeneration of affected content upon any change to <tooltip content="`.md`, `.mbd`, `.mbdf`, `.njk` files ... anything your content depends on!">source files</tooltip>, then reloading the updated site in the Browser.

- Copying <tooltip content="files that don't affect page generation (eg. images), but are used in the site">assets</tooltip> to the site output folder.

Use [the `serve` command](cliCommands.html#serve-command) to launch a live preview.

</span>

<br>

#### `.mbd` extension

Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"markdown-it-texmath": "^0.8.0",
"markdown-it-video": "^0.6.3",
"moment": "^2.24.0",
"nunjucks": "^3.2.0",
"nunjucks": "3.2.2",
"path-is-inside": "^1.0.2",
"progress": "^2.0.3",
"simple-git": "^2.17.0",
Expand Down
45 changes: 27 additions & 18 deletions packages/core/src/Page/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,8 +527,9 @@ class Page {
* @param pageData a page with its front matter collected
* @param {FileConfig} fileConfig
* @param {ComponentPreprocessor} componentPreprocessor for running {@link includeFile} on the layout
* @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
*/
generateExpressiveLayout(pageData, fileConfig, componentPreprocessor) {
generateExpressiveLayout(pageData, fileConfig, componentPreprocessor, pageSources) {
const layoutPath = path.join(this.pageConfig.rootPath, LAYOUT_FOLDER_PATH, this.layout);
const layoutPagePath = path.join(layoutPath, LAYOUT_PAGE);

Expand All @@ -543,14 +544,14 @@ class Page {
Render {{ MAIN_CONTENT_BODY }} and {% raw/endraw %} back to itself first,
which is then dealt with in the call below to {@link renderSiteVariables}.
*/
.then(result => this.pageConfig.variableProcessor.renderPage(layoutPagePath, result, {
.then(result => this.pageConfig.variableProcessor.renderPage(layoutPagePath, result, pageSources, {
[LAYOUT_PAGE_BODY_VARIABLE]: `{{${LAYOUT_PAGE_BODY_VARIABLE}}}`,
}, true))
// Include file with the cwf set to the layout page path
.then(result => componentPreprocessor.includeFile(layoutPagePath, result))
// Note: The {% raw/endraw %}s previously kept are removed here.
.then(result => this.pageConfig.variableProcessor.renderSiteVariables(
this.pageConfig.rootPath, result, {
this.pageConfig.rootPath, result, pageSources, {
[LAYOUT_PAGE_BODY_VARIABLE]: pageData,
}));
}
Expand All @@ -560,8 +561,9 @@ class Page {
* Determines if a fixed header is present, update the page config accordingly
* @param pageData a page with its front matter collected
* @param {FileConfig} fileConfig
* @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
*/
insertHeaderFile(pageData, fileConfig) {
insertHeaderFile(pageData, fileConfig, pageSources) {
if (!this.header || !fs.existsSync(this.header)) {
return pageData;
}
Expand All @@ -578,15 +580,16 @@ class Page {
this.includedFiles.add(this.header);

const renderedHeader = this.pageConfig.variableProcessor.renderSiteVariables(this.pageConfig.sourcePath,
headerContent);
headerContent, pageSources);
return `${renderedHeader}\n${pageData}`;
}

/**
* Inserts the footer specified in front matter to the end of the page
* @param pageData a page with its front matter collected
* @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
*/
insertFooterFile(pageData) {
insertFooterFile(pageData, pageSources) {
if (!this.footer || !fs.existsSync(this.footer)) {
return pageData;
}
Expand All @@ -596,16 +599,17 @@ class Page {
this.includedFiles.add(this.footer);

const renderedFooter = this.pageConfig.variableProcessor.renderSiteVariables(this.pageConfig.sourcePath,
footerContent);
footerContent, pageSources);
return `${pageData}\n${renderedFooter}`;
}

/**
* Inserts a site navigation bar using the file specified in the front matter
* @param pageData, a page with its front matter collected
* @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
* @throws (Error) if there is more than one instance of the <navigation> tag
*/
insertSiteNav(pageData) {
insertSiteNav(pageData, pageSources) {
if (!this.siteNav || !fs.existsSync(this.siteNav)) {
this.siteNav = false;
return pageData;
Expand All @@ -619,7 +623,7 @@ class Page {
this.includedFiles.add(this.siteNav);

const siteNavMappedData = this.pageConfig.variableProcessor.renderSiteVariables(
this.pageConfig.sourcePath, siteNavContent);
this.pageConfig.sourcePath, siteNavContent, pageSources);

// Check navigation elements
const $ = cheerio.load(siteNavMappedData);
Expand Down Expand Up @@ -785,7 +789,11 @@ class Page {
}
}

collectHeadFiles() {
/**
* Collect head files into {@link headFileTopContent} and {@link headFileBottomContent}
* @param {PageSources} pageSources to add dependencies found during nunjucks rendering to
*/
collectHeadFiles(pageSources) {
if (!this.head) {
this.headFileTopContent = '';
this.headFileBottomContent = '';
Expand All @@ -803,7 +811,7 @@ class Page {
this.includedFiles.add(headFilePath);

const headFileMappedData = this.pageConfig.variableProcessor.renderSiteVariables(
this.pageConfig.sourcePath, headFileContent).trim();
this.pageConfig.sourcePath, headFileContent, pageSources).trim();
// Split top and bottom contents
const $ = cheerio.load(headFileMappedData);
if ($('head-top').length) {
Expand Down Expand Up @@ -880,30 +888,31 @@ class Page {
const componentParser = new ComponentParser(fileConfig);

return fs.readFile(this.pageConfig.sourcePath, 'utf-8')
.then(result => this.pageConfig.variableProcessor.renderPage(this.pageConfig.sourcePath, result))
.then(result => this.pageConfig.variableProcessor.renderPage(this.pageConfig.sourcePath,
result, pageSources))
.then(result => componentPreprocessor.includeFile(this.pageConfig.sourcePath, result))
.then((result) => {
this.collectFrontMatter(result);
this.processFrontMatter();
return Page.removeFrontMatter(result);
})
.then(result => this.generateExpressiveLayout(result, fileConfig, componentPreprocessor))
.then(result => this.generateExpressiveLayout(result, fileConfig, componentPreprocessor, pageSources))
.then(result => Page.removePageHeaderAndFooter(result))
.then(result => Page.addScrollToTopButton(result))
.then(result => Page.addContentWrapper(result))
.then(result => this.collectPluginSources(result))
.then(result => this.preRender(result))
.then(result => this.insertSiteNav((result)))
.then(result => this.insertHeaderFile(result, fileConfig))
.then(result => this.insertFooterFile(result))
.then(result => this.insertSiteNav(result, pageSources))
.then(result => this.insertHeaderFile(result, fileConfig, pageSources))
.then(result => this.insertFooterFile(result, pageSources))
.then(result => Page.insertTemporaryStyles(result))
.then(result => componentParser.render(this.pageConfig.sourcePath, result))
.then(result => this.postRender(result))
.then(result => this.collectPluginsAssets(result))
.then(result => Page.unwrapIncludeSrc(result))
.then((result) => {
this.addLayoutScriptsAndStyles();
this.collectHeadFiles();
this.collectHeadFiles(pageSources);

this.content = result;

Expand Down Expand Up @@ -1190,7 +1199,7 @@ class Page {
const componentParser = new ComponentParser(fileConfig);

return fs.readFile(dependency.to, 'utf-8')
.then(result => this.pageConfig.variableProcessor.renderPage(dependency.to, result))
.then(result => this.pageConfig.variableProcessor.renderPage(dependency.to, result, pageSources))
.then(result => componentPreprocessor.includeFile(dependency.to, result, file))
.then(result => Page.removeFrontMatter(result))
.then(result => this.collectPluginSources(result))
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/Site/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ class Site {
const filePathArray = Array.isArray(filePaths) ? filePaths : [filePaths];
const uniquePaths = _.uniq(filePathArray);
this.runBeforeSiteGenerateHooks();
this.variableProcessor.invalidateCache(); // invalidate internal nunjucks cache for file changes

return this.regenerateAffectedPages(uniquePaths)
.then(() => fs.remove(this.tempPath))
Expand Down Expand Up @@ -742,8 +743,9 @@ class Site {

_rebuildSourceFiles() {
logger.info('Page added or removed, updating list of site\'s pages...');
const removedPageFilePaths = this.updateAddressablePages();
this.variableProcessor.invalidateCache(); // invalidate internal nunjucks cache for file removals

const removedPageFilePaths = this.updateAddressablePages();
return this.removeAsset(removedPageFilePaths)
.then(() => {
if (this.onePagePath) {
Expand Down
149 changes: 149 additions & 0 deletions packages/core/src/patches/nunjucks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
Patch for nunjucks to emit the 'load' event, even if the template is accessed from its internal cache.
https://mozilla.github.io/nunjucks/api.html#load-event
This allows page dependencies to be properly collected for live preview in {@link VariableRenderer}.
Patch is written against nunjucks v3.2.2
Changes are delimited with a // CHANGE HERE comment
*/

const { Environment, Template, lib } = require('nunjucks');

/* eslint-disable */

var noopTmplSrc = {
type: 'code',
obj: {
root: function root(env, context, frame, runtime, cb) {
try {
cb(null, '');
} catch (e) {
cb(handleError(e, null, null));
}
}
}
};

Environment.prototype.getTemplate = function getTemplate(name, eagerCompile, parentName, ignoreMissing, cb) {
var _this3 = this;

var that = this;
var tmpl = null;

if (name && name.raw) {
// this fixes autoescape for templates referenced in symbols
name = name.raw;
}

if (lib.isFunction(parentName)) {
cb = parentName;
parentName = null;
eagerCompile = eagerCompile || false;
}

if (lib.isFunction(eagerCompile)) {
cb = eagerCompile;
eagerCompile = false;
}

if (name instanceof Template) {
tmpl = name;
} else if (typeof name !== 'string') {
throw new Error('template names must be a string: ' + name);
} else {
for (var i = 0; i < this.loaders.length; i++) {
var loader = this.loaders[i];
tmpl = loader.cache[this.resolveTemplate(loader, parentName, name)];

if (tmpl) {
// CHANGE HERE

// pathsToNames in nunjucks.loaders.FileSystemLoader maintains a reverse mapping of fullPath: name
Object.entries(loader.pathsToNames).forEach(([fullPath, templateName]) => {
if (name === templateName) {
// Emit the load event
this.emit('load', name, {
src: tmpl,
path: fullPath, // we only need this
noCache: loader.noCache
}, loader)
}
});

break;
}
}
}

if (tmpl) {
if (eagerCompile) {
tmpl.compile();
}

if (cb) {
cb(null, tmpl);
return undefined;
} else {
return tmpl;
}
}

var syncResult;

var createTemplate = function createTemplate(err, info) {
if (!info && !err && !ignoreMissing) {
err = new Error('template not found: ' + name);
}

if (err) {
if (cb) {
cb(err);
return;
} else {
throw err;
}
}

var newTmpl;

if (!info) {
newTmpl = new Template(noopTmplSrc, _this3, '', eagerCompile);
} else {
newTmpl = new Template(info.src, _this3, info.path, eagerCompile);

if (!info.noCache) {
info.loader.cache[name] = newTmpl;
}
}

if (cb) {
cb(null, newTmpl);
} else {
syncResult = newTmpl;
}
};

lib.asyncIter(this.loaders, function (loader, i, next, done) {
function handle(err, src) {
if (err) {
done(err);
} else if (src) {
src.loader = loader;
done(null, src);
} else {
next();
}
} // Resolve name relative to parentName


name = that.resolveTemplate(loader, parentName, name);

if (loader.async) {
loader.getSource(name, handle);
} else {
handle(null, loader.getSource(name));
}
}, createTemplate);
return syncResult;
}
Loading

0 comments on commit 38856e5

Please sign in to comment.