-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathembed-fontawesome.php
374 lines (318 loc) · 10.7 KB
/
embed-fontawesome.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Plugin;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
// Load the other parts of this plugin
use Grav\Plugin\EmbedFontAwesomePlugin\IconNotFoundError;
class EmbedFontAwesomePlugin extends Plugin
{
private $failBehaviour;
private $iconClass;
private $retainIconName;
private $usedIcons = array();
/**
* Gets the subscribed events and registers them
* @return array
*/
public static function getSubscribedEvents()
{
return [
'onPluginsInitialized' => [
['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7
['onPluginsInitialized', 0],
]
];
}
/**
* Composer autoload.
*is
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
/**
* Initialize the plugin, only loading the bits we need
*/
public function onPluginsInitialized()
{
// Don't proceed if we are in the admin plugin
if ($this->isAdmin()) return;
// Get config
$config = $this->config['plugins.embed-fontawesome'];
// Check if plugin is enabled
if ($config['enabled']) {
// Get some configuration options
$this->failBehaviour = isset($config['fail_behaviour']) ? $config["fail_behaviour"] : "hard";
$this->iconClass = isset($config['icon_class']) ? $config["icon_class"] : "icon";
$this->retainIconName = isset($config['retain_icon_name']) ? $config['retain_icon_name'] : false;
// Define default events
$events = array(
'onOutputGenerated' => ['onOutputGenerated', 10], // This needs to run before Advanced Pagecache
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0]
);
// Enable CSS loading (if enabled)
if ($config['builtin_css']) {
$events = array_merge($events, ['onAssetsInitialized' => ['onAssetsInitialized', 0]]);
}
// Enable :emoji: style icons in Markdown (if enabled)
if ($config['emoji_icons']) {
$events = array_merge($events, ['onMarkdownInitialized' => ['enableEmojiIcons', 0]]);
}
// Enable the [icon=... /] shortcode (if enabled)
if ($config['shortcode_icons']) {
$events = array_merge($events, ['onShortcodeHandlers' => ['registerIconShortcode', 0]]);
}
// Enable the relevant events
$this->enable($events);
}
}
/**
* Load the CSS
*/
public function onAssetsInitialized() {
$this->grav["assets"]->addCss("plugin://embed-fontawesome/css/icons.css");
}
/**
* Load the icons as Twig templates
*/
public function onTwigTemplatePaths() {
// Make the icons available as Twig templates
$templatePath = $this->grav["locator"]->findResource("user-data://fontawesome");
// Check that it exists to avoid an error message
if (file_exists($templatePath)) {
$this->grav['twig']->twig_paths[] = $templatePath;
}
}
/**
* Enable :emoji: style icons in Markdown
* Based on: https://github.com/N-Parsons/grav-plugin-markdown-fontawesome
*/
public function enableEmojiIcons(Event $event)
{
$markdown = $event['markdown'];
// Initialize Text example
$markdown->addInlineType(':', 'EmojiIcon');
// Add function to handle this
$markdown->inlineEmojiIcon = function($excerpt) {
// Search $excerpt['text'] for regex and store whole matching string in $matches[0], store icon name in $matches[1]
if (preg_match('/^:(?:icon )?icon-([a-zA-Z0-9- ]+):/', $excerpt['text'], $matches))
{
return array(
'extent' => strlen($matches[0]),
'element' => array(
'name' => 'i',
'text' => '',
'attributes' => array(
'class' => $this->iconClass.' icon-'.$matches[1],
),
),
);
}
};
}
/**
* Enable [icon=... /] shortcode
*/
public function registerIconShortcode()
{
$this->grav['shortcode']->registerAllShortcodes(__DIR__.'/shortcodes');
}
/**
* Embed Font Awesome icons
*/
public function onOutputGenerated()
{
// Check that it's an HTML page, abort if not
if ($this->grav["page"]->templateFormat() !== 'html') {
return;
}
// Get the rendered content (HTML)
$content = $this->grav->output;
// Rewrite the output: embed icons as inline SVGs
$this->grav->output = $this->embedIcons($content);
}
/**
* Embeds icons into the generated HTML as inline SVGs
*
* @param string $content Generated HTML to embed icons in
* @return string Output HTML with embedded icons
*/
private function embedIcons($content)
{
// Get all matches for icons
if (version_compare($ver = PHP_VERSION, $req = "7.3.0", '<')) {
$iconRegex = '/<i (?<preClass>.*?)(?<= )class=(?<quot>"|\')(?<classPreFA>[a-zA-Z0-9 :_-]*)(?<=["\' ])(?<weightFA>(?:fa[srlbd]?)|(?:icon)) (?<classMidFA>((?!((fa)|(icon)))[a-zA-Z0-9 _-]*)*)(?<= )(?<iconType>fa|icon)-(?<iconFA>[a-z0-9-]+)(?<classPostFA>[a-zA-Z0-9 :_-]*)\k<quot>(?<postClass>.*?)><\/i>/';
} else {
$iconRegex = '/<i (?<preClass>.*?)(?<= )class=(?<quot>"|\')(?<classPreFA>[a-zA-Z0-9 :_-]*)(?<=( |\k<quot>))(?<weightFA>(?:fa[srlbd]?)|(?:icon)) (?<classMidFA>((?!((fa)|(icon)))[a-zA-Z0-9 _-]*)*)(?<= )(?<iconType>fa|icon)-(?<iconFA>[a-z0-9-]+)(?<classPostFA>[a-zA-Z0-9 :_-]*)\k<quot>(?<postClass>.*?)><\/i>/';
}
if (preg_match_all(
$iconRegex,
$content,
$matchesRaw
)) {
// Reconfigure the matches into a more useful structure
foreach($matchesRaw as $n => $set) {
foreach($set as $m => $match) {
$matches[$m][$n] = $match;
}
}
// Replace the matches
foreach($matches as $match) {
$fullMatch = $match[0];
// Get the replacement HTML
$replace = $this->getIconHtml($match);
// Perform replacement, only replacing the first instance
$content = str_replace_once($fullMatch, $replace, $content);
}
}
return $content;
}
/**
* Takes an array of components from the regex, and returns the HTML for the inline SVG
*
* @param array $match Associative array of regex matches
* @return string HTML for the inline SVG
*/
private function getIconHtml($match)
{
// Get other attributes
$attributes = trim($match["preClass"] . " " . $match["postClass"]);
if ($attributes) {
$attributes = " " . $attributes;
}
// Construct the classes
$classes = class_merge($this->iconClass, $match["classPreFA"], $match["classMidFA"], $match["classPostFA"]);
if ($this->retainIconName) {
$classes = array_merge($classes, [$match["iconType"].'-'.$match["iconFA"]]);
}
$classSpan = implode(" ", array_unique($classes));
// Determine the icon ID
$iconId = $match["weightFA"]."_".$match["iconType"]."-".$match["iconFA"];
if (isset($this->usedIcons[$iconId])) {
// Get the viewBox
$viewBox = $this->usedIcons[$iconId];
// Create a twig template
$twigTemplate = '<span class="' . $classSpan . '"' . $attributes . '><svg xmlns="http://www.w3.org/2000/svg" '.$viewBox.'><use href="#'.$iconId.'" /></svg></span>';
// Process the template
$inlineSvg = $this->processIconTemplate($twigTemplate);
} else {
// Get the icon path
$path = $this->getTemplatePath($match["iconFA"], $match["weightFA"]);
// Create a twig template
$twigTemplate = '<span class="' . $classSpan . '"' . $attributes . '>{% include "' . $path . '" %}</span>';
// Process the template
$inlineSvg = $this->processIconTemplate($twigTemplate);
// Get the viewBox
preg_match('/viewBox="[0-9.]+ [0-9.]+ [0-9.]+ [0-9.]+"/', $inlineSvg, $viewBoxMatches);
$viewBox = $viewBoxMatches[0];
// Store the key details of this icon
$this->usedIcons[$iconId] = $viewBox;
// Insert the ID
$inlineSvg = str_replace('<svg ', '<svg id="'.$iconId.'" ', $inlineSvg);
}
return $inlineSvg;
}
/**
* Gets the template path for a specified icon and variant
*
* @param string $icon Icon name
* @param string $weight Icon weight/variant
* @return string Template path for the icon
*/
private function getTemplatePath($icon, $weight) {
switch ($weight) {
case "fas":
case "fa":
$folder = "solid/";
break;
case "far":
$folder = "regular/";
break;
case "fal":
$folder = "light/";
break;
case "fab":
$folder = "brands/";
break;
case "fad":
$folder = "duotone/";
break;
default: // "icon"
$folder = "custom/";
}
return $folder . $icon . ".svg";
}
/**
* Safely processes an icon Twig template
*
* @param string $template Twig template
* @return string Inline SVG
*/
private function processIconTemplate($template)
{
try {
$result = $this->grav["twig"]->processString($template);
} catch (RuntimeException $e) {
// Extract the path and create a meaningful message
if (preg_match('/\{% include (.+) %\}/', $template, $matches)) {
$msg = "Icon not found: ".$matches[1].".";
// Process the failure as configured
if ($this->failBehaviour === "soft") {
// Log the failure
$this->grav["debugger"]->addMessage($msg);
// Replace the missing icon with a question mark
$fallback = file_get_contents(__DIR__."/assets/missing_icon.svg");
$result = str_replace($matches[0], $fallback, $template);
} else {
// Otherwise, throw a more meaningful error
throw new IconNotFoundError($msg, 404, $e);
}
} else {
// There should always be a match, but if not, something has gone wrong...
throw $e;
}
}
return $result;
}
}
/**
* Replaces only the first instance of a string
*
* @param string $search String to search for and replace
* @param string $replace Replacement for the search term
* @param string $subject Content to search
* @return string $subject with the first instance of $search replaced by $replace
*/
function str_replace_once($search, $replace, $subject)
{
$position = strpos($subject, $search);
if ($position !== false){
return substr_replace($subject, $replace, $position, strlen($search));
} else {
return $subject;
}
}
/**
* Merges HTML classes into an array
*
* @param string ...$classes HTML classes to combine
* @return array Array of HTML classes
*/
function class_merge(...$classes)
{
$classList = array();
foreach($classes as $class) {
$trimmed = trim($class);
if ($trimmed) {
$classList = array_merge(
explode(" ", $trimmed),
$classList
);
}
};
return $classList;
}