UNPKG

x

Version:

Apply HTML transformations using attributes

186 lines (164 loc) 5.62 kB
'use strict' const path = require('path') const Denque = require('denque') const Parser = require('parse5/lib/parser') const vinylFile = require('vinyl-file') const pify = require('pify') const async = pify(require('async')) const fs = pify(require('fs')) const util = require('./lib/util') // TODO add comments module.exports = class X { constructor (opts) { this.opts = opts || {} this.opts.format = opts.format || /^x-(.+)$/ this.opts.resolvers = opts.resolvers || ['.'] this.opts.cwd = opts.cwd || process.cwd() this.opts.base = opts.base || this.opts.cwd this.opts.directives = opts.directives || {} this._resolvePathMemo = Object.create(null) this._resolveFileMemo = Object.create(null) this._parser = new Parser() this._parser._bootstrap = (document, fragmentContext) => { Parser.prototype._bootstrap.call(this._parser, document, fragmentContext) this._parser.tokenizer._isDuplicateAttr = () => false } this.ast = Symbol() this.file = Symbol() this.dirtyNodes = Symbol() this.pipeline = Symbol() this.linkAttr = Symbol() this.lastFile = Symbol() } resolvePath (filePath, extraPaths = []) { let paths = this.opts.resolvers.concat(extraPaths) let memoPath = JSON.stringify({filePath, paths}) if (memoPath in this._resolvePathMemo) { return this._resolvePathMemo[memoPath] } let fullPaths = paths.map(r => path.join(this.opts.cwd, r, filePath)) return async.detect(fullPaths, (fullPath, next) => { fs.access(fullPath, err => next(null, !err)) }) .then(result => { if (result) { return path.normalize(path.resolve(result)) } throw new Error(`Could not resolve ${filePath}`) }) } resolveFile (filePath) { if (filePath in this._resolveFileMemo) { return this._resolveFileMemo[filePath] } return (this._resolveFileMemo[filePath] = vinylFile.read(filePath, {cwd: this.opts.cwd, base: this.opts.base})) } getDirective (attrName) { return (this.opts.format.exec(attrName) || [])[1] } _initPipeline (nodes, runtime) { nodes[this.dirtyNodes] = new Denque() return async.each(util.walkNodes(nodes), (node, nextNode) => { let pipeline let ref async.filterSeries(node.attrs, (attr, nextAttr) => { let directive = this.getDirective(attr.name) if (directive) { if (directive in this.opts.directives) { if (!pipeline) { pipeline = node[this.pipeline] = new Denque() nodes[this.dirtyNodes].push(node) } pipeline.push(this.opts.directives[directive](attr.value, runtime, this)) } else { return nextNode(new Error(`Unknown directive: ${directive}`)) } } else if ((attr.name === 'src' || attr.name === 'href') && !util.isExternal(attr.value)) { node[this.linkAttr] = attr ref = attr.value } nextAttr(null, !directive) }) .then(attrs => { node.attrs = attrs if (pipeline && ref) { let parent = [] if (!ref.startsWith('/')) { parent = path.relative(this.opts.cwd, nodes[this.file].dirname) } this.resolvePath(ref, parent) .then(fullPath => this.resolveFile(fullPath)) .then(file => (node[this.lastFile] = file)) .then(() => nextNode(), err => nextNode(err)) } else { nextNode() } }) }) .then(() => nodes) } _execPipeline (nodes) { let dirtyNodes = nodes[this.dirtyNodes] if (dirtyNodes) { let node return async.whilst(() => (node = dirtyNodes.shift()), nextNode => { let current let pipeline = node[this.pipeline] if (pipeline) { async.whilst(() => (current = pipeline.shift()), execCallback => current(node, execCallback)) .then(() => nextNode(), err => nextNode(err)) } else { nextNode() } }) } } _processTargets (targets, runtime) { return async.each(targets, (target, nextTarget) => { let filePromise // FIXME like, better type checking if (typeof target === 'string') { filePromise = this.resolvePath(target) .then(fullPath => this.resolveFile(fullPath)) } else { filePromise = Promise.resolve(target) } filePromise .then(file => { runtime.output.add(file) let ast = this._parser.parse(file.contents.toString()) file[this.ast] = ast ast[this.file] = file return ast }) .then(nodes => this._initPipeline(nodes, runtime)) .then(nodes => this._execPipeline(nodes)) .then(() => nextTarget(), err => nextTarget(err)) }) } _runPostHooks (runtime) { let hook return async.whilst(() => (hook = runtime.postHooks.shift()), nextHook => hook(nextHook)) } _serializeASTs (runtime) { return async.each(runtime.output, (output, nextOutput) => { if (output[this.ast]) { output.contents = new Buffer(util.parse5.serialize(output[this.ast])) } nextOutput() }) } process (targets) { let runtime = { output: new Set(), postHooks: new Denque() } if (!Array.isArray(targets)) { targets = [targets] } return this._processTargets(targets, runtime) .then(() => this._runPostHooks(runtime)) .then(() => this._serializeASTs(runtime)) .then(() => [...runtime.output]) } }