diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8ad0bc9 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: compile clean release setup test + +bin = node_modules/.bin + +compile: + @$(bin)/coffee --compile --output lib src + +clean: + @rm -rf node_modules + @git checkout -- lib + +release: +ifndef VERSION + $(error VERSION is undefined) +endif + @sed -i '' 's!\("version": "\)[0-9.]*\("\)!\1$(VERSION)\2!' package.json + @sed -i '' "s!\(.version = '\)[0-9.]*\('\)!\1$(VERSION)\2!" src/string-format.coffee + @make + @git add package.json src/string-format.coffee lib/string-format.js + @git commit --message $(VERSION) + @echo 'remember to run `npm publish`' + +setup: + @npm install + +test: + @$(bin)/mocha --compilers coffee:coffee-script diff --git a/README.md b/README.md index 167229d..8578747 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ greet("Steve", 1) ### Running the test suite - npm install - npm test + make setup + make test [1]: http://docs.python.org/library/stdtypes.html#str.format diff --git a/string-format.js b/lib/string-format.js similarity index 84% rename from string-format.js rename to lib/string-format.js index ebd475a..457694d 100644 --- a/string-format.js +++ b/lib/string-format.js @@ -1,10 +1,10 @@ -// Generated by CoffeeScript 1.3.3 +// Generated by CoffeeScript 1.4.0 (function() { var format, lookup, resolve, __slice = [].slice; format = String.prototype.format = function() { - var args, error, explicit, idx, implicit, + var args, explicit, idx, implicit, message, _this = this; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; if (args.length === 0) { @@ -16,7 +16,7 @@ } idx = 0; explicit = implicit = false; - error = 'cannot switch from {} to {} numbering'.format(); + message = 'cannot switch from {} to {} numbering'.format(); return this.replace(/([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, function(match, literal, key, transformer) { var fn, value, _ref, _ref1, _ref2; if (literal) { @@ -25,13 +25,13 @@ if (key.length) { explicit = true; if (implicit) { - throw error('implicit', 'explicit'); + throw new Error(message('implicit', 'explicit')); } value = (_ref = lookup(args, key)) != null ? _ref : ''; } else { implicit = true; if (explicit) { - throw error('explicit', 'implicit'); + throw new Error(message('explicit', 'implicit')); } value = (_ref1 = args[idx++]) != null ? _ref1 : ''; } @@ -68,6 +68,6 @@ format.transformers = {}; - format.version = '0.2.0'; + format.version = '0.2.1'; }).call(this); diff --git a/package.json b/package.json index 47d5953..b2fb6ee 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,23 @@ { "name": "string-format", + "version": "0.2.1", "description": "Adds a `format` method to `String.prototype`. Inspired by Python's `str.format()`.", + "author": "David Chambers ", "keywords": ["string", "formatting", "language", "util"], + "main": "./lib/string-format", "homepage": "https://github.com/davidchambers/string-format", "bugs": "https://github.com/davidchambers/string-format/issues", + "licenses": [{ + "type": "WTFPL", + "url": "https://raw.github.com/davidchambers/string-format/master/LICENSE" + }], "repository": { "type": "git", - "url": "https://github.com/davidchambers/string-format" + "url": "git://github.com/davidchambers/string-format.git" }, - "author": "David Chambers ", - "main": "string-format.js", - "version": "0.2.0", "devDependencies": { - "coffee-script": "1.3.x", - "jessie": "0.4.x" - }, - "scripts": { - "test": "node_modules/jessie/bin/jessie spec" + "coffee-script": "1.4.x", + "mocha": "1.7.x", + "should": "1.2.x" } } diff --git a/spec/spec_helper.js b/spec/spec_helper.js deleted file mode 100644 index 6bf8ea3..0000000 --- a/spec/spec_helper.js +++ /dev/null @@ -1,2 +0,0 @@ -require('coffee-script') -require('jessie').sugar() diff --git a/spec/stringformat_spec.coffee b/spec/stringformat_spec.coffee deleted file mode 100644 index 2b1b574..0000000 --- a/spec/stringformat_spec.coffee +++ /dev/null @@ -1,99 +0,0 @@ -require '../string-format' - -describe 'String::format', -> - - it 'interpolates positional arguments', -> - '{0}, you have {1} unread message{2}'.format('Holly', 2, 's') - .should_be 'Holly, you have 2 unread messages' - - it 'strips unmatched placeholders', -> - '{0}, you have {1} unread message{2}'.format('Steve', 1) - .should_be 'Steve, you have 1 unread message' - - it 'allows indexes to be omitted if they are entirely sequential', -> - '{}, you have {} unread message{}'.format('Steve', 1) - .should_be 'Steve, you have 1 unread message' - - it 'replaces all occurrences of a placeholder', -> - 'the meaning of life is {0} ({1} x {2} is also {0})'.format(42, 6, 7) - .should_be 'the meaning of life is 42 (6 x 7 is also 42)' - - it 'creates a reusable template function when invoked with no arguments', -> - explicit = '{0}, you have {1} unread message{2}'.format() - implicit = '{}, you have {} unread message{}'.format() - explicit('Holly', 2, 's').should_be 'Holly, you have 2 unread messages' - implicit('Holly', 2, 's').should_be 'Holly, you have 2 unread messages' - explicit('Steve', 1).should_be 'Steve, you have 1 unread message' - implicit('Steve', 1).should_be 'Steve, you have 1 unread message' - - it 'does not allow explicit and implicit numbering to be intermingled', -> - expect(-> '{} {0}'.format 'foo', 'bar') - .toThrow 'cannot switch from implicit to explicit numbering' - expect(-> '{1} {}'.format 'foo', 'bar') - .toThrow 'cannot switch from explicit to implicit numbering' - expect(-> '{1} {}'.format() 'foo', 'bar') - .toThrow 'cannot switch from explicit to implicit numbering' - - it 'treats "{{" and "}}" as "{" and "}"', -> - '{{ {}: "{}" }}'.format('foo', 'bar').should_be '{ foo: "bar" }' - - it 'supports property access via dot notation', -> - bobby = first_name: 'Bobby', last_name: 'Fischer' - garry = first_name: 'Garry', last_name: 'Kasparov' - '{0.first_name} {0.last_name} vs. {1.first_name} {1.last_name}'.format(bobby, garry) - .should_be 'Bobby Fischer vs. Garry Kasparov' - - it 'accepts a shorthand for properties of the first positional argument', -> - bobby = first_name: 'Bobby', last_name: 'Fischer' - '{first_name} {last_name}'.format(bobby).should_be 'Bobby Fischer' - - it 'invokes methods', -> - '{0.toLowerCase}'.format('III').should_be 'iii' - '{0.toUpperCase}'.format('iii').should_be 'III' - '{0.getFullYear}'.format(new Date '26 Apr 1984').should_be '1984' - '{pop}{pop}{pop}'.format(['one', 'two', 'three']).should_be 'threetwoone' - '{quip.toUpperCase}'.format(quip: -> 'Bazinga!').should_be 'BAZINGA!' - - String::format.transformers.s = -> 's' unless +this is 1 - - it 'applies transformers to explicit positional arguments', -> - text = '{0}, you have {1} unread message{1!s}' - text.format('Steve', 1).should_be 'Steve, you have 1 unread message' - text.format('Holly', 2).should_be 'Holly, you have 2 unread messages' - - it 'applies transformers to implicit positional arguments', -> - text = 'The Cure{!s}, The Door{!s}, The Smith{!s}' - text.format(1, 2, 3).should_be 'The Cure, The Doors, The Smiths' - - it 'applies transformers to properties of explicit positional arguments', -> - text = 'view message{0.length!s}' - text.format(new Array 1).should_be 'view message' - text.format(new Array 2).should_be 'view messages' - - it 'applies transformers to properties of implicit positional arguments', -> - text = 'view message{length!s}' - text.format(new Array 1).should_be 'view message' - text.format(new Array 2).should_be 'view messages' - - it "passes applicable tests from Python's test suite", -> - ''.format(null).should_be '' - 'abc'.format(null).should_be 'abc' - '{0}'.format('abc').should_be 'abc' - 'X{0}'.format('abc').should_be 'Xabc' - '{0}X'.format('abc').should_be 'abcX' - 'X{0}Y'.format('abc').should_be 'XabcY' - '{1}'.format(1, 'abc').should_be 'abc' - 'X{1}'.format(1, 'abc').should_be 'Xabc' - '{1}X'.format(1, 'abc').should_be 'abcX' - 'X{1}Y'.format(1, 'abc').should_be 'XabcY' - '{0}'.format(-15).should_be '-15' - '{0}{1}'.format(-15, 'abc').should_be '-15abc' - '{0}X{1}'.format(-15, 'abc').should_be '-15Xabc' - '{{'.format(null).should_be '{' - '}}'.format(null).should_be '}' - '{{}}'.format(null).should_be '{}' - '{{x}}'.format(null).should_be '{x}' - '{{{0}}}'.format(123).should_be '{123}' - '{{{{0}}}}'.format(null).should_be '{{0}}' - '}}{{'.format(null).should_be '}{' - '}}x{{'.format(null).should_be '}x{' diff --git a/string-format.coffee b/src/string-format.coffee similarity index 79% rename from string-format.coffee rename to src/string-format.coffee index 158dd78..042a1d7 100644 --- a/string-format.coffee +++ b/src/string-format.coffee @@ -5,7 +5,7 @@ format = String::format = (args...) -> idx = 0 explicit = implicit = no - error = 'cannot switch from {} to {} numbering'.format() + message = 'cannot switch from {} to {} numbering'.format() @replace \ /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, @@ -14,11 +14,11 @@ format = String::format = (args...) -> if key.length explicit = yes - throw error('implicit', 'explicit') if implicit + throw new Error message 'implicit', 'explicit' if implicit value = lookup(args, key) ? '' else implicit = yes - throw error('explicit', 'implicit') if explicit + throw new Error message 'explicit', 'implicit' if explicit value = args[idx++] ? '' value = value.toString() @@ -39,4 +39,4 @@ resolve = (object, key) -> format.transformers = {} -format.version = '0.2.0' +format.version = '0.2.1' diff --git a/test/string-format.coffee b/test/string-format.coffee new file mode 100644 index 0000000..525fc7b --- /dev/null +++ b/test/string-format.coffee @@ -0,0 +1,103 @@ +assert = require 'assert' +require 'should' + +require '../src/string-format' + + +describe 'String::format', -> + + it 'interpolates positional arguments', -> + '{0}, you have {1} unread message{2}'.format('Holly', 2, 's') + .should.equal 'Holly, you have 2 unread messages' + + it 'strips unmatched placeholders', -> + '{0}, you have {1} unread message{2}'.format('Steve', 1) + .should.equal 'Steve, you have 1 unread message' + + it 'allows indexes to be omitted if they are entirely sequential', -> + '{}, you have {} unread message{}'.format('Steve', 1) + .should.equal 'Steve, you have 1 unread message' + + it 'replaces all occurrences of a placeholder', -> + 'the meaning of life is {0} ({1} x {2} is also {0})'.format(42, 6, 7) + .should.equal 'the meaning of life is 42 (6 x 7 is also 42)' + + it 'creates a reusable template function when invoked with no arguments', -> + explicit = '{0}, you have {1} unread message{2}'.format() + implicit = '{}, you have {} unread message{}'.format() + explicit('Holly', 2, 's').should.equal 'Holly, you have 2 unread messages' + implicit('Holly', 2, 's').should.equal 'Holly, you have 2 unread messages' + explicit('Steve', 1).should.equal 'Steve, you have 1 unread message' + implicit('Steve', 1).should.equal 'Steve, you have 1 unread message' + + it 'does not allow explicit and implicit numbering to be intermingled', -> + (-> '{} {0}'.format 'foo', 'bar') + .should.throw 'cannot switch from implicit to explicit numbering' + (-> '{1} {}'.format 'foo', 'bar') + .should.throw 'cannot switch from explicit to implicit numbering' + (-> '{1} {}'.format() 'foo', 'bar') + .should.throw 'cannot switch from explicit to implicit numbering' + + it 'treats "{{" and "}}" as "{" and "}"', -> + '{{ {}: "{}" }}'.format('foo', 'bar').should.equal '{ foo: "bar" }' + + it 'supports property access via dot notation', -> + bobby = first_name: 'Bobby', last_name: 'Fischer' + garry = first_name: 'Garry', last_name: 'Kasparov' + '{0.first_name} {0.last_name} vs. {1.first_name} {1.last_name}'.format(bobby, garry) + .should.equal 'Bobby Fischer vs. Garry Kasparov' + + it 'accepts a shorthand for properties of the first positional argument', -> + bobby = first_name: 'Bobby', last_name: 'Fischer' + '{first_name} {last_name}'.format(bobby).should.equal 'Bobby Fischer' + + it 'invokes methods', -> + '{0.toLowerCase}'.format('III').should.equal 'iii' + '{0.toUpperCase}'.format('iii').should.equal 'III' + '{0.getFullYear}'.format(new Date '26 Apr 1984').should.equal '1984' + '{pop}{pop}{pop}'.format(['one', 'two', 'three']).should.equal 'threetwoone' + '{quip.toUpperCase}'.format(quip: -> 'Bazinga!').should.equal 'BAZINGA!' + + String::format.transformers.s = -> 's' unless +this is 1 + + it 'applies transformers to explicit positional arguments', -> + text = '{0}, you have {1} unread message{1!s}' + text.format('Steve', 1).should.equal 'Steve, you have 1 unread message' + text.format('Holly', 2).should.equal 'Holly, you have 2 unread messages' + + it 'applies transformers to implicit positional arguments', -> + text = 'The Cure{!s}, The Door{!s}, The Smith{!s}' + text.format(1, 2, 3).should.equal 'The Cure, The Doors, The Smiths' + + it 'applies transformers to properties of explicit positional arguments', -> + text = 'view message{0.length!s}' + text.format(new Array 1).should.equal 'view message' + text.format(new Array 2).should.equal 'view messages' + + it 'applies transformers to properties of implicit positional arguments', -> + text = 'view message{length!s}' + text.format(new Array 1).should.equal 'view message' + text.format(new Array 2).should.equal 'view messages' + + it "passes applicable tests from Python's test suite", -> + ''.format(null).should.equal '' + 'abc'.format(null).should.equal 'abc' + '{0}'.format('abc').should.equal 'abc' + 'X{0}'.format('abc').should.equal 'Xabc' + '{0}X'.format('abc').should.equal 'abcX' + 'X{0}Y'.format('abc').should.equal 'XabcY' + '{1}'.format(1, 'abc').should.equal 'abc' + 'X{1}'.format(1, 'abc').should.equal 'Xabc' + '{1}X'.format(1, 'abc').should.equal 'abcX' + 'X{1}Y'.format(1, 'abc').should.equal 'XabcY' + '{0}'.format(-15).should.equal '-15' + '{0}{1}'.format(-15, 'abc').should.equal '-15abc' + '{0}X{1}'.format(-15, 'abc').should.equal '-15Xabc' + '{{'.format(null).should.equal '{' + '}}'.format(null).should.equal '}' + '{{}}'.format(null).should.equal '{}' + '{{x}}'.format(null).should.equal '{x}' + '{{{0}}}'.format(123).should.equal '{123}' + '{{{{0}}}}'.format(null).should.equal '{{0}}' + '}}{{'.format(null).should.equal '}{' + '}}x{{'.format(null).should.equal '}x{'