├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── git_hooks └── pre-commit ├── lib ├── constraintInterpreter.js ├── errors.js ├── escaper.js ├── functionalHelpers.js ├── grouper.js ├── quantifier.js ├── regularity.js ├── specialIdentifiers.js └── wordUtils.js ├── package.json └── spec ├── regularity_spec.js └── support └── jasmine.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # For posting to coveralls.io from your local machine. 4 | # This file contains the 'repo_token', which MUST NOT 5 | # be made public! Hence, tell Git to ignore it. 6 | .coveralls.yml 7 | 8 | 9 | # This is where istanbul drops its coverage report (when run locally) 10 | coverage 11 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | # Prevent tests from being instrumented so as to not skew coverage results 3 | excludes: ["**/spec/**"] 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | # this is the default (see http://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Travis-CI-uses-npm) 6 | # install: "npm install" 7 | 8 | 9 | # this is the default (see http://docs.travis-ci.com/user/languages/javascript-with-nodejs/#Default-Test-Script) 10 | # script: "npm test" 11 | 12 | 13 | # run 'istanbul' and post coverage results to coveralls.io 14 | after_script: "npm run post-to-coveralls-io" 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | jshint: { 5 | all: ["Gruntfile.js", "lib/**/*.js", "spec/**/*.js"], 6 | options: { 7 | /* See http://jshint.com/docs/options */ 8 | node: true, 9 | jasmine: true 10 | } 11 | } 12 | }); 13 | 14 | 15 | grunt.loadNpmTasks('grunt-contrib-jshint'); 16 | 17 | grunt.registerTask('default', ['jshint']); 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Fernando Martínez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # regularity — regular expressions for humans 2 | 3 | [![Build Status][travis-image]][travis-url] 4 | [![Coverage Status][coveralls-image]][coveralls-url] 5 | [![Code Climate][codeclimate-image]][codeclimate-url] 6 | 7 | [![NPM][nodeico-image]][nodeico-url] 8 | 9 | 10 | 11 | 12 | **regularity** is a friendly regular expression builder 13 | for [Node](https://nodejs.org). 14 | It is a JavaScript port 15 | of the very fine [`regularity` Ruby gem](https://rubygems.org/gems/regularity). 16 | 17 | 18 | Regular expressions are a powerful way 19 | of matching patterns against text, 20 | but too often they are _write once, read never_. 21 | After all, who wants to try and decipher 22 | 23 | ```javascript 24 | /^(?:[0-9]){3}-(?:[A-Za-z]){2}(?:#)?(?:a|b)(?:a){2,4}\$$/i 25 | ``` 26 | 27 | when you could express it as 28 | 29 | ```javascript 30 | var Regularity = require('regularity'); 31 | var regularity = new Regularity(); 32 | 33 | var myRegexp = regularity 34 | .startWith(3, 'digits') 35 | .then('-') 36 | .then(2, 'letters') 37 | .maybe('#') 38 | .oneOf('a', 'b') 39 | .between([2, 4], 'a') 40 | .endWith('$') 41 | .insensitive() 42 | .done(); 43 | ``` 44 | 45 | While taking up a bit more space, 46 | regular expressions created using **regularity** 47 | are much more readable than their cryptic counterparts. 48 | But they are still native regular expressions! 49 | 50 | 51 | 52 | ## Installation 53 | 54 | To get **regularity**, 55 | you need to have [npm](https://www.npmjs.com/) installed. 56 | It should have been installed 57 | along with [Node](https://nodejs.org). 58 | Once you have it, just run 59 | 60 | ``` 61 | $ npm install regularity 62 | ``` 63 | 64 | in your terminal, 65 | and it should promptly download 66 | into the current folder. 67 | 68 | 69 | 70 | ## Usage 71 | 72 | When you [require](https://nodejs.org/api/modules.html#modules_modules) **regularity**, 73 | all you get is a constructor function. 74 | To start building a regular expression, 75 | you should instantiate an object using `new`, 76 | as demonstrated above, 77 | and then call 78 | any of the methods it provides 79 | with the proper arguments. 80 | 81 | When you are done building the regular expression, 82 | tell **regularity** so by calling [`done`](#done). 83 | This call will return a native [`RegExp`][regexp-mdn] 84 | implementing the pattern which you described 85 | using the methods of the **regularity** object, 86 | which you can then use 87 | for your own purposes. 88 | 89 | Notice that you should probably use 90 | one new **regularity** instance 91 | for each regular expression 92 | that you want to build. 93 | If you keep calling methods 94 | on an existing **regularity** instance, 95 | you will be reusing 96 | the declarations you made on that object before. 97 | 98 | 99 | 100 | ## Documentation 101 | 102 | **regularity** instances expose a set of methods 103 | (the _[DSL](https://en.wikipedia.org/wiki/Domain-specific_language) methods_) 104 | which allow you to declaratively build 105 | the regular expression you want. 106 | They all return `this`, 107 | so they are chainable. 108 | Notice that the order in which you call these methods 109 | determines the order in which the pattern is assembled. 110 | 111 | All _DSL methods_ accept at least 112 | one of the following signatures: 113 | either an _unnumbered constraint_, 114 | which is expected to be a single string, 115 | such as `then('xyz')`, 116 | or a _numbered constraint_, 117 | composed by a count and a pattern, 118 | such as `atLeast(2, 'ab')`. 119 | 120 | In addition, the following _special identifers_ 121 | are supported as a shorthand 122 | for some common patterns: 123 | 124 | ```javascript 125 | 'digit' : '[0-9]' 126 | 'lowercase' : '[a-z]' 127 | 'uppercase' : '[A-Z]' 128 | 'letter' : '[A-Za-z]' 129 | 'alphanumeric' : '[A-Za-z0-9]' 130 | 'whitespace' : '\s' 131 | 'space' : ' ' 132 | 'tab' : '\t' 133 | ``` 134 | 135 | _Special identifiers_ may be pluralized, 136 | and **regularity** will still understand them. 137 | This allows you 138 | to write more meaningful declarations, 139 | because `then(2, 'letters')` works 140 | in addition to `then(1, 'letter')`. 141 | 142 | 143 | The following is a more detailed explanation 144 | of all the _DSL methods_ and their signatures. 145 | Should you have any doubts, 146 | please refer to the [spec](./spec/regularity_spec.js), 147 | where you can find examples 148 | of all the supported use cases. 149 | 150 | Bear in mind that, in what follows, 151 | `pattern` stands for any string, 152 | which might or might not be 153 | any of the _special identifiers_, 154 | and which might include characters 155 | which need escaping (you don't need 156 | to escape them yourself, as **regularity** 157 | will take care of that), 158 | and `n` stands for any positive integer 159 | (that is, any integer 160 | greater than or equal to `1`). 161 | Where `n` is optional 162 | (denoted by `[n,]` in the signature), 163 | passing `1` as `n` 164 | is equivalent to not passing `n` at all. 165 | 166 | - [**`startWith([n,] pattern)`**](#startWith): 167 | Require that `pattern` occur 168 | exactly `n` times 169 | at the beginning of the input. 170 | This method may be called only once. 171 | 172 | - [**`append([n,] pattern)`**](#append): 173 | Require that `pattern` occur 174 | exactly `n` times 175 | after what has been declared so far 176 | and before anything that is declared afterwards. 177 | 178 | - [**`then([n,] pattern)`**](#then): 179 | This is just an alias for [**`append`**](#append). 180 | 181 | - [**`endWith([n,] pattern)`**](#endWith): 182 | Require that `pattern` occur 183 | exactly `n` times 184 | at the end of the input. 185 | This method may be called only once. 186 | 187 | 188 | - [**`maybe(pattern)`**](#maybe): 189 | Require that `pattern` occur 190 | either one or zero times. 191 | 192 | - [**`oneOf(firstPattern[, secondPattern[, ...]])`**](#oneOf): 193 | Require that at least 194 | one of the passed `pattern`s occur. 195 | 196 | - [**`between(range, pattern)`**](#between): 197 | Require that `pattern` occur 198 | a number of consecutive times 199 | between `range[0]` and `range[1]`, both included. 200 | `range` is expected to be an array 201 | containing two positive integers. 202 | 203 | - [**`zeroOrMore(pattern)`**](#zeroOrMore): 204 | Require that `pattern` occur 205 | any number of consecutive times, 206 | including zero times. 207 | 208 | - [**`oneOrMore(pattern)`**](#oneOrMore): 209 | Require that `pattern` occur 210 | consecutively at least once. 211 | 212 | - [**`atLeast(n, pattern)`**](#atLeast): 213 | Require that `pattern` occur 214 | consecutively at least `n` times. 215 | Typically, here `n` should be greater than `1` 216 | (if you want it to be exactly `1`, you should use [**`oneOrMore`**](#oneOrMore)). 217 | 218 | - [**`atMost(n, pattern)`**](#atMost): 219 | Require that `pattern` occur 220 | consecutively at most `n` times. 221 | Typically, here `n` should be greater than `1` 222 | (if you want it to be exactly `1`, you should use [**`maybe`**](#maybe)). 223 | 224 | 225 | 226 | Besides the _DSL methods_, **regularity** instances 227 | also expose the following methods: 228 | 229 | - [**`insensitive()`**](#insensitive): 230 | Specify that the regular expression 231 | mustn't distinguish 232 | between uppercacase and lowercase letters. 233 | 234 | - [**`global()`**](#global): 235 | Specify that the regular expression 236 | must match against all possible matches in the string 237 | (instead of matching just the first, 238 | which is the default behaviour). 239 | 240 | - [**`multiline()`**](#multiline): 241 | Specify that the string 242 | against which the [`RegExp`][regexp-mdn] is to be matched 243 | may span multiple lines. 244 | 245 | - [**`done()`**](#done): 246 | Return the native [`RegExp`][regexp-mdn] object 247 | representing the pattern which you described 248 | by means of the previous calls 249 | on that **regularity** instance. 250 | 251 | - [**`regexp()`**](#regexp): 252 | This is just an alias for [**`done`**](#done). 253 | 254 | 255 | 256 | ## Credits 257 | 258 | Original idea and [Ruby](https://rubygems.org/gems/regularity) 259 | [implementation](https://github.com/andrewberls/regularity) 260 | are by [Andrew Berls](https://github.com/andrewberls/). 261 | 262 | If you are unsure about the [`RegExp`][regexp-mdn] 263 | you just built, [`regulex`](https://jex.im/regulex) 264 | is a great tool which will draw you 265 | a fancy _railroad diagram_ of it. 266 | 267 | 268 | ## License 269 | 270 | This project is licensed under the 271 | [MIT License](http://opensource.org/licenses/MIT). 272 | For more details, see the [`LICENSE`](./LICENSE) file 273 | at the root of the repository. 274 | 275 | 276 | 277 | [travis-image]: https://travis-ci.org/angelsanz/regularity.svg?branch=master 278 | [travis-url]: https://travis-ci.org/angelsanz/regularity 279 | [coveralls-image]: https://coveralls.io/repos/angelsanz/regularity/badge.svg?branch=master 280 | [coveralls-url]: https://coveralls.io/r/angelsanz/regularity?branch=master 281 | [codeclimate-image]: https://codeclimate.com/github/angelsanz/regularity/badges/gpa.svg 282 | [codeclimate-url]: https://codeclimate.com/github/angelsanz/regularity 283 | [nodeico-image]: https://nodei.co/npm/regularity.png?downloads=true&stars=true 284 | [nodeico-url]: https://nodei.co/npm/regularity/ 285 | 286 | [regexp-mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp 287 | -------------------------------------------------------------------------------- /git_hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | git stash -q --keep-index 4 | 5 | npm test 6 | TEST_RESULT="$?" 7 | 8 | git stash pop -q 9 | 10 | exit $TEST_RESULT 11 | -------------------------------------------------------------------------------- /lib/constraintInterpreter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var quantify = require('./quantifier'); 4 | var _escapeRegExp = require('./escaper').escapeRegExp; 5 | var _translate = require('./specialIdentifiers').translate; 6 | 7 | 8 | var interpret = function(userArguments) { 9 | return constraintBuilderFromArgCount[userArguments.length].apply(null, userArguments); 10 | }; 11 | 12 | 13 | var _buildNumberedConstraint = function(count, type) { 14 | return quantify.exactly(_buildUnnumberedConstraint(type), count); 15 | }; 16 | 17 | var _buildUnnumberedConstraint = function(pattern) { 18 | return _translate(_escapeRegExp(pattern)); 19 | }; 20 | 21 | var constraintBuilderFromArgCount = { 22 | 1: _buildUnnumberedConstraint, 23 | 2: _buildNumberedConstraint 24 | }; 25 | 26 | module.exports = { 27 | interpret: interpret 28 | }; 29 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | MethodCalledMoreThanOnce: function(methodName) { 5 | return { 6 | name: 'MethodCalledMoreThanOnce', 7 | message: (methodName + ' must only be called once') 8 | }; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/escaper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var charactersWhichNeedToBeEscaped = /[.*+?^${}()|[\]\\]/g; 4 | 5 | 6 | var escapeRegExp = function(string) { 7 | return string.replace(charactersWhichNeedToBeEscaped, '\\$&'); 8 | }; 9 | 10 | module.exports = { 11 | escapeRegExp: escapeRegExp 12 | }; 13 | -------------------------------------------------------------------------------- /lib/functionalHelpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var errors = require('./errors'); 4 | 5 | var onceOrThrow = function(func) { 6 | var called = false; 7 | 8 | return function() { 9 | if (called) { 10 | throw errors.MethodCalledMoreThanOnce(func.name); 11 | } 12 | 13 | 14 | called = true; 15 | 16 | return func.apply(this, arguments); 17 | }; 18 | }; 19 | 20 | 21 | module.exports = { 22 | onceOrThrow: onceOrThrow 23 | }; 24 | -------------------------------------------------------------------------------- /lib/grouper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var nonCapturing = function(pattern) { 4 | return ('(?:' + pattern + ')'); 5 | }; 6 | 7 | var oneOf = function(choices) { 8 | return (nonCapturing(choices.join('|'))); 9 | }; 10 | 11 | var atBeginning = function(pattern) { 12 | return ('^' + pattern); 13 | }; 14 | 15 | var atEnd = function(pattern) { 16 | return (pattern + '$'); 17 | }; 18 | 19 | 20 | module.exports = { 21 | nonCapturing: nonCapturing, 22 | oneOf: oneOf, 23 | 24 | atBeginning: atBeginning, 25 | atEnd: atEnd 26 | }; 27 | -------------------------------------------------------------------------------- /lib/quantifier.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var group = require('./grouper'); 4 | 5 | var zeroOrOne = function(pattern) { 6 | return (group.nonCapturing(pattern) + '?'); 7 | }; 8 | 9 | var zeroOrMore = function(pattern) { 10 | return (group.nonCapturing(pattern) + '*'); 11 | }; 12 | 13 | var oneOrMore = function(pattern) { 14 | return (group.nonCapturing(pattern) + '+'); 15 | }; 16 | 17 | var inRange = function(pattern, beginning, end) { 18 | return (group.nonCapturing(pattern) + _makeRangeDelimiter(beginning, end)); 19 | }; 20 | 21 | var _makeRangeDelimiter = function(beginning, end) { 22 | return ('{' + (beginning || '0') + ',' + (end || '') + '}'); 23 | }; 24 | 25 | var exactly = function(pattern, times) { 26 | return ((times === 1) ? 27 | pattern : 28 | (group.nonCapturing(pattern) + '{' + times + '}')); 29 | }; 30 | 31 | module.exports = { 32 | zeroOrOne: zeroOrOne, 33 | zeroOrMore:zeroOrMore, 34 | oneOrMore: oneOrMore, 35 | inRange: inRange, 36 | exactly: exactly 37 | }; 38 | -------------------------------------------------------------------------------- /lib/regularity.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./functionalHelpers'); 4 | var group = require('./grouper'); 5 | var quantify = require('./quantifier'); 6 | 7 | var _translate = require('./specialIdentifiers').translate; 8 | var _interpret = require('./constraintInterpreter').interpret; 9 | var _escapeRegExp = require('./escaper').escapeRegExp; 10 | 11 | 12 | 13 | var Regularity = function() { 14 | 15 | var _regexpSource = '', 16 | _beginning = '', 17 | _end = '', 18 | _flags = ''; 19 | 20 | 21 | 22 | var _appendPatternChunk = function(patternChunk) { 23 | _regexpSource += patternChunk; 24 | }; 25 | 26 | var _enableFlag = function(flagIdentifier) { 27 | _flags += flagIdentifier; 28 | }; 29 | 30 | var _setBeginning = function(pattern) { 31 | _beginning = group.atBeginning(pattern); 32 | }; 33 | 34 | var _setEnd = function(pattern) { 35 | _end = group.atEnd(pattern); 36 | }; 37 | 38 | 39 | 40 | 41 | this.startWith = _.onceOrThrow(function startWith() { 42 | _setBeginning(_interpret(arguments)); 43 | 44 | return this; 45 | }); 46 | 47 | this.append = function() { 48 | _appendPatternChunk(_interpret(arguments)); 49 | 50 | return this; 51 | }; 52 | this.then = this.append; 53 | 54 | this.endWith = _.onceOrThrow(function endWith() { 55 | _setEnd(_interpret(arguments)); 56 | 57 | return this; 58 | }); 59 | 60 | this.maybe = function() { 61 | _appendPatternChunk(quantify.zeroOrOne(_interpret(arguments))); 62 | 63 | return this; 64 | }; 65 | 66 | this.oneOf = function() { 67 | var choices = Array.prototype.slice.call(arguments) 68 | .map(_escapeRegExp) 69 | .map(_translate); 70 | 71 | _appendPatternChunk(group.oneOf(choices)); 72 | 73 | return this; 74 | }; 75 | 76 | this.between = function(range, pattern) { 77 | _appendPatternChunk(quantify.inRange(_interpret([pattern]), range[0], range[1])); 78 | 79 | return this; 80 | }; 81 | 82 | this.zeroOrMore = function() { 83 | _appendPatternChunk(quantify.zeroOrMore(_interpret(arguments))); 84 | 85 | return this; 86 | }; 87 | 88 | this.oneOrMore = function() { 89 | _appendPatternChunk(quantify.oneOrMore(_interpret(arguments))); 90 | 91 | return this; 92 | }; 93 | 94 | this.atLeast = function(times, pattern) { 95 | return this.between([times, null], pattern); 96 | }; 97 | 98 | this.atMost = function(times, pattern) { 99 | return this.between([null, times], pattern); 100 | }; 101 | 102 | this.insensitive = _.onceOrThrow(function insensitive() { 103 | _enableFlag('i'); 104 | 105 | return this; 106 | }); 107 | 108 | this.global = _.onceOrThrow(function global() { 109 | _enableFlag('g'); 110 | 111 | return this; 112 | }); 113 | 114 | this.multiline = _.onceOrThrow(function multiline() { 115 | _enableFlag('m'); 116 | 117 | return this; 118 | }); 119 | 120 | this.done = function() { 121 | return (new RegExp(_beginning + _regexpSource + _end, _flags)); 122 | }; 123 | this.regexp = this.done; 124 | }; 125 | 126 | module.exports = exports = Regularity; 127 | -------------------------------------------------------------------------------- /lib/specialIdentifiers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var word = require('./wordUtils'); 4 | 5 | 6 | var _specialIdentifiers = { 7 | 'digit' : '[0-9]', 8 | 'lowercase' : '[a-z]', 9 | 'uppercase' : '[A-Z]', 10 | 'letter' : '[A-Za-z]', 11 | 'alphanumeric': '[A-Za-z0-9]', 12 | 'whitespace' : '\\s', 13 | 'space' : ' ', 14 | 'tab' : '\\t' 15 | }; 16 | 17 | var translate = function(pattern) { 18 | return (_specialIdentifiers[word.singularize(pattern)] || pattern); 19 | }; 20 | 21 | 22 | module.exports = { 23 | translate: translate 24 | }; 25 | -------------------------------------------------------------------------------- /lib/wordUtils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var singularize = function(word) { 4 | return ((word[word.length - 1] === 's') ? 5 | word.substring(0, word.length - 1) : 6 | word); 7 | }; 8 | 9 | 10 | module.exports = { 11 | singularize: singularize 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regularity", 3 | "version": "0.5.1", 4 | "description": "regular expressions for humans", 5 | "main": "lib/regularity.js", 6 | "author": "Fernando Martinez de la Cueva (http://oinak.com/)", 7 | "contributors": [ 8 | "Elías Alonso (http://redradix.com/)", 9 | "Laura Paredes Viera (http://wearepeople.io)", 10 | "Alejandra Goicoechea (http://wearepeople.io)", 11 | "Ángel Sanz (https://github.com/angelsanz)" 12 | ], 13 | "thanks": "Andrew Berls for the original idea and the Regularity Ruby gem (https://rubygems.org/gems/regularity)", 14 | "homepage": "https://github.com/oinak/regularity", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/oinak/regularity.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/oinak/regularity/issues" 21 | }, 22 | "license": "MIT", 23 | "keywords": [ 24 | "regular", 25 | "expression", 26 | "regex", 27 | "regexp", 28 | "dsl" 29 | ], 30 | "scripts": { 31 | "test": "jasmine", 32 | "istanbul-local": "istanbul cover jasmine", 33 | "post-to-coveralls-io": "istanbul cover jasmine && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage" 34 | }, 35 | "devDependencies": { 36 | "coveralls": "^2.11.2", 37 | "grunt": "^0.4.5", 38 | "grunt-contrib-jshint": "^0.11.2", 39 | "istanbul": "^0.3.14", 40 | "jasmine": "^2.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /spec/regularity_spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Regularity = require('../lib/regularity.js'); 4 | var errors = require('../lib/errors'); 5 | 6 | describe("Regularity", function() { 7 | var regularity; 8 | 9 | beforeEach(function() { 10 | regularity = new Regularity(); 11 | }); 12 | 13 | it("is an object constructor", function() { 14 | expect(typeof Regularity).toBe('function'); 15 | expect(typeof regularity).toBe('object'); 16 | }); 17 | 18 | describe("escapes regexp special characters", function() { 19 | var charactersToBeEscaped = ['*', '.', '?', '^', '+', 20 | '$', '|', '(', ')', '[', 21 | ']', '{', '}']; 22 | 23 | charactersToBeEscaped.forEach(function testCharacterIsEscaped(character) { 24 | it("escapes '" + character + "'", function() { 25 | var currentRegexp = regularity.append(character).done(); 26 | 27 | expect(currentRegexp.source).toBe("\\" + character); 28 | }); 29 | }); 30 | }); 31 | 32 | describe("#startWith requires that the passed pattern occur exactly at the beginning of the input", function() { 33 | var regexp; 34 | 35 | describe("unnumbered", function() { 36 | it("single character", function() { 37 | regexp = regularity.startWith('a').done(); 38 | expect(regexp).toEqual(/^a/); 39 | }); 40 | 41 | it("multiple characters", function() { 42 | regexp = regularity.startWith('abc').done(); 43 | expect(regexp).toEqual(/^abc/); 44 | }); 45 | }); 46 | 47 | describe("numbered", function() { 48 | it("special identifiers", function() { 49 | regexp = regularity.startWith(4, 'digits').done(); 50 | expect(regexp).toEqual(/^(?:[0-9]){4}/); 51 | }); 52 | 53 | it("one occurrence of one character", function() { 54 | regexp = regularity.startWith(1, 'p').done(); 55 | expect(regexp).toEqual(/^p/); 56 | }); 57 | 58 | it("more than one occurence of one character", function() { 59 | regexp = regularity.startWith(6, 'p').done(); 60 | expect(regexp).toEqual(/^(?:p){6}/); 61 | }); 62 | 63 | it("one occurence of several characters", function() { 64 | regexp = regularity.startWith(1, 'hey').done(); 65 | expect(regexp).toEqual(/^hey/); 66 | }); 67 | 68 | it("more than one occurence of several characters", function() { 69 | regexp = regularity.startWith(5, 'hey').done(); 70 | expect(regexp).toEqual(/^(?:hey){5}/); 71 | }); 72 | }); 73 | 74 | it("can only be called once", function() { 75 | expect(function() { 76 | regularity.startWith('a').startWith('b'); 77 | }).toThrow(errors.MethodCalledMoreThanOnce('startWith')); 78 | }); 79 | }); 80 | 81 | describe("#endWith requires that the passed pattern occur exactly at the end of the input", function() { 82 | var regexp; 83 | 84 | describe("unnumbered", function() { 85 | it("single character", function() { 86 | regexp = regularity.endWith('a').done(); 87 | expect(regexp).toEqual(/a$/); 88 | }); 89 | 90 | it("multiple characters", function() { 91 | regexp = regularity.endWith('abc').done(); 92 | expect(regexp).toEqual(/abc$/); 93 | }); 94 | }); 95 | 96 | describe("numbered", function() { 97 | it("numbered special identifier", function() { 98 | regexp = regularity.endWith(4, 'alphanumeric').done(); 99 | expect(regexp).toEqual(/(?:[A-Za-z0-9]){4}$/); 100 | }); 101 | 102 | it("one occurrence of one character", function() { 103 | regexp = regularity.endWith(1, 'p').done(); 104 | expect(regexp).toEqual(/p$/); 105 | }); 106 | 107 | it("more than one occurence of one character", function() { 108 | regexp = regularity.endWith(6, 'p').done(); 109 | expect(regexp).toEqual(/(?:p){6}$/); 110 | }); 111 | 112 | it("one occurence of several characters", function() { 113 | regexp = regularity.endWith(1, 'hey').done(); 114 | expect(regexp).toEqual(/hey$/); 115 | }); 116 | 117 | it("more than one occurence of several characters", function() { 118 | regexp = regularity.endWith(5, 'hey').done(); 119 | expect(regexp).toEqual(/(?:hey){5}$/); 120 | }); 121 | }); 122 | 123 | it("can only be called once", function() { 124 | expect(function() { 125 | regularity.endWith('y').endWith('z'); 126 | }).toThrow(errors.MethodCalledMoreThanOnce('endWith')); 127 | }); 128 | }); 129 | 130 | describe("#maybe requires that the passed pattern occur either one or zero times", function() { 131 | var regexp; 132 | 133 | it("special identifier", function() { 134 | regexp = regularity.maybe('letter').done(); 135 | expect(regexp).toEqual(/(?:[A-Za-z])?/); 136 | }); 137 | 138 | it("single character", function() { 139 | regexp = regularity.maybe('a').done(); 140 | expect(regexp).toEqual(/(?:a)?/); 141 | }); 142 | 143 | it("multiple characters", function() { 144 | regexp = regularity.maybe('abc').done(); 145 | expect(regexp).toEqual(/(?:abc)?/); 146 | }); 147 | }); 148 | 149 | describe("#oneOf requires that at least one of the passed patterns occur", function() { 150 | var regexp; 151 | 152 | describe("special identifiers", function() { 153 | it("digit or tab", function() { 154 | regexp = regularity.oneOf('digit', 'tab').done(); 155 | expect(regexp).toEqual(/(?:[0-9]|\t)/); 156 | }); 157 | 158 | it("uppercase or whitespace", function() { 159 | regexp = regularity.oneOf('uppercase', 'whitespace').done(); 160 | expect(regexp).toEqual(/(?:[A-Z]|\s)/); 161 | }); 162 | 163 | it("letter or space", function() { 164 | regexp = regularity.oneOf('letter', 'space').done(); 165 | expect(regexp).toEqual(/(?:[A-Za-z]| )/); 166 | }); 167 | }); 168 | 169 | 170 | it("one argument, one character", function() { 171 | regexp = regularity.oneOf('a').done(); 172 | expect(regexp).toEqual(/(?:a)/); 173 | }); 174 | 175 | it("one argument, more than one character", function() { 176 | regexp = regularity.oneOf('bc').done(); 177 | expect(regexp).toEqual(/(?:bc)/); 178 | }); 179 | 180 | it("multiple arguments, one character each", function() { 181 | regexp = regularity.oneOf('a', 'b', 'c').done(); 182 | expect(regexp).toEqual(/(?:a|b|c)/); 183 | }); 184 | 185 | it("multiple arguments, some more than one character", function() { 186 | regexp = regularity.oneOf('a', 'bc', 'def', 'gh').done(); 187 | expect(regexp).toEqual(/(?:a|bc|def|gh)/); 188 | }); 189 | }); 190 | 191 | describe("#between requires that the passed pattern occur a number of consecutive times within the specified interval", function() { 192 | var regexp; 193 | 194 | describe("special identifiers", function() { 195 | it("digits", function() { 196 | regexp = regularity.between([3, 5], 'digits').done(); 197 | expect(regexp).toEqual(/(?:[0-9]){3,5}/); 198 | }); 199 | 200 | it("whitespace", function() { 201 | regexp = regularity.between([2, 6], 'whitespaces').done(); 202 | expect(regexp).toEqual(/(?:\s){2,6}/); 203 | }); 204 | 205 | it("lowercase", function() { 206 | regexp = regularity.between([4, 8], 'lowercases').done(); 207 | expect(regexp).toEqual(/(?:[a-z]){4,8}/); 208 | }); 209 | }); 210 | 211 | it("one character", function() { 212 | regexp = regularity.between([2, 4], 'a').done(); 213 | expect(regexp).toEqual(/(?:a){2,4}/); 214 | }); 215 | 216 | it("more than one character", function() { 217 | regexp = regularity.between([2, 4], 'abc').done(); 218 | expect(regexp).toEqual(/(?:abc){2,4}/); 219 | }); 220 | 221 | 222 | 223 | it("throws a native error when the lower bound is greater than the upper bound", function() { 224 | expect(function() { 225 | var regexp = regularity.between([5, 3], 'k').done(); 226 | }).toThrowError(SyntaxError); 227 | }); 228 | }); 229 | 230 | describe("#append requires that the passed pattern occur after what has been declared so far (and before whatever is declared afterwards), as many times as specified (or one, by default)", function() { 231 | var regexp; 232 | 233 | describe("unnumbered", function() { 234 | it("one character", function() { 235 | regexp = regularity.append('a').done(); 236 | expect(regexp).toEqual(/a/); 237 | }); 238 | 239 | it("more than one character", function() { 240 | regexp = regularity.append('abc').done(); 241 | expect(regexp).toEqual(/abc/); 242 | }); 243 | }); 244 | 245 | describe("numbered", function() { 246 | it("one time, one character", function() { 247 | regexp = regularity.append(1, 'a').done(); 248 | expect(regexp).toEqual(/a/); 249 | }); 250 | 251 | it("more than one time, one character", function() { 252 | regexp = regularity.append(3, 'a').done(); 253 | expect(regexp).toEqual(/(?:a){3}/); 254 | }); 255 | 256 | it("one time, more than one character", function() { 257 | regexp = regularity.append(1, 'abc').done(); 258 | expect(regexp).toEqual(/abc/); 259 | }); 260 | 261 | it("more than one time, more than one character", function() { 262 | regexp = regularity.append(5, 'abc').done(); 263 | expect(regexp).toEqual(/(?:abc){5}/); 264 | }); 265 | }); 266 | 267 | describe("special identifiers", function() { 268 | it("letters", function() { 269 | regexp = regularity.append(3, 'letters').done(); 270 | expect(regexp).toEqual(/(?:[A-Za-z]){3}/); 271 | }); 272 | 273 | it("uppercase", function() { 274 | regexp = regularity.append(1, 'uppercase').done(); 275 | expect(regexp).toEqual(/[A-Z]/); 276 | }); 277 | }); 278 | }); 279 | 280 | it("#then is just an alias for #append", function() { 281 | expect(regularity.then).toBe(regularity.append); 282 | }); 283 | 284 | describe("#zeroOrMore requires that the passed pattern occur consecutively any number of consecutive times, including zero", function() { 285 | var regexp; 286 | 287 | describe("special identifiers", function() { 288 | it("lowercase", function() { 289 | regexp = regularity.zeroOrMore('lowercases').done(); 290 | expect(regexp).toEqual(/(?:[a-z])*/); 291 | }); 292 | }); 293 | 294 | it("one character", function() { 295 | regexp = regularity.zeroOrMore('a').done(); 296 | expect(regexp).toEqual(/(?:a)*/); 297 | }); 298 | 299 | it("more than one character", function() { 300 | regexp = regularity.zeroOrMore('abc').done(); 301 | expect(regexp).toEqual(/(?:abc)*/); 302 | }); 303 | }); 304 | 305 | describe("#oneOrMore requires that the passed pattern occur consecutively at least once", function() { 306 | var regexp; 307 | 308 | describe("special identifiers", function() { 309 | it("digits", function() { 310 | regexp = regularity.oneOrMore('digits').done(); 311 | expect(regexp).toEqual(/(?:[0-9])+/); 312 | }); 313 | }); 314 | 315 | it("one character", function() { 316 | regexp = regularity.oneOrMore('a').done(); 317 | expect(regexp).toEqual(/(?:a)+/); 318 | }); 319 | 320 | it("more than one character", function() { 321 | regexp = regularity.oneOrMore('abc').done(); 322 | expect(regexp).toEqual(/(?:abc)+/); 323 | }); 324 | }); 325 | 326 | describe("#atLeast requires that the passed pattern occur consecutively at least the specified number of times", function() { 327 | var regexp; 328 | 329 | describe("special identifiers", function() { 330 | it("tabs", function() { 331 | regexp = regularity.atLeast(4, 'tabs').done(); 332 | expect(regexp).toEqual(/(?:\t){4,}/); 333 | }); 334 | }); 335 | 336 | it("one character", function() { 337 | regexp = regularity.atLeast(3, 'a').done(); 338 | expect(regexp).toEqual(/(?:a){3,}/); 339 | }); 340 | 341 | it("more than one character", function() { 342 | regexp = regularity.atLeast(5, 'abc').done(); 343 | expect(regexp).toEqual(/(?:abc){5,}/); 344 | }); 345 | }); 346 | 347 | describe("#atMost requires that the passed pattern occur consecutively at most the specified number of times", function() { 348 | var regexp; 349 | 350 | describe("special identifiers", function() { 351 | it("spaces", function() { 352 | regexp = regularity.atMost(8, 'spaces').done(); 353 | expect(regexp).toEqual(/(?: ){0,8}/); 354 | }); 355 | }); 356 | 357 | it("one character", function() { 358 | regexp = regularity.atMost(3, 'a').done(); 359 | expect(regexp).toEqual(/(?:a){0,3}/); 360 | }); 361 | 362 | it("more than one character", function() { 363 | regexp = regularity.atMost(5, 'abc').done(); 364 | expect(regexp).toEqual(/(?:abc){0,5}/); 365 | }); 366 | }); 367 | 368 | describe("#insensitive specifies that the matching must be done case-insensitively", function() { 369 | beforeEach(function() { 370 | regularity.insensitive(); 371 | }); 372 | 373 | it("sets the 'insensitive' native flag", function() { 374 | var regexp = regularity.done(); 375 | expect(regexp.ignoreCase).toBe(true); 376 | }); 377 | 378 | it("can only be called once", function() { 379 | expect(function() { 380 | regularity.insensitive(); 381 | }).toThrow(errors.MethodCalledMoreThanOnce('insensitive')); 382 | }); 383 | }); 384 | 385 | describe("#global specifies that the matching must be performed as many times as necessary to identify all matches", function() { 386 | beforeEach(function() { 387 | regularity.global(); 388 | }); 389 | 390 | it("sets the 'global' native flag", function() { 391 | var regexp = regularity.done(); 392 | expect(regexp.global).toBe(true); 393 | }); 394 | 395 | it("can only be called once", function() { 396 | expect(function() { 397 | regularity.global(); 398 | }).toThrow(errors.MethodCalledMoreThanOnce('global')); 399 | }); 400 | }); 401 | 402 | describe("#multiline specifies that the input may span multiple lines", function() { 403 | beforeEach(function() { 404 | regularity.multiline(); 405 | }); 406 | 407 | it("sets the 'multiline' native flag", function() { 408 | var regexp = regularity.done(); 409 | expect(regexp.multiline).toBe(true); 410 | }); 411 | 412 | it("can only be called once", function() { 413 | expect(function() { 414 | regularity.multiline(); 415 | }).toThrow(errors.MethodCalledMoreThanOnce('multiline')); 416 | }); 417 | }); 418 | 419 | describe("#done", function() { 420 | it("returns a RegExp instance", function() { 421 | expect(regularity.done() instanceof RegExp).toBe(true); 422 | }); 423 | 424 | it("returns an empty regexp by default", function() { 425 | expect(regularity.done()).toEqual(new RegExp()); 426 | }); 427 | }); 428 | 429 | it("#regexp is just an alias for #done", function() { 430 | expect(regularity.regexp).toBe(regularity.done); 431 | }); 432 | }); 433 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------