@akairos/pipeline 0.18.1 → 0.18.5

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 (212) hide show
  1. package/ARTICLE-THEMES.md +2 -0
  2. package/README.md +5 -3
  3. package/package.json +5 -4
  4. package/src/article-theme.test.ts +55 -0
  5. package/src/article-theme.ts +10 -2
  6. package/src/commands/article-theme.ts +1 -1
  7. package/src/commands/init.ts +2 -0
  8. package/src/commands/publish.ts +1 -1
  9. package/src/commands/typeset-assets-sync.test.ts +9 -1
  10. package/src/commands/wechat-export.ts +9 -4
  11. package/src/providers/index.ts +4 -2
  12. package/src/runtime-package.ts +4 -3
  13. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1280 -5
  14. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  15. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  16. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  17. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  18. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  19. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  20. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  21. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  22. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  23. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  24. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  25. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  26. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  27. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  28. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  29. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  30. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  31. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  32. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  33. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  34. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  35. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  36. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  37. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  38. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  39. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  40. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  41. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  42. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  43. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  44. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  45. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  46. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  47. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  48. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  49. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  50. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  51. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  52. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  53. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  54. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  55. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  56. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  57. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  58. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  59. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  60. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  61. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  62. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  63. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  64. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  65. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  66. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  67. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  68. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  69. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  70. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  71. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  72. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  73. package/src/wechat-preview/assets/browser-dist/assets/apl-B4CMkyY2.js +1 -0
  74. package/src/wechat-preview/assets/browser-dist/assets/asciiarmor-Df11BRmG.js +1 -0
  75. package/src/wechat-preview/assets/browser-dist/assets/asn1-EdZsLKOL.js +1 -0
  76. package/src/wechat-preview/assets/browser-dist/assets/asterisk-B-8jnY81.js +1 -0
  77. package/src/wechat-preview/assets/browser-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
  78. package/src/wechat-preview/assets/browser-dist/assets/clike-B9uivgTg.js +1 -0
  79. package/src/wechat-preview/assets/browser-dist/assets/clojure-BMjYHr_A.js +1 -0
  80. package/src/wechat-preview/assets/browser-dist/assets/cmake-BQqOBYOt.js +1 -0
  81. package/src/wechat-preview/assets/browser-dist/assets/cobol-CWcv1MsR.js +1 -0
  82. package/src/wechat-preview/assets/browser-dist/assets/coffeescript-S37ZYGWr.js +1 -0
  83. package/src/wechat-preview/assets/browser-dist/assets/commonlisp-DBKNyK5s.js +1 -0
  84. package/src/wechat-preview/assets/browser-dist/assets/crystal-SjHAIU92.js +1 -0
  85. package/src/wechat-preview/assets/browser-dist/assets/css-BnMrqG3P.js +1 -0
  86. package/src/wechat-preview/assets/browser-dist/assets/cypher-C_CwsFkJ.js +1 -0
  87. package/src/wechat-preview/assets/browser-dist/assets/d-pRatUO7H.js +1 -0
  88. package/src/wechat-preview/assets/browser-dist/assets/diff-DbItnlRl.js +1 -0
  89. package/src/wechat-preview/assets/browser-dist/assets/dockerfile-BKs6k2Af.js +1 -0
  90. package/src/wechat-preview/assets/browser-dist/assets/dtd-DF_7sFjM.js +1 -0
  91. package/src/wechat-preview/assets/browser-dist/assets/dylan-DwRh75JA.js +1 -0
  92. package/src/wechat-preview/assets/browser-dist/assets/ebnf-CDyGwa7X.js +1 -0
  93. package/src/wechat-preview/assets/browser-dist/assets/ecl-Cabwm37j.js +1 -0
  94. package/src/wechat-preview/assets/browser-dist/assets/eiffel-CnydiIhH.js +1 -0
  95. package/src/wechat-preview/assets/browser-dist/assets/elm-vLlmbW-K.js +1 -0
  96. package/src/wechat-preview/assets/browser-dist/assets/erlang-BNw1qcRV.js +1 -0
  97. package/src/wechat-preview/assets/browser-dist/assets/factor-kuTfRLto.js +1 -0
  98. package/src/wechat-preview/assets/browser-dist/assets/fcl-Kvtd6kyn.js +1 -0
  99. package/src/wechat-preview/assets/browser-dist/assets/forth-Ffai-XNe.js +1 -0
  100. package/src/wechat-preview/assets/browser-dist/assets/fortran-DYz_wnZ1.js +1 -0
  101. package/src/wechat-preview/assets/browser-dist/assets/gas-Bneqetm1.js +1 -0
  102. package/src/wechat-preview/assets/browser-dist/assets/gherkin-heZmZLOM.js +1 -0
  103. package/src/wechat-preview/assets/browser-dist/assets/groovy-D9Dt4D0W.js +1 -0
  104. package/src/wechat-preview/assets/browser-dist/assets/haskell-Cw1EW3IL.js +1 -0
  105. package/src/wechat-preview/assets/browser-dist/assets/haxe-H-WmDvRZ.js +1 -0
  106. package/src/wechat-preview/assets/browser-dist/assets/http-DBlCnlav.js +1 -0
  107. package/src/wechat-preview/assets/browser-dist/assets/idl-BEugSyMb.js +1 -0
  108. package/src/wechat-preview/assets/browser-dist/assets/index-B13lCSuc.js +1 -0
  109. package/src/wechat-preview/assets/browser-dist/assets/index-BDdU9pNh.js +1 -0
  110. package/src/wechat-preview/assets/browser-dist/assets/index-BFLGVgsp.js +1 -0
  111. package/src/wechat-preview/assets/browser-dist/assets/index-BHJY0B4E.js +8 -0
  112. package/src/wechat-preview/assets/browser-dist/assets/index-BOnKEJxh.js +7 -0
  113. package/src/wechat-preview/assets/browser-dist/assets/index-BPmBcGI-.js +1 -0
  114. package/src/wechat-preview/assets/browser-dist/assets/index-BTVIa90Y.js +1 -0
  115. package/src/wechat-preview/assets/browser-dist/assets/index-BYV6TA8f.js +1 -0
  116. package/src/wechat-preview/assets/browser-dist/assets/index-BlVpb0dF.js +1 -0
  117. package/src/wechat-preview/assets/browser-dist/assets/index-BqKARoyW.js +1 -0
  118. package/src/wechat-preview/assets/browser-dist/assets/index-BzHql5Rp.js +20 -0
  119. package/src/wechat-preview/assets/browser-dist/assets/index-CPFEJ9fX.js +1 -0
  120. package/src/wechat-preview/assets/browser-dist/assets/index-CdxKyTX6.js +1 -0
  121. package/src/wechat-preview/assets/browser-dist/assets/index-CyXDvcwY.js +1 -0
  122. package/src/wechat-preview/assets/browser-dist/assets/index-DA7Z4paG.js +13 -0
  123. package/src/wechat-preview/assets/browser-dist/assets/index-DDQGWU_D.js +3 -0
  124. package/src/wechat-preview/assets/browser-dist/assets/index-DENMoE5l.js +1 -0
  125. package/src/wechat-preview/assets/browser-dist/assets/index-D_EI5vFS.js +1 -0
  126. package/src/wechat-preview/assets/browser-dist/assets/index-DpuVrwze.js +1 -0
  127. package/src/wechat-preview/assets/browser-dist/assets/index-IBlhKHQr.js +8 -0
  128. package/src/wechat-preview/assets/browser-dist/assets/index-ifI5svkD.js +2 -0
  129. package/src/wechat-preview/assets/browser-dist/assets/index-vRiJxvj2.js +1 -0
  130. package/src/wechat-preview/assets/browser-dist/assets/javascript-iXu5QeM3.js +1 -0
  131. package/src/wechat-preview/assets/browser-dist/assets/julia-DuME0IfC.js +1 -0
  132. package/src/wechat-preview/assets/browser-dist/assets/livescript-BwQOo05w.js +1 -0
  133. package/src/wechat-preview/assets/browser-dist/assets/lua-VAEuO923.js +1 -0
  134. package/src/wechat-preview/assets/browser-dist/assets/mathematica-DTrFuWx2.js +1 -0
  135. package/src/wechat-preview/assets/browser-dist/assets/mbox-CNhZ1qSd.js +1 -0
  136. package/src/wechat-preview/assets/browser-dist/assets/mirc-CjQqDB4T.js +1 -0
  137. package/src/wechat-preview/assets/browser-dist/assets/mllike-CXdrOF99.js +1 -0
  138. package/src/wechat-preview/assets/browser-dist/assets/modelica-Dc1JOy9r.js +1 -0
  139. package/src/wechat-preview/assets/browser-dist/assets/mscgen-BA5vi2Kp.js +1 -0
  140. package/src/wechat-preview/assets/browser-dist/assets/mumps-BT43cFF4.js +1 -0
  141. package/src/wechat-preview/assets/browser-dist/assets/nginx-DdIZxoE0.js +1 -0
  142. package/src/wechat-preview/assets/browser-dist/assets/nsis-LY0VT_u5.js +1 -0
  143. package/src/wechat-preview/assets/browser-dist/assets/ntriples-BfvgReVJ.js +1 -0
  144. package/src/wechat-preview/assets/browser-dist/assets/octave-Ck1zUtKM.js +1 -0
  145. package/src/wechat-preview/assets/browser-dist/assets/oz-BzwKVEFT.js +1 -0
  146. package/src/wechat-preview/assets/browser-dist/assets/pascal--L3eBynH.js +1 -0
  147. package/src/wechat-preview/assets/browser-dist/assets/perl-CdXCOZ3F.js +1 -0
  148. package/src/wechat-preview/assets/browser-dist/assets/pig-CevX1Tat.js +1 -0
  149. package/src/wechat-preview/assets/browser-dist/assets/powershell-CFHJl5sT.js +1 -0
  150. package/src/wechat-preview/assets/browser-dist/assets/properties-C78fOPTZ.js +1 -0
  151. package/src/wechat-preview/assets/browser-dist/assets/protobuf-ChK-085T.js +1 -0
  152. package/src/wechat-preview/assets/browser-dist/assets/pug-DeIclll2.js +1 -0
  153. package/src/wechat-preview/assets/browser-dist/assets/puppet-DMA9R1ak.js +1 -0
  154. package/src/wechat-preview/assets/browser-dist/assets/python-BuPzkPfP.js +1 -0
  155. package/src/wechat-preview/assets/browser-dist/assets/q-pXgVlZs6.js +1 -0
  156. package/src/wechat-preview/assets/browser-dist/assets/r-B6wPVr8A.js +1 -0
  157. package/src/wechat-preview/assets/browser-dist/assets/rpm-CTu-6PCP.js +1 -0
  158. package/src/wechat-preview/assets/browser-dist/assets/ruby-BK5aLJev.js +1 -0
  159. package/src/wechat-preview/assets/browser-dist/assets/sas-B4kiWyti.js +1 -0
  160. package/src/wechat-preview/assets/browser-dist/assets/scheme-C41bIUwD.js +1 -0
  161. package/src/wechat-preview/assets/browser-dist/assets/shell-CjFT_Tl9.js +1 -0
  162. package/src/wechat-preview/assets/browser-dist/assets/sieve-C3Gn_uJK.js +1 -0
  163. package/src/wechat-preview/assets/browser-dist/assets/simple-mode-GW_nhZxv.js +1 -0
  164. package/src/wechat-preview/assets/browser-dist/assets/smalltalk-CnHTOXQT.js +1 -0
  165. package/src/wechat-preview/assets/browser-dist/assets/solr-DehyRSwq.js +1 -0
  166. package/src/wechat-preview/assets/browser-dist/assets/sparql-DkYu6x3z.js +1 -0
  167. package/src/wechat-preview/assets/browser-dist/assets/spreadsheet-BCZA_wO0.js +1 -0
  168. package/src/wechat-preview/assets/browser-dist/assets/sql-D0XecflT.js +1 -0
  169. package/src/wechat-preview/assets/browser-dist/assets/stex-C3f8Ysf7.js +1 -0
  170. package/src/wechat-preview/assets/browser-dist/assets/style-Cif_gYUW.css +1 -0
  171. package/src/wechat-preview/assets/browser-dist/assets/stylus-B533Al4x.js +1 -0
  172. package/src/wechat-preview/assets/browser-dist/assets/swift-BzpIVaGY.js +1 -0
  173. package/src/wechat-preview/assets/browser-dist/assets/tcl-DVfN8rqt.js +1 -0
  174. package/src/wechat-preview/assets/browser-dist/assets/textile-CnDTJFAw.js +1 -0
  175. package/src/wechat-preview/assets/browser-dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  176. package/src/wechat-preview/assets/browser-dist/assets/tiki-DGYXhP31.js +1 -0
  177. package/src/wechat-preview/assets/browser-dist/assets/toml-Bm5Em-hy.js +1 -0
  178. package/src/wechat-preview/assets/browser-dist/assets/troff-wAsdV37c.js +1 -0
  179. package/src/wechat-preview/assets/browser-dist/assets/ttcn-CfJYG6tj.js +1 -0
  180. package/src/wechat-preview/assets/browser-dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  181. package/src/wechat-preview/assets/browser-dist/assets/turtle-B1tBg_DP.js +1 -0
  182. package/src/wechat-preview/assets/browser-dist/assets/vb-CmGdzxic.js +1 -0
  183. package/src/wechat-preview/assets/browser-dist/assets/vbscript-BuJXcnF6.js +1 -0
  184. package/src/wechat-preview/assets/browser-dist/assets/velocity-D8B20fx6.js +1 -0
  185. package/src/wechat-preview/assets/browser-dist/assets/verilog-C6RDOZhf.js +1 -0
  186. package/src/wechat-preview/assets/browser-dist/assets/vhdl-lSbBsy5d.js +1 -0
  187. package/src/wechat-preview/assets/browser-dist/assets/webidl-ZXfAyPTL.js +1 -0
  188. package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-GgNrlSLi.js → wechat-renderer-CcHuq7M-.js} +83 -61
  189. package/src/wechat-preview/assets/browser-dist/assets/xquery-DzFWVndE.js +1 -0
  190. package/src/wechat-preview/assets/browser-dist/assets/yacas-BJ4BC0dw.js +1 -0
  191. package/src/wechat-preview/assets/browser-dist/assets/z80-Hz9HOZM7.js +1 -0
  192. package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
  193. package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +215 -165
  194. package/src/wechat-preview/assets/browser-dist/studio.js +1148 -237
  195. package/src/wechat-preview/browser/studio/color-palettes.ts +28 -0
  196. package/src/wechat-preview/browser/studio/config-manager.ts +49 -0
  197. package/src/wechat-preview/browser/studio/milkdown-article-menu.ts +251 -0
  198. package/src/wechat-preview/browser/studio/myno-editor.ts +122 -0
  199. package/src/wechat-preview/browser/studio/studio.css +493 -250
  200. package/src/wechat-preview/browser/studio/studio.html +213 -163
  201. package/src/wechat-preview/browser/studio/studio.ts +684 -211
  202. package/src/wechat-preview/browser/studio/style-packs.ts +201 -0
  203. package/src/wechat-preview/browser/studio/syntax-presets.test.ts +10 -32
  204. package/src/wechat-preview/browser/studio/syntax-presets.ts +19 -357
  205. package/src/wechat-preview/fixtures/all-nodes.md +2 -11
  206. package/src/wechat-preview/index.ts +29 -13
  207. package/src/wechat-preview/server/http.ts +8 -7
  208. package/src/wechat-preview/server/studio.test.ts +25 -0
  209. package/src/wechat-preview/wechat-export.integration.test.ts +49 -8
  210. package/src/wechat-preview/wechat-renderer.ts +134 -11
  211. package/src/workflow.test.ts +2 -0
  212. package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +0 -1
@@ -26,7 +26,7 @@ import {
26
26
  rewriteHtmlLocalAssets,
27
27
  } from "./local-file";
28
28
  import { renderDashboardHtml, renderFailedEntryHtml } from "./ui";
29
- import { buildWechatPreviewShell, resolveWechatArticleTheme } from "../index";
29
+ import { buildWechatPreviewShell, resolveWechatArticleTheme, rewriteRelativeImagePaths } from "../index";
30
30
  import { getPluginDocs } from "@akairos/milkdown-article-plugins";
31
31
  import { readArticleTheme } from "../../article-theme";
32
32
  import { PipelineConfigSchema, WechatExportThemeOverridesSchema, WechatStructureOverridesSchema } from "../../schema/config";
@@ -210,8 +210,10 @@ export async function startPreviewServer(
210
210
  const accountsData: Record<string, unknown> = {};
211
211
 
212
212
  for (const [key, account] of Object.entries(config.wx.accounts)) {
213
+ const articleThemePath = resolveConfigRelativePath(account.articleTheme, configPath);
214
+ const articleTheme = articleThemePath ? await readArticleTheme(articleThemePath) : null;
213
215
  let customCssContent = "";
214
- if (account.customCss) {
216
+ if (account.customCss && articleTheme?.manifest.css === undefined) {
215
217
  const resolved = resolveConfigRelativePath(account.customCss, configPath);
216
218
  if (resolved) {
217
219
  try {
@@ -222,9 +224,7 @@ export async function startPreviewServer(
222
224
  }
223
225
  }
224
226
 
225
- const articleThemePath = resolveConfigRelativePath(account.articleTheme, configPath);
226
- const articleTheme = articleThemePath ? await readArticleTheme(articleThemePath) : null;
227
- const { theme, structure } = resolveWechatArticleTheme(key, account.theme, articleTheme);
227
+ const { theme, structure, syntaxPresets } = resolveWechatArticleTheme(key, account.theme, articleTheme);
228
228
 
229
229
  accountsData[key] = {
230
230
  name: account.name || key,
@@ -233,11 +233,12 @@ export async function startPreviewServer(
233
233
  theme: {
234
234
  editorVars: theme.editorVars,
235
235
  exportTheme: theme.exportTheme,
236
- syntaxPresets: account.theme?.syntaxPresets || {},
236
+ syntaxPresets: syntaxPresets || {},
237
237
  structure: structure || {},
238
238
  },
239
239
  articleThemeCss: articleTheme?.css || "",
240
240
  articleTheme: account.articleTheme ?? null,
241
+ articleThemeStandalone: articleTheme?.manifest.css !== undefined,
241
242
  };
242
243
  }
243
244
 
@@ -245,7 +246,7 @@ export async function startPreviewServer(
245
246
  const fixturePath = join(WECHAT_PREVIEW_DIR, "fixtures/all-nodes.md");
246
247
  if (existsSync(fixturePath)) {
247
248
  try {
248
- sampleMarkdown = readFileSync(fixturePath, "utf-8");
249
+ sampleMarkdown = rewriteRelativeImagePaths(readFileSync(fixturePath, "utf-8"), dirname(fixturePath));
249
250
  } catch {
250
251
  // ignore
251
252
  }
@@ -160,11 +160,36 @@ describe("wechat-preview studio server APIs", () => {
160
160
  }
161
161
  });
162
162
 
163
+ test("loads standalone defaults without depending on account CSS or styles", async () => {
164
+ const path = join(testDir, "config", "studio-style.json");
165
+ writeFileSync(path, JSON.stringify({ schemaVersion: 1, engineVersion: 1, id: "style", name: "Style", version: "1.0.0",
166
+ css: "p { color: #123456; }", exportTheme: { bodyFontSize: "18px" }, syntaxPresets: { h2: "contrast-green" } }));
167
+ const config = loadConfig();
168
+ const original = { ...config.wx.accounts.default };
169
+ config.wx.accounts.default.articleTheme = "studio-style.json";
170
+ config.wx.accounts.default.customCss = "missing.css";
171
+ saveConfig(config);
172
+ try {
173
+ const response = await fetch(`${server.url}/api/studio/config`);
174
+ expect(response.status).toBe(200);
175
+ const data = await response.json() as any;
176
+ expect(data.accounts.default.theme.exportTheme.bodyFontSize).toBe("18px");
177
+ expect(data.accounts.default.theme.syntaxPresets.h2).toBe("contrast-green");
178
+ expect(data.accounts.default.articleThemeCss).toContain("#123456");
179
+ expect(data.accounts.default.customCssContent).toBe("");
180
+ expect(data.accounts.default.articleThemeStandalone).toBe(true);
181
+ } finally {
182
+ config.wx.accounts.default = original;
183
+ saveConfig(config);
184
+ }
185
+ });
186
+
163
187
  test("GET /studio serves the Studio HTML dashboard", async () => {
164
188
  const res = await fetch(`${server.url}/studio`);
165
189
  expect(res.status).toBe(200);
166
190
  const html = await res.text();
167
191
  expect(html).toContain("WeChat Visual Studio");
168
192
  expect(html).toContain("studio.js");
193
+ expect(html).toContain('id="btn-export-config"');
169
194
  });
170
195
  });
@@ -44,10 +44,27 @@ describe("semantic WeChat HTML export", () => {
44
44
  expect(html).toContain("Chapter</span>");
45
45
  expect(html).toContain("01</span>");
46
46
  expect(html).toContain("Citation</p>");
47
+ // 公众号编辑器解析不了 currentcolor/多值 border 简写,导出必须展开为可见边单值属性
48
+ expect(html).not.toContain("currentcolor");
49
+ // 引用导出为 section(规避微信编辑器对原生 blockquote 的强制灰色左边框),样式全靠内联
50
+ expect(html).not.toContain("<blockquote");
51
+ expect(html).toContain("border-left: 3px solid");
47
52
  expect(html).toContain("Advice</strong>");
48
53
  expect(html).toMatch(/background-color: (?:#1f7a4d|rgb\(31, 122, 77\))/);
49
54
  }, 30_000);
50
55
 
56
+ test("h2 pillar preset exports border-left on the heading section", async () => {
57
+ tempDir = await mkdtemp(join(tmpdir(), "zzhub-wechat-pillar-"));
58
+ const markdownPath = join(tempDir, "post.md");
59
+ await writeFile(markdownPath, "## 竖条标题\n\nBody.");
60
+ const { html } = await exportMarkdownToWechatHtml({
61
+ markdownPath, outPath: join(tempDir, "post.html"), account: "default",
62
+ themeOverrides: { syntaxPresets: { h2: "pillar" } },
63
+ });
64
+ const h2Tag = html.match(/<section[^>]*>(?:<span[^>]*>[^<]*<\/span>)*竖条标题/)?.[0] ?? "";
65
+ expect(h2Tag).toContain("border-left: 4px solid");
66
+ }, 30_000);
67
+
51
68
  test.skipIf(findChrome() === null)("Studio saves and resets the same style and ignores stale renders", async () => {
52
69
  const studio = await readFile(join(DIST_DIR, "src/wechat-preview/browser/studio/studio.html"), "utf8");
53
70
  const theme = getWechatPreviewTheme("default");
@@ -60,14 +77,20 @@ window.fetch = async (url, options) => {
60
77
  }
61
78
  return new Response(JSON.stringify(${JSON.stringify({ defaultAccount: "default", accounts: { default: { name: "default", theme, customCssContent: "" } }, plugins: [], sampleMarkdown: "# Title\n\nInitial body." })}));
62
79
  };
80
+ const originalCreateObjectURL = URL.createObjectURL;
81
+ URL.createObjectURL = blob => { window.exportedBlob = blob; return originalCreateObjectURL.call(URL, blob); };
82
+ HTMLAnchorElement.prototype.click = function() { window.downloadName = this.download; };
63
83
  window.__zzhubRenderReady = (async () => {
64
84
  const waitFor = async predicate => { while (!predicate()) await new Promise(resolve => setTimeout(resolve, 20)); };
65
85
  const edit = (id, value) => { const input = document.getElementById(id); input.value = value; input.dispatchEvent(new Event('input')); };
66
86
  const source = () => document.getElementById('html-source-code')?.value || '';
67
87
  await waitFor(() => source().includes('Initial body.'));
68
88
  edit('input-font-size', '18'); edit('input-para-spacing', '1.44');
69
- edit('input-heading-label', 'Chapter'); edit('input-custom-css', 'p { color: #123456; }');
89
+ edit('input-heading-label', 'Chapter'); edit('input-custom-css', '.milkdown .editor p { color: #123456; }');
70
90
  await waitFor(() => source().includes('font-size: 18px') && source().includes('margin-bottom: 1.44em'));
91
+ document.getElementById('btn-export-config').click();
92
+ const exported = JSON.parse(await window.exportedBlob.text());
93
+ const exportDidNotSave = !window.saved;
71
94
  document.getElementById('btn-save').click();
72
95
  await waitFor(() => window.saved);
73
96
  await new Promise(resolve => setTimeout(resolve, 20));
@@ -79,10 +102,10 @@ window.__zzhubRenderReady = (async () => {
79
102
  Image.prototype.decode = async function() { await new Promise(resolve => setTimeout(resolve, 250)); return originalDecode.call(this); };
80
103
  edit('editor-markdown', '![0.50](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)');
81
104
  await new Promise(resolve => setTimeout(resolve, 100));
82
- edit('editor-markdown', 'Latest body.');
83
- await waitFor(() => source().includes('Latest body.'));
105
+ edit('editor-markdown', 'Latest body.\\n\\n![1.00](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 "Crepe 图注")');
106
+ await waitFor(() => source().includes('Latest body.') && source().includes('Crepe 图注</p>'));
84
107
  const result = document.createElement('script'); result.id = 'zzhub-wechat-export-result'; result.type = 'application/json';
85
- result.textContent = JSON.stringify({status:'success',html:JSON.stringify({saved:window.saved,resetOk,html:source()})});
108
+ result.textContent = JSON.stringify({status:'success',html:JSON.stringify({saved:window.saved,exported,exportDidNotSave,downloadName:window.downloadName,resetOk,html:source()})});
86
109
  document.body.appendChild(result);
87
110
  })();
88
111
  </script>`);
@@ -90,12 +113,30 @@ window.__zzhubRenderReady = (async () => {
90
113
  expect(result.status).toBe("success");
91
114
  if (!("html" in result)) throw new Error("Studio did not produce HTML");
92
115
  const checked = JSON.parse(result.html);
116
+ expect(checked.exported.account).toBeUndefined();
117
+ expect(checked.exported.exportTheme.bodyFontSize).toBe("18px");
118
+ expect(checked.exported.structure.headingLabel).toBe("Chapter");
119
+ expect(checked.exported.css).toContain("#123456");
120
+ expect(checked.exportDidNotSave).toBe(true);
121
+ expect(checked.downloadName).toBe("wechat-style.json");
122
+ tempDir = await mkdtemp(join(tmpdir(), "zzhub-standalone-export-"));
123
+ const profilePath = join(tempDir, "wechat-style.json");
124
+ const markdownPath = join(tempDir, "post.md");
125
+ await writeFile(profilePath, JSON.stringify(checked.exported));
126
+ await writeFile(markdownPath, "## Section\n\nPortable body.");
127
+ const portable = await exportMarkdownToWechatHtml({
128
+ markdownPath, outPath: join(tempDir, "post.html"), account: "ancientone", articleThemePath: profilePath,
129
+ themeOverrides: { exportTheme: { bodyFontSize: "14px" } },
130
+ });
131
+ expect(portable.html).toMatch(/<p[^>]+font-size: 18px[^>]*>Portable body\.<\/p>/);
132
+ expect(portable.html).toContain("Chapter</span>");
133
+ expect(portable.html).toMatch(/color: (?:#123456|rgb\(18, 52, 86\))/);
93
134
  expect(checked.saved.exportTheme.bodyFontSize).toBe("18px");
94
135
  expect(checked.saved.exportTheme.bodyParagraphSpacing).toBe("1.44em");
95
136
  expect(checked.saved.structure.headingLabel).toBe("Chapter");
96
137
  expect(checked.resetOk).toBe(true);
97
138
  expect(checked.html).toContain("Latest body.");
98
- expect(checked.html).not.toContain("<img");
139
+ expect(checked.html).toContain("Crepe 图注</p>");
99
140
  }, 20_000);
100
141
 
101
142
  test.skipIf(findChrome() === null)("renders all nodes, cascades CSS, and emits a safe exact preview", async () => {
@@ -123,8 +164,8 @@ window.__zzhubRenderReady = (async () => {
123
164
 
124
165
  expect(result.html).toBe(html);
125
166
  expect(html.length).toBeLessThan(65_400);
126
- expect(html).toContain("一级标题:克制的中文编辑部");
127
- expect(html).toContain("<blockquote");
167
+ expect(html).toContain("二级标题:清晰但不过度装饰");
168
+ expect(html).not.toContain("<blockquote");
128
169
  expect(html).toContain("重点结论</span>");
129
170
  expect(html).not.toContain("==重点结论==");
130
171
  expect(html).toContain("==不高亮==");
@@ -170,7 +211,7 @@ window.__zzhubRenderReady = (async () => {
170
211
 
171
212
  const tagNames = Array.from(html.matchAll(/<\/?([a-z][a-z0-9-]*)\b/gi), (match) => match[1]?.toLowerCase());
172
213
  const allowedTags = new Set([
173
- "section", "p", "blockquote", "ul", "ol", "li", "pre", "code",
214
+ "section", "p", "ul", "ol", "li", "pre", "code",
174
215
  "span", "strong", "em", "b", "i", "u", "s", "del", "a", "img",
175
216
  "br", "hr", "table", "thead", "tbody", "tr", "th", "td",
176
217
  ]);
@@ -29,6 +29,7 @@ export interface WechatRenderContext {
29
29
  theme: WechatExportTheme;
30
30
  references: WechatLinkReference[];
31
31
  structure?: ArticleStructure;
32
+ strictImageDecode?: boolean;
32
33
  }
33
34
 
34
35
  /** 模板仅声明有限的结构变化,不注入脚本或任意 HTML。 */
@@ -61,6 +62,7 @@ export interface RenderWechatHtmlInput {
61
62
  editorVars?: Record<string, string>;
62
63
  theme: WechatExportTheme;
63
64
  renderers?: WechatRendererOverrides;
65
+ strictImageDecode?: boolean;
64
66
  }
65
67
 
66
68
  const ALLOWED_TAGS = new Set([
@@ -131,6 +133,7 @@ const ALLOWED_STYLE_PROPERTIES = new Set([
131
133
  "text-decoration-style",
132
134
  "text-indent",
133
135
  "background",
136
+ "background-image",
134
137
  "background-color",
135
138
  "border",
136
139
  "border-top",
@@ -234,7 +237,8 @@ function imageIsOnlyContent(parent: HTMLElement, image: HTMLElement): boolean {
234
237
  });
235
238
  }
236
239
 
237
- // 图片缩放与图注共用导出流程,分别读取 alt 和 title。
240
+ // 图片缩放与图注共用导出流程:Markdown 往返读两位小数 alt 与 title;
241
+ // Crepe 块级图片 DOM 读 ratio 与 caption 属性。
238
242
  const imageRenderer: WechatElementRenderer = {
239
243
  kind: "image",
240
244
  selector: "img",
@@ -245,35 +249,66 @@ const imageRenderer: WechatElementRenderer = {
245
249
  if (src) element.setAttribute("data-src", src);
246
250
 
247
251
  const parent = element.parentElement;
248
- if (!parent || parent.tagName.toLowerCase() !== "p" || !imageIsOnlyContent(parent, element)) {
252
+ const inOwnParagraph =
253
+ !!parent && parent.tagName.toLowerCase() === "p" && imageIsOnlyContent(parent, element);
254
+ // Crepe 块级图片是文章根的顶层裸 <img>,同样视为独立图片。
255
+ const topLevelBlock = parent === context.root;
256
+ if (!parent || (!inOwnParagraph && !topLevelBlock)) {
249
257
  return;
250
258
  }
251
259
 
252
- // Crepe 仅将独立图片的缩放比例序列化为两位小数。
260
+ const ratioRaw = element.getAttribute("ratio");
261
+ const ratioFromAttr = ratioRaw ? Number(ratioRaw) : NaN;
253
262
  const alt = element.getAttribute("alt") ?? "";
254
- const ratio = /^(?:0|[1-9]\d*)\.\d{2}$/.test(alt) ? Number(alt) : 0;
263
+ const ratio =
264
+ Number.isFinite(ratioFromAttr) && ratioFromAttr > 0
265
+ ? ratioFromAttr
266
+ : /^(?:0|[1-9]\d*)\.\d{2}$/.test(alt) ? Number(alt) : 0;
255
267
  if (ratio > 0 && Number.isFinite(ratio)) {
256
268
  // DOMParser 文档不加载图片,用浏览器图片对象读取原始尺寸。
257
269
  const sourceImage = new Image();
258
- sourceImage.src = src ?? "";
270
+ let imageSrc = src ?? "";
271
+ if (
272
+ typeof window !== "undefined" &&
273
+ window.location &&
274
+ window.location.origin &&
275
+ window.location.origin.startsWith("http") &&
276
+ imageSrc &&
277
+ !/^(?:https?:|data:|blob:|\/\/)/i.test(imageSrc)
278
+ ) {
279
+ if (imageSrc.startsWith("/") && !imageSrc.startsWith("/local-file")) {
280
+ imageSrc = `/local-file?path=${encodeURIComponent(imageSrc)}`;
281
+ }
282
+ }
283
+ sourceImage.src = imageSrc;
259
284
  try {
260
285
  await sourceImage.decode();
261
286
  if (!sourceImage.naturalWidth) throw new Error("图片原始宽度为空");
287
+ element.style.width = `${Math.min(ratio, 1) * 100}%`;
288
+ element.style.maxWidth = `${sourceImage.naturalWidth * ratio}px`;
289
+ element.style.height = "auto";
262
290
  } catch (error) {
263
- throw new Error(`无法读取缩放图片的原始尺寸:${element.getAttribute("title") || "无图注"}`, { cause: error });
291
+ if (context.strictImageDecode ?? true) {
292
+ throw new Error(`无法读取缩放图片的原始尺寸:${element.getAttribute("title") || "无图注"}`, { cause: error });
293
+ }
294
+ // 非严格模式(如 Studio 可视化编辑预览)降级处理,保证编辑器正常渲染
295
+ element.style.width = `${Math.min(ratio, 1) * 100}%`;
296
+ element.style.height = "auto";
264
297
  }
265
- element.style.width = `${Math.min(ratio, 1) * 100}%`;
266
- element.style.maxWidth = `${sourceImage.naturalWidth * ratio}px`;
267
- element.style.height = "auto";
268
298
  }
269
299
 
270
300
  const figure = context.document.createElement("figure");
271
301
  setNodeKind(figure, "image-block");
272
- parent.replaceWith(figure);
302
+ if (inOwnParagraph) {
303
+ parent.replaceWith(figure);
304
+ } else {
305
+ element.replaceWith(figure);
306
+ }
273
307
  figure.appendChild(element);
274
308
 
275
- const caption = element.getAttribute("title")?.trim();
309
+ const caption = (element.getAttribute("caption") || element.getAttribute("title") || "").trim();
276
310
  element.removeAttribute("title");
311
+ element.removeAttribute("caption");
277
312
  if (caption) {
278
313
  const figcaption = context.document.createElement("figcaption");
279
314
  setNodeKind(figcaption, "image-caption");
@@ -382,6 +417,10 @@ const blockquoteRenderer: WechatElementRenderer = {
382
417
  element.prepend(label);
383
418
  }
384
419
  },
420
+ finalize(element) {
421
+ // 微信编辑器会给原生 blockquote 强制加灰色左边框,导出时降级为 section,外观全靠内联样式。
422
+ replaceTag(element, "section");
423
+ },
385
424
  };
386
425
 
387
426
  /** Normalize URL for reference dedupe (same target → one footnote entry). */
@@ -631,6 +670,33 @@ export function buildWechatThemeCss(
631
670
  .milkdown .editor p {
632
671
  ${theme.bodyFontSize ? `font-size: ${theme.bodyFontSize};` : ""}
633
672
  ${theme.bodyParagraphSpacing ? `margin-bottom: ${theme.bodyParagraphSpacing};` : ""}
673
+ ${theme.bodyLineHeight ? `line-height: ${theme.bodyLineHeight};` : ""}
674
+ ${theme.bodyLetterSpacing ? `letter-spacing: ${theme.bodyLetterSpacing};` : ""}
675
+ ${theme.bodyColor ? `color: ${theme.bodyColor};` : ""}
676
+ }
677
+ `;
678
+ }
679
+
680
+ /**
681
+ * 结构装饰(编号 / 章节标签 / 引用标签)在导出时是真实节点,编辑器里是 ::before。
682
+ * 此处样式与 studio 编辑器的 ::before 视觉对齐,否则预览中退化为普通正文。
683
+ */
684
+ function buildStructureDecorationCss(structure?: ArticleStructure): string {
685
+ if (!structure?.numberedHeadings && !structure?.headingLabel && !structure?.quoteLabel) return "";
686
+ return `
687
+ .milkdown .editor [data-wechat-node="heading-number"],
688
+ .milkdown .editor [data-wechat-node="heading-label"] {
689
+ margin-right: 0.3em;
690
+ opacity: 0.7;
691
+ font-weight: 600;
692
+ }
693
+ .milkdown .editor [data-wechat-node="quote-label"] {
694
+ margin: 0 0 6px;
695
+ font-size: 11px;
696
+ font-weight: 600;
697
+ letter-spacing: 0.05em;
698
+ color: var(--wx-brand-ink, var(--wx-brand-accent, #ca6093));
699
+ text-transform: uppercase;
634
700
  }
635
701
  `;
636
702
  }
@@ -639,6 +705,59 @@ function parseHtml(html: string): Document {
639
705
  return new DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, "text/html");
640
706
  }
641
707
 
708
+ const BORDER_SIDES = ["top", "right", "bottom", "left"] as const;
709
+
710
+ /**
711
+ * 浏览器把 `border: none; border-left: X` 或 `border-left: none` 序列化成多值简写或
712
+ * none 边的长属性,其中 border-color 是 currentcolor——公众号编辑器解析不了,整条
713
+ * 声明作废后边框退回正文灰色。展开为可见边的单值属性,丢弃 none/hidden 边。
714
+ */
715
+ function normalizeBorderShorthands(element: HTMLElement): void {
716
+ const style = element.style;
717
+ const sides = BORDER_SIDES.map((side) => ({
718
+ side,
719
+ width: style.getPropertyValue(`border-${side}-width`),
720
+ style: style.getPropertyValue(`border-${side}-style`),
721
+ color: style.getPropertyValue(`border-${side}-color`),
722
+ }));
723
+
724
+ const invisible = (entry: (typeof sides)[number]) => entry.style === "none" || entry.style === "hidden";
725
+ const hasShorthand =
726
+ !!style.getPropertyValue("border-style") ||
727
+ !!style.getPropertyValue("border-width") ||
728
+ !!style.getPropertyValue("border-color");
729
+ if (!hasShorthand && !sides.some(invisible)) return;
730
+
731
+ if (!hasShorthand) {
732
+ // 单边 none/hidden 长属性:只删除,其余保持原样
733
+ for (const { side, style: borderStyle } of sides) {
734
+ if (borderStyle !== "none" && borderStyle !== "hidden") continue;
735
+ style.removeProperty(`border-${side}`);
736
+ style.removeProperty(`border-${side}-width`);
737
+ style.removeProperty(`border-${side}-style`);
738
+ style.removeProperty(`border-${side}-color`);
739
+ }
740
+ return;
741
+ }
742
+
743
+ const removable = ["border", "border-width", "border-style", "border-color"];
744
+ for (const { side } of sides) {
745
+ removable.push(`border-${side}`, `border-${side}-width`, `border-${side}-style`, `border-${side}-color`);
746
+ }
747
+ for (const property of removable) style.removeProperty(property);
748
+
749
+ for (const entry of sides) {
750
+ if (!entry.style || invisible(entry)) continue;
751
+ if (entry.width && entry.color) {
752
+ style.setProperty(`border-${entry.side}`, `${entry.width} ${entry.style} ${entry.color}`);
753
+ } else {
754
+ if (entry.width) style.setProperty(`border-${entry.side}-width`, entry.width);
755
+ style.setProperty(`border-${entry.side}-style`, entry.style);
756
+ if (entry.color) style.setProperty(`border-${entry.side}-color`, entry.color);
757
+ }
758
+ }
759
+ }
760
+
642
761
  function sanitizeStyle(element: HTMLElement): void {
643
762
  const propertyNames: string[] = [];
644
763
  for (let index = 0; index < element.style.length; index += 1) {
@@ -711,6 +830,7 @@ function sanitizeTree(root: HTMLElement): void {
711
830
  sanitizeImageUrl(element, "data-src");
712
831
  }
713
832
  sanitizeStyle(element);
833
+ normalizeBorderShorthands(element);
714
834
  }
715
835
  }
716
836
 
@@ -753,11 +873,13 @@ export async function renderWechatHtml(input: RenderWechatHtmlInput): Promise<st
753
873
  theme: input.theme,
754
874
  references,
755
875
  structure: input.structure,
876
+ strictImageDecode: input.strictImageDecode ?? true,
756
877
  });
757
878
 
758
879
  const css = [
759
880
  input.baseCss,
760
881
  buildWechatThemeCss(input.editorVars ?? {}, input.theme),
882
+ buildStructureDecorationCss(input.structure),
761
883
  pluginCss,
762
884
  input.customCss ?? "",
763
885
  ].filter(Boolean).join("\n");
@@ -788,6 +910,7 @@ export async function renderWechatHtml(input: RenderWechatHtmlInput): Promise<st
788
910
  theme: input.theme,
789
911
  references,
790
912
  structure: input.structure,
913
+ strictImageDecode: input.strictImageDecode ?? true,
791
914
  });
792
915
  sanitizeTree(finalRoot);
793
916
  return finalRoot.outerHTML;
@@ -150,12 +150,14 @@ describe("init", () => {
150
150
  "--content-form", "article",
151
151
  "--targets", "wechat@ancientone",
152
152
  "--content-origin", "user",
153
+ "--article-theme", "./styles/standalone.json",
153
154
  ]));
154
155
  const state = await readState(output.state_path);
155
156
  expect(state.publish_targets).toEqual([
156
157
  { route: "wechat-article", account: "ancientone" },
157
158
  ]);
158
159
  expect(state.route.primary).toBe("wechat-article");
160
+ expect(state.intent.article_theme).toBe(join(process.cwd(), "styles/standalone.json"));
159
161
  });
160
162
 
161
163
  test("normalizes underscore flags and resolves route/account from intent text", async () => {
@@ -1 +0,0 @@
1
- :root{--bg-main: #f8fafc;--bg-panel: #ffffff;--border: #e2e8f0;--border-focus: #ca6093;--text-main: #1e293b;--text-muted: #64748b;--brand: #ca6093;--brand-hover: #b35082;--brand-light: rgba(202, 96, 147, .08);--success: #10b981;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;--font-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*{box-sizing:border-box;margin:0;padding:0}body{font-family:var(--font-sans);color:var(--text-main);background:var(--bg-main);height:100vh;display:flex;flex-direction:column;overflow:hidden}.studio-header{height:52px;background:var(--bg-panel);border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 16px;z-index:10}.brand-section{display:flex;align-items:center;gap:10px}.brand-logo{font-size:18px}.brand-title{font-size:15px;font-weight:700;color:var(--text-main);letter-spacing:-.01em}.brand-badge{font-size:11px;font-weight:600;background:var(--brand-light);color:var(--brand);padding:2px 8px;border-radius:999px}.header-controls{display:flex;align-items:center;gap:12px}.account-select-wrap{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--text-muted)}.account-select{padding:5px 10px;font-size:13px;border-radius:6px;border:1px solid var(--border);background:#fff;color:var(--text-main);outline:none;cursor:pointer}.account-select:focus{border-color:var(--border-focus)}.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;border:1px solid var(--border);background:#fff;color:var(--text-main);transition:all .15s ease;text-decoration:none}.btn:hover{background:#f1f5f9}.btn-primary{background:var(--brand);color:#fff;border-color:var(--brand)}.btn-primary:hover{background:var(--brand-hover);border-color:var(--brand-hover)}.btn-success{background:#059669;color:#fff;border-color:#059669}.btn-success:hover{background:#047857;border-color:#047857}.studio-body{flex:1;display:flex;min-height:0;overflow:hidden}.panel{display:flex;flex-direction:column;background:var(--bg-panel);min-height:0}.panel-header{padding:10px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:#fafbfc}.panel-title{font-size:13px;font-weight:650;color:var(--text-main);display:flex;align-items:center;gap:6px}.left-panel{width:360px;border-right:1px solid var(--border);flex-shrink:0}.tab-nav{display:flex;border-bottom:1px solid var(--border);background:#f8fafc}.tab-btn{flex:1;padding:10px 12px;font-size:13px;font-weight:600;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}.tab-btn:hover{color:var(--text-main)}.tab-btn.active{color:var(--brand);background:#fff;border-bottom-color:var(--brand)}.tab-content{flex:1;overflow-y:auto;padding:16px}.config-section{margin-bottom:22px}.section-label{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:12px;display:flex;align-items:center;gap:6px}.form-group{margin-bottom:14px}.form-label{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--text-main);margin-bottom:6px}.form-val{font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}.slider-input{width:100%;accent-color:var(--brand);cursor:pointer}.color-input-row{display:flex;align-items:center;gap:8px}.color-picker{-webkit-appearance:none;appearance:none;border:1px solid var(--border);border-radius:6px;width:32px;height:32px;cursor:pointer;padding:2px;background:#fff}.color-picker::-webkit-color-swatch-wrapper{padding:0}.color-picker::-webkit-color-swatch{border:none;border-radius:4px}.color-hex{flex:1;padding:6px 10px;font-family:var(--font-mono);font-size:12px;border:1px solid var(--border);border-radius:6px;color:var(--text-main)}.switch-label{display:flex;align-items:center;justify-content:space-between;cursor:pointer;font-size:12px}.switch-toggle{position:relative;display:inline-block;width:38px;height:20px}.switch-toggle input{opacity:0;width:0;height:0}.slider-round{position:absolute;cursor:pointer;inset:0;background-color:#cbd5e1;transition:.2s;border-radius:20px}.slider-round:before{position:absolute;content:"";height:14px;width:14px;left:3px;bottom:3px;background-color:#fff;transition:.2s;border-radius:50%}input:checked+.slider-round{background-color:var(--brand)}input:checked+.slider-round:before{transform:translate(18px)}.css-editor{width:100%;height:140px;font-family:var(--font-mono);font-size:12px;line-height:1.5;padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:#fafbfc;color:#0f172a;resize:vertical;outline:none}.css-editor:focus{border-color:var(--border-focus);background:#fff}.syntax-card{border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:12px;background:#fff;transition:border-color .15s}.syntax-card:hover{border-color:#cbd5e1}.syntax-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}.syntax-card-title{font-size:13px;font-weight:650;color:var(--text-main)}.syntax-card-badge{font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;background:#f1f5f9;color:var(--text-muted)}.syntax-card-desc{font-size:12px;color:var(--text-muted);margin-bottom:8px;line-height:1.4}.syntax-code-snippet{background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px 10px;font-family:var(--font-mono);font-size:11px;color:#334155;white-space:pre-wrap;margin-bottom:8px}.syntax-btn-insert{width:100%;padding:4px 8px;font-size:11px;font-weight:600;border:1px dashed var(--border);border-radius:4px;background:#fafbfc;color:var(--brand);cursor:pointer}.syntax-btn-insert:hover{background:var(--brand-light);border-color:var(--brand)}.editor-panel{flex:1;border-right:1px solid var(--border);min-width:320px}.editor-toolbar{display:flex;gap:8px;align-items:center}.editor-textarea{flex:1;width:100%;border:none;outline:none;resize:none;padding:16px 20px;font-family:var(--font-mono);font-size:13px;line-height:1.7;color:#1e293b;background:#fff}.editor-footer{padding:6px 16px;border-top:1px solid var(--border);background:#fafbfc;font-size:11px;color:var(--text-muted);display:flex;justify-content:space-between}.preview-panel{width:480px;flex-shrink:0;background:#f1f5f9}.preview-container{flex:1;overflow-y:auto;display:flex;flex-direction:column;align-items:center;padding:24px 16px 48px}.device-frame{width:430px;min-height:680px;background:#fff;border-radius:18px;box-shadow:0 10px 30px #00000014,0 1px 3px #0000000d;overflow:hidden;display:flex;flex-direction:column;border:1px solid #e2e8f0}.device-notch-bar{height:28px;background:#fafbfc;border-bottom:1px solid #f1f5f9;display:flex;align-items:center;justify-content:space-between;padding:0 16px;font-size:11px;font-weight:600;color:#64748b}.device-screen{flex:1;padding:20px 16px 36px;box-sizing:border-box;overflow-y:auto}.toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none}.toast{background:#1e293b;color:#fff;padding:10px 16px;border-radius:8px;font-size:13px;font-weight:500;box-shadow:0 6px 18px #00000026;opacity:0;transform:translateY(10px);transition:all .25s ease;pointer-events:auto}.toast.show{opacity:1;transform:translateY(0)}.toast.toast-success{background:#065f46;border:1px solid #10b981}.preset-group{margin-bottom:10px;padding:10px 12px;background:#f8fafc;border:1px solid var(--border);border-radius:var(--radius-sm);transition:border-color .15s}.preset-group:hover{border-color:#cbd5e1}.preset-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.preset-title{font-size:12px;font-weight:650;color:var(--text-main);display:flex;align-items:center;gap:5px}.preset-select{width:100%;padding:5px 8px;font-size:12px;font-weight:500;border:1px solid var(--border);border-radius:4px;background:#fff;color:var(--text-main);outline:none;cursor:pointer;transition:border-color .15s}.preset-select:focus{border-color:var(--brand)}.preset-desc{font-size:11px;color:var(--text-muted);margin-top:5px;line-height:1.4;padding-left:2px}.btn-extract-css{padding:2px 7px;font-size:11px;font-weight:550;color:var(--brand);background:#fff;border:1px solid var(--border);border-radius:4px;cursor:pointer;transition:all .15s ease;white-space:nowrap}.btn-extract-css:hover{background:#f1f5f9;border-color:var(--brand)}#studio-editor-host{display:none}