diff --git a/.github/workflows/on-release.yml b/.github/workflows/on-release.yml index e6cc6bc4..8b0f2785 100644 --- a/.github/workflows/on-release.yml +++ b/.github/workflows/on-release.yml @@ -37,3 +37,7 @@ jobs: update_flutter: uses: ./.github/workflows/update_zeta_flutter.yml secrets: inherit + + update_android: + uses: ./.github/workflows/update_zds_android.yml + secrets: inherit diff --git a/.github/workflows/update_zds_android.yml b/.github/workflows/update_zds_android.yml new file mode 100644 index 00000000..57223540 --- /dev/null +++ b/.github/workflows/update_zds_android.yml @@ -0,0 +1,19 @@ +name: Update zds_android + +on: + workflow_dispatch: + workflow_call: + secrets: + PAT: + required: true + +jobs: + publish_android: + uses: ./.github/workflows/copy-content.yml + secrets: inherit + with: + repo: ZebraDevs/zds_android + branch: "update-zeta-icons" + source_dir: "./outputs/android/." + destination_dir: "components/src/main/res/drawable" + commit_msg: "Update icons" diff --git a/package-lock.json b/package-lock.json index fb461bad..f12f4254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@types/react": "^18.3.11", "@types/sinon": "^17.0.3", "chai": "^5.1.0", + "cheerio": "^1.0.0", "dotenv": "^16.4.5", "jest": "^29.7.0", "md5": "^2.3.0", @@ -3762,6 +3763,13 @@ "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", "dev": true }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/boxen": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.1.tgz", @@ -4166,6 +4174,60 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.1.tgz", + "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4568,6 +4630,36 @@ "node": "*" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssstyle": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", @@ -4812,18 +4904,77 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/domino": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", "dev": true }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -4874,6 +5025,20 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -5595,6 +5760,26 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -8564,6 +8749,19 @@ "node": ">=8" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nunjucks": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", @@ -8953,6 +9151,33 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/package.json b/package.json index 185a2fdd..deb0121b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/react": "^18.3.11", "@types/sinon": "^17.0.3", "chai": "^5.1.0", + "cheerio": "^1.0.0", "dotenv": "^16.4.5", "jest": "^29.7.0", "md5": "^2.3.0", @@ -73,4 +74,4 @@ "url": "https://github.com/zebratechnologies/zeta-icons/issues" }, "homepage": "https://github.com/zebratechnologies/zeta-icons#readme" -} \ No newline at end of file +} diff --git a/scripts/fetch-icons/fetchIcons.ts b/scripts/fetch-icons/fetchIcons.ts index 98017c66..9b4b23b5 100644 --- a/scripts/fetch-icons/fetchIcons.ts +++ b/scripts/fetch-icons/fetchIcons.ts @@ -9,12 +9,14 @@ import { clearDirectory } from "../utils/fileUtils.js"; import { generateHash } from "../utils/hash.js"; import { optimizeSVGs } from "../utils/optimizeSvgs.js"; import { saveSVGs } from "../utils/saveSvgs.js"; +import { generateAndroidIcons } from "./generators/generateAndroidIcons.js"; export const iconsDir = "/icons"; export const tempDir = "/temp"; export const pngDir = "/png"; export const flutterDir = "/flutter"; export const webDir = "/web"; +export const androidDir = "/android"; /** * Main function to run icons action. For slightly more information, see {@link https://miro.com/app/board/uXjVKUMv1ME=/?share_link_id=952145602435 | Miro } @@ -41,6 +43,7 @@ export default async function main( const pngOutputDir = outputDir + pngDir; const dartOutputDir = outputDir + flutterDir; const tsOutputDir = outputDir + webDir; + const androidOutputDir = outputDir + androidDir; const response = await getFigmaDocument(figmaFileId, figmaToken); console.log("✅ - Fetched figma document"); @@ -83,6 +86,9 @@ export default async function main( const generateFontResult = await generateFonts(tempOutputDir, "zeta-icons", dartOutputDir, tsOutputDir); console.log("✅ - Generated fonts"); + generateAndroidIcons(androidOutputDir, manifest); + console.log("✅ - Generated Android icons."); + generateDefinitionFiles(outputDir, generateFontResult, manifest); console.log("✅ - Generated definition files."); diff --git a/scripts/fetch-icons/generators/generateAndroidIcons.ts b/scripts/fetch-icons/generators/generateAndroidIcons.ts new file mode 100644 index 00000000..59cd9156 --- /dev/null +++ b/scripts/fetch-icons/generators/generateAndroidIcons.ts @@ -0,0 +1,57 @@ +import { readFileSync, writeFileSync } from "fs"; +import { IconManifest } from "../../types/customTypes.js"; +import * as cheerio from "cheerio"; +import { createFolder, toSnakeCase } from "../../utils/fileUtils.js"; + +/** + * Generates an android icon for each icon in the manifest. + * @param iconManifest The manifest of icons to generate. + * @param outputDir The directory to save the generated icons to. + */ +export const generateAndroidIcons = (outputDir: string, iconManifest: IconManifest) => { + createFolder(outputDir); + for (const icon of iconManifest) { + const definition = icon[1]; + const svg = readFileSync(definition.roundPath).toString(); + try { + const file = generateAndroidIcon(svg); + if (file) { + writeFileSync(`${outputDir}/${getAndroidIconFileName(definition.name)}`, file); + } + } catch (e) { + console.error(`Error generating Android icon for ${definition.name}`); + } + } +}; + +/** + * Creates the file name for an Android icon. + * @param iconName The name of the icon. + * @returns The file name for the Android icon. + */ +export const getAndroidIconFileName = (iconName: string) => `ic_${toSnakeCase(iconName)}_24.xml`; + +/** + * Creates the contents of an xml file for an Android icon. + * @param svg The svg data for the icon. + * @returns The xml file contents for the Android icon as a string. + */ +export const generateAndroidIcon = (svg: string): string => { + const path = extractPath(svg); + let file = readFileSync("./scripts/fetch-icons/templates/android-icon.xml.template").toString(); + return file.replace("{{svgPath}}", path); +}; + +/** + * Extracts the path from an svg string. + * @param svgData The svg string to extract the path from. + * @returns The path from the svg string. + */ +export const extractPath = (svgData: string): string => { + const svg = cheerio.load(svgData); + const path = svg("path").attr("d"); + if (!path) { + throw new Error("Path not found"); + } + return path; +}; diff --git a/scripts/fetch-icons/templates/android-icon.xml.template b/scripts/fetch-icons/templates/android-icon.xml.template new file mode 100644 index 00000000..d907dcca --- /dev/null +++ b/scripts/fetch-icons/templates/android-icon.xml.template @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/data/add-icon.xml b/test/data/add-icon.xml new file mode 100644 index 00000000..c6b25cd6 --- /dev/null +++ b/test/data/add-icon.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/test/data/index.ts b/test/data/index.ts index 4be96ad3..2d274497 100644 --- a/test/data/index.ts +++ b/test/data/index.ts @@ -14,6 +14,8 @@ export const componentSets: ComponentSets = new Map( Object.entries(JSON.parse(readFileSync("./test/data/componentSets.json").toString())), ); +export const androidAddIcon = readFileSync("./test/data/add-icon.xml").toString(); + export const manifest: IconManifest = new Map( Object.entries(JSON.parse(readFileSync("./test/data/manifest.json").toString())), ); diff --git a/test/fetch-icons/integration/generateAndroidIcons.test.ts b/test/fetch-icons/integration/generateAndroidIcons.test.ts new file mode 100644 index 00000000..5a7322b0 --- /dev/null +++ b/test/fetch-icons/integration/generateAndroidIcons.test.ts @@ -0,0 +1,47 @@ +import { + extractPath, + generateAndroidIcon, + generateAndroidIcons, +} from "../../../scripts/fetch-icons/generators/generateAndroidIcons.js"; +import { assert } from "chai"; +import { allImageFiles, androidAddIcon, categoryNames, manifest } from "../../data/index.js"; +import { checkAndroidIconFilesExist } from "../utils.js"; +import { saveSVGs } from "../../../scripts/utils/saveSvgs.js"; +import { testIconsOutputDir } from "../../data/constants.js"; + +describe("generateAndroidIcons", () => { + before(async () => { + await saveSVGs(allImageFiles, testIconsOutputDir, categoryNames); + }); + + it("extractPath should extract the path from an svg string", () => { + const path = + "M18 13H13V18C13 18.55 12.55 19 12 19C11.45 19 11 18.55 11 18V13H6C5.45 13 5 12.55 5 12C5 11.45 5.45 11 6 11H11V6C11 5.45 11.45 5 12 5C12.55 5 13 5.45 13 6V11H18C18.55 11 19 11.45 19 12C19 12.55 18.55 13 18 13Z"; + const svg = `\n\n\n\n\n\n`; + + const result = extractPath(svg); + + assert.equal(result, path); + }); + + it("extractPath should throw an error if the given svg does not have a path", () => { + const svg = `\n\n\n\n\n`; + + assert.throws(() => extractPath(svg), "Path not found"); + }); + + it("generateAndroidIcon should create an xml file containing the correct icon", () => { + const svg = + '\n\n\n\n\n\n\n\n\n\n'; + + const file = generateAndroidIcon(svg); + + assert.deepEqual(file, androidAddIcon); + }); + + it("generateAndroidIcons should generate an xml file for each icon", () => { + generateAndroidIcons("./test/outputs/android", manifest); + + checkAndroidIconFilesExist(manifest, "./test/outputs/android"); + }); +}); diff --git a/test/fetch-icons/utils.ts b/test/fetch-icons/utils.ts index 43849b79..70fdc54c 100644 --- a/test/fetch-icons/utils.ts +++ b/test/fetch-icons/utils.ts @@ -1,6 +1,7 @@ import { assert } from "chai"; import { FontType, IconManifest } from "../../scripts/types/customTypes.js"; import { existsSync } from "fs"; +import { getAndroidIconFileName } from "../../scripts/fetch-icons/generators/generateAndroidIcons.js"; export function checkIconsExist(manifest: IconManifest) { manifest.forEach((definition) => { @@ -9,6 +10,12 @@ export function checkIconsExist(manifest: IconManifest) { }); } +export function checkAndroidIconFilesExist(manifest: IconManifest, outputDir: string) { + manifest.forEach((icon) => { + assert.equal(existsSync(`${outputDir}/${getAndroidIconFileName(icon.name)}`), true); + }); +} + export function checkFontsExist(fontName: string, dartDir: string, tsDir: string) { assert.equal(fontExists("ttf", "round", fontName, dartDir), true); assert.equal(fontExists("ttf", "sharp", fontName, dartDir), true);