'use strict' const replaceOperator = require('./replace-operator') const replaceFunction = require('./replace-function') const replaceVariable = require('./replace-variable') const concat = require('concat-stream') const setIndent = require('./indent') const through = require('through2') const Vinyl = require('vinyl') const PluginError = require('plugin-error') const extend = require('extend') const path = require('path') const fs = require('fs') const JSON5 = require('json5') module.exports = function(opts) { if (typeof opts === 'string') { opts = {prefix: opts} } opts = extend({}, { basepath: '@file', prefix: '@@', suffix: '', context: {}, filters: false, indent: false }, opts) if (opts.basepath !== '@file') { opts.basepath = opts.basepath === '@root' ? process.cwd() : path.resolve(opts.basepath) } var customWebRoot = !!opts.context.webRoot var includeOnceFiles = {}; function fileInclude(file, enc, cb) { if (!customWebRoot) { // built-in webRoot variable, example usage: <link rel=stylesheet href=@@webRoot/style.css> opts.context.webRoot = path.relative(path.dirname(file.path), file.base).replace(/\\/g, '/') || '.' } if (file.isNull()) { cb(null, file) } else if (file.isStream()) { file.contents.pipe(concat(function(data) { try { data = include(file, String(data)) cb(null, data) } catch (e) { cb(new PluginError('gulp-file-include', e.message)) } })) } else if (file.isBuffer()) { try { file = include(file, String(file.contents)) cb(null, file) } catch (e) { cb(new PluginError('gulp-file-include', e.message)) } } } return through.obj(fileInclude) /** * utils */ function stripCommentedIncludes(content, opts) { // remove single line html comments that use the format: <!-- @@include() --> var regex = new RegExp('<!--(.*)' + opts.prefix + '[ ]*include([\\s\\S]*?)[ ]*' + opts.suffix + '-->', 'g') return content.replace(regex, '') } function include(file, text, data, sourceFile = '') { var filebase = opts.basepath === '@file' ? path.dirname(file.path) : opts.basepath var currentFilename = path.resolve(file.base, file.path) data = extend(true, {}, opts.context, data || {}) data.content = text text = stripCommentedIncludes(text, opts) text = replaceOperator(text, { prefix: opts.prefix, suffix: opts.suffix, name: 'if', handler: conditionalHandler, sourceFile: sourceFile }) text = replaceOperator(text, { prefix: opts.prefix, suffix: opts.suffix, name: 'for', handler: forHandler, sourceFile: sourceFile }) text = replaceVariable(text, data, opts) text = replaceFunction(text, { prefix: opts.prefix, suffix: opts.suffix, name: 'include_once', handler: includeOnceHandler, sourceFile: sourceFile }) text = replaceFunction(text, { prefix: opts.prefix, suffix: opts.suffix, name: 'include', handler: includeHandler, sourceFile: sourceFile }) text = replaceFunction(text, { prefix: opts.prefix, suffix: opts.suffix, name: 'loop', handler: loopHandler, sourceFile: sourceFile }) function conditionalHandler(inst) { try { var condition = new Function('var context = this; with (context) { return ' + inst.args + '; }').call(data) // eslint-disable-line } catch (error) { throw new Error(error.message + ': ' + inst.args) } return condition ? inst.body : '' } function forHandler(inst) { var forLoop = 'for' + inst.args + ' { result+=`' + inst.body + '`; }' var condition = 'var context = this; with (context) { var result=""; ' + forLoop + ' return result; }' try { var result = new Function(condition).call(data) // eslint-disable-line } catch (error) { throw new Error(error.message + ': ' + forLoop) } return result } function includeOnceHandler(inst) { var args = /[^)"']*["']([^"']*)["'](,\s*({[\s\S]*})){0,1}\s*/.exec(inst.args) if (args) { if (typeof includeOnceFiles[inst.sourceFile] === 'undefined') { includeOnceFiles[inst.sourceFile] = []; } if (includeOnceFiles[inst.sourceFile].indexOf(args[1]) === -1) { includeOnceFiles[inst.sourceFile].push(args[1]); return includeHandler(inst) } else { return ''; } } } function includeHandler(inst) { var args = /[^)"']*["']([^"']*)["'](,\s*({[\s\S]*})){0,1}\s*/.exec(inst.args) if (args) { var includePath = path.resolve(filebase, args[1]) // for checking if we are not including the current file again if (currentFilename.toLowerCase() === includePath.toLowerCase()) { throw new Error('recursion detected in file: ' + currentFilename) } var includeContent = fs.readFileSync(includePath, 'utf-8') if (opts.indent) { includeContent = setIndent(inst.before, inst.before.length, includeContent) } // need to double each `$` to escape it in the `replace` function // includeContent = includeContent.replace(/\$/gi, '$$$$'); // apply filters on include content if (typeof opts.filters === 'object') { includeContent = applyFilters(includeContent, args.input) } var recFile = new Vinyl({ cwd: process.cwd(), base: file.base, path: includePath, contents: Buffer.from(includeContent) }) recFile = include(recFile, includeContent, args[3] ? JSON5.parse(args[3]) : {}, inst.sourceFile != '' ? inst.sourceFile : currentFilename) return String(recFile.contents) } } function loopHandler(inst) { var args = /[^)"']*["']([^"']*)["'](,\s*([\s\S]*())){0,1}\s*/.exec(inst.args) var arr = [] if (args) { // loop array in the json file if (args[3].match(/^('|")[^']|[^"]('|")$/)) { // clean filename var and define path var jsonPath = args[3].replace(/^('|")/, '').replace(/('|")$/, '') var jsonfile = path.join(file.base, jsonPath) // check if json file exists if (fs.existsSync(jsonfile)) { // make sure we are getting the updated version of the json file delete require.cache[jsonfile] arr = require(jsonfile) } else { return console.error('JSON file not exists:', jsonfile) } } else { // loop array in the function try { arr = JSON5.parse(args[3]) } catch (err) { return console.error(err, args[3]) } } if (arr) { var includePath = path.resolve(filebase, args[1]) // for checking if we are not including the current file again if (currentFilename.toLowerCase() === includePath.toLowerCase()) { throw new Error('recursion detected in file: ' + currentFilename) } var includeContent = fs.readFileSync(includePath, 'utf-8') if (opts.indent) { includeContent = setIndent(inst.before, inst.before.length, includeContent) } // apply filters on include content if (typeof opts.filters === 'object') { includeContent = applyFilters(includeContent, args.input) } var recFile = new Vinyl({ cwd: process.cwd(), base: file.base, path: includePath, contents: Buffer.from(includeContent) }) var contents = '' for (var i in arr) { if (arr.hasOwnProperty(i)) { var context = arr[i] recFile = include(recFile, includeContent, args[3] ? context : {}, inst.sourceFile != '' ? inst.sourceFile : currentFilename) // why handler dont reconize underscore? // if (typeof context == 'object' && typeof context['_key'] == 'undefined') { // context['_key'] = i; // } contents += String(recFile.contents) } } } return contents } } file.contents = Buffer.from(text) return file } function applyFilters(includeContent, match) { if (!match.match(/\)+$/)) { // nothing to filter return unchanged return includeContent } // now get the ordered list of filters var filterlist = match.split('(').slice(0, -1) filterlist = filterlist.map(function(str) { return opts.filters[str.trim()] }) // compose them together into one function var filter = filterlist.reduce(compose) // check match for filter options object var options = match.match('{([^}]*)}') // and apply the composed function to the stringified content if (options) { options = JSON5.parse(options[0]) return filter(String(includeContent), options) } else { return filter(String(includeContent)) } } } function compose(f, g) { return function(x) { return f(g(x)) } }