site-ui/gulp.d/tasks/build-preview-pages.js (200 lines of code) (raw):
'use strict'
const asciidoctor = require('asciidoctor.js')()
const fs = require('fs-extra')
const handlebars = require('handlebars')
const merge = require('merge-stream')
const ospath = require('path')
const path = ospath.posix
const requireFromString = require('require-from-string')
const { Transform } = require('stream')
const map = (transform = () => {}, flush = undefined) => new Transform({ objectMode: true, transform, flush })
const vfs = require('vinyl-fs')
const yaml = require('js-yaml')
const ASCIIDOC_ATTRIBUTES = { experimental: '', icons: 'font', sectanchors: '', 'source-highlighter': 'highlight.js' }
module.exports = (src, previewSrc, previewDest, sink = () => map()) => (done) =>
Promise.all([
loadSampleUiModel(previewSrc),
toPromise(
merge(compileLayouts(src), registerPartials(src), registerHelpers(src), copyImages(previewSrc, previewDest))
),
])
.then(([baseUiModel, { layouts }]) => [{ ...baseUiModel, env: process.env }, layouts])
.then(([baseUiModel, layouts]) =>
vfs
.src('**/*.adoc', { base: previewSrc, cwd: previewSrc })
.pipe(
map((file, enc, next) => {
const siteRootPath = path.relative(ospath.dirname(file.path), ospath.resolve(previewSrc))
const uiModel = { ...baseUiModel }
uiModel.siteRootPath = siteRootPath
uiModel.siteRootUrl = path.join(siteRootPath, 'index.html')
uiModel.uiRootPath = path.join(siteRootPath, '_')
if (file.stem === '404') {
uiModel.page = { layout: '404', title: 'Page Not Found' }
} else {
const pageModel = (uiModel.page = { ...uiModel.page })
const doc = asciidoctor.load(file.contents, { safe: 'safe', attributes: ASCIIDOC_ATTRIBUTES })
const attributes = doc.getAttributes()
pageModel.layout = doc.getAttribute('page-layout', 'default')
pageModel.title = doc.getDocumentTitle()
pageModel.url = '/' + file.relative.slice(0, -5) + '.html'
if (file.stem === 'tutorials') pageModel.tutorials = true
const componentName = doc.getAttribute('page-component-name', pageModel.src.component)
const versionString = doc.getAttribute(
'page-version',
doc.hasAttribute('page-component-name') ? undefined : pageModel.src.version
)
let component
let componentVersion
if (componentName) {
component = pageModel.component = uiModel.site.components[componentName]
componentVersion = pageModel.componentVersion = versionString
? component.versions.find(({ version }) => version === versionString)
: component.latest
} else {
component = pageModel.component = Object.values(uiModel.site.components)[0]
componentVersion = pageModel.componentVersion = component.latest
}
pageModel.module = 'ROOT'
pageModel.relativeSrcPath = file.relative
pageModel.version = componentVersion.version
pageModel.displayVersion = componentVersion.displayVersion
pageModel.editUrl = pageModel.origin.editUrlPattern.replace('%s', file.relative)
pageModel.navigation = componentVersion.navigation || []
pageModel.breadcrumbs = findNavPath(pageModel.url, pageModel.navigation)
if (pageModel.component.versions.length > 1) {
pageModel.versions = pageModel.component.versions.map(({ version, displayVersion, url }, idx, arr) => {
const pageVersion = { version, displayVersion: displayVersion || version, url }
if (version === component.latest.version) pageVersion.latest = true
if (idx === arr.length - 1) {
delete pageVersion.url
pageVersion.missing = true
}
return pageVersion
})
}
pageModel.attributes = Object.entries({ ...attributes, ...componentVersion.asciidoc.attributes })
.filter(([name, val]) => name.startsWith('page-'))
.reduce((accum, [name, val]) => ({ ...accum, [name.substr(5)]: val }), {})
pageModel.contents = Buffer.from(doc.convert())
}
file.extname = '.html'
try {
file.contents = Buffer.from(layouts.get(uiModel.page.layout)(uiModel))
next(null, file)
} catch (e) {
next(transformHandlebarsError(e, uiModel.page.layout))
}
})
)
.pipe(vfs.dest(previewDest))
.on('error', done)
.pipe(sink())
)
function loadSampleUiModel (src) {
return fs.readFile(ospath.join(src, 'ui-model.yml'), 'utf8').then((contents) => {
const uiModel = yaml.safeLoad(contents)
uiModel.env = process.env
Object.entries(uiModel.site.components).forEach(([name, component]) => {
component.name = name
if (!component.versions) component.versions = [(component.latest = { url: '#' })]
component.versions.forEach((version) => {
Object.defineProperty(version, 'name', { value: component.name, enumerable: true })
if (!('displayVersion' in version)) version.displayVersion = version.version
if (!('asciidoc' in version)) version.asciidoc = { attributes: {} }
})
Object.defineProperties(component, {
asciidoc: {
get () {
return this.latest.asciidoc
},
},
title: {
get () {
return this.latest.title
},
},
url: {
get () {
return this.latest.url
},
},
})
})
return uiModel
})
}
function registerPartials (src) {
return vfs.src('partials/*.hbs', { base: src, cwd: src }).pipe(
map((file, enc, next) => {
handlebars.registerPartial(file.stem, file.contents.toString())
next()
})
)
}
function registerHelpers (src) {
handlebars.registerHelper('relativize', relativize)
handlebars.registerHelper('resolvePage', resolvePage)
handlebars.registerHelper('resolvePageURL', resolvePageURL)
return vfs.src('helpers/*.js', { base: src, cwd: src }).pipe(
map((file, enc, next) => {
handlebars.registerHelper(file.stem, requireFromString(file.contents.toString()))
next()
})
)
}
function compileLayouts (src) {
const layouts = new Map()
return vfs.src('layouts/*.hbs', { base: src, cwd: src }).pipe(
map(
(file, enc, next) => {
const srcName = path.join(src, file.relative)
layouts.set(file.stem, handlebars.compile(file.contents.toString(), { preventIndent: true, srcName }))
next()
},
function (done) {
this.push({ layouts })
done()
}
)
)
}
function copyImages (src, dest) {
return vfs.src('**/*.{png,svg}', { base: src, cwd: src }).pipe(vfs.dest(dest))
}
function findNavPath (currentUrl, node = [], current_path = [], root = true) {
for (const item of node) {
const { url, items } = item
if (url === currentUrl) {
return current_path.concat(item)
} else if (items) {
const activePath = findNavPath(currentUrl, items, current_path.concat(item), false)
if (activePath) return activePath
}
}
if (root) return []
}
function relativize (url) {
return url ? (url.charAt() === '#' ? url : url.slice(1)) : '#'
}
function resolvePage (spec, context = {}) {
if (spec) return { pub: { url: resolvePageURL(spec) } }
}
function resolvePageURL (spec, context = {}) {
if (spec) return '/' + (spec = spec.split(':').pop()).slice(0, spec.lastIndexOf('.')) + '.html'
}
function transformHandlebarsError ({ message, stack }, layout) {
const m = stack.match(/^ *at Object\.ret \[as (.+?)\]/m)
const templatePath = `src/${m ? 'partials/' + m[1] : 'layouts/' + layout}.hbs`
const err = new Error(`${message}${~message.indexOf('\n') ? '\n^ ' : ' '}in UI template ${templatePath}`)
err.stack = [err.toString()].concat(stack.substr(message.length + 8)).join('\n')
return err
}
function toPromise (stream) {
return new Promise((resolve, reject, data = {}) =>
stream
.on('error', reject)
.on('data', (chunk) => chunk.constructor === Object && Object.assign(data, chunk))
.on('finish', () => resolve(data))
)
}