@scaffold-ui-kit/preview 0.0.0-stage → 2.3.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 (350) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1868 -2
  3. package/application/host/bootstrap.ts +43 -0
  4. package/application/host/main.ts +31 -0
  5. package/application/host/shell-host.ts +47 -0
  6. package/application/index.html +12 -0
  7. package/application/lib/base-url.ts +15 -0
  8. package/application/lib/no-code-block.ts +7 -0
  9. package/application/lib/no-setup.ts +3 -0
  10. package/application/lib/no-views.ts +6 -0
  11. package/application/preview/code/code.html +12 -0
  12. package/application/preview/code/main.ts +89 -0
  13. package/application/preview/globals.ts +254 -0
  14. package/application/preview/html/main.ts +93 -0
  15. package/application/preview/html/preview.html +12 -0
  16. package/application/preview/react/main.tsx +146 -0
  17. package/application/preview/react/preview.html +12 -0
  18. package/application/preview/svelte/Preview.svelte +104 -0
  19. package/application/preview/svelte/main.ts +13 -0
  20. package/application/preview/svelte/preview.html +12 -0
  21. package/application/preview/svelte/story-props.ts +172 -0
  22. package/application/preview/view/main.ts +124 -0
  23. package/application/preview/view/view.html +12 -0
  24. package/application/preview/vue/main.ts +145 -0
  25. package/application/preview/vue/preview.html +12 -0
  26. package/application/shells/charcell/components/Application/index.css +37 -0
  27. package/application/shells/charcell/components/Application/index.tsx +65 -0
  28. package/application/shells/charcell/components/Bar/index.css +68 -0
  29. package/application/shells/charcell/components/Bar/index.tsx +60 -0
  30. package/application/shells/charcell/components/CodePane/index.css +60 -0
  31. package/application/shells/charcell/components/CodePane/index.tsx +113 -0
  32. package/application/shells/charcell/components/ComponentStage/index.css +26 -0
  33. package/application/shells/charcell/components/ComponentStage/index.tsx +110 -0
  34. package/application/shells/charcell/components/Control/index.css +20 -0
  35. package/application/shells/charcell/components/Control/index.tsx +118 -0
  36. package/application/shells/charcell/components/CopyLink/index.tsx +35 -0
  37. package/application/shells/charcell/components/Description/index.css +6 -0
  38. package/application/shells/charcell/components/Description/index.tsx +24 -0
  39. package/application/shells/charcell/components/EditorAddDialog/index.css +9 -0
  40. package/application/shells/charcell/components/EditorAddDialog/index.tsx +158 -0
  41. package/application/shells/charcell/components/EditorPanel/index.css +48 -0
  42. package/application/shells/charcell/components/EditorPanel/index.tsx +496 -0
  43. package/application/shells/charcell/components/EventsRow/index.css +42 -0
  44. package/application/shells/charcell/components/EventsRow/index.tsx +68 -0
  45. package/application/shells/charcell/components/HeadingRow/index.css +11 -0
  46. package/application/shells/charcell/components/HeadingRow/index.tsx +16 -0
  47. package/application/shells/charcell/components/Hint/index.css +16 -0
  48. package/application/shells/charcell/components/Hint/index.tsx +81 -0
  49. package/application/shells/charcell/components/HtmlWarnings/index.tsx +45 -0
  50. package/application/shells/charcell/components/KeyHints/index.css +27 -0
  51. package/application/shells/charcell/components/KeyHints/index.tsx +106 -0
  52. package/application/shells/charcell/components/KitCodeBlock/index.css +55 -0
  53. package/application/shells/charcell/components/KitCodeBlock/index.tsx +18 -0
  54. package/application/shells/charcell/components/Markdown/index.tsx +16 -0
  55. package/application/shells/charcell/components/Navigation/index.css +13 -0
  56. package/application/shells/charcell/components/Navigation/index.tsx +98 -0
  57. package/application/shells/charcell/components/NavigationSplit/index.css +16 -0
  58. package/application/shells/charcell/components/NavigationSplit/index.tsx +82 -0
  59. package/application/shells/charcell/components/OptionControls/index.tsx +93 -0
  60. package/application/shells/charcell/components/PaneStatus/index.css +7 -0
  61. package/application/shells/charcell/components/PaneStatus/index.tsx +47 -0
  62. package/application/shells/charcell/components/PaneWidth/index.tsx +45 -0
  63. package/application/shells/charcell/components/PreviewGrid/index.css +39 -0
  64. package/application/shells/charcell/components/PreviewGrid/index.tsx +344 -0
  65. package/application/shells/charcell/components/PreviewPane/index.css +127 -0
  66. package/application/shells/charcell/components/PreviewPane/index.tsx +182 -0
  67. package/application/shells/charcell/components/PromptBar/index.tsx +133 -0
  68. package/application/shells/charcell/components/ScenarioRow/index.css +36 -0
  69. package/application/shells/charcell/components/ScenarioRow/index.tsx +71 -0
  70. package/application/shells/charcell/components/Select/index.tsx +48 -0
  71. package/application/shells/charcell/components/SlotOutlet/index.css +7 -0
  72. package/application/shells/charcell/components/SlotOutlet/index.tsx +25 -0
  73. package/application/shells/charcell/components/StageBar/index.css +7 -0
  74. package/application/shells/charcell/components/StageBar/index.tsx +109 -0
  75. package/application/shells/charcell/components/StageSplit/index.css +11 -0
  76. package/application/shells/charcell/components/StageSplit/index.tsx +99 -0
  77. package/application/shells/charcell/components/StatusBar/index.tsx +30 -0
  78. package/application/shells/charcell/components/ViewControls/index.css +15 -0
  79. package/application/shells/charcell/components/ViewControls/index.tsx +58 -0
  80. package/application/shells/charcell/components/ViewHeading/index.css +13 -0
  81. package/application/shells/charcell/components/ViewHeading/index.tsx +8 -0
  82. package/application/shells/charcell/components/ViewStage/index.css +9 -0
  83. package/application/shells/charcell/components/ViewStage/index.tsx +25 -0
  84. package/application/shells/charcell/components/WrapToggle/index.tsx +27 -0
  85. package/application/shells/charcell/configuration.ts +45 -0
  86. package/application/shells/charcell/controller-context.ts +27 -0
  87. package/application/shells/charcell/fit-labels.ts +42 -0
  88. package/application/shells/charcell/html-warnings.ts +18 -0
  89. package/application/shells/charcell/index.tsx +82 -0
  90. package/application/shells/charcell/page-loading.html +57 -0
  91. package/application/shells/charcell/placement.ts +24 -0
  92. package/application/shells/charcell/shell-options.ts +23 -0
  93. package/application/shells/charcell/shell.css +37 -0
  94. package/application/shells/charcell/shell.tsx +25 -0
  95. package/application/shells/charcell/split-reset.ts +47 -0
  96. package/application/shells/charcell/stacked-rows.ts +117 -0
  97. package/application/shells/charcell/stage-toggles.ts +93 -0
  98. package/application/types/manifest.d.ts +6 -0
  99. package/application/types/preview-configuration.d.ts +10 -0
  100. package/application/types/preview-shell.d.ts +7 -0
  101. package/application/types/react-registry.d.ts +15 -0
  102. package/application/types/vue-registry.d.ts +6 -0
  103. package/dist/build.d.ts +24 -0
  104. package/dist/build.js +34 -0
  105. package/dist/check.d.ts +48 -0
  106. package/dist/check.js +781 -0
  107. package/dist/cli.d.ts +2 -0
  108. package/dist/cli.js +88 -0
  109. package/dist/context.d.ts +179 -0
  110. package/dist/context.js +783 -0
  111. package/dist/controller/catalog.d.ts +339 -0
  112. package/dist/controller/catalog.js +367 -0
  113. package/dist/controller/create-preview-controller.d.ts +532 -0
  114. package/dist/controller/create-preview-controller.js +1707 -0
  115. package/dist/controller/editor-tree.d.ts +161 -0
  116. package/dist/controller/editor-tree.js +373 -0
  117. package/dist/controller/html-source.d.ts +62 -0
  118. package/dist/controller/html-source.js +61 -0
  119. package/dist/controller/index.d.ts +9 -0
  120. package/dist/controller/index.js +12 -0
  121. package/dist/controller/persistence.d.ts +191 -0
  122. package/dist/controller/persistence.js +315 -0
  123. package/dist/controller/regeneration.d.ts +30 -0
  124. package/dist/controller/regeneration.js +27 -0
  125. package/dist/controller/source.d.ts +80 -0
  126. package/dist/controller/source.js +236 -0
  127. package/dist/controller/store.d.ts +27 -0
  128. package/dist/controller/store.js +40 -0
  129. package/dist/declarations/application/shells/charcell/components/Application/index.d.ts +2 -0
  130. package/dist/declarations/application/shells/charcell/components/Bar/index.d.ts +11 -0
  131. package/dist/declarations/application/shells/charcell/components/CodePane/index.d.ts +5 -0
  132. package/dist/declarations/application/shells/charcell/components/ComponentStage/index.d.ts +2 -0
  133. package/dist/declarations/application/shells/charcell/components/Control/index.d.ts +9 -0
  134. package/dist/declarations/application/shells/charcell/components/CopyLink/index.d.ts +1 -0
  135. package/dist/declarations/application/shells/charcell/components/Description/index.d.ts +5 -0
  136. package/dist/declarations/application/shells/charcell/components/EditorAddDialog/index.d.ts +6 -0
  137. package/dist/declarations/application/shells/charcell/components/EditorPanel/index.d.ts +2 -0
  138. package/dist/declarations/application/shells/charcell/components/EventsRow/index.d.ts +4 -0
  139. package/dist/declarations/application/shells/charcell/components/HeadingRow/index.d.ts +6 -0
  140. package/dist/declarations/application/shells/charcell/components/Hint/index.d.ts +11 -0
  141. package/dist/declarations/application/shells/charcell/components/HtmlWarnings/index.d.ts +5 -0
  142. package/dist/declarations/application/shells/charcell/components/KeyHints/index.d.ts +2 -0
  143. package/dist/declarations/application/shells/charcell/components/KitCodeBlock/index.d.ts +5 -0
  144. package/dist/declarations/application/shells/charcell/components/Markdown/index.d.ts +3 -0
  145. package/dist/declarations/application/shells/charcell/components/Navigation/index.d.ts +8 -0
  146. package/dist/declarations/application/shells/charcell/components/NavigationSplit/index.d.ts +7 -0
  147. package/dist/declarations/application/shells/charcell/components/OptionControls/index.d.ts +7 -0
  148. package/dist/declarations/application/shells/charcell/components/PaneStatus/index.d.ts +2 -0
  149. package/dist/declarations/application/shells/charcell/components/PaneWidth/index.d.ts +4 -0
  150. package/dist/declarations/application/shells/charcell/components/PreviewGrid/index.d.ts +7 -0
  151. package/dist/declarations/application/shells/charcell/components/PreviewPane/index.d.ts +9 -0
  152. package/dist/declarations/application/shells/charcell/components/PromptBar/index.d.ts +1 -0
  153. package/dist/declarations/application/shells/charcell/components/ScenarioRow/index.d.ts +2 -0
  154. package/dist/declarations/application/shells/charcell/components/Select/index.d.ts +16 -0
  155. package/dist/declarations/application/shells/charcell/components/SlotOutlet/index.d.ts +5 -0
  156. package/dist/declarations/application/shells/charcell/components/StageBar/index.d.ts +2 -0
  157. package/dist/declarations/application/shells/charcell/components/StageSplit/index.d.ts +17 -0
  158. package/dist/declarations/application/shells/charcell/components/StatusBar/index.d.ts +1 -0
  159. package/dist/declarations/application/shells/charcell/components/ViewControls/index.d.ts +9 -0
  160. package/dist/declarations/application/shells/charcell/components/ViewHeading/index.d.ts +4 -0
  161. package/dist/declarations/application/shells/charcell/components/ViewStage/index.d.ts +4 -0
  162. package/dist/declarations/application/shells/charcell/components/WrapToggle/index.d.ts +1 -0
  163. package/dist/declarations/application/shells/charcell/configuration.d.ts +31 -0
  164. package/dist/declarations/application/shells/charcell/controller-context.d.ts +9 -0
  165. package/dist/declarations/application/shells/charcell/fit-labels.d.ts +18 -0
  166. package/dist/declarations/application/shells/charcell/html-warnings.d.ts +5 -0
  167. package/dist/declarations/application/shells/charcell/index.d.ts +39 -0
  168. package/dist/declarations/application/shells/charcell/placement.d.ts +7 -0
  169. package/dist/declarations/application/shells/charcell/shell-options.d.ts +15 -0
  170. package/dist/declarations/application/shells/charcell/shell.d.ts +9 -0
  171. package/dist/declarations/application/shells/charcell/split-reset.d.ts +30 -0
  172. package/dist/declarations/application/shells/charcell/stacked-rows.d.ts +58 -0
  173. package/dist/declarations/application/shells/charcell/stage-toggles.d.ts +28 -0
  174. package/dist/declarations/src/controller/catalog.d.ts +339 -0
  175. package/dist/declarations/src/controller/create-preview-controller.d.ts +532 -0
  176. package/dist/declarations/src/controller/editor-tree.d.ts +161 -0
  177. package/dist/declarations/src/controller/html-source.d.ts +62 -0
  178. package/dist/declarations/src/controller/index.d.ts +9 -0
  179. package/dist/declarations/src/controller/persistence.d.ts +191 -0
  180. package/dist/declarations/src/controller/regeneration.d.ts +30 -0
  181. package/dist/declarations/src/controller/source.d.ts +80 -0
  182. package/dist/declarations/src/controller/store.d.ts +27 -0
  183. package/dist/declarations/src/emit/common.d.ts +103 -0
  184. package/dist/declarations/src/emit/manifest.d.ts +138 -0
  185. package/dist/declarations/src/emit/source.d.ts +87 -0
  186. package/dist/declarations/src/lib/jsx-roots.d.ts +22 -0
  187. package/dist/declarations/src/lib/module-of-source.d.ts +18 -0
  188. package/dist/declarations/src/lib/naming.d.ts +29 -0
  189. package/dist/declarations/src/lib/react-spelling.d.ts +68 -0
  190. package/dist/declarations/src/lib/scenario-story.d.ts +41 -0
  191. package/dist/declarations/src/lib/slot-fill.d.ts +15 -0
  192. package/dist/declarations/src/lib/types.d.ts +741 -0
  193. package/dist/declarations/src/options.d.ts +224 -0
  194. package/dist/declarations/src/protocol.d.ts +541 -0
  195. package/dist/declarations/src/react-view.d.ts +22 -0
  196. package/dist/declarations/src/react.d.ts +17 -0
  197. package/dist/declarations/src/shell.d.ts +99 -0
  198. package/dist/declarations/src/views.d.ts +152 -0
  199. package/dist/development-server.d.ts +121 -0
  200. package/dist/development-server.js +331 -0
  201. package/dist/emit/common.d.ts +103 -0
  202. package/dist/emit/common.js +170 -0
  203. package/dist/emit/html-source.d.ts +46 -0
  204. package/dist/emit/html-source.js +322 -0
  205. package/dist/emit/manifest.d.ts +138 -0
  206. package/dist/emit/manifest.js +74 -0
  207. package/dist/emit/react.d.ts +16 -0
  208. package/dist/emit/react.js +210 -0
  209. package/dist/emit/scratch.d.ts +40 -0
  210. package/dist/emit/scratch.js +116 -0
  211. package/dist/emit/source.d.ts +87 -0
  212. package/dist/emit/source.js +789 -0
  213. package/dist/emit/svelte.d.ts +9 -0
  214. package/dist/emit/svelte.js +177 -0
  215. package/dist/emit/vue.d.ts +2 -0
  216. package/dist/emit/vue.js +152 -0
  217. package/dist/enumerate.d.ts +2 -0
  218. package/dist/enumerate.js +162 -0
  219. package/dist/generate.d.ts +43 -0
  220. package/dist/generate.js +302 -0
  221. package/dist/html-stylesheet.d.ts +43 -0
  222. package/dist/html-stylesheet.js +145 -0
  223. package/dist/index.d.ts +18 -0
  224. package/dist/index.js +21 -0
  225. package/dist/landing.d.ts +16 -0
  226. package/dist/landing.js +45 -0
  227. package/dist/lib/exported-types.d.ts +42 -0
  228. package/dist/lib/exported-types.js +86 -0
  229. package/dist/lib/html-attributes.d.ts +2 -0
  230. package/dist/lib/html-attributes.js +111 -0
  231. package/dist/lib/html-warnings.d.ts +19 -0
  232. package/dist/lib/html-warnings.js +79 -0
  233. package/dist/lib/jsx-roots.d.ts +22 -0
  234. package/dist/lib/jsx-roots.js +243 -0
  235. package/dist/lib/module-of-source.d.ts +18 -0
  236. package/dist/lib/module-of-source.js +24 -0
  237. package/dist/lib/naming.d.ts +29 -0
  238. package/dist/lib/naming.js +56 -0
  239. package/dist/lib/parse-type.d.ts +32 -0
  240. package/dist/lib/parse-type.js +103 -0
  241. package/dist/lib/react-spelling.d.ts +68 -0
  242. package/dist/lib/react-spelling.js +197 -0
  243. package/dist/lib/scenario-story.d.ts +41 -0
  244. package/dist/lib/scenario-story.js +84 -0
  245. package/dist/lib/slot-fill.d.ts +15 -0
  246. package/dist/lib/slot-fill.js +92 -0
  247. package/dist/lib/template-files.d.ts +14 -0
  248. package/dist/lib/template-files.js +25 -0
  249. package/dist/lib/types.d.ts +741 -0
  250. package/dist/lib/types.js +13 -0
  251. package/dist/lib/void-elements.d.ts +7 -0
  252. package/dist/lib/void-elements.js +13 -0
  253. package/dist/load.d.ts +37 -0
  254. package/dist/load.js +82 -0
  255. package/dist/model.d.ts +44 -0
  256. package/dist/model.js +464 -0
  257. package/dist/options.d.ts +224 -0
  258. package/dist/options.js +223 -0
  259. package/dist/prop-types.d.ts +26 -0
  260. package/dist/prop-types.js +102 -0
  261. package/dist/props-from.d.ts +84 -0
  262. package/dist/props-from.js +510 -0
  263. package/dist/protocol.d.ts +541 -0
  264. package/dist/protocol.js +819 -0
  265. package/dist/react-view.d.ts +22 -0
  266. package/dist/react-view.js +36 -0
  267. package/dist/react.d.ts +17 -0
  268. package/dist/react.js +23 -0
  269. package/dist/sass-importers.d.ts +26 -0
  270. package/dist/sass-importers.js +32 -0
  271. package/dist/shell-acceptance.d.ts +21 -0
  272. package/dist/shell-acceptance.js +37 -0
  273. package/dist/shell-page.d.ts +36 -0
  274. package/dist/shell-page.js +90 -0
  275. package/dist/shell-peers.d.ts +38 -0
  276. package/dist/shell-peers.js +61 -0
  277. package/dist/shell.d.ts +99 -0
  278. package/dist/shell.js +52 -0
  279. package/dist/shells/charcell/page.d.ts +8 -0
  280. package/dist/shells/charcell/page.js +69 -0
  281. package/dist/story-source.d.ts +113 -0
  282. package/dist/story-source.js +271 -0
  283. package/dist/story.d.ts +5 -0
  284. package/dist/story.js +28 -0
  285. package/dist/testing.d.ts +218 -0
  286. package/dist/testing.js +284 -0
  287. package/dist/views.d.ts +152 -0
  288. package/dist/views.js +7 -0
  289. package/dist/vite-configuration.d.ts +114 -0
  290. package/dist/vite-configuration.js +578 -0
  291. package/package.json +123 -3
  292. package/src/build.ts +51 -0
  293. package/src/check.ts +1062 -0
  294. package/src/cli.ts +123 -0
  295. package/src/context.ts +1168 -0
  296. package/src/controller/catalog.ts +719 -0
  297. package/src/controller/create-preview-controller.ts +2683 -0
  298. package/src/controller/editor-tree.ts +530 -0
  299. package/src/controller/html-source.ts +114 -0
  300. package/src/controller/index.ts +105 -0
  301. package/src/controller/persistence.ts +403 -0
  302. package/src/controller/regeneration.ts +55 -0
  303. package/src/controller/source.ts +333 -0
  304. package/src/controller/store.ts +55 -0
  305. package/src/development-server.ts +442 -0
  306. package/src/emit/common.ts +201 -0
  307. package/src/emit/html-source.ts +402 -0
  308. package/src/emit/manifest.ts +241 -0
  309. package/src/emit/react.ts +263 -0
  310. package/src/emit/scratch.ts +175 -0
  311. package/src/emit/source.ts +1094 -0
  312. package/src/emit/svelte.ts +230 -0
  313. package/src/emit/vue.ts +194 -0
  314. package/src/enumerate.ts +220 -0
  315. package/src/generate.ts +443 -0
  316. package/src/html-stylesheet.ts +173 -0
  317. package/src/index.ts +60 -0
  318. package/src/landing.ts +58 -0
  319. package/src/lib/exported-types.ts +114 -0
  320. package/src/lib/html-attributes.ts +113 -0
  321. package/src/lib/html-warnings.ts +81 -0
  322. package/src/lib/jsx-roots.ts +238 -0
  323. package/src/lib/module-of-source.ts +26 -0
  324. package/src/lib/naming.ts +65 -0
  325. package/src/lib/parse-type.ts +143 -0
  326. package/src/lib/react-spelling.ts +235 -0
  327. package/src/lib/scenario-story.ts +97 -0
  328. package/src/lib/slot-fill.ts +102 -0
  329. package/src/lib/template-files.ts +31 -0
  330. package/src/lib/types.ts +778 -0
  331. package/src/lib/void-elements.ts +15 -0
  332. package/src/load.ts +144 -0
  333. package/src/model.ts +568 -0
  334. package/src/options.ts +435 -0
  335. package/src/prop-types.ts +133 -0
  336. package/src/props-from.ts +678 -0
  337. package/src/protocol.ts +1135 -0
  338. package/src/react-view.ts +45 -0
  339. package/src/react.ts +34 -0
  340. package/src/sass-importers.ts +45 -0
  341. package/src/shell-acceptance.ts +45 -0
  342. package/src/shell-page.ts +133 -0
  343. package/src/shell-peers.ts +70 -0
  344. package/src/shell.ts +194 -0
  345. package/src/shells/charcell/page.ts +82 -0
  346. package/src/story-source.ts +391 -0
  347. package/src/story.ts +42 -0
  348. package/src/testing.ts +497 -0
  349. package/src/views.ts +183 -0
  350. package/src/vite-configuration.ts +675 -0
@@ -0,0 +1,1094 @@
1
+ // Print a story as the source a consumer would write: the component with its
2
+ // props and slot content, importing from the kit's package entry per target.
3
+ // The registry emitters (react.ts, vue.ts, svelte.ts) print what the preview
4
+ // mounts - one line per story, props spread from the controls; this prints the
5
+ // same story as readable, standalone code for a docs page.
6
+
7
+ import {
8
+ reactFill,
9
+ reactProps,
10
+ styleProperties,
11
+ } from '../lib/react-spelling.js';
12
+ import { singleJsxRoot } from '../lib/jsx-roots.js';
13
+ import {
14
+ hasFragileWhitespace,
15
+ svelteText,
16
+ vueJunction,
17
+ vueSourceText,
18
+ } from './common.js';
19
+ import { collectParts, collectSlotComponents } from '../lib/slot-fill.js';
20
+ import type {
21
+ JsonValue,
22
+ PartFill,
23
+ SlotFill,
24
+ StoryAround,
25
+ Target,
26
+ } from '../lib/types.js';
27
+
28
+ /**
29
+ * A two-way prop the story gives, which the printed source binds to state of
30
+ * its own so the component's changes come back: React holds it in `useState`
31
+ * and passes the setter as `changeProp`, Vue binds it with `v-model` (bare
32
+ * with `vueModel`) or `v-model:<prop>`, and Svelte with `bind:<prop>`.
33
+ */
34
+ export interface SourceBinding {
35
+ /** The two-way prop. */
36
+ prop: string;
37
+ /** The event prop called with each new value. Without one, React prints
38
+ * the prop's value alone, as it has no way to hear a change. */
39
+ changeProp?: string;
40
+ /** The prop the component's own value starts from when the two-way prop
41
+ * is not given, which the source leaves out while it gives the prop. */
42
+ defaultProp?: string;
43
+ /** Whether a Vue consumer binds the prop with a bare `v-model`. */
44
+ vueModel?: boolean;
45
+ /** The prop's type as the component declares it, which types the state
46
+ * where the value alone would read narrower. */
47
+ declaredType?: string;
48
+ /** Whether the component takes type parameters, so its prop's type is
49
+ * the consumer's to infer rather than to name. */
50
+ generic?: boolean;
51
+ }
52
+
53
+ /** A story resolved for printing: its root component, props and slot content. */
54
+ export interface SourceStory {
55
+ /** The root component's local name (its export name). */
56
+ component: string;
57
+ /** The props to print, already merged and filtered. */
58
+ props: Record<string, JsonValue>;
59
+ /** Default-slot content. */
60
+ children?: SlotFill;
61
+ /** Named-slot content, keyed by slot name. */
62
+ named: Record<string, SlotFill>;
63
+ /** The content beside the component, printed as its siblings: React holds
64
+ * them in a fragment, Vue's template and Svelte's markup list them. */
65
+ around?: StoryAround;
66
+ /** Import lines' modules per local component name, and the names the
67
+ * component declares that React would otherwise respell
68
+ * (`declaredAttributes`: `class`, `style`, an attribute React renames),
69
+ * which React takes as they are. */
70
+ imports: Map<
71
+ string,
72
+ { module: string; exported: string; declaredAttributes?: string[] }
73
+ >;
74
+ /** The root component's two-way props, those given in `props` bound to
75
+ * state of the source's own. */
76
+ bindings?: SourceBinding[];
77
+ /** The mode the kit builds its Svelte target in, which the state a bound
78
+ * prop is held in follows: `$state` in runes mode, a plain `let` in
79
+ * legacy mode (the default). */
80
+ svelteMode?: 'legacy' | 'runes';
81
+ /** How React spells the HTML attributes it renames (`tabindex` as
82
+ * `tabIndex`), by their HTML names. A story spells a component's
83
+ * attributes as HTML does; React's source takes `class` as `className`,
84
+ * a `style` string as a style object and these by their React names,
85
+ * unless the component declares a prop by that name. Without it, only
86
+ * `class` and `style` are respelled. */
87
+ reactAttributeNames?: Readonly<Record<string, string>>;
88
+ /** The type names the kit's package entries export, each with the module
89
+ * it is imported from for this target: a bound prop's state is typed by
90
+ * its declared type where it names these and the language's own types
91
+ * alone, importing them. */
92
+ exportedTypes?: ReadonlyMap<string, string>;
93
+ }
94
+
95
+ const INDENT = ' ';
96
+ /** An opening tag longer than this puts each attribute on its own line. */
97
+ const MAXIMUM_WIDTH = 80;
98
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
99
+
100
+ /** Escapes a JavaScript single-quoted string literal needs: a line break or
101
+ * other control character inside one is a syntax error, and U+2028/U+2029
102
+ * read as line breaks in some parsers. */
103
+ const STRING_ESCAPES: Record<string, string> = {
104
+ '\\': '\\\\',
105
+ "'": "\\'",
106
+ '\n': '\\n',
107
+ '\r': '\\r',
108
+ '\t': '\\t',
109
+ };
110
+
111
+ function stringLiteral(value: string): string {
112
+ const escaped = value.replace(
113
+ /[\\'\u0000-\u001f\u2028\u2029]/g,
114
+ (character) =>
115
+ STRING_ESCAPES[character] ??
116
+ `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
117
+ );
118
+ return `'${escaped}'`;
119
+ }
120
+
121
+ /** A JavaScript expression for a JSON value, with single-quoted strings. */
122
+ function expression(value: JsonValue): string {
123
+ if (value === undefined) return 'undefined';
124
+ if (value === null) return 'null';
125
+ if (typeof value === 'string') return stringLiteral(value);
126
+ if (Array.isArray(value)) return `[${value.map(expression).join(', ')}]`;
127
+ if (typeof value === 'object') {
128
+ const entries = Object.entries(value).map(
129
+ ([key, entry]) =>
130
+ `${IDENTIFIER.test(key) ? key : expression(key)}: ${expression(entry)}`
131
+ );
132
+ return entries.length ? `{ ${entries.join(', ')} }` : '{}';
133
+ }
134
+ return String(value);
135
+ }
136
+
137
+ /** The narrowest a value is wrapped to, however deep its tag sits. */
138
+ const MINIMUM_VALUE_WIDTH = 40;
139
+
140
+ /**
141
+ * A JavaScript expression for a JSON value, as {@link expression} writes it
142
+ * while it fits in `width`; a longer array or object is broken over lines,
143
+ * one element or entry to a line with a trailing comma, each element broken
144
+ * in turn while it is still too long. An array of numbers, booleans or
145
+ * `null` fills each line instead.
146
+ */
147
+ function wrappedExpression(value: JsonValue, width: number): string {
148
+ const single = expression(value);
149
+ if (single.length <= width || typeof value !== 'object' || value === null)
150
+ return single;
151
+ const [open, close] = Array.isArray(value) ? ['[', ']'] : ['{', '}'];
152
+ const inner = Math.max(width - INDENT.length, MINIMUM_VALUE_WIDTH);
153
+ if (
154
+ Array.isArray(value) &&
155
+ value.length > 0 &&
156
+ value.every(
157
+ (element) =>
158
+ element === null ||
159
+ typeof element === 'number' ||
160
+ typeof element === 'boolean'
161
+ )
162
+ ) {
163
+ const lines: string[] = [];
164
+ for (const element of value.map((element) => `${expression(element)},`)) {
165
+ const last = lines.length - 1;
166
+ if (last >= 0 && lines[last].length + 1 + element.length <= inner)
167
+ lines[last] += ` ${element}`;
168
+ else lines.push(element);
169
+ }
170
+ return [open, ...lines.map((line) => `${INDENT}${line}`), close].join('\n');
171
+ }
172
+ const items = Array.isArray(value)
173
+ ? value.map((element) => wrappedExpression(element, inner))
174
+ : Object.entries(value).map(([key, entry]) => {
175
+ const name = IDENTIFIER.test(key) ? key : expression(key);
176
+ return `${name}: ${wrappedExpression(entry, inner - name.length - 2)}`;
177
+ });
178
+ if (items.length === 0) return single;
179
+ const body = items.map(
180
+ (item) => `${INDENT}${item.replace(/\n/g, `\n${INDENT}`)},`
181
+ );
182
+ return [open, ...body, close].join('\n');
183
+ }
184
+
185
+ /**
186
+ * The width a prop's value has on its own line under an opening tag at
187
+ * `depth`: the line's width less the attribute's column and its name, and
188
+ * never narrower than {@link MINIMUM_VALUE_WIDTH}.
189
+ */
190
+ function valueWidth(attributeHead: string, depth: number): number {
191
+ return Math.max(
192
+ MAXIMUM_WIDTH - INDENT.length * (depth + 1) - attributeHead.length - 1,
193
+ MINIMUM_VALUE_WIDTH
194
+ );
195
+ }
196
+
197
+ /** A plain-CSS `style` string as a React style object. */
198
+ function styleObject(css: string): string {
199
+ return expression(styleProperties(css));
200
+ }
201
+
202
+ /** How one target spells attributes, text, and slot content. */
203
+ interface Dialect {
204
+ /** Whether a line break between two siblings reads as a space, as in
205
+ * Vue's and Svelte's templates, which collapse it to one; JSX drops the
206
+ * whitespace at a line's edges instead. */
207
+ lineBreakIsSpace: boolean;
208
+ /** Whether a line break between two elements or components reads as a
209
+ * space too, as in Svelte; Vue drops it. */
210
+ elementBreakIsSpace: boolean;
211
+ /** A prop as an attribute, its value wrapped for a tag at `depth`. */
212
+ attribute(
213
+ name: string,
214
+ value: JsonValue,
215
+ html: boolean,
216
+ depth: number
217
+ ): string;
218
+ text(value: string): string;
219
+ /** The line before a junction, given the content joined onto it, changed
220
+ * where the two would read together as something else. */
221
+ junction(line: string, next: string): string;
222
+ /** Named-slot content inside an element: lines, or attributes on the element (React). */
223
+ namedSlot(
224
+ slot: string,
225
+ fill: SlotFill,
226
+ depth: number
227
+ ): { attributes?: string[]; lines?: string[] };
228
+ raw(fill: Extract<SlotFill, { kind: 'raw' }>): string;
229
+ /** A call to a part, as lines at `depth`. */
230
+ part(fill: PartFill, depth: number): string[];
231
+ }
232
+
233
+ function attributeList(
234
+ dialect: Dialect,
235
+ props: Record<string, JsonValue> | undefined,
236
+ html: boolean,
237
+ depth: number,
238
+ bound: ReadonlyMap<string, string[]> = new Map()
239
+ ): string[] {
240
+ return Object.entries(props ?? {}).flatMap(
241
+ ([name, value]) =>
242
+ bound.get(name) ?? [dialect.attribute(name, value, html, depth)]
243
+ );
244
+ }
245
+
246
+ /** An opening tag's lines: attributes on one line, or one per line past the
247
+ * width or when an attribute spans lines itself (a React prop holding JSX),
248
+ * each of its lines indented under the tag. */
249
+ function openTag(
250
+ pad: string,
251
+ name: string,
252
+ attributes: string[],
253
+ selfClosing: boolean
254
+ ): string[] {
255
+ const close = selfClosing ? ' />' : '>';
256
+ const spansLines = attributes.some((attribute) => attribute.includes('\n'));
257
+ const single = `${pad}<${name}${attributes.length ? ` ${attributes.join(' ')}` : ''}${close}`;
258
+ if (
259
+ !spansLines &&
260
+ (single.length <= MAXIMUM_WIDTH || attributes.length === 0)
261
+ )
262
+ return [single];
263
+ return [
264
+ `${pad}<${name}`,
265
+ ...attributes.flatMap((attribute) =>
266
+ attribute.split('\n').map((line) => `${pad}${INDENT}${line}`)
267
+ ),
268
+ `${pad}${selfClosing ? '/>' : '>'}`,
269
+ ];
270
+ }
271
+
272
+ /** Lines for a slot fill at `depth`. */
273
+ function fillLines(dialect: Dialect, fill: SlotFill, depth: number): string[] {
274
+ const pad = INDENT.repeat(depth);
275
+ switch (fill.kind) {
276
+ case 'text':
277
+ return [`${pad}${dialect.text(fill.value)}`];
278
+ case 'raw':
279
+ return [`${pad}${dialect.raw(fill)}`];
280
+ case 'part':
281
+ return dialect.part(fill, depth);
282
+ case 'scoped':
283
+ // Scoped content belongs to a named slot; nested elsewhere it renders nothing.
284
+ return [];
285
+ case 'fragment':
286
+ return fragmentLines(dialect, fill, depth);
287
+ case 'html':
288
+ return elementLines(
289
+ dialect,
290
+ fill.tag,
291
+ attributeList(dialect, fill.props, true, depth),
292
+ fill.children,
293
+ {},
294
+ depth
295
+ );
296
+ case 'element':
297
+ return elementLines(
298
+ dialect,
299
+ fill.component,
300
+ attributeList(dialect, fill.props, false, depth),
301
+ fill.children,
302
+ fill.slots ?? {},
303
+ depth
304
+ );
305
+ }
306
+ }
307
+
308
+ /** A fragment's children, those of a fragment inside it in its place, and
309
+ * empty text and scoped content, which renders nothing there, left out. */
310
+ function flattenedChildren(fill: SlotFill): SlotFill[] {
311
+ if (fill.kind === 'fragment') return fill.children.flatMap(flattenedChildren);
312
+ if (fill.kind === 'scoped') return [];
313
+ return fill.kind === 'text' && fill.value === '' ? [] : [fill];
314
+ }
315
+
316
+ /** Whether a sibling starts (`'start'`) or ends (`'end'`) with whitespace:
317
+ * text and raw content by their own, anything else never. */
318
+ function edgeSpace(
319
+ dialect: Dialect,
320
+ fill: SlotFill,
321
+ edge: 'start' | 'end'
322
+ ): boolean {
323
+ const content =
324
+ fill.kind === 'text'
325
+ ? fill.value
326
+ : fill.kind === 'raw'
327
+ ? dialect.raw(fill)
328
+ : undefined;
329
+ if (content === undefined) return false;
330
+ return edge === 'start' ? /^\s/.test(content) : /\s$/.test(content);
331
+ }
332
+
333
+ /**
334
+ * Lines for a fragment's children at `depth`, one sibling after another,
335
+ * printed so the whitespace between text and the siblings beside it reads as
336
+ * the story gives it. Where the story has whitespace there, the text's edge
337
+ * loses it for the line break, which Vue and Svelte read as a space, and
338
+ * React keeps it as `{' '}`, as JSX drops whitespace at a line's edges.
339
+ * Where it has none, the two share a line in Vue and Svelte (and two texts
340
+ * in React), so no break adds a space. Whitespace alone between two
341
+ * siblings is `{' '}` in React, and one space between them on one line in
342
+ * Vue and Svelte.
343
+ */
344
+ function fragmentLines(
345
+ dialect: Dialect,
346
+ fill: SlotFill,
347
+ depth: number
348
+ ): string[] {
349
+ const pad = INDENT.repeat(depth);
350
+ const children = flattenedChildren(fill);
351
+ const lines: string[] = [];
352
+ let glueNext = false;
353
+ children.forEach((child, index) => {
354
+ const previous = children[index - 1];
355
+ const next = children[index + 1];
356
+ let glue = glueNext;
357
+ glueNext = false;
358
+ // Siblings that touch share a line where a break would read as a space:
359
+ // text or raw content beside anything in Vue and Svelte, two elements
360
+ // or components in Svelte alone, and two texts in React.
361
+ const flowing = (sibling: SlotFill): boolean =>
362
+ sibling.kind === 'text' || sibling.kind === 'raw';
363
+ if (
364
+ previous !== undefined &&
365
+ !glue &&
366
+ !edgeSpace(dialect, previous, 'end') &&
367
+ !edgeSpace(dialect, child, 'start') &&
368
+ (dialect.lineBreakIsSpace
369
+ ? flowing(previous) || flowing(child) || dialect.elementBreakIsSpace
370
+ : previous.kind === 'text' && child.kind === 'text')
371
+ )
372
+ glue = true;
373
+ let childLines: string[];
374
+ if (child.kind === 'text') {
375
+ const before = previous !== undefined && /^\s/.test(child.value);
376
+ const after = next !== undefined && /\s$/.test(child.value);
377
+ const trimmed = child.value.slice(
378
+ before ? child.value.length - child.value.trimStart().length : 0,
379
+ after ? child.value.trimEnd().length : child.value.length
380
+ );
381
+ if (trimmed === '' && (before || after)) {
382
+ // Whitespace alone between siblings; at an edge, Vue and Svelte
383
+ // drop it as they do whitespace at an element's edge.
384
+ if (!dialect.lineBreakIsSpace) childLines = [`${pad}{' '}`];
385
+ else if (previous !== undefined && next !== undefined) {
386
+ childLines = [' '];
387
+ glue = true;
388
+ glueNext = true;
389
+ } else childLines = [];
390
+ } else if (dialect.lineBreakIsSpace) {
391
+ childLines = [`${pad}${dialect.text(trimmed)}`];
392
+ } else {
393
+ const space = "{' '}";
394
+ childLines = [
395
+ `${pad}${before ? space : ''}${dialect.text(trimmed)}${after ? space : ''}`,
396
+ ];
397
+ }
398
+ } else childLines = fillLines(dialect, child, depth);
399
+ if (childLines.length === 0) return;
400
+ if (glue && lines.length > 0) {
401
+ const first = childLines[0] === ' ' ? ' ' : childLines[0].trimStart();
402
+ const last = dialect.junction(lines.pop()!, first);
403
+ lines.push(`${last}${first}`, ...childLines.slice(1));
404
+ } else lines.push(...childLines);
405
+ });
406
+ return lines;
407
+ }
408
+
409
+ /** Lines for an element with default and named content. */
410
+ function elementLines(
411
+ dialect: Dialect,
412
+ name: string,
413
+ attributes: string[],
414
+ children: SlotFill | undefined,
415
+ named: Record<string, SlotFill>,
416
+ depth: number
417
+ ): string[] {
418
+ const pad = INDENT.repeat(depth);
419
+ const allAttributes = [...attributes];
420
+ const namedLines: string[] = [];
421
+ // Scoped content for the default slot is printed as a scoped slot is: the
422
+ // function child, `#default`, the `children` snippet.
423
+ const scopedChildren = children?.kind === 'scoped' ? children : undefined;
424
+ const slotEntries: Array<[string, SlotFill]> = [
425
+ ...(scopedChildren
426
+ ? [['default', scopedChildren] as [string, SlotFill]]
427
+ : []),
428
+ ...Object.entries(named),
429
+ ];
430
+ for (const [slot, fill] of slotEntries) {
431
+ const rendered = dialect.namedSlot(slot, fill, depth + 1);
432
+ allAttributes.push(...(rendered.attributes ?? []));
433
+ namedLines.push(...(rendered.lines ?? []));
434
+ }
435
+ const childLines =
436
+ children && !scopedChildren ? fillLines(dialect, children, depth + 1) : [];
437
+ const content = [...childLines, ...namedLines];
438
+ if (content.length === 0) return openTag(pad, name, allAttributes, true);
439
+ const open = openTag(pad, name, allAttributes, false);
440
+ // A lone text child stays on a one-line element's line.
441
+ if (
442
+ content.length === 1 &&
443
+ namedLines.length === 0 &&
444
+ children?.kind === 'text' &&
445
+ open.length === 1
446
+ ) {
447
+ const line = `${open[0]}${content[0].trim()}</${name}>`;
448
+ if (line.length <= MAXIMUM_WIDTH) return [line];
449
+ }
450
+ return [...open, ...content, `${pad}</${name}>`];
451
+ }
452
+
453
+ const react: Dialect = {
454
+ lineBreakIsSpace: false,
455
+ elementBreakIsSpace: false,
456
+ attribute(name, value, html, depth) {
457
+ const key = html && name === 'class' ? 'className' : name;
458
+ if (html && name === 'style' && typeof value === 'string')
459
+ return `style={${styleObject(value)}}`;
460
+ if (value === true) return key;
461
+ // A quoted JSX attribute reads `&` as a character reference.
462
+ if (typeof value === 'string' && !/["\n&]/.test(value))
463
+ return `${key}="${value}"`;
464
+ return `${key}={${wrappedExpression(value, valueWidth(`${key}={`, depth))}}`;
465
+ },
466
+ // JSX reads braces and angle brackets as code, and `&` as a reference,
467
+ // and drops whitespace at the edge of a line of text.
468
+ text: (value) =>
469
+ /[{}<>&]/.test(value) || hasFragileWhitespace(value)
470
+ ? `{${expression(value)}}`
471
+ : value,
472
+ junction: (line) => line,
473
+ namedSlot(slot, fill, depth) {
474
+ if (fill.kind === 'scoped') {
475
+ if (!fill.react) return {};
476
+ const head =
477
+ fill.reactScope === 'positional'
478
+ ? `(${fill.parameters.join(', ')})`
479
+ : `({ ${fill.parameters.join(', ')} })`;
480
+ // Sibling roots or text are no arrow body until a fragment holds them.
481
+ const content = singleJsxRoot(fill.react);
482
+ const body = content.includes('\n') ? `(${content})` : content;
483
+ // A render function named `children` reads as the element's child.
484
+ if (fill.slot.react === 'children')
485
+ return { lines: [`${INDENT.repeat(depth)}{${head} => ${body}}`] };
486
+ return { attributes: [`${fill.slot.react}={${head} => ${body}}`] };
487
+ }
488
+ if (fill.kind === 'text')
489
+ return { attributes: [react.attribute(slot, fill.value, false, depth)] };
490
+ // A raw body in a React prop: sibling roots, or text, are one expression
491
+ // in a fragment.
492
+ if (fill.kind === 'raw') {
493
+ if (fill.react.trim() === '') return {};
494
+ const content = singleJsxRoot(fill.react);
495
+ const body = content.includes('\n') ? `(${content})` : content;
496
+ return { attributes: [`${slot}={${body}}`] };
497
+ }
498
+ // Content in a React prop: text alone is a string, one line stays on the
499
+ // prop's line, and more becomes an indented block, its lines relative to
500
+ // the prop so the tag can indent them; several siblings, or text beside
501
+ // anything, are held in a fragment.
502
+ const flattened = flattenedChildren(fill);
503
+ if (flattened.length === 0) return {};
504
+ if (flattened.length === 1 && flattened[0].kind === 'text')
505
+ return {
506
+ attributes: [react.attribute(slot, flattened[0].value, false, depth)],
507
+ };
508
+ const lines = fillLines(react, fill, 0);
509
+ const siblings = flattened.length > 1;
510
+ if (lines.length === 1)
511
+ return {
512
+ attributes: [
513
+ siblings
514
+ ? `${slot}={<>${lines[0].trim()}</>}`
515
+ : `${slot}={${lines[0].trim()}}`,
516
+ ],
517
+ };
518
+ const block = siblings
519
+ ? ['<>', ...lines.map((line) => `${INDENT}${line}`), '</>']
520
+ : lines;
521
+ return {
522
+ attributes: [
523
+ [`${slot}={`, ...block.map((line) => `${INDENT}${line}`), '}'].join(
524
+ '\n'
525
+ ),
526
+ ],
527
+ };
528
+ },
529
+ raw: (fill) => fill.react,
530
+ part: (fill, depth) => componentCall(react, fill, depth),
531
+ };
532
+
533
+ const vue: Dialect = {
534
+ lineBreakIsSpace: true,
535
+ elementBreakIsSpace: false,
536
+ attribute(name, value, _html, depth) {
537
+ if (value === true) return name;
538
+ if (typeof value === 'string' && !/["\n]/.test(value))
539
+ return `${name}="${value}"`;
540
+ return `:${name}="${wrappedExpression(value, valueWidth(`:${name}="`, depth)).replace(/"/g, '&quot;')}"`;
541
+ },
542
+ text: vueSourceText,
543
+ // A `{` before content starting with one would open an interpolation.
544
+ junction: vueJunction,
545
+ namedSlot(slot, fill, depth) {
546
+ const pad = INDENT.repeat(depth);
547
+ if (fill.kind === 'scoped') {
548
+ return fill.vue
549
+ ? {
550
+ lines: [
551
+ `${pad}<template #${fill.slot.vue}="{ ${fill.parameters.join(', ')} }">`,
552
+ `${pad}${INDENT}${fill.vue}`,
553
+ `${pad}</template>`,
554
+ ],
555
+ }
556
+ : {};
557
+ }
558
+ return {
559
+ lines: [
560
+ `${pad}<template #${slot}>`,
561
+ ...fillLines(vue, fill, depth + 1),
562
+ `${pad}</template>`,
563
+ ],
564
+ };
565
+ },
566
+ raw: (fill) => fill.vue,
567
+ part: (fill, depth) => componentCall(vue, fill, depth),
568
+ };
569
+
570
+ const svelte: Dialect = {
571
+ lineBreakIsSpace: true,
572
+ elementBreakIsSpace: true,
573
+ attribute(name, value, _html, depth) {
574
+ if (value === true) return name;
575
+ if (typeof value === 'string' && !/["\n{}]/.test(value))
576
+ return `${name}="${value}"`;
577
+ return `${name}={${wrappedExpression(value, valueWidth(`${name}={`, depth))}}`;
578
+ },
579
+ text: svelteText,
580
+ junction: (line) => line,
581
+ namedSlot(slot, fill, depth) {
582
+ const pad = INDENT.repeat(depth);
583
+ if (fill.kind === 'scoped') {
584
+ return fill.svelte
585
+ ? {
586
+ lines: [
587
+ `${pad}{#snippet ${fill.slot.svelte}({ ${fill.parameters.join(', ')} })}`,
588
+ `${pad}${INDENT}${fill.svelte}`,
589
+ `${pad}{/snippet}`,
590
+ ],
591
+ }
592
+ : {};
593
+ }
594
+ return {
595
+ lines: [
596
+ `${pad}{#snippet ${slot}()}`,
597
+ ...fillLines(svelte, fill, depth + 1),
598
+ `${pad}{/snippet}`,
599
+ ],
600
+ };
601
+ },
602
+ raw: (fill) => fill.svelte,
603
+ part: (fill, depth) => [
604
+ `${INDENT.repeat(depth)}{@render ${fill.name}(${expression(fill.props ?? {})})}`,
605
+ ],
606
+ };
607
+
608
+ /** A part called as a component, in React and Vue. */
609
+ function componentCall(
610
+ dialect: Dialect,
611
+ fill: PartFill,
612
+ depth: number
613
+ ): string[] {
614
+ return elementLines(
615
+ dialect,
616
+ fill.name,
617
+ attributeList(dialect, fill.props, false, depth),
618
+ undefined,
619
+ {},
620
+ depth
621
+ );
622
+ }
623
+
624
+ /** The parts the story's content calls, each declared once. */
625
+ function partsOf(story: SourceStory): PartFill[] {
626
+ return [
627
+ ...collectParts([
628
+ story.around?.before,
629
+ story.children,
630
+ ...Object.values(story.named),
631
+ story.around?.after,
632
+ ]).values(),
633
+ ];
634
+ }
635
+
636
+ /** Whether the story has content beside its component. */
637
+ function hasAround(story: SourceStory): boolean {
638
+ return (
639
+ story.around?.before !== undefined || story.around?.after !== undefined
640
+ );
641
+ }
642
+
643
+ /** The component's lines between the lines of the content beside it, at
644
+ * `depth`. */
645
+ function besideLines(
646
+ dialect: Dialect,
647
+ story: SourceStory,
648
+ component: string[],
649
+ depth: number
650
+ ): string[] {
651
+ const { before, after } = story.around ?? {};
652
+ return [
653
+ ...(before ? fillLines(dialect, before, depth) : []),
654
+ ...component,
655
+ ...(after ? fillLines(dialect, after, depth) : []),
656
+ ];
657
+ }
658
+
659
+ /** A part's destructured props with their type: each parameter's own, or `any`. */
660
+ function typedParameters(part: PartFill): string {
661
+ const types = part.parameters.map(
662
+ (name) => `${name}: ${part.parameterTypes?.[name] ?? 'any'}`
663
+ );
664
+ return `{ ${part.parameters.join(', ')} }: { ${types.join('; ')} }`;
665
+ }
666
+
667
+ /** A body's lines at `depth`, each of its own lines indented. */
668
+ function bodyLines(body: string, depth: number): string[] {
669
+ return body.split('\n').map((line) => `${INDENT.repeat(depth)}${line}`);
670
+ }
671
+
672
+ /** `import { A, B as C, type D } from 'module';` lines, one per module, in
673
+ * first-seen order: of every component the story names, or only of `only`,
674
+ * and of the exported types `types` names, each beside the components of its
675
+ * module or in an `import type` of its own. */
676
+ function importLines(
677
+ story: SourceStory,
678
+ only?: Set<string>,
679
+ types: readonly string[] = []
680
+ ): string[] {
681
+ const byModule = new Map<string, { names: string[]; types: string[] }>();
682
+ const entry = (module: string) => {
683
+ const found = byModule.get(module) ?? { names: [], types: [] };
684
+ byModule.set(module, found);
685
+ return found;
686
+ };
687
+ for (const [local, { module, exported }] of story.imports) {
688
+ if (only && !only.has(local)) continue;
689
+ entry(module).names.push(
690
+ exported === local ? local : `${exported} as ${local}`
691
+ );
692
+ }
693
+ for (const type of types) {
694
+ const module = story.exportedTypes?.get(type);
695
+ if (module !== undefined) entry(module).types.push(type);
696
+ }
697
+ return [...byModule].map(([module, { names, types: typeNames }]) =>
698
+ names.length === 0
699
+ ? `import type { ${typeNames.join(', ')} } from '${module}';`
700
+ : `import { ${[...names, ...typeNames.map((type) => `type ${type}`)].join(', ')} } from '${module}';`
701
+ );
702
+ }
703
+
704
+ /** The words a type made of the language's own types alone is written in. */
705
+ const BUILT_IN_TYPE_WORDS =
706
+ /\b(?:string|number|boolean|bigint|null|undefined|true|false|unknown|any|readonly|Array|ReadonlyArray)\b/g;
707
+
708
+ /**
709
+ * The names a type uses beyond the language's own types and literals, when
710
+ * each of them is one of `exported` (none for a type of the language's own
711
+ * alone), so a consumer's file can write it as it stands, importing those;
712
+ * `undefined` when it uses any other name or a shape written out in place.
713
+ */
714
+ function writableTypeNames(
715
+ type: string,
716
+ exported: ReadonlyMap<string, string> = new Map()
717
+ ): string[] | undefined {
718
+ const names: string[] = [];
719
+ const rest = type
720
+ .replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, '')
721
+ .replace(BUILT_IN_TYPE_WORDS, '')
722
+ .replace(/[A-Za-z_$][\w$]*/g, (name) => {
723
+ if (!exported.has(name)) return name;
724
+ if (!names.includes(name)) names.push(name);
725
+ return '';
726
+ })
727
+ .replace(/-?\d+(?:\.\d+)?/g, '');
728
+ return /^[\s|&()[\]<>,]*$/.test(rest) ? names : undefined;
729
+ }
730
+
731
+ /** The type a JSON value is inferred as where state starts from it, when it
732
+ * is a primitive's: `boolean`, `number` or `string`. */
733
+ function inferredType(value: JsonValue): string | undefined {
734
+ return ['boolean', 'number', 'string'].includes(typeof value)
735
+ ? typeof value
736
+ : undefined;
737
+ }
738
+
739
+ /**
740
+ * The type argument the state holding a bound prop takes: none where the
741
+ * value reads as the declared type, or as a primitive with no declared type;
742
+ * the declared type where it names the language's own types alone; none for
743
+ * or types the kit's package entry exports, importing those (`typeImports`);
744
+ * none for a generic component, whose type parameters the value infers;
745
+ * else the component's own prop type, as `propsType` names it
746
+ * (`ComponentProps<typeof X>['open']`), so an array or `null` never starts
747
+ * the state narrower than the prop.
748
+ */
749
+ function stateType(
750
+ binding: SourceBinding,
751
+ value: JsonValue,
752
+ propsType: (prop: string) => string,
753
+ exportedTypes?: ReadonlyMap<string, string>
754
+ ): { type?: string; propsLookup?: boolean; typeImports?: string[] } {
755
+ const declared = binding.declaredType?.trim();
756
+ const inferred = inferredType(value);
757
+ if (declared ? declared === inferred : inferred !== undefined) return {};
758
+ const named = declared
759
+ ? writableTypeNames(declared, exportedTypes)
760
+ : undefined;
761
+ if (declared && named)
762
+ return {
763
+ type: declared,
764
+ ...(named.length > 0 ? { typeImports: named } : {}),
765
+ };
766
+ if (binding.generic) return {};
767
+ return { type: propsType(binding.prop), propsLookup: true };
768
+ }
769
+
770
+ /**
771
+ * The story as its source prints it for a target: a prop given `null` left
772
+ * out, as a scenario's `null` unsets it, unless the target binds it as a
773
+ * two-way prop, whose state then starts from `null`; and the default prop of
774
+ * each two-way prop the story gives left out, as the component's value
775
+ * starts from the given prop instead.
776
+ */
777
+ function printedStory(story: SourceStory, target: Target): SourceStory {
778
+ const bound = new Set(boundOf(story, target).map((binding) => binding.prop));
779
+ const props: Record<string, JsonValue> = {};
780
+ for (const [name, value] of Object.entries(story.props))
781
+ if (value !== null || bound.has(name)) props[name] = value;
782
+ for (const binding of story.bindings ?? [])
783
+ if (binding.defaultProp !== undefined && binding.prop in props)
784
+ delete props[binding.defaultProp];
785
+ // A name the story imports as a component is a value, never a type.
786
+ const exportedTypes = story.exportedTypes
787
+ ? new Map(
788
+ [...story.exportedTypes].filter(([name]) => !story.imports.has(name))
789
+ )
790
+ : undefined;
791
+ return { ...story, props, ...(exportedTypes ? { exportedTypes } : {}) };
792
+ }
793
+
794
+ /** The story's bindings this target binds: those for props the story gives,
795
+ * named as a variable can be, and in React those with a change prop. */
796
+ function boundOf(story: SourceStory, target: Target): SourceBinding[] {
797
+ return (story.bindings ?? []).filter(
798
+ (binding) =>
799
+ binding.prop in story.props &&
800
+ IDENTIFIER.test(binding.prop) &&
801
+ (target !== 'react' ||
802
+ (binding.changeProp !== undefined &&
803
+ IDENTIFIER.test(binding.changeProp)))
804
+ );
805
+ }
806
+
807
+ /** A setter's name for a state variable: `open` → `setOpen`. */
808
+ function setterName(name: string): string {
809
+ return `set${name[0].toUpperCase()}${name.slice(1)}`;
810
+ }
811
+
812
+ /** The story as a React component module, the attributes it gives a
813
+ * component spelled as React takes them (see `reactAttributeNames`). */
814
+ export function reactSource(given: SourceStory): string {
815
+ const printed = printedStory(given, 'react');
816
+ const names = printed.reactAttributeNames ?? {};
817
+ const declares = (component: string, name: string): boolean =>
818
+ printed.imports.get(component)?.declaredAttributes?.includes(name) === true;
819
+ const story: SourceStory = {
820
+ ...printed,
821
+ props: reactProps(
822
+ printed.props,
823
+ (name) => declares(printed.component, name),
824
+ names
825
+ ) as Record<string, JsonValue>,
826
+ children: reactFill(printed.children, declares, names),
827
+ named: Object.fromEntries(
828
+ Object.entries(printed.named).map(([slot, fill]) => [
829
+ slot,
830
+ reactFill(fill, declares, names),
831
+ ])
832
+ ),
833
+ ...(printed.around
834
+ ? {
835
+ around: {
836
+ ...(printed.around.before
837
+ ? { before: reactFill(printed.around.before, declares, names) }
838
+ : {}),
839
+ ...(printed.around.after
840
+ ? { after: reactFill(printed.around.after, declares, names) }
841
+ : {}),
842
+ },
843
+ }
844
+ : {}),
845
+ };
846
+ // A bound prop's state, and its value and setter passed to the component.
847
+ const bindings = boundOf(story, 'react');
848
+ let propsLookup = false;
849
+ const typeImports = new Set<string>();
850
+ const states = bindings.map((binding) => {
851
+ const value = story.props[binding.prop];
852
+ const {
853
+ type,
854
+ propsLookup: looked,
855
+ typeImports: named,
856
+ } = stateType(
857
+ binding,
858
+ value,
859
+ (prop) => `ComponentProps<typeof ${story.component}>['${prop}']`,
860
+ story.exportedTypes
861
+ );
862
+ propsLookup ||= looked === true;
863
+ for (const name of named ?? []) typeImports.add(name);
864
+ return `${INDENT}const [${binding.prop}, ${setterName(binding.prop)}] = useState${type ? `<${type}>` : ''}(${expression(value)});`;
865
+ });
866
+ const bound = new Map(
867
+ bindings.map((binding) => [
868
+ binding.prop,
869
+ [
870
+ `${binding.prop}={${binding.prop}}`,
871
+ `${binding.changeProp}={${setterName(binding.prop)}}`,
872
+ ],
873
+ ])
874
+ );
875
+ // Content beside the component is returned in a fragment with it.
876
+ const beside = hasAround(story);
877
+ const depth = beside ? 3 : 2;
878
+ const component = elementLines(
879
+ react,
880
+ story.component,
881
+ attributeList(react, story.props, false, depth, bound),
882
+ story.children,
883
+ story.named,
884
+ depth
885
+ );
886
+ const pad = INDENT.repeat(2);
887
+ const body = beside
888
+ ? [`${pad}<>`, ...besideLines(react, story, component, depth), `${pad}</>`]
889
+ : component;
890
+ const returned =
891
+ body.length === 1
892
+ ? [`${INDENT}return ${body[0].trim()};`]
893
+ : [`${INDENT}return (`, ...body, `${INDENT});`];
894
+ // A part is a function component above the example.
895
+ const parts = partsOf(story).flatMap((part) => [
896
+ `function ${part.name}(${typedParameters(part)}) {`,
897
+ `${INDENT}return (`,
898
+ ...bodyLines(singleJsxRoot(part.react), 2),
899
+ `${INDENT});`,
900
+ '}',
901
+ '',
902
+ ]);
903
+ return [
904
+ ...(states.length
905
+ ? [
906
+ `import { useState${propsLookup ? ', type ComponentProps' : ''} } from 'react';`,
907
+ ]
908
+ : []),
909
+ ...importLines(story, undefined, [...typeImports]),
910
+ '',
911
+ ...parts,
912
+ `export function ${story.component}Example() {`,
913
+ ...(states.length ? [...states, ''] : []),
914
+ ...returned,
915
+ '}',
916
+ '',
917
+ ].join('\n');
918
+ }
919
+
920
+ /** The story as a Vue single-file component. */
921
+ export function vueSource(given: SourceStory): string {
922
+ const story = printedStory(given, 'vue');
923
+ // A bound prop's ref, bound with `v-model`.
924
+ const bindings = boundOf(story, 'vue');
925
+ const typeImports = new Set<string>();
926
+ const states = bindings.map((binding) => {
927
+ const value = story.props[binding.prop];
928
+ const { type, typeImports: named } = stateType(
929
+ binding,
930
+ value,
931
+ (prop) => `InstanceType<typeof ${story.component}>['$props']['${prop}']`,
932
+ story.exportedTypes
933
+ );
934
+ for (const name of named ?? []) typeImports.add(name);
935
+ return `const ${binding.prop} = ref${type ? `<${type}>` : ''}(${expression(value)});`;
936
+ });
937
+ const bound = new Map(
938
+ bindings.map((binding) => [
939
+ binding.prop,
940
+ [
941
+ binding.vueModel
942
+ ? `v-model="${binding.prop}"`
943
+ : `v-model:${binding.prop}="${binding.prop}"`,
944
+ ],
945
+ ])
946
+ );
947
+ const body = besideLines(
948
+ vue,
949
+ story,
950
+ elementLines(
951
+ vue,
952
+ story.component,
953
+ attributeList(vue, story.props, false, 1, bound),
954
+ story.children,
955
+ story.named,
956
+ 1
957
+ ),
958
+ 1
959
+ );
960
+ // A part is a single-file component of its own, printed after the story's
961
+ // under a comment naming its file; each imports what it uses.
962
+ const parts = partsOf(story);
963
+ const used = new Set([story.component]);
964
+ collectSlotComponents(story.children, used, false);
965
+ for (const fill of [
966
+ ...Object.values(story.named),
967
+ story.around?.before,
968
+ story.around?.after,
969
+ ])
970
+ collectSlotComponents(fill, used, false);
971
+ return [
972
+ '<script setup lang="ts">',
973
+ ...(states.length ? ["import { ref } from 'vue';"] : []),
974
+ ...importLines(story, used, [...typeImports]),
975
+ ...parts.map((part) => `import ${part.name} from './${part.name}.vue';`),
976
+ ...(states.length ? ['', ...states] : []),
977
+ '</script>',
978
+ '',
979
+ '<template>',
980
+ ...body,
981
+ '</template>',
982
+ '',
983
+ ...parts.flatMap((part) => [
984
+ `<!-- ${part.name}.vue -->`,
985
+ '<script setup lang="ts">',
986
+ ...importLines(story, new Set(part.imports)),
987
+ ...(part.imports?.length ? [''] : []),
988
+ part.parameterTypes
989
+ ? `defineProps<{ ${part.parameters.map((name) => `${name}: ${part.parameterTypes?.[name] ?? 'any'}`).join('; ')} }>();`
990
+ : `defineProps([${part.parameters.map((name) => stringLiteral(name)).join(', ')}]);`,
991
+ '</script>',
992
+ '',
993
+ '<template>',
994
+ ...bodyLines(part.vue, 1),
995
+ '</template>',
996
+ '',
997
+ ]),
998
+ ].join('\n');
999
+ }
1000
+
1001
+ /** The story as a Svelte component. */
1002
+ export function svelteSource(given: SourceStory): string {
1003
+ const story = printedStory(given, 'svelte');
1004
+ // A bound prop's state, bound with `bind:`: `$state` in runes mode, a plain
1005
+ // `let` in legacy mode.
1006
+ const bindings = boundOf(story, 'svelte');
1007
+ const runes = story.svelteMode === 'runes';
1008
+ let propsLookup = false;
1009
+ const typeImports = new Set<string>();
1010
+ const states = bindings.map((binding) => {
1011
+ const value = story.props[binding.prop];
1012
+ const {
1013
+ type,
1014
+ propsLookup: looked,
1015
+ typeImports: named,
1016
+ } = stateType(
1017
+ binding,
1018
+ value,
1019
+ (prop) => `ComponentProps<typeof ${story.component}>['${prop}']`,
1020
+ story.exportedTypes
1021
+ );
1022
+ propsLookup ||= looked === true;
1023
+ for (const name of named ?? []) typeImports.add(name);
1024
+ return runes
1025
+ ? `${INDENT}let ${binding.prop} = $state${type ? `<${type}>` : ''}(${expression(value)});`
1026
+ : `${INDENT}let ${binding.prop}${type ? `: ${type}` : ''} = ${expression(value)};`;
1027
+ });
1028
+ const bound = new Map(
1029
+ bindings.map((binding) => [binding.prop, [`bind:${binding.prop}`]])
1030
+ );
1031
+ const body = besideLines(
1032
+ svelte,
1033
+ story,
1034
+ elementLines(
1035
+ svelte,
1036
+ story.component,
1037
+ attributeList(svelte, story.props, false, 0, bound),
1038
+ story.children,
1039
+ story.named,
1040
+ 0
1041
+ ),
1042
+ 0
1043
+ );
1044
+ // A part is a snippet at the top of the markup.
1045
+ const parts = partsOf(story).flatMap((part) => [
1046
+ `{#snippet ${part.name}(${typedParameters(part)})}`,
1047
+ ...bodyLines(part.svelte, 1),
1048
+ '{/snippet}',
1049
+ '',
1050
+ ]);
1051
+ return [
1052
+ '<script lang="ts">',
1053
+ ...(propsLookup
1054
+ ? [`${INDENT}import type { ComponentProps } from 'svelte';`]
1055
+ : []),
1056
+ ...importLines(story, undefined, [...typeImports]).map(
1057
+ (line) => `${INDENT}${line}`
1058
+ ),
1059
+ ...(states.length ? ['', ...states] : []),
1060
+ '</script>',
1061
+ '',
1062
+ ...parts,
1063
+ ...body,
1064
+ '',
1065
+ ].join('\n');
1066
+ }
1067
+
1068
+ /**
1069
+ * A story's props in the order the component declares them, so the source
1070
+ * prints them alike however they were gathered (the template's defaults, the
1071
+ * story file's, a scenario's); a prop the component does not declare follows,
1072
+ * in the order given.
1073
+ *
1074
+ * @param props - The props to print.
1075
+ * @param declared - The component's prop names, in declaration order.
1076
+ * @returns The same props, reordered.
1077
+ */
1078
+ export function inDeclarationOrder(
1079
+ props: Record<string, JsonValue>,
1080
+ declared: readonly string[]
1081
+ ): Record<string, JsonValue> {
1082
+ const ordered: Record<string, JsonValue> = {};
1083
+ for (const name of declared) if (name in props) ordered[name] = props[name];
1084
+ for (const [name, value] of Object.entries(props))
1085
+ if (!(name in ordered)) ordered[name] = value;
1086
+ return ordered;
1087
+ }
1088
+
1089
+ /** The printer per target. */
1090
+ export const SOURCE_PRINTERS: Record<Target, (story: SourceStory) => string> = {
1091
+ react: reactSource,
1092
+ vue: vueSource,
1093
+ svelte: svelteSource,
1094
+ };