@akairos/pipeline 0.18.1 → 0.19.0

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 (217) hide show
  1. package/ARTICLE-THEMES.md +2 -0
  2. package/README.md +27 -4
  3. package/package.json +7 -5
  4. package/src/adapter-loader.ts +24 -4
  5. package/src/adapter-types.ts +14 -0
  6. package/src/article-theme.test.ts +55 -0
  7. package/src/article-theme.ts +10 -2
  8. package/src/commands/article-theme.ts +1 -1
  9. package/src/commands/init.ts +2 -0
  10. package/src/commands/publish.ts +1 -1
  11. package/src/commands/typeset-assets-sync.test.ts +12 -1
  12. package/src/commands/wechat-export.ts +9 -4
  13. package/src/plugin-runner.ts +10 -4
  14. package/src/plugin-runtime.test.ts +19 -0
  15. package/src/plugin-runtime.ts +11 -2
  16. package/src/providers/index.ts +4 -2
  17. package/src/runtime-package.ts +13 -6
  18. package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1280 -5
  19. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  20. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  21. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  22. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  23. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  24. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  25. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  26. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  27. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  28. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  29. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  30. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  31. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  32. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  33. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  34. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  35. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  36. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  37. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  38. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  39. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  40. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  41. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  42. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  43. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  44. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  45. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  46. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  47. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  48. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  49. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  50. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  51. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  52. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  53. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  54. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  55. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  56. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  57. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  58. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  59. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  60. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  61. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  62. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  63. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  64. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  65. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  66. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  67. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  68. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  69. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  70. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  71. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  72. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  73. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  74. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  75. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  76. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  77. package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  78. package/src/wechat-preview/assets/browser-dist/assets/apl-B4CMkyY2.js +1 -0
  79. package/src/wechat-preview/assets/browser-dist/assets/asciiarmor-Df11BRmG.js +1 -0
  80. package/src/wechat-preview/assets/browser-dist/assets/asn1-EdZsLKOL.js +1 -0
  81. package/src/wechat-preview/assets/browser-dist/assets/asterisk-B-8jnY81.js +1 -0
  82. package/src/wechat-preview/assets/browser-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
  83. package/src/wechat-preview/assets/browser-dist/assets/clike-B9uivgTg.js +1 -0
  84. package/src/wechat-preview/assets/browser-dist/assets/clojure-BMjYHr_A.js +1 -0
  85. package/src/wechat-preview/assets/browser-dist/assets/cmake-BQqOBYOt.js +1 -0
  86. package/src/wechat-preview/assets/browser-dist/assets/cobol-CWcv1MsR.js +1 -0
  87. package/src/wechat-preview/assets/browser-dist/assets/coffeescript-S37ZYGWr.js +1 -0
  88. package/src/wechat-preview/assets/browser-dist/assets/commonlisp-DBKNyK5s.js +1 -0
  89. package/src/wechat-preview/assets/browser-dist/assets/crystal-SjHAIU92.js +1 -0
  90. package/src/wechat-preview/assets/browser-dist/assets/css-BnMrqG3P.js +1 -0
  91. package/src/wechat-preview/assets/browser-dist/assets/cypher-C_CwsFkJ.js +1 -0
  92. package/src/wechat-preview/assets/browser-dist/assets/d-pRatUO7H.js +1 -0
  93. package/src/wechat-preview/assets/browser-dist/assets/diff-DbItnlRl.js +1 -0
  94. package/src/wechat-preview/assets/browser-dist/assets/dockerfile-BKs6k2Af.js +1 -0
  95. package/src/wechat-preview/assets/browser-dist/assets/dtd-DF_7sFjM.js +1 -0
  96. package/src/wechat-preview/assets/browser-dist/assets/dylan-DwRh75JA.js +1 -0
  97. package/src/wechat-preview/assets/browser-dist/assets/ebnf-CDyGwa7X.js +1 -0
  98. package/src/wechat-preview/assets/browser-dist/assets/ecl-Cabwm37j.js +1 -0
  99. package/src/wechat-preview/assets/browser-dist/assets/eiffel-CnydiIhH.js +1 -0
  100. package/src/wechat-preview/assets/browser-dist/assets/elm-vLlmbW-K.js +1 -0
  101. package/src/wechat-preview/assets/browser-dist/assets/erlang-BNw1qcRV.js +1 -0
  102. package/src/wechat-preview/assets/browser-dist/assets/factor-kuTfRLto.js +1 -0
  103. package/src/wechat-preview/assets/browser-dist/assets/fcl-Kvtd6kyn.js +1 -0
  104. package/src/wechat-preview/assets/browser-dist/assets/forth-Ffai-XNe.js +1 -0
  105. package/src/wechat-preview/assets/browser-dist/assets/fortran-DYz_wnZ1.js +1 -0
  106. package/src/wechat-preview/assets/browser-dist/assets/gas-Bneqetm1.js +1 -0
  107. package/src/wechat-preview/assets/browser-dist/assets/gherkin-heZmZLOM.js +1 -0
  108. package/src/wechat-preview/assets/browser-dist/assets/groovy-D9Dt4D0W.js +1 -0
  109. package/src/wechat-preview/assets/browser-dist/assets/haskell-Cw1EW3IL.js +1 -0
  110. package/src/wechat-preview/assets/browser-dist/assets/haxe-H-WmDvRZ.js +1 -0
  111. package/src/wechat-preview/assets/browser-dist/assets/http-DBlCnlav.js +1 -0
  112. package/src/wechat-preview/assets/browser-dist/assets/idl-BEugSyMb.js +1 -0
  113. package/src/wechat-preview/assets/browser-dist/assets/index-B13lCSuc.js +1 -0
  114. package/src/wechat-preview/assets/browser-dist/assets/index-BDdU9pNh.js +1 -0
  115. package/src/wechat-preview/assets/browser-dist/assets/index-BFLGVgsp.js +1 -0
  116. package/src/wechat-preview/assets/browser-dist/assets/index-BHJY0B4E.js +8 -0
  117. package/src/wechat-preview/assets/browser-dist/assets/index-BOnKEJxh.js +7 -0
  118. package/src/wechat-preview/assets/browser-dist/assets/index-BPmBcGI-.js +1 -0
  119. package/src/wechat-preview/assets/browser-dist/assets/index-BTVIa90Y.js +1 -0
  120. package/src/wechat-preview/assets/browser-dist/assets/index-BYV6TA8f.js +1 -0
  121. package/src/wechat-preview/assets/browser-dist/assets/index-BlVpb0dF.js +1 -0
  122. package/src/wechat-preview/assets/browser-dist/assets/index-BqKARoyW.js +1 -0
  123. package/src/wechat-preview/assets/browser-dist/assets/index-BzHql5Rp.js +20 -0
  124. package/src/wechat-preview/assets/browser-dist/assets/index-CPFEJ9fX.js +1 -0
  125. package/src/wechat-preview/assets/browser-dist/assets/index-CdxKyTX6.js +1 -0
  126. package/src/wechat-preview/assets/browser-dist/assets/index-CyXDvcwY.js +1 -0
  127. package/src/wechat-preview/assets/browser-dist/assets/index-DA7Z4paG.js +13 -0
  128. package/src/wechat-preview/assets/browser-dist/assets/index-DDQGWU_D.js +3 -0
  129. package/src/wechat-preview/assets/browser-dist/assets/index-DENMoE5l.js +1 -0
  130. package/src/wechat-preview/assets/browser-dist/assets/index-D_EI5vFS.js +1 -0
  131. package/src/wechat-preview/assets/browser-dist/assets/index-DpuVrwze.js +1 -0
  132. package/src/wechat-preview/assets/browser-dist/assets/index-IBlhKHQr.js +8 -0
  133. package/src/wechat-preview/assets/browser-dist/assets/index-ifI5svkD.js +2 -0
  134. package/src/wechat-preview/assets/browser-dist/assets/index-vRiJxvj2.js +1 -0
  135. package/src/wechat-preview/assets/browser-dist/assets/javascript-iXu5QeM3.js +1 -0
  136. package/src/wechat-preview/assets/browser-dist/assets/julia-DuME0IfC.js +1 -0
  137. package/src/wechat-preview/assets/browser-dist/assets/livescript-BwQOo05w.js +1 -0
  138. package/src/wechat-preview/assets/browser-dist/assets/lua-VAEuO923.js +1 -0
  139. package/src/wechat-preview/assets/browser-dist/assets/mathematica-DTrFuWx2.js +1 -0
  140. package/src/wechat-preview/assets/browser-dist/assets/mbox-CNhZ1qSd.js +1 -0
  141. package/src/wechat-preview/assets/browser-dist/assets/mirc-CjQqDB4T.js +1 -0
  142. package/src/wechat-preview/assets/browser-dist/assets/mllike-CXdrOF99.js +1 -0
  143. package/src/wechat-preview/assets/browser-dist/assets/modelica-Dc1JOy9r.js +1 -0
  144. package/src/wechat-preview/assets/browser-dist/assets/mscgen-BA5vi2Kp.js +1 -0
  145. package/src/wechat-preview/assets/browser-dist/assets/mumps-BT43cFF4.js +1 -0
  146. package/src/wechat-preview/assets/browser-dist/assets/nginx-DdIZxoE0.js +1 -0
  147. package/src/wechat-preview/assets/browser-dist/assets/nsis-LY0VT_u5.js +1 -0
  148. package/src/wechat-preview/assets/browser-dist/assets/ntriples-BfvgReVJ.js +1 -0
  149. package/src/wechat-preview/assets/browser-dist/assets/octave-Ck1zUtKM.js +1 -0
  150. package/src/wechat-preview/assets/browser-dist/assets/oz-BzwKVEFT.js +1 -0
  151. package/src/wechat-preview/assets/browser-dist/assets/pascal--L3eBynH.js +1 -0
  152. package/src/wechat-preview/assets/browser-dist/assets/perl-CdXCOZ3F.js +1 -0
  153. package/src/wechat-preview/assets/browser-dist/assets/pig-CevX1Tat.js +1 -0
  154. package/src/wechat-preview/assets/browser-dist/assets/powershell-CFHJl5sT.js +1 -0
  155. package/src/wechat-preview/assets/browser-dist/assets/properties-C78fOPTZ.js +1 -0
  156. package/src/wechat-preview/assets/browser-dist/assets/protobuf-ChK-085T.js +1 -0
  157. package/src/wechat-preview/assets/browser-dist/assets/pug-DeIclll2.js +1 -0
  158. package/src/wechat-preview/assets/browser-dist/assets/puppet-DMA9R1ak.js +1 -0
  159. package/src/wechat-preview/assets/browser-dist/assets/python-BuPzkPfP.js +1 -0
  160. package/src/wechat-preview/assets/browser-dist/assets/q-pXgVlZs6.js +1 -0
  161. package/src/wechat-preview/assets/browser-dist/assets/r-B6wPVr8A.js +1 -0
  162. package/src/wechat-preview/assets/browser-dist/assets/rpm-CTu-6PCP.js +1 -0
  163. package/src/wechat-preview/assets/browser-dist/assets/ruby-BK5aLJev.js +1 -0
  164. package/src/wechat-preview/assets/browser-dist/assets/sas-B4kiWyti.js +1 -0
  165. package/src/wechat-preview/assets/browser-dist/assets/scheme-C41bIUwD.js +1 -0
  166. package/src/wechat-preview/assets/browser-dist/assets/shell-CjFT_Tl9.js +1 -0
  167. package/src/wechat-preview/assets/browser-dist/assets/sieve-C3Gn_uJK.js +1 -0
  168. package/src/wechat-preview/assets/browser-dist/assets/simple-mode-GW_nhZxv.js +1 -0
  169. package/src/wechat-preview/assets/browser-dist/assets/smalltalk-CnHTOXQT.js +1 -0
  170. package/src/wechat-preview/assets/browser-dist/assets/solr-DehyRSwq.js +1 -0
  171. package/src/wechat-preview/assets/browser-dist/assets/sparql-DkYu6x3z.js +1 -0
  172. package/src/wechat-preview/assets/browser-dist/assets/spreadsheet-BCZA_wO0.js +1 -0
  173. package/src/wechat-preview/assets/browser-dist/assets/sql-D0XecflT.js +1 -0
  174. package/src/wechat-preview/assets/browser-dist/assets/stex-C3f8Ysf7.js +1 -0
  175. package/src/wechat-preview/assets/browser-dist/assets/style-Cif_gYUW.css +1 -0
  176. package/src/wechat-preview/assets/browser-dist/assets/stylus-B533Al4x.js +1 -0
  177. package/src/wechat-preview/assets/browser-dist/assets/swift-BzpIVaGY.js +1 -0
  178. package/src/wechat-preview/assets/browser-dist/assets/tcl-DVfN8rqt.js +1 -0
  179. package/src/wechat-preview/assets/browser-dist/assets/textile-CnDTJFAw.js +1 -0
  180. package/src/wechat-preview/assets/browser-dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  181. package/src/wechat-preview/assets/browser-dist/assets/tiki-DGYXhP31.js +1 -0
  182. package/src/wechat-preview/assets/browser-dist/assets/toml-Bm5Em-hy.js +1 -0
  183. package/src/wechat-preview/assets/browser-dist/assets/troff-wAsdV37c.js +1 -0
  184. package/src/wechat-preview/assets/browser-dist/assets/ttcn-CfJYG6tj.js +1 -0
  185. package/src/wechat-preview/assets/browser-dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  186. package/src/wechat-preview/assets/browser-dist/assets/turtle-B1tBg_DP.js +1 -0
  187. package/src/wechat-preview/assets/browser-dist/assets/vb-CmGdzxic.js +1 -0
  188. package/src/wechat-preview/assets/browser-dist/assets/vbscript-BuJXcnF6.js +1 -0
  189. package/src/wechat-preview/assets/browser-dist/assets/velocity-D8B20fx6.js +1 -0
  190. package/src/wechat-preview/assets/browser-dist/assets/verilog-C6RDOZhf.js +1 -0
  191. package/src/wechat-preview/assets/browser-dist/assets/vhdl-lSbBsy5d.js +1 -0
  192. package/src/wechat-preview/assets/browser-dist/assets/webidl-ZXfAyPTL.js +1 -0
  193. package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-GgNrlSLi.js → wechat-renderer-CcHuq7M-.js} +83 -61
  194. package/src/wechat-preview/assets/browser-dist/assets/xquery-DzFWVndE.js +1 -0
  195. package/src/wechat-preview/assets/browser-dist/assets/yacas-BJ4BC0dw.js +1 -0
  196. package/src/wechat-preview/assets/browser-dist/assets/z80-Hz9HOZM7.js +1 -0
  197. package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
  198. package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +215 -165
  199. package/src/wechat-preview/assets/browser-dist/studio.js +1148 -237
  200. package/src/wechat-preview/browser/studio/color-palettes.ts +28 -0
  201. package/src/wechat-preview/browser/studio/config-manager.ts +49 -0
  202. package/src/wechat-preview/browser/studio/milkdown-article-menu.ts +251 -0
  203. package/src/wechat-preview/browser/studio/myno-editor.ts +122 -0
  204. package/src/wechat-preview/browser/studio/studio.css +493 -250
  205. package/src/wechat-preview/browser/studio/studio.html +213 -163
  206. package/src/wechat-preview/browser/studio/studio.ts +684 -211
  207. package/src/wechat-preview/browser/studio/style-packs.ts +201 -0
  208. package/src/wechat-preview/browser/studio/syntax-presets.test.ts +10 -32
  209. package/src/wechat-preview/browser/studio/syntax-presets.ts +19 -357
  210. package/src/wechat-preview/fixtures/all-nodes.md +2 -11
  211. package/src/wechat-preview/index.ts +35 -14
  212. package/src/wechat-preview/server/http.ts +64 -8
  213. package/src/wechat-preview/server/studio.test.ts +60 -1
  214. package/src/wechat-preview/wechat-export.integration.test.ts +49 -8
  215. package/src/wechat-preview/wechat-renderer.ts +134 -11
  216. package/src/workflow.test.ts +2 -0
  217. package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +0 -1
@@ -25,6 +25,7 @@ import type { WechatThemeOverrides } from "../schema/config";
25
25
  import { getWechatPreviewStyleName, getWechatPreviewTheme } from "./themes";
26
26
  import { stripLeadingH1 } from "../text";
27
27
  import { buildCombinedPresetsCss } from "./browser/studio/syntax-presets";
28
+ import type { ArticleStructure } from "./wechat-renderer";
28
29
 
29
30
  interface ViteManifestEntry {
30
31
  file: string;
@@ -99,6 +100,8 @@ export interface ExportMarkdownToWechatHtmlInput {
99
100
  timeoutMs?: number;
100
101
  /** Write intermediate artifacts for debugging. */
101
102
  debugDir?: string;
103
+ /** 外部插件提供的已构建入口;浏览器和文件输出继续由宿主管理。 */
104
+ browserScriptPath?: string;
102
105
  }
103
106
 
104
107
  export interface ExportMarkdownToWechatHtmlResult {
@@ -131,6 +134,10 @@ export function getWechatPreviewBundleSources(): string[] {
131
134
  join(WECHAT_PREVIEW_DIR, "browser/studio/studio.css"),
132
135
  join(WECHAT_PREVIEW_DIR, "browser/studio/syntax-presets.ts"),
133
136
  join(WECHAT_PREVIEW_DIR, "browser/studio/color-palettes.ts"),
137
+ join(WECHAT_PREVIEW_DIR, "browser/studio/style-packs.ts"),
138
+ join(WECHAT_PREVIEW_DIR, "browser/studio/milkdown-article-menu.ts"),
139
+ join(WECHAT_PREVIEW_DIR, "browser/studio/myno-editor.ts"),
140
+ join(WECHAT_PREVIEW_DIR, "browser/studio/config-manager.ts"),
134
141
  join(WECHAT_PREVIEW_DIR, "themes.ts"),
135
142
  join(WECHAT_PREVIEW_DIR, "frontmatter-handler.ts"),
136
143
  join(PACKAGE_ROOT, "src/text.ts"),
@@ -140,24 +147,27 @@ export function getWechatPreviewBundleSources(): string[] {
140
147
  ];
141
148
  }
142
149
 
143
- /** Studio 与 CLI 共用模板、账号覆盖和页首素材的解析顺序。 */
150
+ /** 独立配置跨账号复用;目录模板仍沿用账号覆盖顺序。 */
144
151
  export function resolveWechatArticleTheme(
145
152
  account: string,
146
153
  overrides?: Partial<WechatThemeOverrides>,
147
154
  articleTheme?: ArticleThemePackage | null,
148
155
  ) {
156
+ const standalone = articleTheme?.manifest.css !== undefined;
157
+ const accountOverrides = standalone ? undefined : overrides;
149
158
  const { headerImage, ...templateStructure } = articleTheme?.manifest.structure ?? {};
150
- const theme = getWechatPreviewTheme(account, articleTheme ? {
151
- editorVars: { ...articleTheme.manifest.editorVars, ...overrides?.editorVars },
152
- exportTheme: { footerText: "", ...articleTheme.manifest.exportTheme, ...overrides?.exportTheme },
153
- } : overrides);
154
- const structure = articleTheme ? {
159
+ const theme = getWechatPreviewTheme(standalone ? "default" : account, articleTheme ? {
160
+ editorVars: { ...articleTheme.manifest.editorVars, ...accountOverrides?.editorVars },
161
+ exportTheme: { footerText: "", ...articleTheme.manifest.exportTheme, ...accountOverrides?.exportTheme },
162
+ } : accountOverrides);
163
+ const structure: ArticleStructure | undefined = articleTheme ? {
155
164
  ...templateStructure,
156
- ...overrides?.structure,
165
+ ...accountOverrides?.structure,
157
166
  headerImageUrl: headerImage
158
- ? `data:image/${headerImage.split(".").pop()!.toLowerCase().replace("jpg", "jpeg")};base64,${Buffer.from(articleTheme.files.get(headerImage)!).toString("base64")}` : null,
159
- } : overrides?.structure;
160
- return { theme, structure };
167
+ ? `data:image/${headerImage.split(".").pop()!.toLowerCase().replace("jpg", "jpeg")};base64,${Buffer.from(articleTheme.files.get(headerImage)!).toString("base64")}` : articleTheme.manifest.structure.headerImageUrl ?? null,
168
+ } : accountOverrides?.structure;
169
+ const syntaxPresets = accountOverrides?.syntaxPresets ?? articleTheme?.manifest.syntaxPresets;
170
+ return { theme, structure, syntaxPresets };
161
171
  }
162
172
 
163
173
  export function isWechatPreviewBundleStale(
@@ -264,6 +274,14 @@ export function rewriteRelativeImagePaths(markdown: string, baseDir: string): st
264
274
  }
265
275
  return `![${alt}](${needsBrackets ? "<" : ""}${resolved}${needsBrackets ? ">" : ""}${titleStr})`;
266
276
  })
277
+ .replace(/^([ \t]*\[[^\]]+\]:\s*)(\S+)(.*)$/gm, (match, prefix, rawUrl, suffix) => {
278
+ if (isExternalUrl(rawUrl)) return match;
279
+ const cleanUrl = rawUrl.replace(/^<([^>]+)>$/, "$1");
280
+ const hadAngle = rawUrl.startsWith("<") && rawUrl.endsWith(">");
281
+ const resolved = resolveMarkdownAsset(cleanUrl, baseDir);
282
+ const needsBrackets = resolved ? resolved.includes(" ") : hadAngle;
283
+ return `${prefix}${needsBrackets ? "<" : ""}${resolved}${needsBrackets ? ">" : ""}${suffix}`;
284
+ })
267
285
  .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'][^>]*>)/gi, (_match, prefix, src, suffix) => {
268
286
  const resolved = resolveMarkdownAsset(String(src ?? ""), baseDir);
269
287
  return `${prefix}${resolved}${suffix}`;
@@ -389,7 +407,9 @@ export async function exportMarkdownToWechatHtml(
389
407
 
390
408
  let bundle: BundleAssets;
391
409
  try {
392
- bundle = ensureBundle();
410
+ bundle = input.browserScriptPath
411
+ ? { scriptPath: input.browserScriptPath, rebuilt: false, stale: false }
412
+ : ensureBundle();
393
413
  bundleRebuilt = bundle.rebuilt;
394
414
  bundleStale = bundle.stale;
395
415
  } catch (error) {
@@ -404,9 +424,9 @@ export async function exportMarkdownToWechatHtml(
404
424
  const bodyMarkdown = stripLeadingH1(stripped).trimStart();
405
425
  const markdown = bodyMarkdown;
406
426
  const articleTheme = input.articleThemePath ? await readArticleTheme(input.articleThemePath) : null;
407
- const { theme, structure } = resolveWechatArticleTheme(input.account, input.themeOverrides, articleTheme);
408
- const presetsCss = input.themeOverrides?.syntaxPresets
409
- ? buildCombinedPresetsCss(input.themeOverrides.syntaxPresets)
427
+ const { theme, structure, syntaxPresets } = resolveWechatArticleTheme(input.account, input.themeOverrides, articleTheme);
428
+ const presetsCss = syntaxPresets
429
+ ? buildCombinedPresetsCss(syntaxPresets)
410
430
  : "";
411
431
  const customCss = [presetsCss, articleTheme?.css, input.customCss ? await readFile(input.customCss, "utf-8") : ""].filter(Boolean).join("\n\n");
412
432
  const shell = readUtf8(TEMPLATE_PATH);
@@ -416,6 +436,7 @@ export async function exportMarkdownToWechatHtml(
416
436
  assetBaseUrl: pathToFileURL(`${dirname(resolve(input.markdownPath))}/`).href,
417
437
  editorVars: theme.editorVars,
418
438
  exportTheme: theme.exportTheme,
439
+ syntaxPresets,
419
440
  customCss,
420
441
  structure,
421
442
  }),
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
- import { dirname, join } from "path";
2
+ import { realpath, stat } from "fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
3
4
  import { z } from "zod";
4
5
  import {
5
6
  previewBaseUrl,
@@ -26,12 +27,14 @@ import {
26
27
  rewriteHtmlLocalAssets,
27
28
  } from "./local-file";
28
29
  import { renderDashboardHtml, renderFailedEntryHtml } from "./ui";
29
- import { buildWechatPreviewShell, resolveWechatArticleTheme } from "../index";
30
+ import { buildWechatPreviewShell, resolveWechatArticleTheme, rewriteRelativeImagePaths } from "../index";
30
31
  import { getPluginDocs } from "@akairos/milkdown-article-plugins";
31
32
  import { readArticleTheme } from "../../article-theme";
32
33
  import { PipelineConfigSchema, WechatExportThemeOverridesSchema, WechatStructureOverridesSchema } from "../../schema/config";
33
34
  import { loadConfig, saveConfig, getPipelineConfigPath, resolveConfigRelativePath } from "../../config";
34
35
  import { DIST_DIR, WECHAT_PREVIEW_DIR } from "../../runtime-paths";
36
+ import { resolveMarkdownRenderer } from "../../adapter-loader";
37
+ import type { MarkdownStudioAssets } from "../../adapter-types";
35
38
  import type { PreviewRegisterInput } from "./types";
36
39
 
37
40
  /** 保存入口先校验完整请求,避免写入 CLI 无法加载的配置。 */
@@ -105,6 +108,32 @@ export async function startPreviewServer(
105
108
  clearServerLock();
106
109
  }
107
110
 
111
+ // 工作台与导出使用同一个插件;没有工作台能力时给出明确提示。
112
+ const pluginConfig = loadConfig();
113
+ const externalMarkdown = Boolean(pluginConfig.plugins.markdownRenderer);
114
+ let studioAssets: Promise<MarkdownStudioAssets | undefined> | undefined;
115
+ /** 按需加载工作台,故障插件不影响预览登记和历史页面。 */
116
+ function getStudio() {
117
+ return studioAssets ??= resolveMarkdownRenderer(pluginConfig).then(async renderer => {
118
+ const studio = await renderer.studio?.();
119
+ return studio ? { ...studio, root: await realpath(studio.root) } : undefined;
120
+ });
121
+ }
122
+
123
+ /** 插件静态资源独立挂载,真实路径也必须留在插件资源目录内。 */
124
+ async function studioFile(name: string): Promise<string | null> {
125
+ try {
126
+ if (!externalMarkdown) return null;
127
+ const studioRoot = (await getStudio())?.root;
128
+ if (!studioRoot) return null;
129
+ const target = await realpath(resolve(studioRoot, name));
130
+ const rel = relative(studioRoot, target);
131
+ return rel && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`) && (await stat(target)).isFile() ? target : null;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+
108
137
  let server: ReturnType<typeof Bun.serve>;
109
138
  server = Bun.serve({
110
139
  hostname: host,
@@ -210,8 +239,10 @@ export async function startPreviewServer(
210
239
  const accountsData: Record<string, unknown> = {};
211
240
 
212
241
  for (const [key, account] of Object.entries(config.wx.accounts)) {
242
+ const articleThemePath = resolveConfigRelativePath(account.articleTheme, configPath);
243
+ const articleTheme = articleThemePath ? await readArticleTheme(articleThemePath) : null;
213
244
  let customCssContent = "";
214
- if (account.customCss) {
245
+ if (account.customCss && articleTheme?.manifest.css === undefined) {
215
246
  const resolved = resolveConfigRelativePath(account.customCss, configPath);
216
247
  if (resolved) {
217
248
  try {
@@ -222,9 +253,7 @@ export async function startPreviewServer(
222
253
  }
223
254
  }
224
255
 
225
- const articleThemePath = resolveConfigRelativePath(account.articleTheme, configPath);
226
- const articleTheme = articleThemePath ? await readArticleTheme(articleThemePath) : null;
227
- const { theme, structure } = resolveWechatArticleTheme(key, account.theme, articleTheme);
256
+ const { theme, structure, syntaxPresets } = resolveWechatArticleTheme(key, account.theme, articleTheme);
228
257
 
229
258
  accountsData[key] = {
230
259
  name: account.name || key,
@@ -233,11 +262,12 @@ export async function startPreviewServer(
233
262
  theme: {
234
263
  editorVars: theme.editorVars,
235
264
  exportTheme: theme.exportTheme,
236
- syntaxPresets: account.theme?.syntaxPresets || {},
265
+ syntaxPresets: syntaxPresets || {},
237
266
  structure: structure || {},
238
267
  },
239
268
  articleThemeCss: articleTheme?.css || "",
240
269
  articleTheme: account.articleTheme ?? null,
270
+ articleThemeStandalone: articleTheme?.manifest.css !== undefined,
241
271
  };
242
272
  }
243
273
 
@@ -245,7 +275,7 @@ export async function startPreviewServer(
245
275
  const fixturePath = join(WECHAT_PREVIEW_DIR, "fixtures/all-nodes.md");
246
276
  if (existsSync(fixturePath)) {
247
277
  try {
248
- sampleMarkdown = readFileSync(fixturePath, "utf-8");
278
+ sampleMarkdown = rewriteRelativeImagePaths(readFileSync(fixturePath, "utf-8"), dirname(fixturePath));
249
279
  } catch {
250
280
  // ignore
251
281
  }
@@ -316,6 +346,21 @@ export async function startPreviewServer(
316
346
  }
317
347
 
318
348
  if (pathname === "/studio" || pathname === "/studio/" || pathname === "/studio/index.html") {
349
+ if (externalMarkdown) {
350
+ let studio: MarkdownStudioAssets | undefined;
351
+ try { studio = await getStudio(); }
352
+ catch (error) { return json({ error: error instanceof Error ? error.message : String(error) }, 500); }
353
+ if (!studio) return html("<h1>当前 Markdown 渲染插件未提供 Studio 工作台。</h1>", 404);
354
+ const entry = await studioFile(studio.entry);
355
+ if (!entry) return html("<h1>插件 Studio 入口不存在或超出资源目录。</h1>", 500);
356
+ // 相对资源按插件入口解析,统一放入命名空间且保留页面导航链接。
357
+ return html(readFileSync(entry, "utf8").replace(/<(?:script|link)\b[^>]*>/g,
358
+ tag => tag.replace(/\b(src|href)=(["'])([^"']+)\2/g, (attribute, name, quote, value: string) => {
359
+ if (/^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(value)) return attribute;
360
+ const asset = new URL(value, `http://studio/${studio.entry}`);
361
+ return `${name}=${quote}/studio-assets${asset.pathname}${asset.search}${quote}`;
362
+ })));
363
+ }
319
364
  const studioHtmlPath = join(DIST_DIR, "src/wechat-preview/browser/studio/studio.html");
320
365
  const fallbackPath = join(DIST_DIR, "studio.html");
321
366
  const targetHtmlPath = existsSync(studioHtmlPath) ? studioHtmlPath : fallbackPath;
@@ -325,6 +370,17 @@ export async function startPreviewServer(
325
370
  return html("<h1>Studio not built. Run bun run build:wechat-preview first.</h1>", 500);
326
371
  }
327
372
 
373
+ if (pathname.startsWith("/studio-assets/")) {
374
+ let name: string;
375
+ try { name = decodeURIComponent(pathname.slice("/studio-assets/".length)); }
376
+ catch { return json({ error: "invalid asset path" }, 400); }
377
+ const target = await studioFile(name);
378
+ if (!target) return json({ error: "Studio asset not found" }, 404);
379
+ return new Response(new Uint8Array(readFileSync(target)), {
380
+ headers: { "content-type": guessContentType(target), "cache-control": "no-cache" },
381
+ });
382
+ }
383
+
328
384
  if (pathname === "/studio.js" || pathname === "/editor-export.js") {
329
385
  const filePath = join(DIST_DIR, pathname.slice(1));
330
386
  if (existsSync(filePath)) {
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
 
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
- import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
5
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "fs";
6
6
  import { startPreviewServer, type StartPreviewServerResult } from "./http";
7
7
  import { loadConfig, saveConfig } from "../../config";
8
8
 
@@ -160,11 +160,70 @@ 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"');
194
+ });
195
+
196
+ test("external Studio serves plugin resources without exposing files outside its root", async () => {
197
+ const root = join(testDir, "plugin-assets");
198
+ mkdirSync(root);
199
+ writeFileSync(join(root, "studio.html"), '<script src="/studio.js"></script><link href="/style.css"><a href="/">History</a>');
200
+ writeFileSync(join(root, "studio.js"), "/* plugin studio */");
201
+ writeFileSync(join(testDir, "private.txt"), "private");
202
+ symlinkSync(join(testDir, "private.txt"), join(root, "outside.txt"));
203
+ const pluginPath = join(testDir, "studio-plugin.mjs");
204
+ writeFileSync(pluginPath, `export default {name:'studio-test',render(){},studio(){return {root:${JSON.stringify(root)},entry:'studio.html'}}}`);
205
+ const config = loadConfig();
206
+ const original = config.plugins.markdownRenderer;
207
+ config.plugins.markdownRenderer = pluginPath;
208
+ saveConfig(config);
209
+ let external: StartPreviewServerResult | undefined;
210
+ const originalServerDir = process.env.ZZHUB_WECHAT_PREVIEW_DIR;
211
+ process.env.ZZHUB_WECHAT_PREVIEW_DIR = join(testDir, "external-server");
212
+ try {
213
+ external = await startPreviewServer({ host: "127.0.0.1", port: 0, reuseExisting: false });
214
+ const page = await (await fetch(`${external.url}/studio`)).text();
215
+ expect(page).toContain('src="/studio-assets/studio.js"');
216
+ expect(page).toContain('href="/studio-assets/style.css"');
217
+ expect(page).toContain('<a href="/">History</a>');
218
+ expect(await (await fetch(`${external.url}/studio-assets/studio.js`)).text()).toBe("/* plugin studio */");
219
+ expect((await fetch(`${external.url}/studio-assets/outside.txt`)).status).toBe(404);
220
+ expect((await fetch(`${external.url}/studio-assets/..%2fprivate.txt`)).status).toBe(404);
221
+ expect((await fetch(`${external.url}/api/studio/config`)).status).toBe(200);
222
+ } finally {
223
+ external?.stop?.();
224
+ process.env.ZZHUB_WECHAT_PREVIEW_DIR = originalServerDir;
225
+ config.plugins.markdownRenderer = original;
226
+ saveConfig(config);
227
+ }
169
228
  });
170
229
  });
@@ -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
  ]);