diff --git a/index.d.ts b/index.d.ts index b28f8a9..29703bd 100644 --- a/index.d.ts +++ b/index.d.ts @@ -113,31 +113,100 @@ declare namespace slugify { } } -/** -Slugify a string. +declare const slugify: { + /** + Slugify a string. -@param string - String to slugify. + @param string - String to slugify. -@example -``` -import slugify = require('@sindresorhus/slugify'); + @example + ``` + import slugify = require('@sindresorhus/slugify'); -slugify('I ♥ Dogs'); -//=> 'i-love-dogs' + slugify('I ♥ Dogs'); + //=> 'i-love-dogs' -slugify(' Déjà Vu! '); -//=> 'deja-vu' + slugify(' Déjà Vu! '); + //=> 'deja-vu' -slugify('fooBar 123 $#%'); -//=> 'foo-bar-123' + slugify('fooBar 123 $#%'); + //=> 'foo-bar-123' -slugify('я люблю единорогов'); -//=> 'ya-lyublyu-edinorogov' -``` -*/ -declare function slugify( - string: string, - options?: slugify.Options -): string; + slugify('я люблю единорогов'); + //=> 'ya-lyublyu-edinorogov' + ``` + */ + ( + string: string, + options?: slugify.Options + ): string; + + /** + Returns a new instance of `slugify(string, options?)` with a counter to handle multiple occurences of the same string. + + @param string - String to slugify. + + @example + ``` + import slugify = require('@sindresorhus/slugify'); + + const countableSlugify = slugify.counter(); + countableSlugify('foo bar'); + //=> 'foo-bar' + + countableSlugify('foo bar'); + //=> 'foo-bar-2' + + countableSlugify.reset(); + + countableSlugify('foo bar'); + //=> 'foo-bar' + ``` + + __Use case example of counter__ + + If, for example, you have a document with multiple sections where each subsection has an example. + + ``` + ## Section 1 + + ### Example + + ## Section 2 + + ### Example + ``` + + You can then use `slugify.counter()` to generate unique HTML `id`'s to ensure anchors will link to the right headline. + */ + counter: () => { + ( + string: string, + options?: slugify.Options + ): string; + + /** + Reset the counter. + + @example + ``` + import slugify = require('@sindresorhus/slugify'); + + const countableSlugify = slugify.counter(); + countableSlugify('foo bar'); + //=> 'foo-bar' + + countableSlugify('foo bar'); + //=> 'foo-bar-2' + + countableSlugify.reset(); + + countableSlugify('foo bar'); + //=> 'foo-bar' + ``` + */ + reset(): void; + }; +} export = slugify; diff --git a/index.js b/index.js index 5ae1569..87ca329 100644 --- a/index.js +++ b/index.js @@ -21,7 +21,7 @@ const removeMootSeparators = (string, separator) => { .replace(new RegExp(`^${escapedSeparator}|${escapedSeparator}$`, 'g'), ''); }; -module.exports = (string, options) => { +const slugify = (string, options) => { if (typeof string !== 'string') { throw new TypeError(`Expected a string, got \`${typeof string}\``); } @@ -65,3 +65,35 @@ module.exports = (string, options) => { return string; }; + +const counter = () => { + const occurrences = new Map(); + + const countable = (string, options) => { + string = slugify(string, options); + + if (!string) { + return ''; + } + + const stringLower = string.toLowerCase(); + const numberless = occurrences.get(stringLower.replace(/(?:-\d+?)+?$/, '')) || 0; + const counter = occurrences.get(stringLower); + occurrences.set(stringLower, typeof counter === 'number' ? counter + 1 : 1); + const newCounter = occurrences.get(stringLower) || 2; + if (newCounter >= 2 || numberless > 2) { + string = `${string}-${newCounter}`; + } + + return string; + }; + + countable.reset = () => { + occurrences.clear(); + }; + + return countable; +}; + +module.exports = slugify; +module.exports.counter = counter; diff --git a/index.test-d.ts b/index.test-d.ts index e6b83bf..b60084e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -5,7 +5,9 @@ expectType(slugify('I ♥ Dogs')); expectType(slugify('BAR and baz', {separator: '_'})); expectType(slugify('Déjà Vu!', {lowercase: false})); expectType(slugify('fooBar', {decamelize: false})); -expectType( - slugify('I ♥ 🦄 & 🐶', {customReplacements: [['🐶', 'dog']]}) -); +expectType(slugify('I ♥ 🦄 & 🐶', {customReplacements: [['🐶', 'dog']]})); expectType(slugify('_foo_bar', {preserveLeadingUnderscore: true})); + +// counter +expectType(slugify.counter()('I ♥ Dogs')); +expectType(slugify.counter().reset()); diff --git a/readme.md b/readme.md index c8d0f9c..3c20534 100644 --- a/readme.md +++ b/readme.md @@ -164,6 +164,68 @@ slugify('_foo_bar', {preserveLeadingUnderscore: true}); //=> '_foo-bar' ``` +### slugify.counter() + +Returns a new instance of `slugify(string, options?)` with a counter to handle multiple occurences of the same string. + +#### Example + +```js +const slugify = require('@sindresorhus/slugify'); + +const countableSlugify = slugify.counter(); + +countableSlugify('foo bar'); +//=> 'foo-bar' + +countableSlugify('foo bar'); +//=> 'foo-bar-2' + +countableSlugify.reset(); + +countableSlugify('foo bar'); +//=> 'foo-bar' +``` + +#### Use-case example of counter + +If, for example, you have a document with multiple sections where each subsection has an example. + +```md +## Section 1 + +### Example + +## Section 2 + +### Example +``` + +You can then use `slugify.counter()` to generate unique HTML `id`'s to ensure anchors will link to the right headline. + +### slugify.reset() + +Reset the counter + +#### Example + +```js +const slugify = require('@sindresorhus/slugify'); + +const countableSlugify = slugify.counter(); + +countableSlugify('foo bar'); +//=> 'foo-bar' + +countableSlugify('foo bar'); +//=> 'foo-bar-2' + +countableSlugify.reset(); + +countableSlugify('foo bar'); +//=> 'foo-bar' +``` + ## Related - [slugify-cli](https://github.com/sindresorhus/slugify-cli) - CLI for this module diff --git a/test.js b/test.js index 2757187..3e46825 100644 --- a/test.js +++ b/test.js @@ -131,3 +131,39 @@ test('leading underscore', t => { t.is(slugify('__foo__bar', {preserveLeadingUnderscore: true}), '_foo-bar'); t.is(slugify('____-___foo__bar', {preserveLeadingUnderscore: true}), '_foo-bar'); }); + +test('counter', t => { + const countableSlugify = slugify.counter(); + t.is(countableSlugify('foo bar'), 'foo-bar'); + t.is(countableSlugify('foo bar'), 'foo-bar-2'); + + countableSlugify.reset(); + + t.is(countableSlugify('foo'), 'foo'); + t.is(countableSlugify('foo'), 'foo-2'); + t.is(countableSlugify('foo 1'), 'foo-1'); + t.is(countableSlugify('foo-1'), 'foo-1-2'); + t.is(countableSlugify('foo-1'), 'foo-1-3'); + t.is(countableSlugify('foo'), 'foo-3'); + t.is(countableSlugify('foo'), 'foo-4'); + t.is(countableSlugify('foo-1'), 'foo-1-4'); + t.is(countableSlugify('foo-2'), 'foo-2-1'); + t.is(countableSlugify('foo-2'), 'foo-2-2'); + t.is(countableSlugify('foo-2-1'), 'foo-2-1-1'); + t.is(countableSlugify('foo-2-1'), 'foo-2-1-2'); + t.is(countableSlugify('foo-11'), 'foo-11-1'); + t.is(countableSlugify('foo-111'), 'foo-111-1'); + t.is(countableSlugify('foo-111-1'), 'foo-111-1-1'); + t.is(countableSlugify('fooCamelCase', {lowercase: false, decamelize: false}), 'fooCamelCase'); + t.is(countableSlugify('fooCamelCase', {decamelize: false}), 'foocamelcase-2'); + t.is(countableSlugify('_foo'), 'foo-5'); + t.is(countableSlugify('_foo', {preserveLeadingUnderscore: true}), '_foo'); + t.is(countableSlugify('_foo', {preserveLeadingUnderscore: true}), '_foo-2'); + + const countableSlugify2 = slugify.counter(); + t.is(countableSlugify2('foo'), 'foo'); + t.is(countableSlugify2('foo'), 'foo-2'); + + t.is(countableSlugify2(''), ''); + t.is(countableSlugify2(''), ''); +});