@storybook/addon-docs
Version:
Document component usage and properties in Markdown
195 lines (147 loc) • 7.1 kB
JavaScript
import "core-js/modules/es.array.reduce.js";
/* eslint no-underscore-dangle: ["error", { "allow": ["_vnode"] }] */
import { addons } from '@storybook/addons';
import { logger } from '@storybook/client-logger';
import { SourceType, SNIPPET_RENDERED } from '../../shared';
export const skipSourceRender = context => {
var _context$parameters$d;
const sourceParams = context === null || context === void 0 ? void 0 : (_context$parameters$d = context.parameters.docs) === null || _context$parameters$d === void 0 ? void 0 : _context$parameters$d.source;
const isArgsStory = context === null || context === void 0 ? void 0 : context.parameters.__isArgsStory; // always render if the user forces it
if ((sourceParams === null || sourceParams === void 0 ? void 0 : 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 === null || sourceParams === void 0 ? void 0 : sourceParams.code) || (sourceParams === null || sourceParams === void 0 ? void 0 : sourceParams.type) === SourceType.CODE;
};
export const sourceDecorator = (storyFn, context) => {
const story = storyFn(); // See ../react/jsxDecorator.tsx
if (skipSourceRender(context)) {
return story;
}
const channel = addons.getChannel();
const storyComponent = getStoryComponent(story.options.STORYBOOK_WRAPS);
return {
components: {
Story: story
},
// We need to wait until the wrapper component to be mounted so Vue runtime
// struct VNode tree. We get `this._vnode == null` if switch to `created`
// lifecycle hook.
mounted() {
// Theoretically this does not happens but we need to check it.
if (!this._vnode) {
return;
}
try {
const storyNode = lookupStoryInstance(this, storyComponent);
const code = vnodeToString(storyNode._vnode);
const emitFormattedTemplate = async () => {
const prettier = await import('prettier/standalone');
const prettierHtml = await import('prettier/parser-html');
channel.emit(SNIPPET_RENDERED, (context || {}).id, prettier.format(`<template>${code}</template>`, {
parser: 'vue',
plugins: [prettierHtml],
// Because the parsed vnode missing spaces right before/after the surround tag,
// we always get weird wrapped code without this option.
htmlWhitespaceSensitivity: 'ignore'
}));
};
emitFormattedTemplate();
} catch (e) {
logger.warn(`Failed to generate dynamic story source: ${e}`);
}
},
template: '<story />'
};
};
export function vnodeToString(vnode) {
var _vnode$data, _vnode$componentOptio, _vnode$data2;
const attrString = [...((_vnode$data = vnode.data) !== null && _vnode$data !== void 0 && _vnode$data.slot ? [['slot', vnode.data.slot]] : []), ['class', stringifyClassAttribute(vnode)], ...((_vnode$componentOptio = vnode.componentOptions) !== null && _vnode$componentOptio !== void 0 && _vnode$componentOptio.propsData ? Object.entries(vnode.componentOptions.propsData) : []), ...((_vnode$data2 = vnode.data) !== null && _vnode$data2 !== void 0 && _vnode$data2.attrs ? Object.entries(vnode.data.attrs) : [])].filter(([name], index, list) => list.findIndex(item => item[0] === name) === index).map(([name, value]) => stringifyAttr(name, value)).filter(Boolean).join(' ');
if (!vnode.componentOptions) {
// Non-component elements (div, span, etc...)
if (vnode.tag) {
if (!vnode.children) {
return `<${vnode.tag} ${attrString}/>`;
}
return `<${vnode.tag} ${attrString}>${vnode.children.map(vnodeToString).join('')}</${vnode.tag}>`;
} // TextNode
if (vnode.text) {
if (/[<>"&]/.test(vnode.text)) {
return `{{\`${vnode.text.replace(/`/g, '\\`')}\`}}`;
}
return vnode.text;
} // Unknown
return '';
} // Probably users never see the "unknown-component". It seems that vnode.tag
// is always set.
const tag = vnode.componentOptions.tag || vnode.tag || 'unknown-component';
if (!vnode.componentOptions.children) {
return `<${tag} ${attrString}/>`;
}
return `<${tag} ${attrString}>${vnode.componentOptions.children.map(vnodeToString).join('')}</${tag}>`;
}
function stringifyClassAttribute(vnode) {
var _vnode$data$staticCla, _vnode$data$staticCla2;
if (!vnode.data || !vnode.data.staticClass && !vnode.data.class) {
return undefined;
}
return [...((_vnode$data$staticCla = (_vnode$data$staticCla2 = vnode.data.staticClass) === null || _vnode$data$staticCla2 === void 0 ? void 0 : _vnode$data$staticCla2.split(' ')) !== null && _vnode$data$staticCla !== void 0 ? _vnode$data$staticCla : []), ...normalizeClassBinding(vnode.data.class)].filter(Boolean).join(' ') || undefined;
} // https://vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes
function normalizeClassBinding(binding) {
if (!binding) {
return [];
}
if (typeof binding === 'string') {
return [binding];
}
if (binding instanceof Array) {
// To handle an object-in-array binding smartly, we use recursion
return binding.map(normalizeClassBinding).reduce((a, b) => [...a, ...b], []);
}
if (typeof binding === 'object') {
return Object.entries(binding).filter(([, active]) => !!active).map(([className]) => className);
} // Unknown class binding
return [];
}
function stringifyAttr(attrName, value) {
if (typeof value === 'undefined' || typeof value === 'function') {
return null;
}
if (value === true) {
return attrName;
}
if (typeof value === 'string') {
return `${attrName}=${quote(value)}`;
} // TODO: Better serialization (unquoted object key, Symbol/Classes, etc...)
// Seems like Prettier don't format JSON-look object (= when keys are quoted)
return `:${attrName}=${quote(JSON.stringify(value))}`;
}
function quote(value) {
return value.includes(`"`) && !value.includes(`'`) ? `'${value}'` : `"${value.replace(/"/g, '"')}"`;
}
/**
* Skip decorators and grab a story component itself.
* https://github.com/pocka/storybook-addon-vue-info/pull/113
*/
function getStoryComponent(w) {
let matched = w;
while (matched && matched.options && matched.options.components && matched.options.components.story && matched.options.components.story.options && matched.options.components.story.options.STORYBOOK_WRAPS) {
matched = matched.options.components.story.options.STORYBOOK_WRAPS;
}
return matched;
}
/**
* Find the story's instance from VNode tree.
*/
function lookupStoryInstance(instance, storyComponent) {
if (instance.$vnode && instance.$vnode.componentOptions && instance.$vnode.componentOptions.Ctor === storyComponent) {
return instance;
}
for (let i = 0, l = instance.$children.length; i < l; i += 1) {
const found = lookupStoryInstance(instance.$children[i], storyComponent);
if (found) {
return found;
}
}
return null;
}