generate-svg-icon.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. const fs = require('fs')
  2. const path = require('path')
  3. const readline = require('readline')
  4. const cliInput = prompt => {
  5. return new Promise((resolve, reject) => {
  6. const rl = readline.createInterface({
  7. input: process.stdin,
  8. output: process.stdout,
  9. })
  10. rl.question(prompt, ipt => {
  11. resolve(ipt)
  12. rl.close()
  13. })
  14. })
  15. }
  16. const { optimize } = require('svgo')
  17. const parseOptions = () => {
  18. const argv = process.argv.slice(2)
  19. const opts = {}
  20. argv.forEach(arg => {
  21. if (arg.indexOf('=') > -1) {
  22. const o = arg.split('=')
  23. opts[o[0]] = o[1]
  24. } else {
  25. opts[arg] = true
  26. }
  27. })
  28. return opts
  29. }
  30. const options = parseOptions()
  31. const regColorFormat = /#([0-9A-F]{3}|[0-9A-F]{6}|[0-9A-F]{8})|(?:rgb|hsl|hwb|lab|lch|oklab|oklch)a?\([\d.,\/%]+\)/i
  32. const regCurrentColor = /([:"'] *)currentColor/g
  33. const root = path.resolve(__dirname + '/../../..')
  34. if (fs.existsSync(root + '/src')) {
  35. root = root + '/src'
  36. }
  37. const svgo = root + '/svgo.config.js'
  38. if (!fs.existsSync(svgo)) {
  39. fs.copyFileSync(__dirname + '/svgo.config.js', svgo)
  40. }
  41. // 需要处理的颜色属性
  42. let svgBase = root + '/static'
  43. const svgFolder = options.source || svgBase + '/icons'
  44. // console.log(svgFolder,1212);
  45. if (!fs.existsSync(svgFolder)) {
  46. fs.mkdirSync(svgFolder, { recursive: true })
  47. }
  48. const svgLibFile = root + `/static/${options.lib || 'svg-icons-lib'}.js`
  49. const svgLibCurrent = (() => {
  50. try {
  51. let raw = fs.readFileSync(svgLibFile, { encoding: 'utf-8' })
  52. const start = raw.indexOf('const collections = {') + 20
  53. const end = raw.indexOf('// == collection end')
  54. raw = raw.substring(start, end).trim().replace(/;$/, '')
  55. return JSON.parse(raw).default
  56. } catch (err) {}
  57. return {}
  58. })()
  59. const svgPath = path.resolve(svgFolder)
  60. const svgLib = {}
  61. const svgList = (() => {
  62. const regFile = /\.svg$/i
  63. const fileList = []
  64. const loadSvgList = searchPath => {
  65. const files = fs.readdirSync(searchPath, { recursive: false })
  66. for (const file of files) {
  67. const filePath = path.posix.join(searchPath, file)
  68. const stat = fs.statSync(filePath)
  69. if (stat.isFile()) {
  70. if (!regFile.test(filePath)) continue
  71. const item = filePath.slice(filePath.lastIndexOf('svg-icons/') + 10)
  72. // const name = item.slice(0, -4).replace(/[/!@#$%^&*()+=\[\]{};:'",.<>\?`]/g, '-').toLowerCase()
  73. const name = item.split('/').pop()?.replace('.svg', '');
  74. // console.log(name,item,121212);
  75. const content = fs.readFileSync(filePath, {
  76. encoding: 'utf-8',
  77. })
  78. fileList.push({
  79. name,
  80. content,
  81. hasCurrentColor: regCurrentColor.test(content),
  82. file: filePath,
  83. })
  84. }
  85. //
  86. else if (stat.isDirectory()) {
  87. loadSvgList(filePath)
  88. }
  89. }
  90. return fileList
  91. }
  92. return loadSvgList(svgPath).filter(item => !!item)
  93. })(svgPath)
  94. //
  95. const defaultColor = '#22ac38'
  96. let currentColor = svgLibCurrent.currentColor || ''
  97. let palette = []
  98. const generateIcon = svgRaw => {
  99. // svgo 会过滤纯黑, 此处对纯黑做简单处理
  100. svgRaw = svgRaw.replace(regCurrentColor, `$1${currentColor}`).replace(/#0{3,8}/g, '#ZZZZZZ')
  101. const result = optimize(svgRaw, {
  102. multipass: true,
  103. })
  104. result.data = result.data.replace(/#Z{3,8}/gi, '#000')
  105. const regColor = /(fill|stroke|stop-color):([^;}]+)/g
  106. const parseColor = colorStr => {
  107. if (!regRef.test(colorStr)) {
  108. return colorStr
  109. }
  110. // 从 Gradient 引用里获取颜色
  111. const match = colorStr.match(regRef)
  112. const ref = gradients.find(item => {
  113. return item.id === match[1]
  114. })
  115. return ref ? ref.colors : []
  116. }
  117. // Step 1, find all Gradient define and make KV map
  118. const regGradient = /<(\w+Gradient) id="([^"]+)" [^>]+>(.+?)<\/\1>/g
  119. const regStopColors = /stop-color="([^"]+)"/g
  120. const gradients = [...result.data.matchAll(regGradient)].map(item => {
  121. const colors = [...item[3].matchAll(regStopColors)].map(item => item[1])
  122. return {
  123. id: item[2],
  124. content: item[3],
  125. colors,
  126. }
  127. })
  128. // Step 2, find all class define and make KV map
  129. const regClass = /\.(cls-\d+)\{([^}]+)\}/g
  130. const regRef = /url\(#(.+)\)/
  131. const classes = [...result.data.matchAll(regClass)].map(item => {
  132. // Search colors from item[2]
  133. // find fill, stroke, stop-color
  134. const colors = [...item[2].matchAll(regColor)].map(item => parseColor(item[2]))
  135. return {
  136. id: item[1],
  137. content: item[2],
  138. colors: colors,
  139. }
  140. })
  141. // Step 3, find all style, class, stroke property and search color in value
  142. const regProps = /(fill|stroke|class|style)="([^"]+)"/g
  143. const props = [...result.data.matchAll(regProps)].map(content => {
  144. let colors = []
  145. if (content[1] === 'class') {
  146. const item = classes.find(item => item.id === content[2])
  147. colors = item ? item.colors : []
  148. } else if (content[1] === 'style') {
  149. colors = [...content[2].matchAll(regColor)].map(item => parseColor(item[2]))
  150. } else if (content[1] === 'fill') {
  151. colors = parseColor(content[2])
  152. } else {
  153. colors = content[2]
  154. }
  155. return {
  156. prop: content[1],
  157. content: content[2],
  158. // 定义里的颜色
  159. colors: colors,
  160. }
  161. })
  162. // Step 4, filter
  163. let colors = props
  164. .map(item => item.colors)
  165. .flat(2)
  166. .filter(item => item !== 'none' && !/^url/.test(item))
  167. .map(item => (item === 'currentColor' ? currentColor : item))
  168. colors = Array.from(new Set(colors))
  169. // Append new colors to palette
  170. palette = Array.from(new Set([...palette, ...colors]))
  171. // Build color index
  172. let colorMap = colors.map(c => palette.indexOf(c))
  173. const colorTotal = colors.length
  174. if (colorTotal === 0) {
  175. const fixable = /<(path|circle|ellipse|polygon|polyline|rect|use) /g
  176. if (fixable.test(result.data)) {
  177. return generateIcon(result.data.replace(fixable, `<$1 fill="${currentColor || defaultColor}" `))
  178. } else {
  179. // console.log(' SVG 图片没有配置颜色, 并且无法进行预处理。请联系作者修复此问题。https://ext.dcloud.net.cn/plugin?id=13964')
  180. }
  181. } else if (colorTotal > 0) {
  182. // console.log(' ', JSON.stringify(colors))
  183. }
  184. return [result.data, ...colorMap]
  185. }
  186. ;(async () => {
  187. // 检测是否存在 currentColor
  188. const hasCurrentColor = svgList.find(item => item.hasCurrentColor)
  189. if (!currentColor && hasCurrentColor) {
  190. // console.log('\n')
  191. // console.log('::>> 检测到 svg 文件中使用了 currentColor 变量,该变量在组件中不被支持。\n')
  192. currentColor = defaultColor
  193. // console.log(`::>> 需要指定一个颜色替代,默认黑色为(${currentColor})。\n`)
  194. do {
  195. const color = await cliInput(`请输入颜色,直接回车(enter)使用默认值:`)
  196. if (color && color.length && !regColorFormat.test(color)) {
  197. // console.log('\n::>> 颜色格式不正确,请输入以下格式的颜色值:\n')
  198. // console.log('::>>', ['#000', '#000000', 'rgb(0, 0, 0)', 'rgba(0, 0, 0, 1)'].join(' '), '\n')
  199. } else {
  200. currentColor = color && color.length ? color.replace(/ /g, '') : defaultColor
  201. }
  202. } while (!currentColor)
  203. }
  204. svgList.forEach(item => {
  205. // console.log(item.name)
  206. svgLib[item.name] = generateIcon(item.content)
  207. })
  208. const data = {
  209. icons: JSON.parse(JSON.stringify(svgLib)),
  210. currentColor,
  211. $_colorPalette: palette,
  212. }
  213. const hasChange = JSON.stringify(svgLibCurrent) !== JSON.stringify(data)
  214. if (hasChange) {
  215. const scriptTpl = fs.readFileSync(__dirname + '/svg-icons-lib.tpl.js', {
  216. encoding: 'utf-8',
  217. })
  218. const params = {
  219. datetime: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
  220. default: JSON.stringify(data, null, 2).split('\n').join('\n '),
  221. }
  222. const script = scriptTpl.replace(/__(\w+)__/g, (_, key) => {
  223. return params[key] || _
  224. })
  225. fs.writeFileSync(svgLibFile, script)
  226. // console.log(`\nTotal ${Object.keys(svgLib).length} svg icon(s) generated.`)
  227. } else {
  228. // console.log(`\nTotal ${Object.keys(svgLib).length} svg icon(s) generated, nochange.`)
  229. }
  230. if (hasCurrentColor) {
  231. // console.log('\n')
  232. // console.log(' 当前有使用到 currentColor 变量,可通过文件 static/svg-icons-lib.js 里的 currentColor 属性进行修改。')
  233. // console.log('\n')
  234. }
  235. })()