@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.
- package/LICENSE +21 -0
- package/README.md +1868 -2
- package/application/host/bootstrap.ts +43 -0
- package/application/host/main.ts +31 -0
- package/application/host/shell-host.ts +47 -0
- package/application/index.html +12 -0
- package/application/lib/base-url.ts +15 -0
- package/application/lib/no-code-block.ts +7 -0
- package/application/lib/no-setup.ts +3 -0
- package/application/lib/no-views.ts +6 -0
- package/application/preview/code/code.html +12 -0
- package/application/preview/code/main.ts +89 -0
- package/application/preview/globals.ts +254 -0
- package/application/preview/html/main.ts +93 -0
- package/application/preview/html/preview.html +12 -0
- package/application/preview/react/main.tsx +146 -0
- package/application/preview/react/preview.html +12 -0
- package/application/preview/svelte/Preview.svelte +104 -0
- package/application/preview/svelte/main.ts +13 -0
- package/application/preview/svelte/preview.html +12 -0
- package/application/preview/svelte/story-props.ts +172 -0
- package/application/preview/view/main.ts +124 -0
- package/application/preview/view/view.html +12 -0
- package/application/preview/vue/main.ts +145 -0
- package/application/preview/vue/preview.html +12 -0
- package/application/shells/charcell/components/Application/index.css +37 -0
- package/application/shells/charcell/components/Application/index.tsx +65 -0
- package/application/shells/charcell/components/Bar/index.css +68 -0
- package/application/shells/charcell/components/Bar/index.tsx +60 -0
- package/application/shells/charcell/components/CodePane/index.css +60 -0
- package/application/shells/charcell/components/CodePane/index.tsx +113 -0
- package/application/shells/charcell/components/ComponentStage/index.css +26 -0
- package/application/shells/charcell/components/ComponentStage/index.tsx +110 -0
- package/application/shells/charcell/components/Control/index.css +20 -0
- package/application/shells/charcell/components/Control/index.tsx +118 -0
- package/application/shells/charcell/components/CopyLink/index.tsx +35 -0
- package/application/shells/charcell/components/Description/index.css +6 -0
- package/application/shells/charcell/components/Description/index.tsx +24 -0
- package/application/shells/charcell/components/EditorAddDialog/index.css +9 -0
- package/application/shells/charcell/components/EditorAddDialog/index.tsx +158 -0
- package/application/shells/charcell/components/EditorPanel/index.css +48 -0
- package/application/shells/charcell/components/EditorPanel/index.tsx +496 -0
- package/application/shells/charcell/components/EventsRow/index.css +42 -0
- package/application/shells/charcell/components/EventsRow/index.tsx +68 -0
- package/application/shells/charcell/components/HeadingRow/index.css +11 -0
- package/application/shells/charcell/components/HeadingRow/index.tsx +16 -0
- package/application/shells/charcell/components/Hint/index.css +16 -0
- package/application/shells/charcell/components/Hint/index.tsx +81 -0
- package/application/shells/charcell/components/HtmlWarnings/index.tsx +45 -0
- package/application/shells/charcell/components/KeyHints/index.css +27 -0
- package/application/shells/charcell/components/KeyHints/index.tsx +106 -0
- package/application/shells/charcell/components/KitCodeBlock/index.css +55 -0
- package/application/shells/charcell/components/KitCodeBlock/index.tsx +18 -0
- package/application/shells/charcell/components/Markdown/index.tsx +16 -0
- package/application/shells/charcell/components/Navigation/index.css +13 -0
- package/application/shells/charcell/components/Navigation/index.tsx +98 -0
- package/application/shells/charcell/components/NavigationSplit/index.css +16 -0
- package/application/shells/charcell/components/NavigationSplit/index.tsx +82 -0
- package/application/shells/charcell/components/OptionControls/index.tsx +93 -0
- package/application/shells/charcell/components/PaneStatus/index.css +7 -0
- package/application/shells/charcell/components/PaneStatus/index.tsx +47 -0
- package/application/shells/charcell/components/PaneWidth/index.tsx +45 -0
- package/application/shells/charcell/components/PreviewGrid/index.css +39 -0
- package/application/shells/charcell/components/PreviewGrid/index.tsx +344 -0
- package/application/shells/charcell/components/PreviewPane/index.css +127 -0
- package/application/shells/charcell/components/PreviewPane/index.tsx +182 -0
- package/application/shells/charcell/components/PromptBar/index.tsx +133 -0
- package/application/shells/charcell/components/ScenarioRow/index.css +36 -0
- package/application/shells/charcell/components/ScenarioRow/index.tsx +71 -0
- package/application/shells/charcell/components/Select/index.tsx +48 -0
- package/application/shells/charcell/components/SlotOutlet/index.css +7 -0
- package/application/shells/charcell/components/SlotOutlet/index.tsx +25 -0
- package/application/shells/charcell/components/StageBar/index.css +7 -0
- package/application/shells/charcell/components/StageBar/index.tsx +109 -0
- package/application/shells/charcell/components/StageSplit/index.css +11 -0
- package/application/shells/charcell/components/StageSplit/index.tsx +99 -0
- package/application/shells/charcell/components/StatusBar/index.tsx +30 -0
- package/application/shells/charcell/components/ViewControls/index.css +15 -0
- package/application/shells/charcell/components/ViewControls/index.tsx +58 -0
- package/application/shells/charcell/components/ViewHeading/index.css +13 -0
- package/application/shells/charcell/components/ViewHeading/index.tsx +8 -0
- package/application/shells/charcell/components/ViewStage/index.css +9 -0
- package/application/shells/charcell/components/ViewStage/index.tsx +25 -0
- package/application/shells/charcell/components/WrapToggle/index.tsx +27 -0
- package/application/shells/charcell/configuration.ts +45 -0
- package/application/shells/charcell/controller-context.ts +27 -0
- package/application/shells/charcell/fit-labels.ts +42 -0
- package/application/shells/charcell/html-warnings.ts +18 -0
- package/application/shells/charcell/index.tsx +82 -0
- package/application/shells/charcell/page-loading.html +57 -0
- package/application/shells/charcell/placement.ts +24 -0
- package/application/shells/charcell/shell-options.ts +23 -0
- package/application/shells/charcell/shell.css +37 -0
- package/application/shells/charcell/shell.tsx +25 -0
- package/application/shells/charcell/split-reset.ts +47 -0
- package/application/shells/charcell/stacked-rows.ts +117 -0
- package/application/shells/charcell/stage-toggles.ts +93 -0
- package/application/types/manifest.d.ts +6 -0
- package/application/types/preview-configuration.d.ts +10 -0
- package/application/types/preview-shell.d.ts +7 -0
- package/application/types/react-registry.d.ts +15 -0
- package/application/types/vue-registry.d.ts +6 -0
- package/dist/build.d.ts +24 -0
- package/dist/build.js +34 -0
- package/dist/check.d.ts +48 -0
- package/dist/check.js +781 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +88 -0
- package/dist/context.d.ts +179 -0
- package/dist/context.js +783 -0
- package/dist/controller/catalog.d.ts +339 -0
- package/dist/controller/catalog.js +367 -0
- package/dist/controller/create-preview-controller.d.ts +532 -0
- package/dist/controller/create-preview-controller.js +1707 -0
- package/dist/controller/editor-tree.d.ts +161 -0
- package/dist/controller/editor-tree.js +373 -0
- package/dist/controller/html-source.d.ts +62 -0
- package/dist/controller/html-source.js +61 -0
- package/dist/controller/index.d.ts +9 -0
- package/dist/controller/index.js +12 -0
- package/dist/controller/persistence.d.ts +191 -0
- package/dist/controller/persistence.js +315 -0
- package/dist/controller/regeneration.d.ts +30 -0
- package/dist/controller/regeneration.js +27 -0
- package/dist/controller/source.d.ts +80 -0
- package/dist/controller/source.js +236 -0
- package/dist/controller/store.d.ts +27 -0
- package/dist/controller/store.js +40 -0
- package/dist/declarations/application/shells/charcell/components/Application/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Bar/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/CodePane/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/ComponentStage/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Control/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/CopyLink/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/Description/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/EditorAddDialog/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/EditorPanel/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/EventsRow/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/HeadingRow/index.d.ts +6 -0
- package/dist/declarations/application/shells/charcell/components/Hint/index.d.ts +11 -0
- package/dist/declarations/application/shells/charcell/components/HtmlWarnings/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/KeyHints/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/KitCodeBlock/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/Markdown/index.d.ts +3 -0
- package/dist/declarations/application/shells/charcell/components/Navigation/index.d.ts +8 -0
- package/dist/declarations/application/shells/charcell/components/NavigationSplit/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/OptionControls/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PaneStatus/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/PaneWidth/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/PreviewGrid/index.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/components/PreviewPane/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/PromptBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ScenarioRow/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/Select/index.d.ts +16 -0
- package/dist/declarations/application/shells/charcell/components/SlotOutlet/index.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/components/StageBar/index.d.ts +2 -0
- package/dist/declarations/application/shells/charcell/components/StageSplit/index.d.ts +17 -0
- package/dist/declarations/application/shells/charcell/components/StatusBar/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/components/ViewControls/index.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/components/ViewHeading/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/ViewStage/index.d.ts +4 -0
- package/dist/declarations/application/shells/charcell/components/WrapToggle/index.d.ts +1 -0
- package/dist/declarations/application/shells/charcell/configuration.d.ts +31 -0
- package/dist/declarations/application/shells/charcell/controller-context.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/fit-labels.d.ts +18 -0
- package/dist/declarations/application/shells/charcell/html-warnings.d.ts +5 -0
- package/dist/declarations/application/shells/charcell/index.d.ts +39 -0
- package/dist/declarations/application/shells/charcell/placement.d.ts +7 -0
- package/dist/declarations/application/shells/charcell/shell-options.d.ts +15 -0
- package/dist/declarations/application/shells/charcell/shell.d.ts +9 -0
- package/dist/declarations/application/shells/charcell/split-reset.d.ts +30 -0
- package/dist/declarations/application/shells/charcell/stacked-rows.d.ts +58 -0
- package/dist/declarations/application/shells/charcell/stage-toggles.d.ts +28 -0
- package/dist/declarations/src/controller/catalog.d.ts +339 -0
- package/dist/declarations/src/controller/create-preview-controller.d.ts +532 -0
- package/dist/declarations/src/controller/editor-tree.d.ts +161 -0
- package/dist/declarations/src/controller/html-source.d.ts +62 -0
- package/dist/declarations/src/controller/index.d.ts +9 -0
- package/dist/declarations/src/controller/persistence.d.ts +191 -0
- package/dist/declarations/src/controller/regeneration.d.ts +30 -0
- package/dist/declarations/src/controller/source.d.ts +80 -0
- package/dist/declarations/src/controller/store.d.ts +27 -0
- package/dist/declarations/src/emit/common.d.ts +103 -0
- package/dist/declarations/src/emit/manifest.d.ts +138 -0
- package/dist/declarations/src/emit/source.d.ts +87 -0
- package/dist/declarations/src/lib/jsx-roots.d.ts +22 -0
- package/dist/declarations/src/lib/module-of-source.d.ts +18 -0
- package/dist/declarations/src/lib/naming.d.ts +29 -0
- package/dist/declarations/src/lib/react-spelling.d.ts +68 -0
- package/dist/declarations/src/lib/scenario-story.d.ts +41 -0
- package/dist/declarations/src/lib/slot-fill.d.ts +15 -0
- package/dist/declarations/src/lib/types.d.ts +741 -0
- package/dist/declarations/src/options.d.ts +224 -0
- package/dist/declarations/src/protocol.d.ts +541 -0
- package/dist/declarations/src/react-view.d.ts +22 -0
- package/dist/declarations/src/react.d.ts +17 -0
- package/dist/declarations/src/shell.d.ts +99 -0
- package/dist/declarations/src/views.d.ts +152 -0
- package/dist/development-server.d.ts +121 -0
- package/dist/development-server.js +331 -0
- package/dist/emit/common.d.ts +103 -0
- package/dist/emit/common.js +170 -0
- package/dist/emit/html-source.d.ts +46 -0
- package/dist/emit/html-source.js +322 -0
- package/dist/emit/manifest.d.ts +138 -0
- package/dist/emit/manifest.js +74 -0
- package/dist/emit/react.d.ts +16 -0
- package/dist/emit/react.js +210 -0
- package/dist/emit/scratch.d.ts +40 -0
- package/dist/emit/scratch.js +116 -0
- package/dist/emit/source.d.ts +87 -0
- package/dist/emit/source.js +789 -0
- package/dist/emit/svelte.d.ts +9 -0
- package/dist/emit/svelte.js +177 -0
- package/dist/emit/vue.d.ts +2 -0
- package/dist/emit/vue.js +152 -0
- package/dist/enumerate.d.ts +2 -0
- package/dist/enumerate.js +162 -0
- package/dist/generate.d.ts +43 -0
- package/dist/generate.js +302 -0
- package/dist/html-stylesheet.d.ts +43 -0
- package/dist/html-stylesheet.js +145 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +21 -0
- package/dist/landing.d.ts +16 -0
- package/dist/landing.js +45 -0
- package/dist/lib/exported-types.d.ts +42 -0
- package/dist/lib/exported-types.js +86 -0
- package/dist/lib/html-attributes.d.ts +2 -0
- package/dist/lib/html-attributes.js +111 -0
- package/dist/lib/html-warnings.d.ts +19 -0
- package/dist/lib/html-warnings.js +79 -0
- package/dist/lib/jsx-roots.d.ts +22 -0
- package/dist/lib/jsx-roots.js +243 -0
- package/dist/lib/module-of-source.d.ts +18 -0
- package/dist/lib/module-of-source.js +24 -0
- package/dist/lib/naming.d.ts +29 -0
- package/dist/lib/naming.js +56 -0
- package/dist/lib/parse-type.d.ts +32 -0
- package/dist/lib/parse-type.js +103 -0
- package/dist/lib/react-spelling.d.ts +68 -0
- package/dist/lib/react-spelling.js +197 -0
- package/dist/lib/scenario-story.d.ts +41 -0
- package/dist/lib/scenario-story.js +84 -0
- package/dist/lib/slot-fill.d.ts +15 -0
- package/dist/lib/slot-fill.js +92 -0
- package/dist/lib/template-files.d.ts +14 -0
- package/dist/lib/template-files.js +25 -0
- package/dist/lib/types.d.ts +741 -0
- package/dist/lib/types.js +13 -0
- package/dist/lib/void-elements.d.ts +7 -0
- package/dist/lib/void-elements.js +13 -0
- package/dist/load.d.ts +37 -0
- package/dist/load.js +82 -0
- package/dist/model.d.ts +44 -0
- package/dist/model.js +464 -0
- package/dist/options.d.ts +224 -0
- package/dist/options.js +223 -0
- package/dist/prop-types.d.ts +26 -0
- package/dist/prop-types.js +102 -0
- package/dist/props-from.d.ts +84 -0
- package/dist/props-from.js +510 -0
- package/dist/protocol.d.ts +541 -0
- package/dist/protocol.js +819 -0
- package/dist/react-view.d.ts +22 -0
- package/dist/react-view.js +36 -0
- package/dist/react.d.ts +17 -0
- package/dist/react.js +23 -0
- package/dist/sass-importers.d.ts +26 -0
- package/dist/sass-importers.js +32 -0
- package/dist/shell-acceptance.d.ts +21 -0
- package/dist/shell-acceptance.js +37 -0
- package/dist/shell-page.d.ts +36 -0
- package/dist/shell-page.js +90 -0
- package/dist/shell-peers.d.ts +38 -0
- package/dist/shell-peers.js +61 -0
- package/dist/shell.d.ts +99 -0
- package/dist/shell.js +52 -0
- package/dist/shells/charcell/page.d.ts +8 -0
- package/dist/shells/charcell/page.js +69 -0
- package/dist/story-source.d.ts +113 -0
- package/dist/story-source.js +271 -0
- package/dist/story.d.ts +5 -0
- package/dist/story.js +28 -0
- package/dist/testing.d.ts +218 -0
- package/dist/testing.js +284 -0
- package/dist/views.d.ts +152 -0
- package/dist/views.js +7 -0
- package/dist/vite-configuration.d.ts +114 -0
- package/dist/vite-configuration.js +578 -0
- package/package.json +123 -3
- package/src/build.ts +51 -0
- package/src/check.ts +1062 -0
- package/src/cli.ts +123 -0
- package/src/context.ts +1168 -0
- package/src/controller/catalog.ts +719 -0
- package/src/controller/create-preview-controller.ts +2683 -0
- package/src/controller/editor-tree.ts +530 -0
- package/src/controller/html-source.ts +114 -0
- package/src/controller/index.ts +105 -0
- package/src/controller/persistence.ts +403 -0
- package/src/controller/regeneration.ts +55 -0
- package/src/controller/source.ts +333 -0
- package/src/controller/store.ts +55 -0
- package/src/development-server.ts +442 -0
- package/src/emit/common.ts +201 -0
- package/src/emit/html-source.ts +402 -0
- package/src/emit/manifest.ts +241 -0
- package/src/emit/react.ts +263 -0
- package/src/emit/scratch.ts +175 -0
- package/src/emit/source.ts +1094 -0
- package/src/emit/svelte.ts +230 -0
- package/src/emit/vue.ts +194 -0
- package/src/enumerate.ts +220 -0
- package/src/generate.ts +443 -0
- package/src/html-stylesheet.ts +173 -0
- package/src/index.ts +60 -0
- package/src/landing.ts +58 -0
- package/src/lib/exported-types.ts +114 -0
- package/src/lib/html-attributes.ts +113 -0
- package/src/lib/html-warnings.ts +81 -0
- package/src/lib/jsx-roots.ts +238 -0
- package/src/lib/module-of-source.ts +26 -0
- package/src/lib/naming.ts +65 -0
- package/src/lib/parse-type.ts +143 -0
- package/src/lib/react-spelling.ts +235 -0
- package/src/lib/scenario-story.ts +97 -0
- package/src/lib/slot-fill.ts +102 -0
- package/src/lib/template-files.ts +31 -0
- package/src/lib/types.ts +778 -0
- package/src/lib/void-elements.ts +15 -0
- package/src/load.ts +144 -0
- package/src/model.ts +568 -0
- package/src/options.ts +435 -0
- package/src/prop-types.ts +133 -0
- package/src/props-from.ts +678 -0
- package/src/protocol.ts +1135 -0
- package/src/react-view.ts +45 -0
- package/src/react.ts +34 -0
- package/src/sass-importers.ts +45 -0
- package/src/shell-acceptance.ts +45 -0
- package/src/shell-page.ts +133 -0
- package/src/shell-peers.ts +70 -0
- package/src/shell.ts +194 -0
- package/src/shells/charcell/page.ts +82 -0
- package/src/story-source.ts +391 -0
- package/src/story.ts +42 -0
- package/src/testing.ts +497 -0
- package/src/views.ts +183 -0
- 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, '"')}"`;
|
|
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
|
+
};
|