@akairos/myno 0.0.0-stage → 0.2.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 (231) hide show
  1. package/README.md +28 -2
  2. package/dist/.vite/manifest.json +1266 -0
  3. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  4. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  5. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  10. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  11. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  16. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  17. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  19. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  20. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  22. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  25. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  29. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  31. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  32. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  34. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  35. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  46. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  47. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  49. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  50. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  52. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  53. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  54. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  55. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  57. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  58. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  60. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  61. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  62. package/dist/assets/apl-B4CMkyY2.js +1 -0
  63. package/dist/assets/asciiarmor-Df11BRmG.js +1 -0
  64. package/dist/assets/asn1-EdZsLKOL.js +1 -0
  65. package/dist/assets/asterisk-B-8jnY81.js +1 -0
  66. package/dist/assets/brainfuck-C4LP7Hcl.js +1 -0
  67. package/dist/assets/clike-B9uivgTg.js +1 -0
  68. package/dist/assets/clojure-BMjYHr_A.js +1 -0
  69. package/dist/assets/cmake-BQqOBYOt.js +1 -0
  70. package/dist/assets/cobol-CWcv1MsR.js +1 -0
  71. package/dist/assets/coffeescript-S37ZYGWr.js +1 -0
  72. package/dist/assets/commonlisp-DBKNyK5s.js +1 -0
  73. package/dist/assets/crystal-SjHAIU92.js +1 -0
  74. package/dist/assets/css-BnMrqG3P.js +1 -0
  75. package/dist/assets/cypher-C_CwsFkJ.js +1 -0
  76. package/dist/assets/d-pRatUO7H.js +1 -0
  77. package/dist/assets/diff-DbItnlRl.js +1 -0
  78. package/dist/assets/dockerfile-BKs6k2Af.js +1 -0
  79. package/dist/assets/dtd-DF_7sFjM.js +1 -0
  80. package/dist/assets/dylan-DwRh75JA.js +1 -0
  81. package/dist/assets/ebnf-CDyGwa7X.js +1 -0
  82. package/dist/assets/ecl-Cabwm37j.js +1 -0
  83. package/dist/assets/eiffel-CnydiIhH.js +1 -0
  84. package/dist/assets/elm-vLlmbW-K.js +1 -0
  85. package/dist/assets/erlang-BNw1qcRV.js +1 -0
  86. package/dist/assets/factor-kuTfRLto.js +1 -0
  87. package/dist/assets/fcl-Kvtd6kyn.js +1 -0
  88. package/dist/assets/forth-Ffai-XNe.js +1 -0
  89. package/dist/assets/fortran-DYz_wnZ1.js +1 -0
  90. package/dist/assets/gas-Bneqetm1.js +1 -0
  91. package/dist/assets/gherkin-heZmZLOM.js +1 -0
  92. package/dist/assets/groovy-D9Dt4D0W.js +1 -0
  93. package/dist/assets/haskell-Cw1EW3IL.js +1 -0
  94. package/dist/assets/haxe-H-WmDvRZ.js +1 -0
  95. package/dist/assets/http-DBlCnlav.js +1 -0
  96. package/dist/assets/idl-BEugSyMb.js +1 -0
  97. package/dist/assets/index-6xZjfNBY.js +1 -0
  98. package/dist/assets/index-7jqFg9Mv.js +13 -0
  99. package/dist/assets/index-AdP2vXVD.js +1 -0
  100. package/dist/assets/index-BHPv-A3N.js +1 -0
  101. package/dist/assets/index-BL9wSBtZ.js +1 -0
  102. package/dist/assets/index-BjawAflQ.js +1 -0
  103. package/dist/assets/index-C0JZnjgf.js +3 -0
  104. package/dist/assets/index-C1vHAMos.js +1 -0
  105. package/dist/assets/index-CAXLlEvK.js +1 -0
  106. package/dist/assets/index-CDgREgYC.js +2 -0
  107. package/dist/assets/index-CIYtVPt-.js +1 -0
  108. package/dist/assets/index-CddMuxp_.js +6 -0
  109. package/dist/assets/index-CmlqrlxY.js +7 -0
  110. package/dist/assets/index-Crr2UUqW.js +1 -0
  111. package/dist/assets/index-D3-l1kuh.js +1 -0
  112. package/dist/assets/index-D5AbviI1.js +1 -0
  113. package/dist/assets/index-D8eibMZn.js +1 -0
  114. package/dist/assets/index-DLh7eEmr.js +1 -0
  115. package/dist/assets/index-Q5rzr3Yl.js +1 -0
  116. package/dist/assets/index-RrNfwOco.js +1 -0
  117. package/dist/assets/index-kgpju2R2.js +1 -0
  118. package/dist/assets/index-qjQPn-9r.js +1 -0
  119. package/dist/assets/javascript-iXu5QeM3.js +1 -0
  120. package/dist/assets/julia-DuME0IfC.js +1 -0
  121. package/dist/assets/livescript-BwQOo05w.js +1 -0
  122. package/dist/assets/lua-VAEuO923.js +1 -0
  123. package/dist/assets/mathematica-DTrFuWx2.js +1 -0
  124. package/dist/assets/mbox-CNhZ1qSd.js +1 -0
  125. package/dist/assets/mirc-CjQqDB4T.js +1 -0
  126. package/dist/assets/mllike-CXdrOF99.js +1 -0
  127. package/dist/assets/modelica-Dc1JOy9r.js +1 -0
  128. package/dist/assets/mscgen-BA5vi2Kp.js +1 -0
  129. package/dist/assets/mumps-BT43cFF4.js +1 -0
  130. package/dist/assets/nginx-DdIZxoE0.js +1 -0
  131. package/dist/assets/nsis-LY0VT_u5.js +1 -0
  132. package/dist/assets/ntriples-BfvgReVJ.js +1 -0
  133. package/dist/assets/octave-Ck1zUtKM.js +1 -0
  134. package/dist/assets/oz-BzwKVEFT.js +1 -0
  135. package/dist/assets/pascal--L3eBynH.js +1 -0
  136. package/dist/assets/perl-CdXCOZ3F.js +1 -0
  137. package/dist/assets/pig-CevX1Tat.js +1 -0
  138. package/dist/assets/powershell-CFHJl5sT.js +1 -0
  139. package/dist/assets/properties-C78fOPTZ.js +1 -0
  140. package/dist/assets/protobuf-ChK-085T.js +1 -0
  141. package/dist/assets/pug-DeIclll2.js +1 -0
  142. package/dist/assets/puppet-DMA9R1ak.js +1 -0
  143. package/dist/assets/python-BuPzkPfP.js +1 -0
  144. package/dist/assets/q-pXgVlZs6.js +1 -0
  145. package/dist/assets/r-B6wPVr8A.js +1 -0
  146. package/dist/assets/rpm-CTu-6PCP.js +1 -0
  147. package/dist/assets/ruby-BK5aLJev.js +1 -0
  148. package/dist/assets/sas-B4kiWyti.js +1 -0
  149. package/dist/assets/scheme-C41bIUwD.js +1 -0
  150. package/dist/assets/shell-CjFT_Tl9.js +1 -0
  151. package/dist/assets/sieve-C3Gn_uJK.js +1 -0
  152. package/dist/assets/simple-mode-GW_nhZxv.js +1 -0
  153. package/dist/assets/smalltalk-CnHTOXQT.js +1 -0
  154. package/dist/assets/solr-DehyRSwq.js +1 -0
  155. package/dist/assets/sparql-DkYu6x3z.js +1 -0
  156. package/dist/assets/spreadsheet-BCZA_wO0.js +1 -0
  157. package/dist/assets/sql-D0XecflT.js +1 -0
  158. package/dist/assets/stex-C3f8Ysf7.js +1 -0
  159. package/dist/assets/style-Cif_gYUW.css +1 -0
  160. package/dist/assets/style-DG6dI-L6.css +1 -0
  161. package/dist/assets/stylus-B533Al4x.js +1 -0
  162. package/dist/assets/swift-BzpIVaGY.js +1 -0
  163. package/dist/assets/tcl-DVfN8rqt.js +1 -0
  164. package/dist/assets/textile-CnDTJFAw.js +1 -0
  165. package/dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
  166. package/dist/assets/tiki-DGYXhP31.js +1 -0
  167. package/dist/assets/toml-Bm5Em-hy.js +1 -0
  168. package/dist/assets/troff-wAsdV37c.js +1 -0
  169. package/dist/assets/ttcn-CfJYG6tj.js +1 -0
  170. package/dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
  171. package/dist/assets/turtle-B1tBg_DP.js +1 -0
  172. package/dist/assets/vb-CmGdzxic.js +1 -0
  173. package/dist/assets/vbscript-BuJXcnF6.js +1 -0
  174. package/dist/assets/velocity-D8B20fx6.js +1 -0
  175. package/dist/assets/verilog-C6RDOZhf.js +1 -0
  176. package/dist/assets/vhdl-lSbBsy5d.js +1 -0
  177. package/dist/assets/webidl-ZXfAyPTL.js +1 -0
  178. package/dist/assets/xquery-DzFWVndE.js +1 -0
  179. package/dist/assets/yacas-BJ4BC0dw.js +1 -0
  180. package/dist/assets/z80-Hz9HOZM7.js +1 -0
  181. package/dist/editor/article-menu.d.ts +36 -0
  182. package/dist/editor/article-menu.d.ts.map +1 -0
  183. package/dist/editor/article-menu.js +251 -0
  184. package/dist/editor/article-menu.js.map +1 -0
  185. package/dist/editor/image-upload.d.ts +9 -0
  186. package/dist/editor/image-upload.d.ts.map +1 -0
  187. package/dist/editor/image-upload.js +65 -0
  188. package/dist/editor/image-upload.js.map +1 -0
  189. package/dist/editor/index.d.ts +4 -0
  190. package/dist/editor/index.d.ts.map +1 -0
  191. package/dist/editor/index.js +4 -0
  192. package/dist/editor/index.js.map +1 -0
  193. package/dist/editor/myno-editor.d.ts +35 -0
  194. package/dist/editor/myno-editor.d.ts.map +1 -0
  195. package/dist/editor/myno-editor.js +117 -0
  196. package/dist/editor/myno-editor.js.map +1 -0
  197. package/dist/index.d.ts +8 -0
  198. package/dist/index.d.ts.map +1 -0
  199. package/dist/index.js +8 -0
  200. package/dist/index.js.map +1 -0
  201. package/dist/src/studio/studio.html +342 -0
  202. package/dist/studio/color-palettes.d.ts +16 -0
  203. package/dist/studio/color-palettes.d.ts.map +1 -0
  204. package/dist/studio/color-palettes.js +129 -0
  205. package/dist/studio/color-palettes.js.map +1 -0
  206. package/dist/studio/config-manager.d.ts +34 -0
  207. package/dist/studio/config-manager.d.ts.map +1 -0
  208. package/dist/studio/config-manager.js +30 -0
  209. package/dist/studio/config-manager.js.map +1 -0
  210. package/dist/studio/studio.d.ts +2 -0
  211. package/dist/studio/studio.d.ts.map +1 -0
  212. package/dist/studio/studio.js +1031 -0
  213. package/dist/studio/studio.js.map +1 -0
  214. package/dist/studio/style-packs.d.ts +24 -0
  215. package/dist/studio/style-packs.d.ts.map +1 -0
  216. package/dist/studio/style-packs.js +179 -0
  217. package/dist/studio/style-packs.js.map +1 -0
  218. package/dist/studio/syntax-presets.d.ts +16 -0
  219. package/dist/studio/syntax-presets.d.ts.map +1 -0
  220. package/dist/studio/syntax-presets.js +408 -0
  221. package/dist/studio/syntax-presets.js.map +1 -0
  222. package/dist/studio/themes.d.ts +32 -0
  223. package/dist/studio/themes.d.ts.map +1 -0
  224. package/dist/studio/themes.js +153 -0
  225. package/dist/studio/themes.js.map +1 -0
  226. package/dist/studio/wechat-renderer.d.ts +59 -0
  227. package/dist/studio/wechat-renderer.d.ts.map +1 -0
  228. package/dist/studio/wechat-renderer.js +797 -0
  229. package/dist/studio/wechat-renderer.js.map +1 -0
  230. package/dist/studio.js +2359 -0
  231. package/package.json +49 -4
@@ -0,0 +1,1031 @@
1
+ import articleCss from "@akairos/milkdown-article-style/article.css?raw";
2
+ import { Editor, defaultValueCtx, rootCtx } from "@milkdown/kit/core";
3
+ import { commonmark } from "@milkdown/kit/preset/commonmark";
4
+ import { gfm } from "@milkdown/kit/preset/gfm";
5
+ import { getHTML } from "@milkdown/kit/utils";
6
+ import { getCombinedMilkdownPlugins, getCombinedDefaultCss } from "@akairos/milkdown-article-plugins";
7
+ import { MynoEditor } from "../editor/myno-editor";
8
+ import { STYLE_PACKS } from "./style-packs";
9
+ import { SYNTAX_CATEGORIES, buildCombinedPresetsCss } from "./syntax-presets";
10
+ import { COLOR_PALETTES } from "./color-palettes";
11
+ import { parseStyleConfig, exportStyleConfigBlob } from "./config-manager";
12
+ import { renderWechatHtml, buildWechatThemeCss } from "./wechat-renderer";
13
+ let accounts = {};
14
+ let currentAccount = "default";
15
+ let pluginsList = [];
16
+ let latestWechatHtml = "";
17
+ let debounceTimer = null;
18
+ let showingHtmlSource = false;
19
+ let currentSyntaxPresets = {};
20
+ let renderRevision = 0;
21
+ let rendering = false;
22
+ let mynoEditor = null;
23
+ let currentEditorMode = "visual";
24
+ let standaloneThemeName = "";
25
+ // ── DOM Elements ────────────────────────────────────────────────────────
26
+ const accountSelect = document.getElementById("account-select");
27
+ const configModeBadge = document.getElementById("config-mode-badge");
28
+ const editorMarkdown = document.getElementById("editor-markdown");
29
+ const milkdownEditorRoot = document.getElementById("milkdown-editor-root");
30
+ const previewScreen = document.getElementById("preview-screen");
31
+ const editorStats = document.getElementById("editor-stats");
32
+ const htmlSourceContainer = document.getElementById("html-source-container");
33
+ const htmlSourceCode = document.getElementById("html-source-code");
34
+ const toastContainer = document.getElementById("toast-container");
35
+ // Sliders & Text Inputs
36
+ const inputFontSize = document.getElementById("input-font-size");
37
+ const valFontSize = document.getElementById("val-font-size");
38
+ const inputLineHeight = document.getElementById("input-line-height");
39
+ const valLineHeight = document.getElementById("val-line-height");
40
+ const inputLetterSpacing = document.getElementById("input-letter-spacing");
41
+ const valLetterSpacing = document.getElementById("val-letter-spacing");
42
+ const inputParaSpacing = document.getElementById("input-para-spacing");
43
+ const valParaSpacing = document.getElementById("val-para-spacing");
44
+ const paletteSelect = document.getElementById("palette-select");
45
+ const paletteDesc = document.getElementById("palette-desc");
46
+ const pickerBrandColor = document.getElementById("picker-brand-color");
47
+ const hexBrandColor = document.getElementById("hex-brand-color");
48
+ const pickerTextColor = document.getElementById("picker-text-color");
49
+ const hexTextColor = document.getElementById("hex-text-color");
50
+ const pickerH2Color = document.getElementById("picker-h2-color");
51
+ const hexH2Color = document.getElementById("hex-h2-color");
52
+ const pickerH3Color = document.getElementById("picker-h3-color");
53
+ const hexH3Color = document.getElementById("hex-h3-color");
54
+ const pickerQuoteColor = document.getElementById("picker-quote-color");
55
+ const hexQuoteColor = document.getElementById("hex-quote-color");
56
+ const pickerDividerColor = document.getElementById("picker-divider-color");
57
+ const hexDividerColor = document.getElementById("hex-divider-color");
58
+ const switchNumberedHeadings = document.getElementById("switch-numbered-headings");
59
+ const inputHeadingLabel = document.getElementById("input-heading-label");
60
+ const inputQuoteLabel = document.getElementById("input-quote-label");
61
+ const inputCustomCss = document.getElementById("input-custom-css");
62
+ const btnModeVisual = document.getElementById("btn-mode-visual");
63
+ const btnModeSource = document.getElementById("btn-mode-source");
64
+ const fileImportConfig = document.getElementById("file-import-config");
65
+ const btnImportConfig = document.getElementById("btn-import-config");
66
+ const btnExportConfig = document.getElementById("btn-export-config");
67
+ const btnSave = document.getElementById("btn-save");
68
+ const btnReset = document.getElementById("btn-reset");
69
+ const btnCopy = document.getElementById("btn-copy");
70
+ const btnToggleView = document.getElementById("btn-toggle-view");
71
+ const btnLoadSample = document.getElementById("btn-load-sample");
72
+ const btnClearMd = document.getElementById("btn-clear-md");
73
+ // ── Toast System ────────────────────────────────────────────────────────
74
+ function showToast(message, type = "info") {
75
+ if (!toastContainer)
76
+ return;
77
+ const toast = document.createElement("div");
78
+ toast.className = `toast ${type === "success" ? "toast-success" : type === "error" ? "toast-error" : ""}`;
79
+ toast.textContent = message;
80
+ toastContainer.appendChild(toast);
81
+ requestAnimationFrame(() => toast.classList.add("show"));
82
+ setTimeout(() => {
83
+ toast.classList.remove("show");
84
+ setTimeout(() => toast.remove(), 300);
85
+ }, 3000);
86
+ }
87
+ // ── Color Picker Pairing ────────────────────────────────────────────────
88
+ function bindColorPair(picker, hex, onChange) {
89
+ picker.addEventListener("input", () => {
90
+ hex.value = picker.value;
91
+ onChange();
92
+ });
93
+ hex.addEventListener("input", () => {
94
+ if (/^#[0-9a-f]{6}$/i.test(hex.value)) {
95
+ picker.value = hex.value;
96
+ onChange();
97
+ }
98
+ });
99
+ }
100
+ function updateStats() {
101
+ const text = getActiveMarkdown();
102
+ const chars = text.replace(/\s+/g, "").length;
103
+ const minutes = Math.max(1, Math.round(chars / 400));
104
+ if (editorStats) {
105
+ editorStats.textContent = `字数: ${chars.toLocaleString()} 字 | 预计阅读: ${minutes} 分钟`;
106
+ }
107
+ }
108
+ function getActiveMarkdown() {
109
+ if (currentEditorMode === "visual" && mynoEditor) {
110
+ return mynoEditor.getMarkdown();
111
+ }
112
+ return editorMarkdown.value;
113
+ }
114
+ function stripLeadingH1(markdown) {
115
+ return markdown.replace(/^#\s+[^\r\n]*(?:\r?\n|$)/, "");
116
+ }
117
+ function extractFrontmatter(markdown) {
118
+ const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
119
+ if (!match)
120
+ return { frontmatter: null, content: markdown };
121
+ return { frontmatter: {}, content: markdown.slice(match[0].length) };
122
+ }
123
+ /** 品牌色衍生的卡片底色(--brand-bg 系),非 6 位 hex 时返回 null 以保留账号默认值。 */
124
+ function brandTint(hex, alpha) {
125
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
126
+ if (!m)
127
+ return null;
128
+ const n = parseInt(m[1], 16);
129
+ return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
130
+ }
131
+ // ── Read Style Controls ─────────────────────────────────────────────────
132
+ function readStyleControls() {
133
+ const account = accounts[currentAccount] || {
134
+ theme: { exportTheme: {}, editorVars: {}, structure: {} },
135
+ };
136
+ const exportTheme = {
137
+ ...account.theme.exportTheme,
138
+ bodyFontSize: `${inputFontSize.value}px`,
139
+ bodyParagraphSpacing: `${inputParaSpacing.value}em`,
140
+ bodyColor: hexTextColor.value,
141
+ h2Color: hexH2Color.value,
142
+ h3Color: hexH3Color.value,
143
+ primaryColor: hexBrandColor.value,
144
+ dividerColor: hexDividerColor.value,
145
+ blockquoteBorderColor: hexQuoteColor.value,
146
+ bodyLineHeight: inputLineHeight.value,
147
+ bodyLetterSpacing: `${inputLetterSpacing.value}em`,
148
+ };
149
+ const brandBg = brandTint(hexBrandColor.value, 0.08);
150
+ const brandBgDeep = brandBg && brandTint(hexBrandColor.value, 0.12);
151
+ const editorVars = {
152
+ ...account.theme.editorVars,
153
+ "--brand": hexBrandColor.value,
154
+ ...(brandBg && brandBgDeep ? { "--brand-bg": brandBg, "--brand-bg-deep": brandBgDeep } : {}),
155
+ "--text": hexTextColor.value,
156
+ "--divider": hexDividerColor.value,
157
+ };
158
+ const structure = {
159
+ ...account.theme.structure,
160
+ numberedHeadings: switchNumberedHeadings.checked,
161
+ headingLabel: inputHeadingLabel.value.trim(),
162
+ quoteLabel: inputQuoteLabel.value.trim(),
163
+ };
164
+ return { editorVars, exportTheme, structure };
165
+ }
166
+ // ── Live Editor Style Synchronization ───────────────────────────────────
167
+ function applyLiveEditorStyles(style, customCss) {
168
+ let styleTag = document.getElementById("studio-live-editor-css");
169
+ if (!styleTag) {
170
+ styleTag = document.createElement("style");
171
+ styleTag.id = "studio-live-editor-css";
172
+ document.head.appendChild(styleTag);
173
+ }
174
+ const themeCss = buildWechatThemeCss(style.editorVars, style.exportTheme);
175
+ const pluginCss = getCombinedDefaultCss();
176
+ // Structure decorations for editor (simulating heading labels / counters)
177
+ let structureCss = "";
178
+ if (style.structure.numberedHeadings) {
179
+ const labelPrefix = style.structure.headingLabel ? `"${style.structure.headingLabel} " ` : "";
180
+ structureCss += `
181
+ .milkdown .editor {
182
+ counter-reset: wx-heading-counter;
183
+ }
184
+ .milkdown .editor h2::before {
185
+ counter-increment: wx-heading-counter;
186
+ content: ${labelPrefix}counter(wx-heading-counter, decimal-leading-zero) " ";
187
+ margin-right: 0.3em;
188
+ opacity: 0.7;
189
+ font-weight: 600;
190
+ }
191
+ `;
192
+ }
193
+ else if (style.structure.headingLabel) {
194
+ structureCss += `
195
+ .milkdown .editor h2::before {
196
+ content: "${style.structure.headingLabel} ";
197
+ margin-right: 0.3em;
198
+ opacity: 0.7;
199
+ font-weight: 600;
200
+ }
201
+ `;
202
+ }
203
+ if (style.structure.quoteLabel) {
204
+ structureCss += `
205
+ .milkdown .editor blockquote::before {
206
+ content: "${style.structure.quoteLabel}";
207
+ display: block;
208
+ font-size: 11px;
209
+ font-weight: 600;
210
+ letter-spacing: 0.05em;
211
+ color: var(--wx-brand-ink, var(--wx-brand-accent, #ca6093));
212
+ margin-bottom: 6px;
213
+ text-transform: uppercase;
214
+ }
215
+ `;
216
+ }
217
+ styleTag.textContent = `
218
+ /* ── Crepe resets: 后置于运行时,同特异性即可覆盖 Crepe 主题;不可用 !important,否则压过下方预设 ── */
219
+ .milkdown .ProseMirror {
220
+ padding: 0;
221
+ }
222
+ .milkdown .ProseMirror h1,
223
+ .milkdown .ProseMirror h2,
224
+ .milkdown .ProseMirror h3,
225
+ .milkdown .ProseMirror h4,
226
+ .milkdown .ProseMirror h5,
227
+ .milkdown .ProseMirror h6 {
228
+ padding-top: 0;
229
+ padding-bottom: 0;
230
+ }
231
+ .milkdown .ProseMirror p {
232
+ padding: 0;
233
+ }
234
+ .milkdown .ProseMirror blockquote {
235
+ position: static;
236
+ box-sizing: border-box;
237
+ }
238
+ .milkdown .ProseMirror blockquote::before {
239
+ display: none;
240
+ }
241
+
242
+ /* ── Dynamic WeChat Theme Variables & Container Styling ── */
243
+ ${themeCss}
244
+
245
+ /* ── Structure Enhancements (Numbered Headings, Chapter / Quote Labels) ── */
246
+ ${structureCss}
247
+
248
+ /* ── Article Plugins Styling (Callouts, Badges, Kbd, Article Cards, etc.) ── */
249
+ ${pluginCss}
250
+
251
+ /* ── Active Syntax Presets & Custom CSS Overrides ── */
252
+ ${customCss}
253
+ `;
254
+ }
255
+ function refreshLiveEditorStyles() {
256
+ const style = readStyleControls();
257
+ const account = accounts[currentAccount];
258
+ const customCss = [
259
+ buildCombinedPresetsCss(currentSyntaxPresets),
260
+ account?.articleThemeCss,
261
+ inputCustomCss.value,
262
+ ].filter(Boolean).join("\n\n");
263
+ applyLiveEditorStyles(style, customCss);
264
+ }
265
+ // ── Inlined WeChat Render Pass ──────────────────────────────────────────
266
+ async function doRender() {
267
+ if (rendering)
268
+ return;
269
+ rendering = true;
270
+ const revision = renderRevision;
271
+ const rawMd = getActiveMarkdown();
272
+ const markdown = stripLeadingH1(extractFrontmatter(rawMd).content).trimStart();
273
+ const style = readStyleControls();
274
+ const account = accounts[currentAccount];
275
+ const customCss = [
276
+ buildCombinedPresetsCss(currentSyntaxPresets),
277
+ account?.articleThemeCss,
278
+ inputCustomCss.value,
279
+ ].filter(Boolean).join("\n\n");
280
+ applyLiveEditorStyles(style, customCss);
281
+ updateStats();
282
+ try {
283
+ let semanticHtml = "";
284
+ if (currentEditorMode === "visual" && mynoEditor) {
285
+ const crepe = mynoEditor.getCrepe();
286
+ if (crepe) {
287
+ try {
288
+ semanticHtml = crepe.editor.action(getHTML());
289
+ }
290
+ catch {
291
+ // ignore and fallback
292
+ }
293
+ }
294
+ }
295
+ if (!semanticHtml) {
296
+ const hiddenHost = document.getElementById("studio-editor-host") || document.createElement("div");
297
+ hiddenHost.innerHTML = "";
298
+ const editor = Editor.make()
299
+ .config((ctx) => {
300
+ ctx.set(rootCtx, hiddenHost);
301
+ ctx.set(defaultValueCtx, markdown);
302
+ })
303
+ .use(commonmark)
304
+ .use(gfm)
305
+ .use(getCombinedMilkdownPlugins());
306
+ try {
307
+ await editor.create();
308
+ semanticHtml = editor.action(getHTML());
309
+ }
310
+ finally {
311
+ await editor.destroy();
312
+ }
313
+ }
314
+ const inlinedHtml = await renderWechatHtml({
315
+ semanticHtml,
316
+ baseCss: articleCss,
317
+ customCss,
318
+ structure: style.structure,
319
+ editorVars: style.editorVars,
320
+ theme: style.exportTheme,
321
+ strictImageDecode: false,
322
+ });
323
+ if (revision !== renderRevision)
324
+ return;
325
+ latestWechatHtml = inlinedHtml;
326
+ if (previewScreen)
327
+ previewScreen.innerHTML = inlinedHtml;
328
+ if (htmlSourceCode)
329
+ htmlSourceCode.value = inlinedHtml;
330
+ }
331
+ catch (error) {
332
+ if (revision !== renderRevision)
333
+ return;
334
+ console.error("myno render error:", error);
335
+ const message = error instanceof Error ? error.message : String(error);
336
+ if (previewScreen)
337
+ previewScreen.textContent = `渲染错误: ${message}`;
338
+ if (htmlSourceCode)
339
+ htmlSourceCode.value = "";
340
+ }
341
+ finally {
342
+ rendering = false;
343
+ if (revision !== renderRevision) {
344
+ if (debounceTimer)
345
+ clearTimeout(debounceTimer);
346
+ void doRender();
347
+ }
348
+ }
349
+ }
350
+ function scheduleRender(delay = 60) {
351
+ renderRevision += 1;
352
+ latestWechatHtml = "";
353
+ refreshLiveEditorStyles();
354
+ if (debounceTimer)
355
+ clearTimeout(debounceTimer);
356
+ debounceTimer = setTimeout(() => {
357
+ void doRender();
358
+ }, delay);
359
+ }
360
+ // ── Style Packs (One-Click Application) ──────────────────────────────────
361
+ function renderStylePacks() {
362
+ const container = document.getElementById("style-packs-grid");
363
+ if (!container)
364
+ return;
365
+ container.innerHTML = "";
366
+ for (const pack of STYLE_PACKS) {
367
+ const card = document.createElement("div");
368
+ card.className = "style-pack-card";
369
+ card.dataset.packId = pack.id;
370
+ const head = document.createElement("div");
371
+ head.className = "style-pack-head";
372
+ const titleWrap = document.createElement("div");
373
+ const name = document.createElement("span");
374
+ name.className = "style-pack-name";
375
+ name.textContent = pack.name;
376
+ const sub = document.createElement("span");
377
+ sub.className = "style-pack-subtitle";
378
+ sub.textContent = ` · ${pack.subtitle}`;
379
+ titleWrap.appendChild(name);
380
+ titleWrap.appendChild(sub);
381
+ const swatches = document.createElement("div");
382
+ swatches.className = "style-pack-swatches";
383
+ for (const color of pack.swatch) {
384
+ const dot = document.createElement("span");
385
+ dot.className = "swatch-dot";
386
+ dot.style.background = color;
387
+ swatches.appendChild(dot);
388
+ }
389
+ head.appendChild(titleWrap);
390
+ head.appendChild(swatches);
391
+ card.appendChild(head);
392
+ const desc = document.createElement("div");
393
+ desc.className = "style-pack-desc";
394
+ desc.textContent = pack.description;
395
+ card.appendChild(desc);
396
+ card.addEventListener("click", () => {
397
+ applyStylePack(pack);
398
+ document.querySelectorAll(".style-pack-card").forEach((c) => c.classList.remove("active"));
399
+ card.classList.add("active");
400
+ showToast(`已套用「${pack.name}」风格套系`, "info");
401
+ });
402
+ container.appendChild(card);
403
+ }
404
+ }
405
+ function applyStylePack(pack) {
406
+ const t = pack.theme;
407
+ // Font Size
408
+ inputFontSize.value = t.fontSize;
409
+ valFontSize.textContent = `${t.fontSize}px`;
410
+ syncSegmentedState("seg-font-size", t.fontSize);
411
+ // Line Height
412
+ inputLineHeight.value = t.lineHeight;
413
+ valLineHeight.textContent = t.lineHeight;
414
+ syncSegmentedState("seg-line-height", t.lineHeight);
415
+ // Colors
416
+ pickerBrandColor.value = hexBrandColor.value = t.brandColor;
417
+ pickerTextColor.value = hexTextColor.value = t.textColor;
418
+ pickerH2Color.value = hexH2Color.value = t.h2Color;
419
+ pickerH3Color.value = hexH3Color.value = t.h3Color;
420
+ pickerQuoteColor.value = hexQuoteColor.value = t.quoteColor;
421
+ pickerDividerColor.value = hexDividerColor.value = t.dividerColor;
422
+ syncSwatchActiveState(t.brandColor);
423
+ // H2 Preset
424
+ currentSyntaxPresets.h2 = t.h2Preset;
425
+ syncPillActiveState("seg-h2-preset", t.h2Preset);
426
+ // H3 Preset
427
+ if (t.h3Preset)
428
+ currentSyntaxPresets.h3 = t.h3Preset;
429
+ // Numbered Headings
430
+ switchNumberedHeadings.checked = t.numberedHeadings;
431
+ scheduleRender(20);
432
+ }
433
+ function syncSegmentedState(containerId, value) {
434
+ const container = document.getElementById(containerId);
435
+ if (!container)
436
+ return;
437
+ container.querySelectorAll(".seg-btn").forEach((btn) => {
438
+ const el = btn;
439
+ if (el.dataset.val === value) {
440
+ el.classList.add("active");
441
+ }
442
+ else {
443
+ el.classList.remove("active");
444
+ }
445
+ });
446
+ }
447
+ function syncPillActiveState(containerId, presetId) {
448
+ const container = document.getElementById(containerId);
449
+ if (!container)
450
+ return;
451
+ container.querySelectorAll(".pill-btn").forEach((btn) => {
452
+ const el = btn;
453
+ if (el.dataset.preset === presetId) {
454
+ el.classList.add("active");
455
+ }
456
+ else {
457
+ el.classList.remove("active");
458
+ }
459
+ });
460
+ }
461
+ function syncSwatchActiveState(hexColor) {
462
+ document.querySelectorAll(".swatch-btn").forEach((btn) => {
463
+ const el = btn;
464
+ if (el.dataset.color?.toLowerCase() === hexColor.toLowerCase()) {
465
+ el.style.transform = "scale(1.2)";
466
+ el.style.boxShadow = "0 0 0 2px var(--brand)";
467
+ }
468
+ else {
469
+ el.style.transform = "";
470
+ el.style.boxShadow = "0 0 0 1px var(--border)";
471
+ }
472
+ });
473
+ }
474
+ // ── Bind Segmented Controls ─────────────────────────────────────────────
475
+ function initSegmentedControls() {
476
+ // Font Size
477
+ const segFontSize = document.getElementById("seg-font-size");
478
+ segFontSize?.querySelectorAll(".seg-btn").forEach((btn) => {
479
+ btn.addEventListener("click", () => {
480
+ const val = btn.dataset.val || "16";
481
+ inputFontSize.value = val;
482
+ valFontSize.textContent = `${val}px`;
483
+ syncSegmentedState("seg-font-size", val);
484
+ scheduleRender(20);
485
+ });
486
+ });
487
+ // Line Height
488
+ const segLineHeight = document.getElementById("seg-line-height");
489
+ segLineHeight?.querySelectorAll(".seg-btn").forEach((btn) => {
490
+ btn.addEventListener("click", () => {
491
+ const val = btn.dataset.val || "1.84";
492
+ inputLineHeight.value = val;
493
+ valLineHeight.textContent = val;
494
+ syncSegmentedState("seg-line-height", val);
495
+ scheduleRender(20);
496
+ });
497
+ });
498
+ // Quick Swatches
499
+ document.querySelectorAll(".swatch-btn").forEach((btn) => {
500
+ btn.addEventListener("click", () => {
501
+ const color = btn.dataset.color || "#ca6093";
502
+ pickerBrandColor.value = hexBrandColor.value = color;
503
+ syncSwatchActiveState(color);
504
+ scheduleRender(20);
505
+ });
506
+ });
507
+ // H2 Pills
508
+ const segH2 = document.getElementById("seg-h2-preset");
509
+ segH2?.querySelectorAll(".pill-btn").forEach((btn) => {
510
+ btn.addEventListener("click", () => {
511
+ const preset = btn.dataset.preset || "pillar";
512
+ currentSyntaxPresets.h2 = preset;
513
+ syncPillActiveState("seg-h2-preset", preset);
514
+ scheduleRender(20);
515
+ });
516
+ });
517
+ }
518
+ // ── Syntax Presets in Accordion ─────────────────────────────────────────
519
+ function renderSyntaxPresetControls() {
520
+ const container = document.getElementById("syntax-preset-container");
521
+ if (!container)
522
+ return;
523
+ container.innerHTML = "";
524
+ for (const cat of SYNTAX_CATEGORIES) {
525
+ if (cat.key === "h2")
526
+ continue; // H2 has its own pills at top level
527
+ const group = document.createElement("div");
528
+ group.className = "control-group";
529
+ const header = document.createElement("div");
530
+ header.className = "control-label";
531
+ const title = document.createElement("span");
532
+ title.textContent = `${cat.icon} ${cat.label}`;
533
+ header.appendChild(title);
534
+ const hasMultiplePresets = Boolean(cat.presets && cat.presets.length > 1);
535
+ if (hasMultiplePresets && cat.presets) {
536
+ const select = document.createElement("select");
537
+ select.className = "select-control full-width";
538
+ for (const preset of cat.presets) {
539
+ const opt = document.createElement("option");
540
+ opt.value = preset.id;
541
+ opt.textContent = preset.name;
542
+ select.appendChild(opt);
543
+ }
544
+ const activePresetId = currentSyntaxPresets[cat.key] || cat.defaultPresetId || cat.presets[0].id;
545
+ select.value = activePresetId;
546
+ select.addEventListener("change", () => {
547
+ currentSyntaxPresets[cat.key] = select.value;
548
+ scheduleRender(20);
549
+ });
550
+ group.appendChild(header);
551
+ group.appendChild(select);
552
+ }
553
+ else {
554
+ group.appendChild(header);
555
+ }
556
+ container.appendChild(group);
557
+ }
558
+ }
559
+ // ── Tab 2: Syntax Cards ─────────────────────────────────────────────────
560
+ function renderSyntaxCards(plugins) {
561
+ const container = document.getElementById("syntax-card-list");
562
+ if (!container)
563
+ return;
564
+ container.innerHTML = "";
565
+ for (const doc of plugins) {
566
+ const card = document.createElement("div");
567
+ card.className = "syntax-card";
568
+ const header = document.createElement("div");
569
+ header.className = "syntax-card-header";
570
+ const title = document.createElement("span");
571
+ title.className = "syntax-card-title";
572
+ title.textContent = doc.name;
573
+ const badge = document.createElement("span");
574
+ badge.className = "syntax-card-badge";
575
+ badge.textContent = doc.category === "inline" ? "行内扩展" : "块级组件";
576
+ header.appendChild(title);
577
+ header.appendChild(badge);
578
+ card.appendChild(header);
579
+ const desc = document.createElement("p");
580
+ desc.className = "syntax-card-desc";
581
+ desc.textContent = doc.description;
582
+ card.appendChild(desc);
583
+ const code = document.createElement("pre");
584
+ code.className = "syntax-code-snippet";
585
+ code.textContent = doc.sampleMarkdown;
586
+ card.appendChild(code);
587
+ const insertBtn = document.createElement("button");
588
+ insertBtn.className = "syntax-btn-insert";
589
+ insertBtn.textContent = "+ 插入示例到当前文章";
590
+ insertBtn.addEventListener("click", () => {
591
+ insertTextAtCursor(doc.sampleMarkdown);
592
+ showToast(`已插入「${doc.name}」示例`, "info");
593
+ });
594
+ card.appendChild(insertBtn);
595
+ container.appendChild(card);
596
+ }
597
+ }
598
+ function insertTextAtCursor(text) {
599
+ if (currentEditorMode === "visual" && mynoEditor) {
600
+ const current = mynoEditor.getMarkdown();
601
+ const updated = `${current.trimEnd()}\n\n${text}\n\n`;
602
+ mynoEditor.setMarkdown(updated);
603
+ editorMarkdown.value = updated;
604
+ }
605
+ else {
606
+ const start = editorMarkdown.selectionStart;
607
+ const end = editorMarkdown.selectionEnd;
608
+ const original = editorMarkdown.value;
609
+ const insertText = `\n\n${text}\n\n`;
610
+ editorMarkdown.value = original.slice(0, start) + insertText + original.slice(end);
611
+ editorMarkdown.focus();
612
+ editorMarkdown.selectionStart = editorMarkdown.selectionEnd = start + insertText.length;
613
+ }
614
+ scheduleRender(20);
615
+ }
616
+ // ── Account & Config Loading ────────────────────────────────────────────
617
+ function applyAccountConfig(accountKey) {
618
+ const account = accounts[accountKey];
619
+ if (!account)
620
+ return;
621
+ const standalone = Boolean(account.articleThemeStandalone);
622
+ configModeBadge.textContent = standalone ? "独立排版文件" : "账号默认配置";
623
+ configModeBadge.className = `badge ${standalone ? "badge-primary" : "badge-subtle"}`;
624
+ btnSave.disabled = Boolean(account.articleThemeStandalone);
625
+ btnSave.title = account.articleThemeStandalone
626
+ ? "当前账号引用独立配置,请通过「导出配置」保存调整后替换原文件"
627
+ : "保存当前调整至本地 config.json 的当前账号";
628
+ const exportTheme = account.theme?.exportTheme || {};
629
+ const editorVars = account.theme?.editorVars || {};
630
+ // Font Size
631
+ const fontSize = exportTheme.bodyFontSize?.replace(/px$/, "") || "16";
632
+ inputFontSize.value = fontSize;
633
+ valFontSize.textContent = `${fontSize}px`;
634
+ syncSegmentedState("seg-font-size", fontSize);
635
+ // Line Height
636
+ const lineHeight = exportTheme.bodyLineHeight || "1.84";
637
+ inputLineHeight.value = lineHeight;
638
+ valLineHeight.textContent = lineHeight;
639
+ syncSegmentedState("seg-line-height", lineHeight);
640
+ // Letter & Paragraph Spacing
641
+ inputLetterSpacing.value = String(parseFloat(exportTheme.bodyLetterSpacing || "0.012em"));
642
+ valLetterSpacing.textContent = `${inputLetterSpacing.value}em`;
643
+ inputParaSpacing.value = String(parseFloat(exportTheme.bodyParagraphSpacing || "1.18em"));
644
+ valParaSpacing.textContent = `${inputParaSpacing.value}em`;
645
+ // Colors
646
+ const brand = editorVars["--brand"] || exportTheme.primaryColor || "#ca6093";
647
+ pickerBrandColor.value = hexBrandColor.value = brand;
648
+ syncSwatchActiveState(brand);
649
+ const text = exportTheme.bodyColor || editorVars["--text"] || "#292526";
650
+ pickerTextColor.value = hexTextColor.value = text;
651
+ if (exportTheme.h2Color)
652
+ pickerH2Color.value = hexH2Color.value = exportTheme.h2Color;
653
+ if (exportTheme.h3Color)
654
+ pickerH3Color.value = hexH3Color.value = exportTheme.h3Color;
655
+ const quote = exportTheme.blockquoteBorderColor || brand;
656
+ pickerQuoteColor.value = hexQuoteColor.value = quote;
657
+ const divider = exportTheme.dividerColor || editorVars["--divider"] || "#e2dcdf";
658
+ pickerDividerColor.value = hexDividerColor.value = divider;
659
+ inputCustomCss.value = account.customCssContent || "";
660
+ // Syntax Presets
661
+ const savedPresets = account.theme?.syntaxPresets || {};
662
+ currentSyntaxPresets = {};
663
+ for (const cat of SYNTAX_CATEGORIES) {
664
+ if (cat.defaultPresetId) {
665
+ currentSyntaxPresets[cat.key] = savedPresets[cat.key] || cat.defaultPresetId;
666
+ }
667
+ }
668
+ syncPillActiveState("seg-h2-preset", currentSyntaxPresets.h2 || "pillar");
669
+ renderSyntaxPresetControls();
670
+ // Structure
671
+ const structure = account.theme?.structure || {};
672
+ switchNumberedHeadings.checked = Boolean(structure.numberedHeadings);
673
+ inputHeadingLabel.value = structure.headingLabel || "";
674
+ inputQuoteLabel.value = structure.quoteLabel || "";
675
+ scheduleRender(20);
676
+ }
677
+ // ── Initialize Color Palettes Dropdown ───────────────────────────────────
678
+ function initColorPalettes() {
679
+ if (!paletteSelect)
680
+ return;
681
+ paletteSelect.innerHTML = `<option value="">-- 选择推荐调色盘 (一键应用) --</option>`;
682
+ for (const p of COLOR_PALETTES) {
683
+ const opt = document.createElement("option");
684
+ opt.value = p.id;
685
+ opt.textContent = `${p.name} · ${p.category}`;
686
+ paletteSelect.appendChild(opt);
687
+ }
688
+ paletteSelect.addEventListener("change", () => {
689
+ const pal = COLOR_PALETTES.find((p) => p.id === paletteSelect.value);
690
+ if (!pal)
691
+ return;
692
+ pickerBrandColor.value = hexBrandColor.value = pal.colors.brand;
693
+ pickerTextColor.value = hexTextColor.value = pal.colors.text;
694
+ pickerH2Color.value = hexH2Color.value = pal.colors.h2;
695
+ pickerH3Color.value = hexH3Color.value = pal.colors.h3;
696
+ pickerQuoteColor.value = hexQuoteColor.value = pal.colors.quote;
697
+ pickerDividerColor.value = hexDividerColor.value = pal.colors.divider;
698
+ syncSwatchActiveState(pal.colors.brand);
699
+ if (paletteDesc)
700
+ paletteDesc.textContent = pal.description;
701
+ showToast(`已套用「${pal.name}」色盘`, "info");
702
+ scheduleRender(20);
703
+ });
704
+ }
705
+ // ── Studio Main Initialization ──────────────────────────────────────────
706
+ async function initStudio() {
707
+ initColorPalettes();
708
+ initSegmentedControls();
709
+ renderStylePacks();
710
+ // Bind Color Pairs
711
+ bindColorPair(pickerBrandColor, hexBrandColor, () => {
712
+ syncSwatchActiveState(pickerBrandColor.value);
713
+ scheduleRender(20);
714
+ });
715
+ bindColorPair(pickerTextColor, hexTextColor, () => scheduleRender(20));
716
+ bindColorPair(pickerH2Color, hexH2Color, () => scheduleRender(20));
717
+ bindColorPair(pickerH3Color, hexH3Color, () => scheduleRender(20));
718
+ bindColorPair(pickerQuoteColor, hexQuoteColor, () => scheduleRender(20));
719
+ bindColorPair(pickerDividerColor, hexDividerColor, () => scheduleRender(20));
720
+ try {
721
+ const res = await fetch("/api/studio/config");
722
+ if (!res.ok)
723
+ throw new Error(`HTTP ${res.status}`);
724
+ const data = (await res.json());
725
+ accounts = data.accounts || {};
726
+ currentAccount = data.defaultAccount || "default";
727
+ pluginsList = data.plugins || [];
728
+ // Account selector
729
+ accountSelect.innerHTML = "";
730
+ for (const [key, acc] of Object.entries(accounts)) {
731
+ const opt = document.createElement("option");
732
+ opt.value = key;
733
+ opt.textContent = `${acc.name || key} (${key})`;
734
+ if (key === currentAccount)
735
+ opt.selected = true;
736
+ accountSelect.appendChild(opt);
737
+ }
738
+ const initialMd = data.sampleMarkdown || "# 微信排版标题\n\n欢迎使用 myno 微信排版工作台!";
739
+ editorMarkdown.value = initialMd;
740
+ // Mount MynoEditor (Milkdown Crepe)
741
+ if (milkdownEditorRoot) {
742
+ mynoEditor = new MynoEditor({
743
+ root: milkdownEditorRoot,
744
+ defaultValue: initialMd,
745
+ onMarkdownChange: (md) => {
746
+ editorMarkdown.value = md;
747
+ scheduleRender(40);
748
+ },
749
+ });
750
+ await mynoEditor.create();
751
+ }
752
+ renderSyntaxCards(pluginsList);
753
+ applyAccountConfig(currentAccount);
754
+ }
755
+ catch (err) {
756
+ console.error("Failed to load studio config:", err);
757
+ showToast("无法加载服务器配置,使用本地默认设置", "error");
758
+ scheduleRender(20);
759
+ }
760
+ }
761
+ // ── Event Handlers ──────────────────────────────────────────────────────
762
+ accountSelect.addEventListener("change", () => {
763
+ currentAccount = accountSelect.value;
764
+ applyAccountConfig(currentAccount);
765
+ });
766
+ editorMarkdown.addEventListener("input", () => {
767
+ // 源码模式下 textarea 是编辑面;视觉模式下它隐藏,但仍可能被脚本/按钮更新,需回同步给 Crepe
768
+ if (mynoEditor) {
769
+ mynoEditor.setMarkdown(editorMarkdown.value);
770
+ }
771
+ scheduleRender(40);
772
+ });
773
+ // Dual Mode Switcher
774
+ btnModeVisual.addEventListener("click", () => {
775
+ if (currentEditorMode === "visual")
776
+ return;
777
+ currentEditorMode = "visual";
778
+ btnModeVisual.classList.add("active");
779
+ btnModeSource.classList.remove("active");
780
+ milkdownEditorRoot.style.display = "block";
781
+ editorMarkdown.style.display = "none";
782
+ if (mynoEditor) {
783
+ mynoEditor.setMarkdown(editorMarkdown.value);
784
+ }
785
+ refreshLiveEditorStyles();
786
+ showToast("已切换至所见即所得编辑模式", "info");
787
+ });
788
+ btnModeSource.addEventListener("click", () => {
789
+ if (currentEditorMode === "source")
790
+ return;
791
+ currentEditorMode = "source";
792
+ btnModeSource.classList.add("active");
793
+ btnModeVisual.classList.remove("active");
794
+ if (mynoEditor) {
795
+ editorMarkdown.value = mynoEditor.getMarkdown();
796
+ }
797
+ milkdownEditorRoot.style.display = "none";
798
+ editorMarkdown.style.display = "block";
799
+ editorMarkdown.focus();
800
+ showToast("已切换至 Markdown 源码模式", "info");
801
+ });
802
+ // Sliders and Text Inputs
803
+ inputFontSize.addEventListener("input", () => {
804
+ valFontSize.textContent = `${inputFontSize.value}px`;
805
+ syncSegmentedState("seg-font-size", inputFontSize.value);
806
+ scheduleRender(20);
807
+ });
808
+ inputLineHeight.addEventListener("input", () => {
809
+ valLineHeight.textContent = inputLineHeight.value;
810
+ syncSegmentedState("seg-line-height", inputLineHeight.value);
811
+ scheduleRender(20);
812
+ });
813
+ inputLetterSpacing.addEventListener("input", () => {
814
+ valLetterSpacing.textContent = `${inputLetterSpacing.value}em`;
815
+ scheduleRender(20);
816
+ });
817
+ inputParaSpacing.addEventListener("input", () => {
818
+ valParaSpacing.textContent = `${inputParaSpacing.value}em`;
819
+ scheduleRender(20);
820
+ });
821
+ switchNumberedHeadings.addEventListener("change", () => scheduleRender(20));
822
+ inputHeadingLabel.addEventListener("input", () => scheduleRender(40));
823
+ inputQuoteLabel.addEventListener("input", () => scheduleRender(40));
824
+ inputCustomCss.addEventListener("input", () => scheduleRender(60));
825
+ // Tabs
826
+ const tabBtnStyle = document.getElementById("tab-btn-style");
827
+ const tabBtnSyntax = document.getElementById("tab-btn-syntax");
828
+ const tabContentStyle = document.getElementById("tab-content-style");
829
+ const tabContentSyntax = document.getElementById("tab-content-syntax");
830
+ tabBtnStyle?.addEventListener("click", () => {
831
+ tabBtnStyle.classList.add("active");
832
+ tabBtnSyntax.classList.remove("active");
833
+ tabContentStyle.style.display = "block";
834
+ tabContentSyntax.style.display = "none";
835
+ });
836
+ tabBtnSyntax?.addEventListener("click", () => {
837
+ tabBtnSyntax.classList.add("active");
838
+ tabBtnStyle.classList.remove("active");
839
+ tabContentStyle.style.display = "none";
840
+ tabContentSyntax.style.display = "block";
841
+ });
842
+ // Load sample / clear
843
+ btnLoadSample?.addEventListener("click", async () => {
844
+ const res = await fetch("/api/studio/config");
845
+ const data = (await res.json());
846
+ const sample = data.sampleMarkdown || "";
847
+ editorMarkdown.value = sample;
848
+ if (mynoEditor)
849
+ mynoEditor.setMarkdown(sample);
850
+ scheduleRender(20);
851
+ showToast("已加载全语法官方测试稿", "info");
852
+ });
853
+ btnClearMd?.addEventListener("click", () => {
854
+ editorMarkdown.value = "";
855
+ if (mynoEditor)
856
+ mynoEditor.setMarkdown("");
857
+ scheduleRender(20);
858
+ });
859
+ // Toggle HTML view
860
+ btnToggleView?.addEventListener("click", () => {
861
+ showingHtmlSource = !showingHtmlSource;
862
+ if (showingHtmlSource) {
863
+ htmlSourceContainer.style.display = "block";
864
+ btnToggleView.textContent = "📱 返回视图";
865
+ }
866
+ else {
867
+ htmlSourceContainer.style.display = "none";
868
+ btnToggleView.textContent = "🔍 HTML 源码";
869
+ }
870
+ });
871
+ // Reset
872
+ btnReset?.addEventListener("click", () => {
873
+ applyAccountConfig(currentAccount);
874
+ showToast("已重置为当前账号默认样式", "info");
875
+ });
876
+ // Save to config.json
877
+ btnSave?.addEventListener("click", async () => {
878
+ const account = accounts[currentAccount];
879
+ if (!account)
880
+ return;
881
+ const { structure, ...theme } = readStyleControls();
882
+ const { headerImageUrl: _headerImageUrl, ...savedStructure } = structure;
883
+ const payload = {
884
+ account: currentAccount,
885
+ ...theme,
886
+ syntaxPresets: { ...currentSyntaxPresets },
887
+ customCss: inputCustomCss.value,
888
+ structure: savedStructure,
889
+ };
890
+ try {
891
+ const res = await fetch("/api/studio/save-config", {
892
+ method: "POST",
893
+ headers: { "content-type": "application/json" },
894
+ body: JSON.stringify(payload),
895
+ });
896
+ const data = await res.json();
897
+ if (res.ok && data.ok) {
898
+ account.theme = { ...theme, structure, syntaxPresets: payload.syntaxPresets };
899
+ account.customCssContent = payload.customCss;
900
+ showToast("✅ 配置已成功保存至本地 config.json!", "success");
901
+ }
902
+ else {
903
+ showToast(`保存失败: ${data.error || "未知错误"}`, "error");
904
+ }
905
+ }
906
+ catch (err) {
907
+ showToast(`网络请求失败: ${String(err)}`, "error");
908
+ }
909
+ });
910
+ // Export Config (wechat-style.json)
911
+ btnExportConfig?.addEventListener("click", () => {
912
+ const account = accounts[currentAccount];
913
+ if (!account)
914
+ return;
915
+ const styleConfig = {
916
+ schemaVersion: 1,
917
+ engineVersion: 1,
918
+ id: standaloneThemeName || "wechat-style",
919
+ name: "myno WeChat 排版配置",
920
+ version: "1.0.0",
921
+ ...readStyleControls(),
922
+ syntaxPresets: { ...currentSyntaxPresets },
923
+ css: [account.articleThemeCss, inputCustomCss.value].filter(Boolean).join("\n\n"),
924
+ };
925
+ const { url, filename } = exportStyleConfigBlob(styleConfig);
926
+ const link = document.createElement("a");
927
+ link.href = url;
928
+ link.download = filename;
929
+ link.click();
930
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
931
+ showToast("已导出独立排版文件,CLI 使用 --article-theme 引用该文件", "success");
932
+ });
933
+ // Import Config (wechat-style.json)
934
+ btnImportConfig?.addEventListener("click", () => {
935
+ fileImportConfig.click();
936
+ });
937
+ fileImportConfig?.addEventListener("change", () => {
938
+ const file = fileImportConfig.files?.[0];
939
+ if (!file)
940
+ return;
941
+ const reader = new FileReader();
942
+ reader.onload = () => {
943
+ const content = reader.result;
944
+ const result = parseStyleConfig(content);
945
+ if (!result.ok) {
946
+ showToast(`导入失败: ${result.error}`, "error");
947
+ return;
948
+ }
949
+ const cfg = result.config;
950
+ standaloneThemeName = cfg.id || file.name.replace(/\.json$/i, "");
951
+ // Apply values to UI
952
+ if (cfg.exportTheme.bodyFontSize) {
953
+ const fs = cfg.exportTheme.bodyFontSize.replace(/px$/, "");
954
+ inputFontSize.value = fs;
955
+ valFontSize.textContent = `${fs}px`;
956
+ syncSegmentedState("seg-font-size", fs);
957
+ }
958
+ if (cfg.exportTheme.bodyLineHeight) {
959
+ inputLineHeight.value = cfg.exportTheme.bodyLineHeight;
960
+ valLineHeight.textContent = cfg.exportTheme.bodyLineHeight;
961
+ syncSegmentedState("seg-line-height", cfg.exportTheme.bodyLineHeight);
962
+ }
963
+ if (cfg.exportTheme.primaryColor || cfg.editorVars["--brand"]) {
964
+ const brand = cfg.exportTheme.primaryColor || cfg.editorVars["--brand"];
965
+ pickerBrandColor.value = hexBrandColor.value = brand;
966
+ syncSwatchActiveState(brand);
967
+ }
968
+ if (cfg.exportTheme.bodyColor || cfg.editorVars["--text"]) {
969
+ pickerTextColor.value = hexTextColor.value = cfg.exportTheme.bodyColor || cfg.editorVars["--text"];
970
+ }
971
+ if (cfg.exportTheme.h2Color)
972
+ pickerH2Color.value = hexH2Color.value = cfg.exportTheme.h2Color;
973
+ if (cfg.exportTheme.h3Color)
974
+ pickerH3Color.value = hexH3Color.value = cfg.exportTheme.h3Color;
975
+ if (cfg.exportTheme.blockquoteBorderColor)
976
+ pickerQuoteColor.value = hexQuoteColor.value = cfg.exportTheme.blockquoteBorderColor;
977
+ if (cfg.exportTheme.dividerColor)
978
+ pickerDividerColor.value = hexDividerColor.value = cfg.exportTheme.dividerColor;
979
+ if (cfg.syntaxPresets) {
980
+ currentSyntaxPresets = { ...cfg.syntaxPresets };
981
+ if (currentSyntaxPresets.h2)
982
+ syncPillActiveState("seg-h2-preset", currentSyntaxPresets.h2);
983
+ }
984
+ if (cfg.structure) {
985
+ switchNumberedHeadings.checked = Boolean(cfg.structure.numberedHeadings);
986
+ inputHeadingLabel.value = cfg.structure.headingLabel || "";
987
+ inputQuoteLabel.value = cfg.structure.quoteLabel || "";
988
+ }
989
+ if (cfg.css) {
990
+ inputCustomCss.value = cfg.css;
991
+ }
992
+ configModeBadge.textContent = `独立配置: ${file.name}`;
993
+ configModeBadge.className = "badge badge-primary";
994
+ showToast(`✅ 成功导入独立配置: ${file.name}`, "success");
995
+ scheduleRender(20);
996
+ };
997
+ reader.readAsText(file);
998
+ });
999
+ // Copy to WeChat
1000
+ btnCopy?.addEventListener("click", async () => {
1001
+ if (!latestWechatHtml) {
1002
+ showToast("文章内容为空", "error");
1003
+ return;
1004
+ }
1005
+ try {
1006
+ const blobHtml = new Blob([latestWechatHtml], { type: "text/html" });
1007
+ const blobText = new Blob([latestWechatHtml], { type: "text/plain" });
1008
+ await navigator.clipboard.write([
1009
+ new ClipboardItem({
1010
+ "text/html": blobHtml,
1011
+ "text/plain": blobText,
1012
+ }),
1013
+ ]);
1014
+ showToast("📋 已复制富文本!可在微信公众号后台直接粘贴 (Cmd+V)", "success");
1015
+ }
1016
+ catch (err) {
1017
+ console.error("Clipboard write error:", err);
1018
+ const listener = (e) => {
1019
+ e.clipboardData?.setData("text/html", latestWechatHtml);
1020
+ e.clipboardData?.setData("text/plain", latestWechatHtml);
1021
+ e.preventDefault();
1022
+ };
1023
+ document.addEventListener("copy", listener);
1024
+ document.execCommand("copy");
1025
+ document.removeEventListener("copy", listener);
1026
+ showToast("📋 已复制富文本(后备通道)", "success");
1027
+ }
1028
+ });
1029
+ // Start
1030
+ void initStudio();
1031
+ //# sourceMappingURL=studio.js.map