generate-svg-icon.js 8.2 KB

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