1
0
mirror of https://github.com/konpa/devicon.git synced 2025-08-18 04:11:48 +02:00

Merge pull request #268 from devicons/build-integrate

Introducing github action for build automatisation (using python)
This commit is contained in:
Clemens Bastian
2020-09-07 12:20:33 +02:00
committed by GitHub
21 changed files with 4577 additions and 12 deletions

View 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))

View 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()

View 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")

View 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

71
.github/scripts/icomoon_upload.py vendored Normal file
View 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
View File

@@ -0,0 +1 @@
selenium==3.141.0

42
.github/workflows/build_icons.yml vendored Normal file
View 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
View File

@@ -1,3 +1,6 @@
node_modules
.DS_Store
.idea
geckodriver.log
__pycache__
*.pyc

View File

@@ -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;

View File

@@ -620,7 +620,7 @@
"tags": ["testing"],
"versions": {
"svg": ["plain"],
"font": ["plain"]
"font": []
}
},
{

2
devicon.min.css vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

4014
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff