UNPKG

@storybook/addon-docs

Version:

Document component usage and properties in Markdown

195 lines (147 loc) • 7.1 kB
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, '&quot;')}"`; } /** * 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; }