Merge branch 'master' of github.com:jorilallo/atlas into tom/static
This commit is contained in:
@@ -1,15 +1,17 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
import { observer } from 'mobx-react';
|
import { observer } from 'mobx-react';
|
||||||
import { Editor, Plain } from 'slate';
|
import { Editor, Plain } from 'slate';
|
||||||
import keydown from 'react-keydown';
|
import keydown from 'react-keydown';
|
||||||
import type { Document, State, Editor as EditorType } from './types';
|
import type { State, Editor as EditorType } from './types';
|
||||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||||
import Flex from 'components/Flex';
|
import Flex from 'components/Flex';
|
||||||
import ClickablePadding from './components/ClickablePadding';
|
import ClickablePadding from './components/ClickablePadding';
|
||||||
import Toolbar from './components/Toolbar';
|
import Toolbar from './components/Toolbar';
|
||||||
import BlockInsert from './components/BlockInsert';
|
import BlockInsert from './components/BlockInsert';
|
||||||
import Placeholder from './components/Placeholder';
|
import Placeholder from './components/Placeholder';
|
||||||
|
import Contents from './components/Contents';
|
||||||
import Markdown from './serializer';
|
import Markdown from './serializer';
|
||||||
import createSchema from './schema';
|
import createSchema from './schema';
|
||||||
import createPlugins from './plugins';
|
import createPlugins from './plugins';
|
||||||
@@ -37,10 +39,7 @@ type KeyData = {
|
|||||||
editor: EditorType;
|
editor: EditorType;
|
||||||
schema: Object;
|
schema: Object;
|
||||||
plugins: Array<Object>;
|
plugins: Array<Object>;
|
||||||
|
@observable editorState: State;
|
||||||
state: {
|
|
||||||
state: State,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -51,10 +50,10 @@ type KeyData = {
|
|||||||
onImageUploadStop: props.onImageUploadStop,
|
onImageUploadStop: props.onImageUploadStop,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.text) {
|
if (props.text.trim().length) {
|
||||||
this.state = { state: Markdown.deserialize(props.text) };
|
this.editorState = Markdown.deserialize(props.text);
|
||||||
} else {
|
} else {
|
||||||
this.state = { state: Plain.deserialize('') };
|
this.editorState = Plain.deserialize('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,15 +73,16 @@ type KeyData = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (state: State) => {
|
onChange = (editorState: State) => {
|
||||||
this.setState({ state });
|
if (this.editorState !== editorState) {
|
||||||
};
|
this.props.onChange(Markdown.serialize(editorState));
|
||||||
|
}
|
||||||
|
|
||||||
onDocumentChange = (document: Document, state: State) => {
|
this.editorState = editorState;
|
||||||
this.props.onChange(Markdown.serialize(state));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDrop = async (ev: SyntheticEvent) => {
|
handleDrop = async (ev: SyntheticEvent) => {
|
||||||
|
if (this.props.readOnly) return;
|
||||||
// check if this event was already handled by the Editor
|
// check if this event was already handled by the Editor
|
||||||
if (ev.isDefaultPrevented()) return;
|
if (ev.isDefaultPrevented()) return;
|
||||||
|
|
||||||
@@ -92,7 +92,9 @@ type KeyData = {
|
|||||||
|
|
||||||
const files = getDataTransferFiles(ev);
|
const files = getDataTransferFiles(ev);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await this.insertImageFile(file);
|
if (file.type.startsWith('image/')) {
|
||||||
|
await this.insertImageFile(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,7 +164,7 @@ type KeyData = {
|
|||||||
const transform = state.transform();
|
const transform = state.transform();
|
||||||
transform.collapseToStartOf(state.document);
|
transform.collapseToStartOf(state.document);
|
||||||
transform.focus();
|
transform.focus();
|
||||||
this.setState({ state: transform.apply() });
|
this.editorState = transform.apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
focusAtEnd = () => {
|
focusAtEnd = () => {
|
||||||
@@ -170,7 +172,7 @@ type KeyData = {
|
|||||||
const transform = state.transform();
|
const transform = state.transform();
|
||||||
transform.collapseToEndOf(state.document);
|
transform.collapseToEndOf(state.document);
|
||||||
transform.focus();
|
transform.focus();
|
||||||
this.setState({ state: transform.apply() });
|
this.editorState = transform.apply();
|
||||||
};
|
};
|
||||||
|
|
||||||
render = () => {
|
render = () => {
|
||||||
@@ -187,11 +189,12 @@ type KeyData = {
|
|||||||
>
|
>
|
||||||
<MaxWidth column auto>
|
<MaxWidth column auto>
|
||||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||||
|
<Contents state={this.editorState} />
|
||||||
{!readOnly &&
|
{!readOnly &&
|
||||||
<Toolbar state={this.state.state} onChange={this.onChange} />}
|
<Toolbar state={this.editorState} onChange={this.onChange} />}
|
||||||
{!readOnly &&
|
{!readOnly &&
|
||||||
<BlockInsert
|
<BlockInsert
|
||||||
state={this.state.state}
|
state={this.editorState}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onInsertImage={this.insertImageFile}
|
onInsertImage={this.insertImageFile}
|
||||||
/>}
|
/>}
|
||||||
@@ -202,10 +205,9 @@ type KeyData = {
|
|||||||
schema={this.schema}
|
schema={this.schema}
|
||||||
plugins={this.plugins}
|
plugins={this.plugins}
|
||||||
emoji={emoji}
|
emoji={emoji}
|
||||||
state={this.state.state}
|
state={this.editorState}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onDocumentChange={this.onDocumentChange}
|
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
@@ -246,22 +248,6 @@ const StyledEditor = styled(Editor)`
|
|||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
.anchor {
|
|
||||||
visibility: hidden;
|
|
||||||
color: #dedede;
|
|
||||||
padding-left: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.anchor {
|
|
||||||
visibility: visible;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #cdcdcd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:first-of-type {
|
h1:first-of-type {
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { color } from 'styles/constants';
|
|||||||
import type { Props } from '../types';
|
import type { Props } from '../types';
|
||||||
|
|
||||||
export default function Code({ children, node, readOnly, attributes }: Props) {
|
export default function Code({ children, node, readOnly, attributes }: Props) {
|
||||||
|
const language = node.data.get('language') || 'javascript';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
{readOnly && <CopyButton text={node.text} />}
|
{readOnly && <CopyButton text={node.text} />}
|
||||||
<Pre>
|
<Pre className={`language-${language}`}>
|
||||||
<code {...attributes}>
|
<code {...attributes} className={`language-${language}`}>
|
||||||
{children}
|
{children}
|
||||||
</code>
|
</code>
|
||||||
</Pre>
|
</Pre>
|
||||||
@@ -20,7 +22,7 @@ export default function Code({ children, node, readOnly, attributes }: Props) {
|
|||||||
|
|
||||||
const Pre = styled.pre`
|
const Pre = styled.pre`
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
background: ${color.smoke};
|
background: ${color.smokeLight};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid ${color.smokeDark};
|
border: 1px solid ${color.smokeDark};
|
||||||
|
|
||||||
|
|||||||
149
frontend/components/Editor/components/Contents.js
Normal file
149
frontend/components/Editor/components/Contents.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// @flow
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { observable } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { List } from 'immutable';
|
||||||
|
import { color } from 'styles/constants';
|
||||||
|
import headingToSlug from '../headingToSlug';
|
||||||
|
import type { State, Block } from '../types';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
state: State,
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer class Contents extends Component {
|
||||||
|
props: Props;
|
||||||
|
@observable activeHeading: ?string;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('scroll', this.updateActiveHeading);
|
||||||
|
this.updateActiveHeading();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('scroll', this.updateActiveHeading);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateActiveHeading = () => {
|
||||||
|
const elements = this.headingElements;
|
||||||
|
if (!elements.length) return;
|
||||||
|
|
||||||
|
let activeHeading = elements[0].id;
|
||||||
|
|
||||||
|
for (const element of elements) {
|
||||||
|
const bounds = element.getBoundingClientRect();
|
||||||
|
if (bounds.top <= 0) activeHeading = element.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeHeading = activeHeading;
|
||||||
|
};
|
||||||
|
|
||||||
|
get headingElements(): HTMLElement[] {
|
||||||
|
const elements = [];
|
||||||
|
const tagNames = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||||
|
|
||||||
|
for (const tagName of tagNames) {
|
||||||
|
for (const ele of document.getElementsByTagName(tagName)) {
|
||||||
|
elements.push(ele);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
get headings(): List<Block> {
|
||||||
|
const { state } = this.props;
|
||||||
|
|
||||||
|
return state.document.nodes.filter((node: Block) => {
|
||||||
|
if (!node.text) return false;
|
||||||
|
return node.type.match(/^heading/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// If there are one or less headings in the document no need for a minimap
|
||||||
|
if (this.headings.size <= 1) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<Sections>
|
||||||
|
{this.headings.map(heading => {
|
||||||
|
const slug = headingToSlug(heading);
|
||||||
|
const active = this.activeHeading === slug;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem type={heading.type} active={active}>
|
||||||
|
<Anchor href={`#${slug}`} active={active}>
|
||||||
|
{heading.text}
|
||||||
|
</Anchor>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Sections>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 150px;
|
||||||
|
z-index: 100;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Anchor = styled.a`
|
||||||
|
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||||
|
font-weight: ${props => (props.active ? 500 : 400)};
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 100ms ease-in-out;
|
||||||
|
margin-right: -5px;
|
||||||
|
padding: 2px 0;
|
||||||
|
pointer-events: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${color.primary};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ListItem = styled.li`
|
||||||
|
position: relative;
|
||||||
|
margin-left: ${props => (props.type.match(/heading[12]/) ? '8px' : '16px')};
|
||||||
|
text-align: right;
|
||||||
|
color: ${color.slate};
|
||||||
|
padding-right: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
color: ${props => (props.active ? color.slateDark : color.slate)};
|
||||||
|
content: "${props => (props.type.match(/heading[12]/) ? '—' : '–')}";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Sections = styled.ol`
|
||||||
|
margin: 0 0 0 -8px;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 13px;
|
||||||
|
width: 100px;
|
||||||
|
transition-delay: 1s;
|
||||||
|
transition: width 100ms ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
width: 300px;
|
||||||
|
transition-delay: 0s;
|
||||||
|
|
||||||
|
${Anchor} {
|
||||||
|
opacity: 1;
|
||||||
|
margin-right: 0;
|
||||||
|
background: ${color.white};
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Contents;
|
||||||
@@ -2,13 +2,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Document } from 'slate';
|
import { Document } from 'slate';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import _ from 'lodash';
|
import headingToSlug from '../headingToSlug';
|
||||||
import slug from 'slug';
|
|
||||||
import type { Node, Editor } from '../types';
|
import type { Node, Editor } from '../types';
|
||||||
import Placeholder from './Placeholder';
|
import Placeholder from './Placeholder';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React$Element<any>,
|
children: React$Element<*>,
|
||||||
placeholder?: boolean,
|
placeholder?: boolean,
|
||||||
parent: Node,
|
parent: Node,
|
||||||
node: Node,
|
node: Node,
|
||||||
@@ -31,7 +30,7 @@ function Heading(props: Props) {
|
|||||||
const parentIsDocument = parent instanceof Document;
|
const parentIsDocument = parent instanceof Document;
|
||||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
const slugish = headingToSlug(node);
|
||||||
const showHash = readOnly && !!slugish;
|
const showHash = readOnly && !!slugish;
|
||||||
const Component = component;
|
const Component = component;
|
||||||
const emoji = editor.props.emoji || '';
|
const emoji = editor.props.emoji || '';
|
||||||
@@ -40,8 +39,10 @@ function Heading(props: Props) {
|
|||||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component {...rest}>
|
<Component {...rest} id={slugish}>
|
||||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
<Wrapper hasEmoji={startsWithEmojiAndSpace}>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
{showPlaceholder &&
|
{showPlaceholder &&
|
||||||
<Placeholder contentEditable={false}>
|
<Placeholder contentEditable={false}>
|
||||||
{editor.props.placeholder}
|
{editor.props.placeholder}
|
||||||
@@ -53,7 +54,7 @@ function Heading(props: Props) {
|
|||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Anchor = styled.a`
|
const Anchor = styled.a`
|
||||||
@@ -66,19 +67,31 @@ const Anchor = styled.a`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Heading1 = styled(Heading)`
|
export const StyledHeading = styled(Heading)`
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
${Anchor} {
|
${Anchor} {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const Heading2 = Heading1.withComponent('h2');
|
export const Heading1 = (props: Props) => (
|
||||||
export const Heading3 = Heading1.withComponent('h3');
|
<StyledHeading component="h1" {...props} />
|
||||||
export const Heading4 = Heading1.withComponent('h4');
|
);
|
||||||
export const Heading5 = Heading1.withComponent('h5');
|
export const Heading2 = (props: Props) => (
|
||||||
export const Heading6 = Heading1.withComponent('h6');
|
<StyledHeading component="h2" {...props} />
|
||||||
|
);
|
||||||
export default Heading;
|
export const Heading3 = (props: Props) => (
|
||||||
|
<StyledHeading component="h3" {...props} />
|
||||||
|
);
|
||||||
|
export const Heading4 = (props: Props) => (
|
||||||
|
<StyledHeading component="h4" {...props} />
|
||||||
|
);
|
||||||
|
export const Heading5 = (props: Props) => (
|
||||||
|
<StyledHeading component="h5" {...props} />
|
||||||
|
);
|
||||||
|
export const Heading6 = (props: Props) => (
|
||||||
|
<StyledHeading component="h6" {...props} />
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { fontWeight, color } from 'styles/constants';
|
import { fontWeight, color } from 'styles/constants';
|
||||||
import Document from 'models/Document';
|
import Document from 'models/Document';
|
||||||
import GoToIcon from 'components/Icon/GoToIcon';
|
import NextIcon from 'components/Icon/NextIcon';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
innerRef?: Function,
|
innerRef?: Function,
|
||||||
@@ -14,7 +14,7 @@ type Props = {
|
|||||||
function DocumentResult({ document, ...rest }: Props) {
|
function DocumentResult({ document, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<ListItem {...rest} href="">
|
<ListItem {...rest} href="">
|
||||||
<i><GoToIcon light /></i>
|
<i><NextIcon light /></i>
|
||||||
{document.title}
|
{document.title}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -114,9 +114,10 @@ class LinkToolbar extends Component {
|
|||||||
const { state } = this.props;
|
const { state } = this.props;
|
||||||
const transform = state.transform();
|
const transform = state.transform();
|
||||||
|
|
||||||
if (state.selection.isExpanded) {
|
if (href) {
|
||||||
|
transform.setInline({ type: 'link', data: { href } });
|
||||||
|
} else {
|
||||||
transform.unwrapInline('link');
|
transform.unwrapInline('link');
|
||||||
if (href) transform.wrapInline({ type: 'link', data: { href } });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange(transform.apply());
|
this.props.onChange(transform.apply());
|
||||||
@@ -179,7 +180,7 @@ class LinkToolbar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SearchResults = styled.div`
|
const SearchResults = styled.div`
|
||||||
background: rgba(34, 34, 34, .95);
|
background: #2F3336;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
9
frontend/components/Editor/headingToSlug.js
Normal file
9
frontend/components/Editor/headingToSlug.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// @flow
|
||||||
|
import { escape } from 'lodash';
|
||||||
|
import type { Node } from './types';
|
||||||
|
import slug from 'slug';
|
||||||
|
|
||||||
|
export default function headingToSlug(node: Node) {
|
||||||
|
const level = node.type.replace('heading', 'h');
|
||||||
|
return escape(`${level}-${slug(node.text)}-${node.key}`);
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ export default async function insertImageFile(
|
|||||||
try {
|
try {
|
||||||
// load the file as a data URL
|
// load the file as a data URL
|
||||||
const id = uuid.v4();
|
const id = uuid.v4();
|
||||||
|
const alt = '';
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener('load', () => {
|
reader.addEventListener('load', () => {
|
||||||
const src = reader.result;
|
const src = reader.result;
|
||||||
@@ -24,7 +25,7 @@ export default async function insertImageFile(
|
|||||||
.insertBlock({
|
.insertBlock({
|
||||||
type: 'image',
|
type: 'image',
|
||||||
isVoid: true,
|
isVoid: true,
|
||||||
data: { src, id, loading: true },
|
data: { src, id, alt, loading: true },
|
||||||
})
|
})
|
||||||
.apply();
|
.apply();
|
||||||
editor.onChange(state);
|
editor.onChange(state);
|
||||||
@@ -45,7 +46,7 @@ export default async function insertImageFile(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return finalTransform.setNodeByKey(placeholder.key, {
|
return finalTransform.setNodeByKey(placeholder.key, {
|
||||||
data: { src, loading: false },
|
data: { src, alt, loading: false },
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -73,8 +73,22 @@ export default function MarkdownShortcuts() {
|
|||||||
let { mark, shortcut } = key;
|
let { mark, shortcut } = key;
|
||||||
let inlineTags = [];
|
let inlineTags = [];
|
||||||
|
|
||||||
|
// only add tags if they have spaces around them or the tag is beginning or the end of the block
|
||||||
for (let i = 0; i < startBlock.text.length; i++) {
|
for (let i = 0; i < startBlock.text.length; i++) {
|
||||||
if (startBlock.text.slice(i, i + shortcut.length) === shortcut)
|
const { text } = startBlock;
|
||||||
|
const start = i;
|
||||||
|
const end = i + shortcut.length;
|
||||||
|
const beginningOfBlock = start === 0;
|
||||||
|
const endOfBlock = end === text.length;
|
||||||
|
const surroundedByWhitespaces = [
|
||||||
|
text.slice(start - 1, start),
|
||||||
|
text.slice(end, end + 1),
|
||||||
|
].includes(' ');
|
||||||
|
|
||||||
|
if (
|
||||||
|
text.slice(start, end) === shortcut &&
|
||||||
|
(beginningOfBlock || endOfBlock || surroundedByWhitespaces)
|
||||||
|
)
|
||||||
inlineTags.push(i);
|
inlineTags.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type Editor = {
|
|||||||
export type Node = {
|
export type Node = {
|
||||||
key: string,
|
key: string,
|
||||||
kind: string,
|
kind: string,
|
||||||
|
type: string,
|
||||||
length: number,
|
length: number,
|
||||||
text: string,
|
text: string,
|
||||||
data: Map<string, any>,
|
data: Map<string, any>,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type Props = {
|
|||||||
primary?: boolean,
|
primary?: boolean,
|
||||||
color?: string,
|
color?: string,
|
||||||
size?: number,
|
size?: number,
|
||||||
|
onClick?: Function,
|
||||||
};
|
};
|
||||||
|
|
||||||
type BaseProps = {
|
type BaseProps = {
|
||||||
@@ -18,6 +19,7 @@ type BaseProps = {
|
|||||||
export default function Icon({
|
export default function Icon({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
onClick,
|
||||||
...rest
|
...rest
|
||||||
}: Props & BaseProps) {
|
}: Props & BaseProps) {
|
||||||
const size = rest.size ? rest.size + 'px' : '24px';
|
const size = rest.size ? rest.size + 'px' : '24px';
|
||||||
@@ -36,6 +38,7 @@ export default function Icon({
|
|||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
className={className}
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
15
frontend/components/Icon/NextIcon.js
Normal file
15
frontend/components/Icon/NextIcon.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from './Icon';
|
||||||
|
import type { Props } from './Icon';
|
||||||
|
|
||||||
|
export default function NextIcon(props: Props) {
|
||||||
|
return (
|
||||||
|
<Icon {...props}>
|
||||||
|
<path
|
||||||
|
d="M9.29289322,16.2928932 C8.90236893,16.6834175 8.90236893,17.3165825 9.29289322,17.7071068 C9.68341751,18.0976311 10.3165825,18.0976311 10.7071068,17.7071068 L15.7071068,12.7071068 C16.0976311,12.3165825 16.0976311,11.6834175 15.7071068,11.2928932 L10.7071068,6.29289322 C10.3165825,5.90236893 9.68341751,5.90236893 9.29289322,6.29289322 C8.90236893,6.68341751 8.90236893,7.31658249 9.29289322,7.70710678 L13.5857864,12 L9.29289322,16.2928932 Z"
|
||||||
|
id="path-1"
|
||||||
|
/>
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -182,27 +182,27 @@ const DocumentLink = observer(
|
|||||||
>
|
>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
to={document.url}
|
to={document.url}
|
||||||
hasChildren={document.children.length > 0}
|
expand={showChildren}
|
||||||
expanded={showChildren}
|
expandedContent={
|
||||||
|
document.children.length
|
||||||
|
? <Children column>
|
||||||
|
{document.children.map(childDocument => (
|
||||||
|
<DocumentLink
|
||||||
|
key={childDocument.id}
|
||||||
|
history={history}
|
||||||
|
document={childDocument}
|
||||||
|
activeDocument={activeDocument}
|
||||||
|
prefetchDocument={prefetchDocument}
|
||||||
|
depth={depth + 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Children>
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{document.title}
|
{document.title}
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
</DropToImport>
|
</DropToImport>
|
||||||
|
|
||||||
{showChildren &&
|
|
||||||
<Children column>
|
|
||||||
{document.children &&
|
|
||||||
document.children.map(childDocument => (
|
|
||||||
<DocumentLink
|
|
||||||
key={childDocument.id}
|
|
||||||
history={history}
|
|
||||||
document={childDocument}
|
|
||||||
activeDocument={activeDocument}
|
|
||||||
prefetchDocument={prefetchDocument}
|
|
||||||
depth={depth + 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Children>}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { observable, action } from 'mobx';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { color, fontWeight } from 'styles/constants';
|
import { color, fontWeight } from 'styles/constants';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -54,22 +56,54 @@ type Props = {
|
|||||||
onClick?: SyntheticEvent => *,
|
onClick?: SyntheticEvent => *,
|
||||||
children?: React$Element<*>,
|
children?: React$Element<*>,
|
||||||
icon?: React$Element<*>,
|
icon?: React$Element<*>,
|
||||||
hasChildren?: boolean,
|
expand?: boolean,
|
||||||
expanded?: boolean,
|
expandedContent?: React$Element<*>,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SidebarLink({ icon, children, expanded, ...rest }: Props) {
|
@observer class SidebarLink extends Component {
|
||||||
const Component = styleComponent(rest.to ? NavLink : StyleableDiv);
|
props: Props;
|
||||||
|
|
||||||
return (
|
componentDidMount() {
|
||||||
<Flex>
|
if (this.props.expand) this.handleExpand();
|
||||||
<Component exact activeStyle={activeStyle} {...rest}>
|
}
|
||||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
|
||||||
{rest.hasChildren && <StyledGoTo expanded={expanded} />}
|
componentDidReceiveProps(nextProps: Props) {
|
||||||
<Content>{children}</Content>
|
if (nextProps.expand) this.handleExpand();
|
||||||
</Component>
|
}
|
||||||
</Flex>
|
|
||||||
);
|
@observable expanded: boolean = false;
|
||||||
|
|
||||||
|
@action handleClick = (event: SyntheticEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.expanded = !this.expanded;
|
||||||
|
};
|
||||||
|
|
||||||
|
@action handleExpand = () => {
|
||||||
|
this.expanded = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { icon, children, expandedContent, ...rest } = this.props;
|
||||||
|
const Component = styleComponent(rest.to ? NavLink : StyleableDiv);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex column>
|
||||||
|
<Component
|
||||||
|
exact
|
||||||
|
activeStyle={activeStyle}
|
||||||
|
hasChildren={expandedContent}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||||
|
{expandedContent &&
|
||||||
|
<StyledGoTo expanded={this.expanded} onClick={this.handleClick} />}
|
||||||
|
<Content onClick={this.handleExpand}>{children}</Content>
|
||||||
|
</Component>
|
||||||
|
{this.expanded && expandedContent}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = styled.div`
|
const Content = styled.div`
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import 'normalize.css/normalize.css';
|
|||||||
import 'styles/base.css';
|
import 'styles/base.css';
|
||||||
import 'styles/fonts.css';
|
import 'styles/fonts.css';
|
||||||
import 'styles/transitions.css';
|
import 'styles/transitions.css';
|
||||||
import 'styles/prism-tomorrow.css';
|
import 'styles/prism.css';
|
||||||
import 'styles/hljs-github-gist.css';
|
|
||||||
|
|
||||||
import Home from 'scenes/Home';
|
import Home from 'scenes/Home';
|
||||||
import Dashboard from 'scenes/Dashboard';
|
import Dashboard from 'scenes/Dashboard';
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import Document from 'models/Document';
|
|||||||
import DocumentMove from './components/DocumentMove';
|
import DocumentMove from './components/DocumentMove';
|
||||||
import UiStore from 'stores/UiStore';
|
import UiStore from 'stores/UiStore';
|
||||||
import DocumentsStore from 'stores/DocumentsStore';
|
import DocumentsStore from 'stores/DocumentsStore';
|
||||||
|
import CollectionsStore from 'stores/CollectionsStore';
|
||||||
import DocumentMenu from 'menus/DocumentMenu';
|
import DocumentMenu from 'menus/DocumentMenu';
|
||||||
import SaveAction from './components/SaveAction';
|
import SaveAction from './components/SaveAction';
|
||||||
import LoadingPlaceholder from 'components/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/LoadingPlaceholder';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import DropToImport from 'components/DropToImport';
|
|
||||||
import LoadingIndicator from 'components/LoadingIndicator';
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
import Collaborators from 'components/Collaborators';
|
import Collaborators from 'components/Collaborators';
|
||||||
import CenteredContent from 'components/CenteredContent';
|
import CenteredContent from 'components/CenteredContent';
|
||||||
@@ -45,6 +45,7 @@ type Props = {
|
|||||||
location: Object,
|
location: Object,
|
||||||
keydown: Object,
|
keydown: Object,
|
||||||
documents: DocumentsStore,
|
documents: DocumentsStore,
|
||||||
|
collections: CollectionsStore,
|
||||||
newDocument?: boolean,
|
newDocument?: boolean,
|
||||||
ui: UiStore,
|
ui: UiStore,
|
||||||
};
|
};
|
||||||
@@ -55,7 +56,6 @@ type Props = {
|
|||||||
|
|
||||||
@observable editCache: ?string;
|
@observable editCache: ?string;
|
||||||
@observable newDocument: ?Document;
|
@observable newDocument: ?Document;
|
||||||
@observable isDragging = false;
|
|
||||||
@observable isLoading = false;
|
@observable isLoading = false;
|
||||||
@observable isSaving = false;
|
@observable isSaving = false;
|
||||||
@observable notFound = false;
|
@observable notFound = false;
|
||||||
@@ -196,14 +196,6 @@ type Props = {
|
|||||||
this.props.history.push(url);
|
this.props.history.push(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
onStartDragging = () => {
|
|
||||||
this.isDragging = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
onStopDragging = () => {
|
|
||||||
this.isDragging = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
renderNotFound() {
|
renderNotFound() {
|
||||||
return <Search notFound />;
|
return <Search notFound />;
|
||||||
}
|
}
|
||||||
@@ -213,7 +205,9 @@ type Props = {
|
|||||||
const isMoving = this.props.match.path === matchDocumentMove;
|
const isMoving = this.props.match.path === matchDocumentMove;
|
||||||
const document = this.document;
|
const document = this.document;
|
||||||
const isFetching = !document;
|
const isFetching = !document;
|
||||||
const titleText = get(document, 'title', '');
|
const titleText =
|
||||||
|
get(document, 'title', '') ||
|
||||||
|
this.props.collections.titleForDocument(this.props.location.pathname);
|
||||||
|
|
||||||
if (this.notFound) {
|
if (this.notFound) {
|
||||||
return this.renderNotFound();
|
return this.renderNotFound();
|
||||||
@@ -222,11 +216,6 @@ type Props = {
|
|||||||
return (
|
return (
|
||||||
<Container column auto>
|
<Container column auto>
|
||||||
{isMoving && document && <DocumentMove document={document} />}
|
{isMoving && document && <DocumentMove document={document} />}
|
||||||
|
|
||||||
{this.isDragging &&
|
|
||||||
<DropHere align="center" justify="center">
|
|
||||||
Drop files here to import into Atlas.
|
|
||||||
</DropHere>}
|
|
||||||
{titleText && <PageTitle title={titleText} />}
|
{titleText && <PageTitle title={titleText} />}
|
||||||
{this.isLoading && <LoadingIndicator />}
|
{this.isLoading && <LoadingIndicator />}
|
||||||
{isFetching &&
|
{isFetching &&
|
||||||
@@ -235,73 +224,60 @@ type Props = {
|
|||||||
</CenteredContent>}
|
</CenteredContent>}
|
||||||
{!isFetching &&
|
{!isFetching &&
|
||||||
document &&
|
document &&
|
||||||
<StyledDropToImport
|
<Flex justify="center" auto>
|
||||||
documentId={document.id}
|
<Prompt
|
||||||
history={this.props.history}
|
when={document.hasPendingChanges}
|
||||||
onDragEnter={this.onStartDragging}
|
message={DISCARD_CHANGES}
|
||||||
onDragLeave={this.onStopDragging}
|
/>
|
||||||
onDrop={this.onStopDragging}
|
<Editor
|
||||||
disabled={this.isEditing}
|
key={document.id}
|
||||||
>
|
text={document.text}
|
||||||
<Flex justify="center" auto>
|
emoji={document.emoji}
|
||||||
<Prompt
|
onImageUploadStart={this.onImageUploadStart}
|
||||||
when={document.hasPendingChanges}
|
onImageUploadStop={this.onImageUploadStop}
|
||||||
message={DISCARD_CHANGES}
|
onChange={this.onChange}
|
||||||
/>
|
onSave={this.onSave}
|
||||||
<Editor
|
onCancel={this.onDiscard}
|
||||||
key={document.id}
|
readOnly={!this.isEditing}
|
||||||
text={document.text}
|
/>
|
||||||
emoji={document.emoji}
|
<Meta align="center" justify="flex-end" readOnly={!this.isEditing}>
|
||||||
onImageUploadStart={this.onImageUploadStart}
|
<Flex align="center">
|
||||||
onImageUploadStop={this.onImageUploadStop}
|
{!isNew &&
|
||||||
onChange={this.onChange}
|
!this.isEditing &&
|
||||||
onSave={this.onSave}
|
<Collaborators document={document} />}
|
||||||
onCancel={this.onDiscard}
|
<HeaderAction>
|
||||||
readOnly={!this.isEditing}
|
{this.isEditing
|
||||||
/>
|
? <SaveAction
|
||||||
<Meta
|
isSaving={this.isSaving}
|
||||||
align="center"
|
onClick={this.onSave.bind(this, true)}
|
||||||
justify="flex-end"
|
disabled={
|
||||||
readOnly={!this.isEditing}
|
!(this.document && this.document.allowSave) ||
|
||||||
>
|
this.isSaving
|
||||||
<Flex align="center">
|
}
|
||||||
{!isNew &&
|
isNew={!!isNew}
|
||||||
!this.isEditing &&
|
/>
|
||||||
<Collaborators document={document} />}
|
: <a onClick={this.onClickEdit}>
|
||||||
<HeaderAction>
|
Edit
|
||||||
{this.isEditing
|
|
||||||
? <SaveAction
|
|
||||||
isSaving={this.isSaving}
|
|
||||||
onClick={this.onSave.bind(this, true)}
|
|
||||||
disabled={
|
|
||||||
!(this.document && this.document.allowSave) ||
|
|
||||||
this.isSaving
|
|
||||||
}
|
|
||||||
isNew={!!isNew}
|
|
||||||
/>
|
|
||||||
: <a onClick={this.onClickEdit}>
|
|
||||||
Edit
|
|
||||||
</a>}
|
|
||||||
</HeaderAction>
|
|
||||||
{this.isEditing &&
|
|
||||||
<HeaderAction>
|
|
||||||
<a onClick={this.onDiscard}>Discard</a>
|
|
||||||
</HeaderAction>}
|
|
||||||
{!this.isEditing &&
|
|
||||||
<HeaderAction>
|
|
||||||
<DocumentMenu document={document} />
|
|
||||||
</HeaderAction>}
|
|
||||||
{!this.isEditing && <Separator />}
|
|
||||||
<HeaderAction>
|
|
||||||
{!this.isEditing &&
|
|
||||||
<a onClick={this.onClickNew}>
|
|
||||||
<NewDocumentIcon />
|
|
||||||
</a>}
|
</a>}
|
||||||
</HeaderAction>
|
</HeaderAction>
|
||||||
</Flex>
|
{this.isEditing &&
|
||||||
</Meta>
|
<HeaderAction>
|
||||||
</Flex>
|
<a onClick={this.onDiscard}>Discard</a>
|
||||||
</StyledDropToImport>}
|
</HeaderAction>}
|
||||||
|
{!this.isEditing &&
|
||||||
|
<HeaderAction>
|
||||||
|
<DocumentMenu document={document} />
|
||||||
|
</HeaderAction>}
|
||||||
|
{!this.isEditing && <Separator />}
|
||||||
|
<HeaderAction>
|
||||||
|
{!this.isEditing &&
|
||||||
|
<a onClick={this.onClickNew}>
|
||||||
|
<NewDocumentIcon />
|
||||||
|
</a>}
|
||||||
|
</HeaderAction>
|
||||||
|
</Flex>
|
||||||
|
</Meta>
|
||||||
|
</Flex>}
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,18 +301,6 @@ const HeaderAction = styled(Flex)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DropHere = styled(Flex)`
|
|
||||||
pointer-events: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: ${layout.sidebarWidth};
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
text-align: center;
|
|
||||||
background: rgba(255,255,255,.9);
|
|
||||||
z-index: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Meta = styled(Flex)`
|
const Meta = styled(Flex)`
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -357,9 +321,6 @@ const LoadingState = styled(LoadingPlaceholder)`
|
|||||||
margin: 90px 0;
|
margin: 90px 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDropToImport = styled(DropToImport)`
|
export default withRouter(
|
||||||
display: flex;
|
inject('ui', 'user', 'documents', 'collections')(DocumentScene)
|
||||||
flex: 1;
|
);
|
||||||
`;
|
|
||||||
|
|
||||||
export default withRouter(inject('ui', 'user', 'documents')(DocumentScene));
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Options = {
|
|||||||
type DocumentPathItem = {
|
type DocumentPathItem = {
|
||||||
id: string,
|
id: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
url: string,
|
||||||
type: 'document' | 'collection',
|
type: 'document' | 'collection',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,16 +60,16 @@ class CollectionsStore {
|
|||||||
let results = [];
|
let results = [];
|
||||||
const travelDocuments = (documentList, path) =>
|
const travelDocuments = (documentList, path) =>
|
||||||
documentList.forEach(document => {
|
documentList.forEach(document => {
|
||||||
const { id, title } = document;
|
const { id, title, url } = document;
|
||||||
const node = { id, title, type: 'document' };
|
const node = { id, title, url, type: 'document' };
|
||||||
results.push(_.concat(path, node));
|
results.push(_.concat(path, node));
|
||||||
travelDocuments(document.children, _.concat(path, [node]));
|
travelDocuments(document.children, _.concat(path, [node]));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isLoaded) {
|
if (this.isLoaded) {
|
||||||
this.data.forEach(collection => {
|
this.data.forEach(collection => {
|
||||||
const { id, name } = collection;
|
const { id, name, url } = collection;
|
||||||
const node = { id, title: name, type: 'collection' };
|
const node = { id, title: name, url, type: 'collection' };
|
||||||
results.push([node]);
|
results.push([node]);
|
||||||
travelDocuments(collection.documents, [node]);
|
travelDocuments(collection.documents, [node]);
|
||||||
});
|
});
|
||||||
@@ -87,6 +88,11 @@ class CollectionsStore {
|
|||||||
return this.pathsToDocuments.find(path => path.id === documentId);
|
return this.pathsToDocuments.find(path => path.id === documentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
titleForDocument(documentUrl: string): ?string {
|
||||||
|
const path = this.pathsToDocuments.find(path => path.url === documentUrl);
|
||||||
|
if (path) return path.title;
|
||||||
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
|
|
||||||
@action fetchAll = async (): Promise<*> => {
|
@action fetchAll = async (): Promise<*> => {
|
||||||
|
|||||||
@@ -101,11 +101,11 @@ samp {
|
|||||||
}
|
}
|
||||||
code,
|
code,
|
||||||
samp {
|
samp {
|
||||||
font-size: 87.5%;
|
font-size: 85%;
|
||||||
padding: 0.125em;
|
padding: 0.125em;
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
font-size: 87.5%;
|
font-size: 85%;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
blockquote {
|
blockquote {
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
/**
|
|
||||||
* GitHub Gist Theme
|
|
||||||
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
|
|
||||||
*/
|
|
||||||
|
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
background: white;
|
|
||||||
padding: 0.5em;
|
|
||||||
color: #333333;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-comment,
|
|
||||||
.hljs-meta {
|
|
||||||
color: #969896;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-string,
|
|
||||||
.hljs-variable,
|
|
||||||
.hljs-template-variable,
|
|
||||||
.hljs-strong,
|
|
||||||
.hljs-emphasis,
|
|
||||||
.hljs-quote {
|
|
||||||
color: #df5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-keyword,
|
|
||||||
.hljs-selector-tag,
|
|
||||||
.hljs-type {
|
|
||||||
color: #a71d5d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-literal,
|
|
||||||
.hljs-symbol,
|
|
||||||
.hljs-bullet,
|
|
||||||
.hljs-attribute {
|
|
||||||
color: #0086b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-section,
|
|
||||||
.hljs-name {
|
|
||||||
color: #63a35c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-tag {
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-title,
|
|
||||||
.hljs-attr,
|
|
||||||
.hljs-selector-id,
|
|
||||||
.hljs-selector-class,
|
|
||||||
.hljs-selector-attr,
|
|
||||||
.hljs-selector-pseudo {
|
|
||||||
color: #795da3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-addition {
|
|
||||||
color: #55a532;
|
|
||||||
background-color: #eaffea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-deletion {
|
|
||||||
color: #bd2c00;
|
|
||||||
background-color: #ffecec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hljs-link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
|
||||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
|
||||||
* @author Rose Pritchard
|
|
||||||
*/
|
|
||||||
|
|
||||||
code[class*='language-'],
|
|
||||||
pre[class*='language-'] {
|
|
||||||
color: #ccc;
|
|
||||||
background: none;
|
|
||||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*='language-'] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code[class*='language-'],
|
|
||||||
pre[class*='language-'] {
|
|
||||||
background: #2d2d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre) > code[class*='language-'] {
|
|
||||||
padding: 0.1em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.block-comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.tag,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.namespace,
|
|
||||||
.token.deleted {
|
|
||||||
color: #e2777a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.function-name {
|
|
||||||
color: #6196cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.boolean,
|
|
||||||
.token.number,
|
|
||||||
.token.function {
|
|
||||||
color: #f08d49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.class-name,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol {
|
|
||||||
color: #f8c555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.important,
|
|
||||||
.token.atrule,
|
|
||||||
.token.keyword,
|
|
||||||
.token.builtin {
|
|
||||||
color: #cc99cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.regex,
|
|
||||||
.token.variable {
|
|
||||||
color: #7ec699;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url {
|
|
||||||
color: #67cdcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.inserted {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
141
frontend/styles/prism.css
Normal file
141
frontend/styles/prism.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
Based on Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/)
|
||||||
|
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||||
|
|
||||||
|
*/
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
-webkit-font-smoothing: initial;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.375;
|
||||||
|
direction: ltr;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
color: #24292e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #6a737d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #5e6687;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property {
|
||||||
|
color: #c08b30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag {
|
||||||
|
color: #3d8fd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.string {
|
||||||
|
color: #032f62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector {
|
||||||
|
color: #6679cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-name {
|
||||||
|
color: #c76b29;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #22a2c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword,
|
||||||
|
.token.control,
|
||||||
|
.token.directive,
|
||||||
|
.token.unit {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function {
|
||||||
|
color: #6f42c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.statement,
|
||||||
|
.token.regex,
|
||||||
|
.token.atrule {
|
||||||
|
color: #22a2c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.placeholder,
|
||||||
|
.token.variable {
|
||||||
|
color: #3d8fd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.deleted {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.inserted {
|
||||||
|
border-bottom: 1px dotted #202746;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important {
|
||||||
|
color: #c94922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code.highlight {
|
||||||
|
outline: 0.4em solid #c94922;
|
||||||
|
outline-offset: .4em;
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
|||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
ctx.cache.set(document.id, document);
|
ctx.cache.set(document.id, document);
|
||||||
|
|
||||||
|
// For empty document content, return the title
|
||||||
|
if (document.text.trim().length === 0)
|
||||||
|
document.text = `# ${document.title || 'Untitled document'}`;
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id: document.id,
|
id: document.id,
|
||||||
url: document.getUrl(),
|
url: document.getUrl(),
|
||||||
|
|||||||
Reference in New Issue
Block a user