|
@@ -0,0 +1,301 @@
|
|
|
+'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 || {})
|
|
|
+
|
|
|
+ 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))
|
|
|
+ }
|
|
|
+}
|