|
| 1 | +'use strict' |
| 2 | + |
| 3 | +const { name: packageName } = require('../package.json') |
| 4 | +const fs = require('fs') |
| 5 | +const crypto = require('crypto') |
| 6 | +const { promises: fsp } = fs |
| 7 | +const LazyReadable = require('./lazy-readable') |
| 8 | +const MultiFileReadStream = require('./multi-file-read-stream') |
| 9 | +const ospath = require('path') |
| 10 | +const template = require('./template') |
| 11 | + |
| 12 | +function register ({ config: { rows, cols, autoPlay, ...unknownOptions } }) { |
| 13 | + const logger = this.getLogger(packageName) |
| 14 | + |
| 15 | + if (Object.keys(unknownOptions).length) { |
| 16 | + const keys = Object.keys(unknownOptions) |
| 17 | + throw new Error(`Unrecognized option${keys.length > 1 ? 's' : ''} specified for ${packageName}: ${keys.join(', ')}`) |
| 18 | + } |
| 19 | + |
| 20 | + const defaultOptions = { rows, cols, autoPlay } |
| 21 | + |
| 22 | + this.on('uiLoaded', async ({ playbook, uiCatalog }) => { |
| 23 | + playbook.env.SITE_ASCIINEMA_PROVIDER = 'asciinema' |
| 24 | + const asciinemaDir = 'asciinema' |
| 25 | + const uiOutputDir = playbook.ui.outputDir |
| 26 | + vendorJsFile( |
| 27 | + uiCatalog, |
| 28 | + logger, |
| 29 | + uiOutputDir, |
| 30 | + 'asciinema-player/dist/bundle/asciinema-player.min.js', |
| 31 | + 'asciinema-player.js' |
| 32 | + ) |
| 33 | + vendorCssFile( |
| 34 | + uiCatalog, |
| 35 | + logger, |
| 36 | + uiOutputDir, |
| 37 | + 'asciinema-player/dist/bundle/asciinema-player.css', |
| 38 | + 'asciinema-player.css' |
| 39 | + ) |
| 40 | + |
| 41 | + const asciinemaLoadScriptsPartialPath = 'asciinema-load.hbs' |
| 42 | + if (!uiCatalog.findByType('partial').some(({ path }) => path === asciinemaLoadScriptsPartialPath)) { |
| 43 | + const asciinemaLoadScriptsPartialFilepath = ospath.join(__dirname, asciinemaDir, asciinemaLoadScriptsPartialPath) |
| 44 | + uiCatalog.addFile({ |
| 45 | + contents: Buffer.from(template(await fsp.readFile(asciinemaLoadScriptsPartialFilepath, 'utf8'), {})), |
| 46 | + path: asciinemaLoadScriptsPartialPath, |
| 47 | + stem: 'asciinema-load-scripts', |
| 48 | + type: 'partial', |
| 49 | + }) |
| 50 | + } |
| 51 | + |
| 52 | + const asciinemaCreateScriptsPartialPath = 'asciinema-create.hbs' |
| 53 | + if (!uiCatalog.findByType('partial').some(({ path }) => path === asciinemaCreateScriptsPartialPath)) { |
| 54 | + const asciinemaCreateScriptsPartialFilepath = ospath.join( |
| 55 | + __dirname, |
| 56 | + asciinemaDir, |
| 57 | + asciinemaCreateScriptsPartialPath |
| 58 | + ) |
| 59 | + uiCatalog.addFile({ |
| 60 | + contents: Buffer.from(template(await fsp.readFile(asciinemaCreateScriptsPartialFilepath, 'utf8'), {})), |
| 61 | + path: asciinemaCreateScriptsPartialPath, |
| 62 | + stem: 'asciinema-create-scripts', |
| 63 | + type: 'partial', |
| 64 | + }) |
| 65 | + } |
| 66 | + |
| 67 | + const asciinemaStylesPartialPath = 'asciinema-styles.hbs' |
| 68 | + if (!uiCatalog.findByType('partial').some(({ path }) => path === asciinemaStylesPartialPath)) { |
| 69 | + const asciinemaStylesPartialFilepath = ospath.join(__dirname, asciinemaDir, asciinemaStylesPartialPath) |
| 70 | + uiCatalog.addFile({ |
| 71 | + contents: Buffer.from(template(await fsp.readFile(asciinemaStylesPartialFilepath, 'utf8'), {})), |
| 72 | + path: asciinemaStylesPartialPath, |
| 73 | + stem: 'asciinema-styles', |
| 74 | + type: 'partial', |
| 75 | + }) |
| 76 | + } |
| 77 | + |
| 78 | + const splitHelperPartialPath = 'asciinema-split-helper.js' |
| 79 | + const splitHelperPartialFilepath = ospath.join(__dirname, asciinemaDir, splitHelperPartialPath) |
| 80 | + uiCatalog.addFile({ |
| 81 | + contents: Buffer.from(template(await fsp.readFile(splitHelperPartialFilepath, 'utf8'), {})), |
| 82 | + path: 'helpers/' + splitHelperPartialPath, |
| 83 | + stem: 'asciinema-split', |
| 84 | + type: 'helper', |
| 85 | + }) |
| 86 | + |
| 87 | + const optionsHelperPartialPath = 'asciinema-options-helper.js' |
| 88 | + const optionsHelperPartialFilepath = ospath.join(__dirname, asciinemaDir, optionsHelperPartialPath) |
| 89 | + uiCatalog.addFile({ |
| 90 | + contents: Buffer.from(template(await fsp.readFile(optionsHelperPartialFilepath, 'utf8'), {})), |
| 91 | + path: 'helpers/' + optionsHelperPartialPath, |
| 92 | + stem: 'asciinema-options', |
| 93 | + type: 'helper', |
| 94 | + }) |
| 95 | + }) |
| 96 | + |
| 97 | + this.on('contentClassified', async ({ siteAsciiDocConfig, uiCatalog }) => { |
| 98 | + if (!siteAsciiDocConfig.extensions) siteAsciiDocConfig.extensions = [] |
| 99 | + siteAsciiDocConfig.extensions.push({ |
| 100 | + register: (registry, _context) => { |
| 101 | + registry.block('asciinema', processAsciinemaBlock(uiCatalog, defaultOptions, _context)) |
| 102 | + return registry |
| 103 | + }, |
| 104 | + }) |
| 105 | + }) |
| 106 | +} |
| 107 | + |
| 108 | +function processAsciinemaBlock (uiCatalog, defaultOptions, context) { |
| 109 | + return function () { |
| 110 | + this.onContext(['listing', 'literal']) |
| 111 | + this.positionalAttributes(['target', 'format']) |
| 112 | + this.process((parent, reader, attrs) => { |
| 113 | + const { file } = context |
| 114 | + const source = reader.getLines().join('\n') |
| 115 | + return toBlock(attrs, parent, source, this, uiCatalog, defaultOptions, file) |
| 116 | + }) |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +const fromHash = (hash) => { |
| 121 | + const object = {} |
| 122 | + const data = hash.$$smap |
| 123 | + for (const key in data) { |
| 124 | + object[key] = data[key] |
| 125 | + } |
| 126 | + return object |
| 127 | +} |
| 128 | + |
| 129 | +const toBlock = (attrs, parent, source, context, uiCatalog, defaultOptions, file) => { |
| 130 | + if (typeof attrs === 'object' && '$$smap' in attrs) { |
| 131 | + attrs = fromHash(attrs) |
| 132 | + } |
| 133 | + const doc = parent.getDocument() |
| 134 | + const subs = attrs.subs |
| 135 | + if (subs) { |
| 136 | + source = doc.$apply_subs(attrs.subs, doc.$resolve_subs(subs)) |
| 137 | + } |
| 138 | + const idAttr = attrs.id ? ` id="${attrs.id}"` : '' |
| 139 | + const classAttr = attrs.role ? `${attrs.role} videoblock` : 'videoblock' |
| 140 | + |
| 141 | + const block = context.$create_pass_block(parent, '', Opal.hash(attrs)) |
| 142 | + |
| 143 | + const title = attrs.title |
| 144 | + if (title) { |
| 145 | + block.title = title |
| 146 | + delete block.caption |
| 147 | + const caption = attrs.caption |
| 148 | + delete attrs.caption |
| 149 | + block.assignCaption(caption, 'figure') |
| 150 | + } |
| 151 | + |
| 152 | + const asciinemaId = crypto.createHash('md5').update(source, 'utf8').digest('hex') |
| 153 | + if (file.asciidoc.attributes['page-asciinemacasts']) { |
| 154 | + file.asciidoc.attributes['page-asciinemacasts'] = |
| 155 | + file.asciidoc.attributes['page-asciinemacasts'] + ',' + asciinemaId |
| 156 | + } else { |
| 157 | + file.asciidoc.attributes['page-asciinemacasts'] = asciinemaId |
| 158 | + } |
| 159 | + |
| 160 | + uiCatalog.addFile({ |
| 161 | + contents: Buffer.from(source), |
| 162 | + path: '_asciinema/' + asciinemaId + '.cast', |
| 163 | + type: 'asset', |
| 164 | + out: { path: '_asciinema/' + asciinemaId + '.cast' }, |
| 165 | + }) |
| 166 | + |
| 167 | + const asciinemaOptions = JSON.stringify(buildOptions(attrs, defaultOptions)) |
| 168 | + file.asciidoc.attributes['page-asciinema-options-' + asciinemaId] = asciinemaOptions |
| 169 | + |
| 170 | + const titleElement = title ? `<div class="title">${block.caption}${title}</div>` : '' |
| 171 | + const style = `${Object.hasOwn(attrs, 'width') ? `width: ${attrs.width}px;` : ''} ${ |
| 172 | + Object.hasOwn(attrs, 'height') ? `height: ${attrs.height}px;` : '' |
| 173 | + }` |
| 174 | + block.lines = [ |
| 175 | + `<div${idAttr} class="${classAttr}">`, |
| 176 | + `<div class="content"><div id="${asciinemaId}" style="${style}"></div></div>`, |
| 177 | + `${titleElement}</div>`, |
| 178 | + ] |
| 179 | + return block |
| 180 | +} |
| 181 | + |
| 182 | +function buildOptions (attrs, defaultOptions) { |
| 183 | + const options = {} |
| 184 | + const rows = attrs.rows ? attrs.rows : defaultOptions.rows |
| 185 | + if (rows) { |
| 186 | + options.rows = rows |
| 187 | + } |
| 188 | + const cols = attrs.cols ? attrs.cols : defaultOptions.cols |
| 189 | + if (cols) { |
| 190 | + options.cols = cols |
| 191 | + } |
| 192 | + const autoPlay = attrs.autoPlay ? attrs.autoPlay : defaultOptions.autoPlay |
| 193 | + if (autoPlay) { |
| 194 | + options.autoPlay = autoPlay |
| 195 | + } |
| 196 | + return options |
| 197 | +} |
| 198 | + |
| 199 | +function assetFile ( |
| 200 | + uiCatalog, |
| 201 | + logger, |
| 202 | + uiOutputDir, |
| 203 | + assetDir, |
| 204 | + basename, |
| 205 | + assetPath = assetDir + '/' + basename, |
| 206 | + contents = new LazyReadable(() => fs.createReadStream(ospath.join(__dirname, '../data', assetPath))), |
| 207 | + overwrite = false |
| 208 | +) { |
| 209 | + const outputDir = uiOutputDir + '/' + assetDir |
| 210 | + const existingFile = uiCatalog.findByType('asset').some(({ path }) => path === assetPath) |
| 211 | + if (existingFile) { |
| 212 | + if (overwrite) { |
| 213 | + logger.warn(`Please remove the following file from your UI since it is managed by ${packageName}: ${assetPath}`) |
| 214 | + existingFile.contents = contents |
| 215 | + delete existingFile.stat |
| 216 | + } else { |
| 217 | + logger.info(`The following file already exists in your UI: ${assetPath}, skipping`) |
| 218 | + } |
| 219 | + } else { |
| 220 | + uiCatalog.addFile({ |
| 221 | + contents, |
| 222 | + type: 'asset', |
| 223 | + path: assetPath, |
| 224 | + out: { dirname: outputDir, path: outputDir + '/' + basename, basename }, |
| 225 | + }) |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +function vendorJsFile (uiCatalog, logger, uiOutputDir, requireRequest, basename = requireRequest.split('/').pop()) { |
| 230 | + let contents |
| 231 | + if (Array.isArray(requireRequest)) { |
| 232 | + const filepaths = requireRequest.map(require.resolve) |
| 233 | + contents = new LazyReadable(() => new MultiFileReadStream(filepaths)) |
| 234 | + } else { |
| 235 | + const filepath = require.resolve(requireRequest) |
| 236 | + contents = new LazyReadable(() => fs.createReadStream(filepath)) |
| 237 | + } |
| 238 | + const jsVendorDir = 'js/vendor' |
| 239 | + assetFile(uiCatalog, logger, uiOutputDir, jsVendorDir, basename, jsVendorDir + '/' + basename, contents) |
| 240 | +} |
| 241 | + |
| 242 | +function vendorCssFile (uiCatalog, logger, uiOutputDir, requireRequest, basename = requireRequest.split('/').pop()) { |
| 243 | + let contents |
| 244 | + if (Array.isArray(requireRequest)) { |
| 245 | + const filepaths = requireRequest.map(require.resolve) |
| 246 | + contents = new LazyReadable(() => new MultiFileReadStream(filepaths)) |
| 247 | + } else { |
| 248 | + const filepath = require.resolve(requireRequest) |
| 249 | + contents = new LazyReadable(() => fs.createReadStream(filepath)) |
| 250 | + } |
| 251 | + const jsVendorDir = 'css/vendor' |
| 252 | + assetFile(uiCatalog, logger, uiOutputDir, jsVendorDir, basename, jsVendorDir + '/' + basename, contents) |
| 253 | +} |
| 254 | + |
| 255 | +module.exports = { register } |
0 commit comments