├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bin └── authorize-push.js ├── demo └── index.html ├── index.d.ts ├── index.js ├── lib └── initials.js ├── package-lock.json ├── package.json └── test └── initials-test.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | - next 7 | - beta 8 | - "*.x" 9 | jobs: 10 | release: 11 | name: release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: lts/* 18 | cache: npm 19 | - run: npm ci 20 | - run: npm run build 21 | - run: npx semantic-release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | "on": 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: lts/* 18 | cache: npm 19 | - run: npm ci 20 | - run: npm test 21 | - run: npm run build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .coveralls.yml 3 | .DS_Store 4 | coverage/ 5 | # generated by `npm run build:demo` 6 | demo/initials.js 7 | dist/ 8 | node_modules/ 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource+coc@martynus.net. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gregor Martynus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Initials. Because JD is shorter than John Doe 2 | ============================================= 3 | 4 | > extracts initials from and adds initials to names 5 | 6 | [![Build Status](https://travis-ci.org/gr2m/initials.svg?branch=main)](https://travis-ci.org/gr2m/initials) 7 | [![Coverage Status](https://coveralls.io/repos/gr2m/initials/badge.svg?branch=main)](https://coveralls.io/r/gr2m/initials?branch=main) 8 | [![Greenkeeper badge](https://badges.greenkeeper.io/gr2m/initials.svg)](https://greenkeeper.io/) 9 | 10 | ## Installation 11 | 12 | Install using [npm](https://npmjs.org/) for node.js: 13 | 14 | ``` 15 | npm install --save initials 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | ```js 22 | initials('John Doe') 23 | // 'JD' 24 | 25 | initials(['John Doe', 'Robert Roe']) 26 | // ['JD', 'RR'] 27 | 28 | // alias for initials('John Doe') 29 | initials.find('John Doe') 30 | 31 | // parse name(s) 32 | initials.parse('John Doe') 33 | // {name: 'John Doe', initials: 'JD'} 34 | 35 | // add initials to name(s) 36 | initials.addTo('John Doe') 37 | // 'John Doe (JD)' 38 | 39 | // pass existing initials for names 40 | initials(['John Doe', 'Jane Dane'], { 41 | existing: { 'John Doe': 'JD' } 42 | }) 43 | // ['JD', 'JDa'] 44 | ``` 45 | 46 | ## Notes 47 | 48 | Preffered initials can be passed in `(JD)`, e.g. 49 | 50 | ```js 51 | console.log( initials('John Doe (JoDo)') ); 52 | // 'JoDo' 53 | ``` 54 | 55 | If a name contains an email, it gets ignored when calculating initials 56 | 57 | ```js 58 | console.log( initials('John Doe joe@example.com') ); 59 | // 'JD' 60 | ``` 61 | 62 | If a name _is_ an email, the domain part gets ignored 63 | 64 | ```js 65 | console.log( initials('joe@example.com') ); 66 | // 'jo' 67 | ``` 68 | 69 | When passing an Array of names, duplicates of initials are avoided 70 | 71 | ```js 72 | console.log( initials(['John Doe', 'Jane Dane']) ); 73 | // ['JDo', 'JDa'] 74 | ``` 75 | 76 | ## Test 77 | 78 | ``` 79 | npm test 80 | ``` 81 | 82 | 83 | ## LICENSE 84 | 85 | [MIT](LICENSE) 86 | -------------------------------------------------------------------------------- /bin/authorize-push.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var exec = require('child_process').exec 4 | 5 | var GH_TOKEN = process.env.GH_TOKEN 6 | var repo = require('../package.json').repository.url 7 | 8 | if (!(process.env.CI && GH_TOKEN && repo)) { 9 | console.log('[authorize-push] ignodered because condition not fullfillled (process.env.CI: %s, GH_TOKEN: %s, repo: %s)', !!process.env.CI, !!GH_TOKEN, !!repo) 10 | process.exit(1) 11 | } 12 | 13 | var commands = [ 14 | 'git remote set-url origin ' + repo.replace('https://', 'https://' + GH_TOKEN + '@'), 15 | 'git config user.email "gregor@martynus.net"', 16 | 'git config user.name "gr2m"' 17 | ] 18 | commands.forEach(function (command) { 19 | console.log('[authorize-push] %s', command.replace(GH_TOKEN, '***GH_TOKEN***')) 20 | exec(command) 21 | }) 22 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | initials 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 |
26 |

initials

27 |
Made by the Hoodie Community.
28 |
29 | 30 |
31 |

Initials

32 |

33 | a JavaScript library to extract initials from and adds initials to names. 34 |

35 |

36 | 37 | status of tests 38 | 39 |

40 |
initials('John Doe');
 41 | // > 'JD'
 42 | 
 43 | initials(['John Doe', 'Jane Dane']);
 44 | // > ['JDo', 'JDa']
 45 | 
 46 | // alias
 47 | initials.find('John Doe')
 48 | 
 49 | // parse name(s)
 50 | initials.parse('John Doe')
 51 | // {name: 'John Doe', initials: 'JD'}
 52 | 
 53 | // add initials to name(s)
 54 | initials.addTo('John Doe')
 55 | // 'John Doe (JD)'
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 72 | 75 | 76 | 77 |
NameInitialsCombined
67 | 68 | 70 | 71 | 73 | 74 |
78 | 79 |

80 | Note: This demo is using the Bootstrap Editable Table 81 | plugin for auto-adding of new rows, and for styling. 82 |

83 |
84 | 85 | 86 | 87 | 88 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'initials' { 2 | type NameOrNames = string | string[]; 3 | 4 | type ParsedNames = {name: string, initials: string}; 5 | 6 | interface Options { 7 | existing: { 8 | [name: string]: string 9 | } 10 | } 11 | 12 | function initials(nameOrNames: T, options?: Options): T; 13 | 14 | export function find(name: string): string; 15 | 16 | export function parse(name: string): ParsedNames | undefined; 17 | 18 | export function addTo(name: string): string | undefined; 19 | 20 | export default initials; 21 | } 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/initials') 2 | -------------------------------------------------------------------------------- /lib/initials.js: -------------------------------------------------------------------------------- 1 | module.exports = initials 2 | 3 | // extend public API 4 | initials.addTo = addInitialsTo 5 | initials.parse = parse 6 | initials.find = initials 7 | 8 | // defaults 9 | var defaultLength = 2 10 | 11 | // there is no support for look-behinds in JS, and the \b selector 12 | // doesn't work with diacritics. So we maintain a blacklist of 13 | // "non letters", that we use later to build our regex. 14 | var nonLetters = ' -\\/:-@\\[-`\\{-\\~' 15 | // regex patterns 16 | var uppercaseLettersOnlyPattern = /^[A-Z]+$/ 17 | var initialsInNamePattern = /\(([^)]+)\)/ 18 | var nameIsEmailPattern = /^[^\s]+@[^\s]+$/ 19 | var findDomainInEmailPattern = /@[^\s]+/ 20 | var findEmailPattern = /[\w._-]+@[\w.-]+[\w]/g 21 | 22 | // match everything that is a "non letter" (see above) 23 | // followed by all but a "non letter". 24 | // Expl: "Jörg Jäger-Franke" => ["J", " J", "-F"] 25 | var findFirstLettersOfWordsPattern = new RegExp('(^|[' + nonLetters + '])[^' + nonLetters + ']', 'g') 26 | var findAllNonCharactersPattern = new RegExp('[' + nonLetters + ']', 'g') 27 | 28 | // PUBLIC API METHODS 29 | 30 | // 31 | // initials allows to be used with either a string or an array of strings 32 | // 33 | function initials (nameOrNames, options) { 34 | if (!nameOrNames) return '' 35 | if (typeof nameOrNames === 'string') return initialsForSingleName(nameOrNames, normalize(options)) 36 | return initialsForMultipleNames(nameOrNames, normalize(options)) 37 | } 38 | 39 | // 40 | // finds initials in a name and adds them to the right side 41 | // 42 | function addInitialsTo (nameOrNames, options) { 43 | if (!nameOrNames) return '' 44 | if (typeof nameOrNames === 'string') return addInitialsToSingleName(nameOrNames, normalize(options)) 45 | return addInitialsToMultipleNames(nameOrNames, normalize(options)) 46 | } 47 | 48 | // 49 | // extract name, initials, email 50 | // 51 | function parse (nameOrNames, options) { 52 | if (!nameOrNames) return {} 53 | if (typeof nameOrNames === 'string') return parseSingleName(nameOrNames, normalize(options)) 54 | return parseMultipleNames(nameOrNames, normalize(options)) 55 | } 56 | 57 | // HELPER METHODS 58 | 59 | // 60 | // Find initials in a single given name string 61 | // 62 | function initialsForSingleName (name, options) { 63 | var matches 64 | var result 65 | var initials 66 | var length = options.length || 2 67 | 68 | initials = findPreferredInitials(name, options) 69 | if (initials) return initials 70 | 71 | name = cleanupName(name) 72 | if (!name) return '' 73 | 74 | // there is no support for look-behinds in JS, and the \b selector 75 | // doesn't work with diacritics. So we match everything that is a 76 | // "non character" followed by all but a "non character". To fix 77 | // that, we map the results to its last character. 78 | // Expl: "Jörg Jäger" => ["J", " J"] => ["J", "J"] 79 | matches = name.match(findFirstLettersOfWordsPattern).map(function (match) { 80 | return match[match.length - 1] 81 | }) 82 | 83 | if (matches.length < 2) { 84 | if (name.length > length) { 85 | return name.substr(0, length) 86 | } else { 87 | return name 88 | } 89 | } else { 90 | result = matches.join('') 91 | } 92 | 93 | if (result.length >= length) { 94 | return result 95 | } 96 | 97 | // This is where it gets complicated. 98 | // Let's say we're in initials('John Doe', 3), so up to here 99 | // we have `result === 'JD'`, but what we want is `result === `JDo`. 100 | 101 | // First, we calculate all remaining options that we have 102 | var possibleInitials = getPossibleInitialsForName(name) 103 | var option 104 | 105 | // then we return the first option that has the required length 106 | for (var i = 0; i < possibleInitials.length; i++) { 107 | if (possibleInitials[i].length >= length) return possibleInitials[i] 108 | }; 109 | 110 | // if that didn't work, we return the last possible option 111 | return option 112 | } 113 | 114 | // 115 | // 116 | // 117 | function initialsForMultipleNames (names, options) { 118 | var optionsForNames = [] 119 | var optionsCountForNames 120 | var map = {} 121 | var duplicatesMap = {} 122 | var initialsForNamesMap = {} 123 | var initials 124 | var possibleInitials 125 | var length = options.length || 2 126 | 127 | // get all possible initials for all names for given length 128 | names.forEach(function (name) { 129 | // normalize 130 | if (!name) name = '' 131 | 132 | // known name? Gets same initials, stop here 133 | if (initialsForNamesMap[name]) return 134 | 135 | // too short to extract initials from? Use name as initials. 136 | if (name.length < length) { 137 | initialsForNamesMap[name] = [name] 138 | return 139 | } 140 | 141 | // preferred initials like (JD)? Use these 142 | initials = findPreferredInitials(name, options) 143 | if (initials) { 144 | map[initials] = 1 145 | initialsForNamesMap[name] = [initials] 146 | return 147 | } 148 | 149 | // return all possible initials for given length 150 | possibleInitials = getPossibleInitialsForName(name).filter(function (initials) { 151 | if (initials.length !== length) return false 152 | if (map[initials]) duplicatesMap[initials] = 1 153 | map[initials] = 1 154 | return true 155 | }) 156 | 157 | initialsForNamesMap[name] = possibleInitials 158 | }) 159 | 160 | // remove duplicates 161 | var keys = [] 162 | for (var k in initialsForNamesMap) { 163 | keys.unshift(k) 164 | } 165 | for (var c = keys.length, n = 0; n < c; n++) { 166 | possibleInitials = initialsForNamesMap[keys[n]] 167 | optionsForNames.push(possibleInitials) 168 | 169 | for (var i = 0; i < possibleInitials.length; i++) { 170 | if (duplicatesMap[possibleInitials[i]] > 0) { 171 | duplicatesMap[possibleInitials[i]]-- 172 | possibleInitials.splice(i, 1) 173 | } 174 | } 175 | } 176 | 177 | // make sure we still have options for every name 178 | optionsCountForNames = optionsForNames.map(function (options) { return options.length }) 179 | 180 | // if names were empty, optionsCountForNames is empty. In that case stop here 181 | if (optionsCountForNames.length === 0) return names 182 | 183 | if (Math.min.apply(null, optionsCountForNames) === 0) { 184 | options.length++ 185 | return initialsForMultipleNames(names, options) 186 | } 187 | 188 | // if we do, return the first option for each 189 | return names.map(function (name) { return initialsForNamesMap[name][0] }) 190 | } 191 | 192 | // 193 | // 194 | // 195 | function addInitialsToSingleName (name, options) { 196 | var parts = parseSingleName(name, options) 197 | return format(parts) 198 | } 199 | 200 | // 201 | // 202 | // 203 | function addInitialsToMultipleNames (names, options) { 204 | return parseMultipleNames(names, options).map(format) 205 | } 206 | 207 | // 208 | // 209 | // 210 | function parseSingleName (name, options) { 211 | var initials 212 | var email 213 | var matches 214 | var parts = {} 215 | 216 | if (!name) return {} 217 | 218 | // are initials part of the name? 219 | initials = findPreferredInitials(name, options) 220 | if (initials) { 221 | // if yes, remove it from name 222 | name = name.replace(uppercaseLettersOnlyPattern, '') 223 | name = name.replace(initialsInNamePattern, '') 224 | } 225 | 226 | // use preferred initials if passed 227 | if (options.initials) initials = options.initials 228 | 229 | // if no initials found yet, extract initials from name 230 | if (!initials) initials = initialsForSingleName(name, options) 231 | 232 | // is there an email in the name? 233 | matches = name.match(findEmailPattern) 234 | if (matches != null) email = matches.pop() 235 | if (email) { 236 | // if yes, remove it from name 237 | name = name.replace(email, '') 238 | 239 | // if the email and the name are the same, initials can not be rendered 240 | // the initials method uses email for rendering just when the name is false 241 | // see https://github.com/gr2m/initials/issues/7 for more details 242 | if (name.trim() === '<' + email + '>') { 243 | // set the name to undefined 244 | name = '' 245 | 246 | // fire up the initials again with the email 247 | if (!initials) { 248 | initials = initialsForSingleName(email, options) 249 | } 250 | } 251 | } 252 | 253 | // clean up the rest 254 | name = name.replace(findAllNonCharactersPattern, ' ').trim() 255 | 256 | // do only return what's present 257 | if (name) parts.name = name 258 | if (initials) parts.initials = initials 259 | if (email) parts.email = email 260 | 261 | return parts 262 | } 263 | 264 | // 265 | // 266 | // 267 | function parseMultipleNames (names, options) { 268 | var initialsArray = initialsForMultipleNames(names, options) 269 | 270 | return names.map(function (name, i) { 271 | options.existing[name] = initialsArray[i] 272 | return parseSingleName(name, options) 273 | }) 274 | } 275 | 276 | // 277 | // 278 | // 279 | function format (parts) { 280 | // neither name nor email: return initials 281 | if (!parts.name && !parts.email) return parts.initials 282 | 283 | // no email: return name with initials 284 | if (!parts.email) return parts.name + ' (' + parts.initials + ')' 285 | 286 | // no name: return email with initials 287 | if (!parts.name) return parts.email + ' (' + parts.initials + ')' 288 | 289 | // return name with initials & name 290 | return parts.name + ' (' + parts.initials + ') <' + parts.email + '>' 291 | } 292 | 293 | // 294 | // 295 | // 296 | function cleanupName (name) { 297 | // in case the name is an email address, remove the @xx.yy part 298 | // otherwise remove an eventual email address from name 299 | if (nameIsEmailPattern.test(name)) { 300 | name = name.replace(findDomainInEmailPattern, '') 301 | } else { 302 | name = name.replace(findEmailPattern, '') 303 | } 304 | 305 | // replace all non characters with ' ' & trim 306 | name = name.replace(findAllNonCharactersPattern, ' ').trim() 307 | 308 | return name 309 | } 310 | 311 | // 312 | // 313 | // 314 | function findPreferredInitials (name, options) { 315 | var matches 316 | 317 | // if preferred initials passed for current name 318 | if (options.existing[name]) return options.existing[name] 319 | 320 | // if the name contains only upcase letters, let's take it as the initials as well 321 | if (uppercaseLettersOnlyPattern.test(name)) { 322 | return name 323 | } 324 | 325 | // are the initials part of the given name, e.g. »Eddie Murphie (em)«? 326 | matches = name.match(initialsInNamePattern) 327 | 328 | // if yes, return them 329 | if (matches != null) { 330 | return matches.pop() 331 | } 332 | } 333 | 334 | // 335 | // e.g. for John Doe: 336 | // - JDo 337 | // - JDoe 338 | // - JoDoe 339 | // - JohDoe 340 | // - JohnDoe 341 | // 342 | var cache = {} 343 | function getPossibleInitialsForName (name) { 344 | var parts 345 | var partsPossibilities 346 | var options = [] 347 | 348 | name = cleanupName(name) 349 | 350 | if (cache[name]) { 351 | return cache[name].slice(0) // return copy 352 | } 353 | 354 | // split names into parts 355 | // 'John Doe' => ['Doe', 'John'] 356 | parts = name.split(' ') 357 | 358 | // map parts to all its possible initials 359 | // 'John' => ['J', 'Jo', 'Joh', 'John'] 360 | partsPossibilities = parts.map(getPossibleInitialsForWord) 361 | 362 | options = combineAll(partsPossibilities) 363 | 364 | // sort options, shortest first 365 | options = options.sort(function (a, b) { 366 | return a.length - b.length || options.indexOf(a) - options.indexOf(b) 367 | }) 368 | 369 | // cache for future 370 | cache[name] = options 371 | 372 | // return options 373 | return options.slice(0) 374 | } 375 | 376 | // 377 | // 378 | // 379 | function combineAll (array) { 380 | var current = array.shift() 381 | var temp 382 | var results 383 | if (array.length > 0) { 384 | results = [] 385 | temp = combineAll(array) 386 | current.forEach(function (value1) { 387 | temp.forEach(function (value2) { 388 | results.push(value1 + value2) 389 | }) 390 | }) 391 | return results 392 | } else { 393 | return current 394 | } 395 | } 396 | 397 | // 398 | // 399 | // 400 | function getPossibleInitialsForWord (word) { 401 | var options = [] 402 | while (word.length) { 403 | options.unshift(word) 404 | word = word.substr(0, word.length - 1) 405 | } 406 | return options 407 | } 408 | 409 | // 410 | // make sure that options is always an object, and that 411 | // * options.length is a number and >= defaultLength 412 | // * existing is set and an object 413 | // 414 | function normalize (options) { 415 | if (!options) options = { length: defaultLength } 416 | if (typeof options === 'number') options = { length: options } 417 | 418 | options.length = Math.max(options.length || 0, defaultLength) 419 | options.existing = options.existing || {} 420 | 421 | return options 422 | } 423 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "initials", 3 | "version": "0.0.0-development", 4 | "description": "initials for names", 5 | "scripts": { 6 | "start": "npm run build:demo && beefy ./index.js --cwd=./demo", 7 | "prebuild": "rimraf dist && mkdirp dist", 8 | "build": "browserify index.js -s initials -o dist/initials.js", 9 | "build:demo": "browserify index.js -s initials -o demo/initials.js", 10 | "deploy": "npm run build:demo && npm run deploydocs", 11 | "predeploydocs": "./bin/authorize-push.js", 12 | "deploydocs": "gh-pages-deploy", 13 | "pretest": "standard", 14 | "test": "npm run -s test:node | tap-spec", 15 | "test:coverage": "istanbul cover test/initials-test.js", 16 | "test:node": "node test/initials-test.js" 17 | }, 18 | "gh-pages-deploy": { 19 | "staticpath": "demo", 20 | "noprompt": true 21 | }, 22 | "repository": "github:gr2m/initials", 23 | "author": "Gregor Martynus ", 24 | "license": "MIT", 25 | "homepage": "http://gr2m.github.io/initials", 26 | "devDependencies": { 27 | "beefy": "^2.1.5", 28 | "browserify": "^16.0.0", 29 | "gh-pages-deploy": "^0.5.0", 30 | "istanbul": "^0.4.0", 31 | "istanbul-coveralls": "^1.0.1", 32 | "mkdirp": "^1.0.0", 33 | "rimraf": "^3.0.0", 34 | "semantic-release": "^17.0.0", 35 | "standard": "^14.0.0", 36 | "tap-spec": "^5.0.0", 37 | "tape": "^5.0.0" 38 | }, 39 | "standard": { 40 | "ignore": [ 41 | "demo" 42 | ] 43 | }, 44 | "types": "./index.d.ts", 45 | "release": { 46 | "branches": [ 47 | "+([0-9])?(.{+([0-9]),x}).x", 48 | "main", 49 | "next", 50 | "next-major", 51 | { 52 | "name": "beta", 53 | "prerelease": true 54 | }, 55 | { 56 | "name": "alpha", 57 | "prerelease": true 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/initials-test.js: -------------------------------------------------------------------------------- 1 | var initials = require('../lib/initials') 2 | var test = require('tape') 3 | 4 | test('has function', function (t) { 5 | t.is(typeof initials, 'function', 'has method initials') 6 | t.is(typeof initials.addTo, 'function', 'has method initials.addTo') 7 | t.is(typeof initials.parse, 'function', 'has method initials.parse') 8 | t.is(typeof initials.find, 'function', 'has method initials.find') 9 | 10 | t.end() 11 | }) 12 | 13 | test('initials(name)', function (t) { 14 | t.equal(initials('John Doe'), 'JD', 'John Doe ☛ JD') 15 | t.equal(initials('john doe'), 'jd', 'joe doe ☛ jd') 16 | t.equal(initials('John Doe '), 'JD', 'John Doe ☛ JD') 17 | t.equal(initials('joe@example.com'), 'jo', 'joe@example.com ☛ jo') 18 | t.equal(initials('John Doe (dj)'), 'dj', 'John Doe (dj) ☛ dj') 19 | 20 | // https://github.com/gr2m/initials/issues/6 21 | t.equal(initials('안형준'), '안형', '안형준 -> 안형') 22 | 23 | t.end() 24 | }) 25 | 26 | test('initials(name, 3)', function (t) { 27 | t.equal(initials('John Doe', 3), 'JDo', 'John Doe ☛ JDo') 28 | t.equal(initials('John D.', 3), 'JoD', 'John D. ☛ JoD') 29 | 30 | t.end() 31 | }) 32 | 33 | test('initials(namesArray)', function (t) { 34 | t.deepEqual(initials(['John Doe', 'Robert Roe', 'Larry Loe']), ['JD', 'RR', 'LL'], 'John Doe, Robert Roe, Larry Loe ☛ JD, RR, LL') 35 | t.deepEqual(initials(['John Doe', 'Jane Dane']), ['JDo', 'JDa'], 'guarantees unique initials: John Doe, Jane Dane ☛ JDo, JDa') 36 | t.deepEqual(initials(['John Doe (JD)', 'Jane Dane']), ['JD', 'JDa'], 'guarantees unique initials, respecting preferences: John Doe (JD), Jane Dane ☛ JDo, JDa') 37 | t.deepEqual(initials(['John Doe', 'Jane Dane', 'John Doe']), ['JDo', 'JDa', 'JDo'], 'same initials for same names: John Doe, Jane Dane, John Doe ☛ JDo, JDa, JDo') 38 | t.deepEqual(initials(['John Smith', 'Jane Smith']), ['JSm', 'JaS'], 'shortest initials possible: John Smith, Jane Smith ☛ JSm, JaS') 39 | t.deepEqual(initials(['John Doe (JoDo)', 'Jane Dane']), ['JoDo', 'JD'], 'respects preferred initials: John Doe (JoDo), Jane Dane ☛ JoDo, JD') 40 | t.deepEqual(initials(['John Doe (JoDo)', 'Jane Dane (JoDo)']), ['JoDo', 'JoDo'], 'conflicting initials can be enforced: John Doe (JoDo), Jane Dane (JoDo) ☛ JoDo, JoDo') 41 | t.deepEqual(initials(['John Doe (JD)', 'Jane Dane']), ['JD', 'JDa'], 'preferred initials are respected in other names: John Doe (JD), Jane Dane ☛ JD, JDa') 42 | t.deepEqual(initials(['John Doe ']), ['JD'], 'emails are ignored in arrays') 43 | t.deepEqual(initials(['joe@example.com']), ['jo'], 'domains are ignored when a name is an email') 44 | 45 | // https://github.com/gr2m/initials/issues/1 46 | t.deepEqual(initials(['j']), ['j'], 'j ☛ j') 47 | 48 | // https://github.com/gr2m/initials/issues/14 49 | t.deepEqual(initials(['Moe Minutes', 'Moe Min']).sort(), ['MMi', 'MoM'], '["Moe Minutes", "Moe Min"] ☛ ["MoM", "MMi"]') 50 | 51 | t.end() 52 | }) 53 | 54 | test('initials(nameOrNames, {existing: initialsForNames})', function (t) { 55 | t.equal(initials('John Doe', { 56 | existing: { 57 | 'John Doe': 'JoDo' 58 | } 59 | }), 'JoDo', 'respect existing initials') 60 | 61 | t.deepEqual(initials(['John Doe', 'Jane Dane'], { 62 | existing: { 63 | 'John Doe': 'JD' 64 | } 65 | }), ['JD', 'JDa'], 'respect existing initials') 66 | 67 | t.end() 68 | }) 69 | 70 | test('initials.addTo(name)', function (t) { 71 | t.equal(initials.addTo('John Doe'), 'John Doe (JD)', 'John Doe ☛ John Doe (JD)') 72 | t.equal(initials.addTo('(JJ) Jack Johnson'), 'Jack Johnson (JJ)', 'Jack Johnson ☛ Jack Johnson (JJ)') 73 | t.equal(initials.addTo('JD'), 'JD', 'JD ☛ JD') 74 | t.equal(initials.addTo('JD (JD)'), 'JD (JD)', 'JD (JD) ☛ JD (JD)') 75 | t.equal(initials.addTo('John Doe (JoDo) joe@example.com'), 'John Doe (JoDo) ', 'John Doe (JoDo) joe@example.com ☛ John Doe (JoDo) ') 76 | t.equal(initials.addTo('joe@example.com'), 'joe@example.com (jo)', 'joe@example.com ☛ joe@example.com (jo)') 77 | t.equal(initials.addTo('joe (j)'), 'joe (j)', 'joe (j) ☛ joe (j)') 78 | t.equal(initials.addTo('Frönkää Üüd'), 'Frönkää Üüd (FÜ)', 'Frönkää Üüd ☛ Frönkää Üüd (FÜ)') 79 | t.equal(initials.addTo('funky (fu)'), 'funky (fu)', 'funky (fu) ☛ funky (fu)') 80 | 81 | // https://github.com/gr2m/initials/issues/7 82 | t.equal(initials.addTo('test.test@test.org '), 'test.test@test.org (tt)', 'test.test@test.org ☛ test.test@test.org (tt)') 83 | 84 | t.end() 85 | }) 86 | 87 | test('initials.addTo(namesArray)', function (t) { 88 | t.deepEqual(initials.addTo(['John Doe', 'Robert Roe', 'Larry Loe']), ['John Doe (JD)', 'Robert Roe (RR)', 'Larry Loe (LL)'], 'John Doe, Robert Roe, Larry Loe ☛ John Doe (JD), Robert Roe (RR), Larry Loe (LL)') 89 | t.deepEqual(initials.addTo(['John Doe', 'Jane Dane']), ['John Doe (JDo)', 'Jane Dane (JDa)'], 'John Doe, Jane Dane ☛ John Doe (JDo), Jane Dane (JDa)') 90 | 91 | t.end() 92 | }) 93 | 94 | test('initials.addTo(nameOrNames, {existing: initialsForNames})', function (t) { 95 | t.equal( 96 | initials.addTo('John Doe', { 97 | existing: { 98 | 'John Doe': 'JoDo' 99 | } 100 | }), 'John Doe (JoDo)', 'respect existing initials') 101 | 102 | t.deepEqual( 103 | initials.addTo(['John Doe', 'Jane Dane'], { 104 | existing: { 105 | 'John Doe': 'JD' 106 | } 107 | }), ['John Doe (JD)', 'Jane Dane (JDa)'], 'respect existing initials') 108 | 109 | t.end() 110 | }) 111 | 112 | test('initials.parse(name)', function (t) { 113 | t.deepEqual(initials.parse('John Doe'), { name: 'John Doe', initials: 'JD' }, 'John Doe ☛ name: John Doe, initials: JD') 114 | t.deepEqual(initials.parse('JD'), { initials: 'JD' }, 'JD ☛ initials: JD') 115 | t.deepEqual(initials.parse('joe@example.com'), { email: 'joe@example.com', initials: 'jo' }, 'joe@example.com ☛ email: joe@example.com, initials: jo') 116 | t.deepEqual(initials.parse('John Doe '), { name: 'John Doe', initials: 'JD', email: 'joe@example.com' }, 'joe@example.com ☛ email: joe@example.com, initials: jo') 117 | 118 | t.end() 119 | }) 120 | 121 | test('initials.parse(namesArray)', function (t) { 122 | t.deepEqual(initials.parse(['John Doe', 'Robert Roe', 'Larry Loe']), [{ name: 'John Doe', initials: 'JD' }, { name: 'Robert Roe', initials: 'RR' }, { name: 'Larry Loe', initials: 'LL' }], 'John Doe, Robert Roe, Larry Loe ☛ name: John Doe, initials: JD; name: Robert Roe, initials: RR; name: Larry Loe, initials: LL') 123 | 124 | t.end() 125 | }) 126 | 127 | test('initials.parse(nameOrNames, {existing: initialsForNames})', function (t) { 128 | t.deepEqual(initials.parse('John Doe', { 129 | existing: { 130 | 'John Doe': 'JoDo' 131 | } 132 | }), { name: 'John Doe', initials: 'JoDo' }, 'respect existing initials for single name') 133 | 134 | t.deepEqual(initials.parse(['John Doe', 'Jane Dane'], { 135 | existing: { 136 | 'John Doe': 'JD' 137 | } 138 | }), [{ name: 'John Doe', initials: 'JD' }, { name: 'Jane Dane', initials: 'JDa' }], 'respect existing initials for multiple names') 139 | 140 | t.end() 141 | }) 142 | 143 | test('initials(), no params', function (t) { 144 | t.equal(initials(), '', 'initials() without nameOrNames, no initials') 145 | t.equal(initials.addTo(), '', 'initials.addTo() without nameOrNames, no initials') 146 | t.deepEqual(initials.parse(), {}, 'initials.parse() without nameOrNames, no initials') 147 | 148 | t.deepEqual(initials(['', '']), ['', ''], 'initials with multiple persons but no names') 149 | 150 | t.end() 151 | }) 152 | 153 | test('initials(), name.length is less 3', function (t) { 154 | t.equal(initials('K'), 'K', 'name.length is < 2, so the initials are equal to name') 155 | t.equal(initials('Mo'), 'Mo', 'name.length is < 3, so the initials are equal to name') 156 | 157 | t.end() 158 | }) 159 | 160 | // uncomment the block below to run a single test only 161 | // test.only('debug', function (t) { 162 | // t.equal(initials('foo'), 'bar', 'debugging') 163 | // }) 164 | --------------------------------------------------------------------------------