diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9c0d9770 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = tab +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..16e959be --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +# and /bower_components/* ignored by default + +# Ignore built files except build/index.js +/node_modules/* diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..a209ab6c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "root": true, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2017 + }, + "env": { + "node": true, + "es6": true + }, + "rules": { + "no-console": 0 + }, + "globals": { + "console": true, + "log": true + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..21256661 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.gitignore b/.gitignore index cdcd2d66..cf8edbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,41 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* -testing +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# npm, yarn, build etc +.vscode +.sass-cache +yarn.lock +package-lock.json + +# Bower dependency directory (https://bower.io/) +bower_components + +# Dependency directories +node_modules/ +jspm_packages/ + +# Optional eslint cache +.eslintcache + +# Os +Thumbs.db +ehthumbs.db +Desktop.ini +.directory +*~ +.DS_Store + +# maven & eclipse +target/ +.classpath +.project + +# project specific +.tmp diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..4171b836 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "useTabs": true, + "semi": true, + "singleQuote": true +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..d9340af3 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "plantuml-icon-font-sprites", + "version": "0.1.0", + "author": "tupadr3", + "description": "A usefull description", + "license": "MIT", + "scripts": { + "devel": "node ./src/lib/index.js --devel", + "build": "rm .tmp/build -rf && node ./src/lib/index.js -c 4 --release", + "clean": "rm .tmp -rf" + }, + "devDependencies": {}, + "dependencies": { + "bluebird": "^3.5.1", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.0.2", + "dateformat": "^3.0.3", + "eslint": "^5.9.0", + "extend": "^3.0.1", + "fs-extra": "^6.0.1", + "isomorphic-git": ">=1.8.2", + "lodash": "^4.17.10", + "minimist": "^1.2.0", + "ncp": "^2.0.0", + "os": "^0.1.1", + "pngjs": "^3.3.3", + "progress": "^2.0.0", + "winston": "^2.4.2", + "xml2js": "^0.4.19" + } +} diff --git a/src/assets/bin/plantuml.jar b/src/assets/bin/plantuml.jar new file mode 100644 index 00000000..c9fdc185 Binary files /dev/null and b/src/assets/bin/plantuml.jar differ diff --git a/src/assets/bin/rsvg-convert.exe b/src/assets/bin/rsvg-convert.exe new file mode 100644 index 00000000..02d59919 Binary files /dev/null and b/src/assets/bin/rsvg-convert.exe differ diff --git a/src/lib/cliOptions.js b/src/lib/cliOptions.js new file mode 100644 index 00000000..bcd82a18 --- /dev/null +++ b/src/lib/cliOptions.js @@ -0,0 +1,114 @@ +const os = require('os'); + +let cpuCount = os.cpus().length; + +if (cpuCount > 2) { + cpuCount = Number((cpuCount / 2).toFixed(0)); + if (cpuCount > 6) { + cpuCount--; + cpuCount--; + } +} + +let binRsvgDft = os.platform() === 'linux' ? 'rsvg-convert' : 'src/assets/bin/rsvg-convert.exe'; + +module.exports = [ + { + name: 'limit', + type: Number, + alias: 'l', + defaultValue: 0, + }, + { + name: 'concurrency', + type: Number, + alias: 'c', + defaultValue: cpuCount, + }, + { + name: 'progress', + type: Boolean, + alias: 'p', + defaultValue: true, + }, + { + name: 'verbose', + type: Boolean, + alias: 'v', + defaultValue: false, + }, + { + name: 'formats', + type: String, + multiple: true, + defaultValue: ['png', 'svg', 'puml'], + typeLabel: '{underline format} ...', + }, + { + name: 'release', + type: Boolean, + defaultValue: false, + }, + { + name: 'colors', + type: String, + multiple: true, + defaultValue: ['black'], + typeLabel: '{underline color} ...', + }, + { + name: 'sizes', + type: Number, + multiple: true, + defaultValue: [48], + typeLabel: '{underline size} ...', + }, + { + name: 'icons', + type: String, + multiple: true, + defaultValue: [], + typeLabel: '{underline icon} ...', + }, + { + name: 'fonts', + type: String, + multiple: true, + defaultValue: [], + typeLabel: '{underline font} ...', + }, + { + name: 'binPlantuml', + type: String, + defaultValue: 'src/assets/bin/plantuml.jar', + description: 'The path to the PlantUML executable (.jar) to use', + }, + { + name: 'binRsvg', + type: String, + defaultValue: binRsvgDft, + description: 'The path to the rsvg-convert executable to use', + }, + { + name: 'build', + type: String, + defaultValue: 'build', + }, + { + name: 'temp', + type: String, + defaultValue: '.tmp', + }, + { + name: 'devel', + type: Boolean, + defaultValue: false, + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Display this usage guide.', + defaultValue: false, + }, +]; diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 00000000..2c9e4c27 --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,58 @@ +/** + * @authot tupadr3 + */ +const cliArgs = require('command-line-args'), + cliOptions = require('./cliOptions.js'), + path = require('path'); + +function initConfig() { + const cfg = cliArgs(cliOptions); + const fontsDef = require('./fonts').def(); + let fonts = cfg.fonts; + cfg.fonts = []; + + // validate fonts + if (fonts.length > 0) { + fonts.forEach((item) => { + const found = fontsDef.find(function (element) { + return element.type === item; + }); + if (found) { + cfg.fonts.push(found); + } else { + throw new Error('Font ' + item + ' not found'); + } + }); + } else { + cfg.fonts = fontsDef; + } + + cfg.png = false; + cfg.puml = false; + cfg.svg = false; + + if (cfg.formats) { + cfg.formats.forEach((item) => { + if (item === 'png') { + cfg.png = true; + } + if (item === 'puml') { + cfg.puml = true; + } + if (item === 'svg') { + cfg.svg = true; + } + }); + } + + // setup dirs + cfg.dirs = {}; + cfg.dirs.temp = path.resolve(cfg.temp); + cfg.dirs.project = path.resolve('./'); + cfg.dirs.generated = path.resolve(cfg.temp + '/generated'); + cfg.dirs.build = path.resolve(cfg.temp + '/' + cfg.build); + cfg.dirs.fonts = path.resolve(cfg.temp + '/fonts'); + return cfg; +} + +module.exports = initConfig(); diff --git a/src/lib/fonts.js b/src/lib/fonts.js new file mode 100644 index 00000000..2fa07ac6 --- /dev/null +++ b/src/lib/fonts.js @@ -0,0 +1,340 @@ +/** + * @authot tupadr3 + */ +const log = require('./logger'), + fs = require('fs-extra'), + git = require('isomorphic-git'), + http = require('isomorphic-git/http/node'), + PNG = require('pngjs').PNG; + +function def() { + const fontDefs = [ + { + prefix: 'FA5', + name: 'font-awesome-5', + type: 'fa5', + repo: 'https://github.com/FortAwesome/Font-Awesome.git', + branch: '5.15.3', + }, + { + prefix: 'FA', + name: 'font-awesome', + type: 'fa', + repo: 'https://github.com/FortAwesome/Font-Awesome.git', + branch: 'fa-4', + }, + { + prefix: 'DEV', + name: 'devicons', + type: 'dev', + repo: 'https://github.com/vorillaz/devicons.git', + branch: 'master', + }, + { + prefix: 'GOV', + name: 'govicons', + type: 'gov', + repo: 'https://github.com/540co/govicons.git', + branch: '1.5.1', + }, + { + prefix: 'WEATHER', + name: 'weather', + type: 'weather', + repo: 'https://github.com/erikflowers/weather-icons.git', + branch: 'master', + }, + { + prefix: 'MATERIAL', + name: 'material', + type: 'material', + repo: 'https://github.com/google/material-design-icons.git', + branch: '3.0.2', + }, + { + prefix: 'DEV2', + name: 'devicons2', + type: 'dev2', + repo: 'https://github.com/devicons/devicon.git', + branch: 'v2.12.0', + }, + ]; + return fontDefs; +} + +async function load(cfg) { + // Prep the folder structue + let buildFolder = cfg.dirs.build; + await fs.ensureDirSync(buildFolder); + + let work = []; + for (let item of cfg.fonts) { + try { + item.path = await repo(cfg, item); + } catch (err) { + throw err; + } + + let buildSetFolder = buildFolder + '/' + item.type; + await fs.ensureDirSync(buildSetFolder); + await fs.ensureDirSync(cfg.dirs.generated); + + if (cfg.svg) { + await fs.ensureDirSync(buildSetFolder + '/svg'); + } + if (cfg.puml) { + await fs.ensureDirSync(buildSetFolder + '/puml'); + } + if (cfg.png) { + await fs.ensureDirSync(buildSetFolder + '/png'); + for (let index in cfg.sizes) { + await fs.ensureDirSync(buildSetFolder + '/png/' + cfg.sizes[index]); + } + } + + const fontHandler = require('./handler/' + item.type); + let fontData = await fontHandler.load(cfg, item); + + if (cfg.icons.length > 0) { + log.debug(`Filtering selection to ${cfg.icons}`); + fontData = fontData.filter((item) => { + if (cfg.icons.includes(`${item.type}-${item.id}`)) { + return true; + } + return false; + }); + } + work = work.concat(fontData); + } + + if (cfg.limit > 0) { + log.debug(`Trimming selection from ${work.length} to ${cfg.limit}`); + work = work.slice(0, cfg.limit); + } + + return work; +} + +async function repo(cfg, item) { + log.info('Loading repo ' + item.repo + ' into ' + cfg.dirs.fonts); + + const repoDir = cfg.dirs.fonts + '/' + item.type, + repoGitDir = cfg.dirs.fonts + '/' + item.type + '/.git'; + + try { + await fs.ensureDirSync(repoDir); + + // check if it already exists + log.debug(`checking dir ${repoGitDir} for repo`); + const repoExists = fs.existsSync(repoGitDir); + + if (!repoExists) { + await git.clone({ + fs, + http, + dir: repoDir, + url: item.repo, + singleBranch: true, + ref: item.branch, + depth: 10, + }); + } + + log.debug(`checkout ${item.repo} branch:${item.branch} completed to dir ${repoDir}`); + } catch (err) { + log.error('repo error', err); + throw err; + } + return repoDir; +} + +async function generate(cfg, item) { + if (cfg.svg) { + try { + await generateSvg(cfg, item); + } catch (error) { + log.error(error); + } + } + + if (cfg.png) { + for (let index in cfg.sizes) { + try { + await generatePng(cfg, item, cfg.sizes[index]); + } catch (error) { + log.error(error); + } + } + } + if (cfg.puml) { + try { + await generatePuml(cfg, item); + } catch (error) { + log.error(error); + } + } +} + +function generateSvg(cfg, item) { + log.debug('Generating svg for ' + item.type + '-' + item.id); + + return new Promise((resolve) => { + var svgCode = getSvgCode(cfg, item); + var filename = cfg.dirs.build + '/' + item.type + '/svg/' + item.id + '.svg'; + log.debug('Wrting svg for ' + item.type + '-' + item.id + ' to ' + filename); + fs.writeFileSync(filename, svgCode); + resolve(filename); + }); +} + +function getSvgCode(cfg, item) { + const fontHandler = require('./handler/' + item.type); + let svgCode = fontHandler.getSvgCode(cfg, item); + return svgCode; +} + +function generatePng(cfg, item, size, path) { + return new Promise(function (resolve, reject) { + let destPath = path; + if (!destPath) { + destPath = cfg.dirs.build + '/' + item.type + '/png/' + size + '/' + item.id + '.png'; + } + + log.debug('Generating png for ' + item.type + '-' + item.id); + + let cliparams = ['-a', '-w', size, '-h', size, '-f', 'png', '-o', destPath], + error; + log.debug(cfg.binRsvg, cliparams.join(' ')); + let rsvgConvert = require('child_process').spawn(cfg.binRsvg, cliparams); + rsvgConvert.stderr.on('data', (data) => { + error += data.toString(); + }); + rsvgConvert.once('close', function (code) { + if (code > 0) { + return reject(error); + } + resolve(destPath); + }); + rsvgConvert.stdin.end(getSvgCode(cfg, item)); + }); +} + +function generatePuml(cfg, item) { + return new Promise(async function (resolve, reject) { + log.debug('Generating plantuml for ' + item.type + '-' + item.id); + + let pngInterFileName = cfg.dirs.generated + '/' + item.type + '-png-48-' + item.id + '.png'; + let pngFileName = await generatePng(cfg, item, 48, pngInterFileName); + + // now we need to modify the png a little bit + fs.createReadStream(pngFileName) + .pipe( + new PNG({ + colorType: 2, + }) + ) + .on('error', function (err) { + log.error(err); + }) + .on('parsed', async function () { + log.debug('Modifing for puml generation for icon ' + item.type + '-' + item.id); + + for (var y = 0; y < this.height; y++) { + for (var x = 0; x < this.width; x++) { + var idx = (this.width * y + x) << 2; + // invert color + if (this.data[idx] > 0) { + this.data[idx] = 0; + this.data[idx + 1] = 0; + this.data[idx + 2] = 0; + } + } + } + + // i don't know if we have to wait + this.pack().pipe(fs.createWriteStream(pngInterFileName)); + + let pumlCode; + try { + pumlCode = await getPumlCode(cfg, item, pngInterFileName); + } catch (error) { + reject(error); + return; + } + + var filename = cfg.dirs.build + '/' + item.type + '/puml/' + item.id + '.puml'; + await fs.writeFileSync(filename, pumlCode); + resolve(filename); + }); + }); +} + +function getPumlCode(cfg, item, pngPath) { + return new Promise(function (resolve, reject) { + var plantumlJar, + result = '', + error = ''; + + var template = + '@startuml' + + '\n' + + '{sprite}' + + '\n' + + '!define {set}_{entity}(_alias) ENTITY(rectangle,black,{id},_alias,{set} {entity})' + + '\n' + + '!define {set}_{entity}(_alias, _label) ENTITY(rectangle,black,{id},_label, _alias,{set} {entity})' + + '\n' + + '!define {set}_{entity}(_alias, _label, _shape) ENTITY(_shape,black,{id},_label, _alias,{set} {entity})' + + '\n' + + '!define {set}_{entity}(_alias, _label, _shape, _color) ENTITY(_shape,_color,{id},_label, _alias,{set} {entity})' + + '\n' + + 'skinparam folderBackgroundColor<<{set} {entity}>> White' + + '\n' + + '@enduml'; + + var plantumlParams = [ + '-Djava.awt.headless=true', + '-jar', + cfg.binPlantuml, + '-encodesprite', + '16', + pngPath, + ]; + + log.debug('java ' + plantumlParams.join(' ')); + + plantumlJar = require('child_process').spawn('java', plantumlParams); + plantumlJar.stdout.on('data', (data) => { + result += data.toString(); + }); + plantumlJar.stderr.on('data', (data) => { + error += data.toString(); + }); + plantumlJar.once('close', function (code) { + if (code > 0) { + reject(error); + return; + } + + var out = template.substr(0); + + var params = { + sprite: result.replace('$' + item.type, '$' + item.id.replace(/-/g, '_')), + set: item.prefix.toUpperCase(), + entity: item.id.replace(/-/g, '_').toUpperCase(), + id: item.id.replace(/-/g, '_'), + }; + + Object.keys(params).forEach(function (key) { + out = out.replace(new RegExp('{' + key + '}', 'g'), params[key]); + }); + resolve(out); + }); + }); +} + +module.exports = { + def: def, + load: load, + generate: generate, +}; diff --git a/src/lib/handler/dev.js b/src/lib/handler/dev.js new file mode 100644 index 00000000..f3b555b7 --- /dev/null +++ b/src/lib/handler/dev.js @@ -0,0 +1,107 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + loadash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + let iconList = await loadIcons(cfg, item); + let icons = await loadFontData(cfg, item, iconList); + return icons; +} + +async function loadIcons(cfg, item) { + log.debug("Loading devicons id's"); + + let content = await readFile(item.path + '/css/devicons.css'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /devicons-([\w-]*).*\s.*"\S([0-9a-f]+)"/gm; + while ((match = regex.exec(lines))) { + result.push({ + id: match[1], + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading devicons font-data'); + + let content = await readFile(item.path + '/fonts/devicons.svg'); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontData = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0) + }; + }); + + let indexFontData = await loadash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList.map(icon => { + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: indexFontData[icon.unicodeDec].data + }; + iconConfig.advWidth = parseInt(iconConfig.data['horiz-adv-x'] || svghorz); + iconConfig.offset = parseInt(offset); + iconConfig.size = parseInt(size); + return iconConfig; + }); + + return icons; +} +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: 0, + shiftY: -item.size - item.offset + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/dev2.js b/src/lib/handler/dev2.js new file mode 100644 index 00000000..4382e3e0 --- /dev/null +++ b/src/lib/handler/dev2.js @@ -0,0 +1,108 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + lodash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + return await loadFontData(cfg, item, await loadIcons(cfg, item)); +} + +async function loadIcons(cfg, item) { + log.debug("Loading devicons2 id's"); + + let content = await readFile(item.path + '/devicon.css'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /.devicon-([\w-]*):before\s?{\s*content:\s?"\\([\w|\d]*)";\s*}/; + while ((match = regex.exec(lines))) { + log.verbose('DEV2 - found ' + match[1]); + result.push({ + id: match[1].replace('-plain', ''), + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + lines = lines.replace(match[0], ''); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading devicons2 font-data'); + + let content = await readFile(item.path + '/fonts/devicon.svg'); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontData = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0) + }; + }); + + let indexFontData = await lodash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList.map(icon => { + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: indexFontData[icon.unicodeDec].data + }; + iconConfig.advWidth = parseInt(iconConfig.data['horiz-adv-x'] || svghorz); + iconConfig.offset = parseInt(offset); + iconConfig.size = parseInt(size); + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: 0, + shiftY: -item.size - item.offset + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/fa.js b/src/lib/handler/fa.js new file mode 100644 index 00000000..3d4065fc --- /dev/null +++ b/src/lib/handler/fa.js @@ -0,0 +1,108 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + loadash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString), + extend = require('extend'); + +async function load(cfg, item) { + let iconList = await loadIcons(cfg, item); + let icons = await loadFontData(cfg, item, iconList); + return icons; +} + +async function loadIcons(cfg, item) { + log.debug("Loading fa id's"); + let content = await readFile(item.path + '/less/variables.less'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /@fa-var-([\w-]+):\s*"\\([0-9a-f]+)";/g; + while ((match = regex.exec(lines))) { + result.push({ + id: match[1], + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading fa font-data'); + let content = await readFile(item.path + '/fonts/fontawesome-webfont.svg'); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontData = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0) + }; + }); + + let indexFontData = await loadash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList.map(icon => { + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: indexFontData[icon.unicodeDec].data + }; + iconConfig.advWidth = parseInt(iconConfig.data['horiz-adv-x'] || svghorz); + iconConfig.offset = parseInt(offset); + iconConfig.size = parseInt(size); + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: item.advWidth / 10 / 2, + shiftY: -item.size - item.offset - item.size / 10 / 2 + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/fa5.js b/src/lib/handler/fa5.js new file mode 100644 index 00000000..41fae246 --- /dev/null +++ b/src/lib/handler/fa5.js @@ -0,0 +1,132 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + loadash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + let iconList = await loadIcons(cfg, item); + let icons = await loadFontData(cfg, item, iconList); + return icons; +} + +async function loadIcons(cfg, item) { + log.debug("Loading fa id's"); + let content = await readFile(item.path + '/less/_variables.less'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /@fa-var-([\w-]+):\s*"\\([0-9a-f]+)";/g; + while ((match = regex.exec(lines))) { + result.push({ + id: match[1], + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading fa5 font-data'); + + let filesList = [ + item.path + '/webfonts/fa-regular-400.svg', + item.path + '/webfonts/fa-brands-400.svg', + item.path + '/webfonts/fa-solid-900.svg' + ]; + + let fontData = []; + for (let key in filesList) { + let item = filesList[key]; + let content = await readFile(item); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontDataItem = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0), + svghorz: svghorz, + offset: -offset, + size: size + }; + }); + fontData = fontData.concat(fontDataItem); + } + + let indexFontData = await loadash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList + .filter(icon => { + if (!indexFontData[icon.unicodeDec]) { + log.debug(`Skipping ${icon.unicodeHex}`); + return false; + } + return true; + }) + .map(icon => { + let iconData = indexFontData[icon.unicodeDec]; + + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: iconData.data, + offset: parseInt(iconData.offset), + size: parseInt(iconData.size), + advWidth: parseInt(iconData.data['horiz-adv-x'] || iconData.svghorz) + }; + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: item.advWidth / 10 / 2, + shiftY: -item.size +item.offset -item.size / 10 / 2 + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/gov.js b/src/lib/handler/gov.js new file mode 100644 index 00000000..d412296a --- /dev/null +++ b/src/lib/handler/gov.js @@ -0,0 +1,128 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + loadash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + let iconList = await loadIcons(cfg, item); + let icons = await loadFontData(cfg, item, iconList); + return icons; +} + +async function loadIcons(cfg, item) { + log.debug("Loading fa id's"); + let content = await readFile(item.path + '/less/variables.less'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /@gi-([\w-]+):\s*"\\([0-9a-f]+)";/g; + while ((match = regex.exec(lines))) { + result.push({ + id: match[1], + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading fa5 font-data'); + + let filesList = [item.path + '/fonts/govicons-webfont.svg']; + + let fontData = []; + for (let key in filesList) { + let item = filesList[key]; + let content = await readFile(item); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontDataItem = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0), + svghorz: svghorz, + offset: offset, + size: size + }; + }); + fontData = fontData.concat(fontDataItem); + } + + let indexFontData = await loadash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList + .filter(icon => { + if (!indexFontData[icon.unicodeDec]) { + log.debug(`Skipping ${icon.unicodeHex}`); + return false; + } + return true; + }) + .map(icon => { + let iconData = indexFontData[icon.unicodeDec]; + + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: iconData.data, + offset: parseInt(iconData.offset), + size: parseInt(iconData.size), + advWidth: parseInt(iconData.data['horiz-adv-x'] || iconData.svghorz) + }; + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: item.advWidth / 10 / 2, + shiftY: -item.size - item.offset - item.size / 10 / 2 + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/material.js b/src/lib/handler/material.js new file mode 100644 index 00000000..21b06731 --- /dev/null +++ b/src/lib/handler/material.js @@ -0,0 +1,106 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + let icons = await loadFontData(cfg, item); + return icons; +} + +async function loadFontData(cfg, item) { + log.debug(`Loading ${item.type} font-data`); + + let filesList = [item.path + '/iconfont/MaterialIcons-Regular.svg']; + + let fontData = []; + for (let key in filesList) { + let item = filesList[key]; + let content = await readFile(item); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontDataItem = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + if (!data.$.d) { + return false; + } + if (data.$.d === 'M0 0z') { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0), + svghorz: svghorz, + offset: offset, + size: size + }; + }); + + + fontData = fontData.concat(fontDataItem); + } + + let icons = fontData.map(icon => { + let iconData = icon.data; + + let iconConfig = { + id: icon.data['glyph-name'].split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: iconData, + offset: parseInt(icon.offset), + size: parseInt(icon.size), + advWidth: parseInt(iconData['horiz-adv-x'] || icon.svghorz) + }; + + log.verbose('MATERIAL - found ' + iconConfig.id); + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: 0, + shiftY: -item.size - item.offset + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/handler/weather.js b/src/lib/handler/weather.js new file mode 100644 index 00000000..5fcc29b3 --- /dev/null +++ b/src/lib/handler/weather.js @@ -0,0 +1,128 @@ +const fs = require('fs-extra'), + util = require('util'), + log = require('./../logger'), + readFile = util.promisify(fs.readFile), + loadash = require('lodash'), + parseXml = util.promisify(require('xml2js').parseString); + +async function load(cfg, item) { + let iconList = await loadIcons(cfg, item); + let icons = await loadFontData(cfg, item, iconList); + return icons; +} + +async function loadIcons(cfg, item) { + log.debug("Loading fa id's"); + let content = await readFile(item.path + '/css/weather-icons.css'); + let lines = content.toString(); + + let match, + result = []; + + const regex = /wi-([\w-]*).*\s.*"\S([0-9a-f]+)"/g; + while ((match = regex.exec(lines))) { + result.push({ + id: match[1], + unicodeHex: match[2], + unicodeDec: parseInt(match[2], 16) + }); + } + return result; +} + +async function loadFontData(cfg, item, iconList) { + log.debug('Loading fa5 font-data'); + + let filesList = [item.path + '/font/weathericons-regular-webfont.svg']; + + let fontData = []; + for (let key in filesList) { + let item = filesList[key]; + let content = await readFile(item); + content = content.toString('utf-8'); + + let parsedXml = await parseXml(content), + glyph = parsedXml.svg.defs[0].font[0].glyph, + svghorz = parsedXml.svg.defs[0].font[0].$['horiz-adv-x'], + offset = parsedXml.svg.defs[0].font[0]['font-face'][0].$['descent'], + size = parsedXml.svg.defs[0].font[0]['font-face'][0].$['units-per-em']; + + let fontDataItem = glyph + .filter(data => { + if (!data.$.unicode) { + return false; + } + return true; + }) + .map(data => { + return { + data: data.$, + unicodeDec: data.$.unicode.charCodeAt(0), + svghorz: svghorz, + offset: offset, + size: size + }; + }); + fontData = fontData.concat(fontDataItem); + } + + let indexFontData = await loadash.keyBy(fontData, 'unicodeDec'); + + let icons = iconList + .filter(icon => { + if (!indexFontData[icon.unicodeDec]) { + log.debug(`Skipping ${icon.unicodeHex}`); + return false; + } + return true; + }) + .map(icon => { + let iconData = indexFontData[icon.unicodeDec]; + + let iconConfig = { + id: icon.id.split('-').join('_'), + type: item.type, + prefix: item.prefix, + unicodeHex: icon.unicodeHex, + unicodeDec: icon.unicodeDec, + data: iconData.data, + offset: parseInt(iconData.offset), + size: parseInt(iconData.size), + advWidth: parseInt(iconData.data['horiz-adv-x'] || iconData.svghorz) + }; + return iconConfig; + }); + + return icons; +} + +function getSvgCode(cfg, item) { + log.debug('Getting svg code for ' + item.type + '-' + item.id); + + let params = { + color: cfg.color || 'black', + path: item.data.d, + width: item.advWidth, + height: item.size, + shiftX: item.advWidth / ((10 / 2) * 2), + shiftY: -item.size - item.offset - item.size / ((10 / 2) * 2) + }; + + return ( + `\n` + + `\t\n` + + `\t\t\n` + + `\t\t\t\n` + + `\t\t\n` + + `\t\n` + + `` + ); +} + +module.exports = { + load: load, + getSvgCode: getSvgCode +}; diff --git a/src/lib/index.js b/src/lib/index.js new file mode 100644 index 00000000..050fcdaa --- /dev/null +++ b/src/lib/index.js @@ -0,0 +1,237 @@ +const Bluebird = require('bluebird'), + cliOptions = require('./cliOptions.js'), + cliUsage = require('command-line-usage'), + fs = require('fs-extra'), + path = require('path'), + ProgressBar = require('progress'), + utils = require('./utils'); + +// config +const cfg = require('./config'); +const log = require('./logger'); + +if (cfg.devel) { + cfg.limit = cfg.limit == 0 ? 5 : cfg.limit; + cfg.verbose = true; +} + +if (cfg.verbose) { + log.level = 'debug'; +} else { + log.level = 'warn'; +} + +if (cfg.help) { + const usage = cliUsage([ + { + header: 'Options', + optionList: cliOptions, + }, + ]); + + process.stdout.write(usage); +} else { + printInfo(); + generate(); +} + +async function generate() { + const fonts = require('./fonts'); + let work = [], + icons = []; + + if (cfg.github) { + cfg.png = true; + cfg.puml = true; + cfg.colors = ['black']; + cfg.sizes = [48]; + } + + if (cfg.devel) { + cfg.png = true; + cfg.puml = true; + cfg.svg = true; + cfg.limit = 25; + cfg.sizes = [128]; + cfg.icons = [ + 'fa5-user_alt', + 'fa5-gitlab', + 'fa5-server', + 'fa5-database', + 'fa-gears', + 'fa-fire', + 'fa-clock_o', + 'fa-lock', + 'fa-cloud', + 'fa-server', + 'dev-nginx', + 'dev-mysql', + 'dev-redis', + 'dev-docker', + 'dev-linux', + 'dev2-html5', + 'gov-ambulance', + 'weather-night_alt_thunderstorm', + 'material-3d_rotation', + ]; + } + + try { + icons = await fonts.load(cfg); + } catch (err) { + throw err; + } + + log.debug(`Starting work for ${icons.length} icons`); + + let progressBar = new ProgressBar('working [:bar] :percent :etas :info', { + complete: '=', + incomplete: ' ', + width: 50, + total: icons.length, + }); + + work.push( + Bluebird.map( + icons, + (item) => { + if (cfg.progress) { + progressBar.tick({ + info: item.type + '-' + item.id, + }); + } + return fonts.generate(cfg, item); + }, + { + concurrency: cfg.concurrency, + } + ) + ); + + await Promise.all(work); + + if (cfg.release) { + // copy icons to project + for (let item of cfg.fonts) { + log.debug('Copying ' + item.name); + + let releasePath = cfg.dirs.project + '/' + item.name, + pngPath = cfg.dirs.build + '/' + item.type + '/png/' + cfg.sizes[0], + pumlPath = cfg.dirs.build + '/' + item.type + '/puml'; + + await fs.ensureDirSync(releasePath); + await fs.emptyDirSync(releasePath); + + let files = await utils.getFiles(pngPath); + files = files + .map((file) => { + return { + file: path.parse(file).name + path.parse(file).ext, + name: path.parse(file).name, + ext: path.parse(file).ext, + path: file, + }; + }) + .sort(function (a, b) { + return a.name > b.name ? 1 : b.name > a.name ? -1 : 0; + }); + + log.debug('Found ' + item.name + ' ' + files.length); + + let indexFileName = releasePath + '/index.md'; + let indexContent = `# ${item.name}\n\n\n`; + indexContent += `### Overview\n`; + indexContent += `| Name | Macro | Image | Url |\n`; + indexContent += `|-------|--------|-------|-----|\n`; + + for (let file of files) { + await fs.copyFileSync(file.path, releasePath + '/' + file.file); + + indexContent += `${file.name} |`; + indexContent += `${item.type.toUpperCase()}_${file.name.toUpperCase()} |`; + indexContent += `![image-${file.name}](${file.name}.png) |`; + indexContent += `${file.name}.puml |\n`; + } + + fs.writeFileSync(indexFileName, indexContent); + + let pumlFiles = await utils.getFiles(pumlPath); + pumlFiles = pumlFiles + .map((file) => { + return { + file: path.parse(file).name + path.parse(file).ext, + name: path.parse(file).name, + ext: path.parse(file).ext, + path: file, + }; + }) + .sort(function (a, b) { + return a.name > b.name ? 1 : b.name > a.name ? -1 : 0; + }); + + for (let file of pumlFiles) { + await fs.copyFileSync(file.path, releasePath + '/' + file.file); + } + } + // Render examples + let examplesPath = cfg.dirs.project + '/examples'; + let exampleFiles = await utils.getFiles(examplesPath); + exampleFiles = exampleFiles.filter((file) => path.parse(file).ext === '.puml'); + + for (let file of exampleFiles) { + await renderPuml(file); + } + } + + console.log('Done'); +} + +function renderPuml(path) { + return new Promise(function (resolve, reject) { + var plantumlJar, + error = ''; + + var plantumlParams = ['-Djava.awt.headless=true', '-jar', cfg.binPlantuml, path]; + + log.debug('java ' + plantumlParams.join(' ')); + + plantumlJar = require('child_process').spawn('java', plantumlParams); + plantumlJar.stderr.on('data', (data) => { + error += data.toString(); + }); + plantumlJar.once('close', function (code) { + if (code > 0) { + reject(error); + return; + } + resolve(); + }); + }); +} + +function printInfo() { + // info + let msg = '\nSettings:\n'; + if (cfg.icons.length > 0) { + msg += 'icons: '; + cfg.icons.forEach((element) => (msg += ' ' + element)); + } else { + msg += 'fonts: '; + cfg.fonts.forEach((element) => (msg += ' ' + element.name)); + } + + msg += '\nformats:'; + msg += cfg.puml ? ' puml' : ''; + msg += cfg.png ? ' png' : ''; + msg += cfg.svg ? ' svg' : ''; + + msg += cfg.limit > 0 ? ' \nlimit: ' + cfg.limit : ''; + + msg += '\ncolors: '; + cfg.colors.forEach((element) => (msg += ' ' + element)); + + msg += '\nsizes: '; + cfg.sizes.forEach((element) => (msg += ' ' + element)); + + log.debug(msg); +} diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 00000000..f322ee33 --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,54 @@ +/** + * Based on https://gist.github.com/spmason/1670196 + */ +const util = require('util'), + winston = require('winston'), + logger = new winston.Logger(), + env = (process.env.NODE_ENV || '').toLowerCase(), + dateFormat = require('dateformat'); + +// Override the built-in console methods with winston hooks +switch (env) { + case 'production': + logger.add(winston.transports.File, { + filename: __dirname + '/application.log', + handleExceptions: true, + exitOnError: false + }); + break; + case 'test': + // Don't set up the logger overrides + return; + default: + logger.add(winston.transports.Console, { + colorize: true, + timestamp: function() { + return dateFormat(new Date(), 'HH:MM:ss'); + } + }); + break; +} + +function formatArgs(args) { + return [util.format.apply(util.format, Array.prototype.slice.call(args))]; +} +console.log = function() { + logger.debug.apply(logger, formatArgs(arguments)); +}; +console.info = function() { + logger.info.apply(logger, formatArgs(arguments)); +}; +console.warn = function() { + logger.warn.apply(logger, formatArgs(arguments)); +}; +console.error = function() { + logger.error.apply(logger, formatArgs(arguments)); +}; +console.debug = function() { + logger.debug.apply(logger, formatArgs(arguments)); +}; +console.progress = function() { + logger.debug.apply(logger, formatArgs(arguments)); +}; + +module.exports = logger; diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 00000000..162cea5d --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,54 @@ +const fs = require('fs-extra'), + git = require('isomorphic-git'), + http = require('isomorphic-git/http/node'), + log = require('./logger'), + { promisify } = require('util'), + { resolve } = require('path'), + readdir = promisify(fs.readdir), + stat = promisify(fs.stat); + +async function repo(repo, branch, target) { + log.debug('Loading Repo ' + repo + ' into ' + target); + + try { + await fs.ensureDirSync(target); + + // check if it already exists + log.debug(`checking dir ${target}/.git for repo`); + const repoExists = fs.existsSync(target + '/.git'); + + if (!repoExists) { + await git.clone({ + fs, + http, + dir: target, + url: repo, + singleBranch: true, + ref: branch, + depth: 10, + }); + } + + log.info(`checkout ${repo} branch:${branch} completed to dir ${target}`); + } catch (err) { + log.error('repo error', err); + throw err; + } + return target; +} + +async function getFiles(dir) { + const subdirs = await readdir(dir); + const files = await Promise.all( + subdirs.map(async (subdir) => { + const res = resolve(dir, subdir); + return (await stat(res)).isDirectory() ? getFiles(res) : res; + }) + ); + return files.reduce((a, f) => a.concat(f), []); +} + +module.exports = { + repo: repo, + getFiles: getFiles, +};