Added workflow for building the repo
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))
|
232
.github/scripts/build_assets/SeleniumRunner.py
vendored
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
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
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
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 = 20
|
||||||
|
|
||||||
|
"""
|
||||||
|
The medium wait time for the driver in seconds.
|
||||||
|
"""
|
||||||
|
MED_WAIT_IN_SEC = 5
|
||||||
|
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
headless=True):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
svg folders.
|
||||||
|
:param headless: whether to run browser in headless (no UI) mode.
|
||||||
|
"""
|
||||||
|
self.icomoon_json_path = icomoon_json_path
|
||||||
|
self.download_path = download_path
|
||||||
|
self.headless = headless
|
||||||
|
self.driver = None
|
||||||
|
self.set_options()
|
||||||
|
|
||||||
|
def set_options(self):
|
||||||
|
"""
|
||||||
|
Build the WebDriver with Firefox Options allowing downloads and
|
||||||
|
set download to download_path.
|
||||||
|
|
||||||
|
: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 = self.headless
|
||||||
|
|
||||||
|
self.driver = WebDriver(options=options)
|
||||||
|
self.driver.get(self.ICOMOON_URL)
|
||||||
|
assert "IcoMoon App" in self.driver.title
|
||||||
|
|
||||||
|
def upload_icomoon(self):
|
||||||
|
"""
|
||||||
|
Upload the icomoon_test.json to icomoon.io.
|
||||||
|
:raises TimeoutException: happens when elements are not found.
|
||||||
|
"""
|
||||||
|
print("Uploading 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:
|
||||||
|
print("hi")
|
||||||
|
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_test.json",
|
||||||
|
"Ensure that the icomoon_test.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.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):
|
||||||
|
"""
|
||||||
|
Test for the possible alert when we upload the svgs.
|
||||||
|
:return: None.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
dismiss_btn = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC, 0.15).until(
|
||||||
|
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Dismiss']"))
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
|
||||||
|
color_tab = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||||
|
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||||
|
)
|
||||||
|
color_tab.click()
|
||||||
|
|
||||||
|
color_tab = self.driver\
|
||||||
|
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||||
|
color_tab.click()
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Download the icomoon.zip from icomoon.io.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("Downloading Font files...")
|
||||||
|
self.driver.find_element_by_css_selector(
|
||||||
|
"a[href='#/select/font']"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
button = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||||
|
)
|
||||||
|
button.click()
|
||||||
|
print("Font files downloaded.")
|
||||||
|
except Exception as e:
|
||||||
|
self.close()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def close(self, err=True):
|
||||||
|
"""
|
||||||
|
Close the SeleniumRunner instance.
|
||||||
|
:param err, whether this was called due to an error or not.
|
||||||
|
"""
|
||||||
|
print("Closing down SeleniumRunner...")
|
||||||
|
self.driver.quit()
|
||||||
|
|
||||||
|
if err:
|
||||||
|
code = 1
|
||||||
|
else:
|
||||||
|
code = 0
|
||||||
|
exit(code)
|
143
.github/scripts/build_assets/filehandler.py
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
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.exists() and folder_path.is_dir()):
|
||||||
|
print(f"Invalid path. This is not a directory: {folder_path}\nSkipping entry...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
aliases = icon_info["aliases"]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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")
|
BIN
.github/scripts/build_assets/geckodriver.exe
vendored
Normal file
51
.github/scripts/icomoon_upload.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from build_assets.SeleniumRunner import SeleniumRunner
|
||||||
|
import build_assets.filehandler as filehandler
|
||||||
|
from pathlib import Path
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
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("icomoon_json_path",
|
||||||
|
help="The path to the icomoon_test.json aka the selection.json created by Icomoon",
|
||||||
|
action=PathResolverAction)
|
||||||
|
|
||||||
|
parser.add_argument("devicon_json_path",
|
||||||
|
help="The path to the devicon_test.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()
|
||||||
|
|
||||||
|
runner = SeleniumRunner(args.icomoon_json_path, args.download_path,
|
||||||
|
args.headless)
|
||||||
|
runner.upload_icomoon()
|
||||||
|
|
||||||
|
new_icons = filehandler.find_new_icons(args.devicon_json_path, args.icomoon_json_path)
|
||||||
|
svgs = filehandler.get_svgs_paths(new_icons, args.icons_folder_path)
|
||||||
|
runner.upload_svgs(svgs)
|
||||||
|
runner.download_icomoon_fonts()
|
||||||
|
|
||||||
|
zip_name = "devicon-v1.0.zip"
|
||||||
|
zip_path = str(Path(args.download_path, zip_name))
|
||||||
|
# filehandler.extract_files(zip_path, args.download_path)
|
||||||
|
# filehandler.rename_extracted_files(args.download_path)
|
||||||
|
runner.close(err=False)
|
||||||
|
print("Task completed.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1
.github/scripts/requirements.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
selenium
|
15
.github/workflows/build_icons.yml
vendored
@@ -6,7 +6,16 @@ jobs:
|
|||||||
name: Get Fonts From Icomoon
|
name: Get Fonts From Icomoon
|
||||||
runs-on: windows-2019
|
runs-on: windows-2019
|
||||||
steps:
|
steps:
|
||||||
- name: Hello World
|
- uses: actions/checkout@v2
|
||||||
uses: actions/hello-world-javascript-action@v1
|
- name: Set up Python 3.8
|
||||||
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
who-to-greet: 'Mona the Octocat'
|
python-version: 3.8
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r ./.github/scripts/requirements.txt
|
||||||
|
- name: Add geckodriver to PATH
|
||||||
|
run: set PATH=${{ format('{0}/.github/scripts/build_assets/geckodriver.exe', github.workspace) }};%PATH%
|
||||||
|
- name: Run icomoon_upload.py
|
||||||
|
run: python ./.github/scripts/icomoon_upload.py ./icomoon.json ./devicon.json ./icons ./ --headless
|
||||||
|
13
devicon.json
@@ -1,12 +1,15 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "amazonwebservices",
|
"name": "haskell",
|
||||||
"tags": ["cloud", "hosting", "server"],
|
"color": "#5d5086",
|
||||||
|
"tags": ["language", "functional"],
|
||||||
"versions": {
|
"versions": {
|
||||||
"svg": ["original", "original-wordmark", "plain-wordmark"],
|
"svg": ["original", "original-wordmark", "plain", "plain-wordmark"],
|
||||||
"font": ["original", "plain-wordmark"]
|
"font": ["plain", "plain-wordmark"]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
"aliases": []
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "android",
|
"name": "android",
|
||||||
"tags": ["os", "mobile"],
|
"tags": ["os", "mobile"],
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 816 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 684 B |