diff --git a/CHANGELOG.md b/CHANGELOG.md index cfd5f2a..0798b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ * ability to embed ng-include * update javadoc with comparison with gulp-angular-templatecache +2.3.1 / 2017-09-01 +================== + * Add support for embedding style templates. Support includes CSS (default) and LESS. You can use the styleType configuration option to enable the "less" processor. + {sourceType: 'ts', styleType: 'less', styleOptions: {compress: true}} + 2.3.0 / 2016-08-08 ================== * Keep attribute quotes by default (add quotes if they missed in source code). Removing quotes caused serious of issues with bindings. You can get old behaviour by specifying config property minimize: {quotes: false} diff --git a/README.md b/README.md index fc64423..06fd10b 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ src +-hello-world |-hello-world-component.ts +-hello-world-template.html + +-hello-world-template.css ``` `hello-world-component.ts`: @@ -93,11 +94,13 @@ class Component extends Directive { controller: Controller; controllerAs: string = "vm"; templateUrl: string = "angular2-template.html"; + styleUrls: string[] = ["hello-world-template.css"] } // or @View({ ... - templateUrl: 'angular2-template.html' + templateUrl: 'angular2-template.html', + styleUrls: ["hello-world-template.css"] }) ``` @@ -109,6 +112,14 @@ class Component extends Directive { ``` +`angular2-template.css`: + +```css +.my-style { + padding-right: 4px; +} +``` + `gulpfile.js`: ```javascript @@ -129,11 +140,13 @@ class Component extends Directive { restrict: string = "E"; controller: Controller; controllerAs: string = "vm"; + styles: string[] = ['.my-style{padding-right:4px}'] template:string='{{index}}'; } // or @View({ ... + styles:['.my-style{padding-right:4px}'], template:'{{index}}' }) ``` @@ -149,6 +162,14 @@ Type: `String`. Default value: 'js'. Available values: - 'js' both for Angular 1.x syntax `templateUrl: 'path'` and Angular 2.x syntax `@View({templateUrl: 'path'})` - 'ts' additionally support Angular 2.x TypeScript syntax `class Component {templateUrl: string = 'path'}` +#### options.styleType +Type: `String`. Default value: 'css'. Available values: +- 'css' Use a CSS style processor for style URL templates. +- 'less' Use a LESS style processor for style URL templates. + +#### options.styleOptions +Type: `Object`. Options passed on to the style processor. For example `styleOptions: {compress: true}` will enabled the compression option on the style processor and embedded the compressed styles. + #### options.basePath Type: `String`. By default plugin use path specified in 'templateUrl' as a relative path to corresponding '.js' file (file with 'templateUrl'). This option allow to specify another basePath to search templates as 'basePath'+'templateUrl' diff --git a/index.js b/index.js index da885c5..c7b7458 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ var PluginError = gutil.PluginError; var ProcessorEngine = require('./lib/ProcessorEngine'); var Angular1Processor = require('./lib/Angular1Processor'); var Angular2TypeScriptTemplateProcessor = require('./lib/Angular2TypeScriptProcessor'); +var Angular2TypeScriptStyleTemplateProcessor = require('./lib/Angular2TypeScriptStylesProcessor'); var utils = require('./lib/utils'); const PLUGIN_NAME = 'gulp-angular-embed-template'; @@ -16,7 +17,7 @@ module.exports = function (options) { delete options.sourceType; switch (sourceType) { case 'ts': - options.processors = [new Angular1Processor(), new Angular2TypeScriptTemplateProcessor()]; + options.processors = [new Angular1Processor(), new Angular2TypeScriptTemplateProcessor(), new Angular2TypeScriptStyleTemplateProcessor()]; break; case 'js': default: diff --git a/lib/Angular1Processor.js b/lib/Angular1Processor.js index 688471d..3ad93b1 100644 --- a/lib/Angular1Processor.js +++ b/lib/Angular1Processor.js @@ -9,20 +9,6 @@ var RegexpProcessor = require('./RegexpProcessor'); const TEMPLATE_BEGIN = Buffer('template:\''); const TEMPLATE_END = Buffer('\''); -function escapeSingleQuotes(string) { - const ESCAPING = { - '\'': '\\\'', - '\\': '\\\\', - '\n': '\\n', - '\r': '\\r', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - return string.replace(/['\\\n\r\u2028\u2029]/g, function (character) { - return ESCAPING[character]; - }); -} - var Angular1Processor = extend(RegexpProcessor, { init : function(config) { this._super.init(config); @@ -35,8 +21,12 @@ var Angular1Processor = extend(RegexpProcessor, { } this.minimizer = new Minimize(this.config.minimize); if (!this.config.minimize.parser) { + var htmlOptions = this.config.minimize.dom || {lowerCaseAttributeNames:false}; + if (htmlOptions.lowerCaseAttributeNames === undefined) { + htmlOptions.lowerCaseAttributeNames = false; + } this.minimizer.htmlparser = new html.Parser( - new html.DomHandler(this.minimizer.emits('read')), this.config.minimize.dom || {lowerCaseAttributeNames:false} + new html.DomHandler(this.minimizer.emits('read')), htmlOptions ); } @@ -82,6 +72,7 @@ var Angular1Processor = extend(RegexpProcessor, { } } + var _this = this; var embedTemplate = this.embedTemplate.bind(this); var minimizer = this.minimizer; fs.readFile(templatePath, {encoding: this.config.templateEncoding}, function(err, templateContent) { @@ -96,7 +87,7 @@ var Angular1Processor = extend(RegexpProcessor, { return; } - var templateBuffer = Buffer(escapeSingleQuotes(minifiedContent)); + var templateBuffer = Buffer(_this.escapeSingleQuotes(minifiedContent)); cb(embedTemplate(match, templateBuffer)); }); }); @@ -108,7 +99,21 @@ var Angular1Processor = extend(RegexpProcessor, { length: match[0].length, replace: [TEMPLATE_BEGIN, templateBuffer, TEMPLATE_END] } - } + }, + + escapeSingleQuotes: function(string) { + const ESCAPING = { + '\'': '\\\'', + '\\': '\\\\', + '\n': '\\n', + '\r': '\\r', + '\u2028': '\\u2028', + '\u2029': '\\u2029' + }; + return string.replace(/['\\\n\r\u2028\u2029]/g, function (character) { + return ESCAPING[character]; + }); + } }); module.exports = Angular1Processor; diff --git a/lib/Angular2TypeScriptStylesProcessor.js b/lib/Angular2TypeScriptStylesProcessor.js new file mode 100644 index 0000000..a433867 --- /dev/null +++ b/lib/Angular2TypeScriptStylesProcessor.js @@ -0,0 +1,140 @@ +var fs = require('fs'); +var pathModule = require('path'); +var cssProcessor = require('clean-css'); +var lessProcessor = require('less'); + +var extend = require('./utils').extend; +var Angular1Processor = require('./Angular1Processor'); + +//const TEMPLATE_BEGIN = Buffer('styles:string[]=['); +const TEMPLATE_BEGIN = Buffer('styles:['); +const TEMPLATE_END = Buffer(']'); + +var Angular2TypeScriptStylesProcessor = extend(Angular1Processor, { + init : function(config) { + this._super.init(config); + + if (!this.config.styleOptions) { + this.config.styleOptions = {}; + } + + var styleType = this.config.styleType; + var styleOptions = this.config.styleOptions; + switch (styleType) { + case 'less': + this.minimizer = { + template: function(path) { + return path.replace(/\.css$/, ".less"); + }, + process: function(path, source, cb) { + var processorOptions = Object.assign({}, styleOptions); + processorOptions["filename"] = path; + lessProcessor.render(source, processorOptions, function(err, minified) { + if (err) { + cb(minified == null ? err : minified.errors, null); + return; + } + cb(null, minified.css); + }); + } + }; + break; + case 'css': + default: + this.minimizer = { + template: function(path) { + return path; + }, + process: function(path, source, cb) { + new cssProcessor(styleOptions).minify(source, function(err, minified) { + if (err) { + cb(minified.errors, null); + return; + } + cb(null, minified.styles); + }); + } + }; + } + }, + /** + * @override + */ + getPattern : function() { + // for typescript: 'styleUrls: string[] = ["template.css"]' + //return '[\'"]?styleUrls[\'"]?[\\s]*:[\\s]*string\[][\\s]*=[\\s]*(\[[^](.[^]*?)\])'; + return '[\'"]?styleUrls[\'"]?[\\s]*:[\\s]*(\[[^](.[^]*?)\])'; + }, + + /** + * Find next "styleUrls:", and try to replace url with content if template available, less then maximum size. + * This is recursive function: it call itself until one of two condition happens: + * - error happened (error emitted in pipe and stop recursive calls) + * - no 'styleUrls' left (call 'fileCallback' and stop recursive calls) + * + * @param {Object} fileContext source file content + * @param {Object} match Regexp.exec result + * @param {Function} cb to call after match replaced + * @param {Function} onErr error handler + */ + replaceMatch : function(fileContext, match, cb, onErr) { + var urls = JSON.parse(match[1].replace(/'/g, '"')); + var relativeTemplatePath = match[1]; + var templatePath = pathModule.join(fileContext.path, relativeTemplatePath); + var warnNext = function(msg) { + this.logger.warn(msg); + cb(); + }.bind(this); + var onError = this.config.skipErrors ? warnNext : onErr; + + var embedTemplate = this.embedTemplate.bind(this); + var minimizer = this.minimizer; + + var _this = this; + var templateBuffers = []; + var numFiles = urls.length; + urls.map(function (relativeTemplatePath) { + var templatePath = pathModule.join(fileContext.path, minimizer.template(relativeTemplatePath)); + _this.logger.debug('template path: %s', templatePath); + + if (_this.config.maxSize) { + var fileStat = fs.statSync(templatePath); + if (fileStat && fileStat.size > _this.config.maxSize) { + warnNext('template file "' + templatePath + '" exceeds configured max size "' + _this.config.maxSize + '" actual size is "' + fileStat.size + '"'); + return; + } + } + + fs.readFile(templatePath, {encoding: _this.config.templateEncoding}, function(err, templateContent) { + if (err) { + onError('Can\'t read template file: "' + templatePath + '". Error details: ' + err); + return; + } + minimizer.process(templatePath, templateContent, function (err, minifiedContent) { + if (err) { + onError('Error while minifying angular style template "' + templatePath + '". Error from "style minimizer" plugin: ' + err); + return; + } + var beginTmpl = templateBuffers.length == 0 ? '\'' : ',\n\''; + var num = templateBuffers.push(new Buffer(beginTmpl + _this.escapeSingleQuotes(minifiedContent) + '\'')); + if (num == numFiles) { + cb(embedTemplate(match, Buffer.concat(templateBuffers))); + } + }); + }); + }); + }, + + /** + * @override + */ + embedTemplate : function(match, templateBuffer) { + return { + start : match.index, + length: match[0].length, + replace: [TEMPLATE_BEGIN, templateBuffer, TEMPLATE_END] + } + } +}); + +module.exports = Angular2TypeScriptStylesProcessor; \ No newline at end of file diff --git a/package.json b/package.json index f6c6901..945a82e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gulp-angular-embed-templates", - "version": "2.3.0", + "version": "2.3.1", "description": "gulp plugin to include the contents of angular templates inside directive's code", "main": "index.js", "scripts": { @@ -30,7 +30,9 @@ "minimize": "^2.0.0", "through2": "^2.0.1", "htmlparser2": "~3.9.1", - "object-assign": "4.1.0" + "object-assign": "4.1.0", + "clean-css": "^4.1.7", + "less": "^2.7.2" }, "devDependencies": { "mocha": "^3.0.2", diff --git a/test/cases/angular2-less/directive.js b/test/cases/angular2-less/directive.js new file mode 100644 index 0000000..49e4a4d --- /dev/null +++ b/test/cases/angular2-less/directive.js @@ -0,0 +1,5 @@ +@Component({ + selector: "my-component", + styleUrls: ["template.css"], + directives: [ROUTER_DIRECTIVES] +}) \ No newline at end of file diff --git a/test/cases/angular2-less/embedded.js b/test/cases/angular2-less/embedded.js new file mode 100644 index 0000000..47bb39a --- /dev/null +++ b/test/cases/angular2-less/embedded.js @@ -0,0 +1,5 @@ +@Component({ + selector: "my-component", + styles:['.my-style{padding-right:4px}.my-style:hover{border:1px}'], + directives: [ROUTER_DIRECTIVES] +}) \ No newline at end of file diff --git a/test/cases/angular2-less/template.less b/test/cases/angular2-less/template.less new file mode 100644 index 0000000..7f889e3 --- /dev/null +++ b/test/cases/angular2-less/template.less @@ -0,0 +1,6 @@ +.my-style { + padding-right: 4px; + &:hover { + border: 1px; + } +} \ No newline at end of file diff --git a/test/cases/angular2-styleUrls/directive.js b/test/cases/angular2-styleUrls/directive.js new file mode 100644 index 0000000..49e4a4d --- /dev/null +++ b/test/cases/angular2-styleUrls/directive.js @@ -0,0 +1,5 @@ +@Component({ + selector: "my-component", + styleUrls: ["template.css"], + directives: [ROUTER_DIRECTIVES] +}) \ No newline at end of file diff --git a/test/cases/angular2-styleUrls/embedded.js b/test/cases/angular2-styleUrls/embedded.js new file mode 100644 index 0000000..f94ee22 --- /dev/null +++ b/test/cases/angular2-styleUrls/embedded.js @@ -0,0 +1,5 @@ +@Component({ + selector: "my-component", + styles:['.my-style{padding-right:4px}'], + directives: [ROUTER_DIRECTIVES] +}) \ No newline at end of file diff --git a/test/cases/angular2-styleUrls/template.css b/test/cases/angular2-styleUrls/template.css new file mode 100644 index 0000000..698174c --- /dev/null +++ b/test/cases/angular2-styleUrls/template.css @@ -0,0 +1,3 @@ +.my-style { + padding-right: 4px; +} \ No newline at end of file diff --git a/test/mocha.js b/test/mocha.js index 56e889c..75c5e10 100644 --- a/test/mocha.js +++ b/test/mocha.js @@ -217,5 +217,13 @@ describe('gulp-angular-embed-templates', function () { it('should allow to remove attribute quotes', function (done) { testEmbed('attr-quotes-remove', done, {minimize:{quotes: false}}); - }) + }) + + it('should embed styleUrls: path in Angular2.x just fine', function(done) { + testEmbed('angular2-styleUrls', done, {sourceType: 'ts', debug:true}); + }); + + it('should embed styleUrls with less: path in Angular2.x just fine', function(done) { + testEmbed('angular2-less', done, {sourceType: 'ts', styleType: 'less', styleOptions: {compress: true}}); + }); }); \ No newline at end of file