dsh-visualkit 0.0.0-stage → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +26 -0
  3. package/README.md +443 -2
  4. package/THIRD_PARTY.md +55 -0
  5. package/cordis.patch.yml +29 -0
  6. package/data/assets/README.md +38 -0
  7. package/data/assets/icons/brands/bilibili.svg +1 -0
  8. package/data/assets/icons/brands/discord.svg +1 -0
  9. package/data/assets/icons/brands/neteasecloudmusic.svg +1 -0
  10. package/data/assets/icons/brands/qq.svg +1 -0
  11. package/data/assets/icons/brands/sinaweibo.svg +1 -0
  12. package/data/assets/icons/brands/steam.svg +1 -0
  13. package/data/assets/icons/brands/telegram.svg +1 -0
  14. package/data/assets/icons/brands/tiktok.svg +1 -0
  15. package/data/assets/icons/brands/twitch.svg +1 -0
  16. package/data/assets/icons/brands/wechat.svg +1 -0
  17. package/data/assets/icons/brands/x.svg +1 -0
  18. package/data/assets/icons/brands/youtube.svg +1 -0
  19. package/data/assets/icons/lucide/bell.svg +1 -0
  20. package/data/assets/icons/lucide/calendar.svg +1 -0
  21. package/data/assets/icons/lucide/clock.svg +1 -0
  22. package/data/assets/icons/lucide/gamepad-2.svg +1 -0
  23. package/data/assets/icons/lucide/gift.svg +1 -0
  24. package/data/assets/icons/lucide/heart.svg +1 -0
  25. package/data/assets/icons/lucide/mic.svg +1 -0
  26. package/data/assets/icons/lucide/play.svg +1 -0
  27. package/data/assets/icons/lucide/star.svg +1 -0
  28. package/data/assets/icons/lucide/video.svg +1 -0
  29. package/data/assets/icons/phosphor/LICENSE +21 -0
  30. package/data/assets/icons/phosphor/atom-duotone.svg +1 -0
  31. package/data/assets/icons/phosphor/binary-duotone.svg +1 -0
  32. package/data/assets/icons/phosphor/blueprint-duotone.svg +1 -0
  33. package/data/assets/icons/phosphor/circuitry-duotone.svg +1 -0
  34. package/data/assets/icons/phosphor/cloud-arrow-up-duotone.svg +1 -0
  35. package/data/assets/icons/phosphor/code-duotone.svg +1 -0
  36. package/data/assets/icons/phosphor/compass-duotone.svg +1 -0
  37. package/data/assets/icons/phosphor/cpu-duotone.svg +1 -0
  38. package/data/assets/icons/phosphor/cube-duotone.svg +1 -0
  39. package/data/assets/icons/phosphor/database-duotone.svg +1 -0
  40. package/data/assets/icons/phosphor/flask-duotone.svg +1 -0
  41. package/data/assets/icons/phosphor/frame-corners-duotone.svg +1 -0
  42. package/data/assets/icons/phosphor/function-duotone.svg +1 -0
  43. package/data/assets/icons/phosphor/hard-drives-duotone.svg +1 -0
  44. package/data/assets/icons/phosphor/hexagon-duotone.svg +1 -0
  45. package/data/assets/icons/phosphor/infinity-duotone.svg +1 -0
  46. package/data/assets/icons/phosphor/lightning-duotone.svg +1 -0
  47. package/data/assets/icons/phosphor/magic-wand-duotone.svg +1 -0
  48. package/data/assets/icons/phosphor/moon-stars-duotone.svg +1 -0
  49. package/data/assets/icons/phosphor/paint-brush-duotone.svg +1 -0
  50. package/data/assets/icons/phosphor/pen-nib-duotone.svg +1 -0
  51. package/data/assets/icons/phosphor/planet-duotone.svg +1 -0
  52. package/data/assets/icons/phosphor/rocket-launch-duotone.svg +1 -0
  53. package/data/assets/icons/phosphor/ruler-duotone.svg +1 -0
  54. package/data/assets/icons/phosphor/selection-plus-duotone.svg +1 -0
  55. package/data/assets/icons/phosphor/shapes-duotone.svg +1 -0
  56. package/data/assets/icons/phosphor/sparkle-duotone.svg +1 -0
  57. package/data/assets/icons/phosphor/sun-horizon-duotone.svg +1 -0
  58. package/data/assets/icons/phosphor/terminal-window-duotone.svg +1 -0
  59. package/data/assets/icons/phosphor/triangle-duotone.svg +1 -0
  60. package/data/assets/icons/phosphor/waveform-duotone.svg +1 -0
  61. package/data/assets/icons/tabler/alarm.svg +1 -0
  62. package/data/assets/icons/tabler/badge-ad.svg +1 -0
  63. package/data/assets/icons/tabler/bell.svg +1 -0
  64. package/data/assets/icons/tabler/bolt.svg +1 -0
  65. package/data/assets/icons/tabler/brand-bilibili.svg +1 -0
  66. package/data/assets/icons/tabler/brand-discord.svg +1 -0
  67. package/data/assets/icons/tabler/brand-qq.svg +1 -0
  68. package/data/assets/icons/tabler/brand-twitch.svg +1 -0
  69. package/data/assets/icons/tabler/brand-wechat.svg +1 -0
  70. package/data/assets/icons/tabler/brand-weibo.svg +1 -0
  71. package/data/assets/icons/tabler/brand-youtube.svg +1 -0
  72. package/data/assets/icons/tabler/broadcast.svg +1 -0
  73. package/data/assets/icons/tabler/calendar-event.svg +1 -0
  74. package/data/assets/icons/tabler/calendar-week.svg +1 -0
  75. package/data/assets/icons/tabler/clock-hour-3.svg +1 -0
  76. package/data/assets/icons/tabler/coin.svg +1 -0
  77. package/data/assets/icons/tabler/crown.svg +1 -0
  78. package/data/assets/icons/tabler/currency-yen.svg +1 -0
  79. package/data/assets/icons/tabler/device-gamepad-2.svg +1 -0
  80. package/data/assets/icons/tabler/diamond.svg +1 -0
  81. package/data/assets/icons/tabler/flame.svg +1 -0
  82. package/data/assets/icons/tabler/gift.svg +1 -0
  83. package/data/assets/icons/tabler/headphones.svg +1 -0
  84. package/data/assets/icons/tabler/heart-filled.svg +1 -0
  85. package/data/assets/icons/tabler/heart.svg +1 -0
  86. package/data/assets/icons/tabler/live-view.svg +1 -0
  87. package/data/assets/icons/tabler/microphone.svg +1 -0
  88. package/data/assets/icons/tabler/mood-smile.svg +1 -0
  89. package/data/assets/icons/tabler/mood-tongue.svg +1 -0
  90. package/data/assets/icons/tabler/movie.svg +1 -0
  91. package/data/assets/icons/tabler/player-play.svg +1 -0
  92. package/data/assets/icons/tabler/playlist.svg +1 -0
  93. package/data/assets/icons/tabler/rocket.svg +1 -0
  94. package/data/assets/icons/tabler/sparkles.svg +1 -0
  95. package/data/assets/icons/tabler/star-filled.svg +1 -0
  96. package/data/assets/icons/tabler/star.svg +1 -0
  97. package/data/assets/icons/tabler/tag.svg +1 -0
  98. package/data/assets/icons/tabler/thumb-up.svg +1 -0
  99. package/data/assets/icons/tabler/ticket.svg +1 -0
  100. package/data/assets/icons/tabler/video.svg +1 -0
  101. package/data/assets/illustrations/openpeeps/LICENSE +8 -0
  102. package/data/assets/illustrations/openpeeps/peep-10.svg +29 -0
  103. package/data/assets/illustrations/openpeeps/peep-100.svg +32 -0
  104. package/data/assets/illustrations/openpeeps/peep-101.svg +25 -0
  105. package/data/assets/illustrations/openpeeps/peep-102.svg +28 -0
  106. package/data/assets/illustrations/openpeeps/peep-103.svg +32 -0
  107. package/data/assets/illustrations/openpeeps/peep-105.svg +25 -0
  108. package/data/assets/illustrations/openpeeps/peep-12.svg +29 -0
  109. package/data/assets/illustrations/openpeeps/peep-13.svg +27 -0
  110. package/data/assets/illustrations/openpeeps/peep-16.svg +27 -0
  111. package/data/assets/illustrations/openpeeps/peep-17.svg +31 -0
  112. package/data/assets/illustrations/openpeeps/peep-22.svg +29 -0
  113. package/data/assets/illustrations/openpeeps/peep-25.svg +29 -0
  114. package/data/assets/illustrations/openpeeps/peep-26.svg +29 -0
  115. package/data/assets/illustrations/openpeeps/peep-27.svg +29 -0
  116. package/data/assets/illustrations/openpeeps/peep-30.svg +29 -0
  117. package/data/assets/illustrations/openpeeps/peep-31.svg +27 -0
  118. package/data/assets/illustrations/openpeeps/peep-34.svg +29 -0
  119. package/data/assets/illustrations/openpeeps/peep-38.svg +29 -0
  120. package/data/assets/illustrations/openpeeps/peep-39.svg +31 -0
  121. package/data/assets/illustrations/openpeeps/peep-4.svg +29 -0
  122. package/data/assets/illustrations/openpeeps/peep-41.svg +29 -0
  123. package/data/assets/illustrations/openpeeps/peep-44.svg +29 -0
  124. package/data/assets/illustrations/openpeeps/peep-47.svg +26 -0
  125. package/data/assets/illustrations/openpeeps/peep-48.svg +29 -0
  126. package/data/assets/illustrations/openpeeps/peep-50.svg +31 -0
  127. package/data/assets/illustrations/openpeeps/peep-51.svg +29 -0
  128. package/data/assets/illustrations/openpeeps/peep-53.svg +27 -0
  129. package/data/assets/illustrations/openpeeps/peep-54.svg +26 -0
  130. package/data/assets/illustrations/openpeeps/peep-55.svg +29 -0
  131. package/data/assets/illustrations/openpeeps/peep-56.svg +27 -0
  132. package/data/assets/illustrations/openpeeps/peep-58.svg +29 -0
  133. package/data/assets/illustrations/openpeeps/peep-61.svg +27 -0
  134. package/data/assets/illustrations/openpeeps/peep-64.svg +27 -0
  135. package/data/assets/illustrations/openpeeps/peep-68.svg +27 -0
  136. package/data/assets/illustrations/openpeeps/peep-69.svg +27 -0
  137. package/data/assets/illustrations/openpeeps/peep-7.svg +31 -0
  138. package/data/assets/illustrations/openpeeps/peep-72.svg +29 -0
  139. package/data/assets/illustrations/openpeeps/peep-76.svg +31 -0
  140. package/data/assets/illustrations/openpeeps/peep-77.svg +27 -0
  141. package/data/assets/illustrations/openpeeps/peep-80.svg +27 -0
  142. package/data/assets/illustrations/openpeeps/peep-81.svg +29 -0
  143. package/data/assets/illustrations/openpeeps/peep-83.svg +29 -0
  144. package/data/assets/manifest.json +1 -0
  145. package/data/assets/parts//342/206/223/346/212/225/345/275/261.png +0 -0
  146. package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2711.png +0 -0
  147. package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2712.png +0 -0
  148. package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2713.png +0 -0
  149. package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2714.png +0 -0
  150. package/data/assets/parts//345/217/214/351/251/254/345/260/276.png +0 -0
  151. package/data/assets/parts//345/217/214/351/251/254/345/260/2761.png +0 -0
  152. package/data/assets/parts//345/217/2211.png +0 -0
  153. package/data/assets/parts//345/217/2212.png +0 -0
  154. package/data/assets/parts//345/217/2213.png +0 -0
  155. package/data/assets/parts//345/217/221/345/234/210.png +0 -0
  156. package/data/assets/parts//345/217/221/345/244/2711.png +0 -0
  157. package/data/assets/parts//345/217/221/345/244/2712.png +0 -0
  158. package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/211/215.png +0 -0
  159. package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/220/216.png +0 -0
  160. package/data/assets/parts//345/217/263/344/270/212/347/234/274/347/235/221.png +0 -0
  161. package/data/assets/parts//345/217/263/346/211/213.png +0 -0
  162. package/data/assets/parts//345/217/263/346/211/2131.png +0 -0
  163. package/data/assets/parts//345/217/263/347/234/274/347/220/203.png +0 -0
  164. package/data/assets/parts//345/217/263/347/234/274/347/231/275.png +0 -0
  165. package/data/assets/parts//345/217/263/350/205/277.png +0 -0
  166. package/data/assets/parts//345/221/206/346/257/233.png +0 -0
  167. package/data/assets/parts//345/230/264/345/267/264.png +0 -0
  168. package/data/assets/parts//345/260/276/345/267/264.png +0 -0
  169. package/data/assets/parts//345/267/246/344/270/212/347/234/274/347/235/221.png +0 -0
  170. package/data/assets/parts//345/267/246/345/217/214/347/234/274/347/232/256.png +0 -0
  171. package/data/assets/parts//345/267/246/346/211/213.png +0 -0
  172. package/data/assets/parts//345/267/246/346/211/2131.png +0 -0
  173. package/data/assets/parts//345/267/246/347/234/274/347/220/203.png +0 -0
  174. package/data/assets/parts//345/267/246/347/234/274/347/231/275.png +0 -0
  175. package/data/assets/parts//345/267/246/350/205/277.png +0 -0
  176. package/data/assets/parts//346/212/225/345/275/261.png +0 -0
  177. package/data/assets/parts//347/234/211/346/257/233.png +0 -0
  178. package/data/assets/parts//347/234/274/347/275/251.png +0 -0
  179. package/data/assets/parts//350/200/263/346/234/2651.png +0 -0
  180. package/data/assets/parts//350/200/263/346/234/2652.png +0 -0
  181. package/data/assets/parts//350/204/221/345/243/263.png +0 -0
  182. package/data/assets/parts//350/241/243/346/234/215/345/211/215.png +0 -0
  183. package/data/assets/parts//350/241/243/346/234/215/345/220/216.png +0 -0
  184. package/data/assets/parts//350/241/243/346/234/215/351/273/221.png +0 -0
  185. package/data/assets/parts//350/241/243/346/234/215/351/273/2211.png +0 -0
  186. package/data/assets/parts//350/272/253/344/275/223.png +0 -0
  187. package/data/assets/parts//351/223/201/346/235/277.png +0 -0
  188. package/data/assets/parts//351/273/221/347/234/274/345/234/210.png +0 -0
  189. package/data/assets/parts//351/273/221/347/234/274/345/234/2102.png +0 -0
  190. package/data/assets/patterns/checker.svg +1 -0
  191. package/data/assets/patterns/circles-overlap.svg +1 -0
  192. package/data/assets/patterns/crosshatch.svg +1 -0
  193. package/data/assets/patterns/diagonal.svg +1 -0
  194. package/data/assets/patterns/dots.svg +1 -0
  195. package/data/assets/patterns/grid.svg +1 -0
  196. package/data/assets/patterns/halftone.svg +1 -0
  197. package/data/assets/patterns/plus.svg +1 -0
  198. package/data/assets/patterns/scales.svg +1 -0
  199. package/data/assets/patterns/triangles.svg +1 -0
  200. package/data/assets/patterns/waves.svg +1 -0
  201. package/data/assets/patterns/zigzag.svg +1 -0
  202. package/data/assets/stickers/openmoji/1F375.svg +12 -0
  203. package/data/assets/stickers/openmoji/1F9E8.svg +21 -0
  204. package/data/assets/stickers/twemoji/1f389.svg +1 -0
  205. package/data/assets/stickers/twemoji/1f3a4.svg +1 -0
  206. package/data/assets/stickers/twemoji/1f3a7.svg +1 -0
  207. package/data/assets/stickers/twemoji/1f3ae.svg +1 -0
  208. package/data/assets/stickers/twemoji/1f44b.svg +1 -0
  209. package/data/assets/stickers/twemoji/1f44d.svg +1 -0
  210. package/data/assets/stickers/twemoji/1f494.svg +1 -0
  211. package/data/assets/stickers/twemoji/1f4a2.svg +1 -0
  212. package/data/assets/stickers/twemoji/1f4a8.svg +1 -0
  213. package/data/assets/stickers/twemoji/1f4b5.svg +1 -0
  214. package/data/assets/stickers/twemoji/1f4d6.svg +1 -0
  215. package/data/assets/stickers/twemoji/1f525.svg +1 -0
  216. package/data/assets/stickers/twemoji/1f600.svg +1 -0
  217. package/data/assets/stickers/twemoji/1f602.svg +1 -0
  218. package/data/assets/stickers/twemoji/1f60d.svg +1 -0
  219. package/data/assets/stickers/twemoji/1f62d.svg +1 -0
  220. package/data/assets/stickers/twemoji/1f680.svg +1 -0
  221. package/data/assets/stickers/twemoji/1f979.svg +1 -0
  222. package/data/assets/stickers/twemoji/1f97a.svg +1 -0
  223. package/data/assets/stickers/twemoji/26a1.svg +1 -0
  224. package/data/assets/stickers/twemoji/2705.svg +1 -0
  225. package/data/assets/stickers/twemoji/274c.svg +1 -0
  226. package/data/assets/stickers/twemoji/2764.svg +1 -0
  227. package/data/assets/stickers/twemoji/2b50.svg +1 -0
  228. package/data/assets/stickers/twemoji_1f525.svg +1 -0
  229. package/data/assets/stickers/twemoji_1f600.svg +1 -0
  230. package/data/assets/stickers/twemoji_1f979.svg +1 -0
  231. package/data/assets/stickers/twemoji_2764.svg +1 -0
  232. package/data/assets/textures/grain-dark.png +0 -0
  233. package/data/assets/textures/grain-light.png +0 -0
  234. package/data/assets/textures/grain-warm.png +0 -0
  235. package/data/lucide-icons.json +1 -0
  236. package/data/packs/tabler.icons.json +1 -0
  237. package/icon.svg +26 -0
  238. package/lib/assets.js +228 -0
  239. package/lib/audit.js +284 -0
  240. package/lib/contrast-render.js +213 -0
  241. package/lib/css-rewrite.js +379 -0
  242. package/lib/detect.js +701 -0
  243. package/lib/diff.js +153 -0
  244. package/lib/emit.js +557 -0
  245. package/lib/html-context.js +315 -0
  246. package/lib/icon-render.js +107 -0
  247. package/lib/icondb.js +402 -0
  248. package/lib/image-normalize.js +140 -0
  249. package/lib/index.js +1289 -0
  250. package/lib/layouts/common.js +48 -0
  251. package/lib/layouts/image.js +1010 -0
  252. package/lib/layouts/index.js +10 -0
  253. package/lib/layouts/intent.js +66 -0
  254. package/lib/layouts/metrics-text.js +91 -0
  255. package/lib/layouts/png.js +75 -0
  256. package/lib/layouts/rules-contrast.js +575 -0
  257. package/lib/layouts/rules-punct.js +486 -0
  258. package/lib/layouts/rules-structure.js +182 -0
  259. package/lib/layouts/source.js +1961 -0
  260. package/lib/layouts/svg.js +1204 -0
  261. package/lib/morphcore.js +139 -0
  262. package/lib/preview.js +225 -0
  263. package/lib/render-html.js +255 -0
  264. package/lib/render.js +486 -0
  265. package/lib/rules.js +267 -0
  266. package/lib/shadow.js +83 -0
  267. package/lib/skip.js +56 -0
  268. package/lib/style.js +688 -0
  269. package/lib/svg.js +729 -0
  270. package/lib/taste-image.js +323 -0
  271. package/lib/taste.js +219 -0
  272. package/locale/en.json +6 -0
  273. package/locale/zh.json +6 -0
  274. package/package.json +92 -4
  275. package/vendor/morphicons-adapters.iife.js +903 -0
  276. package/vendor/morphicons.iife.js +2441 -0
  277. package/vendor/resvg/LICENSE +373 -0
  278. package/vendor/resvg/index.d.ts +98 -0
  279. package/vendor/resvg/index.js +23 -0
  280. package/vendor/resvg/js-binding.d.ts +52 -0
  281. package/vendor/resvg/js-binding.js +1 -0
  282. package/vendor/resvg/package.json +138 -0
  283. package/vendor/resvg/resvgjs.win32-x64-msvc.node +0 -0
package/lib/emit.js ADDED
@@ -0,0 +1,557 @@
1
+ /**
2
+ * emit.js — 同一图标按各项目约定出码(icon action=emit 的实现核心)。
3
+ *
4
+ * 四种 target 全部对齐现有代码的真实约定:
5
+ * colibri js/ui.js ICONS 字典一行条目(camelCase 键 + 内联串)
6
+ * echo import { X } from "lucide" + <MorphIcon> 片段(STYLE_GUIDE §3)
7
+ * dsh-plugin 插件包 icon.svg(package.json "icon" 字段用)
8
+ * whale JS 单引号字符串(innerHTML 直塞)
9
+ * raw 完整独立 <svg> 标记
10
+ */
11
+ import { getIcon } from "./icondb.js";
12
+ import { checkIcon, iconToInnerSvg, renderComposed, renderSvg, renderSvgChip, renderSvgGradient, renderSvgLayers } from "./svg.js";
13
+ import { CAP_TO_JOIN, PRESETS, clampDim, parseGradientStops } from "./icon-render.js";
14
+ import { parseShadows } from "./shadow.js";
15
+ import { normalizePathData, normalizeShape, pathNode } from "./svg.js";
16
+
17
+ const TARGETS = ["colibri", "echo", "dsh-plugin", "whale", "raw"];
18
+
19
+ // 审查修复 #282:自由文本参数会拼进生成代码(keyName 进 JS 字典键位、
20
+ // color/className 进 SVG 属性且产物直塞 innerHTML)——白名单校验挡住
21
+ // 引号/尖括号/等号/反斜杠的注入面(含属性越界与字符串字面量损坏)
22
+ const KEY_NAME_RE = /^[A-Za-z_$][\w$]*$/; // JS 标识符(ICONS 键位)
23
+ const COLOR_RE = /^[\w#(),.%+\- ]+$/; // #hex / rgb() / 命名色 / oklch() 等颜色写法
24
+ const CLASS_RE = /^[\w\-.: ]+$/; // class 属性(含 BEM/Tailwind 常见字符)
25
+ const SPRINGS = ["snappy", "smooth", "bouncy"];
26
+ // 尺寸/描边取值域:先前直接拼进 SVG 属性,`size: 0/-5/NaN` 会产出 width="0"/"-5"/"NaN"
27
+ // 这种坏 SVG(渲染空白或直接报错),必须在出码前拦下。
28
+ const SIZE_MIN = 4;
29
+ const SIZE_MAX = 4096;
30
+ const STROKE_MAX = 64;
31
+
32
+ /**
33
+ * codereview #481:三个走 renderSvg 的 target 原先各自手写同一套 size/strokeWidth/color
34
+ * 默认值合并(仅 size 缺省不同:whale 16、其余 24),strokeWidth=2/currentColor 还与
35
+ * renderSvg 内部默认重复、双方调整会漂移——统一在这里维护。未显式给的键不挂
36
+ * (undefined 回退交给 renderSvg 内部的 ?? 处理);className 仅 raw target 消费。
37
+ */
38
+ function renderOpts(opts, defaultSize, withClassName = false) {
39
+ const out = { size: opts.size ?? defaultSize };
40
+ if (opts.strokeWidth !== undefined) out.strokeWidth = opts.strokeWidth;
41
+ if (opts.color !== undefined) out.color = opts.color;
42
+ if (withClassName && opts.className !== undefined) out.className = opts.className;
43
+ return out;
44
+ }
45
+
46
+ /** 包数据里的 fill 风格图标(品牌/实心):stroke 型 target 渲染会走样,出码时附提示。 */
47
+ function fillDrawn(node) {
48
+ return node.some(([, attrs]) => typeof attrs?.fill === "string" && attrs.fill !== "none");
49
+ }
50
+ function fillNote(icon, target) {
51
+ if (!fillDrawn(icon.node)) return null;
52
+ return target === "raw" || target === "dsh-plugin"
53
+ ? `该图标为 fill 风格(来自 ${icon.pack} 包),已保留内部 fill 属性;如需纯 stroke 请换 lucide 图标`
54
+ : `该图标为 fill 风格(来自 ${icon.pack} 包),${target} 走 stroke 渲染管线可能走样;品牌/实心图标建议改用 raw target`;
55
+ }
56
+
57
+ /**
58
+ * 出码样式(九种视觉语言,前四种是字重/填充变体,后五种是不同设计语言):
59
+ * outline 2px 线性(Feather/Lucide 基线,默认)
60
+ * hairline 1px 细线(高端编辑/奢侈品观感,小尺寸慎用)
61
+ * bold 2.8px 粗线(强识别,16px 仍清楚)
62
+ * sharp butt/miter 尖角(Material sharp 方向)
63
+ * solid 填充版(tabler -filled,16px 下是最清楚的)
64
+ * duotone 填充 20% + 线稿(Phosphor duotone 观感)
65
+ * badge 圆角方块底色 + 反白图标(SaaS 功能入口)
66
+ * knockout 正圆底色 + 反白图标(app icon 语言)
67
+ * gradient 渐变描边(营销/展示位)
68
+ */
69
+ const STYLES = ["outline", "hairline", "bold", "sharp", "solid", "duotone", "badge", "knockout", "gradient"];
70
+ /** 字体细线与粗线的固定字重(比让模型猜 1 还是 2.8 可靠)。 */
71
+ const WEIGHT_OF_STYLE = { hairline: 1, bold: 2.8, badge: 1.8, knockout: 1.8, gradient: 2.2 };
72
+
73
+ /** 同包内的填充版/线性版配对。tabler 有 1088 个 −filled,其中 1057 个有线性双胞胎;
74
+ * lucide 没有 −filled。duotone 就靠这对双胞胎合成,不需要额外的 duotone 数据集。 */
75
+ function twinOf(icon, suffix, { crossPack = false } = {}) {
76
+ // 按「主名 + 别名」找配对:lucide 的 home 是别名→house,而 tabler 里同名填充版叫 home-filled,
77
+ // 只查主名会漏。solid 允许跨包(整个图标被替换,不会混搭);duotone 必须同包(两层要叠在一起)。
78
+ const bases = suffix ? [icon.name, ...(icon.aliases ?? [])] : [icon.name.replace(/-filled$/, "")];
79
+ for (const base of bases) {
80
+ const wanted = `${base}${suffix}`;
81
+ if (wanted === "" || wanted === icon.name) continue;
82
+ const same = getIcon(`${icon.pack}:${wanted}`);
83
+ if (same !== undefined && same.pack === icon.pack) return same;
84
+ if (crossPack) {
85
+ const any = getIcon(wanted);
86
+ if (any !== undefined) return any;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * 把「样式意图」解成两层图标 + 端点设置。
94
+ * @returns {{primary:object, secondary?:object, linecap:string, linejoin:string, style:string, notes:string[]}}
95
+ */
96
+ /** 找 duotone 配对(线性 + 填充,必须同一包才能叠):先同包,找不到再按名字/别名
97
+ * 在其它包里找一对(如 lucide 的 heart 无填充版,但 tabler 的 heart/heart-filled 是完整一对)。 */
98
+ function duotonePair(icon) {
99
+ const bases = [icon.name, ...(icon.aliases ?? [])];
100
+ if (icon.name.endsWith("-filled")) {
101
+ const outline = twinOf(icon, "");
102
+ if (outline !== undefined) return { outline, filled: icon };
103
+ }
104
+ // 同包同名配对(最常见:tabler 的 heart + heart-filled)
105
+ const sameFilled = twinOf(icon, "-filled");
106
+ if (sameFilled !== undefined) return { outline: icon, filled: sameFilled };
107
+ // 同包「别名」配对(名字不同但指同一概念)
108
+ for (const base of bases) {
109
+ if (base === icon.name) continue;
110
+ const viaAlias = twinOf({ ...icon, name: base }, "-filled");
111
+ if (viaAlias === undefined) continue;
112
+ const aliasOutline = getIcon(`${icon.pack}:${base}`);
113
+ return { outline: aliasOutline ?? icon, filled: viaAlias };
114
+ }
115
+ for (const base of bases) {
116
+ const filled = getIcon(`${base}-filled`);
117
+ if (filled === undefined) continue;
118
+ const outline = twinOf(filled, "");
119
+ if (outline !== undefined) return { outline, filled };
120
+ }
121
+ return undefined;
122
+ }
123
+
124
+ /**
125
+ * 把「预设 + 显式维度覆盖」合成为一组轴。
126
+ *
127
+ * 设计要点:style 只是**默认起点的快捷命名**,下面每一个维度都可以单独覆盖——
128
+ * 所以「圆角方容器 + both 绘法 + 平头线端 + 径向渐变 + 32° 角度」这种组合能表达,
129
+ * 而不是只能在 9 个互斥配方里选一个。
130
+ * 传入的维度值优先于预设;传入 none/null 可以主动关掉预设的某一维。
131
+ */
132
+ function mergeAxes(style, opts = {}) {
133
+ const preset = PRESETS[style] ?? PRESETS.outline;
134
+ const axes = {
135
+ paint: preset.paint ?? "stroke",
136
+ weight: preset.weight,
137
+ caps: preset.caps ?? "round",
138
+ join: preset.caps === "butt" || preset.caps === "square" ? "miter" : "round",
139
+ container:
140
+ preset.container === undefined
141
+ ? null
142
+ : { kind: preset.container, radius: preset.radius, inset: preset.inset },
143
+ gradient: preset.gradient === undefined ? null : { kind: preset.gradient, angle: preset.gradientAngle ?? 45 },
144
+ duo: preset.duo === undefined ? null : { opacity: preset.duo }
145
+ };
146
+
147
+ // —— 显式覆盖:逐维 ——
148
+ if (opts.paint !== undefined) axes.paint = opts.paint;
149
+ if (opts.caps !== undefined) {
150
+ axes.caps = opts.caps;
151
+ axes.join = CAP_TO_JOIN[opts.caps]?.[1] ?? axes.join;
152
+ }
153
+ if (opts.join !== undefined) axes.join = opts.join;
154
+
155
+ // 容器:none 主动去掉;给了具体形状则新建/改写
156
+ if (opts.container !== undefined) {
157
+ if (opts.container === "none") axes.container = null;
158
+ else axes.container = { ...(axes.container ?? {}), kind: opts.container };
159
+ }
160
+ if (axes.container !== null) {
161
+ const r = clampDim("radius", opts.radius);
162
+ const i = clampDim("inset", opts.inset);
163
+ if (r !== undefined) axes.container.radius = r;
164
+ if (i !== undefined) axes.container.inset = i;
165
+ }
166
+
167
+ // 渐变:none 主动去掉;给了类型则新建
168
+ if (opts.gradient !== undefined) {
169
+ if (opts.gradient === "none") axes.gradient = null;
170
+ else axes.gradient = { ...(axes.gradient ?? {}), kind: opts.gradient };
171
+ }
172
+ if (axes.gradient !== null) {
173
+ const ang = clampDim("gradientAngle", opts.gradientAngle);
174
+ if (ang !== undefined) axes.gradient.angle = ang;
175
+ }
176
+
177
+ // 双色:duoOpacity > 0 就启用(即使预设不是 duotone)
178
+ if (opts.duoOpacity !== undefined) {
179
+ const o = clampDim("duo", opts.duoOpacity);
180
+ if (o !== undefined) axes.duo = o > 0 ? { opacity: o } : null;
181
+ }
182
+
183
+ const w = clampDim("weight", opts.strokeWidth);
184
+ if (w !== undefined) axes.weight = w;
185
+ return axes;
186
+ }
187
+
188
+ /** 需要填充孪生体的场景:fill 绘法、容器型、双色底图。 */
189
+ function needsFillTwin(axes) {
190
+ return axes.paint === "fill" || axes.paint === "both" || axes.container !== null || axes.duo !== null;
191
+ }
192
+
193
+ function resolveStyle(icon, style, opts = {}) {
194
+ const axes = mergeAxes(style, opts);
195
+ const notes = [];
196
+ let primary = icon;
197
+ let secondary;
198
+
199
+ // 容器型:字形优先用填充版(容器内小尺寸下描边容易糊成一片)。
200
+ // 但如果容器是**全透明**(借它的缩放/留白但不画底色),就不该换填充版:
201
+ // 没底色衬托,换掉只会把细线风格变成实心块(实测:简约风格被改成黑齿轮)。
202
+ const containerInvisible =
203
+ axes.container !== null && /^(transparent|rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?(\.0*)?\s*\))$/i.test(String(opts.color ?? ""));
204
+ // 传了 dash 就是“我要描边虚线”,也不该换成填充字形(换了就没描边可虚线)。
205
+ const wantsStroke = Array.isArray(opts.dash) && opts.dash.length > 0;
206
+ // 自绘路径不进库内替换逻辑:用户自己画的形状,paint 直接生效就是对的,
207
+ // 去库里找「填充版」既找不到(实测会抡错),也违背自绘的本意。
208
+ const isCustom = icon.custom === true;
209
+ if (!isCustom && axes.container !== null && !containerInvisible && !wantsStroke && !fillDrawn(icon.node)) {
210
+ const glyph = twinOf(icon, "-filled", { crossPack: true });
211
+ if (glyph !== undefined) {
212
+ primary = glyph;
213
+ notes.push(`容器型字形用填充版 ${glyph.name}(小尺寸更清)`);
214
+ }
215
+ }
216
+
217
+ // 双色:需要线性 + 填充 一对(自绘路径没有配对,直接跳过)
218
+ if (!isCustom && axes.duo !== null) {
219
+ const pair = duotonePair(primary);
220
+ if (pair === undefined) {
221
+ throw new Error(`「${primary.name}」找不到线性+填充配对,不能做双色:用 icon action=find query="填充的${primary.name}" 选带 filled 的图标,或去掉 duoOpacity`);
222
+ }
223
+ primary = pair.outline;
224
+ secondary = pair.filled;
225
+ }
226
+
227
+ // 纯填充(且无双色):需要填充节点(自绘路径本身就是可填充形状,跳过)
228
+ if (!isCustom && axes.paint === "fill" && axes.duo === null && !fillDrawn(primary.node)) {
229
+ const filled = twinOf(primary, "-filled", { crossPack: true });
230
+ if (filled === undefined) {
231
+ throw new Error(`「${primary.name}」没有填充版:用 icon action=find query="填充的${primary.name}" 直接选 filled 图标,或改 paint=stroke`);
232
+ }
233
+ primary = filled;
234
+ notes.push(`fill 绘法已改用填充版 ${filled.name}${filled.pack !== icon.pack ? `(来自 ${filled.pack} 包)` : ""}`);
235
+ }
236
+
237
+ // 兼容字段:colibri/echo 等 emitter 靠它们判断「本 target 能不能表达这个外观」
238
+ // 容器 + 渐变时字形固定白(否则 color2 双重语义会让字形不可见):说出口,
239
+ // 不要让人以为 color2 还能控字形色。
240
+ if (axes.container !== null && axes.gradient !== null) {
241
+ notes.push("容器 + 渐变:渐变画在容器底上,字形固定白色(color2 只控渐变末色)——iOS/Win11 图标的典型做法");
242
+ }
243
+ // strokeWidth 对填充字形不生效(描填充字形只会在外圈糊一层):明确说出来,
244
+ // 不能像以前那样静默忽略——AI 会以为自己调了粗细而实际什么都没变。
245
+ if (
246
+ opts.strokeWidth !== undefined &&
247
+ fillDrawn(primary.node) &&
248
+ axes.paint === "stroke"
249
+ ) {
250
+ notes.push(
251
+ `该图标用填充字形,strokeWidth=${opts.strokeWidth} 不生效(描填充字形会在外圈糊一层);要描边请换线性图标,或传 paint=both 显式两层`
252
+ );
253
+ }
254
+
255
+ return {
256
+ primary,
257
+ secondary,
258
+ style,
259
+ axes,
260
+ // wrap 保留「容器类型名」作为对外回填值(chip 形状:rounded/circle/squircle)
261
+ ...(axes.container !== null ? { wrap: { ...axes.container, kind: axes.container.kind } } : {}),
262
+ ...(axes.gradient !== null ? { gradient: true } : {}),
263
+ notes
264
+ };
265
+ }
266
+
267
+ /**
268
+ * 按轴渲染完整 SVG(raw / dsh-plugin / whale 共用)。
269
+ * 不再按 style 分派,而是把 spec.axes 交给 renderComposed 合成——
270
+ * 这样任意维度组合(容器 + both + 径向渐变 + 平头)都能表达。
271
+ */
272
+ function styledSvg(spec, opts, defaultSize, withClassName = false) {
273
+ const ro = renderOpts(opts, defaultSize, withClassName);
274
+ const a = spec.axes;
275
+ const baseColor = spec.wrap !== undefined ? (opts.color ?? "#141414") : (opts.color ?? (spec.gradient ? "#E4002B" : "currentColor"));
276
+ // 阴影是**图元**(偏移+模糊+颜色),不是风格:给任意轮廓加上它,
277
+ // 高程体系/卡片/浮层/按奥/浮雕/双光源都能自己拼。
278
+ const shadows = parseShadows(opts.shadow).list;
279
+ const dash = Array.isArray(opts.dash) && opts.dash.length > 0 ? opts.dash : undefined;
280
+ // 多形状组合(对象数组):{ name, scale?, dx?, dy?, rotate?, color?, weight?, subtract? }
281
+ // 这是通用图元:环/中空/徽章+图形/多层插画全靠它拼,不内置任何具体形状。
282
+ const gradientStops = parseGradientStops(opts.stops);
283
+ const shapes = (Array.isArray(opts.shapes) ? opts.shapes : [])
284
+ .map((raw) => {
285
+ const spec = normalizeShape(raw);
286
+ if (spec === null) return null;
287
+ // 自绘图层不需要查库
288
+ if (typeof spec.path === "string") return { ...spec, node: pathNode(spec.path) };
289
+ const entry = getIcon(spec.name);
290
+ return entry === undefined ? null : { ...spec, node: entry.node };
291
+ })
292
+ .filter((s) => s !== null);
293
+ const svg = renderComposed({
294
+ node: spec.primary.node,
295
+ paint: a.paint,
296
+ strokeWidth: a.weight ?? (spec.wrap !== undefined ? 1.8 : 2),
297
+ caps: a.caps,
298
+ join: a.join,
299
+ size: ro.size,
300
+ color: baseColor,
301
+ className: ro.className,
302
+ ...(shadows.length > 0 ? { shadows } : {}),
303
+ ...(dash !== undefined ? { dash } : {}),
304
+ ...(shapes.length > 0 ? { shapes } : {}),
305
+ // 顶层 fill-rule:让自绘路径自己就能画环(多子路径 + evenodd)
306
+ ...(opts.fillRule === "evenodd" || opts.fillRule === "nonzero" ? { fillRule: opts.fillRule } : {}),
307
+ // 图标内文字:给了就取代字形(字母标/字母徽章)
308
+ ...(typeof opts.text === "string" && opts.text.trim() !== ""
309
+ ? { text: { content: opts.text.trim(), ...(Number.isFinite(opts.textSize) && opts.textSize > 0 ? { size: opts.textSize } : {}) } }
310
+ : {}),
311
+ // **color2 的语义冲突(实测 bug)**:容器 + 渐变同时存在时,
312
+ // color2 既是「渐变末色」又被拿去当字形色,结果字形跟容器同色系、几乎看不见
313
+ // (iOS / Win11 / 拟物图标全中招)。现实里这两类图标的字形几乎总是白的,
314
+ // 所以:有渐变时字形固定白(color2 专心控渐变);没渐变时 color2 才是字形色。
315
+ ...(spec.wrap !== undefined
316
+ ? {
317
+ container: {
318
+ ...spec.wrap,
319
+ color: baseColor,
320
+ glyphColor: spec.gradient === true ? "#FFFFFF" : (opts.color2 ?? "#FFFFFF")
321
+ }
322
+ }
323
+ : {}),
324
+ // 传了 stops 却没传 gradient → 默认当线性渐变。
325
+ // 否则 stops 会被静默丢弃(实测:容器渲染成一片黑,而调用方以为自己给了渐变)——
326
+ // “看起来能用的参数实际零效果”是最坑的一类。
327
+ ...((spec.gradient !== undefined && a.gradient !== null) || gradientStops !== null
328
+ ? {
329
+ gradient: {
330
+ kind: spec.gradient !== undefined && a.gradient !== null ? a.gradient.kind : "linear",
331
+ angle: spec.gradient !== undefined && a.gradient !== null ? a.gradient.angle : (Number.isFinite(opts.gradientAngle) ? opts.gradientAngle : 45),
332
+ ...(gradientStops !== null ? { stops: gradientStops } : { color: opts.color ?? "#E4002B", ...(opts.color2 !== undefined ? { color2: opts.color2 } : {}) })
333
+ }
334
+ }
335
+ : {}),
336
+ ...(spec.secondary !== undefined && a.duo !== null ? { duo: { node: spec.secondary.node, opacity: a.duo.opacity } } : {})
337
+ });
338
+ // 非 hex 颜色算不出末色:回退实色绘法,而不是默默产出坏渐变
339
+ if (svg !== null) return svg;
340
+ return (
341
+ renderComposed({
342
+ node: spec.primary.node,
343
+ paint: a.paint,
344
+ strokeWidth: a.weight ?? 2,
345
+ caps: a.caps,
346
+ join: a.join,
347
+ size: ro.size,
348
+ color: baseColor,
349
+ className: ro.className,
350
+ ...(dash !== undefined ? { dash } : {}),
351
+ ...(spec.secondary !== undefined && a.duo !== null ? { duo: { node: spec.secondary.node, opacity: a.duo.opacity } } : {})
352
+ }) ?? renderSvg(spec.primary.node, { ...ro, strokeWidth: a.weight ?? 2, linecap: a.caps, linejoin: a.join })
353
+ );
354
+ }
355
+
356
+ function emitColibri(spec, opts) {
357
+ const icon = spec.primary;
358
+ const camel = opts.keyName ?? icon.camel;
359
+ // 容器型/渐变型在 colibri 上不可表达:ICONS 字典的 inner 只有 currentColor 一色
360
+ if (spec.wrap !== undefined || spec.gradient === true) {
361
+ return {
362
+ code: `${camel}: '${iconToInnerSvg(icon.node)}',`,
363
+ filename: null,
364
+ notes: [
365
+ `${spec.style} 在 colibri 上不可表达(ICONS 字典的 inner 只能单色 currentColor),已退化为 outline`,
366
+ `粘贴到项目 js/ui.js 的 ICONS 字典;渲染走 UI.svg('${camel}')`
367
+ ]
368
+ };
369
+ }
370
+ // duotone 的填充层不能用 <g>(Colibri parseIconNodes 不认分组),下推到元素属性
371
+ const inner = spec.secondary !== undefined
372
+ ? iconToInnerSvg(spec.secondary.node, { fill: "currentColor", "fill-opacity": "0.2", stroke: "none" }) + iconToInnerSvg(icon.node)
373
+ : iconToInnerSvg(
374
+ icon.node,
375
+ spec.axes.caps === "round" ? undefined : { "stroke-linecap": spec.axes.caps, "stroke-linejoin": spec.axes.join }
376
+ );
377
+ const check = checkIcon(icon.node);
378
+ const notes = [
379
+ `粘贴到项目 js/ui.js 的 ICONS 字典;渲染走 UI.svg('${camel}')`,
380
+ check.ok ? "已通过 morph 兼容体检,可直接用于 morphTo" : "注意:未通过 morph 兼容体检(见 morph action=check 结果)"
381
+ ];
382
+ return { code: `${camel}: '${inner}',`, filename: null, notes };
383
+ }
384
+
385
+ /** echo 是 Vue 项目形态:不同图标集的 import 源不同。
386
+ * 旧实现硬编码 lucide,给 tabler 图标(如 heart-filled)生成 `import … from "lucide"` 直接报错。 */
387
+ const ECHO_IMPORT_SOURCE = { lucide: "lucide", tabler: "@tabler/icons-vue" };
388
+
389
+ /** 按图标集分组生成 import 行(同名去重;跨包时输出多行)。 */
390
+ function echoImportLines(icons) {
391
+ const bySource = new Map();
392
+ for (const icon of icons) {
393
+ const source = ECHO_IMPORT_SOURCE[icon.pack] ?? icon.pack;
394
+ const names = bySource.get(source) ?? [];
395
+ if (!names.includes(icon.pascal)) names.push(icon.pascal);
396
+ bySource.set(source, names);
397
+ }
398
+ return [...bySource].map(([source, names]) => `import { ${names.join(", ")} } from "${source}";`);
399
+ }
400
+
401
+ function emitEcho(spec, opts) {
402
+ const icon = spec.primary;
403
+ const lines = [];
404
+ const notes = ["组件来自 morphicons/vue;两态切换才用 MorphIcon,静态图标用 vicons(emoji 全面禁用)"];
405
+ // MorphIcon 只能挂一个组件、且不暴露 linecap:双层/尖角/容器样式在 echo 上不可表达,退化为 outline
406
+ if (spec.secondary !== undefined) notes.push("echo/MorphIcon 不支持双层 duotone,已退化为 outline;需要双色请用 target=raw/dsh-plugin");
407
+ if (spec.wrap !== undefined) notes.push(`echo/MorphIcon 不支持 ${spec.style} 容器样式(底色需另一元素),已退化为 outline;需要容器请用 target=raw/dsh-plugin`);
408
+ if (spec.gradient === true) notes.push("echo/MorphIcon 不支持渐变描边,已退化为 outline;需要渐变请用 target=raw");
409
+ if (spec.axes.caps !== "round") notes.push(`${spec.style}(caps=${spec.axes.caps}):echo/MorphIcon 不暴露 linecap/linejoin,在 echo 上不生效;需要非圆角线端请用 target=raw`);
410
+ const usedIcons = [icon];
411
+ if (icon.pack !== "lucide") {
412
+ notes.push(`这是 ${icon.pack} 图标:import 源是 ${ECHO_IMPORT_SOURCE[icon.pack] ?? icon.pack}(Echo 项目约定只用 lucide,接线前确认依赖已装)`);
413
+ }
414
+ lines.push(echoImportLines(usedIcons).join("\n") + (icon.pack === "lucide" && !opts.partner ? " // 仅作数据源(STYLE_GUIDE §3:lucide 只为 morphicons 供数)" : ""));
415
+ if (opts.partner) {
416
+ const partner = getIcon(opts.partner);
417
+ // 审查修复 #281:伙伴解析到同一主名(别名撞车,如 home/house 同指 house)
418
+ // 会生成 import { House, House } 重复绑定(ES 语法错误)且两态切换无意义
419
+ // ——退回单图标片段,并把降级原因写进 notes(原先只有代码内 HTML 注释,
420
+ // 调用方无从得知两态片段未生成)
421
+ if (partner !== undefined && partner.pascal !== icon.pascal) {
422
+ usedIcons.push(partner);
423
+ lines[0] = echoImportLines(usedIcons).join("\n");
424
+ if (partner.pack !== "lucide") {
425
+ notes.push(`伙伴图标也来自 ${partner.pack}:import 源 ${ECHO_IMPORT_SOURCE[partner.pack] ?? partner.pack}`);
426
+ }
427
+ if (partner.pack !== icon.pack) {
428
+ notes.push(`两态图标来自不同图标集(${icon.pack} ↔ ${partner.pack}):几何基准可能不同,接线前先 morph action=check 对比`);
429
+ }
430
+ lines.push(`<MorphIcon :icon="state ? ${partner.pascal} : ${icon.pascal}" spring="${opts.spring ?? "snappy"}" />`);
431
+ lines.push(`<!-- 两态切换:${icon.name} ↔ ${partner.name};snappy=状态反馈 / smooth=稳重过渡 -->`);
432
+ } else {
433
+ const reason = partner === undefined ? `"${opts.partner}" 不在 lucide 库中` : `"${opts.partner}" 与 ${icon.name} 解析为同一图标(${icon.pascal}),两态切换无意义`;
434
+ lines.push(`<!-- 伙伴图标 ${reason},仅生成单图标片段 -->`);
435
+ lines.push(`<MorphIcon :icon="${icon.pascal}" />`);
436
+ notes.push(`两态片段未生成:${reason};如需两态请换一个不同的伙伴图标`);
437
+ }
438
+ } else {
439
+ lines.push(`<MorphIcon :icon="${icon.pascal}" />`);
440
+ lines.push(`<!-- 注意:静态功能图标不得用 MorphIcon 装载,走 n-icon + @vicons(三层分工) -->`);
441
+ }
442
+ return { code: lines.join("\n"), filename: null, notes };
443
+ }
444
+
445
+ function emitDshPlugin(spec, opts) {
446
+ const color = opts.color ?? "currentColor";
447
+ const svg = styledSvg(spec, opts, 24);
448
+ return {
449
+ code: svg,
450
+ filename: "icon.svg",
451
+ notes: [
452
+ '保存为插件目录内的 icon.svg,并在 package.json 声明 "icon": "./icon.svg"',
453
+ color === "currentColor" ? "currentColor 需内联渲染才生效;若作为独立图片引用请用 color 参数指定具体颜色" : null
454
+ ].filter(Boolean)
455
+ };
456
+ }
457
+
458
+ function emitWhale(spec, opts) {
459
+ const svg = styledSvg(spec, opts, 16);
460
+ // 审查修复 #282:先转义反斜杠再转义单引号——原顺序下内容含 \ 时,
461
+ // \' 会被解析成「转义后的引号」吞掉闭合引号,字符串字面量直接损坏
462
+ const oneLine = "'" + svg.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'";
463
+ return { code: `el.innerHTML = ${oneLine};`, filename: null, notes: ["单行 JS 字符串,直接塞 innerHTML;emoji 图标请全部替换为本方式"] };
464
+ }
465
+
466
+ function emitRaw(spec, opts) {
467
+ const icon = spec.primary;
468
+ const svg = styledSvg(spec, opts, 24, true);
469
+ // codereview #482:outPath 以分隔符结尾(如 "icons/" 或 "./")时 basename 为空串——
470
+ // 原样返回 filename:'' 会误导调用方;回退为 <icon.name>.svg(与「落盘到该目录」的
471
+ // 意图一致)。outPath 未给时维持 null(与原先一致,由调用方决定命名)。
472
+ const basename = typeof opts.outPath === "string" ? opts.outPath.split(/[\\/]/).pop() : null;
473
+ return {
474
+ code: svg,
475
+ filename: basename ? basename : (opts.outPath !== undefined ? `${icon.name}.svg` : null),
476
+ notes: [
477
+ fillDrawn(icon.node)
478
+ ? "fill 风格图标:内部 path 自带 fill,根壳用 color 属性承接 color 参数(不再叠加描边)"
479
+ : "24×24 stroke 风格,fill=none + currentColor;如需填色风格自行调整属性"
480
+ ]
481
+ };
482
+ }
483
+
484
+ /** 出码入口:输入 lucide 名(或已解析条目),返回 { target, code, filename, notes, icon }。 */
485
+ function emit(nameOrIcon, target, opts = {}) {
486
+ if (!TARGETS.includes(target)) throw new Error(`unknown emit target "${target}"(可用:${TARGETS.join(", ")})`);
487
+ // 审查修复 #282:自由文本参数注入面校验(keyName/color/className 白名单,
488
+ // 非法值响亮失败——与 unknown target 同一错误风格)
489
+ if (opts.keyName !== undefined && !KEY_NAME_RE.test(opts.keyName)) {
490
+ throw new Error(`keyName 必须是合法 JS 标识符(收到 "${opts.keyName}")——它会被拼进 ICONS 字典键位`);
491
+ }
492
+ if (opts.color !== undefined && !COLOR_RE.test(opts.color)) {
493
+ throw new Error(`color 含不允许的字符(收到 "${opts.color}")——仅接受 #hex / rgb() / 命名色等颜色写法(白名单:字母数字与 # ( ) , . % + - 空格)`);
494
+ }
495
+ if (opts.className !== undefined && !CLASS_RE.test(opts.className)) {
496
+ throw new Error(`className 含不允许的字符(收到 "${opts.className}")——class 属性白名单:字母数字与 - _ . : 空格`);
497
+ }
498
+ if (opts.size !== undefined) {
499
+ if (typeof opts.size !== "number" || !Number.isFinite(opts.size) || opts.size < SIZE_MIN || opts.size > SIZE_MAX) {
500
+ throw new Error(`size 必须是 ${SIZE_MIN}–${SIZE_MAX} 的数字(收到 ${JSON.stringify(opts.size)})——它会被拼进 svg 的宽高属性`);
501
+ }
502
+ }
503
+ if (opts.strokeWidth !== undefined) {
504
+ if (typeof opts.strokeWidth !== "number" || !Number.isFinite(opts.strokeWidth) || opts.strokeWidth < 0 || opts.strokeWidth > STROKE_MAX) {
505
+ throw new Error(`strokeWidth 必须是 0–${STROKE_MAX} 的数字(收到 ${JSON.stringify(opts.strokeWidth)})`);
506
+ }
507
+ }
508
+ if (opts.spring !== undefined && !SPRINGS.includes(opts.spring)) {
509
+ throw new Error(`unknown spring "${opts.spring}"(可用:${SPRINGS.join(", ")})`);
510
+ }
511
+ if (opts.color2 !== undefined && !COLOR_RE.test(opts.color2)) {
512
+ throw new Error(`color2 含不允许的字符(收到 "${opts.color2}")——仅接受 #hex / rgb() / 命名色等颜色写法`);
513
+ }
514
+ // codereview #483:空值防护同时拦 undefined 与 null——原先只查 undefined,
515
+ // null 条目会漏到 emitter 内部访问 icon.node 时抛裸 TypeError(无法定位);
516
+ // 非字符串入参为空值时也不再误报成 unknown lucide icon "null/undefined"。
517
+ // 图标内文字场景下 name 可以不传:文字会取代字形,但内部仍需一个占位图标
518
+ // (container / 阴影轮廓 / 元信息都依赖它)。用 circle 作占位,它不会被绘制。
519
+ const hasText = typeof opts.text === "string" && opts.text.trim() !== "";
520
+ // 自绘:给了 path 就直接用路径当字形,不需要图标名。
521
+ // 包成与库图标同构的节点,所以容器/阴影/渐变/遮罩/虚线/旋转全部照旧生效。
522
+ const customPath = normalizePathData(opts.path);
523
+ const icon = customPath !== null ? { name: "(custom path)", node: pathNode(customPath), custom: true } : typeof nameOrIcon === "string" && nameOrIcon.trim() !== "" ? getIcon(nameOrIcon) : hasText ? getIcon("circle") : nameOrIcon;
524
+ if (icon === undefined || icon === null) {
525
+ throw new Error(
526
+ typeof nameOrIcon === "string"
527
+ ? `unknown lucide icon "${nameOrIcon}"(lucide/tabler 是通用图标集,不含品牌 logo;品牌请用 design_render action=asset mode=find 检索内置素材库)`
528
+ : "emit 需要图标名(string)或已解析条目(icon 对象),收到空值"
529
+ );
530
+ }
531
+ const style = opts.style ?? "outline";
532
+ if (!STYLES.includes(style)) throw new Error(`unknown style "${style}"(style 只是预设起点,可用:${STYLES.join(", ")};要自定义请用 paint/caps/container/radius/inset/gradient/gradientAngle 这些维度参数)`);
533
+ const spec = resolveStyle(icon, style, opts);
534
+ const emitter = { colibri: emitColibri, echo: emitEcho, "dsh-plugin": emitDshPlugin, whale: emitWhale, raw: emitRaw }[target];
535
+ const out = emitter(spec, opts);
536
+ // 填色提醒只在 outline 有意义;solid/duotone 本来就是要填的
537
+ const note = style === "outline" ? fillNote(spec.primary, target) : null;
538
+ return {
539
+ target,
540
+ style,
541
+ ...(spec.wrap !== undefined ? { container: spec.wrap.kind } : {}),
542
+ // 只写**真的有值**的字段:自绘路径没有 pascal/camel/pack,
543
+ // 直接写 spec.primary.pascal 会得到 undefined 属性质,输出就不是无损 JSON 了
544
+ // (实测实机报 invalid output: value is not lossless JSON)。
545
+ icon: {
546
+ name: spec.primary.name,
547
+ ...(spec.primary.pascal !== undefined ? { pascal: spec.primary.pascal } : {}),
548
+ ...(spec.primary.camel !== undefined ? { camel: spec.primary.camel } : {}),
549
+ nodeCount: spec.primary.node.length,
550
+ ...(spec.primary.pack !== undefined && spec.primary.pack !== "lucide" ? { pack: spec.primary.pack } : {})
551
+ },
552
+ ...out,
553
+ notes: [...(note === null ? [] : [note]), ...spec.notes, ...(out.notes ?? [])]
554
+ };
555
+ }
556
+
557
+ export { STYLES, TARGETS, emit };