-
Notifications
You must be signed in to change notification settings - Fork 0
/
svg2vue.js
145 lines (130 loc) · 3.64 KB
/
svg2vue.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
const { optimize } = require('svgo');
const fs = require('fs');
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
let inputDir = './input'; // input directory by default
let outputDir = './output';
const arguments = process.argv;
if (arguments.length === 4) {
inputDir = arguments[2];
outputDir = arguments[3];
}
const tags = new Set();
let filenames;
try {
filenames = fs.readdirSync(inputDir);
if (!filenames.length) {
console.error('No files found');
return;
}
} catch(error) {
console.log(`${inputDir} doesn't exist`);
}
if (!filenames) return;
const processFileName = (filename) => {
const prefix = filename.split('.')[0].toLowerCase(); // remove .svg extension
const processedFileName = prefix.split('-').map(str => {
const capitalized = str[0].toUpperCase().concat(str.substr(1));
return capitalized;
}).join('');
return processedFileName;
}
// file name validation
filenames.forEach(async filename => {
if (!/^[a-zA-Z-]+.svg$/.test(filename)) {
console.error(`${filename}: Invalid svg file. File name should only contain letters and -`);
return;
}
console.log(`start processing ${filename}`);
const processedFileName = processFileName(filename);
const svgString = fs.readFileSync(`${inputDir}/${filename}`, 'utf-8');
const optimizedSvg = optimize(svgString); // optimize svg
const obj = await new parser.parseStringPromise(optimizedSvg.data);
// modify svg
const width = optimizedSvg.info.width;
cleanSourceSvgFile(obj.svg, width);
// build into new svg
const builder = new xml2js.Builder();
const newXml = builder.buildObject(obj);
// build into vue template
const template = convertSvgToVue(newXml, width, processedFileName);
fs.writeFileSync(`${outputDir}/Icon${processedFileName}.vue`, template);
})
const cleanSourceSvgFile = (svg, size) => {
// reset width/height/fill
delete svg.$.height;
svg.$.width = 'none';
svg.$.fill = 'none';
svg.$.viewBox = `0 0 ${size} ${size}`;
// remove style attribute
if (svg.$.style) delete svg.$.style;
// 1) remove fill attribute from all descendant tags
// 2) collect different descendant tags for indentation
tags.add('svg');
const traverseSvg = (obj) => {
for(let key in obj) {
if (key === '$') continue;
// collect tag
tags.add(key);
const array = obj[key];
for(let ele of array) {
if (ele.$?.fill) delete ele.$.fill;
traverseSvg(ele);
}
}
}
traverseSvg(svg);
}
/**
* format svg
*/
const formatSvg = (svg) => {
// add width/fill variables to svg
let s = svg;
if (/<?xml/.test(s)) {
const lines = s.split('\n');
lines.shift();
s = lines.join('\n');
}
s = s.replace('width="none"', ':width="width"');
s = s.replace('fill="none"', ':fill="fill"');
// adjust indentation for svg and path
tags.forEach(tag => {
// indent opening tag
s = s.replace(/<${tag}/ig, `\t<${tag}`);
// indent closing tag
s = s.replace(/<\/${tag}/ig, `\t<\/${tag}`);
})
return s;
}
/**
* convert svg to vue template
*/
const convertSvgToVue = (svg, width, name) => {
const template = `<template>\n${formatSvg(svg)}\n</template>`;
const script = `\n<script lang='ts'>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Icon${name}',
props: {
size: {
type: Number,
default: ${width}
},
color: {
type: String,
default: '#000'
}
},
computed: {
width: function() {
return this.size
},
fill: function() {
return this.color
}
}
})\n</script>`
const templateStr = template.concat(script);
return templateStr;
}