UNPKG

13.6 kBJavaScriptView Raw
1export { Link } from 'gatsby';
2import { createContext, createElement, useContext, Fragment, Component, useMemo, useEffect, useRef, useState, memo } from 'react';
3import _merge from 'lodash/fp/merge';
4import equal from 'fast-deep-equal';
5import _kebabCase from 'lodash/fp/kebabCase';
6import _mapValues from 'lodash/fp/mapValues';
7import _get from 'lodash/fp/get';
8import _first from 'lodash/fp/first';
9import _assoc from 'lodash/fp/assoc';
10import { pascalCase } from 'pascal-case';
11import marksy from 'marksy';
12import sort from 'array-sort';
13import _unionBy from 'lodash/fp/unionBy';
14import _flattenDepth from 'lodash/fp/flattenDepth';
15import _omit from 'lodash/fp/omit';
16import _pipe from 'lodash/fp/pipe';
17import { ulid } from 'ulid';
18import match from 'match-sorter';
19import _throttle from 'lodash/fp/throttle';
20import { __rest } from 'tslib';
21import capitalize from 'capitalize';
22
23const DefNotFound = () => createElement(Fragment, null, "Not found");
24
25const DefLayout = ({
26 children
27}) => createElement(Fragment, null, children);
28
29const DefPlayground = ({
30 component,
31 code
32}) => createElement("div", null, component, createElement("pre", null, code));
33
34const defaultComponents = {
35 layout: DefLayout,
36 notFound: DefNotFound,
37 playground: DefPlayground
38};
39const ctx = createContext(defaultComponents);
40const ComponentsProvider = ({
41 components: themeComponents = {},
42 children
43}) => createElement(ctx.Provider, {
44 value: Object.assign({}, defaultComponents, themeComponents)
45}, children);
46const useComponents = () => {
47 return useContext(ctx);
48};
49
50function create(initial) {
51 var _a;
52
53 const ctx = createContext(initial);
54 const listeners = new Set();
55
56 const dispatch = fn => {
57 listeners.forEach(listener => listener(fn));
58 };
59
60 return {
61 context: ctx,
62 set: fn => dispatch(fn),
63 Provider: (_a = class Provider extends Component {
64 constructor() {
65 super(...arguments);
66 this.state = this.props.initial || initial || {};
67 }
68
69 static getDerivedStateFromProps(props, state) {
70 if (!equal(props.initial, state)) return props.initial;
71 return null;
72 }
73
74 componentDidMount() {
75 listeners.add(fn => this.setState(fn));
76 }
77
78 componentWillUnmount() {
79 listeners.clear();
80 }
81
82 render() {
83 return createElement(ctx.Provider, {
84 value: this.state
85 }, this.props.children);
86 }
87
88 }, _a.displayName = 'DoczStateProvider', _a)
89 };
90}
91
92const doczState = create({});
93
94const useConfig = () => {
95 const state = useContext(doczState.context);
96 const {
97 transform,
98 config,
99 themeConfig = {}
100 } = state;
101
102 const newConfig = _merge(themeConfig, config ? config.themeConfig : {});
103
104 const transformed = transform ? transform(newConfig) : newConfig;
105 return Object.assign({}, config, {
106 themeConfig: transformed
107 });
108};
109
110const useComponentProps = ({
111 componentName,
112 fileName
113}) => {
114 const components = useComponents();
115 const {
116 props: stateProps
117 } = useContext(doczState.context);
118
119 const componentMatcher = (componentName, item) => {
120 const matchingPatterns = [fileName, `/${componentName}.`, `/${_kebabCase(componentName)}.`, `/${pascalCase(componentName)}.`];
121 return !!matchingPatterns.find(pattern => item.key.includes(pattern));
122 };
123
124 const found = stateProps && stateProps.length > 0 && stateProps.find(item => componentMatcher(componentName, item));
125 const value = _get('value', found) || [];
126
127 const firstDefinition = _first(value);
128
129 const definition = value.find(i => i.displayName === componentName);
130 const compile = useMemo(() => marksy({
131 createElement,
132 elements: components
133 }), [components]);
134 const props = useMemo(() => {
135 const props = _get('props', definition || firstDefinition);
136
137 const parseDescs = _mapValues(prop => {
138 const desc = _get('description', prop);
139
140 return !desc ? prop : _assoc('description', compile(desc).tree, prop);
141 });
142
143 return parseDescs(props);
144 }, [compile, definition || firstDefinition]);
145 return props;
146};
147
148const useCurrentDoc = () => {
149 const state = useContext(doczState.context);
150 return _get('currentEntry.value', state);
151};
152
153const updateState = ev => {
154 const {
155 type,
156 payload
157 } = JSON.parse(ev.data);
158 const prop = type.startsWith('state.') && type.split('.')[1];
159
160 if (prop) {
161 doczState.set(state => Object.assign({}, state, {
162 [prop]: payload
163 }));
164 }
165};
166
167const useDataServer = url => {
168 useEffect(() => {
169 if (!url) return;
170 const socket = new WebSocket(url);
171 socket.onmessage = updateState;
172 return () => socket.close();
173 }, []);
174};
175
176function flatArrFromObject(arr, prop) {
177 const reducer = (arr, obj) => {
178 const value = _get(prop)(obj);
179
180 return value ? arr.concat([value]) : arr;
181 };
182
183 return Array.from(new Set(arr.reduce(reducer, [])));
184}
185function compare(a, b, reverse) {
186 if (a < b) return reverse ? 1 : -1;
187 if (a > b) return reverse ? -1 : 1;
188 return 0;
189}
190
191const useDocs = () => {
192 const {
193 entries = []
194 } = useContext(doczState.context);
195 const arr = entries.map(({
196 value
197 }) => value);
198 return sort(arr, (a, b) => compare(a.name, b.name));
199};
200
201const noMenu = entry => !entry.menu;
202
203const fromMenu = menu => entry => entry.menu === menu;
204
205const entriesOfMenu = (menu, entries) => entries.filter(fromMenu(menu));
206
207const parseMenu = entries => name => ({
208 name,
209 menu: entriesOfMenu(name, entries)
210});
211
212const menusFromEntries = entries => {
213 const entriesWithoutMenu = entries.filter(noMenu);
214 const menus = flatArrFromObject(entries, 'menu').map(parseMenu(entries));
215 return _unionBy('name', menus, entriesWithoutMenu);
216};
217
218const parseItemStr = item => typeof item === 'string' ? {
219 name: item
220} : item;
221
222const normalize = item => {
223 const selected = parseItemStr(item);
224 return Object.assign({}, selected, {
225 id: selected.id || ulid(),
226 parent: _get('parent', selected) || _get('parent', item),
227 menu: Array.isArray(selected.menu) ? selected.menu.map(normalize) : selected.menu
228 });
229};
230
231const clean = item => item.href || item.route ? _omit('menu', item) : item;
232
233const normalizeAndClean = _pipe(normalize, clean);
234
235const mergeMenus = (entriesMenu, configMenu) => {
236 const first = entriesMenu.map(normalizeAndClean);
237 const second = configMenu.map(normalizeAndClean);
238
239 const merged = _unionBy('name', first, second);
240
241 return merged.map(item => {
242 if (!item.menu) return item;
243 const found = second.find(i => i.name === item.name);
244 const foundMenu = found && found.menu;
245 return Object.assign({}, item, {
246 menu: foundMenu ? mergeMenus(item.menu, foundMenu) : item.menu || found.menu
247 });
248 });
249};
250
251const UNKNOWN_POS = Infinity;
252
253const findPos = (item, orderedList = []) => {
254 const name = typeof item !== 'string' ? _get('name', item) : item;
255 const pos = orderedList.findIndex(item => item === name);
256 return pos !== -1 ? pos : UNKNOWN_POS;
257};
258
259const compareWithMenu = (to = []) => (a, b) => {
260 const list = to.map(i => i.name || i);
261 return compare(findPos(a, list), findPos(b, list));
262};
263
264const sortByName = (a, b) => {
265 return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
266};
267
268const sortMenus = (first, second = []) => {
269 const sorted = sort(first, compareWithMenu(second), sortByName);
270 return sorted.map(item => {
271 if (!item.menu) return item;
272 const found = second.find(menu => menu.name === item.name);
273 const foundMenu = found && found.menu;
274 return Object.assign({}, item, {
275 menu: foundMenu ? sortMenus(item.menu, foundMenu) : sort(item.menu, sortByName)
276 });
277 });
278};
279
280const search = (val, menu) => {
281 const items = menu.map(item => [item].concat(item.menu || []));
282
283 const flattened = _flattenDepth(2, items);
284
285 const flattenedDeduplicated = Array.from(new Set(flattened));
286 return match(flattenedDeduplicated, val, {
287 keys: ['name']
288 });
289};
290
291const filterMenus = (items, filter) => {
292 if (!filter) return items;
293 return items.filter(filter).map(item => {
294 if (!item.menu) return item;
295 return Object.assign({}, item, {
296 menu: item.menu.filter(filter)
297 });
298 });
299};
300
301const useMenus = opts => {
302 const {
303 query = ''
304 } = opts || {};
305 const {
306 entries,
307 config
308 } = useContext(doczState.context);
309 if (!entries) return null;
310 const arr = entries.map(({
311 value
312 }) => value);
313 const entriesMenu = menusFromEntries(arr);
314 const sorted = useMemo(() => {
315 const merged = mergeMenus(entriesMenu, config.menu);
316 const result = sortMenus(merged, config.menu);
317 return filterMenus(result, opts && opts.filter);
318 }, [entries, config]);
319 return query && query.length > 0 ? search(query, sorted) : sorted;
320};
321
322const usePrevious = (value, defaultValue) => {
323 const ref = useRef(defaultValue);
324 useEffect(() => {
325 ref.current = value;
326 });
327 return ref.current;
328};
329
330const isClient = typeof window === 'object';
331
332const getSize = (initialWidth, initialHeight) => ({
333 innerHeight: isClient ? window.innerHeight : initialHeight,
334 innerWidth: isClient ? window.innerWidth : initialWidth,
335 outerHeight: isClient ? window.outerHeight : initialHeight,
336 outerWidth: isClient ? window.outerWidth : initialWidth
337});
338
339const useWindowSize = (throttleMs = 300, _initialWidth = Infinity, initialHeight = Infinity) => {
340 const [windowSize, setWindowSize] = useState(getSize(initialHeight, initialHeight));
341
342 const tSetWindowResize = _throttle(throttleMs, () => setWindowSize(getSize(initialHeight, initialHeight)));
343
344 useEffect(() => {
345 window.addEventListener('resize', tSetWindowResize);
346 return () => void window.removeEventListener('resize', tSetWindowResize);
347 }, []);
348 return windowSize;
349};
350
351const Playground = ({
352 className,
353 children,
354 style,
355 wrapper,
356 __scope,
357 __position,
358 __code,
359 language,
360 useScoping
361}) => {
362 const components = useComponents();
363 const PlaygroundComponent = components.playground;
364 if (!PlaygroundComponent) return null;
365 return createElement(PlaygroundComponent, {
366 components: components,
367 component: children,
368 className: className,
369 style: style,
370 wrapper: wrapper,
371 scope: __scope,
372 position: __position,
373 code: __code,
374 language: language,
375 useScoping: useScoping
376 });
377};
378
379const RE_OBJECTOF = /(?:React\.)?(?:PropTypes\.)?objectOf\((?:React\.)?(?:PropTypes\.)?(\w+)\)/;
380
381const getTypeStr = type => {
382 switch (type.name.toLowerCase()) {
383 case 'instanceof':
384 return `Class(${type.value})`;
385
386 case 'enum':
387 if (type.computed) return type.value;
388 return type.value ? type.value.map(v => `${v.value}`).join(' │ ') : type.raw;
389
390 case 'union':
391 return type.value ? type.value.map(t => `${getTypeStr(t)}`).join(' │ ') : type.raw;
392
393 case 'array':
394 return type.raw;
395
396 case 'arrayof':
397 return `Array<${getTypeStr(type.value)}>`;
398
399 case 'custom':
400 if (type.raw.indexOf('function') !== -1 || type.raw.indexOf('=>') !== -1) return 'Custom(Function)';else if (type.raw.toLowerCase().indexOf('objectof') !== -1) {
401 const m = type.raw.match(RE_OBJECTOF);
402 if (m && m[1]) return `ObjectOf(${capitalize(m[1])})`;
403 return 'ObjectOf';
404 }
405 return 'Custom';
406
407 case 'bool':
408 return 'Boolean';
409
410 case 'func':
411 return 'Function';
412
413 case 'shape':
414 const shape = type.value;
415 const rst = {};
416 Object.keys(shape).forEach(key => {
417 rst[key] = getTypeStr(shape[key]);
418 });
419 return JSON.stringify(rst, null, 2);
420
421 default:
422 return type.name;
423 }
424};
425
426const humanize = type => getTypeStr(type);
427
428const getPropType = prop => {
429 const propName = _get('name', prop.flowType || prop.type);
430
431 if (!propName) return null;
432 const isEnum = propName.startsWith('"') || propName === 'enum';
433 const name = isEnum ? 'enum' : propName;
434
435 const value = _get('type.value', prop);
436
437 if (!name) return null;
438
439 if (isEnum && typeof value === 'string' || !prop.flowType && !isEnum && !value || prop.flowType && !prop.flowType.elements) {
440 return name;
441 }
442
443 return prop.flowType ? humanize(prop.flowType) : humanize(prop.type);
444};
445const Props = _a => {
446 var {
447 title,
448 isToggle,
449 isRaw,
450 of: component
451 } = _a,
452 rest = __rest(_a, ["title", "isToggle", "isRaw", "of"]);
453
454 const components = useComponents();
455 const PropsComponent = components.props;
456
457 const fileName = _get('__filemeta.filename', component);
458
459 const filemetaName = _get('__filemeta.name', component);
460
461 const componentName = filemetaName || _get('displayName', component) || _get('name', component);
462
463 const props = useComponentProps({
464 componentName,
465 fileName
466 });
467 if (!PropsComponent) return null;
468 return createElement(PropsComponent, Object.assign({
469 title: title,
470 isRaw: isRaw,
471 isToggle: isToggle,
472 props: props,
473 getPropType: getPropType,
474 of: component
475 }, rest));
476};
477
478function theme(themeConfig, transform = c => c) {
479 return WrappedComponent => {
480 const Theme = memo(props => {
481 const {
482 db,
483 currentEntry,
484 children
485 } = props;
486 const initial = Object.assign({}, db, {
487 currentEntry,
488 themeConfig,
489 transform
490 });
491 return createElement(doczState.Provider, {
492 initial: initial
493 }, createElement(WrappedComponent, null, children));
494 });
495 Theme.displayName = WrappedComponent.displayName || 'DoczTheme';
496 return Theme;
497 };
498}
499
500export { ComponentsProvider, Playground, Props, doczState, theme, useComponentProps, useComponents, useConfig, useCurrentDoc, useDataServer, useDocs, useMenus, usePrevious, useWindowSize };