Add Ability to Easily Extend string.js #57

Merged
merged 3 commits into from Sep 16, 2013

Projects

None yet

5 participants

@jeffgrann
Contributor

Making string.js More Extensible

I love string.js! However, I'd like to have the ability to easily extend it in a safe, consistent and simple manner.

Why?

Simply stated, I have requirements that string.js does not meet on its own. I'm sure that others do too. Now someone in my position could simply modify string.js and submit a pull request. But what if some of the changes really don't belong in a generic library like string.js? What if they're too specific to a particular application domain? The author, as they should, would nix those changes.

So extending string.js, just like the author has extended the native Javascript String object, is the way to go.

Why Not Just Modify the String.js Prototype?

What if you're working on a large project with several developers or using other libraries which depend on string.js and you modify string.js? Now you're in the same boat as you would be if you modified the String Object prototype. Other modules may use string.js and expect certain behavior that you have modified. Bugs galore!

For an example, one of my requirements is case insensitivity with the ability to use case sensitivity on demand. If I modify the string.js replaceAll method to make it default to case insensitive matches, other code that expects string.js to be case sensitive will break.

How?

While attempting to create such a string.js extension, I found that string.js needed to be modified in four simple ways. The good news is, none of the changes affect the module's interface. Everything works just like before. The changes just make it more generic and extensible.

1. Add an initialze function containing the current S constructor's code.

The S constructor and the new setValue method (see below) both call this new function.

2. Add a setValue method.

This method allows the constructor in a module which extends string.js to set the string value just like the S constructor without knowing how to set the necessary values (s and orig) itself.

The constructor in this other module would look something like this:

ExtendedString.prototype = S('');

ExtendedString.prototype.constructor = ExtendedString;

function ExtendedString (value) {
    this.setValue(value);
}

3. Change returned values from methods.

Instead of returning a new S object, methods must return a new object using the given object's constructor. For string.js objects, the constructor will be S. For extended objects, it will be the object's constructor (ExtendedString in the example above). This way, string.js methods will always return the same kind of object that they were given.

Doing this prevents each extended module from having to jump through hoops to pull in the string.js methods and force them to return its own kind of object. This is what string.js has to do to get the native String object methods to return string.js objects (in the Attach Native JavaScript String Properties section).

So each method that returns a new string, now does this:

return new this.constructor(s);

Instead of this:

return new S(s);

4. Set the constructor to S after setting the prototype.

When setting the prototype of S to the object containing the methods, the constructor is obliterated. So string.js objects are instanceof Object but they're not instanceof S when they should be.

Extension Example

The following code shows a string.js extension module which creates and manipulates ExtendedStrings. It modifies the contains string.js method to default to case-insensitive searches and provides callers with a way to force the search to be case sensitive.

var CASE_OPTIONS;
var parentPrototype;
var stringJSObject;

//-------------------------------------------------------------------------------------
// caseOptions
//-------------------------------------------------------------------------------------
CASE_OPTIONS =
    Object.freeze({
        CASE_INSENSITIVE : 'CASE_INSENSITIVE',
        CASE_SENSITIVE   : 'CASE_SENSITIVE'
    });

//-------------------------------------------------------------------------------------
// ExtendedStrings constructor
//-------------------------------------------------------------------------------------
stringJSObject = S('');

parentPrototype = Object.getPrototypeOf(stringJSObject);

ExtendedStrings.prototype = stringJSObject;

ExtendedStrings.prototype.constructor = ExtendedStrings;

function ExtendedStrings (value) {
    this.setValue(value);
}

//-------------------------------------------------------------------------------------
// extendedStringMaker
//-------------------------------------------------------------------------------------
function extendedStringMaker (value) {
    if (value instanceof ExtendedStrings) {
        return value;
    }

    return new ExtendedStrings(value);
};

//-------------------------------------------------------------------------------------
// contains
//-------------------------------------------------------------------------------------
ExtendedStrings.prototype.contains =
    function contains (value, caseOption) {
        if (caseOption === CASE_OPTIONS.CASE_SENSITIVE) {
            return parentPrototype.contains.call(this, value);
        }
        else {
            return parentPrototype.contains.call(this.toUpperCase(), value.toUpperCase());
        }
    };

//-------------------------------------------------------------------------------------
// Set this module's public interface.
//-------------------------------------------------------------------------------------
extendedStringMaker.CASE_OPTIONS = CASE_OPTIONS;

return extendedStringMaker;

Example usage:

extendedStringMaker('This is a test').contains('this'); // true
@jprichardson
Owner

I read over your comments and glanced over the code. It looks great. Let me review the code a bit more thoroughly tomorrow morning. This looks like this might be one of the stepping stones that I was looking for on the path to 2.0.

Thanks a lot for your contribution!

@jeffgrann
Contributor

Forgot to mention that I tested it with Safari 6.0.5 on Mac OS X 10.7.5 and it passes all tests.

Testing with Firefox 23.0.1 on Mac passes all tests except for restorePrototype. This test fails even with the current strings.js 1.5.1 (without my code changes) since Firefox 17 introduced its own endsWith method for the String object. You'll need to change the way your test code checks to make sure the String prototype has not been altered instead of checking for the existence of endsWith.

Also tried to test string.js 1.5.1 on Chrome 29.0.1547.65 on Mac and I get a blank page. Didn't look into it, so I'm not sure why. You may want to look into what is going on with your test on Chrome.

Haven't had a chance to run the test on any Windows or mobile platforms.

Have used Jasmine to test my own string.js extension module with case-insensitive methods and it passes all of my tests in all three browsers mentioned above on Mac. It's a pretty good test of the ability to create extensions using my changes to string.js. Again, I haven't had a chance to run my test on any Windows or mobile platforms.

On another note, here is a handy method I added to my string module that you may want to incorporate into string.js:

function orPlural (number, pluralSuffix, plural) {
    pluralSuffix = pluralSuffix || 's';

    return number === 1 ? this : plural ? new this.constructor(plural.toString()) : new this.constructor(this.toString() + pluralSuffix.toString());
}

It allows you to write sentences with proper singular or plural words or phrases:

'There ' + S('is').orPlural(foxCount, '', 'are').s + foxCount + S('fox').orPlural(foxCount, 'es').s + '.'

     // If foxCount is 1: There is 1 fox.
     // Otherwise: There are 5 foxes.

// It defaults to a suffix of 's' for plurals:

S('horse').orPlural(2)  // horses

// And it handles the weird plural cases:

S('goose').orPlural(7, '', 'geese')  // geese
@jprichardson
Owner

Cool, thanks. I'm going to have to push back reviewing this until Monday, Sept 16th.

@jeffgrann
Contributor

No worries. And thank you for developing string.js!

This was referenced Sep 16, 2013
@jprichardson jprichardson merged commit 24d5ccd into jprichardson:master Sep 16, 2013

1 check passed

default The Travis CI build passed
Details
@yumitsu yumitsu referenced this pull request Sep 3, 2014
Closed

Cyrillic support. #46

@zeden
zeden commented Mar 9, 2015

Hi,
how to strip all tags but not some specified tags !
thank's a lot.

@az7arul
Collaborator
az7arul commented Mar 9, 2015

I don't think there are any methods that do this. stripTags removes all tags or only the specified tags but not the other way around.

@born2net

It's worth mentioning in this post that for a normal extension (i.e.: not enhancing existing function) do:

var S = require('string')

MyS.prototype = S('');
MyS.prototype.constructor = MyS;

function MyS(val) {
  this.setValue(val);
}

MyS.prototype.toTitleCase = function() {
  var newStr = this.s.split(' ').map(function(w) { return w.substr(0, 1).toUpperCase() + w.substr(1) }).join(' ');
  return new this.constructor(newStr);
}

S = function(str) {
  return new MyS(str);
}

var w = S('a simple CCTV test').toTitleCase().s;
console.log(w)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment