├── .gitignore ├── .travis.yml ├── phpunit.xml.dist ├── composer.json ├── LICENSE ├── README.md ├── tests └── SlugifierTest.php └── src └── slugifier.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | bin/ 4 | vendor/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | 7 | before_script: 8 | - curl -s http://getcomposer.org/installer | php 9 | - php composer.phar install --dev 10 | 11 | script: bin/phpunit 12 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keyvanakbary/slugifier", 3 | "description": "A full-featured, simple, clean and pure functional implementation for creating slugs", 4 | "keywords": ["slugifier", "slug", "slugify", "url", "urlize"], 5 | "license": "MIT", 6 | "minimum-stability": "stable", 7 | "authors": [ 8 | { 9 | "name": "Keyvan Akbary", 10 | "email": "kiwwito@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.6" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "~4.0" 18 | }, 19 | "autoload": { 20 | "files": ["src/slugifier.php"] 21 | }, 22 | "config": { 23 | "bin-dir": "bin" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Keyvan Akbary 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slugifier 2 | 3 | [![Build Status](https://secure.travis-ci.org/keyvanakbary/slugifier.svg?branch=master)](http://travis-ci.org/keyvanakbary/slugifier) 4 | 5 | A full-featured, simple, clean and pure functional implementation for creating [slugs](http://en.wikipedia.org/wiki/Semantic_URL#Slug). No third party libraries or extensions needed. 6 | 7 | ## Installation 8 | 9 | ``` bash 10 | composer require keyvanakbary/slugifier 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```php 16 | use slugifier as s; 17 | 18 | s\slugify('JúST å fëw wørds'); // just-a-few-words 19 | 20 | s\slugify('Αυτή είναι μια δοκιμή'); // ayti-einai-mia-dokimi 21 | 22 | s\slugify('Wikipedia style', '_'); // wikipedia_style 23 | ``` 24 | 25 | ### Modifiers 26 | 27 | Sometimes the default character map is not accurate enough. Slugifier supports custom *modifiers* 28 | 29 | ```php 30 | s\slugify('Pingüino', '-', ['ü' => 'u']); // pinguino 31 | ``` 32 | 33 | Some [language iso code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) modifiers are supported 34 | 35 | ```php 36 | s\slugify('Estaĵo', '-', s\MOD['eo']); // estajxo 37 | 38 | s\slugify('Örnektir', '-', s\MOD['tr']); // ornektir 39 | ``` 40 | -------------------------------------------------------------------------------- /tests/SlugifierTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($expectedSlug, $slug); 16 | } 17 | 18 | public function supportedStrings() 19 | { 20 | return [ 21 | ['Word', 'word'], 22 | ['JúST å fëw wørds', 'just-a-few-words'], 23 | ['J\'étudie le français', 'j-etudie-le-francais'], 24 | ['An awesome slug', 'an-awesome-slug'], 25 | [' should trim this text ', 'should-trim-this-text'], 26 | ['Práctica de acentuación', 'practica-de-acentuacion'], 27 | ['Cumpleaños del muerciélago', 'cumpleanos-del-muercielago'], 28 | ['هذا هو الاختبار', 'hth-ho-l-khtb-r'], 29 | ['Блоґ їжачка', 'blog-jizhachka'], 30 | ['Это тест', 'eto-test'], 31 | ['Це тест', 'ce-test'], 32 | ['Đây là một thử nghiệm', 'day-la-mot-thu-nghiem'], 33 | ['Αυτή είναι μια δοκιμή', 'ayti-einai-mia-dokimi'], 34 | ['°¹²³@¶', '0123atp'] 35 | ]; 36 | } 37 | 38 | /** 39 | * @test 40 | * @dataProvider notSupportedStrings 41 | */ 42 | public function shouldCreateEmptySlug($text) 43 | { 44 | $this->assertEmpty(slugify($text)); 45 | } 46 | 47 | public function notSupportedStrings() 48 | { 49 | return [ 50 | [' ..`-. '], 51 | ['테스트'], 52 | ['- --'], 53 | ['這是一個測試'] 54 | ]; 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function shouldCreateSlugWithCustomSeparator() 61 | { 62 | $slug = slugify('Wikipedia style', '_'); 63 | 64 | $this->assertEquals('wikipedia_style', $slug); 65 | } 66 | 67 | /** 68 | * @test 69 | * @dataProvider slugModifiers 70 | */ 71 | public function shouldCreateSlugsWithModifiers($text, $modifier, $expectedSlug) 72 | { 73 | $slug = slugify($text, '-', $modifier); 74 | 75 | $this->assertEquals($expectedSlug, $slug); 76 | } 77 | 78 | public function slugModifiers() 79 | { 80 | return [ 81 | ['Bu bir örnektir', MOD['tr'], 'bu-bir-ornektir'], 82 | ['Supernatura estaĵo', MOD['eo'], 'supernatura-estajxo'], 83 | ['Interesting flavors', ['o' => 'ou'], 'interesting-flavours'] 84 | ]; 85 | } 86 | 87 | /** 88 | * @test 89 | * @dataProvider supportedIsoModifiers 90 | */ 91 | public function shouldSupportIsoModifiers($iso) 92 | { 93 | $this->assertNotNull(MOD[$iso]); 94 | } 95 | 96 | public function supportedIsoModifiers() 97 | { 98 | return [ 99 | ['eo'], 100 | ['tr'] 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/slugifier.php: -------------------------------------------------------------------------------- 1 | '0', 'æ' => 'ae', 'ǽ' => 'ae', 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Å' => 'A', 'Ǻ' => 'A', 8 | 'Ă' => 'A', 'Ǎ' => 'A', 'Æ' => 'AE', 'Ǽ' => 'AE', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'å' => 'a', 9 | 'ǻ' => 'a', 'ă' => 'a', 'ǎ' => 'a', 'ª' => 'a', '@' => 'at', 'Ĉ' => 'C', 'Ċ' => 'C', 'ĉ' => 'c', 'ċ' => 'c', 10 | '©' => 'c', 'Ð' => 'Dj', 'Đ' => 'D', 'ð' => 'dj', 'đ' => 'd', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 11 | 'Ĕ' => 'E', 'Ė' => 'E', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ĕ' => 'e', 'ė' => 'e', 'ƒ' => 'f', 12 | 'Ĝ' => 'G', 'Ġ' => 'G', 'ĝ' => 'g', 'ġ' => 'g', 'Ĥ' => 'H', 'Ħ' => 'H', 'ĥ' => 'h', 'ħ' => 'h', 'Ì' => 'I', 13 | 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ĩ' => 'I', 'Ĭ' => 'I', 'Ǐ' => 'I', 'Į' => 'I', 'IJ' => 'IJ', 'ì' => 'i', 14 | 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ĩ' => 'i', 'ĭ' => 'i', 'ǐ' => 'i', 'į' => 'i', 'ij' => 'ij', 'Ĵ' => 'J', 15 | 'ĵ' => 'j', 'Ĺ' => 'L', 'Ľ' => 'L', 'Ŀ' => 'L', 'ĺ' => 'l', 'ľ' => 'l', 'ŀ' => 'l', 'Ñ' => 'N', 'ñ' => 'n', 16 | 'ʼn' => 'n', 'Ò' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ō' => 'O', 'Ŏ' => 'O', 'Ǒ' => 'O', 'Ő' => 'O', 'Ơ' => 'O', 17 | 'Ø' => 'O', 'Ǿ' => 'O', 'Œ' => 'OE', 'ò' => 'o', 'ô' => 'o', 'õ' => 'o', 'ō' => 'o', 'ŏ' => 'o', 'ǒ' => 'o', 18 | 'ő' => 'o', 'ơ' => 'o', 'ø' => 'o', 'ǿ' => 'o', 'º' => 'o', 'œ' => 'oe', 'Ŕ' => 'R', 'Ŗ' => 'R', 'ŕ' => 'r', 19 | 'ŗ' => 'r', 'Ŝ' => 'S', 'Ș' => 'S', 'ŝ' => 's', 'ș' => 's', 'ſ' => 's', 'Ţ' => 'T', 'Ț' => 'T', 'Ŧ' => 'T', 20 | 'Þ' => 'TH', 'ţ' => 't', 'ț' => 't', 'ŧ' => 't', 'þ' => 'th', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ũ' => 'U', 21 | 'Ŭ' => 'U', 'Ű' => 'U', 'Ų' => 'U', 'Ư' => 'U', 'Ǔ' => 'U', 'Ǖ' => 'U', 'Ǘ' => 'U', 'Ǚ' => 'U', 'Ǜ' => 'U', 22 | 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ũ' => 'u', 'ŭ' => 'u', 'ű' => 'u', 'ų' => 'u', 'ư' => 'u', 'ǔ' => 'u', 23 | 'ǖ' => 'u', 'ǘ' => 'u', 'ǚ' => 'u', 'ǜ' => 'u', 'Ŵ' => 'W', 'ŵ' => 'w', 'Ý' => 'Y', 'Ÿ' => 'Y', 'Ŷ' => 'Y', 24 | 'ý' => 'y', 'ÿ' => 'y', 'ŷ' => 'y', 25 | 26 | // Greek 27 | 'Α' => 'A', 'Β' => 'B', 'Γ' => 'G', 'Δ' => 'D', 'Ε' => 'E', 'Ζ' => 'Z', 'Η' => 'I', 'Θ' => 'Th', 'Ι' => 'I', 28 | 'Κ' => 'K', 'Λ' => 'L', 'Μ' => 'M', 'Ν' => 'N', 'Ξ' => 'Ks', 'Ο' => 'O', 'Π' => 'P', 'Ρ' => 'R', 'Σ' => 'S', 29 | 'Τ' => 'T', 'Υ' => 'Y', 'Φ' => 'Ph', 'Χ' => 'Ch', 'Ψ' => 'Ps', 'Ω' => 'O', 'Ϊ' => 'I', 'Ϋ' => 'Y', 'ά' => 'a', 30 | 'έ' => 'e', 'ή' => 'i', 'ί' => 'i', 'ΰ' => 'Y', 'α' => 'a', 'β' => 'b', 'γ' => 'g', 'δ' => 'd', 'ε' => 'e', 31 | 'ζ' => 'z', 'η' => 'i', 'θ' => 'th', 'ι' => 'i', 'κ' => 'k', 'λ' => 'l', 'μ' => 'm', 'ν' => 'n', 'ξ' => 'ks', 32 | 'ο' => 'o', 'π' => 'p', 'ρ' => 'r', 'ς' => 's', 'σ' => 's', 'τ' => 't', 'υ' => 'y', 'φ' => 'ph', 'χ' => 'x', 33 | 'ψ' => 'ps', 'ω' => 'o', 'ϊ' => 'i', 'ϋ' => 'y', 'ό' => 'o', 'ύ' => 'y', 'ώ' => 'o', 'ϐ' => 'b', 'ϑ' => 'th', 34 | 'ϒ' => 'Y', 35 | 36 | // Turkish 37 | 'Ç' => 'C', 'Ğ' => 'G', 'İ' => 'I', 'Ş' => 'S', 'ç' => 'c', 'ğ' => 'g', 'ı' => 'i', 'ş' => 's', 38 | 39 | // Czech 40 | 'Č' => 'C', 'Ď' => 'D', 'Ě' => 'E', 'Ň' => 'N', 'Ř' => 'R', 'Š' => 'S', 'Ť' => 'T', 'Ů' => 'U', 'Ž' => 'Z', 41 | 'č' => 'c', 'ď' => 'd', 'ě' => 'e', 'ň' => 'n', 'ř' => 'r', 'š' => 's', 'ť' => 't', 'ů' => 'u', 'ž' => 'z', 42 | 43 | // Arabic 44 | 'أ' => 'a', 'ب' => 'b', 'ت' => 't', 'ث' => 'th', 'ج' => 'g', 'ح' => 'h', 'خ' => 'kh', 'د' => 'd', 'ذ' => 'th', 45 | 'ر' => 'r', 'ز' => 'z', 'س' => 's', 'ش' => 'sh', 'ص' => 's', 'ض' => 'd', 'ط' => 't', 'ظ' => 'th', 'ع' => 'aa', 46 | 'غ' => 'gh', 'ف' => 'f', 'ق' => 'k', 'ك' => 'k', 'ل' => 'l', 'م' => 'm', 'ن' => 'n', 'ه' => 'h', 'و' => 'o', 47 | 'ي' => 'y', 48 | 49 | // Vietnamese 50 | 'ạ' => 'a', 'ả' => 'a', 'ầ' => 'a', 'ấ' => 'a', 'ậ' => 'a', 'ẩ' => 'a', 'ẫ' => 'a', 'ằ' => 'a', 'ắ' => 'a', 51 | 'ặ' => 'a', 'ẳ' => 'a', 'ẵ' => 'a', 'ẹ' => 'e', 'ẻ' => 'e', 'ẽ' => 'e', 'ề' => 'e', 'ế' => 'e', 'ệ' => 'e', 52 | 'ể' => 'e', 'ễ' => 'e', 'ị' => 'i', 'ỉ' => 'i', 'ọ' => 'o', 'ỏ' => 'o', 'ồ' => 'o', 'ố' => 'o', 'ộ' => 'o', 53 | 'ổ' => 'o', 'ỗ' => 'o', 'ờ' => 'o', 'ớ' => 'o', 'ợ' => 'o', 'ở' => 'o', 'ỡ' => 'o', 'ụ' => 'u', 'ủ' => 'u', 54 | 'ừ' => 'u', 'ứ' => 'u', 'ự' => 'u', 'ử' => 'u', 'ữ' => 'u', 'ỳ' => 'y', 'ỵ' => 'y', 'ỷ' => 'y', 'ỹ' => 'y', 55 | 'Ạ' => 'A', 'Ả' => 'A', 'Ầ' => 'A', 'Ấ' => 'A', 'Ậ' => 'A', 'Ẩ' => 'A', 'Ẫ' => 'A', 'Ằ' => 'A', 'Ắ' => 'A', 56 | 'Ặ' => 'A', 'Ẳ' => 'A', 'Ẵ' => 'A', 'Ẹ' => 'E', 'Ẻ' => 'E', 'Ẽ' => 'E', 'Ề' => 'E', 'Ế' => 'E', 'Ệ' => 'E', 57 | 'Ể' => 'E', 'Ễ' => 'E', 'Ị' => 'I', 'Ỉ' => 'I', 'Ọ' => 'O', 'Ỏ' => 'O', 'Ồ' => 'O', 'Ố' => 'O', 'Ộ' => 'O', 58 | 'Ổ' => 'O', 'Ỗ' => 'O', 'Ờ' => 'O', 'Ớ' => 'O', 'Ợ' => 'O', 'Ở' => 'O', 'Ỡ' => 'O', 'Ụ' => 'U', 'Ủ' => 'U', 59 | 'Ừ' => 'U', 'Ứ' => 'U', 'Ự' => 'U', 'Ử' => 'U', 'Ữ' => 'U', 'Ỳ' => 'Y', 'Ỵ' => 'Y', 'Ỷ' => 'Y', 'Ỹ' => 'Y', 60 | 61 | // Polish 62 | 'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'E', 'Ł' => 'L', 'Ń' => 'N', 'Ó' => 'O', 'Ś' => 'S', 'Ź' => 'Z', 'Ż' => 'Z', 63 | 'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n', 'ó' => 'o', 'ś' => 's', 'ź' => 'z', 'ż' => 'z', 64 | 65 | // Latvian 66 | 'Ā' => 'A', 'Ē' => 'E', 'Ģ' => 'G', 'Ī' => 'I', 'Ķ' => 'K', 'Ļ' => 'L', 'Ņ' => 'N', 'Ū' => 'U', 'ā' => 'a', 67 | 'ē' => 'e', 'ģ' => 'g', 'ī' => 'i', 'ķ' => 'k', 'ļ' => 'l', 'ņ' => 'n', 'ū' => 'u', 68 | 69 | // German 70 | 'Ä' => 'AE', 'Ö' => 'OE', 'Ü' => 'UE', 'ß' => 'ss', 'ä' => 'ae', 'ö' => 'oe', 'ü' => 'ue', 71 | 72 | // Ukrainian 73 | 'Ґ' => 'G', 'І' => 'I', 'Ї' => 'Ji', 'Є' => 'Ye', 'ґ' => 'g', 'і' => 'i', 'ї' => 'ji', 'є' => 'ye', 74 | 75 | // Serbian 76 | 'ђ' => 'dj', 'ј' => 'j', 'љ' => 'lj', 'њ' => 'nj', 'ћ' => 'c', 'џ' => 'dz', 'Ђ' => 'Dj', 'Ј' => 'j', 77 | 'Љ' => 'Lj', 'Њ' => 'Nj', 'Ћ' => 'C', 'Џ' => 'Dz', 78 | 79 | // Russian 80 | 'Ъ' => '', 'Ь' => '', 'А' => 'A', 'Б' => 'B', 'Ц' => 'C', 'Ч' => 'Ch', 'Д' => 'D', 'Е' => 'E', 'Ё' => 'E', 81 | 'Э' => 'E', 'Ф' => 'F', 'Г' => 'G', 'Х' => 'H', 'И' => 'I', 'Й' => 'J', 'Я' => 'Ja', 'Ю' => 'Ju', 'К' => 'K', 82 | 'Л' => 'L', 'М' => 'M', 'Н' => 'N', 'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Ш' => 'Sh', 'Щ' => 'Shch', 83 | 'Т' => 'T', 'У' => 'U', 'В' => 'V', 'Ы' => 'Y', 'З' => 'Z', 'Ж' => 'Zh', 'ъ' => '', 'ь' => '', 'а' => 'a', 84 | 'б' => 'b', 'ц' => 'c', 'ч' => 'ch', 'д' => 'd', 'е' => 'e', 'ё' => 'e', 'э' => 'e', 'ф' => 'f', 'г' => 'g', 85 | 'х' => 'h', 'и' => 'i', 'й' => 'j', 'я' => 'ja', 'ю' => 'ju', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 86 | 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'ш' => 'sh', 'щ' => 'shch', 'т' => 't', 'у' => 'u', 'в' => 'v', 87 | 'ы' => 'y', 'з' => 'z', 'ж' => 'zh', 88 | 89 | // Other 90 | '¹' => '1', '²' => '2', '³' => '3', '¶' => 'P' 91 | ]; 92 | 93 | const MOD = [ 94 | 'tr' => ['Ö' => 'O', 'Ü' => 'U', 'ö' => 'o', 'ü' => 'u'], 95 | 'eo' => [ 96 | 'ĉ' => 'cx', 'ĝ' => 'gx', 'ĥ' => 'hx', 'ĵ' => 'jx', 'ŝ' => 'sx', 'ŭ' => 'ux', 'Ĉ' => 'CX', 'Ĝ' => 'GX', 97 | 'Ĥ' => 'HX', 'Ĵ' => 'JX', 'Ŝ' => 'SX', 'Ŭ' => 'UX' 98 | ] 99 | ]; 100 | 101 | function slugify($text, $separator = '-', array $modifier = []) 102 | { 103 | $normalized = strtolower(strtr($text, $modifier + CHAR_MAP)); 104 | $cleaned = preg_replace($unwantedChars = '/([^a-z0-9]|-)+/', $separator, $normalized); 105 | 106 | return trim($cleaned, $separator); 107 | } 108 | --------------------------------------------------------------------------------