3 namespace Drupal\typogrify;
6 * Class \Drupal\typogrify\Typogrify.
11 * Enable custom styling of ampersands.
13 * Wraps apersands in html with '<span class="amp">', so they can be
14 * styled with CSS. Ampersands are also normalized to '&. Requires
15 * ampersands to have whitespace or an ' ' on both sides.
17 * It won't mess up & that are already wrapped, in entities or URLs
23 public static function amp($text) {
24 $amp_finder = "/(\s| )(&|&|&\#38;|&)(\s| )/";
26 return preg_replace($amp_finder, '\\1<span class="amp">&</span>\\3', $text);
30 * Puts a   before and after an &ndash or —.
32 * Dashes may have whitespace or an `` `` on both sides
38 public static function dash($text) {
39 $dash_finder = "/(\s| | )*(—|–|–|–|—|—)(\s| | )*/";
41 return preg_replace($dash_finder, ' \\2 ', $text);
45 * Helper method for caps method - used for preg_replace_callback.
47 public static function _cap_wrapper($matchobj) {
48 if (!empty($matchobj[2])) {
49 return sprintf('<span class="caps">%s</span>', $matchobj[2]);
52 $mthree = $matchobj[3];
53 if (($mthree{strlen($mthree) - 1}) == ' ') {
54 $caps = substr($mthree, 0, -1);
62 return sprintf('<span class="caps">%s</span>%s', $caps, $tail);
69 * Wraps multiple capital letters in ``<span class="caps">``
70 * so they can be styled with CSS.
72 * Uses the smartypants tokenizer to not screw with HTML or with tags it
75 public static function caps($text) {
76 $tokens = SmartyPants::tokenizeHtml($text);
78 $in_skipped_tag = FALSE;
81 (\b[[\p{Lu}=\d]* # Group 2: Any amount of caps and digits
82 [[\p{Lu}][[\p{Lu}\d]* # A cap string much at least include two caps (but they can have digits between them)
83 (?:&)? # allowing ampersand in caps.
84 [[\p{Lu}'\d]*[[\p{Lu}\d]) # Any amount of caps and digits
85 | (\b[[\p{Lu}]+\.\s? # OR: Group 3: Some caps, followed by a '.' and an optional space
86 (?:[[\p{Lu}]+\.\s?)+) # Followed by the same thing at least once more
87 (\s|\b|$|[)}\]>]))/xu";
89 foreach ($tokens as $token) {
90 if ($token[0] == 'tag') {
91 // Don't mess with tags.
92 $result[] = $token[1];
93 $close_match = preg_match(SmartyPants::SMARTYPANTS_TAGS_TO_SKIP, $token[1]);
95 $in_skipped_tag = true;
98 $in_skipped_tag = false;
102 if ($in_skipped_tag) {
103 $result[] = $token[1];
106 $result[] = preg_replace_callback($cap_finder, 'self::_cap_wrapper', $token[1]);
111 return implode('', $result);
115 * Helper method for initial_quotes method - used for preg_replace_callback.
117 public static function _quote_wrapper($matchobj) {
118 if (!empty($matchobj[7])) {
120 $quote = $matchobj[7];
124 $quote = $matchobj[8];
127 return sprintf('%s<span class="%s">%s</span>', $matchobj[1], $classname, $quote);
133 * Wraps initial quotes in ``class="dquo"`` for double quotes or
134 * ``class="quo"`` for single quotes. Works in these block tags ``(h1-h6, p, li)``
135 * and also accounts for potential opening inline elements ``a, em, strong, span, b, i``
136 * Optionally choose to apply quote span tags to Gullemets as well.
138 public static function initial_quotes($text, $do_guillemets = false) {
139 $quote_finder = "/((<(p|h[1-6]|li)[^>]*>|^) # start with an opening p, h1-6, li or the start of the string
140 \s* # optional white space!
141 (<(a|em|span|strong|i|b)[^>]*>\s*)*) # optional opening inline tags, with more optional white space for each.
142 ((\"|“|&\#8220;)|('|‘|&\#8216;)) # Find me a quote! (only need to find the left quotes and the primes)
143 # double quotes are in group 7, singles in group 8
146 if ($do_guillemets) {
147 $quote_finder = "/((<(p|h[1-6]|li)[^>]*>|^) # start with an opening p, h1-6, li or the start of the string
148 \s* # optional white space!
149 (<(a|em|span|strong|i|b)[^>]*>\s*)*) # optional opening inline tags, with more optional white space for each.
150 ((\"|“|&\#8220;|\xAE|&\#171;|«)|('|‘|&\#8216;)) # Find me a quote! (only need to find the left quotes and the primes) - also look for guillemets (>> and << characters))
151 # double quotes are in group 7, singles in group 8
155 return preg_replace_callback($quote_finder, 'self::_quote_wrapper', $text);
161 * Replaces the space between the last two words in a string with `` ``
162 * Works in these block tags ``(h1-h6, p, li)`` and also accounts for
163 * potential closing inline elements ``a, em, strong, span, b, i``
165 * Empty HTMLs shouldn't error
167 public static function widont($text) {
168 // This regex is a beast, tread lightly
169 $widont_finder = "/([^<>\s]+|<\/span>) # ensure more than 1 word
170 (\s+) # the space to replace
171 ([^<>\s]+ # must be flollowed by non-tag non-space characters
172 \s* # optional white space!
173 (<\/(a|em|span|strong|i|b)[^>]*>\s*)* # optional closing inline tags with optional white space after each
174 ((<\/(p|h[1-6]|li|dt|dd)>)|$)) # end with a closing p, h1-6, li or the end of the string
177 return preg_replace($widont_finder, '$1 $3', $text);
183 * The super typography filter.
184 * Applies the following filters: widont, smartypants, caps, amp, initial_quotes
185 * Optionally choose to apply quote span tags to Gullemets as well.
187 public static function filter($text, $do_guillemets = FALSE) {
188 $text = self::amp($text);
189 $text = self::widont($text);
190 $text = SmartyPants::process($text);
191 $text = self::caps($text);
192 $text = self::initial_quotes($text, $do_guillemets);
193 $text = self::dash($text);