feat: Add MDX Slate style

This commit is contained in:
Till Kolter 2020-08-23 22:44:18 +02:00
parent 029b82b209
commit 49f04d321f
21 changed files with 461 additions and 69 deletions

View File

@ -1,4 +1,9 @@
{
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false
}
},
}

View File

@ -27,15 +27,13 @@ const specUrl =
let store;
const options: RedocRawOptions = {
nativeScrollbars: false,
maxDisplayedEnumValues: 3,
expandAllSchemaFields: true,
hideHttpVerbs: true,
hideShelfIcon: true,
hideHttpVerbs: true
maxDisplayedEnumValues: 3,
nativeScrollbars: false,
};
console.log('options', options)
async function init() {
const spec = await loadAndBundleSpec(specUrl);
store = new AppStore(spec, specUrl, options);

34
package-lock.json generated
View File

@ -1973,8 +1973,7 @@
"@mdx-js/react": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.16.tgz",
"integrity": "sha512-+FhuSVOPo7+4fZaRwWuCSRUcZkJOkZu0rfAbBKvoCg1LWb1Td8Vzi0DTLORdSvgWNbU6+EL40HIgwTOs00x2Jw==",
"dev": true
"integrity": "sha512-+FhuSVOPo7+4fZaRwWuCSRUcZkJOkZu0rfAbBKvoCg1LWb1Td8Vzi0DTLORdSvgWNbU6+EL40HIgwTOs00x2Jw=="
},
"@mdx-js/util": {
"version": "1.6.16",
@ -9098,6 +9097,20 @@
"property-information": "^5.0.0",
"vfile": "^4.0.0",
"web-namespaces": "^1.0.0"
},
"dependencies": {
"hastscript": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz",
"integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==",
"dev": true,
"requires": {
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.0.0",
"property-information": "^5.0.0",
"space-separated-tokens": "^1.0.0"
}
}
}
},
"hast-util-is-element": {
@ -9175,18 +9188,6 @@
"integrity": "sha512-I5GTdSfhYfAPNztx2xJRQpG8cuDSNt599/7YUn7Gx/WxNMsG+a835k97TDkFgk123cwjfwINaZknkKkphx/f2A==",
"dev": true
},
"hastscript": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-5.1.2.tgz",
"integrity": "sha512-WlztFuK+Lrvi3EggsqOkQ52rKbxkXL3RwB6t5lwoa8QLMemoWfBuL43eDrwOamJyR7uKQKdmKYaBH1NZBiIRrQ==",
"dev": true,
"requires": {
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.0.0",
"property-information": "^5.0.0",
"space-separated-tokens": "^1.0.0"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@ -15192,6 +15193,11 @@
}
}
},
"prism-react-renderer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-1.1.1.tgz",
"integrity": "sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug=="
},
"prismjs": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.20.0.tgz",

View File

@ -140,6 +140,7 @@
"styled-components": "^4.1.1"
},
"dependencies": {
"@mdx-js/react": "^1.6.16",
"@types/node": "^13.11.1",
"classnames": "^2.2.6",
"decko": "^1.2.0",
@ -155,6 +156,7 @@
"openapi-sampler": "^1.0.0-beta.16",
"perfect-scrollbar": "^1.4.0",
"polished": "^3.6.5",
"prism-react-renderer": "^1.1.1",
"prismjs": "^1.20.0",
"prop-types": "^15.7.2",
"react-dropdown-aria": "^2.0.7",

View File

@ -0,0 +1,15 @@
import styled from 'styled-components'
export const Flex = styled.div`
display: flex;
justify-content: ${(props) => props.justifyContent};
width: 100%;
`;
export const HFlex = styled(Flex)`
flex-direction: row;
`;
export const VFlex = styled(Flex)`
flex-direction: column;
`;

View File

@ -17,9 +17,11 @@ export class ContentItems extends React.Component<{
if (items.length === 0) {
return null;
}
return items.map(item => {
return <ContentItem key={item.id} item={item} />;
});
return items
.filter((item) => item.type !== 'extra')
.map((item) => {
return <ContentItem key={item.id} item={item} />;
});
}
}
@ -61,7 +63,7 @@ export class ContentItem extends React.Component<ContentItemProps> {
}
}
const middlePanelWrap = component => <MiddlePanel compact={true}>{component}</MiddlePanel>;
const middlePanelWrap = (component) => <MiddlePanel compact={true}>{component}</MiddlePanel>;
@observer
export class SectionItem extends React.Component<ContentItemProps> {

View File

@ -1,5 +1,6 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { MDXProvider } from '@mdx-js/react';
import { ThemeProvider } from '../../styled-components';
import { OptionsProvider } from '../OptionsProvider';
@ -15,6 +16,8 @@ import { ApiContentWrap, BackgroundStub, RedocWrap } from './styled.elements';
import { SearchBox } from '../SearchBox/SearchBox';
import { StoreProvider } from '../StoreBuilder';
import { sections, components } from '../../markdown';
export interface RedocProps {
store: AppStore;
}
@ -41,26 +44,31 @@ export class Redoc extends React.Component<RedocProps> {
<ThemeProvider theme={options.theme}>
<StoreProvider value={this.props.store}>
<OptionsProvider value={options}>
<RedocWrap className="redoc-wrap">
<StickyResponsiveSidebar menu={menu} className="menu-content">
<ApiLogo info={spec.info} />
{(!options.disableSearch && (
<SearchBox
search={search!}
marker={marker}
getItemById={menu.getItemById}
onActivate={menu.activateAndScroll}
/>
)) ||
null}
<SideMenu menu={menu} />
</StickyResponsiveSidebar>
<ApiContentWrap className="api-content">
<ApiInfo store={store} />
<ContentItems items={menu.items as any} />
</ApiContentWrap>
<BackgroundStub />
</RedocWrap>
<MDXProvider components={components}>
<RedocWrap className="redoc-wrap">
<StickyResponsiveSidebar menu={menu} className="menu-content">
<ApiLogo info={spec.info} />
{(!options.disableSearch && (
<SearchBox
search={search!}
marker={marker}
getItemById={menu.getItemById}
onActivate={menu.activateAndScroll}
/>
)) ||
null}
<SideMenu menu={menu} />
</StickyResponsiveSidebar>
<ApiContentWrap className="api-content">
<ApiInfo store={store} />
{sections.map((MDXComponent, idx) => {
return <MDXComponent key={`section-${idx}`} />;
})}
<ContentItems items={menu.items as any} />
</ApiContentWrap>
<BackgroundStub />
</RedocWrap>
</MDXProvider>
</OptionsProvider>
</StoreProvider>
</ThemeProvider>

View File

@ -2,6 +2,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { IMenuItem } from '../../services';
import { ExtraContent } from '../../services/models/ExtraContent';
import { MenuItem } from './MenuItem';
import { MenuItemUl } from './styled.elements';
@ -12,6 +13,7 @@ export interface MenuItemsProps {
onActivate?: (item: IMenuItem) => void;
style?: React.CSSProperties;
root?: boolean;
extra?: any;
className?: string;
}
@ -19,7 +21,7 @@ export interface MenuItemsProps {
@observer
export class MenuItems extends React.Component<MenuItemsProps> {
render() {
const { items, root, className } = this.props;
const { items, root, className, extra } = this.props;
const expanded = this.props.expanded == null ? true : this.props.expanded;
return (
<MenuItemUl
@ -28,6 +30,16 @@ export class MenuItems extends React.Component<MenuItemsProps> {
expanded={expanded}
{...(root ? { role: 'navigation' } : {})}
>
{extra &&
extra.map((headline, ids) => (
<MenuItem
key={ids}
item={
new ExtraContent({ id: headline.id, name: headline.text, depth: headline.depth })
}
onActivate={this.props.onActivate}
/>
))}
{items.map((item, idx) => (
<MenuItem key={idx} item={item} onActivate={this.props.onActivate} />
))}

View File

@ -9,7 +9,11 @@ import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
import { RedocAttribution } from './styled.elements';
@observer
export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
export class SideMenu extends React.Component<{
menu: MenuStore;
className?: string;
extra?: any;
}> {
static contextType = OptionsContext;
private _updateScroll?: () => void;
@ -23,7 +27,12 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
wheelPropagation: false,
}}
>
<MenuItems items={store.items} onActivate={this.activate} root={true} />
<MenuItems
items={store.items}
extra={this.props.extra}
onActivate={this.activate}
root={true}
/>
<RedocAttribution>
<a target="_blank" rel="noopener noreferrer" href="https://github.com/Redocly/redoc">
Documentation Powered by ReDoc
@ -46,7 +55,7 @@ export class SideMenu extends React.Component<{ menu: MenuStore; className?: str
});
};
private saveScrollUpdate = upd => {
private saveScrollUpdate = (upd) => {
this._updateScroll = upd;
};
}

View File

@ -1,6 +1,5 @@
import * as classnames from 'classnames';
import { darken } from 'polished';
import { deprecatedCss, ShelfIcon } from '../../common-elements';
import styled, { css, ResolvedThemeInterface } from '../../styled-components';
@ -107,12 +106,17 @@ export const menuItemDepth = {
1: css`
font-size: 0.929em;
text-transform: ${({ theme }) => theme.sidebar.level1Items.textTransform};
font-weight: ${({ theme }) => theme.sidebar.level1Items.fw};
font-size: ${({ theme }) => theme.sidebar.level1Items.fontSize};
color: ${({theme}) => theme.sidebar.level1Items.color};
&:hover {
color: ${props => props.theme.sidebar.activeTextColor};
}
`,
2: css`
color: ${props => props.theme.sidebar.textColor};
color: ${({ theme }) => theme.sidebar.level1Items.color};
font-size: ${({ theme }) => theme.sidebar.level2Items.fontSize};
`,
};
@ -133,7 +137,7 @@ export const MenuItemLabel = styled.label.attrs((props: MenuItemLabelType) => ({
color: ${props =>
props.active ? props.theme.sidebar.activeTextColor : props.theme.sidebar.textColor};
margin: 0;
padding: 12.5px ${props => props.theme.spacing.unit * 4}px;
padding: 12.5px ${props => !props.depth || props.depth === 0 ? `${props.theme.spacing.unit * 2}px` : `${props.depth}rem`};
${({ depth, type, theme }) =>
(type === 'section' && depth > 1 && 'padding-left: ' + theme.spacing.unit * 8 + 'px;') || ''}
display: flex;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import { HFlex } from '../common-elements/flex';
import styled from 'styled-components';
const Button = styled.button`
margin-left: auto;
`;
const TryOutWidget: React.FC = () => {
const onClick = (e) => {
e.preventDefault();
window.location.href = 'https://en.wikipedia.org/wiki/Register';
};
return (
<HFlex>
<Button onClick={onClick}>TRY OUT</Button>
</HFlex>
);
};
export default TryOutWidget;

55
src/markdown/auth.mdx Normal file
View File

@ -0,0 +1,55 @@
# Authentication
```bash
#!/bin/bash
BSDEX_API_KEY=put-your-api-key-here
BSDEX_API_SECRET=put-your-api-secret-here
DATE="`date -u '+%a, %d %b %Y %T %Z'`"
# The signature is in fact HMAC signature of a few fields, currently only "date", using your API secret.
AUTHORIZATION="hmac username=\"$BSDEX_API_KEY\", algorithm=\"hmac-sha1\", headers=\"date\", signature=\"`/bin/echo -n "date: ${DATE}" | openssl sha1 -binary -hmac "${BSDEX_API_SECRET}" | base64 `\""
curl -v \
-H "Date: $DATE" \
-H "ApiKey: $BSDEX_API_KEY" \
-H "Authorization: $AUTHORIZATION" \
-X GET "https://api-public.prelive.cex.tribe.sh/api/v1/balance"
```
> And you'll receive a response such as:
```json
[
{
"asset_id": "btc",
"available": "0",
"locked": "0"
},
{
"asset_id": "eur",
"available": "34163",
"locked": "123"
}
]
```
The requests are secured via Keyed-Hashing for Message Authentication (HMAC).
In order to authenticate, you need to add the following headers in your HTTP requests in addition to the `Authentication` header:
| Header | Description | Example |
| ------ | ----------- | ------- |
|`Date`| Current date in RFC7231 format | Wed, 12 Aug 2020 12:49:26 UTC |
|`ApiKey`| Your API key | put-your-api-key-here |
The `Authentication` header's value has the pattern
`hmac username=<ApiKey>, algorithm="hmac-sha1", headers="date", signature="<signed_date>"`
where `signed_date` is the base64-encoded HMAC-SHA1 signature for the expression `date: <Date>`.
Have a look at the code example to understand the HMAC signing process better.
<aside class="notice">
It is important to note that the value of the <em>Date</em> header must match the signed date value.
</aside>

View File

@ -0,0 +1,37 @@
import * as React from 'react';
import Highlight, { defaultProps, Language } from 'prism-react-renderer';
import styled from 'styled-components';
type Props = {
className: string;
};
const OverflowHighlighter = styled.pre`
overflow-x: scroll;
`;
const Highligher: React.FC<Props> = ({ children, className }) => {
const language = className.replace(/language-/, '');
if (!children) {
return null;
}
return (
<Highlight {...defaultProps} code={children.toString()} language={language as Language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<OverflowHighlighter className={className} style={{ ...style, padding: '20px' }}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</OverflowHighlighter>
)}
</Highlight>
);
};
export default Highligher;

100
src/markdown/index.tsx Normal file
View File

@ -0,0 +1,100 @@
import * as React from 'react';
import {
DarkRightPanel,
H1,
H2,
MiddlePanel,
PropertiesTable,
Row,
Section,
ShareLink,
} from '../common-elements';
import Highlighter from './code/Highlighter';
const INCLUDES = ['auth', 'welcome'];
const H1Comp = ({ children, ...props }) => {
return (
<H1 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H1>
);
};
const H2Comp = ({ children, ...props }) => (
<H2 id={props.id}>
<ShareLink to={props.id as string} />
{children}
</H2>
);
const Wrapper = ({ children }) => {
const getSections = (children) => {
let currentSection: any[] = [];
let currentId: number | null = null;
let currentCodeBlocks: any[] = [];
const result: any[] = [];
children.forEach((child) => {
const type = child.props.mdxType;
if (['h1', 'h2', 'h3', 'h4', 'h5'].includes(type)) {
if (currentSection.length > 0) {
result.push({
id: currentId,
middle: currentSection,
right: currentCodeBlocks,
});
}
currentId = child.props.id;
currentSection = [];
currentCodeBlocks = [];
}
if (['code', 'pre', 'blockquote'].includes(type)) {
currentCodeBlocks.push(child);
} else {
currentSection.push(child);
}
});
if (currentSection.length > 0) {
result.push({ id: currentId, middle: currentSection, right: currentCodeBlocks });
}
return result;
};
const sections = getSections(children);
return (
<>
{sections.map((section) => (
<Section key={`section-${section.id}`} id={section.id}>
<Row id={section.id}>
{section.middle && <MiddlePanel>{section.middle}</MiddlePanel>}
{section.right && <DarkRightPanel>{section.right}</DarkRightPanel>}
</Row>
</Section>
))}
</>
);
};
const includedSections = INCLUDES.map((include) => require(`./${include}.mdx`));
export const sections = includedSections.map((sect) => sect.default);
export const headings = includedSections.reduce(
(acc, section) => [...acc, ...section.headings],
[],
);
export const components = {
code: Highlighter,
h1: H1Comp,
h2: H2Comp,
table: PropertiesTable,
wrapper: Wrapper,
};
export default sections;

View File

@ -0,0 +1,38 @@
'use strict';
const visit = require('unist-util-visit');
module.exports = slug;
function slug() {
return transformer;
}
// Patch slugs on heading nodes.
function transformer(ast) {
const headlines: any[] = [];
visit(ast, 'heading', visitor);
function visitor(node) {
const data = node.data || (node.data = {});
const props = data.hProperties || (data.hProperties = {});
const sectionId = `section/${data.id}`;
data.id = sectionId;
props.id = sectionId;
visit(node, 'text', (textNode) => {
headlines.push({ depth: node.depth, id: data.id, text: textNode.value });
});
}
const value = `export const headings = ${JSON.stringify(headlines)};`;
const meta = {
default: false,
type: 'export',
value,
};
ast.children.splice(0, 0, meta);
}

11
src/markdown/welcome.mdx Normal file
View File

@ -0,0 +1,11 @@
# Hello, world!
First intro
## Hello subline
Some text
import TryOutWidget from './TryOutWidget';
<TryOutWidget />

View File

@ -89,7 +89,7 @@ export class AppStore {
this.search.indexItems(this.menu.items);
}
this.disposer = observe(this.menu, 'activeItemIdx', change => {
this.disposer = observe(this.menu, 'activeItemIdx', (change) => {
this.updateMarkOnMenu(change.newValue as number);
});
}

View File

@ -1,14 +1,14 @@
import { action, observable } from 'mobx';
import { querySelector } from '../utils/dom';
import { SpecStore } from './models';
import { history as historyInst, HistoryService } from './HistoryService';
import { ScrollService } from './ScrollService';
import { flattenByProp, SECURITY_SCHEMES_SECTION_PREFIX } from '../utils';
import { querySelector } from '../utils/dom';
import { history as historyInst, HistoryService } from './HistoryService';
import { GROUP_DEPTH } from './MenuBuilder';
import { SpecStore } from './models';
import { ScrollService } from './ScrollService';
import { ExtraContent } from './models/ExtraContent';
import { headings } from '../markdown';
export type MenuItemGroupType = 'group' | 'tag' | 'section';
export type MenuItemGroupType = 'group' | 'tag' | 'section' | 'extra';
export type MenuItemType = MenuItemGroupType | 'operation';
/** Generic interface for MenuItems */
@ -76,7 +76,15 @@ export class MenuStore {
* @param scroll scroll service instance used by this menu
*/
constructor(spec: SpecStore, public scroll: ScrollService, public history: HistoryService) {
this.items = spec.contentItems;
const extraItems = headings.map(
({ id, text, depth }) =>
new ExtraContent({
id,
name: text,
depth,
}),
);
this.items = [...extraItems, ...spec.contentItems];
this.flatItems = flattenByProp(this.items || [], 'items');
this.flatItems.forEach((item, idx) => (item.absoluteIdx = idx));
@ -142,12 +150,12 @@ export class MenuStore {
}
let item: IMenuItem | undefined;
item = this.flatItems.find(i => i.id === id);
item = this.flatItems.find((i) => i.id === id);
if (item) {
this.activateAndScroll(item, false);
} else {
if (id.startsWith(SECURITY_SCHEMES_SECTION_PREFIX)) {
item = this.flatItems.find(i => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
item = this.flatItems.find((i) => SECURITY_SCHEMES_SECTION_PREFIX.startsWith(i.id));
this.activate(item);
}
this.scroll.scrollIntoViewBySelector(`[${SECTION_ATTR}="${id}"]`);
@ -183,7 +191,7 @@ export class MenuStore {
}
getItemById = (id: string) => {
return this.flatItems.find(item => item.id === id);
return this.flatItems.find((item) => item.id === id);
};
/**

View File

@ -0,0 +1,58 @@
import { action, observable } from 'mobx';
import { ContentItemModel } from '..';
import { OpenAPIExternalDocumentation } from '../../types';
import { IMenuItem } from '../MenuStore';
export class ExtraContent implements IMenuItem {
//#region IMenuItem fields
id: string;
absoluteIdx?: number;
name: string;
description?: string;
type = 'extra' as const;
content: JSX.Element
items: ContentItemModel[] = [];
parent?: ExtraContent;
externalDocs?: OpenAPIExternalDocumentation;
@observable
active: boolean = false;
@observable
expanded: boolean = false;
depth: number;
level: number;
constructor({ id, name, depth }) {
this.id = id
this.name = name
this.depth = depth
}
/**
* set operation as active (used by side menu)
*/
@action
activate() {
this.active = true;
}
/**
* set operation as inactive (used by side menu)
*/
@action
deactivate() {
this.active = false;
}
expand() {
if (this.parent) {
this.parent.expand();
}
}
collapse() {
/* do nothing */
}
}

View File

@ -1,13 +1,15 @@
export * from '../SpecStore';
export * from './Group.model';
export * from './Operation';
export * from './RequestBody';
export * from './ApiInfo';
export * from './Callback';
export * from './Example';
export * from './ExtraContent';
export * from './Field';
export * from './Group.model';
export * from './MediaContent';
export * from './MediaType';
export * from './Operation';
export * from './RequestBody';
export * from './Response';
export * from './Schema';
export * from './Field';
export * from './ApiInfo';
export * from './SecuritySchemes';
export * from './Callback';

1
src/types/mdx.d.ts vendored
View File

@ -1,4 +1,5 @@
declare module '*.mdx' {
let MDXComponent: (props: any) => JSX.Element;
export const headings: any;
export default MDXComponent;
}