├── .gitattributes ├── .gitignore ├── .npminclude ├── Gruntfile.js ├── LICENSE ├── README.md ├── docs ├── options.md ├── process.md ├── remove.md ├── reuse.md ├── scripts_styles_link.md └── sections.md ├── example.html ├── fixtures ├── css │ ├── another.less │ ├── dev.css │ ├── example.inline.css │ ├── index.inline.css │ ├── inline.css │ └── libs.css ├── scripts │ ├── app.js │ ├── libs.js │ └── main.js └── views │ ├── view-recursive.html │ ├── view1.html │ ├── view2.html │ └── view3.html ├── index.html ├── package.json ├── samples ├── example.html └── index.html └── tasks └── build-html.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | .vs/ 41 | *.suo 42 | *.user 43 | *.sln.docstates 44 | *.sln 45 | *.*proj 46 | 47 | # Build results 48 | [Dd]ebug/ 49 | [Rr]elease/ 50 | *_i.c 51 | *_p.c 52 | *.ilk 53 | *.meta 54 | *.obj 55 | *.pch 56 | *.pdb 57 | *.pgc 58 | *.pgd 59 | *.rsp 60 | *.sbr 61 | *.tlb 62 | *.tli 63 | *.tlh 64 | *.tmp 65 | *.vspscc 66 | .builds 67 | *.dotCover 68 | 69 | # Projects files 70 | *.sln 71 | *.csproj 72 | 73 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 74 | packages/ 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opensdf 81 | *.sdf 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | 87 | # ReSharper is a .NET coding add-in 88 | _ReSharper* 89 | 90 | # Installshield output folder 91 | [Ee]xpress 92 | 93 | # DocProject is a documentation generator add-in 94 | DocProject/buildhelp/ 95 | DocProject/Help/*.HxT 96 | DocProject/Help/*.HxC 97 | DocProject/Help/*.hhc 98 | DocProject/Help/*.hhk 99 | DocProject/Help/*.hhp 100 | DocProject/Help/Html2 101 | DocProject/Help/html 102 | 103 | # Click-Once directory 104 | publish 105 | 106 | # Others 107 | [Bb]in 108 | [Oo]bj 109 | sql 110 | TestResults 111 | *.Cache 112 | ClientBin 113 | stylecop.* 114 | ~$* 115 | *.dbmdl 116 | Generated_Code #added for RIA/Silverlight projects 117 | 118 | # Backup & report files from converting an old project file to a newer 119 | # Visual Studio version. Backup files are not needed, because we have git ;-) 120 | _UpgradeReport_Files/ 121 | Backup*/ 122 | UpgradeLog*.XML 123 | 124 | # NodeJS 125 | node_modules 126 | npm-debug.log 127 | 128 | ############ 129 | ## Windows 130 | ############ 131 | 132 | # Windows image file caches 133 | Thumbs.db 134 | 135 | # Folder config file 136 | Desktop.ini 137 | 138 | 139 | ############# 140 | ## Python 141 | ############# 142 | 143 | *.py[co] 144 | 145 | # Packages 146 | *.egg 147 | *.egg-info 148 | dist 149 | build 150 | eggs 151 | parts 152 | bin 153 | var 154 | sdist 155 | develop-eggs 156 | .installed.cfg 157 | 158 | # Installer logs 159 | pip-log.txt 160 | 161 | # Unit test / coverage reports 162 | .coverage 163 | .tox 164 | 165 | #Translations 166 | *.mo 167 | 168 | #Mr Developer 169 | .mr.developer.cfg 170 | 171 | # Mac crap 172 | .DS_Store 173 | -------------------------------------------------------------------------------- /.npminclude: -------------------------------------------------------------------------------- 1 | package.json 2 | tasks/build-html.js 3 | LICENSE 4 | README.md -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | fixturesPath: "fixtures", 4 | 5 | htmlbuild: { 6 | dist: { 7 | src: './*.html', 8 | dest: './samples/', 9 | options: { 10 | beautify: true, 11 | //allowUnknownTags: true, 12 | //parseTag: 'htmlbuild', 13 | // keepTags: true, 14 | relative: true, 15 | processFiles: true, 16 | scripts: { 17 | bundle: [ 18 | '<%= fixturesPath %>/scripts/*.js', 19 | '!**/main.js', 20 | ], 21 | bundle_remote: [ 22 | "//cdn.jsdelivr.net/jquery/2.1.0/jquery.min.js", 23 | "//cdn.jsdelivr.net/bootstrap/3.1.1/js/bootstrap.min.js" 24 | ], 25 | inlineapp: '<%= fixturesPath %>/scripts/app.js', 26 | main: '<%= fixturesPath %>/scripts/main.js' 27 | }, 28 | styles: { 29 | bundle: { 30 | cwd: '<%= fixturesPath %>', 31 | files: [ 32 | 'css/libs.css', 33 | 'css/dev.css', 34 | 'css/another.less' 35 | ] 36 | }, 37 | test: '<%= fixturesPath %>/css/inline.css', 38 | pageSpecific: '<%= fixturesPath %>/css/$(filename).inline.css' 39 | }, 40 | sections: { 41 | views: '<%= fixturesPath %>/views/**/*.html', 42 | templates: '<%= fixturesPath %>/templates/**/*.html', 43 | }, 44 | data: { 45 | version: "0.1.0", 46 | title: "test", 47 | }, 48 | } 49 | } 50 | } 51 | }); 52 | 53 | grunt.loadTasks('tasks'); 54 | 55 | grunt.registerTask('default', ['htmlbuild']); 56 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 SPA Tools 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 18 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 19 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build [![NPM version](https://badge.fury.io/js/grunt-html-build.png)](http://badge.fury.io/js/grunt-html-build) 2 | 3 | [Grunt][grunt] HTML Builder - Appends scripts and styles, Removes debug parts, append html partials, Template options 4 | 5 | ## Getting Started 6 | 7 | Install this grunt plugin next to your project's gruntfile with: `npm install grunt-html-build --save-dev` 8 | 9 | Then add this line to your project's `Gruntfile.js` : 10 | 11 | ```javascript 12 | grunt.loadNpmTasks('grunt-html-build'); 13 | ``` 14 | 15 | Then specify your config: ([more informations][doc-options]) 16 | 17 | ```javascript 18 | grunt.initConfig({ 19 | fixturesPath: "fixtures", 20 | 21 | htmlbuild: { 22 | dist: { 23 | src: 'index.html', 24 | dest: 'samples/', 25 | options: { 26 | beautify: true, 27 | prefix: '//some-cdn', 28 | relative: true, 29 | basePath: false, 30 | scripts: { 31 | bundle: [ 32 | '<%= fixturesPath %>/scripts/*.js', 33 | '!**/main.js', 34 | ], 35 | main: '<%= fixturesPath %>/scripts/main.js' 36 | }, 37 | styles: { 38 | bundle: [ 39 | '<%= fixturesPath %>/css/libs.css', 40 | '<%= fixturesPath %>/css/dev.css' 41 | ], 42 | test: '<%= fixturesPath %>/css/inline.css' 43 | }, 44 | sections: { 45 | views: '<%= fixturesPath %>/views/**/*.html', 46 | templates: '<%= fixturesPath %>/templates/**/*.html', 47 | layout: { 48 | header: '<%= fixturesPath %>/layout/header.html', 49 | footer: '<%= fixturesPath %>/layout/footer.html' 50 | } 51 | }, 52 | data: { 53 | // Data to pass to templates 54 | version: "0.1.0", 55 | title: "test", 56 | }, 57 | } 58 | } 59 | } 60 | }); 61 | ``` 62 | 63 | Using the configuration above, consider the following example html to see it in action: 64 | 65 | ```html 66 | 67 | 68 | grunt-html-build - Test Page 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 106 | 107 | 108 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | After running the grunt task it will be stored on the samples folder as 125 | 126 | ```html 127 | 128 | 129 | grunt-html-build - Test Page 130 | 131 | 132 | 137 | 138 | 139 |
...
140 |
...
141 |
...
142 |
...
143 | 144 | 145 | 146 | 147 | 151 | 152 | 156 | 160 | 161 | 162 | ``` 163 | 164 | There 5 types of processors: 165 | 166 | * [script][doc-scripts-styles] 167 | * append script reference from configuration to dest file. 168 | * [style][doc-scripts-styles] 169 | * append style reference from configuration to dest file. 170 | * [section][doc-sections] 171 | * append partials from configuration to dest file. 172 | * [process][doc-process] 173 | * process grunt template on the block. 174 | * [remove][doc-remove] 175 | * it will erase the whole block. 176 | 177 | [grunt]: https://github.com/gruntjs/grunt 178 | [doc-options]: https://github.com/spatools/grunt-html-build/wiki/Task-Options 179 | [doc-scripts-styles]: https://github.com/spatools/grunt-html-build/wiki/Linking-Scripts-and-Styles 180 | [doc-sections]: https://github.com/spatools/grunt-html-build/wiki/Creating-HTML-Sections 181 | [doc-process]: https://github.com/spatools/grunt-html-build/wiki/Using-HTML-as-Template 182 | [doc-remove]: https://github.com/spatools/grunt-html-build/wiki/Removing-parts 183 | [doc-reuse]: https://github.com/spatools/grunt-html-build/wiki/Creating-reusable-HTML-Layout-Template 184 | 185 | ## Release History 186 | * 0.1.0 Initial Release 187 | * 0.1.1 Cleaning, adding optional tags, using js-beautify 188 | * 0.1.2 Adding expand options to tags paths and write docs 189 | * 0.1.3 Fixing nodejs dependencies 190 | * 0.1.4 Fixing nodejs dependencies 191 | * 0.1.5 Optimize src loop / Fix js-beautify options 192 | * 0.1.6 Allow build tag customization 193 | * 0.2.0 194 | * Fix and optimisation 195 | * Allow replacing src file by built file 196 | * Allow filename in dest path 197 | * Allow prefixing src files 198 | * 0.2.1 Allow non relative file names + per file tag parameter 199 | * 0.2.2 Fix issue in options.relative 200 | * 0.3.0 201 | * Fix issue when building multiple html files using custom file globbing 202 | * Allow sub parameters in all options paths 203 | * 0.3.1 204 | * Fix issue when using prefix on Windows environment 205 | * 0.3.2 206 | * Update js-beautify dependency to 1.4.2 207 | * Remove peerDependencies to avoid versions conflict 208 | * 0.4.0 209 | * Fix bug in prefix option 210 | * Allow recursive build using section processor 211 | * 0.4.1 212 | * Fix prefix's ability to contain //, as in https:// 213 | * Add a new recursive option which can be added to section tag. 214 | * 0.4.2 215 | * Fix relative path error since 0.4.0 216 | * Add noprocess option to tags to avoid underscore processing issues. 217 | * 0.4.3 218 | * Allow remove task to be configured by using current target. 219 | * 0.5.0 220 | * Avoid javascript errors when parsing unknown tags. 221 | * Add an `allowUnknownTags` option to ignore unknown tags without failing the task. 222 | * Allow to specify `attributes` on script and styles tags. 223 | * Allow http, https or // links to be processed as links. 224 | * Automatically adapt generated `link` tag for less files. 225 | * 0.5.1 226 | * Fix issue in linefeed simplification 227 | * 0.5.2 228 | * Fix issue in relative path management 229 | * Remove deprecated undercore reference and replace by lodash dependency 230 | * Fix deep array flattening for Node.JS >= 4 231 | * 0.6.0 232 | * Add `keepTags` option to keep htmlbuild tags after build. 233 | * Fix broken inlining for scripts containing `$x`. 234 | * Upgrade `lodash` to v4 and fix template issue. 235 | * Ensure `grunt` v1.0.0 compatibility. 236 | * 0.6.1 237 | * Optimize some code parts. 238 | * Added option `basePath` allow keeping original folder structure. 239 | * 0.6.2 240 | * Fix some issues. 241 | * 0.7.0 242 | * Fix EOL inconsistent issue. 243 | * Add suffix option to customize files query parameters (for cache). 244 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Task Options 4 | 5 | grunt-html-build is a multi task grunt plugin, so it's possible to configure differents targets within htmlbuild tag. 6 | 7 | ### Example 8 | 9 | ```javascript 10 | grunt.initConfig({ 11 | htmlbuild: { 12 | dist: { 13 | src: 'index.html', 14 | dest: 'samples/', 15 | options: { 16 | beautify: true, 17 | scripts: { 18 | bundle: [ 19 | 'scripts/*.js', 20 | '!**/main.js', 21 | ], 22 | main: 'scripts/main.js' 23 | }, 24 | styles: { 25 | bundle: [ 26 | 'css/libs.css', 27 | 'css/dev.css' 28 | ], 29 | test: 'css/inline.css' 30 | }, 31 | sections: { 32 | views: 'views/**/*.html', 33 | templates: 'templates/**/*.html', 34 | }, 35 | data: { 36 | // Data to pass to templates 37 | version: "0.1.0", 38 | title: "test", 39 | }, 40 | } 41 | } 42 | } 43 | }); 44 | ``` 45 | 46 | ### src 47 | **type:** string, array | 48 | **required** 49 | 50 | Specify input files to build. 51 | Accept globbing patterns 52 | 53 | ### dest 54 | **type:** string | 55 | **optional** | 56 | **default:** './' 57 | 58 | Specify output directory to create results in 59 | 60 | ### options 61 | **type:** object | 62 | **required** 63 | 64 | Additional Options 65 | 66 | ### options.scripts, options.styles, options.sections 67 | **type:** object | 68 | **required if there is scripts or styles or sections tags in html** 69 | 70 | Object representing files to be inserted into html : 71 | 72 | #### Examples: 73 | 74 | ##### Simple configuration 75 | 76 | ```javascript 77 | scripts: { 78 | 'name1': '/path/to/file/with/**/globbing.*.ext' 79 | 'name2': [ 80 | '/path/to/file/with/**/globbing.*.ext', 81 | '!/path/to/file/to/exclude/globbing.*.ext' 82 | ] 83 | } 84 | ``` 85 | ```html 86 | 87 | 88 | 89 | 90 | 91 | ``` 92 | 93 | ##### Advanced configuration 94 | 95 | ```javascript 96 | scripts: { 97 | 'name1': { 98 | nocase: true, 99 | files: '/path/to/file/with/**/globbing.*.ext' 100 | }, 101 | 'name2': { 102 | cwd: '/path/to/file', 103 | files: [ 104 | 'with/**/globbing.*.ext', 105 | '!to/exclude/globbing.*.ext' 106 | ] 107 | } 108 | } 109 | ``` 110 | ```html 111 | 112 | 113 | 114 | 115 | 116 | ``` 117 | 118 | ### options.prefix 119 | **type :** string | 120 | **optional** | 121 | **default:** null 122 | 123 | Append this prefix to all paths in script and style references. 124 | 125 | ### options.suffix 126 | **type :** string, function | 127 | **optional** | 128 | **default:** null 129 | 130 | Append this suffix to to CSS and JS files. Could be a `function(filename, url)` which return a string. The result will be appended to the URL in the form `{url}?{suffix}` 131 | 132 | ### options.relative 133 | **type :** string | 134 | **optional** | 135 | **default:** true 136 | 137 | Make generated path relative to dest path. If this arguments is specified with false value, generated paths will be written as you configure in your Gruntfile. 138 | 139 | ### options.replace 140 | **type :** bool | 141 | **optional** | 142 | **default:** false 143 | 144 | True to replace src file instead of creating a new file. 145 | 146 | ### options.keepTags 147 | **type:** boolean | 148 | **optional** | 149 | **default:** false 150 | 151 | True to keep `htmlbuild` special tags after HTML compilation. 152 | 153 | ### options.beautify 154 | **type :** bool | 155 | **optional** | 156 | **default:** false 157 | 158 | True to beautify HTML result 159 | 160 | ### options.basePath 161 | 162 | **type :** string | 163 | **optional** | 164 | **default:** false 165 | 166 | Set to copy the whole folder structure. 167 | 168 | ```javascript 169 | grunt.initConfig({ 170 | htmlbuild: { 171 | dev: { // compile with dev options 172 | src: 'source/app/**/*.html', 173 | dest: 'tmp/', 174 | options: { 175 | basePath: 'source/', 176 | // destination path = dest + ( src with 'basePath' cut away) 177 | // sourcefile: 'source/app/customers/customer.html' 178 | // dest = 'tmp/' + 'app/customers/customer.html' 179 | // dest = 'tmp/app/customers/customer.html' 180 | } 181 | } 182 | } 183 | )}; 184 | ``` 185 | 186 | ### options.logOptionals 187 | **type :** bool | 188 | **optional** | 189 | **default:** false 190 | 191 | Log an alert in console if some optional tags are not rendered 192 | 193 | ### options.allowUnknownTags 194 | **type :** bool | 195 | **optional** | 196 | **default:** false 197 | 198 | Do not fail the task if the parser meet unknown tags. 199 | Useful when working with `grunt-usemin`. 200 | 201 | ### options.parseTag 202 | **type:** string | 203 | **optional** | 204 | **default:** 'build' 205 | 206 | Specify the html-build tag name, default is 'build'. 207 | Format : 208 | 209 | ### options.EOL 210 | **type:** string | 211 | **optional** | 212 | **default:** *autodectect* 213 | 214 | Force output EOL. If not specified, it will be detected from the input file. 215 | 216 | 217 | ### options.processFiles 218 | 219 | **type :** boolean | 220 | **optional** | 221 | **default:** false 222 | 223 | Set to true to enable files src configuration replacement. 224 | By enabling it, the following keywords will be replaced in src for each file processed during build: 225 | * `$(filename)`: Current filename (without extension). 226 | * `$(file)`: Current filename (with extension). 227 | * `$(dirname)`: Current directory name. 228 | * `$(path)`: Current file path. 229 | * `$(dir)`: Current directory path. 230 | * `$(platform)`: The result of `process.platform`. -------------------------------------------------------------------------------- /docs/process.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Using HTML as Template 4 | 5 | When using grunt-html-build, you can make some parts of HTML files to be processed as grunt template. 6 | You can use values from config and from htmlbuild's option's data as globals. 7 | 8 | ### index.html 9 | 10 | ```html 11 | 12 | 13 | 14 | <%= pkg.name %> 15 | 16 | ... 17 | 18 | 19 |

No process here

20 |

<%= noprocess %>

21 | 22 | 26 | 27 | 28 | 29 | ``` 30 | 31 | ### Gruntfile.js 32 | 33 | ```javascript 34 | grunt.initConfig({ 35 | pkg: grunt.file.readJSON("package.json"), 36 | 37 | htmlbuild: { 38 | dist: { 39 | src: 'index.html', 40 | dest: 'dist/', 41 | options: { 42 | data: { 43 | baseUrl: "http://my.prod.site.com/" 44 | } 45 | } 46 | } 47 | } 48 | }); 49 | ``` 50 | 51 | ### Result 52 | 53 | ```html 54 | 55 | 56 | YourPackageName 57 | ... 58 | 59 | 60 |

No process here

61 |

<%= noprocess %>

62 | 63 | 67 | 68 | 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/remove.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Removing parts 4 | 5 | When using grunt-html-build, you can generate a HTML page which remove some dev parts. 6 | 7 | ### index.html 8 | 9 | ```html 10 | 11 | 12 | ... 13 | 14 | 15 |
...
16 |

...

17 | 18 |

19 | 20 | 21 | 22 |

23 | 24 | 25 | 26 | ``` 27 | 28 | ### Gruntfile.js 29 | 30 | ```javascript 31 | grunt.initConfig({ 32 | htmlbuild: { 33 | dist: { 34 | src: 'index.html', 35 | dest: 'dist/' 36 | } 37 | } 38 | }); 39 | ``` 40 | 41 | ### Result 42 | 43 | ```html 44 | 45 | 46 | ... 47 | 48 | 49 |
...
50 |

...

51 |

52 | 53 | 54 | ``` 55 | 56 | ### Per target 57 | 58 | Note that __.prod-only__ is part is kept because we configured __remove__ task to remove this part only during __dev__ or __test__ target. 59 | -------------------------------------------------------------------------------- /docs/reuse.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Creating reusable HTML Layout Template 4 | 5 | When using grunt-html-build, you can reuse same HTML layout template between multiple projects. 6 | But when creating reusable HTML layout template, you must take care of some things : 7 | 8 | 1. Think of features you need en each project 9 | * Custom Title 10 | * Styles 11 | * Scripts 12 | * Views 13 | * Templates 14 | 2. Use optional attribute on optional tags 15 | * Prevent grunt from fail when no match is found for a tag in the config 16 | * Use it precautially, it prevents some errors, in a project, some parts are require, some are optional 17 | 3. Think reuse 18 | * Create some optional sections to help customize layout between projects 19 | 20 | ### index.html 21 | 22 | ```html 23 | 24 | 25 | 26 | <%= pkg.name %> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/scripts_styles_link.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Linking Scripts and Styles 4 | 5 | When using grunt-html-build, you can generate a HTML page which replace debug scripts and style links by production ones. 6 | 7 | Let's take an application directory like this : 8 | 9 | * app 10 | * module1.js 11 | * module2.js 12 | * libs 13 | * lib1.js 14 | * lib2.js 15 | * css 16 | * lib1.css 17 | * lib2.css 18 | * app.css 19 | * main.js 20 | * index.html 21 | 22 | 23 | ### index.html 24 | 25 | ```html 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | ``` 61 | 62 | ### Gruntfile.js 63 | 64 | ```javascript 65 | grunt.initConfig({ 66 | htmlbuild: { 67 | src: 'index.html', 68 | dest: 'dist/', 69 | options: { 70 | scripts: { 71 | libs: 'libs/*.js', 72 | app: 'app/*.js', 73 | main: 'main.js' 74 | }, 75 | styles: { 76 | libs: [ 77 | 'css/lib1.css', 78 | 'css/lib2.css' 79 | ], 80 | app: 'css/app.css' 81 | } 82 | } 83 | } 84 | }); 85 | ``` 86 | 87 | ### Result 88 | 89 | After grunt build, created index.html will contains links to files specified in options. 90 | 91 | ```html 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 109 | 114 | 115 | 116 | ``` 117 | 118 | ### Tag options 119 | 120 | * __optional__: Specifies that the tag can be omited from configuration. If not specified and no configuration exists for this particular tag. The task will fail. 121 | * __inline__: Specifies that the tag must render the content of files directly in the resulting HTML. 122 | * __noprocess__: Specifies that the tag content must not be processed by grunt.js templating engine. Must be used with __inline__. 123 | * __[attributes]__: Specifies attributes that will be added to the resultings tags. 124 | -------------------------------------------------------------------------------- /docs/sections.md: -------------------------------------------------------------------------------- 1 | # grunt-html-build 2 | 3 | ## Creating HTML Sections 4 | 5 | When using grunt-html-build, you can generate a HTML page using many input files. 6 | 7 | Let's take an application directory like this : 8 | 9 | * views 10 | * view1.html 11 | * view2.html 12 | * templates 13 | * tmpl1.html 14 | * tmpl2.html 15 | * index.html 16 | 17 | 18 | ### index.html 19 | 20 | ```html 21 | 22 | 23 | ... 24 | 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | ### Gruntfile.js 33 | 34 | ```javascript 35 | grunt.initConfig({ 36 | htmlbuild: { 37 | src: 'index.html', 38 | dest: 'dist/', 39 | options: { 40 | sections: { 41 | views: 'views/**/*.html', 42 | templates: 'templates/**/*.html', 43 | } 44 | } 45 | } 46 | }); 47 | ``` 48 | 49 | After grunt build process the index.html file will contain files in views and template directories. 50 | 51 | ### Recursive option 52 | 53 | If the __recursive__ option is specified in the tag, __grunt-html-build__ will recursively build every views passed in the section. 54 | 55 | Useful to create a main layout file and cut your work in little files. 56 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Specific Inline Example 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /fixtures/css/another.less: -------------------------------------------------------------------------------- 1 | .this-is-less { 2 | 3 | } -------------------------------------------------------------------------------- /fixtures/css/dev.css: -------------------------------------------------------------------------------- 1 | .this-is-not-inline { 2 | 3 | } -------------------------------------------------------------------------------- /fixtures/css/example.inline.css: -------------------------------------------------------------------------------- 1 | .this-is-example-inline { 2 | font-style: italic; 3 | } -------------------------------------------------------------------------------- /fixtures/css/index.inline.css: -------------------------------------------------------------------------------- 1 | .this-is-index-inline { 2 | background: red; 3 | } -------------------------------------------------------------------------------- /fixtures/css/inline.css: -------------------------------------------------------------------------------- 1 | .this-is-inline { 2 | font-weight: bold; 3 | } -------------------------------------------------------------------------------- /fixtures/css/libs.css: -------------------------------------------------------------------------------- 1 | .this-is-not-inline { 2 | 3 | } -------------------------------------------------------------------------------- /fixtures/scripts/app.js: -------------------------------------------------------------------------------- 1 | var version = "<%= version %>"; 2 | -------------------------------------------------------------------------------- /fixtures/scripts/libs.js: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /fixtures/scripts/main.js: -------------------------------------------------------------------------------- 1 | productionMain(); -------------------------------------------------------------------------------- /fixtures/views/view-recursive.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |

<%= title %> - <%= version %>

4 | 5 |
6 | -------------------------------------------------------------------------------- /fixtures/views/view1.html: -------------------------------------------------------------------------------- 1 | 
...
2 | -------------------------------------------------------------------------------- /fixtures/views/view2.html: -------------------------------------------------------------------------------- 1 | 
...
2 | -------------------------------------------------------------------------------- /fixtures/views/view3.html: -------------------------------------------------------------------------------- 1 | 
...
2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | grunt-html-build - Test Page 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-html-build", 3 | "description": "Grunt HTML Builder - Appends scripts and styles, Removes debug parts, append html partials, Template options", 4 | "version": "0.7.1", 5 | "homepage": "https://github.com/spatools/grunt-html-build.git", 6 | "license": "MIT", 7 | "author": { 8 | "name": "SPA Tools", 9 | "url": "http://github.com/spatools/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/spatools/grunt-html-build.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/spatools/grunt-html-build/issues" 17 | }, 18 | "scripts": { 19 | "test": "grunt --stack" 20 | }, 21 | "devDependencies": { 22 | "grunt": "^1.0.1" 23 | }, 24 | "dependencies": { 25 | "js-beautify": "^1.6.2", 26 | "lodash": "^4.12.0" 27 | }, 28 | "keywords": [ 29 | "gruntplugin", 30 | "grunt", 31 | "htmlbuild", 32 | "html", 33 | "build", 34 | "htmlgen", 35 | "generate", 36 | "link" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /samples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Specific Inline Example 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | grunt-html-build - Test Page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 21 | 26 | 27 | 28 | 29 |
30 | 31 |

32 | <%= title %> - 33 | <%= version %> 34 |

35 | 36 |
37 | 38 |
...
39 | 40 |
...
41 | 42 |
...
43 | 44 |
45 | 46 |

test - 0.1.0

47 | 48 |
49 |
...
50 |
...
51 |
...
52 | 53 | 54 | 55 | 56 | 57 | 60 | 63 | 64 | 68 | 69 | 70 | 71 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /tasks/build-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | * grunt-html-build 3 | * https://github.com/spatools/grunt-html-build 4 | * Copyright (c) 2013 SPA Tools 5 | * Code below is licensed under MIT License 6 | * 7 | * Permission is hereby granted, free of charge, to any person 8 | * obtaining a copy of this software and associated documentation 9 | * files (the "Software"), to deal in the Software without restriction, 10 | * including without limitation the rights to use, copy, modify, merge, 11 | * publish, distribute, sublicense, and/or sell copies of the Software, 12 | * and to permit persons to whom the Software is furnished to do so, 13 | * subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 23 | * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | module.exports = function (grunt) { 28 | //#region Global Properties 29 | 30 | var // Init 31 | _ = require("lodash"), 32 | URL = require("url"), 33 | path = require("path"), 34 | beautifier = require("js-beautify"), 35 | beautify = { 36 | js: beautifier.js, 37 | css: beautifier.css, 38 | html: beautifier.html 39 | }, 40 | 41 | // Tags Regular Expressions 42 | regexTagStartTemplate = "", // {} required () optional 43 | regexTagEndTemplate = "", // 44 | regexTagStart = "", 45 | regexTagEnd = "", 46 | isFileRegex = /\.(\w+){2,4}$/, 47 | processFileRegex = /\$\(([^\)]*)\)/; 48 | 49 | //#endregion 50 | 51 | //#region Private Methods 52 | 53 | function getBuildTags(content) { 54 | var lines = content.replace(/\r?\n/g, "\n").split(/\n/), 55 | tag = false, 56 | tags = [], 57 | last; 58 | 59 | lines.forEach(function (l) { 60 | var tagStart = l.match(new RegExp(regexTagStart)), 61 | tagEnd = new RegExp(regexTagEnd).test(l); 62 | 63 | if (tagStart) { 64 | tag = true; 65 | last = { 66 | type: tagStart[1], 67 | inline: !!tagStart[2], 68 | optional: !!tagStart[3], 69 | recursive: !!tagStart[4], 70 | noprocess: !!tagStart[5], 71 | name: tagStart[6], 72 | attributes: tagStart[7], 73 | lines: [] 74 | }; 75 | tags.push(last); 76 | } 77 | 78 | // switch back tag flag when endbuild 79 | if (tag && tagEnd) { 80 | last.lines.push(l); 81 | tag = false; 82 | } 83 | 84 | if (tag && last) { 85 | last.lines.push(l); 86 | } 87 | }); 88 | 89 | return tags; 90 | } 91 | function defaultProcessPath(pathes, params, opt) { //takes an array of paths and validates them 92 | var local = grunt.file.expand(opt, pathes), 93 | flattenPaths = _.isArray(pathes) ? _.flattenDeep(pathes) : pathes, 94 | remote = flattenPaths.filter(function (path) { //for loading from cdn 95 | return /^((http|https):)?(\\|\/\/)/.test(path); //is http, https, or // 96 | }); 97 | 98 | if (params.relative && opt.cwd) { 99 | local = local.map(function (src) { return path.join(opt.cwd, src); }); 100 | } 101 | 102 | return _.uniq(local.concat(remote)); 103 | } 104 | function validateBlockWithName(tag, params) { 105 | var src = params[tag.type + "s"], 106 | 107 | keys = tag.name.split("."), 108 | ln = keys.length; 109 | 110 | for (var i = 0; i < ln; i++) { 111 | src = src[keys[i]]; // Search target 112 | } 113 | 114 | if (src) { 115 | // check if we want to use current file name, if so update src where necessary 116 | if( params.useFileName && src.toString().match(/_CURRENT_FILE_NAME_/g) ) { 117 | src = src.toString().replace("_CURRENT_FILE_NAME_", processedFile); 118 | } 119 | 120 | var opt = {}, 121 | files = src; 122 | 123 | if (_.isObject(src)) { 124 | if (src.files) { 125 | opt = _.omit(src, "files"); 126 | files = src.files; 127 | } 128 | else { 129 | // if paths are named, just take values 130 | files = _.values(src); 131 | } 132 | } 133 | 134 | if (!Array.isArray(files)) { 135 | files = [files]; 136 | } 137 | 138 | if (params.processFiles) { 139 | var filesContext = params.filesContext; 140 | files = files.map(function(f) { 141 | return f.replace(processFileRegex, function(val, name) { 142 | return filesContext[name] || val; 143 | }); 144 | }); 145 | } 146 | 147 | return params.processPath(files, params, opt); 148 | } 149 | } 150 | function validateBlockAlways(tag) { 151 | return true; 152 | } 153 | 154 | function setTagRegexes(parseTag) { 155 | regexTagStart = regexTagStartTemplate.replace(/%parseTag%/, function () { return parseTag }); 156 | regexTagEnd = regexTagEndTemplate.replace(/%parseTag%/, function () { return parseTag }); 157 | } 158 | 159 | function createFilesContext(src) { 160 | return { 161 | path: src, 162 | dir: path.dirname(src), 163 | file: path.basename(src), 164 | filename: path.basename(src, path.extname(src)), 165 | dirname: path.basename(path.dirname(src)), 166 | platform: process.platform 167 | }; 168 | } 169 | 170 | //#endregion 171 | 172 | //#region Processors Methods 173 | 174 | function createTemplateData(options, src, attrs) { 175 | var extend = { 176 | src: src || "", 177 | attributes: attrs || "" 178 | }; 179 | 180 | return { 181 | data: _.extend({}, options.data, extend) 182 | }; 183 | } 184 | function processTemplate(template, options, src, attrs) { 185 | return grunt.template.process(template, createTemplateData(options, src, attrs)); 186 | } 187 | function createAttributes(options, src) { 188 | var attrs = options.attributes || ""; 189 | 190 | if (options.type === "script") { 191 | attrs = 'type="text/javascript" ' + attrs; 192 | } 193 | else if (options.type === "style" && !options.inline) { 194 | if (path.extname(src) === ".less") { 195 | attrs = 'type="text/css" rel="stylesheet/less" ' + attrs; 196 | } 197 | else { 198 | attrs = 'type="text/css" rel="stylesheet" ' + attrs; 199 | } 200 | } 201 | 202 | return attrs.trim(); 203 | } 204 | function processHtmlTagTemplate(options, src) { 205 | var template = templates[options.type + (options.inline ? "-inline" : "")], 206 | attrs = createAttributes(options, src); 207 | 208 | if (!options.inline || options.noprocess) { 209 | return template 210 | .replace(/\<\%\= (src|attributes) \%\>/g, function (match, p1) { 211 | if (p1 === "src") { 212 | return src; 213 | } 214 | else if (p1 === "attributes") { 215 | return attrs; 216 | } 217 | }); 218 | } 219 | else { 220 | return processTemplate(template, options, src, attrs); 221 | } 222 | } 223 | 224 | function processHtmlTag(options) { 225 | if (options.inline) { 226 | var content = options.files.map(grunt.file.read).join(options.EOL); 227 | return processHtmlTagTemplate(options, content); 228 | } 229 | else { 230 | var destDir = options.relative && isFileRegex.test(options.dest) ? path.dirname(options.dest) : options.dest; 231 | 232 | return options.files.map(function (f) { 233 | var url = (options.relative && !/^((http|https):)?(\\|\/\/)/.test(f)) ? path.relative(destDir, f) : f; 234 | url = url.replace(/\\/g, "/"); 235 | 236 | if (options.prefix) { 237 | url = URL.resolve(options.prefix.replace(/\\/g, "/"), url); 238 | } 239 | 240 | if (options.suffix) { 241 | var suffix = typeof options.suffix === "function" ? 242 | options.suffix(f, url) : 243 | options.suffix; 244 | 245 | if (suffix) { 246 | url += "?" + suffix; 247 | } 248 | } 249 | 250 | return processHtmlTagTemplate(options, url); 251 | }).join(options.EOL); 252 | } 253 | } 254 | 255 | //#endregion 256 | 257 | //#region Processors / Validators / Templates 258 | 259 | var 260 | templates = { 261 | 'script': '', 262 | 'script-inline': '', 263 | 'style': ' href="<%= src %>" />', 264 | 'style-inline': '' 265 | }, 266 | validators = { 267 | script: validateBlockWithName, 268 | style: validateBlockWithName, 269 | section: validateBlockWithName, 270 | 271 | process: validateBlockAlways, 272 | remove: validateBlockAlways, 273 | 274 | //base method 275 | validate: function (tag, params) { 276 | if (!validators[tag.type]) { 277 | return false; 278 | } 279 | 280 | return validators[tag.type](tag, params); 281 | } 282 | }, 283 | processors = { 284 | script: processHtmlTag, 285 | style: processHtmlTag, 286 | section: function (options) { 287 | return options.files.map(function (f) { 288 | var content = grunt.file.read(f).toString(); 289 | 290 | return options.recursive ? 291 | transformContent(content, options.params, options.dest) : 292 | content; 293 | }).join(options.EOL); 294 | }, 295 | 296 | process: function (options) { 297 | return options.lines 298 | .map(function (l) { return processTemplate(l, options); }) 299 | .join(options.EOL) 300 | .replace(new RegExp(regexTagStart), "") 301 | .replace(new RegExp(regexTagEnd), ""); 302 | }, 303 | remove: function (options) { 304 | if (!options.name) return ""; 305 | 306 | var targets = options.name.split(","); 307 | if (targets.indexOf(grunt.task.current.target) < 0) { 308 | return options.lines.join(options.EOL).replace(new RegExp(regexTagStart), "").replace(new RegExp(regexTagEnd), ""); 309 | } 310 | 311 | return ""; 312 | }, 313 | 314 | //base method 315 | transform: function (options) { 316 | return processors[options.type](options); 317 | } 318 | }; 319 | 320 | //#endregion 321 | 322 | function ensureContent(content, params) { 323 | if (!params.EOL) { 324 | var match = content.match(/\r?\n/); 325 | params.EOL = match ? match[0] : "\n"; 326 | } 327 | 328 | return content.replace(/\r?\n/g, params.EOL); 329 | } 330 | 331 | function transformContent(content, params, dest) { 332 | var tags = getBuildTags(content), 333 | config = grunt.config(); 334 | 335 | tags.forEach(function (tag) { 336 | var raw = tag.lines.join(params.EOL), 337 | result = "", prefix = "", suffix = "", 338 | tagFiles = validators.validate(tag, params); 339 | 340 | if (tagFiles) { 341 | var options = _.extend({}, tag, { 342 | data: _.extend({}, config, params.data), 343 | files: tagFiles, 344 | dest: dest, 345 | prefix: params.prefix, 346 | suffix: params.suffix, 347 | relative: params.relative, 348 | EOL: params.EOL, 349 | params: params 350 | }); 351 | 352 | result = processors.transform(options); 353 | } 354 | else if (tagFiles === false) { 355 | grunt.log.warn("Unknown tag detected: '" + tag.type + "'"); 356 | 357 | if (!params.allowUnknownTags) { 358 | grunt.fail.warn("Use 'parseTag' or 'allowUnknownTags' options to avoid this issue"); 359 | } 360 | } 361 | else if (tag.optional) { 362 | if (params.logOptionals) { 363 | grunt.log.warn("Tag with type: '" + tag.type + "' and name: '" + tag.name + "' is not configured in your Gruntfile.js but is set optional, deleting block !"); 364 | } 365 | } 366 | else { 367 | grunt.fail.warn("Tag with type '" + tag.type + "' and name: '" + tag.name + "' is not configured in your Gruntfile.js !"); 368 | } 369 | 370 | if (params.keepTags) { 371 | prefix = raw.match(new RegExp(regexTagStart + "\\s*"))[0]; 372 | suffix = raw.match(new RegExp(regexTagEnd + "\\s*"))[0]; 373 | } 374 | 375 | content = content.replace(raw, function () { return prefix + result + suffix; }); 376 | }); 377 | 378 | if (params.beautify) { 379 | content = beautify.html(content, _.isObject(params.beautify) ? params.beautify : {}); 380 | } 381 | 382 | return content; 383 | } 384 | 385 | grunt.registerMultiTask('htmlbuild', "Grunt HTML Builder - Replace scripts and styles, Removes debug parts, append html partials, Template options", function () { 386 | var params = this.options({ 387 | beautify: false, 388 | logOptionals: false, 389 | relative: true, 390 | basePath: false, 391 | keepTags: false, 392 | scripts: {}, 393 | styles: {}, 394 | sections: {}, 395 | data: {}, 396 | parseTag: 'build', 397 | processFiles: false, 398 | processPath: defaultProcessPath 399 | }); 400 | 401 | setTagRegexes(params.parseTag); 402 | 403 | this.files.forEach(function (file) { 404 | var dest = file.dest || "", 405 | destPath, content; 406 | 407 | file.src.forEach(function (src) { 408 | if (params.processFiles) { 409 | params.filesContext = createFilesContext(src); 410 | } 411 | 412 | // replace files in the same folder 413 | if (params.replace) { 414 | destPath = src; 415 | } 416 | // copy original folder structure into dest folder and compile templates 417 | else if (params.basePath || params.basePath === "") { 418 | // new path = dest + (src path without basePath at the beginning) 419 | destPath = dest + src.substring(params.basePath.length, src.length); 420 | } 421 | // Regex for path 422 | else if (isFileRegex.test(dest)) { 423 | destPath = dest; 424 | } 425 | // default: copy all files into destination 426 | else { 427 | destPath = path.join(dest, path.basename(src)); 428 | } 429 | 430 | content = grunt.file.read(src); 431 | content = ensureContent(content, params); 432 | content = transformContent(content, params, dest); 433 | 434 | // write the contents to destination 435 | grunt.file.write(destPath, content); 436 | grunt.log.ok("File " + destPath + " created !"); 437 | }); 438 | }); 439 | }); 440 | }; 441 | --------------------------------------------------------------------------------