Skip to content

Commit

Permalink
Extended on the work done by Scotty Eckenthal (@scottyeck):
Browse files Browse the repository at this point in the history
- extended the test coverage, including some edge cases

- `options.replaceContent` now is the `options.replace` setting itself: that option now accepts the search string as it was before by `options.replaceContent`.

- augmented the replace abilities of `grunt-banner`:

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

  + when replacement fails, i.e. no existing banner could be spotted, then `grunt-banner` falls back to regular non-replacement behaviour.

    In short: `grunt-banner` will always either *replace* or *add* a banner! This fixes the edge condition in the previous work where `position: replace` would *require* a pre-existing banner or else *fail to add* a banner.

  + `options.replace` parameter now can be:

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

    + `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 a literal string, as 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 for that instead: `replace: "\/\* blurb \*\/"`.

    + (RegExp) - a rexexp instance to match against. Otherwise the same as for the (string) type value above.

    + (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 banner eligible for replacement removed and replaced by a simple `insertPositionMarker` string (see below). Your locate-and-match callback may insert multiple markers or none: `grunt-banner` will check how many markers you injected and either replace them when one(1) or more markers are seen, or revert to its basic `top|bottom` position-based banner *insertion* process when zero markers are found in your returned result.

      The callback function parameters:

      + `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* this object! The fact that you *can* doesn't mean you *should* fiddle with it!**

## The 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 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 sourcefile!
  • Loading branch information
GerHobbelt committed Sep 28, 2015
1 parent 5aa9e9e commit 173d2d6
Show file tree
Hide file tree
Showing 21 changed files with 539 additions and 36 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
112 changes: 94 additions & 18 deletions tasks/usebanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ 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) {
console.warn("test: match: ", match, "\n p1: ", p1, "\n p2: ", p2, "\n p3: ", arguments, "\nregex match: ", copyright_re.exec(match));
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 +60,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 +113,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 173d2d6

Please sign in to comment.