1<?php 2/** 3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15require_once(__DIR__ . '/PluginUtility.php'); 16 17 18/** 19 * Class IconUtility 20 * @package ComboStrap 21 * @see https://combostrap.com/icon 22 * 23 * 24 * Material design does not have a repository structure where we can extract the location 25 * from the name 26 * https://material.io/resources/icons https://google.github.io/material-design-icons/ 27 * 28 * Injection via javascript to avoid problem with the php svgsimple library 29 * https://www.npmjs.com/package/svg-injector 30 */ 31class Icon extends ImageSvg 32{ 33 const CONF_ICONS_MEDIA_NAMESPACE = "icons_namespace"; 34 const CONF_ICONS_MEDIA_NAMESPACE_DEFAULT = ":" . PluginUtility::COMBOSTRAP_NAMESPACE_NAME . ":icons"; 35 // Canonical name 36 const NAME = "icon"; 37 38 39 const ICON_LIBRARY_URLS = array( 40 self::BOOTSTRAP => "https://raw.githubusercontent.com/twbs/icons/main/icons", 41 self::MATERIAL_DESIGN => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg", 42 self::FEATHER => "https://raw.githubusercontent.com/feathericons/feather/master/icons", 43 self::CODE_ICON => "https://raw.githubusercontent.com/microsoft/vscode-codicons/main/src/icons", 44 self::LOGOS => "https://raw.githubusercontent.com/gilbarbara/logos/master/logos", 45 self::CARBON => "https://raw.githubusercontent.com/carbon-design-system/carbon/main/packages/icons/src/svg/32", 46 self::TWEET_EMOJI => "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg", 47 self::ANT_DESIGN => "https://raw.githubusercontent.com/ant-design/ant-design-icons/master/packages/icons-svg/svg", 48 self::FAD => "https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs", 49 self::CLARITY => "https://raw.githubusercontent.com/vmware/clarity-assets/master/icons/essential", 50 self::OCTICON => "https://raw.githubusercontent.com/primer/octicons/main/icons" 51 ); 52 53 const ICON_LIBRARY_WEBSITE_URLS = array( 54 self::BOOTSTRAP => "https://icons.getbootstrap.com/", 55 self::MATERIAL_DESIGN => "https://materialdesignicons.com/", 56 self::FEATHER => "https://feathericons.com/", 57 self::CODE_ICON => "https://microsoft.github.io/vscode-codicons/", 58 self::LOGOS => "https://svgporn.com/", 59 self::CARBON => "https://www.carbondesignsystem.com/guidelines/icons/library/", 60 self::TWEET_EMOJI => "https://twemoji.twitter.com/", 61 self::ANT_DESIGN => "https://ant.design/components/icon/", 62 self::CLARITY => "https://clarity.design/foundation/icons/", 63 self::OCTICON => "https://primer.style/octicons/" 64 ); 65 66 const CONF_DEFAULT_ICON_LIBRARY = "defaultIconLibrary"; 67 const CONF_DEFAULT_ICON_LIBRARY_DEFAULT = self::MATERIAL_DESIGN_ACRONYM; 68 69 /** 70 * Deprecated library acronym / name 71 */ 72 const DEPRECATED_LIBRARY_ACRONYM = array( 73 "bs" => self::BOOTSTRAP, // old one (deprecated) - the good acronym is bi (seen also in the class) 74 "md" => self::MATERIAL_DESIGN 75 ); 76 77 /** 78 * Public known acronym / name (Used in the configuration) 79 */ 80 const PUBLIC_LIBRARY_ACRONYM = array( 81 "bi" => self::BOOTSTRAP, 82 self::MATERIAL_DESIGN_ACRONYM => self::MATERIAL_DESIGN, 83 "fe" => self::FEATHER, 84 "codicon" => self::CODE_ICON, 85 "logos" => self::LOGOS, 86 "carbon" => self::CARBON, 87 "twemoji" => self::TWEET_EMOJI, 88 "ant-design" => self::ANT_DESIGN, 89 "fad" => self::FAD, 90 "clarity" => self::CLARITY, 91 "octicon" => self::OCTICON 92 ); 93 94 const FEATHER = "feather"; 95 const BOOTSTRAP = "bootstrap"; 96 const MATERIAL_DESIGN = "material-design"; 97 const CODE_ICON = "codicon"; 98 const LOGOS = "logos"; 99 const CARBON = "carbon"; 100 const MATERIAL_DESIGN_ACRONYM = "mdi"; 101 const TWEET_EMOJI = "twemoji"; 102 const ANT_DESIGN = "ant-design"; 103 const FAD = "fad"; 104 const CLARITY = "clarity"; 105 const OCTICON = "octicon"; 106 107 108 /** 109 * The function used to render an icon 110 * @param TagAttributes $tagAttributes - the icon attributes 111 * @return Icon 112 * @throws ExceptionCombo 113 */ 114 static public function create(TagAttributes $tagAttributes): Icon 115 { 116 117 118 $name = "name"; 119 if (!$tagAttributes->hasComponentAttribute($name)) { 120 throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::NAME); 121 } 122 123 /** 124 * The Name 125 */ 126 $iconNameAttribute = $tagAttributes->getValue($name); 127 128 /** 129 * If the name have an extension, it's a file from the media directory 130 * Otherwise, it's an icon from a library 131 */ 132 $mediaDokuPath = DokuPath::createMediaPathFromId($iconNameAttribute); 133 if (!empty($mediaDokuPath->getExtension())) { 134 135 // loop through candidates until a match was found: 136 // May be an icon from the templates 137 if (!FileSystems::exists($mediaDokuPath)) { 138 139 // Trying to see if it's not in the template images directory 140 $message = "The media file could not be found in the media library. If you want an icon from an icon library, indicate a name without extension."; 141 $message .= "<BR> Media File Library tested: $mediaDokuPath"; 142 throw new ExceptionCombo($message, self::NAME); 143 144 145 } 146 147 } else { 148 149 // It may be a icon already downloaded 150 $iconNameSpace = PluginUtility::getConfValue(self::CONF_ICONS_MEDIA_NAMESPACE, self::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT); 151 if (substr($iconNameSpace, 0, 1) != DokuPath::PATH_SEPARATOR) { 152 $iconNameSpace = DokuPath::PATH_SEPARATOR . $iconNameSpace; 153 } 154 if (substr($iconNameSpace, -1) != DokuPath::PATH_SEPARATOR) { 155 $iconNameSpace = $iconNameSpace . ":"; 156 } 157 $mediaPathId = $iconNameSpace . $iconNameAttribute . ".svg"; 158 $mediaDokuPath = DokuPath::createMediaPathFromAbsolutePath($mediaPathId); 159 160 // Bug: null file created when the stream could not get any byte 161 // We delete them 162 if (FileSystems::exists($mediaDokuPath)) { 163 if (FileSystems::getSize($mediaDokuPath) == 0) { 164 FileSystems::delete($mediaDokuPath); 165 } 166 } 167 168 if (!FileSystems::exists($mediaDokuPath)) { 169 170 /** 171 * Download the icon 172 */ 173 174 // Create the target directory if it does not exist 175 $iconDir = $mediaDokuPath->getParent(); 176 if (!FileSystems::exists($iconDir)) { 177 try { 178 FileSystems::createDirectory($iconDir); 179 } catch (ExceptionCombo $e) { 180 throw new ExceptionCombo("The icon directory ($iconDir) could not be created.", self::NAME, 0, $e); 181 } 182 } 183 184 // Name parsing to extract the library name and icon name 185 $sepPosition = strpos($iconNameAttribute, ":"); 186 $library = PluginUtility::getConfValue(self::CONF_DEFAULT_ICON_LIBRARY, self::CONF_DEFAULT_ICON_LIBRARY_DEFAULT); 187 $iconName = $iconNameAttribute; 188 if ($sepPosition != false) { 189 $library = substr($iconNameAttribute, 0, $sepPosition); 190 $iconName = substr($iconNameAttribute, $sepPosition + 1); 191 } 192 193 // Get the qualified library name 194 $acronymLibraries = self::getLibraries(); 195 if (isset($acronymLibraries[$library])) { 196 $library = $acronymLibraries[$library]; 197 } 198 199 // Get the url 200 $iconLibraries = self::ICON_LIBRARY_URLS; 201 if (!isset($iconLibraries[$library])) { 202 throw new ExceptionCombo("The icon library ($library) is unknown. The icon could not be downloaded.", self::NAME); 203 } else { 204 $iconBaseUrl = $iconLibraries[$library]; 205 } 206 207 /** 208 * Name processing 209 */ 210 switch ($library) { 211 212 case self::TWEET_EMOJI: 213 try { 214 $iconName = self::getEmojiCodePoint($iconName); 215 } catch (ExceptionCombo $e) { 216 throw new ExceptionCombo("The emoji name $iconName is unknown. The emoji could not be downloaded.", self::NAME, 0, $e); 217 } 218 break; 219 case self::ANT_DESIGN: 220 // table-outlined where table is the svg, outlined the category 221 // ordered-list-outlined where ordered-list is the svg, outlined the category 222 $iconProcessed = $iconName; 223 $index = strrpos($iconProcessed, "-"); 224 if ($index === false) { 225 throw new ExceptionCombo ("We expect that a ant design icon name ($iconName) has two parts separated by a `-` (example: table-outlined). The icon could not be downloaded.", self::NAME); 226 } 227 $iconName = substr($iconProcessed, 0, $index); 228 $iconType = substr($iconProcessed, $index + 1); 229 $iconBaseUrl .= "/$iconType"; 230 break; 231 case self::CARBON: 232 $iconName = self::getCarbonPhysicalName($iconName); 233 break; 234 case self::FAD: 235 $iconName = self::getFadPhysicalName($iconName); 236 } 237 238 239 // The url 240 $downloadUrl = "$iconBaseUrl/$iconName.svg"; 241 $filePointer = @fopen($downloadUrl, 'r'); 242 if ($filePointer != false) { 243 244 $numberOfByte = @file_put_contents($mediaDokuPath->toLocalPath()->toAbsolutePath()->toString(), $filePointer); 245 if ($numberOfByte != false) { 246 LogUtility::msg("The icon ($iconName) from the library ($library) was downloaded to ($mediaPathId)", LogUtility::LVL_MSG_INFO, self::NAME); 247 } else { 248 LogUtility::msg("Internal error: The icon ($iconName) from the library ($library) could no be written to ($mediaPathId)", LogUtility::LVL_MSG_ERROR, self::NAME); 249 } 250 251 } else { 252 253 // (ie no icon file found at ($downloadUrl) 254 $urlLibrary = self::ICON_LIBRARY_WEBSITE_URLS[$library]; 255 LogUtility::msg("The library (<a href=\"$urlLibrary\">$library</a>) does not have a icon (<a href=\"$downloadUrl\">$iconName</a>).", LogUtility::LVL_MSG_ERROR, self::NAME); 256 257 } 258 259 } 260 261 } 262 263 /** 264 * After optimization, the width and height of the svg are gone 265 * but the icon type set them again 266 * 267 * The icon type is used to set: 268 * * the default dimension 269 * * color styling 270 * * disable the responsive properties 271 * 272 */ 273 $tagAttributes->addComponentAttributeValue(TagAttributes::TYPE_KEY, SvgDocument::ICON_TYPE); 274 275 return new Icon($mediaDokuPath, $tagAttributes); 276 277 } 278 279 /** 280 * @param $iconName 281 * @param $mediaFilePath 282 * @deprecated Old code to download icon from the material design api 283 */ 284 public 285 static function downloadIconFromMaterialDesignApi($iconName, $mediaFilePath) 286 { 287 // Try the official API 288 // Read the icon meta of 289 // Meta Json file got all icons 290 // 291 // * Available at: https://raw.githubusercontent.com/Templarian/MaterialDesign/master/meta.json 292 // * See doc: https://github.com/Templarian/MaterialDesign-Site/blob/master/src/content/api.md) 293 $arrayFormat = true; 294 $iconMetaJson = json_decode(file_get_contents(__DIR__ . '/../resources/dictionary/icon-meta.json'), $arrayFormat); 295 $iconId = null; 296 foreach ($iconMetaJson as $key => $value) { 297 if ($value['name'] == $iconName) { 298 $iconId = $value['id']; 299 break; 300 } 301 } 302 if ($iconId != null) { 303 304 // Download 305 // Call to the API 306 // https://dev.materialdesignicons.com/contribute/site/api 307 $downloadUrl = "https://materialdesignicons.com/api/download/icon/svg/$iconId"; 308 $filePointer = file_put_contents($mediaFilePath, fopen($downloadUrl, 'r')); 309 if ($filePointer == false) { 310 LogUtility::msg("The file ($downloadUrl) could not be downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_ERROR, self::NAME); 311 } else { 312 LogUtility::msg("The material design icon ($iconName) was downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_INFO, self::NAME); 313 } 314 315 } 316 317 } 318 319 private static function getLibraries(): array 320 { 321 return array_merge( 322 self::PUBLIC_LIBRARY_ACRONYM, 323 self::DEPRECATED_LIBRARY_ACRONYM 324 ); 325 } 326 327 /** 328 * @throws ExceptionCombo 329 */ 330 public static function getEmojiCodePoint(string $emojiName) 331 { 332 $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/emojis.json"); 333 $jsonContent = FileSystems::getContent($path); 334 $jsonArray = Json::createFromString($jsonContent)->toArray(); 335 return $jsonArray[$emojiName]; 336 } 337 338 /** 339 * Iconify normalized the name of the carbon library (making them lowercase) 340 * 341 * For instance, CSV is csv (https://icon-sets.iconify.design/carbon/csv/) 342 * 343 * This dictionary reproduce it. 344 * 345 * @param string $logicalName 346 * @return mixed 347 * @throws ExceptionCombo 348 */ 349 private static function getCarbonPhysicalName(string $logicalName) 350 { 351 $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/carbon-icons.json"); 352 $jsonContent = FileSystems::getContent($path); 353 $jsonArray = Json::createFromString($jsonContent)->toArray(); 354 $physicalName = $jsonArray[$logicalName]; 355 if ($physicalName === null) { 356 LogUtility::msg("The icon ($logicalName) is unknown as 32x32 carbon icon"); 357 // by default, just lowercase 358 return lower($logicalName); 359 } 360 return $physicalName; 361 } 362 363 /** 364 * @throws ExceptionCombo 365 */ 366 private static function getFadPhysicalName($logicalName) 367 { 368 $path = LocalPath::createFromPath(Resources::getDictionaryDirectory() . "/fad-icons.json"); 369 $jsonContent = FileSystems::getContent($path); 370 $jsonArray = Json::createFromString($jsonContent)->toArray(); 371 $physicalName = $jsonArray[$logicalName]; 372 if ($physicalName === null) { 373 LogUtility::msg("The icon ($logicalName) is unknown as fad icon"); 374 return $logicalName; 375 } 376 return $physicalName; 377 } 378 379 380 public function render(): string 381 { 382 383 if (FileSystems::exists($this->getPath())) { 384 385 $svgImageLink = SvgImageLink::createMediaLinkFromPath( 386 $this->getPath(), 387 $this->getAttributes() 388 ); 389 return $svgImageLink->renderMediaTag(); 390 391 } else { 392 393 LogUtility::msg("The icon ($this) does not exist and cannot be rendered."); 394 return ""; 395 396 } 397 398 } 399 400 401} 402