(node ? node.appendChild(html) : null)}
- />
- );
- },
- },
-};
diff --git a/addons/docs/src/frameworks/html/prepareForInline.tsx b/addons/docs/src/frameworks/html/prepareForInline.tsx
new file mode 100644
index 000000000000..578f42c0bb34
--- /dev/null
+++ b/addons/docs/src/frameworks/html/prepareForInline.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { StoryFn } from '@storybook/addons';
+
+export function prepareForInline(storyFn: StoryFn
) {
+ const html = storyFn();
+ if (typeof html === 'string') {
+ // eslint-disable-next-line react/no-danger
+ return ;
+ }
+ return (
+ (node ? node.appendChild(html) : null)} />
+ );
+}
diff --git a/addons/docs/src/frameworks/html/sourceDecorator.test.ts b/addons/docs/src/frameworks/html/sourceDecorator.test.ts
new file mode 100644
index 000000000000..6efefd6cfac4
--- /dev/null
+++ b/addons/docs/src/frameworks/html/sourceDecorator.test.ts
@@ -0,0 +1,113 @@
+import { addons, StoryContext } from '@storybook/addons';
+import { sourceDecorator } from './sourceDecorator';
+import { SNIPPET_RENDERED } from '../../shared';
+
+jest.mock('@storybook/addons');
+const mockedAddons = addons as jest.Mocked
;
+
+expect.addSnapshotSerializer({
+ print: (val: any) => val,
+ test: (val) => typeof val === 'string',
+});
+
+const makeContext = (name: string, parameters: any, args: any, extra?: object): StoryContext => ({
+ id: `html-test--${name}`,
+ kind: 'js-text',
+ name,
+ parameters,
+ args,
+ argTypes: {},
+ globals: {},
+ ...extra,
+});
+
+describe('sourceDecorator', () => {
+ let mockChannel: { on: jest.Mock; emit?: jest.Mock };
+ beforeEach(() => {
+ mockedAddons.getChannel.mockReset();
+
+ mockChannel = { on: jest.fn(), emit: jest.fn() };
+ mockedAddons.getChannel.mockReturnValue(mockChannel as any);
+ });
+
+ it('should render dynamically for args stories', () => {
+ const storyFn = (args: any) => `args story
`;
+ const context = makeContext('args', { __isArgsStory: true }, {});
+ sourceDecorator(storyFn, context);
+ expect(mockChannel.emit).toHaveBeenCalledWith(
+ SNIPPET_RENDERED,
+ 'html-test--args',
+ 'args story
'
+ );
+ });
+
+ it('should dedent source by default', () => {
+ const storyFn = (args: any) => `
+
+ args story
+
+ `;
+ const context = makeContext('args', { __isArgsStory: true }, {});
+ sourceDecorator(storyFn, context);
+ expect(mockChannel.emit).toHaveBeenCalledWith(
+ SNIPPET_RENDERED,
+ 'html-test--args',
+ ['', ' args story', '
'].join('\n')
+ );
+ });
+
+ it('should skip dynamic rendering for no-args stories', () => {
+ const storyFn = () => `classic story
`;
+ const context = makeContext('classic', {}, {});
+ sourceDecorator(storyFn, context);
+ expect(mockChannel.emit).not.toHaveBeenCalled();
+ });
+
+ it('should use the originalStoryFn if excludeDecorators is set', () => {
+ const storyFn = (args: any) => `args story
`;
+ const decoratedStoryFn = (args: any) => `
+ ${storyFn(args)}
+ `;
+ const context = makeContext(
+ 'args',
+ {
+ __isArgsStory: true,
+ docs: {
+ source: {
+ excludeDecorators: true,
+ },
+ },
+ },
+ {},
+ { originalStoryFn: storyFn }
+ );
+ sourceDecorator(decoratedStoryFn, context);
+ expect(mockChannel.emit).toHaveBeenCalledWith(
+ SNIPPET_RENDERED,
+ 'html-test--args',
+ 'args story
'
+ );
+ });
+
+ it('allows the snippet output to be modified by transformSource', () => {
+ const storyFn = (args: any) => `args story
`;
+ const transformSource = (dom: string) => `${dom}
`;
+ const docs = { transformSource };
+ const context = makeContext('args', { __isArgsStory: true, docs }, {});
+ sourceDecorator(storyFn, context);
+ expect(mockChannel.emit).toHaveBeenCalledWith(
+ SNIPPET_RENDERED,
+ 'html-test--args',
+ 'args story
'
+ );
+ });
+
+ it('provides the story context to transformSource', () => {
+ const storyFn = (args: any) => `args story
`;
+ const transformSource = jest.fn((x) => x);
+ const docs = { transformSource };
+ const context = makeContext('args', { __isArgsStory: true, docs }, {});
+ sourceDecorator(storyFn, context);
+ expect(transformSource).toHaveBeenCalledWith('args story
', context);
+ });
+});
diff --git a/addons/docs/src/frameworks/html/sourceDecorator.ts b/addons/docs/src/frameworks/html/sourceDecorator.ts
new file mode 100644
index 000000000000..fa45e820ed4c
--- /dev/null
+++ b/addons/docs/src/frameworks/html/sourceDecorator.ts
@@ -0,0 +1,44 @@
+/* global window */
+import { addons, StoryContext, StoryFn } from '@storybook/addons';
+import dedent from 'ts-dedent';
+import { SNIPPET_RENDERED, SourceType } from '../../shared';
+
+function skipSourceRender(context: StoryContext) {
+ const sourceParams = context?.parameters.docs?.source;
+ const isArgsStory = context?.parameters.__isArgsStory;
+
+ // always render if the user forces it
+ if (sourceParams?.type === SourceType.DYNAMIC) {
+ return false;
+ }
+
+ // never render if the user is forcing the block to render code, or
+ // if the user provides code, or if it's not an args story.
+ return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE;
+}
+
+// By default, just remove indentation
+function defaultTransformSource(source: string) {
+ // Have to wrap dedent so it doesn't serialize the context
+ return dedent(source);
+}
+
+function applyTransformSource(source: string, context: StoryContext): string {
+ const docs = context.parameters.docs ?? {};
+ const transformSource = docs.transformSource ?? defaultTransformSource;
+ return transformSource(source, context);
+}
+
+export function sourceDecorator(storyFn: StoryFn, context: StoryContext) {
+ const story = context?.parameters.docs?.source?.excludeDecorators
+ ? context.originalStoryFn(context.args)
+ : storyFn();
+
+ if (typeof story === 'string' && !skipSourceRender(context)) {
+ const source = applyTransformSource(story, context);
+
+ addons.getChannel().emit(SNIPPET_RENDERED, context.id, source);
+ }
+
+ return story;
+}
diff --git a/docs/frameworks.js b/docs/frameworks.js
index 4848ca94a20b..b777951f33b0 100644
--- a/docs/frameworks.js
+++ b/docs/frameworks.js
@@ -121,7 +121,7 @@ module.exports = {
},
{
name: 'Dynamic source',
- supported: ['react', 'vue', 'angular', 'svelte', 'web-components'],
+ supported: ['react', 'vue', 'angular', 'svelte', 'web-components', 'html'],
path: 'writing-docs/doc-blocks#source',
},
{
diff --git a/examples/html-kitchen-sink/.storybook/preview.js b/examples/html-kitchen-sink/.storybook/preview.js
index 4d8de85177bc..00412fe3b266 100644
--- a/examples/html-kitchen-sink/.storybook/preview.js
+++ b/examples/html-kitchen-sink/.storybook/preview.js
@@ -1,7 +1,5 @@
import { addParameters } from '@storybook/html';
-const SOURCE_REGEX = /^\(\) => [`'"](.*)['`"]$/;
-
addParameters({
a11y: {
config: {},
@@ -12,9 +10,5 @@ addParameters({
},
docs: {
iframeHeight: '200px',
- transformSource: (src) => {
- const match = SOURCE_REGEX.exec(src);
- return match ? match[1] : src;
- },
},
});
diff --git a/examples/html-kitchen-sink/stories/__snapshots__/addon-docs.stories.storyshot b/examples/html-kitchen-sink/stories/__snapshots__/addon-docs.stories.storyshot
index 63efe8baf032..e4acdbf09b6b 100644
--- a/examples/html-kitchen-sink/stories/__snapshots__/addon-docs.stories.storyshot
+++ b/examples/html-kitchen-sink/stories/__snapshots__/addon-docs.stories.storyshot
@@ -18,6 +18,12 @@ exports[`Storyshots Addons/Docs heading 1`] = `
`;
+exports[`Storyshots Addons/Docs standard source 1`] = `
+
+ Standard source
+
+`;
+
exports[`Storyshots Addons/Docs transformed source 1`] = `
Some source
diff --git a/examples/html-kitchen-sink/stories/addon-docs.stories.mdx b/examples/html-kitchen-sink/stories/addon-docs.stories.mdx
index 81a113956ebb..b844dc5e346d 100644
--- a/examples/html-kitchen-sink/stories/addon-docs.stories.mdx
+++ b/examples/html-kitchen-sink/stories/addon-docs.stories.mdx
@@ -25,6 +25,14 @@ How you like them apples?!
}}
+## Standard source
+
+
+
## Custom source