├── test ├── mocha.opts └── index.js ├── .npmrc ├── .gitignore ├── .config ├── .travis.yml ├── .eslintrc.json ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── MIT-LICENSE ├── README.md └── index.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui tdd 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output/ 2 | /coverage/ 3 | /node_modules/ 4 | -------------------------------------------------------------------------------- /.config: -------------------------------------------------------------------------------- 1 | repo-owner = davidchambers 2 | repo-name = string-format 3 | author-name = David Chambers 4 | version-tag-prefix = 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | - "8" 6 | - "10" 7 | before_install: 8 | - git fetch origin refs/heads/master:refs/heads/master 9 | - if [[ "$TRAVIS_PULL_REQUEST_BRANCH" ]] ; then git checkout -b "$TRAVIS_PULL_REQUEST_BRANCH" ; fi 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["./node_modules/sanctuary-style/eslint-es3.json"], 4 | "overrides": [ 5 | { 6 | "files": ["*.md"], 7 | "globals": {"format": false, "user": false}, 8 | "rules": { 9 | "comma-dangle": ["error", "always-multiline"], 10 | "max-len": ["off"], 11 | "no-unused-vars": ["error", {"varsIgnorePattern": "^(format|user)$"}], 12 | "semi": ["off"] 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 3 | Version 2, December 2004 4 | 5 | Copyright (c) 2018 David Chambers 6 | 7 | Everyone is permitted to copy and distribute verbatim or modified 8 | copies of this license document, and changing it is allowed as long 9 | as the name is changed. 10 | 11 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 12 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 13 | 14 | 0. You just DO WHAT THE FUCK YOU WANT TO. 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Note: __README.md__ is generated from comments in __index.js__. Do not modify 4 | __README.md__ directly. 5 | 6 | 1. Update local master branch: 7 | 8 | $ git checkout master 9 | $ git pull upstream master 10 | 11 | 2. Create feature branch: 12 | 13 | $ git checkout -b feature-x 14 | 15 | 3. Make one or more atomic commits, and ensure that each commit has a 16 | descriptive commit message. Commit messages should be line wrapped 17 | at 72 characters. 18 | 19 | 4. Run `npm test`, and address any errors. Preferably, fix commits in place 20 | using `git rebase` or `git commit --amend` to make the changes easier to 21 | review. 22 | 23 | 5. Push: 24 | 25 | $ git push origin feature-x 26 | 27 | 6. Open a pull request. 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-format", 3 | "version": "2.0.0", 4 | "description": "String formatting inspired by Python's str.format()", 5 | "author": "David Chambers ", 6 | "keywords": [ 7 | "string", 8 | "formatting", 9 | "language", 10 | "util" 11 | ], 12 | "homepage": "https://github.com/davidchambers/string-format", 13 | "bugs": "https://github.com/davidchambers/string-format/issues", 14 | "license": "WTFPL OR MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/davidchambers/string-format.git" 18 | }, 19 | "files": [ 20 | "LICENSE", 21 | "README.md", 22 | "index.js", 23 | "package.json" 24 | ], 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "sanctuary-scripts": "3.2.x" 28 | }, 29 | "scripts": { 30 | "doctest": "sanctuary-doctest", 31 | "lint": "sanctuary-lint", 32 | "release": "sanctuary-release", 33 | "test": "npm run lint && sanctuary-test && npm run doctest" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 David Chambers 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # string-format 2 | 3 | string-format is a small JavaScript library for formatting strings, 4 | based on Python's [`str.format()`][1]. For example: 5 | 6 | ```javascript 7 | > const user = { 8 | . firstName: 'Jane', 9 | . lastName: 'Smith', 10 | . email: 'jsmith@example.com', 11 | . } 12 | ``` 13 | 14 | ```javascript 15 | > '"{firstName} {lastName}" <{email}>'.format (user) 16 | '"Jane Smith" ' 17 | ``` 18 | 19 | The equivalent concatenation: 20 | 21 | ```javascript 22 | > '"' + user.firstName + ' ' + user.lastName + '" <' + user.email + '>' 23 | '"Jane Smith" ' 24 | ``` 25 | 26 | ### Installation 27 | 28 | #### Node 29 | 30 | 1. Install: 31 | 32 | ```console 33 | $ npm install string-format 34 | ``` 35 | 36 | 2. Require: 37 | 38 | ```javascript 39 | const format = require ('string-format') 40 | ``` 41 | 42 | #### Browser 43 | 44 | 1. Define `window.format`: 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | ### Modes 51 | 52 | string-format can be used in two modes: [function mode](#function-mode) 53 | and [method mode](#method-mode). 54 | 55 | #### Function mode 56 | 57 | ```javascript 58 | > format ('Hello, {}!', 'Alice') 59 | 'Hello, Alice!' 60 | ``` 61 | 62 | In this mode the first argument is a template string and the remaining 63 | arguments are values to be interpolated. 64 | 65 | #### Method mode 66 | 67 | ```javascript 68 | > 'Hello, {}!'.format ('Alice') 69 | 'Hello, Alice!' 70 | ``` 71 | 72 | In this mode values to be interpolated are supplied to the `format` 73 | method of a template string. This mode is not enabled by default. 74 | The method must first be defined via [`format.extend`](#format.extend): 75 | 76 | ```javascript 77 | > format.extend (String.prototype, {}) 78 | ``` 79 | 80 | `format (template, $0, $1, …, $N)` and `template.format ($0, $1, …, $N)` 81 | can then be used interchangeably. 82 | 83 | 84 | 85 | ### `format (template, $0, $1, …, $N)` 86 | 87 | Returns the result of replacing each `{…}` placeholder in the template 88 | string with its corresponding replacement. 89 | 90 | Placeholders may contain numbers which refer to positional arguments: 91 | 92 | ```javascript 93 | > '{0}, you have {1} unread message{2}'.format ('Holly', 2, 's') 94 | 'Holly, you have 2 unread messages' 95 | ``` 96 | 97 | Unmatched placeholders produce no output: 98 | 99 | ```javascript 100 | > '{0}, you have {1} unread message{2}'.format ('Steve', 1) 101 | 'Steve, you have 1 unread message' 102 | ``` 103 | 104 | A format string may reference a positional argument multiple times: 105 | 106 | ```javascript 107 | > "The name's {1}. {0} {1}.".format ('James', 'Bond') 108 | "The name's Bond. James Bond." 109 | ``` 110 | 111 | Positional arguments may be referenced implicitly: 112 | 113 | ```javascript 114 | > '{}, you have {} unread message{}'.format ('Steve', 1) 115 | 'Steve, you have 1 unread message' 116 | ``` 117 | 118 | A format string must not contain both implicit and explicit references: 119 | 120 | ```javascript 121 | > 'My name is {} {}. Do you like the name {0}?'.format ('Lemony', 'Snicket') 122 | ! ValueError: cannot switch from implicit to explicit numbering 123 | ``` 124 | 125 | `{{` and `}}` in format strings produce `{` and `}`: 126 | 127 | ```javascript 128 | > '{{}} creates an empty {} in {}'.format ('dictionary', 'Python') 129 | '{} creates an empty dictionary in Python' 130 | ``` 131 | 132 | Dot notation may be used to reference object properties: 133 | 134 | ```javascript 135 | > const bobby = {firstName: 'Bobby', lastName: 'Fischer'} 136 | > const garry = {firstName: 'Garry', lastName: 'Kasparov'} 137 | 138 | > '{0.firstName} {0.lastName} vs. {1.firstName} {1.lastName}'.format (bobby, garry) 139 | 'Bobby Fischer vs. Garry Kasparov' 140 | ``` 141 | 142 | `0.` may be omitted when referencing a property of `{0}`: 143 | 144 | ```javascript 145 | > const repo = {owner: 'davidchambers', slug: 'string-format'} 146 | 147 | > 'https://github.com/{owner}/{slug}'.format (repo) 148 | 'https://github.com/davidchambers/string-format' 149 | ``` 150 | 151 | If the referenced property is a method, it is invoked with no arguments 152 | to determine the replacement: 153 | 154 | ```javascript 155 | > const sheldon = { 156 | . firstName: 'Sheldon', 157 | . lastName: 'Cooper', 158 | . dob: new Date ('1970-01-01'), 159 | . fullName: function() { return this.firstName + ' ' + this.lastName }, 160 | . quip: function() { return 'Bazinga!' }, 161 | . } 162 | 163 | > '{fullName} was born at precisely {dob.toISOString}'.format (sheldon) 164 | 'Sheldon Cooper was born at precisely 1970-01-01T00:00:00.000Z' 165 | 166 | > "I've always wanted to go to a goth club. {quip.toUpperCase}".format (sheldon) 167 | "I've always wanted to go to a goth club. BAZINGA!" 168 | ``` 169 | 170 | 171 | 172 | ### `format.create (transformers)` 173 | 174 | This function takes an object mapping names to transformers and returns 175 | a formatting function. A transformer is applied if its name appears, 176 | prefixed with `!`, after a field name in a template string. 177 | 178 | ```javascript 179 | > const fmt = format.create ({ 180 | . escape: s => 181 | . s.replace (/[&<>"'`]/g, c => '&#' + c.charCodeAt (0) + ';'), 182 | . upper: s => 183 | . s.toUpperCase (), 184 | . }) 185 | 186 | > fmt ('Hello, {!upper}!', 'Alice') 187 | 'Hello, ALICE!' 188 | 189 | > fmt ('{name!escape}', { 190 | . name: 'Anchor & Hope', 191 | . url: 'http://anchorandhopesf.com/', 192 | . }) 193 | 'Anchor & Hope' 194 | ``` 195 | 196 | 197 | 198 | ### `format.extend (prototype, transformers)` 199 | 200 | This function takes a prototype (presumably `String.prototype`) and an 201 | object mapping names to transformers, and defines a `format` method on 202 | the prototype. A transformer is applied if its name appears, prefixed 203 | with `!`, after a field name in a template string. 204 | 205 | ```javascript 206 | > format.extend (String.prototype, { 207 | . escape: s => 208 | . s.replace (/[&<>"'`]/g, c => '&#' + c.charCodeAt (0) + ';'), 209 | . upper: s => 210 | . s.toUpperCase (), 211 | . }) 212 | 213 | > 'Hello, {!upper}!'.format ('Alice') 214 | 'Hello, ALICE!' 215 | 216 | > '{name!escape}'.format ({ 217 | . name: 'Anchor & Hope', 218 | . url: 'http://anchorandhopesf.com/', 219 | . }) 220 | 'Anchor & Hope' 221 | ``` 222 | 223 | ### Running the test suite 224 | 225 | ```console 226 | $ npm install 227 | $ npm test 228 | ``` 229 | 230 | [1]: http://docs.python.org/library/stdtypes.html#str.format 231 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require ('assert'); 4 | 5 | var format = require ('..'); 6 | 7 | 8 | var eq = assert.strictEqual; 9 | 10 | function s(num) { return num === 1 ? '' : 's'; } 11 | 12 | 13 | suite ('format', function() { 14 | 15 | test ('is a function with "create" and "extend" functions', function() { 16 | eq (typeof format, 'function'); 17 | eq (typeof format.create, 'function'); 18 | eq (typeof format.extend, 'function'); 19 | }); 20 | 21 | test ('interpolates positional arguments', function() { 22 | eq (format ('{0}, you have {1} unread message{2}', 'Holly', 2, 's'), 23 | 'Holly, you have 2 unread messages'); 24 | }); 25 | 26 | test ('strips unmatched placeholders', function() { 27 | eq (format ('{0}, you have {1} unread message{2}', 'Steve', 1), 28 | 'Steve, you have 1 unread message'); 29 | }); 30 | 31 | test ('allows indexes to be omitted if they are entirely sequential', function() { 32 | eq (format ('{}, you have {} unread message{}', 'Steve', 1), 33 | 'Steve, you have 1 unread message'); 34 | }); 35 | 36 | test ('replaces all occurrences of a placeholder', function() { 37 | eq (format ('the meaning of life is {0} ({1} x {2} is also {0})', 42, 6, 7), 38 | 'the meaning of life is 42 (6 x 7 is also 42)'); 39 | }); 40 | 41 | test ('does not allow explicit and implicit numbering to be intermingled', function() { 42 | assert.throws ( 43 | function() { format ('{} {0}', 'foo', 'bar'); }, 44 | function(err) { 45 | return err instanceof Error && 46 | err.name === 'ValueError' && 47 | err.message === 'cannot switch from ' + 48 | 'implicit to explicit numbering'; 49 | } 50 | ); 51 | assert.throws ( 52 | function() { format ('{1} {}', 'foo', 'bar'); }, 53 | function(err) { 54 | return err instanceof Error && 55 | err.name === 'ValueError' && 56 | err.message === 'cannot switch from ' + 57 | 'explicit to implicit numbering'; 58 | } 59 | ); 60 | }); 61 | 62 | test ('uses default string representations', function() { 63 | eq (format ('result: {}', null), 'result: null'); 64 | eq (format ('result: {}', undefined), 'result: undefined'); 65 | eq (format ('result: {}', [1, 2, 3]), 'result: 1,2,3'); 66 | eq (format ('result: {}', {foo: 42}), 'result: [object Object]'); 67 | }); 68 | 69 | test ('treats "{{" and "}}" as "{" and "}"', function() { 70 | eq (format ('{{ {}: "{}" }}', 'foo', 'bar'), '{ foo: "bar" }'); 71 | }); 72 | 73 | test ('supports property access via dot notation', function() { 74 | var bobby = {first: 'Bobby', last: 'Fischer'}; 75 | var garry = {first: 'Garry', last: 'Kasparov'}; 76 | eq (format ('{0.first} {0.last} vs. {1.first} {1.last}', bobby, garry), 77 | 'Bobby Fischer vs. Garry Kasparov'); 78 | }); 79 | 80 | test ('accepts a shorthand for properties of the first positional argument', function() { 81 | var bobby = {first: 'Bobby', last: 'Fischer'}; 82 | eq (format ('{first} {last}', bobby), 'Bobby Fischer'); 83 | }); 84 | 85 | test ('defaults to "" if lookup fails', function() { 86 | eq (format ('result: {foo.bar.baz}', null), 'result: '); 87 | eq (format ('result: {foo.bar.baz}', 'x'), 'result: '); 88 | eq (format ('result: {foo.bar.baz}', {}), 'result: '); 89 | eq (format ('result: {foo.bar.baz}', {foo: null}), 'result: '); 90 | eq (format ('result: {foo.bar.baz}', {foo: 'x'}), 'result: '); 91 | eq (format ('result: {foo.bar.baz}', {foo: {}}), 'result: '); 92 | eq (format ('result: {foo.bar.baz}', {foo: {bar: null}}), 'result: '); 93 | eq (format ('result: {foo.bar.baz}', {foo: {bar: 'x'}}), 'result: '); 94 | eq (format ('result: {foo.bar.baz}', {foo: {bar: {}}}), 'result: '); 95 | eq (format ('result: {foo.bar.baz}', {foo: {bar: {baz: null}}}), 'result: null'); 96 | eq (format ('result: {foo.bar.baz}', {foo: {bar: {baz: 'x'}}}), 'result: x'); 97 | eq (format ('result: {foo.bar.baz}', {foo: {bar: {baz: {}}}}), 'result: [object Object]'); 98 | }); 99 | 100 | test ('invokes methods', function() { 101 | eq (format ('{0.toLowerCase}', 'III'), 'iii'); 102 | eq (format ('{0.toUpperCase}', 'iii'), 'III'); 103 | eq (format ('{0.getFullYear}', new Date ('26 Apr 1984')), '1984'); 104 | eq (format ('{pop}{pop}{pop}', ['one', 'two', 'three']), 'threetwoone'); 105 | eq (format ('{quip.toUpperCase}', {quip: function() { return 'Bazinga!'; }}), 'BAZINGA!'); 106 | }); 107 | 108 | test ("passes applicable tests from Python's test suite", function() { 109 | eq (format (''), ''); 110 | eq (format ('abc'), 'abc'); 111 | eq (format ('{0}', 'abc'), 'abc'); 112 | eq (format ('X{0}', 'abc'), 'Xabc'); 113 | eq (format ('{0}X', 'abc'), 'abcX'); 114 | eq (format ('X{0}Y', 'abc'), 'XabcY'); 115 | eq (format ('{1}', 1, 'abc'), 'abc'); 116 | eq (format ('X{1}', 1, 'abc'), 'Xabc'); 117 | eq (format ('{1}X', 1, 'abc'), 'abcX'); 118 | eq (format ('X{1}Y', 1, 'abc'), 'XabcY'); 119 | eq (format ('{0}', -15), '-15'); 120 | eq (format ('{0}{1}', -15, 'abc'), '-15abc'); 121 | eq (format ('{0}X{1}', -15, 'abc'), '-15Xabc'); 122 | eq (format ('{{'), '{'); 123 | eq (format ('}}'), '}'); 124 | eq (format ('{{}}'), '{}'); 125 | eq (format ('{{x}}'), '{x}'); 126 | eq (format ('{{{0}}}', 123), '{123}'); 127 | eq (format ('{{{{0}}}}'), '{{0}}'); 128 | eq (format ('}}{{'), '}{'); 129 | eq (format ('}}x{{'), '}x{'); 130 | }); 131 | 132 | suite ('format.create', function() { 133 | 134 | test ('returns a format function with access to provided transformers', function() { 135 | function append(suffix) { return function(s) { return s + suffix; }; } 136 | var formatA = format.create ({x: append (' (formatA)')}); 137 | var formatB = format.create ({x: append (' (formatB)')}); 138 | 139 | eq (formatA ('{!x}', 'abc'), 'abc (formatA)'); 140 | eq (formatB ('{!x}', 'abc'), 'abc (formatB)'); 141 | }); 142 | 143 | }); 144 | 145 | test ('applies transformers to explicit positional arguments', function() { 146 | var $format = format.create ({s: s}); 147 | var text = '{0}, you have {1} unread message{1!s}'; 148 | eq ($format (text, 'Steve', 1), 'Steve, you have 1 unread message'); 149 | eq ($format (text, 'Holly', 2), 'Holly, you have 2 unread messages'); 150 | }); 151 | 152 | test ('applies transformers to implicit positional arguments', function() { 153 | var $format = format.create ({s: s}); 154 | var text = 'The Cure{!s}, The Door{!s}, The Smith{!s}'; 155 | eq ($format (text, 1, 2, 3), 'The Cure, The Doors, The Smiths'); 156 | }); 157 | 158 | test ('applies transformers to properties of explicit positional arguments', function() { 159 | var $format = format.create ({s: s}); 160 | var text = 'view message{0.length!s}'; 161 | eq ($format (text, new Array (1)), 'view message'); 162 | eq ($format (text, new Array (2)), 'view messages'); 163 | }); 164 | 165 | test ('applies transformers to properties of implicit positional arguments', function() { 166 | var $format = format.create ({s: s}); 167 | var text = 'view message{length!s}'; 168 | eq ($format (text, new Array (1)), 'view message'); 169 | eq ($format (text, new Array (2)), 'view messages'); 170 | }); 171 | 172 | test ('throws if no such transformer is defined', function() { 173 | assert.throws ( 174 | function() { format ('foo-{!toString}-baz', 'bar'); }, 175 | function(err) { 176 | return err instanceof Error && 177 | err.name === 'ValueError' && 178 | err.message === 'no transformer named "toString"'; 179 | } 180 | ); 181 | }); 182 | 183 | suite ('format.extend', function() { 184 | 185 | test ('defines String.prototype.format', function() { 186 | format.extend (String.prototype, {}); 187 | eq (typeof String.prototype.format, 'function'); 188 | eq ('Hello, {}!'.format ('Alice'), 'Hello, Alice!'); 189 | delete String.prototype.format; 190 | }); 191 | 192 | test ('defines "format" method on arbitrary object', function() { 193 | var prototype = {}; 194 | format.extend (prototype, {}); 195 | eq (typeof String.prototype.format, 'undefined'); 196 | eq (typeof prototype.format, 'function'); 197 | eq (prototype.format.call ('Hello, {}!', 'Alice'), 'Hello, Alice!'); 198 | }); 199 | 200 | }); 201 | 202 | }); 203 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | void function(global) { 2 | 3 | 'use strict'; 4 | 5 | // ValueError :: String -> Error 6 | function ValueError(message) { 7 | var err = new Error (message); 8 | err.name = 'ValueError'; 9 | return err; 10 | } 11 | 12 | // create :: Object -> String,*... -> String 13 | function create(transformers) { 14 | return function(template) { 15 | var args = Array.prototype.slice.call (arguments, 1); 16 | var idx = 0; 17 | var state = 'UNDEFINED'; 18 | 19 | return template.replace ( 20 | /([{}])\1|[{](.*?)(?:!(.+?))?[}]/g, 21 | function(match, literal, _key, xf) { 22 | if (literal != null) { 23 | return literal; 24 | } 25 | var key = _key; 26 | if (key.length > 0) { 27 | if (state === 'IMPLICIT') { 28 | throw ValueError ('cannot switch from ' + 29 | 'implicit to explicit numbering'); 30 | } 31 | state = 'EXPLICIT'; 32 | } else { 33 | if (state === 'EXPLICIT') { 34 | throw ValueError ('cannot switch from ' + 35 | 'explicit to implicit numbering'); 36 | } 37 | state = 'IMPLICIT'; 38 | key = String (idx); 39 | idx += 1; 40 | } 41 | 42 | // 1. Split the key into a lookup path. 43 | // 2. If the first path component is not an index, prepend '0'. 44 | // 3. Reduce the lookup path to a single result. If the lookup 45 | // succeeds the result is a singleton array containing the 46 | // value at the lookup path; otherwise the result is []. 47 | // 4. Unwrap the result by reducing with '' as the default value. 48 | var path = key.split ('.'); 49 | var value = (/^\d+$/.test (path[0]) ? path : ['0'].concat (path)) 50 | .reduce (function(maybe, key) { 51 | return maybe.reduce (function(_, x) { 52 | return x != null && key in Object (x) ? 53 | [typeof x[key] === 'function' ? x[key] () : x[key]] : 54 | []; 55 | }, []); 56 | }, [args]) 57 | .reduce (function(_, x) { return x; }, ''); 58 | 59 | if (xf == null) { 60 | return value; 61 | } else if (Object.prototype.hasOwnProperty.call (transformers, xf)) { 62 | return transformers[xf] (value); 63 | } else { 64 | throw ValueError ('no transformer named "' + xf + '"'); 65 | } 66 | } 67 | ); 68 | }; 69 | } 70 | 71 | // format :: String,*... -> String 72 | var format = create ({}); 73 | 74 | // format.create :: Object -> String,*... -> String 75 | format.create = create; 76 | 77 | // format.extend :: Object,Object -> () 78 | format.extend = function(prototype, transformers) { 79 | var $format = create (transformers); 80 | prototype.format = function() { 81 | var args = Array.prototype.slice.call (arguments); 82 | args.unshift (this); 83 | return $format.apply (global, args); 84 | }; 85 | }; 86 | 87 | /* istanbul ignore else */ 88 | if (typeof module !== 'undefined') { 89 | module.exports = format; 90 | } else if (typeof define === 'function' && define.amd) { 91 | define (function() { return format; }); 92 | } else { 93 | global.format = format; 94 | } 95 | 96 | /* istanbul ignore if */ 97 | if (typeof __doctest !== 'undefined') { 98 | format.extend (String.prototype, {}); 99 | } 100 | 101 | //. # string-format 102 | //. 103 | //. string-format is a small JavaScript library for formatting strings, 104 | //. based on Python's [`str.format()`][1]. For example: 105 | //. 106 | //. ```javascript 107 | //. > const user = { 108 | //. . firstName: 'Jane', 109 | //. . lastName: 'Smith', 110 | //. . email: 'jsmith@example.com', 111 | //. . } 112 | //. ``` 113 | //. 114 | //. ```javascript 115 | //. > '"{firstName} {lastName}" <{email}>'.format (user) 116 | //. '"Jane Smith" ' 117 | //. ``` 118 | //. 119 | //. The equivalent concatenation: 120 | //. 121 | //. ```javascript 122 | //. > '"' + user.firstName + ' ' + user.lastName + '" <' + user.email + '>' 123 | //. '"Jane Smith" ' 124 | //. ``` 125 | //. 126 | //. ### Installation 127 | //. 128 | //. #### Node 129 | //. 130 | //. 1. Install: 131 | //. 132 | //. ```console 133 | //. $ npm install string-format 134 | //. ``` 135 | //. 136 | //. 2. Require: 137 | //. 138 | //. ```javascript 139 | //. const format = require ('string-format') 140 | //. ``` 141 | //. 142 | //. #### Browser 143 | //. 144 | //. 1. Define `window.format`: 145 | //. 146 | //. ```html 147 | //. 148 | //. ``` 149 | //. 150 | //. ### Modes 151 | //. 152 | //. string-format can be used in two modes: [function mode](#function-mode) 153 | //. and [method mode](#method-mode). 154 | //. 155 | //. #### Function mode 156 | //. 157 | //. ```javascript 158 | //. > format ('Hello, {}!', 'Alice') 159 | //. 'Hello, Alice!' 160 | //. ``` 161 | //. 162 | //. In this mode the first argument is a template string and the remaining 163 | //. arguments are values to be interpolated. 164 | //. 165 | //. #### Method mode 166 | //. 167 | //. ```javascript 168 | //. > 'Hello, {}!'.format ('Alice') 169 | //. 'Hello, Alice!' 170 | //. ``` 171 | //. 172 | //. In this mode values to be interpolated are supplied to the `format` 173 | //. method of a template string. This mode is not enabled by default. 174 | //. The method must first be defined via [`format.extend`](#format.extend): 175 | //. 176 | //. ```javascript 177 | //. > format.extend (String.prototype, {}) 178 | //. ``` 179 | //. 180 | //. `format (template, $0, $1, …, $N)` and `template.format ($0, $1, …, $N)` 181 | //. can then be used interchangeably. 182 | //. 183 | //. 184 | //. 185 | //. ### `format (template, $0, $1, …, $N)` 186 | //. 187 | //. Returns the result of replacing each `{…}` placeholder in the template 188 | //. string with its corresponding replacement. 189 | //. 190 | //. Placeholders may contain numbers which refer to positional arguments: 191 | //. 192 | //. ```javascript 193 | //. > '{0}, you have {1} unread message{2}'.format ('Holly', 2, 's') 194 | //. 'Holly, you have 2 unread messages' 195 | //. ``` 196 | //. 197 | //. Unmatched placeholders produce no output: 198 | //. 199 | //. ```javascript 200 | //. > '{0}, you have {1} unread message{2}'.format ('Steve', 1) 201 | //. 'Steve, you have 1 unread message' 202 | //. ``` 203 | //. 204 | //. A format string may reference a positional argument multiple times: 205 | //. 206 | //. ```javascript 207 | //. > "The name's {1}. {0} {1}.".format ('James', 'Bond') 208 | //. "The name's Bond. James Bond." 209 | //. ``` 210 | //. 211 | //. Positional arguments may be referenced implicitly: 212 | //. 213 | //. ```javascript 214 | //. > '{}, you have {} unread message{}'.format ('Steve', 1) 215 | //. 'Steve, you have 1 unread message' 216 | //. ``` 217 | //. 218 | //. A format string must not contain both implicit and explicit references: 219 | //. 220 | //. ```javascript 221 | //. > 'My name is {} {}. Do you like the name {0}?'.format ('Lemony', 'Snicket') 222 | //. ! ValueError: cannot switch from implicit to explicit numbering 223 | //. ``` 224 | //. 225 | //. `{{` and `}}` in format strings produce `{` and `}`: 226 | //. 227 | //. ```javascript 228 | //. > '{{}} creates an empty {} in {}'.format ('dictionary', 'Python') 229 | //. '{} creates an empty dictionary in Python' 230 | //. ``` 231 | //. 232 | //. Dot notation may be used to reference object properties: 233 | //. 234 | //. ```javascript 235 | //. > const bobby = {firstName: 'Bobby', lastName: 'Fischer'} 236 | //. > const garry = {firstName: 'Garry', lastName: 'Kasparov'} 237 | //. 238 | //. > '{0.firstName} {0.lastName} vs. {1.firstName} {1.lastName}'.format (bobby, garry) 239 | //. 'Bobby Fischer vs. Garry Kasparov' 240 | //. ``` 241 | //. 242 | //. `0.` may be omitted when referencing a property of `{0}`: 243 | //. 244 | //. ```javascript 245 | //. > const repo = {owner: 'davidchambers', slug: 'string-format'} 246 | //. 247 | //. > 'https://github.com/{owner}/{slug}'.format (repo) 248 | //. 'https://github.com/davidchambers/string-format' 249 | //. ``` 250 | //. 251 | //. If the referenced property is a method, it is invoked with no arguments 252 | //. to determine the replacement: 253 | //. 254 | //. ```javascript 255 | //. > const sheldon = { 256 | //. . firstName: 'Sheldon', 257 | //. . lastName: 'Cooper', 258 | //. . dob: new Date ('1970-01-01'), 259 | //. . fullName: function() { return this.firstName + ' ' + this.lastName }, 260 | //. . quip: function() { return 'Bazinga!' }, 261 | //. . } 262 | //. 263 | //. > '{fullName} was born at precisely {dob.toISOString}'.format (sheldon) 264 | //. 'Sheldon Cooper was born at precisely 1970-01-01T00:00:00.000Z' 265 | //. 266 | //. > "I've always wanted to go to a goth club. {quip.toUpperCase}".format (sheldon) 267 | //. "I've always wanted to go to a goth club. BAZINGA!" 268 | //. ``` 269 | //. 270 | //. 271 | //. 272 | //. ### `format.create (transformers)` 273 | //. 274 | //. This function takes an object mapping names to transformers and returns 275 | //. a formatting function. A transformer is applied if its name appears, 276 | //. prefixed with `!`, after a field name in a template string. 277 | //. 278 | //. ```javascript 279 | //. > const fmt = format.create ({ 280 | //. . escape: s => 281 | //. . s.replace (/[&<>"'`]/g, c => '&#' + c.charCodeAt (0) + ';'), 282 | //. . upper: s => 283 | //. . s.toUpperCase (), 284 | //. . }) 285 | //. 286 | //. > fmt ('Hello, {!upper}!', 'Alice') 287 | //. 'Hello, ALICE!' 288 | //. 289 | //. > fmt ('{name!escape}', { 290 | //. . name: 'Anchor & Hope', 291 | //. . url: 'http://anchorandhopesf.com/', 292 | //. . }) 293 | //. 'Anchor & Hope' 294 | //. ``` 295 | //. 296 | //. 297 | //. 298 | //. ### `format.extend (prototype, transformers)` 299 | //. 300 | //. This function takes a prototype (presumably `String.prototype`) and an 301 | //. object mapping names to transformers, and defines a `format` method on 302 | //. the prototype. A transformer is applied if its name appears, prefixed 303 | //. with `!`, after a field name in a template string. 304 | //. 305 | //. ```javascript 306 | //. > format.extend (String.prototype, { 307 | //. . escape: s => 308 | //. . s.replace (/[&<>"'`]/g, c => '&#' + c.charCodeAt (0) + ';'), 309 | //. . upper: s => 310 | //. . s.toUpperCase (), 311 | //. . }) 312 | //. 313 | //. > 'Hello, {!upper}!'.format ('Alice') 314 | //. 'Hello, ALICE!' 315 | //. 316 | //. > '{name!escape}'.format ({ 317 | //. . name: 'Anchor & Hope', 318 | //. . url: 'http://anchorandhopesf.com/', 319 | //. . }) 320 | //. 'Anchor & Hope' 321 | //. ``` 322 | //. 323 | //. ### Running the test suite 324 | //. 325 | //. ```console 326 | //. $ npm install 327 | //. $ npm test 328 | //. ``` 329 | //. 330 | //. [1]: http://docs.python.org/library/stdtypes.html#str.format 331 | 332 | }.call (this, this); 333 | --------------------------------------------------------------------------------