Skip to content

Commit

Permalink
CASSANDRA-16066: Add Example site-ui components
Browse files Browse the repository at this point in the history
Added to the 'site-ui' directory an example an Antora site UI implementation.
The example was taken from https://gitlab.com/antora/antora-ui-default. It
shows the styling components that are needed in the 'site-ui' and how the
ui-bundle.zip can be generated.

The associated tooling to call Gulp to preview, build and generate the
ui-bundle.zip will be added in another commit.

patch by Anthony Grasso; reviewed by Mick Semb Wever, Lorina Poland, Melissa Logan, Paul Au for CASSANDRA-16066
  • Loading branch information
ossarga authored and michaelsembwever committed Sep 2, 2021
1 parent 3701033 commit e833a5d
Show file tree
Hide file tree
Showing 118 changed files with 19,170 additions and 1 deletion.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
.DS_Store
/.idea/
.DS_Store

/site-content/build/

/site-ui/build/
/site-ui/node_modules/
/site-ui/public/
14 changes: 14 additions & 0 deletions site-ui/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "standard",
"rules": {
"arrow-parens": ["error", "always"],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline"
}],
"max-len": [1, 120, 2],
"spaced-comment": "off"
}
}
7 changes: 7 additions & 0 deletions site-ui/.stylelintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "stylelint-config-standard",
"rules": {
"comment-empty-line-before": null,
"no-descending-specificity": null,
}
}
373 changes: 373 additions & 0 deletions site-ui/LICENSE

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions site-ui/gulp.d/lib/create-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict'

const metadata = require('undertaker/lib/helpers/metadata')
const { watch } = require('gulp')

module.exports = ({ name, desc, opts, call: fn, loop }) => {
if (name) {
const displayName = fn.displayName
if (displayName === '<series>' || displayName === '<parallel>') {
metadata.get(fn).tree.label = `${displayName} ${name}`
}
fn.displayName = name
}
if (loop) {
const delegate = fn
name = delegate.displayName
delegate.displayName = `${name}:loop`
fn = () => watch(loop, { ignoreInitial: false }, delegate)
fn.displayName = name
}
if (desc) fn.description = desc
if (opts) fn.flags = opts
return fn
}
14 changes: 14 additions & 0 deletions site-ui/gulp.d/lib/export-tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict'

module.exports = (...tasks) => {
const seed = {}
if (tasks.length) {
if (tasks.lastIndexOf(tasks[0]) > 0) {
const task1 = tasks.shift()
seed.default = Object.assign(task1.bind(null), { description: `=> ${task1.displayName}`, displayName: 'default' })
}
return tasks.reduce((acc, it) => (acc[it.displayName || it.name] = it) && acc, seed)
} else {
return seed
}
}
44 changes: 44 additions & 0 deletions site-ui/gulp.d/lib/gulp-prettier-eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict'

const log = require('fancy-log')
const PluginError = require('plugin-error')
const prettierEslint = require('prettier-eslint')
const { Transform } = require('stream')
const map = (transform) => new Transform({ objectMode: true, transform })

module.exports = () => {
const report = { changed: 0, unchanged: 0 }
return map(format).on('finish', () => {
if (report.changed > 0) {
const changed = 'formatted '
.concat(report.changed)
.concat(' file')
.concat(report.changed === 1 ? '' : 's')
const unchanged = 'left '
.concat(report.unchanged)
.concat(' file')
.concat(report.unchanged === 1 ? '' : 's')
.concat(' unchanged')
log(`prettier-eslint: ${changed}; ${unchanged}`)
} else {
log(`prettier-eslint: left ${report.unchanged} file${report.unchanged === 1 ? '' : 's'} unchanged`)
}
})

function format (file, enc, next) {
if (file.isNull()) return next()
if (file.isStream()) return next(new PluginError('gulp-prettier-eslint', 'Streaming not supported'))

const input = file.contents.toString()
const output = prettierEslint({ text: input, filePath: file.path })

if (input === output) {
report.unchanged += 1
} else {
report.changed += 1
file.contents = Buffer.from(output)
}

next(null, file)
}
}
214 changes: 214 additions & 0 deletions site-ui/gulp.d/tasks/build-preview-pages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
'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))
)
}
Loading

0 comments on commit e833a5d

Please sign in to comment.