You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
thumbsup/src/website/theme.js

159 lines
5.0 KiB
JavaScript

const fs = require('node:fs')
const path = require('node:path')
const async = require('async')
const debug = require('debug')('thumbsup:debug')
const fsextra = require('fs-extra')
const handlebars = require('handlebars')
const less = require('less')
class Theme {
constructor (themeDir, destDir, opts) {
// directory that contains the theme
this.dir = path.resolve(themeDir)
// target directory
this.dest = path.resolve(destDir)
// custom options
this.opts = opts
}
// load all theme helpers
// and copy assets into the output folder (static files, CSS...)
prepare (done) {
this.options = this.loadPackageOptions()
this.dir = path.join(this.dir, this.options.themeRoot || '')
// validate that the theme looks well structured
this.validateStructure()
// compiled template
this.template = compileTemplate(path.join(this.dir, 'album.hbs'))
this.loadPartials()
this.loadHelpers()
async.series([
next => this.copyPublic(next),
next => this.renderStyles(next)
], done)
}
// look for an explicit root in the theme's <package.json>
loadPackageOptions () {
try {
const packagePath = path.join(this.dir, 'package.json')
const contents = fs.readFileSync(packagePath).toString()
const pkg = JSON.parse(contents)
return pkg.thumbsup || {}
} catch (ex) {
debug('Theme does not have a package.json, using default options')
return {}
}
}
// make sure the given folder is a valid theme
validateStructure () {
if (!fs.existsSync(this.dir)) {
throw new Error(`Theme does not exist: ${this.dir}`)
}
const template = fs.existsSync(path.join(this.dir, 'album.hbs'))
const style = fs.existsSync(path.join(this.dir, 'theme.less'))
if (!template || !style) {
throw new Error(`Invalid theme structure in ${this.dir}`)
}
}
// renders the given album HTML page
render (targetPath, data, next) {
const fullPath = path.join(this.dest, targetPath)
debug(`Theme rendering ${targetPath}`)
const contents = this.template(data)
fs.mkdirSync(path.dirname(fullPath), { recursive: true })
fs.writeFile(fullPath, contents, next)
}
// ------------------------
// private methods
// ------------------------
loadPartials () {
if (!isDirectory(path.join(this.dir, 'partials'))) {
return
}
// load all files in the <partials> folder
const partials = fs.readdirSync(path.join(this.dir, 'partials'))
const isTemplate = filepath => path.extname(filepath) === '.hbs'
partials.filter(isTemplate).forEach(filename => {
debug(`Loading partial ${filename}`)
const templateName = path.basename(filename, path.extname(filename))
handlebars.registerPartial(templateName, compileTemplate(path.join(this.dir, 'partials', filename)))
})
}
loadHelpers () {
if (!isDirectory(path.join(this.dir, 'helpers'))) {
return
}
// load all files in the <helpers> folder
const helpers = fs.readdirSync(path.join(this.dir, 'helpers'))
const isHelper = filepath => path.extname(filepath) === '.js'
helpers.filter(isHelper).forEach(filename => {
debug(`Loading helper ${filename}`)
const name = path.basename(filename, path.extname(filename))
const fullPath = path.join(this.dir, 'helpers', filename)
handlebars.registerHelper(name, require(fullPath))
})
}
renderStyles (done) {
debug('Rendering theme styles')
// full LESS content
const themeFile = path.join(this.dir, 'theme.less')
const themeLess = fs.readFileSync(themeFile, 'utf-8')
// optional custom LESS file
const customInclude = this.customStylesInclude()
// now render both together
const lessOptions = {
paths: [this.dir]
}
less.render(themeLess + customInclude, lessOptions, (err, output) => {
if (err) return done(err)
const filename = this.opts.stylesheetName || 'theme.css'
const dest = path.join(this.dest, 'public', filename)
fs.mkdirSync(path.join(this.dest, 'public'), { recursive: true })
fs.writeFile(dest, output.css, done)
})
}
customStylesInclude () {
const customPath = this.opts.customStylesPath
if (customPath) {
// see http://lesscss.org/features/#import-atrules-feature
// we use (inline) to avoid processing raw CSS files
const includeType = path.extname(customPath) === '.css' ? '(inline) ' : ''
return `\n@import ${includeType}'${customPath}';`
}
return ''
}
copyPublic (done) {
if (!isDirectory(path.join(this.dir, 'public'))) {
return done()
}
// copy all files in the <public> folder
const src = path.join(this.dir, 'public')
const dest = path.join(this.dest, 'public')
fsextra.copy(src, dest, done)
}
}
function compileTemplate (hbsFile) {
const src = fs.readFileSync(hbsFile)
return handlebars.compile(src.toString())
}
function isDirectory (fullPath) {
try {
return fs.statSync(fullPath).isDirectory()
} catch (ex) {
return false
}
}
module.exports = Theme