├── phpstan.neon ├── ecs.php ├── src ├── translations │ └── en │ │ └── typogrify.php ├── services │ ├── ServicesTrait.php │ └── TypogrifyService.php ├── icon.svg ├── Typogrify.php ├── twigextensions │ └── TypogrifyTwigExtension.php ├── config.php ├── variables │ └── TypogrifyVariable.php └── models │ └── Settings.php ├── CHANGELOG.md ├── LICENSE.md ├── composer.json └── README.md /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 8 | __DIR__ . '/src', 9 | __FILE__, 10 | ]); 11 | $ecsConfig->parallel(); 12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); 13 | }; 14 | -------------------------------------------------------------------------------- /src/translations/en/typogrify.php: -------------------------------------------------------------------------------- 1 | 'Typogrify plugin loaded', 18 | ]; 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Typogrify Changelog 2 | 3 | ## 5.0.1 - 2024.06.18 4 | ### Added 5 | * Added `ServicesTrait` for the plugin service component registration 6 | 7 | ## 5.0.0 - 2024.04.16 8 | ### Added 9 | * Stable release for Craft CMS 5 10 | 11 | ## 5.0.0-beta.2 - 2024.03.27 12 | ### Fixed 13 | * Fixed a regression that happened when modernizing the `default_escape` functionality ([#86](https://github.com/nystudio107/craft-typogrify/issues/86)) 14 | 15 | ## 5.0.0-beta.1 - 2024.03.27 16 | ### Added 17 | * Initial Craft CMS 5 compatibility 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) nystudio107 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/services/ServicesTrait.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'typogrify' => TypogrifyService::class, 36 | ], 37 | ]; 38 | } 39 | 40 | // Public Methods 41 | // ========================================================================= 42 | 43 | /** 44 | * Returns the typogrify service 45 | * 46 | * @return TypogrifyService The typogrify service 47 | * @throws InvalidConfigException 48 | */ 49 | public function getTypogrify(): TypogrifyService 50 | { 51 | return $this->get('typogrify'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/craft-typogrify", 3 | "description": "Typogrify prettifies your web typography by preventing ugly quotes and 'widows' and more", 4 | "type": "craft-plugin", 5 | "version": "5.0.1", 6 | "keywords": [ 7 | "craft", 8 | "cms", 9 | "craftcms", 10 | "craft-plugin", 11 | "typogrify" 12 | ], 13 | "support": { 14 | "docs": "https://nystudio107.com/docs/typogrify/", 15 | "issues": "https://nystudio107.com/plugins/typogrify/support", 16 | "source": "https://github.com/nystudio107/craft-typogrify" 17 | }, 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "nystudio107", 22 | "homepage": "https://nystudio107.com/" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "prefer-stable": true, 27 | "require": { 28 | "php": "^8.2", 29 | "craftcms/cms": "^5.0.0", 30 | "michelf/php-smartypants": "^1.8", 31 | "mundschenk-at/php-typography": "^6.0" 32 | }, 33 | "require-dev": { 34 | "craftcms/ecs": "dev-main", 35 | "craftcms/phpstan": "dev-main", 36 | "craftcms/rector": "dev-main" 37 | }, 38 | "scripts": { 39 | "phpstan": "phpstan --ansi --memory-limit=1G", 40 | "check-cs": "ecs check --ansi", 41 | "fix-cs": "ecs check --fix --ansi" 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "craftcms/plugin-installer": true, 46 | "yiisoft/yii2-composer": true 47 | }, 48 | "optimize-autoloader": true, 49 | "sort-packages": true 50 | }, 51 | "autoload": { 52 | "psr-4": { 53 | "nystudio107\\typogrify\\": "src/" 54 | } 55 | }, 56 | "extra": { 57 | "class": "nystudio107\\typogrify\\Typogrify", 58 | "handle": "typogrify", 59 | "name": "Typogrify" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/badges/quality-score.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/?branch=v1) [![Code Coverage](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/badges/coverage.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/?branch=v1) [![Build Status](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/badges/build.png?b=v1)](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/build-status/v1) [![Code Intelligence Status](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/badges/code-intelligence.svg?b=v1)](https://scrutinizer-ci.com/code-intelligence) 2 | 3 | # Typogrify plugin for Craft CMS 4.x 4 | 5 | Typogrify prettifies your web typography by preventing ugly quotes and 'widows' and more 6 | 7 | ![Screenshot](./docs/docs/resources/img/plugin-logo.png) 8 | 9 | ## Requirements 10 | 11 | This plugin requires Craft CMS 4.0.0 or later. 12 | 13 | ## Installation 14 | 15 | To install the plugin, follow these instructions. 16 | 17 | 1. Open your terminal and go to your Craft project: 18 | 19 | cd /path/to/project 20 | 21 | 2. Then tell Composer to require the plugin: 22 | 23 | composer require nystudio107/craft-typogrify 24 | 25 | 3. Install the plugin via `./craft install/plugin typogrify` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Typogrify. 26 | 27 | You can also install Typogrify via the **Plugin Store** in the Craft Control Panel. 28 | 29 | ## Documentation 30 | 31 | Click here -> [Typogrify Documentation](https://nystudio107.com/plugins/typogrify/documentation) 32 | 33 | ## Typogrify Roadmap 34 | 35 | Some things to do, and ideas for potential features: 36 | 37 | * Whatever else @mikestecker asks for 38 | 39 | Brought to you by [nystudio107](https://nystudio107.com/) 40 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 17 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Typogrify.php: -------------------------------------------------------------------------------- 1 | view->registerTwigExtension(new TypogrifyTwigExtension()); 82 | // Register our variables 83 | Event::on( 84 | CraftVariable::class, 85 | CraftVariable::EVENT_INIT, 86 | static function(Event $event) { 87 | /** @var CraftVariable $variable */ 88 | $variable = $event->sender; 89 | $variable->set('typogrify', self::$variable); 90 | } 91 | ); 92 | 93 | Craft::info( 94 | Craft::t( 95 | 'typogrify', 96 | '{name} plugin loaded', 97 | ['name' => $this->name] 98 | ), 99 | __METHOD__ 100 | ); 101 | } 102 | 103 | // Protected Methods 104 | // ========================================================================= 105 | 106 | /** 107 | * @inheritdoc 108 | */ 109 | protected function createSettingsModel(): ?Model 110 | { 111 | return new Settings(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/twigextensions/TypogrifyTwigExtension.php: -------------------------------------------------------------------------------- 1 | false, 31 | 32 | // sets tags where typography of children will be untouched 33 | "set_tags_to_ignore" => [ 34 | "code", 35 | "head", 36 | "kbd", 37 | "object", 38 | "option", 39 | "pre", 40 | "samp", 41 | "script", 42 | "noscript", 43 | "noembed", 44 | "select", 45 | "style", 46 | "textarea", 47 | "title", 48 | "var", 49 | "math", 50 | ], 51 | 52 | // sets classes where typography of children will be untouched 53 | "set_classes_to_ignore" => [ 54 | "vcard", 55 | "noTypo", 56 | ], 57 | 58 | // sets IDs where typography of children will be untouched 59 | "set_ids_to_ignore" => [ 60 | ], 61 | 62 | // curl quotemarks 63 | "set_smart_quotes" => true, 64 | 65 | // Primary quotemarks style 66 | // allowed values for $style 67 | // "doubleCurled" => "“foo”", 68 | // "doubleCurledReversed" => "”foo”", 69 | // "doubleLow9" => "„foo”", 70 | // "doubleLow9Reversed" => "„foo“", 71 | // "singleCurled" => "‘foo’", 72 | // "singleCurledReversed" => "’foo’", 73 | // "singleLow9" => "‚foo’", 74 | // "singleLow9Reversed" => "‚foo‘", 75 | // "doubleGuillemetsFrench" => "« foo »", 76 | // "doubleGuillemets" => "«foo»", 77 | // "doubleGuillemetsReversed" => "»foo«", 78 | // "singleGuillemets" => "‹foo›", 79 | // "singleGuillemetsReversed" => "›foo‹", 80 | // "cornerBrackets" => "「foo」", 81 | // "whiteCornerBracket" => "『foo』", 82 | "set_smart_quotes_primary" => Quote_Style::DOUBLE_CURLED, 83 | 84 | // Secondary quotemarks style 85 | // allowed values for $style 86 | // "doubleCurled" => "“foo”", 87 | // "doubleCurledReversed" => "”foo”", 88 | // "doubleLow9" => "„foo”", 89 | // "doubleLow9Reversed" => "„foo“", 90 | // "singleCurled" => "‘foo’", 91 | // "singleCurledReversed" => "’foo’", 92 | // "singleLow9" => "‚foo’", 93 | // "singleLow9Reversed" => "‚foo‘", 94 | // "doubleGuillemetsFrench" => "« foo »", 95 | // "doubleGuillemets" => "«foo»", 96 | // "doubleGuillemetsReversed" => "»foo«", 97 | // "singleGuillemets" => "‹foo›", 98 | // "singleGuillemetsReversed" => "›foo‹", 99 | // "cornerBrackets" => "「foo」", 100 | // "whiteCornerBracket" => "『foo』", 101 | "set_smart_quotes_secondary" => Quote_Style::SINGLE_CURLED, 102 | 103 | // replaces "a--a" with En Dash " -- " and "---" with Em Dash 104 | "set_smart_dashes" => true, 105 | 106 | // Sets the typographical conventions used by smart_dashes. 107 | // 108 | // Allowed values for $style: 109 | // - "traditionalUS" 110 | // - "international" 111 | "set_smart_dashes_style" => Dash_Style::TRADITIONAL_US, 112 | 113 | // replaces "..." with "…" 114 | "set_smart_ellipses" => true, 115 | 116 | // replaces "creme brulee" with "crème brûlée" 117 | "set_smart_diacritics" => true, 118 | 119 | // defines hyphenation language for text 120 | "set_diacritic_language" => "en-US", 121 | 122 | // $customReplacements must be 123 | // an array formatted array(needle=>replacement, needle=>replacement...), or 124 | // a string formatted `"needle"=>"replacement","needle"=>"replacement",...` 125 | "set_diacritic_custom_replacements" => [ 126 | ], 127 | 128 | // replaces (r) (c) (tm) (sm) (p) (R) (C) (TM) (SM) (P) with ® © ™ ℠ ℗ 129 | "set_smart_marks" => true, 130 | 131 | // replaces 1*4 with 1x4, etc. 132 | "set_smart_math" => true, 133 | 134 | // replaces 2^4 with 24 135 | "set_smart_exponents" => true, 136 | 137 | // replaces 1/4 with 14 138 | "set_smart_fractions" => true, 139 | 140 | // Enables/disables replacement of 1st with 1st 141 | "set_smart_ordinal_suffix" => true, 142 | 143 | // single character words are forced to next line with insertion of   144 | "set_single_character_word_spacing" => true, 145 | 146 | // fractions are kept together with insertion of   147 | "set_fraction_spacing" => true, 148 | 149 | // units and values are kept together with insertion of   150 | "set_unit_spacing" => true, 151 | 152 | // Enables/disables extra whitespace before certain punction marks, as is the French custom. 153 | "set_french_punctuation_spacing" => false, 154 | 155 | // a list of units to keep with their values 156 | "set_units" => [ 157 | ], 158 | 159 | // Em and En dashes are wrapped in thin spaces 160 | "set_dash_spacing" => true, 161 | 162 | // Remove extra space characters 163 | "set_space_collapse" => true, 164 | 165 | // Enable usage of true "no-break narrow space" ( ) instead of the normal no-break space ( ). 166 | "set_true_no_break_narrow_space" => false, 167 | 168 | // enables widow handling 169 | "set_dewidow" => true, 170 | 171 | // establishes maximum length of a widows that will be protected 172 | "set_max_dewidow_length" => 5, 173 | 174 | // establishes the maximum number of words considered for dewidowing. 175 | "set_dewidow_word_number" => 1, 176 | 177 | // establishes maximum length of pulled text to keep widows company 178 | "set_max_dewidow_pull" => 5, 179 | 180 | // enables wrapping at hard hyphens internal to a word with the insertion of a zero-width-space 181 | "set_wrap_hard_hyphens" => true, 182 | 183 | // enables wrapping of urls 184 | "set_url_wrap" => true, 185 | 186 | // enables wrapping of email addresses 187 | "set_email_wrap" => true, 188 | 189 | // establishes minimum character requirement after a url wrapping point 190 | "set_min_after_url_wrap" => 5, 191 | 192 | // wrap ampersands in 193 | "set_style_ampersands" => true, 194 | 195 | // wrap caps in 196 | "set_style_caps" => true, 197 | 198 | // wrap initial quotes in or 199 | "set_style_initial_quotes" => true, 200 | 201 | // wrap numbers in 202 | "set_style_numbers" => true, 203 | 204 | // sets tags where initial quotes and guillemets should be styled 205 | "set_initial_quote_tags" => [ 206 | "p", 207 | "h1", 208 | "h2", 209 | "h3", 210 | "h4", 211 | "h5", 212 | "h6", 213 | "blockquote", 214 | "li", 215 | "dd", 216 | "dt", 217 | ], 218 | 219 | // enables hyphenation of text 220 | "set_hyphenation" => true, 221 | 222 | // defines hyphenation language for text 223 | "set_hyphenation_language" => "en-US", 224 | 225 | // establishes minimum length of a word that may be hyphenated 226 | "set_min_length_hyphenation" => 5, 227 | 228 | // establishes minimum character requirement before a hyphenation point 229 | "set_min_before_hyphenation" => 3, 230 | 231 | // establishes minimum character requirement after a hyphenation point 232 | "set_min_after_hyphenation" => 2, 233 | 234 | // allows/disallows hyphenation of title/heading text 235 | "set_hyphenate_headings" => true, 236 | 237 | // allows hyphenation of strings of all capital characters 238 | "set_hyphenate_all_caps" => true, 239 | 240 | // allows hyphenation of strings of all capital characters 241 | "set_hyphenate_title_case" => true, 242 | 243 | // defines custom word hyphenations 244 | // expected input is an array of words with all hyphenation points marked with a hard hyphen 245 | "set_hyphenation_exceptions" => [ 246 | ], 247 | 248 | // Enable lenient parser error handling (HTML is "best guess" if enabled). 249 | "set_ignore_parser_errors" => true, 250 | 251 | // Sets an optional handler for parser errors. Invalid callbacks will be silently ignored 252 | "set_parser_errors_handler" => null, 253 | ]; 254 | -------------------------------------------------------------------------------- /src/variables/TypogrifyVariable.php: -------------------------------------------------------------------------------- 1 | normalizeText($text); 45 | return Template::raw(Typogrify::$plugin->typogrify->typogrify($text, $isTitle)); 46 | } 47 | 48 | /** 49 | * Typogrify applies a veritable kitchen sink of typographic treatments to 50 | * beautify your web typography but in a way that is appropriate for RSS 51 | * (or similar) feeds -- i.e. excluding processes that may cause issues in 52 | * contexts with limited character set intelligence. 53 | * 54 | * @param string|int|float|null $text The text or HTML fragment to process 55 | * @param bool $isTitle Optional. If the HTML fragment is a title. 56 | * Default false 57 | * 58 | * @return Markup 59 | */ 60 | public function typogrifyFeed(string|int|float|null $text, bool $isTitle = false): Markup 61 | { 62 | $text = $this->normalizeText($text); 63 | return Template::raw(Typogrify::$plugin->typogrify->typogrifyFeed($text, $isTitle)); 64 | } 65 | 66 | /** 67 | * @param string|int|float|null $text 68 | * 69 | * @return Markup 70 | */ 71 | public function smartypants(string|int|float|null $text): Markup 72 | { 73 | $text = $this->normalizeText($text); 74 | return Template::raw(Typogrify::$plugin->typogrify->smartypants($text)); 75 | } 76 | 77 | /** 78 | * @return Settings 79 | */ 80 | public function getPhpTypographySettings(): Settings 81 | { 82 | return Typogrify::$plugin->typogrify->phpTypographySettings; 83 | } 84 | 85 | /** 86 | * Truncates the string to a given length. If $substring is provided, and 87 | * truncating occurs, the string is further truncated so that the substring 88 | * may be appended without exceeding the desired length. 89 | * 90 | * @param string|int|float|null $string The string to truncate 91 | * @param int $length Desired length of the truncated string 92 | * @param string $substring The substring to append if it can fit 93 | * 94 | * @return string with the resulting $str after truncating 95 | */ 96 | public function truncate(string|int|float|null $string, int $length, string $substring = '…'): string 97 | { 98 | return Typogrify::$plugin->typogrify->truncate($string, $length, $substring); 99 | } 100 | 101 | /** 102 | * Truncates the string to a given length, while ensuring that it does not 103 | * split words. If $substring is provided, and truncating occurs, the 104 | * string is further truncated so that the substring may be appended without 105 | * exceeding the desired length. 106 | * 107 | * @param string|int|float|null $string The string to truncate 108 | * @param int $length Desired length of the truncated string 109 | * @param string $substring The substring to append if it can fit 110 | * 111 | * @return string with the resulting $str after truncating 112 | */ 113 | public function truncateOnWord(string|int|float|null $string, int $length, string $substring = '…'): string 114 | { 115 | return Typogrify::$plugin->typogrify->truncateOnWord($string, $length, $substring); 116 | } 117 | 118 | /** 119 | * Creates a Stringy object and assigns both string and encoding properties 120 | * the supplied values. $string is cast to a string prior to assignment, and if 121 | * $encoding is not specified, it defaults to mb_internal_encoding(). It 122 | * then returns the initialized object. Throws an InvalidArgumentException 123 | * if the first argument is an array or object without a __toString method. 124 | * 125 | * @param string|int|float|null $string The string initialize the Stringy object with 126 | * @param null|string $encoding The character encoding 127 | * 128 | * @return Stringy 129 | */ 130 | public function stringy(string|int|float|null $string = '', ?string $encoding = null): Stringy 131 | { 132 | return Typogrify::$plugin->typogrify->stringy($string, $encoding); 133 | } 134 | 135 | /** 136 | * Formats the value in bytes as a size in human readable form for example `12 KB`. 137 | * 138 | * This is the short form of [[asSize]]. 139 | * 140 | * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) 141 | * (e.g. kibibyte/KiB, mebibyte/MiB, ...) are used in the formatting result. 142 | * 143 | * @param string|int|float $bytes value in bytes to be formatted. 144 | * @param int $decimals the number of digits after the decimal point. 145 | * 146 | * @return string the formatted result. 147 | */ 148 | public function humanFileSize(string|int|float $bytes, int $decimals = 1): string 149 | { 150 | return Typogrify::$plugin->typogrify->humanFileSize($bytes, $decimals); 151 | } 152 | 153 | /** 154 | * Represents the value as duration in human readable format. 155 | * 156 | * @param DateInterval|string|int $value the value to be formatted. Acceptable formats: 157 | * - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php) 158 | * - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds` 159 | * - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration: 160 | * `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values 161 | * `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value 162 | * `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value 163 | * `P1D2H30M` - simply a date interval 164 | * `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`) 165 | * 166 | * @return string the formatted duration. 167 | */ 168 | public function humanDuration(DateInterval|string|int $value): string 169 | { 170 | return Typogrify::$plugin->typogrify->humanDuration($value); 171 | } 172 | 173 | /** 174 | * Formats the value as the time interval between a date and now in human readable form. 175 | * 176 | * This method can be used in three different ways: 177 | * 178 | * 1. Using a timestamp that is relative to `now`. 179 | * 2. Using a timestamp that is relative to the `$referenceTime`. 180 | * 3. Using a `DateInterval` object. 181 | * 182 | * @param int|string|DateTime|DateInterval $value the value to be formatted. The following 183 | * types of value are supported: 184 | * 185 | * - an integer representing a UNIX timestamp 186 | * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php). 187 | * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given. 188 | * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object 189 | * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future) 190 | * 191 | * @param null|int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now` 192 | * when `$value` is not a `DateInterval` object. 193 | * 194 | * @return string the formatted result. 195 | */ 196 | public function humanRelativeTime(int|string|DateTime|DateInterval $value, null|int|string|DateTime $referenceTime = null): string 197 | { 198 | return Typogrify::$plugin->typogrify->humanRelativeTime($value, $referenceTime); 199 | } 200 | 201 | /** 202 | * Converts number to its ordinal English form 203 | * For example, converts 13 to 13th, 2 to 2nd 204 | * 205 | * @param int $number 206 | * 207 | * @return string 208 | */ 209 | public function ordinalize(int $number): string 210 | { 211 | return Typogrify::$plugin->typogrify->ordinalize($number); 212 | } 213 | 214 | /** 215 | * Converts a word to its plural form 216 | * For example, 'apple' will become 'apples', and 'child' will become 'children' 217 | * 218 | * @param string $word 219 | * @param int $number 220 | * 221 | * @return string 222 | */ 223 | public function pluralize(string $word, int $number = 2): string 224 | { 225 | return Typogrify::$plugin->typogrify->pluralize($word, $number); 226 | } 227 | 228 | /** 229 | * Converts a word to its singular form 230 | * For example, 'apples' will become 'apple', and 'children' will become 'child' 231 | * 232 | * @param string $word 233 | * @param int $number 234 | * 235 | * @return string 236 | */ 237 | public function singularize(string $word, int $number = 1): string 238 | { 239 | return Typogrify::$plugin->typogrify->singularize($word, $number); 240 | } 241 | 242 | /** 243 | * Returns transliterated version of a string 244 | * For example, 获取到 どちら Українська: ґ,є, Српска: ђ, њ, џ! ¿Español? 245 | * will be transliterated to huo qu dao dochira Ukrainsʹka: g,e, Srpska: d, n, d! ¿Espanol? 246 | * 247 | * @param string $string 248 | * @param null $transliterator 249 | * 250 | * @return string 251 | */ 252 | public function transliterate(string $string, $transliterator = null): string 253 | { 254 | return Typogrify::$plugin->typogrify->transliterate($string, $transliterator); 255 | } 256 | 257 | /** 258 | * Limits a string by word count. If $substring is provided, and truncating occurs, the 259 | * string is further truncated so that the substring may be appended without 260 | * exceeding the desired length. 261 | * 262 | * @param string $string 263 | * @param int $length 264 | * @param string $substring 265 | * 266 | * @return string 267 | */ 268 | public function wordLimit(string $string, int $length, string $substring = '…'): string 269 | { 270 | return Typogrify::$plugin->typogrify->wordLimit($string, $length, $substring); 271 | } 272 | 273 | // Private Methods 274 | // ========================================================================= 275 | 276 | /** 277 | * Normalize the passed in text to ensure that untrusted strings are escaped 278 | * 279 | * @param $text 280 | * 281 | * @return string 282 | */ 283 | private function normalizeText($text): string 284 | { 285 | /* @TODO: try to resolve at a later date; Twig's `| raw` just returns a string, not `Markup` so we can't use that as a check 286 | * if ($text instanceof Markup) { 287 | * // Either came from a Redactor field (or the like) or they manually added a |raw tag. We can trust it 288 | * $text = (string)$text; 289 | * } else { 290 | * // We don't trust it, so escape any HTML 291 | * $twig = Craft::$app->view->twig; 292 | * try { 293 | * $text = twig_escape_filter($twig, $text); 294 | * } catch (\Twig_Error_Runtime $e) { 295 | * $error = $e->getMessage(); 296 | * Craft::error($error, __METHOD__); 297 | * // We don't want unescaped text slipping through, so set the text to the error message 298 | * $text = $error; 299 | * } 300 | * } 301 | */ 302 | // If it's null or otherwise empty, just return an empty string 303 | if (empty($text)) { 304 | $text = ''; 305 | } 306 | $text = (string)$text; 307 | 308 | $settings = Typogrify::$plugin->getSettings(); 309 | 310 | if ($settings && $settings['default_escape'] === true) { 311 | $twig = Craft::$app->getView()->getTwig(); 312 | $twig_escape_filter = $twig->getFilter('e'); 313 | if ($twig_escape_filter) { 314 | $text = $twig_escape_filter->getCallable()($twig, $text); 315 | } 316 | } 317 | 318 | return $text; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/services/TypogrifyService.php: -------------------------------------------------------------------------------- 1 | phpTypographySettings = new Settings(); 57 | 58 | // Create a new PhpTypography instance 59 | $this->phpTypography = new PHP_Typography(); 60 | 61 | // Apply our default settings 62 | $settings = Typogrify::$plugin->getSettings(); 63 | if ($settings) { 64 | $settingsArray = $settings->toArray(); 65 | foreach ($settingsArray as $key => $value) { 66 | if ($key !== 'default_escape') { 67 | $this->phpTypographySettings->{$key}($value); 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Typogrify applies a veritable kitchen sink of typographic treatments to 75 | * beautify your web typography 76 | * 77 | * @param string|int|float|null $text The text or HTML fragment to process 78 | * @param bool $isTitle Optional. If the HTML fragment is a title. 79 | * Default false 80 | * 81 | * @return string The processed HTML 82 | */ 83 | public function typogrify(string|int|float|null $text, bool $isTitle = false): string 84 | { 85 | if (empty($text)) { 86 | return ''; 87 | } 88 | 89 | return $this->phpTypography->process((string)$text, $this->phpTypographySettings, $isTitle); 90 | } 91 | 92 | /** 93 | * Typogrify applies a veritable kitchen sink of typographic treatments to 94 | * beautify your web typography but in a way that is appropriate for RSS 95 | * (or similar) feeds -- i.e. excluding processes that may cause issues in 96 | * contexts with limited character set intelligence. 97 | * 98 | * @param string|int|float|null $text The text or HTML fragment to process 99 | * @param bool $isTitle Optional. If the HTML fragment is a title. 100 | * Default false 101 | * 102 | * @return string The processed HTML 103 | */ 104 | public function typogrifyFeed(string|int|float|null $text, bool $isTitle = false): string 105 | { 106 | if (empty($text)) { 107 | return ''; 108 | } 109 | 110 | return $this->phpTypography->process_feed((string)$text, $this->phpTypographySettings, $isTitle); 111 | } 112 | 113 | /** 114 | * @param string|int|float|null $text 115 | * 116 | * @return string 117 | */ 118 | public function smartypants(string|int|float|null $text): string 119 | { 120 | if (empty($text)) { 121 | return ''; 122 | } 123 | 124 | return SmartyPants::defaultTransform((string)$text); 125 | } 126 | 127 | /** 128 | * Truncates the string to a given length. If $substring is provided, and 129 | * truncating occurs, the string is further truncated so that the substring 130 | * may be appended without exceeding the desired length. 131 | * 132 | * @param string|int|float|null $string The string to truncate 133 | * @param int $length Desired length of the truncated string 134 | * @param string $substring The substring to append if it can fit 135 | * 136 | * @return string with the resulting $str after truncating 137 | */ 138 | public function truncate(string|int|float|null $string, int $length, string $substring = '…'): string 139 | { 140 | $result = (string)$string; 141 | 142 | if (!empty($string)) { 143 | $string = strip_tags($string); 144 | $result = (string)Stringy::create($string)->truncate($length, $substring); 145 | } 146 | 147 | return $result; 148 | } 149 | 150 | /** 151 | * Truncates the string to a given length, while ensuring that it does not 152 | * split words. If $substring is provided, and truncating occurs, the 153 | * string is further truncated so that the substring may be appended without 154 | * exceeding the desired length. 155 | * 156 | * @param string|int|float|null $string The string to truncate 157 | * @param int $length Desired length of the truncated string 158 | * @param string $substring The substring to append if it can fit 159 | * 160 | * @return string with the resulting $str after truncating 161 | */ 162 | public function truncateOnWord(string|int|float|null $string, int $length, string $substring = '…'): string 163 | { 164 | $result = (string)$string; 165 | 166 | if (!empty($string)) { 167 | $string = strip_tags($string); 168 | $result = (string)Stringy::create($string)->safeTruncate($length, $substring); 169 | } 170 | 171 | return $result; 172 | } 173 | 174 | /** 175 | * Creates a Stringy object and assigns both string and encoding properties 176 | * the supplied values. $string is cast to a string prior to assignment, 177 | * and if 178 | * $encoding is not specified, it defaults to mb_internal_encoding(). It 179 | * then returns the initialized object. Throws an InvalidArgumentException 180 | * if the first argument is an array or object without a __toString method. 181 | * 182 | * @param string|int|float|null $string The string initialize the Stringy object with 183 | * @param null|string $encoding The character encoding 184 | * 185 | * @return Stringy 186 | */ 187 | public function stringy(string|int|float|null $string = '', ?string $encoding = null): Stringy 188 | { 189 | return Stringy::create($string, $encoding); 190 | } 191 | 192 | /** 193 | * Formats the value in bytes as a size in human readable form for example 194 | * `12 KB`. 195 | * 196 | * This is the short form of [[asSize]]. 197 | * 198 | * If [[sizeFormatBase]] is 1024, [binary 199 | * prefixes](http://en.wikipedia.org/wiki/Binary_prefix) 200 | * (e.g. kibibyte/KiB, mebibyte/MiB, ...) are used in the formatting 201 | * result. 202 | * 203 | * @param string|int|float $bytes value in bytes to be formatted. 204 | * @param int $decimals the number of digits after the decimal 205 | * point. 206 | * 207 | * @return string the formatted result. 208 | */ 209 | public function humanFileSize(string|int|float $bytes, int $decimals = 1): string 210 | { 211 | $oldSize = Craft::$app->formatter->sizeFormatBase; 212 | Craft::$app->formatter->sizeFormatBase = 1000; 213 | $result = Craft::$app->formatter->asShortSize($bytes, $decimals); 214 | Craft::$app->formatter->sizeFormatBase = $oldSize; 215 | 216 | return $result; 217 | } 218 | 219 | /** 220 | * Represents the value as duration in human readable format. 221 | * 222 | * @param DateInterval|string|int $value the value to be formatted. 223 | * Acceptable formats: 224 | * - [DateInterval 225 | * object](http://php.net/manual/ru/class.dateinterval.php) 226 | * - integer - number of seconds. 227 | * For example: value `131` 228 | * represents `2 minutes, 11 229 | * seconds` 230 | * - ISO8601 duration format. For 231 | * example, all of these values 232 | * represent `1 day, 2 hours, 30 233 | * minutes` duration: 234 | * `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` 235 | * - between two datetime values 236 | * `2015-01-01T13:00:00Z/P1D2H30M` - 237 | * time interval after datetime 238 | * value 239 | * `P1D2H30M/2015-01-02T13:30:00Z` - 240 | * time interval before datetime 241 | * value 242 | * `P1D2H30M` - simply a date 243 | * interval 244 | * `P-1D2H30M` - a negative date 245 | * interval (`-1 day, 2 hours, 30 246 | * minutes`) 247 | * 248 | * @return string the formatted duration. 249 | */ 250 | public function humanDuration(DateInterval|string|int $value): string 251 | { 252 | return Craft::$app->formatter->asDuration($value); 253 | } 254 | 255 | /** 256 | * Formats the value as the time interval between a date and now in human 257 | * readable form. 258 | * 259 | * This method can be used in three different ways: 260 | * 261 | * 1. Using a timestamp that is relative to `now`. 262 | * 2. Using a timestamp that is relative to the `$referenceTime`. 263 | * 3. Using a `DateInterval` object. 264 | * 265 | * @param int|string|DateTime|DateInterval $value the value to be 266 | * formatted. The 267 | * following types 268 | * of value are 269 | * supported: 270 | * 271 | * - an integer representing a UNIX timestamp 272 | * - a string that can be [parsed to create a DateTime 273 | * object](http://php.net/manual/en/datetime.formats.php). The timestamp is 274 | * assumed to be in [[defaultTimeZone]] unless a time zone is explicitly 275 | * given. 276 | * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object 277 | * - a PHP DateInterval object (a positive time interval will refer to the 278 | * past, a negative one to the future) 279 | * 280 | * @param null|int|string|DateTime $referenceTime if specified 281 | * the value is 282 | * used as a 283 | * reference time 284 | * instead of 285 | * `now` when 286 | * `$value` is not 287 | * a 288 | * `DateInterval` 289 | * object. 290 | * 291 | * @return string the formatted result. 292 | */ 293 | public function humanRelativeTime(int|string|DateTime|DateInterval $value, null|int|string|DateTime $referenceTime = null): string 294 | { 295 | return Craft::$app->formatter->asRelativeTime($value, $referenceTime); 296 | } 297 | 298 | /** 299 | * Converts number to its ordinal English form 300 | * For example, converts 13 to 13th, 2 to 2nd 301 | * 302 | * @param int $number 303 | * 304 | * @return string 305 | */ 306 | public function ordinalize(int $number): string 307 | { 308 | return Inflector::ordinalize($number); 309 | } 310 | 311 | /** 312 | * Converts a word to its plural form 313 | * For example, 'apple' will become 'apples', and 'child' will become 314 | * 'children' 315 | * 316 | * @param string $word 317 | * @param int $number 318 | * 319 | * @return string 320 | */ 321 | public function pluralize(string $word, int $number = 2): string 322 | { 323 | return abs($number) === 1 ? $word : Inflector::pluralize($word); 324 | } 325 | 326 | /** 327 | * Converts a word to its singular form 328 | * For example, 'apples' will become 'apple', and 'children' will become 329 | * 'child' 330 | * 331 | * @param string $word 332 | * @param int $number 333 | * 334 | * @return string 335 | */ 336 | public function singularize(string $word, int $number = 1): string 337 | { 338 | return abs($number) === 1 ? Inflector::singularize($word) : $word; 339 | } 340 | 341 | /** 342 | * Returns transliterated version of a string 343 | * For example, 获取到 どちら Українська: ґ,є, Српска: ђ, њ, џ! ¿Español? 344 | * will be transliterated to huo qu dao dochira Ukrainsʹka: g,e, Srpska: d, 345 | * n, d! ¿Espanol? 346 | * 347 | * @param string $string 348 | * @param null $transliterator 349 | * 350 | * @return string 351 | */ 352 | public function transliterate(string $string, $transliterator = null): string 353 | { 354 | return Inflector::transliterate($string, $transliterator); 355 | } 356 | 357 | /** 358 | * Limits a string by word count. If $substring is provided, and truncating 359 | * occurs, the string is further truncated so that the substring may be 360 | * appended without exceeding the desired length. 361 | * 362 | * @param string $string 363 | * @param int $length 364 | * @param string $substring 365 | * 366 | * @return string 367 | */ 368 | public function wordLimit(string $string, int $length, string $substring = '…'): string 369 | { 370 | $words = preg_split("/[\s]+/u", strip_tags($string)); 371 | $result = implode(' ', array_slice($words, 0, $length)); 372 | 373 | return count($words) > $length ? $result . $substring : $result; 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/models/Settings.php: -------------------------------------------------------------------------------- 1 | replacement, needle=>replacement...), or 172 | * a string formatted `"needle"=>"replacement","needle"=>"replacement",...` 173 | * 174 | * @var array 175 | */ 176 | public array $set_diacritic_custom_replacements = [ 177 | ]; 178 | 179 | /** 180 | * replaces (r) (c) (tm) (sm) (p) (R) (C) (TM) (SM) (P) with ® © ™ ℠ ℗ 181 | * 182 | * @var bool 183 | */ 184 | public bool $set_smart_marks = true; 185 | 186 | /** 187 | * replaces 1*4 with 1x4, etc. 188 | * 189 | * @var bool 190 | */ 191 | public bool $set_smart_math = true; 192 | 193 | /** 194 | * replaces 2^4 with 24 195 | * 196 | * @var bool 197 | */ 198 | public bool $set_smart_exponents = true; 199 | 200 | /** 201 | * replaces 1/4 with 14 202 | * 203 | * @var bool 204 | */ 205 | public bool $set_smart_fractions = true; 206 | 207 | /** 208 | * Enables/disables replacement of 1st with 1st 209 | * 210 | * @var bool 211 | */ 212 | public bool $set_smart_ordinal_suffix = true; 213 | 214 | /** 215 | * single character words are forced to next line with insertion of   216 | * 217 | * @var bool 218 | */ 219 | public bool $set_single_character_word_spacing = true; 220 | 221 | /** 222 | * fractions are kept together with insertion of   223 | * 224 | * @var bool 225 | */ 226 | public bool $set_fraction_spacing = true; 227 | 228 | /** 229 | * units and values are kept together with insertion of   230 | * 231 | * @var bool 232 | */ 233 | public bool $set_unit_spacing = true; 234 | 235 | /** 236 | * Enables/disables extra whitespace before certain punction marks, as is the French custom. 237 | * 238 | * @var bool 239 | */ 240 | public bool $set_french_punctuation_spacing = false; 241 | 242 | /** 243 | * a list of units to keep with their values 244 | * 245 | * @var array 246 | */ 247 | public array $set_units = [ 248 | ]; 249 | 250 | /** 251 | * Em and En dashes are wrapped in thin spaces 252 | * 253 | * @var bool 254 | */ 255 | public bool $set_dash_spacing = true; 256 | 257 | /** 258 | * Remove extra space characters 259 | * 260 | * @var bool 261 | */ 262 | public bool $set_space_collapse = true; 263 | 264 | /** 265 | * Enable usage of true "no-break narrow space" ( ) instead of the normal no-break space ( ). 266 | * 267 | * @var bool 268 | */ 269 | public bool $set_true_no_break_narrow_space = false; 270 | 271 | /** 272 | * enables widow handling 273 | * 274 | * @var bool 275 | */ 276 | public bool $set_dewidow = true; 277 | 278 | /** 279 | * establishes maximum length of a widows that will be protected 280 | * 281 | * @var int 282 | */ 283 | public int $set_max_dewidow_length = 5; 284 | 285 | /** 286 | * establishes the maximum number of words considered for dewidowing. 287 | * 288 | * @var int 289 | */ 290 | public int $set_dewidow_word_number = 1; 291 | 292 | /** 293 | * establishes maximum length of pulled text to keep widows company 294 | * 295 | * @var int 296 | */ 297 | public int $set_max_dewidow_pull = 5; 298 | 299 | /** 300 | * enables wrapping at hard hyphens internal to a word with the insertion of a zero-width-space 301 | * 302 | * @var bool 303 | */ 304 | public bool $set_wrap_hard_hyphens = true; 305 | 306 | /** 307 | * enables wrapping of urls 308 | * 309 | * @var bool 310 | */ 311 | public bool $set_url_wrap = true; 312 | 313 | /** 314 | * enables wrapping of email addresses 315 | * 316 | * @var bool 317 | */ 318 | public bool $set_email_wrap = true; 319 | 320 | /** 321 | * establishes minimum character requirement after a url wrapping point 322 | * 323 | * @var int 324 | */ 325 | public int $set_min_after_url_wrap = 5; 326 | 327 | /** 328 | * wrap ampersands in 329 | * 330 | * @var bool 331 | */ 332 | public bool $set_style_ampersands = true; 333 | 334 | /** 335 | * wrap caps in 336 | * 337 | * @var bool 338 | */ 339 | public bool $set_style_caps = true; 340 | 341 | /** 342 | * wrap initial quotes in or 343 | * 344 | * @var bool 345 | */ 346 | public bool $set_style_initial_quotes = true; 347 | 348 | /** 349 | * wrap numbers in 350 | * 351 | * @var bool 352 | */ 353 | public bool $set_style_numbers = true; 354 | 355 | /** 356 | * sets tags where initial quotes and guillemets should be styled 357 | * 358 | * @var array 359 | */ 360 | public array $set_initial_quote_tags = [ 361 | "p", 362 | "h1", 363 | "h2", 364 | "h3", 365 | "h4", 366 | "h5", 367 | "h6", 368 | "blockquote", 369 | "li", 370 | "dd", 371 | "dt", 372 | ]; 373 | 374 | /** 375 | * enables hyphenation of text 376 | * 377 | * @var bool 378 | */ 379 | public bool $set_hyphenation = true; 380 | 381 | /** 382 | * defines hyphenation language for text 383 | * 384 | * @var string 385 | */ 386 | public string $set_hyphenation_language = "en-US"; 387 | 388 | /** 389 | * establishes minimum length of a word that may be hyphenated 390 | * 391 | * @var int 392 | */ 393 | public int $set_min_length_hyphenation = 5; 394 | 395 | /** 396 | * establishes minimum character requirement before a hyphenation point 397 | * 398 | * @var int 399 | */ 400 | public int $set_min_before_hyphenation = 3; 401 | 402 | /** 403 | * establishes minimum character requirement after a hyphenation point 404 | * 405 | * @var int 406 | */ 407 | public int $set_min_after_hyphenation = 2; 408 | 409 | /** 410 | * allows/disallows hyphenation of title/heading text 411 | * 412 | * @var bool 413 | */ 414 | public bool $set_hyphenate_headings = true; 415 | 416 | /** 417 | * allows hyphenation of strings of all capital characters 418 | * 419 | * @var bool 420 | */ 421 | public bool $set_hyphenate_all_caps = true; 422 | 423 | /** 424 | * allows hyphenation of strings of all capital characters 425 | * 426 | * @var bool 427 | */ 428 | public bool $set_hyphenate_title_case = true; 429 | 430 | /** 431 | * defines custom word hyphenations 432 | * expected input is an array of words with all hyphenation points marked with a hard hyphen 433 | * 434 | * @var array 435 | */ 436 | public array $set_hyphenation_exceptions = [ 437 | ]; 438 | 439 | /** 440 | * Enable lenient parser error handling (HTML is "best guess" if enabled). 441 | * 442 | * @var bool 443 | */ 444 | public bool $set_ignore_parser_errors = true; 445 | 446 | /** 447 | * Sets an optional handler for parser errors. Invalid callbacks will be silently ignored 448 | * 449 | * @var callable|null 450 | */ 451 | public $set_parser_errors_handler = null; 452 | 453 | // Public Methods 454 | // ========================================================================= 455 | 456 | /** 457 | * @inheritdoc 458 | */ 459 | public function rules(): array 460 | { 461 | return [ 462 | [ 463 | [ 464 | 'set_smart_quotes_primary', 465 | 'set_smart_quotes_secondary', 466 | 'set_smart_quotes_secondary', 467 | 'set_diacritic_language', 468 | 'set_hyphenation_language', 469 | ], 470 | 'string', 471 | ], 472 | [ 473 | [ 474 | 'set_max_dewidow_length', 475 | 'set_dewidow_word_number', 476 | 'set_max_dewidow_pull', 477 | 'set_min_after_url_wrap', 478 | 'set_min_length_hyphenation', 479 | 'set_min_before_hyphenation', 480 | 'set_min_after_hyphenation', 481 | ], 482 | 'integer', 483 | ], 484 | [ 485 | [ 486 | 'set_smart_quotes', 487 | 'set_smart_dashes', 488 | 'set_smart_ellipses', 489 | 'set_smart_diacritics', 490 | 'set_smart_dashes', 491 | 'set_smart_ellipses', 492 | 'set_smart_diacritics', 493 | 'set_smart_marks', 494 | 'set_smart_math', 495 | 'set_smart_exponents', 496 | 'set_smart_fractions', 497 | 'set_smart_ordinal_suffix', 498 | 'set_single_character_word_spacing', 499 | 'set_fraction_spacing', 500 | 'set_unit_spacing', 501 | 'set_dash_spacing', 502 | 'set_space_collapse', 503 | 'set_true_no_break_narrow_space', 504 | 'set_dewidow', 505 | 'set_wrap_hard_hyphens', 506 | 'set_url_wrap', 507 | 'set_email_wrap', 508 | 'set_style_ampersands', 509 | 'set_style_caps', 510 | 'set_style_initial_quotes', 511 | 'set_style_numbers', 512 | 'set_hyphenation', 513 | 'set_hyphenate_headings', 514 | 'set_hyphenate_all_caps', 515 | 'set_hyphenate_title_case', 516 | 'set_ignore_parser_errors', 517 | ], 518 | 'boolean', 519 | ], 520 | [ 521 | [ 522 | 'set_tags_to_ignore', 523 | 'set_classes_to_ignore', 524 | 'set_ids_to_ignore', 525 | 'set_diacritic_custom_replacements', 526 | 'set_units', 527 | 'set_initial_quote_tags', 528 | 'set_hyphenation_exceptions', 529 | ], 530 | ArrayValidator::class, 531 | ], 532 | [ 533 | [ 534 | 'set_parser_errors_handler', 535 | ], 536 | 'safe', 537 | ], 538 | ]; 539 | } 540 | } 541 | --------------------------------------------------------------------------------