|
@@ -0,0 +1,270 @@
|
|
|
|
+#!/usr/bin/env node
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * SVG 图标组件生成器
|
|
|
|
+ *
|
|
|
|
+ * 转换 SVG 图标为 inline 数据
|
|
|
|
+ *
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+const fs = require('fs')
|
|
|
|
+const path = require('path')
|
|
|
|
+const readline = require('readline')
|
|
|
|
+const cliInput = prompt => {
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
+ const rl = readline.createInterface({
|
|
|
|
+ input: process.stdin,
|
|
|
|
+ output: process.stdout,
|
|
|
|
+ })
|
|
|
|
+ rl.question(prompt, ipt => {
|
|
|
|
+ resolve(ipt)
|
|
|
|
+ rl.close()
|
|
|
|
+ })
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+const { optimize } = require('svgo')
|
|
|
|
+
|
|
|
|
+const parseOptions = () => {
|
|
|
|
+ const argv = process.argv.slice(2)
|
|
|
|
+ const opts = {}
|
|
|
|
+ argv.forEach(arg => {
|
|
|
|
+ if (arg.indexOf('=') > -1) {
|
|
|
|
+ const o = arg.split('=')
|
|
|
|
+ opts[o[0]] = o[1]
|
|
|
|
+ } else {
|
|
|
|
+ opts[arg] = true
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ return opts
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const options = parseOptions()
|
|
|
|
+
|
|
|
|
+const regColorFormat = /#([0-9A-F]{3}|[0-9A-F]{6}|[0-9A-F]{8})|(?:rgb|hsl|hwb|lab|lch|oklab|oklch)a?\([\d.,\/%]+\)/i
|
|
|
|
+const regCurrentColor = /([:"'] *)currentColor/g
|
|
|
|
+
|
|
|
|
+const root = path.resolve(__dirname + '/../../..')
|
|
|
|
+if (fs.existsSync(root + '/src')) {
|
|
|
|
+ root = root + '/src'
|
|
|
|
+}
|
|
|
|
+const svgo = root + '/svgo.config.js'
|
|
|
|
+if (!fs.existsSync(svgo)) {
|
|
|
|
+ fs.copyFileSync(__dirname + '/svgo.config.js', svgo)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 需要处理的颜色属性
|
|
|
|
+let svgBase = root + '/assets'
|
|
|
|
+const svgFolder = options.source || svgBase + '/svg-icons'
|
|
|
|
+
|
|
|
|
+if (!fs.existsSync(svgFolder)) {
|
|
|
|
+ fs.mkdirSync(svgFolder, { recursive: true })
|
|
|
|
+}
|
|
|
|
+const svgLibFile = root + `/static/${options.lib || 'svg-icons-lib'}.js`
|
|
|
|
+
|
|
|
|
+const svgLibCurrent = (() => {
|
|
|
|
+ try {
|
|
|
|
+ let raw = fs.readFileSync(svgLibFile, { encoding: 'utf-8' })
|
|
|
|
+ const start = raw.indexOf('const collections = {') + 20
|
|
|
|
+ const end = raw.indexOf('// == collection end')
|
|
|
|
+ raw = raw.substring(start, end).trim().replace(/;$/, '')
|
|
|
|
+ return JSON.parse(raw).default
|
|
|
|
+ } catch (err) {}
|
|
|
|
+ return {}
|
|
|
|
+})()
|
|
|
|
+const svgPath = path.resolve(svgFolder)
|
|
|
|
+const svgLib = {}
|
|
|
|
+const svgList = (() => {
|
|
|
|
+ const regFile = /\.svg$/i
|
|
|
|
+ const fileList = []
|
|
|
|
+ const loadSvgList = searchPath => {
|
|
|
|
+ const files = fs.readdirSync(searchPath, { recursive: false })
|
|
|
|
+ for (const file of files) {
|
|
|
|
+ const filePath = path.posix.join(searchPath, file)
|
|
|
|
+ const stat = fs.statSync(filePath)
|
|
|
|
+ if (stat.isFile()) {
|
|
|
|
+ if (!regFile.test(filePath)) continue
|
|
|
|
+
|
|
|
|
+ const item = filePath.slice(filePath.lastIndexOf('svg-icons/') + 10)
|
|
|
|
+ // const name = item.slice(0, -4).replace(/[/!@#$%^&*()+=\[\]{};:'",.<>\?`]/g, '-').toLowerCase()
|
|
|
|
+ const name = item.slice(0, -4).replace(/[\/\\]/g, '-').toLowerCase()
|
|
|
|
+ const content = fs.readFileSync(filePath, {
|
|
|
|
+ encoding: 'utf-8',
|
|
|
|
+ })
|
|
|
|
+ fileList.push({
|
|
|
|
+ name,
|
|
|
|
+ content,
|
|
|
|
+ hasCurrentColor: regCurrentColor.test(content),
|
|
|
|
+ file: filePath,
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ //
|
|
|
|
+ else if (stat.isDirectory()) {
|
|
|
|
+ loadSvgList(filePath)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return fileList
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return loadSvgList(svgPath).filter(item => !!item)
|
|
|
|
+})(svgPath)
|
|
|
|
+
|
|
|
|
+//
|
|
|
|
+const defaultColor = '#22ac38'
|
|
|
|
+let currentColor = svgLibCurrent.currentColor || ''
|
|
|
|
+let palette = []
|
|
|
|
+
|
|
|
|
+const generateIcon = svgRaw => {
|
|
|
|
+ // svgo 会过滤纯黑, 此处对纯黑做简单处理
|
|
|
|
+ svgRaw = svgRaw.replace(regCurrentColor, `$1${currentColor}`).replace(/#0{3,8}/g, '#ZZZZZZ')
|
|
|
|
+ const result = optimize(svgRaw, {
|
|
|
|
+ multipass: true,
|
|
|
|
+ })
|
|
|
|
+ result.data = result.data.replace(/#Z{3,8}/gi, '#000')
|
|
|
|
+
|
|
|
|
+ const regColor = /(fill|stroke|stop-color):([^;}]+)/g
|
|
|
|
+ const parseColor = colorStr => {
|
|
|
|
+ if (!regRef.test(colorStr)) {
|
|
|
|
+ return colorStr
|
|
|
|
+ }
|
|
|
|
+ // 从 Gradient 引用里获取颜色
|
|
|
|
+ const match = colorStr.match(regRef)
|
|
|
|
+ const ref = gradients.find(item => {
|
|
|
|
+ return item.id === match[1]
|
|
|
|
+ })
|
|
|
|
+ return ref ? ref.colors : []
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Step 1, find all Gradient define and make KV map
|
|
|
|
+ const regGradient = /<(\w+Gradient) id="([^"]+)" [^>]+>(.+?)<\/\1>/g
|
|
|
|
+ const regStopColors = /stop-color="([^"]+)"/g
|
|
|
|
+ const gradients = [...result.data.matchAll(regGradient)].map(item => {
|
|
|
|
+ const colors = [...item[3].matchAll(regStopColors)].map(item => item[1])
|
|
|
|
+ return {
|
|
|
|
+ id: item[2],
|
|
|
|
+ content: item[3],
|
|
|
|
+ colors,
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // Step 2, find all class define and make KV map
|
|
|
|
+ const regClass = /\.(cls-\d+)\{([^}]+)\}/g
|
|
|
|
+ const regRef = /url\(#(.+)\)/
|
|
|
|
+ const classes = [...result.data.matchAll(regClass)].map(item => {
|
|
|
|
+ // Search colors from item[2]
|
|
|
|
+ // find fill, stroke, stop-color
|
|
|
|
+ const colors = [...item[2].matchAll(regColor)].map(item => parseColor(item[2]))
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ id: item[1],
|
|
|
|
+ content: item[2],
|
|
|
|
+ colors: colors,
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // Step 3, find all style, class, stroke property and search color in value
|
|
|
|
+ const regProps = /(fill|stroke|class|style)="([^"]+)"/g
|
|
|
|
+ const props = [...result.data.matchAll(regProps)].map(content => {
|
|
|
|
+ let colors = []
|
|
|
|
+ if (content[1] === 'class') {
|
|
|
|
+ const item = classes.find(item => item.id === content[2])
|
|
|
|
+ colors = item ? item.colors : []
|
|
|
|
+ } else if (content[1] === 'style') {
|
|
|
|
+ colors = [...content[2].matchAll(regColor)].map(item => parseColor(item[2]))
|
|
|
|
+ } else if (content[1] === 'fill') {
|
|
|
|
+ colors = parseColor(content[2])
|
|
|
|
+ } else {
|
|
|
|
+ colors = content[2]
|
|
|
|
+ }
|
|
|
|
+ return {
|
|
|
|
+ prop: content[1],
|
|
|
|
+ content: content[2],
|
|
|
|
+ // 定义里的颜色
|
|
|
|
+ colors: colors,
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // Step 4, filter
|
|
|
|
+ let colors = props
|
|
|
|
+ .map(item => item.colors)
|
|
|
|
+ .flat(2)
|
|
|
|
+ .filter(item => item !== 'none' && !/^url/.test(item))
|
|
|
|
+ .map(item => (item === 'currentColor' ? currentColor : item))
|
|
|
|
+ colors = Array.from(new Set(colors))
|
|
|
|
+
|
|
|
|
+ // Append new colors to palette
|
|
|
|
+ palette = Array.from(new Set([...palette, ...colors]))
|
|
|
|
+
|
|
|
|
+ // Build color index
|
|
|
|
+ let colorMap = colors.map(c => palette.indexOf(c))
|
|
|
|
+ const colorTotal = colors.length
|
|
|
|
+
|
|
|
|
+ if (colorTotal === 0) {
|
|
|
|
+ const fixable = /<(path|circle|ellipse|polygon|polyline|rect) /g
|
|
|
|
+ if (fixable.test(result.data)) {
|
|
|
|
+ return generateIcon(result.data.replace(fixable, `<$1 fill="${currentColor || defaultColor}" `))
|
|
|
|
+ } else {
|
|
|
|
+ console.log(' SVG 图片没有配置颜色, 并且无法进行预处理。请联系作者修复此问题。https://ext.dcloud.net.cn/plugin?id=13964')
|
|
|
|
+ }
|
|
|
|
+ } else if (colorTotal > 0) {
|
|
|
|
+ console.log(' ', JSON.stringify(colors))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return [result.data, ...colorMap]
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+;(async () => {
|
|
|
|
+ // 检测是否存在 currentColor
|
|
|
|
+ const hasCurrentColor = svgList.find(item => item.hasCurrentColor)
|
|
|
|
+ if (!currentColor && hasCurrentColor) {
|
|
|
|
+ console.log('\n')
|
|
|
|
+ console.log('::>> 检测到 svg 文件中使用了 currentColor 变量,该变量在组件中不被支持。\n')
|
|
|
|
+ currentColor = defaultColor
|
|
|
|
+ console.log(`::>> 需要指定一个颜色替代,默认黑色为(${currentColor})。\n`)
|
|
|
|
+
|
|
|
|
+ do {
|
|
|
|
+ const color = await cliInput(`请输入颜色,直接回车(enter)使用默认值:`)
|
|
|
|
+ if (color && color.length && !regColorFormat.test(color)) {
|
|
|
|
+ console.log('\n::>> 颜色格式不正确,请输入以下格式的颜色值:\n')
|
|
|
|
+ console.log('::>>', ['#000', '#000000', 'rgb(0, 0, 0)', 'rgba(0, 0, 0, 1)'].join(' '), '\n')
|
|
|
|
+ } else {
|
|
|
|
+ currentColor = color && color.length ? color.replace(/ /g, '') : defaultColor
|
|
|
|
+ }
|
|
|
|
+ } while (!currentColor)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ svgList.forEach(item => {
|
|
|
|
+ console.log(item.name)
|
|
|
|
+ svgLib[item.name] = generateIcon(item.content)
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ const data = {
|
|
|
|
+ icons: JSON.parse(JSON.stringify(svgLib)),
|
|
|
|
+ currentColor,
|
|
|
|
+ $_colorPalette: palette,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const hasChange = JSON.stringify(svgLibCurrent) !== JSON.stringify(data)
|
|
|
|
+ if (hasChange) {
|
|
|
|
+ const scriptTpl = fs.readFileSync(__dirname + '/svg-icons-lib.tpl.js', {
|
|
|
|
+ encoding: 'utf-8',
|
|
|
|
+ })
|
|
|
|
+ const params = {
|
|
|
|
+ datetime: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
|
|
|
|
+ default: JSON.stringify(data, null, 2).split('\n').join('\n '),
|
|
|
|
+ }
|
|
|
|
+ const script = scriptTpl.replace(/__(\w+)__/g, (_, key) => {
|
|
|
|
+ return params[key] || _
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ fs.writeFileSync(svgLibFile, script)
|
|
|
|
+ console.log(`\nTotal ${Object.keys(svgLib).length} svg icon(s) generated.`)
|
|
|
|
+ } else {
|
|
|
|
+ console.log(`\nTotal ${Object.keys(svgLib).length} svg icon(s) generated, nochange.`)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (hasCurrentColor) {
|
|
|
|
+ console.log('\n')
|
|
|
|
+ console.log(' 当前有使用到 currentColor 变量,可通过文件 static/svg-icons-lib.js 里的 currentColor 属性进行修改。')
|
|
|
|
+ console.log('\n')
|
|
|
|
+ }
|
|
|
|
+})()
|