Skip to content

Commit

Permalink
More sophisticated Bootprint module analysis in the "docEngine"
Browse files Browse the repository at this point in the history
* Functions to compute callers and the callees of templates/partials
* Functions for creating a call-hierarchy of templates and partials
* Functions for extracting comments from templates and partials
* (Fixup) The file-extension of partial-files (.hbs or .handlebars) is, like
  in the real engine, removed from the name when it is used as key
  in the partials- or templates-object.
  • Loading branch information
nknapp committed Jun 16, 2017
1 parent 74528a0 commit 57d1ce2
Show file tree
Hide file tree
Showing 7 changed files with 815 additions and 74 deletions.
114 changes: 110 additions & 4 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,64 @@ helpers are not loaded, but the path to the file is collected into an array.
```
{
"handlebars": {
"callHierarchy": {
"children": [
{
"children": [
{
"children": [
],
"comments": [
],
"name": "footer",
"path": "partials2/footer.hbs",
"type": "partial"
}
],
"comments": [
],
"name": "subdir/text3.txt",
"path": "templates/subdir/text3.txt.hbs",
"type": "template"
},
{
"children": [
{
"children": [
],
"comments": [
],
"name": "footer",
"path": "partials2/footer.hbs",
"type": "partial"
}
],
"comments": [
],
"name": "text1.txt",
"path": "templates/text1.txt.hbs",
"type": "template"
},
{
"children": [
{
"children": [
],
"comments": [
],
"name": "footer",
"path": "partials2/footer.hbs",
"type": "partial"
}
],
"comments": [
],
"name": "text2.txt",
"path": "templates/text2.txt.hbs",
"type": "template"
}
]
},
"data": {
"city": "Darmstadt",
"name": "nknapp"
Expand All @@ -227,7 +285,31 @@ helpers are not loaded, but the path to the file is collected into an array.
"partialWrapper": [
],
"partials": {
"footer.hbs": {
"footer": {
"calledBy": [
{
"line": 1,
"name": "subdir/text3.txt",
"path": "templates/subdir/text3.txt.hbs",
"type": "template"
},
{
"line": 5,
"name": "text1.txt",
"path": "templates/text1.txt.hbs",
"type": "template"
},
{
"line": 5,
"name": "text2.txt",
"path": "templates/text2.txt.hbs",
"type": "template"
}
],
"callsPartial": [
],
"comments": [
],
"contents": "------\nBlog: {{{github.blog}}}\n",
"path": "partials2/footer.hbs"
}
Expand All @@ -236,15 +318,39 @@ helpers are not loaded, but the path to the file is collected into an array.
"hb-preprocessor.js"
],
"templates": {
"subdir/text3.txt.hbs": {
"subdir/text3.txt": {
"callsPartial": [
{
"line": 1,
"name": "footer"
}
],
"comments": [
],
"contents": "{{>footer}}",
"path": "templates/subdir/text3.txt.hbs"
},
"text1.txt.hbs": {
"text1.txt": {
"callsPartial": [
{
"line": 5,
"name": "footer"
}
],
"comments": [
],
"contents": "I'm {{name}}\n\nI'm living in {{city}}.\n\n{{>footer}}",
"path": "templates/text1.txt.hbs"
},
"text2.txt.hbs": {
"text2.txt": {
"callsPartial": [
{
"line": 5,
"name": "footer"
}
],
"comments": [
],
"contents": "I'm {{name}}\n\nI'm living in {{shout city}}.\n\n{{>footer}}",
"path": "templates/text2.txt.hbs"
}
Expand Down
17 changes: 3 additions & 14 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var path = require('path')
var promisedHandlebars = require('promised-handlebars')

var contents = function (partials) {
return _.mapObject(partials, stripHandlebarsExt, (value) => value.contents)
return _.mapObject(partials, _.stripHandlebarsExt, (value) => value.contents)
}

/**
Expand Down Expand Up @@ -181,8 +181,8 @@ module.exports = {
if (config.addSourceLocators) {
// Lookup-tables for partial-/template-name to the source-file
// (which contains the original path to the actual file)
var partialToSourceFile = _.mapKeys(config.partials, stripHandlebarsExt)
var templateToSourceFile = _.mapKeys(config.templates, stripHandlebarsExt)
var partialToSourceFile = _.mapKeys(config.partials, _.stripHandlebarsExt)
var templateToSourceFile = _.mapKeys(config.templates, _.stripHandlebarsExt)
result = _.mapValues(result, function (contents, filename) {
// Post-process locator-tags to include file-paths
return contents.then((resolvedContents) => resolvedContents.replace(
Expand All @@ -203,17 +203,6 @@ module.exports = {
}
}

/**
* Used in _.mapKeys to remove the hbs extension
* @param {*} value ignored
* @param {string} key the original filename
* @returns {string} the filename without .hbs
* @access private
*/
function stripHandlebarsExt (value, key) {
return key.replace(/\.(handlebars|hbs)$/, '')
}

/**
* Internal function that returns `require`s a module if the parameter is a string.
*
Expand Down
15 changes: 13 additions & 2 deletions lib/doc-engine.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {files} = require('customize/helpers-io')
const Handlebars = require('handlebars')
const {augment, hierarchy} = require('./partial-details')
const _ = require('./utils')

/**
* This customize-engine takes the same configuration schema as
Expand Down Expand Up @@ -42,7 +43,17 @@ module.exports = {
},

run (config) {
return config
// Remove .hbs from partial and template names
config.templates = _.mapKeys(config.templates, _.stripHandlebarsExt)
config.partials = _.mapKeys(config.partials, _.stripHandlebarsExt)
return Object.assign(
config,
// Augmented partials and templates
augment(config),
{
callHierarchy: hierarchy(config)
}
)
}
}

Expand Down
185 changes: 185 additions & 0 deletions lib/partial-details.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
const Handlebars = require('handlebars')
const _ = require('./utils')

/**
* This module exports helper functions to compute details of Handlebars configurations.
* At the moment, this is
*
* * Compute callees and callers for each partial and template registered with
* `customize-engine-handlebars`
* * Extract comments from partials
* * Create a call-hierarchy (templates -> partials -> partials)
* the can be rendered using for example `thought`
* @type {{augmentSingleFile: function, augment: function, hierarchy: function}}
*/
module.exports = {augmentSingleFile, augment, hierarchy}

/**
* Compute a hierarchy tree of templates and partials calling other partials.
* The return value can be used as input for the 'renderTree'-helper and
* has the form
*
* ```
* [{
* "name": "template1",
* "type": "template",
* "path": "src/tmpl/template1.hbs
* "comments": ["a comment for template1"],
* "children": [
* "name": "partial1",
* "path": "src/prt/partial1.hbs
* "type": "partial",
* "comments": ["a comment for partial 1"],
* "children": []
* ]
* }]
* ```
*
* @param {{templates: object, partials: object}} config
* @return {object}
*/
function hierarchy (config) {
const templates = _.mapValues(config.templates, augmentSingleFile)
const partials = _.mapValues(config.partials, augmentSingleFile)
return {
children: Object.keys(templates).map((name) => {
let template = templates[name]
return {
name: name,
type: 'template',
path: template.path,
comments: template.comments,
children: template.callsPartial.map((callee) => partialForCallTree(callee.name, partials))
}
})
}
}

/**
* Inner function that returns a partial and its callees as tree
* @param {string} name the name of the partial
* @param {object} partials an object of all partials (by name). This is assumed to be an augmentedPartial with comment
* and callee
* @param {object<boolean>=} visitedNodes names of the visited nodes for breaking cycles (values are alwasy "true")
* @returns {{name: *, type: string, comment}}
*/
function partialForCallTree (name, partials, visitedNodes) {
visitedNodes = visitedNodes || {}
try {
const cycleFound = visitedNodes[name]
visitedNodes[name] = true
const partial = partials[name]
if (!partial) {
throw new Error(`Missing partial "${name}"`)
}
let children
if (!cycleFound) {
children = partial.callsPartial.map((callee) => partialForCallTree(callee.name, partials, visitedNodes))
}
return {
name,
type: 'partial',
comments: partial.comments,
path: partial.path,
children,
cycleFound
}
} finally {
delete visitedNodes[name]
}
}

/**
* Augment a whole customize-engine-handlebars configuration (templates and partials)
* by adding comment, callers and callees
* @param {{templates: object, partials: object}} config
*/
function augment (config) {
let augmentedTemplates = _.mapValues(config.templates, augmentSingleFile)
let augmentedPartials = _.mapValues(config.partials, augmentSingleFile)
// Prepare caller array in each partial
_.forEachValue(augmentedPartials, (file) => { file.calledBy = [] })
;[augmentedTemplates, augmentedPartials].forEach((obj) => {
_.forEachValue(obj, (file, name) => {
file.callsPartial.forEach((callee) => {
let partial = augmentedPartials[callee.name]
if (!partial) {
throw new Error(`Missing partial "${callee.name}"`)
}
partial.calledBy.push({
name: name,
path: file.path,
type: augmentedPartials === obj ? 'partial' : 'template',
line: callee.line
})
})
})
})
return {
templates: augmentedTemplates,
partials: augmentedPartials
}
}

/**
* Augments the 'fileWithContents' with several properties derived from the files contents:
*
* * **calls**: A list of one object `{name: 'abc'}` for each partial that is called from within the current
* template or partials.
* * **apidocComment**: The contents of first comment that contains the string `@apidoc`.
*
*
* @param {{path:string, contents: string}} fileWithContents an object containing the path to
* a file and its contents.
* @return {{path: string, contents: string, calls: string[], comment?:string}} the input object
* augmented by the partialnames called from this template or partial
*/
function augmentSingleFile (fileWithContents) {
const result = Object.assign({
callsPartial: [],
comments: []
}, fileWithContents)
const ast = Handlebars.parse(fileWithContents.contents)
new PartialVisitor(partialCall => result.callsPartial.push(partialCall)).accept(ast)
new ApiDocCommentVisitor(comment => result.comments.push(comment)).accept(ast)
return result
}

/**
* Visitory to collect partial-calls from the ast
* @see https://github.com/wycats/handlebars.js/blob/master/docs/compiler-api.md#ast-visitor
*/
class PartialVisitor extends Handlebars.Visitor {
/**
*
* @param {function({name:string, line:number})} fn function that is called for each partial-call in the template
*/
constructor (fn) {
super()
this.fn = fn
}

PartialStatement (partialCall) {
super.PartialStatement(partialCall)
this.fn({
name: partialCall.name.original,
line: partialCall.loc.start.line
})
}
}

class ApiDocCommentVisitor extends Handlebars.Visitor {
constructor (fn) {
super()
this.fn = fn
}

/**
* Visit a comment statement
* @param {string} comment
*/
CommentStatement (comment) {
super.CommentStatement(comment)
this.fn(comment.value.trim())
}
}
Loading

0 comments on commit 57d1ce2

Please sign in to comment.