feat: Integrate build into this repo

This commit is contained in:
tupadr3
2021-10-20 18:16:01 +02:00
parent e53fd61259
commit 14bbf0ac16
22 changed files with 1795 additions and 1 deletions

21
.editorconfig Normal file
View File

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

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
# and /bower_components/* ignored by default
# Ignore built files except build/index.js
/node_modules/*

18
.eslintrc.json Normal file
View File

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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto

41
.gitignore vendored
View File

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

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"printWidth": 100,
"useTabs": true,
"semi": true,
"singleQuote": true
}

31
package.json Normal file
View File

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

BIN
src/assets/bin/plantuml.jar Normal file

Binary file not shown.

Binary file not shown.

114
src/lib/cliOptions.js Normal file
View File

@@ -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,
},
];

58
src/lib/config.js Normal file
View File

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

340
src/lib/fonts.js Normal file
View File

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

107
src/lib/handler/dev.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

108
src/lib/handler/dev2.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

108
src/lib/handler/fa.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) scale(0.9)` +
` translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

132
src/lib/handler/fa5.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) scale(0.9)` +
` translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

128
src/lib/handler/gov.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) scale(0.9)` +
` translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

106
src/lib/handler/material.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

128
src/lib/handler/weather.js Normal file
View File

@@ -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 (
`<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.height} ${params.height}" preserveAspectRatio="xMinYMid slice">\n` +
`\t<svg width="${params.height}" height="${params.height}"` +
` viewBox="0 0 ${params.width} ${params.height}">\n` +
`\t\t<g transform="scale(1 -1) scale(0.8)` +
` translate(${params.shiftX} ${params.shiftY})">\n` +
`\t\t\t<path d="${params.path}" fill="${params.color}" />\n` +
`\t\t</g>\n` +
`\t</svg>\n` +
`</svg>`
);
}
module.exports = {
load: load,
getSvgCode: getSvgCode
};

237
src/lib/index.js Normal file
View File

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

54
src/lib/logger.js Normal file
View File

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

54
src/lib/utils.js Normal file
View File

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