Merge pull request #268 from devicons/build-integrate
Introducing github action for build automatisation (using python)
19
.github/scripts/build_assets/PathResolverAction.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class PathResolverAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
path = Path(values).resolve()
|
||||
if not path.exists():
|
||||
raise ValueError(f"{path} doesn't exist.")
|
||||
|
||||
if self.dest == "icons_folder_path":
|
||||
if not path.is_dir():
|
||||
raise ValueError("icons_folder_path must be a directory")
|
||||
|
||||
elif self.dest == "download_path":
|
||||
if not path.is_dir():
|
||||
raise ValueError("download_path must be a directory")
|
||||
|
||||
setattr(namespace, self.dest, str(path))
|
264
.github/scripts/build_assets/SeleniumRunner.py
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
|
||||
class SeleniumRunner:
|
||||
"""
|
||||
A runner that upload and download Icomoon resources using Selenium.
|
||||
The WebDriver will use Firefox.
|
||||
"""
|
||||
|
||||
"""
|
||||
The long wait time for the driver in seconds.
|
||||
"""
|
||||
LONG_WAIT_IN_SEC = 25
|
||||
|
||||
"""
|
||||
The medium wait time for the driver in seconds.
|
||||
"""
|
||||
MED_WAIT_IN_SEC = 6
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
SHORT_WAIT_IN_SEC = 0.6
|
||||
|
||||
"""
|
||||
The Icomoon Url.
|
||||
"""
|
||||
ICOMOON_URL = "https://icomoon.io/app/#/select"
|
||||
|
||||
def __init__(self, icomoon_json_path: str, download_path: str,
|
||||
geckodriver_path: str, headless):
|
||||
"""
|
||||
Create a SeleniumRunner object.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:param download_path: the location where you want to download
|
||||
the icomoon.zip to.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
"""
|
||||
self.icomoon_json_path = icomoon_json_path
|
||||
self.download_path = download_path
|
||||
self.driver = None
|
||||
self.set_options(geckodriver_path, headless)
|
||||
|
||||
def set_options(self, geckodriver_path: str, headless: bool):
|
||||
"""
|
||||
Build the WebDriver with Firefox Options allowing downloads and
|
||||
set download to download_path.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
|
||||
:raises AssertionError: if the page title does not contain
|
||||
"IcoMoon App".
|
||||
"""
|
||||
options = Options()
|
||||
allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
|
||||
# disable prompt to download from Firefox
|
||||
options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
|
||||
options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
|
||||
|
||||
# set the default download path to downloadPath
|
||||
options.set_preference("browser.download.folderList", 2)
|
||||
options.set_preference("browser.download.dir", self.download_path)
|
||||
options.headless = headless
|
||||
|
||||
self.driver = WebDriver(options=options, executable_path=geckodriver_path)
|
||||
self.driver.get(self.ICOMOON_URL)
|
||||
assert "IcoMoon App" in self.driver.title
|
||||
|
||||
def upload_icomoon(self):
|
||||
"""
|
||||
Upload the icomoon.json to icomoon.io.
|
||||
:raises TimeoutException: happens when elements are not found.
|
||||
"""
|
||||
print("Uploading icomoon.json file...")
|
||||
try:
|
||||
# find the file input and enter the file path
|
||||
import_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "div#file input"))
|
||||
)
|
||||
import_btn.send_keys(self.icomoon_json_path)
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
try:
|
||||
confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
except SeleniumTimeoutException as e:
|
||||
print(e.stacktrace)
|
||||
print("Cannot find the confirm button when uploading the icomoon.json",
|
||||
"Ensure that the icomoon.json is in the correct format for Icomoon.io",
|
||||
sep='\n')
|
||||
self.close()
|
||||
|
||||
print("JSON file uploaded.")
|
||||
|
||||
def upload_svgs(self, svgs: List[str]):
|
||||
"""
|
||||
Upload the SVGs provided in folder_info
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
"""
|
||||
try:
|
||||
print("Uploading SVGs...")
|
||||
|
||||
edit_mode_btn = self.driver.find_element_by_css_selector(
|
||||
"div.btnBar button i.icon-edit"
|
||||
)
|
||||
edit_mode_btn.click()
|
||||
|
||||
self.click_hamburger_input()
|
||||
|
||||
for svg in svgs:
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
"li.file input[type=file]"
|
||||
)
|
||||
import_btn.send_keys(svg)
|
||||
print(f"Uploaded {svg}")
|
||||
self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC, "Dismiss")
|
||||
self.remove_color_from_icon()
|
||||
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
def click_hamburger_input(self):
|
||||
"""
|
||||
Click the hamburger input until the pop up menu appears. This
|
||||
method is needed because sometimes, we need to click the hamburger
|
||||
input two times before the menu appears.
|
||||
:return: None.
|
||||
"""
|
||||
try:
|
||||
hamburger_input = self.driver.find_element_by_css_selector(
|
||||
"button.btn5.lh-def.transparent i.icon-menu"
|
||||
)
|
||||
|
||||
menu_appear_callback = ec.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "h1#setH2 ul")
|
||||
)
|
||||
|
||||
while not menu_appear_callback(self.driver):
|
||||
hamburger_input.click()
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
def test_for_possible_alert(self, wait_period: float, btn_text: str):
|
||||
"""
|
||||
Test for the possible alert when we upload the svgs.
|
||||
:param wait_period: the wait period for the possible alert
|
||||
in seconds.
|
||||
:param btn_text: the text that the alert's button will have.
|
||||
:return: None.
|
||||
"""
|
||||
try:
|
||||
dismiss_btn = WebDriverWait(self.driver, wait_period, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']"))
|
||||
)
|
||||
dismiss_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass
|
||||
|
||||
def remove_color_from_icon(self):
|
||||
"""
|
||||
Remove the color from the most recent uploaded icon.
|
||||
:return: None.
|
||||
"""
|
||||
try:
|
||||
recently_uploaded_icon = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@id='set0']//mi-box[1]//div"))
|
||||
)
|
||||
recently_uploaded_icon.click()
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
try:
|
||||
color_tab = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||
)
|
||||
color_tab.click()
|
||||
|
||||
remove_color_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||
remove_color_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
try:
|
||||
close_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-close")
|
||||
close_btn.click()
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
def download_icomoon_fonts(self, zip_path: Path):
|
||||
"""
|
||||
Download the icomoon.zip from icomoon.io.
|
||||
:param zip_path: the path to the zip file after it's downloaded.
|
||||
"""
|
||||
try:
|
||||
print("Downloading Font files...")
|
||||
self.driver.find_element_by_css_selector(
|
||||
"a[href='#/select/font']"
|
||||
).click()
|
||||
|
||||
self.test_for_possible_alert(self.MED_WAIT_IN_SEC, "Continue")
|
||||
download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||
)
|
||||
download_btn.click()
|
||||
if self.wait_for_zip(zip_path):
|
||||
print("Font files downloaded.")
|
||||
else:
|
||||
raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.")
|
||||
except Exception as e:
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
def wait_for_zip(self, zip_path: Path) -> bool:
|
||||
"""
|
||||
Wait for the zip file to be downloaded by checking for its existence
|
||||
in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time
|
||||
is 1 sec.
|
||||
:param zip_path: the path to the zip file after it's
|
||||
downloaded.
|
||||
:return: True if the file is found within the allotted time, else
|
||||
False.
|
||||
"""
|
||||
end_time = time.time() + self.LONG_WAIT_IN_SEC
|
||||
while time.time() <= end_time:
|
||||
if zip_path.exists():
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the SeleniumRunner instance.
|
||||
"""
|
||||
print("Closing down SeleniumRunner...")
|
||||
self.driver.quit()
|
145
.github/scripts/build_assets/filehandler.py
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
import json
|
||||
from zipfile import ZipFile
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def find_new_icons(devicon_json_path: str, icomoon_json_path: str) -> List[dict]:
|
||||
"""
|
||||
Find the newly added icons by finding the difference between
|
||||
the devicon_test.json and the icomoon_test.json.
|
||||
:param devicon_json_path, the path to the devicon_test.json.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:return: a list of the new icons as JSON objects.
|
||||
"""
|
||||
with open(devicon_json_path) as json_file:
|
||||
devicon_json = json.load(json_file)
|
||||
|
||||
with open(icomoon_json_path) as json_file:
|
||||
icomoon_json = json.load(json_file)
|
||||
|
||||
new_icons = []
|
||||
for icon in devicon_json:
|
||||
if is_not_in_icomoon_json(icon, icomoon_json):
|
||||
new_icons.append(icon)
|
||||
|
||||
return new_icons
|
||||
|
||||
|
||||
def is_not_in_icomoon_json(icon, icomoon_json) -> bool:
|
||||
"""
|
||||
Checks whether the icon's name is not in the icomoon_json.
|
||||
:param icon: the icon object we are searching for.
|
||||
:param icomoon_json: the icomoon json object parsed from
|
||||
icomoon_test.json.
|
||||
:return: True if icon's name is not in the icomoon_test.json, else False.
|
||||
"""
|
||||
pattern = re.compile(f"^{icon['name']}-")
|
||||
|
||||
for font in icomoon_json["icons"]:
|
||||
if pattern.search(font["properties"]["name"]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_svgs_paths(new_icons: List[dict], icons_folder_path: str) -> List[str]:
|
||||
"""
|
||||
Get all the suitable svgs file path listed in the devicon_test.json.
|
||||
:param new_icons, a list containing the info on the new icons.
|
||||
:param icons_folder_path, the path where the function can find the
|
||||
listed folders.
|
||||
:return: a list of svg file paths that can be uploaded to Icomoon.
|
||||
"""
|
||||
file_paths = []
|
||||
for icon_info in new_icons:
|
||||
folder_path = Path(icons_folder_path, icon_info['name'])
|
||||
|
||||
if not folder_path.is_dir():
|
||||
raise ValueError(f"Invalid path. This is not a directory: {folder_path}.")
|
||||
|
||||
# TODO: remove the try-except when the devicon.json is upgraded
|
||||
try:
|
||||
aliases = icon_info["aliases"]
|
||||
except KeyError:
|
||||
aliases = [] # create empty list of aliases if not provided in devicon.json
|
||||
|
||||
for font_version in icon_info["versions"]["font"]:
|
||||
if is_alias(font_version, aliases):
|
||||
continue
|
||||
|
||||
file_name = f"{icon_info['name']}-{font_version}.svg"
|
||||
path = Path(folder_path, file_name)
|
||||
|
||||
if path.exists():
|
||||
file_paths.append(str(path))
|
||||
else:
|
||||
raise ValueError(f"This path doesn't exist: {path}")
|
||||
|
||||
return file_paths
|
||||
|
||||
|
||||
def is_alias(font_version: str, aliases: List[dict]):
|
||||
"""
|
||||
Check whether the font version is an alias of another version.
|
||||
:return: True if it is, else False.
|
||||
"""
|
||||
for alias in aliases:
|
||||
if font_version == alias["alias"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def extract_files(zip_path: str, extract_path: str, delete=True):
|
||||
"""
|
||||
Extract the style.css and font files from the devicon.zip
|
||||
folder. Must call the gulp task "get-icomoon-files"
|
||||
before calling this.
|
||||
:param zip_path, path where the zip file returned
|
||||
from the icomoon.io is located.
|
||||
:param extract_path, the location where the function
|
||||
will put the extracted files.
|
||||
:param delete, whether the function should delete the zip file
|
||||
when it's done.
|
||||
"""
|
||||
print("Extracting zipped files...")
|
||||
|
||||
icomoon_zip = ZipFile(zip_path)
|
||||
target_files = ('selection.json', 'fonts/', 'fonts/devicon.ttf',
|
||||
'fonts/devicon.woff', 'fonts/devicon.eot',
|
||||
'fonts/devicon.svg', "style.css")
|
||||
for file in target_files:
|
||||
icomoon_zip.extract(file, extract_path)
|
||||
|
||||
print("Files extracted")
|
||||
|
||||
if delete:
|
||||
print("Deleting devicon zip file...")
|
||||
icomoon_zip.close()
|
||||
os.remove(zip_path)
|
||||
|
||||
|
||||
def rename_extracted_files(extract_path: str):
|
||||
"""
|
||||
Rename the extracted files selection.json and style.css.
|
||||
:param extract_path, the location where the function
|
||||
can find the extracted files.
|
||||
:return: None.
|
||||
"""
|
||||
print("Renaming files")
|
||||
old_to_new_list = [
|
||||
{
|
||||
"old": Path(extract_path, "selection.json"),
|
||||
"new": Path(extract_path, "icomoon.json")
|
||||
},
|
||||
{
|
||||
"old": Path(extract_path, "style.css"),
|
||||
"new": Path(extract_path, "devicon.css")
|
||||
}
|
||||
]
|
||||
|
||||
for dict_ in old_to_new_list:
|
||||
os.replace(dict_["old"], dict_["new"])
|
||||
|
||||
print("Files renamed")
|
6
.github/scripts/build_assets/geckodriver-v0.27.0-win64/README.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
## geckodriver
|
||||
Proxy for using W3C WebDriver compatible clients to interact with Gecko-based browsers.
|
||||
This program provides the HTTP API described by the WebDriver protocol to communicate with Gecko browsers, such as Firefox.
|
||||
It translates calls into the Marionette remote protocol by acting as a proxy between the local- and remote ends.
|
||||
|
||||
This application was taken from: https://github.com/mozilla/geckodriver/releases/tag/v0.27.0
|
BIN
.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe
vendored
Normal file
71
.github/scripts/icomoon_upload.py
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
|
||||
# pycharm complains that build_assets is an unresolved ref
|
||||
# don't worry about it, the script still runs
|
||||
from build_assets.SeleniumRunner import SeleniumRunner
|
||||
from build_assets import filehandler
|
||||
from build_assets.PathResolverAction import PathResolverAction
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description="Upload svgs to Icomoon to create icon files.")
|
||||
|
||||
parser.add_argument("--headless",
|
||||
help="Whether to run the browser in headless/no UI mode",
|
||||
action="store_true")
|
||||
|
||||
parser.add_argument("geckodriver_path",
|
||||
help="The path to the firefox executable file",
|
||||
action=PathResolverAction)
|
||||
|
||||
parser.add_argument("icomoon_json_path",
|
||||
help="The path to the icomoon.json aka the selection.json created by Icomoon",
|
||||
action=PathResolverAction)
|
||||
|
||||
parser.add_argument("devicon_json_path",
|
||||
help="The path to the devicon.json",
|
||||
action=PathResolverAction)
|
||||
|
||||
parser.add_argument("icons_folder_path",
|
||||
help="The path to the icons folder",
|
||||
action=PathResolverAction)
|
||||
|
||||
parser.add_argument("download_path",
|
||||
help="The path where you'd like to download the Icomoon files to",
|
||||
action=PathResolverAction)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path)
|
||||
if len(new_icons) == 0:
|
||||
print("No files need to be uploaded. Ending script...")
|
||||
return
|
||||
|
||||
# print list of new icons, separated by comma
|
||||
print("List of new icons:")
|
||||
print(*new_icons, sep = "\n")
|
||||
try:
|
||||
runner = SeleniumRunner(args.icomoon_json_path, args.download_path,
|
||||
args.geckodriver_path, args.headless)
|
||||
runner.upload_icomoon()
|
||||
svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path)
|
||||
runner.upload_svgs(svgs)
|
||||
|
||||
|
||||
zip_name = "devicon-v1.0.zip"
|
||||
zip_path = Path(args.download_path, zip_name)
|
||||
runner.download_icomoon_fonts(zip_path)
|
||||
filehandler.extract_files(str(zip_path), args.download_path)
|
||||
filehandler.rename_extracted_files(args.download_path)
|
||||
runner.close()
|
||||
print("Task completed.")
|
||||
except TimeoutException as e:
|
||||
print(e)
|
||||
print(e.stacktrace)
|
||||
runner.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
.github/scripts/requirements.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
selenium==3.141.0
|
42
.github/workflows/build_icons.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build Icons
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build:
|
||||
name: Get Fonts From Icomoon
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
- name: Setup Python v3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies (python, pip, npm)
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r ./.github/scripts/requirements.txt
|
||||
npm install
|
||||
- name: Run icomoon_upload.py
|
||||
run: >
|
||||
python ./.github/scripts/icomoon_upload.py
|
||||
./.github/scripts/build_assets/geckodriver-v0.27.0-win64/geckodriver.exe
|
||||
./icomoon.json ./devicon.json ./icons ./ --headless
|
||||
- name: Upload geckodriver.log for debugging purposes
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: geckodriver-log
|
||||
path: ./geckodriver.log
|
||||
- name: Running gulp default task for building devicon.min.css
|
||||
run: |
|
||||
gulp default
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v4
|
||||
with:
|
||||
commit_message: Build new icons, icomoon.json and devicon.css
|
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.idea
|
||||
geckodriver.log
|
||||
__pycache__
|
||||
*.pyc
|
10
devicon.css
@@ -1,10 +1,10 @@
|
||||
@font-face {
|
||||
font-family: 'devicon';
|
||||
src: url('fonts/devicon.eot?xdc21g');
|
||||
src: url('fonts/devicon.eot?xdc21g#iefix') format('embedded-opentype'),
|
||||
url('fonts/devicon.ttf?xdc21g') format('truetype'),
|
||||
url('fonts/devicon.woff?xdc21g') format('woff'),
|
||||
url('fonts/devicon.svg?xdc21g#devicon') format('svg');
|
||||
src: url('fonts/devicon.eot?yl3aib');
|
||||
src: url('fonts/devicon.eot?yl3aib#iefix') format('embedded-opentype'),
|
||||
url('fonts/devicon.ttf?yl3aib') format('truetype'),
|
||||
url('fonts/devicon.woff?yl3aib') format('woff'),
|
||||
url('fonts/devicon.svg?yl3aib#devicon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
|
@@ -620,7 +620,7 @@
|
||||
"tags": ["testing"],
|
||||
"versions": {
|
||||
"svg": ["plain"],
|
||||
"font": ["plain"]
|
||||
"font": []
|
||||
}
|
||||
},
|
||||
{
|
||||
|
2
devicon.min.css
vendored
@@ -11,4 +11,4 @@
|
||||
<path class="st1" d="M0,71.3l6.2-9.3L0,52.6h4.7l6.2,9.3l-6.2,9.3H0z"/>
|
||||
<path class="st2" d="m6.2 71.3 6.2-9.3-6.2-9.3h4.7l12.5 18.7h-4.7l-3.9-5.8-3.9 5.8h-4.7z"/>
|
||||
<path class="st3" d="m21.3 65.8-2.1-3.1h7.3v3.1h-5.2zm-3.1-4.7-2.1-3.1h10.4v3.1h-8.3z"/>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -8,4 +8,4 @@
|
||||
<path class="st0" d="M0,110.2L30.1,65L0,19.9h22.6L52.7,65l-30.1,45.1H0z"/>
|
||||
<path class="st1" d="M30.1,110.2L60.2,65L30.1,19.9h22.6l60.2,90.3H90.4L71.5,81.9l-18.8,28.2H30.1z"/>
|
||||
<path class="st2" d="m102.9 83.8-10-15.1h35.1v15.1h-25.1zm-15.1-22.5-10-15.1h50.2v15.1h-40.2z"/>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 551 B |
@@ -5,4 +5,4 @@
|
||||
<path d="M0,71.3l6.2-9.3L0,52.6h4.7l6.2,9.3l-6.2,9.3H0z"/>
|
||||
<path d="m6.2 71.3 6.2-9.3-6.2-9.3h4.7l12.5 18.7h-4.7l-3.9-5.8-3.9 5.8h-4.7z"/>
|
||||
<path d="m21.3 65.8-2.1-3.1h7.3v3.1h-5.2zm-3.1-4.7-2.1-3.1h10.4v3.1h-8.3z"/>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -3,4 +3,4 @@
|
||||
<path d="M0,110.2L30.1,65L0,19.9h22.6L52.7,65l-30.1,45.1H0z"/>
|
||||
<path d="M30.1,110.2L60.2,65L30.1,19.9h22.6l60.2,90.3H90.4L71.5,81.9l-18.8,28.2H30.1z"/>
|
||||
<path d="m102.9 83.8-10-15.1h35.1v15.1h-25.1zm-15.1-22.5-10-15.1h50.2v15.1h-40.2z"/>
|
||||
</svg>
|
||||
</svg>
|
Before Width: | Height: | Size: 420 B After Width: | Height: | Size: 419 B |