* */ namespace ComboStrap; require_once(__DIR__ . '/PluginUtility.php'); /** * Class Icon * @package ComboStrap * @see https://combostrap.com/icon * * * Material design does not have a repository structure where we can extract the location * from the name * https://material.io/resources/icons https://google.github.io/material-design-icons/ * * Injection via javascript to avoid problem with the php svgsimple library * https://www.npmjs.com/package/svg-injector */ class IconDownloader { const CONF_ICONS_MEDIA_NAMESPACE = "icons_namespace"; const CONF_ICONS_MEDIA_NAMESPACE_DEFAULT = ":" . PluginUtility::COMBOSTRAP_NAMESPACE_NAME . ":icons"; const ICON_LIBRARY_URLS = array( self::ANT_DESIGN => "https://raw.githubusercontent.com/ant-design/ant-design-icons/master/packages/icons-svg/svg", self::BOOTSTRAP => "https://raw.githubusercontent.com/twbs/icons/main/icons", self::CARBON => "https://raw.githubusercontent.com/carbon-design-system/carbon/main/packages/icons/src/svg/32", self::CLARITY => "https://raw.githubusercontent.com/vmware/clarity-assets/master/icons/essential", self::CODE_ICON => "https://raw.githubusercontent.com/microsoft/vscode-codicons/main/src/icons", self::ELEGANT_THEME => "https://raw.githubusercontent.com/pprince/etlinefont-bower/master/images/svg/individual_icons", self::ENTYPO => "https://raw.githubusercontent.com/hypermodules/entypo/master/src/Entypo", self::ENTYPO_SOCIAL => "https://raw.githubusercontent.com/hypermodules/entypo/master/src/Entypo%20Social%20Extension", self::EVA => "https://raw.githubusercontent.com/akveo/eva-icons/master/package/icons", self::FEATHER => "https://raw.githubusercontent.com/feathericons/feather/master/icons", self::FAD => "https://raw.githubusercontent.com/fefanto/fontaudio/master/svgs", self::ICONSCOUT => "https://raw.githubusercontent.com/Iconscout/unicons/master/svg/line", self::LOGOS => "https://raw.githubusercontent.com/gilbarbara/logos/master/logos", self::MATERIAL_DESIGN => "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg", self::OCTICON => "https://raw.githubusercontent.com/primer/octicons/main/icons", self::TWEET_EMOJI => "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg", self::SIMPLE_LINE => "https://raw.githubusercontent.com/thesabbir/simple-line-icons/master/src/svgs", self::ICOMOON => "https://raw.githubusercontent.com/Keyamoon/IcoMoon-Free/master/SVG", self::DASHICONS => "https://raw.githubusercontent.com/WordPress/dashicons/master/svg-min", self::ICONOIR => "https://raw.githubusercontent.com/lucaburgio/iconoir/master/icons", self::BOX_ICON => "https://raw.githubusercontent.com/atisawd/boxicons/master/svg", self::LINE_AWESOME => "https://raw.githubusercontent.com/icons8/line-awesome/master/svg", self::FONT_AWESOME_SOLID => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/solid", self::FONT_AWESOME_BRANDS => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/brands", self::FONT_AWESOME_REGULAR => "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/master/svgs/regular", self::VAADIN => "https://raw.githubusercontent.com/vaadin/vaadin-icons/master/assets/svg", self::CORE_UI_BRAND => "https://raw.githubusercontent.com/coreui/coreui-icons/master/svg/brand", self::FLAT_COLOR_ICON => "https://raw.githubusercontent.com/icons8/flat-color-icons/master/svg", self::PHOSPHOR_ICONS => "https://raw.githubusercontent.com/phosphor-icons/phosphor-icons/master/assets", self::VSCODE => "https://raw.githubusercontent.com/vscode-icons/vscode-icons/master/icons", self::SI_GLYPH => "https://raw.githubusercontent.com/frexy/glyph-iconset/master/svg", self::AKAR_ICONS => "https://raw.githubusercontent.com/artcoholic/akar-icons/master/src/svg", self::ARCTICONS => "https://raw.githubusercontent.com/Donnnno/Arcticons/main/icons/black", self::HEALTH_ICONS => "https://raw.githubusercontent.com/resolvetosavelives/healthicons/main/public/icons/svg" ); const ICON_LIBRARY_WEBSITE_URLS = array( self::BOOTSTRAP => "https://icons.getbootstrap.com/", self::MATERIAL_DESIGN => "https://materialdesignicons.com/", self::FEATHER => "https://feathericons.com/", self::CODE_ICON => "https://microsoft.github.io/vscode-codicons/", self::LOGOS => "https://svgporn.com/", self::CARBON => "https://www.carbondesignsystem.com/guidelines/icons/library/", self::TWEET_EMOJI => "https://twemoji.twitter.com/", self::ANT_DESIGN => "https://ant.design/components/icon/", self::CLARITY => "https://clarity.design/foundation/icons/", self::OCTICON => "https://primer.style/octicons/", self::ICONSCOUT => "https://iconscout.com/unicons/explore/line", self::ELEGANT_THEME => "https://github.com/pprince/etlinefont-bower", self::EVA => "https://akveo.github.io/eva-icons/", self::ENTYPO_SOCIAL => "http://www.entypo.com", self::ENTYPO => "http://www.entypo.com", self::SIMPLE_LINE => "https://thesabbir.github.io/simple-line-icons", self::ICOMOON => "https://icomoon.io/", self::DASHICONS => "https://developer.wordpress.org/resource/dashicons/", self::ICONOIR => "https://iconoir.com", self::BOX_ICON => "https://boxicons.com", self::LINE_AWESOME => "https://icons8.com/line-awesome", self::FONT_AWESOME => "https://fontawesome.com/", self::FONT_AWESOME_SOLID => "https://fontawesome.com/", self::FONT_AWESOME_BRANDS => "https://fontawesome.com/", self::FONT_AWESOME_REGULAR => "https://fontawesome.com/", self::VAADIN => "https://vaadin.com/icons", self::CORE_UI_BRAND => "https://coreui.io/icons/", self::FLAT_COLOR_ICON => "https://icons8.com/icons/color", self::PHOSPHOR_ICONS => "https://phosphoricons.com/", self::VSCODE => "https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons", self::SI_GLYPH => "https://glyph.smarticons.co/", self::AKAR_ICONS => "https://akaricons.com/", self::ARCTICONS => "https://arcticons.com/", self::HEALTH_ICONS => "https://healthicons.org/", self::MATERIAL_DESIGN_ACRONYM => "https://materialdesignicons.com/", self::COMBO => "" ); const CONF_DEFAULT_ICON_LIBRARY = "defaultIconLibrary"; const CONF_DEFAULT_ICON_LIBRARY_DEFAULT = self::MATERIAL_DESIGN_ACRONYM; /** * Deprecated library acronym / name */ const DEPRECATED_LIBRARY_ACRONYM = array( "bs" => self::BOOTSTRAP, // old one (deprecated) - the good acronym is bi (seen also in the class) "md" => self::MATERIAL_DESIGN ); /** * Public known acronym / name (Used in the configuration) */ const PUBLIC_LIBRARY_ACRONYM = array( "bi" => self::BOOTSTRAP, self::MATERIAL_DESIGN_ACRONYM => self::MATERIAL_DESIGN, "fe" => self::FEATHER, "codicon" => self::CODE_ICON, "logos" => self::LOGOS, "carbon" => self::CARBON, "twemoji" => self::TWEET_EMOJI, "ant-design" => self::ANT_DESIGN, "fad" => self::FAD, "clarity" => self::CLARITY, "octicon" => self::OCTICON, "uit" => self::ICONSCOUT, "et" => self::ELEGANT_THEME, "eva" => self::EVA, "entypo-social" => self::ENTYPO_SOCIAL, "entypo" => self::ENTYPO, "simple-line-icons" => self::SIMPLE_LINE, "icomoon-free" => self::ICOMOON, "dashicons" => self::DASHICONS, "iconoir" => self::ICONOIR, "bx" => self::BOX_ICON, "la" => self::LINE_AWESOME, "fa-solid" => self::FONT_AWESOME_SOLID, "fa-brands" => self::FONT_AWESOME_BRANDS, "fa-regular" => self::FONT_AWESOME_REGULAR, "vaadin" => self::VAADIN, "cib" => self::CORE_UI_BRAND, "flat-color-icons" => self::FLAT_COLOR_ICON, "ph" => self::PHOSPHOR_ICONS, "vscode-icons" => self::VSCODE, "si-glyph" => self::SI_GLYPH, "akar-icons" => self::AKAR_ICONS, "arcticons" => self::ARCTICONS, "healthicons" => self::HEALTH_ICONS, "combo" => self::COMBO ); const FEATHER = "feather"; const BOOTSTRAP = "bootstrap"; const MATERIAL_DESIGN = "material-design"; const CODE_ICON = "codicon"; const LOGOS = "logos"; const CARBON = "carbon"; const MATERIAL_DESIGN_ACRONYM = "mdi"; const TWEET_EMOJI = "twemoji"; const ANT_DESIGN = "ant-design"; const FAD = "fad"; const CLARITY = "clarity"; const OCTICON = "octicon"; const ICONSCOUT = "iconscout"; const ELEGANT_THEME = "elegant-theme"; const EVA = "eva"; const ENTYPO_SOCIAL = "entypo-social"; const ENTYPO = "entypo"; const SIMPLE_LINE = "simple-line"; const ICOMOON = "icomoon"; const DASHICONS = " dashicons"; const ICONOIR = "iconoir"; const BOX_ICON = "box-icon"; const LINE_AWESOME = "line-awesome"; const FONT_AWESOME_SOLID = "font-awesome-solid"; const FONT_AWESOME_BRANDS = "font-awesome-brands"; const FONT_AWESOME_REGULAR = "font-awesome-regular"; const FONT_AWESOME = "font-awesome"; const VAADIN = "vaadin"; const CORE_UI_BRAND = "cib"; const FLAT_COLOR_ICON = "flat-color-icons"; const PHOSPHOR_ICONS = "ph"; const VSCODE = "vscode"; const SI_GLYPH = "si-glyph"; const COMBO = WikiPath::COMBO_DRIVE; const AKAR_ICONS = "akar-icons"; const ARCTICONS = "articons"; const HEALTH_ICONS = "healthicons"; /** * The icon library * @var mixed|null */ private $library; /** * @var false|string */ private $iconName; private WikiPath $path; /** * @throws ExceptionBadArgument|ExceptionFileSystem */ public function __construct(string $name) { $iconNameSpace = SiteConfig::getConfValue(self::CONF_ICONS_MEDIA_NAMESPACE, self::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT); if (substr($iconNameSpace, 0, 1) != WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { $iconNameSpace = WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT . $iconNameSpace; } if (substr($iconNameSpace, -1) != WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT) { $iconNameSpace = $iconNameSpace . ":"; } $mediaPathId = $iconNameSpace . $name . ".svg";; $this->path = WikiPath::createMediaPathFromPath($mediaPathId); // Bug: null file created when the stream could not get any byte // We delete them if (FileSystems::exists($this->path)) { if (FileSystems::getSize($this->path) === 0) { FileSystems::delete($this->path); } } /** * Name parsing to extract the library name and icon name */ // default $confValue = SiteConfig::getConfValue(self::CONF_DEFAULT_ICON_LIBRARY, self::CONF_DEFAULT_ICON_LIBRARY_DEFAULT); $this->setLibrary($confValue); $this->setIconName($name); // parse $sepPosition = strpos($name, ":"); if ($sepPosition !== false) { $libraryName = substr($name, 0, $sepPosition); $this->setLibrary($libraryName); $iconName = substr($name, $sepPosition + 1); $this->setIconName($iconName); /** * Special case, internal library */ if ($this->getLibrary() === self::COMBO) { $this->path = WikiPath::createComboResource($iconName . ".svg"); } } } public static function isInIconDirectory(Path $path): bool { $iconNameSpace = SiteConfig::getConfValue(IconDownloader::CONF_ICONS_MEDIA_NAMESPACE, IconDownloader::CONF_ICONS_MEDIA_NAMESPACE_DEFAULT); if (strpos($path->toAbsoluteId(), $iconNameSpace) !== false) { return true; } return false; } /** * @throws ExceptionBadArgument - if the icon library is not supported * @throws ExceptionFileSystem */ public static function createFromName(string $name): IconDownloader { return new IconDownloader($name); } /** * @throws ExceptionCompile */ private static function getPhysicalNameFromDictionary(string $logicalName, string $library) { $jsonArray = Dictionary::getFrom("$library-icons"); $physicalName = $jsonArray[$logicalName]; if ($physicalName === null) { LogUtility::msg("The icon ($logicalName) is unknown for the library ($library)"); // by default, just lowercase return strtolower($logicalName); } return $physicalName; } public function getIconName(): string { return $this->iconName; } /** * @throws ExceptionCompile */ public function getDownloadUrl(): string { /** * The test of the supported library * happens lately because the user may install them manually */ $library = $this->library; if (!in_array($library, array_keys($this->getLibraries()))) { throw new ExceptionBadArgument("The library ($library) is not a icon library supported"); } // Get the qualified library name $acronymLibraries = self::getLibraries(); if (isset($acronymLibraries[$library])) { $library = $acronymLibraries[$library]; } // Get the url $iconLibraries = self::ICON_LIBRARY_URLS; if (!isset($iconLibraries[$library])) { throw new ExceptionCompile("The icon library ($library) is unknown. The icon could not be downloaded.", Icon::ICON_CANONICAL_NAME); } else { $iconBaseUrl = $iconLibraries[$library]; } /** * Name processing */ $iconName = $this->iconName; switch ($library) { case self::VSCODE: case self::FLAT_COLOR_ICON: $iconName = str_replace("-", "_", $iconName); break; case self::TWEET_EMOJI: try { $iconName = self::getEmojiCodePoint($iconName); } catch (ExceptionCompile $e) { throw new ExceptionCompile("The emoji name $iconName is unknown. The emoji could not be downloaded.", Icon::ICON_CANONICAL_NAME, 0, $e); } break; case self::ANT_DESIGN: // table-outlined where table is the svg, outlined the category // ordered-list-outlined where ordered-list is the svg, outlined the category [$iconName, $iconType] = self::explodeInTwoPartsByLastPosition($iconName, "-"); $iconBaseUrl .= "/$iconType"; break; case self::CARBON: /** * Iconify normalized the name of the carbon library (making them lowercase) * * For instance, CSV is csv (https://icon-sets.iconify.design/carbon/csv/) * * This dictionary reproduce it. */ $iconName = self::getPhysicalNameFromDictionary($iconName, self::CARBON); break; case self::FAD: $iconName = self::getPhysicalNameFromDictionary($iconName, self::FAD); break; case self::ICOMOON: $iconName = self::getPhysicalNameFromDictionary($iconName, self::ICOMOON); break; case self::CORE_UI_BRAND: $iconName = self::getPhysicalNameFromDictionary($iconName, self::CORE_UI_BRAND); break; case self::EVA: // Eva // example: eva:facebook-fill [$iconName, $iconType] = self::explodeInTwoPartsByLastPosition($iconName, "-"); $iconBaseUrl .= "/$iconType/svg"; if ($iconType === "outline") { // for whatever reason, the name of outline icon has outline at the end // and not for the fill icon $iconName .= "-$iconType"; } break; case self::PHOSPHOR_ICONS: // example: activity-light [$iconShortName, $iconType] = self::explodeInTwoPartsByLastPosition($iconName, "-"); $iconBaseUrl .= "/$iconType"; break; case self::SIMPLE_LINE: // Bug if ($iconName === "social-pinterest") { $iconName = "social-pintarest"; } break; case self::BOX_ICON: [$iconType, $extractedIconName] = self::explodeInTwoPartsByLastPosition($iconName, "-"); switch ($iconType) { case "bxl": $iconBaseUrl .= "/logos"; break; case "bx": $iconBaseUrl .= "/regular"; break; case "bxs": $iconBaseUrl .= "/solid"; break; default: throw new ExceptionCompile("The box-icon icon ($iconName) has a type ($iconType) that is unknown, we can't determine the location of the icon to download"); } break; case self::SI_GLYPH: $iconName = "si-glyph-" . $iconName; break; case self::HEALTH_ICONS: [$extractedIconName, $iconType] = self::explodeInTwoPartsByLastPosition($iconName, "-"); switch ($iconType) { case "outline": case "negative": $iconBaseUrl .= "/$iconType"; $iconName = $extractedIconName; break; default: // no $iconBaseUrl .= "/filled"; } $iconName = self::getPhysicalNameFromDictionary($iconName, self::HEALTH_ICONS); break; } // The url return "$iconBaseUrl/$iconName.svg"; } /** * @throws ExceptionCompile */ public function download() { $libraryName = $this->getLibrary(); $mediaDokuPath = $this->path; /** * Create the target directory if it does not exist */ $iconDir = $mediaDokuPath->getParent(); if (!FileSystems::exists($iconDir)) { try { FileSystems::createDirectory($iconDir); } catch (ExceptionCompile $e) { throw new ExceptionCompile("The icon directory ($iconDir) could not be created.", Icon::ICON_CANONICAL_NAME, 0, $e); } } /** * Download the icon * The `@` delete the E_WARNING upon failure * * https://www.php.net/manual/en/function.fopen.php */ $downloadUrl = $this->getDownloadUrl(); ErrorHandler::phpErrorAsException(); try { $filePointer = fopen($downloadUrl, 'r'); } catch (\Exception $e) { // (ie no icon file found at ($downloadUrl) $message = "We couldn't find the icon $this->iconName) from the"; try { $urlLibrary = $this->getLibraryUrl(); $message = "$message library $libraryName"; } catch (ExceptionNotFound $e) { if (PluginUtility::isDevOrTest()) { throw $e; } $message = "$message library $libraryName"; } $message = "$message. Error: {$e->getMessage()}"; throw new ExceptionCompile($message, Icon::ICON_CANONICAL_NAME); } finally { ErrorHandler::restore(); } $numberOfByte = file_put_contents($mediaDokuPath->toLocalPath()->toAbsolutePath()->toAbsoluteId(), $filePointer); if ($numberOfByte !== false) { LogUtility::msg("The icon ($this) from the library ($libraryName) was downloaded to ($mediaDokuPath)", LogUtility::LVL_MSG_INFO, Icon::ICON_CANONICAL_NAME); } else { LogUtility::msg("Internal error: The icon ($this) from the library ($libraryName) could no be written to ($mediaDokuPath)", LogUtility::LVL_MSG_ERROR, Icon::ICON_CANONICAL_NAME); } } /** * @param $iconName * @param $mediaFilePath * @deprecated Old code to download icon from the material design api */ public static function downloadIconFromMaterialDesignApi($iconName, $mediaFilePath) { // Try the official API // Read the icon meta of // Meta Json file got all icons // // * Available at: https://raw.githubusercontent.com/Templarian/MaterialDesign/master/meta.json // * See doc: https://github.com/Templarian/MaterialDesign-Site/blob/master/src/content/api.md) $arrayFormat = true; $iconMetaJson = json_decode(file_get_contents(__DIR__ . '/../resources/dictionary/icon-meta.json'), $arrayFormat); $iconId = null; foreach ($iconMetaJson as $key => $value) { if ($value['name'] == $iconName) { $iconId = $value['id']; break; } } if ($iconId != null) { // Download // Call to the API // https://dev.materialdesignicons.com/contribute/site/api $downloadUrl = "https://materialdesignicons.com/api/download/icon/svg/$iconId"; $filePointer = file_put_contents($mediaFilePath, fopen($downloadUrl, 'r')); if ($filePointer == false) { LogUtility::msg("The file ($downloadUrl) could not be downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_ERROR, Icon::ICON_CANONICAL_NAME); } else { LogUtility::msg("The material design icon ($iconName) was downloaded to ($mediaFilePath)", LogUtility::LVL_MSG_INFO, Icon::ICON_CANONICAL_NAME); } } } private static function getLibraries(): array { return array_merge( self::PUBLIC_LIBRARY_ACRONYM, self::DEPRECATED_LIBRARY_ACRONYM ); } /** * @throws ExceptionCompile */ public static function getEmojiCodePoint(string $emojiName) { $path = DirectoryLayout::getComboDictionaryDirectory()->resolve("emojis.json"); $jsonContent = FileSystems::getContent($path); $jsonArray = Json::createFromString($jsonContent)->toArray(); return $jsonArray[$emojiName]; } /** * @param string $iconName * @param string $sep * @return array * @throws ExceptionCompile */ private static function explodeInTwoPartsByLastPosition(string $iconName, string $sep = "-"): array { $index = strrpos($iconName, $sep); if ($index === false) { throw new ExceptionCompile ("We expect that the icon name ($iconName) has two parts separated by a `-` (example: table-outlined). The icon could not be downloaded.", Icon::ICON_CANONICAL_NAME); } $firstPart = substr($iconName, 0, $index); $secondPart = substr($iconName, $index + 1); return [$firstPart, $secondPart]; } public function __toString() { return $this->getIconName(); } private function getLibrary() { return $this->library; } /** * @noinspection PhpReturnValueOfMethodIsNeverUsedInspection */ private function setLibrary($libraryName): IconDownloader { /** * The library may be not supported * but the users can install them manually * We test the support of the library if the logo does not exists * on the file system */ $this->library = $libraryName; return $this; } private function setIconName(string $iconName) { $this->iconName = $iconName; } public function getPath(): WikiPath { return $this->path; } /** * @throws ExceptionNotFound */ public function getLibraryUrl() { $library = $this->getLibrary(); $libraryUrl = @self::ICON_LIBRARY_WEBSITE_URLS[$library]; if ($libraryUrl === null) { throw new ExceptionNotFound("The url for the library ($library) was not found"); } return $libraryUrl; } }