docpad-plugin-partials
Version:
Adds support for Partials (re-usable templates) to DocPad
349 lines (299 loc) • 8.68 kB
JavaScript
/* eslint-disable class-methods-use-this */
// @ts-nocheck
// Export Plugin
module.exports = function (BasePlugin) {
// Requires
const Errlop = require('errlop').default
const extendr = require('extendr')
const { Task, TaskGroup } = require('taskgroup')
const pathUtil = require('path')
const { format } = require('util')
// Define Plugin
return class PartialsPlugin extends BasePlugin {
// Plugin Name
get name() {
return 'partials'
}
// Default Configuration
get initialConfig() {
return {
partialPaths: ['partials'],
collectionName: 'partials',
performanceFirst: false,
}
}
// Variables
constructor(...args) {
super(...args)
this.locale = {
addingPartial: 'Adding partial: %s',
partialNotFound:
'The partial "%s" was not found, as such it will not be rendered.',
renderPartial: 'Rendering partial: %s',
renderedPartial: 'Rendered partial: %s',
renderPartialFailed: 'Rendering partial failed: %s. The error follows:',
}
this.foundPartials = {}
this.partialsCache = {}
}
// -----------------------------
// Initialize
// Prepare our Configuration
setConfig(...args) {
// Apply
super.setConfig(...args)
// Prepare
const config = this.getConfig()
const { docpad } = this
// ensure config name backward compatibility (partial[s]Path, partial[]Path[s])
if (config.partialsPath) {
docpad.log(
'note',
'partials plugin configuration had partialsPath defined, but it is partialPaths'
)
config.partialPaths = config.partialsPath
}
// ensure the partialPaths is an array
if (!Array.isArray(config.partialPaths)) {
config.partialPaths = [config.partialPaths]
}
// Adjust
config.partialPaths.forEach(function (partialPath, index) {
config.partialPaths[index] = pathUtil.resolve(
docpad.getPath('source'),
partialPath
)
})
// Debug
console.log(config, args)
}
// -----------------------------
// Events
// Populate Collections
populateCollections(opts, next) {
// Prepare
const config = this.getConfig()
const { docpad } = this
// Load our partials directory
let error = null
config.partialPaths.forEach(function (partialPath) {
docpad.parseDocumentDirectory({ path: partialPath }, function (
err,
results
) {
// stack the errors
if (err) error = new Errlop(err, error)
})
})
// complete with the error if we have one
next(error)
}
// Extend Collections
extendCollections(opts) {
// Prepare
const config = this.getConfig()
const { docpad, locale } = this
const database = docpad.getDatabase()
// Add our partials collection
docpad.setCollection(
config.collectionName,
database
.createLiveChildCollection()
.setQuery('isPartial', {
$or: {
isPartial: true,
fullPath: {
$startsWith: config.partialPaths,
},
},
})
.on('add', function (model) {
docpad.log(
'debug',
format(locale.addingPartial, model.getFilePath())
)
model.setDefaults({
isPartial: true,
render: false,
write: false,
})
})
)
}
// -----------------------------
// Rendering
// Render Partial
// Render a partial asynchronously
// next(err,result,document)
renderPartial(partial, next) {
// Prepare
const { docpad, partialsCache } = this
let result
// Check if our partial is cacheable
let cacheable = partial.document.getMeta().get('cacheable')
if (cacheable == null) cacheable = false
else if (cacheable) {
result = partialsCache[partial.cacheId]
}
// Got from cache, so use that
if (result != null) {
return next(null, result)
}
// Render
docpad.renderDocument(
partial.document,
{ templateData: partial.data },
function (err, result, document) {
// Check
if (err) return next(err)
// Cache
if (cacheable) {
partialsCache[partial.cacheId] = result
}
// Forward
return next(null, result == null ? null : result)
}
)
}
// Extend Template Data
// Inject our partial methods
extendTemplateData({ templateData }) {
// Prepare
const me = this
const { docpad, locale } = this
// Apply
templateData.partial = function (partialName, ...objs) {
// Reference others
const config = me.getConfig()
if (this.referencesOthers) {
this.referencesOthers()
}
// Prepare
// const file = this.documentModel
const partial = {}
// Fetch our partial
// partialFuzzyPath = pathUtil.join(config.partialsPath, partialName)
// partial.document ?= docpad.getCollection('partials').fuzzyFindOne(partialFuzzyPath)
const collection = docpad.getCollection('partials')
config.partialPaths.forEach(function (partialPath) {
const partialFuzzyPath = pathUtil.join(partialPath, partialName)
if (!partial.document) {
partial.document = collection.fuzzyFindOne(partialFuzzyPath)
}
})
// partial not found
if (!partial.document) {
const message = format(locale.partialNotFound, partialName)
const err = new Error(message)
if (partial.err == null) {
partial.err = err
}
return message
}
// Prepare the initial partial data
partial.data = {}
// If no object is provided then provide the current template data as the first thing
// if the performance first option is set to false (the default)
if (config.performanceFirst === false) {
if ([false, this].includes(objs[0]) === false) {
objs.unshift(this)
}
}
// Cycle through the objects merging them together
// ignore boolean values
for (const obj of objs) {
if (obj && obj !== true) {
extendr.extend(partial.data, obj)
// ^ why do we just do a shallow extend here instead of a deep extend?
}
}
// Prepare our partial id
partial.path = partial.document.getFilePath()
partial.cacheId = partial.document.id
partial.id = Math.random() // require('crypto').createHash('md5').update(partial.cacheId+'|'+JSON.stringify(partial.data)).digest('hex')
partial.container = '[partial:' + partial.id + ']'
// Check if a partial with this id already exists!
if (me.foundPartials[partial.id]) {
return partial.container
}
// Store the partial
me.foundPartials[partial.id] = partial
// Create the task for our partial
partial.task = new Task(`renderPartial: ${partial.path}`, function (
complete
) {
me.renderPartial(partial, function (err, result) {
if (partial.err == null) partial.err = err
if (partial.err) {
partial.result = partial.err.toString()
} else if (result) {
partial.result = result
} else {
partial.result = '???'
}
return complete(partial.err)
})
})
// Return the container
return partial.container
}
}
// Render the Document
// Render our partials
renderDocument(opts, next) {
// Prepare
const me = this
const { templateData, file } = opts
// Check
const partialContainerRegex = /\[partial:([^\]]+)\]/g
const partialContainers =
(opts.content || '').match(partialContainerRegex) || []
if (partialContainers.length === 0) {
return next()
}
const filePath = file.getFilePath()
// Prepare
const tasks = new TaskGroup(`Partials for ${filePath}`, {
concurrency: 0,
}).done(function (err) {
// Replace containers with results
opts.content = opts.content.replace(partialContainerRegex, function (
match,
partialId
) {
// Fetch partial
const partial = me.foundPartials[partialId]
// Return result
return partial.result
})
// Complete
return next(err)
})
// Wait for found partials to complete rendering
partialContainers.forEach(function (partialContainer) {
// Fetch partial
const partialId = partialContainer.replace(partialContainerRegex, '$1')
const partial = me.foundPartials[partialId]
// Wait for all the partials to complete rendering
if (partial.task) {
tasks.addTask(partial.task)
}
})
// Run the tasks
tasks.run()
}
// Generate Before
// Reset the found partials before each generate, otherwise it will get very big
generateBefore() {
this.foundPartials = {}
this.partialsCache = {}
}
// Generate After
// Reset the found partials after each generate, otherwise it will linger uncessarily
generateAfter() {
this.foundPartials = {}
this.partialsCache = {}
}
}
}