├── 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 | [](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/?branch=v1) [](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/?branch=v1) [](https://scrutinizer-ci.com/g/nystudio107/craft-typogrify/build-status/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 | 
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 |
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 1⁄4
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 1⁄4
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 |
--------------------------------------------------------------------------------