Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… into GerHobbelt-pullreq-25
  • Loading branch information
mattstyles committed Sep 29, 2015
2 parents 663e59d + 178a1da commit 71263b1
Show file tree
Hide file tree
Showing 22 changed files with 616 additions and 41 deletions.
81 changes: 79 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,91 @@ module.exports = function ( grunt ) {
bannerReplace: {
options: {
position: 'replace',
replaceContent: '// replace-this-comment',
replace: '// replace-this-comment',
banner: '// the banner'
},
files: {
src: [ 'test/tmp/someReplace.js']
}
},

bannerReplaceMultiple: {
options: {
position: 'replace',
replace: '// replace-this-comment',
banner: '// the banner'
},
files: {
src: [ 'test/tmp/someReplaceMultiple.js']
}
},

bannerReplaceToTop: {
options: {
position: 'top',
replace: '// replace-this-comment',
banner: '// the banner'
},
files: {
src: [ 'test/tmp/someReplaceToTop.js']
}
},

bannerReplaceDefaultsToTop: {
options: {
position: 'replace',
replace: '// replace-this-comment',
banner: '// the banner'
},
files: {
src: [ 'test/tmp/someReplaceDefaultsToTop.js']
}
},

bannerReplaceSmart: {
options: {
position: 'replace',
replace: true,
banner: '// the banner'
},
files: {
src: 'test/tmp/someReplaceSmart.js'
}
},

bannerReplaceSmartMore: {
options: {
position: 'replace',
replace: '// replace-this-comment',
banner: '// the banner'
},
files: {
src: 'test/tmp/someMoreReplaceSmarts.js'
}
},

bannerReplaceSmartMore2: {
options: {
position: 'replace',
replace: true,
banner: '// the banner'
},
files: {
src: 'test/tmp/someMoreReplaceSmarts2.js'
}
},

bannerReplaceSmartToBottom: {
options: {
position: 'bottom',
replace: true,
banner: '// the banner'
},
files: {
src: [ 'test/tmp/someReplaceSmartToBottom.js']
}
},

bannerNoLineBreak: {
options: {
banner: 'console.log("loaded"); ',
Expand All @@ -91,7 +168,7 @@ module.exports = function ( grunt ) {
linebreak: true
},
files: {
src: [ 'test/tmp/lineBreakTrue.js' ]
src: [ 'test/tmp/someLineBreakTrue.js' ]
}
},

Expand Down
83 changes: 78 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,69 @@ grunt.initConfig({
### Options

#### options.position

Type: `String`
Default: `'top'`
Value range: `'top'` or `'bottom'` or `'replace'`

The position to place the banner - *either* the top or bottom or in place of the contents in the desired file specified by `'replaceContent'`.
The position to place the banner - *either* the top or bottom or in place of the contents in the desired file specified by `'replaceContent'` when and existing banner is replaced by `grunt-banner`.

When ```position``` is set to `'replace'`, this *implies* ```options.replace: true``` unless that option has explicitly been set by the user already (see below).

When ```position``` is set to `'replace'` and replacement fails, i.e. no existing banner could be spotted, then `grunt-banner` falls back to regular ```position: 'top'``` | ```position: 'bottom'``` banner insertion behaviour.

In short: `grunt-banner` will always either *replace* or *add* a banner!

#### options.replace

Type: `Boolean`, `String`, `RegExp` or `Function`

The text in the specified file that the banner should replace. When ```position``` is set to `'replace'`, every occurrence of a banner (see below for more on how existing banners are located) will be replaced by the new one. When ```position``` is set to either `'top'` or `'bottom'`, then the existing banners will be removed and replaced by a single new banner at the top or bottom of the file as directed by the ```position``` setting.

These ```options.replace``` parameter types / values are supported:

+ Boolean `false` (default) - do not look for existing banners; simply add the banner at the specified position (top / bottom).

+ Boolean `true` - 'smart' replace mode: use the built-in 'smart' locate-and-mark scanner to dig out the existing banners (more on the rules what maketh a banner below).

+ (string) - replace any part of the source code which matches this *implicit regex*.

> This means most strings are matched as-is, but do not get mistaken about this: dot `.`, star `*` et al will not be the *literal characters* you might have expected, but are treated as regex operators! E.g. `replace: "/* blurb */"` will **not** work as if a literal string, since the stars `*` in there will make it match lines like `// blurb //` but **will not** match an actual C-style comment line `/* blurb */`. You will need to specify the proper regex string for that instead: `replace: "\/\* blurb \*\/"`.
> Also note that *every* regex match will be replaced by the specified banner. If your regex is not selective or precise enough, you may end up with some surprising replacements. **This is not a bug. You are responsible for providing *fitting* regexes to have `grunt-banner` match against.**
+ (RegExp) - a rexexp instance to match against. The same caveats as the (string) type value above apply.

+ (function) - provide your own callback method to locate and mark the input. The interface for the callback function is:

```
function (fileContents, newBanner, insertPositionMarker, src, options)
```

which should *return* the marked `fileContents`, i.e. the `fileContents` with all banners eligible for replacement removed and replaced by a simple `insertPositionMarker` string (see below). Your locate-and-match callback may insert *one*, *multiple* markers or *none*: `grunt-banner` will check how many markers you injected and either replace them when one or more markers are seen, or when none are found, revert to its basic `top|bottom` position-based banner *insertion* process.

The callback function parameters:

#### options.replaceContent
Type: `String` or `RegExp`
+ `fileContents` (string) - the contents of the `src` file.

+ `newBanner` (string) - the new banner to be inserted by `grunt-banner`.

> This (and the `options` parameter, see below) allows you to customize `grunt-banner` behaviour to an extreme degree, even providing your own custom *replacer* entirely: simply return your processed result with a single marker and reduce the `options.banner` to an empty string. But I digress...
+ `insertPositionMarker` (string) - the insert marker.

Currently this is the Unicode `REPLACEMENT CHARACTER` character, i.e. `\uFFFD`. We *assume* your original file content does not contain this marker already.

+ `src` (string) - the path to the file being processed.

+ `options` (object reference) - a *reference* to the current `options` object as used by `grunt-banner`.

> This **is not** the same as the `options` object you provided through your `Gruntfile`; this is a reference to the updated/augmented clone of that original as used by `grunt-banner` internally.
>
> Though the following coding practice should be frowned upon as 'side effects' are generally undesirable, you *can* tweak the `options.banner` value to suit your custom needs, for example.
**Tread with great care when you are about to *edit* attributes in this `options` object reference! The fact that you *can* doesn't mean you *should* fiddle with it!**

The text in the specified file that the banner should replace. Valid only when ```position``` is set to `'replace'`.

#### options.banner
Type: `String`
Expand All @@ -97,6 +150,24 @@ Type: `Function`
Allows the banner to be generated for each file using the output of the process function.




### The ```options.replace: true``` default locate-and-mark functionality

The default locate-and-mark process, invoked when you specify the `replace: true` option (or `position: "replace"` without any `replace:` value to go with that one) is set up to locate copyright banner comment chunks in either C or C++ style format, i.e. surrounded by `/*....*/` or single- or multiline `//` comment chunks.

The process will inspect each comment chunk which start at the **left edge** (hence we ignore all *indented* comment chunks!) and which span *entire* lines, hence ruling out any comments which are leading or trailing source code statements *on the same line*.

The last restriction placed on any 'old' banner to replace is that it **must** have the (case-**in**sensitive) word `Copyright` in there somewhere. And that word **must** be followed by a bit of non-whitespace blurb on the same line: generally a year, a name or both suffices to satisfy this last condition.

Any such 'banner' block is marked for replacement in its entirety.

> #### Warning Note:
>
> The replacer *does not* check if the *new* banner also includes the `Copyright` phrase, hence multiple applications of `grunt-banner` may lead to the later rounds of `grunt-banner` application *adding* the shiny new banner at the top (or bottom) of the source file.
>

### Usage Examples

#### Basic Usage
Expand Down Expand Up @@ -157,7 +228,9 @@ usebanner: {

### Notes

`grunt-banner` simply adds the banner to the head or foot of the files that are specified by the array passed to `files.src`, it makes no attempt to see if a banner already exists and it is up to the user to ensure that the file should not already contain a banner. To this end it is recommended to use the [grunt-contrib-clean](https://github.com/gruntjs/grunt-contrib-clean) task and only add banners to built code.
`grunt-banner` *adds* the banner to the head or foot of the files that are specified by the array passed to `files.src` unless ways to see if a banner already exists have been properly set up (```options.replace``` and/or ```position: 'replace'```).

It is up to the user to ensure that either the file should not already contain a banner or that the configured locate-and-mark means (default locate-and-mark function, user-specified regex or user-specified callback function) are sufficient to ensure that no undesired code chunk replacements may occur. To this end it is recommended to use the [grunt-contrib-clean](https://github.com/gruntjs/grunt-contrib-clean) task and only add banners to built code.


## Contributing
Expand Down
111 changes: 93 additions & 18 deletions tasks/usebanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,44 @@ var chalk = require ( 'chalk' );

module.exports = function ( grunt ) {

/* @const */ var insertPositionMarker = '\uFFFD'; // Unicode REPLACEMENT CHARACTER -- http://www.fileformat.info/info/unicode/char/fffd/index.htm

function defaultOldBannerRemover(fileContents, newBanner, insertPositionMarker /* , src, options */) {
// Find a full-lines-spanning comment with the phrase `Copyright (c) <year/name/blah>` in it, case-insensitive and `(c)` being optional.
// That will be our old banner and we kill the *entire* comment, it being C or C++ style, multiline or not.
//
// We only accept comments which start at column 1, i.e. at the left edge. Anything else is considered a minor - and thus irrelevant - comment.

// Regex for the question: do we have one line in there which starts with `Copyright <blabla>`?
// It's okay when it's preceded by some basic comment markers, but it MUST be followed by at *least*
// one(1) character of 'bla bla', whatever that blurb actually may be.
var copyright_re = /(^|\r\n|\n|\r)[\/*#|\s]*Copyright\s+[^\s\r\n]+/i;
// Regex for the question: do we have a one-or-many lines covering C comment?
var c_comment_re = /(^|\r\n|\n|\r)\/\*[^\0]*?\*\/\s*($|\r\n|\n|\r)/gi;
// Regex for the question: do we have a single or a whole *consecutive* bunch of `//` prefixed C++ style comment lines?
var cpp_comment_re = /(^|\r\n|\n|\r)(?:\/\/[^\n\r]*(?:\r\n|\n|\r))*\/\/[^\n\r]*($|\r\n|\n|\r)/gi;

function check_n_replace(match, p1, p2) {
if (copyright_re.test(match)) {
// got one!
return p1 + insertPositionMarker + p2;
}
// else: no dice! Do *not* alter:
return match;
}

// We *do* expect the exceptional case of multiple old banners (and in different formats)
// to sit in the input file: we want to kill/replace them *all*!
//
// Hence we execute both regex replacements, irrespective of whether the first replace already
// delivered a hit.
//
// To emphasize: we want *all* the banners in there and kill/replace them *all*!
fileContents = fileContents.replace(c_comment_re, check_n_replace);
fileContents = fileContents.replace(cpp_comment_re, check_n_replace);
return fileContents;
}

// Please see the Grunt documentation for more information regarding task
// creation: http://gruntjs.com/creating-tasks

Expand All @@ -21,21 +59,34 @@ module.exports = function ( grunt ) {
position: 'top',
banner: '',
linebreak: true,
process: false
process: false,
replace: false // boolean (true/false), string, RegExp or function which will filter the content before applying the new banner
});

if ( options.position !== 'top' && options.position !== 'bottom' && options.position !== 'replace') {
if ( options.position !== 'top' && options.position !== 'bottom' && options.position !== 'replace' ) {
options.position = 'top';
}

// Verify that if user wishes to replace content with a banner, that they have correctly
// supplied the content they wish to replace.
if ( options.position === 'replace' ) {
if ( ! (('replaceContent' in options)) ) {
grunt.util.error('Detected option `replace` without accompanying option `replaceContent`.');
} else if ( typeof options.replaceContent !== 'string' || ! (options.replaceContent instanceof RegExp) ) {
grunt.util.error('Detected option `replaceContent` with invalid type - type must be String or RegExp.');
if ( options.replace ) {
switch ( typeof options.replace ) {
case 'boolean':
case 'string':
case 'function':
break;

case 'object':
if ( options.replace instanceof RegExp ) {
break;
}
/* falls through */
default:
grunt.util.error('Detected option `replace` with invalid type - type must be Boolean, String, RegExp or filter Function.');
return;
}
} else {
options.replace = (options.position === 'replace');
}

var re = null;
Expand All @@ -61,19 +112,43 @@ module.exports = function ( grunt ) {
options.banner = options.process( src );
}

if ( options.position === 'replace' ) {
if ( ! (options.replaceContent instanceof RegExp) ) {
options.replaceContent = new RegExp(options.replaceContent);
var replacing_previous = false;

if ( options.replace ) {
switch ( typeof options.replace ) {
case 'boolean':
fileContents = defaultOldBannerRemover(fileContents, options.banner, insertPositionMarker, src, options);
break;

case 'function':
fileContents = options.replace(fileContents, options.banner, insertPositionMarker, src, options);
break;

case 'string':
// Treat a String-type replace spec as an implicit *global* RexExp, spanning at least one(1) entire line:
options.replace = new RegExp(options.replace, 'g');
/* falls through */
case 'object':
//assert( options.replace instanceof RegExp );
fileContents = fileContents.replace(options.replace, insertPositionMarker);
break;
}
fileContents = fileContents.replace(options.replaceContent, options.banner);
grunt.file.write( src, fileContents );
} else {
grunt.file.write( src,
options.position === 'top' ?
options.banner + linebreak + fileContents :
fileContents + linebreak + options.banner
);

replacing_previous = (fileContents.indexOf(insertPositionMarker) >= 0);
}

// - When `options.position === 'replace'` it is treated as 'top' when there's no banner to replace.
//
// - When `options.position` has another value (top|bottom) that setting will be adhered to
// and the existing banner(s) will be removed, while the new banner will be placed at the specified position.
var insertPositionMarker_re = new RegExp(insertPositionMarker, 'g');
grunt.file.write( src,
( options.position === 'replace' && replacing_previous ) ?
fileContents.replace(insertPositionMarker_re, options.banner /* + linebreak */ ) :
options.position !== 'bottom' ?
options.banner + linebreak + fileContents.replace(insertPositionMarker_re, '') :
fileContents.replace(insertPositionMarker_re, '') + linebreak + options.banner
);

grunt.verbose.writeln( 'Banner added to file ' + chalk.cyan( src ) );
}
Expand Down
Loading

0 comments on commit 71263b1

Please sign in to comment.